目录
- 前言
- 问题
- 解决办法
- 详细实现思路
- 具体代码
- 总结
前言
对于前端来说,需要后端提供一个人脸识别接口,前端传入图片,接口识别并返回结果,如此看来,其实前端只需实现图片传入即可,但是其实不然,在传入图片时,需要进行以下几点操作:
- 判断图片格式,市场上比较常见的是.jpg、.jpeg、.png
- 计算文件大小,一般要求不超过5MB
- 对图片进行base64加密
其实前2点具体要看接口要求,但是第3点,是实现人脸识别必备步骤,下文重点讲述一下移动端实现人脸识别的base64加密方法
问题
项目主要使用的技术栈是uniapp,uniapp的优点是上手快,基于vue开发,但缺点也很明显,多环境兼容导致兼容性较差,真机调试和运行较慢。比如h5端可以轻松实现base64加密,但是安卓环境完全不行,因为本地上传图片时,会返回一个blob流,但是uniapp的blob流是以<http://localhost>…(安卓环境无法识别localhost)开始,导致无法进行base64加密
解决办法
经过多方实现后,借用html5+ api的多个结合方法(plus.zip.compressImage、plus.io.resolveLocalFileSystemURL、plus.io.FileReader)实现加密,主要代码如下:
//app压缩图片 用for循环 来处理图片压缩 的问题,原因是 plus.zip.compressImage 方法 是异步执行的,for循环很快, 同时手机可执行的压缩方法有限制:应该是个吧。超出直接就不执行了。所以 原理就是 在图片压缩成功后 继续 回调 压缩函数。 以到达循环压缩图片的功能。 | |
app_img(num, rem) { | |
let that = this; | |
let index = rem.tempFiles[num].path.lastIndexOf('.'); //获取图片地址最后一个点的位置 | |
let img_type = rem.tempFiles[num].path.substring(index +, rem.tempFiles[num].path.length); //截取图片类型如png jpg | |
let img_yuanshi = rem.tempFiles[num].path.substring(, index); //截取图片原始路径 | |
let d = new Date().getTime(); //时间戳 | |
//压缩图片 | |
plus.zip.compressImage( | |
{ | |
src: rem.tempFiles[num].path, //你要压缩的图片地址 | |
dst: img_yuanshi + d + '.' + img_type, //压缩之后的图片地址(注意压缩之后的路径最好和原生路径的位置一样,不然真机上报code-5) | |
quality: //[10-100] | |
}, | |
function (e) { | |
//压缩之后路径转base位的 | |
//通过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.baseImg = evt.target.result.split(',')[1]; //拿到‘data:image/jpeg;base64,‘后面的 | |
console.log('that.baseImg', that.base64Img); | |
// rem.tempFiles[num].Base_Path = evt.target.result.split(',')[1]; | |
}; | |
}); | |
}); | |
// that.baseImg = that.base64Img.concat(rem.tempFiles[num]); | |
// 【注意】在此人脸认证中,只会传一张图片,故不考虑多张图片情况 | |
//利用递归循环来实现多张图片压缩 | |
// if (num == rem.tempFiles.length -) { | |
// return; | |
// } else { | |
// that.app_img(num +, rem); | |
// } | |
}, | |
function (error) { | |
console.log('Compress error!'); | |
console.log(JSON.stringify(error)); | |
uni.showToast({ | |
title: '编码失败' + error | |
}); | |
} | |
); | |
}, |
详细实现思路
其实对于uniapp实现人脸识别功能来讲,大概要经过这么几个步骤
- onImage():打开手机相册上传图片,获取blob流(本地临时地址)
- #ifdef APP-PLUS/#ifndef APP-PLUS:判断系统环境,是h5还是安卓环境,然后在进行图片压缩和加密,具体实现代码如下:
//#ifdef APP-PLUS | |
//图片压缩 | |
that.app_img(, res); | |
//#endif | |
// #ifndef APP-PLUS | |
that.blobTobase(res.tempFilePaths[0]); | |
// #endif |
- app_img()/blobTobase64():对要识别的图片进行base64加密
- onSave()—>upImage():附件上传,并处理识别信息
具体代码
<!-- 人脸认证 --> | |
<template> | |
<view> | |
<view class="u-margin- text-center"><u-avatar size="600" :src="imageSrc"></u-avatar></view> | |
<view class="u-margin-"> | |
<u-button type="primary" class="u-margin-top-" @click="onImage">{{ !imageSrc ? '拍照' : '重拍' }}</u-button> | |
<!-- <u-button type="primary" class="u-margin-top-">重拍</u-button> --> | |
<u-button type="primary" class="u-margin-top-" @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: {}, | |
baseImg: '', | |
base: '' | |
}; | |
}, | |
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(); | |
}, | |
// h压缩图片的方式,url为图片流 | |
blobTobase(url) { | |
console.log('进来了', url); | |
let imgFile = url; | |
let _this = this; | |
uni.request({ | |
url: url, | |
method: 'GET', | |
responseType: 'arraybuffer', | |
success: res => { | |
let base = uni.arrayBufferToBase64(res.data); //把arraybuffer转成base64 | |
_this.baseImg = 'data:image/jpeg;base64,' + base64; //不加上这串字符,在页面无法显示 | |
} | |
}); | |
}, | |
//app压缩图片 用for循环 来处理图片压缩 的问题,原因是 plus.zip.compressImage 方法 是异步执行的,for循环很快, 同时手机可执行的压缩方法有限制:应该是个吧。超出直接就不执行了。所以 原理就是 在图片压缩成功后 继续 回调 压缩函数。 以到达循环压缩图片的功能。 | |
app_img(num, rem) { | |
let that = this; | |
let index = rem.tempFiles[num].path.lastIndexOf('.'); //获取图片地址最后一个点的位置 | |
let img_type = rem.tempFiles[num].path.substring(index +, rem.tempFiles[num].path.length); //截取图片类型如png jpg | |
let img_yuanshi = rem.tempFiles[num].path.substring(, index); //截取图片原始路径 | |
let d = new Date().getTime(); //时间戳 | |
//压缩图片 | |
plus.zip.compressImage( | |
{ | |
src: rem.tempFiles[num].path, //你要压缩的图片地址 | |
dst: img_yuanshi + d + '.' + img_type, //压缩之后的图片地址(注意压缩之后的路径最好和原生路径的位置一样,不然真机上报code-5) | |
quality: //[10-100] | |
}, | |
function(e) { | |
//压缩之后路径转base位的 | |
//通过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.baseImg = evt.target.result.split(',')[1]; //拿到‘data:image/jpeg;base64,‘后面的 | |
console.log('that.baseImg', that.base64Img); | |
// rem.tempFiles[num].Base_Path = evt.target.result.split(',')[1]; | |
}; | |
}); | |
}); | |
// that.baseImg = that.base64Img.concat(rem.tempFiles[num]); | |
// 【注意】在此人脸认证中,只会传一张图片,故不考虑多张图片情况 | |
//利用递归循环来实现多张图片压缩 | |
// if (num == rem.tempFiles.length -) { | |
// return; | |
// } else { | |
// that.app_img(num +, 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:, //默认9 | |
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 | |
sourceType: ['camera'], // 打开摄像头-'camera',从相册选择-'album' | |
success: function(res) { | |
console.log('文件结果', res); | |
if (res.tempFilePaths.length >) { | |
// Blob流地址 | |
that.imageSrc = res.tempFilePaths[]; | |
//#ifdef APP-PLUS | |
//图片压缩 | |
that.app_img(, res); | |
//#endif | |
// #ifndef APP-PLUS | |
that.blobTobase(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环境了'); | |
// 指定要获取摄像头的索引值,表示主摄像头,2表示辅摄像头。如果没有设置则使用系统默认主摄像头。 | |
// 平台支持【注意注意注意】 | |
// Android -.2+ (不支持) : | |
// 暂不支持设置默认使用的摄像头,忽略此属性值。打开拍摄界面后可操作切换。 | |
// iOS -.3+ (支持) | |
// var cmr = plus.camera.getCamera(); | |
// var res = cmr.supportedImageResolutions[]; | |
// var fmt = cmr.supportedImageFormats[]; | |
// 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.baseImg) { | |
this.$refs.uToast.show({ | |
title: '无图片信息', | |
type: 'error' | |
}); | |
return; | |
} | |
const params = { | |
identityId: this.lastData.IdCard, //身份证号码 | |
imgInfo: this.baseImg, //头像采用base64编码 | |
userId: this.lastData.Id, //劳务人员Id | |
userName: 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: | |
}; | |
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[].virtualPath; | |
that.lastData.Certification =; | |
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: | |
}); | |
},); | |
} else { | |
this.$refs.uToast.show({ | |
title: '修改失败,' + res.message, | |
type: 'error' | |
}); | |
} | |
}) | |
.finally(() => { | |
uni.hideLoading(); | |
}); | |
} | |
} | |
}; | |
</script> | |
<style scoped lang="less"></style> |