
简介实例分割是计算机视觉中精细识别目标个体的核心技术能够输出像素级掩码区分不同对象在目标检测基础上实现更精准的计数、测长与形态分析。YOLOv8作为主流目标检测与分割框架提供了高效训练与部署能力广泛应用于工业视觉场景。在水产养殖领域罗非鱼与鲶鱼的密集养殖环境对个体识别提出挑战公开数据集稀缺自建数据成为落地关键。本文基于实际项目构建的罗非鱼与鲶鱼实例分割数据集系统讲解了数据采集、标注规范、格式转换、模型训练与边缘部署的完整流程并分享了数据划分、参数调优及避坑经验。无论是初学者还是从业者均可借助该路线图快速构建自己的分割数据集并训练可用模型助力养殖密度监测、自动分拣等任务落地。 罗非鱼与鲶鱼实例分割数据集这个压缩包是我在一次水产养殖自动化项目里折腾出来的副产物。当时团队需要同时完成鱼群计数、个体长度估算和种类区分翻遍公开数据集都找不到同时覆盖这两类鱼、还带实例分割掩码的资源。于是我自己下塘拍摄、逐张标注、做格式转换最后用YOLOv8的实例分割分支训练出可用模型。这篇博文不讲高大上的理论只聊这个数据集从0到1的真实过程以及你拿到类似数据后怎么踩到最少坑、快速用起来。这份数据集从实际项目中来适用于养殖密度监测、自动分拣、渔业资源调查等场景格式上一开始就按YOLO分割格式整理也保留了转换前的中间产物。无论你是刚开始接触实例分割的新手还是已经把YOLOv8跑得滚瓜烂熟的老人只要手头有类似的水下鱼类图片都可以把这篇当作一份可复用的操作路线图。1. 养殖场景里的鱼脸识别为什么偏偏是这两种鱼1.1 实际生产中的需求缺口罗非鱼和鲶鱼是淡水养殖里出镜率极高的两个品种但市面上大多数公开视觉数据集要么是自然水体中的珊瑚礁鱼类检测框要么是通用物体分割。真正面向养殖池、暂养箱这类高密度、浑浊水质、多遮挡场景的水产鱼类实例分割数据几乎找不到。这个问题在项目初期非常致命算法工程师可以花一天调模型但没法凭空变出覆盖罗非鱼鲶鱼两类目标的标注数据。我当时需要解决的业务问题有三个。第一是计数鱼塘里放了多少尾鱼不能靠捞起来数得靠摄像头自动数。第二是测长通过分割出的鱼体轮廓估算体长为投喂决策提供依据。第三是分拣混养捕捞后要把罗非鱼和鲶鱼分开。这三个需求都要求算法输出哪一个像素属于哪一条鱼也就是实例分割而不是简单的目标框。公开数据集在这一块几乎是空白工业界能落地的水产分割模型多数是企业自建数据训练的不会公开。这和自动驾驶数据集、无人机数据集的情况不一样那些场景参与者众多数据平台完善而养殖视觉还属于比较垂直的领域。所以做这个数据集本质上是在补一个行业缺口。1.2 实例分割比目标检测更适合什么活目标检测和实例分割最大的区别在于输出粒度。检测框是矩形哪怕鱼体弯曲成U形框里也必然包含大量背景如果两条鱼紧挨着框与框大面积重叠下游计数和测长就会互相干扰。实例分割输出的是每个鱼个体的像素级掩码相当于给每条鱼画了一幅“抠图”轮廓信息保留得非常充分。在养殖场景里这个区别会直接决定业务能不能落地。用目标框测体长框的边界可能比鱼体多出10%到20%的长度误差过大用掩码提取主轴线再按像素比例换算实际长度精度可以做到可接受。用检测框计数两条重叠的鱼会被当成一个目标用分割掩码加轮廓分析即使部分遮挡只要可见面积足够也能分离出独立个体。另外一个关键点是实例分割模型训练出来的特征通常比检测模型更关注鱼体边缘和形状差异。罗非鱼体侧扁、背鳍连续鲶鱼头部宽扁、长须明显这些在矩形框里容易被平均化但在像素级标签下模型必须学会利用边界信息最终在复杂背景中的鲁棒性往往更好。2. 从池塘到标签文件数据采集与标注的那些坑2.1 采集条件设计别只在晴天中午拍数据集的泛化能力很大程度上取决于采集阶段是否够“刁钻”。很多人第一步就做错了在一个干净的暂养箱里拍了几百张照片标注训练后一拿到浑浊的养殖塘就崩。我的经验是采集阶段就要刻意引入环境变化把你能预见到的部署场景都拍一遍。我当时的采集覆盖了三个环境室内暂养箱、室外土塘、半流水水泥池。设备没有用专业水下摄像机就是一台支持4K录像的运动相机加防水壳再配合手机拍摄补充视角。拍摄时间覆盖晴天上午、阴天下午和傍晚灯光条件其中傍晚和浑浊水质占了大约三分之一。这个比例很关键因为夜间或低照度条件下鱼体轮廓会变模糊模型训练时如果缺少这类样本上线后大概率会在光线变化时掉点。鱼的状态也要尽量多样。不要只拍游动中的侧视姿态还要拍俯视、仰视、半遮挡、两条鱼交叠、鱼群拥挤、幼鱼和成鱼同框等状态。鲶鱼的长须在侧游时会紧贴身体在正面视角会向前伸展这种形态差异必须让模型见过否则推理时容易把须子漏掉或者误判成另一条鱼。当时踩过的一个坑是只拍静止鱼群结果捕捞分拣时的水花和气泡造成了大量误检。后来补拍了一轮动态视频再从视频抽帧模型抗水花干扰的能力明显提升。所以不要只拍静图录像抽帧是低成本获得海量难例的好办法。2.2 筛选和预处理删掉一张图比标十张图更值采集回来的原始素材里真正能用的可能不到一半。模糊、过曝、完全被水草遮挡、目标个体只有几十个像素的图片标注后只会成为训练噪音。我采用了一个四步筛选流程可以照抄清晰度筛选肉眼扫一遍删除运动模糊和失焦图像。目标可见度筛选确保每张图里至少有一条鱼的主要身体轮廓可辨认。多样性筛选同一段视频连续抽帧得到的相似图像每组保留不超过3张避免数据冗余。分辨率筛选删除短边小于640像素的图片因为后续训练要缩放到640或更高分辨率太低会直接损失信息。有人会问目标很小的图片删掉会不会可惜我的看法是在实例分割任务里小目标标注本身误差率就高几个像素的偏移对mAP的影响很大与其留一堆错标样本不如先删掉等模型基本可用后再通过“难例挖掘”阶段专门补充小目标。预处理方面我没有做太多图像增强只统一把EXIF信息里的旋转方向校正掉避免标注坐标和实际像素对不上。这里有个容易忽略的细节手机拍摄的竖版照片在部分工具里打开时显示是正的但读取原始像素时可能是旋转过的导致多边形标注位置整体偏移。用Labelme打开前先确认图像已经被正确旋转归一化。2.3 标注规范用Labelme画多边形的边界条件标注工具我选的是Labelme没有用专门的分割标注平台。原因很简单数据量在几千张级别时Labelme完全够用输出JSON格式方便二次处理而且是开源免费的。标注规范一定要在团队开始工作前定死否则每个人都按自己的理解画最后返工成本极高。我定的规范有三条。第一多边形只标注鱼体的可见部分不脑补被遮挡或被水草盖住的部分。原因很实际如果两条鱼重叠逐像素判断哪部分属于谁本身就很难定义让标注员脑补完整形状会导致标签冲突。YOLOv8训练时两个重叠的掩码会互相竞争模型输出会变得混乱。第二鲶鱼的须如果清晰可见就纳入轮廓如果因为角度原因贴在身上分不清边界宁可少画须也不要画出超出实际轮廓的锯齿。第三鱼体轮廓至少要12个点但也不超过60个点太少的点拟合不了弯曲体形太多的点标注效率低且容易抖动。类别命名也要一开始就统一。我在JSON里直接用可读的英文类别名比如tilapia和catfish而不是用0和1。可读名称在团队沟通和配置管理时不容易出错最终转YOLO格式时再映射到数字ID。2.4 质量抽检与实例数量统计标注完成后质量抽检是必须做的一道工序。我的方法是让标注员交叉检查每人抽查别人标注的10%图像重点看三类问题多边形是否贴合鱼体边缘、类别是否混淆、同一个目标是否被重复标注。抽检不通过就退回修改直到合格率达到95%以上。这一步不能省。实例分割的标签质量比目标检测更敏感因为掩码边界直接参与损失计算轮廓偏移10个像素mAP可能掉好几个点。尤其鲶鱼和罗非鱼的幼鱼在外观上会有相似特征标注员一旦凭感觉下笔错误会系统地传导到模型里。最终统计下来这个数据集的标签分布大致如下类别图像数实例总数平均每图实例数tilapia罗非鱼118026802.27catfish鲶鱼92015401.67两张类别在多数图像中同时出现这个分布能支持模型学习类别间的对比特征。如果某个类别只有几百个实例训练时就要考虑类别权重或者过采样了。3. 数据集的内功目录结构、掩码格式与YOLO转换3.1 目录组织与data.yaml配置一个干净的数据集目录结构能让训练代码少踩很多坑。最终发布的zip里面核心的结构是这样的dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── labelme_json/ # 转换前的原始标注便于复查 └── data.yamlimages和labels一一对应同名不同后缀。YOLO训练时默认会在图像目录的上一级找同名labels目录所以只要能保持这个同步关系改起路径来很省事。data.yaml是关键配置内容如下path: . # 相对路径的根目录是data.yaml所在目录 train: images/train val: images/val test: images/test names: 0: tilapia 1: catfish这里有一个大家经常忽略的细节Ultralytics读取path时以当前工作目录为基准而不是以data.yaml所在目录为基准。如果你在项目根目录训练但data.yaml在dataset/data.yaml那么path应该写成./dataset而不是.。很多人报错image not found十有八九就是这个相对路径的问题。我建议统一做法训练命令从dataset的上级目录跑然后data.yaml里写path: dataset这样最直观。3.2 标签文件到底长什么样YOLO实例分割的标签格式每一行代表一个实例格式是class_id x1 y1 x2 y2 ... xn yn其中class_id从0开始坐标是归一化到0到1之间的浮点数所有点的顺序要按轮廓的顺时针或逆时针走一圈。注意这和COCO的RLE格式完全不同本质上是一个多边形坐标序列。一张图里有几个实例txt里就有几行。比如一条罗非鱼的标签文件可能是0 0.5182 0.4317 0.5236 0.4421 0.5312 0.4563 ... 1 0.7210 0.5183 0.7298 0.5302 ...这里的坐标都是相对于原图宽高的比例。训练时模型会根据这个比例关系把预测的归一化坐标映射回原图尺寸。如果坐标传成像素值模型会直接不收敛这是新手最容易踩的坑。3.3 Labelme JSON转YOLO分割txt的脚本Labelme生成的JSON结构里每个shape对象包含label和points其中points是像素坐标的多边形列表。转换脚本的大逻辑是遍历所有JSON文件取出类别名和坐标归一化后写入txt。下面是可以直接用的脚本import json import os from glob import glob class_names [tilapia, catfish] def convert_labelme_to_yolo(json_path, output_dir): with open(json_path, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue class_id class_names.index(label) points shape[points] if len(points) 3: continue norm_points [] for x, y in points: nx round(x / img_w, 6) ny round(y / img_h, 6) # 防止坐标越界但最好在标注阶段就避免 nx min(max(nx, 0.0), 1.0) ny min(max(ny, 0.0), 1.0) norm_points.extend([str(nx), str(ny)]) lines.append(f{class_id} .join(norm_points)) if not lines: return out_path os.path.join( output_dir, os.path.splitext(os.path.basename(json_path))[0] .txt ) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 用法 for json_path in glob(labelme_json/*.json): convert_labelme_to_yolo(json_path, labels/train/)脚本里有个细节坐标保留6位小数。对640分辨率来说6位小数相当于0.00064像素完全够用保留过长的浮点数反而会让txt文件变大读取效率变低。3.4 训练集/验证集划分的泄漏陷阱数据集划分是另一个容易出大问题的地方。很多人用train_test_split随机划分看起来很公平但在鱼群场景下这样做会导致数据泄漏。原因是同一个视频抽帧出来的连续图像非常相似随机划分后验证集里很可能混入与训练集几乎一样的画面mAP会虚高。到真实场景一测效果立刻打回原形。正确做法是按“来源片段”划分同一段视频或同一批连续拍摄的图片要么全进训练集要么全进验证集不能让它们跨集合。我的划分比例是train : val : test 7 : 2 : 1。其中test集是最后才抽出来的只用来做一次最终评估不能反复看它的结果调参否则也相当于过拟合。凡是沾了验证集和测试集的信息都会让你低估模型在真实养殖塘里的掉点幅度。4. YOLOv8实例分割训练参数调优和效果复盘4.1 选backbone模型与训练命令数据准备好了训练部分我直接用Ultralytics YOLOv8。原因很简单API统一、训练命令简单、文档和社区资料多对快速验证很友好。第一步是安装pip install ultralytics训练命令长这样yolo train \ modelyolov8n-seg.pt \ datadata.yaml \ epochs120 \ imgsz640 \ batch16 \ device0这里我故意先用了yolov8n-seg.pt也就是最小的分割模型。有人会问为什么不用yolov8m或者yolov8x那样精度不是更高吗我的经验是第一步先跑通nano确认数据没问题再升级模型。nano训练快能让你在半小时内发现标签文件坏了、路径错了、类别名称映射错了这些致命问题。等nano能稳定跑到mAP50在90以上再换s或m模型吃精度红利。对这份数据集来说最终线上部署用的是yolov8s-seg.pt。s模型比nano在鱼体边缘的拟合能力明显更强尤其鲶鱼须条这种细长结构nano很容易在掩码重建时丢掉。如果硬件资源允许直接上m也不是不行但推理速度会掉部署到边缘设备前要考虑清楚。4.2 训练参数怎么调训练参数里我重点关注四个imgsz、epochs、batch和数据增强。imgsz默认是640但鱼在画面里通常占比较大放大输入尺寸能保留更多轮廓细节。把imgsz调到800后mAP50-95大约涨了1.5个点代价是显存占用和训练时间增加。如果你的GPU只有8G显存建议先用640跑通再尝试用--rect或者减小batch来适配更大分辨率。batch的大小直接关系到显存占用和训练稳定性。我用的GPU是RTX 4090 24Gbatch设16没问题。显存不够时不要盲目调小imgsz优先减小batch然后看是否需要开启梯度累积。Ultralytics没有直接的梯度累积参数但可以通过固定batch为1、多次迭代来模拟麻烦一点但有效。数据增强方面Ultralytics默认会开mosaic和flip我基本保留默认。额外开启的是scale0.5和degrees10因为鱼在水里姿态多变轻微旋转和缩放能让模型更鲁棒。不要开太大的旋转角度大于45度会产生大量不合理姿态反而把训练带偏。epochs我设了120配合早停策略Ultralytics默认有patience50基本在80到100轮就收敛了。如果数据量更大可以适当增加到150到200但收益会递减。4.3 训练结果评估与可视化训练完成后直接看验证集指标模型mAP50mAP50-95推理速度(ms)yolov8n-seg0.9120.7733.2yolov8s-seg0.9410.8155.6mAP50在0.94对鱼群密集场景来说已经能用了。mAP50-95在0.81左右说明掩码质量也不错。但数字只是参考我建议每次训练后都抽看验证集的可视化结果重点看三类图密集重叠的鱼、低光照下的鱼、尾部被遮挡的鱼。模型往往在这几类图上暴露出mAP看不出的问题。可视化可以用Ultralytics自带的model.val()它会保存验证集的预测图到runs/segment/val目录。如果发现某个类别的掩码经常粘连在一起或者轮廓把水花气泡也包进去就要考虑是不是该补充难例或者调后处理参数。4.4 训练中容易翻车的三个问题第一个问题是标签文件为空。Labelme转换时如果某些图像的JSON里没有有效类别脚本会直接跳过最终导致labels目录里缺文件训练时模型报错找不到标签。解决方法是转换后写一个检查脚本确保images和labels目录下同名文件一一对应空标签文件也要保留空文件但不能缺失。第二个问题是类别权重失衡。如果罗非鱼图像远多于鲶鱼模型会对鲶鱼明显拟合不足。我当时通过在data.yaml里设置可选的类别权重参数或者简单粗暴地对鲶鱼图像做重复采样来缓解。这个数据集本身两类实例比例还算均衡但如果你自己采集的数据偏科一定要关注。第三个问题是过拟合。在数据量不足时模型会把背景纹理也学进去验证集指标高换环境就崩。我见过最典型的例子是模型把蓝色池壁当成了鱼因为训练图几乎都是蓝色背景。解决办法是加入不同背景、不同水质的数据而不是盲目依赖增强。5. 部署到实际场景评估细节与避坑清单5.1 别只盯着mAP要看混淆矩阵和错误例子mAP是一个聚合指标很多错误被平均掉了。比如鲶鱼须被误检成罗非鱼这类错误在mAP里可能只降低0.1个点但在实际分拣时会产生严重误判。所以我强烈建议在评估阶段拉出混淆矩阵分析每一类被错分的原因。我对验证集的错误例子做了归因发现三大来源一是两条不同鱼种紧贴时模型会偶尔把边界混淆二是低照度下鲶鱼须条特征消失模型误判成罗非鱼三是水花反光在部分角度下被当成鱼体边缘。前两个问题靠补充样本解决第三个问题我通过在训练集里混入更多有气泡和水花的视频帧让模型学会抑制这些干扰。部署前最好再做一次跨场景测试单独挑出另一个鱼塘拍摄的数据完全不参与训练只用来测试。这一步能非常诚实地告诉你模型的泛化能力。我见过太多模型在验证集上mAP50超过0.9但到新环境只剩0.7的案例。5.2 边缘设备部署的mask重建问题如果你打算把模型部署到Jetson这类边缘设备不能直接跑原版PyTorch模型。Ultralytics提供了导出功能yolo export modelbest.pt formatengine device0导出成TensorRT engine后推理速度能快数倍但有几个细节要注意。第一个是输入分辨率。导出时imgsz必须和训练时一致或者稍微向大取整否则模型无法正确推理。第二个是mask后处理。YOLOv8的实例分割输出包括检测框、类别、置信度和一组mask系数需要再配合原型mask做矩阵计算才能得到最终掩码。Ultralytics的Python API会帮你完成这一步但如果你用C或者TensorRT的纯推理接口就要自己写mask重建逻辑。我实际上踩过一个坑TensorRT加速后推理帧率上去了但mask轮廓明显变粗糙。原因是export时有个dynamic参数如果设置不当mask分支会被简化。解决方法是在导出时固定batch size和尺寸不让模型动态选择或者提升workspace参数让TensorRT保留更多精度。5.3 从掩码到计数和测长的处理思路实例分割的最终目的是支撑业务。对水产养殖来说拿到mask之后经常要做两件事计数和测长。计数很简单每个独立掩码就是一个个体直接用掩码数量累加即可。但注意严重遮挡时模型可能把一个目标分成两段或者把两个紧贴目标合并成一个。我建议在计数逻辑里加一个面积过滤小于某个像素阈值的掩码不计数大于平均面积两倍的掩码要做二次轮廓分割。这个阈值可以通过验证集统计得到不用拍脑袋。测长需要现场标定。单纯从mask算像素长度没有物理意义必须先知道图像中每像素对应多少毫米。我用的方法是在鱼池里放一个有固定尺寸的标定板或已知长度的参考杆模型检测到后自动计算像素比例。然后对每个鱼体掩码用OpenCV提取主方向并计算投影长度import cv2 import numpy as np mask ... # 模型输出的二值mask类型为uint8 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取最大轮廓作为鱼体 cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) pixel_length max(rect[1]) # 外接矩形的长边 real_length_mm pixel_length * mm_per_pixel这个方法的精度取决于鱼体是否侧向展开。如果鱼正在转弯mask的主轴会缩短测出来的体长会偏小。实际应用时我更推荐对同一鱼个体的连续多帧做中值滤波能有效消除姿态变化带来的波动。5.4 数据集复用与持续迭代数据集做完之后不要急着封存。实际部署中产生的难例是继续提升模型的最佳燃料。我的做法是每两周从现场捞一批模型预测置信度低的图片人工复核后加入训练集。这个过程叫难例挖掘是最朴素的主动学习。比如之前鲶鱼须条因为角度问题频繁漏检后来专门补了一批须条清晰伸出的图像再训练后这类错误减少了约两成。如果后续想扩展到其他鱼种比如鲤鱼或草鱼可以在现有模型权重上做增量学习。保留tilapia和catfish新增类别时只需要在data.yaml的names里加新类别然后把新类别的标注数据与部分旧数据混合训练。注意一定要保留足够比例的旧数据否则模型会灾难性遗忘。这个数据集对我最大的价值不是mAP多少而是让我把采集—标注—转换—训练—部署—迭代这套链路完整走通了。你拿到这个zip之后完全可以从训练一个demo模型开始把它当成一方试验田再慢慢长出自己的数据积累。最后再分享一个小技巧不管你是做检测还是分割训练前花十分钟打开几张标签图和原图叠加的可视化确认多边形没有错位。这一步能避免你后面所有的时间都浪费在回滚上。本文还有配套的精品资源点击获取