简介:面向水产养殖监测、海洋生态研究、渔业资源管理与水下机器人视觉等场景,这份基于真实水下环境采集的鱼类实例分割数据集,包含七种常见经济鱼类及一个通用物体类别,标注采用YOLO格式多边形点坐标,可精确勾勒鱼类轮廓,直接适配YOLOv8-Seg、Mask R-CNN等主流分割框架。全量数据按训练集九百零一张、验证集二百五十张、测试集一百二十三张划分,兼顾模型训练与效果评估需求;压缩包共两千个文件,其中一千二百七十四个txt标注文件、七百二十四张jpg原图,另含一个yaml配置文件与一份docx说明文档,整体六十一点一七MB,目录清晰即下即用。目前已有167人浏览学习,适合算法工程师、科研人员及水产智能化开发者用于实例分割模型调试、鱼种识别与种群计数等任务。相比通用分割数据集,其多边形标注细致、场景覆盖光照与浑浊度变化,有助于提升模型在水下复杂环境中的泛化能力。
1. 水下鱼类实例分割数据集.zip:它解决的是一个“计数 + 定位 + 轮廓”三合一的问题
拿到水下鱼类实例分割数据集的 zip 压缩包,很多人第一反应是“这不就是一套带标注的鱼图吗”,可真把压缩包解开、跑一遍训练才发现,它和普通目标检测数据集完全是两码事。目标检测给的是框,框住整条鱼;实例分割给的是每一条鱼的轮廓掩码,鱼尾巴翘起来、鱼鳍张开、两条鱼叠在一起,模型都要把它们各自分清楚。这个能力直接决定了水下养殖监测、捕捞评估这类场景能不能做:你说这一网有三百条鱼,用户要的是“哪条是哪条”,不是一个把鱼群框成一个大矩形的数字。这套数据集的价值就在这儿:它把“识别鱼”这件事从“知道图里有鱼”推进到“知道每条鱼长什么样、占多大面积、从哪到哪”,是后续做鱼群计数、体长估计、行为分析的地基。
适合谁用也很明确:你在做水产养殖的投喂监测、水下机器人的视觉抓取、或者渔业资源调查的视频自动分析,那这套数据就是你绕不开的第一步。它会让你同时面对“实例分割”“数据集”“水下图像退化”三个问题,缺一个都跑不出像样的结果。下面我把这套数据从格式、转换、训练到踩坑按实际落地顺序拆开讲。
2. 拆解水下鱼类实例分割任务:为什么鱼群场景必须用“轮廓”而不是“框”
2.1 实例分割、语义分割与目标检测的分界:鱼群场景的天然约束
先把三个相邻概念摆清楚。目标检测输出的是“类别 + 包围框”,对一条鱼来说,框往往会把相邻的鱼、水草、气泡一起框进去;语义分割输出的是“像素级类别”,整张图里所有鱼的像素都被标成一类,它回答的是“哪些像素是鱼”,但没有“这是第几条鱼”的概念;实例分割在语义分割的基础上多了个体编号,每个连通区域是一条独立的鱼,精确到轮廓。
水下鱼群恰恰是这三类任务里最难的那种:鱼是高度重叠的,尾巴和身体经常交叉,个体的边界在低照度、水体浑浊时非常模糊。检测框在这种场景下会把两条贴在一起的鱼合并成一个目标;语义分割把整群鱼糊成一大片;只有实例分割能给出“第 1 条鱼从背鳍到尾鳍的完整轮廓”,这样你后续做的体长测量、重量预估才有像素级依据。这就是为什么这套数据集要按下“实例分割”而不是检测来标注。
2.2 解压后的典型目录结构与标注格式:COCO 与 YOLO seg 的关系
解压 zip 后,常见做法是得到一个类似下面的结构。这个结构不是硬性标准,但它能让你一眼看出训练需要哪三样东西:图片、标注、类别清单。
water_fish_seg/ ├── images/ │ ├── train/ # 训练图片,一般是原始分辨率 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 与图片同名的 txt 标注文件(YOLO seg 格式) │ └── val/ ├── annotations/ │ └── instances_train.json # COCO 格式完整标注 ├── classes.txt # 类别名,每行一个 └── README.md这套水下鱼类数据集常见的标注载体是 COCO JSON,因为 COCO 是当前实例分割标注的事实标准。COCO JSON 里两个最关键的字段是images和annotations。images记录每张图的id、file_name、width、height;annotations里每条记录包含image_id、category_id、segmentation、bbox、area、iscrowd。这里最值得注意的就是segmentation字段:它在多边形标注下是形如[[x1, y1, x2, y2, ...]]的绝对像素坐标数组,一圈点连起来就是这条鱼的轮廓;在少数密集标注场景下也可以是 RLE 编码。如果你拿到的是 RLE,需要先用 pycocotools 的maskToPolygon转回多边形才能喂给 YOLO 系模型。
而 YOLO 实例分割格式则完全不同。每个 txt 文件对应一张同名图片,每一行对应一个实例,格式是:
class_id x1 y1 x2 y2 ... xn yn坐标统一除以图片宽度和高度做归一化,所以全是 0~1 的小数。注意两点:第一,YOLO seg 不支持多段轮廓,如果一条鱼的 mask 因为遮挡断成了两块,你只能保留面积最大的那块;第二,多边形点数不宜过多,否则训练时解码慢、显存占用高。生成标注的时候我会用 Douglas-Peucker 算法把轮廓点数压到 20~40 个点之间,既保轮廓又保效率。
2.3 质量摸底:动手训练前先花十分钟看数据的地基
拿到数据集后最忌讳直接开训。我习惯先写一段统计脚本,看三件事:类别是否均衡、mask 面积分布是否合理、轮廓点密度是否异常。
import json from collections import Counter import numpy as np ann_path = "water_fish_seg/annotations/instances_train.json" with open(ann_path) as f: anns = json.load(f) cat_id2name = {cat["id"]: cat["name"] for cat in anns["categories"]} # 1. 每个类别的实例数量 cat_counter = Counter(a["category_id"] for a in anns["annotations"]) for cat_id, cnt in cat_counter.most_common(): print(f"{cat_id2name[cat_id]:>12s}: {cnt:5d}") # 2. mask 面积分布,画直方图或者直接看 min/median/max areas = np.array([a["area"] for a in anns["annotations"]]) print("area min/median/max:", areas.min(), np.median(areas), areas.max()) # 3. 轮廓点数分布,点数过千要小心后续转换开销 pt_counts = [] for a in anns["annotations"]: seg = a["segmentation"] if seg and isinstance(seg[0], list): pt_counts.append(len(seg[0]) // 2) print("points per polygon: min/median/max", np.min(pt_counts), np.median(pt_counts), np.max(pt_counts))这段代码的核心作用不是跑出结论给你看,而是帮你建立对标注质量的直觉。类别计数如果出现某类只有几十条、另一类上千条,后面就必须处理类别不均衡。面积分布如果最小值和中位数差出两个数量级,说明数据集里既有贴脸特写的大鱼,也有远景的几十像素小目标,imgsz该设多大就要重新权衡。轮廓点数如果中位数超过 300,转换之后训练效率会明显变差,需要做轮廓简化。这个摸底是整套流程里最便宜的一步,省掉它,后面所有环节都可能被数据本身的问题带偏。
3. 把水下鱼类数据集喂给 YOLOv8-seg:COCO 转 YOLO 格式与最小可跑通路线
3.1 COCO 多边形标注转 YOLO 分割 txt:完整脚本与参数说明
水下鱼类数据集最常见的实际使用路径是拿来训练 YOLOv8-seg 或 YOLO11-seg,因为它们开箱即用、部署方便。但 YOLO 不认 COCO JSON,需要先做转换。这个转换脚本看起来简单,实际有四个坑:坐标归一化、RLE 判断、多段轮廓、空 mask 过滤。下面是一份我常用的转换代码。
import json import os import numpy as np from pycocotools import mask as coco_mask IMG_BASE = "water_fish_seg/images/train" ANN_FILE = "water_fish_seg/annotations/instances_train.json" OUT_LABEL_DIR = "water_fish_seg/yolo_labels/train" with open(ANN_FILE) as f: coco = json.load(f) img_id2info = {img["id"]: img for img in coco["images"]} os.makedirs(OUT_LABEL_DIR, exist_ok=True) def simplify_polygon(points, max_pts=40, eps=1.0): """Douglas-Peucker 简化轮廓点,注意输入是成对坐标的扁平数组""" arr = np.array(points).reshape(-1, 2) # 保证多边形是闭合的,否则 DP 结果会少一段 if not np.allclose(arr[0], arr[-1]): arr = np.vstack([arr, arr[0]]) # 这里省略 DP 实现细节,实际使用可调用 cv2.approxPolyDP return arr for ann in coco["annotations"]: img = img_id2info[ann["image_id"]] w, h = img["width"], img["height"] seg = ann["segmentation"] # 情况一:segmentation 是多边形列表 if isinstance(seg, list) and seg and isinstance(seg[0], list): poly = seg[0] # YOLO seg 只需要单个轮廓 pts = np.array(poly).reshape(-1, 2).astype(np.float32) pts[:, 0] /= w pts[:, 1] /= h # 过滤退化轮廓:点太少或面积太小 if len(pts) < 6: continue line = [str(ann["category_id"] - 1)] line += [f"{x:.6f}" for x in pts.flatten().tolist()] out_name = os.path.splitext(img["file_name"])[0] + ".txt" with open(os.path.join(OUT_LABEL_DIR, out_name), "a") as f: f.write(" ".join(line) + "\n") # 情况二:segmentation 是 RLE,走 pycocotools 转多边形 elif isinstance(seg, dict) and "counts" in seg: rle = coco_mask.frPyObjects(seg, h, w) mask_array = coco_mask.decode(rle) contours, _ = cv2.findContours(mask_array, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: continue largest = max(contours, key=cv2.contourArea) # 轮廓坐标是 (x, y) 但 OpenCV 返回的是 (col, row),顺序恰好一致 pts = largest.squeeze().astype(np.float32) if pts.ndim != 2 or len(pts) < 6: continue pts[:, 0] /= w pts[:, 1] /= h line = [str(ann["category_id"] - 1)] line += [f"{x:.6f}" for x in pts.flatten().tolist()] out_name = os.path.splitext(img["file_name"])[0] + ".txt" with open(os.path.join(OUT_LABEL_DIR, out_name), "a") as f: f.write(" ".join(line) + "\n")这段脚本逻辑上分两条分支走:多边形标注直接拿第一个多边形(seg[0]),RLE 标注先解码成二值 mask 再提取最大外轮廓。两个分支都要做两件事:归一化坐标和过滤退化轮廓。为什么过滤点少于 6 个的实例?因为少于 3 个点连不成多边形,少于 6 个坐标差的实例在 YOLO 训练里基本全是噪音,不滤掉会让 loss 上出现莫名其妙的尖峰。category_id - 1是因为 COCO 的类别 id 通常从 1 开始,而 YOLO 要求类别从 0 开始。算错了这个偏移,训练出来的模型所有类别都会错一个位,但损失函数正常下降,属于最典型的一类“看着在训练、实际全错”的翻车事故。
3.2 转换后可视化核对:mask 叠图,检查轮廓是否变性
转换完不是马上开训,先做一次叠图确认。把 YOLO txt 里的归一化坐标还原回像素坐标,画在原图上,用随机颜色区分每一条鱼,然后肉眼扫一遍。
import cv2 import numpy as np def draw_yolo_seg(img_path, label_path, classes, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] colors = [(np.random.randint(0,255), np.random.randint(0,255), np.random.randint(0,255)) for _ in range(100)] with open(label_path) as f: for line in f: parts = line.strip().split() cls = int(parts[0]) coords = np.array([float(x) for x in parts[1:]]).reshape(-1, 2) coords[:, 0] *= w coords[:, 1] *= h pts = coords.astype(np.int32).reshape(-1, 1, 2) cv2.polylines(img, [pts], True, colors[cls % 100], 2) cv2.imwrite(out_path, img) # 随机抽 20 张做人工目视检查 import glob, random imgs = glob.glob("water_fish_seg/images/train/*.jpg")[:20] for img_path in imgs: label_path = img_path.replace("images/train", "yolo_labels/train").replace(".jpg", ".txt") draw_yolo_seg(img_path, label_path, None, "check_" + os.path.basename(img_path))这一步要重点看三类异常:一是轮廓点是否因为归一化的四舍五入出现向内收缩,尤其在鱼尾这种细长部位,如果:.6f精度不够,小目标的轮廓会磨掉一圈;二是原本鱼鳃部位的缺口是不是被简化操作“拉直”了;三是重叠的鱼有没有出现两只鱼共享一个 mask 的情况。任何一类异常都不要急着进训练,先回到转换逻辑里修。这个核验过程每批次数据只需要做一次,但它能省下后面调模型的大量时间。
3.3 数据集划分:同一个视频的帧不能跨训练验证
划分这步看起来只是train_test_split一行代码的事,实际上水下数据集有一个特殊坑:很多图是从同一段视频里连续抽帧来的。如果随机划分,同一群鱼的前半段帧进了 train,后半段帧进了 val,验证分数会虚高得离谱。常见做法是按视频源文件或按文件名前缀分组,保证同一个视频的帧只能落在同一个集合里。
import glob, os, random from collections import defaultdict imgs = sorted(glob.glob("water_fish_seg/images/train/*.jpg")) # 假设文件名形如 site01_00123.jpg,site01 是视频站点编号 grouped = defaultdict(list) for p in imgs: site = os.path.basename(p).split("_")[0] grouped[site].append(p) sites = list(grouped.keys()) random.seed(42) random.shuffle(sites) split_point = int(len(sites) * 0.8) train_sites = set(sites[:split_point]) val_sites = set(sites[split_point:]) train_files = [p for site in train_sites for p in grouped[site]] val_files = [p for site in val_sites for p in grouped[site]] print(f"train: {len(train_files)} images, val: {len(val_files)} images")以site01这样的前缀作为分组键,核心思想是“同一水域、同一鱼群、同一光照条件下拍出的帧,只能出现在一个集合里”。水下视频帧之间高度相关,鱼游动轨迹连续,如果不分组切分,模型在验证集上看到的其实就是训练集的“下一帧”,这种虚高指标会在实际部署时立刻打断你的信心。按站点分组后,泛化评估才可信。
4. 用 YOLO 训练水下鱼类实例分割模型:选型、参数与验证指标
4.1 模型选型:从 n 到 m 的取舍,先跑通再提精度
把数据准备好后,第一个决策是选哪个模型。这里我不直接给“最优解”,因为模型选择取决于你有几张显卡、每张图平均有几十条鱼、部署端是边缘盒子还是服务器。常见的几条路线我在下表里列清楚。
| 模型 | 参数量级 | 分割精度 | 推理速度 | 适用场景 |
|---|---|---|---|---|
| YOLOv8n-seg | 小 | 中等偏下 | 最快 | 先跑通全流程、验证数据质量 |
| YOLOv8s-seg | 中 | 中等 | 快 | 单卡训练、部署到工控机 |
| YOLOv8m-seg | 中偏大 | 较高 | 中等 | 离线分析、服务器推理 |
| Mask R-CNN (R50) | 大 | 高 | 慢 | 标注质量极高、对边界要求苛刻 |
我自己的习惯是:第一次跑永远用YOLOv8n-seg,它训练快、显存占用低,可以在半小时内验证数据和配置是否连通。如果你连 n 模型都跑不起来,调imgsz和 batch 的优先级远高于换大模型。等 n 模型的 mAP 曲线能正常上升,再切换到 s 或 m 提精度。这套水下鱼类数据如果只做单类鱼群分割,s 模型基本够用;如果包括十几类鱼种细分,建议用 m,因为鱼种之间外观差异小,小模型容易把海鲈和石斑混淆。
4.2 关键训练参数:图像尺寸、batch、epochs 与数据增强
选定模型后,最重要的五个参数是imgsz、batch、epochs、lr0和增强策略。下面是针对水下鱼类数据集的典型配置。
yolo segment train \ model=yolov8s-seg.pt \ data=waterfish.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ lr0=0.005 \ hsv_h=0.02 hsv_s=0.5 hsv_v=0.3 \ degrees=5 translate=0.1 scale=0.3 \ flipud=0.3 mosaic=1.0data=waterfish.yaml是数据集配置,内容指向训练、验证目录和类别名:
path: /data/water_fish_seg train: images/train val: images/val nc: 7 names: ['seabass', 'grouper', 'yellow_croaker', 'hairtail', 'pomfret', 'miiuy_croaker', 'gobies']逐项说明为什么这么设:
imgsz=640:640 是速度和精度之间的常见折中。如果数据里大量是几十像素的小鱼,建议试imgsz=960或1280,但 batch 要相应减半,否则显存直接爆。做水下生态监测我一般先跑 640 看 baseline,再对比 1280 看收益是否值得多出的推理耗时。batch=16:batch 大小受显存约束。训练阶段 batch 大能稳定梯度,但水下数据集通常有很强的帧间相似性,batch 太大反而会增强这种相关性,加大过拟合风险。16 是 8~32 之间不容易翻车的中间值。lr0=0.005:默认是 0.01,但实例分割任务里 mask 分支对学习率偏敏感,水下图像的低对比度又放大了不稳定性。我习惯起步降到 0.005,如果前 10 个 epoch loss 仍然震荡,再降到 0.002。- 增强项里
degrees=5很关键:水下鱼群中鱼是任意姿态游动的,但轻微旋转就够。翻转这里我同时开了flipud(上下翻转)和默认的水平翻转,因为鱼游动方向不确定,上下翻转能模拟从不同深度视角看鱼的效果。 mosaic=1.0:mosaic 拼图增强对鱼群密集场景尤其重要,它把四张图拼在一起,等于制造了更多鱼密度变化和遮挡样本,对实例分割的 mask 泛化帮助很大。
4.3 看结果不只看总 mAP:分鱼种看 PR 曲线才算数
训练结束后,验证输出里会给出三个关键指标:mAP50、mAP50-95和mask_mAP50-95。很多人只看第一行总 mAP,这是一个常见误区。水下鱼类数据集的类别分布极不均衡,总 mAP 很容易被大头类别拉上去或拉下来,你要做的是按类别看 PR 曲线。
yolo segment val \ model=runs/segment/train/weights/best.pt \ data=waterfish.yaml \ imgsz=640 \ save_json=Truesave_json=True会输出predictions.json,里面逐类给出了 AP。我判断模型是否可用有三个标准:
- 主线经济鱼种的 mask AP50 不低于 0.85,否则后续计数误差太大;
- 类别间 mAP 差距不超过 15 个百分点,差距过大说明稀有类样本量不足或特征太接近;
- 小目标(面积占比小于 5% 的鱼)的 mask 是否稳定出现,如果没有独立维度看小目标表现,就按
area分组统计。
另外,训练过程中 loss 曲线的形态也要看明白。前 20 个 epoch loss 快速下降是正常的;如果 loss 在前 5 个 epoch 直接掉到接近 0,多半是标签和输入图像没对上(常见是类别偏移);如果 50 个 epoch 后 loss 还在锯齿状大幅波动,常见原因是 batch 太小或学习率太高,先降学习率,不要盲目加 epoch。
5. 水下鱼类实例分割的避坑清单:这五个坑我几乎每次都会撞上
5.1 类别严重不均衡:loss 平稳但稀有鱼种 AP 惨不忍睹
现象:训练时 loss 曲线非常平稳地下降,总 mAP 到了 0.9,但看单类指标,名贵鱼种比如石斑鱼 AP 只有 0.4,甚至压根没被模型预测出来。原因:数据集中常见鱼种占上千条,稀有鱼种只有几十条,模型学到的特征分布被大头类别完全压制。解决:先按 2.3 的统计脚本确认不均衡比例,然后做两类操作。第一,对稀有类别做复制粘贴增强——把稀有鱼类的 mask 区域随机裁剪后贴到没有该类别的位置,配合尺度缩放和旋转;第二,在训练配置里给稀有类加大 loss 权重。YOLO 不直接暴露 per-class loss 权重,常见做法是通过class_weight配置项传入字典,或者把稀有类的 train 图片按倍数重复,我一个项目里就是把石斑鱼图片 x5 复制进 train 目录,配合轻微增强后才把 AP 拉回 0.72。
5.2 低照度下 mask 边界标注模糊:验证集边界抖动严重
现象:模型训练指标正常,但可视化回放时发现验证集中鱼的轮廓边缘在相邻帧之间剧烈抖动,这一帧尾巴尖出去了,下一帧尾巴尖又缩回来。原因:水下低照度图片中鱼体与水体背景的对比度极低,标注员在标注时把同一只鱼在不同帧里勾出了不同形状,本质是标注噪声。解决:先统计单只鱼在连续帧里 mask 面积的变异系数,如果超过 15%,说明标注不稳定。解决分两步:一是对 mask 做形态学后处理,用开运算去掉边界毛刺,把抖动的高频成分磨平;二是训练时增大scale增强以抵消边界噪声,并让模型用更大的感受野覆盖鱼体,而不是盯着边界逐像素拟合。
5.3 鱼群重叠导致实例粘连:一个 mask 覆盖了两条鱼
现象:密集鱼群中两条鱼身体交叉,标注文件里只标了一个多边形,训练出来的模型也会把这两条鱼预测成一个实例。原因:标注规范里没有写“遮挡到一定程度必须分段标注”,或者标注员偷懒只画了可见部分最完整的那条。解决:写一个小工具,检查一个 mask 多边形是否可以用一条直线近似分割成两个面积相近的区域。实践中更简单的办法是设置面积异常阈值——如果某个实例的 mask 面积比该类中位数大出 2.5 倍以上,拉出来人工检查。如果确实粘连,就只保留遮挡最严重的那条鱼前面那条的 mask,宁可把被挡住的鱼去掉,也不要让模型学出“两个头合成一个身体”的错误轮廓。
5.4 灰度与彩色图像混存:模型在单通道图上直接崩掉
现象:训练中途报 shape mismatch,或者训练正常但验证集 AP 暴跌。原因:水下视频来源不同,有的水下相机输出彩色图,有的深水相机只输出灰度图;数据集打包时没有统一都转成三通道,转换脚本读入单通道图后直接复制成三通道,导致颜色分布出现一整类“伪 RGB”样本。解决:数据预处理强制全部转 RGB,并且记录灰度图比例。如果灰度图占比超过 10%,干脆把增强里的hsv_h、hsv_s全部改为 0,避免颜色增强产生无效扰动。
5.5 同源视频跨集合导致数据泄漏:验证指标完全失真
现象:训练时 loss 正常下降,验证 mAP 高达 0.97,一拿到新的水下视频就明显变差。原因:同一个视频连续帧被随机切分到 train 和 val,模型在验证集里看到了自己在训练集里的“邻居帧”。鱼在水中游动缓慢,相邻帧之间几乎只有像素级噪声差异,模型等于在异常简单的复制任务上测试。解决:回到 3.3 的分组划分逻辑,按视频站点分组。但注意一个补充坑:如果文件名前缀正好是站点编号,分组没问题;如果文件名是随机 hash,就按文件元数据的录制时间戳分组,时间差小于 5 秒的帧必须归入同一集合。
6. 进阶验证手法:把预测结果按帧回放,看边界失真而不是只看 mAP
训练完模型、指标也漂亮了,但真正交付前我还会多做一步:把预测结果和真值叠在一起,按视频帧顺序做回放。mAP 是一个集合统计指标,它会告诉你平均表现,但不会告诉你某一段水草背景下背景被误判成鱼、鱼群高速游过时 mask 出现拖影这种瞬时故障。回放能让你看到模型在时间维度上的稳定性。
import cv2, glob, os from ultralytics import YOLO model = YOLO("runs/segment/train/weights/best.pt") imgs = sorted(glob.glob("water_fish_seg/video_frames/*.jpg"))[:200] out = cv2.VideoWriter("pred_replay.mp4", cv2.VideoWriter_fourcc(*"mp4v"), 10, (640, 480)) for img_path in imgs: frame = cv2.imread(img_path) frame = cv2.resize(frame, (640, 480)) result = model(frame, verbose=False)[0] masks = result.masks if masks is not None: for i, mask in enumerate(masks.data): mask = mask.cpu().numpy().astype("uint8") contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 给每个实例画随机颜色轮廓 color = (0, 255 * (i % 2), 255 * ((i + 1) % 2)) cv2.drawContours(frame, contours, -1, color, 2) out.write(frame) out.release()这段代码把连续帧的预测结果写成了 mp4。回放时要盯三个东西:mask 边界在鱼转身时是否出现“毛刺”、两条鱼交错时实例是否互换身份、鱼尾快速摆动时轮廓是否缩水。用人的眼睛去观察这些瞬时问题,比任何指标都直接。这套方法论是我从一次渔业资源调查项目里沉淀下来的:指标全绿,结果一帧帧看发现浅色鱼在白色水底下的 mask 边界几乎无法用,最后靠回放才定位到是增强策略里hsv_v太高。从那以后,每次交付前我都会强制自己做一次回放抽查,而不是只看 Excel 表格里的 mAP 数字。数据集的可用性,最终还是体现在你能不能用它稳定地生产出可信的逐帧结果上。希望这套从解包到回放的路径能帮到你少走几段弯路。
本文还有配套的精品资源,点击获取