简介:当一个程序员想要个漫画风的头像时...
<关注 Serverless 公众号后台回复 手册 免费获取 2022 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 架构进行了部分改良的:
上面的代码,更多是和 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 模型的接口,就有比较头疼的点:
所以这里需要借助 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,整个项目就只有一个页面:
页面相关布局:
第一步:选择图片 本地上传图片 获取当前头像 * 点击图片可预览,长按图片可编辑 第二步:选择图片处理方案 wx:for-index="style" bindtap="changeStyle" data-> { {style}} wx:for-index="substyle" bindtap="changeStyle" data-sub bindlongpress="showModal" data-target="Image"> { {substyle}} * 长按风格圆圈可以预览模板效果 type="">{ { userChosePhoho ? (getPhotoStatus ? 'AI将花费较长时间' : '生成图片') : '请先选择图片' }} 生成结果 服务暂时不可用,请稍后重试 或联系开发者微信: zhihuiyushaiqi * 点击图片可预览,长按图片可保存 自豪的采用 Serverless Devs 搭建 Powered By Anycodes { {"<"}}作者的话{ {">"}} 作者的话 大家好,我是刘宇,很感谢您可以关注和使用这个小程序,这个小程序是我用业余时间做的一个头像生成小工具,基于“人工智障”技术,反正现在怎么看怎么别扭,但是我会努力让这小程序变得“智能”起来的。如果你有什么好的意见也欢迎联系我 邮箱 或者微信 ,另外值得一提的是,本项目基于阿里云Serverless架构,通过Serverless Devs开发者工具建设。关闭预览 页面逻辑也是比较简单的: // 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 微信公众号(ID:serverlessdevs),汇集 Serverless 技术最全内容,定期举办 Serverless 活动、直播,用户最佳实践。
原文链接:https://developer.aliyun.com/article/882384?
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。