基于 Serverless 架构的头像漫画风处理小程序

Posted 阿里云开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Serverless 架构的头像漫画风处理小程序相关的知识,希望对你有一定的参考价值。

简介:当一个程序员想要个漫画风的头像时...

​<关注 Serverless 公众号后台回复 手册 免费获取 2022 Serverless工具书>


前言


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

后端项目

后端项目采用业界鼎鼎有名的动漫风格转化滤镜库 AnimeGAN 的 v2 版本,效果大概如下:


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


from PIL import Image
import io
import torch
import base64
import bottle
import random
import json
cacheDir = '/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 result
app = 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.com/fc/yaml/readme


完成s.yaml的编写:

edition: 1.0.0
name: start-ai
access: "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-bp1wqgi5lptlmk8nk5yi0
services:
  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/python
s 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
        </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-sub 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" 
       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)
        
      )
    )
  


完成之后配置一下后台接口,发布审核即可。


本文作者刘宇(花名:江昱)

更多精彩内容可关注 Serverless 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。

原文链接:https://developer.aliyun.com/article/882384?

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。 以上是关于基于 Serverless 架构的头像漫画风处理小程序的主要内容,如果未能解决你的问题,请参考以下文章

基于 Serverless 架构的头像漫画风处理小程序

基于 Serverless 架构的编程学习小工具

基于 Serverless 架构的编程学习小工具

轻松构建基于 Serverless 架构的小程序

轻松构建基于 Serverless 架构的小程序

基于 K8s 的 Serverless 架构搭建