前言

我始终都想要有一个漫画版的头像,奈何手太笨,用了很多软件 “捏不进去”,所以就在想着,是否能够基于 AI 实现这样一个性能,并部署到 Serverless 架构上让更多人来尝试应用呢?

后端我的项目

后端我的项目采纳业界鼎鼎有名的动漫格调转化滤镜库 AnimeGAN 的 v2 版本,成果大略如下:


对于这个模型的具体的信息,在这里不做具体的介绍和阐明。通过与 Python Web 框架联合,将 AI 模型通过接口对外裸露:

from PIL import Imageimport ioimport torchimport base64import bottleimport randomimport jsoncacheDir = '/tmp/'modelDir = './model/bryandlee_animegan2-pytorch_main'getModel = lambda modelName: torch.hub.load(modelDir, "generator", pretrained=modelName, source='local')models = {    'celeba_distill': getModel('celeba_distill'),    'face_paint_512_v1': getModel('face_paint_512_v1'),    'face_paint_512_v2': getModel('face_paint_512_v2'),    'paprika': getModel('paprika')}randomStr = lambda num=5: "".join(random.sample('abcdefghijklmnopqrstuvwxyz', num))face2paint = torch.hub.load(modelDir, "face2paint", size=512, source='local')@bottle.route('/images/comic_style', method='POST')def getComicStyle():    result = {}    try:        postData = json.loads(bottle.request.body.read().decode("utf-8"))        style = postData.get("style", 'celeba_distill')        image = postData.get("image")        localName = randomStr(10)        # 图片获取        imagePath = cacheDir + localName        with open(imagePath, 'wb') as f:            f.write(base64.b64decode(image))        # 内容预测        model = models[style]        imgAttr = Image.open(imagePath).convert("RGB")        outAttr = face2paint(model, imgAttr)        img_buffer = io.BytesIO()        outAttr.save(img_buffer, format='JPEG')        byte_data = img_buffer.getvalue()        img_buffer.close()        result["photo"] = 'data:image/jpg;base64, %s' % base64.b64encode(byte_data).decode()    except Exception as e:        print("ERROR: ", e)        result["error"] = True    return resultapp = bottle.default_app()if __name__ == "__main__":    bottle.run(host='localhost', port=8099)

整个代码是基于 Serverless 架构进行了局部改进的:

  1. 实例初始化的时候,进行模型的加载,曾经可能的缩小频繁的冷启动带来的影响状况;
  2. 在函数模式下,往往只有/tmp目录是可写的,所以图片会被缓存到/tmp目录下;
  3. 尽管说函数计算是“无状态”的,然而实际上也有复用的状况,所有数据在存储到tmp的时候进行了随机命名;
  4. 尽管局部云厂商反对二进制的文件上传,然而大部分的 Serverless 架构对二进制上传反对的并不敌对,所以这里仍旧采纳 Base64 上传的计划;

下面的代码,更多是和 AI 相干的,除此之外,还须要有一个获取模型列表,以及模型门路等相干信息的接口:

import bottle@bottle.route('/system/styles', method='GET')def styles():    return {      "AI动漫风": {        'color': 'red',        'detailList': {          "格调1": {            'uri': "images/comic_style",            'name': 'celeba_distill',            'color': 'orange',            'preview': 'https://serverless-article-picture.oss-cn-hangzhou.aliyuncs.com/1647773808708_20220320105649389392.png'          },          "格调2": {            'uri': "images/comic_style",            'name': 'face_paint_512_v1',            'color': 'blue',            'preview': 'https://serverless-article-picture.oss-cn-hangzhou.aliyuncs.com/1647773875279_20220320105756071508.png'          },          "格调3": {            'uri': "images/comic_style",            'name': 'face_paint_512_v2',            'color': 'pink',            'preview': 'https://serverless-article-picture.oss-cn-hangzhou.aliyuncs.com/1647773926924_20220320105847286510.png'          },          "格调4": {            'uri': "images/comic_style",            'name': 'paprika',            'color': 'cyan',            'preview': 'https://serverless-article-picture.oss-cn-hangzhou.aliyuncs.com/1647773976277_20220320105936594662.png'          },        }      },    }app = bottle.default_app()if __name__ == "__main__":    bottle.run(host='localhost', port=8099)

