Skip to content

Commit bfaa6d1

Browse files
authored
Add stereo rectify (#2)
* rename * rmat * finish
1 parent bcf68dc commit bfaa6d1

18 files changed

+257
-106
lines changed

Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "camera-intrinsic-model"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2021"
55
authors = ["Powei Lin <[email protected]>"]
66
readme = "README.md"
@@ -20,4 +20,11 @@ serde = { version = "1.0.215", features = ["derive"] }
2020
serde_json = "1.0.133"
2121

2222
[[example]]
23-
name = "remap"
23+
name = "remap"
24+
25+
[[example]]
26+
name = "stereo_rectify"
27+
28+
[dev-dependencies]
29+
imageproc = "0.25.0"
30+
rand = "0.8.5"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ For calibration to get the precise parameters. Please use [camera-intrinsic-cali
1919
```sh
2020
# undistort and remap
2121
cargo run -r --example remap
22+
23+
# undistort and rectify
24+
cargo run -r --example stereo_rectify
2225
```
2326

2427
## Acknowledgements
@@ -32,6 +35,6 @@ Papers:
3235
* Usenko, Vladyslav, Nikolaus Demmel, and Daniel Cremers. "The double sphere camera model." 2018 International Conference on 3D Vision (3DV). IEEE, 2018.
3336

3437
## TODO
35-
* [ ] Stereo Rectify
38+
* [x] Stereo Rectify
3639
* [x] FTheta Model
3740
* [ ] Python bindings

data/cam0.png

282 KB
Loading

data/cam1.png

281 KB
Loading
File renamed without changes.

data/eucm1.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"EUCM": {
3+
"fx": 190.28225119174908,
4+
"fy": 190.24433342367266,
5+
"cx": 252.58698029794184,
6+
"cy": 255.00661648870613,
7+
"alpha": 0.6278324184009648,
8+
"beta": 1.043827798877542,
9+
"width": 512,
10+
"height": 512
11+
}
12+
}

examples/remap.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ fn main() {
1818
];
1919
let model0 = eucm::EUCM::new(&params, 512, 512);
2020
model_to_json("eucm.json", &GenericModel::EUCM(model0));
21-
let model1 = model_from_json("data/eucm.json");
21+
let model1 = model_from_json("data/eucm0.json");
2222
let new_w_h = 1024;
2323
let p = model1.estimate_new_camera_matrix_for_undistort(1.0, Some((new_w_h, new_w_h)));
24-
let (xmap, ymap) = model1.init_undistort_map(&p, (new_w_h, new_w_h));
24+
let (xmap, ymap) = model1.init_undistort_map(&p, (new_w_h, new_w_h), None);
2525
let remaped = remap(&img, &xmap, &ymap);
2626
remaped.save("remaped.png").unwrap()
2727
}

examples/stereo_rectify.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use camera_intrinsic_model::*;
2+
use image::{ImageReader, Rgb, Rgba};
3+
use nalgebra as na;
4+
5+
fn main() {
6+
let img0 = ImageReader::open("data/cam0.png")
7+
.unwrap()
8+
.decode()
9+
.unwrap();
10+
let img1 = ImageReader::open("data/cam1.png")
11+
.unwrap()
12+
.decode()
13+
.unwrap();
14+
// let img0 = image::DynamicImage::ImageRgb8(img0.to_rgb8());
15+
// let img1 = image::DynamicImage::ImageRgb8(img1.to_rgb8());
16+
let model0 = model_from_json("data/eucm0.json");
17+
let model1 = model_from_json("data/eucm1.json");
18+
let tvec = na::Vector3::new(
19+
-0.10098947190325333,
20+
-0.0020811599784744455,
21+
-0.0012888359197775197,
22+
);
23+
let quat = na::Quaternion::new(
24+
0.9997158799903332,
25+
0.02382966001551074,
26+
-0.00032454324393309654,
27+
0.00044863167728445325,
28+
);
29+
let rvec = na::UnitQuaternion::from_quaternion(quat).scaled_axis();
30+
let (r0, r1, p) = stereo_rectify(&model0, &model1, &rvec, &tvec, None);
31+
let image_w_h = (
32+
model0.width().round() as u32,
33+
model0.height().round() as u32,
34+
);
35+
let (xmap0, ymap0) = model0.init_undistort_map(&p, image_w_h, Some(r0));
36+
let remaped0 = remap(&img0, &xmap0, &ymap0);
37+
let (xmap1, ymap1) = model1.init_undistort_map(&p, image_w_h, Some(r1));
38+
let remaped1 = remap(&img1, &xmap1, &ymap1);
39+
40+
let remaped0 = remaped0.rotate90().to_rgb8();
41+
let remaped1 = remaped1.rotate90().to_rgb8();
42+
let width = remaped0.width();
43+
let height = remaped0.height() * 2;
44+
let mut c0 = remaped0.to_vec();
45+
let mut c1 = remaped1.to_vec();
46+
c0.append(&mut c1);
47+
let cur = image::ImageBuffer::<Rgb<u8>, Vec<u8>>::from_vec(width, height, c0).unwrap();
48+
let mut cur = image::DynamicImage::ImageRgb8(cur).rotate270();
49+
50+
for row in (10..width).step_by(20) {
51+
imageproc::drawing::draw_line_segment_mut(
52+
&mut cur,
53+
(0.0, row as f32),
54+
(height as f32, row as f32),
55+
Rgba::<u8>([rand::random(), rand::random(), rand::random(), 255]),
56+
);
57+
}
58+
cur.save("undistort_rectified.png").unwrap()
59+
}

