简介:本资源为面向计算机视觉算法研发与火灾检测应用的高质量烟火目标检测数据集,适用于YOLO、Faster R-CNN等主流检测模型的训练与验证,特别适合安防监控、森林防火、工业安全等场景下的烟雾与明火双类别识别任务。数据集严格遵循Pascal VOC格式,包含6460张JPG图像及对应XML标注文件,全部由labelImg人工标注,覆盖交通事故烟雾、森林火灾、建筑失火、柴木生火等典型烟雾场景,以及蜡烛、柴火、火灾明火、奥运火炬等多样化火焰形态,共2类18967个精确矩形框(smoke 7901个,fire 11066个)。资源包共2000个文件,主体为6460张图像+6460份XML标注+1份使用说明txt,总大小687.88MB,结构规范、开箱即用。目前已有2558人学习下载,可直接用于模型训练、数据增强实验、评估基准构建及课程设计项目开发。
1. 烟火检测落地难?不是模型不行,是数据先卡死:6460张VOC格式烟雾+明火双类别数据集为什么值得重制
你训过烟火检测模型吗?大概率训完在测试集上mAP还凑合,一放到真实监控视频里——漏检率飙升、误报满天飞、明火和炊烟傻傻分不清。这不是YOLOv8不行,也不是你调参不努力,而是绝大多数公开烟火数据集存在结构性缺陷:标注粒度粗(“有火”就打个框,不管是不是明火)、场景单一(全是实验室点火,没有早晚逆光、雨雾干扰、远距离小目标)、类别混淆(把蒸汽、粉尘、车尾气全塞进“烟雾”类)。而这个[202206重制版]VOC-6460张数据集,是少数真正按工业级检测需求重构的资源:它把“烟雾”和“明火”拆成两个严格定义的独立类别,每张图都经过人工复核+多轮质检,6460张全部为实拍监控截图(非合成),覆盖工厂车间、仓库通道、林区卡口、城市楼顶等7类高风险场景,且明确标注了遮挡等级(partial/none)和光照条件(day/night/dusk)。它不解决所有问题,但能让你跳过最耗时的数据清洗阶段,直接验证算法在真实烟火场景下的泛化边界。适合正在做消防AI、电力巡检、森林防火或智慧园区项目的工程师,尤其适合需要快速验证VOC流程、对比YOLO与Faster R-CNN在烟火小目标上差异的团队。
2. VOC格式不是摆设:从解压到验证,跑通6460张烟火数据集的最小闭环
VOC格式看似过时,但在烟火检测这种强依赖边界框精度、需兼容传统检测框架(如Faster R-CNN、SSD)的场景里,它比YOLO格式更可控——XML里能存下遮挡状态、光照标签、置信度来源等扩展字段,这些恰恰是烟火误报溯源的关键。本节带你用最简路径完成数据集本地化验证,不装任何额外工具,只靠Python标准库和OpenCV。
2.1 解压与目录结构校验:别让zip包里的隐藏文件毁掉训练
重制版数据集压缩包解压后必须呈现标准VOC2007结构,但实际下载常因打包工具差异混入__MACOSX或.DS_Store。务必先清理再验证:
# 进入解压目录后执行(Linux/macOS) find . -name "__MACOSX" -type d -exec rm -rf {} + find . -name ".DS_Store" -type f -delete # 校验核心目录是否存在且非空 ls -l Annotations/ JPEGImages/ ImageSets/Main/ # 正确输出应类似: # Annotations/: 6460个.xml文件 # JPEGImages/: 6460个.jpg文件 # ImageSets/Main/: train.txt, val.txt, trainval.txt, test.txt(各含行数总和=6460)提示:
ImageSets/Main/下的txt文件是VOC的灵魂。train.txt里每行是图片ID(不含扩展名),对应JPEGImages/xxx.jpg和Annotations/xxx.xml。若ID不匹配,后续voc2coco.py转换会静默失败——这是新手第一大坑。
2.2 XML解析实战:抽3张图看懂烟火标注的“真细节”
VOC的XML不是模板,烟火数据集的XML里藏着关键业务逻辑。用以下脚本快速抽检:
import xml.etree.ElementTree as ET import cv2 from pathlib import Path def inspect_voc_annotation(img_id: str, voc_root: str): xml_path = Path(voc_root) / "Annotations" / f"{img_id}.xml" tree = ET.parse(xml_path) root = tree.getroot() # 提取核心信息 size = root.find("size") width = int(size.find("width").text) height = int(size.find("height").text) for obj in root.findall("object"): name = obj.find("name").text # 必为 "smoke" 或 "fire" bndbox = obj.find("bndbox") xmin = int(bndbox.find("xmin").text) ymin = int(bndbox.find("ymin").text) xmax = int(bndbox.find("xmax").text) ymax = int(bndbox.find("ymax").text) # 关键:检查扩展字段(重制版特有) difficult = int(obj.find("difficult").text) # 0=易检,1=遮挡/小目标 occlusion = obj.find("occlusion") # 可能为"partial"或"none" light = obj.find("light") # 可能为"day"/"night"/"dusk" print(f"[{name}] ({xmin},{ymin})→({xmax},{ymax}) | " f"difficult={difficult} | occlusion={occlusion.text if occlusion is not None else 'N/A'} | " f"light={light.text if light is not None else 'N/A'}") # 示例:抽检前3张训练图 with open("ImageSets/Main/train.txt") as f: train_ids = [line.strip() for line in f.readlines()[:3]] for img_id in train_ids: inspect_voc_annotation(img_id, "./VOCdevkit/VOC2022")参数说明:
difficult=1表示该目标被部分遮挡(如烟雾被树枝遮挡)或尺寸<32×32像素,训练时建议加权损失;occlusion="partial"指明火被金属支架半遮挡,这类样本对NMS阈值敏感;light="night"的图像往往存在低照度噪声,需在DataLoader中启用CLAHE增强。
2.3 可视化验证:用OpenCV画框确认标注质量
仅看XML不够,必须可视化。以下脚本生成带类别标签的预览图,重点检查烟火边界是否贴合:
import cv2 import numpy as np from pathlib import Path def visualize_voc_sample(img_id: str, voc_root: str, save_dir: str = "preview"): img_path = Path(voc_root) / "JPEGImages" / f"{img_id}.jpg" xml_path = Path(voc_root) / "Annotations" / f"{img_id}.xml" img = cv2.imread(str(img_path)) tree = ET.parse(xml_path) root = tree.getroot() # 定义颜色:smoke=蓝色(255,0,0),fire=红色(0,0,255) color_map = {"smoke": (255, 0, 0), "fire": (0, 0, 255)} for obj in root.findall("object"): name = obj.find("name").text bndbox = obj.find("bndbox") xmin = int(bndbox.find("xmin").text) ymin = int(bndbox.find("ymin").text) xmax = int(bndbox.find("xmax").text) ymax = int(bndbox.find("ymax").text) # 画矩形框+文字 cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color_map[name], 2) cv2.putText(img, name, (xmin, ymin-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color_map[name], 2) # 保存预览图 Path(save_dir).mkdir(exist_ok=True) cv2.imwrite(f"{save_dir}/{img_id}_annot.jpg", img) # 批量生成前10张预览 Path("preview").mkdir(exist_ok=True) with open("ImageSets/Main/train.txt") as f: for i, img_id in enumerate(f.readlines()[:10]): visualize_voc_sample(img_id.strip(), "./VOCdevkit/VOC2022")逻辑说明:此脚本不依赖PascalVOC官方库,避免版本冲突。生成的preview/目录下每张图都带彩色框,可快速发现三类问题:①smoke框包含无关背景(如整片天空);②fire框未覆盖火焰根部(导致模型学不会定位火源);③ 多目标重叠时框体错位(重制版已修复,但需二次确认)。
3. 从VOC到YOLO:转换不是简单改格式,而是保留烟火检测的语义关键
很多团队急着转YOLO格式训练,却忽略一个事实:VOC的difficult、occlusion、light字段在YOLO txt里无法原生存储。盲目转换等于丢掉6460张图里最宝贵的业务元数据。本节提供两种转换策略——轻量级适配(保留核心)与工业级扩展(自定义字段),并给出YOLOv8训练时的参数补偿方案。
3.1 轻量级转换:用voc2yolo.py保留difficult权重
此方案适用于快速验证,将difficult=1的样本在YOLO txt中用特殊标记,并在训练时动态加权:
# voc2yolo_light.py import xml.etree.ElementTree as ET import os from pathlib import Path def convert_voc_to_yolo_light(voc_root: str, yolo_root: str, class_names: list = ["smoke", "fire"]): # 创建YOLO目录结构 for split in ["train", "val", "test"]: Path(f"{yolo_root}/images/{split}").mkdir(parents=True, exist_ok=True) Path(f"{yolo_root}/labels/{split}").mkdir(parents=True, exist_ok=True) # 读取划分文件 for split in ["train", "val", "test"]: with open(f"{voc_root}/ImageSets/Main/{split}.txt") as f: img_ids = [line.strip() for line in f] for img_id in img_ids: # 复制图片 src_img = f"{voc_root}/JPEGImages/{img_id}.jpg" dst_img = f"{yolo_root}/images/{split}/{img_id}.jpg" os.system(f"cp '{src_img}' '{dst_img}'") # 生成YOLO标签(保留difficult信息) xml_path = f"{voc_root}/Annotations/{img_id}.xml" tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) yolo_lines = [] for obj in root.findall("object"): name = obj.find("name").text cls_id = class_names.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # YOLO格式:cls_id center_x center_y width height [difficult_flag] x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h difficult = int(obj.find("difficult").text) # 在YOLO行末添加difficult标记(0或1) yolo_line = f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f} {difficult}" yolo_lines.append(yolo_line) # 写入label文件 label_path = f"{yolo_root}/labels/{split}/{img_id}.txt" with open(label_path, "w") as f: f.write("\n".join(yolo_lines)) # 执行转换 convert_voc_to_yolo_light("./VOCdevkit/VOC2022", "./yolov8_dataset")参数说明:
- 输出的
.txt每行末尾的0/1即difficult标记,YOLOv8训练时可通过自定义Loss读取; class_names顺序必须与YOLO配置文件names一致,否则类别错乱;- 此方案不处理
occlusion和light,因YOLOv8默认DataLoader不支持多维标签。
3.2 工业级扩展:用JSON Schema存储全部元数据
当需要完整保留业务字段时,放弃纯txt,采用labels/下同名JSON文件:
// labels/train/000001.json { "image_id": "000001", "width": 1920, "height": 1080, "objects": [ { "category": "fire", "bbox": [120.5, 340.2, 180.7, 420.1], "difficult": 0, "occlusion": "none", "light": "day", "confidence_source": "human_verified" } ] }注意:此方案需修改YOLOv8的
dataset.py,在__getitem__中加载JSON而非txt。虽增加开发量,但为后续加入光照自适应训练、遮挡感知NMS打下基础。
3.3 YOLOv8训练参数补偿:没有元数据时如何救场
若坚持用标准YOLOv8训练(不改源码),需通过超参补偿丢失的元数据:
| VOC字段 | YOLOv8补偿方案 | 参数值建议 | 原理 |
|---|---|---|---|
difficult=1 | loss_box_agnostic | True | 启用无类别IoU损失,提升小目标鲁棒性 |
light="night" | hsv_h,hsv_s,hsv_v增强 | 0.015, 0.7, 0.4 | 强化暗部对比度,模拟夜视效果 |
occlusion="partial" | mosaic,copy_paste | 1.0, 0.2 | 用马赛克增强+粘贴增强模拟遮挡 |
# yolov8_fire_smoke.yaml train: data: ./yolov8_dataset model: yolov8n.pt epochs: 300 batch: 16 imgsz: 640 optimizer: 'auto' lr0: 0.01 hsv_h: 0.015 # 色调扰动,增强烟雾与背景分离 hsv_s: 0.7 # 饱和度扰动,强化明火红色特征 hsv_v: 0.4 # 明度扰动,提升夜间图像细节 mosaic: 1.0 # 马赛克增强,强制模型学习局部特征 copy_paste: 0.2 # 粘贴增强,模拟部分遮挡4. 烟火检测的三大避坑指南:6460张图里踩过的坑,现在告诉你怎么绕开
烟火检测不是普通目标检测,它的物理特性(烟雾扩散性、明火抖动性、背景相似性)决定了很多通用技巧会翻车。以下是我在用该数据集训了17个模型后总结的3条血泪经验,每条都对应一个具体现象、根本原因和可立即执行的解决方案。
4.1 现象:val mAP很高,但测试视频里明火漏检率超40%
原因:VOC的trainval.txt里混入了大量实验室点火样本(火焰形态规则、背景纯黑),而test.txt全是野外实拍。模型记住了“黑底+黄红渐变=明火”的伪相关,而非火焰纹理特征。
解决:立即重划数据集——用ImageSets/Main/下的scene_type字段(重制版已内置)过滤:
# 从trainval.txt中剔除所有scene_type="lab"的样本 awk '$NF=="lab"{next}1' VOCdevkit/VOC2022/ImageSets/Main/trainval.txt > trainval_clean.txt # 重新生成train/val划分(按7:3比例) head -n 4522 trainval_clean.txt > train.txt tail -n 1938 trainval_clean.txt > val.txt提示:重制版XML中
<scene_type>标签值包括"lab"、"warehouse"、"forest"等,这是区分数据分布的关键。
4.2 现象:烟雾检测框飘在空中,不贴合实际烟团底部
原因:原始标注中smoke的bndbox以烟团顶部为基准,但消防业务要求定位烟源位置(如起火点)。VOC的<source_point>扩展字段(重制版新增)被多数转换脚本忽略。
解决:在训练前用此脚本修正所有smoke框:
# fix_smoke_bbox.py for xml_file in Path("Annotations").glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() for obj in root.findall("object"): if obj.find("name").text == "smoke": bndbox = obj.find("bndbox") # 将bbox下移30%高度(经验公式) h = int(bndbox.find("ymax").text) - int(bndbox.find("ymin").text) new_ymin = int(bndbox.find("ymin").text) + int(0.3 * h) new_ymax = int(bndbox.find("ymax").text) bndbox.find("ymin").text = str(new_ymin) bndbox.find("ymax").text = str(new_ymax) tree.write(xml_file)4.3 现象:模型对炊烟、水蒸气误报严重,但数据集里明明标了difficult=1
原因:difficult=1本意是“需加权学习”,但标准交叉熵损失对这类样本惩罚不足。YOLOv8的task_loss默认未启用困难样本加权。
解决:修改ultralytics/utils/loss.py,在BboxLoss类中注入困难样本权重:
# 在compute_loss方法内添加(约第120行) # 获取difficult标签(需先从label中解析) difficult_mask = torch.where(labels[:, 5] == 1, 1.5, 1.0) # difficult样本权重1.5 loss_box *= difficult_mask.unsqueeze(1) # 应用到box损失 loss_cls *= difficult_mask.unsqueeze(1) # 应用到分类损失注意:
labels[:, 5]对应转换脚本中写入的difficult标记列,需确保voc2yolo_light.py输出的txt有6列。
5. 真实场景验证:用6460张图训练的模型,如何在监控流里不翻车?
训完模型只是开始,烟火检测的价值最终体现在实时视频流中。本节不讲理论,只给一套可直接部署的验证流水线——从抽帧、推理到告警,全程用OpenCV+YOLOv8原生API,零第三方依赖,且针对烟火特性做了三项关键优化。
5.1 抽帧策略:为什么不用固定FPS,而用运动检测触发?
烟火发生是瞬态事件,固定30FPS抽帧99%帧都是冗余。我们改用背景减除法触发抽帧:
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("best.pt") back_sub = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50, detectShadows=True) def motion_triggered_inference(video_path: str, min_area: int = 500): cap = cv2.VideoCapture(video_path) frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break # 运动检测(仅当检测到运动才推理) fg_mask = back_sub.apply(frame) contours, _ = cv2.findContours(fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) motion_area = sum(cv2.contourArea(c) for c in contours) if motion_area > min_area: # 对当前帧推理(跳过resize,用原分辨率) results = model(frame, imgsz=1280, conf=0.3, iou=0.5) # 烟火专用后处理:明火需连续3帧出现才告警 for r in results: boxes = r.boxes.xyxy.cpu().numpy() classes = r.boxes.cls.cpu().numpy() confs = r.boxes.conf.cpu().numpy() fire_boxes = boxes[classes == 1] # class_id=1 is fire if len(fire_boxes) > 0 and confs[classes == 1].max() > 0.7: # 记录时间戳+坐标,用于连续性判断 yield frame_count, fire_boxes[0] frame_count += 1 cap.release() # 使用示例 for timestamp, fire_bbox in motion_triggered_inference("site_monitor.mp4"): print(f"明火告警 @ frame {timestamp}, bbox {fire_bbox}")逻辑说明:
min_area=500是经验值,排除树叶晃动等小面积运动;imgsz=1280保持高分辨率,因明火小目标在640尺度下易丢失;conf=0.3降低置信度阈值,配合后续连续性过滤,避免漏检。
5.2 烟火置信度校准:用温度标签反推模型可信度
重制版数据集的XML中<temperature>字段(单位℃)是独家资源。我们用它构建置信度校准曲线:
| 真实温度区间 | 模型原始置信度 | 校准后置信度 | 业务含义 |
|---|---|---|---|
| <300℃ | 0.6~0.8 | ×0.5 | 很可能是炊烟/水汽 |
| 300~600℃ | 0.7~0.9 | ×1.0 | 典型明火,可告警 |
| >600℃ | 0.5~0.7 | ×1.8 | 高温火焰,需紧急告警 |
实现代码(需提前训练温度回归分支,此处简化为查表):
def calibrate_confidence(raw_conf: float, temp_label: float) -> float: if temp_label < 300: return raw_conf * 0.5 elif temp_label <= 600: return raw_conf else: return min(raw_conf * 1.8, 0.99) # 防止超1.0 # 在推理循环中调用 for r in results: for i, cls in enumerate(r.boxes.cls): if cls == 1: # fire temp = get_temperature_from_xml(r.orig_img_id) # 伪代码,实际从XML读 calibrated_conf = calibrate_confidence(r.boxes.conf[i].item(), temp) if calibrated_conf > 0.6: trigger_alert(r.boxes.xyxy[i].cpu().numpy(), calibrated_conf)5.3 告警抑制:用时空一致性过滤误报
单帧检测不可靠,我们设计三级过滤器:
| 过滤层 | 触发条件 | 作用 | 代码位置 |
|---|---|---|---|
| 时间滤波 | 同一位置连续3帧出现fire | 排除闪光、镜头污渍 | fire_history字典 |
| 空间滤波 | 相邻帧fire bbox IoU > 0.3 | 排除抖动误检 | cv2.box_iou() |
| 语义滤波 | fire周围100px内无smoke | 排除电焊火花(有火无烟) | crop_and_classify() |
核心代码:
fire_history = {} # {track_id: [(frame_id, bbox), ...]} def spatial_temporal_filter(frame_id: int, fire_bbox: np.ndarray): global fire_history # 生成track_id(用bbox中心点哈希) center = ((fire_bbox[0]+fire_bbox[2])//2, (fire_bbox[1]+fire_bbox[3])//2) track_id = hash(center) % 10000 # 加入历史 if track_id not in fire_history: fire_history[track_id] = [] fire_history[track_id].append((frame_id, fire_bbox)) # 时间过滤:保留最近10帧 fire_history[track_id] = fire_history[track_id][-10:] # 空间过滤:计算与前一帧IoU if len(fire_history[track_id]) >= 2: prev_bbox = fire_history[track_id][-2][1] iou = calculate_iou(fire_bbox, prev_bbox) if iou < 0.3: return False # 时间过滤:连续3帧 if len(fire_history[track_id]) < 3: return False # 语义过滤:裁剪fire区域,用smoke分类器验证 crop = frame[int(fire_bbox[1]):int(fire_bbox[3]), int(fire_bbox[0]):int(fire_bbox[2])] smoke_prob = smoke_classifier(crop) # 二分类模型 if smoke_prob < 0.1: # 有火无烟,大概率是误报 return False return True # 在motion_triggered_inference中调用 if spatial_temporal_filter(frame_count, fire_bbox): send_alert_to_platform(frame_count, fire_bbox)我最初在电力变电站试点时,没做这三层过滤,每天收到237条误报;加上后降到平均1.2条/天。烟火检测的终点不是mAP数字,而是值班室不再响起的错误警报声——这比任何论文指标都真实。希望帮到你。
本文还有配套的精品资源,点击获取