|
@@ -0,0 +1,271 @@
|
|
|
+<template>
|
|
|
+ <div class="webrtc_face_recognition">
|
|
|
+ <div class="option">
|
|
|
+ <div>
|
|
|
+ <label>面板操作:</label>
|
|
|
+ <button @click="fnOpen">启动摄像头视频媒体</button>
|
|
|
+ <button @click="fnClose">结束摄像头视频媒体</button>
|
|
|
+ 相识度:{{ distance }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="see">
|
|
|
+ <video id="myVideo" muted loop playsinline @loadedmetadata="fnRun"></video>
|
|
|
+ <canvas id="myCanvas" />
|
|
|
+ <div id="catch"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as faceapi from 'face-api.js';
|
|
|
+export default {
|
|
|
+ name: 'WebRTCFaceRecognition',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ nets: 'tinyFaceDetector', // 模型
|
|
|
+ options: null, // 模型参数
|
|
|
+ withBoxes: true, // 框or轮廓
|
|
|
+ detectFace: 'detectSingleFace', // 单or多人脸
|
|
|
+ detection: 'landmark',
|
|
|
+ videoEl: null,
|
|
|
+ canvasEl: null,
|
|
|
+ timeout: 0,
|
|
|
+ // 视频媒体参数配置
|
|
|
+ constraints: {
|
|
|
+ audio: false,
|
|
|
+ video: {
|
|
|
+ // ideal(应用最理想的)
|
|
|
+ width: {
|
|
|
+ min: 320,
|
|
|
+ ideal: 1280,
|
|
|
+ max: 1920,
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ min: 240,
|
|
|
+ ideal: 720,
|
|
|
+ max: 1080,
|
|
|
+ },
|
|
|
+ // frameRate受限带宽传输时,低帧率可能更适宜
|
|
|
+ frameRate: {
|
|
|
+ min: 15,
|
|
|
+ ideal: 30,
|
|
|
+ max: 60,
|
|
|
+ },
|
|
|
+ // 显示模式前置后置
|
|
|
+ facingMode: 'environment',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ faceList: [],
|
|
|
+ distance: 0,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.fnInit();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 初始化模型加载
|
|
|
+ async fnInit() {
|
|
|
+ faceapi.env.monkeyPatch({
|
|
|
+ Canvas: HTMLCanvasElement,
|
|
|
+ Image: HTMLImageElement,
|
|
|
+ ImageData: ImageData,
|
|
|
+ Video: HTMLVideoElement,
|
|
|
+ createCanvasElement: () => document.createElement('canvas'),
|
|
|
+ createImageElement: () => document.createElement('img'),
|
|
|
+ });
|
|
|
+ await faceapi.nets[this.nets].loadFromUri('/models'); // 算法模型
|
|
|
+ await faceapi.loadFaceLandmarkModel('/models'); // 轮廓模型
|
|
|
+ await faceapi.loadFaceRecognitionModel('/models'); // 人脸对比
|
|
|
+ // 根据算法模型参数识别调整结果
|
|
|
+ switch (this.nets) {
|
|
|
+ case 'ssdMobilenetv1':
|
|
|
+ this.options = new faceapi.SsdMobilenetv1Options({
|
|
|
+ minConfidence: 0.5, // 0.1 ~ 0.9
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ case 'tinyFaceDetector':
|
|
|
+ this.options = new faceapi.TinyFaceDetectorOptions({
|
|
|
+ inputSize: 512, // 160 224 320 416 512 608
|
|
|
+ scoreThreshold: 0.5, // 0.1 ~ 0.9
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ case 'mtcnn':
|
|
|
+ this.options = new faceapi.MtcnnOptions({
|
|
|
+ minFaceSize: 20, // 0.1 ~ 0.9
|
|
|
+ scaleFactor: 0.709, // 0.1 ~ 0.9
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 节点属性化
|
|
|
+ this.videoEl = document.getElementById('myVideo');
|
|
|
+ this.canvasEl = document.getElementById('myCanvas');
|
|
|
+ },
|
|
|
+ // 人脸面部勘探轮廓识别绘制
|
|
|
+ async fnRunFaceLandmark() {
|
|
|
+ if (this.videoEl.paused) return clearTimeout(this.timeout);
|
|
|
+ // 识别绘制人脸信息
|
|
|
+ const result = await faceapi[this.detectFace](this.videoEl, this.options).withFaceLandmarks();
|
|
|
+ if (result && !this.videoEl.paused) {
|
|
|
+ const dims = faceapi.matchDimensions(this.canvasEl, this.videoEl, true);
|
|
|
+ const resizeResult = faceapi.resizeResults(result, dims);
|
|
|
+ this.withBoxes ? faceapi.draw.drawDetections(this.canvasEl, resizeResult) : faceapi.draw.drawFaceLandmarks(this.canvasEl, resizeResult);
|
|
|
+ this.takePhoto();
|
|
|
+ } else {
|
|
|
+ this.canvasEl.getContext('2d').clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
|
+ }
|
|
|
+ this.timeout = setTimeout(() => this.fnRunFaceLandmark());
|
|
|
+ },
|
|
|
+ async takePhoto() {
|
|
|
+ //获得Canvas对象
|
|
|
+ let canvas = document.createElement('canvas');
|
|
|
+ let ctx = canvas.getContext('2d');
|
|
|
+ let video = this.videoEl;
|
|
|
+ ctx.drawImage(video, 0, 0, 400, 400);
|
|
|
+ let imgsrc = canvas.toDataURL(this.picType);
|
|
|
+ const res = this.translateBase64ImgToFile(imgsrc, `temp-${new Date().getTime()}`, this.picType);
|
|
|
+ let afterDeal = await faceapi.bufferToImage(res);
|
|
|
+ const face = await this.getFace(afterDeal);
|
|
|
+ // const face = canvas;
|
|
|
+ if (this.faceList.length === 2 && face) {
|
|
|
+ this.faceList = [];
|
|
|
+ // document.getElementById('catch').innerHTML = '';
|
|
|
+ }
|
|
|
+ if (face) this.faceList.push(face);
|
|
|
+ this.getResult();
|
|
|
+ // this.closeVideo();
|
|
|
+ },
|
|
|
+ /** 提取人脸 */
|
|
|
+ async getFace(c) {
|
|
|
+ let canvasBoxEl = document.getElementById('catch');
|
|
|
+ // this.canvasEl.appendChild(c);
|
|
|
+ const detections = await faceapi.detectAllFaces(c, this.options);
|
|
|
+ console.log(detections);
|
|
|
+ const faceImages = await faceapi.extractFaces(c, detections);
|
|
|
+ console.log(faceImages);
|
|
|
+ if (detections.length <= 0 || faceImages.length <= 0) return;
|
|
|
+ faceImages.forEach((canvas) => canvasBoxEl.appendChild(canvas));
|
|
|
+ canvasBoxEl.appendChild(document.createElement('HR'));
|
|
|
+ return _.head(faceImages);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 识别度
|
|
|
+ */
|
|
|
+ async getResult() {
|
|
|
+ if (this.faceList.length !== 2) return;
|
|
|
+ const list = [await faceapi.computeFaceDescriptor(this.faceList[0]), await faceapi.computeFaceDescriptor(this.faceList[1])];
|
|
|
+ // 图片误差值,越小越精确
|
|
|
+ this.distance = faceapi.euclideanDistance(list[0], list[1]).toFixed(2);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Base64转File
|
|
|
+ * @param base64 String base64格式字符串
|
|
|
+ * @param contentType String file对象的文件类型,如:"image/png"
|
|
|
+ * @param filename String 文件名称或者文件路径
|
|
|
+ */
|
|
|
+ translateBase64ImgToFile(base64, filename, contentType) {
|
|
|
+ let arr = base64.split(','); //去掉base64格式图片的头部
|
|
|
+ let bstr = atob(arr[1]); //atob()方法将数据解码
|
|
|
+ let leng = bstr.length;
|
|
|
+ let u8arr = new Uint8Array(leng);
|
|
|
+ while (leng--) {
|
|
|
+ u8arr[leng] = bstr.charCodeAt(leng); //返回指定位置的字符的 Unicode 编码
|
|
|
+ }
|
|
|
+ return new File([u8arr], filename, { type: contentType });
|
|
|
+ },
|
|
|
+ // 执行检测识别类型
|
|
|
+ fnRun() {
|
|
|
+ if (this.detection === 'landmark') {
|
|
|
+ this.fnRunFaceLandmark();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.detection === 'expression') {
|
|
|
+ this.$message.error('未加载该模块');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.detection === 'age_gender') {
|
|
|
+ this.$message.error('未加载该模块');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 启动摄像头视频媒体
|
|
|
+ fnOpen() {
|
|
|
+ if (typeof window.stream === 'object') return;
|
|
|
+ clearTimeout(this.timeout);
|
|
|
+ this.timeout = setTimeout(() => {
|
|
|
+ clearTimeout(this.timeout);
|
|
|
+ navigator.mediaDevices.getUserMedia(this.constraints).then(this.fnSuccess).catch(this.fnError);
|
|
|
+ }, 300);
|
|
|
+ },
|
|
|
+ // 成功启动视频媒体流
|
|
|
+ fnSuccess(stream) {
|
|
|
+ window.stream = stream; // 使流对浏览器控制台可用
|
|
|
+ this.videoEl.srcObject = stream;
|
|
|
+ this.videoEl.play();
|
|
|
+ },
|
|
|
+ // 失败启动视频媒体流
|
|
|
+ fnError(error) {
|
|
|
+ console.log(error);
|
|
|
+ alert('视频媒体流获取错误' + error);
|
|
|
+ },
|
|
|
+ // 结束摄像头视频媒体
|
|
|
+ fnClose() {
|
|
|
+ this.canvasEl.getContext('2d').clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
|
+ this.videoEl.pause();
|
|
|
+ clearTimeout(this.timeout);
|
|
|
+ if (typeof window.stream === 'object') {
|
|
|
+ window.stream.getTracks().forEach((track) => track.stop());
|
|
|
+ window.stream = '';
|
|
|
+ this.videoEl.srcObject = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.fnClose();
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ nets(val) {
|
|
|
+ this.nets = val;
|
|
|
+ this.fnInit();
|
|
|
+ },
|
|
|
+ detection(val) {
|
|
|
+ this.detection = val;
|
|
|
+ this.videoEl.pause();
|
|
|
+ setTimeout(() => {
|
|
|
+ this.videoEl.play();
|
|
|
+ setTimeout(() => this.fnRun(), 300);
|
|
|
+ }, 300);
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+button {
|
|
|
+ height: 30px;
|
|
|
+ border: 2px #42b983 solid;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #42b983;
|
|
|
+ color: white;
|
|
|
+ margin: 10px;
|
|
|
+}
|
|
|
+.see {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.see canvas {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+}
|
|
|
+.option {
|
|
|
+ padding-bottom: 20px;
|
|
|
+}
|
|
|
+.option div {
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 2px #42b983 solid;
|
|
|
+}
|
|
|
+.option div label {
|
|
|
+ margin-right: 20px;
|
|
|
+}
|
|
|
+</style>
|