src/generic_functions.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use super::generic_model::*;
2+
3+
use nalgebra as na;
4+
use rayon::prelude::*;
5+
6+
pub fn init_undistort_map(
7+
camera_model: &dyn CameraModel<f64>,
8+
projection_mat: &na::Matrix3<f64>,
9+
new_w_h: (u32, u32),
10+
rotation: Option<na::Rotation3<f64>>,
11+
) -> (na::DMatrix<f32>, na::DMatrix<f32>) {
12+
if projection_mat.shape() != (3, 3) {
13+
panic!("projection matrix has the wrong shape");
14+
}
15+
let fx = projection_mat[(0, 0)];
16+
let fy = projection_mat[(1, 1)];
17+
let cx = projection_mat[(0, 2)];
18+
let cy = projection_mat[(1, 2)];
19+
let rmat_inv = rotation
20+
.unwrap_or(na::Rotation3::identity())
21+
.inverse()
22+
.matrix()
23+
.to_owned();
24+
let p3ds: Vec<na::Vector3<f64>> = (0..new_w_h.1)
25+
.into_par_iter()
26+
.flat_map(|y| {
27+
(0..new_w_h.0)
28+
.into_par_iter()
29+
.map(|x| {
30+
rmat_inv * na::Vector3::new((x as f64 - cx) / fx, (y as f64 - cy) / fy, 1.0)
31+
})
32+
.collect::<Vec<na::Vector3<f64>>>()
33+
})
34+
.collect();
35+
let p2ds = camera_model.project(&p3ds);
36+
let (xvec, yvec): (Vec<f32>, Vec<f32>) = p2ds
37+
.par_iter()
38+
.map(|xy| {
39+
if let Some(xy) = xy {
40+
(xy[0] as f32, xy[1] as f32)
41+
} else {
42+
(f32::NAN, f32::NAN)
43+
}
44+
})
45+
.unzip();
46+
let xmap = na::DMatrix::from_vec(new_w_h.1 as usize, new_w_h.0 as usize, xvec);
47+
let ymap = na::DMatrix::from_vec(new_w_h.1 as usize, new_w_h.0 as usize, yvec);
48+
(xmap, ymap)
49+
}
50+
51+
pub fn estimate_new_camera_matrix_for_undistort(
52+
camera_model: &dyn CameraModel<f64>,
53+
balance: f64,
54+
new_image_w_h: Option<(u32, u32)>,
55+
) -> na::Matrix3<f64> {
56+
if !(0.0..=1.0).contains(&balance) {
57+
panic!("balance should be [0.0-1.0], got {}", balance);
58+
}
59+
let params = camera_model.params();
60+
let cx = params[2];
61+
let cy = params[3];
62+
let w = camera_model.width();
63+
let h = camera_model.height();
64+
let p2ds = vec![
65+
na::Vector2::new(cx, 0.0),
66+
na::Vector2::new(w - 1.0, cy),
67+
na::Vector2::new(cx, h - 1.0),
68+
na::Vector2::new(0.0, cy),
69+
];
70+
let undist_pts = camera_model.unproject(&p2ds);
71+
let mut min_x = f64::MAX;
72+
let mut min_y = f64::MAX;
73+
let mut max_x = f64::MIN;
74+
let mut max_y = f64::MIN;
75+
for p in undist_pts {
76+
let p = p.unwrap();
77+
min_x = min_x.min(p.x);
78+
min_y = min_y.min(p.y);
79+
max_x = max_x.max(p.x);
80+
max_y = max_y.max(p.y);
81+
}
82+
min_x = min_x.abs();
83+
min_y = min_y.abs();
84+
let (new_w, new_h) = if let Some((new_w, new_h)) = new_image_w_h {
85+
(new_w, new_h)
86+
} else {
87+
(camera_model.width() as u32, camera_model.height() as u32)
88+
};
89+
let new_cx = new_w as f64 * min_x / (min_x + max_x);
90+
let new_cy = new_h as f64 * min_y / (min_y + max_y);
91+
let fx = new_w as f64 / (min_x + max_x);
92+
let fy = new_h as f64 / (min_y + max_y);
93+
let fmin = fx.min(fy);
94+
let fmax = fx.max(fy);
95+
let new_f = balance * fmin + (1.0 - balance) * fmax;
96+
97+
let mut out = na::Matrix3::identity();
98+
out[(0, 0)] = new_f;
99+
out[(1, 1)] = new_f;
100+
out[(0, 2)] = new_cx;
101+
out[(1, 2)] = new_cy;
102+
out
103+
}
104+
105+
pub fn stereo_rectify(
106+
camera_model0: &GenericModel<f64>,
107+
camera_model1: &GenericModel<f64>,
108+
rvec: &na::Vector3<f64>,
109+
tvec: &na::Vector3<f64>,
110+
new_image_w_h: Option<(u32, u32)>,
111+
) -> (na::Rotation3<f64>, na::Rotation3<f64>, na::Matrix3<f64>) {
112+
// compensate for rotation first
113+
let r_half_inv = -rvec / 2.0;
114+
115+
// another rotation for compensating the translation
116+
// use translation to determine x axis rectify or y axis rectify
117+
let rotated_t = na::Rotation3::from_scaled_axis(r_half_inv) * tvec;
118+
let idx = if rotated_t.x.abs() > rotated_t.y.abs() {
119+
0
120+
} else {
121+
1
122+
};
123+
let axis_to_rectify = rotated_t[idx];
124+
let translation_norm = rotated_t.norm();
125+
let mut unit_vector = na::Vector3::zeros();
126+
if axis_to_rectify > 0.0 {
127+
unit_vector[idx] = 1.0;
128+
} else {
129+
unit_vector[idx] = -1.0;
130+
}
131+
132+
let mut axis_for_rotation = rotated_t.cross(&unit_vector);
133+
let axis_norm = axis_for_rotation.norm();
134+
let mut rotation_for_compensating_translation = na::Rotation3::identity();
135+
if axis_norm > 0.0 {
136+
axis_for_rotation /= axis_norm;
137+
axis_for_rotation *= (axis_to_rectify.abs() / translation_norm).acos();
138+
rotation_for_compensating_translation = na::Rotation3::from_scaled_axis(axis_for_rotation);
139+
}
140+
141+
let rotation_for_cam0 =
142+
rotation_for_compensating_translation * na::Rotation3::from_scaled_axis(-r_half_inv);
143+
let rotation_for_cam1 =
144+
rotation_for_compensating_translation * na::Rotation3::from_scaled_axis(r_half_inv);
145+
146+
// new projection matrix
147+
let new_cam_mat0 = camera_model0.estimate_new_camera_matrix_for_undistort(0.0, new_image_w_h);
148+
let new_cam_mat1 = camera_model1.estimate_new_camera_matrix_for_undistort(0.0, new_image_w_h);
149+
let mut avg_new_mat = na::Matrix3::identity();
150+
let avg_f = (new_cam_mat0[(0, 0)] + new_cam_mat1[(0, 0)]) / 2.0;
151+
avg_new_mat[(0, 0)] = avg_f;
152+
avg_new_mat[(1, 1)] = avg_f;
153+
let avg_cx = (new_cam_mat0[(0, 2)] + new_cam_mat1[(0, 2)]) / 2.0;
154+
avg_new_mat[(0, 2)] = avg_cx;
155+
let avg_cy = (new_cam_mat0[(1, 2)] + new_cam_mat1[(1, 2)]) / 2.0;
156+
avg_new_mat[(1, 2)] = avg_cy;
157+
(rotation_for_cam0, rotation_for_cam1, avg_new_mat)
158+
}

