水下鱼类实例分割:从数据集到YOLOv8-seg训练实战
发布时间:2026/10/2 2:51:40来源:尧图网络
简介这份水下鱼类实例分割数据集面向目标检测与实例分割开发者、水产养殖与海洋生态研究人员提供1274张真实水下场景图像覆盖7种鱼类及1个通用物体类别可用于鱼类识别、种群计数、体态分析、养殖监测等任务。包内共2000个文件以1274个YOLO格式txt标注文件、724张jpg原图为主另含1个yaml配置文件和1份docx说明文档压缩包约61.17MB所有标注均采用多边形点精确勾勒鱼类轮廓可直接接入YOLOv8-Seg、Mask R-CNN等主流分割框架无需额外转换。数据集已按训练901张、验证250张、测试123张划分方便直接训练与评估同时涵盖光照变化、水体浑浊等真实水下挑战有助于提升模型在复杂环境中的鲁棒性。目前已有167人学习浏览适合需要高精度分割基准数据、开展水产养殖监测或海洋生态研究的开发者使用。1. 水下鱼类实例分割数据集先想清楚它用来解决什么再点下载这份水下鱼类实例分割数据集解决的是“检测框说不清”的问题。做水下生态监测、渔业资源调查或者养殖智能监控时鱼经常扎堆游、互相遮挡目标检测给出的矩形框要么把两条鱼圈在一起要么只框住半条尾巴统计数量时误差很大。实例分割比检测框多输出一层像素级轮廓能把每条鱼单独抠出来之后做尺寸估算、密度统计、个体追踪都方便得多。这个 zip 资源解压后就是图片和标签配套的标准目录结构标签按 YOLO 的实例分割格式组织可以直接喂给 YOLOv8-seg 训练不用再花几周去水下拍摄和人工标注。适合正在做生态环境目标检测、水下机器人视觉的工程师也适合刚接触 YOLO 实例分割、想找一份现成数据集跑通全流程的新手。2. 解压后先理清目录与标注COCO 标签转 YOLO-seg 的格式转换说到实例分割得先区分一个概念实例分割和语义分割不是一回事。语义分割把画面里所有“鱼”的像素都标成一类不区分个体实例分割则要求把每条鱼分开哪怕是两条紧紧贴在一起的同类鱼也要输出两个独立 mask。这份数据集的标注目标是后者所以标签里存的不是类别热图而是每条鱼的轮廓坐标。理解这一点很重要因为后面所有格式转换和训练参数都是围绕“实例”这两个字展开的。拿到 zip 之后不要急着训练先把标注格式和目录结构摸透否则后面跑出来的模型会莫名其妙地漏检或糊边。2.1 目录布局images 与 labels 的对应关系解压后通常能看到 images 和 labels 两个顶层目录各自下面按 train 和 val 划分再附带一个类别文件和一个供 YOLO 读取的配置文件。首次解压后的第一动作我先用命令把目录层级打出来确认文件组织方式而不是一上来就翻图片。unzip 水下鱼类实例分割数据集.zip -d fish_seg_dataset cd fish_seg_dataset find . -maxdepth 2 -type d | sortunzip -d指定解压目录避免 zip 内文件散落一地find只列出目录层用来确认 train 和 val 的划分是否齐整。常见目录结构如下fish_seg_dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ ├── classes.txt └── data.yaml这里的关键对应关系是images/train/0001.jpg和labels/train/0001.txt必须同名YOLO 训练时按图片名找同名 txt对不上就报 FileNotFoundError。labels 目录里的 txt 不是空的每一行代表一条鱼的实例分割数据。如果发现某个目录缺文件先别急着删很可能是解压时路径嵌套了比如多了一层文件夹。路径作用images/train训练图片JPG 或 PNG 格式images/val验证图片用于评估模型泛化能力labels/train与训练图片同名的分割标签 txtlabels/val与验证图片同名的分割标签 txtclasses.txt类别名称一行一个顺序对应标签里的 class_iddata.yaml供 YOLO 读取的路径和类别配置2.2 标注格式解析从多边形顶点到归一化坐标YOLO 实例分割的标签文件与检测标签同宗同源但格式更复杂。检测标签每行只有class_id x_center y_center width height五个数字而分割标签每行是class_id x1 y1 x2 y2 ... xn yn(x_i, y_i)是这条鱼轮廓多边形的一组顶点顶点数量不固定一条轮廓密集的鱼可能有几十个点稀疏的十几个。所有坐标都归一化到[0, 1]x 除以图片宽度y 除以图片高度。0 0.5210 0.4735 0.5342 0.4801 0.5489 0.4912 ...这个文件里如果有一个坐标落到了 1.0 之外说明原始标注或转换脚本里坐标归一化出了问题训练时的 mask 损失会异常高但检测损失看着正常。这也是为什么我强调先打开一个 txt 目测一下坐标范围——这种一眼能发现的问题不值得花一整天去调模型。如果这份资源解压后是 COCO 格式的annotations.json那就要做一次格式转换。COCO 的segmentation字段存储的是绝对像素坐标的 polygon或者 RLE 压缩编码YOLO-seg 需要的是归一化后的多边形点序列。转换时最容易被忽略的坑是 COCO 的category_id不一定从 0 连续递增而 YOLO 的 class_id 必须从 0 开始否则类别对不上。2.3 转换脚本COCO json 到 YOLO-seg txt 的落地实现我一般会用下面这个 Python 脚本做转换。它接收 COCO 的 json 路径和输出目录把每条标注写成一个独立的 txt 文件。import json from pathlib import Path def coco_to_yolo_seg(json_path, out_dir): with open(json_path, r, encodingutf-8) as f: coco json.load(f) # COCO 的 category_id 不连续这里重排成 0..N-1 cat_map {} for new_id, cat in enumerate(coco[categories]): cat_map[cat[id]] new_id class_names [cat[name] for cat in coco[categories]] # 建立 image_id 到图片信息的索引 img_map {img[id]: img for img in coco[images]} out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) for ann in coco[annotations]: img img_map[ann[image_id]] img_w, img_h img[width], img[height] seg ann[segmentation] # COCO 的 segmentation 可能是 polygon 列表也可能是 RLE 字典 if isinstance(seg, dict): from pycocotools import mask as mask_utils rle mask_utils.frPyObjects(seg, img_h, img_w) mask mask_utils.decode(rle) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polygons [c.flatten().tolist() for c in contours] else: polygons seg txt_name Path(img[file_name]).stem .txt with open(out_dir / txt_name, w) as out: for poly in polygons: # poly 是 [x1,y1,x2,y2,...] 的扁平列表 pts [] for i in range(0, len(poly), 2): x min(max(poly[i] / img_w, 0), 1) y min(max(poly[i 1] / img_h, 0), 1) pts.append(f{x:.6f} {y:.6f}) out.write(f{cat_map[ann[category_id]]} .join(pts) \n) # 顺带把类别名写进 classes.txt with open(out_dir.parent / classes.txt, w) as f: f.write(\n.join(class_names))这个脚本有三个关键点。一是类别映射用cat_map把原始category_id重排成从 0 开始的连续编号这一步漏了的话后续训练出来的结果类别全乱。二是坐标归一化除以图片宽高后再用min(max())做一次边界裁剪防止标注点越界——水下标注时偶尔会有超出图片边缘的轮廓点不裁的话 YOLO 在解析标签时会直接报错。三是 RLE 解码遇到 RLE 格式的 segmentation用 pycocotools 解码成二值 mask再用 OpenCV 的findContours提取外轮廓。这里注意RETR_EXTERNAL只取最外层轮廓如果鱼身上有花纹导致标注里出现内部孔洞这个参数可以避免生成重叠的轮廓点。3. YOLOv8-seg 训练水下鱼类分割模型从数据划分到参数逐项拆解数据格式确认无误后就进入训练阶段。YOLOv8-seg 是目前实例分割里对这类标准 YOLO 格式数据集支持最顺手的模型训练命令和检测版本几乎一致只是模型权重换成了-seg后缀。这一章从环境准备到训练参数实测按实际执行的顺序写。3.1 环境准备与数据划分训练环境只需要 ultralytics 一个包它会连带把 torch、opencv 等依赖装好。建议用 Python 3.9 以上的虚拟环境避免和系统环境互相污染。pip install ultralytics装完之后先把数据按 9:1 划分成训练集和验证集。如果这份 zip 里已经分好了images/train和images/val这步可以跳过但如果没有用下面这个脚本自动划分省得手动拖文件。from pathlib import Path import random, shutil random.seed(42) src Path(fish_seg_dataset/images) train_dir src / train val_dir src / val train_dir.mkdir(exist_okTrue) val_dir.mkdir(exist_okTrue) all_imgs list(src.glob(*.jpg)) list(src.glob(*.png)) random.shuffle(all_imgs) val_cnt int(len(all_imgs) * 0.1) for img in all_imgs[:val_cnt]: shutil.move(str(img), str(val_dir / img.name)) for img in all_imgs[val_cnt:]: shutil.move(str(img), str(train_dir / img.name))划分脚本的逻辑很直接把图片打乱前 10% 进 val其余进 train。固定random.seed(42)是为了结果可复现不然每次跑数据分布都不一样实验对比没有意义。这里只移动了图片labels 目录要手动按同名文件对应移动我一般用一个小循环补上for f in labels/*.txt; do n$(basename $f) if [ -f images/val/${n%.txt}.jpg ]; then mv $f labels/val/ else mv $f labels/train/ fi done3.2 data.yaml路径与类别配置训练前还要写一个 data.yaml。YOLO 训练时会从这个文件里读取图片路径和类别名称列表。path: /absolute/path/to/fish_seg_dataset train: images/train val: images/val names: 0: seabass 1: snapper 2: grouper 3: salmon 4: tunapath建议写绝对路径因为 YOLO 在训练时会基于这个字段拼接 train 和 val 路径。我踩过好几次相对路径的坑——在某个子目录下启动训练时数据路径没有正确吃到./前缀直接报找不到图片。names列表的顺序必须和 labels 里 class_id 一一对应也就是说classes.txt第一行对应 id 0第二行对应 id 1以此类推。提示解压后如果自带data.yaml也别直接开跑先检查names数量和classes.txt行数是否一致。类别数量不匹配时模型权重最后一层的输出维度会报错这个错误信息不太直观容易误判成 CUDA 问题。3.3 训练命令与关键超参数数据就绪后用yolo segment train启动训练。以 YOLOv8n-seg 为例yolo segment train \ datafish_seg_dataset/data.yaml \ modelyolov8n-seg.pt \ epochs200 \ imgsz640 \ batch16 \ lr00.01这里面几个参数直接决定训练效果和显存占用参数取值建议说明modelyolov8n/s/m/l-seg.ptn 最快但精度低m 是折中选择。先拿 n 跑通流程再换大模型提精度imgsz640水下小鱼目标多时建议 960输入分辨率越大小目标保留得越好但显存翻倍batch显存决定8G 显存跑 n 模型用 16换 l 模型必须降到 4 以下lr00.01迁移学习场景常用 0.01数据量太小时降到 0.001 更稳epochs200看 val loss 是否收敛100 轮后没有下降趋势就尽早停训练日志里每个 epoch 会输出box_loss、seg_loss、cls_loss、dfl_loss四项。这里最值得盯的是seg_loss——它是实例分割 mask 的 loss这个值不降说明模型根本没学会拟合鱼的轮廓。如果你发现检测部分的 loss 很低但 seg_loss 一直横盘多半不是模型问题而是标签格式不干净比如多边形顶点坐标没归一化、或者一条鱼出现了两个互相重叠的多边形标注。另外一个容易被忽略的参数是mask_ratio它控制训练时下采样 mask 的比例默认是 4也就是把 mask 压缩四倍再算损失。对鱼这种轮廓细节多的小目标我一般建议调成 2代价是显存占用和训练时间增加约三成但 mask 边缘的锯齿会明显减少。4. 验证指标与部署mAP 之外还要看 mask 贴合度和推理速度训练完成后验证和部署环节有自己的一套判断标准。实例分割的评估比检测多了一个维度除了框得准不准还要看轮廓贴不贴合。这里面的经验和踩坑点值得单独说。4.1 评估指标box AP 与 mask APyolo segment val会输出两组指标一组是检测框的 mAP一组是 mask 的 mAP。两者都有 mAP50 和 mAP50-95 两个版本mAP50 指的是 IoU 阈值固定在 0.5mAP50-95 是 IoU 从 0.5 到 0.95 每隔 0.05 算一次再取平均。对实例分割来说mask AP 比 box AP 更有参考价值因为 mask 的 IoU 计算要用到逐像素的掩码对齐。yolo segment val \ datafish_seg_dataset/data.yaml \ modelruns/segment/train/weights/best.pt跑完之后看输出表格。我一般关注三个数mask_mAP50、mask_mAP50-95和每张图的推理耗时。水下鱼类场景里mAP50 达到 0.8 以上基本可用mAP50-95 则受轮廓精细度影响很大常常比 box 的对应指标低 5 到 10 个百分点这是正常的——轮廓预测比框预测更难不用焦虑。指标关注点box mAP50目标有没有被准确定位mask mAP50轮廓主体是否咬合正确mask mAP50-95轮廓边界质量数值低说明边缘粗糙单图推理耗时部署可行性实时场景要求更高4.2 导出 INT8/ONNX 与推理验证验证指标过得去下一步就是部署。YOLOv8-seg 可以导出成 ONNX方便用 TensorRT 或 OpenVINO 做推理加速。yolo export modelruns/segment/train/weights/best.pt formatonnx opset12导出后用 Python 脚本加载 ONNX 模型直接对单张图片做推理验证导出的模型和 PyTorch 模型输出是否一致。from ultralytics import YOLO import cv2 model YOLO(best.onnx) img cv2.imread(test_underwater.jpg) results model(img) for r in results: boxes r.boxes.xyxy.cpu().numpy() masks r.masks.data.cpu().numpy() for i, box in enumerate(boxes): x1, y1, x2, y2 box.astype(int) mask masks[i] # 按 mask 阈值二值化再做连通域过滤 mask_bin (mask 0.5).astype(uint8) cv2.drawContours(img, [cv2.findContours( mask_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0][0]], -1, (0, 255, 0), 2)推理结果的masks.data是[N, H, W]的浮点数组每个通道对应一条鱼的分割概率图。直接用它画轮廓会出现大量毛刺通常先做阈值二值化再用findContours提取轮廓来画。这里的0.5是置信阈值调低一点轮廓会大一圈调高则偏保守对粘连场景作用明显。提示导出 ONNX 后一定要跑一次对比。PyTorch 模型和 ONNX 模型对同一张图的输出 IoU 如果低于 0.98基本可以断定导出时 opset 不匹配或者输入尺寸和训练时不一致。这种情况在稀疏小目标的水下场景里尤其容易出现。5. 避坑指南zip 伪加密、mask 空洞和低召回率的实战排查记录这个领域值得单独拉出来写。说几个我在处理这份资源和同类水下数据集时真实踩过的坑按现象到解决完整走一遍。5.1 数据解压与校验阶段记录 1zip 解压提示“文件头部损坏需要密码”现象用右键快速解压报错提示压条包被加密要求输入密码。但这份资源本来就该免密解压。原因zip 文件存在“伪加密”现象本地文件头的通用标志位里加密标记位被置 1但文件数据本身并没有加密。这是 zip 规范里的一个标志位某些打包工具或者传输过程会把标志位写掉。解决不用找密码先用支持 zip 修复的工具把标志位修正再用 Python 快速处理import struct with open(bad.zip, rb) as f: data bytearray(f.read()) # zip 本地文件头签名是 PK\x03\x04进入循环修正所有文件头 offset 0 while offset len(data) - 4: if data[offset:offset4] bPK\x03\x04: flag_offset offset 6 flag struct.unpack(H, data[flag_offset:flag_offset2])[0] # bit0 1 表示加密这里强制清零 if flag 0x0001: flag ~0x0001 data[flag_offset:flag_offset2] struct.pack(H, flag) offset 30 # 跳过本地文件头固定部分 else: offset 1 with open(fixed.zip, wb) as f: f.write(data)原理是为每个本地文件头定位加密标志位并清零。zip 文件头里第 6 到 7 字节是通用标志位最低位为 1 代表加密。这类伪加密不修改数据区所以直接改标志位就能解压。记录 2解压后图片能打开但训练时读取图片报损坏现象反复裁剪标注后训练发现有一部分图片在 OCR 阶段直接崩掉cv2.imread 返回 None。原因压缩包传输不完整或解压工具跳过错误继续解压导致个别 jpg 文件尾部数据缺失但文件头完整导致图片能显示缩略图但无法被完整解码。解决解压后先跑一轮校验对所有图片重新编码一遍损坏的直接挑出来重新下载。用一个小脚本批量校验import cv2, glob for path in glob.glob(fish_seg_dataset/**/*.jpg, recursiveTrue): img cv2.imread(path) if img is None: print(corrupted:, path) # 从原始包重新解压这一个文件或者记录文件名申请补发5.2 训练与推理阶段记录 3seg_loss 不降拟合轮廓粗糙现象训练 100 轮后检测指标已经不错但 mask 边界坑坑洼洼seg_loss 不再下降。原因训练时mask_ratio默认 4 的时候mask 被压缩到 1/4 尺寸轮廓细节信息丢失另一个原因是标注多边形顶点过密模型学习到的是噪点。解决把mask_ratio改为 2并在转标签时对顶点数做简化用 Douglas-Peucker 算法把轮廓点控制在 16 到 32 个之间点数适中比点数多更容易拟合。记录 4小鱼漏检严重mAP50 低现象模型对成鱼识别效果可以但鱼苗这类小目标基本不输出 mask。原因imgsz640对小鱼来说目标只有十几个像素经过模型下采样小目标的语义特征几乎消失就算有也用不上。解决先统计训练集中目标框的平均尺寸如果大部分目标小于 32×32直接把imgsz提到 960并开启augmentTrue让模型在训练时通过随机裁剪增强小目标。记录 5显存溢出OOM 崩溃现象batch 设为 16 时训练正常换用yolov8m-seg.pt后直接 CUDA out of memory。原因Mask head 比检测 head 多输出一层高分辨率特征显存占用比检测模型大得多大模型 大 batch 一定会爆显存。解决把 batch 降到 4开启梯度累积到一个等效 stepyolo segment train ... batch4同时在训练命令里加accumulate4网络每一步只处理 4 张图但梯度累积 4 次再更新等效于 batch16代价是训练时间变长但可以稳定跑大模型。6. 进阶水下图像预处理与增强参数让分割结果再稳一个档次水下图像有典型的偏色问题水体会吸收红光所以拍出来的画面普遍偏蓝绿对比度低远处还有严重的雾化散射。直接用原始数据训练不是不行但分割边界会不稳定特别是鱼体和背景颜色相近的鱼种。我这里给一套稳定的预处理加增强方案专门应对这个场景。6.1 水下偏色的预处理链我会先在推理阶段做白平衡加 CLAHE而不是训练阶段。原因很简单保持训练数据的原始分布模型学到的特征才真实推理前预处理相当于给模型一个更干净的输入。import cv2 import numpy as np def underwater_enhance(img): # 灰度世界白平衡偏蓝绿的区域会被拉回中性色 lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) a a - a.mean() 128 b b - b.mean() 128 # CLAHE 只对亮度通道增强避免色彩失真 clahe cv2.createCLAHE(clipLimit2.5, tileGridSize(8, 8)) l clahe.apply(l) enhanced cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR) return enhancedclipLimit2.5控制对比度增强的程度太大容易把水体噪声也放大tileGridSize8是局部区域大小对边缘细节保留有帮助。这条链路在推理时逐帧调用对于养殖监控和机器人视觉都适用。6.2 训练增强参数的两个调整习惯在训练增强参数里有两个针对水下场景的调整。第一把flipud关掉水下鱼总有一个重力方向上下翻转后模型会把“肚皮朝上”当作训练样本推理时遇到正常姿态反而犹豫。第二把hsv_h调高一些到 0.02水下光线随深度变化色相的小幅扰动可以让模型对水色变化不敏感。yolo segment train ... \ flipud0.0 \ hsv_h0.02 \ hsv_s0.7flipud0.0设置了上下翻转的概率为 0hsv_h0.02表示色相扰动范围是 2%水下蓝绿光的色相变化远大于这个值但调太大会把鱼的天然颜色模型学歪这需要实际验证。我现在每次训练水下分割模型之前都会强制先跑一遍“预处理增强对比”——同一份 val 数据跑一遍裸推理和一遍增强推理看 mAP50 的差距是否在合理范围。这个习惯已经帮我避掉好几次“肉眼看着挺清楚、模型一上测试集就翻车”的情况。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网