☰
用991张吸烟图像训练YOLO检测器:VOC格式转换与避坑实战
2026/10/5 1:06:19 网站建设 项目流程

简介:面向计算机视觉目标检测学习者的吸烟行为检测数据集,包含991张原始图片及一一对应的PASCAL VOC格式XML标注,可用于YOLO、SSD、Faster R-CNN等主流检测模型的训练与评估,也可用于行为识别算法验证,应用场景覆盖智慧安防、公共场所禁烟管理和边缘端实时监测等方向。压缩包共1982个文件,由991张jpg原图与991个xml标注文件组成,整体约44.92MB;jpg提供真实环境中的吸烟姿态图像,xml按PASCAL VOC规范记录每个目标的类别和边界框坐标,文件一一对应且目录结构简洁,可直接按比例划分训练集与验证集,也便于转换为COCO等其他标注格式。平均识别率88.3%表明该数据的标注一致性与质量较为稳定,当前已有262人学习下载。针对目标检测入门者、算法验证人员以及需要构建吸烟识别系统的开发者,这份数据集能省去自行采集图像和人工标注的繁琐环节,用户可直接加载训练;88.3%的基准识别率也可作为复现实验、算法调优或横向对比的参照,显著缩短项目前期的数据准备周期。对于毕业设计、竞赛练手或科研预研均具有实用价值。

1. 吸烟数据集:991张原图够不够训练一个能落地的检测器

吸烟数据集在公开渠道里一直是稀缺资源。工业安全、工位行为识别、公共场所违规预警,都需要检测"人手里有没有烟"这个目标,而大部分通用目标检测数据集里根本没有这个类。这份数据集一共991张原始图片,标注格式是PASICAL VOC XML,项目标注者给出的平均识别率在88.3%——注意,这个数字是一个基线参考,不是买到手就能直接复现的保证,它跟你选的模型、训练参数、验证集划分和置信度阈值都强相关。它真正的价值在于标注干净、类别单一、图片来自真实场景,拿来跑一遍"数据解析→格式转换→模型训练→指标验证"的完整目标检测流程,素材完全够用。适合两类人:第一次想跑通数据标注到模型训练全流程的新手,以及需要快速验证吸烟检测方案可行性的工程师。

2. 数据集的真实构成:991张原图与PASICAL VOC XML的字段拆解

2.1 文件名里的信息:Roboflow导出的典型特征

拿到数据集先别急着训练,先看文件名。以1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg为例,这段命名是有规律的:1277是原始图片编号,_jpg是Roboflow平台对.jpg扩展名的转写(点号被替换成下划线),rf是Roboflow标识,后面一长串是导出时生成的随机hash。对应的XML文件叫1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.xml,也就是说图片和标注文件的主文件名完全一致,只有扩展名不同。

我在拿到这类Roboflow导出数据时,第一件事一定是做配对校验,统计jpg和xml数量是否一致。991张图就意味着应该有991个XML文件,如果数量对不上,后面训练时一定会报"找不到标签"的错。用两条命令就能确认:

