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

1"""Synthetic speckle pattern generation. 

2 

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""" 

9 

10from dataclasses import dataclass 

11from typing import NamedTuple 

12 

13import numpy as np 

14from scipy.ndimage import gaussian_filter 

15 

16 

17class ImageSize(NamedTuple): 

18 """Image size specification as a width and a height. 

19 

20 Attributes: 

21 width: Image width in pixels. 

22 height: Image height in pixels. 

23 """ 

24 

25 width: int 

26 height: int 

27 

28 

29@dataclass(frozen=True) 

30class RostaParameters: 

31 """Configuration parameters for Rosta pattern generation. 

32 

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 """ 

40 

41 image_size: ImageSize 

42 dot_size: float 

43 density: float 

44 smoothness: float 

45 random_seed: int 

46 name: str = "rosta" 

47 

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 ) 

59 

60 

61def rosta_pattern(rp: RostaParameters) -> np.ndarray: 

62 """Generate a Rosta speckle pattern as a 2D array. 

63 

64 Args: 

65 rp: Configuration parameters for the pattern. 

66 

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) 

73 

74 merge_sigma = rp.dot_size * minimum_side_length / 1000.0 

75 blur_sigma = rp.smoothness * minimum_side_length / 1000.0 

76 

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)) 

81 

82 noise_blurred = gaussian_filter(input=noise, sigma=merge_sigma) 

83 

84 peak_to_peak = np.ptp(noise_blurred) 

85 noise_blurred_normalized = (noise_blurred - np.min(noise_blurred)) / peak_to_peak 

86 

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] 

90 

91 clipped = np.zeros_like(noise_blurred_normalized) 

92 clipped[noise_blurred_normalized > clip_value] = 1.0 

93 

94 pattern = gaussian_filter(input=clipped, sigma=blur_sigma) * -1.0 + 1.0 

95 

96 return pattern 

97 

98 

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. 

109 

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. 

113 

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. 

121 

122 Returns: 

123 A 2D uint8 array of shape (height, width), grayscale in [0, 255]. 

124 

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)