290 lines
12 KiB
Markdown
290 lines
12 KiB
Markdown
# 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)
|
||
│ └── <motion_id>/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
|
||
<activity
|
||
android:name=".MakeupActivity"
|
||
android:exported="false"
|
||
android:screenOrientation="portrait"
|
||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|smallestScreenSize" />
|
||
```
|
||
|
||
### 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<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 元数据,描述每帧贴图的**锚点偏移**(贴在脸上的相对位置,画布坐标系,原点在屏幕中心):
|
||
|
||
```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/<your.package>/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`)上验证通过
|