Coverage for src/dictk/rosta.py: 100%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-27 23:53 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-27 23:53 +0000
1"""Synthetic speckle pattern generation.
3Implements the Rosta algorithm described by Olufsen SN, Andersen ME,
4Fagerholt E. "muDIC: An open-source toolkit for digital image
5correlation." SoftwareX. 2020 Jan 1;11:100391, Algorithm 1, page 6.
6https://doi.org/10.1016/j.softx.2019.100391
7https://github.com/PolymerGuy/muDIC
8"""
10from dataclasses import dataclass
11from typing import NamedTuple
13import numpy as np
14from scipy.ndimage import gaussian_filter
17class ImageSize(NamedTuple):
18 """Image size specification as a width and a height.
20 Attributes:
21 width: Image width in pixels.
22 height: Image height in pixels.
23 """
25 width: int
26 height: int
29@dataclass(frozen=True)
30class RostaParameters:
31 """Configuration parameters for Rosta pattern generation.
33 Attributes:
34 image_size: Target image dimensions.
35 dot_size: Size factor for pattern dots (0.0 to 100.0).
36 density: Density of pattern elements (0.0 to 1.0).
37 smoothness: Smoothness factor for final blur (0.0 to 100.0).
38 random_seed: Seed for reproducible random number generation.
39 """
41 image_size: ImageSize
42 dot_size: float
43 density: float
44 smoothness: float
45 random_seed: int
46 name: str = "rosta"
48 def __post_init__(self):
49 if not 0.0 < self.dot_size < 100.0:
50 raise ValueError(
51 f"dot_size {self.dot_size} must be in the range (0.0, 100.0)"
52 )
53 if not 0.0 < self.density < 1.0:
54 raise ValueError(f"density {self.density} must be in the range (0.0, 1.0)")
55 if not 0.0 < self.smoothness < 100.0:
56 raise ValueError(
57 f"smoothness {self.smoothness} must be in the range (0.0, 100.0)"
58 )
61def rosta_pattern(rp: RostaParameters) -> np.ndarray:
62 """Generate a Rosta speckle pattern as a 2D array.
64 Args:
65 rp: Configuration parameters for the pattern.
67 Returns:
68 A 2D NumPy array of shape (rp.image_size.height, rp.image_size.width),
69 normalized grayscale values in the range [0.0, 1.0].
70 """
71 # Characteristic length: the minimum image dimension
72 minimum_side_length = min(rp.image_size.width, rp.image_size.height)
74 merge_sigma = rp.dot_size * minimum_side_length / 1000.0
75 blur_sigma = rp.smoothness * minimum_side_length / 1000.0
77 rng = np.random.default_rng(rp.random_seed)
78 # Explicit height x width call: numpy's random shape convention is
79 # ambiguous unless the axis order is spelled out.
80 noise = rng.standard_normal(size=(rp.image_size.height, rp.image_size.width))
82 noise_blurred = gaussian_filter(input=noise, sigma=merge_sigma)
84 peak_to_peak = np.ptp(noise_blurred)
85 noise_blurred_normalized = (noise_blurred - np.min(noise_blurred)) / peak_to_peak
87 sorted_gray_scales = np.sort(noise_blurred_normalized.flatten())
88 clip_index = int(rp.density * np.size(sorted_gray_scales))
89 clip_value = sorted_gray_scales[clip_index]
91 clipped = np.zeros_like(noise_blurred_normalized)
92 clipped[noise_blurred_normalized > clip_value] = 1.0
94 pattern = gaussian_filter(input=clipped, sigma=blur_sigma) * -1.0 + 1.0
96 return pattern
99def rosta(
100 *,
101 width: int = 200,
102 height: int = 200,
103 dot_size: float = 4.0,
104 density: float = 0.32,
105 smoothness: float = 2.0,
106 random_seed: int = 42,
107) -> np.ndarray:
108 """Generate a Rosta speckle pattern as a uint8 grayscale image array.
110 Same parameters and pixel values as the `dictk rosta` CLI command, minus
111 the file write — see the `dictk` package docstring for why the API
112 layer stops at the array.
114 Args:
115 width: Image width in pixels.
116 height: Image height in pixels.
117 dot_size: Size factor for pattern dots (0.0 to 100.0).
118 density: Density of pattern elements (0.0 to 1.0).
119 smoothness: Smoothness factor for final blur (0.0 to 100.0).
120 random_seed: Seed for reproducible random number generation.
122 Returns:
123 A 2D uint8 array of shape (height, width), grayscale in [0, 255].
125 Raises:
126 ValueError: If dot_size, density, or smoothness is out of range.
127 """
128 rp = RostaParameters(
129 image_size=ImageSize(width=width, height=height),
130 dot_size=dot_size,
131 density=density,
132 smoothness=smoothness,
133 random_seed=random_seed,
134 )
135 pattern = rosta_pattern(rp)
136 return (pattern * 255).astype(np.uint8)