OpenCV 计算机视觉完全教程 / 第 14 章 — 相机标定与立体视觉
第 14 章 — 相机标定与立体视觉
14.1 相机模型
针孔相机模型
世界坐标 (X,Y,Z) → 相机坐标 → 图像坐标 → 像素坐标 (u,v)
像素坐标:
u = fx * X/Z + cx
v = fy * Y/Z + cy
内参矩阵 K:
┌ ┐
│ fx 0 cx │
│ 0 fy cy │
│ 0 0 1 │
└ ┘
畸变参数: [k1, k2, p1, p2, k3]
k1, k2, k3: 径向畸变
p1, p2: 切向畸变
畸变类型
| 畸变 | 形状 | 说明 |
|---|
| 桶形畸变 | 向内弯曲 | 广角镜头常见 |
| 枕形畸变 | 向外弯曲 | 长焦镜头常见 |
| 切向畸变 | 倾斜 | 传感器不平行 |
14.2 相机标定
使用棋盘格图案进行相机标定。
import cv2
import numpy as np
import glob
def calibrate_camera(image_dir, pattern_size=(9, 6)):
"""
相机标定
参数:
image_dir: 棋盘格图像目录
pattern_size: 棋盘格内角点数 (列, 行)
"""
# 3D 世界坐标(假设棋盘格在 z=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)
# 实际尺寸(假设每个方格 25mm)
square_size = 25.0
objp *= square_size
obj_points = [] # 3D 点
img_points = [] # 2D 点
images = glob.glob(f"{image_dir}/*.jpg")
print(f"找到 {len(images)} 张标定图像")
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:
# 亚像素精化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
30, 0.001)
corners_refined = cv2.cornerSubPix(
gray, corners, (11, 11), (-1, -1), criteria
)
obj_points.append(objp)
img_points.append(corners_refined)
# 可视化
cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
cv2.imshow("标定", img)
cv2.waitKey(500)
print(f" ✓ {fname}")
else:
print(f" ✗ {fname} (未检测到棋盘格)")
cv2.destroyAllWindows()
# 执行标定
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, gray.shape[::-1], None, None
)
print(f"\n=== 标定结果 ===")
print(f"RMS 重投影误差: {ret:.4f} 像素")
print(f"\n内参矩阵 K:\n{camera_matrix}")
print(f"\n畸变系数: {dist_coeffs.ravel()}")
return camera_matrix, dist_coeffs, rvecs, tvecs
# 使用
# K, dist, rvecs, tvecs = calibrate_camera("calibration_images/", (9, 6))
// C++ 相机标定
std::vector<std::vector<cv::Point3f>> obj_points;
std::vector<std::vector<cv::Point2f>> img_points;
cv::Size pattern_size(9, 6);
// ... 收集角点 ...
cv::Mat camera_matrix, dist_coeffs;
std::vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(obj_points, img_points,
image_size, camera_matrix,
dist_coeffs, rvecs, tvecs);
标定参数参考
| 参数 | 典型范围 | 说明 |
|---|
| fx, fy | 500-3000 | 焦距(像素) |
| cx, cy | 图像中心 | 主点坐标 |
| k1 | -0.5 ~ 0.5 | 径向畸变系数 1 |
| k2 | -0.1 ~ 0.1 | 径向畸变系数 2 |
| p1, p2 | -0.01 ~ 0.01 | 切向畸变系数 |
| RMS 误差 | < 0.5 像素 | 标定质量指标 |
14.3 畸变校正
import cv2
import numpy as np
def undistort_image(image, camera_matrix, dist_coeffs):
"""去畸变"""
h, w = image.shape[:2]
# 方法 1: 简单去畸变
undistorted = cv2.undistort(image, camera_matrix, dist_coeffs)
# 方法 2: 使用最优新内参
new_mtx, roi = cv2.getOptimalNewCameraMatrix(
camera_matrix, dist_coeffs, (w, h), alpha=0,
newImageSize=(w, h)
)
# alpha=0: 裁剪所有黑边
# alpha=1: 保留所有像素(可能有黑边)
undistorted2 = cv2.undistort(image, camera_matrix, dist_coeffs,
None, new_mtx)
# 裁剪有效区域
x, y, rw, rh = roi
cropped = undistorted2[y:y+rh, x:x+rw]
return cropped
# 使用映射表(更快,适合视频流)
def create_undistort_maps(camera_matrix, dist_coeffs, image_size):
w, h = image_size
new_mtx, _ = cv2.getOptimalNewCameraMatrix(
camera_matrix, dist_coeffs, (w, h), 0, (w, h)
)
map1, map2 = cv2.initUndistortRectifyMap(
camera_matrix, dist_coeffs, None, new_mtx,
(w, h), cv2.CV_32FC1
)
return map1, map2
def fast_undistort(image, map1, map2):
return cv2.remap(image, map1, map2, cv2.INTER_LINEAR)
14.4 立体视觉
14.4.1 立体标定
import cv2
import numpy as np
def stereo_calibrate(obj_points, img_points_l, img_points_r,
image_size, K1, dist1, K2, dist2):
"""立体标定"""
flags = cv2.CALIB_FIX_INTRINSIC
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100, 1e-6)
ret, K1, dist1, K2, dist2, R, T, E, F = cv2.stereoCalibrate(
obj_points, img_points_l, img_points_r,
K1, dist1, K2, dist2, image_size,
criteria=criteria, flags=flags
)
print(f"立体标定 RMS 误差: {ret:.4f}")
print(f"旋转矩阵 R:\n{R}")
print(f"平移向量 T:\n{T}")
print(f"基线距离: {np.linalg.norm(T):.2f} mm")
return R, T, E, F
14.4.2 立体校正
def stereo_rectify(K1, dist1, K2, dist2, image_size, R, T):
"""立体校正"""
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
K1, dist1, K2, dist2, image_size, R, T,
alpha=0 # 裁剪黑边
)
# 生成映射表
map1_l, map2_l = cv2.initUndistortRectifyMap(
K1, dist1, R1, P1, image_size, cv2.CV_32FC1)
map1_r, map2_r = cv2.initUndistortRectifyMap(
K2, dist2, R2, P2, image_size, cv2.CV_32FC1)
return (map1_l, map2_l), (map1_r, map2_r), Q
def rectify_images(img_left, img_right, maps_left, maps_right):
"""应用立体校正"""
rect_l = cv2.remap(img_left, maps_left[0], maps_left[1],
cv2.INTER_LINEAR)
rect_r = cv2.remap(img_right, maps_right[0], maps_right[1],
cv2.INTER_LINEAR)
return rect_l, rect_r
14.5 深度图生成
import cv2
import numpy as np
def compute_depth_map(img_left, img_right, Q=None):
"""计算视差图/深度图"""
# 转灰度
gray_l = cv2.cvtColor(img_left, cv2.COLOR_BGR2GRAY)
gray_r = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY)
# 方法 1: StereoBM(块匹配,速度快)
stereo_bm = cv2.StereoBM_create(numDisparities=128, blockSize=15)
disparity_bm = stereo_bm.compute(gray_l, gray_r)
# 方法 2: StereoSGBM(半全局匹配,精度高)
stereo_sgbm = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=128, # 必须是 16 的倍数
blockSize=5,
P1=8 * 3 * 5**2, # 惩罚项 1
P2=32 * 3 * 5**2, # 惩罚项 2
disp12MaxDiff=1,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY
)
disparity_sgbm = stereo_sgbm.compute(gray_l, gray_r)
# 归一化显示
disp_normalized = cv2.normalize(
disparity_sgbm, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
)
# 如果有 Q 矩阵,重投影为 3D 点云
if Q is not None:
points_3D = cv2.reprojectImageTo3D(disparity_sgbm.astype(np.float32) / 16.0, Q)
return disparity_sgbm, disp_normalized, points_3D
return disparity_sgbm, disp_normalized, None
StereoBM vs StereoSGBM
| 方法 | 速度 | 精度 | 适用场景 |
|---|
| StereoBM | ★★★★★ | ★★★ | 实时、低精度 |
| StereoSGBM | ★★★☆☆ | ★★★★★ | 离线、高精度 |
14.6 ArUco 标记与 AR
import cv2
import numpy as np
def detect_aruco(image):
"""ArUco 标记检测"""
# 选择字典
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)
detector = cv2.aruco.ArucoDetector(aruco_dict)
# 检测
corners, ids, rejected = detector.detectMarkers(image)
if ids is not None:
print(f"检测到 {len(ids)} 个标记: {ids.flatten()}")
cv2.aruco.drawDetectedMarkers(image, corners, ids)
return corners, ids
def estimate_pose(image, corners, ids, camera_matrix, dist_coeffs,
marker_length=0.05):
"""估计标记位姿"""
rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
corners, marker_length, camera_matrix, dist_coeffs
)
result = image.copy()
for i in range(len(ids)):
cv2.drawFrameAxes(result, camera_matrix, dist_coeffs,
rvecs[i], tvecs[i], marker_length * 0.5)
# 输出位姿信息
t = tvecs[i][0]
distance = np.linalg.norm(t)
print(f"标记 {ids[i][0]}: 位置=({t[0]:.3f},{t[1]:.3f},{t[2]:.3f}), "
f"距离={distance:.3f}m")
return result, rvecs, tvecs
14.7 点云处理
import cv2
import numpy as np
def generate_point_cloud(disparity, Q, image, min_disp=10, num_disp=128):
"""从视差图生成彩色点云"""
# 重投影到 3D
points_3D = cv2.reprojectImageTo3D(
disparity.astype(np.float32) / 16.0, Q
)
# 获取颜色
colors = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 过滤有效点
mask = disparity > disparity.min()
mask &= disparity < (min_disp + num_disp)
points = points_3D[mask]
colors = colors[mask]
# 保存为 PLY 文件
save_ply("pointcloud.ply", points, colors)
return points, colors
def save_ply(filename, points, colors):
"""保存点云为 PLY 格式"""
with open(filename, 'w') as f:
f.write("ply\n")
f.write("format ascii 1.0\n")
f.write(f"element vertex {len(points)}\n")
f.write("property float x\nproperty float y\nproperty float z\n")
f.write("property uchar red\nproperty uchar green\nproperty uchar blue\n")
f.write("end_header\n")
for p, c in zip(points, colors):
f.write(f"{p[0]:.4f} {p[1]:.4f} {p[2]:.4f} "
f"{c[0]} {c[1]} {c[2]}\n")
14.8 实战应用
| 场景 | 技术 | 说明 |
|---|
| 机器人导航 | 双目深度 | 避障、路径规划 |
| 工业测量 | 标定 + 立体视觉 | 三维尺寸检测 |
| AR 应用 | ArUco + solvePnP | 虚实叠加 |
| 自动驾驶 | LiDAR + 相机标定 | 多传感器融合 |
| 3D 扫描 | 结构光/双目 | 物体三维重建 |
14.9 扩展阅读
本章小结: 掌握了相机标定、畸变校正、立体视觉、深度图生成和 ArUco 标记检测等 3D 视觉技术。