4、畸变校正算法实现:cv2.undistort() 与 cv2.initUndistortRectifyMap()
好,咱们进入正题。前面聊了畸变模型和标定,现在该动手实现了。OpenCV 给了我们两条路:一条是懒人专用,一条是性能优先。我个人习惯,在原型阶段用前者,到了嵌入式部署,必须用后者。
4.1 两条路:cv2.undistort() vs cv2.initUndistortRectifyMap()
先看最简单的用法。你标定完了,拿到了相机矩阵和畸变系数,直接一行代码就能出结果:
import cv2
import numpy as np
# 假设你已经有了这些参数
# mtx: 相机内参矩阵, dist: 畸变系数
undistorted_img = cv2.undistort(img, mtx, dist, None, mtx)
嗯,就这么简单。但这里有个坑——cv2.undistort() 每调用一次,内部就会重新计算一次映射表。你想想看,如果你在视频流里每帧都这么干,CPU 直接拉满。我在一个嵌入式项目里就吃过这个亏,当时用 Jetson Nano 跑 30fps 的视频流,直接掉到 8fps。
所以,工程化的做法是:
- 先算好映射表(只算一次)
- 然后用 remap() 去查表(每帧执行)
这就是 cv2.initUndistortRectifyMap() 的用武之地:
# 计算映射表
map1, map2 = cv2.initUndistortRectifyMap(
mtx, dist, None, mtx,
(img.shape[1], img.shape[0]),
cv2.CV_32FC1 # 映射表精度
)
# 每帧只需要执行 remap
undistorted_img = cv2.remap(img, map1, map2, cv2.INTER_LINEAR)
核心区别一句话:cv2.undistort() 是「计算+查表」打包在一起,适合单张图片调试;cv2.initUndistortRectifyMap() + cv2.remap() 是「预计算+运行时查表」分离,适合视频流和嵌入式部署。
4.2 映射表生成原理
说白了,映射表就是一张「像素搬家地图」。它告诉算法:输出图像上 (u, v) 位置的像素,应该从输入图像的哪个位置取。
生成过程分三步:
- 归一化:把输出图像的像素坐标 (u, v) 转换到归一化平面 (x, y)
- 加畸变:根据畸变模型,计算畸变后的归一化坐标 (x_dist, y_dist)
- 反投影:把畸变后的坐标映射回输入图像的像素坐标 (u_src, v_src)
我画了一张图,帮你理解这个流程:
你可能会问:为什么映射表里存的是浮点数?因为畸变后的坐标几乎不可能刚好落在整数像素上。这就引出了下一个问题——怎么取像素值?
4.3 双线性插值与最近邻插值
映射表给了你一个浮点坐标,比如 (23.4, 56.7)。但图像里只有整数坐标的像素。怎么办?插值。
OpenCV 的 cv2.remap() 支持多种插值方式,最常用的就两种:
| 插值方法 | 速度 | 质量 | 适用场景 |
|---|---|---|---|
| cv2.INTER_NEAREST(最近邻) | 极快 | 低(有锯齿) | 实时预览、低功耗设备 |
| cv2.INTER_LINEAR(双线性) | 较快 | 高(平滑) | 大多数场景,默认选项 |
4.3.1 最近邻插值
说白了就是「四舍五入」。坐标 (23.4, 56.7) → 取 (23, 57) 的像素值。简单粗暴,速度快,但图像边缘会有明显的锯齿。
我的经验:在嵌入式设备上做实时畸变校正时,如果帧率不够,可以先切到最近邻插值看看效果。我曾经在一个 ARM Cortex-A72 的项目里,双线性插值只能跑 15fps,换成最近邻直接飙到 28fps。虽然画质差了点,但客户验收时更看重流畅度。
4.3.2 双线性插值
这个就聪明多了。它取目标点周围 4 个像素,按距离加权平均。公式不复杂:
假设目标点 (x, y),周围四个像素:
Q11 = (floor(x), floor(y))
Q12 = (floor(x), ceil(y))
Q21 = (ceil(x), floor(y))
Q22 = (ceil(x), ceil(y))
先沿 x 方向插值两次:
f(x, y1) = (x2 - x)/(x2 - x1) * Q11 + (x - x1)/(x2 - x1) * Q21
f(x, y2) = (x2 - x)/(x2 - x1) * Q12 + (x - x1)/(x2 - x1) * Q22
再沿 y 方向插值一次:
f(x, y) = (y2 - y)/(y2 - y1) * f(x, y1) + (y - y1)/(y2 - y1) * f(x, y2)
嗯,看着公式有点长,但 OpenCV 底层用 SIMD 优化过了,实际跑起来很快。
注意:双线性插值会引入轻微的模糊,相当于一个低通滤波器。如果你的图像本身就很锐利,或者要做后续的特征点提取,可以考虑用 cv2.INTER_CUBIC(三次插值),但速度会慢不少。嵌入式设备上慎用。
4.4 映射表的数据类型选择
这里有个容易被忽略的细节——映射表的数据类型。OpenCV 支持两种:
| 类型 | 精度 | 内存占用 | 速度 |
|---|---|---|---|
| CV_32FC1(两通道浮点) | 高 | 8 字节/像素 | 较慢 |
| CV_16SC2(两通道短整型) | 中 | 4 字节/像素 | 较快 |
我建议:
- PC 端原型验证:用 CV_32FC1,精度高,省心
- 嵌入式部署:用 CV_16SC2,内存减半,查表速度更快
曾经我在一个内存只有 256MB 的摄像头模组上做校正,映射表占了将近 30MB 内存。换成 CV_16SC2 后,直接降到 15MB,系统终于不 OOM 了。
4.5 完整代码示例
最后,给你一个完整的工程化代码模板:
import cv2
import numpy as np
class DistortionCorrector:
def __init__(self, mtx, dist, img_size, map_type=cv2.CV_16SC2):
self.mtx = mtx
self.dist = dist
self.img_size = img_size
# 预计算映射表
self.map1, self.map2 = cv2.initUndistortRectifyMap(
mtx, dist, None, mtx,
img_size, map_type
)
def correct(self, img, interpolation=cv2.INTER_LINEAR):
return cv2.remap(img, self.map1, self.map2, interpolation)
# 使用示例
cap = cv2.VideoCapture(0)
corrector = DistortionCorrector(mtx, dist, (640, 480))
while True:
ret, frame = cap.read()
if not ret:
break
# 畸变校正
corrected = corrector.correct(frame)
cv2.imshow('Corrected', corrected)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
工程化要点总结:
- 映射表只算一次,放在 __init__ 里
- remap() 放在循环里,每帧只查表不计算
- 嵌入式环境用 CV_16SC2 节省内存
- 帧率不够时先降级到最近邻插值
嗯,这一章的内容就到这。映射表生成和插值选择,说白了就是「空间换时间」和「精度换速度」的权衡。你在实际项目中多试几次,自然就找到感觉了。