鱼眼相机标定实战:从Kannala-Brandt模型到OpenCV C++高效实现
在机器人、自动驾驶和增强现实等领域,鱼眼相机凭借其超广视野成为感知系统的关键传感器。然而,其独特的成像模型也给开发者带来了标定上的挑战。传统的针孔模型标定方法在这里完全失效,而Kannala-Brandt模型则为我们提供了一套优雅的数学框架来精确描述鱼眼镜头的畸变特性。本文将带你深入理解这一模型,并手把手教你使用OpenCV的fisheye模块,在C++环境中快速、准确地完成鱼眼相机标定。
1. 鱼眼相机模型:超越针孔的几何世界
鱼眼镜头的光学设计与传统镜头有本质区别。它不是为了产生“横平竖直”的透视图像,而是为了在有限的传感器尺寸上捕捉尽可能宽广的视野。这种设计导致了严重的径向畸变——图像边缘的直线会弯曲成弧形。
1.1 为什么需要专门的鱼眼模型?
传统针孔相机模型假设图像点与空间点之间存在线性关系,这在小视野镜头中近似成立。但当视野超过120度时,这种近似就完全崩溃了。鱼眼镜头通常有160度甚至更大的视野,必须使用更复杂的投影模型。
Kannala-Brandt模型的核心思想是等距投影:图像平面上点到图像中心的距离与入射角(光线与光轴的夹角)成正比。这与针孔模型的r = f * tan(θ)形成鲜明对比。
| 投影类型 | 数学关系 | 适用视野范围 | 特点 |
|---|---|---|---|
| 针孔投影 | r = f * tan(θ) | < 120° | 直线保持直线,但视野有限 |
| 等距投影 | r = f * θ | ≤ 180° | 保持角度线性关系,适合鱼眼 |
| 等立体角投影 | r = 2f * sin(θ/2) | ≤ 180° | 保持立体角比例 |
| 正交投影 | r = f * sin(θ) | ≤ 180° | 保持正交性 |
关键洞察:Kannala-Brandt模型实际上是一个多项式模型,它用奇次多项式来近似真实的鱼眼投影函数,而不仅仅是简单的
r = f * θ关系。
1.2 Kannala-Brandt模型的数学表达
Kannala-Brandt模型将三维点投影到图像平面的过程分为几个步骤:
- 归一化坐标计算:将相机坐标系下的点投影到单位球面
- 角度计算:计算入射角θ和方位角φ
- 多项式畸变:用奇次多项式描述实际的投影关系
- 像素坐标转换:应用内参矩阵得到最终像素坐标
具体的投影公式如下:
// 伪代码表示Kannala-Brandt投影过程
Point3d P_c = R * P_w + t; // 世界坐标转相机坐标
double theta = atan2(sqrt(P_c.x*P_c.x + P_c.y*P_c.y), P_c.z);
double phi = atan2(P_c.y, P_c.x);
// 多项式畸变:r_d = theta * (1 + k1*theta^2 + k2*theta^4 + k3*theta^6 + k4*theta^8)
double theta_d = theta * (1 + k1*pow(theta,2) + k2*pow(theta,4)
+ k3*pow(theta,6) + k4*pow(theta,8));
// 归一化平面坐标
double x_d = theta_d * cos(phi) / theta;
double y_d = theta_d * sin(phi) / theta;
// 像素坐标
double u = f_x * x_d + c_x;
double v = f_y * y_d + c_y;
这个模型需要标定的参数包括:
- 内参矩阵K:
[f_x, 0, c_x; 0, f_y, c_y; 0, 0, 1] - 畸变系数D:
[k1, k2, k3, k4](有时还包括k5) - 外参:每张标定图像的旋转矩阵R和平移向量t
2. 标定准备:从棋盘格到代码环境
2.1 标定板的选择与制作
鱼眼相机标定最常用的仍然是棋盘格标定板,但有几个特殊注意事项:
- 棋盘格尺寸要足够大:由于鱼眼图像边缘畸变严重,棋盘格角点必须清晰可辨
- 建议使用非对称棋盘格:OpenCV支持非对称圆网格,这有助于消除标定图像的歧义
- 打印质量要高:模糊的角点会严重影响标定精度
我个人的经验是,对于典型的160度鱼眼镜头,棋盘格至少应占据图像中心区域的1/3。可以使用以下Python代码生成标定板:
import cv2
import numpy as np
# 生成非对称圆网格标定板
pattern_size = (7, 10) # 内部角点数量(列,行)
square_size = 0.025 # 每个方格的实际尺寸(米)
# 创建标定板图像
board = np.zeros((1080, 1920), dtype=np.uint8)
board[:] = 255
# 绘制棋盘格
for i in range(pattern_size[1]):
for j in range(pattern_size[0]):
if (i + j) % 2 == 0:
x_start = j * 100 + 100
y_start = i * 100 + 100
board[y_start:y_start+80, x_start:x_start+80] = 0
cv2.imwrite("calibration_board.png", board)
2.2 图像采集的最佳实践
采集标定图像是标定成功的关键。以下是我在实际项目中总结的采集要点:
- 多角度覆盖:确保棋盘格出现在图像的不同区域,特别是边缘
- 不同距离:从近到远拍摄,覆盖不同的深度范围
- 不同倾斜角度:让棋盘格以各种角度出现在画面中
- 足够的数量:通常需要15-30张高质量图像
- 避免运动模糊:使用三脚架或确保相机稳定
经验分享:我发现最有效的采集策略是"中心-边缘-倾斜"三部曲。先拍几张棋盘格在图像正中央的,然后分别让棋盘格出现在四个角落,最后拍摄一些棋盘格有明显倾斜角度的图像。
2.3 开发环境配置
对于C++开发环境,你需要确保OpenCV已正确安装并配置了fisheye模块:
# Ubuntu/Debian系统安装
sudo apt-get update
sudo apt-get install libopencv-dev
# 或者从源码编译(推荐,确保包含contrib模块)
git clone https://github.com/opencv/opencv.git
git clone https://github.com/opencv/opencv_contrib.git
cd opencv
mkdir build && cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
-D WITH_GTK=ON \
-D BUILD_EXAMPLES=OFF ..
make -j$(nproc)
sudo make install
CMakeLists.txt的基本配置:
cmake_minimum_required(VERSION 3.10)
project(fisheye_calibration)
set(CMAKE_CXX_STANDARD 11)
find_package(OpenCV REQUIRED)
add_executable(fisheye_calibration main.cpp)
target_link_libraries(fi

&spm=1001.2101.3001.5002&articleId=153544300&d=1&t=3&u=75fa8f6d2f8541fdbaae6bf10214f0d0)
355

被折叠的 条评论
为什么被折叠?



