ARTICLE DETAIL

资讯详情

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

抽烟检测数据集VOC+YOLO双格式22559张:转换、校验与训练避坑指南

抽烟检测数据集VOC+YOLO双格式22559张:转换、校验与训练避坑指南 简介面向目标检测开发者和安防场景算法工程师这份数据集提供22559张真实抽烟场景图片以VOC与YOLO双格式输出标注覆盖cig-pack烟盒/烟包与smoke烟雾两类目标合计28472个矩形标注框其中smoke类25565个、cig-pack类2907个可直接用于YOLO、SSD、Faster R-CNN等主流检测模型训练与调优。标注采用labelImg工具完成矩形框边界规范图片与标注一一对应两个类别样本量差异明显也适合作为数据增强、难例挖掘等技巧的练习场景。压缩包约802MB共2000个文件内含1999个xml标注文件与1份说明txtxml保存VOC格式的框坐标与类别信息txt梳理标注规则与目录结构资源仅提供标注数据不附带训练权重便于快速接入训练流程。目前已有420人学习下载适合需要快速获得批量标注数据、减少人工采集标注成本的初学者与算法工程师。1. 抽烟检测数据集VOCYOLO格式22559张双格式标注能省下什么拿到“抽烟检测数据集VOCYOLO格式22559张2类别.7z”这样一个压缩包第一反应不是急着解压而是想清楚它到底省了什么。做安防场景检测的人都知道工地、加油站、校园楼道这类禁烟位置抽烟行为难拍也难标正样本可能只是画面里半截烟头大小一个框要放大核对好几遍才有信心提交。22559张图、2个类别、VOC和YOLO双格式标注同时齐备意味着把整个最烧人力的数据准备阶段压缩成了三件事——校验文件、检查标注、写配置文件。这篇笔记就顺着这三件事往下落给新手一条能直接照做的路径也给熟手一套排查清单。无论你是第一次用YOLO跑自己的数据集还是已经训练过几十个模型先花一小时处理数据远好过硬着头皮开训。2. 拿到22559张双格式数据集先校验完整性和目录结构别急着解压2.1 双格式不是双份冗余而是两条工作流并存很多第一次接触双格式数据集的人会问既然都是同一批标注为什么发布者要同时给VOC和YOLO两套这不是为了凑体积而是两类工具链各自认各自的格式。VOC格式以xml文件存储标注里面记录的是图片文件名、图片宽高、目标类别名以及bndbox里的绝对坐标YOLO格式把每个目标写成txt文件的一行内容是类别索引加四个归一化浮点数。用labelImg手动复核、做二次编辑时VOC的xml更直观而YOLOv5、YOLOv8这类训练管线几乎把所有训练逻辑都建立在txt标签和data.yaml之上。所以双格式的意思是同一份标注信息存成了两种能被不同工具直接消费的形态。如果你拿到的压缩包里两个格式的内容对不上那不是格式差异而是数据有问题。交叉验证的思路后面会讲到这里先建立判断标准。2.2 解压前的完整性校验7z t 与 7z x 的正确用法.7z后缀用的是7-Zip压缩格式Linux下解压它不能用系统的unzip要装p7zip工具包。Debian和Ubuntu系直接一条命令就能装好装完先做压缩包完整性测试再实际解压# 安装 p7zip 工具包 apt install p7zip-full # 先测试压缩包完整性返回 Everything is Ok 再继续 7z t 抽烟检测数据集VOCYOLO格式22559张2类别.7z # 解压到指定目录-o 与路径之间不能有空格 7z x 抽烟检测数据集VOCYOLO格式22559张2类别.7z -o./smoking_dataset这里有几个参数值得说清楚。t是test模式只校验压缩包的CRC不生成文件。网络下载的大压缩包很容易在传输过程中出现文件头损坏直接解压会在一半时报错文件散了一地不说你还不知道哪个文件坏了重传。先跑一遍测试输出“Everything is Ok”再解压是我处理任何数据集压缩包的习惯动作。x是extract with full paths保留压缩包内部的目录结构输出目录用-o指定。注意-o和目录路径之间不能加空格p7zip会把空格当成路径的一部分生成一个带空格的诡异目录名。至于e参数它会把所有文件平铺到同一个目录丢掉层级关系在数据集场景下相当于帮你把标注和图片全混在一起属于要主动避开的用法。另外如果发行方给压缩包加了密码x后面跟-p密码即可公开数据集一般不会这么干。2.3 看文件树与统计图片数确认VOC和YOLO目录是否对齐解压完成只是开始接下来要确认交付物和标题描述一致。我一般会用find和ls把目录整体结构先摸一遍不做这个动作直接拖进训练脚本后面目录写错一个层级都得回头重来# 列出顶层结构避免一上来就淹在文件堆里 ls -la ./smoking_dataset find ./smoking_dataset -maxdepth 2 -type d | head -30 # 统计 jpg 图片总数 find ./smoking_dataset -name *.jpg | wc -l # 统计 xml 与 txt 标签数 find ./smoking_dataset -name *.xml | wc -l find ./smoking_dataset -name *.txt | wc -lfind的-name按文件名匹配-maxdepth限制递归深度防止把标注备份目录里的旧文件也算进来wc -l统计行数这里统计的就是文件个数。图片数应该落在22559张的量级xml和txt标签数也应与图片数接近。如果两个数字差得远先别怀疑标注质量大概率是图片后缀不只是jpg。有的数据集混着png或jpeg把统计命令改成find . -iname *.jpg -o -iname *.png再跑一次-iname不区分大小写能捕到.JPG这种写法。为什么坚持先做这一步因为下一步的格式转换和目录重组都建立在“图片、xml、txt三者一一对应”这个前提上前提不成立后面全是白忙活。3. 把VOC格式转成YOLO格式转换脚本与四个边界坑3.1 换算关系xml绝对坐标到txt归一化坐标VOC的xml里一张目标框记录在bndbox节点下四个值分别是xmin、ymin、xmax、ymax表示左上角和右下角的绝对像素坐标。图片的宽高记录在size节点下。而YOLO的txt每行只有五个数类别索引、框中心点的x、框中心点的y、框宽度、框高度全部除以图片宽高做归一化。换算关系并不复杂x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h公式简单但有个细节我吃过亏不要直接信任xml里的size节点。一批数据经过预处理、resize或裁剪之后标注文件里的size字段不会自动跟着图片更新有些标注工具导出的size甚至宽高写反。我在转换时宁愿对每张图片重新读一遍真实尺寸也不要省这一步。这种谨慎在22559张的规模下只会多花一两分钟却能避免几百个框坐标整体漂移。3.2 写一个最小转换函数xml_parse 与 bbox 换算用Python做转换不需要引入额外依赖xml.etree.ElementTree是标准库图片尺寸用PIL读取。下面是每个从VOC转YOLO的人都会写到的核心函数import xml.etree.ElementTree as ET def voc_xml_to_yolo_txt(xml_path, img_w, img_h, class_map): xml_pathVOC标注文件路径 img_w, img_h这张图片实际的宽和高不要直接信xml里的size class_map类别名到索引的映射例如 {smoking: 0, no_smoking: 1} box_lines [] tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in class_map: continue bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h box_lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return \n.join(box_lines) \n这个函数里name.text.strip().lower()是在防御类名不一致的问题。多人标注的数据集里同一个类别被写成“Smoking”和“smoking”的情况很常见不统一大小写转换时就会把本该是一类的目标漏掉。class_map的键由实际数据集的类别名决定抽烟检测二分类项目的名称可能写在xml里也可能写在压缩包附带的类别说明文件里跑转换前先把所有xml里的name取出去重看一眼再定映射。返回值末尾手动拼了一个换行符保证每个txt都以下一行的空位结束这个细节主要是让后续统计脚本读文件时行为一致。3.3 批量转换脚本与文件名对齐校验有了单文件转换函数批量处理的核心其实变成了“找同名图片”这道工序。VOC约定图片和xml同名但交付时图片可能在images目录、xml可能在Annotations目录甚至命名带不同后缀。所以批量脚本必须把文件名对齐这件事做得足够健壮import os from PIL import Image import glob voc_annotation_dir Annotations # VOC xml 所在目录 yolo_label_dir labels # 转换后 txt 输出目录 image_dir images # 图片所在目录 os.makedirs(yolo_label_dir, exist_okTrue) class_map {smoking: 0, no_smoking: 1} # 按 xml 里实际类别名修改 for xml_path in glob.glob(os.path.join(voc_annotation_dir, *.xml)): stem os.path.splitext(os.path.basename(xml_path))[0] # 找同名图片按常见后缀逐个尝试 img_path None for ext in (.jpg, .jpeg, .png): candidate os.path.join(image_dir, stem ext) if os.path.exists(candidate): img_path candidate break if img_path is None: print(f[skip] 找不到图片: {stem}) continue # 从图片重新读取实际宽高不信 xml 里 size 字段 with Image.open(img_path) as im: img_w, img_h im.size txt voc_xml_to_yolo_txt(xml_path, img_w, img_h, class_map) with open(os.path.join(yolo_label_dir, stem .txt), w) as f: f.write(txt)os.path.splitext只取文件名主干兼容*.xml和*.jpg的不同后缀组合外层for循环里先尝试jpg、再尝试jpeg和png是为了覆盖图片导出的常见三种格式。找不到同名图片时脚本不中断而是打印一行skip日志22559张的规模下前几十张缺图很正常最后集中看日志比让脚本崩在中间高效得多。os.makedirs(..., exist_okTrue)保证输出目录不存在时自动创建第二次运行时也不会因为目录已存在而报错。如果跑完发现skip数量异常比如上百张找不到图那就不是脚本问题而是数据集本身的命名规则不统一需要回去看目录结构。3.4 四个边界坑空尺寸、乱类名、越界框、空txt转换这步是全流程里最容易翻车的地方四个坑按出现频率从高到低排一下。坑一是xml里width和height字段不可信前面已经反复强调过。现象是转换后某些框肉眼可见地偏离目标但只是部分图不是全部。原因是这批图在压缩前被统一缩放过但xml没有同步修改。解决路径就是3.3代码里用PIL直接读真实尺寸代价是转换时间变长但换来的是坐标可信。坑二是类别名不一致。现象是转换日志一切正常但统计txt时发现某个类别框数异常少。原因可能是“smoking”和“Smoking”被当成两个类。解决方法是转换前先跑一个脚本把所有xml里的name提取出去重输出一张类别清单人工核对完再定class_map。这一步看起来能省实际不能省。坑三是越界坐标。现象是生成的txt里出现负数或大于1的浮点数。原因多半是标注时把框拖出了画布边缘或者图片resize后xml没跟着裁。解决方法是转换时代码里对x、y、w、h做范围检查越界的框直接丢弃而不是clamp——clamp完的框虽然坐标合法但形状已经和标注意图不符训练时就是个隐形的错标签。坑四是空txt被误删。有些图片本身没有标注xml里没有object节点转换后txt就是空文件。很多人看到0字节txt会当成异常清理掉这是错的。无标注图片在抽烟检测里通常是负样本删掉它们等于人为拉低模型区分“有烟头”和“没烟头”的能力。空txt在YOLO训练里是有意义的它告诉模型这张图没有目标。4. 训练前数据体检与避坑清单7项检查和5个血泪坑4.1 目录重组与data.yamltrain/val划分与类别映射YOLO训练对数据集目录有固定要求图片和标签各自按train/val划分。最常见的组织方式是把22559张图拆成约20000训练和2500验证拆分时保证每张图片的jpg和txt进同一个桶这个环节用脚本做手工拖文件夹在几千张图规模下必然出错mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val拆分逻辑很简单遍历所有图片按随机数分桶同一个文件名前缀的jpg和txt一起移动。做这一步时把随机种子固定住比如random.seed(42)这样后续复跑能得到完全相同的划分结果。划分完成后data.yaml是训练管线唯一要读的数据描述文件path: ./dataset train: images/train val: images/val nc: 2 names: [smoking, no_smoking]nc是类别数必须与names数组长度一致names的索引顺序必须和转换阶段class_map里的数字一一对应。这里是最隐蔽的一个错位点class_map里把smoking定义为0data.yaml里names却写成了[no_smoking, smoking]训练不会报错但模型输出的0类永远对应错的目标。我自己遇到过一次训练完val mAP看着正常可视化检测框时才暴露出来浪费了整整一个下午。4.2 训练前的7项数据体检统计脚本一次跑完把22559张图直接丢给训练脚本前先花三分钟跑一遍体检脚本它能直接回答“这批数据能不能用来训练”这个核心问题import os from PIL import Image label_dir labels image_dir images total_boxes 0 class_count {} empty_label 0 invalid_box 0 bad_image 0 for root, _, files in os.walk(label_dir): for name in files: if not name.endswith(.txt): continue p os.path.join(root, name) with open(p) as f: lines f.read().strip().splitlines() if not lines: empty_label 1 continue for line in lines: parts line.split() if len(parts) ! 5: invalid_box 1 continue cls int(parts[0]) x, y, w, h map(float, parts[1:]) class_count[cls] class_count.get(cls, 0) 1 total_boxes 1 if x 0 or y 0 or w 0 or h 0 or x w 1 or y h 1: invalid_box 1 for root, _, files in os.walk(image_dir): for name in files: if name.lower().endswith((.jpg, .png, .jpeg)): im_path os.path.join(root, name) try: Image.open(im_path).verify() except Exception: bad_image 1 print(f总框数: {total_boxes}) print(f空标签: {empty_label}) print(f非法框: {invalid_box}) print(f坏图: {bad_image}) print(f类别统计: {class_count})这段脚本的几个统计指标各有用途。class_count告诉你2个类别是否均衡抽烟检测里两类数量差出5倍以上就要考虑类别加权empty_label统计空txt数量空标签占比过高说明负样本太多模型会倾向把所有区域都预测成背景invalid_box专门抓坐标越界的框这个数字只要不是0就说明转换阶段有漏网的越界坐标bad_image用PIL的verify()只读文件结构不加载完整像素秒级完成检测损坏图片比打开看快得多。total_boxes除以图片总数得到平均每张图的框数抽烟检测场景这个值通常远小于1因为大量帧里根本没有烟头。这些指标打印出来后每一项都要和训练预期对得上再进训练对不上就回头处理数据这比训了10个epoch才发现数据有问题划算得多。4.3 训练与诊断的5个坑bn崩溃、混淆矩阵、类别失衡、小目标、续训第一个坑是yolo训练中bn崩溃现象是训练到某个epoch时loss突然变成NaN后面全黑。原因是batch normalization的统计量发散常见诱因是学习率过高、batch size太小或者数据里有极端宽高比的框。解决路径按顺序试先把初始学习率从0.01降到0.001再看是不是开了amp混合精度关掉它batch size至少给到8。这是YOLO模型训练里最经典的一类翻车排查顺序不能反先改数据、再换模型都没用十有八九是训练配置问题。第二个坑是混淆矩阵的总和看着不对矩阵里多出一列背景预测。原因不是模型错乱而是YOLO的混淆矩阵把背景预测也画进去了且行列分别做了不同方向的归一化某一行之和不为1非常正常。解决方法是看每一行的召回率和每一列的精确率不要盯着全局总和非要凑成一个整数这是一个理解性的坑不是代码问题。第三个坑是类别失衡。抽烟检测2类别数据集的通病是正样本框数量远少于负样本体检脚本打印的class_count能直接暴露出来。解决方法是给少数类加loss权重或者对少数类样本做离线增强比如对含烟头的图片做马赛克、旋转、局部放大。不能靠简单复制少数类样本那样模型只是记住了这几张图的像素泛化能力毫无提升。第四个坑是烟头目标太小导致检不出来。22559张图里很多正样本框在整图中占比极小默认训练分辨率640x640会直接把烟头缩到几个像素。解决方法是把imgsz提升到1280或者针对小目标增加浅层特征图参与预测。这个坑在抽烟检测项目里几乎必踩因为烟头本身就是人身上最小的可见特征。第五个坑是中断后resume训练质量退化。现象是手动停了训练再用resume续训曲线掉回去。原因是last.pt里保存的状态很可能与当前数据增强配置不一致或者上次训练中途修改过学习率。解决方法是先确认args.yaml里的配置没被动过再执行resume如果改过参数老老实实从头训。续训不是后悔药不能指望它无缝衔接我自己现在基本只在没改任何配置的情况下才用resume。5. 验证一次训练结果用混淆矩阵与PR曲线做取舍5.1 训练产物里先看哪张图训练跑完后模型目录下会生成result.png、confusion_matrix.png和一堆PR曲线图。result.png要看三条趋势box_loss、cls_loss、dfl_loss都平稳下降说明数据质量没过关的概率低如果某一项在中间台阶式跳水十有八九是学习率调度在起作用不必恐慌。confusion_matrix.png则是决定模型能不能上线的那张图。抽烟检测项目最怕的不是漏检而是误报——把打火机、反光点、烟灰误判成抽烟行为。矩阵中背景列的非零值就是这类误报的来源看到背景列有亮块说明负样本不足或背景多样性不够别急着调模型结构先回去补数据。5.2 用它做迁移小样本现场数据微调22559张双格式数据训出的模型是一个很好的起点但每个现场的光线、机位角度、摄像头清晰度都不一样。实际项目里我一般会做一次迁移微调从训练好的权重出发用自己现场拍的一两百张图继续训20到30轮。微调时把学习率调低保持数据增强开启loss曲线稳定下降就说明预训练权重里的特征被有效复用如果loss不降反升先检查现场图的标注是否和原数据集类别顺序一致这个错位在迁移场景里出现频率极高。做完这些再回头算一笔账直接拿公开数据集训练一个完整体验下来最耗时的不是GPU时间而是清洗标注、转换格式、对齐目录这些脏活。双格式数据把VOC的易复核和YOLO的易训练同时给到省下的是两周的标注周期。我自己踩过最大的坑就是拿到手后跳过校验直接开训结果类别映射错位整个epoch白跑。现在无论什么数据集第一步永远是看文件树第二步永远是统计类别第三步才是训练。先把这三步理顺剩下的交给时间去迭代——希望帮到你。本文还有配套的精品资源点击获取
返回列表