简介:面向计算机、电子信息工程、数学等专业学生在课程设计、期末大作业或毕业设计中经常遇到的海量数据标注与清洗难题,可直接采用这套基于Qt与C++的YOLO数据集清洗工具。压缩包共7个文件,体积仅12KB,以cpp源文件、ui界面文件、pro工程文件为主,并附带说明文档,代码设计采用参数化编程,关键参数可方便更改,注释明细,逻辑清晰,还包含运行结果示意,便于使用者快速掌握工具的运行流程。工具可直接用于筛选、整理与清洗目标检测模型训练所需的数据,适配计算机视觉、目标检测相关课题的课设与毕设环节。目前已有377人学习下载,资源作者为资深算法工程师,从事YOLO算法仿真工作多年,代码均经过测试运行成功,若遇到运行问题还可私信沟通,能够降低读者二次开发和调试的门槛。
1. 跑 YOLO 训练的人迟早会被数据集反咬一口
模型结构没改、学习率没动、epoch 翻倍,换了个数据集之后 loss 曲线开始乱跳,val 指标忽高忽低,甚至训练直接报IndexError: index 14 is out of bounds。这种问题大概率不是网络的问题,而是训练数据里的标注文本在拖后腿。YOLO 的标注文件是个极简的 txt,一行五个数字,看起来工整得像文本文件里的三好学生,但类别 id 越界、坐标越界、空标签、重复图、损坏图,每一类都会在不同阶段给你颜色看。所谓数据集清洗工具,就是把这些脏问题在进训练 pipeline 之前扫出来、隔离开、并输出一份能定位问题来源的报告。适合谁用?自己标过数据、从网上扒过数据集、或者从 kitti 标注转 yolo 做过格式转换的人——你不是缺模型技巧,是缺一次把数据底细摸清的操作。
2. yolo 标注的真实约束:哪些问题必须清,哪些问题不该动
2.1 yolo 格式的隐藏假定
YOLO 标注每行由五个浮点数构成:class_id x_center y_center width height。前两个问题是绕不开的:坐标全部归一化到[0,1],类别 id 是从 0 开始的整数。这背后有两个容易被忽略的假定。
第一个假定是“类别 id 必须在模型的 names 列表长度之内”。如果数据集的类别文件里有 10 个类,但某张图的标注里写了class 10,yolov8 训练自己的数据集时,DataLoader 不会报错——它会在 loss 计算阶段悄悄把这个 target 当成无效数据,反而让你误以为是模型不收敛。
第二个假定是“图片必须有对应尺寸才能算归一化坐标”。坐标归一化之后,人眼无法直接判断0.2 0.3 0.0001 0.0001到底意味着什么。它可能是一个在 640×640 图上只有 0.06 像素宽的小框,也可能是一个因转换脚本写错而导致的负值。清洗工具要做的第一件事,就是把归一化坐标还原成像素坐标去审视。
所以清洗工具不能只做“格式校验”这一步,它需要同时处理图片文件、标注文件、类别文件,三者之间的关系才是脏数据藏身之处。
2.2 硬错误 vs 软问题:清洗工具的边界
先明确哪些是硬错误——必须过滤或修复,否则训练过程会出现不可预期的行为。
| 问题类型 | 判定方式 | 处理建议 |
|---|---|---|
| 类别 id 越界 | class_id < 0或class_id >= len(names) | 过滤该图或修正到有效范围 |
| 坐标越界 | 还原像素后 `x1<0 | |
| 尺寸异常 | 还原像素后w<=0或h<=0 | 直接过滤该标注行 |
| 宽高比极端 | w/h > max_ratio或< 1/max_ratio | 过滤,常见阈值 20 |
| 图片损坏 | cv2.imread返回None | 该图及其标注一起隔离 |
| 空标注文件 | txt 大小为 0 或只含换行符 | 按项目策略决定是否保留 |
另一种是软问题,例如重复图片、模糊图片、标注框过小(比如小于 5×5 像素)、类别分布严重失衡。这些不该由工具自动删除,因为“小目标”在某些场景下就是要关注的。清洗工具应该默认只隔离硬错误,把软问题统计进报告,由人来决定下一步。把边界划清楚,工具才不会误伤数据。
2.3 不是所有“看起来脏”的东西都要清
做清洗工具最容易犯的错是拿“数据增强”的逻辑去剪数据。随机裁剪产生的小目标、旋转后贴近边缘的目标,在原始图中可能确实坐标越界几个像素,但这在 mosaic 增强里本来就是常态。我的建议很直接:工具里选一个--strict标志,默认不严格。这才是可靠的从业方案——先统计,再处理,不要自动删掉你不了解的数据。
3. yolo 清洗工具的核心实现:解析、检查、隔离、报告
3.1 工具结构设计
按“单机脚本 + 配置控制”的方式落地,不引入训练框架的依赖,只靠标准库加 OpenCV。目录结构如下:
yolo_cleaner/ ├── cleaner.py ├── config.yaml ├── requirements.txt └── README.mdcleaner.py负责四件事:扫描数据集、解析标注、执行检查、输出报告。扫描阶段要把图片目录和标注目录配对;解析阶段按行读取 txt;检查阶段是一个规则列表,每条规则返回一个问题类型;报告阶段输出经过隔离后的清单和统计。规则写成列表而非 if-else 堆叠,方便扩展。
3.2 解析与检查的代码骨架
import argparse import hashlib from pathlib import Path def parse_label_line(line: str, img_w: int, img_h: int): """解析yolo标注的一行,返回像素坐标和类别id""" parts = line.strip().split() if len(parts) != 5: raise ValueError(f"标注行格式错误: {line!r}") cls_id = int(float(parts[0])) x_c, y_c, w, h = map(float, parts[1:]) if w <= 0 or h <= 0: raise ValueError("目标框宽高必须大于0") x1 = (x_c - w / 2) * img_w y1 = (y_c - h / 2) * img_h x2 = (x_c + w / 2) * img_w y2 = (y_c + h / 2) * img_h return cls_id, x1, y1, x2, y2这段代码先把归一化坐标转成像素坐标,再做后续判断。注意int(float(parts[0])):有些标注工具会写出1.0这样的类别 id,直接用int(parts[0])会抛 ValueError,先转 float 再转 int 更稳。
接下来是检查函数的组合方式:
def check_image_and_label(image_path: Path, label_path: Path, names: list, min_side: int = 5, max_ratio: float = 20.0): """返回该图像的标注检查结果列表""" import cv2 img = cv2.imread(str(image_path)) if img is None: return ["image_broken"] img_h, img_w = img.shape[:2] issues = [] valid_lines = [] for line in label_path.read_text().strip().splitlines(): try: cls_id, x1, y1, x2, y2 = parse_label_line(line, img_w, img_h) except ValueError as e: issues.append(f"label_parse_error: {e}") continue if cls_id < 0 or cls_id >= len(names): issues.append("class_id_out_of_range") continue if x1 < 0 or y1 < 0 or x2 > img_w or y2 > img_h: issues.append("bbox_out_of_image") continue if (x2 - x1) < min_side or (y2 - y1) < min_side: issues.append("bbox_too_small") continue if (x2 - x1) / ((y2 - y1) + 1e-6) > max_ratio or (y2 - y1) / ((x2 - x1) + 1e-6) > max_ratio: issues.append("bbox_extreme_ratio") continue valid_lines.append(line) return issues, valid_lines这里的关键是valid_lines的保留逻辑:一行有问题只丢一行,不丢整张图。整张图被隔离的只有两种情况——图片无法解码,或者所有标注行都被判定为硬错误。其余场景,只修正并写回清洗后的 txt。bbox_extreme_ratio的阈值是经验值,长条形目标(比如桥墩病害数据集里的裂缝、电线)可以在配置里调大。
3.3 重复图片检测
重复图片问题常见于爬取积累的数据集,hashlib的方案简单有效:
def deduplicate_images(images: list) -> set: """基于md5查找重复图片,返回需要隔离的路径集合""" seen_md5 = {} duplicates = set() for img_path in images: h = hashlib.md5(img_path.read_bytes()).hexdigest() if h in seen_md5: duplicates.add(img_path) duplicates.add(seen_md5[h]) # 两张都进隔离区,让人决定留哪张 else: seen_md5[h] = img_path return duplicatesMD5 只用于去重,不用于安全校验,所以碰撞概率忽略不计。两张重复图都放进隔离区而不是只留一张,因为不同目录下的同名标注可能对应不同的标注结果,单方面保留任意一张都可能丢失有效标注。
3.4 隔离策略:复制而不是移动
工具默认把问题文件复制到quarantine/下,而不是直接删掉或移动源文件。原因很朴素:你不确定“问题”是不是真问题,特别是当数据来自第三方工具(比如 cvat 导出的 yolo 格式偶尔会把空标签生成 0 字节文件)。复制之后,原始数据保持原样,清洗结果可以直接喂给训练 pipeline,二者互不影响。这是我在多轮实践中确定下来的方案,配合下面的报告文件,回溯成本几乎为零。
4. 把清洗工具接进 yolov8 训练自己的数据集流程
4.1 命令行参数与配置文件
配置用 YAML,命令行参数只覆盖最常用的选项。这样既能跑默认策略,又能在不同项目间复用规则。
| 参数 | 默认值 | 说明 |
|---|---|---|
--data-dir | 无 | 数据集根目录,需包含 images 和 labels 子目录 |
--classes-file | classes.txt | 类别文件,每行一个类名 |
--quarantine | ./quarantine | 问题文件复制目的地 |
--min-side | 5 | 最小目标边长(像素) |
--max-ratio | 20.0 | 最大宽高比 |
--empty-strategy | report | report仅报告,filter则隔离空标签 |
--dedup | false | 是否启用重复图片检测 |
启动命令示例:
python cleaner.py \ --data-dir ./bridge_defect_dataset \ --classes-file ./classes.txt \ --quarantine ./quarantine \ --min-side 8 \ --max-ratio 15 \ --dedup true逻辑说明:bridge_defect_dataset目录下要求是images/和labels/的并列结构,这是 yolo 系列最常用的数据集布局。--min-side 8比默认更严格,针对桥墩病害这类细节纹理目标,太小的框(比如几像素宽)对训练的影响往往是噪声而不是特征。--max-ratio 15会放过线状目标,但如果数据集是普通物体检测,这个值建议降到10以下。
4.2 软问题的报告输出
工具跑完会在输出目录生成三份东西:
cleaned/ ├── images/ ├── labels/ ├── report.csv └── report.jsonreport.csv的列包含image_path, label_path, issues, action。issues是逗号分隔的问题类型,action是kept/filtered/copied。用 Excel 打开后能直接按问题类型排序,比终端输出更适合整理给标注团队看。report.json则给脚本用,后续接 CI 或者自动评估时解析方便。
4.3 与标转 yolo 的常见错位
从 kitti 标注转 yolo 的脚本有一个高频 bug:kitti 的坐标是像素值,转 yolo 时需要除以图片宽高,但很多脚本用的是x2/w而不是(x2-x1)的归一化。这个错误的后果是解析时宽高比异常和越界全部爆发。清洗工具正好能卡住这一层:跑一遍发现大量bbox_extreme_ratio时,第一反应不应该是调阈值,而应该是回去查转换脚本。
5. 从清洗报告到验证:不要只看过滤数量
清洗工具最后的产出不是“删了多少张”,而是一张能验证清洗必要性的对照表。具体做法是:把清洗后的数据集作为一个新的数据版本,与原始数据集分别跑一个短训练实验,对比前 20 个 epoch 的 val 指标曲线。如果清洗后的曲线明显更平滑,说明之前的问题是数据噪声导致;如果两条曲线几乎重合,说明你清掉的大部分是冗余样本,那 min-side 之类的参数需要调宽松。这个验证过程才是清洗工具最被低估的价值——他能帮你确认问题不在模型侧。
report.csv里还有一个常被忽略的字段:filtered_count。如果某个类别被过滤的框数占该类总数的比例超过 10%,就要警惕是否是标注规范问题。比如某个标注员把超出图像边界的完整目标也框了进去,这类错误通常集中在个别文件名前缀里,说明是人的标注习惯差异,而不是数据本身的问题。仅靠自动工具无法发现这一点,需要定期抽看报告中的这个分布。
最后一个顺手技巧:清洗完成后,用train/val划分前做一次全量校验。我通常会把工具以--empty-strategy report模式再跑一遍,确认清洗后的数据集里没有空标注文件混进 val 集。空标注样本在 val 阶段不会直接报错,但 mAP 计算时会成为 recall 的隐藏负数——它让模型永远无法从这张图中学会任何东西,指标维度上却算作一次有效预测。这是 yolov8 训练自己的数据集时最容易被忽略的最后一个坑。
本文还有配套的精品资源,点击获取