网站首页 > 技术文章 正文
每到大型节假日,我们常会发现社交平台都会提供生成头像装饰的小工具,很是新奇好玩。如果从技术的维度看,这类平台 / 工具一般都是通过下面两个方法给我们生成头像装饰的:
- 一是直接加装饰,例如在头像外面加边框,在下面加 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, 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 与人工智能联手增加头像装饰
直接加装饰的方式其实是可以在前端实现的,但是既然用到了后端服务和云函数,那么我们不妨就将人工智能与 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元迷你书!
猜你喜欢
- 2024-11-13 JS二进制:Blob、File、FileReader、ArrayBuffer、Base64
- 2024-11-13 实现HTML5网站中常见的拖拽上传文件
- 2024-11-13 input上传图片并压缩(vue,前端,js)
- 2024-11-13 怎样用Nodejs,Express和FFmpeg.wasm构建一个处理多媒体的API
- 2024-11-13 为什么 JS 开发者更喜欢 Axios 而不是 Fetch?
- 2024-11-13 leaflet地图截图批量导出 leaflet 底图
- 2024-11-13 拖拽外部文件进行读取-FileReader
- 2024-11-13 JavaScript `FileReader` 接口提供的文件处理方法
- 2024-11-13 vue手把手教学~搭建web聊天室 vue实现聊天功能
- 2024-11-13 Html5 上传图片 移动端、PC端通用
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-