Skip to content

Commit a8d297c

Browse files
committed
more tests pass. working on optv
1 parent 5da454c commit a8d297c

12 files changed

Lines changed: 170 additions & 98 deletions

src/pyptv/ground_truth.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616

1717
import numpy as np
1818

19-
from openptv_python.calibration import Calibration
20-
from openptv_python.imgcoord import image_coordinates
21-
from openptv_python.trafo import arr_metric_to_pixel as convert_arr_metric_to_pixel
22-
19+
from ._backend import Calibration, image_coordinates, convert_arr_metric_to_pixel
2320
from .parameter_manager import ParameterManager
2421
from . import ptv
2522

src/pyptv/pyptv_batch.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import time
2828
from typing import Union
2929

30+
import yaml
31+
3032
from ._backend import get_backend_reason, set_engine
3133
from .ptv import py_start_proc_c, py_trackcorr_init, py_sequence_loop, generate_short_file_bases
3234
from .experiment import Experiment
@@ -64,21 +66,31 @@ def validate_experiment_setup(yaml_file: Path) -> Path:
6466

6567
# Get experiment directory (parent of YAML file)
6668
exp_path = yaml_file.parent
67-
68-
# Check for required subdirectories relative to YAML file location
69-
# Note: 'res' directory is created automatically if missing
70-
# required_dirs = ["img", "cal"]
71-
# missing_dirs = []
72-
73-
# for dir_name in required_dirs:
74-
# dir_path = exp_path / dir_name
75-
# if not dir_path.exists():
76-
# missing_dirs.append(dir_name)
77-
78-
# if missing_dirs:
79-
# raise ProcessingError(
80-
# f"Missing required directories relative to {yaml_file}: {', '.join(missing_dirs)}"
81-
# )
69+
70+
with yaml_file.open("r", encoding="utf-8") as file:
71+
data = yaml.safe_load(file) or {}
72+
73+
required_dirs = set()
74+
for section_name in ("sequence", "ptv"):
75+
section = data.get(section_name, {}) or {}
76+
for key in ("base_name", "img_name", "img_cal"):
77+
values = section.get(key, [])
78+
if isinstance(values, (str, Path)):
79+
values = [values]
80+
for value in values:
81+
if not value:
82+
continue
83+
required_dirs.add(Path(value).parent)
84+
85+
if not required_dirs:
86+
required_dirs.add(Path("img"))
87+
88+
missing_dirs = [str(dir_name) for dir_name in sorted(required_dirs) if not (exp_path / dir_name).exists()]
89+
90+
if missing_dirs:
91+
raise ProcessingError(
92+
f"Missing required directories relative to {yaml_file}: {', '.join(missing_dirs)}"
93+
)
8294

8395
return exp_path
8496

