☰
抽烟检测数据集VOC+YOLO双格式落地:从解压到训练全攻略
2026/10/1 4:01:24 网站建设 项目流程

简介:面向吸烟行为检测的目标检测数据集,涵盖22559个图像样本,提供与图片一一对应的Pascal VOC格式xml和YOLO格式txt标注,共cig-pack、smoke两个类别,总框数28472个(烟盒2907个,吸烟行为25565个)。数据由labelImg绘制矩形框完成,标注规范、类别界定清晰,可用于安全监控、公共场所吸烟识别等场景下检测模型的训练与效果评估,也适合有一定目标检测基础的开发者直接接入训练流程。资源包共2000个文件,以xml标注文件为主,另含1个txt说明文档,压缩包整体约802MB。目前已有420人学习下载,按VOC或YOLO约定组织目录即可使用,省去格式转换环节,便于快速开展模型迭代。

1. 抽烟检测为什么要一份“能直接喂给模型”的数据集

不少做安防、加油站和化工厂项目的人,拿到手的原始数据往往是几百段监控视频,要自己抽帧、清洗、标注,折腾两周还没开始训练。而这份标题里的“抽烟检测数据集VOC+YOLO格式22559张2类别.7z”,本质上把前面最痛苦的环节替你完成了:两万两千多张已经标注好的图片,VOC和YOLO两种格式各一份,解压之后直接按YOLO训练的标准流程跑就能出模型。它的价值不在“数据集”三个字,而在“VOC+YOLO格式”这个双格式设计——你不需要再做任何格式转换就能同时兼容老派的VOC训练流程和当前主流的YOLOv5/v8训练流程。适合的人群很明确:正在做人员违规行为识别、明火作业监管,或者想快速验证一个抽烟检测Demo的算法工程师和学生。这篇文章就沿着这份数据集的完整落地路径走一遍:格式是什么、怎么校验、怎么转换、怎么训练、坑在哪。

2. 读懂VOC和YOLO两种标注格式:目录结构与标签语义

2.1 VOC格式的组织方式:XML里存的是什么

VOC格式源自Pascal VOC竞赛,它的目录结构约定俗成,你拿到压缩包解压后应该能看到类似这样的布局:

dataset_root/ ├── Annotations/ # 存放XML标注文件 ├── JPEGImages/ # 存放原始图片 └── ImageSets/ └── Main/ # 存放train.txt / val.txt / test.txt

每个XML文件对应一张同名JPG图片,里面记录的是这张图里每个目标的类别和位置。一个典型的XML标注文件长这样:

<annotation> <folder>JPEGImages</folder> <filename>smoking_00001.jpg</filename> <source> <database>smoking_dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>smoking</name> <bndbox> <xmin>512</xmin> <ymin>301</ymin> <xmax>743</xmax> <ymax>586</ymax> </bndbox> </object> </annotation>

关键就两块:<name>是类别名,<bndbox>是目标的左上角和右下角坐标,单位是像素。这里有一个容易忽略的点:<size>里的宽高必须和JPG图片的真实宽高一致,否则坐标换算到YOLO格式时会整体偏移。我处理过多份公开数据集,图片被二次压缩过导致尺寸和XML对不上的情况并不少见,所以拿到数据的第一步不是急着转换,而是做一次尺寸校验。

2.2 YOLO格式与归一化坐标:为什么它更适合训练

YOLO格式每个目标对应txt文件里的一行,格式是class x_center y_center width height,其中坐标值全部做了归一化,范围在0到1之间。还是上面那个例子,转换成YOLO格式后是:

0 0.326822 0.410648 0.120313 0.263889

计算方式是:x_center = (xmin + xmax) / 2 / width,y_center = (ymin + ymax) / 2 / height,框的宽高也分别除以图片宽高。归一化的好处很明显:模型训练时不管输入图片被缩放到什么尺寸,标注的相对位置都不会变,这也是YOLO系列训练时能随意设置imgsz而不用改标注的原因。

数据集的目录结构和每种格式的标注文件数量是基本信息,拿到压缩包后先确认这些再动手,能省掉后面很多调试时间。通常Annotations下有多少个XML,JPEGImages下就应该有多少张JPG,ImageSets/Main里的train.txt和val.txt则划分好了训练集和验证集的图片文件名列表,每行一个不带扩展名的文件名。

3. 22559张两类别数据集落地:解压、校验与数据划分

3.1 解压7z压缩包:Linux和Windows两条路径

后缀是.7z,那就绕不开7-Zip工具。多数训练环境是Linux服务器,Ubuntu/Debian系统上装p7zip再解压是常规操作:

