导弹发射与爆炸烟雾火焰数据集:xml与txt双标注检测训练实战指南
简介:面向计算机视觉目标检测学习者与算法改进实验者的导弹发射、爆炸烟雾火焰数据集,从视频抽帧制作并完成标注,可用于YOLO系列模型的训练、验证与算法改进对比实验。资源包共约2000个文件,以930个txt标签、923个xml标注和147张jpg预览图为主,VOC与YOLO两种格式并存,方便直接接入不同检测框架;压缩包约114.95MB。影像共923幅,像方分辨率1280×720,涵盖烟雾(smoke)与火焰(fire)两类目标,其中训练集839幅、验证集84幅,划分明确,适合快速搭建实验基线。已有123人学习下载,可作为小样本烟雾火焰检测、视频抽帧数据质量分析及模型泛化验证的实践素材。
1. 导弹发射与爆炸烟雾火焰数据集:一份带 xml 和 txt 双标注的检测训练素材,到底能拿来干什么
手里有一批导弹发射、飞行、命中爆炸的现场帧,画面里同时出现尾焰、推进剂白烟、爆点黑烟和橘红火球,标注文件一半是 Pascal VOC 的 xml,一半是 YOLO 的 txt。这种「双格式混装」的数据集,第一眼看上去像是打包时偷懒,实际用起来才发现它恰好卡在两条主流检测链路的中间:xml 能直接喂给以 VOC 为默认输入的老一代训练脚本,txt 能直接进 YOLO 系列的 data 目录。对做烟火检测、发射事件识别、红外/可见光融合预警的从业者来说,这类素材的稀缺性不在「导弹」三个字,而在于同一场景里烟雾和火焰的形态跨度极大——发射段是高速喷射的锥形白烟,爆炸段是低速扩散的团状黑烟加瞬时高亮火球,模型很容易在其中一类上过拟合。这篇笔记就按「先看清数据长什么样、再把它统一成可训练格式、然后跑通一个最小检测基线、最后处理标注本身的坑」这条线走,适合已经会用 YOLO 或 Faster R-CNN 跑通公开数据集、但第一次接触这类特种场景素材的人。
2. 先摸清 xml 与 txt 的对应关系:别急着训练,先做一次数据体检
拿到混装标注,最忌讳的动作是直接train.py一把梭。xml 和 txt 往往不是同一批图的两种备份,而是不同标注轮次、不同标注人、甚至不同类别体系拼起来的。先体检,再决定要不要合并、怎么合并。
2.1 xml 与 txt 各自承载了什么信息
Pascal VOC 的 xml 是「一图一文件」,根节点annotation下挂filename、size(宽高深)、以及若干object,每个 object 里有name(类别)、bndbox(xmin/ymin/xmax/ymax,绝对像素坐标)。YOLO 的 txt 是「一图一文件」,每行class_id cx cy w h,全部归一化到 0~1。两者最本质的差异不是坐标,而是类别用字符串还是整数:xml 里写的是smoke、flame这类词,txt 里是0、1。所以合并前必须先建立一张类别映射表,否则同一个「烟」在两边会被当成两个类。
体检脚本我一般这么写,跑一遍就能看出两边图数量、类别词表、坐标越界情况:
import os, glob import xml.etree.ElementTree as ET from collections import Counter xml_dir = "labels_xml" txt_dir = "labels_txt" img_dir = "images" # 1) 统计 xml 侧类别词表与框数 xml_classes = Counter() xml_files = glob.glob(os.path.join(xml_dir, "*.xml")) bad_box = 0 for f in xml_files: root = ET.parse(f).getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) for obj in root.findall("object"): name = obj.find("name").text.strip() xml_classes[name] += 1 b = obj.find("bndbox") x1, y1 = float(b.find("xmin").text), float(b.find("ymin").text) x2, y2 = float(b.find("xmax").text), float(b.find("ymax").text) # 越界或退化框计数:这类框进训练会直接拉低召回 if x1 < 0 or y1 < 0 or x2 > w or y2 > h or x2 <= x1 or y2 <= y1: bad_box += 1 # 2) 统计 txt 侧类别 id 分布 txt_classes = Counter() txt_files = glob.glob(os.path.join(txt_dir, "*.txt")) for f in txt_files: for line in open(f): line = line.strip() if not line: continue cid = int(line.split()[0]) txt_classes[cid] += 1 print("xml 文件数:", len(xml_files), "类别:", dict(xml_classes), "异常框:", bad_box) print("txt 文件数:", len(txt_files), "类别 id:", dict(txt_classes))这段脚本干三件事:把 xml 的类别词频拉出来、把 txt 的类别 id 频布拉出来、顺手数一遍 xml 里的越界框和退化框。逻辑说明上,size/width和size/height必须读,因为后面判断越界要用原图尺寸;x2 <= x1这种退化框在爆炸高亮区域特别常见,标注人框火球时手一抖就框反了。参数上没什么可调的,路径按自己目录改即可。跑完你会得到两张频次表,接下来做映射就有了依据。
2.2 建立类别映射表:烟、焰、尾迹要不要分开
这类场景的类别体系通常有三种切法:粗粒度smoke / flame两类;中粒度launch_smoke / explosion_smoke / flame三类;细粒度再加missile_body、trail。我的经验是先按中粒度落地,粗粒度做兜底。原因很直接:发射段的白烟和爆炸段的黑烟在颜色、纹理、扩散速度上差异巨大,合成一类会让模型学到一个「平均烟」,在两类上都表现平庸;但细粒度到trail这种细长尾迹,标注一致性极差,不同人框出来的长度能差一倍,训进去就是噪声。
映射表建议落成一个 json,训练和转换脚本都读它,避免两处硬编码:
{ "smoke": 0, "flame": 1, "launch_smoke": 0, "explosion_smoke": 0, "missile_body": 2 }这里把launch_smoke和explosion_smoke都归到 0,是给「先跑通两类基线」留的后路;等你确认数据量够、标注一致性够,再把它们拆成 0 和 3。missile_body单独给 2,是因为弹体在画面里往往很小、运动快,和烟火混在一类会互相干扰。注意这张表的方向是「原始词 → 目标 id」,转换时只做单向查表,不做反向推断,避免歧义。
2.3 用一张对照表判断数据能不能直接合并
体检完别急着写转换脚本,先填一张对照表,把「能不能合并」这件事量化:
| 检查项 | 通过标准 | 不通过时的处理 |
|---|---|---|
| 图像文件名在 xml 与 txt 两侧是否一一对应 | 缺失率 < 2% | 缺失的图单独列出,人工补标或剔除 |
| 两侧类别词表能否映射到同一 id 体系 | 无未映射词 | 补映射表,禁止默认归 0 |
| xml 越界/退化框占比 | < 1% | 超标的图回炉重标 |
| txt 每行字段数是否为 5 | 100% | 字段数异常的整文件隔离 |
| 同一图两侧框数量差异 | 差异图占比 < 5% | 差异大的图以 xml 为准人工复核 |
这张表的价值在于把「感觉数据挺乱」变成「哪一项不达标、处理动作是什么」。我见过太多人跳过这步,训到一半发现某类召回死活上不去,回头查才发现是 txt 里有一批图的类别 id 整体错位了一位。血泪经验:体检花两小时,能省两天排错。
3. 把 xml 转成 YOLO txt:坐标归一化与四个边界坑
统一格式是绕不开的一步。哪怕你最终用 VOC 链路训练,也建议先转一份 txt 出来做交叉校验——两套坐标互相印证,能抓出不少标注错误。
3.1 归一化公式与转换脚本
VOC 的xmin/ymin/xmax/ymax是绝对像素,YOLO 要的是中心点加宽高、且除以图宽图高。公式不复杂,坑都在边界上:
import os, glob import xml.etree.ElementTree as ET import json xml_dir = "labels_xml" out_dir = "labels_yolo" img_dir = "images" cls_map = json.load(open("class_map.json")) os.makedirs(out_dir, exist_ok=True) def clamp(v, lo=0.0, hi=1.0): return max(lo, min(hi, v)) for f in glob.glob(os.path.join(xml_dir, "*.xml")): root = ET.parse(f).getroot() w = float(root.find("size/width").text) h = float(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in cls_map: continue # 未映射类别直接跳过,不猜 b = obj.find("bndbox") x1 = float(b.find("xmin").text) y1 = float(b.find("ymin").text) x2 = float(b.find("xmax").text) y2 = float(b.find("ymax").text) # 先裁到图内,再算中心与宽高,顺序不能反 x1, x2 = clamp(x1, 0, w), clamp(x2, 0, w) y1, y2 = clamp(y1, 0, h), clamp(y2, 0, h) if x2 - x1 < 1 or y2 - y1 < 1: continue # 裁完退化,丢弃 cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") stem = os.path.splitext(os.path.basename(f))[0] with open(os.path.join(out_dir, stem + ".txt"), "w") as out: out.write("\n".join(lines))逻辑上最关键的一句是「先裁到图内,再算中心与宽高」。如果先算再裁,中心点可能落在图外,归一化后出现负数或大于 1 的值,YOLO 训练时这类框会被静默丢弃或产生梯度异常。参数上,clamp的上下界固定 0 和 1 是归一化后的语义,但裁剪时用的是像素界0~w、0~h,两者别混。保留 6 位小数足够,再多没意义。未映射类别选择「跳过」而不是「归 0」,是因为归 0 会把未知目标硬塞进烟类,污染标签。
3.2 四个边界坑:越界、退化、极小框、坐标反写
转换脚本跑通不代表数据干净,下面四类问题在爆炸帧里出现频率极高:
越界框。火球高亮时标注人容易把框拉到画面外,xmax大于图宽。处理就是上面的 clamp,但要注意 clamp 之后框会变形,如果变形超过原框面积 20%,建议整图回炉而不是硬裁。
退化框。x2 <= x1或y2 <= y1,多发生在快速运动的弹体上——标注时参照的是前一帧位置。这类框必须丢,留着会让 loss 出现 NaN。
极小框。宽或高小于 4 像素的框,在 640 输入下缩放后不足 1 像素,学不到东西还拉低平均精度。我的做法是转换时统计,小于阈值的单独记日志,训练前统一过滤,阈值按输入尺寸定:输入 640 时取 4,输入 1280 时取 8。
坐标反写。xmin和xmax填反,表现为框跑到画面另一侧。体检脚本里x2 <= x1能抓到一部分,但填反且数值合法的抓不到,只能靠可视化抽查。建议转换后随机抽 30 张画框看一眼,这一步别省。
3.3 转换后做一次双向校验
转完 txt,拿几张图把 xml 和 txt 各画一遍框,叠在一起看是否重合。重合度用 IoU 粗算,低于 0.95 的图列出来人工看。这个校验能抓出「宽高读错节点」「用了缩略图尺寸而非原图尺寸」这类隐蔽错误——后者特别常见,xml 里size写的是原图,但实际图被预处理过,尺寸对不上,归一化全错。校验脚本不必复杂,画框存图、肉眼扫一遍即可,重点是别信转换脚本,信叠图。
4. 跑通一个最小检测基线:从 data.yaml 到第一次验证
格式统一后,先别追求高精度,目标是「能训、能出框、能看懂指标」。这一步跑通,后面调参才有参照系。
4.1 目录组织与 data.yaml 的写法
YOLO 系列对目录结构有约定,混装数据整理成下面这样最省事:
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml里三个字段必须对:path指数据集根、train/val指相对子目录、names按 id 顺序写。类别顺序一旦定下就不能改,改了等于把旧权重全废。划分比例上,这类场景样本本身不多,我一般 8:2,且按事件划分而不是按帧随机划分——同一段发射视频的相邻帧高度相似,随机划分会让验证集里混进训练集的近邻帧,指标虚高。按事件划分的意思是:同一次发射/爆炸的所有帧只进训练或只进验证。
4.2 训练命令与关键参数怎么定
最小基线用轻量模型先跑,命令大致如下:
python train.py \ --data dataset/data.yaml \ --img 640 \ --batch 16 \ --epochs 100 \ --weights yolov8n.pt \ --cache ram \ --patience 20参数逐个说:--img 640是速度和精度的折中,烟火目标通常占画面比例不小,640 够用;如果弹体是小目标,升到 960 或 1280,但显存要跟上。--batch 16在单卡 16G 上比较稳,爆显存就降到 8 并开梯度累积。--epochs 100配--patience 20,意思是 20 轮验证指标不升就早停,避免在过拟合区间空转。--cache ram把图缓存进内存,这类数据集通常几千张以内,缓存后每轮快不少。--weights用预训练权重,别从零训,样本量撑不住。
训练启动后盯三个东西:box_loss是否稳定下降、mAP50是否在 30 轮后开始抬头、验证集的可视化框有没有大面积漏检烟。如果box_loss前几轮就震荡,多半是标注里有退化框或类别 id 越界,回第 3 章查。
4.3 验证阶段看什么指标、怎么定位问题类
验证不只看一个 mAP。把每类的 precision/recall 单独拉出来,做成表:
| 类别 | Precision | Recall | 典型失败形态 |
|---|---|---|---|
| smoke | 0.7 左右 | 偏低 | 稀薄尾烟漏检 |
| flame | 较高 | 较高 | 高亮过曝区框偏大 |
| missile_body | 波动大 | 低 | 小目标、运动模糊 |
如果 smoke 的 recall 明显低于 flame,先别调模型,回去看标注:稀薄烟到底标没标、边界画在哪。这类场景里「烟到什么浓度算烟」本身就没有统一标准,标注一致性差是召回上不去的头号原因。定位问题类之后,再决定是补标、拆类还是加数据增强。增强上,烟火场景慎用大角度旋转和强色彩抖动——火焰颜色是重要特征,色偏太狠等于教模型学错。
5. 避坑与排查:这类特种场景数据集最容易翻车的五件事
5.1 现象:训练 loss 正常但验证集几乎不出框
原因通常是验证集图片路径在data.yaml里写错,或者验证集 txt 文件名和图片名对不上(比如图片是.jpg、标签是.JPG.txt)。YOLO 找不到标签时不会报错,只会当成「无目标」图,于是模型被训练成「什么都别框」。解决:写个脚本比对 images/val 与 labels/val 的文件名主干集合,差集打印出来,逐个修。
5.2 现象:某一类 mAP 始终为 0
先查类别 id 是否越界。names里写了 3 类(id 0/1/2),但 txt 里出现 id 3,训练时这类样本被忽略,验证时自然为 0。再查映射表是否把两个原始词映到了同一个 id,导致其中一类被覆盖。解决:训练前跑一遍全量 txt 的 id 频次统计,和names长度对齐,不一致就停。
5.3 现象:模型把爆炸火球同时框成 smoke 和 flame
这是标注体系本身的重叠问题——火球周围既有烟又有焰,标注人可能两个都标。模型学到的是「这块区域两个类都对」,推理时双框。解决:定一条互斥规则,比如「火球主体标 flame,外扩烟标 smoke,重叠面积超过 30% 时只保留 flame」,然后按规则回洗一遍标注。规则要写进标注规范,不然下一批还是乱。
5.4 现象:同一段视频相邻帧指标忽高忽低
按帧随机划分导致训练集和验证集共享近邻帧,验证指标虚高且不稳定。解决:按事件/视频段划分,同一段只进一侧。划分后验证集指标会掉一截,但那是真实水平,别慌着调回去。
5.5 现象:xml 和 txt 对同一张图的框数量差很多
多半是两批标注来自不同轮次,txt 是早期粗标、xml 是后期精标,或者反过来。解决:以框更多、更细的一侧为准,另一侧整图重转,不要逐框合并——逐框合并会产生大量重复框,NMS 都救不回来。合并前先按文件名对齐,差异图单独列清单人工过。
6. 进阶:用「事件级」评估替代单帧 mAP,以及一个我常备的抽查习惯
单帧 mAP 高,不代表这套模型能用在发射事件预警上。真实链路里,你关心的是「一次发射从点火到爆炸,模型有没有在正确的时间段给出连续、稳定的检测」,而不是某一帧框得准不准。所以我一般会补一层事件级评估:把验证集按事件分组,对每个事件统计「首检帧相对真值首帧的延迟」「事件内检测覆盖率」「误检帧占比」三个数。延迟用帧数差除以帧率换算成秒,覆盖率用「有正确检测的帧数 / 事件总帧数」,误检帧占比看的是事件外帧被误报的比例。这三个数比 mAP 更能说明能不能上线。
具体做法不复杂:验证集推理时保留每帧的检测结果 json,按事件 id 聚合,写个小脚本算上面三个指标。阈值上,延迟超过 0.5 秒基本没法用于预警,覆盖率低于 0.6 说明模型在事件中段掉链子,误检帧占比高于 0.1 就要回去看负样本够不够。这套评估跑一遍,你会对「模型到底能不能用」有完全不同的判断。
另一个习惯是每次改完标注或转换脚本,固定抽查 20 张图叠框。不抽随机,专抽三类:极小目标多的、爆炸高亮的、烟特别稀薄的。这三类是最容易在转换和标注里出错的,抽它们比随机抽有效得多。这个习惯救过我很多次——有一次批量转换后坐标整体偏移了半个框,随机抽查没看出来,专抽高亮帧一眼就发现框全飘了。
说到底,这类带 xml 和 txt 双标注的特种场景数据集,价值不在「导弹」这个题材本身,而在于它逼你把数据体检、格式统一、类别体系、事件级评估这条链路完整走一遍。走通了,换任何烟火类场景都能复用。希望帮到你。
本文还有配套的精品资源,点击获取