Skip to content

Commit bb3b7a2

Browse files
committed
Adding FaceLiveliness Detector
1 parent a74cbc2 commit bb3b7a2

File tree

11 files changed

+353
-173
lines changed

11 files changed

+353
-173
lines changed

.idea/deploymentTargetSelector.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

FaceVerificationLib/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
namespace = "com.aria.danesh.faceverificationlib"
99
defaultConfig {
1010
minSdk = 24
11-
compileSdk = 34
11+
compileSdk = 36
1212
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1313
consumerProguardFiles("consumer-rules.pro")
1414
}

FaceVerificationLib/src/main/java/com/aria/danesh/faceverificationlib/managers/FaceDetectionManager.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import androidx.compose.runtime.ComposableOpenTarget
1515
import androidx.core.content.ContextCompat
1616
import androidx.lifecycle.LifecycleOwner
1717
import com.aria.danesh.faceverificationlib.callback.FaceDetectionCallback
18+
import com.aria.danesh.faceverificationlib.view.compose.UniversalData.isFrontCamera
19+
import com.google.common.util.concurrent.ListenableFuture
1820
import com.google.mlkit.vision.common.InputImage
1921
import com.google.mlkit.vision.face.Face
2022
import com.google.mlkit.vision.face.FaceDetectorOptions
@@ -41,9 +43,10 @@ fun faceDetector(
4143
return FaceDetection.getClient(faceDetectorOptions)
4244
}
4345

