罗非鱼与鲶鱼实例分割数据集构建与YOLOv8训练部署实战
简介:实例分割是计算机视觉中精细识别目标个体的核心技术,能够输出像素级掩码,区分不同对象,在目标检测基础上实现更精准的计数、测长与形态分析。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(罗非鱼) | 1180 | 2680 | 2.27 |
| catfish(鲶鱼) | 920 | 1540 | 1.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, encoding="utf-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", encoding="utf-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 \ model=yolov8n-seg.pt \ data=data.yaml \ epochs=120 \ imgsz=640 \ batch=16 \ device=0这里我故意先用了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 24G,batch设16没问题。显存不够时,不要盲目调小imgsz,优先减小batch,然后看是否需要开启梯度累积。Ultralytics没有直接的梯度累积参数,但可以通过固定batch为1、多次迭代来模拟,麻烦一点但有效。
数据增强方面,Ultralytics默认会开mosaic和flip,我基本保留默认。额外开启的是scale=0.5和degrees=10,因为鱼在水里姿态多变,轻微旋转和缩放能让模型更鲁棒。不要开太大的旋转角度,大于45度会产生大量不合理姿态,反而把训练带偏。
epochs我设了120,配合早停策略(Ultralytics默认有patience=50),基本在80到100轮就收敛了。如果数据量更大,可以适当增加到150到200,但收益会递减。
4.3 训练结果评估与可视化
训练完成后,直接看验证集指标:
| 模型 | mAP50 | mAP50-95 | 推理速度(ms) |
|---|---|---|---|
| yolov8n-seg | 0.912 | 0.773 | 3.2 |
| yolov8s-seg | 0.941 | 0.815 | 5.6 |
mAP50在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 model=best.pt format=engine device=0导出成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, key=cv2.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模型开始,把它当成一方试验田,再慢慢长出自己的数据积累。
最后再分享一个小技巧:不管你是做检测还是分割,训练前花十分钟打开几张标签图和原图叠加的可视化,确认多边形没有错位。这一步能避免你后面所有的时间都浪费在回滚上。
本文还有配套的精品资源,点击获取
