资讯详情

VOC烟火数据集6460张:从XML解析到YOLO训练全指南

📅 2026/10/9 18:55:18 | 华诺云谱 👁 阅读
VOC烟火数据集6460张:从XML解析到YOLO训练全指南
简介一份面向烟火检测任务的数据集资源采用Pascal VOC标注格式包含烟雾smoke与明火fire两个类别适用于目标检测、火灾预警等方向的模型训练与算法验证。资源包约687.88MB内含6460张jpg原图与6460个xml标注文件另附使用说明txt文件组织清晰可直接配合YOLO、Faster R-CNN等主流检测框架使用。标注由labelImg工具完成共包含smoke框7901个、fire框11066个覆盖交通事故、森林火灾、建筑失火、蜡烛、柴火、奥运火炬等多种真实场景有助于提升模型对不同烟火形态的泛化能力。已有2560人学习下载适合计算机视觉初学者与研究人员用于数据扩充、模型微调及精度对比实验。1. 烟火检测数据集里最容易被低估的一份VOC-6460张到底能干什么做火灾预警、工地安全帽旁路监控、化工厂烟雾报警这类项目时最头疼的往往不是模型结构而是训练数据。公开的火焰数据集要么只有明火没有烟雾要么标注格式混乱转成YOLO要自己写半天脚本。这份2022年6月重制版VOC烟火数据集一共6460张图两个类别烟雾、明火标注格式是VOC XML。它解决的核心问题很直接烟雾和明火通常同时出现只检测明火往往等火苗蹿起来才报警而烟雾是更早的信号。适合两类人群一是刚入门目标检测、想拿一份干净数据跑通训练流程的开发者二是做工业视觉或安防监控、需要快速验证烟火检测方案是否可行的一线工程师。重制版意味着原始数据被清洗过一轮标注噪声比初版小后面会专门讲怎么验证清洗效果。2. 数据集结构与XML标注读取用之前把这三层目录看明白VOC格式的坑通常不在训练代码而在你对标注文件的理解。很多开发者拿数据集先跑训练脚本报错了才回头研究目录结构白白浪费半天不如在解压后花20分钟把数据摸到底。2.1 VOC三层目录结构JPEGImages、Annotations、ImageSets/Main这份数据集解压后核心目录是三个它们的分工非常明确。VOC2022/ ├── JPEGImages/ # 6460张原始图片统一JPG格式 ├── Annotations/ # 6460个XML标注文件文件名与图片一一对应 └── ImageSets/ └── Main/ # 存放train.txt、val.txt、test.txt等划分文件JPEGImages里是训练用的原图Annoations里是每个图的标注信息ImageSets/Main里的txt文件则告诉训练脚本哪些图进训练集、哪些进验证集。注意训练脚本读取的是ImageSets/Main里的文件名列表不是直接遍历JPEGImages。很多人忽略这一点结果就是自己写的划分脚本和官方划分对不上训练集里混进了验证集图片。这种结构的好处是数据划分和图像存储解耦你改划分时只需要动txt文件不需要移动图片也不会污染原始数据。坏处是如果你用的是现成的目标检测框架比如YOLO系列它默认不认VOC目录结构需要先做一次格式转换。2.2 XML标注字段逐一拆解bounding box、difficult标记和图片尺寸随便打开Annotations下的一个XML文件你会看到一组固定的字段这些字段决定了后面转换脚本怎么写得严谨。annotation folderVOC2022/folder filename000001.jpg/filename pathVOC2022/JPEGImages/000001.jpg/path source databasesmoke_fire_dataset/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namesmoke/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin356/xmin ymin289/ymin xmax812/xmax ymax671/ymax /bndbox /object /annotation关键是三组数据。 里的width和height是原图尺寸转换归一化坐标时必须用这两个值不能用别的图片去套。 四元组是目标的左上角和右下角坐标注意VOC坐标从0开始算转换时不需要加1。truncated和difficult这两个标记大多数转换脚本会直接忽略但训练前建议检查一下difficult1的样本数量如果占比高说明原图里存在大量遮挡模糊目标模型推理阶段的置信度阈值需要相应调高。2.3 写个XML解析脚本摸清数据家底在动手训练之前我一般会先跑一个统计脚本确认每一类的目标数量、图片数量、标注框大小分布。这一步能提前暴露数据不平衡或者小目标过多的问题免得训练到一半才发现。import xml.etree.ElementTree as ET import glob, os xml_files glob.glob(Annotations/*.xml) class_count {smoke: 0, fire: 0} box_sizes [] for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.findall(object): name obj.find(name).text cls name if name in class_count else unknown class_count[cls] 1 box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) # 计算标注框面积占比判断小目标比例 area_ratio (xmax - xmin) * (ymax - ymin) / (img_w * img_h) box_sizes.append(area_ratio) print(类别统计:, class_count) print(标注框总数:, len(box_sizes)) print(面积占比0.01的框个数:, sum(1 for r in box_sizes if r 0.01))这段脚本做的事很简单遍历所有XML、解析类别名和边界框、按面积占比粗略统计小目标数量。它跑完你可以得到三个结论类别是否均衡可能出现的$ unknown $类别有没有混进来小目标面积占比小于1%数量会不会拖着模型精度。参数上area_ratio小于0.01是个经验阈值适用于1920x1080附近的图片如果你的原图分辨率不同需要按平方关系重新算。3. 把VOC转成YOLO和COCO转换脚本与边界参数处理VOC格式是通用中间格式但实际训练时你大概率需要的是YOLO的txt格式或者COCO的json格式。这段转换是绕不过去的而转换脚本的细节直接决定能不能顺利跑通训练。3.1 VOC转YOLO txt归一化坐标计算和类别映射表YOLO格式要求每个标注框一行文本内容是“类别索引 归一化中心x 归一化中心y 归一化宽 归一化高”全部是浮点数。转换的核心是把VOC的绝对坐标转换成归一化相对坐标。import xml.etree.ElementTree as ET import os # 类别映射必须和训练配置里的类别顺序保持完全一致 class_dict {smoke: 0, fire: 1} def voc_to_yolo(xml_file, output_dir): tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) base_name os.path.splitext(os.path.basename(xml_file))[0] yolo_lines [] for obj in root.findall(object): cls obj.find(name).text if cls not in class_dict: continue # 跳过不认识的类别避免训练报错 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_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 边界裁剪防止坐标越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(w, 1.0) h min(h, 1.0) yolo_lines.append(f{class_dict[cls]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if yolo_lines: with open(os.path.join(output_dir, base_name .txt), w) as f: f.write(\n.join(yolo_lines)) # 批量转换 in_dir Annotations out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(in_dir): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(in_dir, xml_file), out_dir)有两个参数需要你根据实际情况微调。第一是class_dict这份映射表必须和训练时模型配置文件里的names顺序一致否则会出现“学习的是烟雾、推理时当成火”这种错位事故。第二是坐标裁剪的边界逻辑min(max(...))这行代码很多人会省略但部分标注框会有1到2个像素的越界尤其在图片边缘的目标上裁剪后能避免YOLO在数据增强阶段报错。3.2 训练集、验证集、测试集划分按比例抽样本而不是按文件名数据集制作方一般在ImageSets/Main里提供了划分好的txt文件但如果你需要重新划分不要用简单的前70%后30%这种顺序截断必须随机打乱。import os import random random.seed(42) # 固定随机种子保证可复现 images_dir JPEGImages all_images [f.replace(.jpg, ) for f in os.listdir(images_dir) if f.endswith(.jpg)] random.shuffle(all_images) train_ratio 0.8 val_ratio 0.1 # 测试集取剩余10% train_cut int(len(all_images) * train_ratio) val_cut int(len(all_images) * (train_ratio val_ratio)) train_list all_images[:train_cut] val_list all_images[train_cut:val_cut] test_list all_images[val_cut:] def write_split(file_list, path): with open(path, w) as f: f.write(\n.join(file_list)) os.makedirs(ImageSets/Main, exist_okTrue) write_split(train_list, ImageSets/Main/train.txt) write_split(val_list, ImageSets/Main/val.txt) write_split(test_list, ImageSets/Main/test.txt) print(ftrain{len(train_list)}, val{len(val_list)}, test{len(test_list)})随机种子seed42不是玄学固定它能保证每次运行得到完全相同的划分结果这对复现模型精度至关重要。如果你的项目里烟气和明火样本存在场景聚集比如某些图片来自同一段视频建议按场景ID分组划分否则模型在验证集上的表现会虚高。6450多张图按8:1:1划分的结果大约是5160张训练、646张验证、646张测试对单卡训练来说数据量是够的。3.3 yaml配置文件与目录对齐最后一步的易错点转移到YOLO训练时数据集的yaml文件是一个高频翻车点。很多人改了路径却没改类别数或者类别顺序和转换脚本不一致。# dataset.yaml path: ./VOC2022 train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt test: ImageSets/Main/test.txt nc: 2 names: [smoke, fire]这里有三个细节值得较真。train字段指向txt文件列表而不是JPEGImages目录这是YOLO系列和VOC原生的目录读取逻辑的差异。nc必须等于2names的顺序要和YOLO转换脚本里的class_dict值一一对应比如名字顺序是smoke、fire那smoke就对应索引0fire对应索引1两边错一位模型就废了。另外path字段是相对路径时工作目录必须切到该路径的上级否则报错说找不到图片。4. 烟火数据集的五个常见坑现象、原因和解决办法一份标注好的数据集不代表它可以直接喂给模型。烟火场景有它的特殊性有些问题不处理训练过程会莫名其妙地崩掉或者模型上线后误报漏报齐飞。4.1 类不均衡fire目标远少于smoke收敛后fire精度上不去现象训练20个epoch后验证集上smoke的mAP到了0.75fire却只有0.4。排查发现smoke的标注框数量是fire的三倍以上。原因数据集中烟雾往往先出现、面积大、持续帧数多明火出现晚、目标小、帧数少。模型优先学容易学的类别fire类梯度被smoke稀释。解决先把类别统计跑出来用第2.3节的脚本如果比例超过2:1考虑两类策略。第一是采样调整在训练时对fire类做过采样把含fire的图片复制两份加入训练列表。第二是损失函数加权在YOLO的loss配置里增大fire类的cls_loss权重常见做法是把权重调到1.2到1.5倍。我做这类项目时习惯先过采样简单直接改动最小。4.2 小目标漏检远处的烟雾占全图不到1%特征在下采样中丢失现象模型对近距离的烟雾和火焰检测很准对画面中远端小面积的烟雾几乎无反应可视化test图片时目标框完全缺失。原因数据集里的标注框大部分是近景和中景目标落在非常远的射频范围的框占比不高。如果训练时输入尺寸是640x640原图1920x1080下采样后一个50x50像素的小烟雾区域在特征图上只剩几个像素卷积核一池化就直接蒸发了。解决在训练参数上做两个调整。输入分辨率往上拉到960或1280扛得住显存的话可以试1280如果显存不够使用SAHI切片推理。切片推理的做法是把大图切成512x512的小块每块独立推理后再合并结果对小目标mAP的提升幅度通常在0.1以上。数据增强方面mosaic和mixup对这类场景有正收益但别开太大mosaic0.4左右就够。4.3 数据划分串场景同一个监控画面出现在训练集和验证集里现象训练loss还在降验证集mAP也很高但部署到真实监控时准确率明显下跌误报率却升高。原因烟火数据集的原始采集大概率是视频抽帧同一场景连续帧的相似度极高。如果划分txt时是纯随机抽的相邻帧会同时出现在训练集和验证集模型相当于背了答案验证指标严重虚高。解决重新划分时必须按场景分组。先对所有图片按文件名前缀或采集时间戳归组保证同一组图片整体划入训练集或验证集绝不能割裂。规范的划分脚本会给一个scene_id字段没有的话就人工看文件名。交替帧的烟雾轨迹往往是持续演变的连续帧在训练集、相隔50帧的帧在验证集这种操作相当于强制模型学到了记忆而不是泛化特征。4.4 半透明烟团的标注边界争议目标边界模糊导致loss震荡现象训练过程中尤其是前10个epochloss曲线出现周期性尖峰而不是平滑下降。可视化标注时发现部分烟团标注框和可见烟雾区域明显不贴合。原因烟雾本身是半透明的边界不好确认标注者在框选时主观性很强。同一团烟一个人框到可见边缘另一个人框到扩散外围。标注框边界的不确定性直接放大边界回归损失。解决如果训练收敛后精度还是不达标挑出所有difficult1的样本看看。这些样本里如果大部分是烟雾边缘的模糊框可以单独做一个难例集用bootstrapping思路在第二轮训练中加入它们的hard negative挖掘结果。另外把YOLO的box_loss权重从默认值降一点比如从7.5降到5.0能让模型对边界噪声更钝感。4.5 重制版不等于纯净版切图残留和标注错位要刷新检查现象某一次训练前严格清洗了数据但训练数据加载时报Assertion failed: label boxes out of bounds或者验证时发现某个类别的数量是0。原因重制版在清洗过程中重新切了图但部分切图的尺寸信息和XML里的size字段没同步更新。图片是800x600XML里还写着1920x1080归一化后坐标严重偏移有些直接超出边界。解决转换脚本里的边界裁剪不能省就是为了兜住这类错误。同时加一道校验遍历每个XML如果xmax或ymax大于图片实际尺寸把该样本记进异常列表第一轮先剔除而不是自作主张修改等完整跑完一轮训练后确认是普遍问题再统一处理。做数据清洗时永远留一手备份避免删了图之后发现是标注对应错了没有后悔药。5. 训练前加一道标注自检统计分布、可视化抽查、异常拒训数据清洗这件事做一次的价值比调一周参数都大。我在这类数据集上花的最后一道工序是自动自检加人工抽查缺一不可。自检脚本一般做三件事类别数量分布、框面积分布、坐标越界检查。第2.3节的脚本已经覆盖了前两个现在补上越界检测和按异常样本直接拒训的开关。import xml.etree.ElementTree as ET import os from PIL import Image root_dir VOC2022 img_dir os.path.join(root_dir, JPEGImages) ann_dir os.path.join(root_dir, Annotations) bad_list [] area_ratios [] for ann_file in os.listdir(ann_dir): if not ann_file.endswith(.xml): continue base os.path.splitext(ann_file)[0] img_path os.path.join(img_dir, base .jpg) if not os.path.exists(img_path): bad_list.append(f缺少图片: {ann_file}) continue tree ET.parse(os.path.join(ann_dir, ann_file)) root tree.getroot() xml_w int(root.find(size/width).text) xml_h int(root.find(size/height).text) # 读取实际图片尺寸做比对 with Image.open(img_path) as img: real_w, real_h img.size if xml_w ! real_w or xml_h ! real_h: bad_list.append(f尺寸不匹配: {ann_file} XML({xml_w},{xml_h}) 实际({real_w},{real_h})) for obj in root.findall(object): box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) if xmin 0 or ymin 0 or xmax real_w or ymax real_h: bad_list.append(f框越界: {ann_file} ({xmin},{ymin},{xmax},{ymax})) area_ratios.append((xmax-xmin) * (ymax-ymin) / (real_w * real_h)) print(f异常样本数: {len(bad_list)}) for msg in bad_list[:20]: print(msg) # 小目标占比过高时直接拒训 small_ratio sum(1 for r in area_ratios if r 0.01) / max(len(area_ratios), 1) print(f小目标(面积1%)占比: {small_ratio:.2%}) if small_ratio 0.5: raise SystemExit(f小目标占比过高({small_ratio:.2%})建议先做切片增强再训练)这个脚本跑完后你要人工抽查至少40张图。最有效的抽查方式不是单看原图而是把标注框画在图上对比重点关注那些面积占比特别小、或者truncated1的框它们最可能错标或漏标。如果异常样本超过总量的3%建议先修数据再训练不要硬扛。烟火这类目标在监控场景中往往只占画面很小的比例训练时Batch Size开大会加重小目标被压制的程度因此我会把小目标占比超过50%的数据集会强制走一遍切片策略将原图左上右下各1/4区域切成子图参与训练。这是我在多次烟火项目中验证过最稳妥的做法从那以后每个烟火数据集到手上我都强制跑一遍自检再谈训练参数。自检脚本每次都能抓出几个原制作方视角下看不见的脏样本省下的调试时间远大于跑脚本的时间。希望这些经验和脚本能帮你在做烟火检测时少走几趟弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