185 lines
9.4 KiB
Markdown
185 lines
9.4 KiB
Markdown
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. |