diff --git a/dao/settings.py b/dao/settings.py index e2bcba4..b874687 100644 --- a/dao/settings.py +++ b/dao/settings.py @@ -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 diff --git a/ui/classifier_ui.py b/ui/classifier_ui.py index 99bfc9d..c89933a 100644 --- a/ui/classifier_ui.py +++ b/ui/classifier_ui.py @@ -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