2026-05-05 21:44:42 +08:00
2026-05-05 21:44:42 +08:00
2026-04-26 22:42:10 +08:00
2026-04-27 22:15:09 +08:00
2025-11-12 10:53:41 +08:00
2025-10-21 18:56:09 +08:00
2026-05-05 21:44:42 +08:00
2025-10-20 18:44:36 +08:00
2026-04-26 22:42:10 +08:00
2026-04-23 11:29:18 +08:00
2025-11-12 10:53:41 +08:00
2026-04-25 16:42:20 +08:00
2025-12-20 22:34:23 +08:00
2026-04-23 11:29:18 +08:00
2026-04-26 00:12:56 +08:00

Face SDK 使用说明

基于 Vulkan + MediaPipe FaceLandmarker 的实时人脸贴图渲染 SDK,主要面向美妆 / AR 试妆场景:摄像头预览 → 人脸 landmark 检测 → 多帧 PNG 贴图按 motion 序列贴在人脸上。

支持任意分辨率手机(采用"短边正方形画布 + 长边 letterbox"布局)、前置 / 后置摄像头自动切换、镜子镜像效果。


目录结构

face_sdk/
├── app/                      ← SDK 主体(可作为 AAR 给业务方接入)
│   ├── src/main/java/com/hmwl/face_sdk/
│   │   ├── FaceActivity.java       ← 业务方继承的核心 Activity
│   │   ├── FaceLandmarkerHelper.kt ← MediaPipe 封装
│   │   ├── DebugLog.java           ← Java/C++ 统一日志(写文件)
│   │   ├── InitArg.java / Motion.java / MotionList.java / Frame.java ← JSON 配置 POJO
│   │   └── ...
│   ├── src/main/cpp/main.cpp       ← JNI + native 主循环
│   └── src/main/assets/shaders/    ← Vulkan GLSLbg/texture/...
│
├── vulkan/                   ← 跨平台 Vulkan 渲染核心
│   ├── Application.{h,cpp}         ← Vulkan 初始化 / 窗口生命周期
│   └── FaceApp.{h,cpp}             ← 业务渲染(face mesh + bg quad + motion 调度)
│
├── example/                  ← 示例 app,演示完整接入
│   ├── src/main/java/com/inewme/uvmirror/
│   │   ├── MainActivity.kt         ← 入口(Compose UI,前/后置选择)
│   │   ├── MakeupActivity.kt       ← 业务 Activity(继承 FaceActivity
│   │   └── MotionManager.kt        ← motion 资源加载封装
│   ├── src/main/AndroidManifest.xml
│   └── src/main/assets/pic/
│       ├── motion_data_ex.json     ← motion 元数据(帧文件名 + 锚点 x/y)
│       └── <motion_id>/000xxx.png  ← motion 各帧贴图
│
├── compile_shader.bat        ← 编译所有 .vert/.frag 为 .spv(必须在改 shader 后执行)
└── third_party/              ← VMA / glm / glfw / mediapipe …

快速上手

1. 编译 + 运行 example

# Windows 上,确保 SDK 路径在 local.properties 里,以及 NDK 已安装
.\gradlew.bat :example:installDebug

启动后:

  • 主界面有"前置摄像头 / 后置摄像头"两个 RadioButton(默认前置)
  • 点击 "Go to Makeup" 进入 MakeupActivity
  • MakeupActivity 自动加载 assets/pic/motion_data_ex.json 配置的 motion,按 update() 里的脚本播放

2. 改 shader 后必须重编

.\compile_shader.bat
.\gradlew.bat :example:installDebug

在自己的 App 里集成

Step 1:依赖 SDK 模块

在你的 settings.gradle / build.gradle 引入 :app 模块(或打包成 AAR)。

Step 2:在 AndroidManifest.xml 声明业务 Activity

业务 Activity 必须竖屏,且屏蔽常见 configChanges 不要重建

<activity
    android:name=".MakeupActivity"
    android:exported="false"
    android:screenOrientation="portrait"
    android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|smallestScreenSize" />

Step 3:继承 FaceActivity 写业务 Activity

class MakeupActivity : FaceActivity() {
    lateinit var motionMgr: MotionManager
    private var isMotionLoadFinished = false

    override fun onCreate(savedInstanceState: Bundle?) {
        // 在 super.onCreate 之前构造 MotionManager(它会调 SetInitArg
        motionMgr = MotionManager(
            this,
            /* fps      */ 10,
            /* zoom     */ 1.5f,
            /* r,g,b    */ 0f, 0f, 1f,        // letterbox / 圆外纯色(蓝)
            /* radius   */ 240f,              // 480 设计基线下的圆半径,SDK 自动按 canvas_size/480 缩放
            /* offset   */ 200f, -200f         // 480 设计基线下的中心偏移
        )

        super.onCreate(savedInstanceState)

        // 一次性加载所有要用到的 motion,加载完才能 PlayMotionList
        motionMgr.LoadMotionList(listOf("4", "56")) {
            isMotionLoadFinished = true
        }

        // 自己的 update 循环(生命周期感知,避免泄漏)
        lifecycleScope.launch {
            while (true) {
                update()
                delay(10)
            }
        }
    }

    private fun update() {
        if (!isMotionLoadFinished) return
        // 想播什么就调,调 SDK API(详见下面"API 参考"
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            Stop()      // ← 退出 Activity 前必须先 Stop(),否则 native 线程仍在跑
            finish()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }
}

Step 4:从入口 Activity 启动业务 Activity,传入摄像头朝向

import androidx.camera.core.CameraSelector
import com.hmwl.face_sdk.FaceActivity

val intent = Intent(context, MakeupActivity::class.java).apply {
    // 前置:CameraSelector.LENS_FACING_FRONT;后置:LENS_FACING_BACK
    // 不传时 SDK 默认走后置(向后兼容)
    putExtra(FaceActivity.EXTRA_LENS_FACING, CameraSelector.LENS_FACING_FRONT)
}
context.startActivity(intent)

前置摄像头 SDK 会自动:

  • 在 GPU 端把 bg + face mesh 整体水平翻转 → 镜子效果
  • mediapipe 输入仍是 raw(未镜像)帧,所以 landmark 与 raw 帧 UV 严格对齐

API 参考(FaceActivity 公开方法)

方法 说明
static final String EXTRA_LENS_FACING Intent extra key,值为 CameraSelector.LENS_FACING_FRONTLENS_FACING_BACK
void SetInitArg(String json) 设置全局参数(zoom / r,g,b / radius / offset_x,offset_y / action_fps),JSON 格式见 InitArg
String PreLoadAction(String json, LoadMotionListFinishedCallback cb) 异步预加载一组 motion 资源;加载完毕回调 cb
void PlayMotionList(List<String> motionList, boolean loop, PlayMotionListFinishedCallback cb) 播放 motion 序列,loop=true 时无限循环,cb 为播完一轮的回调(loop 模式下不会触发,传 null 即可)
void StopMotion() 暂停当前 motion 播放(停在当前帧)
void ResumeMotion() 恢复 StopMotion() 之前的播放进度
void Stop() 完全停止 native 渲染,退出 Activity 前必须调

注意: LoadMotionList / PreLoadAction 是异步的,回调里设置 isMotionLoadFinished=true 之后才能 PlayMotionList,否则播放命令会被静默丢弃。


资源准备

Motion 帧图

每个 motion 用一个独立目录,PNG 帧按文件名顺序播放:

assets/pic/4/000000.png
assets/pic/4/000001.png
...
assets/pic/4/000031.png

PNG 必须是 RGBA(透明通道),SDK 直接采样后 alpha-blend 到 bg 上方。

motion_data_ex.json

motion 元数据,描述每帧贴图的锚点偏移(贴在脸上的相对位置,画布坐标系,原点在屏幕中心):

{
  "4": {
    "000000.png": { "x": 0.0, "y": 0.0 },
    "000001.png": { "x": 0.0, "y": 0.0 },
    ...
  },
  "56": {
    "000000.png": { "x": 0.0, "y": 0.0 },
    ...
  }
}

设计要点(接入方可不读,但要改 shader / 渲染时必读)

1. 画布与 letterbox

  • 画布:屏幕短边的正方形,居中。屏幕长边方向自然 letterbox(上下或左右黑边)。
  • 画布坐标系("画布 NDC")∈ [-1, +1]²,原点在画布中心。
  • mediapipe FaceLandmarker 输出的归一化 landmark ∈ [0, 1]² 直接对应画布 NDC(经过 *2-1)。
  • 业务参数 zoom / offset_x / offset_y / radius 都是屏幕物理像素SDK 端按 canvas_size / 480 自动缩放,业务方用 480 设计基线即可)。

