pyvale 2025.5.3__cp311-cp311-musllinux_1_2_aarch64.whl → 2025.7.1__cp311-cp311-musllinux_1_2_aarch64.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.
- pyvale/__init__.py +12 -0
- pyvale/blendercalibrationdata.py +3 -1
- pyvale/blenderscene.py +7 -5
- pyvale/blendertools.py +27 -5
- pyvale/camera.py +1 -0
- pyvale/cameradata.py +3 -0
- pyvale/camerasensor.py +147 -0
- pyvale/camerastereo.py +4 -4
- pyvale/cameratools.py +23 -61
- pyvale/cython/rastercyth.c +1657 -1352
- pyvale/cython/rastercyth.cpython-311-aarch64-linux-musl.so +0 -0
- pyvale/cython/rastercyth.py +71 -26
- pyvale/data/DIC_Challenge_Star_Noise_Def.tiff +0 -0
- pyvale/data/DIC_Challenge_Star_Noise_Ref.tiff +0 -0
- pyvale/data/plate_hole_def0000.tiff +0 -0
- pyvale/data/plate_hole_def0001.tiff +0 -0
- pyvale/data/plate_hole_ref0000.tiff +0 -0
- pyvale/data/plate_rigid_def0000.tiff +0 -0
- pyvale/data/plate_rigid_def0001.tiff +0 -0
- pyvale/data/plate_rigid_ref0000.tiff +0 -0
- pyvale/dataset.py +96 -6
- pyvale/dic/cpp/dicbruteforce.cpp +370 -0
- pyvale/dic/cpp/dicfourier.cpp +648 -0
- pyvale/dic/cpp/dicinterpolator.cpp +559 -0
- pyvale/dic/cpp/dicmain.cpp +215 -0
- pyvale/dic/cpp/dicoptimizer.cpp +675 -0
- pyvale/dic/cpp/dicrg.cpp +137 -0
- pyvale/dic/cpp/dicscanmethod.cpp +677 -0
- pyvale/dic/cpp/dicsmooth.cpp +138 -0
- pyvale/dic/cpp/dicstrain.cpp +383 -0
- pyvale/dic/cpp/dicutil.cpp +563 -0
- pyvale/dic2d.py +164 -0
- pyvale/dic2dcpp.cpython-311-aarch64-linux-musl.so +0 -0
- pyvale/dicchecks.py +476 -0
- pyvale/dicdataimport.py +247 -0
- pyvale/dicregionofinterest.py +887 -0
- pyvale/dicresults.py +55 -0
- pyvale/dicspecklegenerator.py +238 -0
- pyvale/dicspecklequality.py +305 -0
- pyvale/dicstrain.py +387 -0
- pyvale/dicstrainresults.py +37 -0
- pyvale/errorintegrator.py +10 -8
- pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +124 -113
- pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +124 -132
- pyvale/examples/basics/ex1_3_customsens_therm3d.py +199 -195
- pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +125 -121
- pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +145 -141
- pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +96 -101
- pyvale/examples/basics/ex1_7_spatavg_therm2d.py +109 -105
- pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +92 -91
- pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +96 -90
- pyvale/examples/basics/ex2_3_sensangle_disp2d.py +88 -89
- pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +172 -171
- pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +88 -86
- pyvale/examples/basics/ex3_1_basictensors_strain2d.py +90 -90
- pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +93 -91
- pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +172 -160
- pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +154 -148
- pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +249 -231
- pyvale/examples/dic/ex1_region_of_interest.py +98 -0
- pyvale/examples/dic/ex2_plate_with_hole.py +149 -0
- pyvale/examples/dic/ex3_plate_with_hole_strain.py +93 -0
- pyvale/examples/dic/ex4_dic_blender.py +95 -0
- pyvale/examples/dic/ex5_dic_challenge.py +102 -0
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +4 -2
- pyvale/examples/renderblender/ex1_1_blenderscene.py +152 -105
- pyvale/examples/renderblender/ex1_2_blenderdeformed.py +151 -100
- pyvale/examples/renderblender/ex2_1_stereoscene.py +183 -116
- pyvale/examples/renderblender/ex2_2_stereodeformed.py +185 -112
- pyvale/examples/renderblender/ex3_1_blendercalibration.py +164 -109
- pyvale/examples/renderrasterisation/ex_rastenp.py +74 -35
- pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +6 -13
- pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +2 -2
- pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +2 -4
- pyvale/imagedef2d.py +3 -2
- pyvale/imagetools.py +137 -0
- pyvale/rastercy.py +34 -4
- pyvale/rasternp.py +300 -276
- pyvale/rasteropts.py +58 -0
- pyvale/renderer.py +47 -0
- pyvale/rendermesh.py +52 -62
- pyvale/renderscene.py +51 -0
- pyvale/sensorarrayfactory.py +2 -2
- pyvale/sensortools.py +19 -35
- pyvale/simcases/case21.i +1 -1
- pyvale/simcases/run_1case.py +8 -0
- pyvale/simtools.py +2 -2
- pyvale/visualsimplotter.py +180 -0
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/METADATA +11 -57
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/RECORD +96 -56
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/WHEEL +1 -1
- pyvale.libs/libgcc_s-69c45f16.so.1 +0 -0
- pyvale.libs/libgomp-b626072d.so.1.0.0 +0 -0
- pyvale.libs/libstdc++-1f1a71be.so.6.0.33 +0 -0
- pyvale/examples/visualisation/ex1_1_plot_traces.py +0 -102
- pyvale/examples/visualisation/ex2_1_animate_sim.py +0 -89
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.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
|