——从人脸检测到朝向、表情与 HUD 状态显示

关键词:YOLO、MediaPipe、FaceLandmarker、NPC HUD、人脸语义分析、实时视觉系统


一、背景:当“人脸检测”不再只是画框

在很多视觉项目中,人脸检测通常止步于三件事:

  • 画一个框

  • 标一个置信度

  • 结束

但如果你做的是 互动系统 / 虚拟人 / 游戏化 UI / 实时感知,这远远不够。

你真正想要的是:

  • 这个“人”在看哪里?

  • 他有没有眨眼、张嘴?

  • 多个人同时出现时,状态是否稳定?

  • 能不能用 NPC HUD 的形式 展示这些信息?

这篇文章介绍一套 工程级方案,把“人脸检测”升级为:

多 NPC 的实时感知与状态展示系统


二、核心设计思想:职责拆分 + 语义叠加

整个系统遵循一个非常清晰的分层原则:

1️⃣ YOLO:负责“是谁 + 在哪”

  • 多人脸检测

  • 动态数量

  • 自带 tracking(track_id

2️⃣ MediaPipe:负责“这个人在做什么”

  • 精细 landmark(468 点)

  • 表情(BlendShapes)

  • 头部姿态(Transformation Matrix)

3️⃣ HUD 层:负责“如何让人看懂”

  • NPC 血条(置信度)

  • 朝向状态

  • 表情状态

  • 游戏化 UI 风格


三、为什么必须是 YOLO + MediaPipe 组合?

MediaPipe 的硬限制

MediaPipe FaceLandmarker 有一个绕不开的问题:

人脸数量必须在初始化时固定(num_faces

这在多脸场景中非常致命。

正确解法不是“强行调参数”

而是 架构层面的职责分离

  • YOLO 决定“有几张脸”

  • MediaPipe 永远只处理一张脸(ROI)

整帧 → YOLO 多脸检测 ↓ Face ROI × N ↓ MediaPipe(num_faces=1)

这样做的好处:

  • 不浪费算力

  • 不漏脸

  • 不需要多实例 MediaPipe

  • 工程稳定、可扩展


四、NPC 化 HUD:把检测结果变成“状态条”

1️⃣ 为什么用“血条”表示置信度?

置信度是一个 [0,1] 的连续值,天然适合映射成:

  • 血量

  • 能量条

  • 状态强度

在 HUD 中,它比数字更直观

HUD 设计规则

  • 宽度 = 检测框宽度

  • 高度固定(20px)

  • 黑底 + 绿色填充

  • 2px 边框

  • 不画检测框(去工程感)


五、语义分析:让 NPC “活起来”

1️⃣ 头部朝向(Direction)

通过 MediaPipe 输出的 Facial Transformation Matrix

yaw = mat[0][2] pitch = mat[1][2]

简单阈值即可区分:

  • Left / Right

  • Up / Down

  • Forward

这已经足够用于:

  • 注意力判断

  • 互动触发

  • 镜头引导


2️⃣ 表情状态(Expression)

使用 BlendShapes:

if blends.get("eyeBlinkLeft", 0) > 0.5: expression.append("Left Eye Closed")

当前实现的状态包括:

  • 左 / 右眼闭合

  • 张嘴

  • Neutral(默认)

这是 “语义级信息”,不是几何数据。


六、稳定可读:track_id → 固定颜色

多人同时出现时,最怕的不是“检测不准”,而是人分不清

解决方式很简单但非常关键:

同一个 track_id → 同一种满饱和度颜色

hue = int((track_id * 37) % 180)
  • 不随机

  • 不闪烁

  • 高区分度

  • 跨帧一致


七、完整实现代码(工程级)

特点:

  • YOLO 多脸 + tracking

  • MediaPipe 单脸语义分析

  • NPC HUD 状态展示

  • 原始画面不破坏

import cv2
import numpy as np
from ultralytics import YOLO

import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision


class YOLOFaceNPCAnalyzer:
    """
    YOLO 多脸检测 + MediaPipe 语义分析(二合一)

    HUD:
    - NPC 血条(黑底 + 绿色填充 + 2px 边框)
    - 两行状态文字(朝向 / 表情)
    """

    def __init__(
        self,
        yolo_model_path="yolov11l-face.pt",
        mp_model_path="文件地址/face_landmarker.task",
        device="cuda"
    ):
        # ---------- YOLO ----------
        self.yolo = YOLO(yolo_model_path).to(device)
        self.device = device

        # ---------- MediaPipe ----------
        base_options = python.BaseOptions(model_asset_path=mp_model_path)
        options = vision.FaceLandmarkerOptions(
            base_options=base_options,
            output_face_blendshapes=True,
            output_facial_transformation_matrixes=True,
            num_faces=1
        )
        self.detector = vision.FaceLandmarker.create_from_options(options)

    # =====================================================
    # MediaPipe detect
    # =====================================================
    def _detect_face(self, face_bgr):
        mp_image = mp.Image(
            image_format=mp.ImageFormat.SRGB,
            data=cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB)
        )
        return self.detector.detect(mp_image)

    # =====================================================
    # track_id → 满饱和度颜色
    # =====================================================
    def _color_by_track_id(self, track_id):
        hue = int((track_id * 37) % 180) if track_id is not None else 0
        hsv = np.uint8([[[hue, 255, 255]]])
        bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0][0]
        return int(bgr[0]), int(bgr[1]), int(bgr[2])

    # =====================================================
    # landmark 映射绘制(只画点)
    # =====================================================
    def _draw_landmarks(self, frame, face_landmarks, box, color):
        x1, y1, x2, y2 = box
        w, h = x2 - x1, y2 - y1

        for lm in face_landmarks:
            px = int(x1 + lm.x * w)
            py = int(y1 + lm.y * h)
            cv2.circle(frame, (px, py), 2, color, -1, cv2.LINE_AA)

    # =====================================================
    # 语义分析(朝向 + 表情)
    # =====================================================
    def _analyze_semantics(self, result):
        direction = "Forward"
        expression = []

        # ---------- 头部朝向 ----------
        if result.facial_transformation_matrixes:
            mat = result.facial_transformation_matrixes[0]
            yaw = mat[0][2]
            pitch = mat[1][2]

            if yaw > 0.15:
                direction = "Right"
            elif yaw < -0.15:
                direction = "Left"

            if pitch > 0.15:
                direction += " Down"
            elif pitch < -0.15:
                direction += " Up"

        # ---------- 表情 ----------
        if result.face_blendshapes:
            blends = {b.category_name: b.score for b in result.face_blendshapes[0]}

            if blends.get("eyeBlinkLeft", 0) > 0.5:
                expression.append("Left Eye Closed")
            if blends.get("eyeBlinkRight", 0) > 0.5:
                expression.append("Right Eye Closed")
            if blends.get("jawOpen", 0) > 0.4:
                expression.append("Mouth Open")

        expr_text = ", ".join(expression) if expression else "Neutral"
        return direction, expr_text

    # =====================================================
    # HUD(血条 + 边框 + 状态文本)
    # =====================================================
    def _draw_hud(self, frame, box, conf, direction, expression):
        x1, y1, x2, _ = box
        bar_h = 20
        border = 2

        bar_y2 = y1
        bar_y1 = max(0, y1 - bar_h)

        # ---------- 黑色背景 ----------
        cv2.rectangle(
            frame,
            (x1, bar_y1),
            (x2, bar_y2),
            (0, 0, 0),
            -1
        )

        # ---------- 绿色填充(血量) ----------
        inner_w = x2 - x1 - border * 2
        hp_w = int(inner_w * conf)

        cv2.rectangle(
            frame,
            (x1 + border, bar_y1 + border),
            (x1 + border + hp_w, bar_y2 - border),
            (0, 255, 0),
            -1
        )

        # ---------- 2px 边框 ----------
        cv2.rectangle(
            frame,
            (x1, bar_y1),
            (x2, bar_y2),
            (0, 255, 0),
            border
        )

        # ---------- 文本 ----------
        font = cv2.FONT_HERSHEY_SIMPLEX
        scale = 0.6
        thickness = 2

        cv2.putText(
            frame,
            f"Dir: {direction}",
            (x1 + 4, bar_y1 - 24),
            font,
            scale,
            (0, 255, 0),
            thickness,
            cv2.LINE_AA
        )

        cv2.putText(
            frame,
            f"Expr: {expression}",
            (x1 + 4, bar_y1 - 6),
            font,
            scale,
            (0, 255, 0),
            thickness,
            cv2.LINE_AA
        )

    # =====================================================
    # 对外入口(device 必须存在)
    # =====================================================
    def do(self, frame, device):
        if frame is None:
            return None

        output = frame.copy()
        h, w, _ = frame.shape

        results = self.yolo.track(
            frame,
            persist=True,
            verbose=False,
            device=device
        )[0]

        if results.boxes is None:
            return output

        boxes = results.boxes.xyxy.cpu().numpy()
        confs = results.boxes.conf.cpu().numpy()
        track_ids = (
            results.boxes.id.cpu().numpy()
            if results.boxes.id is not None
            else [None] * len(boxes)
        )

        for box, conf, track_id in zip(boxes, confs, track_ids):
            x1, y1, x2, y2 = map(int, box)
            x1, y1 = max(0, x1), max(0, y1)
            x2, y2 = min(w, x2), min(h, y2)

            face_crop = frame[y1:y2, x1:x2]
            if face_crop.size == 0:
                continue

            result = self._detect_face(face_crop)
            if not result.face_landmarks:
                continue

            color = self._color_by_track_id(track_id)

            self._draw_landmarks(
                output,
                result.face_landmarks[0],
                (x1, y1, x2, y2),
                color
            )

            direction, expression = self._analyze_semantics(result)

            self._draw_hud(
                output,
                (x1, y1, x2, y2),
                conf,
                direction,
                expression
            )

        return output

👉 实际项目中建议直接模块化复用此类结构


八、系统能力边界

✅ 已实现

  • 多人脸稳定追踪

  • 朝向判断

  • 表情识别

  • NPC HUD 可视化

  • 工程级稳定性

🔜 可自然扩展

  • 表情权重条(情绪值)

  • 注视目标判断

  • NPC 敌我阵营 UI

  • 行为触发(看你 / 对你眨眼)

  • 状态平滑(OneEuro / EMA)


九、总结

这套系统并不是“把几个库拼起来”,而是一个明确的感知架构

  • YOLO = 世界感知

  • MediaPipe = 个体理解

  • HUD = 人类可读接口

当你开始用 NPC 的视角 看待“人脸检测”,
你做的就已经不是 CV Demo,而是交互系统。

对 PiscTrace or PiscCode感兴趣?更多精彩内容请移步官网看看~🔗 PiscTrace

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