Skip to content

Tích hợp Thư viện - Flow Dữ liệu Chi tiết

Mục tiêu: Giải thích chi tiết các thư viện được sử dụng trong eKYC, cách dữ liệu chuyển từ thư viện này sang thư viện khác, và cách tạo ra kết quả cuối cùng. Tất cả dựa trên implementation thực tế trong dự án Portal.

1. Tổng quan các Thư viện

1.1. Danh sách Thư viện Chính

typescript
// package.json dependencies
{
  "@mediapipe/face_mesh": "^0.4.1633559619",  // Phát hiện khuôn mặt + 468 landmarks
  "three": "^0.181.0",                          // Tính toán góc Euler từ rotation matrix
  "axios": "^1.7.9",                            // HTTP client cho API calls
  "vue": "^3.5.13",                             // Framework chính
  "howler": "^2.2.4"                            // Audio feedback
}

1.2. Vai trò từng Thư viện

┌─────────────────────────────────────────────────────────────┐
│                                                              │
│  📹 Camera (HTMLVideoElement)                                │
│     ↓                                                         │
│  🔍 MediaPipe FaceMesh                                        │
│     → Phát hiện khuôn mặt                                     │
│     → 468 landmarks (x, y, z)                                │
│     ↓                                                         │
│  📐 face-liveness.ts (Custom Utility)                          │
│     → Chọn 3 landmarks chính (127, 356, 6)                    │
│     → Tạo vectors, cross product, normalize                   │
│     ↓                                                         │
│  🎯 Three.js (Matrix3, Matrix4, Euler)                       │
│     → Tạo rotation matrix                                     │
│     → Trích xuất góc Euler (yaw, pitch, roll)                │
│     ↓                                                         │
│  ✅ faceLiveNessCheck()                                       │
│     → So sánh góc với thresholds                              │
│     → Trả về true/false                                       │
│     ↓                                                         │
│  📸 Canvas/Video Capture                                      │
│     → Chuyển đổi sang base64                                  │
│     ↓                                                         │
│  🌐 Axios (HTTP Client)                                       │
│     → Gửi ảnh + OCR data lên backend                          │
│     → Nhận kết quả verification                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2. Flow Dữ liệu Chi tiết (Giải thích Đơn giản)

Lưu ý: Phần này giải thích bằng ngôn ngữ đơn giản, không đi sâu vào toán học phức tạp. Chỉ cần hiểu "làm gì" và "tại sao", không cần hiểu "như thế nào" chi tiết.

2.1. Bước 1: Camera → MediaPipe FaceMesh

Đầu vào: Video từ camera (giống như bạn đang quay phim)

Làm gì:

  • MediaPipe nhận video từ camera
  • Mỗi 10ms (rất nhanh!), MediaPipe "nhìn" vào 1 khung hình
  • MediaPipe tìm khuôn mặt trong khung hình đó
  • Nếu tìm thấy, MediaPipe đánh dấu 468 điểm trên khuôn mặt (mắt, mũi, miệng, cằm, v.v.)

Đầu ra: Danh sách 468 điểm (gọi là "landmarks")

Ví dụ dễ hiểu:

Giống như bạn vẽ chấm lên ảnh:
- Chấm số 127 = ở má trái
- Chấm số 356 = ở má phải
- Chấm số 6 = ở sống mũi
- ... và 465 chấm khác nữa

Dữ liệu thực tế (bạn không cần hiểu chi tiết, chỉ cần biết có 468 điểm):

typescript
results = {
  multiFaceLandmarks: [
    [
      { x: 0.3, y: 0.5, z: -0.01 }, // Điểm 127 (má trái)
      { x: 0.7, y: 0.5, z: -0.01 }, // Điểm 356 (má phải)
      { x: 0.5, y: 0.4, z: -0.05 }, // Điểm 6 (sống mũi)
      // ... 465 điểm khác
    ],
  ],
  image: { width: 640, height: 480 },
};

