拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于YOLOv8的本地鱼类识别与计数工具MiroFish实战解析

开始动笔写MiroFish之前我先花了很长时间想清楚一件事这个工具到底要解决什么问题。我家里有几个水族缸每次给鱼“点名”都是一件让人崩溃的事。鱼这种东西吧游来游去没有定数你盯着看两分钟数出来的数量和五分钟后再数一次往往对不上。市面上确实有能识别鱼类的App可大多数识别请求要上传到云端有些还要订阅付费精度也不算稳定。于是我就想干脆自己动手做一个能跑在本地电脑上的小工具用图片或者摄像头画面自动识别出常见的观赏鱼是什么品种、一共有多少条每次观测完再把结果存下来方便后续追踪鱼缸生态的变化。这个项目就是MiroFish。MiroFish这个名字是从西班牙语mirar演变来的意思是“看”后面挂一个Fish合起来就是“认真看见水里的鱼”。项目用Python作为主语言目标检测部分基于YOLOv8实现图形界面用PySide6来写数据落库用SQLite。整套方案既能打包成桌面程序也能拆出接口跑在服务器上。这篇文章是我的完整复盘从需求拆解、图片数据准备到模型训练、本地部署再到上线后遇到的各种问题和排查过程都会写清楚。不管你是想给自己鱼缸做个点名录还是想完整走一遍目标检测项目流程这篇内容都可以直接参考。1. 项目概述MiroFish要解决的痛点和选型思路1.1 给鱼缸“点名”的真实需求MiroFish的核心目标可以用一句话概括输入一张水族箱图片返回图中每条鱼的类别和位置并统计各类数量。听起来简单实际拆开看需求有几层。第一是细粒度识别。观赏鱼里有大量近缘品种比如红绿灯和宝莲灯远看很像区别只在于腹部红线的长度和位置。普通的图像分类模型如果训练数据不够很容易把这种近似品种搞混所以MiroFish在设计时就要把“细粒度识别”当成一个重要指标。第二是数量统计。鱼是动态的一条鱼游过去可能只露出半个身体或者几条鱼重叠在一起。如果只做“图像分类”模型只会告诉你“这张图里有红绿灯”不会告诉你到底有几条。所以这里必须用到目标检测用边界框把每条鱼的位置框出来再进行计数。第三是离线运行。我不想把鱼缸的照片传到别人的服务器上一方面考虑隐私另一方面也担心在没网的时候工具就废了。所以从最初就确定整个推理链路必须在本地完成依赖的模型文件不能超过几百兆CPU上也要能跑得动。第四是长期记录。每次点名的结果如果只看一眼就丢掉那和手动数鱼没什么区别。MiroFish会把每一条检测记录写入SQLite数据库这样一周后、一个月后我都能回查“上周一共几条”“这周死了几条”“哪种鱼的数量下降了”。这种长期数据对于养鱼状态的判断太重要了。1.2 为什么选目标检测而不是图像分类很多第一次接触这类需求的人会问识别鱼的品种用分类网络不就行了吗确实如果只是想知道“这张图里有没有金鱼”分类网络足够。但在MiroFish这种场景里分类网络有两个致命缺陷。分类网络只能回答整张图“属于哪个类别”它不关心图里有几个目标。当鱼缸里有五条红绿灯和两条宝莲灯时分类网络根本没法给出数量分布。目标检测网络则是先在图上找到“可能是有鱼的目标区域”再对这些区域逐一分类天然就支持定位和计数。目标检测还能处理遮挡和重叠。鱼群游动时身体互相遮挡是常态。检测模型通过锚点和非极大值抑制NMS机制可以保留置信度最高的框并抑制同一目标上的重复框这样即使鱼前面有另一条鱼游过模型也能根据露出的部分做一个判断。这是分类模型做不到的。我做过一个简单的对比测试同一张鱼缸图片用ResNet分类网络只能输出一个全图类别标签例如“红绿灯”而用YOLOv8检测模型能得到5个边界框其中4个标为红绿灯、1个标为宝莲灯置信度都在0.8以上。这个差异决定了MiroFish必须走目标检测路线。1.3 整体技术栈与功能清单MiroFish的选型遵循一个原则优先用成熟方案自己只写业务逻辑。以下是最终确定的技术栈模块选型理由检测模型YOLOv8实现简单训练工具链完整社区群众基础好推理引擎ONNXRuntime跨平台CPU推理优化明显便于后期部署界面框架PySide6Python生态里桌面端最成熟的选择组件丰富数据库SQLite单文件零配置记录观测历史足够用图像处理OpenCV读图、画框、缩放等操作绕不开它功能清单包括单张图片识别与计数、批量文件夹识别、摄像头实时预览识别、识别结果可视化画框标签、观测数据存储与简单统计。这些功能覆盖了我日常养鱼记录的核心需求后续如果有必要再扩展。2. 数据准备模型精度的地基2.1 图像采集图片来源与数量规划很多人做目标检测项目上来就找公开数据集但鱼类这种细粒度场景公开数据集往往覆盖不够而且标注风格不统一。MiroFish的做法是“公开数据打底 自己采集补强”。我首先跑了一圈公开的鱼类数据集比如一些水族摄影网站和科研机构的开放数据整理出大概8个目标品种红绿灯、宝莲灯、斑马鱼、孔雀鱼、金鱼、斗鱼、黑壳虾对虾也一起检测了、清道夫。最初每个品种大约只有200到300张可用的图片数量明显不够。于是我在自己鱼缸和几个朋友鱼缸里补拍了大量照片。补拍的时候要注意几个点多角度、多光照、多背景。固定在三脚架上的摄像头拍出来的图角度永远是俯视或者平视鱼身姿态千篇一律模型的泛化能力会很差。我用手机在不同位置、不同时间、不同灯光色温下拍摄同时保留了部分缸壁反光、水草遮挡、鱼群重叠的“困难样本”。这些困难样本对于实际使用非常重要。我的经验是每个类别的图片数量至少要达到500张以上边缘类别、容易混淆的类别最好到800张。总图片量在5000张左右时训练出来的模型在自家鱼缸场景里已经相当可用了。如果图片总数只有1000张那模型稍微换个鱼缸就会失准后面调试成本会远高于补数据的成本。2.2 数据清洗与增强策略图片收集回来后不能直接拿去标注和训练先要做一轮清洗。我会把所有图片按品种分类放进不同目录然后一张一张快速过目把以下几类图片挑出来删掉严重过曝或欠曝的、鱼身被水草完全遮住仅能看到一个尾巴尖的、画面中鱼占的面积小于几十个像素的、焦距完全模糊的、以及带有明显水印文字干扰的。清洗干净之后再做数据增强这一步用albumentations库来实现。MiroFish用的增强策略是水平翻转概率0.5注意不要做垂直翻转鱼不会倒着游做了反而会让模型学到错误姿态旋转范围正负15度亮度对比度扰动范围0.8到1.2色温扰动模拟不同灯光下的色彩偏移随机裁剪缩放模拟鱼在画面中不同大小的情况增强后的图片数量和原始图片比例控制在3比1以内。不要一股脑把每张图片增强到10倍一是训练时间变长二是增强过度的图片失真明显反而拉低精度。2.3 标注规范与常见标注错误标注环节我用的工具是X-AnyLabeling它对YOLO格式的支持很好也支持自动标注后人工修正。标注的输出格式是每个图片对应一个txt文件每一行的格式是类别ID 归一化中心x 归一化中心y 归一化宽度 归一化高度注意边界框的坐标必须归一化到0到1之间很多新手在这里翻车训练的时候会报坐标范围错误或者loss直接变成NaN。开始标注之前我给自己定了几条规范鱼身被遮挡超过三分之一的不标注或者标注露出部分的可见区域两条鱼头尾相连时分别标注各自的框宁可框略微重叠也不要只画一个框把两条鱼包进去画框要紧贴鱼身轮廓不要像画分类框一样四周留一大圈空白模糊但能辨别品种的鱼标成对应的类别模糊到连人都看不出来的直接跳过标注过程中最常见的错误是“一个框包含多个目标”。在鱼群密集的图片里人会下意识地用一个框把拥挤的鱼群圈起来对模型来说这是一个非常糟糕的学习信号。模型会去学“一个框里可以有无数条鱼”推理的时候就会漏检。我的经验是密集场景宁可漏标一两条也不能用一个框包住一整群。3. 模型训练与推理优化3.1 模型版本选型YOLOv8s还是YOLOv8nYOLOv8提供了n、s、m、l、x五档模型从轻到重。MiroFish的第一版直接用的YOLOv8s因为我的开发机有一块3060显卡训练速度可以接受。后来部署到普通的笔记本CPU上发现推理速度偏慢一张640x640的图片要跑到500毫秒以上于是又试了YOLOv8n。两者对比下来YOLOv8n的模型体积只有6MB左右CPU推理时间能压到200毫秒以内精度mAP50大约下降2到3个百分点。对于鱼缸识别这种场景2到3个百分点的精度损失完全能接受。最终MiroFish默认型号是YOLOv8n但代码里保留了切换模型的选项如果你有更好的显卡、对精度要求更高可以一键换回YOLOv8s甚至YOLOv8m。这里还要解释一下mAP的含义。mAP50指的是预测框和真实框的IoU交并比超过0.5才算匹配正确时的平均精度mAP50-95则是在0.5到0.95之间多个IoU阈值下取平均。日常使用中mAP50更接近人的直观感受如果mAP50-95很低而mAP50很高说明模型框的位置还不够精确但识别大体是对的对计数工具来说问题不大。3.2 训练参数与收敛策略训练直接用ultralytics框架。训练前把数据集按8比1比1拆分成训练集、验证集、测试集注意先按图片所属的鱼缸场景分组再划分避免同一个场景的相似图片既出现在训练集又出现在验证集否则评估结果会虚高。核心训练参数如下yolo detect train \ data./data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ augmentTrue \ patience20 \ seed42其中patience20表示连续20个epoch验证集指标不再提升就提前停止训练。以我的数据量来看一般在90到120个epoch时收敛提前停止可以省掉不少时间。学习率策略我用的也是默认的SGD配合余弦退火。如果发现loss曲线震荡得厉害优先检查是不是标注数据有问题而不是急着调学习率。我踩过的坑是某次训练loss怎么都降不下去翻了好久的代码才发现是导出标注txt时类别ID写串了红绿灯和宝莲灯的ID反了模型当然学不进去。3.3 ONNX导出与CPU端加速训练完成后模型格式是.pt文件用ultralytics自带的API可以直接导出成ONNX格式from ultralytics import YOLO model YOLO(best.pt) model.export(formatonnx, opset12, simplifyTrue)导出ONNX的意义在于ONNXRuntime在CPU端的推理优化做得很成熟比直接加载PyTorch模型跑要快不少。我实测同一个YOLOv8n模型在笔记本CPU上PyTorch推理需要约300毫秒ONNXRuntime能压到200毫秒左右。如果你的电脑是Intel平台还可以进一步用OpenVINO引擎做加速推理时间能再降30%。我自己最终部署用的是ONNXRuntime CPU模式单帧推理时间稳定在180到250毫秒。对于图片识别和低于每秒5帧的摄像头采样完全够用。如果后期要实时视频流分析再考虑用OpenVINO或者直接用TensorRT加GPU推理。4. 本地工具实操从代码到可运行的桌面程序4.1 环境准备与项目结构先创建一个干净的虚拟环境避免把系统Python搞乱python -m venv mirofish_env source mirofish_env/bin/activate pip install ultralytics onnxruntime opencv-python PySide6 albumentations sqlalchemyMiroFish的项目结构不复杂核心就几个文件MiroFish/ ├── models/ │ └── best.onnx # 训练好的检测模型 ├── mirofish/ │ ├── detector.py # 推理封装 │ ├── database.py # 数据库操作 │ ├── main_window.py # 主界面逻辑 │ └── resources/ ├── data/ │ └── observations.db # SQLite数据库 ├── run.py # 程序入口 └── requirements.txt目录结构设计的原则是把模型推理、数据访问和界面展示分离这样以后要加API接口或者命令行工具只需要复用detector和database两个模块就行。4.2 核心推理流程代码实现detector.py的代码核心很简洁伪代码如下import cv2 import numpy as np import onnxruntime as ort class FishDetector: def __init__(self, model_path, conf_thres0.35, iou_thres0.5): self.session ort.InferenceSession(model_path, providers[CPUExecutionProvider]) self.conf_thres conf_thres self.iou_thres iou_thres self.class_names [black_shrimp, bubble_eye, guppy, ...] def detect(self, image_bgr): img cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) # 缩放加letterbox填充保持长宽比 resized letterbox(img, 640) # 归一化并转成模型输入格式 blob np.expand_dims(resized.transpose(2, 0, 1), 0).astype(np.float32) / 255.0 outputs self.session.run(None, {self.session.get_inputs()[0].name: blob}) # 解析outputs、过滤低置信度框、做NMS boxes postprocess(outputs[0], self.conf_thres, self.iou_thres) return boxes这里最需要注意的是letterbox操作。YOLO系模型训练时会把图片等比缩放再填充推理时也必须用同样的方式处理否则目标的位置坐标会全部偏掉。很多人在部署时直接粗暴resize到640x640出来的检测框位置明显偏移就是这个原因。置信度阈值我默认设置成0.35这是一个折中的值。阈值设太低比如0.1误检会变多缸壁反光都被当成鱼阈值设太高比如0.7鱼被遮挡或者姿态不标准时又会漏检。如果你对精准率要求更高可以往上调到0.5如果要求召回率更高往下调到0.25具体要看使用场景。4.3 界面交互与数据落库界面的实现主要分成三个区域左侧是图片显示区识别后的结果画框直接叠加在图片上右上角是结果列表每一条记录对应一个检测框显示类别、置信度和坐标右下角是操作按钮和统计信息。用PySide6实现时我的做法是识别逻辑放在一个后台线程里避免处理大图或者流量视频时界面卡死。每完成一张图片后台线程通过信号把结果发送到主界面主界面负责刷新显示和更新数据库。数据库设计也很直接就是一张观测记录表CREATE TABLE observations ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_path TEXT NOT NULL, species TEXT NOT NULL, confidence REAL NOT NULL, x_center REAL, y_center REAL, width REAL, height REAL, observed_at DATETIME DEFAULT CURRENT_TIMESTAMP );每次识别完成就把每条鱼作为一个独立记录插入表中。这样后续查“最后一次观测后哪条鱼消失了”只需要按时间筛选同品种数量做个简单的对比就能知道。我用SQLalchemy来操作数据库虽然比直接写sqlite3多了一层抽象但代码可读性好很多而且后续如果要换MySQL之类的数据库改动也小。5. 常见问题与排查技巧实录5.1 识别不准的排查思路MiroFish上线第一周被朋友拿去识别他家鱼缸结果问题一堆照片里一条宝莲灯没识别出来反而把气泵的气泡框成了鱼。我排查了一圈总结了三条经验。先看图片质量。手机隔着缸壁拍又顶着强光下的反光模型识别困难非常正常。解决办法是拍摄时尽量让镜头平行于缸壁关掉鱼缸灯避免反光或者等鱼游到中间层再拍。在代码层面可以增强预处理比如做一个简单的去反光处理用OpenCV的直方图均衡化提升暗部细节。再看阈值设置。气泡被识别成鱼说明置信度阈值偏低把conf_thres从0.35调到0.5误检基本消失。观察下来正常鱼只的置信度通常在0.7到0.9之间低于0.5的框大概率就是噪声。最后看训练数据这也是最根本的。如果模型在特定角度下漏检严重说明训练集里这个角度的样本太少。我会把漏检的图片补充到训练集重新训练一轮。这个“bad case补数据”的循环是模型精度提升最快的手段。5.2 视频流卡顿和掉帧处理摄像头实时识别对算力要求高初期我把每一帧都送进模型笔记本CPU直接拉到100%画面像幻灯片。后来做了两个优化。一个是跳帧。把识别频率降到每秒2帧在两次识别之间只显示画面不做推理。鱼游动的速度并不快每秒2帧的采样率足够捕捉数量和位置变化。另一个是缩小推理分辨率。把输入从640降到480CPU推理时间从200毫秒左右降到120毫秒精度损失在近缘品种上大约1到2个百分点肉眼几乎感知不到。如果画面中有很多小鱼重叠不建议这么做精度损失会被重叠问题放大。5.3 训练集不平衡与难样本问题MiroFish最初的8个类别里黑壳虾图片最少只有不到200张训练出来的模型对黑壳虾的召回率惨不忍睹10只虾只能认出3只。这是典型的类别不平衡问题。我的解决办法分两步。第一步是给黑壳虾专门补拍和网上筛选把数量拉到500张以上。第二步是在训练参数里设置了类别权重让数量少的类别在loss计算中占更大比重。训练之后黑壳虾的召回率从30%提升到大约75%。如果补数据实在困难还可以用“拼接合成”的方法把少数类目标抠出来粘贴到不同背景的图片上同时自动生成标注文件也能缓解不平衡。5.4 问题排查速查表现象可能原因解决建议单张图识别非常慢用的PyTorch直接推理导出ONNX换ONNXRuntime检测框位置整体偏移推理时没有用letterbox改用等比缩放填充同一个鱼在相邻几帧反复跳动计数阈值过低误检提高置信度阈值或加帧间去重逻辑某些品种始终召回率低训练集样本太少补数据、加类别权重、拼接合成训练loss一直降不下来标注ID写错了检查txt标注文件可视化抽查界面拖动时卡顿推理在主线程执行用QThread把推理挪到后台线程6. 踩坑心得和后续扩展方向6.1 我踩过的三个实打实的坑第一个坑是标注初期不够细致画框时习惯性地往鱼身四周多留一圈空白结果模型训练完检测框总是偏大而且两个相近的鱼合并成一个框的概率很高。后来把所有标注框统一收紧到紧贴鱼身问题立刻好转。第二个坑是数据划分不科学。早期我直接把所有图片随机打乱划分训练集和验证集同一个鱼缸同一个视角连拍的照片会同时出现在两个集合里训练时的验证mAP看起来高达0.95一拿到新场景直接掉到0.6。改成“按场景分组划分”以后评估结果才真实反映模型泛化能力。第三个坑是增强参数设置得太大。我想着数据量不够就用增强凑把旋转范围设置到45度亮度扰动拉到0.5到1.8结果模型训练得很慢而且学到了一些扭曲的鱼形态。后来把旋转限制在15度以内亮度范围收到0.8到1.2训练稳定多了。6.2 MiroFish还能朝哪些方向扩展MiroFish目前的形态只是一个本地桌面工具但实际上训练好的模型和推理代码完全可以复用到更多场景。第一是接RTSP网络摄像头做鱼缸实时监控加数量预警。比如鱼突然扎堆一侧或者数量急剧下降可以推送报警到手机。第二是增加行为分析。检测是第一步检测完之后还可以跟踪鱼的游动轨迹统计活跃程度观测鱼是否生病、是否繁殖。这部分可以用ByteTrack之类的多目标跟踪算法来实现。第三是把观测数据做成可视化报表。SQLite里存了那么多历史数据只用表格看太浪费。接一个前端或者用pyecharts直接生成图表就能看到每个品种的数量变化曲线对水质调整、喂食策略的判断都有帮助。我在实际使用中最大的体会是工具本身的代码改动并不复杂真正难的是把数据准备好、把预期管理好。模型不可能一次到位永远是“部署 - 收集失败案例 - 补充数据 - 重训 - 再部署”的循环。MiroFish做到现在这个可用状态一半的时间其实花在整理图片和修补标注上。如果你也想做一个类似的识别工具我的建议很简单先把数据搞扎实再把模型跑通最后再考虑界面漂不漂亮。顺序反了后期一定会加倍返工。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门