# 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 GLSL(bg/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) │ └── /000xxx.png ← motion 各帧贴图 │ ├── compile_shader.bat ← 编译所有 .vert/.frag 为 .spv(必须在改 shader 后执行) └── third_party/ ← VMA / glm / glfw / mediapipe … ``` --- ## 快速上手 ### 1. 编译 + 运行 example ```powershell # 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 后必须重编 ```powershell .\compile_shader.bat .\gradlew.bat :example:installDebug ``` --- ## 在自己的 App 里集成 ### Step 1:依赖 SDK 模块 在你的 `settings.gradle` / `build.gradle` 引入 `:app` 模块(或打包成 AAR)。 ### Step 2:在 `AndroidManifest.xml` 声明业务 Activity 业务 Activity 必须**竖屏**,且**屏蔽常见 configChanges 不要重建**: ```xml ``` ### Step 3:继承 `FaceActivity` 写业务 Activity ```kotlin 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,传入摄像头朝向 ```kotlin 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_FRONT` 或 `LENS_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 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 元数据,描述每帧贴图的**锚点偏移**(贴在脸上的相对位置,画布坐标系,原点在屏幕中心): ```json { "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.0,shader 里把最终 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/H` 和 `camera_rotation`(CameraX 给的 rotationDegrees)做 UV 反变换 - 摄像头帧分辨率/朝向变化时,SDK 会在 `processCameraFrame` 里自动重建 bg 纹理 ### 6. 窗口生命周期 - `APP_CMD_TERM_WINDOW` → `Application::cleanupForWindowLost()`:只销毁**窗口资源**(surface / swapchain / framebuffers / imageViews / commandBuffers / sync),保留 renderPass / pipelines / device / VMA / textures - `APP_CMD_INIT_WINDOW` → `Application::reinitForNewWindow()`:复用全局资源,只重建窗口资源 - `android_main` 退出时**不再额外销毁** Vulkan 资源(保留给下次 NativeActivity 复用),避免 use-after-free 崩溃 --- ## 调试 ### 日志文件 SDK 把 Java/Kotlin 日志(通过 `com.hmwl.face_sdk.DebugLog.i / e`)和 C++ 日志(`DebugLog::log`)**统一写入**: ``` /data/user/0//files/face_sdk_debug.log ``` ```powershell # 拉到本地分析 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 29,targetSdk 看 `app/build.gradle` - 设备需支持 Vulkan 1.0+ 和 MediaPipe TFLite delegate - 已在 MI 9(Android 10、Adreno 640、Vulkan vendor `qglinternal`)上验证通过