做工业视觉这些年我越来越觉得图像处理里最容易被低估的算法就是霍夫圆检测。这套视觉工作流系列写到第50章前面已经聊过采集、标定、模板匹配、边缘提取今天把“找圆”这件事单独拿出来讲清楚——在 C# 里基于 OpenCV 做霍夫圆检测HoughCircles从原理到参数再到工程化落地。如果你正在做瓶盖定位、圆孔测量、焊球检测这类场景这一章应该能帮你少走不少弯路。很多人觉得找圆不就是 cv.HoughCircles 一行代码吗实际上在真实产线上跑起来之后漏检、误检、圆心漂移、重复圆这些问题一个接一个。这篇文章我会把霍夫找圆背后的原理拆开再把我在几个项目里调参踩坑的过程完整走一遍最后整理一份问题速查表。无论你是刚接触视觉的新手还是已经在写上位机的老手都能从里面找到直接能抄的配置和思路。1. 从工作流视角看“找圆”这件事1.1 为什么视觉工作流需要专门的找圆环节一套典型的视觉工作流大概是相机采图 → 图像预处理 → 特征定位 → 测量判定 → 输出结果。定位环节里“找圆”几乎是发生率最高的操作之一因为工业场景里的圆形特征太多了瓶盖内垫、法兰盘螺栓孔、芯片焊球、轴承外圈、印刷靶标……只要产品设计里出现圆柱结构视觉端大概率就跑不掉一个找圆算子。但圆的定位问题比找边、找角点要麻烦得多。一条直线在二维平面里能用两个参数描述一个圆却有圆心横坐标、圆心纵坐标、半径三个自由度。如果沿用经典的暴力搜索思路需要在三维参数空间里逐个累加计算量和内存开销都非常大。这也是为什么很多视觉库里早期只提供找直线、找边缘的算子找圆要么走最小二乘拟合要么干脆靠模板匹配直到霍夫梯度法出现之后才算有了一个在速度和鲁棒性上都比较实用的通用方案。在这个系列的工作流设计里我习惯把“找圆”做成一个独立的节点输入是一张灰度图或者彩色图自动转灰度输出是若干个圆的圆心坐标和半径。这样做的好处是上游的采集节点、预处理节点不用关心圆的逻辑下游的测量节点、机械手引导节点也不用关心圆是怎么找到的。节点之间只传标准化的数据结构换算法、调参都只改一个节点内部不影响整条流水线。第50章之所以专门讲霍夫找圆就是因为它在这个工作流里的定位非常典型——既是一个独立的算法节点又承担着给后续测量提供基准的职责。1.2 C# 生态下 OpenCV 的两种打开方式在 C# 这边要调用 OpenCV主流无非两个封装库OpenCvSharp 和 Emgu CV。我自己的项目里绝大多数用的都是 OpenCvSharp简单说说两者差异。对比项OpenCvSharpEmgu CVAPI 风格贴近 C 原生 OpenCV函数名几乎一致.NET 化封装较重习惯上有一定转换成本内存管理使用引用计数封装 Mat配合 Dispose 释放相对可控版本迭代中关于内存泄漏的讨论比较多需要额外小心版本更新跟随 OpenCV 主干发布新功能跟得快发布节奏偏慢部分新算子要等较长时间中文资料社区分享较多示例代码量大资料也有但风格偏老学习曲线适合已经懂一点 OpenCV 的人直接搬 API适合从零开始、只想用 C# 思维的开发者选 OpenCvSharp 还有一个很现实的原因工作流里要用到的 Canny、HoughCircles、GaussianBlur 这些算子的参数名和 C 版本完全对得上网上找到的 C 示例可以直接翻译成 C#排查问题的时候对照官方文档非常方便。需要提醒的是HoughCircles 在不同 OpenCV 版本里的行为有细微差别比如某些版本对 param1 的默认处理方式不一样返回的圆形结果结构也有过调整。如果你升级过 OpenCvSharp 大版本工作流里的霍夫节点一定要重新做一轮回归测试不要只看编译能过就上线。2. 霍夫圆检测的原理以及它“笨”在哪里聪明在哪里2.1 从霍夫直线到霍夫圆霍夫变换最早是拿来找直线的。直线用极坐标表示成 r x * cosθ y * sinθ图像空间里的每一个边缘点都在 (r, θ) 参数空间里对应一条正弦曲线。多个边缘点投出的曲线会在某个 (r, θ) 位置交汇交汇点就是图像中那条直线的参数。这个过程本质上是一个“由点反向投票”的机制。圆的问题比直线高一个维度。圆的一般方程是 (x - a)² (y - b)² r²要把检测对象的边缘点映射到参数空间就需要对 (a, b, r) 三个未知量投票。如果直接建一个三维累加器图像稍微大一点累加器体积就爆炸了——假设图像是 800×600半径取值范围 0~300那累加器就有 800×600×300 ≈ 1.44 亿个格子光内存就得几百 MB速度更是没法看。所以“暴力三维投票”这条路在实际工程中基本走不通。2.2 霍夫梯度法的投票规则OpenCV 的 HoughCircles 默认使用霍夫梯度法Hough Gradient Method它把三维问题巧妙地拆成了两个二维问题这也是它聪明的地方。第一步先用 Canny 算子做边缘检测得到边缘点集合。Canny 的高阈值由 param1 控制低阈值自动取高阈值的一半。第二步对每个边缘点计算梯度方向。梯度的方向垂直于边缘方向而圆的边缘梯度方向恰好指向圆心或者背离圆心取决于明暗关系。这一步通常用 Sobel 算子算 x、y 方向导数再合成梯度向量。第三步沿着每个边缘点的梯度方向以及反向在所有可能的位置上往累加器投票。也就是说如果这个边缘点确实属于某个圆它的梯度方向应该正对着那个圆的圆心。累加器里票数高的位置就极有可能是圆心。这一步把半径问题暂时抛在一边只确定圆心累加器只要二维就够了。第四步确定候选圆心之后再统计每个圆心到各边缘点的距离。对某个圆心来说属于同一个圆的边缘点到它的距离应当近似相等所以在距离直方图上会形成一个明显的峰值这个峰值对应的距离就是圆的半径。OpenCV 内部会对每个候选圆心计算半径支持度最终输出半径和对应的票数。所以严格说OpenCV 的霍夫圆检测做的是“先猜圆心、再投票定半径”的贪心策略。它比三维暴力搜索快得多但也因此牺牲了一部分准确性——比如有些弧形边缘并非完整圆只要足够多的点朝同一个位置投票就可能被误判成圆。这就是它“聪明”和“笨”同时存在的地方。2.3 参数表每个参数到底在管什么HoughCircles 的函数签名OpenCvSharp 版本是CircleSegment[] HoughCircles( Mat image, HoughMethods method, double dp, double minDist, double param1, double param2, int minRadius 0, int maxRadius 0 )六个关键参数的直观含义我按项目里调参的经验整理成表参数作用调大调小我的默认起点dp累加器分辨率与输入图像分辨率的比值速度提升、精度下降、小圆容易丢精度提升、速度变慢1.2minDist检测出的圆心之间的最小距离容易漏掉相邻圆同一圆被重复检出大于目标圆直径的 1/3param1Canny 高阈值边缘强弱的门槛边缘变少弱边缘圆丢失边缘变多噪声边缘混入80~120param2圆心累加器投票阈值圆变少、误检变少但也可能漏检圆变多、容易出假圆25~35minRadius半径下界过滤小圆更多小圆实测半径的 80%maxRadius半径上界更多大圆过滤大圆实测半径的 120%这里有个很容易忽略的耦合关系如果 minRadius 和 maxRadius 给得不准即便圆心投票完全正确半径投票阶段也可能落空导致整个圆被丢弃。反过来如果半径范围给得太宽又会让一些本该被过滤掉的弧线因为“恰好匹配某个半径峰值”而混进来。所以我调参的顺序永远是先确定半径范围再调 param2最后动 param1。3. 在 C# 里落地一个找圆算子3.1 环境准备与工程结构在 NuGet 里装这两个包就行OpenCvSharp4 OpenCvSharp4.runtime.win装了运行时包之后OpenCV 的原生 dll 会被自动拷贝到输出目录不需要手动配置环境变量。有一点要注意OpenCvSharp4 默认只支持 Windows 桌面端如果你后面要部署到 Linux 工控机或者 ARM 平台上需要换成对应平台的 OpenCvSharp4 扩展包。我的工作流工程一般这样组织目录Camera相机采集与回调负责把帧转成 MatPreprocess灰度、滤波、二值化、形态学Operators定位算子比如找圆、找直边、模板匹配Measure几何测量距离、角度、圆度Logic流程控制管节点之间的数据流Communication串口、TCP、PLC 通信霍夫找圆节点就放在 Operators/Geometry 下面输入输出都用自定义的结果结构不直接暴露 OpenCV 类型给上层。“找圆”这个节点在整个流程里的位置通常在预处理之后、测量之前如果项目要做机械手引导它还会负责把像素坐标换算成机械手坐标这个放到第 6 节详聊。3.2 最小可用的找圆代码先来一段可以直接跑起来的最小代码。假设已经有一张图像文件你要检测里面所有圆并绘制出来using OpenCvSharp; class HoughCircleDemo { public static void Run(string imagePath) { // 1. 读图并转灰度 using Mat src Cv2.ImRead(imagePath, ImreadModes.Color); if (src.Empty()) { Console.WriteLine(图像加载失败); return; } using Mat gray new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); // 2. 高斯滤波去噪比什么都重要 using Mat blur new Mat(); Cv2.GaussianBlur(gray, blur, new Size(9, 9), 0); // 3. 霍夫圆检测 CircleSegment[] circles Cv2.HoughCircles( blur, HoughMethods.Gradient, dp: 1.2, minDist: 30, param1: 100, param2: 28, minRadius: 20, maxRadius: 80 ); // 4. 绘制结果 using Mat display src.Clone(); foreach (CircleSegment circle in circles) { Cv2.Circle(display, (Point)circle.Center, (int)circle.Radius, new Scalar(0, 255, 0), 2); Cv2.Circle(display, (Point)circle.Center, 2, new Scalar(0, 0, 255), -1); Console.WriteLine($圆心: ({circle.Center.X:F2}, {circle.Center.Y:F2}), 半径: {circle.Radius:F2}); } using (new Window(Hough Circles, display)) { Cv2.WaitKey(0); } } }代码本身不复杂但有两个细节经常被新手忽略。第一个是高斯滤波。很多项目上来就直接把原始灰度图丢给 HoughCircles结果边缘噪声一大把param2 被迫调得特别高然后真正的圆又检不出来了。我几乎在所有找圆场景里都会先做一次高斯模糊核大小一般取 5×5 到 9×9。核太小去不掉噪点核太大又把细边缘磨没了具体取多少取决于圆的大小——大圆用大核小圆用小核。第二个是 Cv2.HoughCircles 返回的 CircleSegment 数组。数组里已经按投票分数从高到低排好序了所以如果你只需要“最明显的那个圆”直接取第一个元素即可。如果怀疑画面里有干扰圆先把半径范围收紧再从上往下取比事后增加复杂过滤逻辑要省事得多。3.3 把找圆结果接进工作流实际项目里我不会直接让上层代码去操作 CircleSegment 数组而是定义一层结果结构体public class CircleResult { public Point2f Center { get; set; } public float Radius { get; set; } public int Votes { get; set; } // 某些版本的 CircleSegment 不暴露票数可从内部逻辑估算 public bool IsValid Radius 0; }这样做的直接好处是霍夫节点的实现细节被隔离在工作流内部。比如某一天你要把霍夫圆检测换成最小二乘拟合圆只需要改 Operators 那一层Logic 和 Communication 完全不用动。接入工作流的时候还要考虑“找不到圆”的分支。产线上一旦找不到圆不能直接把异常抛给主线程而是应该让工作流进入重试或者容错逻辑先检查图像质量可能是来料位置偏移太大导致圆出界也可能是光源闪烁导致图像过暗。我习惯把每次检测的图像缓存下来配合结果标记一起存储这样排查问题时能直接回放。别小看这个设计现场出了问题不知道是算法问题还是图像问题是最常见也最耗时的困境。4. 一个实战案例瓶盖密封圈项目的参数漂移与调整4.1 项目背景与原始图像这个案例来自我做过的某化妆品瓶盖产线要求检测瓶盖内部的橡胶垫片圆心引导机械手将垫片与瓶盖精确贴合。相机从上往下拍瓶盖外径大约 130 像素垫片内圈大约 100 像素来料在传送带上会有 ±2 毫米的偏移再加上现场环光打在金属瓶盖边缘上会产生反光。项目一开始我把 HoughCircles 的半径范围定在 100~140dp1param180param230minDist20信心满满地跑了测试。实测结果很打脸圆心位置在连续几帧之间跳了 3~5 个像素偶尔会检出金属反光形成的小圆弧当成一个完整圆报出来垫片内圈和外圈边缘同时存在霍夫有时候把外圈当成圆有时候又把内圈当成圆结果不稳定。4.2 第一次参数尝试的错误示范先复盘一下为什么第一版参数会翻车。这张图的真实边缘结构是“双层”的垫片内圈是一条闭合圆边金属外沿又是一条闭合圆边两条边之间的距离只有十几个像素。当 minDist20 的时候外圈和内圈的圆心距离比较近它们各自投票时互相干扰于是霍夫算法一会儿认为这个位置有圆一会儿又认为那边也有圆。再加上 param180 偏小Canny 把金属反光形成的破碎边缘也当成有效边缘这些破碎边缘虽然长度不够但数量多累积投票之后也能凑出一个假圆心。至于那 3~5 像素的圆心漂移主要是由于双层边缘参与投票的比例不稳定导致累加器峰值在真实圆心周围来回摆动。4.3 正确的调参路径第二轮调整我没有再继续玩数字而是先把图像处理链路理顺了。顺序如下第一步加 ROI。原始图像里瓶盖只占中间区域传送带边缘、遮光罩边界都在画面里这些干扰区域会贡献大量无效边缘。我直接裁出以图像中心为原点、边长为 300 像素的正方形区域把找圆范围锁死在瓶盖周围。第二步换预处理策略。高斯模糊从 5×5 加大到 9×9进一步压掉金属反光的细节纹理。同时用固定阈值做了一次二值化把垫片区域和背景分开让后续 Canny 只在垫片区域输出边缘。这一步很关键相当于给霍夫算法“画了重点”。第三步收半径范围。经过实际量测垫片内圈半径稳定在 48~52 像素金属外沿半径稳定在 62~67 像素。我把 minRadius 设为 45、maxRadius 设为 70让算法想漏都漏不掉中间那些弧线。第四步回过来调 param1 和 param2。param1 提到 120Canny 只保留真正强边缘param2 放在 26既能滤掉杂散投票又不至于把真圆过滤掉。dp 从 1.0 改成 1.2累加器分辨率适当降低换来大约 15% 的速度提升精度没有明显损失。最终参数组合是dp: 1.2 minDist: 25 param1: 120 param2: 26 minRadius: 45 maxRadius: 70跑下来的结果是连续 2000 帧检测零漏检圆心位置波动控制在 ±0.3 像素以内单帧检测耗时约 15ms。这套参数至今还在那条产线上稳定运行。回头总结问题不是霍夫算法本身多难调而是我在第一步没有把“该检测的圆到底是什么、图像里还有哪些干扰”想清楚就直接跳到参数环节里瞎试。5. 六个高频问题的排查速查表5.1 圆心漂移、明明有圆但中心偏了先把预处理链捋一遍尤其是高斯模糊核大小和边缘提取阈值。圆心漂移最常见的原因是参与投票的边缘点不对称比如圆的上半部分清楚、下半部分模糊投票中心就会往清楚的那半边偏。解决办法要么是改善打光均匀性要么在检测前做一次形态学闭运算把断裂边缘补上要么把 Canny 阈值稍微降一点让弱边缘也能参与投票。记住霍夫圆的输出精度大概率不是严格的亚像素级如果项目要求重复精度在 0.1 毫米以内后面一定要加精定位环节。5.2 同一张图里检出多个圆全是重复圆先看 minDist。它代表两个圆心之间的最小距离如果你把 minDist 设得比目标圆半径还小同一个圆完全可能被多次检出每次圆心差一两个像素。做个粗略估算法把 minDist 设为预计半径的 1.5~2 倍基本上就不会有连续重复了。如果设大了还会重复那就要怀疑是不是边缘断裂导致投票峰值分裂回到预处理补边缘而不是继续加大 minDist——因为 minDist 过大会把真实相邻圆一起过滤掉。5.3 圆只有半圈检出另外半圈断了这种情况绝大多数不是霍夫参数问题而是图像本身边缘断裂。光照不均匀、工件表面反光、遮挡都会造成边缘中断。我会优先尝试亮度不均就做顶帽变换或者局部直方图均衡反光就调整光源角度遮挡就只能接受并加大 param2 的容错空间。另外霍夫圆本身对缺口的容忍度取决于弧长占比弧长低于一定比例投票峰值撑不起来所以不要指望霍夫能“脑补”出一个超过 30% 残缺的圆。5.4 背景里的圆形纹理被误检成目标圆这是最气人的情况。解决思路通常分三步走第一步收紧 minRadius 和 maxRadius把背景圆形纹理的半径区间排除掉第二步用 ROI 或者模板匹配先粗定位目标区域只在小范围内做霍夫第三步如果还误检加一层后验证在圆心周边的小窗口内检查边缘密度或者灰度均值不符合目标特征的直接丢弃。后验证这层逻辑虽然不复杂但在产线上非常管用。5.5 检测速度不达标先看是不是整幅图都在跑霍夫。一个高效组合是先缩图到 1/2 或 1/4 尺寸做个快速初检拿到大概位置后映射回原图再在 ROI 里做一次精检总耗时通常只有全图检测的百分之二三十。另外把 dp 从 1.0 提到 1.2~1.5 也能明显提速代价是累加器分辨率降低圆心输出可能偏差一到两个像素。如果真的对速度极其敏感可以考虑用 GPU 版 OpenCV不过霍夫圆检测的并行效率相对一般收益没有其他算子那么高。5.6 多个目标圆全局参数怎么给场景里有多个大小不同的圆时全局参数常常顾此失彼。我的做法是分两轮走第一轮用宽松参数找到最大的外圆把它作为区域基准第二轮在“外圆内部区域”分别设置各自的半径范围再做局部霍夫。这样整套流程仍然是一条线性工作流只不过在逻辑上多了一层“由粗到精”的嵌套。实际项目中这个模式非常稳定强烈推荐。6. 几个藏在参数背后的工程经验6.1 预处理往往比霍夫参数更重要写到这里我想再说一个贯穿所有找圆项目的经验把 80% 的精力花在预处理和图像质量上比在 HoughCircles 参数里抠数值要划算得多。灯的亮度、相机的曝光、工件表面的清洁度每一个因素对检测稳定性的影响都比 param2 从 27 改成 28 大。前期调试时我经常做的事就是先把图像缓存一批然后拿同一批图反复跑参数确保算法在“最差的那一帧”上也不挂。上线前还有一个习惯收一定量的坏样本哪怕当时产线跑得好好的也要把可能出现的反光、遮挡、尘点样本提前做进测试集。6.2 坐标转换从像素到机械手的距离找圆最终要给机械手或者 PLC 用像素坐标本身没什么意义。我通常会在相机视场里贴一个标定板用至少 3 个点的像素坐标和机械手坐标算一个仿射变换矩阵然后把圆心坐标投到机械手坐标系。做这一步时有一个容易踩的坑标定板平面必须和实际工件所在的平面高度一致高度差个几毫米边缘位置的坐标误差就会超出允许范围。另外如果相机装在运动轴上标定结果会随位置漂移最好做成多点标定或者用随动补偿。6.3 霍夫粗定位 最小二乘精定位的组合拳最后分享一个我实际项目里的“标准套餐”霍夫圆检测负责粗定位拿到圆心初值和半径初值然后提取圆附近一定范围内的边缘点排除离群点后用最小二乘拟合出精确圆最后再用拟合圆的残留误差判断这个圆是否合格。这样做的原因是霍夫圆在抗遮挡和抗噪声上很强但精度上限一般最小二乘拟合在边缘干净时能到亚像素级但对遮挡和噪声很敏感。两者一粗一精正好互补。我最近一个项目用这套组合在直径约 20mm 的圆上做到了 0.02mm 的重复测量精度。回到这个系列本身从第 1 章到第 50 章每一个算子其实都只是工具真正难的是知道在什么场景下用哪个工具、怎么组合工具。霍夫找圆也是这样它不是一个能凭空扛住所有圆形检测需求的万能算子而是需要你把它放进整个视觉工作流里配合预处理、ROI、后验证和精定位来使用。希望这篇第 50 章的内容能让你在下一个找圆项目里少一些“调一万个参数还是不稳定”的折磨多一些一次就能跑通的把握。
阅读完成 · 觉得有帮助?