ls images/*.jpg | wc -l ls annotations/*.xml | wc -l

这两条命令分别统计图片目录和标注目录里的文件数量。wc -l是统计行数,因为ls每个文件输出一行,所以行数就是文件数。如果两个数字不一致,再用comm命令找出差异文件,这个在第四章避坑部分会详细展开。另外还要顺手看一下图片尺寸是否统一,这份数据里图片大多是640x480左右的分辨率,但也有少量不同尺寸,后续做训练时需要统一resize,这个变量会直接影响标注框归一化坐标的准确性。

2.2 打开一个XML标注:五个关键节点决定检测框是否可用

VOC格式的XML本质上是一个纯文本文件,可以用任何文本编辑器打开。Windows下直接用记事本就能看,但结构化浏览和编辑我一般推荐VSCode装个XML插件,缩进和标签高亮会让字段关系一目了然。随机打开一个标注文件,典型结构如下:

<?xml version="1.0" encoding="utf-8"?> <annotation> <folder>images</folder> <filename>1277_jpg.rf.9c8828412f0497d36739ffc3e4553b84.jpg</filename> <source> <database>smoking_dataset</database> </source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>smoking</name> <bndbox> <xmin>165</xmin> <ymin>98</ymin> <xmax>286</xmax> <ymax>215</ymax> </bndbox> </object> </annotation>

这里的关键节点有五个:filename对应原图文件名,size节点里的width和height是图片原始尺寸,object节点里的name是类别名(这里就是smoking),bndbox子节点里的xmin、ymin、xmax、ymax四个值标定了目标框的左上角和右下角坐标。

注意一个细节:VOC格式允许一张图里有多个<object>节点,也就是说一张图可以同时标注多个人、多个烟头。我写了个小脚本扫描整份数据集,统计每张图的目标框数量分布,这样能快速判断数据集的复杂度:

import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter cnt = Counter() for xml_file in Path("annotations").glob("*.xml"): root = ET.parse(xml_file).getroot() obj_count = len(root.findall("object")) cnt[obj_count] += 1 print(cnt) # 输出示例:Counter({1: 871, 2: 102, 3: 18})

这段代码用xml.etree.ElementTree遍历所有XML文件,findall("object")取出一张图里所有目标节点,Counter统计出现频次。执行后能看到大部分图片是单目标场景,少部分有两到三个目标,这说明数据分布偏向简单场景,模型训练起来不会太难,但也意味着密集场景下的泛化能力需要自己额外补充验证。对新手来说,先确认"991张图、单类别、每图1~3个目标"这个基本面,再开始训练会踏实很多。

2.3 平均识别率88.3%的含义:先别急着把它当预期指标

项目描述里的"平均识别率在88.3%",这个数字很多新手会理解成"我训练完能达到88.3%",实际上它是标注者在某个模型、某个验证集划分下测出来的一个基准值。在目标检测领域,识别率最常见的量化指标是mAP@0.5,即IoU阈值设为0.5时的平均精确度。88.3%放在单类别检测任务里属于中等偏上的水平,不算惊艳,但说明数据质量没问题,标注框和类别基本一致,模型能学到足够的特征。

我一般会把这个数字当作"这份数据能跑通且效果不差"的信号,而不是精确的预期目标。原因在于:mAP的数值和验证集划分方式极度敏感——如果991张图里随机抽20%做验证,那么同一场景、同一人物的多张相似图很可能同时出现在训练集和验证集里,测出来的指标天然虚高。真正可靠的验证方式应该是按场景、按时间段划分,这个后面第四章和第五章都会讲到。所以拿到数据先别纠结88.3%和你自己训练出的指标差多少,先理解VOC格式的解析和转换,把整个流程跑通,再回头评价这个基准值。

3. 把VOC转成YOLO训练格式:转换脚本与数据集划分

3.1 为什么要转格式:VOC是通用标准,训练器需要的是归一化txt

VOC XML是数据交换层面的标准格式,它完整保留了标注的语义信息,适合人工阅读和跨工具迁移。但主流目标检测框架在训练时并不直接读取XML,比如YOLO系列用的是纯文本格式,每一行代表一个目标框:类别ID 归一化中心x 归一化中心y 归一化宽度 归一化高度。归一化的意思是将坐标值除以图片宽高,让所有数值落在0到1之间,这样无论原始图片是640x480还是1920x1080,模型输入的坐标空间都是一致的。

这里顺带解释一个容易混淆的点:VOC里的xmin/ymin/xmax/ymax是绝对像素坐标,而YOLO需要的是中心点坐标和宽高,且需要归一化。转换公式很简单:x_center = (xmin + xmax) / 2 / img_width,width = (xmax - xmin) / img_width,高度方向同理。只要理解了这个换算关系,转换脚本本质上就是一个XML解析加四则运算的问题,没有任何黑匣子。

3.2 转换脚本:用ElementTree解析XML并计算归一化坐标

下面这段脚本我直接拿来处理这份数据集,可以完整地读取annotations目录下的所有XML,并输出对应的YOLO格式txt文件。建议把脚本和数据集目录放在同一级,运行前先确认路径正确。

import os import xml.etree.ElementTree as ET from pathlib import Path # 路径配置 xml_dir = "annotations" # VOC XML 所在目录 txt_dir = "labels" # 输出 YOLO txt 的目录 os.makedirs(txt_dir, exist_ok=True) # 类别映射:类别名 -> 类别ID(单类从0开始) classes = {"smoking": 0} def clip_coord(val, low, high): """将坐标裁剪到合法范围内,处理标注越界""" return max(low, min(high, val)) for xml_path in sorted(Path(xml_dir).glob("*.xml")): root = ET.parse(xml_path).getroot() # 从 size 节点读取图片原始尺寸 size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: continue # 跳过未定义类别 cls_id = classes[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) # 坐标裁剪到图片范围内 xmin = clip_coord(xmin, 0, img_w) xmax = clip_coord(xmax, 0, img_w) ymin = clip_coord(ymin, 0, img_h) ymax = clip_coord(ymax, 0, img_h) # VOC 左上右下 -> YOLO 中心点+宽高,再归一化 x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") # 写出同名 txt out_path = Path(txt_dir) / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") if not lines: print(f"[warning] {xml_path.name} 没有可用目标,已生成空文件")

这段脚本的逻辑按顺序做了四件事:遍历XML、读取图片尺寸、解析每个目标框、按公式转换坐标并写文件。clip_coord函数是我后来加的,因为实际跑转换时发现少量XML的xmax或ymax比图片宽高还大,应该是标注时手滑拉过了头,直接归一化会产生大于1的异常坐标。参数方面需要注意的是classes字典目前只有smoking一个类别,如果以后要扩展多类别,按行追加即可;输出文件名的.stem取自XML的主文件名,和原图主文件名一致,这样YOLO训练时能通过同名规则找到标签。

转换完成后,顺手做一次校验,确认输出的txt都非空、坐标值都在0到1之间:

find labels -name "*.txt" | xargs grep -l "^0 [1-9]\|1\.\d" | head -n 20

这条命令的逻辑是查找所有txt文件中不符合"0 0.x"格式的行,如果输出为空,说明坐标基本合法。反过来说,如果有一堆文件被列出来,就要回到对应的XML检查数据质量问题。

3.3 数据集划分与训练参数:991张图怎么分、怎么跑

转换完成后就是划分数据集。991张图,我按80/20的比例划分,即792张训练、199张验证。这里用的随机划分只是为了快速验证流程能跑通,更严谨的按场景划分方法在第四章会讲。划分脚本如下:

import random from pathlib import Path import shutil random.seed(42) # 固定随机种子,保证可复现 images = list(Path("images").glob("*.jpg")) random.shuffle(images) val_count = int(len(images) * 0.2) splits = { "train": images[val_count:], "val": images[:val_count], } for split_name, imgs in splits.items(): img_dir = Path(f"smoking_dataset/images/{split_name}") lbl_dir = Path(f"smoking_dataset/labels/{split_name}") img_dir.mkdir(parents=True, exist_ok=True) lbl_dir.mkdir(parents=True, exist_ok=True) for img in imgs: shutil.copy(img, img_dir / img.name) label = Path("labels") / (img.stem + ".txt") if label.exists(): shutil.copy(label, lbl_dir / label.name)

这段划分脚本的核心是利用random.shuffle打乱列表顺序,然后按比例切分。random.seed(42)是固定随机种子,这样每次运行得到的划分结果一致,方便复现和对比实验。注意val_count是验证集数量,前val_count张进验证集,其余进训练集。复制时同时复制图片和对应的txt标签,保持目录结构符合YOLO训练器的预期。

划分完成后,写一个data.yaml配置文件供YOLO训练使用:

path: smoking_dataset train: images/train val: images/val names: 0: smoking

这里path指向数据集根目录,train和val用相对路径,names是一个字典,键是类别ID,值是类别名。ID必须和转换脚本里的classes映射保持一致。

训练命令我一般这样起:

yolo detect train data=smoking.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16

参数含义逐一说清:model=yolov8s.pt表示使用YOLOv8s预训练权重作为起点,s版本是速度和精度均衡的选择,991张图用n版本(更小)也可以;epochs=100是训练轮数,这个数据量下50到100轮足够,轮数太多反而容易过拟合;imgsz=640是输入分辨率,如果原图本身就是640x480,这个值和原始尺寸接近,训练效果最好;batch=16是批大小,显存小于6GB的建议降到8。训练过程重点盯两个指标:train/box_loss是否持续下降、metrics/mAP50(B)是否在验证集上稳步上升。如果训练集loss在降但验证集mAP停滞不前,说明开始过拟合了,需要提前停止或加大数据增强。

4. 训练避坑:991张小数据集最常见的五类翻车现场

4.1 图片与XML配对失败:文件名hash与后缀不一致

现象:训练刚启动就报错,提示No labels found in /smoking_dataset/labels/train或者一堆图片找不到对应的txt。原因:Roboflow导出时图片和标注文件的主文件名理论上一一对应,但数据集经过多次拷贝、解压,可能出现文件缺失;另一个常见情况是转换脚本里用xml_path.stem生成txt文件名,但XML主文件名里如果混入了空格或特殊字符,和图片的主文件名就对不上了。解决:写一个两行脚本先做配对检查,找出所有在图片目录有jpg、却在labels目录没有txt的文件:

cd smoking_dataset for img in images/train/*.jpg; do base=$(basename "$img" .jpg) [ ! -f "labels/train/${base}.txt" ] && echo "missing: $img" done

这条命令遍历所有训练图片,用basename取主文件名,再检查对应txt是否存在。输出的missing列表就是问题文件,逐个确认后删除或重新转换即可。从那以后我每次拿到新数据集都会先跑一遍这种配对校验,再往后走流程。

4.2 标注框越界:坐标比图片宽高还大

现象:训练过程中loss出现NaN,或者推理时检测框一部分超出了图片边缘。原因:标注人员在标注时把框拉出了图片边界,或者VOC XML里手误填了超出尺寸的坐标值。如果不加处理直接归一化,xmax - xmin可能为负数或大于1,模型学到的框几何关系就是乱的。解决:转换脚本里加clip_coord函数做边界裁剪,我在3.2节的脚本里已经内置了这段逻辑;另外建议转换后跑一遍全量统计,检查所有txt里是否存在小于0或大于1的坐标值:

awk '{if ($2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1) print $0}' labels/*.txt

awk的$1到$5分别对应类别ID、中心x、中心y、宽度、高度。如果这个命令输出为空,说明坐标全部合法;一旦有输出,找到对应的XML人工核对。这里的教训是:标注工具给的坐标不能盲信,一定要在转换阶段做一次合法性兜底。

4.3 验证集指标虚高:同一场景被随机划分成了两份

现象:训练时mAP达到90%以上,自己拿手机拍了一段视频测试,漏检特别严重。原因:随机划分数据集时,同一摄像头、同一时间段、同一人物的多张连续帧,被分配到了训练集和验证集两边。模型在验证时等于是"看到了"训练时见过的场景,指标自然虚高,但一到新场景就露馅。解决:划分前先看文件名里的原始编号,把同一编号前缀的图片归为一组,按组划分而不是按单张划分。具体做法是把文件名按_jpg前面的数字分组,同一组全部进训练集或验证集。991张图的数据量不大,手工分组几分钟就能完成,但这几分钟能省掉后面调模型的几天时间。

4.4 过拟合:训练集准确率99%,验证集mAP只有70%

现象:训练到30轮以后,训练集的cls_loss降到0.01以下,验证集的mAP却从85%跌到70%左右,并且继续训练也不反弹。原因:991张图对一个检测模型来说属于非常小的数据量,模型把训练集里的背景、光线、人物姿态都背下来了,没有泛化到验证集。解决:少用大规模预训练模型微调,换成更小的模型版本(如yolov8n.pt);训练轮数控制在50轮左右,配上patience=15让它在验证集指标连续15轮不提升时自动停止;数据增强开大一点,尤其是随机翻转、色彩抖动和Mosaic。另外最有效的一招是补充负样本——收集一些完全没有吸烟行为的人脸、手部图片,标注成背景类,让模型学会"没有烟就是没有烟"。

4.5 labelImg打开XML报错:版本兼容问题

现象:想用labelImg复查标注结果,打开部分XML时直接报错,或者加载图片后看不到目标框。原因:Roboflow导出的XML和旧版labelImg生成的XML在标签结构上有差异,labelImg对某些字段顺序和缺失字段比较敏感。解决:优先用VSCode直接查看XML内容,或者用我上面的Python脚本做数据质量统计,不一定非要经过labelImg。如果确实需要人工复查标注,用labelImg打开图片后重新保存一次XML,让工具按自己的格式重写,再传给训练流程。这个问题的本质是标注工具之间的格式方言差异,花时间去找兼容方案性价比很低,直接用代码路径绕过它。

5. 让88.3%可复现:用混淆矩阵与测试集压测验证真实识别率

训练完成后,验证工作比训练本身更重要。先用YOLO自带的验证接口跑一遍测试集指标:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.val(data="smoking.yaml", conf=0.25, iou=0.5) print(f"mAP50: {results.box.map50:.3f}") print(f"precision: {results.box.mp:.3f}") print(f"recall: {results.box.mr:.3f}")

这里conf=0.25是置信度阈值,表示只有当模型对目标预测的概率超过0.25时才把它当作检测结果;iou=0.5是IoU阈值,判定预测框和标注框是否匹配的重合度标准。map50就是平均mAP@0.5,mp是平均精确率,mr是平均召回率。输出结果里results.box.map50就是这块数据在你这套配置下的真实识别率基线。

接着看混淆矩阵。YOLO训练完会在runs/detect/train/目录下自动生成confusion_matrix.png,里面能看到四类数值:被正确检测为正样本的目标、被漏检为正样本但模型没检出的目标、误检为吸烟的目标、真正检出的背景。对单类别数据集来说,混淆矩阵的价值在于帮你数清楚漏检和误检分别是多少,然后针对性地调conf阈值。如果漏检多(召回率低),把conf降到0.1或0.15,代价是误检增多;如果误检多(精确率低),把conf提高到0.4或更高,代价是漏检增多。我一般会按0.05的步长从0.1到0.5做一组对比,把每组mAP、精确率、召回率记下来,然后挑业务场景里更关心的指标作为最终配置。

最后做一个压测:把标注数据集里没有的场景类型单独整理成一个小测试包,比如户外逆光、多人同时吸烟、手遮挡烟头这类难例,喂给模型看表现。991张图覆盖不到的长尾场景,用这种方式补出真实边界。我这次跑完后发现逆光场景的漏检率比正常光照高出一倍,后来靠补充这类训练图和调低conf才压回来。从那以后我每次跑完小数据集都会强制走一遍"测试集压测→混淆矩阵核对→conf调参"这个循环,把88.3%当起点而不是终点,模型的泛化边界才算真正摸清。希望帮到你。

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

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

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

立即咨询