简介:面向火灾与人员检测场景的YOLO系列算法目标检测数据集,适用于巡检监控、消防预警、智慧园区等视觉识别任务,特别适合需要快速训练和验证YOLOv5、Y7、V8、V9、V10、YOLO11模型的开发者与学习者。压缩包共2000个文件,以XML标注文件为主,同时提供YOLO格式TXT标签,覆盖人、烟、火三类目标,并已划分好训练集、验证集和测试集,附带可直接引用的data.yaml配置文件,注释格式规范,方便迁移到不同版本。数据集大小141.83MB,已有46人浏览学习。使用时可分别读取对应文件夹中的标注,YOLO格式采用class x_center y_center width height归一化坐标,适合不熟悉格式转换的初学者直接上手训练;XML格式则便于使用LabelImg等工具二次检查。整体目录结构清晰,可帮助读者省去大量标注和整理时间,专注模型调参与效果对比。
1. 火灾和人员探测数据集:3039张图像带标签背后的YOLO格式与脏数据问题
摄像头前突然出现烟雾,报警再早几秒就可能是完全不同的故事。火灾和人员探测就是这种对“漏报”零容忍的任务:人形多变、烟雾透明无边界、火焰小且闪烁,监控相机的角度和光照又千差万别。这个数据集给出了3039张已标注图像,人、烟、火三类,标签是YOLO格式的txt文件——看起来拿到就能跑YOLO算法,实际做过训练的人都知道,“带标签”和“能训出可用模型”之间还隔着标签清洗、数据划分、类别不平衡和阈值选择四道坎。
下面按YOLO算法训练自己的数据集的流程走一遍:先解析标签确认人/烟/火的数量与框质量,再组织成YOLOv8可训练目录并跑通训练命令,接着做清洗与增强,最后落在验证集的分类别置信度调优上。适合正在准备火灾识别数据集的工程师,也适合刚把数据集下载下来第一次接触YOLO格式的同学。
2. 解压后先别训练:看懂YOLO标签文件并统计人/烟/火分布
2.1 YOLO txt标签的坐标规范与ZIP目录两种常见结构
YOLO算法使用的标签不是VOC的XML,也不是COCO的JSON,而是每张图对应一个同名txt。txt里每一行是五个数字:类别id、归一化中心点x、归一化中心点y、归一化框宽w、归一化框高h。比如一张640×480的图上,人的框左上在(220,120)、右下在(380,340),那么中心点是(300,230),框宽160、框高220,归一化后写进txt的一行就是0 0.46875 0.47917 0.25 0.45833。所有坐标先除以图像宽高再写入,所以这套标签不依赖输入分辨率,这也是YOLO格式比VOC格式在扩缩图像时少一步转换的原因。
ZIP解压后先看目录,常见两种结构:
| 结构 | 示例 | 后续处理 |
|---|---|---|
| 已分好train/val | images/train/.jpg + labels/train/.txt | 直接写data.yaml |
| 只有images与labels | images/.jpg + labels/.txt | 先划分训练集和验证集 |
不管是哪种,图像与标签都靠“文件名相同、后缀不同”配对:frame_001.jpg对应labels/frame_001.txt。有些压缩包还会单独放一个classes.txt,里面按行写类别名;没有也别慌,YOLOv8只用txt第一列的整数id,不强制读取类别文件。先把整个包解压到一个无中文、无空格的路径下,再用脚本确认图像扩展名是否统一为.jpg,有的人图会混入.jpeg或.png,这个差异会在配对阶段暴露出来。
2.2 用Python批量解析标签:统计人/烟/火框数与框尺寸
这一类数据集最怕的不是训练慢,而是类别分布偏到只剩人。用下面脚本把每类的框总数、每张图的目标数、以及面积占比小于1%的小目标框数一次算出来:
from pathlib import Path from collections import Counter, defaultdict import cv2 label_dir = Path("labels") img_dir = Path("images") class_names = {0: "person", 1: "smoke", 2: "fire"} label_counter = Counter() # 每个类别的框总数 per_image_counter = Counter() # 每张图的目标数量分布 wh_stats = defaultdict(list) # 每个类别的归一化宽高,用来算小目标占比 for txt_path in sorted(label_dir.rglob("*.txt")): if txt_path.name == "classes.txt": continue img_path = img_dir / (txt_path.stem + ".jpg") if not img_path.exists(): img_path = img_dir / (txt_path.stem + ".png") image = cv2.imread(str(img_path)) if image is None: print(txt_path, "对应图像缺失") continue h_img, w_img = image.shape[:2] lines = txt_path.read_text(encoding="utf-8").strip().splitlines() per_image_counter[len(lines)] += 1 for line in lines: parts = line.split() if len(parts) != 5: continue cls_id = int(parts[0]) w = float(parts[3]) h = float(parts[4]) label_counter[class_names.get(cls_id, "unknown")] += 1 wh_stats[class_names.get(cls_id, "unknown")].append((w, h)) print("各类别框数:", dict(label_counter)) print("每张图目标数出现最多的五种情况:", per_image_counter.most_common(5)) small_ratio = { name: sum(1 for w, h in wh if w * h < 0.01) / len(wh) for name, wh in wh_stats.items() if len(wh) > 0 } print("各类别中面积占比小于1%的框比例:", small_ratio)这段脚本有两个作用:一是确认三类目标都真实存在,二是看小目标比例。如果smoke或fire框数远低于person,后面训练时模型会天然偏向人,尤其是火苗只有十几个像素时,w*h算出来经常低于0.01。per_image_counter输出里如果大量出现“0行”标签,说明很多空标签文件占着位置,这也是火灾数据的常见问题:画面干干净净但标注人员没给“无火”帧写任何内容。
2.3 用OpenCV随机抽图可视化,核对标签是否贴着烟和火
统计暴露数量却暴露不了质量。随机抽10张图,把标注框画出来看,常见情况有三种:人的框问题最小,烟框经常把一大片半透明区域全包进去,火苗框则经常比实际火焰大出两三倍。烟雾没有明确边界,标注员主观判断差异很大,这会直接让回归loss在多轮训练里震荡。
import cv2 import random from pathlib import Path image_paths = list(Path("images").rglob("*.jpg")) random.seed(42) sample_paths = random.sample(image_paths, 10) colors = [(0, 255, 0), (0, 165, 255), (0, 0, 255)] # person绿 smoke橙 fire红 check_dir = Path("check") check_dir.mkdir(exist_ok=True) for img_path in sample_paths: img = cv2.imread(str(img_path)) h_img, w_img = img.shape[:2] txt_path = Path("labels") / (img_path.stem + ".txt") with open(txt_path, encoding="utf-8") as fp: for line in fp: cls_id, x_c, y_c, bw, bh = map(float, line.split()) x1 = int((x_c - bw / 2) * w_img) y1 = int((y_c - bh / 2) * h_img) x2 = int((x_c + bw / 2) * w_img) y2 = int((y_c + bh / 2) * h_img) cv2.rectangle(img, (x1, y1), (x2, y2), colors[int(cls_id)], 2) cv2.imwrite(str(check_dir / (img_path.stem + "_check.jpg")), img)抽出来的图不要看一眼就删,存到check/目录逐张过。如果发现大量烟框只框住了烟雾中心而漏了边缘扩散区,或火苗框比实际火焰大很多,那么后续训练前要先回堆标签。此时要做的不是上来就换YOLO算法改进、加注意力机制,而是先解决数据标注偏移,否则每个epoch都在拟合错误的框中心。
3. 把数据集组织成YOLOv8可训练格式:data.yaml划分与训练命令
3.1 训练集/验证集划分:按场景而不是纯随机
只有3039张图,常用比例是8:2或9:1。ZIP里如果已经带train/val目录,这步直接跳过;只有一级images/和labels/时,拿到数据的第一步就是划分。划分时最忌讳纯随机:如果这些图来自几十段连续视频监控帧,纯随机会把同一段视频的相似画面一半放进train、一半放进val,验证mAP虚高,换一个真实场景立刻掉点。按文件名前缀或目录名代表场景去分组,再把整个场景组划入train或val。
import random import shutil from pathlib import Path from collections import defaultdict image_dir = Path("images_flat") label_dir = Path("labels_flat") train_ratio = 0.9 random.seed(0) groups = defaultdict(list) for img_path in sorted(image_dir.rglob("*")): if img_path.suffix.lower() not in (".jpg", ".jpeg", ".png"): continue prefix = img_path.name.split("_")[0] # 按场景前缀分组 groups[prefix].append(img_path) group_names = list(groups.keys()) random.shuffle(group_names) cut = int(len(group_names) * train_ratio) train_groups = set(group_names[:cut]) val_groups = set(group_names[cut:]) train_img = Path("images/train") val_img = Path("images/val") train_lbl = Path("labels/train") val_lbl = Path("labels/val") for d in (train_img, val_img, train_lbl, val_lbl): d.mkdir(parents=True, exist_ok=True) def move_sample(img_path, target_img_dir: Path, target_lbl_dir: Path): txt_path = label_dir / (img_path.stem + ".txt") if txt_path.exists(): shutil.move(str(img_path), str(target_img_dir / img_path.name)) shutil.move(str(txt_path), str(target_lbl_dir / txt_path.name)) for group in train_groups: for img_path in groups[group]: move_sample(img_path, train_img, train_lbl) for group in val_groups: for img_path in groups[group]: move_sample(img_path, val_img, val_lbl)img_path.name.split("_")[0]只是一个容易落地的规则,具体前缀怎么取要看你文件名结构。如果图片是fire001_0001.jpg这种,前缀fire001能代表视频片段;如果文件名没有意义,就按文件所在子目录划分。shutil.move而不是Path.rename,是因为跨磁盘卷复制时rename会报错,move更稳。3039张图量不大,完整跑一遍即使有重复前缀,分组数量也不会太少。
3.2 编写data.yaml:路径、类别名与nc顺序的关系
YOLOv8的data.yaml描述数据接口,最小可用版本长这样:
path: /opt/datasets/fire_person train: images/train val: images/val nc: 3 names: 0: person 1: smoke 2: firepath写绝对路径单人用最省事,但这文件如果放进仓库和同事共享,建议改成相对路径,YOLOv8会把相对路径解析成yaml文件所在位置的相对地址。names列表的顺序必须和txt里第一列的类别id严格一致,如果标注文件约定0是人、1是烟、2是火,names就按这个顺序写,写反了不会报错,训练出来的模型会把烟火识别成互相调换的类别。nc: 3在yaml里容易漏,YOLO不会自动从names长度推断,漏写会在启动时报错。
还有一种不常见的组织方式是用train.txt和val.txt列出所有图片绝对路径,data.yaml里写train: train.txt、val: val.txt。如果ZIP包内已经给了这种列表,也可以直接沿用,但注意列表里的路径如果指向打包前的旧位置,需要全文替换路径前缀。
3.3 跑通最小训练命令并理解epochs/batch/imgsz的取舍
第一次跑不要直接100个epoch,先用小模型和短周期验证数据链路正常:
yolo detect train \ data=fire_person.yaml \ model=yolov8s.pt \ epochs=50 \ imgsz=640 \ batch=16 \ cache=True \ project=runs/detect \ name=fire_person_exp \ patience=10model=yolov8s.pt会下载COCO预训练权重并改用3类输出层;person在COCO里存在,所以第一轮就能继承一部分人形特征,smoke和fire则要从头学。imgsz=640是速度与框精度的折中点,如果前面的小目标比例很高,后期可以抬高到960,但显存和训练时间会明显上涨。cache=True会把3039张图提前缓存进内存,极大减少小数据集每epoch的读盘等待;3060 12GB上开batch=16通常不爆显存,老卡可以先试8。
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8s.pt | 先试small,别一上来用xl |
| epochs | 50起,最终100 | 看val曲线收敛情况再翻倍 |
| batch | 8/16/32 | 显存允许尽量大,但不要为凑batch降分辨率 |
| imgsz | 640或960 | 小目标多时加大,否则维持640 |
| patience | 10 | 连续10轮val mAP不涨就停,省时间 |
| cache | True | 3039张图完全能缓存,打开 |
训练过程中不要只看train loss,要盯val box_loss和mAP50。这个数据集的person类会先涨起来,smoke/fire可能到第20个epoch才有明显起色。如果到第40个epoch smoke class mAP还在0附近,一般不是训练轮次不够,而是第2章的标签分布或框质量出了问题,回去看small_ratio和可视化图。
4. 数据清洗与增强:处理小目标、漏检与类别不平衡
4.1 用脚本找出坏样本:标签越界、空标签、图像损坏
YOLO官方在训练时对越界框有裁剪容忍,但空标签和损坏图像会直接导致loss变成nan或训练进程中断。3039张图像说多不多,训练前用脚本做一次全量体检,比训练到一半再去排查高效得多。下面脚本会对每一对图-标签返回错误类型:
import cv2 from pathlib import Path def inspect_sample(img_path: Path, txt_path: Path): if not img_path.exists() or not txt_path.exists(): return "missing_pair" image = cv2.imread(str(img_path)) if image is None: return "broken_image" lines = txt_path.read_text(encoding="utf-8").strip().splitlines() if not lines: return "empty_label" for line in lines: parts = line.split() if len(parts) != 5: return "bad_format" cls_id, x_c, y_c, bw, bh = map(float, parts) if cls_id not in (0, 1, 2): return "bad_cls" if bw <= 0 or bh <= 0 or x_c < 0 or x_c > 1 or y_c < 0 or y_c > 1: return "bad_box" if (x_c - bw / 2) < -0.02 or (x_c + bw / 2) > 1.02 or \ (y_c - bh / 2) < -0.02 or (y_c + bh / 2) > 1.02: return "out_of_bound" return None for txt_path in Path("labels").rglob("*.txt"): img_path = Path("images") / (txt_path.stem + ".jpg") err = inspect_sample(img_path, txt_path) if err: print(f"{txt_path} -> {err}")out_of_bound给了0.02的容错余量,这是考虑标注员在图像边缘手抖时框可能略微超出画布。超过2%的越界属于标签错误,应该把那个txt剔除或手动修正。bad_cls出现时先别急着删,很可能是类别顺序和我们想的不一样,你需要回去核对原压缩包的classes.txt。空标签在火灾数据里要单独看:纯背景帧没有人和火,留着对训练无益但也不致命,真正危险的是图像和标签命名错位导致本来有烟的框落在了一张无关图上。
4.2 用增强补充烟火样本而不是无脑调YOLO算法
当第2章的统计结果显示smoke只有几百框、person有几千框时,模型会倾向于把置信度预算全给人。这时候别急着改网络结构,先补充烟火正样本。YOLOv8自带Mosaic、HSV变化、随机翻转等在线增强,但像Copy-Paste这类对小目标特别有效的增强不一定默认开启。常见做法是离线生成一批烟火增强图,让同一张烟火灾图出现在更多样化的背景下。
用albumentations在训练目录外生成增强图:
import albumentations as A import cv2 from pathlib import Path transform = A.Compose([ A.HueSaturationValue(hue_shift_limit=12, val_shift_limit=35, p=0.8), A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.8), A.HorizontalFlip(p=0.5), ], bbox_params=A.BboxParams(format="yolo", label_fields=["category_ids"])) src_txt_dir = Path("labeled_extra/txt") src_img_dir = Path("labeled_extra/img") out_img_dir = Path("augmented/img") out_txt_dir = Path("augmented/txt") out_img_dir.mkdir(parents=True, exist_ok=True) out_txt_dir.mkdir(parents=True, exist_ok=True) for txt_path in src_txt_dir.rglob("*.txt"): img_path = src_img_dir / (txt_path.stem + ".jpg") image = cv2.imread(str(img_path)) boxes, labels = [], [] with open(txt_path, encoding="utf-8") as fp: for line in fp: parts = line.strip().split() labels.append(int(parts[0])) boxes.append(list(map(float, parts[1:]))) transformed = transform(image=image, bboxes=boxes, category_ids=labels) cv2.imwrite(str(out_img_dir / ("aug_" + img_path.name)), transformed["image"]) with open(out_txt_dir / ("aug_" + txt_path.name), "w", encoding="utf-8") as fp: for label, bbox in zip(transformed["category_ids"], transformed["bboxes"]): fp.write(f"{label} {' '.join(f'{v:.6f}' for v in bbox)}\n")format="yolo"告诉albumentations输入输出都是归一化的中心点宽高坐标,库内部会转成像素坐标做变换,再转回归一化格式。HorizontalFlip对人员目标完全适用,但在带风向的烟雾场景要谨慎:翻转后烟柱方向会变,模型可能学到对称偏差。离线生成的增强图要放在训练目录里一并参与train/val划分,避免同一张图的不同变体同时出现在train和val中造成评估乐观。
4.3 增强参数表与烟火小目标的典型设置
| 增强策略 | 推荐参数 | 适用说明 |
|---|---|---|
| Mosaic | YOLOv8默认开启 | 混合四图,帮助模型适应不同尺度烟/火 |
| Copy-Paste | 离线脚本实现 | 把火苗小目标粘贴到无火背景,缓解类别不平衡 |
| RandomBrightnessContrast | brightness≤0.15, contrast≤0.15 | 模拟夜间监控低照度,火灾场景很常用 |
| HorizontalFlip | p=0.5 | 人形适用,烟火方向对称时再开 |
| HueSaturationValue | hue_shift≤12, val_shift≤35 | 火焰是橙红色,色相变化过大会产生紫色火 |
这些参数不是越大越好。火焰的橙红色是强先验,hue范围超过15会把火苗变紫,模型学会的不是“火”而是“橙红色物体”;val_shift太大会让火焰和背景一起过曝。每个参数改完,用plots=True或重新可视化生成一批增强预览图,确认增强后的box仍然贴着目标边缘。如果增强后框偏移明显,说明某个变换和box参数冲突,应该关掉那个变换而不是继续加强度。
5. 验证集上的人/烟/火分割调优:conf阈值与离线难例回流
5.1 用val评估分类别的mAP
训练完用best.pt做验证,重点看三个类别各自的mAP50:
yolo detect val \ data=fire_person.yaml \ model=runs/detect/fire_person_exp/weights/best.pt \ imgsz=640 \ conf=0.001 \ iou=0.5conf=0.001是关键。默认验证阈值是0.25,烟火小目标置信度普遍低,用0.25会把许多实际检出的框当成负样本,mAP被压得很惨。先降到0.001看模型的召回上限,部署时再单独调阈值。输出结果里如果person的mAP50在0.8以上,smoke只有0.3,这通常不是网络容量问题,而是第2章统计里烟框数量太少或框得不准。不要急着换大模型,先把smoke标签质量修一遍,往往比多跑50个epoch有效。
5.2 分类别置信度阈值:漏烟漏火的代价远大于误报
验证结束后,部署推理脚本里不要用同一个conf阈值处理所有类别。人的误检只是多画几个框,烟和火的漏报却可能让系统错过报警。常见做法是把person阈值抬高,烟/火阈值放低:
from ultralytics import YOLO model = YOLO("runs/detect/fire_person_exp/weights/best.pt") results = model.predict("camera_frame.jpg", conf=0.05, iou=0.5, verbose=False) class_conf_threshold = {0: 0.30, 1: 0.05, 2: 0.08} final_boxes = [] for box in results[0].boxes: cls_id = int(box.cls) conf = float(box.conf) if conf >= class_conf_threshold[cls_id]: final_boxes.append((box.xyxy[0].tolist(), cls_id, conf))model.predict里的conf=0.05只是为了防止推理阶段过早过滤掉低分框,真正决定是否上报的是后面按类别二次判断。烟和火的阈值低,意味着会带来更多误报,所以一般还要叠加一个帧间确认规则:连续两到三帧同一位置出现低置信度烟火框,再触发报警。
5.3 难例回流:把烟火低置信度帧变回训练数据
巡检真实监控视频时,把最终判定为smoke或fire但置信度低于0.1的帧按视频时间戳保存,每周挑一批交给标注人员补框,加入训练集重训。这是比持续调整YOLO算法改进更常见的烟火检测迭代方式:标注数据永远比网络重写更容易成为瓶颈。3039张图能训出可用的火灾和人员检测模型,但要让烟火漏报率继续下降,后续的难例回流与二次标注才是整个pipeline里回报最高的部分。
本文还有配套的精品资源,点击获取