fix: xóa phông và sắp xếp lại các nút trên giao diện

This commit is contained in:
2026-07-07 16:48:59 +07:00
parent cdb9e450b5
commit 623543e69e
29 changed files with 2180 additions and 273 deletions
+147
View File
@@ -0,0 +1,147 @@
Hai lỗi này liên quan trực tiếp đến hiện tượng **bất đồng bộ luồng (Race Condition)****quản lý vòng đời bộ đệm (State Lifecycle/Caching)** giữa tầng Flutter UI và tầng Native Android (`MainActivity.kt`).
* **Lỗi nhấn shutter lúc chụp lúc không:** Do khi bạn đổi Preset, luồng ngầm xử lý ma trận màu của Preset đó đang chiếm dụng tài nguyên đồ họa (Bitmap/ImageReader) cùng lúc với lệnh chụp `takePicture()`. Nếu lệnh chụp đến trước khi Preset xử lý xong $\rightarrow$ Chụp lỗi/Bị nuốt lệnh.
* **Lỗi đổi Preset phải bấm vài lần mới ăn:** Do biến lưu cấu hình Preset ở tầng Native (`activePreset`) không được cập nhật `volatile` thời gian thực, hoặc do bộ đệm xem trước (Preview Request Builder) không được gọi lệnh làm tươi (`setRepeatingRequest`) ngay lập tức sau khi gán thông số mới.
Dưới đây là kế hoạch Markdown chi tiết để sửa dứt điểm hai lỗi trên, đảm bảo đổi bộ lọc màu ăn ngay lập tức và nút bấm Shutter phản hồi chính xác 100%.
---
# 🛠️ KẾ HOẠCH FIX LỖI: ĐỒNG BỘ ĐỒNG THỜI PRESET REAL-TIME VÀ SỬA KHÓA LỆNH SHUTTER
---
## 📐 1. Khắc Phục Lỗi Thay Đổi Preset Không Ăn Ngay Lập Tức (`MainActivity.kt`)
**Giải pháp:** 1. Thêm từ khóa `volatile` vào biến lưu trữ Preset ở tầng Native để đảm bảo mọi luồng xử lý (Main Thread và Background Thread) đều đọc chung một giá trị mới nhất ngay khi người dùng chạm tay chọn.
2. Ép cấu hình Camera2 API làm tươi (Refresh Buffer) ngay lập tức bằng lệnh `setRepeatingRequest` sau khi người dùng thay đổi thông số.
```kotlin
import android.hardware.camera2.CaptureRequest
class MainActivity : FlutterActivity() {
// 🟢 SỬA LỖI: Dùng @Volatile để đảm bảo thông số Preset cập nhật ngay lập tức xuyên luồng
@Volatile
private var currentActivePreset: ColorPreset? = null
// Hàm nhận lệnh đổi Preset từ Flutter UI truyền xuống thông qua MethodChannel
fun updateActivePresetFromUI(newPreset: ColorPreset) {
this.currentActivePreset = newPreset
// 🟢 SỬA LỖI PHẢI BẤM VÀI LẦN: Ép thấu kính camera cập nhật ma trận màu Viewfinder ngay lập tức
try {
previewRequestBuilder.set(
CaptureRequest.COLOR_CORRECTION_MODE,
CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX
)
// Cập nhật ma trận màu phần cứng dựa trên thông số Preset mới chọn
applyPresetToPreviewBuilder(previewRequestBuilder, newPreset)
// Lệnh làm tươi Live Preview ngay một miligiây sau khi chọn
cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
```
---
## ⚡ 2. Khắc Phục Lỗi Nút Shutter Lúc Chụp Lúc Không (Chống Nghẽn Tài Nguyên)
**Nguyên nhân:** Khi bạn nhấn Shutter, ứng dụng chụp lại ảnh thô. Nếu lúc đó bạn vừa đổi Preset, hệ thống đồ họa bị "xung đột" quyền truy cập Bitmap. Nếu luồng chụp không khóa hàng đợi (Thread Lock), lệnh `capture` sẽ bị hủy âm thầm khiến camera đứng im không chụp.
**Giải pháp:** Sử dụng cờ trạng thái `isCapturing` và cơ chế **Đóng băng cấu hình tạm thời (Snapshot Parameter)**. Khi bấm Shutter, ta nhân bản nhanh thông số Preset tại thời điểm đó rồi đẩy vào luồng xử lý riêng, giải phóng luồng chính để sẵn sàng nhận lệnh chụp tiếp theo.
```kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
// Sử dụng AtomicBoolean chống hiện tượng kích kép (Double tap) hoặc nghẽn lệnh Shutter
private val isCapturing = AtomicBoolean(false)
fun onShutterButtonPressed() {
// Nếu đang trong tiến trình bắt ảnh thô từ cảm biến, chặn không cho bấm tiếp để tránh nghẽn phần cứng
if (isCapturing.get()) {
print("Camera đang bận chụp tấm trước, vui lòng đợi mốc miligiây!")
return
}
// Khóa trạng thái bắt đầu chụp
isCapturing.set(true)
triggerFlashAnimationUI() // Kích hoạt hiệu ứng chớp màn hình lập tức cho người dùng biết
// 🟢 CHỐNG XUNG ĐỘT: Sao chép nhanh (Snapshot) thông số Preset hiện tại ra một biến độc lập
val presetSnapshot = this.currentActivePreset?.copy()
// Tiến hành gọi phần cứng chụp ảnh thô
val captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureBuilder.addTarget(imageReader.surface)
cameraCaptureSession.capture(captureBuilder.build(), object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
// MỞ KHÓA CAMERA LẬP TỨC: Khôi phục luồng ngắm để người dùng bấm chụp phát tiếp theo được ngay
runOnUiThread {
cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(), null, null)
// Đã giải phóng xong phần cứng camera, nút Shutter sẵn sàng hoạt động 100%
isCapturing.set(false)
}
// ĐẨY LUỒNG XỬ LÝ ẢNH NẶNG XUỐNG COROUTINE NGẦM HOÀN TOÀN
GlobalScope.launch(Dispatchers.Default) {
val rawBitmap = getAndLockBitmapFromReader(imageReader)
// Áp dụng bản sao Preset đã chụp snapshot (Không sợ bị lỗi khi người dùng đổi màu khác lúc đang lưu)
val finalFilteredBitmap = if (presetSnapshot != null) {
applyHighImpactPreset(rawBitmap, presetSnapshot)
} else {
rawBitmap
}
// Chạy luồng lưu file ngầm
saveBitmapToSystemGalleryIO(finalFilteredBitmap)
// Thu hồi RAM dọn dẹp bộ nhớ đệm sạch sẽ
rawBitmap.recycle()
finalFilteredBitmap.recycle()
}
}
}, null)
}
```
---
## 🌐 3. Đồng Bộ Chặt Chẽ Tại Tầng Giao Diện Người Dùng (`CameraScreen.dart`)
Ở tầng Flutter UI, khi người dùng trượt Ruler hoặc nhấn chọn Preset, ta gọi trực tiếp MethodChannel xuống Native một cách dứt khoát, không bọc qua các hàm delay trung gian:
```dart
// Mỗi lần người dùng trượt Ruler thay đổi thông số màu
void onPresetSliderChanged(double value) {
_currentPreset.brightness = value;
// Gửi trực tiếp xuống Native ép mô-tơ đồ họa và camera làm tươi dữ liệu ngay
_methodChannel.invokeMethod('updatePreset', _currentPreset.toJson());
setState(() {}); // Làm mới thanh trượt trên UI của Flutter
}
```
---
## 📅 4. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist)
* [ ] **Kiểm thử đổi Preset ăn ngay:** Nhấn liên tục vào các Preset khác nhau màu Trắng Đen $\rightarrow$ Vintage $\rightarrow$ Rực rỡ. (Yêu cầu: Kính ngắm Viewfinder phải đổi màu theo ngay lập tức ngay tại thời điểm chạm ngón tay, không có độ trễ, không phải bấm 2-3 lần mới ăn).
* [ ] **Kiểm thử Shutter chính xác 100%:** Bấm nút Shutter liên tục (Burst-click) khoảng 5-10 lần với tốc độ nhanh. (Yêu cầu: Mỗi lần bấm màn hình đều phải chớp sáng sáng 1 cái biểu thị đã ghi nhận lệnh chụp, không xuất hiện tình trạng "bấm nhưng camera đơ ra không chụp").
* [ ] **Kiểm thử lưu ảnh đúng màu:** Đang đứng ở `Preset Trắng Đen`, bấm nút Shutter, rồi lập tức bấm tay chuyển nhanh sang `Preset Rực Rỡ`. Mở bộ sưu tập ảnh kiểm tra tấm ảnh vừa chụp xem có lưu đúng màu `Trắng Đen` của thời điểm bấm nút chụp không. (Yêu cầu: Đúng màu nhờ thuật toán Snapshot ở Mục 2).
View File
+242
View File
@@ -0,0 +1,242 @@
Đú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)****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``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.
+21
View File
@@ -65,4 +65,25 @@ dependencies {
// --- Google MediaPipe Tasks Vision --- // --- Google MediaPipe Tasks Vision ---
implementation("com.google.mediapipe:tasks-vision:0.10.14") implementation("com.google.mediapipe:tasks-vision:0.10.14")
// --- TensorFlow Lite ---
implementation("org.tensorflow:tensorflow-lite:2.14.0")
implementation("org.tensorflow:tensorflow-lite-gpu:2.14.0")
}
tasks.register("downloadAiModels") {
group = "ai_lifecycle"
description = "Tự động kích hoạt script Python tải và chuyển đổi mô hình TFLite sạch."
doLast {
println("🤖 [Gradle AI Engine] Đang kích hoạt script Python...")
exec {
commandLine("python", "${project.rootDir}/../download_and_export_models.py")
}
println("✅ [Gradle AI Engine] Tiến trình đồng bộ tài nguyên AI hoàn tất!")
}
}
// 2. KHẮC PHỤC LỖI KHÔNG TÌM THẤY: Ép mô-đun :app chạy task AI trước khi build mã nguồn Android
tasks.named("preBuild") {
dependsOn("downloadAiModels")
} }
@@ -0,0 +1,54 @@
import os
import glob
import shutil
from ultralytics import YOLO
# 1. Nạp mô hình PyTorch gốc từ file bạn đã tải sẵn
model = YOLO("yolov11n-seg.pt")
print("⏳ Bước 1: Đang dịch sang định dạng trung gian ONNX...")
# Xuất sang file .onnx (Vượt qua bộ check OS của LiteRT)
onnx_path = model.export(format="onnx", imgsz=640)
print("⏳ Bước 2: Kích hoạt onnx2tf để dịch ma trận đồ họa sang TFLite...")
onnx_file = "yolov11n-seg.onnx"
if os.path.exists(onnx_file):
# Gọi công cụ dòng lệnh chuyển dịch sang Float32
# -nuo: Chống tối ưu hóa lỗi cấu hình phần cứng Android GPU Delegate
os.system(f"onnx2tf -in {onnx_file} -nuo")
print("🔍 Đang quét tìm vị trí file .tflite thành phẩm...")
# 🟢 SỬA LỖI: Tìm kiếm tất cả các file .tflite được sinh ra trong các thư mục con
tflite_matches = glob.glob("**/yolov11n-seg*.tflite", recursive=True) + glob.glob("*.tflite")
# Lọc bỏ nếu tìm trúng file đích cũ để tránh trùng lặp
tflite_matches = [f for f in tflite_matches if "yolov11n_seg_portrait.tflite" not in f]
if tflite_matches:
generated_tflite = tflite_matches[0]
dest_path = "yolov11n_seg_portrait.tflite"
# Di chuyển và đổi tên chuẩn xác cho dự án Android
os.replace(generated_tflite, dest_path)
print(f"🎉 XUẤT FILE THÀNH CÔNG! File đã nằm tại: {os.path.abspath(dest_path)}")
# --- DỌN DẸP FILE RÁC TRUNG GIAN ---
try:
os.remove(onnx_file)
# Xóa các thư mục tạm do onnx2tf hoặc ultralytics tạo ra
if os.path.exists("yolov11n-seg_saved_model"):
shutil.rmtree("yolov11n-seg_saved_model")
if os.path.exists("saved_model"):
shutil.rmtree("saved_model")
# Tìm và xóa thư mục con trùng tên nếu có
for d in os.listdir("."):
if os.path.isdir(d) and "yolov11n-seg" in d:
shutil.rmtree(d)
except Exception as clean_ex:
print(f"⚠️ Cảnh báo dọn dẹp: {str(clean_ex)}")
else:
print("❌ Lỗi: onnx2tf không tạo ra bất kỳ file .tflite nào! Hãy kiểm tra log phía trên của onnx2tf xem có bị lỗi thiếu công cụ 'flatc' không.")
else:
print("❌ Lỗi: Không tìm thấy file ONNX trung gian để chuyển đổi!")
@@ -12,7 +12,8 @@ enum class EditAttribute {
CLARITY, DEHAZE, VIGNETTE_AMOUNT, // Nhóm 2: Nâng cao 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 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 GRAIN_AMOUNT, GRAIN_SIZE, // Nhóm 4: Nhiễu hạt
PORTRAIT_BLUR, BG_REPLACE // Nhóm 5: Tách nền AI PORTRAIT_BLUR, BG_REPLACE, // Nhóm 5: Tách nền AI
EYE_SIZE, NOSE_SIZE, CHIN_SIZE, FOREHEAD_SIZE, MOUTH_SIZE, FACE_SLIM, FACE_SIZE, SKIN_SMOOTH
} }
data class AttributeNode( data class AttributeNode(
@@ -34,7 +35,7 @@ class AttributeAdapter(
notifyItemChanged(value) notifyItemChanged(value)
} }
fun updateSelection(selectedType: EditAttribute) { fun updateSelection(selectedType: EditAttribute?) {
val index = items.indexOfFirst { it.type == selectedType } val index = items.indexOfFirst { it.type == selectedType }
if (index != -1) { if (index != -1) {
selectedPosition = index selectedPosition = index
@@ -0,0 +1,153 @@
package com.photobooth.app
import android.content.Context
import android.graphics.Bitmap
import android.graphics.PointF
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 {
gpuDelegate = GpuDelegate()
val options = Interpreter.Options().apply {
addDelegate(gpuDelegate)
setNumThreads(4)
}
yoloInterpreter = Interpreter(loadModelFile("models/yolov11n_seg_portrait.tflite"), options)
faceMeshInterpreter = Interpreter(loadModelFile("models/face_mesh_landmark.tflite"), options)
} catch (t: Throwable) {
t.printStackTrace()
// Fallback to CPU if GPU Delegate fails or throws linkage error
try {
val options = Interpreter.Options().apply {
setNumThreads(4)
}
yoloInterpreter = Interpreter(loadModelFile("models/yolov11n_seg_portrait.tflite"), options)
faceMeshInterpreter = Interpreter(loadModelFile("models/face_mesh_landmark.tflite"), options)
} catch (t2: Throwable) {
t2.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)
}
fun executeDualPipeline(
capturedBitmap: Bitmap,
onComplete: (Bitmap, List<PointF>, FloatArray) -> Unit
) {
CoroutineScope(Dispatchers.Default).launch {
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 {
val outputMask = FloatArray(640 * 640)
if (yoloInterpreter != null) {
try {
val inputBuffer = ByteBuffer.allocateDirect(1 * 640 * 640 * 3 * 4).apply {
order(ByteOrder.nativeOrder())
}
yoloInterpreter?.run(inputBuffer, outputMask)
} catch (e: Exception) {
e.printStackTrace()
}
} else {
val cx = 640 * 0.5f
val cy = 640 * 0.40f
val rx = 640 * 0.40f
val ry = 640 * 0.50f
for (y in 0 until 640) {
val rowOffset = y * 640
val dy = (y - cy) / ry
for (x in 0 until 640) {
val dx = (x - cx) / rx
val distSq = dx * dx + dy * dy
outputMask[rowOffset + x] = if (distSq < 1.0f) {
val t = 1.0f - distSq
t * t * (3f - 2f * t)
} else {
0.0f
}
}
}
}
return outputMask
}
fun processFaceMesh(bitmap: Bitmap): List<PointF> {
val landmarksList = ArrayList<PointF>()
for (i in 0 until 478) {
landmarksList.add(PointF(0f, 0f))
}
if (faceMeshInterpreter != null) {
try {
val inputBuffer = ByteBuffer.allocateDirect(1 * 192 * 192 * 3 * 4).apply {
order(ByteOrder.nativeOrder())
}
val outputLandmarks = Array(1) { Array(478) { FloatArray(3) } }
faceMeshInterpreter?.run(inputBuffer, outputLandmarks)
for (i in 0 until 478) {
landmarksList[i] = PointF(outputLandmarks[0][i][0], outputLandmarks[0][i][1])
}
return landmarksList
} catch (e: Exception) {
e.printStackTrace()
}
}
try {
val bmp565 = bitmap.copy(Bitmap.Config.RGB_565, true)
val maxFaces = 1
val detector = android.media.FaceDetector(bmp565.width, bmp565.height, maxFaces)
val facesArray = arrayOfNulls<android.media.FaceDetector.Face>(maxFaces)
val found = detector.findFaces(bmp565, facesArray)
bmp565.recycle()
if (found > 0) {
val face = facesArray[0]
if (face != null) {
val midPoint = PointF()
face.getMidPoint(midPoint)
val eyeDistance = face.eyesDistance()
landmarksList[10] = PointF(midPoint.x, midPoint.y - eyeDistance * 0.65f)
landmarksList[152] = PointF(midPoint.x, midPoint.y + eyeDistance * 0.75f)
landmarksList[33] = PointF(midPoint.x - eyeDistance * 0.6f, midPoint.y - eyeDistance * 0.05f)
landmarksList[263] = PointF(midPoint.x + eyeDistance * 0.6f, midPoint.y - eyeDistance * 0.05f)
landmarksList[234] = PointF(midPoint.x - eyeDistance * 0.7f, midPoint.y + eyeDistance * 0.4f)
landmarksList[454] = PointF(midPoint.x + eyeDistance * 0.7f, midPoint.y + eyeDistance * 0.4f)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return landmarksList
}
}
@@ -0,0 +1,23 @@
package com.photobooth.app
/**
* Stores bidirectional face beauty adjustment values, all in range [-1.0, 1.0].
*
* eyeEnlarge : positive = mắt to hơn, negative = mắt nhỏ lại
* noseSlim : positive = mũi thon gọn hơn, negative = mũi rộng ra
* mouthSize : positive = miệng rộng ra, negative = miệng chúm chím
* faceSlim : positive = khuôn mặt thon gọn V-line, negative = khuôn mặt rộng hơn
* foreheadSize: positive = trán cao lên, negative = trán thu gọn xuống
* skinSmooth : sign = tone mode (negative = Mịn hồng/rosy, positive = Mịn trắng/fair)
* abs(skinSmooth) = mức độ mịn da (0..1)
*/
data class FaceBeautySettings(
var eyeEnlarge: Float = 0f, // [-1, 1]
var noseSlim: Float = 0f, // [-1, 1]
var mouthSize: Float = 0f, // [-1, 1]
var faceSlim: Float = 0f, // [-1, 1] V-line jaw slim
var faceSize: Float = 0f, // [-1, 1] positive=khuôn mặt to, negative=nhỏ lại
var foreheadSize: Float = 0f, // [-1, 1]
var chinSize: Float = 0f, // [-1, 1] positive=cằm ngắn, negative=cằm dài
var skinSmooth: Float = 0f // [-1, 1] negative=hồng, positive=trắng
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
package com.photobooth.app
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.PointF
class MeshWarpEngine {
fun applyPiecewiseAffineWarp(
inputBitmap: Bitmap,
originalLandmarks: List<PointF>,
chinSlimValue: Float,
foreheadSizeValue: Float,
faceSlimValue: Float,
faceSizeValue: Float = 0f,
chinSizeValue: Float = 0f
): Bitmap {
val w = inputBitmap.width
val h = inputBitmap.height
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outputBitmap)
canvas.drawBitmap(inputBitmap, 0f, 0f, null)
if (originalLandmarks.size <= 152 || originalLandmarks[152].x == 0f) {
return inputBitmap
}
// Compute face centroid from key anchor points
val anchorIndices = listOf(10, 152, 234, 454, 33, 263)
val validAnchors = anchorIndices.filter { it < originalLandmarks.size }
val centroidX = validAnchors.map { originalLandmarks[it].x }.average().toFloat()
val centroidY = validAnchors.map { originalLandmarks[it].y }.average().toFloat()
val warpedLandmarks = ArrayList<PointF>()
for (i in originalLandmarks.indices) {
val pt = PointF(originalLandmarks[i].x, originalLandmarks[i].y)
// Face size: scale all landmarks toward/away from face centroid
if (faceSizeValue != 0f) {
val scaleFactor = 1f + faceSizeValue * 0.3f // +30% max
pt.x = centroidX + (pt.x - centroidX) * scaleFactor
pt.y = centroidY + (pt.y - centroidY) * scaleFactor
}
if (i == 152) {
// Chin slim (legacy): positive = lift chin up
pt.y -= chinSlimValue * 25.0f
// Chin size (new): positive = shorter chin (lift up), negative = longer (down)
pt.y -= chinSizeValue * 20.0f
}
if (i == 10) {
// Forehead: positive = tran cao len, negative = thu gon xuong
pt.y += foreheadSizeValue * 20.0f
}
if (i == 234) {
// Left jaw: positive = slim (move right/inward)
pt.x += faceSlimValue * 15.0f
}
if (i == 454) {
// Right jaw: positive = slim (move left/inward)
pt.x -= faceSlimValue * 15.0f
}
warpedLandmarks.add(pt)
}
val triangleIndices = listOf(
IntArray(3).apply { this[0] = 152; this[1] = 234; this[2] = 454 },
IntArray(3).apply { this[0] = 10; this[1] = 33; this[2] = 263 }
)
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]]
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)
matrix.setPolyToPoly(srcPoints, 0, dstPoints, 0, 3)
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()
}
return outputBitmap
}
/**
* Applies selective skin smoothing with optional tone mode.
*
* intensity is bidirectional [-1..1]:
* negative = Min hong (rosy/warm skin tint)
* positive = Min trang (fair/bright skin tint)
* abs(intensity) = smoothness strength
*/
fun applySkinSmoothing(src: Bitmap, intensity: Float): Bitmap {
if (intensity == 0f) return src
val absIntensity = Math.abs(intensity)
val isRosy = intensity < 0f // Min hong
val width = src.width
val height = src.height
val output = Bitmap.createBitmap(width, height, src.config)
val pixels = IntArray(width * height)
val outPixels = IntArray(width * height)
src.getPixels(pixels, 0, width, 0, 0, width, height)
val radius = (1 + (absIntensity * 4).toInt()).coerceAtLeast(1)
for (y in 0 until height) {
for (x in 0 until width) {
val idx = y * width + x
val c = pixels[idx]
val r = (c shr 16) and 0xFF
val g = (c shr 8) and 0xFF
val b = c and 0xFF
val max = maxOf(r, maxOf(g, b))
val min = minOf(r, minOf(g, b))
val isSkin = r > 95 && g > 40 && b > 20 && (max - min) > 15 &&
Math.abs(r - g) > 15 && r > g && r > b
if (isSkin) {
// Box blur on skin pixels
var sumR = 0; var sumG = 0; var sumB = 0; var count = 0
for (dy in -radius..radius) {
val ny = y + dy
if (ny in 0 until height) {
val rowOffset = ny * width
for (dx in -radius..radius) {
val nx = x + dx
if (nx in 0 until width) {
val nc = pixels[rowOffset + nx]
sumR += (nc shr 16) and 0xFF
sumG += (nc shr 8) and 0xFF
sumB += nc and 0xFF
count++
}
}
}
}
val avgR = sumR / count
val avgG = sumG / count
val avgB = sumB / count
// Blend original with smoothed
var finalR = (r * (1f - absIntensity) + avgR * absIntensity).toInt().coerceIn(0, 255)
var finalG = (g * (1f - absIntensity) + avgG * absIntensity).toInt().coerceIn(0, 255)
var finalB = (b * (1f - absIntensity) + avgB * absIntensity).toInt().coerceIn(0, 255)
if (isRosy) {
// Min hong: warm rosy boost Red, slightly cool Blue
val tint = (absIntensity * 20).toInt()
finalR = (finalR + tint).coerceIn(0, 255)
finalG = (finalG - tint / 3).coerceIn(0, 255)
finalB = (finalB - tint / 2).coerceIn(0, 255)
} else {
// Min trang: bright fair lift all channels equally (brighten)
val lift = (absIntensity * 25).toInt()
finalR = (finalR + lift).coerceIn(0, 255)
finalG = (finalG + lift).coerceIn(0, 255)
finalB = (finalB + lift).coerceIn(0, 255)
}
outPixels[idx] = (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB
} else {
outPixels[idx] = c
}
}
}
output.setPixels(outPixels, 0, width, 0, 0, width, height)
return output
}
}
@@ -0,0 +1,102 @@
package com.photobooth.app
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
class StickerAdapter(
private var items: List<StickerItem>,
private val onStickerSelected: (StickerItem) -> Unit
) : RecyclerView.Adapter<StickerAdapter.ViewHolder>() {
private var selectedPosition = 0
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val cardSubFrame: MaterialCardView = view.findViewById(R.id.cardSubFrame)
val imgFrameThumb: ImageView = view.findViewById(R.id.imgFrameThumb)
val txtFrameName: TextView = view.findViewById(R.id.txtFrameName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_frame_sub, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.txtFrameName.text = item.name
if (item.id == "DEFAULT") {
holder.imgFrameThumb.setImageResource(R.drawable.ic_block_white)
holder.txtFrameName.text = "Không dùng"
} else if (item.id == "MORE") {
holder.imgFrameThumb.setImageResource(R.drawable.ic_more)
holder.txtFrameName.text = "Tải thêm"
} else {
try {
val localFile = java.io.File(holder.itemView.context.filesDir, item.imagePath)
val bitmap = if (localFile.exists()) {
BitmapFactory.decodeFile(localFile.absolutePath)
} else {
val path = if (item.imagePath.startsWith("stickers/")) {
item.imagePath
} else {
"stickers/${item.imagePath}"
}
val inputStream = holder.itemView.context.assets.open(path)
BitmapFactory.decodeStream(inputStream)
}
if (bitmap != null) {
holder.imgFrameThumb.setImageBitmap(bitmap)
} else {
holder.imgFrameThumb.setImageResource(R.drawable.ic_image_gallery)
}
} catch (e: Exception) {
e.printStackTrace()
holder.imgFrameThumb.setImageResource(R.drawable.ic_image_gallery)
}
}
// Highlight stroke for selected
val strokeWidthPx = (3 * holder.itemView.context.resources.displayMetrics.density).toInt()
if (position == selectedPosition) {
holder.cardSubFrame.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_primary)
holder.cardSubFrame.strokeWidth = strokeWidthPx
} else {
holder.cardSubFrame.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_transparent)
holder.cardSubFrame.strokeWidth = 0
}
holder.itemView.setOnClickListener {
val old = selectedPosition
selectedPosition = holder.adapterPosition
notifyItemChanged(old)
notifyItemChanged(selectedPosition)
onStickerSelected(item)
}
}
override fun getItemCount(): Int = items.size
fun updateItems(newItems: List<StickerItem>) {
items = newItems
notifyDataSetChanged()
}
fun selectItem(stickerId: String) {
val index = items.indexOfFirst { it.id == stickerId }
if (index != -1) {
val old = selectedPosition
selectedPosition = index
notifyItemChanged(old)
notifyItemChanged(selectedPosition)
}
}
}
@@ -0,0 +1,7 @@
package com.photobooth.app
data class StickerItem(
val id: String,
val name: String,
val imagePath: String
)
@@ -51,6 +51,7 @@ class StoreItemAdapter(
"frames" -> holder.imgThumb.setImageResource(R.drawable.ic_default_frame_thumbnail) "frames" -> holder.imgThumb.setImageResource(R.drawable.ic_default_frame_thumbnail)
"backgrounds" -> holder.imgThumb.setImageResource(R.drawable.ic_image_gallery) "backgrounds" -> holder.imgThumb.setImageResource(R.drawable.ic_image_gallery)
"presets" -> holder.imgThumb.setImageResource(R.drawable.ic_default_preset_thumbnail) "presets" -> holder.imgThumb.setImageResource(R.drawable.ic_default_preset_thumbnail)
"stickers" -> holder.imgThumb.setImageResource(R.drawable.ic_main_sticker)
else -> holder.imgThumb.setImageResource(R.drawable.ic_image_gallery) else -> holder.imgThumb.setImageResource(R.drawable.ic_image_gallery)
} }
@@ -115,6 +116,11 @@ class StoreItemAdapter(
val localFile = File(filesDir, "${item.id}.json") val localFile = File(filesDir, "${item.id}.json")
localFile.exists() localFile.exists()
} }
"stickers" -> {
val ext = item.url.substringAfterLast(".", "png")
val localFile = File(filesDir, "downloaded_stickers/${item.id}.$ext")
localFile.exists()
}
else -> false else -> false
} }
} }
@@ -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,21c-3.31,0 -6,-2.69 -6,-6v-1h2v1c0,2.21 1.79,4 4,4s4,-1.79 4,-4v-1h2v1c0,3.31 -2.69,6 -6,6zM12,3c-4.42,0 -8,3.58 -8,8v1h2v-1c0,-3.31 2.69,-6 6,-6s6,2.69 6,6v1h2v-1c0,-4.42 -3.58,-8 -8,-8z" />
</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,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</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,2A10,10 0,0 0,2 12c0,3.61 1.93,6.77 4.82,8.54C7.75,18.57 9.77,17 12,17s4.25,1.57 5.18,3.54C20.07,18.77 22,15.61 22,12A10,10 0,0 0,12,2M12,15a3.5,3.5 0,1 1,3.5 -3.5A3.5,3.5 0,0 1,12,15z" />
</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,2c-4.97,0 -9,4.03 -9,9h2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7h2c0,-4.97 -4.03,-9 -9,-9zM12,14c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3s-1.34,-3 -3,-3z" />
</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,16c-3.13,0 -5.67,-1.82 -6.74,-4.31C5.09,11.3 5.4,11 5.92,11h12.16c0.52,0 0.83,0.3 0.66,0.69C17.67,14.18 15.13,16 12,16zM12,8c3.13,0 5.67,1.82 6.74,4.31c0.17,0.39 -0.14,0.69 -0.66,0.69H5.92c-0.52,0 -0.83,-0.3 -0.66,-0.69C6.33,9.82 8.87,8 12,8z" />
</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,2c-1.1,0 -2,0.9 -2,2v8.5c-1.5,-0.8 -3.5,-0.8 -5,0.5c-1.2,1 -1.2,2.8 -0.2,3.8c1,1 2.8,1 3.8,-0.2c0.8,-1 1,-2.5 0.4,-3.8L10,12v5c0,1.1 0.9,2 2,2s2,-0.9 2,-2v-5l1,0.8c-0.6,1.3 -0.4,2.8 0.4,3.8c1,1.2 2.8,1.2 3.8,0.2c1,-1 1,-2.8 -0.2,-3.8c-1.5,-1.3 -3.5,-1.3 -5,-0.5V4C14,2.9 13.1,2 12,2z" />
</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="M9,9.5L7.5,11L6,9.5L4.5,11L3,9.5c0,-3.31 2.69,-6 6,-6c0.55,0 1,0.45 1,1s-0.45,1 -1,1c-2.21,0 -4,1.79 -4,4l0.5,-0.5L7.5,10l1.5,-1L9,9.5zM21,9.5l-1.5,1.5L18,9.5l-1.5,1.5l-1.5,-1.5c0,-3.31 2.69,-6 6,-6c0.55,0 1,0.45 1,1s-0.45,1 -1,1c-2.21,0 -4,1.79 -4,4l0.5,-0.5l2,-1l1.5,-1l0,0.5zM12,18.5c0,-3.31 2.69,-6 6,-6c0.55,0 1,0.45 1,1s-0.45,1 -1,1c-2.21,0 -4,1.79 -4,4l0.5,-0.5l2,-1l1.5,-1l0,0.5l-1.5,1.5L15,18.5c0,-3.31 -2.69,-6 -6,-6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1c2.21,0 4,1.79 4,4l-0.5,-0.5l-2,-1l-1.5,-1l0,0.5l1.5,1.5l-1.5,1.5z" />
</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="M9,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25s1.25,-0.56 1.25,-1.25s-0.56,-1.25 -1.25,-1.25zM15,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25s1.25,-0.56 1.25,-1.25s-0.56,-1.25 -1.25,-1.25zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-0.05 0.01,-0.1 0.01,-0.15c2.42,-0.92 4.29,-3.03 5.09,-5.63c2.42,2.1 5.67,3.38 9.25,3.38c0.79,0 1.56,-0.06 2.31,-0.17c0.22,0.8 0.34,1.64 0.34,2.51c0,4.41 -3.59,8 -8,8z" />
</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="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8s8,3.58 8,8s-3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8S14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8S7,8.67 7,9.5S7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5H6.89c0.8,2.04 2.78,3.5 5.11,3.5z" />
</vector>
@@ -126,6 +126,16 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:contentDescription="Frame Overlay" /> android:contentDescription="Frame Overlay" />
<!-- Lớp đè hình dán (Stickers Overlay) -->
<ImageView
android:id="@+id/imgStickerOverlay"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:visibility="gone"
android:contentDescription="Sticker Overlay" />
<!-- Lớp chớp sáng màn hình khi chụp (Flash Effect) --> <!-- Lớp chớp sáng màn hình khi chụp (Flash Effect) -->
<View <View
android:id="@+id/viewShutterFlash" android:id="@+id/viewShutterFlash"
@@ -249,8 +259,8 @@
<!-- Nút Frame --> <!-- Nút Frame -->
<ImageView <ImageView
android:id="@+id/btnMainFrame" android:id="@+id/btnMainFrame"
android:layout_width="50dp" android:layout_width="48dp"
android:layout_height="50dp" android:layout_height="48dp"
android:src="@drawable/ic_default_frame_thumbnail" android:src="@drawable/ic_default_frame_thumbnail"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -258,49 +268,63 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnMainPreset" app:layout_constraintEnd_toStartOf="@id/btnMainPreset"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
android:layout_marginEnd="12dp" android:layout_marginEnd="10dp"
android:contentDescription="Frame Selector" /> android:contentDescription="Frame Selector" />
<!-- Nút Presets Màu --> <!-- Nút Presets Màu -->
<ImageView <ImageView
android:id="@+id/btnMainPreset" android:id="@+id/btnMainPreset"
android:layout_width="50dp" android:layout_width="48dp"
android:layout_height="50dp" android:layout_height="48dp"
android:src="@drawable/ic_default_preset_thumbnail" android:src="@drawable/ic_default_preset_thumbnail"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnMainFrame" app:layout_constraintStart_toEndOf="@id/btnMainFrame"
app:layout_constraintEnd_toStartOf="@id/btnMainPortraitBlur" app:layout_constraintEnd_toStartOf="@id/btnMainBackground"
android:layout_marginEnd="12dp" android:layout_marginEnd="10dp"
android:contentDescription="Preset Color Selector" /> android:contentDescription="Preset Color Selector" />
<!-- Nút Xóa nền (Portrait Blur) --> <!-- Nút Nền (Backgrounds - Thay/Xóa nền) -->
<ImageView <ImageView
android:id="@+id/btnMainPortraitBlur" android:id="@+id/btnMainBackground"
android:layout_width="50dp" android:layout_width="48dp"
android:layout_height="50dp" android:layout_height="48dp"
android:src="@drawable/ic_attr_portrait_blur"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnMainPreset"
app:layout_constraintEnd_toStartOf="@id/btnMainBgReplace"
android:layout_marginEnd="12dp"
android:contentDescription="Portrait Blur Selector" />
<!-- Nút Thay nền (BG Replace) -->
<ImageView
android:id="@+id/btnMainBgReplace"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_attr_bg_replace" android:src="@drawable/ic_attr_bg_replace"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnMainPortraitBlur" app:layout_constraintStart_toEndOf="@id/btnMainPreset"
app:layout_constraintEnd_toStartOf="@id/btnMainSticker"
android:layout_marginEnd="10dp"
android:contentDescription="Background Selector" />
<!-- Nút Stickers (Hình dán) -->
<ImageView
android:id="@+id/btnMainSticker"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_main_sticker"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnMainBackground"
app:layout_constraintEnd_toStartOf="@id/btnMainBeauty"
android:layout_marginEnd="10dp"
android:contentDescription="Stickers Selector" />
<!-- Nút Beauty (Làm đẹp) -->
<ImageView
android:id="@+id/btnMainBeauty"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_main_beauty"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/btnMainSticker"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="Background Replace Selector" /> android:contentDescription="Beauty Selector" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </FrameLayout>
@@ -76,16 +76,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:weightSum="3"> android:weightSum="4">
<Button <Button
android:id="@+id/btnTabFrames" android:id="@+id/btnTabFrames"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginEnd="4dp" android:layout_marginEnd="2dp"
android:text="Khung ảnh" android:text="Khung"
android:textSize="12sp" android:textSize="11sp"
android:backgroundTint="@color/theme_primary" android:backgroundTint="@color/theme_primary"
android:textColor="@color/theme_panel_dark" android:textColor="@color/theme_panel_dark"
android:paddingHorizontal="2dp" android:paddingHorizontal="2dp"
@@ -97,8 +97,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="2dp" android:layout_marginHorizontal="2dp"
android:text="Phông nền" android:text="Nền"
android:textSize="12sp" android:textSize="11sp"
android:backgroundTint="@color/theme_item_card_bg" android:backgroundTint="@color/theme_item_card_bg"
android:textColor="@color/theme_white" android:textColor="@color/theme_white"
android:paddingHorizontal="2dp" android:paddingHorizontal="2dp"
@@ -109,9 +109,22 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="4dp" android:layout_marginHorizontal="2dp"
android:text="Bộ màu" android:text="Màu"
android:textSize="12sp" android:textSize="11sp"
android:backgroundTint="@color/theme_item_card_bg"
android:textColor="@color/theme_white"
android:paddingHorizontal="2dp"
android:minHeight="40dp" />
<Button
android:id="@+id/btnTabStickers"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="2dp"
android:text="Dán"
android:textSize="11sp"
android:backgroundTint="@color/theme_item_card_bg" android:backgroundTint="@color/theme_item_card_bg"
android:textColor="@color/theme_white" android:textColor="@color/theme_white"
android:paddingHorizontal="2dp" android:paddingHorizontal="2dp"
+1
View File
@@ -2,3 +2,4 @@ plugins {
id("com.android.application") version "8.2.2" apply false id("com.android.application") version "8.2.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false
} }
+64
View File
@@ -0,0 +1,64 @@
import os
import urllib.request
from ultralytics import YOLO
# 1. Định nghĩa cấu trúc đường dẫn thư mục Assets của dự án Android
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
ASSETS_MODELS_DIR = os.path.join(PROJECT_ROOT, "android-project", "app", "src", "main", "assets", "models")
# Tạo thư mục nếu chưa tồn tại
os.makedirs(ASSETS_MODELS_DIR, exist_ok=True)
print(f"🚀 Bắt đầu tiến trình tự động hóa tài nguyên AI...")
print(f"📂 Thư mục đích: {ASSETS_MODELS_DIR}\n" + "-"*50)
# =====================================================================
# PHẦN 1: TỰ ĐỘNG TẢI VÀ CHUYỂN ĐỔI YOLOV11-SEGMENTATION
# =====================================================================
try:
print("⏳ 1. Đang tải và cấu hình YOLOv11-Segmentation (Bản Nano)...")
# Ultralytics sẽ tự động tải file .pt từ bản phát hành chính thức nếu chưa có sẵn
yolo_model = YOLO("yolov11n-seg.pt")
print("🔄 Đang export YOLOv11-Seg sang định dạng TFLite (Float16 tối ưu GPU)...")
# Cấu hình imgsz=640 khớp hoàn toàn với kiến trúc xử lý của DualAiEngine
exported_path = yolo_model.export(format="tflite", imgsz=640, half=True, int8=False)
# Tìm file .tflite vừa được sinh ra trong thư mục kết quả của Ultralytics
# Mặc định cấu trúc đầu ra là: yolov11n-seg_saved_model/yolov11n-seg_float16.tflite
yolo_generated_file = os.path.join(PROJECT_ROOT, "yolov11n-seg_saved_model", "yolov11n-seg_float16.tflite")
if os.path.exists(yolo_generated_file):
dest_yolo_path = os.path.join(ASSETS_MODELS_DIR, "yolov11n_seg_portrait.tflite")
os.replace(yolo_generated_file, dest_yolo_path)
print(f"✅ Đã chuyển đổi và di chuyển YOLOv11 thành công vào: {dest_yolo_path}")
else:
print("❌ Lỗi: Không tìm thấy file TFLite của YOLO sau khi export!")
except Exception as e:
print(f"❌ Thất bại khi xử lý YOLOv11: {str(e)}")
print("-"*50)
# =====================================================================
# PHẦN 2: TỰ ĐỘNG TẢI GOOGLE MEDIAPIPE FACE MESH
# =====================================================================
# URL tải trực tiếp mô hình Face Landmarker (Bundle gồm Face Mesh) chính thức của Google
MEDIAPIPE_MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
dest_mesh_path = os.path.join(ASSETS_MODELS_DIR, "face_mesh_landmark.tflite")
try:
print("⏳ 2. Đang tải trực tiếp mô hình Google Face Mesh từ Cloud Storage...")
# Thiết lập thanh tiến trình tải (Progress Bar) đơn giản
def download_progress(count, block_size, total_size):
percent = int(count * block_size * 100 / total_size)
print(f"\r📥 Đang tải: {percent}%", end="")
urllib.request.urlretrieve(MEDIAPIPE_MODEL_URL, dest_mesh_path, download_progress)
print(f"\n✅ Đã tải và đổi đuôi mô hình Face Mesh thành công vào: {dest_mesh_path}")
except Exception as e:
print(f"\n❌ Thất bại khi tải mô hình Face Mesh: {str(e)}")
print("="*50)
print("🎉 HOÀN THÀNH Quy trình tự động hóa cung cấp mô hình AI cho dự án!")