水域水面垃圾数据集实战:从YOLO/VOC格式校验到模型训练与验收

发布时间:2026/10/7 6:02:57
水域水面垃圾数据集实战:从YOLO/VOC格式校验到模型训练与验收 简介面向水面垃圾目标检测的实用数据集适用于YOLO、VOC系列检测框架帮助解决河道、湖面、海岸等场景下塑料废弃物识别训练样本不足的问题。数据聚焦饮料罐、玻璃瓶、塑料袋、塑料瓶、塑料废弃物、塑料容器、塑料垃圾等7类常见目标图片已经过增强处理有助于提升模型在不同光照、水纹和拍摄角度下的泛化能力。压缩包共2000个文件以VOC格式xml标注文件为主1999个另含1个说明txt整体约196.91MBxml标注与YOLO格式txt标注两种形式均可直接接入常见训练流程。标注框数存在明显不均衡例如塑料瓶框数约5686个玻璃瓶、塑料袋、塑料容器等类别样本相对少可作为类别不均衡调参的练习数据。已有475人学习压缩包内同时提供VOC与YOLO两种格式标注目录命名清晰便于快速核对样本与标注适合目标检测入门、算法对比及水面垃圾监测项目验证。1. 水域水面垃圾数据集4961 张增强图片能不能直接喂给 YOLO做河道漂浮物打捞、水库坝前垃圾拦截、无人机水面巡检这类目标检测项目最卡脖子的不是模型选型而是训练数据。自己下水拍素材、一框一框标坐标一个千级数据集往往要耗掉两到三周人力。这个名为“水域水面垃圾数据集”的 zip 压缩包声称提供 yolovoc 双格式、共 4961 张已增强图片正好命中“拿现成标注数据起步”的诉求。它的核心价值是省掉了采集和标注环节解压后可以直接切训练/验证集喂给 yolov5、yolov8 这类常用检测框架。但“已增强”三个字也意味着要对标注一致性多留个心眼不能解压完闭眼全量开训。接下来我按双格式对账、坐标转换、训练配置、快速验证这条路径把这份数据真正吃到模型里踩过的坑和判断标准一起说清楚。2. 解压 zip 后的双格式对账jpg、xml、txt 三件套缺一样都别急着训练2.1 先看 zip 内部是不是标准目录结构拿到 zip 压缩包第一步不是直接开训而是解压并确认目录结构。YOLO 系的规范是images/放图片、labels/放同名 txt 标签文件VOC 系则是JPEGImages/放图、Annotations/放 xml、ImageSets/Main/放 train.txt 和 val.txt 列表。很多数据打包会同时给出这两套各放各的目录。用 unzip 解压后先看两层目录结构unzip 水域水面垃圾数据集.zip -d water_garbage tree water_garbage -L 2如果系统没有 unzip直接用 Python 解压 None 也能完成同样的事还顺带能看到压缩包里的文件清单前几行import zipfile with zipfile.ZipFile(水域水面垃圾数据集.zip) as zf: names zf.namelist() print(压缩包内文件数:, len(names)) for n in names[:20]: print(n)tree -L 2只显示两层目录足够判断是 YOLO 风格还是 VOC 风格还是两套都在根目录下分开放。此时不用急着给每一个文件验身先建立整体印象图片格式是不是统一 jpg标签文件是不是和图片同名有没有单独的 ImageSets 目录。如果发现压缩包内文件名除了正常 stem 外还带_flip、_brightness、_rotate这类后缀说明增强副本是直接复制文件改名生成的这类副本的标注坐标需要额外校验后面专门讲。2.2 用 Python 脚本对账三件套缺失清单检测项目的常见翻车现场是压缩包下载到一半损坏、解压时漏文件、或者某张图片只有 xml 没有 txt导致训练时终端报错且报错信息往往只给一个文件名不带上下文。与其让训练跑到一半炸掉不如提前把三件套对账做掉。下面的脚本以图片目录为基准检查每张 jpg 是否都有同名的 txt 和 xmlimport os from pathlib import Path root Path(water_garbage/VOC) # 换成你的实际解压根目录 img_dir root / JPEGImages ann_dir root / Annotations yolo_dir root / labels # 有些包把 yolo 标签也放 VOC 根下 image_files [p for p in img_dir.iterdir() if p.suffix.lower() in {.jpg, .jpeg, .png}] missing_ann [p.name for p in image_files if not (ann_dir / (p.stem .xml)).exists()] missing_txt [p.name for p in image_files if not (yolo_dir / (p.stem .txt)).exists()] print(图片总数:, len(image_files)) print(缺 xml:, len(missing_ann), missing_ann[:10]) print(缺 txt:, len(missing_txt), missing_txt[:10])这段脚本的核心逻辑是用图片作为唯一基准反查标注文件是否存在而不是遍历标注目录找图片因为标注缺失会静默跳过而图片缺失时训练报错更明显。如果发现缺xml先看是不是只有某类增强副本缺比如所有_flip后缀的都没有 xml而原图都有那就是打包生成增强数据时漏跑了标注复制需要从原图 xml 复制后改名。如果缺 txt同理。两套格式里只要有一套齐全训练还能跑但两套都缺同一批文件就要重新下载压缩包不要自己脑补修复。2.3 VOC 的 xml 和 YOLO 的 txt 字段如何对应对账完成后要确认两套格式的坐标类型和字段语义一致否则后面转格式一定会出错。VOC 的 xml 存的是绝对像素坐标YOLO 的 txt 存的是归一化中心点加宽高两者换算关系如下表信息VOC xmlYOLO txt图片宽高size/width、size/height不存依赖图片实际尺寸目标类别object/name字符串第一列整型类别 ID目标坐标bndbox/xmin、ymin、xmax、ymax绝对像素第二至五列 cx、cy、w、h 归一化到 0~1数量多个object节点每行一个目标需要注意一个坑有些打包脚本偷懒把 YOLO 的归一化坐标直接填进 xml 的 bndbox此时 xmin/ymin 是 0 到 1 的小数而不是像素值LabelImg 这类工具打开会显示坐标错乱。判断方法很简单随机抽一个 xml看 bndbox 的文本内容如果是小数就是被处理过的伪 VOC 格式。这类 xml 跑 mmdetection 时会诡异出错但对 YOLO 训练影响不大因为训练读的是 txt。这里给一个判断坐标类型的快速脚本import xml.etree.ElementTree as ET from pathlib import Path sample Path(water_garbage/VOC/Annotations/000001.xml) root ET.parse(sample).getroot() box root.find(.//bndbox) xmin float(box.find(xmin).text) print(xmin 值:, xmin, 小于 1 说明可能是归一化伪格式)2.4 双格式不一致时到底以哪个为准实际拿到手的数据集txt 和 xml 大概率会存在轻微不一致比如某个类在 xml 里叫plastic在 txt 里对应 ID 是 1又比如同一张图的边界框坐标在小数点后第四位不一致。我的处理原则是做 YOLO 训练以 txt 为准因为训练脚本只读 txt做 VOC 工具链或需要可视化标注审查时以 xml 为准把 xml 拉回 LabelImg 里人工看一眼。核心原则是图片才是唯一真源所有坐标都从图片尺寸推导所以下一步我会用转换脚本把两套格式重新对齐一遍而不是信任打包方声称的“双格式完整”。3. VOC 与 YOLO 互转坐标换算脚本里的四个必改参数3.1 从 VOC xml 转 YOLO txt归一化与边界裁剪如果你的工作流要基于这份数据集重新清洗标签或者想把 xml 中的类别名变成符合自己规范的 ID最常见的操作就是把 VOC 转成 YOLO txt。转换本身不复杂但有几个参数改错会直接污染训练集归一化时除的是图片宽高裁剪时限制的是 0~1 区间类别映射查表不能写错。下面给一个可直接用的转换脚本import xml.etree.ElementTree as ET from pathlib import Path def voc2yolo(xml_path: Path, out_txt_path: Path, class_names: list): 将 VOC xml 转成 YOLO txt class_names: 整个数据集的完整类别列表txt 中 ID 就是该列表的下标 tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: print(f跳过未登记类别: {name}) continue cls_id class_names.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 关键先把坐标裁剪到图片范围内防止负数或超界框 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax xmin or ymax ymin: print(f跳过空框: {xml_path.name} {name}) continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) Path(out_txt_path).write_text(\n.join(lines), encodingutf-8)参数说明分三点。第一class_names必须是完整类别表不能只写你关心的三个类否则没有出现在表中的类会被静默丢弃丢完后训练时类别数会和你预期对不上。第二归一化除数是图片原始宽高不是某个固定值 640因为增强副本可能已经被 resize 过若还用缓存宽高会导致坐标错位。第三.6f保留 6 位小数精度约 0.0001%对检测任务足够不要保留 2 位小目标边界框会明显偏移。xmax xmin这种空框检查不能省水面垃圾数据里常出现标注残影和退化框。3.2 反向转换YOLO txt 转 VOC xml 时尺寸必须从图片读反过来要把这份数据的 YOLO txt 转成 VOC xml用于 LabelImg 人工复核或喂给 mmdetection 时最容易踩的坑是自作聪明从训练配置文件里读图片尺寸。txt 里根本没有宽高信息而归一化坐标必须乘以真实宽高才能还原像素坐标from PIL import Image from pathlib import Path def yolo2voc(txt_path: Path, img_path: Path, out_xml_path: Path, class_names: list): with Image.open(img_path) as im: img_w, img_h im.size # 必须实时读图不要用任何缓存 boxes [] for line in Path(txt_path).read_text().strip().splitlines(): parts line.split() if len(parts) 5: continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:5]) xmin (cx - bw / 2) * img_w ymin (cy - bh / 2) * img_h xmax (cx bw / 2) * img_w ymax (cy bh / 2) * img_h # 越界框做线性裁剪保证 xml 里不会出现负数或超宽高坐标 xmin max(0, round(xmin)) ymin max(0, round(ymin)) xmax min(img_w, round(xmax)) ymax min(img_h, round(ymax)) if class_names and 0 cls_id len(class_names): boxes.append((class_names[cls_id], xmin, ymin, xmax, ymax)) # 拼接 xml 内容关键字段一个都不能少 xml_parts [ annotation, f filename{img_path.name}/filename, f sizewidth{img_w}/widthheight{img_h}/heightdepth3/depth/size, ] for cls_name, xmin, ymin, xmax, ymax in boxes: xml_parts.append( object) xml_parts.append(f name{cls_name}/name) xml_parts.append( bndbox) xml_parts.append(f xmin{xmin}/xminymin{ymin}/ymin) xml_parts.append(f xmax{xmax}/xmaxymax{ymax}/ymax) xml_parts.append( /bndbox) xml_parts.append( /object) xml_parts.append(/annotation) Path(out_xml_path).write_text(\n.join(xml_parts), encodingutf-8)这段代码里有两个容易被忽略的细节filename标签只写文件名不带上级目录size/depth固定写 3 通道。前者是 VOC 工具的常规约定后者是大多数检测框架读取 xml 时的硬性要求。round()把浮点坐标整成像素整数是必要的因为 VOC 协议本身接受浮点但部分工具链解析整数更稳妥。3.3 增强副本的坐标一致性校验“已增强”数据最常见的坑是图片做了水平翻转或亮度调整标注却直接复制原图 txt没有重新计算坐标。水平翻转后的真实框应该是 cx 变为 1-cx如果打包方偷懒这个关系就会被破坏。我一般会单独写一段校验脚本来检查翻转副本from pathlib import Path def check_flip_pair(orig_txt: Path, flip_txt: Path, tol: float 1e-3): 检查水平翻转后的 txt 是否与原图坐标镜像一致 def parse(txt_path): rows [] for line in Path(txt_path).read_text().strip().splitlines(): parts line.split() if len(parts) 5: rows.append((int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]))) return rows orig parse(orig_txt) flip parse(flip_txt) if len(orig) ! len(flip): return False, 目标数量不一致 for o, f in zip(orig, flip): # 水平翻转后 cx 应等于 1 - cx if abs((1.0 - o[1]) - f[1]) tol: return False, fcx 不一致: {o} vs {f} return True, OK垂直翻转对应 cy 变为 1-cy旋转 90 度则 cx/cy 会交换并且要按新宽高重新归一化。实际增强流水线中水面垃圾这类目标做水平翻转最安全因为目标语义左右对称旋转 90 度后塑料瓶和树枝的朝向改变模型学到的特征可能反而不利于部署。校验脚本跑一遍如果发现大量翻转副本坐标不一致说明这份“已增强”数据的标注没有重算训练时宁可只用原图也不要带病使用。4. 已增强数据集的训练配置与五个高频坑4.1 训练集和验证集分离按图像组去重而不是按文件随机分4961 张里包含增强副本意味着原始图片数量可能只有一千多张其余都是翻转、调亮度、加噪等变换产物。如果直接把所有文件随机按比例切到 train 和 val增强副本和它的原图会同时出现在两边验证集就不是“没见过的数据”mAP 会虚高正式部署时立刻打回原形。正确的切分方式是把同一来源的图片归为一组按组划分import hashlib from pathlib import Path def group_key(stem: str) - str: 按文件名去掉增强后缀还原来源图片组名 for suffix in (_flip, _brightness, _rotate, _hsv): stem stem.replace(suffix, ) return stem img_dir Path(water_garbage/JPEGImages) groups {} for p in img_dir.glob(*.jpg): key group_key(p.stem) groups.setdefault(key, []).append(p) # 用组名的 md5 值取模划分保证同源图片一定进同一侧 val_keys set() for key in groups: digest hashlib.md5(key.encode()).hexdigest() if int(digest, 16) % 10 0: val_keys.add(key) val_files [p for key in val_keys for p in groups[key]] train_files [p for key, ps in groups.items() if key not in val_keys for p in ps] print(ftrain 组数: {len(train_files)}, val 组数: {len(val_files)})用 md5 取模做划分好处是以后新增图片时同源组仍能稳定落在同侧不会因为随机种子变化导致数据泄漏。val 比例通过取模数值控制% 10 0大约是 10% 的组进入验证如果觉得太少改成% 5 0就是 20%。这个比例要根据原始真实图片数量来定不要盯着增强后的总量算。4.2 数据集 yaml 的三个必写字段与类别 ID 硬对齐ultralytics 训练入口读的是数据集 yaml字段缺失或顺序错乱是新手最容易低频翻车的地方。最简配置如下# water_garbage.yaml path: D:/water_data # 数据集根目录建议纯英文路径 train: images/train # 相对于 path 的训练图片目录 val: images/val # 相对于 path 的验证图片目录 names: 0: floating_garbage 1: water_vegetation 2: plastic_bottle三个字段缺一不可。names的 key 必须和 txt 标签文件里的第一列整数严格对应比如 txt 里第一列出现过 2那 names 里必须存在 key 为 2 的类别。path千万不要有中文ultralytics 在 Windows 上对中文路径的支持是出了名的玄学明明文件都在却报 Not Found把数据集放到D:/water_data这样的纯英文路径能省掉大量这类折腾。另外类别名建议用英文小写下划线避免后续可视化时 matplotlib 渲染中文乱码。4.3 已增强数据集的五个高频坑坑一loss 正常下降但验证 mAP 为 0。现象是训练过程 loss 曲线很漂亮val 阶段所有图片都预测不出框。原因是 txt 里的类别 ID 超出了 yaml names 的 key 范围比如 txt 里第三列是 5但 names 只定义到 2验证阶段类别映射失败。解决方法是训练前先统计标签里实际出现的类别 IDawk {print $1} water_garbage/labels/train/*.txt | sort -u把输出结果和 yaml 的 names key 逐一对照出现未定义的 ID 就回头重新生成标签。坑二用 OpenCV 画框可视化时图片全空白。现象是cv2.rectangle画出的框不在目标上或者画到图片角落。原因是有人把归一化坐标直接当像素坐标用了没有乘图片宽高。解决方法是画框前先frame.shape取高宽把 cx、cy、w、h 分别换算成 xmin、ymin、xmax、ymax 再画。坑三验证集 mAP 高达 0.9但拍一段真实水面视频检测效果稀烂。现象是训练指标亮眼落地就拉胯十有八九是增强副本和原图泄漏到了同侧数据集。前面 4.1 的按组划分就是针对这个问题的你可以在训练前打印 train 和 val 的文件名前缀人工确认不存在同名前缀分别落在两侧的情况。坑四xml 里filename没有路径工具链识别不了。现象是 LabelImg 打开找不到图片或者转 COCO 时图片路径拼接失败。原因是打包脚本只用Path.name取了文件名没有拼上完整子目录。解决方法是重新生成 ImageSets/Main/train.txt逐行写入不含扩展名的 stem再让工具通过 stem 反查图片路径。坑五训练没几个 epochloss 突变为 nan。现象是 loss 曲线突然跳到 nan 然后不再恢复。原因通常是某个 bbox 坐标越界归一化值大于 1 或小于 0模型算 IoU 时出现负面积。解决方法是训练前跑一遍坐标合法性扫描把所有 txt 里超出 [0,1] 的坐标过滤掉或执行第 3 章里的裁剪逻辑强制还原到合法区间。5. 用 15 分钟小实验验证这套数据能不能用5 轮训练加推理反查5.1 最小训练命令与止损线要不要在这份数据集上投入更多时间先用一个最小训练实验来判断5 轮、yolov8n、不调任何超参数跑完就能看出数据的健康程度pip install ultralytics yolo detect train datawater_garbage.yaml modelyolov8n.pt \ epochs5 imgsz640 batch8 projectwater_check选 yolov8n 而不是 yolov8s 或更大的模型是因为 n 是最小参数量版本训练速度快能在 15 分钟内跑完且如果连小模型都能在 5 轮内让 loss 明显下降说明数据标注质量到位反之如果 5 轮下来 val loss 纹丝不动甚至上扬就不是模型容量问题而是标签本身有问题。imgsz640是默认值如果你的数据集中水面垃圾占比小可以改成 960但显存占用会涨一倍左右batch8是 8GB 显存的安全值显存小就降到 4。跑完看两个指标训练 loss 曲线是否稳步下降验证 loss 是否同步走低。如果两者都满足说明这套数据可以被模型正常拟合如果 val loss 呈现出先降后升的形态说明训练集和验证集分布有偏差优先回去查 4.1 的分组逻辑。5.2 用 best.pt 做一次推理反查标注质量训练输出的 best.pt 是最好的数据质量快照。选 10 张不在训练集里的真实水面照片跑一次推理看框的贴合度mkdir -p sample_imgs yolo predict modelwater_check/detect/train/weights/best.pt \ sourcesample_imgs saveTrue打开runs/detect/predict目录下保存的可视化结果重点看两类情况预测框是否准确包住漂浮物边缘还是明显大一圈包住了整片水面以及是否存在同一片漂浮物上叠加了多个半透明框。第一个问题说明标注框本身比物体大第二个问题说明同一目标在标注时被重复框了多条这类脏数据会让模型收敛后输出重复检测NMS 都压不掉。如果看到框整体偏左上或偏右下说明 xml/txt 里存在系统性坐标偏移有可能是标注工具导出时上下翻转造成的。5.3 我每次拿新数据集的验收习惯这套验证流程我已经跑过很多次现在拿到任何 zip 格式的检测数据集都固定先做三件事解压后对账三件套抽查一个 xml 和一个 txt 的坐标合法性最后跑一次 5 轮小训练。三件事加起来耗时不超过半小时但能过滤掉八成以上“标题很漂亮、数据不能练”的坑。对这份 4961 张的水域水面垃圾数据只要解压后目录结构完整、增强副本坐标能通过 3.3 的一致性校验、5 轮训练里 val loss 有下降趋势就可以放心投入完整训练了之后要做的无非是调 imgsz、调增强策略和类别权重这些常规优化。数据集的验收成本不该被省掉否则后面所有训练时间都只是在给坏数据交学费。希望这份操作路径对你有帮助也祝你的水面垃圾检测器早日跑起来。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询