简介:粉尘检测数据集包含922张真实环境监测场景图片及其标注文件,采用YOLO目标检测格式,面向需要训练粉尘颗粒物检测模型的AI开发者、环保技术研究人员与工业安全工程人员。数据集划分明确:训练集797张、验证集83张、测试集42张,统一标注为粉尘类别,边界框坐标与类别标签完整,可直接接入YOLO系列框架训练与验证。压缩包共1846个文件,主要包含922张图像、922份标签文本、1个模型配置文件与1份说明文档,总大小约59.23MB,目录清晰便于按划分集使用。目前已有261人学习下载,适合用于环境空气监测预警、建筑工地与工厂粉尘防控等专项模型开发。依托真实场景中多样化的粉尘形态,可提升模型在复杂光照与背景下的泛化能力;配套说明文档与配置文件能帮助初学者快速了解数据结构与训练参数,聚焦粉尘检测这一细分任务,为环保与职业健康安全领域的AI落地提供可靠的数据基础。
1. 粉尘检测数据集.zip 是什么:解压前先弄清楚这几件事
"粉尘检测数据集.zip"这一名字听着像个现成武器,但多数人解压后就卡住:图片有一堆,标签格式对不上,类别比例失衡,远处的粉尘小到标注框只有几十个像素。我把处理这类数据集的路子完整走一遍,从解压一个 zip 开始到 YOLOv8 训练,再到拿指标排查为什么漏检,适合做工地扬尘、工厂车间、矿山粉尘等视觉识别的算法工程师或安监项目集成商。看完你可以判断这个方向值不值得投入——如果数据质量本身有问题,后面的模型调参大多是玄学。
2. 解压与盘点:先把 zip 伪加密、解压乱码和目录结构一次看全
2.1 用 7-Zip 识别 zip 伪加密并安全解压
拿到任何标注数据集的 zip,我都不建议直接在 Windows 上右键“全部提取”。这类包从网盘、同事移动硬盘、线上标注平台传下来,很可能被加过密码或压缩工具改过标志位。最常见的是 zip 伪加密:文件头里有个通用位标记(general purpose bit flag),第 0 位被置成 1,表示“此条目有密码”,但实际数据流没有做任何加密。Windows 的资源管理器信了这个标志,弹窗要密码;7-Zip 则直接无视它。
7z x 粉尘检测数据集.zip -o./dust_data -y-o指定解压目录,后面不能加空格,写成-o./dust_data;-y表示遇到覆盖询问全部同意。如果 7-Zip 直接开始解压,这个包就是伪加密,不用去找 zip 密码移除之类的工具。如果命令停在Enter password,说明确实有加密,只能向提供数据的人要密码。在 Linux 服务器上如果只有 unzip,伪加密包会提示needs password,这时先apt install p7zip-full再回来用7z。
解压前建议先测试压缩包完整性,避免解压到一半报错:
7z t 粉尘检测数据集.zip输出的每一项如果显示OK,则文件没问题;有Data Error的项说明包体损坏。这种情况直接重新下载或找原始文件,后续所有步骤都别再基于这个包做。
2.2 解压后的目录盘点:图片、标注、类别文件各归哪
解压完先别急着翻图。我一般用三条命令看清楚里面有什么。
find dust_data -maxdepth 2 -type f | sed 's/.*\.//' | sort | uniq -c ls dust_data第一条从扩展名角度统计文件类型分布,第二条看顶层目录。正常的粉尘检测包基本长这样:images里是 jpg/png,annotations里是 xml/json/txt,classes.txt写类别名。如果只有images而没有标注目录,那这个 zip 就只是个图片包,不是可训练数据集,后续需要重新标注。
再看图片和标注数量是否对得上:
find dust_data/images -type f | wc -l find dust_data/annotations -type f | wc -l如果标注数量明显少于图片,说明里面有大量负样本,也就是没有目标的背景图。YOLO 训练时负样本需要单独处理,不能直接和正样本一起当训练对。如果两类数量完全一致,大概率每张图都有标注,训练前还要确认标注文件里有没有空文件。
然后校验图片是否能正常打开。有的包为了压缩体积,把传输过程中损坏的图片也塞进来了,文件存在但图像数据是坏的。
from pathlib import Path from PIL import Image img_dir = Path("dust_data/images") broken = [] for p in img_dir.iterdir(): if p.suffix.lower() not in (".jpg", ".jpeg", ".png"): continue try: with Image.open(p) as im: im.load() except Exception as e: broken.append((p.name, e)) print(f"broken images: {len(broken)}") for name, err in broken[:10]: print(name, err)im.load()会把像素数据真正读入内存,能抓出文件头正常但内容已损坏的图片。如果 broken 数量过大,直接从源头重下,不要硬着头皮训练。这个脚本在拿到任何数据集 zip 后都值得保留,后面每次换数据集都能用。
2.3 判断标注格式:VOC、COCO、YOLO 一眼认出
粉尘检测数据的标注格式最常见是三种,其中 COCO 结构基本沿用 coco2017 数据集结构。判断方法不用靠猜,打开标注目录扫一眼即可。
import json from pathlib import Path ann_dir = Path("dust_data/annotations") if list(ann_dir.glob("*.xml")): print("VOC XML") elif list(ann_dir.glob("*.json")): p = list(ann_dir.glob("*.json"))[0] data = json.loads(p.read_text(encoding="utf-8")) print("keys:", list(data.keys())) if "annotations" in data and "categories" in data: print("COCO-style JSON") else: txt_count = len(list(ann_dir.glob("*.txt"))) print(f"possible YOLO txt, count={txt_count}")COCO json 的顶层一定是images、annotations、categories三个字段,annotations里的每条记录有area、bbox、category_id,其中bbox是[x, y, width, height],单位是像素。VOC 格式是 xml,每个对象在object节点下,边界框字段bndbox是xmin, ymin, xmax, ymax,也就是左上角和右下角坐标。YOLO 格式则是 txt,每行五个数:类别索引、归一化中心 x、归一化中心 y、归一化宽、归一化高。
从标注格式能反推数据产生的时间线:人工手工标的多是 VOC,标注平台导出多半是 COCO,搬运过一手大概率被转成 YOLO。如果是 YOLO txt,还要打开看几行确认不是绝对坐标。
3. 把 VOC/COCO 粉尘标注转换成 YOLO 格式:转换脚本与四个参数
3.1 为什么训练前必须统一格式
YOLOv8 的训练管线只认 YOLO 格式的标签,一张图对应一个 txt,省去运行时解析 json/xml 的开销,也更容易和 OpenCV 的数据增强配合。因此第一步是把标注统一成 YOLO 格式,而不是把 yaml 直接指向 VOC 或 COCO 文件。转换过程常见的四个参数是类别顺序、图像宽高、归一化方式、输出目录。任何一项错了,训练都能跑通,但指标永远是废的。
3.2 VOC XML 转 YOLO 的脚本
下面这段脚本可以处理大部分 VOC 标注烟尘数据。
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, class_names, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) 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) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") class_names = ["dust"] # 按实际 classes.txt 顺序填 xml_dir = Path("dust_data/annotations") out_dir = Path("dust_data/labels") out_dir.mkdir(exist_ok=True) for xml_path in xml_dir.glob("*.xml"): voc_to_yolo(xml_path, class_names, out_dir)逻辑说明:脚本先从size节点读取图像宽高,坐标归一化必须有这个值;bndbox的四个数是绝对像素坐标,要转成中心坐标和宽高比;class_names的顺序必须和之后的data.yaml完全一致,如果一个类别在classes.txt里排在 index 2,这里也要放 index 2。如果 xml 里出现脚本没见过的类别名,会被直接跳过,所以转换前先检查类别拼写。
先跑这条命令看实际标注里有哪些类别名:
grep -h "<name>" dust_data/annotations/*.xml | sort | uniq -c输出每一行的类别名和出现次数。把出现过的类别按你想要的目标顺序填进class_names,不要按字母序或外观顺序乱排。
3.3 COCO JSON 转 YOLO 的脚本
如果打开 json 后看到典型的 COCO 结构,用下面脚本转换。
import json from pathlib import Path def coco_to_yolo(json_path, out_dir): data = json.loads(open(json_path, encoding="utf-8").read()) cat_id_to_idx = {cat["id"]: i for i, cat in enumerate(data["categories"])} img_id_to_info = {img["id"]: img for img in data["images"]} anns_by_img = {} for ann in data["annotations"]: anns_by_img.setdefault(ann["image_id"], []).append(ann) for img_id, anns in anns_by_img.items(): img = img_id_to_info[img_id] w, h = img["width"], img["height"] name = Path(img["file_name"]).stem lines = [] for ann in anns: cls_id = cat_id_to_idx[ann["category_id"]] x, y, bw, bh = ann["bbox"] x_center = (x + bw / 2) / w y_center = (y + bh / 2) / h bw_norm = bw / w bh_norm = bh / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw_norm:.6f} {bh_norm:.6f}") (out_dir / f"{name}.txt").write_text("\n".join(lines), encoding="utf-8") out_dir = Path("dust_data/labels") out_dir.mkdir(exist_ok=True) coco_to_yolo("dust_data/annotations/train.json", out_dir)这段脚本有三个关键点。第一,COCO 的category_id不是连续索引,可能从 1 开始,也可能中间缺号,所以要先通过categories列表重新映射成 0 开始的索引。第二,COCO 的bbox是[x, y, width, height],转 YOLO 时要先算出中心点再归一化。第三,file_name可能是相对路径,这里取Path(img["file_name"]).stem避免带出子目录名。
转换后如果出现负数坐标,或因边框越过图像边界导致宽高比大于 1,可以在写行前加一段保护:
x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) bw_norm = min(bw_norm, 1.0) bh_norm = min(bh_norm, 1.0)这段保护不是万能药。坐标大面积越界说明原 json 的width/height和实际图片尺寸不一致,需要优先查数据源头。
3.4 转换后的四轮检查清单
转换脚本跑完,训练前要过四轮检查。
# 1. 标注文件数量是否等于图片数量 ls dust_data/images/*.jpg | wc -l ls dust_data/labels/*.txt | wc -l # 2. 抽查坐标范围,x_center, y_center, w, h 都应小于 1 head -3 dust_data/labels/00001.txt # 3. 找出越界坐标 awk '{if ($2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1) print}' dust_data/labels/*.txt | head # 4. 空标注文件统计 find dust_data/labels -name "*.txt" -size 0 | wc -l坐标越界的行会直接打在屏幕上,有输出就要回查转换脚本。空 txt 表示该图没有目标,YOLO 训练时会把这张图当背景。如果空文件占比超过 10%,模型很容易偏向负样本,导致 recall 掉得厉害。
还有一个常见坑:手机或相机拍的图带 EXIF 旋转信息,但 json 里的宽高用的是文件头尺寸,两者相反时,所有框都会错位。转换前随机找几张图,用 OpenCV 读一次,和标注文件的宽高对比。
python -c "import cv2; im = cv2.imread('dust_data/images/00001.jpg'); print(im.shape)"如果输出是(width, height, 3)还是(height, width, 3)要看具体写法,重要的是先确认图片实际排列和标注一致。
4. 用 YOLOv8 训练自己的粉尘数据集:最小命令与三个关键参数
4.1 目录重组与 data.yaml
训练前先把数据整理成 YOLO 要求的目录结构:
dust_data/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── dust.yamlimages和labels目录必须同级同名,YOLO 才能通过图片路径自动找到对应的 txt。如果解压出来是单一images和labels目录,需要先划分,但不能按单张图随机分。粉尘数据很多来自视频抽帧,同一场景的连续帧在画面内容上非常相似,如果同时进训练集和验证集,指标会虚高到不真实。
按场景分组的拆分脚本如下:
import random from pathlib import Path img_root = Path("dust_data/images") src_labels = Path("dust_data/labels") scene_groups = {} for img in img_root.glob("*.jpg"): scene_id = img.name.rsplit("_", 1)[0] # 假设文件名是 scene_0001.jpg scene_groups.setdefault(scene_id, []).append(img) scene_list = list(scene_groups.keys()) random.Random(42).shuffle(scene_list) train_scenes = scene_list[:int(len(scene_list) * 0.85)] val_scenes = scene_list[int(len(scene_list) * 0.85):] for split, scenes in [("train", train_scenes), ("val", val_scenes)]: (img_root / split).mkdir(exist_ok=True) (src_labels / split).mkdir(exist_ok=True) for scene in scenes: for img in scene_groups[scene]: img.rename(img_root / split / img.name) txt = src_labels / (img.stem + ".txt") if txt.exists(): txt.rename(src_labels / split / txt.name)固定随机种子Random(42)保证每次拆分结果一致,方便对比模型改动前后的效果。用rename会直接移动原始文件,如果不想改原目录,把rename换成shutil.copy2即可。文件名中间的分隔符不一定是下划线,先ls dust_data/images | head -5看命名规律再改拆分逻辑。
拆分后写dust.yaml:
path: C:/work/dust_data train: images/train val: images/val names: 0: dust 1: smokepath建议写绝对路径,避免训练时相对路径解析出错;train和val是相对path的目录,不要再写成dust_data/images/train。names的索引顺序必须与转换脚本里的class_names顺序一致。如果只有一个类别,也要写成0: dust,YOLO 不支持从 1 开始的类别编号。
4.2 训练命令:先小模型跑通,再谈调优
环境装好后,最小可用训练命令是这样:
pip install ultralytics yolo detect train data=dust.yaml model=yolov8n.pt epochs=50 imgsz=640 batch=8 device=0model=yolov8n.pt是 nano 版本,先拿它跑通流程,验证数据和标注没有大问题,再换yolov8s.pt或yolov8m.pt。一次性上大模型遇到代码或数据问题,排错成本高很多。epochs=50是第一批的稳妥值,不要一上来就训练 300 轮;看前 50 轮的 loss 和 mAP 趋势就能判断数据质量。batch=8按显存调整,8GB 显存跑 640x640 的 nano 问题不大,OOM 就降到 4 或 2。
如果训练中因为断电、显存溢出中断,有后悔药可用:
yolo detect train resume model=runs/detect/train2/weights/last.ptultralytics 会自动读取训练状态并续跑,不需要重新加载数据集。这也提醒我们训练中途尽量不要手动改data.yaml或目录结构,否则续跑时标签对不上。
4.3 三个关键参数:imgsz、batch、close_mosaic
粉尘检测最关键的参数是imgsz。粉尘目标通常比较小,远处扬尘可能只占几十个像素,imgsz=640只是起步线。显存允许的情况下,试试imgsz=960,小目标的 mAP 往往能涨 5 个点以上。显存不够时不要先降 imgsz,优先降 batch,然后用梯度累积补回来。
第二个是batch。它不只是一个显存参数,也会影响 BN 层的统计量。小数据集上 batch 不要拉太大,模型在小 batch 下收敛更稳。粉尘数据集通常只有几千到几万张图,batch=16已经够用。
第三个容易被忽略的是close_mosaic。建议在训练末尾把 mosaic 增强关掉:
yolo detect train data=dust.yaml model=yolov8n.pt epochs=50 imgsz=960 batch=8 close_mosaic=10close_mosaic=10表示最后 10 个 epoch 不再使用 mosaic。mosaic 会把四张图拼在一起,小目标被切碎,后期模型需要回到真实数据分布上精调,否则检测框会偏大或位置不稳。
4.4 跑完先看五个数字,再决定要不要继续
训练结束后的验证命令:
yolo detect val model=runs/detect/train/weights/best.pt data=dust.yaml输出里先看五个数字:precision、recall、mAP50、mAP50-95、fitness。粉尘检测的应用场景是安监和环保,漏检一个扬尘源的代价比误报更大,所以重点看recall而不是单纯 mAP。如果mAP50高但mAP50-95低,说明框的定位不稳,框的边界和真实边界吻合度差,这和标注框边界模糊高度相关。此时调模型不如先清理标注。
第一次跑通后,去runs/detect/train/目录下看results.png、confusion_matrix.png和val_batch*.jpg。results.png里如果 loss 曲线在最后 10 轮还在大幅波动,说明学习率或增强策略还有问题;confusion_matrix.png则能直接暴露模型把背景误判成粉尘的情况。
5. 粉尘检测常见坑与排查:从类别失衡到边界框抖动
这个方向坑特别多,而且很多不在模型侧,在数据和标注侧。我按三个层面记录踩过的坑,每条按现象、原因、解决排。
5.1 文件层:解压乱码和损坏图片
现象:解压后文件目录名和图片名全是乱码,用 Python 读标注文件报UnicodeDecodeError。
原因:压缩包里文件名用的是 GBK 编码,Windows 自带解压按本地编码处理,老工具打包时常出这个问题。标注文件内容也可能不是 UTF-8,导致 json/xml 解析失败。
解决:用 7-Zip 重新解压,它在编码处理上更宽容;如果还不行,用 Pythonzipfile读取原始文件名字节,再按 GBK 解码后重命名。最直接的办法是让提供方重新打包一份 UTF-8 编码的 zip,很多包重传一次就省掉后面的折腾。
现象:训练到某个 epoch 突然报错,Loss变成nan,而且复现时随机崩,重新跑一遍可能不崩。
原因:图片文件损坏但扩展名还是 .jpg,数据加载时 OpenCV 读到空图,某些增强操作把空图变成nan张量。这类问题很隐蔽,因为数据集管理器认为文件存在就合法。
解决:训练前用cv2.imread遍历每张图,返回None的直接删除,或者把这些图片单独放到 ignore 目录,不打进数据 yaml。习惯上我会把这一步写进数据体检脚本,每次拿到新 zip 先跑一遍,避免中途翻车。
5.2 标注层:边界模糊、小目标和类别不平衡
现象:同一个扬尘点在相邻几帧里检测框忽大忽小,模型输出也跟着抖动。
原因:粉尘边缘是半透明渐变,标注员很难统一“哪条边是边界”。有人框核心浓度区,有人把淡薄外沿也框进去,模型学到的边界标准是乱的。
解决:在标注规范里定义“可见浓度边”,只框高浓度核心区,低浓度外沿不框。如果数据集已经标完,清洗比调参更有效——把明显的越界框重新标一遍,或者删掉边界争议大的样本。这是血泪经验,模型对边界模糊的框再怎么调 IoU 阈值都没用。
现象:模型对远处粉尘完全没反应,但整体 mAP 不低。
原因:远处粉尘框只有十几个像素,YOLO 骨干下采样到 1/32 后特征几乎消失;mAP 被近处大目标拉高了,远处小目标的问题被平均指标掩盖。
解决:训练时把imgsz提到 960,数据增强里保留更多小目标;推理阶段用切片推理,把大图切成 640x640 的小块分别检测后再合并。小目标不是玄学,是分辨率和特征尺度的问题。
现象:某一类样本太少,训练后该类别的 precision/recall 永远是 0。
原因:类别不平衡严重,模型把少数类别全当成背景。粉尘相关数据里常见“只有烟尘,没有火焰”或“只有火焰,没有烟尘”的单边分布。
解决:先统计每个类别的实例数。方法是遍历 labels 下所有 txt,统计第一列每个索引的出现次数。少数类别过采样:复制该类别的图片和 txt,加入训练集;或者删除部分多数类别样本把比例压到 1:3 以内。YOLOv8 没有直接的 class weight 参数,常见做法就是过采样,别指望 loss 自动均衡。
5.3 训练策略层:数据泄漏和类别索引错位
现象:验证集 mAP 高达 0.93,部署到现场却漏检严重。
原因:数据划分时按单张图随机切分,同一视频的连续帧同时进了 train 和 val,网络等于提前见过答案。视频抽帧数据集尤其容易犯。
解决:按视频片段或场景目录划分,而不是按单张随机分。上面 4.1 的脚本已经处理了这一点,但要注意文件名前缀的提取必须准确,否则分组失效。
现象:某张图预测出来的类别索引和实际完全对不上,比如粉尘标成了 3。
原因:data.yaml的names顺序和转换脚本里的class_names不一致,或者原标注数据里有一个类别标号偏离。
解决:训练前检查所有标签文件的最大类别索引,必须小于names的长度。
ls dust_data/labels/*.txt | xargs awk '{if ($1 > max) max=$1} END {print "max class idx:", max}'如果输出大于等于类别数,说明有标签索引越界,回查转换脚本的类别映射。这个坑经常在“别人转好的数据”里出现,拿到手不要直接信标注版本号,先跑这一行。
6. 验证与进阶:用混淆矩阵和切片推理压出模型上限
训练完先不要急着部署。我一般会打开runs/detect/train/confusion_matrix.png,重点看背景列非零值。粉尘检测最常见的问题不是把粉尘认成别的,而是把背景认成粉尘,直观表现就是背景那一列非零值过大。如果只盯 mAP,很容易被加权结果骗过去。
下一步是切片推理。粉尘监控画面往往是大画幅相机或 4K 球机,整图直接送进模型,远处的目标缩到几十像素,检测效果很差。常见做法是用 SAHI 这类切片推理工具,把大图切成小块,每块单独做检测,再把结果合并回原图坐标。
from sahi.model import Yolov8DetectionModel from sahi.predict import get_sliced_prediction model = Yolov8DetectionModel( model_path="runs/detect/train/weights/best.pt", confidence_threshold=0.25, ) result = get_sliced_prediction( "test_images/dust_site_014.jpg", model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, )切片大小和训练时的imgsz保持一致,重叠率 0.2 能避免目标刚好被切到边界时丢失。拼接回原图后,如果检测框边缘锯齿明显,说明切片重叠率不够或置信度阈值太低。
我的习惯是每次拿到这样的数据集包,先写一个检查脚本,把文件数量、空标签、越界坐标、重复图片 md5 全部打印出来,再做格式转换和训练。模型调参再精细,也补不了数据集的债。先花半天把数据底细摸清,后面能省下好几天的调参时间。希望帮到你。
本文还有配套的精品资源,点击获取