相机标定与3D重建技术通俗讲解
一、什么是相机标定?能解决什么问题?
相机标定是计算机视觉中的基础技术,简单来说,就是确定相机从3D世界拍摄到2D图像时的"转换规则"。具体解决两个核心问题:
- 相机内部属性:如焦距(决定图像缩放)、主点位置(图像中心偏移)
- 镜头畸变:真实镜头会导致图像变形,比如手机拍摄的广角照片边缘会出现桶形畸变
二、针孔相机模型:3D到2D的投影原理
想象相机是一个暗箱,光线通过小孔在底片上成像,这就是针孔模型的直观理解。数学上,这个过程通过两个矩阵完成:
-
内参矩阵(Camera Intrinsics):相机的"固有属性"
- 形式: [ f x 0 c x 0 f y c y 0 0 1 ] \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} fx000fy0cxcy1
- 含义: f x , f y f_x, f_y fx,fy是焦距(单位为像素), c x , c y c_x, c_y cx,cy是主点坐标(理想情况下是图像中心)
-
外参矩阵(Camera Extrinsics):相机的"位置与朝向"
- 由旋转矩阵 R R R(3×3)和平移向量 t t t(3×1)组成
- 作用:将3D世界坐标转换为相机坐标
-
投影过程:
- 3D点 P w P_w Pw→相机坐标 P c P_c Pc→图像像素点 p p p
- 公式: s ⋅ p = A [ R ∣ t ] P w s \cdot p = A[R|t]P_w s⋅p=A[R∣t]Pw,其中 s s s是缩放因子
三、镜头畸变:为什么照片会变形?
真实镜头存在多种畸变,OpenCV支持以下模型:
-
径向畸变:越靠近图像边缘变形越明显
- 桶形畸变(如鱼眼镜头):线条向外弯曲
- 枕形畸变(如长焦镜头):线条向内收缩
- 参数: k 1 , k 2 , k 3 , k 4 , k 5 , k 6 k_1, k_2, k_3, k_4, k_5, k_6 k1,k2,k3,k4,k5,k6
-
切向畸变:镜头安装倾斜导致的变形
- 参数: p 1 , p 2 p_1, p_2 p1,p2
-
薄棱镜畸变:更复杂的光学误差
- 参数: s 1 , s 2 , s 3 , s 4 s_1, s_2, s_3, s_4 s1,s2,s3,s4
四、3D重建:从多张照片到立体模型
3D重建的核心逻辑:
- 单目相机:通过运动恢复结构(SfM),利用相机在不同位置拍摄的图像计算深度
- 双目相机:利用左右眼视差计算深度(类似人眼感知距离)
- 关键技术:
- 立体校正:让左右相机的光轴平行,便于计算视差
- 三角测量:通过同一物体在不同视角的投影计算3D坐标
五、Python实战:相机标定与图像校正
下面通过一个完整的Python示例,展示如何使用OpenCV进行相机标定和图像畸变校正:
import numpy as np
import cv2
import glob
import matplotlib.pyplot as plt# 1. 准备标定数据:拍摄棋盘格多角度图像
# 假设已拍摄一组棋盘格图像并保存到calibration_images文件夹
images = glob.glob('calibration_images/*.jpg')# 2. 定义棋盘格尺寸(内角点行列数)
pattern_size = (9, 6) # 9列6行的内角点# 3. 存储3D点(世界坐标,假设棋盘格在Z=0平面)
obj_points = [] # 3D点
img_points = [] # 2D图像点# 创建3D点模板(棋盘格每个角点的世界坐标)
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)# 4. 遍历图像,检测棋盘格角点
for fname in images:img = cv2.imread(fname)gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 检测棋盘格角点ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)if ret:obj_points.append(objp)img_points.append(corners)# 绘制角点并显示(可选)cv2.drawChessboardCorners(img, pattern_size, corners, ret)cv2.imshow('Corners Detected', img)cv2.waitKey(500)cv2.destroyAllWindows()# 5. 执行相机标定
img_size = (gray.shape[1], gray.shape[0])
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, img_size, None, None)# 6. 打印标定结果
print("内参矩阵:\n", mtx)
print("畸变参数:\n", dist)# 7. 畸变校正:读取一张图像进行验证
img = cv2.imread(images[0])
h, w = img.shape[:2]# 获取校正映射
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5)# 应用映射进行校正
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)# 裁剪校正后的图像(去除黑边)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]# 8. 显示原始图像与校正后图像对比
plt.figure(figsize=(12, 6))
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('原始图像'), plt.axis('off')
plt.subplot(122), plt.imshow(cv2.cvtColor(dst, cv2.COLOR_BGR2RGB))
plt.title('校正后图像'), plt.axis('off')
plt.tight_layout()
plt.show()# 9. 保存标定结果(便于后续使用)
np.savez('camera_calibration.npz', mtx=mtx, dist=dist)
六、代码解析:标定流程拆解
- 数据准备:拍摄至少10张不同角度的棋盘格图像,越多越准确
- 角点检测:
findChessboardCorners
函数自动识别棋盘格内角点 - 相机标定:
calibrateCamera
函数计算内参与畸变参数- 内参矩阵包含焦距和主点
- 畸变参数包含径向和切向畸变系数
- 图像校正:
getOptimalNewCameraMatrix
计算最优校正矩阵initUndistortRectifyMap
生成映射表remap
应用映射完成图像校正
七、扩展应用:双目相机立体视觉
如果需要3D重建,可以扩展到双目相机标定:
# 双目相机标定示例(简化流程)
ret, mtx1, dist1, mtx2, dist2, R, T, E, F = cv2.stereoCalibrate(obj_points, img_points1, img_points2, mtx1, dist1, mtx2, dist2, img_size)# 立体校正
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(mtx1, dist1, mtx2, dist2, img_size, R, T, flags=cv2.CALIB_ZERO_DISPARITY)# 生成校正映射
mapx1, mapy1 = cv2.initUndistortRectifyMap(mtx1, dist1, R1, P1, img_size, 5)
mapx2, mapy2 = cv2.initUndistortRectifyMap(mtx2, dist2, R2, P2, img_size, 5)
八、常见问题与注意事项
-
标定精度:
- 棋盘格尺寸应已知(如每个格子30mm×30mm)
- 图像应覆盖相机的全视野范围
- 拍摄角度应多样化(包括倾斜、远近)
-
畸变校正效果:
- 校正后图像边缘可能出现黑边,需裁剪(代码中已处理)
- 鱼眼镜头需使用专门的
fisheye
模块
-
3D重建基础:
- 单目重建存在尺度不确定性(需额外信息)
- 双目重建精度取决于基线长度(两相机距离越远,深度越准确)
通过上述技术,相机标定为计算机视觉应用奠定了基础,从自动驾驶的环境感知到AR游戏的虚实融合,再到工业质检的尺寸测量,都离不开精准的相机标定技术。OpenCV的calib3d模块主要用于相机标定和三维重建,是计算机视觉中处理3D空间与2D图像映射关系的核心工具。这个模块提供了从基础几何变换到复杂场景重建的一系列功能。
核心概念
-
相机标定
相机标定是确定相机内部参数(如焦距、主点)和外部参数(位置、姿态)的过程。真实世界中的3D点通过相机投影到2D图像上时会产生畸变(如径向畸变、切向畸变),标定可以校正这些畸变。 -
坐标系统
- 世界坐标系:真实世界中的3D坐标。
- 相机坐标系:以相机为原点的3D坐标。
- 图像坐标系:2D像素坐标。
-
基础矩阵(Fundamental Matrix)与本质矩阵(Essential Matrix)
- 本质矩阵(E):描述两个相机坐标系之间的关系,包含旋转和平移信息。
- 基础矩阵(F):描述两个图像平面上点的对应关系,是本质矩阵的扩展,包含相机内参。
关键函数
-
相机标定
cv2.findChessboardCorners()
:检测棋盘格角点。cv2.calibrateCamera()
:计算相机内参和畸变系数。cv2.undistort()
:校正图像畸变。
-
立体视觉
cv2.stereoCalibrate()
:双目相机联合标定。cv2.stereoRectify()
:计算校正变换矩阵。cv2.StereoBM_create()
/cv2.StereoSGBM_create()
:计算视差图。
-
姿态估计
cv2.solvePnP()
:已知3D点和对应2D点,求解相机位姿。cv2.findEssentialMat()
/cv2.findFundamentalMat()
:计算本质矩阵和基础矩阵。
Python示例:相机标定与畸变校正
下面是一个使用棋盘格进行相机标定的完整示例:
import cv2
import numpy as np
import glob# 设置棋盘格参数
pattern_size = (9, 6) # 棋盘格内角点数(横向和纵向)
square_size = 25.0 # 棋盘格方块的实际尺寸(毫米)# 准备对象点,如 (0,0,0), (1,0,0), (2,0,0) ..., (8,5,0)
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) * square_size# 存储对象点和图像点的数组
objpoints = [] # 3D点(世界坐标系)
imgpoints = [] # 2D点(图像平面)# 获取所有棋盘格图像
images = glob.glob('calibration_images/*.jpg')for fname in images:img = cv2.imread(fname)gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 查找棋盘格角点ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)# 如果找到,添加对象点和图像点if ret:objpoints.append(objp)# 亚像素级角点检测,提高精度corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1),(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))imgpoints.append(corners2)# 绘制并显示角点cv2.drawChessboardCorners(img, pattern_size, corners2, ret)cv2.imshow('img', img)cv2.waitKey(500)cv2.destroyAllWindows()# 相机标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)# 打印标定结果
print("相机内参矩阵:")
print(mtx)
print("\n畸变系数:")
print(dist)# 畸变校正示例
img = cv2.imread('calibration_images/left01.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))# 方法1:使用initUndistortRectifyMap和remap
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, np.eye(3), newcameramtx, (w, h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)# 方法2:直接使用undistort
dst2 = cv2.undistort(img, mtx, dist, None, newcameramtx)# 裁剪图像
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Undistorted', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()# 计算重投影误差(评估标定质量)
mean_error = 0
for i in range(len(objpoints)):imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)mean_error += error
print(f"\n总重投影误差: {mean_error/len(objpoints)}")
示例:双目相机立体匹配
下面是一个计算视差图的示例:
import cv2
import numpy as np# 读取双目相机图像
imgL = cv2.imread('left_image.jpg', 0)
imgR = cv2.imread('right_image.jpg', 0)# 初始化立体匹配器(StereoBM)
stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)# 计算视差图
disparity = stereo.compute(imgL, imgR)# 归一化视差图以便显示
disparity_normalized = cv2.normalize(disparity, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)# 显示结果
cv2.imshow('Left Image', imgL)
cv2.imshow('Right Image', imgR)
cv2.imshow('Disparity Map', disparity_normalized)
cv2.waitKey(0)
cv2.destroyAllWindows()
应用场景
- 机器人导航:通过立体视觉计算深度信息。
- 增强现实(AR):将虚拟物体准确叠加到真实场景中。
- 3D建模:从多角度图像重建物体的三维模型。
- 自动驾驶:检测障碍物和估计距离。
通过掌握calib3d模块,你可以解决计算机视觉中许多与3D空间相关的复杂问题。