From 7fdce924b3a0694fd078ea321a01b5840cb4f96f Mon Sep 17 00:00:00 2001 From: locphamtran Date: Mon, 6 Jul 2026 21:17:42 +0700 Subject: [PATCH] Implement 30_FOCUS_API.md: physical autofocus and autoexposure via CameraX API, and disable software blur rendering in Portrait Blur mode --- 30_FOCUS_API.md | 133 ++++++++++++++++++ .../java/com/photobooth/app/MainActivity.kt | 35 +++++ 2 files changed, 168 insertions(+) create mode 100644 30_FOCUS_API.md diff --git a/30_FOCUS_API.md b/30_FOCUS_API.md new file mode 100644 index 0000000..090711f --- /dev/null +++ b/30_FOCUS_API.md @@ -0,0 +1,133 @@ +# 🟢 Giải pháp triệt để: + +Khi đã dùng **Focus vật lý**, nhiệm vụ của AI lúc này **không phải là đi làm mờ (Blur) nữa**, mà chỉ đóng vai trò là **"Mắt thần" hỗ trợ điều khiển động cơ thấu kính** di chuyển đến đúng vị trí (`CONTROL_AF_MODE`). Đồng thời, ta phải **tắt hoàn toàn các hàm `boxBlur` hay trộn Alpha Mask** để trả lại chất lượng thấu kính tự nhiên nhất. + +--- + +# 🛠️ KẾ HOẠCH MÀU SẮC & PHẦN CỨNG: CHUYỂN ĐỔI SANG FOCUS VẬT LÝ CAMERA2 API + +--- + +## 📐 1. Tắt Hoàn Toàn Thuật Toán Xử Lý Đồ Họa Phần Mềm (`MainActivity.kt`) + +Bước đầu tiên, chúng ta gỡ bỏ hoàn toàn hàm trộn pixel và làm mờ phông nền nhân tạo để giải phóng hoàn toàn CPU/GPU và triệt tiêu lỗi răng cưa ở tóc. + +```kotlin +fun processHardwareFocusPipeline(capturedBitmap: Bitmap): Bitmap { + // ❌ XÓA HOÀN TOÀN: boxBlur(), alphaMask, getNearestPureBackgroundPixel... + // 🟢 TRẢ VỀ: Ảnh thô 100% thu được từ cảm biến thấu kính camera vật lý + return capturedBitmap +} + +``` + +--- + +## 💻 2. Cấu Hình Điều Khiển Thấu Kính Vật Lý Khi Chạm Khung Ngắm (`MainActivity.kt`) + +Chúng ta sẽ sử dụng gói **Android Camera2 API** để gửi lệnh ép thấu kính dịch chuyển (Lens Vector Focus) dựa trên vị trí ngón tay chạm trên màn hình. + +### 💻 Logic khóa nét vật lý theo tọa độ chạm: + +```kotlin +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.params.MeteringRectangle +import android.hardware.camera2.CameraMetadata + +fun applyHardwareTouchToFocus( + previewRequestBuilder: CaptureRequest.Builder, + captureSession: CameraCaptureSession, + percentX: Float, + percentY: Float +) { + // 1. Chuyển đổi tỷ lệ chạm % về tọa độ ma trận cảm biến của phần cứng camera (Sensor Active Array) + val sensorRegion = cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) + val sensorW = sensorRegion?.width() ?: 2000 + val sensorH = sensorRegion?.height() ?: 1500 + + val focusX = (percentX * sensorW).toInt().coerceIn(0, sensorW) + val focusY = (percentY * sensorH).toInt().coerceIn(0, sensorH) + + // Thiết lập vùng lấy nét vật lý (kích thước ô vuông lấy mốc đo tiêu cự là 100x100 pixel) + val focusAreaWeight = 1000 // Độ ưu tiên lấy nét cao nhất + val focusRectangle = MeteringRectangle( + Math.max(focusX - 50, 0), + Math.max(focusY - 50, 0), + 100, + 100, + focusAreaWeight + ) + + try { + // 2. HỦY CHẾ ĐỘ TỰ ĐỘNG LẤY NÉT KHÔNG ĐỊNH HƯỚNG (CANCEL AF) + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL) + captureSession.capture(previewRequestBuilder.build(), null, null) + + // 3. ÉP CAMERA ĐIỀU KHIỂN ĐỘNG CƠ THẤU KÍNH ĐẾN TỌA ĐỘ VẬT LÝ MỚI + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(focusRectangle)) + previewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(focusRectangle)) // Đồng bộ đo sáng tại điểm chạm + + // Kích hoạt lệnh di chuyển thấu kính (START AF TRIGGER) + previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START) + + // Gửi lệnh xuống phần cứng camera cập nhật Preview lập tức + captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null) + + } catch (e: Exception) { + e.printStackTrace() + } +} + +``` + +--- + +## ⚡ 3. Đồng Bộ Sang Tầng Giao Diện Người Dùng (`CameraScreen.dart`) + +Trên tầng giao diện Flutter, gỡ bỏ hoàn toàn các widget bộ lọc phần mềm như `BackdropFilter` hay `Container` đục lỗ làm mờ cũ (vì chúng gây lag và không đúng thực tế). Chỉ giữ lại **Vòng tròn lấy nét vật lý**: + +```dart +Widget _buildHardwareFocusViewfinder() { + return GestureDetector( + onTapUp: (TapUpDetails details) { + final RenderBox box = context.findRenderObject() as RenderBox; + final Offset localPosition = box.globalToLocal(details.globalPosition); + + double percentX = (localPosition.dx / box.size.width).clamp(0.0, 1.0); + double percentY = (localPosition.dy / box.size.height).clamp(0.0, 1.0); + + setState(() { + _focusTouchPosition = localPosition; + }); + + // Gọi lệnh Native ép mô-tơ thấu kính di chuyển vật lý + _sendHardwareFocusCommand(percentX, percentY); + + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) setState(() { _focusTouchPosition = null; }); + }); + }, + child: Stack( + children: [ + CameraPreview(_cameraController), // Kính ngắm chạy thuần 100% phần cứng, 60 FPS tuyệt đối trơn tru + if (_focusTouchPosition != null) + Positioned( + left: _focusTouchPosition!.dx - 25, + top: _focusTouchPosition!.dy - 25, + child: const HardwareFocusRing(), // Hiện vòng tròn màu vàng/xanh khóa nét nhiếp ảnh + ), + ], + ), + ); +} + +``` + +--- + +## 📅 4. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist) + +* [ ] **Xác minh 1:** Chạm vào khuôn mặt. Thấu kính camera sẽ di chuyển lấy nét vào mặt. Kiểm tra phần áo ba lỗ và hai bên vai xem đã nét đều và tự nhiên theo cơ thể chưa (Yêu cầu: Không còn vệt mờ phần mềm đè lên vai). +* [ ] **Xác minh 2:** Chạm vào song sắt cửa sổ ở hậu cảnh phía sau. Thấu kính vật lý sẽ lùi ra sau lấy nét vào cửa sổ, khiến khuôn mặt bạn mờ nhòe đi một cách quang học tự nhiên nhất. (Yêu cầu: Phần viền đầu tóc không còn các vệt bóc tách AI màu xám loang lổ). +* [ ] **Kiểm thử độ lag:** Vuốt màn hình và bấm lấy nét liên tục xem Viewfinder có bị giật hay đứng hình không. (Yêu cầu: Khung hình chạy mượt mà 60 FPS ổn định hoàn toàn do CPU không còn phải gánh bất kỳ vòng lặp pixel nào). \ 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 08d3184..17eaeb6 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 @@ -732,6 +732,21 @@ class MainActivity : AppCompatActivity() { userSelectedClassFocus = mask[maskY * mW + maskX].toInt() } + // 1. Gửi lệnh Focus vật lý và đo sáng qua CameraX API (CONTROL_AF_MODE_AUTO, CONTROL_AF_REGIONS, CONTROL_AE_REGIONS) + val cameraCtrl = this@MainActivity.cameraControl + if (cameraCtrl != null) { + try { + val factory = binding.viewFinder.meteringPointFactory + val point = factory.createPoint(event.x, event.y) + val action = androidx.camera.core.FocusMeteringAction.Builder(point, androidx.camera.core.FocusMeteringAction.FLAG_AF or androidx.camera.core.FocusMeteringAction.FLAG_AE) + .setAutoCancelDuration(4, java.util.concurrent.TimeUnit.SECONDS) + .build() + cameraCtrl.startFocusAndMetering(action) + } catch (e: Exception) { + e.printStackTrace() + } + } + // Hiện vòng tròn lấy nét màu cam tại vị trí chạm val focusRing = binding.viewFocusRing focusRing.x = event.x - focusRing.width / 2f @@ -1708,7 +1723,20 @@ class MainActivity : AppCompatActivity() { return capturedImage } + // Tắt hoàn toàn thuật toán xử lý mờ/trộn nhân tạo cho Portrait Blur (khi selectedBgAssetPath == null) + // và trả về ảnh thô 100% để sử dụng tiêu cự thấu kính vật lý của camera. + if (selectedBgAssetPath == null && blurIntensity > 0f) { + if (!isStreamMode) { + // Trong chế độ chụp ảnh: Trả về ảnh thô ngay lập tức, không chạy phân đoạn AI + return capturedImage + } + } + if (!isAiSupported) { + // Khi không hỗ trợ AI và chỉ cần Portrait Blur, trả về ảnh thô để dùng thấu kính vật lý + if (selectedBgAssetPath == null && blurIntensity > 0f) { + return capturedImage + } if (blurIntensity > 0f) { val opticalIntensity = Math.pow(blurIntensity.toDouble(), 2.5).toFloat() val radius = (40 * opticalIntensity).toInt().coerceIn(1, 40) @@ -1745,6 +1773,13 @@ class MainActivity : AppCompatActivity() { cachedMaskHeight = h categoryMaskBuffer.rewind() + // Ở chế độ Portrait Blur thấu kính vật lý (selectedBgAssetPath == null), + // sau khi đã lưu cache mask để hỗ trợ xác định đối tượng chạm, + // ta bỏ qua hoàn toàn việc tạo mặt nạ alpha và làm mờ phông nền nhân tạo. + if (selectedBgAssetPath == null) { + return capturedImage + } + // Create and blur alpha mask with Touch-to-Focus class selection val focusClassId = if (userSelectedClassFocus >= 0) userSelectedClassFocus else -1 val alphaMask = FloatArray(w * h)