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
# 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_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 元数据,描述每帧贴图的锚点偏移(贴在脸上的相对位置,画布坐标系,原点在屏幕中心):
{
"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 / texturesAPP_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
# 拉到本地分析
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)上验证通过