☰
YOLOv11水面垃圾检测实战:从数据集标注到边缘部署全流程
2026/10/6 11:10:03 网站建设 项目流程

简介:这份资源面向具备一定Python基础的研究人员与环保监测行业技术人员,提供一套基于YOLOv11的水面垃圾检测系统完整实现方案,用于自动识别与定位水面漂浮污染物,可服务于水域污染治理与环保监测等场景。压缩包内共1个docx文档,约41KB,内容涵盖项目介绍、环境准备、数据集配置、模型训练、ONNX模型导出、性能评估与指标可视化、GUI界面搭建及完整代码整合等模块,并附有参考资料、注意事项与未来改进方向。文档以流程记录与关键源码样例为主,读者可据此复现从数据准备到模型部署的全过程,理解多类别垃圾识别与评估指标可视化的实现思路,同时获得超参数调整、过拟合规避等排错经验。目前已有441人学习下载,适合希望快速上手水面垃圾检测项目、对照实操并查漏补缺的中级开发者参考。

1. 水面垃圾检测为什么值得用 YOLOv11 重做一遍

去年夏天帮一个景区做水面巡检,对方原本靠人工划船拍照、上岸后肉眼数漂浮物,一天下来两个人只能覆盖两公里河道,漏检率还高得离谱。换成基于 YOLOv11 的水面垃圾检测系统之后,同一段河道用无人船跑一圈,二十分钟出结果,塑料瓶、泡沫块、枯枝这些目标都能框出来。这不是炫技,是刚需——水面垃圾检测系统要解决的核心问题就三个:小目标多、反光干扰强、部署环境算力有限。YOLOv11 恰好在这三点上都有针对性改进,C3k2 模块和 C2PSA 注意力机制让它在小目标上的召回比前代更稳,同时模型体积还能压到边缘设备能跑的程度。

这篇文章面向两类人:一类是手里有水面图像数据、想训一个能用的检测模型但不知道从哪下手的工程师;另一类是已经跑过 YOLO 系列、想搞清楚水面场景下参数该怎么调、坑在哪的老手。我会把数据准备、环境配置、训练调参、推理部署整条链路拆开讲,每个环节都给可复现的命令和代码,不跳步。整套方案包含完整的程序和数据组织方式,你照着走一遍就能得到自己的水面垃圾检测模型。

2. 水面垃圾数据集怎么整:从原始照片到 YOLO 格式的完整链路

2.1 水面垃圾的类别定义与标注边界

水面垃圾检测和通用目标检测最大的区别在于类别边界模糊。一个泡了一半的塑料袋,你说它是塑料还是其他垃圾?一根挂着水草的树枝,算枯枝还是算水草?我的做法是先把类别收敛到 4 到 6 类,覆盖实际巡检中最需要上报的目标:塑料瓶、泡沫块、塑料袋、枯枝、水草团、其他漂浮物。类别太多会导致标注一致性下降,模型学到的特征反而混乱。

标注工具用 LabelImg 或 X-AnyLabeling 都行,导出 YOLO 格式的 txt。这里有个血泪经验:标注框不要贴太紧。水面目标边缘往往被水花或反光模糊,框贴太紧会让模型学到噪声。我一般让框比目标实际边缘外扩 3 到 5 个像素,小目标可以外扩 2 个像素。另外,水面倒影不要标,只标实际物体。倒影标注是新手最容易翻车的地方,模型会把倒影当目标,导致水面平静时误检率飙升。

标注完成后,每个图像对应一个同名 txt 文件,每行格式是类别索引 中心x 中心y 宽 高,坐标全部归一化到 0 到 1。检查一遍有没有越界值,有的话说明标注时拖出了图像边界,需要修正。

2.2 数据增强策略:水面场景该加什么、不该加什么

YOLOv11 自带的数据增强里,HSV 色调抖动、随机翻转、马赛克增强都是默认开的。但水面场景有它的特殊性,不能无脑用默认配置。

