Fisheye Projection
A pinhole projection divides by depth, so it cannot represent a ray at or beyond 90° from the optical axis, and it stretches the image toward that angle. A fisheye lens instead maps the incidence angle to the image radius.
Equidistant projection
For a camera-frame point , with and incidence angle :
The normalized point lies in the direction of the point's azimuth, at a
radius equal to in radians, so fx, fy are pixels per radian.
Every direction but straight back () projects; points with
are visible. Back-projection returns the unit direction
.
Kannala-Brandt distortion
KannalaBrandt4 is an odd radial polynomial in the radius of the
normalized point:
On equidistant coordinates , and the camera is the
Kannala-Brandt fisheye model, the one OpenCV's fisheye module calibrates:
ProjectionKind::Equidistant with DistortionKind::KannalaBrandt4. On
pinhole coordinates the same polynomial is a radial-only distortion with a
fourth term. Undistortion inverts the polynomial by Newton's method, which is
unique where increases: across the field a calibration covers.
#![allow(unused)] fn main() { use vision_calibration::core::{ CameraModelSpec, CameraParams, DistortionKind, DistortionParams, FxFyCxCy, ProjectionKind, ProjectionParams, SensorKind, SensorParams, }; let spec = CameraModelSpec::new( ProjectionKind::Equidistant, DistortionKind::KannalaBrandt4, SensorKind::Identity, ); let camera = CameraParams { projection: ProjectionParams::Equidistant, distortion: DistortionParams::from_values(spec.distortion, &[-0.02, 0.002, 0.0, 0.0])?, sensor: SensorParams::Identity, intrinsics: FxFyCxCy { fx: 300.0, fy: 300.0, cx: 640.0, cy: 400.0 }, }; let model = camera.build()?; }
In Python the same camera is
vc.Camera(intrinsics=..., distortion=vc.KannalaBrandt4Distortion(...), projection=vc.EquidistantProjection()), and vc.CameraModel projects and
back-projects it.
In the workflows
Every workflow that calibrates a camera accepts an equidistant one: set
camera.model.projection to Equidistant (a rig can do so per camera).
Its linear stage is the
bearing-based fisheye initializer in place of Zhang's
method, so no focal length or image size is needed; the initializer fits
k1, k2 unless the camera holds them, and the refinement frees the
rest.
#![allow(unused)] fn main() { use vision_calibration::core::{CameraModelSpec, DistortionKind, ProjectionKind, SensorKind}; use vision_calibration::planar_intrinsics::PlanarIntrinsicsConfig; let mut config = PlanarIntrinsicsConfig::default(); config.camera.model = CameraModelSpec::new( ProjectionKind::Equidistant, DistortionKind::KannalaBrandt4, SensorKind::Identity, ); }
Fisheye Calibration: OpenCV's Test Set runs this on real fisheye images and compares the result with OpenCV's.
The analysis tools — projection uncertainty, next-view planning, the information matrix and the reprojection report — follow the camera's rays, so they cover the whole fisheye image, beyond 90° included.
Some combinations have no initializer and are configuration errors:
- an equidistant camera with a Scheimpflug sensor;
- the line-distance laser residual (
LineDistNormalized), which needs the laser line's image to be straight: a fisheye laser workflow usesPointToPlane.
Seeds are values in the camera's own model: a fisheye camera takes seeded
intrinsics and a KannalaBrandt4 distortion seed, and poses then come from
the bearings of the pixels back-projected through the seeded camera.