4. 鱼眼相机标定实战:OpenCV fisheye模块、标定板制作、采集标定图片、运行标定脚本、重投影误差分析
鱼眼相机标定,说白了就是给镜头“算命”。你得知道它把世界扭曲成了什么样,才能在算法里把它掰回来。我最早接触鱼眼标定时,踩过不少坑——标定板打印歪了、图片拍糊了、参数调错了,结果重投影误差大得离谱。今天咱们就把这整套流程捋一遍,从做板子到看误差,一步不落。
核心流程速览
制作标定板 → 采集多角度图片 → 运行OpenCV fisheye标定 → 分析重投影误差 → 保存内参/畸变系数
4.1 标定板制作——别在这步省事
标定板是标定的“尺子”。尺子不准,后面全白搭。我个人习惯用棋盘格,因为OpenCV的角点检测对棋盘格支持最好。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 棋盘格尺寸 | 9×6 或 11×8(内角点) | 内角点 = 格子数 - 1 |
| 方格边长 | 20mm ~ 30mm | 太小了角点检测不准,太大了视野装不下 |
| 打印介质 | 哑光相纸或PVC板 | 反光材料会导致角点检测失败 |
| 底板 | 硬质平板(亚克力/铝板) | 软纸板会弯曲,引入额外误差 |
我曾经踩过的坑:用普通A4纸打印棋盘格,贴在纸箱上就开拍。结果标定出来的内参每次都不一样,重投影误差0.8像素以上。后来换成3mm亚克力板贴哑光相纸,误差直接降到0.3像素。标定板一定要平!
4.2 采集标定图片——角度比数量重要
很多人以为拍个十几张就够了。其实不然。鱼眼镜头视场角大(通常≥180°),边缘畸变严重。你需要让标定板出现在画面的各个位置——中心、左上、右下、边缘、角落,还要有不同倾斜角度。
我建议至少拍20-30张,遵循以下原则:
- 覆盖全视野:标定板要出现在画面的9个区域(上中下×左中右)
- 多角度倾斜:前后倾斜30°、左右倾斜30°,模拟实际安装姿态
- 距离变化:从0.5米到2米,不同距离各拍几张
- 光照均匀:避免强光直射标定板,否则角点检测会失败
- 标定板占画面1/3以上:太小了角点提取精度不够
小技巧:采集时让标定板稍微旋转,让棋盘格线条与图像边缘成15°-45°角。这样角点检测的亚像素精度更高。纯水平或垂直摆放反而不好。
4.3 OpenCV fisheye模块——为什么用它?
OpenCV有两个标定模块:普通的cv::calibrateCamera和cv::fisheye::calibrate。普通模型用针孔模型+径向切向畸变,对鱼眼镜头(尤其是视场角>120°)拟合效果很差。fisheye模块用的是Scaramuzza的等距投影模型,专门为大视场角设计。
说白了,普通模型是给手机镜头用的,鱼眼模型才是给环视摄像头用的。我见过有人用普通模型标鱼眼,结果边缘畸变校正后图像都扭曲成S形了。
4.4 运行标定脚本——手把手来一遍
下面是我常用的标定脚本框架。注意,这里用的是C++版本,Python版本逻辑完全一样。
#include <opencv2/opencv.hpp>
#include <opencv2/fisheye.hpp>
#include <iostream>
#include <vector>
int main() {
// 1. 读取图片列表
std::vector<std::string> imgPaths = {...}; // 你的图片路径
cv::Size boardSize(9, 6); // 内角点数量
float squareSize = 25.0f; // 方格边长,单位mm
// 2. 准备世界坐标系点
std::vector<cv::Point3f> objp;
for(int i = 0; i < boardSize.height; i++)
for(int j = 0; j < boardSize.width; j++)
objp.push_back(cv::Point3f(j*squareSize, i*squareSize, 0));
std::vector<std::vector<cv::Point3f>> objPoints;
std::vector<std::vector<cv::Point2f>> imgPoints;
// 3. 提取角点
cv::Mat gray;
for(auto& path : imgPaths) {
cv::Mat img = cv::imread(path);
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
std::vector<cv::Point2f> corners;
bool found = cv::findChessboardCorners(gray, boardSize, corners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if(found) {
cv::cornerSubPix(gray, corners, cv::Size(5,5), cv::Size(-1,-1),
cv::TermCriteria(cv::TermCriteria::EPS+cv::TermCriteria::COUNT, 30, 0.1));
objPoints.push_back(objp);
imgPoints.push_back(corners);
}
}
// 4. 执行鱼眼标定
cv::Mat K, D; // 内参矩阵和畸变系数
std::vector<cv::Mat> rvecs, tvecs;
int flags = cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC |
cv::fisheye::CALIB_CHECK_COND |
cv::fisheye::CALIB_FIX_SKEW;
double rms = cv::fisheye::calibrate(objPoints, imgPoints,
gray.size(), K, D, rvecs, tvecs, flags,
cv::TermCriteria(cv::TermCriteria::EPS+cv::TermCriteria::COUNT, 100, 1e-5));
// 5. 输出结果
std::cout << "重投影误差: " << rms << " 像素" << std::endl;
std::cout << "内参矩阵 K:\n" << K << std::endl;
std::cout << "畸变系数 D:\n" << D << std::endl;
return 0;
}
关键参数说明:
CALIB_FIX_SKEW:固定skew参数为0,大多数鱼眼镜头满足这个假设CALIB_CHECK_COND:检查矩阵条件数,防止标定结果不稳定CALIB_RECOMPUTE_EXTRINSIC:每次迭代重新计算外参,提高精度
4.5 重投影误差分析——标定质量的照妖镜
重投影误差,就是把标定板上的3D点,用标定出来的内参和外参投影回图像,算算投影点和实际检测到的角点差了多少像素。这个值越小,说明标定越准。
| 误差范围 | 标定质量 | 建议 |
|---|---|---|
| < 0.3 像素 | 优秀 | 可以直接用于环视拼接 |
| 0.3 ~ 0.6 像素 | 良好 | 可以接受,但建议检查是否有异常图片 |
| 0.6 ~ 1.0 像素 | 及格 | 需要排查标定板或采集问题 |
| > 1.0 像素 | 不合格 | 重新采集,检查标定板是否平整 |
为什么会误差大?我总结了几种常见情况:
- 标定板弯曲:这是最常见的原因。用硬质底板能解决。
- 角点检测不准:光照太强或太暗,导致角点亚像素提取偏移。
- 图片数量不够:少于15张,参数拟合不稳定。
- 角度覆盖不全:所有图片标定板都在画面中心,边缘畸变没被约束。
我的排查习惯:标定完成后,我会逐张图片画出重投影误差的分布图。如果某张图片的误差明显大于其他图片(比如0.8像素 vs 平均0.3像素),就把这张图剔除,重新标定。通常剔除2-3张异常图片,整体误差就能降下来。
4.6 保存标定结果——为后续环节做准备
标定完成后,需要保存两个核心数据:内参矩阵K和畸变系数D。在环视系统中,这两个数据会被用于后续的畸变校正和投影变换。
// 保存为YAML文件
cv::FileStorage fs("calib_result.yaml", cv::FileStorage::WRITE);
fs << "image_width" << gray.cols;
fs << "image_height" << gray.rows;
fs << "camera_matrix" << K;
fs << "distortion_coefficients" << D;
fs.release();
保存的YAML文件内容大概长这样:
%YAML:1.0
image_width: 1280
image_height: 720
camera_matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 3.2e2, 0., 6.4e2, 0., 3.2e2, 3.6e2, 0., 0., 1. ]
distortion_coefficients: !!opencv-matrix
rows: 4
cols: 1
dt: d
data: [ -0.02, 0.01, -0.005, 0.001 ]
注意:鱼眼畸变系数D是4个参数(k1, k2, k3, k4),对应OpenCV fisheye模型的等距投影畸变。普通针孔模型的畸变系数是5个参数(k1, k2, p1, p2, k3),两者不能混用。保存时一定要标注清楚用的是哪个模型。