44-
fun cameraSelector(): CameraSelector {
46+
fun cameraSelector(lensFacing: Int = CameraSelector.LENS_FACING_BACK): CameraSelector {
47+
isFrontCamera = lensFacing==CameraSelector.LENS_FACING_FRONT
4548
return CameraSelector.Builder()
46-
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
49+
.requireLensFacing(lensFacing)
4750
.build()
4851
}
4952

@@ -53,11 +56,13 @@ class FaceDetectionManager(
5356
private val context: Context,
5457
private val lifecycleOwner: LifecycleOwner,
5558
private val previewView: PreviewView,
56-
private val callback: FaceDetectionCallback
57-
) {
58-
private var cameraProviderFuture = ProcessCameraProvider.getInstance(context)
59-
private val detector = faceDetector()
59+
private val callback: FaceDetectionCallback,
60+
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(context),
61+
private val detector: FaceDetector = faceDetector(),
62+
private val cameraSelector: CameraSelector =cameraSelector(),
6063
private val executor: ExecutorService = Executors.newFixedThreadPool(10)
64+
) {
65+
6166

6267
init {
6368
startCamera()
@@ -72,7 +77,6 @@ class FaceDetectionManager(
7277

7378
private fun bindPreview(cameraProvider: ProcessCameraProvider) {
7479
val preview: Preview = Preview.Builder().build()
75-
val cameraSelector =cameraSelector()
7680
val imageAnalysis = ImageAnalysis.Builder()
7781
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
7882
.build()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.aria.danesh.faceverificationlib.state
2+
3+
import androidx.camera.core.ImageProxy
4+
5+
sealed class LivelinessState {
6+
object Initial : LivelinessState()
7+
data class Processing(val message: String) : LivelinessState()
8+
data class Success(val imageProxy: ImageProxy) : LivelinessState()
9+
data class Failed(val error: String) : LivelinessState()
10+
}
Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
package com.aria.danesh.faceverificationlib.utils
22

3+
import android.util.Log
34
import androidx.annotation.OptIn
45
import androidx.camera.core.ExperimentalGetImage
56
import androidx.camera.core.ImageAnalysis
67
import androidx.camera.core.ImageProxy
8+
import com.aria.danesh.faceverificationlib.state.LivelinessState
79
import com.google.mlkit.vision.common.InputImage
810
import com.google.mlkit.vision.face.Face
911
import com.google.mlkit.vision.face.FaceDetection
12+
import com.google.mlkit.vision.face.FaceDetector
1013
import com.google.mlkit.vision.face.FaceDetectorOptions
1114
import com.google.mlkit.vision.face.FaceLandmark
1215
import kotlinx.coroutines.flow.MutableStateFlow
1316
import kotlinx.coroutines.flow.StateFlow
1417
import java.util.concurrent.TimeUnit
1518
import kotlin.math.abs
1619

17-
class LivenessDetectorAnalyzer(
18-
private val onLivenessResult: (LivenessState) -> Unit,
20+
class LivelinessDetectorAnalyzer(
21+
private val onLivelinessResult: (LivelinessState) -> Unit,
1922
private val requiredBlinks: Int = 1,
2023
private val motionThreshold: Float = 5f, // Threshold for landmark movement
21-
private val motionDetectionWindowMs: Long = 500L
22-
) {
24+
private val motionDetectionWindowMs: Long = 500L,
25+
private val eyeOpenThreshold: Float = 0.2f //make it public
26+
) : ImageAnalysis.Analyzer
27+
{
2328

2429
private var blinkCount = 0
2530
private var isLeftEyeClosed = false
@@ -30,23 +35,90 @@ class LivenessDetectorAnalyzer(
3035
private var previousFace: Face? = null
3136
private var lastMotionDetectionTime = 0L
3237

33-
private val _livenessState = MutableStateFlow<LivenessState>(LivenessState.Initial)
38+
private val _livelinessState = MutableStateFlow<LivelinessState>(LivelinessState.Initial)
39+
val livelinessState: StateFlow<LivelinessState> = _livelinessState
3440

3541
init {
36-
onLivenessResult(LivenessState.Initial)
42+
onLivelinessResult(LivelinessState.Initial)
3743
}
3844

39-
internal fun processFace(currentFace: Face,ip : ImageProxy) {
45+
fun faceDetector(
46+
performance: Int = FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE,
47+
landmark: Int = FaceDetectorOptions.LANDMARK_MODE_ALL,
48+
classification: Int = FaceDetectorOptions.CLASSIFICATION_MODE_ALL,
49+
contour: Int = FaceDetectorOptions.CONTOUR_MODE_ALL,
50+
minFaceSize: Float = 0.05f
51+
): FaceDetector {
52+
val faceDetectorOptions = FaceDetectorOptions.Builder()
53+
.setPerformanceMode(performance)
54+
.setLandmarkMode(landmark)
55+
.setClassificationMode(classification)
56+
.setContourMode(contour)
57+
.setMinFaceSize(minFaceSize)
58+
.enableTracking()
59+
.build()
60+
return FaceDetection.getClient(faceDetectorOptions)
61+
}
62+
63+
@OptIn(ExperimentalGetImage::class)
64+
override fun analyze(imageProxy: ImageProxy) {
65+
val mediaImage = imageProxy.image
66+
if (mediaImage != null) {
67+
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
68+
val detector = faceDetector()
69+
detector.process(image)
70+
.addOnSuccessListener { faces ->
71+
if (faces.isNotEmpty()) {
72+
if (faces.size==1) {
73+
val face = faces.last()
74+
if (face.boundingBox.width() in 100..290 &&
75+
face.boundingBox.height() in 100..290 &&
76+
face.boundingBox.top in 100..300 &&
77+
face.boundingBox.left in 100..300 &&
78+
face.boundingBox.bottom in 300..500 &&
79+
face.boundingBox.right in 300..500
80+
) {
81+
Log.d(
82+
"LivenessDetector",
83+
"Face detected: ${faces.size} faces"
84+
) //basic face detection log
85+
processFace(face, imageProxy)
86+
}else{
87+
_livelinessState.value = LivelinessState.Failed("Face isn't in the center of the frame")
88+
onLivelinessResult(LivelinessState.Failed("Face isn't in the center of the frame"))
89+
}
90+
}
91+
} else {
92+
resetLiveness()
93+
_livelinessState.value = LivelinessState.Failed("No face detected")
94+
onLivelinessResult(LivelinessState.Failed("No face detected"))
95+
Log.d("LivenessDetector", "No face detected") //log when no face
96+
}
97+
}
98+
.addOnFailureListener { e ->
99+
Log.e("LivenessDetector", "Face detection failed: ${e.message}")
100+
resetLiveness()
101+
_livelinessState.value = LivelinessState.Failed("Face detection failed: ${e.message}")
102+
onLivelinessResult(LivelinessState.Failed("Face detection failed: ${e.message}"))
103+
}
104+
.addOnCompleteListener { imageProxy.close() }
105+
} else {
106+
imageProxy.close()
107+
Log.d("LivenessDetector", "ImageProxy image is null") //check if the image is null
108+
}
109+
}
110+
111+
internal fun processFace(currentFace: Face, ip: ImageProxy) {
112+
40113
detectBlink(currentFace)
41114
detectMotion(currentFace)
42-
43115
if (blinkCount >= requiredBlinks) {
44-
_livenessState.value = LivenessState.Success(imageProxy = ip)
45-
onLivenessResult(LivenessState.Success(imageProxy = ip))
46-
// Optionally stop analysis here if liveness is confirmed
116+
_livelinessState.value = LivelinessState.Success(imageProxy = ip)
117+
onLivelinessResult(LivelinessState.Success(imageProxy = ip))
118+
Log.d("LivenessDetector", "Liveness Success. Blink Count: $blinkCount")
47119
} else {
48-
_livenessState.value = LivenessState.Processing("Detected $blinkCount/$requiredBlinks blinks.")
49-
onLivenessResult(LivenessState.Processing("Detected $blinkCount/$requiredBlinks blinks."))
120+
_livelinessState.value = LivelinessState.Processing("Detected $blinkCount/$requiredBlinks blinks.")
121+
onLivelinessResult(LivelinessState.Processing("Detected $blinkCount/$requiredBlinks blinks."))
50122
}
51123
previousFace = currentFace
52124
}
@@ -55,14 +127,20 @@ class LivenessDetectorAnalyzer(
55127
val leftEyeOpenProbability = currentFace.leftEyeOpenProbability ?: 1.0f
56128
val rightEyeOpenProbability = currentFace.rightEyeOpenProbability ?: 1.0f
57129

58-
val currentLeftClosed = leftEyeOpenProbability < 0.2f
59-
val currentRightClosed = rightEyeOpenProbability < 0.2f
130+
val currentLeftClosed = leftEyeOpenProbability < eyeOpenThreshold
131+
val currentRightClosed = rightEyeOpenProbability < eyeOpenThreshold
60132
val currentTime = System.currentTimeMillis()
61133

134+
Log.d(
135+
"LivenessDetector",
136+
"Eye Probabilities: Left = $leftEyeOpenProbability, Right = $rightEyeOpenProbability, threshold = $eyeOpenThreshold"
137+
) //added logs
138+
62139
if ((!isLeftEyeClosed && !isRightEyeClosed && currentLeftClosed && currentRightClosed) &&
63140
(currentTime - lastBlinkTime > blinkDebounceThreshold)) {
64141
blinkCount++
65142
lastBlinkTime = currentTime
143+
Log.d("LivenessDetector", "Blink Detected! Count: $blinkCount") //log when blink is detected
66144
}
67145

68146
isLeftEyeClosed = currentLeftClosed
@@ -80,14 +158,22 @@ class LivenessDetectorAnalyzer(
80158
val deltaX = abs(noseBaseCurrent.position.x - noseBasePrevious.position.x)
81159
val deltaY = abs(noseBaseCurrent.position.y - noseBasePrevious.position.y)
82160

161+
Log.d(
162+
"LivenessDetector",
163+
"Motion: DeltaX = $deltaX, DeltaY = $deltaY"
164+
) //log motion values
165+
83166
if (deltaX > motionThreshold || deltaY > motionThreshold) {
84167
// Consider motion as a sign of liveness (can be combined with blink)
85-
if (_livenessState.value is LivenessState.Processing) {
86-
_livenessState.value = LivenessState.Processing("Detected $blinkCount/$requiredBlinks blinks and motion.")
87-
onLivenessResult(LivenessState.Processing("Detected $blinkCount/$requiredBlinks blinks and motion."))
88-
} else if (_livenessState.value is LivenessState.Initial) {
89-
_livenessState.value = LivenessState.Processing("Detected initial motion.")
90-
onLivenessResult(LivenessState.Processing("Detected initial motion."))
168+
if (_livelinessState.value is LivelinessState.Processing) {
169+
_livelinessState.value =
170+
LivelinessState.Processing("Detected $blinkCount/$requiredBlinks blinks and motion.")
171+
onLivelinessResult(
172+
LivelinessState.Processing("Detected $blinkCount/$requiredBlinks blinks and motion.")
173+
)
174+
} else if (_livelinessState.value is LivelinessState.Initial) {
175+
_livelinessState.value = LivelinessState.Processing("Detected initial motion.")
176+
onLivelinessResult(LivelinessState.Processing("Detected initial motion."))
91177
}
92178
lastMotionDetectionTime = currentTime
93179
}
@@ -103,12 +189,9 @@ class LivenessDetectorAnalyzer(
103189
lastBlinkTime = 0L
104190
previousFace = null
105191
lastMotionDetectionTime = 0L
192+
_livelinessState.value = LivelinessState.Initial //reset
106193
}
107194
}
108195

109-
sealed class LivenessState {
110-
object Initial : LivenessState()
111-
data class Processing(val message: String) : LivenessState()
112-
data class Success(val imageProxy:ImageProxy) : LivenessState()
113-
data class Failed(val error: String) : LivenessState()
114-
}
196+
197+

0 commit comments

Comments
 (0)