2.2. Bước 2: Chọn 3 Điểm Quan trọng

Đầu vào: 468 điểm từ MediaPipe

Làm gì:

  • Trong 468 điểm, chúng ta chỉ cần 3 điểm quan trọng:
    • Điểm 127 = Má trái
    • Điểm 356 = Má phải
    • Điểm 6 = Sống mũi

Tại sao chỉ 3 điểm?

  • Giống như bạn cần 3 điểm để xác định một mặt phẳng (ví dụ: 3 chân bàn)
  • 3 điểm này đủ để biết khuôn mặt đang quay về hướng nào

Ví dụ dễ hiểu:

Nếu bạn nhìn vào gương:
- Má trái và má phải = cho biết bạn quay trái hay phải
- Sống mũi = cho biết bạn ngước hay cúi đầu

Chuyển đổi tọa độ (đơn giản hóa):

  • MediaPipe trả về tọa độ dạng 0-1 (ví dụ: 0.3 = 30% từ trái sang)
  • Chúng ta nhân với kích thước ảnh để ra số pixel thực tế
  • Ví dụ: 0.3 × 640 = 192 pixel từ trái sang

2.3. Bước 3: Tính Toán Hướng Khuôn mặt (Đơn giản hóa)

Đầu vào: 3 điểm (má trái, má phải, sống mũi)

Làm gì:

  • Tính toán xem khuôn mặt đang quay về hướng nào
  • Giống như bạn nhìn vào la bàn để biết hướng Bắc/Nam/Đông/Tây

Ví dụ dễ hiểu:

Nếu bạn quay đầu sang trái:
- Má trái sẽ "xa" camera hơn
- Má phải sẽ "gần" camera hơn
- Code sẽ tính toán và biết: "Ồ, bạn đang quay trái 30 độ!"

Nếu bạn ngước đầu lên:
- Sống mũi sẽ "cao" hơn so với má
- Code sẽ tính toán và biết: "Ồ, bạn đang ngước lên 15 độ!"

Bạn không cần hiểu chi tiết toán học:

  • Code sẽ tự động tính toán các "vector" (mũi tên chỉ hướng)
  • Code sẽ tự động tính "cross product" (phép toán để tìm hướng vuông góc)
  • Code sẽ tự động "normalize" (chuẩn hóa về độ dài = 1)

Chỉ cần biết: Code đang tính toán hướng khuôn mặt dựa trên 3 điểm!


2.4. Bước 4: Chuyển Đổi Thành 3 Góc (Yaw, Pitch, Roll)

Đầu vào: Hướng khuôn mặt (từ bước 3)

Làm gì:

  • Three.js (thư viện toán học) sẽ chuyển đổi hướng thành 3 góc dễ hiểu:
    • Yaw = Quay trái/phải (giống như bạn lắc đầu "không")
    • Pitch = Ngước/cúi (giống như bạn gật đầu "có")
    • Roll = Nghiêng đầu (giống như bạn nghiêng đầu nghe điện thoại)

Ví dụ dễ hiểu:

Nếu bạn nhìn thẳng vào camera:
- Yaw = 0° (không quay trái/phải)
- Pitch = 0° (không ngước/cúi)
- Roll = 0° (không nghiêng)

Nếu bạn quay đầu sang trái 30 độ:
- Yaw = 30° (quay trái)
- Pitch = 0° (không ngước/cúi)
- Roll = 0° (không nghiêng)

Nếu bạn ngước đầu lên 15 độ:
- Yaw = 0° (không quay trái/phải)
- Pitch = 15° (ngước lên)
- Roll = 0° (không nghiêng)

Bạn không cần hiểu "rotation matrix":

  • Đây là công thức toán học phức tạp
  • Three.js đã làm sẵn cho bạn
  • Chỉ cần biết: Input = hướng khuôn mặt, Output = 3 góc (yaw, pitch, roll)

