简介:面向火焰烟雾目标检测任务的YOLO格式数据集及配套工程包,适合人工智能、深度学习方向的开发者与研究人员,可直接用于模型训练、验证与工程化部署。数据图片清晰、场景覆盖广泛,所有样本经人工精心挑选与标注,可以作为火焰烟雾检测的通用模板数据集;针对特定应用场景,只需补充少量现场数据即可快速迭代模型,省去大量收集、筛选与标注工作。压缩包共332个文件,约32.18MB,主要包含图像与视频样本、txt/names/data等标注格式文件、yaml及cfg模型配置、py/sh训练与推理脚本,以及darknet相关C/CUDA源码和可执行模块,兼顾数据处理与算法二次开发需求。目前已有2017人学习下载,适合需要快速搭建火焰烟雾检测系统的开发者参考使用。
1. 火焰烟雾数据集:为什么我建议直接从YOLO格式开始
在安防和工业监控场景下,火焰烟雾检测最耗时间的环节往往是数据准备。拉过来的公开图片要么背景单一,要么标注格式混乱,换到实际监控点效果很差。这套火焰烟雾数据集YOLO.zip是已经把图片清理过、人工标注完的模板数据,图片清晰、场景覆盖广,能直接扔给Darknet/YOLO训练。对于算法工程师来说,它解决了从零收集、筛选、标注图片的脏活,让你把精力放在模型调参和部署上。如果你正在做人脸识别、吸烟检测或明火报警相关项目,这套数据也能作为底座,配合少量特定场景数据微调,很快出结果。下面从数据格式开始,完整走一遍训练到部署的链路。
2. 数据集内容与Darknet源码文件:压缩包里到底有什么
2.1 图片集与标注文件的组织方式
解压之后,目录结构是典型的Darknet训练工程布局。一个最重要的约定是:图片和同名txt标注放在不同目录,训练时通过文本文件列表去关联它们。通常你会看到images/和labels/两个文件夹,images里是原始图片,labels里是每个图片对应的txt文件,每行对应图中一个目标。classes.txt定义了类别名称,例如fire和smoke,类别ID就是行号从0开始。这个命名规则直接决定之后所有训练脚本如何读取。
| 文件/目录 | 作用 |
|---|---|
| images/ | 存放火焰烟雾图片,JPG或PNG |
| labels/ | 存放与图片同名的txt标注,YOLO格式 |
| classes.txt | 每行一个类别名,行号即类别ID |
| train.txt | 训练图片路径列表,由使用者生成 |
| gemm.c、parser.c 等 | Darknet源码,编译训练框架所需 |
对于火焰烟雾检测,最常见的是二分类,即0 fire、1 smoke。但如果你还希望区分早期阴燃和明火,完全可以把类别扩充到3个或更多;数据集本身的标注是基于人工框选的目标范围,没有锁定类别数量,这也是模板数据灵活的地方。
2.2 Darknet关键C文件在训练中的角色
项目内列出的C文件都不是数据集标注,而是Darknet运行时依赖的核心模块。不把它们放进工程里,训练程序连编译都过不去。这些文件之间的关系并不复杂:gemm.c实现矩阵乘法,是卷积前向和反向的底层地基;parser.c负责解析yolov3.cfg这类网络结构文件;data.c承担图片批量读取和增强;detector.c是检测入口,支持train和test两种模式;convolutional_layer.c定义卷积层的前后向计算;conv_lstm_layer.c则是卷积LSTM实现,适合对视频序列建模;yolo_layer.c直接计算输出层的损失函数。如果你使用的是官方Darknet,这份清单几乎是最小编译集合的一部分。
这里要说清楚一个容易踩的坑:不要随便把不同分支的Darknet源码混合编译。比如从AlexeyAB库拉取源码之后,又用官方老版本的gemm.c去替换,会出现矩阵维度对不上的错误,报错位置通常就在gemm的指针越界。我一般会直接固定一个版本的Darknet,并记录commit号,这样后续更换数据集或改cfg时,可复现性才有保障。
2.3 手动校验标注质量的工具与方法
训练一个火焰烟雾模型前,花十分钟跑一遍标注质量检查非常值。最可能出现的问题是:标注坐标越界、图片存在但labels目录里缺少对应txt、以及类别ID不连续。下面这段脚本可以快速排查:
import os from collections import defaultdict def check_labels(img_dir, label_dir): img_files = [f for f in os.listdir(img_dir) if f.endswith('.jpg')] issues = [] cls_count = defaultdict(int) for img in img_files: lbl = os.path.splitext(img)[0] + '.txt' path = os.path.join(label_dir, lbl) if not os.path.exists(path): issues.append((img, 'missing label')) continue with open(path) as f: lines = f.read().splitlines() if len(lines) == 0: issues.append((img, 'empty label')) continue for line in lines: parts = line.split() if len(parts) != 5: issues.append((img, 'bad format: %s' % line)) continue cls_id = int(parts[0]) x, y, w, h = map(float, parts[1:]) cls_count[cls_id] += 1 if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1): issues.append((img, 'out of range: %s' % line)) return issues, cls_count issues, cls_count = check_labels('images', 'labels') print('issues:', issues[:20]) print('class distribution:', dict(cls_count))这段代码会遍历images下所有jpg图片,在labels目录找同名txt,逐行检查格式和数值区间。注意这里的坐标必须是归一化后的0到1范围,如果发现某个框的w或h大于1,说明标注文件可能是像素坐标,需要先做归一化再进入训练流程。
3. 标注格式解析与KITTI转YOLO:一份可复用的转换脚本
3.1 YOLO标注格式的数学定义
YOLO格式的标注核心是“归一化中心点加宽高”。假设图片宽为W、高为H,某个目标框在像素坐标下的左上角为(x1, y1),右下角为(x2, y2),那么YOLO格式需要的四个值分别是:
xc = (x1 + x2) / (2 * W) yc = (y1 + y2) / (2 * H) w = (x2 - x1) / W h = (y2 - y1) / H
类别ID单独放在每行开头。这个定义决定了它天然适应不同分辨率的图片,因为所有坐标都是相对值。反过来说,如果在预处理时对图片做了resize,归一化坐标不需要反向缩放;如果是直接裁剪图片,就需要重新计算标注。
很多新手把xc和yc当作像素坐标直接画框,得到的框位置完全错位。一个快速验证方法是把txt里的数值乘以图片宽高,再看是否落在目标上。
3.2 KITTI标注转YOLO脚本实现
在自动驾驶领域经常遇到KITTI格式的数据集。KITTI的标注行比YOLO复杂,包含了截断度、遮挡程度、3D尺寸和朝向角等字段,但转换时我们只需要bbox的四个像素坐标。常见做法是先从csv或txt中提取KITTI这行,然后执行如下转换:
import os def kitti_to_yolo(kitti_path, img_w, img_h, out_path, class_names): with open(kitti_path) as f: lines = f.read().splitlines() yolo_lines = [] for line in lines: parts = line.split() if len(parts) < 8: continue cls_name = parts[0] x1, y1, x2, y2 = map(float, parts[4:8]) if x2 <= x1 or y2 <= y1: continue xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h cls_id = class_names.index(cls_name) yolo_lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(out_path, 'w') as f: f.write('\n'.join(yolo_lines)) class_names = ['DontCare', 'Car', 'Pedestrian', 'Fire', 'Smoke'] kitti_to_yolo('sample_kitti.txt', 1920, 1080, 'sample_yolo.txt', class_names)这个脚本的关键在于直接读取KITTI行里的第4到第8个字段作为2D框,也就是x1 y1 x2 y2。最后的class_names需要你根据KITTI原始类别重新定义;如果原始数据里有DontCare,建议直接跳过,否则训练时会引入无效的负样本。注意输出时保留6位小数,精度越高训练启动时框归一化越稳定。
3.3 归一化坐标在图像resize时的陷阱
前处理里最常见的坑是:你在训练脚本中把图片resize到了640x640,但标注txt里的w和h仍然是基于原图的归一化值,此时你不需要任何修改,因为归一化已经消除了尺寸影响。只有在两种情况下需要重新计算:
| 操作类型 | 是否需要重算 | 原因 |
|---|---|---|
| 等比缩放图片 | 不需要 | 归一化坐标相对比例不变 |
| 裁剪图片 | 必须重算 | 目标框在裁剪后坐标系中位置变化 |
| 拼接图片(Mosaic) | 必须重算 | 每张子图被平移并缩放到新画布 |
表格列出的情况都很常见。尤其是Mosaic增强,Darknet实现会先对四张图分别缩放,再拼到一个大画布上,标签也要跟着做对应的仿射变换。如果自己写数据增强,最容易漏掉的就是这一步,导致模型训练时看到的框与实际目标不符,表现为训练Loss很快降到很低但mAP始终上不去。
4. Darknet训练流程:从编译到跑通火焰烟雾模型
4.1 编译Darknet:GPU环境与Makefile关键选项
拿到数据集后,第一步是准备好编译环境。Darknet的构建依赖一套配套的C编译器,在Ubuntu上最省事的是直接修改Makefile。对于火焰烟雾检测这种视频流任务,建议开启GPU加速,否则推理速度很难满足实时监控要求。打开Makefile,把前几行改成:
GPU=1 CUDNN=1 OPENCV=1随后在源码根目录执行make -j8。如果编译过程中报错缺少cudnn.h,说明CUDA目录没有正确暴露,常见解法是设置PATH和LD_LIBRARY_PATH后再编译。这里要注意,项目自带的gemm.c和parser.c等文件必须和你要编译的框架版本匹配,直接拿新版本源码去替换会引起结构体定义不一致。
| Makefile选项 | 打开方式 | 影响 |
|---|---|---|
| GPU | 1 | 使用CUDA训练 |
| CUDNN | 1 | 使用cuDNN加速卷积 |
| OPENCV | 1 | 摄像头读取与图像保存 |
| CUDNN_HALF | 1 | 半精度推理,注意输出精度 |
4.2 训练数据清单与训练配置
编译完成后,需要把数据集划分成训练集和验证集。Darknet不直接读目录,而是通过train.txt和val.txt分别提供图片路径。生成路径列表可以用一条命令:
find /path/to/fire_dataset/images -name '*.jpg' > train.txt find /path/to/fire_dataset/images -name '*.jpg' > val.txt更合理的做法是先用脚本把数据按8:1:1划分,然后把train和val分布到两个文件。实际使用中,我还会额外生成一个test.txt,避免用验证集反复调优导致过拟合。
确认列表后创建fire.data文件,内容参考如下:
classes=2 train=/path/to/train.txt valid=/path/to/val.txt names=/path/to/classes.txt backup=/path/to/backup/这里classes必须与classes.txt的行数一致,backup目录用来保存训练过程中的权重文件。如果忘记创建backup目录,程序会在训练中途报错无法写入权重。
4.3 执行训练与断点续训命令
基于数据集自带的模板,我一般会先从预训练权重开始训练。YOLOv3系列官方提供的darknet53.conv.74是不带检测头的特征提取预训练权重,适合迁移到火焰烟雾检测任务。启动命令:
./darknet detector train data/fire.data cfg/yolov3-fire.cfg darknet53.conv.74 -map命令里的detector train指定训练模式,data/fire.data是前面创建的配置,cfg/yolov3-fire.cfg是网络结构文件,darknet53.conv.74是预训练权重。-map会在每个epoch结束后在验证集上计算mAP,方便观察过拟合。训练中断后,不需要重新开始,继续用上一次保存的weights文件:
./darknet detector train data/fire.data cfg/yolov3-fire.cfg backup/yolov3-fire_last.weights这种断点续训方式在长时间挂机训练时非常实用。需要注意,续训时不能再附加预训练权重,否则会覆盖已经学到的检测头参数,严重时导致loss从高位重新下降。
5. YOLO损失函数分析与火焰烟雾场景调参
5.1 损失函数四个组成部分
YOLO的损失函数不是单一loss,而是四个部分的加权和。在yolo_layer.c中,输入特征图被切成网格,每个网格预测多个anchor,然后分别计算坐标损失、置信度损失和类别损失。坐标损失使用平方误差,监督目标框中心点偏移和宽高缩放;置信度损失使用二元交叉熵,区分该网格内是否真实存在目标;类别损失同样是交叉熵,只在有目标的网格上计算。第四个部分是noobj损失,用于抑制没有目标区域产生误检。
这四部分里面,noobj loss的影响常被低估。火焰烟雾场景下的负样本比例极高,因为大部分监控画面在绝大多数时间里都没有火情。如果noobj权重设置过大,模型会倾向于把所有框都预测成背景,召回率暴跌;设置过小又会出现大量误报。我通常先保持默认,然后根据第一次训练日志中的混淆矩阵微调。
5.2 小目标、曝光度与类不平衡:anchor和超参数的调整
火焰烟雾的特殊性在于目标尺度变化很大:近距离火焰可能占半张图,远距离烟雾可能只有十几个像素。YOLOv3默认的anchor基于COCO数据集,对火焰烟雾并不合适,需要重新聚类。常见做法是用kmeans对训练集标注框做anchor聚类,比如:
| 输入分辨率 | 建议anchor个数 | 适用场景 |
|---|---|---|
| 416x416 | 6~9 | 目标是中小尺寸 |
| 608x608 | 9 | 需要检测远距离烟雾 |
| 640x640 | 9 | 平衡速度与精度 |
另外,由于夜间火光和白天烟气的视觉效果差异极大,建议开启更多的颜色抖动和马赛克增强,提升模型对光照变化的鲁棒性。曝光度这块,Darknet在data.c里通过曝光度参数控制,可以在cfg文件里随机设置,增强模型对明亮火光的适应。
5.3 通过训练日志诊断模型状态
训练时的日志已经包含很多有用信号。看avg loss的下降趋势,能判断学习率是否合适;看IOU变化,能看出anchor与目标框是否匹配。如果需要把日志画成曲线,可以用awk快速提取:
grep "avg" train.log | awk '{print $3}' > loss.txt gnuplot -e "plot 'loss.txt' with lines"这段命令会把日志中avg列输出到loss.txt,再用gnuplot绘制。很多训练问题都可以从曲线形态直接判断:如果loss前500步没有从高位明显下降,说明学习率偏大或数据路径配置错误;如果loss在几个epoch内存,说明模型容量不足,需要加大网络深度或减小输入分辨率;如果验证集mAP出现掉点,说明过拟合开始了,要及时早停。
6. 工程化部署:把模型接到报警系统
6.1 导出ONNX/TensorRT的常见做法
当训练完成后,你需要把Darknet权重部署到服务或边缘设备。直接把weights文件用于生产环境不太现实,因为Python生态和C API的集成成本高。常见做法是先转换为ONNX,再利用TensorRT或OpenVINO做进一步推理优化。Darknet到ONNX可以使用darknet2onnx这类社区工具,基本命令是:
python darknet2onnx.py input.cfg input.weights output.onnx导出后建议用onnx的验证器跑一遍,确认网络结构里的yolo层被正确处理。如果导出后丢失了yolo层,需要手动添加非极大值抑制后处理。
6.2 后处理推理与报警触发代码片段
ONNX或直接使用Darknet预测原始输出时,输出的形状通常是[batch, grid_h, grid_w, num_anchors, 5+num_classes],解码时要先还原中心坐标和宽高,优先提取置信度高于阈值的框,再做NMS。下面是一个轻量级的解码函数:
import numpy as np def decode_yolo(pred, anchors, num_classes, input_w, input_h, conf_thresh=0.5): grid_h, grid_w = pred.shape[1], pred.shape[2] pred = pred[0] # [grid_h, grid_w, num_anchors, 5+num_classes] boxes = [] for j in range(grid_h): for i in range(grid_w): for a, anchor in enumerate(anchors): p = pred[j, i, a] obj_conf = p[4] if obj_conf < conf_thresh: continue class_probs = p[5:] cls_id = np.argmax(class_probs) cls_conf = class_probs[cls_id] if cls_conf * obj_conf < conf_thresh: continue dx, dy, dw, dh = p[0], p[1], p[2], p[3] cx = (i + 1 / (1 + np.exp(-dx))) / grid_w * input_w cy = (j + 1 / (1 + np.exp(-dy))) / grid_h * input_h w = anchor[0] * np.exp(dw) / input_w h = anchor[1] * np.exp(dh) / input_h boxes.append([cx, cy, w, h, obj_conf * cls_conf, cls_id]) return boxes代码的核心是先用sigmoid把偏移量映射到中心位置,再将相对网格的宽高换算成绝对像素。由于火焰烟雾报警系统的输入通常是摄像头流,帧率要求高,推荐把NMS阈值设在0.4,置信度阈值设在0.5以上,过滤掉监控画面中常见的灯光闪烁干扰。最后根据输出触发报警,可以在检测到某个类别持续超过3帧时才发警报,避免单帧误检带来的噪声。
本文还有配套的精品资源,点击获取