更新记录
1.0.11(2025-11-06)
- Android兼容一些电子班牌大屏等设备
1.0.10(2025-10-18)
- 增加设置摄像头方向接口setCameraOrient
1.0.9(2025-10-16)
- 相机视频和静态图片处理分隔开会话处理,以增加识别度
- 优化iOS相机长时间识别的性能问题
查看更多
平台兼容性
uni-app(3.6.12)
| Vue2 |
Vue3 |
Chrome |
Safari |
app-vue |
app-nvue |
Android |
iOS |
鸿蒙 |
| √ |
√ |
- |
- |
√ |
√ |
7.0 |
√ |
- |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
快应用-华为 |
快应用-联盟 |
| - |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
uni-app x(3.6.12)
| Chrome |
Safari |
Android |
iOS |
鸿蒙 |
微信小程序 |
| - |
- |
7.0 |
√ |
- |
- |
其他
人脸识别离线识别活体识别
主要功能:
- 本地离线识别,无需网络
- 活体识别
- 相机录入人脸
- 本地图片/远程图片录入
- 本地人脸库管理
- 从人脸库里搜索某个图片
- 同时识别多人
- 判断两张图片是否是同一人
- 实时识别人脸信息(包含人脸数、人脸位置、年龄、性别、人种、眼睛张开、表情、摇头等等)
开发文档
- 拷贝demo里的AndroidManifest.xml、Info.plist文件到项目根目录
- 集成插件,集成插件步骤请参考
https://www.cnblogs.com/wenrisheng/p/18323027
- demo里集成了sqlite数据库https://ext.dcloud.net.cn/plugin?name=wrs-uts-sqlite,用来保存用户名/用户ID等,可以自行选择数据保存方式
- 购买激活码/咨询或定制请点击上面"进入交流群"私聊作者
横竖屏切换问题
- iOS横竖屏切换都没问题
- Android横竖屏切换:
- 由竖屏进入调到识别界面,不切换横屏时可以正常识别,当竖屏切换到横屏时,需要调用下打开相机(startCamera)业务
- 由横屏进入到识别界面,切换横竖屏可以正常识别
活体检测问题
- 插件支持rgb活体检测和动作活体检测,建议实际项目采用两者相结合的方式来测试,有些手机的相机rgb不是很准确,需要用户动作配合
- 动作活体检测可以没次都随机抽取左右睁眼、张嘴、摇头、抬头这几个动作进行随机组合的方式,可以根据项目情况自行处理
图片录入问题
- 优先采用本设备相机录入
- 批量录入时,如果返回有失败的图片,可以再次使用失败图片重新录入,尽量分多批次来录入
- 录入时图片人物不要带口罩、眼镜等容易造成误差的物件
组件
<wrs-uts-face ref="arcfacepro" :style="'width:'+width+'px;height:'+height+'px;'" @onEvent="onEvent"
:params="params"></wrs-uts-face>
- 使用了该组件的页面要使用nvue或uvue
- 从相机录入人脸到本地人脸库,注册结果在onEvent里回调
let imageName = getUserName()
let filePath = plus.io.convertLocalFileSystemURL("_download/" + imageName + ".jpg");
let newParams = {}
newParams.businessArray = [{
business: "startRegister",
params: { // 参数可选
needBase64: false, // 是否返回图片的base64
filePath: filePath // 是否保存到本地文件
}
}]
let newParamsStr = this.formatNewParams(newParams)
// android、ios
this.params = newParamsStr
// 鸿蒙
this.options = {
params: newParamsStr
}
- 从相机识别识别人脸,识别结果在onEvent里回调
let newParams = {}
newParams.businessArray = [{
business: "startCompare", // 对比业务
params: {
compare: true // true:开始对比,false:停止对比
}
}]
this.params = JSON.stringify(newParams)
let newParams = {}
newParams.businessArray = [{
business: "switchCamera"
}]
this.params = JSON.stringify(newParams)
let newParams = {}
newParams.businessArray = [{
business: "startCamera"
}]
let newParamsStr = this.formatNewParams(newParams)
// android、ios
this.params = newParamsStr
// 鸿蒙
this.options = {
params: newParamsStr
}
let newParams = {}
newParams.businessArray = [{
business: "stopCamera"
}]
let newParamsStr = this.formatNewParams(newParams)
// android、ios
this.params = newParamsStr
// 鸿蒙
this.options = {
params: newParamsStr
}
let imageName = getUserName()
let filePath = plus.io.convertLocalFileSystemURL("_download/" + imageName + ".jpg");
let newParams = {}
newParams.businessArray = [{
business: "getCurImage",
params: {
needBase64: false, // 是否返回base64
filePath: filePath // 是否保存为本地文件
}
}]
let newParamsStr = this.formatNewParams(newParams)
// android、ios
this.params = newParamsStr
// 鸿蒙
this.options = {
params: newParamsStr
}
let newParams = {}
newParams.businessArray = [{
business: "setDetectAttri",
params: {
rgbLiveness: true, // 是否开启RGB活体识别
faceMask: true, // 是否开启口罩识别
faceInteractionState: true, // 是否开启眼睛识别:左右眼睁/闭
faceInteractionsActions: true, // 是否脸部动作识别:摇头、张嘴、抬头等
faceAttribute: true, // 是否开启属性识别:年龄、种族、性别
faceEmotion: true // 是否开启表情识别,喜怒哀乐等
}
}]
let newParamsStr = this.formatNewParams(newParams)
// android、ios
this.params = newParamsStr
// 鸿蒙
this.options = {
params: newParamsStr
}
- onLoadView,视图加载事件
- onEvent,人脸注册/识别事件
onEvent(e) {
// console.log("onEvent:" + JSON.stringify(e))
let detail = e.detail
let opt = detail.opt
if (opt != "onDetectFace" && opt != "onRecognize" && opt != "onDetectFace") {
console.log("onEvent:" + JSON.stringify(e))
}
switch (opt) {
// 加载完视图
case "onLoadView": {
}
break;
// 获取当前图片的回调
case "getCurImage": {
let filePath = detail.filePath
if (filePath) {
this.curImageSrc = "file://" + filePath
}
}
break;
// 注册回调
case "onRegister": {
let success = detail.success
let msg = ""
if (success) { // 注册成功
let filePath = detail.filePath
if (filePath) {
this.registerImageSrc = "file://" + filePath
}
let id = detail.id
// 随机生成一个用户名
let name = getUserName()
let user = {}
user.id = id
user.name = name
// 保存用户信息到数据库
insertUser([user], (resp) => {
})
msg = "注册成功:" + name
} else { // 注册失败
msg = "注册失败:" + JSON.stringify(detail)
}
console.log(msg)
this.showToast(msg)
}
break;
// 对比结果回调
case "onRecognize": {
// {
// "faceInfo": {
// "faceAttribute": {
// "ageBracket": [4],
// "gender": [0],
// "num": 1,
// "race": [3]
// },
// "faceData": {
// "angles": [{
// "pitch": 33.292926788330078,
// "roll": 2.8457748889923096,
// "yaw": 10.13283634185791
// }],
// "detConfidence": [0],
// "detectedNum": 1,
// "rects": [{
// "height": 206,
// "width": 193,
// "x": 213,
// "y": 133
// }],
// "trackIds": [18]
// },
// "faceEmotion": {
// "emotion": [],
// "num": 1
// },
// "faceInteractionsActions": {
// "blink": [0],
// "headRaise": [1],
// "jawOpen": [0],
// "normal": [0],
// "num": 1,
// "shake": [0]
// },
// "faceInteractionState": {
// "leftEyeStatusConfidence": [1],
// "num": 1,
// "rightEyeStatusConfidence": [1]
// },
// "faceMask": {
// "confidence": [0.089397557079792023],
// "num": 1
// },
// "rgbLiveness": {
// "confidence": [0.9998619556427002],
// "num": 1
// }
// },
// "faceList": [{
// "bottom": 283.91250610351562,
// "id": -1,
// "left": 117.38749694824219,
// "right": 279.02499389648438,
// "top": 111.38749694824219
// }],
// "viewHeight": 402,
// "viewWidth": 414
// }
let data = detail.data
let dataObj = JSON.parse(data)
let faceNum = 0
let faceList = dataObj.faceList
if (faceList) {
faceNum = faceList.length
}
this.dataArray.length = 0
this.dataArray = []
let idArray = []
let faceInfo = dataObj.faceInfo
if (faceNum > 0) {
for (let i = 0; i < faceNum; i++) {
let userDesc = converFaceInfo(faceList, i, faceInfo, this.recognizedUserArray)
this.dataArray.push(userDesc)
}
}
}
break;
default:
break;
}
},
接口
import {
UTSFaceMgr
UTSFaceEngine
} from "@/uni_modules/wrs-uts-face"
- 激活引擎,激活引擎接口业务一般放到app启动的onLaunch函数里,激活码请进入交流群联系作者
let params = {}
params["activeCode"] = "xxx" // 激活码
UTSFaceEngine.activeEngine(params, (resp) => {
let code = resp.code
let str = ""
if (code == 0) {
str = "激活成功"
} else if (code == 11) {
str = "包名绑定错误code:" + code
} else if (code == 12) {
str = "激活码已过期code:" + code
} else if (code == 13) {
str = "读写权限未授权code:" + code
} else if (code == 14) {
str = "设备指纹错误code:" + code
} else if (code == 15) {
str = "产品错误code:" + code
} else if (code == 16) {
str = "平台错误code:" + code
} else {
str = "激活失败code:" + code
}
this.msg = str
this.showToast(str)
})
UTSFaceEngine.getDeviceFinger((resp) => {
let data = resp.data
if (data) {
this.showMsg("设备指纹:" + data)
this.showToast("指纹已复制到粘贴板")
} else {
this.showMsg("获取设备指纹失败")
}
})
let url1 = "/xxx/xxx/xx.jpg"
let url2 = "/sss/xxx/xx.jpg"
UTSFaceEngine.compareFace(url1, url2, (resp)=>{
// {
// "percentage": 0.9221189618110657, // 相似度百分比
// "faceNum2": 1, // 第二张图人脸数
// "similarity": 0.7389912009239197, // 相似度
// "faceNum1": 1 // 第一张图人脸数
// }
console.log(JSON.stringify(resp))
let similarity = resp.similarity
if(similarity > 0.48) {
this.showMsg("两张图片是同一个人")
} else {
this.showMsg("两张图片不是同一个人")
}
})
- 获取图片人脸信息(包含人脸数、人脸位置、年龄、性别、人种、眼睛张开、表情、摇头等等)
UTSFaceEngine.getFaceInfo(this.url, (resp) => {
let str = ""
let faceData = resp.faceData
if (faceData) {
let detectedNum = faceData.detectedNum
if (detectedNum > 0) {
str = str + "总人数:" + detectedNum + "人\n"
let faceStr = "人脸坐标:\n"
for (let i = 0; i < detectedNum; i++) {
let rect = faceData.rects[i]
faceStr = faceStr + " 第" + ( i + 1 ) + "人: " + JSON.stringify(rect) + " \n"
}
str = str + faceStr + "\n"
// 眼睛状态
let faceInteractionState = resp.faceInteractionState
if (faceInteractionState) {
// 多少对眼睛
let eysStr = "眼睛状态:\n"
let num = faceInteractionState.num
for (let i = 0; i < num; i++) {
let rightEye = faceInteractionState.rightEyeStatusConfidence[i]
let leftEye = faceInteractionState.leftEyeStatusConfidence[i]
let leftEyeStr = ""
let rightEyeStr = ""
if (leftEye == 1) {
leftEyeStr = "睁开"
} else {
leftEyeStr = "关闭"
}
if (rightEye == 1) {
rightEyeStr = "睁开"
} else {
rightEyeStr = "关闭"
}
eysStr = eysStr + "第" + ( i + 1 ) + "个人:" + "左眼:" + leftEyeStr + " 右眼:" + rightEyeStr +
"\n"
}
str = str + eysStr + "\n"
}
let faceQuality = resp.faceQuality
if (faceQuality) {
let num = faceQuality.num
let faceQualityStr = "人脸质量:\n"
for (let i = 0; i < num; i++) {
let confidence = faceQuality.confidence[i]
let str = ""
if (confidence > 0.5) {
str = "人脸清晰"
} else {
str = "人脸模糊"
}
faceQualityStr = faceQualityStr + "第" + ( i + 1 ) + "个人:" + str + "\n"
}
str = str + faceQualityStr + "\n"
}
let faceEmotion = resp.faceEmotion
if (faceEmotion) {
let num = faceEmotion.num
let emotionStr = "人物表情\n"
for (let i = 0; i < num; i++) {
let emotion = faceEmotion.emotion[i]
let str = ""
// 0: 没表情 1:开心 2:伤心 3:吃惊 4:恐惧 5:厌恶 6:愤怒
switch (emotion) {
case 0:
str = "没表情"
break;
case 1:
str = "开心"
break;
case 2:
str = "伤心"
break;
case 3:
str = "吃惊"
break;
case 4:
str = "恐惧"
break;
case 5:
str = "厌恶"
break;
case 6:
str = "愤怒"
break;
default:
break;
}
emotionStr = emotionStr + "第" + ( i + 1 ) + "个人:" + str + "\n"
}
str = str + emotionStr + "\n"
}
let faceAttribute = resp.faceAttribute
if (faceAttribute) {
let num = faceAttribute.num
let faceAttributeStr = "人物属性:\n"
for (let i = 0; i < num; i++) {
console.log("3.0")
let ageBracket = faceAttribute.ageBracket[i]
let gender = faceAttribute.gender[i]
let race = faceAttribute.race[i]
let ageBracketStr = ""
switch (ageBracket) {
case 0:
ageBracketStr = "0~2"
break;
case 1:
ageBracketStr = "3~9"
break;
case 2:
ageBracketStr = "10~19"
break;
case 3:
ageBracketStr = "20~29"
break;
case 4:
ageBracketStr = "30~39"
break;
case 5:
ageBracketStr = "40~49"
break;
case 6:
ageBracketStr = "50~59"
break;
case 7:
ageBracketStr = "60~67"
break;
case 8:
ageBracketStr = "大于70"
break;
default:
break;
}
let genderStr = ""
if (gender == 0) {
genderStr = "女"
} else {
genderStr = "男"
}
let raceStr = ""
console.log("3.2")
switch (race) {
case 0:
raceStr = "黑人"
break;
case 1:
raceStr = "亚洲人"
break;
case 2:
raceStr = "拉丁裔和西班牙裔"
break;
case 3:
raceStr = "中东"
break;
case 4:
raceStr = "白人"
break;
default:
break;
}
faceAttributeStr = faceAttributeStr + "第" + ( i + 1 ) + "个人:" + " 年龄:" + ageBracketStr +
" 性别:" + genderStr + " 种族:" + raceStr + "\n"
}
str = str + faceAttributeStr + "\n"
}
let faceMask = resp.faceMask
if (faceMask) {
let num = faceMask.num
let faceMaskStr = "口罩:\n"
for (let i = 0; i < num; i++) {
let confidence = faceMask.confidence[i]
console.log("口罩阈值:" + confidence)
let str = ""
if (confidence > 0.5) {
str = "有带口罩"
} else {
str = "没带口罩"
}
faceMaskStr = faceMaskStr + "第" + ( i + 1 ) + "个人:" + str + "\n"
}
str = str + faceMaskStr + "\n"
}
let faceInteractionsActions = resp.faceInteractionsActions
if (faceInteractionsActions) {
let num = faceInteractionsActions.num
let faceInteractionsActionsStr = "脸部动作:\n"
// "faceInteractionsActions": {
// "headRaise": [0], // 抬头
// "jawOpen": [0], // 张嘴
// "num": 1,
// "normal": [1],// 有没有动作
// "blink": [0], // 眨眼动作
// "shake": [0] // 左右摇头
// }
for (let i = 0; i < num; i++) {
let headRaise = faceInteractionsActions.headRaise[i]
let jawOpen = faceInteractionsActions.jawOpen[i]
let normal = faceInteractionsActions.normal[i]
let blink = faceInteractionsActions.blink[i]
let shake = faceInteractionsActions.shake[i]
let headRaiseStr = ""
// 0: 没表情 1:开心 2:伤心 3:吃惊 4:恐惧 5:厌恶 6:愤怒
switch (headRaise) {
case 0:
headRaiseStr = "抬头:无"
break;
case 1:
headRaiseStr = "抬头:有"
break;
default:
break;
}
let jawOpenStr = ""
if (jawOpen == 0) {
jawOpenStr = "张嘴:无"
} else {
jawOpenStr = "张嘴:有"
}
let normalStr = ""
switch (normal) {
case 0:
normalStr = "动作:无"
break;
case 1:
normalStr = "动作:有"
break;
default:
break;
}
let blinkStr = ""
switch (blink) {
case 0:
blinkStr = "眨眼:无"
break;
case 1:
blinkStr = "眨眼:有"
break;
default:
break;
}
let shakeStr = ""
switch (shake) {
case 0:
shakeStr = "摇头:无"
break;
case 1:
shakeStr = "摇头:有"
break;
default:
break;
}
faceInteractionsActionsStr = faceInteractionsActionsStr + "第" + ( i + 1 ) + "个人:" +
headRaiseStr + " " + jawOpenStr + " " + normalStr + " " + blinkStr + " " +
shakeStr + "\n"
}
str = str + faceInteractionsActionsStr + "\n"
}
} else {
str = "没有检测到人脸"
}
}
console.log(JSON.stringify(resp))
this.showMsg(str)
// {
// "faceData": { // 人脸信息
// "detectedNum": 1, // 人脸数
// "trackIds": [-1],
// "detConfidence": [0],
// "rects": [{ // 人脸位置
// "height": 284,
// "x": 239,
// "y": 272,
// "width": 280
// }],
// "angles": [{ // 角度
// "roll": -2.7232625484466553,
// "pitch": -8.401390075683594,
// "yaw": -10.91862678527832
// }]
// },
// "faceInteractionState": { // 眼睛状态
// "rightEyeStatusConfidence": [1], // 所有右眼状态:1:睁开 0:关闭
// "num": 1, // 一对眼睛
// "leftEyeStatusConfidence": [1] // 所有左眼状态:1:睁开 0:关闭
// },
// "faceQuality": { // 图片人脸质量
// "confidence": [0.820976734161377], // 质量阈值
// "num": 1
// },
// "faceEmotion": { // 表情
// "num": 1,
// "emotion": [0] // 0: 没表情 1:开心 2:伤心 3:吃惊 4:恐惧 5:厌恶 6:愤怒
// },
// "faceAttribute": {
// "ageBracket": [3], // 年龄区域 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
// "num": 1,
// "gender": [0], // 性别 0:女 1:男
// "race": [1] // 种族 0:黑人 1:亚洲人 2:拉丁裔和西班牙裔 3:中东 4:白人
// },
// "rgbLiveness": {
// "confidence": [0.8709309101104736],
// "num": 1
// },
// "faceMask": { // 是否带口罩
// "confidence": [0.08445872366428375], // 带口罩系数,一般大于0.5认为是带了口罩
// "num": 1
// },
// "faceInteractionsActions": {
// "headRaise": [0], // 抬头
// "jawOpen": [0], // 张嘴
// "num": 1,
// "normal": [1],// 有没有动作
// "blink": [0], // 眨眼动作
// "shake": [0] // 左右摇头
// }
// }
// this.showMsg(JSON.stringify(resp))
})
人脸库管理
let localImagePath = UTSFaceEngine.getResourcePath("/static/yifei.png")
let remoteImagePath = this.remoteImageUrl
let userName = getUserName()
let userNames = [userName + "_1", userName + "_2"]
let params = {}
params.data = [localImagePath, remoteImagePath]
// 源图片路径,支持绝对路径,如:/afa/ss/aa.png或远程网络路径,如:https://ssss/ss.png
UTSFaceMgr.registerUser(params, (resp) => {
let data = resp.data
let allSuccess = true
let length = data.length
let userArray = []
for (let i = 0; i < length; i++) {
let success = data[i].success
if (success) {
let id = data[i].id
let name = userNames[i]
let user = {}
user.id = id
user.name = name
userArray.push(user)
} else {
allSuccess = false
}
}
if(userArray.length > 0) {
// 注册成功后保存到本地数据库
insertUser(userArray, (resp)=>{
this.refreshFaceData()
})
}
});
// 根据id删除,注册的时候会返回id
let id = 2
let params = {}
params.data = [id]
// 删除人脸库
UTSFaceMgr.deleteUser(params, () => {
let resultArray = resp.data
let length = resultArray.length;
let suc = true;
for(let i = 0; i < length; i ++) {
suc = resultArray[i]
}
if(suc) {
// 删除数据
deleteUser(id, (resp)=>{
this.refreshFaceData()
})
}
})
let page = 1 // 页码
let pageSize = 20 // 每页数量
query(page, pageSize, (userArray)=>{
this.dataArray.push(...userArray)
if(userArray.length == 0) {
this.showToast("没有下一页数据了")
}
})
getTotalCount((resp) => {
let totalCount = resp.data
this.showMsg("总数:" + totalCount)
})
// path支持本地图片或网络图片
let path = "/xxxx/xxx/xxx.jpg"
UTSFaceMgr.recognizeImage(path, (resp) => {
this.showMsg(JSON.stringify(resp))
let isRegistered = false
let data = resp.data
if(data) {
let length = data.length
if(length > 0) {
isRegistered = true
}
}
if(isRegistered) {
this.showMsg("这张图片已经注册")
} else {
this.showMsg("这张图片没有注册")
}
})
参数设置
let threshold = 0.48 // 大于0.48就认为是同一个人
UTSFaceEngine.setSearchHubThreshold(threshold)
let isFront = true
UTSFaceEngine.setCameraOrient(isFront)
let isFront = UTSFaceEngine.getCameraOrient()