过滤手和空盘类别
This commit is contained in:
parent
a095f0b776
commit
bf98a717aa
|
|
@ -511,6 +511,31 @@ class PartTemplateManager:
|
|||
return True, "·软阈值"
|
||||
return False, ""
|
||||
|
||||
@staticmethod
|
||||
def _match_filter_names():
|
||||
"""模板比对后需丢弃的菜名(如手、空盘)。"""
|
||||
try:
|
||||
from dao.settings import DISH_MATCH_FILTER_NAMES
|
||||
return {str(n).strip() for n in (DISH_MATCH_FILTER_NAMES or []) if str(n).strip()}
|
||||
except Exception:
|
||||
return {"手", "空盘"}
|
||||
|
||||
def _filtered_match_result(self, name, score, feat, top_k=None, scope="filtered"):
|
||||
print(f"[match] filtered template hit: {name} score={score}")
|
||||
return {
|
||||
"text": f"已过滤:{name}",
|
||||
"is_unknown": False,
|
||||
"is_filtered": True,
|
||||
"label": None,
|
||||
"score": float(score) if score is not None else None,
|
||||
"suggested_category": name,
|
||||
"filtered_as": name,
|
||||
"top_k": top_k or [],
|
||||
"pending_id": None,
|
||||
"feature": feat,
|
||||
"match_scope": scope,
|
||||
}
|
||||
|
||||
def predict_from_feature(self, feat, image_path=None, auto_pending=True):
|
||||
"""已有特征时直接匹配(避免待学习提交时重复提特征)。
|
||||
|
||||
|
|
@ -567,25 +592,53 @@ class PartTemplateManager:
|
|||
if period_only:
|
||||
staple_names = self.staple_candidate_names()
|
||||
period_set = set(period_names or [])
|
||||
allowed = set(staple_names) | period_set
|
||||
filter_names = self._match_filter_names()
|
||||
# 过滤类若本地有模板,纳入候选:先比中再丢弃,避免误认成其他菜
|
||||
with self.lock:
|
||||
lib_names = {str(k).strip() for k in (self.template_library or {}).keys() if str(k).strip()}
|
||||
filter_in_lib = filter_names & lib_names
|
||||
allowed = set(staple_names) | period_set | filter_in_lib
|
||||
ranked, _, _ = (
|
||||
self._rank_categories(feat, allowed_names=allowed)
|
||||
if allowed else ([], None, -1.0)
|
||||
)
|
||||
|
||||
# 从合并结果里拆出主食 / 餐段各自最高分
|
||||
# 从合并结果里拆出主食 / 餐段 / 过滤类各自最高分
|
||||
best_st, max_st = None, -1.0
|
||||
best_p, max_p = None, -1.0
|
||||
best_f, max_f = None, -1.0
|
||||
for name, score in ranked:
|
||||
sc = float(score)
|
||||
if name in filter_in_lib and sc > max_f:
|
||||
best_f, max_f = name, sc
|
||||
if name in staple_names and sc > max_st:
|
||||
best_st, max_st = name, sc
|
||||
if name in period_set and sc > max_p:
|
||||
best_p, max_p = name, sc
|
||||
|
||||
# 过滤类达标:直接丢弃该框(不再与菜品比优先)
|
||||
f_hit, _ = self._score_hit(best_f, max_f, staple_thr, staple_soft)
|
||||
# 过滤类用与主食相同阈值;也可用餐段阈值取较松者
|
||||
if not f_hit:
|
||||
f_hit, _ = self._score_hit(best_f, max_f, small_thr, soft_thr)
|
||||
|
||||
st_hit, st_how = self._score_hit(best_st, max_st, staple_thr, staple_soft)
|
||||
p_hit, p_how = self._score_hit(best_p, max_p, small_thr, soft_thr)
|
||||
|
||||
top_k = [{"category": n, "score": s} for n, s in (ranked or [])[:3]]
|
||||
|
||||
if f_hit and best_f is not None:
|
||||
# 过滤类分不低于当前菜/主食命中分 → 认定应丢弃
|
||||
other_best = -1.0
|
||||
if st_hit:
|
||||
other_best = max(other_best, float(max_st))
|
||||
if p_hit:
|
||||
other_best = max(other_best, float(max_p))
|
||||
if float(max_f) >= other_best - float(staple_margin):
|
||||
return self._filtered_match_result(
|
||||
str(best_f).strip(), float(max_f), feat, top_k=top_k, scope="filtered",
|
||||
)
|
||||
|
||||
chosen = None
|
||||
how = ""
|
||||
scope = "period_miss"
|
||||
|
|
@ -597,10 +650,12 @@ class PartTemplateManager:
|
|||
elif st_hit:
|
||||
chosen, how, scope = best_st, f"主食+餐段{st_how}", "staple"
|
||||
|
||||
top_k = [{"category": n, "score": s} for n, s in (ranked or [])[:3]]
|
||||
|
||||
if chosen is not None:
|
||||
score = float(max_st) if scope == "staple" else float(max_p)
|
||||
if str(chosen).strip() in filter_names:
|
||||
return self._filtered_match_result(
|
||||
str(chosen).strip(), score, feat, top_k=top_k, scope=scope,
|
||||
)
|
||||
text = f"{chosen} 相似度:{score:.3f}({how})"
|
||||
return {
|
||||
"text": text,
|
||||
|
|
@ -615,11 +670,19 @@ class PartTemplateManager:
|
|||
}
|
||||
|
||||
sug, sug_score = None, -1.0
|
||||
if best_f is not None and float(max_f) > sug_score:
|
||||
sug, sug_score = best_f, float(max_f)
|
||||
if best_st is not None and float(max_st) > sug_score:
|
||||
sug, sug_score = best_st, float(max_st)
|
||||
if best_p is not None and float(max_p) > sug_score:
|
||||
sug, sug_score = best_p, float(max_p)
|
||||
|
||||
# 最接近手/空盘:整框丢弃,不进未知/待学习
|
||||
if sug is not None and str(sug).strip() in filter_names:
|
||||
return self._filtered_match_result(
|
||||
str(sug).strip(), float(sug_score), feat, top_k=top_k, scope="period_miss",
|
||||
)
|
||||
|
||||
pending_id = None
|
||||
if auto_pending and image_path:
|
||||
pending_id = self.pending_store.add(image_path, feat, sug, sug_score)
|
||||
|
|
|
|||
|
|
@ -738,6 +738,7 @@ class PendingLearningUIMixin:
|
|||
final_result = ""
|
||||
dish_labels = []
|
||||
dishes = []
|
||||
kept_boxes = []
|
||||
unknown_count = 0
|
||||
for i in range(n):
|
||||
if progress_callback:
|
||||
|
|
@ -750,12 +751,17 @@ class PendingLearningUIMixin:
|
|||
image_path=img_path if isinstance(img_path, str) else None,
|
||||
auto_pending=auto_pending,
|
||||
)
|
||||
if detail.get("is_filtered"):
|
||||
print(f"[match] skip dish box filtered_as={detail.get('filtered_as')}")
|
||||
continue
|
||||
if detail.get("is_unknown"):
|
||||
unknown_count += 1
|
||||
idx = len(dishes) + 1
|
||||
dish_labels.append(format_dish_box_label(detail))
|
||||
final_result += f"【菜品{i + 1}】{format_predict_detail(detail)}\n"
|
||||
final_result += f"【菜品{idx}】{format_predict_detail(detail)}\n"
|
||||
kept_boxes.append(box)
|
||||
dishes.append({
|
||||
"index": i + 1,
|
||||
"index": idx,
|
||||
"label": detail.get("label"),
|
||||
"score": detail.get("score"),
|
||||
"is_unknown": bool(detail.get("is_unknown")),
|
||||
|
|
@ -768,10 +774,12 @@ class PendingLearningUIMixin:
|
|||
"feature": detail.get("feature"),
|
||||
})
|
||||
except Exception as e:
|
||||
idx = len(dishes) + 1
|
||||
dish_labels.append("识别失败")
|
||||
final_result += f"【菜品{i + 1}】加载/识别失败:{e}\n"
|
||||
final_result += f"【菜品{idx}】加载/识别失败:{e}\n"
|
||||
kept_boxes.append(box)
|
||||
dishes.append({
|
||||
"index": i + 1,
|
||||
"index": idx,
|
||||
"label": None,
|
||||
"score": None,
|
||||
"is_unknown": True,
|
||||
|
|
@ -792,7 +800,7 @@ class PendingLearningUIMixin:
|
|||
(d.get("label") or d.get("suggested_category") or f"菜品{d.get('index') or i + 1}")
|
||||
for i, d in enumerate(dishes)
|
||||
]
|
||||
labeled_path = self.build_dish_labeled_image(source_image_path, boxes, viz_labels)
|
||||
labeled_path = self.build_dish_labeled_image(source_image_path, kept_boxes, viz_labels)
|
||||
_mark("batch: draw_labels done")
|
||||
except Exception as e:
|
||||
_mark(f"batch: draw_labels fail {e}")
|
||||
|
|
|
|||
|
|
@ -125,11 +125,13 @@ DISH_LIVE_ORDER_SAME_SCENE_FRAMES = int(os.environ.get('DISH_LIVE_ORDER_SAME_SCE
|
|||
DISH_LIVE_ORDER_LAYOUT_IOU = float(os.environ.get('DISH_LIVE_ORDER_LAYOUT_IOU', '0.40'))
|
||||
DISH_LIVE_ORDER_LAYOUT_MATCH_RATIO = float(os.environ.get('DISH_LIVE_ORDER_LAYOUT_MATCH_RATIO', '0.70'))
|
||||
|
||||
# YOLO 检测需直接丢弃的类别(不裁剪、不识别、不上报),逗号分隔
|
||||
# 模板库比对后需丢弃的菜名(YOLO 无此类时仍生效):命中「手」「空盘」等则整框过滤,不入清单/不上报
|
||||
# 环境变量名沿用 DISH_DETECT_FILTER_NAMES
|
||||
DISH_DETECT_FILTER_NAMES = [
|
||||
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
|
||||
if x.strip()
|
||||
]
|
||||
DISH_MATCH_FILTER_NAMES = DISH_DETECT_FILTER_NAMES
|
||||
# 贴边不完整框过滤:托盘刚入画只露一角时 YOLO 常会检出,直接丢弃
|
||||
# 框任一边缘距画面边界 ≤ margin 像素,视为贴边
|
||||
DISH_DETECT_EDGE_MARGIN_PX = int(os.environ.get('DISH_DETECT_EDGE_MARGIN_PX', '8'))
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,7 @@ class PendingLearningUIMixin:
|
|||
final_result = ""
|
||||
dish_labels = []
|
||||
dishes = []
|
||||
kept_boxes = []
|
||||
unknown_count = 0
|
||||
for i in range(n):
|
||||
if progress_callback:
|
||||
|
|
@ -1054,6 +1055,11 @@ class PendingLearningUIMixin:
|
|||
image_path=img_path if isinstance(img_path, str) else None,
|
||||
auto_pending=auto_pending,
|
||||
)
|
||||
# 模板命中手/空盘等:整框丢弃
|
||||
if detail.get("is_filtered"):
|
||||
filtered_as = detail.get("filtered_as") or detail.get("suggested_category")
|
||||
print(f"[match] skip dish box filtered_as={filtered_as}")
|
||||
continue
|
||||
if detail.get("is_unknown"):
|
||||
unknown_count += 1
|
||||
match_scope = detail.get("match_scope")
|
||||
|
|
@ -1064,10 +1070,12 @@ class PendingLearningUIMixin:
|
|||
label_src["det_conf"] = det_conf
|
||||
label_src["in_today_menu"] = in_menu
|
||||
label_src["match_scope"] = match_scope
|
||||
idx = len(dishes) + 1
|
||||
dish_labels.append(format_dish_box_label(label_src))
|
||||
final_result += f"【菜品{i + 1}】{format_predict_detail(detail)}\n"
|
||||
final_result += f"【菜品{idx}】{format_predict_detail(detail)}\n"
|
||||
kept_boxes.append(box)
|
||||
dishes.append({
|
||||
"index": i + 1,
|
||||
"index": idx,
|
||||
"label": detail.get("label"),
|
||||
"score": detail.get("score"),
|
||||
"det_conf": det_conf,
|
||||
|
|
@ -1083,10 +1091,12 @@ class PendingLearningUIMixin:
|
|||
"in_today_menu": in_menu,
|
||||
})
|
||||
except Exception as e:
|
||||
idx = len(dishes) + 1
|
||||
dish_labels.append("识别失败")
|
||||
final_result += f"【菜品{i + 1}】加载/识别失败:{e}\n"
|
||||
final_result += f"【菜品{idx}】加载/识别失败:{e}\n"
|
||||
kept_boxes.append(box)
|
||||
dishes.append({
|
||||
"index": i + 1,
|
||||
"index": idx,
|
||||
"label": None,
|
||||
"score": None,
|
||||
"det_conf": det_conf,
|
||||
|
|
@ -1109,7 +1119,7 @@ class PendingLearningUIMixin:
|
|||
]
|
||||
viz_styles = [dish_overlay_style(d) for d in dishes]
|
||||
labeled_path = self.build_dish_labeled_image(
|
||||
source_image_path, boxes, viz_labels, styles=viz_styles,
|
||||
source_image_path, kept_boxes, viz_labels, styles=viz_styles,
|
||||
)
|
||||
except Exception:
|
||||
labeled_path = None
|
||||
|
|
|
|||
Loading…
Reference in New Issue