• 微信小程序canvas 证件照制作


    小程序制作证件照过程

    利用canvas制作生活中常用的证件照,压缩图片,修改图片dpi。希望给大家带来方便。

    证件照小程序制作要点

    1. 上传合适的图片,方便制作证件照
    2. 调用AI接口,将图像进行人像分割。这里我用的是百度AI
    3. 调用人体关键点为分析图片中头部和肩部的位置信息。为后满裁剪图片提供依据
    4. 利用canvas 将头部和肩部位置制作为新的证件照尺寸照片
    5. 改变图片的背景颜色,生成不同要求的背景证件照
    6. 导出图品前将图片修改为符合打印要求的dpi。
    7. 下载最终生成好的证件照

    上传合适的图片,方便制作证件照

    selectImg(selectid){
      let _this = this
      let typelist = selectid === 1 ? ['camera'] : ['album']
      uni.chooseImage({
        count: 1,
        sourceType: typelist,
        success: (res)=> {
          
        }
      });
    }
    

    调用AI接口,把图像进行人像分割,分析图像中头部肩部位置信息

    1. [参考链接地址] https://cloud.baidu.com/doc/BODY/s/Fk3cpyxua
      参考图片

    2. 该接口中要求上传的图片格式为base64 格式,大小不超过4M. 并且需要access_token
      参考图片

    3. 获取access_token 参照百度AI 的文档 https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjhhu
      参考图片

    4. 定义好请求地址。和请求的请求方法

    const baseUrl = 'https://picapp.gxwj123.top/prod-api/'
    const baidubce = 'https://aip.baidubce.com/rest/2.0/image-classify/v1/'
    export const tokenUrl = `${baseUrl}txy/zjz/token`
    export const body_seg_url = `${baidubce}body_seg?access_token=`
    export const body_analysis_url = `${baidubce}body_analysis?access_token=`
    
    import {tokenUrl, body_seg_url, body_analysis_url} from './url.js'
    export const request = async (url) => {
      let header = {
        'Content-Type': 'application/json',
      };
    	let result = await new Promise((resolve, reject) => {
    		uni.request({
          url: url,
          method: 'post',
          header: header,
          success(res) {
            if (res.statusCode == 200 && res.data.code == 200) {
              resolve(res.data.data);
            }
          },
          fail(err) {
            reject(err);
          }
        });
      });
      return result
    };
    export const baiduRequest = async (url, data) => {
      let header = {
        'Content-Type': 'application/x-www-form-urlencoded',
      };
    	let result = await new Promise((resolve, reject) => {
    		uni.request({
          url: url,
          method: 'post',
          header: header,
          data: {
            image: data.image
          },
          success(res) {
            resolve(res);
          },
          fail(err) {
            reject(err);
          }
        });
      });
      return result
    };
    export const getAccessToken = (data) => {
      return request(tokenUrl, data,)
    }
    export const body_seg = (data) => {
      let url = `${body_seg_url}${data.access_token}`;
      return baiduRequest(url, data)
    }
    export const body_analysis = (data) => {
      let url = `${body_analysis_url}${data.access_token}`;
      return baiduRequest(url, data)
    }
    
    export const getImageInfos = (data) => {
      return new Promise((resolve, reject) => {
        Promise.all([body_seg(data), body_analysis(data)]).then(([seg, analysis]) => {
          console.log(seg, analysis)
          if (seg.statusCode == 200 && analysis.statusCode == 200) {
            let data = {
              bodySeg: seg.data,
              bodyAns: analysis.data
            }
            resolve(data)
          }else {
            reject('请求任务出错')
          }
        })
      })
    }
    
    1. 上传的图片格式调整为base64
    toBase64(file) {
      let _this = this
      uni.getFileSystemManager().readFile({
          filePath: file, //选择图片返回的相对路径
          encoding: 'base64', //编码格式
          success: res => {
              // 成功的回调
              // 'data:image/jpeg;base64,'
              let base64 = res.data;
              _this.getImgInfos(base64)
          }
      });
    },
    

    将人像分割接口返回的图片和人体位置信息分析的坐标结合。生成用于制作证件照的素材。下面的将使用1寸证件照的尺寸和dpi 来进行分析。

    1. 从位置信息分析接口中取出要使用的位置,比如头部,肩部。人像分析中取foreground,为去掉原图中人物信息以外的图片
    initImgData(bodyAns,bodySeg) {
      if (bodyAns.person_num > 1) {
        uni.showToast({
          title: '图片检测到多个人像,请重新上传',
          icon:'none',
          duration: 2000
        });
        return
      }
      if (bodyAns.person_num == 0) {
        uni.showToast({
          title: '图片未检测到人像,请重新上传',
          icon:'none',
          duration: 2000
        });
        return
      }
      let widthInfo = bodyAns.person_info[0]
      let location = this.imgwidthsum(widthInfo)
      this.location = location
      let foreground = bodySeg.foreground
      this.foreground = foreground
      this.previewImg('data:image/png;base64,' + foreground, location).then(filePath => {
        this.canvasImages = filePath
        this.buildOver = true
      })
    },
    imgwidthsum(data) {
      let body_parts = data.body_parts
      return {
        top_head: body_parts.top_head, 
        left_shoulder: body_parts.left_shoulder,
        right_shoulder: body_parts.right_shoulder
      }
    },
    
    1. 使用uni.getImageInfo 读取图片,需要先将上一步中base64d 图片转为本地图片
    const fsm = wx.getFileSystemManager();
    const FILE_BASE_NAME = 'tmp_base64src';
    
    const base64src = function(base64data, pathName) {
      return new Promise((resolve, reject) => {
        const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
        if (!format) {
          reject(new Error('ERROR_BASE64SRC_PARSE'));
        }
        const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME+pathName}.${format}`;
        const buffer = wx.base64ToArrayBuffer(bodyData);
        fsm.writeFile({
          filePath,
          data: buffer,
          encoding: 'binary',
          success() {
            resolve(filePath);
          },
          fail() {
            reject(new Error('ERROR_BASE64SRC_WRITE'));
          },
        });
      });
    };
    
    export default base64src;
    
    1. 将图片按照要一定的比列绘制在canvas 中
    let IMG_RATIO
    let ratio = 295/413
    let initWidth = 295
    let initHeight = 413
    let scrollTop = 250
    
    let IMG_REAL_W,IMG_REAL_H
    IMG_REAL_H = initHeight
    IMG_REAL_W = IMG_REAL_H*IMG_RATIO
    let canH = imgW * IMG_REAL_H / IMG_REAL_W
    const ctx = uni.createCanvasContext("myselfCanvas", _this);
    if (color) {
      ctx.setFillStyle(color)
      ctx.fillRect(0,0,IMG_REAL_W,IMG_REAL_H)
    }
    // 绘制的时候将选中的背景颜色填充到画布中
    ctx.drawImage(res.path, 0, 0, IMG_REAL_W, IMG_REAL_H);
    
    1. 根据原图中头像位置坐标。换算出需要在原图上裁剪出来的区域
    let x = location.right_shoulder.x //右肩位置的坐标 x
    let y = location.top_head.y - scrollTop // 头部坐标位置 减去一定比列的坐标 y 
    let x1 = location.left_shoulder.x // 左肩位置坐标 x
    
    var canvasW = ((x1 - x) / imgW) * IMG_REAL_W;
    // 左肩坐标 减去右肩坐标 和原图的宽度比列 计算出 在上一步绘制的图中裁剪的宽度
    var canvasH = canvasW/ratio // 根据证件照的比列 计算出 裁剪的高度
    var canvasL = (x / imgW) * IMG_REAL_W;
    var canvasT = (y / imgH) * IMG_REAL_H;
    // 计算裁剪的起始坐标位置
    
    1. 在canvas 绘制图后导出证件照需要的尺寸
    ctx.draw(false,(ret)=>{						 
      uni.showToast({
        icon:'success',
        mask:true,
        title: '绘制完成',
      });
      
      uni.canvasToTempFilePath({ // 保存canvas为图片
        x: canvasL,
        y: canvasT,
        width: canvasW, //canvasH,
        height: canvasH, //canvasH,
        destWidth: initWidth,
        destHeight: initHeight,
        canvasId: 'myselfCanvas',
        quality: 1,
        fileType: color? 'jpg': 'png',
        complete: function(res) {
          resolve(res.tempFilePath)	   
        } ,
      })
    });
    

    导出证件照之前,还需要修改图片的dpi

    1. 修改图片dpi 是将图片转为base64 格式后修改。本项目使用changedpi 插件
    2. npm install changedpi
    import {changeDpiDataUrl} from 'changedpi'
    export const changeDpi = (url, dpi) => {
      return new Promise((resolve) => {
        if (dpi) {
          uni.getFileSystemManager().readFile({
              filePath: url, //选择图片返回的相对路径
              encoding: 'base64', //编码格式
              success: res => {
                  // 成功的回调
                  // 'data:image/jpeg;base64,'
                  
                  let base64 = res.data;
                  let str = changeDpiDataUrl('data:image/jpeg;base64,' + base64, dpi)
                  base64src(str).then(filePath => {
      
                    resolve(filePath)
                  })
              }
          });
        }else {
          resolve(url)
        }
      })
    }
    
    1. 在小程序中使用需要注意 插件中直接使用了btoa atob 两个函数。 但是在小程序是不支持直接调用的。需要重写这两个方法
    2. 重写的方法
    (function(f) {
    
      'use strict';
    
      /* istanbul ignore else */
      if (typeof exports === 'object' && exports != null &&
          typeof exports.nodeType !== 'number') {
        module.exports = f ();
      } else if (typeof define === 'function' && define.amd != null) {
        define ([], f);
      } else {
        var base64 = f ();
        var global = typeof self !== 'undefined' ? self : $.global;
        if (typeof global.btoa !== 'function') global.btoa = base64.btoa;
        if (typeof global.atob !== 'function') global.atob = base64.atob;
      }
    
    } (function() {
    
      'use strict';
    
      var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
    
      function InvalidCharacterError(message) {
        this.message = message;
      }
      InvalidCharacterError.prototype = new Error ();
      InvalidCharacterError.prototype.name = 'InvalidCharacterError';
    
      // encoder
      // [https://gist.github.com/999166] by [https://github.com/nignag]
      function btoa(input) {
        var str = String (input);
        for (
          // initialize result and counter
          var block, charCode, idx = 0, map = chars, output = '';
          // if the next str index does not exist:
          //   change the mapping table to "="
          //   check if d has no fractional digits
          str.charAt (idx | 0) || (map = '=', idx % 1);
          // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
          output += map.charAt (63 & block >> 8 - idx % 1 * 8)
        ) {
          charCode = str.charCodeAt (idx += 3 / 4);
          if (charCode > 0xFF) {
            throw new InvalidCharacterError ("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
          }
          block = block << 8 | charCode;
        }
        return output;
      }
    
      // decoder
      // [https://gist.github.com/1020396] by [https://github.com/atk]
      function atob(input) {
        var str = (String (input)).replace (/[=]+$/, ''); // #31: ExtendScript bad parse of /=
        // if (str.length % 4 === 1) {
        //   throw new InvalidCharacterError ("'atob' failed: The string to be decoded is not correctly encoded.");
        // }
        for (
          // initialize result and counters
          var bc = 0, bs, buffer, idx = 0, output = '';
          // get next character
          buffer = str.charAt (idx++); // eslint-disable-line no-cond-assign
          // character found in table? initialize bit storage and add its ascii value;
          ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
            // and if not first of each 4 characters,
            // convert the first 8 bits to one ascii character
            bc++ % 4) ? output += String.fromCharCode (255 & bs >> (-2 * bc & 6)) : 0
        ) {
          // try to find character in table (0-63, not found => -1)
          buffer = chars.indexOf (buffer);
        }
        return output;
      }
    
      return {btoa: btoa, atob: atob};
    
    }));
    
    1. 在源码中修改调用
    const polyfill = require('../../../util/btoa.js');
    const {btoa, atob} = polyfill;
    

    下载证件照到手机相册

    export const savePoster = (url) => {
      const that = this
      wx.saveImageToPhotosAlbum({
          filePath: url,
          success: function() {
              wx.showToast({
                  title: '保存成功',
                  icon: 'none',
                  duration: 1500
              });
          },
          fail(err) {
            if (err.errMsg === "saveImageToPhotosAlbum:fail:auth denied" || err.errMsg === "saveImageToPhotosAlbum:fail auth deny" || err.errMsg === "saveImageToPhotosAlbum:fail authorize no response") {
              wx.showModal({
                title: '提示',
                content: '需要您授权保存相册',
                showCancel: false,
                success: modalSuccess => {
                  wx.openSetting({
                    success(settingdata) {
                      if (settingdata.authSetting['scope.writePhotosAlbum']) {
                          wx.saveImageToPhotosAlbum({
                              filePath: url,
                              success: function () {
                                wx.showToast({
                                  title: '保存成功',
                                  icon: 'success',
                                  duration: 2000
                                })
                              },
                          })
                      } else {
                          wx.showToast({
                              title: '授权失败,请稍后重新获取',
                              icon: 'none',
                              duration: 1500
                          });
                      }
                    }
                  })
                }
              })
            }
          }
        })
    }
    
    

    下面是利用canvas 做的小应用,欢迎大家扫描体验,并提出建议。让我们共同进步

    制作头像小程序

    [项目代码] https://gitee.com/eyes-star/txy-openmp.git
    制作头像小程序

    [项目代码] https://gitee.com/eyes-star/zjz-openmp.git

  • 相关阅读:
    说一说 Backscatter communication
    PDF怎么转换成PPT
    【matlab网络通信】tcpserver参数详解
    jarvisoj_level3_x64
    QGis软件 —— 9、QGis - 由点绘制热力图来模拟人流量(绘制点方式、excel导入数据方式)
    每日小题打卡
    Spring 使用指南 ~ 1、Spring 的 IOC 和 DI 简介
    深入解析OLED透明屏的工作原理与优势,智能家居的未来之选
    Vue项目后台部分5,ECharts,Home首页的制作,权限管理的菜单权限的相关逻辑
    用户端APP自动化测试_L2
  • 原文地址:https://www.cnblogs.com/buxiugangzi/p/16870441.html