能够看到,此时我的做法是,新增了一个函数作为新接口对外裸露,那么为什么不在刚刚的我的项目中,减少这样的一个接口呢?而是要多保护一个函数呢?

  1. AI 模型加载速度慢,如果把获取AI解决列表的接口集成进去,势必会影响该接口的性能;
  2. AI 模型所需配置的内存会比拟多,而获取 AI 解决列表的接口所须要的内存非常少,而内存会和计费有肯定的关系,所以离开有助于老本的升高;

对于第二个接口(获取 AI 解决列表的接口),相对来说是比较简单的,没什么问题,然而针对第一个 AI 模型的接口,就有比拟头疼的点:

  1. 模型所须要的依赖,可能波及到一些二进制编译的过程,所以导致无奈间接跨平台应用;
  2. 模型文件比拟大 (单纯的 Pytorch 就超过 800M),函数计算的上传代码最多才 100M,所以这个我的项目无奈间接上传;

所以这里须要借助 Serverless Devs 我的项目来进行解决:

参考 https://www.serverless-devs.c... 实现 s.yaml 的编写:

edition: 1.0.0name: start-aiaccess: "default"vars: # 全局变量  region: cn-hangzhou  service:    name: ai    nasConfig:                  # NAS配置, 配置后function能够拜访指定NAS      userId: 10003             # userID, 默认为10003      groupId: 10003            # groupID, 默认为10003      mountPoints:              # 目录配置        - serverAddr: 0fe764bf9d-kci94.cn-hangzhou.nas.aliyuncs.com # NAS 服务器地址          nasDir: /python3          fcDir: /mnt/python3    vpcConfig:      vpcId: vpc-bp1rmyncqxoagiyqnbcxk      securityGroupId: sg-bp1dpxwusntfryekord6      vswitchIds:        - vsw-bp1wqgi5lptlmk8nk5yi0services:  image:    component:  fc    props: #  组件的属性值      region: ${vars.region}      service: ${vars.service}      function:        name: image_server        description: 图片解决服务        runtime: python3        codeUri: ./        ossBucket: temp-code-cn-hangzhou        handler: index.app        memorySize: 3072        timeout: 300        environmentVariables:          PYTHONUSERBASE: /mnt/python3/python      triggers:        - name: httpTrigger          type: http          config:            authType: anonymous            methods:              - GET              - POST              - PUT      customDomains:        - domainName: avatar.aialbum.net          protocol: HTTP          routeConfigs:            - path: /*

而后进行:

1、依赖的装置:s build --use-docker
2、我的项目的部署:s deploy
3、在 NAS 中创立目录,上传依赖:

s nas command mkdir /mnt/python3/pythons nas upload -r 本地依赖门路 /mnt/python3/python

实现之后能够通过接口对我的项目进行测试。

另外,微信小程序须要 https 的后盾接口,所以这里还须要配置 https 相干的证书信息,此处不做开展。

小程序我的项目

小程序我的项目仍旧采纳 colorUi,整个我的项目就只有一个页面:

页面相干布局:

