diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/Facefy.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/Facefy.kt index 502eccc..40e4537 100644 --- a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/Facefy.kt +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/Facefy.kt @@ -2,8 +2,8 @@ package ai.cyberlabs.yoonit.facefy import ai.cyberlabs.yoonit.facefy.model.FaceDetected import ai.cyberlabs.yoonit.facefy.model.FacefyOptions +import android.graphics.RectF import com.google.mlkit.vision.common.InputImage -import java.lang.Exception class Facefy { @@ -27,7 +27,37 @@ class Facefy { field = value } - fun detect(image: InputImage, onFaceDetected: (FaceDetected) -> Unit, onFaceUndetected: (Exception) -> Unit) { - facefyController.detect(image, onFaceDetected, onFaceUndetected) + var roiEnable: Boolean = FacefyOptions.roi.enable + set(value) { + FacefyOptions.roi.enable = value + field = value + } + + var roiRect: RectF = FacefyOptions.roi.rectOffset + set(value) { + FacefyOptions.roi.rectOffset = value + field = value + } + + var roiDetectMinSize: Float = FacefyOptions.roi.minimumSize + set(value) { + FacefyOptions.roi.minimumSize = value + field = value + } + + var detectMinSize: Float = FacefyOptions.detectMinSize + set(value) { + FacefyOptions.detectMinSize = value + field = value + } + + var detectMaxSize: Float = FacefyOptions.detectMaxSize + set(value) { + FacefyOptions.detectMaxSize = value + field = value + } + + fun detect(image: InputImage, onFaceDetected: (FaceDetected) -> Unit, onMessage: (String) -> Unit) { + facefyController.detect(image, onFaceDetected, onMessage) } } \ No newline at end of file diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/FacefyController.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/FacefyController.kt index bb3c98a..7bd51b4 100644 --- a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/FacefyController.kt +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/FacefyController.kt @@ -2,12 +2,15 @@ package ai.cyberlabs.yoonit.facefy import ai.cyberlabs.yoonit.facefy.model.FaceDetected import ai.cyberlabs.yoonit.facefy.model.FacefyOptions +import ai.cyberlabs.yoonit.facefy.model.Message import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import androidx.core.graphics.toRectF import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.FaceDetection import com.google.mlkit.vision.face.FaceDetectorOptions -import java.lang.Exception internal class FacefyController { @@ -28,7 +31,7 @@ internal class FacefyController { fun detect( inputImage: InputImage, onFaceDetected: (FaceDetected) -> Unit, - onFaceUndetected: (Exception) -> Unit + onMessage: (String) -> Unit ) { this.detector .process(inputImage) @@ -42,6 +45,7 @@ internal class FacefyController { closestFace?.let { face -> val faceContours = mutableListOf() + var roiRect = Rect() var leftEyeOpenProbability: Float? = null var rightEyeOpenProbability: Float? = null var smilingProbability: Float? = null @@ -60,6 +64,24 @@ internal class FacefyController { } } + if (FacefyOptions.roi.enable) { + roiRect = Rect( + (inputImage.width * FacefyOptions.roi.rectOffset.left).toInt(), + (inputImage.height * FacefyOptions.roi.rectOffset.top).toInt(), + (inputImage.width - (inputImage.width * FacefyOptions.roi.rectOffset.right)).toInt(), + (inputImage.height - (inputImage.height * FacefyOptions.roi.rectOffset.bottom)).toInt() + ) + } + + val boundingBox: RectF = face.boundingBox.toRectF() + + val message = this.getMessage(boundingBox, inputImage.width, inputImage.height) + + if (message.isNotEmpty()) { + onMessage(message) + return@addOnSuccessListener + } + onFaceDetected( FaceDetected( leftEyeOpenProbability, @@ -69,12 +91,61 @@ internal class FacefyController { face.headEulerAngleY, face.headEulerAngleZ, faceContours, - face.boundingBox + face.boundingBox, + roiRect ) ) + + return@addOnSuccessListener } + + onMessage(Message.FACE_UNDETECTED) + } + .addOnFailureListener { e -> + e.message?.let { message -> onMessage(message) } } - .addOnFailureListener { e -> onFaceUndetected(e) } .addOnCompleteListener { detector.close() } } + + private fun getMessage(boundingBox: RectF, imageWidth: Int, imageHeight: Int): String { + val boundingBoxWidthRelatedWithImage = boundingBox.width() / imageWidth + + if (boundingBoxWidthRelatedWithImage < FacefyOptions.detectMinSize) { + return Message.INVALID_MIN_SIZE + } + + if (boundingBoxWidthRelatedWithImage > FacefyOptions.detectMaxSize) { + return Message.INVALID_MAX_SIZE + } + + val topOffset: Float = boundingBox.top / imageHeight + val rightOffset: Float = (imageWidth - boundingBox.right) / imageWidth + val bottomOffset: Float = (imageHeight - boundingBox.bottom) / imageHeight + val leftOffset: Float = boundingBox.left / imageWidth + + if (FacefyOptions.roi.isOutOf( + topOffset, + rightOffset, + bottomOffset, + leftOffset) + ) { + return Message.INVALID_FACE_OUT_OF_ROI + } + + if (FacefyOptions.roi.hasChanges) { + + // Face is inside the region of interest and faceROI is setted. + // Face is smaller than the defined "minimumSize". + val roiWidth: Float = + imageWidth - + ((FacefyOptions.roi.rectOffset.right + FacefyOptions.roi.rectOffset.left) * imageWidth) + val faceRelatedWithROI: Float = boundingBox.width() / roiWidth + + if (FacefyOptions.roi.minimumSize > faceRelatedWithROI) { + return Message.INVALID_ROI_MIN_SIZE + } + } + + return "" + } } \ No newline at end of file diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FaceDetected.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FaceDetected.kt index b96bfef..55557f0 100644 --- a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FaceDetected.kt +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FaceDetected.kt @@ -11,5 +11,6 @@ data class FaceDetected( var headEulerAngleY: Float, var headEulerAngleZ: Float, var contours: MutableList = mutableListOf(), - var boundingBox: Rect + var boundingBox: Rect, + var roiRect: Rect ) \ No newline at end of file diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FacefyOptions.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FacefyOptions.kt index 98501e0..5b111db 100644 --- a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FacefyOptions.kt +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/FacefyOptions.kt @@ -2,9 +2,25 @@ package ai.cyberlabs.yoonit.facefy.model object FacefyOptions { + var roi: ROI = ROI() + var classification: Boolean = true var contours: Boolean = true var boundingBox: Boolean = true + + /** + * * Limit the minimum face capture size. + * This variable is the face detection box percentage in relation with the UI graphic view. + * The value must be between 0 and 1. + */ + var detectMinSize: Float = 0.0f + + /** + * Limit the maximum face capture size. + * This variable is the face detection box percentage in relation with the UI graphic view. + * The value must be between 0 and 1. + */ + var detectMaxSize: Float = 1.0f } \ No newline at end of file diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/Message.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/Message.kt new file mode 100644 index 0000000..218302b --- /dev/null +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/Message.kt @@ -0,0 +1,17 @@ +package ai.cyberlabs.yoonit.facefy.model + +object Message { + // Face width percentage in relation of the screen width is less than the CaptureOptions.faceCaptureMinSize + const val INVALID_MIN_SIZE = "INVALID_MIN_SIZE" + + // Face width percentage in relation of the screen width is more than the CaptureOptions.faceCaptureMinSize + const val INVALID_MAX_SIZE = "INVALID_MAX_SIZE" + + // Face bounding box is out of the setted region of interest. + const val INVALID_FACE_OUT_OF_ROI = "INVALID_FACE_OUT_OF_ROI" + + // Face width percentage in relation of the screen width is less than the CaptureOptions.FaceROI.minimumSize. + const val INVALID_ROI_MIN_SIZE = "INVALID_ROI_MIN_SIZE" + + const val FACE_UNDETECTED = "FACE_UNDETECTED" +} \ No newline at end of file diff --git a/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/ROI.kt b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/ROI.kt new file mode 100644 index 0000000..665c2a6 --- /dev/null +++ b/yoonit-facefy/src/main/java/ai/cyberlabs/yoonit/facefy/model/ROI.kt @@ -0,0 +1,48 @@ +package ai.cyberlabs.yoonit.facefy.model + +import android.graphics.RectF + +/** + * Model to set face region of interest. + */ +class ROI { + // Enable or disable ROI. + var enable: Boolean = false + + // Region of interest in percentage. + // Values valid [0, 1]. + var rectOffset: RectF = RectF() + + // Minimum face size in percentage in relation of the ROI. + var minimumSize: Float = 0.0f + + // Return if any attributes has modifications. + val hasChanges: Boolean + get() { + return (this.rectOffset.top != 0.0f || + this.rectOffset.right != 0.0f || + this.rectOffset.bottom != 0.0f || + this.rectOffset.left != 0.0f) + } + + /** + * Current offsets is out of the offset parameters. + * + * @param topOffset top offset. + * @param rightOffset right offset. + * @param bottomOffset bottom offset. + * @param leftOffset left offset. + * @return is out of the offset parameters. + */ + fun isOutOf( + topOffset: Float, + rightOffset: Float, + bottomOffset: Float, + leftOffset: Float + ): Boolean { + return (this.rectOffset.top > topOffset || + this.rectOffset.right > rightOffset || + this.rectOffset.bottom > bottomOffset || + this.rectOffset.left > leftOffset) + } +} \ No newline at end of file