Serverless 实战:如何为你的头像增加点装饰?

发布于:2020 年 6 月 28 日 11:57

Serverless实战:如何为你的头像增加点装饰?

每到大型节假日,我们常会发现社交平台都会提供生成头像装饰的小工具,很是新奇好玩。如果从技术的维度看,这类平台 / 工具一般都是通过下面两个方法给我们生成头像装饰的:

  • 一是直接加装饰,例如在头像外面加边框,在下面加 logo 等;
  • 二是通过机器学习算法增加装饰,例如增加一个圣诞帽等;

使用 Serverless 直接增加头像装饰

增加头像装饰的功能其实很容易实现,首先选择一张图片,上传自己的头像,然后函数部分进行图像的合成,这一部分并没有涉及到机器学习算法,仅仅是图像合成相关算法。

通过用户上传的图片,在指定位置增加预定图片 / 用户选择的图片作为装饰物进行添加:

  • 将预定图片 / 用户选择的图片进行美化,此处仅是将其变成圆形:
复制代码
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
  • 除此之外,为了方便本地测试,项目增加了test()方法模拟 API 网关传递的数据:
复制代码
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, json
from PIL import Image
import 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实战:如何为你的头像增加点装饰?

Serverless 与人工智能联手增加头像装饰

直接加装饰的方式其实是可以在前端实现的,但是既然用到了后端服务和云函数,那么我们不妨就将人工智能与 Serverless 架构结果来实现一个增加装饰的小工具。

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 cv2
import dlib
import base64
import 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 架构下,其弹性伸缩特点让我们不惧怕高并发,其按量付费模式让我们不惧怕成本支出。

阅读数:3 发布于:2020 年 6 月 28 日 11:57

评论

发布
暂无评论