资讯动态

YOLO烟雾检测数据集实战:从标注校验到训练避坑全指南

发布时间:2026/10/10 18:20:47 来源:尧图企业网站定制
简介面向 YOLO 烟雾检测模型训练的数据标注资源包标题所指的大规模烟雾图像与标签数据可帮助计算机视觉初学者、算法工程师及需要扩充烟雾样本的开发者解决烟雾场景训练数据不足、标注格式不统一等问题。压缩包实际包含 2000 个 XML 标注文件整体约 268.9MB采用 zip 格式封装XML 文件通常对应 PASCAL VOC 标注结构含目标类别、边界框坐标等信息可配合原图用于 YOLOv5/YOLOv8 等模型训练与验证。目前已有 482 人学习浏览适合作为烟雾识别项目的数据补充或标注格式参考。拿到后可直接解析 XML 并转换为 YOLO 格式的 txt 标签梳理类别名称与标注框分布快速构建自己的烟雾检测训练集对于刚接触目标检测的读者可参照这些标注文件理解 VOC 标注语法与数据结构。1. 拿到这个烟雾数据集先别急着训练三个细节决定你能不能用它“yolo算法-烟雾数据集-21578张图像带标签-烟smoke100-uwe4t.zip”这类打包好的数据集名字看着很全——数量、标注状态、类别都写明了但它是第三方整理过的别人机器的目录结构、标注格式、类别编号都和你本地环境不一定对得上。直接解压丢进YOLO训练脚本里十有八九是loss飞掉、验证集指标很差甚至报“no labels found”这种让人摸不着头脑的错。常见做法是先花十分钟把它“验货”第一步确认标注是VOC xml、COCO json还是YOLO txt第二步确认类别字段真的叫smoke且编号从0开始第三步抽几张图在本地画一下框看坐标有没有越界。这三件事做完后面训练才会顺利。这篇文章就按这个顺序展开覆盖从拿到zip到最终验收模型的完整路径新手可以照着做熟手也能在参数和边界上做一些校验。2. 解压后的目录结构先确认标注格式和类别文件再谈训练2.1 压缩包内可能存在的三种文件形态第三方数据集的命名习惯不统一同一个标题下可能拆出来不同布局。最常见的三种情况是VOC风格即JPEGImages和Annotations两个目录标注是xml已经转好的YOLO风格即images和labels两个目录标注是txt且每行五个数还有COCO风格即一个json文件配一个图片目录。这三种形态的处理路径差别很大所以在跑任何脚本前我一般先把整个目录树打印出来看一遍。# 解压后先看目录树别急着打开IDE cd /path/to/smoke_dataset find . -maxdepth 2 -type d | sort echo ---- 前10个文件样例 ---- find . -type f | head -10 find . -type f -name *.xml | wc -l find . -type f -name *.txt | wc -l find . -type f -name *.json | wc -l这段命令先统计目录层数再统计三类标注文件的数量。如果xml数量接近21578且txt为0说明是VOC格式反过来是YOLO格式。这里有个细节值得注意文件数不等于图像数因为有的包会把每张图对应的标签单独放也有个别包把标签合并到一个大文件里需要看输出比例来推断。2.2 先检查类别标签一个很隐蔽的类别编号错位很多人在这一步直接跳过结果训练时发现模型把烟雾框识别成“person”或“background”。问题往往出在类别编号错位。打包者用的类别文件可能叫classes.txt、obj.names、labels.txt里面对应的序号和你本地配置不一致。常见错误是类别文件里写着smoke在第2行YOLO读取时从1开始计数或者xml里的name字段是“smoke100”而训练配置写的是“smoke”。# 找到所有可能的类别文件并打印内容 find . -maxdepth 3 \( -name classes.txt -o -name *.names -o -name labels.txt -o -name obj.names \) cat $(find . -maxdepth 3 -name classes.txt | head -1) echo ---- labels目录里的第一行标注 ---- find . -type f -path *labels* -name *.txt | head -1 | xargs cat如果输出的是“smoke100”或者只有一个类名但标注文件里有大于0的行首数字就要调整。YOLO的类别编号从0开始单类别时所有标注行的第一个数必须是0。这条命令能直接把标注文件的第一行打出来快速验证编号是否合法。2.3 用一段Python脚本统计每类标注数量当目录和txt文件都确认没问题后我习惯写个临时统计脚本把每个类别的标注框数量、图片数量拉出来看一眼。这一步能发现两类隐蔽问题一是标称21578张图但实际可用的不到两万因为部分xml里没有object节点二是类别内部数量极度不均衡后期可能要考虑欠采样或加权重。import xml.etree.ElementTree as ET from collections import Counter import glob xml_files glob.glob(Annotations/*.xml) class_counter Counter() empty_annotations [] for xml_path in xml_files: tree ET.parse(xml_path) root tree.getroot() objects root.findall(object) if not objects: empty_annotations.append(xml_path) continue for obj in objects: name obj.findtext(name, unknown).strip() class_counter[name] 1 print(类别统计:, dict(class_counter)) print(无标注框的xml数量:, len(empty_annotations)) print(有效标注图片:, len(xml_files) - len(empty_annotations))这段脚本的逻辑很简单遍历所有xml统计每个object的name字段并找出没有标注框的空文件。参数上没什么特别要调的唯一需要注意的是xml里name字段的取值。比如有的包把烟雾标成“smoke”有的标成“smoke100”如果统计结果里出现多个看似同义的类名后续就得先做类别统一否则训练时模型学到的语义是割裂的。3. 把标注转成YOLO格式VOC转TXT的脚本与归一化坐标计算3.1 为什么YOLO要求txt标注边界框的xywh归一化YOLO系列从v5到v8训练时读取的标签格式统一是txt文件每行五个数类别编号、归一化中心点x、归一化中心点y、归一化宽度w、归一化高度h。所有坐标都要除以图片的宽和高结果落在0到1之间。这样设计的好处是模型输入尺寸变化时标签不用跟着重新算。VOC xml里存的是xmin、ymin、xmax、ymax这类绝对值坐标所以转换时核心就是做两组除法。计算并不复杂但有一个边界坑当标注框超出图像边界时比如xmax大于图宽直接转换会让中心点和宽高算出来的值大于1训练时YOLO虽然不立刻报错但会让损失计算变得不稳定。这也是为什么转换脚本里必须做边界裁剪。3.2 VOC xml转YOLO txt的完整脚本下面这份脚本我用了很多次针对单类别烟雾检测做过精简可以直接跑。它会把Annotations目录下的所有xml转换到labels目录下并自动裁剪越界框过滤掉面积过小的框。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image annotations_dir Path(Annotations) images_dir Path(JPEGImages) labels_dir Path(labels) labels_dir.mkdir(exist_okTrue) def convert_voc_to_yolo(xml_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() results [] for obj in root.findall(object): name obj.findtext(name, ).strip() if name ! smoke: # 类别统一按需修改 continue box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 裁剪越界框 xmin max(0, min(xmin, img_width - 1)) xmax max(0, min(xmax, img_width - 1)) ymin max(0, min(ymin, img_height - 1)) ymax max(0, min(ymax, img_height - 1)) if xmax - xmin 3 or ymax - ymin 3: continue cx (xmin xmax) / 2.0 / img_width cy (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height results.append(f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return results for xml_path in sorted(annotations_dir.glob(*.xml)): img_path images_dir / (xml_path.stem .jpg) if not img_path.exists(): img_path images_dir / (xml_path.stem .png) if not img_path.exists(): print(缺图片:, xml_path.name) continue with Image.open(img_path) as img: img_width, img_height img.size lines convert_voc_to_yolo(xml_path, img_width, img_height) if lines: out_path labels_dir / (xml_path.stem .txt) out_path.write_text(\n.join(lines) \n) print(转换完成:, xml_path.name, 共, len(lines), 个框) else: print(跳过空标注:, xml_path.name)参数说明脚本里img_width和img_height必须从同名的图片文件读取不能假设所有图尺寸一致。烟雾数据集里很多图像来自监控视频抽帧分辨率跨度很大从640x480到1920x1080都有一旦用固定值转换归一化坐标全错。另一个参数是过滤面积阈值代码里设为3像素小于这个尺寸的框通常是标注噪点。3这个数值适用于远端烟雾场景因为烟雾往往小如果设成10会误杀大量有效框。3.3 转换之后先抽样画框验证转换脚本跑完后不要急着划分数据集抽十几张图把归一化坐标还原成像素坐标用OpenCV画出来对比原标注。这一步能发现两类问题一类是宽高比不对说明图片读取顺序或坐标换算有误另一类是框明显偏移说明xml文件名和图片名没有一一对应。import cv2 import random from pathlib import Path labels_dir Path(labels) images_dir Path(JPEGImages) def draw_yolo_box(img_path, label_path): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(label_path) as f: for line in f: _, cx, cy, bw, bh map(float, line.strip().split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) return img label_files list(labels_dir.glob(*.txt)) for label_path in random.sample(label_files, 10): img_path images_dir / (label_path.stem .jpg) if not img_path.exists(): img_path images_dir / (label_path.stem .png) annotated draw_yolo_box(img_path, label_path) cv2.imwrite(fcheck_{label_path.stem}.jpg, annotated) print(抽查完成请逐个查看check_开头的图片)这段脚本把归一化坐标乘回图像宽高还原成像素框。逻辑本身不复杂但它是转换环节最重要的拦水坝因为如果这一步不查所有错误都会传导到训练和验证阶段。4. 数据划分与训练配置跑通YOLOv8烟雾检测的最小命令4.1 按8:1:1划分数据集注意同源视频帧不能进两个集合数据集转换完成、抽检画框没有明显问题后就该把21578张图分成训练、验证、测试三份。常规做法是8:1:1。这里有个比比例本身更重要的原则如果图像是从视频中抽帧得到的同一条视频的帧不能既出现在训练集又出现在验证集否则会产生严重的数据泄漏验证指标虚高。一个可靠的处理方式是先把文件名前缀通常是视频名或时间戳前段提取出来按前缀分组后再拆分。执行拆分的脚本不需要很复杂但一定要在划分前打印出组数和每组帧数的分布。import random from pathlib import Path from collections import defaultdict images_dir Path(JPEGImages) train_dir, val_dir, test_dir Path(train/images), Path(val/images), Path(test/images) train_lbl, val_lbl, test_lbl Path(train/labels), Path(val/labels), Path(test/labels) for d in [train_dir, val_dir, test_dir, train_lbl, val_lbl, test_lbl]: d.mkdir(parentsTrue, exist_okTrue) # 按前缀分组前缀取文件名前8个字符通常是视频ID groups defaultdict(list) for img_path in sorted(images_dir.glob(*.jpg)) sorted(images_dir.glob(*.png)): prefix img_path.stem[:8] groups[prefix].append(img_path) group_names list(groups.keys()) random.shuffle(group_names) n len(group_names) train_groups group_names[:int(n * 0.8)] val_groups group_names[int(n * 0.8):int(n * 0.9)] test_groups group_names[int(n * 0.9):] def move_images(groups, img_dest, lbl_dest): for group in groups: for img_path in groups[group]: lbl_path Path(labels) / (img_path.stem .txt) if lbl_path.exists(): lbl_dest.mkdir(parentsTrue, exist_okTrue) lbl_path.rename(lbl_dest / lbl_path.name) img_dest.mkdir(parentsTrue, exist_okTrue) img_path.rename(img_dest / img_path.name) move_images(train_groups, train_dir, train_lbl) move_images(val_groups, val_dir, val_lbl) move_images(test_groups, test_dir, test_lbl) print(f训练组: {len(train_groups)}, 验证组: {len(val_groups)}, 测试组: {len(test_groups)})这段脚本最关键的是prefix取值。如果文件名是“cam01_000123.jpg”这种结构前缀取前8个字符能完整保留“cam01_”部分。前缀长度看着像玄学但它直接决定了会不会把同源帧拆散。对监控烟雾场景同源连续帧之间只有微弱光照变化放进两个集合会让模型对“记忆”而非“理解”产生依赖这点务必在拆分前打印一下分组数确认。4.2 写data.yaml类别、路径和中文目录的坑YOLO训练脚本通过data.yaml指定数据路径和类别。烟雾数据集如果解压路径带中文或是用了Windows系统路径里反斜杠会带来一些解析问题。我一般把data.yaml的路径写成相对于当前工作目录的相对路径或者统一用正斜杠。# data.yaml train: train/images val: val/images test: test/images nc: 1 names: [smoke]这个文件本身不需要什么参数调优真正容易翻车的是三个细节train路径指向的是图片目录而不是图片列表names列表的索引必须和txt标注里的第一个数字严格对应如果训练时看到“Assertion numel 1 failed”这类错误多半是labels目录的层级不对YOLO惯例是train/labels和train/images同级labels里放img同名txt少了目录或大小写错了都读不到。4.3 用YOLOv8训练输入尺寸、batch和epoch的取舍训练命令本身很短但参数选择有讲究。烟雾检测属于小目标居多、背景复杂的任务输入尺寸不能像通用目标检测那样用640很多监控画面里烟雾只占整张图百分之几的面积建议用832或1024。显存不够时优先降batch而不是降输入尺寸。yolo train datadata.yaml modelyolov8s.pt imgsz832 batch16 epochs100 patience20 projectsmoke100 nameexp1参数含义说明data.yaml里配置好的数据路径在这里生效yolov8s.pt是预训练权重烟雾纹理和通用物体差异大但前几层的边缘特征仍可复用所以用它做起点比从头训收敛快很多。imgsz设为832是为了让小目标保留更多像素batch16在单张16GB显存的卡上基本跑得动如果报OOM就降到12或8。epochs设100配合patience20早停通常到40-60轮就会稳定下来。还有一个值得养成的习惯训练时开wandb或者至少把results.csv留好方便后面复盘损失曲线。训练日志里重点关注两列val_cls_loss和val_box_loss。烟雾的文本特征不强cls损失如果长期高于box损失很多可以考虑减少背景干扰也就是在数据划分时就剔除那些几乎没有烟雾但标注了远景微小飘烟的低质量帧。5. 避坑烟雾数据集训练中常见的5个问题与排查5.1 训练一启动就报“No labels found”现象train命令执行后很快弹出warning提示train目录下没有任何标注文件然后自动退出。原因有三类一是labels目录命名不对YOLO默认找与images同级的labels目录大小写不能错二是txt文件存在于目录中但内容为空因为前面的转换脚本跳过了所有空标注三是data.yaml里的train路径写成了绝对路径但路径里包含中文或空格解析时被截断。处理方式按顺序排查先确认train/labels目录存在且txt数量与图片数量接近再随意打开一个txt文件看里面是否有五个数字最后把data.yaml里的路径改成相对路径或使用不带空格的目录名。顺手养成一个习惯训练前先跑一下yolo val同款检查比盯着日志猜原因快得多。5.2 训练正常但mAP50始终在0.1以下现象损失正常下降验证集上也确实在收敛但mAP50只有0.05到0.1。原因大概率是标注框质量差或类别混乱而不是模型参数不对。烟雾标注里有大量真实噪点有些标注把远处树梢的抖动也标成了烟雾有些把烟雾的高光部分单独拆成多个框还有些帧的烟雾只有几个像素大标注框和背景几乎无法区分。处理做法是把训练集里置信度最低的100张图抽出来人工看一遍统计属于标注问题时直接清洗。烟雾标注本身主观性很强同一个帧不同标记者可能给出差异很大的框这种场景下更可靠的做法是重新标注一小部分边界框用于修正。我的经验是抽出500张图把明显标错的部分重标一遍mAP50能从0.1跳到0.5以上。5.3 验证集指标不错但实拍视频里框在乱跳现象验证集mAP50有0.7地面真实视频里烟雾确实能框住但框的尺寸和位置在相邻帧之间剧烈变化甚至闪断。原因就是标注本身不稳定。烟雾边缘是半透明的标记者在连续帧里框的位置可能有十几个像素的抖动模型学到的是这种抖动而不仅仅是烟雾的位置变化。处理方式可以分两支想快速见效的话在推理阶段用后处理做时序平滑比如对连续帧的结果做指数移动平均让框坐标变化放缓想从根源解决的话把每段视频的第1、30、60帧筛出来人工校准标注。时序平滑相当于给你的检测结果加了一个低通滤波器换来的是视觉上稳定一些。5.4 训练中期loss突然变成nan现象前20轮正常第21轮开始loss变成nan之后一直不恢复。原因通常是显存里的梯度溢出或者学习率设置过高导致梯度爆炸。烟雾数据集的图像里常有大量高光区域局部亮度极高激活值在深层网络中容易溢出。处理方式首先把batch减一半再跑同配置看是否还出nan如果还出把初始学习率从默认的0.01改为0.001或者开启AMP混合精度训练。锚点框参数也用默认就好。训练到一半出nan还有一种少见但存在的情况某个样本的标注框尺寸为0数据加载时计算损失出现除零。用之前的过滤脚本重新检查一遍最小框面积确保全部大于3像素。5.5 显存明明还有剩余训练却在100轮左右OOM现象训练前几十轮正常到第90轮时突然报CUDA out of memory。原因并不是显存真的被占满而是PyTorch的缓存分配器在长时间运行后积累了大量碎块加上验证阶段额外计算导致瞬时峰值超限。处理方式最简单的操作是设置PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:32让分配器更积极地释放碎片其次可以把验证集eval间隔从默认的1轮改成2轮val_period2。如果这两个都不太管用就直接把batch从16降到12。这类问题属于老生常谈的显存管理不算数据集本身的坑但数据库密集训练时很容易遇到。6. 进阶用小目标切片和混淆矩阵验证烟雾检测模型的实际效果6.1 大图切小块推理小目标烟雾的召回变化烟雾检测的难点主要在小目标上。监控摄像头拍摄的烟雾往往在画面中占比不到5%缩放到输入尺寸后可能只剩十几个像素检测器很难捕获。一种常见的做法是把原图切成几块分别推理后在融合框。对烟雾场景切四块通常就能把召回拉起来因为每块中小目标的比例大幅提升。import cv2 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) def detect_large_image(img_path): img cv2.imread(img_path) h, w img.shape[:2] half_w, half_h w // 2, h // 2 crops [ img[0:half_h, 0:half_w], img[0:half_h, half_w:w], img[half_h:h, 0:half_w], img[half_h:h, half_w:w], ] results model.predict(crops, imgsz832, conf0.25) return results这里切片尺寸是按原图一半切的如果原图很大可以继续切更细但要关注推理耗时。切片后检测框的坐标要映射回原图坐标系才能用于后续告警逻辑这一部分需要做简单的坐标加上偏移量即可。6.2 用混淆矩阵和验证集图片检查模型的“偏科”训练结束后不要只看mAP我习惯跑一次yolo val生成混淆矩阵同时从验证集里挑出预测错误样本看它是漏检了还是误报。烟雾数据集只有单类混淆矩阵主要看背景被误判为烟雾的比例。如果背景误报很高通常意味着训练数据里烟雾的纹理多样性不足需要补充不同光照、不同天气下的烟雾样本。yolo val modelruns/detect/train/weights/best.pt datadata.yaml imgsz832跑完后会生成confusion_matrix.png重点看右下角label为background的列属于smoke类别的那一格数值。如果烟雾被背景误判的比例超过20%先把置信度阈值调高到0.4再考虑要不要补充数据。还有一个常用的验收习惯是提前准备一段完全没参与训练的视频模拟真实部署时的输入流。用模型逐帧跑一遍观察框的稳定性与延迟。这个动作比任何指标都有说服力因为它反映了模型在实际现场的表现。剪辑一个两分钟视频文件用OpenCV按每秒两帧推理拼接结果能直观感受模型在真实烟雾变化过程中的表现。这一步走完这个数据集才算真正被你用起来了。我用这类数据集的习惯是先花一小时验证再花半小时清洗最后才启动训练。因为烟雾检测的成败大半取决于数据质量模型架构反而是最不折腾的部分。希望这篇笔记能让你少走一些弯路尤其是别第一次训练就在标注格式上卡半天。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