添加人脸识别

This commit is contained in:
wangxiaowei
2026-01-06 16:16:57 +08:00
parent 42f3dbd362
commit a564ef78bd
18 changed files with 1300 additions and 7 deletions

View File

@ -0,0 +1,36 @@
## 1.1.12023-04-13
文档更新:动作容器的使用案例
## 1.1.02023-04-13
整理代码,补充文档信息
## 1.0.142023-04-13
更新文档
## 1.0.132023-04-13
更新文档信息
## 1.0.122023-04-13
修复授权弹窗bug
## 1.0.112023-04-11
文档调整
## 1.0.102023-04-11
更新文档
## 1.0.92023-04-11
1.优化点头摇头动作
2.解决页面回退在ios出现空白页
3.调整ui布局
## 1.0.82023-04-03
修改文档并提出在ios中二次调用bug的解决方案
## 1.0.72023-03-28
未通过核验停留两秒对未授权拍照bug修复
## 1.0.62023-03-27
提示用户开启摄像头权限
## 1.0.52023-03-22
bug修复文档修订
## 1.0.42023-03-21
更新文档
## 1.0.32023-03-21
新增 动作容器ActionContainer 类 开发者可通过自定义动作 添加到该容器中管理。也可使用已有动作进行排列组合创建动作组
## 1.0.22023-03-16
调整通过条件,一次行为就可以通过验证
## 1.0.12023-03-15
参数调整,提高通过率
## 1.0.02023-03-14
基于微信小程序的人脸识别做的简易版活体检测

View File

@ -0,0 +1,59 @@
class ActionContainer {
//动作组 actions
//起始动作下标 index
//初始提示 tip
//绑定 完成所有动作的回调 endFun
//绑定 动作进行中 ingFun
//绑定 动作完成时 successFun
//绑定 动作失败时 failFun
constructor(actions, index, tip, endFun, ingFun, successFun, failFun) {
this.actions = actions || []
this.index = index || 0
this.tip = tip || '检测不到人脸'
this.endFun = endFun || this.__tempFun
this.ingFun = ingFun || this.__tempFun
this.successFun = successFun || this.__tempFun
this.failFun = failFun || this.__tempFun
}
__tempFun(){
}
next(faceData) {
if (this.index >= this.actions.length) {
this.endFun()
return this
}
if (this.actions[this.index].state === 'ing') {
this.tip = this.actions[this.index].tip
this.actions[this.index].check(faceData)
this.ingFun()
return this
} else if (this.actions[this.index].state === 'success') {
this.index++;
this.successFun()
return this
} else if (this.actions[this.index].state === 'fail') {
this.failFun()
return this
}
return this
}
end(fun) { //绑定 完成所有动作的回调
this.endFun = fun || this.__tempFun
return this
}
ing(fun) { //绑定 动作进行中
this.ingFun = fun || this.__tempFun
return this
}
success(fun) { //绑定 动作完成时
this.successFun = fun || this.__tempFun
return this
}
fail(fun) { //绑定 动作失败时
this.failFun = fun || this.__tempFun
return this
}
}
export default ActionContainer

View File

@ -0,0 +1,57 @@
const STATE = {
ING:'ing',
SUCCESS:'success',
FAIL:'fail'
}
export default class Action {
constructor(second=10,fun,limit,initTip) {
this.second = second
this.endTime = Infinity
this.frames = []
this.tip = initTip
this.initTip = initTip
this.state = STATE.ING
this.fun = fun
this.limit = limit
}
end(){
if(this.fun){
this.fun(this.state)
}
}
check(faceData){
if(this.endTime === Infinity){
this.endTime = new Date().getTime() + (this.second*1000)
}
if(this.state !== STATE.ING ){
return
}
if(new Date().getTime()>this.endTime){
this.state = STATE.FAIL
this.end()
return
}
if(this.frames.length>=this.limit){
this.state = STATE.SUCCESS
this.end()
return
}
this.takeFrameAfter(faceData)?.takeFrame(faceData)
}
takeFrame(faceData){
}
takeFrameAfter(faceData){
let face = faceData.faceInfo[0]
this.tip = this.initTip
if(faceData.x == -1 || faceData.y == -1) {
this.tip = '检测不到人脸'
return null
}
if(faceData.faceInfo.length > 1) {
this.tip = '请保证只有一人做核验'
return null
}
return this
}
}

