Files
photobooth-app/29_FIX_FOCUS.md

11 KiB

Dựa vào hình ảnh thực tế image_def201.jpg bạn cung cấp và mô tả lỗi, chúng ta đang gặp phải 2 vấn đề kỹ thuật nghiêm trọng về mặt xử lý thuật toán và quản lý luồng phần cứng:

🔴 Phân tích nguyên nhân lỗi:

  1. Lỗi lấy nét "Vòng tròn phẳng" (image_def201.jpg): Do khi gỡ bỏ MiDaS và chuyển sang multiclass_segmenter.tflite, thuật toán logic cũ đang áp dụng một vùng Oval/Vòng tròn dựa theo tọa độ chạm tuyệt đối để giữ nét, dẫn đến việc trên cùng một cơ thể người (cùng một mặt phẳng khoảng cách) nhưng khuôn mặt lại bị nhòe mờ tịt còn phần ngực/áo lại nét. Như vậy là sai quy luật vật lý quang học DoF (máy ảnh lấy nét theo mặt phẳng tiêu cự cắt ngang, chứ không lấy nét theo hình tròn).
  2. Lỗi khựng lag và treo cứng Viewfinder khi nhấn Shutter:
  • Viewfinder vẫn lag: Do khi bạn vuốt màn hình chỉnh Ruler, lớp BackdropFilter đồ họa tuy xử lý bằng GPU nhưng cấu trúc phân vùng đục lỗ Oval vẫn đang ép UI Thread tính toán vẽ lại liên tục.
  • Dừng luôn Viewfinder sau khi nhấn Shutter: Do luồng GlobalScope.launch gọi hàm chụp từ camera đang chiếm dụng/khóa cứng CameraSession (phần cứng camera) hoặc do hàm xử lý đổ Bitmap đầu ra đè lên buffer luồng preview mà không giải phóng, khiến camera bị treo ("Freeze").

Dưới đây là kế hoạch Markdown chi tiết để khắc phục triệt để hai lỗi trên.


🛠️ KẾ HOẠCH FIX LỖI: ĐỒNG BỘ MẶT PHẲNG TIÊU CỰ QUANG HỌC VÀ KHÔI PHỤC LUỒNG CAMERA


📐 1. Giải Pháp Sửa Lỗi Lấy Nét Theo "Mặt Phẳng Tiêu Cự Đa Lớp" (Semantic Focus Plane)

Để không còn bị lỗi nhòe mặt như trong image_def201.jpg, khi sử dụng multiclass_segmenter.tflite, chúng ta phải lấy nét theo toàn bộ khối thực thể lớp (Class ID) được chạm, thay vì lấy nét theo tọa độ vòng tròn hình học. Nếu chạm trúng một pixel thuộc lớp Người (classId == 1), toàn bộ cơ thể người từ đầu đến vai phải nét đều nhau.

💻 Cập nhật lại Logic xử lý mặt nạ trong MainActivity.kt:

// Lưu trữ Class ID được chọn để khóa tiêu cự (Mặc định -1 là tự động lấy nét toàn bộ người)
var lockedClassIdFocus = -1 

fun onTouchViewfinderToFocus(percentX: Float, percentY: Float, categoryMaskBuffer: ByteBuffer, maskW: Int, maskH: Int) {
    val maskX = (percentX * maskW).toInt().coerceIn(0, maskW - 1)
    val maskY = (percentY * maskH).toInt().coerceIn(0, maskH - 1)
    val index = maskY * maskW + maskX
    
    categoryMaskBuffer.rewind()
    // Khóa chính xác ID của thực thể tại điểm chạm (Không dùng vòng tròn phẳng)
    lockedClassIdFocus = categoryMaskBuffer.get(index).toInt()
}

