196 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# IMX219 Camera Calibration Workflow
Full step-by-step calibration procedure for the 4× IMX219 160° fisheye CSI cameras
on saltybot (cam0=front, cam1=right, cam2=rear, cam3=left).
## Hardware
| Camera | Position | Yaw | Topic |
|--------|----------|-------|------------------------------|
| cam0 | Front | 0° | /camera/front/image_raw |
| cam1 | Right | -90° | /camera/right/image_raw |
| cam2 | Rear | 180° | /camera/rear/image_raw |
| cam3 | Left | 90° | /camera/left/image_raw |
Mount geometry: radius=5cm from robot centre, height=30cm, 90° angular spacing.
## Prerequisites
- OpenCV with fisheye support: `pip3 install opencv-python numpy pyyaml`
- For ROS2 mode: ROS2 Humble, cv_bridge, image_transport
- Printed checkerboard: 8×6 inner corners, 25mm squares (A3 paper recommended)
- Generator: `python3 -c "import cv2; board=cv2.aruco.CharucoBoard((9,7),0.025,0.0175,cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)); cv2.imwrite('board.png', board.generateImage((700,500)))"`
- Or download from: https://calib.io/pages/camera-calibration-pattern-generator
## Step 1 — Print and Prepare the Checkerboard
- Print the 8×6 inner-corner checkerboard at exactly 25mm per square
- Mount flat on a rigid board (cardboard or foamcore) — no warping allowed
- Measure actual square size with calipers, update `--square-size` if different
## Step 2 — Start Cameras
```bash
# On the Jetson, in the saltybot docker container:
ros2 launch saltybot_cameras cameras.launch.py
# Verify all 4 topics are publishing:
ros2 topic list | grep camera
ros2 topic hz /camera/front/image_raw
```
## Step 3 — Capture Calibration Images (30 per camera)
Run for each camera in turn. Move the board to many different positions and
angles within the field of view — especially corners and edges.
```bash
# Option A: ROS2 mode (recommended on Jetson, requires running cameras)
ros2 run saltybot_calibration capture_calibration_images.py \
--ros --camera front --n-images 30 \
--output-dir /mnt/nvme/saltybot/calibration/raw
ros2 run saltybot_calibration capture_calibration_images.py \
--ros --camera right --n-images 30 \
--output-dir /mnt/nvme/saltybot/calibration/raw
ros2 run saltybot_calibration capture_calibration_images.py \
--ros --camera rear --n-images 30 \
--output-dir /mnt/nvme/saltybot/calibration/raw
ros2 run saltybot_calibration capture_calibration_images.py \
--ros --camera left --n-images 30 \
--output-dir /mnt/nvme/saltybot/calibration/raw
# Option B: All cameras sequentially (prompts between cameras)
ros2 run saltybot_calibration capture_calibration_images.py \
--ros --camera all --n-images 30 \
--output-dir /mnt/nvme/saltybot/calibration/raw
# Option C: Standalone (no ROS2, uses /dev/videoX directly)
python3 capture_calibration_images.py \
--camera front --n-images 30 \
--device /dev/video0 \
--output-dir /mnt/nvme/saltybot/calibration/raw
```
### Capture Tips for Fisheye Lenses
- Cover the entire FoV — include corners and edges (where distortion is strongest)
- Use 5+ different orientations of the board (tilt, rotate, skew)
- Keep the board fully visible (no partial views)
- Good lighting, no motion blur
- Aim for 20-30 images with varied positions
## Step 4 — Compute Intrinsics per Camera
```bash
for cam in front right rear left; do
ros2 run saltybot_calibration calibrate_camera.py \
--images-dir /mnt/nvme/saltybot/calibration/raw/cam_${cam} \
--output /mnt/nvme/saltybot/calibration/${cam}/camera_info.yaml \
--camera-name camera_${cam} \
--board-size 8x6 \
--square-size 0.025 \
--image-size 640x480 \
--show-reprojection
done
```
## Step 5 — Verify Reprojection Error
Target: RMS reprojection error < 1.5 px per camera.
If RMS > 1.5 px:
1. Remove outlier images (highest per-image error from table)
2. Recapture with more varied board positions
3. Check board is truly flat and square size is correct
4. Try `--image-size 1280x720` if capturing at higher resolution
```bash
# Quick check — view undistorted preview
ros2 run saltybot_calibration preview_undistorted.py \
--image /mnt/nvme/saltybot/calibration/raw/cam_front/frame_0000.png \
--calibration /mnt/nvme/saltybot/calibration/front/camera_info.yaml
```
## Step 6 — Compute Extrinsics (Mechanical Mode)
Uses known mount geometry (radius=5cm, height=30cm, 90° spacing) to compute
analytically exact camera-to-base_link transforms.
```bash
ros2 run saltybot_calibration calibrate_extrinsics.py \
--mode mechanical \
--mount-radius 0.05 \
--mount-height 0.30 \
--output /mnt/nvme/saltybot/calibration/extrinsics.yaml \
--generate-launch \
~/ros2_ws/src/saltybot_bringup/launch/static_transforms.launch.py
```
If mechanical dimensions differ from defaults, update `--mount-radius` and
`--mount-height` from your actual measurements.
## Step 7 — Generate Static Transform Launch File
(Already done by --generate-launch in Step 6, but can regenerate independently:)
```bash
ros2 run saltybot_calibration generate_static_transforms.py \
--extrinsics /mnt/nvme/saltybot/calibration/extrinsics.yaml \
--output ~/ros2_ws/src/saltybot_bringup/launch/static_transforms.launch.py
```
## Step 8 — Update surround_vision_params.yaml
Edit `jetson/config/surround_vision_params.yaml` (or the equivalent in your
saltybot_surround package) to point camera_info_url at the new calibration files:
```yaml
camera_info_urls:
front: 'file:///mnt/nvme/saltybot/calibration/front/camera_info.yaml'
right: 'file:///mnt/nvme/saltybot/calibration/right/camera_info.yaml'
rear: 'file:///mnt/nvme/saltybot/calibration/rear/camera_info.yaml'
left: 'file:///mnt/nvme/saltybot/calibration/left/camera_info.yaml'
```
## Step 9 — Restart Nodes
```bash
# Restart camera and surround nodes to pick up new calibration
ros2 lifecycle set /saltybot_cameras shutdown
ros2 launch saltybot_bringup cameras.launch.py
ros2 launch saltybot_surround surround.launch.py
```
## Output Files
After full calibration, you should have:
```
/mnt/nvme/saltybot/calibration/
front/camera_info.yaml # intrinsics: K, D (equidistant)
right/camera_info.yaml
rear/camera_info.yaml
left/camera_info.yaml
extrinsics.yaml # base_link -> camera_*_optical_frame
```
Template files are in `jetson/calibration/` (tracked in git) with placeholder
IMX219 values (fx=229px @ 640×480). Replace with calibrated values.
## Distortion Model Reference
The IMX219 160° fisheye uses the **equidistant** (Kannala-Brandt) model:
```
r(theta) = k1*theta + k2*theta^3 + k3*theta^5 + k4*theta^7
```
where theta is the angle of incidence. OpenCV's `cv2.fisheye` module implements
this model with 4 coefficients [k1, k2, k3, k4].
This is different from the `plumb_bob` (radial/tangential) model used for
standard lenses — do not mix them up.