Spaces:
Running on Zero
Running on Zero
| """Image preprocessing and visualization utilities.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import io | |
| import base64 | |
| from pathlib import Path | |
| from typing import Any, Iterable | |
| from PIL import Image, ImageDraw, ImageFont, ImageOps | |
| from data.schemas import LABEL_DISPLAY_NAMES, bbox_to_pixels | |
| LABEL_STYLE = { | |
| "dust": ((245, 158, 11), 2), | |
| "dirt": ((217, 119, 6), 2), | |
| "scratch": ((220, 38, 38), 3), | |
| "long_hair": ((124, 58, 237), 2), | |
| "short_hair": ((8, 145, 178), 2), | |
| "emulsion_damage": ((226, 232, 240), 3), | |
| "chemical_stain": ((22, 163, 74), 3), | |
| "light_leak": ((244, 114, 182), 3), | |
| } | |
| DEFAULT_STYLE = ((255, 255, 255), 2) | |
| def load_image(image: str | Path | Image.Image) -> Image.Image: | |
| """Load an image-like value and return RGB PIL Image.""" | |
| if isinstance(image, Image.Image): | |
| pil = image | |
| else: | |
| pil = Image.open(image) | |
| pil = ImageOps.exif_transpose(pil) | |
| if pil.mode == "RGBA": | |
| background = Image.new("RGB", pil.size, (24, 22, 20)) | |
| background.paste(pil, mask=pil.getchannel("A")) | |
| return background | |
| if pil.mode != "RGB": | |
| return pil.convert("RGB") | |
| return pil.copy() | |
| def image_to_png_bytes(image: Image.Image) -> bytes: | |
| buf = io.BytesIO() | |
| load_image(image).save(buf, format="PNG", optimize=True) | |
| return buf.getvalue() | |
| def image_to_data_uri( | |
| image: Image.Image, | |
| *, | |
| max_side: int = 1800, | |
| image_format: str = "JPEG", | |
| quality: int = 92, | |
| ) -> str: | |
| """Return a browser-openable image data URI for review previews.""" | |
| pil = resize_for_preview(load_image(image), max_side=max_side) | |
| fmt = image_format.upper() | |
| buf = io.BytesIO() | |
| if fmt in {"JPG", "JPEG"}: | |
| pil = pil.convert("RGB") | |
| pil.save(buf, format="JPEG", quality=quality, optimize=True) | |
| mime = "image/jpeg" | |
| elif fmt == "PNG": | |
| pil.save(buf, format="PNG", optimize=True) | |
| mime = "image/png" | |
| else: | |
| raise ValueError(f"unsupported image_format: {image_format}") | |
| encoded = base64.b64encode(buf.getvalue()).decode("ascii") | |
| return f"data:{mime};base64,{encoded}" | |
| def image_sha256(image: Image.Image | bytes) -> str: | |
| if isinstance(image, bytes): | |
| payload = image | |
| else: | |
| payload = image_to_png_bytes(image) | |
| return hashlib.sha256(payload).hexdigest() | |
| def resize_for_preview(image: Image.Image, max_side: int = 1400) -> Image.Image: | |
| pil = load_image(image) | |
| if max(pil.size) <= max_side: | |
| return pil | |
| out = pil.copy() | |
| out.thumbnail((max_side, max_side), Image.Resampling.LANCZOS) | |
| return out | |
| def draw_defects( | |
| image: Image.Image, | |
| defects: Iterable[dict[str, Any]], | |
| *, | |
| title: str | None = None, | |
| max_boxes: int = 300, | |
| ) -> Image.Image: | |
| """Draw normalized defect boxes onto an RGB copy of an image.""" | |
| out = load_image(image) | |
| draw = ImageDraw.Draw(out) | |
| width, height = out.size | |
| font = ImageFont.load_default() | |
| if title: | |
| draw.rectangle((0, 0, min(width, 440), 24), fill=(12, 10, 9)) | |
| draw.text((8, 6), title, fill=(254, 243, 199), font=font) | |
| drawn = 0 | |
| for defect in defects: | |
| if drawn >= max_boxes: | |
| break | |
| label = str(defect.get("label", "unknown")) | |
| pixels = bbox_to_pixels(defect.get("bbox"), width, height) | |
| if pixels is None: | |
| continue | |
| x_min, y_min, x_max, y_max = pixels | |
| color, line_width = LABEL_STYLE.get(label, DEFAULT_STYLE) | |
| draw.rectangle((x_min, y_min, x_max, y_max), outline=color, width=line_width) | |
| label_text = LABEL_DISPLAY_NAMES.get(label, label) | |
| text_bbox = draw.textbbox((x_min, max(0, y_min - 16)), label_text, font=font) | |
| draw.rectangle(text_bbox, fill=(12, 10, 9)) | |
| draw.text((text_bbox[0] + 1, text_bbox[1]), label_text, fill=color, font=font) | |
| drawn += 1 | |
| return out | |
| __all__ = [ | |
| "DEFAULT_STYLE", | |
| "LABEL_STYLE", | |
| "draw_defects", | |
| "image_sha256", | |
| "image_to_data_uri", | |
| "image_to_png_bytes", | |
| "load_image", | |
| "resize_for_preview", | |
| ] | |