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:
@@ -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)** và **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).
|
||||
@@ -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)** 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.
|
||||
@@ -65,4 +65,25 @@ dependencies {
|
||||
|
||||
// --- Google MediaPipe Tasks Vision ---
|
||||
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!")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,8 @@ enum class EditAttribute {
|
||||
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
|
||||
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(
|
||||
@@ -34,7 +35,7 @@ class AttributeAdapter(
|
||||
notifyItemChanged(value)
|
||||
}
|
||||
|
||||
fun updateSelection(selectedType: EditAttribute) {
|
||||
fun updateSelection(selectedType: EditAttribute?) {
|
||||
val index = items.indexOfFirst { it.type == selectedType }
|
||||
if (index != -1) {
|
||||
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)
|
||||
"backgrounds" -> holder.imgThumb.setImageResource(R.drawable.ic_image_gallery)
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -115,6 +116,11 @@ class StoreItemAdapter(
|
||||
val localFile = File(filesDir, "${item.id}.json")
|
||||
localFile.exists()
|
||||
}
|
||||
"stickers" -> {
|
||||
val ext = item.url.substringAfterLast(".", "png")
|
||||
val localFile = File(filesDir, "downloaded_stickers/${item.id}.$ext")
|
||||
localFile.exists()
|
||||
}
|
||||
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: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) -->
|
||||
<View
|
||||
android:id="@+id/viewShutterFlash"
|
||||
@@ -249,8 +259,8 @@
|
||||
<!-- Nút Frame -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainFrame"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_default_frame_thumbnail"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -258,49 +268,63 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnMainPreset"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="Frame Selector" />
|
||||
|
||||
<!-- Nút Presets Màu -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainPreset"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_default_preset_thumbnail"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btnMainFrame"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnMainPortraitBlur"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnMainBackground"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="Preset Color Selector" />
|
||||
|
||||
<!-- Nút Xóa nền (Portrait Blur) -->
|
||||
<!-- Nút Nền (Backgrounds - Thay/Xóa nền) -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainPortraitBlur"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
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:id="@+id/btnMainBackground"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_attr_bg_replace"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintTop_toTopOf="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"
|
||||
android:contentDescription="Background Replace Selector" />
|
||||
android:contentDescription="Beauty Selector" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -76,16 +76,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:weightSum="3">
|
||||
android:weightSum="4">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnTabFrames"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="Khung ảnh"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:text="Khung"
|
||||
android:textSize="11sp"
|
||||
android:backgroundTint="@color/theme_primary"
|
||||
android:textColor="@color/theme_panel_dark"
|
||||
android:paddingHorizontal="2dp"
|
||||
@@ -97,8 +97,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:text="Phông nền"
|
||||
android:textSize="12sp"
|
||||
android:text="Nền"
|
||||
android:textSize="11sp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:paddingHorizontal="2dp"
|
||||
@@ -109,9 +109,22 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="Bộ màu"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:text="Màu"
|
||||
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:textColor="@color/theme_white"
|
||||
android:paddingHorizontal="2dp"
|
||||
|
||||
@@ -2,3 +2,4 @@ plugins {
|
||||
id("com.android.application") version "8.2.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -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!")
|
||||
Reference in New Issue
Block a user