OpenCV张正友标定法实战:从棋盘格到相机内参完整流程
2026/9/9 11:59:28 网站建设 项目流程

简介:基于OpenCV的相机标定完整工程,围绕张正友标定法展开,面向计算机视觉初学者和需要相机内外参求解、畸变矫正的开发者,解决标定流程中参数计算与图像去畸变问题。工程包含完整C++源码、VS解决方案及棋盘图,可一键运行,实现相机标定、输出内参外参与旋转平移矩阵、标定效果评价,并利用标定结果对原始棋盘图进行矫正。压缩包为RAR格式,共61个文件,大小17.07MB,主要含C++源文件、Visual Studio工程文件、14张JPG与14张BMP棋盘标定图、编译生成的exe及中间文件,并附带标定输入与结果文本,代码与工程结构清晰,便于直接运行和二次开发。目前已有2057人学习,适合对照代码理解张正友标定从角点提取、单应矩阵计算到内参外参求解、畸变系数优化的完整流程。 做视觉项目绕不开相机标定。不管你是做机器人抓取、AR叠加、三维重建,还是简单做个畸变矫正,第一步都是把相机内部参数搞清楚。OpenCV里集成的张正友标定法,是最经典、也是上手最快的一种方案:只要拿一块棋盘格,拍十几张不同角度的照片,跑一段代码就能得到相机内参和畸变系数。这篇文章我就把“棋盘图生成 + 拍照采集 + 代码实现 + 误差评估”整个流程完整拆开讲,附带可以直接跑的Python代码。适合刚接触计算机视觉的初学者,也适合项目中需要自己完成相机标定的开发者参考。

很多人在这一步会卡住:标定板上场就检测不到角点、标定结果重投影误差大、或者拍了一堆图却不知道怎么筛选。这些问题我都会在后面的实操环节里逐一说明。先把最关键的事情说清楚——张正友标定法到底在求什么,又是怎么求出来的。

1. 张正友标定法的原理:为什么棋盘格能标定相机

1.1 标定到底在求什么

相机成像可以理解成把三维世界坐标映射到二维像素坐标。这个过程里有两个“中间商”:一个是相机内部本身的属性,比如焦距、光心位置、像素倾斜程度,这些叫内参;另一个是相机相对于世界坐标系的位置和姿态,也就是旋转和平移,这些叫外参。除此之外,镜头还会引入畸变,最常见的是径向畸变和切向畸变。

张正友标定法的核心目标,就是通过若干张已知图案的照片,把这几个东西全部估计出来。所谓“已知图案”,就是棋盘格——黑白格子交替排列,每个格子的物理尺寸我们知道,比如一格是30毫米,那么棋盘上任意两个角点的三维相对位置就是确定的。既然位置已知,图案又被相机拍成了二维图像,那么图案点和图像点之间就建立了一组“对应关系”。多组对应关系组合起来,就是一个可以解的数学方程系统。

这里有个容易混淆的点:标定求出的内参并不是“测出来的”,而是“算出来的”。相机在出厂时有个理论焦距,但实际镜头安装、光学结构都存在公差,理论值并不准。标定做的事情,就是通过拍棋盘,反推出一组让“三维点投影到像素坐标”这个过程误差最小的内参和畸变参数。

1.2 棋盘格为什么是标定界的“标准答案”

棋盘格在标定场景里几乎是无敌的存在,原因可以归纳为三点。

第一,角点特征极其明显。棋盘格里黑白方块的交界处,就是“角点”。这些点在图像里表现为灰度值的剧烈变化,算法提取时非常稳定,而且亚像素级别的精度也比较容易达到。相比之下,圆点标定板虽然也能用,但在倾斜角度较大时,圆形会变成椭圆,圆心提取的精度会受影响,棋盘格的角点则没有这个问题。

第二,图案信息“自描述”。只要知道棋盘内角点的行列数(比如9×6,指内部角点数量,不是格子数量)和单个格子的物理尺寸,就能完整还原出所有角点的三维坐标。不需要额外设计复杂的编码图案,因为它不需要区分“哪个角点是哪个”,只需要按照行列顺序逐一对应就行——前提是拍照时棋盘没有旋转超过90度导致顺序错乱。

