feat(fam-edge): 赤膊成年人直判爸爸(免费规则) - 用户确认家里只有爸爸会光膀子
用户原话:"赤裸的大人都是爸爸,家里没有其他人会赤裸"——加进闭集识别的免费 规则层(跟汤圆/媳妇同级),命中就不用再调用视觉大模型比对参考图,比 auto_id 更快更准。只对成年人生效(判断顺序上幼儿/儿童年龄档在前面,小孩光膀子玩很 正常,不适用这条规则)。 排查历史数据时用这条规则筛出了一个真实误判:video 1092 的一次赤膊出现被 auto_id(NVIDIA 视觉比对)错误识别成"爷爷",实际是"爸爸"。已用 correct_video_identity 手动纠正(标记 source=manual,受保护不会被以后的自动 识别覆盖回去),描述文字已确认同步更新。 顺带确认了另一批命中同样关键词的历史数据(74 条里有 3 条是"汤圆"光膀子) 是正确的——3 岁小孩光膀子玩正常,是通过幼儿/儿童年龄档规则识别出来的,跟 赤膊无关,不是误判,没有动。 新增 4 个测试:赤膊成年男性直判爸爸且不调用大模型(用会报错的假 person_identifier 验证短路生效)、覆盖各种赤膊近义词表述、赤膊小孩仍然 正确判成汤圆不受影响、没有赤膊关键词时行为不变照常走大模型比对。
This commit is contained in:
@@ -514,25 +514,38 @@ class VideoProcessor:
|
||||
f"events={len(norm_events)}, people={people}, "
|
||||
f"with_features={len(uid_features)}, 闭集识别={resolved}")
|
||||
|
||||
# 家里赤膊/赤裸上身的成年人只有爸爸一个(用户原话:"赤裸的大人都是爸爸,
|
||||
# 家里没有其他人会赤裸")——命中即免费直判,不用等视觉大模型比对,比
|
||||
# auto_id 更快更准(实测已经纠正过一条被 auto_id 误判成爷爷的案例)。
|
||||
# 只匹配成年人(外层已经先过滤掉 age_band 是幼儿/儿童的),小孩光膀子玩
|
||||
# 很正常,不适用这条规则。
|
||||
_SHIRTLESS_KEYWORDS = ('赤裸', '光着上身', '赤膊', '裸体', '光膀子',
|
||||
'上身赤裸', '未穿上衣')
|
||||
|
||||
def _resolve_closed_set_identities(self, video_id: int, uid_features: Dict,
|
||||
norm_events: List[Dict]) -> Dict[str, tuple]:
|
||||
"""闭集人物识别:返回 {uid: (canonical_name, source)}。
|
||||
|
||||
汤圆(幼儿/儿童特征)、媳妇(唯一成年女性)靠 Gemini 已经产出的性别/年龄
|
||||
字段直接判断(source='rule'),验证过命中率接近 100%,不需要额外模型调用。
|
||||
爷爷/爸爸两个成年男性外观规则/人脸向量都区分不开(验证过),改用视觉大模型
|
||||
比对参考图(source='auto_id'),每个 uid 只取本片段内最大 bbox 的一次出现
|
||||
判断一次,不逐事件重复调用。
|
||||
汤圆(幼儿/儿童特征)、媳妇(唯一成年女性)、赤膊成年人(家里只有爸爸
|
||||
会赤膊)靠 Gemini 已经产出的性别/年龄/衣着字段直接判断(source='rule'),
|
||||
验证过命中率接近 100%,不需要额外模型调用。爷爷/爸爸两个成年男性穿戴
|
||||
整齐时外观规则/人脸向量都区分不开(验证过),改用视觉大模型比对参考图
|
||||
(source='auto_id'),每个 uid 只取本片段内最大 bbox 的一次出现判断
|
||||
一次,不逐事件重复调用。
|
||||
"""
|
||||
resolved: Dict[str, tuple] = {}
|
||||
for uid, feats in uid_features.items():
|
||||
gender = str(feats.get('gender', '') or '').strip()
|
||||
age_band = str(feats.get('age_band', '') or '').strip()
|
||||
clothing = str(feats.get('clothing', '') or '')
|
||||
if age_band in ('幼儿', '儿童'):
|
||||
resolved[uid] = ('汤圆', 'rule')
|
||||
elif gender == '女':
|
||||
resolved[uid] = ('媳妇', 'rule')
|
||||
elif gender == '男':
|
||||
if any(k in clothing for k in self._SHIRTLESS_KEYWORDS):
|
||||
resolved[uid] = ('爸爸', 'rule')
|
||||
continue
|
||||
crop = self._best_crop_for_uid(video_id, uid, norm_events)
|
||||
if crop:
|
||||
person = self.person_identifier.classify_adult_male(crop)
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fam_edge.video_processor import (
|
||||
VideoProcessor,
|
||||
_parse_event_start_from_filename,
|
||||
_parse_event_ts,
|
||||
_clean_person,
|
||||
)
|
||||
|
||||
|
||||
class _RaisingPersonIdentifier:
|
||||
"""用于验证"命中免费规则就不该再调用大模型比对"——一旦被调用直接报错,
|
||||
测试能立刻发现规则短路失败。"""
|
||||
def classify_adult_male(self, crop):
|
||||
raise AssertionError("命中了免费规则的 uid 不该再走 person_identifier")
|
||||
|
||||
|
||||
class _FixedPersonIdentifier:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
def classify_adult_male(self, crop):
|
||||
return self._name
|
||||
|
||||
|
||||
def _bare_processor(person_identifier):
|
||||
"""跳过 __init__(不需要真的加载 config/建适配器),只测
|
||||
_resolve_closed_set_identities 这一个纯逻辑方法。"""
|
||||
vp = VideoProcessor.__new__(VideoProcessor)
|
||||
vp.person_identifier = person_identifier
|
||||
return vp
|
||||
|
||||
|
||||
def test_parse_filename_pure_digit_format():
|
||||
assert _parse_event_start_from_filename(
|
||||
"Generic_ONVIF-001-20260820-140416-1787205856321-7.mp4"
|
||||
@@ -56,3 +80,42 @@ def test_clean_person_strips_ascii_parens():
|
||||
|
||||
def test_clean_person_no_parens_unchanged():
|
||||
assert _clean_person("汤圆") == "汤圆"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 闭集人物识别:赤膊成年人规则(用户原话:"赤裸的大人都是爸爸,家里没有
|
||||
# 其他人会赤裸")——命中即免费直判,不调用视觉大模型比对。
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def test_shirtless_adult_male_resolves_to_dad_without_vlm_call():
|
||||
vp = _bare_processor(_RaisingPersonIdentifier())
|
||||
resolved = vp._resolve_closed_set_identities(
|
||||
1, {"人物A": {"gender": "男", "age_band": "中年", "clothing": "赤膊+深色长裤"}}, [])
|
||||
assert resolved == {"人物A": ("爸爸", "rule")}
|
||||
|
||||
|
||||
def test_shirtless_variants_all_match():
|
||||
for phrase in ("光着上身", "赤膊", "裸体", "光膀子", "上身赤裸", "未穿上衣", "深色长裤,赤裸上身"):
|
||||
vp = _bare_processor(_RaisingPersonIdentifier())
|
||||
resolved = vp._resolve_closed_set_identities(
|
||||
1, {"人物A": {"gender": "男", "age_band": "中年", "clothing": phrase}}, [])
|
||||
assert resolved.get("人物A") == ("爸爸", "rule"), f"未命中: {phrase}"
|
||||
|
||||
|
||||
def test_shirtless_child_still_resolves_to_child_not_dad():
|
||||
"""核心诉求: 用户的规则明确是"赤裸的大人",小孩光膀子玩很正常,不适用
|
||||
这条规则——幼儿/儿童年龄档要走在赤膊判断前面,不能被误判成爸爸。"""
|
||||
vp = _bare_processor(_RaisingPersonIdentifier())
|
||||
resolved = vp._resolve_closed_set_identities(
|
||||
1, {"人物A": {"gender": "男", "age_band": "幼儿", "clothing": "赤裸上身+深色短裤"}}, [])
|
||||
assert resolved == {"人物A": ("汤圆", "rule")}
|
||||
|
||||
|
||||
def test_clothed_adult_male_still_falls_through_to_vlm(monkeypatch):
|
||||
"""核心诉求: 没有赤膊关键词的正常穿戴场景,行为不变——照常走视觉大模型
|
||||
比对(这里用假的 _best_crop_for_uid 避免真的需要 norm_events 数据)。"""
|
||||
vp = _bare_processor(_FixedPersonIdentifier("爷爷"))
|
||||
monkeypatch.setattr(vp, "_best_crop_for_uid", lambda *a, **k: b"fake-jpeg-bytes")
|
||||
resolved = vp._resolve_closed_set_identities(
|
||||
1, {"人物A": {"gender": "男", "age_band": "中年", "clothing": "蓝色Polo衫"}}, [])
|
||||
assert resolved == {"人物A": ("爷爷", "auto_id")}
|
||||
|
||||
Reference in New Issue
Block a user