From 490def69dca69a45e9c89ea4886790d44b6f288a Mon Sep 17 00:00:00 2001 From: Gian PG Date: Thu, 10 Apr 2025 14:01:15 +0700 Subject: [PATCH 1/6] nyoba --- Backend/mfa_presensi.db | Bin 73728 -> 73728 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Backend/mfa_presensi.db b/Backend/mfa_presensi.db index 4b7b06c7c11b439e5b820ea26504fe3875020005..b2af81e7e6660f5c9d9ecd0bf90c3af0eefcc1af 100644 GIT binary patch delta 39 vcmZoTz|wGlWr8$g$wV1v#*&Q*ocsAyckMVQ}sdBu@`; delta 39 vcmZoTz|wGlWr8$g@kAMC#^Q|$ocsCIm_xr_z3mV^!{bzm&d<%|`#Tc=E!z;r From 50d798aea3a34476849b80e0cd306206b768d529 Mon Sep 17 00:00:00 2001 From: Gian PG Date: Thu, 12 Jun 2025 14:29:57 +0700 Subject: [PATCH 2/6] nyoba --- .../java/com/mfa/facedetector/BlurDetector.kt | 79 +++++++++++++++++++ .../view/activity/FaceProcessorActivity.kt | 21 +++-- 2 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt diff --git a/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt new file mode 100644 index 0000000..ba2fd46 --- /dev/null +++ b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt @@ -0,0 +1,79 @@ +package com.mfa.facedetector + +import android.graphics.Bitmap +import com.mfa.preprocessor.PreprocessingUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +class BlurDetector { + fun isBlurry(pixels: Array): Boolean { + val threshold = 1.2 + val variance = calculateLaplaceScore(pixels) + return variance < threshold + } + + fun isBlurry(bitmap: Bitmap) : Boolean { + val p = PreprocessingUtils() + val array = p.convertRawGreyImg(bitmap) + return isBlurry(array) + } + + private fun calculateLaplaceScore(pixels: Array): Double { + val p = PreprocessingUtils() + var laplaceKernel : FloatArray = arrayOf( + 1f, 1f, 1f, + 1f, -8f, 1f, + 1f, 1f, 1f + ).toFloatArray(); + var newPixels : Array + val gaussianKernel = p.generateGaussianKernel(9, 1.5f); + newPixels = p.convolve(pixels, gaussianKernel, 9) + newPixels = p.convolve(newPixels, laplaceKernel, 3) + + val mean = calculateAverageBrightness(newPixels) + val variance = calculateVariance(newPixels) + return Math.sqrt(variance) / mean; + } + + fun calculateVariance(array: Array): Double = runBlocking { + val rows = array.size + val cols = array[0].size + val totalElements = rows * cols + + if (totalElements == 0) return@runBlocking 0.0 + val sum = withContext(Dispatchers.Default) { + array.map { row -> + async { row.sum() } + }.awaitAll().sum() + } + val mean = sum.toDouble() / totalElements + val sumSquaredDifferences = withContext(Dispatchers.Default) { + array.map { row -> + async { + row.sumOf { value -> + val diff = value - mean + diff * diff + } + } + }.awaitAll().sum() + } + sumSquaredDifferences / totalElements + } + + + private fun calculateAverageBrightness(pixels: Array): Double { + if (pixels.isEmpty() || pixels[0].isEmpty()) return 0.0 // Edge case + var totalBrightness = 0.0 + val width = pixels.size + val height = pixels[0].size + for (x in 0 until width) { + for (y in 0 until height) { + totalBrightness += pixels[x][y] // Clamp to valid range + } + } + return totalBrightness / (width * height) + } +} \ No newline at end of file diff --git a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt index 7221f4c..f1b3172 100644 --- a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt +++ b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt @@ -47,6 +47,7 @@ import com.mfa.camerax.CameraManager import com.mfa.databinding.ActivityCaptureFaceBinding import com.mfa.databinding.DialogAddFaceBinding import com.mfa.di.Injection +import com.mfa.facedetector.BlurDetector import com.mfa.facedetector.EkspresiRecognizer import com.mfa.facedetector.FaceAntiSpoofing import com.mfa.facedetector.FaceRecognizer @@ -98,7 +99,7 @@ class FaceProcessorActivity : AppCompatActivity() { private val selectedExpressions = allExpressions.shuffled().take(5).toMutableList() private var currentIndex = 0 private fun startExpressionChallenge() { - if (currentIndex < selectedExpressions.size) { + if (currentIndex < selectedExpressions.size && false) { val currentExpression = selectedExpressions[currentIndex] Log.d("FaceProcessor", "Mulai tantangan ekspresi: $currentExpression") // πŸ”₯ Log ekspresi binding.expressionCommandText.text = "Yuk coba berekspresi: $currentExpression" @@ -251,7 +252,6 @@ class FaceProcessorActivity : AppCompatActivity() { } } - val handler = Handler(Looper.getMainLooper()) fun stop_handler(){ //handler.removeCallbacks(runnableDetectionHandler) @@ -303,11 +303,15 @@ class FaceProcessorActivity : AppCompatActivity() { //usage because photo cannot make an expression private fun antiSpoofDetection(faceBitmap: Bitmap): Boolean { val laplaceScore: Int = fas.laplacian(faceBitmap) - if (laplaceScore < FaceAntiSpoofing.LAPLACIAN_THRESHOLD) { + val blurDetector = BlurDetector() +// if (laplaceScore < FaceAntiSpoofing.LAPLACIAN_THRESHOLD) { +// Toast.makeText(this, "Image too blurry!", Toast.LENGTH_LONG).show() +// return false +// } + if (blurDetector.isBlurry(faceBitmap)) { Toast.makeText(this, "Image too blurry!", Toast.LENGTH_LONG).show() return false } - val start = System.currentTimeMillis() val score = fas.antiSpoofing(faceBitmap) val end = System.currentTimeMillis() @@ -353,9 +357,6 @@ class FaceProcessorActivity : AppCompatActivity() { } } - - - private fun handleFifthExpressionMatch() { Log.d("FaceVerification", "πŸŽ‰ Ekspresi ke-5 cocok! Menyiapkan capture...") binding.expressionCommandText.text = "Pemanasan selesai" @@ -448,6 +449,7 @@ class FaceProcessorActivity : AppCompatActivity() { } var processedBitmap = bitmap + Log.d("FaceVerification", "Memproses gambar untuk ekstraksi embedding: ${processedBitmap.width}x${processedBitmap.height}") // Flip jika menggunakan kamera depan if (CameraManager.cameraOption == CameraSelector.LENS_FACING_FRONT) { @@ -465,6 +467,11 @@ class FaceProcessorActivity : AppCompatActivity() { return } + if (antiSpoofDetection(bitmap)) { + Log.e("Anti Spoofing", "Spoofing Gagal") + return + } + try { // Log sebelum ekstraksi embedding Log.d("FaceVerification", "Menyiapkan untuk ekstraksi embedding wajah... Gambar ukuran: ${bitmap.width}x${bitmap.height}") From 6934b2de61e9640c73b8281e69f6d35faebd5fef Mon Sep 17 00:00:00 2001 From: Gian PG Date: Thu, 12 Jun 2025 16:55:43 +0700 Subject: [PATCH 3/6] testing --- .../app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt | 3 ++- .../app/src/main/java/com/mfa/facedetector/BlurDetector.kt | 4 +++- .../main/java/com/mfa/view/activity/FaceProcessorActivity.kt | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt b/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt index 802eac7..9acbfaa 100644 --- a/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt +++ b/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt @@ -16,7 +16,8 @@ object ApiConfig { .build() val retrofit = Retrofit.Builder() // .baseUrl("http://192.168.0.107:3000/") - .baseUrl("http://192.168.0.244:3000/") +// .baseUrl("http://192.168.0.244:3000/") + .baseUrl("http://10.0.10.124:3000/") .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() diff --git a/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt index ba2fd46..301527e 100644 --- a/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt +++ b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt @@ -1,6 +1,7 @@ package com.mfa.facedetector import android.graphics.Bitmap +import android.util.Log import com.mfa.preprocessor.PreprocessingUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -10,8 +11,9 @@ import kotlinx.coroutines.withContext class BlurDetector { fun isBlurry(pixels: Array): Boolean { - val threshold = 1.2 + val threshold = 1.0 val variance = calculateLaplaceScore(pixels) + Log.d("Is BLUR", "$variance") return variance < threshold } diff --git a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt index f1b3172..032a338 100644 --- a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt +++ b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt @@ -316,7 +316,7 @@ class FaceProcessorActivity : AppCompatActivity() { val score = fas.antiSpoofing(faceBitmap) val end = System.currentTimeMillis() Log.d(TAG, "Spoof detection process time: ${end - start} ms") - + Log.d("Anti Spoof Score", "$score") return score < FaceAntiSpoofing.THRESHOLD } @@ -467,7 +467,7 @@ class FaceProcessorActivity : AppCompatActivity() { return } - if (antiSpoofDetection(bitmap)) { + if (!antiSpoofDetection(bitmap)) { Log.e("Anti Spoofing", "Spoofing Gagal") return } From 5ddb3307f679a66f25ea4b571a696ac77e17301e Mon Sep 17 00:00:00 2001 From: Gian PG Date: Fri, 13 Jun 2025 11:14:04 +0700 Subject: [PATCH 4/6] Fix Convolution: Add 101 padding --- .../java/com/mfa/api/retrofit/ApiConfig.kt | 2 +- .../java/com/mfa/preprocessor/Convolution.kt | 55 +++++++++++++++---- .../view/activity/FaceProcessorActivity.kt | 7 ++- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt b/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt index 9acbfaa..0f12aa2 100644 --- a/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt +++ b/AndroidApps/app/src/main/java/com/mfa/api/retrofit/ApiConfig.kt @@ -17,7 +17,7 @@ object ApiConfig { val retrofit = Retrofit.Builder() // .baseUrl("http://192.168.0.107:3000/") // .baseUrl("http://192.168.0.244:3000/") - .baseUrl("http://10.0.10.124:3000/") + .baseUrl("http://10.0.4.144:3000/") .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() diff --git a/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt b/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt index fa38a00..ea0ee80 100644 --- a/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt +++ b/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt @@ -4,6 +4,36 @@ import kotlinx.coroutines.* import kotlin.math.abs class Convolution { + + fun padWithReplicate(input: Array, padY: Int, padX: Int): Array { + val inputRows = input.size + val inputCols = input[0].size + val paddedRows = inputRows + 2 * padY + val paddedCols = inputCols + 2 * padX + + val padded = Array(paddedRows) { IntArray(paddedCols) } + + for (i in 0 until paddedRows) { + for (j in 0 until paddedCols) { + val srcI = when { + i < padY -> 0 + i >= padY + inputRows -> inputRows - 1 + else -> i - padY + } + + val srcJ = when { + j < padX -> 0 + j >= padX + inputCols -> inputCols - 1 + else -> j - padX + } + + padded[i][j] = input[srcI][srcJ] + } + } + + return padded + } + fun convolve( input: Array, kernel: FloatArray, @@ -12,27 +42,32 @@ class Convolution { ): Array = runBlocking { val inputRows = input.size val inputCols = input[0].size - val outputRows = inputRows - kernelRows + 1 - val outputCols = inputCols - kernelCols + 1 - val output = Array(outputRows) { IntArray(outputCols) } - // Use coroutines for actual parallelism + val padY = kernelRows / 2 + val padX = kernelCols / 2 + + val paddedInput = padWithReplicate(input, padY, padX) + val output = Array(inputRows) { IntArray(inputCols) } + coroutineScope { - (0 until outputRows).map { i -> - launch(Dispatchers.Default) { // Launch parallel coroutines - for (j in 0 until outputCols) { + (0 until inputRows).map { i -> + launch(Dispatchers.Default) { + for (j in 0 until inputCols) { var sum = 0f for (ki in 0 until kernelRows) { for (kj in 0 until kernelCols) { - sum += input[i + ki][j + kj] * kernel[ki * kernelCols + kj] + val pi = i + ki + val pj = j + kj + sum += paddedInput[pi][pj] * kernel[ki * kernelCols + kj] } } - output[i][j] = abs(sum.toInt()).coerceIn(0, 255) // More readable + output[i][j] = abs(sum.toInt()).coerceIn(0, 255) } } - }.joinAll() // Wait for all coroutines to finish + }.joinAll() } output } + } diff --git a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt index 032a338..ce014f6 100644 --- a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt +++ b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt @@ -99,7 +99,8 @@ class FaceProcessorActivity : AppCompatActivity() { private val selectedExpressions = allExpressions.shuffled().take(5).toMutableList() private var currentIndex = 0 private fun startExpressionChallenge() { - if (currentIndex < selectedExpressions.size && false) { +// currentIndex < selectedExpressions.size && + if (false) { val currentExpression = selectedExpressions[currentIndex] Log.d("FaceProcessor", "Mulai tantangan ekspresi: $currentExpression") // πŸ”₯ Log ekspresi binding.expressionCommandText.text = "Yuk coba berekspresi: $currentExpression" @@ -210,8 +211,8 @@ class FaceProcessorActivity : AppCompatActivity() { askCameraPermission() // **Pastikan ekspresi pertama muncul** - startExpressionChallenge() // πŸ”₯ Tambahkan ini agar perintah pertama muncul - +// startExpressionChallenge() // πŸ”₯ Tambahkan ini agar perintah pertama muncul + startFaceVerification() // onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { // override fun handleOnBackPressed() { // showCustomDialog( From f9464f809f62a0abb76266db0249c54be99408db Mon Sep 17 00:00:00 2001 From: Gian PG Date: Thu, 10 Jul 2025 14:24:46 +0700 Subject: [PATCH 5/6] Add Blur Detector and Refactor Gaussian Filter --- .../java/com/mfa/facedetector/BlurDetector.kt | 4 +- .../java/com/mfa/preprocessor/Convolution.kt | 74 ++- .../mfa/preprocessor/PreprocessingUtils.kt | 473 ++++++++++++++---- 3 files changed, 443 insertions(+), 108 deletions(-) diff --git a/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt index 301527e..ba2fd46 100644 --- a/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt +++ b/AndroidApps/app/src/main/java/com/mfa/facedetector/BlurDetector.kt @@ -1,7 +1,6 @@ package com.mfa.facedetector import android.graphics.Bitmap -import android.util.Log import com.mfa.preprocessor.PreprocessingUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -11,9 +10,8 @@ import kotlinx.coroutines.withContext class BlurDetector { fun isBlurry(pixels: Array): Boolean { - val threshold = 1.0 + val threshold = 1.2 val variance = calculateLaplaceScore(pixels) - Log.d("Is BLUR", "$variance") return variance < threshold } diff --git a/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt b/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt index ea0ee80..1d28ddb 100644 --- a/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt +++ b/AndroidApps/app/src/main/java/com/mfa/preprocessor/Convolution.kt @@ -1,10 +1,11 @@ package com.mfa.preprocessor +import android.graphics.Bitmap +import android.graphics.Color import kotlinx.coroutines.* import kotlin.math.abs class Convolution { - fun padWithReplicate(input: Array, padY: Int, padX: Int): Array { val inputRows = input.size val inputCols = input[0].size @@ -70,4 +71,75 @@ class Convolution { output } + fun convolveBitmap( + input: Bitmap, + kernel: FloatArray, + kernelRows: Int, + kernelCols: Int + ): Bitmap = runBlocking { + val width = input.width + val height = input.height + val outputWidth = width - kernelCols + 1 + val outputHeight = height - kernelRows + 1 + val rChannel = Array(height) { IntArray(width) } + val gChannel = Array(height) { IntArray(width) } + val bChannel = Array(height) { IntArray(width) } + + for (y in 0 until height) { + for (x in 0 until width) { + val color = input.getPixel(x, y) + rChannel[y][x] = Color.red(color) + gChannel[y][x] = Color.green(color) + bChannel[y][x] = Color.blue(color) + } + } + + val channels = arrayOf(rChannel, gChannel, bChannel) + val outputChannels = Array(3) { Array(outputHeight) { IntArray(outputWidth) } } + + coroutineScope { + for (c in 0 until 3) { + (0 until outputHeight).map { i -> + launch(Dispatchers.Default) { + for (j in 0 until outputWidth) { + var sum = 0f + for (ki in 0 until kernelRows) { + for (kj in 0 until kernelCols) { + sum += channels[c][i + ki][j + kj] * kernel[ki * kernelCols + kj] + } + } + outputChannels[c][i][j] = abs(sum.toInt()).coerceIn(0, 255) + } + } + }.joinAll() + } + } + + // Combine RGB back into bitmap + val outputBitmap = Bitmap.createBitmap(outputWidth, outputHeight, Bitmap.Config.ARGB_8888) + for (i in 0 until outputHeight) { + for (j in 0 until outputWidth) { + val r = outputChannels[0][i][j] + val g = outputChannels[1][i][j] + val b = outputChannels[2][i][j] + val color = Color.rgb(r, g, b) + outputBitmap.setPixel(j, i, color) + } + } + + outputBitmap + } + + fun convolveBitmapGrayScale( + input: Bitmap, + kernel: FloatArray, + kernelRows: Int, + kernelCols: Int + ): Bitmap { + val p = PreprocessingUtils() + val inputArray = p.bitmapToGrayIntArray(input); + val outputArray = convolve(inputArray, kernel, kernelRows, kernelCols); + val out = p.grayIntArrayToBitmap(outputArray) + return out; + } } diff --git a/AndroidApps/app/src/main/java/com/mfa/preprocessor/PreprocessingUtils.kt b/AndroidApps/app/src/main/java/com/mfa/preprocessor/PreprocessingUtils.kt index 9d66666..e29b476 100644 --- a/AndroidApps/app/src/main/java/com/mfa/preprocessor/PreprocessingUtils.kt +++ b/AndroidApps/app/src/main/java/com/mfa/preprocessor/PreprocessingUtils.kt @@ -1,24 +1,66 @@ package com.mfa.preprocessor +import android.content.ContentValues +import android.content.Context import android.graphics.Bitmap -import android.widget.Toast +import android.graphics.BitmapFactory +import android.graphics.Color +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import java.util.* +import java.io.File +import java.io.IOException +import java.util.Arrays +import kotlin.math.exp +import kotlin.math.pow + class PreprocessingUtils { private val conv : Convolution = Convolution(); + companion object { + lateinit var appContext: Context + } private fun gaussian(x: Int, y: Int, sigma: Double): Double { require(!(sigma <= 0)) { "Sigma must be positive." } val squaredDistance = (x * x + y * y).toDouble() val denominator = 2 * sigma * sigma val exponent = -squaredDistance / denominator - return 1 / (2 * Math.PI * sigma * sigma) * Math.exp(exponent) +// return 1 / (2 * Math.PI * sigma * sigma) * exp(exponent) + return Math.exp(exponent) } + fun medianFilter(pixels: Array, ksize: Int) : Array{ + val width = pixels.size + val height = pixels[0].size + val out = arrayOfNulls(pixels.size) + for (i in pixels.indices) { + out[i] = Arrays.copyOf(pixels[i], pixels[i].size) + } + val kernel = FloatArray(ksize * ksize) + val half_k = ksize / 2 + for (i in half_k until width - half_k) { + for (j in half_k until height - half_k) { + for (ki in kernel.indices) { + val kx = ki % ksize + val ky = ki / ksize + val offset_x = kx - half_k + val offset_y = ky - half_k + kernel[ki] = pixels[i + offset_y][j + offset_x].toFloat() + } + kernel.sort() + out[i]!![j] = kernel[ksize*ksize / 2].toInt(); + } + } + return out.requireNoNulls(); + } + + fun calculateVariance(array: Array): Double = runBlocking { val rows = array.size val cols = array[0].size @@ -45,137 +87,105 @@ class PreprocessingUtils { } }.awaitAll().sum() // Combine results from all coroutines } - // Step 3: Divide by the number of elements to get the variance sumSquaredDifferences / totalElements } - fun histogramEqualization(image: Array, width: Int, height: Int): Array { - val histogram = IntArray(256) // Histogram for pixel values 0 to 255 - val cdf = IntArray(256) // Cumulative Distribution Function - val equalizedImage = Array(height) { IntArray(width) } - - // Calculate the histogram - for (y in 0 until height) { - for (x in 0 until width) { - // Clamp the pixel value between 0 and 255 - val pixelValue = image[y][x].coerceIn(0, 255) - histogram[pixelValue]++ + fun generateGaussianKernel(size: Int, sigma: Float): FloatArray { + val kernel = FloatArray(size * size) + val mean = size / 2 + var sum = 0f + + for (x in 0 until size) { + for (y in 0 until size) { + val dx = x - mean + val dy = y - mean + println("dx=$dx dy=$dy") + val value = gaussian(dx, dy, sigma.toDouble()) + kernel[y * size + x] = value.toFloat() + sum += value.toFloat() } } - - // Calculate the CDF (Cumulative Distribution Function) - cdf[0] = histogram[0] - for (i in 1..255) { - cdf[i] = cdf[i - 1] + histogram[i] - } - - val cdfMin = cdf[0] - val totalPixels = width * height - val equalizationMap = IntArray(256) - for (i in 0..255) { - equalizationMap[i] = Math.round((cdf[i] - cdfMin) / (totalPixels - cdfMin).toFloat() * 255) - if (equalizationMap[i] < 0) equalizationMap[i] = 0 - if (equalizationMap[i] > 255) equalizationMap[i] = 255 - } - - // Apply the equalization map to get the equalized image - for (y in 0 until height) { - for (x in 0 until width) { - val pixelValue = image[y][x].coerceIn(0, 255) - equalizedImage[y][x] = equalizationMap[pixelValue] - } + // Normalize the kernel + for (i in kernel.indices) { + kernel[i] /= sum } - return equalizedImage + return kernel } - - fun generateGaussianKernel(size: Int, sigma: Float): FloatArray { - val arr = Array(size * size) { FloatArray(2) } - var x = 0f - var y = 0f - val offset = size / 2 - for (i in 0 until size * size) { - arr[i] = floatArrayOf(x - offset, y - offset) - y++ - if (y >= size) { - y = 0f - x++ - } - } - val doubleResult = Arrays.stream(arr) - .mapToDouble { n: FloatArray -> gaussian(n[0].toInt(), offset, sigma.toDouble()).toFloat().toDouble() } - .toArray() - val sum = Arrays.stream(doubleResult).sum() - for (i in doubleResult.indices) { - doubleResult[i] /= sum - } - val result = FloatArray(doubleResult.size) - for (i in result.indices) { - result[i] = doubleResult[i].toFloat() - } - return result + fun convertBitmapToGray(bitmap : Bitmap) : Bitmap { + val grayIntArray = convertRawGreyImg(bitmap) + val grayBitmap = convertArrayToBitmap(grayIntArray) + return grayBitmap } fun convolve(pixels: Array, kernel: FloatArray, ksize: Int): Array { return conv.convolve(pixels, kernel, ksize, ksize); } + fun convolveGrayscale(bitmap : Bitmap, kernel: FloatArray, ksize: Int): Bitmap { + return conv.convolveBitmapGrayScale(bitmap, kernel, ksize, ksize); + } - fun isBlurry(pixels: Array): Boolean { - val threshold = 500; - val gaussianKernel = generateGaussianKernel(3, 3f/6f); - val lKernel2 : FloatArray = arrayOf( - 1f, 1f, 1f, - 1f, -8f, 1f, - 1f, 1f, 1f - ).toFloatArray(); - var newPixels : Array; - val eqPixels = histogramEqualization(pixels, pixels[0].size, pixels.size); - newPixels = convolve(eqPixels, gaussianKernel, 3); - newPixels = convolve(newPixels, lKernel2, 3); - val variance = calculateVariance(newPixels); - println(variance); -// Toast.makeText(this, "${variance}", Toast.LENGTH_SHORT).show(); - return variance < threshold; - } - - fun isBlurryD(pixels: Array): Double { - val threshold = 500; - val gaussianKernel = generateGaussianKernel(3, 3f/6f); - val lKernel2 : FloatArray = arrayOf( + fun convolveRGB(bitmap: Bitmap, kernel: FloatArray, ksize: Int) : Bitmap { + return conv.convolveBitmap(bitmap, kernel, ksize, ksize) + } + + fun isBlurry(pixels: Array): Boolean { + val threshold = 1.2 + val variance = isBlurryD(pixels) + return variance < threshold + } + + fun isBlurryD(pixels: Array, kernelSize: Int = 0 ): Double { + var lKernel2 : FloatArray = arrayOf( 1f, 1f, 1f, 1f, -8f, 1f, 1f, 1f, 1f ).toFloatArray(); var newPixels : Array; - val eqPixels = histogramEqualization(pixels, pixels[0].size, pixels.size); - newPixels = convolve(eqPixels, gaussianKernel, 3); - newPixels = convolve(newPixels, lKernel2, 3); - val variance = calculateVariance(newPixels); - println(variance); -// Toast.makeText(this, "${variance}", Toast.LENGTH_SHORT).show(); - return variance; + if (kernelSize > 0) { + val gaussianKernel = generateGaussianKernel(kernelSize, kernelSize.toFloat()/6f); + newPixels = convolve(pixels, gaussianKernel, 5) + newPixels = medianFilter(newPixels, 7) + newPixels = convolve(newPixels, lKernel2, 3) + } else { + newPixels = convolve(pixels, lKernel2, 3) + } + val mean = calculateAverageBrightness(newPixels) + val variance = calculateVariance(newPixels) + return Math.sqrt(variance) / mean; + } + + + fun calculateAverageBrightness(pixels: Array): Double { + if (pixels.isEmpty() || pixels[0].isEmpty()) return 0.0 // Edge case + var totalBrightness = 0.0 + val width = pixels.size + val height = pixels[0].size + for (x in 0 until width) { + for (y in 0 until height) { + totalBrightness += pixels[x][y] // Clamp to valid range + } + } + return totalBrightness / (width * height) } fun convertRawGreyImg(bitmap: Bitmap): Array { val w = bitmap.width val h = bitmap.height - - val pixels = IntArray(h * w) - bitmap.getPixels(pixels, 0, w, 0, 0, w, h) - + val pixels = IntArray(w * h) val result = Array(h) { IntArray(w) } + bitmap.getPixels(pixels, 0, w, 0, 0, w, h) for (i in 0 until h) { for (j in 0 until w) { val data = pixels[w * i + j] - val red = ((data shr 16) and 0xFF) val green = ((data shr 8) and 0xFF) val blue = (data and 0xFF) - var grey = (red.toFloat() * 0.3 + green.toFloat() * 0.59 + blue.toFloat() * 0.11).toInt() + var grey = (red.toFloat() * 0.299 + green.toFloat() * 0.587 + blue.toFloat() * 0.114).toInt() result[i][j] = grey } } @@ -185,7 +195,6 @@ class PreprocessingUtils { fun convertGreyImg(bitmap: Bitmap): Array { val w = bitmap.width val h = bitmap.height - val pixels = IntArray(h * w) bitmap.getPixels(pixels, 0, w, 0, 0, w, h) @@ -210,21 +219,277 @@ class PreprocessingUtils { fun convertArrayToBitmap(pixelArray: Array): Bitmap { val height = pixelArray.size val width = pixelArray[0].size - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val pixels = IntArray(width * height) - for (y in pixelArray.indices) { for (x in pixelArray[0].indices) { - val gray = pixelArray[y][x].coerceIn(0, 255) // Ensure valid grayscale range + val gray = pixelArray[y][x].coerceIn(0, 255) pixels[y * width + x] = (0xFF shl 24) or (gray shl 16) or (gray shl 8) or gray } } - bitmap.setPixels(pixels, 0, width, 0, 0, width, height) return bitmap } + fun saveBitmapToPicturesSubfolder( + bitmap: Bitmap, + fileName: String, + subfolder: String // e.g., "sharp" or "blur" + ): Boolean { + val context = appContext + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, "$fileName.png") + put(MediaStore.Images.Media.MIME_TYPE, "image/png") + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/$subfolder") + put(MediaStore.Images.Media.IS_PENDING, 1) + } + + val contentResolver = context.contentResolver + val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + return try { + imageUri?.let { uri -> + contentResolver.openOutputStream(uri).use { outputStream -> + if (outputStream != null) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + } + // Mark as not pending (available to gallery) + contentValues.clear() + contentValues.put(MediaStore.Images.Media.IS_PENDING, 0) + contentResolver.update(uri, contentValues, null, null) + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + fun medianFilterRGB(bitmap: Bitmap, kernelSize: Int): Bitmap { + val width = bitmap.width + val height = bitmap.height + val resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + // Get pixels of the original bitmap + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + // The median filter process + val halfKernelSize = kernelSize / 2 + for (y in halfKernelSize until height - halfKernelSize) { + for (x in halfKernelSize until width - halfKernelSize) { + val rValues = mutableListOf() + val gValues = mutableListOf() + val bValues = mutableListOf() + + // Collect pixels from the neighborhood + for (ky in -halfKernelSize..halfKernelSize) { + for (kx in -halfKernelSize..halfKernelSize) { + val px = pixels[(y + ky) * width + (x + kx)] + val r = Color.red(px) + val g = Color.green(px) + val b = Color.blue(px) + + rValues.add(r) + gValues.add(g) + bValues.add(b) + } + } + + // Sort values and get the median + rValues.sort() + gValues.sort() + bValues.sort() + + val medianR = rValues[rValues.size / 2] + val medianG = gValues[gValues.size / 2] + val medianB = bValues[bValues.size / 2] + + // Set the new pixel value for the filtered image + val newPixel = Color.rgb(medianR, medianG, medianB) + resultBitmap.setPixel(x, y, newPixel) + } + } + + return resultBitmap + } + fun bilateralRGB(bitmap: Bitmap, diameter: Int, sigmaColor: Double, sigmaSpace: Double): Bitmap { + val width = bitmap.width + val height = bitmap.height + val resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + // Get pixels of the original bitmap + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + // Define the kernel size (diameter of the filter) + val halfDiameter = diameter / 2 + val gaussianSpace = Array(diameter) { DoubleArray(diameter) } + + // Precompute spatial Gaussian kernel (based on sigmaSpace) + for (y in -halfDiameter..halfDiameter) { + for (x in -halfDiameter..halfDiameter) { + val distance = (x * x + y * y).toDouble() + gaussianSpace[y + halfDiameter][x + halfDiameter] = exp(-distance / (2 * sigmaSpace.pow(2))) + } + } + + // Apply bilateral filter + for (y in halfDiameter until height - halfDiameter) { + for (x in halfDiameter until width - halfDiameter) { + var rSum = 0.0 + var gSum = 0.0 + var bSum = 0.0 + var weightSum = 0.0 + + for (dy in -halfDiameter..halfDiameter) { + for (dx in -halfDiameter..halfDiameter) { + val pixel = pixels[(y + dy) * width + (x + dx)] + val r = Color.red(pixel) + val g = Color.green(pixel) + val b = Color.blue(pixel) + + // Calculate color similarity using Gaussian function + val colorDistance = (r - Color.red(pixels[y * width + x])).toDouble().pow(2) + + (g - Color.green(pixels[y * width + x])).toDouble().pow(2) + + (b - Color.blue(pixels[y * width + x])).toDouble().pow(2) + + val gaussianColor = exp(-colorDistance / (2 * sigmaColor.pow(2))) + + // Calculate the weight using spatial and color Gaussian functions + val weight = gaussianSpace[dy + halfDiameter][dx + halfDiameter] * gaussianColor + rSum += r * weight + gSum += g * weight + bSum += b * weight + weightSum += weight + } + } + + // Set the new pixel value based on weighted sum + val newR = (rSum / weightSum).toInt().coerceIn(0, 255) + val newG = (gSum / weightSum).toInt().coerceIn(0, 255) + val newB = (bSum / weightSum).toInt().coerceIn(0, 255) + + val newPixel = Color.rgb(newR, newG, newB) + resultBitmap.setPixel(x, y, newPixel) + } + } + + return resultBitmap + } + + fun writeLog(fileName: String, logText: String): Boolean { + val file = File(Environment.getExternalStorageDirectory(), "Documents/logs/$fileName") + return try { + // Create directories if they don't exist + file.parentFile?.mkdirs() + + // Append the text to the file + file.appendText(logText + "\n") + true + } catch (e: IOException) { + e.printStackTrace() + false + } + } + + fun loadBitmapsFromLuxSharp(): List { + val sharpDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "lux_90-100/sharp" + ) + + if (!sharpDir.exists() || !sharpDir.isDirectory) return emptyList() + + val imageExtensions = listOf("jpg", "jpeg", "png", "webp") + + return sharpDir.listFiles { file -> + file.isFile && file.extension.lowercase() in imageExtensions + }?.mapNotNull { file -> + BitmapFactory.decodeFile(file.absolutePath)?.also { + Log.d("BitmapLoad", "Loaded: ${file.name}") + } + } ?: emptyList() + } + + + fun loadBitmapsFromMediaStore(): List { + val context = appContext + val bitmaps = mutableListOf() + + val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.RELATIVE_PATH + ) + + val selection = "${MediaStore.Images.Media.RELATIVE_PATH} = ?" + val selectionArgs = arrayOf("Pictures/lux_90-100/sharp/") + + val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" + + context.contentResolver.query( + collection, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val uri = Uri.withAppendedPath(collection, id.toString()) + + try { + val inputStream = context.contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream?.close() + + bitmap?.let { bitmaps.add(it) } + } catch (e: Exception) { + Log.e("MediaStoreLoader", "Error loading bitmap: $uri", e) + } + } + } + + return bitmaps + } + + fun bitmapToGrayIntArray(bitmap: Bitmap): Array { + val width = bitmap.width + val height = bitmap.height + val result = Array(height) { IntArray(width) } + + for (y in 0 until height) { + for (x in 0 until width) { + val pixel = bitmap.getPixel(x, y) + val gray = Color.red(pixel) // or green/blue, since they’re equal in grayscale + result[y][x] = gray + } + } + + return result + } + + fun grayIntArrayToBitmap(data: Array): Bitmap { + val height = data.size + val width = data[0].size + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + for (y in 0 until height) { + for (x in 0 until width) { + val gray = data[y][x].coerceIn(0, 255) + val color = Color.rgb(gray, gray, gray) + bitmap.setPixel(x, y, color) + } + } + + return bitmap + } + } \ No newline at end of file From 1903b786f0adcd8988ce7c556c30b7c37ce3e43d Mon Sep 17 00:00:00 2001 From: Gian PG Date: Thu, 10 Jul 2025 14:30:17 +0700 Subject: [PATCH 6/6] Apply Blur Detection --- .../main/java/com/mfa/view/activity/FaceProcessorActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt index ce014f6..cad3231 100644 --- a/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt +++ b/AndroidApps/app/src/main/java/com/mfa/view/activity/FaceProcessorActivity.kt @@ -565,13 +565,16 @@ class FaceProcessorActivity : AppCompatActivity() { // Proses gambar (crop + konversi ke grayscale) processCapturedImage(bitmap) { processedBitmap -> verify_counter++ - if (verify_counter < 5) { Handler(Looper.getMainLooper()).postDelayed( { autoCaptureForVerification() }, 1000 ) } else { + val bd = BlurDetector() + if (bd.isBlurry(processedBitmap)) { + Toast.makeText(this@FaceProcessorActivity, "Gambar Terlalu Buram!", Toast.LENGTH_LONG).show() + } verifyFace(processedBitmap) } }