☰
YOLO金鱼疾病检测数据集构建与训练实战全流程
2026/9/26 23:45:08 网站建设 项目流程

简介:面向金鱼健康监测与水产养殖智能化场景,这份目标检测数据集涵盖健康金鱼、腹水病、白点病和败血症四类,适合训练YOLOv8等主流检测模型,也适合渔业研究人员、养殖从业者与爱好者学习金鱼疾病识别。数据按标准格式划分为684个训练样本、96个验证样本和48个测试样本;压缩包共1657个文件,包括828张jpg原图及对应828个txt标签文件,另有1个yaml配置文件,整体约33.74MB,结构紧凑,可直接用于YOLO系列模型训练与评估。目前已有815人学习。样本图像采集兼顾不同光线、拍摄角度与背景,经专家筛选标注,泛化性较好;可快速搭建家庭鱼缸或养殖场的金鱼疾病自动识别流程,实现早期预警、减少经济损失,也可作为鱼类健康科普和日常实验教学的直观素材。

1. 金鱼疾病检测不是通用目标检测:数据集和模型绑定在一起才有价值

养过金鱼的人都知道,白点病、水霉病在早期几乎无法靠肉眼判断。鱼身上出现几个白点时不处理,三天后可能扩散到全缸;等烂尾、充血肉眼能看见时,治疗成本已经翻了几倍。这也是为什么很多渔业养殖场和水族设备商开始把 YOLO 这类目标检测算法直接装到缸边摄像头后面,实时盯着鱼的状态。但这个方向的落地瓶颈不在模型结构,而在训练数据——公开渠道能下载到的鱼群检测数据集大多是“找鱼”而不是“找病”,能同时区分正常金鱼、白点病、水霉病、烂尾烂鳍的数据集非常少见。下文围绕“YOLO金鱼与金鱼疾病检测数据集”展开,把从采集、标注、训练到验证的完整链路讲清楚,适合两类人:渔业或水产养殖从业者,以及基于 YOLO 做垂直领域数据集和模型训练的算法工程师。

2. 金鱼疾病检测的数据集构成:正常、患病与干扰三类样本怎么配比

金鱼疾病检测的目标不是“识鱼”,而是识别鱼的异常体表状态。和烟草病虫害数据集、燃气管道图像数据集这类垂直检测项目一样,核心难点在于样本获取,而不是模型结构。金鱼不同疾病在视觉上的差异很明显:白点病是小瓜虫寄生,体表和鱼鳍上出现针尖大小的白色颗粒;水霉病是棉絮状白色菌丝,常出现在伤口附近;烂尾烂鳍表现为鳍条边缘发白、缺损,严重时只剩骨架;充血出血则表现为体表红斑、鳍条血丝。这些症状的目标尺寸、对比度和运动状态差别不小,如果只按通用目标检测的做法随便找一批鱼的照片标注就去训练,模型在真实水族缸里的表现会非常不稳定。

类别视觉特征标注建议
正常金鱼体表光滑、各鳍完整、游动正常框躯干加完整尾鳍
白点病针尖状白色颗粒,多分布在体表和鳍条白点密集区单独框出
水霉病棉絮状白色菌丝,附着在体表或伤口框住菌丝主体区域
烂尾烂鳍鳍条边缘发白、缺损、呈锯齿状优先只框躯干,严重时配合旋转框
充血出血体表红斑、鳍条血丝框住明显红斑区域

第一版不建议直接做满五类。常见做法是先做三类:正常金鱼、白点病、水霉病。这三类的视觉差异最大,标注一致性好,模型容易收敛。烂尾和充血的特征有重叠且早期形态不稳定,放到第二迭代再加,否则类别之间相互干扰,模型反而哪一类都学不好。

2.1 训练集、验证集和测试集的比例该怎么划

养殖现场的检测场景有两个特点:金鱼在不停游动,姿态变化快;水质、光线和缸壁反光直接影响成像质量。这意味着数据不能只在干净的实验水体里拍,否则模型在真实鱼缸或养殖池里会大幅掉点。

我一般按 7:2:1 做划分,但顺序不是先随机打散再切分,而是先按“时间片段”或“拍摄批次”整体划分。最容易犯的错是:同一段视频连续帧的相似度极高,如果直接按帧随机划分,训练集和验证集里会出现同一只鱼、同一姿态的相邻帧,验证指标虚高,部署时立刻露馅。正确做法是把一次拍摄生成的完整视频作为一个整体,只允许它进入其中一个集合。文件命名时把批次号写进去,比如batch01_0001.jpg,后续脚本按批次号分组操作,比事后靠时间戳推断省事得多。

