过滤手和空盘类别
This commit is contained in:
parent
a095f0b776
commit
bf98a717aa
|
|
@ -511,6 +511,31 @@ class PartTemplateManager:
|
||||||
return True, "·软阈值"
|
return True, "·软阈值"
|
||||||
return False, ""
|
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):
|
def predict_from_feature(self, feat, image_path=None, auto_pending=True):
|
||||||
"""已有特征时直接匹配(避免待学习提交时重复提特征)。
|
"""已有特征时直接匹配(避免待学习提交时重复提特征)。
|
||||||
|
|
||||||
|
|
@ -567,25 +592,53 @@ class PartTemplateManager:
|
||||||
if period_only:
|
if period_only:
|
||||||
staple_names = self.staple_candidate_names()
|
staple_names = self.staple_candidate_names()
|
||||||
period_set = set(period_names or [])
|
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, _, _ = (
|
ranked, _, _ = (
|
||||||
self._rank_categories(feat, allowed_names=allowed)
|
self._rank_categories(feat, allowed_names=allowed)
|
||||||
if allowed else ([], None, -1.0)
|
if allowed else ([], None, -1.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 从合并结果里拆出主食 / 餐段各自最高分
|
# 从合并结果里拆出主食 / 餐段 / 过滤类各自最高分
|
||||||
best_st, max_st = None, -1.0
|
best_st, max_st = None, -1.0
|
||||||
best_p, max_p = None, -1.0
|
best_p, max_p = None, -1.0
|
||||||
|
best_f, max_f = None, -1.0
|
||||||
for name, score in ranked:
|
for name, score in ranked:
|
||||||
sc = float(score)
|
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:
|
if name in staple_names and sc > max_st:
|
||||||
best_st, max_st = name, sc
|
best_st, max_st = name, sc
|
||||||
if name in period_set and sc > max_p:
|
if name in period_set and sc > max_p:
|
||||||
best_p, max_p = name, sc
|
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)
|
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)
|
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
|
chosen = None
|
||||||
how = ""
|
how = ""
|
||||||
scope = "period_miss"
|
scope = "period_miss"
|
||||||
|
|
@ -597,10 +650,12 @@ class PartTemplateManager:
|
||||||
elif st_hit:
|
elif st_hit:
|
||||||
chosen, how, scope = best_st, f"主食+餐段{st_how}", "staple"
|
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:
|
if chosen is not None:
|
||||||
score = float(max_st) if scope == "staple" else float(max_p)
|
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})"
|
text = f"{chosen} 相似度:{score:.3f}({how})"
|
||||||
return {
|
return {
|
||||||
"text": text,
|
"text": text,
|
||||||
|
|
@ -615,11 +670,19 @@ class PartTemplateManager:
|
||||||
}
|
}
|
||||||
|
|
||||||
sug, sug_score = None, -1.0
|
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:
|
if best_st is not None and float(max_st) > sug_score:
|
||||||
sug, sug_score = best_st, float(max_st)
|
sug, sug_score = best_st, float(max_st)
|
||||||
if best_p is not None and float(max_p) > sug_score:
|
if best_p is not None and float(max_p) > sug_score:
|
||||||
sug, sug_score = best_p, float(max_p)
|
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
|
pending_id = None
|
||||||
if auto_pending and image_path:
|
if auto_pending and image_path:
|
||||||
pending_id = self.pending_store.add(image_path, feat, sug, sug_score)
|
pending_id = self.pending_store.add(image_path, feat, sug, sug_score)
|
||||||
|
|
|
||||||
|
|
@ -738,6 +738,7 @@ class PendingLearningUIMixin:
|
||||||
final_result = ""
|
final_result = ""
|
||||||
dish_labels = []
|
dish_labels = []
|
||||||
dishes = []
|
dishes = []
|
||||||
|
kept_boxes = []
|
||||||
unknown_count = 0
|
unknown_count = 0
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
|
|
@ -750,12 +751,17 @@ class PendingLearningUIMixin:
|
||||||
image_path=img_path if isinstance(img_path, str) else None,
|
image_path=img_path if isinstance(img_path, str) else None,
|
||||||
auto_pending=auto_pending,
|
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"):
|
if detail.get("is_unknown"):
|
||||||
unknown_count += 1
|
unknown_count += 1
|
||||||
|
idx = len(dishes) + 1
|
||||||
dish_labels.append(format_dish_box_label(detail))
|
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({
|
dishes.append({
|
||||||
"index": i + 1,
|
"index": idx,
|
||||||
"label": detail.get("label"),
|
"label": detail.get("label"),
|
||||||
"score": detail.get("score"),
|
"score": detail.get("score"),
|
||||||
"is_unknown": bool(detail.get("is_unknown")),
|
"is_unknown": bool(detail.get("is_unknown")),
|
||||||
|
|
@ -768,10 +774,12 @@ class PendingLearningUIMixin:
|
||||||
"feature": detail.get("feature"),
|
"feature": detail.get("feature"),
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
idx = len(dishes) + 1
|
||||||
dish_labels.append("识别失败")
|
dish_labels.append("识别失败")
|
||||||
final_result += f"【菜品{i + 1}】加载/识别失败:{e}\n"
|
final_result += f"【菜品{idx}】加载/识别失败:{e}\n"
|
||||||
|
kept_boxes.append(box)
|
||||||
dishes.append({
|
dishes.append({
|
||||||
"index": i + 1,
|
"index": idx,
|
||||||
"label": None,
|
"label": None,
|
||||||
"score": None,
|
"score": None,
|
||||||
"is_unknown": True,
|
"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}")
|
(d.get("label") or d.get("suggested_category") or f"菜品{d.get('index') or i + 1}")
|
||||||
for i, d in enumerate(dishes)
|
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")
|
_mark("batch: draw_labels done")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_mark(f"batch: draw_labels fail {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_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'))
|
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 = [
|
DISH_DETECT_FILTER_NAMES = [
|
||||||
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
|
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
|
||||||
if x.strip()
|
if x.strip()
|
||||||
]
|
]
|
||||||
|
DISH_MATCH_FILTER_NAMES = DISH_DETECT_FILTER_NAMES
|
||||||
# 贴边不完整框过滤:托盘刚入画只露一角时 YOLO 常会检出,直接丢弃
|
# 贴边不完整框过滤:托盘刚入画只露一角时 YOLO 常会检出,直接丢弃
|
||||||
# 框任一边缘距画面边界 ≤ margin 像素,视为贴边
|
# 框任一边缘距画面边界 ≤ margin 像素,视为贴边
|
||||||
DISH_DETECT_EDGE_MARGIN_PX = int(os.environ.get('DISH_DETECT_EDGE_MARGIN_PX', '8'))
|
DISH_DETECT_EDGE_MARGIN_PX = int(os.environ.get('DISH_DETECT_EDGE_MARGIN_PX', '8'))
|
||||||
|
|
|
||||||
|
|
@ -1041,6 +1041,7 @@ class PendingLearningUIMixin:
|
||||||
final_result = ""
|
final_result = ""
|
||||||
dish_labels = []
|
dish_labels = []
|
||||||
dishes = []
|
dishes = []
|
||||||
|
kept_boxes = []
|
||||||
unknown_count = 0
|
unknown_count = 0
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
|
|
@ -1054,6 +1055,11 @@ class PendingLearningUIMixin:
|
||||||
image_path=img_path if isinstance(img_path, str) else None,
|
image_path=img_path if isinstance(img_path, str) else None,
|
||||||
auto_pending=auto_pending,
|
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"):
|
if detail.get("is_unknown"):
|
||||||
unknown_count += 1
|
unknown_count += 1
|
||||||
match_scope = detail.get("match_scope")
|
match_scope = detail.get("match_scope")
|
||||||
|
|
@ -1064,10 +1070,12 @@ class PendingLearningUIMixin:
|
||||||
label_src["det_conf"] = det_conf
|
label_src["det_conf"] = det_conf
|
||||||
label_src["in_today_menu"] = in_menu
|
label_src["in_today_menu"] = in_menu
|
||||||
label_src["match_scope"] = match_scope
|
label_src["match_scope"] = match_scope
|
||||||
|
idx = len(dishes) + 1
|
||||||
dish_labels.append(format_dish_box_label(label_src))
|
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({
|
dishes.append({
|
||||||
"index": i + 1,
|
"index": idx,
|
||||||
"label": detail.get("label"),
|
"label": detail.get("label"),
|
||||||
"score": detail.get("score"),
|
"score": detail.get("score"),
|
||||||
"det_conf": det_conf,
|
"det_conf": det_conf,
|
||||||
|
|
@ -1083,10 +1091,12 @@ class PendingLearningUIMixin:
|
||||||
"in_today_menu": in_menu,
|
"in_today_menu": in_menu,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
idx = len(dishes) + 1
|
||||||
dish_labels.append("识别失败")
|
dish_labels.append("识别失败")
|
||||||
final_result += f"【菜品{i + 1}】加载/识别失败:{e}\n"
|
final_result += f"【菜品{idx}】加载/识别失败:{e}\n"
|
||||||
|
kept_boxes.append(box)
|
||||||
dishes.append({
|
dishes.append({
|
||||||
"index": i + 1,
|
"index": idx,
|
||||||
"label": None,
|
"label": None,
|
||||||
"score": None,
|
"score": None,
|
||||||
"det_conf": det_conf,
|
"det_conf": det_conf,
|
||||||
|
|
@ -1109,7 +1119,7 @@ class PendingLearningUIMixin:
|
||||||
]
|
]
|
||||||
viz_styles = [dish_overlay_style(d) for d in dishes]
|
viz_styles = [dish_overlay_style(d) for d in dishes]
|
||||||
labeled_path = self.build_dish_labeled_image(
|
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:
|
except Exception:
|
||||||
labeled_path = None
|
labeled_path = None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue