共计 11810 个字符,预计需要花费 30 分钟才能阅读完成。
前言
我始终都想要有一个漫画版的头像,奈何手太笨,用了很多软件“捏不进去”,所以就在想着,是否能够基于 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 架构进行了局部改进的:
- 实例初始化的时候,进行模型的加载,曾经可能的缩小频繁的冷启动带来的影响状况;
- 在函数模式下,往往只有 /tmp 目录是可写的,所以图片会被缓存到 /tmp 目录下;
- 尽管说函数计算是“无状态”的,然而实际上也有复用的状况,所有数据在存储到 tmp 的时候进行了随机命名;
- 尽管局部云厂商反对二进制的文件上传,然而大部分的 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)
能够看到,此时我的做法是,新增了一个函数作为新接口对外裸露,那么为什么不在刚刚的我的项目中,减少这样的一个接口呢?而是要多保护一个函数呢?
- AI 模型加载速度慢,如果把获取 AI 解决列表的接口集成进去,势必会影响该接口的性能;
- AI 模型所需配置的内存会比拟多,而获取 AI 解决列表的接口所须要的内存非常少,而内存会和计费有肯定的关系,所以离开有助于老本的升高;
对于第二个接口(获取 AI 解决列表的接口),相对来说是比较简单的,没什么问题,然而针对第一个 AI 模型的接口,就有比拟头疼的点:
- 模型所须要的依赖,可能波及到一些二进制编译的过程,所以导致无奈间接跨平台应用;
- 模型文件比拟大(单纯的 Pytorch 就超过 800M),函数计算的上传代码最多才 100M,所以这个我的项目无奈间接上传;
所以这里须要借助 Serverless Devs 我的项目来进行解决:
参考 https://www.serverless-devs.c… 实现 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)
}
})
})
}
实现之后配置一下后盾接口,公布审核即可。