File size: 3,791 Bytes
3f820d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""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__)


@dataclass
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),
        )