Skip to content

Commit 512b76c

Browse files
committed
adjusting to optv
1 parent facaeda commit 512b76c

3 files changed

Lines changed: 76 additions & 11 deletions

File tree

docs/native_backend_unification.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ The important user-facing contract is simple:
1010
- the same Python calls should work with and without `optv` installed
1111
- native acceleration is an implementation detail, not a second API surface
1212

13+
## Implementation Checklist
14+
15+
Use this checklist when adding or changing backend routing:
16+
17+
- [ ] Import backend-facing symbols from `pyptv._backend`, not directly from `optv` or `openptv_python`, in pipeline code.
18+
- [ ] Ensure the active engine is selected through `_native_compat.set_engine()` and queried through `should_use_native(...)` or the backend helper functions.
19+
- [ ] When `optv` is installed and selected, use the native module objects and native parameter/calibration classes.
20+
- [ ] When `optv` is missing or disabled, keep the Python/Numba fallback path callable with the same function signatures.
21+
- [ ] Convert Python objects to native objects at backend boundaries only; do not leak backend-specific types into unrelated code paths.
22+
- [ ] Convert native objects back to Python objects only when a downstream caller still expects the Python implementation model.
23+
- [ ] Preserve input and output shapes between backends for detection, correspondences, calibration, reconstruction, and tracking.
24+
- [ ] Keep a fallback path for calibration routines that fail in the native backend, but make sure the fallback sees the same data model as the native path.
25+
- [ ] Add or update tests whenever a backend boundary changes so the native and Python paths stay behaviorally aligned.
26+
1327
The runtime flow is:
1428

1529
| Layer | Responsibility |
@@ -94,6 +108,15 @@ The important distinction is not whether a native implementation exists. It is
94108
whether the normal `openptv_python` runtime path already chooses that native
95109
implementation transparently for the user.
96110

111+
### Contract Rules
112+
113+
- `pyptv._backend` is the only layer that should decide which implementation is active.
114+
- Public helpers must keep one stable signature, regardless of backend.
115+
- Backend-specific object construction must stay inside conversion helpers or backend dispatch code.
116+
- If a native routine expects a native object type, the wrapper must build it before the call.
117+
- If a fallback routine expects Python dataclasses, the wrapper must convert back before the call.
118+
- A failed native path may fall back to Python, but only after restoring or rebuilding the input objects the fallback expects.
119+
97120
## Why `tests/test_native_stress_performance.py` matters
98121

99122
The best executable description of this backend strategy is

src/openptv_python/imgcoord.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010
from .trafo import flat_to_dist
1111

1212

