Spaces:
Build error
Build error
| import random | |
| import re | |
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw | |
| from augmenator.background_replace import replace_background as do_replace_background | |
| from augmenator.text_regions import detect_text_boxes, find_safe_rect | |
| SHAPES = ("rounded_rect", "ellipse", "rect") | |
| MAX_CUTOUT_COUNT = 4 | |
| def infer_fill(instruction: str) -> str: | |
| lowered = instruction.lower() | |
| if any(word in lowered for word in ("blur", "soft", "soften")): | |
| return "blur" | |
| return "solid" | |
| def _patch_size( | |
| image: Image.Image, | |
| strength: float, | |
| scale_w: float = 1.0, | |
| scale_h: float = 1.0, | |
| ) -> tuple[int, int]: | |
| w, h = image.size | |
| ratio = 0.18 + 0.10 * max(0.5, min(1.5, strength) - 0.5) | |
| patch_w = max(16, int(w * ratio * scale_w)) | |
| patch_h = max(16, int(h * ratio * scale_h)) | |
| return patch_w, patch_h | |
| def _varied_patch_size(image: Image.Image, strength: float) -> tuple[int, int]: | |
| scale_w = random.uniform(0.55, 1.45) | |
| scale_h = random.uniform(0.55, 1.45) | |
| return _patch_size(image, strength, scale_w=scale_w, scale_h=scale_h) | |
| def _scaled_patch_size(image: Image.Image, strength: float) -> tuple[int, int]: | |
| scale = random.uniform(0.65, 1.35) | |
| return _patch_size(image, strength, scale_w=scale, scale_h=scale) | |
| def infer_cover_count(instruction: str, explicit: int | None = None) -> int: | |
| if explicit is not None: | |
| return min(3, max(1, explicit)) | |
| return _infer_count(instruction, "cover", default=1, max_count=3) | |
| def _infer_count(instruction: str, kind: str, default: int = 1, max_count: int = 4) -> int: | |
| lowered = instruction.lower() | |
| patterns = ( | |
| rf"\b(\d+)\s+{kind}s?\b", | |
| rf"\b(\d+)\s+{kind}\b", | |
| r"\b(\d+)\s+(hole|holes|patch|patches)\b", | |
| ) | |
| for pattern in patterns: | |
| match = re.search(pattern, lowered) | |
| if match: | |
| return min(max_count, max(1, int(match.group(1)))) | |
| if any(word in lowered for word in ("several", "multiple", "many")): | |
| return random.randint(2, max_count) | |
| if "few" in lowered: | |
| return random.randint(2, min(3, max_count)) | |
| return default | |
| def _resolve_cutout_count(op: dict, instruction: str) -> int: | |
| explicit = op.get("cutout_count") | |
| if explicit is not None: | |
| return min(MAX_CUTOUT_COUNT, max(1, int(explicit))) | |
| lowered = instruction.lower() | |
| if any( | |
| phrase in lowered | |
| for phrase in ("one cutout", "a cutout", "single cutout", "one hole", "a hole") | |
| ): | |
| return 1 | |
| if re.search(r"\bone\b", lowered) and "cutout" in lowered: | |
| return 1 | |
| patterns = ( | |
| r"\b(\d+)\s+cutouts?\b", | |
| r"\b(\d+)\s+holes?\b", | |
| r"\b(\d+)\s+(hole|holes|patch|patches)\b", | |
| ) | |
| for pattern in patterns: | |
| match = re.search(pattern, lowered) | |
| if match: | |
| return min(MAX_CUTOUT_COUNT, max(1, int(match.group(1)))) | |
| if any(word in lowered for word in ("several", "multiple", "many")): | |
| return random.randint(2, MAX_CUTOUT_COUNT) | |
| return random.randint(1, MAX_CUTOUT_COUNT) | |
| def _pick_shape(instruction: str, varied: bool = False, force_varied: bool = False) -> str: | |
| if varied or force_varied: | |
| return random.choice(SHAPES) | |
| lowered = instruction.lower() | |
| if any(word in lowered for word in ("circle", "round", "oval", "ellipse")): | |
| return "ellipse" | |
| if any(word in lowered for word in ("rectangle", "square", "rect")): | |
| return "rect" | |
| return "rounded_rect" | |
| def _pick_rect( | |
| image: Image.Image, | |
| avoid_text: bool, | |
| patch_w: int, | |
| patch_h: int, | |
| ) -> tuple[tuple[int, int, int, int], int, bool]: | |
| text_boxes = detect_text_boxes(image) if avoid_text else [] | |
| if avoid_text and text_boxes: | |
| rect, used_fallback = find_safe_rect(image.size, text_boxes, patch_w, patch_h) | |
| return rect, len(text_boxes), used_fallback | |
| w, h = image.size | |
| max_x = max(0, w - patch_w) | |
| max_y = max(0, h - patch_h) | |
| x = random.randint(0, max_x) if max_x > 0 else 0 | |
| y = random.randint(0, max_y) if max_y > 0 else 0 | |
| return (x, y, patch_w, patch_h), len(text_boxes), False | |
| def cover_patch(image: Image.Image, rect: tuple[int, int, int, int], fill: str = "blur") -> Image.Image: | |
| result = image.copy() | |
| x, y, w, h = rect | |
| region = result.crop((x, y, x + w, y + h)) | |
| if fill == "blur": | |
| arr = np.array(region) | |
| blurred = cv2.GaussianBlur(arr, (0, 0), sigmaX=8, sigmaY=8) | |
| region = Image.fromarray(blurred) | |
| else: | |
| region = Image.new("RGB", (w, h), color=(140, 140, 140)) | |
| result.paste(region, (x, y)) | |
| return result | |
| def _draw_shape_mask(draw: ImageDraw.ImageDraw, xy: tuple, shape: str) -> None: | |
| x, y, w, h = xy | |
| box = (x, y, x + w, y + h) | |
| if shape == "ellipse": | |
| draw.ellipse(box, fill=255) | |
| elif shape == "rect": | |
| draw.rectangle(box, fill=255) | |
| else: | |
| radius = min(w, h) // 6 | |
| draw.rounded_rectangle(box, radius=radius, fill=255) | |
| def procedural_cutout( | |
| image: Image.Image, | |
| rect: tuple[int, int, int, int], | |
| shape: str = "rounded_rect", | |
| style: str = "solid", | |
| color: tuple[int, int, int] = (255, 200, 80), | |
| ) -> Image.Image: | |
| x, y, w, h = rect | |
| result = image.convert("RGBA") | |
| if style == "transparent": | |
| mask = Image.new("L", result.size, 0) | |
| draw = ImageDraw.Draw(mask) | |
| _draw_shape_mask(draw, (x, y, w, h), shape) | |
| arr = np.array(result) | |
| arr[..., 3] = np.where(np.array(mask) > 0, 0, arr[..., 3]) | |
| return Image.fromarray(arr) | |
| overlay = Image.new("RGBA", result.size, (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(overlay) | |
| fill = (*color, 255) | |
| outline = tuple(max(0, c - 40) for c in color) + (255,) | |
| box = (x, y, x + w, y + h) | |
| if shape == "ellipse": | |
| draw.ellipse(box, fill=fill, outline=outline, width=3) | |
| elif shape == "rect": | |
| draw.rectangle(box, fill=fill, outline=outline, width=3) | |
| else: | |
| radius = min(w, h) // 6 | |
| draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=3) | |
| return Image.alpha_composite(result, overlay) | |
| def apply_cutouts( | |
| image: Image.Image, | |
| op: dict, | |
| strength: float, | |
| instruction: str, | |
| ) -> tuple[Image.Image, list[dict], int, int, bool]: | |
| style = op.get("cutout_style", "solid") | |
| color = tuple(op.get("cutout_color", (255, 200, 80))) | |
| varied_shapes = op.get("varied_shapes", True) | |
| cutout_count = _resolve_cutout_count(op, instruction) | |
| avoid_text = op.get("avoid_text_cutouts", op.get("avoid_text", False)) | |
| result = image.copy() | |
| cutout_details: list[dict] = [] | |
| text_count = 0 | |
| used_fallback = False | |
| for _ in range(cutout_count): | |
| patch_w, patch_h = _varied_patch_size(result, strength) | |
| rect, found, fallback = _pick_rect( | |
| result, avoid_text=avoid_text, patch_w=patch_w, patch_h=patch_h | |
| ) | |
| text_count = max(text_count, found) | |
| used_fallback = used_fallback or fallback | |
| use_varied = varied_shapes and cutout_count > 1 | |
| shape = _pick_shape(instruction, varied=varied_shapes, force_varied=use_varied) | |
| result = procedural_cutout( | |
| result, | |
| rect, | |
| shape=shape, | |
| style=style, | |
| color=color, | |
| ) | |
| cutout_details.append({"rect": rect, "shape": shape, "style": style}) | |
| return result, cutout_details, cutout_count, text_count, used_fallback | |
| def apply_cover_and_cutouts( | |
| image: Image.Image, | |
| op: dict, | |
| strength: float, | |
| instruction: str, | |
| ) -> tuple[Image.Image, dict]: | |
| fill = op.get("fill") or infer_fill(instruction) | |
| avoid_text_for_covers = op.get("avoid_text", False) | |
| cover_count = infer_cover_count(instruction, op.get("cover_count")) | |
| result = image.copy() | |
| cover_rects: list[tuple[int, int, int, int]] = [] | |
| text_count = 0 | |
| used_fallback = False | |
| for _ in range(cover_count): | |
| patch_w, patch_h = _scaled_patch_size(result, strength) | |
| rect, found, fallback = _pick_rect(result, avoid_text_for_covers, patch_w, patch_h) | |
| text_count = max(text_count, found) | |
| used_fallback = used_fallback or fallback | |
| result = cover_patch(result, rect, fill=fill) | |
| cover_rects.append(rect) | |
| result, cutout_details, cutout_count, cutout_text_count, cutout_fallback = apply_cutouts( | |
| result, op, strength, instruction | |
| ) | |
| text_count = max(text_count, cutout_text_count) | |
| used_fallback = used_fallback or cutout_fallback | |
| return result, { | |
| "op": op.get("op", "cover_and_cutout"), | |
| "cover_rects": cover_rects, | |
| "cutouts": cutout_details, | |
| "fill": fill, | |
| "cutout_style": op.get("cutout_style", "solid"), | |
| "text_regions_found": text_count, | |
| "used_fallback": used_fallback, | |
| "cover_count": cover_count, | |
| "cutout_count": cutout_count, | |
| } | |
| def apply_spatial_op( | |
| image: Image.Image, | |
| op: dict, | |
| strength: float, | |
| instruction: str, | |
| ) -> tuple[Image.Image, dict]: | |
| op_name = op.get("op", "") | |
| if op_name == "replace_background": | |
| result, meta = do_replace_background( | |
| image, | |
| query=op.get("query"), | |
| instruction=instruction, | |
| ) | |
| return result, meta | |
| if op_name in ("cover_and_cutout", "cover_and_cutout_avoid_text"): | |
| op_copy = dict(op) | |
| if op_name == "cover_and_cutout_avoid_text": | |
| op_copy["avoid_text"] = True | |
| op_copy["avoid_text_cutouts"] = True | |
| op_copy["varied_shapes"] = True | |
| return apply_cover_and_cutouts(image, op_copy, strength, instruction) | |
| fill = op.get("fill") or infer_fill(instruction) | |
| avoid_text = op.get("avoid_text", op_name.endswith("avoid_text")) | |
| patch_w, patch_h = _patch_size(image, strength) | |
| rect, text_count, used_fallback = _pick_rect(image, avoid_text, patch_w, patch_h) | |
| meta = { | |
| "op": op_name, | |
| "rect": rect, | |
| "fill": fill, | |
| "cutout_style": op.get("cutout_style"), | |
| "text_regions_found": text_count, | |
| "used_fallback": used_fallback, | |
| } | |
| if op_name in ("cover_avoid_text", "cover_random", "cover"): | |
| result = cover_patch(image, rect, fill=fill) | |
| elif op_name in ("add_cutout", "cutout"): | |
| cutout_op = dict(op) | |
| if op.get("avoid_text"): | |
| cutout_op["avoid_text_cutouts"] = True | |
| result, cutout_details, cutout_count, text_count, used_fallback = apply_cutouts( | |
| image, cutout_op, strength, instruction | |
| ) | |
| meta = { | |
| "op": op_name, | |
| "cutouts": cutout_details, | |
| "cutout_count": cutout_count, | |
| "cutout_style": op.get("cutout_style", "solid"), | |
| "text_regions_found": text_count, | |
| "used_fallback": used_fallback, | |
| } | |
| else: | |
| return image, meta | |
| return result, meta | |
| def apply_spatial_ops( | |
| image: Image.Image, | |
| spatial_ops: list[dict], | |
| strength: float, | |
| instruction: str, | |
| ) -> tuple[Image.Image, list[dict]]: | |
| priority = { | |
| "replace_background": 0, | |
| "cover_and_cutout": 1, | |
| "cover_and_cutout_avoid_text": 1, | |
| "cover_avoid_text": 1, | |
| "cover_random": 1, | |
| "cover": 1, | |
| "add_cutout": 2, | |
| "cutout": 2, | |
| } | |
| ordered = sorted(spatial_ops, key=lambda op: priority.get(op.get("op", ""), 99)) | |
| result = image | |
| applied = [] | |
| for op in ordered: | |
| result, meta = apply_spatial_op(result, op, strength, instruction) | |
| applied.append(meta) | |
| return result, applied | |