@@ -17,6 +17,7 @@ package com.amplifyframework.ui.liveness.ml
1717
1818import  android.content.Context 
1919import  android.graphics.RectF 
20+ import  androidx.annotation.VisibleForTesting 
2021import  com.amplifyframework.predictions.aws.models.FaceTargetMatchingParameters 
2122import  com.amplifyframework.ui.liveness.R 
2223import  com.amplifyframework.ui.liveness.camera.LivenessCoordinator.Companion.TARGET_HEIGHT 
@@ -64,6 +65,14 @@ internal class FaceDetector(private val livenessState: LivenessState) {
6465            var  mouthX =  outputBoxes[0 ][i][10 ]
6566            var  mouthY =  outputBoxes[0 ][i][11 ]
6667
68+             //  the face's right ear is actually the one on the left on screen, and vice versa.
69+             //  this is the same for the eyes, but we need the ears to be correct with respect to the
70+             //  bounding box for the algorithm to work.
71+             var  rightEarX =  outputBoxes[0 ][i][12 ]
72+             var  rightEarY =  outputBoxes[0 ][i][13 ]
73+             var  leftEarX =  outputBoxes[0 ][i][14 ]
74+             var  leftEarY =  outputBoxes[0 ][i][15 ]
75+ 
6776            xCenter =  xCenter /  X_SCALE  *  anchors[i].w +  anchors[i].xCenter
6877            yCenter =  yCenter /  Y_SCALE  *  anchors[i].h +  anchors[i].yCenter
6978            h =  h /  H_SCALE  *  anchors[i].h
@@ -85,13 +94,20 @@ internal class FaceDetector(private val livenessState: LivenessState) {
8594            mouthX =  mouthX /  X_SCALE  *  anchors[i].w +  anchors[i].xCenter
8695            mouthY =  mouthY /  Y_SCALE  *  anchors[i].h +  anchors[i].yCenter
8796
97+             leftEarX =  leftEarX /  X_SCALE  *  anchors[i].w +  anchors[i].xCenter
98+             leftEarY =  leftEarY /  Y_SCALE  *  anchors[i].h +  anchors[i].yCenter
99+             rightEarX =  rightEarX /  X_SCALE  *  anchors[i].w +  anchors[i].xCenter
100+             rightEarY =  rightEarY /  Y_SCALE  *  anchors[i].h +  anchors[i].yCenter
101+ 
88102            detections.add(
89103                Detection (
90104                    RectF (xMin, yMin, xMax, yMax),
91105                    Landmark (leftEyeX, leftEyeY),
92106                    Landmark (rightEyeX, rightEyeY),
93107                    Landmark (noseX, noseY),
94108                    Landmark (mouthX, mouthY),
109+                     Landmark (leftEarX, leftEarY),
110+                     Landmark (rightEarX, rightEarY),
95111                    score
96112                )
97113            )
@@ -111,6 +127,8 @@ internal class FaceDetector(private val livenessState: LivenessState) {
111127        val  renormalizedDetections =  mutableListOf<Detection >()
112128        weightedDetections.forEach { detection -> 
113129            //  Change landmark coordinates to be for actual image size instead of model input size
130+             val  scaledBottom =  (detection.location.bottom /  Y_SCALE ) *  TARGET_HEIGHT 
131+ 
114132            val  scaledLeftEyeX =  (detection.leftEye.x /  X_SCALE ) *  TARGET_WIDTH 
115133            val  scaledLeftEyeY =  (detection.leftEye.y /  Y_SCALE ) *  TARGET_HEIGHT 
116134            val  scaledRightEyeX =  (detection.rightEye.x /  X_SCALE ) *  TARGET_WIDTH 
@@ -119,19 +137,28 @@ internal class FaceDetector(private val livenessState: LivenessState) {
119137            val  scaledNoseY =  (detection.nose.y /  Y_SCALE ) *  TARGET_HEIGHT 
120138            val  scaledMouthX =  (detection.mouth.x /  X_SCALE ) *  TARGET_WIDTH 
121139            val  scaledMouthY =  (detection.mouth.y /  Y_SCALE ) *  TARGET_HEIGHT 
140+             val  scaledLeftEarX =  (detection.leftEar.x /  X_SCALE ) *  TARGET_WIDTH 
141+             val  scaledLeftEarY =  (detection.leftEar.y /  Y_SCALE ) *  TARGET_HEIGHT 
142+             val  scaledRightEarX =  (detection.rightEar.x /  X_SCALE ) *  TARGET_WIDTH 
143+             val  scaledRightEarY =  (detection.rightEar.y /  Y_SCALE ) *  TARGET_HEIGHT 
122144
123145            val  scaledLeftEye =  Landmark (scaledLeftEyeX, scaledLeftEyeY)
124146            val  scaledRightEye =  Landmark (scaledRightEyeX, scaledRightEyeY)
125147            val  scaledNose =  Landmark (scaledNoseX, scaledNoseY)
126148            val  scaledMouth =  Landmark (scaledMouthX, scaledMouthY)
149+             val  scaledLeftEar =  Landmark (scaledLeftEarX, scaledLeftEarY)
150+             val  scaledRightEar =  Landmark (scaledRightEarX, scaledRightEarY)
127151
128152            //  Generate the face bounding box from the landmarks
129153            val  renormalizedBoundingBox = 
130154                generateBoundingBoxFromLandmarks(
155+                     scaledBottom,
131156                    scaledLeftEye,
132157                    scaledRightEye,
133158                    scaledNose,
134-                     scaledMouth
159+                     scaledMouth,
160+                     scaledLeftEar,
161+                     scaledRightEar,
135162                )
136163            renormalizedDetections.add(
137164                Detection (
@@ -140,18 +167,23 @@ internal class FaceDetector(private val livenessState: LivenessState) {
140167                    scaledRightEye,
141168                    scaledNose,
142169                    scaledMouth,
170+                     scaledLeftEar,
171+                     scaledRightEar,
143172                    detection.score
144173                )
145174            )
146175        }
147176        return  renormalizedDetections
148177    }
149178
150-     private  fun  generateBoundingBoxFromLandmarks (
179+     fun  generateBoundingBoxFromLandmarks (
180+         faceBottom :  Float ,
151181        leftEye :  Landmark ,
152182        rightEye :  Landmark ,
153183        nose :  Landmark ,
154-         mouth :  Landmark 
184+         mouth :  Landmark ,
185+         leftEar :  Landmark ,
186+         rightEar :  Landmark 
155187    ): RectF  {
156188        val  pupilDistance =  calculatePupilDistance(leftEye, rightEye)
157189        val  faceHeight =  calculateFaceHeight(leftEye, rightEye, mouth)
@@ -163,18 +195,16 @@ internal class FaceDetector(private val livenessState: LivenessState) {
163195        val  eyeCenterY =  (leftEye.y +  rightEye.y) /  2 
164196
165197        var  cx =  eyeCenterX
166-         var  cy =  eyeCenterY
167198        val  ovalInfo =  livenessState.faceTargetChallenge
168199        if  (ovalInfo !=  null  &&  eyeCenterY <=  ovalInfo.targetCenterY /  2 ) {
169200            cx =  (eyeCenterX +  nose.x) /  2 
170-             cy =  (eyeCenterY +  nose.y) /  2 
171201        }
172202
173-         val  left  =  cx  -  ow  /   2 
174-         val  top  =  cy  -  oh  /  2 
175-         val  right =  left  +  ow
176-          val  bottom  =  top  +  oh 
177-         return  RectF (left, top, right, bottom )
203+         val  top  =  faceBottom  -  oh 
204+         val  left  =  min(cx  -  ow  /  2 , rightEar.x) 
205+         val  right =  max(cx  +  ow  /   2 , leftEar.x) 
206+ 
207+         return  RectF (left, top, right, faceBottom )
178208    }
179209
180210    private  fun  generateAnchors (): List <Anchor > {
@@ -284,6 +314,8 @@ internal class FaceDetector(private val livenessState: LivenessState) {
284314            var  weightedRightEye =  detection.rightEye
285315            var  weightedNose =  detection.nose
286316            var  weightedMouth =  detection.mouth
317+             var  weightedLeftEar =  detection.leftEar
318+             var  weightedRightEar =  detection.rightEar
287319            if  (candidates.isNotEmpty()) {
288320                var  wXMin =  0.0f 
289321                var  wYMin =  0.0f 
@@ -297,6 +329,10 @@ internal class FaceDetector(private val livenessState: LivenessState) {
297329                var  wNoseY =  0.0f 
298330                var  wMouthX =  0.0f 
299331                var  wMouthY =  0.0f 
332+                 var  wLeftEarX =  0.0f 
333+                 var  wLeftEarY =  0.0f 
334+                 var  wRightEarX =  0.0f 
335+                 var  wRightEarY =  0.0f 
300336                var  totalScore =  0.0f 
301337                candidates.forEach { candidate -> 
302338                    totalScore + =  candidate.score
@@ -305,6 +341,8 @@ internal class FaceDetector(private val livenessState: LivenessState) {
305341                    val  rightEye =  detections[candidate.index].rightEye
306342                    val  nose =  detections[candidate.index].nose
307343                    val  mouth =  detections[candidate.index].mouth
344+                     val  leftEar =  detections[candidate.index].leftEar
345+                     val  rightEar =  detections[candidate.index].rightEar
308346
309347                    wXMin + =  bbox.left *  candidate.score
310348                    wYMin + =  bbox.top *  candidate.score
@@ -321,6 +359,11 @@ internal class FaceDetector(private val livenessState: LivenessState) {
321359
322360                    wMouthX + =  mouth.x *  candidate.score
323361                    wMouthY + =  mouth.y *  candidate.score
362+ 
363+                     wLeftEarX + =  leftEar.x *  candidate.score
364+                     wLeftEarY + =  leftEar.y *  candidate.score
365+                     wRightEarX + =  rightEar.x *  candidate.score
366+                     wRightEarY + =  rightEar.y *  candidate.score
324367                }
325368                weightedLocation.left =  wXMin /  totalScore *  INPUT_SIZE_WIDTH 
326369                weightedLocation.top =  wYMin /  totalScore *  INPUT_SIZE_HEIGHT 
@@ -342,6 +385,13 @@ internal class FaceDetector(private val livenessState: LivenessState) {
342385                val  weightedMouthX =  wMouthX /  totalScore *  INPUT_SIZE_WIDTH 
343386                val  weightedMouthY =  wMouthY /  totalScore *  INPUT_SIZE_HEIGHT 
344387                weightedMouth =  Landmark (weightedMouthX, weightedMouthY)
388+ 
389+                 val  weightedLeftEarX =  wLeftEarX /  totalScore *  INPUT_SIZE_WIDTH 
390+                 val  weightedLeftEarY =  wLeftEarY /  totalScore *  INPUT_SIZE_HEIGHT 
391+                 weightedLeftEar =  Landmark (weightedLeftEarX, weightedLeftEarY)
392+                 val  weightedRightEarX =  wRightEarX /  totalScore *  INPUT_SIZE_WIDTH 
393+                 val  weightedRightEarY =  wRightEarY /  totalScore *  INPUT_SIZE_HEIGHT 
394+                 weightedRightEar =  Landmark (weightedRightEarX, weightedRightEarY)
345395            }
346396            remainedIndexedScores.clear()
347397            remainedIndexedScores.addAll(remained)
@@ -351,6 +401,8 @@ internal class FaceDetector(private val livenessState: LivenessState) {
351401                weightedRightEye,
352402                weightedNose,
353403                weightedMouth,
404+                 weightedLeftEar,
405+                 weightedRightEar,
354406                detection.score
355407            )
356408            outputLocations.add(weightedDetection)
@@ -382,6 +434,8 @@ internal class FaceDetector(private val livenessState: LivenessState) {
382434        val  rightEye :  Landmark ,
383435        val  nose :  Landmark ,
384436        val  mouth :  Landmark ,
437+         val  leftEar :  Landmark ,
438+         val  rightEar :  Landmark ,
385439        val  score :  Float 
386440    )
387441    private  class  IndexedScore (val  index :  Int , val  score :  Float )
@@ -508,11 +562,13 @@ internal class FaceDetector(private val livenessState: LivenessState) {
508562            return  calibratedPupilDistance /  ovalWidth
509563        }
510564
511-         private  fun  calculatePupilDistance (leftEye :  Landmark , rightEye :  Landmark ): Float  {
565+         @VisibleForTesting(VisibleForTesting .PRIVATE )
566+         internal  fun  calculatePupilDistance (leftEye :  Landmark , rightEye :  Landmark ): Float  {
512567            return  sqrt((leftEye.x -  rightEye.x).pow(2 ) +  (leftEye.y -  rightEye.y).pow(2 ))
513568        }
514569
515-         private  fun  calculateFaceHeight (leftEye :  Landmark , rightEye :  Landmark , mouth :  Landmark ):
570+         @VisibleForTesting(VisibleForTesting .PRIVATE )
571+         internal  fun  calculateFaceHeight (leftEye :  Landmark , rightEye :  Landmark , mouth :  Landmark ):
516572            Float  {
517573            val  eyeCenterX =  (leftEye.x +  rightEye.x) /  2 
518574            val  eyeCenterY =  (leftEye.y +  rightEye.y) /  2 
0 commit comments