Skip to content

Commit 3849e6d

Browse files
authored
Clean up (#24)
* update aprilgrid * clean up * add tests * clean up * docstring
1 parent b18da34 commit 3849e6d

File tree

16 files changed

+666
-86
lines changed

16 files changed

+666
-86
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ jobs:
2020
run: cargo fmt --check --verbose
2121
- name: Linting
2222
run: cargo clippy
23+
- name: Test
24+
run: cargo test --verbose
2325
- name: Build
2426
run: cargo build --verbose

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "camera-intrinsic-calibration"
3-
version = "0.9.0"
3+
version = "0.10.0"
44
edition = "2024"
55
authors = ["Powei Lin <poweilin1994@gmail.com>"]
66
readme = "README.md"
@@ -21,7 +21,7 @@ exclude = [
2121
]
2222

2323
[dependencies]
24-
aprilgrid = "0.6.1"
24+
aprilgrid = "0.7.0"
2525
camera-intrinsic-model = "0.6.0"
2626
clap = { version = "4.5", features = ["derive"] }
2727
colorous = "1.0.16"

src/bin/camera_calibration.rs

Lines changed: 130 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,56 @@ fn main() {
7272

7373
let cli = CCRSCli::parse();
7474
let detector = TagDetector::new(&cli.tag_family, None);
75-
let board = if let Some(board_config_path) = cli.board_config {
76-
Board::from_config(&object_from_json(&board_config_path))
75+
let board = setup_board(&cli);
76+
let output_folder = setup_output_folder(&cli);
77+
78+
let recording = rerun::RecordingStreamBuilder::new("calibration")
79+
.save(format!("{}/logging.rrd", output_folder))
80+
.unwrap();
81+
recording
82+
.log_static("/", &rerun::ViewCoordinates::RDF())
83+
.unwrap();
84+
85+
let cams_detected_feature_frames = load_feature_data(&cli, &detector, &board, &recording);
86+
87+
let (calibrated_intrinsics, cam_rtvecs) =
88+
calibrate_all_cameras(&cli, &cams_detected_feature_frames, &recording);
89+
90+
let t_cam_i_0_init = init_camera_extrinsic(&cam_rtvecs);
91+
92+
save_and_validate_results(
93+
&cli,
94+
&output_folder,
95+
&cams_detected_feature_frames,
96+
&calibrated_intrinsics,
97+
&cam_rtvecs,
98+
&t_cam_i_0_init,
99+
&recording,
100+
);
101+
}
102+
103+
/// Loads the board configuration specified in the CLI arguments or creates a default one.
104+
///
105+
/// If a config file path is provided via `--board-config`, it loads from that file.
106+
/// Otherwise, it creates a default 6x6 AprilGrid configuration and saves it to `default_board_config.json`.
107+
fn setup_board(cli: &CCRSCli) -> Board {
108+
if let Some(board_config_path) = &cli.board_config {
109+
Board::from_config(&object_from_json(board_config_path))
77110
} else {
78111
let config = BoardConfig::default();
79112
object_to_json("default_board_config.json", &config);
80113
Board::from_config(&config)
81-
};
82-
let dataset_root = &cli.path;
83-
let now = Instant::now();
84-
let output_folder = if let Some(output_folder) = cli.output_folder {
85-
output_folder
114+
}
115+
}
116+
117+
/// Sets up the output directory for calibration results.
118+
///
119+
/// If `--output-folder` is specified, uses that path.
120+
/// Otherwise, creates a directory named with the current timestamp under `results/`.
121+
/// Ensures the directory exists.
122+
fn setup_output_folder(cli: &CCRSCli) -> String {
123+
let output_folder = if let Some(output_folder) = &cli.output_folder {
124+
output_folder.clone()
86125
} else {
87126
let now = OffsetDateTime::now_local().unwrap();
88127
format!(
@@ -96,52 +135,84 @@ fn main() {
96135
)
97136
};
98137
std::fs::create_dir_all(&output_folder).expect("Valid path");
138+
output_folder
139+
}
99140

100-
let recording = rerun::RecordingStreamBuilder::new("calibration")
101-
.save(format!("{}/logging.rrd", output_folder))
102-
.unwrap();
103-
recording
104-
.log_static("/", &rerun::ViewCoordinates::RDF())
105-
.unwrap();
141+
/// Loads feature data from the dataset.
142+
///
143+
/// Supports Euroc and General dataset formats.
144+
/// Uses the provided tag detector and board configuration to extract features.
145+
/// Logs images to Rerun if enabled.
146+
///
147+
/// # Returns
148+
/// A vector of vectors, where each inner vector contains `Option<FrameFeature>` for a camera.
149+
fn load_feature_data(
150+
cli: &CCRSCli,
151+
detector: &TagDetector,
152+
board: &Board,
153+
recording: &rerun::RecordingStream,
154+
) -> Vec<Vec<Option<FrameFeature>>> {
106155
trace!("Start loading data");
107156
println!("Start loading images and detecting charts.");
157+
let now = Instant::now();
108158
let mut cams_detected_feature_frames: Vec<Vec<Option<FrameFeature>>> = match cli.dataset_format
109159
{
110160
DatasetFormat::Euroc => load_euroc(
111-
dataset_root,
112-
&detector,
113-
&board,
161+
&cli.path,
162+
detector,
163+
board,
114164
cli.start_idx,
115165
cli.step,
116166
cli.cam_num,
117-
Some(&recording),
167+
Some(recording),
118168
),
119169
DatasetFormat::General => load_others(
120-
dataset_root,
121-
&detector,
122-
&board,
170+
&cli.path,
171+
detector,
172+
board,
123173
cli.start_idx,
124174
cli.step,
125175
cli.cam_num,
126-
Some(&recording),
176+
Some(recording),
127177
),
128178
};
129179
let duration_sec = now.elapsed().as_secs_f64();
130180
println!("detecting feature took {:.6} sec", duration_sec);
131-
println!("total: {} images", cams_detected_feature_frames[0].len());
181+
if !cams_detected_feature_frames.is_empty() {
182+
println!("total: {} images", cams_detected_feature_frames[0].len());
183+
println!(
184+
"avg: {} sec",
185+
duration_sec / cams_detected_feature_frames[0].len() as f64
186+
);
187+
}
188+
132189
cams_detected_feature_frames
133190
.iter_mut()
134191
.for_each(|f| f.truncate(cli.max_images));
135-
println!(
136-
"avg: {} sec",
137-
duration_sec / cams_detected_feature_frames[0].len() as f64
138-
);
139-
let (calibrated_intrinsics, cam_rtvecs): (Vec<_>, Vec<_>) = cams_detected_feature_frames
192+
193+
cams_detected_feature_frames
194+
}
195+
196+
/// Calibrates all cameras individually.
197+
///
198+
/// Iterates through each camera, detecting features and running the optimization.
199+
/// Retries calibration up to 3 times if it fails.
200+
///
201+
/// # Returns
202+
/// A tuple containing:
203+
/// - `Vec<GenericModel<f64>>`: The calibrated intrinsic models for each camera.
204+
/// - `Vec<HashMap<usize, RvecTvec>>`: estimated camera poses for each frame.
205+
fn calibrate_all_cameras(
206+
cli: &CCRSCli,
207+
cams_detected_feature_frames: &[Vec<Option<FrameFeature>>],
208+
recording: &rerun::RecordingStream,
209+
) -> (Vec<GenericModel<f64>>, Vec<HashMap<usize, RvecTvec>>) {
210+
cams_detected_feature_frames
140211
.iter()
141212
.enumerate()
142213
.map(|(cam_idx, feature_frames)| {
143214
let topic = format!("/cam{}", cam_idx);
144-
log_feature_frames(&recording, &topic, feature_frames);
215+
log_feature_frames(recording, &topic, feature_frames);
145216
let mut calibrated_result: Option<(GenericModel<f64>, HashMap<usize, RvecTvec>)> = None;
146217
let max_trials = 3;
147218
let cam0_fixed_focal = if cam_idx == 0 { cli.fixed_focal } else { None };
@@ -153,9 +224,9 @@ fn main() {
153224
for trial in 0..max_trials {
154225
calibrated_result = init_and_calibrate_one_camera(
155226
cam_idx,
156-
&cams_detected_feature_frames,
227+
cams_detected_feature_frames,
157228
&cli.model,
158-
&recording,
229+
recording,
159230
&calib_params,
160231
trial > 0,
161232
);
@@ -169,19 +240,35 @@ fn main() {
169240
cam_idx, max_trials
170241
);
171242
}
172-
let (final_result, rtvec_map) = calibrated_result.unwrap();
173-
(final_result, rtvec_map)
243+
calibrated_result.unwrap()
174244
})
175-
.unzip();
176-
let t_cam_i_0_init = init_camera_extrinsic(&cam_rtvecs);
177-
for t in &t_cam_i_0_init {
245+
.unzip()
246+
}
247+
248+
/// Saves calibration results and performs validation.
249+
///
250+
/// saves intrinsics, extrinsics, and pose data to JSON files.
251+
/// Generates a validation report and logs visualization data to Rerun.
252+
/// If multiple cameras are present, it also attempts to calibrate extrinsics between cameras.
253+
#[allow(clippy::too_many_arguments)]
254+
fn save_and_validate_results(
255+
cli: &CCRSCli,
256+
output_folder: &str,
257+
cams_detected_feature_frames: &[Vec<Option<FrameFeature>>],
258+
intrinsics: &[GenericModel<f64>],
259+
cam_rtvecs: &[HashMap<usize, RvecTvec>],
260+
t_cam_i_0_init: &[RvecTvec],
261+
recording: &rerun::RecordingStream,
262+
) {
263+
for t in t_cam_i_0_init {
178264
println!("r {} t {}", t.na_rvec(), t.na_tvec());
179265
}
266+
180267
if let Some((camera_intrinsics, t_i_0, board_rtvecs)) = calib_all_camera_with_extrinsics(
181-
&calibrated_intrinsics,
182-
&t_cam_i_0_init,
183-
&cam_rtvecs,
184-
&cams_detected_feature_frames,
268+
intrinsics,
269+
t_cam_i_0_init,
270+
cam_rtvecs,
271+
cams_detected_feature_frames,
185272
cli.one_focal || cli.fixed_focal.is_some(),
186273
cli.disabled_distortion_num,
187274
cli.fixed_focal.is_some(),
@@ -215,7 +302,7 @@ fn main() {
215302
intrinsic,
216303
&new_rtvec_map,
217304
&cams_detected_feature_frames[cam_idx],
218-
Some(&recording),
305+
Some(recording),
219306
);
220307
rep_rms.push(rep);
221308
println!(
@@ -232,15 +319,13 @@ fn main() {
232319
);
233320
} else {
234321
let mut rep_rms = Vec::new();
235-
for (cam_idx, (intrinsic, rtvec_map)) in
236-
calibrated_intrinsics.iter().zip(cam_rtvecs).enumerate()
237-
{
322+
for (cam_idx, (intrinsic, rtvec_map)) in intrinsics.iter().zip(cam_rtvecs).enumerate() {
238323
let rep = validation(
239324
cam_idx,
240325
intrinsic,
241-
&rtvec_map,
326+
rtvec_map,
242327
&cams_detected_feature_frames[cam_idx],
243-
Some(&recording),
328+
Some(recording),
244329
);
245330
rep_rms.push(rep);
246331
println!(

src/board.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use glam;
22
use serde::{Deserialize, Serialize};
33
use std::collections::HashMap;
44

5+
/// Configuration for an AprilGrid board.
56
#[derive(Debug, Serialize, Deserialize)]
67
pub struct BoardConfig {
78
tag_size_meter: f32,
@@ -23,7 +24,9 @@ impl Default for BoardConfig {
2324
}
2425
}
2526

27+
/// Represents a calibration board with known 3D points.
2628
pub struct Board {
29+
/// Maps tag ID to its 3D position (usually top-left or other corner definition).
2730
pub id_to_3d: HashMap<u32, glam::Vec3>,
2831
}
2932

@@ -37,6 +40,9 @@ impl Board {
3740
board_config.first_id,
3841
)
3942
}
43+
/// Initializes an AprilGrid board with standard layout.
44+
///
45+
/// The grid is generated starting from (0,0,0) and expanding in positive X (cols) and negative Y (rows).
4046
pub fn init_aprilgrid(
4147
tag_size_meter: f32,
4248
tag_spacing: f32,
@@ -89,6 +95,7 @@ impl Board {
8995
}
9096
}
9197

98+
/// Creates a default 6x6 AprilGrid board.
9299
pub fn create_default_6x6_board() -> Board {
93100
Board::init_aprilgrid(0.088, 0.3, 6, 6, 0)
94101
}

src/data_loader.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use rerun::TimeCell;
1414

1515
const MIN_CORNERS: usize = 24;
1616

17+
/// Parses the timestamp from a file path.
18+
///
19+
/// Assumes the filename (without extension) is a timestamp in nanoseconds.
1720
fn path_to_timestamp(path: &Path) -> i64 {
1821
let time_ns: i64 = path
1922
.file_stem()
@@ -25,6 +28,11 @@ fn path_to_timestamp(path: &Path) -> i64 {
2528
time_ns
2629
}
2730

31+
/// Detects features in an image and converts it to a `FrameFeature`.
32+
///
33+
/// Uses `aprilgrid` detector to find tags.
34+
/// matches detected point IDs to 3D board coordinates.
35+
/// Returns `None` if the number of detected corners is less than `min_corners`.
2836
fn image_to_option_feature_frame(
2937
tag_detector: &TagDetector,
3038
img: &DynamicImage,
@@ -72,6 +80,18 @@ fn img_filter(rp: glob::GlobResult) -> Option<std::path::PathBuf> {
7280
None
7381
}
7482

83+
/// Loads data from a Euroc-style dataset.
84+
///
85+
/// Iterates through camera folders, loads images, detects features in parallel.
86+
///
87+
/// # Arguments
88+
/// * `root_folder` - Path to the dataset root.
89+
/// * `tag_detector` - The tag detector instance.
90+
/// * `board` - The calibration board configuration.
91+
/// * `start_idx` - Starting image index.
92+
/// * `step` - Step size for sampling images.
93+
/// * `cam_num` - Number of cameras.
94+
/// * `recording_option` - Optional Rerun recording stream for visualization.
7595
pub fn load_euroc(
7696
root_folder: &str,
7797
tag_detector: &TagDetector,
@@ -92,8 +112,7 @@ pub fn load_euroc(
92112
sorted_path.sort();
93113
let new_paths: Vec<_> = sorted_path.iter().skip(start_idx).step_by(step).collect();
94114
let mut time_frame: Vec<_> = new_paths
95-
.iter()
96-
.par_bridge()
115+
.par_iter()
97116
.progress_count(new_paths.len() as u64)
98117
.map(|path| {
99118
let time_ns = path_to_timestamp(path);
@@ -124,6 +143,20 @@ pub fn load_euroc(
124143
.collect()
125144
}
126145

146+
/// Loads data from a general dataset structure.
147+
///
148+
/// Iterates through camera folders matching `**/cam{}/**/*`.
149+
/// Loads images and detects features in parallel.
150+
/// Timestamps are generated artificially based on index if not present in filename (though this function ignores filename timestamp logic in favor of index-based).
151+
///
152+
/// # Arguments
153+
/// * `root_folder` - Path to the dataset root.
154+
/// * `tag_detector` - The tag detector instance.
155+
/// * `board` - The calibration board configuration.
156+
/// * `start_idx` - Starting image index.
157+
/// * `step` - Step size for sampling images.
158+
/// * `cam_num` - Number of cameras.
159+
/// * `recording_option` - Optional Rerun recording stream for visualization.
127160
pub fn load_others(
128161
root_folder: &str,
129162
tag_detector: &TagDetector,
@@ -149,8 +182,7 @@ pub fn load_others(
149182
.enumerate()
150183
.collect();
151184
let mut time_frame: Vec<_> = new_paths
152-
.iter()
153-
.par_bridge()
185+
.par_iter()
154186
.progress_count(new_paths.len() as u64)
155187
.map(|(idx, path)| {
156188
let time_ns = *idx as i64 * 100000000;

0 commit comments

Comments
 (0)