判定运动,不用时间去判断,改为帧判断

This commit is contained in:
wanghanbo 2026-10-12 16:40:33 +08:00
parent e19e462837
commit ff6da8fcfe
2 changed files with 36 additions and 34 deletions

View File

@ -103,16 +103,17 @@ DISH_MEAL_REFRESH_MINUTE = int(os.environ.get('DISH_MEAL_REFRESH_MINUTE', '0'))
# 实时扫描:未知菜写入待学习前的最短稳定秒数(客人放盘短,默认 1 秒)
# 盘子移走时不刷入,避免移盘过程中的误检未知
DISH_LIVE_PENDING_STABLE_SECONDS = float(os.environ.get('DISH_LIVE_PENDING_STABLE_SECONDS', '1.0'))
# 运动判定:从动到静后需保持静止的秒数,达到后 YOLO 识别一次;持续静止不再识别,
# 须再经历「静→动→静」才触发下一次
DISH_LIVE_STILL_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_STILL_HOLD_SECONDS', '0.2'))
# 运动判定:从动到静后,连续多少帧与静止锚点相似,才触发 YOLO 一次(按相机帧,不按秒)。
# 持续静止不再识别,须再经历「静→动→静」。默认 4 帧(约对应原 0.2s@20Hz 轮询)。
DISH_LIVE_STILL_HOLD_FRAMES = int(os.environ.get('DISH_LIVE_STILL_HOLD_FRAMES', '4'))
# 已有订单时,菜品区域相似度阈值(更低=更宽松,手在中间晃仍算静止,减少误触发)
DISH_LIVE_MOTION_SIM_WITH_ORDER = float(os.environ.get('DISH_LIVE_MOTION_SIM_WITH_ORDER', '0.970'))
# 订单防抖:菜品名称集合去重 + 时间间隔(秒),避免同一次晃动重复上传
# 默认 2s(建议 2~3s)
DISH_LIVE_ORDER_DEBOUNCE_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_DEBOUNCE_SECONDS', '2'))
# 无餐盘持续该秒数后清场解锁,允许下一盘(含相同菜名)重新存图/上报
DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS', '0.5'))
DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS', '1.5'))
DEBUG = True

View File

