简介:这是一套基于Python与OpenCV的烟丝检测分割项目代码,面向计算机视觉初学者及烟草行业质检场景,解决图像中烟丝自动识别、轮廓提取与面积宽度测量等问题。项目涵盖图像读取、颜色空间转换、滤波去噪、Canny边缘检测、轮廓发现、形态学优化及区域特征统计等典型流程,适合学习OpenCV图像处理管线的人对照练习。包体共12个文件,以Python主脚本为核心,附带多张示例图像、轮廓结果图及开发环境配置文件,压缩包约13.32MB,结构紧凑便于直接运行。已有591人学习下载。代码内置了多种场景的图像样本,可直观对比检测分割效果,并借助轮廓面积、周长计算等函数对烟丝区域做量化分析,为烟丝宽度、面积统计提供可扩展基础。整个实现既涵盖传统图像处理的关键步骤,又具备实际工业质检的迁移价值,适合作为计算机视觉项目的入门参考与二次开发起点。
1. 烟丝检测分割:用 OpenCV 把细长条物体从照片里单独拎出来
拿到cut_tobacco.zip这个压缩包的时候,我第一反应是烟草行业的质检场景又来了。质检线上拍摄的烟丝图像普遍有个特点:背景纹理杂、单根烟丝细长且相互交叠,靠人工目检效率低,靠深度学习标注成本又高。这个项目走的是经典计算机视觉路线,用 Python + OpenCV 把烟丝从图像里自动分割出来,再统计每根烟丝的面积、周长和宽度。包里main.py是完整处理链路,train.jpg、test.jpg是不同批次的待测样本,contoursImage2.jpg是轮廓可视化结果,bee.jpg则用来验证算法在非烟丝场景下的泛化能力。如果你手头也有细长条物体的检测需求——纤维、毛发、短线缆,这套代码的思路可以直接平移过去。
2. 图像预处理:为什么灰度化之后要先滤波再二值化
2.1 三个关键 CV 操作的选型理由
烟丝图像处理的第一步不是直接找轮廓,而是把图像调整到适合做分割的状态。使用cv2.imread()读取到的原始图像是 BGR 三通道格式,烟丝通常是黄褐色,背景可能是深色托盘或传送带,两者在灰度空间里已经能拉开差距。此时用cv2.cvtColor()转灰度,比在彩色空间里分割省掉大量计算量。
灰度化之后图像里仍然有噪声,典型来源是传送带表面的细微纹理和拍摄时产生的传感器噪点。噪声如果不处理,会让后续 Canny 边缘检测多出一圈假边界。常见做法是先用cv2.GaussianBlur()做高斯滤波,使用 5x5 的核,sigma 取 0。高斯滤波对高斯噪声抑制效果最好,而且不会像中值滤波那样把烟丝的细长结构磨掉。
滤波完进入二值化环节。固定阈值cv2.threshold()在这个场景下不好使,因为不同批次烟丝的颜色深度和光线条件有差异。用大津法(OTSU)自动求阈值更稳,它能根据图像灰度分布算出最优分割阈值,相当于省掉了每次调参的过程。这个项目里二值化的结果直接决定了后续轮廓检测的质量,二值化做得干净,轮廓提取就成功了一半。
2.2 main.py 预处理链路的实际代码与参数说明
import cv2 import numpy as np # 读取测试图像,IMREAD_COLOR 是默认模式,保留原始色彩信息 img = cv2.imread('train.jpg', cv2.IMREAD_COLOR) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 5x5 高斯核,sigmaX 设为 0 表示由核尺寸自动计算 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # OTSU 自动阈值,返回值为阈值本身和二值化结果 thresh_val, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) print(f"OTSU 自动计算出的阈值为: {thresh_val}")这段代码是预处理链路的骨架。GaussianBlur的核尺寸如果设成(3, 3)会保留更多细节,但噪声抑制能力弱;设成(7, 7)则过度平滑,烟丝边缘会被抹掉一部分。实际操作中(5, 5)是烟丝这类中等纹理目标的上限——比它大就容易让细长物体断成碎片。
cv2.threshold()的 OTSU 模式要求传入的第二个参数随意填,0只是占位,真正的阈值由算法计算。注意THRESH_BINARY + THRESH_OTSU是标志位相加,不能只写一个。二值化图像里烟丝为白色前景,背景为黑色,这是 OpenCV 轮廓检测的默认预期格式。
调试时我建议加一行cv2.imwrite('binary_debug.jpg', binary),把二值化结果存下来看。如果烟丝大面积断线,说明滤波核太大把细节抹平了;如果背景上还有大量白点,说明 OTSU 在这种光照下失效,需要退回手动阈值或改用自适应阈值cv2.adaptiveThreshold()。
3. 轮廓提取与筛选:findContours 的参数一错,结果全乱
3.1 RETR_EXTERNAL 与 CHAIN_APPROX_SIMPLE 到底怎么选
预处理做完,核心环节是用cv2.findContours()提取烟丝轮廓。这个函数有三个关键参数:轮廓检索模式、近似方法和偏移量。检索模式里最常用的是RETR_EXTERNAL和RETR_TREE。RETR_EXTERNAL只取最外层轮廓,适合目标物体互不嵌套的场景;RETR_TREE会提取全部层级关系,适合有内外结构的物体。
烟丝场景取RETR_EXTERNAL就够。烟丝之间即便相互交叠,在二值化图像里也是并排的独立连通域,没有嵌套关系。如果误用RETR_TREE,轮廓数量会爆炸,一根烟丝可能被拆成多段,后续面积统计全部失真。
近似方法选CHAIN_APPROX_SIMPLE,它只保留轮廓的端点,把直线段压缩掉,极大减少内存占用。对烟丝这种细长轮廓来说,端点信息已经足够描述形状。CHAIN_APPROX_NONE虽然保留全部点,但轮廓数组会大好几倍,处理大图时内存压力明显。
3.2 用 contourArea 和 arcLength 把噪点挡在门外
# 提取轮廓,注意 OpenCV 版本差异,返回值可能是 2 个也可能是 3 个 contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) valid_contours = [] area_list = [] perimeter_list = [] for cnt in contours: area = cv2.contourArea(cnt) perimeter = cv2.arcLength(cnt, True) # 面积阈值过滤掉小噪点,周长阈值过滤掉零散短线 if area < 50 or perimeter < 20: continue # 计算外接矩形宽高比,烟丝是细长条,宽高比通常大于 3 x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = float(w) / h if h > 0 else 0 if aspect_ratio > 3 or aspect_ratio < 0.33: valid_contours.append(cnt) area_list.append(area) perimeter_list.append(perimeter) print(f"烟丝候选: 面积={area:.1f}, 周长={perimeter:.1f}, 宽高比={aspect_ratio:.2f}")这段筛选逻辑里三个参数的物理意义要搞清楚。面积阈值 50 是像素平方单位,如果你的图像分辨率是 4000x3000,这个阈值可以往上调到 200;分辨率只有 640x480 时,50 反而可能误杀真实烟丝。阈值应该是图像分辨率的函数,不建议一把尺子量所有图。
周长阈值过滤的是那些面积达标但形状零碎的轮廓。周长过短意味着这个连通域不够完整,大概率是噪声聚集。宽高比过滤是烟丝检测里最关键的几何约束——烟丝本质是细长条,外接矩形必然是一边远大于另一边。如果检测目标是碎烟末而非完整烟丝,这个宽高比条件可以放宽甚至删除。
另一个常见做法是把面积和周长组合成一个圆形度指标4 * pi * area / (perimeter ** 2)。烟丝的圆形度远小于 1,而圆形颗粒噪声的圆形度接近 1,用这个指标能很好地区分两者。
cv2.arcLength()的第二个参数True表示轮廓是闭合的。如果你在处理的是开放轮廓,这个参数要改成False,否则计算结果会偏大不少。我在实际项目里就吃过这个亏,统计周长偏大 10% 以上。
4. 形态学优化与分割结果可视化:如何把断裂的烟丝接回去
4.1 闭运算补缺口、开运算去毛刺的先后顺序
二值化图像里的烟丝常有断裂,原因包括烟丝本身纹理不均匀、光照在边缘产生阴影、高斯滤波过度平滑等。直接用cv2.findContours()会把一根烟丝拆成两三段。这时候形态学操作就派上用场了。cv2.morphologyEx()支持四种常见操作:腐蚀、膨胀、开运算、闭运算。腐蚀去掉小凸起,膨胀填充小凹陷,开运算是先腐蚀后膨胀,闭运算是先膨胀后腐蚀。
烟丝断裂处通常是几个像素宽的缝隙,用闭运算正好能把它接上。核尺寸(3, 3)起步,如果断口超过 5 个像素,(5, 5)的核更合适。相反,如果二值化之后背景残留细小噪点,开运算先把这些噪点腐蚀掉,再膨胀回来,效果比直接面积过滤更彻底。
我在这个项目里推荐的顺序是:先开运算去噪点,再闭运算接断口。顺序反了的话,闭运算会把噪点也连进烟丝主体,后续面积统计虚高。形态学核的形状用椭圆形cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))比矩形核更贴合烟丝的圆形截面,不会在边缘产生方形突出。
4.2 轮廓填充与结果标注的常见做法
# 生成形态学核,MORPH_ELLIPSE 椭圆核适合细长条目标 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) closing = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel, iterations=2) # 形态学处理后再提取一次轮廓 contours, _ = cv2.findContours(closing, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 创建一个全黑画布用于填充烟丝区域 mask = np.zeros_like(gray) cv2.drawContours(mask, contours, -1, 255, thickness=cv2.FILLED) # 在原图上绘制彩色轮廓,便于人工核对 result = img.copy() cv2.drawContours(result, contours, -1, (0, 255, 0), 2)注意这段代码里cv2.morphologyEx的iterations参数。闭运算迭代 2 次意味着膨胀和腐蚀各执行两轮,断口的连接效果更强,但代价是烟丝轮廓轻微膨胀变粗。迭代次数超过 3 后,两根平行烟丝之间的缝隙可能被闭运算误连成一根,这个度要自己把握。
cv2.drawContours()的thickness=cv2.FILLED是填充模式,它生成的mask是逐像素标记的分割结果。如果你只需要轮廓线做展示,把thickness改成2就行。这里有个细节:填充用的mask是单通道 8 位图像,像素值为 0 或 255,后续可以用cv2.bitwise_and()把背景抠掉,只保留烟丝区域。
轮廓绘制完成后,建议把原图和结果图拼接在一起做对比,cv2.hconcat()或np.hstack()都可以。项目里的contoursImage2.jpg就是这种可视化产物,它存在的意义是让工程师一眼看出有没有漏检或误检,比直接看数据更直观。
5. 避坑清单:烟丝检测最容易翻车的五个位置
5.1 OpenCV 版本差异导致 findContours 返回值不一致
现象:代码在本地运行正常,部署到服务器上直接报ValueError: not enough values to unpack。
原因:OpenCV 4.x 之前的版本cv2.findContours()返回 3 个值,4.x 之后返回 2 个值。很多旧教程写的image, contours, hierarchy = cv2.findContours(...)拿到新版本上必然炸。
解决:用contours, hierarchy = cv2.findContours(...)统一接收。如果确实需要兼容旧环境,用try判断返回值长度,或者固定写死要求部署环境 OpenCV 版本不低于 4.2。版本号在requirements.txt里写清楚比在代码里做兼容更省事。
5.2 contourArea 突然报未定义标识符
现象:cv2.contourArea(cnt)在执行时报AttributeError: module 'cv2' has no attribute 'contourArea'。
原因:多数情况是cv2被其他同名的 Python 模块覆盖了,或者是安装的 opencv-python 不完整,只装上了cv2的头文件,底层 C++ 扩展没编译成功。我在一个项目里遇到过 pip 安装 opencv-python 时网络中断,包体不完整,部分函数缺失。
解决:pip uninstall opencv-python后重新安装pip install opencv-python --no-cache-dir。如果还不行,去 Python 交互环境里打印cv2.__version__和cv2.__file__,确认实际加载的是哪个路径下的模块。
5.3 二值化后烟丝大面积断裂,findContours 数量爆炸
现象:处理test.jpg时轮廓数量超过 500 个,但肉眼数下烟丝顶多 30 根,面积统计完全失真。
原因:预处理阶段的高斯滤波核太大,加上二值化阈值偏低,烟丝内部出现大量空洞。烟丝本身的颜色是不均匀的,同一根烟丝上可能有深色段和浅色段,固定阈值会把这些色差直接切碎。
解决:把GaussianBlur核降到(3, 3),或者干脆换用cv2.adaptiveThreshold()自适应阈值,它会对局部区域分别计算阈值,烟丝内部的均匀区域不会被打散。形态学闭运算的iterations提高到 2 或 3,能把小碎段重新连上。
5.4 imshow 在无显示器服务器上直接崩溃
现象:代码执行到cv2.imshow()时抛出cv2.error: ... GUI error,或者直接显示段错误退出。
原因:服务器上没有 X Server 或 Windows 图形环境,OpenCV 的高层 GUI 接口无处渲染窗口。很多接手这个项目的同事把调试代码原封不动丢到生产环境,就在这一步翻车。
解决:用cv2.imwrite()代替cv2.imshow(),把结果写到磁盘再查看。如果确实需要实时预览,用cv2.namedWindow('result', cv2.WINDOW_NORMAL)配合cv2.resizeWindow(),但前提是系统得有图形环境。
5.5 形态学核尺寸纯靠玄学,过度调参浪费时间
现象:闭运算核从(3, 3)调到(7, 7)再到(11, 11),结果不是断口没接上,就是两根烟丝粘连成一根。
原因:核尺寸应该由图像里烟丝的最小宽度决定。如果烟丝最细处是 5 个像素宽,核超过(7, 7)就会把相邻烟丝之间的空隙也填上。
解决:在调试前先量一下目标物体的像素宽度,用cv2.getStructuringElement()生成略小于目标宽度的核。这是一个物理约束问题,不是调参玄学。我从那以后每次做形态学先量目标尺寸,省下至少两小时的盲目试参。
6. 面积与宽度统计:把分割结果变成能写进质检报告的数据
得到各烟丝的contourArea和arcLength之后,很多初学者会直接把这两个数输出,但这些原始数据对质检报告没用。烟丝面积受到拍摄距离、分辨率、光照角度影响,不同批次的图像之间不能直接比较。真正有用的是相对分布和归一化指标。
第一类统计是面积直方图。用numpy.histogram()把area_list分成 20 个区间,看烟丝面积分布是单峰还是多峰。单峰说明这批烟丝粗细均匀,双峰说明存在两个粗细等级,在生产上这意味着切丝机可能需要调整。这个分析可以用matplotlib.pyplot.hist()画出来,不画图的话直接打印频数分布也可以。
第二类统计是单根烟丝的宽度估算。常见做法是用面积除以轮廓骨架长度,骨架可以用cv2.ximgproc.thinning()提取,这个函数在 OpenCV 贡献库里,需要pip install opencv-contrib-python才能用。没有贡献库的环境下,退而求其次用面积除以周长再乘以 2,得到的宽度估计误差在 15% 以内,足够做趋势判断。
第三类是离散度指标。计算所有烟丝面积的标准差和变异系数 CV,即标准差除以均值,这是衡量批次均匀性的核心指标。如果 CV 超过 0.3,说明烟丝尺寸差异大,需要回看预处理阶段是否漏检了碎末。
最后把结果整合成 CSV 文件输出,方便和 MES 系统对接。
import csv import numpy as np # 统计特征计算 areas = np.array(area_list, dtype=np.float64) perimeters = np.array(perimeter_list, dtype=np.float64) mean_area = np.mean(areas) std_area = np.std(areas) cv_value = std_area / mean_area if mean_area > 0 else 0 width_estimate = 2 * areas / perimeters # 写入 CSV,保留原始轮廓信息用于后续追溯 with open('tobacco_stats.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['index', 'area', 'perimeter', 'estimated_width']) for i in range(len(areas)): writer.writerow([i, round(areas[i], 2), round(perimeters[i], 2), round(width_estimate[i], 2)]) print(f"烟丝数量: {len(areas)}, 平均面积: {mean_area:.1f}, CV: {cv_value:.3f}") print("统计结果已保存到 tobacco_stats.csv")在代码运行完后,我建议把不同批次的 CV 值画在一张折线图上,这个趋势比单批次数值更能说明设备稳定性。我在类似项目里的习惯是每次跑完数据,把图片路径、处理参数、统计结果三样绑在一起存档,参数改了哪些在文件名里标注好,如果后续质检反馈有误检,回溯起来十分钟就能定位到是哪一步处理出了问题。此前的项目里我吃过不少没记录参数就改代码的亏,从那以后每次跑烟丝检测都会强制走一遍这套记录流程。希望帮到你。
本文还有配套的精品资源,点击获取