11package com.aria.danesh.faceverificationlib.utils
22
3+ import android.util.Log
34import androidx.annotation.OptIn
45import androidx.camera.core.ExperimentalGetImage
56import androidx.camera.core.ImageAnalysis
67import androidx.camera.core.ImageProxy
8+ import com.aria.danesh.faceverificationlib.state.LivelinessState
79import com.google.mlkit.vision.common.InputImage
810import com.google.mlkit.vision.face.Face
911import com.google.mlkit.vision.face.FaceDetection
12+ import com.google.mlkit.vision.face.FaceDetector
1013import com.google.mlkit.vision.face.FaceDetectorOptions
1114import com.google.mlkit.vision.face.FaceLandmark
1215import kotlinx.coroutines.flow.MutableStateFlow
1316import kotlinx.coroutines.flow.StateFlow
1417import java.util.concurrent.TimeUnit
1518import 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