多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

火灾检测COCO数据集:烟火/火焰/烟雾分离标注与YOLO转换

火灾检测COCO数据集:烟火/火焰/烟雾分离标注与YOLO转换 简介带COCO标注的火灾检测数据集聚焦烟火、火焰与烟雾三分类目标检测适合算法工程师、计算机视觉研究者以及消防智能预警项目开发人员使用。压缩包共2000个文件具体包含1995张JPG图像、3个JSON标注文件和2个TXT说明文件整体约550.82MB。JPG提供火灾场景图像JSON采用COCO标注结构TXT可用于记录类别或数据集划分下载后能较为方便地对接YOLOv8等主流检测模型。目前已有903人学习下载说明该数据集具备实际参考价值值得在目标检测场景中进一步尝试。读者可基于这些数据完成数据划分、格式转换、模型微调与评估借助烟火、火焰和烟雾的精细标注提升检测细粒度降低误报率对烟火和烟雾外观接近的复杂场景该区分能力能明显提升模型可用性对早期火灾预警、社区消防和野外火情监控等场景有直接帮助。1. 带COCO标记的火灾检测数据集烟火、火焰、烟雾分开标火灾预警模型才算真的能落地火灾预警的实际部署里有个规律早期先冒烟火焰随后才起来等火完全烧旺了烟雾又被火光压下去。很多模型只标一个fire类训练完要么把白色烟囱当烟雾要么夜间漏掉小火苗。这批带COCO标记的火灾检测数据集总共9332张图片把烟火、火焰、烟雾分开标注本质上是要让检测器同时学会“烟”和“火”两种形态的差异而不是笼统归成火灾。对刚进场的目标检测开发者、做烟火识别的学生、或者想快速验证YOLO输出效果的工程师来说这套数据提供了现成的categories定义、annotations映射和图片清单拿到就能开始做可视化与训练不需要再从头自己标一遍。2. 先摸清数据集结构三类标注边界、目录划分与COCO JSON的关键字段2.1 三类标注的语义边界烟火、火焰、烟雾到底按什么标准切分这个数据集我拆开看的时候第一件事不是跑代码而是看categories表里每一条的命名和注释。很多人拿数据集上手就跑脚本结果模型训练完才发现烟雾类里混进了大量标注为火焰的图这种问题追究到底是语义边界没定义清楚。常见的数据集定义方式是这样烟火指的是“有火源但被烟大量遮挡”的状态画面里能看到火光边缘但轮廓不清晰火焰指的是“肉眼可见的明火区域”轮廓相对锐利颜色偏黄橙烟雾则是“没有明火、只有烟气扩散”的区域形态模糊边缘过渡大颜色多为灰白或蓝灰。这个区分不是三种类别三套标准而是按“火源可见度”和“烟气浓度”两个维度组合出来的。实际标注时这三类经常会互相覆盖。比如一张厨房起火的图锅底有明火但上方一大片油烟已经把火包裹住标注人员很容易把整片区域画成烟火而不是火焰。比较严的标注规范会规定只要明火区域面积超过框面积的30%就必须标为火焰否则标为烟火。烟雾类的判定相对独立只关注烟气形态不考虑火源是否在这张图里出现。也就是说一张图可以同时存在烟雾框和火焰框它们互斥但并不互相排斥。这个语义边界决定了后续训练时模型的学习目标。如果你把烟火类做得太泛火焰和烟雾的框都会被卷进来模型会学出一个“介于两者之间”的模糊特征推理时置信度普遍偏低。所以拿到数据集后第一步建议按类别抽样本人工过一遍看100张左右心里对边界有数再进训练流程。2.2 图片目录与划分方式train、val的常见比例与场景分层问题COCO格式数据集的图片存放一般有两种组织方式一种是所有图片平铺在一个文件夹里靠JSON里的file_name字段关联另一种是按train/val子目录分好。这批数据我拿到手看了一下图片文件名按场景序列编号比如同一处火灾现场的多帧连续图像会共享前缀这种命名习惯对划分数据很有用。划分上常见的做法是8:2或者9:1这本身没问题但不建议直接随机切。火灾检测场景里同一事件的多帧图像之间相似度极高如果随机切分同一个着火点的几十帧会被同时分进train和val验证集的难度被严重低估线上推断时表现会明显下跌。我一般会先按文件名前缀分组前缀相同的所有帧必须进同一个集合再在组级别做随机划分。这个操作不需要额外工具一个按前缀分组的脚本就能解决。val集合的大小不用太大。检测任务里val集主要用来挑anchor、看loss曲线和做early stopping400到800张就够用。这批9332张图按8:2分下来val大约有1800多张实际上是偏多的。如果训练时发现val指标波动太大可以考虑再从val里按场景抽一半出来单独做test把剩余部分作为真正的验证集。到底是分三层还是两层取决于你要不要对外报评估数据。2.3 直接读COCO JSON把图片数、标注数、类别数一次跑出来COCO格式的JSON顶层就三个字段images、annotations、categories。images里每项是单张图的file_name、width、height和idcategories里每项是类别的id、name和supercategoryannotations里每项则是bbox、area、image_id、category_id和segmentation。要快速了解数据集健康度不需要打开JSON看跑一段脚本最直接。import json from collections import Counter # 修改成你解压后的实际路径 ann_path fire_dataset/coco_annotations/instances_train.json with open(ann_path, r, encodingutf-8) as f: coco json.load(f) images coco[images] annotations coco[annotations] categories coco[categories] # 建立category_id到name的映射 cat_id2name {cat[id]: cat[name] for cat in categories} # 统计每个类别的实例数 cat_counter Counter() for ann in annotations: cat_counter[cat_id2name[ann[category_id]]] 1 # 统计每张图上的目标数量 img_id2n Counter() for ann in annotations: img_id2n[ann[image_id]] 1 print(f图片总数: {len(images)}) print(f标注框总数: {len(annotations)}) print(f类别定义: {categories}) print(f每个类别的实例数: {cat_counter}) print(f单图目标数分布: min{min(img_id2n.values())}, max{max(img_id2n.values())}, 平均{len(annotations)/len(images):.2f})这段脚本的逻辑很简单加载JSON后先建映射再对annotations列表做遍历统计。参数方面注意categories里的id不一定是0、1、2连续排布可能存在跳过或乱序后面转YOLO格式时建议以这里的id映射为准而不是想当然用数组下标。单图目标数能反映数据集的密度分布如果平均每张只有0.5个目标大量图片其实是背景图这种数据集训练出来的模型误检率会偏高。跑完这段脚本你应该已经能回答三个问题这批数据里每类各有多少实例、标注密度如何、类别定义是否合理。接下来才可以放心做可视化和转换。3. 可视化标注与类别统计bbox画框、实例计数与小目标筛选脚本3.1 类别实例数统计先判断数据是否平衡再决定要不要做采样训练火灾检测模型时类别不平衡是绕不开的问题。烟雾往往是大面积飘散形态一个框能框住半张图实例总数看起来不多但覆盖面积很大火焰框小而密集实例数通常偏高。只看总数会上当要看“实例数”和“像素占比”两个维度。import json import numpy as np with open(fire_dataset/coco_annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) # 高度和宽度字段直接从annotations里读COCO的bbox是[x,y,width,height] box_sizes {cat[id]: [] for cat in coco[categories]} for ann in coco[annotations]: cat_id ann[category_id] w, h ann[bbox][2], ann[bbox][3] box_sizes[cat_id].append(w * h) for cat in coco[categories]: areas np.array(box_sizes[cat[id]]) if len(areas) 0: continue # 统计像素面积的对数分布火焰目标一般偏小烟雾目标偏大 log_areas np.log10(areas 1) print(f类别 {cat[name]}: 实例数{len(areas)}, f面积中位数{np.median(areas):.0f}px, f面积均值{areas.mean():.0f}px, flog10面积均值{log_areas.mean():.2f})这里用log10做归一化是因为烟雾框面积动辄几十万像素火焰框可能只有几百像素直接算均值会被大目标带偏。加1是为了避免面积为0的异常框取对数时报错。跑完后你会发现三个类别的面积分布大概率存在明显分层这是数据本身的性质不是标注错误后面做训练时要针对小目标类适当增加copy-paste或马赛克增强否则小火焰框很容易被当成背景忽略掉。3.2 在原图画bbox人工目测三类标注质量的核心方法统计数字只能说明“有多少框”不能说明“框得对不对”。可视化抽查是任何检测数据集落地前必须做的一步。下面这段脚本用matplotlib把标注框画在原图上同时按类别用不同颜色区分。import json import cv2 import matplotlib.pyplot as plt import matplotlib.patches as patches def visualize_samples(json_path, img_dir, num_samples8): with open(json_path, r, encodingutf-8) as f: coco json.load(f) # 建立image_id到图片信息的映射 img_id2info {img[id]: img for img in coco[images]} # 把annotations按image_id聚合 ann_group {} for ann in coco[annotations]: ann_group.setdefault(ann[image_id], []).append(ann) # 从全部图片ID里均匀抽样避免连续抽到同一个场景的多帧 all_ids list(img_id2info.keys()) sampled_ids all_ids[:: max(1, len(all_ids) // num_samples)][:num_samples] fig, axes plt.subplots(2, 4, figsize(16, 8)) axes axes.flatten() for idx, img_id in enumerate(sampled_ids): img_info img_id2info[img_id] img_path f{img_dir}/{img_info[file_name]} img_bgr cv2.imread(img_path) img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) axes[idx].imshow(img_rgb) for ann in ann_group.get(img_id, []): x, y, w, h ann[bbox] cat_id ann[category_id] # 给三个类别分别上色这里按实际类别自行调整 color_map {1: red, 2: yellow, 3: cyan} rect patches.Rectangle( (x, y), w, h, linewidth2, edgecolorcolor_map.get(cat_id, white), facecolornone ) axes[idx].add_patch(rect) axes[idx].text( x, y - 4, f{cat_id}, colorcolor_map.get(cat_id, white), fontsize9, bboxdict(facecolorblack, alpha0.5) ) axes[idx].axis(off) plt.tight_layout() plt.show() # 调用示例路径请替换成实际目录 visualize_samples( fire_dataset/coco_annotations/instances_train.json, fire_dataset/train_images, num_samples8 )这段脚本里两个参数值得多说num_samples控制抽多少张图建议不要一次看太多8到12张比较合适眼睛能记住细节抽样步长的写法用的是均匀抽帧而不是random.seed随机抽取好处是能看到不同场景序列的分布避免随机抽出来的全部是同一个着火点的连续帧。判断质量时看三件事第一火焰框是否紧贴明火边缘还是松垮地包了一圈背景第二烟雾框是否沿着烟气扩散方向覆盖了主体区域第三小目标的框有没有被漏掉特别是远处起火的初期火苗。3.3 小目标与畸形框筛选训练前挑出需要单独增强的样本火灾检测里的“小目标”和通用目标检测里定义不太一样。通用数据集里小于32x32像素算小目标但火灾场景中一个远处的火苗可能只有8x8像素在1080P原图上占比非常低。可视化阶段除了看质量还要顺手统计一下小目标占比这直接决定训练策略。import json with open(fire_dataset/coco_annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) img_id2wh {img[id]: (img[width], img[height]) for img in coco[images]} small_count, mid_count, large_count 0, 0, 0 total 0 for ann in coco[annotations]: w, h ann[bbox][2], ann[bbox][3] img_w, img_h img_id2wh[ann[image_id]] # 以相对原图面积的比例判断目标大小 ratio (w * h) / (img_w * img_h) if ratio 0.01: small_count 1 elif ratio 0.1: mid_count 1 else: large_count 1 total 1 print(f小目标(面积占比1%): {small_count} ({small_count/total*100:.1f}%)) print(f中目标(1%-10%): {mid_count} ({mid_count/total*100:.1f}%)) print(f大目标(10%): {large_count} ({large_count/total*100:.1f}%))这里用面积占比而不是绝对像素是因为图片分辨率不统一同一块火苗在小分辨率图里占比高在2K图里占比低。阈值1%和10%是我做检测训练时习惯用的分界线你可以按自己的场景调。如果小目标占比超过三成训练时就别把图片粗暴resize到640x640建议用letterbox保留原始比例或者训练时做多尺度采样否则小目标会被压缩到几个像素根本学不到特征。4. COCO转YOLO格式坐标归一化脚本、类别映射与四个边界问题4.1 转换脚本把COCO的bbox转成YOLO训练用的txt文件YOLO系列训练时需要的标注格式是每张图一个txt每行类别索引加归一化后的中心点坐标和宽高。COCO里的bbox是左上角坐标加宽高转换公式并不复杂但写错一个维度就会整体偏移。import json import os def coco2yolo(json_path, output_dir): with open(json_path, r, encodingutf-8) as f: coco json.load(f) # 建立image_id到图片信息的映射 img_id2info {img[id]: img for img in coco[images]} # 建立image_id到标注列表的映射 ann_group {} for ann in coco[annotations]: ann_group.setdefault(ann[image_id], []).append(ann) os.makedirs(output_dir, exist_okTrue) for img_id, img_info in img_id2info.items(): img_w, img_h img_info[width], img_info[height] txt_path os.path.join( output_dir, os.path.splitext(img_info[file_name])[0] .txt ) lines [] for ann in ann_group.get(img_id, []): x, y, w, h ann[bbox] cat_id ann[category_id] # 转成归一化的中心点坐标和宽高 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h # 避免坐标越界轻微裁剪到[0,1]区间 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) lines.append(f{cat_id - 1} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f转换完成共处理 {len(img_id2info)} 张图片输出到 {output_dir}) coco2yolo(fire_dataset/coco_annotations/instances_train.json, fire_dataset/yolo_labels)这里有个容易忽略的点YOLO的类别索引从0开始而COCO的category_id可能从1开始所以我写了cat_id - 1。如果数据集里的category_id本身就从0开始这行就要去掉。转换完成之后建议核对每个txt文件的行数与这张图在COCO里对应的标注数是否一致不一致说明有图片在JSON里缺失了file_name对应的实体文件或者标注被跳过。4.2 边界问题一segmentation多边形与bbox不一致时该信谁COCO里很多标注同时带有segmentation和bbox两个字段理论上bbox应该恰好包住segmentation的外接矩形。但有些标注工具生成的数据里两者并不严格对应甚至会出现bbox明显大于segmentation范围的情况。当两者冲突时我一般以segmentation为准重新计算外接矩形。原因很简单segmentation是标注人员逐点描出来的边界bbox可能是工具自动生成的自动生成的更容易包含无关背景。重新计算的方法是用numpy读取segmentation的所有坐标点取x和y方向的最小最大值作为新的bbox。虽然这个操作增加了预处理步骤但能让模型学到的目标特征更干净特别是对烟雾这类边缘模糊的目标。4.3 边界问题二归一化后坐标越界COCO标注里偶尔会出现宽高为0的异常框或者xw超过图片宽度的越界框。这种框直接转YOLO后会产生负数坐标或者大于1的值训练时YOLO会在计算损失时把锚框推往没有意义的位置表现为loss发散的准备工作。解决方式是在转换脚本里做两件事第一过滤掉宽高小于1像素的框这类框在很大程度上是标注时的误操作第二对归一化后的坐标做裁剪让中心点和宽高都落在[0,1]区间。注意裁剪只能解决数值合法性问题不能修复语义错误。一个框如果越界超过原图宽度的20%说明标注本身有问题应该在可视化环节排查原始标注。4.4 边界问题三空标注文件是否保留火灾数据集中存在不少背景图可能一张图里完全没有烟火但模型训练时需要这些负样本。转成YOLO格式时这张图的txt文件是空的有些转换脚本会跳过空文件结果训练时少了一批负样本误检率会显著上升。我在转换时会保留空txt文件同时在生成训练列表时把这类图也放进去。不同框架对空标注的处理不同比如某些YOLO训练脚本遇到空txt会直接跳过不报错另一些会报读取失败。如果你用的训练框架不支持空标注图参与训练那就需要单独建一个“负样本图片列表”在训练配置里通过参数指定。4.5 边界问题四categories无序导致的类别索引错位这个问题最隐蔽。COCO的categories数组顺序不一定是按id从小到大排的如果你的转换脚本用数组下标而不是id字段类别就全乱了。比如categories数组顺序是[火焰, 烟火, 烟雾]实际id可能是[2, 1, 3]用枚举下标转换后火焰会被当成类别0跟训练配置里的类别顺序对不上。我的做法是在转换前先打印出完整的categories数组确认id和name的对应关系再显式定义一个类别映射字典而不是依赖cat_id - 1这种打哪儿指哪儿的写法。如果是多类别数据集这个映射表应该单独保存成一个JSON文件后续做评估和推理时也用它保证全流程类别语义一致。5. 常见问题与避坑训练火灾检测模型时最容易翻车的五个点5.1 现象验证集指标很漂亮现场却把白色窗帘当烟雾验证集mAP到了0.85以上看起来效果不错部署到现场后晴天里的白色外墙、窗帘、汽车尾气全被判成烟雾。原因是训练数据里烟雾样本集中在灰黑色工业烟和室内烟场景没覆盖白色烟雾类型。解决这个问题需要从数据层面补充“白色烟雾”和“类烟雾干扰物”的负样本把白色墙体包进背景类里反向训练而不是单纯增加烟雾正样本。我一般会从训练集中抽出20%的烟雾图手动把干扰区域标注成背景类强迫模型学到烟雾的纹理特征而非颜色特征。5.2 现象大面积扩散烟雾拉出接近全图的框极大目标拖慢收敛这种现象在室外火灾场景里很常见烟气铺开之后覆盖半片天空标注框几乎等于整张图。大目标框参与训练时IOU计算消耗大且在损失函数里的梯度占比过高模型会把大量能力花在拟合“全图都是烟雾”这个极端样本上。解决思路有两个方向一是训练时对面积占比超过50%的框做随机裁剪让模型看到烟雾的局部特征二是对这类样本设置独立的数据增强管线比如先按框的中心点裁剪出区域再做Mosaic增强。批次里大目标样本比例建议控制在10%以内超过就做下采样。5.3 现象夜间样本漏检率明显高于白天火焰在夜间的光学特征和白天差异很大同一处火苗夜间因为背景暗边缘反而清晰但小目标会显得特别亮。如果训练集里夜间图片占比不到5%模型基本学不到夜间的亮度分布特征。数据层面最直接的办法是按时间维度切分数据把夜间图片单独抽出来做一次小批量重复训练或者给夜间图做亮度抖动增强。注意不要把白天样本直接转成灰度图来模拟夜间这种伪夜间数据会让模型学到错误的纹理信息。5.4 现象烟火类实例数远少于烟雾类训练结果明显偏向烟雾烟火类定义的是火源被烟包裹的过渡状态这种状态本身在真实场景里持续时间短所以实例数天然少。如果烟火类只有烟雾类的五分之一训练出来的模型会把烟火框全部判成烟雾导致早期火情识别失效。这种情况要优先做类别重加权给烟火类更高的损失权重同时用小目标的复制粘贴增强拉高实例数量。我习惯先把权重系数设成两类实例数的倒数比再根据验证结果做微调避免权重过大导致模型过拟合到烟火样本。5.5 现象同一场景的多帧连续图像同时出现在train和val这个问题在拆数据时最容易踩。火灾事件的连续帧之间背景几乎没变只有火苗和烟雾形态在微调如果连续帧被随机切分train里某几帧和val里某几帧实际是同一个场景。验证集指标会虚高大约5到8个点等模型部署到新场景时才发现泛化能力不足。正确做法是先用文件名前缀或者感知哈希做聚类把同一事件的所有帧归入同一集合再做场景级别的划分。这一步虽然麻烦但对评估结果的可靠性影响极大。6. 数据质检技巧训练前用坐标自洽性检查给数据集做体检除了肉眼抽查和统计分布我每次拿到新的检测数据集都会先跑一遍“坐标自洽性检查”。这个检查不依赖任何外部标注纯粹用数据集内部的字段关系判断标注是否合规能挡掉大部分低级错误。import json import numpy as np with open(fire_dataset/coco_annotations/instances_train.json, r, encodingutf-8) as f: coco json.load(f) img_id2wh {img[id]: (img[width], img[height]) for img in coco[images]} errors [] aspect_ratios {cat[name]: [] for cat in coco[categories]} for ann in coco[annotations]: x, y, w, h ann[bbox] img_w, img_h img_id2wh[ann[image_id]] # 检查1: 坐标不能为负宽高必须为正 if x 0 or y 0 or w 0 or h 0: errors.append(f非法坐标: {ann[image_id]} bbox{ann[bbox]}) # 检查2: bbox不能超出图片范围 if x w img_w 1 or y h img_h 1: errors.append(f越界框: {ann[image_id]} bbox{ann[bbox]} 图像尺寸{img_w}x{img_h}) # 检查3: 过滤掉极端长宽比火焰框比例一般不会超过5:1 cat_name [c[name] for c in coco[categories] if c[id] ann[category_id]][0] aspect w / h if h 0 else 0 aspect_ratios[cat_name].append(aspect) if aspect 10 or aspect 0.1: errors.append(f异常长宽比: {ann[image_id]} bbox{ann[bbox]} aspect{aspect:.2f}) print(f发现 {len(errors)} 个异常标注:) for e in errors[:20]: print( , e) for cat_name, ratios in aspect_ratios.items(): if ratios: print(f类别 {cat_name}: 长宽比中位数{np.median(ratios):.2f}, fP95{np.percentile(ratios, 95):.2f})这段代码的逻辑分三层。第一层检查坐标数值合法性负坐标和零宽高通常来自标注工具的删除操作没同步干净第二层检查越界个别越界几个像素可以容忍超过10%就要回到原图看标注第三层检查长宽比正常情况下火焰的形态不会特别细长如果大量框的长宽比超过10大概率是标注时把背景边缘拉进去了。这套检查跑完我会再单独打印每个类别的长宽比P95和面积分布中位数如果火焰类的面积中位数和烟雾类差两个数量级就说明小目标占比偏高后面的训练策略要跟着调。从那以后我每次拿到火灾检测数据第一件事不再是直接扔进YOLO而是先跑一遍坐标统计加上人工抽查200张图宁可多花两小时做数据体检也不要让模型训练一周之后才发现标注是歪的。这套方法你套用到自己的数据上能少走很多弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表