File size: 3,465 Bytes
1a12965
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Thin, deterministic wrappers around Albumentations transforms.

Every wrapper collapses the transform's own randomized magnitude range down
to values derived from our ``strength in [0, 1]`` convention, and executes
via a freshly seeded ``A.Compose`` so results are fully reproducible under
:mod:`src.utils.random_utils` seed derivation rather than Albumentations'
global random state.
"""

from __future__ import annotations

import albumentations as A
import numpy as np


def _run(transform: "A.BasicTransform", image: np.ndarray, seed: int) -> np.ndarray:
    pipeline = A.Compose([transform], seed=seed, p=1.0)
    return pipeline(image=image)["image"]


def motion_blur(image: np.ndarray, strength: float, seed: int) -> np.ndarray:
    """Directional motion blur; ``strength`` controls kernel size (3..21 px)."""
    k = int(round(3 + strength * 18))
    k = k + 1 if k % 2 == 0 else k
    k = max(3, min(k, 21))
    return _run(A.MotionBlur(blur_limit=(k, k), p=1.0), image, seed)


def gaussian_blur(image: np.ndarray, strength: float, seed: int) -> tuple[np.ndarray, float]:
    """Isotropic Gaussian blur; ``strength`` controls sigma (0.2..4.0 px)."""
    sigma = round(0.2 + strength * 3.8, 3)
    out = _run(A.GaussianBlur(blur_limit=0, sigma_limit=(sigma, sigma), p=1.0), image, seed)
    return out, sigma


def perspective_warp(image: np.ndarray, strength: float, seed: int) -> np.ndarray:
    """Subtle perspective distortion; ``strength`` controls corner-jitter scale."""
    scale = round(0.01 + strength * 0.09, 4)
    return _run(A.Perspective(scale=(scale, scale), keep_size=True, fit_output=False, p=1.0), image, seed)


def contrast_shift(image: np.ndarray, strength: float, seed: int) -> np.ndarray:
    """Global contrast shift; ``strength`` controls the max +/- magnitude."""
    mag = round(strength * 0.45, 4)
    t = A.RandomBrightnessContrast(brightness_limit=(0.0, 0.0), contrast_limit=(-mag, mag), p=1.0)
    return _run(t, image, seed)


def gamma_shift(image: np.ndarray, strength: float, seed: int) -> tuple[np.ndarray, int]:
    """Gamma correction; ``strength`` controls deviation from gamma=100."""
    delta = int(round(strength * 45))
    lo, hi = max(100 - delta, 40), 100 + delta
    out = _run(A.RandomGamma(gamma_limit=(lo, hi), p=1.0), image, seed)
    return out, lo if lo != hi else lo


def jpeg_compression(image: np.ndarray, min_quality: int, max_quality: int, seed: int) -> tuple[np.ndarray, int]:
    """JPEG re-compression artifact simulation over ``[min_quality, max_quality]``."""
    rng = np.random.default_rng(seed)
    quality = int(rng.integers(min_quality, max_quality + 1))
    out = _run(A.ImageCompression(compression_type="jpeg", quality_range=(quality, quality), p=1.0), image, seed)
    return out, quality


def salt_and_pepper(image: np.ndarray, strength: float, seed: int) -> np.ndarray:
    """Salt-and-pepper impulse noise; ``strength`` controls the affected pixel fraction."""
    amount = round(0.002 + strength * 0.09, 5)
    t = A.SaltAndPepper(amount=(amount, amount), salt_vs_pepper=(0.45, 0.55), p=1.0)
    return _run(t, image, seed)


def sensor_noise(image: np.ndarray, strength: float, seed: int) -> np.ndarray:
    """Gaussian sensor/scanner noise; ``strength`` controls the noise std as a fraction of range."""
    std = round(0.01 + strength * 0.12, 5)
    t = A.GaussNoise(std_range=(std, std), mean_range=(0.0, 0.0), per_channel=False, p=1.0)
    return _run(t, image, seed)