前端实现人脸识别
引言:
灵活的调用硬件配置是原生开发的长项,在我接触到与人脸识别相关的项目的时候,第一时间想到的就是hybrid形式借助原生app或是第三方平台客户端API去实现,最近的项目便是借助原生app混合开发来完成人脸识别。借助该形式实现人脸识别的总体流程是:app端开启摄像头实时捕获人脸图像,面部识别追踪通过第三方的sdk实现,再识别到面部图像后将其传至后端接口进行比对,比对结果通过jsbridge通信通知前端应用。实际开发期间,需要前端去沟通后端、安卓端、或是ios端。
那么或许可以完全由前端来实现人脸识别,实现起来其实只需要能够调起摄像头实时捕获人脸面部图像,再调用后端接口,毕竟图像比对的工作交由后端来实现了,前端只需要负责采集,那么其复杂程度就降低了很多。
前置需要了解什么?:
WebRTC:
那么想要实现实时的图像采集,可以想到必须要获取一个实时的可控视频流,而不是简单的调用相机,那么这里就需要提到WebRTC API了,MDN对于WebRTC的介绍是这样的:
WebRTC(Web Real-Time Communications)是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能,WebRTC可在不同程度上在Chrome,Firefox,Safari,Edge,Android和iOS中本地使用,并且是一种广泛流行的视频通话工具。
WebRTC API的3个主要组件:
API名称 | 介绍 |
---|---|
MediaStream(GetUserMedia) | MediaStream API提供了一种使用JavaScript访问设备摄像头和麦克风的方法。 它控制在哪里消费多媒体流数据,并提供对产生媒体的设备的一些控制。 它还公开了有关能够捕获和呈现媒体的设备的信息。 |
RTCPeerConnection | 对等连接是WebRTC标准的核心。 它为参与者提供了一种与对等方建立直接连接的方式,而无需中间服务器(除了信令之外)。 每个参与者都将从媒体流API中获取的媒体插入到对等连接中以创建音频或视频提要。 PeerConnection API在幕后发生了很多事情。 它处理SDP协商,编解码器实现,NAT遍历,数据包丢失,带宽管理和媒体传输。 |
RTCDataChannel | 设置了RTCDataChannel API,以允许直接在同级之间进行任何类型的数据(媒体或其他类型)的双向数据传输。它被设计为模仿WebSocket API,而不是依赖于TCP连接,尽管该TCP连接虽然可靠性高,延迟高并且容易出现瓶颈,但它使用基于UDP的流以及具有流控制传输协议(SCTP)协议的可配置性。 这种设计兼顾了两全其美:像TCP一样可靠的传送,但像UDP一样减少了网络的拥塞。 |
每个组件在WebRTC规范中都扮演着独特的角色,第2和第3个API这里先仅作了解,因为实现图像采集目前只需要使用第一个API访问摄像头来捕获媒体流。
GetUserMedia:
要使用GetUserMedia这个API那必须要使用mediaDevices这个关键属性,它是Navigator的只读属性,返回一个 MediaDevices
单例对象,该对象可提供对相机和麦克风等媒体输入设备的连接访问,也包括屏幕共享。GetUserMedia是MediaDevices的对象成员,调用它来获取MediaStream(媒体流)。
let mediaDevices = navigator.mediaDevices;
// MediaStreamConstraints对象,指定了请求的媒体类型和相对应的参数,也就是一个getUserMedia的option,包含了video和audio两个可配置属性
let constraints = {
audio: true,
video: true
}
mediaDevices
.getUserMedia(constraints)
.then(function (stream) {
/* 使用这个 MediaStream */
})
.catch(function (err) {
/* 处理 error */
});
MediaDevices.getUserMedia() - Web API 接口参考 | MDN (mozilla.org)
GetUserMedia调试注意说明:
通过 MediaDevices.getUserMedia() 获取用户多媒体权限时,其只能工作于以下三种环境:
localhost
域- 开启了 HTTPS 的域
- 使用
file:///
协议打开的本地文件
其他情况下,比如在一个 HTTP
站点上,navigator.mediaDevices
的值为 undefined
。
如果想要 HTTP
环境下也能使用和调试 MediaDevices.getUserMedia()
,可通过开启 Chrome 的相应参数。
1. 通过相应参数启动 Chrome
传递相应参数来启动 Chrome,以 http://example.com
为例,
--unsafely-treat-insecure-origin-as-secure="http://example.com"
open -n /Applications/Google\ Chrome.app/ --args --unsafely-treat-insecure-origin-as-secure="http://example.com"
2. 开启相应 flag
通过传递相应参数来启动 Chrome Insecure origins treated as secure
flag 并填入相应白名单。
- 打开
chrome://flags/#unsafely-treat-insecure-origin-as-secure
- 将该 flag 切换成
enable
状态 - 输入框中填写需要开启的域名,譬如
http://example.com"
,多个以逗号分隔。 - 重启后生效。
使用getUserMedia:
使用getUserMedia获取mediaStream,接着获取video元素,将获取的mediaStream设定到video元素,这时,流程和代码都没有问题的情况,可以看到页面上video正常显示摄像头捕获到的画面,画面是左右镜像的,可以使用css将video镜像翻转一下。
<video src="#"></video>
const constraints = { audio: false, video: { width: 360, height: 360 } };
//以下是此方法的兼容性写法
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) {
var getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia ||
navigator.oGetUserMedia ||
MediaDevices;
if (!getUserMedia) {
return Promise.reject(
new Error("getUserMedia is not implemented in this browser")
);
}
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
// 获取video元素,将视频流设定到video元素
navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => {
let video = document.querySelector("video");
if (video) {
if ("srcObject" in video) {
// 旧的浏览器可能没有 srcObject
// HTMLMediaElement 接口的 srcObject 属性设定或返回一个对象
// 这个对象提供了一个与HTMLMediaElement关联的媒体源,这个对象通常是 MediaStream
// 也可以是 MediaSource, Blob 或者 File
video.srcObject = mediaStream;
} else {
// 防止在新的浏览器里使用它,应为它已经不再支持了
// URL.createObjectURL() 静态方法会创建一个 DOMString
// 其中包含一个表示参数中给出的对象的 URL
// 这个 URL 的生命周期和创建它的窗口中的 document 绑定
// 这个新的 URL 对象表示指定的 File 对象或 Blob 对象。
// 这里是创建一个关于 MediaStream 的对象 URL
video.src = window.URL.createObjectURL(mediaStream);
}
video.onloadedmetadata = (e) => {
video.play();
};
}
}).catch(() => {
})
接下来使用canvas来尝试捕捉视频帧画面,用于上传。
let video = document.querySelector("video");
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 获取canvas的二维渲染上下文,设置左右镜像翻转,再绘图
ctx.translate(video.videoWidth, 0);
ctx.scale(-1, 1);
ctx.drawImage(video, 0, 0, video?.videoWidth, video?.videoHeight);
// 将canvas转为dataUrl,设定到img的src属性,即可展示图像
// 这里的dataURL是一个base64字符串
// 如果上传,那么需要借助base64转file、base64转blob方法来上传
let img = canvas.toDataURL('image/jpeg', 0.7)
// 或者直接将canvas转为blob,toBlob是异步方法,需要使用promise包裹
// let img = null
// canvas.toBlob((blob) => {
// img = blob
// }, 'image/jpeg', 0.7)
// base64转file
function base64ImgtoFile(dataurl, filename = 'file') {
let arr = dataurl.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let suffix = mime.split('/')[1];
let bstr = atob(arr[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], `${filename}.${suffix}`, {
type: mime
})
}
// base64转blob
function dataURLtoBlob(url) {
let arr = url.split(',');
let mime = arr[0].match(/:(.*?);/)[1];
let bstr = atob(arr[1]);
let i = bstr.length;
let u8arr = new Uint8Array(n);
while(i--) {
u8arr[i] = bstr.charCodeAt(i);
}
return new Blob([u8arr], {type:mime});
}
到目前为止,基本实现了图像实时上传后端的功能。
面部跟踪识别:
接下来,要实现人脸跟踪识别的功能,需要借助第三方库tracking.js,tracking.js (trackingjs.com)。实际上,检测物体如人脸,是相当复杂的有许多因素需要考虑。光照条件、角度、脸部的装饰品,但是这里仅实现人脸识别并捕获人脸图像,精度要求也并不是很高,所以整体也可以接收。
那么接下来下载tracking.js,在项目中导入。
使用一个宽高等同于video的canvas元素,用于绘制人脸识别框
<div style="width:180px;height:180px;">
<video id="video" />
<canvas id="canvas" width="180" height="180" />
</div>
<style scoped>
video {
transform: rotateY(180deg);
height: 180px;
width: 180px;
}
canvas {
position: absolute;
left: 0;
top: 0;
transform: rotateY(180deg);
}
</style>
import '../../public/trackingjs/tracking.js'; // 导入后会在window对象挂载一个tracking成员属性
import '../../public/trackingjs/face.js'; // 经过训练的面部识别模型数据
import { onMounted, ref, reactive } from 'vue';
const trackingRect = ref<any>({})
let trackerTask = ref<any>(null)
onMounted(() => {
let canvas: any = document.getElementById('canvas');
let context = canvas.getContext('2d');
let tracker = new window.tracking.ObjectTracker('face');
trackerTask.value = window.tracking.track('#video', tracker);
tracker.on('track', (event: any) => {
if (event.data.length === 0) {
// No objects were detected in this frame.
} else {
context.clearRect(0, 0, canvas.width, canvas.height);
event.data.forEach((rect: any) => {
// 这里获取到的是识别到的人脸在视频中x、y位置参数和边界参数
// rect.x, rect.y, rect.height, rect.width
// 这里在trackingRect保存这些识别信息
trackingRect.value = rect
context.strokeStyle = '#a64ceb';
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
context.font = '11px Helvetica';
context.fillStyle = "#fff";
context.fillText('x: ' + rect.x + 'px', rect.x + rect.width + 5, rect.y + 11);
context.fillText('y: ' + rect.y + 'px', rect.x + rect.width + 5, rect.y + 22);
});
}
});
})
// 获取视频帧画面,这里借助人脸识别库获取的面部识别位置边界参数,可以将视频帧画面中只裁剪出面部图像
// 如果后续需要实时定时获取面部图像,可以判断摄像头有面部识别信息时才进行图像捕获和上传
const getFaceImage = () => {
let video = document.querySelector("video");
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
const rect = trackingRect.value
const compress = 1
const expandW = 10 // 将宽度扩大20px
const expandH = 20 // 将高度扩大40px
canvas.width = (rect.width + expandW*2) / compress;
canvas.height = (rect.height + expandH*2) / compress;
// 左右镜像翻转
ctx.translate((rect.width + expandW*2) / compress, 0);
ctx.scale(-1, 1);
ctx.drawImage(
video,
rect.x-expandW, rect.y-expandH,
rect.width+expandW*2, rect.height+expandH*2,
0, 0,
canvas.width, canvas.height
);
return new Promise<void>((resolve, reject) => {
// canvas.toBlob是异步的
canvas.toBlob((blob) => {
resolve(blob)
}, 'image/jpeg', 0.7)
})
}
关闭摄像头媒体流、并关闭面部识别:
const video: any = document.querySelector("video");
const canvas: any = document.getElementById('canvas');
const context = canvas.getContext('2d');
// 关闭摄像头捕捉视频流
video?.srcObject.getTracks().forEach((track: any) => {
track.stop()
});
// 关闭面部识别追踪
trackerTask.value.stop() // trackerTask.value.run() 启动面部识别追踪
// 清除canvas面部识别追踪框
setTimeout(() => {
context.clearRect(0, 0, canvas.width, canvas.height);
}, 50);