前言

对于前端来说,需要后端提供一个人脸识别接口,前端传入图片,接口识别并返回结果,如此看来,其实前端只需实现图片传入即可,但是其实不然,在传入图片时,需要进行以下几点操作:

  1. 判断图片格式,市场上比较常见的是.jpg.jpeg.png
  2. 计算文件大小,一般要求不超过5MB
  3. 对图片进行base64加密

其实前2点具体要看接口要求,但是第3点,是实现人脸识别必备步骤,下文重点讲述一下移动端实现人脸识别的base64加密方法

问题

项目主要使用的技术栈是uniapp,uniapp的优点是上手快,基于vue开发,但缺点也很明显,多环境兼容导致兼容性较差,真机调试和运行较慢。比如h5端可以轻松实现base64加密,但是安卓环境完全不行,因为本地上传图片时,会返回一个blob流,但是uniapp的blob流是以http://localhost…(安卓环境无法识别localhost) 开始,导致无法进行base64加密

解决办法

经过多方实现后,借用html5+ api的多个结合方法(plus.zip.compressImageplus.io.resolveLocalFileSystemURLplus.io.FileReader)实现加密,主要代码如下:

//app压缩图片用for循环 来处理图片压缩 的问题,原因是 plus.zip.compressImage 方法 是异步执行的,for循环很快, 同时手机可执行的压缩方法有限制:应该是3个吧。超出直接就不执行了。所以 原理就是 在图片压缩成功后 继续 回调 压缩函数。 以到达循环压缩图片的功能。app_img(num, rem) {let that = this;let index = rem.tempFiles[num].path.lastIndexOf('.'); //获取图片地址最后一个点的位置let img_type = rem.tempFiles[num].path.substring(index + 1, rem.tempFiles[num].path.length); //截取图片类型如png jpglet img_yuanshi = rem.tempFiles[num].path.substring(0, index); //截取图片原始路径let d2 = new Date().getTime(); //时间戳//压缩图片plus.zip.compressImage({src: rem.tempFiles[num].path, //你要压缩的图片地址dst: img_yuanshi + d2 + '.' + img_type, //压缩之后的图片地址(注意压缩之后的路径最好和原生路径的位置一样,不然真机上报code-5)quality: 70 //[10-100]},function (e) {//压缩之后路径转base64位的//通过URL参数获取目录对象或文件对象plus.io.resolveLocalFileSystemURL(e.target, function (entry) {// 可通过entry对象操作test.html文件entry.file(function (file) {//获取文件数据对象var fileReader = new plus.io.FileReader(); // 文件系统中的读取文件对象,用于获取文件的内容//alert("getFile:" + JSON.stringify(file));fileReader.readAsDataURL(file); //以URL编码格式读取文件数据内容fileReader.onloadend = function (evt) {//读取文件成功完成的回调函数that.base64Img = evt.target.result.split(',')[1]; //拿到‘data:image/jpeg;base64,‘后面的console.log('that.base64Img', that.base64Img);// rem.tempFiles[num].Base64_Path = evt.target.result.split(',')[1];};});});// that.base64Img = that.base64Img.concat(rem.tempFiles[num]);// 【注意】在此人脸认证中,只会传一张图片,故不考虑多张图片情况//利用递归循环来实现多张图片压缩// if (num == rem.tempFiles.length - 1) {// return;// } else {// that.app_img(num + 1, rem);// }},function (error) {console.log('Compress error!');console.log(JSON.stringify(error));uni.showToast({title: '编码失败' + error});});},

详细实现思路

其实对于uniapp实现人脸识别功能来讲,大概要经过这么几个步骤

  1. onImage():打开手机相册上传图片,获取blob流(本地临时地址)
  2. #ifdef APP-PLUS/#ifndef APP-PLUS:判断系统环境,是h5还是安卓环境,然后在进行图片压缩和加密,具体实现代码如下:
//#ifdef APP-PLUS//图片压缩that.app_img(0, res);//#endif// #ifndef APP-PLUSthat.blobTobase64(res.tempFilePaths[0]);// #endif
  1. app_img()/blobTobase64():对要识别的图片进行base64加密
  2. onSave()—>upImage():附件上传,并处理识别信息

具体代码

<!-- 人脸认证 --><template><view><view class="u-margin-30 text-center"><u-avatar size="600" :src="imageSrc"></u-avatar></view><view class="u-margin-60"><u-button type="primary" class="u-margin-top-60" @click="onImage">{{ !imageSrc ? '拍照' : '重拍' }}</u-button><!-- <u-button type="primary" class="u-margin-top-30">重拍</u-button> --><u-button type="primary" class="u-margin-top-50" @click="onSave">保存</u-button></view><u-toast ref="uToast" /></view></template><script>import { registerOrUpdateFaceInfo, UpdateLaborPersonnel } from '@/api/mww/labor.js';import { UploadByProject } from '@/api/sys/upload.js';import { sysConfig } from '@/config/config.js';import storage from 'store';import { ACCESS_TOKEN } from '@/store/mutation-types';export default {name: 'face-authentication',data() {return {imageSrc: '',lastData: {},base64Img: '',base64: ''};},onLoad(option) {this.lastData = JSON.parse(decodeURIComponent(option.lastData));console.log('前一个页面数据', this.lastData);uni.setNavigationBarTitle({title: this.lastData.CnName + '-人脸认证 '});},methods: {onSave() {if (!this.imageSrc) {this.$refs.uToast.show({title: '请先拍照',type: 'error'});}// 人脸上传,附件上传,劳务人员信息修改this.upImage();},// h5压缩图片的方式,url为图片流blobTobase64(url) {console.log('进来了2', url);let imgFile = url;let _this = this;uni.request({url: url,method: 'GET',responseType: 'arraybuffer',success: res => {let base64 = uni.arrayBufferToBase64(res.data); //把arraybuffer转成base64_this.base64Img = 'data:image/jpeg;base64,' + base64; //不加上这串字符,在页面无法显示}});},//app压缩图片用for循环 来处理图片压缩 的问题,原因是 plus.zip.compressImage 方法 是异步执行的,for循环很快, 同时手机可执行的压缩方法有限制:应该是3个吧。超出直接就不执行了。所以 原理就是 在图片压缩成功后 继续 回调 压缩函数。 以到达循环压缩图片的功能。app_img(num, rem) {let that = this;let index = rem.tempFiles[num].path.lastIndexOf('.'); //获取图片地址最后一个点的位置let img_type = rem.tempFiles[num].path.substring(index + 1, rem.tempFiles[num].path.length); //截取图片类型如png jpglet img_yuanshi = rem.tempFiles[num].path.substring(0, index); //截取图片原始路径let d2 = new Date().getTime(); //时间戳//压缩图片plus.zip.compressImage({src: rem.tempFiles[num].path, //你要压缩的图片地址dst: img_yuanshi + d2 + '.' + img_type, //压缩之后的图片地址(注意压缩之后的路径最好和原生路径的位置一样,不然真机上报code-5)quality: 70 //[10-100]},function(e) {//压缩之后路径转base64位的//通过URL参数获取目录对象或文件对象plus.io.resolveLocalFileSystemURL(e.target, function(entry) {// 可通过entry对象操作test.html文件entry.file(function(file) {//获取文件数据对象var fileReader = new plus.io.FileReader(); // 文件系统中的读取文件对象,用于获取文件的内容//alert("getFile:" + JSON.stringify(file));fileReader.readAsDataURL(file); //以URL编码格式读取文件数据内容fileReader.onloadend = function(evt) {//读取文件成功完成的回调函数that.base64Img = evt.target.result.split(',')[1]; //拿到‘data:image/jpeg;base64,‘后面的console.log('that.base64Img', that.base64Img);// rem.tempFiles[num].Base64_Path = evt.target.result.split(',')[1];};});});// that.base64Img = that.base64Img.concat(rem.tempFiles[num]);// 【注意】在此人脸认证中,只会传一张图片,故不考虑多张图片情况//利用递归循环来实现多张图片压缩// if (num == rem.tempFiles.length - 1) {// return;// } else {// that.app_img(num + 1, rem);// }},function(error) {console.log('Compress error!');console.log(JSON.stringify(error));uni.showToast({title: '编码失败' + error});});},// 打开手机相机相册功能onImage() {const that = this;// 安卓系统无法默认打开前置摄像头,具体请看下面app-plus原因,uni.chooseImage({count: 1, //默认9sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有sourceType: ['camera'], // 打开摄像头-'camera',从相册选择-'album'success: function(res) {console.log('文件结果', res);if (res.tempFilePaths.length > 0) {// Blob流地址that.imageSrc = res.tempFilePaths[0];//#ifdef APP-PLUS//图片压缩that.app_img(0, res);//#endif// #ifndef APP-PLUSthat.blobTobase64(res.tempFilePaths[0]);// #endif} else {that.$refs.uToast.show({title: '无文件信息',type: 'error'});}},fail: function(res) {console.log('失败了', res.errMsg);that.$refs.uToast.show({title: res.errMsg,type: 'error'});}});// #ifdef APP-PLUS// console.log('app环境了');// 指定要获取摄像头的索引值,1表示主摄像头,2表示辅摄像头。如果没有设置则使用系统默认主摄像头。// 平台支持【注意注意注意】// Android - 2.2+ (不支持) :// 暂不支持设置默认使用的摄像头,忽略此属性值。打开拍摄界面后可操作切换。// iOS - 4.3+ (支持)// var cmr = plus.camera.getCamera(1);// var res = cmr.supportedImageResolutions[0];// var fmt = cmr.supportedImageFormats[0];// console.log('Resolution: ' + res + ', Format: ' + fmt);// cmr.captureImage(// function(path) {// alert('Capture image success: ' + path);// },// function(error) {// alert('Capture image failed: ' + error.message);// },// { resolution: res, format: fmt }// );// #endif},// 上传附件至[人脸认证]服务器upImage() {if (!this.base64Img) {this.$refs.uToast.show({title: '无图片信息',type: 'error'});return;}const params = {identityId: this.lastData.IdCard, //身份证号码imgInfo: this.base64Img, //头像采用base64编码userId: this.lastData.Id, //劳务人员IduserName: this.lastData.CnName //劳务姓名};uni.showLoading();registerOrUpdateFaceInfo(params).then(res => {if (res.success) {this.$refs.uToast.show({title: '认证成功',type: 'success'});// 上传至附件服务器+修改劳务人员信息this.uploadFile();} else {this.$refs.uToast.show({title: '认证失败,' + res.message,type: 'error'});uni.hideLoading();}}).catch(err => {uni.hideLoading();uni.showModal({title: '提示',content: err});});},// 上传附件至附件服务器uploadFile() {const obj = {project: this.lastData.OrgCode || this.$store.getters.projectCode.value,module: 'mww.personnelCertification',segment: this.lastData.OrgCode,businessID: this.lastData.Id,storageType: 1};let str = `project=${obj.project}&module=${obj.module}&segment=${obj.segment}&businessID=${obj.businessID}&storageType=${obj.storageType}`;console.log('str', str);// const url = '';// console.log('url', url);// const formData = new FormData();// formData.append('file', this.imageSrc, '.png');// UploadByProject(str, formData).then(res => {// if (res.success) {// this.$refs.uToast.show({// title: '上传成功',// type: 'success'// });// } else {// this.$refs.uToast.show({// title: res.message,// type: 'error'// });// }// });const token = uni.getStorageSync(ACCESS_TOKEN);const that = this;// 需要使用uniapp提供的api,因为that.imageSrc的blob流为地址头为localhost(本地临时文件)uni.uploadFile({url: `${sysConfig().fileServer}/UploadFile/UploadByProject?${str}`,filePath: that.imageSrc,formData: {...obj},header: {// 必须传token,不然会报[系统标识不能为空]authorization: `Bearer ${token}`},name: 'file',success: res => {that.$refs.uToast.show({title: '上传成功',type: 'success'});that.lastData.CertificationUrl = res.data[0].virtualPath;that.lastData.Certification = 1;that.updateLaborPersonnel();},fail: err => {console.log('上传失败了', err);that.$refs.uToast.show({title: '上传失败,' + err,type: 'error'});uni.hideLoading();}});},// 修改劳务人员信息updateLaborPersonnel() {UpdateLaborPersonnel(this.lastData).then(res => {if (res.success) {this.$refs.uToast.show({title: '修改成功',type: 'success'});// uni.showToast({// title: '成功了'// });setTimeout(() => {uni.navigateBack({delta: 1});}, 800);} else {this.$refs.uToast.show({title: '修改失败,' + res.message,type: 'error'});}}).finally(() => {uni.hideLoading();});}}};</script><style scoped lang="less"></style>