CalibrationSession

CalibrationSession<P: ProblemType> is the central state container for calibration workflows. It holds all data for a calibration run — configuration, input observations, intermediate state, final output, and an audit log — with full JSON serialization for checkpointing.

Structure

A session has two public fields and otherwise exposes its data through methods:

MemberAccessPurpose
configpub fieldAlgorithm parameters (iterations, fix masks, loss functions)
exportspub fieldTimestamped ExportRecords created by export()
inputinput(), require_input(), set_input(), clear_input()Observation data
outputoutput(), require_output()Final calibration result, set by the last step
metadata()methodSessionMetadata: problem type name, schema version, timestamps, description
log()methodAudit trail of operations, &[LogEntry]

Intermediate state is internal to the pipeline. Read intermediate results from the typed values returned by each step function (for example IntrinsicsInitResult), from log(), or from export().

The public methods of CalibrationSession<P>:

GroupMethods
Constructionnew(), with_description(..), with_input(..)
Inputset_input, input, input_mut, require_input, require_input_mut, has_input, clear_input
Configset_config, update_config
Outputoutput, output_mut, require_output, set_output, has_output, clear_output
Exportexport, export_with_notes, export_peek
Validationvalidate
Loglog, log_success, log_success_with_notes, log_failure
Metadatametadata
Resetreset_state, reset_output, reset
Serializationto_json, from_json

Lifecycle

1. Create

#![allow(unused)]
fn main() {
let mut session = CalibrationSession::<PlanarIntrinsicsProblem>::new();
// Or with a description:
let mut session = CalibrationSession::<PlanarIntrinsicsProblem>::with_description(
    "Lab camera calibration"
);
}

2. Set Input

#![allow(unused)]
fn main() {
session.set_input(dataset)?;
}

Input is validated via ProblemType::validate_input(). Setting input clears computed state (per the invalidation policy).

3. Configure

#![allow(unused)]
fn main() {
session.update_config(|c| {
    c.solver.max_iters = 50;
    c.solver.robust_loss = RobustLoss::Huber { scale: 2.0 };
})?;
}

Configuration is validated via ProblemType::validate_config().

4. Run Steps

#![allow(unused)]
fn main() {
step_init(&mut session, None)?;
step_optimize(&mut session, None)?;
}

Step functions are free functions operating on &mut CalibrationSession<P>. Each step reads input/state, performs computation, and updates state (or output).

5. Export

#![allow(unused)]
fn main() {
let export = session.export()?;
}

Creates an ExportRecord with the current timestamp and output. Multiple exports can be created (e.g., after re-optimization with different settings).

JSON Checkpointing

The entire session can be serialized and restored:

#![allow(unused)]
fn main() {
// Save
let json = session.to_json()?;
std::fs::write("calibration.json", &json)?;

// Restore
let json = std::fs::read_to_string("calibration.json")?;
let mut restored = CalibrationSession::<PlanarIntrinsicsProblem>::from_json(&json)?;

// Resume from where we left off
step_optimize(&mut restored, None)?;
}

This enables:

  • Interruption recovery: Save progress and resume later
  • Reproducibility: Share exact calibration state
  • Debugging: Inspect the session at any point

Invalidation Policies

When input or configuration changes, computed state may need to be cleared:

EventDefault policy
set_input()Clear state and output (CLEAR_COMPUTED)
update_config()Keep everything (KEEP_ALL)
clear_input()Same as set_input() (CLEAR_COMPUTED), then the input is removed

Each problem type may override these policies.

Audit Log

Every step function appends to the session log:

#![allow(unused)]
fn main() {
pub struct LogEntry {
    pub timestamp: u64,
    pub operation: String,
    pub success: bool,
    pub notes: Option<String>,
}
}

The log records what was done and when, useful for tracking calibration history.

Reading Results