翻转增强要小心。水平翻转对水面垃圾是安全的,因为垃圾在水面上没有固定朝向。但垂直翻转要关掉,水面倒影和天空在上方,垂直翻转会产生物理上不合理的图像,模型会学到错误的空间先验。在data.yaml同级目录的hyp.yaml里把flipud设为 0.0。

马赛克增强对水面小目标有帮助,但mosaic概率不要拉到 1.0。我一般设 0.5 到 0.7,太高会让小目标在拼接图里被裁切得太碎,训练后期 loss 震荡。另外,水面反光是高频干扰,可以加一点随机亮度调整,但对比度调整幅度要小,否则会把反光区域压成和垃圾一样的暗斑。

如果你手头数据量少于 2000 张,建议额外加 Cutout 增强,随机遮挡图像中一小块区域,强迫模型不依赖局部特征。这个在 ultralytics 框架里可以通过自定义 augment 实现,也可以离线用 albumentations 做一遍再喂进去。

2.3 数据集目录结构与 data.yaml 配置

YOLOv11 要求的数据集目录结构很固定,我直接给一个我常用的模板:

water_garbage/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

data.yaml的内容如下:

path: /home/user/water_garbage train: images/train val: images/val test: images/test names: 0: plastic_bottle 1: foam 2: plastic_bag 3: branch 4: waterweed 5: other

这里有个容易忽略的点:path用绝对路径,不要用相对路径。ultralytics 在不同版本里对相对路径的解析基准不一样,用绝对路径能避免训练时找不到数据的玄学问题。另外,names的索引必须从 0 开始连续,中间不能跳号,否则训练时类别映射会错位。

划分比例我一般按 7:2:1 分 train/val/test。如果数据量少于 1500 张,test 集可以省掉,用 val 集同时做验证和最终评估,但要在报告里注明。划分时注意同一段视频截出来的帧不要跨集分布,否则验证集精度会虚高。正确做法是按视频源或拍摄时段划分,保证验证集和训练集来自不同场景。

3. YOLOv11 环境配置与训练:从零跑通第一个水面垃圾模型

3.1 环境配置的版本锁定与常见依赖冲突

ultralytics 框架更新很快,不同版本之间 API 有差异。我建议锁定一个稳定版本,不要盲目追新。截至我写这篇时的稳定组合是 Python 3.10 + PyTorch 2.1 + ultralytics 8.3.x。CUDA 版本根据你的显卡驱动来,30 系卡用 CUDA 11.8 或 12.1 都行。

安装命令:

conda create -n yolo11 python=3.10 -y conda activate yolo11 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.3.0 pip install opencv-python albumentations

这里有个坑:如果你之前装过其他版本的 ultralytics,先pip uninstall ultralytics再装,不要直接覆盖安装。残留的旧版本文件会导致from ultralytics import YOLO时报一些莫名其妙的 AttributeError。另一个坑是 numpy 版本,ultralytics 8.3 要求 numpy<2.0,如果你环境里是 numpy 2.x,opencv 会报错,先降级。

验证安装是否成功:

from ultralytics import YOLO model = YOLO("yolo11n.pt") print(model.info())

如果能看到模型结构信息输出,说明环境没问题。yolo11n.pt是 nano 版本的预训练权重,第一次运行会自动下载。如果下载慢,可以手动从 ultralytics 的 release 页面下载后放到当前目录,代码里直接写本地路径。

3.2 用预训练权重做迁移训练的最小命令

水面垃圾检测数据量通常不大,从零训练不现实,必须用 COCO 预训练权重做迁移学习。最小训练命令:

yolo detect train \ model=yolo11s.pt \ data=/home/user/water_garbage/data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ device=0 \ project=water_garbage_runs \ name=exp1 \ pretrained=True \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ warmup_epochs=3 \ cos_lr=True \ patience=30

