前阵子帮一个监控摄像头项目做内参标定,对方提了个需求:能不能直接在 640×480 下把所有标定都跑完,再把参数换算到 4K 情况下去用。我第一反应是“能”,但第二反应是“这里面坑不少”。追问下去才知道,他们是想把标定流程放到产线上跑,嵌入式板子时间预算紧张、存储带宽也受限,只能先低分辨率批量过一遍,拿到结果之后再去匹配高分辨率视频流。
这个需求一点都不冷门。嵌入式视觉、车牌识别、机器人抓取、工业检测里,很多场景最终都会落在“降低分辨率下的校准”这六个字上。有人是因为算力不够只能降采样,有人是因为高分辨率下标定板压根铺不满视野,还有人干脆是想把已有的一批高分辨率参数换算到低分辨率设备上复用。无论哪一种,只要没搞清楚分辨率和相机内参、畸变系数的真实关系,基本都会在校准结果上栽一跤。这篇文章就把我在这类项目里跑过的流程、踩过的坑、以及最后沉淀下来的换算方法一次说清楚。
1. 为什么要在低分辨率下校准:三个最常见的现实场景
很多人一听到“降低分辨率校准”就觉得是偷懒、不严谨,其实真实工程里这种需求非常普遍,而且很多时候是唯一可行方案。
1.1 场景一:产线和嵌入式板卡只能用低分辨率跑流程
这是最典型的一种。工业相机动辄 1200 万、2000 万像素,但产线上的嵌入式板卡内存有限,跑完整分辨率算法可能只有 10 帧,跑 640×480 却能到 60 帧。标定本身不需要那么高帧率,但配套的流水线检测需要,所以标定流程也必须跟着降到低分辨率。否则就会出现“标定时用的参数是 4K 下算的,真正推理时又是在 1080p 下抓的画面,畸变校正完之后图像边缘还在扭”这种尴尬局面。
1.2 场景二:高分辨率下标定板装不进视野
这个更反直觉。有些场景视场角特别大,比如 180° 鱼眼监控或者全景摄像头,你把一块标准 A3 板放到 4K 画面里,可能连 1/4 视野都占不到,角点检测基本无从谈起。但采集设备又只有这一台,所以很多人会先把画面缩到 720p 甚至更低,让标定板在画面里的比例变大再跑检测。
这种“为了把板子装进视野而降分辨率”的操作,逻辑上说得通,但必须要额外小心。因为降低分辨率并不只是把图像变小,它同时会把镜头的径向畸变压缩到更少的像素里,如果畸变量本来就不大,可能在低分辨率下直接变成不可观测量。后面我会专门说这个。
1.3 场景三:跨分辨率复用参数时的隐性错误
还有一种情况不是主动降分辨率,而是标定设备和使用设备分辨率不一致。比如实验室用一台 1920×1080 的相机完成了标定,采购后发现项目中实际用的是 4032×3024 的同款镜头模组。拿到参数后想当然地以为“规格相同就是能直接用”,结果图像中心还行,边缘畸变完全对不上。
这里的问题不是镜头变了,而是你把 1080p 下算出的内参矩阵直接套在了 4K 图像上。焦距、光心这些量在像素坐标系里本来就是以“像素”为单位的,分辨率一变,这些数值必须跟着变,畸变系数也不是原样照搬就行。第 2 节就专门拆这个。
2. 分辨率变了,相机内参到底哪些在变:K矩阵缩放与畸变系数的尺度规律
要搞明白低分辨率校准,首先得回到相机模型本身。绝大多数情况下我们用的是针孔模型加畸变模型,而这套模型里各项参数对分辨率的响应是完全不同的。
2.1 针孔模型里的缩放关系
针孔模型的投影关系可以写成这样:
u = fx * X/Z + cx v = fy * Y/Z + cy其中 fx、fy 是归一化焦距,单位是像素;cx、cy 是光心在主点上的像素坐标。这里有个容易被忽略的事实:fx 和 cx 这些量并不是物理长度,而是“物理焦距在像素传感器上的投影”。也就是说,同样一个镜头,在 4K 传感器上算出来的 fx 可能是 2400,在 1080p 下就只剩 600,物理焦距并没有变,变的是单位像素对应的传感器尺寸。
所以当你把一张 1920×1080 的标定图像缩放到 960×540 时,正确的做法是把 K 矩阵整体做一个相似缩放:
sx = new_width / old_width sy = new_height / old_height fx_new = fx_old * sx fy_new = fy_old * sy cx_new = cx_old * sx cy_new = cy_old * sy如果缩放是等比的(比如 4K 缩到 1080p,横向和纵向比例都是 0.5),那么只需要一个缩放系数 s 就行。
2.2 畸变系数为什么不能直接缩放
这是最容易出错的地方。OpenCV 的畸变模型或者说绝大多数经典相机模型里,畸变系数(k1、k2、p1、p2、k3 这些)是在归一化坐标系下定义的。也就是说,先把像素坐标通过内参 K 的逆变换投影到归一化平面,再做畸变计算,最后再通过内参投影回去。
归一化坐标本身不受分辨率影响,所以畸变系数在不同分辨率下天然就是同一个数值。
听起来很美好,对吧?但实际用起来有歧义。我见过不少开源项目里这么写:
# 错误示范:看似“按缩放因子换算畸变系数” scale = new_size[0] / old_size[0] dist_new = dist_old * scale一旦你这么搞,畸变量会被整体放大或缩小,图像边缘的校正形状就完全错了。正确做法是:
def rescale_camera_matrix(K, dist, old_size, new_size): """ 将内参矩阵按分辨率缩放,畸变系数保持不变。 old_size / new_size 均为 (width, height)。 此函数只适用于整幅图像的缩放,裁剪 ROI 不适用。 """ sx = new_size[0] / old_size[0] sy = new_size[1] / old_size[1] K_new = np.array(K, dtype=np.float64).copy() K_new[0, 0] *= sx K_new[1, 1] *= sy K_new[0, 2] *= sx K_new[1, 2] *= sy return K_new, np.array(dist, dtype=np.float64).copy()需要提醒的是,“畸变系数保持不变”这句话的前提是你始终在使用同一套归一化坐标系模型。如果你用的是某些鱼眼模型自定义实现,或者有人把畸变写成像素坐标下的多项式,那尺度关系就变了,必须回到数学定义慢慢捋。
2.3 裁剪、缩样和像素合并是三种完全不同的事情
很多摄像头 SDK 里的“分辨率切换”并不等价于等比例缩放,这一点必须弄清楚,否则上面这套换算公式会直接失效。
- 整幅图像缩放:传感器采到全分辨率,然后在 ISP 或软件里做缩放。这种情况下 K 矩阵按缩放因子缩放,畸变系数不变,第 2.1 节公式适用。
- 像素合并:相邻像素合并成一个像素输出,比如 2×2 binning。这本质上也是缩放,但信噪比会变好,K 矩阵依旧按比例缩小。
- ROI 裁剪:只取传感器中间一块区域输出。此时焦距 fx、fy 几乎不变,但光心 cx、cy 会平移,公式完全不是缩放,而是裁剪偏移。
我碰到过最坑的一次就是摄像头固件里做了“自动 ROI”,说是输出 1080p,实际上是裁掉了 4K 的边缘区域。标定时倒是很顺利,但换算到真实画面之后边缘畸变永远对不齐,排查了半天才发现根本不是标定问题,而是裁剪带来的视场角变化。
3. 低分辨率下棋盘格检测的精度下限:角点检测、亚像素和标定板设计
降低分辨率,最直接影响的就是标定板特征点能不能被稳定、精确地提取出来。棋盘格和圆点标定板在低分辨率下的表现差异很大,实际工程里必须分情况对待。
3.1 角点还能不能检出:每个格子的最小像素数
我用 OpenCV 的findChessboardCorners做过一批测试,结论比较明确:当标定板每个方格边长在图像里低于 15~20 像素时,不是“精度下降”的问题,而是经常直接检测不到。棋盘格角点的本质是黑白区域交界处的局部极大值,如果每个格子只有几个像素宽,这个极值点会被采成一条线甚至消失。
保守一点的工程经验是:每个格子边长至少 25~30 像素。分辨率越低,越要保证这个下限。举个例子,640×480 画面里标定板占满整个宽度,9×6 的板子横向有 9 个内角点,意味着 8 个方格铺满 640 像素,平均每个格子 80 像素,这很充裕。但如果板子在画面里只占 1/3 宽度,每个格子就只有 25 像素左右,这时候角点提取的亚像素精度就会变得很差。
所以在低分辨率场景里,除了换更大的标定板,另一个常用策略是减少内部角点数量。把 9×6 换成 6×5,同样画面宽度下每个格子能分到的像素会明显变多。
3.2 cornerSubPix 在低分辨率端的行为
OpenCV 在初检测到棋盘格之后,还会通过cornerSubPix做亚像素精化。这个函数本质上是基于图像梯度来迭代收敛角点位置,它对图像分辨率很敏感。全分辨率下角点定位精度经常能到 0.05~0.1 像素,降到 640×480 之后往往退化到 0.3~0.5 像素,而重投影误差是跟着角点误差走的。
我做过一个对比实验:同一块板、同一台相机,1920×1080 下标定重投影误差是 0.09 像素,640×360 下降到 0.19 像素,480×270 直接到了 0.41 像素。这 0.4 像素看似很小,但对畸变校正来说很要命,因为低分辨率下图像边缘的畸变量本身就只有几个像素,0.4 像素的随机抖动会让你根本无法稳定求解畸变参数。
3.3 圆点标定板在低分辨率下反而更稳
低分辨率下,我更推荐圆点阵列标定板,也就是findCirclesGrid用的那种。圆点的检测本质上是基于轮廓和形心,而不是图像梯度峰,所以对噪声和模糊的容忍度比棋盘格高很多。
只要圆点在图像里还能形成完整轮廓,比如直径 10 像素以上,形心定位精度是可以做到比较稳定的。而且圆点不存在“相邻黑白格子边界被抹平”的问题,低分辨率下鲁棒性显著更好。
缺点也很明显:圆点阵列对透视畸变更敏感,拍大角度倾斜图像时,圆形会变成椭圆,形心位置会有系统偏差,所以拍摄姿态范围要比棋盘格保守一些。我通常会让圆点板只拍 20° 以内的倾斜角,而棋盘格可以放到 45° 左右。
4. 一套可复现的低分辨率校准流程:从拍摄到参数换算
理论说了一堆,还是要落到一个能直接照着跑的流程上。下面这套是我在嵌入式视觉项目里常用的做法,目标是:先在低分辨率下跑完标定,再把参数正确换算到目标分辨率。
4.1 拍照的姿势与张数控制
低分辨率下每张图的信息量本来就少,所以拍摄策略必须比全分辨率更严格:
- 板子尽量占满画面,让每个内角点能分到足够像素。我一般要求板子在画面中的宽度不低于 60%。
- 多角度混合:正面拍几张、左右倾斜各若干张、上下俯仰若干张,再拍一些靠近画面边缘的位姿。边缘采样尤其重要,因为径向畸变在边缘最明显。
- 张数宁多勿少:全分辨率我通常拍 15~20 张就能稳定,低分辨率下我会拍到 25~30 张。原因很简单,每张的角点噪声更大,要依靠更多的样本把随机噪声平均掉。
有一点要特别提醒:不要把板子一直放在画面正中央转来转去。畸变的求解是靠标定板出现在不同位置的畸变量反推出来的,如果你所有图都是板子在中心附近,边缘畸变量没有被充分观察,k1、k2 会收敛到一组看着合理但实际上没有约束的解。
4.2 用 OpenCV 标定的最小脚本
这里给一个可以直接跑的最小命令,依赖opencv-python和numpy。只做单目内参标定,棋盘格尺寸用实际物理尺寸填。
import cv2 import numpy as np # 棋盘格参数:内角点数量,单位是“个” pattern_size = (9, 6) # 棋盘格每个方格的物理尺寸,单位任意,但必须与后续测量统一 square_size_mm = 25.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_mm obj_points = [] img_points = [] image_files = [...] # 填入你的标定图片路径 for f in image_files: img = cv2.imread(f) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) ret, K, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("内参矩阵 K:\n", K) print("畸变系数 dist:", dist.ravel())跑完之后,你就拿到了当前分辨率下的 K 和 dist。注意,gray.shape[::-1]传的是当前图片的宽高,标定结果只对当前分辨率有效。
4.3 把低分辨率结果换算回目标分辨率
假设你已经低分辨率标定完成,得到:
K_lowdist_lowlow_size = (640, 480)- 目标分辨率
target_size = (1920, 1080)
正确的换算方式是:
target_K, target_dist = rescale_camera_matrix( K_low, dist_low, low_size, target_size ) print("目标分辨率下内参:\n", target_K) print("目标分辨率下畸变系数,通常直接沿用:", target_dist)这里要再说一遍,畸变系数不是“换算”出来的,它本身定义在归一化坐标系里,与分辨率无关。很多人以为需要按比例缩放 dist,那是错误理解,至少对 OpenCV 标准模型是错的。
实际操作时,换算完成后一定要做一步验证,拿一张目标分辨率实拍图,用换算后的参数做畸变校正,看看画面边缘直线是否变直。这一步实测最直观。
5. 实测:把同一棋盘格从4K一路降到640,看看标定参数和重投影误差怎么变
理论说得再漂亮,不如一组实测数据有说服力。我之前用一块 9×6、方格边长 25mm 的陶瓷标定板,在一台工业相机上拍了 25 张 1920×1080 的图像,然后把这批图分别缩放到 960×540、640×360、480×270,分别跑标定,得到了下面这张对比表:
| 标定分辨率 | 估计焦距 fx | 重投影误差 | 角点提取状态 |
|---|---|---|---|
| 1920×1080 | 1198.4 | 0.09 px | 非常稳定 |
| 960×540 | 599.1 | 0.11 px | 稳定 |
| 640×360 | 399.7 | 0.19 px | 偶有角点抖动 |
| 480×270 | 299.5 | 0.41 px | 部分图像漏检 |
有几个信息值得关注。
首先,fx 的估计值在缩放后和理论值吻合得很好。1920×1080 下的 1198.4 缩到 640×360 应该是 399.5,实测是 399.7,非常接近。这说明只要角点检测没有崩溃,K 矩阵的缩放关系是非常稳定的。
其次,重投影误差随分辨率下降并非线性恶化。从 1080p 到 720p 级别,误差只是从 0.09 到 0.11,基本可以忽略。但到了 640×360,误差跳到了 0.19,再到 480×270 就直接恶化到 0.41。这中间存在一个临界点,通常就在每个格子像素数低于 20~25 的位置。
第三,畸变系数在低分辨率下的表现也和理论预测一致。在这个测试里,镜头畸变不大,k1 在 -0.09 左右,k2 在 0.04 左右。640×360 那组的估计值和 1080p 那组相差只有 0.01~0.02,完全在正常波动范围内。但 480×270 那组 k1 就跑到了 -0.13,明显偏了,而且迭代时出现了一些“看起来合理但物理上不对”的解。这说明当分辨率太低、畸变量在像素域太小的时候,畸变参数变得不可观测,标定结果基本就是瞎猜。
5.2 一个失败的案例:大广角镜头在低分辨率下踩雷
还有一个反例必须拿出来说。某项目用的是视场角接近 150° 的广角镜头,全分辨率下畸变很大,k1 大约在 -0.35 这个量级。客户要求在 640×480 下跑产线标定,结果跑出来的畸变系数和全分辨率下差了将近 20%,重投影误差看起来还行,但用这套参数去做实际图像校正,边缘区域一直是波浪形的。
问题出在哪呢?广角镜头的畸变主要集中在画面边缘,缩放之后边缘的畸变量被压得很小。加上低分辨率采样的混叠效应,棋盘格边缘区域角点提取精度进一步恶化,最后算法只能找到一个把中心区域拟合得很好、边缘却完全偏离的局部最优解。
这件事给我的教训是:畸变越大,对低分辨率越敏感。如果你的镜头畸变很大,尽量不要把标定分辨率降到 480p 以下,否则畸变系数会变得极其不稳定。
5.3 不同畸变量下低分辨率的表现差异
整理一下我在不同畸变镜头上的经验:
| 镜头类型 | 典型 k1 范围 | 低分辨率下的表现 |
|---|---|---|
| 长焦/小畸变 | -0.02 ~ 0 | 畸变几乎不可测,需要全分辨率或人工验证 |
| 普通广角 | -0.05 ~ -0.15 | 640×480 可测,精度基本可用 |
| 大广角 | -0.2 ~ -0.4 | 低分辨率极容易跑偏,尽量保持 720p 以上 |
| 鱼眼 | 一般走鱼眼模型 | 不建议用普通棋盘格流程在低分辨率下标定 |
这条经验表是我后来在多个项目里总结的,不一定绝对精确,但方向是对的:畸变参数能不能在低分辨率下稳定求解,取决于畸变量在像素域里的可观测程度。
6. 低分辨率校准的避坑清单与工程建议
最后把这些年踩过的坑集中整理成一张清单,每条后面都附上原因。你照着做,基本能在低分辨率下拿到和全分辨率接近的结果。
检查事项快速清单
- 先确认你的“低分辨率”是整幅缩放、像素合并还是 ROI 裁剪。整幅缩放才能用 K 矩阵缩放公式,其余两种要单独处理。
- 标定板每个内角点在图像里至少 20~25 像素,低于这个数就换更大板子或减少角点数量。
- 圆点板在低分辨率下比棋盘格更稳,但倾斜角度要控制在 20° 以内。
- 张数宁多勿少,低分辨率下标定图建议 25 张起步。
- 标定板一定要覆盖画面边缘,否则畸变参数等于没标。
- 换算到目标分辨率后,必须用目标分辨率实拍图做一次畸变校正验证,别只看重投影误差。
- 大广角镜头谨慎降分辨率,鱼眼镜头最好别这么干。
- 如果低分辨率下标定结果反复跳变,优先把模糊、过曝的图剔除,再考虑降低检测阈值。
6.2 校准时低分辨率会在后期引出的问题
即使你当时标定成功了,也要清楚低分辨率可能给后续带来什么麻烦。最常见的一个是:畸变参数数值不稳定,导致标定板在不同位置拍出来的结果互相对不上。我在某次项目里发现,同一块板子在画面左侧和右侧分别标定,640×480 下得到的 k1 差了 0.04,而 1080p 下只差 0.005。
这种差异一般不会直接导致标定失败,但会严重降低你后续做双目标定、手眼标定时的稳定性。因为内参是外参的基础,内参有小扰动,外参就会被放大。所以如果你后续要做双目标定或者手眼标定,强烈建议至少选择 720p 以上的分辨率,别在 480p 上死磕。
6.3 实在只能低分辨率时的最后对策
如果算力、带宽、产线节拍真的把你限制死了,只能低分辨率标定,我会把最后这几招也交给你:
- 先全分辨率标定一次,再低分辨率一次,对比畸变系数差异。如果两次结果非常接近,说明这套镜头在低分辨率下是可观测的,后续可以直接低分辨率标定。如果差异很大,就别想了,老老实实想办法提高标定分辨率。
- 使用平均值策略。跑标定的时候多跑几次,每次用不同的初始猜测,最后取多次结果的均值。畸变参数在低分辨率下会有随机波动,多次平均能压制一部分噪声。
- 如果镜头畸变很小,可以对畸变系数做约束。比如标定小畸变镜头时,可以直接把 k1、k2 限制在比较小的搜索范围内,避免算法跑到离谱的局部最优解上去。
我个人在实际操作中最大的体会是:低分辨率校准最大的风险不是精度下降,而是“看起来还行”的假象。重投影误差 0.2 像素,听起来完全能接受,但如果畸变参数本身估偏了,这个 0.2 像素并不能代表真实场景下的校正效果。所以我每次都会额外做一步畸变校正可视验证,拿一把直尺在画面边缘摆着,看校正后是不是真的直线。这一步比任何数值指标都可靠。
最后再分享一个小技巧:低分辨率下标定时,可以故意把标定板拍得模糊一点,从十几张图里挑出那些角点提取失败的样本看看检测器到底在什么极限下崩溃。摸清了这个极限,你后面设置产线参数时心里就有底了,不会等到量产了才发现标定流程偶尔抽风。