给本地图库的“人“加上属性:DeepFace + MediaPipe Pose 联合分析(含 GitHub 镜像踩坑)

关键词:DeepFace.analyze、MediaPipe Pose 33 关键点、性别/年龄/族裔外貌、姿势分类、取景分类、SQLite ALTER TABLE 迁移、gh.idayer.com 镜像、enforce_detection=False 误报修正

一、背景

上一篇 我们做了一个"自动给图打 ImageNet 标签"的本地图库浏览器。但 ImageNet 标签对"人物"几乎没区分力------所有有人的图都被分到 groomsuitminiskirt 之类的衣物类里,搜不到"亚洲女性、约 30 岁、站姿、半身"这种维度。

这篇我们给图库里"图里的人"叠加结构化属性:

  • DeepFace:性别、年龄、族裔外貌;
  • MediaPipe PoseLandmarker:从 33 个人体关键点推姿势(站立/坐姿/双臂上举/插腰/抱臂...)和取景(全身/半身/上半身/仅面部)。

集成进上一篇的 image_indexer,前端加一排过滤器,能按这些维度组合检索。

声明 :种族/族裔识别在很多场景里有伦理争议。我把字段命名为 ethnic_appearance(族裔外貌 ),明示这是 DeepFace 基于像素的视觉判断,不是身份/国籍/血统。生产场景里用此功能前请评估法律和伦理风险。

"C:\Users\86182\Desktop\MediaPipe\demo_image_indexer.py"

二、工具能做什么、不能做什么

想要的属性 能 / 不能 用什么
性别 DeepFace gender
年龄 DeepFace age
族裔外貌 DeepFace race(6 类)
姿势 MediaPipe Pose + 几何规则
半身/全身 MediaPipe Pose 关键点可见性
衣着颜色 不能 需要 CLIP 或自训分类器
发型 / 发色 不能 同上
是否化妆 不能 同上,且训练数据极敏感
国籍 不能 视觉无法可靠判别,逻辑上不成立

我把"国籍"重新框定为"族裔外貌","衣着/发型/化妆"暂不做(要真做就得叠 CLIP,下一篇再写)。

三、安装与一个绕不开的网络坑

bash 复制代码
pip install deepface tensorflow opencv-python pillow

DeepFace 第一次调用 DeepFace.analyze自动从 GitHub Releases 下载权重。在国内网络环境下:

复制代码
github.com → raw.githubusercontent.com → 经常超时

DeepFace 内部直接 urlretrieve,没有 proxies 选项也不会自动重试。我在多次失败后用 gh.idayer.com 这个反代镜像手动下载,把三个文件放到 DeepFace 默认缓存目录:

复制代码
C:\Users\<you>\.deepface\weights\
├── age_model_weights.h5            (514 MB)
├── gender_model_weights.h5         (513 MB)
└── race_model_single_batch.h5      (513 MB)

