猫咪如厕检测与分类识别系统系列【十二】猫咪进出事件逻辑及日志优化
前情提要
家里养了三只猫咪,其中一只布偶猫经常出入厕所。但因为平时忙于学业,没法时刻关注牠的行为。我知道猫咪的如厕频率和时长与健康状况密切相关,频繁如厕可能是泌尿问题,停留过久也可能是便秘或不适。为了更科学地了解牠的如厕习惯,我计划搭建一个基于视频监控和AI识别的系统,自动识别猫咪进出厕所的行为,记录如厕时间和停留时长,并区分不同猫咪。这样即使我不在家,也能掌握猫咪的健康状态,更安心地照顾它们。
🎓 各位的关注与点赞是我持续分享的最大动力,衷心感谢大家的支持!
📢 欢迎正在攻读硕博学位的同学,或是对人工智能充满热情的朋友们,关注我的个人公众号。在这里,我将持续更新博士期间阅读的前沿论文解读、项目实战经验分享,以及我对AI技术趋势的思考与探讨。
✨ 无论你是科研工作者、工程开发者,还是AI初学者,都能在这里找到干货与灵感。让我们一起交流、成长、探索人工智能的无限可能!
已完成工作:
✅猫咪如厕检测与分类识别系统系列【一】 功能需求分析及猫咪分类特征提取
✅猫咪如厕检测与分类识别系统系列【二】多图上传及猫咪分类特征提取更新
✅猫咪如厕检测与分类识别系统系列【三】 融合yolov11目标检测
✅猫咪如厕检测与分类识别系统系列【四】融合检测日志输出及前端展示界面制作
✅猫咪如厕检测与分类识别系统系列【五】信息存储数据库改进+添加猫咪页面制作+猫咪躯体匹配算法架构更新
✅猫咪如厕检测与分类识别系统系列【六】分类模型训练+混合检测分类+未知目标自动更新
✅猫咪如厕检测与分类识别系统系列【七】 当前阶段总结报告
✅猫咪如厕检测与分类识别系统系列【八】 检测推理事件整合+视频推流架构分析
✅猫咪如厕检测与分类识别系统系列【九】 视频检测区域在线绘制+支持摄像头+网络摄像头+整体构建【上】
✅猫咪如厕检测与分类识别系统系列【九】 视频检测区域在线绘制+支持摄像头+网络摄像头+整体构建【下】
✅猫咪如厕检测与分类识别系统系列【十】 视频检测区域动态监测及实时更新
✅猫咪如厕检测与分类识别系统系列【十一】区域进入事件相应逻辑鲁棒性更新
本小节继续更新猫咪进出事件逻辑及日志优化
def log_enter(self, name, enter_time, enter_img, method="classifier", score=0.0):time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")with open("enter_log.txt", "a", encoding="utf-8") as f:f.write(f"[{time_str}] 🟢 {name} 进入区域(方法:{method},评分:{score:.2f}) 图像:{enter_img}\\n")
⚠️ 确保此方法放在类 ToiletLogger
里。
✅ 2. 确认导入的是 更新后的 toilet_logger.py
在 main.py
或 session_tracker.py
中导入时:
from toilet_logger import ToiletLogger
确保这个模块是刚刚更新的那一个路径下的( cat_monitor/toilet_logger.py
)
✅ 3. 重启环境
如果已经改了文件但还在报错,有可能是旧模块缓存没有刷新(特别是在 notebook / 热加载情况下):
# 重启 Python 解释器,确保 log_enter 方法生效
✅ 快速验证(临时测试)
在项目根目录开一个 Python 控制台测试是否含有该方法:
from toilet_logger import ToiletLogger
logger = ToiletLogger()
print(hasattr(logger, "log_enter")) # 应输出 True
如厕记录没有被写入数据库或日志 ,很可能是以下某一部分逻辑出了问题,我们逐一定位:
✅ 一步步排查方向:
✅ 1. 是否进入了 update()
中 in_region 的判断逻辑?
print(f"[DEBUG] 猫进入区域: {cat_name}, 状态: {session['status']}")
确认是不是根本没触发记录逻辑。
✅ 2. 是否正确调用了 log()
或 log_enter()
可以在 CatSessionTracker
中加一行:
print(f"[记录中] 调用 logger.log_enter for {cat_name}")
print(f"[记录中] 调用 logger.log for {cat_name}")
✅ 3. logger.log()
中是否有异常但被静默?
临时加上异常捕获打印看看:
try:self.logger.log(...)
except Exception as e:print(f"[错误] 写入日志失败:{e}")
✅ 4. 检查数据库是否真的写入失败
打开 records/toilet_log.db
,手动查询是否有数据:
sqlite3 records/toilet_log.db
sqlite> SELECT * FROM toilet_logs ORDER BY rowid DESC LIMIT 5;
✅ 最快的恢复方案:
-
输出当前应使用的
CatSessionTracker
+toilet_logger.py
-
包含打印和异常捕获
-
确保进入一次就有图有日志
-
包括一次完整的检测流程样例
经过全量排查并还原一个稳定可运行的检测 + 记录系统核心逻辑 ✅
🎯 目标
阶段 | 行为 | 是否记录 | 截图 | 保存到 |
---|---|---|---|---|
猫进入区域 | ✅ 立即记录 | ✅ | enter_log.txt | |
猫离开区域 | ✅ 如厕结束 | ✅ | toilet_log.db |
✅ 步骤
输出两段最关键的更新版代码,确保能运行:
✅ 1. CatSessionTracker
(完整 + 带 debug 打印)
import time
import cv2
import os
from config.paths import RECORDS_DIR
from collections import defaultdictclass CatSessionTracker:def __init__(self, logger, min_interval=10, min_duration=10):self.logger = loggerself.min_interval = min_intervalself.min_duration = min_durationself.sessions = defaultdict(lambda: {"status": "IDLE","entry_time": None,"last_exit_time": 0,"method": "classifier","score": 0.0})def update(self, frame, result):annotated, cat_name, in_region, method, score = resultif cat_name == "Unknown":print("[跳过] 未识别猫")return annotatedsession = self.sessions[cat_name]now = time.time()# 🟢 猫进入if in_region:if session["status"] == "IDLE" and (now - session["last_exit_time"]) > self.min_interval:print(f"🟢 猫进入:{cat_name}")session["status"] = "ENTERED"session["entry_time"] = nowsession["method"] = methodsession["score"] = scoreenter_img = f"{cat_name}_enter_{int(now)}.jpg"enter_path = os.path.join("static/records", enter_img)cv2.imwrite(os.path.join(RECORDS_DIR, enter_img), frame)session["enter_img"] = enter_pathself.logger.log_enter(cat_name, now, enter_path, method, score)# 🔴 猫离开elif session["status"] == "ENTERED":session["status"] = "IDLE"exit_time = nowsession["last_exit_time"] = exit_timeduration = exit_time - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫离开:{cat_name},如厕 {int(duration)} 秒")exit_img = f"{cat_name}_exit_{int(now)}.jpg"exit_path = os.path.join("static/records", exit_img)cv2.imwrite(os.path.join(RECORDS_DIR, exit_img), frame)try:self.logger.log(cat_name,session["entry_time"],exit_time,session["enter_img"],exit_path,session["method"],session["score"])except Exception as e:print(f"❌ 写入数据库失败: {e}")else:print(f"⚠️ 猫 {cat_name} 如厕时间太短({int(duration)} 秒),未记录")return annotated
✅ 2. result
的生成方式(在 detector
或 main.py
中应输出的是):
result = (annotated_frame, cat_name, in_region, method, score)
tracker.update(frame, result)
🧠 然而以上代码仍然“没反应”,提供摄像头流和区域判断部分的代码(process_frame
)
没检测到猫咪的时候一直返回的 if cat_name == “Unknown”:
return annotated 根本没有执行到出去
问题的根源是:
当 没有检测到猫咪 或识别失败为
"Unknown"
时,CatSessionTracker.update()
直接return annotated
,完全跳过了“猫是否离开”的判断逻辑 。
❗这会导致的问题
-
猫已经离开了,但因为没有检测出猫,所以逻辑无法触发“离开”状态转移
-
session["status"]
一直卡在ENTERED
,无法恢复为IDLE
-
所有如厕流程卡死在中途
✅ 正确的修复方案:即使识别失败也要判断是否离开区域
我们只需小改这个判断逻辑:
✅ 原错误代码:
if cat_name == "Unknown":return annotated
✅ 修复代码:
if cat_name == "Unknown":# 即使识别失败,也可能是上一只猫已经离开for name, session in self.sessions.items():if session["status"] == "ENTERED":now = time.time()duration = now - session["entry_time"]if duration > self.min_duration:print(f"🔴 {name} 猫可能已离开(当前检测不到),如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = now# 可选择:记录“模糊离开”日志或跳过写入return annotated
✅ 或者简洁一点:
if cat_name == "Unknown":# 只有状态为 ENTERED 的猫,且当前帧没有识别出它 → 视为离开for name, session in self.sessions.items():if session["status"] == "ENTERED":print(f"🔴 猫 {name} 可能离开,但当前未识别出任何猫,自动重置状态")session["status"] = "IDLE"session["last_exit_time"] = time.time()return annotated
🎯 只需要:
在 CatSessionTracker.update()
中找到:
if cat_name == "Unknown":return annotated
并改为上述“兜底处理所有 session”的逻辑即可 ✅
但是这样后面的记录如厕 根本执行不到,
所以我们不能一刀切地在 "Unknown"
时直接 return
,否则就永远不会触发:
-
离开区域的判断
-
记录如厕行为的逻辑
✅ 正确的处理方式是:
始终保留上一个已知猫的状态 ,即使当前帧无法识别出猫,也要根据“区域是否空了”来判断猫是否已经离开!
🎯完美兼容“断识别”的情况:
✅ 修改逻辑:
# 猫识别失败时,不直接 return,而是判断之前是否有猫在区域中
if cat_name == "Unknown":for name, session in self.sessions.items():if session["status"] == "ENTERED":# 当前区域为空,说明猫可能已经离开if not in_region:now = time.time()duration = now - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫 {name} 离开(未被识别),记录如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = now# 截图保存exit_img = f"{name}_exit_{int(now)}.jpg"exit_path = os.path.join("static/records", exit_img)cv2.imwrite(os.path.join(RECORDS_DIR, exit_img), frame)self.logger.log(name,session["entry_time"],now,session.get("enter_img", ""),exit_path,session.get("method", "unknown"),session.get("score", 0.0))else:print(f"⚠️ 猫 {name} 离开(未被识别),但如厕时间太短")session["status"] = "IDLE"session["last_exit_time"] = nowreturn annotated
✅ 整体思路简述
-
如果识别不出猫(unknown)
-
就回头看看之前哪只猫状态是
"ENTERED"
-
如果区域现在是空的 → 猫离开 → 自动记录 ✅
以下是 修复后的完整版本 CatSessionTracker.py
,支持:
-
进入时记录 enter 图 & 写日志
-
离开时记录 exit 图 & 写数据库
-
即使识别失败,只要区域空了也能记录“猫已离开”
✅ CatSessionTracker.py
import time
import cv2
import os
from config.paths import RECORDS_DIR
from collections import defaultdictclass CatSessionTracker:def __init__(self, logger, min_interval=10, min_duration=10):self.logger = loggerself.min_interval = min_interval # 两次如厕最短间隔self.min_duration = min_duration # 最短如厕时间self.sessions = defaultdict(lambda: {"status": "IDLE","entry_time": None,"last_exit_time": 0,"method": "classifier","score": 0.0})def update(self, frame, result):annotated, cat_name, in_region, method, score = resultnow = time.time()# 🟡 猫识别失败时,也检查是否有猫要离开if cat_name == "Unknown":for name, session in self.sessions.items():if session["status"] == "ENTERED" and not in_region:duration = now - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫 {name} 离开(未被识别),记录如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = nowexit_img = f"{name}_exit_{int(now)}.jpg"exit_path = os.path.join("static/records", exit_img)cv2.imwrite(os.path.join(RECORDS_DIR, exit_img), frame)self.logger.log(name,session["entry_time"],now,session.get("enter_img", ""),exit_path,session.get("method", "unknown"),session.get("score", 0.0))else:print(f"⚠️ 猫 {name} 离开时间过短,未记录")session["status"] = "IDLE"session["last_exit_time"] = nowreturn annotated# 🟢 猫进入区域session = self.sessions[cat_name]if in_region:if session["status"] == "IDLE" and (now - session["last_exit_time"]) > self.min_interval:print(f"🟢 猫进入:{cat_name}")session["status"] = "ENTERED"session["entry_time"] = nowsession["method"] = methodsession["score"] = scoreenter_img = f"{cat_name}_enter_{int(now)}.jpg"enter_path = os.path.join("static/records", enter_img)cv2.imwrite(os.path.join(RECORDS_DIR, enter_img), frame)session["enter_img"] = enter_pathself.logger.log_enter(cat_name, now, enter_path, method, score)# 🔴 猫识别正常,但已经不在区域 → 离开elif session["status"] == "ENTERED":exit_time = nowduration = exit_time - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫离开:{cat_name},如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = exit_timeexit_img = f"{cat_name}_exit_{int(now)}.jpg"exit_path = os.path.join("static/records", exit_img)cv2.imwrite(os.path.join(RECORDS_DIR, exit_img), frame)self.logger.log(cat_name,session["entry_time"],exit_time,session.get("enter_img", ""),exit_path,method,score)else:print(f"⚠️ 猫 {cat_name} 离开时间太短,未记录")session["status"] = "IDLE"session["last_exit_time"] = exit_timereturn annotated