src/generic.rs renamed to src/generic_model.rs

Lines changed: 2 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::str::FromStr;
22

3+
use super::generic_functions::*;
34
use super::{Ftheta, KannalaBrandt4, OpenCVModel5, EUCM, EUCMT, UCM};
45
use image::DynamicImage;
56
use nalgebra as na;
@@ -68,7 +69,7 @@ macro_rules! generic_impl_self {
6869
};
6970
}
7071
impl GenericModel<f64> {
71-
generic_impl!(init_undistort_map, (na::DMatrix<f32>, na::DMatrix<f32>), projection_mat: &na::Matrix3<f64>, new_w_h: (u32, u32));
72+
generic_impl!(init_undistort_map, (na::DMatrix<f32>, na::DMatrix<f32>), projection_mat: &na::Matrix3<f64>, new_w_h: (u32, u32), rotation: Option<na::Rotation3<f64>>);
7273
generic_impl!(estimate_new_camera_matrix_for_undistort, na::Matrix3<f64>, balance: f64, new_image_w_h: Option<(u32, u32)>);
7374
}
7475

@@ -229,94 +230,3 @@ where
229230
.collect()
230231
}
231232
}
232-
233-
pub fn init_undistort_map(
234-
camera_model: &dyn CameraModel<f64>,
235-
projection_mat: &na::Matrix3<f64>,
236-
new_w_h: (u32, u32),
237-
) -> (na::DMatrix<f32>, na::DMatrix<f32>) {
238-
if projection_mat.shape() != (3, 3) {
239-
panic!("projection matrix has the wrong shape");
240-
}
241-
let fx = projection_mat[(0, 0)];
242-
let fy = projection_mat[(1, 1)];
243-
let cx = projection_mat[(0, 2)];
244-
let cy = projection_mat[(1, 2)];
245-
let p3ds: Vec<na::Vector3<f64>> = (0..new_w_h.1)
246-
.into_par_iter()
247-
.flat_map(|y| {
248-
(0..new_w_h.0)
249-
.into_par_iter()
250-
.map(|x| na::Vector3::new((x as f64 - cx) / fx, (y as f64 - cy) / fy, 1.0))
251-
.collect::<Vec<na::Vector3<f64>>>()
252-
})
253-
.collect();
254-
let p2ds = camera_model.project(&p3ds);
255-
let (xvec, yvec): (Vec<f32>, Vec<f32>) = p2ds
256-
.par_iter()
257-
.map(|xy| {
258-
if let Some(xy) = xy {
259-
(xy[0] as f32, xy[1] as f32)
260-
} else {
261-
(f32::NAN, f32::NAN)
262-
}
263-
})
264-
.unzip();
265-
let xmap = na::DMatrix::from_vec(new_w_h.1 as usize, new_w_h.0 as usize, xvec);
266-
let ymap = na::DMatrix::from_vec(new_w_h.1 as usize, new_w_h.0 as usize, yvec);
267-
(xmap, ymap)
268-
}
269-
270-
pub fn estimate_new_camera_matrix_for_undistort(
271-
camera_model: &dyn CameraModel<f64>,
272-
balance: f64,
273-
new_image_w_h: Option<(u32, u32)>,
274-
) -> na::Matrix3<f64> {
275-
if !(0.0..=1.0).contains(&balance) {
276-
panic!("balance should be [0.0-1.0], got {}", balance);
277-
}
278-
let params = camera_model.params();
279-
let cx = params[2];
280-
let cy = params[3];
281-
let w = camera_model.width();
282-
let h = camera_model.height();
283-
let p2ds = vec![
284-
na::Vector2::new(cx, 0.0),
285-
na::Vector2::new(w - 1.0, cy),
286-
na::Vector2::new(cx, h - 1.0),
287-
na::Vector2::new(0.0, cy),
288-
];
289-
let undist_pts = camera_model.unproject(&p2ds);
290-
let mut min_x = f64::MAX;
291-
let mut min_y = f64::MAX;
292-
let mut max_x = f64::MIN;
293-
let mut max_y = f64::MIN;
294-
for p in undist_pts {
295-
let p = p.unwrap();
296-
min_x = min_x.min(p.x);
297-
min_y = min_y.min(p.y);
298-
max_x = max_x.max(p.x);
299-
max_y = max_y.max(p.y);
300-
}
301-
min_x = min_x.abs();
302-
min_y = min_y.abs();
303-
let (new_w, new_h) = if let Some((new_w, new_h)) = new_image_w_h {
304-
(new_w, new_h)
305-
} else {
306-
(camera_model.width() as u32, camera_model.height() as u32)
307-
};
308-
let new_cx = new_w as f64 * min_x / (min_x + max_x);
309-
let new_cy = new_h as f64 * min_y / (min_y + max_y);
310-
let fx = new_w as f64 / (min_x + max_x);
311-
let fy = new_h as f64 / (min_y + max_y);
312-
let fmin = fx.min(fy);
313-
let fmax = fx.max(fy);
314-
let new_f = balance * fmin + (1.0 - balance) * fmax;
315-
316-
let mut out = na::Matrix3::identity();
317-
out[(0, 0)] = new_f;
318-
out[(1, 1)] = new_f;
319-
out[(0, 2)] = new_cx;
320-
out[(1, 2)] = new_cy;
321-
out
322-
}

0 commit comments

Comments
 (0)