diff --git a/AndroidApps/app/src/main/assets/DeepTreeFaceAntiSpoofing.tflite b/AndroidApps/app/src/main/assets/DeepTreeFaceAntiSpoofing.tflite deleted file mode 100644 index 7eb8fee..0000000 Binary files a/AndroidApps/app/src/main/assets/DeepTreeFaceAntiSpoofing.tflite and /dev/null differ diff --git a/AndroidApps/app/src/main/assets/model_anti_spoofing_final_test.tflite b/AndroidApps/app/src/main/assets/model_anti_spoofing_final_test.tflite new file mode 100644 index 0000000..b2d1d5e Binary files /dev/null and b/AndroidApps/app/src/main/assets/model_anti_spoofing_final_test.tflite differ 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..2ec223f 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,7 @@ 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.221:3000/") .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() diff --git a/AndroidApps/app/src/main/java/com/mfa/camerax/CameraManager.kt b/AndroidApps/app/src/main/java/com/mfa/camerax/CameraManager.kt index 0f9bc75..4cfa352 100644 --- a/AndroidApps/app/src/main/java/com/mfa/camerax/CameraManager.kt +++ b/AndroidApps/app/src/main/java/com/mfa/camerax/CameraManager.kt @@ -98,6 +98,9 @@ class CameraManager( } fun onTakeImage(callback: OnTakeImageCallback) { + Handler(Looper.getMainLooper()).post { + callback.onTakeImageStart() + } imageCapture.takePicture(cameraExecutor, object : OnImageCapturedCallback() { override fun onCaptureSuccess(imageProxy: ImageProxy) { @SuppressLint("UnsafeOptInUsageError") @@ -139,8 +142,6 @@ class CameraManager( } } - - override fun onError(exception: ImageCaptureException) { Handler(Looper.getMainLooper()).post { callback.onTakeImageError(exception) @@ -172,6 +173,7 @@ class CameraManager( interface OnTakeImageCallback { fun onTakeImageSuccess(image : Bitmap) fun onTakeImageError(exception: Exception) + fun onTakeImageStart() } companion object { diff --git a/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceAntiSpoofing.kt b/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceAntiSpoofing.kt index 31a20bd..facf900 100644 --- a/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceAntiSpoofing.kt +++ b/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceAntiSpoofing.kt @@ -22,42 +22,38 @@ class FaceAntiSpoofing @Throws(IOException::class) constructor(assetManager: Ass } fun antiSpoofing(bitmap: Bitmap): Float { - // Resize the face to 256X256, because the shape of the placeholder required for the feed data below is (1, 256, 256, 3) + // 1. Resize ke 224x224 val bitmapScale = Bitmap.createScaledBitmap(bitmap, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, true) - val img: Array> = normalizeImage(bitmapScale) - val input: Array>?> = arrayOfNulls(1) - input[0] = img - val clss_pred = Array(1) { FloatArray(8) } - val leaf_node_mask = Array(1) { FloatArray(8) } - val outputs: MutableMap = HashMap() - outputs[interpreter.getOutputIndex("Identity")] = clss_pred - outputs[interpreter.getOutputIndex("Identity_1")] = leaf_node_mask - interpreter.runForMultipleInputsOutputs(arrayOf(input), outputs) - - Log.i( - TAG, "[" + clss_pred[0][0] + ", " + clss_pred[0][1] + ", " - + clss_pred[0][2] + ", " + clss_pred[0][3] + ", " + clss_pred[0][4] + ", " - + clss_pred[0][5] + ", " + clss_pred[0][6] + ", " + clss_pred[0][7] + "]" - ) - Log.i( - TAG, "[" + leaf_node_mask[0][0] + ", " + leaf_node_mask[0][1] + ", " - + leaf_node_mask[0][2] + ", " + leaf_node_mask[0][3] + ", " + leaf_node_mask[0][4] + ", " - + leaf_node_mask[0][5] + ", " + leaf_node_mask[0][6] + ", " + leaf_node_mask[0][7] + "]" - ) - return leaf_score1(clss_pred, leaf_node_mask); - } + // 2. Normalisasi gambar ke array [224][224][3] + val normalizedImage = normalizeImage(bitmapScale) - private fun leaf_score1(clss_pred: Array, leaf_node_mask: Array): Float { - var score = 0f - for (i in 0..7) { - score += abs(clss_pred[0][i]) * leaf_node_mask[0][i] + // 3. Bungkus ke dalam shape [1][224][224][3] sesuai input model + val input = Array(1) { + Array(INPUT_IMAGE_SIZE) { + Array(INPUT_IMAGE_SIZE) { + FloatArray(3) + } + } } - return score - } - private fun leaf_score2(clss_pred: Array): Float { - return clss_pred[0][ROUTE_INDEX] + for (i in 0 until INPUT_IMAGE_SIZE) { + for (j in 0 until INPUT_IMAGE_SIZE) { + input[0][i][j] = normalizedImage[i][j] + } + } + + // 4. Output model adalah [1][1], jadi kita siapkan array untuk menampung hasilnya + val output = Array(1) { FloatArray(1) } + + // 5. Jalankan model + interpreter.run(input, output) + + // 6. Ambil nilai prediksi dari output[0][0] + val prediction = output[0][0] + Log.d(TAG, "Anti-spoofing prediction result: $prediction") + + return prediction } fun normalizeImage(bitmap: Bitmap): Array> { @@ -97,9 +93,15 @@ class FaceAntiSpoofing @Throws(IOException::class) constructor(assetManager: Ass */ fun laplacian(bitmap: Bitmap): Int { // Resize the face to a size of 256X256, because the shape of the placeholder that needs feed data below is (1, 256, 256, 3) - val bitmapScale = Bitmap.createScaledBitmap(bitmap, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, true) + val bitmapScale = Bitmap.createScaledBitmap(bitmap, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, false + ) - val laplace = arrayOf(intArrayOf(0, 1, 0), intArrayOf(1, -4, 1), intArrayOf(0, 1, 0)) +// val laplace = arrayOf(intArrayOf(0, 1, 0), intArrayOf(1, -4, 1), intArrayOf(0, 1, 0)) + val laplace = arrayOf( + intArrayOf(1, 1, 1), + intArrayOf(1, -8, 1), + intArrayOf(1, 1, 1) + ) val size = laplace.size val img: Array = Utils.convertGreyImg(bitmapScale) val height = img.size @@ -115,7 +117,7 @@ class FaceAntiSpoofing @Throws(IOException::class) constructor(assetManager: Ass result += (img[x + i][y + j] and 0xFF) * laplace[i][j] } } - if (result > LAPLACE_THRESHOLD) { + if (abs(result) > LAPLACE_THRESHOLD) { score++ } } @@ -125,10 +127,10 @@ class FaceAntiSpoofing @Throws(IOException::class) constructor(assetManager: Ass companion object { private val TAG = "FaceAntiSpoofing" - private val MODEL_FILE = "DeepTreeFaceAntiSpoofing.tflite" + private val MODEL_FILE = "model_anti_spoofing_final_test.tflite" - val INPUT_IMAGE_SIZE: Int = 256 // The width and height of the placeholder image that needs feed data - val THRESHOLD: Float = 0.2f // Set a threshold, greater than this value is considered an attack + val INPUT_IMAGE_SIZE: Int = 224 // The width and height of the placeholder image that needs feed data + val THRESHOLD: Float = 0.5f // Set a threshold, greater than this value is considered an attack val ROUTE_INDEX: Int = 6 // Route indices observed during training diff --git a/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceRecognizer.kt b/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceRecognizer.kt index c5da7dd..b129e70 100644 --- a/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceRecognizer.kt +++ b/AndroidApps/app/src/main/java/com/mfa/facedetector/FaceRecognizer.kt @@ -39,6 +39,7 @@ class FaceRecognizer @Throws(IOException::class) constructor(assetManager: Asset return evaluate(embeddings) } +// change into images into embedding with mobile facenet fun getEmbeddingsOfImage(bitmapImage: Bitmap): Array { val bitmapScale = Bitmap.createScaledBitmap(bitmapImage, INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, true) val datasets: Array>> = getImagesDataset(bitmapScale) diff --git a/AndroidApps/app/src/main/java/com/mfa/utils/Utils.kt b/AndroidApps/app/src/main/java/com/mfa/utils/Utils.kt index a1a0f1d..8b4fcf1 100644 --- a/AndroidApps/app/src/main/java/com/mfa/utils/Utils.kt +++ b/AndroidApps/app/src/main/java/com/mfa/utils/Utils.kt @@ -126,14 +126,30 @@ class Utils { return dataImage } - fun setFirebaseEmbedding(embedingFloatList: List ){ - val mdatabase = FirebaseDatabase.getInstance().reference - val user = FirebaseAuth.getInstance().currentUser - val userName = user!!.uid + "_" + user.displayName!!.replace(" ", "") - //overwrites existing records instead of appending them - mdatabase.child("newfaceantispooflog").child(userName) - .child("faceEmbeddings") - .setValue(embedingFloatList) + fun setFirebaseEmbedding(embeddingFloatList: List) { + try { + val user = FirebaseAuth.getInstance().currentUser + if (user == null) { + Log.e("FirebaseEmbedding", "User not logged in") + return + } + + val userName = user.uid + "_" + (user.displayName ?: "unknown").replace(" ", "") + val database = FirebaseDatabase.getInstance().reference + + database.child("newfaceantispooflog") + .child(userName) + .child("faceEmbeddings") + .setValue(embeddingFloatList) + .addOnSuccessListener { + Log.d("FirebaseEmbedding", "Embeddings saved successfully") + } + .addOnFailureListener { e -> + Log.e("FirebaseEmbedding", "Failed to save embeddings", e) + } + } catch (e: Exception) { + Log.e("FirebaseEmbedding", "Error saving embeddings", e) + } } /** 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 7a294fc..ca0cb9d 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 @@ -1,11 +1,15 @@ package com.mfa.view.activity +import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.Intent import android.content.pm.PackageManager +import android.content.res.ColorStateList +import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.util.Log @@ -65,6 +69,8 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall private lateinit var faceRecognizer: FaceRecognizer // Face recognizer private lateinit var fas: FaceAntiSpoofing // Face anti-spoofing + private var loadingDialog: LoadingDialogFragment? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityCaptureFaceBinding.inflate(layoutInflater) @@ -103,17 +109,43 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall // Handle back button press onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - showCustomDialog( - title = "Pemberitahuan", - message = "Mohon selesaikan proses presensi", - buttonText = "Oke" - ) { - onResume() +// showCustomDialog( +// title = "Pemberitahuan", +// message = "Mohon selesaikan proses presensi", +// buttonText = "Oke", +// color = R.color.green_primary +// ) { +// onResume() +// } + val builder = AlertDialog.Builder(this@FaceProcessorActivity,R.style.CustomAlertDialogStyle) + builder.setTitle("Pemberitahuan") + builder.setMessage("Apakah kamu ingin membatalkan presensi?") + builder.setPositiveButton("Iya"){ _, _ -> + val back = Intent(this@FaceProcessorActivity, PresensiActivity::class.java) + back.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + startActivity(back) + } + builder.setNegativeButton("Batalkan"){ _, _-> +// system will handle it } + builder.setCancelable(false) + builder.show() } }) } + private fun showLoadingDialog() { + if (loadingDialog?.isAdded == true) return // Jangan tampilkan jika sudah ada + + loadingDialog = LoadingDialogFragment() + loadingDialog?.show(supportFragmentManager, "loadingDialog") + } + + private fun dismissLoadingDialog() { + loadingDialog?.dismiss() + loadingDialog = null + } + private fun buttonClicks() { binding.buttonTurnCamera.setOnClickListener { cameraManager.changeCamera() @@ -150,25 +182,63 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall } } + @SuppressLint("ResourceAsColor") override fun onTakeImageSuccess(image: Bitmap) { +// binding.progressBar.visibility = View.GONE + showLoadingDialog() + binding.buttonStopCamera.isEnabled = true val addFaceBinding = DialogAddFaceBinding.inflate(layoutInflater) addFaceBinding.capturedFace.setImageBitmap(image) val dialog = AlertDialog.Builder(this) .setView(addFaceBinding.root) - .setTitle("Confirm Face") - .setPositiveButton("OK", null) - .setNegativeButton("Cancel") { d, _ -> d.dismiss() } + .setTitle("Konfirmasi wajah") + .setPositiveButton("KONFIRMASI", null) + .setNegativeButton("BATALKAN") { d, _ -> d.dismiss() } .create() + dialog.setOnShowListener { val okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - okButton.setOnClickListener { + + dialog.window?.setBackgroundDrawableResource(R.color.gray_light) // Ganti dengan warna yang diinginkan + // Atur padding untuk konten dialog (jika menggunakan custom view) + + // 1. Title: Putih & Bold + dialog.findViewById(android.R.id.title)?.apply { + setTextColor(Color.WHITE) + setTypeface(typeface, Typeface.BOLD) + } + + // 2. Tombol Positif (Verifikasi): Teks putih + Background hijau + dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply { + setTextColor(Color.WHITE) + setBackgroundColor(ContextCompat.getColor(context, R.color.teal_700)) + } + + // 3. Tombol Negatif (Batalkan): Teks putih + Background merah + dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply { + setTextColor(Color.WHITE) + setBackgroundColor(ContextCompat.getColor(context, R.color.red)) + } + + // Opsional: Atur padding tombol + listOf(AlertDialog.BUTTON_POSITIVE, AlertDialog.BUTTON_NEGATIVE).forEach { buttonType -> + dialog.getButton(buttonType)?.setPadding(32.toPx(), 16.toPx(), 32.toPx(), 16.toPx()) + } + dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { + dismissLoadingDialog() dialog.dismiss() + } + + okButton.setOnClickListener { + dialog.dismiss() +// dismissLoadingDialog() lifecycleScope.launch(Dispatchers.Main) { try { - if (antiSpoofDetection(image)) { + //if wajah asli + if (antiSpoofDetection(image)!=false) { val embeddings = withContext(Dispatchers.IO) { faceRecognizer.getEmbeddingsOfImage(image) } if (embeddings.isEmpty() || embeddings[0].isEmpty()) { @@ -191,30 +261,86 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall } dialog.show() } + fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt() + override fun onTakeImageStart() { +// binding.progressBar.visibility = View.VISIBLE + // Disable tombol capture sementara + showLoadingDialog() +// binding.buttonStopCamera.isEnabled = false + } private fun antiSpoofDetection(faceBitmap: Bitmap): Boolean { - val laplaceScore: Int = fas.laplacian(faceBitmap) + // 1. Blur Detection with Laplacian + val laplaceScore = fas.laplacian(faceBitmap) + Log.d(TAG, "Laplacian Blur Score: $laplaceScore") + dismissLoadingDialog() + if (laplaceScore < FaceAntiSpoofing.LAPLACIAN_THRESHOLD) { - Toast.makeText(this, "Image too blurry!", Toast.LENGTH_LONG).show() + runOnUiThread { + Toast.makeText( + this, + "Kualitas foto rendah. Pastikan wajah terlihat jelas dan tidak blur.", + Toast.LENGTH_LONG + ).show() + } + Log.w(TAG, "Blur detected - Laplacian score too low ($laplaceScore)") return false } - val start = System.currentTimeMillis() - val score = fas.antiSpoofing(faceBitmap) - val end = System.currentTimeMillis() - Log.d(TAG, "Spoof detection process time: ${end - start} ms") - - return score < FaceAntiSpoofing.THRESHOLD + // 2. Anti-Spoofing Analysis + Log.d(TAG, "Starting anti-spoofing analysis...") + val startTime = System.currentTimeMillis() + + try { + val spoofScore = fas.antiSpoofing(faceBitmap) + val processingTime = System.currentTimeMillis() - startTime + + Log.d(TAG, """ + Anti-Spoofing Results: + - Score: $spoofScore + - Threshold: ${FaceAntiSpoofing.THRESHOLD} + - Processing Time: ${processingTime}ms + """.trimIndent()) + + if (spoofScore >= FaceAntiSpoofing.THRESHOLD) { + runOnUiThread { + Toast.makeText( + this, + "Deteksi kecurangan: Wajah tidak asli!", + Toast.LENGTH_LONG + ).show() + showCustomDialog("Hasil verifikasi wajah", + "Mohon jangan gunakan foto", + "Oke", R.color.red){ + } + } + Log.w(TAG, "Potential spoof detected (score: $spoofScore)") + } + // if wajah dianggap asli == true + return spoofScore < FaceAntiSpoofing.THRESHOLD + } catch (e: Exception) { + Log.e(TAG, "Anti-spoofing failed: ${e.message}", e) + runOnUiThread { + Toast.makeText( + this, + "Gagal memproses deteksi wajah. Coba lagi.", + Toast.LENGTH_LONG + ).show() + } + return false + } } override fun onTakeImageError(exception: Exception) { - Toast.makeText(this, "onTakeImageError: ${exception.message}", Toast.LENGTH_SHORT).show() +// binding.progressBar.visibility = View.GONE + dismissLoadingDialog() + binding.buttonStopCamera.isEnabled = true + Toast.makeText(this, "Gagal mengambil foto: ${exception.message}", Toast.LENGTH_SHORT).show() } private fun handleEmbeddings(embeddingList: ArrayList) { // Tampilkan loading saat proses verifikasi dimulai - val loadingDialog = LoadingDialogFragment() - loadingDialog.show(supportFragmentManager, "loadingDialog") + showLoadingDialog() lifecycleScope.launch(Dispatchers.Main) { try { @@ -246,16 +372,19 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall showCustomDialog( title = "Hasil verifikasi wajah", message = "Selamat! Anda berhasil menyelesaikan verifikasi wajah.", - buttonText = "Lihat status presensi" + buttonText = "Lihat status presensi", + color = R.color.green_primary ) { + dismissLoadingDialog() reqFaceApi() } } else { Toast.makeText(this@FaceProcessorActivity, "Face verification failed!", Toast.LENGTH_LONG).show() showCustomDialog( title = "Hasil verifikasi wajah", - message = "Maaf, kami gagal mengenali Anda. Mohon gunakan wajah Anda sendiri untuk verifikasi.", - buttonText = "Coba lagi" + message = "Maaf, kami gagal mengenali Anda. Mohon gunakan wajah anda sendiri untuk verifikasi.", + buttonText = "Coba lagi", + color = R.color.red ) { onResume() } @@ -268,31 +397,38 @@ class FaceProcessorActivity : AppCompatActivity(), CameraManager.OnTakeImageCall Toast.makeText(this@FaceProcessorActivity, "Gagal memproses verifikasi: ${e.message}", Toast.LENGTH_LONG).show() } finally { // Sembunyikan loading setelah proses selesai - loadingDialog.dismiss() +// loadingDialog.dismiss() + dismissLoadingDialog() } } } - private fun showCustomDialog(title: String, message: String, buttonText: String, action: () -> Unit) { - val dialog = Dialog(this) - val dialogView: View = LayoutInflater.from(this).inflate(R.layout.custom_alert_dialog, null) - - dialog.setContentView(dialogView) - dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val tvTitle = dialogView.findViewById(R.id.tvTitle) - val tvMessage = dialogView.findViewById(R.id.tvMessage) - val btnConfirm = dialogView.findViewById