更新记录
1.0.0(2026-11-09)
- 首个版本:Android 离线人脸识别 / 活体检测 uni-app x 标准模式组件,支持蒸汽(Vapor)模式
- module API:
init / detect / compare / register / search / remove / count / listUsers / rename / setThreshold / version / release / requestPermission
- 标准模式组件
<zy-face-liveness-x>:native-view 绑定原生相机预览与人脸框,支持实时检测 / 识别事件
- 组件方法:
start / stop / switchCamera / setDetectFlags / registerFace / capture / setScaleType / release
平台兼容性
uni-app x(4.31)
| Chrome |
Safari |
Android |
Android插件版本 |
iOS |
鸿蒙 |
微信小程序 |
| - |
- |
7.0 |
1.0.0 |
- |
- |
- |
其他
| 多语言 |
暗黑模式 |
宽屏模式 |
蒸汽模式 |
| × |
× |
× |
√ |
zy-face-liveness-x 离线人脸识别 / 活体检测
Android 平台 uni-app x 标准模式组件(UTS 插件,components + native-view 封装),兼容 VDOM 与蒸汽(Vapor)模式,全程离线,不依赖任何云服务。
| 能力 |
说明 |
| 人脸检测 |
单图 / 相机实时,支持同时识别多人,返回位置、角度、置信度、跟踪 ID |
| 人脸识别 |
1:N 本地人脸库检索、1:1 两图比对(判断是否同一人) |
| 活体检测 |
RGB 活体(静默)+ 动作活体(眨眼 / 张嘴 / 摇头 / 抬头) |
| 人脸属性 |
年龄段、性别、人种、口罩、睁闭眼、表情、人脸质量 |
| 人脸库 |
注册(单/批量)、检索、删除、分页名单、重命名、总数,持久化到本机 |
| 相机组件 |
<zy-face-liveness-x> 预览 + 人脸框 + 实时检测事件 + 相机录入 + 抓拍 + 前后摄切换 |
- 支持平台:App-Android(uni-app x)
- 最低系统:Android 7.0(API 24)
- 模型包:
Pikachu(约 17MB,默认,速度快)/ Megatron(约 61MB,精度更高),随 SDK 一起打进 apk,无需额外下载
- 组件依赖
androidx.camera 与离线引擎 SDK,首次使用需重做自定义基座(详见文末)
示例页面
| 示例 |
文件 |
内容 |
| 离线人脸识别(API) |
pages/face-liveness-x/face-liveness-x.uvue |
初始化/模型包切换、单图多人检测与全部属性、两图比对、人脸库注册/检索/名单/删除/改名、运行日志 |
| 人脸相机(组件) |
pages/face-liveness-x/face-liveness-x-camera.uvue |
<zy-face-liveness-x> 预览、人脸框、实时属性、开关热切换、相机录入、抓拍 |
组件用法要点:标准模式组件按 easycom 规范自动解析,页面里直接写标签 <zy-face-liveness-x>,不需要 import、也不需要 components 注册。
组件的属性/事件/方法在 components/zy-face-liveness-x/zy-face-liveness-x.uvue 中声明,能力实现调用 utssdk/app-android/index.uts 的 ZyFaceLivenessView。
注意:native-view 不支持 border / background / box-shadow,需要背景或圆角时请在组件外层用 view 包裹并设置样式。
使用场景
- 门禁 / 考勤 / 访客登记:本地人脸库录入后离线 1:N 识别,断网可用。
- 实名核验辅助:1:1 比对两张图是否同一人,配合活体分值判断是否为真人。
- 相机实时预览:需要边预览边显示人脸框、实时属性(年龄/性别/口罩)的业务场景。
- 合规与隐私敏感场景:人脸数据只留在本机,不出设备。
使用流程
init(options) 初始化离线引擎(首次解压模型,耗时数秒,必须展示 loading)。
- 需要相机时调用
requestPermission(options) 申请 CAMERA 权限(插件内统一申请)。
- 调用业务 API:
detect / compare / register / search / listUsers 等,在 success / fail 中取结果。
- 需要相机预览时,页面中使用
<zy-face-liveness-x>,监听 @load(原生视图就绪)后调用组件方法 start()。
- 监听组件
@onEvent,按 detail.type 分发(detect / recognize / register / capture / error 等)。
- 页面卸载时调用组件
release() 与插件 release({}),释放相机与引擎。
用法
import {
init,
detect,
compare,
register,
search,
remove,
count,
listUsers,
rename,
setThreshold,
version,
release,
requestPermission
} from '@/uni_modules/zy-face-liveness-x'
1. 初始化引擎
init({
packName: 'Pikachu', // 'Pikachu'(快)/ 'Megatron'(准)
thresholds: 0.48, // 1:N 阈值,越大越严格
maxDetectFaceNum: 5,
success: (res) => {
console.log('引擎就绪', res.version, '重复初始化=', res.alreadyInited)
},
fail: (err) => {
console.error('初始化失败', err.errCode, err.errMsg)
}
})
2. 识别一张图片的人脸信息
detect({
image: '/storage/emulated/0/DCIM/a.jpg', // 路径 / file:// / content:// / http(s) / base64
flags: {
rgbLiveness: true,
quality: true,
mask: true,
eyeState: true,
actions: true,
attribute: true,
emotion: true,
recognize: false
},
success: (res) => {
console.log('人脸数', res.faceNum)
const face = res.faces[0]
console.log('位置', face.rect.x, face.rect.y, face.rect.width, face.rect.height)
console.log('角度', face.angle.pitch, face.angle.roll, face.angle.yaw)
console.log('活体', face.liveness, '质量', face.quality, '口罩', face.mask)
console.log('年龄', face.ageBracket, '性别', face.gender, '人种', face.race)
console.log('动作', face.actions.blink, face.actions.jawOpen, face.actions.shake, face.actions.headRaise)
if (face.faceId > 0) {
console.log('人脸库命中', face.faceId, face.name)
}
},
fail: (err) => {
console.error('检测失败', err.errCode, err.errMsg)
}
})
3. 判断两张图片是否同一人
compare({
image1: '/sdcard/a.jpg',
image2: '/sdcard/b.jpg',
thresholds: 0.48,
success: (res) => {
console.log('相似度', res.similarity, res.percentage + '%', '是否同一人', res.same)
},
fail: (err) => {
// 9280005 表示某张图没检测到人脸
}
})
4. 录入人脸库并检索
// 录入(可批量)
register({
images: ['/sdcard/a.jpg', '/sdcard/b.jpg'],
names: ['张三', '李四'],
success: (res) => {
res.items.forEach((item) => {
console.log(item.success, item.id, item.name, item.message)
})
}
})
// 从人脸库找人
search({
image: '/sdcard/c.jpg',
topK: 3,
success: (res) => {
if (res.matched) {
console.log('命中', res.items[0].id, res.items[0].name, res.items[0].similarity)
}
}
})
// 名单管理
count({ success: (res) => console.log('特征总数', res.count, '已命名', res.namedCount) })
listUsers({ page: 1, pageSize: 20, success: (res) => console.log(res.total, res.users) })
rename({ id: 3, name: '王五', success: (res) => console.log(res.message) })
remove({ ids: [3, 4], success: (res) => console.log('删除', res.removed) })
5. 申请相机权限
requestPermission({
success: (res) => {
// 授权与拒绝都走 success,用 granted 判断
console.log('granted=', res.granted, res.msg, '不再询问=', res.doNotAskAgain)
}
})
6. 页面卸载时释放
onUnmounted(() => {
release({})
})
组件(相机预览)
<template>
<view class="camera-wrap">
<zy-face-liveness-x
ref="faceRef"
class="camera"
:front="true"
:auto-start="false"
@load="onload"
@onEvent="onFaceEvent"></zy-face-liveness-x>
</view>
</template>
<script setup lang="uts">
import { init } from '@/uni_modules/zy-face-liveness-x'
import {
FaceStartOptions,
FaceComponentFlags,
FaceComponentCallbackOptions,
FaceSwitchCameraOptions,
FaceRegisterFaceOptions,
FaceCaptureOptions
} from '@/uni_modules/zy-face-liveness-x/utssdk/interface.uts'
// 事件负载类型(纯 TS 写法,事件桥只给 JSON 字符串,不需要 UTSJSONObject 的 getXxx 方法)
type FaceEventFace = { faceId: number, name: string, liveness: number, quality: number }
type FaceDetectPayload = { faceNum: number, width: number, height: number, faces: FaceEventFace[] }
// 组件暴露的方法(vapor 模式下用带类型的 ref 取方法)
type FaceComponentMethods = {
start: (options?: FaceStartOptions) => void
stop: (options?: FaceComponentCallbackOptions) => void
switchCamera: (options?: FaceSwitchCameraOptions) => void
setDetectFlags: (flags: FaceComponentFlags, options?: FaceComponentCallbackOptions) => void
registerFace: (options?: FaceRegisterFaceOptions) => void
capture: (options?: FaceCaptureOptions) => void
setScaleType: (scaleType: string, options?: FaceComponentCallbackOptions) => void
release: () => void
}
const faceRef = ref<FaceComponentMethods | null>(null)
function onload() {
// 相机检测依赖引擎单例,务必先 init
init({
packName: 'Pikachu',
success: () => faceRef.value?.start({ front: true })
})
}
function onFaceEvent(e: UniNativeViewEvent) {
// 外层字段用下标读(事件桥只保留属性),负载再按类型 parse
// 不要用「dataJson 必填」的类型去 JSON.parse 整份 detail,缺字段会抛
// Failed to contruct type, missing required property
const type = e.detail['type'] as string
const json = e.detail['dataJson']
const dataJson = json == null ? '' : json as string
if (type == 'detect' && dataJson.length > 0) {
const payload = JSON.parse<FaceDetectPayload>(dataJson)
if (payload != null) {
console.log('人脸数', payload!.faceNum, '第一张脸活体', payload!.faces[0].liveness)
}
}
if (type == 'recognize') {
console.log('识别命中', dataJson)
}
}
function startRegister() {
faceRef.value?.registerFace({ name: '张三' })
}
</script>
<style>
.camera-wrap {
width: 100%;
height: 260px;
background-color: #000000;
}
.camera {
width: 100%;
height: 100%;
}
</style>
组件属性
| 属性 |
类型 |
默认 |
说明 |
front |
Boolean |
true |
是否前置摄像头(人脸场景建议前置) |
auto-start |
Boolean |
true |
原生视图就绪并拿到相机权限后自动开始预览 |
detect-flags |
String |
"" |
初始检测开关(JSON 字符串),如 '{"rgbLiveness":true,"recognize":true,"attribute":true}' |
组件方法
| 方法 |
参数 |
说明 |
start |
{ front?, cameraId?, physicalCameraId? } |
开始预览,未授权时先申请相机权限;相机就绪时触发 success,拒绝授权 fail 回 9280010(cameraId / physicalCameraId 当前版本预留) |
stop |
{ success?, fail? } |
停止预览 |
switchCamera |
{ success?, fail? } |
前后摄切换 |
setDetectFlags |
{ rgbLiveness?, quality?, mask?, eyeState?, actions?, attribute?, emotion?, recognize? } |
运行时更新检测开关(整体传入,未传字段回落到原生默认) |
registerFace |
{ name?, savePath?, needBase64? } |
请求录入:下一帧若恰好 1 张人脸则写入人脸库,结果经 onEvent(type=register) |
capture |
{ savePath?, needBase64? } |
抓拍当前帧,结果经 onEvent(type=capture) |
setScaleType |
'FILL_CENTER' \| 'FIT_CENTER' |
画面缩放模式(默认铺满) |
release |
- |
释放相机资源(组件卸载时会自动调用) |
组件事件 @onEvent(e)
e.detail = { type, code, msg, dataJson },dataJson 是负载的 JSON 字符串——跨 native-view 事件桥后嵌套对象会退化成普通 JS 对象(没有 UTSJSONObject 的 getNumber/getArray 等方法),所以负载统一给字符串,页面用 JSON.parse<自定类型>(dataJson) 还原成 TS 对象后直接属性访问。另外 JSON.parse<类型> 是强校验的:类型里声明的必填字段在 JSON 里必须真的存在(可能缺的字段要写成 ? 可选),否则运行期抛 Failed to contruct type, missing required property: xxx;也不要拿“dataJson 必填”的类型去 parse 整份 e.detail(旧基座没有该字段),外层字段一律下标读取。
| type |
dataJson(解析后的字段) |
started / switchCamera |
{ front } |
stopped |
{} |
detect |
{ faceNum, width, height, faces: FaceInfo[] }(约每 60ms 一帧,未检测到人脸也会回调) |
recognize |
{ faceNum, width, height, faces: FaceInfo[] }(仅命中人脸库的人脸) |
register |
{ success, id, name, code, message, path?, base64? } |
capture |
{ path, base64, width, height } |
flags / scaleType |
{ ... }(开关/缩放设置回显) |
error |
{}(看 detail.code / detail.msg) |
插件方法
| 方法 |
说明 |
回调 |
init(options) |
初始化离线引擎(幂等) |
FaceInitResult |
detect(options) |
单图人脸信息 |
FaceDetectResult |
compare(options) |
两图比对 |
FaceCompareResult |
register(options) |
批量注册人脸 |
FaceRegisterResult |
search(options) |
1:N 检索 |
FaceSearchResult |
remove(options) |
按 ID 删除 |
FaceRemoveResult |
count(options) |
人脸库特征总数 |
FaceCountResult |
listUsers(options) |
分页名单 |
FaceListResult |
rename(options) |
修改姓名 |
FaceSimpleResult |
setThreshold(options) |
修改 1:N 阈值 |
FaceSimpleResult |
version(options) |
引擎版本(同步回调) |
FaceVersionResult |
release(options) |
释放引擎 |
FaceSimpleResult |
requestPermission(options) |
申请相机权限 |
FacePermissionResult |
所有方法统一 success / fail 回调,失败为 UniError(errSubject = 'zy-face-liveness-x')。
init 参数
| 字段 |
默认 |
说明 |
packName |
'Pikachu' |
模型包:Pikachu(17MB,快)/ Megatron(61MB,准) |
thresholds |
0.48 |
1:N 检索阈值 |
maxDetectFaceNum |
5 |
单帧最多人脸数 |
searchMode |
0 |
0 EAGER(快)/ 1 EXHAUSTIVE(准) |
videoDetectMode |
2 |
视频:0 每帧检测 / 1 轻量跟踪 / 2 检测+跟踪 |
imageDetectMode |
0 |
图片:0 每帧检测 |
detectPixelLevel |
-1 |
检测像素级别,-1 自动 |
FaceInfo 字段
| 字段 |
说明 |
index |
本帧序号(0 开始) |
trackId |
跟踪 ID(视频稳定,图片为 -1) |
confidence |
检测置信度 |
rect |
{ x, y, width, height } 像素坐标 |
angle |
{ pitch, roll, yaw } 欧拉角(度) |
faceId |
命中的人脸库 ID(>0 命中;-1 未命中/未开启识别) |
name |
命中人脸库时的姓名,未命中为空串 |
liveness |
RGB 活体 0~1,>0.5 判为活体 |
quality |
人脸质量 0~1,>0.5 判为清晰 |
mask |
口罩置信度 0~1,>0.5 判为戴口罩 |
leftEyeOpen / rightEyeOpen |
1 睁 / 0 闭 |
emotion |
0 无表情 1 开心 2 伤心 3 吃惊 4 恐惧 5 厌恶 6 愤怒 |
ageBracket |
0:0~2 1:3~9 2:10~19 3:20~29 4:30~39 5:40~49 6:50~59 7:60~67 8:>70 |
gender |
0 女 / 1 男 |
race |
0 黑人 1 亚洲人 2 拉丁裔 3 中东 4 白人 |
actions |
{ blink, jawOpen, shake, headRaise, normal },0 未发生 / 1 发生 |
错误码
| 错误码 |
说明 |
| 9280001 |
参数错误 |
| 9280002 |
引擎未初始化,请先调用 init |
| 9280003 |
引擎初始化失败(模型包缺失 / 设备不支持) |
| 9280004 |
图片读取失败(路径不存在 / 下载失败 / base64 非法) |
| 9280005 |
未检测到人脸 |
| 9280006 |
检测到多张人脸(注册要求单张) |
| 9280007 |
特征提取失败 |
| 9280008 |
人脸库操作失败 |
| 9280009 |
相机不可用(无摄像头 / 打开失败 / 被占用) |
| 9280010 |
未获得相机权限 |
| 9280011 |
无法获取应用上下文(请在页面 onLoad 之后调用) |
| 9280012 |
引擎已初始化(重复 init 按成功返回,此码保留) |
| 9280013 |
文件保存失败 |
| 9280014 |
操作失败(含异常信息) |
权限说明
| 权限 |
用途 |
android.permission.CAMERA |
相机预览、相机录入、抓拍 |
android.permission.INTERNET |
仅在 image 传入 http(s) 地址时下载图片(离线识别本身不联网) |
android.permission.ACCESS_NETWORK_STATE |
远程图片下载时的网络状态判断 |
注意事项
- 必须自定义基座:组件依赖
androidx.camera 与离线引擎 SDK(云端 maven 依赖),标准基座不含这些库,需「发行 → 制作自定义基座」后才能运行;改完原生代码需重新制作基座。
- 云打包需能访问 jitpack.io(拉取离线引擎 aar),首次打包会下载约数十 MB 依赖。
- 只开需要的开关:
quality / mask / eyeState / actions / attribute / emotion 每开一项都会增加单帧耗时;相机实时识别默认只开 rgbLiveness,需要属性时再开,recognize: true 会额外做特征提取 + 检索,最耗性能。
- 动作活体不要只看单帧,建议统计 2~3 秒内连续帧(如
blink 命中 ≥1 且 liveness > 0.5 的帧数占比)。
- RGB 活体建议连续采集多帧,活体帧数多于非活体帧数时判为真人。
- 注册质量:录入图不要戴口罩 / 墨镜,人脸正对、光线均匀;先看
detect 结果里 quality > 0.5 再注册。
- init 时机:首次 init 会解压模型,务必在页面可交互后调用并给 loading;相机组件每帧检测依赖引擎单例,未 init 时
onEvent 会持续回 9280002。
- 存储位置:特征库
Android/data/<包名>/files/zy_face_liveness/face_feature.db,名单表 .../face_names.db;卸载或清除数据会丢失人脸库。