第三,算法成熟,工具链完备。OpenCV里专门封装了找棋盘角点的函数,从findChessboardCornerscornerSubPix再到calibrateCamera,整条流水线开箱即用。这也是大多数项目选择棋盘格而不是其他标定图案的直接原因。

1.3 数学流程:从单应矩阵到内外参分解

张正友标定法的数学流程,可以拆成三个关键步骤,理解这三步,代码里的每个参数你就能真正看懂了。

第一步,求解单应矩阵。把棋盘格放在一个平面上,这个平面在某个世界坐标系下可以视为Z=0。此时,三维点投影到图像像素坐标的变换,就退化成了一个3×3的单应矩阵H。每张棋盘照片都能估计出一个H。这个H里同时混着内参和外参的信息,但它是一步到位能算出来的中间量。

第二步,从单应矩阵分解出内参。由于旋转矩阵有“列向量相互正交且模长为1”的约束条件,把这些约束代入单应矩阵的表达式,就能列出关于内参的方程组。理论上拍两三张不同角度的照片就够解了,但实际中我们通常拍15到20张,引入大量冗余数据来抑制噪声。

第三步,估计畸变系数并整体优化。先用线性方法求出内参和外参的初始值,再把畸变参数加进来,把“三维点投影到像素坐标”的误差作为目标函数,用非线性最小二乘(OpenCV里用的是Levenberg-Marquardt算法)迭代优化所有参数。最终得到的,就是那组让重投影误差最小的参数组合。

提示:张正友标定法的中文名经常被打成“张正有标定法”,搜代码的时候两种写法都会出现,原理上指的是同一个算法。论文原题是《A Flexible New Technique for Camera Calibration》,1998年发表的。

2. 标定前的准备:棋盘图生成、打印与拍摄

2.1 用代码生成一张标准棋盘图

市面上的棋盘图模板质量参差不齐,有的没有留白边,有的网格比例不对。最靠谱的方式是自己用代码生成。下面这段代码用OpenCV生成一张8×6内角点(即9×7个格子)的棋盘图,单格尺寸对应打印后为30mm,A4纸打印即可。

import cv2 import numpy as np # 内角点数 rows=6, cols=8,对应格子为 7x9 rows = 6 cols = 8 square_size = 100 # 像素单位,打印时再按实际尺寸换算 # 图像尺寸:格子数*单格尺寸 + 四周留白 margin = 200 img_h = rows * square_size + margin * 2 img_w = cols * square_size + margin * 2 board = np.full((img_h, img_w), 255, dtype=np.uint8) for i in range(rows + 1): for j in range(cols + 1): if (i + j) % 2 == 0: y1 = margin + i * square_size y2 = margin + (i + 1) * square_size x1 = margin + j * square_size x2 = margin + (j + 1) * square_size board[y1:y2, x1:x2] = 0 cv2.imwrite("chessboard.png", board)

这里的关键参数是rowscols,它们表示的不是黑白格子的数量,而是内部角点的数量。比如8×6的意思,就是横向有8个内部角点、纵向有6个内部角点,对应格子是9×7。这个数字在后续代码里还要用到,两处必须保持一致,否则findChessboardCorners永远找不到角点。

生成图片后,打印时要注意两点:一是尽量用激光打印机,黑白格边缘更锐利,喷墨容易洇墨导致边缘发虚;二是把图片打印到硬纸板或铝板上,保持棋盘平面平整,不要用软纸,尤其是不要在标定过程中弯折它。棋盘弯曲是标定误差的一大来源,而且很难事后排查出来。

2.2 拍摄采集的实操要点

棋盘图准备好之后,拍摄这一步决定了标定质量的上限。拍得太随意,后面代码再优化也救不回来。我自己的经验是遵循几个原则。

数量上,至少拍15张,建议20张左右。太少的话方程约束不足,尤其是畸变参数很难收敛;太多则增加筛选成本,边际收益递减。

