Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(liveness): Update Liveness UI #100

Merged
merged 20 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ internal class LivenessCoordinator(
private val sessionId: String,
private val region: String,
private val credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
disableStartView: Boolean,
private val onChallengeComplete: OnChallengeComplete,
val onChallengeFailed: Consumer<FaceLivenessDetectionException>,
val onChallengeFailed: Consumer<FaceLivenessDetectionException>
) {

private val analysisExecutor = Executors.newSingleThreadExecutor()

val livenessState = LivenessState(
sessionId,
context,
disableStartView,
this::processCaptureReady,
this::startLivenessSession,
this::processSessionError,
Expand Down Expand Up @@ -270,10 +272,10 @@ internal class LivenessCoordinator(

fun destroy(context: Context) {
// Destroy all resources so a new coordinator can safely be created
// livenessWebSocket.destroy()
encoder.stop {
encoder.destroy()
}
livenessState.onDestroy(true)
unbindCamera(context)
analysisExecutor.shutdown()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import android.graphics.RectF
import com.amplifyframework.ui.liveness.R
import com.amplifyframework.ui.liveness.ml.FaceDetector

internal sealed class LivenessCheckState(val instructionId: Int? = null) {
class Initial(instructionId: Int? = null) : LivenessCheckState(instructionId) {
internal sealed class LivenessCheckState(val instructionId: Int? = null, val isActionable: Boolean = true) {
class Initial(
instructionId: Int? = null,
isActionable: Boolean = true
) : LivenessCheckState(instructionId, isActionable) {
companion object {
fun withMoveFaceMessage() =
Initial(R.string.amplify_ui_liveness_challenge_instruction_move_face)
Expand All @@ -29,10 +32,12 @@ internal sealed class LivenessCheckState(val instructionId: Int? = null) {
fun withMoveFaceFurtherAwayMessage() =
Initial(R.string.amplify_ui_liveness_challenge_instruction_move_face_further)
fun withConnectingMessage() =
Initial(R.string.amplify_ui_liveness_challenge_connecting)
Initial(R.string.amplify_ui_liveness_challenge_connecting, false)
fun withStartViewMessage() =
Initial(R.string.amplify_ui_liveness_get_ready_center_face_label)
}
}
class Running(instructionId: Int? = null) : LivenessCheckState(instructionId) {
class Running(instructionId: Int? = null) : LivenessCheckState(instructionId, true) {
companion object {
fun withMoveFaceMessage() = Running(
R.string.amplify_ui_liveness_challenge_instruction_move_face_closer
Expand All @@ -44,7 +49,7 @@ internal sealed class LivenessCheckState(val instructionId: Int? = null) {
Running(faceOvalPosition.instructionStringRes)
}
}
object Error : LivenessCheckState()
object Error : LivenessCheckState(isActionable = false)
class Success(val faceGuideRect: RectF) :
LivenessCheckState(R.string.amplify_ui_liveness_challenge_verifying)
LivenessCheckState(R.string.amplify_ui_liveness_challenge_verifying, false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,19 @@ import com.amplifyframework.ui.liveness.model.LivenessCheckState
import com.amplifyframework.ui.liveness.ui.helper.VideoViewportSize
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import kotlin.concurrent.schedule

internal data class InitialStreamFace(val faceRect: RectF, val timestamp: Long)

internal data class LivenessState(
val sessionId: String,
val context: Context,
val disableStartView: Boolean,
val onCaptureReady: () -> Unit,
val onFaceDistanceCheckPassed: () -> Unit,
val onSessionError: (FaceLivenessDetectionException, Boolean) -> Unit,
val onFinalEventsSent: () -> Unit
val onFinalEventsSent: () -> Unit,
) {
var videoViewportSize: VideoViewportSize? by mutableStateOf(null)
var livenessCheckState = mutableStateOf<LivenessCheckState>(
Expand All @@ -58,13 +60,15 @@ internal data class LivenessState(
var initialFaceDistanceCheckPassed by mutableStateOf(false)
var initialLocalFaceFound by mutableStateOf(false)

var showingStartView by mutableStateOf(!disableStartView)

private var initialStreamFace: InitialStreamFace? = null
@VisibleForTesting
var faceMatchOvalStart: Long? = null
@VisibleForTesting
var faceMatchOvalEnd: Long? = null
private var initialFaceOvalIou = -1f
private var faceOvalMatchTimerStarted = false
private var faceOvalMatchTimer: TimerTask? = null
private var detectedFaceMatchedOval = false

@VisibleForTesting
Expand All @@ -85,6 +89,12 @@ internal data class LivenessState(

fun onError(stopLivenessSession: Boolean) {
livenessCheckState.value = LivenessCheckState.Error
onDestroy(stopLivenessSession)
}

// Cleans up state when challenge is completed or cancelled
fun onDestroy(stopLivenessSession: Boolean) {
faceOvalMatchTimer?.cancel()
readyForOval = false
faceGuideRect = null
runningFreshness = false
Expand Down Expand Up @@ -127,28 +137,44 @@ internal data class LivenessState(
* @return true if FrameAnalyzer should continue processing the frame
*/
fun onFrameAvailable(): Boolean {
val livenessCheckState = livenessCheckState.value
if (livenessCheckState == LivenessCheckState.Error) return false
if (livenessCheckState !is LivenessCheckState.Success) return true

if (readyToSendFinalEvents) {
readyToSendFinalEvents = false

livenessSessionInfo!!.sendChallengeResponseEvent(
FaceTargetChallengeResponse(
colorChallenge!!.challengeId,
livenessCheckState.faceGuideRect,
Date(faceMatchOvalStart!!),
Date(faceMatchOvalEnd!!)
)
)
if (showingStartView) return false

return when (val livenessCheckState = livenessCheckState.value) {
is LivenessCheckState.Error -> false
is LivenessCheckState.Initial, is LivenessCheckState.Running -> {
/**
* Start freshness check if the face has matched oval (we know this if faceMatchOvalStart is not null)
* We trigger this in onFrameAvailable instead of onFrameFaceUpdate in the event the user moved the face
* away from the camera. We want to run this check on every frame if the challenge is in process.
*/
if (!runningFreshness && colorChallenge?.challengeType ==
ColorChallengeType.SEQUENTIAL &&
faceMatchOvalStart?.let { (Date().time - it) > 1000 } == true
ankpshah marked this conversation as resolved.
Show resolved Hide resolved
) {
runningFreshness = true
}
true
}
is LivenessCheckState.Success -> {
if (readyToSendFinalEvents) {
readyToSendFinalEvents = false

livenessSessionInfo!!.sendChallengeResponseEvent(
ankpshah marked this conversation as resolved.
Show resolved Hide resolved
FaceTargetChallengeResponse(
colorChallenge!!.challengeId,
livenessCheckState.faceGuideRect,
Date(faceMatchOvalStart!!),
Date(faceMatchOvalEnd!!)
)
)

// Send empty video event to signal we're done sending video
livenessSessionInfo!!.sendVideoEvent(VideoEvent(ByteArray(0), Date()))
onFinalEventsSent()
// Send empty video event to signal we're done sending video
livenessSessionInfo!!.sendVideoEvent(VideoEvent(ByteArray(0), Date()))
onFinalEventsSent()
}
false
}
}

return false
}

fun onFrameFaceCountUpdate(faceCount: Int) {
Expand Down Expand Up @@ -178,12 +204,19 @@ internal data class LivenessState(
}
}

/**
* returns true if face update inspect, false if thrown away
*/
fun onFrameFaceUpdate(
faceRect: RectF,
leftEye: FaceDetector.Landmark,
rightEye: FaceDetector.Landmark,
mouth: FaceDetector.Landmark
) {
): Boolean {
if (showingStartView) {
return false
}

if (!initialFaceDistanceCheckPassed) {
val faceDistance = FaceDetector.calculateFaceDistance(
leftEye, rightEye, mouth,
Expand Down Expand Up @@ -261,29 +294,25 @@ internal data class LivenessState(

// Start timer and then timeout if the detected face doesn't match
// the oval after a period of time
if (!detectedFaceMatchedOval && !faceOvalMatchTimerStarted) {
faceOvalMatchTimerStarted = true
Timer().schedule(faceTargetChallenge!!.faceTargetMatching.ovalFitTimeout.toLong()) {
if (!detectedFaceMatchedOval && faceGuideRect != null) {
readyForOval = false
val timeoutError =
FaceLivenessDetectionException(
"Face did not match oval within time limit."
)
onSessionError(timeoutError, true)
if (!detectedFaceMatchedOval && faceOvalMatchTimer == null) {
faceOvalMatchTimer =
Timer().schedule(faceTargetChallenge!!.faceTargetMatching.ovalFitTimeout.toLong()) {
if (!detectedFaceMatchedOval && faceGuideRect != null) {
readyForOval = false
val timeoutError =
FaceLivenessDetectionException(
"Face did not match oval within time limit."
)
onSessionError(timeoutError, true)
}
cancel()
}
faceOvalMatchTimerStarted = false
cancel()
}
}

// Start freshness check if it's not already started and face is in oval
if (!runningFreshness && colorChallenge?.challengeType ==
ColorChallengeType.SEQUENTIAL &&
faceOvalPosition == FaceDetector.FaceOvalPosition.MATCHED
) {
runningFreshness = true
}
}
return true
}

fun onStartViewComplete() {
showingStartView = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import com.amplifyframework.ui.liveness.ui.helper.VideoViewportSize
internal fun FaceGuide(
modifier: Modifier,
faceGuideRect: RectF?,
videoViewportSize: VideoViewportSize
videoViewportSize: VideoViewportSize,
backgroundColor: Color = Color.White
) {

val scaledBoundingRect = faceGuideRect?.let {
Expand All @@ -50,7 +51,7 @@ internal fun FaceGuide(
Canvas(modifier.graphicsLayer(alpha = 0.99f)) {

drawRect(
color = Color.White,
color = backgroundColor,
size = size
)

Expand Down
Loading