资讯动态

水下鱼类实例分割实战:标注格式转换与YOLOv8-seg训练

发布时间:2026/10/2 2:51:20 来源:尧图企业网站定制
简介面向水产养殖监测、海洋生态研究、渔业资源管理与水下机器人视觉等场景这份基于真实水下环境采集的鱼类实例分割数据集包含七种常见经济鱼类及一个通用物体类别标注采用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、heightannotations里每条记录包含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坐标统一除以图片宽度和高度做归一化所以全是 01 的小数。注意两点第一YOLO seg 不支持多段轮廓如果一条鱼的 mask 因为遮挡断成了两块你只能保留面积最大的那块第二多边形点数不宜过多否则训练时解码慢、显存占用高。生成标注的时候我会用 Douglas-Peucker 算法把轮廓点数压到 2040 个点之间既保轮廓又保效率。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-segCOCO 转 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_okTrue) def simplify_polygon(points, max_pts40, eps1.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, keycv2.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.jpgsite01 是视频站点编号 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(ftrain: {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 \ modelyolov8s-seg.pt \ datawaterfish.yaml \ epochs200 \ imgsz640 \ batch16 \ lr00.005 \ hsv_h0.02 hsv_s0.5 hsv_v0.3 \ degrees5 translate0.1 scale0.3 \ flipud0.3 mosaic1.0datawaterfish.yaml是数据集配置内容指向训练、验证目录和类别名path: /data/water_fish_seg train: images/train val: images/val nc: 7 names: [seabass, grouper, yellow_croaker, hairtail, pomfret, miiuy_croaker, gobies]逐项说明为什么这么设imgsz640640 是速度和精度之间的常见折中。如果数据里大量是几十像素的小鱼建议试imgsz960或1280但 batch 要相应减半否则显存直接爆。做水下生态监测我一般先跑 640 看 baseline再对比 1280 看收益是否值得多出的推理耗时。batch16batch 大小受显存约束。训练阶段 batch 大能稳定梯度但水下数据集通常有很强的帧间相似性batch 太大反而会增强这种相关性加大过拟合风险。16 是 832 之间不容易翻车的中间值。lr00.005默认是 0.01但实例分割任务里 mask 分支对学习率偏敏感水下图像的低对比度又放大了不稳定性。我习惯起步降到 0.005如果前 10 个 epoch loss 仍然震荡再降到 0.002。增强项里degrees5很关键水下鱼群中鱼是任意姿态游动的但轻微旋转就够。翻转这里我同时开了flipud上下翻转和默认的水平翻转因为鱼游动方向不确定上下翻转能模拟从不同深度视角看鱼的效果。mosaic1.0mosaic 拼图增强对鱼群密集场景尤其重要它把四张图拼在一起等于制造了更多鱼密度变化和遮挡样本对实例分割的 mask 泛化帮助很大。4.3 看结果不只看总 mAP分鱼种看 PR 曲线才算数训练结束后验证输出里会给出三个关键指标mAP50、mAP50-95和mask_mAP50-95。很多人只看第一行总 mAP这是一个常见误区。水下鱼类数据集的类别分布极不均衡总 mAP 很容易被大头类别拉上去或拉下来你要做的是按类别看 PR 曲线。yolo segment val \ modelruns/segment/train/weights/best.pt \ datawaterfish.yaml \ imgsz640 \ save_jsonTruesave_jsonTrue会输出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, verboseFalse)[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 数字。数据集的可用性最终还是体现在你能不能用它稳定地生产出可信的逐帧结果上。希望这套从解包到回放的路径能帮到你少走几段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