需求:
安卓原生app端接入騰訊人臉核身sdk,采集人臉信息和賬號(hào)綁定袁辈。用戶(hù)使用同一賬號(hào)登錄微信公眾號(hào)h5端菜谣,h5采集人臉信息,調(diào)用騰訊人臉比對(duì)接口吵瞻,和原生app端采集的人臉信息比對(duì)葛菇,是否是同一個(gè)人,然后再進(jìn)行下一步操作橡羞。
H5實(shí)現(xiàn)思路:
1.如何調(diào)用移動(dòng)端設(shè)備攝像頭
2.使用tracking.js捕捉人臉
3.對(duì)人臉進(jìn)行拍照眯停,描繪到canvas畫(huà)布上面
4.將照片轉(zhuǎn)換成base64格式
5.調(diào)用后臺(tái)接口,后臺(tái)調(diào)用騰訊人臉比對(duì)接口進(jìn)行比對(duì)卿泽。(由于調(diào)用騰訊人臉比對(duì)接口需要簽名莺债,簽名在后臺(tái)做比較合適)
代碼以及注釋
1.下載tracking.js 網(wǎng)址:https://trackingjs.com? ?將需要的js文件復(fù)制到靜態(tài)資源文件夾
2.使用video標(biāo)簽調(diào)用手機(jī)攝像頭滋觉,playsinline、webkit-playsinline屬性非常重要齐邦,如果不寫(xiě)會(huì)掉不起來(lái)蘋(píng)果手機(jī)的攝像頭椎侠。微信開(kāi)發(fā)者文檔上面有明確的說(shuō)明,喜歡鉆研的同學(xué)可以到微信開(kāi)發(fā)者文檔中查看文檔措拇。video和canvas的寬高的寫(xiě)法是為了適配手機(jī)屏幕的大小我纪。
```
<template>
? <div id="app">
? ? <div v-show="showContainer" class="face-capture" id="face-capture">
? ? ? <p class="tip">請(qǐng)保持人像在取景框內(nèi)</p>
? ? ? <video
? ? ? ? id="video"
? ? ? ? :width="vwidth"
? ? ? ? :height="vheight"
? ? ? ? playsinline
? ? ? ? webkit-playsinline
? ? ? ></video>
? ? ? <canvas id="refCanvas" :width="cwidth" :height="cheight"></canvas>
? ? ? <img
? ? ? ? class="img-cover"
? ? ? ? src="@/assets/img/face/yuanxingtouming.png"
? ? ? ? alt=""
? ? ? />
? ? ? <p class="contentp">{{ scanTip }}</p>
? ? </div>
? ? <div v-if="!showContainer" class="img-face">
? ? ? <img class="imgurl" :src="imgUrl" />
? ? </div>
? </div>
</template>
```
3.獲取媒體設(shè)備,video開(kāi)始播放
```
import tracking from "@/assets/js/tracking-min.js";
import "@/assets/js/data/face-min.js";
import "@/assets/js/data/eye-min.js";
import "@/assets/js/data/mouth-min.js";
import { faceInfo, conFace } from "@/request/api/my.js";
export default {
? data() {
? ? return {
? ? ? screenSize: {
? ? ? ? width: window.screen.width,
? ? ? ? height: window.screen.height,
? ? ? },
? ? ? URL: null,
? ? ? streamIns: null, // 視頻流
? ? ? showContainer: true, // 顯示
? ? ? tracker: null,
? ? ? tipFlag: false, // 提示用戶(hù)已經(jīng)檢測(cè)到
? ? ? flag: false, // 判斷是否已經(jīng)拍照
? ? ? context: null, // canvas上下文
? ? ? profile: [], // 輪廓
? ? ? removePhotoID: null, // 停止轉(zhuǎn)換圖片
? ? ? scanTip: "人臉識(shí)別中...", // 提示文字
? ? ? imgUrl: "",
? ? ? canvas: null,
? ? ? trackertask: null,
? ? ? vwidth: "266",
? ? ? vheight: "266",
? ? ? cwidth: "266",
? ? ? cheight: "266",
? ? ? userInfo: {},
? ? ? orderData: {},
? ? };
? },
mounted() {
? ? //設(shè)置video canvas寬高
? ? const scale = this.screenSize.width / 375;
? ? this.vwidth = 266 * scale;
? ? this.vheight = 266 * scale;
? ? this.cwidth = 266 * scale;
? ? this.cheight = 266 * scale;
? ? this.playVideo();
? },
methods: {
playVideo() {
? ? ? this.getUserMedia(
? ? ? ? {
? ? ? ? ? //攝像頭拍攝的區(qū)域
? ? ? ? ? video: {
? ? ? ? ? ? width: 500,
? ? ? ? ? ? height: 500,
? ? ? ? ? ? facingMode: "user",
? ? ? ? ? } /* 前置優(yōu)先 */,
? ? ? ? },
? ? ? ? this.success,
? ? ? ? this.error
? ? ? );
? ? },
? ? // 訪(fǎng)問(wèn)用戶(hù)媒體設(shè)備
? ? getUserMedia(constrains, success, error) {
? ? ? if (navigator.mediaDevices.getUserMedia) {
? ? ? ? //最新標(biāo)準(zhǔn)API
? ? ? ? navigator.mediaDevices
? ? ? ? ? .getUserMedia(constrains)
? ? ? ? ? .then(success)
? ? ? ? ? .catch(error);
? ? ? } else if (navigator.webkitGetUserMedia) {
? ? ? ? //webkit內(nèi)核瀏覽器
? ? ? ? navigator.webkitGetUserMedia(constrains).then(success).catch(error);
? ? ? } else if (navigator.mozGetUserMedia) {
? ? ? ? //Firefox瀏覽器
? ? ? ? navagator.mozGetUserMedia(constrains).then(success).catch(error);
? ? ? } else if (navigator.getUserMedia) {
? ? ? ? //舊版API
? ? ? ? navigator.getUserMedia(constrains).then(success).catch(error);
? ? ? } else {
? ? ? ? this.scanTip = "你的瀏覽器不支持訪(fǎng)問(wèn)用戶(hù)媒體設(shè)備";
? ? ? }
? ? },
}
```
4.安卓手機(jī)在允許調(diào)用攝像頭之后正常調(diào)用和播放丐吓,蘋(píng)果手機(jī)第一次允許之后調(diào)不出來(lái)攝像頭浅悉,返回到上個(gè)頁(yè)面再次進(jìn)入人臉識(shí)別頁(yè)面,可以正常調(diào)用券犁。這個(gè)問(wèn)題想了很多方向术健,使用了微信api里面的一些監(jiān)聽(tīng)方法,依然無(wú)效粘衬。想了一些策略荞估,在前一個(gè)頁(yè)面獲取媒體設(shè)備等,都不是完美方案稚新。 耽誤了兩天時(shí)間勘伺,這天中午午休,我做夢(mèng)夢(mèng)到一個(gè)美女褂删。那美女竟然穿著女仆裝娇昙,超短的蕾絲花邊裙,修長(zhǎng)光溜溜的美腿笤妙,潔白秀美的臉蛋,烏黑發(fā)亮的發(fā)絲噪裕,圓潤(rùn)豐滿(mǎn)的胸脯蹲盘,簡(jiǎn)直是夢(mèng)想中的女朋友。我對(duì)著那個(gè)美女拋了一個(gè)媚眼膳音,那美女便婀娜多姿地眨著眼睛回應(yīng)我召衔。我咽了一口口水,忍不住便問(wèn):你如何才能做我女朋友祭陷。那美女張開(kāi)櫻桃小口苍凛,突出幾個(gè)調(diào)皮的字來(lái):你抓到我,我便以身相許兵志。此話(huà)一聽(tīng)醇蝴,我哪能安奈住自己激動(dòng)的內(nèi)心。迅速向她撲去想罕,可是我向她跑去悠栓,她就轉(zhuǎn)身向后跑,我停下來(lái),她就停下來(lái)對(duì)我拋飛吻惭适。我追呀追笙瑟,她跑呀跑,她始終跟我保持著一定的距離癞志,我是怎么都追不上她往枷,氣的我吐血。突然凄杯,我心想错洁,上次做夢(mèng),我得到了一件短暫時(shí)間穿越能力盾舌。只要我集中精力發(fā)功墓臭,我就能穿越到未來(lái)1秒以后的時(shí)間。想到此我就開(kāi)始集中精力妖谴,我一追她便跑窿锉,我一停她便停。突然這個(gè)世界相對(duì)與我靜止了1秒鐘膝舅,我利用這1秒的時(shí)間嗡载,迅速接近女仆裝美女。1秒1秒又1秒仍稀,我終于抱住了女仆裝美女洼滚,那美女對(duì)我眉開(kāi)眼笑,還用櫻桃小口往我臉上親......突然技潘,音樂(lè)響起遥巴,午休時(shí)間結(jié)束,我意猶未盡的醒來(lái)享幽,發(fā)現(xiàn)抱枕上面都是口水铲掐,趕快趁人不注意用紙擦掉。又是突然值桩,我想到了是否可以在調(diào)用video.play方法的時(shí)候摆霉,給它加一個(gè)定時(shí)器,讓它1秒鐘之后再執(zhí)行奔坟。? 沒(méi)想到加上之后携栋,蘋(píng)果手機(jī)上面的問(wèn)題解決了。自此咳秉,我猜想:當(dāng)我們?cè)儐?wèn)是否可以調(diào)用手機(jī)攝像頭的時(shí)候婉支,蘋(píng)果的系統(tǒng)彈框阻礙了js的執(zhí)行,在那一刻js代碼停止執(zhí)行澜建,但是當(dāng)我們點(diǎn)擊允許之后磅摹,js代碼沒(méi)有繼續(xù)執(zhí)行滋迈,因?yàn)橄到y(tǒng)沒(méi)有手段或者說(shuō)事件去觸發(fā)代碼繼續(xù)走。我們加上setTimeout函數(shù)之后户誓,系統(tǒng)有了可以讓代碼繼續(xù)執(zhí)行的觸發(fā)手段饼灿,所以js代碼繼續(xù)執(zhí)行,我們就可以進(jìn)行下一步人臉捕捉了帝美。如果同學(xué)們有更好的解釋?zhuān)瑲g迎留言碍彭。
代碼
```
success(stream) {
? ? ? this.streamIns = stream;
? ? ? const video = document.getElementById("video");
? ? ? // webkit內(nèi)核瀏覽器
? ? ? this.URL = window.URL || window.webkitURL;
? ? ? if ("srcObject" in video) {
? ? ? ? video.srcObject = stream;
? ? ? } else {
? ? ? ? video.src = this.URL.createObjectURL(stream);
? ? ? }
? ? ? // 蘋(píng)果手機(jī)的系統(tǒng)彈框會(huì)阻止js的線(xiàn)程的繼續(xù)執(zhí)行 手動(dòng)0.1秒之后自動(dòng)執(zhí)行代碼
? ? ? setTimeout(() => {
? ? ? ? video.play();
? ? ? ? this.initTracker();// 人臉捕捉
? ? ? }, 100);
? ? },
? ? error(e) {
? ? ? this.scanTip = "訪(fǎng)問(wèn)用戶(hù)媒體失敗";
? ? },
```
5.設(shè)置各種參數(shù) 實(shí)例化人臉捕捉實(shí)例對(duì)象
```
// 人臉捕捉 設(shè)置各種參數(shù) 實(shí)例化人臉捕捉實(shí)例對(duì)象,注意canvas上面的動(dòng)畫(huà)效果。
? ? initTracker() {
? ? ? this.context = document.getElementById("refCanvas").getContext("2d"); // 畫(huà)布
? ? ? this.canvas = document.getElementById("refCanvas");
? ? ? this.tracker = new window.tracking.ObjectTracker("face"); // tracker實(shí)例
? ? ? this.tracker.setInitialScale(4);
? ? ? this.tracker.setStepSize(2); // 設(shè)置步長(zhǎng)
? ? ? this.tracker.setEdgesDensity(0.1);
? ? ? try {
? ? ? ? this.trackertask = window.tracking.track("#video", this.tracker); // 開(kāi)始追蹤
? ? ? } catch (e) {
? ? ? ? this.scanTip = "訪(fǎng)問(wèn)用戶(hù)媒體失敗悼潭,請(qǐng)重試";
? ? ? }
? ? ? //開(kāi)始捕捉方法 一直不停的檢測(cè)人臉直到檢測(cè)到人臉
? ? ? this.tracker.on("track", (e) => {
? ? ? ? //畫(huà)布描繪之前清空畫(huà)布
? ? ? ? this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
? ? ? ? if (e.data.length === 0) {
? ? ? ? ? this.scanTip = "未檢測(cè)到人臉";
? ? ? ? } else {
? ? ? ? ? e.data.forEach((rect) => {
? ? ? ? ? ? //設(shè)置canvas 方框的顏色大小
? ? ? ? ? ? this.context.strokeStyle = "#42e365";
? ? ? ? ? ? this.context.lineWidth = 2;
? ? ? ? ? ? this.context.strokeRect(rect.x, rect.y, rect.width, rect.height);
? ? ? ? ? });
? ? ? ? ? if (!this.tipFlag) {
? ? ? ? ? ? this.scanTip = "檢測(cè)成功庇忌,正在拍照,請(qǐng)保持不動(dòng)2秒";
? ? ? ? ? }
? ? ? ? ? // 1.5秒后拍照舰褪,僅拍一次 給用戶(hù)一個(gè)準(zhǔn)備時(shí)間
? ? ? ? ? // falg 限制一直捕捉人臉皆疹,只要拍照之后就停止檢測(cè)
? ? ? ? ? if (!this.flag) {
? ? ? ? ? ? this.scanTip = "拍照中...";
? ? ? ? ? ? this.flag = true;
? ? ? ? ? ? this.removePhotoID = setTimeout(() => {
? ? ? ? ? ? ? this.tackPhoto();
? ? ? ? ? ? ? document.getElementById("video").pause();
? ? ? ? ? ? ? this.tipFlag = true;
? ? ? ? ? ? }, 1500);
? ? ? ? ? }
? ? ? ? }
? ? ? });
? ? },
```
6.拍照 繪制照片,計(jì)算照片大小占拍,騰訊人臉比對(duì)接口對(duì)照片大小和格式有要求略就。
```
// 拍照
? ? tackPhoto() {
? ? ? // 在畫(huà)布上面繪制拍到的照片
? ? ? this.context.drawImage(
? ? ? ? document.getElementById("video"),
? ? ? ? 0,
? ? ? ? 0,
? ? ? ? this.vwidth,
? ? ? ? this.vwidth
? ? ? );
? ? ? // 保存為base64格式
? ? ? this.imgUrl = this.saveAsPNG(document.getElementById("refCanvas"));
? ? ? /** 拿到base64格式圖片之后就可以在this.compare方法中去調(diào)用后端接口比較了,也可以調(diào)用getBlobBydataURI方法轉(zhuǎn)化成文件再去比較
? ? ? ?* 我們項(xiàng)目里有一個(gè)設(shè)置個(gè)人頭像的地方晃酒,先保存一下用戶(hù)的圖片表牢,然后去拿這個(gè)圖片的地址和當(dāng)前拍照?qǐng)D片給后端接口去比較。
? ? ? ?* */
? ? ? // this.compare(imgUrl)
? ? ? //判斷圖片大小
? ? ? this.imgSize();
? ? ? this.faceToTengXun(); // 人臉比對(duì)
? ? ? this.close();
? ? },
? ? imgSize() {
? ? ? if (this.imgUrl) {
? ? ? ? // 獲取base64圖片byte大小
? ? ? ? const equalIndex = this.imgUrl.indexOf("="); // 獲取=號(hào)下標(biāo)
? ? ? ? let size;
? ? ? ? if (equalIndex > 0) {
? ? ? ? ? const str = this.imgUrl.substring(0, equalIndex); // 去除=號(hào)
? ? ? ? ? const strLength = str.length;
? ? ? ? ? const fileLength = strLength - (strLength / 8) * 2; // 真實(shí)的圖片byte大小
? ? ? ? ? size = Math.floor(fileLength / 1024); // 向下取整
? ? ? ? ? console.log("size", size + "KB");
? ? ? ? } else {
? ? ? ? ? const strLength = this.imgUrl.length;
? ? ? ? ? const fileLength = strLength - (strLength / 8) * 2;
? ? ? ? ? size = Math.floor(fileLength / 1024); // 向下取整
? ? ? ? ? console.log("size", size + "KB");
? ? ? ? }
? ? ? ? if (size > 1024) {
? ? ? ? ? // 圖片超過(guò)1M 按比例壓縮
? ? ? ? ? this.imgUrl = document
? ? ? ? ? ? .getElementById("refCanvas")
? ? ? ? ? ? .toDataURL("image/png", 1024 / size);
? ? ? ? }
? ? ? }
? ? },
? ? // Base64轉(zhuǎn)文件
? ? getBlobBydataURI(dataURI, type) {
? ? ? var binary = window.atob(dataURI.split(",")[1]);
? ? ? var array = [];
? ? ? for (var i = 0; i < binary.length; i++) {
? ? ? ? array.push(binary.charCodeAt(i));
? ? ? }
? ? ? return new Blob([new Uint8Array(array)], {
? ? ? ? type: type,
? ? ? });
? ? },
? ? // compare(url) {
? ? // ? ? let blob = this.getBlobBydataURI(url, 'image/png')
? ? // ? ? let formData = new FormData()
? ? // ? ? formData.append("file", blob, "file_" + Date.parse(new Date()) + ".png")
? ? // ? ? // TODO 得到文件后進(jìn)行人臉識(shí)別
? ? // },
? ? // 保存為png,base64格式圖片
? ? saveAsPNG(c) {
? ? ? return c.toDataURL("image/png", 0.4);
? ? },
```
7. 人臉采集之后贝次,移除實(shí)例化對(duì)象崔兴,初始化參數(shù)和css樣式部分代碼
```
close() {
? ? ? this.flag = false;
? ? ? this.tipFlag = false;
? ? ? this.showContainer = false;
? ? ? this.context = null;
? ? ? this.scanTip = "人臉識(shí)別中...";
? ? ? clearTimeout(this.removePhotoID);
? ? ? if (this.streamIns) {
? ? ? ? this.streamIns.enabled = false;
? ? ? ? this.streamIns.getTracks()[0].stop();
? ? ? ? this.streamIns.getVideoTracks()[0].stop();
? ? ? }
? ? ? this.streamIns = null;
? ? ? this.trackertask.stop();
? ? ? this.tracker = null;
? ? }
<style>
.face-capture {
? display: flex;
? flex-direction: column;
? align-items: center;
? justify-content: center;
}
.tip {
? position: fixed;
? top: 48px;
? z-index: 5;
? font-size: 18px;
? font-family: PingFangSC-Medium, PingFang SC;
? font-weight: 500;
? color: #333333;
? line-height: 25px;
}
.face-capture video,
.face-capture canvas {
? position: fixed;
? top: 117.5px;
? object-fit: cover;
? z-index: 2;
? background-repeat: no-repeat;
? background-size: 100% 100%;
}
.face-capture .img-cover {
? position: fixed;
? top: 63px;
? width: 375px;
? height: 375px;
? object-fit: cover;
? z-index: 3;
? background-repeat: no-repeat;
? background-size: 100% 100%;
}
.face-capture .contentp {
? position: fixed;
? top: 438px;
? font-size: 18px;
? font-weight: 500;
? color: #333333;
}
.face-capture .rect {
? border: 2px solid #0aeb08;
? position: fixed;
? z-index: 4;
}
.img-face {
? display: flex;
? flex-direction: column;
? align-items: center;
? justify-content: center;
}
.img-face .imgurl {
? position: fixed;
? top: 117.5px;
? width: 266px;
? height: 266px;
? border-radius: 133px;
}
</style>
```
總結(jié):
1.人臉捕捉技術(shù)使用的tracking.js,關(guān)鍵是要理解它的運(yùn)作原理和一下參數(shù)配置蛔翅。
2.因?yàn)橐{(diào)用手機(jī)媒體設(shè)備敲茄,兼容性問(wèn)題是大問(wèn)題,特別是蘋(píng)果手機(jī)的問(wèn)題山析,往往不知道如何下手折汞,需要有豐富的開(kāi)發(fā)經(jīng)驗(yàn)和各種曲線(xiàn)救國(guó)的開(kāi)發(fā)思想。
3.那個(gè)夢(mèng)真的很重要盖腿。
4.對(duì),美女更重要损同。