@ -47,7 +47,7 @@ from dao.settings import (
DISH_BASE_NAME,
DISH_MGMT_MEAL_ON_START,
DISH_LIVE_PENDING_STABLE_SECONDS,
DISH_LIVE_STILL_HOLD_SECONDS,
DISH_LIVE_STILL_HOLD_FRAMES,
DISH_LIVE_MOTION_SIM_WITH_ORDER,
DISH_LIVE_ORDER_DEBOUNCE_SECONDS,
DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS,
@ -147,12 +147,12 @@ class BaseDishApp(PendingLearningUIMixin):
self._frame_sim_threshold = 0.992 # 帧相似度阈值:判定静止 / 运动
self._motion_sim_with_order = float(DISH_LIVE_MOTION_SIM_WITH_ORDER)
self._order_debounce_seconds = float(DISH_LIVE_ORDER_DEBOUNCE_SECONDS)
# 运动状态机:动→静保持 still_hold 秒 → YOLO 一次 → 静待运动 → 再静…
self._live_still_hold_seconds = float(DISH_LIVE_STILL_HOLD_SECONDS)
# 运动状态机:动→静连续 N 帧相似 → YOLO 一次 → 静待运动 → 再静…
self._live_still_hold_frames = max(1, int(DISH_LIVE_STILL_HOLD_FRAMES))
self._motion_state = "moving" # moving | settling | wait_motion
self._motion_prev_fp = None
self._motion_settle_fp = None
self._motion_still_since = None
self._motion_still_count = 0 # settling 期间已连续相似的帧数
self._yolo_trigger_pending = False
self.cfg = Config()
self.app = YOLOv11PlateExtractor(self.cfg)
@ -240,7 +240,7 @@ class BaseDishApp(PendingLearningUIMixin):
print(f"保存订单帧失败: {e}")
return None
def _cleanup_old_live_frames(self, keep=3000):
def _cleanup_old_live_frames(self, keep=300000):
"""清理过期订单帧,防止 images/ 无限增长。"""
try:
folder = os.path.abspath("images")
@ -1939,11 +1939,11 @@ class BaseDishApp(PendingLearningUIMixin):
self.start_live_scan()
def _reset_motion_gate(self, initial_state="moving"):
"""重置动→静触发状态机。默认 moving:开扫后首段静止满 hold 也会识别一次。"""
"""重置动→静触发状态机。默认 moving:开扫后连续静止帧满也会识别一次。"""
self._motion_state = initial_state
self._motion_prev_fp = None
self._motion_settle_fp = None
self._motion_still_since = None
self._motion_still_count = 0
self._yolo_trigger_pending = False
def _motion_fingerprint(self, frame_bgr):
@ -1953,14 +1953,18 @@ class BaseDishApp(PendingLearningUIMixin):
boxes=getattr(self, "_last_motion_boxes", None),
)
def _still_hold_frames(self):
return max(1, int(getattr(self, "_live_still_hold_frames", DISH_LIVE_STILL_HOLD_FRAMES) or 4))
def _update_motion_gate(self, frame_bgr):
"""根据当前帧更新运动状态;从动到静保持 hold 秒时置位 YOLO 触发。
"""根据当前帧更新运动状态;连续 N 帧与静止锚点相似时触发 YOLO 一次。
运动只看餐盘区域(有上次检测框用框,否则画面中央),避免旁人晃动误触发。
用帧计数而非秒数,避免轮询抖动 / 帧率波动导致触发不稳。
状态:
moving — 菜品区域在动(或刚开扫)
settling — 菜品刚停住,累计静止时间
settling — 菜品刚停住,累计连续相似帧
wait_motion — 已识别过,菜品静止中忽略,直到菜品再出现运动
"""
if frame_bgr is None:
@ -1969,9 +1973,8 @@ class BaseDishApp(PendingLearningUIMixin):
if fp is None:
return False
hold = float(getattr(self, "_live_still_hold_seconds", DISH_LIVE_STILL_HOLD_SECONDS) or 0.1)
need = self._still_hold_frames()
state = getattr(self, "_motion_state", "moving")
now = time.time()
if state == "wait_motion":
# 已有订单时放宽「静止」判定,手在中间区域晃动尽量不触发再识别
@ -1985,7 +1988,7 @@ class BaseDishApp(PendingLearningUIMixin):
self._motion_state = "moving"
self._motion_prev_fp = fp
self._motion_settle_fp = None
self._motion_still_since = None
self._motion_still_count = 0
if sim is not None:
print(f"[live] 菜品区域运动 sim={sim:.4f},等待静止后识别")
return False
@ -1999,7 +2002,7 @@ class BaseDishApp(PendingLearningUIMixin):
if similar:
self._motion_state = "settling"
self._motion_settle_fp = fp
self._motion_still_since = now
self._motion_still_count = 1
return False
if state == "settling":
@ -2008,16 +2011,18 @@ class BaseDishApp(PendingLearningUIMixin):
self._motion_state = "moving"
self._motion_prev_fp = fp
self._motion_settle_fp = None
self._motion_still_since = None
self._motion_still_count = 0
return False
since = self._motion_still_since or now
if (now - since) >= hold:
self._motion_still_count = int(getattr(self, "_motion_still_count", 0) or 0) + 1
# 锚点用最新静止帧,减轻缓慢漂移
self._motion_settle_fp = fp
if self._motion_still_count >= need:
self._yolo_trigger_pending = True
self._motion_state = "wait_motion"
self._motion_settle_fp = None
self._motion_still_since = None
self._motion_still_count = 0
self._motion_prev_fp = fp
print(f"[live] 菜品静止满 {hold:g}s,触发 YOLO 识别一次")
print(f"[live] 菜品连续静止 {need} 帧,触发 YOLO 识别一次")
return True
return False
@ -2108,18 +2113,18 @@ class BaseDishApp(PendingLearningUIMixin):
self.camera_btn.config(text="暂停扫描")
except Exception:
pass
still_hold = float(getattr(self, "_live_still_hold_seconds", DISH_LIVE_STILL_HOLD_SECONDS) or 0.1)
still_n = self._still_hold_frames()
hold_s = float(getattr(self, "_live_stable_seconds", DISH_LIVE_PENDING_STABLE_SECONDS) or 1.0)
hold_txt = f"{hold_s:g}"
if hasattr(self, "scan_tip_label"):
try:
self.scan_tip_label.config(
text=f"实时扫描中 · 动→静 {still_hold:g}s 识别一次 · 未知约 {hold_txt}s 记待学习"
text=f"实时扫描中 · 动→静连续 {still_n} 帧识别一次 · 未知约 {hold_txt}s 记待学习"
)
except Exception:
pass
self._set_result_text(
f"实时扫描已开启(动→静保持 {still_hold:g}s 识别一次;未知约 {hold_txt}s 写入待学习)",
f"实时扫描已开启(动→静连续 {still_n} 帧识别一次;未知约 {hold_txt}s 写入待学习)",
"ok",
)
self._schedule_live_tick(immediate=True)
@ -2418,19 +2423,15 @@ class BaseDishApp(PendingLearningUIMixin):
if getattr(self, "_motion_state", None) == "wait_motion":
self._live_on_similar_frame_skip()
elif getattr(self, "_motion_state", None) == "settling":
hold = float(
getattr(self, "_live_still_hold_seconds", DISH_LIVE_STILL_HOLD_SECONDS) or 0.1
)
since = self._motion_still_since
left = hold
if since:
left = max(0.0, hold - (time.time() - since))
need = self._still_hold_frames()
got = int(getattr(self, "_motion_still_count", 0) or 0)
left = max(0, need - got)
def _settling_tip(remain=left):
def _settling_tip(remain=left, total=need, cur=got):
if hasattr(self, "scan_tip_label"):
try:
self.scan_tip_label.config(
text=f"扫描中 · 已静止,约 {remain:.1f}s 后识别"
text=f"扫描中 · 已静止 {cur}/{total} 帧,还需 {remain} 帧后识别"
)
except Exception:
pass