uniapp 开发 H5 实现人脸融合

29 篇文章 1 订阅

效果图
在这里插入图片描述

一, 项目说明

  1. 两张相片融合使用的是百度人脸融合接口(https://ai.baidu.com/tech/face/merge)
  2. 制作动态相册用的是腾讯人像渐变接口(https://cloud.tencent.com/product/ft?fromSource=gwzcw.3561173.3561173.3561173&utm_medium=cpc&utm_id=gwzcw.3561173.3561173.3561173)
  3. 上传图片可以是URL或者base64 。如果用URL方式上传,需要搭建自已的图片服务器,或者使用图床,本项目使用的是搭建图片服务器。

二, 实现技术

  1. 后端 node.js 搭建图片服务器,用于接收并保存用户上传的图片,并把图片链接返回给前端提交到相应的接口。

  2. 前端 uniapp 开发 H5 页面,打开相册或者相机拍摄上传人脸图片,等待后端接口返回处理好的图片和视频并显示到页面。

三, 核心代码

1. 后端 node.js 代码


var https = require('https');
var qs = require('querystring');
var express=require('express');  //引入模块
var app=express();
var path=require('path');
var fs = require('fs');
var bodyParser = require('body-parser');  //接收base64格式文件用到的模块

// 设置跨域访问
app.all("*",function(req,res,next){    
    res.header("Access-Control-Allow-Origin","*");  //设置允许跨域的域名,*代表允许任意域名跨域    
    res.header("Access-Control-Allow-Headers","content-type");  //允许的header类型    
    res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");  //跨域允许的请求方式 
    if (req.method.toLowerCase() == 'options'){
		res.sendStatus(200);  //让options尝试请求快速结束
	}else{
		next();
	}    
})

app.use(express.static(path.join(__dirname,'facePic')));  //设置facePic文件夹下的文件能通过网址打开 如: http://192.168.1.100:8888/logo.png
app.use(express.static(path.join(__dirname,'imgup')));
app.use(bodyParser.json({limit: '10mb'}));  //限制前端POST请求方式上传数据的大小
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));

//* 获取token -- 使用百度AI人脸融合接口需要鉴权token *//
app.get('/token',function (req,res) {
	var dataString="";
	const param = qs.stringify({
		'grant_type': 'client_credentials',
		'client_id': 'Gd*********************HH',  //改成自己的
		'client_secret': '89***************KaV'   //改成自己的
	});

	https.get(
		{
			hostname: 'aip.baidubce.com',
			path: '/oauth/2.0/token?' + param,
			agent: false
		},
		function (res2) {
			// 在标准输出中查看运行结果
			//res2.pipe(process.stdout);
			
			// 写入文件
			//res2.pipe(fs.createWriteStream('./baidu-token.json'));
			
			//需要用的时候可以读取文件内容
			//fs.readFileSync('./baidu-token.json', 'utf8');
			
			res2.on("data",function(data){
				dataString+=data;
			});
			res2.on("end",function(){
				console.log(dataString); //这里是https.get()返回的完整的数据
			});
			
		}
	);
	
	//等待三秒,让数据完全写入变量后再发给前端
	setTimeout(()=>{
		res.send(dataString)
	},3000)
	
});


//* 接收图片 *//
app.post('/up',function(req,res) {
	//req.body 解析前端POST传递过来的数据
	var imgBase64 = req.body.imgBase64 ; 
	var base64Data = imgBase64.replace(/^data:image\/\w+;base64,/, "");  //过滤 data:image/jpeg;base64,
	var dataBuffer = new Buffer.from(base64Data, 'base64');
	
	var save_path = './imgup/'  //保存路径
	var file_name = Date.now() + '.png'  //以时间戳做文件名
	var save_name = save_path + file_name
	
	fs.writeFile(save_name, dataBuffer, function(err) {
		if(err){
		  res.send(err);
		}else{
		  res.send(file_name);
		  console.log("保存成功!")
		}
	});	
	
});



