ARTICLE DETAIL

资讯详情

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

小样本下YOLOv8传送带异物检测实战:VOC转YOLO格式与训练部署要点

小样本下YOLOv8传送带异物检测实战:VOC转YOLO格式与训练部署要点 简介这套流水线皮带传送带异物检测数据集面向工业视觉质检与目标检测入门及进阶开发者提供Pascal VOC和YOLO双格式标注专门服务流水线场景下 anomaly 单类别异物识别模型的训练与验证。资源共332个文件压缩包约17.76MB包含110张jpg原图、110份VOC格式xml标注以及112个txt文本含YOLO标签及相关配置。全部标注经由labelImg以矩形框完成类别统一为anomaly整个数据集共191个异物框标注规则明确、格式可直接加载免去自行转换格式与重新标注的麻烦。目前已有391人学习下载适合需要快速获取带精确标注目标检测数据集、验证异物检测算法效果的开发者使用也可作为工业质检类项目的实验数据支撑。1. 从“110张”说起单类别异物检测为什么先拿这个数据集打样为了一条旧皮带线我抽帧、标注、清洗数据折腾了一星期最终能跑通流程的反而是这份110张图的VOCYOLO双格式数据包。110张图对目标检测项目来说看起来寒酸但“传送带找异物”是单类别小目标问题数据量需求本来就不高。换来的是省掉标注和格式转换的重复劳动解压后直接进YOLO训练脚本快速验证算法选型与部署方案。VOC格式方便人读和二次编辑YOLO格式让模型直接消费两套标注还能互相校验。它适合两类人被现场工期和标注成本卡住的视觉工程师以及刚入门yolo、想拿真实工业样本练手的学习者。2. VOC与YOLO两套标注格式目录结构、坐标体系与转换脚本拿到这类“VOCYOLO格式”的压缩包第一步不是急着训练而是先摸清两套标注的关系。VOC格式是XML每张图配一个同名XML文件坐标是(xmin, ymin, xmax, ymax)这种绝对像素值看着直观也方便在LabelImg里继续编辑。YOLO格式则是每张图配一个同名TXT每行写 class_id cx cy w h中心点坐标和宽高都做了0到1的归一化训练脚本直接读取。常见的数据包会把JPEG图放在images目录XML或TXT放在对应的标注目录少数包还会带一个classes.txt列出全部类别名。动手前先确认文件名完全一致JPEG的主文件名和标注文件名必须严格同名。YOLO训练是按图像路径推断标签路径的命名差一个字符那张图就被当成无目标的负样本跳过。110张图里丢十几张训练集的多样性立刻缩水后面调什么参数都补不回来。2.1 两种格式的核心差异xml里的是绝对坐标txt里的是归一化中心坐标VOC和YOLO不只是文件格式不同坐标体系也完全不同直接决定了转换逻辑。维度VOC/XMLYOLO/TXT文件结构属性树含object、bndbox等节点纯文本每行一条目标坐标表达xmin、ymin、xmax、ymax像素值cx、cy、w、h0~1归一化类别表达name字符串如foreign_object从0开始的整数class_id多条目标多个object节点并列每行一条行数即目标数这个差异带来一个容易忽略的事实不管你VOC里把异物类名写成“foreign_object”“scrap”还是“waste”转成YOLO之后它都是0。单类别数据集里所有目标共享同一个id类名只活在人类阅读层模型根本不看字符串。后续训练和部署时names顺序一旦变动class_id对不上号模型输出就全乱了我在第5章会单独展开这个翻车点。2.2 从VOC到YOLO的批量转换一个能直接跑的Python脚本有些数据包只给VOC标注需要自己转成YOLO常见做法是写一个遍历XML的脚本。我一般不会去修改原始XML而是转换成独立的TXT目录保留一手数据后悔药得留着。import os import glob import xml.etree.ElementTree as ET def voc_to_yolo(xml_path: str, txt_path: str, class_map: dict) - bool: 把单个VOC xml转成YOLO txt。class_map是{类名: id}的映射 tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) if img_w 0 or img_h 0: return False lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: print(f[warn] 未注册的类名: {name}, 跳过 {xml_path}) continue cls_id class_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化后夹紧到[0,1]防止浮点误差把框推出图像 cx min(1.0, max(0.0, (xmin xmax) / 2.0 / img_w)) cy min(1.0, max(0.0, (ymin ymax) / 2.0 / img_h)) w min(1.0, max(0.0, (xmax - xmin) / img_w)) h min(1.0, max(0.0, (ymax - ymin) / img_h)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) if lines: with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n) return bool(lines) if __name__ __main__: class_map {foreign_object: 0} # 按数据集真实类名调整 xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) for xml_file in glob.glob(os.path.join(xml_dir, *.xml)): base os.path.basename(xml_file).replace(.xml, .txt) if voc_to_yolo(xml_file, os.path.join(txt_dir, base), class_map): print(转换完成:, xml_file)这段脚本的核心在归一化和夹紧。坐标计算一定用float完成并且保留6位小数。1个像素的误差在当前640输入下大约只有千分之一的归一化偏差在精度上不是大问题真正的坑是有人为了省事把归一化结果乘回整数再round或者直接整除图片宽高小目标框就漂移了。夹紧那句min/max很多人会漏掉XML里如果出现xmax比图宽还大的脏数据不做边界处理后续loss直接异常。转换完成后抽几张图把TXT里解析出的坐标画回去看看。不要信任批量转换结果可视化检查这一步不能省。花两分钟用OpenCV把框画出来标错一目了然。2.3 解压后的文件清单images、labels与data.yaml的关系一个能直接训练的数据包解压后最常见的结构是images目录放图片、labels目录放同名TXT、外加一个data.yaml负责把两个目录串起来。我一般会先整理出这样一份清单路径/文件作用与要求images/全部JPEG原图文件名与标注完全同名labels/全部YOLO格式TXT每行一个异物框classes.txt纯文本每行一个类名顺序即class_iddata.yaml告诉YOLO训练器去哪里找图和标签data.yaml是最容易被忽略的一环。单类别场景我通常写path: ./ train: images val: images nc: 1 names: 0: foreign_object注意train和val都指向同一个images目录这在快速试跑时可以但指标会虚高真正评估必须切分。这个写法只适合“确认代码能不能跑通”不适合拿去判断模型好坏。正确做法是生成train.txt和val.txt后让data.yaml的train和val指向这两个清单文件训练器会按清单逐行读图。切分的具体脚本我在下一章给。3. 用YOLOv8在110张样本上把异物检测跑起来切分、训练与参数格式理清之后下一步是把训练流程跑通。对这个工业场景我一般先选YOLOv8而不是Faster R-CNN或SSD。不是因为它最新而是单类别异物检测要的是实时性、部署链路短、参数好调这三点YOLOv8都占了而且社区里yolo数据集的处理工具最全遇到问题能搜到大量现成方案。3.1 为什么先选YOLOv8而不是Faster R-CNN或SSD传送带皮带线普遍以每秒0.5米到1米的速度运行检测系统需要25帧以上的处理能力才能在目标移出视野前完成报警。Faster R-CNN的两阶段检测精度上限高但在工控机上跑不到这样的帧率SSD虽然快但对小目标的召回率在低光照场景下明显弱。YOLOv8用anchor-free结构在小目标回归上比前代更稳训练和部署都走ultralytics一条链路从数据集到onnx再到TensorRT每一步都有现成命令。用预训练权重做迁移学习110张图片足够把异物这个类别的特征“叠加”到已有特征空间里。yolo入门阶段最容易犯的错误是拿随机初始化的权重从零训小样本下loss很难收敛正确姿势是下载yolov8s.pt在它的基础上微调。3.2 按8:2切分train/val固定随机种子只是起步110张图全部丢进去训练是能收敛但你没法知道模型是不是过拟合更没法判断现场效果。切出一个验证集是负责任的做法。90张训练、20张验证是常见比例但切分方式比比例更重要。import os import random image_dir images label_dir labels random.seed(42) images sorted( [f for f in os.listdir(image_dir) if f.lower().endswith((.jpg, .jpeg, .png))] ) random.shuffle(images) train_imgs images[:90] val_imgs images[90:] def write_list(path, imgs): with open(path, w, encodingutf-8) as f: for name in imgs: # 写绝对路径避免训练器在不同工作目录下崩溃 f.write(os.path.join(os.path.abspath(image_dir), name) \n) write_list(train.txt, train_imgs) write_list(val.txt, val_imgs) # 检查标签是否成对存在 for name in images: label_file os.path.join(label_dir, os.path.splitext(name)[0] .txt) assert os.path.exists(label_file), f标签缺失: {label_file} print(ftrain: {len(train_imgs)}, val: {len(val_imgs)})这里固定随机种子保证了每次切分结果一致便于复现对比实验。写绝对路径是个实用细节尤其是后续把项目目录移到别的机器上相对路径经常让训练器在测试阶段找不到图。标签成对检查必须放进去110张图里如果有一张没有TXTYOLO会把它当成负样本悄悄跳过你很难从训练日志里发现。切分的随机性藏着更大的问题如果110张图是从同一段视频里连续抽帧来的随机切分会让同一皮带区的相邻帧同时落进训练集和验证集模型背住了场景而不是学会找异物。一个更可靠的做法是按拍摄时间或皮带区段切分比如前段视频的帧做训练、后段做验证或者从三段不同光照条件里各取一部分。数据包本身不保证现场分布均衡切分时要自己留意帧与帧之间的相似度。3.3 训练命令与关键超参数epochs、batch、imgsz、patience训练用ultralytics的命令行即可先把data.yaml写好path: ./ train: train.txt val: val.txt nc: 1 names: 0: foreign_object然后执行yolo train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/detect \ nameconveyor几个参数按现场情况改参数推荐值说明epochs100小样本配合早停不需要跑满300轮batch16根据显存调整显存不足降到8或4imgsz640原图分辨率更高时让模型缩放处理patience20验证集连续20轮无提升就停freeze10前期冻结backbone防止小数据破坏预训练特征训练中最值得盯的是yolo损失函数曲线。train/box_loss持续下降而val/box_loss掉头向上就是过拟合信号说明模型把训练帧的背景纹理当成了特征。如果val/box_loss波动剧烈完全不降先怀疑学习率其次怀疑标签坐标转换出了问题优先回到第2章的脚本重新审查TXT。训练结束后用best.pt做一次推理检查yolo predict \ modelruns/detect/conveyor/weights/best.pt \ sourcetest_01.jpg \ conf0.25conf的默认0.25在异物检测里偏低会把皮带纹理的轻微变化当成目标。如果现场误报多调到0.4以上如果漏报多降到0.15以下。这个阈值直接影响现场的报警频次需要和产线一起试跑再定。4. 110张数据不够怎么办增强、迁移学习与小样本训练的三种补法110张图能跑通但距离稳定上线还有差距。异物在传送带上出现的位置、角度、光照都不固定小样本模型的泛化能力要靠数据增强和迁移学习补足。增强有两个方向训练时在线做以及提前离线生成新样本。两者解决的问题不一样最好是都做但克制地做。4.1 在线增强优先开自带的mosaic、HSV与翻转ultralytics默认开了不少在线增强通过命令行参数覆盖最方便。我会在不改变目标语义的前提下把光照扰动和翻转加大yolo train \ modelyolov8s.pt \ datadata.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.5 \ fliplr0.5 \ translate0.1 \ scale0.5hsv_h/hsv_s/hsv_v三项模拟不同光源下的色偏和亮度变化传送带车间最常见的灯管频闪和暖光冷光切换靠这三项就能覆盖一部分。fliplr水平翻转对皮带线有意义异物从左右哪个方向过来并不影响判定。translate平移和scale缩放能模拟异物在画面中不同位置、不同距离的情况。是否需要开mosaic要看具体场景mosaic把四张图拼在一起对小目标检测通常有帮助但传送带背景纹理相对均匀异物本身又小四张图混叠可能让模型更难学“异物”这个概念的边界。我一般在第一轮实验不开确认baseline之后再对比开mosaic的效果。在线增强的好处是无磁盘占用、每轮迭代看到的图都不同缺点是没法人工检查增强后的结果所以离线增强用来补充在线增强覆盖不到的情况。4.2 离线增强亮度扰动与水平翻转的样本生成脚本现场如果有明显的光照跳变或皮带反光我会离线生成一批增强样本确保模型至少在训练样本里见过这类变化。生成新样本的同时必须同步生成新TXT这是离线增强最容易出错的地方坐标随图片变换后要跟着换算。import cv2 import os import glob import numpy as np image_dir images label_dir labels out_img_dir images_aug out_label_dir labels_aug os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_label_dir, exist_okTrue) def augment_image(img, label_lines): h, w img.shape[:2] # 1. 随机亮度扰动模拟不同光照 factor np.random.uniform(0.7, 1.3) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hsv[:, :, 2] np.clip(hsv[:, :, 2] * factor, 0, 255) img cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) # 2. 高斯噪声模拟低照度下的传感器噪声 noise np.random.normal(0, 3, img.shape).astype(np.int16) img np.clip(img.astype(np.int16) noise, 0, 255).astype(np.uint8) # 3. 水平翻转概率0.5注意归一化cx要变为 1 - cx if np.random.rand() 0.5: img img[:, ::-1].copy() flipped_lines [] for line in label_lines: parts line.strip().split() cls_id, cx, cy, w, h parts cx 1.0 - float(cx) flipped_lines.append(f{cls_id} {cx:.6f} {cy} {w} {h}\n) label_lines flipped_lines return img, label_lines for img_path in glob.glob(os.path.join(image_dir, *.jpg)): base os.path.basename(img_path) img cv2.imread(img_path) if img is None: continue label_path os.path.join(label_dir, os.path.splitext(base)[0] .txt) with open(label_path, encodingutf-8) as f: label_lines f.readlines() for i in range(3): # 每张原图生成3张增强图 aug_img, aug_labels augment_image(img, label_lines) aug_name base.replace(.jpg, f_aug{i}.jpg) cv2.imwrite(os.path.join(out_img_dir, aug_name), aug_img) aug_txt aug_name.replace(.jpg, .txt) with open(os.path.join(out_label_dir, aug_txt), w, encodingutf-8) as f: f.writelines(aug_labels) print(离线增强完成)注意翻转后只有cx需要变成1 - cxcy、w、h都不变因为水平翻转不影响垂直方向和物体宽高。这是一个很隐蔽又很基础的坑坐标算错一个增强样本越多模型学得越偏。生成之后把图片和框可视化检查一遍确认增强后框还在目标上再放进训练目录。不要做随意的旋转和裁剪增强。异物在皮带上是平躺的常见角度变化有限大角度旋转会造出现场根本不存在的姿态。裁剪类增强更危险改变了异物相对传送带的尺寸比例检测器会把比例当特征。小样本问题的增强原则是“贴近现场可能的变体”不是把数据量做上去就完事。4.3 迁移学习的关键一招冻结backbone先训头在线增强和离线增强都做了110张图最能见效的兜底手段其实是迁移学习。YOLOv8的官方预训练权重在COCO上学到了丰富的纹理和边缘特征这些底层特征对异物识别同样有效。直接全量微调小样本很容易把预训练的特征空间冲垮最常见翻车现象是训练集loss低、验证集mAP低。冻结backbone训练的做法是yolo train \ modelyolov8s.pt \ datadata.yaml \ epochs50 \ imgsz640 \ batch16 \ freeze10 \ patience10freeze10表示冻结前10层让梯度只更新检测头部分。这样模型先学习“异物这个类别在我的数据里长什么样”不破坏底层已经学好的边缘和纹理特征。跑完50轮后再不带freeze参数解冻整个网络微调20轮左右让backbone适应传送带的真实光照和皮带纹理。两阶段训练的关键是先看第一阶段的val loss是否下降到稳定值再决定要不要解冻。如果第一阶段就波动很大多半是标签里有脏数据不是训练策略的问题优先回头检查标注。小样本训练里数据质量永远是第一位的训练策略只能缓解问题不能填坑。5. 传送带异物检测的常见问题排查反光误检、运动模糊、切分和标注退化的5个坑到这章写的全是血泪经验换来的排查记录。每条都是实际项目中遇到过的现象、原因和解决路径。如果你训练的模型在现场表现不如验证集按这个顺序排查基本能定位到根因。5.1 金属反光被识别成异物漏标注伪样本的代价现象模型在皮带侧面的金属护板上频繁出框把一片高光反光判断成异物。原因训练集只标了异物没有标反光。模型学到的是“和周围背景不一样的亮斑”这个抽象特征金属高光本质上也是一种亮斑于是被归成同类。110张图里如果都是在稳定光照下拍的模型根本没见过反光误检几乎不可避免。解决最直接的办法是把高反光的现场帧抽出来不清洗直接加进训练集不标注任何目标让模型学会把这种亮斑当作背景。另一个做法是调高conf阈值到0.35以上减少低置信度误报但这会牺牲部分小异物召回。真正治本的是在采集阶段用偏振片滤掉金属反光从源头让训练分布和现场分布更一致。5.2 运动模糊让检测框漂移先治帧率再治算法现象皮带速度稍快的时候小异物边缘出现拖影输出框比实际目标大一整圈中心点还偏在后面。原因工业相机曝光时间太长或者帧率不够目标在曝光周期内移动了多个像素画面自然拖影。YOLO训练时见过的都是清晰静止帧遇到拖影框就不知道怎么回归了。解决现场优先把曝光时间压低一般做到500微秒以下配合额外补光保证亮度。算法层面在离线增强里用OpenCV的运动模糊核模拟拖影模型见过模糊样本后框会更稳。如果推理耗时卡在30帧以上就触发丢帧下一帧目标位置已经变远这也是拖影的间接来源最终要回到TensorRT部署把速度提上去。5.3 VOC转YOLO时坐标精度丢失小目标“框不住”的元凶现象训练时loss下降缓慢可视化验证发现很多框偏了尤其是宽度只有二三十像素的小异物框完全压不住目标。原因转换脚本用了整数运算或者归一化只保留到3位小数。小目标的宽度假设是30像素归一化后约0.0473位小数会抹掉一部分精度IoU计算直接跌破0.5模型学到的是错误回归目标。解决转换代码按第2章的写法保留6位小数全程float计算最后clamp夹紧。再加一步自动化校验遍历所有TXT检查cx、cy、w、h是否都在0到1之间超出范围打warning。这个校验脚本虽然简单但能拦住大量转换脏数据。5.4 随机切分训练集和验证集mAP虚高的假象现象验证集mAP50跑出接近1.0模型拿到现场连续漏检上线第一天就被产线叫停。原因110张图如果来自同一段连续视频随机切分会让相邻帧同时进入训练集和验证集。相邻帧之间背景几乎一样只有异物位置微动模型记住了背景就“答对”了验证题。解决切分改按时间或皮带区段进行前70%的帧做训练后30%做验证保证验证帧和训练帧不存在同场景重复。验证集指标只用来对比实验选型真正判断能不能上线要拿现场一段新拍视频做盲测看误报率和漏报率是否达到产线要求。5.5 类名与class_id对不上部署时最隐蔽的翻车点现象训练时一切正常导出模型部署后推理结果全错有时候画出来的框对应的是别的类名。原因数据包里的classes.txt写的是字符串类名正式训练时data.yaml里的names顺序必须和TXT里的class_id一一对应。单类别数据集只有一个类看起来无所谓但如果你换了数据包或者调整过names顺序class_id就悄悄错位了。解决训练前写一个校验脚本统计labels目录里出现过哪些class_id再和data.yaml的names长度比对越界直接报错。部署时先打印一张真实样本的推理结果人工核对而不是直接接PLC报警。这个校验习惯能省掉上线现场最尴尬的半小时排错。6. 验证与落地从mAP评估、ONNX导出到TensorRT实时预算6.1 验证指标不能只看mAP50验证集上别只盯mAP50一个数。对异物检测来说我更看重recall——漏掉一个异物可能意味着异物进入下游设备造成损坏。precision和recall要一起看再结合误报率评估现场可用性。110张图能提供的不多建议把验证结果按帧率、异物大小分桶统计小目标recall尤其关键。6.2 导出ONNX与动态输入训练完成后导出onnx模型是部署的必经一步yolo export \ modelruns/detect/conveyor/weights/best.pt \ formatonnx \ dynamicTrue \ opset12dynamicTrue让模型支持动态batch和动态分辨率方便后续在服务里接多路rtsp流或处理不同分辨率输入。opset版本要和你用的推理框架兼容TensorRT 8.x选opset 12比较稳。导出后先用onnxruntime跑一张现场图确认输出和YOLO原生推理基本一致再进下一步。6.3 TensorRT实时预算部署到工控机时TensorRT的加速收益最明显。640分辨率输入下优化后的模型单帧推理通常能压到个位数毫秒这个数字和显卡型号强相关不能只看宣传值要自己benchmark。以25帧为例一帧的时间预算40毫秒扣掉解码和前后处理推理占用的预算必须留出安全余量。我自己的习惯是先在目标工控机上跑满1000张现场图记录平均延迟、p99延迟和显存占用再决定接几路摄像头。传送带产线一般一路相机就够但如果你要同时看多条皮带线就得先算清楚单路GPU占用再扩容否则就是现场踩坑。最后说一个个人教训任何数据集的validation指标都只代表“能在测试集上复现”不代表现场能稳定运行。拿到这种压缩包最快的落地路径是快速跑通训练、抽帧盲测、接PLC联动测试三步确认没问题再谈优化。部署前一定保留一套干净的现场抽帧作为回归测试集每次改模型都跑一遍这比反复调conf阈值有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表