记录--如何在H5中实现OCR拍照识别身份证功能
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
业务背景
由于当前项目中需要实现身份证拍照识别的功能,如果是小程序可以使用微信提供的 ocr-navigator 插件实现,但是在企业微信的H5中没有提供该插件,所以需要手动实现该功能。
需求分析及资料查阅
众所周知,前端H5中浏览器打开相机打开的是原生相机,无法在相机的界面上覆盖自定义的元素,比如实现类似下面的UI界面,无法使用相机拍照功能来直接实现,所以只能另辟蹊径。
-
通过查阅资料发现,可以通过MediaDevices.getUserMedia()来实现媒体流的输出,这时可以在页面中添加video元素,然后把stream流的值赋值给video的srcObject属性,就可以把video输出到页面上,这样就可以在video元素上面添加自定义元素,实现UI效果。
-
还需要解决的问题是:如何点击下面的拍照按钮时把获取画面转换成图片,并调用Api实现图片识别功能。 此时需要使用canvas来实现。通过canvas将video视频的当前帧绘制到画布上,然后将其转换成图片,然后调用接口来实现身份证识别。
1 2 3 4 5 6 7 8 | 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 + 企业微信环境)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | <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
1 2 3 4 | devServer: { https: true , xxx... } |
- 页面中的元素使用fixed定位,并设置z-index高一些
- 设置视频流帧率和视频流的分辨率大小,下面的width和height可根据实际情况来调整大小
1 2 3 4 5 6 7 8 9 | const constraints = { audio: false , video: { width: 1150, height: 768, frameRate: { ideal: 60 }, // 视频流帧率 facingMode: "environment" // 后置摄像头 } }; |
-
ios有滚动条问题,尝试了一些css处理方案,无效,欢迎大家评论区指点迷津。
-
调用ocr图片识别可以调用后端接口或者第三方的API来实现,例如腾讯云OCR 最后实现效果
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
2022-11-03 记录--手写vm.$mount方法