Kết quả:

typescript
{
  yaw: 0,      // 0° = nhìn thẳng
  pitch: 0,    // 0° = không ngước/cúi
  roll: 0      // 0° = không nghiêng
}

2.5. Bước 5: Kiểm Tra Xem Bạn Có Làm Đúng Không?

Đầu vào: 3 góc (yaw, pitch, roll) + Hành động cần làm (forward/left/right/up/down)

Làm gì:

  • So sánh góc thực tế với góc yêu cầu
  • Nếu khớp → Trả về true (đúng!)
  • Nếu không khớp → Trả về false (sai, làm lại!)

Ví dụ dễ hiểu:

Trường hợp 1: "Nhìn thẳng" (forward)

Yêu cầu:
- Yaw: -10° đến +10° (cho phép sai số nhỏ)
- Pitch: -7° đến +7° (cho phép sai số nhỏ)

Nếu bạn nhìn thẳng (yaw = 0°, pitch = 0°):
→ ✅ Đúng! Trả về true

Nếu bạn quay trái quá nhiều (yaw = 20°):
→ ❌ Sai! Trả về false

Trường hợp 2: "Quay trái" (left)

Yêu cầu:
- Yaw >= 25° (phải quay trái rõ ràng, không phải hơi xoay)

Nếu bạn quay trái 30°:
→ ✅ Đúng! Trả về true

Nếu bạn chỉ quay trái 10°:
→ ❌ Sai! Trả về false (quay chưa đủ)

Trường hợp 3: "Ngước lên" (up)

Yêu cầu:
- Pitch >= 11° (phải ngước lên rõ ràng)
- Yaw: -20° đến +20° (không được quay ngang quá nhiều)

Nếu bạn ngước lên 15° và không quay ngang:
→ ✅ Đúng! Trả về true

Nếu bạn ngước lên nhưng quay ngang 30°:
→ ❌ Sai! Trả về false (quay ngang quá nhiều)

Kết quả: true hoặc false (đúng hoặc sai)


2.6. Bước 6: Chụp Ảnh Khi Đã Làm Đúng

Đầu vào: Video từ camera (khi liveness check = true)

Làm gì:

  1. Đợi 500ms (nửa giây) để đảm bảo ảnh không bị mờ
  2. "Chụp" 1 khung hình từ video (giống như bạn chụp ảnh màn hình)
  3. Chuyển đổi ảnh thành chuỗi text (gọi là "base64")

Ví dụ dễ hiểu:

Giống như bạn:
1. Quay phim bằng điện thoại
2. Dừng lại ở 1 khung hình đẹp
3. Chụp ảnh màn hình
4. Gửi ảnh đó cho bạn bè

Code cũng làm vậy:
1. Video đang chạy
2. Dừng lại ở 1 khung hình (khi bạn làm đúng)
3. "Chụp" khung hình đó
4. Chuyển thành text để gửi lên server

Tại sao đợi 500ms?

  • Camera cần thời gian để ổn định
  • Giảm blur (mờ) trong ảnh
  • Ảnh sẽ rõ nét hơn

Kết quả: Một chuỗi text dài (base64) đại diện cho ảnh


2.7. Bước 7: Gửi Ảnh Lên Server

Đầu vào:

  • Ảnh khuôn mặt (base64)
  • Ảnh CCCD mặt trước (base64)
  • Ảnh CCCD mặt sau (base64)
  • Thông tin OCR (đã đọc từ CCCD)

Làm gì:

  • Gửi tất cả dữ liệu lên server qua HTTP request
  • Server sẽ so sánh khuôn mặt với ảnh trên CCCD
  • Server trả về kết quả: thành công hay thất bại

Ví dụ dễ hiểu:

Giống như bạn:
1. Chụp ảnh khuôn mặt
2. Chụp ảnh CCCD
3. Gửi tất cả lên server
4. Server kiểm tra: "Khuôn mặt này có khớp với ảnh trên CCCD không?"
5. Server trả về: "Khớp!" hoặc "Không khớp!"

