前阵子整理鱼缸的时候被一条混进来的鲫鱼搞得头疼原本以为是普通草金结果越养越不对翻图鉴翻到半夜也没确认。后来跟水产养殖的朋友吐槽他说这种“认鱼难”的问题在养殖场更严重品种混养、病鱼识别都靠老师傅肉眼效率低不说还容易漏。聊着聊着我说索性做个能自动认鱼的东西这就是MiroFish的由来——Miro取“mirror/视觉映射”那层意思Fish就是鱼合起来就是用视觉把“看到一条鱼”变成“看懂一条鱼”。MiroFish是一个以计算机视觉为核心的鱼类识别与养殖监测系统。它能做三件事第一拍照或视频实时识别鱼的品种覆盖常见观赏鱼和养殖鱼第二对鱼的健康状况做初步判断比如体表白点、鳍条破损这些明显症状第三接入养殖环境数据结合图像结果做异常告警。适合三类人参考水族爱好者想自动记录缸里的生物水产养殖户想降低人工巡检成本以及刚入门计算机视觉、想找一个完整项目练手的人。这个项目从头到尾的坑我都踩过一轮从数据采集、模型训练到边缘部署每一步都有值得展开说的细节。下面按我实际做的顺序拆开讲。1. 项目设计与技术选型视觉方案怎么从“能跑”变成“能用”1.1 核心思路为什么不做单纯分类而是检测加分类两层最开始我的想法特别简单拿一个分类模型输入一张鱼的照片输出品种名称。但真正上手后发现这条路走不通。真实场景里鱼缸或养殖池的背景特别乱水草、石头、其他鱼全混在一起分类模型会把整张图作为输入很容易被背景干扰。更麻烦的是一条画面里经常同时出现好几条鱼分类模型一次只能给一个整体结果根本没法回答“画面里有什么鱼、各在什么位置”。所以我把方案改成了目标检测加分类的两层结构。第一层用目标检测模型把每条鱼的位置框出来并且顺便给出鱼种类别第二层针对检测框里的鱼体区域再做精细的健康状态判断。这样做的好处很直接检测模型天生就是处理“图里有什么、在哪里”的问题而且能同时输出多个目标健康分类只看检测框内部不受背景干扰准确率明显更高。代价是多一层模型推理但对现在常用的边缘设备来说这个开销完全可接受。1.2 系统架构感知层、智能层、应用层各管什么整个MiroFish分成三层来设计每个层的边界都想清楚了再动手省得后面改结构。感知层负责数据进入。图像方面用的是普通USB免驱摄像头加防水壳三百元左右的经济方案养殖池环境够用条件允许的话也可以用支持RTSP推流的网络摄像头布线更灵活。环境数据靠几路传感器采集水温、pH和溶解氧用ESP32板子做数据汇聚通过Wi-Fi传到后端。传感器这一块不用买太贵的精度满足日常监测就行关键是要稳定。智能层是整个系统的核心跑两个模型。MiroFish-Core用YOLOv8s做鱼种检测MiroFish-Health用MobileNetV3做健康状态分类。推理引擎在边缘设备上执行我一开始用Jetson Nano后来换成树莓派4B也跑得动靠的就是模型轻量化和TensorRT量化那套优化。所有图像结果和环境数据都汇总到后端服务后端用FastAPI写的逻辑很简单接图像结果、接传感器数据、落库、判断是否触发告警。应用层就是给用户看和操作的部分。我做了一个网页大屏展示实时画面和指标曲线另外接了一个企业微信机器人检测到疑似病鱼或者水温超阈值就往手机推消息。告警规则不复杂图像侧连续三帧检测到同一条鱼身上有白点特征就告警环境侧某项指标连续十分钟超出设定范围就告警避免单帧偶然抖动误报。1.3 关键选型模型、硬件和部署方式是怎么权衡的选型这块我反复推翻了三次最后确定的原则是在能满足实时性的前提下优先选社区活跃、迭代快的方案不追求单点性能极限。模型方面选了YOLOv8而不是更重的YOLOv5或Faster R-CNN。YOLOv8在检测精度和速度之间平衡最好而且ultralytics的生态做得很好训练、验证、导出一条命令搞定这对个人项目太重要了。当时也考虑过RT-DETR这种端到端方案但部署资料相对少一旦出问题排查成本高就放弃了。健康分类选MobileNetV3纯粹是因为它在CPU上跑得快树莓派上单张图推理能控制在50毫秒以内比大模型有质的差别。硬件方面对比过几个方案我把经验整理成表格供参考。设备方案图像能力推理速度YOLOv8s成本适合场景Jetson Nano很强支持TensorRT约30-60ms中等养殖场边缘节点树莓派4B够用需量化约100-150ms较低小型鱼缸/个人用户普通PCCPU足够约80-120ms视已有设备原型验证/开发调试云服务器GPU强约10-20ms长期成本高不适合每帧都传部署方式我最终选了边缘部署加中心管理的混合架构。图像推理放在现场设备上只把检测结果和低频率的截图上传到服务器环境数据直接传后端。原因很简单养殖场的网络经常不稳定如果全部依赖云端推理断网就等于系统失效边缘部署即使断网本地告警也能继续工作。代价是现场设备需要定期更新模型我写了一个简单的OTA脚本新模型打包成zip放到服务器设备启动时检查版本决定是否下载。2. 数据是第一步鱼类图像数据集的采集、清洗与增强2.1 数据来源公开数据集、实地拍摄和网络补充怎么搭配做视觉项目的人都懂标注数据是最耗时又最绕不开的环节。MiroFish用到的数据集由三部分拼起来。第一部分是公开数据集。鱼类识别方向有几个经典的公开资源Fish4Knowledge提供了大量水下摄像头拍摄的鱼视频帧DeepFish也有不少带框标注的图片。这些数据质量不错但有个问题——它们的拍摄环境偏向科研用的珊瑚礁区域鱼种和我们常见的观赏鱼、养殖鱼重合度不高。所以我只把它们作为预训练阶段的补充丰富模型对“鱼”这个大类的基本特征理解。第二部分是自己实地拍的这个最关键也最费功夫。我在朋友的水产基地蹲了两天用防水相机拍了草鱼、鲤鱼、鲫鱼、锦鲤、金鱼等几个品种在不同光线、不同角度下的照片大约累计两千多张。还专门在鱼缸前架上手机支架录了半小时视频然后按帧抽取确保同一品种能覆盖游动姿态、身体弯曲、尾部摆动这些动态变化。这种数据对模型泛化能力帮助特别大光靠静态图训练出来的模型遇到游动中的鱼很容易漏检。第三部分是网络图片补充主要是从图片搜索和社交媒体话题里找一些罕见品种的照片。爬下来之后必须人工一张张过因为搜索结果的图片质量参差不齐有些是手绘图、有些是重复图、还有些压根不是目标鱼种不清理会严重拖累训练效果。2.2 标注规范坐标系、框边界和多人协作要注意的细节标注我用的是LabelImg它简单直接输出YOLO格式的txt文件和YOLO系列正好配套。两条经验特别想说。第一标注框一定要贴着鱼身不要图省事把鱼鳍也圈出去太多。鱼游动的时候鳍会张开如果框得太大背景里的水草会被大量包进来模型学到的特征就是“鱼草”的混合特征检测框会变得特别大甚至在鱼游走了还误报。我刚开始偷懒框得松结果mAP一直上不去后来严格贴边标注之后提升非常明显。第二病鱼和健康鱼要分开标。比如白点病最典型的症状是体表和鳍条上密布白色小点检测框会把整个鱼体框住但健康分类模型要的就是“框内有没有病斑特征”。所以我在标注的时候除了给整条鱼一个检测框还会额外用另一个类别标签“ich_spot”把明显的白点区域单独标出来。这样健康分类模型既可以看整鱼框也可以重点关注局部病灶区域判断依据更充分。多人协作标注的时候一定要先统一规则最好写一份简单的标注规范文档把“框要紧贴鱼身”“病斑单独标注”“模糊得完全看不清的小目标直接跳过不标”这些条款写清楚。不然两个人标出来的框形态差很多模型学出来的框回归目标不一致效果会互相拖后腿。2.3 数据增强针对水下场景做的几种特殊处理通用目标检测里常用的增强方法像水平翻转、随机裁剪、HSV颜色抖动、Mosaic拼接这些我都用了。但水下场景有其特殊性必须单独处理。水下图像最明显的问题是偏色。深水区拍出来的照片严重偏蓝绿色浅水区又容易过曝发白。如果在训练数据里完全没有这种样本模型在实际部署时第一帧就会被色彩分布差异打懵。我在增强流水线里加入了两条针对性策略一是随机调整色温让图片在偏蓝和偏黄之间做扰动模拟不同水深的色差二是对亮度做较大范围的随机变化从暗光环境的欠曝光到强光直射的过曝光都覆盖到。这个调整做完之后模型在真实鱼塘里的识别率提升了一个档次。另外水下环境里鱼体反光很常见鳞片在灯光下会闪出高光区域。这个没法靠普通颜色增强模拟我用了Cutout增强随机在图像上挖掉一小块区域强迫模型不要过度依赖某个局部像素而是学会看整体形状和纹理特征。这对处理鳞片高光导致的误判很有帮助。具体增强策略我整理成了下面这个表格。增强方法作用使用强度水平翻转增加鱼体朝向多样性0.5概率随机裁剪/缩放模拟不同拍摄距离0.3概率HSV色温扰动模拟不同水深偏色0.5概率亮度扰动模拟光照强弱变化0.4概率Mosaic拼接增加单图目标数和背景多样性每批次开启Cutout抑制高光/遮挡干扰0.2概率3. MiroFish核心实现模型训练、推理与部署全流程3.1 训练环境与数据集配置环境方面我用的是一台带RTX 3060的台式机训练YOLOv8s绰绰有余。软件版本是Python 3.9、CUDA 11.8、PyTorch 2.0.1ultralytics库建议用8.0以上的版本。如果你手里没有NVIDIA显卡用CPU也能训小数据集就是时间会拉长很多一个100轮的小模型可能得跑四五个小时勉强能接受。数据集目录结构按YOLO规范建好这个结构是硬性的不能乱改。dataset/ ├── images/ │ ├── train/ # 约80%的图片 │ └── val/ # 约20%的图片 └── labels/ ├── train/ # 与images/train一一对应的txt └── val/ # 与images/val一一对应的txt然后写一个数据集配置文件告诉ultralytics去哪里读数据、有几个类别、类别名是什么。我的mirofish.yaml长这样。path: /home/mirofish/dataset train: images/train val: images/val nc: 5 names: 0: koi 1: goldfish 2: tilapia 3: grass_carp 4: ich_spot这里有个经验把ich_spot作为检测类别而不是单纯分类标签训练出来的模型既能检测鱼种又能在同一个框里输出病灶位置后面做告警判断特别方便。3.2 训练与调参参数表、完整命令和我的调参过程训练命令其实很简洁ultralytics封装了大量细节。我第一次训练时用的命令是这个。yolo detect train \ datamirofish.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.005 \ momentum0.937 \ weight_decay0.0005 \ warmup_epochs5 \ device0关键参数我逐个解释一下。imgsz640输入图像尺寸。越大越能保住小目标的细节但显存占用和推理时间也跟着涨。我的数据集里鱼的尺寸还算大640够用。batch16在3060上刚好占用约10GB显存再多就会OOM。lr00.005初学率。因为是从yolov8s.pt官方预训练权重开始迁移学习学习率不宜太高否则容易把预训练学到的特征冲掉。warmup_epochs5前几个epoch用较低学习率热身让模型参数先稳定一下再进入主训练阶段能有效防止前期loss爆炸。训练过程中我习惯看两个指标train/loss持续下降说明模型在学东西val/box_loss如果出现先降后升说明开始过拟合可以提前停止或者加大数据增强。我第一次训练到第80轮的时候发现val loss开始反弹于是把epochs降到90加了一点Mosaic概率最后效果反而更好。别等到100轮跑完才看结果训练中每隔十轮瞄一眼日志是最稳妥的。验证集评估命令也一并给出。yolo detect val \ datamirofish.yaml \ modelruns/detect/train/weights/best.pt3.3 推理接口与边缘设备部署训练完输出的best.pt可以直接用在Python环境里推理下面的代码是MiroFish-Core的服务端调用核心。from ultralytics import YOLO model YOLO(best.pt) results model.predict( sourcefish.jpg, conf0.45, iou0.5, imgsz640, saveTrue, classes[0, 1, 2, 3], # 只检测鱼体类别 verboseFalse ) for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) print(f{model.names[cls_id]} 置信度{conf:.2f} 位置({x1},{y1},{x2},{y2}))实际部署到边缘设备时我做了两步优化。第一步是把模型导出为TensorRT格式。Jetson上跑PyTorch原版推理速度太慢导出之后提速非常明显。导出命令是这个。yolo export \ modelbest.pt \ formatengine \ device0 \ halfTrue第二步是处理视频流。摄像头通过RTSP推流到设备OpenCV直接读视频流通常会很卡因为底层对RTSP的解码优化不好。我换用极低延迟的推流方案把视频解码放在另一个线程推理线程从队列里取帧这样两个环节互不阻塞实测帧率从不到5fps提升到了15fps左右。4. 落地过程中最常见的6个问题与排查思路4.1 水下图像偏色导致误检率飙升第一次拿到鱼塘实地视频时我直接被检测效果震住了模型把一大堆水草和石头误检成鱼原因是鱼塘的水偏黄绿色整张图的颜色分布和训练集里的鱼缸场景差太多。模型把“颜色接近鱼体”的区域都当成了目标。解决办法分两步走。预处理阶段加了一个灰度世界自动白平衡算法把每个通道的均值拉回接近相等颜色偏色问题立刻缓解。训练阶段给数据增强增加了色温扰动让模型在训练时就看到不同色调下的鱼而不是只见过干净清澈的鱼缸图。这两步做完鱼塘场景下的误检率降低了一半以上。4.2 鱼体遮挡和游动模糊导致漏检鱼在水里是持续游动的两条鱼擦身而过、尾巴交叠是常态。单帧检测在这种场景下经常丢目标。我的处理办法是多帧追踪加投票。用ByteTrack做跨帧关联把同一帧里检测到的鱼赋予一个稳定ID连续多帧跟踪后对这个ID的所有检测结果做一个置信度投票只有被多数帧都确认存在的目标才进入最终结果。单帧里哪怕有一两帧因为遮挡没检测到追踪算法也能靠前后帧的位置预测把它补回来。这个方案比单纯提高单帧检测效果要稳得多。4.3 小目标鱼苗识别不出来鱼苗阶段个体小在640分辨率的输入图里可能只有十几像素YOLOv8s对小目标的召回率确实比较弱。我的做法是引入SAHI切片推理。推理时把原图切成2x2或4x4的重叠小块分别送入模型检测再把结果合并回全图坐标。这个方法的原理很好理解相当于把原来模糊的小目标放大到模型更容易识别的大小。代价是推理时间增加两三倍但鱼苗检测场景对实时性要求没那么高一次巡检慢几秒完全能接受。4.4 训练Loss不收敛、精度忽高忽低有阵子训练出来的模型在验证集上AP值一直跳来跳去同一个验证集每次测结果都不一样。后来定位到两个根因。第一是训练集里混进了不少错误标签有的是标注框偏移有的是类别标错。这种噪声会让模型无所适从。解决方法是把训练预测结果里置信度低但置信度高的样本挑出来人工复查筛掉了大概几十张问题图重新训练后收敛稳定很多。第二是学习率设置偏高训练后期loss在最优值附近来回震荡。我把lr0从0.01降到0.005并启用cosine学习率调度让学习率在训练后期平滑衰减震荡问题就消失了。如果你遇到类似问题建议先检查标签再动参数标签问题才是根源。4.5 边缘设备推理延迟太高在树莓派上跑FP32的YOLOv8s单帧推理要400毫秒以上完全没法实时看画面。优化下来我只用了两招就把延迟压到了可接受范围。第一招是把模型量化为INT8。树莓派CPU对INT8矩阵运算有硬件加速量化后推理速度大约提升3倍。第二招是降低输入分辨率从640降到416虽然小目标识别能力弱了一点但对于成鱼这种中等尺寸目标影响不大。两步叠加后单帧推理降到120毫秒左右配合多线程视频解码肉眼基本感觉不到卡顿。4.6 少数鱼种样本太少模型总漏锦鲤和草金鱼长得像我的数据集里草金鱼图片明显偏少导致模型经常把草金鱼认成锦鲤。这种类别不平衡问题在真实项目里非常常见不是加几个增强就能彻底解决的。我尝试过几种办法最有效的是两类结合一是收集更多草金鱼图片把训练集中草金鱼的比例提升到和其他类别接近二是对草金鱼类别单独加大数据增强概率让有限样本产生更多形态变异。经过这两轮调整草金鱼的召回率从不到60%提升到了85%左右。如果你的项目也遇到类似情况只有少数类别样本少最值得做的还是尽量去补数据模型结构上的调整只能缓解很难根治。问题现象排查方向解决方案效果误检率飙升颜色分布漂移自动白平衡色温增强误检率降低50%以上遮挡/模糊漏检单帧检测能力不足多帧追踪投票漏检率大幅下降鱼苗识别不了小目标分辨率不足SAHI切片推理小目标召回率提升明显Loss震荡不收敛标签噪声/学习率偏高清洗标签学习率衰减训练稳定收敛边缘推理太慢模型未量化/输入过大INT8量化降分辨率推理速度提升3倍少数类别总漏类别样本不平衡补充数据针对性增强召回率从60%到85%玩MiroFish这段时间我最大的感受是视觉项目真正难的从来不是跑通一个Demo而是让系统在鱼塘那种光照多变、水质偏色、目标又一直在动的环境里稳定干活。每一项优化都是在真实场景的问题倒逼下做出来的而不是靠拍脑袋定方案。如果你也准备做类似的产品我的建议是别一上来就追新模型先把数据标注规范定好把检测加分类的框架跑通后面所有性能优化都有清晰的衡量基线。最后再分享一个小技巧重视检测框贴边标注这个细节省下的调参时间能顶你好几个周末。
