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

Posted Serverless

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 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="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)

)
)

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

以上是关于基于 Serverless 架构的头像漫画风处理小程序的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

基于 K8s 的 Serverless 架构搭建