添加人脸识别
This commit is contained in:
@ -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
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,9 @@
|
||||
import NodHead from "./NodHead.js"
|
||||
import ShakeHead from "./ShakeHead.js"
|
||||
import StraightenHead from "./StraightenHead.js"
|
||||
|
||||
export {
|
||||
NodHead,
|
||||
ShakeHead,
|
||||
StraightenHead,
|
||||
}
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user