相机标定与 3D 重建
相机标定是计算机视觉中的重要步骤,用于确定相机的内部参数和外部参数。本章节介绍如何使用 OpenCV 进行相机标定和简单的 3D 重建。
相机模型
针孔相机模型
针孔相机模型描述了 3D 世界坐标到 2D 图像坐标的映射关系。这个映射由相机内参矩阵描述:
其中:
- 是图像坐标
- 是相机坐标系下的 3D 坐标
- 是焦距(以像素为单位)
- 是主点坐标
畸变模型
实际相机镜头会产生畸变,主要包括径向畸变和切向畸变:
径向畸变:由镜头形状引起,表现为图像边缘的弯曲。
切向畸变:由镜头安装不平行引起。
棋盘格标定
棋盘格是最常用的标定工具,OpenCV 提供了完整的标定流程。
棋盘格检测
import cv2
import numpy as np
import glob
# 棋盘格参数
chessboard_size = (9, 6) # 内角点数量
square_size = 25 # 棋盘格方格大小(毫米)
# 准备物体点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= 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, chessboard_size, None)
if ret:
objpoints.append(objp)
# 亚像素角点精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners_refined)
# 绘制角点
cv2.drawChessboardCorners(img, chessboard_size, corners_refined, ret)
cv2.imshow('角点检测', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
计算相机参数
import cv2
import numpy as np
# 假设已经收集了 objpoints 和 imgpoints
# objpoints: 3D 点列表
# imgpoints: 2D 点列表
# 获取图像尺寸
image = cv2.imread('calibration_images/image1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image_size = gray.shape[::-1]
# 相机标定
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, image_size, None, None
)
print("标定结果:", "成功" if ret else "失败")
print("\n相机内参矩阵:")
print(camera_matrix)
print("\n畸变系数:")
print(dist_coeffs)
# 保存标定结果
np.savez('calibration_data.npz',
camera_matrix=camera_matrix,
dist_coeffs=dist_coeffs,
rvecs=rvecs,
tvecs=tvecs)
图像去畸变
import cv2
import numpy as np
# 加载标定数据
data = np.load('calibration_data.npz')
camera_matrix = data['camera_matrix']
dist_coeffs = data['dist_coeffs']
# 读取图像
image = cv2.imread('distorted_image.jpg')
h, w = image.shape[:2]
# 方法1:使用 undistort 函数
undistorted = cv2.undistort(image, camera_matrix, dist_coeffs)
# 方法2:先计算映射,再重映射(适合多张图像)
new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(
camera_matrix, dist_coeffs, (w, h), 1, (w, h)
)
mapx, mapy = cv2.initUndistortRectifyMap(
camera_matrix, dist_coeffs, None, new_camera_matrix, (w, h), cv2.CV_16SC2
)
undistorted = cv2.remap(image, mapx, mapy, cv2.INTER_LINEAR)
# 裁剪有效区域
x, y, w, h = roi
undistorted = undistorted[y:y+h, x:x+w]
cv2.imshow('原图', image)
cv2.imshow('去畸变', undistorted)
cv2.waitKey(0)
评估标定质量
import cv2
import numpy as np
# 加载标定数据
data = np.load('calibration_data.npz')
camera_matrix = data['camera_matrix']
dist_coeffs = data['dist_coeffs']
rvecs = data['rvecs']
tvecs = data['tvecs']
# 计算重投影误差
total_error = 0
for i in range(len(objpoints)):
# 投影 3D 点到图像平面
imgpoints_proj, _ = cv2.projectPoints(
objpoints[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs
)
# 计算误差
error = cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj)
total_error += error
mean_error = total_error / len(objpoints)
print(f"平均重投影误差: {mean_error:.4f} 像素")
重投影误差越小,标定质量越好。通常误差小于 0.5 像素被认为是好的标定结果。
姿态估计
姿态估计是确定物体在 3D 空间中的位置和方向。
单目标姿态估计
import cv2
import numpy as np
# 相机参数
camera_matrix = np.array([
[800, 0, 320],
[0, 800, 240],
[0, 0, 1]
], dtype=np.float32)
dist_coeffs = np.zeros((5, 1))
# 定义物体的 3D 点(例如一个立方体)
object_points = np.array([
[0, 0, 0],
[1, 0, 0],
[1, 1, 0],
[0, 1, 0],
[0, 0, 1],
[1, 0, 1],
[1, 1, 1],
[0, 1, 1]
], dtype=np.float32) * 100 # 缩放到合适大小
# 图像中对应的 2D 点
image_points = np.array([
[200, 300],
[350, 300],
[350, 200],
[200, 200],
[220, 280],
[370, 280],
[370, 180],
[220, 180]
], dtype=np.float32)
# 求解 PnP 问题
success, rvec, tvec = cv2.solvePnP(object_points, image_points, camera_matrix, dist_coeffs)
if success:
print("旋转向量:", rvec.flatten())
print("平移向量:", tvec.flatten())
# 将旋转向量转换为旋转矩阵
rotation_matrix, _ = cv2.Rodrigues(rvec)
print("\n旋转矩阵:")
print(rotation_matrix)
增强现实示例
import cv2
import numpy as np
# 相机参数
camera_matrix = np.array([
[800, 0, 320],
[0, 800, 240],
[0, 0, 1]
], dtype=np.float32)
dist_coeffs = np.zeros((5, 1))
# 定义棋盘格参数
chessboard_size = (9, 6)
square_size = 25
object_points = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
object_points[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
object_points *= square_size
# 定义要绘制的 3D 坐标轴
axis = np.float32([[50, 0, 0], [0, 50, 0], [0, 0, 50], [0, 0, 0]]).reshape(-1, 3)
# 定义要绘制的立方体
cube = np.float32([
[0, 0, 0], [0, 50, 0], [50, 50, 0], [50, 0, 0],
[0, 0, 50], [0, 50, 50], [50, 50, 50], [50, 0, 50]
])
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 查找棋盘格
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret:
# 亚像素精确化
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1),
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
# 求解姿态
ret, rvec, tvec = cv2.solvePnP(object_points, corners, camera_matrix, dist_coeffs)
if ret:
# 投影 3D 点到图像
imgpts, _ = cv2.projectPoints(axis, rvec, tvec, camera_matrix, dist_coeffs)
# 绘制坐标轴
origin = tuple(imgpts[3].ravel().astype(int))
frame = cv2.line(frame, origin, tuple(imgpts[0].ravel().astype(int)), (0, 0, 255), 3)
frame = cv2.line(frame, origin, tuple(imgpts[1].ravel().astype(int)), (0, 255, 0), 3)
frame = cv2.line(frame, origin, tuple(imgpts[2].ravel().astype(int)), (255, 0, 0), 3)
# 投影立方体
cube_pts, _ = cv2.projectPoints(cube, rvec, tvec, camera_matrix, dist_coeffs)
cube_pts = np.int32(cube_pts).reshape(-1, 2)
# 绘制立方体底面
frame = cv2.drawContours(frame, [cube_pts[:4]], -1, (0, 255, 0), 3)
# 绘制立方体顶面
frame = cv2.drawContours(frame, [cube_pts[4:]], -1, (0, 255, 0), 3)
# 绘制侧边
for i in range(4):
frame = cv2.line(frame, tuple(cube_pts[i]), tuple(cube_pts[i + 4]), (0, 255, 0), 3)
cv2.imshow('AR', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
立体视觉
立体视觉使用两个相机来获取深度信息。
立体相机标定
import cv2
import numpy as np
import glob
# 棋盘格参数
chessboard_size = (9, 6)
square_size = 25
# 准备物体点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size
# 存储点
objpoints = []
imgpoints_left = []
imgpoints_right = []
# 读取图像对
left_images = sorted(glob.glob('left/*.jpg'))
right_images = sorted(glob.glob('right/*.jpg'))
for left_path, right_path in zip(left_images, right_images):
left_img = cv2.imread(left_path)
right_img = cv2.imread(right_path)
left_gray = cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY)
right_gray = cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY)
# 查找棋盘格
ret_left, corners_left = cv2.findChessboardCorners(left_gray, chessboard_size, None)
ret_right, corners_right = cv2.findChessboardCorners(right_gray, chessboard_size, None)
if ret_left and ret_right:
objpoints.append(objp)
# 亚像素精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_left = cv2.cornerSubPix(left_gray, corners_left, (11, 11), (-1, -1), criteria)
corners_right = cv2.cornerSubPix(right_gray, corners_right, (11, 11), (-1, -1), criteria)
imgpoints_left.append(corners_left)
imgpoints_right.append(corners_right)
# 单独标定每个相机
image_size = left_gray.shape[::-1]
ret_left, mtx_left, dist_left, rvecs_left, tvecs_left = cv2.calibrateCamera(
objpoints, imgpoints_left, image_size, None, None
)
ret_right, mtx_right, dist_right, rvecs_right, tvecs_right = cv2.calibrateCamera(
objpoints, imgpoints_right, image_size, None, None
)
# 立体标定
flags = cv2.CALIB_FIX_INTRINSIC
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6)
ret, mtx_left, dist_left, mtx_right, dist_right, R, T, E, F = cv2.stereoCalibrate(
objpoints, imgpoints_left, imgpoints_right,
mtx_left, dist_left, mtx_right, dist_right,
image_size, criteria=criteria, flags=flags
)
print("立体标定结果:")
print("旋转矩阵 R:")
print(R)
print("\n平移向量 T:")
print(T)
立体校正
import cv2
import numpy as np
# 加载标定数据
# mtx_left, dist_left, mtx_right, dist_right, R, T
# 立体校正
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
mtx_left, dist_left, mtx_right, dist_right,
image_size, R, T, alpha=0
)
# 计算映射
map1_left, map2_left = cv2.initUndistortRectifyMap(
mtx_left, dist_left, R1, P1, image_size, cv2.CV_16SC2
)
map1_right, map2_right = cv2.initUndistortRectifyMap(
mtx_right, dist_right, R2, P2, image_size, cv2.CV_16SC2
)
# 校正图像
left_img = cv2.imread('left_image.jpg')
right_img = cv2.imread('right_image.jpg')
left_rectified = cv2.remap(left_img, map1_left, map2_left, cv2.INTER_LINEAR)
right_rectified = cv2.remap(right_img, map1_right, map2_right, cv2.INTER_LINEAR)
# 显示校正后的图像
cv2.imshow('左图校正', left_rectified)
cv2.imshow('右图校正', right_rectified)
cv2.waitKey(0)
视差图计算
import cv2
import numpy as np
# 读取校正后的图像对
left = cv2.imread('left_rectified.jpg', cv2.IMREAD_GRAYSCALE)
right = cv2.imread('right_rectified.jpg', cv2.IMREAD_GRAYSCALE)
# 创建立体匹配器
stereo = cv2.StereoBM_create(numDisparities=16*10, blockSize=15)
# 计算视差图
disparity = stereo.compute(left, right)
# 归一化显示
disparity_normalized = cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX)
disparity_normalized = np.uint8(disparity_normalized)
cv2.imshow('视差图', disparity_normalized)
cv2.waitKey(0)
# 使用 SGBM(效果更好)
stereo_sgbm = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=16*10,
blockSize=5,
P1=8 * 3 * 5**2,
P2=32 * 3 * 5**2,
disp12MaxDiff=1,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32
)
disparity_sgbm = stereo_sgbm.compute(left, right).astype(np.float32) / 16.0
cv2.imshow('SGBM 视差图', disparity_sgbm / disparity_sgbm.max())
cv2.waitKey(0)
深度图计算
import cv2
import numpy as np
# 假设已经有视差图和 Q 矩阵
# Q 是立体校正时得到的重投影矩阵
# 计算 3D 点云
points = cv2.reprojectImageTo3D(disparity_sgbm, Q)
# 获取深度图
depth = points[:, :, 2]
# 过滤无效值
mask = disparity_sgbm > disparity_sgbm.min()
depth_filtered = np.where(mask, depth, 0)
# 显示深度图
depth_normalized = cv2.normalize(depth_filtered, None, 0, 255, cv2.NORM_MINMAX)
depth_normalized = np.uint8(depth_normalized)
depth_colored = cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET)
cv2.imshow('深度图', depth_colored)
cv2.waitKey(0)
标定注意事项
棋盘格准备
- 使用高质量的棋盘格打印件,确保方格大小准确
- 棋盘格应该平整,不能弯曲
- 方格数量建议不少于 8x6 个内角点
- 方格大小根据标定距离选择,通常 20-30mm
图像采集
- 采集至少 15-20 张不同角度的图像
- 覆盖图像的不同区域(中心、边缘、角落)
- 棋盘格应该占据图像的大部分区域
- 包含不同的倾斜角度(绕 X、Y、Z 轴旋转)
- 避免过度倾斜(不超过 45 度)
提高精度
- 使用高分辨率图像
- 确保光照均匀
- 使用亚像素角点检测
- 过滤掉检测失败的图像
- 使用圆形网格代替棋盘格(精度更高)
下一步
掌握了相机标定后,下一章节我们将学习深度学习模块,了解如何在 OpenCV 中使用深度学习模型。