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

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

  • 一是直接加装饰,例如在头像外面加边框,在下面加 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))

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

复制代码




2020 头像大变样 - 头像 SHOW - 自豪的采用腾讯云 Serverless 架构!




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));
}
}





2020 头像 SHOW



第一步:选择一个你喜欢的图片

第二步:上传一张你的头像
第三步:点击生成按钮获取新年头像
本项目自豪的
通过 Serverless Framework
搭建在腾讯云 SCF 上

完成之后:

复制代码

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 架构结果来实现一个增加装饰的小工具。

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

peace