资讯动态

机器视觉弹簧圈数自动计数:六大核心算法与实战指南

发布时间:2026/10/18 19:51:22 来源:尧图企业网站定制
在来料质检里弹簧圈数不对是个老难题。一批弹簧几百上千个人工数圈数既费眼又容易错尤其表面发黑、间距很密的弹簧数到一半就花了。之前接了一个项目要求用工业相机做金属弹簧圈数自动计数用于来料快速检验节拍要求单件2秒以内误检率压到很低。整套方案做下来最核心的不是某个单一算法而是不同场景下怎么组合使用6个核心算法。这篇文章把整个项目从头到尾拆开讲包括硬件选型、6个算法怎么用、OpenCV和Halcon的实战代码还有我在现场踩过的坑给做机器视觉和来料质检的朋友做个参考。整套流程从硬件到软件走下来最直观的感受是弹簧计数看起来只是数“几圈”一旦放到产线上问题就全出来了——反光、遮挡、端部不完整、振动模糊、物料尺寸波动每一样都能让结果翻车。所以下面的内容不只是贴算法还会讲清楚每个算法在什么条件下好用、在什么条件下会失效以及对应的补救手段。1. 项目需求与整体方案设计1.1 这道题到底难在哪弹簧圈数自动计数本质是一个周期结构检测问题。但和螺丝螺纹这种规则件不一样弹簧是三维螺旋结构从相机一侧看过去线圈之间有相互遮挡同一圈的金属丝在投影图上可能被压成一条线也可能因为透视关系产生扭曲。另一个麻烦是端圈。弹簧两端通常有收口和磨平端面的半圈、压并圈和中间圈在图像上的形态完全不一样如果直接按整体周期去数很容易多一截或者少一截。再加上来料弹簧可能有轻微变形圈距不恒定表面有油污、锈迹或反光图像质量很难保证。从项目需求上说来料检验和研发阶段的“测着玩”差别很大。来料检验要求的是稳定、快速、傻瓜化每颗弹簧进来要么判合格要么判退货操作工不需要懂算法。因此方案不能只做一个“能跑通”的Demo必须把成像、算法、通讯这几个环节都固定下来并且要能处理产线上出现的各种干扰。1.2 成像系统的选型逻辑项目用的是某德系工业相机面阵全局快门分辨率500万像素配一颗低倍远心镜头光源用的是背光源。为什么这样搭弹簧属于细长零件放在背光源上面得到的是剪影图前景弹簧是暗的背景是亮的这样受表面反光和纹理影响最小轮廓最干净。如果用正光或环形光弹簧表面的金属纹理、油污会严重干扰二值化计数算法基本上就没法稳定工作。远心镜头的作用是消除透视误差。普通镜头靠近相机一端看弹簧会“近大远小”弹簧两端看起来圈距不一样这对周期计数非常致命。远心镜头在一定景深范围内保持放大倍率一致图像中各处的圈距都真实反映物体的几何比例后面做投影或者频域分析才靠谱。光源这一块我踩过坑。最开始用一个普通的LED平板背光亮度不均匀弹簧边缘一部分清晰一部分发虚二值化后出现断裂带。后来换成了高均匀性的背光源并且在光源面板上加了一层漫射板效果好了很多。对于表面发黑、吸光的弹簧背光亮度还要适当调高否则弹簧边缘和背景灰度差太小阈值分割不稳定。1.3 六条算法线的整体脉络这个项目里我把算法拆成6条线不是每个项目都要全部用而是按实际工况选择组合。它们分别是图像预处理增强——去噪、提升对比度、统一灰度灰度投影计数——从投影曲线数峰谷最直接边缘轮廓极值计数——抓上下包络线的极值点形态学处理与骨架端点分析——处理断裂和端部干扰频域周期估计——用FFT算主频抗局部噪声模板匹配校验——用已知圈数模板做最终确认。这6条线的定位不完全一样。第1条是基础第2、3、5条是“计数主体”第4条是预处理里的关键补充第6条是“校验兜底”。实际项目里我通常用第2条或第5条做主计数用第6条做二次确认最终结果取两者一致的判断。下面逐条展开。2. 六个核心算法逐个拆解2.1 算法一图像预处理增强所有计数算法都依赖清晰、干净的图像但相机拿到的原始图往往带噪声、灰度不均、反光点。预处理的主要目标是让弹簧区域和背景区域尽量“泾渭分明”。我习惯的顺序是转灰度 → 中值滤波 → 对比度增强 → 二值化。中值滤波对去除椒盐噪声和细小反光点非常有效同时相对高斯滤波来说能更好地保留边缘锐度。对于弹簧这种金属件反光造成的孤立亮点经常出现中值滤波比均值滤波更好用。滤波核大小一般取3×3或5×5过大会把细小的线圈边缘磨平尤其小弹簧上很致命。对比度增强可以用直方图均衡化也可以用自适应Gamma校正。工业场景下我更推荐限制对比度的自适应直方图均衡化CLAHE它能避免把背光不均匀的亮度差异放大成伪边缘。二值化这里有一个关键点如果背景亮、目标暗需要用THRESH_BINARY_INV如果弹簧表面很亮、背景暗反过来。而且尽量不要用固定阈值不同批次弹簧的透光率和表面状态会有差异固定阈值很容易出问题。我的做法是先用大津法Otsu自动算初始阈值再根据实测样本手动微调一个偏移量。OpenCV的预处理代码大致这样import cv2 import numpy as np img cv2.imread(spring_raw.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 中值滤波去噪核大小选5太大会损失边缘 gray cv2.medianBlur(gray, 5) # CLAHE增强clipLimit控制对比度强度 clahe cv2.createCLAHE(clipLimit3.0, tileGridSize(8, 8)) enhanced clahe.apply(gray) # 大津法二值化背景亮、目标暗所以用INV _, binary cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 开运算去除细小的孤立噪点 kernel np.ones((3, 3), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1)跑完这段弹簧尽量是一个完整的白色区域背景是黑色。如果弹簧中间还有断开说明预处理还需要进一步调参数而不是急着上计数算法。2.2 算法二灰度投影计数灰度投影是我在这个项目里最常用的算法也是最好理解的一个。把二值化后的弹簧图像沿轴向做像素统计因为弹簧一圈一圈有规律地排列每一圈在投影曲线上会对应一个峰或谷数峰谷的数量就能得到圈数。举例假设弹簧水平放置轴向是X方向。我对图像每一列统计白色像素的数量得到一条“列投影曲线”。弹簧每个线圈在图像上占据一个区域线圈的疏密交替反映成曲线的周期性波动。对规则弹簧来说相邻两个波峰或波谷之间代表一个完整的螺旋周期所以圈数波峰数-1或波峰数具体要看弹簧端部和图像截断情况。使用OpenCV和SciPy的代码import cv2 import numpy as np from scipy.signal import find_peaks # binary是上一步预处理得到的二值图 # 统计每一列白色像素数 col_sum np.sum(binary 0, axis0).astype(np.float32) # 曲线做平滑避免毛刺峰 kernel np.ones(5) / 5 col_smooth np.convolve(col_sum, kernel, modesame) # 找峰peak height 根据实际像素高度设置distance按圈距像素设置 peaks, props find_peaks( col_smooth, height10, # 小于10个白像素的峰忽略 distance20 # 两个峰至少间隔20像素防止误分 ) turns len(peaks) - 1 # 实测后校准偏移量这里有两个参数需要根据实际图像标定height代表一个有效周期在投影曲线上的最小响应强度如果弹簧很细或投影重叠严重数值要调低distance代表最小圈距像素数防止一个圈被误认为两个峰。我通常会在项目初期打印投影曲线人工对照实测圈数来标定这两个参数。投影法的优势是计算量极小一毫秒内就能出结果非常适合高速来料检验。缺点也很明显如果弹簧圈距不均匀、端部压并圈太多或者两个线圈在水平方向上有局部重叠峰谷位置会偏移或者合并造成计数错误。这种时候就要结合边缘轮廓法或者频域法了。2.3 算法三边缘检测与轮廓周期分析投影法是对整幅图做统计圈与圈之间的局部畸变容易被平均掉。边缘检测法则更“聚焦”它只抓住弹簧上边缘或下边缘的轮廓曲线把这条轮廓线当成一个周期信号来分析。具体做法是先用Canny边缘检测提取弹簧的外轮廓然后按X坐标排序取上边缘Y值较小的点构成一条曲线。弹簧在侧影中每一圈都会在上边缘形成一个拱形曲线呈波浪状。数出波浪的数量就等于圈数。算法的关键在边缘点的提取和排序。findContours返回的是轮廓点集合需要先把整个弹簧区域的轮廓点合并再按照X坐标排序。注意弹簧可能分为多个轮廓片段要把属于同一根弹簧的点都合并成一个数组。参考代码import cv2 import numpy as np edges cv2.Canny(enhanced, 30, 100) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if len(contours) 0: raise ValueError(No contour) # 把轮廓点合并并获取上边缘轮廓点 all_pts np.vstack([c.reshape(-1, 2) for c in contours]) # 按X排序每个X只保留Y最小的点作为上边缘 sorted_pts all_pts[np.argsort(all_pts[:, 0])] upper_y [] last_x None for x, y in sorted_pts: if last_x is None or x ! last_x: upper_y.append(y) last_x x upper_y np.array(upper_y, dtypenp.float32) # 平滑 upper_smooth cv2.GaussianBlur(upper_y.reshape(-1, 1), (1, 15), 0).flatten() # 检测极值点一阶差分变号处 dy np.diff(upper_smooth) sign_changes np.where(np.diff(np.sign(dy)) ! 0)[0] 1 # 极值点数量的一半一个完整周期含一个峰一个谷就是圈数 turns len(sign_changes) // 2这个方法对局部畸变的容忍度比投影法高因为上边缘的波浪在单个圈上即使有毛刺极值检测也能通过平滑去掉。但需要注意如果弹簧是倾斜的或者上下边缘重叠严重上边缘曲线会被截断极值数偏少。这时候先在预处理里做旋转校正或者使用远心镜头保证轴向水平效果会好很多。另外Canny的两个阈值也要按图像噪声调节。我发现很多人在Canny参数上很随意但在工业现场图像灰度分布很稳定后阈值一般固定在一个范围就不会乱跳。2.4 算法四形态学处理与骨架端点分析形态学这步最开始我是放在预处理里的后来单独拎出来当算法用原因是有不少弹簧表面有锈斑或油污二值化之后区域内部会有空洞边缘上会有锯齿。形态学开运算能去毛刺闭运算能填空洞这是图像层面的净化。但形态学对计数真正的价值在于它能帮我们定位弹簧的端点位置尤其是端部半圈。来料弹簧的两端形状往往不完整如果直接按投影曲线计数端部的半圈容易被算成整圈或者被忽略。通过形态学腐蚀和骨架提取可以把弹簧区域“瘦”成一条中心线然后再分析中心线的弯曲次数这样对端部缺失不敏感。Halcon在形态学和骨架提取上比OpenCV更顺手直接用算子就能拿到骨架。下面是一段典型Halcon代码处理流程是读取图像 - 灰度化 - 阈值分割 - 连通域筛选排除小杂质 - 骨架化read_image (Image, spring.png) rgb1_to_gray (Image, GrayImage) // 背景亮、弹簧暗所以阈值区域选0到中间灰度 threshold (GrayImage, Region, 0, 100) // 连通域分离去掉小于2000像素的杂质 connection (Region, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, area, and, 2000, 9999999) // 填充内部空洞再开运算平滑边界 fill_up (SelectedRegions, RegionFillUp) opening_circle (RegionFillUp, RegionOpened, 3.5) // 骨架化 skeleton (RegionOpened, Skeleton)有了骨架之后可以再配合get_region_polygon等算子把骨架转成折线分析折线的转折点数量。转折点数量与圈数也呈线性关系具体要用已知样品标定一下偏移量。这个方法的好处是不需要依赖上下边缘即使弹簧有轻微扭转也不会丢圈缺点是对骨架的连续性要求高如果预处理造成骨架断裂转折点会少很多所以前面的连通域筛选和洞填充非常重要。2.5 算法五频域周期估计如果你是“算力洁癖”的人一定会喜欢FFT。灰度投影曲线本质上是一个沿轴向的周期信号弹簧圈距稳定时这个信号的频率是很集中的。通过傅里叶变换找出主频就能算出平均周期长度用弹簧总长度除以周期长度就得到圈数。这个方法的抗局部干扰能力非常好。即使某几个圈因为反光在图像上糊掉了只要整体周期还在频域里的主峰就不会消失。它特别适合那种“圈数很多、局部拍不清、但整体规律”的弹簧。代码要点是先去掉投影曲线的直流分量否则FFT结果里0频分量会直接盖住主频import numpy as np # col_smooth来自2.2节的投影曲线 signal col_smooth - np.mean(col_smooth) # 去直流 n len(signal) # 加窗减少频谱泄漏 window np.hanning(n) signal_win signal * window # FFT spectrum np.fft.fft(signal_win) freqs np.fft.fftfreq(n, d1.0) # d1个像素 # 只看正频部分忽略直流 half_n n // 2 mag np.abs(spectrum[1:half_n]) peak_idx np.argmax(mag) 1 # 主频对应的周期像素数 period_pix n / peak_idx # 弹簧在图像中的长度像素数 spring_len_pix np.count_nonzero(binary 0) # 或者用轮廓外接矩形长 turns spring_len_pix / period_pix turns int(round(turns))这里有个工程细节如果弹簧的端部压并圈长度与正常圈距差别很大频谱主峰会被“拉宽”导致peak_idx搜索不够精确。解决办法是先做一个粗略ROI把端部压并圈区域裁掉只对中间正常圈部分做FFT得到的圈数再加上修正值。像这类“先截断再处理”的思路在整个项目里反复用到了。FFT和投影法可以互为校验投影法峰谷计数对局部敏感FFT对局部不敏感两者结果一致说明计数可信不一致时触发重新拍照或人工复核。2.6 算法六模板匹配校验模板匹配这番操作听起来最“不炫”但在来料检验里反而是最稳的兜底方案。思路很直接提前制作圈数分别为10圈、12圈、15圈等的一组模板然后对待检弹簧图像做形状模板匹配看它和哪个模板得分最高从而判断圈数。如果已有的弹簧图库够全用Halcon模板匹配实现起来很快部署也很稳定。Halcon的形状模板匹配对灰度变化、局部遮挡有很强的鲁棒性特别适合现场光照有小幅波动的情况。核心代码// 离线阶段制作模板 read_image (TemplateImage, template_12.jpg) reduce_domain (TemplateImage, TemplateRegion, TemplateReduced) create_shape_model (TemplateReduced, ShapeModel, auto, -0.39, 0.79, auto, auto, ignore_local_polarity, 5) // 在线阶段找模板 read_image (TestImage, spring_test.jpg) find_shape_model (TestImage, ShapeModel, -0.39, 0.79, 0.6, 1, 0.5, least_squares, 0, 0.7, Row, Column, Angle, Score) if (Score 0.75) // 匹配成功用形状对应的圈数作为结果 endif注意模板匹配返回的Score代表匹配相似度圈数不同但外观接近的弹簧得分可能很接近。所以我通常不只看最高分而是看前两名分数的差值如果第一名明显领先第二名就采信第一名如果领先不明显就交给其他算法复核。这本质上是做一次“相似度鉴别”而不是单纯找模板。模板匹配的另一个好处是可以同时给出弹簧的位置和角度方便后续机器人抓取或分类摆料。在来料检验工位上我把它放在最后一步前面投影法或FFT先算一个圈数模板匹配再去验证两票一致才放行。这样误检率可以压得非常低。3. 参数计算与工程调优3.1 相机分辨率和视野怎么算工业视觉项目里很多人一上来就选高分辨率相机其实没必要。弹簧圈数计数取决于能否分辨单个圈距而不是弹簧表面的微小瑕疵。先算两个指标弹簧最大长度和最小圈距。假设弹簧最长50mm最小圈距1.5mm要求至少用10个像素去表示一圈那么一个圈距在图像里就是10像素。50mm的总长度至少需要50/1.5×10≈333像素的轴向分辨率。考虑到需要留出20%余量防止弹簧摆放偏移400像素就够。听着很小对不对但如果弹簧直径较大你要保证整个弹簧都在视野内就需要更大的传感器这时优先保证覆盖范围而不是盲目追求高像素。实际项目里我选了500万像素相机视野覆盖60mm左右圈距最小1.2mm时一个圈距对应约20个像素完全够用。分辨率再高不仅浪费成本还会带来更大的图像处理耗时。3.2 曝光时间怎么定来料检验有节拍要求弹簧可能是连续通过也可能停在一个工位上。如果是停在工位拍照曝光时间不用太短只要够亮、无手振即可。如果是输送带上动态拍照曝光时间必须短到让图像中的运动模糊小于一个像素。计算方式很简单假设输送带速度200mm/s曝光时间5ms那弹簧在曝光期间移动了1mm如果这个1mm对应10个像素那图像糊得非常明显。要把移动量控制在0.2mm以内曝光时间就要小于1ms。工业面阵相机的全局快门一般能做到几十微秒问题不大但光源亮度也要相应提高否则图像会太暗。3.3 阈值和形态学参数的标定技巧阈值不要以单张图像看着“顺眼”为准要拿20~30张不同批次的弹簧图去跑一遍二值化统计灰度直方图的谷底位置。我发现弹簧表面发黑时目标灰度峰和背景峰距离很近Otsu自动阈值有时会偏到背景里去。解决办法是用Otsu算出的阈值减去一个固定偏移例如把阈值设为Otsu值的90%让分割偏向“宁可多一点不可少一块”后续再用开运算去掉多余部分。形态学的核尺寸也要以最小圈距作为参考。开运算的核宽度不能超过最小圈距的一半否则会把正常线圈之间的间隙都给“开”没了。正常我取3到5像素如果图像中一圈距离是20像素那么开运算核3像素就很安全。4. 常见问题与排查技巧4.1 端部压并圈干扰计数弹簧端部压并圈之间通常没有间隙投影曲线上看不到明显的峰谷而中间圈又很规律。按整体投影峰谷数算压并圈段会被漏数。我的处理办法是先找出投影曲线中峰谷响应最强的中间区域用这个区域的平均峰距外推整根弹簧的圈数然后根据端部形态加补偿值。还可以直接使用算法四的骨架端点分析识别压并圈形成的“实体端”从总长里扣除端部长度再折算圈数。4.2 反光造成轮廓断裂弹簧表面有油膜或者抛光得很亮时背光下依然可能反射出高光斑使二值化后弹簧区域内部出现孔洞。小孔洞可以通过fill_holes填充大范围的断裂则需要检查光源角度是否正对。最好在光源上贴漫射板或者更换偏振背光。偏振方案效果很好但会降低整体亮度需要相应增加曝光时间或光源强度。4.3 弹簧倾斜导致计数偏少如果弹簧在料盘里没有完全水平轴向和相机坐标轴偏了一个角度投影曲线会被拉伸峰距变大FFT主频偏移。最省事的办法是加一个V型槽治具让弹簧每次都落在同一个水平位置。如果无法加治具就用模板匹配先找到弹簧的角度旋转图像后再做投影。日常维护中也要定期检查治具是否磨损V型槽变形会让弹簧高度变化影响景深和放大倍率。4.4 OpenCV和Halcon结果不一致现场出现过投影法计数是15圈Halcon模板匹配判成16圈的情况。原因多半是模板匹配对不同圈数的相似度区分不够或者投影法把某一段毛刺误判成峰。建议建立一个“仲裁机制”两种算法结果相同则直接输出不同则用FFT再投一票三取二仍然不一致就把图像存到一个“待人工确认”文件夹由质检员二次判断。这样既能保证效率又能积累一批难例来反复优化算法。5. 实测效果与个人心得这个项目实际跑下来主计数用灰度投影加FFT校验模板匹配做后端确认识别单根弹簧耗时大约0.6秒包含拍照和图像处理节拍完全满足。准确率在正常来料状态下可以达到99.5%以上主要剩余误差来自弹簧严重变形和端部过度压并。为了应对这两种情况我额外加了一个“置信度过低自动转人工确认”的逻辑而不是强行给一个结果。最后分享一个小技巧不管用哪个算法都要在界面上把投影曲线实时画出来哪怕是只给调试人员看。曲线的周期形态一眼就能看出问题比看二值图直观太多。调试的时候我花了大量时间盯着这条曲线调参数测峰峰距、看频谱主峰几乎一半的优化都是在这里完成的。弹簧计数看着是个小需求但把成像、算法、节拍、容错全串起来一点都不比大项目轻松。希望这套思路能帮你少走几步弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