Kết quả:

typescript
{
  message: {
    error_code: 'SUCCESS',
    error_message: 'Verification successful',
  }
}

Hoặc nếu lỗi:

typescript
{
  message: {
    error_code: 'ERR01',
    error_message: 'Khuôn mặt không khớp với CCCD',
  }
}

3. Sơ đồ Flow Đơn giản (Dễ hiểu)

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  📹 BƯỚC 1: Camera                                         │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Video từ camera (giống như quay phim)                │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  🔍 BƯỚC 2: MediaPipe Tìm Khuôn mặt                        │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ MediaPipe "nhìn" vào video                            │ │
│  │ Tìm thấy khuôn mặt → Đánh dấu 468 điểm               │ │
│  │ (mắt, mũi, miệng, cằm, v.v.)                          │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  📐 BƯỚC 3: Chọn 3 Điểm Quan trọng                         │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Trong 468 điểm, chọn 3 điểm:                         │ │
│  │ - Má trái (điểm 127)                                   │ │
│  │ - Má phải (điểm 356)                                   │ │
│  │ - Sống mũi (điểm 6)                                    │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  🎯 BƯỚC 4: Tính Toán Hướng Khuôn mặt                      │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Dựa vào 3 điểm, tính toán:                            │ │
│  │ - Bạn đang quay trái/phải bao nhiêu độ? (Yaw)         │ │
│  │ - Bạn đang ngước/cúi bao nhiêu độ? (Pitch)            │ │
│  │ - Bạn đang nghiêng bao nhiêu độ? (Roll)               │ │
│  │                                                         │ │
│  │ Ví dụ: Yaw = 0°, Pitch = 0°, Roll = 0°                │ │
│  │ → Bạn đang nhìn thẳng!                                 │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  ✅ BƯỚC 5: Kiểm Tra Xem Bạn Có Làm Đúng Không?            │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ So sánh góc thực tế với góc yêu cầu:                  │ │
│  │                                                         │ │
│  │ Nếu yêu cầu "nhìn thẳng":                              │ │
│  │   → Yaw: -10° đến +10° ✓                               │ │
│  │   → Pitch: -7° đến +7° ✓                               │ │
│  │   → Kết quả: TRUE (đúng!)                              │ │
│  │                                                         │ │
│  │ Nếu yêu cầu "quay trái":                               │ │
│  │   → Yaw >= 25° ✓                                       │ │
│  │   → Kết quả: TRUE (đúng!)                              │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  📸 BƯỚC 6: Chụp Ảnh (Khi Đã Làm Đúng)                      │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Đợi 500ms → Chụp 1 khung hình từ video                │ │
│  │ Chuyển đổi thành text (base64)                        │ │
│  └───────────────────────────────────────────────────────┘ │
│                        ↓                                     │
│  🌐 BƯỚC 7: Gửi Lên Server                                  │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Gửi ảnh khuôn mặt + ảnh CCCD lên server              │ │
│  │ Server so sánh: "Khuôn mặt có khớp với CCCD không?"  │ │
│  │                                                         │ │
│  │ Response: {                                            │ │
│  │   error_code: 'SUCCESS',                               │ │
│  │   error_message: 'Verification successful'             │ │
│  │ }                                                       │ │
│  └───────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Tóm tắt bằng lời:

  1. Camera quay video
  2. MediaPipe tìm khuôn mặt và đánh dấu 468 điểm
  3. Chọn 3 điểm quan trọng (má trái, má phải, sống mũi)
  4. Tính toán xem bạn đang quay về hướng nào (yaw, pitch, roll)
  5. Kiểm tra xem bạn có làm đúng yêu cầu không
  6. Nếu đúng → Chụp ảnh
  7. Gửi ảnh lên server để xác thực

