水下鱼类实例分割实战:从COCO数据集到YOLOv8训练全流程
简介在计算机视觉领域目标检测与实例分割是图像理解的两大核心任务。目标检测通过矩形框定位物体而实例分割则进一步输出像素级轮廓尤其适合密集、遮挡严重的场景。水下环境由于光吸收与散射图像普遍存在偏色、低对比度等问题常规检测模型难以胜任因此需要针对性的数据集与训练策略。实例分割技术可支撑个体计数、体长估计与生物量评估等下游任务在渔业监测与水下机器人领域有重要价值。本文从数据完整性校验出发详述COCO标注转YOLO格式的方法并结合YOLOv8实例分割模型探讨密集鱼群场景下的训练参数调整、数据增强与推理优化。通过一套完整的水下鱼类实例分割数据集实践为相关开发者提供可复用的工程经验。 做水下视觉的同行大概都有过这种体验目标检测模型在陆地上跑得好好一到水下面对一群密集游动的鱼就把你打回原形。我用过不少目标检测方案框和框叠在一起别说数清个体有时候连一条鱼的边界都分不出来。后来我把方案切到实例分割找到了一份标注好的水下鱼类实例分割数据集才把训练链路真正跑通。这篇就是我从解压数据到训练YOLOv8实例分割模型的全过程记录包括文件完整性排查、COCO标注转YOLO格式、训练参数调整以及密集鱼群场景下的优化经验适合正在做水下目标识别、打算用YOLOv8训练自定义分割数据集的朋友参考。1. 水下鱼类实例分割这份数据集切入的痛点1.1 为什么必须用实例分割从数不清的鱼说起先说一个反面案例。我最早参与的一个水产养殖监测项目用的还是普通的目标检测模型。模型在单条鱼的画面上表现不错一旦到了真实的鱼群场景密集目标互相遮挡、粘连边界框算法瞬间失灵。要么一个框里框进三条鱼要么一条鱼被框切成两段最后统计出来的数量连参考价值都没有。更麻烦的是下游还要根据鱼的轮廓估算体长光靠矩形框根本没法做。实例分割要解决的问题就是这里有几条鱼每条鱼具体占据哪些像素。它会在图像上给出每个鱼个体的像素级轮廓掩码mask而不是一个粗糙的矩形框。水下鱼类实例分割数据集这类资源就是专门服务于这个任务的。它把原始水下图像逐张筛选、切分、去重再用多边形逐实例标注出鱼的轮廓。有了这些轮廓后续的个体计数、体长估计、轨迹跟踪甚至生物量评估才真正有得做。1.2 水下场景让常规检测模型失效的原因很多人拿到水下数据集习惯性地直接套用自然场景目标检测模型结果发现效果远不如预期。这不是模型不行而是水下成像的分布和常规训练数据差得太远。光在水下的传播有两条路吸收和散射。红光波长长的部分先被水吸收所以水下图像普遍偏蓝绿颜色分布和COCO这些自然图像数据集几乎不重合。模型从自然图像迁移过来的颜色特征在水下场景经常不适用。散射则造成低对比度鱼的边缘和周围背景是糊在一起的再加上不少浅水鱼类的保护色和礁石背景很像模型很容易把背景纹理误判成鱼或者把鱼的轮廓切到背景里去。另外鱼群是动的。快速游动产生的运动模糊会让轮廓在帧间变得残缺水面的波光、水下机器人的补光灯又会造成局部过曝和阴影。这些噪声叠加起来普通检测模型在水下场景出现漏检、误检几乎是必然的。所以一份专门收集并标注的水下鱼类实例分割数据集它的价值不只是省去标注时间更关键的是把水下成像的这些分布特征固化在数据里。你不需要在训练阶段做大量人工预处理数据本身已经替你表达了水下环境的真实分布。1.3 资源包里通常有什么这类zip解压出来的内容常见结构是images目录和annotations目录配套出现有的会直接按train、val、test分好。我拿到的版本是images目录下放jpg或png原图annotations目录里有一个COCO格式的JSON文件另外附一份classes.txt。不同发布者对标注格式的偏好差别很大有的给COCO多边形坐标有的给YOLO的seg txt有的直接给RLE掩码。遇到RLE要注意它是针对crowd紧密人群/鱼群区域的压缩编码和普通多边形坐标完全是两种数据结构。拿到数据后第一件事永远是读README或classes.txt搞清楚标注字段和类别顺序而不是急着开train。类别顺序尤其重要如果发布者的classes.txt和标注文件里的id对不上你后面所有转换都会错位。这个坑我在别的数据集上踩过所以多说一句。2. 解压即用先处理zip完整性这个拦路虎数据集下载最糟心的不是训练效果差而是文件本身打不开。我第一次处理这份水下鱼类数据集时解压就遇到了报错系统提示file is not a zip file再细看是could not find EOCD。2.1 could not find EOCD到底在说什么ZIP文件的结构可以粗略分成四个部分文件头、压缩数据区、中央目录Central Directory、末尾的中央目录记录End of Central Directory简称EOCD。EOCD是zip文件最尾部的索引信息解压工具靠它来定位前面所有压缩条目。如果报错说找不到EOCD意思就是整个zip文件的结尾索引缺失了。最常见原因是下载不完整文件传输过程中被截断尾部恰好丢了也可能是跨平台复制时用了不靠谱的传输方式比如U盘拔早了或者云盘同步只同步了部分文件。报这个错通常说明文件已经损坏硬解压出来的内容大概率有缺失。遇到这种情况先冷静下来做两步检查。第一步看文件的字节数和发布页面标注的大小是否一致差得很大就直接重新下载。第二步是核对校验和SHA256发布方一般会给一个哈希值用工具算一下本地文件的哈希对不上就删掉重来。不要抱着侥幸心理去改后缀或者用第三方工具硬解损坏的zip即使解出来图片和标注JSON也往往是不完整的。2.2 Linux下校验、修复与解压的完整操作我习惯在Linux环境里处理这类数据用的是一套固定的操作流程。先把文件大小和哈希核对好再测试zip完整性最后才真正解压。ls -lh 水下鱼类实例分割数据集.zip sha256sum 水下鱼类实例分割数据集.zip # 测试zip完整性最后一行显示 No errors detected 才算完整 unzip -t 水下鱼类实例分割数据集.zip # 通过测试后解压到指定目录 unzip 水下鱼类实例分割数据集.zip -d 水下鱼类数据集如果不想记忆这些命令行参数也可以用Python内置的zipfile模块做完整性检查命令更短python -m zipfile -t 水下鱼类实例分割数据集.zip输出OK才算通过。如果zip文件确实坏了但还差一点可以用zip -FF尝试修复zip -FF 水下鱼类实例分割数据集.zip --out 修复后的数据集.zip不过实测下来zip -FF的修复成功率堪忧。它只能修复结构仍完整的zip如果文件是被直接截断的EOCD都没了修复工具也巧妇难为无米之炊。所以最稳妥的方案还是重新下载或者让发布者重新打包传一次。再说个容易被忽视的小点如果发布者给压缩包设置了密码正常的做法是找发布者要别去用来路不明的移除zip密码工具。这类工具大概率捆绑恶意程序为了一份数据集去冒机器中毒的风险完全不值得。2.3 解压后第一时间核对数据与标注条目数解压成功不等于数据能用。我就吃过这种亏解压完直接进入训练环节训练到一半才发现某些类别的AP全部为0查来查去才发现是数据本身缺了标注。所以解压完成后我会立刻写个小脚本统计图片数量和标注实例数量是否匹配。import json import os ann json.load(open(annotations/instances_train.json)) print(JSON中图片数量:, len(ann[images])) print(JSON中标注实例数量:, len(ann[annotations])) print(类别列表:, [(c[id], c[name]) for c in ann[categories]]) print(images目录实际图片数量:, len(os.listdir(images/train)))这段脚本会显示几个核心数字。如果图片目录里有500张图JSON里的images应该有500条记录annotations只会更多而不会更少。如果annotations数量明显偏少说明打包时数据切分有问题或者存在大量空标注。这种低级问题在训练初期几乎发现不了直到你觉得训练集挺大但模型一直学不进去的时候回头查才能揪出来。所以这个核对环节建议一次都不要省。3. 标注格式转换COCO多边形与YOLO分割txt的互转3.1 COCO实例分割标注的字段拆解COCO格式是实例分割领域最通用的数据格式这份数据集如果提供的是COCO JSON那我的工作流就从这里开始。COCO JSON的核心结构是三个数组images、annotations、categories。images数组里每条记录对应一张图包含id、file_name、width、height这几个字段。categories记录类别信息至少包含id和name。annotations是重点每条记录对应一个实例通常包含image_id这个实例属于哪张图category_id属于哪个类别bbox外接矩形框格式是[x, y, width, height]area掩码面积segmentation掩码的具体坐标描述iscrowd是否为拥挤区域标注关键在于segmentation字段。它有几种写法一种是多边形坐标集合segmentation是一个列表每个元素是某个连通区域的坐标序列[x1, y1, x2, y2, ...]另一种是RLE编码通常出现在iscrowd1的样本里用于密集聚集的鱼群或人群。多边形转成RLE相对容易反过来要把RLE解码成多边形就比较麻烦。我处理水下鱼类数据时polygon类型的标注是主体但拥挤鱼群区域会出现RLE。如果你打算用YOLO系列的实例分割模型需要把这两类都处理好否则crowd区域的监督信号会全部丢失对密集场景的效果影响很大。3.2 COCO转YOLO的脚本实现YOLO的实例分割标注格式很简洁每个txt文件对应一张图每一行代表一个实例格式是class_id x1 y1 x2 y2 ... xn yn坐标值全部归一化到0到1之间。有一个容易踩的坑是YOLO只认单个多边形。如果COCO标注里一个实例有多个多边形比如鱼被水草部分遮挡标注被拆成了几块直接转换会导致训练时mask形状错乱。我的处理方式是先合并成最大外轮廓或者选择面积最大的那个连通区域确保每个实例只剩一个多边形。下面这段脚本可以直接改用来做转换注意它只是基础版本遇到RLE会跳过import json import os def convert_coco_to_yolo(coco_json, output_dir): os.makedirs(output_dir, exist_okTrue) with open(coco_json) as f: data json.load(f) cat_id_to_idx {c[id]: i for i, c in enumerate(data[categories])} img_id_to_info {img[id]: img for img in data[images]} anns_by_img {} for ann in data[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) for img_id, anns in anns_by_img.items(): img img_id_to_info[img_id] w, h img[width], img[height] out_lines [] for ann in anns: cat_idx cat_id_to_idx[ann[category_id]] seg ann[segmentation] # RLE编码跳过实际使用需要先解码成多边形 if isinstance(seg, dict): continue for polygon in seg: pts [(polygon[i] / w, polygon[i1] / h) for i in range(0, len(polygon), 2)] line [str(cat_idx)] for p in pts: line.append(f{p[0]:.6f}) line.append(f{p[1]:.6f}) out_lines.append( .join(line)) base os.path.splitext(img[file_name])[0] out_path os.path.join(output_dir, base .txt) with open(out_path, w) as f: f.write(\n.join(out_lines))实际工程里RLE样本不能无脑丢。更完整的做法是用pycocotools把RLE解码成mask再把mask转成多边形。流程是mask coco.annToMask(ann)然后用maskToPolygons之类的工具把单个连通域提取出来。鱼群crowd区域的标注如果全丢了密集场景的mask AP会明显下滑所以如果你这份数据集的crowd占比高建议把RLE转换这块补齐。3.3 转换后可视化验证最容易被跳过的环节转换完直接开train是大忌。YOLO的txt文件里全是纯数字没有人眼可读性坐标写错位了光看文本根本发现不了。我见过不少同行转换完就丢进训练结果mAP一直上不去最后才发现是坐标归一化出了问题。我的流程是转换后立刻写一个可视化脚本读原图把txt里的坐标乘回图片宽高用cv2.polylines画轮廓再叠上类别标签生成对比图人工抽查。抽个20到30张图重点看轮廓是否贴合鱼身有没有坐标跑到图外的异常情况。这一步虽然多花二十分钟但几乎每次都能发现问题。我这次转换时就遇到过某几张图的JSON里记录的图片宽高和实际JPEG宽高不一致归一化坐标整体歪掉。这种错误要是潜伏到训练集里模型会在部分样本上学到错误的轮廓映射并且很难排查。4. 用YOLOv8在自定义鱼类数据集上跑通训练4.1 数据集划分与yaml配置YOLOv8对数据集的目录结构要求不复杂关键就是train和val两个目录目录下分开放images和labels两侧的文件名一一对应。划分数据时建议固定随机种子保证结果可复现。水下数据有一个容易被忽略的泄漏问题如果原始数据来自连续视频帧同一个鱼群场景的相邻帧可能高度相似。如果不加区分地随机划分同一批相近帧会同时出现在train和val里导致验证指标虚高真实部署时立刻被打回原形。我的做法是尽量把同一采集批次、同一段视频的图片放进同一个分区也就是按视频片段而不是按单帧来划分。数据集配置yaml内容大致如下path: /data/underwater_fish train: images/train val: images/val names: 0: fish如果你的数据里有多个鱼种names就按照classes.txt里的顺序展开。这里要再次强调类别id顺序和txt文件里的数字必须严格对应这是YOLO系列最容易踩但不报错的坑。一个不报错、但训练结果混乱的配置错误比一个直接崩溃的报错难排查十倍。4.2 训练参数的选择逻辑与调参基准我第一次跑这份数据集用的是yolov8s-seg.pt预训练权重imgsz默认640。水下鱼类的个体通常不大如果原图里鱼占像素比例偏低建议把imgsz拉高到960甚至1280。更高的输入分辨率对小型目标的分割效果有明显帮助代价是显存占用和训练时间一起涨。我的基础训练命令是yolo tasksegment modetrain modelyolov8s-seg.pt \ dataunderwater_fish.yaml \ epochs100 batch16 imgsz960 \ projectruns/segment namefish_basebatch大小取决于显存。如果batch16跑不动就降到8或4别硬扛。epochs我不建议一步到位先用50个epoch跑一个小规模的baseline观察loss曲线稳定程度再决定是继续还是停。这样能省下大量重复训练的时间。还有一个常见误区很多人听说mosaic增强对密集目标不友好就一上来把mosaic关掉。其实YOLOv8在训练最后阶段会自动关闭mosaic让模型在接近真实分布的数据上做微调不需要手动干预。只要训练后期没有出现严重loss震荡就保持默认。4.3 从权重复用看迁移学习的作用为什么从COCO预训练权重起步能显著加速收敛这要从卷积网络的特征分层说起。深层网络靠近输入的前几层学到的是边缘、颜色块、简单纹理这些通用视觉基元。COCO虽然不包含水下场景但鱼身轮廓、礁石纹理、水流边缘这些基础特征在通用图像数据集里同样大量存在所以预训练权重的底层特征依然可以直接复用。我在实验里对比过用COCO预训练权重起步前10个epoch的mask loss下降非常快如果没有预训练权重收敛速度至少慢三分之一而且最终mAP可能更低。更进阶的操作是先用一份公开的水下目标检测数据集做一轮简单预训练再切到这份实例分割数据上微调。这种先在相近域预训练再在下游微调的思路在数据量只有几百张的时候性价比尤其高。如果数据量少到极致连完整微调都可能过拟合可以考虑冻结backbone前几层只训练分割头。这样模型能保留预训练阶段学到的通用特征避免在水下小样本上被噪声带偏。5. 实测YOLOv8在密集鱼群上的表现与针对性调优5.1 水下图像增强给模型一副水下眼镜水下图像偏蓝绿的色偏是模型失效的重要来源。我在训练阶段做过对照实验一组用原始图片直接训练另一组先做CLAHE对比度增强和灰度世界颜色校正再送入训练。实验结果是后者的mAP50提升了接近两个百分点。也就是说简单的图像预处理就能帮模型减去不少负担。更进阶的思路是域随机化。在训练管线里随机调整色偏、对比度、亮度模拟不同水深、不同水质和不同补光条件下的成像变化相当于让模型见过更多种水下环境。YOLOv8自带的hsv_h、hsv_s、hsv_v增强参数就是这个作用。默认值可以直接用但我在水下数据上会把hsv_s稍微调高因为水下颜色衰减严重饱和度变化范围比陆上场景更大。另外水下图像常伴有模糊和细节丢失训练时适当加入高斯模糊或运动模糊增强能让模型对低清晰度输入更鲁棒。这些增强策略都是从数据本身反推出来的水下图像最缺什么就补什么。5.2 密集场景下NMS与类别不平衡的处理鱼群密集时我遇到最典型的推理问题同一条鱼被重复检出mask区域大面积重叠。原因是默认的NMS IoU阈值对密集目标太严格相邻目标的框重叠度高一个该保留的检测被另一个误伤抑制掉了。在推理阶段我会把iou阈值从默认的0.7调到0.5左右同时适当降低conf阈值把更多低置信度但位置正确的目标保留下来。具体调到多少要结合业务目标如果只是种群数量估计漏检比误检更致命就可以把conf压得更低如果要做精细行为分析宁可少检也不要太多误检conf阈值反而要调高。类别不平衡是另一个隐形杀手。如果数据集中某类鱼占了60%另一类只占1%少数类的loss会被多数类淹没训练结果是少数类的AP几乎为零。我的处理顺序是优先按类别加权loss或者做类别过采样代价最小如果还不行就把少数类样本的增强范围扩大用mosaic把这些稀缺样本拼进更多训练组合里。直接用类别权重最省事实测在多数场景下就够用了。5.3 评估指标mAP50-95之外的细节YOLOv8训练完会自动输出mAP50和mAP50-95但水下鱼类场景只盯这两个数远远不够。我还会额外看三样东西第一各类别的单独AP分布重点看尾部类别是不是被平均指标掩盖了问题第二小目标的AP值因为鱼群里很多鱼只有二三十像素模型很容易在中等以上目标上表现得不错小目标全线崩溃第三密集区域的可视化结果从验证集挑几张鱼最多的图逐帧检查mask轮廓和真实鱼身的贴合程度。mAP再高如果画出来的mask边缘锯齿严重后续做体长估计时误差会直接传导到业务结果。另外要注意如果验证集中crowd标注RLE占比不低mAP会显得偏低。因为模型输出的轮廓和crowd区域做匹配时天然吃亏。评估时不要只看绝对值要结合数据本身的标注特性去解释指标不然容易误判模型能力。6. 基于这套数据集的扩展玩法与进阶思路6.1 从静态数据集到真实水下机器人部署这份数据集用于实验室验证绰绰有余但真实部署另有一道坎。水下机器人的摄像头安装位置、防水壳材质、人工补光角度都会改变图像分布。我实测过把实验室训练好的模型直接接到水下机器人的视频流上前几分钟一切正常机器人一调整下潜深度画面颜色突变误检率就直线上升。解决思路是两条路并行部署端加一个轻量的颜色校正前处理同时采集一小段目标水域的视频帧做在线微调。哪怕只标几十张图效果都天差地别。实时推理这边也要换血。YOLOv8n-seg配合TensorRT在嵌入式板上可以跑到实时但小模型需要更高质量的监督信号。我的做法是先用s或m级模型跑一轮挖出一批高置信度的伪标注人工挑错后交给小模型训练。这种大模型产出数据小模型负责部署的思路在数据采集成本高的水下场景尤其实用。6.2 把标注轮子滚起来半自动标注与迭代实例分割标注成本高是行业共识很多团队连启动数据集的成本都凑不齐。我的经验是先用这份数据集训练一个可用的初始模型然后让它对未见视频帧做推理生成预标注掩码再交给标注员在掩码基础上修正只拖拽边界点而不是从零画多边形。这个流程能把标注效率提升两到三倍。不过有个细节自动生成的掩码如果边缘参差不齐标注员反而更费事。我的处理是在导出预标注前做一个多边形简化用Douglas-Peucker这类算法把边界点压缩到合理范围让轮廓既保持贴合又方便人工微调。现在条件允许的话用SAM这类通用分割模型先做预处理再对鱼类别做微调分类也是一个效率很高的新方向尤其适合鱼这种轮廓相对光滑的目标。6.3 下游任务计数、体长与生物量估算实例分割的最终价值在于它能支撑比检测更细的下游任务。有了每条鱼的像素级轮廓可以做个体计数、体长体宽估计、鱼群面积覆盖率计算甚至基于面积估算生物量。以体长估计为例关键不是直接取mask外接矩形的长度而是要先求出mask的主轴方向沿主轴测量投影长度再结合相机标定得到的像素-真实尺度换算关系。这里mask轮廓的边缘精度会直接影响测量误差所以前面强调的转换后可视化验证mAP之外的边缘贴合检查在业务落地时都不是可有可无的洁癖而是有实际经济价值的步骤。这套数据集的另一个衍生价值是如果发布者提供了同一视频流的时间戳或帧序信息你可以尝试做跨帧跟踪和重复计数消除这是鱼类洄游监测、水产养殖卖鱼计数里的刚需。哪怕没有时间戳单靠重叠度和NMS也能做单帧去重至少把明显的重复框先压下去。做完这一整套流程我对数据集是项目起点这句话的体会又深了一层。真正拉开模型效果差距的往往不是训练算力而是数据从下载到进入训练管线之间的每一道细活zip完整性检查、标注格式转换、坐标可视化验证、类别不平衡处理、密集场景的NMS调参。每一步都不起眼但每一步都可能让模型从能用变成没法用。希望这份水下鱼类实例分割数据集能让你的起步顺利一点也提醒你一句拿到任何数据集先别急着炫技把最枯燥的校验和转换做完再开训练你会感谢自己。本文还有配套的精品资源点击获取