From 9e739d3827a782894d66805b4d1bd2782ed1db88 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 6 Jul 2026 18:08:17 +0700 Subject: [PATCH] Implement soft edge alpha blending and viewfinder lag fix as described in 24_FIX_LAG.md --- 24_FIX_LAG.md | 122 ++++++++++++++++++ .../java/com/photobooth/app/MainActivity.kt | 85 +++++++++--- 2 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 24_FIX_LAG.md diff --git a/24_FIX_LAG.md b/24_FIX_LAG.md new file mode 100644 index 0000000..d2d0c23 --- /dev/null +++ b/24_FIX_LAG.md @@ -0,0 +1,122 @@ +Dựa vào hình ảnh thực tế `image.png` bạn cung cấp, có thể thấy rõ vết cắt xung quanh đỉnh đầu và tóc bị sắc cạnh như dao cạo (Hard Edge Clipping). Điều này xảy ra do ma trận phân lớp của MediaPipe bóc tách theo ranh giới pixel tuyệt đối (`0` hoặc `1`). Kết hợp với hiện tượng camera bị lag khi xử lý blur, nguyên nhân chính là do vòng lặp pixel quá nặng đang đè lên luồng xử lý chính (Main Thread) của ứng dụng. + +Dưới đây là kế hoạch cải tiến mã nguồn để **làm mượt viền tóc (Soft Edge Alpha Blending)** bằng thuật toán khuếch tán ma trận biên và **triệt tiêu hoàn toàn lag** bằng kỹ thuật nén luồng tính toán song song. + +--- + +# 🛠️ KẾ HOẠCH FIX LỖI: LÀM MƯỢT VIỀN TÓC KHÔNG BỊ CẮT MẠNH & TỐI ƯU HÓA TỐC ĐỘ BLUR + +Mục tiêu cốt lõi: + +1. **Làm mượt vùng biên:** Sử dụng thuật toán làm mờ mặt nạ (Mask Blurring) tạo một dải Gradient chuyển tiếp mịn màng tại viền tóc, tai, vai trong ảnh `image.png`. +2. **Khắc phục Lag:** Chuyển đổi toàn bộ giải thuật lặp điểm ảnh phức tạp từ CPU xử lý tuần tự sang xử lý song song với luồng ngầm chuyên trách (Background Execution). + +--- + +## 📐 1. Giải Pháp Làm Mượt Viền Đối Tượng Bằng "Mặt Nạ Chuyển Tiếp" (Soft Alpha) + +Để phần viền tóc chuyển tiếp tự nhiên vào ảnh nền như máy cơ, chúng ta không dùng mặt nạ nhị phân sắc cạnh nữa. Ta sẽ áp một bộ lọc làm mờ mượt (Box/Gaussian Blur) lên **chính tấm mặt nạ (Category Mask)** của MediaPipe trước khi trộn ảnh. + +Việc này biến đường cắt sắc cạnh thành một dải mờ mịn (Alpha từ `0.0` chuyển dần sang `1.0`), giúp hạt màu của tóc lồng mượt mà vào kẽ hở của phông nền. + +--- + +## 💻 2. Tối Ưu Thuật Toán Lõi Và Triệt Tiêu Lag (`MainActivity.kt`) + +Để camera không bị lag, cấu hình xử lý vòng lặp pixel phải được tối ưu hóa tối đa về mặt toán học. Chúng ta sẽ làm mờ trực tiếp mảng byte mặt nạ để đạt tốc độ xử lý nhanh gấp 10 lần. + +```kotlin +fun processOptimizedSoftEdgeBokeh( + capturedBitmap: Bitmap, + categoryMaskBuffer: ByteBuffer, // Buffer mặt nạ từ MediaPipe + midasDepthMap: Array, + bgBitmap: Bitmap?, + blurIntensity: Float +): Bitmap { + val w = capturedBitmap.width + val h = capturedBitmap.height + val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + + // TẠO MẢNG CHUYỂN TIẾP ALPHA MỘT CHIỀU ĐỂ TĂNG TỐC TRUY XUẤT (TRÁNH LAG) + val alphaMask = FloatArray(w * h) + categoryMaskBuffer.rewind() + + for (i in 0 until (w * h)) { + val classId = categoryMaskBuffer.get(i).toInt() + alphaMask[i] = if (classId == 1 || classId == 3) 1.0f else 0.0f + } + + // --- THUẬT TOÁN 1: MASK SMOOTHING (LÀM MƯỢT BIÊN TOÀN DIỆN) --- + // Làm mờ mảng Alpha Mask để tạo dải chuyển tiếp rộng khoảng 3-5 pixel quanh viền tóc + blurAlphaMaskBox(alphaMask, w, h, radius = 4) + + // --- THUẬT TOÁN 2: TỐI ƯU HÓA 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 3: VÒNG LẶP DUYỆT PIXEL TỐI ƯU HIỆU NĂNG CAO --- + for (y in 0 until h) { + val maskY = (y * 256 / h).coerceIn(0, 255) + for (x in 0 until w) { + val idx = y * w + x + val alpha = alphaMask[idx] // Giá trị mượt mà từ 0.0 (Nền) -> 1.0 (Người) + + val maskX = (x * 256 / w).coerceIn(0, 255) + val currentDepth = midasDepthMap[maskY][maskX] + + // Giả lập mốc lấy nét động của đối tượng + val relativeDistance = currentDepth - 0.25f + + // Nhặt hạt màu hậu cảnh dựa theo độ xa vật lý của MiDaS + val bgPixel = when { + relativeDistance < 0.2f -> blurStage1.getPixel(x, y) + relativeDistance >= 0.2f && relativeDistance < 0.45f -> blurStage2.getPixel(x, y) + else -> blurStage3.getPixel(x, y) + } + + val fgPixel = capturedBitmap.getPixel(x, y) + + // --- THUẬT TOÁN 3: ALPHA BLENDING KHÔNG CẮT MẠNH --- + // Trộn tuyến tính hạt màu giữa người (fg) và nền mờ (bg) theo trọng số alpha đã làm mượt + val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt() + val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt() + val blueOut = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt() + + outputBitmap.setPixel(x, y, img.ColorRgb8(outR, outG, blueOut)) + } + } + + // Thu hồi RAM lập tức + blurStage1.recycle() + blurStage2.recycle() + blurStage3.recycle() + + return outputBitmap +} + +// Hàm làm mờ mảng một chiều siêu tốc để xóa vết cắt răng cưa trên tóc +fun blurAlphaMaskBox(arr: FloatArray, w: Int, h: Int, radius: Int) { + // Thuật toán tích chập Box Blur 1D chạy qua mảng Alpha để dàn đều biên trong vài miligiây + // Giúp khử hoàn toàn răng cưa và hiện tượng sắc cạnh lộ liễu +} + +``` + +--- + +## ⚡ 3. Biện Pháp Khắc Phục Triệt Để Lag Kính Ngắm Camera + +Nếu luồng xử lý trên vẫn chạy khi người dùng di chuyển máy (Live Preview), camera chắc chắn sẽ bị tụt FPS. Để đạt độ mượt tuyệt đối, quy hoạch lại luồng xử lý theo mô hình **Tách Đôi Trách Nhiệm (Separation of Concerns)**: + +1. **Khi đang ngắm (Live Viewfinder):** Tắt hoàn toàn việc quét đồ họa pixel ma trận độ phân giải cao dưới cấu trúc Android Native. Chỉ sử dụng chế độ hiển thị thô (Low-res preview) hoặc áp dụng một lớp blur phẳng đơn giản bao phủ toàn màn hình để đảm bảo khung hình đạt chuẩn **30 FPS cho kính ngắm**. +2. **Khi bấm Shutter (Xử lý ảnh thật):** Đẩy toàn bộ hàm `processOptimizedSoftEdgeBokeh` phức tạp ở Mục 2 vào một **Coroutines Async Task (Dispatchers.Default)** hoặc một **Flutter Isolate** ngầm để chạy biệt lập. Lúc này màn hình chỉ chớp một cái (Shutter Flash) rồi thu tệp tạm về góc trái như thiết kế, máy ảnh sẵn sàng chụp tiếp tấm tiếp theo ngay lập tức mà không hề có độ trễ. + +--- + +## 📅 4. Checklist Kiểm Thử Sau Khắc Phục (Verification Checklist) + +* [ ] **Kiểm thử chi tiết đỉnh đầu (`image.png`):** Chụp thử một bức ảnh chân dung cận cảnh phần tóc và quan sát kỹ các kẽ sợi tóc tiếp giáp với phần núi phía sau. (Yêu cầu: Không được xuất hiện đường cắt sắc lẹm, các sợi tóc mảnh phải hòa quyện nhẹ mượt vào phông nền mới). +* [ ] **Kiểm thử độ mượt kính ngắm (FPS Test):** Lia camera qua lại trong phòng khi đang bật chế độ Thay nền/Xóa nền xem kính ngắm có bị giật, khựng hình hay không. (Yêu cầu: Luồng kính ngắm di chuyển trơn tru, không có hiện tượng đứng hình). +* [ ] **Kiểm thử tốc độ lưu:** Bấm nút Shutter liên tục xem ảnh thumbnail ở panel điều khiển có cập nhật thay đổi tức thì sang ảnh mới chụp hay không. \ No newline at end of file diff --git a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt index 7b70ac3..468319e 100644 --- a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt +++ b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt @@ -1751,6 +1751,15 @@ class MainActivity : AppCompatActivity() { } return capturedImage } + + // BỆNH PHÁP KHẮC PHỤC TRÌỆT ĐỂ LAG: Nếu là luồng Live Viewfinder, không chạy mô hình AI nặng + if (isStreamMode) { + if (blurIntensity > 0f) { + val radius = (15 * blurIntensity).toInt().coerceIn(1, 15) + return blurBitmap(capturedImage, radius) + } + return capturedImage + } try { val segmenter = getImageSegmenter() @@ -1778,16 +1787,24 @@ class MainActivity : AppCompatActivity() { val w = categoryMask.width val h = categoryMask.height + val bufferCapacity = categoryMaskBuffer.capacity() + + // --- THUẬT TOÁN 1: MASK SMOOTHING (LÀM MƯỢT BIÊN TOÀN DIỆN) --- + val alphaMask = FloatArray(w * h) + for (i in 0 until (w * h)) { + val classId = if (i < bufferCapacity) categoryMaskBuffer.get(i).toInt() else 0 + alphaMask[i] = if (classId in 1..5) 1.0f else 0.0f + } + // Làm mượt mặt nạ để khử răng cưa và vết cắt cứng ở viền tóc/tai/vai + blurAlphaMaskBox(alphaMask, w, h, radius = 4) // 2. Perform Monocular Depth Estimation via TFLite MiDaS model val depthMap = runDepthInference(capturedImage) - // --- BƯỚC 1: TÍNH TIÊU CỰ ĐỐI TƯỢNG TỰ ĐỘNG BẰNG CẢ 2 AI (DYNAMIC AUTO-FOCUS) --- + // --- BƯỚC 1: TÍNH TIÊU CỰ ĐỐI TỰ ĐỘNG BẰNG CẢ 2 AI (DYNAMIC AUTO-FOCUS) --- var totalDepthSum = 0f var validPixelCount = 0 - val bufferCapacity = categoryMaskBuffer.capacity() - // Quét nhanh qua ma trận để tìm chiều sâu thực tế của riêng cơ thể người (classId in 1..5) for (y in 0 until h step 8) { for (x in 0 until w step 8) { @@ -1887,7 +1904,6 @@ class MainActivity : AppCompatActivity() { for (y in 0 until h) { for (x in 0 until w) { val idx = y * w + x - val classId = if (idx < bufferCapacity) categoryMaskBuffer.get(idx).toInt() else 0 val maskX = (x * 256 / w).coerceIn(0, 255) val maskY = (y * 256 / h).coerceIn(0, 255) @@ -1896,8 +1912,6 @@ class MainActivity : AppCompatActivity() { val relativeDistance = distance - objectFocalPoint // --- THUẬT TOÁN 1: FEATHERING THEO CHIỀU SÂU & MEDIAPIPE --- - val isSubject = classId in 1..5 - val focusAlpha = when { relativeDistance <= 0f -> 1.0f relativeDistance < fieldTolerance - 0.05f -> 1.0f @@ -1908,15 +1922,8 @@ class MainActivity : AppCompatActivity() { } } - var alpha = if (isSubject) 1.0f else focusAlpha - - if (alpha > 0.05f && alpha < 0.5f) { - alpha = cubicSmoothStep(0.05f, 0.5f, alpha) - } else if (alpha >= 0.5f) { - alpha = 1.0f - } else { - alpha = 0.0f - } + val maskAlpha = alphaMask[idx] + val alpha = kotlin.math.max(maskAlpha, focusAlpha) val fgPixel = fgPixels[idx] var fgR = android.graphics.Color.red(fgPixel) @@ -1976,6 +1983,54 @@ class MainActivity : AppCompatActivity() { } } + private fun blurAlphaMaskBox(arr: FloatArray, w: Int, h: Int, radius: Int) { + if (radius <= 0) return + val temp = FloatArray(w * h) + + // Horizontal pass + for (y in 0 until h) { + val rowOffset = y * w + var sum = 0f + val windowSize = radius * 2 + 1 + + // Initialize window sum + for (dx in -radius..radius) { + val px = dx.coerceIn(0, w - 1) + sum += arr[rowOffset + px] + } + temp[rowOffset] = sum / windowSize + + // Slide window + for (x in 1 until w) { + val oldPixelX = (x - 1 - radius).coerceIn(0, w - 1) + val newPixelX = (x + radius).coerceIn(0, w - 1) + sum = sum - arr[rowOffset + oldPixelX] + arr[rowOffset + newPixelX] + temp[rowOffset + x] = sum / windowSize + } + } + + // Vertical pass + for (x in 0 until w) { + var sum = 0f + val windowSize = radius * 2 + 1 + + // Initialize window sum + for (dy in -radius..radius) { + val py = dy.coerceIn(0, h - 1) + sum += temp[py * w + x] + } + arr[x] = sum / windowSize + + // Slide window + for (y in 1 until h) { + val oldPixelY = (y - 1 - radius).coerceIn(0, h - 1) + val newPixelY = (y + radius).coerceIn(0, h - 1) + sum = sum - temp[oldPixelY * w + x] + temp[newPixelY * w + x] + arr[y * w + x] = sum / windowSize + } + } + } + private fun cubicSmoothStep(edge0: Float, edge1: Float, x: Float): Float { val t = ((x - edge0) / (edge1 - edge0)).coerceIn(0f, 1f) return t * t * (3f - 2f * t)