简介面向金鱼健康监测与水产养殖智能化场景这份目标检测数据集涵盖健康金鱼、腹水病、白点病和败血症四类适合训练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, encodingutf-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(fdataset/{split_name}.txt, w, encodingutf-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(ftrain{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: congestiondata.yaml 里的 names 必须严格按字典顺序排列。yaml 解析后是 dictultralytics 会按 key 排序如果键写的是字符串而不是整数类别顺序可能和训练标签错位这是目标检测数据集处理里最隐蔽的坑。训练命令用 YOLOv8 的标准入口yolo detect train \ datadataset/goldfish.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.001 \ patience30 \ projectgoldfish_exp \ namerun1几个关键参数的实际意义modelyolov8s.pt从 COCO 预训练权重开始。鱼体、水体边界这些通用特征已经被模型学过了迁移到金鱼检测可以显著缩短训练周期。如果数据量少于 2000 张不建议直接用 yolov8l 或从随机权重开始容易过拟合。imgsz640是默认分辨率。金鱼在画面里占比小的时候比如养殖池全景需要调到 960 甚至 1280。显存不够时优先减 batch不要动 imgsz——batch 影响 BN 统计量和收敛稳定性一般 8 到 32 之间差异不大imgsz 直接影响小目标召回率在金鱼疾病检测里比 batch 重要得多。patience30表示训练 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_h0.015表示色调偏移幅度在正负 1.5% 内hsv_s0.7表示饱和度偏移 70%hsv_v0.4表示明度偏移 40%。金鱼体色鲜艳hsv_s 可以适当加大。不同鱼缸的灯光色温差异很大饱和度扰动不足模型在偏暗环境下容易漏检。随机模糊经常被忽略但对游动中的金鱼非常关键。1/30 秒快门下鱼尾部位必然出现运动模糊如果训练集里全是清晰帧模型对模糊病灶区域的学习不足。开启轻微的高斯模糊核大小控制在 0.5 到 1.5 像素太强的模糊会让白点病的小白点和噪声混在一起反而降低对初期病灶的敏感度。4.2 翻转增强对非对称病灶是双刃剑通用检测任务里水平翻转几乎是必开的增强项但在疾病检测场景要格外谨慎。白点病和充血出血在鱼体两侧都可能出现水平翻转不影响标签语义可以开但烂尾烂鳍往往只发生在单侧。在我的实际项目里金鱼发病位置存在明显的侧向偏好单侧病灶是常态。如果鱼只向左游动时右侧鳍条受损翻转之后变成左侧鳍条受损模型学到的“哪一侧更容易患病”的先验就被破坏了。遇到这种情况要么关闭fliplr要么把“左侧烂鳍”和“右侧烂鳍”作为不同类别分别标注。后者的标注成本和类别混淆风险都会上升。我的建议是先用默认开启翻转训练一轮观察病类别在单侧发病样本上的 recall如果明显偏低就关掉用实验数据做决定而不是凭直觉。4.3 针对小目标和游动模糊的增强参数示例金鱼疾病检测里的“小目标”和通用场景不同——鱼在画面里尺寸不小但白点病的单个白点只有几个像素属于病灶级小目标。模型检测整条鱼并分类疾病增强的目标是让鱼框内的病灶纹理尽量多样。yolo detect train \ datadataset/goldfish.yaml \ modelgoldfish_exp/run1/weights/best.pt \ epochs100 \ imgsz640 \ batch16 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ scale0.5 \ translation0.1 \ fliplr0.5 \ mosaic0.7 \ degrees0.0 \ projectgoldfish_exp \ namerun2_aug这几个参数里最容易踩的细节是degrees0.0。部署摄像头通常水平安装鱼体在画面中基本保持水平加入旋转增强会让模型浪费参数去学习现实中不存在的倒鱼姿态。scale0.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 表再谈其他指标。这个习惯帮我从一个又一个因为类别不平衡翻车的项目里脱身出来希望也能帮到你。本文还有配套的精品资源点击获取
