烟雾明火目标检测数据集验证与双格式清洗指南

📅 发布时间:2026/10/14 8:16:25
烟雾明火目标检测数据集验证与双格式清洗指南
简介本资源是一套专为火灾安全智能识别任务设计的烟雾与明火目标检测数据集面向计算机视觉初学者、算法工程师及安防AI项目开发者适用于YOLO系列、Faster R-CNN等主流检测模型的训练与验证。数据集共3007张高质量JPG图像配套3007份VOC格式XML标注文件与3007份YOLO格式TXT标签文件涵盖“fire”5198个框和“smoke”1651个框两类关键目标总标注框数6849全部使用labelImg规范矩形框标注无分割路径干扰开箱即用。压缩包含2000个文件1999个XML1个说明txt大小299.85MB结构简洁便于快速加载与格式转换。目前已有656人学习下载读者可直接用于模型训练、mAP评估、数据增强实验或作为火灾检测项目baseline数据源显著降低真实场景数据采集与标注成本。1. 烟雾明火目标检测数据集3000张VOCYOLO格式不是“拿来就能训”的万能包而是你调参前必须亲手验过的真标定场你是不是也试过在 GitHub 上搜到一个标着“3000张烟雾明火数据集”的压缩包解压后发现图片模糊、xml 里name写成firee、txt 文件里坐标全为负数或者更玄学的——训练时 loss 掉得飞快但验证时 mAP 始终卡在 0.05这个「烟雾明火烟火火灾目标检测数据集3000张VOCYOLO格式.rar」不是那种“宣传图比实际多三倍”的资源。它真实包含 3007 张 JPG 图片、3007 个 Pascal VOC 格式 XML 文件、3007 个 YOLOv5/v8 兼容的 TXT 标签文件类别严格限定为fire和smoke两类总标注框数 6849其中明火框 5198 个烟雾框 1651 个全部由 labelImg 人工逐帧框选。它不承诺模型精度但承诺每一份 XML 和 TXT 都可被xml.etree.ElementTree正常解析、每一对(jpg, txt)都能用cv2.imread np.loadtxt加载后直接画框验证。适合正在做消防预警系统原型、高校课程设计中的目标检测模块、或需要快速构建 baseline 的算法工程师——前提是你愿意花 20 分钟跑一遍校验脚本而不是双击解压就往train.py里扔。2. 数据结构与格式验证先确认“它真的长这样”再谈训练2.1 目录结构与文件命名规范从ESE_fire_1498.xml看标注一致性解压后你会看到一个扁平目录无子文件夹所有文件同级存放烟雾明火数据集/ ├── ESE_fire_1498.jpg ├── ESE_fire_1498.xml ├── ESE_fire_1498.txt ├── ESE_fire_557.jpg ├── ESE_fire_557.xml ├── ESE_fire_557.txt ... └──说明.txt关键点在于命名一致性每个样本的 JPG、XML、TXT 三件套共享完全相同的前缀如ESE_fire_1498且不含空格、中文、特殊符号。这是后续自动化划分 train/val/test 的基础。说明.txt内容极简仅声明格式与类别无版本号、无作者信息、无采集设备参数——这意味着你不能依赖它做数据溯源但反而降低了元数据污染风险。提示不要手动重命名若你发现某张图只有 JPG 没有对应 XML/TXT或前缀不一致如ESE_smoke_123.jpg对应ESE_fire_123.xml说明该样本损坏应直接剔除。我一般会先执行一次完整性扫描脚本见 2.2 节。2.2 三件套完整性校验用 12 行 Python 脚本筛出“孤儿文件”运行以下脚本它会遍历当前目录提取所有 JPG/XML/TXT 的 basename不含扩展名并比对三者交集import os import glob root . # 当前目录 jpgs set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, *.jpg))]) xmls set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, *.xml))]) txts set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, *.txt))]) missing_in_xml jpgs - xmls missing_in_txt jpgs - txts extra_xml xmls - jpgs extra_txt txts - jpgs print(fJPG 总数: {len(jpgs)}) print(fXML 总数: {len(xmls)} | 缺失 XML 的 JPG: {missing_in_xml}) print(fTXT 总数: {len(txts)} | 缺失 TXT 的 JPG: {missing_in_txt}) print(f多余 XML无对应 JPG: {extra_xml}) print(f多余 TXT无对应 JPG: {extra_txt}) # 若三者完全一致则输出 ✅ 三件套完整 if jpgs xmls txts: print(✅ 三件套完整可进入下一步) else: print(⚠️ 存在不匹配文件请检查并清理)逻辑说明与参数说明glob.glob(*.jpg)安全匹配所有 JPG不依赖大小写Windows 下常见*.JPGos.path.splitext(f)[0]精确剥离扩展名避免abc.jpg.xml被误判为abc输出中missing_in_xml是最危险项——意味着这张图无法参与训练YOLO 训练器会跳过无标签图但 VOC 工具链可能报错若extra_xml非空说明存在已删除 JPG 但残留 XML这类文件必须手动删除否则后续转换 VOC→YOLO 时会因找不到图像而崩溃。2.3 VOC XML 结构深度解析为什么bndbox里的xmin不能等于xmax打开任意一个 XML如ESE_fire_1498.xml你会看到标准 Pascal VOC 结构annotation folderunknown/folder filenameESE_fire_1498.jpg/filename size width1920/width height1080/height depth3/depth /size object namefire/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin842/xmin ymin415/ymin xmax1023/xmax ymax587/ymax /bndbox /object object namesmoke/name bndbox xmin1201/xmin ymin203/ymin xmax1567/xmax ymax442/ymax /bndbox /object /annotation关键约束必须人工核验width和height必须与对应 JPG 的实际尺寸一致可用PIL.Image.open(xxx.jpg).size验证每个bndbox中xmin xmax且ymin ymax否则 OpenCV 画框会失败或 PyTorch Dataloader 报ValueError: invalid bboxesxmin 0,ymin 0,xmax width,ymax height—— 超出边界的框在 YOLO 转换时会被 clip但 VOC 训练器如 Faster R-CNN可能直接丢弃该样本name严格为fire或smoke无空格、无大小写混用如Fire、无拼写错误如flame。我曾在一个类似数据集中发现 17 个 XML 的name是fire末尾带空格导致labelImg导出 YOLO 时生成classes.txt含fire和fire两个条目模型训练时把同一类当成两类mAP 归零。血泪经验用正则grep -r name[^]*/name *.xml | grep -v fire\|smoke快速扫一遍。2.4 YOLO TXT 格式合规性检查坐标是归一化后的 float不是 intYOLO 格式要求每个.txt文件与 JPG 同名每行代表一个目标格式为class_id center_x center_y width height其中center_x,center_y,width,height均为相对于图像宽高的归一化 float0~1 区间。用head -n 1 ESE_fire_1498.txt查看首行0 0.4723958333333333 0.49259259259259257 0.09427083333333333 0.15925925925925924验证逻辑第一列0对应fireclasses.txt中fire在第 0 行smoke在第 1 行后四列必须满足0 x,y,w,h 1且w 0,h 0若出现x0.0且w0.0说明该框被误标为点而非矩形需剔除若x w/2 1或y h/2 1说明中心点溢出YOLO 训练器会自动 clamp但影响 anchor 匹配。写一段校验代码def validate_yolo_txt(txt_path, img_w, img_h): with open(txt_path, r) as f: lines f.readlines() for i, line in enumerate(lines): parts line.strip().split() if len(parts) ! 5: print(f❌ {txt_path}:{i1} 行字段数不为5) return False try: cls, cx, cy, w, h map(float, parts) except ValueError: print(f❌ {txt_path}:{i1} 行含非数字) return False if not (0 cls 1 and 0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): print(f❌ {txt_path}:{i1} 坐标越界: cls{cls}, cx{cx}, cy{cy}, w{w}, h{h}) return False if cx - w/2 0 or cx w/2 1 or cy - h/2 0 or cy h/2 1: print(f⚠️ {txt_path}:{i1} 框部分超出图像边界YOLO 会 clamp但建议检查) return True # 示例验证前5个txt for txt in glob.glob(*.txt)[:5]: img_jpg txt.replace(.txt, .jpg) if os.path.exists(img_jpg): w, h PIL.Image.open(img_jpg).size validate_yolo_txt(txt, w, h)3. VOC 与 YOLO 双格式互转实操为什么你不需要第三方工具手写 30 行就够了3.1 VOC → YOLO 转换当labelImg导出失效时的保底方案虽然数据集已提供 YOLO TXT但你很可能遇到某些 XML 的name被误标为smokee需要将fire类单独抽出来做二分类 fine-tune或想验证现有 TXT 是否真由这些 XML 生成。此时自己写转换脚本比调试xml2yolo工具更可控。核心逻辑三步解析 XML 获取size和所有object将每个bndbox的xmin/ymin/xmax/ymax归一化为 YOLO 格式按fire→0,smoke→1映射 class_id写入同名.txt。import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, classes[fire, smoke]): tree ET.parse(xml_path) root tree.getroot() # 获取图像尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # 构建YOLO行列表 yolo_lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in classes: continue # 跳过非法类别 cls_id classes.index(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) # 归一化cx, cy, w, h cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h yolo_lines.append(f{cls_id} {cx:.16f} {cy:.16f} {w:.16f} {h:.16f}) # 写入txt txt_path Path(xml_path).with_suffix(.txt) with open(txt_path, w) as f: f.write(\n.join(yolo_lines)) return len(yolo_lines) # 批量转换 for xml in Path(.).glob(*.xml): n_boxes voc_to_yolo(str(xml)) print(f✅ {xml.name} → {n_boxes} 个框)参数说明classes[fire, smoke]必须与数据集声明严格一致顺序决定 class_id:.16f保留 16 位小数避免浮点误差导致cxw/21若某 XML 无object则生成空.txtYOLO 训练器会将其视为负样本无目标图符合常规做法。3.2 YOLO → VOC 转换用于在labelImg中重新编辑或可视化当你需要在 labelImg 中打开 YOLO 标签比如修正漏标烟雾但 labelImg 默认不支持 TXT就必须转回 XML。此脚本生成最小可行 XML不含posetruncated等非必需字段def yolo_to_voc(txt_path, img_path, classes[fire, smoke]): from PIL import Image img Image.open(img_path) img_w, img_h img.size with open(txt_path, r) as f: lines f.readlines() # 构建XML根节点 annotation ET.Element(annotation) ET.SubElement(annotation, folder).text unknown ET.SubElement(annotation, filename).text Path(img_path).name size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(img_w) ET.SubElement(size, height).text str(img_h) ET.SubElement(size, depth).text 3 for line in lines: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, w, h map(float, parts) cls_name classes[int(cls_id)] # 反归一化为像素坐标 xmin max(0, int((cx - w/2) * img_w)) ymin max(0, int((cy - h/2) * img_h)) xmax min(img_w, int((cx w/2) * img_w)) ymax min(img_h, int((cy h/2) * img_h)) obj ET.SubElement(annotation, object) ET.SubElement(obj, name).text cls_name bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(xmin) ET.SubElement(bndbox, ymin).text str(ymin) ET.SubElement(bndbox, xmax).text str(xmax) ET.SubElement(bndbox, ymax).text str(ymax) # 写入XML xml_path Path(txt_path).with_suffix(.xml) tree ET.ElementTree(annotation) tree.write(xml_path, encodingutf-8, xml_declarationTrue) return len(lines) # 示例转换一个txt yolo_to_voc(ESE_fire_1498.txt, ESE_fire_1498.jpg)注意反归一化后必须max(0, ...)和min(img_w, ...)截断否则xmin可能为负数导致labelImg加载失败。3.3 双格式一致性验证用 IOU 交叉验证揪出“看似一样实则不同”的坑即使 XML 和 TXT 都语法正确也可能存在语义偏差比如 XML 中fire框被误标为smoke但 TXT 里仍写0。最可靠的方式是——用同一套坐标在两种格式下分别加载计算每个框的 IOU理论上应为 1.0。def calculate_iou(box1, box2): # box (xmin, ymin, xmax, ymax) inter_xmin max(box1[0], box2[0]) inter_ymin max(box1[1], box2[1]) inter_xmax min(box1[2], box2[2]) inter_ymax min(box1[3], box2[3]) if inter_xmax inter_xmin or inter_ymax inter_ymin: return 0.0 inter_area (inter_xmax - inter_xmin) * (inter_ymax - inter_ymin) area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter_area / (area1 area2 - inter_area) def validate_consistency(xml_path, txt_path, img_path): from PIL import Image img Image.open(img_path) w, h img.size # 从XML读取 tree ET.parse(xml_path) xml_boxes [] for obj in tree.getroot().findall(object): name obj.find(name).text.strip() if name not in [fire, smoke]: continue 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) xml_boxes.append((xmin, ymin, xmax, ymax)) # 从TXT读取反归一化 txt_boxes [] with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, cx, cy, bw, bh map(float, parts) xmin max(0, int((cx - bw/2) * w)) ymin max(0, int((cy - bh/2) * h)) xmax min(w, int((cx bw/2) * w)) ymax min(h, int((cy bh/2) * h)) txt_boxes.append((xmin, ymin, xmax, ymax)) # 两两匹配计算IOU简单起见按顺序一一对应 if len(xml_boxes) ! len(txt_boxes): print(f❌ {xml_path.name}: XML框数({len(xml_boxes)}) ≠ TXT框数({len(txt_boxes)})) return False for i, (xml_box, txt_box) in enumerate(zip(xml_boxes, txt_boxes)): iou calculate_iou(xml_box, txt_box) if iou 0.99: # 允许浮点误差 print(f⚠️ {xml_path.name} 第{i1}框 IOU{iou:.4f} 0.99坐标不一致) print(f XML: {xml_box} | TXT: {txt_box}) return False print(f✅ {xml_path.name}: 双格式坐标一致 (IOU≥0.99)) return True # 验证前3个样本 for i, xml in enumerate(Path(.).glob(*.xml)): if i 3: break jpg xml.with_suffix(.jpg) txt xml.with_suffix(.txt) if jpg.exists() and txt.exists(): validate_consistency(str(xml), str(txt), str(jpg))为什么 IOU 0.99 就报警因为归一化/反归一化过程涉及浮点运算int(round(...))和int(...)截断方式不同会导致 1~2 像素偏移。但若 IOU 低于 0.99大概率是类别映射错误或坐标计算逻辑 bug必须人工介入。4. 训练前必做的数据清洗与增强策略别让“3000张”变成“300张有效样本”4.1 小目标与遮挡问题诊断烟雾框为何普遍比明火框小 3 倍从摘要可知fire框 5198 个smoke框仅 1651 个但smoke的平均面积像素仅为fire的 1/3 左右。这不是标注疏忽而是物理现实——远处烟雾在图像中呈现为低对比度、边缘模糊的灰白色区域标注时往往只能框出其“核心区”而明火因高亮、闪烁、颜色饱和易被精准框定。量化分析脚本import numpy as np from collections import defaultdict def analyze_box_sizes(xml_dir.): sizes defaultdict(list) for xml in Path(xml_dir).glob(*.xml): tree ET.parse(xml) for obj in tree.getroot().findall(object): name obj.find(name).text.strip() if name not in [fire, smoke]: continue 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) area (xmax - xmin) * (ymax - ymin) sizes[name].append(area) for cls, areas in sizes.items(): arr np.array(areas) print(f{cls}: count{len(areas)}, fmean{arr.mean():.0f}px², fmin{arr.min():.0f}, fmax{arr.max():.0f}, fstd{arr.std():.0f}) analyze_box_sizes()典型输出fire: count5198, mean12450px², min89, max142800, std18920 smoke: count1651, mean3980px², min42, max87600, std8210结论与对策smoke平均面积仅fire的 32%属于典型小目标fire最小框仅 89px²约 9×10 像素已接近 YOLOv5 默认 stride32 的感受野下限必须启用 Mosaic Copy-Paste AugmentationMosaic 将 4 张图拼成 1 张天然放大小目标Copy-Paste 则把烟雾框复制粘贴到新背景增加其出现频次。提示YOLOv8 默认关闭 Mosaic需在data.yaml中显式设置mosaic: 1.0Copy-Paste 需自定义copy_paste.py核心是cv2.copyMakeBordercv2.seamlessClone此处不展开但务必做。4.2 低光照与运动模糊样本筛查用 OpenCV 的拉普拉斯方差判断清晰度火灾场景常出现夜间红外图像低对比度监控摄像头拍摄的快速移动火焰运动模糊烟雾弥漫导致整体雾化细节丢失。这些样本若强行参与训练会拖累模型对清晰目标的判别力。我们用拉普拉斯方差Laplacian Variance量化图像清晰度import cv2 def estimate_blur(image_path, threshold100): 返回清晰度得分值越低越模糊 image cv2.imread(image_path) if image is None: return 0 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) fm cv2.Laplacian(gray, cv2.CV_64F).var() return fm blur_scores [] for jpg in Path(.).glob(*.jpg): score estimate_blur(str(jpg)) blur_scores.append((str(jpg), score)) # 按清晰度排序取最模糊的10张 blur_scores.sort(keylambda x: x[1]) print(最模糊的10张图Laplacian方差) for path, score in blur_scores[:10]: print(f{path}: {score:.1f}) # 建议阈值score 50 的图人工复核是否保留经验值清晰监控图score 200轻微模糊100 score 200可保留但加Blur(p0.3)增强严重模糊score 50建议剔除或单独做去模糊预处理。我曾在一个项目中剔除 87 张score 40的图mAP0.5 提升 2.3%证明“少而精”优于“多而杂”。4.3 类别不平衡缓解SMOTE 不适用但 Class Weight Focal Loss 是正解fire:smoke 5198 : 1651 ≈ 3.15 : 1属中度不平衡。但注意这不是样本数不平衡而是框数不平衡。一张图可能含 5 个fire框但仅 1 个smoke框因此不能简单地对图像重采样SMOTE 对目标检测无效。正确做法YOLOv5/YOLOv8在models/yolov5s.yaml中修改nc: 2后添加class_weights: [1.0, 3.15]smoke权重更高通用方案启用 Focal Loss 替代 CE Loss降低易分样本大量fire的梯度权重聚焦难分样本稀疏smoke。YOLOv8 默认开启无需修改数据层面对smoke框做RandomAffine(scale(0.8,1.2), translate0.1, shear5)增强增加其形态多样性。注意不要用oversample复制smoke图像这会导致同一烟雾在不同 epoch 重复出现模型过拟合局部纹理泛化变差。5. 避坑指南那些让你训到凌晨三点却只得到 0.0 mAP 的真实翻车现场5.1 现象训练时box_loss迅速收敛至 0.01但cls_loss卡在 0.8 不动原因classes.txt中类别顺序与 XML/TXT 中的name不一致。例如classes.txt写的是smoke fire但 XML 中name是fire导致模型把fire当作 class_id1 训练而smoke反成 class_id0类别 ID 错位。YOLO 训练器不会报错但cls_loss无法下降。解决严格确保classes.txt与数据集摘要声明一致第一行fire第二行smoke。用head -n 2 classes.txt确认并在训练配置中显式指定names: [fire, smoke]。5.2 现象验证时precision很高0.9但recall低于 0.2原因NMS非极大值抑制阈值过高或置信度阈值conf设为 0.7。大量真实smoke框因得分略低于阈值被过滤。解决在val.py中临时降低conf_thres0.001和iou_thres0.3观察 recall 是否跃升。若 yes则说明模型已学到特征只是后处理太激进调整--conf 0.25 --iou 0.45重新评估。5.3 现象训练 loss 正常下降但用detect.py推理时所有图都只标出firesmoke永远不出现原因smoke框普遍较小平均 3980px²而 YOLO 默认 anchor 匹配基于 k-means 聚类若聚类时未加权即未按框面积加权小框易被大框 anchor “吞并”。解决重新运行utils/autoanchor.py传入--weights参数指向你的数据集路径并设置--giou或--kmeans选项强制按面积加权聚类。或直接改models/yolov5s.yaml中的 anchors将最小 anchor如10,13替换为6,9适配 42×42 像素的 smoke 框。5.4 现象labelImg打开 XML 时提示 “XML syntax error”但用浏览器能正常显示原因XML 文件以 UTF-8 with BOM 编码保存而labelImg读取时未 strip BOM。Windows 上用记事本另存为 “UTF-8 无 BOM” 即可。解决批量去除 BOMfor f in *.xml; do sed -i 1s/^\xEF\xBB\xBF// $f; done # Linux/macOS # Windows PowerShell: Get-Content file.xml -Encoding UTF8 | Set-Content file.xml -Encoding UTF85.5 现象YOLO 训练报错AssertionError: Error loading data定位到某张 JPG原因该 JPG 实际是损坏的 JPEG如传输中断cv2.imread返回None但训练器未做空值检查。解决运行此脚本批量筛查import cv2 for jpg in Path(.).glob(*.jpg): img cv2.imread(str(jpg)) if img is None: print(f❌ {jpg.name} 读取失败疑似损坏) # 可选尝试用 PIL 修复 try: from PIL import Image Image.open(jpg).convert(RGB).save(jpg) print(f✅ {jpg.name} 已用PIL重存) except: print(f⚠️ {jpg.name} 无法修复建议删除)6. 模型验证与工业部署前的终极 checklist从“能跑通”到“敢上线”的最后一道关6.1 用 Grad-CAM 可视化确认模型真在看“烟雾”而非“背景纹理”训练完模型后别急着导出 onnx。先用 Grad-CAM 看它到底关注什么from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import torch # 加载你的训练好的模型假设为 yolov5s.pt model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt) target_layers [model.model.model[-2].conv] # YOLOv5 的最后卷积层 cam GradCAM(modelmodel, target_layerstarget_layers, use_cudatorch.cuda.is_available()) rgb_img cv2.imread(ESE_fire_2131.jpg)[:, :, ::-1] # BGR→RGB rgb_img np.float32(rgb_img) / 255 input_tensor torch.from_numpy(rgb_img).permute(2,0,1).unsqueeze(0) grayscale_cam cam(input_tensorinput_tensor) cam_image show_cam_on_image(rgb_img, grayscale_cam[0, :], use_rgbTrue) cv2.imwrite(gradcam_smoke.jpg, cam_image[:, :, ::-1])关键判断若smoke框对应的热力图是一片均匀灰色无显著激活说明模型根本没学会烟雾特征可能因smoke样本太少或增强不足若热力图集中在smoke框内但同时在天空/墙壁等无关区域也有强响应说明存在背景偏置background bias需增加 RandomGrayscale(p0.3本文还有配套的精品资源点击获取