逐项说明:model=yolo11s.pt选 small 版本,nano 太小欠拟合,medium 以上在边缘设备跑不动,s 是精度和速度的平衡点。imgsz=640是默认输入尺寸,水面小目标多的话可以提到 800 或 960,但显存占用会明显上升,8G 显存建议不超过 800。batch=16根据显存调,显存不够就降到 8 或 4,同时把lr0按比例降一点。optimizer=AdamW比 SGD 收敛快,适合小数据集。cos_lr=True开启余弦退火,后期 loss 更稳。patience=30是早停耐心值,30 轮验证集指标不提升就停,省时间。

训练过程中重点看三个指标:metrics/mAP50、metrics/mAP50-95和train/box_loss。mAP50 到 0.85 以上基本可用,mAP50-95 到 0.55 以上算不错。如果 box_loss 一直不降,检查标注有没有问题;如果 mAP 震荡大,把batch调大或lr0调小。

3.3 小目标优化的三个关键参数

水面垃圾里小目标占比很高,一个远处的塑料瓶在 640 分辨率下可能只有 10 几个像素。YOLOv11 本身对小目标做了改进,但参数不调还是白搭。

第一个参数是imgsz。前面说了可以提到 800 或 960,但更聪明的做法是用rect=True做矩形推理,保持长边不变、短边补齐,减少无效填充。不过训练时rect默认就是开的,推理时可以在predict里显式设rect=True。

第二个参数是anchor相关的。YOLOv11 是 anchor-free 的,没有传统 anchor 调参,但boxloss 的权重会影响小目标回归。在hyp.yaml里把box从默认 7.5 提到 8.5 到 9.0,让小目标定位更准。同时把cls从 0.5 降到 0.3 到 0.4,因为水面垃圾类别边界模糊,分类 loss 权重太高会压制定位学习。

第三个参数是overlap_mask和mask_ratio,如果你用的是分割模型。检测模型不用管这两个。但检测模型里有个max_det参数,默认 300,水面垃圾密集场景可能不够,推理时设到 500。

3.4 训练日志怎么看:loss 曲线与指标解读

训练启动后,runs/detect/water_garbage_runs/exp1/目录下会有results.csv和weights/文件夹。results.csv每行是一个 epoch 的指标,重点看这几列:train/box_loss、train/cls_loss、metrics/mAP50、metrics/mAP50-95。

正常训练曲线应该是:box_loss 和 cls_loss 在前 10 个 epoch 快速下降,之后缓慢下降并趋于平稳;mAP50 在前 20 个 epoch 快速上升,之后缓慢上升。如果 box_loss 下降但 mAP 不涨,说明模型在过拟合训练集,检查验证集标注质量。如果 cls_loss 震荡剧烈,把cls权重再降一点或者增大batch。

weights/下会保存best.pt和last.pt。best.pt是验证集 mAP50 最高的权重,推理用这个。last.pt是最后一个 epoch 的权重,用来恢复训练。不要用last.pt做推理,除非你确定最后一个 epoch 是最好的。

4. 推理、部署与效果验证:让模型真正跑在水面巡检设备上

4.1 单张图像和视频流的推理命令

训练完拿到best.pt,先做单张推理验证:

yolo detect predict \ model=runs/detect/water_garbage_runs/exp1/weights/best.pt \ source=/home/user/test_images/ \ imgsz=800 \ conf=0.35 \ iou=0.5 \ save=True \ save_txt=True \ project=inference_results \ name=test1

conf=0.35是置信度阈值,水面场景建议设 0.3 到 0.4,太低误检多,太高漏检多。iou=0.5是 NMS 的 IoU 阈值,密集目标可以降到 0.45。save_txt=True会把检测框坐标存成 txt,方便后续统计。imgsz=800和训练时保持一致,不要训练用 640 推理用 1280,尺度不匹配会导致精度下降。

视频流推理把source换成视频文件路径或摄像头索引0。如果是 RTSP 流,ultralytics 也支持,但延迟会高一些,建议用stream=True做逐帧处理,避免内存堆积。

4.2 用 Python 脚本做批量推理与结果保存

