简介:水下鱼类实例分割数据集面向水产养殖监测、海洋生态调查、渔业资源管理及水下机器人视觉等场景,包含7种经济鱼类的精细多边形轮廓标注,总计1274张真实水下图像,划分为训练集901张、验证集250张、测试集123张,可直接用于YOLOv8-Seg、Mask R-CNN等实例分割模型的训练与评估。包体共2000个文件,以1274个YOLO格式txt标注文件为主,辅以724张水下实拍jpg图像、类别配置yaml及说明文档docx,压缩包大小约61.17MB,即取即用。目前已有167人学习下载,适合算法工程师、科研人员及水产智能化开发者。数据集采集自真实水下环境,涵盖光照变化、水体浑浊等实际挑战,标注精度达到像素级,可用于鱼种识别、种群计数、尺寸统计等细粒度任务;同时7类鱼种覆盖不同姿态与视角,为海洋生态保护和可持续渔业决策提供可靠视觉数据支撑。
1. 水下鱼类实例分割数据集.zip:压缩包里装的不只是一堆鱼图
“水下鱼类实例分割数据集”这个标题,乍看像是一个随手命名的压缩包,但解压之后你会发现,它是一套典型的垂直场景实例分割训练资产:水下拍摄的鱼类图像、逐实例标注的掩码、类别清单,以及分布在训练和验证目录下的标注文件。它的目标非常具体——让实例分割模型在蓝绿色调主导、低能见度的水下画面里,把每条鱼单独分割出来,而不是把整群鱼糊成一团。它适合水产养殖监测、海洋牧场巡检、鱼类行为分析和生态调查这类场景。对刚入门实例分割的新手来说,这个数据集的价值在于把水下低对比度、色彩偏移、鱼群遮挡这些真实痛点,提前封装进了标注里,省去重新采集视频、逐帧勾边的功夫。
2. 从解压到可训练:目录结构、标注格式与数据完整性校验
2.1 先分清两种标注载体:COCO JSON与YOLO txt在实例分割里的差异
解开水下鱼类实例分割数据集.zip之后,第一件事不是双击看图片,而是先分清楚标注放在哪里、以什么格式存放。实例分割数据集的标注载体,主流就两种:COCO JSON和YOLO txt。
COCO风格用一个全局JSON承载所有信息,至少包含images、annotations、categories三块。images记录图片文件名、宽高和ID;annotations记录每个实例的image_id、category_id、bbox,以及关键字段segmentation;categories记录类别ID与名称。segmentation字段有两种写法:一是多边形顶点列表,二是RLE运行长度编码。多边形直观,但标注粗糙时顶点动辄上千;RLE编码体积小,人眼没法直接读,解码时得靠pycocotools。COCO2017数据集结构就是这么设计的,很多水下鱼类数据集也沿用了这套规范。
YOLO风格则相反,不搞全局文件,每张图片对应一个同名txt。txt每行是一个实例,第一列为类别ID,之后是归一化的多边形坐标,即“x1 y1 x2 y2 …”。这种格式喂给YOLO训练脚本最直接,文件夹挪动也方便,但你想做类别分布统计、面积分析时,反而要把所有txt重新解析一遍。实例分割和语义分割的差异在格式上也体现得很明显:语义分割给每个类别一张全图mask,实例分割必须为每个个体单独写一组多边形。
实操中碰到最多的组合是:数据集.zip解压后是COCO格式,而你想跑YOLOv8-seg或YOLO26这一类实例分割训练。这时候必须做一次格式转换:COCO JSON解码出每个实例的mask,再转成多边形,最后归一化写入txt。第二常见的情况反过来:数据集给的是YOLO txt,但没有类别名,只有类别ID,你得自己维护一份类别映射表才能接训练。这两步决定后面所有流程能不能顺利走通,值得在开头就理顺。
2.2 解剖一个典型样本:图像、mask、标注三者的对应关系
一个合格的鱼类实例分割样本,至少由三样东西组成:图像本身、掩码、标注条目。
水下鱼类图像和通用目标检测图的差别很大。常规场景里目标物体与背景有明确颜色分界,水下则是蓝绿色调覆盖全场,鱼的体色与背景灰度接近,而且随着深度、光照不同,同一条鱼在不同帧里的色彩偏移可以漂移很多。因此,这个数据集的图像帧质量,往往决定了训练上限,而不是模型结构。掩码则是数据集的核心资产,同一个类别的不同个体要有独立标注,这也是实例分割和语义分割在数据层面的根本区别。标注条目记录类别、实例ID、bbox和分割轮廓。
目标检测只要框住鱼就算对,实例分割要求轮廓贴着鱼身走。框里的背景像素在检测里是容忍噪声,在实例分割里会直接污染mask边缘,表现为训练后轮廓锯齿、鳍部丢边。水下鱼类的胸鳍、尾鳍经常与背景融为一体,标注时如果没逐帧放大钩边,很容易留下锯齿边或把鳍裁掉。这种边缘质量的问题,后面训练时会集中爆发。
还有一个容易被忽略的问题:类别颗粒度。有的数据集只分到“鱼”,有的分到具体物种。颗粒度越细,类别不均衡和样本量需求就越大。拿到压缩包后,先看分布再决定要不要重采样,不要直接拉进训练脚本。我在处理类似数据集时养成一个习惯,第一件事永远是跑统计脚本,把类别数量、bbox面积、平均实例数打印出来,再做后续决策。这个习惯帮我避开过不少坑。
2.3 用Python脚本快速scan数据集的完整性
解压之后先写一个校验脚本,用数据看全貌,不要靠肉眼去猜。
# scan_coco.py import json from collections import Counter from pathlib import Path data_root = Path("underwater_fish_dataset") ann_path = data_root / "annotations" / "instances_train.json" with open(ann_path, "r", encoding="utf-8") as f: coco = json.load(f) images = coco["images"] annos = coco["annotations"] cats = coco["categories"] print("图片总数:", len(images)) print("实例总数:", len(annos)) print("类别:", [c["name"] for c in cats]) cat_id_to_name = {c["id"]: c["name"] for c in cats} cat_cnt = Counter() area_list = [] for ann in annos: name = cat_id_to_name.get(ann["category_id"], "unknown") cat_cnt[name] += 1 bbox = ann["bbox"] area_list.append(bbox[2] * bbox[3]) print("类别分布:", dict(cat_cnt)) if area_list: print("平均bbox面积:", round(sum(area_list) / len(area_list), 1))这段脚本回答三个问题:体量够不够、类别分布是否均匀、目标平均尺度多大。水下鱼类场景里类别分布几乎天然不均衡,容易拍的鱼占大头,难拍的鱼只有零头。如果分布相差十倍以上,训练时少数类极易被淹没。此时应先做类别重采样,比训练完再抢救便宜得多。
脚本跑完还要人工抽查一下file_name是否真实存在于图片目录。JSON指向的图片如果根本不存在,训练缓存生成阶段不会报错,但训练批次里会混入空标注,样本数量对不上。这种问题最隐蔽,属于典型的“黑匣子”阶段难排查的类型。先把扫描做完,再进转换,顺序不要颠倒。
3. 把COCO标注转成YOLO-seg格式:RLE解码、轮廓提取与全量转换脚本
3.1 读取COCO JSON并解析实例标注:RLE解码与轮廓提取
COCO JSON里的segmentation字段,对同一个实例可能有两种形态:多边形顶点列表,或者RLE字典。pycocotools的mask模块能把两者统一转成二值mask。解码之后,一个实例对应一张与图像同尺寸的二值图,像素值为1的位置就是这条鱼的身体。
from pycocotools import mask as maskUtil import numpy as np def coco_ann_to_mask(ann, h, w): seg = ann["segmentation"] if isinstance(seg, dict): # RLE 压缩编码,直接解码 rle = maskUtil.frPyObjects([seg], h, w)[0] return maskUtil.decode(rle).astype(np.uint8) # 多边形顶点列表,可能是多个子区域 rles = maskUtil.frPyObjects(seg, h, w) merged = np.zeros((h, w), dtype=np.uint8) for r in rles: merged += maskUtil.decode(r).astype(np.uint8) return (merged > 0).astype(np.uint8)这段代码的逻辑是:先判断segmentation的类型。如果是字典,说明是RLE,直接用frPyObjects包装后decode;如果是列表,说明是多个多边形顶点序列,先逐个转成掩码再累加,最后压回0/1。为什么要累加而不是直接覆盖?因为COCO允许同一个实例由多个不相邻的轮廓组成,比如鱼鳍被水草遮挡后分成了两块。直接覆盖会丢掉第二块区域,累加再二值化最稳妥。
decode得到的mask默认是uint8,但某些版本的numpy相加时要注意溢出。水下鱼类的mask面积通常不大,累加次数有限,溢出风险不高。为保险起见,最后统一做布尔比较转成二值,这一步习惯保留。
拿到mask后,下一步是提取轮廓坐标。这里用OpenCV的findContours,配合approxPolyDP做简化。
import cv2 def mask_to_polygons(mask, eps_ratio=0.002, min_points=4): contours, _ = cv2.findContours( mask.astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE ) polys = [] for cnt in contours: cnt = cnt.squeeze(axis=1) if len(cnt) < min_points: continue peri = cv2.arcLength(cnt, True) simplified = cv2.approxPolyDP(cnt, eps_ratio * peri, True) polys.append(simplified.reshape(-1).tolist()) return polysRETR_LIST模式会返回所有轮廓,不建立层级关系。水下鱼类掩码如果有内孔,这些内轮廓也会被一并返回,转换成YOLO多边形时能保留孔洞的负空间。CHAIN_APPROX_SIMPLE只保留必要的拐点,减少坐标数量。approxPolyDP的eps_ratio是关键参数,它表示简化容差占轮廓周长的比例。我一般从0.002起步,分辨率高的图片可以调到0.001以下,否则边缘会明显失真。
3.2 归一化坐标与类别映射:转换脚本全量代码
把上面两步拼接起来,就是一个完整的COCO转YOLO-seg工具。下面这个版本是我常用的最小实现。
# coco_to_yolo_seg.py import json import cv2 import os import numpy as np from pathlib import Path def coco_to_yolo_seg(coco_json, img_root, out_root, cat_id_offset=0, min_area=4, eps_ratio=0.002): Path(out_root).mkdir(parents=True, exist_ok=True) with open(coco_json, "r", encoding="utf-8") as f: coco = json.load(f) # 类别ID重映射,从0开始或偏移指定值 cat_map = { c["id"]: i + cat_id_offset for i, c in enumerate(coco["categories"]) } for img in coco["images"]: img_path = os.path.join(img_root, img["file_name"]) image = cv2.imread(img_path) if image is None: print("跳过:", img_path) continue h, w = image.shape[:2] # 找到属于当前图片的所有实例 anns = [a for a in coco["annotations"] if a["image_id"] == img["id"]] lines = [] for ann in anns: mask = coco_ann_to_mask(ann, h, w) if int(mask.sum()) < min_area: continue poly_list = mask_to_polygons(mask, eps_ratio=eps_ratio) for poly in poly_list: norm = [] for i in range(0, len(poly), 2): norm.append(f"{poly[i] / w:.6f}") norm.append(f"{poly[i + 1] / h:.6f}") cls_id = cat_map[ann["category_id"]] lines.append(str(cls_id) + " " + " ".join(norm)) if lines: out_txt = out_root / f"{Path(img['file_name']).stem}.txt" with open(out_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines)) print("转换完成")执行流程是:读取COCO全局JSON,重映射类别ID,遍历images块,逐图读取图像尺寸,匹配该图所有标注,解码mask,过滤小面积噪声,提取轮廓,归一化坐标,最后写txt。类别ID重映射这步很多新手会漏,COCO的category_id可能从1开始甚至有跳号,YOLO训练要求从0开始连续编号。cat_id_offset参数用来处理“0留作背景”的自定义约定。
归一化坐标统一保留6位小数。太短会丢掉轮廓细节,太长文件膨胀。对常见分辨率,6位小数已经能把精度控制在亚像素级。如果图片宽高不是整数倍,直接用float除法即可,YOLO训练时会做双线性插值,坐标越界反而麻烦。
3.3 转换的四个边界坑:坐标越界、空mask、多实例合并、类别对不齐
转换脚本看着简单,实际能踩的坑不少,我整理成四条边界情况。
第一是坐标越界。标注员勾边时手抖,多边形某个顶点可能落在图像外面。这样的坐标写进txt,YOLO训练时会把该顶点解释为“按比例放大到整幅图”,轻则mask变形,重则整个batch出错。做法是在归一化之后做一次clip,把x限制在0到1之间,y同理,但要注意如果整个轮廓都飞出画面,就直接丢弃。
第二是空mask。有些标注的segmentation解码之后面积几乎为零,可能是标注过程中误操作留下的空轨迹。min_area参数就是兜底用的,我习惯设4像素,低于这个阈值直接跳过。设太高会把小鱼丢掉,设太低又挡不住噪声点。
第三是多实例合并。鱼群密集时,标注可能把两只鱼的轮廓画进了同一个ann条目。转换后的txt会输出一个类别ID+两组坐标,模型训练时无法区分这是两只鱼还是一条鱼的两个碎片。处理办法是转换时按轮廓是否封闭、面积占比做拆分,至少要在转换后的结果里人工抽查10%的txt。
第四是类别对不齐。水下鱼类数据集有时候会出现categories块和annotations块的类别ID不一致,某个ann里写了一个categories中不存在的ID。cat_map里直接取会抛KeyError,代码里应改成cat_map.get(ann["category_id"], -1),转换时把-1跳过并打印警告,避免整个脚本中断。这四条坑处理完,转换出来找一组图片叠加可视化,确认轮廓确实贴着鱼身,再进入训练环节。
4. 用YOLOv8-seg训练水下鱼类实例分割:YAML配置、最小命令与可视化验证
4.1 数据集目录组织与YAML配置
转换完成后的标签,要和图片按train/val划分对齐。常见的做法是数据目录里放四块:images/train、images/val、labels/train、labels/val,图片和同名txt分别放进去。水下鱼类数据集的拍摄往往是一条视频截多帧,帧之间高度相似,划分时最好按视频片段分,不要随机分帧,否则验证集会泄漏训练信息,mAP虚高。
数据目录组织好之后,建一个YAML文件,指向图片路径并声明类别名。
# fish_seg.yaml path: ./underwater_fish_dataset train: images/train val: images/val names: 0: carp 1: tilapia 2: perchpath字段写绝对路径或相对路径,ultralytics会以它为基准拼接train和val。names的ID必须和转换脚本里的类别映射对应,这里不需要写labels路径,训练器会自动把images/train替换成labels/train来找txt。如果你自己改了目录名,比如把标签放在annos/train,就必须在训练参数里额外指定或者保持默认同名约定。
YOLOv8-seg的标签格式来自YOLO生态约定,水下属性的 txt 文件与第3章转换脚本输出一致。类别0、1、2的顺序,决定了模型输出的索引,训练阶段不要频繁改动,否则权重和类别对齐关系会乱。
4.2 用YOLOv8实例分割训练的最小命令
目录和YAML就绪后,跑一次完整训练的最小命令如下。
yolo segment train \ model=yolov8n-seg.pt \ data=fish_seg.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ cache=True \ patience=15 \ project=run/fish_seg \ name=exp1model参数换成yolov8n-seg.pt,这是实例分割系列里参数量最小的预训练权重,适合先验证数据链路是否通畅。imgsz设成640是起步值,水下小目标多时可以升到768,但训练速度和显存占用会明显上升。batch要按显存调整,12GB显存跑n模型640分辨率,batch=16基本能放下,如果换yolov8s-seg或更大模型,batch得往下压。
cache=True会把数据集预加载到内存,几万张图时能大幅缩短读取时间。注意如果图片总量超过内存,不要硬开,会直接把机器拖死。patience=15是早停耐心值,也就是连续15个epoch验证mAP没有提升就提前结束。水下数据集噪声大,损失曲线经常抖动,patience设太小容易在真正收敛前被叫停。
这里需要补充一句关于yolo26和实例分割生态的说明:YOLO系列从v8到更新的版本,训练入口和标签格式保持稳定,选择实例分割还是语义分割只影响输出头的配置,不改变数据集txt结构。因此第3章的转换脚本,对后续版本依然可用。
4.3 训练完成后先做可视化验证
训练结束先别急着看mAP数字,先用模型在验证集上跑一次推理,把预测mask叠加到原图上,肉眼看几条鱼。
yolo segment val \ model=run/fish_seg/exp1/weights/best.pt \ data=fish_seg.yaml \ plots=True \ save_json=Trueplots=True会生成预测图和混淆矩阵,保存到run/fish_seg/exp1/下。save_json=True会输出一个包含每张图预测结果的JSON文件,之后做统计分析和mask面积比对时非常有用。打开生成的验证图,重点看三类情况:小鱼是否被漏检,鱼身轮廓是否贴着真实边缘,两条鱼紧贴时是否被合并成一个mask。这三类问题靠mAP难以直接反映,但肉眼看图几秒钟就能判断。
如果轮廓有轻微锯齿,不用急着改标注,先调整验证时的conf阈值和iou阈值。若mAP数值正常但mask边缘飘,大概率要回第3章检查epsilon参数。要是基本轮廓都对得上,再进入后面的细分优化。
5. 水下鱼类实例分割训练避坑:五个常见问题的现象、原因与对策
5.1 现象一:训练Loss稳定下降,验证mAP却高频震荡
这在水下场景里太常见了。现象是Training loss一路走低,甚至降到0.01以下,验证mAP却每隔几个epoch大幅摆动,最终收敛值很低。原因在于水下光照和色温随深度变化剧烈,训练集里如果混入了多个拍摄时段的数据,模型学到的颜色统计特征并不稳定,在验证集上表现飘忽。解决方法是先做一层简单的色彩归一化,把图像的RGB均值拉到相近水平,再从数据增强里引入亮度、对比度扰动,让模型不能依赖单一色偏。同时把学习率调低一档,比如把lr0从0.01改成0.001,并适当加大patience。
5.2 现象二:分割mask边缘锯齿明显,轮廓“整圈毛边”
推理图上mask边缘像毛刺一样,局部凹凸不平。原因有三层:原始标注就不够精细,鳍部轮廓勾得很粗;3.1节里approxPolyDP的容差设得太大,把真实细节抹平了;训练时输入尺寸被下采样,高分辨率边缘信息丢失。解决优先级从低到高:先把imgsz从640提到768,看边缘是否改善;再把eps_ratio降到0.0005,重新转换数据集;最后对少数边缘极差样本做人工修补。如果前两步做完仍严重,问题大概率在标注本身,不用纠结模型参数。
5.3 现象三:类别不均衡,少数类被多数类“吃掉”
水下拍摄天然不均衡,某种鱼容易被拍到,实例数占绝大多数,其他种类样本极少。现象是训练完成后,少数类几乎不出现在预测结果里,mAP对多数类虚高。解决的关键在数据层面,不要在模型层面硬调loss。我一般先按第2.3节统计分布,对数量不足的类别做水平翻转、轻度旋转、尺度抖动等增强;仍不够就把该类别的实例复制到训练集,或者用类别权重给损失函数加权。类别重采样是这里性价比最高的手段。
5.4 现象四:小鱼群漏检,尤其是远离镜头的个体
鱼群画面里,近处的大鱼分割得很干净,远处的小鱼直接丢。原因是imgsz=640下采样后,小鱼可能只占几个像素,特征提不出来。解决思路按代价从低到高排列:先提高输入分辨率,最常见的做法是把imgsz升到768甚至960;再对大图做tile切分,把原图切成几块分别推理,最后按坐标拼接结果。后者会增加推理耗时,但水下小目标场景里往往比换大模型更有效。
5.5 现象五:两条鱼重叠时,mask被合并成一个整体
观测结果是两只鱼紧贴游动时,模型输出的mask覆盖了两只鱼连成一片的区域,无法分开。原因是实例分割头在两个实例交叠区域得分接近,后处理里对重叠mask的抑制力度不足。解决方法是调整验证和推理时的NMS阈值,在ultralytics里对应nms_iou参数。默认0.7在通用场景好用,水下紧密遮蔽时降到0.5附近,能明显减少合并。同时检查转换后的标签里是不是存在“两个实例共享同一区域”的情况,这种标注本身会教坏模型,发现后直接修数据。
这五条坑贯穿数据、转换、训练、推理四个环节,没有一条是纯模型结构的锅。先排数据链路,再动训练参数,是处理水下鱼类实例分割最稳妥的路线。血泪经验是:遇到mAP不涨,先怀疑数据,不要急着换大模型。
6. 跑通之后的验证技巧:mask面积直方图与混浊水体推理微调
训练出了best.pt,第一件值得做的事不是反复横跳调参,而是用验证集预测结果做一次mask面积统计。理由很简单,mAP是综合指标,掩盖了模型在“偏大”还是“偏小”上的系统性偏差,而mask面积直方图能直观看出来。
用第4.3节生成的JSON预测文件,做一个面积比统计:
# mask_area_ratio.py import json import numpy as np with open("run/fish_seg/exp1/predictions.json", "r") as f: preds = json.load(f) ratios = [] for p in preds: # 用bbox面积近似,或用mask像素数 pred_area = p["bbox"][2] * p["bbox"][3] true_area = p["true_bbox"][2] * p["true_bbox"][3] if true_area > 0: ratios.append(pred_area / true_area) print("面积比中位数:", round(np.median(ratios), 3)) print("面积比P10:", round(np.percentile(ratios, 10), 3)) print("面积比P90:", round(np.percentile(ratios, 90), 3))中位数如果低于0.8,说明模型预测的mask系统性偏小,鱼鳍边缘丢得严重,这时应回查标注边缘质量和imgsz设置。P10到P90的跨度如果过大,说明模型在部分遮挡、低对比度样本上极不稳定,优先处理最差的10%。这样一刀,比自己眼扫几百张图靠谱得多。
混浊水体推理时还有一个微调习惯:不要盲目提升对比度。水下图像增强常把蓝绿色通道拉平,但对实例分割模型而言,它学习的边缘特征来自亮度梯度,暴力增强会制造假边缘,反而增加mask抖动。我在推理阶段只会做轻度直方图均衡,confidence阈值保持在0.25左右,不再往高调,给低可见度鱼留出召回空间。
这个面积比统计方法,我几乎每次在新数据集上跑出模型之后都会立刻执行,因为mAP只是一个数字,面积分布才暴露模型真正学会了多少轮廓。希望帮到你。
本文还有配套的精品资源,点击获取