入门 · 1-2周
项目八:相机标定系统
实现完整张正友标定流水线——棋盘格角点亚像素检测、内参矩阵求解、畸变校正与重投影误差评估。
实现完整张正友标定流水线——棋盘格角点亚像素检测、内参矩阵求解、畸变校正与重投影误差评估。
你用三坐标测量机(CMM)校准零件尺寸——已知标准件(棋盘格),测量多个姿态下的偏差,最小二乘拟合系统误差模型。相机标定是完全一样的逻辑:已知格点间距=标准件尺寸,检测角点=CMM探头,标定结果=误差补偿表。
相机标定就是求出针孔相机模型的5个内参:焦距$f_x, f_y$、主点$c_x, c_y$、畸变参数$k_1, k_2, p_1, p_2, k_3$。这些参数告诉你:像素坐标(u,v)对应的3D空间射线方向是什么。
camera_calibration/
├── data/
│ ├── images/ # 20+张不同姿态棋盘格照片
│ │ ├── calib_001.jpg
│ │ ├── calib_002.jpg
│ │ └── ...
│ └── validation/ # 验证集(未参与标定的图片)
│ ├── val_001.jpg
│ └── val_002.jpg
├── src/
│ ├── calibrate.py # 主标定脚本
│ ├── undistort.py # 畸变校正脚本
│ └── verify.py # 验证脚本
├── output/
│ ├── calibration.yaml # 标定结果(K, dist, rvecs, tvecs)
│ ├── corners_visualized/ # 角点检测可视化
│ └── reprojection_report.png
└── README.md
# 安装依赖
pip install opencv-python numpy matplotlib pyyaml
# 打印棋盘格(10列×7行,方格30mm)
# 贴到平整刚性板上——铝板最好,纸板会翘曲
# 拍照要点:
# 1. 覆盖相机视野的四角+中心
# 2. 每张图棋盘格姿态不同(旋转>15°,倾斜>20°)
# 3. 至少20张——更多图片→更稳定解
# 4. 对焦清晰,无运动模糊
# 5. 棋盘格占据图像面积>30%
import cv2
import numpy as np
import glob, yaml, os
from pathlib import Path
class CameraCalibrator:
"""张正友相机标定——完整的工业级实现"""
def __init__(self, pattern_size=(9, 6), square_size=0.030):
"""
Args:
pattern_size: 棋盘格内角点数 (cols, rows)
square_size: 方格边长 (米)
"""
self.pattern_size = pattern_size
self.square_size = square_size
self.K = None # 内参矩阵
self.dist = None # 畸变系数
self.rvecs = [] # 每个视角的旋转向量
self.tvecs = [] # 每个视角的平移向量
def detect_corners(self, image_paths):
"""检测所有图像中的棋盘格角点(亚像素精度)"""
# 3D世界点(棋盘格平面z=0)
objp = np.zeros((
self.pattern_size[0] * self.pattern_size[1], 3
), np.float32)
objp[:, :2] = np.mgrid[
0:self.pattern_size[0],
0:self.pattern_size[1]
].T.reshape(-1, 2)
objp *= self.square_size
obj_points = [] # 3D点
img_points = [] # 2D像素点
valid_paths = []
for path in image_paths:
img = cv2.imread(path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. 粗略角点检测
ret, corners = cv2.findChessboardCorners(
gray, self.pattern_size,
flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK
)
if not ret:
print(f"⚠️ 未检测到棋盘格: {path}")
continue
# 2. 亚像素精细化(关键步骤!精度提升10x)
criteria = (cv2.TermCriteria_EPS + cv2.TermCriteria_MAX_ITER, 30, 0.001)
corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
obj_points.append(objp)
img_points.append(corners_sub)
valid_paths.append(path)
print(f"✅ 成功检测 {len(obj_points)}/{len(image_paths)} 张图像")
return obj_points, img_points, valid_paths
def calibrate(self, obj_points, img_points, image_size):
"""执行张正友标定"""
print("
🔧 开始标定...")
print(f" 图像数量: {len(obj_points)}")
print(f" 图像尺寸: {image_size}")
print(f" 棋盘格: {self.pattern_size}")
print(f" 方格边长: {self.square_size*1000}mm")
# OpenCV的张正友标定实现
ret, K, dist, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, image_size,
cameraMatrix=None, distCoeffs=None,
flags=cv2.CALIB_RATIONAL_MODEL # k1,k2,p1,p2,k3,k4,k5,k6
)
self.K = K
self.dist = dist
self.rvecs = rvecs
self.tvecs = tvecs
# 计算重投影误差
total_error = 0
per_view_errors = []
for i in range(len(obj_points)):
img_points_proj, _ = cv2.projectPoints(
obj_points[i], rvecs[i], tvecs[i], K, dist)
error = cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj)
total_error += error
per_view_errors.append(error)
mean_error = total_error / len(obj_points)
print(f"
📊 标定结果:")
print(f" 内参矩阵 K =")
print(f" [[{K[0,0]:.1f}, {0.0:.1f}, {K[0,2]:.1f}],")
print(f" [ {0.0:.1f}, {K[1,1]:.1f}, {K[1,2]:.1f}],")
print(f" [ {0.0:.1f}, {0.0:.1f}, {1.0:.1f}]]")
print(f" 平均重投影误差: {mean_error:.4f} px")
if mean_error > 0.5:
print(f" ⚠️ 误差过大(>0.5px),建议:")
print(f" 1. 检查是否有运动模糊的照片")
print(f" 2. 增加标定图像数量(>25张)")
print(f" 3. 确保棋盘格平整无翘曲")
# 找出误差最大的图像并建议移除
worst_idx = np.argmax(per_view_errors)
print(f" 4. 考虑移除误差最大的图像(#{worst_idx}: {per_view_errors[worst_idx]:.4f}px)")
return mean_error
def save(self, filepath):
"""保存标定结果为YAML"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
data = {
'image_width': self.image_size[0],
'image_height': self.image_size[1],
'camera_matrix': {
'rows': 3, 'cols': 3,
'data': self.K.flatten().tolist()
},
'distortion_coefficients': {
'rows': 1, 'cols': len(self.dist[0]),
'data': self.dist[0].tolist()
}
}
with open(filepath, 'w') as f:
yaml.dump(data, f, default_flow_style=False)
print(f"💾 标定参数已保存到 {filepath}")
# ========== 主程序 ==========
if __name__ == '__main__':
calibrator = CameraCalibrator(
pattern_size=(9, 6),
square_size=0.030 # 30mm方格
)
# 1. 检测角点
images = sorted(glob.glob('data/images/*.jpg'))
obj_pts, img_pts, valid = calibrator.detect_corners(images)
# 2. 标定
img = cv2.imread(images[0])
calibrator.calibrate(obj_pts, img_pts, img.shape[1::-1])
# 3. 保存
calibrator.save('output/calibration.yaml')
径向畸变(桶形/枕形)+ 切向畸变(镜头组装不平行):
def undistort_image(img, K, dist, alpha=0.0):
"""畸变校正,alpha=0→裁剪到有效区,alpha=1→保留所有像素(带黑边)"""
h, w = img.shape[:2]
# 计算最优新内参(基于alpha平衡裁剪/黑边)
new_K, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), alpha, (w, h))
# 畸变校正
undistorted = cv2.undistort(img, K, dist, None, new_K)
# 裁剪有效区域
if alpha == 0:
x, y, w_roi, h_roi = roi
undistorted = undistorted[y:y+h_roi, x:x+w_roi]
return undistorted, new_K
# 验证:校正后直线的straightness
def verify_undistortion(original, undistorted, K, dist):
"""校正前后对比:在原始图上画棋盘格检测线,校正后应成直线"""
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].imshow(cv2.cvtColor(original, cv2.COLOR_BGR2RGB))
axes[0].set_title('原始图像(注意边缘弯曲)')
axes[1].imshow(cv2.cvtColor(undistorted, cv2.COLOR_BGR2RGB))
axes[1].set_title('校正后(直线应笔直)')
plt.savefig('output/undistort_comparison.png', dpi=150)
| 指标 | 优秀 | 可接受 | 不合格 |
|---|---|---|---|
| 平均重投影误差 | < 0.3 px | 0.3-0.5 px | > 0.5 px |
| 最大重投影误差 | < 0.8 px | 0.8-1.5 px | > 1.5 px |
| fx/fy差异 | < 2% | 2-5% | > 5%(镜头异常) |
| 主点偏移 | < 图像尺寸的2% | 2-5% | > 5%(安装偏差大) |
def diagnose_calibration(calibrator, obj_pts, img_pts):
"""标定故障诊断"""
# 问题1: 误差集中在某几张图
per_view_errors = []
for i in range(len(obj_pts)):
proj, _ = cv2.projectPoints(obj_pts[i], calibrator.rvecs[i],
calibrator.tvecs[i], calibrator.K, calibrator.dist)
error = cv2.norm(img_pts[i], proj, cv2.NORM_L2) / len(proj)
per_view_errors.append(error)
# 找出异常图像
mean_err = np.mean(per_view_errors)
std_err = np.std(per_view_errors)
outliers = [i for i, e in enumerate(per_view_errors) if e > mean_err + 2*std_err]
print(f"标定诊断报告:")
print(f" 平均误差: {mean_err:.4f} px")
print(f" 误差标准差: {std_err:.4f} px")
if outliers:
print(f" ⚠️ 异常图像(#): {outliers} (误差 > 均值+2σ)")
print(f" 建议: 重新拍摄这些视角或从标定集移除")
# 问题2: fx/fy不合理
fx, fy = calibrator.K[0,0], calibrator.K[1,1]
if abs(fx/fy - 1.0) > 0.05:
print(f" ⚠️ fx/fy = {fx/fy:.3f}, 像素非正方形?检查传感器")
# 问题3: 主点偏移过大
cx, cy = calibrator.K[0,2], calibrator.K[1,2]
if abs(cx - img_w/2) > img_w * 0.05:
print(f" ⚠️ 主点cx={cx:.1f}偏移图像中心{img_w/2:.1f} > 5%,镜头安装可能偏心")