【Vue】前端人脸识别框架 tracking.js 活体检测/拍照在 vue2 的使用
Tracking.js 是一个独立的JavaScript库,用于跟踪从相机实时收到的数据。跟踪的数据既可以是颜色,也可以是人,也就是说我们可以通过检测到某特定颜色,或者检测一个人体/脸的出现与移动,来触发JavaScript 事件。它是非常易于使用的API,具有数个方法和事件(足够使用了)。
做项目要用到活体检测和拍照的
实现效果
活体检测组件
包需到下载 tracking-min.js 和 face-min.js 压缩文件,自行百度。
点击查看活体检测组件代码
<template>
<el-dialog
:visible="modalVisible"
width="681px"
custom-class="compaines-dialog"
title="Complete Registration"
class="face-dialog"
v-dialogDrag
:close-on-click-modal="false"
@close="close"
:append-to-body="true"
>
<div class="face" v-loading="faceloading">
<p class="big-title">Liveness detection</p>
<!-- <p v-if="steps === 'open' || steps === 'screen'">
Please place your face into the outlined area,we will take the photo
automatically!
</p> -->
<p v-if="steps === 'open' || steps === 'screen'">
{{ faceTips[imgList.length] }}
</p>
<div
v-if="steps === 'screen' && imgList.length > 0"
style="text-align: center; font-size: 30px"
>
<svg-icon :iconClass="faceLocation[imgList.length]" />
</div>
<p v-if="steps === 'end'">
<span v-if="faceOk" class="success">Successful!</span>
<span v-else class="fail">Failed!</span>
</p>
<div class="video-container" v-show="steps !== 'end'">
<video
id="video"
preload
autoplay
loop
muted
width="295"
height="345"
:style="reverse ? 'transform:rotateY(180deg);' : ''"
></video>
<canvas id="canvas" width="295" height="345"></canvas>
<canvas
id="shortCut"
width="295"
height="345"
style="opacity: 0"
></canvas>
<canvas
id="canvas1"
width="295"
height="345"
style="opacity: 0"
></canvas>
<i @click="reverseVideo" v-show="steps === 'screen'">
<svg-icon iconClass="icon-camera" class="icon-camera" />
</i>
</div>
<div v-show="steps === 'end'" style="text-align: center">
<img v-if="faceOk" src="@/icons/img/face-success.png" alt="" />
<img v-else src="@/icons/img/face-fail.png" alt="" />
</div>
<div class="btns">
<div class="add-num w300" @click="start" v-if="steps === 'open'">
TURN CAMERA ON
</div>
<div
class="retake-btn w300"
style="margin-right: 0"
@click="close"
v-if="steps === 'screen'"
>
TURN CAMERA OFF
</div>
<div
class="retake-btn w300"
@click="tryAgain"
v-if="steps === 'end' && !faceOk"
>
TRY AGAIN
</div>
<div
class="add-num w300"
@click="finish"
v-if="steps === 'end' && faceOk"
>
FINISH
</div>
<p class="tips" v-if="steps === 'screen'">
(To complete the liveness detection your camera must remain on)
</p>
</div>
<div class="imgs" v-show="false">
<p>未保存图片</p>
<p>已保存图片</p>
<div id="img"></div>
</div>
</div>
</el-dialog>
</template>
<script>
import './tracking-min.js'
import './face-min.js'
import { livenessCheck } from '@modules/kyc/api/system/system'
import debounce from 'lodash/debounce'
export default {
name: 'testTracking',
model: {
prop: 'formData',
event: 'input',
},
props: {
modalVisible: {
type: Boolean,
default: false,
},
formData: {
type: Object | String,
},
option: {
type: String,
default: '',
},
},
data() {
return {
saveArray: {},
imgView: false,
timer: true,
steps: 'open',
imgList: [],
faceTips: [
'Please place your face into the outlined area,we will take the photo automatically!',
'Look to the left',
'Look to the right',
'Tilt your head up',
'Tilt your head down',
],
faceLocation: [
'',
'arrow-left',
'arrow-right',
'arrow-up',
'arrow-down',
'',
],
faceloading: false,
faceOk: false,
startPhoto: false,
reverse: true,
}
},
created() {
this.start = debounce(this.start, 500)
},
methods: {
// 打开摄像头
start() {
window.navigator.mediaDevices
.getUserMedia({
video: true,
})
.then((stream) => {
this.openVideo()
})
.catch((err) => {
this.$message.error('Cannot capture user camera')
})
},
openVideo() {
let that = this
this.steps = 'screen'
this.startPhoto = true
let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d')
let tracker = new tracking.ObjectTracker('face')
tracker.setInitialScale(4)
tracker.setStepSize(2)
tracker.setEdgesDensity(0.1)
this.trackerTask = tracking.track('#video', tracker, { camera: true })
tracker.on('track', function (event) {
context.clearRect(0, 0, canvas.width, canvas.height)
event.data.forEach(function (rect) {
if (that.reverse) {
context.strokeRect(
295 - rect.x - rect.width,
rect.y,
rect.width,
rect.height
)
} else {
context.strokeRect(rect.x, rect.y, rect.width, rect.height)
}
context.strokeStyle = '#fff'
context.fillStyle = '#fff'
that.saveArray.x = rect.x
that.saveArray.y = rect.y
that.saveArray.width = rect.width
that.saveArray.height = rect.height
})
})
this.timer = true
this.setPhotoInterval()
},
setPhotoInterval() {
const countFun = () => {
setTimeout(() => {
if (this.timer && this.startPhoto) {
countFun()
if (this.reverse) {
if (
this.saveArray.x < 150 &&
this.saveArray.y < 150 &&
this.saveArray.width > 150 &&
this.saveArray.height > 150
) {
this.getPhoto()
}
} else {
if (
295 - this.saveArray.x - this.saveArray.width < 150 &&
this.saveArray.y < 150 &&
this.saveArray.width > 150 &&
this.saveArray.height > 150
) {
this.getPhoto()
}
}
}
}, 1000)
}
countFun()
},
// 获取人像照片
getPhoto() {
try {
let video = document.getElementById('video')
let cut = document.getElementById('shortCut')
let context2 = cut.getContext('2d')
context2.drawImage(video, 0, 0, 295, 345)
this.keepImg()
} catch (error) {}
},
// 将canvas转化为图片
convertCanvasToImage(canvas) {
let image = new Image()
image.src = canvas.toDataURL('image/png')
return image
},
//将base64转换为文件,dataurl为base64字符串,filename为文件名(必须带后缀名,如.jpg,.png)
dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, { type: mime })
},
// 保存图片
keepImg() {
//先保存完整的截图
let cut = document.getElementById('shortCut')
let context = cut.getContext('2d')
//从完整截图里面,截取左侧294x345大小的图片,添加到canvas1里面
let imgData = context.getImageData(0, 0, 294, 345)
let canvas1 = document.getElementById('canvas1')
let context1 = canvas1.getContext('2d')
context1.putImageData(imgData, 0, 0)
let img = document.getElementById('img')
//把canvas1里面的294x345大小的图片保存
let photoImg = document.createElement('img')
photoImg.src = this.convertCanvasToImage(canvas1).src
img.appendChild(photoImg)
this.imgList.push(
this.dataURLtoFile(
this.convertCanvasToImage(canvas1).src,
`person${this.imgList.length}.jpg`
)
)
this.timer = false
//捕捉成功后停顿3秒,再捕捉下一张,捕捉5张后上传文件
if (this.imgList.length === 5) {
this.sendImages()
} else {
setTimeout(() => {
this.timer = true
this.setPhotoInterval()
}, 3000)
}
},
sendImages() {
let formData = new FormData()
this.imgList.forEach((item) => {
formData.append('files', item)
})
this.faceloading = true
formData.append('actionId', this.formData.id)
livenessCheck(formData, { isUpload: true })
.then((res) => {
if (res.success) {
this.faceOk = true
} else {
this.faceOk = false
}
this.faceloading = false
this.steps = 'end'
})
.catch((err) => {
this.openVideo()
this.faceloading = false
this.imgList = []
})
},
tryAgain() {
this.imgList = []
this.openVideo()
},
finish() {
this.closeFace()
this.$emit('Liveness check success')
this.$emit('close', 'success')
},
clearCanvas() {
let c = document.getElementById('canvas')
let c1 = document.getElementById('canvas1')
let cxt = c.getContext('2d')
let cxt1 = c1.getContext('2d')
cxt.clearRect(0, 0, 581, 436)
cxt1.clearRect(0, 0, 581, 436)
},
closeFace() {
try {
this.startPhoto = false
this.timer = false
this.imgList = []
this.clearCanvas()
// 关闭摄像头
let video = document.getElementById('video')
video.srcObject.getTracks()[0].stop()
// 停止侦测
this.trackerTask.stop()
} catch (error) {}
},
close() {
this.closeFace()
if(this.faceOk){
this.$emit('close', 'success')
}else{
this.$emit('close')
}
},
reverseVideo() {
this.reverse = !this.reverse
},
},
watch: {},
}
</script>
<style lang="scss">
@import './face.scss';
</style>
拍照组件代码
点击查看拍照组件代码
<template>
<el-dialog
:visible="modalVisible"
width="681px"
custom-class="compaines-dialog"
title="Complete Registration"
class="face-dialog"
@close="close"
>
<div class="face" v-loading="faceloading">
<p class="big-title" v-if="steps !== 'save'">Take a profile picture</p>
<p class="big-title success" v-if="steps === 'save'">Complete!</p>
<p v-if="steps === 'open'">
Please turn on your camera and center your face within the below guides.
</p>
<p v-if="steps === 'take'">Position your face within the outlined area,click the button and the image will be saved as your profile picture!</p>
<p v-if="steps === 'save'">
Happy with your picture? If not, you can take another one.
</p>
<div class="video-container">
<video
id="video"
preload
autoplay
loop
muted
width="295"
height="345"
:style="reverse ? 'transform:rotateY(180deg);' : ''"
></video>
<canvas id="canvas" width="295" height="345"></canvas>
<canvas id="shortCut" v-show="false"></canvas>
<img :src="imgSrc" alt="" v-show="steps === 'save'" />
<i @click="reverseVideo" v-show="steps==='take'">
<svg-icon iconClass="icon-camera" class="icon-camera" />
</i>
</div>
<div class="btns">
<div class="add-num w300" @click="start" v-if="steps === 'open'">
TURN CAMERA ON
</div>
<div class="add-num w300" @click="getPhoto" v-if="steps === 'take'">
SAVE THE IMAGE
</div>
<div class="retake-btn" @click="openVideo" v-if="steps === 'save'">
RETAKE
</div>
<div class="add-num" @click="keepImg" v-if="steps === 'save'">
USE PICTURE
</div>
<p class="tips" v-if="steps === 'take'">
(To take a profile picture your camera must remain on)
</p>
<div class="imgs" v-show="false">
<canvas
id="canvas1"
width="295"
height="345"
v-show="steps === 'save'"
></canvas>
<p>save images</p>
<div id="img"></div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import './tracking-min.js'
import './face-min.js'
import { individualPhoto } from '@modules/kyc/api/system/system'
import debounce from 'lodash/debounce'
export default {
name: 'testTracking',
model: {
prop: 'formData',
event: 'input',
},
props: {
modalVisible: {
type: Boolean,
default: false,
},
formData: {
type: Object,
},
option: {
type: String,
default: '',
},
},
data() {
return {
saveArray: {},
imgView: false,
timer: null,
steps: 'open',
imageFile: {},
faceloading: false,
btnLoading: false,
imgSrc: '',
reverse: true,
}
},
created() {
this.start = debounce(this.start, 500)
},
methods: {
// 打开摄像头
start() {
window.navigator.mediaDevices
.getUserMedia({
video: true,
})
.then((stream) => {
this.openVideo()
})
.catch((err) => {
this.$message.error('Cannot capture user camera')
})
},
openVideo() {
let that = this
this.steps = 'take'
let saveArray = {}
let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d')
let tracker = new tracking.ObjectTracker('face')
tracker.setInitialScale(4)
tracker.setStepSize(1.5)
tracker.setEdgesDensity(0.1)
this.imgSrc = ''
this.trackerTask = tracking.track('#video', tracker, { camera: true })
tracker.on('track', function (event) {
context.clearRect(0, 0, canvas.width, canvas.height)
event.data.forEach(function (rect) {
if (that.reverse) {
context.strokeRect(
295 - rect.x - rect.width,
rect.y,
rect.width,
rect.height
)
} else {
context.strokeRect(rect.x, rect.y, rect.width, rect.height)
}
context.strokeStyle = '#fff'
context.fillStyle = '#fff'
saveArray.x = rect.x
saveArray.y = rect.y
saveArray.width = rect.width
saveArray.height = rect.height
})
})
},
// 获取人像照片
getPhoto() {
let video = document.getElementById('video')
let can = document.getElementById('shortCut')
can.width = video.videoWidth
can.height = video.videoHeight
let context2 = can.getContext('2d')
if (this.reverse) {
context2.scale(-1, 1)
context2.translate(-video.videoWidth, 0)
}
context2.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
this.imgSrc = this.convertCanvasToImage(can).src
this.steps = 'save'
this.clearCanvas()
// 停止侦测
this.trackerTask.stop()
// 关闭摄像头
video.srcObject.getTracks()[0].stop()
// this.imgView = true
},
// 截屏
screenshot() {
this.getPhoto()
},
// 将canvas转化为图片
convertCanvasToImage(canvas) {
let image = new Image()
image.src = canvas.toDataURL('image/png')
return image
},
//将base64转换为文件,dataurl为base64字符串,filename为文件名(必须带后缀名,如.jpg,.png)
dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], filename, { type: mime })
},
// 保存图片
keepImg() {
// let can = document.getElementById('shortCut')
// let context = can.getContext('2d')
// let imgData = context.getImageData(0, 0, 294, 345)
// let canvas1 = document.getElementById('canvas1')
// let context1 = canvas1.getContext('2d')
// context1.putImageData(imgData, 0, 0)
// let img = document.getElementById('img')
// let photoImg = document.createElement('img')
// photoImg.src = this.convertCanvasToImage(canvas1).src
// img.appendChild(photoImg)
// this.imageFile = this.dataURLtoFile(
// this.convertCanvasToImage(canvas1).src,
// 'person.jpg'
// )
this.imageFile = this.dataURLtoFile(this.imgSrc, 'person.jpg')
let formData = new FormData()
formData.append('file', this.imageFile)
this.faceloading = true
individualPhoto(formData, { isUpload: true })
.then((res) => {
if (res.success) {
this.faceloading = false
this.closeFace()
this.$emit('close', 'success')
this.$store
.dispatch('GetBusinessUser')
.then((res) => {})
.catch((err) => {})
} else {
this.openVideo()
this.faceloading = false
this.imgList = []
}
})
.catch((err) => {
this.openVideo()
this.faceloading = false
this.imgList = []
})
},
clearCanvas() {
let c = document.getElementById('canvas')
let c1 = document.getElementById('canvas1')
let cxt = c.getContext('2d')
let cxt1 = c1.getContext('2d')
cxt.clearRect(0, 0, 581, 436)
cxt1.clearRect(0, 0, 581, 436)
},
closeFace() {
try {
this.steps = 'open'
this.clearCanvas()
// 关闭摄像头
let video = document.getElementById('video')
video.srcObject.getTracks()[0].stop()
// 停止侦测
this.trackerTask.stop()
} catch (error) {}
},
close() {
this.closeFace()
this.$emit('close')
},
reverseVideo() {
this.reverse = !this.reverse
},
},
watch: {
faceView(v) {
if (v == false) {
this.closeFace()
}
},
},
destroyed() {
// clearInterval(this.timer)
},
}
</script>
<style lang="scss">
@import './face.scss';
</style>
样式文件
点击查看样式代码
.face {
p {
text-align: center;
font-size: 16px;
color: #333333;
}
.big-title {
margin-top: 36PX;
font-size: 24PX;
font-weight: 600;
color: #030229;
text-align: center;
}
.success {
color: #47BFAF;
}
.fail {
color: #C81223;
}
.video-container {
background: url(~@/icons/img/face.png);
background-size: cover;
position: relative;
width: 295PX;
height: 345PX;
border-radius: 4%;
overflow: hidden;
margin: 0 auto;
margin-top: 34PX;
img,
video,
#canvas,
#shortCut,
#canvas1 {
position: absolute;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
video{
object-fit: cover;
}
.icon-camera{
position: absolute;
font-size: 32px;
bottom: 20px;
right: 20px;
cursor: pointer;
}
}
.btns {
padding: 10PX;
text-align: center;
margin-top: 6PX;
.tips {
font-size: 14PX;
color: #666;
margin-top: 16PX;
line-height: 24PX;
}
}
.imgs {
padding: 10PX;
p {
font-size: 16PX;
}
}
.add-num {
display: inline-block;
font-size: 16PX;
padding: 12PX 14PX;
margin-right: 4PX;
border-radius: 4PX;
color: #ffffff;
background: #47BFAF;
cursor: pointer;
}
.retake-btn {
width: 145PX;
display: inline-block;
font-size: 16PX;
padding: 12PX 14PX;
margin-right: 24PX;
border-radius: 4PX;
color: #828282;
background: #E7E7E7;
cursor: pointer;
}
.w300 {
width: 300PX;
}
}
.face-dialog {
.el-dialog__body {
padding-top: 0;
padding-bottom: 15px;
}
}