fun processSemanticDofBokeh(
    capturedBitmap: Bitmap,
    categoryMaskBuffer: ByteBuffer,
    rulerValue: Int
): Bitmap {
    val w = capturedBitmap.width
    val h = capturedBitmap.height
    if (rulerValue == 0) return capturedBitmap

    val alphaMask = FloatArray(w * h)
    categoryMaskBuffer.rewind()

    // QUY LUẬT DOF VẬT LÝ QUANG HỌC MỚI: Lấy nét theo mảng thực thể phẳng
    for (i in 0 until (w * h)) {
        val currentPixelClassId = categoryMaskBuffer.get(i).toInt()
        
        if (lockedClassIdFocus == 1) {
            // Trường hợp 1: Nếu chạm vào NGƯỜI -> Toàn bộ pixel thuộc Người (ID 1) ĐỀU PHẢI NÉT CĂNG ĐỒNG ĐỀU
            alphaMask[i] = if (currentPixelClassId == 1) 1.0f else 0.0f
        } else if (lockedClassIdFocus == 3) {
            // Trường hợp 2: Nếu chạm vào ĐỒ VẬT -> Toàn bộ đồ vật nét, người và nền mờ đi
            alphaMask[i] = if (currentPixelClassId == 3) 1.0f else 0.0f
        } else {
            // Mặc định tự động: Giữ nét toàn bộ thực thể chính (Người + Phụ kiện cận cảnh)
            alphaMask[i] = if (currentPixelClassId == 1 || currentPixelClassId == 2) 1.0f else 0.0f
        }
    }

    // Làm mờ mảng mặt nạ (radius = 4) tạo dải mềm Gradient để viền tóc chuyển tiếp tự nhiên, không bị răng cưa
    blurAlphaMask1D(alphaMask, w, h, radius = 4)

    // Tạo phông nền sạch chống Ghosting bao quanh người
    val cleanBg = createCleanBackground(capturedBitmap, alphaMask, w, h)
    val maxRadius = (35 * Math.pow((rulerValue / 100.0f).toDouble(), 2.5)).toFloat()

    // Tạo bậc thang mờ mô phỏng khoảng cách hậu cảnh mờ mịn tăng tiến
    val blurStage1 = boxBlur(cleanBg, (maxRadius * 0.3f).toInt().coerceAtLeast(1))
    val blurStage2 = boxBlur(cleanBg, (maxRadius * 0.6f).toInt().coerceAtLeast(1))
    val blurStage3 = boxBlur(cleanBg, maxRadius.toInt().coerceAtLeast(1))
    cleanBg.recycle()

    val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)

    for (y in 0 until h) {
        for (x in 0 until w) {
            val idx = y * w + x
            val alpha = alphaMask[idx]

            // Mô phỏng DoF vật lý: Càng xa rìa biên của đối tượng lấy nét thì phông nền càng nhòe sâu
            val edgeDistanceFactor = calculateEdgeDistanceFactor(x, y, alphaMask, w, h)
            val bgPixel = when {
                edgeDistanceFactor < 0.2f -> blurStage1.getPixel(x, y)
                edgeDistanceFactor < 0.5f -> blurStage2.getPixel(x, y)
                else -> blurStage3.getPixel(x, y)
            }

            val fgPixel = capturedBitmap.getPixel(x, y)
            // Trộn Alpha Blending mượt mà cấu trúc
            outputBitmap.setPixel(x, y, Color.rgb(
                (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt(),
                (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt(),
                (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
            ))
        }
    }
    
    blurStage1.recycle(); blurStage2.recycle(); blurStage3.recycle()
    return outputBitmap
}


2. Sửa Lỗi Lag Và Treo Đóng Băng Kính Ngắm Viewfinder Khi Nhấn Shutter

🔴 Nguyên nhân đóng băng:

Khi bạn gọi hàm takePicture() trong luồng nền mà không thiết lập cơ chế khôi phục Preview, hoặc do Coroutine chiếm dụng cấu trúc đồ họa mà chưa giải phóng imageReader, phần cứng camera trên hệ điều hành Android sẽ tự ngắt luồng hiển thị giao diện.

🟢 Giải pháp: Giải phóng tài nguyên và khởi động lại Preview độc lập

Bước 2.1: Tối ưu UI Viewfinder tuyệt đối để hết lag hoàn toàn (CameraScreen.dart)

Thay vì tính toán đục lỗ mờ bằng cấu trúc Widget phức tạp ép UI vẽ lại, ta chỉ dùng bộ lọc làm mờ thuần túy phủ đều, vùng nét sẽ được quyết định hoàn toàn ở bức ảnh xuất xưởng sau khi chụp.

Widget _buildNonLagViewfinder() {
  return AspectRatio(
    aspectRatio: 3 / 4,
    child: Stack(
      children: [
        // Kính ngắm camera thô chạy trực tiếp bằng phần cứng (Luôn đạt 60 FPS mượt mà)
        CameraPreview(_cameraController),
        
        // Chỉ dùng duy nhất 1 lớp filter nhẹ của GPU, không tính toán biểu thức hình học trên UI
        if (_blurIntensity > 0)
          Positioned.fill(
            child: BackdropFilter(
              filter: ImageFilter.blur(sigmaX: _blurIntensity * 8.0, sigmaY: _blurIntensity * 8.0),
              child: Container(color: Colors.black.withOpacity(0.05)),
            ),
          ),
      ],
    ),
  );
}

Bước 2.2: Sửa mã nguồn Native tránh nghẽn luồng đóng băng Camera (MainActivity.kt)

Đảm bảo sau khi chụp xong tệp tin tạm, lệnh startPreview() phải được gọi ngay lập tức trên luồng giao diện để đánh thức camera làm việc tiếp, tách biệt hoàn toàn với luồng ngầm đang xử lý lưu ảnh AI.

fun captureImagePipeline(cameraDevice: CameraDevice, previewRequestBuilder: CaptureRequest.Builder, captureSession: CameraCaptureSession) {
    // 1. Kích hoạt chớp màn hình trên điện thoại ngay lập tức
    triggerFlashAnimationUI()

    // 2. Thiết lập lệnh chụp ảnh thô
    val captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
    captureBuilder.addTarget(imageReader.surface)

    captureSession.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
        override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
            super.onCaptureCompleted(session, request, result)
            
            // 🔥 KHẮC PHỤC LỖI TREO CỨNG CAMERA: 
            // Ngay khi luồng phần cứng bắt được ảnh thô xong, lập tức khởi động lại Viewfinder trên Main Thread
            runOnUiThread {
                try {
                    // Cấp lại luồng vẽ trực tiếp cho kính ngắm Viewfinder tiếp tục chạy mượt mà
                    captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }

            // 3. ĐẨY TOÀN BỘ TIẾN TRÌNH XỬ LÝ AI VÀ BOKEH CỦA MEDIAPIPE XUỐNG LUỒNG NGẦM COUTINE ĐỘC LẬP
            // Tiến trình này chạy độc lập hoàn toàn, không đụng chạm gì đến luồng Preview của Camera nữa
            GlobalScope.launch(Dispatchers.Default) {
                val rawBitmap = getInternalBitmapFromReader(imageReader)
                val categoryMask = runMediaPipeMulticlassProcessor(rawBitmap)
                
                val finalProcessedBitmap = processSemanticDofBokeh(rawBitmap, categoryMask, currentRulerValue)
                
                saveToGalleryAsynchronously(finalProcessedBitmap)
                rawBitmap.recycle()
                finalProcessedBitmap.recycle()
            }
        }
    }, null)
}


📅 3. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)

  • Kiểm thử lấy nét đồng khối (image_def201.jpg): Thực hiện chạm vào khuôn mặt nhân vật trên kính ngắm rồi chụp. Kiểm tra ảnh lưu trong máy xem khuôn mặt và toàn bộ phần vai/áo có nét căng đồng đều nhau không. (Yêu cầu: Đã sửa triệt để lỗi mờ nhòe mặt như trong hình image_def201.jpg).
  • Kiểm thử đóng băng Viewfinder: Nhấn nút Shutter chụp liên tiếp 5 tấm. Quan sát kính ngắm camera xem sau mỗi lần bấm máy có bị khựng hình hay treo đứng luôn không. (Yêu cầu: Màn hình chỉ chớp trắng 1 cái, kính ngắm vẫn hoạt động liên tục, không bị ngắt quãng).
  • Kiểm thử trượt Ruler: Kéo thanh trượt Ruler qua lại thật nhanh xem kính ngắm phản hồi ngay lập tức ở tốc độ 60 FPS không.