save code
This commit is contained in:
+1
-2
@@ -13,7 +13,7 @@ android {
|
||||
compileSdk 36
|
||||
defaultConfig {
|
||||
//applicationId "com.hmwl.face_sdk"
|
||||
minSdk 30
|
||||
minSdk 29
|
||||
targetSdk 36
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
@@ -117,7 +117,6 @@ dependencies {
|
||||
//implementation fileTree(dir: 'libs/tasks-vision', include: ['*.jar'])
|
||||
//implementation fileTree(dir: 'libs/tasks-core', include: ['*.jar'])
|
||||
|
||||
|
||||
def camerax_version = '1.4.2'
|
||||
implementation "androidx.camera:camera-core:$camerax_version"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
<activity
|
||||
android:name=".FaceActivity"
|
||||
android:exported="true"
|
||||
tools:ignore="MissingClass"> <!-- 如果IDE报类找不到,可以添加这个 -->
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|smallestScreenSize"
|
||||
tools:ignore="MissingClass,LockedOrientationActivity">
|
||||
<!-- 锁定竖屏:SDK 渲染按"屏幕中央正方形画布 + 上下黑边 letterbox"
|
||||
的方式适配任意分辨率,目前只验证过竖屏。
|
||||
configChanges 全包:避免软键盘/导航栏变化触发 Activity 重建——SDK
|
||||
自己会通过 onWindowLost/onWindowInit 重建 swapchain,性能更好。 -->
|
||||
|
||||
<!-- 重要:移除 MAIN/LAUNCHER intent-filter -->
|
||||
<!-- 让使用库的应用来决定哪个是主Activity -->
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
#version 450
|
||||
|
||||
// 输入变量(与顶点着色器输出对应)
|
||||
layout(location = 0) in vec2 inTexCoord;
|
||||
|
||||
// 输出颜色
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
// 纹理采样器
|
||||
layout(binding = 0) uniform sampler2D texSampler;
|
||||
|
||||
// ★ PushConstants 必须和 bg.vert / FaceApp.h 完全一致(包括我们没用到的
|
||||
// 字段 zoom/ux/uy/offset_x/offset_y,因为 push constant 是按 offset 取的,
|
||||
// 缺字段会导致后面 radius/screen_w/screen_h 落到错误偏移)。
|
||||
layout(push_constant) uniform PushConstants {
|
||||
float zoom;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
} pushConstants;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
float canvas_size;
|
||||
float screen_w;
|
||||
float screen_h;
|
||||
// 相机帧元信息(fragment 不用,仅为对齐 push constant layout)
|
||||
float camera_aspect;
|
||||
float camera_rotation;
|
||||
float mirror_x;
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
// outFragColor = color;
|
||||
// 获取当前片元的屏幕坐标(左下角为原点)
|
||||
// gl_FragCoord 是屏幕 framebuffer 里的像素坐标(左下原点)。圆形 mask 中心
|
||||
// 锚定在屏幕真实中心 = (screen_w, screen_h)/2。这里不再用画布中心,因为
|
||||
// letterbox 后画布中心和屏幕中心其实是同一个点(min 边居中),但用屏幕坐标
|
||||
// 写法更直观也对 cover 模式更友好。
|
||||
vec2 fragCoord = gl_FragCoord.xy;
|
||||
|
||||
// 屏幕中心(480x480 的中心是 (240, 240))
|
||||
vec2 center = vec2(240.0, 240.0);
|
||||
|
||||
// 计算到中心的距离(欧氏距离,单位:像素)
|
||||
vec2 center = vec2(pc.screen_w, pc.screen_h) * 0.5;
|
||||
float dist = length(fragCoord - center);
|
||||
|
||||
// 判断是否在半径之外
|
||||
if (dist > pushConstants.radius) {
|
||||
// 使用 push constant 中的 RGB 颜色(注意 alpha 设为 1.0)
|
||||
outColor = vec4(pushConstants.r, pushConstants.g, pushConstants.b, 1.0);
|
||||
if (dist > pc.radius) {
|
||||
// 画布外(含 letterbox 黑边)+ 圆外区域统一用业务给的纯色覆盖。
|
||||
// 业务一般给 r=g=0 / b=1 之类,与原版语义保持一致。
|
||||
outColor = vec4(pc.r, pc.g, pc.b, 1.0);
|
||||
} else {
|
||||
// 采样纹理
|
||||
outColor = texture(texSampler, inTexCoord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,68 +1,131 @@
|
||||
#version 450
|
||||
|
||||
// 硬编码的顶点数据 - 4个顶点组成三角形带覆盖整个屏幕
|
||||
// const vec2 positions[6] = vec2[6](
|
||||
// vec2( 1.0, 1.0), // 右上 - 三角形1
|
||||
// vec2(-1.0, 1.0), // 左上 - 三角形1
|
||||
// vec2(-1.0, -1.0), // 左下 - 三角形1
|
||||
|
||||
// vec2(-1.0, -1.0), // 左下 - 三角形2
|
||||
// vec2( 1.0, -1.0), // 右下 - 三角形2
|
||||
// vec2( 1.0, 1.0) // 右上 - 三角形2
|
||||
// );
|
||||
|
||||
const float offset = (640-480)/(480.0);
|
||||
|
||||
|
||||
// ★ bg quad 顶点放大到 ±10(而不是 ±1)的原因:
|
||||
//
|
||||
// bg 与 face mesh 的对齐依赖一个关键不变量——
|
||||
// "屏幕上同一像素位置,bg 显示的 raw UV = 把这个屏幕位置反算回 mediapipe
|
||||
// landmark 坐标"。要保住这个不变量,bg quad 必须经过和 face mesh 完全相
|
||||
// 同的 zoom × offset × letterbox × mirror 链路 (见下方 gl_Position 计算),
|
||||
// 而 outTexCoord 必须基于"未变换的 quad 顶点 pos"算 raw UV。这样屏幕 fragment
|
||||
// 反算出的 quad pos 就等价于"对应的 landmark NDC",UV 完美对齐。
|
||||
//
|
||||
// 但是 zoom (>1) + 非零 offset + screen_h > screen_w 的 letterbox 会让 quad
|
||||
// 在屏幕上呈非对称分布——之前 quad 顶点 ±1 时实测在 zoom=1.5 offset=(450,-450)
|
||||
// 屏幕 1080×2340 下,quad 在 NDC.y 只覆盖 [-0.885, 0.500],屏幕底部
|
||||
// [0.500, 1.0] 大片 letterbox 没有被 bg 覆盖,露出 RenderPass 的 clearColor
|
||||
// 黑色——表现为"上面有蓝色填充,下面没有"。
|
||||
//
|
||||
// 解法:把 quad 顶点扩大到 ±10。GPU 会把超出 NDC [-1, 1] 的部分硬件 clip 掉,
|
||||
// 但 clip 之后留在屏幕内的 fragment 上,outTexCoord 是顶点的"线性插值",**屏幕
|
||||
// 内 fragment 反算出的 pos 与 quad 顶点取值范围无关**——所以 face mesh 对齐
|
||||
// 完全不受影响。同时 ±10 在任何合理的 zoom/offset/letterbox 下都能保证 quad
|
||||
// 在屏幕上完全覆盖 [-1, 1]² 含 letterbox。
|
||||
//
|
||||
// Worst-case 验证:zoom=1.5, offset=(450,-450), canvas=1080, screen=1080×2340,
|
||||
// pos.y = -10 → screen_ndc.y = (-10*1.5 + (-450/1080)) * 1080/2340 = -7.12 ✓
|
||||
// pos.y = +10 → screen_ndc.y = (+10*1.5 + (-450/1080)) * 1080/2340 = +6.73 ✓
|
||||
// 远超屏幕范围,clip 后保证整屏 letterbox 都被 bg.frag 的 r/g/b 蓝色覆盖。
|
||||
const vec2 positions[6] = vec2[6](
|
||||
vec2( 1.0, -1.0 -offset), // 右下
|
||||
vec2(1.0, 1.0 + offset), // 右上 - 三角形1
|
||||
vec2(-1.0, 1.0 + offset), // 左上 - 三角形1
|
||||
vec2( 10.0, -10.0),
|
||||
vec2( 10.0, 10.0),
|
||||
vec2(-10.0, 10.0),
|
||||
|
||||
vec2(-1.0, 1.0 + offset), // 左上 - 三角形2
|
||||
vec2( -1.0, -1.0-offset), // 左下 - 三角形2
|
||||
vec2( 1.0, -1.0-offset) // 右下 - 三角形2
|
||||
vec2(-10.0, 10.0),
|
||||
vec2(-10.0, -10.0),
|
||||
vec2( 10.0, -10.0)
|
||||
);
|
||||
|
||||
|
||||
|
||||
const vec2 texCoords[6] = vec2[6](
|
||||
vec2(1.0, 1.0), // 右上
|
||||
vec2(0.0, 1.0), // 左上
|
||||
vec2(0.0, 0.0), // 左下
|
||||
|
||||
vec2(0.0, 0.0), // 左下
|
||||
vec2(1.0, 0.0), // 右下
|
||||
vec2(1.0, 1.0) // 右上
|
||||
);
|
||||
|
||||
// 定义 Push Constant,只有一个 float
|
||||
// ★ PushConstants 必须和 C++ FaceApp.h 严格一致。前 9 个 float 是业务原始值
|
||||
// (SDK 已按 canvas_size/480 缩放到屏幕物理像素),后 5 个 float 是当前帧
|
||||
// 的画布几何 + 相机帧元信息,由 FaceApp::render() 每帧写入。
|
||||
layout(push_constant) uniform PushConstants {
|
||||
float zoom;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
} pushConstants;
|
||||
float offset_y;
|
||||
// 画布几何(每帧写入)
|
||||
float canvas_size;
|
||||
float screen_w;
|
||||
float screen_h;
|
||||
// 相机帧元信息(每次相机帧上传时写入)
|
||||
float camera_aspect; // raw width / height(典型 4:3 = 1.333…)
|
||||
float camera_rotation; // CameraX rotationDegrees: 0/90/180/270
|
||||
// 镜子效果开关:1.0 = 前置摄像头,最终 NDC.x 翻转一次。
|
||||
// 这与 FaceLandmarkerHelper 在前置时对 bitmap 做 postScale(-1,1) 的语义对偶:
|
||||
// ① mediapipe 输入是"用户视角"图像 → landmark 也是用户视角;
|
||||
// ② bg.vert 算出的 outTexCoord 还是 raw 帧 UV(旋转矩阵把 user 视角→raw);
|
||||
// ③ 最后我们把 NDC.x 翻转,bg 在屏幕上呈现为水平镜像 = 镜子效果;
|
||||
// ④ texture.vert 同样翻转一次,face 贴图与 bg 完美对齐。
|
||||
float mirror_x;
|
||||
} pc;
|
||||
|
||||
|
||||
// 输出变量
|
||||
layout(location = 0) out vec2 outTexCoord;
|
||||
|
||||
void main() {
|
||||
// 获取顶点位置
|
||||
vec2 position = positions[gl_VertexIndex];
|
||||
position = position * pushConstants.zoom;
|
||||
position.x = position.x + (pushConstants.offset_x/480);
|
||||
position.y = position.y + (pushConstants.offset_y/480);
|
||||
|
||||
// 设置输出位置(Vulkan使用不同的坐标系)
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
|
||||
// 输出纹理坐标
|
||||
outTexCoord = texCoords[gl_VertexIndex];
|
||||
}
|
||||
vec2 pos = positions[gl_VertexIndex];
|
||||
|
||||
// ===== outTexCoord:基于"未变换的 quad pos" 算 raw UV =====
|
||||
//
|
||||
// 重点:这里必须用未做 zoom/offset/letterbox/mirror 变换的原始 pos 来计算
|
||||
// outTexCoord,而 gl_Position 在下面才做这些变换。这样:
|
||||
// - 屏幕 fragment X 处对应的 quad pos = (X 反过 mirror、letterbox、offset、zoom)
|
||||
// - face mesh 顶点的 mediapipe landmark 在屏幕上的位置 X' 经过相同链路
|
||||
// - 当 X = X' 时(face mesh 顶点重叠某个屏幕像素),fragment 反算的 pos
|
||||
// 刚好等于 face landmark 的画布 NDC,bg 该 fragment 显示 landmark 对应
|
||||
// raw UV → 与 face mesh 完美贴合。
|
||||
//
|
||||
// 公式链:画布 NDC ─[逆时针 rotation]→ 中央方形 NDC ─[scale & shift]→ raw UV
|
||||
//
|
||||
// 关于旋转方向:
|
||||
// Android Matrix.postRotate(deg) 在 Bitmap canvas(y 朝下)里是「视觉顺时针」。
|
||||
// Vulkan NDC 也是 y 朝下。所以画布 NDC → 中央方形 NDC 是「视觉逆时针 rotation」。
|
||||
// GLSL column-major mat2(c, -s, s, c) 对应矩阵 [c, s; -s, c]。
|
||||
// 验证 rotation=0 时矩阵 = identity ✓
|
||||
// 验证 rotation=90 时矩阵 = [0, 1; -1, 0],apply 到画布右上 (1,-1):
|
||||
// (0*1 + 1*(-1), -1*1 + 0*(-1)) = (-1, -1) → 中央方形左上 ✓
|
||||
// 物理:mediapipe 输入右上像素 = 中央方形顺时针 90° 后的右上 = 中央方形左上 ✓
|
||||
float ang = radians(pc.camera_rotation);
|
||||
float c = cos(ang);
|
||||
float s = sin(ang);
|
||||
mat2 R = mat2(c, -s, s, c); // 画布 NDC → 中央方形 NDC(视觉逆时针 rotation)
|
||||
vec2 cm = R * pos;
|
||||
|
||||
// 中央方形 NDC ∈ [-1, +1]² → raw 帧 UV。中央方形在 raw 帧 UV 上是中央
|
||||
// min(W, H) x min(W, H) 的正方形,对应的 UV 半径:
|
||||
// raw 横向长(W >= H):halfU = H/(2W) = 1/(2 * aspect),halfV = 0.5
|
||||
// raw 纵向长(H > W):halfU = 0.5,halfV = W/(2H) = aspect/2
|
||||
float halfU, halfV;
|
||||
if (pc.camera_aspect >= 1.0) {
|
||||
halfU = 0.5 / pc.camera_aspect;
|
||||
halfV = 0.5;
|
||||
} else {
|
||||
halfU = 0.5;
|
||||
halfV = 0.5 * pc.camera_aspect;
|
||||
}
|
||||
outTexCoord = vec2(0.5 + halfU * cm.x, 0.5 + halfV * cm.y);
|
||||
|
||||
// ===== gl_Position:业务 zoom/offset + letterbox + mirror,与 texture.vert 严格一致 =====
|
||||
//
|
||||
// pos 仍按 quad 顶点 ±10 参与位置计算,pc.offset_x / pc.offset_y 已是屏幕物理
|
||||
// 像素(SDK 端做过 canvas_size/480 缩放),/canvas_size 后是画布 NDC 单位;
|
||||
// 最后 *canvas_size/screen_{w,h} 把画布缩到屏幕中央正方形,长边方向自然
|
||||
// letterbox。quad 顶点放大到 ±10 让 GPU clip 后整屏覆盖。
|
||||
pos *= pc.zoom;
|
||||
pos.x += pc.offset_x / pc.canvas_size;
|
||||
pos.y += pc.offset_y / pc.canvas_size;
|
||||
pos.x *= pc.canvas_size / pc.screen_w;
|
||||
pos.y *= pc.canvas_size / pc.screen_h;
|
||||
|
||||
// 镜子效果:前置时 mirror_x=1.0,把 NDC.x 翻转一次。无分支:
|
||||
// 后置:(1 - 2*0) = +1 → 不变;
|
||||
// 前置:(1 - 2*1) = -1 → 水平翻转。
|
||||
// texture.vert 也做同样翻转,bg + face 整体水平镜像,face 贴图与 bg 完美对齐。
|
||||
pos.x *= (1.0 - 2.0 * pc.mirror_x);
|
||||
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -5,40 +5,48 @@ layout (binding = 1) uniform sampler2D samplerColor;
|
||||
layout (location = 0) in vec2 inUV;
|
||||
layout (location = 1) in vec3 inNormal;
|
||||
|
||||
// ★ PushConstants 必须和 C++ FaceApp.h / texture.vert 严格一致。
|
||||
// 关键:原版这里只声明到 uy,缺了 offset_x 之后的字段,那时之所以没报错是因为
|
||||
// fragment 没读到那些字段。本次重构 fragment 要读 screen_w/screen_h,所以必须
|
||||
// 把整个块声明完整,否则 layout offset 会错位。
|
||||
layout(push_constant) uniform PushConstants {
|
||||
float zoom;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
} pushConstants;
|
||||
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
float canvas_size;
|
||||
float screen_w;
|
||||
float screen_h;
|
||||
// 相机帧元信息(fragment 不用,仅为对齐 push constant layout)
|
||||
float camera_aspect;
|
||||
float camera_rotation;
|
||||
float mirror_x;
|
||||
} pc;
|
||||
|
||||
layout (location = 0) out vec4 outFragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
// outFragColor = color;
|
||||
// 获取当前片元的屏幕坐标(左下角为原点)
|
||||
// gl_FragCoord 是屏幕 framebuffer 像素坐标。圆形 mask 中心锚定在屏幕真实中心,
|
||||
// 跟随分辨率自动适配;不再写死 (240,240)。
|
||||
vec2 fragCoord = gl_FragCoord.xy;
|
||||
|
||||
// 屏幕中心(480x480 的中心是 (240, 240))
|
||||
vec2 center = vec2(240.0, 240.0);
|
||||
|
||||
// 计算到中心的距离(欧氏距离,单位:像素)
|
||||
vec2 center = vec2(pc.screen_w, pc.screen_h) * 0.5;
|
||||
float dist = length(fragCoord - center);
|
||||
|
||||
// 判断是否在半径之外
|
||||
if (dist > pushConstants.radius) {
|
||||
// 使用 push constant 中的 RGB 颜色(注意 alpha 设为 1.0)
|
||||
outFragColor = vec4(pushConstants.r, pushConstants.g, pushConstants.b, 1.0);
|
||||
if (dist > pc.radius) {
|
||||
// 画布外(含 letterbox 黑边)+ 圆外区域用业务纯色覆盖,与 bg.frag 一致。
|
||||
outFragColor = vec4(pc.r, pc.g, pc.b, 1.0);
|
||||
} else {
|
||||
vec2 pos = vec2(pushConstants.ux/4096.f, pushConstants.uy/2048.f);
|
||||
vec2 uv = pos + vec2(inUV.x/8.f, inUV.y/4.f);
|
||||
// out.png 是 4096x2048 的 8x4 子图图集;ux/uy 选定当前 motion 帧子图,
|
||||
// inUV 在子图内插值。这里和分辨率重构无关,保持原版逻辑不动。
|
||||
vec2 pos = vec2(pc.ux / 4096.0, pc.uy / 2048.0);
|
||||
vec2 uv = pos + vec2(inUV.x / 8.0, inUV.y / 4.0);
|
||||
vec4 color = texture(samplerColor, uv);
|
||||
outFragColor = color;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,5 @@
|
||||
#version 450
|
||||
|
||||
|
||||
layout (location = 0) in vec3 inPos;
|
||||
layout (location = 1) in vec2 inUV;
|
||||
layout (location = 2) in vec3 inNormal;
|
||||
@@ -16,17 +15,31 @@ layout (binding = 0) uniform UBO
|
||||
layout (location = 0) out vec2 outUV;
|
||||
layout (location = 1) out vec3 outNormal;
|
||||
|
||||
// ★ PushConstants 必须和 C++ FaceApp.h / bg.vert / bg.frag 严格一致。
|
||||
// 前 9 个 float 是业务原始值(已被 SDK 在 render() 里按 canvas_size/480 缩放成
|
||||
// 屏幕物理像素);后 3 个 float 是当前帧画布几何,由 SDK 每帧写入。
|
||||
layout(push_constant) uniform PushConstants {
|
||||
float zoom;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
} pushConstants;
|
||||
float r;
|
||||
float g;
|
||||
float b;
|
||||
float radius;
|
||||
float ux;
|
||||
float uy;
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
float canvas_size;
|
||||
float screen_w;
|
||||
float screen_h;
|
||||
// 相机帧元信息:camera_aspect / camera_rotation 在 face 渲染中不用——face
|
||||
// landmark 已经是"正立中央方形"的归一化坐标,mediapipe 端转过;保留只是
|
||||
// 为了对齐 push constant layout。
|
||||
float camera_aspect;
|
||||
float camera_rotation;
|
||||
// mirror_x:1.0 表示前置摄像头镜子效果——把最终 gl_Position.x 翻转一次。
|
||||
// bg.vert 也做同样翻转,所以 bg 与 face 同步水平镜像、互相对齐。
|
||||
float mirror_x;
|
||||
} pc;
|
||||
|
||||
out gl_PerVertex
|
||||
{
|
||||
@@ -37,13 +50,33 @@ void main()
|
||||
{
|
||||
outUV = inUV;
|
||||
|
||||
vec2 position = vec2(inPos.xy*2 -1);
|
||||
position = position * pushConstants.zoom;
|
||||
position.x = position.x + (pushConstants.offset_x/480);
|
||||
position.y = position.y + (pushConstants.offset_y/480);
|
||||
gl_Position = vec4(position, 0.5, 1.0);
|
||||
// 模型顶点坐标 inPos.xy 来自 OBJ,已经是 [0,1]² 归一化范围。先映射到
|
||||
// "画布 NDC ∈ [-1,+1]"。
|
||||
vec2 position = vec2(inPos.xy * 2.0 - 1.0);
|
||||
|
||||
vec4 pos = ubo.model * vec4(inPos, 1.0);
|
||||
// 画布坐标系:缩放 + 业务像素偏移。
|
||||
// pc.offset_x / pc.offset_y 是屏幕物理像素(SDK 已做过 canvas_size/480 缩放),
|
||||
// 除以 canvas_size 转成画布 NDC,跟原版 "/480" 在 480 屏上完全等价。
|
||||
position = position * pc.zoom;
|
||||
position.x = position.x + (pc.offset_x / pc.canvas_size);
|
||||
position.y = position.y + (pc.offset_y / pc.canvas_size);
|
||||
|
||||
// 画布 NDC → 屏幕 NDC:屏幕长边方向乘以 canvas_size/screen_long < 1.0,
|
||||
// 让模型只占据屏幕中央正方形画布区域,长边方向的画布外是 letterbox。
|
||||
position.x = position.x * (pc.canvas_size / pc.screen_w);
|
||||
position.y = position.y * (pc.canvas_size / pc.screen_h);
|
||||
|
||||
// 镜子效果:前置摄像头时 mirror_x=1.0,把 NDC.x 翻转一次。
|
||||
// 后置:(1 - 2*0) = +1 → 不变
|
||||
// 前置:(1 - 2*1) = -1 → 水平翻转
|
||||
// 无分支写法,配合 bg.vert 同样的翻转,让 bg + face 整体镜像、保持对齐。
|
||||
position.x *= (1.0 - 2.0 * pc.mirror_x);
|
||||
|
||||
gl_Position = vec4(position, 0.5, 1.0);
|
||||
|
||||
// 注意:ubo.projection / ubo.model 在当前管线里没有真正参与 gl_Position 的
|
||||
// 计算(顶点位置已直接给出 NDC)。这里仍按原版保留 normal 的世界变换,避免
|
||||
// 影响其它依赖 outNormal 的 fragment 逻辑(例如 thick 版本)。
|
||||
vec4 pos = ubo.model * vec4(inPos, 1.0);
|
||||
outNormal = mat3(inverse(transpose(ubo.model))) * inNormal;
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
+58
-19
@@ -191,19 +191,35 @@ void android_main(struct android_app *pApp) {
|
||||
}
|
||||
} while (!pApp->destroyRequested);
|
||||
DebugLog::log("android_main: destroyRequested, running cleanup");
|
||||
//application.cleanup();
|
||||
g_Application->cleanupSecondInit();
|
||||
|
||||
// 关键:这里只复位 FaceApp 的"second-init" 状态机标记,**不销毁**任何
|
||||
// Vulkan 资源。
|
||||
//
|
||||
// - 窗口相关资源(surface/swapchain/framebuffers/imageViews/
|
||||
// commandBuffers/sync)已经在 APP_CMD_TERM_WINDOW → onWindowLost()
|
||||
// 里被 cleanupForWindowLost() 干净销毁;
|
||||
// - renderPass / graphicsPipeline / pipelineLayout / VkDevice /
|
||||
// VkInstance / VMA / 所有 textures / FaceApp 自身 pipelines 是 g_Application
|
||||
// 单例的"全局资源",跨 NativeActivity 实例复用。下次 Activity 启动
|
||||
// onWindowInit 会走 reinitForNewWindow() 重建窗口资源、复用全局资源。
|
||||
//
|
||||
// 历史教训:之前这里调过 g_Application->cleanupSecondInit(),那个函数会
|
||||
// 把 renderPass / graphicsPipeline / pipelineLayout 全部 destroy 掉但既
|
||||
// 不置 VK_NULL_HANDLE 也不复位 _applicationInited。结果用户从主界面切镜
|
||||
// 头再进 MakeupActivity 时,onWindowInit 看到 _applicationInited=1 走 reinit
|
||||
// 复用路径,引用悬空的 renderPass 调 vkCreateFramebuffer,validation
|
||||
// layer 检测出 use-after-free 直接 SEGV。
|
||||
g_Application->clearnSecondFaceApp();
|
||||
DebugLog::log("android_main: exit");
|
||||
}
|
||||
}
|
||||
|
||||
extern void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize);
|
||||
extern void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, int rotation, bool mirrorX);
|
||||
extern void ReceiveFacePoint(float* pos, int count, int width, int height);
|
||||
|
||||
void processWithVulkan(uint8_t* data, int width, int height, int format,
|
||||
int rowStride, size_t dataSize) {
|
||||
TextureLoadProcessWithVulkan(data, width, height, rowStride, dataSize);
|
||||
int rowStride, size_t dataSize, int rotation, bool mirrorX) {
|
||||
TextureLoadProcessWithVulkan(data, width, height, rowStride, dataSize, rotation, mirrorX);
|
||||
}
|
||||
|
||||
|
||||
@@ -213,14 +229,13 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_processImageNative(JNIEnv *env, jobject thiz, jobject buffer,
|
||||
jint width, jint height, jint format,
|
||||
jint row_stride, jint pixel_stride,
|
||||
jint rotation) {
|
||||
jint rotation, jboolean mirror_x) {
|
||||
static std::atomic<uint64_t> s_imgCount{0};
|
||||
uint64_t n = s_imgCount.fetch_add(1) + 1;
|
||||
if (n == 1 || n % 300 == 0) {
|
||||
DebugLog::log("processImageNative #%llu tid=%d w=%d h=%d",
|
||||
(unsigned long long)n, dbg_tid(), width, height);
|
||||
DebugLog::log("processImageNative #%llu tid=%d w=%d h=%d mirror=%d",
|
||||
(unsigned long long)n, dbg_tid(), width, height, (int)mirror_x);
|
||||
}
|
||||
// TODO: implement processImageNative()
|
||||
uint8_t* imageData = static_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
|
||||
jlong capacity = env->GetDirectBufferCapacity(buffer);
|
||||
|
||||
@@ -228,9 +243,12 @@ Java_com_hmwl_face_1sdk_FaceActivity_processImageNative(JNIEnv *env, jobject thi
|
||||
return;
|
||||
}
|
||||
|
||||
// 在这里将图像数据传递给 Vulkan
|
||||
// 创建 Vulkan 图像或更新现有图像
|
||||
processWithVulkan(imageData, width, height, format, row_stride, capacity);
|
||||
// rotation = ImageInfo.getRotationDegrees(),给 shader 用来做 UV 旋转,
|
||||
// 让不同 sensor orientation 的设备显示方向一致。
|
||||
// mirror_x = true 表示前置摄像头:所有顶点 shader 会把 gl_Position.x 翻转
|
||||
// 一次,达到"镜子效果"——这是化妆/美妆类 app 的标准体验。
|
||||
processWithVulkan(imageData, width, height, format, row_stride, capacity,
|
||||
(int)rotation, mirror_x == JNI_TRUE);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
@@ -238,19 +256,21 @@ Java_com_hmwl_face_1sdk_FaceActivity_passDataToNative(JNIEnv *env, jobject thiz,
|
||||
jint point_count, jint width, jint height) {
|
||||
static std::atomic<uint64_t> s_ptCount{0};
|
||||
uint64_t n = s_ptCount.fetch_add(1) + 1;
|
||||
if (n == 1 || n % 300 == 0) {
|
||||
DebugLog::log("passDataToNative #%llu tid=%d point_count=%d w=%d h=%d",
|
||||
(unsigned long long)n, dbg_tid(), point_count, width, height);
|
||||
}
|
||||
// TODO: implement passDataToNative()
|
||||
float* pos = static_cast<float*>(env->GetDirectBufferAddress(buffer));
|
||||
|
||||
if (pos == nullptr) {
|
||||
// 处理错误
|
||||
DebugLog::log_throttled("passDataToNative.nullBuffer",
|
||||
"passDataToNative #%llu got NULL DirectBufferAddress!",
|
||||
(unsigned long long)n);
|
||||
return;
|
||||
}
|
||||
// 节流:每 120 次实际数据通路打一行(约每 4 秒一次,足够确认通路活着即可)。
|
||||
if (n == 1 || n % 120 == 0) {
|
||||
DebugLog::log("passDataToNative #%llu point_count=%d w=%d h=%d p0=(%.3f,%.3f,%.3f)",
|
||||
(unsigned long long)n, point_count, width, height,
|
||||
pos[0], pos[1], pos[2]);
|
||||
}
|
||||
|
||||
// 或者直接将指针传递给Vulkan
|
||||
ReceiveFacePoint(pos, point_count, width, height);
|
||||
}
|
||||
|
||||
@@ -491,4 +511,23 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_FaceActivity_ResumeMotionNative(JNIEnv *env, jobject thiz) {
|
||||
DebugLog::log("JNI ResumeMotionNative tid=%d", dbg_tid());
|
||||
g_Application->ResumeMotion();
|
||||
}
|
||||
|
||||
// 给 Java/Kotlin 侧用的日志桥:把 Java 端关键事件写进 native 的
|
||||
// face_sdk_debug.log,使 Java + native + Vulkan 渲染日志能在同一个
|
||||
// 文件里按时间戳排好序,一次 adb pull 就能拿到完整时间线。
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hmwl_face_1sdk_DebugLog_nativeLog(JNIEnv *env, jclass clazz,
|
||||
jstring jtag, jstring jmsg) {
|
||||
if (jmsg == nullptr) return;
|
||||
const char* tag = jtag ? env->GetStringUTFChars(jtag, nullptr) : nullptr;
|
||||
const char* msg = env->GetStringUTFChars(jmsg, nullptr);
|
||||
if (tag) {
|
||||
DebugLog::log("[J][%s] %s", tag, msg);
|
||||
} else {
|
||||
DebugLog::log("[J] %s", msg);
|
||||
}
|
||||
if (tag) env->ReleaseStringUTFChars(jtag, tag);
|
||||
env->ReleaseStringUTFChars(jmsg, msg);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.hmwl.face_sdk;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Java/Kotlin 端日志桥。所有调用最终通过 JNI 写到 native 端的
|
||||
* face_sdk_debug.log 文件(与 native DebugLog::log 共用一份),
|
||||
* 同时镜像到 logcat(tag = 调用方 tag),方便联机调试时一次 adb pull
|
||||
* 就能拿到完整时间线(Java + native + Vulkan 渲染)。
|
||||
*
|
||||
* 使用约定:
|
||||
* - 业务低频事件用 i(tag, msg)
|
||||
* - 错误用 e(tag, msg)
|
||||
* - 高频帧级日志请自行节流,本类不再做二次节流
|
||||
*/
|
||||
public final class DebugLog {
|
||||
private DebugLog() {}
|
||||
|
||||
public static void i(String tag, String msg) {
|
||||
Log.i(tag, msg);
|
||||
try {
|
||||
nativeLog(tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// native 还没 attach 的极早期阶段,吞掉避免崩溃
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String tag, String msg) {
|
||||
Log.e(tag, msg);
|
||||
try {
|
||||
nativeLog("[E]" + tag, msg);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
|
||||
private static native void nativeLog(String tag, String msg);
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.AspectRatio;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
@@ -38,6 +39,27 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
System.loadLibrary("face_sdk");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent extra key:调用方(example/MainActivity 等)通过它告诉 FaceActivity
|
||||
* 启动哪个朝向的摄像头。值类型 int,取自 androidx.camera.core.CameraSelector:
|
||||
* - CameraSelector.LENS_FACING_BACK (0)
|
||||
* - CameraSelector.LENS_FACING_FRONT (1)
|
||||
* 缺省(未提供 extra)时走 LENS_FACING_BACK,保留旧调用方的行为。
|
||||
*
|
||||
* 该值同时决定两件事:
|
||||
* ① CameraSelector 朝向;
|
||||
* ② mirrorX 镜像标志:前置摄像头时 sensor 帧是"用户右手在画面右侧",
|
||||
* 为了显示成"镜子效果"(用户右手在屏幕左侧),所有顶点 shader 在
|
||||
* gl_Position 阶段对 NDC.x 做一次水平翻转。这件事通过 push constant
|
||||
* 的 mirror_x 字段透传到 GPU;对应地 FaceLandmarkerHelper 也会对
|
||||
* bitmap 做 postScale(-1, 1) 让 mediapipe 输入与显示画面方向一致,
|
||||
* 确保 face 贴图与 bg 完美对齐。
|
||||
*/
|
||||
public static final String EXTRA_LENS_FACING = "com.hmwl.face_sdk.LENS_FACING";
|
||||
|
||||
/** 当前 session 使用的摄像头朝向。在 onCreate 里从 Intent extra 读取并固定。 */
|
||||
private int lensFacing = CameraSelector.LENS_FACING_BACK;
|
||||
|
||||
private ProcessCameraProvider cameraProvider;
|
||||
private void initCamera(){
|
||||
backgroundExecutor = Executors.newSingleThreadExecutor();
|
||||
@@ -72,11 +94,28 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
cameraProvider = cameraProviderFuture.get();
|
||||
|
||||
CameraSelector cameraSelector = new CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.requireLensFacing(lensFacing)
|
||||
.build();
|
||||
|
||||
// 显式锁定分析帧分辨率为 480x640(CameraX UI 坐标系,竖屏:短边 x 长边)。
|
||||
// 不同手机的相机硬件原生支持的分辨率差异很大(旗舰机随便给 1920x1440、
|
||||
// 入门机可能是 320x240),CameraX 默认会按"最贴近 setTargetAspectRatio
|
||||
// 的 supported size"挑一个,结果在不同设备上拿到的分析帧大小都不一样。
|
||||
// 这会带来三个麻烦:
|
||||
// ① bg 纹理 GPU 内存占用波动很大(高端机一帧 ~10MB staging buffer);
|
||||
// ② mediapipe 推理耗时随分辨率非线性升高(Pixel 上 1920x1440 比
|
||||
// 640x480 慢 4-6 倍),帧率掉得明显;
|
||||
// ③ 我们的 SDK 用归一化 landmark + 中央方形 crop,本来就不需要更高
|
||||
// 分辨率,多出来的像素是纯浪费。
|
||||
//
|
||||
// 锁定 480x640 后:sensor 坐标系下分析帧固定 640x480 (rotation=90 时),
|
||||
// FaceLandmarkerHelper 中央方形裁切固定 480x480,bg 上传纹理也固定,
|
||||
// 不同手机上行为一致。
|
||||
//
|
||||
// 实际尺寸由 CameraX 决定(会找最接近的硬件支持档位),但设备实测
|
||||
// 几乎都能精确给出 640x480。
|
||||
imageAnalyzer = new ImageAnalysis.Builder()
|
||||
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
|
||||
.setTargetResolution(new Size(480, 640))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
|
||||
.build();
|
||||
@@ -114,24 +153,24 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
}
|
||||
|
||||
private void processImageForVulkan(ImageProxy image) {
|
||||
// 获取图像信息
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
int format = image.getFormat();
|
||||
|
||||
// 获取图像数据平面
|
||||
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
||||
|
||||
// 对于 RGBA_8888 格式,通常只有一个平面
|
||||
if (planes.length > 0) {
|
||||
ImageProxy.PlaneProxy plane = planes[0];
|
||||
ByteBuffer buffer = plane.getBuffer();
|
||||
int rowStride = plane.getRowStride();
|
||||
int pixelStride = plane.getPixelStride();
|
||||
|
||||
// 调用 Native 方法处理图像
|
||||
// mirrorX 为 true 时所有顶点 shader 会把 gl_Position.x 翻转一次,达到
|
||||
// "镜子效果"——前置摄像头唯一需要的额外处理。后置不开镜像。
|
||||
boolean mirrorX = (lensFacing == CameraSelector.LENS_FACING_FRONT);
|
||||
processImageNative(buffer, width, height, format,
|
||||
rowStride, pixelStride, image.getImageInfo().getRotationDegrees());
|
||||
rowStride, pixelStride, image.getImageInfo().getRotationDegrees(),
|
||||
mirrorX);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,11 +193,16 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
// Native 方法
|
||||
private native void processImageNative(ByteBuffer buffer, int width, int height,
|
||||
int format, int rowStride, int pixelStride,
|
||||
int rotation);
|
||||
int rotation, boolean mirrorX);
|
||||
|
||||
private void detectFace(ImageProxy imageProxy) {
|
||||
// 前置摄像头:FaceLandmarkerHelper 内部会对 bitmap 做 postScale(-1, 1),
|
||||
// 让 mediapipe 看到的是"用户视角"的图像。这样 landmark 输出就是用户视角
|
||||
// 归一化坐标,跟 shader 里 gl_Position.x 翻转一次后的 bg 画面方向一致,
|
||||
// face 贴图位置才能精确对齐。
|
||||
boolean isFrontCamera = (lensFacing == CameraSelector.LENS_FACING_FRONT);
|
||||
faceLandmarkerHelper.detectLiveStream(
|
||||
imageProxy,false
|
||||
imageProxy, isFrontCamera
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,6 +225,15 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
// 必须在 initCamera() 之前读,CameraSelector 的朝向就靠这个 lensFacing。
|
||||
// 兼容旧调用方:未提供 EXTRA_LENS_FACING 时默认后置。
|
||||
if (getIntent() != null) {
|
||||
lensFacing = getIntent().getIntExtra(
|
||||
EXTRA_LENS_FACING, CameraSelector.LENS_FACING_BACK);
|
||||
}
|
||||
Log.d(TAG, "onCreate lensFacing=" + lensFacing
|
||||
+ " (front=" + CameraSelector.LENS_FACING_FRONT
|
||||
+ " back=" + CameraSelector.LENS_FACING_BACK + ")");
|
||||
initCamera();
|
||||
|
||||
backgroundExecutor.execute(new Runnable() {
|
||||
@@ -280,11 +333,18 @@ public class FaceActivity extends GameActivity implements FaceLandmarkerHelper.L
|
||||
FloatBuffer floatBuffer = nativeBuffer.asFloatBuffer();
|
||||
floatBuffer.put(points);
|
||||
floatBuffer.position(0);
|
||||
long cur_time = System.currentTimeMillis();
|
||||
Log.i("TimeLatency","Result Data ProcessTime:" + (cur_time-start_time));
|
||||
// 节流:每 30 次调用打一次到 native 文件,方便和 native 那边的
|
||||
// passDataToNative.tick 对账。
|
||||
onResultsCallCount++;
|
||||
if (onResultsCallCount % 30 == 1) {
|
||||
DebugLog.i("FaceActivity",
|
||||
"onResults call#" + onResultsCallCount
|
||||
+ " index=" + index + " input=" + width + "x" + height
|
||||
+ " p0=(" + points[0] + "," + points[1] + "," + points[2] + ")");
|
||||
}
|
||||
passDataToNative(nativeBuffer, index, width, height);
|
||||
Log.i("TimeLatency","passDataToNative ProcessTime:" + (System.currentTimeMillis() - cur_time));
|
||||
}
|
||||
private long onResultsCallCount = 0;
|
||||
|
||||
public native void passDataToNative(ByteBuffer buffer, int pointCount, int width, int height);
|
||||
|
||||
|
||||
@@ -173,6 +173,13 @@ class FaceLandmarkerHelper(
|
||||
" while not using RunningMode.LIVE_STREAM"
|
||||
)
|
||||
}
|
||||
// 节流日志:每 ~30 次调用打一次(detectFace 在 FaceActivity 里只在
|
||||
// frameCount % 3 == 0 时调用,所以 30 次大约对应 90 帧 ≈ 3 秒)。用来
|
||||
// 确认 mediapipe 入口活着、isFrontCamera/rotation/分辨率有没有跑偏。
|
||||
if ((frameId % 30L) == 0L) {
|
||||
Log.i(TAG, "detectLiveStream tick frameId=$frameId src=${imageProxy.width}x${imageProxy.height}" +
|
||||
" rot=${imageProxy.imageInfo.rotationDegrees} isFrontCamera=$isFrontCamera")
|
||||
}
|
||||
val frameTime = SystemClock.uptimeMillis()
|
||||
|
||||
val currentFrameId = frameId++
|
||||
@@ -194,22 +201,40 @@ class FaceLandmarkerHelper(
|
||||
imageProxy.use { bitmapBuffer.copyPixelsFromBuffer(imageProxy.planes[0].buffer) }
|
||||
//imageProxy.close()
|
||||
|
||||
// 重要:这里 *不* 对前置摄像头做 postScale(-1, 1)!理由如下:
|
||||
//
|
||||
// 我们的渲染策略是 —— bg 与 face 顶点 shader 末尾统一对 gl_Position.x
|
||||
// 乘 (1 - 2*mirror_x) 做镜子翻转。bg 是直接采样 sensor 帧、然后 shader
|
||||
// 末尾翻一次;face 顶点用的是 mediapipe 输出的归一化坐标,shader 末尾
|
||||
// 也翻一次。两者只有同时基于"sensor 视角"做输入、再一起在 shader 末尾
|
||||
// 翻一次,face 才能精确对齐 bg 上的脸。
|
||||
//
|
||||
// 如果在这里给 mediapipe 喂"已镜像"的 bitmap(旧实现),mediapipe 输出
|
||||
// 的 landmark 就是"用户视角"坐标,再被 shader 翻一次 → face 实际上被
|
||||
// 翻了两次,跟只翻一次的 bg 错位(通常会在屏幕另一半,看起来就是"face
|
||||
// 完全没渲染")。这正是我们最近调试看到的现象。
|
||||
//
|
||||
// mediapipe 对左右镜像不敏感(训练集本身覆盖各角度),sensor 视角下也
|
||||
// 能稳定检测出 478 个 landmark,无需迎合"镜子视角"。因此 isFrontCamera
|
||||
// 参数保留以备扩展,但当前不再据此修改输入图。
|
||||
val matrix = Matrix().apply {
|
||||
// Rotate the frame received from the camera to be in the same direction as it'll be shown
|
||||
postRotate(imageProxy.imageInfo.rotationDegrees.toFloat())
|
||||
|
||||
// flip image if user use front camera
|
||||
if (isFrontCamera) {
|
||||
postScale(
|
||||
-1f,
|
||||
1f,
|
||||
imageProxy.width.toFloat(),
|
||||
imageProxy.height.toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
// 旧代码硬编码了 (640-480)/2 = 80 和 bitmapBuffer.height(=480),假设
|
||||
// 相机帧是 640x480。在不同手机/不同 CameraX 配置下相机分辨率会变(比如
|
||||
// 480x640 portrait sensor、1280x720 等),硬编码会越界裁切或丢一半画面。
|
||||
//
|
||||
// 改成:根据 imageProxy 的实际尺寸取中央 min(W,H) x min(W,H) 正方形作为
|
||||
// mediapipe 输入。这与 bg.vert 的 UV 计算严格对齐 —— shader 那边也是从
|
||||
// raw 帧的中央 min(W,H) 正方形采样,再按 rotation 旋转。两端语义一致才能
|
||||
// 让 FaceLandmark 输出的归一化坐标 [0,1] 直接对应屏幕画布 NDC。
|
||||
val srcW = imageProxy.width
|
||||
val srcH = imageProxy.height
|
||||
val side = minOf(srcW, srcH)
|
||||
val cropX = (srcW - side) / 2
|
||||
val cropY = (srcH - side) / 2
|
||||
val rotatedBitmap = Bitmap.createBitmap(
|
||||
bitmapBuffer, (640-480)/2, 0, bitmapBuffer.height, bitmapBuffer.height,
|
||||
bitmapBuffer, cropX, cropY, side, side,
|
||||
matrix, true
|
||||
)
|
||||
|
||||
@@ -270,9 +295,14 @@ class FaceLandmarkerHelper(
|
||||
//{
|
||||
val finishTimeMs = SystemClock.uptimeMillis()
|
||||
val inferenceTime = finishTimeMs - result.timestampMs()
|
||||
Log.i("TimeLatency",
|
||||
"总延时: ${inferenceTime}ms | "
|
||||
)
|
||||
hitResultCount++
|
||||
if (hitResultCount % 30L == 1L) {
|
||||
// 节流:检测到人脸的频次。和 emptyResultCount 配合看就能知道
|
||||
// 命中率(hitResultCount / (hitResultCount + emptyResultCount))。
|
||||
Log.i(TAG, "returnLivestreamResult: HIT count=$hitResultCount" +
|
||||
" landmarks=${result.faceLandmarks().firstOrNull()?.size}" +
|
||||
" input=${input.width}x${input.height} infer=${inferenceTime}ms")
|
||||
}
|
||||
faceLandmarkerHelperListener?.onResults(
|
||||
ResultBundle(
|
||||
result,
|
||||
@@ -284,9 +314,19 @@ class FaceLandmarkerHelper(
|
||||
//}
|
||||
}
|
||||
else {
|
||||
// 节流:mediapipe 拿到 frame 但没识别出人脸时,每 30 次空结果打一行。
|
||||
// 只要这条日志在刷,就说明 mediapipe pipeline 正常活着,断点在"喂进去
|
||||
// 的图本身没有可识别的脸"——常见于 isFrontCamera 路径下输入图被翻坏。
|
||||
emptyResultCount++
|
||||
if (emptyResultCount % 30L == 1L) {
|
||||
Log.w(TAG, "returnLivestreamResult: faceLandmarks empty (count=$emptyResultCount)" +
|
||||
" input=${input.width}x${input.height} ts=$frameId")
|
||||
}
|
||||
faceLandmarkerHelperListener?.onEmpty()
|
||||
}
|
||||
}
|
||||
private var emptyResultCount: Long = 0
|
||||
private var hitResultCount: Long = 0
|
||||
|
||||
// Return errors thrown during detection to this FaceLandmarkerHelper's
|
||||
// caller
|
||||
|
||||
Reference in New Issue
Block a user