Download src/rendering/renderer.py from venki101/Venket: direct link, hf CLI and curl.
- Browser
- Download file 3.79 kB
-
https://huggingface.co/venki101/Venket/resolve/main/src/rendering/renderer.py
- Command line
-
hf download hf://venki101/Venket/src/rendering/renderer.py
-
curl -L -o renderer.py https://huggingface.co/venki101/Venket/resolve/main/src/rendering/renderer.py
3.79 kB
| """Composites the placed (and signature-augmented) RGBA signature layer onto the document. | |
| Implements the blend modes and realism passes required by the spec: multiply | |
| blend, darken blend, alpha blend, configurable ink opacity, feathering | |
| (alpha-edge softening), a slight blur to match scan resolution, sensor/scan | |
| noise, and anti-aliasing. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import cv2 | |
| import numpy as np | |
| from src.config.schema import RenderingConfig | |
| from src.utils.logging_utils import get_logger | |
| from src.utils.random_utils import SeedManager, resolve_scalar_or_range | |
| logger = get_logger(__name__) | |
| class RenderResult: | |
| """The blended document image plus the resolved rendering parameters.""" | |
| image: np.ndarray | |
| blend_mode: str | |
| ink_opacity: float | |
| blur_sigma: float | |
| feather_px: float | |
| scanner_noise: float | |
| class Renderer: | |
| """Blends a signature RGBA layer onto a document canvas with configurable realism. | |
| Example: | |
| >>> renderer = Renderer(cfg.rendering) | |
| >>> result = renderer.blend(document_rgb, signature_layer_rgba, seed_manager) | |
| """ | |
| def __init__(self, config: RenderingConfig) -> None: | |
| self.config = config | |
| def blend( | |
| self, document_rgb: np.ndarray, signature_layer_rgba: np.ndarray, seed_manager: SeedManager, | |
| ) -> RenderResult: | |
| """Blend the signature layer onto the document and return the composited image. | |
| Args: | |
| document_rgb: The scanned document canvas, ``(H, W, 3)`` uint8. | |
| signature_layer_rgba: A full-canvas-sized RGBA layer (as produced | |
| by :class:`~src.placement.placement_engine.PlacementEngine`, | |
| after any signature-level augmentation), transparent outside | |
| the signature. | |
| seed_manager: Per-sample deterministic RNG provider. | |
| Returns: | |
| A :class:`RenderResult` with the composited uint8 RGB image and | |
| the resolved rendering parameters (for metadata export). | |
| """ | |
| cfg = self.config | |
| rng = seed_manager.numpy_rng("rendering") | |
| ink_opacity = resolve_scalar_or_range(cfg.ink_opacity, rng) | |
| blur_sigma = resolve_scalar_or_range(cfg.blur_sigma, rng) | |
| feather_px = resolve_scalar_or_range(cfg.feather_px, rng) | |
| scanner_noise = resolve_scalar_or_range(cfg.scanner_noise, rng) | |
| layer = signature_layer_rgba.astype(np.float32) | |
| if feather_px > 0: | |
| layer[:, :, 3] = cv2.GaussianBlur(layer[:, :, 3], (0, 0), sigmaX=feather_px) | |
| if blur_sigma > 0: | |
| layer = cv2.GaussianBlur(layer, (0, 0), sigmaX=blur_sigma) | |
| sig_rgb = layer[:, :, :3] | |
| alpha = np.clip(layer[:, :, 3] / 255.0, 0.0, 1.0) * float(np.clip(ink_opacity, 0.0, 1.0)) | |
| alpha3 = alpha[:, :, None] | |
| base = document_rgb.astype(np.float32) | |
| if cfg.blend_mode == "multiply": | |
| blended_pixel = base * (sig_rgb / 255.0) | |
| elif cfg.blend_mode == "darken": | |
| blended_pixel = np.minimum(base, sig_rgb) | |
| else: # alpha | |
| blended_pixel = sig_rgb | |
| composited = base * (1 - alpha3) + blended_pixel * alpha3 | |
| if scanner_noise > 0: | |
| noise = rng.normal(0.0, scanner_noise * 255.0, size=composited.shape).astype(np.float32) | |
| composited += noise | |
| if cfg.anti_alias: | |
| composited = cv2.GaussianBlur(composited, (0, 0), sigmaX=0.3) | |
| out = np.clip(composited, 0, 255).astype(np.uint8) | |
| return RenderResult( | |
| image=out, blend_mode=cfg.blend_mode, ink_opacity=round(ink_opacity, 4), | |
| blur_sigma=round(blur_sigma, 4), feather_px=round(feather_px, 4), | |
| scanner_noise=round(scanner_noise, 4), | |
| ) | |