1. 相机标定与图像畸变校正

相机标定与图像畸变校正 学习卡片

任务目标

在Jetson平台上,使用棋盘格标定板对USB相机完成内参标定,获取相机矩阵与畸变系数,并利用标定结果对实时图像进行畸变校正,消除镜头引起的桶形/枕形畸变。

环境与依赖

  • 硬件:Jetson Nano/Xavier NX/Orin系列开发板 + USB相机(或CSI相机,需对应驱动)
  • 系统:Ubuntu 18.04/20.04(JetPack 4.x/5.x)
  • Python库:opencv-python(含cv2)、numpy、glob(标准库)

安装命令:pip3 install opencv-python numpy

实现方案

完整流程分为三步:采集标定图像 → 计算内参与畸变系数 → 加载参数并实时校正。以下代码将三步整合为两个可独立运行的脚本,先运行采集脚本生成标定数据,再运行标定与校正脚本完成闭环。

代码实现

第一步:采集棋盘格图像(保存至 calib_images/ 目录)

import cv2
import os

os.makedirs('calib_images', exist_ok=True)
cap = cv2.VideoCapture(0)  # 0为默认相机,按需调整
count = 0

while True:
    ret, frame = cap.read()
    if not ret:
        break
    cv2.putText(frame, f'Captured: {count}/20 (SPACE to save, ESC to exit)', 
                (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    cv2.imshow('Capture', frame)
    key = cv2.waitKey(1) & 0xFF
    if key == ord(' '):
        cv2.imwrite(f'calib_images/img_{count:02d}.jpg', frame)
        count += 1
        print(f'Saved {count}/20')
    elif key == 27:  # ESC
        break
    if count >= 20:
        break

cap.release()
cv2.destroyAllWindows()
print('Capture complete.')

第二步:标定并校正(核心完整代码)

import cv2
import numpy as np
import glob

# ========== 配置 ==========
CHESSBOARD_SIZE = (9, 6)  # 内角点数,需与棋盘格实际规格一致
SQUARE_SIZE = 0.025       # 棋盘格方格边长(米),用于尺度恢复
CALIB_DIR = 'calib_images/'

# ========== 1. 提取角点 ==========
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) * SQUARE_SIZE

objpoints = []  # 世界坐标系中的3D点
imgpoints = []  # 图像中的2D点
images = glob.glob(CALIB_DIR + '*.jpg')

assert len(images) >= 5, f'至少需要5张有效标定图,当前仅{len(images)}张'

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)
        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, CHESSBOARD_SIZE, corners2, ret)
        cv2.imshow('Detected', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()
print(f'成功提取角点的图像数: {len(objpoints)}/{len(images)}')

# ========== 2. 计算内参与畸变系数 ==========
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

print('相机矩阵 (mtx):\n', mtx)
print('畸变系数 (dist):\n', dist)
print('重投影误差:', ret)

# 保存标定结果
np.savez('calib_params.npz', mtx=mtx, dist=dist)

# ========== 3. 畸变校正(单张图像验证) ==========
test_img = cv2.imread(images[0])
h, w = test_img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

# 方法一:undistort(简单直接)
dst = cv2.undistort(test_img, mtx, dist, None, newcameramtx)

# 方法二:remap(适用于实时视频流,预计算映射表)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1)
dst_remap = cv2.remap(test_img, mapx, mapy, cv2.INTER_LINEAR)

# 裁剪ROI区域(去除黑色边框)
x, y, w_roi, h_roi = roi
dst_cropped = dst[y:y+h_roi, x:x+w_roi]

# 拼接对比图
compare = np.hstack((test_img, dst))
cv2.imwrite('calibration_result.jpg', compare)
cv2.imshow('Before | After', compare)
cv2.waitKey(0)
cv2.destroyAllWindows()
print('校正结果已保存为 calibration_result.jpg')

运行与验证

  1. 采集图像:运行第一步脚本,手持棋盘格在不同角度、距离(约30–80cm)下拍摄,确保棋盘格完整出现在画面中且光线均匀。按空格键保存,共采集15–20张后按ESC退出。
  2. 执行标定:运行第二步脚本。终端会打印相机矩阵、畸变系数和重投影误差。
  3. 判定通过标准:
    • 角点检测阶段:成功提取角点的图像数 ≥ 5张(建议≥15张以保证精度);
    • 重投影误差 < 0.5像素(若大于1.0,需重新采集更多角度图像);
    • 弹出对比图中,棋盘格直线边缘由弯曲变为笔直,图像四周的拉伸变形明显消除。

实时视频流校正(可选扩展)

若需对实时视频流校正,只需在读取每帧后调用 cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) 即可,映射表只需计算一次,性能开销极小。

常见问题

  • 角点检测失败:确认棋盘格内角点数设置正确(如9×6表示内角点,而非格子数);避免强反光或阴影遮挡。
  • 标定结果偏差大:图像数量不足或拍摄角度过于单一(如全部正对相机),应包含倾斜、旋转、远近不同姿态。
  • 校正后图像边缘有黑边:使用 getOptimalNewCameraMatrix 的ROI裁剪即可去除。