添加人脸识别

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,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>