简介:这是一份面向环境监测、工业安全与计算机视觉初学者的粉尘检测专用目标检测数据集,覆盖实际监测场景中多样化的粉尘颗粒分布形态,可支撑空气质量评估、工地/工厂粉尘实时预警及AI环保教学等应用。资源按训练、验证、测试清晰划分,训练集797张、验证集83张、测试集42张共922张图片,全部标注为dust单类别,采用YOLO格式边界框,能直接用于主流检测框架训练与部署。压缩包共1846个文件,主体为922张jpg图像及对应922份txt标签文件,另含yaml配置与docx数据说明,总大小59.23MB,目录结构便于按数据集分组调用。已有261人学习下载,适合需要快速搭建粉尘检测模型、完善工业安全监控系统或完成课程设计/竞赛项目的开发者与研究者。
1. 粉尘检测数据集.zip:这份压缩包能不能直接训练,取决于拆包后这三件事
你刚拿到手的这份粉尘检测数据集.zip,十有八九不是为训练准备的。它可能是监控视频抽帧后送标注回来的产物,也可能是群里转了几手的老包,zip 解压出来之后目录结构、标注格式、标注质量全是未知数。直接解压扔进训练脚本,最常见的结局是格式报错、验证集指标虚高、模型把路面反光当成扬尘。
粉尘检测要识别的是烟羽、扬尘这类边界模糊、对比度低的目标,数据质量的决定因素比模型结构更大。这篇笔记会把这套 zip 从拆包、清洗、转格式到训练、复盘的全流程走一遍,重点讲格式怎么判断、坐标怎么换算、参数怎么设、坑在哪。适合正在做工地扬尘、粉料仓、矿山粉尘视觉检测,手里有数据集但还没跑通完整流程的从业者。
2. 解压后的第一件事:先盘目录结构、标注格式和帧间重复
2.1 先看文件清单,再决定解压策略
拿到任何检测数据集,我都不急着解压,先看一眼压缩包内的文件清单。这个习惯帮我避开过很多“标题写着数据集,里面其实是残缺标注”的坑。特别是从网盘或群聊转手的 zip,常常带密码,或者里头又套着一层目录,zip 解压到一半才发现文件不全、标注格式对不上,返工成本很高。先花两分钟读清单,能确认三件事:文件总数、扩展名分布、目录层级。
# 确认压缩包是否完整,看末尾统计行 unzip -l dust_dataset.zip | tail -n 5 # 统计扩展名分布,判断是不是“图片+标注”成对出现 unzip -l dust_dataset.zip | tail -n +5 | awk '{print $NF}' | awk -F. '{print $NF}' | sort | uniq -c | sort -rn第一行里的 tail -n 5 能看到“N files”结尾的总文件数,如果 zip 传输途中损坏,这里会直接报错而不是出统计。第二行跳过 unzip -l 的前四行表头,取每行最后一个字段(文件名)再按扩展名计数。如果 jpg 和 xml 数量接近,大概率是 VOC 风格;jpg 和 txt 数量接近,可能已经拆成 YOLO 格式;如果图片数量是标注文件的好几倍,那就得留心是不是只有部分图片做了标注。
目录层级也要看。标准的 VOC 结构是 JPEGImages 和 Annotations 平级;手工打包的数据集经常所有图片堆在一个目录里,文件名还是随机 hash,这种包漏标概率很高。把清单存下来留着后面做统计基准,比解压完再翻文件省事得多。
2.2 三句话分辨 VOC / COCO / YOLO 标注格式
标注格式决定转换脚本,先认格式再动手。常见做法是随便挑一张图,打开同名的标注文件看字段:VOC 的 XML 里有<object><name>dust</name><bndbox>;COCO 的 JSON 里有images和annotations两个数组;YOLO 的 txt 每一行是“类别 中心点x 中心点y 宽 高”,五个数字全部是相对图片宽高的比例,范围在 0 到 1 之间。
为了确认类别名和样本数,建议直接扫 XML 里的 object 节点做统计。这一步比想象中重要,粉尘数据集的类别名常常是 dust、smoke、dust_small,甚至有人把“道路扬尘”和“烟囱冒烟”分成两类,你得先知道模型到底要学什么。
import xml.etree.ElementTree as ET from collections import Counter from pathlib import Path label_counts = Counter() for xml_file in Path("Annotations").glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.iter("object"): label_counts[obj.find("name").text] += 1 print(label_counts)这段代码只对 VOC 结构生效。如果这里没有.xml文件,就去查有没有.json;如果连 json 都没有,找_classnames.txt之类的说明文件。像 CCPD、CrowdHuman 这类公开数据集有 README 可查,手工打包的粉尘数据集绝大多数没有,类别体系全靠这一步摸出来。统计结果就是后面转 YOLO 时class_map的直接依据,跑完先别删,留着待用。
2.3 帧间重复:最容易被忽略的验证集污染源
粉尘检测数据集大量来自监控或无人机视频抽帧,这就埋了一个雷:连续两帧之间可能只有零点几秒的差异,扬尘位置基本没动。如果直接按随机比例划分训练集和验证集,同一个扬尘画面的两帧可能分别落到 train 和 val 里。训练时模型等于提前见过了验证集答案,指标虚高到看起来完美,真正部署到固定机位的视频上立刻现原形。
先做重复帧排查。完全相同的帧用 md5 去重;差异很小的帧用感知哈希近似去重。粉尘场景因为飞尘颗粒和树影晃动,纯 md5 往往去不掉太多,我一般先按文件名结构做抽稀:命名如果是cam01_0001.jpg、cam01_0002.jpg这种时序结构,就按固定间隔保留,比如每隔 5 帧留 1 帧。
import shutil from pathlib import Path src = Path("images") dst = Path("images_dedup") dst.mkdir(exist_ok=True) for img in sorted(src.glob("*.jpg")): parts = img.stem.split("_") # cam01_0001.jpg -> [cam01, 0001] if len(parts) < 2: continue frame_no = int(parts[-1]) if frame_no % 5 == 0: shutil.copy2(img, dst / img.name)这里取的是下划线后的最后一个数字作为帧号,要求命名规则稳定,否则 int 转换会直接报错。间隔 5 是经验值,扬尘变化慢、镜头静止时可以用 10,风力变化快的场景用 3。如果没有帧号结构,换 md5 方案:
find images -type f -name "*.jpg" -exec md5sum {} + | sort | awk 'seen[$1]++ {print $2}'输出的就是完全重复的文件路径,确认后手动删除或移到备份目录。做完这步再去划分数据集,后面对验证集的判断才有参考价值。
3. 把标注转成可训练格式:VOC 转 YOLO 的最小脚本与坐标边界
3.1 为什么先归一到 YOLO txt:训练框架只认这一行字
主流检测训练框架现在基本统一吃 YOLO 格式的标注:每一行是“类别 中心点x 中心点y 宽 高”,全部基于图片宽高做归一化。VOC 的左上右下绝对值坐标适合人工标注和检查,但不适合直接训练。归一化之后,不同分辨率的图可以塞进同一个 batch,训练时无论怎么 letterbox 都不用重算标注,推理时后处理也简单。
所以转格式这件事躲不掉。与其每次临时凑脚本,不如一次写成可复用的,换个数据集改个class_map就能继续用。下面这段就是我常用的最小实现,不依赖任何标注转换库。
3.2 VOC 转 YOLO 的脚本:坐标换算、越界裁剪和空标注
import xml.etree.ElementTree as ET from pathlib import Path xml_dir = Path("Annotations") txt_dir = Path("labels") txt_dir.mkdir(exist_ok=True) # 类别映射来自第2.2节的统计结果,顺序就是训练时的类别编号 class_map = {"dust": 0, "smoke": 1} for xml_file in sorted(xml_dir.glob("*.xml")): root = ET.parse(xml_file).getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_map: continue bnd = obj.find("bndbox") x1 = float(bnd.find("xmin").text) y1 = float(bnd.find("ymin").text) x2 = float(bnd.find("xmax").text) y2 = float(bnd.find("ymax").text) # 越界框裁剪到图片有效范围内 x1, x2 = max(0.0, x1), min(w - 1.0, x2) y1, y2 = max(0.0, y1), min(h - 1.0, y2) if x2 <= x1 or y2 <= y1: # 无效框直接丢弃 continue x_center = (x1 + x2) / 2 / w y_center = (y1 + y2) / 2 / h box_w = (x2 - x1) / w box_h = (y2 - y1) / h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") (txt_dir / xml_file.stem).with_suffix(".txt").write_text("\n".join(lines))逻辑说明:XML 里的size/width和size/height是图片原始分辨率,归一化必须除以它们,而不是除以训练的输入尺寸如 640。框宽高同样除以原图宽高。坐标越界的处理是 clamp 到 0 到w-1之间,如果 clamp 完发现宽或高已经为 0,说明这个框本身是坏标注,直接丢掉比留着给模型添乱好。
两个容易踩的细节。一是class_map里的 key 必须和 XML 里<name>节点完全一致,中文类别名、大小写、空格都会导致匹配不上;二是没有任何目标的图片会写出空 txt 文件,这是合法的负样本,YOLO 训练时会把它当背景参与学习。很多手工数据集没有负样本,模型就会把地面纹理、路沿当粉尘源,所以空标注文件务必保留,别顺手删了。
3.3 训练/验证集划分:按视频组切,不按单帧随机切
随机划分对独立图片数据集没问题,但对抽帧数据集是灾难。如果 zip 里有 6 路摄像头或 3 段无人机视频,同一事件的关联帧会在随机划分时同时进入 train 和 val,验证指标造假。正确做法是把文件名里同一路摄像头、同一段视频的帧归为一组,按组切分。
import random from collections import defaultdict from pathlib import Path imgs = sorted(Path("JPEGImages").glob("*.jpg")) groups = defaultdict(list) for p in imgs: prefix = p.name.split("_")[0] # cam03_01234.jpg -> cam03 groups[prefix].append(str(p.resolve())) random.seed(123) group_names = list(groups.keys()) random.shuffle(group_names) val_groups = set(group_names[: max(1, int(len(group_names) * 0.15))]) val = [p for g in val_groups for p in groups[g]] train = [p for g in set(group_names) - val_groups for p in groups[g]] Path("train.txt").write_text("\n".join(train)) Path("val.txt").write_text("\n".join(val))这里prefix = p.name.split("_")[0]是假设文件名前缀代表视频来源,如果你的命名是20240112_1530_cam01.jpg这种,就要 split 后取最后一个或指定位置的字段。15% 验证集比例对千张级别够用,数据量更少时可以提高到 20%。切分完打印一下各组的图片数量,如果某个组特别大,单独看它是否同时跨越了不同光照或天气条件,必要时细分成更小的子组。
4. 常见问题排查:粉尘检测数据集标注质量的四类翻车点
4.1 小目标被输入尺寸直接吃没
现象:训练完 recall 很低,远距离扬尘几乎全部漏检,近距离大目标倒是很准。
原因:粉尘源通常在画面深处,标注框可能只有 20×20 像素。输入尺寸设为 640 时,这些框在下采样后只剩极少特征位置,跨尺度特征融合也救不回来。
解决:先统计标注框的像素面积分布。
from pathlib import Path w, h = 1920, 1080 # 换成数据集中主流图片分辨率 areas = [] for txt in Path("labels").glob("*.txt"): for line in txt.read_text().strip().splitlines(): _, cx, cy, bw, bh = map(float, line.split()) areas.append(bw * w * bh * h) areas.sort() print("count:", len(areas)) print("median:", areas[len(areas) // 2])归一化框宽乘图片宽、框高乘图片高,得到像素面积。中位数面积占比低于 0.1%(1920×1080 下约 2000 像素)就要警惕,低于 0.05% 属于极端小目标。解决方向有两个:把 imgsz 提到 960 甚至 1280,或者对原图做切片推理(SAHI 类的 tiling 方案),前者直接有效但吃显存,后者适合部署阶段用。
4.2 同一个画面重复进入训练集和验证集
现象:训练 loss 正常,验证 mAP 高到 0.95 以上,感觉模型已经完美;换一段真实监控视频测试,掉得惨不忍睹。
原因:抽帧后没去重,随机划分又把重复帧分到了两边。验证集见过的内容,训练集早学过了,指标当然是虚的。
解决:训练前做三件事。第一,按 2.3 节方法清掉完全重复的帧;第二,图像增强时不要对验证集做任何随机增强;第三,切分时按视频组走,别按单帧随机。检查已经发生的污染也很容易:统计 train.txt 和 val.txt 中同名文件前缀的重叠数量,重叠大于 0 就说明切分方式有问题。
cut -d'/' -f1- train.txt | sort -u > train_prefix.txt cut -d'/' -f1- val.txt | sort -u > val_prefix.txt comm -12 train_prefix.txt val_prefix.txt | head -20如果输出不为空,删掉重复组或重做划分,再谈后面的训练。
4.3 漏标与标注边界不一致
现象:训练 loss 降到 0.05 附近开始震荡;推理结果中误报集中在某类固定背景,比如干燥路面或卸料口下方。
原因:多人标注时对“粉尘边缘”的理解不统一。一个人框到烟羽外圈,另一个人只框内核;远处的淡烟看不清就直接漏标。负样本区域基本没人标,模型只能靠正样本硬学,自然会把相似纹理当目标。
解决:抽样打开标注叠加可视化,先看同类目标框的宽高比分布,如果同一类框从 0.3 到 3.0 都有,标注一致性堪忧。粉尘这类边界模糊目标,建议定一个规则:烟羽框到可见边缘,外扩不超过 2 像素;透明度低于阈值的不标。漏标区域只能人工补,或者先不补,把误报样本作为 hard example 加入负样本池,训练时让模型自己学会拒绝。
4.4 类别名混乱与空标注文件
现象:转换脚本跑完,发现 labels 目录比 images 少了几十个文件;训练时 class 数量对不上,直接报错。
原因:XML 里中文名和英文名混用,比如“扬尘”“烟尘”“dust_far”指向同一个东西,class_map里只写了dust,匹配不上就被 continue 丢掉了;另外有人把没有目标的图片单独放了个目录,根本没生成空 txt。
解决:类别映射做多对一,别名全部归一到一个编号。用一个小脚本打印所有没生成 txt 的图片,逐个看是没有目标还是漏转换。空 txt 一律补齐,YOLO 不要求每张图都有标签文件,但数量一致能帮你快速发现文件配套问题。
find images -type f -name "*.jpg" | wc -l find labels -type f -name "*.txt" | wc -l两个数量不一致时,用find images ... | while read f; do [ -f labels/$(basename $f .jpg).txt ] || echo $f; done列出缺失清单。
5. 用 YOLOv8 训练自己的粉尘检测数据集:参数怎么设,为什么这么设
5.1 模型规模与输入尺寸:先回答“目标有多小”
粉尘检测的类别通常只有 1 到 2 类,目标特征是边界模糊、低对比度、背景多样。模型容量不是主要矛盾,数据噪声才是。常见做法是从 yolov8s 起步,数据量低于 500 张时用 yolov8n 更稳,大模型如 yolov8l 在这种低信噪比标注下很容易过拟合到标注噪声。
输入尺寸的决策依据是 4.1 节的面积统计。中位数面积占比低于 0.1% 就上 960;低于 0.05% 建议 1280 或者直接切片。显存不够时优先降 batch 而不是降 imgsz,因为对小目标来说输入尺寸的影响远大于 batch。
这时需要一份数据集配置文件,路径、类别数、类别名都在这里定死:
# dust.yaml path: /data/dust train: images/train val: images/val nc: 2 names: 0: dust 1: smokenc必须和转换脚本里的class_map长度一致,names的顺序就是训练输出里类别编号的顺序。改类别名不影响训练,但会影响你后面读预测结果,建议一开始就定清楚。
5.2 数据增强参数表:哪些开大、哪些必须关小
| 参数 | 推荐值 | 为什么这么设 |
|---|---|---|
| model | yolov8s.pt | 用小模型承接过拟合风险,粉尘目标不需要深度语义特征 |
| imgsz | 960 | 小目标是主要矛盾,640 的损失没法靠增强补回来 |
| batch | 16 | 小数据集 batch 过大容易过拟合,16 是折中值 |
| epochs | 100-150 | 配合早停,数据噪声大时训练波动明显 |
| mosaic | 0.5 | 完整烟羽被切碎会丢上下文,全关又牺牲样本利用率 |
| mixup | 0.0 | 粉尘边界本就模糊,mixup 叠加后 label 内容失真严重 |
| close_mosaic | 10 | 最后阶段恢复完整目标分布,让回归头稳定下来 |
| fliplr | 0.0 | 固定机位受光照方向影响,左右翻转可能制造错误对称样本 |
| patience | 20 | 20 轮不涨就停,省时间也防过拟合 |
增强参数的核心逻辑是:粉尘目标的上下文比常规目标更重要。烟羽的形状依赖风场和背景,mosaic 把四张图拼起来会切断烟羽和扬尘源的关联;mixup 直接改变目标像素的透明度,让本来就模糊的目标更不可学。这两个参数是粉尘检测里最值得调低的东西。
5.3 跑一次最小训练:命令行与参数落点
yolo detect train \ data=dust.yaml \ model=yolov8s.pt \ imgsz=960 \ batch=16 \ epochs=120 \ mosaic=0.5 \ mixup=0.0 \ close_mosaic=10 \ fliplr=0.0 \ patience=20 \ device=0训练结束后看runs/detect/train/weights/best.pt,loss 曲线只反映收敛情况,不能只看它判断模型好坏。如果训练 loss 稳定下降但 val loss 震荡,优先回头看数据:负样本够不够、重复帧清了没有,而不是继续调增强参数。增强只是止血,数据问题靠参数救不回来。
6. 越用越准的闭循环:用 Bad Case 反哺标注,按业务阈值选置信度
训练完第一轮,先别急着部署。用 best.pt 跑一遍 val 集,打开save_txt=True和save_conf=True,把预测框连同置信度全部落盘:
yolo predict \ model=runs/detect/train/weights/best.pt \ source=val_images \ save_txt=True \ save_conf=True输出的每个 txt 文件里是“类别 中心点x 中心点y 宽 高 置信度”六个数。接下来我一般写个十几行的小脚本,把预测框和 GT 框做 IoU 匹配,专门挑两类样本:IoU 超过 0.5 但置信度低于 0.4 的“该中没中”,以及置信度高于 0.5 但完全没有 GT 匹配的“不该中中了”。这两类样本分别对应漏标和误报,是数据质量最直接的反馈。
置信度阈值不要照着默认的 0.25 用。先看业务成本:误报代价高(比如触发扬尘报警导致停工检查),阈值提到 0.4 以上;漏检代价高(环保抽查需要保证检出),阈值降到 0.2。从 val 的 PR 曲线上找到 Precision 和 Recall 的交点附近作为起点,再根据真实场景微调。
把这两类 Bad Case 积累下来,人工复核后合并进原数据集,改标注、补负样本、重新训练。一个轮次下来,数据集质量会有肉眼可见的提升,这就是我把模型训练和数据标注做成闭环的原因。我现在拿到任何检测数据集,第一件事永远是统计 GT 框的尺度和数量分布,这个习惯救过我太多次,很多坑不是训练跑出来的,是数据一开始就埋下的。希望帮到你。
本文还有配套的精品资源,点击获取