pyvale 2025.5.3__cp311-cp311-macosx_14_0_arm64.whl → 2025.7.0__cp311-cp311-macosx_14_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyvale might be problematic. Click here for more details.

Files changed (95) hide show
  1. pyvale/.dylibs/libomp.dylib +0 -0
  2. pyvale/.dylibs/libunwind.1.0.dylib +0 -0
  3. pyvale/__init__.py +12 -0
  4. pyvale/blendercalibrationdata.py +3 -1
  5. pyvale/blenderscene.py +7 -5
  6. pyvale/blendertools.py +27 -5
  7. pyvale/camera.py +1 -0
  8. pyvale/cameradata.py +3 -0
  9. pyvale/camerasensor.py +147 -0
  10. pyvale/camerastereo.py +4 -4
  11. pyvale/cameratools.py +23 -61
  12. pyvale/cython/rastercyth.c +1657 -1352
  13. pyvale/cython/rastercyth.cpython-311-darwin.so +0 -0
  14. pyvale/cython/rastercyth.py +71 -26
  15. pyvale/data/plate_hole_def0000.tiff +0 -0
  16. pyvale/data/plate_hole_def0001.tiff +0 -0
  17. pyvale/data/plate_hole_ref0000.tiff +0 -0
  18. pyvale/data/plate_rigid_def0000.tiff +0 -0
  19. pyvale/data/plate_rigid_def0001.tiff +0 -0
  20. pyvale/data/plate_rigid_ref0000.tiff +0 -0
  21. pyvale/dataset.py +96 -6
  22. pyvale/dic/cpp/dicbruteforce.cpp +370 -0
  23. pyvale/dic/cpp/dicfourier.cpp +648 -0
  24. pyvale/dic/cpp/dicinterpolator.cpp +559 -0
  25. pyvale/dic/cpp/dicmain.cpp +215 -0
  26. pyvale/dic/cpp/dicoptimizer.cpp +675 -0
  27. pyvale/dic/cpp/dicrg.cpp +137 -0
  28. pyvale/dic/cpp/dicscanmethod.cpp +677 -0
  29. pyvale/dic/cpp/dicsmooth.cpp +138 -0
  30. pyvale/dic/cpp/dicstrain.cpp +383 -0
  31. pyvale/dic/cpp/dicutil.cpp +563 -0
  32. pyvale/dic2d.py +164 -0
  33. pyvale/dic2dcpp.cpython-311-darwin.so +0 -0
  34. pyvale/dicchecks.py +476 -0
  35. pyvale/dicdataimport.py +247 -0
  36. pyvale/dicregionofinterest.py +887 -0
  37. pyvale/dicresults.py +55 -0
  38. pyvale/dicspecklegenerator.py +238 -0
  39. pyvale/dicspecklequality.py +305 -0
  40. pyvale/dicstrain.py +387 -0
  41. pyvale/dicstrainresults.py +37 -0
  42. pyvale/errorintegrator.py +10 -8
  43. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +124 -113
  44. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +124 -132
  45. pyvale/examples/basics/ex1_3_customsens_therm3d.py +199 -195
  46. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +125 -121
  47. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +145 -141
  48. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +96 -101
  49. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +109 -105
  50. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +92 -91
  51. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +96 -90
  52. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +88 -89
  53. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +172 -171
  54. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +88 -86
  55. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +90 -90
  56. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +93 -91
  57. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +172 -160
  58. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +154 -148
  59. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +249 -231
  60. pyvale/examples/dic/ex1_region_of_interest.py +98 -0
  61. pyvale/examples/dic/ex2_plate_with_hole.py +149 -0
  62. pyvale/examples/dic/ex3_plate_with_hole_strain.py +93 -0
  63. pyvale/examples/dic/ex4_dic_blender.py +95 -0
  64. pyvale/examples/dic/ex5_dic_challenge.py +102 -0
  65. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +4 -2
  66. pyvale/examples/renderblender/ex1_1_blenderscene.py +152 -105
  67. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +151 -100
  68. pyvale/examples/renderblender/ex2_1_stereoscene.py +183 -116
  69. pyvale/examples/renderblender/ex2_2_stereodeformed.py +185 -112
  70. pyvale/examples/renderblender/ex3_1_blendercalibration.py +164 -109
  71. pyvale/examples/renderrasterisation/ex_rastenp.py +74 -35
  72. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +6 -13
  73. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +2 -2
  74. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +2 -4
  75. pyvale/imagedef2d.py +3 -2
  76. pyvale/imagetools.py +137 -0
  77. pyvale/rastercy.py +34 -4
  78. pyvale/rasternp.py +300 -276
  79. pyvale/rasteropts.py +58 -0
  80. pyvale/renderer.py +47 -0
  81. pyvale/rendermesh.py +52 -62
  82. pyvale/renderscene.py +51 -0
  83. pyvale/sensorarrayfactory.py +2 -2
  84. pyvale/sensortools.py +19 -35
  85. pyvale/simcases/case21.i +1 -1
  86. pyvale/simcases/run_1case.py +8 -0
  87. pyvale/simtools.py +2 -2
  88. pyvale/visualsimplotter.py +180 -0
  89. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/METADATA +11 -57
  90. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/RECORD +93 -57
  91. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/WHEEL +1 -1
  92. pyvale/examples/visualisation/ex1_1_plot_traces.py +0 -102
  93. pyvale/examples/visualisation/ex2_1_animate_sim.py +0 -89
  94. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/licenses/LICENSE +0 -0
  95. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/top_level.txt +0 -0
