pivtools 0.1.3__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.
- pivtools-0.1.3.dist-info/METADATA +222 -0
- pivtools-0.1.3.dist-info/RECORD +127 -0
- pivtools-0.1.3.dist-info/WHEEL +5 -0
- pivtools-0.1.3.dist-info/entry_points.txt +3 -0
- pivtools-0.1.3.dist-info/top_level.txt +3 -0
- pivtools_cli/__init__.py +5 -0
- pivtools_cli/_build_marker.c +25 -0
- pivtools_cli/_build_marker.cp311-win_amd64.pyd +0 -0
- pivtools_cli/cli.py +225 -0
- pivtools_cli/example.py +139 -0
- pivtools_cli/lib/PIV_2d_cross_correlate.c +334 -0
- pivtools_cli/lib/PIV_2d_cross_correlate.h +22 -0
- pivtools_cli/lib/common.h +36 -0
- pivtools_cli/lib/interp2custom.c +146 -0
- pivtools_cli/lib/interp2custom.h +48 -0
- pivtools_cli/lib/peak_locate_gsl.c +711 -0
- pivtools_cli/lib/peak_locate_gsl.h +40 -0
- pivtools_cli/lib/peak_locate_gsl_print.c +736 -0
- pivtools_cli/lib/peak_locate_lm.c +751 -0
- pivtools_cli/lib/peak_locate_lm.h +27 -0
- pivtools_cli/lib/xcorr.c +342 -0
- pivtools_cli/lib/xcorr.h +31 -0
- pivtools_cli/lib/xcorr_cache.c +78 -0
- pivtools_cli/lib/xcorr_cache.h +26 -0
- pivtools_cli/piv/interp2custom/interp2custom.py +69 -0
- pivtools_cli/piv/piv.py +240 -0
- pivtools_cli/piv/piv_backend/base.py +825 -0
- pivtools_cli/piv/piv_backend/cpu_instantaneous.py +1005 -0
- pivtools_cli/piv/piv_backend/factory.py +28 -0
- pivtools_cli/piv/piv_backend/gpu_instantaneous.py +15 -0
- pivtools_cli/piv/piv_backend/infilling.py +445 -0
- pivtools_cli/piv/piv_backend/outlier_detection.py +306 -0
- pivtools_cli/piv/piv_backend/profile_cpu_instantaneous.py +230 -0
- pivtools_cli/piv/piv_result.py +40 -0
- pivtools_cli/piv/save_results.py +342 -0
- pivtools_cli/piv_cluster/cluster.py +108 -0
- pivtools_cli/preprocessing/filters.py +399 -0
- pivtools_cli/preprocessing/preprocess.py +79 -0
- pivtools_cli/tests/helpers.py +107 -0
- pivtools_cli/tests/instantaneous_piv/test_piv_integration.py +167 -0
- pivtools_cli/tests/instantaneous_piv/test_piv_integration_multi.py +553 -0
- pivtools_cli/tests/preprocessing/test_filters.py +41 -0
- pivtools_core/__init__.py +5 -0
- pivtools_core/config.py +703 -0
- pivtools_core/config.yaml +135 -0
- pivtools_core/image_handling/__init__.py +0 -0
- pivtools_core/image_handling/load_images.py +464 -0
- pivtools_core/image_handling/readers/__init__.py +53 -0
- pivtools_core/image_handling/readers/generic_readers.py +50 -0
- pivtools_core/image_handling/readers/lavision_reader.py +190 -0
- pivtools_core/image_handling/readers/registry.py +24 -0
- pivtools_core/paths.py +49 -0
- pivtools_core/vector_loading.py +248 -0
- pivtools_gui/__init__.py +3 -0
- pivtools_gui/app.py +687 -0
- pivtools_gui/calibration/__init__.py +0 -0
- pivtools_gui/calibration/app/__init__.py +0 -0
- pivtools_gui/calibration/app/views.py +1186 -0
- pivtools_gui/calibration/calibration_planar/planar_calibration_production.py +570 -0
- pivtools_gui/calibration/vector_calibration_production.py +544 -0
- pivtools_gui/config.py +703 -0
- pivtools_gui/image_handling/__init__.py +0 -0
- pivtools_gui/image_handling/load_images.py +464 -0
- pivtools_gui/image_handling/readers/__init__.py +53 -0
- pivtools_gui/image_handling/readers/generic_readers.py +50 -0
- pivtools_gui/image_handling/readers/lavision_reader.py +190 -0
- pivtools_gui/image_handling/readers/registry.py +24 -0
- pivtools_gui/masking/__init__.py +0 -0
- pivtools_gui/masking/app/__init__.py +0 -0
- pivtools_gui/masking/app/views.py +123 -0
- pivtools_gui/paths.py +49 -0
- pivtools_gui/piv_runner.py +261 -0
- pivtools_gui/pivtools.py +58 -0
- pivtools_gui/plotting/__init__.py +0 -0
- pivtools_gui/plotting/app/__init__.py +0 -0
- pivtools_gui/plotting/app/views.py +1671 -0
- pivtools_gui/plotting/plot_maker.py +220 -0
- pivtools_gui/post_processing/POD/__init__.py +0 -0
- pivtools_gui/post_processing/POD/app/__init__.py +0 -0
- pivtools_gui/post_processing/POD/app/views.py +647 -0
- pivtools_gui/post_processing/POD/pod_decompose.py +979 -0
- pivtools_gui/post_processing/POD/views.py +1096 -0
- pivtools_gui/post_processing/__init__.py +0 -0
- pivtools_gui/static/404.html +1 -0
- pivtools_gui/static/_next/static/chunks/117-d5793c8e79de5511.js +2 -0
- pivtools_gui/static/_next/static/chunks/484-cfa8b9348ce4f00e.js +1 -0
- pivtools_gui/static/_next/static/chunks/869-320a6b9bdafbb6d3.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/_not-found/page-12f067ceb7415e55.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/layout-b907d5f31ac82e9d.js +1 -0
- pivtools_gui/static/_next/static/chunks/app/page-334cc4e8444cde2f.js +1 -0
- pivtools_gui/static/_next/static/chunks/fd9d1056-ad15f396ddf9b7e5.js +1 -0
- pivtools_gui/static/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- pivtools_gui/static/_next/static/chunks/main-a1b3ced4d5f6d998.js +1 -0
- pivtools_gui/static/_next/static/chunks/main-app-8a63c6f5e7baee11.js +1 -0
- pivtools_gui/static/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- pivtools_gui/static/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- pivtools_gui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- pivtools_gui/static/_next/static/chunks/webpack-4a8ca7c99e9bb3d8.js +1 -0
- pivtools_gui/static/_next/static/css/7d3f2337d7ea12a5.css +3 -0
- pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_buildManifest.js +1 -0
- pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_ssgManifest.js +1 -0
- pivtools_gui/static/file.svg +1 -0
- pivtools_gui/static/globe.svg +1 -0
- pivtools_gui/static/grid.svg +8 -0
- pivtools_gui/static/index.html +1 -0
- pivtools_gui/static/index.txt +8 -0
- pivtools_gui/static/next.svg +1 -0
- pivtools_gui/static/vercel.svg +1 -0
- pivtools_gui/static/window.svg +1 -0
- pivtools_gui/stereo_reconstruction/__init__.py +0 -0
- pivtools_gui/stereo_reconstruction/app/__init__.py +0 -0
- pivtools_gui/stereo_reconstruction/app/views.py +1985 -0
- pivtools_gui/stereo_reconstruction/stereo_calibration_production.py +606 -0
- pivtools_gui/stereo_reconstruction/stereo_reconstruction_production.py +544 -0
- pivtools_gui/utils.py +63 -0
- pivtools_gui/vector_loading.py +248 -0
- pivtools_gui/vector_merging/__init__.py +1 -0
- pivtools_gui/vector_merging/app/__init__.py +1 -0
- pivtools_gui/vector_merging/app/views.py +759 -0
- pivtools_gui/vector_statistics/app/__init__.py +1 -0
- pivtools_gui/vector_statistics/app/views.py +710 -0
- pivtools_gui/vector_statistics/ensemble_statistics.py +49 -0
- pivtools_gui/vector_statistics/instantaneous_statistics.py +311 -0
- pivtools_gui/video_maker/__init__.py +0 -0
- pivtools_gui/video_maker/app/__init__.py +0 -0
- pivtools_gui/video_maker/app/views.py +436 -0
- pivtools_gui/video_maker/video_maker.py +662 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
import warnings
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
import cv2
|
|
11
|
+
import dask.array as da
|
|
12
|
+
import numpy as np
|
|
13
|
+
from dask.distributed import get_worker
|
|
14
|
+
from scipy.ndimage import gaussian_filter
|
|
15
|
+
from scipy.signal import convolve2d
|
|
16
|
+
|
|
17
|
+
# Try to import line_profiler for detailed profiling
|
|
18
|
+
try:
|
|
19
|
+
from line_profiler import profile
|
|
20
|
+
except ImportError:
|
|
21
|
+
profile = lambda f: f
|
|
22
|
+
|
|
23
|
+
# Add src to path for unified imports
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from pivtools_core.config import Config
|
|
27
|
+
from pivtools_cli.piv.piv_backend.base import CrossCorrelator
|
|
28
|
+
from pivtools_cli.piv.piv_result import PIVPassResult, PIVResult
|
|
29
|
+
from pivtools_cli.piv.piv_backend.outlier_detection import apply_outlier_detection
|
|
30
|
+
from pivtools_cli.piv.piv_backend.infilling import apply_infilling
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InstantaneousCorrelatorCPU(CrossCorrelator):
|
|
34
|
+
@profile
|
|
35
|
+
def __init__(self, config: Config, precomputed_cache: Optional[dict] = None) -> None:
|
|
36
|
+
super().__init__()
|
|
37
|
+
# Use platform-appropriate library extension
|
|
38
|
+
lib_extension = ".dll" if os.name == "nt" else ".so"
|
|
39
|
+
lib_path = os.path.join(
|
|
40
|
+
os.path.dirname(__file__), "../..", "lib", f"libbulkxcorr2d{lib_extension}"
|
|
41
|
+
)
|
|
42
|
+
lib_path = os.path.abspath(lib_path)
|
|
43
|
+
if not os.path.isfile(lib_path):
|
|
44
|
+
raise FileNotFoundError(f"Required library file not found: {lib_path}")
|
|
45
|
+
# Add vcpkg bin directory to DLL search path on Windows
|
|
46
|
+
if os.name == "nt":
|
|
47
|
+
vcpkg_bin = os.path.join(os.environ.get('FFTW_LIB_PATH', '').replace('lib', 'bin'))
|
|
48
|
+
if vcpkg_bin and os.path.isdir(vcpkg_bin):
|
|
49
|
+
os.add_dll_directory(vcpkg_bin)
|
|
50
|
+
self.lib = ctypes.CDLL(lib_path)
|
|
51
|
+
self.lib.bulkxcorr2d.restype = ctypes.c_ubyte
|
|
52
|
+
self.delta_ab_pred = None
|
|
53
|
+
self.delta_ab_old = None
|
|
54
|
+
self.prev_win_size = None
|
|
55
|
+
self.prev_win_spacing = None
|
|
56
|
+
# Updated to use C-contiguous (row-major) arrays
|
|
57
|
+
self.lib.bulkxcorr2d.argtypes = [
|
|
58
|
+
np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"), # fImageA
|
|
59
|
+
np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"), # fImageB
|
|
60
|
+
np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"), # fMask
|
|
61
|
+
np.ctypeslib.ndpointer(dtype=np.int32, flags="C_CONTIGUOUS"), # nImageSize
|
|
62
|
+
np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"), # fWinCtrsX
|
|
63
|
+
np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"), # fWinCtrsY
|
|
64
|
+
np.ctypeslib.ndpointer(dtype=np.int32, flags="C_CONTIGUOUS"), # nWindows
|
|
65
|
+
np.ctypeslib.ndpointer(
|
|
66
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
67
|
+
), # fWindowWeightA
|
|
68
|
+
ctypes.c_bool, # bEnsemble
|
|
69
|
+
np.ctypeslib.ndpointer(
|
|
70
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
71
|
+
), # fWindowWeightB
|
|
72
|
+
np.ctypeslib.ndpointer(dtype=np.int32, flags="C_CONTIGUOUS"), # nWindowSize
|
|
73
|
+
ctypes.c_int, # nPeaks
|
|
74
|
+
ctypes.c_int, # iPeakFinder
|
|
75
|
+
np.ctypeslib.ndpointer(
|
|
76
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
77
|
+
), # fPkLocX (output)
|
|
78
|
+
np.ctypeslib.ndpointer(
|
|
79
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
80
|
+
), # fPkLocY (output)
|
|
81
|
+
np.ctypeslib.ndpointer(
|
|
82
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
83
|
+
), # fPkHeight (output)
|
|
84
|
+
np.ctypeslib.ndpointer(
|
|
85
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
86
|
+
), # fSx (output)
|
|
87
|
+
np.ctypeslib.ndpointer(
|
|
88
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
89
|
+
), # fSy (output)
|
|
90
|
+
np.ctypeslib.ndpointer(
|
|
91
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
92
|
+
), # fSxy (output)
|
|
93
|
+
np.ctypeslib.ndpointer(
|
|
94
|
+
dtype=np.float32, flags="C_CONTIGUOUS"
|
|
95
|
+
), # fCorrelPlane_Out (output)
|
|
96
|
+
]
|
|
97
|
+
# Window weights should be C-contiguous with shape (win_height, win_width)
|
|
98
|
+
self.win_weights = [
|
|
99
|
+
np.ascontiguousarray(self._window_weight_fun(win_size, config.window_type))
|
|
100
|
+
for win_size in config.window_sizes
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Use precomputed cache if provided, otherwise compute it
|
|
104
|
+
if precomputed_cache is not None:
|
|
105
|
+
self._load_precomputed_cache(precomputed_cache)
|
|
106
|
+
else:
|
|
107
|
+
self._cache_window_padding(config=config)
|
|
108
|
+
self.H, self.W = config.image_shape
|
|
109
|
+
# Cache interpolation grids for performance
|
|
110
|
+
self._cache_interpolation_grids(config=config)
|
|
111
|
+
|
|
112
|
+
# Initialize vector masks (will be set in correlate_batch)
|
|
113
|
+
self.vector_masks = []
|
|
114
|
+
|
|
115
|
+
# Store pass times for profiling
|
|
116
|
+
self.pass_times = []
|
|
117
|
+
|
|
118
|
+
def _load_precomputed_cache(self, cache: dict) -> None:
|
|
119
|
+
"""Load precomputed cache data to avoid redundant computation.
|
|
120
|
+
|
|
121
|
+
:param cache: Dictionary containing precomputed cache data
|
|
122
|
+
:type cache: dict
|
|
123
|
+
"""
|
|
124
|
+
# Load window padding cache
|
|
125
|
+
self.win_ctrs_x = cache['win_ctrs_x']
|
|
126
|
+
self.win_ctrs_y = cache['win_ctrs_y']
|
|
127
|
+
self.win_spacing_x = cache['win_spacing_x']
|
|
128
|
+
self.win_spacing_y = cache['win_spacing_y']
|
|
129
|
+
self.win_ctrs_x_all = cache['win_ctrs_x_all']
|
|
130
|
+
self.win_ctrs_y_all = cache['win_ctrs_y_all']
|
|
131
|
+
self.n_pre_all = cache['n_pre_all']
|
|
132
|
+
self.n_post_all = cache['n_post_all']
|
|
133
|
+
self.ksize_filt = cache['ksize_filt']
|
|
134
|
+
self.sd = cache['sd']
|
|
135
|
+
self.G_smooth_predictor = cache['G_smooth_predictor']
|
|
136
|
+
|
|
137
|
+
# Load image dimensions
|
|
138
|
+
self.H = cache['H']
|
|
139
|
+
self.W = cache['W']
|
|
140
|
+
|
|
141
|
+
# Load interpolation grids cache
|
|
142
|
+
self.im_mesh = cache['im_mesh']
|
|
143
|
+
self.cached_dense_maps = cache['cached_dense_maps']
|
|
144
|
+
self.cached_predictor_maps = cache['cached_predictor_maps']
|
|
145
|
+
|
|
146
|
+
def get_cache_data(self) -> dict:
|
|
147
|
+
"""Extract cache data for sharing across workers.
|
|
148
|
+
|
|
149
|
+
:return: Dictionary containing all cached data
|
|
150
|
+
:rtype: dict
|
|
151
|
+
"""
|
|
152
|
+
return {
|
|
153
|
+
'win_ctrs_x': self.win_ctrs_x,
|
|
154
|
+
'win_ctrs_y': self.win_ctrs_y,
|
|
155
|
+
'win_spacing_x': self.win_spacing_x,
|
|
156
|
+
'win_spacing_y': self.win_spacing_y,
|
|
157
|
+
'win_ctrs_x_all': self.win_ctrs_x_all,
|
|
158
|
+
'win_ctrs_y_all': self.win_ctrs_y_all,
|
|
159
|
+
'n_pre_all': self.n_pre_all,
|
|
160
|
+
'n_post_all': self.n_post_all,
|
|
161
|
+
'ksize_filt': self.ksize_filt,
|
|
162
|
+
'sd': self.sd,
|
|
163
|
+
'G_smooth_predictor': self.G_smooth_predictor,
|
|
164
|
+
'H': self.H,
|
|
165
|
+
'W': self.W,
|
|
166
|
+
'im_mesh': self.im_mesh,
|
|
167
|
+
'cached_dense_maps': self.cached_dense_maps,
|
|
168
|
+
'cached_predictor_maps': self.cached_predictor_maps,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@profile
|
|
172
|
+
def correlate_batch( # type: ignore[override]
|
|
173
|
+
self, images: np.ndarray, config: Config, vector_masks: List[np.ndarray] | None = None
|
|
174
|
+
) -> PIVResult:
|
|
175
|
+
"""Run PIV correlation on a batch of image pairs with MATLAB-style indexing."""
|
|
176
|
+
|
|
177
|
+
N, _, H, W = images.shape
|
|
178
|
+
|
|
179
|
+
piv_result_all = PIVResult()
|
|
180
|
+
self.delta_ab_pred = None
|
|
181
|
+
self.delta_ab_old = None
|
|
182
|
+
|
|
183
|
+
# Clear pass times for this batch
|
|
184
|
+
self.pass_times = []
|
|
185
|
+
|
|
186
|
+
# Use pre-computed vector masks
|
|
187
|
+
self.vector_masks = vector_masks if vector_masks is not None else []
|
|
188
|
+
|
|
189
|
+
y_coords = np.arange(self.H, dtype=np.float32)
|
|
190
|
+
x_coords = np.arange(self.W, dtype=np.float32)
|
|
191
|
+
y_mesh, x_mesh = np.meshgrid(y_coords, x_coords, indexing="ij")
|
|
192
|
+
self.im_mesh = np.stack([y_mesh, x_mesh], axis=-1)
|
|
193
|
+
|
|
194
|
+
for n in range(N):
|
|
195
|
+
try:
|
|
196
|
+
# Convert images to C-contiguous (row-major) format
|
|
197
|
+
image_a = np.asarray(images[n, 0], dtype=np.float32)
|
|
198
|
+
image_b = np.asarray(images[n, 1], dtype=np.float32)
|
|
199
|
+
|
|
200
|
+
if not image_a.flags["C_CONTIGUOUS"]:
|
|
201
|
+
image_a = np.ascontiguousarray(image_a)
|
|
202
|
+
if not image_b.flags["C_CONTIGUOUS"]:
|
|
203
|
+
image_b = np.ascontiguousarray(image_b)
|
|
204
|
+
|
|
205
|
+
# Pass image_size as [H, W] in C-contiguous format
|
|
206
|
+
image_size = np.ascontiguousarray(np.array([H, W], dtype=np.int32))
|
|
207
|
+
|
|
208
|
+
for pass_idx, win_size in enumerate(config.window_sizes):
|
|
209
|
+
pass_start = time.perf_counter()
|
|
210
|
+
image_a_prime, image_b_prime, self.delta_ab_pred = (
|
|
211
|
+
self._predictor_corrector(
|
|
212
|
+
pass_idx,
|
|
213
|
+
image_a,
|
|
214
|
+
image_b,
|
|
215
|
+
win_type=config.window_type,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
(
|
|
220
|
+
win_size_arr,
|
|
221
|
+
n_windows,
|
|
222
|
+
b_mask,
|
|
223
|
+
n_peaks,
|
|
224
|
+
i_peak_finder,
|
|
225
|
+
b_ensemble,
|
|
226
|
+
pk_loc_x,
|
|
227
|
+
pk_loc_y,
|
|
228
|
+
pk_height,
|
|
229
|
+
sx,
|
|
230
|
+
sy,
|
|
231
|
+
sxy,
|
|
232
|
+
correl_plane_out,
|
|
233
|
+
) = self._set_lib_arguments(
|
|
234
|
+
config=config,
|
|
235
|
+
win_size=win_size,
|
|
236
|
+
pass_idx=pass_idx,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Ensure images are C-contiguous before passing to C library
|
|
240
|
+
image_a_prime_c = image_a_prime if image_a_prime.flags["C_CONTIGUOUS"] else np.ascontiguousarray(image_a_prime)
|
|
241
|
+
image_b_prime_c = image_b_prime if image_b_prime.flags["C_CONTIGUOUS"] else np.ascontiguousarray(image_b_prime)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
error_code = self.lib.bulkxcorr2d(
|
|
245
|
+
image_a_prime_c,
|
|
246
|
+
image_b_prime_c,
|
|
247
|
+
b_mask,
|
|
248
|
+
image_size,
|
|
249
|
+
self.win_ctrs_x[pass_idx].astype(np.float32),
|
|
250
|
+
self.win_ctrs_y[pass_idx].astype(np.float32),
|
|
251
|
+
n_windows,
|
|
252
|
+
self.win_weights[pass_idx],
|
|
253
|
+
b_ensemble,
|
|
254
|
+
self.win_weights[pass_idx],
|
|
255
|
+
win_size_arr,
|
|
256
|
+
int(n_peaks),
|
|
257
|
+
int(i_peak_finder),
|
|
258
|
+
pk_loc_x,
|
|
259
|
+
pk_loc_y,
|
|
260
|
+
pk_height,
|
|
261
|
+
sx,
|
|
262
|
+
sy,
|
|
263
|
+
sxy,
|
|
264
|
+
correl_plane_out,
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logging.error(f" Exception type: {type(e).__name__}")
|
|
268
|
+
import traceback
|
|
269
|
+
logging.error(traceback.format_exc())
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
if error_code != 0:
|
|
273
|
+
error_names = {
|
|
274
|
+
1: "ERROR_NOMEM (out of memory)",
|
|
275
|
+
2: "ERROR_NOPLAN_FWD (FFT forward plan failed)",
|
|
276
|
+
4: "ERROR_NOPLAN_BWD (FFT backward plan failed)",
|
|
277
|
+
8: "ERROR_NOPLAN (general plan error)",
|
|
278
|
+
9: "ERROR_OUT_OF_BOUNDS (array access out of bounds)"
|
|
279
|
+
}
|
|
280
|
+
error_msg = error_names.get(error_code, f"Unknown error code {error_code}")
|
|
281
|
+
logging.error(f" bulkxcorr2d returned error code {error_code}: {error_msg}")
|
|
282
|
+
raise RuntimeError(f"bulkxcorr2d failed with error {error_code}: {error_msg}")
|
|
283
|
+
|
|
284
|
+
n_win_y = int(n_windows[0])
|
|
285
|
+
n_win_x = int(n_windows[1])
|
|
286
|
+
mask_bool = b_mask.astype(bool)
|
|
287
|
+
|
|
288
|
+
pk_loc_x[:, mask_bool] = np.nan
|
|
289
|
+
pk_loc_y[:, mask_bool] = np.nan
|
|
290
|
+
pk_height[:, mask_bool] = np.nan
|
|
291
|
+
|
|
292
|
+
win_height, win_width = win_size_arr.astype(np.int32)
|
|
293
|
+
large_disp_mask = (
|
|
294
|
+
(np.abs(pk_loc_x) > win_width / 4.0)
|
|
295
|
+
| (np.abs(pk_loc_y) > win_height / 4.0)
|
|
296
|
+
)
|
|
297
|
+
pk_loc_x[large_disp_mask] = np.nan
|
|
298
|
+
pk_loc_y[large_disp_mask] = np.nan
|
|
299
|
+
pk_height[large_disp_mask] = np.nan
|
|
300
|
+
|
|
301
|
+
# delta_ab_pred[..., 0] = Y-displacement, delta_ab_pred[..., 1] = X-displacement
|
|
302
|
+
# pk_loc_x is X-displacement, pk_loc_y is Y-displacement
|
|
303
|
+
pk_loc_x += self.delta_ab_pred[..., 1][np.newaxis, :, :] # Add X-predictor to X
|
|
304
|
+
pk_loc_y += self.delta_ab_pred[..., 0][np.newaxis, :, :] # Add Y-predictor to Y
|
|
305
|
+
|
|
306
|
+
primary_idx = np.zeros((1, n_win_y, n_win_x), dtype=np.intp)
|
|
307
|
+
ux_mat = np.take_along_axis(pk_loc_x, primary_idx, axis=0)[0]
|
|
308
|
+
uy_mat = np.take_along_axis(pk_loc_y, primary_idx, axis=0)[0]
|
|
309
|
+
# Use direct indexing without meshgrid for outlier detection and peak selection
|
|
310
|
+
n_win_y = int(n_windows[0])
|
|
311
|
+
n_win_x = int(n_windows[1])
|
|
312
|
+
peak_choice = np.ones((n_win_y, n_win_x), dtype=np.int32)
|
|
313
|
+
|
|
314
|
+
# Initial peak selection
|
|
315
|
+
ux_mat = pk_loc_x[0]
|
|
316
|
+
uy_mat = pk_loc_y[0]
|
|
317
|
+
|
|
318
|
+
nan_mask = np.isnan(ux_mat) | np.isnan(uy_mat)
|
|
319
|
+
|
|
320
|
+
# Apply outlier detection if enabled
|
|
321
|
+
if config.outlier_detection_enabled:
|
|
322
|
+
outlier_methods = config.outlier_detection_methods
|
|
323
|
+
if outlier_methods:
|
|
324
|
+
# Get primary peak magnitude for peak_mag detection
|
|
325
|
+
primary_peak_mag_temp = pk_height[0]
|
|
326
|
+
outlier_mask = apply_outlier_detection(
|
|
327
|
+
ux_mat, uy_mat, outlier_methods, peak_mag=primary_peak_mag_temp
|
|
328
|
+
)
|
|
329
|
+
nan_mask |= outlier_mask
|
|
330
|
+
|
|
331
|
+
if config.secondary_peak:
|
|
332
|
+
for pk in range(1, n_peaks):
|
|
333
|
+
# Increment peak_choice for nan_mask locations
|
|
334
|
+
peak_choice[nan_mask] += 1
|
|
335
|
+
# Clamp peak_choice to valid range
|
|
336
|
+
peak_choice = np.clip(peak_choice, 1, n_peaks)
|
|
337
|
+
# Select new peak for nan_mask locations
|
|
338
|
+
ux_mat = np.choose(peak_choice - 1, pk_loc_x)
|
|
339
|
+
uy_mat = np.choose(peak_choice - 1, pk_loc_y)
|
|
340
|
+
if config.outlier_detection_enabled:
|
|
341
|
+
outlier_methods = config.outlier_detection_methods
|
|
342
|
+
if outlier_methods:
|
|
343
|
+
primary_peak_mag_temp = np.choose(peak_choice - 1, pk_height)
|
|
344
|
+
outlier_mask = apply_outlier_detection(
|
|
345
|
+
ux_mat, uy_mat, outlier_methods, peak_mag=primary_peak_mag_temp
|
|
346
|
+
)
|
|
347
|
+
nan_mask |= outlier_mask
|
|
348
|
+
if not nan_mask.any():
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
# Select primary peak magnitude
|
|
352
|
+
primary_peak_mag = np.choose(peak_choice - 1, pk_height)
|
|
353
|
+
nan_mask |= np.isnan(primary_peak_mag)
|
|
354
|
+
|
|
355
|
+
nan_mask |= mask_bool
|
|
356
|
+
nan_mask |= primary_peak_mag < 0.2
|
|
357
|
+
|
|
358
|
+
# Q calculation (peak ratio)
|
|
359
|
+
shifted_pk_height = np.roll(pk_height, shift=-1, axis=0)
|
|
360
|
+
shifted_pk_height[-1, :, :] = pk_height[-1, :, :]
|
|
361
|
+
with warnings.catch_warnings():
|
|
362
|
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
363
|
+
Q_mat = np.divide(
|
|
364
|
+
pk_height,
|
|
365
|
+
shifted_pk_height,
|
|
366
|
+
out=np.zeros_like(pk_height),
|
|
367
|
+
where=shifted_pk_height > 0,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
Q = np.choose(peak_choice - 1, Q_mat)
|
|
371
|
+
|
|
372
|
+
if nan_mask.any():
|
|
373
|
+
ux_mat[nan_mask] = np.nan
|
|
374
|
+
uy_mat[nan_mask] = np.nan
|
|
375
|
+
primary_peak_mag[nan_mask] = np.nan
|
|
376
|
+
Q[nan_mask] = 0.0
|
|
377
|
+
|
|
378
|
+
ux_mat[mask_bool] = 0.0
|
|
379
|
+
uy_mat[mask_bool] = 0.0
|
|
380
|
+
|
|
381
|
+
# Apply infilling for mid-passes or final pass
|
|
382
|
+
is_final_pass = (pass_idx == len(config.window_sizes) - 1)
|
|
383
|
+
|
|
384
|
+
if is_final_pass:
|
|
385
|
+
# Final pass infilling (optional)
|
|
386
|
+
final_infill_cfg = config.infilling_final_pass
|
|
387
|
+
if final_infill_cfg.get('enabled', True) and np.isnan(ux_mat).any():
|
|
388
|
+
infill_mask = np.isnan(ux_mat) | np.isnan(uy_mat)
|
|
389
|
+
ux_mat, uy_mat = apply_infilling(
|
|
390
|
+
ux_mat, uy_mat, infill_mask, final_infill_cfg
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
# Mid-pass infilling (required for predictor)
|
|
394
|
+
if np.isnan(ux_mat).any() or np.isnan(uy_mat).any():
|
|
395
|
+
infill_mask = np.isnan(ux_mat) | np.isnan(uy_mat)
|
|
396
|
+
mid_infill_cfg = config.infilling_mid_pass
|
|
397
|
+
ux_mat, uy_mat = apply_infilling(
|
|
398
|
+
ux_mat, uy_mat, infill_mask, mid_infill_cfg
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
ux_mat[mask_bool] = 0.0
|
|
402
|
+
uy_mat[mask_bool] = 0.0
|
|
403
|
+
peak_choice[nan_mask] = 0
|
|
404
|
+
|
|
405
|
+
ux_mat = np.ascontiguousarray(ux_mat.astype(np.float32))
|
|
406
|
+
uy_mat = np.ascontiguousarray(uy_mat.astype(np.float32))
|
|
407
|
+
nan_mask = np.ascontiguousarray(nan_mask)
|
|
408
|
+
Q = np.ascontiguousarray(Q.astype(np.float32))
|
|
409
|
+
primary_peak_mag = np.ascontiguousarray(
|
|
410
|
+
np.where(nan_mask, 0.0, primary_peak_mag.astype(np.float32))
|
|
411
|
+
)
|
|
412
|
+
pk_height = np.ascontiguousarray(pk_height.astype(np.float32))
|
|
413
|
+
|
|
414
|
+
# Stack as [Y, X] to match im_mesh structure where [..., 0] = Y and [..., 1] = X
|
|
415
|
+
# This ensures correct image warping: im_mesh + delta_ab aligns Y with Y and X with X
|
|
416
|
+
self.delta_ab_old = np.stack([uy_mat, ux_mat], axis=2)
|
|
417
|
+
pre_y, pre_x = self.n_pre_all[pass_idx]
|
|
418
|
+
post_y, post_x = self.n_post_all[pass_idx]
|
|
419
|
+
self.delta_ab_old = np.pad(
|
|
420
|
+
self.delta_ab_old,
|
|
421
|
+
((pre_y, post_y), (pre_x, post_x), (0, 0)),
|
|
422
|
+
mode="edge",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
self.previous_win_spacing = (
|
|
426
|
+
self.win_spacing_y[pass_idx],
|
|
427
|
+
self.win_spacing_x[pass_idx],
|
|
428
|
+
)
|
|
429
|
+
self.prev_win_size = (n_win_y, n_win_x)
|
|
430
|
+
|
|
431
|
+
pass_result = PIVPassResult(
|
|
432
|
+
n_windows=np.array([n_win_y, n_win_x], dtype=np.int32),
|
|
433
|
+
ux_mat=np.copy(ux_mat),
|
|
434
|
+
uy_mat=np.copy(uy_mat),
|
|
435
|
+
nan_mask=np.copy(nan_mask),
|
|
436
|
+
peak_mag=np.copy(pk_height),
|
|
437
|
+
peak_choice=np.copy(peak_choice),
|
|
438
|
+
predictor_field=np.copy(self.delta_ab_old),
|
|
439
|
+
b_mask=b_mask.reshape((n_win_y, n_win_x)).astype(bool),
|
|
440
|
+
window_size=win_size,
|
|
441
|
+
win_ctrs_x=self.win_ctrs_x[pass_idx],
|
|
442
|
+
win_ctrs_y=self.win_ctrs_y[pass_idx],
|
|
443
|
+
|
|
444
|
+
)
|
|
445
|
+
pass_time = time.perf_counter() - pass_start
|
|
446
|
+
self.pass_times.append((n, pass_idx, pass_time))
|
|
447
|
+
piv_result_all.add_pass(pass_result)
|
|
448
|
+
|
|
449
|
+
# Explicit memory cleanup to prevent accumulation
|
|
450
|
+
# These large intermediate arrays can consume 500+ MB per pass
|
|
451
|
+
del pk_loc_x, pk_loc_y, pk_height, correl_plane_out
|
|
452
|
+
del image_a_prime, image_b_prime
|
|
453
|
+
del sx, sy, sxy, Q_mat
|
|
454
|
+
# Force garbage collection after last pass to release memory
|
|
455
|
+
if pass_idx == len(config.window_sizes) - 1:
|
|
456
|
+
import gc
|
|
457
|
+
gc.collect()
|
|
458
|
+
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
logging.error("Error in correlate_batch for image %d: %s", n, exc)
|
|
461
|
+
logging.error(traceback.format_exc())
|
|
462
|
+
raise
|
|
463
|
+
|
|
464
|
+
return piv_result_all
|
|
465
|
+
|
|
466
|
+
def _compute_window_centres(
|
|
467
|
+
self, pass_idx: int, config: Config
|
|
468
|
+
) -> tuple[int, int, np.ndarray, np.ndarray]:
|
|
469
|
+
"""
|
|
470
|
+
Compute window centers and spacing for a given pass.
|
|
471
|
+
|
|
472
|
+
Matches MATLAB logic exactly:
|
|
473
|
+
- win_ctrs_x spans the width dimension (Nx = W = columns)
|
|
474
|
+
- win_ctrs_y spans the height dimension (Ny = H = rows)
|
|
475
|
+
- Window centers are in pixel coordinates (0-based)
|
|
476
|
+
- X corresponds to horizontal (width), Y to vertical (height)
|
|
477
|
+
|
|
478
|
+
:param pass_idx: Index of the current pass.
|
|
479
|
+
:type pass_idx: int
|
|
480
|
+
:param config: Configuration object containing window sizes, overlap, and image shape.
|
|
481
|
+
:type config: Config
|
|
482
|
+
:return: Tuple containing window spacing in x and y, and arrays of window center coordinates in x and y.
|
|
483
|
+
:rtype: tuple[int, int, np.ndarray, np.ndarray]
|
|
484
|
+
"""
|
|
485
|
+
# Image dimensions: config.image_shape = (H, W) = (rows, cols)
|
|
486
|
+
H, W = config.image_shape
|
|
487
|
+
Ny = H # Number of rows (height)
|
|
488
|
+
Nx = W # Number of columns (width)
|
|
489
|
+
|
|
490
|
+
logging.debug(f"_compute_window_centres pass {pass_idx}:")
|
|
491
|
+
logging.debug(f" Image shape (H, W) = ({H}, {W})")
|
|
492
|
+
logging.debug(f" Ny (height/rows) = {Ny}, Nx (width/cols) = {Nx}")
|
|
493
|
+
|
|
494
|
+
# Window size: config.window_sizes[pass_idx] = (win_height, win_width)
|
|
495
|
+
# This matches MATLAB where wsize(1)=height, wsize(2)=width
|
|
496
|
+
win_height, win_width = config.window_sizes[pass_idx]
|
|
497
|
+
overlap = config.overlap[pass_idx]
|
|
498
|
+
|
|
499
|
+
logging.debug(f" Window size (H, W) = ({win_height}, {win_width})")
|
|
500
|
+
logging.debug(f" Overlap = {overlap}%")
|
|
501
|
+
|
|
502
|
+
# Window spacing in pixels
|
|
503
|
+
win_spacing_x = round((1 - overlap / 100) * win_width)
|
|
504
|
+
win_spacing_y = round((1 - overlap / 100) * win_height)
|
|
505
|
+
|
|
506
|
+
logging.debug(f" Window spacing (X, Y) = ({win_spacing_x}, {win_spacing_y})")
|
|
507
|
+
|
|
508
|
+
# MATLAB: win_ctrs_x = 0.5 + wsize(1)/2 : win_spacing_x : Nx - wsize(1)/2 + 0.5
|
|
509
|
+
# But MATLAB then subtracts 1 before passing to C (1-based to 0-based conversion)
|
|
510
|
+
# So in 0-based indexing: win_ctrs_x = -0.5 + wsize(1)/2 : win_spacing_x : Nx - wsize(1)/2 - 0.5
|
|
511
|
+
# For a 128-pixel window (indices 0-127), center is at 63.5
|
|
512
|
+
# For window starting at pixel 0: center = (0 + 127) / 2 = 63.5
|
|
513
|
+
|
|
514
|
+
# First window center in X (width dimension) - 0-based array indexing
|
|
515
|
+
first_ctr_x = (win_width - 1) / 2.0 # For 128: (127)/2 = 63.5
|
|
516
|
+
# Last possible window center in X
|
|
517
|
+
last_ctr_x = Nx - (win_width + 1) / 2.0 # For W=4872, win=128: 4872 - 64.5 = 4807.5
|
|
518
|
+
|
|
519
|
+
# First window center in Y (height dimension) - 0-based array indexing
|
|
520
|
+
first_ctr_y = (win_height - 1) / 2.0
|
|
521
|
+
# Last possible window center in Y
|
|
522
|
+
last_ctr_y = Ny - (win_height + 1) / 2.0
|
|
523
|
+
|
|
524
|
+
logging.debug(f" X range: [{first_ctr_x:.2f}, {last_ctr_x:.2f}]")
|
|
525
|
+
logging.debug(f" Y range: [{first_ctr_y:.2f}, {last_ctr_y:.2f}]")
|
|
526
|
+
|
|
527
|
+
# Number of windows that fit
|
|
528
|
+
n_win_x = int(np.floor((last_ctr_x - first_ctr_x) / win_spacing_x)) + 1
|
|
529
|
+
n_win_y = int(np.floor((last_ctr_y - first_ctr_y) / win_spacing_y)) + 1
|
|
530
|
+
|
|
531
|
+
# Ensure at least one window
|
|
532
|
+
n_win_x = max(1, n_win_x)
|
|
533
|
+
n_win_y = max(1, n_win_y)
|
|
534
|
+
|
|
535
|
+
logging.debug(f" Number of windows (X, Y) = ({n_win_x}, {n_win_y})")
|
|
536
|
+
|
|
537
|
+
# Generate window center arrays using linspace (matches MATLAB's colon operator)
|
|
538
|
+
win_ctrs_x = np.linspace(
|
|
539
|
+
first_ctr_x,
|
|
540
|
+
first_ctr_x + win_spacing_x * (n_win_x - 1),
|
|
541
|
+
n_win_x,
|
|
542
|
+
dtype=np.float32,
|
|
543
|
+
)
|
|
544
|
+
win_ctrs_y = np.linspace(
|
|
545
|
+
first_ctr_y,
|
|
546
|
+
first_ctr_y + win_spacing_y * (n_win_y - 1),
|
|
547
|
+
n_win_y,
|
|
548
|
+
dtype=np.float32,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
logging.debug(f" win_ctrs_x: min={win_ctrs_x.min():.2f}, max={win_ctrs_x.max():.2f}, len={len(win_ctrs_x)}")
|
|
552
|
+
logging.debug(f" win_ctrs_y: min={win_ctrs_y.min():.2f}, max={win_ctrs_y.max():.2f}, len={len(win_ctrs_y)}")
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
win_spacing_x,
|
|
556
|
+
win_spacing_y,
|
|
557
|
+
np.ascontiguousarray(win_ctrs_x),
|
|
558
|
+
np.ascontiguousarray(win_ctrs_y),
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def _check_args(self, *args):
|
|
562
|
+
"""Check the arguments for consistency and validity if debug mode is enabled.
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
*args : list of tuples
|
|
566
|
+
Each tuple contains (name, array) to be checked.
|
|
567
|
+
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
def _describe(arr):
|
|
571
|
+
if isinstance(arr, np.ndarray):
|
|
572
|
+
return (arr.shape, arr.dtype, arr.flags["C_CONTIGUOUS"])
|
|
573
|
+
return (type(arr), arr)
|
|
574
|
+
|
|
575
|
+
for name, arr in args:
|
|
576
|
+
logging.info(f"{name}: {_describe(arr)}")
|
|
577
|
+
@profile
|
|
578
|
+
def _predictor_corrector(
|
|
579
|
+
self,
|
|
580
|
+
pass_idx: int,
|
|
581
|
+
image_a: np.ndarray,
|
|
582
|
+
image_b: np.ndarray,
|
|
583
|
+
interpolator="cubic",
|
|
584
|
+
win_type="A",
|
|
585
|
+
):
|
|
586
|
+
"""Predictor-corrector step to adjust images based on previous displacement estimates."""
|
|
587
|
+
|
|
588
|
+
n_win_y = len(self.win_ctrs_y[pass_idx])
|
|
589
|
+
n_win_x = len(self.win_ctrs_x[pass_idx])
|
|
590
|
+
self.delta_ab_pred = np.zeros((n_win_y, n_win_x, 2), dtype=np.float32)
|
|
591
|
+
|
|
592
|
+
if pass_idx == 0:
|
|
593
|
+
if self.delta_ab_old is None:
|
|
594
|
+
self.delta_ab_old = np.zeros_like(self.delta_ab_pred)
|
|
595
|
+
|
|
596
|
+
self.prev_win_size = (n_win_y, n_win_x)
|
|
597
|
+
self.prev_win_spacing = (
|
|
598
|
+
self.win_spacing_y[pass_idx],
|
|
599
|
+
self.win_spacing_x[pass_idx],
|
|
600
|
+
)
|
|
601
|
+
return image_a.copy(), image_b.copy(), self.delta_ab_pred
|
|
602
|
+
|
|
603
|
+
if self.delta_ab_old is None:
|
|
604
|
+
raise RuntimeError("delta_ab_old is uninitialised before predictor step")
|
|
605
|
+
|
|
606
|
+
interp_flag = cv2.INTER_CUBIC if interpolator == "cubic" else cv2.INTER_LINEAR
|
|
607
|
+
|
|
608
|
+
self.delta_ab_old[..., 0] = gaussian_filter(
|
|
609
|
+
self.delta_ab_old[..., 0],
|
|
610
|
+
sigma=self.sd[pass_idx],
|
|
611
|
+
truncate=(self.ksize_filt[pass_idx][0] - 1) / (2 * self.sd[pass_idx]),
|
|
612
|
+
mode="nearest",
|
|
613
|
+
)
|
|
614
|
+
self.delta_ab_old[..., 1] = gaussian_filter(
|
|
615
|
+
self.delta_ab_old[..., 1],
|
|
616
|
+
sigma=self.sd[pass_idx],
|
|
617
|
+
truncate=(self.ksize_filt[pass_idx][0] - 1) / (2 * self.sd[pass_idx]),
|
|
618
|
+
mode="nearest",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
self.delta_ab_dense = np.zeros((self.H, self.W, 2), dtype=np.float32)
|
|
622
|
+
map_x_2d, map_y_2d = self.cached_dense_maps[pass_idx]
|
|
623
|
+
if map_x_2d is None or map_y_2d is None:
|
|
624
|
+
raise ValueError(f"Dense interpolation maps missing for pass {pass_idx}")
|
|
625
|
+
|
|
626
|
+
# Verify cached dense maps have correct shape
|
|
627
|
+
assert map_x_2d.shape == (self.H, self.W), f"Cached dense map X shape mismatch for pass {pass_idx}: {map_x_2d.shape} vs {(self.H, self.W)}"
|
|
628
|
+
assert map_y_2d.shape == (self.H, self.W), f"Cached dense map Y shape mismatch for pass {pass_idx}: {map_y_2d.shape} vs {(self.H, self.W)}"
|
|
629
|
+
logging.debug(f"Using cached dense interpolation maps for pass {pass_idx}")
|
|
630
|
+
|
|
631
|
+
for d in range(2):
|
|
632
|
+
self.delta_ab_dense[..., d] = cv2.remap(
|
|
633
|
+
self.delta_ab_old[..., d].astype(np.float32),
|
|
634
|
+
map_x_2d,
|
|
635
|
+
map_y_2d,
|
|
636
|
+
interp_flag,
|
|
637
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
638
|
+
borderValue=0,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
delta_0b = self.delta_ab_dense / 2
|
|
642
|
+
delta_0a = -delta_0b
|
|
643
|
+
im_mesh_A = self.im_mesh + delta_0a
|
|
644
|
+
im_mesh_B = self.im_mesh + delta_0b
|
|
645
|
+
|
|
646
|
+
map_x, map_y = self.cached_predictor_maps[pass_idx]
|
|
647
|
+
if map_x is None or map_y is None:
|
|
648
|
+
raise ValueError(f"Predictor interpolation maps missing for pass {pass_idx}")
|
|
649
|
+
|
|
650
|
+
# Verify cached predictor maps have correct shape
|
|
651
|
+
expected_pred_shape = (len(self.win_ctrs_y[pass_idx]), len(self.win_ctrs_x[pass_idx]))
|
|
652
|
+
assert map_x.shape == expected_pred_shape, f"Cached predictor map X shape mismatch for pass {pass_idx}: {map_x.shape} vs {expected_pred_shape}"
|
|
653
|
+
assert map_y.shape == expected_pred_shape, f"Cached predictor map Y shape mismatch for pass {pass_idx}: {map_y.shape} vs {expected_pred_shape}"
|
|
654
|
+
logging.debug(f"Using cached predictor interpolation maps for pass {pass_idx}")
|
|
655
|
+
|
|
656
|
+
for d in range(2):
|
|
657
|
+
remapped = cv2.remap(
|
|
658
|
+
self.delta_ab_old[..., d].astype(np.float32),
|
|
659
|
+
map_x,
|
|
660
|
+
map_y,
|
|
661
|
+
interp_flag,
|
|
662
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
663
|
+
borderValue=0.0,
|
|
664
|
+
)
|
|
665
|
+
self.delta_ab_pred[..., d] = remapped
|
|
666
|
+
|
|
667
|
+
image_a_prime = cv2.remap(
|
|
668
|
+
image_a.astype(np.float32),
|
|
669
|
+
im_mesh_A[..., 1].astype(np.float32),
|
|
670
|
+
im_mesh_A[..., 0].astype(np.float32),
|
|
671
|
+
cv2.INTER_CUBIC,
|
|
672
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
673
|
+
borderValue=0,
|
|
674
|
+
)
|
|
675
|
+
image_b_prime = cv2.remap(
|
|
676
|
+
image_b.astype(np.float32),
|
|
677
|
+
im_mesh_B[..., 1].astype(np.float32),
|
|
678
|
+
im_mesh_B[..., 0].astype(np.float32),
|
|
679
|
+
cv2.INTER_CUBIC,
|
|
680
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
681
|
+
borderValue=0,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
return image_a_prime, image_b_prime, self.delta_ab_pred
|
|
685
|
+
@profile
|
|
686
|
+
def _set_lib_arguments(
|
|
687
|
+
self,
|
|
688
|
+
config: Config,
|
|
689
|
+
win_size: np.ndarray,
|
|
690
|
+
pass_idx: int,
|
|
691
|
+
):
|
|
692
|
+
"""Set library arguments for PIV computation.
|
|
693
|
+
|
|
694
|
+
:param config: Configuration object.
|
|
695
|
+
:type config: Config
|
|
696
|
+
:param win_size: Window size.
|
|
697
|
+
:type win_size: np.ndarray
|
|
698
|
+
:param pass_idx: Pass index.
|
|
699
|
+
:type pass_idx: int
|
|
700
|
+
:return: Tuple of library arguments.
|
|
701
|
+
:rtype: tuple
|
|
702
|
+
"""
|
|
703
|
+
# Window size: [win_height, win_width] in C-contiguous format
|
|
704
|
+
win_size = np.ascontiguousarray(np.array(win_size, dtype=np.int32))
|
|
705
|
+
|
|
706
|
+
n_win_y = len(self.win_ctrs_y[pass_idx])
|
|
707
|
+
n_win_x = len(self.win_ctrs_x[pass_idx])
|
|
708
|
+
# nWindows: [n_win_y, n_win_x] where n_win_y = rows, n_win_x = cols
|
|
709
|
+
n_windows = np.ascontiguousarray(
|
|
710
|
+
np.array([n_win_y, n_win_x], dtype=np.int32)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
total_windows = n_win_y * n_win_x
|
|
714
|
+
|
|
715
|
+
# Use precomputed vector mask for this pass if available
|
|
716
|
+
# Mask shape: (n_win_y, n_win_x) in C-contiguous format
|
|
717
|
+
if hasattr(self, 'vector_masks') and self.vector_masks and pass_idx < len(self.vector_masks):
|
|
718
|
+
cached_mask = self.vector_masks[pass_idx]
|
|
719
|
+
b_mask = np.ascontiguousarray(cached_mask.astype(np.float32))
|
|
720
|
+
else:
|
|
721
|
+
b_mask = np.ascontiguousarray(np.zeros((n_win_y, n_win_x), dtype=np.float32))
|
|
722
|
+
logging.debug("No vector mask applied for pass %d", pass_idx)
|
|
723
|
+
|
|
724
|
+
n_peaks = np.int32(config.num_peaks)
|
|
725
|
+
i_peak_finder = np.int32(config.peak_finder)
|
|
726
|
+
b_ensemble = bool(config.ensemble_piv)
|
|
727
|
+
|
|
728
|
+
# Output arrays shape: (n_peaks, n_win_y, n_win_x) in C-contiguous format
|
|
729
|
+
out_shape = (n_peaks, n_win_y, n_win_x)
|
|
730
|
+
pk_loc_x = np.zeros(out_shape, dtype=np.float32)
|
|
731
|
+
pk_loc_y = np.zeros(out_shape, dtype=np.float32)
|
|
732
|
+
pk_height = np.zeros(out_shape, dtype=np.float32)
|
|
733
|
+
sx = np.zeros(out_shape, dtype=np.float32)
|
|
734
|
+
sy = np.zeros(out_shape, dtype=np.float32)
|
|
735
|
+
sxy = np.zeros(out_shape, dtype=np.float32)
|
|
736
|
+
|
|
737
|
+
# Correlation plane output: flattened array (not used, so use empty to save memory)
|
|
738
|
+
correl_plane_out = np.empty(total_windows * win_size[0] * win_size[1], dtype=np.float32)
|
|
739
|
+
|
|
740
|
+
if config.debug:
|
|
741
|
+
args = [
|
|
742
|
+
("mask", b_mask),
|
|
743
|
+
("win_ctrs_x", self.win_ctrs_x[pass_idx].astype(np.float32)),
|
|
744
|
+
("win_ctrs_y", self.win_ctrs_y[pass_idx].astype(np.float32)),
|
|
745
|
+
("n_windows", n_windows),
|
|
746
|
+
("window_weight_a", self.win_weights[pass_idx]),
|
|
747
|
+
("b_ensemble", b_ensemble),
|
|
748
|
+
("window_weight_b", self.win_weights[pass_idx]),
|
|
749
|
+
("win_size", win_size),
|
|
750
|
+
("n_peaks", int(n_peaks)),
|
|
751
|
+
("i_peak_finder", int(i_peak_finder)),
|
|
752
|
+
("pk_loc_x", pk_loc_x),
|
|
753
|
+
("pk_loc_y", pk_loc_y),
|
|
754
|
+
("pk_height", pk_height),
|
|
755
|
+
("sx", sx),
|
|
756
|
+
("sy", sy),
|
|
757
|
+
("sxy", sxy),
|
|
758
|
+
("correl_plane_out", correl_plane_out),
|
|
759
|
+
]
|
|
760
|
+
self._check_args(*args)
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
win_size,
|
|
764
|
+
n_windows,
|
|
765
|
+
b_mask,
|
|
766
|
+
n_peaks,
|
|
767
|
+
i_peak_finder,
|
|
768
|
+
b_ensemble,
|
|
769
|
+
pk_loc_x,
|
|
770
|
+
pk_loc_y,
|
|
771
|
+
pk_height,
|
|
772
|
+
sx,
|
|
773
|
+
sy,
|
|
774
|
+
sxy,
|
|
775
|
+
correl_plane_out,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
@profile
|
|
779
|
+
def _cache_window_padding(self, config: Config) -> None:
|
|
780
|
+
"""Cache window padding information.
|
|
781
|
+
|
|
782
|
+
:param config: Configuration object.
|
|
783
|
+
:type config: Config
|
|
784
|
+
"""
|
|
785
|
+
self.win_ctrs_x: list[np.ndarray] = []
|
|
786
|
+
self.win_ctrs_y: list[np.ndarray] = []
|
|
787
|
+
self.win_spacing_x: list[int] = []
|
|
788
|
+
self.win_spacing_y: list[int] = []
|
|
789
|
+
self.win_ctrs_x_all: list[np.ndarray] = []
|
|
790
|
+
self.win_ctrs_y_all: list[np.ndarray] = []
|
|
791
|
+
self.n_pre_all: list[tuple[int, int]] = []
|
|
792
|
+
self.n_post_all: list[tuple[int, int]] = []
|
|
793
|
+
self.ksize_filt: list[tuple[int, int]] = []
|
|
794
|
+
self.sd: list[float] = []
|
|
795
|
+
self.G_smooth_predictor: list[np.ndarray] = []
|
|
796
|
+
|
|
797
|
+
H, W = config.image_shape
|
|
798
|
+
|
|
799
|
+
for pass_idx, _ in enumerate(config.window_sizes):
|
|
800
|
+
spacing_x, spacing_y, win_ctrs_x, win_ctrs_y = self._compute_window_centres(
|
|
801
|
+
pass_idx, config
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
win_ctrs_x_pre = np.arange(1, win_ctrs_x[0] - spacing_x / 2, spacing_x)
|
|
805
|
+
if win_ctrs_x_pre.size == 0:
|
|
806
|
+
win_ctrs_x_pre = np.array([1])
|
|
807
|
+
win_ctrs_x_pre -= 1
|
|
808
|
+
win_ctrs_x_post = np.arange(
|
|
809
|
+
W, win_ctrs_x[-1] + spacing_x / 2, -spacing_x
|
|
810
|
+
)
|
|
811
|
+
if win_ctrs_x_post.size == 0:
|
|
812
|
+
win_ctrs_x_post = np.array([W])
|
|
813
|
+
win_ctrs_x_post -= 1
|
|
814
|
+
win_ctrs_x_all = np.concatenate(
|
|
815
|
+
[win_ctrs_x_pre, win_ctrs_x, win_ctrs_x_post[::-1]]
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
win_ctrs_y_pre = np.arange(1, win_ctrs_y[0] - spacing_y / 2, spacing_y)
|
|
819
|
+
if win_ctrs_y_pre.size == 0:
|
|
820
|
+
win_ctrs_y_pre = np.array([1])
|
|
821
|
+
win_ctrs_y_pre -= 1
|
|
822
|
+
win_ctrs_y_post = np.arange(
|
|
823
|
+
H, win_ctrs_y[-1] + spacing_y / 2, -spacing_y
|
|
824
|
+
)
|
|
825
|
+
if win_ctrs_y_post.size == 0:
|
|
826
|
+
win_ctrs_y_post = np.array([H])
|
|
827
|
+
win_ctrs_y_post -= 1
|
|
828
|
+
win_ctrs_y_all = np.concatenate(
|
|
829
|
+
[win_ctrs_y_pre, win_ctrs_y, win_ctrs_y_post[::-1]]
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
n_pre = (len(win_ctrs_y_pre), len(win_ctrs_x_pre))
|
|
833
|
+
n_post = (len(win_ctrs_y_post), len(win_ctrs_x_post))
|
|
834
|
+
|
|
835
|
+
self.win_ctrs_x.append(win_ctrs_x.astype(np.float32))
|
|
836
|
+
self.win_ctrs_y.append(win_ctrs_y.astype(np.float32))
|
|
837
|
+
self.win_spacing_x.append(spacing_x)
|
|
838
|
+
self.win_spacing_y.append(spacing_y)
|
|
839
|
+
self.win_ctrs_x_all.append(win_ctrs_x_all.astype(np.float32))
|
|
840
|
+
self.win_ctrs_y_all.append(win_ctrs_y_all.astype(np.float32))
|
|
841
|
+
self.n_pre_all.append(n_pre)
|
|
842
|
+
self.n_post_all.append(n_post)
|
|
843
|
+
|
|
844
|
+
if pass_idx == 0:
|
|
845
|
+
self.ksize_filt.append((0, 0))
|
|
846
|
+
self.sd.append(0.0)
|
|
847
|
+
self.G_smooth_predictor.append(np.ones((1, 1), dtype=np.float32))
|
|
848
|
+
else:
|
|
849
|
+
prev_counts = (
|
|
850
|
+
len(self.win_ctrs_y[pass_idx - 1]),
|
|
851
|
+
len(self.win_ctrs_x[pass_idx - 1]),
|
|
852
|
+
)
|
|
853
|
+
prev_spacing = (
|
|
854
|
+
self.win_spacing_y[pass_idx - 1],
|
|
855
|
+
self.win_spacing_x[pass_idx - 1],
|
|
856
|
+
)
|
|
857
|
+
k_filt = (
|
|
858
|
+
np.round(np.array(prev_counts) / np.array(prev_spacing)).astype(int)
|
|
859
|
+
+ 1
|
|
860
|
+
)
|
|
861
|
+
k_filt_list = [int(k) for k in k_filt.tolist()]
|
|
862
|
+
k_filt_tuple = (
|
|
863
|
+
k_filt_list[0] + (k_filt_list[0] % 2 == 0),
|
|
864
|
+
k_filt_list[1] + (k_filt_list[1] % 2 == 0),
|
|
865
|
+
)
|
|
866
|
+
self.ksize_filt.append(k_filt_tuple)
|
|
867
|
+
self.sd.append(np.sqrt(np.prod(k_filt_tuple)) / 3 * 0.65)
|
|
868
|
+
g_kernel = self._window_weight_fun(k_filt_tuple, config.window_type)
|
|
869
|
+
g_kernel = g_kernel.astype(np.float32)
|
|
870
|
+
g_kernel /= max(np.sum(g_kernel), 1e-12)
|
|
871
|
+
self.G_smooth_predictor.append(g_kernel)
|
|
872
|
+
|
|
873
|
+
# # Verify window padding cache integrity
|
|
874
|
+
# assert len(self.win_ctrs_x) == len(config.window_sizes), f"Window centers X cache length mismatch: {len(self.win_ctrs_x)} vs {len(config.window_sizes)}"
|
|
875
|
+
# assert len(self.win_ctrs_y) == len(config.window_sizes), f"Window centers Y cache length mismatch: {len(self.win_ctrs_y)} vs {len(config.window_sizes)}"
|
|
876
|
+
# assert len(self.win_spacing_x) == len(config.window_sizes), f"Window spacing X cache length mismatch: {len(self.win_spacing_x)} vs {len(config.window_sizes)}"
|
|
877
|
+
# assert len(self.win_spacing_y) == len(config.window_sizes), f"Window spacing Y cache length mismatch: {len(self.win_spacing_y)} vs {len(config.window_sizes)}"
|
|
878
|
+
|
|
879
|
+
# # Check that cached values are reasonable
|
|
880
|
+
# for pass_idx in range(len(config.window_sizes)):
|
|
881
|
+
# assert len(self.win_ctrs_x[pass_idx]) > 0, f"No X window centers cached for pass {pass_idx}"
|
|
882
|
+
# assert len(self.win_ctrs_y[pass_idx]) > 0, f"No Y window centers cached for pass {pass_idx}"
|
|
883
|
+
# assert self.win_spacing_x[pass_idx] > 0, f"Invalid X spacing for pass {pass_idx}: {self.win_spacing_x[pass_idx]}"
|
|
884
|
+
# assert self.win_spacing_y[pass_idx] > 0, f"Invalid Y spacing for pass {pass_idx}: {self.win_spacing_y[pass_idx]}"
|
|
885
|
+
|
|
886
|
+
logging.info(f"Successfully cached window padding for {len(config.window_sizes)} passes")
|
|
887
|
+
|
|
888
|
+
@profile
|
|
889
|
+
@profile
|
|
890
|
+
def _cache_interpolation_grids(self, config: Config) -> None:
|
|
891
|
+
"""Cache interpolation grid coordinates for reuse across passes.
|
|
892
|
+
|
|
893
|
+
This significantly improves performance by avoiding repeated
|
|
894
|
+
computation of coordinate grids.
|
|
895
|
+
|
|
896
|
+
:param config: Configuration object.
|
|
897
|
+
:type config: Config
|
|
898
|
+
"""
|
|
899
|
+
# Cache the image mesh for dense interpolation
|
|
900
|
+
y_coords = np.arange(self.H, dtype=np.float32)
|
|
901
|
+
x_coords = np.arange(self.W, dtype=np.float32)
|
|
902
|
+
x_mesh, y_mesh = np.meshgrid(x_coords, y_coords)
|
|
903
|
+
self.im_mesh = np.stack([y_mesh, x_mesh], axis=-1)
|
|
904
|
+
|
|
905
|
+
# Pre-cache coordinate mappings for each pass
|
|
906
|
+
self.cached_dense_maps = []
|
|
907
|
+
self.cached_predictor_maps = []
|
|
908
|
+
|
|
909
|
+
for pass_idx in range(len(config.window_sizes)):
|
|
910
|
+
if pass_idx == 0:
|
|
911
|
+
self.cached_dense_maps.append(None)
|
|
912
|
+
self.cached_predictor_maps.append(None)
|
|
913
|
+
else:
|
|
914
|
+
# Cache dense interpolation maps
|
|
915
|
+
points = (
|
|
916
|
+
self.win_ctrs_y_all[pass_idx - 1],
|
|
917
|
+
self.win_ctrs_x_all[pass_idx - 1]
|
|
918
|
+
)
|
|
919
|
+
map_x_1d = np.interp(
|
|
920
|
+
x_coords, points[1], np.arange(len(points[1]))
|
|
921
|
+
)
|
|
922
|
+
map_y_1d = np.interp(
|
|
923
|
+
y_coords, points[0], np.arange(len(points[0]))
|
|
924
|
+
)
|
|
925
|
+
map_x_2d, map_y_2d = np.meshgrid(
|
|
926
|
+
map_x_1d.astype(np.float32),
|
|
927
|
+
map_y_1d.astype(np.float32)
|
|
928
|
+
)
|
|
929
|
+
self.cached_dense_maps.append((map_x_2d, map_y_2d))
|
|
930
|
+
|
|
931
|
+
# Cache predictor interpolation maps
|
|
932
|
+
win_x, win_y = np.meshgrid(
|
|
933
|
+
self.win_ctrs_x[pass_idx],
|
|
934
|
+
self.win_ctrs_y[pass_idx]
|
|
935
|
+
)
|
|
936
|
+
ix = np.interp(
|
|
937
|
+
win_x.ravel(), points[1], np.arange(len(points[1]))
|
|
938
|
+
)
|
|
939
|
+
iy = np.interp(
|
|
940
|
+
win_y.ravel(), points[0], np.arange(len(points[0]))
|
|
941
|
+
)
|
|
942
|
+
map_x = ix.reshape(win_x.shape).astype(np.float32)
|
|
943
|
+
map_y = iy.reshape(win_x.shape).astype(np.float32)
|
|
944
|
+
self.cached_predictor_maps.append((map_x, map_y))
|
|
945
|
+
|
|
946
|
+
# Verify caching integrity
|
|
947
|
+
assert len(self.cached_dense_maps) == len(config.window_sizes), f"Dense maps cache length mismatch: {len(self.cached_dense_maps)} vs {len(config.window_sizes)}"
|
|
948
|
+
assert len(self.cached_predictor_maps) == len(config.window_sizes), f"Predictor maps cache length mismatch: {len(self.cached_predictor_maps)} vs {len(config.window_sizes)}"
|
|
949
|
+
|
|
950
|
+
# Check that non-zero passes have cached maps
|
|
951
|
+
for pass_idx in range(1, len(config.window_sizes)):
|
|
952
|
+
assert self.cached_dense_maps[pass_idx] is not None, f"Dense map for pass {pass_idx} is None"
|
|
953
|
+
assert self.cached_predictor_maps[pass_idx] is not None, f"Predictor map for pass {pass_idx} is None"
|
|
954
|
+
dense_x, dense_y = self.cached_dense_maps[pass_idx]
|
|
955
|
+
pred_x, pred_y = self.cached_predictor_maps[pass_idx]
|
|
956
|
+
assert dense_x.shape == (self.H, self.W), f"Dense map X shape incorrect for pass {pass_idx}: {dense_x.shape} vs {(self.H, self.W)}"
|
|
957
|
+
assert dense_y.shape == (self.H, self.W), f"Dense map Y shape incorrect for pass {pass_idx}: {dense_y.shape} vs {(self.H, self.W)}"
|
|
958
|
+
expected_pred_shape = (len(self.win_ctrs_y[pass_idx]), len(self.win_ctrs_x[pass_idx]))
|
|
959
|
+
assert pred_x.shape == expected_pred_shape, f"Predictor map X shape incorrect for pass {pass_idx}: {pred_x.shape} vs {expected_pred_shape}"
|
|
960
|
+
assert pred_y.shape == expected_pred_shape, f"Predictor map Y shape incorrect for pass {pass_idx}: {pred_y.shape} vs {expected_pred_shape}"
|
|
961
|
+
|
|
962
|
+
logging.info(f"Successfully cached interpolation grids for {len(config.window_sizes)} passes")
|
|
963
|
+
|
|
964
|
+
# def _apply_mask_to_vectors(
|
|
965
|
+
# self,
|
|
966
|
+
# win_ctrs_x: np.ndarray,
|
|
967
|
+
# win_ctrs_y: np.ndarray,
|
|
968
|
+
# mask: np.ndarray
|
|
969
|
+
# ) -> np.ndarray:
|
|
970
|
+
# """
|
|
971
|
+
# Apply user-defined mask to invalidate vectors in masked regions.
|
|
972
|
+
|
|
973
|
+
# A vector is invalidated if its window center falls within a masked region
|
|
974
|
+
# (where mask == True).
|
|
975
|
+
|
|
976
|
+
# Parameters
|
|
977
|
+
# ----------
|
|
978
|
+
# win_ctrs_x : np.ndarray
|
|
979
|
+
# 1D array of window center x-coordinates
|
|
980
|
+
# win_ctrs_y : np.ndarray
|
|
981
|
+
# 1D array of window center y-coordinates
|
|
982
|
+
# mask : np.ndarray
|
|
983
|
+
# Boolean mask array of shape (H, W) where True indicates masked regions
|
|
984
|
+
|
|
985
|
+
# Returns
|
|
986
|
+
# -------
|
|
987
|
+
# np.ndarray
|
|
988
|
+
# Boolean mask of shape (len(win_ctrs_y), len(win_ctrs_x)) where
|
|
989
|
+
# True indicates vectors to invalidate
|
|
990
|
+
# """
|
|
991
|
+
# grid_y, grid_x = np.meshgrid(win_ctrs_y, win_ctrs_x, indexing="ij")
|
|
992
|
+
|
|
993
|
+
# # Round to nearest pixel indices
|
|
994
|
+
# x_idx = np.round(grid_x).astype(int)
|
|
995
|
+
# y_idx = np.round(grid_y).astype(int)
|
|
996
|
+
|
|
997
|
+
# # Clip to valid image bounds
|
|
998
|
+
# x_idx = np.clip(x_idx, 0, mask.shape[1] - 1)
|
|
999
|
+
# y_idx = np.clip(y_idx, 0, mask.shape[0] - 1)
|
|
1000
|
+
|
|
1001
|
+
# # Sample mask at window center locations
|
|
1002
|
+
# # mask[y, x] where True = masked region
|
|
1003
|
+
# vector_mask = mask[y_idx, x_idx]
|
|
1004
|
+
|
|
1005
|
+
# return vector_mask
|