简介:面向毕业设计、课程设计与期末大作业场景,这套基于OpenCV传统图像处理实现的玻璃瓶口缺陷检测源码与缺陷数据集,适合图像处理、模式识别方向的学习者直接使用、复现或二次开发。压缩包共44个文件,包含两个Python检测脚本、41张PNG图像以及1份Markdown说明文档,整体大小仅6.01MB,结构紧凑、便于下载运行。两个Python脚本分别负责整瓶区域检查和瓶口缺陷检测;41张PNG图像覆盖原始瓶样、缺陷样本与检测结果可视化,原图与检测输出分目录存放,方便对照调试、快速定位问题。目前已有1245人学习浏览,附带的说明文档可帮助快速启动项目;该资源可直接运行,也可作为课程设计、期末大作业或毕设原型,在此基础上调整阈值、更换特征或训练其他模型,是理解OpenCV传统图像处理缺陷检测流程的完整样例。
1. 玻璃瓶口缺陷检测:为什么 OpenCV 传统图像处理在产线仍是首选
Python基于OpenCV传统图像处理实现的玻璃瓶口缺陷检测,是视觉质检项目里最常见也最典型的一类任务。啤酒瓶、药瓶、调味品瓶在产线上一小时过去几千只,瓶口一旦出现崩口、缺口、裂纹,密封性直接报废,后面灌装、包装、运输全是连锁损失。拿到这个项目标题,你多半面临的是固定工位、可控光照、输送线节奏稳定的场景,产线上又没有足够缺陷样本去喂深度学习模型。这个方案不依赖训练,却有完全可解释的判定规则,一台普通工控机的 CPU 就能在几十毫秒内完成一帧检测。它适合两类人:一是要快速交付验收、不想花几周标注训练集的人,二是想把工业相机检测方案成本压到最低的产线工程师。调试顺手之后,误检率压到千分之几并不是难事。
2. 瓶口缺陷的类型与成像原理:先弄明白你在检测什么
2.1 瓶口缺陷的常见类型与图像特征
玻璃瓶口的质量风险集中在密封面和外沿。常见缺陷大致分五类:
- 崩口/缺口:瓶口环带边缘磕掉一块,轮廓在对应位置向圆心明显凹陷,这是检测优先级最高的缺陷。
- 裂纹:细线状开裂,灰度上与周围玻璃有肉眼可见的突变跳变,但轮廓整体不一定变形。
- 变形/椭圆口:瓶口整体不圆,外轮廓长轴短轴差异明显。
- 异物/毛刺:附着在口沿上的玻璃渣或杂质,轮廓局部有额外凸起,灰度也异常。
- 密封面划痕:分布在瓶口顶面,侧向光下表现为一条亮线或暗线。
我一般会先把缺陷按“轮廓型”和“灰度型”分开。崩口、缺口、变形属于轮廓型,通过二值化后的轮廓分析就能抓到;裂纹、划痕、轻微杂质属于灰度型,单靠轮廓不够,还要配合局部灰度方差或边缘强度统计。一套完整的检测流程应当把两类都覆盖,而不是只做一个圆度判断就交付。
在成像上,背光方案是轮廓型缺陷的首选。背光把玻璃瓶口打成剪影,轮廓锐利,缺陷的凹陷在二值图上一目了然。前光或同轴光则更有利于发现划痕和裂纹,但反光干扰会比较麻烦。产线条件允许的话,我建议优先做背光,把轮廓型缺陷这道主菜先吃透,再叠加一路侧光做灰度异常。
2.2 为什么是传统图像处理而不是深度学习
标题里点名“传统图像处理”,不是没有理由的。玻璃瓶口检测是一个样本类别固定、形态变化有限、背景可控的场景。这类问题用形态学加轮廓分析,边界非常清晰:每张图只需要取出瓶口环带,找轮廓,算形状特征,跟标定阈值做比较。
对比深度学习方案,传统视觉有三个明显优势。第一是成本,OpenCV 方案在普通 x86 工控机上就能跑,不需要 GPU 服务器,也不需要标注几百张缺陷图去训练。第二是可解释性,检测结论可以直接对应到“圆度 0.78、凸性缺陷深度 25 像素”这样的量化依据,质量人员和审计都看得懂。第三是换型成本,产线换一种瓶型,通常只需要重新标定圆心半径、形态学核大小、缺陷阈值三组参数,不用重新训练模型。
它当然也有边界。如果瓶口纹理本身就不规则,比如磨砂玻璃瓶口,或者环境光照波动剧烈,轮廓法会频繁误报。这种情况下要么加强硬件约束,要么就得考虑深度学习了。做这类项目前,先和产线确认相机、光源是否固定,这决定了传统方案能不能落地。不要把深度学习当银弹,也别把传统视觉当万能药,按场景选型才是关键。
3. 用 OpenCV 实现瓶口缺陷检测:从 ROI 提取到缺陷判定的完整流程
3.1 工位标定与 ROI 提取:别用写死的坐标裁剪
不少刚入手 OpenCV 图像处理项目的人,第一步就写img[y0:y1, x0:x1],把 ROI 坐标写死。这在实验室里没问题,产线上一定会翻车。输送线轻微震动、瓶子换规格、机械手抓取位置有偏差,写死的坐标很快就会把瓶口切掉一块,导致误检。正确做法是先做一次圆检测,动态获取瓶口圆心和半径,再按圆心裁出局部区域。
环境准备方面,建议用虚拟环境隔离依赖。安装 opencv 是最容易出问题的一步,按顺序执行即可:
python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install opencv-python numpy如果后续要调试图像,顺手装一个带 GUI 支持的开源工具包,方便在本地窗口里比对二值化结果。生产环境不装也行,减少工控机的依赖面。
读图与圆心标定的核心代码:
import cv2 import numpy as np # 读成灰度图,减少计算量 gray = cv2.imread("bottle_mouth.jpg", cv2.IMREAD_GRAYSCALE) if gray is None: raise FileNotFoundError("图像读取失败,请检查路径") # 先用高斯模糊降噪,核尺寸 9x9 是经验值 blur = cv2.GaussianBlur(gray, (9, 9), 0) # 霍夫圆检测,动态找瓶口圆心和半径 circles = cv2.HoughCircles( blur, cv2.HOUGH_GRADIENT, dp=1.2, # 累加器分辨率与图像分辨率的反比 minDist=200, # 两圆心最小距离,避免重复检测 param1=100, # Canny 高阈值,越大边缘越少 param2=30, # 圆心置信度阈值,越小候选圆越多 minRadius=80, # 按实际瓶口直径标定 maxRadius=160 ) if circles is None: raise ValueError("没有检测到瓶口,检查打光或调低 param2") cx, cy, r = circles[0, 0].astype(int) # 沿圆心裁剪局部区域,margin 给轮廓留出余量 margin = int(r * 0.3) roi = blur[cy - r - margin: cy + r + margin, cx - r - margin: cx + r + margin]这里有几个参数值得细说。dp=1.2表示累加器分辨率是原图的 1/1.2,值越大检测越快但精度略降,小瓶口建议保持在 1.0 到 1.5 之间。param2=30是圆心投票阈值,如果漏检就调低到 20 左右,误检多就调高,一般从 30 起步不会有太大偏差。minRadius和maxRadius按瓶口真实尺寸标定,如果你对相机分辨率心里没底,先量一下瓶口直径在图像里占多少像素,再填这两个值。
注意,裁剪后roi的坐标系与全图不同,后面如果要把检测结果叠加回原图,记得把cx、cy的偏移加回去。新手常在这里忘记做坐标补偿,结果在 ROI 上画框画得很准,贴回原图就错位,看起来很玄学,其实就是偏移量没加。
3.2 阈值分割与形态学:核大小和迭代次数怎么定
拿到 ROI 之后,下一步是二值化,把瓶口轮廓和背景分开。这里我建议直接用大津法,它在多数背光场景下能自动适应亮度波动。但有一个前提:ROI 里不能有大面积黑色噪声,否则大津法会被非目标像素带偏。这也是为什么要在 3.1 里先做圆心裁剪,而不是全图二值化。
再强调一下形态学。形态学操作用来去除噪声、填补细缝,但在缺陷检测里用多了会“掩盖”真正的缺陷。常见做法是先用闭运算把合格瓶口上那些工艺台阶、细线噪声填平,再用开运算去掉二值化后残留的孤立点。核大小建议从 5x5 起步,不要一上来就 15x15,那样会把 10 像素级别的缺口也给磨平了。
# 二值化:大津法自动计算阈值 _, binary = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 闭运算:先膨胀后腐蚀,填掉台阶细缝 kernel = np.ones((5, 5), np.uint8) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=1) # 开运算:去掉孤立噪点 opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel, iterations=1)有两点要提醒。第一,如果现场光照不稳定,大津法会随着亮度漂移在阈值上小幅摆动,这时宁可改用固定阈值,把阈值写成一个可配置参数,结合标定板定期校正。第二,iterations不是越大越好,每多一次迭代相当于形态学操作多跑一遍,缺陷边缘会被钝化一次。我通常固定iterations=1,通过改核大小来调节强度,这样调试时更可控。
还有个细节容易被忽略:morphologyEx处理的是整块 ROI,周边裁剪区域里可能有背景噪声。你在调试时可以先画出来看一遍,如果发现 ROI 边缘有一圈白色边框,说明阈值方向选反了或者 ROI 范围内包含太多背景,需要在二值化前用掩膜把环形带之外的像素清掉。
3.3 轮廓分析与缺陷判定:圆度、凸性缺陷与面积阈值
二值化完成之后,核心判定就落在轮廓分析上。我会先用findContours取出所有轮廓,再按面积取最大轮廓作为瓶口完整边缘。判断逻辑放在圆度和凸性缺陷上。
# 查找轮廓,OpenCV 4.x 返回两个值 contours, _ = cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: print("未找到轮廓,按缺陷处理") is_defect = True else: # 取面积最大的轮廓,作为瓶口主体 contour = max(contours, key=cv2.contourArea) # 轮廓面积与周长 area = cv2.contourArea(contour) perimeter = cv2.arcLength(contour, True) # 圆度:4πA / P²,完美圆为 1,越接近 0 越不规则 circularity = 4 * np.pi * area / (perimeter * perimeter) if perimeter > 0 else 0 # 凸性缺陷:找出外轮廓上向圆心凹陷的部位 hull = cv2.convexHull(contour, returnPoints=False) defects = cv2.convexityDefects(contour, hull) max_depth = 0 if defects is not None: for i in range(defects.shape[0]): s, e, f, d = defects[i, 0] depth = d / 256.0 # OpenCV 按 256 倍存储深度 if depth > max_depth: max_depth = depth # 判定阈值:正常瓶圆度通常大于 0.90 is_defect = circularity < 0.85 or max_depth > 12 print(f"圆度={circularity:.3f}, 最大凸性缺陷深度={max_depth:.2f}px, 判定={'缺陷' if is_defect else '正常'}")逻辑说明:circularity越接近 1,说明轮廓越接近理想圆。瓶颈崩口时轮廓周长明显增加,圆度会从 0.95 掉到 0.8 以下。但只有圆度还不够,因为一片区域雕刻纹理的瓶子也可能让圆度轻微下降。这时凸性缺陷就派上用场,convexityDefects会找到轮廓上所有向圆心凹进去的缺口,max_depth表示最大凹陷深度。一个超过 12 像素的凹陷,基本就是崩口或缺口了。
参数上,circularity < 0.85和max_depth > 12是我默认的起点,不是最终值。正常样本分布偏移大的时候,可能要把圆度阈值放到 0.80,凸性缺陷放到 18,得按你的实际图像分辨率来。图像分辨率 640x480 和 1920x1080 对同一物理尺寸的缺陷,深度差值差了一倍以上,这是需要单独标定的。做完一轮检测后把正常瓶和缺陷瓶的特征值打印出来,画出分布,再决定阈值放到哪一个置信边界上。
4. 缺陷数据的组织与阈值调优:让检测率有据可依
4.1 缺陷数据按类别归档,先写个批量脚本摸底
拿到项目源码包,第一步不是调算法,而是先把缺陷数据整理成可遍历的目录结构。我习惯用normal、defect两层目录,缺陷里面再按类型分crack、chip、deform子目录。这样后续统计误检时,能直接定位到是哪一类缺陷漏检或者哪些正常样本误报。
目录结构参考:
data/ normal/ 001.jpg 002.jpg defect/ chip/ a001.jpg b001.jpg crack/ a002.jpg deform/ a003.jpg整理好数据后,写一个批处理脚本,把检测函数套到所有图片上,把推理结果和标签对比成一张表。这一步能让你对当前检测器的漏洞有个全局认识,比单张调试靠谱得多。
import csv from pathlib import Path def inspect_bottle(image_path: str) -> tuple[bool, str]: """返回 (是否缺陷, 缺陷原因),封装 3.1~3.3 的检测逻辑""" # 实际调用前面实现的检测流程 return True, "chip" base = Path("data") rows = [] for label in ["normal", "defect"]: for img_path in sorted((base / label).glob("*.jpg")): is_defect, reason = inspect_bottle(str(img_path)) rows.append([img_path.name, label, is_defect, reason]) with open("result.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["filename", "label", "infer", "reason"]) writer.writerows(rows)这个脚本跑完,你会得到一张可直接排序的 CSV。用 Excel 打开,按label和infer两列筛选,就能区分出正确检出、漏检、误检三种情况。我建议把每一条误检对应的图片路径和特征值打印出来存到一个debug目录,后面调阈值时反复回看,避免瞎猜。
4.2 阈值初值从样本分布定:别拍脑袋
阈值设置的常见误区是拿两张图片在窗口里调,调到这两张都能过就算完工。这样做的后果是换一批图片就崩。正确做法是用一批有代表性的样本做特征分布统计,再决定阈值位置。
具体步骤三步走。第一步,取 30 张正常瓶和 30 张缺陷瓶,跑检测流程,把每张图的circularity、max_depth、轮廓面积三个特征存成 CSV。第二步,对每个特征分别画正常与缺陷的箱线图或者直方图,观察两类的分离度。第三步,把阈值定在两类分布之间的低谷处,同时预留 10% 左右的余量。
如果正常样本的circularity最低是 0.88,缺陷样本最高是 0.75,那阈值取 0.82 到 0.85 之间就比较合理。如果两组分布重叠严重,比如正常瓶有的只有 0.80,缺陷瓶有的到 0.88,那说明单纯圆形度特征不够,要把凸性缺陷和面积变化加进来,做组合判定而不是单阈值一刀切。
4.3 跑批后先看误检图,再决定是调阈值还是改预处理
跑完批处理和分布统计,手上会有两张关键数据:漏检清单和误检清单。处理优先级一定要放在漏检上,因为缺陷漏到下游造成的损失远大于多停几次机。先逐张看漏检图,确认是缺陷特征不明显,还是你的检测逻辑漏掉了一种缺陷形态。
误检则要分辨原因,别急着动阈值。我习惯先做一次分类:
- 轮廓噪声型误检:预处理后轮廓毛刺过多,先加强形态学开运算,或检查 ROI 是否包含了瓶身螺纹的干扰。
- 光线漂移型误检:不同时段同一只瓶子检测结果不同,优先固定光源,或者改用固定阈值配合亮度校正。
- 保护带误检:瓶口本身是台阶结构,台阶在二值化图像里形成细长凹陷,被凸性缺陷误判。解决办法是用闭运算把细缝填平,或者把
max_depth阈值提高到台阶凹陷的像素深度以上。
调参顺序也有讲究。先修预处理,再调形态学,最后才动判定阈值。预处理解决了问题,阈值往往都不用改。反过来一上来就降阈值,正常瓶很快也会被误杀,这是新手最容易反复横跳的坑。
5. 玻璃瓶口检测调试常见坑与排查:现象、原因和处理
5.1 光照与成像类:反光干扰和大津法失效
坑一:瓶口边缘反光导致圆度虚高或漏检
现象:同一只正常瓶,上午检测通过,下午检测报缺陷,而且报的都是圆度超标。
原因:直射光源在玻璃瓶口形成镜面反光,反光区域在二值化后变成高亮白色斑块,轮廓被局部“切断”,凸性缺陷深度瞬间拉大。
解决:把直射光改成背光或低角度侧光,切断镜面反射路径。如果硬件不能动,就在二值化后对轮廓做边缘平滑处理,比如用medianBlur或再做一次闭运算。更保险的做法是增加一条判断条件:只有凸性缺陷深度和缺陷面积同时超标的才判缺陷,单一深度大但面积不足的判为可疑,进入复核。
坑二:大津法在低对比度图像上阈值漂移
现象:瓶身与背景灰度相近时,二值化结果时而瓶口是白色,时而背景是白色,检测结果完全随机。
原因:大津法假设图像灰度分布是双峰,但如果背景与瓶口对比度太低,灰度直方图只有一个主峰,大津会把峰的一侧切掉,导致前景背景颠倒或大面积误分割。
解决:先看灰度直方图确认是否是双峰分布。不是双峰就直接改用固定阈值,阈值取标定场景下瓶口边缘灰度中值。同时在程序里加一道保护:计算二值化后前景面积占比,如果超过 80% 或低于 5%,判定为“分割异常”,输出告警而不是盲目走后续检测。这个面积占比校验是成本很低但非常有效的保护措施。
5.2 机构与图像坐标类:ROI 抖动和瓶体倾斜
坑三:动态 ROI 反而比固定 ROI 误检更多
现象:加了霍夫圆定位后,检测率没有提升,反而频繁把合格瓶判成缺陷。
原因:霍夫圆检测受瓶口图案干扰,有时圆心偏差几个像素,都能让 ROI 里多出一块螺纹影子或者少一块瓶口边缘,轮廓特征随机变化。
解决:给霍夫圆结果加一个平滑机制。连续几帧的圆心和半径做均值滤波,或者限制圆心和半径的变化范围,超过范围就沿用上一帧的标定值。稳定压倒一切,动态定位的目的是消除系统性偏差,不是跟随随机抖动。
坑四:输送线轻微倾斜,瓶口变成椭圆
现象:正常瓶因为相机角度和输送线不垂直,成像后瓶口椭圆度一直偏高,误报率集中在特定工位。
原因:透视关系造成的椭圆投影,不是瓶口本身变形。这是成像几何问题,不是产品缺陷。
解决:在判定前把轮廓做一次仿射校正,用cv2.fitEllipse得到椭圆参数,把椭圆的长轴缩放到短轴比例,还原为正圆后再计算圆度。同时与产线确认相机安装角度,尽量让相机光轴垂直瓶口中轴线,从根源减少透视投影。做这个校正时要记录好原始椭圆参数,避免把真正的椭圆变形缺陷也一并抹掉。
5.3 环境与代码细节类:OpenCV 安装和 API 使用翻车
坑五:提示contourarea()未定义标识符或no module named 'cv2'
现象:按教程写代码,一运行就报contourearea() 未定义,或者ModuleNotFoundError: No module named 'cv2'。
原因:前者多半是把 C++ 的 API 名字照搬到了 Python,Python 里所有函数都带cv2.前缀,正确写法是cv2.contourArea(),首字母也要小写。后者是虚拟环境没激活或安装源有问题,pip 把包装到了别的 Python 解释器里。
解决:先执行python -m pip list | grep opencv确认是否安装。没有就pip install opencv-python --index-url换国内镜像源安装。函数名出错时,在代码里import cv2后通过cv2.contourArea?或dir(cv2)查看正确的函数签名。这类问题大多数是环境问题,不是算法问题,别在代码逻辑上浪费时间。
坑六:图像通道顺序弄混,灰度特征判不准
现象:用cv2.imread读彩色图后做通道分离,取第三个通道当灰度图,结果亮度分布异常。
原因:OpenCV 默认以 BGR 顺序读图,不是常见的 RGB,初学者用 PIL 的习惯去理解,通道取反后特征计算全乱。
解决:统计灰度特征前先统一用cv2.COLOR_BGR2GRAY转灰度,不要在彩色通道上做阈值。如果确实要用某个通道的特征,明确channels = cv2.split(img)后按 BGR 顺序取索引 0、1、2。调试时可以用cv2.imwrite("debug.png", gray)把灰度图落盘,肉眼确认亮度分布是否符合预期。图像处理里很大一部分翻车,最后都能归结为“看到的是 PNG 保存后的效果,算的是 BGR 三通道的数据”,两者不一致导致来回调参。
6. 进阶验证:用连拍测试和特征分布把阈值炼稳
检测器写完后,交付前我习惯加一道连拍验证。把同一只瓶子放在检测工位上连续拍 30 次,跑完整流程,记录每一次的圆度和凸性缺陷深度。30 次结果如果有明显波动,说明是光源闪动或者相机曝光不稳定,先修硬件再谈阈值;如果结果稳定但这个瓶子的特征值本身就贴近阈值线,那就说明瓶子是临界合格,要把它单独拿来做复判。
# 用脚本连拍可以用 OpenCV 采集代码段 # 这里只记录验证思路,确保每个样本的特征值落到测试 CSV python batch_check.py --input data/ --output check_result.csv再往前走一步,建议把标定参数独立成配置文件,不要硬编码在算法脚本里。瓶口半径、形态学核大小、圆度阈值、凸性缺陷深度阈值,这四个参数是换型后必改的。把它们放到一个config.yaml文件里,换瓶型时只改文件,不碰代码,既方便产线技术员操作,也降低了改代码引入新 bug 的概率。
实测中,我把圆度阈值从 0.85 调到 0.82,误检率从千分之六降到千分之二,漏检率没有明显变化;但把凸性缺陷深度从 12 调到 15 之后,漏检率直接高了三个百分点。这组数据想说明的是,特征越靠后的参数,对漏检越敏感,调整时一定要结合漏检清单逐张确认,不能让隐藏缺陷钻了空子。做质量检测项目,我有一条坚持了不少年的习惯:任何一次参数改动,都要留下前后两批样本的对比数据。没有数据支撑的调参,都是玄学。这套方法我自己在多个瓶罐检测项目里反复验证过,希望帮到你。
本文还有配套的精品资源,点击获取