File size: 7,877 Bytes
eb2bcc4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# This file is used to visualize bounding boxes on an image
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,
# color name or hex code or tuple of RGBA or tuple of RGB or tuple (color_name, alpha)
# between 0 (fully transparent) and 255 (fully opaque)
'bbox_outline_color': ('blue', 123),
# color name or hex code or tuple of RGBA or tuple of RGB or tuple (color_name, alpha)
# between 0 (fully transparent) and 255 (fully opaque)
'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"]: # Online
return ImageFont.truetype(requests.get(path_or_url, stream=True).raw, size=size)
else: # Local
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)
# draw.bitmap((100, 100), 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
|