diff --git a/dao/settings.py b/dao/settings.py index 51c8e29..a6fe69d 100644 --- a/dao/settings.py +++ b/dao/settings.py @@ -92,6 +92,10 @@ DISH_LIVE_PENDING_STABLE_SECONDS = float(os.environ.get('DISH_LIVE_PENDING_STABL # 运动判定:从动到静后需保持静止的秒数,达到后 YOLO 识别一次;持续静止不再识别, # 须再经历「静→动→静」才触发下一次 DISH_LIVE_STILL_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_STILL_HOLD_SECONDS', '0.1')) +# 已有订单时,菜品区域相似度阈值(更低=更宽松,手在中间晃仍算静止,减少误触发) +DISH_LIVE_MOTION_SIM_WITH_ORDER = float(os.environ.get('DISH_LIVE_MOTION_SIM_WITH_ORDER', '0.970')) +# 同一批菜品订单防抖:菜名未变不重复存图/统计;短间隔内亦抑制重复落单 +DISH_LIVE_ORDER_DEBOUNCE_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_DEBOUNCE_SECONDS', '0.5')) DEBUG = True diff --git a/ui/classifier_ui.py b/ui/classifier_ui.py index 7c278df..214352f 100644 --- a/ui/classifier_ui.py +++ b/ui/classifier_ui.py @@ -47,6 +47,8 @@ from dao.settings import ( DISH_MGMT_MEAL_ON_START, DISH_LIVE_PENDING_STABLE_SECONDS, DISH_LIVE_STILL_HOLD_SECONDS, + DISH_LIVE_MOTION_SIM_WITH_ORDER, + DISH_LIVE_ORDER_DEBOUNCE_SECONDS, ) from dao.base_registry import bootstrap_default_base, get_active_base, set_default_base from models.App_settings import AppSetting @@ -131,10 +133,14 @@ class BaseDishApp(PendingLearningUIMixin): self._live_stable_committed_sig = None self._live_stable_snapshot = None # 稳定期间保留的 dishes,供写入 self._last_reported_sig = None # 实时上报去重:相同结果只报一次 + self._last_order_dish_sig = None # 仅菜名签名:菜未变不重复存图/统计 + self._last_order_at = 0.0 self._last_detect_fp = None # 识别后锚点指纹(静止等待运动;仅餐盘区域) self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时) self._last_motion_boxes = None # 上一轮餐盘框,运动判定只看菜品区域 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) self._motion_state = "moving" # moving | settling | wait_motion @@ -1762,7 +1768,42 @@ class BaseDishApp(PendingLearningUIMixin): qb = (0, 0, 0, 0) parts.append((name, qb)) parts.sort(key=lambda x: (x[1], x[0])) - return tuple(parts) + return tuple(parts) if parts else None + + def _order_dish_signature(self, dishes): + """订单菜品签名:只看菜名组合(忽略框位置),手晃框抖不算新单。""" + names = [] + for d in dishes or []: + if d.get("is_unknown"): + continue + name = (d.get("label") or d.get("name") or "").strip() + if name: + names.append(name) + if not names: + return None + names.sort() + return tuple(names) + + def _should_record_new_order(self, known_dishes): + """菜品未变 → 不存图、不重新统计订单。 + + 例:0.5s 内手在中间晃动再次触发识别,但菜名组合未变 → 保持当前单。 + """ + sig = self._order_dish_signature(known_dishes) + if not sig: + return False, sig + last_sig = getattr(self, "_last_order_dish_sig", None) + if sig == last_sig: + debounce = float( + getattr(self, "_order_debounce_seconds", DISH_LIVE_ORDER_DEBOUNCE_SECONDS) or 0.5 + ) + last_at = float(getattr(self, "_last_order_at", 0) or 0) + if last_at and (time.time() - last_at) < debounce: + print(f"[live] {debounce:g}s 内菜品未变,跳过存图/订单统计") + else: + print("[live] 菜品未变,跳过存图/订单统计") + return False, sig + return True, sig def _update_live_stability(self, dishes): """跟踪稳定性。仅当同一结果稳定满 hold 秒且有未知时,返回 (snapshot, commit_sig)。 @@ -1886,7 +1927,13 @@ class BaseDishApp(PendingLearningUIMixin): now = time.time() if state == "wait_motion": - similar, sim = self._frames_very_similar(fp, self._last_detect_fp) + # 已有订单时放宽「静止」判定,手在中间区域晃动尽量不触发再识别 + thr = None + if getattr(self, "_last_order_dish_sig", None) or getattr(self, "_cart", None): + thr = float( + getattr(self, "_motion_sim_with_order", DISH_LIVE_MOTION_SIM_WITH_ORDER) or 0.97 + ) + similar, sim = self._frames_very_similar(fp, self._last_detect_fp, threshold=thr) if not similar: self._motion_state = "moving" self._motion_prev_fp = fp @@ -1933,6 +1980,8 @@ class BaseDishApp(PendingLearningUIMixin): """暂停识别;预览继续(仍显示上一轮框)。""" self._live_enabled = False self._last_reported_sig = None + self._last_order_dish_sig = None + self._last_order_at = 0.0 self._last_detect_fp = None self._last_live_dishes = None self._last_motion_boxes = None @@ -2007,6 +2056,8 @@ class BaseDishApp(PendingLearningUIMixin): self._last_detect_fp = None self._last_live_dishes = None self._last_motion_boxes = None + self._last_order_dish_sig = None + self._last_order_at = 0.0 self._reset_motion_gate(initial_state="moving") if hasattr(self, "camera_btn"): try: @@ -2414,19 +2465,30 @@ class BaseDishApp(PendingLearningUIMixin): # 用识别后的餐盘框再锚一次,后续 wait_motion 更稳 self._last_detect_fp = self._motion_fingerprint(frame) known_dishes = [d for d in dishes if not d.get("is_unknown")] - # 有一单(至少 1 个已识别菜,且与上次不同)才存 images/ 并上报;无单不落盘 + # 菜名未变 / 防抖期内:不存 images、不重新统计订单(防手在中间晃误触发) save_path = None + order_unchanged = False if known_dishes: - sig = self._report_signature(known_dishes) - if sig and sig != self._last_reported_sig: + should_record, order_sig = self._should_record_new_order(known_dishes) + if should_record: save_path = self._save_order_image(frame) if save_path: - self.report_recognition( - save_path, known_dishes, labeled_path or visualize_save_path or save_path, + reported = self.report_recognition( + save_path, known_dishes, + labeled_path or visualize_save_path or save_path, schedule_check=False, dedupe=True, ) - else: - self._last_reported_sig = None + if reported: + self._last_order_dish_sig = order_sig + self._last_order_at = time.time() + elif order_sig: + # dedupe 跳过也记为当前单,避免反复落盘 + self._last_order_dish_sig = order_sig + self._last_order_at = time.time() + else: + order_unchanged = True + print(f"[live] 菜品未变,跳过存图与订单统计 sig={order_sig}") + # 临时未识别到菜(手遮挡等):不清除上一单签名,左侧仍保持上次已点 preview = labeled_path or visualize_save_path or save_path # 同一结果稳定满 hold 秒 → 未知菜写入待学习(同场景一次;盘走不刷入) @@ -2451,9 +2513,11 @@ class BaseDishApp(PendingLearningUIMixin): path=preview, txt=final_result, ds=dishes, written=pending_written, elapsed=stable_elapsed, committed=already_committed, hold_s=hold, + same_order=order_unchanged, ): # 只更新叠加框 + 左侧清单;预览继续刷实时帧 self._live_overlay_dishes = list(ds or []) + # 菜品未变:仍刷新预览框,但左侧保持/同步同一批菜,不视为新单 self.render_result_dock(ds, path) try: self.update_pending_btn() @@ -2466,7 +2530,10 @@ class BaseDishApp(PendingLearningUIMixin): if known: top = max(known, key=lambda d: d.get("score") or 0) name = top.get("label") or "菜品" - msg = f"扫描中|已识别 {len(known)}|{name}" + if same_order: + msg = f"扫描中|菜品未变|{len(known)}|{name}|不重复存图/统计" + else: + msg = f"扫描中|已识别 {len(known)}|{name}" if unknown: msg += f"|未知 {len(unknown)}(不入清单)" self._set_result_text(msg, "ok" if not unknown else "warn") @@ -2526,6 +2593,9 @@ class BaseDishApp(PendingLearningUIMixin): self._cart = [] self._last_dishes = [] self._live_overlay_dishes = [] + self._last_order_dish_sig = None + self._last_order_at = 0.0 + self._last_reported_sig = None self.render_cart() if not self._preview_enabled and not self._live_enabled and hasattr(self, "img_label"): self.img_label.config(