Face Detection & Angle Calculation - Fundamentals
Mục tiêu: Giải thích từ cơ bản đến nâng cao về face detection, machine learning, và toán học tính góc khuôn mặt trong dự án portal.
1. Face Detection là gì?
1.1. Định nghĩa
Face Detection (phát hiện khuôn mặt) là quá trình tự động phát hiện và định vị khuôn mặt trong ảnh/video.
Đầu vào: Khung hình ảnh/Video
Quá trình: Thuật toán phát hiện
Đầu ra: Hộp giới hạn (bounding box) + Điểm tin cậy (confidence score)
Ví dụ:
┌─────────────────────┐
│ │
│ ┌─────────┐ │ ← Ảnh
│ │ (・_・) │ │
│ └─────────┘ │
│ ↑ │
│Khuôn mặt đã phát hiện│
│ x,y,w,h (hộp) │
└─────────────────────┘1.2. Lịch sử phát triển
Dòng thời gian:
2001: Thuật toán Viola-Jones (Haar Cascades)
→ Nhanh, nhưng không chính xác
→ Được dùng trong OpenCV sớm
2012: Cách mạng Học sâu (AlexNet)
→ Mạng nơ-ron tích chập (CNN)
2015-2018: Phát hiện khuôn mặt hiện đại
→ MTCNN, FaceNet, RetinaFace
→ Độ chính xác cao, thời gian thực
2019: MediaPipe FaceMesh (Google)
→ 468 điểm mốc thời gian thực
→ Chạy trên thiết bị mobile
→ Được dùng trong dự án Portal ✅Nguồn: Google MediaPipe (2019)
1.3. Phát hiện khuôn mặt vs Nhận dạng khuôn mặt vs Điểm mốc khuôn mặt
┌──────────────────────────────────────────────────────┐
│ │
│ Phát hiện khuôn mặt (Bước 1) │
│ ┌─────────┐ │
│ │ (・_・) │ → "Có khuôn mặt tại x,y,w,h" │
│ └─────────┘ │
│ │
│ Điểm mốc khuôn mặt (Bước 2) │
│ ┌─────────┐ │
│ │ • • │ → "468 điểm trên khuôn mặt" │
│ │ • │ (mắt, mũi, miệng, ...) │
│ │ • • • │ │
│ └─────────┘ │
│ │
│ Nhận dạng khuôn mặt (Bước 3) │
│ ┌─────────┐ │
│ │ (・_・) │ → "Đây là người X" │
│ └─────────┘ (khớp danh tính) │
│ │
└──────────────────────────────────────────────────────┘
Dự án Portal sử dụng:
✅ Phát hiện khuôn mặt: MediaPipe (phát hiện khuôn mặt)
✅ Điểm mốc khuôn mặt: MediaPipe (468 điểm)
✅ Nhận dạng khuôn mặt: API Backend (so sánh với CCCD)2. Machine Learning trong Face Detection
2.1. CNN (Convolutional Neural Network)
MediaPipe FaceMesh sử dụng CNN để phát hiện khuôn mặt và điểm mốc.
Kiến trúc CNN (Đơn giản hóa):
Ảnh đầu vào (640x480 pixel)
↓
┌─────────────────────────────────────┐
│ Các lớp tích chập │
│ (Trích xuất đặc trưng) │
│ │
│ Conv1 → ReLU → MaxPool │
│ Conv2 → ReLU → MaxPool │
│ Conv3 → ReLU → MaxPool │
│ ... │
│ │
│ Đặc trưng: cạnh, hình dạng, kết cấu│
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Các lớp kết nối đầy đủ │
│ (Phân loại/Hồi quy) │
│ │
│ FC1 → ReLU │
│ FC2 → ReLU │
│ Đầu ra: 468 điểm mốc (x,y,z) │
└─────────────────────────────────────┘Nguồn: Deep Learning Book - Goodfellow et al. (2016)
2.2. MediaPipe FaceMesh Architecture
┌────────────────────────────────────────────────────┐
│ │
│ MediaPipe FaceMesh Pipeline │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ │ │ │ │
│ │ BlazeFace │───▶│ FaceMesh │ │
│ │ Detector │ │ Model │ │
│ │ │ │ │ │
│ └──────────────┘ └──────────────┘ │
│ ↓ ↓ │
│ Face Bounding Box 468 Landmarks (x,y,z) │
│ │
└────────────────────────────────────────────────────┘
BlazeFace:
- Bộ phát hiện khuôn mặt nhẹ
- Chạy ở 200+ FPS trên mobile
- Đầu ra: Hộp giới hạn khuôn mặt
FaceMesh:
- Lấy vùng khuôn mặt đã cắt
- Dự đoán 468 điểm mốc 3D
- Chạy ở 30-60 FPSNguồn: MediaPipe FaceMesh Paper (2019)
2.3. Tại sao 468 landmarks?
468 điểm = Đủ chi tiết cho:
1. Đường viền khuôn mặt: 72 điểm
→ Hình oval, đường hàm, thái dương
2. Mắt: 142 điểm (71 mỗi mắt)
→ Mí mắt, tròng đen, đồng tử
→ Phát hiện nháy mắt
→ Hướng nhìn
3. Lông mày: 66 điểm (33 mỗi lông mày)
→ Nhận dạng biểu cảm
4. Mũi: 55 điểm
→ Hướng khuôn mặt
→ Tính toán góc ✅
5. Miệng: 80 điểm
→ Đồng bộ môi
→ Biểu cảm (cười, nhăn mặt)
6. Hình oval khuôn mặt: 53 điểm
→ Ước tính tư thế đầu ✅Đánh đổi:
- Nhiều điểm mốc hơn = Chính xác hơn, nhưng chậm hơn
- 468 = Điểm tối ưu cho thời gian thực + độ chính xác
3. Toán học: Tính góc khuôn mặt (Yaw, Pitch, Roll)
3.1. Hệ tọa độ 3D
Hệ tọa độ Camera (Quy tắc bàn tay phải):
Y (lên)
↑
│
│
└────────▶ X (phải)
╱
╱
╱
Z (độ sâu, vào màn hình)
Điểm mốc khuôn mặt:
- x: 0 (trái) → 1 (phải)
- y: 0 (trên) → 1 (dưới)
- z: âm (gần) → dương (xa)Nguồn: Computer Vision: Algorithms and Applications - Szeliski (2010)
3.2. Euler Angles (Yaw, Pitch, Roll)
Định nghĩa:
Pitch
↓
╱───────╲
│ (・_・) │
╲───────╱
↑
Yaw
Roll →Yaw (quay đầu trái/phải):
- Quay quanh trục Y (dọc)
- Quay đầu trái/phải
- Phạm vi: -180° đến +180°
- Ví dụ: Lắc đầu "không"
Pitch (ngước/cúi đầu):
- Quay quanh trục X (ngang)
- Ngước/cúi đầu
- Phạm vi: -90° đến +90°
- Ví dụ: Gật đầu "có"
Roll (nghiêng đầu):
- Quay quanh trục Z (độ sâu)
- Nghiêng đầu sang trái/phải
- Phạm vi: -180° đến +180°
- Ví dụ: Nghiêng đầu nghe điện thoại
Ví dụ trực quan:
Yaw = -30° (quay phải):
╱───────╲
│ (・_・) │───▶
╲───────╱
Pitch = +30° (ngước lên):
╱───────╲
│ (・_・) │
╲───────╱
↑
Roll = -45° (nghiêng trái):
╱───────╲
╱ (・_・) │
╱ ────╱Nguồn: Euler Angles - Wikipedia
4. Code Thực Tế: Bước từng bước
Bước 1: Lấy 3 landmarks chính
// ✅ CODE THỰC TẾ từ face-liveness.ts
// Tại sao chọn 3 điểm này?
const p1 = landmarks[127]; // Má trái
const p2 = landmarks[356]; // Má phải
const p3 = landmarks[6]; // Sống mũi
// Lý do:
// - p1, p2: Tạo thành trục X (trục ngang)
// - p3: Tạo thành điểm tham chiếu cho trục Z (độ sâu)
// - 3 điểm không cùng nằm trên 1 đường thẳng
// → Có thể tạo mặt phẳng của khuôn mặtTrực quan:
Góc nhìn phía trước:
p3 (sống mũi)
●
╱│╲
╱ │ ╲
╱ │ ╲
●───┼───●
p1 │ p2
(má trái)(má phải)Nguồn: Face Pose Estimation - OpenCV Docs
Bước 2: Chuyển đổi sang tọa độ pixel
// ✅ CODE THỰC TẾ
function convertToVector(
landmark: { x: number; y: number; z: number },
width: number,
height: number
): number[] {
return [
landmark.x * width, // x: 0-1 → 0-width pixel
landmark.y * height, // y: 0-1 → 0-height pixel
landmark.z * width, // z: tỷ lệ theo width (để nhất quán)
];
}
// Ví dụ:
// Đầu vào: { x: 0.5, y: 0.4, z: -0.02 }
// Ảnh: 640x480
// Đầu ra: [320, 192, -12.8]Tại sao nhân z với width?
- MediaPipe
zkhông có đơn vị rõ ràng - Nhân với
widthđể tỷ lệ về cùng phạm vi với x, y - Giúp tính toán vector đồng nhất
Bước 3: Tạo vectors từ 3 điểm
// ✅ CODE THỰC TẾ
// Vector helper: từ p1 đến p3 (hướng mũi)
const vhelp = [
p3[0] - p1[0], // dx
p3[1] - p1[1], // dy
p3[2] - p1[2], // dz
];
// Vector X: từ p1 đến p2 (trục ngang)
const vx_d = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];Toán học:
Vector AB từ điểm A đến B:
AB = B - A
AB = [Bx - Ax, By - Ay, Bz - Az]
Ví dụ:
A = [100, 200, -5]
B = [300, 250, -3]
AB = [200, 50, 2]Nguồn: Vector Mathematics - Khan Academy
Bước 4: Cross Product (Tích có hướng)
// ✅ CODE THỰC TẾ
function crossProduct(v1: number[], v2: number[]): number[] {
return [
v1[1] * v2[2] - v1[2] * v2[1], // thành phần i
v1[2] * v2[0] - v1[0] * v2[2], // thành phần j
v1[0] * v2[1] - v1[1] * v2[0], // thành phần k
];
}
// vy_d = vhelp × vx_d (vector vuông góc với cả 2)
const vy_d = crossProduct(vhelp, vx_d);Toán học tích có hướng:
Cho 2 vectors:
u = [u1, u2, u3]
v = [v1, v2, v3]
Tích có hướng u × v:
│ i j k │
│ u1 u2 u3 │ = i(u2*v3 - u3*v2) - j(u1*v3 - u3*v1) + k(u1*v2 - u2*v1)
│ v1 v2 v3 │
Kết quả: vector vuông góc với cả u và v
Hướng: Quy tắc bàn tay phảiTrực quan:
u × v
↑
│
u ←──┼──→ v
│
(vuông góc)Nguồn: Cross Product - Wikipedia
Tại sao cần cross product?
- Để tạo trục Y (vertical) của face
- Trục Y phải vuông góc với cả trục X (p1→p2) và hướng mũi (vhelp)
Bước 5: Normalization (Chuẩn hóa vector)
// ✅ CODE THỰC TẾ
function normalizeVector(v: number[]): number[] {
// Tính độ dài (magnitude)
const length = Math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2);
// Chia mỗi thành phần cho độ dài
return [v[0] / length, v[1] / length, v[2] / length];
}
// Chuẩn hóa các vectors
const vx = normalizeVector(vx_d); // Trục X (ngang)
const vy = normalizeVector(vy_d); // Trục Y (dọc)
const vz = normalizeVector(crossProduct(vy_d, vx_d)); // Trục Z (độ sâu)Toán học chuẩn hóa:
Vector v = [x, y, z]
Độ dài (magnitude):
|v| = √(x² + y² + z²)
Vector đơn vị (độ dài = 1):
v̂ = v / |v| = [x/|v|, y/|v|, z/|v|]
Ví dụ:
v = [3, 4, 0]
|v| = √(9 + 16 + 0) = 5
v̂ = [3/5, 4/5, 0] = [0.6, 0.8, 0]
|v̂| = √(0.36 + 0.64) = 1 ✓Tại sao cần chuẩn hóa?
- Ma trận quay yêu cầu cơ sở trực chuẩn (orthonormal basis)
- Mỗi vector phải có độ dài = 1
- Giúp tính toán góc chính xác
Nguồn: Unit Vector - Wikipedia
Bước 6: Tạo Rotation Matrix
// ✅ CODE THỰC TẾ (sử dụng Three.js)
const rotationMatrix3 = new THREE.Matrix3().fromArray([
...vx, // [vx[0], vx[1], vx[2]] → Hàng 1
...vy, // [vy[0], vy[1], vy[2]] → Hàng 2
...vz, // [vz[0], vz[1], vz[2]] → Hàng 3
]);Ma trận quay (3x3):
Trục X Trục Y Trục Z
┌ ┐
│ vx[0] vx[1] vx[2] │ ← Hàng 1
│ vy[0] vy[1] vy[2] │ ← Hàng 2
│ vz[0] vz[1] vz[2] │ ← Hàng 3
└ ┘
Hoặc:
┌ ┐
│ r11 r12 r13 │
│ r21 r22 r23 │
│ r31 r32 r33 │
└ ┘
Tính chất:
- Trực giao: Mỗi hàng/cột vuông góc với các hàng/cột khác
- Chuẩn hóa: Mỗi hàng/cột có độ dài = 1
- Định thức = 1 (chỉ quay, không co giãn)Ý nghĩa vật lý:
- Ma trận này mô tả hướng (orientation) của khuôn mặt trong không gian 3D
- Mỗi hàng là một trục của hệ tọa độ khuôn mặt
Nguồn: Rotation Matrix - Wikipedia
Bước 7: Chuyển đổi sang góc Euler
// ✅ CODE THỰC TẾ
// Chuyển đổi ma trận 3x3 sang 4x4 (yêu cầu bởi Three.js)
const rotationMatrix4 = new THREE.Matrix4().setFromMatrix3(rotationMatrix3);
// Trích xuất góc Euler sử dụng thứ tự ZYX
const euler = new THREE.Euler().setFromRotationMatrix(rotationMatrix4, "ZYX");
// Chuyển đổi radian sang độ
const yaw = THREE.MathUtils.radToDeg(euler.y); // Quay quanh trục Y
const pitch = THREE.MathUtils.radToDeg(euler.x); // Quay quanh trục X
const roll = THREE.MathUtils.radToDeg(euler.z); // Quay quanh trục ZCông thức trích xuất góc Euler (thứ tự ZYX):
Cho ma trận quay:
┌ ┐
│ r11 r12 r13 │
│ r21 r22 r23 │
│ r31 r32 r33 │
└ ┘
Pitch (quay quanh X):
pitch = -asin(r31)
Yaw (quay quanh Y):
yaw = atan2(r21, r11)
Roll (quay quanh Z):
roll = atan2(r32, r33)
Trong đó:
asin(x) = arcsin(x) = sin nghịch đảo
atan2(y, x) = arctan(y/x) = tan nghịch đảo (4 góc phần tư)Nguồn: Three.js Euler Class
Tại sao dùng thứ tự "ZYX"?
- Góc Euler có 12 thứ tự có thể (XYZ, XZY, YXZ, YZX, ZXY, ZYX, ...)
- ZYX = Áp dụng Roll trước, sau đó Pitch, cuối cùng Yaw
- Thường dùng trong robot và hàng không
- Nhất quán với hệ tọa độ MediaPipe
Vấn đề Gimbal Lock:
Cảnh báo: Góc Euler có gimbal lock khi pitch = ±90°
→ Yaw và Roll không phân biệt được
→ Portal giới hạn pitch trong phạm vi nhỏ hơn để tránh vấn đề nàyNguồn: Gimbal Lock - Wikipedia
5. Ví dụ hoàn chỉnh với số cụ thể
Giả sử ta có 3 điểm mốc (tọa độ đã chuẩn hóa):
// Điểm mốc đầu vào (từ MediaPipe)
const landmarks = {
127: { x: 0.3, y: 0.5, z: -0.01 }, // Má trái
356: { x: 0.7, y: 0.5, z: -0.01 }, // Má phải
6: { x: 0.5, y: 0.4, z: -0.05 }, // Sống mũi
};
// Kích thước ảnh
const width = 640;
const height = 480;Tính toán từng bước:
Bước 1: Chuyển đổi sang pixel
p1 = [0.3 * 640, 0.5 * 480, -0.01 * 640] = [192, 240, -6.4];
p2 = [0.7 * 640, 0.5 * 480, -0.01 * 640] = [448, 240, -6.4];
p3 = [0.5 * 640, 0.4 * 480, -0.05 * 640] = [320, 192, -32];Bước 2: Tạo vectors
vhelp = p3 - p1 = [320-192, 192-240, -32-(-6.4)] = [128, -48, -25.6]
vx_d = p2 - p1 = [448-192, 240-240, -6.4-(-6.4)] = [256, 0, 0]Bước 3: Tích có hướng
vy_d = vhelp × vx_d
= [(-48)*0 - (-25.6)*0, (-25.6)*256 - 128*0, 128*0 - (-48)*256]
= [0, -6553.6, 12288]Bước 4: Chuẩn hóa
|vx_d| = √(256² + 0² + 0²) = 256
vx = [256/256, 0/256, 0/256] = [1, 0, 0]
|vy_d| = √(0² + 6553.6² + 12288²) = 13942.4
vy = [0, -0.47, 0.88]
vz = vy_d × vx_d (normalized)
≈ [0, 0.88, 0.47]Bước 5: Ma trận quay
R = ┌ ┐
│ 1.00 0.00 0.00 │
│ 0.00 -0.47 0.88 │
│ 0.00 0.88 0.47 │
└ ┘Bước 6: Trích xuất góc Euler
pitch = -asin(r31) = -asin(0) = 0°
yaw = atan2(r21, r11) = atan2(0, 1) = 0°
roll = atan2(r32, r33) = atan2(0.88, 0.47) ≈ 61.9°
Kết quả:
yaw = 0° (nhìn thẳng)
pitch = 0° (không ngước/cúi)
roll = 61.9° (nghiêng đầu sang phải)6. Xác thực trong Portal
✅ CODE THỰC TẾ: Ngưỡng
// face-liveness.ts
export function faceLiveNessCheck(results: Results, action: string): boolean {
const angles = calculateFaceAngles(results);
switch (action) {
case "forward":
// Yaw: -10° đến +10°
// Pitch: -7° đến +7°
// → Cho phép sai số nhỏ (không phải hoàn toàn thẳng)
return (
angles.yaw >= -10 &&
angles.yaw <= 10 &&
angles.pitch >= -7 &&
angles.pitch <= 7
);
case "left":
// Yaw >= +25°
// → Phải quay rõ ràng (không phải hơi xoay)
return angles.yaw >= 25;
case "right":
// Yaw <= -25°
return angles.yaw <= -25;
case "up":
// Pitch >= +11°
// Yaw: -20° đến +20° (không quay ngang quá nhiều)
return pitchFromLandmarks >= 11 && angles.yaw >= -20 && angles.yaw <= 20;
case "down":
// Pitch <= -9°
return pitchFromLandmarks <= -9 && angles.yaw >= -20 && angles.yaw <= 20;
default:
return false;
}
}Lý do cho các ngưỡng:
Forward (±10°, ±7°):
→ Cho phép người dùng không cần hoàn toàn thẳng
→ Tăng UX (dễ vượt qua hơn)
→ Vẫn đủ chặt để phát hiện giả mạo
Left/Right (25°):
→ 25° = Góc rõ ràng, dễ phát hiện
→ < 25° = Có thể là nhiễu/dao động
→ > 45° = Quá nghiêng, khó thực hiện
Up/Down (11°, 9°):
→ Phát hiện Pitch khó hơn Yaw
→ Ngưỡng thấp hơn để dễ vượt qua
→ Có 2 phương pháp (quay + điểm mốc) để tăng độ chính xác7. Gỡ lỗi & Trực quan hóa
Log console trong code thực tế
// face-liveness.ts
console.log("[Face Liveness]", action, "angles:", {
yaw: angles.yaw.toFixed(2),
pitch: angles.pitch.toFixed(2),
roll: angles.roll.toFixed(2),
});Ví dụ đầu ra:
[Face Liveness] forward angles: { yaw: '2.34', pitch: '-1.56', roll: '0.89' }
[Face Liveness] forward check: true
[Face Liveness] left angles: { yaw: '28.45', pitch: '3.21', roll: '-2.10' }
[Face Liveness] left check: true
[Face Liveness] right angles: { yaw: '-31.67', pitch: '-0.45', roll: '1.23' }
[Face Liveness] right check: trueThêm trực quan hóa (Tùy chọn)
// Vẽ vectors lên canvas để gỡ lỗi
function drawVectors(canvas: HTMLCanvasElement, p1, p2, p3) {
const ctx = canvas.getContext("2d");
// Vẽ điểm mốc
ctx.fillStyle = "red";
ctx.fillRect(p1[0] - 5, p1[1] - 5, 10, 10); // Má trái
ctx.fillRect(p2[0] - 5, p2[1] - 5, 10, 10); // Má phải
ctx.fillRect(p3[0] - 5, p3[1] - 5, 10, 10); // Mũi
// Vẽ trục X (màu đỏ)
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.moveTo(p1[0], p1[1]);
ctx.lineTo(p2[0], p2[1]);
ctx.stroke();
// Vẽ vector helper (màu xanh lá)
ctx.strokeStyle = "green";
ctx.beginPath();
ctx.moveTo(p1[0], p1[1]);
ctx.lineTo(p3[0], p3[1]);
ctx.stroke();
}8. Tài liệu tham khảo
Bài báo & Tài liệu
MediaPipe FaceMesh
Ma trận quay & Góc Euler
Three.js
- Tài liệu: Three.js Euler Class
- Tài liệu: Three.js Matrix3
Đại số tuyến tính
Phát hiện & Nhận dạng khuôn mặt
Code References
Portal Project (Internal)
portal-client/src/utils/ekyc/face-liveness.tsportal-client/src/pages/information/directive/FaceDetectionCamera.vueportal-client/src/utils/ekyc/MediaPipeFaceMeshWrapper.ts
Ví dụ MediaPipe
Ví dụ Three.js
9. Câu hỏi thường gặp & Vấn đề phổ biến
Q1: Tại sao không dùng 4 hoặc 5 điểm mốc thay vì 3?
A: 3 điểm là tối thiểu để xác định một mặt phẳng (plane) trong không gian 3D. Thêm điểm sẽ:
- Tăng độ phức tạp
- Có thể gây nhiễu (nếu các điểm không đồng phẳng)
- Không cải thiện độ chính xác đáng kể
Q2: Gimbal lock có ảnh hưởng đến Portal không?
A: Ít ảnh hưởng vì:
- Portal không yêu cầu người dùng nghiêng đầu quá 45°
- Gimbal lock xảy ra khi pitch = ±90° (cúi/ngước cực độ)
- Các hành động trong portal: forward (0°), up (+11°), down (-9°) → Phạm vi an toàn
Q3: Tại sao dùng Three.js thay vì tự triển khai?
A:
- Three.js đã được tối ưu hóa và kiểm thử kỹ
- Hỗ trợ nhiều thứ tự góc Euler
- Xử lý các trường hợp biên (gimbal lock, lỗi chuẩn hóa)
- Giảm lỗi, tăng khả năng bảo trì
Q4: Độ chính xác của góc tính được là bao nhiêu?
A:
- Lý thuyết: ±0.1° (nếu điểm mốc hoàn hảo)
- Thực tế: ±2-5° (do nhiễu, ánh sáng, chất lượng camera)
- Ngưỡng Portal: ±7-10° → Đủ dung sai cho điều kiện thực tế
10. Tổng kết
Điểm chính
- Phát hiện khuôn mặt = Mô hình ML (CNN) phát hiện khuôn mặt + 468 điểm mốc
- MediaPipe FaceMesh = Nhẹ, thời gian thực, sẵn sàng cho mobile
- 3 điểm mốc (127, 356, 6) → Xác định hướng khuôn mặt
- Ma trận quay = Cơ sở trực chuẩn từ 3 vectors
- Góc Euler = Yaw (trái/phải), Pitch (lên/xuống), Roll (nghiêng)
- Ngưỡng Portal = Được điều chỉnh cho UX + Bảo mật
Tóm tắt quy trình
MediaPipe FaceMesh
↓
468 điểm mốc (x, y, z)
↓
Chọn 3 điểm chính (127, 356, 6)
↓
Chuyển đổi sang tọa độ pixel
↓
Tạo 3 vectors (vhelp, vx_d, vy_d)
↓
Tích có hướng → Vectors trực giao
↓
Chuẩn hóa → Vectors đơn vị
↓
Xây dựng ma trận quay (3x3)
↓
Trích xuất góc Euler (thứ tự ZYX)
↓
Chuyển đổi radian → độ
↓
Xác thực với ngưỡng
↓
faceLiveNessCheck() → true/falseĐọc thêm
Nếu bạn muốn hiểu sâu hơn:
- Bài 06. Liveness Calculation - Code thực tế
- Bài 03. MediaPipe Intro - Thiết lập MediaPipe
- Bài 04. Wrapper Class - Tích hợp với Vue
Chúc bạn học tốt! 🚀