十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python和OpenCV的烟丝检测图像分割算法与部署实战

基于Python和OpenCV的烟丝检测图像分割算法与部署实战 简介面向计算机视觉学习者与烟草质检自动化场景这套基于Python和OpenCV的烟丝检测分割工程聚焦图像中烟丝的自动识别、轮廓提取与面积宽度分析。源码以main.py为核心覆盖图像读取、颜色空间转换、高斯滤波、Canny边缘检测、findContours轮廓查找及形态学开闭运算等关键环节并借助contourArea和arcLength量化烟丝几何属性预处理阶段还包含灰度化与去噪步骤配合形态学操作去除细小颗粒使烟丝目标更完整。压缩包约13.32MB共含12个文件其中4张jpg图像覆盖训练、测试等多场景1个Python脚本实现完整流程若干XML/IML文件为工程配置便于直接运行与二次开发也可根据实际场景调整阈值与滤波参数。目前已有591人学习适合希望掌握OpenCV图像分割实战流程的开发者也可为烟草行业烟丝质量检测相关项目提供基础实现参考。1. 烟丝检测分割从视觉测量到图像分割的必然选择一条制丝线每分钟送出几十公斤烟丝工人从传送带抓一把样品去测长度和填充值结果往往滞后十分钟。更麻烦的是烟丝颜色与铁锈、碎叶相近在运动过程中堆叠、翻转传统光电传感器只能数“有没有”数不了“多长多宽”。基于Python和OpenCV的烟丝检测分割代码就是把这套人眼加卡尺的流程换成图像像素的量化先用预处理把烟丝从复杂背景中剥出来再用分割算法把每一根烟丝的轮廓标定成独立区域最后统计长度分布、碎丝率。对做机器视觉的工程师和烟草设备商而言这条链路最典型的落地方式是做“边缘检测形态学”的轻量方案而不是直接上深度学习原因是产线上需要每帧几十毫秒出结果且烟丝形状相对规则。这篇内容就照这条路径走从环境到部署每段代码都能直接跑。2. 基于PythonOpenCV的环境搭建与烟丝图像预处理图像预处理的地位比分割本身更关键。烟丝是细长的薄片切割后表面有卷曲在漫射光源下会出现高光和阴影如果不先做光照校正再好的分割算法都会被边缘误判拖垮。我一般把预处理拆成三步消除光照不均、增强细丝边缘、去除传送带纹理。这三步用到OpenCV的函数不多但参数匹配要对着实际图像调。2.1 版本选择与基于Python的OpenCV安装Python版本选3.10或3.11OpenCV对应使用4.8以上版本。4.5.2虽然还有人在用但它对部分国产相机的GigE协议支持不完整且自带DNN模块的推理速度偏慢。烟丝检测涉及大量图像缩放和形态学操作4.8之后的UMat和Parallel框架能更好利用多核CPU。安装时用虚拟环境隔离不会污染全局Python。python -m venv tobacco_env source tobacco_env/bin/activate pip install opencv-python4.9.0.80 opencv-contrib-python4.9.0.80 numpy scipy scikit-image这里opencv-contrib-python带extra模块后续做分水岭用到的connectedComponents在contrib包里更完整。numpy和scipy用来做数值统计与滤波。skimage只在调试时用于计算结构相似度产线部署可以不装。2.2 烟丝图像采集与预处理链路设计采集端的相机建议用面阵彩色相机分辨率200万像素左右帧率30fps以上。烟丝颜色是淡黄褐色背景传送带常设计成深绿色或黑色这样RGB三个通道里绿通道的对比度最大。预处理链路不需要把图像转到HSV直接用BGR的Blue和Green通道做差再融合能有效压制传送带的反光。预处理步骤OpenCV函数核心参数目的通道分离split无提取高对比度通道光照补偿createCLAHEclipLimit2.0, tileGridSize(8,8)降低阴影和高光干扰去噪GaussianBlurksize(3,3), sigmaX0平滑细丝表面纹理背景抑制morphologyExMORPH_TOPHAT, kernel15x15去除大面积低灰度背景对比度增强convertScaleAbsalpha1.5, beta-60拉开烟丝与背景灰度差TopHat变换是预处理里的关键动作。烟丝在图像中属于亮目标TopHat用开运算后的图像与原图相减把尺寸小于结构元素的亮细节留在结果里而传送带这类缓慢变化的背景被开运算抹平。结构元素尺寸选15像素既能保留细丝也不会卷进大块烟叶。2.3 预处理代码实现与参数说明下面这段代码可以直接放到preprocess.py中输入一张烟丝俯视图输出做成分割前的高对比单通道图。import cv2 import numpy as np def preprocess_tobacco(image): # 分离BG通道取绿色通道为基 b, g, r cv2.split(image) # G减去B烟丝为黄褐色G高于B背景深绿则抵消 diff cv2.subtract(g, b) # CLAHE限制对比度 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(diff) # 高斯去噪 blurred cv2.GaussianBlur(enhanced, (3, 3), 0) # TopHat提取亮细节 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(blurred, cv2.MORPH_TOPHAT, kernel) # 线性拉伸增强对比度 result cv2.convertScaleAbs(tophat, alpha1.5, beta-60) return result if __name__ __main__: frame cv2.imread(tobacco_sample.jpg) processed preprocess_tobacco(frame) cv2.imwrite(preprocessed.png, processed)代码里cv2.subtract(g, b)在背景深绿时会把背景灰度压到很低而烟丝的G和B接近减完后仍保留中等灰度相当于一阶差分做背景消除比单纯灰度化更干净。convertScaleAbs的alpha和beta需要配合实际灰度直方图调整优先保证烟丝的灰度中值落在128以上背景落在32以下。若现场相机拍摄的烟丝颜色发白则可以把通道差改为r-g保持背景抵消逻辑不变。预处理完成后分割算法面对的图像信噪比已经明显提高此时再进入阈值或分水岭阶段参数稳定性会好很多。3. 烟丝检测核心分割算法与参数调优烟丝分割的难点不在“找出烟丝”而在“把重叠的烟丝分开”。工业场景下烟丝是散堆状态单根周长只有几毫米堆叠后投影区域粘连严重直接做最大类间方差得到的二值图里多根烟丝会连成一大块。这一章先从最简单的阈值入手再逐步引入形态学标记和分水岭三种方法的代码都能跑但适用场景不一样。3.1 阈值分割从固定阈值到自适应阈值传送带速度稳定的前提下固定阈值可以快速出结果但遇到车间灯光漂移就得频繁重调。Otsu大津法自动计算灰度门限对烟丝和背景灰度呈双峰分布的图像有效。如果烟丝含水率不同导致反射差异大双峰不明显Otsu会偏亮侧这时要改用自适应阈值。import cv2 import numpy as np def threshold_segment(processed, methodotsu): if method otsu: _, binary cv2.threshold(processed, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) else: # 自适应阈值窗口大小必须大于烟丝宽度 binary cv2.adaptiveThreshold(processed, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 2) # 去掉极小的噪点 binary cv2.medianBlur(binary, 5) return binarythreshold的第二个参数传0时被Otsu忽略它会根据直方图自动计算阈值。adaptiveThreshold中blockSize设为31这个值要大于烟丝最大宽度像素否则会被当成局部噪声滤除。常数C取2控制在边缘处的灵敏度。这种方案适合烟丝稀疏、没有严重堆叠的取样图一旦出现手指宽的烟块二值图会连片就必须走形态学或分水岭。3.2 形态学操作处理烟丝边缘与毛刺烟丝切割时有碎末分割后边缘会呈现锯齿状直接测量长度会偏大。开运算先腐蚀后膨胀能去除细于结构元素的突起闭运算先膨胀后腐蚀能弥合烟丝内部的小空洞。它们结构元素不能超过烟丝宽度的三分之一通常用3x3或5x5的椭圆。def morphological_refine(binary): kernel_open cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) kernel_close cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open, iterations1) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel_close, iterations2) return closed这里的iterations是关键开运算迭代一次就够过度迭代会把细烟丝整体抹掉。闭运算迭代两次能明显填充因反光造成的内部黑点但会让相邻烟丝的间距缩小如果发现粘连加剧要把迭代次数改回1。形态学处理只是表象修复真正解决粘连要回到分割算法本身。3.3 分水岭算法解决粘连烟丝分割分水岭把图像看作地形图灰度值代表海拔从局部最小值开始注水在水位上涨时用标记区分不重叠的集水盆。直接对二值图做距离变换每个烟丝区域内部会形成山脊但粘连区域只有一个极大值所以需要先找出每个烟丝内部的独立标记。def watershed_segment(binary): # 距离变换 dist cv2.distanceTransform(binary, cv2.DIST_L2, 5) # 归一化突出极大值 cv2.normalize(dist, dist, 0, 255, cv2.NORM_MINMAX) dist dist.astype(np.uint8) # 找局部极大值作为种子 _, markers cv2.threshold(dist, 0.5 * dist.max(), 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(markers, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) markers np.zeros(dist.shape, dtypenp.int32) for i, c in enumerate(contours, start1): cv2.drawContours(markers, [c], -1, i, -1) # 边界标记为0应用分水岭 unknown cv2.subtract(binary, markers.astype(np.uint8)) markers[unknown 0] 0 markers cv2.watershed(cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR), markers) seg_result np.where(markers -1, 0, binary).astype(np.uint8) return seg_resultthreshold提取距离图中前50%高亮区域相当于筛选最粗的烟丝中心点。findContours把这些中心点画成先后顺序的标记分水岭会以这些标记为起点向外扩展。如果烟丝相互重叠且粗细差异大前50%的阈值会把细烟丝直接淘汰需要降低到0.3倍并配合面积过滤避免单个目标生成过多种子。分水岭输出的markers -1是边界线将这些边界置0得到的就是逐根分离的烟丝区域。算法优点缺点适用场景Otsu速度极快参数自适应粘连区域无法分开烟丝稀疏取样形态学自适应抗光照漂移需要反复调窗口光照波动大的产线分水岭能分离重叠烟丝种子选取敏感高密度烟丝堆叠三种算法可以组合使用先用Otsu拿到全局二值图再用形态学清理最后只对面积大于某个阈值的连通域执行分水岭。这样避免了全图分水岭的计算开销也能保证精度。4. 分割评估与后处理从像素到烟丝特征的量化分割代码写完最怕的是肉眼看着好一到量化就失真。烟丝检测最终要给生产系统提供两个数据碎丝率长度小于2mm的占比和整丝率长度大于6mm的占比这依赖分割结果的准确评估与精细后处理。本章先把分割质量量化再做连通域过滤和几何特征提取。4.1 用IoU和Dice评估分割结果人工标注一张烟丝二值图作为Ground Truth与算法输出对比。IoU衡量像素级重合率Dice在类别不平衡时比IoU更敏感。烟丝目标在全图占比通常只有10%到20%IoU的惩罚力度过强Dice能更清晰地反映细丝保留程度。from sklearn.metrics import jaccard_score def dice_coef(y_true, y_pred): smooth 1e-6 intersection (y_true * y_pred).sum() return (2. * intersection smooth) / (y_true.sum() y_pred.sum() smooth) # 加载人工标注和分割结果 gt cv2.imread(gt_mask.png, 0) pred cv2.imread(pred_mask.png, 0) gt_bin (gt 127).astype(np.uint8) pred_bin (pred 127).astype(np.uint8) print(IoU:, jaccard_score(gt_bin.flatten(), pred_bin.flatten())) print(Dice:, dice_coef(gt_bin, pred_bin))jaccard_score默认计算前景类IoU对于严重类别不平衡的图像没有加权重只作参考。Dice用smooth防止分母为0。当Dice低于0.85时优先检查预处理是否放大了烟丝边缘的假目标而不是急着调分割阈值。参数调优时把一组测试图像放入循环自动搜索使平均Dice最高的分水岭距离阈值比手工试参快得多。4.2 连通域分析清除小颗粒噪点分割结果中存在颗粒状噪声它们大多是碎烟末或扫描到的传送带污渍。使用connectedComponentsWithStats把每个独立区域标号按面积阈值过滤。面积单位以像素计若相机分辨率200万像素覆盖视场200x150mm则一个像素对应0.0075mm²0.2mm²的杂质颗粒约27像素。def filter_connected(binary, min_area27, max_area1e6): num, labels, stats, _ cv2.connectedComponentsWithStats(binary, connectivity8) out np.zeros_like(binary) for i in range(1, num): area stats[i, cv2.CC_STAT_AREA] if min_area area max_area: out[labels i] 255 return outconnectivity8使对角相邻的区域合并适合细长烟丝因旋转而呈对角线相接的情形。min_area依据像素当量换算若现场视场大小改变这个值要同步更新。过小的噪点清掉后烟丝间粘连区域如果面积过大没有经过分水岭的话会保留下来所以这里要结合第3章提到的分水岭结果输入。4.3 烟丝长度/宽度的形态学特征提取每根烟丝是细长条用最小外接矩形minAreaRect可以获得长度、宽度和角度。对二值图逐轮廓计算注意烟丝弯曲成弧形时单个矩形会高估长度折线拟合更准确若只是做粗细分级矩形足够。def extract_features(binary): contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) features [] for cnt in contours: rect cv2.minAreaRect(cnt) (x, y), (w, h), angle rect # w恒为短边 length max(w, h) width min(w, h) features.append((length, width, angle, cnt)) # 按长度排序用于碎丝率统计 features.sort(keylambda x: x[0], reverseTrue) return featuresminAreaRect返回的矩形已按长边角度给出旋转角但w、h与长边的关系不定所以用max/min统一。烟丝宽度通常在0.8-1.2mm换算像素后如果检测到的宽度超过2mm说明两张烟丝叠在一起没分开需要回退到分水岭重新分割。碎丝率统计时定义一个毫米与像素的换算系数scale 200 / image_width_mm长度像素乘以scale得到毫米数再按阈值区间累计占比。后处理输出可以用CSV记录每批次烟丝的长度分布为后续填充值测试提供图像依据。5. 面向产线部署相机接入、多线程与性能调优代码在实验室跑通只是第一步上线后还要接工业相机并保证30帧不卡顿。OpenCV的VideoCapture通过GStreamer自动探测工业相机GigE相机则需要厂商SDK把图像转成numpy数组再喂给处理函数。整个流程中耗时最大的不是分水岭而是等待图像传输和形态学迭代。我的做法是开两个线程一个线程只做采集另一个线程做预处理、分割和特征统计中间用队列切换。队列长度设为3超过后丢帧保实时性。import threading import queue import cv2 frame_q queue.Queue(maxsize3) process_q queue.Queue(maxsize3) def capture_loop(cap): while True: ok, frame cap.read() if not ok: continue frame_q.put(frame) def process_loop(): from preprocess import preprocess_tobacco from segments import watershed_segment while True: frame frame_q.get() processed preprocess_tobacco(frame) binary watershed_segment(processed) binary filter_connected(binary, min_area27) features extract_features(binary) process_q.put(features)采集线程里cap.read()默认阻塞如果取流失败会拖慢应该在循环前设置cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)减短缓冲队列确保读到的总是最新帧。进程线程处理完会产生特征列表控制逻辑再根据长度分布去驱动剔除阀。若分水岭耗时超过每帧预算可以限制只在检测到大面积粘连区域时调用平时用快速阈值输出配合一个标记位告诉后续线程当前分割模式是哪一种。性能调优上把相机分辨率降到1280x800烟丝宽度仍能占到10像素以上统计精度足够。同时给watershed_segment的distanceTransform启用cv2.DIST_L2和一个5x5掩膜比默认maskSize3快约15%。最后用PyInstaller加--exclude-modulescipy这类参数打包产线机器不用装Python但要拷贝OpenCV运行时DLL。打包后的exe在配置低的工控机上也能跑前提是形态学内核尺寸固定后不再随意改动。调试阶段可以把分水岭距离阈值做成JSON配置运行时热重载避免修改代码重新打包。本文还有配套的精品资源点击获取
返回列表