var jobId = '';

//* 生成动态相册 --使用腾讯 ai 接口 *//
app.get('/video',function(req,res) {
	//req.query 解析前端get传递过来的数据	
	var urlArrStr = req.query.urlArr;
	// 字符串转为数组
	var urlArr = urlArrStr.split(",");
	console.log(urlArr)
	
	// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
	const tencentcloud = require("tencentcloud-sdk-nodejs");
	const FtClient = tencentcloud.ft.v20200304.Client;
	// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
	// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
	const clientConfig = {
		credential: {
			secretId: "AKI**************lxwI", //改成自己的
			secretKey: "LO***************mEG", //改成自己的
		},
		region: "ap-guangzhou",
		profile: {
			httpProfile: {
				endpoint: "ft.tencentcloudapi.com",
			},
		},
	};
	// 实例化要请求产品的client对象,clientProfile是可选的
	// https://console.cloud.tencent.com/api/explorer?Product=ft&Version=2020-03-04&Action=MorphFace
	const client = new FtClient(clientConfig);	
	const params = {
		"Urls": [ 
			urlArr[0],
			urlArr[1],
			urlArr[2]
		]
	};
	client.MorphFace(params).then(
		(data) => {
			console.log(data);
			jobId = data.JobId;
			res.send(data);			
		},
		(err) => {
			console.log("生成动态相册失败", err);
			res.send(err);
		}
	);
	
});

/* 根据jobId查询动态相册视频--生成视频需要10秒左右 */
app.get('/query',function(req,res) {
	// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
	const tencentcloud = require("tencentcloud-sdk-nodejs");
	const FtClient = tencentcloud.ft.v20200304.Client;
	// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
	// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
	const clientConfig = {
		credential: {
			secretId: "AKI**************lxwI", //改成自己的
			secretKey: "LO***************mEG", //改成自己的
		},
		region: "ap-guangzhou",
		profile: {
			httpProfile: {
				endpoint: "ft.tencentcloudapi.com",
			},
		},
	};
	// 实例化要请求产品的client对象,clientProfile是可选的
	// https://console.cloud.tencent.com/api/explorer?Product=ft&Version=2020-03-04&Action=MorphFace
	const client = new FtClient(clientConfig);	
	const params = {
		"JobId": jobId
	};
	
	client.QueryFaceMorphJob(params).then(
		(data) => {
			console.log(data);
			res.send(data);
		},
		(err) => {
			console.log("查询动态相册失败", err);
			res.send(err);
		}
	);
	
});


// 创建服务, 上线后需要把 '127.0.0.1' 修改为 '0.0.0.0' , 否则服务没有响应
var server =app.listen(8888,'127.0.0.1',function(req,res,next){
  var host = server.address().address
  var port = server.address().port  
  console.log("服务开启成功,访问地址为 http://%s:%s", host, port)
});

2. 前端 uniapp 代码

// index.vue
<template>
	<view>
		<view class='all'>

			<!-- 图片 -->
			<view class="images-box">				
				<block v-for="(item, index) in templateArr" :key="index">
				  <view class='img-box'>
					<image class='img' :src='item' mode='aspectFill'></image>
					<view class='img-delete' @click='imgDelete1' :data-delindex="index">
					    <image class='img' src='../../static/delete.png' ></image>  
					</view>
				  </view>
				</block>
				
				<view class='img-box' @click='addPic1' v-if="templateArr.length<1">
					<image class='img-camera' src='../../static/camera.png'></image>  
				</view>
			</view>
			
			<!-- 图片 -->
			<view class="images-box">				
				<block v-for="(item, index) in targetArr" :key="index">
				  <view class='img-box'>
					<image class='img' :src='item' mode='aspectFill'></image>
					<view class='img-delete' @click='imgDelete2' :data-delindex="index">
					    <image class='img' src='../../static/delete.png' ></image>  
					</view>
				  </view>
				</block>
				
				<view class='img-box' @click='addPic2' v-if="targetArr.length<1">
					<image class='img-camera' src='../../static/camera.png'></image>  
				</view>
			</view>
			

			<button @click='uploadimage'>上传图片</button>			
			<button @click='viewPic'>查看融合图</button>
		</view>		
	</view>	
</template>

<script>
	// 后端服务地址, 外网必须能够访问
	var serverUrl = 'http://4h2***********.com:8888/'  
	var resArr = []
	var access_token = ''
	
	export default {		
		data() {
			return {				
				templateArr: [],
				targetArr:[],
				isUp: true
			}
		},
		
		onLoad() {				
			// 域名写入缓存,以方便其它页面使用
			uni.setStorageSync('serverUrl',serverUrl)			
			// 调用请求token函数
			this.getToken()			
		},
		
		methods: {	
			// 选择图片
			addPic1: async function() {
				this.isUp = true				
				uni.chooseImage({
					count: 1,
					sizeType: ['compressed'],
					sourceType: ['album','camera'],
					success: (res) => {
						this.templateArr = res.tempFilePaths						
					}
				});
			},
			
			// 选择图片
			addPic2: async function() {
				this.isUp = true				
				uni.chooseImage({
					count: 1,
					sizeType: ['compressed'],
					sourceType: ['album','camera'],
					success: (res) => {
						this.targetArr = res.tempFilePaths						
					}
				});
			},
			
			// 上传图片
			uploadimage: function () {				
				if(!this.isUp){
					uni.showToast({
					  title: "请不要重复上传",
					  icon: 'none',
					  duration: 2000,
					})
				}else{
					if (this.templateArr.length != 0 && this.targetArr.length != 0 ) { //数组不为空的时候才执行
						let arr1 = this.templateArr
						let arr2 = this.targetArr
						arr1= arr1.concat(arr2) //合并数组

						this.imgCompress(arr1) //调用函数imgCompress
						
						this.isUp = false
						uni.showLoading({
							title: "上传中!",
							icon: 'loading',
						})
						
					} else {
						uni.showToast({
						  title: "请选择图片!",
						  icon: 'none',
						  duration: 1000,
						})
					}
				}
				
			},
			

		    // 循环调用压缩图片
			imgCompress(tempFilePaths) {
				let compressImgs = [];
				let results = [];
				tempFilePaths.forEach((item, index) => {
					compressImgs.push(new Promise((resolve, reject) => {
						// #ifndef H5
						uni.compressImage({
							src: item,
							quality: 0.5,
							success: res => {
								//console.log('compressImage', res.tempFilePath)
								results.push(res.tempFilePath);
								resolve(res.tempFilePath);
							},
							fail: (err) => {
								//console.log(err.errMsg);
								reject(err);
							},
							complete: () => {
								//uni.hideLoading();
							}
						})
						// #endif
						
						// #ifdef H5				
						//调用压缩图片函数 canvasDataURL
						this.canvasDataURL(item, {quality: 0.5}, (base64Codes) => {
							//this.imgUpload(base64Codes);
							results.push(base64Codes);
							resolve(base64Codes);
						})
						// #endif
					}))
				});
				
				Promise.all(compressImgs) //执行所有需请求的接口
				.then((results) => {
					//uni.hideLoading();
					//this.imgUpload(results);
				})
				.catch((res, object) => {
					//uni.hideLoading();
				});
			},
			
			//压缩图片
			canvasDataURL(path, obj, callback) {
				var this_ = this; //指向本页
				var img = new Image();
				img.src = path;
				img.onload = function() {
					var that = this; //指向本函数
					
					// 按比例压缩
					var w, h, scale ;
					if(that.height >= 3000 || that.width >= 3000){
						w = that.width / 5,
						h = that.height / 5,
						scale = w / h;
					}
					else if(that.height >= 2000 || that.width >= 2000){
						w = that.width / 4,
						h = that.height / 4,
						scale = w / h;
					}
					else if(that.height >= 1000 || that.width >= 1000){
						w = that.width / 2,
						h = that.height / 2,
						scale = w / h;
					}else{
						w = that.width ,
						h = that.height ,
						scale = w / h;
					}
					
					var quality = 0.5; // 图片质量 quality值越小,所绘制出的图像越模糊
					//生成canvas
					var canvas = document.createElement('canvas');
					var ctx = canvas.getContext('2d');
					// 创建属性节点
					var anw = document.createAttribute("width");
					anw.nodeValue = w;
					var anh = document.createAttribute("height");
					anh.nodeValue = h;
					canvas.setAttributeNode(anw);
					canvas.setAttributeNode(anh);
					ctx.drawImage(that, 0, 0, w, h);
					
					// if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
					// 	quality = obj.quality;
					// }

					var base64 = canvas.toDataURL('image/jpg', quality);					
					callback(base64);  // 回调函数返回base64的值
					
					this_.upBase64(base64) //调用函数
					
				}
			},
			
			// 向后端上传图片
			upBase64(img_Base64){
				uni.request({
					url: serverUrl + 'up',
					data: {        
						imgBase64: img_Base64  							 
					},
					method: 'POST',
					header: { 'content-type': 'application/json' },
					//请求成功
					success: (res) => { 
						if(resArr.length >= 3){
							resArr = [] //清空数组
							resArr.push(res.data)
							uni.setStorageSync('imgName',resArr) // 写入缓存
							// console.log(resArr)
						}else{
							resArr.push(res.data) //追加到数组
							uni.setStorageSync('imgName',resArr) // 写入缓存
							// console.log(resArr)
						}
					},
					fail: (err) => {
						console.log('错误:' , err)
					},
					complete: () => {
						//uni.hideLoading()
						if(resArr.length == 2){
							this.getData()
							
						}
					}
				})
				
			},
			
			// 请求百度token
			getToken(){
				uni.request({
					url: serverUrl + 'token',  
					data: {	},
					method: 'GET',
					success: (res) => {
						console.log(res)
						access_token = res.data.access_token
					},
					fail: (err) => {
						console.log('请求百度token失败:',err)
					},
					complete: () => {
						
					}
				})
			},
			
			// 请求百度人脸融合接口
			getData(){				
				let url_a = serverUrl + resArr[0]
				let url_b = serverUrl + resArr[1]
				
				uni.request({
					url: ' https://aip.baidubce.com/rest/2.0/face/v1/merge?access_token=' + access_token ,
					data: {
						"version":"2.0",
						"image_template":{"image":url_a,"image_type":"URL"},
						"image_target":{"image":url_b,"image_type":"URL"}
					},
					method: 'POST',
					header: {
						'Content-Type': 'application/json' //请求头信息
					},
					//请求成功
					success: (res) => { 
					    console.log('成功融合:' , res)
						
						let imgBase64 = 'data:image/png;base64,' + res.data.result.merge_image
						this.upBase64(imgBase64) //调用函数,把融合后的图片上传到后端保存
						
						setTimeout(()=>{
							this.getVideo() //调用函数							
							uni.navigateTo({  //跳转到指定页面
								url: "../faceView/faceView",
							})
						},2000)
						
					},
					fail: (err) => {
						console.log('错误:' , err)
					},
					complete: () => {
						uni.hideLoading()
						
					}
				})
				
				
			},
			
			// 删除已经选择的图片
			imgDelete1: function (e) {				
				//let index = e.currentTarget.dataset.delindex; 
				//获取要删除的图片的下标,否则删除的永远是第一张 ,对应 <view class='img-delect' @click='imgDelete1' :data-delindex="index">
				this.templateArr.splice(0, 1);			  	
			},
			
			imgDelete2: function (e) {
				//let index = e.currentTarget.dataset.delindex; 
				//获取要删除的图片的下标,否则删除的永远是第一张 ,对应 <view class='img-delect' @click='imgDelete1' :data-delindex="index">
				this.targetArr.splice(0, 1);			  	
			},
			
			// 查看融合图
			viewPic(){
				if(this.isUp){
					uni.showToast({
						title:'请先上传图片',
						icon:'error',
						duration:2000
					})
				}else{
					uni.navigateTo({  //跳转到指定页面
						url: "../faceView/faceView",
					})
				}
				
			},
			
			/* 请求合成动态相册 */
			getVideo(){
				let urlArr = []
				if(resArr.length >= 3){
					// 域名+图片名称 ,循环拼接成完整的url 例:http://4h237******oip.com:8888/1664094464371.png
					for(let i=0 ; i<resArr.length; i++){
						urlArr.push(serverUrl + resArr[i])
					}
				
					uni.request({
						url: serverUrl + 'video',  
						data: {
							urlArr: urlArr,  //赋值给变量并传送到后端
						},
						method: 'GET',
						success: (res) => {
							console.log(res)
						},
						fail: (err) => {
							console.log(err)
						},
						complete: () => {
							
						}
					})
					
				}else{
					console.log('resArr长度不够3')
				}
			}
			
			
		}
	}
</script>

<style>
	@import "./index.css";
</style>

// faceView.vue
<template>
	<view class="">
		<view class="img-box">
			<image class="img" :src="imgUrl" mode="aspectFill"></image>
		</view>
		<video :src="videoUrl" controls object-fit="cover" autoplay="true" loop="true" ></video>
		
		<!-- <button @click='doQuery' :disabled="isDisabled">刷新视频</button> -->
		<button @click='backToHome'>返回主页</button>
	</view>

</template>

<script>
	var serverUrl = ''
	
	export default {
		data() {
			return {
				imgUrl:'',
				videoUrl:'',
				isDisabled:true
			}
		},

		onLoad() {
			serverUrl = uni.getStorageSync('serverUrl')			
			this.getUrl()
		},
		
		onShow() {
			// this.videoUrl = uni.getStorageSync('morphUrl')
			// 默认视频
			this.videoUrl = serverUrl + 'loading.mp4'
			
			// 10秒后调用请求视频函数,因为腾讯接口合成视频需要几秒时间
			setTimeout(()=>{
				this.doQuery()
				this.isDisabled = false
			},10000) 
		},

		methods: {
			backToHome() {
				// 跳转到指定页面
				uni.navigateTo({
					url: "../index/index",
				})
				
			},
			
			/* 从缓存中读取融合后的相片链接 */
			getUrl(){
				try {
					let value = uni.getStorageSync('imgName');
					if (value) {
						// console.log(value)
						this.imgUrl = serverUrl + value[2];
					}else{
						this.imgUrl = '../../static/noPic.png';
					}
				} catch (e) {
					// error
					this.imgUrl = '../../static/noPic.png';
				}
			},
			
			/* 查询动态相册视频 */
			doQuery(){
				uni.request({
					url: serverUrl + 'query',  
					data: { },
					method: 'GET',
					success: (res) => {
						console.log(res)
						let morphUrl = res.data.FaceMorphOutput.MorphUrl
						this.videoUrl = morphUrl
						// uni.setStorageSync('morphUrl',morphUrl)
						
					},
					fail: (err) => {
						console.log('查询失败',err)
					},
					complete: () => {
						
					}
				})
				
			}
			
		}
	}
</script>

<style>
	.img-box {
		display: flex;
		justify-content: center;
	}

	.img{
		width: 700rpx;
		height: 600rpx;
	}

	button {
		width: 90%;
		margin-top: 20rpx;
		background-color: #ffaa00;
	}
	
	video{
		width: 94%;
		height: 500rpx;
		margin: 24rpx;
	}
</style>

源码下载地址:
链接:https://pan.baidu.com/s/1AVB71AjEX06wpc4wbcV_tQ?pwd=l9zp
提取码:l9zp
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值