实际巡检场景需要批量处理一个文件夹里的所有图像,并输出结构化结果。下面这个脚本我用了很多次,直接改路径就能跑:

from ultralytics import YOLO import os import json from pathlib import Path model = YOLO("runs/detect/water_garbage_runs/exp1/weights/best.pt") image_dir = Path("/home/user/test_images") output_dir = Path("/home/user/inference_output") output_dir.mkdir(parents=True, exist_ok=True) all_results = [] for img_path in sorted(image_dir.glob("*.jpg")): results = model.predict( source=str(img_path), imgsz=800, conf=0.35, iou=0.5, verbose=False ) for r in results: boxes = r.boxes if boxes is None: continue for i in range(len(boxes)): cls_id = int(boxes.cls[i].item()) conf = float(boxes.conf[i].item()) xyxy = boxes.xyxy[i].tolist() all_results.append({ "image": img_path.name, "class": model.names[cls_id], "confidence": round(conf, 4), "bbox": [round(v, 2) for v in xyxy] }) # 保存带标注的图像 r.save(filename=str(output_dir / f"annotated_{img_path.name}")) with open(output_dir / "detections.json", "w", encoding="utf-8") as f: json.dump(all_results, f, ensure_ascii=False, indent=2) print(f"共处理 {len(list(image_dir.glob('*.jpg')))} 张图像,检测到 {len(all_results)} 个目标")

逻辑说明:遍历图像目录,逐张推理,把每个检测框的类别、置信度、坐标存进列表,最后统一写 JSON。r.save()保存带框图像,方便人工复核。verbose=False关掉每张图的详细日志,批量处理时输出更干净。参数方面,conf和iou根据实际场景微调,如果发现漏检多就降conf,误检多就升conf。

4.3 边缘设备部署:ONNX 导出与推理速度优化

水面巡检设备通常是 Jetson 或树莓派级别的算力,直接跑 PyTorch 权重太慢。导出 ONNX 再推理是常见做法:

yolo export model=runs/detect/water_garbage_runs/exp1/weights/best.pt format=onnx imgsz=640 opset=12 simplify=True

导出后在 Jetson 上用 ONNX Runtime 或 TensorRT 推理。TensorRT 速度最快,但转换麻烦;ONNX Runtime 兼容性好,速度也够用。实测在 Jetson Xavier NX 上,yolo11s 导出 ONNX 后单帧推理约 45ms,换算下来 22 FPS,满足实时巡检需求。

如果还要更快,可以把imgsz降到 480 或 512,精度会掉 2 到 3 个点,但速度能翻倍。另一个优化点是量化,用 FP16 推理,速度提升约 30%,精度几乎不掉。INT8 量化要小心,水面反光区域容易产生量化噪声,导致误检增加,建议先用 FP16。

4.4 效果验证:mAP 之外还要看什么

mAP 是标准指标,但水面巡检场景不能只看 mAP。我一般还会看三个业务指标:漏检率、误检率和单帧耗时。漏检率用测试集里漏掉的目标数除以总目标数,误检率用误检框数除以总检测框数。单帧耗时在目标设备上实测,不要用桌面显卡的数据糊弄自己。

另外,按目标尺寸分层统计 mAP。把测试集目标按面积分成小(<32²)、中(32²到96²)、大(>96²)三档,分别算 mAP。水面垃圾检测的瓶颈几乎都在小目标上,如果小目标 mAP 低于 0.5,说明模型还没真正可用,需要回去调imgsz或加小目标增强。

5. 水面垃圾检测避坑清单:5 个我踩过的真实坑

5.1 坑一:验证集精度很高,实际部署一塌糊涂

现象:训练完 mAP50 到 0.92,信心满满拿到河边跑,结果漏检一大片。

原因:训练集和验证集来自同一段视频的相邻帧,画面几乎一样,模型只是记住了背景。这是数据划分的锅,不是模型的锅。

