基于 Serverless 架构的头像漫画风处理小程序
后台回复 手册 即刻免费下载 2022 Serverless 工具书
我一直都想要有一个漫画版的头像,奈何手太笨,用了很多软件 “捏不出来”,所以就在想着,是否可以基于 AI 实现这个功能,并部署到 Serverless 架构上让更多人来尝试使用呢?
项目开发
「01」
后端项目
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)
实例初始化的时候,进行模型的加载,已经可能的减少频繁的冷启动带来的影响情况; 在函数模式下,往往只有
/tmp
目录是可写的,所以图片会被缓存到/tmp
目录下;虽然说函数计算是“无状态”的,但是实际上也有复用的情况,所有数据在存储到
tmp
的时候进行了随机命名;虽然部分云厂商支持二进制的文件上传,但是大部分的 Serverless 架构对二进制上传支持的并不友好,所以这里依旧采用 Base64 上传的方案;
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)
AI 模型加载速度慢,如果把获取AI处理列表的接口集成进去,势必会影响该接口的性能; AI 模型所需配置的内存会比较多,而获取 AI 处理列表的接口所需要的内存非常少,而内存会和计费有一定的关系,所以分开有助于成本的降低;
模型所需要的依赖,可能涉及到一些二进制编译的过程,所以导致无法直接跨平台使用; 模型文件比较大 (单纯的 Pytorch 就超过 800M),函数计算的上传代码最多才 100M,所以这个项目无法直接上传;
请参考:
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: /*
然后进行:
s build --use-docker
s deploy
s nas command mkdir /mnt/python3/python
s nas upload -r 本地依赖路径 /mnt/python3/python
另外,微信小程序需要 https 的后台接口,所以这里还需要配置 https 相关的证书信息,此处不做展开。
「02」
小程序项目
<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)
}
})
})
}
完成之后配置一下后台接口,发布审核即可。
项目体验
RECRUITMENT
极速上手 Serverless
随着 Serverless 热度不断升高,越来越多人期望在实际工作中能快速上手。为了让更多 Serverless 初学者真正学会 Serverless 理论知识,在工作中根据需要灵活应用 Serverless 技术,阿里云 Serverless 团队推出技术图谱,本课程包含机频、动手实验、电子书、直播、开源项目多种形式内容,让各位开发者即学即用,跑步入场享受 Serverless 技术红利!点击“阅读原文”即刻学习。
评论