到大型節假日,我們常會發現社交平臺都會提供生成頭像裝飾的小工具,很是新奇好玩。如果從技術的維度看,這類平臺 / 工具一般都是通過下面兩個方法給我們生成頭像裝飾的:
增加頭像裝飾的功能其實很容易實現,首先選擇一張圖片,上傳自己的頭像,然后函數部分進行圖像的合成,這一部分并沒有涉及到機器學習算法,僅僅是圖像合成相關算法。
通過用戶上傳的圖片,在指定位置增加預定圖片 / 用戶選擇的圖片作為裝飾物進行添加:
復制代碼
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元迷你書!
果圖:
用工具:一個網站→http://h5.qzone.qq.com/q/tu/websites/Christmas2015/index.html
1:進入網站,點擊“定制圣誕頭像”,選擇自己要制作的頭像
2:選擇自己想要的圣誕帽,自己長按即可保存圖片到本地
最后制作效果
*請認真填寫需求信息,我們會在24小時內與您取得聯系。