4. Code Examples Từ Dự án Thực tế

4.1. MediaPipeFaceMeshWrapper.ts

typescript
// ✅ CODE THỰC TẾ từ MediaPipeFaceMeshWrapper.ts

export class MediaPipeFaceMeshWrapper {
  private faceMesh: any = null;
  private videoElement: HTMLVideoElement | null = null;
  private intervalId: number | null = null;

  async initialize(): Promise<void> {
    // Dynamic import để tránh build issues
    const faceMeshModule = await import("@mediapipe/face_mesh");
    const FaceMeshClass = faceMeshModule.FaceMesh;

    // Tạo instance với local files
    this.faceMesh = new FaceMeshClass({
      locateFile: (file: string) => `/face_mesh/${file}`,
    });

    // Cấu hình options
    this.faceMesh.setOptions({
      selfieMode: true,
      maxNumFaces: 1,
      refineLandmarks: true,
      minDetectionConfidence: 0.5,
      minTrackingConfidence: 0.5,
    });
  }

  startDetection(): void {
    // Gửi video frame mỗi 10ms (≈100 FPS)
    this.intervalId = window.setInterval(async () => {
      if (this.videoElement && this.videoElement.readyState === 4) {
        await this.faceMesh.send({ image: this.videoElement });
      }
    }, 10);
  }

  onResults(callback: (results: Results) => void): void {
    this.faceMesh.onResults(callback);
  }
}

4.2. face-liveness.ts

typescript
// ✅ CODE THỰC TẾ từ face-liveness.ts

import * as THREE from "three";

export function calculateFaceAngles(results: Results): IFaceAngles {
  // 1. Chọn 3 landmarks chính
  const landmarks = results.multiFaceLandmarks[0];
  const p1 = landmarks[127]; // Má trái
  const p2 = landmarks[356]; // Má phải
  const p3 = landmarks[6]; // Sống mũi

  // 2. Chuyển đổi sang pixel coordinates
  const width = results.image.width;
  const height = results.image.height;
  const p1_vec = [p1.x * width, p1.y * height, p1.z * width];
  const p2_vec = [p2.x * width, p2.y * height, p2.z * width];
  const p3_vec = [p3.x * width, p3.y * height, p3.z * width];

  // 3. Tạo vectors
  const vhelp = [
    p3_vec[0] - p1_vec[0],
    p3_vec[1] - p1_vec[1],
    p3_vec[2] - p1_vec[2],
  ];
  const vx_d = [
    p2_vec[0] - p1_vec[0],
    p2_vec[1] - p1_vec[1],
    p2_vec[2] - p1_vec[2],
  ];
  const vy_d = crossProduct(vhelp, vx_d);

  // 4. Chuẩn hóa
  const vx = normalizeVector(vx_d);
  const vy = normalizeVector(vy_d);
  const vz = normalizeVector(crossProduct(vy_d, vx_d));

  // 5. Tạo rotation matrix và trích xuất góc Euler
  const rotationMatrix3 = new THREE.Matrix3().fromArray([...vx, ...vy, ...vz]);
  const rotationMatrix4 = new THREE.Matrix4().setFromMatrix3(rotationMatrix3);
  const euler = new THREE.Euler().setFromRotationMatrix(rotationMatrix4, "ZYX");

  return {
    yaw: THREE.MathUtils.radToDeg(euler.y),
    pitch: THREE.MathUtils.radToDeg(euler.x),
    roll: THREE.MathUtils.radToDeg(euler.z),
  };
}

4.3. FaceDetectionCamera.vue

typescript
// ✅ CODE THỰC TẾ từ FaceDetectionCamera.vue

import { MediaPipeFaceMeshWrapper } from "@/utils/ekyc/MediaPipeFaceMeshWrapper";
import { faceLiveNessCheck, type Results } from "@/utils/ekyc/face-liveness";