如果项目从零开始,建议先拍 30 段短视频,每段 30-60 秒,覆盖不同水色、不同光照角度、不同鱼品种。这样得到的原始素材比集中拍几百张静态照片的信息量高很多,因为鱼在游动中会呈现各种姿态,疾病区域也会在不同角度下展示出完全不同的纹理特征。

2.2 标注格式用 YOLO txt 还是 VOC xml:转换脚本与边界坑

目标检测常用标注工具里,LabelImg、Labelme、X-AnyLabeling 都是可选方案,但标注格式这件事在团队协作和迭代中影响很大。

  • VOC XML 适合人工标注和核验,LabelImg 默认导出格式,标注过程直观;
  • YOLO txt 是训练器的原生输入,文件体积小,训练前不用二次转换,但坐标是归一化值,人眼很难直接检查某个框是否正确。

团队规模大于两人时,不建议直接用 txt 标注。YOLO 的标签文件里没有图像路径,一个 txt 标错了很难定位是哪张图。统一用 VOC 格式做标注,训练前用一个转换脚本一次性生成 YOLO txt 集,是更可靠的工作流。

import os import xml.etree.ElementTree as ET # 类别清单,顺序必须和训练时 data.yaml 里的 names 顺序完全一致 CLASSES = { "goldfish": 0, # 正常金鱼 "ich": 1, # 白点病 "saprolegnia": 2, # 水霉病 "fin_rot": 3, # 烂尾烂鳍 "congestion": 4 # 充血出血 } def convert_voc2yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.findtext("filename") img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) txt_lines = [] for obj in root.findall("object"): cls_name = obj.findtext("name") if cls_name not in CLASSES: continue cls_id = CLASSES[cls_name] xmin = float(obj.findtext("bndbox/xmin")) ymin = float(obj.findtext("bndbox/ymin")) xmax = float(obj.findtext("bndbox/xmax")) ymax = float(obj.findtext("bndbox/ymax")) # 坐标必须归一化到 0~1,否则训练时所有框都会偏掉 cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h bw = (xmax - xmin) / img_w bh = (ymax - ymin) / img_h # 越界坐标裁剪到 [0,1],防止贴边目标导致标签超界 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) txt_lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, os.path.splitext(img_name)[0] + ".txt") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(txt_lines))

这段脚本把 XML 里的目标框解析出来,中心点和宽高换算成归一化数值,再做一次越界裁剪。归一化和越界裁剪是两个不能省的动作——YOLO 训练时如果标签里出现 1.2 或 -0.1 这类越界值,OpenCV 的绘图逻辑和 anchor 匹配并不会直接报错,模型就带着错误标签训下去,表现是 mAP 上不去而且非常难排查。

提示:转换脚本里的类别清单顺序,必须和 data.yaml 里的 names 顺序一致。YOLO 标签文件里只有数字 ID,没有类别名,ID 错位会造成所有类别全部学反,这种问题在训练日志里几乎看不出来。

3. 从原始图片到可训练的数据集:整理脚本、数据划分和训练命令

用 YOLOv8 训练自己的数据集时,目录结构和类别配置出错是报错频率最高的环节。YOLOv8 的标准数据布局是 images 和 labels 平级、同前缀对应:一张IMG_0001.jpg对应一个IMG_0001.txt。训练时如果某张图片没有对应标签,训练器会直接跳过它;反之某个 txt 没有对应图片,训练器会报错。所以整理阶段就做一次全量校验,比训练时报错再回头查高效得多。

3.1 数据整理脚本:统一命名、生成 train/val/test 清单

import os import random from pathlib import Path random.seed(42) # 固定随机种子,保证两次运行划分结果一致 image_dir = Path("dataset/images") label_dir = Path("dataset/labels") images = sorted([p.name for p in image_dir.glob("*.jpg")]) labels = sorted([p.name for p in label_dir.glob("*.txt")]) # 只保留图片和标签都齐全的样本,缺失项打印警告便于人工补漏 valid_images = [] for img_name in images: base = Path(img_name).stem if (label_dir / f"{base}.txt").exists(): valid_images.append(img_name) else: print(f"[WARN] label missing: {img_name}") orphan_labels = [lbl for lbl in labels if not (image_dir / (Path(lbl).stem + ".jpg")).exists()] if orphan_labels: print(f"[WARN] {len(orphan_labels)} labels without matching image") random.shuffle(valid_images) n_total = len(valid_images) n_train = int(n_total * 0.7) n_val = int(n_total * 0.2) train_files = sorted(valid_images[:n_train]) val_files = sorted(valid_images[n_train:n_train + n_val]) test_files = sorted(valid_images[n_train + n_val:]) def write_split(split_name, file_list): with open(f"dataset/{split_name}.txt", "w", encoding="utf-8") as f: for img_name in file_list: f.write(str((image_dir / img_name).resolve()) + "\n") write_split("train", train_files) write_split("val", val_files) write_split("test", test_files) print(f"train={len(train_files)} val={len(val_files)} test={len(test_files)}")

脚本做的事有三件:校验图片和标签是否一一对应、按 7:2:1 比例切分数据、生成训练器能直接读取的清单文件。写清单时用resolve()把相对路径转成绝对路径,可以避免从其他目录启动训练时找不到文件。random.seed(42)保证两次运行得到相同的划分,这个细节在复现实验时很重要——不固定种子,每次跑训练的数据组成都不同,指标没有可比性。

关于验证集和测试集的区别,很多新手容易混。验证集用于训练过程中选 best.pt,测试集只用于最终验收,不能参与任何调参决策。如果测试集被反复用来比较实验效果,它实际上就变成了验证集,最终报告出来的精度会偏乐观。

3.2 用 YOLOv8 训练金鱼检测模型的最小命令与参数解读

数据整理完成后,先写一个data.yaml:

# dataset/goldfish.yaml path: /path/to/dataset train: train.txt val: val.txt test: test.txt names: 0: goldfish 1: ich 2: saprolegnia 3: fin_rot 4: congestion

data.yaml 里的 names 必须严格按字典顺序排列。yaml 解析后是 dict,ultralytics 会按 key 排序,如果键写的是字符串而不是整数,类别顺序可能和训练标签错位,这是目标检测数据集处理里最隐蔽的坑。

训练命令用 YOLOv8 的标准入口:

yolo detect train \ data=dataset/goldfish.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.001 \ patience=30 \ project=goldfish_exp \ name=run1

几个关键参数的实际意义:

model=yolov8s.pt从 COCO 预训练权重开始。鱼体、水体边界这些通用特征已经被模型学过了,迁移到金鱼检测可以显著缩短训练周期。如果数据量少于 2000 张,不建议直接用 yolov8l 或从随机权重开始,容易过拟合。

imgsz=640是默认分辨率。金鱼在画面里占比小的时候,比如养殖池全景,需要调到 960 甚至 1280。显存不够时优先减 batch,不要动 imgsz——batch 影响 BN 统计量和收敛稳定性,一般 8 到 32 之间差异不大;imgsz 直接影响小目标召回率,在金鱼疾病检测里比 batch 重要得多。

patience=30表示训练 30 个 epoch 验证集指标不上升就提前停止。金鱼不同疾病的收敛速度差异大:白点病特征明显,收敛快;水霉病早期形态模糊,收敛慢。patience 太小,水霉病还没学到位就停了,最终权重在部署时会选择性漏检。

训练结束后,goldfish_exp/run1/weights/best.pt就是当前最优权重。验证时不要只看总 mAP50,要按类别逐个看。很多疾病检测项目的总指标不难看,但某一个类的 recall 惨不忍睹,这就是类别不平衡的典型症状。

4. 金鱼场景的数据增强配置:哪些增强能用、哪些必须关

金鱼检测的训练数据和真实部署之间有三个明显差异:鱼是动的、水是浑的、光是变的。多拍视频能补一部分,但数据增强是提升泛化能力更直接的手段。直接照搬通用目标检测的增强配置会翻车,下面逐项说明取舍。

4.1 马赛克增强、HSV 扰动和随机模糊的取舍

YOLOv8 默认开启的马赛克增强适合大多数场景,但在金鱼疾病检测里需要控制概率。马赛克把四张图拼在一起时,鱼体可能被分割到边框处,病变区域被裁剪掉一半,标签框的有效 IoU 变差。另一个问题是四张图水色不同,拼出来的图色彩过渡生硬,模型容易学到“蓝绿色边界”这种与疾病无关的伪特征。我的做法是把马赛克概率从默认的 1.0 降到 0.5 到 0.7,保留其他增强作为补充。

HSV 扰动对水色变化非常有效。养殖场景的水质从清澈到黄绿色变化很大,不同密度下水的色相、饱和度和明度都不同。hsv_h=0.015表示色调偏移幅度在正负 1.5% 内,hsv_s=0.7表示饱和度偏移 70%,hsv_v=0.4表示明度偏移 40%。金鱼体色鲜艳,hsv_s 可以适当加大。不同鱼缸的灯光色温差异很大,饱和度扰动不足,模型在偏暗环境下容易漏检。

随机模糊经常被忽略,但对游动中的金鱼非常关键。1/30 秒快门下鱼尾部位必然出现运动模糊,如果训练集里全是清晰帧,模型对模糊病灶区域的学习不足。开启轻微的高斯模糊,核大小控制在 0.5 到 1.5 像素,太强的模糊会让白点病的小白点和噪声混在一起,反而降低对初期病灶的敏感度。

4.2 翻转增强对非对称病灶是双刃剑

通用检测任务里水平翻转几乎是必开的增强项,但在疾病检测场景要格外谨慎。白点病和充血出血在鱼体两侧都可能出现,水平翻转不影响标签语义,可以开;但烂尾烂鳍往往只发生在单侧。在我的实际项目里,金鱼发病位置存在明显的侧向偏好,单侧病灶是常态。如果鱼只向左游动时右侧鳍条受损,翻转之后变成左侧鳍条受损,模型学到的“哪一侧更容易患病”的先验就被破坏了。

遇到这种情况,要么关闭fliplr,要么把“左侧烂鳍”和“右侧烂鳍”作为不同类别分别标注。后者的标注成本和类别混淆风险都会上升。我的建议是:先用默认开启翻转训练一轮,观察病类别在单侧发病样本上的 recall,如果明显偏低就关掉,用实验数据做决定,而不是凭直觉。

4.3 针对小目标和游动模糊的增强参数示例

金鱼疾病检测里的“小目标”和通用场景不同——鱼在画面里尺寸不小,但白点病的单个白点只有几个像素,属于病灶级小目标。模型检测整条鱼并分类疾病,增强的目标是让鱼框内的病灶纹理尽量多样。

yolo detect train \ data=dataset/goldfish.yaml \ model=goldfish_exp/run1/weights/best.pt \ epochs=100 \ imgsz=640 \ batch=16 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ scale=0.5 \ translation=0.1 \ fliplr=0.5 \ mosaic=0.7 \ degrees=0.0 \ project=goldfish_exp \ name=run2_aug

这几个参数里最容易踩的细节是degrees=0.0。部署摄像头通常水平安装,鱼体在画面中基本保持水平,加入旋转增强会让模型浪费参数去学习现实中不存在的倒鱼姿态。scale=0.5表示缩放幅度在正负 50% 之间,适合缸内全景和特写镜头混合的数据;如果数据全是特写,scale 可以降到 0.2,防止框抖动。

从已经训好的best.pt继续跑,比从预训练权重重新开始更容易判断增强项的实际效果。对比 run1 和 run2 的验证集 PR 曲线时,重点看水霉病和烂尾这两类在增强后 recall 是否提升。如果只是 precision 波动而 recall 不变,说明增强策略没有本质帮助,需要回头补采集,而不是继续调参数。

5. 金鱼疾病检测避坑:四个最容易翻车的环节

金鱼疾病检测数据集这个方向,真正耗时间的是数据而不是训练。下面四个问题基本都是必经之路,按现象、原因、解决三个层次拆开讲。

5.1 类别不平衡:正常鱼几千张,水霉病只有几十张

现象:训练完成后总 mAP 不低,但按类别一看,水霉病的 recall 不到 15%,验证集里的病鱼大部分被识别成正常金鱼。

原因:养殖场景下正常鱼占绝大多数,疾病样本天然稀疏。水霉病早期只是小块棉絮,中期才变成大片白毛,如果不特意去采集严重样本,数据集中疾病类和正常类的比例可能高达 1:50。目标检测的损失函数对正负样本数量天然敏感,头部类别的梯度会淹没尾部类别。

解决:训练前先用脚本统计每类目标框数量,对少数类做过采样。YOLOv8 可以在 dataset 层传repeat参数做简单过采样;更有效的是补采,把患病严重的鱼放到浅色容器里单独拍一段视频,让它在同一场景里呈现多种姿态和角度,这比几十张零散照片的信息量高得多。另外不要在 data.yaml 里手工调 loss 权重,先保证每类目标框数量收敛到同一量级,再考虑类别损失加权。

5.2 气泡和水面高光被识别成白点病

现象:模型部署在真实缸边时,白点病的 precision 指标看起来还可以,但气泡密集区、缸壁高光点被大量标记为白点病,产生大量假阳性报警,运维人员一周后就不再信任这个系统。

原因:白点病的视觉特征就是“带白点”,和气泡、反光点在单帧图像里几乎没有纹理差异。鱼缸内壁的气泡同样呈圆形、同样偏白、同样成群出现,模型在小感受野上无法区分。

解决:训练数据里显式加入“气泡”和“高光”作为负样本类别,标注时把可见气泡框出来,类别名可以叫bubble。模型学到的就不是泛泛的“白点特征”,而是“白点特征是否出现在鱼体表面”。同时引入时序约束,部署时对连续多帧的检测结果做投票,只有同一位置连续出现才输出报警。气泡不会跟着鱼游动,鱼身上的白点会随鱼体移动,时序信息是最可靠的判别依据。

5.3 标注框不贴合鱼尾:AP 曲线锯齿状震荡

现象:训练过程中验证集 mAP 曲线不是平滑上升,而是每个 epoch 上下跳动,最终模型的预测框有时偏头有时偏尾。

原因:金鱼游动时尾巴摆动幅度很大,矩形标注框一会儿框住全部鳍条,一会儿只框住半截。同一只鱼在不同帧里的标注框宽高差异可能超过 30%,与正常目标检测任务里“同一目标框基本稳定”的前提冲突,模型拟合时来回摇摆。

解决:标注规则上约定疾病类别只框躯干,不框完整尾鳍,从根源上消除尾巴摆动造成的标注抖动。对于必须框尾部的用例,比如烂尾检测,建议用旋转框标注。YOLOv8 的 OBB 检测模式支持旋转框,虽然训练和部署复杂度上了一个台阶,但比矩形框硬拟合稳定得多。另一个数据侧技巧是删除连续帧中标注框面积突变超过 50% 的样本,这类帧大多是尾鳍突然展开的瞬间,属于标注噪声。

5.4 混淆矩阵每一列加起来不是 100%:先看口径,再找问题

现象:训练完成后打印混淆矩阵,每一列的数值之和明显不等于 1,看起来像代码 bug。

原因:YOLO 输出的混淆矩阵有原始计数和归一化两种形式。按列归一化时,某一列代表“真实类别为 A 的全部样本被分到了哪些预测类别”,列和是 1;按总量归一化时,列和不等于 1 是正常的。更隐蔽的是,混淆矩阵里还带 background 行,把背景行算进去,列和当然不等于 1。

解决:先确认打印出来的是原始matrix属性还是normalized属性。matrix是原始计数,列和等于该真实类别的样本总数,不表示 100%;normalized才是比例。判断模型好坏的顺序应该是:先看归一化矩阵对角线值是否够高,再看每类 recall,最后看误检具体落在哪一列。只盯着列和是否为 100% 去调参,会把时间浪费在根本不存在的 bug 上。

6. 养殖现场验证模型:三类指标、半自动标注与迭代节奏

训练集之外还需要保留一组与训练集拍摄条件不同、拍摄时间不同的测试集,用于最终验收。养殖现场的验证不能只看 mAP,建议分三个维度看。

6.1 现场测试集的验收指标怎么定

验证维度指标参考值
目标框准确度测试集 mAP50病类别 mAP50 不低于正常类别 8 个百分点
早期发现能力初期病灶 recall白点病少量白点图片的 recall 大于 70%
误报水平连续 10 分钟部署录像的误报次数误报不超过 2 次每小时

这三个维度对应养殖现场的三个真实诉求:检测得准不准、能不能在早期发现、会不会频繁误报干扰运维。参考值是我在类似项目里的经验阈值,不是行业标准,不同养殖密度和摄像头安装位置会有出入,但它能帮助判断模型离可部署差多远。

6.2 半自动标注与持续迭代节奏

mAP 达标不代表现场能用。现场摄像头位置固定,水色和训练集差异大时模型会掉点,需要准备一版现场数据微调。微调用 100 到 200 张现场截图就够,重点是覆盖现场的玻璃反光、水深和鱼体色,用已训练的best.pt继续训 30 个 epoch。

后续迭代中,半自动标注是省时间的关键。用训练好的模型对新增视频做预测,置信度大于 0.7 的检测框直接作为预标注,人工只修正边界和错误类别;置信度在 0.3 到 0.7 之间的框标记为“待确认”;小于 0.3 的帧跳过。这个流程能把标注效率提升一倍以上,但有一个前提:每个批次的新数据都要有人工复核,不能直接信任预标注结果,否则标注错误会沿权重传递,越滚越大。

最后说一个我的教训:第一版模型验收时只看了总 mAP,忽略了按类别拆分,结果删掉水霉病样本后总指标反而更好看,直到部署到现场才发现严重水霉病的鱼几乎全被漏报。后来每次训练结束先打印按类别的 recall 表,再谈其他指标。这个习惯帮我从一个又一个因为类别不平衡翻车的项目里脱身出来,希望也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询