ARTICLE DETAIL

资讯详情

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

从PASCAL VOC到YOLOv8:991张吸烟检测数据集的完整训练实践

从PASCAL VOC到YOLOv8:991张吸烟检测数据集的完整训练实践 简介面向计算机视觉与目标检测学习者这是一份包含991张真实吸烟场景图像的标注数据集可用于吸烟检测、行为识别等模型的训练与算法验证。压缩包内共1982个文件包含991张JPG原图和991个对应的Pascal VOC XML标注文件整体大小44.92MB图像与标注一一对应可直接接入YOLO、Faster R-CNN等主流检测框架。目前已有262人学习浏览。所有图像均经过统一标注平均识别率达到88.3%标注文件包含目标类别与边界框位置信息可直接用于模型训练与性能评估既适合作为毕业设计、课程项目的数据基础也适用于安全监控、公共场所行为分析等场景的模型迭代。研究者借助该数据集可大幅节省手工标注时间快速开展算法对比与调参实验。1. 一张991张图片的吸烟数据集凭什么值得你从头到尾跑一遍做安全巡检的人大多撞过同一个需求在工地、机房、加油站或者校园的监控画面里检测到有人吸烟立刻告警。真去落地的时候你会发现模型不是最难的部分最难的是手里没有一张干净可用的标注数据。网上能翻到的吸烟图片零零散散背景杂、分辨率乱、标注格式五花八门光洗数据就能耗掉两三天。而这份991张原始图片、PASCAL VOC XML格式标注、平均识别率88.3%的吸烟数据集恰好解决了“先用起来”这件事——它规模不大但格式标准、类别明确、有可复现的基线数字适合用来跑通从标注解析、格式转换到模型训练评估的完整流程。无论你是第一次接触目标检测还是已经训过几个模型想快速验证思路这份数据都能当作一块稳定的试验田。2. 为什么是PASCAL VOC XML而不是COCO或YOLO标注格式选型与结构拆解2.1 VOC XML的目录约定和annotation内部结构PASCAL VOC的标注格式在目标检测领域属于“老炮儿”2005年由VOC挑战赛定下来到今天大量开源数据集还在沿用。它的根目录通常长成这个样子VOCdevkit/ ├── VOC2007/ │ ├── JPEGImages/ # 原始图片 │ ├── Annotations/ # 每个图片对应的XML标注文件 │ └── ImageSets/ │ └── Main/ # train.txt, val.txt, test.txt991张原始图片对应991个XML文件文件名一一对应比如IMG_0001.jpg对应IMG_0001.xml。打开一个XML核心结构是这样的annotation folderVOC2007/folder filenameIMG_0001.jpg/filename size width1280/width height720/height depth3/depth /size object namesmoking/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin412/xmin ymin238/ymin xmax587/xmax ymax402/ymax /bndbox /object /annotation看到没有VOC的XML是给人读的标签名字直接写在标签里边框坐标清清楚楚。size节点记录图片宽高深后面转其他格式的时候要用它做归一化。truncated表示目标是否被边界截断difficult表示是否难以辨认——这两项在评测时会影响算分方式。对吸烟检测来说常见的标注策略是只标一个类别smoking框住“手拿烟靠近嘴部”的区域也有些数据会把人和烟分开标那份数据的取舍以实际场景为准。2.2 三种标注格式的取舍VOC、COCO、YOLO的差异表用过目标检测的人都知道同样是框三种格式的存法完全不同。这里做一张表方便你对着自己的工具链选。格式存储方式坐标表示典型工具/框架优势劣势PASCAL VOCXML文件xmin, ymin, xmax, ymaxlabelImg, CVAT, 各类老旧数据集直观、可读性强、元信息丰富文件多、占空间、解析略慢COCO单个JSON[x, y, width, height]labelme(转), CVAT, detectron2统一管理、支持分割与关键点结构嵌套深、手写容易错YOLO纯文本txtcx, cy, w, h归一化labelImg(YOLO模式), 各类YOLO训练轻量、训练读取快、无冗余信息可读性差、丢了图片尺寸就废选VOC格式最大的实际好处是它「无依赖」一个XML就是一个独立文件哪怕某个标注坏了也只影响那一张图不会像COCO那样一个JSON里坏一处整个训练集读不了。而且labelImg默认的输出格式就是VOC格式CVAT导出选项里也有它数据换手成本极低。你后面如果想用yolov8训练照样可以写个脚本一键转成YOLO的txt格式这个转换脚本在下一章会直接给你。2.3 991张图和88.3%识别率这两个数字该怎么读先看规模。991张原始图片对于单类别目标检测来说属于“小但够用”的级别。按一张图平均标两个烟头框算大约有2000个左右的标注实例。一个类别2000个实例正常训练yolov8或者SSD跑出来的模型在类似场景下能到85%到92%的mAP50这份数据标称的88.3%正好落在这个区间里数字是可信的。如果你做的是多类别检测或者密集小目标场景这个量级就明显不够至少得到3000到5000张起步。再看88.3%这个数字。严格来说“平均识别率”在目标检测里有两种解释一种指mAP50即IoU阈值取0.5时的平均精度另一种指所有测试图片上检测正确的比例。从工程复现角度我一般把它理解成mAP50水平——在一次常规参数训练后的合理水平。它说明这份数据“能出活”但离生产级部署还有距离后面第6章会讲怎么往上提。要注意的是这个88.3%大概率是在它自带的划分方式下得到的你换了验证集划分比例分数浮动三五个点都正常。3. 拿到手先做三件事解析XML、校验标注、转YOLO格式3.1 用Python解析全部XML统计类别与尺寸分布拿到这份数据集第一步不是急着训练先把家底盘一遍。991个XML逐个解析统计每个类别的数量、每张图的标注框数量分布、图片尺寸是否有不统一的情况。这一步能帮你提前发现数据里埋的雷。下面这个脚本直接跑import os import xml.etree.ElementTree as ET from collections import Counter annotations_dir Annotations class_counter Counter() boxes_per_image [] widths set() errors [] for xml_file in os.listdir(annotations_dir): if not xml_file.endswith(.xml): continue path os.path.join(annotations_dir, xml_file) try: tree ET.parse(path) root tree.getroot() # 统计图片尺寸后面转YOLO格式时要用 size root.find(size) widths.add((size.find(width).text, size.find(height).text)) # 统计每个object的类别 objects root.findall(object) boxes_per_image.append(len(objects)) for obj in objects: class_counter[obj.find(name).text] 1 except Exception as e: errors.append((xml_file, str(e))) print(类别统计:, class_counter) print(每张图框数分布: 最小{}, 最大{}, 平均{:.2f}.format( min(boxes_per_image), max(boxes_per_image), sum(boxes_per_image) / len(boxes_per_image))) print(图片尺寸种类:, widths) print(解析失败文件:, errors[:5] if errors else 无)这段脚本用xml.etree.ElementTree做解析它是Python标准库不需要额外安装依赖。核心逻辑是先取size节点确认所有图片尺寸是否一致再遍历所有object节点统计类别名和框数量。如果有文件解析失败会记录到errors列表里。跑完你该得到类似“类别只有smoking每张图平均2.1个框所有图都是1280x720”这样的结论。如果发现有些图片尺寸是1920x1080后面转换脚本里归一化就按各自图的size算不能写死。3.2 校验标注越界框、空标注、错类名三查标注数据是人工做的就一定有手滑的时候。最常见的问题有三个xmax超过图片宽度、某张图一个框都没标、类别名拼写不一致比如smoking和Smoking混用。这些问题不提前清掉训练时轻则loss异常重则直接报错中断。用下面这段脚本做全面体检import os import xml.etree.ElementTree as ET annotations_dir Annotations image_dir JPEGImages issues [] for xml_file in sorted(os.listdir(annotations_dir)): if not xml_file.endswith(.xml): continue path os.path.join(annotations_dir, xml_file) tree ET.parse(path) root tree.getroot() # 1. 校验对应的图片文件是否存在 filename root.find(filename).text img_path os.path.join(image_dir, filename) if not os.path.exists(img_path): issues.append(f{xml_file}: 图片文件 {filename} 缺失) # 2. 校验边框是否越界 width int(root.find(size/width).text) height int(root.find(size/height).text) objects root.findall(object) if not objects: issues.append(f{xml_file}: 没有任何标注框) for obj in objects: name obj.find(name).text if name not in (smoking, person): # 按实际类别调整 issues.append(f{xml_file}: 未知类别 {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) if xmin 0 or ymin 0 or xmax width or ymax height: issues.append(f{xml_file}: 越界框 ({xmin}, {ymin}) - ({xmax}, {ymax})) if xmax xmin or ymax ymin: issues.append(f{xml_file}: 宽高为负的非法框) if issues: for i in issues: print(i) print(f共发现 {len(issues)} 个问题) else: print(所有标注文件检查通过)这段脚本把三类高频问题一次扫完。边框越界是最隐蔽的坑因为人工标注时鼠标稍微拖过头一点框就出了图边界VOC格式本身不报错但转YOLO时归一化坐标会变成负数或大于1训练时某些版本的ultralytics会直接崩。校验通过后再进训练流程能省掉大量排查时间。如果发现问题文件数量少直接改XML或删掉对应样本就行如果数量多就得回到标注工具里重新导出一遍。3.3 一步到位VOC转YOLO格式脚本为什么必须转YOLO格式再训练因为yolov8、yolov5这些框架的DataLoader对YOLO txt格式支持最成熟读起来快而且在目标检测训练管线里YOLO格式的归一化坐标天然适合anchor匹配计算。转换逻辑不复杂从XML里读出bndbox的绝对坐标除以图片宽高得到相对坐标再由 (xmin, ymin, xmax, ymax) 算出中心点坐标和宽高。脚本如下import os import xml.etree.ElementTree as ET from pathlib import Path def convert_voc_to_yolo(xml_file, output_dir, class_names): tree ET.parse(xml_file) root tree.getroot() # 读取图片尺寸用于归一化 size root.find(size) img_width int(size.find(width).text) img_height int(size.find(height).text) # 输出文件名与XML同名后缀改为.txt xml_name Path(xml_file).stem out_path os.path.join(output_dir, xml_name .txt) lines [] for obj in root.findall(object): class_name obj.find(name).text if class_name not in class_names: continue # 跳过未知类别 class_id class_names.index(class_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) # 绝对坐标转归一化中心点宽高 x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height # 防止浮点误差导致边界越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) width min(max(width, 0.0), 1.0) height min(max(height, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) # 使用示例 class_names [smoking] # 单类别检测按实际类别修改 xml_dir Annotations yolo_label_dir yolo_labels os.makedirs(yolo_label_dir, exist_okTrue) for xml_file in sorted(os.listdir(xml_dir)): if xml_file.endswith(.xml): convert_voc_to_yolo(os.path.join(xml_dir, xml_file), yolo_label_dir, class_names) print(f转换完成输出到 {yolo_label_dir})转换脚本里最容易写错的是class_id class_names.index(class_name)这一行——如果XML里有类别名拼写错误index()会抛异常所以建议先跑3.2节的校验再转换。另外注意输出坐标做了min/max截断防止浮点计算后出现1.000001这种值虽然不会崩但会让某些版本的模型在计算IoU时出现诡异的行为。转换完随机打开一个txt看看内容应该是类似0 0.5123 0.4567 0.1789 0.2334的一行行文本。3.4 训练集/验证集/测试集划分991张图怎么分才靠谱数据量只有991张划分方式直接影响88.3%这个基线能不能复现。常见做法是训练集、验证集、测试集按 8:1:1 的比例随机划分也就是大约792张训练、100张验证、99张测试。划分时有一个重要原则不要用随机种子固定得太随意至少试两三个种子看结果稳不稳定。训练集和验证集各自建一个txt文件列出图片路径内容形如JPEGImages/IMG_0001.jpg JPEGImages/IMG_0002.jpg用下面的代码生成划分文件同时保证随机种子可复现import os import random from pathlib import Path random.seed(42) # 固定种子方便复现 images [f for f in os.listdir(JPEGImages) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) train_ratio 0.8 val_ratio 0.1 # test_ratio 0.1 split_point1 int(len(images) * train_ratio) split_point2 int(len(images) * (train_ratio val_ratio)) train_files images[:split_point1] val_files images[split_point1:split_point2] test_files images[split_point2:] def write_list(file_path, file_list, img_dirJPEGImages): with open(file_path, w) as f: for name in file_list: f.write(f{img_dir}/{name}\n) write_list(train.txt, train_files) write_list(val.txt, val_files) write_list(test.txt, test_files) print(f训练集 {len(train_files)} 张验证集 {len(val_files)} 张测试集 {len(test_files)} 张)这里的关键点是img_dir参数写的是相对路径如果你的图片和标注在同一个根目录下这种写法配合yolov8的配置文件最省事。991张图的量级下只用随机划分就够了不需要按视频帧或时间段做聚类划分——那是在做视频时序数据时才用的策略。划分完后建议把val.txt和test.txt里的文件名打印出来人工扫一眼防止某个抽烟姿态特别集中的场景全被分到测试集里。4. 避坑991张图训出来的模型最容易在四个地方翻车4.1 现象验证集mAP50有0.88视频里一帧帧跑起来却疯狂误报原因出在数据分布和评测方式不匹配。验证集里的图片大概率是清晰、正对镜头、光照良好的静态帧而实际监控视频里大量出现的是人物背对镜头、手放低、烟雾被身体挡住等边角情况。看一眼这份数据集的构成你会发现很多图是网上抓的展示图背景相对干净跟真实监控画质有距离。解决方式是在你的真实场景里多截背景帧加入训练并且把验证集的评估粒度换成帧级别而非图片级别误报率会更真实。4.2 现象个别XML里的filename字段和实际图片名对不上一手数据经过多次搬运文件名被重命名很常见。比如XML文件叫IMG_0100.xml里面的filename写的却是IMG_099.jpg少了一个0。直接按XML里的filename找图会找到错的图甚至找不到图。原因基本是有人用脚本批量改过文件名但忘了同步XML内容。解决的土办法是用XML文件名去对应JPEG文件名不要信任XML内部的filename字段也就是3.2节的校验脚本里加一条“用xml文件名去反查同名jpg是否存在”的逻辑命名对得上的前提下XML内部filename字段只是摆设。4.3 现象VOC转YOLO后训练直接报“unable to load dataset”这一般是txt标签文件和图片文件的目录结构不对。yolov8的YOLO格式要求每个txt文件放在和图片同名的label目录里常见结构是datasets/smoking/images/train/xxx.jpg对应datasets/smoking/labels/train/xxx.txt。有人图省事把所有txt和jpg平铺在同一个目录生成一个data.yaml就丢给ultralytics去训结果框架按默认的labels目录找txt找不到自然报错。解决方法是按上面目录结构重新组织并在data.yaml里写清楚train/val/test三个路径和nc类别数。4.4 现象戴帽子的人、背光区域、远景小人全漏检991张图训练出来的模型摸到了数据分布的天花板。这份数据里的人往往穿着普通服装、面部清晰、烟在画面里占比大换个戴安全帽穿反光背心的工地工人模型就不认识了。根源是类内差异大、样本量不足。这时候给模型调参已经没多大意义正确动作是收集真实场景图补充标注或者用半自动标注的方式扩展数据量把训练集扩到2000到3000张。还有一种取巧做法是降低置信度阈值从默认的0.25降到0.1漏检会改善但误报也会跟着涨属于拆东墙补西墙的临时手段。5. 用这份数据集喂yolov8训练参数与评估指标怎么设5.1 最小可用训练命令与data.yaml配置把991张图转好格式、划分好数据集之后接着就是用yolov8把它训起来。装ultralytics包之后最小的训练命令长这样yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience20对应的data.yaml文件内容如下path: /your/absolute/path/to/smoking_dataset train: train.txt val: val.txt test: test.txt nc: 1 names: [smoking]命令解析modelyolov8n.pt用的是nano版本参数量最小、显存占用低991张小数据集完全够用epochs100是折中值单类别数据通常到不了100轮就已经收敛patience20表示20个epoch验证集指标没提升就提前结束省时间device0指的是用第一张GPU纯CPU环境改成devicecpu但训练速度会慢好几倍。5.2 必调参数imgsz、batch、lr0与weight_decay参数里最影响最终分数的是imgsz。数据集里原始图片如果是1280x720640输入会丢失细节直接拉到1280训练显存占用涨四倍小卡跑不动。我的建议是先试imgsz640跑通流程再用imgsz960做对比看mAP有没有实质提升差距小于1个点就直接用640推理速度还快。batch设成16对大多数16GB显存的卡很安全如果显存不够就先降到8不要动imgsz因为imgsz改起来影响面更大。学习率走ultralytics默认的lr00.01就行991张数据量不需要特殊调整。weight_decay默认0.0005不用动真正会过拟合的反而是epochs——看着val_loss还在降其实test集mAP已经在掉所以要以测试集指标为准。提示991张图属于小数据集训练时不要开--cos_lr做余弦退火学习率曲线拉太长模型还没收敛完训练就结束了经验上固定步长衰减更稳。5.3 复现88.3%的评估姿势看mAP50还是mAP50-95训练完用测试集做一次正式评估命令如下yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml \ splittest \ imgsz640 \ conf0.25 \ iou0.5输出结果里两个指标最值得盯mAP50和mAP50-95。mAP50是IoU阈值取0.5时的平均精度对“检测到烟的大致位置”这件事更宽容数值偏高mAP50-95是0.5到0.95之间每隔0.05算一次再取平均要求框又准又稳数值会低不少。从实际经验看991张数据训出来的模型如果mAP500.88那么mAP50-95大概在0.55到0.62之间。评估时把conf设成0.25是为了对齐训练时的默认推理阈值方便不同模型之间做横向对比。如果你想复现数据集标注的88.3%就按这个流程跑一遍mAP50落在85%到91%区间是正常波动别因为差两个点就去怀疑数据或代码。5.4 推理阶段的置信度阈值选择训练完部署时conf阈值是最后一个需要拍板的参数。阈值调高到0.4误报变少但远景的、被遮挡的烟头会漏掉调低到0.1recall上去了误报也跟着翻倍。我的习惯是先用验证集做一次conf阈值扫描from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) for conf in [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]: results model.val(splitval, confconf, iou0.5, verboseFalse) print(fconf{conf:.2f} precision{results.box.mp:.4f} recall{results.box.mr:.4f})把这组输出打出来挑precision和recall的交叉点附近那个阈值。对吸烟检测这种告警场景我一般宁愿误报也不愿意漏报所以阈值会选在偏recall的一侧比如0.15或0.2然后靠告警系统里的二次确认逻辑把误报滤掉。在这个问题上没有标准答案跟你的业务容忍度强相关。6. 把88.3%往上顶的四个方向如果你是用真实监控画面在做落地88.3%大概率不够用至少要顶到93%以上才能让客户觉得“这玩意儿有点用”。方向不外乎四个。第一是半自动标注扩数据。用好模型在真实场景帧上做预标注再丢进CVAT或x-anylabeling里人工修正。CVAT支持跟踪模式一段视频里标第一帧后面的帧能自动补框修正工作量大减。标完500张新图模型和数据的匹配度会显著提升。第二是数据增强里加“模糊和低光”模拟。mobile设备拍的真实监控画面经常有运动模糊、夜间噪点、逆光过曝而这份数据集的图片相对干净。训练时把hsv_v的增强强度从默认值往上调再配合轻微的高斯模糊增强模型在真实画质下的表现会好不少。第三是专门去挖难例。把测试集里漏检的图翻出来做聚类你会发现漏检往往集中在人物背对镜头、烟藏在身体另一侧、画面里烟占比极小这三类针对性的做法是去网上搜大量烟雾缭绕的图片做背景替换或MixUp增强。第四是换更大的backbone或做多尺度推理。从yolov8n换到yolov8s甚至mmAP大概能涨两三个点代价是显存和推理速度。如果部署机器性能够imgsz960加conf0.15的组合性价比最高。这些年我的习惯一直是把数据集当作活资产对待每次从真实场景里攒到一批新样本就补进去重新训练而不是训完一轮就收工。991张是起步线不是终点线。这条路上踩过的坑你们大概率也会再踩一遍希望这篇笔记能帮你少走两趟弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表