pyvale 2025.5.3__cp311-cp311-win_amd64.whl → 2025.7.0__cp311-cp311-win_amd64.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 (93) hide show
  1. pyvale/__init__.py +12 -0
  2. pyvale/blendercalibrationdata.py +3 -1
  3. pyvale/blenderscene.py +7 -5
  4. pyvale/blendertools.py +27 -5
  5. pyvale/camera.py +1 -0
  6. pyvale/cameradata.py +3 -0
  7. pyvale/camerasensor.py +147 -0
  8. pyvale/camerastereo.py +4 -4
  9. pyvale/cameratools.py +23 -61
  10. pyvale/cython/rastercyth.c +1657 -1352
  11. pyvale/cython/rastercyth.cp311-win_amd64.pyd +0 -0
  12. pyvale/cython/rastercyth.py +71 -26
  13. pyvale/data/plate_hole_def0000.tiff +0 -0
  14. pyvale/data/plate_hole_def0001.tiff +0 -0
  15. pyvale/data/plate_hole_ref0000.tiff +0 -0
  16. pyvale/data/plate_rigid_def0000.tiff +0 -0
  17. pyvale/data/plate_rigid_def0001.tiff +0 -0
  18. pyvale/data/plate_rigid_ref0000.tiff +0 -0
  19. pyvale/dataset.py +96 -6
  20. pyvale/dic/cpp/dicbruteforce.cpp +370 -0
  21. pyvale/dic/cpp/dicfourier.cpp +648 -0
  22. pyvale/dic/cpp/dicinterpolator.cpp +559 -0
  23. pyvale/dic/cpp/dicmain.cpp +215 -0
  24. pyvale/dic/cpp/dicoptimizer.cpp +675 -0
  25. pyvale/dic/cpp/dicrg.cpp +137 -0
  26. pyvale/dic/cpp/dicscanmethod.cpp +677 -0
  27. pyvale/dic/cpp/dicsmooth.cpp +138 -0
  28. pyvale/dic/cpp/dicstrain.cpp +383 -0
  29. pyvale/dic/cpp/dicutil.cpp +563 -0
  30. pyvale/dic2d.py +164 -0
  31. pyvale/dic2dcpp.cp311-win_amd64.pyd +0 -0
  32. pyvale/dicchecks.py +476 -0
  33. pyvale/dicdataimport.py +247 -0
  34. pyvale/dicregionofinterest.py +887 -0
  35. pyvale/dicresults.py +55 -0
  36. pyvale/dicspecklegenerator.py +238 -0
  37. pyvale/dicspecklequality.py +305 -0
  38. pyvale/dicstrain.py +387 -0
  39. pyvale/dicstrainresults.py +37 -0
  40. pyvale/errorintegrator.py +10 -8
  41. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +124 -113
  42. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +124 -132
  43. pyvale/examples/basics/ex1_3_customsens_therm3d.py +199 -195
  44. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +125 -121
  45. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +145 -141
  46. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +96 -101
  47. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +109 -105
  48. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +92 -91
  49. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +96 -90
  50. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +88 -89
  51. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +172 -171
  52. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +88 -86
  53. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +90 -90
  54. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +93 -91
  55. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +172 -160
  56. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +154 -148
  57. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +249 -231
  58. pyvale/examples/dic/ex1_region_of_interest.py +98 -0
  59. pyvale/examples/dic/ex2_plate_with_hole.py +149 -0
  60. pyvale/examples/dic/ex3_plate_with_hole_strain.py +93 -0
  61. pyvale/examples/dic/ex4_dic_blender.py +95 -0
  62. pyvale/examples/dic/ex5_dic_challenge.py +102 -0
  63. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +4 -2
  64. pyvale/examples/renderblender/ex1_1_blenderscene.py +152 -105
  65. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +151 -100
  66. pyvale/examples/renderblender/ex2_1_stereoscene.py +183 -116
  67. pyvale/examples/renderblender/ex2_2_stereodeformed.py +185 -112
  68. pyvale/examples/renderblender/ex3_1_blendercalibration.py +164 -109
  69. pyvale/examples/renderrasterisation/ex_rastenp.py +74 -35
  70. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +6 -13
  71. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +2 -2
  72. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +2 -4
  73. pyvale/imagedef2d.py +3 -2
  74. pyvale/imagetools.py +137 -0
  75. pyvale/rastercy.py +34 -4
  76. pyvale/rasternp.py +300 -276
  77. pyvale/rasteropts.py +58 -0
  78. pyvale/renderer.py +47 -0
  79. pyvale/rendermesh.py +52 -62
  80. pyvale/renderscene.py +51 -0
  81. pyvale/sensorarrayfactory.py +2 -2
  82. pyvale/sensortools.py +19 -35
  83. pyvale/simcases/case21.i +1 -1
  84. pyvale/simcases/run_1case.py +8 -0
  85. pyvale/simtools.py +2 -2
  86. pyvale/visualsimplotter.py +180 -0
  87. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/METADATA +11 -57
  88. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/RECORD +91 -56
  89. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/WHEEL +1 -1
  90. pyvale/examples/visualisation/ex1_1_plot_traces.py +0 -102
  91. pyvale/examples/visualisation/ex2_1_animate_sim.py +0 -89
  92. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/licenses/LICENSE +0 -0
  93. {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/top_level.txt +0 -0
pyvale/dic2d.py ADDED
@@ -0,0 +1,164 @@
1
+ # ================================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ================================================================================
6
+
7
+
8
+
9
+ import numpy as np
10
+ from pathlib import Path
11
+
12
+ # import cython module
13
+ import pyvale.dic2dcpp as dic2dcpp
14
+ import pyvale.dicchecks as dicchecks
15
+
16
+
17
+ def dic_2d(reference: np.ndarray | str | Path,
18
+ deformed: np.ndarray | str | Path,
19
+ roi_mask: np.ndarray,
20
+ seed: list[int],
21
+ subset_size: int = 21,
22
+ subset_step: int = 10,
23
+ correlation_criteria: str="ZNSSD",
24
+ shape_function: str="AFFINE",
25
+ interpolation_routine: str="BICUBIC",
26
+ max_iterations: int=40,
27
+ opt_precision: float=0.001,
28
+ opt_threshold: float=0.9,
29
+ bf_threshold: float=0.6,
30
+ max_displacement: int=128,
31
+ scanning_method: str="RG",
32
+ fft_mad: bool=False,
33
+ fft_mad_scale: float=3.0,
34
+ output_at_end: bool=False,
35
+ output_basepath: Path | str = "./",
36
+ output_binary: bool=False,
37
+ output_prefix: str="dic_results_",
38
+ output_delimiter: str=",") -> None:
39
+ """
40
+ Perform 2D Digital Image Correlation (DIC) between a reference image and one or more deformed images.
41
+
42
+ This function wraps a C++ DIC engine by preparing configuration parameters,
43
+ performing input validation, and dispatching image data and settings. It supports
44
+ pixel-level displacement and strain measurement over a defined region of interest (ROI).
45
+
46
+ Parameters
47
+ ----------
48
+ reference : np.ndarray, str or pathlib.Path
49
+ The reference image (2D array) or path to the image file.
50
+ deformed : np.ndarray, str or pathlib.Path
51
+ The deformed image(s) (3D array for multiple images) or path/pattern to image files.
52
+ roi_mask : np.ndarray
53
+ A binary mask indicating the Region of Interest (ROI) for analysis (same size as image).
54
+ seed : list of int, optional
55
+ Coordinates `[x, y]` of the seed point for Reliability-Guided (RG) scanning, default is empty.
56
+ subset_size : int, optional
57
+ Size of the square subset window in pixels (default: 21).
58
+ subset_step : int, optional
59
+ Step size between subset centers in pixels (default: 10).
60
+ correlation_criteria : str, optional
61
+ Metric for matching subsets: "ZNSSD", "NSSD" or "SSD" (default: "ZNSSD").
62
+ shape_function : str, optional
63
+ Deformation model: e.g., "AFFINE", "RIGID" (default: "AFFINE").
64
+ interpolation_routine : str, optional
65
+ Interpolation method used on image intensity. "BICUBIC" is currently the
66
+ only supported option.
67
+ max_iterations : int, optional
68
+ Maximum number of iterations allowed for subset optimization (default: 40).
69
+ opt_precision : float, optional
70
+ Precision threshold for iterative optimization convergence (default: 0.001).
71
+ opt_threshold : float, optional
72
+ Minimum correlation improvement threshold to continue iterations (default: 0.9).
73
+ bf_threshold : float, optional
74
+ Correlation threshold used in rigid bruteforce check for a subset to be considered a
75
+ good match(default: 0.6).
76
+ max_displacement : int, optional
77
+ Estimate for the Maximum displacement in any direction (in pixels) (default: 128).
78
+ scanning_method : str, optional
79
+ Subset scanning method: "RG" for Reliability-Guided (best overall approach),
80
+ "IMAGE_SCAN" for a standard scan across the image with no seeding
81
+ (best performance with for subpixel displacements with high quality images),
82
+ "FFT" for a multi-window FFT based approach (Good for large displacements)
83
+ fft_mad : bool, optional
84
+ The option to smooth FFT windowing data by identifying and replacing outliers using
85
+ a robust statistical method. For each subset, the function collects values from its
86
+ neighboring subsets (within a 5x5 window, i.e., radius = 2), computes the median and
87
+ Median Absolute Deviation (MAD), and determines whether the value at the current
88
+ subset is an outlier. If it is, the value is replaced with the median of
89
+ its neighbors. (default: False)
90
+ fft_mad_scale : bool, optional
91
+ An outlier is defined as a value whose deviation from the local median exceeds
92
+ `fft_mad_scale` times the MAD. This value choses the scaling factor that determines
93
+ the threshold for detecting outliers relative to the MAD.
94
+ output_at_end : bool, optional
95
+ If True, results will only be written at the end of processing (default: False).
96
+ output_basepath : str or pathlib.Path, optional
97
+ Directory path where output files will be written (default: "./").
98
+ output_binary : bool, optional
99
+ Whether to write output in binary format (default: False).
100
+ output_prefix : str, optional
101
+ Prefix for all output files (default: "dic_results_"). results will be
102
+ named with output_prefix + original filename. THe extension will be
103
+ changed to ".csv" or ".dic2d" depending on whether outputting as a binary.
104
+ output_delimiter : str, optional
105
+ Delimiter used in text output files (default: ",").
106
+
107
+ Returns
108
+ -------
109
+ None
110
+ All outputs are written to files; no values are returned.
111
+
112
+ Raises
113
+ ------
114
+ ValueError
115
+ If input checks fail (e.g., invalid image sizes, unsupported parameters).
116
+ FileNotFoundError
117
+ If provided file paths do not exist.
118
+ """
119
+
120
+ # do checks on vars in python land
121
+ dicchecks.print_title("Initial Checks")
122
+ ref_arr, def_arr, roi_c, filenames = dicchecks.check_and_get_images(reference,deformed,roi_mask)
123
+ dicchecks.check_correlation_criteria(correlation_criteria)
124
+ dicchecks.check_interpolation(interpolation_routine)
125
+ dicchecks.check_scanning_method(scanning_method)
126
+ dicchecks.check_thresholds(opt_threshold, bf_threshold, opt_precision)
127
+ dicchecks.check_output_directory(str(output_basepath), output_prefix)
128
+ dicchecks.check_subsets(subset_size, subset_step)
129
+ updated_seed = dicchecks.check_and_update_rg_seed(seed, roi_mask, scanning_method, ref_arr.shape[1], ref_arr.shape[0], subset_size, subset_step)
130
+ num_params = dicchecks.check_shape_function(shape_function)
131
+
132
+
133
+ # Assign values to config struct for c++ land
134
+ config = dic2dcpp.Config()
135
+ config.ss_step = subset_step
136
+ config.ss_size = subset_size
137
+ config.max_iter = max_iterations
138
+ config.precision = opt_precision
139
+ config.opt_threshold = opt_threshold
140
+ config.bf_threshold = bf_threshold
141
+ config.max_disp = max_displacement
142
+ config.corr_crit = correlation_criteria
143
+ config.shape_func = shape_function
144
+ config.interp_routine = interpolation_routine
145
+ config.scan_method = scanning_method
146
+ config.px_hori = ref_arr.shape[1]
147
+ config.px_vert = ref_arr.shape[0]
148
+ config.num_def_img = def_arr.shape[0]
149
+ config.num_params = num_params
150
+ config.rg_seed = updated_seed
151
+ config.filenames = filenames
152
+ config.fft_mad = fft_mad
153
+ config.fft_mad_scale = fft_mad_scale
154
+
155
+ # assigning c++ struct vals for save config
156
+ saveconf = dic2dcpp.SaveConfig()
157
+ saveconf.basepath = str(output_basepath)
158
+ saveconf.binary = output_binary
159
+ saveconf.prefix = output_prefix
160
+ saveconf.delimiter = output_delimiter
161
+ saveconf.at_end = output_at_end
162
+
163
+ # calling the c++ dic engine
164
+ dic2dcpp.dic_engine(ref_arr, def_arr, roi_c, config, saveconf)
Binary file
pyvale/dicchecks.py ADDED
@@ -0,0 +1,476 @@
1
+ # ================================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ================================================================================
6
+
7
+ import numpy as np
8
+ import glob
9
+ import os
10
+ import sys
11
+ from PIL import Image
12
+ from pathlib import Path
13
+
14
+ """
15
+ This module contains functions for checking arguments passed to the 2D DIC
16
+ Engine.
17
+ """
18
+
19
+ def check_output_directory(output_basepath: str,
20
+ output_prefix: str) -> None:
21
+ """
22
+ Check for existing output files in a directory and prompt user confirmation before overwriting.
23
+
24
+ This function verifies whether the specified output directory exists and checks for any existing
25
+ files that match a given prefix and have `.csv` or `.dic2d` extensions. If such files are found,
26
+ a list is displayed and the user is prompted to confirm whether to continue. If the user declines,
27
+ the program exits to prevent data loss.
28
+
29
+ Parameters
30
+ ----------
31
+ output_basepath : str
32
+ Path to the output directory where files are or will be saved.
33
+ output_prefix : str
34
+ Filename prefix used to identify potential conflicting output files.
35
+
36
+ Raises
37
+ ------
38
+ SystemExit
39
+ If the output directory does not exist or the user chooses not to proceed after
40
+ being warned about existing files.
41
+ """
42
+
43
+ # check if there's output files
44
+ try:
45
+ files = os.listdir(output_basepath)
46
+ except FileNotFoundError:
47
+ print("")
48
+ print(f"Output directory '{output_basepath}' does not exist.")
49
+ sys.exit(1)
50
+
51
+ # Check for any matching files
52
+ conflicting_files = [
53
+ f for f in files
54
+ if f.startswith(output_prefix) and (f.endswith(".csv") or f.endswith(".dic2d"))]
55
+
56
+ if conflicting_files:
57
+ conflicting_files.sort()
58
+ print("The following output files already exist and may be overwritten:")
59
+ for f in conflicting_files:
60
+ print(f" - {os.path.join(output_basepath, f)}")
61
+ print("")
62
+
63
+ user_input = input("Do you want to continue? (y/n): ").strip().lower()
64
+
65
+ if user_input not in ("y", "yes", "Y", "YES"):
66
+ print("Aborting to avoid overwriting data in output directory.")
67
+ exit(0)
68
+
69
+
70
+ def check_correlation_criteria(correlation_criteria: str) -> None:
71
+ """
72
+ Validate that the correlation criteria is one of the allowed values.
73
+
74
+ Checks whether input `correlation_criteria` is among the
75
+ accepted options: "SSD", "NSSD", or "ZNSSD". If not, raises a `ValueError`.
76
+
77
+ Parameters
78
+ ----------
79
+ correlation_criteria : str
80
+ The correlation type. Must be one of: "SSD", "NSSD", or "ZNSSD".
81
+
82
+ Raises
83
+ ------
84
+ ValueError
85
+ If `correlation_criteria` is not one of the allowed values.
86
+ """
87
+
88
+ allowed_values = {"SSD", "NSSD", "ZNSSD"}
89
+
90
+ if correlation_criteria not in allowed_values:
91
+ raise ValueError(f"Invalid correlation_criteria: "
92
+ f"{correlation_criteria}. Allowed values are: "
93
+ f"{', '.join(allowed_values)}")
94
+
95
+
96
+
97
+ def check_shape_function(shape_function: str) -> int:
98
+ """
99
+ Checks whether input `shape_function` is one of the allowed
100
+ values ("RIGID" or "AFFINE"). If valid, it returns the number of transformation
101
+ parameters associated with that shape function.
102
+
103
+ Parameters
104
+ ----------
105
+ shape_function : str
106
+ The shape function type. Must be either "RIGID" or "AFFINE".
107
+
108
+ Returns
109
+ -------
110
+ int
111
+ The number of parameters for the specified shape function:
112
+ - 2 for "RIGID"
113
+ - 6 for "AFFINE"
114
+
115
+ Raises
116
+ ------
117
+ ValueError
118
+ If `shape_function` is not one of the allowed values.
119
+ """
120
+
121
+ if (shape_function=="RIGID"):
122
+ num_params = 2
123
+ elif (shape_function=="AFFINE"):
124
+ num_params = 6
125
+ else:
126
+ raise ValueError(f"Invalid shape_function: {shape_function}. "
127
+ f"Allowed values are: 'AFFINE', 'RIGID'.")
128
+
129
+ return num_params
130
+
131
+
132
+
133
+ def check_interpolation(interpolation_routine: str) -> None:
134
+ """
135
+ Validate that the interpolation routine is one of the allowed methods.
136
+
137
+ Checks whether interpolation_routine is a supported
138
+ interpolation method. Allowed values are "BILINEAR" and "BICUBIC". If the input
139
+ is not one of these, a `ValueError` is raised.
140
+
141
+ Parameters
142
+ ----------
143
+ interpolation_routine : str
144
+ The interpolation method to validate. Must be either "BILINEAR" or "BICUBIC".
145
+
146
+ Raises
147
+ ------
148
+ ValueError
149
+ If `interpolation_routine` is not a supported value.
150
+
151
+ """
152
+
153
+ allowed_values = {"BILINEAR", "BICUBIC"}
154
+
155
+ if interpolation_routine not in allowed_values:
156
+ raise ValueError(f"Invalid interpolation_routine: "
157
+ f"{interpolation_routine}. Allowed values are: "
158
+ f"{', '.join(allowed_values)}")
159
+
160
+
161
+
162
+ def check_scanning_method(scanning_method: str) -> None:
163
+ """
164
+ Validate that the scan type one of the allowed methods.
165
+
166
+ Allowed values are "RG", "IMAGE_SCAN", "FFT", "IMAGE_SCAN_WITH_BF", "FFT_test". If `scanning_method`
167
+ is not one of these, a `ValueError` is raised.
168
+
169
+ Parameters
170
+ ----------
171
+ interpolation_routine : str
172
+ The interpolation method to validate. Must be either "BILINEAR" or "BICUBIC".
173
+
174
+ Raises
175
+ ------
176
+ ValueError
177
+ If `interpolation_routine` is not a supported value.
178
+
179
+ """
180
+
181
+ allowed_values = {"RG", "IMAGE_SCAN", "FFT", "IMAGE_SCAN_WITH_BF", "FFT_test"}
182
+
183
+ if scanning_method not in allowed_values:
184
+ raise ValueError(f"Invalid scanning_method: {scanning_method}. "
185
+ f"Allowed values are: {', '.join(allowed_values)}")
186
+
187
+
188
+
189
+ def check_thresholds(opt_threshold: float,
190
+ bf_threshold: float,
191
+ opt_precision: float) -> None:
192
+ """
193
+ Ensures that `opt_threshold`, `bf_threshold`, and `opt_precision`
194
+ are all floats strictly between 0 and 1. Raises a `ValueError` if any condition fails.
195
+
196
+ Parameters
197
+ ----------
198
+ opt_threshold : float
199
+ Threshold for the Levenberg optimization method.
200
+ bf_threshold : float
201
+ Threshold for the brute-force optimization method.
202
+ opt_precision : float
203
+ Desired precision for the optimizer.
204
+
205
+ Raises
206
+ ------
207
+ ValueError
208
+ If any input value is not a float strictly between 0 and 1.
209
+ """
210
+
211
+ if not (0 < opt_threshold < 1):
212
+ raise ValueError("opt_threshold must be a float "
213
+ "strictly between 0 and 1.")
214
+
215
+ if not (0 < bf_threshold < 1):
216
+ raise ValueError("bf_threshold must be a float "
217
+ "strictly between 0 and 1.")
218
+
219
+ if not (0 < opt_precision < 1):
220
+ raise ValueError("Optimizer precision must be a float strictly "
221
+ "between 0 and 1.")
222
+
223
+ def check_subsets(subset_size: int, subset_step: int) -> None:
224
+ """
225
+
226
+ Parameters
227
+ ----------
228
+ subset_size : int
229
+ Threshold for the Levenberg optimization method.
230
+ subset_step : int
231
+ Threshold for the brute-force optimization method.
232
+
233
+ Raises
234
+ ------
235
+ ValueError
236
+ If any input value is not a float strictly between 0 and 1.
237
+ """
238
+
239
+
240
+ # Enforce scalar types for non-FFT methods
241
+ if subset_size % 2 == 0:
242
+ raise ValueError("subset_size must be an odd number.")
243
+
244
+ # check if subset_step is larger than the subset_size
245
+ if subset_step > subset_size:
246
+ raise ValueError("subset_step is larger than the subset_size.")
247
+
248
+
249
+
250
+ def check_and_update_rg_seed(seed: list[int], roi_mask: np.ndarray, scanning_method: str, px_hori: int, px_vert: int, subset_size: int, subset_step: int) -> list[int]:
251
+ """
252
+ Validate and update the region-growing seed location to align with image bounds and subset spacing.
253
+
254
+ This function checks the format and bounds of the seed coordinates used for a region-growing (RG)
255
+ scanning method. It adjusts the seed to the nearest valid grid point based on the subset step size,
256
+ clamps it to the image dimensions, and ensures it lies within the region of interest (ROI) mask.
257
+
258
+ If the scanning method is not "RG", the function returns a default seed of [0, 0].
259
+ This seed is not used any other scan method methods.
260
+
261
+ Parameters
262
+ ----------
263
+ seed : list of int
264
+ The initial seed coordinates as a list of two integers: [x, y].
265
+ roi_mask : np.ndarray
266
+ A 2D binary mask (same size as the image) indicating the region of interest.
267
+ scanning_method : str
268
+ The scanning method to be used. Only "RG" triggers validation and adjustment logic.
269
+ px_hori : int
270
+ Width of the image in pixels.
271
+ px_vert : int
272
+ Height of the image in pixels.
273
+ subset_step : int
274
+ Step size used for subset spacing; seed is aligned to this grid.
275
+
276
+ Returns
277
+ -------
278
+ list of int
279
+ The adjusted seed coordinates [x, y] aligned to the subset grid and within bounds.
280
+
281
+ Raises
282
+ ------
283
+ ValueError
284
+ If the seed is improperly formatted, out of image bounds, or not a list of two integers.
285
+ """
286
+
287
+ if scanning_method != "RG":
288
+ return [0,0]
289
+
290
+ if not (isinstance(seed, list) and len(seed) == 2 and all(isinstance(coord, int) for coord in seed)):
291
+ raise ValueError("Reliability Guided seed is either missing or has been defined incorrectly. must be a list of two integers: seed=[x, y]")
292
+
293
+ x, y = seed
294
+
295
+ corner_x = x - subset_size//2
296
+ corner_y = y - subset_size//2
297
+
298
+ def round_to_step(value: int, step: int) -> int:
299
+ return round(value / step) * step
300
+
301
+ # snap to grid
302
+ new_x = round_to_step(corner_x, subset_step)
303
+ new_y = round_to_step(corner_y, subset_step)
304
+
305
+
306
+ # Clamp to image bounds
307
+ new_x = min(max(new_x, 0), px_hori - 1)
308
+ new_y = min(max(new_y, 0), px_vert - 1)
309
+
310
+ # check if all pixel values within the seed location are within the ROI
311
+ # seed coordinates are the central pixel to the subset
312
+ max_x = new_x + subset_size//2+1
313
+ max_y = new_y + subset_size//2+1
314
+
315
+
316
+ # Check if all pixel values in the ROI are valid
317
+ for i in range(corner_x, max_x):
318
+ for j in range(corner_y, max_y):
319
+
320
+ if i < 0 or i >= px_hori or j < 0 or j >= px_vert:
321
+ raise ValueError(f"Seed ({x}, {y}) goes outside the image bounds at pixel ({i}, {j})")
322
+
323
+ if not roi_mask[j, i]:
324
+ raise ValueError(f"Seed ({x}, {y}) goes outside the ROI at pixel ({i}, {j})")
325
+
326
+ return [new_x, new_y]
327
+
328
+
329
+ def check_and_get_images(reference: np.ndarray | str | Path,
330
+ deformed: np.ndarray | str | Path,
331
+ roi: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[str]]:
332
+ """
333
+ Load and validate reference and deformed images, checks consistency in shape/format.
334
+
335
+ This function accepts either:
336
+ - A file path to a reference image and a glob pattern for a sequence of deformed image files, or
337
+ - Numpy arrays for both reference and deformed images.
338
+
339
+ It ensures:
340
+ - The reference and deformed images are the same type (both paths or both arrays).
341
+ - The reference image exists and is readable (if passed as a path).
342
+ - All deformed images exist and match the reference image shape.
343
+ - If images are RGB or multi-channel, only the first channel is used.
344
+ - The `roi` (region of interest) has the same shape as the reference image (when arrays are used directly).
345
+
346
+ Parameters
347
+ ----------
348
+ reference : np.ndarray, str, pathlib.Path
349
+ Either a NumPy array representing the reference image, or a file path to a reference image.
350
+ deformed : np.ndarray, str, pathlib.Path
351
+ Either a NumPy array representing a sequence of deformed images (shape: [N, H, W]),
352
+ or a glob pattern string pointing to multiple image files.
353
+ roi : np.ndarray
354
+ A 2D NumPy array defining the region of interest. Must match the reference image shape
355
+ if `reference` is an array.
356
+
357
+ Returns
358
+ -------
359
+ ref_arr : np.ndarray
360
+ The reference image as a 2D NumPy array.
361
+ def_arr : np.ndarray
362
+ A 3D NumPy array containing all deformed images with shape (N, H, W).
363
+ filenames : list of str
364
+ List of base filenames of deformed images (empty if deformed images were passed as arrays).
365
+
366
+ Raises
367
+ ------
368
+ ValueError
369
+ If there is a type mismatch between `reference` and `deformed`,
370
+ if image files are not found or unreadable,
371
+ or if image shapes do not match.
372
+ FileNotFoundError
373
+ If no files are found matching the deformed image pattern.
374
+ """
375
+
376
+ filenames = []
377
+
378
+
379
+ # Normalize Path or str to Path
380
+ if isinstance(reference, (str, Path)):
381
+ reference = Path(reference)
382
+ if isinstance(deformed, (str, Path)):
383
+ deformed = Path(deformed)
384
+
385
+ # check matching filetypes
386
+ if type(reference) is not type(deformed):
387
+ raise ValueError(
388
+ f"Mismatch in file types: reference={type(reference)}, "
389
+ f"deformed={type(deformed)}")
390
+
391
+
392
+ # File-based input
393
+ if isinstance(reference, Path):
394
+ assert isinstance(deformed, Path)
395
+
396
+ if not reference.is_file():
397
+ raise ValueError(f"Reference image does not exist: {reference}")
398
+ print("Using reference image: ")
399
+ print(f" - {reference}\n")
400
+
401
+ # Load reference image
402
+ ref_arr = np.array(Image.open(reference))
403
+ print(f"Reference image shape: {ref_arr.shape}")
404
+ if ref_arr.ndim == 3:
405
+ print(f"Reference image appears to have {ref_arr.shape[2]} channels. Using channel 0.")
406
+ ref_arr = ref_arr[:, :, 0]
407
+ print("")
408
+
409
+ # Find deformation image files
410
+ files = sorted(glob.glob(str(deformed)))
411
+ if not files:
412
+ raise FileNotFoundError(f"No deformation images found: {deformed}")
413
+
414
+ print(f"Found {len(files)} deformation images:")
415
+ for file in files:
416
+ print(f" - {file}")
417
+ filenames.append(os.path.basename(file))
418
+ print("")
419
+
420
+ def_arr = np.zeros((len(files), *ref_arr.shape), dtype=ref_arr.dtype)
421
+
422
+ for i, file in enumerate(files):
423
+ img = np.array(Image.open(file))
424
+ if img.ndim == 3:
425
+ print(f"Deformed image {file} appears to have {img.shape[2]} channels. Using channel 0.")
426
+ img = img[:, :, 0]
427
+ if img.shape != ref_arr.shape:
428
+ raise ValueError(f"Shape mismatch: '{file}' has shape {img.shape}, expected {ref_arr.shape}")
429
+ def_arr[i] = img
430
+
431
+ # Array-based input
432
+ else:
433
+ assert isinstance(reference, np.ndarray)
434
+ assert isinstance(deformed, np.ndarray)
435
+ ref_arr = reference
436
+ def_arr = deformed
437
+
438
+ if (reference.shape != deformed[0].shape or reference.shape != roi.shape):
439
+ raise ValueError(f"Shape mismatch: reference {reference.shape}, "
440
+ f"deformed[0] {deformed[0].shape}, roi {roi.shape}")
441
+
442
+ # it might be the case that the roi has been manipulated prior to DIC run
443
+ # and therefore we need to to prevent the roi mask from being a 'view'
444
+ roi_c = np.ascontiguousarray(roi)
445
+
446
+ return ref_arr, def_arr, roi_c, filenames
447
+
448
+
449
+
450
+ def check_strain_files(strain_files: str | Path) -> list[str]:
451
+
452
+ filenames = []
453
+
454
+ # Find deformation image files
455
+ files = sorted(glob.glob(str(strain_files)))
456
+ if not files:
457
+ raise FileNotFoundError(f"No DIC data found: {strain_files}")
458
+
459
+ for file in files:
460
+ filenames.append(os.path.basename(file))
461
+
462
+ return filenames
463
+
464
+
465
+ def print_title(a: str):
466
+ line_width = 80
467
+ half_width = 39
468
+
469
+ print('-' * line_width)
470
+
471
+ # Center the title between dashes
472
+ left_dashes = '-' * (half_width - len(a) // 2)
473
+ right_dashes = '-' * (half_width - len(a) // 2)
474
+ print(f"{left_dashes} {a} {right_dashes}")
475
+
476
+ print('-' * line_width)