角度上,需要覆盖“正对、左右倾斜、上下倾斜、旋转”四种姿态。正对拍几张用来确定焦距和光心的大致位置,然后让棋盘在画面中分别左右倾斜、上下倾斜、绕光轴旋转一定角度。倾斜角度从0度到45度之间分布,但尽量不要出现超过60度的大倾斜,因为大角度下角点检测的亚像素精度会下降。

位置上,让棋盘出现在画面的不同区域——左上角、右下角、中心、边缘都要有覆盖。很多人喜欢把棋盘放在画面正中间拍20张,这会导致边缘区域的畸变参数估计不足。相机标定是一个全局拟合的过程,画面边缘的棋盘照片对畸变系数的约束尤其重要。

还有一个细节:拍摄时棋盘要静止。手持棋盘轻微晃动会导致运动模糊,角点位置提取不准。光线条件也尽量均匀,避免强反光和阴影遮挡格子。如果有条件,用三脚架固定相机、手持棋盘变换姿态,比手持相机去拍固定棋盘更容易控制质量。

2.3 环境快速安装

代码只需要OpenCV和NumPy,安装命令很简单:

pip install opencv-python numpy

如果你用的是Anaconda,也可以:

conda install -c conda-forge opencv

提示:如果只是做标定和图像处理,安装opencv-python就够了,不需要装opencv-contrib-python,后者主要是给SIFT、SURF等需要额外授权的算法用的,体积更大但标定用不上。

3. 核心代码实现:角点检测、标定与去畸变

3.1 角点检测:从findChessboardCorners到亚像素细化

角点检测是整条流水线的第一环,也是最容易出问题的一环。OpenCV提供了一个组合拳:先用findChessboardCorners做初步检测,再用cornerSubPix优化到亚像素精度。

import cv2 import numpy as np import glob # 棋盘内角点数,必须与棋盘图一致 pattern_size = (8, 6) # 存放角点像素坐标和对应世界坐标 obj_points = [] # 世界坐标系下的三维点 img_points = [] # 图像坐标系下的二维点 # 生成棋盘格角点的世界坐标(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) images = glob.glob("calib_images/*.jpg") for fname in images: img = cv2.imread(fname) 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) # 可视化角点,确认检测正确 vis = cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow("corners", vis) cv2.waitKey(200) cv2.destroyAllWindows() print(f"成功检测到角点的图片数: {len(img_points)} / {len(images)}")

findChessboardCorners传入的pattern_size必须和你棋盘的实际内角点数完全一致。经常有人问“为什么我检测不到角点”,八成是这里写错了——比如实际棋盘是9×7个格子,内角点是8×6,代码里却写了(9, 7)。另外,图像不能太大,如果单张图片分辨率超过2000万像素,建议先缩小再检测,否则检测速度很慢,而且在大图上findChessboardCorners的鲁棒性反而可能下降。

cornerSubPix的窗口大小(11, 11)是经验值,它基于角点邻域的灰度梯度来精确定位亚像素位置。窗口太小噪声大,太大又可能跨到相邻格子。实际测试下来11×11的窗口在大多数场景下表现最稳。

3.2 标定主流程:calibrateCamera与参数解读

角点收集完毕,核心调用只需要一行calibrateCamera

# 相机标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("内参矩阵 mtx:") print(mtx) print("\n畸变系数 dist (k1, k2, p1, p2, k3):") print(dist) print(f"\n重投影误差: {ret}")

