| """Placement: find low-variance regions and resolve a position to a pixel box. |
| |
| Low-variance regions (sky, walls, uniform backgrounds) hide faint text best and |
| also give the VLM a clean patch to read. We rank a small grid of candidate |
| boxes by local variance and a position prior. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| from PIL import Image |
|
|
| from veil_pgd.types import Position |
|
|
| REFERENCE = 1024 |
|
|
|
|
| def _position_anchor(position: Position, w: int, h: int, bw: int, bh: int) -> tuple[int, int]: |
| """Top-left pixel for a text box of size (bw, bh) at a named position.""" |
| pad = int(0.03 * max(w, h)) |
| xmid = (w - bw) // 2 |
| ymid = (h - bh) // 2 |
| table = { |
| "center": (xmid, ymid), |
| "top_middle": (xmid, pad), |
| "bottom_middle": (xmid, h - bh - pad), |
| "top_left": (pad, pad), |
| "top_right": (w - bw - pad, pad), |
| "bottom_left": (pad, h - bh - pad), |
| "bottom_right": (w - bw - pad, h - bh - pad), |
| } |
| return table.get(position, table["bottom_middle"]) |
|
|
|
|
| def resolve_box( |
| image: Image.Image, position: Position, box_w: int, box_h: int |
| ) -> tuple[int, int, int, int]: |
| """Return (x0, y0, x1, y1) for the overlay text box, clamped to the image.""" |
| w, h = image.size |
| x0, y0 = _position_anchor(position, w, h, box_w, box_h) |
| x0 = max(0, min(x0, w - box_w)) |
| y0 = max(0, min(y0, h - box_h)) |
| return x0, y0, x0 + box_w, y0 + box_h |
|
|
|
|
| def region_variance(image: Image.Image, box: tuple[int, int, int, int]) -> float: |
| """Mean per-channel variance inside a box (lower = flatter = stealthier).""" |
| crop = np.asarray(image.convert("RGB").crop(box), dtype=np.float32) |
| if crop.size == 0: |
| return float("inf") |
| return float(crop.reshape(-1, 3).var(axis=0).mean()) |
|
|
|
|
| def rank_positions( |
| image: Image.Image, positions: list[Position], box_w: int, box_h: int |
| ) -> list[tuple[Position, float]]: |
| """Rank candidate positions by ascending region variance.""" |
| scored = [] |
| for p in positions: |
| box = resolve_box(image, p, box_w, box_h) |
| scored.append((p, region_variance(image, box))) |
| return sorted(scored, key=lambda kv: kv[1]) |
|
|
|
|
| def scale_px(font_px: int, image: Image.Image) -> int: |
| """Scale a reference (1024px) font size to the true image's short side.""" |
| short = min(image.size) |
| return max(6, round(font_px * short / REFERENCE)) |
|
|