| | |
| | from urllib.parse import urlparse |
| | from PIL import Image, ImageDraw, ImageFont |
| | import numpy as np |
| | import requests |
| | from typing import List, Callable |
| | from functools import cache |
| | import matplotlib.colors as colors |
| |
|
| | DEFAULTS = { |
| | 'bbox_outline_width': 2, |
| | |
| | |
| | 'bbox_outline_color': ('blue', 123), |
| | |
| | |
| | 'bbox_fill_color': ('red', 50), |
| | 'label_text_color': "black", |
| | 'label_fill_color': "red", |
| | 'label_text_padding': 0, |
| | 'label_rectangle_left_margin': 0, |
| | 'label_rectangle_top_margin': 0, |
| | 'label_text_size': 12, |
| | } |
| |
|
| |
|
| | @cache |
| | def get_font(path_or_url: str = 'https://github.com/googlefonts/roboto/raw/main/src/hinted/Roboto-Regular.ttf', size: int = DEFAULTS['label_text_size']): |
| | if urlparse(path_or_url).scheme in ["http", "https"]: |
| | return ImageFont.truetype(requests.get(path_or_url, stream=True).raw, size=size) |
| | else: |
| | return ImageFont.truetype(path_or_url, size=size) |
| |
|
| | named_colors_mapping = colors.get_named_colors_mapping() |
| | @cache |
| | def parse_color(color: str | tuple) -> tuple | str: |
| | if isinstance(color, tuple): |
| | if len(color) == 2: |
| | real_color, alpha = (color[0], int(color[1])) |
| | if colors.is_color_like(real_color): |
| | real_color_rgb = colors.hex2color(named_colors_mapping.get(real_color, real_color)) |
| | if len(real_color_rgb) == 3: |
| | real_color_alpha = (np.array(real_color_rgb, dtype=int) * 255).tolist() + [alpha] |
| | return tuple(real_color_alpha) |
| | return color |
| |
|
| | def draw_bounding_box( |
| | image: Image.Image, |
| | bbox_outline_width: int, |
| | bbox_fill_color: str | list[tuple | str], |
| | bbox_outline_color: str | list[tuple | str], |
| | bbox: List[List[int]], |
| | label_rotate_angle: int = 0, |
| | mask_callback: Callable[[ImageDraw.ImageDraw], None] = None) -> Image.Image: |
| | options = { |
| | 'xy': bbox, |
| | 'fill': parse_color(bbox_fill_color) if bbox_fill_color else None, |
| | 'outline': parse_color(bbox_outline_color) if bbox_outline_color else None, |
| | 'width': bbox_outline_width |
| | } |
| | options = {k: v for k, v in options.items() if v is not None} |
| | rectangle_image = Image.new('RGBA', image.size) |
| | rectangle_image_draw = ImageDraw.Draw(rectangle_image) |
| | rectangle_image_draw.rectangle(**options) |
| | if mask_callback: |
| | mask_callback(rectangle_image_draw) |
| | rectangle_image = rectangle_image.rotate(label_rotate_angle, expand=1) |
| | image.paste(im=rectangle_image, mask=rectangle_image) |
| | |
| | return image |
| |
|
| | def visualize_bboxes_on_image( |
| | image: Image.Image, |
| | bboxes: List[List[int]], |
| | labels: List[str] = None, |
| | bbox_outline_width=DEFAULTS["bbox_outline_width"], |
| | bbox_outline_color=DEFAULTS["bbox_outline_color"], |
| | bbox_fill_color: str | list[tuple | str] = DEFAULTS["bbox_fill_color"], |
| | label_text_color: str | list[tuple | |
| | str] = DEFAULTS["label_text_color"], |
| | label_fill_color=DEFAULTS["label_fill_color"], |
| | label_text_padding=DEFAULTS["label_text_padding"], |
| | label_rectangle_left_margin=DEFAULTS["label_rectangle_left_margin"], |
| | label_rectangle_top_margin=DEFAULTS['label_rectangle_top_margin'], |
| | label_text_size=DEFAULTS["label_text_size"], |
| | convert_to_x0y0x1y1=None, |
| | label_rotate_angle: int = 0) -> Image.Image: |
| | ''' |
| | Visualize bounding boxes on an image |
| | Args: |
| | image: Image to visualize |
| | bboxes: List of bounding boxes |
| | labels: Titles of the bounding boxes |
| | bbox_outline_width: Width of the bounding box |
| | bbox_outline_color: Color of the bounding box |
| | bbox_fill_color: Fill color of the bounding box |
| | label_text_color: Color of the label text |
| | label_fill_color: Color of the label rectangle |
| | label_text_padding: Padding of the label text |
| | label_rectangle_left_margin: Left padding of the label rectangle |
| | label_rectangle_top_margin: Top padding of the label rectangle |
| | label_text_size: Font size of the label text |
| | convert_to_x0y0x1y1: Function to convert bounding box to x0y0x1y1 format |
| | label_rotate_angle: Angle to rotate the label text |
| | Returns: |
| | Image: Image annotated with bounding boxes |
| | ''' |
| | image = image.copy().convert("RGB") |
| | font = get_font(size=label_text_size) |
| | labels = (labels or []) + np.full(len(bboxes) - |
| | len(labels or []), None).tolist() |
| | bbox_fill_colors = bbox_fill_color if isinstance(bbox_fill_color, list) else [ |
| | bbox_fill_color] * len(bboxes) |
| | bbox_outline_colors = bbox_outline_color if isinstance( |
| | bbox_outline_color, list) else [bbox_outline_color] * len(bboxes) |
| | |
| | for bbox, label, _bbox_fill_color, _bbox_outline_color in zip(bboxes, labels, bbox_fill_colors, bbox_outline_colors): |
| | x0, y0, x1, y1 = convert_to_x0y0x1y1( |
| | bbox) if convert_to_x0y0x1y1 is not None else bbox |
| | |
| | image = draw_bounding_box( |
| | image = image, |
| | bbox_outline_width = bbox_outline_width, |
| | bbox_fill_color = _bbox_fill_color, |
| | bbox_outline_color = _bbox_outline_color, |
| | bbox = [x0, y0, x1, y1]) |
| |
|
| | if label is not None: |
| | image = draw_text_on_image( |
| | image = image, |
| | text_position_xy = [x0, y0], |
| | label = label, |
| | label_text_color = label_text_color, |
| | label_fill_color = label_fill_color, |
| | label_text_padding = label_text_padding, |
| | label_rectangle_left_margin = label_rectangle_left_margin, |
| | label_rectangle_top_margin = label_rectangle_top_margin, |
| | label_text_size = label_text_size, |
| | font = font, |
| | label_rotate_angle = label_rotate_angle) |
| | return image |
| |
|
| | def draw_text_on_image( |
| | image: Image.Image, |
| | text_position_xy: List[int], |
| | label: str, |
| | label_text_color=DEFAULTS["label_text_color"], |
| | label_fill_color=DEFAULTS["label_fill_color"], |
| | label_text_padding=DEFAULTS["label_text_padding"], |
| | label_rectangle_left_margin=DEFAULTS["label_rectangle_left_margin"], |
| | label_rectangle_top_margin=DEFAULTS['label_rectangle_top_margin'], |
| | label_text_size=DEFAULTS["label_text_size"], |
| | font: ImageFont.FreeTypeFont = None, |
| | label_rotate_angle: int = 0) -> Image.Image: |
| | image = image.copy().convert("RGB") |
| | font = font or get_font(size=label_text_size) |
| | x0, y0 = text_position_xy |
| | text_position = ( |
| | x0 - label_rectangle_left_margin + label_text_padding, |
| | y0 - label_rectangle_top_margin + label_text_padding) |
| | draw = ImageDraw.Draw(image) |
| | _, _, text_bbox_right, text_bbox_bottom = draw.textbbox(text_position, label, font=font) |
| | xy = [ |
| | text_position[0] - label_text_padding, |
| | text_position[1] - label_text_padding, |
| | text_bbox_right + label_text_padding + label_text_padding, |
| | text_bbox_bottom + label_text_padding + label_text_padding |
| | ] |
| | image = draw_bounding_box( |
| | image = image, |
| | bbox_outline_width = 0, |
| | bbox_fill_color = label_fill_color, |
| | bbox_outline_color = None, |
| | bbox = xy, |
| | label_rotate_angle = label_rotate_angle, |
| | mask_callback = lambda mask_draw: mask_draw.text(text_position, label, font=font, fill=label_text_color)) |
| | return image |
| |
|