返回值里每个参数的含义要搞清楚:

  • mtx:3×3内参矩阵,形式是[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fxfy分别是x、y方向上的焦距(像素单位),cxcy是光心坐标。注意fx并不是物理焦距,而是“物理焦距×单位像素尺寸的倒数”,所以不同分辨率的相机,fx数值没有直接可比性。
  • dist:畸变系数向量。前两个是径向畸变k1, k2,中间两个是切向畸变p1, p2,最后一个是高阶径向畸变k3。如果不启用CALIB_RATIONAL_MODELk3通常不会被估计,向量长度为4。
  • rvecstvecs:每张图片对应的外参,即棋盘平面在相机坐标系下的旋转向量和平移向量。
  • ret:重投影误差,单位是像素,这个数值直接反映标定质量,后面细说。

内参矩阵里还有一个容易被忽略的点:gray.shape[::-1]传的是图像尺寸(width, height),顺序不能反。OpenCV里图像shape是(h, w),而标定函数要求的是(w, h),这里写反了会得到完全错误的结果,而且不会有任何报错。

3.3 重投影误差:标定质量的唯一硬指标

calibrateCamera返回的ret就是重投影误差(RMS reprojection error),它计算的是:用标定出来的内参外参,把世界坐标系下的棋盘角点重新投影到图像平面上,与检测到的角点像素坐标之间的平均距离误差。

这个数字是判断标定质量最直接的指标。一般来说:

  • 小于0.1像素,非常优秀,说明图片质量和角点检测都很好;
  • 0.1到0.3像素,正常水平,多数项目可直接使用;
  • 0.3到0.5像素,可以接受,但需要检查是否有模糊或畸变严重的图片混入;
  • 大于0.5像素,说明有问题,要么是棋盘不平整,要么是某几张图角点检测错了,需要重新拍摄。

如果总误差偏大,可以用一个循环逐张计算每张图片的单独误差,把最差的那几张挑出来删掉再重新标定:

total_error = 0 for i in range(len(obj_points)): imgpoints2, _ = cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(img_points[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) total_error += error print(f"第 {i+1} 张图误差: {error:.4f} px") print(f"平均误差: {total_error / len(obj_points):.4f} px")

这个逐张误差分析很有用。有时候20张图里有1张拍糊了,整体误差会被拉高,但你看总体数字又没那么明显。逐张打印出来后,直接删掉误差明显偏大的图片重新标定,质量立刻上一个台阶。

3.4 畸变矫正与去畸变

标定完后,畸变矫正就是最终目的。OpenCV提供了两个常用函数:undistortremap

# 方式一:一步到位 dst = cv2.undistort(img, mtx, dist, None, mtx) # 方式二:先算映射表,再重映射(批量处理时更快) mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx, (w, h), cv2.CV_32FC1) dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

如果你的应用场景是对视频流逐帧去畸变,建议用方式二:映射表只需要计算一次,之后每帧用remap处理,速度比undistort快很多。

这里有个细节值得注意:undistort之后,图像边缘区域会出现黑色的无效区域,因为畸变矫正本质上是“把弯曲的部分拉直”,边缘像素会被外推。如果你做的是视觉测量项目,需要保证矫正后的图像不被拉伸变形,建议用getOptimalNewCameraMatrix计算裁剪后的内参:

h, w = img.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) # 用新内参去畸变 dst = cv2.undistort(img, mtx, dist, None, newcameramtx) # 裁剪掉黑色区域 x, y, w_roi, h_roi = roi dst_cropped = dst[y:y + h_roi, x:x + w_roi]

alpha参数控制保留多少无效像素:alpha=1保留所有像素(含黑边),alpha=0裁剪到不留黑边,alpha在0到1之间则做折中。对于视觉测量项目,alpha=0通常就够了,因为黑边本来就不可用。

4. 常见问题与排查技巧实录

4.1 角点检测不到?从这几个方向排查

findChessboardCorners返回False是新手遇到最多的问题。我整理过一个排查表,按概率排序:

现象可能原因解决方案
所有图片都检测不到pattern_size与棋盘实际内角数不一致数清楚棋盘内角点,修正代码参数
部分图片检测不到棋盘倾斜角度过大或边缘被截断调整拍摄角度,保证所有角点在画面内
图片模糊导致检测失败快门过慢或手抖提高快门速度,或用三脚架固定相机
图像反光强烈光照不均匀或棋盘表面反光调整光源方向,避免强光直射
分辨率太高检测慢图片过大先缩小到2000像素以内再检测

还有一个小技巧:findChessboardCorners有一个flags参数,最常用的是cv2.CALIB_CB_ADAPTIVE_THRESHcv2.CALIB_CB_NORMALIZE_IMAGE。在光照不均的情况下,自适应阈值和图像归一化能明显提升检测率。代价是检测速度稍微变慢,但对于标定这种离线任务完全可以接受。

4.2 重投影误差大?先别急着调代码

误差大的时候,我的第一反应不是改代码,而是回到图片上找原因。经验里最常见的问题是:棋盘纸不平整。很多人随手把棋盘纸打印出来贴在墙上拍,纸的四角会翘起,平面假设被破坏,标定误差自然大。解决方法是贴到硬纸板、亚克力板或者铝板上,保证表面完全平整。

第二个常见问题是用了不同尺寸的棋盘混拍。如果你中途换了一块不同格数的棋盘,而代码里的pattern_size没改,检测会失败;如果你换了一块格数相同但物理尺寸不同的棋盘,代码不会报错,但世界坐标的单位不一致,内参计算结果会乱掉。同一轮标定里,必须使用同一块棋盘、同一个pattern_size

第三个问题是画面中棋盘占比太小。如果棋盘只占画面不到三分之一,角点的像素精度就有限。拍的时候让棋盘尽量占满画面的大部分区域,但同时保证四个边缘角点都在画面内。

4.3 标定结果不稳定的处理思路

有时候你拍了两组照片,标定出来的内参相差很大,这多半是图片集的信息量不够。比如20张图全是近似正对角度,虽然数量多,但约束单一,解出来的内参方差就会很大。解决办法是增加姿态的多样性——左右倾斜、上下倾斜、旋转、平移都要有,而且要保证这些姿态覆盖画面中心到边缘的各个区域。

另外一个容易踩的坑是:不要用视频帧做标定。虽然看起来视频有很多帧,但相邻帧之间姿态几乎相同,等效于一张图片,信息冗余极高。真正有效的标定数据是“姿态差异大”的图片,而不是“数量多”的图片。如果你从视频里抽帧,建议间隔拉开,每帧之间的棋盘姿态要有明显变化。

5. 完整流程梳理与实操心得

整个流程串起来看就四步:生成棋盘图、打印并采集图片、运行检测与标定代码、评估误差并微调。整条链路里,最容易出问题的是图片采集环节,而不是代码环节——这个话我说过很多次,但每次都有同学不相信,直到自己拍了一组差图回来debug半天才信。

我个人在实际操作中的几点体会:

一是打印精度很重要。如果只是做入门实验,普通A4纸打印的棋盘就够了。但如果要做高精度测量,建议找打印店用高精度设备输出,或者购买工业级陶瓷标定板。标定板的物理精度决定了标定的理论极限,代码再复杂也弥补不了物理器件的误差。

二是每次重拍后必须重新标定。相机如果被拆装过、镜头被重新拧过,或者焦距被调整过,之前标定的内参就不能再用了。很多项目在中期出现测量偏差,最后发现是相机被碰了一下导致镜头位置变了,可代码里还在用旧的内参。

三是不要迷信单一指标。重投影误差小,说明标定数据内部自洽,但不代表标定结果就一定准。因为如果存在系统误差(比如棋盘尺寸标错了),重投影误差依然可以很小。谨慎的做法是做一个实际验证:摆一个已知长度的物体,用标定结果测量它,看看测量值和真实值的偏差。

四是把标定做一个独立的工具模块放进项目工程里,而不是每次临时写脚本。我自己的做法是写一个calibrate.py,支持传入图片目录和棋盘参数,输出标定结果的JSON或YAML文件,这样后续程序直接加载结果文件就行,不需要每次都重新标定。OpenCV还提供了cv2.FileStorage,可以把内参和畸变系数直接存成YAML,再在项目里读出来用,非常方便。

结语

张正友标定法虽然已经有二十多年历史,但它依然是工业界和学术界应用最广泛的相机标定方法,原因就是简单、实用、稳定。只要把棋盘图准备好,拍照时多注意姿态和光照,代码跑通后仔细看看重投影误差和逐张误差,基本就能得到一套可用的内参。

最后再分享一个小扩展:棋盘格练熟之后,可以试试OpenCV的findCirclesGrid,用圆点标定板做同样的流程。对比两种图案在倾斜角度下的检测精度差异,能帮助你更深入理解标定算法的原理。标定这件事,动手跑通一遍,比看十篇原理文章都管用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询