Implement 33_FIX_GRAIN.md: add FilmGrainView custom view for dynamic 60 FPS grain preview, sync intensity, and optimize Gaussian noise generation for saved photos
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
package com.photobooth.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
|
class FilmGrainView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val random = Random()
|
||||||
|
private val paint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
var intensity: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
field = value.coerceIn(0f, 1f)
|
||||||
|
if (field > 0f) {
|
||||||
|
visibility = VISIBLE
|
||||||
|
postInvalidateOnAnimation()
|
||||||
|
} else {
|
||||||
|
visibility = GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (intensity <= 0f) return
|
||||||
|
|
||||||
|
val w = width.toFloat()
|
||||||
|
val h = height.toFloat()
|
||||||
|
if (w <= 0 || h <= 0) return
|
||||||
|
|
||||||
|
// Calculate count based on width, height and intensity
|
||||||
|
val grainCount = (w * h * 0.005f * intensity).toInt().coerceIn(200, 5000)
|
||||||
|
val maxAlpha = (intensity * 110).toInt().coerceIn(1, 255)
|
||||||
|
|
||||||
|
for (i in 0 until grainCount) {
|
||||||
|
val x = random.nextFloat() * w
|
||||||
|
val y = random.nextFloat() * h
|
||||||
|
val gray = random.nextInt(55) + 100 // [100 - 155] range for cinema film grain look
|
||||||
|
val alpha = random.nextInt(maxAlpha)
|
||||||
|
|
||||||
|
paint.color = Color.argb(alpha, gray, gray, gray)
|
||||||
|
|
||||||
|
// Draw tiny rectangular grain shapes
|
||||||
|
canvas.drawRect(x, y, x + 2.5f, y + 2.5f, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repaint continuously at screen refresh rate to simulate dynamic analog noise
|
||||||
|
postInvalidateOnAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -590,11 +590,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (preset.id == "DEFAULT") {
|
if (preset.id == "DEFAULT") {
|
||||||
binding.imgFilterOverlay.visibility = android.view.View.GONE
|
binding.imgFilterOverlay.visibility = android.view.View.GONE
|
||||||
binding.viewFinder.setLayerType(android.view.View.LAYER_TYPE_NONE, null)
|
binding.viewFinder.setLayerType(android.view.View.LAYER_TYPE_NONE, null)
|
||||||
|
binding.viewFilmGrain.intensity = 0f
|
||||||
} else {
|
} else {
|
||||||
binding.imgFilterOverlay.visibility = android.view.View.VISIBLE
|
binding.imgFilterOverlay.visibility = android.view.View.VISIBLE
|
||||||
val filterColor = getFilterColorForPreset(preset)
|
val filterColor = getFilterColorForPreset(preset)
|
||||||
binding.imgFilterOverlay.setBackgroundColor(filterColor)
|
binding.imgFilterOverlay.setBackgroundColor(filterColor)
|
||||||
applyColorMatrixToViewFinder(preset)
|
applyColorMatrixToViewFinder(preset)
|
||||||
|
binding.viewFilmGrain.intensity = preset.grain.grainAmount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,7 +1110,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
EditAttribute.SHADOWS_TINT_R -> preset.toneCurve.shadowsTintR = value.coerceIn(0f, 1f)
|
EditAttribute.SHADOWS_TINT_R -> preset.toneCurve.shadowsTintR = value.coerceIn(0f, 1f)
|
||||||
EditAttribute.SHADOWS_TINT_G -> preset.toneCurve.shadowsTintG = value.coerceIn(0f, 1f)
|
EditAttribute.SHADOWS_TINT_G -> preset.toneCurve.shadowsTintG = value.coerceIn(0f, 1f)
|
||||||
EditAttribute.SHADOWS_TINT_B -> preset.toneCurve.shadowsTintB = value.coerceIn(0f, 1f)
|
EditAttribute.SHADOWS_TINT_B -> preset.toneCurve.shadowsTintB = value.coerceIn(0f, 1f)
|
||||||
EditAttribute.GRAIN_AMOUNT -> preset.grain.grainAmount = value.coerceIn(0f, 1f)
|
EditAttribute.GRAIN_AMOUNT -> {
|
||||||
|
preset.grain.grainAmount = value.coerceIn(0f, 1f)
|
||||||
|
binding.viewFilmGrain.intensity = preset.grain.grainAmount
|
||||||
|
}
|
||||||
EditAttribute.GRAIN_SIZE -> preset.grain.grainSize = value.coerceIn(0f, 1f)
|
EditAttribute.GRAIN_SIZE -> preset.grain.grainSize = value.coerceIn(0f, 1f)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
@@ -1551,40 +1556,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Film Grain effect
|
// 4. Film Grain effect (High-Quality Film Grain Noise Engine)
|
||||||
if (preset.grain.grainAmount > 0f) {
|
val grainBitmap = applyHighQualityFilmGrain(filteredBitmap, preset.grain.grainAmount, preset.grain.grainSize)
|
||||||
val grainAmount = preset.grain.grainAmount
|
|
||||||
val grainSize = preset.grain.grainSize.coerceAtLeast(0.01f)
|
|
||||||
val width = filteredBitmap.width
|
|
||||||
val height = filteredBitmap.height
|
|
||||||
val pixels = IntArray(width * height)
|
|
||||||
filteredBitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
|
||||||
|
|
||||||
val random = java.util.Random()
|
|
||||||
val sizeFactor = (grainSize * 12).toInt().coerceAtLeast(1)
|
|
||||||
|
|
||||||
for (y in 0 until height step sizeFactor) {
|
|
||||||
for (x in 0 until width step sizeFactor) {
|
|
||||||
val noise = (random.nextGaussian() * 127 * grainAmount).toInt()
|
|
||||||
for (ny in 0 until sizeFactor) {
|
|
||||||
if (y + ny >= height) break
|
|
||||||
for (nx in 0 until sizeFactor) {
|
|
||||||
if (x + nx >= width) break
|
|
||||||
val index = (y + ny) * width + (x + nx)
|
|
||||||
val color = pixels[index]
|
|
||||||
val r = (android.graphics.Color.red(color) + noise).coerceIn(0, 255)
|
|
||||||
val g = (android.graphics.Color.green(color) + noise).coerceIn(0, 255)
|
|
||||||
val b = (android.graphics.Color.blue(color) + noise).coerceIn(0, 255)
|
|
||||||
pixels[index] = android.graphics.Color.rgb(r, g, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filteredBitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Merge Instax Frame Overlay
|
// 5. Merge Instax Frame Overlay
|
||||||
var finalBitmap = filteredBitmap
|
var finalBitmap = grainBitmap
|
||||||
val framePath = currentSelectedFramePath
|
val framePath = currentSelectedFramePath
|
||||||
if (framePath != null && framePath.isNotEmpty()) {
|
if (framePath != null && framePath.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
@@ -2291,6 +2267,68 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return outputBitmap
|
return outputBitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyHighQualityFilmGrain(inputBitmap: android.graphics.Bitmap, grainIntensity: Float, grainSize: Float): android.graphics.Bitmap {
|
||||||
|
if (grainIntensity <= 0.0f) return inputBitmap
|
||||||
|
|
||||||
|
val w = inputBitmap.width
|
||||||
|
val h = inputBitmap.height
|
||||||
|
val outputBitmap = android.graphics.Bitmap.createBitmap(w, h, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
val random = java.util.Random()
|
||||||
|
|
||||||
|
// Amplify grain strength
|
||||||
|
val grainStrength = grainIntensity * 45.0f
|
||||||
|
val sizeFactor = (grainSize * 10).toInt().coerceIn(1, 20)
|
||||||
|
|
||||||
|
val pixels = IntArray(w * h)
|
||||||
|
inputBitmap.getPixels(pixels, 0, w, 0, 0, w, h)
|
||||||
|
|
||||||
|
if (sizeFactor <= 1) {
|
||||||
|
for (i in 0 until (w * h)) {
|
||||||
|
val pixel = pixels[i]
|
||||||
|
val r = (pixel shr 16) and 0xFF
|
||||||
|
val g = (pixel shr 8) and 0xFF
|
||||||
|
val b = pixel and 0xFF
|
||||||
|
|
||||||
|
val noise = (random.nextGaussian() * grainStrength).toInt()
|
||||||
|
|
||||||
|
val finalR = (r + noise).coerceIn(0, 255)
|
||||||
|
val finalG = (g + noise).coerceIn(0, 255)
|
||||||
|
val finalB = (b + noise).coerceIn(0, 255)
|
||||||
|
|
||||||
|
pixels[i] = (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply grouped sizing factor for larger vintage analog grain look
|
||||||
|
for (y in 0 until h step sizeFactor) {
|
||||||
|
for (x in 0 until w step sizeFactor) {
|
||||||
|
val noise = (random.nextGaussian() * grainStrength).toInt()
|
||||||
|
for (ny in 0 until sizeFactor) {
|
||||||
|
if (y + ny >= h) break
|
||||||
|
val rowOffset = (y + ny) * w
|
||||||
|
for (nx in 0 until sizeFactor) {
|
||||||
|
if (x + nx >= w) break
|
||||||
|
val index = rowOffset + (x + nx)
|
||||||
|
val pixel = pixels[index]
|
||||||
|
val r = (pixel shr 16) and 0xFF
|
||||||
|
val g = (pixel shr 8) and 0xFF
|
||||||
|
val b = pixel and 0xFF
|
||||||
|
|
||||||
|
val finalR = (r + noise).coerceIn(0, 255)
|
||||||
|
val finalG = (g + noise).coerceIn(0, 255)
|
||||||
|
val finalB = (b + noise).coerceIn(0, 255)
|
||||||
|
|
||||||
|
pixels[index] = (0xFF shl 24) or (finalR shl 16) or (finalG shl 8) or finalB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputBitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||||
|
inputBitmap.recycle()
|
||||||
|
return outputBitmap
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
cameraExecutor.shutdown()
|
cameraExecutor.shutdown()
|
||||||
|
|||||||
@@ -111,6 +111,13 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:contentDescription="Filter Overlay" />
|
android:contentDescription="Filter Overlay" />
|
||||||
|
|
||||||
|
<!-- Lớp phủ tạo nhiễu hạt phim thời gian thực (Dynamic Film Grain Viewfinder Overlay) -->
|
||||||
|
<com.photobooth.app.FilmGrainView
|
||||||
|
android:id="@+id/viewFilmGrain"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
|
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imgFrameOverlay"
|
android:id="@+id/imgFrameOverlay"
|
||||||
|
|||||||
Reference in New Issue
Block a user