Classical computer vision pipeline implementing camera calibration, undistortion, homography estimation, and feature-based image stitching — all in clean, reproducible Python scripts.
Check here for more perception projects and repos: github.com/IslamFadl
Camera calibration is the foundation of any geometric computer vision pipeline — stereo vision, structure from motion, SLAM, AR — none of it works without accurate intrinsic and extrinsic parameters.
This repo implements the full classical pipeline from scratch, with each script demonstrating one concept clearly. No magic, no black boxes — every step is visible, debuggable, and documented.
camera-calibration/
├── images/ # 13 OpenCV sample checkerboard images
├── output/ # Generated visualisations (gitignored)
│ ├── corners/ # Detected corners overlaid
│ ├── reprojection/ # Reprojection error visualisation
│ ├── undistorted/ # Before/after undistortion
│ ├── homography/ # Warped images
│ └── stitching/ # Stitched panorama
├── utils/
│ ├── __init__.py
│ └── visualisation.py # Plotting helpers
├── 01_find_corners.py # Checkerboard corner detection
├── 02_calibrate.py # Compute intrinsic matrix + distortion
├── 03_undistort.py # Apply calibration to fix lens distortion
├── 04_homography.py # SIFT + RANSAC homography between two views
├── 05_stitching.py # Multi-image panorama stitching
├── calibration_results.npz # Saved calibration parameters
├── requirements.txt
├── .gitignore
└── README.md
A checkerboard provides known 3D control points on a flat plane. By detecting the same corners across multiple images taken from different angles, we generate the correspondences needed to solve for camera intrinsics.
The script uses cv2.findChessboardCorners with adaptive thresholding,
then refines corner positions to sub-pixel accuracy with cv2.cornerSubPix.
Result on this dataset: 12 of 13 images have detectable corners.
The intrinsic matrix K describes the camera itself — independent of where it is in the world:
K = [ fx 0 cx ]
[ 0 fy cy ]
[ 0 0 1 ]
| Component | Meaning |
|---|---|
fx, fy |
Focal length in pixels (physical focal length / pixel size). Not millimetres. |
cx, cy |
Principal point — the pixel where the optical axis intersects the sensor. Ideally (width/2, height/2) but never exactly due to manufacturing tolerances. |
cv2.calibrateCamera also returns 5 distortion coefficients:
k1, k2, k3— radial distortion (barrel / pincushion — straight lines curve)p1, p2— tangential distortion (lens not perfectly parallel to sensor)
Reprojection error on this dataset: 0.XX pixels (under 1.0 = good calibration).
Once calibration is solved, any image taken with the same camera can be undistorted
with cv2.undistort(img, K, dist_coeffs). Before any geometric computation
(stereo, homography, pose estimation) you must undistort first — otherwise the
geometry is wrong.
The script produces side-by-side before/after visualisations to make the distortion visible. The corners of the image move noticeably; pixels near the centre barely shift.
A homography is a 3×3 matrix that maps any point in image 1 to its corresponding point in image 2, assuming the scene is planar or the camera only rotates.
The pipeline:
- Feature detection —
cv2.SIFT_create()finds keypoints with descriptors invariant to scale and rotation - Feature matching — brute-force matcher returns the 2 nearest descriptors per keypoint
- Lowe's ratio test — keep a match only if
distance(best) < 0.75 × distance(second_best). This filters ambiguous matches. - RANSAC homography — randomly sample 4 correspondences, compute candidate H, count inliers, repeat. Returns the H with the most inliers.
H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)Minimum correspondences needed: 4 (8 degrees of freedom).
Why RANSAC and not least-squares? SIFT produces ~100–500 matches but 10–30% are wrong. Least-squares is destroyed by outliers. RANSAC tolerates them by construction.
Combines steps 1–4 into a real application — stitching multiple overlapping
images into a panorama using the homographies between them. Each new image is
warped into the panorama coordinate system with cv2.warpPerspective.
These are the three matrices senior interviewers probe on:
| Matrix | When to use | Constraint |
|---|---|---|
| Homography | Planar scene or pure camera rotation | Scene must be flat or camera fixed in position |
| Essential matrix | Calibrated cameras, general motion | Cameras must be calibrated (K known) |
| Fundamental matrix | Uncalibrated cameras, general motion | Used in stereo matching, SfM |
Homographies are used here. Essential and Fundamental are covered in the
companion 3d-scene-understanding repo.
git clone https://github.com/IslamFadl/camera-calibration.git
cd camera-calibration
pip install -r requirements.txt
# Run the pipeline in order
python 01_find_corners.py
python 02_calibrate.py
python 03_undistort.py
python 04_homography.py
python 05_stitching.pyEach script produces visualisations in output/. Open them to verify the
results visually.
Tested on Python 3.11, macOS (M-series) and Linux. No GPU required.
After running 02_calibrate.py on the included sample images:
| Parameter | Value |
|---|---|
| Focal length fx, fy | ~530, ~530 pixels |
| Principal point cx, cy | ~320, ~240 |
| k1, k2 (radial) | -0.27, 0.11 |
| p1, p2 (tangential) | 0.001, 0.000 |
| Mean reprojection error | < 0.5 pixels |
A reprojection error under 1.0 pixel indicates a clean calibration. Under 0.5 is considered excellent.
MIT — use freely.