猫咪如厕检测与分类识别系统系列【十三】猫咪进出事件逻辑及日志优化【下】
前情提要
家里养了三只猫咪,其中一只布偶猫经常出入厕所。但因为平时忙于学业,没法时刻关注牠的行为。我知道猫咪的如厕频率和时长与健康状况密切相关,频繁如厕可能是泌尿问题,停留过久也可能是便秘或不适。为了更科学地了解牠的如厕习惯,我计划搭建一个基于视频监控和AI识别的系统,自动识别猫咪进出厕所的行为,记录如厕时间和停留时长,并区分不同猫咪。这样即使我不在家,也能掌握猫咪的健康状态,更安心地照顾它们。
🎓 各位的关注与点赞是我持续分享的最大动力,衷心感谢大家的支持!
📢 欢迎正在攻读硕博学位的同学,或是对人工智能充满热情的朋友们,关注我的个人公众号。在这里,我将持续更新博士期间阅读的前沿论文解读、项目实战经验分享,以及我对AI技术趋势的思考与探讨。
✨ 无论你是科研工作者、工程开发者,还是AI初学者,都能在这里找到干货与灵感。让我们一起交流、成长、探索人工智能的无限可能!
已完成工作:
✅猫咪如厕检测与分类识别系统系列【一】 功能需求分析及猫咪分类特征提取
✅猫咪如厕检测与分类识别系统系列【二】多图上传及猫咪分类特征提取更新
✅猫咪如厕检测与分类识别系统系列【三】 融合yolov11目标检测
✅猫咪如厕检测与分类识别系统系列【四】融合检测日志输出及前端展示界面制作
✅猫咪如厕检测与分类识别系统系列【五】信息存储数据库改进+添加猫咪页面制作+猫咪躯体匹配算法架构更新
✅猫咪如厕检测与分类识别系统系列【六】分类模型训练+混合检测分类+未知目标自动更新
✅猫咪如厕检测与分类识别系统系列【七】 当前阶段总结报告
✅猫咪如厕检测与分类识别系统系列【八】 检测推理事件整合+视频推流架构分析
✅猫咪如厕检测与分类识别系统系列【九】 视频检测区域在线绘制+支持摄像头+网络摄像头+整体构建【上】
✅猫咪如厕检测与分类识别系统系列【九】 视频检测区域在线绘制+支持摄像头+网络摄像头+整体构建【下】
✅猫咪如厕检测与分类识别系统系列【十】 视频检测区域动态监测及实时更新
✅猫咪如厕检测与分类识别系统系列【十一】区域进入事件相应逻辑鲁棒性更新
✅猫咪如厕检测与分类识别系统系列【十二】猫咪进出事件逻辑及日志优化【上】
本小节继续更新猫咪进出事件逻辑及日志优化
🎯 核心问题
上一小节已经对哦猫咪进出逻辑进行了重构,但此时检测里面的cat_name = “Unknown” 默认就是unknown,在检测流程中,这行代码:
cat_name = "Unknown"
是初始化在进入检测循环之前的,并且:
-
如果没有检测到任何
cat
类目标 -
或者所有检测都未通过区域判断
-
就不会更新
cat_name
→ 仍然是"Unknown"
于是:
-
即使猫已经离开,
CatSessionTracker
中也会一直接收到"Unknown"
的结果 -
但没有对应的
in_region = False
的已识别猫信息 -
记录逻辑就断掉了
假设有一个 process_frame(frame)
的方法,返回:
return annotated_frame, cat_name, in_region, method, score
应该只在没有任何匹配检测目标时 才返回 Unknown
,否则返回检测到的那只。
def process_frame(frame):results = model.predict(frame)boxes = results[0].boxesannotated = frame.copy()for box in boxes:if int(box.cls[0]) != 15:continue # 不是猫xmin, ymin, xmax, ymax = map(int, box.xyxy[0])cx, cy = (xmin + xmax) // 2, (ymin + ymax) // 2if is_in_region(cx, cy):cat_crop = frame[ymin:ymax, xmin:xmax]pil_img = Image.fromarray(cv2.cvtColor(cat_crop, cv2.COLOR_BGR2RGB))# 尝试识别cat_name = classifier.predict(pil_img)method = "classifier"score = 1.0if cat_name == "Unknown":vec = embedder.extract(pil_img)cat_name = matcher.match(vec)method = "matcher"score = 0.9 # 可替换为相似度评分return annotated, cat_name, True, method, score# ❗没有任何猫在如厕区域return annotated, "Unknown", False, "none", 0.0
✅ 最终目标
-
cat_name
是"Unknown"
→ 说明本帧没有猫在区域中 -
cat_name
是具体名字 → 有猫、识别成功 -
in_region = False
→ 无论识别是否成功,只要区域没猫了,系统就会判断“是否离开”
此时我们发现
line 38, in update
self.logger.log(
TypeError: log() takes 6 positional arguments but 8 were given
self.logger.log(...)
调用了 8个参数 ,但ToiletLogger.log()
方法只接受 6个参数 。
✅ 原因分析:
self.logger.log(cat_name, # namesession["entry_time"], # enter_timeexit_time, # exit_timesession.get("enter_img", ""), # enter_imgexit_path, # exit_imgmethod, # ❗️ extrascore # ❗️ extra
)
传了 7个 值 + self
= 8个 positional arguments
✅ 正确写法:
把 method
和 score
改成命名参数 传递:
self.logger.log(cat_name,session["entry_time"],exit_time,session.get("enter_img", ""),exit_path,method=method,score=score
)
✅ 另一种解决方式
也可以在 ToiletLogger.log()
方法定义里显示声明参数顺序和名称:
def log(self, name, enter_time, exit_time, enter_img, exit_img, method="classifier", score=0.0):
这样就支持最多 8个位置参数 。
✅ 建议
将 log 方法定义为:
def log(self, name, enter_time, exit_time, enter_img, exit_img, method="classifier", score=0.0):
然后调用时也用关键字方式(推荐):
self.logger.log(name, enter_time, exit_time,enter_img, exit_img,method=method,score=score
)
下面是应使用的最新版 toilet_logger.py
文件内容,支持:
-
完整如厕日志写入(包括识别方式、评分)
-
猫进入区域时写入日志文本(
enter_log.txt
)
✅ 完整版 toilet_logger.py
import sqlite3
import os
from datetime import datetime
from config.paths import DATABASE_PATHclass ToiletLogger:def __init__(self):self.conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False)self.cursor = self.conn.cursor()def log(self, name, enter_time, exit_time, enter_img, exit_img, method="classifier", score=0.0):duration = int(exit_time - enter_time)time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")self.cursor.execute("""INSERT INTO toilet_logs (Name, "Enter Time", "Exit Time", "Duration(s)", "Enter Image", "Exit Image","Recognition Method","Recognition Time",Score) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", (name,datetime.fromtimestamp(enter_time).strftime("%Y-%m-%d %H:%M:%S"),datetime.fromtimestamp(exit_time).strftime("%Y-%m-%d %H:%M:%S"),duration,enter_img,exit_img,method,time_str,float(score)))self.conn.commit()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")
✅ 使用方式
调用:
logger.log(..., method="classifier", score=0.88)
logger.log_enter(..., method="matcher", score=0.76)
这将自动写入数据库 + 进入日志 ✅
问题:系统虽然记录了猫离开,但始终显示为「未被识别」,也就是
cat_name == "Unknown"
的 fallback 逻辑被触发了。
🎯 问题本质
系统没有正确记住“上一只猫是谁” ,当猫走掉后,由于没有识别结果,无法判断是谁离开的。
✅ 解决思路:记住当前如厕中的猫名
我们可以引入一个变量 last_active_cat
来追踪当前进入区域的猫。哪怕后续识别失败,也能用这个名字进行离开判断和记录。
✅ 推荐修改方案
在 CatSessionTracker.__init__()
中加一行:
self.last_active_cat = None
然后在识别成功进入时设置它:
if cat_name != "Unknown" and in_region:self.last_active_cat = cat_name
然后在 "Unknown"
判断中使用它替代:
if cat_name == "Unknown" and self.last_active_cat:# 假设上一次识别到的猫就是这次离开的猫session = self.sessions[self.last_active_cat]...
✅ 整合代码片段示例:
if cat_name == "Unknown":# 尝试使用上一次识别到的猫名if self.last_active_cat:name = self.last_active_catsession = self.sessions[name]if session["status"] == "ENTERED" and not in_region:...# 离开处理session["status"] = "IDLE"session["last_exit_time"] = time.time()self.last_active_cat = Nonereturn annotated
✅ 效果
-
系统将记住最近一只进入区域的猫
-
即使后续识别失败(遮挡 / 模糊),也能合理判断谁离开了
-
避免重复出现
未被识别
的提示
以下是 完整修复后的 CatSessionTracker.py
,加入了 last_active_cat
追踪变量,确保猫咪在“离开时未被识别”也能正确记录如厕信息。
✅ 完整版 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_intervalself.min_duration = min_durationself.last_active_cat = None # ✅ 记录最近进入区域的猫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()# 🟡 识别失败时也判断是否猫已经离开(基于 last_active_cat)if cat_name == "Unknown":if self.last_active_cat:session = self.sessions[self.last_active_cat]if session["status"] == "ENTERED" and not in_region:duration = now - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫 {self.last_active_cat} 离开(未被识别),记录如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = nowexit_img = f"{self.last_active_cat}_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(self.last_active_cat,session["entry_time"],now,session.get("enter_img", ""),exit_path,session.get("method", "unknown"),session.get("score", 0.0))else:print(f"⚠️ 猫 {self.last_active_cat} 离开时间太短,未记录")session["status"] = "IDLE"session["last_exit_time"] = nowself.last_active_cat = None # 重置return 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)self.last_active_cat = cat_name # ✅ 设置活跃猫名# 🔴 猫被识别到,但不在区域 → 离开elif session["status"] == "ENTERED":duration = now - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫 {cat_name} 离开,如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = nowexit_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"],now,session.get("enter_img", ""),exit_path,method,score)else:print(f"⚠️ 猫 {cat_name} 离开时间太短,未记录")session["status"] = "IDLE"session["last_exit_time"] = nowself.last_active_cat = None # ✅ 离开后重置活跃猫名return annotated
✅ 效果说明
状况 | 效果 |
---|---|
猫被识别到 | 正常记录 ✅ |
猫未被识别,但曾进入 | 使用 last_active_cat 正确记录 ✅ |
猫进入即离开 | 若不足 min_duration,忽略记录 🚫 |
问题:self.last_active_cat = 一直是None
🎯 当前关键问题
self.last_active_cat = None # 始终没有被赋值
🧠 原因:赋值条件不满足!
在以下位置赋值:
if in_region and cat_name != "Unknown":self.last_active_cat = cat_name
但是有可能 cat_name != "Unknown"
成立,但 in_region == False
(比如猫识别到了,但刚好还没进区域),就不会设置 last_active_cat
!
✅ 修复方案:只要识别成功就更新!
应该改为:
if cat_name != "Unknown":self.last_active_cat = cat_name
🔄 替换以下原有逻辑:
self.last_active_cat = cat_name # ✅ 设置活跃猫名
✅ 新写法:
if cat_name != "Unknown":self.last_active_cat = cat_name
💡 进一步增强建议:
为了更健壮,甚至可以检查是否是同一只正在如厕的猫:
if cat_name != "Unknown":if self.sessions[cat_name]["status"] == "ENTERED":self.last_active_cat = cat_name
🔧 小结
现在只需将:
self.last_active_cat = cat_name
替换为:
if cat_name != "Unknown":self.last_active_cat = cat_name
就能保证只要识别到了,就记住这个猫 ✅
下面是修复后的 ✅【完整版本 CatSessionTracker.py
】,修正了 last_active_cat
始终为 None
的问题。
✅ 完整代码(自动追踪最近如厕猫,识别失败也能记录)
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.last_active_cat = None # ✅ 自动记录最近如厕中的猫名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":self.last_active_cat = cat_name# 🟡 如果识别失败,检查是否有猫可能离开了if cat_name == "Unknown":if self.last_active_cat:name = self.last_active_catsession = self.sessions[name]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"] = nowself.last_active_cat = Nonereturn 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":duration = now - session["entry_time"]if duration >= self.min_duration:print(f"🔴 猫 {cat_name} 离开,记录如厕 {int(duration)} 秒")session["status"] = "IDLE"session["last_exit_time"] = nowexit_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"],now,session.get("enter_img", ""),exit_path,method,score)else:print(f"⚠️ 猫 {cat_name} 离开时间太短,未记录")session["status"] = "IDLE"session["last_exit_time"] = nowself.last_active_cat = Nonereturn annotated
✅ 更新说明
修改点 | 效果 |
---|---|
✅ last_active_cat 追踪 | 始终记住最后识别成功的猫 |
✅ 离开判断逻辑优化 | 即使识别失败,也能判断谁离开了 |
✅ 状态重置 | 离开后自动恢复 IDLE 状态,防止重复记录 |
✅ toilet_logger.py
已更新:
-
增加了
log_enter(...)
方法:当猫咪进入如厕区域时立即记录一条简洁日志 -
日志默认写入文件:
enter_log.txt
✅ 下一步
更新 CatSessionTracker
,在猫进入区域时:
-
保存截图 ✅
-
立即调用
logger.log_enter(...)
,记录猫名、时间、识别方式、评分等
现在更新 CatSessionTracker
来调用这个方法,当然,包含进入区域时立即调用 log_enter()
的功能,同时保留完整如厕记录逻辑:
session_tracker.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_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":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:session["status"] = "ENTERED"session["entry_time"] = nowsession["method"] = methodsession["score"] = scoreenter_img = f"{cat_name}_enter_{int(now)}.jpg"cv2.imwrite(os.path.join(RECORDS_DIR, enter_img), frame)enter_path = os.path.join("static/records", enter_img)session["enter_img"] = enter_path# ✅ 进入区域时立即记录self.logger.log_enter(cat_name, now, enter_path, method, score)else:if session["status"] == "ENTERED":session["status"] = "IDLE"exit_time = nowduration = exit_time - session["entry_time"]session["last_exit_time"] = exit_timeif duration >= self.min_duration:exit_img = f"{cat_name}_exit_{int(now)}.jpg"cv2.imwrite(os.path.join(RECORDS_DIR, exit_img), frame)exit_path = os.path.join("static/records", exit_img)self.logger.log(cat_name,session["entry_time"],exit_time,session.get("enter_img", ""),exit_path,method=session.get("method", "classifier"),score=session.get("score", 0.0))print(f"✅ 记录如厕:{cat_name}, 时长 {int(duration)} 秒")else:print(f"⚠️ 猫 {cat_name} 如厕时长过短({int(duration)} 秒),未记录")return annotated
🔧 注意事项
-
result
必须为 5 元组:(annotated_frame, cat_name, in_region, method, score)
-
log_enter()
会写入enter_log.txt
,可以按需查看或导出
result 怎么输入?
为了支持 CatSessionTracker.update()
接收 5 元组的 result
:
annotated, cat_name, in_region, method, score
需要在检测模块(如 inference.py
或 detector.py
)中处理每帧图像时,把检测 + 识别的结果打包好 传入 CatSessionTracker
。
✅ 示例(修改后的推理输出结构)
假设有如下检测函数:
def process_frame(self, frame):results = self.yolo_model.predict(frame, verbose=False)boxes = results[0].boxesannotated = frame.copy()detected_cat = Nonecat_name = "Unknown"method = "matcher"score = 0.0in_region = Falsefor box in boxes:if int(box.cls[0]) != 15: # 只处理cat类