多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLOv8鱼类疾病检测实战:从数据标注到边缘部署全流程

YOLOv8鱼类疾病检测实战:从数据标注到边缘部署全流程 简介一套基于Python与YOLOv8构建的鱼类疾病检测系统源码面向水产养殖技术人员、计算机视觉学习者及Python开发者用于识别出血、眼部缺陷、鳍部缺陷、溃疡等鱼类常见疾病实现养殖现场的自动化监测与预警。系统支持22种鱼类病症检测可对图片、视频及摄像头画面进行实时推理自动保存检测结果并支持Excel导出同时提供Web前端展示界面方便直观查看分析。压缩包共24个文件以Python源文件覆盖训练、预测、验证与界面交互全流程、PNG图像用于结果可视化及Markdown说明文档为主要类型整体大小为2.26MB。配套说明涵盖完整的训练教程与数据集使用方式并支持70余种YOLOv8创新点改进用户可按需训练自定义模型进一步优化检测精度和速度。目前已有90人学习下载适合需要快速搭建鱼类病害检测方案或深入研究目标检测应用的人群。1. 鱼类疾病检测为什么选 YOLOv8 这张技术牌水产养殖里最烧钱的事不是饲料是鱼病发现太晚。白点病从零星几颗到全池爆发往往不到三天等肉眼能看清的时候损失已经按池算了。这就是鱼类疾病检测系统存在的理由——用摄像头把鱼的体表特征实时拉出来让算法代替人眼盯塘口。而 YOLOv8 是这个场景下最合理的起点它有足够成熟的目标检测精度推理速度快到能跑实时视频流而且训练链路被 ultralytics 封装得很短一个 Python 脚本就能把数据、训练、校验串起来。所以这个标题背后其实是一套完整的落地方案Python 搭环境、YOLOv8 做检测、鱼病数据和标注撑训练。适合谁适合水产养殖方向的开发者、想从分类模型转检测的算法工程师还有手里有塘口数据但不知道怎么变现的从业者。接下来我就按自己实际做过的路径把环境和数据、训练和部署一层层拆开讲。2. 搭建 Python GPU 环境先把 ultralytics 跑通再谈训练2.1 用 Miniconda 隔离环境Python 版本与 CUDA 的匹配细节凡是卡在第一步的人九成是环境问题不是模型问题。鱼类疾病检测项目用到 PyTorch 和 ultralytics这两个库对 Python 版本和 CUDA 版本都有隐性要求。我一般不用系统自带的 Python 直接装而是先建一个 Miniconda 独立环境避免把本机的 Python 弄乱。conda create -n fish_disease python3.8 -y conda activate fish_diseasePython 3.8 是兼容性最稳的版本。PyTorch 从 1.8 到 2.0 都能在 3.8 下跑ultralytics 对 3.8 也没有显式设限。如果你直接用系统自带的 Python 3.11 或 3.12大概率会遇到某个依赖包还没发对应版本轮子结果 pip 装一半开始编译源码编译失败后整个环境就乱了。接着是 PyTorch 安装。这一步先确认自己的显卡驱动支持到什么 CUDA 版本再决定装哪个 PyTorch 轮子。nvidia-smi右上角显示的 CUDA Version 是驱动支持的版本不是当前环境里已经装好的版本这两个概念新手最容易混。nvidia-smi pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这段命令里cu118对应 CUDA 11.8是兼容面最广的一档。如果你的显卡比较新RTX 30 系以上可以换成cu121甚至cu124但没必要追新。跑 YOLOv8 训练时真正调用的是 PyTorch 自带的 CUDA runtime跟系统里装没装 CUDA Toolkit 没有直接关系所以只装 PyTorch 轮子就够。装完后用一句命令验证 GPU 是否真正可用这一步不能跳。import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果torch.cuda.is_available()返回False原因通常是 PyTorch 轮子的 CUDA 版本和驱动不匹配或者装成了 CPU 版本。注意看安装时控制台输出的包名如果 torch 后缀是cpu说明安装源给错了重新用--index-url装带cu后缀的版本。这一步跑通之后整个项目的地基才算稳。2.2 安装 ultralytics 并验证 YOLOv8 能跑通的最小命令ultralytics 是 YOLOv8 的官方维护库安装它不需要源码编译直接 pip 拉轮子即可。这一步和标题里的“Python”直接相关因为后面所有训练、推理、导出都是围绕这一个包展开的。pip install ultralytics这个包会自动带上 opencv-python、matplotlib、pandas 等依赖不需要单独装。装完之后别急着训练自己的数据集先下载官方预训练权重跑一次最小推理确认整个链路没断。yolo predict modelyolov8n.pt sourcehttps://ultralytics.com/images/bus.jpg第一次执行会下载yolov8n.pt预训练权重大概 6 MB 左右。yolov8n是 nano 版本参数量最小用来验证环境最合适。如果这行命令能跑出一张带预测框的图片说明 Python 环境、PyTorch、ultralytics 三个环节全部正常。很多人在这一步看到ImportError: libGL.so.1之类的报错这是 opencv-python 缺系统库Ubuntu 下用apt-get install libgl1 -y解决CentOS 下用yum install libgl。验证完再装训练需要的辅助包。pip install tensorboardtensorboard 不是必须的但强烈建议装。YOLOv8 训练时产出的 loss 曲线、学习率曲线、PR 曲线它都能可视化后面判断模型有没有收敛全靠它。3. 整理鱼类疾病数据集从采集到 YOLO 格式的转换全流程3.1 疾病类别怎么定小样本条件下分成几类最合理类别设计直接决定模型能不能收敛、上线后准不准这个决定比调参重要一个量级。鱼类疾病种类很多但做检测系统时不能贪多尤其是数据量只有几百张图的情况下类别越多混淆越严重。我做过一个版本把类别定为白点病、烂鳃病、水霉病、柱状病、打印病五类。为什么是这五类白点病表现为鱼体表白色点状颗粒烂鳃病主要在鳃部水霉病是棉絮状菌丝柱状病烂尾烂鳍打印病是体表红斑。这五类的形态差异大框出来之后模型有明确的视觉区分度。如果你一开始就上十几类比如把车轮虫、指环虫、斜管虫也塞进去它们病灶形态相似标注的人自己都分不清模型学到的一定是噪声。社区里能找到几个公开的鱼类疾病数据集但规模通常不大类别标注质量也参差不齐。我的做法是先人工筛选一遍把模糊的、光线过暗的、病灶不清晰的图片直接删掉宁缺毋滥。数据集本身不是越大越好类别清晰、标注准确的图比堆数量有用得多。3.2 用 LabelImg 标注并导出为 YOLO txt 的实操步骤标注工具我推荐 LabelImg它可以直接输出 YOLO 格式的 txt 文件省去转换的坑。YOLO 格式的标注文件是每个图片一个同名 txt每行代表一个目标框五个数字依次是类别序号、归一化后的中心点 x、中心点 y、框宽、框高。pip install labelimg labelimg打开 LabelImg 后先把默认的 PascalVOC 输出格式切换成 YOLO 格式这个设置在顶部的菜单栏里。然后在Open Dir里选中图片文件夹Change Save Dir里指定标注输出目录。标注时用W键拉框D键翻页CtrlS保存。拉框的原则是病灶区域有多少框多少鱼身上健康的部分不要框。标注完一个类别的图片后检查一下 txt 文件内容是否符合预期。0 0.512345 0.345678 0.123456 0.087654第一列是类别序号0 对应 yaml 文件里 names 列表的第一个元素。后面的四个数字必须都是 0 到 1 之间的小数超过 1 或者负数说明标注框越界了训练时 ultralytics 会直接报错。另一点要注意LabelImg 的 YOLO 模式要求图片和 txt 文件同名如果图片是fish_001.jpg标注文件必须是fish_001.txt不要把 txt 放到子目录里YOLO 训练脚本是按同级目录去找对应标注的。3.3 数据增强与训练集划分别让验证集替你背锅数据量少是鱼类疾病检测最常见的瓶颈。一张鱼体表照片白点病的白点可能只有十几个像素模型需要看足够多不同光线、不同角度的样本才能学到真正的特征而不是死记硬背某一张图。ultralytics 自带增强策略在训练时默认开启包括随机翻转、马赛克、色彩抖动。但有两个参数需要手动调。# fish_disease.yaml path: /home/user/fish_disease train: images/train val: images/val nc: 5 names: [white_spot, gill_rot, saprolegniasis, columnaris, red_spot]这个 yaml 是训练入口之一的配置文件path是数据集根目录绝对路径train和val用相对路径指向图片文件夹。ultralytics 会自动去对应目录找同名的 txt 标注不需要在 yaml 里写标注路径。nc必须和names的列表长度一致写错任何一个训练启动阶段就会报维度不匹配错误。训练集和验证集的切分要格外小心。我见过不少翻车现场随机切分时同一个池子的鱼图片被同时分到训练集和验证集导致验证集的 AP 虚高模型一上真实塘口就现原形。正确的做法是按来源切分比如 1 号池的鱼只进训练集2 号池的鱼只进验证集保证验证集里的场景是模型没见过的。如果图片是从不同视频帧里截取的更要按视频切分别按帧切。划分脚本我一般这样写简单可控。import os import random import shutil random.seed(42) src_images raw_images train_dir fish_disease/images/train val_dir fish_disease/images/val for split_dir in [train_dir, val_dir]: os.makedirs(split_dir, exist_okTrue) all_images [f for f in os.listdir(src_images) if f.endswith((.jpg, .png))] random.shuffle(all_images) val_count int(len(all_images) * 0.2) val_images all_images[:val_count] train_images all_images[val_count:] for img in train_images: shutil.copy(os.path.join(src_images, img), os.path.join(train_dir, img)) txt img.replace(.jpg, .txt).replace(.png, .txt) if os.path.exists(os.path.join(src_images, txt)): shutil.copy(os.path.join(src_images, txt), os.path.join(train_dir, txt)) for img in val_images: shutil.copy(os.path.join(src_images, img), os.path.join(val_dir, img)) txt img.replace(.jpg, .txt).replace(.png, .txt) if os.path.exists(os.path.join(src_images, txt)): shutil.copy(os.path.join(src_images, txt), os.path.join(val_dir, txt))这段脚本做的事情很直接把原始图片随机打乱按 8:2 分成训练集和验证集连同同名 txt 标注一起拷过去。random.seed(42)保证每次运行产生相同划分结果方便复现。shutil.copy保留原文件不做移动防止操作失误把原始数据搞丢。这里的0.2是验证集比例样本总量少于 300 张时可以改成0.15给训练集多留一点数据。4. 训练鱼类疾病检测模型参数怎么调、损失怎么判4.1 训练脚本与参数表一次能跑通的标准配置训练入口是 ultralytics 提供的 Python API把数据配置、模型权重、训练参数一次性传进去。建立一个train.py内容如下。from ultralytics import YOLO if __name__ __main__: model YOLO(yolov8s.pt) results model.train( datafish_disease.yaml, epochs100, imgsz640, batch16, device0, workers4, ampTrue, patience20, lr01e-3, augmentTrue, cacheram, )这组参数是我在 GTX 1660 Ti 级别的卡上验证过的配置6 GB 显存能跑显存紧张就降 batch。model YOLO(yolov8s.pt)表示加载 s 版本预训练权重做迁移学习然后基于它继续训练。为什么不上yolov8m因为 1660 Ti 跑 m 版本batch 只能降到 4训练一轮要很久收益却不大。鱼类疾病检测的目标是小目标和细粒度纹理s 版本的表达能力已经足够关键是数据质量。参数理解上imgsz640是输入分辨率训练时 ultralytics 会自动做 letterbox 填充把任意尺寸的图片缩放成 640x640不需要预处理脚本。batch16是单次迭代的样本量16 在 6 GB 显存下已经是上限。patients20是早停策略验证集的 mAP 连续 20 个 epoch 不提升就自动断。cacheram表示把图片全部缓存进内存能大幅减少磁盘 IO 等待但前提是内存够大16 GB 内存以下就删掉这个参数。4.2 迁移学习权重怎么选yolov8s.pt 是起点不是终点很多人忽略了一个细节预训练权重决定你训练多久能收敛。yolov8n.pt和yolov8s.pt的区别不只是推理速度两者的特征提取能力差距明显。鱼类疾病检测的病灶区域往往很小n 版本在浅层特征图上容易把注意力全放在鱼的整体轮廓上忽略体表细节后期 mAP 涨不上去。我一般直接用yolov8s.pt144 层的结构对病灶纹理有足够的分辨能力。如果你不想用 COCO 预训练权重还有一个更贴合的选择先用yolov8s-cls.pt或yolov8l-cls.pt做一次全鱼分类的微调把分类模型学到的鱼类体表特征作为检测模型的初始权重。不过这个方案需要额外的一轮训练时间而且对数据量有要求属于进阶玩法新手先复现yolov8s.pt的标准流程即可。训练启动后ultralytics 会在runs/detect/train目录下持续输出训练日志和可视化结果。这个目录是整个训练过程的黑匣子里面有weights/best.pt和weights/last.pt两个权重文件。best.pt是验证集 mAP 最高的那一轮last.pt是最后一轮部署时只用best.pt。4.3 训练中怎么判断模型收敛loss 曲线和 PR 曲线的正确读法YOLOv8 训练完会自动生成results.png这张图里包含 box_loss、cls_loss、dfl_loss 三条曲线的变化。读loss曲线有一个经验法则总 loss 在前 10 个 epoch 快速下降20 个 epoch 后进入缓慢下降区间val 曲线的波动幅度逐渐收窄说明训练正在收敛。如果看到 box_loss 一路下降但 val 的 cls_loss 不降反升这是典型的过拟合信号模型在训练集上把鱼类纹理死记硬背了验证集上泛化不动。解决办法是提前停掉训练回滚到 val loss 最低的那个 epoch 的权重也就是best.pt。这时候patience20的早停策略会自动兜底不需要手动盯。loss 之外还要看 PR 曲线。results.png里的 PR 曲线横轴是召回率纵轴是精确率曲线和坐标轴围成的面积就是 AP。鱼类疾病检测重点关注 AP0.5因为这个值衡量的是框有没有框对位置而不严格限制框的精确度。对塘口检测场景来说框稍微偏几个像素没关系漏检才是致命的所以 AP0.5 比 AP0.75 更有参考价值。判断训练结果还有一个更直观的方法直接看验证集的可视化预测图。yolo predict modelruns/detect/train/weights/best.pt sourcefish_disease/images/val saveTrue跑完这行命令在runs/detect/predict目录里按图片编号看预测结果。重点看两类错误一是鱼体表有病灶但没框出来说明召回率不够二是水体浮游物被误检成疾病说明特征判别力不足。如果这两类错误频繁出现先别急着调参数回去检查标注环节是不是漏标了或框得太随意。5. 避坑清单鱼类检测的五个高频翻车现场5.1 水体背景误检把悬浮物判成白点病的根因在数据现象训练完的模型在真实塘口视频上把水面漂浮的白色碎屑、气泡框成白点病置信度还很高。验证集上 AP 正常一上实景就乱报。原因训练数据的背景太干净。实验室拍的鱼缸照片背景是纯色缸壁模型学到的是白点病 鱼体表上的白色小圆斑 纯净背景的联合特征。真实塘口的水体里有大量类似纹理的干扰物模型把这些当成了白点病。解决从两个方向同时下手。第一数据层面加入负样本也就是没有鱼的、只有水体和悬浮物的图片标注文件为空 txt并放进训练集让模型学到“这些背景不算目标”。第二训练和推理时的增强参数要区分开训练时augmentTrue保持默认hsv_v可以调高一点模拟不同水质下的色彩漂移增强模型对水体颜色的适应力。5.2 小目标漏检病斑只有 20 像素时 imgsz 怎么救现象鱼体的白点病斑在 1080p 视频里只有二三十个像素模型检测不到错过的都是早期病兆。原因YOLOv8 默认imgsz640图片缩放后病斑进一步缩小在特征图里只剩几个像素卷积核很难提取有效特征。这是小目标检测的通病不是模型能力不够是输入分辨率配不上目标尺度。解决推理时把imgsz提到 960 或 1280。代价是推理速度下降但检测精度提升非常明显尤其是体表颗粒状病灶。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcepond_video.mp4, imgsz1280, conf0.3, iou0.5, streamTrue, )这个推理脚本里imgsz1280是关键参数它对小目标的召回率提升能到 15% 以上。conf0.3把置信度阈值放低宁可多报几个疑似框不要漏掉早期病斑。streamTrue表示按生成器方式逐帧处理视频避免一次性把全部帧加载进内存直接撑爆。如果imgsz1280之后检测速度掉到不可接受可以用切片推理把原图切成四块分别检测再合并结果这是后话了。5.3 显存 OOMbatch 和 cache 的取舍关系现象训练启动后几秒钟报CUDA out of memory然后进程被杀掉。原因显存超限。ultralytics 默认配置偏向高性能场景batch16加上cacheram图片在内存里解压成大数组后一次性塞进显存计算显存小的卡自然撑不住。解决batch降到 8 或 4cacheram改成cacheFalseworkers降到 2。这样做的代价是训练速度慢一些但至少能跑通。1660 Ti 显存是 6 GBbatch8 能稳跑batch16 要看运气如果是 4 GB 显存直接 batch4。5.4 标注框偏移透明鱼鳍和黏液导致的边界漂移现象训练完的权重在做推理时预测框全部偏向鱼的背鳍方向框和病灶中心始终有错位。原因鱼鳍是半透明的在图片里颜色梯度变化小。标注员在拉框时经常把鱼鳍也框进去或者因为黏液反光把框的边界拉到了鱼体外侧。模型学到的是“框就是比病灶大一圈”的偏移模式推理时也跟着偏移。解决重新检查标注框的边界确保框的四边紧贴病灶主体不包含半透明鱼鳍和反光黏液区域。这不是算法问题是标注规范问题只能人工返工。建议在 LabelImg 里开十字线辅助对齐把框边贴在病灶最外沿的清晰像素上。5.5 推理掉帧视频流检测时 batch 和线程的另一种玩法现象用摄像头实时流做检测模型的 FPS 只有个位数画面明显卡顿完全没法实际用。原因默认的推理方式是一帧一帧地处理每一帧都跑完整的前向传播单帧延迟累加起来就卡了。另外opencv 读取视频帧的线程和检测线程如果不在一条高效路径上CPU 瓶颈会拖垮 GPU 的喂数速度。解决用队列把视频读取和模型推理解耦读取线程只负责抓帧推理线程只负责检测。import cv2 from ultralytics import YOLO from queue import Queue from threading import Thread model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(rtsp://192.168.1.100/stream) frame_queue Queue(maxsize10) def read_frames(): while True: ret, frame cap.read() if not ret: break if frame_queue.qsize() 10: frame_queue.put(frame) Thread(targetread_frames, daemonTrue).start() while True: if not frame_queue.empty(): frame frame_queue.get() results model.predict(frame, imgsz640, conf0.3) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) label r.names[int(box.cls[0])] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(fish_disease_detection, frame) if cv2.waitKey(1) 0xFF ord(q): break这段脚本把视频读取放到了独立线程里frame_queue作为缓冲区检测主循环只从队列取帧IO 等待被消除。maxsize10限住积压避免读取线程把内存吃满。model.predict(frame, imgsz640)直接传入 numpy 数组不需要先写临时文件。队列积压时有两条路加入时间戳判断处理不过来时跳过旧帧或者把imgsz降到 480 换取速度。鱼类疾病检测对连续帧要求不高隔 2 秒能出一帧有效结果就行。6. 从模型到可用的检测端置信度阈值、报警逻辑与边缘设备部署6.1 自动报警逻辑别把所有疑似都推给养殖户模型能出框只是第一步实际使用中真正值钱的是报警逻辑。白点病早期可能只有一条鱼身上有几颗白点这时候推送报警说“白点病爆发”养殖户去看了一圈会觉得系统在狼来了。我这里的做法是引入置信度阈值和连续帧确认机制。先给每个类别单独设置信度阈值而不是全局统一。白点病在验证集上的置信度普遍偏低设成conf0.3没问题烂鳃病特征明显可以提高到conf0.45。做法是在 yaml 里给每个类别指定阈值或者简单一点在推理脚本里按class_id做一次映射过滤。再用连续 3 帧都检测到同类病灶才触发报警条件单帧误检直接忽略掉这能在很大程度上滤掉水体悬浮物的干扰。from collections import deque detect_history deque(maxlen5) def alarm_decision(boxes, frame_id): disease_hits {} for box in boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) disease_hits[cls_id] max(disease_hits.get(cls_id, 0), conf) detect_history.append((frame_id, disease_hits)) stable_count {} for _, hits in detect_history: for cls_id in hits: stable_count[cls_id] stable_count.get(cls_id, 0) 1 alerts [c for c, cnt in stable_count.items() if cnt 3] return alerts这段逻辑的核心是deque(maxlen5)只保留最近 5 帧的目标类别信息超过 5 帧的旧数据自动丢弃不会让历史帧堆积在内存里。stable_count统计每个类别在历史帧中出现的次数达到 3 次才认为稳定检出。报警信号输出到钉钉机器人、企业微信机器人或者短信网关这是部署环节里最简单的接入方式把报警文本拼好之后直接发到 webhook 地址即可。6.2 部署到 RK3588轻量化剪枝和量化踩的坑模型在 PC 上跑通了不代表现场能用。养殖场的实际部署环境通常是边缘设备RK3588 是当前性价比很主流的选择但它的 NPU 不认 PyTorch 模型需要把权重转成 RKNN 格式。这里有几个坑必须先说清楚。RKNN-Toolkit2 的版本和 RK3588 的 NPU 驱动要匹配版本对不上时会报E RKNN: Cannot find rknn_server。解决方法很土但有效pip install rknn-toolkit2和板端的rknn_server要同时更新到同一版本号。转换时需要先 ONNX 再 RKNN中途会做 INT8 量化量化这一步的精度损失在鱼类检测上会吃掉小目标的召回率。量化时要在工具里提供量化数据集就是从训练集里随机选几十张覆盖不同水质、光线的图片让 NPU 算子统计激活值的分布范围。我见过有人直接拿预训练权重转 RKNN 不管量化数据结果白点病的 AP 从 0.85 掉到 0.3。合理做法是转之前先用验证集跑一遍 INT8 模型的推理结果跟 FP16 的 baseline 做对比AP0.5 掉超过 3 个点就考虑换更大的校准数据集。6.3 验证系统的最后一公里线上跑 72 小时的决策模型部署完后要做一个持续至少 72 小时的灰度验证而不是跑几张测试图就宣布验收。把检测端架在真实塘口的摄像头边上记录每一帧的检测框、置信度和报警事件同时让养殖户记录实际发病情况对比两者的重合度。重点看两个指标报警灵敏度也就是真实发病时系统有没有在 48 小时以内给出报警假警率也就是一个没有发病的周期内系统误报了几次。这两个指标合在一起才能回答“这套系统到底值不值得用”。模型本身没有绝对的好坏只有贴合实际场景的检测系统才有价值。我自己在这条路上的最大教训是第一版模型花了两周调参数据集里的图片全部拍自一个池子结果换一个塘口水域就翻车。后来把数据采集范围扩大到不同的养殖周期和水质条件重新标注、重训才真正解决了模型的泛化问题。鱼类疾病检测的难点从来不在 YOLOv8 本身而是在对数据边界的理解和对场景的敬畏。希望这些步骤和踩过的坑能帮你少走一段弯路早点把塘口里的病灶变成屏幕上明确可见的红框。本文还有配套的精品资源点击获取
返回列表