【图像处理基石】如何对图像畸变进行校正?

引言
在计算机视觉入门过程中,你可能会遇到这样的问题:用手机/相机拍出来的照片边缘“变形”——直线变成曲线、矩形拍成梯形,这就是图像畸变。比如鱼眼镜头拍的风景照、广角镜头拍的建筑特写,畸变会严重影响后续的图像分析(如目标检测、尺寸测量)。
本文将从“畸变是什么、为什么会产生、怎么校正”三个核心问题出发,用通俗的语言讲解图像畸变的基础原理,再通过OpenCV实现完整的校正流程,适合刚接触计算机视觉的入门者,代码可直接复现!
一、先搞懂:图像畸变是什么?
图像畸变是指实际拍摄的图像与理想的“透视投影图像”之间的偏差,本质是光线通过相机镜头时的折射/反射不规则导致的。常见的畸变主要分为两类,也是我们校正的核心目标:
1.1 径向畸变(最常见)
光线在镜头径向(圆心到边缘方向)传播时的畸变,表现为图像中心与边缘的变形程度不同,分为两种:
- 桶形畸变:图像中间区域向外膨胀,边缘物体被拉伸(比如鱼眼镜头效果),常见于广角镜头;
- 枕形畸变:图像中间区域向内收缩,边缘物体被压缩,常见于长焦镜头。
1.2 切向畸变(次要但需考虑)
由于镜头制造工艺误差(如镜头安装不平行于成像平面)导致的畸变,表现为物体“倾斜”或“偏移”,比如矩形的边变成倾斜的直线。
二、畸变校正的核心原理
校正的本质是:根据相机的“畸变模型”,将畸变图像中的像素点映射回理想无畸变图像的对应位置。
2.1 关键前提:相机标定
要校正畸变,首先需要知道相机的两个核心参数(通过“相机标定”获取):
- 相机内参矩阵(K):包含相机的焦距(fx, fy)、主点坐标(cx, cy,通常是图像中心),描述相机将3D场景投影到2D图像的内在属性;
- 畸变系数(D):描述畸变程度的参数,OpenCV中通常用5个参数(k1, k2, k3, p1, p2)表示:
- k1, k2, k3:径向畸变系数;
- p1, p2:切向畸变系数。
2.2 校正的数学逻辑(入门无需深究公式)
- 对于畸变图像中的每个像素点(u, v),先通过“畸变模型”计算它在“无畸变图像”中的理想坐标(x, y);
- 用插值算法(如双线性插值)填充理想坐标对应的像素值,得到最终的无畸变图像。
简单说:标定是“获取畸变规律”,校正是“反向应用规律”。
三、入门实战:用OpenCV实现图像畸变校正
本节将通过“棋盘格标定”获取相机参数,再对畸变图像进行校正,全程基于Python+OpenCV,步骤清晰可复现。
3.1 环境准备
1. 安装依赖库
pip install opencv-python numpy matplotlib
(OpenCV是核心,numpy处理矩阵,matplotlib可视化结果)
2. 准备标定数据(关键步骤)
标定需要用“已知尺寸的标定板”,最常用的是棋盘格标定板(黑白相间的正方形格子):
- 自制方法:在Word中插入棋盘格(比如8×6个内角点,格子大小20mm),打印后贴在硬纸板上;
- 拍摄要求:用待标定的相机(如手机、USB摄像头)从不同角度、不同距离拍摄15-20张棋盘格照片,确保棋盘格完整且覆盖图像不同区域(避免全是中心或边缘);
- 数据集结构:创建
calibration_images文件夹,将拍摄的标定图放入其中。
3.2 完整代码实现(分两步:标定+校正)
第一步:相机标定(获取内参和畸变系数)
import cv2
import numpy as np
import os
from matplotlib import pyplot as plt# ---------------------- 1. 配置参数 ----------------------
# 棋盘格内角点数量(横向x,纵向y,注意:不是格子数,是内角点数量)
chessboard_size = (8, 6)
# 棋盘格每个格子的实际尺寸(单位:mm,根据自己的标定板修改)
square_size = 20.0# 标定图路径
calibration_dir = "calibration_images"
image_paths = [os.path.join(calibration_dir, img) for img in os.listdir(calibration_dir) if img.endswith(('.jpg', '.png'))]# 存储棋盘格内角点的世界坐标和图像坐标
obj_points = [] # 世界坐标(3D):(0,0,0), (20,0,0), (40,0,0), ..., (20*(8-1), 20*(6-1), 0)
img_points = [] # 图像坐标(2D):从标定图中检测到的内角点坐标# 生成棋盘格的世界坐标(z轴为0,因为标定板在平面上)
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 # 乘以格子尺寸,转换为实际物理坐标# ---------------------- 2. 检测棋盘格内角点 ----------------------
for img_path in image_paths:# 读取图像img = cv2.imread(img_path)gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 检测棋盘格内角点(cv2.findChessboardCorners)ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)# 如果检测到足够的内角点,进行亚像素级优化if ret:obj_points.append(objp)# 亚像素优化:提高角点检测精度(参数可微调)corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))img_points.append(corners2)# 绘制检测到的角点并显示(可选,用于验证)img_with_corners = cv2.drawChessboardCorners(img, chessboard_size, corners2, ret)cv2.imshow('Chessboard Corners', cv2.resize(img_with_corners, (800, 600)))cv2.waitKey(100) # 每张图显示100mscv2.destroyAllWindows()# ---------------------- 3. 相机标定:计算内参和畸变系数 ----------------------
# 标定核心函数:cv2.calibrateCamera
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None
)# 输出标定结果
print("标定成功!")
print(f"相机内参矩阵(mtx):\n{mtx}")
print(f"畸变系数(dist):\n{dist}")# 保存标定参数(后续校正可直接加载,无需重复标定)
np.savez("camera_calibration_params.npz", mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print("标定参数已保存到 camera_calibration_params.npz")
第二步:利用标定参数进行畸变校正
# ---------------------- 4. 畸变校正实战 ----------------------
def undistort_image(input_img_path, output_img_path, mtx, dist):"""图像畸变校正函数:param input_img_path: 输入畸变图像路径:param output_img_path: 输出校正后图像路径:param mtx: 相机内参矩阵:param dist: 畸变系数"""# 读取畸变图像img = cv2.imread(input_img_path)h, w = img.shape[:2]# 方法1:直接校正(cv2.undistort)- 简单高效undistorted_img = cv2.undistort(img, mtx, dist, None, mtx) # 最后一个参数是新内参(默认用原内参)# 方法2:基于重映射的校正(cv2.initUndistortRectifyMap + cv2.remap)- 灵活(适合视频流)# mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx, (w, h), 5)# undistorted_img = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)# 保存校正后的图像cv2.imwrite(output_img_path, undistorted_img)print(f"校正后的图像已保存到:{output_img_path}")return img, undistorted_img# ---------------------- 5. 测试校正效果 ----------------------
# 加载标定参数(如果已经标定过,可直接加载,跳过前面的标定步骤)
calib_data = np.load("camera_calibration_params.npz")
mtx = calib_data["mtx"]
dist = calib_data["dist"]# 输入畸变图像路径(替换为你的畸变图像)
input_img = "distorted_example.jpg"
# 输出校正后图像路径
output_img = "undistorted_example.jpg"# 执行校正
distorted_img, undistorted_img = undistort_image(input_img, output_img, mtx, dist)# ---------------------- 6. 可视化校正前后对比 ----------------------
# 转换颜色空间(OpenCV默认BGR,matplotlib默认RGB)
distorted_img_rgb = cv2.cvtColor(distorted_img, cv2.COLOR_BGR2RGB)
undistorted_img_rgb = cv2.cvtColor(undistorted_img, cv2.COLOR_BGR2RGB)# 绘制对比图
plt.figure(figsize=(15, 6))
plt.subplot(1, 2, 1)
plt.imshow(distorted_img_rgb)
plt.title("校正前(畸变图像)")
plt.axis("off")plt.subplot(1, 2, 2)
plt.imshow(undistorted_img_rgb)
plt.title("校正后(无畸变图像)")
plt.axis("off")plt.tight_layout()
plt.savefig("correction_comparison.png", dpi=300, bbox_inches="tight")
plt.show()
3.3 代码关键函数解析(入门必懂)
cv2.findChessboardCorners:检测棋盘格内角点,返回是否检测成功(ret)和角点坐标(corners);cv2.cornerSubPix:亚像素级优化,让角点坐标更精准(从像素级到亚像素级);cv2.calibrateCamera:核心标定函数,输入世界坐标和图像坐标,输出内参(mtx)、畸变系数(dist)等;cv2.undistort:直接进行畸变校正,最简单的入门用法;cv2.remap:基于重映射的校正,适合视频流等实时场景(先预计算映射表,再批量校正)。
四、常见问题与解决方法
-
标定失败(ret为False):
- 原因:标定图模糊、棋盘格未完整显示、拍摄角度太少;
- 解决:重新拍摄清晰的标定图(15-20张),确保棋盘格占图像的30%-70%,多角度拍摄(正面、侧面、倾斜)。
-
校正后图像有黑边:
- 原因:畸变校正后,边缘像素映射到图像外,导致黑边;
- 解决:使用
cv2.getOptimalNewCameraMatrix优化内参,裁剪黑边:# 优化内参,去除黑边 newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted_img = cv2.undistort(img, mtx, dist, None, newcameramtx) # 裁剪黑边 x, y, w_roi, h_roi = roi undistorted_img = undistorted_img[y:y+h_roi, x:x+w_roi]
-
校正效果不佳(边缘仍有畸变):
- 原因:标定图数量不足、格子尺寸设置错误、畸变系数未覆盖所有畸变类型;
- 解决:增加标定图数量(≥20张),准确测量棋盘格格子尺寸,尝试使用鱼眼镜头专用标定函数(
cv2.fisheye.calibrate)。
五、入门拓展:无标定的简单校正
如果没有标定板,也可以通过“手动选择校正点”进行简单校正(适合快速验证):
- 在畸变图像中选择4个已知真实形状的点(如矩形的四个角);
- 用
cv2.getPerspectiveTransform计算透视变换矩阵; - 用
cv2.warpPerspective进行透视校正。
示例代码(简化版):
# 无标定快速校正(透视变换)
def simple_undistort(input_img_path):img = cv2.imread(input_img_path)# 手动选择畸变图像中的4个点(如矩形的四个角,按顺序输入)src_points = np.float32([[50, 50], [500, 50], [50, 400], [500, 400]])# 定义理想图像中的4个点(矩形)dst_points = np.float32([[0, 0], [450, 0], [0, 350], [450, 350]])# 计算透视变换矩阵M = cv2.getPerspectiveTransform(src_points, dst_points)# 透视校正result = cv2.warpPerspective(img, M, (450, 350))return result
注意:这种方法仅适用于“透视畸变”(如倾斜拍摄的矩形),无法处理径向畸变,精度较低,适合快速验证场景。
六、总结
图像畸变校正的核心流程是“标定获取参数 → 基于参数校正”,入门阶段重点掌握:
- 两种常见畸变(径向+切向)的概念;
- OpenCV标定函数(
cv2.calibrateCamera)和校正函数(cv2.undistort)的使用; - 标定图的拍摄技巧(确保标定成功的关键)。
对于入门者来说,先通过棋盘格标定完成基础校正,再逐步理解内参、畸变系数的物理意义,后续可深入学习鱼眼镜头畸变校正、实时视频流校正等进阶内容。
如果在实践中遇到问题(如标定失败、校正效果差),欢迎在评论区留言,一起交流解决!
