Spaces:
Running
Running
File size: 6,297 Bytes
f0f6e5c |
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 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
import math
from typing import Any
import cv2
import numpy as np
from pydantic import BaseModel, Field, field_validator
class Point(BaseModel):
x: int
y: int
def __iter__(self):
return iter((self.x, self.y))
def __getitem__(self, index) -> int:
return (self.x, self.y)[index]
def __tuple__(self) -> tuple[int, int]:
return (self.x, self.y)
def __repr__(self) -> str:
return f"Point(x={self.x}, y={self.y})"
class BoundingBox(BaseModel):
label: str = Field(..., description="The label that's given for this bounding box")
left: int = Field(..., description="Left coordinate of the bounding box")
right: int = Field(..., description="Right coordinate of the bounding box")
top: int = Field(..., description="Top coordinate of the bounding box")
bottom: int = Field(..., description="Bottom coordinate of the bounding box")
@field_validator("left", "top", mode="before")
@classmethod
def round_down(cls, v):
return math.floor(float(v))
@field_validator("right", "bottom", mode="before")
@classmethod
def round_up(cls, v):
return math.ceil(float(v))
class POI(BaseModel):
info: dict[str, Any]
element_centroid: Point
bounding_box: BoundingBox
def calculate_dash_points(start, end, dash_length, gap_length):
x1, y1 = start
x2, y2 = end
dx = x2 - x1
dy = y2 - y1
dist = np.sqrt(dx * dx + dy * dy)
if dist == 0:
return []
unit_x = dx / dist
unit_y = dy / dist
dash_points = []
current_dist = 0
while current_dist < dist:
dash_end = min(current_dist + dash_length, dist)
dash_points.extend(
[
(int(x1 + unit_x * current_dist), int(y1 + unit_y * current_dist)),
(int(x1 + unit_x * dash_end), int(y1 + unit_y * dash_end)),
],
)
current_dist += dash_length + gap_length
return dash_points
def draw_dashed_rectangle(
img,
bbox: BoundingBox,
color,
thickness=1,
dash_length=10,
gap_length=5,
):
# Calculate dash points for all sides
top_points = calculate_dash_points(
(bbox.left + 25, bbox.top + 25),
(bbox.right + 25, bbox.top + 25),
dash_length,
gap_length,
)
right_points = calculate_dash_points(
(bbox.right + 25, bbox.top + 25),
(bbox.right + 25, bbox.bottom + 25),
dash_length,
gap_length,
)
bottom_points = calculate_dash_points(
(bbox.right + 25, bbox.bottom + 25),
(bbox.left + 25, bbox.bottom + 25),
dash_length,
gap_length,
)
left_points = calculate_dash_points(
(bbox.left + 25, bbox.bottom + 25),
(bbox.left + 25, bbox.top + 25),
dash_length,
gap_length,
)
# Combine all points
all_points = top_points + right_points + bottom_points + left_points
# Draw all lines at once
if all_points:
all_points = np.array(all_points).reshape((-1, 2, 2))
cv2.polylines(img, all_points, False, color, thickness)
# @time_it(name='Annotate bounding box')
def annotate_bounding_box(image: bytes, bbox: BoundingBox) -> None:
# Draw dashed bounding box
draw_dashed_rectangle(
image,
bbox,
color=(0, 0, 255),
thickness=1,
dash_length=10,
gap_length=5,
)
# Prepare label
font_scale = 0.4 * 4 # Increased by 4x for the larger patch
font = cv2.FONT_HERSHEY_SIMPLEX
thickness = 3 # Increased thickness for the larger patch
# Get text size for the larger patch
(label_width, label_height), _ = cv2.getTextSize(
bbox.label,
font,
font_scale,
thickness,
)
# Create a larger patch (4x)
large_label_patch = np.zeros(
(label_height + 20, label_width + 20, 4),
dtype=np.uint8,
)
large_label_patch[:, :, 0:3] = (0, 0, 255) # BGR color format: Red background
large_label_patch[:, :, 3] = 128 # Alpha channel: 50% opacity (128/255 = 0.5)
# Draw text on the larger patch
cv2.putText(
large_label_patch,
bbox.label,
(8, label_height + 8), # Adjusted position for the larger patch
font,
font_scale,
(255, 255, 255, 128), # White text, 50% opaque (128/255 = 0.5)
thickness,
)
# Scale down the patch to improve anti-aliasing
label_patch = cv2.resize(
large_label_patch,
(label_width // 4 + 5, label_height // 4 + 5),
interpolation=cv2.INTER_AREA,
)
# Calculate position for top-left alignment
offset = 2 # Small offset to prevent touching the bounding box edge
x = min(image.shape[1], max(0, int(bbox.left + 25) - offset))
y = min(image.shape[0], max(0, int(bbox.top + 25) - label_patch.shape[0] - offset))
# Ensure we're not out of bounds
x_end = min(image.shape[1], x + label_patch.shape[1])
y_end = min(image.shape[0], y + label_patch.shape[0])
label_patch = label_patch[: (y_end - y), : (x_end - x)]
# Create a mask for the label patch
alpha_mask = label_patch[:, :, 3] / 255.0
alpha_mask = np.repeat(alpha_mask[:, :, np.newaxis], 3, axis=2)
# Blend the label patch with the image
image_section = image[y:y_end, x:x_end]
blended = (1 - alpha_mask) * image_section + alpha_mask * label_patch[:, :, 0:3]
image[y:y_end, x:x_end] = blended.astype(np.uint8)
def annotate_bounding_boxes(image: bytes, bounding_boxes: list[BoundingBox]) -> bytes:
# Read the image
nparr = np.frombuffer(image, np.uint8)
# Decode the image
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
padded_img = cv2.copyMakeBorder(
img,
top=25, # Value chosen based on label size
bottom=25, # Value chosen based on label size
left=25, # Value chosen based on label size
right=25, # Value chosen based on label size
borderType=cv2.BORDER_CONSTANT,
value=(255, 255, 255),
)
for bounding_box in bounding_boxes:
# Annotate the image in place with the bounding box and the bounding box label
annotate_bounding_box(padded_img, bounding_box)
_, buffer = cv2.imencode(".jpeg", padded_img)
return buffer.tobytes()
|