简介:这份资源是面向目标检测学习者的YOLO烟雾数据集,适合从事火灾预警、工业安全监控、环境烟雾识别等方向的算法工程师与高校学生使用,可解决烟雾目标检测训练样本不足、标注质量参差的问题。压缩包共收录2000个文件,以xml格式的标注文件为主,对应21578张烟雾图像,整体约268.9MB,标注文件可直接用于YOLO系列模型的训练与验证流程。目前已有482人学习下载,说明该数据集在烟雾检测任务中具备一定参考价值。数据集聚焦烟smoke100类别,标注信息完整,读者可据此快速搭建烟雾检测实验环境,用于模型训练、精度对比与消融实验,也可作为课程设计或毕业项目的训练数据来源,省去自行采集与标注图像的时间成本。
1. 烟雾检测数据集落地:21578 张带标签图像到底能跑出什么效果
消防通道的摄像头拍到一缕白烟,传统阈值报警器要等烟雾浓度累积到触发线才响,中间可能已经烧了三四分钟。换成 YOLO 做视觉烟雾检测,理论上能在烟雾刚出现的头几帧就框出来——但前提是你手里有一份标注质量过关、场景覆盖够广的数据集。这次拆的yolo算法-烟雾数据集-21578张图像带标签-烟smoke100-uwe4t.zip就是干这个的:21578 张图像,全部带 XML 标签,单类别 smoke,标注格式是 Pascal VOC 那套。它解决的不是"模型怎么搭"的问题,而是"拿什么数据训"的问题。适合两类人:一是想快速验证烟雾检测可行性、不想从零标数据的算法工程师;二是做安防、森林防火、工业巡检方向,需要一份能直接转 YOLO 格式的现成标注集。下面按"数据长什么样 → 怎么转格式 → 怎么训 → 坑在哪"的顺序拆开讲。
2. 先看清数据底子:VOC XML 标注与 21578 张图的组织方式
2.1 解压后目录结构与文件命名规律
拿到压缩包第一件事不是急着训,是先摸清目录。这类数据集常见组织方式是JPEGImages/放图、Annotations/放 XML,文件名一一对应。从项目正文给的样本看,XML 命名是img_0485_10246.xml这种「前缀_批次_序号」结构,说明图像大概率也是img_0485_10246.jpg同名。先跑一遍统计,确认图和标签是否配对齐全,这一步能提前发现丢标签、丢图的问题。
# 解压后进入根目录,先看结构 unzip yolo算法-烟雾数据集-21578张图像带标签-烟smoke100-uwe4t.zip -d smoke_dataset cd smoke_dataset # 统计图像和标注数量 find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l # 找出有图无标签、有标签无图的文件 comm -3 <(find . -name "*.jpg" -printf "%f\n" | sed 's/.jpg//' | sort) \ <(find . -name "*.xml" -printf "%f\n" | sed 's/.xml//' | sort)comm -3那行是关键:它把图像名和标签名各自排序后做差集,输出任何一边多出来的文件。正常情况应该没有任何输出;如果刷出一堆名字,说明数据集本身有缺失,训练前必须处理掉,否则 YOLO 加载时会直接报找不到标签。-printf "%f\n"只取文件名不带路径,避免路径差异干扰比对。
2.2 单类别 smoke 的标注内容与坐标格式
VOC 格式的 XML 核心就几块:<size>记录图像宽高,<object>里<name>是类别名,<bndbox>是xmin/ymin/xmax/ymax四个像素坐标。这份数据集是单类别,<name>应该统一是smoke。先抽几个 XML 看看,确认类别名没有拼写不一致(比如smoke和Smoke混用,这是血泪经验里最常见的翻车点,YOLO 会当成两个类)。
import glob, xml.etree.ElementTree as ET from collections import Counter names = Counter() sizes = [] for f in glob.glob("Annotations/*.xml")[:500]: # 抽样500个够看分布 root = ET.parse(f).getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) sizes.append((w, h)) for obj in root.findall("object"): names[obj.find("name").text] += 1 print("类别分布:", names) # 期望只有 smoke 一个键 print("尺寸样本:", Counter(sizes).most_common(5))这段代码做两件事:统计类别名是否唯一,统计图像分辨率分布。如果names里冒出第二个键,说明标注不纯,得先统一。分辨率分布决定你训练时的imgsz怎么设——如果绝大多数是 640×640 附近,直接imgsz=640;如果普遍偏小(比如 320×240),硬拉到 640 只会插值糊掉,反而掉点。Counter(sizes).most_common(5)能一眼看出主流尺寸。
提示:抽样 500 个足够判断分布,全量遍历 2 万多个 XML 会慢,正式训练前再全量校验一次即可。
3. VOC 转 YOLO 格式:转换脚本与归一化坐标的四个边界坑
3.1 为什么必须转:YOLO 要的是归一化中心点格式
YOLO 训练不认 VOC 的绝对像素坐标,它要的是每行class_id cx cy w h,且全部归一化到 0~1。cx cy是框中心点,w h是框宽高,都除以图像对应边长。转换本身不难,难在边界处理:框超出图像、宽高为 0、坐标是浮点、类别名映射错位,这四个坑任何一个都能让训练 loss 直接 NaN 或者 mAP 归零。下面这份脚本把校验和转换揉在一起,转完直接能用。
import os, glob, xml.etree.ElementTree as ET CLASS_MAP = {"smoke": 0} # 单类别,映射到 0 IMG_DIR, XML_DIR, OUT_DIR = "JPEGImages", "Annotations", "labels" os.makedirs(OUT_DIR, exist_ok=True) def convert(xml_path): root = ET.parse(xml_path).getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): cls = obj.find("name").text.strip() if cls not in CLASS_MAP: continue b = obj.find("bndbox") xmin = float(b.find("xmin").text); ymin = float(b.find("ymin").text) xmax = float(b.find("xmax").text); ymax = float(b.find("ymax").text) # 边界裁剪:防止框超出图像导致归一化后越界 xmin, xmax = max(0, xmin), min(w, xmax) ymin, ymax = max(0, ymin), min(h, ymax) bw, bh = xmax - xmin, ymax - ymin if bw <= 1 or bh <= 1: # 过滤无效框 continue cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h lines.append(f"{CLASS_MAP[cls]} {cx:.6f} {cy:.6f} {bw/w:.6f} {bh/h:.6f}") return lines for xml in glob.glob(f"{XML_DIR}/*.xml"): stem = os.path.splitext(os.path.basename(xml))[0] lines = convert(xml) with open(f"{OUT_DIR}/{stem}.txt", "w") as f: f.write("\n".join(lines))逻辑说明:max(0, xmin)和min(w, xmax)是边界裁剪,VOC 标注里偶尔会有标注员手抖把框拖出图像,不裁的话归一化后 cx 可能大于 1,YOLO 直接判为非法标签。bw <= 1 or bh <= 1过滤掉宽高不足 1 像素的框,这种框在缩放后基本消失,留着只会污染 loss。:.6f保留六位小数,YOLO 官方推荐精度,太少会累积误差。
3.2 生成 train/val 划分与 data.yaml
转完标签还得划分训练验证集,并写一份 YOLO 认的data.yaml。划分别用随机,烟雾数据如果同一段视频抽帧,随机划分会导致训练集和验证集高度相似,验证 mAP 虚高。常见做法是按文件名前缀(比如img_0485这种批次号)分组划分,保证同批次进同一侧。
import glob, os, random random.seed(42) imgs = sorted(glob.glob("JPEGImages/*.jpg")) # 按前缀分组,避免同源帧泄漏 groups = {} for p in imgs: key = os.path.basename(p).rsplit("_", 1)[0] groups.setdefault(key, []).append(p) keys = list(groups.keys()); random.shuffle(keys) split = int(len(keys) * 0.9) train = [p for k in keys[:split] for p in groups[k]] val = [p for k in keys[split:] for p in groups[k]] for name, items in [("train", train), ("val", val)]: with open(f"{name}.txt", "w") as f: f.write("\n".join(os.path.abspath(p) for p in items)) print(f"train={len(train)} val={len(val)}")data.yaml内容如下,path写数据集根目录绝对路径,nc是类别数,单类别就是 1:
path: /abs/path/to/smoke_dataset train: train.txt val: val.txt nc: 1 names: ["smoke"]注意:
names的顺序必须和转换脚本里CLASS_MAP的 id 严格对应,smoke是 0 就写在第 0 位,写反了模型学出来的全是错的。
4. 用 YOLOv8 跑通训练:参数怎么设、显存怎么省
4.1 环境与启动命令
环境用 ultralytics 官方包最省事,pip install ultralytics一把梭。训练入口就一行命令,但参数得按数据规模调。21578 张图不算小,单卡 8G 显存跑yolov8n或yolov8s比较稳,yolov8m以上得看显存。
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ workers=8 \ cache=True \ patience=20 \ project=runs/smoke \ name=exp1参数逐个说:model=yolov8s.pt是从预训练权重起步,烟雾这种单类别任务,预训练能省一半以上收敛时间,别从yaml空权重训。batch=16是 8G 显存下的保守值,显存够可以往上加,但 batch 太大对小目标烟雾反而不利。cache=True把图像缓存到内存,2 万张图大概吃 6~8G 内存,内存够就开,能明显提速。patience=20是早停,20 轮验证指标不涨就停,省时间。
4.2 关键超参:imgsz、学习率与数据增强
imgsz=640不是随便定的。烟雾在监控画面里往往只占一小块,如果原始分辨率高,缩到 640 后烟雾可能只剩十几个像素,模型根本学不到。这种情况要么提高imgsz到 960/1280(显存翻倍),要么在数据增强里关掉大幅缩放。默认增强里mosaic和scale对烟雾检测是把双刃剑:mosaic 拼图能增加小目标样本,但烟雾本身纹理弱,拼图后容易和背景混淆。
# 如果发现小目标漏检严重,调低缩放增强、提高输入尺寸 yolo detect train data=data.yaml model=yolov8s.pt epochs=100 \ imgsz=960 batch=8 scale=0.3 mosaic=0.5 lr0=0.005scale=0.3把随机缩放幅度压到 ±30%,mosaic=0.5表示一半概率用 mosaic,lr0=0.005比默认 0.01 略低,单类别任务不需要太激进的学习率,太大容易在早期把预训练特征冲垮。这些值不是标准答案,是给一个可调的起点,具体看训练曲线里cls_loss和mAP50的走势再微调。
4.3 训练过程该盯哪几个指标
跑起来后别只等结果,盯runs/smoke/exp1/results.csv。重点看三个:train/box_loss是否稳定下降、metrics/mAP50是否在涨、val/box_loss有没有和训练 loss 背离。如果训练 loss 一直降但验证 mAP 卡住不动,八成是过拟合或者验证集分布和训练集差太远。如果box_loss出现 NaN,回去查第 3 章的转换脚本,大概率是归一化坐标越界或空标签文件。
提示:空标签文件(图像里没有 smoke)在 YOLO 里是合法的,代表纯背景负样本,别删。但如果一个数据集里空标签占比超过 30%,说明标注可能漏标,得回头核对。
5. 避坑与排查:烟雾数据集训练最常见的五个翻车点
5.1 现象:训练启动就报 "No labels found"
原因:data.yaml里train/val指向的 txt 文件路径不对,或者 txt 里写的是相对路径但训练时工作目录变了。YOLO 找标签的规则是:把图像路径里的images替换成labels、后缀换成.txt。如果你的目录不叫images,它就找不到。
解决:要么把目录规范成images/和labels/结构,要么在 txt 里写绝对路径。最稳的是用绝对路径,第 3.2 节脚本里os.path.abspath就是干这个的。
5.2 现象:mAP 一直是 0 或者极低
原因:类别 id 对不上。转换时smoke映射成 0,但data.yaml里names写成了别的顺序,或者 XML 里类别名有大小写混用导致部分框被continue跳过。
解决:跑一遍全量类别统计,确认 XML 里只有一种写法;再检查data.yaml的names和转换脚本的CLASS_MAP是否一致。这两个地方对齐,mAP 不可能为 0。
5.3 现象:显存爆了,报 CUDA out of memory
原因:batch或imgsz设太大,或者cache=True时内存不够被系统杀进程。
解决:先把batch降到 8 或 4,再考虑降imgsz。如果开了cache=True还爆,关掉它,用磁盘 IO 换内存。另外workers别设太高,设成 CPU 核数就行,设太高反而抢内存。
5.4 现象:验证集 mAP 虚高,实际部署漏检严重
原因:训练验证划分时同一段视频的帧被分到了两边,验证集和训练集高度相似,模型等于在"背答案"。
解决:按文件名前缀或时间戳分组划分,保证同源帧进同一侧。第 3.2 节的rsplit("_", 1)[0]就是按前缀分组,如果你的命名规律不同,改成对应的时间戳或批次号。
5.5 现象:烟雾框位置偏移,框偏大或偏小
原因:XML 里size记录的宽高和实际图像尺寸不一致。有些数据集在裁剪或缩放图像后忘了同步更新 XML 里的size,导致归一化基准错了。
解决:转换前用 PIL 读一遍实际图像尺寸,和 XML 里的size比对,不一致的以实际图像为准重新计算。这一步多花几分钟,能省掉后面几小时的排查。
6. 进阶:用混淆矩阵和 PR 曲线判断这份数据到底够不够用
训完一轮别急着收工,runs/smoke/exp1/下会自动生成confusion_matrix.png和PR_curve.png,这两张图比 mAP 一个数字信息量大得多。混淆矩阵看的是分类对不对:单类别任务里,矩阵是 2×2(smoke 和 background),对角线是分对的,非对角线是漏检和误检。如果 background 被大量误判成 smoke(假阳性高),说明模型把云、雾、蒸汽这类纹理当成了烟,得往训练集里补负样本。如果 smoke 大量被判成 background(漏检高),说明烟雾特征学得不够,要么加数据,要么提高输入分辨率。
PR 曲线看的是不同置信度阈值下的查准查全权衡。曲线下的面积就是 mAP,但更实用的是看曲线拐点:拐点靠右上说明模型在较高召回下还能保持查准,适合安防场景(宁可误报不可漏报);拐点靠左下说明模型保守,适合需要低误报的场景。根据你的业务选阈值,别直接用默认 0.25。
from ultralytics import YOLO model = YOLO("runs/smoke/exp1/weights/best.pt") metrics = model.val(data="data.yaml", conf=0.4, iou=0.5) print("mAP50:", metrics.box.map50) print("每类AP:", metrics.box.ap50) # 单类别就一个值conf=0.4是验证时用的置信度阈值,比默认高,能看出模型在高置信下的真实水平。iou=0.5是判定预测框和真值框匹配的 IoU 门槛,标准做法。跑完对比不同conf下的map50,选一个业务上可接受的平衡点。
我自己的习惯是:每次拿到新数据集,先跑一遍基线,把混淆矩阵和 PR 曲线存下来当参照,后面任何改动都跟这个基线比。有一次偷懒没存基线,改完增强策略发现 mAP 涨了 2 个点,结果回头一查是验证集划分变了,白高兴一场。从那以后我每次动数据或改划分,都强制先固定一份 val 集不动,只调训练侧。希望这份拆解能帮你少走点弯路,数据集拿到手先校验再转格式,转完先跑小样本过拟合测试,确认流程通了再上全量。
本文还有配套的精品资源,点击获取