做机器视觉落地这几年,我最常被问到的需求之一就是“帮我看看这块表面平不平”。手动贴膜、玻璃面板、金属外壳这类产品,质检员从前是拿千分表加塞尺一块一块地量,又慢又容易漏,还带有接触力,薄壁件一压就变形。后来我在某产线项目里,用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-Tomasi(OpenCV的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:少一步就DllNotFound
OpenCVSharp在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 List<Point3f[]>(); var imagePoints = new List<Point2f[]>(); // 每张图检测完成后填充两个列表,然后: Cv2.CalibrateCamera( objectPoints, imagePoints, gray.Size(), out Mat cameraMatrix, out Mat distCoeffs, out Mat[] rvecs, out Mat[] tvecs); double fx = cameraMatrix.At<double>(0, 0); double fy = cameraMatrix.At<double>(1, 1); double cx = cameraMatrix.At<double>(0, 2); double cy = cameraMatrix.At<double>(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里有一个专用的图像包装类型Image<Gray, 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返回的是原始图像上的像素位置,镜头畸变会让棋盘格弯掉,尤其是图像的边缘位置。用标定好的内参和畸变系数调UndistortPoints:
using 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.ToArray<Point2f>();这里有个细节容易被忽略:UndistortPoints输出的是归一化相机坐标,也就是相机坐标系下Zc=1平面上点的坐标,单位不再是像素。需要根据外参进一步转换到棋盘格物理坐标系。
获取外参用SolvePnP,输入棋盘格角点的物理坐标和对应的像素坐标,得到旋转向量和平移向量,再把相机坐标转换到世界坐标:
// 生成棋盘格角点的物理坐标,squareSize是棋盘格单格边长,单位毫米 var objectPts = new List<Point3f>(); 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(List<Point3d> 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.Set<double>(i, 0, pts[i].X); A.Set<double>(i, 1, pts[i].Y); A.Set<double>(i, 2, 1.0); b.Set<double>(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.At<double>(0, 0); double bCoef = x.At<double>(1, 0); double c = x.At<double>(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像素,先处理机械固定或者相机曝光时间,再谈检测算法。相机曝光时间能短就短,配合光源打亮,震动的影响会小很多。
从设备安装角度说,相机必须固定在刚性结构上,不要装在悬臂上。悬臂结构看起来方便调整,但低频震动或者人员触碰都会造成微米级的移动,这种误差在纯图像层面很难察觉。
做完整套检测流程,最后再分享一个我个人的习惯。每次开始测量前,我都会拿一块标准平面样件做一次校验。标准件检测结果如果超出了它自身平面度公差的一倍,那就不是被测产品的问题,而是测量系统本身的状态出了问题。平整度检测说到底是相对测量,测量系统自身的稳定性,和算法精度一样重要。这套方案后续做扩展,还可以把角点网格加密、换成更小格距的棋盘格,或者增加多相机拼接覆盖更大面积,但核心思路不变:先把测量问题翻译成图像问题,再用角点检测把平面“数字化”。