fix: sử dụng AI để nhận diện khuôn mặt và focus

This commit is contained in:
2026-07-06 17:25:21 +07:00
parent bc3b3533a7
commit 88fc095bdc
23 changed files with 2191 additions and 60 deletions
+185
View File
@@ -0,0 +1,185 @@
Dưới đây là kế hoạch Markdown chi tiết để thay thế hoàn toàn cụm Google ML Kit Selfie Segmentation cũ bằng **Google MediaPipe Image Segmentation (Chế độ Đa lớp - Multiclass)**. Sự kết hợp này sẽ giúp ứng dụng nhận diện và bóc tách chính xác không chỉ con người (tóc, tai, cơ thể) mà còn cả động vật và các đồ vật thông dụng (ly nước, gấu bông, bàn ghế), từ đó cung cấp một tấm mặt nạ chuẩn để mô hình **MiDaS** tính toán khoảng cách DoF vật lý.
---
# 📊 KẾ HOẠCH PHÁT TRIỂN: TÍCH HỢP GOOGLE MEDIAPIPE IMAGE SEGMENTATION ĐA LỚP
Mục tiêu cốt lõi:
1. Gỡ bỏ hoàn toàn thư viện và tàn dư của `ML Kit Selfie Segmentation`.
2. Tích hợp `MediaPipe Image Segmenter` sử dụng file mô hình đa lớp (ví dụ: `deeplabv3` hoặc `selfie_multiclass` dạng `.tflite`).
3. Trích xuất nhãn phân loại (Category Mask) của từng pixel (Người = 1, Đồ vật = 3, Nền = 0...).
4. Kết hợp nhãn phân loại này với bản đồ chiều sâu MiDaS để khóa tiêu cự DoF chuẩn xác cho cả người và vật.
---
## 📦 1. Tái Cấu Trúc Thư Viện Hệ Thống (`build.gradle`)
Trong dự án Android Native (`MainActivity.kt`), chúng ta cần thực hiện hoán đổi gói thư viện trong file `app/build.gradle`:
```groovy
dependencies {
// ❌ XÓA BỎ HOÀN TOÀN gói ML Kit cũ
// implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta4'
// 🟢 BỔ SUNG thư viện lõi của Google MediaPipe Tasks Vision
implementation 'com.google.mediapipe:tasks-vision:0.10.14'
}
```
> 📥 **Tải File Mô Hình:** Bạn cần tải file mô hình đa lớp `selfie_multiclass_256x256.tflite` hoặc `deeplabv3.tflite` (nặng khoảng ~5MB - 8MB) từ trang chủ Google MediaPipe, đặt vào thư mục `app/src/main/assets/models/multiclass_segmenter.tflite`.
---
## 📐 2. Khởi Tạo MediaPipe Image Segmenter (`MainActivity.kt`)
Thay vì khởi tạo `SelfieSegmenter` của ML Kit, chúng ta thiết lập `ImageSegmenter` của MediaPipe với cấu hình đầu ra dạng **Category Mask** (Mặt nạ phân lớp pixel).
```kotlin
import com.google.mediapipe.tasks.core.BaseOptions
import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenter
import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenter.ImageSegmenterOptions
class ImageProcessingEngine(private val context: Context) {
private var imageSegmenter: ImageSegmenter? = null
init {
// Cấu hình nạp file mô hình từ thư mục Assets
val baseOptions = BaseOptions.builder()
.setModelAssetPath("models/multiclass_segmenter.tflite")
.build()
val options = ImageSegmenterOptions.builder()
.setBaseOptions(baseOptions)
.setOutputCategoryMask(true) // Bật chế độ xuất mặt nạ phân lớp (0, 1, 2, 3...)
.setOutputConfidenceMasks(false) // Tắt bản đồ tin cậy nhị phân để tiết kiệm RAM
.build()
imageSegmenter = ImageSegmenter.createFromOptions(context, options)
}
}
```
---
## 💻 3. Thuật Toán Lõi: Phối Hợp MediaPipe Đa Lớp & MiDaS DoF
Hàm xử lý pixel dưới nền `processDualAiBokeh` sẽ được viết lại hoàn toàn. MediaPipe sẽ phân tích ảnh chụp thành một mảng số nguyên đại diện cho các nhãn (Labels).
*Theo bảng nhãn chuẩn của Google MediaPipe:*
* **ID 0:** Background (Phông nền)
* **ID 1:** Person (Con người - bao gồm cả tóc, tai, quần áo)
* **ID 2:** Selfie-Grooming/Accessories (Phụ kiện, mũ, kính)
* **ID 3:** Đồ vật/Thực thể tùy chọn (Tùy thuộc vào model DeepLabv3 bạn chọn)
```kotlin
import android.graphics.Bitmap
import com.google.mediapipe.framework.image.BitmapImageBuilder
import java.nio.ByteBuffer
fun processMediaPipeWithMidasBokeh(
capturedBitmap: Bitmap,
midasDepthMap: Array<FloatArray>, // Ma trận chiều sâu từ MiDaS (0.0 -> 1.0)
bgBitmap: Bitmap?,
blurIntensity: Float
): Bitmap {
val w = capturedBitmap.width
val h = capturedBitmap.height
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
// --- BƯỚC 1: CHẠY MEDIAPIPE ĐỂ TRÍCH XUẤT MẶT NẠ ĐA LỚP ---
val mpImage = BitmapImageBuilder(capturedBitmap).build()
val segmentationResult = imageSegmenter?.segment(mpImage)
// Lấy ma trận Category Mask dưới dạng ByteBuffer
val categoryMaskBuffer: ByteBuffer = segmentationResult?.categoryMask()?.get()?.byteBuffer ?: return capturedBitmap
categoryMaskBuffer.rewind()
// --- BƯỚC 2: TÍNH TIÊU CỰ TRUNG BÌNH CỦA ĐỐI TƯỢNG (NGƯỜI HOẶC VẬT CHÍNH) ---
var totalDepthSum = 0f
var validPixelCount = 0
for (y in 0 until h step 8) {
for (x in 0 until w step 8) {
val bufferIndex = y * w + x
val classId = categoryMaskBuffer.get(bufferIndex).toInt() // Nhận diện ID vật thể tại pixel này
// LỢI ÍCH ĐA LỚP: Nếu pixel thuộc về NGƯỜI (1) hoặc VẬT THỂ được lấy nét (ví dụ: 3, 4)
if (classId == 1 || classId == 3) {
val maskX = (x * 256 / w).coerceIn(0, 255)
val maskY = (y * 256 / h).coerceIn(0, 255)
totalDepthSum += midasDepthMap[maskY][maskX]
validPixelCount++
}
}
}
// Xác định mốc tiêu cự vật lý động (Tự động dịch chuyển theo người/vật cho dù đứng xa hay gần)
val dynamicFocalPoint = if (validPixelCount > 0) totalDepthSum / validPixelCount else 0.25f
val fieldTolerance = 0.12f // Độ dày trường ảnh (DoF) giữ nét cho các chi tiết tai, vai
// --- BƯỚC 3: TẠO CÁC LỚP BLUR NỀN PHÂN CẤP ---
val baseBg = bgBitmap ?: capturedBitmap
val blurStage1 = boxBlur(baseBg, (5 * blurIntensity).toInt().coerceAtLeast(1))
val blurStage2 = boxBlur(baseBg, (13 * blurIntensity).toInt().coerceAtLeast(1))
val blurStage3 = boxBlur(baseBg, (25 * blurIntensity).toInt().coerceAtLeast(1))
// --- BƯỚC 4: HÒA TRỘN PIXEL XUẤT XƯỞNG ---
for (y in 0 until h) {
for (x in 0 until w) {
val idx = y * w + x
val classId = categoryMaskBuffer.get(idx).toInt() // Nhãn vật thể từ MediaPipe
val maskX = (x * 256 / w).coerceIn(0, 255)
val maskY = (y * 256 / h).coerceIn(0, 255)
val currentDepth = midasDepthMap[maskY][maskX] // Khoảng cách từ MiDaS
val relativeDistance = currentDepth - dynamicFocalPoint
val finalPixel = when {
// ĐIỀU KIỆN GIỮ NÉT CỐ ĐỊNH:
// Hoặc là pixel thuộc cơ thể Người/Vật chính (classId == 1 hoặc 3)
// Hoặc là pixel nằm trong vùng an toàn tiêu cự (DoF)
(classId == 1 || classId == 3) || relativeDistance <= fieldTolerance -> {
capturedBitmap.getPixel(x, y) // Giữ độ nét gốc 100%
}
// CÁC VÙNG CÒN LẠI (HẬU CẢNH XA DẦN): Áp dụng lớp mờ tăng tiến theo chiều sâu MiDaS
relativeDistance > fieldTolerance && relativeDistance < 0.25f -> blurStage1.getPixel(x, y)
relativeDistance >= 0.25f && relativeDistance < 0.5f -> blurStage2.getPixel(x, y)
else -> blurStage3.getPixel(x, y) // Nền xa vô cực tạo hiệu ứng Bokeh tròn mịn
}
outputBitmap.setPixel(x, y, finalPixel)
}
}
// Thu hồi RAM
blurStage1.recycle()
blurStage2.recycle()
blurStage3.recycle()
return outputBitmap
}
```
---
## 🔄 4. Quy Trình Vận Hành Mới Sau Khi Nâng Cấp
Khi người dùng thực hiện thao tác chụp ảnh bằng nút Shutter, chuỗi xử lý bất đồng bộ (Isolate Thread) sẽ chạy như sau:
1. **Nhận diện đa mục tiêu:** Ứng dụng đưa ảnh vào MediaPipe Image Segmenter. Nếu trong khung hình là bạn đang cầm một tách cà phê, MediaPipe sẽ gom cụm cả cơ thể bạn (ID 1) và tách cà phê (ID 3) thành một vùng chọn thống nhất.
2. **Khóa tiêu cự thông minh:** Ứng dụng tính toán khoảng cách trung bình của cụm (Bạn + Tách cà phê) trên bản đồ MiDaS.
3. **Xóa phông lập thể:** Toàn bộ hậu cảnh xung quanh (bức tường, cửa sổ phía xa) sẽ bị làm mờ mịn màng tăng dần, trong khi bạn và tách cà phê được giữ nét căng tuyệt đối từ viền tóc cho đến góc cạnh đồ vật.
---
## 📅 5. Checklist Kiểm Thử Toàn Diện (Testing Checklist)
* [ ] **Kiểm tra biên dịch:** Chạy `./gradlew assembleDebug` xem hệ thống có bị xung đột thư viện giữa MediaPipe và TensorFlow Lite cũ hay không.
* [ ] **Test vật thể không phải người:** Đặt một đồ vật (ví dụ: gấu bông hoặc một chiếc hộp) lên bàn, bấm chụp xóa phông xem hệ thống có nhận diện được vật thể để giữ nét và làm mờ nền xung quanh không.
* [ ] **Test lỗi lùi xa camera:** Di chuyển người ra xa ống kính xem vùng giữ nét có tự động co giãn theo tỷ lệ cơ thể bạn mà không bị dính vệt mờ hay không.