Series này gồm 3 phần:
- Cơ chế hoạt động của các filter Snapchat và Facebook Messenger
- Làm quen với các thư viện và API cần sử dụng
- Gắn râu bằng cách kết hợp Face Detection + Image Processing và… Toán Học
Sau 2 phần trước, chúng ta đã tìm hiểu về cơ chế các filter hoạt động, cũng như cách dùng thư viện để gắn râu vào ảnh.
Tuy nhiên, kết quả vẫn chưa được như mong muốn, chúng ta được tấm hình dị hợm như sau.
Để ghép râu cho khớp và đẹp, chúng ta cần phải:
- Xác định được độ dài hàm râu
- Tìm vị trí đặt râu cho phù hợp
Đây là lúc chúng ta sử dụng lại công nghệ Face Detection ở phần 1 để xác định những điều trên. Cùng bắt đầu thôi nào!
Khi Face Detection kết hợp với Image Processing
Để gắn râu và đúng cách, ta xác lập những điều kiện sau:
- Địa điểm hợp lý để gắn râu là … phía trên môi (position)
- Độ dài của râu nên dài bằng môi (length)
Trước tiên, ta xem lại các landmark có thể được API nhận diện.

Dựa theo tấm hình trên, ta thấy có thể sử dụng 2 landmarks là mouthLeft và mouthRight (Chấm đỏ trong hình) để xác định 2 điều cần tìm.