let faceMeshWrapper: MediaPipeFaceMeshWrapper | null = null;
let videoElement: HTMLVideoElement | null = null;

onMounted(async () => {
  // 1. Khởi tạo wrapper
  faceMeshWrapper = new MediaPipeFaceMeshWrapper();
  await faceMeshWrapper.initialize();

  // 2. Set video element
  faceMeshWrapper.setVideoElement(videoElement);

  // 3. Đăng ký callback
  faceMeshWrapper.onResults((results: Results) => {
    // 4. Kiểm tra liveness
    const currentAction = randomActionSequence.value[step.value]?.action;
    if (faceLiveNessCheck(results, currentAction)) {
      validFrameCount.value += 1;

      // 5. Khi đủ frames, capture image
      if (validFrameCount.value >= VALID_FRAME) {
        captureFinalImage();
      }
    }
  });

  // 6. Bắt đầu detection loop
  faceMeshWrapper.startDetection();
});

const captureFinalImage = async () => {
  // Đợi 500ms để đảm bảo ảnh không bị mờ
  await new Promise((resolve) => setTimeout(resolve, 500));

  // Tạo canvas và capture
  const canvas = document.createElement("canvas");
  canvas.width = videoElement.videoWidth;
  canvas.height = videoElement.videoHeight;
  const ctx = canvas.getContext("2d");
  ctx?.drawImage(videoElement, 0, 0);

  // Chuyển đổi sang base64
  const imageBase64 = canvas.toDataURL("image/jpeg", 0.95);

  // Emit event
  emit("takingImage", imageBase64);
};

4.4. face-verification.ts

typescript
// ✅ CODE THỰC TẾ từ face-verification.ts

import apiClient from "@/config/http-common";

export async function getFaceVerificationData(
  frontCardImage: string,
  faceImage: string,
  backCardImage: string,
  frontCardData: IOcrData,
  backCardData: IOcrData
): Promise<IFaceVerificationData> {
  // 1. Chuẩn bị request body
  const request: IFaceVerificationRequest = {
    frontEndBase64: frontCardImage,
    faceidBase64: faceImage,
    backEndBase64: backCardImage,
    imageCardData: frontCardData,
    imageCardBackendData: backCardData,
    // ... other fields
  };

  // 2. Gửi request qua Axios
  const response = await apiClient().post("api/v1/ekyc/verify-face", request, {
    headers: qrToken ? { qrToken } : {},
  });

  // 3. Parse response
  const portalResponse = response.data;
  if (portalResponse?.status === "error") {
    return {
      message: {
        error_code: portalResponse.values.error_code,
        error_message: portalResponse.values.error_message,
      },
    };
  }

  return portalResponse;
}

5. Data Types & Interfaces

5.1. MediaPipe Results

typescript
interface Results {
  multiFaceLandmarks?: Array<
    Array<{
      x: number; // 0-1 (normalized)
      y: number; // 0-1 (normalized)
      z: number; // depth (relative, có thể âm)
    }>
  >;
  image?: {
    width: number;
    height: number;
    toDataURL?: (format: string) => string;
  };
}

5.2. Face Angles

typescript
interface IFaceAngles {
  yaw: number; // Độ (quay trái/phải)
  pitch: number; // Độ (ngước/cúi)
  roll: number; // Độ (nghiêng)
}

5.3. Face Verification Request

typescript
interface IFaceVerificationRequest {
  frontEndBase64: string; // Base64 image từ OCR front
  faceidBase64: string; // Base64 image từ face capture
  backEndBase64: string; // Base64 image từ OCR back
  imageCardData: IOcrData; // OCR results từ front
  imageCardBackendData: IOcrData; // OCR results từ back
  cookieEmail?: string;
  cookiePhone?: string;
  request_id?: string;
  hostname?: string;
  fileFrontEnd?: { name: string; type: string; size: number };
  fileBackEnd?: { name: string; type: string; size: number };
  fileFaceid?: { name: string; type: string; size: number };
}

