烟火检测数据集实战:从1000张标注图到可用的YOLO模型 简介面向烟火检测与公共安全目标检测任务的数据集适合用YOLO系列训练或评估烟火识别模型的开发者。压缩包内共2000个文件含1000张JPEG原始图像和1000个XML标注文件整体约89.87MB。所有图片均已人工标注边界框标出烟火目标的位置与类别目录按JPEGImages、Annotations组织前者存放原图后者存放对应标注便于直接划分训练集、验证集与测试集也方便转换成VOC或COCO格式后接入不同框架。图像覆盖多种光照、背景和烟火形态可提升模型在复杂场景下的泛化能力标注信息记录目标坐标与类别配合YOLOv3、YOLOv5等模型可完成端到端的检测训练与效果对比。该数据集同时适合入门者理解目标检测标注流程也适合有实际烟火识别需求的工程师快速建立安全监控、森林防火等场景的检测基线。目前已有3246人学习下载作为已标注数据基础能省去大量人工标注成本。1. 烟火检测数据集到手后先想清楚1000张标注图离可用模型还差几步烟火检测数据集11000IMG已标注.zip这个压缩包信息量其实都写在名字里一份标注、1000张图片、已经标注好。拿到这类数据的第一反应通常是“1000张够不够”我的答案是直接训练肯定不够但拿来建基线、验证模型选型、做迁移学习微调完全够用。烟火检测是个老需求森林防火、化工园区、城市消防通道、工地仓库都会用到目标特点是颜色强烈、形状松散、尺度跨度极大——火苗可能只占几十个像素烟雾能铺满半块屏幕。适合看这篇内容的人是手里拿到标注数据、打算用它跑通目标检测全流程的工程师。下面按我自己的惯用流程从拆包装到训练、排坑把这条路捋清楚。2. 拆开 zip 先做数据体检目录结构、标注格式与坐标合法性校验压缩包解压后别急着找训练脚本第一件事是认清目录结构确认这1000张图到底能不能直接喂给目标检测训练器。烟火检测数据集的打包方式通常沿袭通用目标检测数据集的习惯但不同来源的标注风格差别很大有的给VOC格式的xml有的给YOLO格式的txt还有的干脆图片和标注混在一个目录里。这一步做扎实后面能省出大把排错时间。2.1 解压后先认出目录结构三种常见布局判断目录结构最快的方法是解压后用命令行把目录层级打出来再按标注文件后缀定位格式。我在拿到压缩包后一般先跑下面这组命令把家底摸清楚再动手。unzip 烟火检测数据集11000IMG已标注.zip -d ./smoke_fire_raw cd ./smoke_fire_raw find . -maxdepth 2 -type d | sort find . -name *.xml | wc -l find . -name *.txt | wc -l find . \( -name *.jpg -o -name *.jpeg -o -name *.png \) | wc -l第一条find看目录层级后面三条分别统计xml标注、txt标注和图片的数量。1000张图配1000个标注文件是理想状态数量对不上就先停下来排查别带着数量缺口往下走。这里的常见布局基本是三类VOC风格的JPEGImages加AnnotationsYOLO风格的images加labels以及所有文件混排的单一目录。布局风格典型目录标注文件主要问题VOCJPEGImages/ Annotations/.xml信息全但训练器不能直接用YOLOimages/ labels/.txt每行一个框编号加归一化坐标混排图片与标注放同一目录.xml或.txt文件名必须一一对应少一个就埋雷txt标注是最省事的确认一下每行的五个数字格式就行。xml标注则需要走一遍转换转换脚本放在2.3节。混排目录最麻烦图片和标注同名但后缀不同靠文件名关联任何一张图缺标注都会在训练时被静默跳过。提示解压前先du -sh看下压缩包和磁盘剩余空间避免解压到一半报磁盘满。2.2 标注一致性校验坐标越界、类别编号与图片可读性确认标注格式后用脚本给数据做个全面体检。1000张图不多但边框越界、类别编号写错、图片文件损坏这类问题藏得很深肉眼抽查基本看不出来。烟火数据集的标注里有个典型问题烟和火的框大量重叠类别名一会儿是smoke一会儿是fire坐标导出时有的框超出图像边界。我一般先跑一段Python脚本把三类问题一次性筛出来。import os from pathlib import Path from PIL import Image root Path(./smoke_fire_raw) images_dir root / images # 按实际目录调整 labels_dir root / labels report {missing_label: [], unreadable_img: [], bad_line: []} for img_path in sorted(images_dir.glob(*.jpg)): label_path labels_dir / (img_path.stem .txt) if not label_path.exists(): report[missing_label].append(str(img_path)) continue try: with Image.open(img_path) as im: w, h im.size except Exception: report[unreadable_img].append(str(img_path)) continue for line in label_path.read_text().splitlines(): parts line.split() if len(parts) ! 5: report[bad_line].append(f{label_path}: {line}) continue cls int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) if not (0 xc 1 and 0 yc 1 and bw 0 and bh 0): report[bad_line].append(f{label_path}: {line}) for k, v in report.items(): print(f{k}: {len(v)} 个) for item in v[:10]: print( , item)这个脚本先检查图片是否能被PIL正常打开再检查每行标注的坐标是否在归一化范围内。中心坐标落在0到1之外、宽高为负值都属于异常数据。逻辑上分三步缺标注的图进missing_label打不开的图片进unreadable_img格式错误或者坐标越界的行进bad_line。如果bad_line超过几十条不要指望训练器自己扛YOLO在计算损失时会过滤掉无效框最后的结果就是训练日志正常但mAP和数据集的实际标注质量对不上。2.3 VOC XML 转 YOLO TXT换算脚本与边界处理如果压缩包解出来是VOC风格的xml训练前必须先转成YOLO的txt。换算逻辑本身不复杂xml里存的是左上角和右下角的绝对像素坐标YOLO需要的是中心坐标加宽高并除以图像宽高做归一化。真正容易翻车的是边界情况坐标从右边界外开始算、一个xml里挂多个object、类别名不在映射表里。转换脚本我习惯写成带容错处理的版本。import xml.etree.ElementTree as ET from pathlib import Path CLASS_MAP {smoke: 0, fire: 1} # 按数据集实际类别名修改 def voc_to_yolo(xml_path: Path, out_dir: Path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w float(size.find(width).text) h float(size.find(height).text) out_lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue box obj.find(bndbox) xmin max(float(box.find(xmin).text), 0) ymin max(float(box.find(ymin).text), 0) xmax min(float(box.find(xmax).text), w) ymax min(float(box.find(ymax).text), h) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2 / w cy (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h out_lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path out_dir / (xml_path.stem .txt) out_path.write_text(\n.join(out_lines) \n)脚本里的CLASS_MAP是类别名到编号的映射表写之前先打开一个xml文件确认类别名到底叫smoke和fire还是别的写法确认后再填。坐标裁剪用了max和min钳制到图像范围内xmax小于等于xmin的无效框直接跳过。转换完成后抽查几个txt对比源xml的object数量和输出行数是否一致这是检验转换有没有丢框的最直接方法。xml和txt的关联靠同名转换输出目录建议单独建一个labels文件夹不要和原图混在一起。3. 组装训练管线从标注格式转换到 YOLO 第一条训练命令数据体检通过、标注格式统一成txt之后下一步是把数据集按目标检测训练器的目录约定重组好写出配置文件再跑第一条训练命令。这一章完成后你应该能拿到一个训练过程正常、指标曲线稳定下降的基线模型。很多人在这步卡住不是模型选型有问题而是目录结构不合规训练器连数据都读不到。3.1 按 YOLO 规范重组目录images 和 labels 两棵树分开主流目标检测训练器对数据集目录有硬性约定图片放在images/train和images/val标注放在labels/train和labels/val两者目录树镜像对应。标注文件跟图片放在同一目录里直接开训大概率报no labels found。重组目录时有三个细节值得注意用复制而不是移动原始数据保留一份后续清洗能反悔图片和标注必须成对搬运丢一个就废一张目录名里不要出现空格和中文不同环境之间切换容易出问题。mkdir -p datasets/smoke_fire/{images/{train,val},labels/{train,val}} # 先复制前 800 张进 train后 200 张进 val # 注意这只是演示目录结构真实拆分前必须先洗牌见 3.2 节 for f in images/*.jpg; do name$(basename $f) if [ -f labels/${name%.jpg}.txt ]; then cp $f datasets/smoke_fire/images/train/$name cp labels/${name%.jpg}.txt datasets/smoke_fire/labels/train/${name%.jpg}.txt fi done这段脚本按文件名顺序复制到train目录条件判断保证只有标注存在的图片才会进训练集。文件名里的后缀名通过参数展开方式替换jpg对应txt。但按顺序拆分有个隐患如果这1000张图是从视频里抽帧得到的连续帧之间高度相似顺序拆分会把高度相关的图片同时送进训练集和验证集得到虚高的验证指标。正确做法是先随机洗牌再拆这个放在下一节解。3.2 训练集与验证集拆分比例、随机种子与数据泄漏1000张的量级我习惯按8:2拆训练集800张验证集200张。验证集再少mAP的波动就会变得很大单轮评估结果很难判断模型到底变好还是变坏。拆分脚本里必须固定随机种子这样才能保证每次跑出来的训练验证划分一致实验才有对比意义。import random import shutil from pathlib import Path random.seed(0) src_img Path(images) src_lbl Path(labels) dst Path(datasets/smoke_fire) train_img dst / images/train val_img dst / images/val train_lbl dst / labels/train val_lbl dst / labels/val for d in [train_img, val_img, train_lbl, val_lbl]: d.mkdir(parentsTrue, exist_okTrue) all_files sorted(src_img.glob(*.jpg)) random.shuffle(all_files) split_idx int(len(all_files) * 0.8) for i, img in enumerate(all_files): label src_lbl / (img.stem .txt) if not label.exists(): continue if i split_idx: shutil.copy(img, train_img / img.name) shutil.copy(label, train_lbl / label.name) else: shutil.copy(img, val_img / img.name) shutil.copy(label, val_lbl / label.name)random.seed(0)保证了洗牌结果可复现shuffle打乱文件顺序后再按比例切分没有标注的图片直接被跳过。如果原始数据来自视频抽帧更严格的做法是按视频片段分集而不是按帧分集——同一个视频的帧进同一个集合避免验证集里出现训练集的“孪生兄弟”。拆分完成后dataset目录下应该出现四个子目录数量大致是800和200。接下来写data.yaml配置文件。path: datasets/smoke_fire train: images/train val: images/val nc: 2 names: 0: smoke 1: firedata.yaml里的path是相对路径基准是yaml文件所在目录train和val填相对于path的路径。nc是类别数names的编号必须和txt标注文件第一列的数字一致。烟火检测里很常见的一个错误是训练时smoke0、fire1推理时又用fire0、smoke1最后指标和预测结果对不上排查半天发现是类别序搞反了。3.3 第一条训练命令预训练权重、批次与 epoch 设定目录和配置就位后第一条训练命令我建议用最小配置先把流程跑通。模型选nano级别1000张数据量配nano规模正合适s以上的模型在这个数据量下容易过拟合训练时间也成倍上涨。batch设为16对应大约8到10GB显存显卡只有6GB的话降到8或者开混合精度。yolo detect train \ modelyolov8n.pt \ datadatasets/smoke_fire/data.yaml \ epochs100 \ imgsz640 \ batch16 \ workers4modelyolov8n.pt表示加载预训练权重做迁移学习data指定yaml配置文件epochs100是基线值后面根据验证曲线再决定加不加不要一上来就跑300个epoch。训练起来后盯三个地方loss曲线是否下降、验证集mAP是否每轮在涨、显卡利用率是否在90%左右。前两个验证数据方向对不对第三个验证环境配置有没有瓶颈。如果loss降得很慢优先排查学习率而不是加数据。4. 烟火场景训练必调的三个参数类别权重、数据增强与输入尺寸烟火检测和通用目标检测的差异在训练参数上体现得很明显。烟火目标的视觉特征集中在颜色和亮度上形状又极其松散烟雾边缘是渐变的火焰形态不断变化。这三个特性决定了类别权重、数据增强和输入尺寸都需要针对场景做调整不能拿默认参数直接开跑。4.1 类别不平衡烟多火少时先调权重再谈加数据烟火数据集的天然问题是烟雾框比火焰框多出不少尤其早期火情烟雾扩散面积大火焰只占一小块。先跑一段统计脚本拿数字说话再决定怎么处理。from pathlib import Path from collections import Counter label_dir Path(datasets/smoke_fire/labels/train) counter Counter() for txt in label_dir.glob(*.txt): for line in txt.read_text().splitlines(): parts line.split() if len(parts) 5: counter[int(parts[0])] 1 print(类别编号 - 框数量) for cls_id, num in sorted(counter.items()): print(cls_id, num)统计结果如果smoke和fire的比例在3:1以内建议先不动模型在验证集上分别看两个类别的AP再做决定。比例超过3:1优先考虑调整分类损失的权重给少数类加大权重比机械复制样本更稳妥。常见误区是一看到类别不平衡就疯狂复制fire样本结果同一个框在训练集里出现好几遍模型对那几张图过拟合验证mAP反而往下掉。记住一个原则先调权重权重压不住再加数据。4.2 数据增强参数mosaic 与亮度扰动在烟火检测上的取舍YOLO默认的增强参数在自然场景数据集上调过但烟火有特殊性。mosaic把四张图拼在一起小块火苗容易被拼接边界切掉或者缩到太小建议从默认1.0降到0.8甚至0.5。HSV增强里火焰颜色分布在红橙区间色调扰动hsv_h不能给太大0.015是比较稳的值给大了模型学到的颜色规律会被打乱。亮度扰动hsv_v对烟火检测有正向意义因为白天和夜里的火焰亮度差异很大给到0.3能让模型见更多亮度变化。yolo detect train \ modelyolov8n.pt \ datadatasets/smoke_fire/data.yaml \ epochs100 \ imgsz640 \ batch16 \ mosaic0.8 \ hsv_h0.015 \ hsv_v0.3 \ fliplr0.5 \ flipud0.0命令行里的增强参数直接覆盖默认值。mosaic降到0.8保留拼接增强的大部分收益又不会频繁切断小目标fliplr水平翻转可以开因为部署摄像头通常水平安装flipud垂直翻转不要开烟火检测场景里没有倒装摄像头垂直翻转学出来的特征在实拍中没有正向帮助。这些参数是给烟火场景定的一套起点不是最终答案跑完一个完整训练周期后用验证集的各类AP来反向调整。4.3 输入尺寸与锚框小目标烟火为什么值得上 640 以上烟火目标的尺度是数据集里最棘手的问题。早期火苗在1080p监控画面里只有几十个像素烟雾又能铺满全屏。YOLO的下采样倍率决定了小目标能分到哪些特征图640输入尺寸下负责小目标的检测头是80x80一个边长8像素的火焰经过32倍下采样后在深层特征图上已经不足1个像素靠浅层特征图勉强兜住。如果验证时发现小目标召回明显低于大目标把imgsz提到768或896是值得的代价是显存和训练时间同步上涨。输入尺寸小目标召回倾向显存代价训练时间适合情况640中等低快通用基线、部署设备性能有限768较高中中画面里小块火苗占比高896高高慢1080p原图且小目标密集锚框这块不要一上来就跑auto anchor重新聚类。1000张图里的目标尺度分布不一定有代表性聚出来的锚框反而可能偏。先拿默认锚框跑一版训练完看验证集预测框的尺寸分布如果和标注框尺寸分布差得远再考虑重新聚类。锚框在烟火检测里属于锦上添花输入尺寸的影响要直接得多别在锚框上玩玄学。5. 烟火检测落地避坑五个常见翻车点与对应排查思路训练跑通只是开始烟火检测真正耗时间的是训练完之后的落地验证。这一章汇总我在多个烟火项目里踩过的五个典型坑按现象、原因、解决的顺序写清楚每条都可以直接拿来对照排查。这些坑分布在从训练启动到实拍验证的全流程里任何一个都能让前面的工作白做。5.1 no labels found解压后直接训练最常见的失败现象训练脚本启动后日志里出现警告提示labels为空或者训练集和验证集的样本数显示为0训练进程直接退出。原因目录结构不符合训练器的约定是最常见的labels和images没有分开放或者标注文件还是xml没有转成txt。另一个容易被忽略的原因是图片文件名和标注文件名后缀不一致img_001.jpg对应img_001.txt没有问题但如果一个是jpg另一个是JPG在Linux环境下会匹配失败。解决先用两条命令核对数量是否一致。图片和txt数量相等后再抽查文件名前缀是否完全一致。还有一种情况是txt文件里全是空行训练器同样认为没有标签打开几个txt文件确认内容非空。这个坑几乎是所有数据集的共性问题不是烟火场景特有。find datasets/smoke_fire/images -name *.jpg | wc -l find datasets/smoke_fire/labels -name *.txt | wc -l5.2 mAP 虚高验证集和训练集同源现象训练完验证集mAP0.5上到0.85一拿到现场实拍误报漏报满天飞模型表现和验证指标严重不匹配。原因很多烟火数据集是从监控视频里抽帧得到的抽帧间隔不够长时前后帧画面几乎一样。拆分训练集和验证集时没洗牌或者随机种子没固定验证集里每张图都能在训练集找到近似的“孪生兄弟”模型相当于开卷考试。解决确保拆分前先shuffle再切分随机种子固定。更严格的做法是按视频片段分集同一个视频的帧全部进入训练集或全部进入验证集让验证集真正做到“没见过的场景”。判断是否存在数据泄漏有个经验值训练集mAP和验证集mAP差距小于3个点基本可以怀疑验证集和训练集太像了。5.3 白天火焰漏检模型学的是“暗背景上的亮区”现象夜晚场景的火焰召回率很高白天阳光下的火焰即使面积不小也经常漏检大晴天尤其严重。原因烟火数据集中夜晚样本占比通常偏高夜晚火焰的特征是亮块加暗背景模型学到这个捷径后白天强光下火焰和背景的亮度差很小特征没有触发。解决优先给训练集补充白天样本让模型见更多亮度变化其次调大HSV增强里的hsv_v亮度扰动。如果部署场景允许加上红外或热成像通道白天漏检问题基本能压下去。注意别在白天样本不足时硬调亮度增强把颜色信息带歪了反而影响夜晚的检测效果。5.4 标注框把烟雾连背景框进去Loss 降不下去的隐藏原因现象训练loss能降但降到某个值就卡住不再变化验证集PR曲线尾部拖着长尾巴NMS输出的框边缘模糊不清。原因烟雾是半透明的标注员画框时很容易画得很大把树冠、天空、建筑一起包进去。框里背景占比太高监督信号被稀释模型学到的是一片包含烟雾的大方块而不是烟雾本身的边界。解决统一标注规范框紧贴目标主体不要包背景。对已标注数据做一次清洗把宽高比超过3的框、面积特别夸张的框抽出来人工复核。清洗脚本可以用Python写按框面积与目标面积比值排序输出优先处理最大的几个异常框。这个坑在烟雾检测里尤其常见因为烟雾的视觉边界天生模糊标注员很难把握尺度。5.5 火烧云与车尾灯误报模型学的是颜色而不是形状现象实拍时黄昏的火烧云被报成fire红色车尾灯、红色工地围挡频繁触发报警误报率压不下来。原因训练集里全是“看起来像火”的正样本缺少“看起来像火但不是火”的难负样本模型退化成颜色分类器学的是橙红色高亮区域而不是火焰的纹理和形状特征。解决把现场误报的截图收集起来挑出几百张作为负样本加入训练。常见做法是给数据集加一个background类别或者用没有标注的图片作为背景图参与训练。误报压降效果非常明显甚至超过调整任何训练参数。收集误报时注意多样性不同时段的火烧云、不同角度的车尾灯都要覆盖否则模型换个场景继续误报。6. 把 1000 张数据的上限再顶一截小目标烟火的两阶段检测技巧1000张数据训出来的模型直接整图推理时小目标漏检往往是最扎心的。我第一次做烟火项目时就吃过这个亏1200万像素的广角画面里几十像素的火苗在整图缩放后几乎消失模型根本没有机会识别。后来我用滑窗推理解决了这个问题把大图切成小块逐一检测再映射回原图坐标。滑窗推理的思路不复杂推理时把整图切成640x640的块每个块单独过模型检测框坐标加上切块的偏移量映射回原图相邻块之间保留25%重叠防止目标正好卡在切块边界上。阈值从conf0.25起步看误报数量再往上调滑窗模式下同一个目标可能在相邻两个块里被重复检测最后按IoU0.5做一次非极大抑制合并。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) def slide_predict(img, conf0.25, tile640, overlap0.25): h, w img.shape[:2] step int(tile * (1 - overlap)) boxes [] for y0 in range(0, max(h - tile, 0) 1, step): for x0 in range(0, max(w - tile, 0) 1, step): crop img[y0:y0 tile, x0:x0 tile] res model.predict(crop, confconf, verboseFalse)[0] for box in res.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() boxes.append([x0 x1, y0 y1, x0 x2, y0 y2, float(box.conf[0]), int(box.cls[0])]) return boxes代码里的max(h - tile, 0) 1处理了图片尺寸小于tile的边界情况保证至少切一次。图片边缘没被覆盖的区域会漏检稳妥做法是先把图padding到tile的整数倍再推理完事后裁掉padding区域。部署端要注意滑窗让单帧推理耗时成倍上涨对秒级响应的安防场景没有问题但对实时视频流要做性能评估。我习惯把滑窗逻辑写成独立的推理函数训练和部署解耦模型更新不影响推理服务。这个技巧对我而言是把1000张标注数据的检测能力再往上顶一截的最有效手段希望帮到你。本文还有配套的精品资源点击获取