Spaces:
Running on Zero
Running on Zero
| """Synthetic defect augmentation helpers.""" | |
| from __future__ import annotations | |
| import random | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Iterable | |
| from PIL import Image, ImageEnhance, ImageOps | |
| from data.preprocessing import load_image | |
| SYNTHETIC_LABEL_MAP = { | |
| "dust": "dust", | |
| "dirt": "dirt", | |
| "dots": "dust", | |
| "hair": "long_hair", | |
| "hair-short": "short_hair", | |
| "lint": "short_hair", | |
| "scratches": "scratch", | |
| "scratch": "scratch", | |
| "smut": "dirt", | |
| "spots": "dirt", | |
| "sprinkles": "dust", | |
| "stain": "dirt", | |
| } | |
| class OverlayDefect: | |
| path: Path | |
| label: str | |
| def discover_overlays(root: str | Path) -> list[OverlayDefect]: | |
| """Find transparent PNG overlays and infer labels from folder names.""" | |
| root = Path(root) | |
| overlays: list[OverlayDefect] = [] | |
| if not root.exists(): | |
| return overlays | |
| for path in sorted(root.rglob("*.png")): | |
| label = SYNTHETIC_LABEL_MAP.get(path.parent.name.lower()) | |
| if label: | |
| overlays.append(OverlayDefect(path=path, label=label)) | |
| return overlays | |
| def _visible_bbox(alpha: Image.Image) -> tuple[int, int, int, int] | None: | |
| bbox = alpha.getbbox() | |
| if bbox is None: | |
| return None | |
| x_min, y_min, x_max, y_max = bbox | |
| if x_max <= x_min or y_max <= y_min: | |
| return None | |
| return bbox | |
| def _normalized_bbox( | |
| paste_x: int, | |
| paste_y: int, | |
| visible_bbox: tuple[int, int, int, int], | |
| width: int, | |
| height: int, | |
| ) -> list[float]: | |
| x_min, y_min, x_max, y_max = visible_bbox | |
| return [ | |
| round((paste_x + x_min) / width, 6), | |
| round((paste_y + y_min) / height, 6), | |
| round((paste_x + x_max) / width, 6), | |
| round((paste_y + y_max) / height, 6), | |
| ] | |
| def apply_overlay( | |
| base: Image.Image, | |
| overlay: OverlayDefect, | |
| *, | |
| rng: random.Random, | |
| scale_range: tuple[float, float] = (0.35, 1.4), | |
| opacity_range: tuple[float, float] = (0.55, 0.95), | |
| ) -> tuple[Image.Image, dict] | None: | |
| """Paste one defect overlay onto a copy of base and return annotation.""" | |
| out = load_image(base).convert("RGBA") | |
| width, height = out.size | |
| layer = Image.open(overlay.path).convert("RGBA") | |
| if rng.random() < 0.5: | |
| layer = ImageOps.mirror(layer) | |
| if rng.random() < 0.35: | |
| layer = layer.rotate(rng.uniform(-22, 22), expand=True, resample=Image.Resampling.BICUBIC) | |
| scale = rng.uniform(*scale_range) | |
| new_size = ( | |
| max(2, int(layer.width * scale)), | |
| max(2, int(layer.height * scale)), | |
| ) | |
| layer = layer.resize(new_size, Image.Resampling.LANCZOS) | |
| if layer.width >= width or layer.height >= height: | |
| layer.thumbnail((width // 2, height // 2), Image.Resampling.LANCZOS) | |
| alpha = layer.getchannel("A") | |
| alpha = ImageEnhance.Brightness(alpha).enhance(rng.uniform(*opacity_range)) | |
| layer.putalpha(alpha) | |
| visible = _visible_bbox(alpha) | |
| if visible is None: | |
| return None | |
| max_x = max(0, width - layer.width) | |
| max_y = max(0, height - layer.height) | |
| paste_x = rng.randint(0, max_x) if max_x else 0 | |
| paste_y = rng.randint(0, max_y) if max_y else 0 | |
| out.alpha_composite(layer, (paste_x, paste_y)) | |
| annotation = { | |
| "label": overlay.label, | |
| "bbox": _normalized_bbox(paste_x, paste_y, visible, width, height), | |
| } | |
| return out.convert("RGB"), annotation | |
| def augment_image( | |
| base: Image.Image, | |
| overlays: Iterable[OverlayDefect], | |
| *, | |
| seed: int, | |
| defects_per_image: tuple[int, int] = (3, 9), | |
| ) -> tuple[Image.Image, list[dict]]: | |
| """Create one augmented image and its generated annotations.""" | |
| rng = random.Random(seed) | |
| overlay_list = list(overlays) | |
| if not overlay_list: | |
| return load_image(base), [] | |
| by_label: dict[str, list[OverlayDefect]] = {} | |
| for overlay in overlay_list: | |
| by_label.setdefault(overlay.label, []).append(overlay) | |
| labels = sorted(by_label) | |
| out = load_image(base) | |
| annotations: list[dict] = [] | |
| target = rng.randint(*defects_per_image) | |
| for _ in range(target): | |
| label = rng.choice(labels) | |
| overlay = rng.choice(by_label[label]) | |
| result = apply_overlay(out, overlay, rng=rng) | |
| if result is None: | |
| continue | |
| out, annotation = result | |
| annotations.append(annotation) | |
| return out, annotations | |
| __all__ = [ | |
| "OverlayDefect", | |
| "SYNTHETIC_LABEL_MAP", | |
| "apply_overlay", | |
| "augment_image", | |
| "discover_overlays", | |
| ] | |