ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV Python calib3d 实战:相机标定、姿态估计、极线几何与立体视差完整指南

OpenCV Python calib3d 实战:相机标定、姿态估计、极线几何与立体视差完整指南 OpenCV Python calib3d 实战相机标定、姿态估计、极线几何与立体视差完整指南【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv本文基于 OpenCV 官方 Python 教程中「Camera Calibration and 3D Reconstruction」这一章的目录文档整理成篇围绕 calib3d 模块的四大核心主题展开相机畸变模型与标定、基于 PnP 的三维姿态估计、双目标定中的极线几何以及由立体图像求取深度图。读完本篇你将掌握cv.calibrateCamera、cv.undistort、cv.solvePnP、cv.findFundamentalMat、cv.StereoBM等关键函数的完整调用流程与参数含义并能独立完成从棋盘格标定到三维渲染、立体匹配的工程实践。1. calib3d 模块的文档体系与代码组织本章的入口是 calib3d 教程目录它通过 Doxygen 的subpage指令把整章拆分为四个子页分别对应四个独立的技术主题子页主题官方文档核心能力tutorial_py_calibration相机标定py_calibration.markdown找出相机畸变、求解内参/外参、对图像去畸变tutorial_py_pose姿态估计py_pose.markdown利用标定结果在图像中叠加 3D 坐标轴、渲染立方体tutorial_py_epipolar_geometry极线几何py_epipolar_geometry.markdown理解极线约束求基本矩阵与极线tutorial_py_depthmap立体深度图py_depthmap.markdown从两幅图像提取视差并推导深度这四个教程的底层实现都位于仓库的 modules/calib3d 模块中其中findChessboardCorners、calibrateCamera、solvePnP、findFundamentalMat、StereoBM等函数的 C 实现、性能测试与 Python 绑定均在该目录下。各子教程引用的演示图片也分别存放在各子页的images/目录中例如 径向畸变示例图、标定结果图。2. 相机标定畸变模型、角点检测与去畸变本节内容继承自 py_calibration.markdown。官方教程给出的目标是三点了解相机造成的畸变类型、求出相机的内参与外参、基于这些参数对图像去畸变。2.1 畸变模型径向畸变与切向畸变针孔相机会给图像引入两类主要畸变径向畸变Radial distortion使直线看起来弯曲离图像中心越远的点畸变越大。官方教程给出的示例图中棋盘格边缘本应是直线却整体向外鼓起。径向畸变可表示为x_distorted x * (1 k1*r^2 k2*r^4 k3*r^6) y_distorted y * (1 k1*r^2 k2*r^4 k3*r^6)切向畸变Tangential distortion由于成像透镜没有与成像面完全平行而产生导致图像中某些区域看起来比预期更近。其表达式为x_distorted x [2*p1*x*y p2*(r^2 2*x^2)] y_distorted y [p1*(r^2 2*y^2) 2*p2*x*y]综合两类畸变我们需要求出五个畸变系数(k1, k2, p1, p2, k3)。除畸变系数外标定还需要两组参数内参Intrinsic parameters与相机本身绑定的参数包括焦距(fx, fy)与光心(cx, cy)可组成 3×3 相机矩阵camera matrix [ fx 0 cx 0 fy cy 0 0 1 ]相机矩阵对同一台相机是唯一的求出后可在所有该相机拍摄的图片上复用。外参Extrinsic parameters旋转与平移向量用于把 3D 点坐标转换到相机坐标系。对立体应用来说这些畸变必须先校正。求解思路是提供若干张含有已知图案如棋盘格的样本图像找出其中相对位置已知的特征点棋盘格的方格交点。由于这些点在真实世界空间与图像中的坐标都已知即可解出畸变系数。官方教程强调为获得较好结果至少需要 10 张有效测试图案。2.2 收集角点object points 与 image points官方示例使用了仓库 samples/data 目录中自带的棋盘格图片left01.jpg到left14.jpg。2D image points可以直接从图像中检测。棋盘格中两个黑色方格互相接触的交点即为目标点。3D object points的处理技巧在于那些照片是用固定相机拍摄、棋盘格放在不同位置和姿态下得到的。为了简化可以反向假设棋盘格静止在 XY 平面上Z 恒为 0、而相机在移动这样只需给出 X、Y 值。若以棋盘格方格边长为尺度单位可直接传入(0,0), (1,0), (2,0), ...如果知道方格真实尺寸如 30 mm则传入(0,0), (30,0), (60,0), ...结果单位即为毫米。官方示例的作者不掌握原始方格尺寸因此以方格边长为单位传入。3D 点称为object points2D 点称为image points。角点检测分三步cv.findChessboardCorners()寻找棋盘格角点需传入图案尺寸本例为 6×7 内部角点网格普通棋盘格是 8×8 方格、7×7 内部角点。返回 retval是否找到与按从左到右、从上到下排序的角点cv.cornerSubPix()对角点做亚像素级精化cv.drawChessboardCorners()绘制图案以便检查。官方教程特别提示单帧图像未必都能找到图案稳健的做法是启动相机逐帧检测找到图案后存点、间隔一段时间再读下一帧以便把棋盘格调整到不同方向直到凑足足够数量的有效图案。本例对 14 张图逐一读取、只保留检测成功的。此外官方教程注明也可以改用圆形网格图案此时必须用cv.findCirclesGrid()检测且所需图像数量更少。完整的角点收集代码如下完整继承官方教程代码块import numpy as np import cv2 as cv import glob # termination criteria criteria (cv.TERM_CRITERIA_EPS cv.TERM_CRITERIA_MAX_ITER, 30, 0.001) # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(5,6,0) cols 6 rows 7 objp np.zeros((cols*rows,3), np.float32) objp[:,:2] np.mgrid[0:cols,0:rows].T.reshape(-1,2) # Arrays to store object points and image points from all the images. objpoints [] # 3d point in real world space imgpoints [] # 2d points in image plane. images glob.glob(*.jpg) for fname in images: img cv.imread(fname) gray cv.cvtColor(img, cv.COLOR_BGR2GRAY) # Find the chess board corners ret, corners cv.findChessboardCorners(gray, (cols, rows), None) # If found, add object points, image points (after refining them) if ret True: objpoints.append(objp) corners2 cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria) imgpoints.append(corners2) # Draw and display the corners cv.drawChessboardCorners(img, (cols, rows), corners2, ret) cv.imshow(img, img) cv.waitKey(500) cv.destroyAllWindows()其中criteria (cv.TERM_CRITERIA_EPS cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)表示角点精化在迭代 30 次或精度优于 0.001 像素时终止(11,11)是搜索角点邻域大小的奇数窗口(-1,-1)表示整个图像都参与搜索。检测成功后的角点绘制效果可参见官方教程中的 calib_pattern.jpg。2.3 执行标定calibrateCamera拿到 object points 和 image points 后即可执行标定。cv.calibrateCamera()返回相机矩阵、畸变系数、各帧的旋转与平移向量等ret, mtx, dist, rvecs, tvecs cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)ret是重投影误差gray.shape[::-1]得到(宽, 高)形式的图像尺寸最后两个None表示没有初始相机矩阵与畸变系数可供迭代由算法从零估计。2.4 去畸变两种等价方法去畸变前可先用cv.getOptimalNewCameraMatrix()基于一个自由缩放参数 alpha 优化新的相机矩阵alpha0返回不含多余无效像素的去畸变图像但可能裁掉图像四角的一些像素alpha1保留全部像素代价是画面边缘出现部分黑色填充。该函数同时返回一个 ROI可用于裁剪结果。官方示例取alpha1对新一张图left12.jpg处理img cv.imread(left12.jpg) h, w img.shape[:2] newcameramtx, roi cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))方法一直接使用cv.undistort()最简单的做法调用函数后用上面的 ROI 裁剪# undistort dst cv.undistort(img, mtx, dist, None, newcameramtx) # crop the image x, y, w, h roi dst dst[y:yh, x:xw] cv.imwrite(calibresult.png, dst)方法二使用重映射remapping先求出从畸变图像到去畸变图像的映射再用cv.remap应用# undistort mapx, mapy cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5) dst cv.remap(img, mapx, mapy, cv.INTER_LINEAR) # crop the image x, y, w, h roi dst dst[y:yh, x:xw] cv.imwrite(calibresult.png, dst)两种方法结果相同initUndistortRectifyMap的第三个参数传None表示只做去畸变不做重投影校正插值方式为线性插值。官方教程展示了去畸变结果calib_result.jpg可以看到画面中所有边缘都变直了。最后官方教程建议用 NumPy 的写盘函数np.savez、np.savetxt等保存相机矩阵与畸变系数供后续如姿态估计一节复用。2.5 重投影误差评估标定精度重投影误差能很好地估计所求参数的精确程度误差越接近零说明参数越准确。具体做法是给定内参、畸变、旋转与平移先用cv.projectPoints()把 object points 投影到图像平面再计算其与角点检测结果的误差范数对所有点和所有图像求平方误差平均后开根号得到 RMSEmean_error 0 for i in range(len(objpoints)): imgpoints2, _ cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2SQR) / len(imgpoints2) mean_error error print( total error: {}.format(np.sqrt(mean_error/len(objpoints))) )官方教程在结尾还布置了一个练习尝试用圆形网格circular grid完成相机标定——对应 2.2 节提到的cv.findCirclesGrid()路径。3. 姿态估计在图像中渲染三维坐标轴与立方体本节继承自 py_pose.markdown。核心思想是标定一节已经求出了相机矩阵与畸变系数给定一张图案图像就能利用这些信息计算它的姿态pose即物体在空间中的姿态——如何旋转、如何平移。对平面物体可假设 Z0问题就转化为相机处于什么位置才能看到这张图案图。知道了物体在空间中的姿态就可以在其平面上绘制 2D 图形来模拟 3D 效果。官方教程的具体目标是把 3D 坐标轴X 轴蓝色、Y 轴绿色、Z 轴红色画在棋盘格第一个角点上使 Z 轴看起来垂直于棋盘格平面。3.1 加载标定结果并定义绘制函数首先从上一节保存的标定数据文件官方示例保存为B.npz中读回相机矩阵与畸变系数import numpy as np import cv2 as cv import glob # Load previously saved data with np.load(B.npz) as X: mtx, dist, _, _ [X[i] for i in (mtx,dist,rvecs,tvecs)]然后定义draw函数输入棋盘格角点来自cv.findChessboardCorners()与轴端点投影坐标imgpts把三条彩色轴线从第一个角点画出def draw(img, corners, imgpts): corner tuple(corners[0].ravel().astype(int32)) imgpts imgpts.astype(int32) img cv.line(img, corner, tuple(imgpts[0].ravel()), (255,0,0), 5) img cv.line(img, corner, tuple(imgpts[1].ravel()), (0,255,0), 5) img cv.line(img, corner, tuple(imgpts[2].ravel()), (0,0,255), 5) return img3.2 定义轴端点并用 solvePnP 求姿态与标定一节相同地创建终止条件与 object points另外定义axis points——用于画轴线的三维空间点。由于标定以棋盘格方格边长为尺度单位这里轴长取 3 个方格X 轴从 (0,0,0) 画到 (3,0,0)Y 轴同理Z 轴从 (0,0,0) 画到 (0,0,-3)取负值表示向相机方向伸出。criteria (cv.TERM_CRITERIA_EPS cv.TERM_CRITERIA_MAX_ITER, 30, 0.001) cols 6 rows 7 objp np.zeros((cols*rows,3), np.float32) objp[:,:2] np.mgrid[0:cols,0:rows].T.reshape(-1,2) axis np.float32([[3,0,0], [0,3,0], [0,0,-3]]).reshape(-1,3)主循环中对每幅图像检测 6×7 网格并做亚像素精化然后调用cv.solvePnPRansac()官方教程文字说明用 RANSAC 版本代码示例中使用cv.solvePnP求解旋转和平移向量再用cv.projectPoints()把 axis points 投影到图像平面最后从第一个角点向投影点连线for fname in glob.glob(left*.jpg): img cv.imread(fname) gray cv.cvtColor(img,cv.COLOR_BGR2GRAY) ret, corners cv.findChessboardCorners(gray, (cols, rows), None) if ret True: corners2 cv.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria) # Find the rotation and translation vectors. ret,rvecs, tvecs cv.solvePnP(objp, corners2, mtx, dist) # project 3D points to image plane imgpts, jac cv.projectPoints(axis, rvecs, tvecs, mtx, dist) img draw(img,corners2,imgpts) cv.imshow(img,img) k cv.waitKey(0) 0xFF if k ord(s): cv.imwrite(fname[:6].png, img) cv.destroyAllWindows()运行后每条轴线长度均为 3 个方格效果即 pose_1.jpg 所示。3.3 进阶渲染一个立方体若想在棋盘格上方悬空渲染一个立方体只需修改draw函数与轴端点。修改后的绘制函数把投影点分成地面层绿色轮廓、四根立柱蓝色线段与顶面红色轮廓def draw(img, corners, imgpts): imgpts np.int32(imgpts).reshape(-1,2) # draw ground floor in green img cv.drawContours(img, [imgpts[:4]],-1,(0,255,0),-3) # draw pillars in blue color for i,j in zip(range(4),range(4,8)): img cv.line(img, tuple(imgpts[i]), tuple(imgpts[j]),(255),3) # draw top layer in red color img cv.drawContours(img, [imgpts[4:]],-1,(0,0,255),3) return img轴端点改为 3D 空间立方体的 8 个角点边长 3 个方格axis np.float32([[0,0,0], [0,3,0], [3,3,0], [3,0,0], [0,0,-3],[0,3,-3],[3,3,-3],[3,0,-3] ])渲染结果可参见官方教程中的 pose_2.jpg。官方教程最后指出如果对图形学、增强现实AR感兴趣可以进一步使用 OpenGL 渲染更复杂的图形。4. 极线几何基本矩阵、极线与极点本节继承自 py_epipolar_geometry.markdown学习多视图几何基础极点epipole、极线epilines与极线约束epipolar constraint。4.1 核心概念用针孔相机成像时3D 到 2D 的转换丢失了深度信息。恢复深度的办法是使用多个相机——人眼正是以双相机方式工作即立体视觉stereo vision。只有一台相机时空间中射线OX上的所有点都投影到图像上的同一个点x无法确定 3D 位置引入右相机后OX上不同的点会投影到右图像平面上不同的点x于是可以三角化出正确的 3D 点这就是双目标定的核心思想。由此引出三个术语OX上各点在右图像平面上投影形成的直线l称为点x对应的极线epiline。匹配点的搜索不必遍历整幅图像只需沿极线搜索性能与精度都更好——这就是极线约束Epipolar Constraint包含X、O、O的平面XOO称为极平面Epipolar PlaneO与O是两个相机中心。右相机中心O在左图像上的投影点e称为极点epipole即两相机中心连线与图像平面的交点同理e是左相机对应的极点。某些情况下极点会落在图像之外意味着一台相机看不到另一台。由于所有极线都过极点可以通过求多条极线的交点来定位极点。要计算极线还需要两个配料本质矩阵Essential Matrix, E包含描述第二个相机相对第一个相机在全局坐标系中位置的平移与旋转信息原理图见 essential_matrix.jpg基本矩阵Fundamental Matrix, F我们希望测量以像素坐标进行F 包含与 E 相同的信息外加两台相机的内参信息从而能在像素坐标下关联两个相机若图像已校正并按焦距归一化点坐标则FE。简言之F 把一幅图像中的一个点映射为另一幅图像中的一条极线。F 由两幅图像的匹配点计算使用八点算法时最少需要 8 对匹配点点数越多越好并应使用 RANSAC 获得更稳健的结果。4.2 特征匹配SIFT FLANN 比值检验官方示例使用 SIFT 描述子、基于 FLANN 的匹配器和 Lowe 比值检验从左右两幅图中找出尽可能多的匹配对import numpy as np import cv2 as cv from matplotlib import pyplot as plt img1 cv.imread(myleft.jpg, cv.IMREAD_GRAYSCALE) #queryimage # left image img2 cv.imread(myright.jpg, cv.IMREAD_GRAYSCALE) #trainimage # right image sift cv.SIFT_create() # find the keypoints and descriptors with SIFT kp1, des1 sift.detectAndCompute(img1,None) kp2, des2 sift.detectAndCompute(img2,None) # FLANN parameters FLANN_INDEX_KDTREE 1 index_params dict(algorithm FLANN_INDEX_KDTREE, trees 5) search_params dict(checks50) flann cv.FlannBasedMatcher(index_params,search_params) matches flann.knnMatch(des1,des2,k2) pts1 [] pts2 [] # ratio test as per Lowes paper for i,(m,n) in enumerate(matches): if m.distance 0.8*n.distance: pts2.append(kp2[m.trainIdx].pt) pts1.append(kp1[m.queryIdx].pt)其中 FLANN 参数含义algorithm FLANN_INDEX_KDTREE(1)使用 KD 树索引、trees5建 5 棵树checks50控制搜索时的近似程度。knnMatch(..., k2)返回每个特征点的前 2 近邻比值检验m.distance 0.8*n.distanceLowe 论文的经典 0.8 阈值过滤掉区分度不足的匹配。4.3 求基本矩阵并绘制极线用 LMEDS 鲁棒方法求基本矩阵并只保留内点pts1 np.int32(pts1) pts2 np.int32(pts2) F, mask cv.findFundamentalMat(pts1,pts2,cv.FM_LMEDS) # We select only inlier points pts1 pts1[mask.ravel()1] pts2 pts2[mask.ravel()1]注意computeCorrespondEpilines的输入形状与线画在哪幅图的对应关系第一幅图的点产生的极线要画在第二幅图上。官方示例专门写了一个drawlines函数——极线以(a,b,c)形式给出取x0与x列宽两点即可画出整条直线def drawlines(img1,img2,lines,pts1,pts2): img1 - image on which we draw the epilines for the points in img2 lines - corresponding epilines r,c img1.shape img1 cv.cvtColor(img1,cv.COLOR_GRAY2BGR) img2 cv.cvtColor(img2,cv.COLOR_GRAY2BGR) for r,pt1,pt2 in zip(lines,pts1,pts2): color tuple(np.random.randint(0,255,3).tolist()) x0,y0 map(int, [0, -r[2]/r[1] ]) x1,y1 map(int, [c, -(r[2]r[0]*c)/r[1] ]) img1 cv.line(img1, (x0,y0), (x1,y1), color,1) img1 cv.circle(img1,tuple(pt1),5,color,-1) img2 cv.circle(img2,tuple(pt2),5,color,-1) return img1,img2然后在两幅图上分别求、分别画极线# Find epilines corresponding to points in right image (second image) and # drawing its lines on left image lines1 cv.computeCorrespondEpilines(pts2.reshape(-1,1,2), 2,F) lines1 lines1.reshape(-1,3) img5,img6 drawlines(img1,img2,lines1,pts1,pts2) # Find epilines corresponding to points in left image (first image) and # drawing its lines on right image lines2 cv.computeCorrespondEpilines(pts1.reshape(-1,1,2), 1,F) lines2 lines2.reshape(-1,3) img3,img4 drawlines(img2,img1,lines2,pts2,pts1) plt.subplot(121),plt.imshow(img5) plt.subplot(122),plt.imshow(img3) plt.show()第二个参数1 或 2指明输入点属于哪幅图像。结果如官方教程中的 epiresult.jpg 所示左图像中所有极线汇聚于图像右侧外部的一个点该汇聚点即极点。官方教程对实践效果的提示是应使用分辨率较高、且包含大量非平面点的图像。章节练习部分还指出两条经验其一是相机沿光轴前进运动时两幅图中的极点会出现在相同位置、极线从一个固定点辐射而出其二是基本矩阵估计对匹配质量、离群点很敏感当所有匹配点都位于同一平面时效果会显著变差。5. 从立体图像求深度图StereoBM 与参数调优本节继承自 py_depthmap.markdown目标是从一对立体图像创建深度图disparity map。5.1 视差与深度的几何关系上一节的极线约束已经说明两幅同一场景的图像可以直观地得到深度信息。官方教程给出的示意与推导见 stereo_depth.jpg图中存在相似三角形写出等价方程可得视差公式disparity x - x B*f / Z其中x与x是场景点在图像平面上对应点与其相机中心之间的距离差B是两相机间距基线已知f是焦距已知。也就是说场景中某点的深度与对应图像点距离差成反比。据此可推导出图像中所有像素的深度。实现上先求两图间的对应匹配极线约束使该操作更快更准再得到视差。5.2 用 StereoBM 生成视差图官方示例使用 Tsukuba 测试序列的tsukuba_l.png/tsukuba_r.png用块匹配立体算法cv.StereoBM求视差import numpy as np import cv2 as cv from matplotlib import pyplot as plt imgL cv.imread(tsukuba_l.png, cv.IMREAD_GRAYSCALE) imgR cv.imread(tsukuba_r.png, cv.IMREAD_GRAYSCALE) stereo cv.StereoBM.create(numDisparities16, blockSize15) disparity stereo.compute(imgL,imgR) plt.imshow(disparity,gray) plt.show()结果如官方教程所示左侧为原图、右侧为视差图见 disparity_map.jpg可以看到结果中混入了相当程度的噪声。官方教程指出通过调整numDisparities和blockSize可以改善结果。5.3 关键参数详解官方教程对StereoBM的一组可调参数给出了工程化解释这些参数在算法初始化之后通过setTextureThreshold、setSpeckleRange、setUniquenessRatio等专用 setter/getter 设置详见cv::StereoBM文档texture_threshold过滤掉纹理不足、无法可靠匹配的区域Speckle range 与 speckle_size基于块的匹配器常在物体边界附近产生麻点matching window 一侧是前景、另一侧是背景speckle_size规定视差斑块的像素数低于该值即视为麻点丢弃speckle_range控制斑块内视差值的接近程度二者共同构成视差后处理的麻点滤波numDisparities视差数窗口在水平方向滑动的像素数越大可见深度范围越大但计算量也越大min_disparity从左像素 x 坐标开始搜索的偏移量uniqueness_ratio另一种后过滤——若最佳视差不比搜索范围内其他视差明显更优则过滤掉该像素当texture_threshold和麻点滤波仍放入了伪匹配时可尝试调整prefilter_size 与 prefilter_cap预处理阶段在块匹配前对图像亮度做归一化、对纹理做增强通常不需要调整。章节练习部分还提示OpenCV samples 中有一个生成视差图并做三维重建的示例即 OpenCV-Python 示例见 samples/python 目录中的stereo_match.py可配合本章内容一起阅读验证。6. 小结四个主题的衔接关系回到 calib3d 教程目录 的脉络四个子页实际上构成一条完整的单目标定 → 空间理解 → 多视图重建技术链相机标定py_calibration.markdown用findChessboardCornerscornerSubPix收集点calibrateCamera求解相机矩阵、5 个畸变系数与各帧外参再用undistort或initUndistortRectifyMapremap去畸变并用projectPoints计算 RMSE 检验精度姿态估计py_pose.markdown复用标定结果solvePnP/solvePnPRansac求平面图案的旋转平移projectPoints把任意 3D 轴点投到图像上实现坐标轴/立方体的 3D 叠加渲染极线几何py_epipolar_geometry.markdownSIFTFLANN比值检验获取匹配findFundamentalMatLMEDS/八点法求基本矩阵computeCorrespondEpilines求极线直观验证极线约束立体深度图py_depthmap.markdown由disparity Bf/Z的几何关系用StereoBM求出视差并调优texture_threshold、speckle_size/range、uniqueness_ratio等参数得到干净的深度信息。以上所有函数的 C 实现与测试均可在 modules/calib3d 目录中进一步查阅演示图片与教程源文件位于 doc/py_tutorials/py_calib3d 下对应子目录可作为复现实验时的对照资料。【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表