Nhìn vào hình vẽ, ta dễ thấy:
- Chiều dài bộ râu sẽ bằng khoảng cách giữa 2 điểm, tức x2 – x1.
- Môi trái nằm phía trên, ta có thể đặt bộ râu ở vị trí của landmark mouthLeft, xích lên trên một chút. Tọa độ cụ thể sẽ là (x1, y2)
Sửa lại code mộy chút nào (Hàm detectImage chúng ta đã viết trong phần 1 nhé:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const rp = require('request-promise'); | |
const Jimp = require("jimp"); | |
(async () => { | |
const imageUrl = 'https://pbs.twimg.com/media/DWr05hUXcAA9s8n.jpg'; | |
const detectResult = await detectImage(imageUrl); | |
addMustache(imageUrl, detectResult, 'jav_rau.jpg'); | |
})(); | |
async function addMustache(url, detectResult, output) { | |
// Tính toán vị trí và khuôn mặt | |
const face = detectResult[0]; // Hình chỉ nhận được 1 khuôn mặt | |
const landmarks = face.faceLandmarks; | |
const mouthLeft = landmarks['mouthLeft']; | |
const mouthRight = landmarks['mouthRight']; | |
const x1 = mouthLeft.x, y1 = mouthLeft.y; | |
const x2 = mouthRight.x, y2= mouthRight.y; | |
const mustacheWidth = x2 – x1; | |
let source = await Jimp.read(url); | |
const mustache = await Jimp.read('mustache.png'); | |
mustache.resize(mustacheWidth, Jimp.AUTO); | |
return source.composite(mustache, x1, y2).write(output); | |
} | |
async function detectImage(source) { | |
const subscriptionKey = "3845387fabbf4ba5bfe958a2409b8238"; | |
const uri = "https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect"; | |
const options = { | |
uri, | |
qs: { | |
returnFaceId: true, | |
returnFaceLandmarks: true, | |
}, | |
method: 'POST', | |
headers: { | |
'Ocp-Apim-Subscription-Key': subscriptionKey | |
}, | |
body: { | |
url: source | |
}, | |
json: true // Automatically parses the JSON string in the response | |
}; | |
const result = await rp(options); | |
return result; | |
} |
Kết quả thu được cũng tạm chấp nhận được:

Khi gắn râu cũng cần dùng đến… hình học
Kết quả thu được chưa đẹp lắm, vì em Yua Mikami nghiêng đầu cute, trong khi hàm râu của chúng ta thẳng băng vuông góc.
Chúng ta đã hoàn toàn quên mất góc nghiêng của bộ râu!!
Đây là lúc chúng ta sử dụng kiến thức Toán Học và hình học thu được qua 12 năm trời để tính toán địa điểm và góc độ đặt râu hợp lý lên mặt bé Yua dễ thương.
Chúng ta quay lại hình ban đầu để tính toán nhé.
- Lần lượt gọi A, B, C là các điểm nhưng trong hình, A là giao điểm của 2 đường thằng x2 và y1. Tam giác ABC vuông tại A.
- Nhìn kĩ ta, sẽ thấy chiều dài của râu không phải là x2 – x1 (AC) mà là độ dài đoạn BC (cạnh huyền)
- Bộ râu sẽ nghiêng 1 góc bằng với góc ACB
- Theo toán học: tan = đối/kề => tan ACB = AB/AC. Tính toán ta sẽ tìm ra được góc ACB.
- Để phù hợp, râu nên được đặt ở giữa môi (landmark upperLipTop), dịch lên khoảng 1/2 về phía mũi (landmark noteTip)
Sửa lại mấy dòng code một xíu nhé:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const rp = require('request-promise'); | |
const Jimp = require("jimp"); | |
(async () => { | |
const imageUrl = 'https://pbs.twimg.com/media/DWr05hUXcAA9s8n.jpg'; | |
const detectResult = await detectImage(imageUrl); | |
addMustache(imageUrl, detectResult, 'jav_rau.jpg'); | |
})(); | |
async function addMustache(url, detectResult, output) { | |
// Tính toán vị trí và khuôn mặt | |
const face = detectResult[0]; // Hình chỉ nhận được 1 khuôn mặt | |
const landmarks = face.faceLandmarks; | |
const mouthLeft = landmarks['mouthLeft']; | |
const mouthRight = landmarks['mouthRight']; | |
const x1 = mouthLeft.x, y1 = mouthLeft.y; | |
const x2 = mouthRight.x, y2= mouthRight.y; | |
// Cạnh đối và cạnh kề | |
const ab = y2 – y1; | |
const ac = x2 – x1; | |
// Cạnh huyền = căn tổng bình phương 2 cạnh | |
const bc = Math.sqrt(ab * ab + ac * ac); | |
const tanACB = ab / ac; | |
// Qui đổi radian sang độ | |
const deg = Math.atan(tanACB) * 180 / Math.PI; | |
// Resize và quay bộ râu | |
let source = await Jimp.read(url); | |
const mustache = await Jimp.read('mustache.png'); | |
const mustacheWidth = bc; | |
mustache.resize(mustacheWidth, Jimp.AUTO).rotate(deg); | |
// Xác định vị trí đặt râu | |
const x = landmarks['upperLipTop'].x; | |
const moveY = (landmarks['upperLipTop'].y – landmarks['noseTip'].y) / 2; | |
const y = landmarks['upperLipTop'].y – moveY; | |
// Đặt bộ râu vào giữa tọa độ x,y | |
addImageCenter(source, mustache, x, y); | |
return source.write(output); | |
} | |
function addImageCenter(source, image, x, y) { | |
const { height, width } = image.bitmap; | |
const newX = x – width / 2; | |
const newY = y – height / 2; | |
return source.composite(image, newX, newY); | |
} | |
async function detectImage(source) { | |
const subscriptionKey = "3845387fabbf4ba5bfe958a2409b8238"; | |
const uri = "https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect"; | |
const options = { | |
uri, | |
qs: { | |
returnFaceId: true, | |
returnFaceLandmarks: true, | |
}, | |
method: 'POST', | |
headers: { | |
'Ocp-Apim-Subscription-Key': subscriptionKey | |
}, | |
body: { | |
url: source | |
}, | |
json: true // Automatically parses the JSON string in the response | |
}; | |
const result = await rp(options); | |
return result; | |
} |
Kết quả thu được khá là ưng ý <3.
Các bạn thấy chưa, chỉ một phép gắn râu đơn giản mà chúng ta phải vận dụng một số kiến thức Toán rồi đấy! Ai bảo lập trình không cần nhiều đến Toán đâu nào?
Trong lĩnh vực Image Processing, toán học còn được vận dụng rất nhiều nữa cơ! Ví dụ như chuyển ảnh trắng đen, filter màu … đôi khi phải dùng đến các phép Toán ma trận nữa đấy.
Code của chúng ta hiệu quả tới mức nào?
Sau một hồi code mệt mỏi, chúng ta đã có một đoạn code cho kết quả khá ưng ý.
Tuy nhiên, code của chúng ta chạy tốt với tấm ảnh Yua Mikami không có nghĩa nó sẽ chạy tốt với những tấm hình khác!!
Do vậy, chúng ta thử thay url của ảnh khác vào và chạy lại code nha!
Kết quả cũng khá là ok đấy chứ nhỉ? Hihi, code của mình code mà lại!
Mở rộng thêm
Các bạn có thể mở rộng thêm với các filter đội vòng hoa, đeo kính, gắn tai thỏ:
- Vòng hoa: Nằm ngay phía trên trán, độ cao bằng trán, vòng hoa dài hơn bề ngang khuôn mặt một chút
- Đeo kính: Nằm ngay mắt, độ dài bằng bề ngang khuôn mặt
- Tai thỏ: Khó hơn, có thể dùng mũi chia mặt ra làm 2 phần, đặt hai tai tại 2 trung điểm.
Sau một hồi sửa chữa, chúng ta có hàm addFlower để gắn vòng hoa lên trán. Các bạn thấy đấy, để xác định tọa độ của trán, ta phải biết về kết cấu khuôn mặt + nhân tướng học mới tính được.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(async () => { | |
const imageUrl = 'http://storage.vpopfan.com/sontungmtp.jpg'; | |
const detectResult = await detectImage(imageUrl); | |
addFlower(imageUrl, detectResult, 'jav_flower.jpg'); | |
})(); | |
async function addFlower(url, detectResult, output) { | |
// Tính toán vị trí và khuôn mặt | |
const face = detectResult[0]; // Hình chỉ nhận được 1 khuôn mặt | |
const { top, left, width, height } = face.faceRectangle; | |
// Resize hoa | |
let source = await Jimp.read(url); | |
const flower = await Jimp.read('flower.png'); | |
flower.resize(width * 1.3, Jimp.AUTO); | |
// Tìm địa điểm đặt vòng hoa | |
const x = left + width/2; | |
// Hoa được đặt giữa trán | |
// Tìm tọa độ trán thông qua độ dài mũi | |
// Trán sẽ cách đỉnh mủi 1.5 lần độ dài mũi | |
const landmarks = face.faceLandmarks; | |
const noseRootLeft = landmarks['noseRootLeft']; | |
const noseHeight = landmarks['noseTip'].y – noseRootLeft.y; | |
const y = noseRootLeft.y – noseHeight*1.5; | |
addImageCenter(source, flower, x, y); | |
return source.write(output); | |
} |
Kết quả cũng không đến nỗi nào.

Tuy nhiên, chỉ cần một phát nghiêng đầu cute của bé Yua Mikami cũng khiến vòng hoa đặt lệch ngay!
Lúc này các bạn sẽ phải tính lại góc nghiêng của vòng, vị trí đặt vòng cũng sẽ di chuyển theo góc nghiêng của đầu nhé!
Đến đây thì mình hơi lười rồi nên để phần này làm bài tập cho bạn đọc vậy! Gợi ý nha, bạn có thể tìm góc nghiêng của đầu dựa theo góc nghiêng giữa 2 mắt hay 2 bên môi nha.
Kết
Vậy là sau 3 phần, chúng ta đã hoàn thành ứng dụng Gắn Râu Sơn Tùng, biến tướng thành Gắn Râu cho JAV Idol, sử dụng NodeJS. Chúc mừng bạn đã cùng mình đi hết 3 phần của series.
Ban đầu, mình định viết luôn hướng dẫn cách tách ảnh gif và ghép lại luôn, nhưng phần nào không hay ho mấy nên mình bỏ qua hihi.

Các bạn thấy đấy, đằng sau một tính năng tưởng chừng đơn giản (filter thêm râu, thêm vòng hoa) là hàng đống những kĩ thuật phức tạp từ face recognition, image processing cho tới hình học toán học nhân tướng học! Ngạc nhiên chưa?
Bạn nào có những câu hỏi tương tự về những tính năng “hay ho” trên app mà mình sử dụng thì cứ để lại comment nha. Nếu thú vị hay mình sẽ cùng các bạn nghiên cứu thử.