毕业礼物——“广工云毕业”小程序开发

项目介绍

由于疫情的影响,我们学校无法进行正常的毕业照流程,所以学院找到了我,希望能够开发一款自动合成的毕业照的小程序。

解决方案

  1. 前端:小程序开发
  2. 后台:知晓云
  3. 人脸融合接口:腾讯云人脸融合

体验

由于腾讯云的人脸融合需要付费,所以能够体验的时间为2020年6月-2020年7月
微信搜索“广工云毕业”即可进行
服务器费用已经到期了,不能体验

第一步:界面设计

界面设计
  1. 顶部隐藏栏
    在这里插入图片描述
    微信有一个API接口可以获取顶部状态栏的高度。

     wx.getSystemInfoSync().statusBarHeight
    

    这样就可以避免微信的UI挡住了功能区。

  2. 背景图
    由于<img>标签不可以嵌套内容,而且小程序的css不可以使用URL引用本地文件,但是由于背景图是每一个页面不变的,如果使用网络路径的话,就会导致加载是白屏时间变长,并且由于小程序中的所有的请求都是经过代理的,所以就会导致每次切换页面获取不到缓存文件,每次背景图都需要重新请求,导致服务器的流量不必要的消耗。
    解决方法:

    1. 把图片转变成base64格式
    2. 由于小程序的2M的限制,尽量压缩背景图大小
  3. 手机屏幕适配
    主要使用微信官方提供的rpx来进行设置,为了保持图片的比例,无论是高度与宽度都使用rpx
    来进行设置。
    好处:这样就基本可以不用考虑了y轴适配的问题
    缺点:X轴的高度就需要进行设置滚动区域来进行适配了。

  4. 组件开发
    在开发过程中,会发现很多重复的样式,我们把这些高类似的样式进行提取,做成组件,这样会大大节省我们的开发时间,并且也会为未来增加功能带来便利。
    比如:在一开始开发的时候,按钮是没有动画的,如果我们没有提取成组件的话,这样我们就需要一一对每一个按钮进行重复地添加。而如果我们提取成组件的话,只需要修改组件就可以一下完成所有按钮的动画添加。
    例如不同大小按钮,图片选择器,文字选择器
    我这里就拿文字选择器来举例:
    在这里插入图片描述
    文字选择器有

    1. 选取状态与为选取状态,
    2. 不定量的选择
    3. 点击触发钩子
    	<view class='select-layout' >
        <text wx:for='{{_list}}'
         wx:for-item='item'
         wx:for-index='index'
          wx:key='item' 
          bindtap="onTap"
          class='{{item.selectStatus?"is-select":""}}'
        data-value='{{item.value}}'
        data-index='{{index}}'
        >
       {{item.label.length>2?item.label:(item.label)}}
     </text>
    </view>
    
    	/* component/textListBtn/testListBtn.wxss */
    	.select-layout{
    	  display: flex;
    	  justify-content: space-around;
    	  align-items: center;
    	  padding:0 60rpx ;
    	  box-sizing: border-box;
    	  height: 110rpx;
    	  margin:20rpx 0;
    	  width: 750rpx;
    	}
    	.is-select{
    	  border-bottom: 3px solid #DEA54B;
    	}
    	
    
    	"use strict";
    	Component({
    	    lifetimes: {
    	    // 初始化文字内容与状态
    	        attached: function () {
    	            let _list = [];
    	            let index = 0;
    	            for (let i of this.properties.list) {
    	                let obj = Object.assign({
    	                    selectStatus: false
    	                }, i);
    	                if (index === this.properties.index) {
    	                    obj.selectStatus = true;
    	                }
    	                _list.push(obj);
    	                index++;
    	            }
    	            console.log(_list);
    	            this.setData({
    	                _list,
    	                currentSelect: this.properties.index
    	            });
    	        }
    	    },
    	    properties: {
    	    	// 传入的文字list
    	        list: Array,
    	        // 默认选取的index
    	        index: Number
    	    },
    	    data: {
    	    // 内部的btn list 用于渲染
            _list: new Array(),
            // 内部的“双向绑定”选取index
            currentSelect: 0,
       
        },
    	    methods: {
    	    // 点击事件
    	        onTap(e) {
    	            let { value, index } = e.currentTarget.dataset;
    	            if (index !== this.data.currentSelect) {
    	
    	                var myEventDetail = {
    	                    value,
    	                    index
    	                };
    	                // 设置选取文字显示
    	                let arr = this.data._list;
    	                arr[index].selectStatus = true;
    	                arr[this.data.currentSelect].selectStatus = false;
    	                this.setData({
    	                    currentSelect: index,
    	                    _list: arr
    	                });
    					// 触发父组件的事件钩子
    	                this.triggerEvent('tapEvent', myEventDetail);
    	            }
    	        }
    	    }
    	});
    

    父组件使用:

    	<ui-text-btn
    	list='{{clothingLabelBtn}}' 
    	index="{{previewIndex}}" 
    	bind:tapEvent='textBtnClick' />
    
     	//值
    	data:{
    			previewIndex:0,
    			clothingLabelBtn: [ {
    		      label: '男生',
    		      value: 'men'
    		    }, {
    		      label: '女生',
    		      value: 'women'
    		    }]
        }
    

功能主体

1. 学士服人脸融合

流程图
在这里插入图片描述
前端展示组件
由于学士服模特需要拖动,使用的是movable-areamovable-view这2个组件。
代码示例:

	    <movable-area id='movable-area-edit' 
	    style="position:relative;width:625rpx;height:415rpx;margin:0 auto;">
        <movable-view style="width:100%" disable='false'> <image src='{{selectBackgroundImg}}' /></movable-view>
        <movable-view  
        data-type='clothing' 
        direction='all' 
        bindchange='bindchange'  
        x='{{clothingImgX}}'  y='{{clothingImgY}}' 
        style="width:{{previewWidth}}rpx;height:{{previewHeight}}rpx;">  
        <image 
        src='{{lifeState===lifeStateEditImage?cropperImageSrc:selectClothingImg}}' />
        </movable-view>
    </movable-area> 
  1. x='{{clothingImgX}}' y='{{clothingImgY}}'是模特图的对应位置。

  2. 由于CANVAS画图的适合单位是px,而我这里使用的是微信官方提供的rpx来设置高度与宽度。所以需要进一步转化单位。

  3. 由于网络异步加载图片的原因,如果使用css来加载模特图的话,就可能造成图片还没有下载完就进行合成,这样就会抛出异常。为了这个问题,我给每次切换模特图设置为同步加载,图片加载完成才能进行下一步操作。

    	  async selectClick(e) {
    	    wx.showLoading({
    	      title: "图片下载中……",
    	      mask:true
    	    })
    	    let { index } = e.currentTarget.dataset
    	    let url = `${this.data.previewSrc}${this.data.previewList[index].id}.png`
    	    wx.getImageInfo({
    	      src: url,
    	      success: (res) => {
    	        this.setData({
    	          currentClothingSelect: index + 1,
    	          selectClothingImg: res.path,
    	          previewWidth: this.data.previewHeight / res.height * res.width
    	        })
    	        wx.hideLoading()
    	      }
    	    })
    	  },
    
  4. 合成图片,首先创建CAVANS的上下文

    	/*省略*/
    	  var ctx = wx.createCanvasContext('outPutImage', this);
    	  // canvas高度与宽度
          var w = this.data.canvasWidth;
          var h = this.data.canvasHeight;
          // 画背景图
    	  await this.drawBackground(ctx, w, h)
    	  // 画模特图
    	  await this.drawClothing(ctx)
    	  ctx.draw(true, () => {
    	        wx.canvasToTempFilePath({
    	          x: 0,
    	          y: 0,
    	          width: w,
    	          height: h,
    	          destWidth: w,
    	          destHeight: h,
    	          quality: 0.9,
    	          canvasId: 'outPutImage',
    	          success: function success(res) {
    	            resolve(res.tempFilePath)
    	          }
    	        })
    	      })
    	     /*省略*/
    	      // 其他函数
    	    drawBackground(ctx, w, h) {
    			    let src = this.data.selectBackgroundImg
    			    return new Promise(resolve => {
    			      wx.getImageInfo({
    			        src,
    			        success(res) {
    			          ctx.drawImage(res.path, 0, 0, w, h)
    			          resolve()
    			        }
    			      })
    			    })
    		},
    		drawClothing(ctx) {
    			    let clothingImg = this.data.cropperImageSrc
    			    let c_Y = this.data.clothingImgY * this.data.canvasHeight / this.data.moveableHeight
    			    let c_x = this.data.clothingImgX * this.data.canvasWidth / this.data.moveableWidth
    			    let w = this.data.previewWidth / this.data.pxTurnRpx * this.data.canvasWidth / this.data.moveableWidth*this.data.cropperScale
    			    let h = this.data.previewHeight / this.data.pxTurnRpx * this.data.canvasHeight / this.data.moveableHeight*this.data.cropperScale
    			    return new Promise(resolve => {
    			      wx.getImageInfo({
    			        src: clothingImg, 
    			        success(res) {
    			          ctx.drawImage(res.path,c_x,c_Y,w,h)
    			          resolve()
    			        }
    			      })
    			    })
    			
    		},
    

    这里拿生成模特图的来讲下数据如何进行转换,由于用户的手机端的屏幕限制,我们不可能直接生成用户预览图的大小,因为直接生成的话,图片会很小也不清晰,所以为了解决这个问题,需要进行放大图片的大小,所以需要按照一定的比例大小进行缩放。

    这里的canvasHeightcanvasWidth就是我们固定输出的图片长宽。分别为1500与1000.
    moveableWidthmoveableHeight会根据用户使用的移动端不同而不同,所以我们需要进行按照一定的比例进行缩放,当然这里长宽的比例也是3:2.,这样才能保持图片不变形。
    我们从模特的横向的坐标进行计算,
    this.data.clothingImgX * this.data.canvasWidth / this.data.moveableWidth这样,我们就能够获取到模特图进行缩放之后的新的位置。
    同理,模特图的纵向坐标与长宽按照这样计算,就可以获取到进行缩放后的真实的位置。

  5. 人脸融合
    人脸融合我使用的是腾讯ai平台的提供的接口,如果按照费用计算好像是百度的比较便宜一点,由于我之前就使用过腾讯ai的接口,所以这次也直接使用腾讯的接口。
    由于我这个项目没有使用到服务器,所以我的人脸融合的请求是直接在小程序上进行请求的。大家可以参考下腾讯ai平台提供的文档。
    腾讯ai也提供了小程序的云开发的sdk,使用这个应该可以节省很多麻烦,因为自己手写的接口的确很多坑。这里我就简单介绍下小程序如何不通过云平台来直接请求腾讯ai接口
    首先是接口:在这里插入图片描述
    请求的域名需要添加到小程序的request域名上(幸好是https)。
    然后就是请求的参数了:
    在这里插入图片描述
    人脸融合中比较重要的是ProjectId创建的活动id,ModelId需要合成的模特图ID,Image用户人脸素材,RspImgType返回的数据格式,由于没有服务器就直接返回base64了。
    其他的都是固定的数据字段。我将数据处理操作提取为一个函数进行调用:

     getRequest:function(data,ProjectId,ModelId){
     	//ProjectId 活动id
     	//ModelId 模特素材id
        let SecretId = '人脸融合的id'
        let SecretKey = '人脸融合的key'
        let Action = 'FaceFusion'
        let Nonce = Math.floor(Math.random()*10000);// 随机id
        let Language = 'zh-CN'// 中文
        var timestamp = Date.parse(new Date());// 日期
        timestamp = timestamp / 1000;
        let Timestamp = timestamp
        let Version= '2018-12-01'// 人脸融合的版本
        // 传入
        let Image=data// 图片的base64
       	// 节点选择
        let Region='ap-guangzhou'
        // 返回格式
        let RspImgType='base64'
    
        let Signature = getSignature({Action,Image,Language,ModelId,Nonce,ProjectId,Region,RspImgType,SecretId,Timestamp,Version})
        return {
          Action,SecretId,SecretKey,Timestamp,Nonce,Version,Signature,Language,Region,RspImgType,Region
        }
      }
      // 字典顺序输出
      function getSignature({
    	  Action,
    	  Image,
    	  Language,
    	  ModelId,
    	  Nonce,
    	  ProjectId,
    	  Region,
    	  RspImgType,
    	  SecretId,
    	  Timestamp,
    	  Version
    	}) {
    	  return `Action=${(Action)}&Image=${(Image)}&Language=${(Language)}&ModelId=${(ModelId)}&Nonce=${(Nonce)}&ProjectId=${(ProjectId)}&Region=${(Region)}&RspImgType=${(RspImgType)}&SecretId=${(SecretId)}&Timestamp=${(Timestamp)}&Version=${Version}`
    	}
    

    使用示例:

    let config = getRequest(data,ProjectId,ModelId)
    let sign = `POSTfacefusion.tencentcloudapi.com/?${config.Signature}`// sign是腾讯ai接口识别
    let sign_sha1 = Crypto.HmacSHA1(sign,config.SecretKey).toString(Crypto.enc.Base64)// 这也是腾讯ai需要进行加密
     wx.request({
          url: 'https://facefusion.tencentcloudapi.com',
          method: "post",
          header: { 
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          data:{
            Action:config.Action,
            Version:config.Version,
            Nonce:config.Nonce,
            SecretId:config.SecretId,
            Timestamp:config.Timestamp,
            Signature:sign_sha1,
            ProjectId:ProjectId,
            ModelId:ModelId,
            Image:data,
            RspImgType:config.RspImgType,
            Language:config.Language,
            Region:config.Region
          },
          complete(res){
    		// 处理回调
    		}
     })
    

    腾讯ai 这个接口弄起来很麻烦,因为你很容易就漏掉了一些字段或者一些字段顺序弄错的话,都是无法进行请求的,但是腾讯那边又不会返回具体的错误给你,只会告诉你sign错误,所以这里烦了我挺久的。
    后面我发现腾讯有一个签名的合成器
    在这里插入图片描述
    有了这个合成器网址,我就可以进行验证我自己的最终合成sign有没有出错。
    在这里插入图片描述
    有了这个发现我的合成的确没有错误,然后再去调用发现成功了。

2. 卡通明信片照片拖动合成

卡通明信片和毕业照的合成类似,唯一的区别在于用户的图片在背景图后面,所以需要一个蒙版提供给用户移动用户图片。
在这里插入图片描述

    <movable-area id='movable-area-edit' style="position:relative;width:625rpx;height:407rpx;margin:0 auto;">
    <!--用户图片-->
		  <movable-view direction='all' data-type='cropper' scale='true'  
        x='{{cropperImgX}}' 
        y='{{cropperImgY}}' 
				hidden='{{lifeState!==lifeStateEditImage}}'
				scale-value="{{cropperScale}}"
        style="width:{{cropperImageWidth}}px;height:{{cropperImageHeight}}px;">  
        <image src='{{cropperImageSrc}}' style="width:100%;height:100%;" />
        </movable-view>
        <movable-view style="width:100%" disable='{{false}}'> 
					<image src='{{selectBackgroundImg}}' style="width:625rpx;height:415rpx;position:absolute;" />
				</movable-view>
	<!--用户蒙版-->
        <movable-view direction='all' data-type='cropper' scale='true'  
		bindchange='bindchange'
		bindtouchend='bindchange'
		bindscale='bindchange'
		hidden='{{lifeState!==lifeStateEditImage}}'
        x='{{cropperImgX}}' 
        y='{{cropperImgY}}' 
				scale-max='2'
				scale-value="{{cropperScale}}"
        style="width:{{cropperImageWidth}}px;height:{{cropperImageHeight}}px;background:black;opacity:0.3;">   
       </movable-view>
    </movable-area> 
  1. 图片移动,用户移动蒙版会修改cropperImgXcropperImgY
  2. 图片点击结束:修改用户图片的位置与大小
  3. 双指缩放:修改移动蒙版的cropperImgXcropperImgY以及cropperScale缩放值。
    bindchange(e) {
        if (e.type === 'touchend') {
            this.setData({
                cropperImgX: this.data.cropperImgX,
                cropperImgY: this.data.cropperImgY,
                cropperScale: this.data.cropperScale
            });
        }
        else if (e.type === 'change') {
            this.data.cropperImgX = e.detail.x;
            this.data.cropperImgY = e.detail.y;
        }
        else if (e.type === 'scale'){
            this.data.cropperImgX = e.detail.x;
            this.data.cropperImgY = e.detail.y;
            this.data.cropperScale = e.detail.scale;
        }
    },

其他注意事项

base64转成本地缓存文件

由于人脸融合返回的是base64的文件,为了给用户保存,所以需要转换成文件格式。

      let file  = wx.getFileSystemManager()
      let data=  file.readFileSync(imageData,'base64')

前几次调用都是正常的,但是微信的缓存文件是有大小限制,所以如果超过内存限制就会报错。
为了解决缓存限制的问题,我们每次用户使用的时候,进行清除缓存文件。

  onLoad: async function () {
    // 清除临时文件
    let file = wx.getFileSystemManager()
    file.readdir({
     dirPath:`${wx.env.USER_DATA_PATH}`,
     success(res){
       res.files.forEach(val=>{
         try{
         // 删除
           file.unlinkSync(`${wx.env.USER_DATA_PATH}/${val}`)
         }catch(e){
           
         }
       })
     }
    })
  }

加密方法

这里使用的是cryptojs提供的api,大家要在小程序使用的话直接NPM然后把文件复制过来就可以直接使用。
引入方法:

const Crypto = require('../../utils/crypto-js/index')
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值