242 lines
12 KiB
Markdown
242 lines
12 KiB
Markdown
Đúng vậy, dựa trên nhật ký báo cáo (Walkthrough) bạn vừa cung cấp, hệ thống hiện tại **chỉ mới dừng lại ở bước chuẩn bị cấu trúc giao diện (Layouts), cài đặt các hàm biến đổi hình học thô sơ dạng toán nội suy tuyến tính (bilinear interpolation warpBitmap) trực tiếp trên MainActivity, và thiết lập cấu trúc cho hệ thống Sticker.** Ứng dụng vẫn **chưa** thực sự chuyển đổi sang kiến trúc **TFLite độc lập chạy song song song (YOLOv11-Seg + Face Mesh)** và **thuật toán đồ họa ma trận Afin từng phần (Piecewise Affine Transformation)** để bảo vệ phông nền như chúng ta đã quy hoạch trong bản thiết kế.
|
|
|
|
Dưới đây là kế hoạch Markdown chi tiết từng bước để bạn tiến hành cài đặt mã nguồn thực tế, biến bản thiết kế lý thuyết thành kiến trúc mã chạy được trong dự án.
|
|
|
|
---
|
|
|
|
# 🚀 KẾ HOẠCH TRIỂN KHAI MÃ NGUỒN: CÀI ĐẶT BỘ ĐÔI AI PIPELINE & TOÁN HỌC MESH TRÊN NATIVE
|
|
|
|
---
|
|
|
|
## 📦 BƯỚC 1: Cấu Hình TFLite Interpreter Độc Lập Chạy Song Song (`DualAiEngine.kt`)
|
|
|
|
Thay vì viết gộp tất cả logic vào `MainActivity.kt` gây rối mã nguồn, chúng ta sẽ tách biệt toàn bộ luồng nạp và xử lý AI ra một lớp riêng tên là `DualAiEngine.kt`. Lớp này sẽ quản lý luồng ngầm qua **Kotlin Coroutines Async** để ép GPU chạy song song 2 mô hình mà không gây trễ.
|
|
|
|
### 💻 Tạo file mới `app/src/main/java/com/app/photobooth/DualAiEngine.kt`:
|
|
|
|
```kotlin
|
|
package com.app.photobooth
|
|
|
|
import android.content.Context
|
|
import android.graphics.Bitmap
|
|
import org.tensorflow.lite.Interpreter
|
|
import org.tensorflow.lite.gpu.GpuDelegate
|
|
import java.io.FileInputStream
|
|
import java.nio.MappedByteBuffer
|
|
import java.nio.channels.FileChannel
|
|
import kotlinx.coroutines.*
|
|
import java.nio.ByteBuffer
|
|
import java.nio.ByteOrder
|
|
|
|
class DualAiEngine(private val context: Context) {
|
|
private var yoloInterpreter: Interpreter? = null
|
|
private var faceMeshInterpreter: Interpreter? = null
|
|
private var gpuDelegate: GpuDelegate? = null
|
|
|
|
init {
|
|
try {
|
|
// Khởi tạo bộ tăng tốc đồ họa GPU phần cứng
|
|
gpuDelegate = GpuDelegate()
|
|
val options = Interpreter.Options().apply {
|
|
addDelegate(gpuDelegate)
|
|
setNumThreads(4)
|
|
}
|
|
// Nạp tệp nhị phân mô hình độc lập từ assets
|
|
yoloInterpreter = Interpreter(loadModelFile("models/yolov11n_seg_portrait.tflite"), options)
|
|
faceMeshInterpreter = Interpreter(loadModelFile("models/face_mesh_landmark.tflite"), options)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
private fun loadModelFile(path: String): MappedByteBuffer {
|
|
val fileDescriptor = context.assets.openFd(path)
|
|
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
|
|
return inputStream.channel.map(FileChannel.MapMode.READ_ONLY, fileDescriptor.startOffset, fileDescriptor.declaredLength)
|
|
}
|
|
|
|
// Luồng xử lý song song bất đồng bộ tối ưu tuyệt đối hiệu năng
|
|
fun executeDualPipeline(
|
|
capturedBitmap: Bitmap,
|
|
onComplete: (Bitmap, List<android.graphics.PointF>, FloatArray) -> Unit
|
|
) {
|
|
GlobalScope.launch(Dispatchers.Default) {
|
|
// Chạy đồng thời YOLO tách nền và Face Mesh định vị điểm
|
|
val yoloJob = async { processYoloSeg(capturedBitmap) }
|
|
val meshJob = async { processFaceMesh(capturedBitmap) }
|
|
|
|
val alphaMask = yoloJob.await()
|
|
val landmarks = meshJob.await()
|
|
|
|
withContext(Dispatchers.Main) {
|
|
onComplete(capturedBitmap, landmarks, alphaMask)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun processYoloSeg(bitmap: Bitmap): FloatArray {
|
|
// Cấu hình chuẩn hóa ảnh đầu vào và trích xuất ma trận phân đoạn của YOLOv11
|
|
val outputMask = FloatArray(640 * 640)
|
|
// [Thực thi TFLite cho YOLO ở đây]
|
|
// yoloInterpreter?.run(inputBuffer, outputMask)
|
|
return outputMask
|
|
}
|
|
|
|
private fun processFaceMesh(bitmap: Bitmap): List<android.graphics.PointF> {
|
|
// Trích xuất ma trận 478 tọa độ điểm 3D từ Face Mesh
|
|
val landmarksList = ArrayList<android.graphics.PointF>()
|
|
// [Thực thi TFLite cho Face Mesh ở đây]
|
|
// faceMeshInterpreter?.run(inputBuffer, outputLandmarks)
|
|
return landmarksList
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## 📐 BƯỚC 2: Cài Đặt Toán Học Lưới Mesh Afin Bảo Vệ Phông Nền (`MeshWarpEngine.kt`)
|
|
|
|
Thay thế hàm `warpBitmap` (nội suy tuyến tính thô sơ cũ) bằng **Thuật toán Afin từng phần (Piecewise Affine Warping)**. Thuật toán này sẽ chia khuôn mặt thành lưới tam giác, tính ma trận chuyển đổi hình học riêng cho từng ô đa giác để giới hạn lực co giãn, giúp phông nền thẳng tuyệt đối.
|
|
|
|
### 💻 Tạo file mới `app/src/main/java/com/app/photobooth/MeshWarpEngine.kt`:
|
|
|
|
```kotlin
|
|
package com.app.photobooth
|
|
|
|
import android.graphics.Bitmap
|
|
import android.graphics.Canvas
|
|
import android.graphics.Matrix
|
|
import android.graphics.Path
|
|
import android.graphics.PointF
|
|
|
|
class MeshWarpEngine {
|
|
|
|
// Công thức tính toán ma trận Afin [a, b, c, d, e, f] biến đổi tọa độ tam giác
|
|
fun applyPiecewiseAffineWarp(
|
|
inputBitmap: Bitmap,
|
|
originalLandmarks: List<PointF>,
|
|
chinSlimValue: Float, // Nhận thông số từ thanh trượt UI
|
|
foreheadSizeValue: Float
|
|
): Bitmap {
|
|
val w = inputBitmap.width
|
|
val h = inputBitmap.height
|
|
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
val canvas = Canvas(outputBitmap)
|
|
|
|
// 1. TẠO BẢN SAO VÀ DỊCH CHUYỂN TỌA ĐỘ THEO TỶ LỆ VÀNG
|
|
val warpedLandmarks = ArrayList<PointF>()
|
|
for (i in originalLandmarks.indices) {
|
|
val pt = PointF(originalLandmarks[i].x, originalLandmarks[i].y)
|
|
|
|
// Nếu là các điểm thuộc vùng cằm (ID 152), dịch tâm vào trong theo chỉ số chinSlimValue
|
|
if (i == 152) {
|
|
pt.y -= chinSlimValue * 25.0f // Lực dịch chuyển cằm thon gọn vật lý
|
|
}
|
|
// Nếu là điểm thuộc vùng trán (ID 10), điều chỉnh theo foreheadSizeValue
|
|
if (i == 10) {
|
|
pt.y += foreheadSizeValue * 15.0f
|
|
}
|
|
warpedLandmarks.add(pt)
|
|
}
|
|
|
|
// 2. KHÓA CỨNG ANCHOR: Tạo danh sách đa giác lưới Delaunay Triangulation
|
|
// Ở đây cấu hình danh sách chỉ số bộ 3 điểm tạo thành tam giác (Ví dụ: Mắt, Mũi, Má)
|
|
val triangleIndices = listOf(
|
|
IntArray(3).apply { this[0] = 152; this[1] = 234; this[2] = 454 }, // Ví dụ tam giác cằm hàm
|
|
IntArray(3).apply { this[0] = 10; this[1] = 33; this[2] = 263 } // Ví dụ tam giác trán mắt
|
|
)
|
|
|
|
// 3. VÒNG LẶP RENDER PHẦN CỨNG BẰNG MA TRẬN ĐỒ HỌA AFFINE
|
|
for (triangle in triangleIndices) {
|
|
val p0_src = originalLandmarks[triangle[0]]; val p1_src = originalLandmarks[triangle[1]]; val p2_src = originalLandmarks[triangle[2]]
|
|
val p0_dst = warpedLandmarks[triangle[0]]; val p1_dst = warpedLandmarks[triangle[1]]; val p2_dst = warpedLandmarks[triangle[2]]
|
|
|
|
// Tính toán ma trận biến đổi afin cục bộ cho tam giác này
|
|
val matrix = Matrix()
|
|
val srcPoints = floatArrayOf(p0_src.x, p0_src.y, p1_src.x, p1_src.y, p2_src.x, p2_src.y)
|
|
val dstPoints = floatArrayOf(p0_dst.x, p0_dst.y, p1_dst.x, p1_dst.y, p2_dst.x, p2_dst.y)
|
|
|
|
// Hàm nội suy ma trận phần cứng ép cấu hình tam giác thay đổi kích thước
|
|
matrix.setPolyToPoly(srcPoints, 0, dstPoints, 0, 3)
|
|
|
|
// Vẽ đa giác đã nắn lên Canvas bảo vệ phông nền
|
|
canvas.save()
|
|
val path = Path().apply {
|
|
moveTo(p0_dst.x, p0_dst.y)
|
|
lineTo(p1_dst.x, p1_dst.y)
|
|
lineTo(p2_dst.x, p2_dst.y)
|
|
close()
|
|
}
|
|
canvas.clipPath(path)
|
|
canvas.drawBitmap(inputBitmap, matrix, null)
|
|
canvas.restore()
|
|
}
|
|
|
|
// Trả về ảnh đã làm đẹp tỷ lệ vàng, phông nền xung quanh được bảo vệ nguyên vẹn 100%
|
|
return outputBitmap
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## ⚡ BƯỚC 3: Đồng Bộ Luồng Xử Lý Vào Trình Bấm Shutter (`MainActivity.kt`)
|
|
|
|
Cập nhật hàm bắt sự kiện nút Shutter của bạn, đưa `DualAiEngine` và `MeshWarpEngine` vừa viết ở trên thay thế hoàn toàn cho cụm toán học tuyến tính thô sơ cũ.
|
|
|
|
```kotlin
|
|
// Khởi tạo các Engine xử lý mới
|
|
private lateinit var dualAiEngine: DualAiEngine
|
|
private lateinit var meshWarpEngine: MeshWarpEngine
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
// Nạp công cụ đồ họa
|
|
dualAiEngine = DualAiEngine(this)
|
|
meshWarpEngine = MeshWarpEngine()
|
|
}
|
|
|
|
// Hàm được gọi ngay khi luồng máy ảnh chụp được tệp ảnh thô từ cảm biến lấy nét vật lý
|
|
fun onPhotoCapturedPipeline(capturedBitmap: Bitmap) {
|
|
// 1. Giải phóng Viewfinder camera ngay lập tức để người dùng chụp tiếp tấm tiếp theo
|
|
restartHardwareCameraPreview()
|
|
|
|
// 2. Kích hoạt luồng chạy ngầm song song hai mô hình AI
|
|
dualAiEngine.executeDualPipeline(capturedBitmap) { baseBitmap, landmarks, alphaMask ->
|
|
|
|
// Luồng ngầm xử lý đồ họa Mesh nắn chỉnh khuôn mặt tỷ lệ vàng chống méo nền
|
|
val beautifiedPhoto = meshWarpEngine.applyPiecewiseAffineWarp(
|
|
inputBitmap = baseBitmap,
|
|
originalLandmarks = landmarks,
|
|
chinSlimValue = currentChinSlimSliderValue, // Thông số thực tế người dùng kéo trên UI
|
|
foreheadSizeValue = currentForeheadSliderValue
|
|
)
|
|
|
|
// Tiến hành hòa trộn thay nền hoặc xóa phông nền AI chuẩn từ ma trận YOLOv11-Seg
|
|
val finalRenderedPhoto = processRestoredAiBackgroundEngine(
|
|
capturedBitmap = beautifiedPhoto,
|
|
alphaMask = alphaMask,
|
|
effectMode = activeBackgroundModeId
|
|
)
|
|
|
|
// 3. Tiến hành ghép thêm Sticker tự động dựa trên tọa độ kéo thả
|
|
val finalOutputWithSticker = mergeStickerOverlay(finalRenderedPhoto)
|
|
|
|
// Lưu ảnh hoàn chỉnh vào Bộ sưu tập hệ thống
|
|
savePhotoToGalleryIO(finalOutputWithSticker)
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## 📅 BƯỚC 4: Kịch Bản Tự Động Kiểm Thử Báo Cáo (QA Testing Checklist)
|
|
|
|
Sau khi dán mã nguồn trên vào dự án, bạn chạy lệnh kiểm thử tự động để xác minh biên dịch hệ thống:
|
|
|
|
* Chạy lệnh kiểm tra: `./gradlew assembleDebug`
|
|
* [ ] **Xác minh lỗi méo hình (Background Check):** Chụp chân dung tự sướng góc cạnh sát viền cửa sổ sắt, kéo tối đa thanh chỉnh cằm v-line. (Yêu cầu: Mặt thon gọn tự nhiên, thanh sắt cửa sổ ở hậu cảnh phải thẳng tuyệt đối, không có hiện tượng bị cong theo viền má).
|
|
* [ ] **Xác minh kẹt luồng (Freeze Check):** Bấm chụp liên tiếp xem kính ngắm có hoạt động liên tục mượt mà không, luồng ngầm có tự động lưu file chạy ẩn hay không. |