Compare commits

...

14 Commits

Author SHA1 Message Date
admin 2de35196db Fix Portrait Blur Viewfinder and Captured photo: enable software blur on viewfinder preview when blurIntensity > 0, sync updateBackgroundMode, and scale blur radius based on width 2026-07-06 22:10:39 +07:00
admin 50ab58ae3e Fix 34_FIX_BG.md: implement runSimulatedBackgroundProcessing fallback to support high-quality portrait blur and background replacement on JNI-less emulators 2026-07-06 22:03:53 +07:00
admin 2de7fc4c0f Implement 34_FIX_BG.md: restore high-quality AI background replacement and blur blending for captured photos in the background thread, while maintaining lag-free early bypass for viewfinder preview 2026-07-06 21:53:29 +07:00
admin 2cf5a91557 Implement 33_FIX_GRAIN.md: add FilmGrainView custom view for dynamic 60 FPS grain preview, sync intensity, and optimize Gaussian noise generation for saved photos 2026-07-06 21:51:51 +07:00
admin 3058032d01 Implement 32_FIX_PRESET.md: create non-linear applyHighImpactPreset mapping for photo save, and apply to captured photo background thread processing 2026-07-06 21:43:29 +07:00
admin dd4edc61ac Implement 31_FIX_UXUI.md: add BackgroundMode, skip analyzer in non-AI_BLUR to prevent lag, add highlight and shadow to ColorPreset and processAndSaveCapturedPhoto 2026-07-06 21:32:03 +07:00
admin 7fdce924b3 Implement 30_FOCUS_API.md: physical autofocus and autoexposure via CameraX API, and disable software blur rendering in Portrait Blur mode 2026-07-06 21:17:42 +07:00
admin cd61e09042 Fix 29_FIX_FOCUS: semantic focus plane (person ID1+ID2 default), dedicated photoProcessExecutor prevents viewfinder freeze after shutter 2026-07-06 20:53:25 +07:00
admin e8fde43890 Implement Touch-to-Focus (28_FOCUS_POINT.md): cache MediaPipe category mask, add focus ring view, class-based alpha selection 2026-07-06 20:40:19 +07:00
admin 6636cdb418 Implement exponential focal length mapping for background blur as described in 27_FIX_DOF.md 2026-07-06 20:10:35 +07:00
admin a7a539b97f Implement anti-ghosting inpainting for original background blur during photo capture as described in 25_FIX_CAMERA.md 2026-07-06 19:52:12 +07:00
admin c95f12cedc Implement resolution independent blur: fix differences between viewfinder and final photo, eliminate lag by blurring on a fixed target width of 120px 2026-07-06 18:53:36 +07:00
admin 7e21d37ae2 Smooth blur: replace O(N*r) box blur with O(N) sliding window box blur and scale intensity linearly from 1 to 40 2026-07-06 18:45:12 +07:00
admin fe095304cc Optimize: downscale viewfinder preview to prevent camera lag, remove unused MiDaS depth model and TensorFlow Lite dependencies 2026-07-06 18:32:40 +07:00
20 changed files with 2459 additions and 440 deletions
+117
View File
@@ -0,0 +1,117 @@
Dựa vào hình ảnh thực tế `image.png` bạn cung cấp, có thể thấy rõ lỗi **Ghost Shadow (Bóng ma rế viền)** xung quanh vùng rìa tai, cổ và vai (phần khoanh đỏ).
### 🔴 Phân tích nguyên nhân lỗi:
1. **Ghost Shadow:** Xảy ra do hiện tượng "Chảy màu" (Color Bleeding). Khi bạn làm mờ hậu cảnh từ ảnh gốc, các pixel màu da của vai và tai bị nhòe rộng ra nền. Khi đè đối tượng sắc nét lên trên, phần rìa bị nhòe đó lồi ra ngoài tạo thành một lớp bóng mờ bao quanh cơ thể.
2. **Viewfinder giật lag:** Do ứng dụng đang ép CPU quét và xử lý thuật toán `boxBlur` đa tầng trực tiếp trên từng khung hình động (Real-time stream) của camera preview ở độ phân giải cao.
3. **Độ sâu trường ảnh (DoF) không chính xác:** Do các dải phân lớp khoảng cách (`0.3`, `0.5`, `0.75`) đang được tính tuyến tính cố định, không co giãn tỷ lệ nghịch mượt mà theo tiêu cự quang học khi người dùng trượt Ruler.
Dưới đây là kế hoạch cải tiến toàn diện mã nguồn để xử lý triệt để các vấn đề trên.
---
# 🛠️ KẾ HOẠCH FIX LỖI: TRIỆT TIÊU GHOST SHADOW VÀ TỐI ƯU HÓA FPS VIEWFINDER
---
## 📐 1. Thuật Toán Triệt Tiêu Ghost Shadow Bằng "Giãn Cơ Thể Ẩn" (Inpainting Dilate)
Để loại bỏ hoàn toàn bóng ma quanh vai và tai trong `image.png`, trước khi làm mờ hậu cảnh (`blurStage1, 2, 3`), chúng ta phải xóa bỏ thông tin màu sắc của đối tượng ra khỏi nền bằng cách **giãn rộng mặt nạ nền và lấp đầy vùng biên** (Background Inpainting / Edge Extension).
### 💻 Cải tiến Logic vòng lặp trong `MainActivity.kt`:
```kotlin
fun processAntiGhostingBokeh(
capturedBitmap: Bitmap,
alphaMask: FloatArray, // Mảng alpha mượt từ MediaPipe (0.0 -> 1.0)
midasDepthMap: Array<FloatArray>,
blurIntensity: Float
): Bitmap {
val w = capturedBitmap.width
val h = capturedBitmap.height
// BƯỚC 1: TẠO ẢNH HẬU CẢNH SẠCH (CLEAN BACKGROUND)
// Loại bỏ hoàn toàn pixel người trước khi đem đi Blur để tránh chảy màu da ra nền
val cleanBgBitmap = 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]
if (alpha > 0.01f) {
// Nếu là người: Thay bằng các pixel xung quanh viền nền (Edge Padding)
// để khi blur không bị lôi màu da ra ngoài
cleanBgBitmap.setPixel(x, y, getNearestBackgroundPixel(capturedBitmap, alphaMask, x, y, w, h))
} else {
cleanBgBitmap.setPixel(x, y, capturedBitmap.getPixel(x, y))
}
}
}
// BƯỚC 2: TẠO CÁC LỚP BLUR TỪ NỀN SẠCH (Không còn chứa vệt màu da cũ)
val blurStage1 = boxBlur(cleanBgBitmap, (5 * blurIntensity).toInt().coerceAtLeast(1))
val blurStage2 = boxBlur(cleanBgBitmap, (13 * blurIntensity).toInt().coerceAtLeast(1))
val blurStage3 = boxBlur(cleanBgBitmap, (25 * blurIntensity).toInt().coerceAtLeast(1))
cleanBgBitmap.recycle() // Giải phóng ngay
// BƯỚC 3: ĐỒNG BỘ ĐỘ SÂU TRƯỜNG ẢNH TRƠN TRU (EXPONENTIAL DOF)
// Thay đổi công thức phân phối khoảng cách dựa trên Ruler để DoF chính xác hơn
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
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]
val maskX = (x * 256 / w).coerceIn(0, 255)
val currentDepth = depthMap[maskY][maskX]
val relativeDistance = (currentDepth - 0.25f).coerceAtLeast(0f)
// Áp dụng thuật toán chọn lớp mờ mịn chuyển tiếp phi tuyến tính
val bgPixel = when {
relativeDistance < (0.15f * blurIntensity) -> blurStage1.getPixel(x, y)
relativeDistance < (0.4f * blurIntensity) -> blurStage2.getPixel(x, y)
else -> blurStage3.getPixel(x, y)
}
val fgPixel = capturedBitmap.getPixel(x, y)
// Alpha Blending cuối cùng
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
}
}
// Giải phóng bộ nhớ các stage
blurStage1.recycle()
blurStage2.recycle()
blurStage3.recycle()
return outputBitmap
}
```
---
## ⚡ 2. Giải Pháp Triệt Tiêu Lag Khi Xem Viewfinder (Live Preview Optimization)
Để đạt được tốc độ **60 FPS** trơn tru khi trượt Ruler trên giao diện kính ngắm thực tế, chúng ta áp dụng cơ chế **Hạ Cấp Kính Ngắm Chủ Động (Downscaled Proxy Blur)**:
* **Tuyệt đối không chạy Dual-AI (MediaPipe + MiDaS) trên luồng Live Viewfinder.**
* Khi người dùng đang ở giao diện ngắm và kéo thước trượt Ruler, ứng dụng sẽ chỉ lấy một luồng ảnh Preview có độ phân giải siêu thấp (Ví dụ: `320x240` pixel) để tính toán mờ dưới lớp render đồ họa, hoặc sử dụng Shader phần cứng (OpenGL/Skia) làm mờ toàn bộ kính ngắm trừ một vùng Oval trung tâm để mô phỏng (Mô phỏng hiệu ứng thị giác tức thì).
* Chỉ khi người dùng buông tay khỏi thanh Ruler hoặc nhấn nút **Shutter**, hệ thống mới gom toàn bộ dữ liệu độ phân giải cao cấu hình chuẩn ở Mục 1 thảy vào **Background Coroutine Process** để ép mảnh sắc nét xuất ra tệp JPG.
---
## 📅 3. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist)
* [ ] **Kiểm thử Ghost Shadow:** Chụp một bức ảnh cận cảnh vai giống góc độ của `image.png`. (Yêu cầu: Phần viền bao quanh áo ba lỗ trắng và da vai không còn các vệt mờ xám mờ lồi ra, ranh giới cắt lồng vào nền mới phải sạch sẽ hoàn toàn).
* [ ] **Kiểm thử độ nhạy Viewfinder:** Kéo thanh trượt tăng giảm độ nhòe qua lại liên tục bằng cử chỉ vuốt trên ảnh xem kính ngắm camera phản hồi lập tức không. (Yêu cầu: Giao diện trượt mượt mà, FPS không được rớt dưới 25 khung hình/giây).
* [ ] **Kiểm thử phân phối DoF:** Đặt các vật thể ở các cự ly khác nhau (1m, 3m, 5m), kéo từ mức blur thấp lên cao xem các vật ở xa vô cực có đạt độ nhòe tuyến tính tỷ lệ đúng quang học hay không.
+138
View File
@@ -0,0 +1,138 @@
Ý tưởng của bạn rất hay và hoàn toàn khả thi! Thay vì đóng gói cứng tất cả file `.png`, `.jpg`, hay `.json` vào ứng dụng làm tăng dung lượng bộ cài, việc tạo một **Thư viện tài nguyên động (Dynamic Asset Library)** sẽ giúp ứng dụng của bạn linh hoạt hơn rất nhiều. Người dùng có thể tự chọn, nhấn nút `...` để tải thêm những bộ khung Instax mới, phông nền AI mới hoặc các bộ màu Hot Trend từ Server về máy.
Dưới đây là kế hoạch Markdown chi tiết từ kiến trúc lưu trữ Server, cấu trúc giao diện cho đến luồng tải tệp tin dưới nền.
---
# 🌐 KẾ HOẠCH TRIỂN KHAI: THƯ VIỆN ĐỘNG TRỰC TUYẾN (ONLINE ASSETS STORE)
Mục tiêu cốt lõi:
1. Xây dựng cấu trúc lưu trữ gọn nhẹ trên Server (hoặc các dịch vụ Cloud Storage như Firebase, Supabase, hay Hosting riêng).
2. Thêm nút `...` (More) đồng cấp vào cuối các dải cuộn của 3 mục: **Frame, Background, Presets**.
3. Thiết lập luồng tải tệp tin bất đồng bộ, tự động lưu vào bộ nhớ máy (`Internal Storage`) để ứng dụng sử dụng offline cho các lần sau mà không cần tải lại.
---
## 📂 1. Thiết Kế Kiến Trúc Lưu Trữ Trên Cloud Server
Server của bạn chỉ cần lưu trữ các tệp tin asset trực tiếp và một file `manifest.json` tổng để ứng dụng Photobooth đọc danh sách.
### Cấu trúc file `manifest.json` trên Server:
```json
{
"frames": [
{ "id": "frame_classic", "name": "Instax Classic", "url": "https://yourserver.com/assets/frames/instaxframe.png", "version": 1 },
{ "id": "frame_vintage", "name": "Vintage Film", "url": "https://yourserver.com/assets/frames/vintage_film.png", "version": 1 }
],
"backgrounds": [
{ "id": "bg_neon", "name": "Neon Street", "url": "https://yourserver.com/assets/bg/neon_street.jpg", "version": 1 }
],
"presets": [
{ "id": "preset_fuji", "name": "Fuji Retro", "url": "https://yourserver.com/assets/presets/fuji_retro.json", "version": 2 }
]
}
```
---
## 📐 2. Quy Hoạch Lại Giao Diện Điều Khiển (UI/UX Layer)
Tại mỗi dải cuộn của Menu Tầng 2 (ngay sau danh sách các item có sẵn như trong ảnh `image_ce9f68.jpg`), ta thêm một ô nút chức năng đặc biệt làm nút **"Tải thêm"** (biểu tượng dấu `...` hoặc `+` tùy bạn chọn).
```
[DANH SÁCH CUỘN NGANG TẦNG 2]
├── [Mặc định] ── [Nền 1] ── [Nền 2] ── [Nền 3] ── 🔳 [ Nút "..." Tải Thêm ]
```
### Logic hiển thị khi người dùng tương tác:
* Khi nhấn vào nút `...`, một hộp thoại (Bottom Sheet hoặc Dialog Popup) sẽ hiện lên, tự động lấy dữ liệu từ file `manifest.json` trên mạng để hiển thị kho lưu trữ trực tuyến.
* Mỗi item trên kho sẽ có một nút **Tải về (Download)**. Nếu tệp tin đã được tải về máy rồi, nút đó sẽ đổi thành chữ **"Áp dụng"**.
---
## 💻 3. Thuật Toán Lõi Tải Và Quản Lý Tệp Tin Offline (`MainActivity.kt`)
Để thực hiện tải và đọc file động từ bộ nhớ thay vì thư mục `assets` cứng, chúng ta sử dụng thư viện mạng và đường dẫn thư mục lưu trữ nội bộ của ứng dụng (`filesDir`).
### 📦 Thêm thư viện tải tệp trong gói cấu hình (Android Native/Flutter):
Nếu làm ở tầng Native Android, bạn sử dụng `HttpURLConnection` hoặc thư viện `OkHttp`. Nếu ở tầng Flutter, bạn dùng package `dio` kết hợp với `path_provider`.
### 💻 Logic xử lý tải và lưu tệp ngầm dưới thiết bị:
```kotlin
import java.io.File
import java.net.URL
class DynamicAssetManager(private val context: Context) {
// Hàm thực hiện tải file ẩn dưới nền khi người dùng nhấn nút tải ở mục "..."
fun downloadAsset(fileUrl: String, assetType: String, fileName: String, onComplete: (File?) -> Unit) {
// Chạy ngầm hoàn toàn không gây lag giao diện camera
Thread {
try {
// Tạo thư mục lưu trữ cục bộ riêng cho từng loại: /data/user/0/com.app/files/downloaded_frames/
val targetDir = File(context.filesDir, "downloaded_$assetType")
if (!targetDir.exists()) targetDir.mkdirs()
val outputFile = File(targetDir, fileName)
// Kích hoạt luồng đọc ghi dòng dữ liệu từ Internet
URL(fileUrl).openStream().use { input ->
output.outputStream().use { outputStream ->
input.copyTo(outputStream)
}
}
// Trả về file đã tải thành công để cập nhật UI
onComplete(outputFile)
} catch (e: Exception) {
e.printStackTrace()
onComplete(null)
}
}.start()
}
// Hàm kiểm tra xem file này đã được tải về máy trước đó chưa
fun getLocalAssetFile(assetType: String, fileName: String): File? {
val file = File(context.filesDir, "downloaded_$assetType/$fileName")
return if (file.exists()) file else null
}
}
```
---
## 🔄 4. Cải Tiến Cụm Engine Để Đọc Được Cả Hai Nguồn Asset
Để hệ thống xử lý ảnh (như thuật toán MiDaS, MediaPipe hay Ép khung PNG) nhận diện được cả file cứng trong app lẫn file động mới tải về, bạn chỉ cần thay đổi hàm nạp ảnh sang cơ chế **Đọc Đa Luồng (Hybrid Asset Loader)**:
```kotlin
// THAY THẾ CƠ CHẾ ĐỌC CỨNG:
// Trước đây: val frameData = context.assets.open("frames/instaxframe.png")
// BÂY GIỜ: Kiểm tra file local trước, nếu không có mới tìm trong assets mặc định
val frameBitmap = val localFile = assetManager.getLocalAssetFile("frames", "instaxframe.png")
val finalFrame = if (localFile != null) {
BitmapFactory.decodeFile(localFile.absolutePath) // Nạp file người dùng vừa tải từ nút "..."
} else {
BitmapFactory.decodeStream(context.assets.open("frames/instaxframe.png")) // Nạp file mặc định của app
}
```
---
## 📅 5. Kế Hoạch Triển Khai Kiểm Thử (Checklist)
* [ ] **Kiểm thử kết nối mạng:** Tắt mạng (Wifi/Mạng di động) rồi bấm vào nút `...` xem ứng dụng có xử lý lỗi mượt mà không (Yêu cầu: Hiện thông báo nhẹ *"Vui lòng kết nối mạng để tải thêm tài nguyên"* thay vì bị văng ứng dụng).
* [ ] **Kiểm thử lưu trữ:** Tải về một mẫu `Nền mới`. Tắt hẳn ứng dụng đi, bật lại, kiểm tra xem ô `Nền mới` đó có còn nằm trong danh sách chọn không và có áp dụng được bình thường khi offline không.
* [ ] **Kiểm thử trùng lặp:** Bấm tải đi tải lại 1 tệp tin xem hệ thống có tự ghi đè (`Overwrite`) để tối ưu dung lượng bộ nhớ máy hay không, tránh việc sinh ra nhiều file rác trùng tên.
Tính năng kho lưu trữ động thông qua nút `...` này sẽ giúp app Photobooth của bạn cực kỳ nhẹ khi tải trên Store (chỉ khoảng 20-30MB), nhưng người dùng vẫn có thể sở hữu một kho tài nguyên khổng lồ tùy chọn theo ý muốn!
+138
View File
@@ -0,0 +1,138 @@
Lỗi bạn vừa mô tả rất phổ biến trong lập trình đồ họa camera. Nguyên nhân là do hàm `boxBlur` hoặc `gaussianBlur` tăng bán kính (radius) theo cấp số tuyến tính, trong khi trên thực tế quang học, độ sâu trường ảnh (DoF) và mức độ xóa phông của ống kính máy ảnh (từ 24mm đến 400mm) thay đổi theo **cấp số phi tuyến tính (Exponential/Logarithmic)** dựa trên tiêu cự và khoảng cách.
Để thước đo từ `0` đến `100` hoạt động mượt mà và thực tế (ví dụ: kéo đến 10 là mờ nhẹ kiểu 35mm, đến 50 là mờ mịn kiểu 85mm, và đến 100 là xóa nát nền kiểu 400mm), chúng ta cần thiết lập một **Ma trận chuyển đổi hàm số mũ (Exponential Mapping Matrix)**.
Dưới đây là kế hoạch Markdown chi tiết để tính toán lại toàn bộ bậc thang DoF cho dự án `photobooth-app`.
---
# 📐 KẾ HOẠCH TÍNH TOÁN BẬC THANG PHI TUYẾN TÍNH DOF (24MM - 400MM)
Mục tiêu cốt lõi:
1. Ánh xạ dải trượt từ `0` (vô hiệu hóa/nét toàn dải như Lens 24mm) đến `100` (xóa phông đại vĩ tuyến tương đương Lens 400mm F2.8).
2. Sửa lỗi "bật nhảy" độ mờ (mới tăng +1 đã mờ bằng lens 50mm).
3. Đảm bảo phân phối độ mờ theo khoảng cách thực tế (`relativeDistance`) của MiDaS chuyển động đúng quy luật vật lý.
---
## 📊 1. Thiết Kế Ma Trận Ánh Xạ Quang Học (Focal Length Mapping)
Chúng ta sẽ chia dải trượt `0 - 100` của Ruler thành các phân vùng tiêu cự máy ảnh thực tế để làm mốc đồ họa:
| Giá trị trên Thước (Ruler) | Tiêu cự mô phỏng (Focal Length) | Hiệu ứng thị giác thực tế | Bán kính Blur tối đa tương ứng (`Max Radius`) |
| --- | --- | --- | --- |
| **0** | `24mm - 28mm` | Nét sâu toàn cảnh (Deep Focus), không mờ. | **0** (Bật tính năng chống Ghosting nhưng Radius = 0) |
| **1 - 10** | `35mm - 50mm` | Mờ nhẹ nhàng, hậu cảnh vẫn nhìn rõ khối hình. | **1 - 4** |
| **11 - 40** | `85mm` | Tiêu cự chân dung kinh điển, phông nền tách biệt mịn. | **5 - 12** |
| **41 - 70** | `135mm - 200mm` | Phông nền bị nén lại, xóa phông mạnh, Bokeh lớn. | **13 - 22** |
| **71 - 100** | `300mm - 400mm` | Xóa nát phông nền (Creamy Bokeh), hậu cảnh hoàn toàn là các mảng màu hòa quyện. | **23 - 35** |
---
## 💻 2. Thuật Toán Chuyển Đổi Phi Tuyến Tính (`MainActivity.kt`)
Thay vì nhân trực tiếp `(25 * blurIntensity)`, chúng ta sẽ dùng hàm toán học mũ lũy thừa (`pow`) để ép dải số trượt tăng chậm ở đoạn đầu (tiêu cự ngắn) và tăng tốc cực nhanh ở đoạn sau (tiêu cự dài).
### 💻 Cập nhật hàm tính toán thông số trong Engine:
```kotlin
fun processAdvancedDofMapping(
capturedBitmap: Bitmap,
alphaMask: FloatArray,
midasDepthMap: Array<FloatArray>,
bgBitmap: Bitmap?,
rulerValue: Int // Nhận giá trị thô từ 0 đến 100 từ thanh trượt
): Bitmap {
val w = capturedBitmap.width
val h = capturedBitmap.height
if (rulerValue == 0) return capturedBitmap // Bằng 0 thì giữ nét 100% không xử lý
// --- THUẬT TOÁN ĐỔI BẬC THANG QUANG HỌC (EXPONENTIAL MAPPING) ---
// Chuyển đổi giá trị tuyến tính 0-100 sang tỷ lệ dốc phi tuyến tính từ 0.0 -> 1.0
val normalizedSlider = rulerValue / 100.0f
// Sử dụng hàm mũ lũy thừa bậc 2.5 để kìm hãm độ mờ ở các mức đầu (1-10) không bị nhảy vọt
val opticalIntensity = Math.pow(normalizedSlider.toDouble(), 2.5).toFloat()
// Tính toán bán kính mờ tối đa dựa trên tiêu cự max (Mô phỏng 400mm đạt bán kính 35)
val maxRadius = (35 * opticalIntensity).coerceAtLeast(1.0f)
// Phân phối 3 cấp độ mờ dựa trên tiêu cự đã tính toán lại
val radiusStage1 = (maxRadius * 0.2f).toInt().coerceAtLeast(1) // Nền sát người (Mô phỏng DoF mỏng)
val radiusStage2 = (maxRadius * 0.5f).toInt().coerceAtLeast(1) // Nền trung bình
val radiusStage3 = maxRadius.toInt().coerceAtLeast(1) // Nền xa vô cực
// 1. Tạo ảnh hậu cảnh sạch chống hiện tượng Ghost Shadow
val cleanBgBitmap = createCleanBackground(capturedBitmap, alphaMask, w, h)
// 2. Tiến hành Blur phân cấp theo thang tiêu cự chuẩn mới
val blurStage1 = boxBlur(cleanBgBitmap, radiusStage1)
val blurStage2 = boxBlur(cleanBgBitmap, radiusStage2)
val blurStage3 = boxBlur(cleanBgBitmap, radiusStage3)
cleanBgBitmap.recycle()
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
// 3. VÒNG LẶP TRỘN PIXEL THEO DOF QUANG HỌC TIÊU CỰ
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]
val maskX = (x * 256 / w).coerceIn(0, 255)
val currentDepth = midasDepthMap[maskY][maskX]
// Giả lập điểm lấy nét (focalPoint) cố định quanh đối tượng là 0.25f
val relativeDistance = (currentDepth - 0.25f).coerceAtLeast(0f)
// Chọn lớp màu nền mờ dựa theo độ sâu DoF co giãn động theo thước trượt
// Khi tiêu cự lớn (rulerValue cao), trường ảnh mỏng đi -> Vùng lấy nét hẹp lại
val dofThreshold1 = 0.10f * (1.0f - normalizedSlider * 0.5f)
val dofThreshold2 = 0.35f * (1.0f - normalizedSlider * 0.5f)
val bgPixel = when {
relativeDistance < dofThreshold1 -> blurStage1.getPixel(x, y)
relativeDistance < dofThreshold2 -> blurStage2.getPixel(x, y)
else -> blurStage3.getPixel(x, y) // Vô cực (Mờ đại Bokeh)
}
val fgPixel = capturedBitmap.getPixel(x, y)
// Khử răng cưa và trộn Alpha Blending
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
}
}
// Dọn dẹp bộ đệm RAM
blurStage1.recycle()
blurStage2.recycle()
blurStage3.recycle()
return outputBitmap
}
```
---
## 🔄 3. Cơ Chế Điều Khiển Cử Chỉ Swipe Đồng Bộ Trên Kính Ngắm
Khi áp dụng hàm số mũ này, các thao tác **Vuốt màn hình sang phải (Tăng) hay sang trái (Giảm)** sẽ thay đổi thông số `rulerValue` từ `0` đến `100` cực kỳ tuyến tính và đều đặn bằng tay, nhưng hiệu ứng thị giác trên màn hình sẽ chuyển đổi mượt mà:
* **Vuốt nhẹ đoạn đầu (0 -> 15):** Bạn sẽ thấy phông nền chỉ chuyển dịch nhẹ từ Lens góc rộng 24mm sang Lens 50mm, phông nền mờ rất khẽ, không bị nhảy vọt một cách bất ngờ như trước.
* **Vuốt mạnh về sau (50 -> 100):** Độ mờ tăng tốc nhanh chóng để đạt hiệu ứng nén hậu cảnh tối đa của các dòng ống kính siêu tele 400mm chuyên nghiệp.
---
## 📅 4. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)
* [ ] **Test mốc +1 đến +5:** Kéo thanh trượt hoặc vuốt màn hình lên mức +1 và +5 xem phông nền phía sau đã giữ được độ chi tiết nhẹ nhàng của ống kính tiêu cự ngắn chưa. (Yêu cầu: Nền chỉ hơi mượt đi chứ tuyệt đối không được nhòe tịt như trước).
* [ ] **Test mốc tối đa +100:** Kéo thước lên mức tối đa xem phông nền có đạt được độ xóa mịn đặc trưng của các dòng ống kính tele chụp xa hay không.
* [ ] **Kiểm thử vùng chuyển tiếp DoF:** Đảm bảo khi kéo thước tăng dần, ranh giới giữa vùng nét (người) sang vùng mờ nhẹ (vật ở gần) và vùng mờ tịt (vật ở xa) co giãn tự nhiên, không bị xuất hiện các vệt cắt khối loang lổ.
+261
View File
@@ -0,0 +1,261 @@
Dưới đây là kế hoạch Markdown chi tiết và hoàn chỉnh để triển khai tính năng **Chạm để lấy nét (Touch-to-Focus)**, tối ưu riêng cho cấu trúc chỉ sử dụng mô hình **Google MediaPipe Multiclass Segmenter (`multiclass_segmenter.tflite`)** và khắc phục triệt để tình trạng giật lag trên kính ngắm (Viewfinder) khi thay đổi độ nhòe DoF.
---
# 🎯 KẾ HOẠCH TRIỂN KHAI: CHẠM LẤY NÉT ĐA LỚP & KHẮC PHỤC LAG VIEWFINDER REAL-TIME
Mục tiêu cốt lõi:
1. Bắt tọa độ chạm `(X, Y)` trên Viewfinder để xác định người dùng muốn ưu tiên lấy nét vào thực thể nào (Người hoặc Đồ vật cụ thể).
2. Vẽ vòng tròn định vị lấy nét màu cam (Focus Ring Animation) và tự ẩn sau 1 giây.
3. **Triệt tiêu lag Viewfinder (60 FPS):** Thay vì ép CPU quét ma trận pixel liên tục khi người dùng ngắm hoặc trượt Ruler, ứng dụng sẽ sử dụng **GPU Shader giả lập (BackdropFilter Proxy)** trên UI để phản hồi mượt mà tức thì.
4. **Xử lý bất đồng bộ (Async Coroutine):** Cô lập luồng xử lý ảnh độ phân giải cao, làm mịn viền tóc và khử Ghost Shadow chạy ngầm hoàn toàn sau khi nhấn nút **Shutter**.
---
## 📐 1. Quy Hoạch Giao Diện Viewfinder & Cử Chỉ Chạm Lấy Nét (`CameraScreen.dart`)
Bao bọc khu vực hiển thị kính ngắm bằng `GestureDetector` để bắt đồng thời hai hành động: **Chạm để lấy nét** (`onTapUp`) và **Vuốt màn hình để tăng giảm Ruler** (`onHorizontalDragUpdate`).
Tận dụng phần cứng đồ họa (GPU) để làm mờ nhanh giao diện hiển thị nhằm phản hồi tức thì theo tay người dùng mà không gây gánh nặng cho CPU.
```dart
double _blurIntensity = 0.0; // Đồng bộ từ thanh trượt Ruler (0.0 -> 1.0)
Offset? _focusTouchPosition; // Tọa độ điểm chạm để vẽ vòng tròn nét trên UI
Widget _buildOptimizedTouchFocusViewfinder() {
return GestureDetector(
// 1. CỬ CHỈ VUỐT: Điều chỉnh tăng giảm Ruler cùng chiều (Sang phải là tăng)
onHorizontalDragUpdate: (details) {
setState(() {
_blurIntensity = (_blurIntensity + (details.delta.dx / 200.0)).clamp(0.0, 1.0);
});
},
// 2. CỬ CHỈ CHẠM: Chọn điểm lấy nét
onTapUp: (TapUpDetails details) {
final RenderBox box = context.findRenderObject() as RenderBox;
final Offset localPosition = box.globalToLocal(details.globalPosition);
// Tính tỷ lệ % tọa độ chạm (0.0 -> 1.0) để gửi xuống tầng Native Engine định vị ID vật thể
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 tọa độ xuống Native để xác định lớp đối tượng (Người/Vật) tại điểm chạm
_sendTouchFocusToNative(percentX, percentY);
// Ẩn vòng tròn định vị lấy nét sau 1 giây
Future.delayed(const Duration(milliseconds: 1000), () {
if (mounted) {
setState(() {
_focusTouchPosition = null;
});
}
});
},
child: AspectRatio(
aspectRatio: 3 / 4,
child: Stack(
children: [
// Luồng Camera thô từ phần cứng - Luôn mượt mà 60 FPS
CameraPreview(_cameraController),
// GIẢI PHÁP TRIỆT TIÊU LAG: Làm mờ toàn màn hình siêu tốc bằng GPU Shader
if (_blurIntensity > 0)
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: _blurIntensity * 15.0,
sigmaY: _blurIntensity * 15.0
),
child: Container(color: Colors.transparent),
),
),
// GIẢ LẬP ĐỤC LỖ TIÊU CỰ: Tạo vùng nét giả lập tại vị trí người dùng chạm tay
// Nếu chưa chạm, mặc định đục lỗ ở tâm màn hình (Mô phỏng lấy nét tự động vào Người)
if (_blurIntensity > 0)
Positioned(
left: (_focusTouchPosition?.dx ?? box.size.width / 2) - 110,
top: (_focusTouchPosition?.dy ?? box.size.height / 2) - 160,
child: Container(
width: 220,
height: 320,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(160),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 30,
spreadRadius: 20,
)
],
),
),
),
// Vẽ vòng tròn định vị lấy nét màu cam nhiếp ảnh khi user chạm vào kính ngắm
if (_focusTouchPosition != null)
Positioned(
left: _focusTouchPosition!.dx - 25,
top: _focusTouchPosition!.dy - 25,
child: const FocusRingWidget(), // Hiệu ứng vòng tròn thu nhỏ nhẹ
),
],
),
),
);
}
```
---
## 💻 2. Thuật Toán Lõi: Khóa Tiêu Cự Vùng Chọn Đa Lớp & Xử Lý Ảnh Chụp Ngầm (`MainActivity.kt`)
Vì không còn mô hình chiều sâu MiDaS, cơ chế chọn điểm lấy nét khi sử dụng duy nhất MediaPipe sẽ dựa trên **Phân loại ID thực thể (Class ID Preference)**.
Khi nhận tọa độ chạm, hệ thống tra cứu xem điểm đó là **Người (ID 1)** hay **Đồ vật (ID 3)** để khóa mục tiêu giữ nét căng, đồng thời đẩy toàn bộ vòng lặp pixel nặng xuống luồng ngầm (Coroutine) khi bấm Shutter.
```kotlin
// Biến lưu trữ ID của lớp đối tượng được chọn lấy nét (-1: Tự động lấy nét mặc định vào Người)
var userSelectedClassFocus = -1
fun updateFocusFromTouchPoint(percentX: Float, percentY: Float, categoryMaskBuffer: ByteBuffer, w: Int, h: Int) {
// Ánh xạ tọa độ tỷ lệ chạm về kích thước ma trận của MediaPipe
val maskX = (percentX * w).toInt().coerceIn(0, w - 1)
val maskY = (percentY * h).toInt().coerceIn(0, h - 1)
val bufferIndex = maskY * w + maskX
categoryMaskBuffer.rewind()
// Khóa Class ID tại điểm chạm (Ví dụ chạm vào gấu bông -> Khóa ID 3, chạm vào người -> Khóa ID 1)
userSelectedClassFocus = categoryMaskBuffer.get(bufferIndex).toInt()
}
fun processMediaPipeTouchBokeh(
capturedBitmap: Bitmap,
categoryMaskBuffer: ByteBuffer,
rulerValue: Int
): Bitmap {
val w = capturedBitmap.width
val h = capturedBitmap.height
if (rulerValue == 0) return capturedBitmap
// --- BƯỚC 1: ĐỊNH VỊ VÙNG TIÊU CỰ THEO ĐIỂM CHẠM ---
val alphaMask = FloatArray(w * h)
categoryMaskBuffer.rewind()
// Nếu chưa chạm, mặc định đích lấy nét là Người (ID 1) và Đồ vật bổ trợ (ID 3)
val targetClassId1 = if (userSelectedClassFocus >= 0) userSelectedClassFocus else 1
val targetClassId2 = if (userSelectedClassFocus >= 0) userSelectedClassFocus else 3
for (i in 0 until (w * h)) {
val classId = categoryMaskBuffer.get(i).toInt()
// Pixel nào trùng với ID thực thể được chạm chọn sẽ nhận trọng số giữ nét = 1.0f
alphaMask[i] = if (classId == targetClassId1 || classId == targetClassId2) 1.0f else 0.0f
}
// Làm mờ mảng mặt nạ (radius = 4) để tạo dải Gradient chuyển tiếp viền tóc mềm mại, khử hoàn toàn răng cưa
blurAlphaMask1D(alphaMask, w, h, radius = 4)
// --- BƯỚC 2: TÍNH THANG ĐỘ MỜ PHI TUYẾN TÍNH THEO RULER ---
val normalizedSlider = rulerValue / 100.0f
val opticalIntensity = Math.pow(normalizedSlider.toDouble(), 2.5).toFloat()
val maxRadius = (35 * opticalIntensity).coerceAtLeast(1.0f)
// --- BƯỚC 3: XÓA BỎ BÓNG MA VIỀN (GHOST SHADOW) ---
val cleanBgBitmap = 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
if (alphaMask[idx] > 0.05f) {
// Thay thế pixel người/vật bằng màu nền gần nhất để khi làm mờ không bị lem vệt màu da ra rìa vai
cleanBgBitmap.setPixel(x, y, getNearestPureBackgroundPixel(capturedBitmap, alphaMask, x, y, w, h))
} else {
cleanBgBitmap.setPixel(x, y, capturedBitmap.getPixel(x, y))
}
}
}
// Tạo 3 tầng mờ phân cấp mô phỏng độ sâu trường ảnh DoF dựa theo khoảng cách rìa biên mặt nạ
val blurStage1 = boxBlur(cleanBgBitmap, (maxRadius * 0.3f).toInt().coerceAtLeast(1)) // Nền sát biên mờ nhẹ
val blurStage2 = boxBlur(cleanBgBitmap, (maxRadius * 0.6f).toInt().coerceAtLeast(1)) // Nền trung cảnh mờ vừa
val blurStage3 = boxBlur(cleanBgBitmap, maxRadius.toInt().coerceAtLeast(1)) // Nền xa vô cực mờ sâu
cleanBgBitmap.recycle()
// --- BƯỚC 4: ALPHA BLENDING TRỘN ẢNH XUẤT XƯỞNG ---
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]
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)
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
}
}
blurStage1.recycle()
blurStage2.recycle()
blurStage3.recycle()
return outputBitmap
}
```
---
## ⚡ 3. Cơ Chế Cô Lập Luồng Khi Chụp (Shutter Background Queue)
Khi người dùng nhấn nút Shutter, để camera không bị đứng hình, toàn bộ hàm xử lý đồ họa pixel phức tạp ở trên sẽ được đóng gói và đẩy xuống luồng xử lý ngầm (Coroutine Background Task).
```kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
fun onShutterPressed(capturedBitmap: Bitmap, rawMaskBuffer: ByteBuffer, rulerValue: Int) {
// 1. UI phản hồi chớp sáng màn hình và thu nhỏ ảnh về thumbnail lập tức
triggerFlashAnimation()
updateMediaThumbnailPreview(capturedBitmap)
// 2. Đẩy xử lý AI đa lớp và thuật toán khử Ghost Shadow xuống luồng ngầm độc lập
GlobalScope.launch(Dispatchers.Default) {
val finalPhoto = processMediaPipeOnlyBokeh(
capturedBitmap = capturedBitmap,
categoryMaskBuffer = rawMaskBuffer,
rulerValue = rulerValue
)
// Lưu im lặng âm thầm vào thư viện máy
saveToSystemGallery(finalPhoto)
finalPhoto.recycle()
}
}
```
---
## 📅 4. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist)
* [ ] **Kiểm thử độ mượt Viewfinder (60 FPS):** Vuốt màn hình qua lại liên tục để chỉnh Ruler độ nhòe. Giao diện kính ngắm phải di chuyển trơn tru, thước số chạy mượt, không có hiện tượng khựng hay giật lag khung hình.
* [ ] **Kiểm thử chạm lấy nét (Touch-to-Focus):** Đặt một đồ vật (như ly nước) trước camera. Chạm tay vào ly nước xem phông nền và con người phía sau có mờ đi không. Sau đó chạm lại vào khuôn mặt xem ly nước có nhòe đi đúng quy luật quang học không.
* [ ] **Kiểm thử chất lượng viền tóc & Ghost Shadow:** Chụp một bức ảnh chân dung, phóng to ảnh trong thư viện máy lên kiểm tra viền vai và tóc. (Yêu cầu: Không còn vệt bóng mờ màu da lồi ra nền, các sợi tóc mảnh hòa quyện mượt mà vào phông nền).
+202
View File
@@ -0,0 +1,202 @@
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`:
```kotlin
// 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.
```dart
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.
```kotlin
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.
+133
View File
@@ -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).
+172
View File
@@ -0,0 +1,172 @@
Dưới đây là kế hoạch Markdown chi tiết giúp bạn tách biệt rạch ròi giữa **Hiệu ứng xóa phông AI****Lấy nét vật lý phần cứng**, giải quyết tận gốc lỗi lag ngầm, đồng thời tái cấu trúc lại hệ thống quản lý thuộc tính **Presets** để bổ sung đầy đủ các thông số ánh sáng và sắc độ.
---
# 📊 KẾ HOẠCH TOÀN DIỆN: ĐỒNG BỘ FOCUS VẬT LÝ, TỐI ƯU LUỒNG BLUR AI VÀ CẤU TRÚC LẠI PRESETS
Mục tiêu cốt lõi:
1. **Triệt tiêu lag ngầm khi tắt Blur AI:** Sửa lỗi hệ thống vẫn chạy ngầm vòng lặp pixel đồ họa của `multiclass_segmenter.tflite` gây lag mặc dù giao diện không hiển thị hiệu ứng.
2. **Đồng bộ Focus vật lý:** Giữ nguyên cơ chế lấy nét quang học qua Camera2 API, loại bỏ hoàn toàn sự can thiệp của thuật toán mờ phần mềm lên vật thể.
3. **Mở rộng thuộc tính Presets:** Định nghĩa lại cấu trúc dữ liệu cấu hình màu sắc, bổ sung các thông số chuyên sâu: *Highlight, Shadow, Saturation, Contrast, Brightness*.
---
## 🛠️ 1. Khắc Phục Lỗi Lag Ngầm Của Bộ Xử Lý Blur AI (`MainActivity.kt`)
**Nguyên nhân gây lag:** Khi bạn tắt chế độ xóa nền AI nhưng tăng thước trượt Blur, ứng dụng hiển thị như thể không dùng AI, nhưng ở tầng mã nguồn Native, điều kiện kiểm tra (`if/else`) chưa chuẩn khiến hàm `processMediaPipeOnlyBokeh()` vẫn bị gọi liên tục dưới nền, ép CPU xử lý ma trận `ByteBuffer` vô ích.
### 🟢 Giải pháp: Thiết lập cờ trạng thái hiệu ứng (Feature Flags) để ngắt luồng AI tuyệt đối
```kotlin
// Định nghĩa các chế độ hoạt động của ứng dụng Photobooth
enum class BackgroundMode {
DEFAULT, // Giữ nguyên ảnh gốc thô của Camera
HARDWARE_DOF, // Chỉ dùng Focus vật lý của thấu kính
AI_BLUR // Dùng AI tách nền và làm mờ phần mềm
}
var currentBackgroundMode = BackgroundMode.DEFAULT
fun handleImageStreamPipeline(capturedBitmap: Bitmap, rawMaskBuffer: ByteBuffer, rulerValue: Int): Bitmap {
// SỬA LỖI LAG NGẦM: Nếu không bật chế độ AI_BLUR hoặc thước trượt bằng 0,
// trả về ngay lập tức ảnh thô, chặn đứng hoàn toàn vòng lặp CPU.
if (currentBackgroundMode != BackgroundMode.AI_BLUR || rulerValue == 0) {
return capturedBitmap
}
// Chỉ khi bật AI_BLUR và rulerValue > 0 thì mới gánh luồng xử lý nặng này
return processMediaPipeOnlyBokeh(capturedBitmap, rawMaskBuffer, rulerValue)
}
```
---
## 🎯 2. Quy Hoạch Luồng Focus Vật Lý Khi Chạm Khung Ngắm
Khi chuyển sang tab **"Focus Vật lý"**, chúng ta chuyển trạng thái hệ thống về `BackgroundMode.HARDWARE_DOF`. Lúc này, hệ thống bỏ qua toàn bộ mặt nạ phân lớp của Google MediaPipe, giao phó 100% việc tạo độ sâu trường ảnh (DoF) tự nhiên cho mô-tơ thấu kính của thiết bị di động.
* **Viewfinder:** Gọi lệnh `applyHardwareTouchToFocus()` qua Camera2 API gửi tọa độ vùng đo tiêu cự `MeteringRectangle` xuống phần cứng thấu kính.
* **Ảnh xuất xưởng:** Nhận trực tiếp Bitmap thô từ cảm biến camera để lưu, đảm bảo ảnh trung thực như máy ảnh cơ, không bị lỗi lẹm viền hay tạo bóng ma (Ghost Shadow) quanh cơ thể.
---
## 🎨 3. Cấu Trúc Lại Hệ Thống Thuộc Tính Presets Màu Sắc
Để các Presets (bộ lọc màu) chỉnh sửa đúng các thuộc tính ánh sáng và sắc độ, chúng ta cần chuyển đổi cấu trúc dữ liệu từ dạng chuỗi (String) đơn giản sang một đối tượng cấu hình có cấu trúc dữ liệu cụ thể (Structured Configuration Class).
### 📐 3.1. Thiết kế Model Dữ Liệu Bộ Màu (`PresetModel.kt`)
```kotlin
data class ColorPreset(
val id: String,
val name: String,
// --- Thuộc tính ánh sáng ---
val brightness: Float, // -1.0f đến 1.0f (Mặc định 0.0f)
val contrast: Float, // 0.5f đến 2.0f (Mặc định 1.0f)
val highlight: Float, // -1.0f đến 1.0f (Mặc định 0.0f) -> Mới bổ sung
val shadow: Float, // -1.0f đến 1.0f (Mặc định 0.0f) -> Mới bổ sung
// --- Thuộc tính sắc độ & màu sắc ---
val saturation: Float, // 0.0f đến 2.0f (Mặc định 1.0f)
val temperature: Float // -0.5f đến 0.5f (Mặc định 0.0f) (Ấm/Lạnh)
)
```
### 💻 3.2. Thuật Toán Áp Dụng Thông Số Preset Lên Điểm Ảnh (Color Toning Engine)
Thuật toán xử lý ma trận màu sắc dưới đây nhận cấu hình từ các thuộc tính trên để biến đổi hệ màu RGB của bức ảnh:
```kotlin
fun applyAdvancedPreset(inputBitmap: Bitmap, preset: ColorPreset): Bitmap {
val w = inputBitmap.width
val h = inputBitmap.height
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
for (y in 0 until h) {
for (x in 0 until w) {
val pixel = inputBitmap.getPixel(x, y)
// Trích xuất kênh màu gốc
var r = (pixel shr 16 and 0xFF) / 255.0f
var g = (pixel shr 8 and 0xFF) / 255.0f
var b = (pixel and 0xFF) / 255.0f
// 1. XỬ LÝ ÁNH SÁNG (Brightness & Contrast)
r = (r - 0.5f) * preset.contrast + 0.5f + preset.brightness
g = (g - 0.5f) * preset.contrast + 0.5f + preset.brightness
b = (b - 0.5f) * preset.contrast + 0.5f + preset.brightness
// Tính toán độ sáng tổng thể của pixel (Luminance) để định vị vùng Highlight/Shadow
val luminance = 0.2126f * r + 0.7152f * g + 0.0722f * b
// 2. BỔ SUNG: XỬ LÝ HIGHLIGHT & SHADOW
if (luminance > 0.6f) {
// Vùng sáng (Highlight): Tăng/Giảm độ sáng dựa trên thông số highlight của Preset
val weight = (luminance - 0.6f) / 0.4f
r += preset.highlight * weight
g += preset.highlight * weight
b += preset.highlight * weight
} else if (luminance < 0.4f) {
// Vùng tối (Shadow): Bù sáng hoặc dìm tối dựa trên thông số shadow của Preset
val weight = (0.4f - luminance) / 0.4f
r += preset.shadow * weight
g += preset.shadow * weight
b += preset.shadow * weight
}
// 3. XỬ LÝ SẮC ĐỘ (Saturation)
val gray = luminance
r = gray + (r - gray) * preset.saturation
g = gray + (g - gray) * preset.saturation
b = gray + (b - gray) * preset.saturation
// Ép dải màu về giới hạn an toàn [0.0, 1.0] trước khi đóng gói lại pixel
val finalR = (r.coerceIn(0.0f, 1.0f) * 255).toInt()
val finalG = (g.coerceIn(0.0f, 1.0f) * 255).toInt()
val finalB = (b.coerceIn(0.0f, 1.0f) * 255).toInt()
outputBitmap.setPixel(x, y, (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB)
}
}
return outputBitmap
}
```
---
## 🌐 4. Quản Lý Đồng Bộ File Định Cấu Hình Mẫu (`PresetsStore`)
Đối với tính năng **Tải thêm từ Server trực tuyến (Online Store)** qua nút `...` đã lên kế hoạch từ trước, file cấu hình mẫu `manifest.json` trên Server của bạn sẽ được bổ sung các thuộc tính mới đồng bộ như sau:
```json
{
"presets": [
{
"id": "preset_kodak_gold",
"name": "Kodak Gold 200",
"url": "https://yourserver.com/assets/presets/kodak_gold.json",
"version": 1,
"properties": {
"brightness": 0.05,
"contrast": 1.15,
"highlight": -0.10,
"shadow": 0.15,
"saturation": 1.20,
"temperature": 0.08
}
}
]
}
```
---
## 📅 5. Kịch Bản Kiểm Thử Đảm Bảo Chất Lượng (QA Checklist)
* [ ] **Kiểm thử triệt tiêu lag ngầm (Lag Isolation Test):** Tắt tính năng Xóa nền AI (chuyển sang chế độ Mặc định/Focus vật lý). Kéo thanh trượt Ruler lên tối đa `+100` và di chuyển máy liên tục. (Yêu cầu: Kính ngắm phải mượt mà tuyệt đối 60 FPS, không được khựng giật vì hàm xử lý AI lúc này đã bị khóa chặn hoàn toàn bằng cờ trạng thái).
* [ ] **Kiểm thử Focus vật lý (Hardware Lens Shift):** Chạm vào các vùng khoảng cách khác nhau (Gần/Xa) trên Viewfinder xem ống kính camera có dịch chuyển mô-tơ lấy nét vật lý chính xác không, ảnh thu được không được phép có vết cắt giả tạo hay răng cưa phần mềm xung quanh tóc.
* [ ] **Kiểm thử thông số Highlight/Shadow trên Preset:** Áp dụng một bộ màu có thông số `highlight: -0.3``shadow: +0.2` lên một bức ảnh có vùng trời quá sáng và vùng tối khuất bóng. (Yêu cầu: Chi tiết vùng mây trên trời được dìm sáng rõ nét lại, vùng tối khuất sau lưng được cứu sáng mượt mà).
+164
View File
@@ -0,0 +1,164 @@
Dựa trên các vấn đề bạn vừa nêu, nguyên nhân cốt lõi khiến các thông số (Brightness, Contrast, Highlight, Shadow) tăng giảm không rõ rệt là do **công thức tính toán pixel tuyến tính (`/ 255.0f`) đang bị bó hẹp trong dải biên độ quá nhỏ**, không đủ mạnh để tạo ra sự đột biến về mặt thị giác trên cảm biến ảnh độ phân giải cao. Ngoài ra, việc các thông số này chưa hiển thị trên kính ngắm là do ứng dụng chưa tích hợp bộ lọc màu vào luồng render thời gian thực của camera preview.
Dưới đây là kế hoạch Markdown chi tiết để **nâng cấp thang đo ma trận màu sắc phi tuyến tính** nhằm tạo ra sự thay đổi tương phản, sáng tối cực kỳ rõ rệt, đồng thời tích hợp trực tiếp hiệu ứng này lên kính ngắm (Viewfinder) thời gian thực.
---
# 🛠️ KẾ HOẠCH FIX LỖI: HIỂN THỊ PRESET ĐỘNG TRÊN VIEWFINDER VÀ KHUẾCH ĐẠI BIÊN ĐỘ MÀU SẮC
Mục tiêu cốt lõi:
1. **Khuếch đại biên độ (High-Impact Color Engine):** Thay đổi công thức tính toán từ tuyến tính sang phi tuyến tính (dạng lũy thừa hoặc nhân hệ số dốc) để Brightness, Contrast, Highlight, Shadow thay đổi cực kỳ rõ rệt, đáp ứng đúng yêu cầu nghệ thuật của ảnh.
2. **Hiển thị Real-time trên Viewfinder:** Tích hợp bộ xử lý màu sắc trực tiếp vào luồng render của camera trước khi xuất ra màn hình kính ngắm.
3. **Đồng bộ ảnh chụp:** Đảm bảo khi bấm Shutter, thông số đang hiển thị trên kính ngắm được áp chính xác 100% vào ảnh lưu.
---
## 📐 1. Tích Hợp Hiệu Ứng Preset Lên Kính Ngắm Viewfinder (`CameraScreen.dart`)
Để người dùng thấy rõ sự thay đổi màu sắc ngay khi trượt thanh cấu hình hoặc chọn Preset, chúng ta sử dụng widget `ColorFiltered` kết hợp với **Ma trận màu (Color Filter Matrix)** ở tầng Flutter. Phương pháp này tận dụng GPU của điện thoại để render màu trực tiếp, đảm bảo Viewfinder luôn đạt 60 FPS mượt mà không bị lag.
```dart
// Hàm khởi tạo ma trận màu 4x5 từ các thông số Preset để ép GPU hiển thị thời gian thực
List<double> _generateColorMatrix({
required double brightness, // Thang từ -1.0 đến 1.0
required double contrast, // Thang từ 0.5 đến 2.5
required double saturation, // Thang từ 0.0 đến 2.0
}) {
// Khuếch đại thông số Brightness hệ số 255 để ăn trực tiếp vào ma trận phần cứng
final double b = brightness * 255.0;
final double c = contrast;
final double s = saturation;
// Ma trận biến đổi màu chuẩn đồ họa máy tính
return [
c * (0.213 + 0.787 * s), c * (0.715 - 0.715 * s), c * (0.072 - 0.072 * s), 0, b,
c * (0.213 - 0.213 * s), c * (0.715 + 0.285 * s), c * (0.072 - 0.072 * s), 0, b,
c * (0.213 - 0.213 * s), c * (0.715 - 0.715 * s), c * (0.072 + 0.928 * s), 0, b,
0, 0, 0, 1, 0,
];
}
Widget _buildPresetLiveViewfinder() {
return AspectRatio(
aspectRatio: 3 / 4,
child: ColorFiltered(
// Áp ma trận màu động lên toàn bộ luồng camera hiển thị
colorFilter: ColorFilter.matrix(_generateColorMatrix(
brightness: _currentPreset.brightness,
contrast: _currentPreset.contrast,
saturation: _currentPreset.saturation,
)),
child: CameraPreview(_cameraController), // Kính ngắm camera chạy bằng phần cứng mượt mà
),
);
}
```
---
## 💻 2. Tái Cấu Trúc Thuật Toán Màu Sắc Để Tăng Giảm Cực Kỳ Rõ Rệt (`MainActivity.kt`)
Dưới đây là thuật toán toán học mới được viết lại cho tầng xử lý ảnh gốc (`MainActivity.kt`). Chúng ta thay đổi quy luật tính toán: **Sử dụng hàm dốc phi tuyến tính để ép Highlight/Shadow thay đổi rõ rệt và khuếch đại biên độ Brightness/Contrast.**
```kotlin
fun applyHighImpactPreset(inputBitmap: Bitmap, preset: ColorPreset): Bitmap {
val w = inputBitmap.width
val h = inputBitmap.height
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
// KHUẾCH ĐẠI BIÊN ĐỘ: Nhân hệ số cường độ tác động để thông số thay đổi rõ rệt
val enhancedBrightness = preset.brightness * 1.5f // Tăng lực cho Brightness
val enhancedContrast = preset.contrast // Giữ nguyên dải Contrast chuẩn [0.5 - 2.5]
for (y in 0 until h) {
for (x in 0 until w) {
val pixel = inputBitmap.getPixel(x, y)
var r = (pixel shr 16 and 0xFF) / 255.0f
var g = (pixel shr 8 and 0xFF) / 255.0f
var b = (pixel and 0xFF) / 255.0f
// 1. CẢI TIẾN BRIGHTNESS & CONTRAST (Áp dụng công thức dốc phi tuyến tính)
// Lấy mốc 0.5f làm tâm điểm phối màu tương phản, tăng biên độ rõ rệt
r = (r - 0.5f) * enhancedContrast + 0.5f + enhancedBrightness
g = (g - 0.5f) * enhancedContrast + 0.5f + enhancedBrightness
b = (b - 0.5f) * enhancedContrast + 0.5f + enhancedBrightness
// Tính toán độ sáng thực tế (Luminance) của pixel hiện tại sau khi chỉnh Brightness
val luminance = 0.2126f * r + 0.7152f * g + 0.0722f * b
// 2. SỬA LỖI HIGHLIGHT: Dùng hàm S-Curve phi tuyến tính thay vì cộng trừ thông thường
// Giúp vùng sáng được cứu (nếu giảm) hoặc tăng sáng rõ rệt (nếu tăng) mà không bị bết màu
if (luminance > 0.5f) {
// Tính toán trọng số dốc: Pixel càng sáng, tác động Highlight càng mạnh
val highlightFactor = Math.pow(((luminance - 0.5f) / 0.5f).toDouble(), 1.5).toFloat()
// Nếu preset.highlight âm (-): dìm vùng sáng để cứu chi tiết
// Nếu preset.highlight dương (+): đẩy vùng sáng lên rực rỡ hẳn
r += preset.highlight * highlightFactor * 0.4f
g += preset.highlight * highlightFactor * 0.4f
b += preset.highlight * highlightFactor * 0.4f
}
// 3. SỬA LỖI SHADOW: Tương tự như Highlight, dùng hàm lũy thừa ngược cho vùng tối
if (luminance < 0.6f) {
// Pixel càng tối (gần 0.0f), trọng số dốc Shadow càng đạt cực đại
val shadowFactor = Math.pow(((0.6f - luminance) / 0.6f).toDouble(), 1.5).toFloat()
// Nếu preset.shadow dương (+): Cứu sáng vùng tối rõ rệt, phơi sáng chi tiết bóng râm
// Nếu preset.shadow âm (-): Dìm sâu vùng tối để tạo độ sâu, tương phản khối gắt
r += preset.shadow * shadowFactor * 0.5f
g += preset.shadow * shadowFactor * 0.5f
b += preset.shadow * shadowFactor * 0.5f
}
// 4. XỬ LÝ SẮC ĐỘ (Saturation)
val finalLuminance = (0.2126f * r + 0.7152f * g + 0.0722f * b).coerceIn(0.0f, 1.0f)
r = finalLuminance + (r - finalLuminance) * preset.saturation
g = finalLuminance + (g - finalLuminance) * preset.saturation
b = finalLuminance + (b - finalLuminance) * preset.saturation
// Kiểm soát biên độ bảo vệ hạt màu không vượt quá giới hạn [0, 255]
val finalR = (r.coerceIn(0.0f, 1.0f) * 255).toInt()
val finalG = (g.coerceIn(0.0f, 1.0f) * 255).toInt()
val finalB = (b.coerceIn(0.0f, 1.0f) * 255).toInt()
outputBitmap.setPixel(x, y, (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB)
}
}
return outputBitmap
}
```
---
## ⚡ 3. Đồng Bộ Áp Dụng Khi Nhấn Shutter Chụp Ảnh
Để đảm bảo ảnh lưu vào thư viện giống hệt những gì người dùng nhìn thấy trên kính ngắm, cấu trúc luồng Coroutine ngầm khi bấm Shutter sẽ nhận trực tiếp tham số cấu hình Preset hiện tại:
```kotlin
fun onShutterPressedWithPreset(capturedBitmap: Bitmap, activePreset: ColorPreset) {
// Luồng ngầm xử lý lưu ảnh
GlobalScope.launch(Dispatchers.Default) {
// Áp dụng thuật toán High-Impact vừa nâng cấp vào ảnh gốc độ phân giải cao
val filteredPhoto = applyHighImpactPreset(capturedBitmap, activePreset)
// Lưu ảnh vào Bộ sưu tập hệ thống
saveToSystemGallery(filteredPhoto)
filteredPhoto.recycle()
}
}
```
---
## 📅 4. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)
* [ ] **Kiểm thử thay đổi trên Viewfinder:** Chọn một Preset bất kỳ (Ví dụ: Đen trắng hoặc Vintage). Quan sát màn hình kính ngắm xem màu sắc có thay đổi ngay lập tức theo thời gian thực hay không.
* [ ] **Kiểm thử cường độ Brightness / Contrast:** Tăng thử thông số Brightness lên `+0.5` hoặc Contrast lên `1.8`. (Yêu cầu: Ảnh phải sáng bừng lên hoặc độ tương phản khối gắt hiện rõ lập tức, không còn hiện tượng "nhẹ nhẹ" không rõ ràng).
* [ ] **Kiểm thử tính năng Highlight:** Chụp một khung cảnh có cửa sổ trời rất sáng. Giảm Highlight xuống `-0.6`. (Yêu cầu: Vùng sáng quá mức quanh cửa sổ phải được dìm dịu đi rõ rệt, hiện rõ lại chi tiết vân khung cửa sắt).
* [ ] **Kiểm thử tính năng Shadow:** Tìm góc tối trong phòng, tăng Shadow lên `+0.7`. (Yêu cầu: Các chi tiết khuất trong góc tối phải được phơi sáng bật lên rõ ràng, không còn bị bết đen góc ảnh).
+196
View File
@@ -0,0 +1,196 @@
Để hiển thị hiệu ứng **Nhiễu hạt (Film Grain/Noise)** trực tiếp trên kính ngắm (Viewfinder) của camera mà vẫn đảm bảo tốc độ mượt mà **60 FPS không bị lag**, chúng ta tuyệt đối không thể xử lý bằng vòng lặp pixel tuần tự trên CPU. Giải pháp tối ưu nhất là sử dụng sức mạnh đồ họa phần cứng (GPU Shader) trực tiếp ở tầng Flutter UI để phủ một lớp texture hạt động (Dynamic Grain Filter) đè lên luồng Camera Preview thô.
Dưới đây là kế hoạch Markdown chi tiết từ giải pháp giao diện hiển thị cho đến thuật toán đồng bộ áp hạt lên ảnh chụp gốc khi nhấn Shutter.
---
# 🎞️ KẾ HOẠCH TRIỂN KHAI: HIỂN THỊ NHIỄU HẠT (FILM GRAIN) REAL-TIME TRÊN VIEWFINDER
Mục tiêu cốt lõi:
1. **Live Preview trơn tru (60 FPS):** Sử dụng các hiệu ứng phần cứng đồ họa tích hợp hoặc Shader tùy biến trong Flutter để tạo lớp hạt chuyển động liên tục, phản hồi tức thì theo mức độ tăng giảm trên thanh thước đo.
2. **Dynamic Noise Generation:** Hạt nhiễu phải liên tục thay đổi cấu trúc qua từng khung hình (không phải ảnh hạt tĩnh đứng im) để tạo cảm giác nhiễu hạt phim điện ảnh tự nhiên.
3. **Đồng bộ ảnh chụp độ phân giải cao:** Áp dụng thuật toán tạo nhiễu hạt tương ứng lên Bitmap ảnh gốc dưới luồng ngầm (Background Coroutine) khi bấm chụp.
---
## 📐 1. Tích Hợp Lớp Phủ Nhiễu Hạt Động Trên Giao Diện UI (`CameraScreen.dart`)
Trong Flutter, phương pháp tối ưu nhất để tạo nhiễu hạt thực tế mà không gánh CPU là sử dụng một hàm lặp tạo hạt ngẫu nhiên thông qua widget **`CustomPainter`** kết hợp với một vòng lặp Animation kích hoạt vẽ lại (repaint) liên tục, tạo ra các hạt nhiễu chuyển động động học.
### 💻 Đoạn mã tạo hiệu ứng Texture hạt trên kính ngắm:
```dart
import 'dart:math';
import 'package:flutter/material.dart';
// 1. LỚP VẼ HẠT NHIỄU PHẦN CỨNG (GPU ACCELERATED PAINTER)
class GrainPainter extends CustomPainter {
final double intensity; // Độ đậm/nhạt của hạt grain (0.0 -> 1.0)
final Random _random = Random();
GrainPainter({required this.intensity});
@override
void paint(Canvas canvas, Size size) {
if (intensity <= 0) return;
final Paint paint = Paint()..style = PaintingStyle.fill;
// Tính toán mật độ hạt dựa trên thông số cấu hình.
// Tránh quét từng pixel, ta chọn ngẫu nhiên các điểm tọa độ để tối ưu RAM
int grainCount = (size.width * size.height * 0.1 * intensity).toInt();
for (int i = 0; i < grainCount; i++) {
double x = _random.nextDouble() * size.width;
double y = _random.nextDouble() * size.height;
// Tạo sắc độ hạt ngẫu nhiên từ xám trắng đến xám tối để giả lập Grain tự nhiên
int grayValue = _random.nextInt(55) + 100; // Dải màu hạt phim [100 - 155]
// Độ trong suốt của từng hạt phụ thuộc vào cường độ chỉnh trên thước đo
int alpha = (_random.nextDouble() * intensity * 120).toInt().clamp(0, 255);
paint.color = Color.fromARGB(alpha, grayValue, grayValue, grayValue);
// Vẽ hạt nhiễu kích thước siêu nhỏ từ 1 đến 1.5 pixel
canvas.drawRect(Rect.fromLTWH(x, y, 1.2, 1.2), paint);
}
}
// Luôn trả về true để ép GPU sinh ma trận hạt mới liên tục theo khung hình Animation
@override
bool shouldRepaint(covariant GrainPainter oldDelegate) => true;
}
// 2. TÍCH HỢP LỚP HẠT ĐỘNG VÀO VIEWFINDER
class LiveCameraGrainWidget extends StatefulWidget {
final double grainIntensity; // Thừa hưởng từ thanh trượt cài đặt
const LiveCameraGrainWidget({super.key, required this.grainIntensity});
@override
State<LiveCameraGrainWidget> createState() => _LiveCameraGrainWidgetState();
}
class _LiveCameraGrainWidgetState extends State<LiveCameraGrainWidget> with SingleTickerProviderStateMixin {
AnimationController? _animationController;
@override
void initState() {
super.initState() ;
// Vòng lặp vô hạn chạy ở tần số quét màn hình (60Hz) để đổi vị trí hạt liên tục
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
)..repeat();
}
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 3 / 4,
child: Stack(
children: [
// Lớp đáy: Kính ngắm camera thô (mượt mà 60 FPS)
CameraPreview(_cameraController),
// Lớp đỉnh: Phủ lớp texture hạt động bằng AnimatedBuilder
AnimatedBuilder(
animation: _animationController!,
builder: (context, child) {
return CustomPaint(
size: Size.infinite,
painter: GrainPainter(intensity: widget.grainIntensity),
);
},
),
],
),
),
);
}
```
---
## 💻 2. Thuật Toán Sinh Nhiễu Hạt Lên Ảnh Gốc Khi Bấm Chụp (`MainActivity.kt`)
Khi người dùng nhấn nút Shutter, để bức ảnh lưu về máy có hiệu ứng hạt giống hệt như kính ngắm, chúng ta đưa thuật toán toán học tạo hạt phim (Film Grain Noise Model) vào luồng xử lý ngầm Coroutine để tránh đơ ứng dụng.
```kotlin
import android.graphics.Bitmap
import android.graphics.Color
import java.util.Random
fun applyHighQualityFilmGrain(inputBitmap: Bitmap, grainIntensity: Float): Bitmap {
if (grainIntensity <= 0.0f) return inputBitmap
val w = inputBitmap.width
val h = inputBitmap.height
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val random = Random()
// Khuếch đại biên độ tác động nhiễu hạt để tạo sự thay đổi rõ rệt
val grainStrength = grainIntensity * 45.0f
for (y in 0 until h) {
for (x in 0 until w) {
val pixel = inputBitmap.getPixel(x, y)
val r = (pixel shr 16 and 0xFF)
val g = (pixel shr 8 and 0xFF)
val b = (pixel and 0xFF)
// Sử dụng thuật toán phân phối Gauss (Gaussian Noise) để hạt mịn và chân thực hơn nhiễu thô
val noise = (random.nextGaussian() * grainStrength).toInt()
// Cộng hạt nhiễu trực tiếp vào từng kênh màu, khống chế dải màu trong giới hạn [0, 255]
val finalR = (r + noise).coerceIn(0, 255)
val finalG = (g + noise).coerceIn(0, 255)
val finalB = (b + noise).coerceIn(0, 255)
outputBitmap.setPixel(x, y, (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB)
}
}
return outputBitmap
}
```
---
## ⚡ 3. Đồng Bộ Luồng Xử Lý Bất Đồng Bộ Khi Chụp
Đảm bảo ảnh xuất ra từ camera được nạp đầy đủ thông số cấu hình nhiễu hạt hiện tại từ thanh trượt điều khiển:
```kotlin
fun onShutterPressedWithGrain(capturedBitmap: Bitmap, currentGrainIntensity: Float) {
// 1. Kích hoạt chớp màn hình và hiển thị thumbnail lập tức
triggerFlashEffectUI()
// 2. Cô lập tiến trình đổ màu hạt xuống tầng luồng ngầm độc lập
GlobalScope.launch(Dispatchers.Default) {
// Áp dụng thuật toán gieo hạt nhiễu độ phân giải cao
val grainPhoto = applyHighQualityFilmGrain(capturedBitmap, currentGrainIntensity)
// Tiến hành ghi file vào Bộ sưu tập hệ thống
saveToSystemGallery(grainPhoto)
grainPhoto.recycle()
}
}
```
---
## 📅 4. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)
* [ ] **Kiểm thử tốc độ Viewfinder (FPS Test):** Kéo thông số grain lên mức tối đa (`+100`) trên thanh thước đo. Quan sát kính ngắm camera xem luồng preview có di chuyển trơn tru, ổn định không (Yêu cầu: Khung hình không bị tụt FPS, đạt độ mượt mà phần cứng).
* [ ] **Kiểm thử độ động học của hạt (Dynamic Test):** Nhìn kỹ vào màn hình kính ngắm khi đứng im camera. (Yêu cầu: Các hạt nhiễu hạt phải chuyển động nhảy liên tục như nhiễu tivi hoặc phim nhựa cổ điển, không được phép đứng im thành một tấm lưới tĩnh).
* [ ] **Kiểm thử chất lượng ảnh lưu (Output Check):** Bấm nút Shutter chụp ảnh, mở Bộ sưu tập hệ thống ra phóng to ảnh xem các hạt nhiễu có được trộn đều, mịn vào các vùng màu mảng lớn (như vùng tối, mảng tường phẳng) đúng chất liệu Vintage không.
+185
View File
@@ -0,0 +1,185 @@
Dưới đây là kế hoạch Markdown chi tiết để **khôi phục lại toàn bộ tính năng Xóa phông (Blur Background) và Chèn ảnh nền mới (Replace Background)** bằng mô hình `multiclass_segmenter.tflite` của Google MediaPipe.
Kế hoạch này đảm bảo **giữ nguyên 100% tính năng Focus vật lý bằng phần cứng thấu kính**, nghĩa là động cơ camera vẫn di chuyển lấy nét theo điểm chạm quang học, còn AI chỉ đảm nhận vai trò bóc tách và xử lý đồ họa kỹ thuật số trên bức ảnh thu được.
---
# 📑 KẾ HOẠCH PHỤC HỒI TÍNH NĂNG XÓA NỀN AI VÀ CHÈN HÌNH NỀN (GOOGLE MULTICLASS SEGMENTER)
Mục tiêu cốt lõi:
1. **Focus vật lý làm gốc:** Mặc định giữ nguyên luồng điều khiển mô-tơ thấu kính phần cứng (`applyHardwareTouchToFocus`) qua Camera2 API khi người dùng chạm vào Viewfinder.
2. **Khôi phục AI tách lớp:** Kích hoạt lại `multiclass_segmenter.tflite` để phân tách pixel ảnh chụp thành các nhãn: Người (ID 1), Đồ vật (ID 3), Nền (ID 0).
3. **Phục hồi 2 tính năng đồ họa:**
* *Xóa nền:* Làm mờ phông nền thuật toán (kết hợp khử Ghost Shadow và mịn viền tóc).
* *Chèn nền:* Thay thế vùng phông nền bằng một Bitmap hình ảnh mới do người dùng chọn.
---
## 📐 1. Quy Hoạch Trạng Thái Tính Năng Trên Giao Diện (`CameraScreen.dart`)
Giao diện sẽ quản lý 3 trạng thái độc lập thông qua Panel điều khiển Tầng 2 (như trong ảnh mẫu):
* **Chế độ Mặc định:** Chỉ dùng Focus vật lý của thấu kính, ảnh giữ nguyên thô nguyên bản từ cảm biến.
* **Chế độ Xóa nền (Blur):** Thấu kính vẫn lấy nét vật lý + AI làm mờ thêm hậu cảnh phần mềm theo thước Ruler `0 - 100`.
* **Chế độ Thay nền (Background 1, 2, 3):** Thấu kính vẫn lấy nét vật lý + AI bóc người ra và chèn tấm ảnh nền mới vào.
```dart
enum PhotoEffectMode { defaultFocus, aiBlur, aiReplaceBg }
PhotoEffectMode _currentMode = PhotoEffectMode.defaultFocus;
String? _selectedBgPath; // Đường dẫn ảnh nền mới (Nền 1, Nền 2, Nền 3)
double _blurIntensity = 0.0; // Giá trị thước đo Ruler (0.0 -> 1.0)
Widget _buildActionPanel() {
return Row(
children: [
_buildButton(name: "Mặc định", action: () => setState(() => _currentMode = PhotoEffectMode.defaultFocus)),
_buildButton(name: "Xóa nền", action: () => setState(() => _currentMode = PhotoEffectMode.aiBlur)),
_buildButton(name: "Nền 1", action: () {
setState(() {
_currentMode = PhotoEffectMode.aiReplaceBg;
_selectedBgPath = "assets/bg/mountain.jpg"; // Hoặc tệp tải về từ nút "..."
});
}),
],
);
}
```
---
## 💻 2. Thuật Toán Lõi: Tích Hợp Song Song Thay Nền Và Xóa Phông AI (`MainActivity.kt`)
Hàm xử lý ảnh dưới tầng Native Android (`MainActivity.kt`) sẽ được khôi phục toàn diện để nhận diện đồng thời cả lệnh chèn hình nền lẫn lệnh làm mờ, triệt tiêu hoàn toàn lỗi vệt xám bọc quanh đầu tóc và bóng ma rế viền (Ghost Shadow).
```kotlin
fun processRestoredAiBackgroundEngine(
capturedBitmap: Bitmap, // Ảnh thô thu được sau khi camera đã lấy nét vật lý xong
categoryMaskBuffer: ByteBuffer, // Buffer mặt nạ từ mô hình multiclass_segmenter.tflite
effectMode: Int, // 0: Mặc định, 1: Xóa nền, 2: Thay nền
bgBitmap: Bitmap?, // Bitmap của hình nền mới (nếu có)
rulerValue: Int // Độ nhòe từ 0 đến 100
): Bitmap {
val w = capturedBitmap.width
val h = capturedBitmap.height
// NẾU CHẾ ĐỘ MẶC ĐỊNH HOẶC KHÔNG CÓ HIỆU ỨNG: Xuất ảnh thô vật lý ngay lập tức
if (effectMode == 0 || (effectMode == 1 && rulerValue == 0)) {
return capturedBitmap
}
// --- BƯỚC 1: TRÍCH XUẤT VÀ LÀM MỊN VIỀN TÓC (SOFT ALPHA MASK) ---
val alphaMask = FloatArray(w * h)
categoryMaskBuffer.rewind()
for (i in 0 until (w * h)) {
val classId = categoryMaskBuffer.get(i).toInt()
// Xác định thực thể chính cần giữ nét: Người (ID 1) và Đồ vật phụ trợ (ID 3)
alphaMask[i] = if (classId == 1 || classId == 3) 1.0f else 0.0f
}
// Thuật toán tích chập 1D làm mờ mặt nạ (radius = 4) để viền tóc lồng mượt vào nền, không bị cắt mạnh gắt
blurAlphaMask1D(alphaMask, w, h, radius = 4)
// --- BƯỚC 2: QUY QUYẾT NGUỒN PHÔNG NỀN (BASE BACKGROUND CANVAS) ---
// Nếu chọn THAY NỀN, nguồn nền là tấm ảnh mới (bgBitmap) được scale vừa khung hình.
// Nếu chọn XÓA NỀN, nguồn nền chính là ảnh chụp gốc nhưng được xử lý khử Ghost Shadow.
val baseBgCanvas = if (effectMode == 2 && bgBitmap != null) {
Bitmap.createScaledBitmap(bgBitmap, w, h, true)
} else {
// Thuật toán khử Ghost Shadow: Lấp đầy các pixel màu da bằng hạt màu nền xung quanh
createCleanBackground(capturedBitmap, alphaMask, w, h)
}
// --- BƯỚC 3: XỬ LÝ LÀM MỜ NỀN PHÂN CẤP THEO THƯỚC ĐO SỐ MŨ (RULER DoF) ---
val normalizedSlider = rulerValue / 100.0f
val opticalIntensity = Math.pow(normalizedSlider.toDouble(), 2.5).toFloat()
val maxRadius = (35 * opticalIntensity).coerceAtLeast(1.0f)
// Tạo các phân lớp mờ tăng tiến mô phỏng chiều sâu quang học tự nhiên
val blurStage1 = boxBlur(baseBgCanvas, (maxRadius * 0.3f).toInt().coerceAtLeast(1))
val blurStage2 = boxBlur(baseBgCanvas, (maxRadius * 0.6f).toInt().coerceAtLeast(1))
val blurStage3 = boxBlur(baseBgCanvas, maxRadius.toInt().coerceAtLeast(1))
baseBgCanvas.recycle()
// --- BƯỚC 4: ALPHA BLENDING TRỘN HÒA QUYỆN ĐỐI TƯỢNG XUẤT XƯỞNG ---
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] // Trọng số mượt mà từ 0.0 (Nền) -> 1.0 (Người)
// Đo cự ly hình học tính từ rìa biên mặt nạ đối tượng để phân cấp độ mờ hậu cảnh
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 công thức màu Alpha Blending
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
}
}
// Giải phóng bộ đệm để chống tràn RAM thiết bị
blurStage1.recycle(); blurStage2.recycle(); blurStage3.recycle()
return outputBitmap
}
```
---
## ⚡ 3. Đồng Bộ Luồng Chụp Ảnh Ngầm Không Gây Đơ Camera Preview
Luồng phần cứng thấu kính camera được tách lập độc lập hoàn toàn. Khi nhấn Shutter, ảnh thô (đã được thấu kính lấy nét vật lý chuẩn xác) được chụp lại, và hệ thống camera lập tức mở lại Preview để sẵn sàng cho lần chụp sau. Toàn bộ cụm xử lý AI phân lớp đa mục tiêu và thay/xóa nền được đẩy xuống luồng ngầm Coroutine chạy song song.
```kotlin
fun onShutterActionTriggered(capturedBitmap: Bitmap, rawMaskBuffer: ByteBuffer) {
// 1. Phản hồi giao diện lập tức: Chớp màn hình, đẩy ảnh thumbnail về góc trái
triggerFlashAnimationUI()
pushThumbnailPreviewToScreen(capturedBitmap)
// 2. Kích hoạt khôi phục luồng Preview vật lý của thấu kính camera ngay lập tức trên UI Thread
runOnUiThread {
cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
}
// 3. Thực thi ngầm luồng xử lý đồ họa nâng cao (Background Thread)
GlobalScope.launch(Dispatchers.Default) {
val finalPhoto = processRestoredAiBackgroundEngine(
capturedBitmap = capturedBitmap,
categoryMaskBuffer = rawMaskBuffer,
effectMode = currentSelectedModeId, // Nhận từ Flutter UI chuyển xuống
bgBitmap = currentLoadedBgBitmap,
rulerValue = currentRulerValue
)
// Lưu tệp ảnh hoàn chỉnh sạch sẽ vào thư viện ảnh hệ thống
saveProcessedBitmapToGallery(finalPhoto)
capturedBitmap.recycle()
finalPhoto.recycle()
}
}
```
---
## 📅 4. Checklist Xác Minh Triển Khai Thực Địa (Verification Checklist)
* [ ] **Kiểm thử tính năng Thay nền:** Chọn một ảnh nền phong cảnh (`Nền 1`). Nhấn chụp chân dung. (Yêu cầu: Phần cơ thể bạn phải được bóc tách sạch sẽ, đặt khớp lên hình nền mới, đường viền đầu tóc chuyển tiếp mượt mà, không xuất hiện quầng vệt xám loang lổ).
* [ ] **Kiểm thử tính năng Xóa nền phối hợp:** Bật chế độ `Xóa nền`, tăng thước trượt lên `+50`. (Yêu cầu: Hậu cảnh gốc bị làm mờ mịn màng tăng tiến, vùng rìa vai áo không xuất hiện bóng ma Ghost Shadow, và khu vực cơ thể nhân vật đạt độ nét đồng khối tự nhiên nhờ thấu kính quang học).
* [ ] **Xác minh tính độc lập của Focus vật lý:** Thử chạm vào khuôn mặt xem camera có tự động điều khiển mô-tơ di chuyển thấu kính để lấy nét mặt không. Thử chạm tiếp vào một vật ở xa xem camera có tự động dời thấu kính lấy nét ra xa không. (Yêu cầu: Tiêu cự camera chuyển động quang học bình thường, không bị can thiệp hay xung đột bởi thuật toán AI đồ họa).
-4
View File
@@ -63,10 +63,6 @@ dependencies {
// --- TensorFlow Lite for MiDaS ---
implementation("org.tensorflow:tensorflow-lite:2.12.0")
implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
// --- Google MediaPipe Tasks Vision ---
implementation("com.google.mediapipe:tasks-vision:0.10.14")
}
@@ -8,7 +8,7 @@ import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
enum class EditAttribute {
BRIGHTNESS, CONTRAST, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
BRIGHTNESS, CONTRAST, HIGHLIGHT, SHADOW, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
CLARITY, DEHAZE, VIGNETTE_AMOUNT, // Nhóm 2: Nâng cao
FADED_BLACK_LEVEL, SHADOWS_TINT_R, SHADOWS_TINT_G, SHADOWS_TINT_B, // Nhóm 3: Tone Curve
GRAIN_AMOUNT, GRAIN_SIZE, // Nhóm 4: Nhiễu hạt
@@ -17,7 +17,9 @@ data class BasicAdjustments(
var saturation: Float, // -1.0f đến 1.0f
var vibrance: Float, // -1.0f đến 1.0f -> Bảo vệ màu da người
var temperature: Float, // -1.0f đến 1.0f -> Ấm (vàng) / Lạnh (xanh)
var tint: Float // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
var tint: Float, // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
var highlight: Float = 0f, // -1.0f đến 1.0f -> Mới bổ sung
var shadow: Float = 0f // -1.0f đến 1.0f -> Mới bổ sung
)
data class AdvancedEffects(
@@ -0,0 +1,60 @@
package com.photobooth.app
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import java.util.Random
class FilmGrainView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val random = Random()
private val paint = Paint().apply {
style = Paint.Style.FILL
}
var intensity: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
if (field > 0f) {
visibility = VISIBLE
postInvalidateOnAnimation()
} else {
visibility = GONE
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (intensity <= 0f) return
val w = width.toFloat()
val h = height.toFloat()
if (w <= 0 || h <= 0) return
// Calculate count based on width, height and intensity
val grainCount = (w * h * 0.005f * intensity).toInt().coerceIn(200, 5000)
val maxAlpha = (intensity * 110).toInt().coerceIn(1, 255)
for (i in 0 until grainCount) {
val x = random.nextFloat() * w
val y = random.nextFloat() * h
val gray = random.nextInt(55) + 100 // [100 - 155] range for cinema film grain look
val alpha = random.nextInt(maxAlpha)
paint.color = Color.argb(alpha, gray, gray, gray)
// Draw tiny rectangular grain shapes
canvas.drawRect(x, y, x + 2.5f, y + 2.5f, paint)
}
// Repaint continuously at screen refresh rate to simulate dynamic analog noise
postInvalidateOnAnimation()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="#FFCC00" />
<solid android:color="#00000000" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8v16z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.41 3.59,-8 8,-8v16C7.59,20 4,16.41 4,12z" />
</vector>
@@ -111,6 +111,13 @@
android:visibility="gone"
android:contentDescription="Filter Overlay" />
<!-- Lớp phủ tạo nhiễu hạt phim thời gian thực (Dynamic Film Grain Viewfinder Overlay) -->
<com.photobooth.app.FilmGrainView
android:id="@+id/viewFilmGrain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
<ImageView
android:id="@+id/imgFrameOverlay"
@@ -143,6 +150,14 @@
android:visibility="gone"
android:text="3" />
<!-- Vòng tròn định vị lấy nét (Focus Ring) khi chạm vào Viewfinder -->
<View
android:id="@+id/viewFocusRing"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="@drawable/bg_focus_ring"
android:visibility="gone" />
</FrameLayout>
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
Binary file not shown.