水下垃圾检测这个项目我最初接到需求时的第一反应是这不就是目标检测换个数据集吗无非是把我熟的那套通用检测流程搬过来数据换一换、模型调一调应该很快就能跑起来。真正把系统部署到水下机器人上、面对浑浊水体里漂浮的塑料袋和缠绕的海草时我才意识到自己有多天真。这个项目从YOLOv12/v11/v8/v5一路实测下来踩过的坑足够写一篇长文了。这篇文章就把我这个项目的完整链路拆开讲一遍从数据怎么整理、模型怎么选、训练怎么调参到最终怎么部署到实际设备上、真实水域测试中模型会怎么骗你希望对正在做水下垃圾检测或类似水下视觉任务的朋友有直接帮助。1. 水下垃圾检测的硬骨头为什么不能直接套用陆地目标检测方案1.1 退化图像让通用模型瞬间失效大多数目标检测的成熟方案都是在相对清晰的通用图像上验证的比如街景里的车、行人、家具。水下环境完全不是一回事。水体对光的吸收和散射会导致图像整体偏蓝绿色对比度断崖式下降加上海水里的悬浮颗粒画面自带一层雾感如果设备在水流中轻微摆动还会叠加运动模糊。这些退化是全局性的不是简单加一层随机噪声就能模拟的。我在做这个项目的第一版时直接拿了陆地上的通用预训练权重用少量水下数据微调了几个 epoch然后拿着测试视频在电脑上一帧一帧看结果。典型的失败场景是一个黑色塑料袋悬浮在深色背景下模型直接把它当背景忽略了而一段缠着藻类的旧渔网模型却信心十足地报告“塑料瓶”置信度还高达 0.9。这个现象的原因不难理解模型在预训练阶段学会的是“物体边缘清晰、纹理明显”的通用特征水下图像的边缘模糊、颜色单一原本的判别线索大部分失效了。所以水下垃圾检测的第一课不是选什么模型而是接受一个事实——模型需要重新学习的不是“垃圾长什么样”而是“水下退化条件下的垃圾长什么样”。1.2 标注难度和评测指标的隐性陷阱水下垃圾的类别定义本身就比陆地场景更模糊。一个啃了一半的塑料瓶、一团半透明的塑料袋、一段长满了水草的渔网人眼都很难快速判断标注员的标注一致性会直接影响模型上限。我整理数据集时发现同样一张图不同标注员对“渔网”和“绳索”的选择完全不同最后不得不做一轮统一标注规范的重返工。评测指标也有陷阱。陆地检测项目里 mAP50 上 80 就很能打了但水下垃圾场景里mAP50 高并不代表系统能用。跑测试时我见过 mAP50 还不错、实际放视频却漏检严重的模型——原因是测试集里大量目标本身就比较清晰模型捡了容易的分数难例全部漏掉。后来我要求团队里每个人必须人工抽看连续视频帧的输出统计“漏检率”而不是只看 mAP。这个习惯帮我发现了大量指标看不出的问题。漏检率的计算方式也很简单人工数一遍视频片段里的目标总数再减去模型正确检出的数量除以总数。2. YOLO系列选型横评v5/v8/v11/v12在我这个项目里的取舍2.1 四个版本的差异到底在哪做检测项目绕不开 YOLO 系列我这次把 v5、v8、v11、v12 都实际跑了对比不是看官方发布时的 showcase而是在同一批水下垃圾数据上做控制变量实验。先快速梳理一下每个版本的核心差异这样后面看结论才有参照。版本检测头设计主干结构特点对水下场景的直观影响v5Anchor-based锚框方案CSPDarknet结构保守稳定小目标召回相对吃力但训练稳定v8Anchor-free无锚框C2f 模块结构简洁泛化性好通用数据上表现均衡v11Anchor-freeC3k2 模块引入部分注意力机制轻量化程度高推理更快v12Anchor-free优化跨尺度融合注意力机制更占主导跨层融合增强小目标和模糊目标上有提升潜力v5 是锚框机制的老将。优点是训练过程很稳数据量不足时不容易崩缺点是需要针对数据集特点调整锚框尺寸水下垃圾的尺度变化很大一个锚框策略很难同时覆盖细长渔网和小尺寸瓶盖。v8 改成无锚框加解耦头之后训练逻辑简单了很多通用场景的迁移能力很好。我拿它作为第一版主力模型基线效果最扎实后期调试也顺畅。v11 给我的直观感受是“轻”。同样输入尺寸在同等硬件上推理速度比 v8 明显更快精度略有波动适合部署在算力受限的边缘设备上。v12 是我最后测试的版本它在跨尺度融合这块做了一些优化对模糊小目标的检测上限确实更高但训练收敛速度稍慢配置要求也更高属于需要喂养更多数据和算力的类型。2.2 我的最终选型结论如果让我用一句话总结选型第一版或者数据量不大时直接选 v8数据质量和数量都上来了、设备算力有富余可以考虑 v12 冲精度上限部署设备性能紧张优先用 v11 做加速v5 不建议在新项目里从零开始了。水下垃圾检测的实际需求里漏检的代价远大于误检。因为漏掉一个垃圾意味着它继续留在水体里而误检顶多是清污机器人白跑一趟。基于这个原则我不推荐为了帧率牺牲输入分辨率宁可推理慢一点也要保证小目标被看见。v12 在 640 输入下对 50 像素以下小目标的召回率比 v8 高了约 4 个点但显存占用和单帧延迟都上去了。所以如果你的设备是水下机器人这种电池和散热都受限的场景v11 的轻量化优势反而更实际。还有一个很容易被忽略的点生态和可维护性。v8 的社区资料、预训练权重、第三方部署工具都最丰富出了问题搜解决方案最快。v12 毕竟新一些很多边缘设备的部署工具还没跟上我自己就在导出时踩过算子兼容的坑。如果一个生产系统要稳定运行几个月我宁愿选生态成熟的版本而不是最潮但周边工具链不完整的版本。这一点建议所有做部署落地的朋友都认真考虑。3. 把数据整理成“能训练”的样子标注规范与增强策略3.1 数据来源和标注规范的统一很多从公开数据集或网上找视频的人都会遇到一个问题数据质量参差不齐标注格式五花八门。我在项目开始时用了一个公开的水下垃圾数据集然后又补充了自己实地拍摄的一些视频并把所有标注统一转成 YOLO 格式。这里面最耗时间的不是格式转换而是标注规范的统一。我后来定下的标注规范是类别只保留四个大类——塑料、渔网/绳索、玻璃/金属、其他杂物。为什么不用细分类因为水下图像质量差细分类会让标注员大量纠结标注一致性反而下降。四个大类足够清污机器人决定捡或不捡也足够统计哪种垃圾占比高。标注时还有一条铁律看不清的不标而不是凭感觉标。模糊样本宁可漏标也不能乱标否则模型会学出错误特征。半透明的塑料袋在画面里轮廓非常弱但只要能看出完整边界就要求标注员画出完整外接框如果只有十几像素的一小块直接舍弃否则训练时这个框会变成噪声。这里可以用一个小工具统计每个标注框的短边像素数设置一个低于阈值的标注框自动进入“人工复核”列表效率高很多。3.2 数据划分的坑按帧切分会让验证集虚高我要特别提醒一个容易犯的错误不要随机地把所有帧分到训练集和验证集。水下视频的相邻帧之间高度相似如果同一段视频的帧一部分进了训练集一部分进了验证集模型在验证集上的表现会虚高得离谱因为它“见过”几乎一样的画面。正确做法是按视频段划分。也就是说先把拍摄的视频切成若干片段再按片段整体分配到训练、验证、测试集。我在项目里按 7:2:1 划分训练、验证、测试并且保证同一个地点拍摄的视频不出现在两个集合中。这一步做完之后我的验证集 mAP50 直接从 0.92 降到了 0.84这个“下降”才是真实水平。如果你发现随便切分大幅提高了指标那一定是数据泄露了不是模型真的变强了。3.3 针对水下环境的增强策略不能只用通用增强通用的马赛克、翻转、缩放增强肯定要用但水下场景还需要针对性增强。我的经验是这几类最有效白平衡扰动、色偏模拟、模糊退化模拟、以及针对稀有类别的复制粘贴增强。白平衡扰动和色偏模拟是模拟不同水深、不同水质下的颜色变化让模型不会依赖“特定蓝色”。模糊退化模拟用的是高斯模糊加随机的运动模糊模拟设备抖动。复制粘贴增强非常关键——水下垃圾类别分布极不平衡瓶盖和塑料袋大量出现而渔网和金属类样本可能只有几十张我直接把稀有类别的目标从原图中抠出来贴到背景图上同时生成对应的标注框把渔网类样本数量翻了几倍。这个操作对类别不平衡问题的改善非常明显渔网类的 F1 分数提高了大约 12 个百分点。这里我贴一下我当时写的小脚本用于按视频段切分数据并做基本统计import os import cv2 import numpy as np def split_video_frames(video_path, seg_len50): cap cv2.VideoCapture(video_path) frames [] n 0 while True: ret, frame cap.read() if not ret: break n 1 frames.append(frame) cap.release() # 按片段分组片段内帧连续确保相同场景不会跨集合 segments [] for i in range(0, n, seg_len): segments.append(frames[i:i seg_len]) return segments def count_boxes_per_class(label_dir): from collections import Counter counter Counter() for f in os.listdir(label_dir): with open(os.path.join(label_dir, f), r, encodingutf-8) as fp: for line in fp: cls line.split()[0] counter[cls] 1 return counter统计完类别分布后再决定哪些类别需要做复制粘贴增强或额外收集数据比闷头训练科学得多。4. 训练与调参我从日志里追回的那个8个点4.1 关键超参数配置和启发式经验模型选好、数据就绪之后训练配置决定了同样一份数据能发挥多少潜力。我常用的 YOLO 训练配置如下不同项目可以在这个基础上微调超参数我的配置说明输入尺寸 imgsz640数据量够时尝试 1280水下小目标多640 是底线批次大小 batch16显存充足时 32过小会导致 BN 统计不稳轮数 epochs300配合早停水下数据噪声大需要足够轮数优化器与初始学习率AdamW lr0.001比 SGD 收敛快适合噪声数据权重衰减0.0005防止过拟合预热 warmup3 epochs前几轮用小学习率避免初期不稳Mosaic 关闭时机最后 10 个 epoch 关闭关闭后让模型适应真实分布这里重点说一下 Mosaic 增强。YOLO 系列默认会在训练中段开启 Mosaic把四张图拼成一张对小目标检测特别有效。但到了训练后期数据分布偏离真实场景太多模型可能学出一些“拼接感”特征。我的习惯是在最后 10 个 epoch 关闭 Mosaic让模型回归到真实图像分布上这一步通常能追回几个 mAP 点。4.2 我如何从训练日志判断问题我训练时每 50 个 epoch 保存一次 checkpoint然后看三样东西训练损失曲线、验证损失曲线、单类别的 PR 曲线。判断逻辑很简单训练损失继续下降但验证损失开始回升就是过拟合两边都降不下去就要怀疑学习率太大、数据标签错误、或模型容量不够。有一次验证集 mAP50 卡在 0.79 上不去训练损失也降不动我差点去改模型结构。后来排查了一圈发现是标注统一之前混入了一批旧格式标签类别编号错位导致部分训练样本的监督信号是错的。重新清洗标签之后mAP50 直接涨了 8 个点。这件事给我的教训是训练跑不动先查数据别急着换模型。另外一个值得注意的指标是各类别的 AP 差距。如果某一类的 AP 明显比整体低 20 个点说明这一类的特征没有被模型充分学到这时候做针对性的数据增强或补样本比盲目增加 epoch 更有效。比如我的项目中渔网类 AP 一直上不去后来通过复制粘贴增强把样本量补齐再单独调高这一类在损失函数中的权重渔网类 AP 才追了上来。5. 部署链路踩坑从训练服务器到水下设备之间没有坦途5.1 导出与量化ONNX 是第一步真正考验在后面训练完的 PyTorch 权重不能直接拿上设备跑。我先把模型导出为 ONNX 格式然后在推理设备上用加速推理引擎做 FP16 或 INT8 量化。这里第一个坑就是算子兼容性v12 模型里的一些注意力相关算子在导出时旧版本推理引擎不支持导致导出失败。解决方式要么升级推理引擎版本要么把模型结构做替换。我当时选了后者因为设备端的推理引擎跟系统绑定太紧升级代价太大。第二个坑是量化后的精度损失。水下图像本身对比度低INT8 量化后特征响应容易被截断导致精度下降幅度比通用数据集上更明显。我的做法是先跑 FP16确认精度可以接受再尝试 INT8如果 mAP50 下降超过 3 个点就退回 FP16。这个权衡对水下检测尤其重要因为垃圾目标普遍是小目标量化噪声对它们的影响远大于大目标。5.2 预处理和后处理必须与训练一致这是最容易在部署时被忽略的问题。训练时我做了 letterbox、归一化、颜色通道转换部署推理如果预处理不一致模型效果会直接垮掉。很多人在训练服务器上测得好好的一上边缘设备就发现大量误检排除模型本身的问题后大概率是预处理环节颜色空间或缩放方式不一致。我建议做一件简单但有效的事:把预处理和后处理写在一个单独的模块里训练的验证脚本和部署程序共用同一个处理函数保证逻辑完全一致。后处理里 NMS 的 IoU 阈值和置信度阈值最好做成可配置参数不要写死在代码里。实际部署时不同水域的浊度不一样置信度阈值可能需要从 0.25 调到 0.4 再调到 0.3写死了就只能重新编译代价很大。5.3 性能画像不是说帧率不够就换小模型设备上实际跑的时候我遇到过推理时间看着不慢但整体帧率上不去的情况。后来做了逐段耗时画像发现瓶颈在预处理把 BGR 图像 resize 到 640 再做归一化居然占了单帧总耗时的一半。优化方式包括换成高效的 resize 实现、提前分配固定内存、把归一化合并到模型输入层之前。这个优化的性价比远高于换一个小模型。延迟分布同样重要。在清污机器人上检测从发出到拿到结果可能只有几百毫秒但如果传输或者排队导致延迟抖动机器人就可能错过目标。我当时加了双缓冲队列让采集线程和推理线程并行同时保证推理结果按帧序号对齐避免帧错位导致垃圾位置计算错误。部署文档里还会写明每部分耗时和参数含义这样其他同事接手时不会一头雾水。如果你是拿一套现成的“源码部署文档”起步我的建议是先跑通一个最短主链——装环境、加载权重、跑通单张图片推理、跑通视频推理然后再大规模修改。千万不要一开始就想着改模型改结构等主链完全跑通再动手排查问题会轻松得多。6. 真实水域实测mAP会骗人漏检清单才是底线6.1 典型失败案例复盘把系统搬到真实水域之后我第一次意识到实验室指标和现场表现之间有多大的鸿沟。第一次野外测试模型对水面漂浮的矿泉水瓶检测很准但一旦垃圾进水深度超过一米画面颜色迅速变暗模型准确率开始明显下滑。最典型的是半透明塑料袋它在水中的视觉特征几乎和周围水体融为一体置信度始终不超过 0.3几乎被全部漏检。我们需要换一个思路来看待这些现象半透明塑料袋的漏检本质是特征不明显而不是模型“不会”。我通过提高输入分辨率、增加难负样本挖掘、以及针对这类目标做更多无渣背景下的模拟才把召回率慢慢拉上来。你没法指望只靠调阈值解决这类问题它必须在数据和增强层面解决。另一个经典失败是黑色轮胎状物体倒扣在阴影里。阴影把物体的下半部分和背景融为一体模型只能检测到上半部分边界输出的目标框只覆盖了物体的二分之一。这类问题的根源在于水下光照不均导致的局部特征缺失。多尺度训练有一点帮助但更有效的方法是加入更丰富的光照扰动增强模拟不同角度水下光源的照射效果。水体中植物也是误检重灾区。水下植物的形态有时和海草样的垃圾高度相似模型会把一大片植物误报成渔网或者绳索。面对这种情况我收集了一批只含植物的纯背景视频作为负样本加入训练降低误检率。这一步极其有效模型误检率降低了一半以上。6.2 我从这个项目里沉淀下来的几条经验这个项目做下来我形成了几个固定习惯。第一任何新版本模型都先在同一个固定测试集上跑一遍完整评测然后由人工抽看视频输出用漏检率而不是 mAP 判断模型是否真的能用。第二数据整理阶段严格要求类别分布统计和标注质量复核每条数据都可能影响最终的模型上限这一环节省不得。第三阈值不要固定而要让操作人员可调。不同水域的差异太大固定阈值会让系统要么漏检过多要么误检过多且无人愿意频繁改代码。把置信度阈值、NMS 阈值暴露成参数配合简单的可视化工具现场人员就能自己调校项目中后期省了大量沟通成本。最后说一个如果重做一遍我会提前做的事给整个训练数据、标注版本、模型权重、参数配置做一套版本管理。项目中期我发现某次实验的复现结果失效最后追查发现是训练数据文件夹被覆盖了旧版本权重对应的数据已经找不回来。从那以后每次实验我都会同步记录数据版本、模型配置和权重哈希值。这个习惯看起来不起眼但在项目需要回溯对比时能挽救无数个周末。