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,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()
|