View File

@ -0,0 +1,24 @@
import Action from "./Action.js"
class NodHead extends Action {
constructor(second=10,fun) {
super(second,fun,1,'请点头')
this.maxPitch = 0
this.minPitch = 0
}
takeFrame(faceData){
let face = faceData.faceInfo[0]
if(face.angleArray.pitch>this.maxPitch){
this.maxPitch = face.angleArray.pitch
}
if(face.angleArray.pitch<this.minPitch){
this.minPitch = face.angleArray.pitch
}
if(Math.abs(this.minPitch-this.maxPitch)>0.45 && (this.minPitch || this.maxPitch) ){
this.frames.push('点头')
this.maxPitch = 0
this.minPitch = 0
}
}
}
export default NodHead

View File

@ -0,0 +1,24 @@
import Action from "./Action.js"
class ShakeHead extends Action {
constructor(second = 10, fun) {
super(second,fun,1,'请摇头')
this.minYaw = 0
this.maxYaw = 0
}
takeFrame(faceData) {
let face = faceData.faceInfo[0]
if(face.angleArray.yaw>this.maxYaw){
this.maxYaw = face.angleArray.yaw
}
if(face.angleArray.yaw<this.minYaw){
this.minYaw = face.angleArray.yaw
}
if(Math.abs(this.minYaw-this.maxYaw)>0.45 && (this.minYaw || this.maxYaw) ){
this.frames.push('摇头')
this.minYaw = 0
this.maxYaw = 0
}
}
}
export default ShakeHead

View File

@ -0,0 +1,27 @@
import Action from "./Action.js"
class StraightenHead extends Action {
constructor(second = 10, fun) {
super(second, fun, 10, '请平视摄像头')
}
takeFrame(faceData) {
let face = faceData.faceInfo[0]
if (Math.abs(face.angleArray.pitch) >= 0.3 || Math.abs(face.angleArray.roll) >= 0.2 || Math.abs(face
.angleArray.yaw) >= 0.2) {
this.frames = []
return
}
if (Math.abs(face.confArray.global) <= 0.8 || Math.abs(face.confArray.leftEye) <= 0.8 || Math.abs(
face.confArray.mouth) <=
0.8 || Math.abs(face.confArray.nose) <= 0.8 || Math.abs(face.confArray.rightEye) <= 0.8) {
this.tip = '请勿遮挡五官'
this.frames = []
return
}
this.tip = '正在核验,请保持'
this.frames.push('正')
}
}
export default StraightenHead

View File

@ -0,0 +1,9 @@
import NodHead from "./NodHead.js"
import ShakeHead from "./ShakeHead.js"
import StraightenHead from "./StraightenHead.js"
export {
NodHead,
ShakeHead,
StraightenHead,
}

View File

@ -0,0 +1,436 @@
<template>
<view class="modal bottom-modal" :class="show ? 'show' : ''">
<camera flash="off" device-position="front" resolution="high" @stop="stop" @error="error"
style="width: 100vw;height: 100vh; position: fixed;top: 0;left: 0;">
<cover-view class="cover">
<cover-view class="cover-top cover-item">
<cover-view class="bar bg-white" style="justify-content: flex-start;">
<cover-view class="action" @tap="close">关闭
</cover-view>
</cover-view>
<template>
<slot>
<cover-view v-if="isDev && face">
<cover-view :style="Math.abs(face.pitch)>0.5?'color:red':''">
{{ face.pitch ? face.pitch.toFixed(2): 'null'}}
</cover-view>
<cover-view :style="Math.abs(face.roll)>0.5?'color:red':''">
{{ face.roll ? face.roll.toFixed(2):'null'}}
</cover-view>
<cover-view :style="Math.abs(face.yaw)>0.5?'color:red':''">
{{ face.yaw ? face.yaw.toFixed(2):'null'}}
</cover-view>
</cover-view>
</slot>
</template>
<cover-view class="detectInfo">{{ isSuccess ? '人脸检测成功' : tipsText}}</cover-view>
</cover-view>
<cover-view class="camera">
<cover-image class="camera" src="../../static/images/cover.png"></cover-image>
</cover-view>
<cover-view class="cover-bottom cover-item"></cover-view>
</cover-view>
</camera>
</view>
</template>
<script>
// 人脸检测
/**
* @event {Function} photoChange 拍照完成事件
* @event {Function} detectFailed 人脸检测失败
* @event {Function} detectOver 人脸检测结束
* @method {Function} initData 初始化人脸检测
*/
import {
NodHead,
StraightenHead,
ShakeHead
} from './actions/index.js'
import ActionContainer from './ActionContainer.js'
export default {
name: 'face-detect',
data() {
return {
show: false,
tipsText: '检测不到人脸',
isSuccess: false, //是否检测完成
face: {},
actionsList: null,
context: "",
tipsTextCss: "tipsTextCss",
listener: null
}
},
props: {
buildActionContainer: Function,
actions: () => {
return []
},
isDev: false,
},
mounted() {
// uni.initFaceDetect()
// this.onCameraFrame()
},
destroyed() {
},
methods: {
setMaxFace(faceData) {
const faces = faceData.faceInfo // 获取到所有人脸信息
let maxFaceIndex = 0
let maxFaceSize = 0
// 遍历所有人脸信息,找到最大的人脸
for (let i = 0; i < faces.length; i++) {
const face = faces[i]
const faceSize = face.width * face.height
if (faceSize > maxFaceSize) {
maxFaceSize = faceSize
maxFaceIndex = i
}
}
// 返回最大的那张脸的坐标信息
const maxFace = faces[maxFaceIndex]
faceData.faceInfo = [maxFace]
},
onCameraFrame() {
uni.initFaceDetect()
let time = new Date().getTime()
this.context = uni.createCameraContext()
this.listener = this.context.onCameraFrame((frame) => {
uni.faceDetect({
frameBuffer: frame.data,
width: frame.width,
height: frame.height,
enablePoint: true,
enableConf: true,
enableAngle: true,
enableMultiFace: true,
success: (faceData) => {
time = new Date().getTime()
this.setMaxFace(faceData)
this.showData(faceData)
this.actionsList.next(faceData)
this.tipsText = this.actionsList.tip
},
fail: (err) => {
if ((time + 10 * 1000) < new Date().getTime()) {
this.tipsText = '检测不到人脸'
this.cameraError()
return
}
if (err.x == -1 || err.y == -1) {
this.tipsText = '检测不到人脸'
}
}
})
})
this.listener.start()
},
error() {
this.tipsText = '相机异常'
this.cameraError()
},
stop() {
this.tipsText = '相机异常'
this.cameraError()
},
// 核验失败
cameraError(e) {
this.t = setTimeout(() => {
clearTimeout(this.t)
this.hideModal()
this.$emit('detectFailed')
}, 2000);
},
close() {
clearTimeout(this.t)
this.hideModal()
this.$emit('detectFailed')
},
// 关闭
hideModal() {
uni.stopFaceDetect()
this.show = false
this.tipsText = '检测不到人脸'
this.face = {}
this.isSuccess = false
},
// 拍照
takePhoto() {
this.context.takePhoto({
quality: 'low',
success: (res) => {
this.$emit('photoChange', res.tempImagePath)
},
fail: (e) => {
console.log(e)
},
complete: (e) => {
console.log(e)
}
});
},
// 检测完成
detectOver() {
this.isSuccess = true
let t = setTimeout(() => {
this.hideModal()
clearTimeout(t)
this.$emit('detectOver')
}, 3000);
},
initData() {
uni.getSetting({
success: (res) => {
if (res.authSetting['scope.camera']===true) {
this.onCameraFrame()
this.faceDetect()
} else if(res.authSetting['scope.camera']=== false) {
this.getCameraAuth()
}else{
this.onCameraFrame()
this.faceDetect()
}
}
})
},
getCameraAuth() {
uni.showModal({
title: '温馨提示',
content: '需要获取您摄像头权限才能更好的为您服务!是否授权摄像头权限?',
confirmText: '授权',
confirmColor: '#f94218',
success: (res) => {
if (res.confirm) {
// 选择弹框内授权
uni.openSetting({
success: (res) => {
if (res.authSetting[
'scope.camera'
]) {
this.onCameraFrame()
this.faceDetect()
} else {
this.tipsText = "您未授权摄像头权限"
this.cameraError()
}
}
})
} else if (res.cancel) {
this.tipsText = "您未授权摄像头权限"
this.cameraError()
}
}
})
},
showData(faceData) {
this.$emit("showData", faceData)
if (this.isDev) {
let face = faceData.faceInfo[0].angleArray
this.face = face
}
},
buildActions() {
if (this.buildActionContainer) {
return this.buildActionContainer()
}
let actions = []
if (!this.actions?.length) {
let nodHead = new NodHead()
const fun = (state) => {
if (state === 'success') {
this.takePhoto()
}
}
let straightenHead = new StraightenHead(10, fun)
let straightenHead2 = new StraightenHead(10)
let shakeHead = new ShakeHead()
actions = [straightenHead2, nodHead, shakeHead, straightenHead]
} else {
actions = this.actions
}
let act = new ActionContainer(actions)
act.end(() => {
this.detectOver()
}).fail(() => {
this.cameraError()
})
return act
},
// 初始化人脸检测
faceDetect() {
this.show = true
this.actionsList = this.buildActions()
}
}
}
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1110;
opacity: 0;
outline: 0;
text-align: center;
/* -ms-transform: scale(1.185);
transform: scale(1.185); */
backface-visibility: hidden;
perspective: 2000rpx;
background: rgba(0, 0, 0, 0.6);
/* transition: all 0.3s ease-in-out 0s; */
pointer-events: none;
}
.modal::before {
content: "\200B";
display: inline-block;
height: 100%;
vertical-align: middle;
}
.modal.show {
opacity: 1;
overflow-x: hidden;
overflow-y: auto;
pointer-events: auto;
}
.modal.bottom-modal::before {
vertical-align: bottom;
}
.modal.bottom-modal .dialog {
width: 100%;
border-radius: 0;
}
.modal.bottom-modal {
margin-bottom: -1000rpx;
}
.modal.bottom-modal.show {
margin-bottom: 0;
}
.dialog {
position: fixed;
display: inline-block;
vertical-align: middle;
margin-left: auto;
margin-right: auto;
/* width: 680rpx; */
height: 100vh;
width: 100vw;
max-width: 100%;
background-color: #f8f8f8;
border-radius: 10rpx;
overflow: hidden;
left: 0;
top: 0;
}
.bar {
display: flex;
position: relative;
align-items: center;
min-height: 180rpx;
height: 180rpx;
padding: 0rpx 40rpx;
justify-content: space-between;
}
.bg-white {
background-color: #ffffff;
color: #666666;
}
.action {
color: #0081ff;
font-size: 35rpx;
padding: 10rpx;
}
.detectInfo {
padding: 20rpx 0rpx;
font-size: 34rpx;
text-align: center;
animation-duration: 1.5s;
color: #000000;
z-index: 999;
}
.faceContent {
height: 700rpx;
position: relative;
}
.successImage {
overflow: hidden;
width: 600rpx;
height: 600rpx;
border-radius: 50%;
position: absolute;
top: 0;
left: 50%;
z-index: 999;
transform: translateX(-50%);
}
.tipsTextCss {
animation: 1.5s tipsTextAnimation;
animation-duration: 1.5s;
}
@keyframes tipsTextAnimation {
0% {
transform: scale(1);
}
20% {
transform: scale(1.5);
}
70% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
.cover {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.cover-item {
flex: 1;
width: 100%;
background-color: #FFFFFF;
}
.camera {
width: 100vw;
height: 100vw;
transform: scale(1.05);
z-index: 999;
}
</style>

View File

@ -0,0 +1,81 @@
{
"id": "face-bio-assay",
"displayName": "face-bio-assay",
"version": "1.1.1",
"description": "纯前端简易版活体检测,易扩展,开发者可通过自定义动作 添加到该容器中管理。也可使用已有动作(点头、摇头、平视)进行排列组合创建动作组",
"keywords": [
"人脸识别",
"活体检测",
"易扩展"
],
"repository": "",
"engines": {
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,185 @@
# props
|参数名|类型|默认值|可选择值|描述|
|----|----|----|---|---|
|buildActionContainer|Function|null|""|创建ActionContainer类的函数要求返回值必需是ActionContainer类|
|actions|Array|[]|""|动作组(目前已有动作:点头,摇头,平视)|
|isDev|boolen|false|true|是否是开发者模式,开启后可显示人脸的三个角度|
# emits
|方法名|参数|描述|
|----|---|---|
|detectFailed|[]|核验失败|
|photoChange|[url:'照片路径']|拍照后的回调|
|detectOver|[]|检测完成|
|showData|[faceData:'人脸数据']|每一帧的人脸数据|
# slots
|插槽名|参数|描述|
|----|---|---|
|default|无|用户可配合showData钩子展示人脸数据方便调试|
# 方法
## initData
开始进行人脸核验 使用案例:
```
//html
<face-bio-assay ref="faceDetect" ></face-bio-assay>
//js
//调用
this.$refs.faceDetect.initData()
```
## takePhoto
拍照获取照片,配合动作使用:
```
在每个动作创建时第二个参数是动作完成的回调 如平视动作的使用:
const fun = (state) => {
//state 有成功success和failing(进行时不会触发该函数,忽略)
if (state === 'success') {
this.$refs.faceDetect.takePhoto() //调用拍照方法
}
}
let straightenHead = new StraightenHead(10, fun)
```
# 使用建议
```
ios中bug解决方案
在ios中二次进入使用该组件有问题解决办法单独将该组件作为一个页面或者下载demo查看代码如下
//主页面
<template>
<view>
<button type="default" @click="init">人脸检测</button>
<image mode="aspectFit" :src="imgSrc" />
</view>
</template>
<script>
export default {
data() {
return {
imgSrc: '',
}
},
methods: {
init(){
uni.navigateTo({
url:"/pages/face/face",
events:{
data: (path) => {
this.imgSrc = path
}
}
})
}
}
}
</script>
//face.vue页
<template>
<view>
<face-bio-assay :isDev="false" ref="faceDetect" @detectFailed="detectFailed" @photoChange="photoChange">
</face-bio-assay>
</view>
</template>
<script>
import faceBioAssay from '@/uni_modules/face-bio-assay/components/face-bio-assay/face-bio-assay.vue'
export default {
components: {
faceBioAssay,
},
onLoad(option) { //一定要onLoadonShow在进入相机授权页面退回时会再次触发
this.$refs.faceDetect.initData()
},
methods: {
detectFailed() {
uni.showToast({
title: "人脸核验失败~",
icon: 'none'
})
uni.navigateBack()
},
photoChange(path) {
uni.navigateBack()
this.getOpenerEventChannel().emit('data',path);
}
}
}
</script>
```
# 类的使用
## Action
动作类,开发者可继承该类重写 takeFrame 方法 如:
```
//点头动作
import Action from "./Action.js"
class NodHead extends Action {
constructor(second=10,fun) {
//时间限制s结束时回调完成次数(limit),基本提示
super(second,fun,1,'请点头')
this.maxPitch = 0
this.minPitch = 0
}
takeFrame(faceData){
let face = faceData.faceInfo[0]
if(face.angleArray.pitch>this.maxPitch){
this.maxPitch = face.angleArray.pitch
}
if(face.angleArray.pitch<this.minPitch){
this.minPitch = face.angleArray.pitch
}
if(Math.abs(this.minPitch-this.maxPitch)>0.45 && (this.minPitch || this.maxPitch) ){
this.frames.push('点头') //frames 完成的帧数组 根据该数组长度和limit判断是否完成
this.maxPitch = 0
this.minPitch = 0
}
}
}
export default NodHead
```
## ActionContainer
动作容器的使用案例
```
buildActions() {
if (this.buildActionContainer) {
return this.buildActionContainer()
}
let actions = []
if (!this.actions?.length) {
let nodHead = new NodHead()
const fun = (state) => {
if (state === 'success') {
this.takePhoto() //拍照
}
}
let straightenHead = new StraightenHead(10, fun) //平视 结束拍照
let straightenHead2 = new StraightenHead(10) //平视
let shakeHead = new ShakeHead()
actions = [straightenHead2, nodHead, shakeHead, straightenHead]
} else {
actions = this.actions
}
//new ActionContainer(actions,...)
//动作组 actions
//起始动作下标 index
//初始提示 tip
//绑定 完成所有动作的回调 endFun
//绑定 动作进行中 ingFun
//绑定 动作完成时 successFun
//绑定 动作失败时 failFun
let act = new ActionContainer(actions)
act.end(() => { //也可用该方法绑定endFun方法
this.detectOver()
}).fail(() => { //也可用该方法绑定failFun方法
this.cameraError()
})
//....其他方法类似
return act
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB