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)
@classmethod
def
get_complementary_rgba_tuple(cls, rgba_tuple: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]:
@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)
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)
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)