5.4. Face Verification Response

typescript
interface IFaceVerificationData {
  message: {
    error_code: string;
    error_message: string;
  };
  // ... other fields
}

6. Performance & Optimization

6.1. Detection Loop Frequency

typescript
// MediaPipeFaceMeshWrapper.ts
// Gửi frame mỗi 10ms (≈100 FPS)
setInterval(async () => {
  await faceMesh.send({ image: videoElement });
}, 10);

Lý do:

  • MediaPipe có thể xử lý ~100 FPS trên mobile
  • 10ms interval = đủ nhanh để phát hiện chuyển động mượt mà
  • Không quá nhanh để tránh lãng phí CPU

6.2. Image Capture Delay

typescript
// FaceDetectionCamera.vue
// Đợi 500ms trước khi capture để đảm bảo ảnh không bị mờ
await new Promise((resolve) => setTimeout(resolve, 500));

Lý do:

  • Camera cần thời gian để ổn định sau khi user thực hiện action
  • Giảm blur trong ảnh cuối cùng
  • Cải thiện chất lượng verification

6.3. Base64 Image Quality

typescript
// JPEG quality: 0.95 (95%)
canvas.toDataURL("image/jpeg", 0.95);

Lý do:

  • Balance giữa chất lượng và kích thước file
  • 0.95 = đủ chất lượng cho face verification
  • Giảm kích thước so với PNG (100%)

7. Error Handling

7.1. MediaPipe Initialization Errors

typescript
try {
  await faceMeshWrapper.initialize();
} catch (error) {
  console.error("MediaPipe init error:", error);
  // Fallback: Show error message to user
  message.error("Không thể khởi tạo camera. Vui lòng thử lại.");
}

7.2. Face Detection Errors

typescript
faceMeshWrapper.onResults((results: Results) => {
  try {
    if (
      !results.multiFaceLandmarks ||
      results.multiFaceLandmarks.length === 0
    ) {
      // Không phát hiện khuôn mặt
      return;
    }
    // ... process results
  } catch (error) {
    console.error("Face detection error:", error);
  }
});

7.3. API Call Errors

typescript
try {
  const response = await apiClient().post("api/v1/ekyc/verify-face", request);
  // ... handle success
} catch (error: any) {
  if (error.response?.data?.values?.error_message) {
    // Hiển thị lỗi từ backend
    message.error(error.response.data.values.error_message);
  } else {
    // Lỗi network hoặc unknown
    message.error("Có lỗi xảy ra. Vui lòng thử lại.");
  }
}

8. Tổng kết (Đơn giản)

8.1. Flow Tóm tắt Bằng Lời

1. Camera quay video
2. MediaPipe tìm khuôn mặt → Đánh dấu 468 điểm
3. Chọn 3 điểm quan trọng (má trái, má phải, sống mũi)
4. Tính toán hướng khuôn mặt → Ra 3 góc (yaw, pitch, roll)
5. Kiểm tra xem có làm đúng yêu cầu không
6. Nếu đúng → Chụp ảnh
7. Gửi ảnh lên server để xác thực

8.2. Điểm Chính (Dễ hiểu)

  1. MediaPipe FaceMesh:

    • Làm gì: Tìm khuôn mặt và đánh dấu 468 điểm
    • Giống như: Bạn vẽ chấm lên ảnh để đánh dấu các bộ phận
  2. face-liveness.ts:

    • Làm gì: Chọn 3 điểm quan trọng, tính toán hướng khuôn mặt
    • Giống như: Bạn nhìn vào la bàn để biết hướng
  3. Three.js:

    • Làm gì: Chuyển đổi hướng thành 3 góc dễ hiểu (yaw, pitch, roll)
    • Giống như: Bạn chuyển đổi "hướng Bắc" thành "0 độ"
  4. faceLiveNessCheck:

    • Làm gì: So sánh góc thực tế với góc yêu cầu
    • Giống như: Bạn kiểm tra xem có làm đúng bài tập không
  5. Canvas API:

    • Làm gì: Chụp ảnh từ video
    • Giống như: Bạn chụp ảnh màn hình
  6. Axios:

    • Làm gì: Gửi ảnh lên server
    • Giống như: Bạn gửi email kèm file đính kèm

