读:SpringBoot 是基于 Java Spring 框架的套件,它预装了 Spring 的一系列组件,让开发者只需要很少的配置就可以创建独立运行的应用程序。在云原生的世界里,有大量的平台可以运行 Spring Boot 应用,例如虚拟机、容器等。但其中最有吸引力的,是以 Serverless 的方式运行 Spring Boot 应用。我将通过一系列文章,从架构,部署,监控、性能、安全等 5 个方面来分析 Serverless 平台运行 Spring Boot 应用的优劣。为了让分析更有代表性,我选择了 Github 上 star 数超过 50k 的电商应用 mall 作为示例。
我们在上一篇的“Spring Boot on FC - 架构”一文中,对 Mall 应用架构以及 Serverless 平台有了一个基本的介绍,下面我会在本篇中为各位介绍如何将 Mall 应用部署到函数计算平台上。
准备阶段:
注意,如果您使用了云主机,请先检查主机对应的安全组配置是否允许入方向的网络请求。一般的主机在创建后,对于入方向的网络端口访问做了严格限制。我们需要手动允许访问 MySQL 的 3306 端口,Redis 的 6379 端口等。如下图所示,我手动设置了安全组,允许所有入方向的网络请求。
1. 部署依赖软件
Mall 应用依赖 MySQL,Redis,MongoDB,ElasticSearch,RabbitMQ 等软件。这些软件在云上都有对应的云产品。在生产环境,推荐使用云产品获得更好的性能和可用性。在个人开发或者 POC 原型演示场景下,我们选择一台 VM 来容器化部署所有依赖的软件。
1.1. Clone 代码仓库
git clone https://github.com/hryang/mall
国内访问 github 网络不太好,如果 clone 太慢,可使用 Gitee 地址。
git clone https://gitee.com/aliyunfc/mall.git
1.2. 构建和运行 Docker 镜像
在代码根目录的 docker 文件夹下,有每个依赖软件对应的 Dockerfile。运行代码根目录下的 run.sh 脚本,会自动构建所有依赖软件的 Docker 镜像,并在本机运行。
sudo bash docker.sh
1.3. 验证依赖软件运行状态
执行 docker ps 命令,检查依赖软件是否正常运行。
sudo docker ps
2.部署 mall 应用
2.1. 修改 mall 应用配置
修改 mall-admin/src/main/resources/application-prod.yml,mall-portal/src/main/resources/application-prod.yml,mall-search/src/main/resources/application-prod.yml 3个yaml 文件,将其中的 host 字段改成您第1步安装 MySQL 等软件的节点的公网 ip。
2.2. 生成 mall 应用容器镜像
执行 maven 打包命令,生成 docker 镜像。
本地是 java8 或者 java11 环境均可
sudo -E mvn package
成功后,将显示如下成功信息。
执行 sudo docker images,应该能看到 mall/mall-admin,mall/mall-portal,mall/mall-search 的 1.0-SNAPSHOT 版本的镜像。
2.3. 将镜像推送到阿里云镜像仓库
首先登录阿里云镜像仓库控制台,选择个人版实例,根据提示让 docker 登录阿里云镜像仓库。
然后创建命名空间。如下图所示,我们创建了名为 quanxi-hryang 的命名空间。
根据之前的步骤,我们已经在本地生成了 mall/mall-admin, mall/mall-portal, mall/mall-search 的镜像。执行下面的命令,将 mall-admin 镜像推送到杭州区域,quanxi-hryang 命名空间下的镜像仓库。请将下面命令中的 cn-hangzhou 和 quanxi-hryang 修改为您自己的镜像仓库地域和命名空间。mall/mall-portal,mall/mall-search 以此类推。
sudo docker tag mall/mall-admin:1.0-SNAPSHOT registry.cn-hangzhou.aliyuncs.com/quanxi-hryang/mall-admin:1.0-SNAPSHOT
sudo docker push registry.cn-hangzhou.aliyuncs.com/quanxi-hryang/mall-admin:1.0-SNAPSHOT
2.4. 修改 Serverless Devs 工具的应用定义
我们使用 Serverless Devs 工具来定义和部署应用。在项目根目录下,有 s.yaml文件,这是 Serverless Devs 工具的项目定义文件。这里面定义了函数计算的资源。如下图所示,我们在函数计算上定义了名为 mall-admin 服务及其下的 mall-admin 函数。函数中定义了 port,内存大小,超时时间,运行时等属性。红框中的内容是您需要根据自己的配置修改的。
建议:上面的镜像地址最好使用 registry-vpc.cn-hangzhou.aliyuncs.com/fc-demo/mall-admin:1.0-SNAPSHOT 这么形式
2.5. 部署 mall 应用到函数计算平台
执行 s deploy 命令,部署成功后,您将看到对应的访问网址。
在浏览器中输入生成的网址,如果显示 “暂未登录或token已经过期”,说明服务部署成功。
注意:Serverless 的特点是系统默认在请求到达后才创建实例,所以第一次启动时间比较长,称之为冷启动。Mall 应用启动一般需要30秒左右。后面我们将在性能调优文章中来回顾这个问题,使用一系列手段优化。
访问对应的 swagger api 调试页面 host/swagger-ui.html,就能调试相关的后端 API 了。
2.6. 查看应用日志
我们在 s.yaml 中为每个服务都设置了 logConfig:auto,代表 serverless-devs 工具会自动为服务创建日志库(LogStore),所有的服务都共享一个日志库。应用所有的日志都输出到。您可以使用 s logs 命令查看所有服务某个时间点的日志;也可以使用 s mall-admin logs 查看 mall-admin 函数的日志;也可以使用 s mall-admin logs -t以跟随模式实时显示当前时间点之后的日志;也可以使用 s mall-admin logs --keyword=abc 查看包含关键词 abc 的日志。s logs 对于您了解服务运行情况和问题诊断非常有用。例如我们执行 s mall-admin logs -t 进入跟随模式,然后在浏览器中访问 mall-admin 服务的 endpoint,就能看到整个应用的启动和请求处理日志。
2.7. 部署 mall 前端项目(可选)
Mall 也提供了一个前端界面,基于 Vue+Element 实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等功能。该项目同样可以无缝运行在函数计算上。
首先在所在机器上安装 nodejs12 和 npm,并下载项目源代码。
git clone https://github.com/hryang/mall-admin-web
国内访问 github 网络不太好,如果 clone 太慢,可使用下面的代理地址。
git clone https://gitee.com/aliyunfc/mall-admin-web.git
注意:必须是 nodejs 12 或者 14,太新的 node 版本会编译失败!
修改 config/prod.env.js,将其中的 BASE_API 改为之前在函数计算上部署成功的 mall-admin 的 endpoint。
在项目根目录执行下面的命令,构建前端项目。
npm install
npm run build
运行成功后,会生成 dist 目录。运行项目根目录下的 docker.sh 脚本,生成镜像。
sudo bash docker.sh
运行 docker images 命令,将看到 mall/mall-admin-web 镜像已经成功生成了。将镜像推送到阿里云镜像仓库。同理,请将下面命令中的 cn-hangzhou 和 quanxi-hryang 修改为您自己的镜像仓库地域和命名空间。
sudo docker tag mall/mall-admin-web:1.0-SNAPSHOT registry.cn-hangzhou.aliyuncs.com/quanxi-hryang/mall-admin-web:1.0-SNAPSHOT
sudo docker push registry.cn-hangzhou.aliyuncs.com/quanxi-hryang/mall-admin-web:1.0-SNAPSHOT
修改项目根目录下的 s.yaml ,和部署 mall-admin 类似,根据您的配置调整 access,region,将 image 改为上一步推送成功的镜像地址。
执行 s deploy,当部署成功后,就能看到 mall-admin-web 服务的网址。通过浏览器访问,将看到登录页面。填入密码 macro123,就能看到完整的效果。
注意:第一次由于冷启动,登录页面可能会报超时错误。重新刷新页面即可。我们将在后面的性能调优文章中优化冷启动性能。
由于 Serverless 平台内置了网关,负责路由,实例拉起/运行/容错/自动扩缩容等功能,因此开发者上传应用代码包或者镜像后,就已经发布了一个弹性高可用的服务。总结起来,只要完成下面5步就在函数计算平台上完整部署了 mall 应用。后续对应用的更新,只需要重复步骤4和5。可见,Serverless 将环境配置和运维等重复性的工作免除了,开发运维效率大幅提升。
相关链接汇总;
https://spring.io/projects/spring-boot
https://github.com/macrozheng/mall
http://serverless-devs.com/zh-cn/docs/installed/cliinstall.html
原文链接:https://developer.aliyun.com/article/845224?utm_content=g_1000315172
本文为阿里云原创内容,未经允许不得转载。
开发Web网站过程中可能会遇到需要添加许多宣传页,这些宣传页往往不需要什么后端逻辑代码,这时候我们就不希望为每一个宣传页都添加一个Action,
而是希望只添加一个Action,然后结合路由动态的指向不同的视图。而动态的指向不同的视图这时候可能就需要用到判断某个视图是否存在,不存在执行某个逻辑。
下面我们来看下Demo:
首先来看下Demo的项目结构
接下来看下核心代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
namespace IsExistsViewDemo.Controllers
{
/// <summary>
/// 静态宣传页
/// </summary>
public class DHtmlController : Controller
{
#region 字段和属性
/// <summary>
/// 混合视图引擎
/// </summary>
private readonly ICompositeViewEngine _compositeViewEngine;
#endregion 字段和属性
#region 构造函数
/// <summary>
/// 构造函数
/// </summary>
public DHtmlController(ICompositeViewEngine compositeViewEngine)
{
_compositeViewEngine=compositeViewEngine;
}
#endregion 构造函数
#region 宣传页
/// <summary>
/// 宣传页
/// </summary>
/// <param name="viewname">
/// 视图名称
/// </param>
/// <returns>
/// </returns>
[Route("dhtml/{viewname}/")]
public IActionResult Index(string viewname)
{
var view=_compositeViewEngine.FindView(ControllerContext, viewname, false)?.View;
if (view==null) //判断指定的视图文件是否存在
{
// TODO
return View("NotFound");
}
return View(viewname);
}
#endregion 宣传页
#region 直通车宣传页
/// <summary>
/// 直通车宣传页
/// </summary>
/// <param name="viewname">
/// 视图名称
/// </param>
/// <returns>
/// </returns>
[Route("dhtml/ztc/{viewname}.html")]
public IActionResult ZTC(string viewname)
{
var view=_compositeViewEngine.FindView(ControllerContext, $"ZTC/{viewname}", false)?.View;
if (view==null) //判断指定的视图文件是否存在
{
// TODO
return View("NotFound");
}
return View($"ZTC/{viewname}");
}
#endregion 直通车宣传页
}
}
到大型节假日,我们常会发现社交平台都会提供生成头像装饰的小工具,很是新奇好玩。如果从技术的维度看,这类平台 / 工具一般都是通过下面两个方法给我们生成头像装饰的:
增加头像装饰的功能其实很容易实现,首先选择一张图片,上传自己的头像,然后函数部分进行图像的合成,这一部分并没有涉及到机器学习算法,仅仅是图像合成相关算法。
通过用户上传的图片,在指定位置增加预定图片 / 用户选择的图片作为装饰物进行添加:
复制代码
def do_circle(base_pic): icon_pic=Image.open(base_pic).convert("RGBA") icon_pic=icon_pic.resize((500, 500), Image.ANTIALIAS) icon_pic_x, icon_pic_y=icon_pic.size temp_icon_pic=Image.new('RGBA', (icon_pic_x + 600, icon_pic_y + 600), (255, 255, 255)) temp_icon_pic.paste(icon_pic, (300, 300), icon_pic) ima=temp_icon_pic.resize((200, 200), Image.ANTIALIAS) size=ima.size # 因为是要圆形,所以需要正方形的图片 r2=min(size[0], size[1]) if size[0] !=size[1]: ima=ima.resize((r2, r2), Image.ANTIALIAS) # 最后生成圆的半径 r3=60 imb=Image.new('RGBA', (r3 * 2, r3 * 2), (255, 255, 255, 0)) pima=ima.load() # 像素的访问对象 pimb=imb.load() r=float(r2 / 2) # 圆心横坐标 for i in range(r2): for j in range(r2): lx=abs(i - r) # 到圆心距离的横坐标 ly=abs(j - r) # 到圆心距离的纵坐标 l=(pow(lx, 2) + pow(ly, 2)) ** 0.5 # 三角函数 半径 if l < r3: pimb[i - (r - r3), j - (r - r3)]=pima[i, j] return imb
复制代码
def add_decorate(base_pic): try: base_pic="./base/%s.png" % (str(base_pic)) user_pic=Image.open("/tmp/picture.png").convert("RGBA") temp_basee_user_pic=Image.new('RGBA', (440, 440), (255, 255, 255)) user_pic=user_pic.resize((400, 400), Image.ANTIALIAS) temp_basee_user_pic.paste(user_pic, (20, 20)) temp_basee_user_pic.paste(do_circle(base_pic), (295, 295), do_circle(base_pic)) temp_basee_user_pic.save("/tmp/output.png") return True except Exception as e: print(e) return False
复制代码
def test(): with open("test.png", 'rb') as f: image=f.read() image_base64=str(base64.b64encode(image), encoding='utf-8') event={ "requestContext": { "serviceId": "service-f94sy04v", "path": "/test/{path}", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "identity": { "secretId": "abdcdxxxxxxxsdfs" }, "sourceIp": "14.17.22.34", "stage": "release" }, "headers": { "Accept-Language": "en-US,en,cn", "Accept": "text/html,application/xml,application/json", "Host": "service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com", "User-Agent": "User Agent String" }, "body": "{\"pic\":\"%s\", \"base\":\"1\"}" % image_base64, "pathParameters": { "path": "value" }, "queryStringParameters": { "foo": "bar" }, "headerParameters": { "Refer": "10.0.2.14" }, "stageVariables": { "stage": "release" }, "path": "/test/value", "queryString": { "foo": "bar", "bob": "alice" }, "httpMethod": "POST" } print(main_handler(event, None)) if __name__=="__main__": test()
复制代码
def return_msg(error, msg): return_data={ "uuid": str(uuid.uuid1()), "error": error, "message": msg } print(return_data) return return_data
复制代码
import base64, jsonfrom PIL import Imageimport uuid def main_handler(event, context): try: print(" 将接收到的 base64 图像转为 pic") imgData=base64.b64decode(json.loads(event["body"])["pic"].split("base64,")[1]) with open('/tmp/picture.png', 'wb') as f: f.write(imgData) basePic=json.loads(event["body"])["base"] addResult=add_decorate(basePic) if addResult: with open("/tmp/output.png", "rb") as f: base64Data=str(base64.b64encode(f.read()), encoding='utf-8') return return_msg(False, {"picture": base64Data}) else: return return_msg(True, " 饰品添加失败 ") except Exception as e: return return_msg(True, " 数据处理异常: %s" % str(e))
完成后端图像合成功能,制作前端页面:
复制代码
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>2020 头像大变样 - 头像 SHOW - 自豪的采用腾讯云 Serverless 架构!</title> <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <script type="text/javascript"> thisPic=null function getFileUrl(sourceId) { var url; thisPic=document.getElementById(sourceId).files.item(0) if (navigator.userAgent.indexOf("MSIE") >=1) { // IE url=document.getElementById(sourceId).value; } else if (navigator.userAgent.indexOf("Firefox") > 0) { // Firefox url=window.URL.createObjectURL(document.getElementById(sourceId).files.item(0)); } else if (navigator.userAgent.indexOf("Chrome") > 0) { // Chrome url=window.URL.createObjectURL(document.getElementById(sourceId).files.item(0)); } return url; } function preImg(sourceId, targetId) { var url=getFileUrl(sourceId); var imgPre=document.getElementById(targetId); imgPre.aaaaaa=url; imgPre.style="display: block;"; } function clickChose() { document.getElementById("imgOne").click() } function getNewPhoto() { document.getElementById("result").innerText=" 系统处理中,请稍后..." var oFReader=new FileReader(); oFReader.readAsDataURL(thisPic); oFReader.onload=function (oFREvent) { var xmlhttp; if (window.XMLHttpRequest) { // IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码 xmlhttp=new XMLHttpRequest(); } else { // IE6, IE5 浏览器执行代码 xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange=function () { if (xmlhttp.readyState==4 && xmlhttp.status==200) { if (JSON.parse(xmlhttp.responseText)["error"]) { document.getElementById("result").innerText=JSON.parse(xmlhttp.responseText)["message"]; } else { document.getElementById("result").innerText=" 长按保存图像 "; document.getElementById("new_photo").aaaaaa="data:image/png;base64," + JSON.parse(xmlhttp.responseText)["message"]["picture"]; document.getElementById("new_photo").style="display: block;"; } } } var url=" http://service-8d3fi753-1256773370.bj.apigw.tencentcs.com/release/new_year_add_photo_decorate" var obj=document.getElementsByName("base"); var baseNum="1" for (var i=0; i < obj.length; i++) { console.log(obj[i].checked) if (obj[i].checked) { baseNum=obj[i].value; } } xmlhttp.open("POST", url, true); xmlhttp.setRequestHeader("Content-type", "application/json"); var postData={ pic: oFREvent.target.result, base: baseNum } xmlhttp.send(JSON.stringify(postData)); } } </script> <!-- 标准 mui.css--> <link rel="stylesheet" href="./css/mui.min.css"></head><body><h3 style="text-align: center; margin-top: 30px">2020 头像 SHOW</h3><div class="mui-card"> <div class="mui-card-content"> <div class="mui-card-content-inner"> 第一步:选择一个你喜欢的图片 </div> </div> <div class="mui-content"> <ul class="mui-table-view mui-grid-view mui-grid-9"> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/1.png" width="100%"><input type="radio" name="base" value="1" checked></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/2.png" width="100%"><input type="radio" name="base" value="2"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/11.png" width="100%"><input type="radio" name="base" value="11"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/4.png" width="100%"><input type="radio" name="base" value="4"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/5.png" width="100%"><input type="radio" name="base" value="5"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/6.png" width="100%"><input type="radio" name="base" value="6"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/12.png" width="100%"><input type="radio" name="base" value="12"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/8.png" width="100%"><input type="radio" name="base" value="8"></label></li> <li class="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label> <img aaaaaa="./base/3.png" width="100%"><input type="radio" name="base" value="3"></label></li> </ul> </div></div><div class="mui-card"> <div class="mui-card-content"> <div class="mui-card-content-inner"> 第二步:上传一张你的头像 </div> <div> <form> <input type="file" name="imgOne" id="imgOne" onchange="preImg(this.id, 'photo')" style="display: none;" accept="image/*"> <center style="margin-bottom: 10px"> <input type="button" value=" 点击此处上传头像 " onclick="clickChose()"/> <img id="photo" aaaaaa="" width="300px" , height="300px" style="display: none;"/> </center> </form> </div> </div></div><div class="mui-card"> <div class="mui-card-content"> <div class="mui-card-content-inner"> 第三步:点击生成按钮获取新年头像 </div> <div> <center style="margin-bottom: 10px"> <input type="button" value=" 生成新年头像 " onclick="getNewPhoto()"/> <p id="result"></p> <img id="new_photo" aaaaaa="" width="300px" , height="300px" style="display: none;"/> </center> </div> </div></div><p style="text-align: center"> 本项目自豪的 <br> 通过 Serverless Framework<br> 搭建在腾讯云 SCF 上</p></body></html>
完成之后:
复制代码
new_year_add_photo_decorate: component: "@serverless/tencent-scf" inputs: name: myapi_new_year_add_photo_decorate codeUri: ./new_year_add_photo_decorate handler: index.main_handler runtime: Python3.6 region: ap-beijing description: 新年为头像增加饰品 memorySize: 128 timeout: 5 events: - apigw: name: serverless parameters: serviceId: service-8d3fi753 environment: release endpoints: - path: /new_year_add_photo_decorate description: 新年为头像增加饰品 method: POST enableCORS: true param: - name: pic position: BODY required: 'FALSE' type: string desc: 原始图片 - name: base position: BODY required: 'FALSE' type: string desc: 饰品 ID myWebsite: component: '@serverless/tencent-website' inputs: code: src: ./new_year_add_photo_decorate/web index: index.html error: index.html region: ap-beijing bucketName: new-year-add-photo-decorate
完成之后就可以实现头像加装饰的功能,效果如下:
直接加装饰的方式其实是可以在前端实现的,但是既然用到了后端服务和云函数,那么我们不妨就将人工智能与 Serverless 架构结果来实现一个增加装饰的小工具。
实现这一功能的主要做法就是通过人工智能算法 (此处是通过 Dlib 实现) 进行人脸检测:
复制代码
print("dlib 人脸关键点检测器, 正脸检测 ")predictorPath="shape_predictor_5_face_landmarks.dat"predictor=dlib.shape_predictor(predictorPath)detector=dlib.get_frontal_face_detector()dets=detector(img, 1)
此处的做法是只检测一张脸,检测到即进行返回:
复制代码
for d in dets: x, y, w, h=d.left(), d.top(), d.right() - d.left(), d.bottom() - d.top() print(" 关键点检测,5 个关键点 ") shape=predictor(img, d) print(" 选取左右眼眼角的点 ") point1=shape.part(0) point2=shape.part(2) print(" 求两点中心 ") eyes_center=((point1.x + point2.x) // 2, (point1.y + point2.y) // 2) print(" 根据人脸大小调整帽子大小 ") factor=1.5 resizedHatH=int(round(rgbHat.shape[0] * w / rgbHat.shape[1] * factor)) resizedHatW=int(round(rgbHat.shape[1] * w / rgbHat.shape[1] * factor)) if resizedHatH > y: resizedHatH=y - 1 print(" 根据人脸大小调整帽子大小 ") resizedHat=cv2.resize(rgbHat, (resizedHatW, resizedHatH)) print(" 用 alpha 通道作为 mask") mask=cv2.resize(a, (resizedHatW, resizedHatH)) maskInv=cv2.bitwise_not(mask) print(" 帽子相对与人脸框上线的偏移量 ") dh=0 bgRoi=img[y + dh - resizedHatH:y + dh, (eyes_center[0] - resizedHatW // 3):(eyes_center[0] + resizedHatW // 3 * 2)] print(" 原图 ROI 中提取放帽子的区域 ") bgRoi=bgRoi.astype(float) maskInv=cv2.merge((maskInv, maskInv, maskInv)) alpha=maskInv.astype(float) / 255 print(" 相乘之前保证两者大小一致(可能会由于四舍五入原因不一致)") alpha=cv2.resize(alpha, (bgRoi.shape[1], bgRoi.shape[0])) bg=cv2.multiply(alpha, bgRoi) bg=bg.astype('uint8') print(" 提取帽子区域 ") hat=cv2.bitwise_and(resizedHat, cv2.bitwise_not(maskInv)) print(" 相加之前保证两者大小一致(可能会由于四舍五入原因不一致)") hat=cv2.resize(hat, (bgRoi.shape[1], bgRoi.shape[0])) print(" 两个 ROI 区域相加 ") addHat=cv2.add(bg, hat) print(" 把添加好帽子的区域放回原图 ") img[y + dh - resizedHatH:y + dh, (eyes_center[0] - resizedHatW // 3):(eyes_center[0] + resizedHatW // 3 * 2)]=addHat return img
在 Serverless 架构下的完整代码:
复制代码
import cv2import dlibimport base64import json def addHat(img, hat_img): print(" 分离 rgba 通道,合成 rgb 三通道帽子图,a 通道后面做 mask 用 ") r, g, b, a=cv2.split(hat_img) rgbHat=cv2.merge((r, g, b)) print("dlib 人脸关键点检测器, 正脸检测 ") predictorPath="shape_predictor_5_face_landmarks.dat" predictor=dlib.shape_predictor(predictorPath) detector=dlib.get_frontal_face_detector() dets=detector(img, 1) print(" 如果检测到人脸 ") if len(dets) > 0: for d in dets: x, y, w, h=d.left(), d.top(), d.right() - d.left(), d.bottom() - d.top() print(" 关键点检测,5 个关键点 ") shape=predictor(img, d) print(" 选取左右眼眼角的点 ") point1=shape.part(0) point2=shape.part(2) print(" 求两点中心 ") eyes_center=((point1.x + point2.x) // 2, (point1.y + point2.y) // 2) print(" 根据人脸大小调整帽子大小 ") factor=1.5 resizedHatH=int(round(rgbHat.shape[0] * w / rgbHat.shape[1] * factor)) resizedHatW=int(round(rgbHat.shape[1] * w / rgbHat.shape[1] * factor)) if resizedHatH > y: resizedHatH=y - 1 print(" 根据人脸大小调整帽子大小 ") resizedHat=cv2.resize(rgbHat, (resizedHatW, resizedHatH)) print(" 用 alpha 通道作为 mask") mask=cv2.resize(a, (resizedHatW, resizedHatH)) maskInv=cv2.bitwise_not(mask) print(" 帽子相对与人脸框上线的偏移量 ") dh=0 bgRoi=img[y + dh - resizedHatH:y + dh, (eyes_center[0] - resizedHatW // 3):(eyes_center[0] + resizedHatW // 3 * 2)] print(" 原图 ROI 中提取放帽子的区域 ") bgRoi=bgRoi.astype(float) maskInv=cv2.merge((maskInv, maskInv, maskInv)) alpha=maskInv.astype(float) / 255 print(" 相乘之前保证两者大小一致(可能会由于四舍五入原因不一致)") alpha=cv2.resize(alpha, (bgRoi.shape[1], bgRoi.shape[0])) bg=cv2.multiply(alpha, bgRoi) bg=bg.astype('uint8') print(" 提取帽子区域 ") hat=cv2.bitwise_and(resizedHat, cv2.bitwise_not(maskInv)) print(" 相加之前保证两者大小一致(可能会由于四舍五入原因不一致)") hat=cv2.resize(hat, (bgRoi.shape[1], bgRoi.shape[0])) print(" 两个 ROI 区域相加 ") addHat=cv2.add(bg, hat) print(" 把添加好帽子的区域放回原图 ") img[y + dh - resizedHatH:y + dh, (eyes_center[0] - resizedHatW // 3):(eyes_center[0] + resizedHatW // 3 * 2)]=addHat return img def main_handler(event, context): try: print(" 将接收到的 base64 图像转为 pic") imgData=base64.b64decode(json.loads(event["body"])["pic"]) with open('/tmp/picture.png', 'wb') as f: f.write(imgData) print(" 读取帽子素材以及用户头像 ") hatImg=cv2.imread("hat.png", -1) userImg=cv2.imread("/tmp/picture.png") output=addHat(userImg, hatImg) cv2.imwrite("/tmp/output.jpg", output) print(" 读取头像进行返回给用户,以 Base64 返回 ") with open("/tmp/output.jpg", "rb") as f: base64Data=str(base64.b64encode(f.read()), encoding='utf-8') return { "picture": base64Data } except Exception as e: return { "error": str(e) }
这样,我们就完成了通过用户上传人物头像进行增加圣诞帽的功能。
传统情况下,如果我们要做一个增加头像装饰的小工具,可能需要一个服务器,哪怕没有人使用,也必须有一台服务器苦苦支撑,这样导致有时仅仅是一个 Demo,也需要无时无刻的支出成本。但在 Serverless 架构下,其弹性伸缩特点让我们不惧怕高并发,其按量付费模式让我们不惧怕成本支出。
关注我并转发此篇文章,私信我“领取资料”,即可免费获得InfoQ价值4999元迷你书!
*请认真填写需求信息,我们会在24小时内与您取得联系。