Adding a New Pipeline Problem Type

This chapter describes how to create a new session-based calibration workflow in vision-calibration-pipeline, using the laserline device module as a template.

Module Structure

Create a new folder under crates/vision-calibration-pipeline/src/:

my_problem/
├── mod.rs         # Module re-exports
├── problem.rs     # ProblemType implementation + Config
├── state.rs       # Intermediate state type
└── steps.rs       # Step functions + pipeline function

Step 1: Define the Problem Type (problem.rs)

#![allow(unused)]
fn main() {
use crate::session::{ProblemType, InvalidationPolicy};

pub struct MyProblem;

#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct MyConfig {
    pub max_iters: usize,
    pub fix_k3: bool,
    // ... other configuration
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MyInput {
    pub views: Vec<MyView>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MyOutput {
    pub calibrated_params: CameraParams,
    pub mean_reproj_error: f64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MyExport {
    pub params: CameraParams,
    pub mean_reproj_error: f64,
}

impl ProblemType for MyProblem {
    type Config = MyConfig;
    type Input = MyInput;
    type State = MyState;  // defined in state.rs
    type Output = MyOutput;
    type Export = MyExport;

    fn name() -> &'static str { "my_problem_v1" }

    fn validate_input(input: &MyInput) -> Result<()> {
        ensure!(input.views.len() >= 3, "Need at least 3 views");
        Ok(())
    }

    fn on_input_change() -> InvalidationPolicy { InvalidationPolicy::CLEAR_COMPUTED }
    fn on_config_change() -> InvalidationPolicy { InvalidationPolicy::KEEP_ALL }

    fn export(output: &MyOutput, _config: &MyConfig) -> Result<MyExport> {
        Ok(MyExport {
            params: output.calibrated_params.clone(),
            mean_reproj_error: output.mean_reproj_error,
        })
    }
}
}

Step 2: Define the State (state.rs)

#![allow(unused)]
fn main() {
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct MyState {
    // Initialization results
    pub initial_intrinsics: Option<FxFyCxCySkew<f64>>,
    pub initial_distortion: Option<BrownConrady5<f64>>,
    pub initial_poses: Option<Vec<Iso3>>,

    // Optimization results
    pub final_cost: Option<f64>,
    pub mean_reproj_error: Option<f64>,
}
}

The state must implement Default (empty state) and Clone + Serialize + Deserialize (for checkpointing).

Step 3: Implement Step Functions (steps.rs)

#![allow(unused)]
fn main() {
use crate::session::CalibrationSession;
use super::problem::MyProblem;

pub fn step_init(
    session: &mut CalibrationSession<MyProblem>,
    _opts: Option<&()>,
) -> Result<()> {
    let input = session.require_input()?;
    let config = &session.config;

    // Run linear initialization
    let intrinsics = /* ... */;
    let poses = /* ... */;

    // Update state
    session.state.initial_intrinsics = Some(intrinsics);
    session.state.initial_poses = Some(poses);

    // Log
    session.log_success("step_init", Some("Initialization complete"));
    Ok(())
}

pub fn step_optimize(
    session: &mut CalibrationSession<MyProblem>,
    _opts: Option<&()>,
) -> Result<()> {
    let input = session.require_input()?;
    let config = &session.config;

    // Require initialization
    let init_k = session.state.initial_intrinsics.as_ref()
        .context("Run step_init first")?;

    // Build and solve optimization problem
    let result = /* ... */;

    // Update state and output
    session.state.final_cost = Some(result.cost);
    session.state.mean_reproj_error = Some(result.reproj_error);
    session.set_output(MyOutput {
        calibrated_params: result.params,
        mean_reproj_error: result.reproj_error,
    })?;

    session.log_success("step_optimize", Some("Optimization complete"));
    Ok(())
}

/// Convenience pipeline function
pub fn run_calibration(
    session: &mut CalibrationSession<MyProblem>,
) -> Result<()> {
    step_init(session, None)?;
    step_optimize(session, None)?;
    Ok(())
}
}

Step 4: Module Re-exports (mod.rs)

#![allow(unused)]
fn main() {
mod problem;
mod state;
mod steps;

pub use problem::{MyProblem, MyConfig, MyInput, MyOutput, MyExport};
pub use state::MyState;
pub use steps::{step_init, step_optimize, run_calibration};
}

Step 5: Register in the Pipeline Crate

In crates/vision-calibration-pipeline/src/lib.rs:

#![allow(unused)]
fn main() {
pub mod my_problem;
}

Step 6: Wire into the Facade Crate

In crates/vision-calibration/src/lib.rs:

#![allow(unused)]
fn main() {
pub mod my_problem {
    pub use vision_calibration_pipeline::my_problem::*;
}
}

Do not add new workflows to the prelude by default; keep the prelude minimal for planar hello-world usage.

Testing

Write an integration test in crates/vision-calibration-pipeline/tests/:

#![allow(unused)]
fn main() {
#[test]
fn my_problem_session_workflow() -> Result<()> {
    let input = generate_synthetic_input();

    let mut session = CalibrationSession::<MyProblem>::new();
    session.set_input(input)?;

    step_init(&mut session, None)?;
    assert!(session.state.initial_intrinsics.is_some());

    step_optimize(&mut session, None)?;
    assert!(session.output().is_some());

    // Test JSON round-trip
    let json = session.to_json()?;
    let restored = CalibrationSession::<MyProblem>::from_json(&json)?;
    assert!(restored.output.is_some());

    Ok(())
}
}