20

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

 3 years ago
source link: https://www.infoq.cn/article/84W44iYPmgPY0KruhBVY
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

使用 Serverless 直接增加头像装饰

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

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

  • 将预定图片 / 用户选择的图片进行美化,此处仅是将其变成圆形:

复制代码

defdo_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])
ifsize[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)# 圆心横坐标

foriinrange(r2):
forjinrange(r2):
lx = abs(i - r)# 到圆心距离的横坐标
ly = abs(j - r)# 到圆心距离的纵坐标
l = (pow(lx,2) + pow(ly,2)) **0.5# 三角函数 半径

ifl < r3:
pimb[i - (r - r3), j - (r - r3)] = pima[i, j]
returnimb

  • 添加该装饰到用户头像上:

复制代码

defadd_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")
returnTrue
exceptExceptionase:
print(e)
returnFalse
  • 除此之外,为了方便本地测试,项目增加了 test() 方法模拟 API 网关传递的数据:

复制代码

deftest():
withopen("test.png",'rb')asf:
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()
  • 为了让函数有同一个返回规范,此处增加统一返回的函数:

复制代码

defreturn_msg(error, msg):
return_data = {
"uuid": str(uuid.uuid1()),
"error": error,
"message": msg
}
print(return_data)
returnreturn_data
  • 最后是涂口函数的写法:

复制代码

importbase64, json
fromPILimportImage
importuuid


defmain_handler(event, context):
try:
print(" 将接收到的 base64 图像转为 pic")
imgData = base64.b64decode(json.loads(event["body"])["pic"].split("base64,")[1])
withopen('/tmp/picture.png','wb')asf:
f.write(imgData)

basePic = json.loads(event["body"])["base"]
addResult = add_decorate(basePic)
ifaddResult:
withopen("/tmp/output.png","rb")asf:
base64Data = str(base64.b64encode(f.read()), encoding='utf-8')
returnreturn_msg(False, {"picture": base64Data})
else:
returnreturn_msg(True," 饰品添加失败 ")
exceptExceptionase:
returnreturn_msg(True," 数据处理异常: %s"% str(e))

完成后端图像合成功能,制作前端页面:

复制代码