13+
def _as_python_calibration(cal: Calibration) -> Calibration:
14+
if isinstance(cal, Calibration):
15+
return cal
16+
17+
from ._native_convert import from_native_calibration
18+
19+
converted = from_native_calibration(cal)
20+
if not isinstance(converted, Calibration):
21+
raise TypeError("Unsupported calibration object")
22+
23+
return converted
24+
25+
1326
def flat_image_coord(
1427
orig_pos: np.ndarray, cal: Calibration, mm: MultimediaPar
1528
) -> Tuple[float, float]:
@@ -28,6 +41,8 @@ def flat_image_coord(
2841
if orig_pos.shape != (3,):
2942
raise ValueError("orig_pos must be a 3D vector")
3043

44+
cal = _as_python_calibration(cal)
45+
3146
cal_t = Calibration(mmlut=cal.mmlut)
3247

3348
# This block calculate 3D position in an imaginary air-filled space,
@@ -76,6 +91,8 @@ def img_coord(
7691
pos: np.ndarray, cal: Calibration, mm: MultimediaPar
7792
) -> Tuple[float, float]:
7893
"""Estimate metric coordinates in image space (mm)."""
94+
cal = _as_python_calibration(cal)
95+
7996
# Estimate metric coordinates in image space using flat_image_coord()
8097
if pos.shape[0] != 3:
8198
raise ValueError("pos must be a 3D vector")
@@ -95,6 +112,8 @@ def image_coordinates(
95112
orig_pos: np.ndarray, cal: Calibration, mm: MultimediaPar
96113
) -> np.ndarray:
97114
"""Image coordinates in array mode."""
115+
cal = _as_python_calibration(cal)
116+
98117
npoints = orig_pos.shape[0]
99118
out = np.empty((npoints, 2), dtype=float)
100119

src/pyptv/standalone_calibration.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111

1212
import numpy as np
1313

14-
from openptv_python.calibration import Calibration
15-
from openptv_python.orientation import external_calibration, full_calibration
14+
from openptv_python._native_compat import should_use_native
1615
from openptv_python.parameters import OrientPar
17-
from openptv_python.tracking_frame_buf import TargetArray
16+
from openptv_python.calibration import Calibration as PythonCalibration
17+
18+
from ._backend import Calibration, external_calibration, full_calibration, TargetArray
1819

1920
from .parameter_manager import ParameterManager
2021
from . import ptv
@@ -156,14 +157,30 @@ def _select_four_indices(mode: str, n: int) -> np.ndarray:
156157
raise ValueError(f"Unknown init mode: {mode}")
157158

158159

160+
def _select_manual_orientation_indices(pm: ParameterManager, cam: int, n: int) -> np.ndarray:
161+
man_ori = pm.parameters.get("man_ori")
162+
if isinstance(man_ori, dict):
163+
nr = man_ori.get("nr")
164+
if isinstance(nr, list) and len(nr) >= (cam + 1) * 4:
165+
cam_nr = nr[cam * 4 : cam * 4 + 4]
166+
indices = np.asarray([int(idx) - 1 for idx in cam_nr], dtype=int)
167+
if np.all((0 <= indices) & (indices < n)):
168+
return indices
169+
170+
return _select_four_indices("first4", n)
171+
172+
159173
def _load_or_init_calibration(ori_path: Path) -> Calibration:
160-
cal = Calibration()
161174
addpar_path = Path(str(ori_path).replace(".ori", ".addpar"))
162175

163176
if ori_path.exists() and addpar_path.exists():
164-
cal.from_file(str(ori_path), str(addpar_path))
177+
if should_use_native("orientation"):
178+
cal = Calibration()
179+
cal.from_file(str(ori_path), str(addpar_path))
180+
return cal
181+
return PythonCalibration.from_file(str(ori_path), str(addpar_path))
165182

166-
return cal
183+
return Calibration() if should_use_native("orientation") else PythonCalibration()
167184

168185

169186
def run_standalone_calibration(
@@ -206,20 +223,26 @@ def run_standalone_calibration(
206223
targs = targets_from_xy(xy[cam], pnr)
207224

208225
if init_external is not None:
209-
idx4 = _select_four_indices(init_external, len(pnr))
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))
210230
sel_xyz = np.asarray(xyz[pnr[idx4]], dtype=float)
211231
sel_xy = np.asarray(xy[cam][idx4], dtype=float)
212232

213233
ok = external_calibration(cal, sel_xyz, sel_xy, cpar)
214234
if ok is False:
215-
raise RuntimeError(f"external_calibration failed for camera {cam}")
235+
print(
236+
f"external_calibration failed for camera {cam}; "
237+
"continuing with the loaded calibration"
238+
)
216239

217240
residuals, targ_ix, err_est = full_calibration(
218241
cal,
219242
np.asarray(xyz, dtype=float),
220243
targs,
221244
cpar,
222-
_orient_par_from_flags(flags),
245+
list(flags) if should_use_native("orientation") else _orient_par_from_flags(flags),
223246
)
224247

225248
packed = np.array(
@@ -235,11 +258,11 @@ def run_standalone_calibration(
235258
if np.any(np.isnan(np.hstack(packed))):
236259
raise ValueError(f"Calibration for camera {cam} contains NaNs")
237260

238-
calibrations.append(cal)
239-
240261
if write:
241262
addpar_path = Path(str(ori_path).replace(".ori", ".addpar"))
242263
ori_path.parent.mkdir(parents=True, exist_ok=True)
243264
cal.write(str(ori_path).encode(), str(addpar_path).encode())
244265

266+
calibrations.append(cal)
267+
245268
return calibrations

0 commit comments

Comments
 (0)