简介:基于Python和YOLOv8的鱼类疾病检测系统,面向水产养殖从业者、计算机视觉学习者和算法工程师,借助深度学习实现鱼类疾病自动化识别,解决人工巡检效率低、漏检率高的问题。系统覆盖22种鱼类及其常见病症(如出血、眼部缺陷、鳍部缺陷、溃疡等),支持图片、视频和摄像头实时检测,检测结果可自动保存并导出为Excel,同时提供Web前端界面展示,便于日常监测与预警。压缩包内共24个文件,含4个Python脚本,分别对应训练、预测、验证及界面交互;19张PNG图片为运行效果与检测示意图,另有README说明文档,整体仅2.26MB,结构清晰易于上手。目前已有90人浏览学习。下载后既可快速复现完整检测流程,也可参考包内提到的70余种YOLOv8创新改进点优化模型精度,配合训练教程和数据集可训练自定义模型,适合作为鱼类检测方向毕业设计或实际项目落地的参考方案。
1. 鱼病检测的痛点,正好是YOLOv8该干的活
水产养殖里,鱼到底生了什么病,尤其早期病斑、烂鳍和寄生虫感染,肉眼判断极不可靠。一个五万尾的池塘,人工巡检一遍两小时起步,等肉眼能确认发病,往往已经传染开。基于Python和YOLOv8的鱼类疾病检测系统,做的就是把这套人工巡检变成目标检测流程:用标注好的鱼体图像训练YOLOv8,得到能框出病灶位置的权重,再对批量图像或实时视频帧输出类别与置信度。这套源码不是一条固定命令,而是一条从数据标注到模型训练的完整链路,适合正在做水产智能化、病害预警的开发者,也适合想拿YOLOv8跑通目标检测全流程的人。下面按实际做这类项目的路径展开:先搞定环境和数据,再训练和调参,然后评估与部署,最后收在细粒度病斑检测的几个优化手段上。
2. YOLOv8环境配置与鱼类图像数据集的组织方式
2.1 Python环境搭建与ultralytics安装
yolov8环境配置是第一个坎。常见做法是用conda建独立环境,避免把系统Python搞乱。Python版本选3.10或3.11都行,ultralytics从8.0.x到当前版本对这两个版本兼容性都不错。如果机器有NVIDIA显卡,先确认CUDA版本,再装对应PyTorch。
# 创建独立环境,避免污染系统python conda create -n fish-yolo python=3.10 -y conda activate fish-yolo # 安装GPU版PyTorch,按自己CUDA版本调整cu118/cu121 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装YOLOv8核心库 pip install ultralytics安装完验证一下:
python -c "import ultralytics; print(ultralytics.__version__)" yolo predict model=yolov8n.pt source=https://ultralytics.com/images/bus.jpg第二条命令会自动下载yolov8n.pt权重并跑一张示例图,能输出推理结果就说明环境通了。要注意PyTorch和CUDA版本必须匹配,否则torch.cuda.is_available()返回False,训练会掉回CPU,速度慢几十倍。验证CUDA可用性用:
python -c "import torch; print(torch.cuda.is_available())"2.2 鱼类图像的采集与标注要点
任何检测系统的天花板在数据质量,不在模型结构。鱼类疾病检测的数据采集有几种来源:养殖场实地拍摄的发病鱼、实验室培养皿内的病鱼特写、以及公开的鱼类图像数据集。每类病害至少准备500张带病图像,才算勉强够训练。标注工具用labelImg最直接:
pip install labelimg labelimg标注时注意三点。第一,病斑区域要框紧密,不要把整条鱼都框进去,YOLO学习的是病灶特征,框得越精准模型越容易区分病种;第二,类别名统一用英文小写加下划线,如white_spot、fin_rot、gill_rot,避免中文路径和类名带来的编码问题;第三,每张图的标注信息是独立的txt文件,文件名与图像名一致。
2.3 从VOC格式到YOLO格式的转换脚本
labelImg默认可以输出YOLO格式,但很多公开数据集给的是VOC的XML标注。写个转换脚本是常见需求:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, class_names): """将VOC格式XML标注转为YOLO格式txt""" tree = ET.parse(xml_path) root = tree.getroot() # 图像宽高是归一化的基准 img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find('bndbox') xmin = int(box.find('xmin').text) ymin = int(box.find('ymin').text) xmax = int(box.find('xmax').text) ymax = int(box.find('ymax').text) # YOLO格式要求:中心点坐标和宽高,全部归一化到0~1 cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, 'w') as f: f.write("\n".join(lines))核心是归一化的计算方式。YOLO不直接存像素坐标,而是存相对图像宽高的比例,这样不同分辨率图像可以统一训练。class_names列表的顺序就是类别ID的映射,后续dataset.yaml里的names顺序必须与此完全一致,这是新手最容易踩的坑。
2.4 数据集目录结构与dataset.yaml配置
yolov8训练自己的数据集,目录结构有固定要求。常见布局如下:
fish-disease/ ├── images/ │ ├── train/ # 训练图像,建议70%~80% │ └── val/ # 验证图像,建议20%~30% ├── labels/ │ ├── train/ # 每张图像对应的txt标注 │ └── val/ └── fish_disease.yaml训练前的核心配置文件是yaml文件:
# fish_disease.yaml path: /home/fish/fish-disease # 数据集根目录绝对路径 train: images/train # 相对path的训练图像目录 val: images/val # 相对path的验证图像目录 names: 0: healthy 1: white_spot 2: fin_rot 3: gill_rotpath建议写绝对路径,相对路径在某些版本的ultralytics里会解析出问题。names从0开始编号,且顺序与标注txt中的类别ID一致。写完后做一次校验:
from ultralytics.data import YOLODataset ds = YOLODataset('fish_disease.yaml', imgsz=640) print(f"训练集大小: {len(ds)}")能正常打印数量就说明数据集没毛病。
3. 用YOLOv8训练鱼类疾病检测模型:从C2f结构到关键参数
3.1 yolov8模型结构中C2f模块的作用
YOLOv8的主干网络里C2f模块替代了YOLOv5的C3。C2f的输入先过一个卷积层,然后拆成两条路径:一条直接传递,另一条经过n个Bottleneck的串行堆叠,每经过一个Bottleneck都会在通道维度上拼接一次。这种结构让梯度回传路径变短且更丰富,同时每个Bottleneck的输出都保留下来,信息流不再是简单的串行压榨。具体到鱼病检测这个任务,C2f带来的直接好处是:鱼身上小尺寸病斑(比如白点病初期只有几个像素的白色颗粒)在浅层特征中更容易被保留,不会因为层数加深而消失。
想可视化YOLOv8网络结构,在Python里画一下:
from ultralytics import YOLO import torch model = YOLO('yolov8n.pt') x = torch.randn(1, 3, 640, 640) y = model.model(x) # 查看C2f结构 for name, module in model.model.named_modules(): if 'c2f' in name: print(name, module)一般会看到三个C2f位于不同下采样阶段,控制着特征图的通道数和分辨率。理解它有助于后面的调参:C2f里的Bottleneck数量多,模型容量大但训练慢、容易过拟合;数量少则反之。自定义模型结构时改yaml文件里的depth_multiple就能统一缩放。
3.2 训练脚本与超参数的选择逻辑
确定网络规模后,写训练脚本:
from ultralytics import YOLO # 加载预训练权重,n/s/m/l/x按数据量和显存选 model = YOLO('yolov8n.pt') model.train( data='fish_disease.yaml', epochs=150, imgsz=640, batch=16, patience=20, optimizer='SGD', lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, augment=True, project='runs/train', name='fish-v1', seed=42 )参数选择背后是有逻辑的,不是随手填:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| imgsz | 640 | 病斑太小时可提到1280,但显存占用翻4倍 |
| batch | 16或32 | 显存不够就减半,别硬上,会OOM |
| epochs | 150 | 鱼病数据量通常几百到几千张,150轮足够收敛 |
| patience | 20 | 连续20轮val指标不涨就早停,省时间 |
| optimizer | SGD | 数据量小时SGD比AdamW泛化好 |
| lr0 | 0.01 | 预训练权重上的常用初始学习率 |
关键理解:patience的意义在于避免后50轮白跑。如果150轮训练在第60轮就停,说明数据简单或者模型容量过大,属于正常。imgsz提升到1280会显著提升小目标检测效果,但训练时间大约翻3倍,先小规模测试再决定。
3.3 用results.csv绘制损失函数曲线
训练过程中最要紧的监控就是损失曲线,训练是否收敛、有没有过拟合都能一眼看出来。ultralytics每次训练都会在输出目录生成results.csv,里面按epoch记录了box_loss、cls_loss、dfl_loss以及各指标。画损失曲线的脚本:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/train/fish-v1/results.csv') # 老版本列名带前导空格,先strip掉 df.columns = [c.strip() for c in df.columns] plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(df['epoch'], df['train/box_loss'], label='train_box') plt.plot(df['epoch'], df['val/box_loss'], label='val_box') plt.xlabel('epoch') plt.ylabel('box_loss') plt.title('Box Loss') plt.legend() plt.grid(True) plt.subplot(1, 2, 2) plt.plot(df['epoch'], df['train/cls_loss'], label='train_cls') plt.plot(df['epoch'], df['val/cls_loss'], label='val_cls') plt.xlabel('epoch') plt.ylabel('cls_loss') plt.title('Cls Loss') plt.legend() plt.grid(True) plt.tight_layout() plt.savefig('loss_curve.png', dpi=200)看曲线的重点是train和val的间距。train_loss持续下降而val_loss在第40轮后开始反弹,就是过拟合信号,回调weight_decay或减小模型到yolov8n。两条线保持同步下降且最终平稳,说明训练状态健康。
3.4 数据不平衡与类别权重
鱼病数据里不同病种的样本数往往差异很大,常见的寄生虫病可能只有几十张图,而健康鱼的标注成百上千。这种不平衡会让模型偏向多数类。常见做法是在yaml文件里用weight字段加类别权重,或者在train参数里调cls损失权重:
model.train( ..., cls=1.5, # 提高分类损失的权重,让模型更重视类别区分 dropout=0.1 # 加dropout缓解少量数据过拟合 )还有一个更实用的做法:对样本少的类别做离线增强,把每张图水平翻转、旋转15度、亮度抖动各生成一张副本,把该类数量补齐到多数类的60%左右。这比单纯调损失权重稳定,因为模型看到的是真实变化后的图像。
4. 模型评估、置信度阈值调优与批量推理
4.1 用val评估模型:mAP、Precision、Recall怎么解读
训练结束后,best.pt保留的是验证集上表现最好的权重。评估命令:
yolo val model=runs/train/fish-v1/weights/best.pt data=fish_disease.yaml输出结果里重点关注这几项:
| 指标 | 含义 | 鱼病场景判断标准 |
|---|---|---|
| mAP50 | IoU=0.5时的平均精度 | 目标明确的大病斑,期望0.9以上 |
| mAP50-95 | IoU从0.5到0.95的平均精度 | 0.55以上算及格 |
| Precision | 预测框中真正是病灶的比例 | 误报高就调高置信度 |
| Recall | 真实病灶中被框出的比例 | 漏检严重就调低置信度,或检查标注 |
只看mAP不够,建议加一条混淆矩阵输出:
yolo val model=runs/train/fish-v1/weights/best.pt data=fish_disease.yaml plots=Trueplots=True会在输出目录生成confusion_matrix.png,一眼看出哪个类别之间互相误判。鱼病场景里最容易混淆的是white_spot和healthy,因为健康鱼身上也有白色鳞片反光。如果这两类混在一起,优先检查标注框是否把非病灶区域也框进去了。
4.2 置信度阈值与NMS参数对漏检误检的影响
模型输出的每个预测框都带一个置信度分数,默认阈值是0.25。实际部署时这个值必须按场景调。漏检严重(真实病鱼没框出来)就降到0.1,宁可多框几个;误报严重(健康鱼被标成病鱼)就往0.4甚至0.5调。写推理脚本时把这几个参数暴露出来:
from ultralytics import YOLO # 加载训练好的最佳权重 model = YOLO('runs/train/fish-v1/weights/best.pt') # 对整张图像批量推理 results = model.predict( source='test_images/', conf=0.35, # 置信度阈值,低于此值的框直接丢弃 iou=0.5, # NMS的IoU阈值,控制重叠框的合并 save=True, save_txt=True, line_width=2 ) for r in results: boxes = r.boxes print(f"图像: {r.path}") for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f" 类别{cls_id} 置信度{conf:.2f} 坐标{xyxy}")iou参数的语义要理解准确:NMS阶段两个框的重叠程度超过0.5就合并成一个,值调大(比如0.7)允许更多重叠框保留,适合病斑密集的场景;值调小(比如0.3)则只保留最精准的框。鱼病图片里如果多条鱼重叠在一起,默认0.5通常表现已经不错。
4.3 导出ONNX做部署推理
训练好的PyTorch权重没法直接扔给生产环境,导出成ONNX是常见第一步:
yolo export model=runs/train/fish-v1/weights/best.pt format=onnx opset=12 imgsz=640导出后使用onnxruntime推理:
import onnxruntime as ort import cv2 import numpy as np session = ort.InferenceSession('best.onnx') input_name = session.get_inputs()[0].name img = cv2.imread('test.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized = cv2.resize(img, (640, 640)) # 归一化并转成NCHW格式 input_tensor = img_resized.astype(np.float32) / 255.0 input_tensor = np.transpose(input_tensor, (2, 0, 1)) input_tensor = np.expand_dims(input_tensor, axis=0) outputs = session.run(None, {input_name: input_tensor}) # outputs[0]是(1, 84, 8400)的tensor,需要后处理ONNX推理比PyTorch快30%到50%,尤其在CPU环境下更明显。8400是640输入下三个检测头输出的锚点总数,84对应4个坐标加80个COCO类别数。如果训练时自定义了类别,这个数字会变为4+N,写后处理时注意别硬编码。
5. 细粒度病斑检测的进阶优化:切片推理与多尺度训练
最后一个环节,也是最值得花时间的地方:鱼病检测里真正难的不是大白斑,而是早期几个像素的小病灶。模型在640分辨率下能看到的细节有限,很多白点病的初期症状就是被下采样抹掉的。
第一个实用技巧是多尺度训练。在train参数里加scale=0.5,模型会在0.5到1.5倍之间随机缩放输入图像,等于变相做了数据增强,让模型见过不同尺寸的目标:
model.train( ..., imgsz=640, scale=0.5, # 随机缩放范围0.5x~1.5x mosaic=1.0, # mosaic增强,4图拼接 fliplr=0.5, hsv_h=0.015, # 色相抖动,适应不同水质光照 hsv_s=0.7, hsv_v=0.4 )鱼生活在不同的水色里,偏绿、偏黄、偏浑浊。hsv增强管的就是这个,让模型不依赖颜色绝对值,而依赖纹理和形状。但注意hsv_h别调太大,否则锦鲤的红色斑纹会被抖动成别的颜色,引入噪声。
第二个技巧是切片推理,处理超大图。养殖场拍的照片常常是3000x4000的全池图像,直接缩放到640会丢失所有小鱼。常见做法是用SAHI这类切片推理库,把大图切成一堆640x640的重叠块分别推理再合并结果:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type='yolov8', model_path='runs/train/fish-v1/weights/best.pt', confidence_threshold=0.3, device='cuda:0' ) result = get_sliced_prediction( 'big_pond.jpg', detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, # 高度方向重叠20% overlap_width_ratio=0.2 # 宽度方向重叠20% ) result.export_visuals(export_dir='sliced_output/')切片重叠的意义:目标刚好落在切片边界时,不重叠会把它切断。20%的重叠比例在精度和计算量之间是合理折中。病斑被切开后,两个切片各含半个目标,NMS会把置信度低的一半去掉,置信度高的另一半保留,总体精度远好于整图缩放。
第三个优化点是用TensorBoard监控训练过程,比盯着终端输出直观得多:
tensorboard --logdir runs/train浏览器打开6006端口,可以交叉查看box_loss、cls_loss、mAP的变化,也能在Images选项卡里看每一轮训练后的预测结果,直接观察病斑有没有被正确框出。数据量不足是鱼病检测最常见的限制。模型再花哨,标注质量差也训练不出可靠结果。建议先拿50张最有代表性的图像做一轮快速实验,确认整个链路没问题再大规模标注;如果小样本上模型已经能把主要病种类别区分开,再扩数据量往往效果更好。
本文还有配套的精品资源,点击获取