2. 摄像头帧 → 画布的几何流水线

raw 摄像头帧 (W×H, 4:3 典型)
  └─① 中央裁切 min(W,H)×min(W,H) 正方形
      └─② Matrix.postRotate(rotationDegrees) 顺时针旋转 → 正立 face crop
          └─③ MediaPipe 输出 landmark ∈ [0,1]² ─────▶ 画布 NDC
                                                         └─ shader 做反向变换贴回 raw 帧 UV (bg)
                                                         └─ shader 直接当 face mesh 顶点 (face)

3. 镜子效果(前置摄像头)

PushConstants.mirror_x ∈ {0.0, 1.0}

  • 前置时 = 1.0shader 里把最终 NDC.x 翻转 (pos.x *= 1.0 - 2.0*mirror_x)
  • bg + face mesh 都做同样翻转 → 整体水平镜像,对齐保持
  • mediapipe 仍接收未镜像的 raw 帧,避免双重镜像把 landmark 弄反

4. bg 与 face mesh 对齐不变量

bg 圆内显示摄像头视频,face mesh 把 motion png 贴到 landmark 位置——两者对齐依赖:

屏幕同一像素位置,bg 显示的 raw UV = 把这个屏幕位置反算回 mediapipe landmark 坐标

实现方式:bg quad 与 face mesh 完全相同地经过 zoom × offset × letterbox × mirror 几何链路;同时 bg 的 outTexCoord 基于"未变换的 quad 顶点 pos"算 raw UV——这样屏幕 fragment 反算的 pos 就等价于"对应 landmark 的画布 NDC"UV 完美贴合。

