Download src/placement/placement_engine.py from InternsProgressUpdate/Venket: direct link, hf CLI and curl.
- Browser
- Download file 7.71 kB
-
https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/src/placement/placement_engine.py
- Command line
-
hf download hf://InternsProgressUpdate/Venket/src/placement/placement_engine.py
-
curl -L -o placement_engine.py https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/src/placement/placement_engine.py
7.71 kB
| """Places a preprocessed signature stamp inside (or intentionally near) a target signature box. | |
| Handles aspect-ratio-preserving scale-to-fit, rotation, anchor-based | |
| positioning, sub-pixel offsets, and controlled "misplacement" (an | |
| intentional, probability-gated extra translation used to generate | |
| hard/misaligned samples for benchmarking). | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import cv2 | |
| import numpy as np | |
| from src.config.schema import PlacementConfig | |
| from src.utils.geometry_utils import Box | |
| from src.utils.logging_utils import get_logger | |
| from src.utils.random_utils import SeedManager, resolve_scalar_or_range | |
| from src.utils.transform_utils import rotate_and_scale_rgba | |
| logger = get_logger(__name__) | |
| AnchorOffsetFn = "dict[str, callable]" | |
| MIN_SIGNATURE_HEIGHT_PX = 16.0 | |
| class PlacementResult: | |
| """Everything downstream stages need to know about how the signature was placed.""" | |
| layer: np.ndarray # full-canvas-sized RGBA layer, transparent outside the signature | |
| target_box: Box | |
| intended_bbox: Box # the signature's transformed bounding box in canvas coordinates | |
| x_offset: float | |
| y_offset: float | |
| rotation_deg: float | |
| scale_factor: float | |
| anchor_point: str | |
| misplaced: bool | |
| misplacement_dx: float | |
| misplacement_dy: float | |
| def _anchor_top_left(anchor: str, box_w: float, box_h: float, sig_w: float, sig_h: float) -> tuple[float, float]: | |
| positions = { | |
| "center": ((box_w - sig_w) / 2.0, (box_h - sig_h) / 2.0), | |
| "top_left": (0.0, 0.0), | |
| "top_right": (box_w - sig_w, 0.0), | |
| "bottom_left": (0.0, box_h - sig_h), | |
| "bottom_right": (box_w - sig_w, box_h - sig_h), | |
| "top_center": ((box_w - sig_w) / 2.0, 0.0), | |
| "bottom_center": ((box_w - sig_w) / 2.0, box_h - sig_h), | |
| "left_center": (0.0, (box_h - sig_h) / 2.0), | |
| "right_center": (box_w - sig_w, (box_h - sig_h) / 2.0), | |
| } | |
| return positions.get(anchor, positions["center"]) | |
| class PlacementEngine: | |
| """Fits, transforms, and positions a signature stamp relative to a target box. | |
| Example: | |
| >>> engine = PlacementEngine(cfg.placement) | |
| >>> result = engine.place(signature_rgba, target_box=(100, 100, 300, 160), | |
| ... canvas_size=(1024, 559), seed_manager=sm) | |
| """ | |
| def __init__(self, config: PlacementConfig) -> None: | |
| self.config = config | |
| def place( | |
| self, | |
| signature_rgba: np.ndarray, | |
| target_box: Box, | |
| canvas_size: tuple[int, int], | |
| seed_manager: SeedManager, | |
| ) -> PlacementResult: | |
| """Compute the placed signature layer for one sample. | |
| Args: | |
| signature_rgba: The tightly-cropped RGBA signature stamp from | |
| :class:`~src.rendering.signature_preprocessor.SignaturePreprocessor`. | |
| target_box: The ``(x1, y1, x2, y2)`` target signature field, in | |
| canvas pixel coordinates. | |
| canvas_size: ``(width, height)`` of the full document canvas. | |
| seed_manager: Per-sample deterministic RNG provider. | |
| Returns: | |
| A :class:`PlacementResult` with the full-canvas RGBA layer ready | |
| for blending, plus every resolved placement parameter for | |
| metadata export. | |
| """ | |
| cfg = self.config | |
| rng = seed_manager.numpy_rng("placement") | |
| canvas_w, canvas_h = canvas_size | |
| rotation_deg = resolve_scalar_or_range(cfg.rotation_deg, rng) | |
| scale_factor = resolve_scalar_or_range(cfg.scale_factor, rng) | |
| x_offset = resolve_scalar_or_range(cfg.x_offset, rng) | |
| y_offset = resolve_scalar_or_range(cfg.y_offset, rng) | |
| x1, y1, x2, y2 = target_box | |
| box_w, box_h = (x2 - x1), (y2 - y1) | |
| margin_x, margin_y = box_w * cfg.margin, box_h * cfg.margin | |
| inner_x1, inner_y1 = x1 + margin_x, y1 + margin_y | |
| inner_w = max(box_w - 2 * margin_x, 1.0) | |
| inner_h = max(box_h - 2 * margin_y, 1.0) | |
| rotated = rotate_and_scale_rgba(signature_rgba, rotation_deg, 1.0) | |
| sig_h, sig_w = rotated.shape[:2] | |
| fit_scale = min(inner_w / max(sig_w, 1), inner_h / max(sig_h, 1)) | |
| final_scale = max(fit_scale * scale_factor, 1e-3) | |
| # Never let the signature shrink below a legible minimum height, even | |
| # inside a very short box (real "sign here" lines are often only | |
| # 12-20px tall at document resolution) -- a genuinely tiny box should | |
| # make the signature overflow it, not vanish into sub-pixel noise. | |
| min_height_scale = MIN_SIGNATURE_HEIGHT_PX / max(sig_h, 1) | |
| final_scale = max(final_scale, min_height_scale) | |
| placed = cv2.resize( | |
| rotated, (max(int(round(sig_w * final_scale)), 1), max(int(round(sig_h * final_scale)), 1)), | |
| interpolation=cv2.INTER_AREA if final_scale < 1.0 else cv2.INTER_CUBIC, | |
| ) | |
| placed_h, placed_w = placed.shape[:2] | |
| ax, ay = _anchor_top_left(cfg.anchor_point, inner_w, inner_h, placed_w, placed_h) | |
| base_x = inner_x1 + ax + x_offset | |
| base_y = inner_y1 + ay + y_offset | |
| misplaced = bool(rng.uniform(0.0, 1.0) < cfg.misplacement_probability) | |
| misplacement_dx = misplacement_dy = 0.0 | |
| if misplaced: | |
| strength = resolve_scalar_or_range(cfg.misplacement_strength, rng) | |
| max_shift = strength * max(box_w, box_h) | |
| angle = rng.uniform(0, 2 * np.pi) | |
| radius = rng.uniform(0.3, 1.0) * max_shift | |
| misplacement_dx = float(np.cos(angle) * radius) | |
| misplacement_dy = float(np.sin(angle) * radius) | |
| final_x = base_x + misplacement_dx | |
| final_y = base_y + misplacement_dy | |
| if not cfg.allow_partial_outside and not misplaced: | |
| final_x = float(np.clip(final_x, inner_x1, inner_x1 + max(inner_w - placed_w, 0))) | |
| final_y = float(np.clip(final_y, inner_y1, inner_y1 + max(inner_h - placed_h, 0))) | |
| # Hard safety clamp: never let the signature drift entirely off the physical page. | |
| final_x = float(np.clip(final_x, -placed_w * 0.6, canvas_w - placed_w * 0.4)) | |
| final_y = float(np.clip(final_y, -placed_h * 0.6, canvas_h - placed_h * 0.4)) | |
| layer = np.zeros((canvas_h, canvas_w, 4), dtype=np.uint8) | |
| self._paste(layer, placed, int(round(final_x)), int(round(final_y))) | |
| intended_bbox: Box = (final_x, final_y, final_x + placed_w, final_y + placed_h) | |
| return PlacementResult( | |
| layer=layer, | |
| target_box=target_box, | |
| intended_bbox=intended_bbox, | |
| x_offset=x_offset, | |
| y_offset=y_offset, | |
| rotation_deg=rotation_deg, | |
| scale_factor=scale_factor, | |
| anchor_point=cfg.anchor_point, | |
| misplaced=misplaced, | |
| misplacement_dx=misplacement_dx, | |
| misplacement_dy=misplacement_dy, | |
| ) | |
| def _paste(canvas: np.ndarray, sprite: np.ndarray, x: int, y: int) -> None: | |
| """Alpha-aware paste of ``sprite`` onto ``canvas`` at ``(x, y)``, clipped to bounds.""" | |
| canvas_h, canvas_w = canvas.shape[:2] | |
| sprite_h, sprite_w = sprite.shape[:2] | |
| dst_x1, dst_y1 = max(x, 0), max(y, 0) | |
| dst_x2, dst_y2 = min(x + sprite_w, canvas_w), min(y + sprite_h, canvas_h) | |
| if dst_x2 <= dst_x1 or dst_y2 <= dst_y1: | |
| logger.warning("Placed signature falls entirely outside the canvas; skipping paste.") | |
| return | |
| src_x1, src_y1 = dst_x1 - x, dst_y1 - y | |
| src_x2, src_y2 = src_x1 + (dst_x2 - dst_x1), src_y1 + (dst_y2 - dst_y1) | |
| canvas[dst_y1:dst_y2, dst_x1:dst_x2] = sprite[src_y1:src_y2, src_x1:src_x2] | |