#![allow(unused)]
fn main() {
// Input (required before steps)
let input = session.require_input()?;  // Returns error if no input

// Intermediate results: the typed value returned by each step
let init = step_init(&mut session, None)?;
println!("Init fx={:.1}", init.camera.intrinsics.fx);

// Output (available after optimize)
let output = session.require_output()?;

// Session bookkeeping
println!("{} ({} log entries)", session.metadata().problem_type, session.log().len());
}

Progress and Cancellation

A long run reports its progress, and stops on request, through a RunControl (in vision_calibration::control): an optional CancelToken and observers that receive each Progress report.

use vision_calibration::control::{CancelToken, Progress, RunControl};

let token = CancelToken::new();
let control = RunControl::new()
    .with_cancel(token.clone())
    .with_observer(|progress| match progress {
        Progress::Step { name, .. } => eprintln!("{name}"),
        Progress::Iteration { iter, cost, .. } => eprintln!("  {iter}: {cost:.3e}"),
        _ => {}
    });
session.set_control(control);
// From another thread (a Ctrl-C handler, a Cancel button): token.cancel();
match run_calibration(&mut session) {
    Err(vision_calibration::Error::Cancelled) => eprintln!("stopped"),
    other => other?,
}
ReportWhen
Detection { done, total }a target image was detected or read from the cache
LaserExtraction { done, total }a laser image was extracted
Step { name }run_calibration begins a step ("intrinsics_init_all", …)
Iteration { iter, max_iters, cost }a solver iteration finished; iter restarts at 1 for each solve

The token is checked before every solver iteration, between the steps of run_calibration and before every image, so a run stops within one iteration or one image of the request. A stopped call returns Error::Cancelled (CalibrateDatasetError::Cancelled from calibrate_dataset, RunError::Cancelled from a build_*_input converter), never a partial result: a stopped step stores nothing in the session. Progress is a side channel: a run with observers returns exactly what it returns without them, bit for bit, and nothing it reports enters a result.

The same control goes to dataset_runner::calibrate_dataset and the build_*_input converters as their last argument, and to optim::problem::solve as SolveOptions::control. Observers run on the calibrating thread at every report, so keep them cheap; one that must redraw a screen can drop reports that arrive too fast.

In Python

Each problem type has its session class, named after it: PlanarIntrinsicsSession, SingleCamHandeyeSession, RigExtrinsicsSession, RigHandeyeSession, LaserlineDeviceSession, RigLaserlineDeviceSession and RigHandeyeLaserlineSession. The step functions are its methods, with the Rust names; each takes the step's options and returns the step's result as a dataclass. Long steps release the GIL.

import vision_calibration as vc

session = vc.PlanarIntrinsicsSession("Lab camera calibration")
session.set_input(dataset)  # a PlanarIntrinsicsInput, or its payload dict
session.set_config(vc.PlanarIntrinsicsConfig(solver=vc.SolverConfig(max_iters=50)))

init = session.step_init(vc.IntrinsicsInitOptions(iterations=3))
print(f"Init fx={init.camera.intrinsics.fx:.1f}")
session.step_optimize()
result = session.export(notes="first pass")  # a PlanarIntrinsicsExport

for entry in session.log:  # the audit log, as LogEntry dataclasses
    print(entry.operation, entry.success, entry.notes)

# Checkpoint and resume
restored = vc.PlanarIntrinsicsSession.from_json(session.to_json())

input and output return the serde payloads (dicts), config the config dataclass, exports the ExportRecords and metadata the SessionMetadata. run_calibration() runs the problem's default steps. An input or config the problem rejects, a step out of order and a seed that does not fit raise ValueError; a numerical failure raises RuntimeError.

Ctrl-C stops a running step with KeyboardInterrupt. set_progress gives the session a callable that receives every report as a dict ({"kind": "iteration", "iter": 3, "max_iters": 50, "cost": 12.5}); raising from it stops the step, and the exception propagates. The run_* functions, calibrate_dataset and the build_*_input converters take the callable as their progress argument:

def show(report):
    if report["kind"] == "detection":
        print(f"detected {report['done']}/{report['total']}")

result = vc.calibrate_dataset("data/kuka_1/dataset.toml", progress=show)