File size: 1,924 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
"""Shared geometric raster transforms used by both placement and augmentation."""

from __future__ import annotations

import cv2
import numpy as np


def rotate_and_scale_rgba(rgba: np.ndarray, angle_deg: float, scale: float = 1.0) -> np.ndarray:
    """Rotate (and optionally scale) an RGBA image about its own center.

    The output canvas automatically expands to tightly bound the rotated
    rectangle so no content is clipped; newly exposed corners are fully
    transparent.
    """
    h, w = rgba.shape[:2]
    if h == 0 or w == 0:
        return rgba
    cx, cy = w / 2.0, h / 2.0
    matrix = cv2.getRotationMatrix2D((cx, cy), angle_deg, scale)
    cos, sin = abs(matrix[0, 0]), abs(matrix[0, 1])
    new_w = max(int(np.ceil(h * sin + w * cos)), 1)
    new_h = max(int(np.ceil(h * cos + w * sin)), 1)
    matrix[0, 2] += new_w / 2.0 - cx
    matrix[1, 2] += new_h / 2.0 - cy
    return cv2.warpAffine(
        rgba, matrix, (new_w, new_h),
        flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0, 0),
    )


def smooth_noise_field(shape: tuple[int, int], rng: np.random.Generator, blur_frac: float = 0.08) -> np.ndarray:
    """Generate a smooth, low-frequency noise field in ``[0, 1]`` of the given ``(h, w)`` shape.

    Built by upsampling coarse random noise, which is a fast and dependency-free
    stand-in for Perlin/simplex noise -- adequate for subtle shading effects
    like paper texture, discoloration, and ink fading.
    """
    h, w = shape
    coarse_h = max(int(h * blur_frac), 4)
    coarse_w = max(int(w * blur_frac), 4)
    coarse = rng.uniform(0.0, 1.0, size=(coarse_h, coarse_w)).astype(np.float32)
    field = cv2.resize(coarse, (w, h), interpolation=cv2.INTER_CUBIC)
    field = cv2.GaussianBlur(field, (0, 0), sigmaX=max(w, h) * 0.01 + 1)
    field -= field.min()
    max_val = field.max()
    if max_val > 1e-6:
        field /= max_val
    return field