简介:这份灭火器识别数据集面向从事目标检测的深度学习开发者与算法学习者,可用于消防场景下的灭火器自动检测任务,帮助快速搭建并验证YOLO系列、Faster R-CNN、SSD等检测模型。资源包共约2000个文件,以txt标签文件为主,另含1个yaml类别配置文件,压缩包大小约311MB,图片与txt标签已按训练集、验证集、测试集划分完毕,并同时提供VOC格式的xml标签,方便不同框架直接读取。数据集类别为extinguisher,图片数量3262张,覆盖多种真实场景,可直接投入YOLOv5至YOLOv10等系列算法的训练与调优。目前已有1427人学习下载,适合需要现成标注数据做模型训练、对比实验或课程设计的中高级开发者,省去自行采集与标注的成本,把精力集中在网络结构改进与精度提升上。
1. 灭火器识别数据集:从标注到部署,一条能跑通的链路
消防通道被杂物堵死、灭火器箱前堆满纸箱、压力表指针已经掉进红区——这些画面在监控里天天出现,但靠人盯着几十路视频去数灭火器,基本等于没数。灭火器识别数据集要解决的就是这件事:让目标检测模型自动框出画面里的灭火器,顺带判断它是不是被遮挡、是不是还在原位。这个方向适合两类人:一类是做智慧消防、园区安防的算法工程师,需要一套能直接训练、能落地到边缘盒子的数据;另一类是刚接触目标检测的开发者,想找一个类别单一、标注清晰、场景真实的练手数据集,把 YOLO 系列从训练到推理的完整流程走一遍。灭火器这个目标有个特点——颜色鲜艳但形态固定,正样本好标,难的是小目标、遮挡和反光,这三类样本决定了模型上线后会不会频繁误报。
2. 灭火器数据集长什么样:类别设计、标注格式与采集边界
2.1 类别怎么定:只框灭火器,还是把箱体和压力表一起标
我见过不少团队一开始只标“灭火器”一个类,训出来的模型在真实画面里会把红色消防栓、红色垃圾桶甚至红色广告牌一起框进去。后来我们把类别拆成三个:extinguisher(瓶体本身)、extinguisher_box(灭火器箱或支架)、extinguisher_sign(指示标志)。拆开之后,模型能区分“灭火器在不在箱子里”和“标志还在但灭火器没了”这两种完全不同的告警场景。
如果你的场景只需要知道“画面里有没有灭火器”,单类就够了,标注成本能省一半以上。但如果你要做“灭火器缺失检测”或者“通道占用检测”,建议至少保留extinguisher和extinguisher_box两类,因为箱体位置固定,瓶体位置会变,两者一对比就能判断是否被挪用。
类别命名不要用中文,也不要带空格,统一小写加下划线。标注文件里出现的类别名必须和训练时的data.yaml完全一致,大小写差一个字母都会导致训练时类别丢失。
2.2 标注格式选 VOC 还是 YOLO:转换脚本与四个边界坑
灭火器数据集的原始标注常见两种来源:一种是外包团队用 LabelImg 出的 Pascal VOC XML,另一种是直接拿 Labelme 转的 JSON。不管来源是什么,最终喂给 YOLO 训练的一定是归一化的class_id x_center y_center width height格式。下面这个脚本是我常用的 VOC 转 YOLO 版本,处理过几千张灭火器图片,边界情况基本覆盖了。
import xml.etree.ElementTree as ET import os from pathlib import Path # 类别映射:必须和 data.yaml 里的 names 顺序一致 CLASS_MAP = {"extinguisher": 0, "extinguisher_box": 1, "extinguisher_sign": 2} def voc_to_yolo(xml_path, img_w, img_h, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue # 跳过未定义类别,避免训练时索引越界 cls_id = CLASS_MAP[name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 边界裁剪:标注框超出图像范围时直接截断,否则归一化后会出现负值 xmin = max(0, min(xmin, img_w)) ymin = max(0, min(ymin, img_h)) xmax = max(0, min(xmax, img_w)) ymax = max(0, min(ymax, img_h)) # 过滤掉宽高为 0 的无效框 if xmax - xmin < 1 or ymax - ymin < 1: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: Path(out_txt_path).write_text("\n".join(lines), encoding="utf-8") return len(lines) # 批量处理:图片和 XML 同名同目录 img_dir = "fire_extinguisher/images" xml_dir = "fire_extinguisher/annotations" label_dir = "fire_extinguisher/labels" os.makedirs(label_dir, exist_ok=True) for xml_file in Path(xml_dir).glob("*.xml"): img_file = Path(img_dir) / (xml_file.stem + ".jpg") if not img_file.exists(): continue # 这里用 PIL 读尺寸,避免 OpenCV 在某些中文路径下读图失败 from PIL import Image with Image.open(img_file) as im: w, h = im.size voc_to_yolo(str(xml_file), w, h, str(Path(label_dir) / (xml_file.stem + ".txt")))这段代码里最容易被忽略的是边界裁剪和无效框过滤。灭火器数据集里经常出现标注员把框拉到图像外面,或者框选了一个几乎看不见的反光点,宽高算出来是 0。这两种情况不处理,训练时 loss 会直接变成 NaN,而且报错信息不会告诉你哪张图有问题,只能一张张翻,属于典型的血泪经验。
参数上,CLASS_MAP的顺序就是最终模型输出的类别索引,改顺序必须同步改data.yaml。归一化保留 6 位小数足够,YOLO 内部会再做一次缩放。如果图片是 PNG 带透明通道,PIL 读出来的尺寸没问题,但训练时建议统一转成 JPG,避免通道数不一致导致 dataloader 报错。
2.3 采集边界:多少张图、哪些场景必须覆盖
灭火器数据集不是越多越好,而是越“杂”越好。我一般按场景分层采集:室内走廊、地下车库、配电房、厨房、仓库各占一定比例。每个场景里再分正常光照、逆光、夜间红外、烟雾遮挡四种条件。一个能上线的灭火器识别数据集,至少需要 2000 张标注图,其中小目标(灭火器在画面中占比小于 5%)不少于 300 张,遮挡样本不少于 200 张。
如果只做 demo,500 张也能跑出 0.8 以上的 mAP,但一到真实摄像头就会翻车,因为 demo 数据集里灭火器都是正对镜头、光线充足、没有遮挡。真实场景里灭火器经常被消防水带挡住一半,或者挂在墙角只露出一个红色瓶底。采集时故意保留这些“不完美”样本,模型上线后的误报率会低很多。
3. 用 YOLOv8 训练灭火器识别模型:从 data.yaml 到第一轮推理
3.1 环境与目录:把数据集摆成 YOLO 认识的形状
YOLOv8 对目录结构有固定要求,摆错了不会报错,但训练时一张图都读不到。标准结构如下:
fire_extinguisher/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片(可选) ├── labels/ │ ├── train/ # 训练集标注 txt │ ├── val/ │ └── test/ └── data.yamldata.yaml内容:
path: /data/fire_extinguisher train: images/train val: images/val test: images/test names: 0: extinguisher 1: extinguisher_box 2: extinguisher_signpath写绝对路径最稳,相对路径在不同工作目录下启动训练时容易找不到。names的索引必须和转换脚本里的CLASS_MAP完全对应,顺序错了模型会把灭火器箱认成灭火器,而且 loss 曲线看起来还挺正常,属于玄学翻车。
安装环境用 pip 即可:
pip install ultralytics yolo checksyolo checks会打印环境信息,重点看 CUDA 是否可用。如果显示 CPU,训练 2000 张图大概要几个小时,建议至少有一张 8G 显存的卡。
3.2 训练命令与必调参数:imgsz、batch、lr0 怎么定
启动训练的命令本身很简单:
yolo detect train \ data=/data/fire_extinguisher/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/fire_extinguisher \ name=exp1参数逐个说。model选yolov8n.pt还是yolov8s.pt取决于部署硬件:边缘盒子用 n 版,服务器用 s 版。灭火器类别少、形态固定,n 版在 2000 张图上通常能到 0.85 mAP 以上,没必要上大模型。
imgsz=640是默认值,但如果你的摄像头画面里灭火器特别小,比如 1080P 画面里只占 40 像素宽,那 640 下采样后只剩 20 多像素,模型很难学到特征。这种情况把imgsz提到 960 或 1280,代价是显存和推理时间翻倍。我一般先用 640 跑一轮,看验证集里小目标的召回率,低于 0.6 再提分辨率。
batch=16是 8G 显存下的安全值,显存不够就降到 8,但 batch 太小会让 BN 层统计不稳定,mAP 波动变大。lr0=0.01是 SGD 的初始学习率,如果 loss 在前 10 个 epoch 就炸到 NaN,降到 0.001 再试。patience=20表示验证集 mAP 连续 20 轮不提升就早停,灭火器数据集通常 60 到 80 轮就收敛了,设 100 轮是留余量。
训练过程中重点看三个指标:box_loss是否稳定下降、mAP50是否持续上升、precision和recall是否差距过大。如果 recall 远低于 precision,说明模型漏检多,优先补小目标和遮挡样本;如果 precision 低,说明误报多,检查标注里有没有把红色非灭火器物体标进去。
3.3 推理与验证:用测试集图片看真实效果
训练完成后,用yolo detect predict跑单张或整个目录:
yolo detect predict \ model=runs/fire_extinguisher/exp1/weights/best.pt \ source=/data/fire_extinguisher/images/test \ conf=0.25 \ iou=0.45 \ save=Trueconf=0.25是置信度阈值,低于这个值的框不输出。灭火器识别里这个值可以适当调低到 0.2,因为漏检一个灭火器的代价比多框一个红色物体高。iou=0.45是 NMS 的 IoU 阈值,如果画面里灭火器密集排列,比如消防柜里并排三个,这个值要提到 0.5 以上,否则相邻的框会被合并成一个。
推理结果会保存在runs/detect/predict下,重点看三类图:小目标灭火器有没有框到、被遮挡一半的有没有框到、红色非灭火器物体有没有被误框。这三类图各挑 20 张人工数一遍,比看 mAP 数字更直观。
4. 灭火器识别落地避坑:标注、训练、部署里的五个真实翻车点
4.1 现象:训练 loss 正常但 mAP 一直是 0
原因通常是data.yaml里的names索引和标注文件里的class_id对不上。比如标注里灭火器是 0,但names里 0 写成了extinguisher_box,模型学到的就是错位的类别。另一种可能是train和val路径下没有图片,YOLO 会静默跳过,loss 显示为 0 但不报错。
解决:训练前用脚本统计一遍标注文件里出现的所有class_id,和names逐一对齐。同时确认images/train和labels/train的文件名一一对应,缺一个都会导致该图被跳过。
4.2 现象:模型在验证集上 mAP 很高,一到真实摄像头就疯狂误报
原因是验证集和真实场景的数据分布不一致。验证集里灭火器都是正对、清晰、光线好,真实摄像头里灭火器可能只露出一个角,或者画面里有红色消防水带、红色安全帽。模型学到的是“红色+圆柱形=灭火器”,而不是灭火器的本质特征。
解决:从真实摄像头里截取 200 张误报图,人工标出其中的灭火器(如果有),加入训练集重新微调。同时把误报的红色物体作为负样本,在标注时不框任何东西,让模型学会区分。
4.3 现象:小目标灭火器召回率极低,mAP50 只有 0.4
原因是imgsz=640下采样后小目标特征丢失。1080P 画面里 40 像素宽的灭火器,缩到 640 后只剩 23 像素,再经过 backbone 的 32 倍下采样,特征图上的响应几乎消失。
解决:把imgsz提到 960 或 1280,同时开启mosaic增强(YOLOv8 默认开启),让模型在拼接图中见到更多小目标组合。如果显存不够,用rect=True做矩形推理,减少 padding 浪费。
4.4 现象:推理时同一张图里灭火器被框了两次,NMS 没起作用
原因是iou阈值设得太高,或者灭火器标注框本身重叠严重。如果训练数据里同一个灭火器被标了两个框,模型会学到重复输出。
解决:先检查标注文件,同一个目标只保留一个框。推理时把iou从默认 0.7 降到 0.45 到 0.5 之间,具体值用验证集试,看 mAP 和框数量的平衡点。
4.5 现象:模型部署到边缘盒子后推理速度只有 2 FPS
原因是模型输入分辨率太高,或者用了yolov8s以上的模型。边缘盒子的算力通常只有几 TOPS,跑 1280 输入的 s 版模型很吃力。
解决:导出 ONNX 或 TensorRT 时固定输入尺寸,用yolov8n加imgsz=640,在盒子上的推理速度能到 15 FPS 以上。如果精度不够,用知识蒸馏把 s 版的能力迁移到 n 版,而不是直接上大模型。
5. 把灭火器识别做稳的进阶习惯:从单帧检测到时序确认
单帧检测永远会有误报,这是目标检测的固有局限。我在实际项目里会把灭火器识别和时序逻辑绑在一起:连续 5 帧里至少 3 帧检测到同一个位置的灭火器,才触发一次有效告警。这个逻辑用简单的跟踪算法就能实现,不需要上复杂的 ReID。
# 简易时序确认:基于 IoU 匹配的帧间计数 from collections import defaultdict class TemporalConfirmer: def __init__(self, window=5, threshold=3, iou_thr=0.5): self.window = window # 滑动窗口帧数 self.threshold = threshold # 触发告警所需命中次数 self.iou_thr = iou_thr # 帧间匹配的 IoU 阈值 self.history = defaultdict(list) # track_id -> 命中记录 def update(self, detections, frame_id): # detections: [(x1, y1, x2, y2, cls_id, conf), ...] # 简化处理:按类别和位置粗略匹配,实际项目可换成 ByteTrack for det in detections: matched = False for tid, records in self.history.items(): last_box = records[-1][1] if self._iou(det[:4], last_box) > self.iou_thr: records.append((frame_id, det[:4])) matched = True break if not matched: new_id = len(self.history) self.history[new_id].append((frame_id, det[:4])) # 清理超出窗口的记录 for tid in list(self.history.keys()): self.history[tid] = [ r for r in self.history[tid] if frame_id - r[0] < self.window ] if not self.history[tid]: del self.history[tid] # 返回确认告警的 track_id confirmed = [] for tid, records in self.history.items(): if len(records) >= self.threshold: confirmed.append(tid) return confirmed @staticmethod def _iou(box_a, box_b): xa = max(box_a[0], box_b[0]) ya = max(box_a[1], box_b[1]) xb = min(box_a[2], box_b[2]) yb = min(box_a[3], box_b[3]) inter = max(0, xb - xa) * max(0, yb - ya) area_a = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1]) area_b = (box_b[2] - box_b[0]) * (box_b[3] - box_b[1]) union = area_a + area_b - inter return inter / union if union > 0 else 0这段代码的核心思想是:单帧检测结果只作为候选,只有连续多帧稳定出现的检测框才被确认为真实目标。window=5表示看最近 5 帧,threshold=3表示至少命中 3 帧。这两个参数根据摄像头帧率调整,25 FPS 下 5 帧只有 0.2 秒,对灭火器这种静态目标足够;如果摄像头有抖动,把window提到 10,threshold提到 6。
iou_thr=0.5是帧间匹配的宽松阈值,因为摄像头轻微晃动会导致同一灭火器的框位置偏移,IoU 可能降到 0.6 左右。如果场景里灭火器密集,这个值要提到 0.6 以上,避免相邻灭火器被误匹配成同一个。
这套逻辑上线后,误报率通常能从单帧的每天几十次降到每天两三次。代价是告警延迟增加了 0.2 秒,对消防场景来说完全可以接受。
我自己的习惯是:每换一个摄像头点位,先跑 24 小时纯推理不告警,把检测结果全部存下来,人工看一遍误报和漏报,再决定conf和时序参数怎么调。这个“后悔药”步骤花不了一天,但能省掉上线后被业务方追着改 bug 的两周。希望帮到你。
本文还有配套的精品资源,点击获取