# Ubuntu / Debian 安装 p7zip sudo apt update sudo apt install -y p7zip-full # 解压到当前目录 7z x smoking_detection_voc_yolo_22559_2cls.7z

7z x命令会保留压缩包内的目录结构解压,而7z e会把所有文件解压到同一个目录里,文件名相同会产生覆盖冲突,所以这里必须用x。解压后的目录里应该同时有VOC风格的文件结构和YOLO风格的文件结构。如果在Windows本地预览,装一个7-Zip桌面版,右键选择“提取到当前目录”即可,但我的习惯是在服务器上直接解压,避免本地和服务器之间来回传文件。

解压之后第一件事是校验文件完整性。压缩包传输过程中可能出现损坏,而XML和JPG文件损坏往往要到训练时报错才能发现,那时再排查就晚了:

# 统计图片数量和标注数量 find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l # 随机抽几张图确认能正常打开 python3 -c " from PIL import Image import glob for p in glob.glob('./JPEGImages/*.jpg')[:10]: img = Image.open(p) img.verify() print('sample images OK') "

如果JPG数量标注数量对不上,大概率是压缩包不完整或者文件被误删,这种情况直接重新解压比手动补齐快得多。Image.verify()只能验证图片文件结构没有被破坏,不会真正解码像素数据,所以它适合做快速体检。

3.2 两类别分别是什么:标签语义决定模型能力边界

标题里写的是“2类别”,这个信息值得仔细想。抽烟检测最常见的标注方案有两种:一种是smoking和no_smoking两个类别,框住整个人或人脸;另一种是smoke和cigarette,分别框烟雾和烟头。不打开Annotations里的XML之前,你其实无法确定是哪一种。

我一般是先扫描一遍所有XML里的<name>标签分布,确认类别名到底是什么:

import xml.etree.ElementTree as ET import glob from collections import Counter counter = Counter() for xml_path in glob.glob('./Annotations/*.xml'): root = ET.parse(xml_path).getroot() for obj in root.iter('object'): counter[obj.find('name').text] += 1 print(counter)

这个输出决定了下一步data.yaml里的类别列表怎么写。如果类别名是smoking和normal,那data.yaml里就对应写;如果XML里实际只有smoke而normal是通过无标注图片隐式表达的,那处理方式完全不同。这个坑我踩过:曾经想当然地把类别名写成smoking,结果训练到一半发现数据加载器报错,说类别索引越界,回去一查XML里写的是smoke。

3.3 按YOLO惯例划分train/val:不要直接拿VOC的划分结果

VOC格式的ImageSets/Main里通常会给出官方划分,但这份数据集不一定在划分上做了精细处理。稳妥做法是自己写脚本重新划分,按8:1:1的比例随机分成训练、验证、测试三份:

import os import random from glob import glob random.seed(42) jpg_paths = sorted(glob('./JPEGImages/*.jpg')) random.shuffle(jpg_paths) n = len(jpg_paths) train = jpg_paths[:int(n * 0.8)] val = jpg_paths[int(n * 0.8):int(n * 0.9)] test = jpg_paths[int(n * 0.9):] def write_names(file_list, out_path): names = [os.path.splitext(os.path.basename(p))[0] for p in file_list] with open(out_path, 'w') as f: f.write('\n'.join(names)) write_names(train, './ImageSets/Main/train.txt') write_names(val, './ImageSets/Main/val.txt') write_names(test, './ImageSets/Main/test.txt') print(f'train: {len(train)}, val: {len(val)}, test: {len(test)}')

这里有个关键点:划分必须在“文件名”层面做,而不是在“图片文件”层面复制移动,这样既保留了数据集的原始目录结构,又能方便后续YOLO训练时引用。划分加入random.seed(42)是为了保证每次实验的训练集一致,这样多次训练的对比才有意义。

4. 把VOC转成YOLO格式:转换脚本与四个边界坑

4.1 转换脚本:从XML到txt的一次性完整方案

拿到VOC格式后转成YOLO格式,是这份数据集使用路径上最核心的一步。转换逻辑不复杂,但边界情况多。下面这个脚本是经过多次修正的版本,覆盖了空XML、坐标越界、类别映射异常等问题:

