资讯动态

基于计算机视觉的鱼类行为监测系统:MiroFish架构与实战

发布时间:2026/9/18 20:54:06 来源:尧图企业网站定制
我养了六年鱼踩过无数次“鱼突然没了精神”的坑最后决定写一个叫 MiroFish 的小项目让家里的鱼缸自己会“开口说话”。先说明白这是个什么东西MiroFish 是我业余时间折腾的一套轻量级水族箱鱼类行为监测系统。它不依赖云端、不靠昂贵的专用硬件用一台普通电脑加一个USB摄像头就能对鱼缸里的鱼做实时识别、追踪、活跃度评分并且在鱼出现异常行为比如长时间趴底、游动节奏突变时通过微信或本地通知提醒你。如果你也是水族爱好者、计算机视觉入门者或者正在考虑给自己的宠物搞一套“AI观察员”这篇文章会很有参考价值。MiroFish 这个名字是从超现实主义画家米罗Joan Miró那里借来的意象——米罗画里的鱼和生物总是带着一种神秘又天真的线条感而我希望用代码去捕捉真实鱼缸里那些同样神秘的小动作。整个项目的核心诉求就三个看得见检测、认得出识别、读得懂行为判断。1. 项目的由来为什么“看鱼”这件事人眼真的不够用很多养鱼的朋友可能都有过类似的经历鱼缸摆在客厅你每天路过时看一眼觉得鱼都挺正常。但等到你真的蹲下来仔细观察发现某条鱼已经缩在角落好几天了鳍条边缘开始发白呼吸急促——这个时候往往已经错过了最佳干预时机。我自己的转折点是一条养了两年的荷兰凤凰。那天晚上我注意到它不像平时那样抢食但当时只以为是白天喂多了第二天早上它已经侧翻在缸底。后来我想明白一个道理人不可能二十四小时盯着鱼缸即使盯了肉眼也很难捕捉到那些缓慢发生的细微变化。鱼不会说话它们表达不适的方式只有行为改变——游动减少、离群、蹭缸、呼吸频率变化、食欲下降。这些信号在人眼里太容易被忽略但在摄像头持续监测下每一个都是可以被量化的数据。这就是 MiroFish 最初的动机用一套全天候运行的低成本视觉系统把“看鱼”这件事从主观感觉变成客观数据。它可以持续记录每条鱼的轨迹、游动速度、活跃时段当某个指标偏离这条鱼自己的历史基线时系统就认为存在潜在风险触发告警。MiroFish 解决的核心问题有这么几个实时监测不需要人一直盯着屏幕对每条鱼进行身份识别而不是把所有鱼当成一个整体行为变化可以被量化、被记录形成历史趋势异常发生时主动通知而不是等人去发现全本地运行不依赖云服务录像和数据分析都在自己设备里完成。这套系统适合的人群其实比想象中广养观赏鱼的普通玩家想给鱼缸加一层“安全网”养殖户或实验室研究人员需要批量跟踪鱼的活跃度变化计算机视觉学习者需要一个既有趣又有实际意义的落地项目。即使你只是单纯对“让鱼缸变得智能一点”这件事感兴趣这篇文章里的思路也可以直接迁移到其他场景——比如花盆土壤湿度监测、宠物活动追踪、甚至阳台鸟类的访客记录。2. MiroFish 整体架构与核心选型逻辑MiroFish 的定位从一开始就很明确它不是一套学术级别的精细生物学分析工具而是一套在家庭环境里能稳定跑起来、结果有参考意义的实用系统。这个定位决定了整个架构的选择方向——能用现成方案解决的就不自己造轮子能离线算的就不上云能跑就行就不追求极致精度。2.1 为什么坚持本地运算不上云很多类似产品会把人脸识别、姿态估计这类计算放在云端前端只负责拍照上传。但水族箱场景有个特殊性鱼的运动是连续且快速的每秒可能产生几十帧有效数据如果全部上传云端带宽成本先不说单是响应延迟就足以让实时告警失去意义。更重要的是很多玩家并不愿意把自家鱼缸的实时影像传到外部服务器上——这涉及隐私和数据的自主权。我选择本地运算还有一层现实考量离线推理可以让整套系统在断网环境下照常工作。对养殖棚这类网络不稳定的场景这一条非常关键。MiroFish 的主程序跑在一台迷你主机上我用的是无风扇的N100小主机功耗约10瓦可以24小时开机摄像头通过USB接入数据流全程不出局域网。2.2 核心工具链从摄像头到告警消息的完整组件先直接给出我最终用的方案清单后面逐条说明为什么这么选。模块方案选型理由视频采集普通USB摄像头海康威视兼容型号UVC协议免驱成本低替换方便支持MJPEG和YUV格式帧率足够目标检测Ultralytics YOLOv8n模型小约6MBCPU上能跑出10-15 FPS检测精度足够且文档完善多目标跟踪ByteTrack单纯基于检测框匹配不依赖ReID外观模型对鱼缸场景更稳——鱼的外观变化大重识别模型容易失效在线特征提取FastReID 自训练模型解决“哪条鱼是哪条”的长期身份问题行为分析自研规则引擎 滑动窗口统计直接基于轨迹数据做速度、活跃度、区域驻留时间等计算告警通知微信个人通知通过企业微信应用推送 本地声光报警零成本、微信打开率高数据存储SQLite 定时快照JPEG轻量、免维护连续跑几个月不担心膨胀这里重点聊聊目标检测和跟踪的取舍这是整套系统最容易走弯路的地方。我最早尝试的是OpenCV自带的目标跟踪器KCF、CSRT它们在没有预训练模型的情况下可以直接用但问题是这些跟踪器本质上是单目标跟踪先框一个目标然后逐帧寻找它去了哪里。鱼缸里通常有多条鱼而且会互相交叉穿过单目标跟踪器一旦跟丢就再也追不回来。后来换成了检测器加多目标跟踪的经典思路——先用YOLO在每帧图像上找出所有鱼的位置检测再用算法把这些检测框关联起来形成轨迹跟踪。这个思路在多目标场景下要稳健得多。YOLOv8 在 COCO 预训练模型上默认可以检测“鱼”这个类别fish但纯靠预训练权重在自家鱼缸里的表现不够理想原因是鱼缸场景的特殊性缸壁反光、水草遮挡、鱼体颜色和背景融为一体。所以正式运行前我自己标注了一批数据微调了模型具体过程下文会说。跟踪阶段放弃 ReID 外观模型并不是因为 ReID 不行而是因为鱼这个目标的特殊性。人脸的 ReID 可以依赖稳定的面部特征但一条鱼游动时体态会不断变化——侧面看是一条细线、正面看是一个扁片、转身时颜色也会因为光线角度发生剧变。这种外观不稳定性会让外观模型输出的特征向量噪声极大。ByteTrack 的核心思路是“短时匹配靠位置长时匹配靠轨迹”鱼在连续几帧里的位置变化是有强相关性的用位置和运动方向做匹配已经足够稳。2.3 摄像头安装位置与参数设置硬件安装看似简单实际上是最影响识别效果的一环。我的经验是摄像头尽量正面朝向鱼缸长边如果条件限制只能斜着放那么后期做区域映射像素坐标转成缸内相对坐标时会麻烦很多。正对视角下鱼的身体比例最接近真实检测框的稳定性也最好。分辨率设置要注意平衡太低了鱼体细节丢失太高了主机CPU压力大。我在 1080p 和 720p 之间反复比较后最终固定在 [1280, 720]、30 FPS 的输入配置。对一条体长 3-5 厘米的灯科鱼来说720p 在 60 厘米缸内大约能覆盖 15-20 像素的宽度足够检测器识别也不会让后续光流分析因分辨率过高而算不动。摄像头自动参数必须全部关掉。自动白平衡在光线变化时会让整帧色温漂移自动曝光会造成亮度突变这些对检测器来说是灾难性的。我在项目里固定了白平衡设定为6500K色温、固定曝光根据鱼缸灯亮度手动调一档、关闭自动对焦固定焦距。这套固定参数让全天的检测效果一致性大幅提升直接反映在误检率的下降上。3. 让系统会“认鱼”从数据采集到多鱼身份识别检测器只能告诉你“这个位置有鱼”下一步是让它知道“这是哪条鱼”。这一步决定了后续所有行为统计能不能按个体拆开。3.1 数据采集与标注看起来最土、其实最关键的环节MiroFish 的检测模型微调和 ReID 模型训练都离不开鱼缸的真实影像数据。我的做法是让系统先以纯录制模式连续运行一周每天自动抽取不同时间段的画面再筛选出包含清晰鱼体的帧。筛选逻辑很简单用预训练 YOLO 检测凡是检测置信度低于 0.85 的帧直接丢弃高于的按检测框数量归类。一周下来攒了大约 4200 张有效图片涉及 6 条鱼。标注工作是整个项目里最耗时的部分。我先用 LabelImg 做了一遍目标检测的框标注标注对象只有一类Fish。这一步的重点是把鱼体完整框住尤其是鱼尾不要漏掉——如果检测框只含半个鱼身后续的速度计算会因为质心偏移而产生误差。检测框标注完成后我又按个体身份把每张图截取出来建立 6 个类别的 ReID 训练集。这里有个小技巧同一张原图里的不同鱼可以直接当作不同类别的样本不用额外拍摄。3.2 检测模型调优用微调代替完全重训YOLOv8n 的 COCO 预训练模型本身认识“鱼”所以不需要从头训练。我用标注好的 4200 张图在预训练权重基础上微调了 80 轮输入尺寸设成 640batch size 为 16使用自带的数据增强策略随机翻转、色域扰动、马赛克增强。在 20% 的验证集上mAP50 从预训练模型的 0.632 提升到了 0.917mAP50-95 从 0.417 提升到 0.738。这个提升幅度符合预期原因很简单COCO 数据集里的鱼大多是市场摊位上整条完整的鱼或者海洋纪录片里的宽阔海域场景和“水族箱里多条鱼游动、玻璃反光、水草遮挡”的分布差异很大。用一个贴近实际场景的数据集做微调比直接用一个看起来更强大的预训练模型更有效。微调完成后有个额外收获模型对部分水草和缸底砂石的误检率明显下降。预训练模型偶尔会把叶片细长的水草误判成鱼微调后这类误判几乎被消除了因为训练数据里的负样本那些被标注为背景但含有水草的图片起了作用。3.3 ReID 模型训练先做好跑路再谈长相身份识别我采用了 FastReID 框架它提供了完整的数据集管理、训练和评估流程。选择的骨干网络是 ResNet18原因是鱼缸推理环境的 CPU 算力有限太重的骨干会导致整体帧率被拖垮。输入分辨率设为 256×256其实是把单条鱼的检测框缩放后喂进网络提取一个 512 维的全局特征向量。训练过程中我发现一个非常反直觉的现象仅仅用鱼的“长相”做身份分类准确率并不高。初期实验里ReID 模型在 6 条鱼上的 Top-1 识别准确率只有 68%——很多鱼外观非常接近尤其是同一品种的个体。后来我在训练里加入了“时间一致性”约束方法是在训练数据里把同一条鱼在相邻时间段的图片组成正样本对把不同鱼甚至同一条鱼相隔很久的图片组成负样本对让模型学习“短期内同一条鱼的外观是连续变化的”这一隐含规律。加入这个约束后Top-1 准确率提升到了 88%。但即便这样我还是不建议把 ReID 作为跟踪的唯一依据。实际方案是短时跟踪用 ByteTrack 的位置匹配当某条鱼被跟踪超过 30 帧以上时用 ReID 特征和数据库里的已知身份做一次软投票。如果投票置信度高于阈值则把这条轨迹绑定到已知身份否则标记为“未知个体”。这种“先跟踪、后识别”的设计比直接用 ReID 逐帧匹配要稳健得多也降低了因为单帧特征噪声导致的身份跳变。3.4 参数效果对比小模型也能干大事我在项目记录里保留了关键对比数据方便后面复盘模型/方案准确率指标CPU推理耗时单帧备注COCO预训练 YOLOv8n不微调mAP50 0.632约38ms误检高水草易触发微调后 YOLOv8nmAP50 0.917约38ms可忽略不计的推理耗时差异纯ByteTrack跟踪不接ReID5分钟内ID保持率 94.2%-交叉遮挡时偶尔换IDByteTrack 投票ReID5分钟内ID保持率 98.6%额外约6ms推荐的生产配置研究这些数据的意义在于回到实际场景你永远不要追求某个单一指标的极致而是要关注整个链条在长时间运行下的稳定表现。3.5%的ID保持率提升意味着一条鱼游过另一条鱼身后它还是“它自己”不会在记录数据里突然消失又出现。这对长期趋势分析至关重要。4. 核心行为分析轨迹、速度与“鱼的情绪状态”识别出每条鱼是谁之后真正的核心问题来了怎么从像素坐标判断这条鱼现在是在“正常闲逛”还是“不安地乱窜”这个环节没有成熟现成的模型可用必须自己设计一套基于轨迹数据的规则引擎。4.1 物理坐标映射把像素位置变成缸内真实位置摄像头拍摄的是一个透视画面不同深度处同样长度的鱼在画面上长度不同。为了后续计算速度的准确性我做了透视校正在鱼缸正面玻璃上用胶带贴四个已知尺寸的定位点测量它们在实际缸内的相对坐标然后计算一个单应性矩阵把画面中的像素坐标映射到缸壁平面坐标系。这一步看起来繁琐但对速度计算的准确性有质的提升。未校正前靠近镜头一侧的鱼随便一摆尾巴计算出的速度是远离镜头一侧鱼的两三倍完全失真。校正后误差可控制在 5% 以内。4.2 行为特征提取速度、加速度、活跃度、区域驻留基于每帧得到的鱼个体位置我用滑动窗口窗口默认 5 秒每秒 10 帧采样计算以下行为特征平均游动速度单位换算为体长每秒Body Length per Second, BL/s这样对不同体型的鱼有统一的物理意义速度标准差衡量游动平稳度正常时速度波动有规律应激时波动加大位移直方图在窗口中累计的游泳方向分布看鱼是否存在长时间单向贴壁运动区域驻留时间把鱼缸划分为 3×3 网格统计鱼在每个网格中的停留时长离群、趴底都会被反映为右下角或底边网格的驻留异常加速度变化率用于捕捉短促的“窜逃”动作。这些特征的组合形成了一条鱼在某个时刻的“行为画像”。系统不试图判断鱼“开心”还是“抑郁”只输出可量化的偏离度——相对于它自己在过去 7 天同时段的行为基线当前行为特征的偏差有多少个标准差。偏离超过 2.5 个标准差并持续 5 分钟以上系统判定为需要告警的异常状态。4.3 进食事件检测最简单也最实用的异常指标这里有个有意思的细节在众多行为特征里我最终发现“进食反应时间”是最灵敏的健康指标。具体实现方法是在投喂点附近划定一个兴趣区ROI投喂时记录鱼进入 ROI 的延迟。健康鱼的延迟通常在 5 秒内而状态不佳的鱼即使游过来也不急于进食延迟会明显拉长。这个指标实现简单、抗干扰强不像速度那样受水流影响。MiroFish 的告警日志中进食反应延迟的变化往往比游动速度的偏离更早反映问题。4.4 行为分析代码实现简析这里给出一段关键逻辑的伪代码展示行为评分是怎么算出来的class FishBehaviorAnalyzer: def __init__(self, window_sec5, fps10, history_days7): self.window_size window_sec * fps self.speed_history deque(maxlenhistory_days * 24 * 3600) # 历史基线 self.current_track {} def update(self, frame_id, fish_id, position_px): # 1. 像素坐标转缸内真实坐标 pos_real homography_map(position_px) # 2. 当前帧速度和加速度 prev self.current_track.get(fish_id) if prev is None: self.current_track[fish_id] pos_real return speed distance(pos_real, prev) * fps self.current_track[fish_id] pos_real # 3. 维护滑动窗口 self.speed_history.append((fish_id, speed, frame_id)) # 4. 计算偏离度 baseline_mean np.mean(self.history_baseline[fish_id]) baseline_std np.std(self.history_baseline[fish_id]) z_score (current_mean_speed - baseline_mean) / (baseline_std 1e-6) return z_score这只是高度简化的示意代码真正工程实现时还有很多细节鱼游出画面再回来后如何正确接续它的轨迹两条鱼短暂交叉时如何避免交换 ID 导致速度异常突刺水面波动造成的检测框抖动如何用卡尔曼滤波平滑投喂时水流变化对检测框的影响如何避免漏检。每个细节都能单独写一篇长文这里先强调一个核心原则行为分析要基于平滑后的轨迹而不是原始检测点。原始检测框的抖动幅度在高分辨率下经常超过鱼每秒实际移动距离的一半直接用会算出大量虚假加速度峰。5. 异常检测、告警链路与长时间运行的可靠性优化行为分析做得再漂亮如果告警链路不靠谱整套系统就只是个高级录像机。MiroFish 的告警逻辑分为三个层次每一层的触发条件和动作不同。5.1 三层告警体系通知、记录、人工确认第一层是提醒级。当行为偏离度达到预设阈值时系统截取一张当前鱼缸快照附上异常鱼的身份、位置和偏离指标通过企业微信应用推送到微信。这个过程从检测异常到消息到达手机实测延迟在 2 秒内。第二层是记录级。如果异常状态持续 30 分钟以上系统将这段时间的视频片段导出、截取关键帧存入一个按日期和鱼身份索引的文件夹。这样即便你当时没看手机事后也可以回头查阅“这条鱼在那天下午到底发生了什么”。第三层是升级级。如果一个小时内同一身份鱼的异常触发次数超过 3 次系统会调用本地的声光报警器一个串口控制的LED灯带和蜂鸣器强制引起附近人的注意。5.2 告警阈值如何设定从拍脑袋到统计校准阈值设定是整个告警系统里最容易拍脑袋、也最需要严谨的环节。我的做法是正式运行前先静默记录两周基线数据把每条鱼每日不同时段早晨、中午、傍晚、夜间的行为特征分布算出来然后按置信区间校准阈值。具体来说先取 z-score 第 95 分位数作为“提醒级”阈值第 99 分位数作为“记录级”阈值。这个做法的好处是不同季节、不同水温下鱼的活跃度基线本身会变阈值不是死数字而是跟随基线数据的更新而滚动调整。MiroFish 默认每 7 天重新计算一次基线并保留过去 4 周的对比避免因为季节切换导致的系统性误报。5.3 长时间运行的可靠性掉线、资源、数据三座山连续运行几个月后稳定性问题会逐渐浮出水面这里分享几个我踩过的最深的坑。第一个是摄像头掉线。USB 摄像头在持续运行数天后偶尔会出现 UVC 协议栈死锁表现为程序拿不到新帧但进程不报错。我增加了一个看门狗线程如果超过 5 秒没有新帧到达自动重置 USB 设备并重连摄像头。这个机制上线后连续运行 60 天未再出现因死锁导致的空窗期。第二个是 CPU 资源占用膨胀。推理进程本身对 CPU 占用稳定在 35% 左右但 OpenCV 的缓冲区如果消费不及时内存会持续增长。我用的是有界队列每次只保留最新帧主动丢弃旧帧确保处理延迟始终在可接受范围内。另外SQLite 的写入操作迁移到了独立线程避免数据库 I/O 阻塞推理主循环。第三个是夜间光线变化。自然光环境下傍晚光线色温和亮度剧烈变化检测置信度会出现周期性下降。我在告警逻辑里加了“光线置信度权重”当全帧平均亮度低于某阈值时降低异常告警的敏感度避免因为检测不稳定造成大量误报。这个改动显著提升了夜间告警的准确率。5.4 数据可视化从告警到长期趋势洞察告警只是 MiroFish 的一个出口更让人上瘾的是长期趋势的可视化。系统每天生成一张“鱼缸简报”内容包括每条鱼的日活跃度曲线、游动距离、进食反应时间变化趋势、以及异常告警时间轴。周报则汇总一周的数据标记出个体行为的长期漂移。我看过最触目惊心的一张图表某条鱼在患病前 5 天日均游动距离从 3200 米降到 600 米而当时从肉眼观察它还看不出任何异样。把行为趋势当成一种“生理仪表盘”这是这套系统带给我最大的价值。6. 实操复现指南从零搭建你自己的 MiroFish前面把架构、算法和可靠性逻辑都讲清楚了这一节提供一个适合直接上手的复现路径。整个复现周期大约需要一个周末算力要求很低——一台 8GB 内存的迷你电脑或旧笔记本就足够。6.1 环境准备清单项推荐配置预算参考主机N100迷你主机 / 旧笔记本8GB RAM64GB SSD500-1500元摄像头USB摄像头1080p支持MJPEG最好有红外夜视备用80-300元操作系统Ubuntu 22.04 LTS长期支持版免费Python3.10免费推理框架OpenCV、Ultralytics YOLOv8、PyTorch CPU版免费消息推送企业微信注册一个应用免费额度足够免费6.2 三步走录制数据、训练模型、上线监控第一步先部署完整流程的“观测模式”。什么都不用训练直接跑预训练 YOLOv8 检测鱼并记录轨迹连续跑 3 天。这期间把可疑帧手动保存用来做后续微调的数据集。同时记录全天亮度变化确认摄像头参数是否需要调整。第二步离线完成检测模型微调和 ReID 模型训练然后用历史录制的视频回放校验效果。回放时注意观察检测漏检发生什么场景、跟踪 ID 是否稳定、ReID 投票是否有误判。把这些结果整理成一份问题清单。第三步将训练好的模型替换进在线系统开启静默记录模式不通知只记录再连续跑 7 天。攒够基线数据后校准告警阈值然后正式开启通知和异常记录。6.3 运行中的常见问题与对应解决思路现象最可能原因处理方案特定时段检测率骤降光线变化导致帧过暗/过曝固定摄像头参数或启用HDR模式增加光线置信度权重两条鱼交叉后 ID 互换跟踪匹配策略不够稳健降低单帧检测框抖动增加ReID投票机制或提高跟踪器IoU阈值告警频率过高阈值设定不合理用基线数据的 95 分位滚动校准排除昼夜周期和季节因素CPU 占用长期超过 70%画面分辨率太高/推理帧率过高降到 720p限制推理帧率到 5-8 FPS行为特征窗口即可保证稳定摄像头每隔几天断连UVC 协议栈死锁加看门狗线程自动重置USB设备6.4 关于数据隐私和道德的最后一句话MiroFish 从设计之初就是一个纯粹的本地系统我不会上传任何一段鱼缸录像到公共服务器。即便你选择把运行数据分析分享给网友也要留意画面里可能出现的室内倒影、窗户外环境等隐私信息。这类家用视觉项目最容易被忽略的就是“它会一直看着你家”在部署前先想清楚哪些画面你愿意留存、哪些应该立刻删除。回看整个 MiroFish 的开发过程最有价值的部分其实不是最终的告警功能而是它逼着我去理解“观察”本身意味着什么。鱼的每一个微小行为都是在跟环境对话而代码只是把这个对话翻译成了人类能看懂的语言。这个项目目前的扩展方向有两个一是加入鱼体长度估算模块辅助评估喂食量是否合理二是把同样一套跟踪和行为分析逻辑复用到龟缸、仓鼠笼和猫狗摄像头上。如果你也在折腾类似的东西记住一条我踩坑无数总结出来的原则先让系统稳定地“记住”每一帧再去想怎么让它“看懂”这一帧。没有稳定的数据输入一切高级分析都是空中楼阁。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价