pyvale/dicresults.py ADDED
@@ -0,0 +1,55 @@
1
+ # ================================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ================================================================================
6
+
7
+
8
+ from dataclasses import dataclass
9
+ import numpy as np
10
+
11
+ @dataclass(slots=True)
12
+ class DICResults:
13
+ """
14
+ Data container for Digital Image Correlation (DIC) analysis results.
15
+
16
+ This dataclass stores the displacements, convergence info, and correlation data
17
+ associated with a DIC computation.
18
+
19
+ Attributes
20
+ ----------
21
+ ss_x : np.ndarray
22
+ The x-coordinates of the subset centers (in pixels).
23
+ ss_y : np.ndarray
24
+ The y-coordinates of the subset centers (in pixels).
25
+ u : np.ndarray
26
+ Horizontal displacements at each subset location.
27
+ v : np.ndarray
28
+ Vertical displacements at each subset location.
29
+ mag : np.ndarray
30
+ Displacement magnitude at each subset location, typically computed as sqrt(u^2 + v^2).
31
+ converged : np.ndarray
32
+ boolean value for whether the subset has converged or not.
33
+ cost : np.ndarray
34
+ Final cost or residual value from the correlation optimization (e.g., ZNSSD).
35
+ ftol : np.ndarray
36
+ Final `ftol` value from the optimization routine, indicating function tolerance.
37
+ xtol : np.ndarray
38
+ Final `xtol` value from the optimization routine, indicating solution tolerance.
39
+ niter : np.ndarray
40
+ Number of iterations taken to converge for each subset point.
41
+ filenames : list[str]
42
+ name of DIC result files that have been found
43
+ """
44
+
45
+ ss_x: np.ndarray
46
+ ss_y: np.ndarray
47
+ u: np.ndarray
48
+ v: np.ndarray
49
+ mag: np.ndarray
50
+ converged: np.ndarray
51
+ cost: np.ndarray
52
+ ftol: np.ndarray
53
+ xtol: np.ndarray
54
+ niter: np.ndarray
55
+ filenames: list[str]
@@ -0,0 +1,238 @@
1
+ # ================================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ================================================================================
6
+
7
+
8
+ from dataclasses import dataclass
9
+ import numpy as np
10
+ from scipy.ndimage import gaussian_filter
11
+ import matplotlib.pyplot as plt
12
+ import PIL
13
+
14
+
15
+ class DICSpeckleGen:
16
+ """
17
+ Dataclass holding summary information for the speckle pattern
18
+ """
19
+ def __init__(self,
20
+ seed=None,
21
+ px_vertical: int=720,
22
+ px_horizontal: int=1280,
23
+ size_radius: int=3,
24
+ size_stddev: float=0.0,
25
+ loc_variance: float=0.6,
26
+ loc_spacing: int=7,
27
+ smooth: bool=True,
28
+ smooth_stddev: float=1.0,
29
+ gray_level: int=4096,
30
+ pattern_digitisation: bool=True):
31
+
32
+ self.seed = seed
33
+ self.px_vertical = px_vertical
34
+ self.px_horizontal = px_horizontal
35
+ self.size_radius = size_radius
36
+ self.size_stddev = size_stddev
37
+ self.loc_variance = loc_variance
38
+ self.loc_spacing = loc_spacing
39
+ self.smooth = smooth
40
+ self.smooth_stddev = smooth_stddev
41
+ self.gray_level = gray_level
42
+ self.pattern_digitisation = pattern_digitisation
43
+ self.array = None
44
+
45
+ # ensure gray level is valid
46
+ if self.gray_level not in {256, 4096, 65536}:
47
+ raise ValueError("gray_level must be one of {256, 4096, 65536}")
48
+
49
+ # generate pattern and store in memory upon calling class
50
+ self.generate_array()
51
+
52
+
53
+
54
+ def generate_array(self) -> None:
55
+
56
+ """
57
+ Generate a speckle pattern based on default or user provided paramters.
58
+
59
+ Args:
60
+ None
61
+
62
+ Returns:
63
+ np.array: 2D speckle pattern.
64
+ """
65
+
66
+ # intialise speckle pattern based on datatype
67
+ pattern_dtype = np.int32 if self.pattern_digitisation else np.float64
68
+ self.array = np.zeros((self.px_vertical, self.px_horizontal), dtype=pattern_dtype)
69
+
70
+
71
+ # set random seed
72
+ np.random.seed(self.seed)
73
+
74
+ # speckles per row/col
75
+ nspeckles_x = self.px_vertical // self.loc_spacing
76
+ nspeckles_y = self.px_horizontal // self.loc_spacing
77
+
78
+ # total number of speckles
79
+ nspeckles = nspeckles_x * nspeckles_y
80
+
81
+ # uniformly spaced grid of speckles.
82
+ grid_x_uniform, grid_y_uniform = self._create_flattened_grid(nspeckles_x, nspeckles_y)
83
+
84
+ # apply random shift
85
+ low = -self.loc_variance * self.loc_spacing
86
+ high = self.loc_variance * self.loc_spacing
87
+ grid_x = self._random_shift_grid(grid_x_uniform, low, high, nspeckles)
88
+ grid_y = self._random_shift_grid(grid_y_uniform, low, high, nspeckles)
89
+
90
+
91
+ # pull speckle size from a normal distribution
92
+ radii = np.random.normal(self.size_radius, self.size_stddev, nspeckles).astype(int)
93
+
94
+ # loop over all grid points and create a circle mask. Mask then applied to pattern array.
95
+ for ii in range(0, nspeckles):
96
+ x,y,mask = self._circle_mask(grid_x[ii], grid_y[ii], radii[ii])
97
+ self.array[x[mask], y[mask]] = self.gray_level-1
98
+
99
+
100
+ # apply smoothing
101
+ if self.smooth is True:
102
+ self.array = gaussian_filter(self.array, self.smooth_stddev).astype(pattern_dtype)
103
+
104
+ return None
105
+
106
+
107
+ def get_array(self) -> np.ndarray:
108
+
109
+ return self.array
110
+
111
+
112
+
113
+ def show(self) -> None:
114
+ """
115
+ Display pattern as an image using Matplotlib.
116
+
117
+ Returns:
118
+ None
119
+ """
120
+ plt.figure()
121
+ plt.xlabel('Pixel')
122
+ plt.ylabel('Pixel')
123
+ plt.imshow(self.array,cmap='gray', vmin=0, vmax=self.gray_level-1)
124
+ plt.colorbar()
125
+ plt.show()
126
+
127
+ return None
128
+
129
+
130
+ def save(self,filename: str) -> None:
131
+ """
132
+ Save the speckle pattern array as an image with PIL package.
133
+ Image can either be saved as 8bit or 16bit image.
134
+ Image Saved in .tiff format.
135
+
136
+ Args:
137
+ filename (str): name/location of output image
138
+
139
+ Returns:
140
+ None: Saves image to directory withuser specified details.
141
+ """
142
+
143
+ if self.gray_level == 256:
144
+ image = PIL.Image.fromarray((self.array).astype(np.uint8))
145
+ elif (self.gray_level == 4096) or (self.gray_level == 65536):
146
+ image = PIL.Image.fromarray((self.array).astype(np.uint16))
147
+ else:
148
+ raise ValueError("gray_level must be one of {256, 4096, 65536}")
149
+
150
+
151
+ image.save(filename, format="TIFF")
152
+
153
+ return None
154
+
155
+
156
+
157
+
158
+ def _create_flattened_grid(self,
159
+ nspeckles_x: int,
160
+ nspeckles_y: int) -> tuple[np.ndarray,np.ndarray]:
161
+ """
162
+ Return a flattened grid for speckle locations.
163
+ Evenly spaced grid based on axis size and the no. speckles along axis.
164
+ Args:
165
+ nspeckles_x (int): Number of speckles along x-axis
166
+ nspeckles_y (int): Number of speckles along y-axis
167
+
168
+ Returns:
169
+ tuple (np.array, np.array): speckle indexes for each axis.
170
+ """
171
+
172
+ grid_x, grid_y = np.meshgrid(np.linspace(0, self.px_vertical-1, nspeckles_x),
173
+ np.linspace(0, self.px_horizontal-1, nspeckles_y))
174
+
175
+ grid_flattened_x = grid_x.flatten()
176
+ grid_flattened_y = grid_y.flatten()
177
+
178
+ return grid_flattened_x, grid_flattened_y
179
+
180
+
181
+ def _random_shift_grid(self,
182
+ grid: np.array,
183
+ low: int,
184
+ high: int,
185
+ nsamples: int) -> np.array:
186
+ """
187
+ Takes a uniformly spaced grid as input as applies a unifirm random shift to each position.
188
+ Args:
189
+ grid (np.array): grid to apply shifts
190
+ low (int): lowest possible value returned from shift
191
+ high (int): high possible value returned from shift
192
+ nsamples (int): number of speckles for shift.
193
+
194
+ Returns:
195
+ np.array: a numpy array of updated speckle locations after applying a random shift.
196
+ """
197
+
198
+ rand_shift_size = np.random.uniform(low, high, nsamples).astype(int)
199
+ updated_grid = grid.astype(int) + rand_shift_size
200
+
201
+ return updated_grid
202
+
203
+
204
+
205
+
206
+
207
+ def _circle_mask(self,
208
+ pos_x: int,
209
+ pos_y: int,
210
+ radius: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
211
+ """
212
+ Generates a circular mask centered at speckle location position with a given radius.
213
+ The mask is applied within image bounds.
214
+
215
+ Args:
216
+ pos_x (int): The x-coordinate of the center of the circle.
217
+ pos_y (int): The y-coordinate of the center of the circle.
218
+ radius (int): The radius of the circle (in pixels).
219
+
220
+ Returns:
221
+ tuple: A tuple containing:
222
+ - x (np.ndarray): The x-coordinates of mask region.
223
+ - y (np.ndarray): The y-coordinates of mask region.
224
+ - mask (np.ndarray): Bool array containing speckle area.
225
+ """
226
+
227
+ min_x = max(pos_x - radius, 0)
228
+ min_y = max(pos_y - radius, 0)
229
+ max_x = min(pos_x + radius + 1, self.px_vertical)
230
+ max_y = min(pos_y + radius + 1, self.px_horizontal)
231
+
232
+ # Generate mesh grid of possible (xx, yy) points
233
+ x, y = np.meshgrid(np.arange(min_x, max_x), np.arange(min_y, max_y))
234
+
235
+ # Update the pattern for points inside the circle's radius
236
+ mask = (x - pos_x)**2 + (y - pos_y)**2 <= radius**2
237
+
238
+ return x, y, mask
@@ -0,0 +1,305 @@
1
+ # ================================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ================================================================================
6
+
7
+
8
+ import math
9
+ import numpy as np
10
+ import matplotlib.pyplot as plt
11
+ import cv2
12
+ import scipy.ndimage as ndi
13
+ from numba import jit
14
+
15
+
16
+
17
+ class DICSpeckleQuality:
18
+
19
+ def __init__(self, pattern: np.ndarray, subset_size: int, subset_step: int, gray_level: int):
20
+ self.pattern = pattern
21
+ self.subset_size = subset_size
22
+ self.subset_step = subset_step
23
+ self.gray_level = gray_level
24
+
25
+ # Internal cache for speckle sizes
26
+ self._speckle_sizes = None
27
+ self._subset_average = None
28
+ self._xvalues = None
29
+ self._yvalues = None
30
+
31
+ #TODO: regoin of interest for staticistics
32
+ # this needs to be a 'sub' array of the overall image
33
+
34
+
35
+
36
+ def mean_intensity_gradient(self) -> float:
37
+ """
38
+ Mean Intensity Gradient. Based on the below:
39
+ https://www.sciencedirect.com/science/article/abs/pii/S0143816613001103
40
+
41
+ Returns:
42
+ mean_intensity_gradient (float): float value for mean_intensity gradient
43
+ """
44
+
45
+ gradient_x, gradient_y = np.gradient(self.pattern)
46
+
47
+ # mag
48
+ gradient_magnitude = np.sqrt(gradient_x**2 + gradient_y**2)
49
+
50
+ # plot for debugging
51
+ plt.figure()
52
+ plt.imshow(gradient_magnitude)
53
+ plt.colorbar(label='Magnitude')
54
+ plt.show()
55
+
56
+ #get mean of 2d array.
57
+ mean_gradient = np.mean(gradient_magnitude)
58
+
59
+ return mean_gradient
60
+
61
+
62
+ def shannon_entropy(self) -> float:
63
+ """
64
+ shannon entropy for speckle patterns. Based on the below:
65
+ https://www.sciencedirect.com/science/article/abs/pii/S0030402615007950
66
+
67
+
68
+ Returns:
69
+ shannon_entropy (float): float value for shannon entropy
70
+ """
71
+
72
+ #count occurances of each value. bincount doesn't like 2d arrays. flatten to 1d.
73
+ bins = np.bincount(self.pattern.flatten()) / self.pattern.size
74
+
75
+ # reset shannon_entropy
76
+ shannon_entropy = 0.0
77
+
78
+ # loop over gray leves
79
+ for i in range(0,2):
80
+ shannon_entropy -= bins[i] * math.log2(bins[i])
81
+
82
+ return shannon_entropy
83
+
84
+ def gray_level_histogram(self) -> None:
85
+ """
86
+ Count the number of occurrences of each gray value.
87
+ plot results as a histogram
88
+ """
89
+
90
+ # Count occurrences of each gray value
91
+ unique_values, counts = np.unique(self.pattern, return_counts=True)
92
+
93
+ # Plot histogram
94
+ plt.figure(figsize=(8, 5))
95
+ plt.bar(unique_values, counts, width=1.0, color='gray', edgecolor='black')
96
+ plt.title('Histogram of Gray Levels')
97
+ plt.xlabel('Gray Level (0-255)')
98
+ plt.ylabel('Count')
99
+ plt.grid(axis='y', linestyle='--', alpha=0.7)
100
+ plt.show()
101
+
102
+ return None
103
+
104
+
105
+
106
+
107
+
108
+ def speckle_size(self) -> tuple[int, np.ndarray, np.ndarray]:
109
+ """
110
+ Calculates the Speckle sizes using a binary map calculaed from otsu threshholding
111
+ (https://learnopencv.com/otsu-thresholding-with-opencv/)
112
+
113
+
114
+ Returns:
115
+ tuple containing:
116
+ num_speckles (int): total number of speckles identified in the binary map
117
+ equivalent_diameters (np.ndarray): Speckle diameter if circle with same area
118
+ labeled_speckles (np.ndarray): Label of the connected elements within speckle
119
+ """
120
+
121
+ # calculate binary map using otsu thresholding with opencv
122
+ _, binary_image = cv2.threshold(self.pattern,
123
+ 0,
124
+ self.gray_level - 1,
125
+ cv2.THRESH_BINARY + cv2.THRESH_OTSU)
126
+
127
+ # Label connected components (speckles)
128
+ labeled_speckles, num_speckles = ndi.label(binary_image)
129
+ speckle_sizes = np.array(ndi.sum(binary_image > 0,
130
+ labeled_speckles,
131
+ index=np.arange(1, num_speckles + 1)))
132
+
133
+ equivalent_diameters = 2 * np.sqrt(speckle_sizes / np.pi)
134
+
135
+ # assign values to cached tuple
136
+ self._speckle_sizes = (num_speckles, equivalent_diameters, labeled_speckles)
137
+
138
+ # Raise exception if there's no speckles
139
+ if num_speckles == 0:
140
+ raise ValueError("No speckles identified.")
141
+
142
+ return self._speckle_sizes
143
+
144
+
145
+ def speckle_size_plot(self) -> None:
146
+
147
+ # get speckle sizes if not computed already
148
+ if self._speckle_sizes is None:
149
+ self.speckle_size()
150
+
151
+ # assign each speckle to a classification group.
152
+ # Group is jst the 'size' unsure whether to bin to discrete sizes
153
+ classifications = self._classify_speckles()
154
+
155
+ # plotting
156
+ fig, axes = plt.subplots(1, 2, figsize=(12, 6), sharex=True, sharey=True)
157
+
158
+ im1 = axes[0].imshow(self.pattern, cmap='gray', vmin=0, vmax=255)
159
+ axes[0].set_title("Speckle Pattern")
160
+ axes[0].axis("off")
161
+ fig.colorbar(im1,ax=axes[0],fraction=0.046, pad=0.04)
162
+
163
+
164
+ im2 = axes[1].imshow(classifications, cmap="turbo", vmin=0, vmax=15)
165
+ axes[1].set_title("Speckle Size")
166
+ axes[1].axis("off")
167
+ fig.colorbar(im2,ax=axes[1],fraction=0.046, pad=0.04)
168
+
169
+ plt.show()
170
+
171
+ return None
172
+
173
+
174
+ def _classify_speckles(self) -> np.ndarray:
175
+ """
176
+ Calculates the Speckle sizes using a binary map calculaed from otsu threshholding
177
+ (https://learnopencv.com/otsu-thresholding-with-opencv/)
178
+
179
+
180
+ Returns:
181
+ classifications (np.ndarray): speckle sizes classified by bin sizes for plots.
182
+ To discuss which bins are appropriate.
183
+ My proposed bins:
184
+ 0-3 small, 3-5 ideal, 5 < big.
185
+ """
186
+
187
+
188
+ num_speckles, speckle_sizes, labeled_speckles = self._speckle_sizes
189
+ classifications = np.zeros_like(labeled_speckles, dtype=np.uint8)
190
+
191
+ #TODO: Not sure whether to bin into three catagorories:
192
+ # 0-3 kinda small, 3-5 ideal, 5 < kinda big.
193
+ # I'm leaving the logic in to deal with this but going to assume continous is probs best
194
+ for i in range(1, num_speckles + 1):
195
+ size = speckle_sizes[i - 1]
196
+ if size <= 3:
197
+ classifications[labeled_speckles == i] = size #1
198
+ elif 3 < size <= 5:
199
+ classifications[labeled_speckles == i] = size #3
200
+ else:
201
+ classifications[labeled_speckles == i] = size #2
202
+
203
+ return classifications
204
+
205
+
206
+ def balance_subset(self) -> np.ndarray:
207
+
208
+ # dont use subsets if rows/cols < edge_cutoff
209
+ edge_cutoff = 100
210
+
211
+ min_x = self.subset_size // 2
212
+ min_y = self.subset_size // 2
213
+ max_x = self.pattern.shape[1] - self.subset_size // 2
214
+ max_y = self.pattern.shape[0] - self.subset_size // 2
215
+
216
+ # image coordiantes array containing the central pixel for each subset
217
+ self._xvalues = np.arange(min_x+edge_cutoff, max_x-edge_cutoff, self.subset_step)
218
+ self._yvalues = np.arange(min_y+edge_cutoff, max_y-edge_cutoff, self.subset_step)
219
+
220
+ # init array to store black/white balance value
221
+ shape = (len(self._yvalues), len(self._xvalues))
222
+ self._subset_average = np.zeros(shape)
223
+
224
+
225
+ # looping over the subsets
226
+ for i, x in enumerate(self._xvalues):
227
+ for j, y in enumerate(self._yvalues):
228
+
229
+ subset = extract_subset(self.pattern, x, y, self.subset_size)
230
+
231
+ # plt.figure()
232
+ # plt.imshow(subset)
233
+ # plt.show()
234
+
235
+ self._subset_average[j,i] = np.average(subset) / self.gray_level
236
+
237
+ return self._subset_average
238
+
239
+
240
+ def balance_image(self) -> float:
241
+
242
+ avg = np.mean(self.pattern) / self.gray_level
243
+
244
+ return avg
245
+
246
+ def balance_subset_avg(self) -> float:
247
+
248
+ if self._subset_average is None:
249
+ self.balance_subset()
250
+
251
+ subset_avg = np.mean(self._subset_average)
252
+
253
+ return subset_avg
254
+
255
+
256
+
257
+ def balance_subset_plot(self) -> None:
258
+
259
+ if self._subset_average is None:
260
+ self.balance_subset()
261
+
262
+ plt.figure(figsize=(10, 10))
263
+ plt.imshow(self.pattern, cmap='gray', interpolation='none')
264
+ extent = [self._xvalues[0], self._xvalues[-1], self._yvalues[-1], self._yvalues[0]] # Match coordinates
265
+ plt.imshow(self._subset_average, cmap='jet', alpha=0.3, extent=extent, interpolation='none')
266
+ plt.xlim(0,self.pattern.shape[1])
267
+ plt.ylim(self.pattern.shape[0],0)
268
+ plt.colorbar(label='Normalized Subset Average')
269
+ plt.title("Black/White Balance Overlay")
270
+ plt.show()
271
+
272
+ return None
273
+
274
+
275
+
276
+ #TODO: This is going to become c++ at some point.
277
+ # I think this is OK to keep in python for calculation of black/white balance
278
+ @jit(nopython=True)
279
+ def extract_subset(image: np.ndarray, x: int, y: int, subset_size: int) -> np.ndarray:
280
+ """
281
+ Parameters
282
+ x (int): x-coord of subset center in image
283
+ y (int): y-coord of subset center in image
284
+
285
+ """
286
+
287
+ half_size = subset_size // 2
288
+
289
+ # reference image subset
290
+ x1, x2 = x - half_size, x + half_size + 1
291
+ y1, y2 = y - half_size, y + half_size + 1
292
+
293
+ # Ensure indices are within bounds
294
+ #TODO: Update this when implementing ROI
295
+ if (x1 < 0 or y1 < 0 or x2 > image.shape[1] or y2 > image.shape[0]):
296
+ raise ValueError(f"Subset exceeds image boundaries.\nSubset Pixel Range:\n"
297
+ f"x1: {x1}\n"
298
+ f"x2: {x2}\n"
299
+ f"y1: {y1}\n"
300
+ f"y2: {y2}")
301
+
302
+ # Extract subsets
303
+ subset = image[y1:y2, x1:x2]
304
+
305
+ return subset