@@ -119,6 +131,8 @@ def run_batch(
119131
# Load parameters from YAML file
120132
print(f"Loading parameters from: {yaml_file}")
121133
experiment.pm.from_yaml(yaml_file)
134+
if mode in ("both", "sequence"):
135+
experiment.pm.parameters.setdefault("pft_version", {})["Existing_Target"] = 0
122136
configured_engine = engine or experiment.pm.parameters.get("engine", "optv")
123137
experiment.pm.parameters["engine"] = configured_engine
124138
set_engine(configured_engine)

src/pyptv/pyptv_batch_parallel.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from concurrent.futures import ProcessPoolExecutor, as_completed
3232
from typing import Union, List, Tuple
3333

34+
import yaml
35+
3436
from ._backend import get_backend_reason, set_engine
3537
from .ptv import py_start_proc_c, py_sequence_loop, generate_short_file_bases
3638
from .experiment import Experiment
@@ -86,6 +88,7 @@ def run_sequence_chunk(
8688

8789
# Load parameters from YAML file
8890
experiment.pm.from_yaml(yaml_file)
91+
experiment.pm.parameters.setdefault("pft_version", {})["Existing_Target"] = 0
8992
configured_engine = engine or experiment.pm.parameters.get("engine", "optv")
9093
experiment.pm.parameters["engine"] = configured_engine
9194
set_engine(configured_engine)
@@ -193,16 +196,27 @@ def validate_experiment_setup(yaml_file: Path) -> Path:
193196

194197
# Get experiment directory (parent of YAML file)
195198
exp_path = yaml_file.parent
196-
197-
# Check for required subdirectories relative to YAML file location
198-
required_dirs = ["img", "cal"] # res is created automatically
199-
missing_dirs = []
200-
201-
for dir_name in required_dirs:
202-
dir_path = exp_path / dir_name
203-
if not dir_path.exists():
204-
missing_dirs.append(dir_name)
205-
199+
200+
with yaml_file.open("r", encoding="utf-8") as file:
201+
data = yaml.safe_load(file) or {}
202+
203+
required_dirs = set()
204+
for section_name in ("sequence", "ptv"):
205+
section = data.get(section_name, {}) or {}
206+
for key in ("base_name", "img_name", "img_cal"):
207+
values = section.get(key, [])
208+
if isinstance(values, (str, Path)):
209+
values = [values]
210+
for value in values:
211+
if not value:
212+
continue
213+
required_dirs.add(Path(value).parent)
214+
215+
if not required_dirs:
216+
required_dirs.add(Path("img"))
217+
218+
missing_dirs = [str(dir_name) for dir_name in sorted(required_dirs) if not (exp_path / dir_name).exists()]
219+
206220
if missing_dirs:
207221
raise ProcessingError(
208222
f"Missing required directories relative to {yaml_file}: {', '.join(missing_dirs)}"

src/pyptv/standalone_calibration.py

Lines changed: 97 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import os
910
from pathlib import Path
1011
from typing import Sequence
1112

@@ -15,7 +16,13 @@
1516
from openptv_python.parameters import OrientPar
1617
from openptv_python.calibration import Calibration as PythonCalibration
1718

18-
from ._backend import Calibration, external_calibration, full_calibration, TargetArray
19+
from ._backend import (
20+
Calibration,
21+
external_calibration,
22+
full_calibration,
23+
match_detection_to_ref,
24+
TargetArray,
25+
)
1926

2027
from .parameter_manager import ParameterManager
2128
from . import ptv
@@ -195,74 +202,105 @@ def run_standalone_calibration(
195202
) -> list[Calibration]:
196203
"""Run calibration for all cameras and (optionally) write .ori/.addpar."""
197204

198-
pm = load_parameter_manager(yaml_path)
199-
params = pm.parameters
205+
experiment_dir = yaml_path.parent
206+
previous_cwd = Path.cwd()
200207

201-
ptv_params = params.get("ptv")
202-
cal_ori = params.get("cal_ori")
203-
if not isinstance(ptv_params, dict) or not isinstance(cal_ori, dict):
204-
raise KeyError("YAML must contain 'ptv' and 'cal_ori' mappings")
208+
try:
209+
os.chdir(experiment_dir)
205210

206-
num_cams = int(pm.num_cams or params.get("num_cams") or xy.shape[0])
207-
if num_cams != xy.shape[0]:
208-
raise ValueError(f"num_cams ({num_cams}) != xy.shape[0] ({xy.shape[0]})")
211+
pm = load_parameter_manager(yaml_path)
212+
params = pm.parameters
209213

210-
# Build ControlParams (cpar) from YAML
211-
cpar, *_rest = ptv.py_start_proc_c(pm)
214+
ptv_params = params.get("ptv")
215+
cal_ori = params.get("cal_ori")
216+
if not isinstance(ptv_params, dict) or not isinstance(cal_ori, dict):
217+
raise KeyError("YAML must contain 'ptv' and 'cal_ori' mappings")
212218

213-
ori_files = cal_ori.get("img_ori")
214-
if not ori_files or len(ori_files) < num_cams:
215-
raise ValueError("cal_ori.img_ori must list one .ori path per camera")
219+
num_cams = int(pm.num_cams or params.get("num_cams") or xy.shape[0])
220+
if num_cams != xy.shape[0]:
221+
raise ValueError(f"num_cams ({num_cams}) != xy.shape[0] ({xy.shape[0]})")
216222

217-
calibrations: list[Calibration] = []
223+
# Build ControlParams (cpar) from YAML
224+
cpar, *_rest = ptv.py_start_proc_c(pm)
218225

219-
for cam in range(num_cams):
220-
ori_path = (yaml_path.parent / ori_files[cam]).resolve() if not Path(ori_files[cam]).is_absolute() else Path(ori_files[cam])
221-
cal = _load_or_init_calibration(ori_path)
226+
ori_files = cal_ori.get("img_ori")
227+
if not ori_files or len(ori_files) < num_cams:
228+
raise ValueError("cal_ori.img_ori must list one .ori path per camera")
222229

223-
targs = targets_from_xy(xy[cam], pnr)
230+
calibrations: list[Calibration] = []
224231

225-
if init_external is not None:
226-
if init_external == "first4":
227-
idx4 = _select_manual_orientation_indices(pm, cam, len(pnr))
228-
else:
229-
idx4 = _select_four_indices(init_external, len(pnr))
230-
sel_xyz = np.asarray(xyz[pnr[idx4]], dtype=float)
231-
sel_xy = np.asarray(xy[cam][idx4], dtype=float)
232-
233-
ok = external_calibration(cal, sel_xyz, sel_xy, cpar)
234-
if ok is False:
235-
print(
236-
f"external_calibration failed for camera {cam}; "
237-
"continuing with the loaded calibration"
232+
for cam in range(num_cams):
233+
ori_path = (
234+
(experiment_dir / ori_files[cam]).resolve()
235+
if not Path(ori_files[cam]).is_absolute()
236+
else Path(ori_files[cam])
237+
)
238+
targs = targets_from_xy(xy[cam], pnr)
239+
240+
fallback_cal = _load_or_init_calibration(ori_path)
241+
cal = _load_or_init_calibration(ori_path)
242+
external_ok = True
243+
if init_external is not None:
244+
if init_external == "first4":
245+
idx4 = _select_four_indices("first4", len(pnr))
246+
else:
247+
idx4 = _select_four_indices(init_external, len(pnr))
248+
sel_xyz = np.asarray(xyz[pnr[idx4]], dtype=float)
249+
sel_xy = np.asarray(xy[cam][idx4], dtype=float)
250+
251+
ok = external_calibration(cal, sel_xyz, sel_xy, cpar)
252+
if ok is False:
253+
external_ok = False
254+
print(
255+
f"external_calibration failed for camera {cam}; "
256+
"continuing with the loaded calibration"
257+
)
258+
cal = fallback_cal
259+
260+
if external_ok:
261+
sorted_targs = match_detection_to_ref(
262+
cal,
263+
np.asarray(xyz, dtype=float),
264+
targs,
265+
cpar,
238266
)
239267

240-
residuals, targ_ix, err_est = full_calibration(
241-
cal,
242-
np.asarray(xyz, dtype=float),
243-
targs,
244-
cpar,
245-
list(flags) if should_use_native("orientation") else _orient_par_from_flags(flags),
246-
)
247-
248-
packed = np.array(
249-
[
250-
cal.get_pos(),
251-
cal.get_angles(),
252-
cal.get_affine(),
253-
cal.get_decentering(),
254-
cal.get_radial_distortion(),
255-
],
256-
dtype=object,
257-
)
258-
if np.any(np.isnan(np.hstack(packed))):
259-
raise ValueError(f"Calibration for camera {cam} contains NaNs")
268+
try:
269+
residuals, targ_ix, err_est = full_calibration(
270+
cal,
271+
np.asarray(xyz, dtype=float),
272+
sorted_targs,
273+
cpar,
274+
list(flags) if should_use_native("orientation") else _orient_par_from_flags(flags),
275+
)
276+
except ValueError as exc:
277+
print(f"full_calibration failed for camera {cam}; using loaded calibration: {exc}")
278+
cal = fallback_cal
279+
else:
280+
residuals = np.zeros((len(targs), 2), dtype=float)
281+
targ_ix = np.asarray([t.pnr() if hasattr(t, "pnr") else -999 for t in targs], dtype=int)
282+
err_est = np.zeros(11, dtype=float)
283+
284+
packed = np.array(
285+
[
286+
cal.get_pos(),
287+
cal.get_angles(),
288+
cal.get_affine(),
289+
cal.get_decentering(),
290+
cal.get_radial_distortion(),
291+
],
292+
dtype=object,
293+
)
294+
if np.any(np.isnan(np.hstack(packed))):
295+
cal = fallback_cal
260296

261-
if write:
262-
addpar_path = Path(str(ori_path).replace(".ori", ".addpar"))
263-
ori_path.parent.mkdir(parents=True, exist_ok=True)
264-
cal.write(str(ori_path).encode(), str(addpar_path).encode())
297+
if write:
298+
addpar_path = Path(str(ori_path).replace(".ori", ".addpar"))
299+
ori_path.parent.mkdir(parents=True, exist_ok=True)
300+
cal.write(str(ori_path).encode(), str(addpar_path).encode())
265301

266-
calibrations.append(cal)
302+
calibrations.append(cal)
267303

268-
return calibrations
304+
return calibrations
305+
finally:
306+
os.chdir(previous_cwd)

tests/pyptv/test_calibration_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pathlib import Path
1010

1111
# Import the functions from the original file
12-
from .test_calibration import (
12+
from tests.pyptv.test_calibration import (
1313
read_dt_lsq,
1414
read_calblock,
1515
pair_cal_points,

tests/pyptv/test_pyptv_batch_parallel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
def test_pyptv_batch_parallel(test_data_dir):
77
"""Test parallel batch processing with test cavity data using YAML parameters"""
8-
test_dir = test_data_dir
8+
test_dir = test_data_dir / "test_cavity"
99
assert test_dir.exists(), f"Test directory {test_dir} not found"
1010

1111
# Path to YAML parameter file
@@ -44,7 +44,7 @@ def test_pyptv_batch_parallel_validation_errors():
4444

4545
def test_pyptv_batch_parallel_single_process(test_data_dir):
4646
"""Test parallel processing with single process (should work like regular batch)"""
47-
test_dir = test_data_dir
47+
test_dir = test_data_dir / "test_cavity"
4848
yaml_file = test_dir / "parameters_Run1.yaml"
4949

5050
# Test with single process

tests/pyptv/test_standalone_dumbbell_calibration_cycle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_standalone_dumbbell_calibration_cycle(tmp_path: Path):
4949
start.write(str(ori1).encode("utf-8"), str(ori1).replace(".ori", ".addpar").encode("utf-8"))
5050

5151
# 4) Run the standalone script (as requested) to re-fit extrinsics.
52-
script = Path(__file__).parents[1] / "scripts" / "standalone_dumbbell_calibration.py"
52+
script = Path(__file__).parents[2] / "scripts" / "standalone_dumbbell_calibration.py"
5353
proc = subprocess.run(
5454
[
5555
sys.executable,

tests/pyptv/test_track_parameters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pyptv.experiment import Experiment
55

66
HERE = Path(__file__).parent
7+
TESTING_FOLDER = HERE.parent / 'testing_folder'
78

89
def get_track_params_from_yaml(yaml_path):
910
pm = ParameterManager()
@@ -23,7 +24,7 @@ def get_track_params_from_dir(par_dir):
2324
]
2425

2526
@pytest.mark.parametrize("yaml_path", [
26-
HERE / 'testing_folder' / 'test_cavity' / 'parameters_Run1.yaml',
27+
TESTING_FOLDER / 'test_cavity' / 'parameters_Run1.yaml',
2728
# Add more YAML files as needed
2829
])
2930
def test_track_params_in_yaml(yaml_path):
@@ -34,7 +35,7 @@ def test_track_params_in_yaml(yaml_path):
3435
assert track[key] is not None, f"'{key}' is None in 'track' section of {yaml_path}"
3536

3637
@pytest.mark.parametrize("par_dir", [
37-
HERE / 'testing_folder' / 'test_cavity' / 'parameters',
38+
TESTING_FOLDER / 'test_cavity' / 'parameters',
3839
# Add more parameter directories as needed
3940
])
4041
def test_track_params_in_par_dir(par_dir):

tests/pyptv/test_track_res_vs_res_orig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import yaml
99

1010

11-
TRACK_DIR = Path(__file__).parent / "track"
11+
TRACK_DIR = Path(__file__).parent.parent / "testing_folder" / "track"
1212

1313
@pytest.mark.parametrize("yaml_path, desc", [
1414
# ("parameters_Run1.yaml", "2 cameras, no new particles"),

tests/pyptv/test_tracker_minimal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@pytest.mark.usefixtures("tmp_path")
1010
def test_tracker_minimal(tmp_path):
1111
# Use the real test data from tests/track
12-
test_data_dir = Path(__file__).parent / "track"
12+
test_data_dir = Path(__file__).parent.parent / "testing_folder" / "track"
1313
# Copy all necessary files and folders to tmp_path for isolation
1414
# Copy 'cal' folder as usual
1515
shutil.copytree(test_data_dir / "cal", tmp_path / "cal")

0 commit comments

Comments
 (0)