作者:劉宇(花名:江昱)
前言
我一直都想要有一個漫畫版的頭像,奈何手太笨,用了很多軟體 “捏不出來”,所以就在想著,是否可以基于 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 = https://www.cnblogs.com/alisystemsoftware/p/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 = https://www.cnblogs.com/alisystemsoftware/p/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處理串列的介面集成進去,勢必會影響該介面的性能;
2.AI 模型所需配置的記憶體會比較多,而獲取 AI 處理串列的介面所需要的記憶體非常少,而記憶體會和計費有一定的關系,所以分開有助于成本的降低;
關于第二個介面(獲取 AI 處理串列的介面),相對來說是比較簡單的,沒什么問題,但是針對第一個 AI 模型的介面,就有比較頭疼的點:
- 模型所需要的依賴,可能涉及到一些二進制編譯的程序,所以導致無法直接跨平臺使用;
- 模型檔案比較大 (單純的 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 >
<image src='https://img.uj5u.com/2022/04/22/308314220800403.jpg' mode='widthFix' class='response'></image>
<view >
<view >
<text ></text>第一步:選擇圖片
</view>
</view>
<view >
<view >
<view bindtap="chosePhoto">本地上傳圖片</view>
<view bindtap="getUserAvatar">獲取當前頭像</view>
</view>
</view>
<view hidden="{{!userChosePhoho}}">
<view >
<image src="https://www.cnblogs.com/alisystemsoftware/p/{{userChosePhoho}}" mode="widthFix" bindtap="previewImage" bindlongpress="editImage" data-image="{{userChosePhoho}}"></image>
</view>
<view >* 點擊圖片可預覽,長按圖片可編輯</view>
</view>
<view >
<view >
<text ></text>第二步:選擇圖片處理方案
</view>
</view>
<view >
<scroll-view scroll-x >
<view >
<view wx:for="{{styleList}}"
wx:for-index="style" bindtap="changeStyle" data-style="{{style}}">
{{style}}
</view>
</view>
</scroll-view>
</view>
<view >
<view wx:for="{{styleList[currentStyle].detailList}}"
wx:for-index="substyle" bindtap="changeStyle" data-substyle="{{substyle}}" bindlongpress="showModal" data-target="Image">
<view hidden="{{currentSubStyle == substyle ? false : true}}"></view>
<text >{{substyle}}</text>
</view>
<view >* 長按風格圓圈可以預覽模板效果</view>
</view>
<view >
<button bindtap="getNewPhoto" disabled="{{!userChosePhoho}}"
type="">{{ userChosePhoho ? (getPhotoStatus ? 'AI將花費較長時間' : '生成圖片') : '請先選擇圖片' }}</button>
</view>
<view hidden="{{!resultPhoto}}">
<view >
<text ></text>生成結果
</view>
</view>
<view hidden="{{!resultPhoto}}">
<view wx:if="{{resultPhoto == 'error'}}">
<view >服務暫時不可用,請稍后重試</view>
<view >或聯系開發者微信:<text data-data="https://www.cnblogs.com/alisystemsoftware/p/zhihuiyushaiqi" bindtap="copyData">zhihuiyushaiqi</text></view>
</view>
<view wx:else>
<view >
<image src="https://www.cnblogs.com/alisystemsoftware/p/{{resultPhoto}}" mode="aspectFit" bindtap="previewImage" bindlongpress="saveImage" data-image="{{resultPhoto}}"></image>
</view>
<view >* 點擊圖片可預覽,長按圖片可保存</view>
</view>
</view>
<view >
<view >自豪的采用 Serverless Devs 搭建</view>
<view >Powered By Anycodes <text bindtap="showModal" data-target="Modal">{{"<"}}作者的話{{">"}}</text></view>
</view>
<view >
<view >
<view >
<view >作者的話</view>
<view bindtap="hideModal">
<text ></text>
</view>
</view>
<view >
大家好,我是劉宇,很感謝您可以關注和使用這個小程式,這個小程式是我用業余時間做的一個頭像生成小工具,基于“人工智障”技術,反正現在怎么看怎么別扭,但是我會努力讓這小程式變得“智能”起來的,如果你有什么好的意見也歡迎聯系我<text data-data="https://www.cnblogs.com/alisystemsoftware/p/[email protected]" bindtap="copyData">郵箱</text>或者<text data-data="https://www.cnblogs.com/alisystemsoftware/p/zhihuiyushaiqi" bindtap="copyData">微信</text>,另外值得一提的是,本專案基于阿里云Serverless架構,通過Serverless Devs開發者工具建設,
</view>
</view>
</view>
<view >
<view >
<view style="background-image: url("{{previewStyle}}");height:200px;">
<view >
<view bindtap="hideModal">
<text ></text>
</view>
</view>
</view>
<view >
<view 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'
})
},
onl oad() {
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)
}
})
})
}
完成之后配置一下后臺介面,發布審核即可,
發布云原生技術最新資訊、匯集云原生技術最全內容,定期舉辦云原生活動、直播,阿里產品及用戶最佳實踐發布,與你并肩探索云原生技術點滴,分享你需要的云原生內容,
關注【阿里巴巴云原生】公眾號,獲取更多云原生實時資訊!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/460806.html
標籤:其他
上一篇:力扣55. 跳躍游戲
