Implement Touch-to-Focus (28_FOCUS_POINT.md): cache MediaPipe category mask, add focus ring view, class-based alpha selection
This commit is contained in:
@@ -0,0 +1,261 @@
|
|||||||
|
Dưới đây là kế hoạch Markdown chi tiết và hoàn chỉnh để triển khai tính năng **Chạm để lấy nét (Touch-to-Focus)**, tối ưu riêng cho cấu trúc chỉ sử dụng mô hình **Google MediaPipe Multiclass Segmenter (`multiclass_segmenter.tflite`)** và khắc phục triệt để tình trạng giật lag trên kính ngắm (Viewfinder) khi thay đổi độ nhòe DoF.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎯 KẾ HOẠCH TRIỂN KHAI: CHẠM LẤY NÉT ĐA LỚP & KHẮC PHỤC LAG VIEWFINDER REAL-TIME
|
||||||
|
|
||||||
|
Mục tiêu cốt lõi:
|
||||||
|
|
||||||
|
1. Bắt tọa độ chạm `(X, Y)` trên Viewfinder để xác định người dùng muốn ưu tiên lấy nét vào thực thể nào (Người hoặc Đồ vật cụ thể).
|
||||||
|
2. Vẽ vòng tròn định vị lấy nét màu cam (Focus Ring Animation) và tự ẩn sau 1 giây.
|
||||||
|
3. **Triệt tiêu lag Viewfinder (60 FPS):** Thay vì ép CPU quét ma trận pixel liên tục khi người dùng ngắm hoặc trượt Ruler, ứng dụng sẽ sử dụng **GPU Shader giả lập (BackdropFilter Proxy)** trên UI để phản hồi mượt mà tức thì.
|
||||||
|
4. **Xử lý bất đồng bộ (Async Coroutine):** Cô lập luồng xử lý ảnh độ phân giải cao, làm mịn viền tóc và khử Ghost Shadow chạy ngầm hoàn toàn sau khi nhấn nút **Shutter**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 1. Quy Hoạch Giao Diện Viewfinder & Cử Chỉ Chạm Lấy Nét (`CameraScreen.dart`)
|
||||||
|
|
||||||
|
Bao bọc khu vực hiển thị kính ngắm bằng `GestureDetector` để bắt đồng thời hai hành động: **Chạm để lấy nét** (`onTapUp`) và **Vuốt màn hình để tăng giảm Ruler** (`onHorizontalDragUpdate`).
|
||||||
|
|
||||||
|
Tận dụng phần cứng đồ họa (GPU) để làm mờ nhanh giao diện hiển thị nhằm phản hồi tức thì theo tay người dùng mà không gây gánh nặng cho CPU.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
double _blurIntensity = 0.0; // Đồng bộ từ thanh trượt Ruler (0.0 -> 1.0)
|
||||||
|
Offset? _focusTouchPosition; // Tọa độ điểm chạm để vẽ vòng tròn nét trên UI
|
||||||
|
|
||||||
|
Widget _buildOptimizedTouchFocusViewfinder() {
|
||||||
|
return GestureDetector(
|
||||||
|
// 1. CỬ CHỈ VUỐT: Điều chỉnh tăng giảm Ruler cùng chiều (Sang phải là tăng)
|
||||||
|
onHorizontalDragUpdate: (details) {
|
||||||
|
setState(() {
|
||||||
|
_blurIntensity = (_blurIntensity + (details.delta.dx / 200.0)).clamp(0.0, 1.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 2. CỬ CHỈ CHẠM: Chọn điểm lấy nét
|
||||||
|
onTapUp: (TapUpDetails details) {
|
||||||
|
final RenderBox box = context.findRenderObject() as RenderBox;
|
||||||
|
final Offset localPosition = box.globalToLocal(details.globalPosition);
|
||||||
|
|
||||||
|
// Tính tỷ lệ % tọa độ chạm (0.0 -> 1.0) để gửi xuống tầng Native Engine định vị ID vật thể
|
||||||
|
double percentX = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
|
||||||
|
double percentY = (localPosition.dy / box.size.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_focusTouchPosition = localPosition;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gửi tọa độ xuống Native để xác định lớp đối tượng (Người/Vật) tại điểm chạm
|
||||||
|
_sendTouchFocusToNative(percentX, percentY);
|
||||||
|
|
||||||
|
// Ẩn vòng tròn định vị lấy nét sau 1 giây
|
||||||
|
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_focusTouchPosition = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 3 / 4,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Luồng Camera thô từ phần cứng - Luôn mượt mà 60 FPS
|
||||||
|
CameraPreview(_cameraController),
|
||||||
|
|
||||||
|
// GIẢI PHÁP TRIỆT TIÊU LAG: Làm mờ toàn màn hình siêu tốc bằng GPU Shader
|
||||||
|
if (_blurIntensity > 0)
|
||||||
|
Positioned.fill(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(
|
||||||
|
sigmaX: _blurIntensity * 15.0,
|
||||||
|
sigmaY: _blurIntensity * 15.0
|
||||||
|
),
|
||||||
|
child: Container(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// GIẢ LẬP ĐỤC LỖ TIÊU CỰ: Tạo vùng nét giả lập tại vị trí người dùng chạm tay
|
||||||
|
// Nếu chưa chạm, mặc định đục lỗ ở tâm màn hình (Mô phỏng lấy nét tự động vào Người)
|
||||||
|
if (_blurIntensity > 0)
|
||||||
|
Positioned(
|
||||||
|
left: (_focusTouchPosition?.dx ?? box.size.width / 2) - 110,
|
||||||
|
top: (_focusTouchPosition?.dy ?? box.size.height / 2) - 160,
|
||||||
|
child: Container(
|
||||||
|
width: 220,
|
||||||
|
height: 320,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
borderRadius: BorderRadius.circular(160),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 30,
|
||||||
|
spreadRadius: 20,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Vẽ vòng tròn định vị lấy nét màu cam nhiếp ảnh khi user chạm vào kính ngắm
|
||||||
|
if (_focusTouchPosition != null)
|
||||||
|
Positioned(
|
||||||
|
left: _focusTouchPosition!.dx - 25,
|
||||||
|
top: _focusTouchPosition!.dy - 25,
|
||||||
|
child: const FocusRingWidget(), // Hiệu ứng vòng tròn thu nhỏ nhẹ
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 2. Thuật Toán Lõi: Khóa Tiêu Cự Vùng Chọn Đa Lớp & Xử Lý Ảnh Chụp Ngầm (`MainActivity.kt`)
|
||||||
|
|
||||||
|
Vì không còn mô hình chiều sâu MiDaS, cơ chế chọn điểm lấy nét khi sử dụng duy nhất MediaPipe sẽ dựa trên **Phân loại ID thực thể (Class ID Preference)**.
|
||||||
|
|
||||||
|
Khi nhận tọa độ chạm, hệ thống tra cứu xem điểm đó là **Người (ID 1)** hay **Đồ vật (ID 3)** để khóa mục tiêu giữ nét căng, đồng thời đẩy toàn bộ vòng lặp pixel nặng xuống luồng ngầm (Coroutine) khi bấm Shutter.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// Biến lưu trữ ID của lớp đối tượng được chọn lấy nét (-1: Tự động lấy nét mặc định vào Người)
|
||||||
|
var userSelectedClassFocus = -1
|
||||||
|
|
||||||
|
fun updateFocusFromTouchPoint(percentX: Float, percentY: Float, categoryMaskBuffer: ByteBuffer, w: Int, h: Int) {
|
||||||
|
// Ánh xạ tọa độ tỷ lệ chạm về kích thước ma trận của MediaPipe
|
||||||
|
val maskX = (percentX * w).toInt().coerceIn(0, w - 1)
|
||||||
|
val maskY = (percentY * h).toInt().coerceIn(0, h - 1)
|
||||||
|
val bufferIndex = maskY * w + maskX
|
||||||
|
|
||||||
|
categoryMaskBuffer.rewind()
|
||||||
|
// Khóa Class ID tại điểm chạm (Ví dụ chạm vào gấu bông -> Khóa ID 3, chạm vào người -> Khóa ID 1)
|
||||||
|
userSelectedClassFocus = categoryMaskBuffer.get(bufferIndex).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processMediaPipeTouchBokeh(
|
||||||
|
capturedBitmap: Bitmap,
|
||||||
|
categoryMaskBuffer: ByteBuffer,
|
||||||
|
rulerValue: Int
|
||||||
|
): Bitmap {
|
||||||
|
val w = capturedBitmap.width
|
||||||
|
val h = capturedBitmap.height
|
||||||
|
|
||||||
|
if (rulerValue == 0) return capturedBitmap
|
||||||
|
|
||||||
|
// --- BƯỚC 1: ĐỊNH VỊ VÙNG TIÊU CỰ THEO ĐIỂM CHẠM ---
|
||||||
|
val alphaMask = FloatArray(w * h)
|
||||||
|
categoryMaskBuffer.rewind()
|
||||||
|
|
||||||
|
// Nếu chưa chạm, mặc định đích lấy nét là Người (ID 1) và Đồ vật bổ trợ (ID 3)
|
||||||
|
val targetClassId1 = if (userSelectedClassFocus >= 0) userSelectedClassFocus else 1
|
||||||
|
val targetClassId2 = if (userSelectedClassFocus >= 0) userSelectedClassFocus else 3
|
||||||
|
|
||||||
|
for (i in 0 until (w * h)) {
|
||||||
|
val classId = categoryMaskBuffer.get(i).toInt()
|
||||||
|
// Pixel nào trùng với ID thực thể được chạm chọn sẽ nhận trọng số giữ nét = 1.0f
|
||||||
|
alphaMask[i] = if (classId == targetClassId1 || classId == targetClassId2) 1.0f else 0.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Làm mờ mảng mặt nạ (radius = 4) để tạo dải Gradient chuyển tiếp viền tóc mềm mại, khử hoàn toàn răng cưa
|
||||||
|
blurAlphaMask1D(alphaMask, w, h, radius = 4)
|
||||||
|
|
||||||
|
// --- BƯỚC 2: TÍNH THANG ĐỘ MỜ PHI TUYẾN TÍNH THEO RULER ---
|
||||||
|
val normalizedSlider = rulerValue / 100.0f
|
||||||
|
val opticalIntensity = Math.pow(normalizedSlider.toDouble(), 2.5).toFloat()
|
||||||
|
val maxRadius = (35 * opticalIntensity).coerceAtLeast(1.0f)
|
||||||
|
|
||||||
|
// --- BƯỚC 3: XÓA BỎ BÓNG MA VIỀN (GHOST SHADOW) ---
|
||||||
|
val cleanBgBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||||
|
for (y in 0 until h) {
|
||||||
|
for (x in 0 until w) {
|
||||||
|
val idx = y * w + x
|
||||||
|
if (alphaMask[idx] > 0.05f) {
|
||||||
|
// Thay thế pixel người/vật bằng màu nền gần nhất để khi làm mờ không bị lem vệt màu da ra rìa vai
|
||||||
|
cleanBgBitmap.setPixel(x, y, getNearestPureBackgroundPixel(capturedBitmap, alphaMask, x, y, w, h))
|
||||||
|
} else {
|
||||||
|
cleanBgBitmap.setPixel(x, y, capturedBitmap.getPixel(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo 3 tầng mờ phân cấp mô phỏng độ sâu trường ảnh DoF dựa theo khoảng cách rìa biên mặt nạ
|
||||||
|
val blurStage1 = boxBlur(cleanBgBitmap, (maxRadius * 0.3f).toInt().coerceAtLeast(1)) // Nền sát biên mờ nhẹ
|
||||||
|
val blurStage2 = boxBlur(cleanBgBitmap, (maxRadius * 0.6f).toInt().coerceAtLeast(1)) // Nền trung cảnh mờ vừa
|
||||||
|
val blurStage3 = boxBlur(cleanBgBitmap, maxRadius.toInt().coerceAtLeast(1)) // Nền xa vô cực mờ sâu
|
||||||
|
cleanBgBitmap.recycle()
|
||||||
|
|
||||||
|
// --- BƯỚC 4: ALPHA BLENDING TRỘN ẢNH XUẤT XƯỞNG ---
|
||||||
|
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||||
|
for (y in 0 until h) {
|
||||||
|
for (x in 0 until w) {
|
||||||
|
val idx = y * w + x
|
||||||
|
val alpha = alphaMask[idx]
|
||||||
|
|
||||||
|
val edgeDistanceFactor = calculateEdgeDistanceFactor(x, y, alphaMask, w, h)
|
||||||
|
val bgPixel = when {
|
||||||
|
edgeDistanceFactor < 0.2f -> blurStage1.getPixel(x, y)
|
||||||
|
edgeDistanceFactor < 0.5f -> blurStage2.getPixel(x, y)
|
||||||
|
else -> blurStage3.getPixel(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fgPixel = capturedBitmap.getPixel(x, y)
|
||||||
|
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
|
||||||
|
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
|
||||||
|
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
|
||||||
|
|
||||||
|
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blurStage1.recycle()
|
||||||
|
blurStage2.recycle()
|
||||||
|
blurStage3.recycle()
|
||||||
|
|
||||||
|
return outputBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 3. Cơ Chế Cô Lập Luồng Khi Chụp (Shutter Background Queue)
|
||||||
|
|
||||||
|
Khi người dùng nhấn nút Shutter, để camera không bị đứng hình, toàn bộ hàm xử lý đồ họa pixel phức tạp ở trên sẽ được đóng gói và đẩy xuống luồng xử lý ngầm (Coroutine Background Task).
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
fun onShutterPressed(capturedBitmap: Bitmap, rawMaskBuffer: ByteBuffer, rulerValue: Int) {
|
||||||
|
// 1. UI phản hồi chớp sáng màn hình và thu nhỏ ảnh về thumbnail lập tức
|
||||||
|
triggerFlashAnimation()
|
||||||
|
updateMediaThumbnailPreview(capturedBitmap)
|
||||||
|
|
||||||
|
// 2. Đẩy xử lý AI đa lớp và thuật toán khử Ghost Shadow xuống luồng ngầm độc lập
|
||||||
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
|
val finalPhoto = processMediaPipeOnlyBokeh(
|
||||||
|
capturedBitmap = capturedBitmap,
|
||||||
|
categoryMaskBuffer = rawMaskBuffer,
|
||||||
|
rulerValue = rulerValue
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lưu im lặng âm thầm vào thư viện máy
|
||||||
|
saveToSystemGallery(finalPhoto)
|
||||||
|
finalPhoto.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 4. Checklist Xác Minh Sau Khi Sửa Lỗi (Verification Checklist)
|
||||||
|
|
||||||
|
* [ ] **Kiểm thử độ mượt Viewfinder (60 FPS):** Vuốt màn hình qua lại liên tục để chỉnh Ruler độ nhòe. Giao diện kính ngắm phải di chuyển trơn tru, thước số chạy mượt, không có hiện tượng khựng hay giật lag khung hình.
|
||||||
|
* [ ] **Kiểm thử chạm lấy nét (Touch-to-Focus):** Đặt một đồ vật (như ly nước) trước camera. Chạm tay vào ly nước xem phông nền và con người phía sau có mờ đi không. Sau đó chạm lại vào khuôn mặt xem ly nước có nhòe đi đúng quy luật quang học không.
|
||||||
|
* [ ] **Kiểm thử chất lượng viền tóc & Ghost Shadow:** Chụp một bức ảnh chân dung, phóng to ảnh trong thư viện máy lên kiểm tra viền vai và tóc. (Yêu cầu: Không còn vệt bóng mờ màu da lồi ra nền, các sợi tóc mảnh hòa quyện mượt mà vào phông nền).
|
||||||
@@ -69,6 +69,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var imageSegmenter: ImageSegmenter? = null
|
private var imageSegmenter: ImageSegmenter? = null
|
||||||
private var isAiSupported = true
|
private var isAiSupported = true
|
||||||
|
|
||||||
|
// Touch-to-Focus: ID lớp đối tượng được chạm chọn lấy nét (-1 = mặc định lấy nét người ID 1)
|
||||||
|
private var userSelectedClassFocus = -1
|
||||||
|
// Cache mặt nạ phân lớp mới nhất từ MediaPipe để phục vụ Touch-to-Focus
|
||||||
|
private var cachedCategoryMask: ByteArray? = null
|
||||||
|
private var cachedMaskWidth = 0
|
||||||
|
private var cachedMaskHeight = 0
|
||||||
|
|
||||||
private fun getImageSegmenter(): ImageSegmenter? {
|
private fun getImageSegmenter(): ImageSegmenter? {
|
||||||
synchronized(aiInferenceLock) {
|
synchronized(aiInferenceLock) {
|
||||||
if (!isAiSupported) return null
|
if (!isAiSupported) return null
|
||||||
@@ -705,6 +712,42 @@ class MainActivity : AppCompatActivity() {
|
|||||||
showSavePresetDialog(preset)
|
showSavePresetDialog(preset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Touch-to-Focus: Chạm vào kính ngắm để chọn điểm lấy nét
|
||||||
|
binding.viewFinder.setOnTouchListener { v, event ->
|
||||||
|
if (event.action == android.view.MotionEvent.ACTION_UP) {
|
||||||
|
val percentX = (event.x / v.width).coerceIn(0f, 1f)
|
||||||
|
val percentY = (event.y / v.height).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
// Cập nhật Class ID lấy nét từ điểm chạm
|
||||||
|
val mask = cachedCategoryMask
|
||||||
|
val mW = cachedMaskWidth
|
||||||
|
val mH = cachedMaskHeight
|
||||||
|
if (mask != null && mW > 0 && mH > 0) {
|
||||||
|
val maskX = (percentX * mW).toInt().coerceIn(0, mW - 1)
|
||||||
|
val maskY = (percentY * mH).toInt().coerceIn(0, mH - 1)
|
||||||
|
userSelectedClassFocus = mask[maskY * mW + maskX].toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hiện vòng tròn lấy nét màu cam tại vị trí chạm
|
||||||
|
val focusRing = binding.viewFocusRing
|
||||||
|
focusRing.x = event.x - focusRing.width / 2f
|
||||||
|
focusRing.y = event.y - focusRing.height / 2f
|
||||||
|
focusRing.visibility = android.view.View.VISIBLE
|
||||||
|
focusRing.alpha = 1f
|
||||||
|
focusRing.scaleX = 1.4f
|
||||||
|
focusRing.scaleY = 1.4f
|
||||||
|
focusRing.animate()
|
||||||
|
.scaleX(1f).scaleY(1f)
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(900)
|
||||||
|
.withEndAction { focusRing.visibility = android.view.View.GONE }
|
||||||
|
.start()
|
||||||
|
|
||||||
|
v.performClick()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun translateButtonToDock(button: android.view.View) {
|
private fun translateButtonToDock(button: android.view.View) {
|
||||||
@@ -1689,12 +1732,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val w = categoryMask.width
|
val w = categoryMask.width
|
||||||
val h = categoryMask.height
|
val h = categoryMask.height
|
||||||
val bufferCapacity = categoryMaskBuffer.capacity()
|
val bufferCapacity = categoryMaskBuffer.capacity()
|
||||||
|
|
||||||
|
// Cache mask bytes for Touch-to-Focus
|
||||||
|
val rawBytes = ByteArray(bufferCapacity)
|
||||||
|
categoryMaskBuffer.rewind()
|
||||||
|
categoryMaskBuffer.get(rawBytes)
|
||||||
|
cachedCategoryMask = rawBytes
|
||||||
|
cachedMaskWidth = w
|
||||||
|
cachedMaskHeight = h
|
||||||
|
categoryMaskBuffer.rewind()
|
||||||
|
|
||||||
// Create and blur alpha mask
|
// Create and blur alpha mask with Touch-to-Focus class selection
|
||||||
|
val focusClassId = if (userSelectedClassFocus >= 0) userSelectedClassFocus else -1
|
||||||
val alphaMask = FloatArray(w * h)
|
val alphaMask = FloatArray(w * h)
|
||||||
for (i in 0 until (w * h)) {
|
for (i in 0 until (w * h)) {
|
||||||
val classId = if (i < bufferCapacity) categoryMaskBuffer.get(i).toInt() else 0
|
val classId = if (i < bufferCapacity) rawBytes[i].toInt() else 0
|
||||||
alphaMask[i] = if (classId in 1..5) 1.0f else 0.0f
|
alphaMask[i] = if (focusClassId >= 0) {
|
||||||
|
if (classId == focusClassId) 1.0f else 0.0f
|
||||||
|
} else {
|
||||||
|
if (classId in 1..5) 1.0f else 0.0f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
blurAlphaMaskBox(alphaMask, w, h, radius = 3)
|
blurAlphaMaskBox(alphaMask, w, h, radius = 3)
|
||||||
|
|
||||||
@@ -1804,11 +1861,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val h = categoryMask.height
|
val h = categoryMask.height
|
||||||
val bufferCapacity = categoryMaskBuffer.capacity()
|
val bufferCapacity = categoryMaskBuffer.capacity()
|
||||||
|
|
||||||
// Create and blur alpha mask
|
// Create and blur alpha mask with Touch-to-Focus class selection
|
||||||
|
val focusClassId = if (userSelectedClassFocus >= 0) userSelectedClassFocus else -1
|
||||||
val alphaMask = FloatArray(w * h)
|
val alphaMask = FloatArray(w * h)
|
||||||
for (i in 0 until (w * h)) {
|
for (i in 0 until (w * h)) {
|
||||||
val classId = if (i < bufferCapacity) categoryMaskBuffer.get(i).toInt() else 0
|
val classId = if (i < bufferCapacity) categoryMaskBuffer.get(i).toInt() else 0
|
||||||
alphaMask[i] = if (classId in 1..5) 1.0f else 0.0f
|
alphaMask[i] = if (focusClassId >= 0) {
|
||||||
|
if (classId == focusClassId) 1.0f else 0.0f
|
||||||
|
} else {
|
||||||
|
if (classId in 1..5) 1.0f else 0.0f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
blurAlphaMaskBox(alphaMask, w, h, radius = 5)
|
blurAlphaMaskBox(alphaMask, w, h, radius = 5)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#FFCC00" />
|
||||||
|
<solid android:color="#00000000" />
|
||||||
|
</shape>
|
||||||
@@ -143,6 +143,14 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:text="3" />
|
android:text="3" />
|
||||||
|
|
||||||
|
<!-- Vòng tròn định vị lấy nét (Focus Ring) khi chạm vào Viewfinder -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/viewFocusRing"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:background="@drawable/bg_focus_ring"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
|
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
|
||||||
|
|||||||
Reference in New Issue
Block a user