bg quad 顶点取 ±10(而非 ±1),保证任何 zoom/offset 下 quad 在屏幕上 GPU clip 后整屏覆盖,letterbox 也被 bg.frag 的 r/g/b 蓝色填充。

5. 摄像头分辨率适配

  • CameraX 用 setTargetResolution(640, 480) 强制拿低分辨率帧(SDK 不需要高分辨率)
  • shader 用 camera_aspect = W/Hcamera_rotationCameraX 给的 rotationDegrees)做 UV 反变换
  • 摄像头帧分辨率/朝向变化时,SDK 会在 processCameraFrame 里自动重建 bg 纹理

6. 窗口生命周期

  • APP_CMD_TERM_WINDOWApplication::cleanupForWindowLost():只销毁窗口资源surface / swapchain / framebuffers / imageViews / commandBuffers / sync),保留 renderPass / pipelines / device / VMA / textures
  • APP_CMD_INIT_WINDOWApplication::reinitForNewWindow():复用全局资源,只重建窗口资源
  • android_main 退出时不再额外销毁 Vulkan 资源(保留给下次 NativeActivity 复用),避免 use-after-free 崩溃

调试

日志文件

SDK 把 Java/Kotlin 日志(通过 com.hmwl.face_sdk.DebugLog.i / e)和 C++ 日志(DebugLog::log统一写入

/data/user/0/<your.package>/files/face_sdk_debug.log
# 拉到本地分析
adb pull /data/user/0/com.inewme.uvmirror.f20260113/files/face_sdk_debug.log .

崩溃时还会自动追加一段含信号、寄存器、backtrace 的 dump========== FACE_SDK CRASH ==========)。

常见问题

现象 排查方向
黑屏 / 没有摄像头预览 检查相机权限是否授予;查看日志是否有 processImageNative 调用
人脸贴图不出现 检查 motion_data_ex.json 里的 motion id 是否在 LoadMotionList 里加载过;检查 isMotionLoadFinished 是否被回调置 true
贴图不贴合人脸 bg 与 face mesh 的 zoom/offset/mirror 必须严格一致——见"设计要点 #4 对齐不变量"
切换摄像头后崩溃 cleanupSecondInit 注释;不要在 Android 路径调用它,仅 desktop 入口可用
改了 shader 不生效 必须先跑 compile_shader.bat 重新生成 .spv

兼容性

  • minSdk 29targetSdk 看 app/build.gradle
  • 设备需支持 Vulkan 1.0+ 和 MediaPipe TFLite delegate
  • 已在 MI 9Android 10、Adreno 640、Vulkan vendor qglinternal)上验证通过
S
Description
No description provided
Readme
83 MiB
Languages
C++ 95.8%
Kotlin 1.5%
Java 1%
Python 0.8%
GLSL 0.6%
Other 0.2%