基於 Serverless 架構的頭像漫畫風處理小程式
阿新 • • 發佈:2022-04-11
簡介: 當一個程式設計師想要個漫畫風的頭像時...
前言
我一直都想要有一個漫畫版的頭像,奈何手太笨,用了很多軟體 “捏不出來”,所以就在想著,是否可以基於 AI 實現這樣一個功能,並部署到 Serverless 架構上讓更多人來嘗試使用呢?
後端專案
後端專案採用業界鼎鼎有名的動漫風格轉化濾鏡庫 AnimeGAN 的 v2 版本,效果大概如下:
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 專案來進行處理:
參考
完成 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="[email protected]" 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) } }) }) }
完成之後配置一下後臺介面,釋出稽核即可。
本文作者劉宇(花名:江昱)
本文為阿里雲原創內容,未經允許不得轉載。