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.
- 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.cp311-win_amd64.pyd +0 -0
- pyvale/cython/rastercyth.py +71 -26
- 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.cp311-win_amd64.pyd +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.0.dist-info}/METADATA +11 -57
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/RECORD +91 -56
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.0.dist-info}/WHEEL +1 -1
- 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.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|