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,544 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ vector_calibration_production.py
4
+
5
+ Production script for calibrating uncalibrated PIV vectors to physical units (m/s).
6
+ Converts pixel-based vectors to physical velocities using camera calibration models.
7
+ """
8
+
9
+ import logging
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import cv2
14
+ import numpy as np
15
+ from scipy.io import loadmat, savemat
16
+
17
+ sys.path.append(str(Path(__file__).parent.parent))
18
+ from ..paths import get_data_paths
19
+ from ..vector_loading import load_coords_from_directory, read_mat_contents
20
+
21
+ # ===================== CONFIGURATION VARIABLES =====================
22
+ # Set these variables for your calibration setup
23
+ BASE_DIR = "/Users/morgan/Library/CloudStorage/OneDrive-UniversityofSouthampton/Documents/#current_processing/query_JHTDB/Planar_Images_with_wall/test"
24
+ image_count = 1000
25
+ DT_SECONDS = 1 # Time step between frames in seconds
26
+ CAMERA_NUM = 1 # Camera number (1-based)
27
+ MODEL_INDEX = 0 # Index of calibration model to use (0-based)
28
+ DOT_SPACING_MM = 28.89 # Physical spacing between calibration dots in mm
29
+ VECTOR_PATTERN = "%05d.mat" # Pattern for vector files (e.g. "B%05d.mat", "%05d.mat")
30
+ TYPE_NAME = "instantaneous" # Type name for uncalibrated data directory (e.g. "Instantaneous", "piv")
31
+ # Example: RUNS_TO_PROCESS = [1, 2, 3] # Process only runs 1, 2, and 3
32
+ # ===================================================================
33
+
34
+ # Add src to path to import modules
35
+
36
+
37
+ # Configure logging
38
+ logging.basicConfig(
39
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
40
+ )
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class VectorCalibrator:
45
+ def __init__(
46
+ self,
47
+ base_dir,
48
+ camera_num,
49
+ model_index,
50
+ dt,
51
+ dot_spacing_mm=28.89,
52
+ vector_pattern="%05d.mat",
53
+ type_name="Instantaneous",
54
+ ):
55
+ """
56
+ Initialize vector calibrator
57
+
58
+ Args:
59
+ base_dir: Base directory containing data
60
+ camera_num: Camera number (1-based)
61
+ model_index: Index of calibration model to use (0-based)
62
+ dt: Time step between frames in seconds
63
+ dot_spacing_mm: Physical spacing of calibration dots in mm
64
+ vector_pattern: Pattern for vector files (e.g. "B%05d.mat", "%05d.mat")
65
+ type_name: Type name for uncalibrated data directory (e.g. "Instantaneous", "piv")
66
+ """
67
+ self.base_dir = Path(base_dir)
68
+ self.camera_num = camera_num
69
+ self.model_index = model_index
70
+ self.dt = dt
71
+ self.dot_spacing_mm = dot_spacing_mm
72
+ self.vector_pattern = vector_pattern
73
+ self.type_name = type_name
74
+
75
+ # Load calibration model
76
+ self.calibration_model = self._load_calibration_model()
77
+ self.homography = self.calibration_model["homography"]
78
+ self.camera_matrix = self.calibration_model["camera_matrix"]
79
+ self.dist_coeffs = self.calibration_model["dist_coeffs"]
80
+ self.dot_spacing_mm = dot_spacing_mm
81
+
82
+ logger.info(f"Initialized calibrator for Camera {camera_num}")
83
+ logger.info(f"Using calibration model index {model_index}")
84
+ logger.info(f"Time step: {dt} seconds")
85
+ logger.info(f"Dot spacing: {dot_spacing_mm} mm")
86
+ logger.info(f"Vector pattern: {vector_pattern}")
87
+ logger.info(f"Type name: {type_name}")
88
+
89
+ def _load_calibration_model(self):
90
+ """Load the specified calibration model"""
91
+ calib_paths = get_data_paths(
92
+ self.base_dir,
93
+ num_images=1, # Not used for calibration paths
94
+ cam=self.camera_num,
95
+ type_name="", # Not used for calibration paths
96
+ calibration=True,
97
+ )
98
+
99
+ calib_dir = calib_paths["calib_dir"]
100
+ # Try common model filenames produced by planar calibrator
101
+ # Always look for the model at "model/camera_model.mat"
102
+ model_path = calib_dir / "model" / "camera_model.mat"
103
+ if not model_path.exists():
104
+ raise FileNotFoundError(f"Calibration model not found: {model_path}")
105
+
106
+ logger.info(f"Loading calibration model: {model_path}")
107
+ model_data = loadmat(str(model_path), squeeze_me=True, struct_as_record=False)
108
+
109
+ required_fields = ["homography", "camera_matrix", "dist_coeffs"]
110
+ missing_fields = [field for field in required_fields if field not in model_data]
111
+ if missing_fields:
112
+ raise ValueError(
113
+ f"Missing required fields in calibration model: {missing_fields}"
114
+ )
115
+
116
+ # Ensure homography is 3x3
117
+ homography = np.array(model_data["homography"])
118
+ if homography.shape != (3, 3):
119
+ raise ValueError(f"Homography must be 3x3, got shape {homography.shape}")
120
+
121
+ # Ensure dist_coeffs is 1D array
122
+ dist_coeffs = np.array(model_data["dist_coeffs"]).flatten()
123
+
124
+ model_data["homography"] = homography.astype(np.float32)
125
+ model_data["dist_coeffs"] = dist_coeffs.astype(np.float32)
126
+
127
+ # Use dt from model if available, otherwise use instance dt
128
+ if "dt" in model_data:
129
+ logger.info(f"Using dt from calibration model: {model_data['dt']} seconds")
130
+ self.dt = float(model_data["dt"])
131
+ else:
132
+ logger.info(
133
+ f"No dt in calibration model, using provided dt: {self.dt} seconds"
134
+ )
135
+
136
+ return model_data
137
+
138
+ def calibrate_coordinates(self, coords_x, coords_y):
139
+ """
140
+ Convert pixel coordinates to physical coordinates in mm using homography and distortion correction.
141
+
142
+ Args:
143
+ coords_x, coords_y: Coordinate arrays in pixels
144
+
145
+ Returns:
146
+ (x_mm, y_mm): Coordinate arrays in mm
147
+ """
148
+ # Stack coordinates for transformation
149
+ pts = np.stack([coords_x.flatten(), coords_y.flatten()], axis=-1).astype(
150
+ np.float32
151
+ )
152
+
153
+ # Ensure we have valid points
154
+ if pts.size == 0:
155
+ return coords_x, coords_y
156
+
157
+ # Undistort points if distortion coefficients are present
158
+ if np.any(self.dist_coeffs):
159
+ # Reshape for cv2.undistortPoints: (N, 1, 2)
160
+ pts_reshaped = pts.reshape(-1, 1, 2)
161
+ try:
162
+ pts_ud = cv2.undistortPoints(
163
+ pts_reshaped,
164
+ self.camera_matrix,
165
+ self.dist_coeffs,
166
+ P=self.camera_matrix,
167
+ )
168
+ pts_ud = pts_ud.reshape(-1, 2)
169
+ except cv2.error as e:
170
+ logger.warning(f"Undistortion failed, using original points: {e}")
171
+ pts_ud = pts
172
+ else:
173
+ pts_ud = pts
174
+
175
+ # Apply homography using OpenCV for numerical stability
176
+ pts_ud = pts_ud.reshape(1, -1, 2) # shape (1, N, 2) for perspectiveTransform
177
+ pts_mapped = cv2.perspectiveTransform(pts_ud, self.homography)[0]
178
+
179
+ # Reshape back to original shape
180
+ x_mm = pts_mapped[:, 0].reshape(coords_x.shape)
181
+ y_mm = pts_mapped[:, 1].reshape(coords_y.shape)
182
+
183
+ logger.info(
184
+ "Converted coordinates from pixels to mm (undistorted + homography)"
185
+ )
186
+
187
+ return x_mm, y_mm
188
+
189
+ def calibrate_vectors(self, ux_px, uy_px, coords_x_px, coords_y_px):
190
+ """
191
+ Convert pixel-based velocity vectors to m/s using camera matrix (pinhole model) and distortion correction.
192
+
193
+ Args:
194
+ ux_px, uy_px: Velocity components in pixels/frame
195
+ coords_x_px, coords_y_px: Grid coordinates in pixels
196
+
197
+ Returns:
198
+ (ux_ms, uy_ms): Velocity components in m/s
199
+ """
200
+ # Stack coordinates
201
+ coords_px = np.stack([coords_x_px, coords_y_px], axis=-1).astype(np.float32)
202
+ shape = coords_px.shape[:-1]
203
+ coords_flat = coords_px.reshape(-1, 2)
204
+
205
+ # Check for valid data
206
+ if coords_flat.size == 0 or ux_px.size == 0 or uy_px.size == 0:
207
+ logger.warning("Empty coordinate or vector data, returning zeros")
208
+ return np.zeros_like(ux_px), np.zeros_like(uy_px)
209
+
210
+ # Ensure arrays have compatible shapes
211
+ if ux_px.shape != uy_px.shape or ux_px.shape != coords_x_px.shape:
212
+ logger.error(
213
+ f"Shape mismatch: ux_px={ux_px.shape}, uy_px={uy_px.shape}, coords_x_px={coords_x_px.shape}"
214
+ )
215
+ return np.zeros_like(ux_px), np.zeros_like(uy_px)
216
+
217
+ # Undistort grid points
218
+ if np.any(self.dist_coeffs):
219
+ # Reshape for cv2.undistortPoints: (N, 1, 2)
220
+ coords_reshaped = coords_flat.reshape(-1, 1, 2)
221
+ try:
222
+ coords_ud = cv2.undistortPoints(
223
+ coords_reshaped,
224
+ self.camera_matrix,
225
+ self.dist_coeffs,
226
+ P=self.camera_matrix,
227
+ )
228
+ coords_ud = coords_ud.reshape(-1, 2)
229
+ except cv2.error as e:
230
+ logger.warning(f"Grid undistortion failed, using original points: {e}")
231
+ coords_ud = coords_flat
232
+ else:
233
+ coords_ud = coords_flat
234
+
235
+ # Apply homography
236
+ coords_ud = coords_ud.reshape(1, -1, 2)
237
+ coords_mm = cv2.perspectiveTransform(coords_ud, self.homography)[0]
238
+
239
+ # Displaced points
240
+ disp_px = coords_flat + np.stack([ux_px.flatten(), uy_px.flatten()], axis=-1)
241
+
242
+ if np.any(self.dist_coeffs):
243
+ # Reshape for cv2.undistortPoints: (N, 1, 2)
244
+ disp_reshaped = disp_px.reshape(-1, 1, 2)
245
+ try:
246
+ disp_ud = cv2.undistortPoints(
247
+ disp_reshaped,
248
+ self.camera_matrix,
249
+ self.dist_coeffs,
250
+ P=self.camera_matrix,
251
+ )
252
+ disp_ud = disp_ud.reshape(-1, 2)
253
+ except cv2.error as e:
254
+ logger.warning(
255
+ f"Displacement undistortion failed, using original points: {e}"
256
+ )
257
+ disp_ud = disp_px
258
+ else:
259
+ disp_ud = disp_px
260
+
261
+ disp_ud = disp_ud.reshape(1, -1, 2)
262
+ disp_mm = cv2.perspectiveTransform(disp_ud, self.homography)[0]
263
+
264
+ # Metric displacement
265
+ delta_mm = disp_mm - coords_mm
266
+
267
+ # Convert to m/s
268
+ ux_ms = (delta_mm[:, 0] / 1000.0) / self.dt # mm to m, frame to s
269
+ uy_ms = (delta_mm[:, 1] / 1000.0) / self.dt
270
+
271
+ ux_ms = ux_ms.reshape(shape)
272
+ uy_ms = uy_ms.reshape(shape)
273
+
274
+ logger.info(
275
+ "Converted vectors from pixels/frame to m/s using undistortion + homography"
276
+ )
277
+
278
+ return ux_ms, uy_ms
279
+
280
+ def process_run(self, image_count, progress_cb=None):
281
+ """
282
+ Process and calibrate vectors for all available runs
283
+
284
+ Args:
285
+ image_count: Number of images in the run
286
+ progress_cb: Optional callback for progress updates
287
+ """
288
+ logger.info(f"Processing run with {image_count} images")
289
+
290
+ # Get data paths for uncalibrated data - use configured type
291
+ paths = get_data_paths(
292
+ self.base_dir,
293
+ num_images=image_count,
294
+ cam=self.camera_num,
295
+ type_name=self.type_name,
296
+ use_uncalibrated=True,
297
+ )
298
+
299
+ uncalib_data_dir = paths["data_dir"]
300
+ logger.info(f"Uncalibrated data directory: {uncalib_data_dir}")
301
+
302
+ # Get output paths for calibrated data
303
+ calib_paths = get_data_paths(
304
+ self.base_dir,
305
+ num_images=image_count,
306
+ cam=self.camera_num,
307
+ type_name=self.type_name,
308
+ )
309
+
310
+ calib_data_dir = calib_paths["data_dir"]
311
+ calib_data_dir.mkdir(parents=True, exist_ok=True)
312
+ logger.info(f"Calibrated data directory: {calib_data_dir}")
313
+
314
+ if not uncalib_data_dir.exists():
315
+ raise FileNotFoundError(
316
+ f"Uncalibrated data directory not found: {uncalib_data_dir}"
317
+ )
318
+
319
+ # Load coordinates for all runs
320
+ logger.info("Loading coordinates...")
321
+ x_coords_list, y_coords_list = load_coords_from_directory(
322
+ uncalib_data_dir, runs=None # Load all available runs
323
+ )
324
+
325
+ if not x_coords_list:
326
+ logger.error("No coordinate data found!")
327
+ raise ValueError("No coordinate data found")
328
+
329
+ logger.info(f"Loaded coordinates for {len(x_coords_list)} runs")
330
+
331
+ # Find runs with valid data
332
+ valid_runs = []
333
+ for i, (x_coords, y_coords) in enumerate(zip(x_coords_list, y_coords_list)):
334
+ run_num = i + 1
335
+ # Ensure None is replaced with empty arrays
336
+ if x_coords is None:
337
+ x_coords = np.array([])
338
+ if y_coords is None:
339
+ y_coords = np.array([])
340
+ valid_coords = np.sum(~np.isnan(x_coords)) + np.sum(~np.isnan(y_coords))
341
+ logger.info(f"Run {run_num}: {valid_coords} valid coordinates")
342
+ if valid_coords > 0:
343
+ valid_runs.append((i, run_num, valid_coords))
344
+
345
+ if not valid_runs:
346
+ raise ValueError("No runs with valid coordinate data found")
347
+
348
+ logger.info(
349
+ f"Found {len(valid_runs)} runs with valid data: {[r[1] for r in valid_runs]}"
350
+ )
351
+
352
+ # Create coordinate structure
353
+ max_run = max([r[1] for r in valid_runs])
354
+ coord_dtype = np.dtype([("x", "O"), ("y", "O")])
355
+ coordinates = np.empty(max_run, dtype=coord_dtype)
356
+
357
+ # Process each valid run
358
+ for run_idx, run_num, valid_coord_count in valid_runs:
359
+ logger.info(
360
+ f"Processing run {run_num} with {valid_coord_count} valid coordinates"
361
+ )
362
+ x_coords_px = x_coords_list[run_idx]
363
+ y_coords_px = y_coords_list[run_idx]
364
+
365
+ # Calibrate coordinates to mm
366
+ x_coords_mm, y_coords_mm = self.calibrate_coordinates(
367
+ x_coords_px, y_coords_px
368
+ )
369
+ coordinates[run_num - 1] = (x_coords_mm, y_coords_mm)
370
+
371
+ # Fill all runs: valid runs get data, others get empty arrays
372
+ valid_run_indices = set(r[1] for r in valid_runs)
373
+ for run_num in range(1, max_run + 1):
374
+ if run_num in valid_run_indices:
375
+ # Already set above for valid runs
376
+ continue
377
+ coordinates[run_num - 1] = (np.array([]), np.array([]))
378
+
379
+ coords_output = {"coordinates": coordinates}
380
+
381
+ # Save calibrated coordinates
382
+ calibrated_dir = (
383
+ self.base_dir
384
+ / "calibrated_piv"
385
+ / str(image_count)
386
+ / f"Cam{self.camera_num}"
387
+ / self.type_name
388
+ )
389
+ calibrated_dir.mkdir(parents=True, exist_ok=True)
390
+ coords_path = calibrated_dir / "coordinates.mat"
391
+ savemat(str(coords_path), coords_output)
392
+ logger.info(f"Saved calibrated coordinates: {coords_path}")
393
+
394
+ # Process vector files using the first valid run's coordinates
395
+ if valid_runs:
396
+ first_run_idx = valid_runs[0][0]
397
+ x_coords_for_vectors = x_coords_list[first_run_idx]
398
+ y_coords_for_vectors = y_coords_list[first_run_idx]
399
+
400
+ self._process_vector_files(
401
+ uncalib_data_dir,
402
+ calib_data_dir,
403
+ image_count,
404
+ x_coords_for_vectors,
405
+ y_coords_for_vectors,
406
+ max_run,
407
+ valid_runs,
408
+ progress_cb,
409
+ )
410
+ else:
411
+ logger.error("No valid runs found for vector processing")
412
+
413
+ def _process_vector_files(
414
+ self,
415
+ uncalib_dir,
416
+ calib_dir,
417
+ num_images,
418
+ coords_x_px,
419
+ coords_y_px,
420
+ max_run,
421
+ valid_runs,
422
+ progress_cb,
423
+ ):
424
+ """Process all vector files in the directory"""
425
+ logger.info("Processing vector files...")
426
+
427
+ # Use configured vector pattern
428
+ vector_pattern = self.vector_pattern
429
+
430
+ processed_vectors = []
431
+
432
+ for i in range(1, num_images + 1):
433
+ vector_file = uncalib_dir / (vector_pattern % i)
434
+
435
+ if not vector_file.exists():
436
+ if i <= 5: # Only log first few missing files
437
+ logger.warning(f"Vector file not found: {vector_file}")
438
+ continue
439
+
440
+ try:
441
+ # Load uncalibrated vectors
442
+ vector_data = read_mat_contents(str(vector_file)) # Shape varies
443
+
444
+ # Handle different vector data formats
445
+ if vector_data.ndim == 4 and vector_data.shape[0] == 1:
446
+ # Single run format: (1, 3, H, W)
447
+ ux_px = vector_data[0, 0, :, :]
448
+ uy_px = vector_data[0, 1, :, :]
449
+ b_mask = vector_data[0, 2, :, :]
450
+ elif vector_data.ndim == 3 and vector_data.shape[0] == 3:
451
+ # Single run format: (3, H, W)
452
+ ux_px = vector_data[0, :, :]
453
+ uy_px = vector_data[1, :, :]
454
+ b_mask = vector_data[2, :, :]
455
+ else:
456
+ logger.warning(
457
+ f"Unexpected vector data shape in {vector_file.name}: {vector_data.shape}"
458
+ )
459
+ continue
460
+
461
+ # Calibrate vectors
462
+ ux_ms, uy_ms = self.calibrate_vectors(
463
+ ux_px, uy_px, coords_x_px, coords_y_px
464
+ )
465
+
466
+ # Create piv_result structure array with proper MATLAB struct format
467
+ piv_dtype = np.dtype([("ux", "O"), ("uy", "O"), ("b_mask", "O")])
468
+ piv_result = np.empty(max_run, dtype=piv_dtype)
469
+
470
+ for run_num in range(1, max_run + 1):
471
+ run_idx = run_num - 1 # Convert to 0-based
472
+
473
+ # Check if this run has valid data
474
+ run_has_data = any(r[1] == run_num for r in valid_runs)
475
+
476
+ if run_has_data:
477
+ # This creates piv_result(run_idx).ux, piv_result(run_idx).uy, piv_result(run_idx).b_mask
478
+ piv_result[run_idx] = (ux_ms, uy_ms, b_mask)
479
+ else:
480
+ # Empty struct for runs not being processed
481
+ piv_result[run_idx] = (np.array([]), np.array([]), np.array([]))
482
+
483
+ # Save calibrated piv_result into calibrated_piv output tree
484
+ output_file = calib_dir / (self.vector_pattern % i)
485
+ savemat(str(output_file), {"piv_result": piv_result})
486
+
487
+ processed_vectors.append(
488
+ {"ux_ms": ux_ms, "uy_ms": uy_ms, "b_mask": b_mask, "frame": i}
489
+ )
490
+
491
+ # Progress callback
492
+ if progress_cb:
493
+ progress = (i / num_images) * 100
494
+ progress_cb(
495
+ {
496
+ "processed_frames": i,
497
+ "total_frames": num_images,
498
+ "progress": progress,
499
+ "successful_frames": len(processed_vectors),
500
+ }
501
+ )
502
+
503
+ except Exception as e:
504
+ logger.error(f"Failed to process {vector_file.name}: {str(e)}")
505
+ continue
506
+
507
+ logger.info(
508
+ f"Successfully processed {len(processed_vectors)} vector files into {calib_dir}"
509
+ )
510
+
511
+
512
+ def main():
513
+ logger.info("Starting vector calibration with configuration:")
514
+ logger.info(f"Base directory: {BASE_DIR}")
515
+ logger.info(f"Run number: {image_count}")
516
+ logger.info(f"Time step: {DT_SECONDS} seconds")
517
+ logger.info(f"Camera: {CAMERA_NUM}")
518
+ logger.info(f"Model index: {MODEL_INDEX}")
519
+ logger.info(f"Dot spacing: {DOT_SPACING_MM} mm")
520
+ logger.info(f"Vector pattern: {VECTOR_PATTERN}")
521
+ logger.info(f"Type name: {TYPE_NAME}")
522
+
523
+ try:
524
+ calibrator = VectorCalibrator(
525
+ base_dir=BASE_DIR,
526
+ camera_num=CAMERA_NUM,
527
+ model_index=MODEL_INDEX,
528
+ dt=DT_SECONDS,
529
+ dot_spacing_mm=DOT_SPACING_MM,
530
+ vector_pattern=VECTOR_PATTERN,
531
+ type_name=TYPE_NAME,
532
+ )
533
+
534
+ calibrator.process_run(image_count)
535
+
536
+ logger.info("Vector calibration completed successfully")
537
+
538
+ except Exception as e:
539
+ logger.error(f"Calibration failed: {str(e)}")
540
+ sys.exit(1)
541
+
542
+
543
+ if __name__ == "__main__":
544
+ main()