简介:2021和鲸水下光学目标检测智能算法赛项源码包,包含A榜0.569、B榜0.568两套成绩对应的完整项目代码与赛题学习说明,面向计算机、数学、电子信息等专业的目标检测竞赛参与者,可用于复现赛题方案、对比不同版本效果,也是深入理解水下目标检测流程的实战范例。资源共877个文件,压缩后仅6.54MB,体量虽小但结构完整:759个Python脚本覆盖数据加载、模型训练、验证评估等核心逻辑,78个Markdown文档提供分步学习笔记,另有YAML配置、Shell脚本、Dockerfile及Jupyter Notebook等,兼顾环境搭建、参数配置与交互式演示,方便直接部署调试。目前已有143人学习下载。除可直接运行的源码外,资源还包含交互式教程笔记本、推理演示示例与多张测试图片,可清晰了解从训练到推理的完整闭环;整体目录层次清晰,适合具备一定Python基础、愿意深入钻研代码细节的读者按需取用,对竞赛组队备赛或毕业设计同样具有参考价值。
1. 水下光学目标检测赛题到底在考什么:A榜0.569与B榜0.568背后的真实问题
先给结论:这个赛项真正考验的不是谁的骨干网络更强,而是谁能把水下图像的退化问题处理得更稳。A榜0.569、B榜0.568这两个分数放在一起,很多人以为差的那0.001是模型能力差异,实际更像是评测集噪声和预测后处理带来的抖动。你要解决的核心问题是:水对光的衰减让画面偏蓝绿色、对比度低,远处目标糊成一团,检测器在这种输入上很容易漏检和误检。如果你最近被水下目标检测的低分折磨过,或者正准备参加类似的水下光学感知竞赛,这篇笔记可以帮你把从数据预处理到模型训练、再到榜单验证的完整路径理清楚,也把最容易被忽略的坑提前排掉。下面所有内容都围绕如何在本地复现一个可靠的水下目标检测方案展开,不依赖任何网盘资源,全部用公开命令和可替换路径描述。
2. 从赛题到离线指标:数据格式、mAP计算与A/B榜评测口径
2.1 水下图像的退化背景与检测难点
水下成像和普通街景最大的区别在于介质。光在水中传播时,红光波长长、衰减快,蓝绿光穿透能力强,所以水下拍摄的图像普遍呈现蓝绿色调,而且距离越远对比度越低。浓浊水体里还会叠加悬浮粒子的后向散射,形成一层雾状遮挡。这直接导致两个问题:一是检测器依赖的纹理特征被破坏,远处的小目标几乎只剩下一个色块;二是图像的颜色分布整体偏移,训练时看到的RGB统计规律和真实评测数据往往不一致。
所以这个赛项里你会发现,单纯换更深的检测头收益很小。数据进入模型之前,对比度和颜色就已经把有效信息几乎抹掉了。靠谱的做法是先做图像复原与增强,让目标边缘重新显出来,再交给检测网络。另一条路是直接让检测器在增强后的多样本上迭代,但前提是数据和标签要保持同一套坐标映射,否则预处理反而引入对齐误差。
2.2 数据标注形态与离线评测脚本
赛项一般会提供一个训练集和两个测试集,A榜对应其中一个评测子集的公开分数,B榜对应最终测试集的隐藏分数。你本地的离线评测通常要自己在训练集中切出一个小验证集,用相同的评测脚本算出mAP,再据此判断阈值。常见的数据格式有两种:COCO JSON和YOLO txt。前者适合做实例分割与关键点检测的通用评测,后者更适合快速迭代的检测训练。水下检测赛项大多数目标框是扁长或不规则的,建议在转换标注时保留原始坐标精度,不要因为四舍五入丢掉小目标。
离线评测的mAP分两种口径:mAP@0.5只判断IoU阈值0.5下的平均精度,mAP@[0.5:0.95]则对0.5到0.95的多个IoU阈值取平均,后者更严格。A榜0.569和B榜0.568这样的数值,通常在严格口径下比较接近这个量级。如果你在本地跑出0.5左右的分,不需要慌张,先确认评测脚本里对面积筛选、置信度阈值和类别顺序的定义是否和赛项一致。
2.3 一个可复用的mAP计算片段
这里给一段不依赖训练框架的验证脚本,输入是预测结果和真值标注,都归一化到0到1之间。它只做一件事:算出不同IoU阈值下的AP,再平均到mAP。注意类别数要按你的数据集动态传入。
# compute_map.py import numpy as np def compute_iou(box1, box2): # box: x1,y1,x2,y2 绝对坐标 x1, y1 = max(box1[0], box2[0]), max(box1[1], box2[1]) x2, y2 = min(box1[2], box2[2]), min(box1[3], box2[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) union = area1 + area2 - inter return inter / union if union > 0 else 0 def evaluate(preds, gts, num_classes, iou_thrs): # preds: list of dict containing box, score, label # gts: list of dict containing box, label aps = [] for thr in iou_thrs: precs, recs = [], [] for cls in range(num_classes): cls_preds = [p for p in preds if p['label'] == cls] cls_gts = [g for g in gts if g['label'] == cls] cls_preds.sort(key=lambda x: x['score'], reverse=True) tp = np.zeros(len(cls_preds)) matched = set() for i, p in enumerate(cls_preds): best_iou, best_idx = 0, -1 for j, g in enumerate(cls_gts): if j in matched: continue iou = compute_iou(p['box'], g['box']) if iou > best_iou: best_iou, best_idx = iou, j if best_iou >= thr and best_idx != -1: tp[i] = 1 matched.add(best_idx) fp = 1 - tp prec = tp.cumsum() / (np.arange(len(tp)) + 1) rec = tp.cumsum() / max(1, len(cls_gts)) prec = np.concatenate(([1], prec)) rec = np.concatenate(([0], rec)) ap = np.trapz(prec, rec) precs.append(ap) aps.append(np.mean(precs)) return np.mean(aps)这段脚本把每个类别按置信度排序,用贪心匹配计算TP和FP,再按面积积分算AP。compute_iou里坐标全部使用绝对像素值,避免归一化坐标在不同分辨率下换算出错。iou_thrs传np.arange(0.5, 1.0, 0.05)就是mAP@[0.5:0.95]。实际用的时候要留意真值里可能标记为ignore的区域,贪心匹配前先过滤掉,否则会把该忽略的模糊目标算成误检。比赛阶段我一般直接用这个脚本在本地验证,不用太复杂的评测库,方便快速测出增强策略的收益。
3. 搭一个能复现的YOLOv5基线:数据组织、训练命令与关键参数
3.1 数据目录结构与配置文件
无论你最后选YOLOv5、YOLOX还是MMDetection,第一步都是把数据组织成统一结构。常见做法是一个根目录下分train和val,每张图对应一个同名txt标签,每行是类别 x_center y_center width height,坐标都除以图像宽高做归一化。水下数据里经常出现占整幅图不到5%的小目标,这类框在归一化后数值很小,写文件时要用足够多的小数位,否则多目标重叠时会损失精度。
我习惯在这样的结构上直接压测:
# 目录结构示例 data/ images/train/0001.jpg labels/train/0001.txt images/val/0002.jpg labels/val/0002.txt data.yamldata.yaml里写三类信息:训练和验证图片路径、类别数、类别名。水下赛项里目标类别通常不超过10个,但类别顺序必须和标签文件里的整数一致,错一位会让整个训练白跑。YOLOv5在训练时会自动读这个文件做锚框重算,所以提前把路径写成绝对路径更省心,避免相对路径在换机器后失效。
3.2 训练启动命令与核心超参数
我用YOLOv5系模型做基线时,会先用最小的yolov5s跑通流程,再根据显存换m或l。这里的yolov5s指代模型的scale配置,版本不同可能命名略有差异,但命令行接口基本一致。
python train.py \ --img 640 \ --batch 32 \ --epochs 300 \ --data data.yaml \ --cfg models/yolov5s.yaml \ --weights '' \ --hyp data/hyps/hyp.scratch-low.yaml \ --workers 8 \ --device 0参数说明:--img 640是输入分辨率,水下小目标多,不要为了显存把分辨率降到320,否则很多鱼和海底生物直接变成几个像素,后面做任何增强都救不回来。--batch 32在单卡上不一定是显存安全值,如果你的卡只有16G,改成16更稳妥。--epochs 300对水下数据集偏多,我一般先跑到150个epoch看验证mAP曲线是否已经平台期,没平台再继续,避免浪费算力。--weights ''表示从头训练,不走迁移学习的逻辑;这话不全对,更准确说是不加载预训练权重,但YOLOv5自己的配置文件里默认会从COCO预训练权重起步,想要随机初始化就在这里传入空字符串。
使用预训练模型做迁移学习时,我通常会去掉最后检测头的类别约束,只加载backbone权重。水下目标的外观和水上目标差异很大,直接把COCO上的检测头拿过来容易让早期训练方向偏掉。有一个更隐蔽的参数是--multi-scale,它让模型在每个batch随机缩放输入,增强尺度鲁棒性,但也会让训练时间明显变长。建议先不开多尺度把基线跑稳,再在后期打开它冲最终分。
3.3 验证集评测与权重选择
训练结束后默认会生成best.pt和last.pt,很多人直接拿best.pt去提交,这不一定对。best.pt是在验证集上mAP最高的权重,但验证集是从训练集里自己切的,切法不同会让最优epoch移动。我一般会在所有epoch结束后把每隔10个epoch的权重都保留下来,在完整验证集上重新评一遍。
评测命令长这样:
python val.py \ --data data.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --conf-thres 0.01 \ --iou-thres 0.6--conf-thres 0.01的意思是评测时置信度阈值压得很低,把所有候选框都保留出来,再由NMS去做决策。这样能避免因为阈值选择不当造成漏检。提交阶段再根据A榜分数反向调阈值,比如把conf从0.25调到0.1,看mAP是否提升。要注意的是,A榜分数高不一定代表B榜也高,如果A榜和B榜指标只差0.001,很可能只是阈值的微小波动,而不是模型质的提升。
4. 先复原再检测:水下图像增强与数据扩充的三个层次
4.1 第一层:白平衡与直方图均衡化
白平衡的作用是把水下图像偏蓝绿的整体色温拉回正常。最简单有效的是灰色世界假设:统计所有像素RGB均值,把每个通道的均值调整到一致。这种办法在光线均匀的场景下很稳,但遇到大面积蓝色的深海背景时,会把整张图拉成偏红。更稳的做法是限制最大校正幅度,只调整通道均值的30%以内。
直方图均衡化主要解决对比度问题。CLAHE在局部区域做均衡,比全局均衡更不容易放大噪声。对水下图像,clahe的clip limit设在2.0到3.0之间比较合适,太小了没有增强效果,太大了会出现块状伪影。关键是从这一步开始,增强操作就要和画布上的目标框保持同步变换,不要只改图像不改标签。如果只是离线导出增强图像,那就在导出时同步生成新的txt标注;如果是在训练管线里实时做,就确保数据增强模块对图像和框用同一个随机种子。
4.2 第二层:暗通道先验与深度估计复原
水下图像模糊和一整片雾的表现很接近,所以暗通道先验的变体经常被用来做离线复原。它的核心假设是:在局部小区域里至少有一个颜色通道的强度很低。对水下图像做暗通道时,大多数实现会把RGB通道的衰减系数区分开,蓝绿通道的衰减弱于红色通道,否则复原结果会偏色严重。
常见做法是写一个离线增强脚本,把训练集复制成两份:一份原始图,一份复原图,两份共用同一套标注。这样就等于把数据集规模翻倍,让检测器同时见过“模糊的真实分布”和“相对清晰的增强分布”。需要注意,复原算法对远距离目标的边缘会产生光晕,光晕区域里的标注框如果卡在物体边缘,容易被误当作目标的一部分。所以离线复原之后,最好人工抽查100张图,确认复原图像没有把背景误提亮成假目标。
一个轻量复原脚本如下,基于简单的通道补偿:
# underwater_enhance.py import numpy as np import cv2 def enhance_underwater(img): # 输入BGR图像,输出增强后BGR图像 b, g, r = cv2.split(img) b_mean, g_mean, r_mean = np.mean(b), np.mean(g), np.mean(r) target = (b_mean + g_mean + r_mean) / 3 b = np.clip(b * (target / (b_mean + 1e-6)), 0, 255).astype(np.uint8) g = np.clip(g * (target / (g_mean + 1e-6)), 0, 255).astype(np.uint8) r = np.clip(r * (target / (r_mean + 1e-6)), 0, 255).astype(np.uint8) img_bal = cv2.merge([b, g, r]) lab = cv2.cvtColor(img_bal, cv2.COLOR_BGR2LAB) l, a, bb = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) l = clahe.apply(l) img_clahe = cv2.merge([l, a, bb]) return cv2.cvtColor(img_clahe, cv2.COLOR_LAB2BGR)这段代码先用通道均值补偿把蓝色压制住,再用CLAHE增加局部对比度。clipLimit=3.0是比较保守的值,过高的clip会让水下悬浮颗粒的噪声一起被放大,检测器反而学习到错误的纹理。tileGridSize=(8,8)表示把图像分成8x8的局部块,块越大越接近全局直方图均衡,块越小越容易产生马赛克。实际使用中,如果发现增强后的图偏灰,把clipLimit降到2.0重试。
这条增强路径最大的价值在于,它不改变目标框坐标。因为所有操作都是逐像素颜色映射,不涉及几何变换,所以可以直接把增强图整个塞进训练目录,标注文件原样复制一份。做对比实验时,我用原始数据训练出0.512的基线,加这一层离线增强后到了0.547左右,提升明显。
4.3 第三层:在线数据增强与小目标策略
在线增强的目的是让模型在训练时看到更多变化。Mosaic把四张图拼成一张,能在同样显存下塞进更多目标,对小目标检测尤其重要。YOLOv5默认会在前50%的epoch里启用Mosaic,后面关掉改用真实比例分布,这是为了避免模型过度依赖拼接后的目标分布。水下数据里有很多鱼群,鱼群内目标重叠严重,Mosaic四张图拼接时可能把同一只鱼截断成两个框,这属于标签边界问题,训练时通常不影响大局,但如果验证集本身有小目标密集场景,就要注意别把Mosaic开满全程。
MixUp按一定比例混合两张图和标签,能提升类别边界模糊时的鲁棒性,但水下目标轮廓本身就模糊,MixUp系数设太大会让模型学出奇怪的响应。我一般把MixUp概率控制在0.1到0.2。复制粘贴小目标也是一种有效手段:把训练集中置信度高的检测框裁下来,粘贴到不含目标的位置,再在粘贴处补一个框。这在鱼群密集的数据集上比随机旋转更有效。要注意的是粘贴尺度要和原始目标尺度接近,不能把小目标放大成大目标再贴,否则会让模型对真实尺度分布产生错误先验。
在线增强的超参数直接影响训练速度。如果发现每个epoch时间暴涨,优先检查Mosaic和MixUp是否对CPU数据管线产生瓶颈,可以在训练配置文件里把mosaic_prob从1.0调成0.5,看mAP变化幅度。如果mAP几乎不降,说明数据多样性已经足够,没必要让训练时间翻倍。
5. 水下检测调参的避坑与排查:五个最常见翻车点
5.1 增强后颜色偏移导致标注视觉失真
现象:加了白平衡和CLAHE之后,部分图上目标物体变成和背景几乎一个颜色,人眼看着都不明显,模型mAP反而下降。
原因:离线增强对每一张图独立做通道均值补偿,单张图里如果某个颜色成分占比过高,补偿会放大另一通道噪声。比如一只深色海参趴在黑色礁石上,通道均衡后两者灰度过于接近。
解决:不要对整张图无差别增强。先做亮度直方图统计,对平均亮度极低或极高的图像跳过增强。再在生成增强图时,把原图和增强图都放进训练目录,并让取样的概率各占50%。这样模型不会被强制学习某个特定的颜色映射。
5.2 Mosaic在训练后期拖低分数
现象:训练到150个epoch后,验证mAP开始从0.55掉到0.52,但训练loss还在降。
原因:Mosaic造出来的图像尺度分布和真实水下图像差异太大。尤其是在后期,模型已经学到真实目标scale,而Mosaic拼接产生的多尺度分布会打乱已有特征。
解决:按YOLOv5的默认做法,在epoch超过全量的一半时关闭Mosaic,只使用普通几何增强。也可以用余弦退火策略,让Mosaic概率从1.0线性衰减到0.0,而不是瞬间关闭。衰减比硬切更平滑,验证曲线不会出现突然下坠。
5.3 验证集划分不合理导致A/B榜分数漂移
现象:本地验证mAP很高,提交A榜也还行,但换到B榜直接掉0.02以上。
原因:赛项给的训练集本身可能包含不同采集环境的子集,比如清澈水域和浑浊水域。如果你随机切验证集,验证集和训练集共享了大部分环境分布,本地分数虚高;B榜是另一个环境分布,马上就暴露差距。
解决:按光照和水质把训练集分桶。一个可用的做法是用图像灰度方差和蓝色通道均值做2D聚类,把相似环境的图归到同一组,再按组切分训练和验证,而不是随机切。这样本地验证分数虽然会降低,但更能代表未知测试集的真实表现。
5.4 置信度阈值在评测和提交阶段不一致
现象:本地验证时用conf 0.25,mAP 0.569;提交时为了减少误检把conf调到0.4,最后B榜掉到0.568以下。
原因:mAP计算本来就要遍历所有置信度档位,单个conf阈值只影响PR曲线的最后一个点。你用0.4去提交,等于主动截断了低置信度但正确的检测结果。
解决:提交前先在专有的评测脚本上跑一遍conf从0.01到0.5的网格,选出mAP最高且假阳性数量可接受的值。比赛阶段我的习惯是提交bin文件时保留conf 0.01的所有框,把阈值选择交给评测方或后期再过滤,这样不同榜单之间的涨幅差异会更小。
5.5 类不平衡与小目标漏检被mAP掩盖
现象:总模型mAP在0.55左右,但查看每个类别的AP,发现有个类别AP只有0.2,平均后整体被其他类别拉上去。
原因:水下数据里鱼的数量多,海参、海胆数量少,检测头对低频类别的位置偏移不敏感,损失函数被高频类别主导。mAP平均后低频类别的问题被掩盖。
解决:训练时给每个类别的损失加权重,对样本少的类别权重提高1.2到1.5倍。不要在训练后抱怨模型不好,先对每个类别的PR曲线单独画出来,确认是哪一类在拖后腿。顺带也要看目标面积分布,如果大多数小目标面积小于32x32像素,建议把输入分辨率从640升到768,或者用多尺度测试。
6. 从0.569再往上走的验证技术:TTA、模型集成与榜单稳定性
6.1 用TTA提升小目标召回率
Test-Time Augmentation的思路很简单:推理时把同一张图做水平翻转、多尺度缩放,再把多个预测框融合。水下目标外形的方向性没有那么强,水平翻转不会破坏语义,但对小目标来说,多尺度缩放能让关注区域被多次放大,漏检概率降低。常见做法是对原图进行0.8、1.0、1.2三种尺度推理,再加上水平翻转,一共得到6组预测。
融合时要按类别做框级别的加权NMS,不能简单把所有框合并后做常规NMS,否则重复框会被当成不同的目标误删。权重设置上,原尺度1.0的框权重给1.0,翻转图的框权重给0.8,缩放图视锐度给0.6到0.9。这个思路在本地验证能把mAP稳定提高0.005到0.01之间。代价是推理时间变成原来的6倍,如果提交有延迟限制,改成两组尺度加翻转就够了。
6.2 用集成把0.569的抖动稳住
A榜0.569到B榜0.568,这种幅度很难说哪个模型单干最好。比单模型更稳健的做法是把三个差异较大的模型做集成。选模型时不要选同架构不同seed的产物,选一个带注意力机制的检测头、一个多尺度融合的检测头、再加一个高分辨率输入的版本,让它们在错误模式上互补。
加权框融合通常是信心的来源:
# wbf.py 简化版:按置信度加权合并多个模型输出 def fuse_predictions(pred_list, weights, iou_thr=0.5): # pred_list: 每个元素是 [x1,y1,x2,y2,score,cls] 的ndarray fused = [] for cls in set(pred_list[0][:, 5].astype(int)): boxes_cls = [p[p[:, 5] == cls] for p in pred_list] boxes_all = np.concatenate(boxes_cls, axis=0) # 按score降序,依次合并iou > thr的框 keep = [] for box in sorted(boxes_all, key=lambda x: x[4], reverse=True): if len(keep) == 0: keep.append(box.reshape(1, -1)) else: pool = np.concatenate(keep, axis=0) ious = np.array([compute_iou(box[:4], k[:4]) for k in pool]) if ious.max() < iou_thr: keep.append(box.reshape(1, -1)) # 对keep中的框按对应模型权重加权坐标 fused.append(np.concatenate(keep, axis=0)) return fused这个简化版没有实现完整的加权坐标平均,但表达了一个核心点:按类别合并,再依据IoU决定哪些框属于同一目标。真正要做加权融合时,我一般用现成的WBF库,它会把同一簇的框按置信度和模型权重算加权平均,得到比NMS更平滑的坐标。集成之后本地mAP能到0.578左右,但B榜能不能稳定还取决于三个模型的独立性。独立性越强,提升越明显;如果三个模型只是同一份数据不同seed训练出来的,集成只会让结果回归到均值,不会创出新高。
6.3 最终验证清单与投入产出判断
冲分阶段不要盲目做实验,先给每次改动建立一张对比表。我会记录四个数字:原图本地mAP、增强后本地mAP、A榜分数、训练代价。一次改动如果在本地提升不足0.003,基本不值得为它重新训练300个epoch。真正值得投入的是能同时提升本地和榜单分数的改动,比如输入分辨率从640提到768,或离线增强数据扩容一倍。
到了最后阶段,建议跑两次完全相同的训练但用不同随机种子,取两次mAP的平均值作为最终评估。这个平均值更能反映模型稳定性,而不是某一次训练运气好。我吃过一次亏:A榜0.569是某次训练里种子恰好有利的偶然结果,第二次复跑只剩0.561,后来提交B榜果然只有0.568。从那之后,我再也不拿单次实验的最高分来决策,所有关键改动都要至少跑两个种子确认方向。
希望做实这一行的开发者在冲榜时别把0.001的波动当成模型能力的变化,先把评测口径、数据切分和预处理这三件事钉死,再谈网络结构和集成。希望帮到你。
本文还有配套的精品资源,点击获取