這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 業務背景 由於當前項目中需要實現身份證拍照識別的功能,如果是小程式可以使用微信提供的 ocr-navigator 插件實現,但是在企業微信的H5中沒有提供該插件,所以需要手動實現該功能。 需求分析及資料查閱 眾所周知,前端H5中瀏覽器打開 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
業務背景
由於當前項目中需要實現身份證拍照識別的功能,如果是小程式可以使用微信提供的 ocr-navigator 插件實現,但是在企業微信的H5中沒有提供該插件,所以需要手動實現該功能。
需求分析及資料查閱
眾所周知,前端H5中瀏覽器打開相機打開的是原生相機,無法在相機的界面上覆蓋自定義的元素,比如實現類似下麵的UI界面,無法使用相機拍照功能來直接實現,所以只能另闢蹊徑。
-
通過查閱資料發現,可以通過MediaDevices.getUserMedia()來實現媒體流的輸出,這時可以在頁面中添加video元素,然後把stream流的值賦值給video的srcObject屬性,就可以把video輸出到頁面上,這樣就可以在video元素上面添加自定義元素,實現UI效果。
-
還需要解決的問題是:如何點擊下麵的拍照按鈕時把獲取畫面轉換成圖片,並調用Api實現圖片識別功能。 此時需要使用canvas來實現。通過canvas將video視頻的當前幀繪製到畫布上,然後將其轉換成圖片,然後調用介面來實現身份證識別。
snapPhoto() { const canvas = document.querySelector("#mycanvas"); canvas.width = this.video.videoWidth; canvas.height = this.video.videoHeight; canvas.getContext("2d").drawImage(this.video, 0, 0); const imageBase64 = canvas.toDataURL("image/png", 0.6); return imageBase64 }
需求實現
話不多說,直接上代碼(註意:該頁面代碼 vue-cli3 + vue2 + vant + 企業微信環境)
<template> <div class="ocr-id-card"> <div id="cover" class="cover"> <div class="id-card-container"></div> <video ref="videoRef" class="media-video" autoplay playsinline></video> </div> <div class="footer-tip font-24 radius-32 color-fff flex-center">請將證件放於框內拍攝</div> <div class="footer-btn"> <div class="album" @click="chooseLocalImage"> <img src="@/assets/parttime-operator/album.png" alt="" class="album-img width-68 height-68" /> </div> <div id="snap" class="record-btn" @click="snapPhoto"></div> </div> <canvas id="mycanvas" class="card-canvas"></canvas> </div> </template> <script> import { uploadFileApi, idCardOcrApi } from "@/apis/common"; import { base64URLToFile } from "@/utils/base64-to-img"; export default { data() { return { image_url: "", // 身份證url imageBase64: "", // 身份證照片 base64 cardSide: "FRONT", // 身份證正反面 FRONT:身份證有照片的一面(人像面)BACK:身份證有國徽的一面(國徽面 video: {}, videoTrack: {} }; }, mounted() { const { cardSide } = this.$route.query; this.cardSide = cardSide; this.watchPageVisible(); }, beforeRouteLeave(to, from, next) { if (this.videoTrack) { this.videoTrack.stop(); } next(); }, methods: { // 調用攝像頭 openCamera() { // constraints: 指定請求的媒體類型和相對應的參數 const constraints = { audio: false, video: { width: 1150, height: 768, frameRate: { ideal: 60 }, // 視頻流幀率 facingMode: "environment" // 後置攝像頭 } }; // 相容部分瀏覽器 if (!navigator.mediaDevices) navigator.mediaDevices = {}; // 一些瀏覽器部分支持 mediaDevices,不能直接給對象設置 getUserMedia // 因為這樣可能會覆蓋已有的屬性,只會在沒有getUserMedia屬性的時候添加它。 if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = function(constraints) { // 首先,如果有getUserMedia的話,就獲得它 const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia; if (!getUserMedia) { return Promise.reject(new Error("getUserMedia is not implemented in this browser")); } // 否則,為老的navigator.getUserMedia方法包裹一個Promise return new Promise(function(resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; } // 獲取視頻流 navigator.mediaDevices .getUserMedia(constraints) .then(stream => { this.videoTrack = stream.getVideoTracks()[0]; this.video = document.querySelector(".media-video"); if (this.video) { this.video.srcObject = stream; this.video.onloadedmetadata = () => { this.video.play(); }; } }) .catch(function(err) { console.error(err); }); }, // 監控頁面visibilitychange watchPageVisible() { document.addEventListener("visibilitychange", () => { if (!document.hidden) { this.openCamera(); } else { if (this.video && this.video.srcObject) { this.video.srcObject.getTracks().forEach(track => track.stop()); } } }); }, // 獲取視頻的一幀作為圖片轉換為base64,調用介面識別身份證信息 snapPhoto() { const canvas = document.querySelector("#mycanvas"); canvas.width = this.video.videoWidth * 0.9; canvas.height = this.video.videoHeight * 0.9; canvas.getContext("2d").drawImage(this.video, 0, 0); const imageBase64 = canvas.toDataURL("image/png", 0.6); this.idCardRecognition(imageBase64); }, // 身份證照片識別 async idCardRecognition(imageBase64) { try { this.$toast.loading({ duration: 0, // 持續展示 message: "識別中...", forbidClick: true, loadingType: "spinner" }); const params = { cardSide: this.cardSide, imageBase64 }; const result = await idCardOcrApi(params); if (Object.keys(result).length) { const { Name, IdNum, ValidDate, AdvancedInfo: { IdCard } } = result; if (IdCard) { const imageBase64 = "data:image/png;base64," + IdCard; const file = await base64URLToFile(imageBase64); this.image_url = await this.uploadFile(file); } const id_card_end_time = ValidDate && ValidDate.indexOf("長期") === -1 ? ValidDate.split("-")[1].replace(/\./g, "/") : ""; const id_card_info = { id_card_name: Name ? Name : "", id_card_num: IdNum ? IdNum : "", long_term: ValidDate ? (ValidDate.indexOf("長期") > -1 ? 1 : 2) : 0, id_card_end_time }; if (this.cardSide === "FRONT") { id_card_info.id_card_front = this.image_url; } else { id_card_info.id_card_back = this.image_url; } this.$store.commit("COMMON/setIdCardInfo", id_card_info); } else { const file = await base64URLToFile(imageBase64); this.image_url = await this.uploadFile(file); const id_card_info = {}; if (this.cardSide === "FRONT") { id_card_info.id_card_front = this.image_url; } else { id_card_info.id_card_back = this.image_url; } this.$store.commit("COMMON/setIdCardInfo", id_card_info); } this.$toast.clear(); this.$toast({ message: "識別成功", duration: 800, onClose: () => { this.$router.go(-1); } }); } catch (err) { console.log(err); } }, // 從相冊選擇圖片 chooseLocalImage() { // eslint-disable-next-line no-undef wx.chooseImage({ count: 1, sizeType: ["compressed"], sourceType: ["album"], success: async res => { const id = res.localIds[0]; // eslint-disable-next-line no-undef wx.getLocalImgData({ localId: id, success: async res => { await this.idCardRecognition(res.localData); this.$toast.clear(); }, fail: err => { console.error("getLocalImgData err", err); } }); } }); }, // 上傳文件 uploadFile(file) { return new Promise(async (resolve, reject) => { try { this.$toast.loading({ message: "上傳並識別中", forbidClick: true, loadingType: "spinner" }); const params = new FormData(); params.append("file", file); params.append("type", 1); params.append("file_name", file.name); const { url } = await uploadFileApi(params); resolve(url); } catch (err) { reject(err); } }); } } }; </script> <style lang="less" scoped> .ocr-id-card { width: 100vw; z-index: 2000; background: #fff; overflow: hidden; -webkit-overflow-scrolling: touch; .cover { width: 100vw; height: calc(100vh - 300px); position: fixed; top: 0; left: 0; z-index: 2001; .id-card-container { width: 708px; height: 460px; background: url("~@/assets/parttime-operator/ocr-border.png") 0 0 no-repeat; background-size: 708px 460px; position: fixed; top: 322px; left: 50%; transform: translateX(-50%); z-index: 2004; } } .media-video { width: 100vw; height: 100%; position: absolute; top: -25px; left: 0; } .footer-tip { width: 312px; height: 64px; background: rgba(0, 0, 0, 0.5); position: fixed; bottom: 392px; left: 50%; transform: translateX(-50%); z-index: 2003; } .footer-btn { width: 100vw; height: 300px; background: #fff; position: fixed; bottom: 0; left: 0; z-index: 2005; .record-btn { width: 108px; height: 108px; background: url("~@/assets/parttime-operator/take-photo.png") 0 0 no-repeat; background-size: 108px 108px; position: absolute; top: 76px; left: 50%; transform: translateX(-50%); z-index: 2006; } .album { width: 80px; height: 80px; position: absolute; top: 90px; left: 120px; z-index: 2006; } } .card-canvas { position: fixed; left: -9999px; top: -9999px; z-index: 0; backface-visibility: hidden; transform: translateZ(0); } } </style>
功能優化及相容性bug修複
相容性問題機註意點
- 本地調試打開相機需要使用https協議下才能正常調用獲取媒體流的api
- ios環境下初次打開相機會展示直播界面,安卓系統正常
- 媒體流幀率問題,視頻解析度問題,頂部空白問題。
- ios有滾動條問題,安卓系統正常
- 頁面退出時關閉媒體流輸入,關閉相機,進入時打開媒體流輸入。
解決方案
- 本地開發時開啟htpps
devServer: { https: true, xxx... }
- 頁面中的元素使用fixed定位,並設置z-index高一些
- 設置視頻流幀率和視頻流的解析度大小,下麵的width和height可根據實際情況來調整大小
const constraints = { audio: false, video: { width: 1150, height: 768, frameRate: { ideal: 60 }, // 視頻流幀率 facingMode: "environment" // 後置攝像頭 } };
-
ios有滾動條問題,嘗試了一些css處理方案,無效,歡迎大家評論區指點迷津。
-
調用ocr圖片識別可以調用後端介面或者第三方的API來實現,例如騰訊雲OCR 最後實現效果