From 53956015bc1df814419ab2f425b3220058af3106 Mon Sep 17 00:00:00 2001 From: LiXin01 <1625205083@qq.com> Date: Mon, 12 Oct 2026 15:15:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E6=B8=85=E5=9C=BA=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E4=B8=8E=E8=8F=9C=E5=90=8D=E5=8E=BB=E9=87=8D=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=A2=E5=8D=95=E5=B8=A7=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- dao/settings.py | 2 ++ ui/classifier_ui.py | 82 +++++++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/dao/settings.py b/dao/settings.py index a6fe69d..468b808 100644 --- a/dao/settings.py +++ b/dao/settings.py @@ -96,6 +96,8 @@ DISH_LIVE_STILL_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_STILL_HOLD_SECOND 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')) +# 无餐盘持续该秒数后清场解锁,允许下一盘(含相同菜名)重新存图/上报 +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 214352f..2610e11 100644 --- a/ui/classifier_ui.py +++ b/ui/classifier_ui.py @@ -19,6 +19,7 @@ import os import sys import threading import time +from collections import Counter from pathlib import Path import cv2 @@ -49,6 +50,7 @@ from dao.settings import ( DISH_LIVE_STILL_HOLD_SECONDS, DISH_LIVE_MOTION_SIM_WITH_ORDER, DISH_LIVE_ORDER_DEBOUNCE_SECONDS, + DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS, ) from dao.base_registry import bootstrap_default_base, get_active_base, set_default_base from models.App_settings import AppSetting @@ -132,9 +134,11 @@ class BaseDishApp(PendingLearningUIMixin): self._live_stable_since = None self._live_stable_committed_sig = None self._live_stable_snapshot = None # 稳定期间保留的 dishes,供写入 - self._last_reported_sig = None # 实时上报去重:相同结果只报一次 + self._last_reported_sig = None # 实时上报去重:相同菜名集合只报一次 self._last_order_dish_sig = None # 仅菜名签名:菜未变不重复存图/统计 self._last_order_at = 0.0 + self._order_clear_since = None # 无盘连续计时起点 + self._order_clear_hold = float(DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS) self._last_detect_fp = None # 识别后锚点指纹(静止等待运动;仅餐盘区域) self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时) self._last_motion_boxes = None # 上一轮餐盘框,运动判定只看菜品区域 @@ -228,13 +232,13 @@ class BaseDishApp(PendingLearningUIMixin): save_path = os.path.abspath(os.path.join("images", f"_live_frame_{stamp}.jpg")) try: if cv2.imwrite(save_path, frame_bgr): - self._cleanup_old_live_frames(keep=3000000) + self._cleanup_old_live_frames(keep=3000) return save_path except Exception as e: print(f"保存订单帧失败: {e}") return None - def _cleanup_old_live_frames(self, keep=30): + def _cleanup_old_live_frames(self, keep=3000): """清理过期订单帧,防止 images/ 无限增长。""" try: folder = os.path.abspath("images") @@ -1753,22 +1757,17 @@ class BaseDishApp(PendingLearningUIMixin): return (len(parts), tuple(parts)) def _report_signature(self, dishes): - """上报去重签名:已知菜品名 + 粗量化框(放置不动时不重复上报)。""" - parts = [] + """上报去重签名:已知菜名多重集合(不含框,避免框抖拆单)。""" + names = [] for d in dishes or []: if d.get("is_unknown"): continue name = (d.get("label") or d.get("name") or "").strip() - if not name: - continue - box = d.get("box") or [] - if isinstance(box, (list, tuple)) and len(box) >= 4: - qb = tuple(int(round(float(x) / 40.0)) * 40 for x in box[:4]) - else: - qb = (0, 0, 0, 0) - parts.append((name, qb)) - parts.sort(key=lambda x: (x[1], x[0])) - return tuple(parts) if parts else None + if name: + names.append(name) + if not names: + return None + return tuple(sorted(Counter(names).items())) def _order_dish_signature(self, dishes): """订单菜品签名:只看菜名组合(忽略框位置),手晃框抖不算新单。""" @@ -1784,10 +1783,39 @@ class BaseDishApp(PendingLearningUIMixin): names.sort() return tuple(names) + def _reset_order_gate(self): + """重置订单防重门闩(清场解锁 / 清空清单 / 启停扫描)。""" + self._order_clear_since = None + self._last_order_dish_sig = None + self._last_order_at = 0.0 + self._last_reported_sig = None + + def _note_order_scene_busy(self): + """画面上仍有托盘/已识别菜:清场计时清零。""" + self._order_clear_since = None + + def _note_order_scene_clear(self): + """无餐盘:累计清场时间,满 hold 后解锁,允许下一盘(含相同菜名)重新上报。""" + now = time.time() + if self._order_clear_since is None: + self._order_clear_since = now + return False + hold = float(getattr(self, "_order_clear_hold", DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS) or 1.5) + if now - self._order_clear_since < hold: + return False + had_order = bool(getattr(self, "_last_order_dish_sig", None) or getattr(self, "_last_reported_sig", None)) + self._reset_order_gate() + if had_order: + print(f"[order] 清场解锁(无盘持续 ≥ {hold:g}s,允许下一单)") + else: + print(f"[order] 清场,已点清单可被下一盘覆盖(无盘 ≥ {hold:g}s)") + return True + def _should_record_new_order(self, known_dishes): """菜品未变 → 不存图、不重新统计订单。 - 例:0.5s 内手在中间晃动再次触发识别,但菜名组合未变 → 保持当前单。 + 例:手在中间晃动再次触发识别,但菜名组合未变 → 保持当前单。 + 清场解锁后 `_last_order_dish_sig` 已被清空,相同菜名可再报一单。 """ sig = self._order_dish_signature(known_dishes) if not sig: @@ -1979,9 +2007,7 @@ class BaseDishApp(PendingLearningUIMixin): def stop_live_scan(self): """暂停识别;预览继续(仍显示上一轮框)。""" self._live_enabled = False - self._last_reported_sig = None - self._last_order_dish_sig = None - self._last_order_at = 0.0 + self._reset_order_gate() self._last_detect_fp = None self._last_live_dishes = None self._last_motion_boxes = None @@ -2056,8 +2082,7 @@ 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_order_gate() self._reset_motion_gate(initial_state="moving") if hasattr(self, "camera_btn"): try: @@ -2428,15 +2453,20 @@ class BaseDishApp(PendingLearningUIMixin): self._last_live_dishes = [] self._last_motion_boxes = None self._reset_live_stability() + unlocked = self._note_order_scene_clear() - def _no_plate(): + def _no_plate(did_unlock=unlocked): self._live_overlay_dishes = [] # 未检出新菜:左侧已点清单保留上次结果,不清空 - self._set_result_text("扫描中:未检测到餐盘(已点清单保持上次)", "warn") + tip = "扫描中:未检测到餐盘(已点清单保持上次)" + if did_unlock: + tip += "|已清场,可报下一单" + self._set_result_text(tip, "warn") if hasattr(self, "scan_tip_label"): try: self.scan_tip_label.config( text="扫描中 · 未检出餐盘 · 左侧保持上次已点 · 等待下一次运动" + + (" · 已解锁下一单" if did_unlock else "") ) except Exception: pass @@ -2469,6 +2499,7 @@ class BaseDishApp(PendingLearningUIMixin): save_path = None order_unchanged = False if known_dishes: + self._note_order_scene_busy() should_record, order_sig = self._should_record_new_order(known_dishes) if should_record: save_path = self._save_order_image(frame) @@ -2481,6 +2512,7 @@ class BaseDishApp(PendingLearningUIMixin): if reported: self._last_order_dish_sig = order_sig self._last_order_at = time.time() + print("[order] 本单已上报,等待清场后再报下一单") elif order_sig: # dedupe 跳过也记为当前单,避免反复落盘 self._last_order_dish_sig = order_sig @@ -2593,9 +2625,7 @@ 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._reset_order_gate() self.render_cart() if not self._preview_enabled and not self._live_enabled and hasattr(self, "img_label"): self.img_label.config(