vkit.mechanism.painter

  1# Copyright 2022 vkit-x Administrator. All Rights Reserved.
  2#
  3# This project (vkit-x/vkit) is dual-licensed under commercial and SSPL licenses.
  4#
  5# The commercial license gives you the full rights to create and distribute software
  6# on your own terms without any SSPL license obligations. For more information,
  7# please see the "LICENSE_COMMERCIAL.txt" file.
  8#
  9# This project is also available under Server Side Public License (SSPL).
 10# The SSPL licensing is ideal for use cases such as open source projects with
 11# SSPL distribution, student/academic purposes, hobby projects, internal research
 12# projects without external distribution, or other projects where all SSPL
 13# obligations can be met. For more information, please see the "LICENSE_SSPL.txt" file.
 14from typing import cast, Union, Tuple, Sequence, Iterable, Any, Optional
 15
 16import cv2 as cv
 17import numpy as np
 18from PIL import ImageColor as PilImageColor
 19
 20from vkit.utility import PathType
 21from vkit.element import (
 22    Shapable,
 23    Point,
 24    PointList,
 25    Line,
 26    Box,
 27    Polygon,
 28    Mask,
 29    ScoreMap,
 30    Image,
 31    ImageMode,
 32)
 33
 34
 35class Painter:
 36
 37    # https://mokole.com/palette.html
 38    PALETTE = (
 39        # darkgreen
 40        '#006400',
 41        # darkblue
 42        '#00008b',
 43        # maroon3
 44        '#b03060',
 45        # red
 46        '#ff0000',
 47        # yellow
 48        '#ffff00',
 49        # burlywood
 50        '#deb887',
 51        # lime
 52        '#00ff00',
 53        # aqua
 54        '#00ffff',
 55        # fuchsia
 56        '#ff00ff',
 57        # cornflower
 58        '#6495ed',
 59    )
 60
 61    @classmethod
 62    def get_rgb_tuple_from_color_name(cls, color_name: str) -> Tuple[int, int, int]:
 63        # https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#color-names
 64        return PilImageColor.getrgb(color_name)  # type: ignore
 65
 66    @classmethod
 67    def get_complementary_rgba_tuple(
 68        cls, rgba_tuple: Tuple[int, int, int, int]
 69    ) -> Tuple[int, int, int, int]:
 70        return tuple(255 - val if idx < 3 else val for idx, val in enumerate(rgba_tuple))
 71
 72    @classmethod
 73    def get_color_names(
 74        cls,
 75        elements_or_num_elements: Union[Iterable[Any], int],
 76        palette: Sequence[str] = PALETTE,
 77    ):
 78        if isinstance(elements_or_num_elements, int):
 79            elements = range(elements_or_num_elements)
 80        else:
 81            elements = elements_or_num_elements
 82        return tuple(palette[idx % len(palette)] for idx, _ in enumerate(elements))
 83
 84    @classmethod
 85    def get_rgb_tuples(
 86        cls,
 87        elements_or_num_elements: Union[Iterable[Any], int],
 88        palette: Sequence[str] = PALETTE,
 89    ):
 90        color_names = cls.get_color_names(elements_or_num_elements, palette=palette)
 91        return tuple(cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names)
 92
 93    @classmethod
 94    def get_rgba_tuples_from_color_names(
 95        cls,
 96        num_elements: int,
 97        color: Optional[Union[str, Iterable[str], Iterable[int]]],
 98        alpha: float,
 99        palette: Sequence[str] = PALETTE,
100    ):
101        if color is None:
102            rgb_tuples = cls.get_rgb_tuples(num_elements, palette=palette)
103
104        elif isinstance(color, str):
105            rgb_tuple = cls.get_rgb_tuple_from_color_name(color)
106            rgb_tuples = (rgb_tuple,) * num_elements
107
108        else:
109            colors = tuple(color)
110            if not colors:
111                color_names = ()
112
113            elif isinstance(colors[0], str):
114                color_names = cast(Tuple[str], colors)
115
116            elif isinstance(colors[0], int):
117                color_indices = cast(Tuple[int], colors)
118                color_names = [palette[color_idx % len(palette)] for color_idx in color_indices]
119
120            else:
121                raise NotImplementedError()
122
123            rgb_tuples = tuple(
124                cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names
125            )
126
127        alpha_uint8 = round(255 * alpha)
128        return tuple((*rgb_tuple, alpha_uint8) for rgb_tuple in rgb_tuples)
129
130    @classmethod
131    def create(
132        cls,
133        image_or_shapable_or_shape: Union[Image, Shapable, Tuple[int, int]],
134        num_channels: int = 3,
135        value: Union[Tuple[int, ...], int] = 255,
136    ):
137        if isinstance(image_or_shapable_or_shape, Image):
138            image = image_or_shapable_or_shape
139            image = image.copy()
140
141        else:
142            if isinstance(image_or_shapable_or_shape, Shapable):
143                shape = image_or_shapable_or_shape.shape
144            else:
145                shape = image_or_shapable_or_shape
146
147            image = Image.from_shape(
148                shape=shape,
149                num_channels=num_channels,
150                value=value,
151            )
152
153        return Painter(image)
154
155    def __init__(self, image: Image):
156        self.image = image.copy()
157
158    def copy(self):
159        return Painter(image=self.image.copy())
160
161    def generate_layer_image(self):
162        # RGBA.
163        return Image.from_shapable(
164            self.image,
165            num_channels=4,
166            value=0,
167        )
168
169    def overlay_layer_image(self, layer_image: Image):
170        alpha = layer_image.mat[:, :, 3].astype(np.float32) / 255.0
171
172        layer_image = Image(mat=layer_image.mat[:, :, :3], mode=ImageMode.RGB)
173        layer_image = layer_image.to_target_mode_image(self.image.mode)
174
175        Box.from_shapable(layer_image).fill_image(
176            self.image,
177            value=layer_image,
178            alpha=alpha,
179        )
180
181    @classmethod
182    def paint_text_to_layer_image(
183        cls,
184        layer_image: Image,
185        text: str,
186        point: Point,
187        rgba_tuple: Tuple[int, int, int, int],
188        font_scale: float = 1.0,
189        enable_complementary_rgba: bool = True,
190    ):
191        if enable_complementary_rgba:
192            color = cls.get_complementary_rgba_tuple(rgba_tuple)
193        else:
194            color = rgba_tuple
195
196        cv.putText(
197            layer_image.mat,
198            text=text,
199            org=(point.x, point.y),
200            fontFace=cv.FONT_HERSHEY_SIMPLEX,
201            fontScale=font_scale,
202            color=color,
203            lineType=cv.LINE_AA,
204        )
205
206    def paint_texts(
207        self,
208        texts: Iterable[str],
209        points: Iterable[Point],
210        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
211        alpha: float = 0.5,
212        palette: Sequence[str] = PALETTE,
213    ):
214        layer_image = self.generate_layer_image()
215
216        texts = tuple(texts)
217        points = tuple(points)
218        assert len(texts) == len(points)
219        rgba_tuples = self.get_rgba_tuples_from_color_names(
220            len(points),
221            color,
222            alpha,
223            palette=palette,
224        )
225        for text, point, rgba_tuple in zip(texts, points, rgba_tuples):
226            self.paint_text_to_layer_image(
227                layer_image=layer_image,
228                text=text,
229                point=point,
230                rgba_tuple=rgba_tuple,
231            )
232
233        self.overlay_layer_image(layer_image)
234
235    def paint_points(
236        self,
237        points: Union[PointList, Iterable[Point]],
238        radius: int = 1,
239        enable_index: bool = False,
240        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
241        alpha: float = 0.5,
242        palette: Sequence[str] = PALETTE,
243    ):
244        layer_image = self.generate_layer_image()
245
246        if not isinstance(points, PointList):
247            points = PointList(points)
248
249        rgba_tuples = self.get_rgba_tuples_from_color_names(
250            len(points),
251            color,
252            alpha,
253            palette=palette,
254        )
255        for idx, (point, rgba_tuple) in enumerate(zip(points, rgba_tuples)):
256            cv.circle(
257                layer_image.mat,
258                center=(point.x, point.y),
259                radius=radius,
260                color=rgba_tuple,
261                thickness=cv.FILLED,
262                lineType=cv.LINE_AA,
263            )
264
265            if enable_index:
266                self.paint_text_to_layer_image(
267                    layer_image=layer_image,
268                    text=str(idx),
269                    point=point,
270                    rgba_tuple=rgba_tuple,
271                )
272
273        self.overlay_layer_image(layer_image)
274
275    def paint_lines(
276        self,
277        lines: Iterable[Line],
278        thickness: int = 1,
279        enable_arrow: bool = False,
280        arrow_length_ratio: float = 0.1,
281        enable_index: bool = False,
282        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
283        alpha: float = 0.5,
284        palette: Sequence[str] = PALETTE,
285    ):
286        layer_image = self.generate_layer_image()
287
288        lines = tuple(lines)
289        rgba_tuples = self.get_rgba_tuples_from_color_names(
290            len(lines),
291            color,
292            alpha,
293            palette=palette,
294        )
295        for idx, (line, rgba_tuple) in enumerate(zip(lines, rgba_tuples)):
296            if not enable_arrow:
297                cv.line(
298                    layer_image.mat,
299                    pt1=(line.point_begin.x, line.point_begin.y),
300                    pt2=(line.point_end.x, line.point_end.y),
301                    color=rgba_tuple,
302                    thickness=thickness,
303                    lineType=cv.LINE_AA,
304                )
305            else:
306                cv.arrowedLine(
307                    layer_image.mat,
308                    pt1=(line.point_begin.x, line.point_begin.y),
309                    pt2=(line.point_end.x, line.point_end.y),
310                    color=rgba_tuple,
311                    thickness=thickness,
312                    line_type=cv.LINE_AA,
313                    tipLength=arrow_length_ratio,
314                )
315
316            if enable_index:
317                center_point = line.get_center_point()
318                self.paint_text_to_layer_image(
319                    layer_image=layer_image,
320                    text=str(idx),
321                    point=center_point,
322                    rgba_tuple=rgba_tuple,
323                )
324
325        self.overlay_layer_image(layer_image)
326
327    def paint_boxes(
328        self,
329        boxes: Iterable[Box],
330        enable_index: bool = False,
331        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
332        border_thickness: Optional[int] = None,
333        alpha: float = 0.5,
334        palette: Sequence[str] = PALETTE,
335    ):
336        layer_image = self.generate_layer_image()
337
338        boxes = tuple(boxes)
339        rgba_tuples = self.get_rgba_tuples_from_color_names(
340            len(boxes),
341            color,
342            alpha,
343            palette=palette,
344        )
345
346        for idx, (box, rgba_tuple) in enumerate(zip(boxes, rgba_tuples)):
347            box.fill_image(image=layer_image, value=rgba_tuple)
348            if border_thickness:
349                inner_box = Box(
350                    up=box.up + border_thickness,
351                    down=box.down - border_thickness,
352                    left=box.left + border_thickness,
353                    right=box.right - border_thickness,
354                )
355                if inner_box.valid:
356                    inner_box.fill_image(image=layer_image, value=0)
357
358            if enable_index:
359                center_point = box.get_center_point()
360                self.paint_text_to_layer_image(
361                    layer_image=layer_image,
362                    text=str(idx),
363                    point=center_point,
364                    rgba_tuple=rgba_tuple,
365                    enable_complementary_rgba=(border_thickness is None),
366                )
367
368        self.overlay_layer_image(layer_image)
369
370    def paint_polygons(
371        self,
372        polygons: Iterable[Polygon],
373        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
374        alpha: float = 0.5,
375        palette: Sequence[str] = PALETTE,
376        enable_index: bool = False,
377        enable_polygon_points: bool = False,
378        polygon_points_color: str = 'red',
379        polygon_points_alpha: float = 1.0,
380    ):
381        layer_image = self.generate_layer_image()
382
383        polygons = tuple(polygons)
384        rgba_tuples = self.get_rgba_tuples_from_color_names(
385            len(polygons),
386            color,
387            alpha,
388            palette=palette,
389        )
390        for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
391            polygon.fill_image(image=layer_image, value=rgba_tuple)
392
393        if enable_index:
394            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
395                center_point = polygon.get_center_point()
396                self.paint_text_to_layer_image(
397                    layer_image=layer_image,
398                    text=str(idx),
399                    point=center_point,
400                    rgba_tuple=rgba_tuple,
401                )
402
403        if enable_polygon_points:
404            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
405                self.paint_points(
406                    polygon.points,
407                    color=polygon_points_color,
408                    alpha=polygon_points_alpha,
409                )
410
411        self.overlay_layer_image(layer_image)
412
413    def paint_mask(
414        self,
415        mask: Mask,
416        color: str = 'red',
417        alpha: float = 0.5,
418    ):
419        layer_image = self.generate_layer_image()
420
421        rgb_tuple = self.get_rgb_tuple_from_color_name(color)
422        alpha_uint8 = round(255 * alpha)
423        mask.fill_image(image=layer_image, value=(*rgb_tuple, alpha_uint8))
424
425        self.overlay_layer_image(layer_image)
426
427    def paint_masks(
428        self,
429        masks: Iterable[Mask],
430        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
431        alpha: float = 0.5,
432        palette: Sequence[str] = PALETTE,
433    ):
434        masks = tuple(masks)
435        rgba_tuples = self.get_rgba_tuples_from_color_names(
436            len(masks),
437            color,
438            alpha,
439            palette=palette,
440        )
441
442        layer_image = self.generate_layer_image()
443        layer_image.fill_by_mask_value_tuples(zip(masks, rgba_tuples))
444        self.overlay_layer_image(layer_image)
445
446    def paint_score_map(
447        self,
448        score_map: ScoreMap,
449        enable_boundary_equalization: bool = False,
450        enable_center_shift: bool = False,
451        cv_colormap: int = cv.COLORMAP_JET,
452        alpha: float = 0.5,
453    ):
454        layer_image = self.generate_layer_image()
455
456        mat = score_map.mat.copy()
457
458        if score_map.is_prob:
459            mat *= 255.0
460
461        if enable_boundary_equalization:
462            # Equalize to [0, 255]
463            val_min = np.min(mat)
464            mat -= val_min
465            val_max = np.max(mat)
466            mat *= 255.0
467            mat /= val_max
468
469        elif enable_center_shift:
470            mat *= 127.5
471
472        mat = np.clip(mat, 0, 255).astype(np.uint8)
473
474        # Apply color map.
475        color_mat = cv.applyColorMap(mat, cv_colormap)
476        color_mat = cv.cvtColor(color_mat, cv.COLOR_BGR2RGB)
477
478        # Add alpha channel.
479        alpha_uint8 = round(255 * alpha)
480        color_mat = np.dstack((
481            color_mat,
482            np.full(color_mat.shape[:2], alpha_uint8, dtype=np.uint8),
483        ))
484
485        if score_map.box:
486            score_map.box.fill_image(layer_image, color_mat)
487        else:
488            layer_image.assign_mat(color_mat)
489
490        self.overlay_layer_image(layer_image)
491
492    def to_file(self, path: PathType, disable_to_rgb_image: bool = False):
493        self.image.to_file(path=path, disable_to_rgb_image=disable_to_rgb_image)
class Painter:
 36class Painter:
 37
 38    # https://mokole.com/palette.html
 39    PALETTE = (
 40        # darkgreen
 41        '#006400',
 42        # darkblue
 43        '#00008b',
 44        # maroon3
 45        '#b03060',
 46        # red
 47        '#ff0000',
 48        # yellow
 49        '#ffff00',
 50        # burlywood
 51        '#deb887',
 52        # lime
 53        '#00ff00',
 54        # aqua
 55        '#00ffff',
 56        # fuchsia
 57        '#ff00ff',
 58        # cornflower
 59        '#6495ed',
 60    )
 61
 62    @classmethod
 63    def get_rgb_tuple_from_color_name(cls, color_name: str) -> Tuple[int, int, int]:
 64        # https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#color-names
 65        return PilImageColor.getrgb(color_name)  # type: ignore
 66
 67    @classmethod
 68    def get_complementary_rgba_tuple(
 69        cls, rgba_tuple: Tuple[int, int, int, int]
 70    ) -> Tuple[int, int, int, int]:
 71        return tuple(255 - val if idx < 3 else val for idx, val in enumerate(rgba_tuple))
 72
 73    @classmethod
 74    def get_color_names(
 75        cls,
 76        elements_or_num_elements: Union[Iterable[Any], int],
 77        palette: Sequence[str] = PALETTE,
 78    ):
 79        if isinstance(elements_or_num_elements, int):
 80            elements = range(elements_or_num_elements)
 81        else:
 82            elements = elements_or_num_elements
 83        return tuple(palette[idx % len(palette)] for idx, _ in enumerate(elements))
 84
 85    @classmethod
 86    def get_rgb_tuples(
 87        cls,
 88        elements_or_num_elements: Union[Iterable[Any], int],
 89        palette: Sequence[str] = PALETTE,
 90    ):
 91        color_names = cls.get_color_names(elements_or_num_elements, palette=palette)
 92        return tuple(cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names)
 93
 94    @classmethod
 95    def get_rgba_tuples_from_color_names(
 96        cls,
 97        num_elements: int,
 98        color: Optional[Union[str, Iterable[str], Iterable[int]]],
 99        alpha: float,
100        palette: Sequence[str] = PALETTE,
101    ):
102        if color is None:
103            rgb_tuples = cls.get_rgb_tuples(num_elements, palette=palette)
104
105        elif isinstance(color, str):
106            rgb_tuple = cls.get_rgb_tuple_from_color_name(color)
107            rgb_tuples = (rgb_tuple,) * num_elements
108
109        else:
110            colors = tuple(color)
111            if not colors:
112                color_names = ()
113
114            elif isinstance(colors[0], str):
115                color_names = cast(Tuple[str], colors)
116
117            elif isinstance(colors[0], int):
118                color_indices = cast(Tuple[int], colors)
119                color_names = [palette[color_idx % len(palette)] for color_idx in color_indices]
120
121            else:
122                raise NotImplementedError()
123
124            rgb_tuples = tuple(
125                cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names
126            )
127
128        alpha_uint8 = round(255 * alpha)
129        return tuple((*rgb_tuple, alpha_uint8) for rgb_tuple in rgb_tuples)
130
131    @classmethod
132    def create(
133        cls,
134        image_or_shapable_or_shape: Union[Image, Shapable, Tuple[int, int]],
135        num_channels: int = 3,
136        value: Union[Tuple[int, ...], int] = 255,
137    ):
138        if isinstance(image_or_shapable_or_shape, Image):
139            image = image_or_shapable_or_shape
140            image = image.copy()
141
142        else:
143            if isinstance(image_or_shapable_or_shape, Shapable):
144                shape = image_or_shapable_or_shape.shape
145            else:
146                shape = image_or_shapable_or_shape
147
148            image = Image.from_shape(
149                shape=shape,
150                num_channels=num_channels,
151                value=value,
152            )
153
154        return Painter(image)
155
156    def __init__(self, image: Image):
157        self.image = image.copy()
158
159    def copy(self):
160        return Painter(image=self.image.copy())
161
162    def generate_layer_image(self):
163        # RGBA.
164        return Image.from_shapable(
165            self.image,
166            num_channels=4,
167            value=0,
168        )
169
170    def overlay_layer_image(self, layer_image: Image):
171        alpha = layer_image.mat[:, :, 3].astype(np.float32) / 255.0
172
173        layer_image = Image(mat=layer_image.mat[:, :, :3], mode=ImageMode.RGB)
174        layer_image = layer_image.to_target_mode_image(self.image.mode)
175
176        Box.from_shapable(layer_image).fill_image(
177            self.image,
178            value=layer_image,
179            alpha=alpha,
180        )
181
182    @classmethod
183    def paint_text_to_layer_image(
184        cls,
185        layer_image: Image,
186        text: str,
187        point: Point,
188        rgba_tuple: Tuple[int, int, int, int],
189        font_scale: float = 1.0,
190        enable_complementary_rgba: bool = True,
191    ):
192        if enable_complementary_rgba:
193            color = cls.get_complementary_rgba_tuple(rgba_tuple)
194        else:
195            color = rgba_tuple
196
197        cv.putText(
198            layer_image.mat,
199            text=text,
200            org=(point.x, point.y),
201            fontFace=cv.FONT_HERSHEY_SIMPLEX,
202            fontScale=font_scale,
203            color=color,
204            lineType=cv.LINE_AA,
205        )
206
207    def paint_texts(
208        self,
209        texts: Iterable[str],
210        points: Iterable[Point],
211        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
212        alpha: float = 0.5,
213        palette: Sequence[str] = PALETTE,
214    ):
215        layer_image = self.generate_layer_image()
216
217        texts = tuple(texts)
218        points = tuple(points)
219        assert len(texts) == len(points)
220        rgba_tuples = self.get_rgba_tuples_from_color_names(
221            len(points),
222            color,
223            alpha,
224            palette=palette,
225        )
226        for text, point, rgba_tuple in zip(texts, points, rgba_tuples):
227            self.paint_text_to_layer_image(
228                layer_image=layer_image,
229                text=text,
230                point=point,
231                rgba_tuple=rgba_tuple,
232            )
233
234        self.overlay_layer_image(layer_image)
235
236    def paint_points(
237        self,
238        points: Union[PointList, Iterable[Point]],
239        radius: int = 1,
240        enable_index: bool = False,
241        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
242        alpha: float = 0.5,
243        palette: Sequence[str] = PALETTE,
244    ):
245        layer_image = self.generate_layer_image()
246
247        if not isinstance(points, PointList):
248            points = PointList(points)
249
250        rgba_tuples = self.get_rgba_tuples_from_color_names(
251            len(points),
252            color,
253            alpha,
254            palette=palette,
255        )
256        for idx, (point, rgba_tuple) in enumerate(zip(points, rgba_tuples)):
257            cv.circle(
258                layer_image.mat,
259                center=(point.x, point.y),
260                radius=radius,
261                color=rgba_tuple,
262                thickness=cv.FILLED,
263                lineType=cv.LINE_AA,
264            )
265
266            if enable_index:
267                self.paint_text_to_layer_image(
268                    layer_image=layer_image,
269                    text=str(idx),
270                    point=point,
271                    rgba_tuple=rgba_tuple,
272                )
273
274        self.overlay_layer_image(layer_image)
275
276    def paint_lines(
277        self,
278        lines: Iterable[Line],
279        thickness: int = 1,
280        enable_arrow: bool = False,
281        arrow_length_ratio: float = 0.1,
282        enable_index: bool = False,
283        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
284        alpha: float = 0.5,
285        palette: Sequence[str] = PALETTE,
286    ):
287        layer_image = self.generate_layer_image()
288
289        lines = tuple(lines)
290        rgba_tuples = self.get_rgba_tuples_from_color_names(
291            len(lines),
292            color,
293            alpha,
294            palette=palette,
295        )
296        for idx, (line, rgba_tuple) in enumerate(zip(lines, rgba_tuples)):
297            if not enable_arrow:
298                cv.line(
299                    layer_image.mat,
300                    pt1=(line.point_begin.x, line.point_begin.y),
301                    pt2=(line.point_end.x, line.point_end.y),
302                    color=rgba_tuple,
303                    thickness=thickness,
304                    lineType=cv.LINE_AA,
305                )
306            else:
307                cv.arrowedLine(
308                    layer_image.mat,
309                    pt1=(line.point_begin.x, line.point_begin.y),
310                    pt2=(line.point_end.x, line.point_end.y),
311                    color=rgba_tuple,
312                    thickness=thickness,
313                    line_type=cv.LINE_AA,
314                    tipLength=arrow_length_ratio,
315                )
316
317            if enable_index:
318                center_point = line.get_center_point()
319                self.paint_text_to_layer_image(
320                    layer_image=layer_image,
321                    text=str(idx),
322                    point=center_point,
323                    rgba_tuple=rgba_tuple,
324                )
325
326        self.overlay_layer_image(layer_image)
327
328    def paint_boxes(
329        self,
330        boxes: Iterable[Box],
331        enable_index: bool = False,
332        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
333        border_thickness: Optional[int] = None,
334        alpha: float = 0.5,
335        palette: Sequence[str] = PALETTE,
336    ):
337        layer_image = self.generate_layer_image()
338
339        boxes = tuple(boxes)
340        rgba_tuples = self.get_rgba_tuples_from_color_names(
341            len(boxes),
342            color,
343            alpha,
344            palette=palette,
345        )
346
347        for idx, (box, rgba_tuple) in enumerate(zip(boxes, rgba_tuples)):
348            box.fill_image(image=layer_image, value=rgba_tuple)
349            if border_thickness:
350                inner_box = Box(
351                    up=box.up + border_thickness,
352                    down=box.down - border_thickness,
353                    left=box.left + border_thickness,
354                    right=box.right - border_thickness,
355                )
356                if inner_box.valid:
357                    inner_box.fill_image(image=layer_image, value=0)
358
359            if enable_index:
360                center_point = box.get_center_point()
361                self.paint_text_to_layer_image(
362                    layer_image=layer_image,
363                    text=str(idx),
364                    point=center_point,
365                    rgba_tuple=rgba_tuple,
366                    enable_complementary_rgba=(border_thickness is None),
367                )
368
369        self.overlay_layer_image(layer_image)
370
371    def paint_polygons(
372        self,
373        polygons: Iterable[Polygon],
374        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
375        alpha: float = 0.5,
376        palette: Sequence[str] = PALETTE,
377        enable_index: bool = False,
378        enable_polygon_points: bool = False,
379        polygon_points_color: str = 'red',
380        polygon_points_alpha: float = 1.0,
381    ):
382        layer_image = self.generate_layer_image()
383
384        polygons = tuple(polygons)
385        rgba_tuples = self.get_rgba_tuples_from_color_names(
386            len(polygons),
387            color,
388            alpha,
389            palette=palette,
390        )
391        for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
392            polygon.fill_image(image=layer_image, value=rgba_tuple)
393
394        if enable_index:
395            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
396                center_point = polygon.get_center_point()
397                self.paint_text_to_layer_image(
398                    layer_image=layer_image,
399                    text=str(idx),
400                    point=center_point,
401                    rgba_tuple=rgba_tuple,
402                )
403
404        if enable_polygon_points:
405            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
406                self.paint_points(
407                    polygon.points,
408                    color=polygon_points_color,
409                    alpha=polygon_points_alpha,
410                )
411
412        self.overlay_layer_image(layer_image)
413
414    def paint_mask(
415        self,
416        mask: Mask,
417        color: str = 'red',
418        alpha: float = 0.5,
419    ):
420        layer_image = self.generate_layer_image()
421
422        rgb_tuple = self.get_rgb_tuple_from_color_name(color)
423        alpha_uint8 = round(255 * alpha)
424        mask.fill_image(image=layer_image, value=(*rgb_tuple, alpha_uint8))
425
426        self.overlay_layer_image(layer_image)
427
428    def paint_masks(
429        self,
430        masks: Iterable[Mask],
431        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
432        alpha: float = 0.5,
433        palette: Sequence[str] = PALETTE,
434    ):
435        masks = tuple(masks)
436        rgba_tuples = self.get_rgba_tuples_from_color_names(
437            len(masks),
438            color,
439            alpha,
440            palette=palette,
441        )
442
443        layer_image = self.generate_layer_image()
444        layer_image.fill_by_mask_value_tuples(zip(masks, rgba_tuples))
445        self.overlay_layer_image(layer_image)
446
447    def paint_score_map(
448        self,
449        score_map: ScoreMap,
450        enable_boundary_equalization: bool = False,
451        enable_center_shift: bool = False,
452        cv_colormap: int = cv.COLORMAP_JET,
453        alpha: float = 0.5,
454    ):
455        layer_image = self.generate_layer_image()
456
457        mat = score_map.mat.copy()
458
459        if score_map.is_prob:
460            mat *= 255.0
461
462        if enable_boundary_equalization:
463            # Equalize to [0, 255]
464            val_min = np.min(mat)
465            mat -= val_min
466            val_max = np.max(mat)
467            mat *= 255.0
468            mat /= val_max
469
470        elif enable_center_shift:
471            mat *= 127.5
472
473        mat = np.clip(mat, 0, 255).astype(np.uint8)
474
475        # Apply color map.
476        color_mat = cv.applyColorMap(mat, cv_colormap)
477        color_mat = cv.cvtColor(color_mat, cv.COLOR_BGR2RGB)
478
479        # Add alpha channel.
480        alpha_uint8 = round(255 * alpha)
481        color_mat = np.dstack((
482            color_mat,
483            np.full(color_mat.shape[:2], alpha_uint8, dtype=np.uint8),
484        ))
485
486        if score_map.box:
487            score_map.box.fill_image(layer_image, color_mat)
488        else:
489            layer_image.assign_mat(color_mat)
490
491        self.overlay_layer_image(layer_image)
492
493    def to_file(self, path: PathType, disable_to_rgb_image: bool = False):
494        self.image.to_file(path=path, disable_to_rgb_image=disable_to_rgb_image)
Painter(image: vkit.element.image.Image)
156    def __init__(self, image: Image):
157        self.image = image.copy()
@classmethod
def get_rgb_tuple_from_color_name(cls, color_name: str) -> Tuple[int, int, int]:
62    @classmethod
63    def get_rgb_tuple_from_color_name(cls, color_name: str) -> Tuple[int, int, int]:
64        # https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#color-names
65        return PilImageColor.getrgb(color_name)  # type: ignore
@classmethod
def get_complementary_rgba_tuple(cls, rgba_tuple: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
67    @classmethod
68    def get_complementary_rgba_tuple(
69        cls, rgba_tuple: Tuple[int, int, int, int]
70    ) -> Tuple[int, int, int, int]:
71        return tuple(255 - val if idx < 3 else val for idx, val in enumerate(rgba_tuple))
@classmethod
def get_color_names( cls, elements_or_num_elements: Union[Iterable[Any], int], palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
73    @classmethod
74    def get_color_names(
75        cls,
76        elements_or_num_elements: Union[Iterable[Any], int],
77        palette: Sequence[str] = PALETTE,
78    ):
79        if isinstance(elements_or_num_elements, int):
80            elements = range(elements_or_num_elements)
81        else:
82            elements = elements_or_num_elements
83        return tuple(palette[idx % len(palette)] for idx, _ in enumerate(elements))
@classmethod
def get_rgb_tuples( cls, elements_or_num_elements: Union[Iterable[Any], int], palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
85    @classmethod
86    def get_rgb_tuples(
87        cls,
88        elements_or_num_elements: Union[Iterable[Any], int],
89        palette: Sequence[str] = PALETTE,
90    ):
91        color_names = cls.get_color_names(elements_or_num_elements, palette=palette)
92        return tuple(cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names)
@classmethod
def get_rgba_tuples_from_color_names( cls, num_elements: int, color: Union[str, Iterable[str], Iterable[int], NoneType], alpha: float, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
 94    @classmethod
 95    def get_rgba_tuples_from_color_names(
 96        cls,
 97        num_elements: int,
 98        color: Optional[Union[str, Iterable[str], Iterable[int]]],
 99        alpha: float,
100        palette: Sequence[str] = PALETTE,
101    ):
102        if color is None:
103            rgb_tuples = cls.get_rgb_tuples(num_elements, palette=palette)
104
105        elif isinstance(color, str):
106            rgb_tuple = cls.get_rgb_tuple_from_color_name(color)
107            rgb_tuples = (rgb_tuple,) * num_elements
108
109        else:
110            colors = tuple(color)
111            if not colors:
112                color_names = ()
113
114            elif isinstance(colors[0], str):
115                color_names = cast(Tuple[str], colors)
116
117            elif isinstance(colors[0], int):
118                color_indices = cast(Tuple[int], colors)
119                color_names = [palette[color_idx % len(palette)] for color_idx in color_indices]
120
121            else:
122                raise NotImplementedError()
123
124            rgb_tuples = tuple(
125                cls.get_rgb_tuple_from_color_name(color_name) for color_name in color_names
126            )
127
128        alpha_uint8 = round(255 * alpha)
129        return tuple((*rgb_tuple, alpha_uint8) for rgb_tuple in rgb_tuples)
@classmethod
def create( cls, image_or_shapable_or_shape: Union[vkit.element.image.Image, vkit.element.type.Shapable, Tuple[int, int]], num_channels: int = 3, value: Union[Tuple[int, ...], int] = 255):
131    @classmethod
132    def create(
133        cls,
134        image_or_shapable_or_shape: Union[Image, Shapable, Tuple[int, int]],
135        num_channels: int = 3,
136        value: Union[Tuple[int, ...], int] = 255,
137    ):
138        if isinstance(image_or_shapable_or_shape, Image):
139            image = image_or_shapable_or_shape
140            image = image.copy()
141
142        else:
143            if isinstance(image_or_shapable_or_shape, Shapable):
144                shape = image_or_shapable_or_shape.shape
145            else:
146                shape = image_or_shapable_or_shape
147
148            image = Image.from_shape(
149                shape=shape,
150                num_channels=num_channels,
151                value=value,
152            )
153
154        return Painter(image)
def copy(self):
159    def copy(self):
160        return Painter(image=self.image.copy())
def generate_layer_image(self):
162    def generate_layer_image(self):
163        # RGBA.
164        return Image.from_shapable(
165            self.image,
166            num_channels=4,
167            value=0,
168        )
def overlay_layer_image(self, layer_image: vkit.element.image.Image):
170    def overlay_layer_image(self, layer_image: Image):
171        alpha = layer_image.mat[:, :, 3].astype(np.float32) / 255.0
172
173        layer_image = Image(mat=layer_image.mat[:, :, :3], mode=ImageMode.RGB)
174        layer_image = layer_image.to_target_mode_image(self.image.mode)
175
176        Box.from_shapable(layer_image).fill_image(
177            self.image,
178            value=layer_image,
179            alpha=alpha,
180        )
@classmethod
def paint_text_to_layer_image( cls, layer_image: vkit.element.image.Image, text: str, point: vkit.element.point.Point, rgba_tuple: Tuple[int, int, int, int], font_scale: float = 1.0, enable_complementary_rgba: bool = True):
182    @classmethod
183    def paint_text_to_layer_image(
184        cls,
185        layer_image: Image,
186        text: str,
187        point: Point,
188        rgba_tuple: Tuple[int, int, int, int],
189        font_scale: float = 1.0,
190        enable_complementary_rgba: bool = True,
191    ):
192        if enable_complementary_rgba:
193            color = cls.get_complementary_rgba_tuple(rgba_tuple)
194        else:
195            color = rgba_tuple
196
197        cv.putText(
198            layer_image.mat,
199            text=text,
200            org=(point.x, point.y),
201            fontFace=cv.FONT_HERSHEY_SIMPLEX,
202            fontScale=font_scale,
203            color=color,
204            lineType=cv.LINE_AA,
205        )
def paint_texts( self, texts: Iterable[str], points: Iterable[vkit.element.point.Point], color: Union[str, Iterable[str], Iterable[int], NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
207    def paint_texts(
208        self,
209        texts: Iterable[str],
210        points: Iterable[Point],
211        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
212        alpha: float = 0.5,
213        palette: Sequence[str] = PALETTE,
214    ):
215        layer_image = self.generate_layer_image()
216
217        texts = tuple(texts)
218        points = tuple(points)
219        assert len(texts) == len(points)
220        rgba_tuples = self.get_rgba_tuples_from_color_names(
221            len(points),
222            color,
223            alpha,
224            palette=palette,
225        )
226        for text, point, rgba_tuple in zip(texts, points, rgba_tuples):
227            self.paint_text_to_layer_image(
228                layer_image=layer_image,
229                text=text,
230                point=point,
231                rgba_tuple=rgba_tuple,
232            )
233
234        self.overlay_layer_image(layer_image)
def paint_points( self, points: Union[vkit.element.point.PointList, Iterable[vkit.element.point.Point]], radius: int = 1, enable_index: bool = False, color: Union[str, Iterable[str], Iterable[int], NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
236    def paint_points(
237        self,
238        points: Union[PointList, Iterable[Point]],
239        radius: int = 1,
240        enable_index: bool = False,
241        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
242        alpha: float = 0.5,
243        palette: Sequence[str] = PALETTE,
244    ):
245        layer_image = self.generate_layer_image()
246
247        if not isinstance(points, PointList):
248            points = PointList(points)
249
250        rgba_tuples = self.get_rgba_tuples_from_color_names(
251            len(points),
252            color,
253            alpha,
254            palette=palette,
255        )
256        for idx, (point, rgba_tuple) in enumerate(zip(points, rgba_tuples)):
257            cv.circle(
258                layer_image.mat,
259                center=(point.x, point.y),
260                radius=radius,
261                color=rgba_tuple,
262                thickness=cv.FILLED,
263                lineType=cv.LINE_AA,
264            )
265
266            if enable_index:
267                self.paint_text_to_layer_image(
268                    layer_image=layer_image,
269                    text=str(idx),
270                    point=point,
271                    rgba_tuple=rgba_tuple,
272                )
273
274        self.overlay_layer_image(layer_image)
def paint_lines( self, lines: Iterable[vkit.element.line.Line], thickness: int = 1, enable_arrow: bool = False, arrow_length_ratio: float = 0.1, enable_index: bool = False, color: Union[str, Iterable[str], Iterable[int], NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
276    def paint_lines(
277        self,
278        lines: Iterable[Line],
279        thickness: int = 1,
280        enable_arrow: bool = False,
281        arrow_length_ratio: float = 0.1,
282        enable_index: bool = False,
283        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
284        alpha: float = 0.5,
285        palette: Sequence[str] = PALETTE,
286    ):
287        layer_image = self.generate_layer_image()
288
289        lines = tuple(lines)
290        rgba_tuples = self.get_rgba_tuples_from_color_names(
291            len(lines),
292            color,
293            alpha,
294            palette=palette,
295        )
296        for idx, (line, rgba_tuple) in enumerate(zip(lines, rgba_tuples)):
297            if not enable_arrow:
298                cv.line(
299                    layer_image.mat,
300                    pt1=(line.point_begin.x, line.point_begin.y),
301                    pt2=(line.point_end.x, line.point_end.y),
302                    color=rgba_tuple,
303                    thickness=thickness,
304                    lineType=cv.LINE_AA,
305                )
306            else:
307                cv.arrowedLine(
308                    layer_image.mat,
309                    pt1=(line.point_begin.x, line.point_begin.y),
310                    pt2=(line.point_end.x, line.point_end.y),
311                    color=rgba_tuple,
312                    thickness=thickness,
313                    line_type=cv.LINE_AA,
314                    tipLength=arrow_length_ratio,
315                )
316
317            if enable_index:
318                center_point = line.get_center_point()
319                self.paint_text_to_layer_image(
320                    layer_image=layer_image,
321                    text=str(idx),
322                    point=center_point,
323                    rgba_tuple=rgba_tuple,
324                )
325
326        self.overlay_layer_image(layer_image)
def paint_boxes( self, boxes: Iterable[vkit.element.box.Box], enable_index: bool = False, color: Union[str, Iterable[str], Iterable[int], NoneType] = None, border_thickness: Union[int, NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
328    def paint_boxes(
329        self,
330        boxes: Iterable[Box],
331        enable_index: bool = False,
332        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
333        border_thickness: Optional[int] = None,
334        alpha: float = 0.5,
335        palette: Sequence[str] = PALETTE,
336    ):
337        layer_image = self.generate_layer_image()
338
339        boxes = tuple(boxes)
340        rgba_tuples = self.get_rgba_tuples_from_color_names(
341            len(boxes),
342            color,
343            alpha,
344            palette=palette,
345        )
346
347        for idx, (box, rgba_tuple) in enumerate(zip(boxes, rgba_tuples)):
348            box.fill_image(image=layer_image, value=rgba_tuple)
349            if border_thickness:
350                inner_box = Box(
351                    up=box.up + border_thickness,
352                    down=box.down - border_thickness,
353                    left=box.left + border_thickness,
354                    right=box.right - border_thickness,
355                )
356                if inner_box.valid:
357                    inner_box.fill_image(image=layer_image, value=0)
358
359            if enable_index:
360                center_point = box.get_center_point()
361                self.paint_text_to_layer_image(
362                    layer_image=layer_image,
363                    text=str(idx),
364                    point=center_point,
365                    rgba_tuple=rgba_tuple,
366                    enable_complementary_rgba=(border_thickness is None),
367                )
368
369        self.overlay_layer_image(layer_image)
def paint_polygons( self, polygons: Iterable[vkit.element.polygon.Polygon], color: Union[str, Iterable[str], Iterable[int], NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed'), enable_index: bool = False, enable_polygon_points: bool = False, polygon_points_color: str = 'red', polygon_points_alpha: float = 1.0):
371    def paint_polygons(
372        self,
373        polygons: Iterable[Polygon],
374        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
375        alpha: float = 0.5,
376        palette: Sequence[str] = PALETTE,
377        enable_index: bool = False,
378        enable_polygon_points: bool = False,
379        polygon_points_color: str = 'red',
380        polygon_points_alpha: float = 1.0,
381    ):
382        layer_image = self.generate_layer_image()
383
384        polygons = tuple(polygons)
385        rgba_tuples = self.get_rgba_tuples_from_color_names(
386            len(polygons),
387            color,
388            alpha,
389            palette=palette,
390        )
391        for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
392            polygon.fill_image(image=layer_image, value=rgba_tuple)
393
394        if enable_index:
395            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
396                center_point = polygon.get_center_point()
397                self.paint_text_to_layer_image(
398                    layer_image=layer_image,
399                    text=str(idx),
400                    point=center_point,
401                    rgba_tuple=rgba_tuple,
402                )
403
404        if enable_polygon_points:
405            for idx, (polygon, rgba_tuple) in enumerate(zip(polygons, rgba_tuples)):
406                self.paint_points(
407                    polygon.points,
408                    color=polygon_points_color,
409                    alpha=polygon_points_alpha,
410                )
411
412        self.overlay_layer_image(layer_image)
def paint_mask( self, mask: vkit.element.mask.Mask, color: str = 'red', alpha: float = 0.5):
414    def paint_mask(
415        self,
416        mask: Mask,
417        color: str = 'red',
418        alpha: float = 0.5,
419    ):
420        layer_image = self.generate_layer_image()
421
422        rgb_tuple = self.get_rgb_tuple_from_color_name(color)
423        alpha_uint8 = round(255 * alpha)
424        mask.fill_image(image=layer_image, value=(*rgb_tuple, alpha_uint8))
425
426        self.overlay_layer_image(layer_image)
def paint_masks( self, masks: Iterable[vkit.element.mask.Mask], color: Union[str, Iterable[str], Iterable[int], NoneType] = None, alpha: float = 0.5, palette: Sequence[str] = ('#006400', '#00008b', '#b03060', '#ff0000', '#ffff00', '#deb887', '#00ff00', '#00ffff', '#ff00ff', '#6495ed')):
428    def paint_masks(
429        self,
430        masks: Iterable[Mask],
431        color: Optional[Union[str, Iterable[str], Iterable[int]]] = None,
432        alpha: float = 0.5,
433        palette: Sequence[str] = PALETTE,
434    ):
435        masks = tuple(masks)
436        rgba_tuples = self.get_rgba_tuples_from_color_names(
437            len(masks),
438            color,
439            alpha,
440            palette=palette,
441        )
442
443        layer_image = self.generate_layer_image()
444        layer_image.fill_by_mask_value_tuples(zip(masks, rgba_tuples))
445        self.overlay_layer_image(layer_image)
def paint_score_map( self, score_map: vkit.element.score_map.ScoreMap, enable_boundary_equalization: bool = False, enable_center_shift: bool = False, cv_colormap: int = 2, alpha: float = 0.5):
447    def paint_score_map(
448        self,
449        score_map: ScoreMap,
450        enable_boundary_equalization: bool = False,
451        enable_center_shift: bool = False,
452        cv_colormap: int = cv.COLORMAP_JET,
453        alpha: float = 0.5,
454    ):
455        layer_image = self.generate_layer_image()
456
457        mat = score_map.mat.copy()
458
459        if score_map.is_prob:
460            mat *= 255.0
461
462        if enable_boundary_equalization:
463            # Equalize to [0, 255]
464            val_min = np.min(mat)
465            mat -= val_min
466            val_max = np.max(mat)
467            mat *= 255.0
468            mat /= val_max
469
470        elif enable_center_shift:
471            mat *= 127.5
472
473        mat = np.clip(mat, 0, 255).astype(np.uint8)
474
475        # Apply color map.
476        color_mat = cv.applyColorMap(mat, cv_colormap)
477        color_mat = cv.cvtColor(color_mat, cv.COLOR_BGR2RGB)
478
479        # Add alpha channel.
480        alpha_uint8 = round(255 * alpha)
481        color_mat = np.dstack((
482            color_mat,
483            np.full(color_mat.shape[:2], alpha_uint8, dtype=np.uint8),
484        ))
485
486        if score_map.box:
487            score_map.box.fill_image(layer_image, color_mat)
488        else:
489            layer_image.assign_mat(color_mat)
490
491        self.overlay_layer_image(layer_image)
def to_file( self, path: Union[str, os.PathLike], disable_to_rgb_image: bool = False):
493    def to_file(self, path: PathType, disable_to_rgb_image: bool = False):
494        self.image.to_file(path=path, disable_to_rgb_image=disable_to_rgb_image)