简介:面向目标检测与火焰烟雾识别任务的带 COCO 标记火灾检测数据集,共 9332 张真实场景图片,适合训练 YOLOv8、Faster R-CNN 等模型。压缩包内含 2000 个文件,以 jpg 图像为主体(1995 张),另有 3 个 json 标注文件与 2 个 txt 说明文件,整体约 550.82MB,可直接用于监督学习与模型微调。已有 903 人学习下载,覆盖烟火、火焰与烟雾三类目标,图片尺寸包含 large、middle、other 等多种规格,便于不同分辨率训练需求。标注格式清晰,json 文件可灵活转换为 COCO 或 YOLO 格式使用,txt 文件提供基础说明,能帮助研究者跳过繁琐的数据整理环节,快速开展火灾检测实验、算法对比或项目演示。可用于模型效果对比、消融实验及部署验证;标注内含类别、边界框坐标等关键信息,便于二次开发。资源目录按图片规格划分,适用性较强,适合计算机视觉初学者、算法工程师及安防消防领域项目团队使用。
1. 带COCO标记的火灾检测数据集:为什么烟比火更难检
我在拿到这个标题描述的数据集时,第一反应不是“又多了一套训练素材”,而是“终于有人把烟雾单独拎出来了”。做过消防预警落地的人都有同感:火焰检测在白天和近距离场景其实不难,真正让报警系统反复漏报、误报的,是烟雾——它出现得比明火早,形态又比明火含糊,半透明、无定形、随气流扩散。这套带COCO标记的火灾检测数据集一共9332张图片,把目标细分为烟火、火焰、烟雾三类,意味着训练出的检测模型不只会回答“有没有火”,还能回答“现在处于什么阶段”。它适合正在做智慧园区、工地监控、室内消防预警,并且计划用目标检测框架自行训练模型的人。接下来我会按拿到数据后实际会走的路线:格式解析、数据清洗、模型训练、分类边界、避坑排查来展开。
2. 从COCO JSON读数据:找类别分布、看bbox质量
拿到任何COCO格式数据集,第一件事不是建训练脚本,而是把JSON里的信息摸清楚。COCO标记本质上是一个自描述的JSON结构,里面藏着图像清单、标注框和类别定义三块内容。这套数据集的标注质量能不能支撑训练,取决于类别分布是否均衡、bbox是否贴合目标、有没有越界或空框。这一章先拆格式,再讲怎么把标注可视化地检查一遍。
2.1 读JSON统计类别分布:一段脚本看清三类比例
COCO标记的JSON核心是images、annotations、categories三个数组。images数组记录每张图的文件名、宽高和id;annotations数组是真正的标注数据,每条包含image_id、category_id、bbox和area;categories数组把category_id映射到类别名称。bbox的格式是[x, y, width, height],单位是像素,坐标原点在图像左上角。想规划训练策略,第一步是把这三类各自有多少个标注框数清楚。
import json from collections import Counter # 假设标注文件放在 annotations/instances_train.json with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) # 建立 image_id 到文件名的映射,后面画框会用到 img_id_to_file = {img["id"]: img["file_name"] for img in coco["images"]} cat_id_to_name = {cat["id"]: cat["name"] for cat in coco["categories"]} # 按类别统计标注框数量 ann_count = Counter(a["category_id"] for a in coco["annotations"]) for cat_id, cnt in ann_count.most_common(): print(f"{cat_id_to_name[cat_id]}: {cnt} 个框") # 统计单张图里标注框数量的分布 per_img_count = Counter(a["image_id"] for a in coco["annotations"]) counts = [per_img_count[img["id"]] for img in coco["images"]] print(f"平均每张图约 {sum(counts) / len(counts):.1f} 个框") print(f"无标注框的图像数量: {sum(1 for c in counts if c == 0)}")这段脚本输出的信息直接决定后续怎么办。如果三类框数量差距超过一倍,训练时多数类会主导loss,少数类即使能检出,mAP也偏低。我的习惯是看到分布后先不做任何增强,而是等训练完第一轮,再看每类AP的差距,差距大再针对性补样本。另一个值得留意的输出是无标注框的图像数,这些图可能是空场景负样本,也可能是标注遗漏。前者有价值,后者是数据噪声,需要抽出来人工确认。如果某张图所属类别明显是火灾现场却没有框,大概率是漏标,训练时模型会被这种图误导,见到火却学不到对应关系。
2.2 可视化抽查:用OpenCV把标注画回图像上
统计只能说明数量,不能说明质量。COCO格式的bbox是人手画的,烟雾这类目标的边界非常主观,同一片烟两个人能画出差异很大的框。我一般会写一个画框脚本,随机抽取几十张图把标注叠加回去,肉眼看一遍。这个步骤能发现三类典型问题:框太大把背景包进去、框太小只框住浓烟中心、以及不同图上对同类目标的画法不一致。
import json import os import cv2 with open("annotations/instances_train.json", "r", encoding="utf-8") as f: coco = json.load(f) img_id_to_file = {img["id"]: img["file_name"] for img in coco["images"]} cat_id_to_name = {cat["id"]: cat["name"] for cat in coco["categories"]} anns_by_img = {} for a in coco["annotations"]: anns_by_img.setdefault(a["image_id"], []).append(a) # 随机抽 10 张图 sample_ids = list(img_id_to_file.keys())[:10] os.makedirs("check_vis", exist_ok=True) for img_id in sample_ids: image_path = os.path.join("images", img_id_to_file[img_id]) img = cv2.imread(image_path) for ann in anns_by_img.get(img_id, []): x, y, w, h = [int(v) for v in ann["bbox"]] name = cat_id_to_name[ann["category_id"]] color = (0, 0, 255) if name == "flame" else (0, 255, 0) # 火焰红色,烟雾绿色 cv2.rectangle(img, (x, y), (x + w, y + h), color, 2) cv2.putText(img, name, (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(f"check_vis/{img_id}.jpg", img)这段代码的画框逻辑很简单,但对排查标注问题是必要的。参数方面,颜色和文字字号是给人看的,不用太讲究;关键在于类别代号要和实际categories定义对上,否则整张图会画乱。抽查时要重点看三类对象的框法:火焰的框是否把外焰包进去了,还是只框了亮度最核心的区域;烟雾的框是否画到边缘羽化处就停了;含烟火的图,框是只包着火点,还是把烟扩散区也圈进来。不同图上同一类目标的框法差异越大,训练收敛后这类目标的定位稳定性就越差。
3. COCO转YOLO并跑通训练:一条转换脚本加一份参数表
COCO标记是通用格式,但实际训练时很多人用的是YOLO生态,它要求的标签是每张图对应一个txt,每行写“类别id 归一化中心x 归一化中心y 归一化宽 归一化高”。从COCO JSON到这个格式,转换脚本不复杂,坑却集中在边界处理和类别映射上。这一章给出可直接用的转换脚本和一组保守的训练参数,照着跑能拿到一个可用的基准模型。
3.1 COCO JSON转YOLO格式:脚本与三个必改参数
转换流程本身不绕:遍历annotations,把bbox从[x, y, width, height]换算成中心点坐标,再分别除以图像宽高做归一化。真正需要花心思的是三件事:类别id必须重映射成从0开始的连续整数;bbox数值要做越界保护;质量太差的框要直接过滤掉。不处理这三件事,训练时要么YOLO报错,要么模型学到一堆错误坐标。
import json import os def coco_to_yolo(ann_path, image_dir, out_dir, cls_map, min_area=4.0): """ cls_map: 把COCO category_id 映射为连续的YOLO类id,例如 {1: 0, 2: 1, 3: 2} min_area: 过滤面积小于该值的框,单位像素 """ with open(ann_path, "r", encoding="utf-8") as f: coco = json.load(f) img_id_to_info = {img["id"]: img for img in coco["images"]} os.makedirs(out_dir, exist_ok=True) for img in coco["images"]: img_id, width, height = img["id"], img["width"], img["height"] lines = [] for a in coco["annotations"]: if a["image_id"] != img_id: continue cat_id = a["category_id"] if cat_id not in cls_map: continue # 只保留你要用的类别 x, y, w, h = a["bbox"] x2, y2 = x + w, y + h # 裁剪到图像边界内 x = max(0, min(x, width - 1)) y = max(0, min(y, height - 1)) x2 = max(0, min(x2, width - 1)) y2 = max(0, min(y2, height - 1)) if x2 - x < 2 or y2 - y < 2: continue # 过滤掉退化框 if (x2 - x) * (y2 - y) < min_area: continue cx = (x + x2) / 2 / width cy = (y + y2) / 2 / height bw = (x2 - x) / width bh = (y2 - y) / height lines.append(f"{cls_map[cat_id]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") stem = os.path.splitext(img["file_name"])[0] with open(os.path.join(out_dir, f"{stem}.txt"), "w", encoding="utf-8") as f: f.write("\n".join(lines)) # 示例:假设 categories 定义中 1=flame, 2=smoke, 3=fire_with_smoke coco_to_yolo( ann_path="annotations/instances_train.json", image_dir="images/train", out_dir="labels/train", cls_map={1: 0, 2: 1, 3: 2}, )逻辑说明放在参数上。width和height必须用标注文件里images数组记录的值,而不是自己读图算,COCO的原始标注可能存在极端情况下宽高记录和实际图不一致,用错会导致所有归一化坐标集体偏移。cli_map这个参数决定了训练时类别顺序,写错会让火焰和烟雾互相调换,这类错误很难通过loss曲线发现,只有在推理阶段才会暴露。min_area过滤值是经验值,太小的框通常是人手误画,对检测模型训练不但没帮助,还会增加定位目标的方差。
3.2 训练配置:data.yaml、超参表与一行启动命令
标签转换完,训练侧还需要一个data.yaml描述数据集路径和类别信息,然后直接启动训练。我倾向用YOLO系的训练入口,因为它把数据加载、增强、评估串成了现成流程,适合第一版基准模型。如果你的环境里装的是其他框架,下面这组参数代表的含义依然是通用的。
# fire.yaml path: /data/fire_dataset train: images/train val: images/val nc: 3 names: 0: flame 1: smoke 2: fire_with_smokeyolo detect train \ model=yolo11n.pt \ data=fire.yaml \ imgsz=640 \ batch=16 \ epochs=150 \ patience=30 \ lr0=0.005训练命令里值得解释的是几个直接影响结果的参数。imgsz默认640对这个数据集可能不够,烟雾是小目标,尤其是远处刚起的淡烟,在640分辨率下只有十几像素,这时候我会上到960甚至1280,代价是显存占用翻倍,训练速度下降。batch大小首先被显存限制,16是个保守值,如果你用大分辨率训练,batch要相应降到8或4。epochs设150配合patience=30意味着连续30轮验证集mAP不涨就提前停止,这是防止过拟合的兜底。lr0取0.005适合从头微调预训练权重,效果不够可以后续再下调。
注意:第一轮训练不要追求最优,先拿一套保守参数把流程跑通,记录训练时长、显存占用、每类AP。后续调参有了这些基线才有对比意义。
4. 烟火、火焰、烟雾三类边界:从标注约定到后处理合并
三类目标里,火焰和烟雾的特征差异很大,模型通常分得开;真正让模型混乱的是“烟火”这一类。烟火字面上是火焰和烟雾同时存在,但不同标注者对它的框法可能完全不同。这一章拆解类间边界问题,并给出一个不用重训就能改善输出的后处理方案。
4.1 烟火类标注的两种框法:为什么mAP会被人为拉低
火焰在图像里的特征是高亮、跳变、轮廓清晰,模型学到的是亮度和纹理的组合;烟雾则是半透明的低对比度区域,模型更多靠颜色和质地区分。烟火类的尴尬在于目标定义不唯一:一种框法只包住火焰本体,另一种把火焰连同扩散的烟雾区域一起框进去。同一个category_id下面混着两种框法,模型在训练时会试图拟合两个矛盾的目标区域,最终表现为这类AP明显低于另外两类。
应对方式分两步。第一步是抽样式地查看烟火类的标注框,如果混框比例确实高,我建议把烟火类样本拆出来做一次轻量重标:定义统一规则,比如“烟火类的框必须覆盖火焰和烟雾的最小外接矩形”,然后只修偏差大的图。第二步是训练时用更强的数据增强,YOLO训练默认开mosaic,它会拼图并随机缩放,对烟雾这种无定形目标比较友好,可以再打开mixup增强类别间的过渡形态,代价是训练时间变长。
4.2 类间后处理合并:不重训也能把三类压回两类
很多业务场景最终只关心“有没有火情”,不关心当前是纯火焰还是烟火并存。这时候与其让模型强行区分,不如在推理输出层做合并逻辑。原理是:火焰框和烟火框如果高度重叠,它们在物理世界大概率指同一处火源,NMS阶段把它们压成一个更合理。做法一般有两种:调整NMS的IoU阈值,或者自定义合并函数。
def merge_fire_outputs(dets, iou_thr=0.5): """ dets: 每项是 dict,包含 bbox(xyxy), score, class_name 合并规则:烟火框与火焰框重叠超过阈值时,保留置信度更高者 """ merged = [] for d in sorted(dets, key=lambda x: x["score"], reverse=True): keep = True for m in merged: if compute_iou(d["bbox"], m["bbox"]) > iou_thr: keep = False break if keep: merged.append(d) return merged这段代码里的compute_iou需要自己实现,实际做的时候直接用框架自带的NMS变体也行。合并策略本身不是模型层面的改动,它只是改变了输出协议,适合在部署阶段加在检测器后面,避免因为烟火类的AP偏低导致漏报。如果不希望程序上写死,更省事的做法是训练时直接把烟火类并入火焰类,只让模型输出火焰和烟雾两类,这样类别混淆问题从源头消失,代价是丢失“烟火阶段”这个业务信息。
5. 用这个数据集易踩的5个坑:现象、原因、排查
下面5个问题是我处理同类COCO火灾数据集时反复遇到的,每一条都按现象、原因、解决三个环节写。这些坑不一定都在这套数据集中出现,但排查顺序值得照做。
5.1 bbox越界与零面积框导致训练中断
现象:转换脚本跑完,训练刚开始就报错,提示标签里有负数坐标或宽高为零的框。原因:手工标注时点在图像边缘,或者JSON里记录的图像尺寸和实际不一致,换算后坐标超出[0,1]范围。解决:转换脚本里对坐标做clamp,宽高小于2像素的直接过滤。前面3.1节的脚本已经内置了这个逻辑,如果你用的是现成转换工具,务必先确认它是否处理了越界。
5.2 类别ID不连续导致标签张冠李戴
现象:训练能正常跑,但验证时发现模型把烟当成了火,或者可视化预测框时颜色对不上。原因:COCO的categories里id不一定是连续整数,比如fire=1、smoke=3、fire_with_smoke=5,直接拿这个id当YOLO类id会让names表错位。解决:严格按照我给出的cls_map重映射,转换后抽一张图看txt内容,确认第一列数字对应的类别名。
5.3 烟雾框重叠过多导致NMS互相压制
现象:模型对烟雾的召回率看着还行,但输出框大量重叠,最终结果里烟雾框经常被吞掉。原因:同一片烟雾被标注了多个大框,彼此IoU很高,训练时模型学成了多个中心点,推理阶段NMS把这些框视作重复检测,只保留一个。解决:转换前对同图同类的高重叠框做一次去重,IoU大于0.7时保留面积大的;或者推理时把NMS阈值从默认0.5调高到0.6,减少压制。
5.4 烟火类标注口径不一致导致AP被拉低
现象:训练完烟火类AP明显低于火焰和烟雾,而且低得没有规律。原因:这类标注边界主观,有人只框火焰,有人框整片烟区,模型拟合了矛盾的目标区域。解决:抽检50张烟火类的图,统计框覆盖范围的差异;差异大就统一定义后重标,或者干脆把烟火类合并到火焰类里,减少类间冲突。
5.5 白天淡烟和夜间火焰表现分裂
现象:总mAP不错,但白天场景的烟雾检出率低,夜间场景反而稳定。原因:白天背景复杂,淡烟对比度低,模型没能学到烟的中低层特征;夜间背景单一,火焰亮度突出,模型实则是靠亮度而非形状做的判断。解决:训练时加亮度扰动和随机遮挡增强,模拟白天强光和复杂背景;评估时把测试集按“白天”“夜间”分开统计AP,别被总指标掩盖了场景缺陷。
6. 增量自举与烟雾类评估:把9332张图用出更多价值
9332张图作为底模够用,但想部署到具体场景,几乎一定还要迭代。我常用的做法是增量自举:用这批数据训练出的模型去跑目标场景的连续帧,把置信度低于0.6的检测框收集起来,人工快速复核后补标,再合并回训练集继续训练。这套流程的产出是带场景伪标签的扩展数据,尤其对烟雾这类外观差异大的目标特别有效,底模负责缩小候选范围,人工只负责确认,成本远低于完全从头标注。
评估环节要养成两个习惯。第一,不用总mAP覆盖一切,单独看smoke类的AP和召回率,烟雾是火灾预警里最有价值也最难检的信号。第二,把测试集按“纯烟”“纯火”“烟+火共存”分组,分别算指标,三个数都不是同一量级时,说明模型实际是场景分类器而不是目标检测器。用pycocotools做评估时可以把IoU阈值固定在0.5,并调大maxDets到300,避免漏掉大量重叠烟雾框的统计。我每次训练结束都会先看一眼烟雾类的每类AP,再决定要不要补数据、调分辨率还是改后处理。这个习惯帮我避开了不少“整体指标好看、现场烂得一塌糊涂”的翻车时刻。这套流程跑通之后,数据集本身只是起点,真正值钱的是你围绕它建立的评估和迭代闭环。希望帮到你。
本文还有配套的精品资源,点击获取