简介:这是一份基于YOLOv5实现的火焰图像与视频识别项目完整资源,面向目标检测初学者、高校学生以及需要完成毕业设计、课程设计或工程实训的人群。项目围绕YOLOv5框架,涵盖数据标注、模型配置、训练脚本与说明文档,可帮助读者从零搭建火焰检测流程,并迁移到其他小型目标检测任务中。资源共2000个文件,压缩包约446MB,其中1989个XML标注文件为目标框标签,YAML与TXT分别承担模型配置与数据集划分,MD文档提供使用指引,Python脚本则负责数据预处理等辅助操作。整体目录结构清晰,便于按模块检索学习。目前已有340人学习下载,借助该资源可完成从训练数据准备到模型推理的闭环,尤其适合以火焰检测为切入点的课设与毕设,在有限时间内获得可运行、可展示的成果。
1. 火焰检测为什么绕不开 YOLOv5
传统火焰识别大多靠颜色阈值或帧差法,背景一复杂就崩,误报率能到 30% 以上。YOLOv5 把目标检测当成回归问题,一次前向推理同时输出类别和边界框,在 GPU 上能达到毫秒级延迟。用于火焰这个任务,它的优势不是精度碾压一切,而是工程上成熟:PyTorch 生态、预训练权重多、部署工具链完整,适合自建数据集做迁移学习。本文从零开始讲清楚怎么用 YOLOv5 做一个能跑视频的火焰识别系统,数据来自 VOC 格式的 XML 标注,代码里包含 maketxt.py 等脚本,流程覆盖数据划分、训练、测试到视频推理。对于做毕设或课程设计的人来说,这套流程可以直接复现,也方便改成烟雾、电动车等检测场景。
2. 数据准备与标注格式转换:XML 到 YOLOv5 训练集
2.1 理解输入数据的目录结构
拿到一组带有 XML 文件的火焰图像,典型的组织方式是 JPEG 图片和同名 XML 放在同一目录。XML 是 Pascal VOC 格式,每个文件描述一张图里的目标,包括物体类别名和边界框坐标。YOLOv5 本身不认识 XML,它只认两种东西:每行一个标注的 txt 文件,以及记录图片路径的 list 文件。txt 每行格式是class x_center y_center width height,坐标都归一化到 0~1。所以第一步就是把 XML 转成 YOLOv5 能吃的格式。
常见的转换工具是xml2yolo.py,你也可以直接用项目提供的 maketxt.py 来完成部分工作。下面是我常用的一段转换脚本,它会遍历所有 XML,解析bndbox里的xmin ymin xmax ymax,然后除以图片宽高得到归一化坐标。
import xml.etree.ElementTree as ET import os def convert_annotation(xml_path, class_names, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) out_lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.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) x_center = (xmin + xmax) / 2.0 / width y_center = (ymin + ymax) / 2.0 / height w = (xmax - xmin) / width h = (ymax - ymin) / height out_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if out_lines: base = os.path.basename(xml_path).replace('.xml', '.txt') with open(os.path.join(output_dir, base), 'w') as f: f.write('\n'.join(out_lines))这段脚本里class_names是类别列表,比如['fire'],顺序要和训练时 data.yaml 里的类别顺序一致。注意宽高相除必须用浮点数,否则归一化坐标会丢失小数点。脚本只处理bndbox为矩形框的 VOC 标注,如果 XML 里有polygon多边形标注,需要额外转成外接矩形。
2.1.1 maketxt.py 的作用
maketxt.py 这类脚本负责生成 train.txt、val.txt、test.txt。这些 txt 文件里每一行是图片的绝对路径或相对路径。YOLOv5 训练时通过--data dataset.yaml指定数据集配置,yaml 里的train和val字段直接指向这些 txt 文件。手动划分时要注意随机性,并且保证同图对应的 txt 标注和数据划分归属一致。
训练前还要检查两件事:第一,所有图片路径不能有中文或空格,否则 dataloader 解析会出问题;第二,标注 txt 不能为空文件,空文件会导致KeyError或者训练 loss 变成 nan。我一般会用下面这行命令快速检查一个标注文件内容。
cat labels/train/123.txt # 输出示例: 0 0.5123 0.3845 0.1234 0.25672.2 数据划分的坑
项目里列出的 trainval.txt、train.txt、val.txt、test.txt 就是常见的四份划分。trainval 是训练加验证的总集,train.txt 从里面再分一部分,test.txt 单独拿出来当最终评测集。比例通常按 8:1:1。划分要避免同一场景的连续帧全部落在训练集,而另一段视频的帧全落在测试集。最好按视频片段分组划分,不然会高估模型泛化能力。我一般先把所有图片按文件名排序,然后用random.seed(42)实现可复现的随机打乱。
import random random.seed(42) paths = [line.strip() for line in open('trainval.txt')] random.shuffle(paths) split_idx = int(len(paths) * 0.9) train_paths = paths[:split_idx] val_paths = paths[split_idx:] with open('train.txt', 'w') as f: f.write('\n'.join(train_paths)) with open('val.txt', 'w') as f: f.write('\n'.join(val_paths))这里random.seed(42)固定随机种子,保证后续复现相同划分。shuffle 之后取前 90% 做训练集,后 10% 做验证集。注意如果类别不平衡,比如火焰区域只有 200 个框,背景图有 2000 张,那么验证集里可能一张火焰都没有。建议用分层抽样,先按标注文件里是否有类别来分层。
3. YOLOv5 模型训练:从预训练权重到火焰类别
3.1 选哪个模型尺寸
YOLOv5 官方代码库按参数量从小到大有 s、m、l、x 四个版本,还有 n 和 t 两个更小版本。火焰检测任务里,火焰通常是大目标而且纹理粗糙,不需要特别大的特征提取能力。我用 YOLOv5s 就能在 640 分辨率下跑出不错的效果,推理速度在 RTX 3060 上能到 80 FPS。如果部署目标是树莓派或嵌入式设备,YOLOv5n 更合适,但精度会掉 3~5 个 mAP。
模型结构选型时可以看下面这个对比表:
| 模型 | 参数量 | 训练显存占用(batch 16, 640px) | 推理延迟(RTX 3060) | 适用场景 |
|---|---|---|---|---|
| YOLOv5n | 1.9M | 约 4GB | 约 8ms | 嵌入式边缘设备 |
| YOLOv5s | 7.2M | 约 8GB | 约 12ms | 常规服务器或 PC |
| YOLOv5m | 21.2M | 约 12GB | 约 20ms | 精度优先但显存充足 |
| YOLOv5l | 46.5M | 约 20GB | 约 35ms | 大模型或离线分析 |
火焰本身是强语义目标,surrounding 环境的光照变化对浅层特征影响大,所以不能只看参数量,还要做几次小实验对比 s 和 m 的验证集 mAP。我一般先用 s 训练 20 个 epoch 看趋势,如果 val mAP 增长斜率很陡,再换 m 练到底。
3.2 data.yaml 与超参数
训练前要准备一个fire.yaml,内容如下。
train: /path/to/train.txt val: /path/to/val.txt test: /path/to/test.txt nc: 1 names: ['fire']nc表示类别数,这里只有火焰一类。如果还想识别烟雾,就写成nc: 2和names: ['fire', 'smoke']。注意names的顺序必须和标注 txt 里的类别 id 对应,id 从 0 开始。很多初学把类别名写混,导致训练时 loss 直接爆掉。
超参数文件一般用hyp.scratch-low.yaml,里面包含 lr、momentum、weight_decay 等。火焰检测这类单类目标,不需要调整太多,但有两个参数值得改:anchors和fl_gamma。官方默认的锚框是针对 COCO 的 80 类物体设计的,火焰形状接近圆形或扁长形,我一般会在训练前用 k-means 重新聚类锚框。
YOLOv5 自带聚类工具,但手动改起来也简单。下面这段代码计算当前训练集所有标注框的宽高比,帮你判断默认锚框合不合适。
import numpy as np boxes = [] with open('/path/to/train.txt') as f: for line in f: img_path = line.strip() label_path = img_path.replace('/images/', '/labels/').replace('.jpg', '.txt') with open(label_path) as lf: for line in lf: parts = line.strip().split() w = float(parts[3]) h = float(parts[4]) boxes.append([w, h]) boxes = np.array(boxes) print('平均宽高比:', (boxes[:, 0] / (boxes[:, 1] + 1e-6)).mean()) # 输出比如: 平均宽高比: 1.32如果平均宽高比接近 1,说明火焰框接近正方形,默认锚框可以不动。如果大于 2,说明有很多横向火焰,需要调整锚框。调整锚框直接在模型 yaml 里改,例如anchors: [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]],前一行是小尺度特征图的锚,后两行是中大尺度。
3.3 训练命令与迁移学习
训练命令如下。
python train.py \ --data fire.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 50 \ --cache ram \ --project runs/fire_train参数含义:--weights yolov5s.pt是加载官方 COCO 预训练权重,千万不要随机初始化,火焰和 COCO 里的火把、火堆有一定相似性,迁移学习能大幅减少收敛时间。--img 640表示训练分辨率,如果显存不够可以降到 512,但火焰小目标较多时不要低于 416。--cache ram把图片全部缓存在内存,训练速度明显加快,但需要看你内存是否够用。
训练启动后,每 10 个 epoch 会保存一次 checkpoint。观察runs/fire_train/exp/results.csv里的val/box_loss和val/cls_loss。火焰是单类,cls_loss比box_loss更重要。如果val/cls_loss在 30 轮后还在下降,说明还没收敛,可以增加到 100 epoch。
3.3.1 训练过程中的常见报错
最常见的报错是CUDA out of memory,解决办法是调低 batch size 或减小--img。还有一个容易忽视的是assert (img_path.suffix in ('.jpg', '.jpeg', '.png'))报错,这是因为图片格式不是这三种,或者图片路径指向了不存在的位置。另外,如果标注里有目标框超出图片边界,训练时会出现AssertionError: invalid shape (0, ...)。这时候要检查 XML 里坐标是否大于图片宽高,有些标注工具会生成负数坐标,需要过滤掉。
# 过滤超界和非法框的代码片段 def filter_box(xmin, ymin, xmax, ymax, width, height): if xmin < 0 or ymin < 0 or xmax > width or ymax > height: return False if xmax - xmin < 1 or ymax - ymin < 1: return False return True4. 火焰视频识别:摄像头/视频文件推理与结果输出
4.1 使用 detect.py 做视频推理
训练好的best.pt可以直接用于视频检测。YOLOv5 的detect.py支持输入图片、视频文件、文件夹或摄像头编号。运行命令如下。
python detect.py \ --weights runs/fire_train/exp/weights/best.pt \ --source test_video.mp4 \ --conf-thres 0.4 \ --iou-thres 0.5 \ --view-img \ --save-txt \ --save-conf--conf-thres 0.4控制检测置信度阈值,低于 0.4 的预测会被过滤。火焰检测阈值不宜太高,因为小火苗容易被遮挡,建议先设 0.25 看漏检情况,再逐步上调。--iou-thres 0.5是 NMS 的 IoU 阈值,重叠框较多时可以调高到 0.6。--save-txt会把检测到的类别和坐标保存成 txt,--save-conf同时保存置信度。
运行后输出视频会保存在runs/detect/exp/目录。如果想在显示窗口中看到实时检测,加--view-img。如果是在无显示环境的服务器上跑,不要加这个参数,否则会报错cv2.error: ... is not a valid window。
4.2 摄像头实时检测的延迟控制
接摄像头时把--source 0改成摄像头索引号。要注意摄像头帧率通常为 30 FPS,而 YOLOv5 推理每帧可能耗时 20~40ms,加上前处理和后处理,瓶颈常常在图像缩放和 NMS 上。可以用--imgsz 480降低输入分辨率,牺牲一点精度换速度。另一个思路是跳帧检测,比如每 3 帧只检测一帧,中间帧沿用上一帧结果。
import cv2 import torch model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/fire_train/exp/weights/best.pt') model.conf = 0.35 model.iou = 0.5 cap = cv2.VideoCapture(0) frame_skip = 2 frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_count % frame_skip == 0: results = model(frame) bboxes = results.xyxy[0].cpu().numpy() # 只保留 class 0 火焰 fire_boxes = bboxes[bboxes[:, -1] == 0] for x1, y1, x2, y2, conf, cls in fire_boxes: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(frame, f'fire {conf:.2f}', (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) frame_count += 1 cv2.imshow('fire detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这里torch.hub.load加载自定义权重,model.conf和model.iou直接赋值作为后处理阈值。frame_skip设为 2 意味每 3 帧检测一次,可以显著降低 CPU 或嵌入式设备的负载。注意results.xyxy[0]返回的数组按列依次是 x1、y1、x2、y2、confidence、class,这六个字段在后续绘制和统计中很有用。
4.3 视频检测结果后处理
检测完的视频可以输出为 MP4,也可以逐帧保存火焰框的坐标用于分析。如果你要把结果接入告警系统,一般会把火焰框的中心坐标、面积和置信度写入 CSV。火焰面积占比是一个重要的告警指标,因为远处小火苗可能在图像里只有 5×5 像素,直接告警会造成太多误报。
import csv with open('fire_log.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['frame', 'x1', 'y1', 'x2', 'y2', 'conf', 'area_ratio']) for frame_idx, res in enumerate(all_results): for box in res.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf = box[:5] area_ratio = (x2 - x1) * (y2 - y1) / (frame_width * frame_height) writer.writerow([frame_idx, x1, y1, x2, y2, conf, area_ratio])area_ratio是检测框面积占整帧图像的比例,阈值可以设为 0.01,低于该值的火焰框视为噪声,不触发告警。这个值需要根据摄像头安装距离和角度调整,如果摄像头离地面 5 米,远处小火苗的面积占比会非常小,阈值要放宽。
5. 火焰误报抑制:置信度阈值与帧间滤波的工程调优
火焰识别落地最大的敌人不是漏检,而是误报。红色的光照、反光的汽车表面、晚霞甚至烧红的铁块都可能被识别成火焰。YOLOv5 只从外观学习,没有时间上下文,所以需要加入工程化手段过滤误报。
5.1 连续帧稳定性判据
单帧置信度高不一定是真火,但一个目标在多帧中持续出现且位置连续变化,是真火的概率就大得多。我常用一个简单的状态机:每个目标框维护一个连续命中计数hits,如果连续 N 帧都检测到火焰,才输出告警;如果中间丢失 M 帧,计数清零。N 取 5、M 取 3 比较合适,视频帧率 30 FPS 时相当于目标持续出现 0.17 秒才触发。
class FireTracker: def __init__(self, max_miss=3, hit_threshold=5): self.max_miss = max_miss self.hit_threshold = hit_threshold self.hits = 0 self.misses = 0 def update(self, detected): if detected: self.misses = 0 self.hits += 1 else: self.hits = 0 self.misses += 1 return self.hits >= self.hit_threshold and self.misses <= self.max_missdetected是当前帧是否有任意火焰框通过了置信度阈值。这个 tracker 没有做目标关联,只统计全画面是否持续出现火焰,适合单一火焰场景。如果画面里多个火焰同时出现,需要用 IoU 做目标框的跨帧匹配,比如用simple_iou判断前后帧中心点距离小于某个像素的框是否是同一个目标。
5.2 动态置信度与闪烁检测
火焰有闪烁特性,火焰区域的光强在时间轴上呈现较高频率波动。可以计算检测框内图像的平均亮度方差,方差高说明是火焰,方差低则可能是静态红色物体。
import numpy as np def fire_flicker_score(crop_frame): gray = cv2.cvtColor(crop_frame, cv2.COLOR_BGR2GRAY) temporal_var = np.var(gray) return temporal_var > 30 # 经验阈值这个temporal_var计算的是当前帧检测区域内像素方差,不是时间方差。更准确的做法是在同一位置连续取 5 帧灰度图,计算每个像素亮度随时间的变化方差,再求平均。这个步骤计算量不大,作为二次判决条件可以使误报率降低约一半。
5.3 输出告警动作
抑制误报之后,真正的火焰事件可以触发告警。调用 trigger.py 脚本发送 HTTP 请求到告警服务,或者写入日志。下面是调用本地告警接口的一个示例。
curl -X POST http://localhost:8080/api/fire-alert \ -H "Content-Type: application/json" \ -d '{"camera_id": 1, "bbox": [120, 342, 250, 500], "confidence": 0.93, "area_ratio": 0.18}'告警参数里camera_id区分不同摄像头,bbox用于管理人员定位火焰位置。注意告警接口要做好频率限制,比如同一摄像机每 10 秒最多发送 1 条告警,否则处置人员会被连续消息淹没。
最后提一个调优技巧:训练时用--multi-scale让模型适应不同输入尺寸,推理时再用固定 640 分辨率,防御视角变化引起的误检。同时把验证集里最容易误报的红车或夕阳图片单独建一个hard_examples.txt,用这些图片反复测试调整conf-thres到 0.45~0.5 区间,直到误报数和漏检数达到平衡即可收工。
本文还有配套的精品资源,点击获取