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,190 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from loguru import logger
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from . import register_reader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_lavision_im7(
|
|
10
|
+
file_path: str, camera_no: int = 1, frames: int = 2
|
|
11
|
+
) -> np.ndarray:
|
|
12
|
+
"""Read LaVision .im7 files.
|
|
13
|
+
|
|
14
|
+
LaVision .im7 files store all cameras in a single file per time instance.
|
|
15
|
+
Each file contains frame pairs (A and B) for all cameras.
|
|
16
|
+
|
|
17
|
+
Structure: For N cameras, the file contains 2*N frames in sequence:
|
|
18
|
+
- Frames 0,1: Camera 1, frames A and B
|
|
19
|
+
- Frames 2,3: Camera 2, frames A and B
|
|
20
|
+
- etc.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_path: Path to the .im7 file
|
|
24
|
+
camera_no: Camera number (1-based indexing)
|
|
25
|
+
frames: Number of frames to read (typically 2 for PIV)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
np.ndarray: Array of shape (frames, H, W) containing the image data
|
|
29
|
+
"""
|
|
30
|
+
import sys
|
|
31
|
+
if sys.platform == "darwin":
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"lvpyio is not shipped or supported on macOS (darwin). Please use a supported platform for LaVision .im7 reading."
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
import lvpyio as lv
|
|
37
|
+
except ImportError:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"LaVision library not available. Please install."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not os.path.exists(file_path):
|
|
43
|
+
raise FileNotFoundError(f"Image file not found: {file_path}")
|
|
44
|
+
|
|
45
|
+
# Read the buffer as a generator
|
|
46
|
+
buffer = lv.read_buffer(file_path)
|
|
47
|
+
|
|
48
|
+
# Calculate which frames we need for this camera
|
|
49
|
+
start_frame = (camera_no - 1) * 2
|
|
50
|
+
end_frame = start_frame + frames
|
|
51
|
+
|
|
52
|
+
# Iterate through the generator, only processing the frames we need
|
|
53
|
+
data = None
|
|
54
|
+
for idx, img in enumerate(buffer):
|
|
55
|
+
if idx < start_frame:
|
|
56
|
+
# Skip frames before our camera
|
|
57
|
+
continue
|
|
58
|
+
elif idx < end_frame:
|
|
59
|
+
# This is one of our frames
|
|
60
|
+
if data is None:
|
|
61
|
+
# Initialize array on first needed frame
|
|
62
|
+
height, width = img.components["PIXEL"].planes[0].shape
|
|
63
|
+
data = np.zeros((frames, height, width), dtype=np.float64)
|
|
64
|
+
|
|
65
|
+
i_scale = img.scales.i.slope
|
|
66
|
+
i_offset = img.scales.i.offset
|
|
67
|
+
u_arr = img.components["PIXEL"].planes[0] * i_scale + i_offset
|
|
68
|
+
data[idx - start_frame, :, :] = u_arr
|
|
69
|
+
else:
|
|
70
|
+
# We've got all our frames, stop iterating
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if data is None:
|
|
74
|
+
raise ValueError(f"Camera {camera_no} not found in file {file_path}")
|
|
75
|
+
|
|
76
|
+
return data.astype(np.float32)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_lavision_pair(file_path: str, camera_no: int = 1) -> np.ndarray:
|
|
80
|
+
"""Read LaVision .im7 file and return as frame pair.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
file_path: Path to the .im7 file (contains all cameras for one time instance)
|
|
84
|
+
camera_no: Camera number (1-based) to extract from the file
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
np.ndarray: Array of shape (2, H, W) containing frame A and B for the specified camera
|
|
88
|
+
"""
|
|
89
|
+
return read_lavision_im7(file_path, camera_no, frames=2)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def read_lavision_ims(file_path: str, camera_no: Optional[int] = None, im_no: Optional[int] = None) -> np.ndarray:
|
|
93
|
+
"""Read LaVision images from a .set file.
|
|
94
|
+
|
|
95
|
+
LaVision .set files contain all cameras and frames in a single container.
|
|
96
|
+
This function reads the set file and extracts the appropriate frames for the
|
|
97
|
+
specified camera and image number.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
file_path: Path to the .set file
|
|
101
|
+
camera_no: Camera number (1-based). If None, extracted from file_path (legacy)
|
|
102
|
+
im_no: Image number (1-based). If None, extracted from file_path (legacy)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
np.ndarray: Array of shape (2, H, W) containing frame A and B
|
|
106
|
+
"""
|
|
107
|
+
import sys
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
if sys.platform == "darwin":
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"LaVision libraries are not supported on macOS (darwin). Please use a supported platform."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
import lvpyio as lv
|
|
117
|
+
except ImportError:
|
|
118
|
+
raise ImportError(
|
|
119
|
+
"LaVision library not available. Please install lvpyio."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
path = Path(file_path)
|
|
123
|
+
|
|
124
|
+
# For .set files, camera_no and im_no must be provided
|
|
125
|
+
if path.suffix.lower() == '.set' or (camera_no is not None and im_no is not None):
|
|
126
|
+
# Modern format: file_path is the .set file
|
|
127
|
+
set_file_path = file_path
|
|
128
|
+
if camera_no is None or im_no is None:
|
|
129
|
+
raise ValueError("camera_no and im_no must be provided for .set files")
|
|
130
|
+
else:
|
|
131
|
+
# Legacy path parsing for backward compatibility
|
|
132
|
+
# Extract camera number from path (e.g., "Cam1" -> 1)
|
|
133
|
+
if camera_no is None:
|
|
134
|
+
camera_match = None
|
|
135
|
+
for part in path.parts:
|
|
136
|
+
if part.startswith("Cam") and part[3:].isdigit():
|
|
137
|
+
camera_match = int(part[3:])
|
|
138
|
+
break
|
|
139
|
+
if camera_match is None:
|
|
140
|
+
raise ValueError(f"Could not extract camera number from path: {file_path}")
|
|
141
|
+
camera_no = camera_match
|
|
142
|
+
|
|
143
|
+
# Extract image number from filename
|
|
144
|
+
if im_no is None:
|
|
145
|
+
stem = path.stem
|
|
146
|
+
if stem.isdigit():
|
|
147
|
+
im_no = int(stem)
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f"Could not extract image number from filename: {path.name}")
|
|
150
|
+
|
|
151
|
+
# Source directory is typically the parent of the CamX directory
|
|
152
|
+
source_dir = path.parent.parent
|
|
153
|
+
set_file_path = str(source_dir)
|
|
154
|
+
|
|
155
|
+
if not Path(set_file_path).exists():
|
|
156
|
+
raise FileNotFoundError(f"Set file path not found: {set_file_path}")
|
|
157
|
+
|
|
158
|
+
# Read the set file
|
|
159
|
+
try:
|
|
160
|
+
set_file = lv.read_set(set_file_path)
|
|
161
|
+
im = set_file[im_no - 1] # 0-based indexing in Python
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise RuntimeError(f"Failed to read set file from {set_file_path}: {e}")
|
|
164
|
+
|
|
165
|
+
# Extract frames for this camera
|
|
166
|
+
data = np.zeros((2, *im.frames[0].components["PIXEL"].planes[0].shape), dtype=np.float64)
|
|
167
|
+
|
|
168
|
+
for j in range(2):
|
|
169
|
+
# Frame indexing: 2*cameraNo-(2-j)
|
|
170
|
+
frame_idx = 2 * camera_no - (2 - j)
|
|
171
|
+
frame = im.frames[frame_idx]
|
|
172
|
+
|
|
173
|
+
# Apply scaling
|
|
174
|
+
i_scale = frame.scales.i.slope
|
|
175
|
+
i_offset = frame.scales.i.offset
|
|
176
|
+
u_arr = frame.components["PIXEL"].planes[0] * i_scale + i_offset
|
|
177
|
+
|
|
178
|
+
data[j, :, :] = u_arr
|
|
179
|
+
|
|
180
|
+
set_file.close()
|
|
181
|
+
return data.astype(np.float32)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def read_lavision_ims_pair(file_path: str, **kwargs) -> np.ndarray:
|
|
185
|
+
"""Read LaVision .set file and return as frame pair."""
|
|
186
|
+
return read_lavision_ims(file_path, **kwargs)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
register_reader([".im7"], read_lavision_pair)
|
|
190
|
+
register_reader([".set"], read_lavision_ims_pair)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Dict, List
|
|
3
|
+
|
|
4
|
+
# Registry for image readers
|
|
5
|
+
_reader_registry: Dict[str, Callable] = {}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_reader(extensions: List[str], reader_func: Callable):
|
|
9
|
+
"""Register an image reader for specific file extensions."""
|
|
10
|
+
for ext in extensions:
|
|
11
|
+
_reader_registry[ext.lower()] = reader_func
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_reader(file_path: str) -> Callable:
|
|
15
|
+
"""Get appropriate reader for a file based on its extension."""
|
|
16
|
+
ext = Path(file_path).suffix.lower()
|
|
17
|
+
if ext not in _reader_registry:
|
|
18
|
+
raise ValueError(f"No reader registered for file type: {ext}")
|
|
19
|
+
return _reader_registry[ext]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_supported_formats() -> List[str]:
|
|
23
|
+
"""List all supported file formats."""
|
|
24
|
+
return list(_reader_registry.keys())
|
pivtools_core/paths.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_data_paths(
|
|
5
|
+
base_dir,
|
|
6
|
+
num_images,
|
|
7
|
+
cam,
|
|
8
|
+
type_name,
|
|
9
|
+
endpoint="",
|
|
10
|
+
use_merged=False,
|
|
11
|
+
use_uncalibrated=False,
|
|
12
|
+
calibration=False, # New argument
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Construct directories for data, statistics, and videos.
|
|
16
|
+
endpoint: optional subfolder ('' ignored).
|
|
17
|
+
use_uncalibrated: if True, return paths for uncalibrated data
|
|
18
|
+
calibration: if True, return calibration directory
|
|
19
|
+
"""
|
|
20
|
+
base_dir = Path(base_dir)
|
|
21
|
+
cam = f"Cam{cam}"
|
|
22
|
+
# Calibration data
|
|
23
|
+
if calibration:
|
|
24
|
+
calib_dir = base_dir / "calibration" / cam
|
|
25
|
+
if endpoint:
|
|
26
|
+
calib_dir = calib_dir / endpoint
|
|
27
|
+
return dict(calib_dir=calib_dir)
|
|
28
|
+
# Uncalibrated data
|
|
29
|
+
if use_uncalibrated:
|
|
30
|
+
data_dir = base_dir / "uncalibrated_piv" / str(num_images) / cam / type_name
|
|
31
|
+
stats_dir = (
|
|
32
|
+
base_dir / "statistics" / "uncalibrated" / str(num_images) / cam / type_name
|
|
33
|
+
)
|
|
34
|
+
video_dir = base_dir / "videos" / "uncalibrated" / str(num_images) / cam
|
|
35
|
+
# Merged data
|
|
36
|
+
elif use_merged:
|
|
37
|
+
data_dir = base_dir / "merged" / str(num_images) / cam / type_name
|
|
38
|
+
stats_dir = base_dir / "statistics" / "merged" / cam / type_name
|
|
39
|
+
video_dir = base_dir / "videos" / "merged" / cam / type_name
|
|
40
|
+
# Regular calibrated data
|
|
41
|
+
else:
|
|
42
|
+
data_dir = base_dir / "calibrated_piv" / str(num_images) / cam / type_name
|
|
43
|
+
stats_dir = base_dir / "statistics" / str(num_images) / cam / type_name
|
|
44
|
+
video_dir = base_dir / "videos" / str(num_images) / cam
|
|
45
|
+
if endpoint:
|
|
46
|
+
data_dir = data_dir / endpoint
|
|
47
|
+
stats_dir = stats_dir / endpoint
|
|
48
|
+
video_dir = video_dir / endpoint
|
|
49
|
+
return dict(data_dir=data_dir, stats_dir=stats_dir, video_dir=video_dir)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
import dask
|
|
6
|
+
import dask.array as da
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.io
|
|
9
|
+
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_mat_contents(
|
|
14
|
+
file_path: str, run_index: Optional[int] = None, return_all_runs: bool = False
|
|
15
|
+
) -> np.ndarray:
|
|
16
|
+
"""
|
|
17
|
+
Reads piv_result from a .mat file.
|
|
18
|
+
If multiple runs are present, selects the specified run_index (0-based).
|
|
19
|
+
If run_index is None, selects the first run with valid (non-empty) data.
|
|
20
|
+
If return_all_runs is True, returns all runs in shape (R, 3, H, W).
|
|
21
|
+
Otherwise returns shape (1, 3, H, W) for the selected run.
|
|
22
|
+
"""
|
|
23
|
+
mat = scipy.io.loadmat(file_path, struct_as_record=False, squeeze_me=True)
|
|
24
|
+
piv_result = mat["piv_result"]
|
|
25
|
+
|
|
26
|
+
# Multiple runs case: numpy array of structs
|
|
27
|
+
if isinstance(piv_result, np.ndarray) and piv_result.dtype == object:
|
|
28
|
+
total_runs = piv_result.size
|
|
29
|
+
|
|
30
|
+
if return_all_runs:
|
|
31
|
+
# Return all runs
|
|
32
|
+
all_runs = []
|
|
33
|
+
for idx in range(total_runs):
|
|
34
|
+
pr = piv_result[idx]
|
|
35
|
+
ux = np.asarray(pr.ux)
|
|
36
|
+
uy = np.asarray(pr.uy)
|
|
37
|
+
b_mask = (
|
|
38
|
+
np.asarray(pr.b_mask).astype(ux.dtype, copy=False)
|
|
39
|
+
if ux.size > 0
|
|
40
|
+
else np.array([])
|
|
41
|
+
)
|
|
42
|
+
if ux.size > 0 and uy.size > 0:
|
|
43
|
+
stacked = np.stack([ux, uy, b_mask], axis=0) # (3, H, W)
|
|
44
|
+
else:
|
|
45
|
+
# Empty run - create placeholder with consistent shape if possible
|
|
46
|
+
stacked = np.array([[], [], []]) # Will be reshaped later
|
|
47
|
+
all_runs.append(stacked)
|
|
48
|
+
return np.array(all_runs) # (R, 3, H, W)
|
|
49
|
+
|
|
50
|
+
# Single run selection (existing logic)
|
|
51
|
+
if run_index is None:
|
|
52
|
+
# Find first valid run (non-empty ux, uy)
|
|
53
|
+
for idx in range(total_runs):
|
|
54
|
+
pr = piv_result[idx]
|
|
55
|
+
ux = np.asarray(pr.ux)
|
|
56
|
+
uy = np.asarray(pr.uy)
|
|
57
|
+
if ux.size > 0 and uy.size > 0:
|
|
58
|
+
run_index = idx
|
|
59
|
+
break
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"No valid runs found in {file_path}")
|
|
62
|
+
if run_index < 0 or run_index >= total_runs:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Invalid run_index {run_index} for {file_path} (total_runs={total_runs})"
|
|
65
|
+
)
|
|
66
|
+
pr = piv_result[run_index]
|
|
67
|
+
ux = np.asarray(pr.ux)
|
|
68
|
+
uy = np.asarray(pr.uy)
|
|
69
|
+
b_mask = np.asarray(pr.b_mask).astype(ux.dtype, copy=False)
|
|
70
|
+
stacked = np.stack([ux, uy, b_mask], axis=0)[None, ...] # (1, 3, H, W)
|
|
71
|
+
return stacked
|
|
72
|
+
|
|
73
|
+
# Single run struct
|
|
74
|
+
if run_index is not None and run_index != 0:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Invalid run_index {run_index} for single-run file {file_path}"
|
|
77
|
+
)
|
|
78
|
+
pr = piv_result
|
|
79
|
+
ux = np.asarray(pr.ux)
|
|
80
|
+
uy = np.asarray(pr.uy)
|
|
81
|
+
b_mask = np.asarray(pr.b_mask).astype(ux.dtype, copy=False)
|
|
82
|
+
|
|
83
|
+
if return_all_runs:
|
|
84
|
+
stacked = np.stack([ux, uy, b_mask], axis=0)[None, ...] # (1, 3, H, W)
|
|
85
|
+
return stacked
|
|
86
|
+
else:
|
|
87
|
+
stacked = np.stack([ux, uy, b_mask], axis=0)[None, ...] # (1, 3, H, W)
|
|
88
|
+
return stacked
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_vectors_from_directory(
|
|
92
|
+
data_dir: Path, config: Config, runs: Optional[Sequence[int]] = None
|
|
93
|
+
) -> da.Array:
|
|
94
|
+
"""
|
|
95
|
+
Load .mat vector files for requested runs.
|
|
96
|
+
- runs: list of 1-based run numbers to include; if None or empty, include all runs in the files.
|
|
97
|
+
Returns Dask array with shape (N_existing, R, 3, H, W).
|
|
98
|
+
"""
|
|
99
|
+
data_dir = Path(data_dir)
|
|
100
|
+
fmt = config.vector_format # e.g. "B%05d.mat"
|
|
101
|
+
expected_paths = [data_dir / (fmt % i) for i in range(1, config.num_images + 1)]
|
|
102
|
+
existing_paths = [p for p in expected_paths if p.exists()]
|
|
103
|
+
|
|
104
|
+
missing_count = len(expected_paths) - len(existing_paths)
|
|
105
|
+
if missing_count == len(expected_paths):
|
|
106
|
+
raise FileNotFoundError(
|
|
107
|
+
f"No vector files found using pattern {fmt} in {data_dir}"
|
|
108
|
+
)
|
|
109
|
+
if missing_count:
|
|
110
|
+
warnings.warn(
|
|
111
|
+
f"{missing_count} vector files missing in {data_dir} (loaded {len(existing_paths)})"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Convert runs (1-based) to zero-based indices for reading
|
|
115
|
+
zero_based_runs: Optional[Sequence[int]] = None
|
|
116
|
+
if runs:
|
|
117
|
+
zero_based_runs = [r - 1 for r in runs]
|
|
118
|
+
|
|
119
|
+
# Detect shape/dtype from first readable file
|
|
120
|
+
first_arr = None
|
|
121
|
+
for p in existing_paths:
|
|
122
|
+
try:
|
|
123
|
+
first_arr = read_mat_contents(
|
|
124
|
+
str(p), run_index=zero_based_runs[0] if zero_based_runs else None
|
|
125
|
+
)
|
|
126
|
+
# Debugging: print shape, dtype, and file info
|
|
127
|
+
if first_arr.ndim != 4:
|
|
128
|
+
warnings.warn(
|
|
129
|
+
f"[DEBUG] Unexpected array ndim={first_arr.ndim} in {p.name}"
|
|
130
|
+
)
|
|
131
|
+
break
|
|
132
|
+
except Exception as e:
|
|
133
|
+
warnings.warn(f"Failed to read {p.name} during probing: {e}")
|
|
134
|
+
raise
|
|
135
|
+
if first_arr is None:
|
|
136
|
+
raise FileNotFoundError(f"Could not read any valid vector files in {data_dir}")
|
|
137
|
+
|
|
138
|
+
shape, dtype = first_arr.shape, first_arr.dtype # (R, 3, H, W), dtype
|
|
139
|
+
|
|
140
|
+
delayed_items = [
|
|
141
|
+
dask.delayed(read_mat_contents)(
|
|
142
|
+
str(p), run_index=zero_based_runs[0] if zero_based_runs else None
|
|
143
|
+
)
|
|
144
|
+
for p in existing_paths
|
|
145
|
+
]
|
|
146
|
+
arrays = [da.from_delayed(di, shape=shape, dtype=dtype) for di in delayed_items]
|
|
147
|
+
stacked = da.stack(arrays, axis=0) # (N, R, 3, H, W)
|
|
148
|
+
return stacked.rechunk({0: config.piv_chunk_size})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_coords_from_directory(
|
|
152
|
+
data_dir: Path, runs: Optional[Sequence[int]] = None
|
|
153
|
+
) -> Tuple[Sequence[np.ndarray], Sequence[np.ndarray]]:
|
|
154
|
+
"""
|
|
155
|
+
Locate and read the coordinates.mat file in data_dir and return (x_list, y_list).
|
|
156
|
+
- runs: list of 1-based run numbers to include; if None or empty, include all runs present in the coords file.
|
|
157
|
+
- Returns:
|
|
158
|
+
x_list: list of x arrays in the same order as 'runs' (or all runs if None)
|
|
159
|
+
y_list: list of y arrays in the same order as 'runs' (or all runs if None)
|
|
160
|
+
"""
|
|
161
|
+
data_dir = Path(data_dir)
|
|
162
|
+
coords_path = data_dir / "coordinates.mat"
|
|
163
|
+
if not coords_path.exists():
|
|
164
|
+
raise FileNotFoundError(f"No coordinates.mat file found in {data_dir}")
|
|
165
|
+
|
|
166
|
+
mat = scipy.io.loadmat(coords_path, struct_as_record=False, squeeze_me=True)
|
|
167
|
+
if "coordinates" not in mat:
|
|
168
|
+
raise KeyError(f"'coordinates' variable not found in {coords_path.name}")
|
|
169
|
+
coords = mat["coordinates"]
|
|
170
|
+
|
|
171
|
+
def _xy_from_struct(obj):
|
|
172
|
+
return np.asarray(obj.x), np.asarray(obj.y)
|
|
173
|
+
|
|
174
|
+
x_list, y_list = [], []
|
|
175
|
+
|
|
176
|
+
if isinstance(coords, np.ndarray) and coords.dtype == object:
|
|
177
|
+
if runs:
|
|
178
|
+
zero_based = [r - 1 for r in runs if 1 <= r <= coords.size]
|
|
179
|
+
if len(zero_based) != len(runs):
|
|
180
|
+
missing = sorted(set(runs) - set([z + 1 for z in zero_based]))
|
|
181
|
+
warnings.warn(
|
|
182
|
+
f"Skipping out-of-range run indices {missing} for coordinates"
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
zero_based = list(range(coords.size))
|
|
186
|
+
|
|
187
|
+
for idx in zero_based:
|
|
188
|
+
x, y = _xy_from_struct(coords[idx])
|
|
189
|
+
x_list.append(x)
|
|
190
|
+
y_list.append(y)
|
|
191
|
+
else:
|
|
192
|
+
if runs and 1 not in runs:
|
|
193
|
+
warnings.warn(
|
|
194
|
+
"Requested runs do not include run 1 present in coordinates; returning empty coords"
|
|
195
|
+
)
|
|
196
|
+
return [], []
|
|
197
|
+
x, y = _xy_from_struct(coords)
|
|
198
|
+
x_list.append(x)
|
|
199
|
+
y_list.append(y)
|
|
200
|
+
|
|
201
|
+
return x_list, y_list
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def save_mask_to_mat(file_path: str, mask: np.ndarray, polygons):
|
|
205
|
+
"""
|
|
206
|
+
Save the given mask array to a .mat file.
|
|
207
|
+
"""
|
|
208
|
+
scipy.io.savemat(file_path, {"mask": mask, "polygons": polygons}, do_compression=True)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def read_mask_from_mat(file_path: str):
|
|
212
|
+
"""
|
|
213
|
+
Read the mask and polygons from a .mat file.
|
|
214
|
+
Returns:
|
|
215
|
+
mask: np.ndarray
|
|
216
|
+
polygons: list of dicts with fields 'index', 'name', 'points'
|
|
217
|
+
"""
|
|
218
|
+
# Load without squeeze_me to avoid 0-d array issues with single-element cells
|
|
219
|
+
# Use struct_as_record=True (default) so structs become record arrays with dict-like access
|
|
220
|
+
mat = scipy.io.loadmat(file_path, squeeze_me=False, struct_as_record=True)
|
|
221
|
+
mask = mat.get("mask", None)
|
|
222
|
+
polygons_raw = mat.get("polygons", None)
|
|
223
|
+
if mask is None or polygons_raw is None:
|
|
224
|
+
raise ValueError(f"Missing 'mask' or 'polygons' in {file_path}")
|
|
225
|
+
|
|
226
|
+
# Squeeze the mask manually if needed
|
|
227
|
+
mask = np.squeeze(mask)
|
|
228
|
+
|
|
229
|
+
# polygons_raw is a numpy object array (MATLAB cell array)
|
|
230
|
+
# Flatten it to iterate (it might be [[obj1], [obj2]] or [[obj]])
|
|
231
|
+
polygons_flat = polygons_raw.flatten()
|
|
232
|
+
|
|
233
|
+
polygons = []
|
|
234
|
+
for poly in polygons_flat:
|
|
235
|
+
# poly is a structured array (record) with named fields accessible via indexing
|
|
236
|
+
# Extract scalar values from 0-d arrays
|
|
237
|
+
idx_raw = poly['index'] if isinstance(poly, np.void) else poly['index'][0, 0]
|
|
238
|
+
name_raw = poly['name'] if isinstance(poly, np.void) else poly['name'][0, 0]
|
|
239
|
+
pts_raw = poly['points'] if isinstance(poly, np.void) else poly['points'][0, 0]
|
|
240
|
+
|
|
241
|
+
idx = int(idx_raw.item() if hasattr(idx_raw, 'item') else idx_raw)
|
|
242
|
+
name = str(name_raw.item() if hasattr(name_raw, 'item') else name_raw)
|
|
243
|
+
|
|
244
|
+
# pts might be a 2D array, convert to list of lists
|
|
245
|
+
points = pts_raw.tolist() if isinstance(pts_raw, np.ndarray) else list(pts_raw)
|
|
246
|
+
polygons.append({"index": idx, "name": name, "points": points})
|
|
247
|
+
|
|
248
|
+
return mask, polygons
|
pivtools_gui/__init__.py
ADDED