相机标定知识梳理 (Camera Calibration Guide)
1. 为什么需要相机标定?
1.1 相机的"数学人设"
简单说:相机就是一个把 3D 世界"压扁"成 2D 图片的函数。
真实世界 (3D) ──[相机函数]──▶ 图片 (2D)
P(X,Y,Z) p(u,v)
- 这个映射是不可逆的:一张照片无法唯一还原三维场景
- 但我们可以用一个简化的数学模型来近似这个复杂过程
- 标定 = 用已知图案(标定板)反推相机内部参数
1.2 标定能解决什么问题?
| 应用场景 | 标定作用 |
|---|---|
| 畸变矫正 | 把弯曲的直线"拉直",提升图像质量 |
| 双目视觉 | 计算左右相机相对位姿,实现深度估计 |
| 三维重建 | 从多视角图像恢复场景 3D 结构 |
| SLAM/导航 | 提供精确的相机内参,支撑位姿估计 |
| AR/VR | 实现虚拟物体与真实场景的精准对齐 |
💡 通俗理解:标定就是给相机"体检",搞清楚它的"内在性格"(焦距、畸变)和"外在姿态"(旋转平移)。
2. 小孔成像与坐标系全家桶 📐
2.1 小孔成像原理(理想模型)
真实世界
│
▼
┌───────────────┐
│ 小孔光圈 │ ← 光线直线传播
└───────────────┘
│
▼
┌───────────────┐
│ 成像平面 │ ← 倒立实像
└───────────────┘
2.2 四大坐标系
| 坐标系 | 符号 | 原点 | 单位 | 物理意义 |
|---|---|---|---|---|
| 世界坐标系 | $\{W\}$ | 任意选定 | 米 (m) | 描述物体在真实空间的位置 |
| 相机坐标系 | $\{C\}$ | 相机光心 | 米 (m) | 以相机为中心的 3D 参考系 |
| 图像物理坐标系 | $\{I\}$ | 主点 (光轴交点) | 毫米 (mm) | 成像平面上的连续坐标 |
| 像素坐标系 | $\{P\}$ | 图像左上角 | 像素 (pixel) | 数字图像中的离散行列索引 |
2.3 坐标转换链条
世界坐标 ──[外参: R, t]──▶ 相机坐标 ──[投影]──▶ 图像坐标 ──[内参: K]──▶ 像素坐标
2.3.1 世界 → 相机(刚体变换)
$$ \begin{bmatrix} X_c \\ Y_c \\ Z_c \end{bmatrix} = R \begin{bmatrix} X_w \\ Y_w \\ Z_w \end{bmatrix} + \mathbf{t} \quad \text{或} \quad \tilde{\mathbf{P}}_c = \begin{bmatrix} R & \mathbf{t} \\ \mathbf{0}^T & 1 \end{bmatrix} \tilde{\mathbf{P}}_w $$$R \in SO(3)$ 是旋转矩阵,$\mathbf{t} \in \mathbb{R}^3$ 是平移向量,合称外参。
2.3.2 相机 → 图像(透视投影)
$$ \begin{bmatrix} x \\ y \end{bmatrix} = f \cdot \begin{bmatrix} X_c / Z_c \\ Y_c / Z_c \end{bmatrix} \quad \Rightarrow \quad s \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} f & 0 & 0 \\ 0 & f & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} X_c \\ Y_c \\ Z_c \end{bmatrix} $$$f$ 是焦距,$s$ 是尺度因子(齐次坐标特性)。
2.3.3 图像 → 像素(仿射变换)
$$ \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \begin{bmatrix} 1/d_x & 0 & u_0 \\ 0 & 1/d_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} $$$d_x, d_y$ 是单个像素的物理尺寸,$(u_0, v_0)$ 是主点像素坐标。
2.3.4 终极合并:世界 → 像素
$$ s \tilde{\mathbf{p}} = K \cdot [R | \mathbf{t}] \cdot \tilde{\mathbf{P}}_w \quad \text{其中} \quad K = \begin{bmatrix} f_x & \gamma & u_0 \\ 0 & f_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} $$内参矩阵 $K$ 包含:$f_x = f/d_x$, $f_y = f/d_y$(等效焦距),$\gamma$(轴倾斜,通常≈0),$(u_0, v_0)$(主点)。
3. 镜头畸变
3.1 为什么会有畸变?
透镜不是完美的!光线经过透镜边缘时会发生非线性折射,导致成像点"跑偏"。
3.2 两大畸变类型
径向畸变 (Radial Distortion)
- 成因:透镜曲率导致,沿径向分布
- 表现:
- 桶形畸变:边缘向外膨胀(广角/鱼眼常见)
- 枕形畸变:边缘向内收缩(长焦常见)
- 数学模型(多项式近似):
$$
\begin{aligned}
x_{\text{dist}} &= x (1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\
y_{\text{dist}} &= y (1 + k_1 r^2 + k_2 r^4 + k_3 r^6)
\end{aligned}
\quad \text{其中} \quad r^2 = x^2 + y^2
$$
$k_1, k_2, k_3$ 是径向畸变系数,通常前两项足够,鱼眼需保留 $k_3$。
切向畸变 (Tangential Distortion)
- 成因:透镜与成像平面不平行(装配误差)
- 数学模型:
$$
\begin{aligned}
x_{\text{dist}} &= x + [2p_1 xy + p_2(r^2 + 2x^2)] \\
y_{\text{dist}} &= y + [p_1(r^2 + 2y^2) + 2p_2 xy]
\end{aligned}
$$
$p_1, p_2$ 是切向畸变系数,通常量级较小。
3.3 完整畸变模型(合并径向+切向)
$$ \begin{bmatrix} u_{\text{dist}} \\ v_{\text{dist}} \end{bmatrix} = \begin{bmatrix} f_x \cdot x_{\text{dist}} + u_0 \\ f_y \cdot y_{\text{dist}} + v_0 \end{bmatrix} $$💡 工程实践:标定过程就是求解 $[K, k_1, k_2, p_1, p_2]$ 这 9 个参数(鱼眼额外加 $k_3$)。
4. 张正友标定法
4.1 核心思想
用平面标定板(如棋盘格)的多视角图像,通过单应性矩阵建立约束,分步求解内参+外参+畸变。
4.2 关键步骤拆解
Step 1: 单应性矩阵 (Homography)
- 定义:平面到平面的投影映射 $ \mathbf{q} = s H \mathbf{Q} $
- 自由度:$H \in \mathbb{R}^{3\times3}$ 有 8 个自由度(齐次尺度不变)
- 求解:至少 4 对点(8 方程),实际用更多点 + RANSAC 抗噪
Step 2: 内参约束方程
利用旋转矩阵列向量的正交性:
$$ \mathbf{h}_1^T K^{-T} K^{-1} \mathbf{h}_2 = 0, \quad \mathbf{h}_1^T K^{-T} K^{-1} \mathbf{h}_1 = \mathbf{h}_2^T K^{-T} K^{-1} \mathbf{h}_2 $$其中 $\mathbf{h}_1, \mathbf{h}_2$ 是 $H$ 的前两列。
Step 3: 线性求解 + 非线性优化
- 线性初值:将多张图像的约束堆叠,用 SVD 求解 $B = K^{-T}K^{-1}$
- Cholesky 分解:从 $B$ 恢复内参 $K$
- 极大似然优化:用 Levenberg-Marquardt 算法最小化重投影误差:
$$
\min_{K, R_i, \mathbf{t}_i, \mathbf{k}} \sum_{i,j} \left\| \mathbf{p}_{ij} - \pi(K, R_i, \mathbf{t}_i, \mathbf{k}, \mathbf{P}_j) \right\|^2
$$
$\pi(\cdot)$ 是包含畸变的完整投影函数。
4.3 为什么不用纯非线性优化?
工程智慧:非线性优化对初值敏感!张正友法先用线性方法给个好初值,再精细优化,既稳定又高效。
5. 单目标定实战 (OpenCV + Python)
5.1 标定板选择指南
| 类型 | 推荐规格 | 注意事项 |
|---|---|---|
| 棋盘格 | 7×8 或 9×10 内角点 | 角点检测最成熟,首选 |
| 圆点阵列 | 间距均匀 | 适合高反光场景 |
| Charuco | 棋盘+ArUco 混合 | 部分遮挡仍可检测 |
备注:标定板应覆盖图像四角+中心,采集10~20 张不同姿态图像。
5.2 完整代码流程
import cv2
import numpy as np
import glob
# === 1. 准备标定板参数 ===
# 棋盘格内角点数量 (列, 行)
pattern_size = (9, 6)
# 每个方格的实际尺寸 (单位:米)
square_size = 0.025
# === 2. 采集图像并检测角点 ===
object_points = [] # 3D 世界坐标 (Z=0)
image_points = [] # 2D 像素坐标
# 生成标定板角点的世界坐标 (假设标定板在 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)
objp *= square_size
# 遍历所有标定图像
for fname in glob.glob('calib_images/*.png'):
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 检测角点 (亚像素精度)
ret, corners = cv2.findChessboardCorners(gray, pattern_size,
cv2.CALIB_CB_ADAPTIVE_THRESH)
if ret:
# 亚像素优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
object_points.append(objp)
image_points.append(corners)
# 可视化检测效果 (可选)
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
cv2.imshow('Detected', img)
cv2.waitKey(500)
# === 3. 执行标定 ===
ret, K, dist, rvecs, tvecs = cv2.calibrateCamera(
object_points, # 3D 点集
image_points, # 2D 点集
gray.shape[::-1], # 图像尺寸 (width, height)
None, None, # 内参/畸变初值 (None=自动估计)
flags=cv2.CALIB_FIX_K3 + cv2.CALIB_ZERO_TANGENT_DIST # 约束条件
)
print(f"重投影误差 (RMS): {ret:.4f} pixels")
print(f"内参矩阵 K:\n{K}")
print(f"畸变系数: {dist.ravel()}")
# === 4. 畸变矫正演示 ===
def undistort_and_show(img, K, dist):
# 方法 1: 直接矫正 (简单)
undist = cv2.undistort(img, K, dist)
# 方法 2: 映射表矫正 (高效,适合视频流)
map1, map2 = cv2.initUndistortRectifyMap(K, dist, None, K,
img.shape[1::-1], cv2.CV_16SC2)
undist_fast = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR)
# 并排显示对比
result = np.hstack([img, undist])
cv2.imshow('Original vs Undistorted', result)
cv2.waitKey(0)
# 测试一张图
test_img = cv2.imread('calib_images/test.png')
undistort_and_show(test_img, K, dist)
5.3 标定效果评估技巧
# 🔍 评估 1: 重投影误差分布
errors = []
for i in range(len(object_points)):
img_points_proj, _ = cv2.projectPoints(
object_points[i], rvecs[i], tvecs[i], K, dist)
error = cv2.norm(image_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj)
errors.append(error)
print(f"平均误差: {np.mean(errors):.3f}px, 最大误差: {np.max(errors):.3f}px")
# 🔍 评估 2: 外参可视化 (以相机为中心)
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
# 绘制相机坐标系
ax.quiver(0, 0, 0, 1, 0, 0, color='r', length=0.1, label='X')
ax.quiver(0, 0, 0, 0, 1, 0, color='g', length=0.1, label='Y')
ax.quiver(0, 0, 0, 0, 0, 1, color='b', length=0.1, label='Z')
# 绘制各次标定的标定板位置
for R_vec, t in zip(rvecs, tvecs):
R, _ = cv2.Rodrigues(R_vec) # 旋转向量→旋转矩阵
# 标定板四个角点 (世界坐标)
corners_3d = np.array([
[0, 0, 0],
[pattern_size[0]*square_size, 0, 0],
[pattern_size[0]*square_size, pattern_size[1]*square_size, 0],
[0, pattern_size[1]*square_size, 0]
]).T
# 变换到相机坐标系
corners_cam = R @ corners_3d + t
ax.plot(corners_cam[0], corners_cam[1], corners_cam[2], 'k-')
ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
ax.set_title('Calibration Poses (Camera-Centered)')
ax.legend(); plt.tight_layout(); plt.show()
合格标准:平均重投影误差 < 0.5 像素,标定板姿态覆盖全视场角。
6. 双目相机:从单眼到立体视觉
6.1 理想双目模型(行对齐)
左相机 ◄───── 基线 T ─────► 右相机
│ │
▼ ▼
图像左 图像右
│ │
└─── 同一特征点 ────┘
│
▼
视差 d = x_left - x_right
│
▼
深度 Z = f·T / d ← 三角测量核心公式!
关键结论:深度 $Z$ 与视差 $d$ 成反比,且非线性——近距离精度高,远距离敏感。
6.2 对极几何:降低匹配搜索维度
核心概念图解
3D 点 P
│
┌─────┴─────┐
▼ ▼
左极点 e_L 右极点 e_R
│ │
▼ ▼
左极线 l_L 右极线 l_R
| 术语 | 定义 | 工程意义 |
|---|---|---|
| 基线 (Baseline) | 两相机光心连线 | 决定深度测量精度 |
| 极点 (Epipole) | 基线与成像平面交点 | 极线汇聚点 |
| 极线 (Epipolar Line) | 3D 点投影在两图像的对应线 | 将 2D 匹配降为 1D 搜索 |
| 极线约束 | 匹配点必在对应极线上 | 大幅提升匹配效率与鲁棒性 |
本征矩阵 (E) vs 基本矩阵 (F)
$$ \mathbf{p}_R^T E \mathbf{p}_L = 0 \quad \text{(相机坐标系)} \quad \mathbf{q}_R^T F \mathbf{q}_L = 0 \quad \text{(像素坐标系)} $$$$ E = [\mathbf{t}]_\times R, \quad F = K_R^{-T} E K_L^{-1} $$
记忆口诀:
E 管外参,F 管像素;E 是 F 的"去内参"版本。
6.3 双目标定 + 立体校正流程
# === 1. 分别标定左右相机 (获取 K1, dist1, K2, dist2) ===
# ... (调用单目标定代码) ...
# === 2. 双目标定:求解相对位姿 [R, T] ===
flags = cv2.CALIB_FIX_INTRINSIC # 固定已标定的内参
ret, K1, dist1, K2, dist2, R, T, E, F = cv2.stereoCalibrate(
object_points, image_points1, image_points2,
K1, dist1, K2, dist2, gray.shape[::-1],
flags=flags, criteria=(cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)
)
# === 3. 立体校正:生成行对齐的映射表 ===
# 输出:校正旋转矩阵 R1, R2 + 投影矩阵 P1, P2 + 重投影矩阵 Q
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
K1, dist1, K2, dist2, gray.shape[::-1], R, T,
alpha=0, # 0=裁剪黑边,1=保留全部像素
newImageSize=(width, height)
)
# === 4. 生成畸变矫正 + 立体校正的映射表 ===
map1_L, map2_L = cv2.initUndistortRectifyMap(K1, dist1, R1, P1[:3,:3],
(width, height), cv2.CV_16SC2)
map1_R, map2_R = cv2.initUndistortRectifyMap(K2, dist2, R2, P2[:3,:3],
(width, height), cv2.CV_16SC2)
# === 5. 实时校正函数 (用于视频流) ===
def rectify_pair(img_L, img_R):
undist_L = cv2.remap(img_L, map1_L, map2_L, cv2.INTER_LINEAR)
undist_R = cv2.remap(img_R, map1_R, map2_R, cv2.INTER_LINEAR)
return undist_L, undist_R
# === 6. 深度计算 (使用重投影矩阵 Q) ===
# 先计算视差图 (见第 7 节),然后:
disparity = stereo_match(rect_L, rect_R) # 单位:像素
depth_map = cv2.reprojectImageTo3D(disparity, Q) # 输出:XYZ 三维坐标
⚠️ 需要注意:
alpha=0会裁剪图像边缘,但保证所有像素有效;alpha=1保留全部像素但有黑边Q矩阵的第四行[0,0,-1/Tx, (cx_L-cx_R)/Tx]编码了基线 $T_x$ 和主点差- 校正后务必可视化极线验证:同一特征点应在同一行
7. 立体匹配:从视差到深度
7.1 匹配四步法(局部/全局通用框架)
校正图像 ──▶ [1. 代价计算] ──▶ [2. 代价聚合] ──▶ [3. 视差选择] ──▶ [4. 视差优化] ──▶ 视差图
Step 1: 匹配代价计算
| 方法 | 公式 | 特点 |
|---|---|---|
| AD (绝对差) | $C = | I_L - I_R |
| SAD (窗口和) | $C = \sum_{w} | I_L - I_R |
| NCC (归一化相关) | $C = \frac{\sum (I_L-\bar{I}_L)(I_R-\bar{I}_R)}{\sigma_L \sigma_R}$ | 光照鲁棒,计算量大 |
| Census | 比特串 Hamming 距离 | 纹理鲁棒,适合弱光照 |
Step 2: 代价聚合
- 固定窗口:简单但边缘过平滑
- 自适应权重 (ASW):根据颜色/空间距离加权,保边效果好
- 半全局匹配 (SGM):16 方向动态规划,精度/速度平衡(OpenCV SGBM 默认)
Step 3: 视差选择
- **WTA **:$d^* = \arg\min_d C(p, d)$
- 亚像素优化:抛物线拟合提升精度到 0.1 像素级
Step 4: 视差优化
- 唯一性检测:过滤左右一致性差的点
- 中值滤波:去除孤立噪声
- 左右一致性检查 (LRC):$|d_L(p) + d_R(p+d_L)| < \text{threshold}$
7.2 OpenCV BM vs SGBM 实战对比
# === BM (Block Matching) - 快速但精度一般 ===
bm = cv2.StereoBM_create(
numDisparities=16*5, # 视差范围 (需为 16 倍数)
blockSize=15 # SAD 窗口大小 (奇数)
)
bm.setPreFilterCap(31)
bm.setTextureThreshold(10)
bm.setUniquenessRatio(10) # 唯一性阈值 (%)
bm.setSpeckleWindowSize(100) # 斑点过滤
bm.setSpeckleRange(32)
# === SGBM (Semi-Global BM) - 精度高,推荐默认使用 ===
sgbm = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=16*5, # 同上
blockSize=5, # 通常 3~7
P1=8*3*5**2, # 小视差变化惩罚
P2=32*3*5**2, # 大视差变化惩罚
disp12MaxDiff=1, # 左右一致性检查阈值
preFilterCap=63,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY # 3 路径/5 路径/全路径
)
# === 计算视差图 ===
disparity = sgbm.compute(rect_L, rect_R).astype(np.float32) / 16.0 # SGBM 输出×16
# === 可视化 (注意:视差图需归一化显示) ===
disp_vis = cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
disp_vis = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET)
cv2.imshow('Disparity', disp_vis)
参数调优口诀:
numDisparities:根据最近物体视差设定,宁大勿小(计算量线性增长)blockSize:纹理丰富用小窗口 (35),弱纹理用大窗口 (915)P1/P2:$P_2 > P_1$,比值越大视差图越平滑(但可能丢失细节)uniquenessRatio:10~15 平衡精度与完整性
8. 鱼眼相机:超广角的特殊处理
8.1 为什么普通模型搞不定鱼眼?
针孔模型假设直线投影为直线,但鱼眼镜头视角>180°,必须用非线性投影模型。
8.2 四种经典投影模型对比
| 模型 | 投影公式 $r = f(\theta)$ | 特点 | 适用场景 |
|---|---|---|---|
| 等距投影 | $r = f \cdot \theta$ | 计算简单,最常用 | 通用鱼眼标定 |
| 等立体角 | $r = 2f \cdot \sin(\theta/2)$ | 保持面积比例 | 全景拼接 |
| 正交投影 | $r = f \cdot \sin\theta$ | 边缘压缩严重 | 特殊光学设计 |
| 体视投影 | $r = 2f \cdot \tan(\theta/2)$ | 保持角度不变 | 天文摄影 |
备注:90% 的鱼眼相机近似服从等距投影,后续标定以此为基础。
8.3 Kannala-Brandt (KB) 畸变模型(现代标准)
核心思想
将鱼眼成像分解为两步:
3D 点 ──▶ 单位球面投影 ──▶ 非线性畸变 ──▶ 像素坐标
数学表达
$$ \begin{aligned} \theta &= \arccos\left( \frac{Z_c}{\sqrt{X_c^2 + Y_c^2 + Z_c^2}} \right) \\ r_d &= \theta + k_1 \theta^3 + k_2 \theta^5 + k_3 \theta^7 + k_4 \theta^9 \\ x_d &= r_d \cdot \frac{X_c}{\sqrt{X_c^2 + Y_c^2}}, \quad y_d = r_d \cdot \frac{Y_c}{\sqrt{X_c^2 + Y_c^2}} \\ \begin{bmatrix} u \\ v \end{bmatrix} &= \begin{bmatrix} f_x \cdot x_d + \gamma y_d + u_0 \\ f_y \cdot y_d + v_0 \end{bmatrix} \end{aligned} $$$k_1 \sim k_4$ 是 KB 畸变系数,$\theta$ 是入射角(非图像坐标!)。
8.4 鱼眼标定实战 (OpenCV fisheye 模块)
import cv2
import numpy as np
# === 1. 准备参数 (注意:用 fisheye 模块!) ===
pattern_size = (9, 6)
square_size = 0.025
flags = cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + \
cv2.fisheye.CALIB_CHECK_COND + \
cv2.fisheye.CALIB_FIX_SKEW
# === 2. 角点检测 (同单目,但用 fisheye 接口) ===
object_points = [] # 3D 点
image_points = [] # 2D 点
# ... (检测代码同前,略) ...
# === 3. 鱼眼标定 ===
K = np.zeros((3, 3))
D = np.zeros((4, 1)) # [k1, k2, k3, k4]
rvecs = []
tvecs = []
rms, _, _, _, _ = cv2.fisheye.calibrate(
object_points, image_points, gray.shape[::-1],
K, D, rvecs, tvecs, flags,
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 100, 1e-6)
)
print(f"鱼眼标定误差: {rms:.4f}px")
print(f"内参:\n{K}")
print(f"KB 畸变系数: {D.ravel()}")
# === 4. 鱼眼畸变矫正 (两种模式) ===
def fisheye_undistort(img, K, D, balance=0.6, fov_scale=1.0):
"""
balance: 0~1, 0=裁剪黑边最多但视场最大, 1=保留全部像素但视场小
fov_scale: >1 放大视场 (可能插值), <1 缩小视场
"""
# 计算新内参 (可选调整视场)
new_K = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify(
K, D, img.shape[1::-1], np.eye(3),
balance=balance, fov_scale=fov_scale)
# 生成映射表
map1, map2 = cv2.fisheye.initUndistortRectifyMap(
K, D, np.eye(3), new_K, img.shape[1::-1], cv2.CV_16SC2)
# 执行矫正
undist = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REPLICATE)
return undist, new_K
# 测试矫正效果
test_img = cv2.imread('fisheye_test.jpg')
undist, new_K = fisheye_undistort(test_img, K, D, balance=0.8)
cv2.imwrite('undistorted.jpg', undist)
8.5 鱼眼矫正的三种实用方案
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 棋盘格标定法 | 用标定板求解精确映射 | 精度高,数学严谨 | 需标定板,边缘拉伸严重 | 机器人导航、测量 |
| 横向展开法 | 以图像中心为视点环视展开 | 保留全部像素,无裁剪 | 几何失真,非真实视角 | 安防监控、全景预览 |
| 经纬度展开 | 按球面经纬度映射到矩形 | 无像素损失,规则网格 | 竖直/水平方向分别处理 | 天空/地面分割分析 |
💡 选择建议:
- 需要精确测量 → 用棋盘格标定 + 精确矫正
- 只需视觉预览 → 用横向展开 (快速+直观)
- 做语义分割 → 用经纬度展开 (保持拓扑)
9.工程注意点与问题原因排查
标定数据采集 Checklist
- [ ] 标定板覆盖图像四角 + 中心 (避免中心密集)
- [ ] 采集 10~20 张,姿态变化 > 30° (旋转+平移)
- [ ] 标定板与相机距离:0.5m ~ 2m (覆盖工作距离)
- [ ] 光照均匀,避免反光/阴影/运动模糊
- [ ] 固定焦距!禁用自动对焦 (标定后勿变焦)
- [ ] 用 PNG 无损格式保存图像 (避免 JPEG 压缩伪影)
参数调试速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 重投影误差 > 1px | 角点检测不准 / 图像模糊 | 用 cornerSubPix + 提高图像质量 |
| 标定板姿态单一 | 约束不足,参数耦合 | 增加旋转角度,尤其绕 X/Y 轴 |
| 鱼眼边缘矫正扭曲 | $k_3/k_4$ 未启用 / 模型不匹配 | 用 CALIB_FIX_K3=0 + 检查投影模型 |
| 双目视差图噪声大 | 纹理弱 / 曝光不一致 | 用 Census 代价 + 直方图均衡预处理 |
| 深度图有"空洞" | 遮挡 / 唯一性过滤过严 | 降低 uniquenessRatio + 用左右一致性填充 |