| """Render review-only overlays from Trace's public image annotations.""" |
|
|
| from __future__ import annotations |
|
|
| from collections.abc import Mapping, Sequence |
| from typing import Any |
|
|
| from PIL import Image, ImageDraw, ImageFont |
|
|
| PUBLIC_ANNOTATION_TYPES = frozenset( |
| { |
| "bbox", |
| "bbox_map", |
| "bbox_sequence", |
| "bbox_set", |
| "bbox_set_map", |
| "point", |
| "point_map", |
| "point_sequence", |
| "point_set", |
| "point_set_map", |
| "segment", |
| "segment_set", |
| } |
| ) |
|
|
| _COLORS = ( |
| (229, 57, 53, 235), |
| (30, 136, 229, 235), |
| (0, 137, 123, 235), |
| (251, 140, 0, 235), |
| (142, 36, 170, 235), |
| ) |
|
|
|
|
| def render_annotation_overlay( |
| source_image: Image.Image, |
| annotation_gt: Mapping[str, Any], |
| ) -> Image.Image: |
| """Return an RGB image with a public annotation drawn over the source.""" |
|
|
| image = source_image.convert("RGBA").copy() |
| draw = ImageDraw.Draw(image, "RGBA") |
| annotation_type = str(annotation_gt.get("type", "")).strip() |
| if annotation_type not in PUBLIC_ANNOTATION_TYPES: |
| raise ValueError(f"unsupported public annotation type: {annotation_type!r}") |
|
|
| items = list(_annotation_items(annotation_type, annotation_gt.get("value"))) |
| for index, (label, geometry_kind, geometry) in enumerate(items): |
| color = _COLORS[index % len(_COLORS)] |
| if geometry_kind == "bbox": |
| _draw_bbox(draw, geometry, color=color, label=label) |
| elif geometry_kind == "point": |
| _draw_point(draw, geometry, color=color, label=label) |
| elif geometry_kind == "segment": |
| _draw_segment(draw, geometry, color=color, label=label) |
|
|
| if not items: |
| _draw_empty_annotation_badge(draw, annotation_type) |
| return image.convert("RGB") |
|
|
|
|
| def _annotation_items(annotation_type: str, value: Any): |
| if annotation_type == "bbox": |
| yield "answer", "bbox", value |
| elif annotation_type in {"bbox_set", "bbox_sequence"} and _sequence(value): |
| for index, bbox in enumerate(value): |
| yield str(index + 1), "bbox", bbox |
| elif annotation_type == "bbox_map" and isinstance(value, Mapping): |
| for key, bbox in sorted(value.items(), key=lambda item: str(item[0])): |
| yield str(key), "bbox", bbox |
| elif annotation_type == "bbox_set_map" and isinstance(value, Mapping): |
| for key, boxes in sorted(value.items(), key=lambda item: str(item[0])): |
| if not _sequence(boxes): |
| continue |
| for index, bbox in enumerate(boxes): |
| yield f"{key}:{index + 1}", "bbox", bbox |
| elif annotation_type == "point": |
| yield "answer", "point", value |
| elif annotation_type in {"point_set", "point_sequence"} and _sequence(value): |
| for index, point in enumerate(value): |
| yield str(index + 1), "point", point |
| elif annotation_type == "point_map" and isinstance(value, Mapping): |
| for key, point in sorted(value.items(), key=lambda item: str(item[0])): |
| yield str(key), "point", point |
| elif annotation_type == "point_set_map" and isinstance(value, Mapping): |
| for key, points in sorted(value.items(), key=lambda item: str(item[0])): |
| if not _sequence(points): |
| continue |
| for index, point in enumerate(points): |
| yield f"{key}:{index + 1}", "point", point |
| elif annotation_type == "segment": |
| yield "answer", "segment", value |
| elif annotation_type == "segment_set" and _sequence(value): |
| for index, segment in enumerate(value): |
| yield str(index + 1), "segment", segment |
|
|
|
|
| def _draw_bbox( |
| draw: ImageDraw.ImageDraw, |
| value: Any, |
| *, |
| color: tuple[int, ...], |
| label: str, |
| ) -> None: |
| if not _numeric_sequence(value, 4): |
| return |
| x0, y0, x1, y1 = (float(item) for item in value) |
| draw.rectangle((x0, y0, x1, y1), outline=color, width=4) |
| _draw_label(draw, (x0, y0), label, color) |
|
|
|
|
| def _draw_point( |
| draw: ImageDraw.ImageDraw, |
| value: Any, |
| *, |
| color: tuple[int, ...], |
| label: str, |
| ) -> None: |
| if not _numeric_sequence(value, 2): |
| return |
| x, y = (float(item) for item in value) |
| radius = 7.0 |
| draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color) |
| _draw_label(draw, (x + radius, y - radius), label, color) |
|
|
|
|
| def _draw_segment( |
| draw: ImageDraw.ImageDraw, |
| value: Any, |
| *, |
| color: tuple[int, ...], |
| label: str, |
| ) -> None: |
| if not _sequence(value) or len(value) != 2: |
| return |
| if not _numeric_sequence(value[0], 2) or not _numeric_sequence(value[1], 2): |
| return |
| points = [(float(point[0]), float(point[1])) for point in value] |
| draw.line(points, fill=color, width=5) |
| _draw_label(draw, points[0], label, color) |
|
|
|
|
| def _draw_label( |
| draw: ImageDraw.ImageDraw, |
| point: tuple[float, float], |
| label: str, |
| color: tuple[int, ...], |
| ) -> None: |
| text = str(label)[:48] |
| if not text: |
| return |
| font = ImageFont.load_default() |
| left, top = max(0.0, float(point[0])), max(0.0, float(point[1]) - 16.0) |
| right = left + max(18.0, float(draw.textlength(text, font=font)) + 8.0) |
| draw.rectangle((left, top, right, top + 16.0), fill=color) |
| draw.text((left + 4.0, top + 2.0), text, fill=(255, 255, 255, 255), font=font) |
|
|
|
|
| def _draw_empty_annotation_badge( |
| draw: ImageDraw.ImageDraw, |
| annotation_type: str, |
| ) -> None: |
| _draw_label( |
| draw, |
| (8.0, 22.0), |
| f"{annotation_type}: empty witness", |
| (69, 90, 100, 235), |
| ) |
|
|
|
|
| def _sequence(value: Any) -> bool: |
| return isinstance(value, Sequence) and not isinstance( |
| value, (str, bytes, bytearray) |
| ) |
|
|
|
|
| def _numeric_sequence(value: Any, length: int) -> bool: |
| if not _sequence(value) or len(value) != length: |
| return False |
| return all( |
| isinstance(item, (int, float)) and not isinstance(item, bool) |
| for item in value |
| ) |
|
|
|
|
| __all__ = ["PUBLIC_ANNOTATION_TYPES", "render_annotation_overlay"] |
|
|