镜像 URL 格式(在原始 GitHub 链接前加 https://gh.idayer.com/):

bash 复制代码
curl -L -C - -o age_model_weights.h5 \
  https://gh.idayer.com/https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5

-C - 是断点续传,512 MB 文件半路断了不用从 0 重来。

我亲测过 ghfast.topmirror.ghproxy.comghps.cc 都不稳,gh.idayer.com 是当前最可靠的(2026 年初)。如果镜像也不行,最简单的办法是去 GitHub Releases 页用浏览器手动下载然后丢进去。

下载完成一定要校验大小不能截断(一个特别迷的事是:DeepFace 加载残缺的 .h5 时会抛 OSError: Unable to synchronously open file (truncated file),但很多教程没提)。

四、DeepFace 的多张脸输出

DeepFace.analyze 有个有意思的设计:单脸返回 dict ,多脸返回 listdict。统一处理:

python 复制代码
results = DeepFace.analyze(
    img_path=img_bgr,                  # 可以直接传 numpy BGR
    actions=("age", "gender", "race"),
    detector_backend="opencv",         # 内置 cv2 Haar,最快
    enforce_detection=False,           # 没检到脸时也别抛异常
    silent=True,
)
if isinstance(results, dict):
    results = [results]

每个 r 字段:

python 复制代码
r["age"]                # int
r["gender"]             # {"Man": 12.3, "Woman": 87.7}
r["dominant_gender"]    # "Woman"
r["race"]               # {"asian": 60.5, "white": 30.2, ...}
r["dominant_race"]      # "asian"
r["region"]             # {"x":..., "y":..., "w":..., "h":...}

我同时保留 dominant_* 和最大值的 score:

python 复制代码
gender_dict = r.get("gender") or {}
gender = max(gender_dict, key=gender_dict.get)
gender_score = float(gender_dict[gender]) / 100.0

/100 是因为 DeepFace 给的是百分比,不是 0,1 概率。

五、enforce_detection=False 的副作用与修正

设了 enforce_detection=False,DeepFace 在没检到脸时也会返回结果------拿整张图当一张脸。这会污染数据。我加了 bbox 三重过滤:

python 复制代码
H, W = img_bgr.shape[:2]
if w <= 1 or h <= 1: continue                 # 无效尺寸
if w * h >= 0.95 * W * H: continue            # 整图回退
if w < 0.03 * W or h < 0.03 * H: continue     # 太小,多半是误检

加完之后大量"风景照里硬塞进一个 Man/27 岁/asian"的脏数据消失了。

六、MediaPipe PoseLandmarker

第一篇 HandLandmarker 同一套 Tasks API,模型换成 pose_landmarker_full.task(9 MB,Google CDN 直连):

复制代码
https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/latest/pose_landmarker_full.task
python 复制代码
opts = vision.PoseLandmarkerOptions(
    base_options=mp_python.BaseOptions(model_asset_path=str(POSE_MODEL)),
    running_mode=vision.RunningMode.IMAGE,        # 离线批处理
    num_poses=1,
    min_pose_detection_confidence=0.5,
)
landmarker = vision.PoseLandmarker.create_from_options(opts)
result = landmarker.detect(mp_image)              # 同步调用

返回 result.pose_landmarks[0] 是 33 个点的 list,每个点有 .x .y .z .visibilityvisibility 是关键:很多关节在画面外或被遮挡时分数低,正好让我们判断取景。

常用索引:

python 复制代码
LM_NOSE = 0
LM_L_SHOULDER, LM_R_SHOULDER = 11, 12
LM_L_HIP, LM_R_HIP = 23, 24
LM_L_KNEE, LM_R_KNEE = 25, 26
LM_L_ANKLE, LM_R_ANKLE = 27, 28

七、取景分类(face_only / upper_body / half_body / three_quarter / full_body)

简单但实用的可见性级联:

python 复制代码
POSE_VIS_THRESH = 0.5

def _classify_framing(vis):
    def seen(*idx):
        return all(vis.get(i, 0.0) >= POSE_VIS_THRESH for i in idx)
    if seen(LM_L_ANKLE) or seen(LM_R_ANKLE): return "full_body"
    if seen(LM_L_KNEE)  or seen(LM_R_KNEE):  return "three_quarter"
    if seen(LM_L_HIP)   or seen(LM_R_HIP):   return "half_body"
    if seen(LM_L_SHOULDER) or seen(LM_R_SHOULDER): return "upper_body"
    if seen(LM_NOSE):                        return "face_only"
    return "unknown"

seenor 容许遮挡------只要有一只脚踝可见就算全身入画。

八、姿势分类的几何规则

肩膀连线为参考,归一化坐标在 0,1 范围内,各种阈值都是经验值:

python 复制代码
def _classify_pose(landmarks, vis):
    def lm(i): return landmarks[i]
    def seen(*idx): return all(vis.get(i, 0.0) >= POSE_VIS_THRESH for i in idx)
    if not seen(LM_L_SHOULDER, LM_R_SHOULDER):
        return "unknown"

    sh_y = (lm(LM_L_SHOULDER).y + lm(LM_R_SHOULDER).y) / 2
    sh_x_l, sh_x_r = lm(LM_L_SHOULDER).x, lm(LM_R_SHOULDER).x

    # 双手腕都高于肩 5%:双臂上举
    if seen(LM_L_WRIST, LM_R_WRIST):
        if lm(LM_L_WRIST).y < sh_y - 0.05 and lm(LM_R_WRIST).y < sh_y - 0.05:
            return "arms_raised"

    # 手腕位于髋附近:插腰
    if seen(LM_L_WRIST, LM_R_WRIST, LM_L_HIP, LM_R_HIP):
        hip_y = (lm(LM_L_HIP).y + lm(LM_R_HIP).y) / 2
        l_close = abs(lm(LM_L_WRIST).y - hip_y) < 0.08 and abs(lm(LM_L_WRIST).x - lm(LM_L_HIP).x) < 0.1
        r_close = abs(lm(LM_R_WRIST).y - hip_y) < 0.08 and abs(lm(LM_R_WRIST).x - lm(LM_R_HIP).x) < 0.1
        if l_close and r_close:
            return "hands_on_hips"

    # 左腕靠近右肩 + 右腕靠近左肩:抱臂
    if seen(LM_L_WRIST, LM_R_WRIST):
        l_to_rsh = abs(lm(LM_L_WRIST).x - sh_x_r) + abs(lm(LM_L_WRIST).y - sh_y)
        r_to_lsh = abs(lm(LM_R_WRIST).x - sh_x_l) + abs(lm(LM_R_WRIST).y - sh_y)
        if l_to_rsh < 0.15 and r_to_lsh < 0.15:
            return "arms_crossed"

    # 髋y与膝y几乎相等:坐姿
    # (站立时膝远低于髋;坐下时它们几乎同高)
    if seen(LM_L_HIP, LM_L_KNEE) or seen(LM_R_HIP, LM_R_KNEE):
        # ...略
        if abs(np.mean(hips_y) - np.mean(knees_y)) < 0.08:
            return "sitting"

    if seen(LM_L_HIP) or seen(LM_R_HIP):
        return "standing"
    return "unknown"

判别力没有训练出来的姿势模型强,但是0 训练成本、几行代码、可解释------很适合"图书馆型"的批处理标注。后面如果想升级,可以把 33 个关键点拍平成 99 维向量训一个 MLP。

九、模块化:person_analyzer.py

把上面这些封到一个独立模块,索引器只调一个函数:

python 复制代码
@dataclass
class PersonAttrs:
    person_idx: int
    bbox: tuple[int, int, int, int] | None
    gender: Optional[str] = None
    gender_score: Optional[float] = None
    age: Optional[int] = None
    ethnic_appearance: Optional[str] = None
    ethnic_score: Optional[float] = None

@dataclass
class ImageAnalysis:
    persons: list[PersonAttrs] = field(default_factory=list)
    pose: Optional[str] = None
    framing: Optional[str] = None
    pose_keypoint_visibility: dict = field(default_factory=dict)
    error: Optional[str] = None

def analyze_image(path):
    out = ImageAnalysis()
    try:
        with Image.open(path) as im:
            arr_rgb = np.array(im.convert("RGB"))
        bgr = arr_rgb[:, :, ::-1].copy()
        out.persons = _analyze_persons(bgr)
        if out.persons:                              # 没人就不跑 pose
            pose, framing, vis = _analyze_pose(arr_rgb)
            out.pose, out.framing, out.pose_keypoint_visibility = pose, framing, vis
    except Exception as e:
        out.error = f"{type(e).__name__}: {e}"
    return out

两个函数都用模块级懒加载,第一次调用时初始化 DeepFace(其实 DeepFace 是首次调用 analyze 才载权重)和 PoseLandmarker,多次调用复用:

python 复制代码
_pose_landmarker = None

def _get_pose_landmarker():
    global _pose_landmarker
    if _pose_landmarker is None:
        _pose_landmarker = vision.PoseLandmarker.create_from_options(opts)
    return _pose_landmarker

十、SQLite 老库迁移

第一篇生成的 images 表没有 pose / framing 列。直接 ALTER TABLE

python 复制代码
cols = {r[1] for r in con.execute("PRAGMA table_info(images)").fetchall()}
if "pose" not in cols:
    con.execute("ALTER TABLE images ADD COLUMN pose TEXT")
if "framing" not in cols:
    con.execute("ALTER TABLE images ADD COLUMN framing TEXT")

人物属性单建一表,一对多(一张图可能多人):

sql 复制代码
CREATE TABLE IF NOT EXISTS person_attrs (
    image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE,
    person_idx INTEGER NOT NULL,
    gender TEXT, gender_score REAL,
    age INTEGER,
    ethnic_appearance TEXT, ethnic_score REAL,
    bbox_x0 INTEGER, bbox_y0 INTEGER, bbox_x1 INTEGER, bbox_y1 INTEGER
);
CREATE INDEX IF NOT EXISTS idx_person_image ON person_attrs(image_id);
CREATE INDEX IF NOT EXISTS idx_person_gender ON person_attrs(gender);
CREATE INDEX IF NOT EXISTS idx_person_age ON person_attrs(age);
CREATE INDEX IF NOT EXISTS idx_person_ethnic ON person_attrs(ethnic_appearance);

十一、动态 SQL:组合过滤器

GUI 里给了一排控件:性别 / 族裔 / 年龄区间 / 姿势 / 取景。用户可以勾任意子集。后端按勾的字段动态拼 SQL:

python 复制代码
def _query(self, f):
    clauses, params, joins = [], [], ""
    if "label" in f:
        joins += " JOIN labels l ON l.image_id = i.id"
        clauses.append("l.label LIKE ?"); params.append(f"%{f['label']}%")

    person_keys = {"gender", "ethnic", "age_min", "age_max"}
    if person_keys & f.keys():
        joins += " JOIN person_attrs p ON p.image_id = i.id"
        if "gender" in f: clauses.append("p.gender = ?"); params.append(f["gender"])
        if "ethnic" in f: clauses.append("p.ethnic_appearance = ?"); params.append(f["ethnic"])
        if "age_min" in f: clauses.append("p.age >= ?"); params.append(f["age_min"])
        if "age_max" in f: clauses.append("p.age <= ?"); params.append(f["age_max"])

    if "pose" in f:    clauses.append("i.pose = ?"); params.append(f["pose"])
    if "framing" in f: clauses.append("i.framing = ?"); params.append(f["framing"])

    where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
    sql = f"SELECT DISTINCT i.id, i.path, i.thumb, ... FROM images i {joins} {where} " \
          f"ORDER BY i.indexed_at DESC LIMIT 500"
    return con.execute(sql, params).fetchall()

SELECT DISTINCT 防止 JOIN labels/person_attrs 之后重复行------一张图的 5 个标签会让它出现 5 次。

十二、效果

我有一个 600 张图的"参考图册",里面是各种站姿、坐姿、人像照,跑了一遍:

  • 索引时间:~12 分钟(DeepFace 推理是大头,每张大约 1-2 秒,CPU);
  • DB 大小:从 30 MB 涨到 38 MB(缩略图占大头,属性 < 1 MB);
  • Gender=Woman, Framing=full_body, Pose=standing 过滤:返回 80 多张,瞄一眼基本都对。

姿势规则的局限:人像侧拍 / 半身镜头里手腕和髋盖经常被裁掉,规则就 fall back 到 standing/unknown。这是当前最大的遗憾。

十三、几个工程上的坑

1. TensorFlow 在 Windows 下首次 import 输出一堆警告

python 复制代码
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0")

放在 import deepface 之前。

2. 控制台 GBK 编码错误 :DeepFace 抛异常带 emoji ,Windows 控制台默认 GBK 直接挂掉。统一加:

python 复制代码
import sys
sys.stdout.reconfigure(encoding="utf-8")
# 或者
os.environ["PYTHONIOENCODING"] = "utf-8"

3. DeepFace 第一次调用很慢:要载入 age/gender/race 三个模型(共 1.5 GB)。子线程后台索引时这个延迟就不那么扎眼了。

4. 数据库 WAL 文件 :跑完之后看到 image_index.db-walimage_index.db-shm,不要手动删,正常关闭后会被合并。

十四、整体架构小结

复制代码
demo_image_indexer.py
    ├── DB 层(建表 + 迁移 + 缩略图 BLOB)
    ├── Classifier (MediaPipe ImageClassifier)
    ├── index_folder()
    │   └── 调 person_analyzer.analyze_image()  ←  本篇
    └── App (Tkinter Treeview + 过滤器 + 预览)

person_analyzer.py
    ├── _analyze_persons()  → DeepFace.analyze + bbox 过滤
    ├── _analyze_pose()     → MediaPipe PoseLandmarker + 几何规则
    └── analyze_image() → ImageAnalysis(persons, pose, framing)

十五、下一步

  • CLIP 开放词表ViT-B/32 ONNX 版本约 350 MB,把图和"穿红色连衣裙的女性"这类自然语言查询放到同一向量空间里,直接补衣着/发型/化妆维度;
  • 更稳的姿势识别:把 99 维关键点喂给一个 3 层 MLP,少量人工标注就能比规则强很多;
  • 重复图检测:pHash 或 CLIP embedding;
  • 多人去重 :当前同一张图里多人都进 person_attrs,但 DeepFace 在大合照上经常漏掉远处的脸------可以再叠 MediaPipe FaceDetector 取人脸候选,再喂给 DeepFace 强制 enforce 模式。

代码量上:person_analyzer.py 约 300 行,demo_image_indexer.py 集成后约 580 行。一个完整的"人物属性图库"从写第一行到能用,全部本地,三天不到。MediaPipe Tasks + DeepFace 这一对组合的性价比真的很高。

十六、下载的模型源地址和目标地址

================================================================================

本项目下载的所有模型文件清单

【一】MediaPipe 官方模型 (Google CDN, 直连可达)

  1. hand_landmarker.task 7.5 MB

    用途: 手部 21 关键点检测 (用于 demo_hand_landmarker.py)

    下载地址: https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task

    存放路径: C:\Users\86182\Desktop\MediaPipe\models\hand_landmarker.task

  2. face_detector.task 225 KB

    用途: 人脸检测 BlazeFace short-range (用于 demo_face_capture.py)

    下载地址: https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite

    存放路径: C:\Users\86182\Desktop\MediaPipe\models\face_detector.task

  3. efficientnet_lite0.tflite 18 MB

    用途: ImageNet 1000 类图像分类 (用于 demo_image_indexer.py)

    下载地址: https://storage.googleapis.com/mediapipe-models/image_classifier/efficientnet_lite0/float32/latest/efficientnet_lite0.tflite

    存放路径: C:\Users\86182\Desktop\MediaPipe\models\efficientnet_lite0.tflite

  4. pose_landmarker_full.task 9.0 MB

    用途: 人体 33 关键点姿势检测 (用于 person_analyzer.py)

    下载地址: https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_full/float16/latest/pose_landmarker_full.task

    存放路径: C:\Users\86182\Desktop\MediaPipe\models\pose_landmarker_full.task

【二】DeepFace 模型 (GitHub, 通过 gh.idayer.com 镜像下载)

  1. age_model_weights.h5 514 MB

    用途: 年龄回归 (DeepFace age 任务)

    原始地址: https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5

    镜像地址: https://gh.idayer.com/https://github.com/serengil/deepface_models/releases/download/v1.0/age_model_weights.h5

    存放路径: C:\Users\86182.deepface\weights\age_model_weights.h5

  2. gender_model_weights.h5 513 MB

    用途: 性别二分类 (DeepFace gender 任务)

    原始地址: https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5

    镜像地址: https://gh.idayer.com/https://github.com/serengil/deepface_models/releases/download/v1.0/gender_model_weights.h5

    存放路径: C:\Users\86182.deepface\weights\gender_model_weights.h5

  3. race_model_single_batch.h5 513 MB

    用途: 族裔外貌六分类 (DeepFace race 任务: asian/white/black/indian/middle_eastern/latino_hispanic)

    原始地址: https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5

    镜像地址: https://gh.idayer.com/https://github.com/serengil/deepface_models/releases/download/v1.0/race_model_single_batch.h5

    存放路径: C:\Users\86182.deepface\weights\race_model_single_batch.h5

【三】总计

文件总数: 7

项目模型目录 (C:\Users\86182\Desktop\MediaPipe\models): 约 35 MB

DeepFace 权重目录 (C:\Users\86182.deepface\weights): 约 1.6 GB

合计: 约 1.6 GB

注: pip 安装的 Python 包 (mediapipe / opencv / deepface / tensorflow 等)

位于 C:\Users\86182\Desktop\MediaPipe\venv, 不在本清单中。

相关推荐
cuso4win1 小时前
Agent 项目里的 Eval 到底是什么?怎么分类?不同项目应该怎么评测?
笔记·python·agent·eval
Metaphor6921 小时前
使用 Python 将 PDF 转换为 PDF/A
python·pdf
程序猿零零漆1 小时前
Python进阶之路:正则表达式、高级语法与核心数据结构(链表、二叉树)全解析
数据结构·python·正则表达式
码云骑士1 小时前
21-接手Django老项目(上)-环境复现与依赖地狱突围
后端·python·django
金銀銅鐵1 小时前
用 Tkinter 实现简单的 15 puzzle
后端·python
Dylan的码园1 小时前
python基础与快速入门
开发语言·python
石榴树下的七彩鱼1 小时前
图片去文字接口,支持去除图片中的文字(附 Python / Java / PHP / JS 示例)
java·python·php·api接口·图片去水印·ai图片修复·图片去文字
极光代码工作室1 小时前
基于机器学习的新闻分类系统
人工智能·python·深度学习·机器学习
枫叶v.1 小时前
Agent 开发架构:从增强型 LLM 到可运维的自治系统
开发语言·python