import os import xml.etree.ElementTree as ET from glob import glob # 类别列表必须和data.yaml里的names一致 CLASSES = ['smoking', 'normal'] def convert_voc_to_yolo(xml_path, out_dir, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() # 跳过没有标注对象的XML objects = root.findall('object') if len(objects) == 0: return False basename = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(out_dir, basename + '.txt') lines = [] for obj in objects: class_name = obj.find('name').text.strip() if class_name not in CLASSES: print(f'[skip] unknown class: {class_name} in {xml_path}') continue class_id = CLASSES.index(class_name) 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) # 坐标安全检查:防止标注框超出图片范围 xmin = max(0, min(xmin, img_width - 1)) ymin = max(0, min(ymin, img_height - 1)) xmax = max(0, min(xmax, img_width - 1)) ymax = max(0, min(ymax, img_height - 1)) # 过滤掉退化框(宽或高为0) if xmax <= xmin or ymax <= ymin: print(f'[skip] degenerate box in {xml_path}') continue x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height lines.append(f'{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') with open(out_path, 'w') as f: f.write('\n'.join(lines) + '\n') return True

脚本的入口部分负责遍历所有XML,并且从图片文件读取真实的宽高而不是XML里写的宽高,这是一个处理标注偏移问题的关键点:

from PIL import Image os.makedirs('./yolo_labels', exist_ok=True) xml_files = glob('./Annotations/*.xml') converted = 0 empty = 0 for xml_path in xml_files: basename = os.path.splitext(os.path.basename(xml_path))[0] img_path = f'./JPEGImages/{basename}.jpg' if not os.path.exists(img_path): print(f'[missing] {img_path}') continue # 从真实图片读取宽高 with Image.open(img_path) as img: w, h = img.size if convert_voc_to_yolo(xml_path, './yolo_labels', w, h): converted += 1 else: empty += 1 print(f'converted: {converted}, empty skipped: {empty}')

4.2 转换脚本的参数说明与设计理由

这个脚本里有三个设计细节值得说明。第一,类别列表CLASSES是一个硬编码数组,它的索引顺序就是YOLO训练时的类别ID,smoking是0、normal是1,这个顺序一旦定下来,data.yaml里的names列表必须严格对应,否则会出现框的位置对但类别全乱的问题。第二,从PIL读取图片真实尺寸而不是用XML里的<size>,是为了规避图片被二次处理过导致标注偏移。第三,对退化框的处理方式是直接跳过并打印日志,而不是让脚本崩溃——实际数据集中总有几条脏标注,跳过是性价比最高的选择。

转换完成后检查一下生成的txt文件数量和内容是否符合预期:

find ./yolo_labels -name "*.txt" | wc -l head -3 ./yolo_labels/smoking_00001.txt

正常的话第一行输出类似0 0.3268 0.4106 0.1203 0.2638这样的格式,class_id是0,四个坐标值都在0到1之间。

4.3 四个边界坑:从数据层面避免训练翻车

第一个坑是坐标越界。标注框有时会略超出图片边缘,比如xmax标注成了1921而图片宽度只有1920,如果不加限制,归一化坐标会超过1.0,轻则训练损失异常,重则训练直接报错。解决方式就是脚本里的min/max夹紧操作,这个处理在推理阶段也有意义,模型输出的预测框同样可能越界。

第二个坑是XML有空洞。部分XML文件里<object>为空或者没有<name>子节点,直接调用.text会拿不到值。脚本里做了objects列表长度为0的判断,但没有更细粒度地处理单个object里缺字段的情况。实际遇到时,我会在解析前加一个检查:class_name = obj.find('name'),如果返回None就跳过这个obj。这个坑在数据清洗阶段不暴露,训练到一半才会因为某张图的loss特别大而注意到。

第三个坑是类别顺序不一致。VOC格式里类别的顺序是字典序或者标注时的先后顺序,YOLO格式里类别ID则是names列表的索引。如果你的训练脚本里data.yaml的names顺序和转换脚本里的CLASSES顺序不一致,模型在验证时mAP会异常低,但训练loss却正常下降。这个坑非常隐蔽,排查时要优先对比这两个列表。

第四个坑是图片文件名与XML文件名不一致。有些数据集在整理时会把文件名统一重命名,但XML内部引用的<filename>还是旧名字。我的脚本是按basename去查找同名图片,如果找不到就跳过并打印[missing],这也是为什么前面强调要校验图片和XML数量——数量一致不等于文件名对得上。

5. 训练YOLO的配置与三个必调参数:数据格式转换只是开始

5.1 准备data.yaml:类别列表与路径设置

转换完标注格式,接下来就是把数据交给YOLO训练框架。无论你用YOLOv5还是YOLOv8,第一步都是写data.yaml。这份文件的路径和类别定义直接决定数据加载器能不能正确工作:

# data.yaml path: /path/to/dataset_root # 数据集的根目录 train: ./ImageSets/Main/train.txt # 训练图片列表 val: ./ImageSets/Main/val.txt # 验证图片列表 nc: 2 names: 0: smoking 1: normal

这里的path字段写的是数据集根目录的绝对路径,而train和val的路径是从path相对延伸的。如果你的YOLO版本对路径解析方式不同,最直接的办法是不用path字段,直接给train和val写绝对路径,能少排查一类路径问题。

5.2 训练参数:三个最容易影响抽烟检测效果的量

第一个必调参数是imgsz。抽烟检测的场景中,画面里通常是全景监控,而烟头或香烟本身很小,如果用默认的640输入尺寸,小目标可能只有几个像素。我的做法是先看数据集中标注框的尺寸分布,如果大量框的宽度小于图片宽度的5%,就需要考虑把imgsz提到960或1280。代价是训练时间变长、显存占用增加,但对小目标检测的提升是实打实的。

第二个必调参数是batch。V100或A100上跑这份两万张的数据集,GTX 2080Ti级别显卡建议batch设为16或32,更小的显卡8也能跑。batch大小直接影响显存占用,而显存不足时YOLO会自动开启梯度累积,这在训练日志里有明确提示。如果看到AutoBatch字样,说明框架在帮你调整batch,此时不用手动干预。

第三个必调参数是lr0。YOLOv5/v8默认学习率是0.01,数据量够大没问题,但如果你做过数据增强导致样本分布变化较大,初始学习率0.01可能导致早期loss剧烈震荡。我一般先用默认值跑20个epoch看曲线,如果loss曲线是锯齿状的,就把lr0降到0.001,同时把lrf(最终学习率因子)保持在0.01不变。训练命令参考:

python train.py \ --data data.yaml \ --weights yolov8s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --patience 15

--patience 15表示如果验证集mAP连续15个epoch没有提升就自动停止,这个设置能帮你节省大量时间,不必坚持跑完所有epoch。

5.3 避坑:训练阶段最常见的三个问题与排查路径

问题一:训练刚起步loss就是NaN。现象是第一个epoch的输出里loss显示nan,或者直接报错说RuntimeError: found inf。原因通常是学习率过大,或者数据里存在损坏的图片,导致前向传播的输出数值爆炸。解决方法是先尝试把lr0降到0.0001看能不能稳定,如果不能,就在数据加载环节加一个图片完整性校验,把打不开的图片从训练列表里剔除。

问题二:验证集上s moking类的recall很低但loss很小。现象是loss曲线收敛得很平滑,但val的recall一直卡在0.5以下。原因往往是类别不平衡——如果normal类样本远多于smoking类,模型学会了把所有样本都预测成normal,loss依然很低,但recall惨不忍睹。解决方式是先统计两个类别的标注数量,如果偏差超过3倍,就要考虑给smoking类加权重,或者做简单的过采样。

问题三:训练正常但推理时小目标完全检测不到。现象是训练日志里mAP50有不错的值,但实际用监控视频测,场景里稍远一点的人嘴上叼着烟就是检测不出来。原因大概率是训练数据里小目标样本本身就少,或者输入分辨率不够。这里有个血泪经验:不要只盯着mAP指标,要按标注框的尺寸分层统计检测率,看看模型对小框、中框、大框的表现差异,很多时候数据增强里的mosaic会随机缩放图片,这个操作反而可能让小目标变得更小。

6. 验证模型与部署细节:mAP之外还要看什么

训练结束后的验证环节,我习惯做三件额外的事:分层统计、错例检查、场景适配测试。分层统计是把验证集的检测结果按标注框大小分成小、中、大三组,分别计算recall,这一步能直接暴露目标尺寸分布带来的能力短板。错例检查是把False Positive的样本单独存下来看,抽烟检测里最常见的误报是手部动作被当成香烟,或者水杯烟雾被当成抽烟烟雾,这类错例往往在mAP上反映不明显,但直接影响实际部署的误报率。

部署阶段要提醒的是输入尺寸的一致性。训练时用了640,导出onnx或TensorRT引擎时就要保持同样尺寸;如果部署端必须用其他尺寸,推理代码里一定要有letterbox预处理,保持原图宽高比不变,否则检测框的位置会整体偏移。还有一个细节:从视频流抽帧做推理时,抽烟行为的判定不能靠单帧结果,连续多帧确认才是工程上可接受的方案,否则一个偶然的误检就会触发错误告警。

这套流程走下来,从解压7z到拿到可用模型,核心工作就两件:把VOC转成YOLO时的边界情况处理干净,以及用分层验证发现数据本身的分布问题。我踩过最深的坑始终是类别顺序不一致带来的静默错误——训练loss正常、mAP也有变化,但实际预测时类别全是错的,所以把CLASSES列表和data.yaml里的names放在一起维护,每次改动同步更新,是我的固定习惯。希望这份沿着数据集落地的实践记录,能帮你少走几趟弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询