<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>2020 头像大变样 - 头像 SHOW - 自豪的采用腾讯云 Serverless 架构!</title>
<metaname="viewport"content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no">
<metaname="apple-mobile-web-app-capable"content="yes">
<metaname="apple-mobile-web-app-status-bar-style"content="black">
<scripttype="text/javascript">
thisPic =null
functiongetFileUrl(sourceId){
varurl;
thisPic =document.getElementById(sourceId).files.item(0)
if(navigator.userAgent.indexOf("MSIE") >=1) {// IE
url =document.getElementById(sourceId).value;
}elseif(navigator.userAgent.indexOf("Firefox") >0) {// Firefox
url =window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));
}elseif(navigator.userAgent.indexOf("Chrome") >0) {// Chrome
url =window.URL.createObjectURL(document.getElementById(sourceId).files.item(0));
}
returnurl;
}
functionpreImg(sourceId, targetId){
varurl = getFileUrl(sourceId);
varimgPre =document.getElementById(targetId);
imgPre.aaaaaa = url;
imgPre.style ="display: block;";
}
functionclickChose(){
document.getElementById("imgOne").click()
}
functiongetNewPhoto(){
document.getElementById("result").innerText =" 系统处理中,请稍后..."
varoFReader =newFileReader();
oFReader.readAsDataURL(thisPic);
oFReader.onload =function(oFREvent){
varxmlhttp;
if(window.XMLHttpRequest) {
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp =newXMLHttpRequest();
}else{
// IE6, IE5 浏览器执行代码
xmlhttp =newActiveXObject("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;";
}
}
}
varurl =" http://service-8d3fi753-1256773370.bj.apigw.tencentcs.com/release/new_year_add_photo_decorate"
varobj =document.getElementsByName("base");
varbaseNum ="1"
for(vari =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");
varpostData = {
pic: oFREvent.target.result,
base: baseNum
}
xmlhttp.send(JSON.stringify(postData));
}
}
</script>
<!-- 标准 mui.css-->
<linkrel="stylesheet"href="./css/mui.min.css">
</head>
<body>
<h3style="text-align: center; margin-top: 30px">2020 头像 SHOW</h3>
<divclass="mui-card">
<divclass="mui-card-content">
<divclass="mui-card-content-inner">
第一步:选择一个你喜欢的图片
</div>
</div>
<divclass="mui-content">
<ulclass="mui-table-view mui-grid-view mui-grid-9">
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/1.png"width="100%"><inputtype="radio"name="base"value="1"checked></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/2.png"width="100%"><inputtype="radio"name="base"value="2"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/11.png"width="100%"><inputtype="radio"name="base"value="11"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/4.png"width="100%"><inputtype="radio"name="base"value="4"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/5.png"width="100%"><inputtype="radio"name="base"value="5"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/6.png"width="100%"><inputtype="radio"name="base"value="6"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/12.png"width="100%"><inputtype="radio"name="base"value="12"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/8.png"width="100%"><inputtype="radio"name="base"value="8"></label></li>
<liclass="mui-table-view-cell mui-media mui-col-xs-4 mui-col-sm-3"><label>
<imgaaaaaa="./base/3.png"width="100%"><inputtype="radio"name="base"value="3"></label></li>
</ul>
</div>
</div>
<divclass="mui-card">
<divclass="mui-card-content">
<divclass="mui-card-content-inner">
第二步:上传一张你的头像
</div>
<div>
<form>
<inputtype="file"name="imgOne"id="imgOne"onchange="preImg(this.id, 'photo')"style="display: none;"
accept="image/*">
<centerstyle="margin-bottom: 10px">
<inputtype="button"value=" 点击此处上传头像 "onclick="clickChose()"/>
<imgid="photo"aaaaaa=""width="300px",height="300px"style="display: none;"/>
</center>
</form>
</div>
</div>
</div>
<divclass="mui-card">
<divclass="mui-card-content">
<divclass="mui-card-content-inner">
第三步:点击生成按钮获取新年头像
</div>
<div>
<centerstyle="margin-bottom: 10px">
<inputtype="button"value=" 生成新年头像 "onclick="getNewPhoto()"/>
<pid="result"></p>
<imgid="new_photo"aaaaaa=""width="300px",height="300px"style="display: none;"/>
</center>
</div>
</div>
</div>
<pstyle="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

完成之后就可以实现头像加装饰的功能,效果如下:

Ij6bIj2.png!web

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

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

eq6j6jZ.png!web

实现这一功能的主要做法就是通过人工智能算法 (此处是通过 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)

此处的做法是只检测一张脸,检测到即进行返回:

复制代码

fordindets:
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))

ifresizedHatH > 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

returnimg

在 Serverless 架构下的完整代码:

复制代码

importcv2
importdlib
importbase64
importjson


defaddHat(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(" 如果检测到人脸 ")
iflen(dets) >0:
fordindets:
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))

ifresizedHatH > 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

returnimg


defmain_handler(event, context):
try:
print(" 将接收到的 base64 图像转为 pic")
imgData = base64.b64decode(json.loads(event["body"])["pic"])
withopen('/tmp/picture.png','wb')asf:
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 返回 ")
withopen("/tmp/output.jpg","rb")asf:
base64Data = str(base64.b64encode(f.read()), encoding='utf-8')

return{
"picture": base64Data
}
exceptExceptionase:
return{
"error": str(e)
}

这样,我们就完成了通过用户上传人物头像进行增加圣诞帽的功能。

总结

传统情况下,如果我们要做一个增加头像装饰的小工具,可能需要一个服务器,哪怕没有人使用,也必须有一台服务器苦苦支撑,这样导致有时仅仅是一个 Demo,也需要无时无刻的支出成本。但在 Serverless 架构下,其弹性伸缩特点让我们不惧怕高并发,其按量付费模式让我们不惧怕成本支出。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK