Implement 31_FIX_UXUI.md: add BackgroundMode, skip analyzer in non-AI_BLUR to prevent lag, add highlight and shadow to ColorPreset and processAndSaveCapturedPhoto

This commit is contained in:
2026-07-06 21:32:03 +07:00
parent 7fdce924b3
commit dd4edc61ac
6 changed files with 278 additions and 18 deletions
@@ -8,7 +8,7 @@ import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
enum class EditAttribute {
BRIGHTNESS, CONTRAST, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
BRIGHTNESS, CONTRAST, HIGHLIGHT, SHADOW, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
CLARITY, DEHAZE, VIGNETTE_AMOUNT, // Nhóm 2: Nâng cao
FADED_BLACK_LEVEL, SHADOWS_TINT_R, SHADOWS_TINT_G, SHADOWS_TINT_B, // Nhóm 3: Tone Curve
GRAIN_AMOUNT, GRAIN_SIZE, // Nhóm 4: Nhiễu hạt
@@ -17,7 +17,9 @@ data class BasicAdjustments(
var saturation: Float, // -1.0f đến 1.0f
var vibrance: Float, // -1.0f đến 1.0f -> Bảo vệ màu da người
var temperature: Float, // -1.0f đến 1.0f -> Ấm (vàng) / Lạnh (xanh)
var tint: Float // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
var tint: Float, // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
var highlight: Float = 0f, // -1.0f đến 1.0f -> Mới bổ sung
var shadow: Float = 0f // -1.0f đến 1.0f -> Mới bổ sung
)
data class AdvancedEffects(
@@ -78,6 +78,23 @@ class MainActivity : AppCompatActivity() {
private var cachedMaskWidth = 0
private var cachedMaskHeight = 0
// Định nghĩa các chế độ hoạt động của ứng dụng Photobooth
enum class BackgroundMode {
DEFAULT, // Giữ nguyên ảnh gốc thô của Camera
HARDWARE_DOF, // Chỉ dùng Focus vật lý của thấu kính
AI_BLUR // Dùng AI tách nền và làm mờ phần mềm
}
private var currentBackgroundMode = BackgroundMode.DEFAULT
private fun updateBackgroundMode() {
currentBackgroundMode = when {
selectedBgAssetPath != null -> BackgroundMode.AI_BLUR
blurIntensity > 0f || isPortraitBlurModeOpen -> BackgroundMode.HARDWARE_DOF
else -> BackgroundMode.DEFAULT
}
}
private fun getImageSegmenter(): ImageSegmenter? {
synchronized(aiInferenceLock) {
if (!isAiSupported) return null
@@ -207,6 +224,8 @@ class MainActivity : AppCompatActivity() {
val brightness = extractJsonDoubleField(content, "brightness").toFloat()
val contrast = extractJsonDoubleField(content, "contrast").toFloat()
val highlight = extractJsonDoubleField(content, "highlight").toFloat()
val shadow = extractJsonDoubleField(content, "shadow").toFloat()
val saturation = extractJsonDoubleField(content, "saturation").toFloat()
val vibrance = extractJsonDoubleField(content, "vibrance").toFloat()
val temperature = extractJsonDoubleField(content, "temperature").toFloat()
@@ -229,7 +248,7 @@ class MainActivity : AppCompatActivity() {
name = name,
themeCategory = category,
isEditable = true,
basic = BasicAdjustments(brightness, contrast, saturation, vibrance, temperature, tint),
basic = BasicAdjustments(brightness, contrast, saturation, vibrance, temperature, tint, highlight, shadow),
advanced = AdvancedEffects(clarity, dehaze, vignetteAmount),
toneCurve = ToneCurveEmulation(fadedBlackLevel, shadowsTintR, shadowsTintG, shadowsTintB),
grain = FilmGrain(grainAmount, grainSize)
@@ -272,6 +291,8 @@ class MainActivity : AppCompatActivity() {
val attributeItems = listOf(
AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness),
AttributeNode(EditAttribute.CONTRAST, "Contrast (Tương phản)", R.drawable.ic_attr_contrast),
AttributeNode(EditAttribute.HIGHLIGHT, "Highlight (Vùng sáng)", R.drawable.ic_attr_highlight),
AttributeNode(EditAttribute.SHADOW, "Shadow (Vùng tối)", R.drawable.ic_attr_shadow),
AttributeNode(EditAttribute.SATURATION, "Saturation (Bão hòa màu)", R.drawable.ic_attr_saturation),
AttributeNode(EditAttribute.VIBRANCE, "Vibrance (Bão hòa thông minh)", R.drawable.ic_attr_vibrance),
AttributeNode(EditAttribute.TEMPERATURE, "Temperature (Nhiệt màu)", R.drawable.ic_attr_temperature),
@@ -305,6 +326,7 @@ class MainActivity : AppCompatActivity() {
)
backgroundOptionAdapter = BackgroundOptionAdapter(backgroundOptions) { option ->
selectedBgAssetPath = option.assetPath
updateBackgroundMode()
}
binding.rvBackgroundOptions.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
binding.rvBackgroundOptions.adapter = backgroundOptionAdapter
@@ -320,6 +342,7 @@ class MainActivity : AppCompatActivity() {
binding.panelRulerView.onValueChangeListener = { valInt ->
binding.tvPanelRulerValue.text = "$valInt"
blurIntensity = valInt / 100f
updateBackgroundMode()
}
}
@@ -454,8 +477,8 @@ class MainActivity : AppCompatActivity() {
))
finalMatrix.postConcat(contrastMatrix)
// 3. Brightness (Độ sáng Exposure thực tế)
val brightnessOffset = preset.basic.brightness * 60f
// 3. Brightness (Độ sáng Exposure thực tế, tích hợp nhè nhẹ Highlight/Shadow để người dùng nhận diện thay đổi trên Viewfinder)
val brightnessOffset = (preset.basic.brightness + (preset.basic.highlight + preset.basic.shadow) * 0.15f) * 60f
val brightnessMatrix = android.graphics.ColorMatrix(floatArrayOf(
1f, 0f, 0f, 0f, brightnessOffset,
0f, 1f, 0f, 0f, brightnessOffset,
@@ -917,11 +940,13 @@ class MainActivity : AppCompatActivity() {
// Sync ruler value
binding.panelRulerView.value = (blurIntensity * 100f).toInt()
binding.tvPanelRulerValue.text = "${(blurIntensity * 100f).toInt()}"
updateBackgroundMode()
} else {
isPortraitBlurModeOpen = false
restoreMainMenuButtons()
binding.viewLeftDockBackground.visibility = android.view.View.GONE
binding.panelRulerContainer.visibility = android.view.View.GONE
updateBackgroundMode()
}
}
@@ -948,11 +973,13 @@ class MainActivity : AppCompatActivity() {
binding.rvBackgroundOptions.visibility = android.view.View.VISIBLE
binding.rvHorizontalTimeline.visibility = android.view.View.GONE
binding.panelRulerContainer.visibility = android.view.View.GONE
updateBackgroundMode()
} else {
isBgReplaceModeOpen = false
restoreMainMenuButtons()
binding.viewLeftDockBackground.visibility = android.view.View.GONE
binding.rvBackgroundOptions.visibility = android.view.View.GONE
updateBackgroundMode()
}
}
@@ -1039,6 +1066,8 @@ class MainActivity : AppCompatActivity() {
return when (currentAttribute) {
EditAttribute.BRIGHTNESS -> preset.basic.brightness
EditAttribute.CONTRAST -> preset.basic.contrast
EditAttribute.HIGHLIGHT -> preset.basic.highlight
EditAttribute.SHADOW -> preset.basic.shadow
EditAttribute.SATURATION -> preset.basic.saturation
EditAttribute.VIBRANCE -> preset.basic.vibrance
EditAttribute.TEMPERATURE -> preset.basic.temperature
@@ -1066,6 +1095,8 @@ class MainActivity : AppCompatActivity() {
when (currentAttribute) {
EditAttribute.BRIGHTNESS -> preset.basic.brightness = value
EditAttribute.CONTRAST -> preset.basic.contrast = value
EditAttribute.HIGHLIGHT -> preset.basic.highlight = value
EditAttribute.SHADOW -> preset.basic.shadow = value
EditAttribute.SATURATION -> preset.basic.saturation = value
EditAttribute.VIBRANCE -> preset.basic.vibrance = value
EditAttribute.TEMPERATURE -> preset.basic.temperature = value
@@ -1121,7 +1152,9 @@ class MainActivity : AppCompatActivity() {
tempPreset.basic.saturation,
tempPreset.basic.vibrance,
tempPreset.basic.temperature,
tempPreset.basic.tint
tempPreset.basic.tint,
tempPreset.basic.highlight,
tempPreset.basic.shadow
),
advanced = AdvancedEffects(
tempPreset.advanced.clarity,
@@ -1150,6 +1183,8 @@ class MainActivity : AppCompatActivity() {
"basic_adjustments": {
"brightness": ${customPreset.basic.brightness},
"contrast": ${customPreset.basic.contrast},
"highlight": ${customPreset.basic.highlight},
"shadow": ${customPreset.basic.shadow},
"saturation": ${customPreset.basic.saturation},
"vibrance": ${customPreset.basic.vibrance},
"temperature": ${customPreset.basic.temperature},
@@ -1211,7 +1246,8 @@ class MainActivity : AppCompatActivity() {
.build()
imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
val isBgActive = (selectedBgAssetPath != null) || (blurIntensity > 0f)
// SỬA LỖI LAG NGẦM: Chỉ chạy MediaPipe phân đoạn AI khi thực sự cần dùng chế độ AI_BLUR
val isBgActive = currentBackgroundMode == BackgroundMode.AI_BLUR
if (isBgActive) {
val rawProxyBitmap = imageProxy.toBitmap()
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
@@ -1523,6 +1559,47 @@ class MainActivity : AppCompatActivity() {
)
}
// Apply non-linear Highlights and Shadows correction
if (preset.basic.highlight != 0f || preset.basic.shadow != 0f) {
val width = filteredBitmap.width
val height = filteredBitmap.height
val pixels = IntArray(width * height)
filteredBitmap.getPixels(pixels, 0, width, 0, 0, width, height)
val highlightVal = preset.basic.highlight
val shadowVal = preset.basic.shadow
for (i in 0 until (width * height)) {
val color = pixels[i]
var r = ((color shr 16) and 0xFF) / 255.0f
var g = ((color shr 8) and 0xFF) / 255.0f
var b = (color and 0xFF) / 255.0f
val luminance = 0.2126f * r + 0.7152f * g + 0.0722f * b
if (luminance > 0.6f) {
// Vùng sáng (Highlight): Tăng/Giảm độ sáng dựa trên thông số highlight của Preset
val weight = (luminance - 0.6f) / 0.4f
r += highlightVal * weight
g += highlightVal * weight
b += highlightVal * weight
} else if (luminance < 0.4f) {
// Vùng tối (Shadow): Bù sáng hoặc dìm tối dựa trên thông số shadow của Preset
val weight = (0.4f - luminance) / 0.4f
r += shadowVal * weight
g += shadowVal * weight
b += shadowVal * weight
}
val finalR = (r.coerceIn(0.0f, 1.0f) * 255f).toInt()
val finalG = (g.coerceIn(0.0f, 1.0f) * 255f).toInt()
val finalB = (b.coerceIn(0.0f, 1.0f) * 255f).toInt()
pixels[i] = (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB
}
filteredBitmap.setPixels(pixels, 0, width, 0, 0, width, height)
}
// 4. Film Grain effect
if (preset.grain.grainAmount > 0f) {
val grainAmount = preset.grain.grainAmount
@@ -1718,20 +1795,11 @@ class MainActivity : AppCompatActivity() {
capturedImage: android.graphics.Bitmap,
isStreamMode: Boolean = false
): android.graphics.Bitmap {
val isBgActive = (selectedBgAssetPath != null) || (blurIntensity > 0f)
if (!isBgActive) {
// Chỉ chạy các thuật toán xử lý phân đoạn AI hoặc mờ phần mềm khi bật chế độ AI_BLUR
if (currentBackgroundMode != BackgroundMode.AI_BLUR) {
return capturedImage
}
// Tắt hoàn toàn thuật toán xử lý mờ/trộn nhân tạo cho Portrait Blur (khi selectedBgAssetPath == null)
// và trả về ảnh thô 100% để sử dụng tiêu cự thấu kính vật lý của camera.
if (selectedBgAssetPath == null && blurIntensity > 0f) {
if (!isStreamMode) {
// Trong chế độ chụp ảnh: Trả về ảnh thô ngay lập tức, không chạy phân đoạn AI
return capturedImage
}
}
if (!isAiSupported) {
// Khi không hỗ trợ AI và chỉ cần Portrait Blur, trả về ảnh thô để dùng thấu kính vật lý
if (selectedBgAssetPath == null && blurIntensity > 0f) {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8v16z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.41 3.59,-8 8,-8v16C7.59,20 4,16.41 4,12z" />
</vector>