ARTICLE DETAIL

资讯详情

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

火焰烟雾数据集双格式标注与YOLOv8训练实战指南

火焰烟雾数据集双格式标注与YOLOv8训练实战指南 简介面向需要火焰与烟雾检测数据的算法工程师、研究者和相关专业学生这份数据集可直接用于训练YOLO目标检测模型典型场景包括消防预警、安全监控、森林防火等。资源描述覆盖18800张图片并提供YOLO与VOC两种标注格式压缩包内收录2000个XML标注文件以VOC格式呈现包体大小765.03MB配合图片素材即可进行模型训练无需额外转换格式。已有1428人学习/下载说明该数据集在同类资源中拥有一定参考价值。从文件构成看数据涵盖火灾现场实拍、视频截帧以及不同来源的火焰/烟雾样本标注包含目标框与类别信息既适合目标检测入门练习也能作为实际消防预警项目的训练基础对需要快速获得高质量火焰烟雾数据的团队而言可明显节省采集与标注的时间成本。1. 火焰烟雾数据集双格式标注解决了谁的痛点做安防预警和消防检测的工程师应该都有同感模型结构不是瓶颈数据才是。火焰和烟雾这种目标边界模糊、形态多变、背景还经常是逆光或夜间自己标注几千张图标注一致性很难保证标着标着就怀疑人生。这份18800张图片的火焰烟雾数据集最直接的价值是省掉数据采集和标注环节而且同时给出YOLO格式的TXT标注和VOC格式的XML标注拿到手就能用。适合正在做火灾检测、烟火识别、无人机巡检、城市监控预警这类项目的从业者无论你熟悉YOLOv5/YOLOv8还是跑过Faster R-CNN的VOC流程都能直接对接不需要在格式转换上浪费一周时间。一句话说清楚这份资源解决的痛点是「没数据」和「格式不对口」两个问题一起出现的尴尬。接下来我会从标注格式拆解、训练前的数据准备、参数配置到踩坑排查全流程走一遍把它真正用起来。2. 看懂YOLO的TXT和VOC的XML坐标体系与解析脚本2.1 图像与标注文件的全景结构拿到数据集先别急着开训第一步是搞清楚文件组织方式。常见做法是按images和annotations两个大目录分好图片和标注一一对应文件名前缀相同。具体来说图片是JPG格式标注同时存在TXT和XML两份TXT是归一化坐标的YOLO格式XML是像素坐标的VOC格式。也就是说同一个标注内容用两种坐标系各表达了一遍。文件清单大致是下面这个结构我通常第一件事就是拉出整棵目录树确认fire_smoke_dataset/ ├── images/ │ ├── fire_001.jpg │ ├── fire_002.jpg │ └── smoke_001.jpg └── annotations/ ├── fire_001.txt ├── fire_001.xml ├── fire_002.txt ├── fire_002.xml └── smoke_001.txt └── smoke_001.xmlTXT和XML并存的好处是省去了自己写转换脚本的时间但代价是两份文件可能出现不一致比如某个目标只在TXT里存在、XML里丢了。所以拿到资源后先跑一遍统计校验脚本确认每张图片的两种标注文件都能对上。这一步很多人都跳过结果训练到一半发现loss不降回去查才发现标注文件是残缺的非常耽误进度。2.2 两种坐标格式的差异归一化与像素坐标YOLO的TXT标签格式是每行一个目标五个字段分别是类别ID、归一化的中心点x、中心点y、归一化的框宽、归一化的框高。所谓归一化就是把像素坐标除以图片本身的宽和高把所有数值压缩到0到1之间这样模型在不同分辨率下训练时坐标尺度一致。一个典型的火焰标注长这样0 0.485937 0.453125 0.231250 0.171875这里0代表fire这个类别后面的四个浮点数分别是目标中心相对图片宽高的比例和框宽高比例。注意如果图片宽是640、高是480那么实际的目标中心像素坐标是(0.485937 * 640, 0.453125 * 480)换算出来大概是(311, 217)框宽约148像素、高约82像素。VOC的XML则直接把像素坐标写死用bndbox节点把左上角和右下角的像素坐标给出。同一个目标在XML里长这样annotation folderimages/folder filenamefire_001.jpg/filename size width640/width height480/height /size object namefire/name bndbox xmin231/xmin ymin207/ymin xmax391/xmax ymax297/ymax /bndbox /object /annotation我用xmin/ymin换算了一下中心点确实是((231391)/2, (207297)/2) (311, 252)和TXT算出来的(311, 217)差了一截——这里要提醒一下实际使用时以数据集的原始标注为准如果发现同类目标两种格式对不上说明其中一份文件在生成时出了问题需要用可视化脚本逐一排查。2.3 用Python脚本解析标注并统计类别分布拿到数据后我一般先跑一个统计脚本把每个类别的目标总数、每张图的平均目标数、有哪些异常文件全部列出来。这样对数据质量心里有底也提前知道类别是否均衡。下面这个脚本可以直接抄路径参数按你的目录改一下import os import glob from collections import Counter img_root images txt_root annotations txt_files glob.glob(os.path.join(txt_root, *.txt)) class_counter Counter() img_with_boxes 0 empty_txt [] line_error_files [] for txt_path in txt_files: base os.path.splitext(os.path.basename(txt_path))[0] img_path os.path.join(img_root, base .jpg) if not os.path.exists(img_path): print(f[缺失图片] {img_path}) continue with open(txt_path, r) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_txt.append(base) continue valid_line_count 0 for line in lines: parts line.split() if len(parts) ! 5: line_error_files.append(base) break cls_id int(parts[0]) # 只统计坐标值在合法范围内的目标越界说明标注有问题 x_center float(parts[1]) y_center float(parts[2]) w float(parts[3]) h float(parts[4]) if not (0 x_center 1 and 0 y_center 1 and 0 w 1 and 0 h 1): line_error_files.append(base) break class_counter[cls_id] 1 valid_line_count 1 if valid_line_count 0: img_with_boxes 1 print(类别ID分布:, dict(class_counter)) print(有效标注图片数:, img_with_boxes) print(空标注文件数:, len(empty_txt)) print(疑似格式错误文件数:, len(line_error_files)) if empty_txt: print(空标注示例:, empty_txt[:5]) if line_error_files: print(格式错误示例:, line_error_files[:5])这段脚本的关键在于在解析TXT整行的同时顺带做了坐标合法性校验——如果x_center、width这些归一化数值不在0到1范围内说明标注文件有越界错误。同时把空标注文件和缺失图片的文件名都单独落下来方便后续处理。类别ID的统计结果可以直接确认这个数据集的类别数我一般会打印出来核对如果出现你没有预期到的类别ID那份数据里的类别映射就和你想的不一样后面训练时yaml配置需要对应调整。跑完这个脚本你就能回答三个问题总共多少张带标注的图、每类目标多少、哪些文件需要清理。这是训练前性价比最高的一步不花十分钟后面省掉的都是排查的时间。3. 训练前的数据准备目录拆分、yaml配置与格式统一3.1 训练集与验证集的划分方案数据集标注质量没问题第二步就是把数据拆成训练集和验证集。大部分YOLO框架默认读取的目录结构是images/train、images/val、labels/train、labels/val这种约定而且划分时要注意同一个图片的TXT和XML都要跟着走不能只移动图片而把标注落在原地。划分比例我一般用8:218800张图拆出约15000张训练、3800张验证。火焰烟雾场景中验证集不需要太大因为验证集只负责看loss和mAP趋势太大反而拉长每个epoch的验证时间。划分时我习惯用分层抽样思想先按类别做一次统计确保验证集中的类别比例和全集一致避免验证集里全是火焰、训练集里全是烟雾这种极端情况。下面这个脚本用随机打散加固定随机种子来做划分顺便把空标注文件提前过滤掉。注意把所有操作放在同一个seed下保证每次跑结果一致——这算是训练可复现的一个习惯同一次实验划分不同对比实验就失真了import os import random import shutil random.seed(42) base_dir fire_smoke_dataset image_dir os.path.join(base_dir, images) txt_dir os.path.join(base_dir, annotations) split_ratio 0.8 all_images [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(all_images) train_count int(len(all_images) * split_ratio) train_images all_images[:train_count] val_images all_images[train_count:] def copy_sample(img_name, dest_img_dir, dest_txt_dir): base os.path.splitext(img_name)[0] src_img os.path.join(image_dir, img_name) src_txt os.path.join(txt_dir, base .txt) if not os.path.exists(src_txt): print(f[跳过无标注] {img_name}) return shutil.copy(src_img, os.path.join(dest_img_dir, img_name)) shutil.copy(src_txt, os.path.join(dest_txt_dir, base .txt)) os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) for name in train_images: copy_sample(name, dataset/images/train, dataset/labels/train) for name in val_images: copy_sample(name, dataset/images/val, dataset/labels/val) print(f训练集: {len(train_images)} 张, 验证集: {len(val_images)} 张)这里我把TXT标注复制到了labels目录XML没有动因为训练时YOLO系列框架只认TXT。注意copy_sample里没有拷贝XML如果你同时也要做VOC格式的评估或者用Detection Transformer那就把XML一并拷贝到另一个目录。另外那个跳过无标注文件的逻辑是为了防止空文件混进训练集导致模型学到空标签这部分数据宁可不要。3.2 修改data.yaml配置类别名与路径YOLOv5和YOLOv8训练前都要准备一个data.yaml核心内容是指明训练集和验证集的路径、类别数量以及类别名。火焰烟雾数据集通常有两个类别类别名默认是fire和smoke。如果标注统计出来的类别ID是0和1那么yaml里就写成0对应fire、1对应smoke顺序不能反不然模型学到的语义和你想表达的完全相反。一个标准的yaml文件长这样path: /root/project/dataset # 数据集根目录绝对路径或相对路径都可以 train: images/train # 相对path的训练集图片目录 val: images/val # 相对path的验证集图片目录 nc: 2 names: 0: fire 1: smoke写这个文件时有两个常见坑。第一个是path字段的路径写法YOLOv5和YOLOv8对path的解释略有不同v5里如果给了绝对path那么train和val只要写相对路径v8同理但有时v5会读不到相对路径下的文件我一般直接写绝对路径省事。第二个是nc必须和names列表长度一致如果你确认数据集中只有fire和smoke就写nc: 2如果统计出来有第三个类别ID这个数字就得改成3否则训练时会对超出nc范围的类别报错。3.3 如果只想用VOC格式TXT与XML的互转脚本虽然数据集两种格式都给了但有人可能只想用自己的VOC流程做训练或者反过来只想用TXT喂给YOLO。这里给一个我常用的VOC转YOLO的脚本把XML的像素坐标换算成归一化坐标写入TXT。核心计算就是把bndbox的xmin/ymin/xmax/ymax换算为框的宽和高再分别除以图片宽高。import os import glob import xml.etree.ElementTree as ET from PIL import Image xml_dir annotations txt_dir converted_txt os.makedirs(txt_dir, exist_okTrue) class_map {fire: 0, smoke: 1} # 按照已知类别映射如果多类别按顺序扩展 for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): base os.path.splitext(os.path.basename(xml_path))[0] tree ET.parse(xml_path) root tree.getroot() # 从XML的size节点读宽高不用额外打开图片速度快很多 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: print(f[未知类别] {xml_path}: {name}) 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) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 防止转换后坐标越界裁到合法区间 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) w min(max(w, 0), 1) h min(max(h, 0), 1) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(txt_dir, base .txt), w) as f: f.write(\n.join(lines)) print(转换完成输出目录:, txt_dir)脚本中从XML的size节点读取图片宽高直接避免了每次打开图片文件的时间消耗。转换公式是x_center等于左右边界的平均除以宽w等于右减左除以宽y同理。最后的坐标裁剪作用在于有些XML标注本身就存在边界溢出比如xmin是-2这种值转换出来就是负数先把它们截断到合法范围避免后续训练时计算损失函数出现NaN。注意这个裁剪只是兜底如果发现大量标注越界还是要回到原数据检查标注质量而不是靠裁剪硬吞。4. 跑通YOLO训练命令、关键参数与玄学现象排查4.1 第一条训练命令与参数解释数据准备好后就可以用YOLOv8开训了。我习惯直接用YOLOv8的官方命令行接口因为它的参数覆盖比v5更全而且对TXT标注的支持完全原生。训练命令写出来就一行但每个参数背后的意义要清楚否则就是在瞎调yolo detect train \ modelyolov8s.pt \ data/root/project/dataset/data.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ project./runs \ namefire_smoke_exp1这个命令里model使用了yolov8s的预训练权重比直接随机初始化收敛快得多。epochs设100是火焰烟雾场景的常用起步值因为这个任务目标外观变化大100轮以上才能让模型充分见过不同背景下的样本。imgsz用640是YOLOv8默认输入分辨率如果你的监控画面里有大量小目标可以往上调到896或者1024代价是显存占用和训练时间同步上升。batch根据你的显卡显存来定我自己的经验是batch从16起底显存够就32batch太小的话BN层的统计量波动会很大。一个常见的疑问是为什么从yolov8s而不是直接上yolov8m或yolov8l。我的习惯是先小模型跑通流程精度有问题再换大模型。火焰烟雾不是那种需要极致小目标精度的场景s模型跑出来的结果在工程上往往已经够用而且训练速度快能快速验证数据质量。如果数据本身有问题大模型只会把错误放大到时候排查起来更痛苦。4.2 关键参数调整锚框、分辨率与多尺度训练训练参数里影响最大的是imgsz、mosaic和anchor相关设置。火焰烟雾这个场景有两个特点一是目标尺寸跨度极大近处一簇火苗可能占图片三分之一远处烟柱只有几十个像素二是目标形状细长烟雾经常是长条形。YOLOv8默认的锚框是针对COCO数据集聚类出来的对火焰烟雾这种细长目标形状并不完全匹配所以一般我会开启自动锚框计算或者在前几次训练后手动聚类更新锚框。yolo detect train \ modelyolov8s.pt \ data/root/project/dataset/data.yaml \ epochs100 \ imgsz896 \ batch16 \ lr00.01 \ mosaic1.0 \ close_mosaic10 \ scale0.5 \ fliplr0.5这里我把imgsz提到896建议如果你标注的火焰目标很多是几十像素的小框可以试试。mosaic保持1.0这表示每张输入图由四张图拼接而成等于在变相增大batch和丰富背景。close_mosaic10表示最后10个epoch关闭mosaic增强因为拼图样本和真实场景分布差异大如果不关掉会干扰最终收敛精度。scale0.5是随机缩放范围允许模型在训练中看到不同尺度的目标。这些参数不是拍脑袋定的实操中我会先用默认参数跑完第一个100轮看验证集mAP曲线再针对性的调其中两三个。YOLO训练调参有点像玄学每改一个参数都要重新验证一次改太多你根本不知道是哪个参数起了作用。4.3 训练中BN崩溃和loss不降的排查训练过程中最让人头疼的两个现象是BN崩溃和loss持平不动。BN崩溃的表现是训练到某个epoch忽然验证集mAP断崖式下降打印日志可以看到BN层的running_mean和running_var出现NaN。出现这种情况优先排查学习率是不是过大lr0从0.01降到0.001再试其次看batch是不是太小如果batch只有4或2BN的统计量在每步更新中偏差很大模型很容易在epoch中后期崩溃。loss不降反而更容易定位打开一张训练图片和它对应的TXT标注确认框是不是真的对准了目标。如果框偏了一半模型学到的就是错误位置的特征loss低不下去。另一个高频原因是标注里混入了空文件YOLO训练时空图片没有目标模型只能学背景表现为验证集mAP非常低但训练loss正常下降。办法就是回到第2章那个统计脚本把空标注文件彻底清掉。5. 常见问题与排查我从18800张图里踩过的五个坑5.1 类别ID错位模型把火识别成烟现象训练完成推理时看到明火却输出smoke看到白烟输出fire两个类别完全颠倒了。原因数据集TXT里的0和1对应的语义和你data.yaml里names顺序不一致。举例来说如果TXT里0代表smoke、1代表fire但yaml里names写着0: fire、1: smoke模型学到的映射就是反的。解决回到第2章的统计脚本重新解析TXT并打印类别ID分布然后随机抽几张图画框验证。确认无误后修正data.yaml的顺序。从那以后我每拿到数据集第一件事就是抽三张图人工核对类别名不核对绝对不写yaml。5.2 TXT和XML两种标注不一致同一个目标丢失了现象某张图片TXT里有两个目标XML里只有一个或者两个文件的目标边界框完全不重合。原因数据集在制作时可能用了两套标注工具先标了YOLO格式后转VOC格式转换脚本出bug导致部分目标被丢弃。这类问题在公开数据集里并不罕见。解决写个脚本逐图对比TXT和XML解析出的目标数量对不上的单独输出到差异清单。如果差异比例超过1%就统一用可信度高的一份格式。我一般优先信任XML因为像素坐标能直接和原图比对。5.3 归一化坐标出现负值或大于1训练loss直接NaN现象训练刚开始就报NaN或者loss曲线出现断崖式下跌到负值。原因标注文件里存在x_center为负数、width大于1这类越界值上一节的解析脚本和转换脚本里都做了坐标裁剪但如果直接用原TXT训练这类非法值会让损失函数计算出现算术异常。解决用我在转换脚本里的min/max裁剪逻辑或者用数据清洗脚本把这些文件整张剔除。我建议直接剔除因为坐标越界往往说明标注质量本身不可信单点裁剪只是治标。5.4 小目标火焰漏检严重远处小火苗全都找不到现象mAP曲线看起来不错但实际部署到高位监控画面中远处的火苗完全漏检只有大团火焰能检测到。原因训练时imgsz用了640小目标像素占比太少经过几次下采样后特征基本丢失。同时默认锚框对10像素以下的框无法有效匹配。解决imgsz提到896或1024重新训练或者用多尺度训练让模型适应不同目标尺寸。另一个通用方案是使用SAHI切片推理把大图切成小图分别检测再合并结果对小目标效果立竿见影。5.5 验证集mAP虚高部署现场疯狂误报现象训练时验证集mAP有0.85但拿到真实监控视频里跑晴天树影、路灯、工厂白色蒸汽全部检测成火焰或烟雾。原因数据集里正样本集中在简单场景没有包含足够多的负样本或者与火焰外观相似的目标。模型学到的特征是「高亮、偏红色区域是火焰」而不是真正的火焰纹理和形状。解决训练后单独收集一批真实场景的困难负样本加入训练集一起微调。如果数据集本身不含负样本就在推理时把置信度阈值从默认0.25提高到0.5以上用阈值过滤掉低置信度误检。误报是这类数据集落地工程中最实际的问题模型再准连续误报几次就没有人信任这个系统了。6. 验证标注与推理结果让每一条框都经得起肉眼检查训练前后都值得做一件最简单但也最有效的事把标注框画到图片上肉眼逐一过一遍。模型训练得怎么样推理结果准不准单看loss和mAP都是间接感知直接看图才是最可靠的验证方式。下面这个脚本可以把TXT标注绘回到原图按类别分配固定颜色框左上角标注类别名输出到check目录import os import cv2 class_names {0: fire, 1: smoke} colors {0: (0, 0, 255), 1: (0, 255, 255)} # fire用红色smoke用黄色 img_dir dataset/images/val txt_dir dataset/labels/val out_dir check_imgs os.makedirs(out_dir, exist_okTrue) for txt_name in os.listdir(txt_dir): if not txt_name.endswith(.txt): continue base os.path.splitext(txt_name)[0] img_path os.path.join(img_dir, base .jpg) img cv2.imread(img_path) if img is None: continue h, w img.shape[:2] with open(os.path.join(txt_dir, txt_name), r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:]) # 把归一化坐标还原成像素坐标 x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls_id], 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) out_path os.path.join(out_dir, base .jpg) cv2.imwrite(out_path, img) print(校验图已输出到:, out_dir)这里有几个细节值得注意。坐标还原公式里(x_c - bw/2)算出左上角x像素坐标再乘图片宽w和TXT的定义方式严格对应。用OpenCV的rectangle画框时我传的(x1, y1, x2, y2)顺序是左上和右下两个点对应YOLO格式中中心点加减宽高一半。颜色区分上fire用红色、smoke用黄色这是监控场景的色彩直觉一眼能分清。画完之后抽样看20张重点检查两类目标框是否紧贴火焰边缘、细长烟雾的框是否只框住了一半。训练完成后的推理验证同理用模型跑一段真实场景视频或一批没训练过的现场照片。关注的不只是检测准不准还有检测框的稳定性——同一簇火焰在相邻帧里框的抖动幅度有多大。如果帧间抖动明显说明模型对目标边界回归不够稳定需要回到数据层面补充更多该形态的样本而不是只调NMS阈值。我现在做火焰烟雾项目养成一个习惯模型训练前抽20张混入白天、傍晚、夜晚、室内、室外五个场景可视化一遍标注推理后再抽40帧视频检查框的稳定性两步走完才会进入部署。这套流程是从一次翻车里学到的——当时跳过第一步直接开训loss刷得飞快验证集mAP却只有0.2查了一晚上才发现标注文件里坐标整体偏移了框全部套在目标右上方。从那以后每拿到一个数据集我都强制自己先跑可视化脚本再动手训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表