资讯详情

资讯详情

玻璃瓶口缺陷检测:传统图像处理与OpenCV实战指南

简介这份资源面向正在做毕设、课程设计或期末大作业的学生以及希望入门计算机视觉与模式识别方向的学习者提供一套基于OpenCV传统图像处理实现玻璃瓶口缺陷检测的完整项目。项目通过bottle_create.py对图像中的瓶子进行定位与框选再由bottle_mouth.py完成瓶口缺陷的检测与判定配套原图与检测结果图便于对照分析。压缩包共44个文件以41张png图像为主包含待检测的原始瓶口与瓶身样本及处理后的结果图另有2个Python源码文件和1份说明文档整体约6.01MB结构清晰、便于直接运行与二次修改。目前已有1246人学习下载适合作为毕设参考或课程设计提交也可在现有代码基础上替换算法、调整参数以训练其他模型帮助读者快速理解传统图像处理在工业缺陷检测中的落地思路与实现细节。1. 玻璃瓶口缺陷检测为什么传统图像处理还没被深度学习淘汰玻璃瓶口缺陷检测这个场景我在产线侧见过不少次。瓶子从退火炉出来瓶口一圈玻璃料如果分布不均就会出现缺口、裂纹、毛刺、内径偏心这几类典型问题。很多刚入行的朋友第一反应是上 YOLO 或者分割网络但真到了产线上你会发现一个尴尬的现实瓶口缺陷的形态高度固定背景几乎不变打光也是固定的环形光或背光这种强约束场景下OpenCV 传统图像处理往往比训练一个模型更快、更稳、更省成本。一套基于 Python OpenCV 的玻璃瓶口缺陷检测方案核心思路就是「固定工位 固定光源 几何先验」用极坐标展开、阈值分割、轮廓分析把缺陷抠出来不需要标注几千张图也不需要 GPU。它适合谁适合做机器视觉落地的工程师、做课程设计或大作业的学生、以及手上有缺陷数据但不想被深度学习训练流程拖住的人。这篇笔记就按「原理怎么立住 → 代码怎么跑通 → 参数怎么调 → 坑在哪」的顺序把这条路线讲透。2. 瓶口缺陷检测的成像与算法选型先想清楚光怎么打2.1 为什么瓶口检测首选背光或环形光玻璃瓶口是透明高反光材质正面打光会直接把相机晃瞎瓶口边缘和缺陷的对比度被反光吃掉。常见做法是背光瓶子立在背光板上瓶口轮廓变成一条清晰的暗边缺口和裂纹会在这条暗边上「断开」或「凸出」。另一种是低角度环形光专门照瓶口端面毛刺和崩边会因为散射差异显出亮斑。选哪种取决于你要检的缺陷类型检缺口、裂纹、内径偏心背光最稳检端面毛刺、划痕环形光更合适。这一步定不下来后面所有阈值都是玄学。成像定了之后算法路线其实就三条一是极坐标展开 一维投影把圆环瓶口拉成矩形缺陷变成竖直条带用列灰度突变定位二是阈值分割 轮廓分析直接找连通域的面积、周长、圆度三是模板差分拿一张标准瓶口图做基准和新图做差。实际项目里我一般把前两条结合先极坐标展开做粗定位再回到原图做轮廓确认。2.2 极坐标展开的数学直觉瓶口是一个圆环直接在上面找缺陷角度和半径两个维度耦合在一起很难设阈值。极坐标展开就是把圆环按角度 θ 和半径 r 重采样成一张矩形图横轴是角度纵轴是半径。展开之后瓶口内圈、外圈变成两条近似水平的直线缺陷就变成矩形图上的局部异常区域。这一步用cv2.warpPolar就能做不需要自己写双线性插值。import cv2 import numpy as np def unwrap_bottle_mouth(img, center, max_radius, out_size(720, 200)): 将瓶口圆环区域展开成矩形图 img: 输入灰度图 center: 瓶口圆心 (cx, cy)先用霍夫圆或轮廓拟合得到 max_radius: 展开的最大半径一般取瓶口外径的 1.1 倍 out_size: 输出尺寸 (角度分辨率, 半径分辨率) # warpPolar 的 flags 里 INTER_LINEAR 保证重采样平滑 # WARP_POLAR_LINEAR 表示线性极坐标展开 unwrapped cv2.warpPolar( img, out_size, center, max_radius, cv2.INTER_LINEAR cv2.WARP_POLAR_LINEAR ) # 展开后默认半径方向是纵轴转置一下让角度在横轴更符合直觉 return unwrapped逻辑说明warpPolar把以center为圆心、max_radius为半径的圆盘区域按极坐标映射到out_size的矩形上。参数out_size的第一个值是角度方向采样数第二个是半径方向采样数。角度分辨率建议 720每 0.5 度一个采样半径分辨率按瓶口环宽来定一般 150 到 250 够用。max_radius取大了会把瓶身也拉进来取小了会切掉外圈缺陷我一般先用霍夫圆测出外径再乘 1.1。2.3 圆心定位整个流程的命门极坐标展开对圆心极其敏感圆心偏 2 个像素展开图上的直线就会变成波浪线后面所有列投影全废。常见做法是先用cv2.HoughCircles粗定位再用瓶口外轮廓做最小二乘拟合精修。如果瓶口本身有缺口霍夫圆会被带偏这时候改用「先阈值分割出瓶口区域再对最大外轮廓做cv2.minEnclosingCircle」更稳。def locate_center(gray): # 背光图里瓶口是暗的先反向二值化 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 开运算去掉毛刺噪声保留瓶口主体 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None # 取面积最大的轮廓假设它就是瓶口 largest max(contours, keycv2.contourArea) (cx, cy), radius cv2.minEnclosingCircle(largest) return (int(cx), int(cy)), int(radius)参数说明THRESH_OTSU在背光均匀时很好用但如果现场光有渐变Otsu 会失效得换成自适应阈值或固定阈值。开运算核大小 5×5 是经验值瓶口毛刺细的话可以降到 3×3但太小去不掉噪声。minEnclosingCircle对缺口不敏感因为它找的是能包住轮廓的最小圆缺口只会让轮廓凹进去不影响外接圆。3. 从展开图到缺陷判定阈值、投影与轮廓三件套3.1 展开图上的列投影找缺口展开成矩形之后瓶口外圈那条暗边在每一列上的位置应该基本一致。如果某一列上这条边突然向内缩说明那个角度有缺口。做法是对展开图做行方向梯度找到每列边缘位置再对边缘位置序列做差分突变点就是缺陷。def detect_gap_by_projection(unwrapped): # 对展开图做纵向 Sobel突出水平边缘瓶口内外圈 grad_y cv2.Sobel(unwrapped, cv2.CV_32F, 0, 1, ksize3) # 取绝对值后按列求最大响应位置即边缘所在行 edge_pos np.argmax(np.abs(grad_y), axis0) # 对边缘位置序列做中值滤波去掉单列噪声 edge_smooth cv2.medianBlur(edge_pos.astype(np.float32).reshape(1, -1), 5).flatten() # 计算与平滑序列的偏差 deviation np.abs(edge_pos - edge_smooth) # 偏差超过阈值认为该角度有缺口 defect_cols np.where(deviation 8)[0] return defect_cols, edge_pos逻辑说明Sobel在 y 方向求导响应最大的行就是灰度突变处也就是瓶口边缘。argmax逐列找边缘行号得到一个长度等于角度采样数的序列。中值滤波核 5 能压掉单列跳变。偏差阈值 8 是像素单位对应展开图半径方向的采样精度如果半径分辨率是 200 而实际环宽 100 像素那 8 像素大约对应 4 个实际像素这个值要根据你的out_size反推。缺陷列找到后还要做角度聚类把相邻列合并成一个缺陷区域避免一个缺口报几十次。3.2 轮廓分析判毛刺和崩边列投影擅长找「边位置突变」但毛刺是「边外侧多出一块」位置不一定突变得回到原图或展开图做轮廓。在展开图上对二值化后的瓶口区域做连通域分析正常瓶口是一个大连通域毛刺会表现为边缘上的小凸起。def detect_burr_by_contour(unwrapped): _, binary cv2.threshold(unwrapped, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) defects [] for cnt in contours: area cv2.contourArea(cnt) # 面积太小的是噪声太大的是瓶口主体 if area 20 or area 5000: continue # 用圆度过滤毛刺轮廓不规则圆度低 perimeter cv2.arcLength(cnt, True) if perimeter 0: continue circularity 4 * np.pi * area / (perimeter ** 2) if circularity 0.3: x, y, w, h cv2.boundingRect(cnt) defects.append((x, y, w, h, area)) return defects参数说明面积下限 20 是滤掉噪点上限 5000 是排除瓶口主体这两个值必须按你的展开图尺寸标定。圆度公式4πA/P²完美圆是 1毛刺一般低于 0.3。boundingRect给出缺陷在展开图上的位置再通过极坐标反变换映射回原图角度就能在原始图像上画框。3.3 模板差分兜底前两种方法对固定形态缺陷有效但遇到内径偏心这种「整体偏移」就不好使。这时候用模板差分存一张标准瓶口的展开图新图与模板做绝对差再阈值化。注意模板要定期更新否则瓶子批次换了、光源衰减了差分图会满屏报警。def detect_by_template(unwrapped, template, diff_thresh30): # 保证尺寸一致 if unwrapped.shape ! template.shape: template cv2.resize(template, (unwrapped.shape[1], unwrapped.shape[0])) diff cv2.absdiff(unwrapped, template) _, mask cv2.threshold(diff, diff_thresh, 255, cv2.THRESH_BINARY) # 形态学闭运算把断裂的缺陷区域连起来 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return maskdiff_thresh取 30 是灰度差经验值光源稳定可以降到 20波动大就提到 40。闭运算核 3×3 是为了把同一缺陷的碎块连起来但核太大会把两个相邻缺陷粘成一个需要按最小缺陷尺寸权衡。4. 避坑与排查瓶口检测翻车的五个真实场景4.1 圆心漂移导致展开图整体波浪现象展开图上瓶口边缘不是直线而是缓慢起伏的波浪列投影满屏报警。原因圆心定位偏了或者瓶子在夹具里有轻微平移。解决不要每帧独立定位圆心改用「首帧定位 后续帧在首帧圆心附近小范围搜索」或者加一个机械定位销保证瓶子位置重复性。如果瓶子本身会转那角度方向也要做配准否则模板差分必挂。4.2 Otsu 阈值在渐变光下失效现象同一批瓶子左边检得出右边检不出二值图一半黑一半白。原因现场光源有渐变Otsu 假设双峰直方图渐变光把双峰抹平了。解决换成cv2.adaptiveThreshold自适应阈值块大小取瓶口环宽的 1/4 左右或者先做背景估计大核高斯模糊再相减。我一般还会在光源前加一层匀光板从源头压掉渐变。4.3 毛刺和噪声分不开现象轮廓分析报出一堆面积 5 到 15 像素的小点人工看原图什么都没有。原因展开重采样引入了插值噪声加上相机本身的热噪声。解决展开前先对原图做一次cv2.GaussianBlur核大小 3×3 或 5×5轮廓面积下限从 20 提到 30再对缺陷做连续帧计数连续 3 帧都报才输出单帧报警直接丢。4.4 极坐标反变换坐标对不上现象在展开图上找到缺陷映射回原图画框框的位置偏了半个瓶口。原因warpPolar展开时角度起点和方向与你的反变换公式不一致OpenCV 默认从 x 轴正方向逆时针展开但图像坐标系 y 轴向下实际是顺时针。解决反变换时用x cx r*cos(θ)、y cy - r*sin(θ)注意 y 的符号。最稳的办法是拿一个已知角度的标记点做一次标定把映射关系验证一遍再上产线。4.5 模板差分被批次差异拖垮现象换了一批瓶子模板差分图满屏都是差异误报率飙升。原因不同批次瓶口的圆度、直径、玻璃料分布有细微差异模板没更新。解决模板不要用单张图用 20 到 50 张良品展开图做像素中值得到一个「平均良品模板」同时给差分加一个空间掩膜只关注瓶口环带区域瓶身和背景不参与差分。5. 把检测结果落到产线标定、节拍与误报率压降5.1 用良品数据标定阈值而不是拍脑袋前面所有阈值——偏差 8 像素、面积 20、圆度 0.3、差分 30——都不该拍脑袋定。正确做法是收集至少 100 张良品图跑一遍流程统计每个指标的分布取「均值 3 倍标准差」作为阈值上限。这样误报率天然控制在千分之三以内。下面这段代码就是做良品标定的def calibrate_thresholds(good_images): deviations, areas, circularities [], [], [] for img in good_images: center, radius locate_center(img) if center is None: continue unwrapped unwrap_bottle_mouth(img, center, int(radius * 1.1)) _, edge_pos detect_gap_by_projection(unwrapped) # 收集良品上的边缘偏差分布 edge_smooth cv2.medianBlur(edge_pos.astype(np.float32).reshape(1, -1), 5).flatten() deviations.extend(np.abs(edge_pos - edge_smooth).tolist()) # 收集良品上的轮廓面积和圆度 _, binary cv2.threshold(unwrapped, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: a cv2.contourArea(cnt) p cv2.arcLength(cnt, True) if 20 a 5000 and p 0: areas.append(a) circularities.append(4 * np.pi * a / (p ** 2)) # 取均值加三倍标准差作为阈值 dev_thresh np.mean(deviations) 3 * np.std(deviations) area_thresh np.mean(areas) 3 * np.std(areas) circ_thresh np.mean(circularities) - 3 * np.std(circularities) return dev_thresh, area_thresh, circ_thresh逻辑说明这段代码把良品图跑一遍收集三个关键指标的分布用统计方式定阈值。dev_thresh是边缘偏差上限area_thresh是轮廓面积上限circ_thresh是圆度下限。注意圆度是「越小越像缺陷」所以取均值减三倍标准差。这套标定跑一次大概几分钟但能把误报率从「凭感觉」压到可量化水平。5.2 节拍不够时先砍分辨率再砍算法产线节拍要求高的时候瓶颈往往在warpPolar和findContours。我的优化顺序是先把相机分辨率从 500 万降到 200 万瓶口区域够用就行再把展开图out_size从 720×200 降到 360×120角度分辨率降一半缺陷定位精度损失可接受最后才考虑把 Python 换成 C 或者用 OpenCV 的 CUDA 模块。不要一上来就上 GPU传统算法在 CPU 上跑 30ms 以内是常态先确认是不是分辨率浪费了。5.3 误报率压降的最后一招多帧投票单帧检测总有偶发误报产线上最实用的压降手段是「连续 N 帧投票」。同一个角度位置连续 3 帧里有 2 帧报缺陷才输出。这招对随机噪声几乎免疫代价是响应延迟增加 2 个节拍。实现上维护一个长度为 3 的环形缓冲区按角度分桶统计命中次数。我一般把桶宽设成 5 度一个缺口通常跨 10 到 30 度分桶不会漏。5.4 一个我踩过的坑别在展开图上直接画框给操作员看早期我把缺陷框画在展开图上给产线操作员看结果没人看得懂。后来改成把展开图坐标反变换回原图在原图上画圆环标记操作员一眼就知道是瓶口哪个角度出了问题。反变换时注意角度方向前面 4.4 提过。另外原图上的框要留一点余量因为展开重采样有精度损失框太紧会让人以为漏检。这套方案我从头跑下来最大的体会是传统图像处理在强约束场景里不是「退而求其次」而是「更对路」。它把问题拆成几何和灰度两个可解释的维度每个参数都能追溯到物理意义出了问题能查、能调、能复现。如果你手上有玻璃瓶口或者其他环形工件的缺陷数据建议先用这套流程跑一遍良品标定看看误报率能不能压到千分之五以内再决定要不要上深度学习。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →