畢業禮物——“廣工雲畢業”小程序開發

項目介紹

由於疫情的影響,我們學校無法進行正常的畢業照流程,所以學院找到了我,希望能夠開發一款自動合成的畢業照的小程序。

解決方案

  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')
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章