Multi-folder experiments¶
For an experiment that's really several runs sharing one calibration --
multiple workpieces, repeated trials, whatever your acquisition splits into
separate frame sequences. See docs/cloud-batch.md first for how a single
parameters_*.yaml runs; this doc is about organizing and running several of
them together, and the calibration/YAML-prep steps specific to that.
This grew out of a real multi-workpiece dataset and the mistakes made setting it up -- the checklists below exist because each one bit us once.
Layout¶
<experiment_root>/
<shared_calibration>/ one calibration, reused by every run below
cam_1.tif ... cam_N.tif calibration images (what .ori/.addpar were fit to)
cam_1.tif.ori ... .addpar exterior + interior/distortion params, per camera
<calblock>.txt 3D calibration-body point coordinates (id x y z)
calib_matches/cam{1..N}_matches.txt detected-vs-reprojected residuals (optional, enables RMS checks)
run1/ run2/ .../ one per independent run
img/ raw acquisition frames
parameters_<name>_sample.yaml small frame range, for a quick check
parameters_<name>_batch.yaml full frame range
res/ sequence/tracking output (created on first run)
Exactly one YAML pair per run. If a folder has more than
*_sample.yaml + *_batch.yaml, one is stale. Cross-check against that
folder's parameters/*.par (if present -- the GUI's own last-saved state,
independent of any YAML) to see which one the GUI actually agrees with, and
remove the other. An ambiguous duplicate left lying around means the next
person -- or the next agent -- has no way to know which one is live.
Path resolution: relative to the YAML, not to cwd¶
Both the GUI and openptv2-batch os.chdir() into the YAML file's own
directory before doing anything else (ParameterManager.from_yaml itself
does zero path resolution -- it just parses the file; every relative path in
it is resolved by whatever the process's cwd happens to be when it's used).
That has two consequences:
- A YAML living in
run1/referencing../<shared_calibration>/cam_1.tif.oriresolves correctly, one level up fromrun1/. - Output is also cwd-relative and NOT configurable.
res/rt_is.*,ptv_is.*,added.*are hardcoded relative paths (no YAML field overrides them). If two YAMLs ever share the same parent directory, they write to the sameres/and silently clobber each other's results. This is why each run needs its own folder -- don't try to save "tidiness" by hoisting multiple runs' YAMLs into one shared directory; that trades a real correctness risk for a cosmetic win. (Parallel execution across runs is still fine -- separate folders means separate cwd means separateres/, safe to run concurrently.)
Preparing one shared calibration for all runs¶
- Confirm it's actually the same physical rig setup. Compare exterior
camera positions (
cam_N.tif.ori, first line: X Y Z in mm) against any calibration already in the target experiment, if one exists. Tens of mm apart in any camera means the rig moved (or it's a different rig/session entirely) -- do not share, calibrate that experiment separately. This is the single most consequential mistake to avoid: a calibration that loads without error and even produces plausible-looking correspondence counts can still be geometrically wrong for the wrong rig -- nothing in the pipeline itself will tell you, you have to check (see below). - Match the acquisition modality, not just the rig position. A
calibration shot as 4 separate per-camera images (
ptv.splitter: false) is not automatically compatible with a run that multiplexes 4 views onto one sensor (ptv.splitter: true) -- the "camera 1/2/3/4" identity in the splitter case comes fromsplitter_orderdemuxing a combined frame, and that quadrant-to-camera mapping has to agree with however the calibration images were assigned their camera numbers. If unsure, find evidence of the actual quadrant layout (a splitting script, a preview image, anything that shows which physical quadrant is "camera 1") rather than assuming the existingsplitter_ordervalue is correct. - Same calibration body, confirmed by diffing the calblock
.txtfiles byte-for-byte, not just by name. - Once confirmed, point every run's YAML at the shared directory with a relative path (see the layout above) -- don't duplicate the calibration files into every run folder.
Preparing the YAML files¶
Per run, starting from a copy of a known-good YAML in this dataset (or
test_data/test_cavity/parameters_Run1.yaml as a generic template):
cal_ori(fixp_name,img_cal_name,img_ori) andptv.img_cal-- point at the shared calibration directory, relative to this YAML's own location (see path resolution above).man_ori.nr+man_ori_coordinates-- 4 calibration-body point IDs visible in every camera (check each camera'scalib_matches/cam{N}_matches.txt-- an ID only appears there if it was detected+matched in that camera; don't assume the same 4 IDs are visible everywhere just because they're a round set like 1/16/32/46), plus their actual per-camera pixel positions -- not copy-pasted from one camera to another. This is the easiest calibration mistake to make silently: the GUI's manual-orientation click doesn't validate that the point you clicked is even the one you meant.- **
sequence.base_name+first/last`` -- check this before anything else if the sequence step fails.** These must match your *actual raw filenames*, not a template default. In splitter mode the loader doesPath(base_name % frame), sobase_nameneeds a real%06d-style placeholder andfirst/lastmust be the literal numbers embedded in your filenames. A copy-pasted template value (e.g. a100001-based convention left over from a different naming scheme) fails withFileNotFoundErroron the very first frame -- and only when you actually run the sequence step; the calibration/detection GUI panels won't catch it, since they only touch the calibration images. Verify with: and matchsequence.first/lastto the numbers actually in those filenames.shaking.shaking_first_frame/shaking_last_frameuse the same numbering -- keep them inside[sequence.first, sequence.last]`.
Sample vs. batch¶
Two YAMLs per run, identical except the frame range:
parameters_<name>_sample.yaml-- ~10 frames, for a fast GUI or CLI check before committing to a full run.parameters_<name>_batch.yaml-- the full range.
Best-strategy order: fix the calibration and frame-naming issues above
first (they're silent and easy to miss), then run the sample through the
GUI and eyeball a tracked-particle overlay, then run the sample headlessly
with openptv2-batch to confirm the CLI path agrees with what the GUI
showed you, and only then run the full batch. A batch run just repeats
whatever the sample did, at scale -- if the sample is wrong, the batch is
the same wrong thing with more frames.
Checking and visualizing the calibration¶
Don't rely on reprojection RMS alone -- a bundle adjustment can converge to
a self-consistent but physically wrong pose (the classic cause: a
manual-orientation seed click landed on the wrong calibration-body point,
or a splitter quadrant/camera-identity mismatch per above). Two tools, same
underlying checks (openptv2.calibration_diagnostics):
Headless / CI / scripted -- scripts/calibration_diagnostics.py:
calib_matches/ is present. Exits non-zero if anything is flagged, so it
can gate a script or CI step. Compare two models (e.g. before/after
recalibration, or two candidate calibrations) with a second label=path in
--models.
Interactive -- src/openptv2/gui/visualize_calibration_nb.py (marimo
notebook; needs the viz extra -- uv run --extra viz installs it on
demand, no permanent dependency change needed for a one-off look):
uv run --extra viz marimo -y run src/openptv2/gui/visualize_calibration_nb.py -- \
--models "current=<shared_calibration>"
.claude/skills/visualize-calibration/) -- ask to "visualize the
calibration" / "compare these two calibration models" / "visualize before
and after full calibration" and it resolves the natural-language request
into the right --models/--calblock args.
Running¶
scripts/run_pipeline_multi.sh <experiment_root> --sample # quick check, all runs
scripts/run_pipeline_multi.sh <experiment_root> --sample --folder run1 # one run
scripts/run_pipeline_multi.sh <experiment_root> # full batch, sequential
scripts/run_pipeline_multi.sh <experiment_root> --parallel # full batch, concurrent
Auto-discovers run folders by looking for parameters_Run1_<variant>.yaml
one level under <experiment_root> (override the prefix with --pattern if
your YAMLs aren't named parameters_Run1_*). Each run gets its own
<run>/pipeline_<variant>.log -- check it ends with Batch processing
completed successfully and that per-frame correspondence counts look
reasonable (hundreds, not near-zero -- near-zero across every frame usually
means the calibration or detection thresholds are off, not just noise).