
简介这份数据集面向从事目标检测算法研究与改进的开发者聚焦战争场景中导弹发射、导弹爆炸产生的烟雾与火焰识别任务。数据主要从视频中抽帧制作共包含923幅1280×720分辨率的影像标注覆盖烟雾smoke与火焰fire两个类别并同时提供VOC格式的xml标注与YOLO格式的txt标签可直接用于YOLO系列目标检测模型的训练与算法改进实验。资源包共约2000个文件以930个txt标签、923个xml标注和147个jpg图像为主压缩包大小约114.95MB其中训练集839幅、验证集84幅划分清晰便于直接开展实验。目前已有123人学习下载适合需要烟雾火焰检测数据、希望验证模型改进效果或进行对比实验的研究者与工程人员参考使用。1. 战场烟雾火焰数据集从标注格式到训练落地的完整拆解做视觉检测的同行大概都有过这种体验想找一个带烟雾、火焰、爆炸效果的标注数据集翻遍公开渠道要么是纯图片没标注要么标注格式单一得让人头疼。最近拆了一份专门针对导弹发射、爆炸场景的烟雾火焰数据集里面同时提供了 xml 和 txt 两套标注文件这个设计本身就挺有意思——xml 对应的是 PASCAL VOC 那套体系txt 则是 YOLO 系列训练直接能吃的格式。换句话说不管你用的是两阶段检测器还是单阶段检测器这份数据都能直接接上省掉了自己写转换脚本的功夫。它适合做军事仿真、安防监控、烟火预警这类方向的算法验证也适合刚入门目标检测、想拿真实场景数据练手的人。接下来我把从数据检查到训练跑通的完整链路拆一遍包括格式差异、参数设置和几个我踩过的坑。2. 标注格式拆解xml 和 txt 到底怎么选2.1 两套标注体系的本质差异PASCAL VOC 的 xml 文件本质上是一个结构化描述每个目标用object标签包起来里面记录类别名和边界框的四个坐标值——注意这四个值是绝对像素坐标左上角和右下角各两个数。YOLO 的 txt 则完全不同每行代表一个目标格式是类别索引 中心x 中心y 宽度 高度而且后四个值全部归一化到 0 到 1 之间。这个差异直接决定了你在读数据时要不要做坐标变换。我一般会先确认一件事txt 里的类别索引和 xml 里的类别名能不能对上。有些数据集是分开标注的xml 里写的是smoke、fire、explosiontxt 里却是 0、1、2中间没有映射表这时候就得自己从 xml 里提取类别列表按字母序或者出现顺序生成索引。这份数据我检查过两套标注的类别体系是一致的省了不少事。2.2 用脚本做一次全量校验拿到数据先别急着往模型里灌跑一遍校验脚本能提前发现大半问题。下面这段代码同时读 xml 和 txt检查文件是否配对、坐标是否越界、类别是否在允许范围内。import os import xml.etree.ElementTree as ET # 数据集根目录按实际路径改 DATA_ROOT ./dataset IMG_DIR os.path.join(DATA_ROOT, images) XML_DIR os.path.join(DATA_ROOT, annotations_xml) TXT_DIR os.path.join(DATA_ROOT, labels_txt) # 允许的类别从 xml 里统计出来后写死防止拼写错误 VALID_CLASSES {smoke, fire, explosion} def check_xml(xml_path): 检查单个 xml 文件的标注合法性 tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) issues [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in VALID_CLASSES: issues.append(f未知类别: {name}) bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) # 坐标越界检查这是最常见的问题 if xmin 0 or ymin 0 or xmax w or ymax h: issues.append(f坐标越界: {xmin},{ymin},{xmax},{ymax} 图像尺寸 {w}x{h}) if xmax xmin or ymax ymin: issues.append(f无效框: 宽高为负) return issues def check_txt(txt_path): 检查 YOLO 格式标注确认归一化值在 0-1 之间 issues [] with open(txt_path, r) as f: for line_no, line in enumerate(f, 1): parts line.strip().split() if len(parts) ! 5: issues.append(f第{line_no}行字段数不对: {len(parts)}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) if not all(0 v 1 for v in [cx, cy, bw, bh]): issues.append(f第{line_no}行归一化值越界) return issues # 主流程遍历所有图片检查配对和标注 img_files {os.path.splitext(f)[0] for f in os.listdir(IMG_DIR)} xml_files {os.path.splitext(f)[0] for f in os.listdir(XML_DIR)} txt_files {os.path.splitext(f)[0] for f in os.listdir(TXT_DIR)} print(f图片数: {len(img_files)}, xml数: {len(xml_files)}, txt数: {len(txt_files)}) print(f有图无xml: {img_files - xml_files}) print(f有图无txt: {img_files - txt_files}) for stem in list(img_files)[:50]: # 先抽检50个全量跑也行 xml_p os.path.join(XML_DIR, stem .xml) txt_p os.path.join(TXT_DIR, stem .txt) if os.path.exists(xml_p): errs check_xml(xml_p) if errs: print(f[XML] {stem}: {errs}) if os.path.exists(txt_p): errs check_txt(txt_p) if errs: print(f[TXT] {stem}: {errs})这段脚本的逻辑很直白先做文件级配对检查确认每张图都有对应的标注文件再对单个文件做内容级校验xml 重点看坐标越界和类别拼写txt 重点看字段数和归一化范围。参数方面VALID_CLASSES一定要根据实际数据改我见过有人直接拿 COCO 的类别列表去套结果所有目标都被判为未知类别。抽检数量可以调数据量大的话建议全量跑一遍几分钟的事比训练到一半发现标注有问题强得多。2.3 格式转换的两种路径如果你拿到的原始标注只有 xml想转成 YOLO 格式核心就是坐标变换。下面这个转换函数我用了很久处理过几万张图没出过岔子。import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, class_list): 将 VOC xml 转为 YOLO txt 格式 class_list: 类别名列表索引即类别 id tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_list: continue cls_id class_list.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 转成中心点宽高再归一化 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 保留6位小数够用且省空间 lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) return lines # 用法示例 classes [smoke, fire, explosion] result voc_to_yolo(./dataset/annotations_xml/0001.xml, classes) for line in result: print(line)转换的关键点有三个一是类别索引必须和训练时的names配置完全一致顺序错了模型学出来的东西就是乱的二是归一化除的是图像宽高不是标注框的宽高这个新手特别容易搞反三是浮点数精度保留 6 位小数足够表达再多了文件体积白白增大。反过来 txt 转 xml 也类似乘回宽高再取整就行注意取整时用int()还是round()会影响一两个像素对训练影响不大但做可视化对比时能看出来。3. 从数据到训练YOLO 和 VOC 两条路怎么走3.1 目录结构怎么摆不管用哪套框架数据目录的组织方式都建议按下面这个结构来后面换模型或者换框架时迁移成本最低。dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── annotations_xml/ # 原始 xml 保留一份方便回溯划分比例我一般按 8:2 走如果数据量特别大超过两万张7:3 也行。关键是划分时要随机打散别按文件名顺序切——有些数据集是按场景连续采集的顺序切会导致训练集和验证集分布差异巨大验证指标虚高。下面这段划分脚本顺手把对应的标注文件也一起挪过去。import os import random import shutil random.seed(42) # 固定随机种子保证可复现 IMG_DIR ./dataset/images LBL_DIR ./dataset/labels_txt OUT_ROOT ./dataset_split stems [os.path.splitext(f)[0] for f in os.listdir(IMG_DIR)] random.shuffle(stems) split_idx int(len(stems) * 0.8) train_stems stems[:split_idx] val_stems stems[split_idx:] for subset, stem_list in [(train, train_stems), (val, val_stems)]: img_out os.path.join(OUT_ROOT, images, subset) lbl_out os.path.join(OUT_ROOT, labels, subset) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for stem in stem_list: # 图片可能是 jpg 也可能是 png这里按 jpg 处理实际按需改 src_img os.path.join(IMG_DIR, stem .jpg) src_lbl os.path.join(LBL_DIR, stem .txt) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(img_out, stem .jpg)) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(lbl_out, stem .txt)) print(f训练集: {len(train_stems)}, 验证集: {len(val_stems)})random.seed(42)这行别省不然每次跑出来的划分结果都不一样实验没法对比。图片扩展名要根据实际情况改有些数据集混着 jpg 和 png那就得先统一格式再划分否则复制时会漏文件。3.2 YOLO 训练配置的关键参数YOLO 系列的配置文件里和这份数据最相关的几个参数是nc、names、train、val。nc是类别数这份数据是 3 类就写 3names的顺序必须和 txt 里的类别索引严格对应。下面是一个最小可用的 data.yaml。# data.yaml path: ./dataset_split train: images/train val: images/val nc: 3 names: 0: smoke 1: fire 2: explosion训练命令以 YOLOv8 为例yolo detect train datadata.yaml modelyolov8n.pt epochs100 imgsz640 batch16imgsz640是默认值但如果你的图片分辨率普遍偏小比如 416 或更小可以调低到 416 或 512能明显提速。batch16要看显存8G 显存跑 640 尺寸大概能到 16再大就爆了。epochs100是起步值烟雾火焰这类目标特征比较明显通常 80 到 120 轮就能收敛看验证集 loss 不再下降就可以停。3.3 VOC 路线的训练要点如果你走的是 Faster R-CNN 这类两阶段检测器数据组织方式要换成 VOC 标准结构核心是生成ImageSets/Main下的 train.txt 和 val.txt里面只写文件名不带扩展名框架会自动去JPEGImages和Annotations里找对应文件。类别映射通过label_map.pbtxt配置格式如下item { id: 1 name: smoke } item { id: 2 name: fire } item { id: 3 name: explosion }注意 VOC 的类别 id 从 1 开始0 是背景这和 YOLO 从 0 开始不一样转换时别搞混。训练参数上两阶段检测器一般学习率设 0.001 到 0.004batch size 受显存限制通常比 YOLO 小8G 显存跑 640 尺寸大概只能到 4 或 8。训练轮数建议 50 到 80因为两阶段收敛通常比单阶段快一些。4. 避坑与排查标注和训练里最容易翻车的几件事4.1 坐标越界导致训练直接报错现象训练启动几秒后报AssertionError或IndexError提示某个坐标超出图像范围。原因xml 里的标注框坐标超过了图像实际宽高常见于标注工具导出时没做边界裁剪或者图像被缩放后标注没同步更新。解决跑一遍第 2 章里的校验脚本把所有越界框找出来。处理方式有两种——要么把坐标裁剪到图像范围内xmin max(0, xmin)xmax min(w, xmax)要么直接丢弃这个标注框。我一般选裁剪因为烟雾火焰这类目标本身边界就模糊裁掉一两个像素影响不大。4.2 类别索引错位导致模型学偏现象训练 loss 正常下降但推理时所有烟雾都被识别成火焰或者置信度普遍偏低。原因txt 里的类别索引和names配置的顺序不一致。比如 txt 里 0 是 fire但 yaml 里 0 写的是 smoke模型学到的就是错位的映射。解决从 xml 里提取所有类别名按字母序排一遍然后检查 txt 里的索引是否和这个顺序一致。不一致就重新生成 txt别手动改容易漏。4.3 空标注文件被当成负样本现象验证集 mAP 波动很大某些轮次突然掉得很低。原因有些图片确实没有目标对应的 txt 是空文件。YOLO 默认会把空 txt 当作负样本背景这本身没问题但如果空文件比例过高比如超过 30%模型会偏向于预测背景导致漏检。解决统计空 txt 的比例如果过高要么补充正样本要么在训练时降低负样本的权重。YOLOv8 里可以通过cls损失权重间接调整但更直接的办法是控制空文件比例在 10% 以内。4.4 图像和标注文件名大小写不一致现象训练时提示找不到标注文件但明明文件就在那里。原因Linux 系统区分大小写图片叫IMG_001.jpg标注叫img_001.txt框架按图片名去找标注时就匹配不上。解决统一转成小写或者写个脚本批量重命名。这个坑在 Windows 上开发、Linux 上训练的场景里特别常见血泪经验。4.5 验证集指标虚高现象验证集 mAP 到 0.9 以上但拿真实场景图片测试时效果很差。原因训练集和验证集划分时没有随机打散或者同一场景的图片被分到了两边导致验证集和训练集高度相似。解决划分前先按场景或时间分组确保同一组的图片只出现在训练集或验证集其中一边。如果数据里没有场景标签至少要做到随机打散别按文件名顺序切。5. 进阶技巧用 xml 做数据增强后的标注同步数据增强是提升小数据集效果最直接的手段但很多人只增强了图片忘了同步增强标注结果增强后的图片没有对应标注白忙一场。这里分享一个我常用的做法用 albumentations 库做增强它支持同时处理图片和边界框输出时再转回 YOLO 格式。import albumentations as A import cv2 import os # 定义增强管道注意 bbox_params 的 format 要设对 transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.MotionBlur(blur_limit5, p0.2), # 模拟运动模糊对烟雾场景很实用 A.RandomFog(p0.1), # 模拟雾天增加烟雾的多样性 ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_ids])) def augment_one(img_path, lbl_path, out_img_dir, out_lbl_dir, prefixaug): 对单张图和标注做增强输出到指定目录 img cv2.imread(img_path) h, w img.shape[:2] boxes [] class_ids [] with open(lbl_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: class_ids.append(int(parts[0])) boxes.append([float(v) for v in parts[1:]]) if not boxes: return # 空标注跳过 try: result transform(imageimg, bboxesboxes, class_idsclass_ids) except Exception as e: print(f增强失败 {img_path}: {e}) return stem os.path.splitext(os.path.basename(img_path))[0] out_img os.path.join(out_img_dir, f{prefix}_{stem}.jpg) out_lbl os.path.join(out_lbl_dir, f{prefix}_{stem}.txt) cv2.imwrite(out_img, result[image]) with open(out_lbl, w) as f: for cid, box in zip(result[class_ids], result[bboxes]): f.write(f{cid} { .join(f{v:.6f} for v in box)}\n) # 批量增强训练集 train_img_dir ./dataset_split/images/train train_lbl_dir ./dataset_split/labels/train aug_img_dir ./dataset_split/images/train_aug aug_lbl_dir ./dataset_split/labels/train_aug os.makedirs(aug_img_dir, exist_okTrue) os.makedirs(aug_lbl_dir, exist_okTrue) for fname in os.listdir(train_img_dir): stem os.path.splitext(fname)[0] img_p os.path.join(train_img_dir, fname) lbl_p os.path.join(train_lbl_dir, stem .txt) if os.path.exists(lbl_p): augment_one(img_p, lbl_p, aug_img_dir, aug_lbl_dir)这段代码里A.BboxParams(formatyolo)是关键它告诉 albumentations 输入的边界框是 YOLO 格式归一化的中心点加宽高库内部会自动做坐标变换输出时还是 YOLO 格式省去了手动换算的麻烦。MotionBlur和RandomFog是我针对烟雾火焰场景特意加的因为真实场景里烟雾经常伴随运动模糊雾天和烟雾在视觉上也有相似性加上这两种增强能提升模型在恶劣条件下的鲁棒性。增强后的数据直接和原始训练集合并重新生成 data.yaml 里的 train 路径即可。验证增强效果有个小技巧增强前后各训一轮对比验证集上的 mAP 和召回率。如果召回率提升明显但 mAP 没怎么动说明增强让模型找到了更多目标但定位精度没跟上这时候可以适当降低增强强度或者增加定位损失的权重。从那以后我每次做增强都会先跑一轮基线再跑一轮增强两个结果放一起看心里才有底。希望帮到你。本文还有配套的精品资源点击获取