简介:这份数据集面向从事目标检测算法研究与改进的开发者,聚焦战争场景中导弹发射、导弹爆炸产生的烟雾与火焰识别任务。数据主要从视频中抽帧制作,共包含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、explosion,txt 里却是 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_ok=True) os.makedirs(lbl_out, exist_ok=True) 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 类就写 3;names的顺序必须和 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 data=data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16imgsz=640是默认值,但如果你的图片分辨率普遍偏小(比如 416 或更小),可以调低到 416 或 512,能明显提速。batch=16要看显存,8G 显存跑 640 尺寸大概能到 16,再大就爆了。epochs=100是起步值,烟雾火焰这类目标特征比较明显,通常 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.004,batch 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(p=0.5), A.RandomBrightnessContrast(p=0.3), A.MotionBlur(blur_limit=5, p=0.2), # 模拟运动模糊,对烟雾场景很实用 A.RandomFog(p=0.1), # 模拟雾天,增加烟雾的多样性 ], bbox_params=A.BboxParams(format="yolo", label_fields=["class_ids"])) def augment_one(img_path, lbl_path, out_img_dir, out_lbl_dir, prefix="aug"): """对单张图和标注做增强,输出到指定目录""" 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(image=img, bboxes=boxes, class_ids=class_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_ok=True) os.makedirs(aug_lbl_dir, exist_ok=True) 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(format="yolo")是关键,它告诉 albumentations 输入的边界框是 YOLO 格式(归一化的中心点加宽高),库内部会自动做坐标变换,输出时还是 YOLO 格式,省去了手动换算的麻烦。MotionBlur和RandomFog是我针对烟雾火焰场景特意加的,因为真实场景里烟雾经常伴随运动模糊,雾天和烟雾在视觉上也有相似性,加上这两种增强能提升模型在恶劣条件下的鲁棒性。增强后的数据直接和原始训练集合并,重新生成 data.yaml 里的 train 路径即可。
验证增强效果有个小技巧:增强前后各训一轮,对比验证集上的 mAP 和召回率。如果召回率提升明显但 mAP 没怎么动,说明增强让模型找到了更多目标,但定位精度没跟上,这时候可以适当降低增强强度,或者增加定位损失的权重。从那以后我每次做增强都会先跑一轮基线,再跑一轮增强,两个结果放一起看,心里才有底。希望帮到你。
本文还有配套的精品资源,点击获取