<scroll-view scroll-y class="scrollPage">  <image src='/images/topbg.jpg' mode='widthFix' class='response'></image>  <view class="cu-bar bg-white solid-bottom margin-top">    <view class="action">      <text class="cuIcon-title text-blue"></text>第一步:抉择图片    </view>  </view>  <view class="padding bg-white solid-bottom">    <view class="flex">      <view class="flex-sub bg-grey padding-sm margin-xs radius text-center" bindtap="chosePhoto">本地上传图片</view>      <view class="flex-sub bg-grey padding-sm margin-xs radius text-center" bindtap="getUserAvatar">获取以后头像</view>    </view>  </view>  <view class="padding bg-white" hidden="{{!userChosePhoho}}">    <view class="images">      <image src="{{userChosePhoho}}" mode="widthFix" bindtap="previewImage" bindlongpress="editImage" data-image="{{userChosePhoho}}"></image>    </view>    <view class="text-right padding-top text-gray">* 点击图片可预览,长按图片可编辑</view>  </view>  <view class="cu-bar bg-white solid-bottom margin-top">    <view class="action">      <text class="cuIcon-title text-blue"></text>第二步:抉择图片解决计划    </view>  </view>  <view class="bg-white">    <scroll-view scroll-x class="bg-white nav">      <view class="flex text-center">        <view class="cu-item flex-sub {{style==currentStyle?'text-orange cur':''}}" wx:for="{{styleList}}"          wx:for-index="style" bindtap="changeStyle" data-style="{{style}}">          {{style}}        </view>      </view>    </scroll-view>  </view>  <view class="padding-sm bg-white solid-bottom">    <view class="cu-avatar round xl bg-{{item.color}} margin-xs" wx:for="{{styleList[currentStyle].detailList}}"      wx:for-index="substyle" bindtap="changeStyle" data-substyle="{{substyle}}" bindlongpress="showModal" data-target="Image">       <view class="cu-tag badge cuIcon-check bg-grey" hidden="{{currentSubStyle == substyle ? false : true}}"></view>      <text class="avatar-text">{{substyle}}</text>    </view>    <view class="text-right padding-top text-gray">* 长按格调圆圈能够预览模板成果</view>  </view>  <view class="padding-sm bg-white solid-bottom">    <button class="cu-btn block bg-blue margin-tb-sm lg" bindtap="getNewPhoto" disabled="{{!userChosePhoho}}"      type="">{{ userChosePhoho ? (getPhotoStatus ? 'AI将破费较长时间' : '生成图片') : '请先抉择图片' }}</button>  </view>  <view class="cu-bar bg-white solid-bottom margin-top" hidden="{{!resultPhoto}}">    <view class="action">      <text class="cuIcon-title text-blue"></text>生成后果    </view>  </view>  <view class="padding-sm bg-white solid-bottom" hidden="{{!resultPhoto}}">    <view wx:if="{{resultPhoto == 'error'}}">      <view class="text-center padding-top">服务临时不可用,请稍后重试</view>      <view class="text-center padding-top">或分割开发者微信:<text class="text-blue" data-data="zhihuiyushaiqi" bindtap="copyData">zhihuiyushaiqi</text></view>    </view>    <view wx:else>      <view class="images">        <image src="{{resultPhoto}}" mode="aspectFit" bindtap="previewImage" bindlongpress="saveImage" data-image="{{resultPhoto}}"></image>      </view>      <view class="text-right padding-top text-gray">* 点击图片可预览,长按图片可保留</view>    </view>  </view>  <view class="padding bg-white margin-top margin-bottom">    <view class="text-center">骄傲的采纳 Serverless Devs 搭建</view>    <view class="text-center">Powered By Anycodes <text bindtap="showModal" class="text-cyan" data-target="Modal">{{"<"}}作者的话{{">"}}</text></view>  </view>  <view class="cu-modal {{modalName=='Modal'?'show':''}}">  <view class="cu-dialog">    <view class="cu-bar bg-white justify-end">      <view class="content">作者的话</view>      <view class="action" bindtap="hideModal">        <text class="cuIcon-close text-red"></text>      </view>    </view>    <view class="padding-xl text-left">      大家好,我是刘宇,很感谢您能够关注和应用这个小程序,这个小程序是我用业余时间做的一个头像生成小工具,基于“人工智障”技术,反正当初怎么看怎么顺当,然而我会致力让这小程序变得“智能”起来的。如果你有什么好的意见也欢送分割我<text class="text-blue" data-data="service@52exe.cn" bindtap="copyData">邮箱</text>或者<text class="text-blue" data-data="zhihuiyushaiqi" bindtap="copyData">微信</text>,另外值得一提的是,本我的项目基于阿里云Serverless架构,通过Serverless Devs开发者工具建设。    </view>  </view></view><view class="cu-modal {{modalName=='Image'?'show':''}}">  <view class="cu-dialog">    <view class="bg-img" style="background-image: url("{{previewStyle}}");height:200px;">      <view class="cu-bar justify-end text-white">        <view class="action" bindtap="hideModal">          <text class="cuIcon-close "></text>        </view>      </view>    </view>    <view class="cu-bar bg-white">      <view class="action margin-0 flex-sub  solid-left" bindtap="hideModal">敞开预览</view>    </view>  </view></view></scroll-view>

页面逻辑也是比较简单的:

