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.
Files changed (127) hide show
  1. pivtools-0.1.3.dist-info/METADATA +222 -0
  2. pivtools-0.1.3.dist-info/RECORD +127 -0
  3. pivtools-0.1.3.dist-info/WHEEL +5 -0
  4. pivtools-0.1.3.dist-info/entry_points.txt +3 -0
  5. pivtools-0.1.3.dist-info/top_level.txt +3 -0
  6. pivtools_cli/__init__.py +5 -0
  7. pivtools_cli/_build_marker.c +25 -0
  8. pivtools_cli/_build_marker.cp311-win_amd64.pyd +0 -0
  9. pivtools_cli/cli.py +225 -0
  10. pivtools_cli/example.py +139 -0
  11. pivtools_cli/lib/PIV_2d_cross_correlate.c +334 -0
  12. pivtools_cli/lib/PIV_2d_cross_correlate.h +22 -0
  13. pivtools_cli/lib/common.h +36 -0
  14. pivtools_cli/lib/interp2custom.c +146 -0
  15. pivtools_cli/lib/interp2custom.h +48 -0
  16. pivtools_cli/lib/peak_locate_gsl.c +711 -0
  17. pivtools_cli/lib/peak_locate_gsl.h +40 -0
  18. pivtools_cli/lib/peak_locate_gsl_print.c +736 -0
  19. pivtools_cli/lib/peak_locate_lm.c +751 -0
  20. pivtools_cli/lib/peak_locate_lm.h +27 -0
  21. pivtools_cli/lib/xcorr.c +342 -0
  22. pivtools_cli/lib/xcorr.h +31 -0
  23. pivtools_cli/lib/xcorr_cache.c +78 -0
  24. pivtools_cli/lib/xcorr_cache.h +26 -0
  25. pivtools_cli/piv/interp2custom/interp2custom.py +69 -0
  26. pivtools_cli/piv/piv.py +240 -0
  27. pivtools_cli/piv/piv_backend/base.py +825 -0
  28. pivtools_cli/piv/piv_backend/cpu_instantaneous.py +1005 -0
  29. pivtools_cli/piv/piv_backend/factory.py +28 -0
  30. pivtools_cli/piv/piv_backend/gpu_instantaneous.py +15 -0
  31. pivtools_cli/piv/piv_backend/infilling.py +445 -0
  32. pivtools_cli/piv/piv_backend/outlier_detection.py +306 -0
  33. pivtools_cli/piv/piv_backend/profile_cpu_instantaneous.py +230 -0
  34. pivtools_cli/piv/piv_result.py +40 -0
  35. pivtools_cli/piv/save_results.py +342 -0
  36. pivtools_cli/piv_cluster/cluster.py +108 -0
  37. pivtools_cli/preprocessing/filters.py +399 -0
  38. pivtools_cli/preprocessing/preprocess.py +79 -0
  39. pivtools_cli/tests/helpers.py +107 -0
  40. pivtools_cli/tests/instantaneous_piv/test_piv_integration.py +167 -0
  41. pivtools_cli/tests/instantaneous_piv/test_piv_integration_multi.py +553 -0
  42. pivtools_cli/tests/preprocessing/test_filters.py +41 -0
  43. pivtools_core/__init__.py +5 -0
  44. pivtools_core/config.py +703 -0
  45. pivtools_core/config.yaml +135 -0
  46. pivtools_core/image_handling/__init__.py +0 -0
  47. pivtools_core/image_handling/load_images.py +464 -0
  48. pivtools_core/image_handling/readers/__init__.py +53 -0
  49. pivtools_core/image_handling/readers/generic_readers.py +50 -0
  50. pivtools_core/image_handling/readers/lavision_reader.py +190 -0
  51. pivtools_core/image_handling/readers/registry.py +24 -0
  52. pivtools_core/paths.py +49 -0
  53. pivtools_core/vector_loading.py +248 -0
  54. pivtools_gui/__init__.py +3 -0
  55. pivtools_gui/app.py +687 -0
  56. pivtools_gui/calibration/__init__.py +0 -0
  57. pivtools_gui/calibration/app/__init__.py +0 -0
  58. pivtools_gui/calibration/app/views.py +1186 -0
  59. pivtools_gui/calibration/calibration_planar/planar_calibration_production.py +570 -0
  60. pivtools_gui/calibration/vector_calibration_production.py +544 -0
  61. pivtools_gui/config.py +703 -0
  62. pivtools_gui/image_handling/__init__.py +0 -0
  63. pivtools_gui/image_handling/load_images.py +464 -0
  64. pivtools_gui/image_handling/readers/__init__.py +53 -0
  65. pivtools_gui/image_handling/readers/generic_readers.py +50 -0
  66. pivtools_gui/image_handling/readers/lavision_reader.py +190 -0
  67. pivtools_gui/image_handling/readers/registry.py +24 -0
  68. pivtools_gui/masking/__init__.py +0 -0
  69. pivtools_gui/masking/app/__init__.py +0 -0
  70. pivtools_gui/masking/app/views.py +123 -0
  71. pivtools_gui/paths.py +49 -0
  72. pivtools_gui/piv_runner.py +261 -0
  73. pivtools_gui/pivtools.py +58 -0
  74. pivtools_gui/plotting/__init__.py +0 -0
  75. pivtools_gui/plotting/app/__init__.py +0 -0
  76. pivtools_gui/plotting/app/views.py +1671 -0
  77. pivtools_gui/plotting/plot_maker.py +220 -0
  78. pivtools_gui/post_processing/POD/__init__.py +0 -0
  79. pivtools_gui/post_processing/POD/app/__init__.py +0 -0
  80. pivtools_gui/post_processing/POD/app/views.py +647 -0
  81. pivtools_gui/post_processing/POD/pod_decompose.py +979 -0
  82. pivtools_gui/post_processing/POD/views.py +1096 -0
  83. pivtools_gui/post_processing/__init__.py +0 -0
  84. pivtools_gui/static/404.html +1 -0
  85. pivtools_gui/static/_next/static/chunks/117-d5793c8e79de5511.js +2 -0
  86. pivtools_gui/static/_next/static/chunks/484-cfa8b9348ce4f00e.js +1 -0
  87. pivtools_gui/static/_next/static/chunks/869-320a6b9bdafbb6d3.js +1 -0
  88. pivtools_gui/static/_next/static/chunks/app/_not-found/page-12f067ceb7415e55.js +1 -0
  89. pivtools_gui/static/_next/static/chunks/app/layout-b907d5f31ac82e9d.js +1 -0
  90. pivtools_gui/static/_next/static/chunks/app/page-334cc4e8444cde2f.js +1 -0
  91. pivtools_gui/static/_next/static/chunks/fd9d1056-ad15f396ddf9b7e5.js +1 -0
  92. pivtools_gui/static/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  93. pivtools_gui/static/_next/static/chunks/main-a1b3ced4d5f6d998.js +1 -0
  94. pivtools_gui/static/_next/static/chunks/main-app-8a63c6f5e7baee11.js +1 -0
  95. pivtools_gui/static/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  96. pivtools_gui/static/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  97. pivtools_gui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  98. pivtools_gui/static/_next/static/chunks/webpack-4a8ca7c99e9bb3d8.js +1 -0
  99. pivtools_gui/static/_next/static/css/7d3f2337d7ea12a5.css +3 -0
  100. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_buildManifest.js +1 -0
  101. pivtools_gui/static/_next/static/vQeR20OUdSSKlK4vukC4q/_ssgManifest.js +1 -0
  102. pivtools_gui/static/file.svg +1 -0
  103. pivtools_gui/static/globe.svg +1 -0
  104. pivtools_gui/static/grid.svg +8 -0
  105. pivtools_gui/static/index.html +1 -0
  106. pivtools_gui/static/index.txt +8 -0
  107. pivtools_gui/static/next.svg +1 -0
  108. pivtools_gui/static/vercel.svg +1 -0
  109. pivtools_gui/static/window.svg +1 -0
  110. pivtools_gui/stereo_reconstruction/__init__.py +0 -0
  111. pivtools_gui/stereo_reconstruction/app/__init__.py +0 -0
  112. pivtools_gui/stereo_reconstruction/app/views.py +1985 -0
  113. pivtools_gui/stereo_reconstruction/stereo_calibration_production.py +606 -0
  114. pivtools_gui/stereo_reconstruction/stereo_reconstruction_production.py +544 -0
  115. pivtools_gui/utils.py +63 -0
  116. pivtools_gui/vector_loading.py +248 -0
  117. pivtools_gui/vector_merging/__init__.py +1 -0
  118. pivtools_gui/vector_merging/app/__init__.py +1 -0
  119. pivtools_gui/vector_merging/app/views.py +759 -0
  120. pivtools_gui/vector_statistics/app/__init__.py +1 -0
  121. pivtools_gui/vector_statistics/app/views.py +710 -0
  122. pivtools_gui/vector_statistics/ensemble_statistics.py +49 -0
  123. pivtools_gui/vector_statistics/instantaneous_statistics.py +311 -0
  124. pivtools_gui/video_maker/__init__.py +0 -0
  125. pivtools_gui/video_maker/app/__init__.py +0 -0
  126. pivtools_gui/video_maker/app/views.py +436 -0
  127. 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