解决:按视频源或拍摄时段划分数据集,确保验证集和训练集来自不同场景。如果数据来源单一,至少按时间间隔划分,比如前 70% 时间段的帧做训练,后 30% 做验证。

5.2 坑二:水面反光被当成垃圾,误检率居高不下

现象:模型在阳光强烈的正午误检率飙升,把水面反光框成泡沫块。

原因:训练集里正午样本太少,模型没见过强反光场景,把反光的高亮区域学成了泡沫特征。

解决:补充不同光照条件的训练样本,尤其是正午强反光和傍晚弱光。如果补不了,在推理时加一个后处理:检测框内像素方差过低(说明是均匀反光)就过滤掉。这个后处理用 OpenCV 几行代码就能实现。

5.3 坑三:训练到一半 loss 突然变 NaN

现象:训练到第 60 多个 epoch,box_loss 突然变成 NaN,训练中断。

原因:学习率太高或者数据里有脏标注。脏标注包括坐标越界、宽高为 0、类别索引超范围。

解决:先检查标注文件,用脚本扫一遍所有 txt,把越界和零宽高的行删掉。然后把lr0从 0.001 降到 0.0005,加warmup_epochs=5。如果还 NaN,把amp关掉(amp=False),混合精度训练在某些显卡上会出这个问题。

5.4 坑四:ONNX 导出成功但推理结果和 PyTorch 不一致

现象:PyTorch 推理正常,导出 ONNX 后用 ONNX Runtime 跑,框的位置偏移明显。

原因:导出时imgsz和推理时不一致,或者opset版本太低导致某些算子行为不同。

解决:导出和推理用同一个imgsz,opset用 12 或以上。导出后先用 ONNX Runtime 跑一张和 PyTorch 相同的图,对比输出,确认一致再部署。如果不一致,检查预处理是否一致,尤其是归一化和通道顺序。

5.5 坑五:模型在 Jetson 上跑不动,帧率只有个位数

现象:桌面显卡上 60 FPS,放到 Jetson 上只有 5 FPS。

原因:PyTorch 权重没导出 ONNX,或者导出后没用 TensorRT 加速。Jetson 的 CPU 很弱,纯 PyTorch 推理跑不动。

解决:导出 ONNX 后用 TensorRT 或 ONNX Runtime GPU 推理。另外把imgsz降到 640 或 512,batch设为 1。如果还慢,换 yolo11n 模型,精度掉一点但速度能上来。

6. 把模型训到能用的最后一公里:三个进阶技巧

第一个技巧是伪标签半监督训练。水面垃圾数据标注成本高,可以先用手头少量标注数据训一个基础模型,用它推理未标注图像,把高置信度(conf>0.7)的检测框作为伪标签加入训练集,再训一轮。我试过用 500 张标注数据加 2000 张伪标签数据,mAP50 比纯 500 张标注数据提升了 6 个点。注意伪标签要过滤低置信度和重叠框,否则会引入噪声。

第二个技巧是测试时增强(TTA)。推理时把图像水平翻转、多尺度缩放各跑一遍,把结果做 NMS 融合。ultralytics 的predict里设augment=True就能开 TTA。实测 mAP50 能提升 1 到 2 个点,代价是推理时间翻 3 倍。如果设备算力够、对精度要求高,值得开。

第三个技巧是类别权重平衡。水面垃圾数据里塑料瓶可能占 60%,水草只占 5%,模型会偏向多数类。在hyp.yaml里设cls_pw或者用focal_loss替代默认的 BCE loss,让少数类获得更高权重。我一般先统计各类别实例数,把权重设为实例数反比的平方根,效果比直接反比更稳。

最后说一个我自己的习惯:每次训完模型,不管指标多好,我都会拿 20 张完全没参与训练的新图跑一遍,人工数漏检和误检。指标是给论文看的,这 20 张图才是给实际巡检用的。如果这 20 张里漏检超过 3 个,不管 mAP 多高,我都回去继续调。这个习惯帮我避免了好几次“指标漂亮、落地翻车”的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询