// index.js// 获取利用实例const app = getApp()Page({  data: {    styleList: {},    currentStyle: "动漫风",    currentSubStyle: "v1模型",    userChosePhoho: undefined,    resultPhoto: undefined,    previewStyle: undefined,    getPhotoStatus: false  },  // 事件处理函数  bindViewTap() {    wx.navigateTo({      url: '../logs/logs'    })  },  onLoad() {    const that = this    wx.showLoading({      title: '加载中',    })    app.doRequest(`system/styles`, {}, option = {      method: "GET"    }).then(function (result) {      wx.hideLoading()      that.setData({        styleList: result,        currentStyle: Object.keys(result)[0],        currentSubStyle: Object.keys(result[Object.keys(result)[0]].detailList)[0],      })    })  },  changeStyle(attr) {    this.setData({      "currentStyle": attr.currentTarget.dataset.style || this.data.currentStyle,      "currentSubStyle": attr.currentTarget.dataset.substyle || Object.keys(this.data.styleList[attr.currentTarget.dataset.style].detailList)[0]    })  },  chosePhoto() {    const that = this    wx.chooseImage({      count: 1,      sizeType: ['compressed'],      sourceType: ['album', 'camera'],      complete(res) {        that.setData({          userChosePhoho: res.tempFilePaths[0],          resultPhoto: undefined        })      }    })  },  headimgHD(imageUrl) {    imageUrl = imageUrl.split('/'); //把头像的门路切成数组    //把大小数值为 46 || 64 || 96 || 132 的转换为0    if (imageUrl[imageUrl.length - 1] && (imageUrl[imageUrl.length - 1] == 46 || imageUrl[imageUrl.length - 1] == 64 || imageUrl[imageUrl.length - 1] == 96 || imageUrl[imageUrl.length - 1] == 132)) {      imageUrl[imageUrl.length - 1] = 0;    }    imageUrl = imageUrl.join('/'); //从新拼接为字符串    return imageUrl;  },  getUserAvatar() {    const that = this    wx.getUserProfile({      desc: "获取您的头像",      success(res) {        const newAvatar = that.headimgHD(res.userInfo.avatarUrl)        wx.getImageInfo({          src: newAvatar,          success(res) {            that.setData({                    userChosePhoho: res.path,                    resultPhoto: undefined                  })          }        })      }    })  },  previewImage(e) {    wx.previewImage({      urls: [e.currentTarget.dataset.image]    })  },  editImage() {    const that = this    wx.editImage({      src: this.data.userChosePhoho,      success(res) {        that.setData({          userChosePhoho: res.tempFilePath        })      }    })  },  getNewPhoto() {    const that = this    wx.showLoading({      title: '图片生成中',    })    this.setData({      getPhotoStatus: true    })    app.doRequest(this.data.styleList[this.data.currentStyle].detailList[this.data.currentSubStyle].uri, {      style: this.data.styleList[this.data.currentStyle].detailList[this.data.currentSubStyle].name,      image: wx.getFileSystemManager().readFileSync(this.data.userChosePhoho, "base64")    }, option = {      method: "POST"    }).then(function (result) {      wx.hideLoading()      that.setData({        resultPhoto: result.error ? "error" : result.photo,        getPhotoStatus: false      })    })  },  saveImage() {    wx.saveImageToPhotosAlbum({      filePath: this.data.resultPhoto,      success(res) {        wx.showToast({          title: "保留胜利"        })      },      fail(res) {        wx.showToast({          title: "异样,稍后重试"        })      }    })  },  onShareAppMessage: function () {    return {      title: "有条有理个性头像",    }  },  onShareTimeline() {    return {      title: "有条有理个性头像",    }  },  showModal(e) {    if(e.currentTarget.dataset.target=="Image"){      const previewSubStyle = e.currentTarget.dataset.substyle      const previewSubStyleUrl = this.data.styleList[this.data.currentStyle].detailList[previewSubStyle].preview      if(previewSubStyleUrl){        this.setData({          previewStyle: previewSubStyleUrl        })      }else{        wx.showToast({          title: "暂无模板预览",          icon: "error"        })        return       }    }    this.setData({      modalName: e.currentTarget.dataset.target    })  },  hideModal(e) {    this.setData({      modalName: null    })  },  copyData(e) {    wx.setClipboardData({      data: e.currentTarget.dataset.data,      success(res) {        wx.showModal({          title: '复制实现',          content: `已将${e.currentTarget.dataset.data}复制到了剪切板`,        })      }    })  },})

因为我的项目会申请比拟屡次的后盾接口,所以,我将申请办法进行额定的形象:

// 对立申请接口  doRequest: async function (uri, data, option) {    const that = this    return new Promise((resolve, reject) => {      wx.request({        url: that.url + uri,        data: data,        header: {          "Content-Type": 'application/json',        },        method: option && option.method ? option.method : "POST",        success: function (res) {          resolve(res.data)        },        fail: function (res) {          reject(null)        }      })    })  }

实现之后配置一下后盾接口,公布审核即可。