资讯详情

资讯详情

OpenCVSharp棋盘格角点检测实现平整度测量全流程

做机器视觉落地这几年我最常被问到的需求之一就是“帮我看看这块表面平不平”。手动贴膜、玻璃面板、金属外壳这类产品质检员从前是拿千分表加塞尺一块一块地量又慢又容易漏还带有接触力薄壁件一压就变形。后来我在某产线项目里用OpenCVSharp加一只普通的工业相机把一块棋盘格标定板当成“尺子”用角点检测把表面的几何起伏变成一组三维点的偏差整套方案跑通了稳定性比预期还要好。这篇文章就把这套思路和代码完整拆开讲。如果你正打算做视觉测量类项目或者刚接触OpenCVSharp不知道从哪入手这篇文章可以直接抄作业。我会把测量逻辑、环境搭建、核心代码、现场踩过的坑全部摊开说工程上的细节比单纯讲算法要重得多。1. 平整度检测为什么离不开角点检测先想清楚测量逻辑很多开发者拿到需求第一件事就是打开IDE写图像处理代码结果拍出来的图完全没法用。因为平整度检测本质上不是图像问题是测量问题。你得先把“被测对象”和“图像特征”之间的桥梁搭起来否则后面全是空转。1.1 平整度到底在测什么PV、RMS和图纸上的平面度平整度在工程上指的是实际表面与理想平面之间的偏差。一块材料放在平台上再好的工艺也不可能绝对平总会有中间凸、边缘翘、局部坑洼。衡量这个偏差通常看两个数最大偏差PV和均方根偏差RMS。PV是最高点和最低点相对拟合平面的差值反映最坏情况RMS是每个采样点到拟合平面距离的平方平均再开方反映整体波动水平。图纸上写“平面度0.2mm”意思就是垂直于理想平面的方向任意两点之间的高度差不能超过0.2mm。传统方法是接触式测量一块板子画好网格然后用千分表逐点打过去。一块300mm见方的板子画5×5网格就是25个点一个点对准、调零、读取、记录熟练工也要十几分钟。而且千分表的测杆有弹簧力对软质材料不友好。换成视觉方案以后检测一块板的时间可以压到一两秒内。但前提是图像里得有一个“可被测量的特征”这就是角点检测的价值。1.2 棋盘格、亚像素角点和理想平面之间的关系角点检测听上去是个纯图像概念但它能用来测平整度是因为我们给被测表面人为地“注入”了一组已知空间关系的特征点。做法很简单拿一块印有黑白棋盘格的薄膜或标定板紧密贴到待测表面上。棋盘格上每个黑白方块的交界处有一个角点这个角点在图像里的位置可以被算法定位到亚像素精度。如果表面是平的所有角点都应该落在一个理想平面附近如果表面有起伏棋盘格薄膜会跟着表面一起变形角点集合就不再共面了。所以整个测量的核心思路是把“看不见的平面”变成“一组坐标点”再用数学方法判断这些点共面到什么程度。这是非常典型的间接测量思路也是角点检测在这个场景里的真正角色。这里需要说一下角点检测算法本身。OpenCV里最常用的两类角点检测思路一类是Harris一类是Shi-TomasiOpenCV的GoodFeaturesToTrack底层就是它。Harris的思路是在一个像素周围开一个小窗口窗口向任意方向移动时灰度变化都很大就认为这里是角点。它的响应函数是R det(M) - k * trace(M)^2其中M是由图像梯度构成的二阶矩矩阵。Shi-Tomasi做了个简化直接取M的两个特征值中较小的那个min(λ1, λ2)作为响应比阈值大就保留。在棋盘格场景里OpenCV的FindChessboardCorners并非简单调用GoodFeaturesToTrack它内部有一套专门针对棋盘格几何结构的搜索算法会根据黑白交替的拓扑关系去定位交点再输出角点的亚像素坐标。但不管怎样底层判断的依据都是角点处两个方向梯度同时剧烈变化的特性。顺便说清楚为什么我不直接对被测对象表面提取角点非要贴一块棋盘格因为大多数工业表面是均匀的喷砂、拉丝、阳极氧化纹理没有规则的角点可供定位。棋盘格的作用是提供“基准特征”把测量问题从“表面自身特征识别”转变成“已知几何的形变分析”可靠性完全不同。1.3 视觉方案和接触式千分表、激光轮廓仪怎么选我经常被问“视觉测平整度是不是精度不够”这里得把方案选择摊开看清楚。方案测量方式精度量级速度单套成本量级主要短板千分表大理石平台接触逐点0.01mm极慢低效率低、有接触力激光轮廓仪非接触扫描0.005mm快高设备贵、对反光敏感棋盘格角点检测非接触拍照0.02~0.1mm很快低依赖靶标贴合、精度受限于相机分辨率用棋盘格方案不是因为它在所有场景里精度最高而是它在成本、速度、可解释性上最平衡。它适合的是宏观平整度检测比如外壳面板、平面玻璃、覆铜板这类产品公差在0.05mm到0.3mm之间的场景。如果要求亚微米级别的平面度需要用激光干涉仪如果表面不能贴任何东西那只能用激光轮廓仪或结构光方案。但很多客户的需求其实没那么极端一台500万像素工业相机加一个棋盘格标定板就能把传统人工抽检替代掉大半。2. 环境准备工作OpenCVSharp项目从引入到能跑的三个关卡先把代码放一边说说OpenCVSharp项目的环境问题。这部分看起来不起眼实际在现场调试时我见过太多人卡在项目根本编译不过、运行时崩掉的阶段。走完下面几步后面才能安心写算法。2.1 NuGet依赖与运行时DLL少一步就DllNotFoundOpenCVSharp在NuGet上的包名就两个OpenCvSharp4和OpenCvSharp4.Windows。第一个包是托管层第二个包带Windows运行库。如果不带Windows那个包项目经常编译通过、一运行就报DllNotFoundException找不到OpenCvSharpExtern.dll。这个原生DLL是C和C#之间的桥少它一切白搭。部署到客户机器时也容易出问题。OpenCvSharp4.Windows会在输出目录里生成runtimes文件夹里面按win-x64、win-x86分目录存放原生DLL。你不能只把exe拷过去得把整个输出目录连同runtimes一起部署或者把对应架构下的OpenCvSharpExtern.dll复制到exe同目录。这个坑在打包成单文件时尤其常见我建议直接保留原生目录结构别做自以为聪明的精简。版本选择上直接用最新4.x即可。网上很多老教程是2.x时代的API方法名、命名空间差别很大比如Mat的创建方式、图像读取的枚举名都有变化对着老代码抄容易被绕进去。2.2 像素和毫米之间的桥相机内参标定的几条硬经验角点检测得到的是像素坐标平整度要的是毫米偏差中间必须经过相机标定。相机标定解决的问题是像素坐标和相机坐标之间的换算关系。简单说一个空间点经过镜头投影到图像传感器上像素位置由内参矩阵决定也就是fx、fy、cx、cy这四个参数加上镜头畸变系数。现场很多项目图省事不做标定直接拿一张已知尺寸的纸计算像素当量也就是每毫米多少像素。这在完全垂直拍摄、物体在固定高度的场景下凑合能用但只要相机光轴稍微倾斜图像边缘的尺度就已经变了算出来的平整度误差会非常难看。标定用的靶标还是棋盘格。这也是为什么我强调棋盘格方案省事的原因一套棋盘格既能做相机标定又能做平整度检测连靶标都不用换。标定时的硬经验是采集15到20张不同角度、不同位置的棋盘格照片棋盘格要铺满画面各个区域尤其是四个角。如果你只把棋盘格放在画面正中间拍几张标定出来的畸变系数在边缘区域完全不可信。后面我还会专门讲这个坑。核心标定代码大致是这样// objectPoints: 每张图中棋盘格角点的物理坐标单位毫米 // imagePoints: 每张图中对应角点的像素坐标 // gray.Size(): 图像宽高 var objectPoints new ListPoint3f[](); var imagePoints new ListPoint2f[](); // 每张图检测完成后填充两个列表然后 Cv2.CalibrateCamera( objectPoints, imagePoints, gray.Size(), out Mat cameraMatrix, out Mat distCoeffs, out Mat[] rvecs, out Mat[] tvecs); double fx cameraMatrix.Atdouble(0, 0); double fy cameraMatrix.Atdouble(1, 1); double cx cameraMatrix.Atdouble(0, 2); double cy cameraMatrix.Atdouble(1, 2);标定一次之后内参和畸变系数可以保存成文件下次运行直接读取没必要每次开机都重新标。2.3 Mat与Bitmap互转最容易写错数据类型的地方OpenCVSharp里图像的基本类型是Mat但很多现场上位机程序用的图片源是Bitmap这就涉及到互转。从Bitmap转Mat有个容易忽略的点像素格式。Bitmap最常见的是Format24bppRgb也就是BGR顺序存储而OpenCV的默认三通道顺序也是BGR所以直接转通常没问题。但如果你用System.Drawing读入的是一张Format32bppArgb的图片就直接拿Mat来用通道数对不上后面CvtColor会崩。标准转换写法// Bitmap - Mat var bitmap new Bitmap(test.bmp); var rect new Rectangle(0, 0, bitmap.Width, bitmap.Height); var bmpData bitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); using var mat new Mat(bitmap.Height, bitmap.Width, MatType.CV_8UC3, bmpData.Scan0); bitmap.UnlockBits(bmpData); // Mat - Bitmap用于显示结果 var result new Mat(); Cv2.CvtColor(mat, result, ColorConversionCodes.BGR2RGB); var resultBitmap BitmapConverter.ToBitmap(result);还有一点OpenCVSharp里有一个专用的图像包装类型ImageGray, byte很多老教程喜欢用它。但FindChessboardCorners这类核心函数接收的是InputArray用Mat更直接没必要绕一层。我建议统一用Mat省得类型转换时参数不匹配。3. 核心流程实现从棋盘格角点到平整度数值的完整代码环境通了之后进入正题。整个检测流程可以拆成几个串行的环节每个环节都有明确的输入输出。实际项目中我会把每一步都封装成独立函数这样一旦数据异常能快速定位是哪一步出的问题。3.1 检测流程总览把测量问题翻译成图像问题完整流程如下在待测表面上贴好棋盘格薄膜确保贴合紧密无气泡。工业相机正对表面拍摄记录原始图像。图像转为灰度图调用FindChessboardCorners提取棋盘格内角点。对初始角点做亚像素细化用CornerSubPix把角点精度从像素级提升到亚像素级。用标定好的内参和畸变系数对所有像素坐标去畸变。通过相机外参把去畸变后的归一化坐标转换到棋盘格物理坐标系。对所有三维点做平面拟合计算每个点到拟合平面的距离。统计最大偏差PV和RMS与合格线比较输出检测结果。这里有个关键前提棋盘格薄膜必须紧密贴合被测表面。如果表面存在明显凹凸薄膜会“浮”在表面上方检测结果就是膜的形状而不是表面的形状。对于平面度精度要求不超过0.1mm的场景几十微米厚的薄膜贴合后误差可以接受但操作时必须用刮板把空气排干净。3.2 棋盘格角点提取与亚像素细化FindChessboardCorners和CornerSubPix的调参角点提取的核心函数是FindChessboardCorners。第一个参数是灰度图第二个参数是patternSize注意这个size指的是内角点的行列数不是棋盘格的方块数。比如一个9×6的棋盘格有9列6行方块内角点就是8×5。这个数字一旦填错函数会一直返回false。我以前在项目里就因为行列搞反排查了整整半天。// 9x6的棋盘格内角点为8x5 var patternSize new Size(8, 5); bool found Cv2.FindChessboardCorners( gray, patternSize, out Point2f[] corners, CornerDetectFlags.AdaptiveThresh | CornerDetectFlags.NormalizeImage);如果不传第三个参数内部使用默认策略在尺度变化大的场景里容易漏检。我建议加上AdaptiveThresh它会在局部做自适应阈值处理对光照不均的抵抗能力明显增强。初始角点只是整数像素精度直接用于平面拟合误差会放大到毫米级别不可接受。接下来必须做亚像素细化Cv2.CornerSubPix( gray, corners, new Size(11, 11), new Size(-1, -1), new TermCriteria(TermCriteria.Type.Count | TermCriteria.Type.Eps, 30, 0.01));第二个参数是搜索窗口大小窗口太小会捕获不到足够梯度信息窗口太大会把周围角点的影响带进来11×11在大多数场景下是稳妥选择。TermCriteria里的30表示迭代次数上限0.01表示亚像素位移精度达到任一条件即停止。细化完成后的角点位置是浮点数精度通常能到0.02到0.1像素。这个精度直接决定了平整度检测的测量分辨率所以这一步绝对不能省。做完这一步可以先可视化一下把角点画在图上确认是否正确。我习惯把提取到的角点按行列顺序连接成网格线一旦哪条线歪了说明角点顺序错了后面计算不可能对。3.3 从像素坐标到三维坐标去畸变、外参与平面拟合角点坐标有了接下来把它变成可用于平面拟合的三维点。首先要去畸变。FindChessboardCorners返回的是原始图像上的像素位置镜头畸变会让棋盘格弯掉尤其是图像的边缘位置。用标定好的内参和畸变系数调UndistortPointsusing var input new Mat(1, corners.Length, MatType.CV_32FC2, corners); using var normalized new Mat(); Cv2.UndistortPoints(input, normalized, cameraMatrix, distCoeffs); var points normalized.ToArrayPoint2f();这里有个细节容易被忽略UndistortPoints输出的是归一化相机坐标也就是相机坐标系下Zc1平面上点的坐标单位不再是像素。需要根据外参进一步转换到棋盘格物理坐标系。获取外参用SolvePnP输入棋盘格角点的物理坐标和对应的像素坐标得到旋转向量和平移向量再把相机坐标转换到世界坐标// 生成棋盘格角点的物理坐标squareSize是棋盘格单格边长单位毫米 var objectPts new ListPoint3f(); for (int j 0; j patternSize.Height; j) { for (int i 0; i patternSize.Width; i) { objectPts.Add(new Point3f(i * squareSize, j * squareSize, 0)); } } bool ok Cv2.SolvePnP( objectPts.ToArray(), corners, cameraMatrix, distCoeffs, out Mat rvec, out Mat tvec); // 旋转向量转旋转矩阵 using var R new Mat(3, 3, MatType.CV_64FC1); Cv2.Rodrigues(rvec, R);注意SolvePnP这里的第三个参数应该用原始像素角点而不是去畸变后的normalized坐标因为SolvePnP内部会自己做畸变补偿。我第一次做的时候把去畸变后的点传进去结果结果是错的折腾了很久。有了旋转矩阵和平移向量可以把每个归一化相机坐标点变换到棋盘格平面上得到三维点集合。由于棋盘格贴合在待测表面这些三维点就是表面形貌的采样点。平面拟合用最小二乘法。把三维点拟合成一个平面z ax by c然后算每个点到拟合平面的距离。这里用OpenCVSharp的Solve配合SVD分解处理超定方程public static (double pv, double rms) EvaluateFlatness(ListPoint3d pts) { int n pts.Count; using var A new Mat(n, 3, MatType.CV_64FC1); using var b new Mat(n, 1, MatType.CV_64FC1); for (int i 0; i n; i) { A.Setdouble(i, 0, pts[i].X); A.Setdouble(i, 1, pts[i].Y); A.Setdouble(i, 2, 1.0); b.Setdouble(i, 0, pts[i].Z); } using var x new Mat(3, 1, MatType.CV_64FC1); Cv2.Solve(A, b, x, DecompTypes.Svd); double a x.Atdouble(0, 0); double bCoef x.Atdouble(1, 0); double c x.Atdouble(2, 0); double maxDev 0; double sumSq 0; foreach (var p in pts) { double dist Math.Abs(a * p.X bCoef * p.Y - p.Z c) / Math.Sqrt(a * a bCoef * bCoef 1); maxDev Math.Max(maxDev, dist); sumSq dist * dist; } double rms Math.Sqrt(sumSq / n); return (maxDev, rms); }拟合平面的SVD解法相比直接求逆更稳定尤其是在数据点接近共面或者分布不均匀的时候。我在现场测试过用SVD解出来的平面系数在边界区域的拟合稳定性明显好于普通正规方程。3.4 平整度报告怎么出PV、RMS与合格线设定建议算法跑通以后真正和客户打交道的是检测报告。我用得非常顺的格式是把角点偏差画成伪彩色分布图再叠加文本输出PV、RMS值和Pass/Fail判定。代码实现上把每个角点的偏差值归一化到0到255区间用ApplyColorMap映射成伪彩色叠加到原图。偏差最大的点用红色标记容易让现场人员一眼看到问题区域。还有两个细节是我踩过坑以后加上的一是固定像素当量并保存到配置里。不要每次检测都重新算一次尺度同一个项目的相机位置、镜头焦距应该保持不变每次重新计算会引入额外波动。二是在报告里记录角点检测耗时和检测日期方便追溯。视觉项目最怕的就是出了问题说不清楚是哪批次的数据带时间戳和参数的报表能帮你少背很多锅。合格线的设定没有通用标准。一般情况下客户图纸会写明平面度公差。如果只给一个PV值我建议把RMS合格线设为PV的三分之一左右这是比较常见的内部管理标准。项目里我曾遇到客户只要求PV小于0.3mm但现场肉眼就能看出板子边缘翘曲后来把RMS线设到0.08mm才把真正不合格的批次拦下来。4. 现场排查实录六个最容易踩的坑和对应的解决方案这一部分是我最想分享的。算法理论上能跑通不代表现场稳定。实际项目里我基本把这几个坑都踩过一遍每个都花了不少时间才定位到根因。4.1 角点检测一直失败返回值常年是false这是新人最常见的第一个问题。棋盘格明明拍得很清楚FindChessboardCorners就是返回false。排查方向按顺序来第一步确认patternSize填的是内角点数不是方块数。第二步确认图像是灰度图且尺寸不太大如果图像超过2000万像素建议先缩小到不影响角点定位的程度检测速度会快很多。第三步看光照棋盘格在强光下产生高光或者阴影里太暗角点检测都会失效。我的经验是拍照时优先保证棋盘格区域的光照均匀不要有明显的渐变或反光。如果现场无法控制环境光就在检测时把建议的AdaptiveThresh | NormalizeImage标志加上。这两个标志分别表示自适应阈值和图像归一化对光照不均的补偿很有帮助。还有一个小技巧如果不确定检测参数可以用Cv2.FindChessboardCorners试着传入不同规格的Size同时把检测到的角点画出来哪些角点位置不对一目了然。调试阶段多花十分钟可视化比盲猜参数强得多。4.2 亚像素角点漂移窗口和终止条件没给对有时候FindChessboardCorners返回true了角点位置肉眼看着也对但最终平整度数据波动很大。检查之后发现角点的亚像素位置在不同的测试里漂移了0.5个像素以上。问题基本出在CornerSubPix的参数上。窗口太小周围的梯度信息不足细化算法拟合不稳定终止条件的精度设得太松比如0.1像素就停那细化和不细化差别不大。我建议窗口取11×11到21×21之间迭代次数30次以上精度0.01。另外输入给CornerSubPix的图像必须是8位灰度图不能是彩色图或者已经做过边缘提取的二值图。有一次我把Canny边缘图传进去角点跑到边缘交叉点的角落上去了定位精度严重下降。还有一个不常被注意的原因棋盘格本身不平整。如果棋盘格薄膜表面有褶皱或者贴附时有气泡那角点位置物理上就不是一个平面算法上再细化也救不回来。所以每次贴膜后我会用刮板仔细排出空气并目视确认膜面没有折痕。4.3 高反射和阴影干扰棋盘格时好时坏工业现场的光照环境千奇百怪。最讨厌的是抛光金属、玻璃面板上方有反光相机拍摄时棋盘格区域出现一条亮带亮带里的黑白方块对比度剧烈下降角点检测时好时坏。遇到这种情况先别急着调算法参数先把光源搞定。我的经验是使用低角度环形光或者带匀光板的漫射光源从两侧斜照可以减少镜面反射。安装位置一般在相机两侧45度方向距离稍远一点。如果光源实在调不好有个取巧的办法把棋盘格稍微旋转一个角度让反射光方向偏离镜头。这个方法在玻璃表面上特别有效。因为镜面反射有方向性你避开那个特定角度反光就消失了。当然如果你的被测表面是镜面本身那就只能用偏振片方案。相机镜头前加线偏振片光源前加偏振片两个偏振方向正交可以把镜面反射光滤掉大部分。这个方法需要额外采购偏振片但对高反光表面效果立竿见影。4.4 去畸变后边缘坐标跳动标定数据没有铺满画面有一段时间我检测25个角点中间区域的数据很稳定但棋盘格四角位置的角点偏差每次都有零点几毫米的跳动。起初以为是被测表面本身不平后来用高精度陶瓷平板验证发现是标定数据没铺满画面导致的。标定的原理决定了不同位置的畸变需要不同位置的样本去约束。如果标定照片里的棋盘格始终在画面中央区域畸变参数在画面四角的取值就会被外推这里没有任何真实数据支撑结果自然不可信。解决办法就是重新标定。采集标定图片时保证棋盘格出现在画面的四个角落和四条边中间同时要有不同角度、不同距离的样本。建议至少20张分布在各个区域。还有一个配套操作标定完成后用重投影误差做一个自查。如果误差大于0.1像素说明标定数据里混入了几组质量很差的图片剔除后重新标定。4.5 像素当量算错一张图的尺度不代表全局尺度这个坑发生在相对测量方案里也就是不做完整标定只通过已知尺寸的物体计算每毫米对应的像素数。问题是单张图像上的像素当量只有在物体处于同一深度、并且镜头光轴垂直于测量平面的前提条件下才是常数。如果板材倾斜、厚度不一、镜头有俯仰角图像边缘的物体看上去比中心小算出来的平整度偏差天然就是假的。我遇到过的实际情况是测量结果显示整块板呈现出明显的凸面但实际用千分表验证是平的。检查下来相机安装角度有些倾斜光轴没有正对测量区域中心导致平面在图像中产生了透视变形。解决方法是做完整标定而不是算像素当量。如果实在不想做标定至少应该保证相机光轴垂直于被测表面并且把被测表面放在同一个高度。这里有一句我总结了无数次的经验所有用单张图像做尺寸类测量的项目都必须把“相机安装位姿的稳定性”当成核心约束来管理。4.6 数据整体像差但检查没问题相机光轴倾斜和震动最后一个坑比前面都隐蔽。数据不算错角点也都检测到了但检测结果整体偏向一侧或者呈现出固定方向的斜坡。排查下来大概率是相机光轴没有垂直于被测表面。光轴倾斜会导致远方一侧的图像被压缩拟合平面自然会有坡度但这个坡度不代表被测表面的真实倾斜。另一种可能性是相机支架刚性不足。产线设备运行时的震动传给相机图像在曝光瞬间轻微拖影角点位置的亚像素精度就崩了。这种情况最误导人因为单张图像看起来还算清晰但角点细化后的坐标已经偏移。我的做法是每次跑数据之前先连续采集同一静态场景的两帧图像比较同一角点的坐标差异。如果两帧图像之间角点坐标跳动超过0.1像素先处理机械固定或者相机曝光时间再谈检测算法。相机曝光时间能短就短配合光源打亮震动的影响会小很多。从设备安装角度说相机必须固定在刚性结构上不要装在悬臂上。悬臂结构看起来方便调整但低频震动或者人员触碰都会造成微米级的移动这种误差在纯图像层面很难察觉。做完整套检测流程最后再分享一个我个人的习惯。每次开始测量前我都会拿一块标准平面样件做一次校验。标准件检测结果如果超出了它自身平面度公差的一倍那就不是被测产品的问题而是测量系统本身的状态出了问题。平整度检测说到底是相对测量测量系统自身的稳定性和算法精度一样重要。这套方案后续做扩展还可以把角点网格加密、换成更小格距的棋盘格或者增加多相机拼接覆盖更大面积但核心思路不变先把测量问题翻译成图像问题再用角点检测把平面“数字化”。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →