☰
基于YOLOv8的鱼病检测系统:从数据集到UI部署全流程
2026/10/21 4:44:59 网站建设 项目流程

鱼病检测这件事,我在水产养殖一线看了太多"发现即晚期"的案例:工人拎着抄网站在池边,一条条捞鱼翻看体表,等真看到白点、出血斑的时候,往往整个池子已经大面积感染,治疗的代价也从几百块变成了几万块。当时我就在想,能不能拿一套视觉方案,让摄像头替人盯住水面,用深度学习模型实时圈出病鱼和病灶位置,再配一个不考验电脑水平的UI界面,让养殖户鼠标一点就知道哪条鱼不对劲。

最后我做出来的这套东西,就是标题里说的:基于YOLOv8深度学习的鱼病害检测系统,包含完整数据集、Python源码、训练好的模型权重,以及带界面的可视化程序。模型负责"看",UI负责"说人话",训练好的权重可以直接加载跑通,不需要从头训。这篇文章把我从选型、攒数据、训练调参、写界面到部署踩坑的完整过程拆开写,重点是那些文档里不会告诉你、只有真跑过才知道的细节。如果你正准备做一个目标检测方向的小项目,或者想在农业场景里落地深度学习模型,这篇应该对你有用。

1. 为什么是"检测"而不是"分类":YOLOv8选型的实际理由

很多人一上来就问:鱼病识别,那不就是个图像分类任务吗?拿一堆健康鱼和病鱼图片,训一个ResNet或者ViT,判断"有病/没病"不就行了?

真做的时候就发现问题了。分类网络的输出是整个图片的标签,它告诉你是"这张图里有一条烂鳃的鱼",但不会告诉你病鱼在画面的哪个位置、病灶面积有多大、现在到底有几条鱼中招。而水产养殖里最关键的信息恰恰是定位:你要知道是哪条鱼、哪个位置有问题,才能精准捞出来隔离,而不是整池下药。目标检测天然就能干这个活,边界框一画,坐标、置信度、类别全部齐了。

1.1 YOLOv8相对之前版本的实际收益

YOLOv8是Ultralytics团队在2023年推出的迭代版本,项目里选它主要因为它解决了我几个实际痛点。

第一,anchor-free机制。之前的YOLOv5还得预先设定anchor尺寸,如果数据里目标特别大(整条鱼)或者特别小(鱼身上的白点),调anchor是个很磨人的活。v8直接引入了anchor-free解耦头,不需要手动预设anchor,小目标和大小差异大的目标友好得多。

第二,模型部署链路完整。ultralytics这个Python包把训练、验证、导出、推理封装得很顺手。训练完一句model.export(format='onnx')就把模型导出去了,UI界面里加载模型推理只需要几行代码。对单人做的小项目来说,不用东拼西凑各种转换脚本,省了大量时间。

第三,训练资源要求比更重的同级别模型低。我用的是YOLOv8s,参数量大概在1100万左右,一块普通的深度学习显卡就能训,批量大小放16左右,显存占用不到8G。在项目落地阶段,这套模型也够在边缘设备上跑,不太吃硬件。

1.2 检测和分类的根本差别

做一个好项目之前要把思路理清楚:分类是"整个画面正常还是异常",检测是"画面里有哪些对象、每个对象在什么位置、它属于哪个类别"。养殖场景里异常和正常鱼往往同时出现在一个画面里,分类网络会把整张图盖上一个"有病"的章,但画面里八成鱼都是健康的,这种粗粒度信息没法指导后续操作。检测模型则能一次性输出"哪条鱼是白点病、哪条是赤皮病、正常的可以不管",这种输出粒度才匹配现场决策需求。

顺带说一句,如果你只想验证算法可行性、不在乎落地,分类方案确实更简单。但既然项目标题里写了"系统+UI+源码",那就肯定是奔着给用户交付一个能判断、能定位、能操作的工具去的,检测是绕不开的选择。

2. 数据集搭建:整场项目里最值得花时间的部分

做数据集这件事,占了我整个项目周期的60%以上。模型再先进,数据质量的提升空间也比调神经网络的参数量大得多。标题里特意写了"YOLO数据集",说明这套项目提供了整理好的数据,不过我还是建议你搞清楚这些数据是怎么来的、标注规范是什么,因为你将来用在别的养殖场景时,一定要自己补充数据做增量训练。

2.1 病害类别怎么定

鱼病有很多种,但我建议第一版不要贪多,类别设计太碎会带来两个问题:一是标注工作量大到做不完,二是相似病征之间会让模型非常困惑。我最后锁定了4个类别:

  • healthy:体表正常、没有明显病灶的鱼
  • white_spot:体表有明显白点,对应小瓜虫等白点类病害
  • gill_rot:鳃部有糜烂、发白或缺损,这一类别拍摄难度比较大
  • hemorrhage:体表有出血斑、发红区域,常见于细菌性出血病

如果你拿到的公开YOLO数据集类别不是这套,映射关系需要你来对齐。常见的情况是别人数据集里用的英文标签,界面里显示成中文,需要做一层标签映射。

2.2 采集设备和拍摄要点

最开始我想直接拿广角摄像头朝水池一拍,画面里全是鱼,看起来很美好。实际测试发现,自由游动的鱼运动模糊严重,鱼在快速转身的瞬间身体形态夸张扭曲,这些样本标了也白标,模型学不到稳定的特征。

我的经验是两条路并行:

一是水下固定机位拍摄,用防水摄像头对着一个定点(比如投饵区)连续录屏,再按帧抽图。鱼在觅食时游动速度相对慢,姿态也比较舒展,抽出来的帧可用率高。二是隔离小容器拍摄,把疑似病鱼单独捞进透明小缸里,用手机或相机从侧面拍摄。这个方式非常推荐,因为侧面平视角度能清晰拍到鳃盖、体表、腹鳍这些关键区域,画面干净无遮挡。

抽帧方面,我一般按每秒抽1到2帧,避开水面波动造成的剧烈反光。一小段10分钟的视频大概能抽到600到1200张可用图片,再去重、去模糊、去重复场景,最终留下有效样本。

样本采集要注意体色多样性,同一种鱼在不同光照、不同水深、不同水质环境下,体色差异很大,只在一个环境拍,模型的泛化很快会露馅。

2.3 标注工具与标注规范

我用的标注工具是LabelImg和X-AnyLabeling,前者的格式是Pascal VOC XML,后者可以直接导出YOLO txt格式。如果你拿到的是VOC格式的标注,记得转成YOLO格式再训练,转换规则不复杂,就是读XML里的<object>字段,把bbox左上角坐标和宽高转成归一化的x_center y_center width height。

标注规范有几个注意点:

  • 病斑很小,比如白点病单颗白点只有几个像素,这时候不要逐颗点白点,否则标注数量巨大且容易漏标。建议按"鱼体"为粒度标注,只要这条鱼身上有明显白点簇,就把整条鱼框出来标成white_spot。
  • 一条鱼同时有白点和出血斑时,我建议只标注最典型的那个病变类别,混合标签会让模型学到的特征不公允。
  • 鱼体被遮挡超过一半的样本,要么删掉,要么标能看到的部分。
  • 背景里的气泡、漂浮物、池壁划痕都可能被框进去当作目标,标注时尽量选画面干净的鱼体样本。

类别样本量方面,我的目标是最少类别也要有800到1200个目标框。如果你的数据里healthy占压倒性多数,白点病样本很少,模型很容易学成"看到鱼就标healthy"。类别不平衡对检测任务的影响很大。

2.4 数据增强和类别平衡

YOLOv8自带的增强策略已经很强,我在训练配置里开启的马赛克增强、翻转、HSV扰动、随机平移就能把训练样本量扩得很大。尤其是马赛克(Mosaic)增强,把4张图拼接成一张,等于让模型在同一个背景下看到多个目标,对小目标训练帮助很明显。

但要注意,马赛克增强在最后几十个epoch会关闭,避免模型长期看不真实的完整图像导致泛化下降。

类别平衡方面,我除了对健康鱼样本做下采样之外,还做了一件事:把病鱼小目标通过随机缩放、镜像粘贴到健康鱼背景图里,生成一批"混合训练样本"。这个操作能显著改善病鱼类别的召回率,因为模型在训练时能看到更多小尺寸、遮挡少的病灶目标。

3. 训练过程复盘:从环境配置到mAP曲线的完整记录

数据准备好后就是训练。我先说环境:我用的是PyTorch + CUDA 11.8的组合,Python版本3.9。ultralytics这个包对CUDA版本比较挑剔,装的时候建议先在命令行跑一下python -c "import torch; print(torch.cuda.is_available())",确认显卡能正常调用再往下走,否则后边训练半天才知道CPU在跑,心态容易崩。

3.1 数据集目录结构与配置文件

YOLOv8要求的数据集目录一般长这样,以我的工程为例:

fish_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── fish_disease.yaml

fish_disease.yaml里面内容如下:

path: ./fish_dataset train: images/train val: images/val nc: 4 names: 0: healthy 1: white_spot 2: gill_rot 3: hemorrhage

每张图片对应的txt标注文件,文件名和图片名保持一致,放在labels目录的同名文件夹里,标注内容一行一个目标:

class_id x_center y_center width height

这里要求的是归一化后的坐标,注意转一下除法,别直接填像素值。

3.2 训练脚本和超参数

我的训练脚本不长,核心大概就是这样:

from ultralytics import YOLO model = YOLO("yolov8s.pt") # 从预训练权重开始 model.train( data="fish_dataset/fish_disease.yaml", epochs=150, imgsz=640, batch=16, lr0=0.01, lrf=0.01, patience=30, augment=True, device=0, project="fish_disease_runs", name="yolov8s_exp1" )

从预训练权重开始而不是从零训练,对鱼病检测这种小数据集项目是必须的。预训练模型已经在海量数据上学过边缘、纹理、颜色等基础特征,微调阶段只需要把注意力重新分配到鱼身和病灶上,收敛速度快得多。

训练轮数我设了150轮,配合patience早停。我这里patience设为30,意思是连续30轮mAP没有明显提升就提前保存最优权重并停止,避免过拟合。

3.3 训练曲线的读法

训练完先不要急着看测试集,先把训练曲线读明白。ultralytics训练结束后会在runs/detect/yolov8s_exp1/下生成results.csv和一堆曲线图。

我那一版训练损失曲线表现比较典型:box_loss从初始的1.8左右快速下降到0.6,然后在0.05到0.03区间震荡;cls_loss从3.1逐渐降到0.8附近。主要看着验证集的性能有没有跟着涨。

如果你看到的训练损失一直在降、但验证损失在20轮后明显反弹,不用怀疑,就是过拟合了。鱼病数据的背景变化相对单一,模型学会了硬背背景,泛化就会差。这个时候优先减小模型规模,从yolov8s降到yolov8n,或者适当增大数据增强强度,而不是加正则化。

3.4 测试集评估指标怎么看

我习惯把20%的数据留出来做测试集,不用ultralytics默认的val数据集评估,因为验证集本身参与了调参,结果还是会偏乐观。单独跑一套测试脚本,统计每个类别的精确率、召回率和mAP@50、mAP@50-95。

以我最终那版模型为例(这一版作为项目默认权重保存在源码里),测试集大概600张图,统计下来的结果大致是:

类别PrecisionRecallmAP@50
healthy0.940.980.96
white_spot0.880.920.91
gill_rot0.790.860.85
hemorrhage0.830.880.87

这个精度做初步落地是够用的,但white_spot里还有不少漏检,主要原因是户外阳光直射下白点被高光反射洗白,这个后边UI里讲怎么处理。

我跑过第二个训练轮次,把epochs提到300,类别白点那个recall反而掉了一点,原因是模型开始过拟合到部分高光纹理上。这一点很值得记下来:不是训练越久越好,数据规模不大的时候,模型参数在150轮左右往往已经达到局部最优,再练下去可能就是背噪声。

4. UI界面设计:让模型从命令行变成普通人能用的工具

算法能跑通只是第一步,整个项目最难啃的反而是UI。水产养殖的用户很多不是学编程的,你给他一个命令行,他抬头看你一眼,你就知道这事儿要黄。所以这个项目的UI界面设计核心原则是:减少认知负担,所有操作必须一看就懂。

4.1 界面技术选型

我最终用了PySide6来做桌面GUI。选它的原因有几个:

  • Python生态无缝衔接,模型加载和推理逻辑直接写在窗体类里,不用额外搭前后端接口。
  • Qt的QGraphicsView组件非常适合画检测框,缩放和绘制性能比直接在label上画好很多。
  • 打包成exe相对方便,用PyInstaller就能出一个免Python环境的独立程序。

如果你更倾向做Web版,Flask或者Gradio也能快速做出一个预览界面,但考虑到养殖场环境通常没有稳定的服务器,桌面版更可靠。

4.2 功能板块拆解

界面布局我分成四大块:

顶部是图片和视频的载入按钮,支持两种输入方式:选本地单张图片测试,或者选视频文件逐帧检测。左侧是模型信息面板,显示当前加载的权重文件、类别列表和检测参数(置信度阈值、IoU阈值、检测间隔)。中间是主画面区,检测结果实时渲染到画面里,每个目标框旁边标注类别和置信度。底部是结果统计面板,用表格列出当前帧/当前图里识别出的病鱼数量、类别分布,并提供当前结果截图和导出报告按钮。

检测置信度阈值我默认设0.4。阈值设太低,误报增多;阈值设太高,漏检变多。拿到新场景的模型,我一般会先用默认0.4跑几张典型图,在界面上反复调阈值,观察哪个值在错报和漏检之间最舒服。

4.3 摄像头实时检测

项目标题里提到了检测系统,摄像头实时检测是必须支持的。PySide6里我用QTimer周期性从cv2.VideoCapture读取画面帧,检测完把带框的画面推回界面刷新。这里有个非常关键的体验点:如果每帧都推理一次,显卡扛得住但普通电脑CPU会卡成幻灯片。我加了一个"检测间隔"滑杆,默认每3帧推理一次,同时用上一次的帧间隔作为缓冲,界面流畅度会好很多。

代码结构大致是这样:

import cv2 from PySide6 import QtGui, QtCore from ultralytics import YOLO class FishDetectionThread(QtCore.QThread): update_frame = QtCore.Signal(QtGui.QImage) def __init__(self, model_path): super().__init__() self.model = YOLO(model_path) self.is_running = True def run(self): cap = cv2.VideoCapture(0) while self.is_running: ret, frame = cap.read() if not ret: break results = self.model.predict(frame, conf=0.4, iou=0.5) for r in results: annotated = r.plot() qt_img = QtGui.QImage( annotated.data, annotated.shape[1], annotated.shape[0], annotated.strides[0], QtGui.QImage.Format_BGR888 ) self.update_frame.emit(qt_img) cap.release()

视频流检测线程必须单独放在QThread里,否则UI会卡死,这是新手最容易踩的坑。

4.4 结果导出与报告

养殖从业人员往往需要留证,比如记录哪一天开始出现白点趋势。我在UI里加了"导出报告"按钮,点一下把当前图片和检测结果拼到一张含时间戳的截图里,再把统计数据输出成CSV。这个功能在实际使用中是被人用最多的,反而是一般开发者最容易忽略的地方——检测只是一瞬间的事,追踪一个病害发展趋势才是养殖户真正关心的。

5. 部署和优化阶段踩过的坑:反光、虚焦、误报与推理性能

训练出来模型漂亮没用,部署到真实现场问题一个接一个。我把这一阶段踩到的高频坑写下来,每一件都是真实测试中碰到的。

5.1 水面反光和运动模糊

户外池子最常见的干扰是阳光照在水面上形成大片高光反射,鱼游过高光区域时,模型的白色病灶检测框会疯狂输出误报。我后来加了三个处理手段:

  • 数据层面:在训练集里刻意纳入带反光、带气泡的样本,让模型见过这些干扰。
  • 图像预处理层面:用OpenCV滤波降低高光区域的局部对比度,让白点病灶和反光的区分度变大。
  • 部署层面:UI界面上支持选择"户外模式",该模式会自动剔除画面顶部高频闪烁区域,减少瞬时误判。

运动模糊的问题靠数据增强和帧间检测一致性一起来改善。帧间一致性指的是连续几帧里同一位置都检测出white_spot才记为一次有效报警,单帧的偶发检测直接忽略。这在视频检测中非常管用,很多误报都是单帧闪烁状出现的。

5.2 误报率与阈值调节

误报是鱼病检测系统最难处理的坑之一。水产从业者对误报的容忍度比想象中低很多——误报一次两次,他可能还看看;误报复旦形成"狼来了"效应,系统再准确他都不信了。

我在UI上做了一件事:把"置信度阈值"和"检测结果保留策略"分离开。阈值用来控制单帧输出,结果保留策略则决定连续帧是否累积报警。默认策略是:连续三帧中至少两帧检测到同一类别的同一区域,才在界面上标红并触发声音提醒。这比单纯调高置信度阈值更符合现场逻辑,代价是会牺牲一些早期微弱病灶的检出,但胜在误报率压得很低。

5.3 模型导出和速度优化

项目源码里保留了三种模型格式:pt、ONNX、和TensorRT的engine文件(如果你有对应显卡的话)。

导出成ONNX后推理速度比原生pt快不少,CPU上提高尤其明显。我之前在普通电脑上测过,pt格式跑单张640x640大概需要160毫秒,导出成fp16 ONNX后降到110毫秒左右,TensorRT如果在NVIDIA显卡上跑,能到30到40毫秒级别。

导出一个ONNX模型的代码:

from ultralytics import YOLO model = YOLO("best.pt") model.export(format="onnx", opset=12, half=True, simplify=True)

如果你是部署到Jetson这类嵌入式设备,那TensorRT几乎是必选项。如果没有NVIDIA设备,在UI里可以通过"检测间隔"滑杆来控制负载,效果也还算可以。

6. 实测效果和一点落地心得

最后说说这套系统在真实环境里的表现和我自己的一些感受。

整套系统在几个测试场景中都跑过完整的流程:一是仿真养殖环境水箱,二是户外小池塘的岸边机位,三是给某位做循环水养殖的朋友安装的试点版本。综合看下来,模型在清晰、光照正常的画面上表现最好,准确率体感很高,能够稳定标出体表白点、鳃部发白的病鱼;光照复杂的户外场景需要打开"户外模式"过滤高光干扰,同时把报警阈值适当调高,误报率基本可控。

要说这套系统还有什么明显的短板,一是鳃部病害检测的可视化效果还很有限,因为鳃盖翻开才能看到糜烂面,常规机位拍不到,目前的gill_rot主要是靠鳃盖区域的色差和开合异常来推断,属于弱特征;二是数据集中病鱼的头部、鱼鳍边缘样本偏少,导致这一类小目标的边界框贴合度一般;三是个体养殖户的硬件条件参差不齐,老旧电脑上帧率只能到个位数,这需要靠更轻量的模型和更合理的抽帧策略来弥补。

实际操作中我最深的体会,是这套系统的调试重心真的不在模型训练上,而在数据采集和用户预期管理上。花一周时间在池边拍各种光线条件下的视频、补标注,模型的泛化能力能涨一截;而再好的模型,如果UI上不给使用者提供结果可解释、报警原因可见的反馈,用户也不会信这个东西。

最后分享一个小经验:不管模型在测试集上看着多好看,现场测试一定要带着用户一起跑。让养殖户自己操作一遍、看他觉得哪里别扭、指着哪些误报骂过,回来改一遍UI和阈值,这个迭代过程才是项目能不能真正被用起来的关键。模型调得再花哨,没人用,一切都是白搭。

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

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

立即咨询