ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv8火灾检测实战:从3039张数据集清洗到模型训练全流程

YOLOv8火灾检测实战:从3039张数据集清洗到模型训练全流程 简介面向YOLO系列算法目标检测的火灾与人员探测数据集专为需要训练火焰、烟雾、人员识别模型的开发者和学习者设计。资源包共141.83MB包含2000个XML标签文件VOC格式并提供对应YOLO格式的txt标签版本标签采用 x_center y_center 归一化坐标格式数据集已划分训练集与验证集附有data.yaml配置文件可直接适配yolov5、yolov8、yolov9、yolov10、yolov11等主流YOLO算法。标签涵盖人员、烟雾、火焰等目标类别文件名末尾还带有部分类别名称方便快速检索两种标签格式分别保存于独立文件夹便于在训练前灵活切换省去手动转换格式的繁杂环节。目前已有48人学习浏览对于需要快速搭建火灾预警、人员检测或智慧安防方案的学生、竞赛选手及工程师来说是一份紧凑且可直接上手的训练数据集适合快速测试模型效果。1. 火灾与人员探测数据集3039张带标签图能训出什么实用模型做消防监控或安防摄像头算法时最卡人的往往不是YOLO算法本身而是没有能直接开训的带标签数据。自己标注起火点和烟雾要一帧帧画框三天下来眼睛都是花的。这份标题指向的数据集恰恰是「人—烟—火」三个类别的YOLO格式标注包3039张图像数量不大却很有针对性。网上讲YOLO的PPT一搜一大把真正常缺的是这种拆开就能用的数据资产。它适合刚接触目标检测的新手做第一个实战项目也适合已经有基础但缺消防领域数据的工程师做迁移学习。这个量级的数据没法指望训出万能模型但配合预训练权重做微调跑通报警场景的检测闭环是够用的。我把拿到这类数据集后的完整处理流程、训练命令和踩过的坑一次说清楚。2. 解压后第一步确认目录结构、标签格式与类别分布2.1 解压后的目录结构images、labels、classes.txt 与划分文件清单很多人在下载完zip后直接拖进训练脚本结果路径找不到、类别对不上白白浪费时间。我的习惯是先在根目录跑一次树状结构查看把家底盘清楚再动手。消防类YOLO数据集最常见的是这种布局fire_person_dataset/ ├── images/ │ ├── img_0001.jpg │ ├── img_0002.jpg │ └── ... ├── labels/ │ ├── img_0001.txt │ ├── img_0002.txt │ └── ... ├── classes.txt ├── train.txt ├── val.txt └── README.txtimages和labels是一一对应的只要文件名相同训练脚本会按这个对应关系自动找标签。classes.txt里按行写三个类名通常是人、烟、火顺序决定了标签文件里class id的值。train.txt和val.txt记录的是划分后的样本路径方便直接用ImageFolder方式读取。这份zip里给出的「3039张图像带标签」说的是总量不代表内部已经按7:3或8:2划分好了。如果包内没有train.txt和val.txt就需要在整理时自己做随机划分。这里有个容易忽视的点按图像随机划分而不是按视频帧连续划分否则同一段视频的相邻帧会同时出现在训练集和验证集里mAP虚高到没参考价值。2.2 YOLO的txt标签怎么写归一化坐标、类别id与ultralytics约定YOLO系列从v5到v8用的是同一种txt标注格式每行一个目标共5个数含义是「类别id、归一化中心x、归一化中心y、归一化宽度、归一化高度」。归一化指坐标除以图像宽高后得到的0~1区间的小数而不是像素绝对值。看一眼真实的标签文件就明白了0 0.542968 0.541406 0.248438 0.573437 1 0.721875 0.376563 0.184375 0.234375 2 0.456250 0.725000 0.031250 0.050000第一行的0对应classes.txt里的第一个类别人后面四个0.54、0.54、0.24、0.57分别表示边界框中心在图像横向54%和纵向54%的位置框宽度占整张图24%高度占57%。第二行的1是烟第三行的2是火。这里最常翻车的不是数字格式而是类别顺序。如果包里提供了classes.txt严格按它的顺序来就好。如果没提供就得自己从标签文件的第一个数字反推。我一般会写个小脚本扫描所有txt文件收集出现过的class id集合再对照图像内容人工确认每个id对应的是人还是烟还是火。这一步做错训练出来的模型会把火当成人来报警代价很大。2.3 盘点脚本统计类别数量、图像尺寸与空标签决定数据清洗策略训练前做一次数据盘点能省掉后面好几个小时的排障。以下脚本统计每个类别的目标数量、图像尺寸分布、空标签数量同时还把「图像存在但标签为空」和「标签存在但图像缺失」的脏数据找出来import os from collections import Counter, defaultdict from PIL import Image img_dir images label_dir labels class_counts Counter() img_sizes Counter() empty_labels [] misaligned [] class_id_to_sizes defaultdict(list) img_files {f.split(.)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} label_files {f.split(.)[0] for f in os.listdir(label_dir) if f.endswith(.txt)} for stem in img_files: label_path os.path.join(label_dir, stem .txt) if not os.path.exists(label_path): misaligned.append(stem) continue with open(label_path, r) as fp: lines [ln.strip() for ln in fp if ln.strip()] if not lines: empty_labels.append(stem) continue for ln in lines: parts ln.split() if len(parts) ! 5: continue cls_id int(parts[0]) class_counts[cls_id] 1 class_id_to_sizes[cls_id].append( (float(parts[3]), float(parts[4])) ) print(类别分布(class id - 目标数):, dict(class_counts)) print(空标签文件数:, len(empty_labels)) print(图像/标签不对齐数:, len(misaligned))逻辑说明先把图像文件名和标签文件名分别取集合用文件名stem做交集对齐然后逐行解析标签过滤空行和非5元素行最后按类别统计目标总数。class_id_to_sizes 这个字典是我用来判断是否存在「小目标扎堆」的如果火的宽高均值乘以图像尺寸后小于32像素说明这份数据里火大多是小目标后面训练要把imgsz调大或用P2层输出。参数说明img_dir和label_dir按实际路径改。PIL读图可以确认尺寸但如果只是想盘点标签不读原图也能跑完。空标签文件不要直接删除如果图像里确实没有目标YOLO训练会当作背景样本但如果是漏标删除后会漏学这一类我习惯把空标签单独挪到backup目录。3. 喂给YOLOv8的最小闭环目录重组、dataset.yaml与训练命令3.1 目录重组脚本把散落图片与标签对齐成images/labels两个根目录解压后如果目录结构不是YOLO标准布局我一般会用脚本重排而不是手动拖文件夹。重排的目标是让所有图片统一进images所有txt统一进labels再做一次7:3随机划分生成train.txt和val.txt。脚本如下import os import random from shutil import move src_root fire_person_dataset_raw dst_root fire_person_dataset os.makedirs(f{dst_root}/images, exist_okTrue) os.makedirs(f{dst_root}/labels, exist_okTrue) image_exts (.jpg, .jpeg, .png, .bmp) image_src os.path.join(src_root, images) # 按实际结构调整 label_src os.path.join(src_root, labels) stems [] for fname in os.listdir(image_src): if fname.lower().endswith(image_exts): stem os.path.splitext(fname)[0] label_fname f{stem}.txt if os.path.exists(os.path.join(label_src, label_fname)): stems.append(stem) move(os.path.join(image_src, f{stem}{ext}), os.path.join(dst_root, images, fname)) move(os.path.join(label_src, f{stem}.txt), os.path.join(dst_root, labels, f{stem}.txt)) random.seed(42) random.shuffle(stems) split_idx int(len(stems) * 0.8) with open(f{dst_root}/train.txt, w) as fp: for stem in stems[:split_idx]: fp.write(f{dst_root}/images/{stem}.jpg\n) with open(f{dst_root}/val.txt, w) as fp: for stem in stems[split_idx:]: fp.write(f{dst_root}/images/{stem}.jpg\n)逻辑说明脚本先扫描源目录下所有图像检查对应的txt是否存在只有「图和标签成对」的样本才进入训练集。这一步能避免训练中途出现FileNotFoundError。然后按8:2划分并写路径清单。random.seed(42)保证每次跑出的划分一致方便复现。参数说明split_idx是划分点0.8表示80%训练。如果后续想用k-fold交叉验证可以改成循环5次重新切分。如果包的README里已经写了官方划分优先用官方划分不要自己重切自己切分可能在验证集里引入与训练集高度相似的帧导致mAP虚高。3.2 写dataset.yaml路径、类别名与验证集划分的关键点ultralytics训练时通过一个yaml文件告诉模型「数据在哪、预测哪几个类」。这个文件比很多人以为的要讲究路径要写绝对路径或用相对路径并保证执行目录正确path: /data/fire_person_dataset train: images # 指向path下的trian图像目录 val: images # 如果只有一份全量临时用images代替 test: names: 0: person 1: smoke 2: firetrain和val的取值是相对path的路径。val指向images目录表示「用训练图像做验证」这个操作不严谨但如果在只有一份全量数据的前提下先跑通流程可以临时这样用正式训练一定要划分出独立的验证集。names字典的key必须从0连续递增不能跳号YOLO读取时会按索引取类别名。这里有个常见做法把val指向images目录后训练本身的收敛曲线不会报错但mAP会异常高因为模型见过这些图。我会在yaml里额外加一行val: images注释说明「暂用待划分」提醒自己不要被指标骗了。3.3 跑通yolov8训练的最小命令epochs、imgsz、batch如何选目录和yaml就绪后直接执行训练命令yolo detect train datafire_dataset.yaml \ modelyolov8s.pt \ epochs100 \ batch16 \ imgsz640 \ patience10 \ project./runs/fire_train \ nameexp1命令说明modelyolov8s.pt表示从COCO预训练权重开始微调这是3000多张的小数据集能训出可用模型的必要条件不要从yolov8s.yaml随机初始化开始。epochs100对这个小数据集足够配合patience10做早停。batch16在8G显存下能跑动如果爆显存就降到8同时imgsz不要超过640——火灾数据集里烟和火都是大尺度目标640足够。参数选择逻辑我一般是这样大目标检测优先保证batch别太小。batch8时BN层统计噪声大在只有3000多张图的小数据集上会放大不稳定性。如果显存只有6G选yolov8n.pt代替s版本保留通道注意力机制的同时显存占用更低。imgsz不是越大越好图像分辨率不够时强行拉到1280只会让模型学到插值噪声。表格式对比一下常用组合显存模型batchimgsz预期表现6Gyolov8n.pt8640可跑通精度一般8Gyolov8s.pt16640推荐起点12Gyolov8m.pt16768小目标效果更好训练跑完后看results.png里的mAP50和mAP50-95两条线。mAP50从0.5涨到0.75附近算正常mAP50-95如果只有0.3~0.4也不要慌消防数据集里烟和火的边界比一般目标模糊这个指标天然偏低。4. 训练前过一遍标注清洗可视化校验与越界坐标修正4.1 可视化校验把边界框画回原图直接暴露漏标和错标数据清洗最有效的手段不是算指标而是把标注框画回原图上肉眼扫一遍。我习惯随机抽100张图把框和类别名画出来存成一个网格图15秒就能看出这份数据集的标注习惯、漏标概率和有没有「框偏了半个身位」的问题import cv2 import random from pathlib import Path img_dir Path(fire_person_dataset/images) label_dir Path(fire_person_dataset/labels) class_names [person, smoke, fire] sample_files random.sample(list(img_dir.glob(*.jpg)), 100) def draw_one(img_path, label_path): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(label_path, r) as fp: lines fp.readlines() for ln in lines: cls_id, xc, yc, bw, bh map(float, ln.split()) x1 int((xc - bw/2) * w) y1 int((yc - bh/2) * h) x2 int((xc bw/2) * w) y2 int((yc bh/2) * h) color (0, 255, 0) if int(cls_id) 0 else (255, 0, 0) if int(cls_id) 1 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) return img for idx, img_path in enumerate(sample_files): label_path label_dir / (img_path.stem .txt) if not label_path.exists(): continue canvas draw_one(img_path, label_path) cv2.imwrite(fcheck_{idx:03d}.jpg, canvas)逻辑说明随机抽100张不连续编号的图像避免只检查了某个时间段的数据。draw_one把归一化坐标还原成像素坐标用不同颜色画框并标注类别。观察重点是烟的区域是否完整覆盖、火是否只标了火焰芯而漏了外焰、远处的行人是否没标。如果烟标得特别碎——同一缕烟被拆成五六个小框模型训练时会无所适从。参数说明sample_files一次抽100张如果想更精细就抽200张但每张单独看图。检查完的check_*.jpg可以直接删掉不影响数据。如果发现漏标严重超过两成图像有漏标说明这份数据集的标注质量不稳定我会选择放弃这一批数据重新找来源而不是花几小时手动补标。4.2 清洗脚本越界坐标修正、重复框去重、空标签与面积过滤标注工具偶尔会输出一些越界坐标比如归一化框的x1小于0或x2大于1还有的工具会连点两下产生两个完全重合的框。这些脏数据不清理训练时NeoViT的anchor匹配会出问题。清洗脚本如下import os def clamp_bbox(values, eps0.001): cls_id, xc, yc, bw, bh values x1 max(0.0, xc - bw/2) y1 max(0.0, yc - bh/2) x2 min(1.0, xc bw/2) y2 min(1.0, yc bh/2) if x2 x1 or y2 y1: return None return [cls_id, (x1x2)/2, (y1y2)/2, x2-x1, y2-y1] label_dir labels backup_dir labels_backup os.makedirs(backup_dir, exist_okTrue) for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue path os.path.join(label_dir, fname) with open(path, r) as fp: lines [ln.strip() for ln in fp if ln.strip()] new_lines [] seen_boxes set() for ln in lines: parts ln.split() if len(parts) ! 5: continue vals [float(p) for p in parts] cls_id int(vals[0]) fixed clamp_bbox(vals) if fixed is None: continue box_key (cls_id, round(fixed[1], 4), round(fixed[2], 4), round(fixed[3], 4), round(fixed[4], 4)) if box_key in seen_boxes: continue seen_boxes.add(box_key) new_lines.append(f{fixed[0]:.0f} {fixed[1]:.6f} {fixed[2]:.6f} {fixed[3]:.6f} {fixed[4]:.6f}\n) if len(new_lines) ! len(lines): os.replace(path, os.path.join(backup_dir, fname)) with open(path, w) as fp: fp.writelines(new_lines)逻辑说明clamp_bbox把越界的框裁剪回[0,1]区间如果裁剪后宽或高为负说明这个框已经完全失效直接丢弃。seen_boxes用四舍五入后的坐标做key把完全重叠的重复框去掉。只要某个文件发生改动先把原文件挪到backup目录再写入清洗后的内容相当于留一份后悔药。参数说明round的4位小数用于去重判定如果只想去掉完全重合的框4位足够想顺带合并「几乎重合」的框就要把小数位缩到2位的欧氏距离判定但容易误删两个相隔很近的人。另外清洗后要复查一次每张图的类别分布如果把某个类别的框都误删了训练时该类会变成负样本报「class 2 has no targets」之类的警告就要注意。5. 避坑训练火灾检测模型时最常翻车的5个现场5.1 现象模型把白墙和路灯误报成火原因数据集中「火」的标注大多集中在燃烧区域的红黄色块而模型学到的是「颜色偏亮偏暖的区域」。白墙在强光下呈暖白色路灯在夜间呈亮黄色特征与火焰高度相似。解决把训练集里的负样本单独整理出来添加一批「无火灾但有人活动」的日常监控图标签为空。让模型在背景类别上学到「这些也是正常环境」。实际操作是新建一个negatives目录放500张没有目标的图在dataset.yaml的train路径里用新目录替代或者额外挂一个负样本目录。其次是增强火的纹理标注只框火焰芯会让模型学不到「外焰是无规则抖动」这个关键区别。5.2 现象烟的框太碎模型漏检率居高不下原因检查可视化结果后发现同一缕烟被标注成六七个互相重叠的小框。yolov8在训练时用NMS合并这些框但目标的尺寸分布被小框主导模型倾向于预测小目标真实扩散的大烟区域反而被当作背景。解决如果只是少数文件的问题在清洗脚本里把同一图像中IoU大于0.5的同类别框合并成外接矩形。如果普遍如此就不要增量清洗了直接放弃这批标注另找数据源。这个判断要果断几千张碎框数据洗到一半官方都容易暴躁出血点在小括号的框合并逻辑上。5.3 现象loss正常下降但mAP很低训练曲线像教科书一样漂亮但验证集指标不动原因数据划分出问题验证集里大多是训练集图像的轻微变换版本或者类别极度不均衡——1000个火框、200个烟框模型把「预测为火」当作捷径烟的召回率趋近于零。解决重新划分数据集确认train.txt和val.txt的样本没有重叠最好按视频源而不是按单帧划分。类别不均衡则计算一下train数据里每个类的框数量如果失衡超过3倍优先做类别重采样——yolo的class_weights参数或者简单地在训练时多复制少数类的图像。5.4 现象训练不报错但检测时fire标签全变成smoke原因classes.txt里类别顺序是person、fire、smoke但标签文件里class id 1对应的其实是smoke。标注工具导出时排序和txt文件里的排序不一致模型学到的语义完全错位而且代码不报错因为id和名字的映射只在推理时存在。解决训练前用下面的方法校准——随机抽5张图把画框可视化脚本直接输出open_cv窗口人工确认颜色对应的类别名。建立类名与id的映射表并把这个映射表写进dataset.yaml不要依赖classes.txt的顺序。5.5 现象远处的行人和烟雾完全漏检近距离目标正常原因原图分辨率可能是1080p或更高标注时小目标只有十几个像素但训练时把图缩放到640小目标缩成4×4像素特征早已丢失。这属于小目标检测的通病火灾场景尤其严重。解决三步处理。第一步把imgsz从640提到768或960显存不够就换模型为yolov8n。第二步做滑窗切片把大图切成4块960×960再训练。第三步是在预处理阶段用超分辨率模型比如Real-ESRGAN把图像重建到2倍分辨率再标注训练。我一般先试imgsz提升不行再上切片。注意切片后标注也要跟着切否则框会错位。6. 从数据集到可部署模型自训练闭环、类别均衡与小目标验证技巧数据集本身是静态的但真实场景里火和烟永远在变。我拿到这个包训练完第一版模型后不会急着上线而是先做一轮半监督自训练用当前模型去跑几十段监控视频把置信度大于0.85的检测结果当作伪标签自动写进labels目录低于0.85的高得分区域截出来人工抽检补标。伪标签有一个严格的边界——只在无需追溯、容忍一定噪声的预研项目里用正式项目必须人工抽检后合入。类别失衡问题在建站式数据集里几乎一定会遇到火的目标框数量明显少于烟。我一般用两个手段一是augmentTrue时开启copy_paste增强让少数类目标在图像间复制粘贴形成新样本二是如果烟的框是火的3倍以上在损失函数上给火类加权重yolov8里通过loss_config或者轻量修改损失计算实现。不要指望靠重复放同样的图像来均衡模型会过拟合到那张图的背景。验证阶段最大的教训是单帧mAP会骗人。火灾检测的真实场景里烟从无到有再到消散是一个时间序列单帧检测结果跳变剧烈。我会把验证方式改为时序测试从测试视频中截取20段1分钟片段人工判断「从第几秒开始连续三帧都输出fire0.5」作为报警时间点和人工观察时间比对。这个指标比mAP更能决定这个模型能不能真的放在监控系统里。最后一点私心经验训练完不要只保留best.pt把last.pt也留着。有一次我在新数据集上继续训练时发现best.pt早停在了一个过拟合点回退到last.pt再微调反而更好。希望帮到你。本文还有配套的精品资源点击获取
返回列表