use super::ProjectionReject;
use crate::pose::Pose3d;
use kornia_algebra::{Vec2F64, Vec3F64};
pub fn project_point_fisheye(
camera: &FisheyeCamera,
pose: &Pose3d,
point_world: &Vec3F64,
) -> Option<(f64, f64, f64)> {
let p_cam = pose.transform_point(point_world);
let (pixel, z_cam) = camera.project(&p_cam)?;
Some((pixel.x, pixel.y, z_cam))
}
#[derive(Debug, Clone)]
pub struct FisheyeCamera {
pub fx: f64,
pub fy: f64,
pub cx: f64,
pub cy: f64,
pub k1: f64,
pub k2: f64,
pub k3: f64,
pub k4: f64,
}
const NEWTON_MAX_ITER: usize = 10;
const NEWTON_EPS: f64 = 1e-12;
const EPSILON: f64 = 1e-12;
impl FisheyeCamera {
fn distortion(&self, theta: f64) -> (f64, f64) {
let theta2 = theta * theta;
let theta4 = theta2 * theta2;
let theta6 = theta4 * theta2;
let theta8 = theta4 * theta4;
let theta_d = theta
+ self.k1 * theta2 * theta
+ self.k2 * theta4 * theta
+ self.k3 * theta6 * theta
+ self.k4 * theta8 * theta;
let f_prime = 1.0
+ 3.0 * self.k1 * theta2
+ 5.0 * self.k2 * theta4
+ 7.0 * self.k3 * theta6
+ 9.0 * self.k4 * theta8;
(theta_d, f_prime)
}
pub fn project(&self, point: &Vec3F64) -> Option<(Vec2F64, f64)> {
let x = point.x;
let y = point.y;
let z = point.z;
let r = (x * x + y * y).sqrt();
if r < EPSILON && z.abs() < EPSILON {
return None;
}
if r < EPSILON && z < 0.0 {
return None;
}
let theta = r.atan2(z);
let (theta_d, _) = self.distortion(theta);
if r < EPSILON {
return Some((Vec2F64::new(self.cx, self.cy), z));
}
let scale = theta_d / r;
let u = self.fx * scale * x + self.cx;
let v = self.fy * scale * y + self.cy;
Some((Vec2F64::new(u, v), z))
}
pub fn project_to_image(
&self,
p_cam: &Vec3F64,
image_size: kornia_image::ImageSize,
) -> Result<Vec2F64, ProjectionReject> {
let (pixel, _) = self
.project(p_cam)
.ok_or(ProjectionReject::InvalidProjection)?;
if pixel.x < 0.0
|| pixel.y < 0.0
|| pixel.x >= image_size.width as f64
|| pixel.y >= image_size.height as f64
{
return Err(ProjectionReject::OutOfImage);
}
Ok(pixel)
}
pub fn reprojection_error_sq_cam(
&self,
p_cam: &Vec3F64,
u_meas: f64,
v_meas: f64,
) -> Option<f64> {
let (projected, _) = self.project(p_cam)?;
let du = projected.x - u_meas;
let dv = projected.y - v_meas;
Some(du * du + dv * dv)
}
pub fn reprojection_error_sq_world(
&self,
pose_world_to_cam: &Pose3d,
p_world: &Vec3F64,
u_meas: f64,
v_meas: f64,
) -> Option<f64> {
let p_cam = pose_world_to_cam.transform_point(p_world);
self.reprojection_error_sq_cam(&p_cam, u_meas, v_meas)
}
pub fn unproject(&self, pixel: &Vec2F64) -> Vec3F64 {
let mx = (pixel.x - self.cx) / self.fx;
let my = (pixel.y - self.cy) / self.fy;
let theta_d = (mx * mx + my * my).sqrt();
if theta_d < EPSILON {
return Vec3F64::new(0.0, 0.0, 1.0);
}
let mut theta = theta_d;
for _ in 0..NEWTON_MAX_ITER {
let (theta_d_calc, f_prime) = self.distortion(theta);
if f_prime.abs() < EPSILON {
break;
}
let f = theta_d_calc - theta_d;
let delta = f / f_prime;
theta -= delta;
if delta.abs() < NEWTON_EPS {
break;
}
}
let phi = my.atan2(mx);
let sin_theta = theta.sin();
Vec3F64::new(sin_theta * phi.cos(), sin_theta * phi.sin(), theta.cos())
}
}
#[cfg(test)]
mod tests {
use super::*;
use kornia_algebra::Mat3F64;
fn fisheye_camera_zero_distortion() -> FisheyeCamera {
FisheyeCamera {
fx: 200.0,
fy: 200.0,
cx: 736.0,
cy: 720.0,
k1: 0.0,
k2: 0.0,
k3: 0.0,
k4: 0.0,
}
}
fn fisheye_camera_with_distortion() -> FisheyeCamera {
FisheyeCamera {
fx: 200.0,
fy: 200.0,
cx: 736.0,
cy: 720.0,
k1: 0.05,
k2: -0.01,
k3: 0.002,
k4: -0.0003,
}
}
#[test]
fn test_fisheye_project_on_axis() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(0.0, 0.0, 5.0);
let (uv, z) = cam.project(&p).unwrap();
assert!((uv.x - cam.cx).abs() < 1e-12);
assert!((uv.y - cam.cy).abs() < 1e-12);
assert!((z - 5.0).abs() < 1e-12);
}
#[test]
fn test_fisheye_project_optical_center_returns_none() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(0.0, 0.0, 0.0);
assert!(cam.project(&p).is_none());
}
#[test]
fn test_fisheye_project_negative_z_axis_returns_none() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(0.0, 0.0, -5.0);
assert!(cam.project(&p).is_none());
}
#[test]
fn test_fisheye_project_zero_distortion_45_deg() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(1.0, 0.0, 1.0);
let (uv, z) = cam.project(&p).unwrap();
let expected_u = cam.fx * std::f64::consts::FRAC_PI_4 + cam.cx;
assert!((uv.x - expected_u).abs() < 1e-10);
assert!((uv.y - cam.cy).abs() < 1e-10);
assert!((z - 1.0).abs() < 1e-12);
}
#[test]
fn test_fisheye_unproject_principal_point() {
let cam = fisheye_camera_zero_distortion();
let ray = cam.unproject(&Vec2F64::new(cam.cx, cam.cy));
assert!((ray.x).abs() < 1e-12);
assert!((ray.y).abs() < 1e-12);
assert!((ray.z - 1.0).abs() < 1e-12);
}
#[test]
fn test_fisheye_roundtrip_zero_distortion() {
let cam = fisheye_camera_zero_distortion();
let points = [
Vec3F64::new(1.0, 0.0, 1.0), Vec3F64::new(0.0, 1.0, 1.0), Vec3F64::new(1.0, 1.0, 1.0), Vec3F64::new(0.5, -0.3, 2.0), Vec3F64::new(0.0, 0.0, 3.0), ];
for pt in &points {
let (pixel, _) = cam.project(pt).unwrap();
let ray = cam.unproject(&pixel);
let len = (pt.x * pt.x + pt.y * pt.y + pt.z * pt.z).sqrt();
let dir = Vec3F64::new(pt.x / len, pt.y / len, pt.z / len);
assert!(
(ray.x - dir.x).abs() < 1e-6
&& (ray.y - dir.y).abs() < 1e-6
&& (ray.z - dir.z).abs() < 1e-6,
"Round-trip failed for point ({}, {}, {}): got ({}, {}, {}), expected ({}, {}, {})",
pt.x,
pt.y,
pt.z,
ray.x,
ray.y,
ray.z,
dir.x,
dir.y,
dir.z,
);
}
}
#[test]
fn test_fisheye_roundtrip_with_distortion() {
let cam = fisheye_camera_with_distortion();
let points = [
Vec3F64::new(1.0, 0.0, 1.0),
Vec3F64::new(0.0, 1.0, 1.0),
Vec3F64::new(1.0, 1.0, 2.0),
Vec3F64::new(-0.5, 0.3, 1.5),
Vec3F64::new(0.1, -0.1, 3.0),
];
for pt in &points {
let (pixel, _) = cam.project(pt).unwrap();
let ray = cam.unproject(&pixel);
let len = (pt.x * pt.x + pt.y * pt.y + pt.z * pt.z).sqrt();
let dir = Vec3F64::new(pt.x / len, pt.y / len, pt.z / len);
assert!(
(ray.x - dir.x).abs() < 1e-6
&& (ray.y - dir.y).abs() < 1e-6
&& (ray.z - dir.z).abs() < 1e-6,
"Round-trip failed for point ({}, {}, {}): got ({}, {}, {}), expected ({}, {}, {})",
pt.x,
pt.y,
pt.z,
ray.x,
ray.y,
ray.z,
dir.x,
dir.y,
dir.z,
);
}
}
#[test]
fn test_fisheye_project_near_optical_axis() {
let cam = fisheye_camera_with_distortion();
let p = Vec3F64::new(1e-10, 1e-10, 5.0);
let (uv, _) = cam.project(&p).unwrap();
assert!((uv.x - cam.cx).abs() < 1.0);
assert!((uv.y - cam.cy).abs() < 1.0);
}
#[test]
fn test_fisheye_roundtrip_wide_angle() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(5.0, 0.0, 1.0);
let (pixel, _) = cam.project(&p).unwrap();
let ray = cam.unproject(&pixel);
let len = (p.x * p.x + p.y * p.y + p.z * p.z).sqrt();
let dir = Vec3F64::new(p.x / len, p.y / len, p.z / len);
assert!((ray.x - dir.x).abs() < 1e-6);
assert!((ray.y - dir.y).abs() < 1e-6);
assert!((ray.z - dir.z).abs() < 1e-6);
}
#[test]
fn test_fisheye_project_behind_camera() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(1.0, 0.0, -1.0);
let (uv, z) = cam.project(&p).unwrap();
let expected_u = cam.fx * (3.0 * std::f64::consts::FRAC_PI_4) + cam.cx;
assert!((uv.x - expected_u).abs() < 1e-10);
assert!((z + 1.0).abs() < 1e-12);
}
#[test]
fn test_project_point_fisheye() {
let cam = fisheye_camera_zero_distortion();
let pose = crate::pose::Pose3d::new(Mat3F64::IDENTITY, Vec3F64::ZERO);
let p_world = Vec3F64::new(1.0, 0.0, 1.0);
let (u, v, z) = project_point_fisheye(&cam, &pose, &p_world).unwrap();
let expected_u = cam.fx * std::f64::consts::FRAC_PI_4 + cam.cx;
assert!((u - expected_u).abs() < 1e-10);
assert!((v - cam.cy).abs() < 1e-10);
assert!((z - 1.0).abs() < 1e-12);
}
#[test]
fn test_reprojection_error_sq_fisheye() {
let cam = fisheye_camera_zero_distortion();
let p = Vec3F64::new(1.0, 0.0, 1.0);
let (uv, _) = cam.project(&p).unwrap();
let err = cam.reprojection_error_sq_cam(&p, uv.x, uv.y).unwrap();
assert!(err < 1e-12);
let err_offset = cam.reprojection_error_sq_cam(&p, uv.x + 1.0, uv.y).unwrap();
assert!((err_offset - 1.0).abs() < 1e-12);
}
#[test]
fn test_fisheye_project_to_image() {
let cam = fisheye_camera_zero_distortion();
let size = kornia_image::ImageSize {
width: 800,
height: 800,
};
let p_in = Vec3F64::new(0.0, 0.0, 5.0);
assert!(cam.project_to_image(&p_in, size).is_ok());
let p_out = Vec3F64::new(10.0, 10.0, 0.5); assert_eq!(
cam.project_to_image(&p_out, size).unwrap_err(),
ProjectionReject::OutOfImage
);
}
}