先说一个我实际见过的场景:某水产基地的师傅每天巡塘,靠肉眼观察鱼群,等发现白点、烂尾、烂鳃的时候,往往已经进入中后期,处理起来死亡率能冲到三成以上。后来我帮他们搭了一套基于深度学习图像识别的鱼类病害早期筛查系统,核心就是用YOLO目标检测框架,配合一套规范的YOLO格式数据集,把“看鱼”这件事交给摄像头和模型。这套系统不是实验室里的摆拍项目,而是能在养殖现场稳定跑的工程方案。
这篇文章我打算把整个项目的链路完整拆开:从为什么需要早期筛查,到数据怎么采、怎么标、怎么转成YOLO格式,再到模型训练的参数怎么调、推理怎么部署,最后把我踩过的一些坑和排查经验一并写出来。无论你是做水产智能化项目的开发者,还是养殖企业里想引入AI的技术负责人,都可以照着这套思路落地一套简易版本。
1. 项目定位与问题背景
1.1 病害早筛到底解决什么行业痛点
水产养殖里的病害发现机制,绝大多数场景还停留在一个非常原始的状态:靠人、靠经验、靠“看”。经验丰富的老师傅能通过鱼群游动姿态、摄食反应、体表光泽变化判断一个塘口是否出了问题,但这种能力没法批量复制。新手养殖户往往要等病鱼出现明显死亡,才开始排查问题,这时损失已经不可逆。
更麻烦的是,很多常见鱼病的早期病灶非常细小。比如白点病的小瓜虫虫体,体表白点早期直径只有1到2毫米,人站在池边看,基本看不清楚。但摄像头近距离采集、加上模型在每一帧图像上做密集检测,反而能捕捉到这些微小病灶。这就让“早期筛查”从口号变成了可执行的技术方案。
早期筛查系统能带来的直接收益,不是诊断精度那一个指标,而是提前处置的时间窗口。以烂鳃病为例,如果能在病鱼摄食量下降、鳃丝局部充血阶段就给出告警,养殖户可以立刻停食、改底、消毒,治愈率比晚期发现高出很多。系统的价值本质上是把“事后处理”变成“事中干预”。
1.2 为什么选YOLO目标检测而非图像分类
初期我也考虑过更简单的方案:图像分类,也就是给一张图直接判断“有病/没病”。但实际在养殖场景里根本不适用,因为一条鱼有病,不代表整池水体的所有鱼都有病。养殖户最关心的是“哪条鱼、什么部位、哪种病”,而不只是一个笼统的概率。
目标检测能同时输出目标的位置框、类别和置信度,天然适合“病鱼定位+病害分类”的需求。检测框的坐标还可以用来统计发病率,比如一个池塘画面里检测出多头白点病鱼,占画面鱼群总数的比例是否在上升,这是分类模型做不到的。
选YOLO而不是Faster R-CNN或SSD,主要原因是工程效率。YOLO系列从v5到v8,把训练、导出、部署的链路做得非常完整,社区生态成熟,预训练权重丰富。对养殖这种需要快速落到边缘设备、推理资源有限的场景,YOLO单阶段检测的速度优势是决定性的。实测下来,同样精度水平下YOLOv8s的推理速度比两阶段模型快一个量级,更适合摄像头多路并发。
2. 数据准备与YOLO格式数据集构建
2.1 图像采集的注意事项与样本规划
数据是整个系统的地基。模型能不能识别早期细微病灶,很大程度上取决于你在采集阶段有没有覆盖足够多变的现场环境。我建议按照这几条原则来做图像采集规划。
第一,分辨率不能低。水下摄像头至少1080p,有条件直接上2K。因为早期的白点、鳃丝出血区域面积很小,分辨率不够的话,标注和检测都无从谈起。第二,场景要足够杂。晴天、阴天、夜间补光、上午逆光、下午顺光,不同水质的清水塘、肥水塘,都要采。第三,同一个塘口不要只拍一个固定角度,建议将摄像头架在投料区、塘边浅水区、增氧机附近这些鱼群密度高的位置交叉采集。
样本量怎么规划更合理?我见过不少团队迷信“样本越多越好”,结果标了大量重复画面,模型效果并不好。更有效的做法是:初期每个病害类别先收集500到1000张包含目标的图像。如果某些类别实在难采,比如水霉病在低温期才出现,就可以先靠数据增强和迁移学习撑住,后续再在流行期补数据。需要说明的是,这类补充方式是基于常见实践的经验值,实际效果要按你的病害类型和场景来验证。
还要注意一个细节:不要只拍静态图。鱼类一直在游动,拍视频后按帧抽图,能拿到更丰富的姿态变化样本。我用过的抽帧策略是每秒抽2到3帧,同一个目标在连续帧里会形成不同的角度、不同的遮挡状态,相当于免费扩充了数据多样性。
2.2 YOLO格式标注规范与工具选择
数据标注的规范程度,决定了训练过程顺不顺。YOLO格式看起来很简单,但很多人第一版标注就翻车。先说明一下这种格式:每一张图片对应一个同名txt文件,每行表示一个目标,内容包括类别id、归一化后的目标中心点x坐标、中心点y坐标、目标宽度、目标高度。比如下面这行:
0 0.5234 0.3618 0.1245 0.0876表示类别0,目标中心在图片横向52.34%、纵向36.18%的位置,框宽占图片宽度12.45%,框高占图片高度8.76%。坐标全部是0到1之间的归一化数值,不需要关心图片原始像素尺寸。
标注工具我实际用下来比较顺手的是X-AnyLabeling和LabelImg。X-AnyLabeling支持YOLO格式导出,还有辅助标注功能,能显著降低人工成本。LabelImg虽然老,但胜在稳定,适合大批量流水线标注。
关于病害类别定义,要重点提醒:类的粒度不是越细越好。类别划分太细,比如把烂尾按轻、中、重度分成三类,标注一致性很难保证,模型也容易混淆。建议第一版控制在6类以内,我实际用的类别是:白点病、烂尾病、烂鳃病、水霉病、赤皮病、健康鱼。健康鱼单独作为一类,是为了让模型学会区分“正常个体”,避免把所有鱼都当成病鱼候选。
2.3 数据增强与类别不平衡处理
标注完第一版数据集,先别急着训练,先看一眼各类别的框数量。水产病害数据天然存在严重的不平衡问题——健康鱼框可能有两万多个,白点病框可能只有三百个。这时候如果不处理,模型的损失函数会被大头类别主导,白点病基本学不出来。
对付不平衡,我按三步走。第一步是过采样,把少类别的图像在训练集里重复若干次。第二步是给损失函数加类别权重,让模型对少数类别犯错付出更大代价。第三步是针对性数据增强,比如对含病斑的小图多做一些锐化、对比度提升,让细微病灶更突出。
YOLO自带的Mosaic增强是水产场景的利器,它把四张图拼接成一张训练图,既能增强模型对“鱼群密度变化”的适应能力,又相当于在每次迭代里变相增加了batch内的样本多样性。在此基础上,我还会手动加一层针对水面环境的增强,比如随机亮度抖动、模拟水面波纹扭曲、加点噪声模拟浑浊水体。这些增强方式不需要额外写复杂代码,在训练配置里给hsv_h、hsv_s、hsv_v设置合适的随机扰动即可。
最后是数据划分。一定要保证验证集和测试集不能和训练集来自同一段视频的连续帧,否则模型严重过拟合。建议按视频片段划分,而不是按单张图随机划分,按8:1:1分成训练、验证、测试。
3. 模型训练与参数调优
3.1 模型选型:YOLOv5还是YOLOv8
鱼病检测属于小目标检测任务,对模型的选择首先考虑推理设备。如果部署在Jetson Orin Nano这类边缘设备上,我推荐YOLOv8s,精度够用、速度较快。如果纯做云端推理、算力充足,可以考虑YOLOv8m或YOLOv8l,精度上限更高。YOLOv5s也是不错的备选,它的生态成熟、导出TensoRT方便,但新项目我一般直接用v8,因为内置的损失函数和C2f结构在同等参数下精度表现更好。
从我的实测经验看,一个简单的对比可以说明问题:同样用640输入、训练150个epoch,YOLOv8s在自建鱼病数据集上的mAP@0.5能做到85%左右,而YOLOv5s大约在81%左右。差距主要来自小目标召回率。如果你没时间调参,直接选YOLOv8s作为默认基线是稳妥的。
3.2 训练配置与超参数解构
训练前需要准备一个数据配置文件,通常是一个yaml文件。下面是我常用的配置:
train: ../datasets/fish_disease/train/images val: ../datasets/fish_disease/val/images test: ../datasets/fish_disease/test/images nc: 6 names: ["white_spot", "tail_rot", "gill_rot", "saprolegniasis", "red_skin", "healthy"]训练命令很简单,官方仓库里直接跑:
yolo train data=fish_disease.yaml model=yolov8s.pt epochs=300 imgsz=640 batch=16 device=0这里有几个关键参数要理解它的含义。epochs设300,不要设成几十个,因为病害特征微弱,需要足够多轮次收敛。batch取决于显存,16G显存跑YOLOv8s的640输入,batch可以到16。imgsz设640,太小会丢失细小病灶信息,太大会拖慢训练速度且容易过拟合。
初始学习率默认是0.01,配合cosine退火策略在训练后期逐步降低学习率,让模型微调到位。weight_decay我建议保持默认0.0005,防止大模型过拟合。还有一个容易被忽略的参数是patience,也就是早停耐心值,我习惯设为30,避免模型在验证集指标停滞时还一直空跑浪费算力。
训练过程中要盯住两组曲线:一组是loss曲线,包括box_loss、cls_loss、dfl_loss,如果训练loss在降但验证loss不降反升,说明过拟合了,需要加大增强或降低模型复杂度。另一组是验证集上的mAP和precision/recall曲线,如果recall长期低于precision,说明漏检严重,要优先提升recall。
3.3 评估指标与迭代策略
很多初学者只盯着mAP一个指标,这是不够的。目标检测的完整评估要看mAP@0.5、mAP@0.5:0.95、precision、recall、F1-score,外加混淆矩阵。mAP@0.5指的是IoU阈值为0.5时的平均精度均值,行业里最常用;mAP@0.5:0.95更严格,反映框定位精度,但对水产场景来说,框稍微偏一点并不影响告警,所以不要求太高。
我实际迭代时的策略是这样的:先保证recall达标,再优化precision。理由很简单,漏检一条病鱼比误报一条正常鱼代价更大。漏检代表病害蔓延风险持续,误报最坏情况是养殖户多看一眼,损失可控。所以模型训练到后期,如果recall低于90%,我会把推理置信度阈值调低到0.2左右,牺牲部分误检率换召回。
混淆矩阵是排查类别混淆的核心工具。我遇到过明明没有烂鳃的鱼,模型总是报成烂鳃,看混淆矩阵才发现,主要错误来源是把“鳃盖微张的正常鱼”误判为烂鳃。解决办法不是盲目加数据,而是专门去采集正常鱼鳃盖微张的画面作为难例样本,喂给模型纠正。这一步很关键,建议每个类别都做一轮难例分析。
4. 推理部署与系统联动
4.1 边缘部署硬件与加速方案
训练完的权重,最终要跑到塘口的摄像头旁边。常见的部署路线有两条:一条是普通网络摄像头+后端GPU服务器集中推理,适合塘口数量多、机房环境好的场景;另一条是边缘盒子,直接在塘口完成推理,只上传告警图片和检测结果,适合网络不稳定、带宽有限的养殖场。
边缘设备的选择上,我推荐目前性价比比较高的Jetson Orin Nano。把训练好的pt权重转成TensorRT的engine格式,用FP16精度推理,640输入下实测检测帧率在30毫秒左右每帧。如果对帧率要求不高,树莓派4B加Coral TPU也能跑,但精度会有损失,前期调试不推荐。
模型转换这一步不建议跳过。TensorRT引擎文件带来的加速效果非常明显,实测比直接跑PyTorch模型快3到5倍,功耗也低得多。转换时优先用FP16,INT8虽然更快但需要校准数据集,在鱼病检测这种强调小目标精度的场景里,INT8量化容易把细微病灶的特征压掉,我不太推荐。
4.2 实时检测流程与告警规则设计
工程上的检测流程不是“模型拿到每一帧就出结果”,而是有一个流水线:视频流接入、按需抽帧、预处理、模型推理、结果过滤、告警判定、数据落库。摄像头数量少时,可以每路摄像头每秒处理1帧;摄像头数量多时,采用轮询策略,比如30路摄像头分三组,每组每两秒处理一轮。
抽帧之后的推理代码框架大致是这样的:
from ultralytics import YOLO import cv2 model = YOLO("fish_disease_v8.engine") cap = cv2.VideoCapture("rtsp://camera_ip/stream") while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.25, iou=0.45, imgsz=640, verbose=False) # 解析并记录检测框、类别、置信度 for r in results: for box in r.boxes: cls = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(cls, conf, xyxy)告警规则的设置是减少人工骚扰的关键。两边我之前都犯过:阈值太高漏报,阈值太低天天被假警报轰炸。最后确定了一套组合规则:单帧置信度超过0.3、连续5帧中至少3帧检出同类别病害、且在画面内占比超过1%,三个条件同时满足才触发告警。这套规则部署后,误报率大约下降了60%。
4.3 告警联动与养殖处置结合
告警不能只停在“哦,模型说有病”,要让养殖户能立即行动。我的做法是:每次告警生成一条结构化记录,包含塘口编号、摄像头编号、检出类别、置信度、当前画面截图、连续检出帧率变化趋势。这些信息推送到后台管理小程序上,养殖户看到截图就能判断是不是误报,不用专门跑塘口确认。
从处置角度,系统还应该提供一个“发病塘”隔离提醒和用药建议接口。比如检测到白点病,系统标注的位置信息和时间规律,能辅助养殖户决定是否分池、升温或者使用特定制剂。当然,AI系统不建议直接输出治疗处方,而是把数据做成现场决策的依据。
5. 实际应用中的坑与应对
5.1 误检重灾区:气泡、饵料颗粒、反光斑
在真实塘口部署后,最大的挑战不是模型精度不够,而是干扰物太多。鱼塘里漂浮的饵料颗粒、增氧机打起的气泡、水面的太阳反光,在模型眼里都可能是“白点病”或者“赤皮病”。这个我在项目初期被搞得焦头烂额。
最有效的办法不是让模型硬扛,而是从源头减少干扰。架设摄像头时尽量用低角度斜向入水,减少水面反射;投料期间暂停检测,因为此时饵料颗粒大量飘浮,误报率会飙升;增氧机附近的气泡密集区域,可以在预处理阶段用背景差分把静态气泡滤掉,只保留运动目标。这里要说明的是,这些工程技巧没有绝对通解,需要根据你自己塘口的水质、设备和光线条件去调整。
另一个有效手段是给模型增加负样本。我专门跑了好几个塘口,采集了大量不带病害但含有气泡、饵料、反光、水草碎屑的画面,标注为“背景干扰”类别。这样模型学会了区分“这堆东西只是杂物,不是病灶”,误报率又降了一个台阶。
5.2 漏检成因分析
漏检比误检更讨厌,因为病害在蔓延而你不知道。我复盘过几十个漏检案例,发现主要集中在三类情况:病灶面积太小,鱼群重叠导致病斑被遮挡,游速过快导致目标模糊。
针对小目标漏检,最直接的手段是增大网络输入尺寸,把imgsz从640提到960,实测对白点病小目标的recall能提升将近十个百分点。代价是推理耗时变长,边缘设备压力变大,需要评估实际场景。如果提升输入尺寸解决不了,再用SAHI切片方式,把大图切成小图分别推理后再合并结果,对小目标检测非常有效。
鱼群重叠的问题,光靠检测模型解决不了,因为一张图里能看到的只是表层鱼。我的做法是加一个时间维度的降噪模块:同一个目标被连续多帧检出,且位置变化符合鱼类游动轨迹,才认定为可靠目标。这样就算某一帧被遮挡漏检,前面的帧还能兜住。
5.3 数据迭代与版本管理
模型上线不是结束,而是持续迭代的开始。我强调过很多次:数据集的版本管理要像代码版本管理一样规范。每次误检、漏检的截图,都要定期回收到数据集里重新标注、重新训练。我见过有的团队三个月不更新模型,误检率一直降不下来,就是因为没有建立闭环。
具体的做法:在系统后台加一个“误报反馈”按钮,养殖户点一下,就把当时的画面和检测结果自动入库。每两周人工筛选一次,把典型难例合并回训练数据,重新训练一轮并对比新旧模型在验证集上的指标。这个过程看起来很繁琐,但它是模型效果持续变好的基本功。
6. 常见问题速查表
| 问题现象 | 可能原因 | 排查建议 |
|---|---|---|
| 模型把气泡误报为白点病 | 负样本不足;水面反光干扰 | 采集气泡/饵料/反光负样本;摄像头角度调低;投料时暂停检测 |
| 白点病小目标漏检严重 | 输入分辨率低;模型容量小 | imgsz提到960;换用YOLOv8m;必要时用SAHI切片推理 |
| 烂鳃病和健康鱼混淆高 | 鳃盖微张被误判;类别定义不清晰 | 补充健康鱼鳃盖微张难例;检查标注框是否规范统一 |
| 训练loss下降但验证mAP不动 | 训练集严重过拟合;数据划分存在泄漏 | 降低模型复杂度;增加数据增强;按视频片段划分数据 |
| 推理速度达不到实时 | 未量化;设备性能不足 | 导出TensorRT引擎;FP16推理;降低imgsz;减少并发路数 |
| 夜间检测效果差 | 光线不足;夜视模式失真 | 增加红外补光;采集夜间样本用于训练 |
从我个人的实操体会来说,这类水产AI项目成败的关键往往不在算法本身,而在从数据、标注、训练到部署的闭环管理是否走得通。做这个项目时,我明显感觉到最大的成本不是买GPU,而是把标注质量管好、把现场误报场景持续收集回来。还有一条建议想分享给后来人:所有原始图片和标注的中间格式一定要保留好,别只存YOLO的txt文件,不然以后想换标注格式或者加类别,回炉重造的痛苦会让你后悔为什么没有多存一份JSON备份。这套系统的扩展空间也很大,后面如果加入水温、溶氧量这些环境传感器数据,让视觉检测和理化指标互相印证,病害预警的准确性还能再上一个台阶。