简介张正友相机标定Opencv实现实例源码面向机器视觉初学者与OpenCV开发者解决相机标定从原理到代码落地的难题。程序基于张正友标定法提供完整VS工程与详细注释的C源码随附棋盘图和14张不同角度标定图一键运行即可完成相机标定输出内参、外参、旋转和平移矩阵并自动进行标定效果评价与图像矫正便于直观理解亚像素角点提取、单应矩阵求解和畸变系数优化等关键环节。压缩包共92个文件以cpp源码、sln/vcxproj工程文件、bmp棋盘图与jpg标定图为主辅以txt标定结果记录和可直接运行的exe程序整体仅10.26MB目录结构清晰适合直接编译调试。已有22738人学习下载无论入门相机标定还是基于OpenCV做视觉开发都能从中获得可复用的工程参考也可依据注释与结果文档反推算法步骤加深对相机模型的理解。1. 张正友相机标定 OpenCV 实现20 张棋盘图换一套可信的内参很多视觉项目刚起步就栽在相机标定上画面边缘的直线是弯的测出来的距离偏几个毫米AR 模型怎么摆都贴不到实景上。用 OpenCV 复现张正友相机标定完整程序棋盘图是解决这类问题最标准的一条路——拿一台相机对着棋盘格拍 15 到 20 张不同角度的照片跑一遍角点检测再用calibrateCamera把内参、畸变系数和每张图的外参一次性算出来。下面要讲的这套流程新手能一步步跑通第一个标定程序熟手可以对照边界条件和排错思路去诊断自己的标定为什么不稳。我会把棋盘图生成、拍摄规范、完整代码、重投影误差验证到高频踩坑点都铺开讲尽量不让你在同一个坑里翻两次车。2. 标定前必须吃透的相机模型与张正友方法的核心思路2.1 针孔模型里内参外参分别管什么畸变系数为什么非要矫正相机成像近似小孔成像三维世界里的点先经过外参变换到相机坐标系再由透视投影落到图像平面最后被像素坐标系缩放和平移。内参矩阵 K 是 3x3 的上三角矩阵fx、fy是焦距换算到像素的尺度cx、cy是主点位置。外参指的是每一张图像拍摄时标定板平面相对相机光心的旋转向量和平移向量在 OpenCV 中标定完成后会以rvecs和tvecs返回。很多初学者把注意力全放在内参上但漏了一个事实内参是所有视角共享的外参则每张图各自不同重投影误差正是在这两组参数共同作用下算出来的。真实镜头还存在畸变。径向畸变让直线在画面中心向外鼓或向内收建模参数是 k1、k2必要时加 k3切向畸变源于镜头与传感器不完全平行参数是 p1、p2。OpenCV 的calibrateCamera默认会估计 k1、k2、p1、p2、k3 五个畸变系数但对普通工业镜头和网络摄像头k3 往往数值很小且不稳定强行估计反而会让结果抖动。我一般习惯用CALIB_ZERO_TANGENT_DIST把切向畸变固定为零只估计 k1、k2这样在照片数量不够多时更稳。2.2 张正友方法为什么用平面棋盘格单应性怎么帮我们解参数张正友标定法有两个关键前提标定板是平面以及棋盘格角点在图像里能被精确提取。把世界坐标系放在棋盘平面上所有角点的 z 坐标都是 0这样三维点到图像点的映射就退化成一个 3x3 的单应矩阵 H。每一张棋盘图能解出一个 H而 H 内部同时包含内参 K 和该视角的外参 [R|t]。不同视角的 H 共享同一个 K把这些约束联立起来先线性求解初值再用最大似然思想做非线性优化最终得到一张或多张图像共同约束下的最优内参和畸变系数。这个思路比早期标定方法实用得多因为它不要求你知道相机移动的轨迹也不需要两块互相垂直的标定板。棋盘格角点检测本身又很稳定findChessboardCorners找到角点后还能用cornerSubPix细化到亚像素精度输入数据的噪声就能压到很低。理解单应性还有一个额外收益当你发现标定结果不对时可以直接怀疑是某个视角的 H 异常而不必盲目怀疑算法本身。2.3 标定结果下游用在哪几个环节提前知道才能验证对不对标定结果不是终点它是给三类下游任务供数据的。第一类是去畸变直接用cv2.undistort矫正整幅图适合显示、拼接和特征匹配如果你已经在两张图里各自提取了特征点只想在坐标层面去畸变该用的是cv2.undistortPoints两者不能乱替代。第二类是位姿估计用solvePnP配合已知的 3D 点和当前图像的 2D 点实时解出标定板或目标物体相对相机的位姿机械臂抓取、AR 叠加都靠这一环。第三类是度量重建有了内参才能把像素坐标还原成归一化相机坐标再做三角化、深度估计和三维重建。当你做完整套标定后至少要回答一个问题我要把它交给哪个环节如果只是去畸变显示重投影误差到 0.3 像素就够用如果是机械臂视觉引导就要尽可能压到 0.1 像素以下。不同精度目标对应的采集张数和姿态要求完全不同这也是后面几章反复强调拍摄规范的原因。3. 棋盘标定图生成与拍摄规范从打印到采集的关键细节3.1 用 OpenCV 生成一张可直接打印的棋盘图标定棋盘可以买现成的也可以自己生成打印。自己生成的自由度更高内角点数量、格子边长、留白都可以按需定制。下面这段代码用 NumPy 和 OpenCV 生成一张 8x11 内角点的 A4 棋盘图直接保存为 PNG 就能送去打印。import numpy as np import cv2 inner_cols, inner_rows 8, 11 # 内角点数量宽方向8个高方向11个 pixel_per_square 150 # 每个格子的像素边长 margin 80 # 四周留白像素打印时防止边缘被裁切 # 实际格子数 内角点数 1 squares_x, squares_y inner_cols 1, inner_rows 1 board_w squares_x * pixel_per_square board_h squares_y * pixel_per_square # 白底画布先填充255黑色格子在循环里填0 board np.full((board_h 2 * margin, board_w 2 * margin), 255, dtypenp.uint8) for i in range(squares_y): for j in range(squares_x): if (i j) % 2 0: # 行列和为偶数时填黑 y0 margin i * pixel_per_square y1 margin (i 1) * pixel_per_square x0 margin j * pixel_per_square x1 margin (j 1) * pixel_per_square board[y0:y1, x0:x1] 0 cv2.imwrite(chessboard_A4.png, board) print(棋盘图已保存)这段代码的核心逻辑是先建一个全白画布再按行列坐标把偶数格填成黑色。inner_cols和inner_rows指的是程序要检测的内角点数量不是棋盘格子的总量实际格子数是内角数加 1。例如 8x11 内角点对应 9x12 个格子检测时findChessboardCorners传入的也是 (8, 11)。pixel_per_square影响打印清晰度150 像素在 A4 纸上大约对应 7 毫米左右的格子角点在画面里能有十几个像素宽足够稳定检测。3.2 打印、贴板与拍摄时的参数要求和数量标准打印棋盘的关键不是纸张类型而是平整度。光面纸容易反光普通复印纸容易皱我一般用激光打印到偏厚的哑光铜版纸再贴到亚克力板或硬纸板上。棋盘一旦有波浪形弯曲角点位置会系统性偏移标定出来的内参会随之跑偏这一步省不得。格子实际边长要用尺子量标定时传入的SQUARE_SIZE直接影响外参中的平移量尺度对单目内参没影响但如果你后续要做三维测量就必须给真实值。拍摄数量我有两个标准最低 15 张推荐 20 到 25 张。少于 10 张时结果基本只能靠运气。拍摄时让棋盘出现在画面的中心、四周、角落不同位置并且每张图都让棋盘绕 x 轴或 y 轴有一个明显的倾斜角度。不要只拿着棋盘在镜头前平移那样所有视角几乎共面约束不足标定出来的焦距和主点会互相补偿结果看着合理但一换场景就失效。另一个容易忽视的点是棋盘格在画面中要占到 1/3 以上面积太小了角点之间的像素间距不足亚像素细化的精度也会打折。3.3 采集图像的命名管理与质量初筛采集回来的照片先不要直接喂给标定程序花两分钟做一次质量初筛能省掉后面大量排错时间。我习惯把图片放到images/目录下统一命名为image_01.jpg、image_02.jpg这种格式方便排序和归档。然后运行一个批量缩略图脚本把 20 张图拼成一张总览图快速检查哪些图模糊、反光或遮挡。import glob import cv2 import numpy as np files sorted(glob.glob(images/*.jpg)) thumb_cols 5 thumb_h, thumb_w 240, 320 rows (len(files) thumb_cols - 1) // thumb_cols sheet np.full((rows * thumb_h, thumb_cols * thumb_w, 3), 255, dtypenp.uint8) for idx, f in enumerate(files): img cv2.imread(f) if img is None: continue t cv2.resize(img, (thumb_w, thumb_h)) r, c divmod(idx, thumb_cols) sheet[r*thumb_h:(r1)*thumb_h, c*thumb_w:(c1)*thumb_w] t cv2.putText(sheet, f.split(/)[-1], (c*thumb_w5, r*thumb_h20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(preview_grid.jpg, sheet)这个脚本把图片压缩成 320x240 的缩略图按 5 列排成网格并在每张缩略图左上角标注文件名。检查时重点看三件事棋盘四角有没有被切掉棋盘表面有没有高光反光整张图有没有运动模糊。反光和模糊的图在角点检测阶段不容易报错但会把角点坐标带偏让重投影误差虚高。发现不合格的直接从目录里删掉补拍时注意让新图和原有图的姿态尽量不重复。4. 用 OpenCV 跑通张正友标定的完整程序角点检测、calibrateCamera 与参数解读4.1 角点提取与亚像素细化找得准是标定的地基标定程序的质量上限由角点坐标的精度决定这一步做不好后面所有优化都是白搭。findChessboardCorners返回的角点坐标是像素级别的整数直接拿去标定也能出结果但误差会明显偏大。更稳的做法是把它返回的初始角点传给cornerSubPix在局部窗口内用梯度信息把坐标细化到亚像素精度。下面是角点检测的核心片段。# 内角点数量与生成棋盘图时的参数保持一致 BOARD_SIZE (8, 11) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ret, corners cv2.findChessboardCorners(gray, BOARD_SIZE, None) if ret: corners2 cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)BOARD_SIZE传的是内角点数量形参顺序是 (宽, 高)也就是列方向 8 个、行方向 11 个和生成棋盘图时的inner_cols, inner_rows一一对应。cornerSubPix的第三个参数是搜索窗口大小常用 (5,5) 到 (11,11)窗口太小细化不充分太大又可能越过角点区域。criteria表示迭代终止条件30 次迭代或精度达到 0.001 像素就停。光照不均时可以在findChessboardCorners里额外传cv2.CALIB_CB_NORMALIZE_IMAGE让算法先对图像做归一化能明显提高检出率。4.2 调用 calibrateCamera 完成求解并解读返回参数角点收集完成后调用calibrateCamera一次就能得到内参、畸变系数和所有视角的外参。这里给出一套完整的标定主程序包含三维世界点构造、图片遍历、角点检测、标定求解和结果保存可以直接放在项目目录里跑。import cv2 import numpy as np import glob import os # ---------- 参数区 ---------- BOARD_SIZE (8, 11) # 内角点数量 SQUARE_SIZE 0.025 # 格子物理边长单位米 CRITERIA (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 构造棋盘格所有内角点的三维坐标z 恒为 0 objp np.zeros((BOARD_SIZE[0] * BOARD_SIZE[1], 3), dtypenp.float32) objp[:, :2] np.mgrid[0:BOARD_SIZE[0], 0:BOARD_SIZE[1]].T.reshape(-1, 2) objp * SQUARE_SIZE objpoints [] # 每张图对应的三维点集合 imgpoints [] # 每张图对应的二维角点集合 image_size None # ---------- 遍历图像检测角点 ---------- for fname in sorted(glob.glob(images/*.jpg)): img cv2.imread(fname) if img is None: continue gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if image_size is None: image_size gray.shape[::-1] # 注意要传 (width, height) ret, corners cv2.findChessboardCorners(gray, BOARD_SIZE, None) if not ret: print(跳过未找到角点:, fname) continue corners2 cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), CRITERIA) objpoints.append(objp) imgpoints.append(corners2) # 保存带角点标注的预览图方便排查 viz img.copy() cv2.drawChessboardCorners(viz, BOARD_SIZE, corners2, ret) cv2.imwrite(corners_ os.path.basename(fname), viz) if len(objpoints) 10: raise SystemExit(有效标定图不足 10 张请重新采集) # ---------- 标定求解 ---------- flags cv2.CALIB_ZERO_TANGENT_DIST ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, image_size, None, None, flagsflags) # ---------- 重投影误差 ---------- per_frame [] all_errs [] for i in range(len(objpoints)): proj2d, _ cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) errs np.linalg.norm(imgpoints[i].reshape(-1, 2) - proj2d.reshape(-1, 2), axis1) per_frame.append(errs.mean()) all_errs.extend(errs) rms float(np.sqrt(np.mean(np.square(all_errs)))) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel()) print(参与标定图片数:, len(objpoints)) print(每张平均误差:, [round(v, 3) for v in per_frame]) print(全局RMS误差: %.4f px % rms) np.savez(calib_result.npz, mtxmtx, distdist, rvecsrvecs, tvecstvecs) print(标定结果已保存到 calib_result.npz)这段程序里最关键的是objp的三维点构造顺序。np.mgrid[0:8, 0:11]生成的坐标序列必须和findChessboardCorners返回的角点顺序一致OpenCV 默认从左到右、从上到下扫描ungrid 的展开方向正好匹配。image_size必须传(width, height)即gray.shape[::-1]如果传成gray.shape变成 (高, 宽)标定不会报错但内参和畸变系数会跟着出错且很难察觉。flags参数是标定中非常值得花时间调的一项下表列出常用标志位。flags 参数作用适用场景默认None自由估计 k1, k2, p1, p2, k3 五个畸变系数图像质量好、数量充足CALIB_ZERO_TANGENT_DIST把切向畸变固定为 0镜头装配合格、想减少参数量CALIB_FIX_INTRINSIC保持内参不变只优化畸变和外参已知内参只做现场标定CALIB_FIX_ASPECT_RATIO固定 fx/fy 比例视角不足、需要约束条件CALIB_FIX_PRINCIPAL_POINT固定主点位置图像中心与主点偏差已知我在大多数项目里用CALIB_ZERO_TANGENT_DIST换来的是程序更稳因为切向畸变在普通镜头中通常小于径向畸变一个数量级。鱼眼镜头或广角畸变严重的镜头五个畸变系数都保留反而更好此时图像要拍到 30 张以上否则优化容易过拟合。4.3 重投影误差当场判断这轮标定能不能用calibrateCamera返回的ret只是算法是否收敛的数值解不能直接代表标定质量。判断标定行不行要看重投影误差把三维世界点用求解出的内参、畸变和外参重新投影回图像平面再和检测到的原始角点坐标求距离差。这个误差以像素为单位是标定精度的金标准。上面的程序里每张图的平均误差被打印出来看它要遵循三个步骤。先看全局 RMS 是否达到应用阈值普通镜头低于 0.1 像素是很好的结果0.1 到 0.3 可用超过 0.5 基本要返工。再看单帧误差有没有离群值如果 20 张图里 19 张在 0.1 左右只有一张是 1.5说明那一张图可能反光或轻微模糊直接删掉重跑。最后看dist中的 k1 符号负值是桶形畸变正值是枕形畸变如果符号与镜头类型明显不符多半是数据出了问题。提示重投影误差只是内恰好反映“参与标定的图”拟合得多好。它不能代替真实场景验证换一个没有参与过标定的场景拍直线矫正后看直线是否变直才是最终说服自己的方式。5. 标定避坑与常见问题排查反复翻车的四个现场5.1 同一块棋盘换个角度就找不到角点检测像玄学现象同一个棋盘格正面拍能检测到角点稍微倾斜一点就findChessboardCorners返回 False连续试几个角度都失败标定程序因此中断。原因棋盘格是黑白方块对称排列的当棋盘旋转约 180 度时出现的图案和原来一样角点检测存在方向歧义。另外光照不均和图像噪声也会导致二值化阈值选不中正确格线棋盘在画面中占比太小同样是常见诱因。解决最简单的方法是换用非对称的棋盘图案比如 8x11 和 9x11 这种行列不同奇偶的规格必要时在棋盘图案边上贴一个黑色标记人为打破对称性。检测时给findChessboardCorners开启预处理标志位cv2.CALIB_CB_ADAPTIVE_THRESH配合cv2.CALIB_CB_NORMALIZE_IMAGE能解决大部分光照问题。先把标志位加上再重新检测比反复调整镜头位置更值得试。5.2 内参看起来正常但畸变矫正后边缘直线反而更弯现象标定结果中 fx、fy、cx、cy 都在合理范围内重投影误差也低于 0.3但用undistort矫正实际场景照片时画面边缘的直线反而弯曲得更严重。原因这是典型的“拟合了数据分布的中心而丢掉了覆盖范围”。如果采集的棋盘格只集中在画面中心calibrateCamera对边缘畸变几乎没有约束它输出的一组参数在中心区域误差很小一到边缘就外推失败。解决重新采集时要保证棋盘格出现在画面的四角和边缘位置角落区域的图至少占三分之一。检查方式很直接把参与标定的标记图按文件名排序用 3.3 节的缩略图脚本看一遍统计棋盘在画面四角出现的次数。如果某个角落一次都没出现过畸变模型在那个区域就是纯外推重投影误差再漂亮都不要信。5.3 标定出的主点 cx、cy 严重偏离图像中心现象图像分辨率是 1920x1080标定出的cx接近 1200、cy接近 300和图像中心偏差超过 10%。虽然全局误差不大但用矫正图时总感觉画面不对称。原因cx、cy在非线性优化中和焦距、畸变系数之间存在耦合。当数据里棋盘格总是偏在画面同一侧时优化器会通过移动主点来补偿视角偏差导致主点被“拉”到棋盘最常出现的那一侧。角点顺序错误也会有类似表现如果objp的生成顺序和图像角点顺序不匹配标定结果通常是主点和畸变系数同时异常。解决先把objp顺序和角点顺序这一项排除掉检查方法是用drawChessboardCorners生成的标记图看角点连线是否横平竖直如果出现斜向错乱说明三维点和二维点没对齐。再看数据分布是否均匀尽量让棋盘在左右和上下各半场交替出现。如果项目要求高精度且主点必须稳定可以在flags里加CALIB_FIX_PRINCIPAL_POINT但前提是你对主点位置有可靠的先验。5.4 每次标定结果差异都很大换一批照片结果就变现象同样的相机和棋盘分两批各拍 20 张图分别标定得到的焦距相差 20%重投影误差也忽高忽低感觉结果不稳定。原因最直接的诱因是标定图像之间的姿态重复度太高所有棋盘都几乎是正面朝向相机只有轻微平移外参自由度没有被充分激发。另一个常见情况是棋盘本身轻微弯曲或者相机对焦没有锁死在奇数导致角点锐度变化。参数过拟合也常见照片只有 12 张却开启了五个畸变系数加四个内参总共九个未知量数据量根本喂不饱。解决先保证姿态多样性让棋盘分别绕 x 轴和 y 轴倾斜 30 度以上并加入几张棋盘旋转 90 度的照片。再检查角点标注图如果同一边缘处的角点坐标明显抖动说明对焦或棋盘平整度有问题。最后把flags收敛一点比如固定切向畸变为零减少自由参数量能明显提高不同批次结果的一致性。一个实用习惯是每次标定完保留当时的照片和结果下次标定后直接对比内参矩阵理想情况差异应在 1% 以内。6. 进阶标定质量交叉验证与工程化落地习惯6.1 用留一法验证标定泛化能力重投影误差衡量的是“参与拟合的图拟合得多好”真实验证要看没参与拟合的图表现如何。留一法最简单从 20 张图里抽出 2 张不参与标定用剩下 18 张标定出内参和畸变再把这些参数作用到抽出的 2 张图上算重投影误差。如果这个误差明显高于参与标定图的误差说明当前模型过拟合了数据量不足或姿态分布有问题。实际操作中我会把每种姿态组合各抽一张而不是只抽同一个角度的图更能反映数据准备的真实质量。6.2 用矫正栅格图直观确认标定效果数值指标之外肉眼检查永远值得做。把标定出的参数传入initUndistortRectifyMap和remap生成畸变矫正图然后在屏幕上对比矫正前后的边缘直线。mapx, mapy cv2.initUndistortRectifyMap( mtx, dist, None, None, image_size, cv2.CV_32FC1) undist cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR) cv2.imwrite(undist_compare.jpg, np.hstack([img, undist]))把矫正前后的图并排保存找门框、桌面边缘这类直线结构看是否变直。如果中心区域很直但边缘仍然弯曲基本可以确定是数据覆盖不全而不是参数求解的问题。6.3 参数保存、日志与抽检的习惯np.savez保存的二进制结果适合程序快速加载但人没法一眼看懂。我会在同目录生成一份文本日志记录哪一组图片、用了什么 flags、全局 RMS 和各帧误差。这样两周后回来不必重新跑程序就能知道这批参数是怎么来的出了问题也能快速排查。一个值得养成的习惯是每次换分辨率、换镜头或重新安装相机后一律重新标定不要复用旧参数。并在标定日志里写上相机的序列号后四位和采集日期。这样一段时间的参数积累下来你能清楚看到同一台相机在温度变化、镜头松动等情况下的参数漂移情况这是标定结果价值最大的用法之一。希望帮到你。本文还有配套的精品资源点击获取