简介:基于多实例学习与YOLOv10的水下目标检测(RUOD)项目源码包,面向计算机视觉开发者、研究学习者及水下目标检测方向参赛者,提供从理论到落地的完整工程参考。整个压缩包仅1.2MB,共508个文件,主干为Python脚本与Markdown说明文档,配以YAML配置、CSV预测结果,另有C++推理示例、Shell辅助脚本及多平台Dockerfile,目录按功能划分清晰,便于快速部署和复现。已有348人学习下载。文档部分系统梳理了目标检测的核心知识,包括Two stage与One stage方法对比、NMS非极大值抑制、IoU重叠度计算以及mAP评估指标,从任务定义到评价指标均有说明,适合新手补齐理论基础;代码部分则围绕YOLOv10展开,给出多实例学习框架下的训练、验证与推理实现,并附有不同大小模型(如n/l/x等)的预测结果文件,方便对比精度与速度。整体结构清晰,是水下目标检测项目实战的一份简明参考。
1. 水下目标检测为什么难:RUOD与YOLOv10的组合拳
水下目标检测和普通的地面目标检测完全是两种难度。RUOD数据集里那些水下机器人拍回来的图像,光是颜色偏蓝绿就够模型喝一壶的,再加上悬浮颗粒、光照衰减、鱼的形态又软又容易变形,很多在COCO上跑得好好的检测器一换到水下场景mAP直接腰斩。这份资源把多实例学习(MIL)和YOLOv10结合到一起,目的就是把RUOD上的检测精度往上拉——MIL处理弱标注和上下文模糊,YOLOv10提供实时的检测骨干与端到端推理。适合手头有水下图像数据、用通用YOLO权重效果不理想、想换个思路做检测的工程师,也适合研究生快速跑通一个基线再往上改进。下面我按数据、模型、训练、排错、部署这条线,把这份资源拆开讲透。
2. RUOD数据集与任务定义:先看懂标注,再谈训练
2.1 水下图像的成像特征与RUOD类别体系
先说清楚RUOD是什么。RUOD全称是Residual Underwater Object Detection,是目前水下目标检测里比较常用的benchmark,图像来自水下机器人(ROV/AUV)在真实作业环境中拍摄的画面,不是实验室水箱里那种理想光照。它和地面数据集最本质的差别在于成像模型:水下图像存在严重的颜色偏移和对比度下降,蓝色和绿色通道占主导,红色通道衰减最厉害,深度越大偏色越明显。很多通用检测器在COCO上学习到的颜色分布先验在水下完全不成立。
RUOD的类别体系也和底栖生物调查、水下养殖巡检直接相关,常见的包括海参、海胆、扇贝、海星、鱼和潜水员等。类别不算多,但每一个都不好做:海参颜色和沙地背景几乎融为一体,海胆是带刺的圆形,扇贝在泥土里只有边缘一圈轮廓,鱼因为游动姿态导致长宽比变化极大。最让人头疼的是同一类别内部差异巨大——同一条鱼,近景时占图像三分之一,远景时就十几个像素,加上水下散射造成的边缘模糊,漏检和错检都很正常。
2.2 标注格式解析:从文本到可训练样本
RUOD的原始标注并不统一,有的版本是VOC格式的XML,有的是YOLO格式的txt,还有的是COCO的JSON。这份资源的代码里能看到inference.cpp和main.cpp这样的文件,说明作者至少跑通了从数据读取到后处理的完整链路。我在复现时第一步不是急着训练,而是先把标注格式统一到YOLO能直接读的形式,否则后面所有环节都会串味。
YOLO格式的核心是每张图对应一个txt文件,每行一个目标,格式为class x_center y_center width height,四个坐标值都归一化到0~1之间。从VOC的XML转成YOLO txt,最常见的方式是用Python脚本解析XML里的<bndbox>节点:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_txt_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_map: continue cls_id = class_map[cls_name] box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) # 防止坐标越界 x1 = max(0, min(x1, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) x2 = max(0, min(x2, img_w - 1)) y2 = max(0, min(y2, img_h - 1)) cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 过滤掉宽或高为0的无效框 if w <= 0 or h <= 0: continue lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines))这段脚本的逻辑分三步:先从XML的size节点读出图片宽高,再遍历每个object节点取类别名和边界框坐标,最后把绝对坐标归一化到0~1区间。有两个地方容易翻车:一是坐标越界,水下图像偶尔会有标注框超出图片边界的情况,如果不做min/max裁剪,训练时loss会莫名变成NaN;二是class_map的映射必须和之后yaml里的类别顺序完全一致,顺序错了模型不会报错,但mAP会一塌糊涂。
2.3 训练集/验证集划分与类别不平衡处理
数据划分直接决定你评估出来的mAP可不可信。RUOD这类数据集的原始划分可能和你的任务不完全匹配,我的习惯是重新划分并保证验证集里每个类别都有一定数量的实例,不能靠随机抽样碰运气。用sklearn的train_test_split按类别做分层抽样:
import numpy as np from sklearn.model_selection import train_test_split # image_paths: 所有图片路径列表 # label_counts: 每张图片包含的类别集合,用于分层 labels_per_image = {} for img_path in image_paths: txt_path = img_path.replace('.jpg', '.txt') classes_in_img = set() with open(txt_path) as f: for line in f: cls_id = int(line.split()[0]) classes_in_img.add(cls_id) labels_per_image[img_path] = classes_in_img # stratify参数不能直接传集合,先转为多标签向量 from sklearn.preprocessing import MultiLabelBinarizer mlb = MultiLabelBinarizer() y = mlb.fit_transform(labels_per_image.values()) train_paths, val_paths = train_test_split( list(labels_per_image.keys()), test_size=0.2, stratify=y, random_state=42 )这里有个关键细节:stratify要求传入的是二维矩阵,所以得先把每张图的类别集合做MultiLabel编码。如果数据集太小、某些类别只有个位数样本,分层抽样依然可能失败,这时候需要放宽条件,比如只对样本数最多的几个类别做分层,或者干脆采用K折交叉验证来评估。
类别不平衡是水下检测的另一大坑。RUOD里鱼和潜水员的数量可能远多于海胆,如果直接训练,模型会对高频类别过拟合、对低频类别欠拟合。常见做法是调整YOLO损失里的cls_pw(正样本权重),或者在数据加载阶段对低频类别的图片做过采样。我一般先统计每个类别的实例数,画个柱状图,如果最大类别和最小类别数量差超过5倍,就优先考虑过采样而不是调权重,因为权重调过头容易让模型在低频类别上产生大量误检。
2.4 水下图像增强:不只是翻转和裁剪
通用检测里的随机翻转、缩放、马赛克增强在水下场景要谨慎用。RUOD图像本来就有颜色偏移,如果增强策略里再叠加随机色度抖动,模型可能学到的是“偏色越严重越像目标”这种伪相关。我的建议是优先做几何增强(翻转、旋转、随机缩放),颜色增强只做轻度亮度扰动,或者干脆先做色彩还原预处理——比如用灰度世界算法把偏蓝的图像校正回接近自然光的色温,再做常规增强。很多复现RUOD的工作都加了这一道预处理,mAP能涨2~3个点。
3. YOLOv10架构与配置文件:yaml文件怎么创建、超参怎么设
3.1 YOLOv10相比v8/v5改了什么
YOLOv10在2024年发布,和v5/v8最大的区别是引入了无NMS的端到端训练机制。传统YOLO在训练时用一对多标签分配(一个GT对应多个预测),推理时靠NMS把冗余框去掉;YOLOv10在训练中同时维护一对多和一对一两套头,推理时直接走一对一头的输出,不再需要NMS。这对工程部署是个利好,省去了后处理里最容易出玄学问题的NMS阈值调参,也减少了推理延时。
但要注意一个问题:YOLOv10虽然官方宣称无NMS,实际使用中如果类别多、场景复杂,直接从一对一头的输出里取框,置信度分数分布和v8的NMS之后的结果不完全一样。不少复现者为了提高mAP还是会在后处理里加一个轻量NMS兜底,尤其是水下目标这种密集小目标场景。项目资源里的inference.cpp就明显是走C++部署链路做的后处理,这也符合工业落地的习惯。
YOLOv10的另一个改动是在Backbone里用了类似RepVGG的结构,训练时是多分支、推理时重参数化融合成单路卷积,好处是推理速度更快、显存占用更低。和v5相比,v10的C2f模块替换了原来的C3模块,梯度流更丰富,小目标特征保留得更好——这对水下检测里大量的小尺寸目标是非常关键的。
3.2 配置文件从哪里来:yaml的完整结构
“yolov10 yaml文件怎么创建”是很多人刚接触YOLOv10时的第一个问题。YOLOv10的模型配置文件不是用来定义网络层结构的(那是模型代码里写死的),而是用来告诉训练脚本三件事:数据集路径、类别名列表、模型尺寸缩放系数。以yolov10n.yaml为例,最简形式如下:
# 数据集配置 path: ./datasets/RUOD # 数据集根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 test: images/test # 测试图片相对路径 # 类别配置 nc: 6 # 类别数量,必须和class_map一致 names: ['sea_cucumber', 'sea_urchin', 'scallop', 'starfish', 'fish', 'diver']这个文件里的nc和names是训练脚本最依赖的信息。names的索引顺序必须和之前转换脚本里的class_map完全一致,比如sea_cucumber在class_map里是0,在这里就必须排在第一个,顺序错乱会导致训练时类别标签和实际物体对不上,模型不报错但验证时mAP接近零。
如果你想用更大的模型,比如yolov10x,配置文件里还需要注意scale参数。YOLOv10的官方仓库通过一个yolov10n.yaml配合命令行参数model=yolov10x.yaml来缩放通道数和层数,实际是解析yaml里的scale字段:
# yolov10x.yaml 里的缩放因子,不需要手动改 scale: 1.0 # 实际宽度乘数:width_multiple width_multiple: 1.0 depth_multiple: 1.0改模型尺寸不是直接把yolov10n.yaml里的width_multiple改成2.0就行,因为不同尺寸的RepVGG块深度和C2f的repeat次数都不同,官方预训练权重也是按固定结构训的。你要用yolov10x.csv或yolov10l.csv这些权重文件,就对应加载官方提供的yolov10x.yaml配置,不要自己乱改结构。
3.3 预训练权重选择:n/l/x怎么挑
资源压缩包里带了yolov10n.csv、yolov10l.csv、yolov10x.csv三个权重文件。n是最轻量的版本,参数量大约在2.3M左右,适合CPU推理或嵌入式设备;l是标准版,参数量约24M,适合GPU训练和推理;x是最大版本,参数量约29M,精度最高但显存和耗时都上去了。
我的选择逻辑是这样的:先看显存够不够,再决定用哪个。显存8G以下直接用n起步,别硬上x,因为训练时的batch size和输入分辨率直接影响显存占用,输入640x640、batch 16的情况下x模型显存占用接近12G。显存充足时,先加载x的COCO预训练权重做迁移学习,等模型收敛后如果精度达标再蒸馏成l或n做部署。水下目标检测的迁移学习效果高度依赖预训练权重在通用物体上的表现,COCO上更强的主干在水下场景通常也能提取到更鲁棒的特征,这个规律在多个水下benchmark上都验证过。
我的另一个习惯是把训练过程中的模型权重按epoch保存,而不是只留最后一个。YOLOv10在训练后期往往会出现epoch 80时mAP最高、到epoch 100开始过拟合的情况,如果只保存最后一轮就后悔药都没得吃。训练脚本里加一行:
# 保存模型结构定义和权重分离,方便后面导出 torch.save(model.state_dict(), f'weights/epoch_{epoch}_best.pt')参数说明:model.state_dict()只保存权重不保存结构,这样每个epoch的权重文件体积小、加载灵活;从epoch_{epoch}_best.pt恢复训练时要同时指定model=yolov10x.yaml和weights/epoch_80_best.pt,二者缺一不可。
4. 多实例学习与水下检测的结合:包级别标签如何变成像素级输出
4.1 MIL原理:从包到实例的聚合逻辑
多实例学习(Multiple Instance Learning)是处理弱监督问题的经典框架。它的核心假设是:一个“包”(Bag)里包含多个“实例”(Instance),包有标签,但包内实例的标签未知。在目标检测场景里,一张图像可以被视为一个包,图像中可能存在的目标区域是实例。如果训练数据里只有“这张图里有没有海参”这种图像级标签,而没有标注海参具体在哪,传统监督学习就用不了,MIL却能通过聚合包内实例的预测分数来训练模型。
MIL在YOLOv10里的落地思路是这样的:先让检测头对每个anchor或query产生分类置信度和回归偏移量,然后通过一个聚合函数(通常取max或top-k均值)把包内所有候选框的分数汇总成一个包级别预测,和图像级标签算损失。反向传播时,只有对包预测贡献最大的那些实例会收到梯度,其他实例的梯度被抑制,这就实现了“用图像级标签找出目标位置”的效果。
4.2 RUOD场景里为什么要用MIL
RUOD数据集的标注并不是完全干净的。我在实际使用中发现,部分图像存在漏标和错标——有些目标被水草遮挡了一半,标注框画得歪歪斜斜;有些小目标在缩略图上看不清,标注者干脆漏掉了。如果用强监督YOLO训练,这些错误标注会直接变成错误梯度,模型会在漏标的位置反复学到“这里没有目标”,导致漏检率上升。
MIL能在一定程度上容忍这种标注噪声。因为MIL只要求包级标签正确,对个别实例的标注错误不敏感。具体做法是:把一张图像的候选目标分成多个子包,每个子包对应一个可能的物体区域;在训练初期不要求每个预选框都精确回归到GT框,而是先让包级别的预测概率对上。等模型收敛到能区分前景和背景时,再用MIL输出的伪标签对检测头做第二阶段的精修。
4.3 在YOLOv10训练流程中融入MIL
把MIL加到YOLOv10的训练流程里,最直接的做法是在原有的一对一分配之后接入一个MIL聚合分支。伪代码如下:
# 假设已有YOLOv10检测头的输出 # cls_preds: [B, num_queries, num_classes] # box_preds: [B, num_queries, 4] # img_labels: [B, num_classes] 包级别标签,1表示图中有该类 import torch import torch.nn.functional as F def mil_loss(cls_preds, img_labels, top_k=3): """ 多实例学习损失 cls_preds: 所有候选query的类别预测logits img_labels: 图像级标签,0/1 """ probs = torch.sigmoid(cls_preds) # 转为概率 B, Q, C = probs.shape # 对每个类别,取包内置信度最高的top-k实例的均值作为包预测 topk_probs, _ = torch.topk(probs, k=top_k, dim=1) # [B, top_k, C] bag_probs = torch.mean(topk_probs, dim=1) # [B, C] # 用二分类交叉熵计算包级别损失 loss = F.binary_cross_entropy( bag_probs, img_labels.float(), reduction='mean' ) return loss这段代码的关键在于topk操作:torch.topk(probs, k=top_k, dim=1)取出每个类别下置信度最高的前3个候选框,再对这3个分数取平均作为包的预测。为什么不用max而用top-k均值?因为max只看最大的那个,容易受单个异常高分框影响;top-k均值更稳定,且梯度能回传到多个候选框上,有利于训练初期的收敛。top_k取值建议在2~5之间,太大则背景噪声混入包预测,太小则训练不稳定。
实际训练时,MIL损失要和YOLOv10原始的一对多损失一起使用,不能单独替换。YOLOv10的一对多分支提供稠密的监督信号,帮助模型快速学到目标的粗定位;MIL分支约束包级别的类别激活,纠偏标注噪声。两个损失的权重比例一般从mil_loss_weight=0.1开始调,如果验证集上mAP不涨,就试着加大到0.3,超过0.5会导致回归精度下降,因为模型只顾着分辨“有没有目标”,忽略了“目标在哪”。
4.4 MIL训练的迭代策略与收敛判断
MIL训练需要特别注意一个现象:训练初期loss降得很快,但验证集mAP几乎不动。这是因为模型在包级别已经把正负样本分开了,但检测框的位置还很不准确。这时候不要急着调参,给它跑够30个epoch,等回归分支的loss占比上来后mAP才开始爬升。我一般会同时监控两个指标:包级别分类准确率和框回归的IoU均值。如果后者一直在0.3以下,说明MIL分支抢了太多梯度,需要降低mil_loss_weight。如果前者都到95%以上而mAP还在30%以下,那问题多半出在数据标注本身,建议抽检一批训练样本看看标注框的质量。
5. 常见问题与排错:训练翻车、收敛异常与推理对不上的排查
5.1 loss变成NaN:不是学习率太高,就是标注坐标越界
现象:训练到第几百个iteration,loss突然从几十分变成NaN,之后再也回不来。
原因:最常见的是标注框坐标越界或宽高为负。水下数据集的XML标注经常出现xmax小于xmin、坐标超出图像宽高范围的情况。YOLOv10的回归损失对框坐标做对数变换时,遇到负的宽高直接计算出log(0)。其次是学习率过高导致梯度爆炸,但YOLOv10训练初期一般不会因为学习率直接NaN。
解决:在数据加载脚本里对每个标注框做一次合法性检查,把宽高小于等于1像素的框直接过滤掉。同时把输入分辨率下的坐标裁剪到[0, img_size]区间内。如果检查完数据没问题还是NaN,就把初始学习率从lr0=0.01降到0.001,并把warmup_epochs从3加到5。
5.2 mAP一直上不去,但训练loss正常下降
现象:训练loss曲线很漂亮,一路降到底,但验证集mAP只有十几二十,怎么调都上不去。
原因:第一,类别顺序不一致——class_map里海参是0,yaml里names第一个是鱼,模型训练时学习的映射和验证时完全错位。第二,样本分布极度不均衡,模型把所有目标都预测成高频类别。第三,验证集和训练集有重叠,评估结果虚高但你不知道,调参调了个寂寞。
解决:先用torch.load加载训练好的权重,随机抽几张验证集图片做可视化,把预测框和类别名画出来。如果发现类别名对不上物体,立刻检查class_map和yaml的names顺序。如果所有框都预测成同一类,就按2.3节的方法做样本重采样或调整cls_pw权重。
5.3 推理速度比预期慢一倍:NMS和预处理拖了后腿
现象:模型从yolov10n换到yolov10x,推理时间涨了不止模型参数比例,甚至换回n也还是慢。
原因:YOLOv10的端到端设计是省了NMS,但很多人习惯性地在推理代码里仍然套一层通用检测框架的NMS后处理,C++端inference.cpp里如果用了循环计算每个类的NMS,在大模型上会因为候选框数量庞大而变成性能瓶颈。另外图像预处理如果用了cv2.resize加BGR2RGB转换,在CPU上也很耗时。
解决:首先确认是否加载了yolov10x.csv对应的完整权重而不是只有state_dict;然后在后处理代码里打印每个阶段的耗时,定位瓶颈。如果NMS耗时占了大头,改用torchvision.ops.nms批量计算,或者直接信任YOLOv10的一对一输出舍去NMS——在RUOD这种目标稀疏的场景,去掉NMS的mAP损失通常不到0.5个点,但速度能翻倍。
5.4 小目标漏检严重:鱼的个体太小,模型根本没“看见”
现象:验证集上海参、扇贝这种中大型目标检测效果还行,但小鱼苗和远处的潜水员漏检率特别高,低于16x16像素的目标基本检不出来。
原因:输入分辨率是640x640,原图里小于16x16的目标经过下采样后特征已经消失。YOLOv10的检测头虽然有P3(80x80特征图)负责小目标,但水下散射导致小目标的边缘模糊,特征提取难度比地面场景大得多。
解决:把训练和推理的输入分辨率从640提到960或1280,小目标召回率会有明显提升,显存占用也随之翻倍。同时把mosaic=1.0的马赛克增强改为只在最后10个epoch关闭,让模型适应真实分布。如果显存不够,就减小batch size同时增大梯度累积步数,保证有效batch不变。
5.5 训练和推理结果不一致:训练时mAP有80,部署时只剩60
现象:训练脚本里验证mAP很漂亮,导出的模型在C++端推理却大打折扣。
原因:Train/Test不一致的经典问题。训练时的数据增强(马赛克、随机翻转)在验证时被关闭,但推理时你又把输入图像做了某种归一化或resize操作,和训练时的预处理流程不一致。另一个常见原因是导出的权重文件没有包含anchor或类别名信息,C++加载时用了默认配置。
解决:把训练脚本里的验证预处理和推理脚本的预处理完全对齐,统一用同一个letterbox函数和相同的归一化均值方差。导出模型时用model.export(format='onnx'),并在ONNX里固定输入尺寸,避免动态shape带来的隐性resize差异。每次改完预处理,都要重新跑一遍完整的训练验证流程再导出,不要只改推理端。
6. 从代码到部署:推理脚本、参数量化与效果验证的技巧
6.1 把训练好的模型导出为ONNX并验证输出
训练完成后第一步不是急着写部署代码,而是先用官方脚本导出ONNX,再用ONNX Runtime做一次推理对比,确认输出和PyTorch一致:
yolo export model=weights/best.pt format=onnx imgsz=640导出时imgsz必须和训练时的输入分辨率一致。我在水下场景常用的是960,因为640对小目标漏检太多。导出后用onnxruntime加载,对比PyTorch的输出:
import onnxruntime as ort import torch import numpy as np # 准备一张测试图,做与训练完全相同的预处理 input_data = preprocess(test_img) # shape: [1,3,960,960], RGB, 归一化 # PyTorch推理 model.eval() with torch.no_grad(): torch_out = model(torch.from_numpy(input_data)) # ONNX推理 sess = ort.InferenceSession('best.onnx') ort_out = sess.run(None, {sess.get_inputs()[0].name: input_data}) # 对比前1000个输出值,看最大误差 diff = np.abs(torch_out[0].cpu().numpy() - ort_out[0]) print(f"Max diff: {diff.max():.6f}")逻辑说明:PyTorch和ONNX的推理结果通常会有微小数值差异,这是浮点数计算顺序不同导致的,只要最大误差小于1e-3就说明部署链路正确。如果误差过大,检查预处理里是否有多余的torch.Tensor类型转换或batch维度的隐式增加。这一步很关键,能筛掉90%的“训练时好好的、部署时全乱套”问题。
6.2 量化压缩:从FP32到INT8的加速与精度损失
RUOD水下检测的落地场景不少是水下机器人上的边缘设备,算力有限。把模型从FP32量化到INT8是一个常用手段。YOLOv10的RepVGG结构在推理时是单路卷积,量化友好度比多分支结构好很多。我用ONNX Runtime做静态量化时,要先准备100~200张训练集图片做校准集,统计每层激活值的分布范围:
from onnxruntime.quantization import quantize_static, QuantType from onnxruntime.quantization import CalibrationDataReader class RUODCalibReader(CalibrationDataReader): def __init__(self, images, input_name='images'): self.images = images self.input_name = input_name self.iter_idx = 0 def get_next(self): if self.iter_idx >= len(self.images): return None img = preprocess(self.images[self.iter_idx]) self.iter_idx += 1 return {self.input_name: img} quantize_static( model_input='best.onnx', model_output='best_int8.onnx', calibration_data_reader=RUODCalibReader(calib_imgs), quant_format=QuantType.QInt8, per_channel=True )量化后我观察到水下场景的mAP下降通常在1~3个点,但推理速度提升1.5~2倍。如果量化后mAP掉得太多,试试把检测头那几层保持FP32不量化,只量化backbone,这能保住大部分精度。per_channel=True是按输出通道分别计算缩放因子,比per-tensor量化精度更高,这是我在水下目标上量化后得出的血泪经验。
6.3 验证部署效果:用脚本批量跑mAP而不是肉眼看图
部署阶段的验证很容易陷入“肉眼看几张图感觉还行就上了”的误区。我的习惯是写一个批量评估脚本,在完整的验证集上计算mAP@0.5和mAP@0.75,并且按类别分别统计。因为水下检测里不同类别的难度差异非常大,只看整体mAP会掩盖“鱼很准、海参全是漏检”的问题。脚本里最关键的部分是把模型的输出解析成标准评估格式:
def parse_output(sess_output, orig_shape, confidence=0.25): """ sess_output: ONNX模型输出,包含boxes和scores orig_shape: 原始图像的宽高 """ boxes = sess_output[0][0] # [num_dets, 4] or [num_queries, 4] scores = sess_output[1][0] # [num_dets, num_classes] dets = [] for i in range(boxes.shape[0]): cls_id = int(np.argmax(scores[i])) score = float(scores[i][cls_id]) if score < confidence: continue # 把归一化坐标换算回原图尺寸 x1 = boxes[i][0] / img_size * orig_shape[1] y1 = boxes[i][1] / img_size * orig_shape[0] x2 = boxes[i][2] / img_size * orig_shape[1] y2 = boxes[i][3] / img_size * orig_shape[0] dets.append([x1, y1, x2, y2, score, cls_id]) return dets把每个检测框换算回原图尺寸,用confidence过滤低置信度结果,然后和验证集的GT框计算IoU匹配,统计每一类别的AP。这个脚本我会在每次部署前跑一遍,记录日志文件。水下场景建议把confidence阈值设在0.1~0.15而不是通用检测的0.25,因为水下目标的置信度分数普遍偏低,阈值太高会漏掉大量真实目标。
从那以后我每次部署水下检测模型,都强制自己走一遍“导出ONNX→对比输出→量化→全验证集评估”的流程,不做完不交付。有没有用肉眼看不到的精度坑,只有跑完完整评估才知道。这套流程我踩过太多次坑,尤其是预处理不一致和类别顺序错乱这两类问题,看似简单却极其隐蔽,希望帮到你。
本文还有配套的精品资源,点击获取