8.3. Thư viện Được Dùng (Đơn giản)

  • @mediapipe/face_mesh: Tìm khuôn mặt
  • three: Tính toán góc (bạn không cần hiểu chi tiết)
  • axios: Gửi dữ liệu lên server
  • vue: Framework chính (giao diện)

Lưu ý: Bạn không cần hiểu chi tiết toán học của Three.js. Chỉ cần biết nó tính toán góc cho bạn!


9. Câu hỏi Thường gặp (FAQ)

Q1: Tôi không hiểu "vector", "cross product", "normalize" là gì?

A: Không sao cả! Bạn không cần hiểu chi tiết. Chỉ cần biết:

  • Code đang tính toán hướng khuôn mặt dựa trên 3 điểm
  • Three.js sẽ tự động làm tất cả phép toán phức tạp cho bạn
  • Kết quả cuối cùng là 3 góc dễ hiểu: yaw, pitch, roll

Ví dụ: Giống như bạn không cần biết động cơ xe hoạt động như thế nào, chỉ cần biết nhấn ga là xe chạy!

Q2: Tại sao cần 3 điểm? Tại sao không dùng 1 hoặc 2 điểm?

A:

  • 1 điểm: Chỉ biết vị trí, không biết hướng
  • 2 điểm: Chỉ biết 1 đường thẳng, không biết mặt phẳng
  • 3 điểm: Đủ để xác định mặt phẳng và hướng

Ví dụ: Giống như bạn cần 3 chân để đặt bàn vững chắc!

Q3: "Rotation matrix" là gì? Tôi có cần hiểu không?

A: Không cần! Đây là công thức toán học phức tạp. Chỉ cần biết:

  • Input: Hướng khuôn mặt (từ 3 điểm)
  • Output: 3 góc (yaw, pitch, roll)
  • Three.js tự động làm tất cả cho bạn

Ví dụ: Giống như bạn không cần biết công thức nấu ăn, chỉ cần biết món ăn ngon!

Q4: Tại sao phải đợi 500ms trước khi chụp ảnh?

A:

  • Camera cần thời gian để ổn định
  • Giảm blur (mờ) trong ảnh
  • Ảnh sẽ rõ nét hơn

Ví dụ: Giống như bạn chụp ảnh, cần đợi camera focus xong mới chụp!

Q5: "Base64" là gì? Tại sao phải chuyển đổi?

A:

  • Base64 là cách chuyển đổi ảnh thành text
  • Dễ dàng gửi qua HTTP request
  • Server có thể nhận và chuyển lại thành ảnh

Ví dụ: Giống như bạn zip file để gửi email, base64 là cách "zip" ảnh thành text!

Q6: Tôi có cần học toán để hiểu phần này không?

A: Không cần!

  • Phần này chỉ cần hiểu "làm gì" và "tại sao"
  • Không cần hiểu "như thế nào" chi tiết
  • Code đã làm tất cả phép toán phức tạp cho bạn

Ví dụ: Giống như bạn không cần học cách làm bánh để ăn bánh!


10. Tài liệu Tham khảo

Code Files

  • portal-client/src/utils/ekyc/MediaPipeFaceMeshWrapper.ts
  • portal-client/src/utils/ekyc/face-liveness.ts
  • portal-client/src/pages/information/directive/FaceDetectionCamera.vue
  • portal-client/src/utils/ekyc/face-verification.ts
  • portal-client/src/utils/ekyc/ocr.ts

Happy Coding! 🚀

Internal documentation for iNET Portal