HTML5+tracking.js实现刷脸支付
<p>最近刷脸支付很火,老板们当然要追赶时代潮流,于是就有了刷脸支付这个项目。前端实现关键的技术是<strong>摄像头录像</strong>,<strong>拍照</strong>和<strong>人脸比对</strong>,本文来探讨一下如何在html5环境中如何实现刷脸支付以及开发过程中遇到的问题。</p><h1>1.摄像头</h1>
<h2>1.1 input获取摄像头</h2>
<p>html5中获取手机上的图片,有两种方式,使用input,如下可以打开摄像头拍照:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="file"</span><span style="color: rgba(255, 0, 0, 1)"> capture</span><span style="color: rgba(0, 0, 255, 1)">="camera"</span><span style="color: rgba(255, 0, 0, 1)"> accept</span><span style="color: rgba(0, 0, 255, 1)">="image/*"</span><span style="color: rgba(0, 0, 255, 1)">/></span></pre>
</div>
<p>另外如果想打开相册,可以这样:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="file"</span><span style="color: rgba(255, 0, 0, 1)"> accept</span><span style="color: rgba(0, 0, 255, 1)">="img/*"</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>但是这两种方式都会有兼容性问题,用过的同学可能都知道。</p>
<h2>1.2 getUserMedia获取摄像头</h2>
<p>getUserMedia是html5一个新的api,官方一点的定义是:</p>
<blockquote>
<p><code><strong>MediaDevices.getUserMedia()</strong></code> 会提示用户给予使用媒体输入的许可,媒体输入会产生一个<code>MediaStream</code>,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。</p>
</blockquote>
<p>简单一点说就是可以获取到用户摄像头。</p>
<p>同上面input一样,这种方式也有兼容性问题,不过可以使用其他方式解决,这里可以参考MediaDevices.getUserMedia(),文档中有介绍"在旧的浏览器中使用新的API"。我这里在网上也找了一些参考,总结出一个相对全面的getUserMedia版本,代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 访问用户媒体设备</span>
<span style="color: rgba(0, 0, 0, 1)">getUserMedia(constrains, success, error) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (navigator.mediaDevices.getUserMedia) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">最新标准API</span>
navigator.mediaDevices.getUserMedia(constrains).then(success).<span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(error);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (navigator.webkitGetUserMedia) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">webkit内核浏览器</span>
navigator.webkitGetUserMedia(constrains).then(success).<span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(error);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (navigator.mozGetUserMedia) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Firefox浏览器</span>
navagator.mozGetUserMedia(constrains).then(success).<span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(error);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (navigator.getUserMedia) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">旧版API</span>
navigator.getUserMedia(constrains).then(success).<span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">(error);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = "你的浏览器不支持访问用户媒体设备"<span style="color: rgba(0, 0, 0, 1)">
}
}</span></pre>
</div>
<h2>1.3 播放视屏</h2>
<p>获取设备方法有两个回调函数,一个是成功,一个是失败。成功了就开始播放视频,播放视屏其实就是给video设置一个url,并调用play方法,这里设置url要考虑不同浏览器兼容性,代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">success(stream) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.streamIns =<span style="color: rgba(0, 0, 0, 1)"> stream
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 设置播放地址,webkit内核浏览器</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.URL = window.URL ||<span style="color: rgba(0, 0, 0, 1)"> window.webkitURL
</span><span style="color: rgba(0, 0, 255, 1)">if</span> ("srcObject" <span style="color: rgba(0, 0, 255, 1)">in</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.$refs.refVideo) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$refs.refVideo.srcObject =<span style="color: rgba(0, 0, 0, 1)"> stream
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$refs.refVideo.src = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.URL.createObjectURL(stream)
}
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.$refs.refVideo.onloadedmetadata = e =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 播放视频</span>
<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.$refs.refVideo.play()
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.initTracker()
}
},
error(e) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = "访问用户媒体失败" + e.name + "," +<span style="color: rgba(0, 0, 0, 1)"> e.message
}</span></pre>
</div>
<p>注意:</p>
<ol>
<li>播放视屏方法最好写在onloadmetadata回调函数中,否则可能会报错。</li>
<li>播放视频的时候出于安全性考虑,必须在本地环境中测试,也就是http://localhost/xxxx中测试,或者带有https://xxxxx环境中测试,不然的话或有跨域问题。</li>
<li>下面用到的initTracker()方法也好放在这个onloadedmetadata回调函数里,不然也会报错。</li>
</ol>
<h1>2. 捕捉人脸</h1>
<h2>2.1 使用tracking.js捕捉人脸</h2>
<p>视屏在video中播放成功之后就开始识别人脸了,这里使用到一个第三方的功能tracking.js,是国外的大神写的JavaScript图像识别插件。关键代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 人脸捕捉</span>
<span style="color: rgba(0, 0, 0, 1)">initTracker() {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.context = <span style="color: rgba(0, 0, 255, 1)">this</span>.$refs.refCanvas.getContext("2d") <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 画布</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.tracker = <span style="color: rgba(0, 0, 255, 1)">new</span> tracking.ObjectTracker(['face']) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> tracker实例</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.tracker.setStepSize(1.7) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 设置步长</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.tracker.on('track', <span style="color: rgba(0, 0, 255, 1)">this</span>.handleTracked) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 绑定监听方法</span>
<span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
tracking.track(</span>'#video', <span style="color: rgba(0, 0, 255, 1)">this</span>.tracker) <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 开始追踪</span>
} <span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (e) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = "访问用户媒体失败,请重试"<span style="color: rgba(0, 0, 0, 1)">
}
}</span></pre>
</div>
<p>捕获到人脸之后,可以在页面上用一个小方框标注出来,这样有点交互效果。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 追踪事件</span>
<span style="color: rgba(0, 0, 0, 1)">handleTracked(e) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (e.data.length === 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = '未检测到人脸'<span style="color: rgba(0, 0, 0, 1)">
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.tipFlag) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = '检测成功,正在拍照,请保持不动2秒'<span style="color: rgba(0, 0, 0, 1)">
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 1秒后拍照,仅拍一次</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.flag) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.scanTip = '拍照中...'
<span style="color: rgba(0, 0, 255, 1)">this</span>.flag = <span style="color: rgba(0, 0, 255, 1)">true</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.removePhotoID = setTimeout(() =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.tackPhoto()
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.tipFlag = <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">
}, </span>2000<span style="color: rgba(0, 0, 0, 1)">)
}
e.data.forEach(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.plot)
}
}</span></pre>
</div>
<p>在页面中画一些方框,标识出人脸:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="rect"</span><span style="color: rgba(255, 0, 0, 1)"> v-for</span><span style="color: rgba(0, 0, 255, 1)">="item in profile"</span><span style="color: rgba(255, 0, 0, 1)">
:style</span><span style="color: rgba(0, 0, 255, 1)">="{ width: item.width + 'px', height: item.height + 'px', left: item.left + 'px', top: item.top + 'px'}"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 绘制跟踪框</span>
<span style="color: rgba(0, 0, 0, 1)">plot({x, y, width: w, height: h}) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 创建框对象</span>
<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.profile.push({ width: w, height: h, left: x, top: y })
}</span></pre>
</div>
<h2>2.2 拍照</h2>
<p>拍照,就是使用video作为图片源,在canvas中保存一张图片下来,注意这里使用toDataURL方法的时候可以设置第二个参数quality,从0到1,0表示图片比较粗糙,但是文件比较小,1表示品质最好。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 拍照</span>
<span style="color: rgba(0, 0, 0, 1)">tackPhoto() {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.context.drawImage(<span style="color: rgba(0, 0, 255, 1)">this</span>.$refs.refVideo, 0, 0, <span style="color: rgba(0, 0, 255, 1)">this</span>.screenSize.width, <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.screenSize.height)
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 保存为base64格式</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.imgUrl = <span style="color: rgba(0, 0, 255, 1)">this</span>.saveAsPNG(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.$refs.refCanvas)
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> this.compare(imgUrl)</span>
<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.close()
},
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Base64转文件</span>
<span style="color: rgba(0, 0, 0, 1)">getBlobBydataURI(dataURI, type) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> binary = window.atob(dataURI.split(','));
</span><span style="color: rgba(0, 0, 255, 1)">var</span> array =<span style="color: rgba(0, 0, 0, 1)"> [];
</span><span style="color: rgba(0, 0, 255, 1)">for</span>(<span style="color: rgba(0, 0, 255, 1)">var</span> i = 0; i < binary.length; i++<span style="color: rgba(0, 0, 0, 1)">) {
array.push(binary.charCodeAt(i));
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span> Blob([<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Uint8Array(array)], {
type: type
});
},
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 保存为png,base64格式图片</span>
<span style="color: rgba(0, 0, 0, 1)">saveAsPNG(c) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> c.toDataURL('image/png', 0.3<span style="color: rgba(0, 0, 0, 1)">)
}</span></pre>
</div>
<p>拍照完成之后就可以把文件发送给后端,让后端进行对比验证,这里后端使用的是阿里云的接口。</p>
<h1>3. 最后效果</h1>
<h2>3.1 参考代码demo</h2>
<p>最后,demo我已经放在github上了,感兴趣可以打开看一下。</p>
<p>效果如下:</p>
<p><img src="https://img2020.cnblogs.com/blog/72678/202004/72678-20200416135617942-133503375.gif" alt=""></p>
<h2>3.2 在项目中落地</h2>
<p>最后放在项目中,无非就是最后一个步骤,去调用接口比对,根据比对结果成功是成功还是失败,决定是人脸支付还是继续使用原来的密码支付,效果如下:</p>
<p><img src="https://img2020.cnblogs.com/blog/72678/202004/72678-20200418115227540-927737288.gif" alt=""></p>
<p>ps:这里人脸比对失败了,是因为我带着口罩,就不呲牙露脸了。后端调用阿里云的接口地址:https://help.aliyun.com/document_detail/154615.html?spm=a2c4g.11186623.6.625.632a37b9brzAoi</p>
<p> </p>
</div>
<div id="MySignature" role="contentinfo">
<p style="background-position: 1% 50%; border-top: #e0e0e0 1px dashed; border-right: #e0e0e0 1px dashed; border-bottom: #e0e0e0 1px dashed; border-left: #e0e0e0 1px dashed; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 60px; background: #969696 url("https://images.cnblogs.com/cnblogs_com/lloydsheng/239039/o_copyright.gif") no-repeat 1% 50%; font-family: 微软雅黑; font-size: 12px; color: #FFFFFF">
作者:<b><span style="font-size: 12px; color: red">Tyler Ning</span></b>
<br>
出处:http://www.cnblogs.com/tylerdonet/
<br>
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,如有问题,请微信联系<strong >冬天里的一把火
</strong>
<div class="van-overlay" style="display: none;z-index: 1;position: fixed;top: 0;left: 0;z-index: 1;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.7);"></div>
<imgid="pop-contect-wechat" style="display:none;width:20em;position: fixed;max-height: 100%;overflow-y: auto;transition: transform 0.3s;" src="https://files-cdn.cnblogs.com/files/tylerdonet/shouwangzhe059187.bmp"/>
</p><br><br>
来源:https://www.cnblogs.com/tylerdonet/p/12711086.html
頁:
[1]