1. 从“盯着鱼缸发呆”到立项:MiroFish 想解决的三个真实问题
养鱼这件事,入门靠热情,坚持下来靠的是耐心。我养了三年观赏鱼,前两年还算从容,后面开始频繁出差,问题就来了:明明出门前换好了水、调好了温度,可每次回家总会发现几条状态不对的鱼,要么缩在角落,要么呼吸急促,更有两次因为没人喂食,活活饿死了一小缸宝莲灯。那段时间我试过视频监控,买过定时投喂器,但都只能用“被动记录”的方式看着鱼缸,根本做不到“主动发现问题”。
后来我想到一个方向:现在的计算机视觉已经能识别宠物、农作物、车辆,为什么不能识别水族箱里的鱼?鱼不会开口说话,但它的游动轨迹、呼吸频率、进食意愿,其实都是肉眼可读的信号。只是人不可能 24 小时站在鱼缸前,机器可以。这就是我做 MiroFish 的初衷:给鱼缸装一双由 AI 驱动的“眼睛”,让它帮我盯着水质之外那些更需要连续观察的生物指标。
MiroFish 这个名字,一半来自胡安·米罗。米罗的画作里全是流动的、抽象的、富有生命感的形态,我觉得这和水里的鱼特别契合;另一半就是 fish 本身。整个项目要做的事可拆成三件:第一,实时识别鱼缸里每条鱼,知道“谁是谁”;第二,持续追踪鱼的游动状态,计算活跃度、呼吸节奏和摄食行为,形成健康评分;第三,把这些数据接到自动投喂器和手机通知上,让设备根据鱼的实际情况决定什么时候喂、喂多少。
这个工程跨了图像识别、边缘计算、嵌入式硬件和 Web 应用几个领域,听起来复杂,但思路一旦捋清,每一步并不难。如果你也养鱼、也写过一点 Python,或者对“摄像头 + AI 模型 + 硬件联动”这套玩法感兴趣,这篇文章会告诉你我从零到一踩过的坑和最终跑通的完整方案。
1.1 传统监控方式的不足:定时器和录像都治标不治本
先说定时投喂器。市面上常见的自动喂食器,核心就是一个定时马达,到点转一下,洒出饲料。它的逻辑是“我在固定时间喂了”,但完全不管鱼吃没吃、吃了几口。我遇到过最尴尬的情况:出差一周回来,发现鱼缸水质浑浊,几条鱼已经腹水,原因就是喂食器到点了照常撒粮,但那几天鱼本身状态不好,根本不想吃,过量饲料全部变成了污染源。
再说视频监控。普通摄像头能录像,但回看几十个小时的视频来观察鱼的状态,基本不现实,而且鱼的状态往往是在某一瞬间开始变化的,等你发现异常再翻录像,可能已经晚了。录像解决的是“事后取证”,不是“事中预警”。
1.2 MiroFish 的定位:AI 眼睛 + 行为逻辑 + 硬件联动
这套系统真正的价值不在于识别出“这是一条宝莲灯”,而在于把识别结果变成可执行的管理动作。
比如,识别出鱼之后,MiroFish 会持续跟踪这条鱼的游动轨迹,如果某条鱼长时间躲在角落、游动距离明显低于平时,系统会打上一个“活跃度异常”的标签;再比如,当投喂器准备投喂时,系统会先检测鱼对饲料的响应情况,如果鱼群根本没有聚集反应,就取消本次投喂。
也就是说,MiroFish 不是一个“看得见”的摄像头,而是一个“看得懂、会行动”的小机器人。它由三个子系统组成:边缘推理子系统负责检测与跟踪,行为分析子系统负责把轨迹变成指标,投喂控制子系统负责把指标变成动作。
2. 整体架构与选型:为什么我选择了“边缘设备 + YOLO + FastAPI”这套组合
2.1 先定硬件:摄像头和处理设备怎么配
MiroFish 首先要解决的是“用什么东西看鱼缸”。我在项目初期列过三套硬件方案,最终选择的是“USB 摄像头 + Jetson Nano 2GB”这套边缘组合。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| PC + NVIDIA 显卡 | 训练和推理快,调试方便 | 功耗高、占地大、风扇噪音大 | 开发测试 |
| Jetson Nano / Xavier NX | 功耗低、体积小、可长期运行 | 算力有限,需要模型轻量化 | 正式部署 |
| 手机 + 云服务 | 零硬件成本,随时随地 | 延迟高、隐私风险、依赖网络 | 临时监控 |
摄像头选用的是罗技 C920,1080P、30FPS,驱动兼容性好,色彩也相对真实。之所以没用树莓派官方摄像头模块,是因为早期测试发现它在低光照下的噪点偏多,而水族箱通常不能开太强的灯,夜间更是只有月光灯,这会让检测模型的效果下降。后来换成了 C920,夜间配合 LED 补光灯,效果提升非常明显。
2.2 软件栈:为什么是 Python + YOLOv8 + FastAPI
软件层面的选择同样做过好几轮对比。最终定下的技术栈是这样的:
- Python 3.10,作为整个系统的主语言;
- Ultralytics YOLOv8,做目标检测和识别;
- OpenCV,负责视频流采集、预处理和画检测框;
- ByteTrack,做多目标跟踪,给每一条鱼一个稳定的 ID;
- FastAPI + WebSocket,把检测结果和实时画面推给 Web 端;
- SQLite,保存历史指标,方便后续统计;
- ESP32-C3 + 舵机,做自动投喂硬件。
这个组合最大的特点是“轻”:除了 YOLO 推理稍微吃一点算力,其余组件都可以跑在低功耗设备上。我没有选择 TensorFlow,因为 YOLOv8 在边缘设备上的部署生态更成熟,Ultralytics 自带导出到 TensorRT 的通道,这对 Jetson 系列特别友好。
2.3 不用传统图像处理的原因
一开始我也尝试过用 OpenCV 的帧差法检测鱼,毕竟“鱼在游动,背景不动”看上去很适合做前景提取。结果在实际鱼缸环境里翻车很严重:水面波纹会不断改变像素,玻璃反光带来大面积亮斑,鱼粪和饲料碎屑也会被当成移动目标。传统算法很难稳定地区分“鱼”和“水面噪声”。
换成 YOLO 之后,模型学的是鱼的形态特征而不是运动差异,所以即使鱼静止不动、被水草遮挡、或者只露出半截身体,也能给出相对稳定的检测框。这个差异决定了整套系统的可信度:我需要的是“知道鱼在哪、知道这是什么鱼”,而不是“知道哪里在动”。
3. 鱼种识别与检测:从数据集清洗到模型部署
3.1 数据集从哪来:自己录视频再抽帧标注
训练识别模型,第一步是数据。我养的主要是宝莲灯、三角灯、绿晶灯、几只珍珠鼠和一对神仙鱼,市面上没有现成的“多鱼种淡水观赏鱼数据集”能直接套用,所以我选择自己采集。
具体操作流程是这样的:
- 用 C920 对着鱼缸连续录制 8 小时视频,覆盖白天高光、傍晚、夜间月光灯等不同光照;
- 由于鱼游动很快,录制帧率设为 30FPS,但抽帧时隔 5 帧取一帧,避免相邻帧过于相似导致数据冗余;
- 每 30 分钟抽一帧,共获得大约 9600 张原始图;
- 用 labelImg 逐张标注,每张图画一个或多个矩形框,并标注类别名称;
- 剔除掉严重模糊、水草完全遮挡、鱼脱出水面的坏图。
标注了一周,眼睛都快花了,但这一步非常值得。数据质量直接决定模型上限,与其后面调试模型不如前面把标注做扎实。我总共标注了约 5200 张有效图片,其中宝莲灯最多,占了四成,因为它是缸里的主力鱼群,神仙鱼只有两条,标注量最少。为了不让模型对神仙鱼“视而不见”,我又额外补拍了一段神仙鱼在鱼缸前半区域活动的素材,人为拉高了这类样本的比例。
3.2 数据增强:模拟鱼缸里的各种“刁钻视角”
鱼不是模特,不会乖乖正对着镜头。它们会侧身、翻转、快速扭头,游到玻璃边缘时还会被曲率扭曲。为了让模型适应这些情况,我在训练时开了 YOLOv8 内置的增强策略,并按需调整:
- Mosaic 增强:把 4 张图拼成一张,让模型学会在小目标占比较小时依然能识别;
- 随机透视与旋转:模拟鱼在不同角度游动;
- HSV 增强:轻微改变色相和饱和度,模拟不同灯光下的颜色偏移;
- 平移和缩放:模拟鱼从远处游近、从画面边缘进入等场景。
这里有个容易忽略的点:增强不是开得越猛越好。曾试过把旋转角度开到 90 度,结果模型把“倒置的鱼”也当成正常采样,实际识别时反而更容易漏检。最终旋转角度我控制在 15 度以内。
3.3 训练与调参:跑通 YOLOv8 的命令
我用的是 YOLOv8n 这个轻量级版本,因为 Jetson Nano 的算力有限,模型太大跑不动实时推理。训练命令如下:
yolo detect train \ data=/home/mirofish/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=15 \ device=0data.yaml 里写清楚训练集、验证集路径,以及类别列表。训练在 PC 上进行,RTX 3060 跑完 100 轮大约需要 4 小时。最终在验证集上的指标是 mAP50 0.914,mAP50-95 0.683。对于 7 个鱼种的小规模任务,这个精度已经够用。
部署到 Jetson 上的时候,记得做一步导出:
yolo export model=best.pt format=engine device=0导成 TensorRT engine 格式后,推理速度从约 480ms 每帧降到 90ms 左右,基本达到实时要求。不用 TensorRT 的话,2GB 版本的 Jetson Nano 很难流畅跑 YOLOv8n。
3.4 部署端推理代码
部署端我用的是 OpenCV 读取视频流,把帧送给模型推理,再把检测结果画回原图:
import cv2 from ultralytics import YOLO model = YOLO("/home/mirofish/weights/best.engine") cap = cv2.VideoCapture(0) while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, imgsz=640, conf=0.45, verbose=False) for r in results: boxes = r.boxes.xyxy.cpu().numpy() clss = r.boxes.cls.cpu().numpy() for box, cls in zip(boxes, clss): x1, y1, x2, y2 = map(int, box) label = f"{model.names[int(cls)]}" cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("MiroFish", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()conf 阈值我设置在 0.45。设置太低会出现大量误检,把饲料颗粒、水草叶片都当成鱼;设置太高又容易漏掉被遮挡的鱼。这个值最好根据你的鱼缸情况实测调整,不要照搬别人的参数。
4. 行为分析:从“认出它是谁”到“知道它在干什么”
4.1 多目标跟踪:为什么必须给鱼一个 ID
检测模型只能回答“这一帧里哪里有鱼”,回答不了“这条鱼两秒前在哪”。为了分析游动行为,必须做目标跟踪,把同一个鱼缸里的同一条鱼关联到同一个 ID 上。
我用了 ByteTrack。相比 DeepSORT,ByteTrack 不需要单独训练外观特征模型,对算力的要求低很多,在 Jetson 上跑也更稳。做法是直接用 YOLO 的检测框作为输入,给 ByteTrack 更新轨迹,然后从轨迹里提取每条鱼的运动特征。
跟踪的难点在于鱼缸里的鱼形态相似,尤其是宝莲灯和三角灯,颜色和大小都很接近。ByteTrack 在鱼群快速交叉时会短暂丢 ID,实测大概每 10 分钟会出现 2 到 3 次。对于行为分析来说,这种短时间断档可以接受,算法会自动衔接两条轨迹。
4.2 活跃度计算:把“活蹦乱跳”量化成数字
我定义了一个简洁的活跃度指标:单位时间内鱼的中心点移动总距离。
import numpy as np def compute_activity(positions, fps=30): if len(positions) < 2: return 0.0 distances = [] for i in range(1, len(positions)): dx = positions[i][0] - positions[i-1][0] dy = positions[i][1] - positions[i-1][1] distances.append(np.hypot(dx, dy)) return float(np.mean(distances[-fps * 60:]))这个函数返回最近 1 分钟的平均移动距离。正常状态下,宝莲灯一分钟的移动距离在 800 到 1500 像素之间。当鱼生病或压力过大时,它们在角落原地摆动,这个值会掉到 200 以下。再加上一个静止时间统计,当某条鱼持续 5 分钟中心点几乎没有位移时,MiroFish 就会标记“疑似状态异常”。
这里有一个我后来才意识到的问题:像素距离受摄像头安装高度和鱼缸大小的影响很大。同一个鱼缸,摄像头装得太远,一条健康的鱼也可能只有 300 像素的位移。所以在部署初期,我先让系统以“自学习模式”运行两天,收集每条鱼在正常状态下的活跃度分布,再以这个分布为基线做判断。这样比写死一个固定阈值靠谱得多。
4.3 浮头识别:一个很便宜但很有效的判据
养鱼的人最怕“浮头”,也就是鱼频繁游到水面吞咽空气,这往往是缺氧的前兆。识别浮头不需要复杂模型,只需要把画面分成上下两个区域,统计“检测框中心点出现在画面顶部 20% 区域的次数比例”。当这个比例在一小时内超过 35%,系统就认为存在浮头风险,自动推送提醒。
这个规则简单到几乎不算 AI,但非常可靠。它的原理在于:正常的鱼不会长时间聚集在水面呼吸,而浮头是一个持续性和群体性的行为,用统计比例就可以滤掉偶然经过水面觅食的个例。我实测过一段 20 分钟的缺氧视频,MiroFish 在鱼群开始浮头后约 8 分钟触发了告警,比我用肉眼发现早了至少 3 分钟。
4.4 进食意愿判断:喂食器要怎么感知“鱼饿不饿”
自动投喂不能只靠定时器,那样和市面几十块钱的投喂器没有任何区别。MiroFish 做的是“进食意愿感知”:在投喂点附近划定一个兴趣区域,投喂器释放少量饲料后,统计 10 秒内进入该区域的鱼的数量和停留时间。
如果 10 秒内至少有 3 条鱼进入投喂区域并停留超过 5 秒,说明鱼愿意吃,就继续投放下一批;如果检测不到明显的进食反应,就停止投喂并记录日志。这个机制有效避免了“鱼生病没胃口,投喂器还在拼命撒粮”的情况。
这个逻辑听上去简单,但实现时有个细节:兴趣区域的边界要稍微画大一点,不能刚好贴着投喂点。因为鱼闻到饲料味之后,会先在附近转圈再冲进去抢食,如果区域太小,会把这种情况误判成“无反应”。我调整了两次 ROI 范围,最终定在投喂点为中心、向外扩展 100 像素的矩形区域。
5. 自动投喂器的 ESP32 联动:协议设计与断电安全
5.1 电机与控制板:喂食器怎么装
自动投喂部分我用了 ESP32-C3 和一个 SG90 舵机。ESP32 负责联网和接收指令,舵机负责转动饲料仓里的拨片。饲料仓我用一个透明的塑料瓶改造,出料口大小需要反复测试,保证每次转动只掉出约 0.1 克饲料。
接线非常简单,SG90 的电源线连 ESP32 的 5V,地线共地,信号线接 GPIO5。注意不要把舵机电源直接接到 ESP32 的 3.3V 引脚,舵机启动瞬间电流较大,会导致板子复位,看起来像是程序崩溃。我第一次就踩了这个坑,后来单独给舵机供 5V 电源才稳定。
5.2 控制协议:我用 HTTP API 而不是串口
有人可能会问:为什么不直接让 Jetson 用串口控制 ESP32?原因很简单:这两台设备在物理上可能相距很远,串口线不好拉,而且只要线一断就必须手动恢复。HTTP API 则没有这个限制,Jetson 只要在局域网内就能发指令。
ESP32 上跑了一个微 Web 服务,核心逻辑如下:
#include <WiFi.h> #include <WebServer.h> #include <ESP32Servo.h> Servo servo; WebServer server(80); void feed() { servo.write(180); delay(300); servo.write(0); delay(200); server.send(200, "text/plain", "fed"); } void setup() { WiFi.begin("MiroFishNet", "your_password"); while (WiFi.status() != WL_CONNECTED) delay(500); servo.attach(5); server.on("/feed", HTTP_GET, feed); server.begin(); } void loop() { server.handleClient(); }Jetson 端通过 requests 库触发投喂时,只需要一句话:
import requests resp = requests.get("http://192.168.1.110/feed", timeout=5) print(resp.status_code)5.3 断网与断电:最担心的事必须有兜底
自动投喂器最恐怖的事故是“卡粮后疯狂转动”或者“断线后重复投喂”。我做了两层防护:
第一层是 ESP32 端限制最小调用间隔。服务端收到/feed请求后,会先检查距上次投喂是否超过 120 秒,不满足直接拒绝。第二层是 Jetson 端的健康检查,每 30 秒探测一次 ESP32 的/health接口,连续 3 次不通就标记喂食器离线,并认为“此时鱼缸状态未知”,不再发出任何投喂指令。
为什么离线时反而要禁止投喂?因为当 AI 和投喂器失去联系时,我们无法获取鱼的进食反馈,任何自动投喂都可能是在向一条状态未知的鱼缸盲目抛粮。保守策略比激进策略安全得多。
5.4 投喂量与时间表的校准过程
刚开始我只配置了每天 3 次投喂时间,分别是 9:00、13:00、18:00。跑了两周之后发现,早上 9 点鱼群进食意愿并不高,因为鱼缸位置靠近阳台,上午阳光直射让水温升高,鱼更倾向于躲在水草阴影里。我把早上的投喂时间挪到了 10:30,反应明显好了很多。
校准投喂量时,我采用的是“增量为 0”的原则:从每次转动一个档位开始,观察 30 分钟。如果鱼群能在 5 分钟内把所有饲料吃完,说明量偏少,第二天增加一个档位;如果 15 分钟后还有残饵,说明量偏多,就减少。这样反复调整一周,就能找到一个相对精准的投喂量。
6. 可视化仪表板与消息推送:把鱼缸状态变成能随时查看的页面
6.1 FastAPI 后端:从模型输出到 Web 数据
为了让用户不打开神秘的黑窗口也能看到鱼缸状态,我写了一个 FastAPI 后端,提供三类接口:
/api/fish:返回当前画面中检测到的鱼种、数量、位置;/api/health:返回每条鱼的活跃度,以及整体健康评分;/api/feed:接受手动投喂请求。
这些接口本身不复杂,但有一个细节值得注意:实时检测是独立的进程,Web 服务进程通过内存队列或 Redis 读取最新结果。不要把模型初始化和 HTTP 请求处理放在同一个进程里,否则视频流推理会阻塞 API 响应,交互体验非常差。
我用的方案是 Python 的 multiprocessing 加一个共享队列:推理进程把检测结果和轨迹数据丢进队列,FastAPI 进程从队列里取。这样就算 Web 端同时有多个请求,推理也不会被阻塞。
6.2 前端页面:不追求花哨,追求一屏看懂
前端我用的是原生 HTML + JavaScript,加上一个简单的 MJPEG 视频流标签。页面分成左右两栏,左边是实时视频流,显示检测框和鱼种标签;右边是状态卡片,显示当前水温、活跃度曲线、最近 24 小时异常事件数、下一次预计投喂时间。
没有引入 Vue 或 React,因为这个项目的核心价值在 AI 识别和联动逻辑,而不在复杂的前端交互。单页静态文件由 FastAPI 直接托管,代码量小、维护成本低。
水温数据不是我额外加的传感器,而是从鱼缸自带的加热棒控制器的串口读出来的,通过另一个串口转 WiFi 模块发到后端。如果你没有这个条件,也可以直接在后端写一个手动录入温度的接口,把温度值存进 SQLite,前端照样能画曲线。
6.3 消息推送:用 Server酱把异常发到手机微信
光有网页还不够,异常状态最好能主动推送到手机。我用的是 Server酱的 HTTP 接口,只需要一个 SendKey 就能把消息发到微信:
import requests def push_alert(title, body): url = "https://sctapi.ftqq.com/YOUR_SENDKEY.send" data = {"title": title, "desp": body} requests.post(url, data=data, timeout=10)推送消息要克制,不然就是骚扰。我设置了三个推送级别:预警(活跃度下降 50%)、告警(持续 30 分钟低活跃)、严重(检测到疑似浮头)。每个级别有冷却时间,同一事件 1 小时内不重复推送。
我最初没有做冷却,结果第一周晚上,氧气泵的气泡被模型误判成“浮头”,手机一晚上收到了 26 条推送,几乎崩溃。加上冷却和夜间免打扰模式之后,推送频率从“每小时多条”降到“每周三五条”,大多数还是真正有价值的预警。
7. 实际运行中的翻车现场与避坑复盘
7.1 玻璃反光和夜间补光:让模型“近视”的元凶
系统刚跑通时,白天的检测率可以达到 95% 以上,但一到夜晚就急剧下降。排查发现是玻璃反光把鱼缸外的物体映射进画面,模型偶尔会把反光里的绿植重影当成一条鱼。
解决办法是给摄像头镜头加了一个偏振镜,转动镜片到某个角度,反光被大幅削减。同时调整补光灯的位置,让它从鱼缸前侧 45 度角照射,而不是正对着玻璃垂直照射。这两个改动之后,夜间的误检率下降了约 70%。
偏振镜这东西我以前觉得只有拍风景才用,没想到在室内监控场景也这么管用。如果你没有偏振镜,还有一个土办法:把摄像头贴在玻璃上,用一块黑色布料包住摄像头与玻璃之间的缝隙,减少环境光从镜头周围射入,也能缓解反光问题。
7.2 水面波动:跟踪器断线的第一个原因
鱼缸的水面不是静止的,特别是开了氧气泵之后,水面会出现大量波纹和气泡。检测框和水面波纹之间的像素变化会让 ByteTrack 误以为有新目标进入,从而打断原有轨迹。
我的处理方式是调整摄像头安装角度,让它从鱼缸正面的偏上方斜向下拍摄,尽量避开水面区域。如果一定要拍摄完整的鱼缸正面,那就需要在检测后加一个 ROI 过滤,把画面最上方 10% 的区域剔除掉。
还有一个经验:氧气泵的气泡如果正好从检测兴趣区域中间穿过,会造成检测框在气泡和目标之间反复跳变。我最后把氧气泵的出气口移到了鱼缸角落,远离中间视野,这个问题就少了很多。硬件排布影响 AI 效果,这点在做视觉项目时很容易被忽略。
7.3 数据漂移:鱼的颜色居然会变
有一个我完全没想到的问题:鱼的体色会随环境、情绪、健康状况变化。宝莲灯在发情期颜色格外鲜艳,病弱时颜色会变淡,如果模型对颜色特征的依赖过强,就会出现“同一条鱼今天识别得出、明天识别不出”的怪象。
为了降低这种影响,我在训练时把 HSV 增强的幅度调得更大了一点,让模型更多依赖鱼的形状和轮廓特征,而不是单纯依赖颜色值。熬过这个阶段之后,模型在颜色变化较大的情况下依然能保持稳定识别。
这种问题在工作里叫“数据漂移”,其实养殖行业很常见。鱼、观赏植物、甚至宠物毛色都会随季节变化,视觉模型如果只在单一季节的数据上训练,换季之后精度就会打折。所以我现在打算每隔三个月补采一轮数据,让模型跟着自然环境一起“换季”。
7.4 长期运行:散热、水汽和微生物
Jetson 设备放在鱼缸旁边,长期运行面临三个实际问题:散热、水汽、藻类滋生。设备外壳上建议加一块小的散热片,机箱尽量选择侧面有通风格栅的款式,但注意不要让水汽直接通过风扇吸入设备内部。我在设备外又套了一个防水透气袋,效果不错。
摄像头镜头上过一两个月就会积累一层水垢,检测效果会逐渐下降,所以维护计划里要加上“每两周擦拭一次镜头”这个动作。别小看这个细节,很多时候系统误检率突然上升,不是代码出了问题,而是镜头脏了。
7.5 我能给后来者的几个建议
第一个建议是“先跑通最小闭环再追求完美”。我一开始就想直接做全功能,结果在模型训练阶段卡了快三周,项目的正反馈迟迟不来。后来把目标缩小到“只要能实时识别鱼种”,一周就完成了,之后所有功能都是在这个基础上逐步叠加的。
第二个建议是“不要迷信模型指标”。mAP 再高,如果训练集和你的鱼缸环境差异很大,部署效果依然可能很差。模型的验证应该以自家鱼缸的连续视频为准,而不是只看验证集数字。
第三个建议是关于复盘的:每一条误检都值得保存。我在跑系统时不断把识别错误的帧存成 JPEG,定期回看,用这些错误样本去扩充训练集,模型会越用越准。坚持了三个月,误检率从最初的 8% 降到了 2% 左右。
最后再分享一个很小的技巧:当你准备把摄像头安装到鱼缸上时,先拍一张带有标尺的照片。因为摄像头的安装角度和高度会影响像素距离和实际距离的比例,标定一次之后,活跃度的“像素”指标就可以粗略换算成“厘米”,后续做行为判断时会更直观。我后来做“鱼的平均游动速度”和“活动半径”统计时,全靠这张照片作为换算基准,省了不少麻烦。