基于 MediaPipe 468 关键点提取几何特征,转 z 分数后与各脸型原型加权匹配。 参考分布与原型靶心取自 1093 张测试集的实测画像,不再靠人工设定绝对阈值。 方形脸占比从 29.6% 降到 13.8%,两处原因:一是参考统计量原先只由 50 张样本 估得,相对全量人群有系统性偏移,且三项偏移都在给方形脸加分;二是原型把 aspect_ratio 当作方形脸的主特征,但实测方脸组该值中位仅 +0.16,真正"宽"的 是圆脸(+1.01),等于在拿脸宽找方脸。 原型参数在「6 张基准标注图判定不变、且领先第二名 >=3 分」的约束下搜索得到。 余量约束是必要的:早前一版余量仅 0.008 分,权重写码时四舍五入就会翻转结论。 测试素材(人像照片)与报告输出体积大,一并加入 .gitignore。 Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
2.6 KiB
Python
84 lines
2.6 KiB
Python
"""
|
||
把各数据集的人脸特征抽取一次并缓存为 JSON,供调参脚本反复使用。
|
||
|
||
MediaPipe 关键点检测是调参循环里唯一的耗时环节,缓存后调参可以秒级迭代。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Dict, List
|
||
|
||
import cv2
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
||
from face_shape_classifier import ( # noqa: E402
|
||
_get_face_mesh,
|
||
extract_face_features,
|
||
)
|
||
|
||
ROOT = Path(__file__).resolve().parent
|
||
IMG_EXT = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
|
||
|
||
|
||
def iter_images(root: Path) -> List[Path]:
|
||
return sorted(p for p in root.rglob("*") if p.suffix.lower() in IMG_EXT)
|
||
|
||
|
||
def features_for(path: Path) -> Dict[str, float] | None:
|
||
bgr = cv2.imread(str(path))
|
||
if bgr is None:
|
||
return None
|
||
h, w = bgr.shape[:2]
|
||
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
||
res = _get_face_mesh().process(rgb)
|
||
if not res.multi_face_landmarks:
|
||
return None
|
||
return extract_face_features(res.multi_face_landmarks[0].landmark, image_size=(w, h))
|
||
|
||
|
||
def main() -> None:
|
||
out_path = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "cache" / "features.json"
|
||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
sources = {
|
||
# 6 张带标注的基准图,文件名即期望脸型
|
||
"benchmark": [p for p in ROOT.joinpath("test_img").glob("*.png")],
|
||
"girl": iter_images(ROOT / "test_img" / "girl"),
|
||
"man": iter_images(ROOT / "test_img" / "man"),
|
||
"dataset": iter_images(ROOT / "test_img" / "脸型测试集合"),
|
||
}
|
||
|
||
records = []
|
||
failed = 0
|
||
for source, paths in sources.items():
|
||
for i, path in enumerate(paths, 1):
|
||
feats = features_for(path)
|
||
if feats is None:
|
||
failed += 1
|
||
continue
|
||
rel = path.relative_to(ROOT)
|
||
records.append(
|
||
{
|
||
"source": source,
|
||
"path": rel.as_posix(),
|
||
"file": path.name,
|
||
# dataset 的上级目录名即原始分组(弱标签,非可信真值)
|
||
"group": path.parent.name if source == "dataset" else source,
|
||
"expected": path.stem if source == "benchmark" else None,
|
||
"features": feats,
|
||
}
|
||
)
|
||
if i % 50 == 0 or i == len(paths):
|
||
print(f"[{source}] {i}/{len(paths)}", flush=True)
|
||
|
||
out_path.write_text(json.dumps(records, ensure_ascii=False), encoding="utf-8")
|
||
print(f"\n写入 {out_path}:{len(records)} 条,检测失败 {failed} 张")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|