diff --git a/.gitignore b/.gitignore index 3c117df..b87cbe2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,12 @@ -# Built application files -*.apk -*.ap_ - -# Files for the ART/Dalvik VM -*.dex - -# Java class files *.class - -# Generated files bin/ gen/ out/ - -# Gradle files .gradle/ build/ - -# Local configuration file (sdk path, etc) local.properties - -# Proguard folder generated by Eclipse proguard/ - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder captures/ - -# IntelliJ *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -.idea/caches - -# Keystore files -# Uncomment the following line if you do not want to check your keystore files in. -#*.jks - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild - -# Google Services (e.g. APIs or Firebase) -google-services.json - -# Freeline -freeline.py -freeline/ -freeline_project_description.json - -# fastlane -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output -fastlane/readme.md \ No newline at end of file +.idea/* +.DS_Store \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 5806fb3..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml deleted file mode 100644 index d819570..0000000 --- a/.idea/markdown-navigator.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml deleted file mode 100644 index 57927c5..0000000 --- a/.idea/markdown-navigator/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ad3cd02..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - Android > Lint > Performance - - - Android > Lint > Security - - - Java - - - Threading issuesJava - - - - - Android - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index f4b4ecb..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d6eb167..7a1215f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,37 +1,25 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: "com.android.application" +apply plugin: "kotlin-android" +apply plugin: "kotlin-android-extensions" android { - compileSdkVersion 28 + compileSdkVersion 30 defaultConfig { applicationId "husaynhakeem.io.facedetectorapp" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 30 versionCode 1 - versionName "1.0" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:design:28.0.0' + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "com.google.android.material:material:1.1.0" // Camera implementation "com.otaliastudios:cameraview:1.5.1" // Android face detector - implementation('com.github.husaynhakeem:android-face-detector:v1.1') { - exclude group: 'com.android.support' - } + implementation(project(":facedetector")) } - -apply plugin: 'com.google.gms.google-services' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fba0cba..df8408e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,6 @@ - diff --git a/app/src/main/java/husaynhakeem/io/facedetectorapp/MainActivity.kt b/app/src/main/java/husaynhakeem/io/facedetectorapp/MainActivity.kt index 0c17172..3bd35f9 100644 --- a/app/src/main/java/husaynhakeem/io/facedetectorapp/MainActivity.kt +++ b/app/src/main/java/husaynhakeem/io/facedetectorapp/MainActivity.kt @@ -1,52 +1,67 @@ package husaynhakeem.io.facedetectorapp import android.os.Bundle -import android.support.v7.app.AppCompatActivity +import android.util.Size +import androidx.appcompat.app.AppCompatActivity import com.otaliastudios.cameraview.Facing import husaynhakeem.io.facedetector.FaceDetector -import husaynhakeem.io.facedetector.models.Frame -import husaynhakeem.io.facedetector.models.Size +import husaynhakeem.io.facedetector.Frame +import husaynhakeem.io.facedetector.LensFacing import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { - private val faceDetector: FaceDetector by lazy { - FaceDetector(facesBoundsOverlay) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - setupCamera() - } - - private fun setupCamera() { - cameraView.addFrameProcessor { - faceDetector.process(Frame( - data = it.data, - rotation = it.rotation, - size = Size(it.size.width, it.size.height), - format = it.format, - isCameraFacingBack = cameraView.facing == Facing.BACK)) - } - revertCameraButton.setOnClickListener { - cameraView.toggleFacing() - } + val lensFacing = + savedInstanceState?.getSerializable(KEY_LENS_FACING) as Facing? ?: Facing.BACK + setupCamera(lensFacing) } override fun onResume() { super.onResume() - cameraView.start() + viewfinder.start() } override fun onPause() { super.onPause() - cameraView.stop() + viewfinder.stop() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putSerializable(KEY_LENS_FACING, viewfinder.facing) + super.onSaveInstanceState(outState) } override fun onDestroy() { super.onDestroy() - cameraView.destroy() + viewfinder.destroy() + } + + private fun setupCamera(lensFacing: Facing) { + val faceDetector = FaceDetector(faceBoundsOverlay) + viewfinder.facing = lensFacing + viewfinder.addFrameProcessor { + faceDetector.process( + Frame( + data = it.data, + rotation = it.rotation, + size = Size(it.size.width, it.size.height), + format = it.format, + lensFacing = if (viewfinder.facing == Facing.BACK) LensFacing.BACK else LensFacing.FRONT + ) + ) + } + + toggleCameraButton.setOnClickListener { + viewfinder.toggleFacing() + } + } + + companion object { + private const val TAG = "MainActivity" + private const val KEY_LENS_FACING = "key-lens-facing" } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2dd4832..029dd92 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,22 +6,23 @@ tools:context=".MainActivity"> - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf1c181..0dd97f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Face Detector Sample + Toggle camera diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..fc07a99 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,11 +1,8 @@ - - diff --git a/build.gradle b/build.gradle index 05ee80a..ec7af6f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.2.51' + ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath "com.android.tools.build:gradle:3.2.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.2.0' } } @@ -17,7 +16,7 @@ allprojects { repositories { google() jcenter() - maven { url 'https://jitpack.io' } + maven { url "https://jitpack.io" } } } diff --git a/facedetector/build.gradle b/facedetector/build.gradle index 778e442..1291f2b 100644 --- a/facedetector/build.gradle +++ b/facedetector/build.gradle @@ -1,13 +1,11 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" android { - compileSdkVersion 28 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" + targetSdkVersion 30 } buildTypes { @@ -17,7 +15,7 @@ android { removeUnusedResources false obfuscate false optimizeCode false - proguardFile 'proguard-rules.pro' + proguardFile "proguard-rules.pro" } } } @@ -25,18 +23,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation 'com.android.support:appcompat-v7:28.0.0' - - // ML Kit - implementation('com.google.firebase:firebase-core:16.0.4') { - exclude group: 'com.android.support' - } - implementation('com.google.firebase:firebase-ml-vision:18.0.1') { - exclude group: 'com.android.support' - } -} - -repositories { - mavenCentral() + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "com.google.mlkit:face-detection:16.0.1" + implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.0" } diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBounds.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBounds.kt new file mode 100644 index 0000000..a4b1a8e --- /dev/null +++ b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBounds.kt @@ -0,0 +1,5 @@ +package husaynhakeem.io.facedetector + +import android.graphics.RectF + +data class FaceBounds(val id: Int?, val box: RectF) \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlay.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlay.kt index abf90b8..d030dca 100644 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlay.kt +++ b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlay.kt @@ -3,37 +3,25 @@ package husaynhakeem.io.facedetector import android.content.Context import android.graphics.Canvas import android.graphics.Paint -import android.graphics.Rect -import android.support.v4.content.ContextCompat +import android.graphics.PointF +import android.graphics.RectF import android.util.AttributeSet import android.view.View -import husaynhakeem.io.facedetector.models.FaceBounds -import husaynhakeem.io.facedetector.models.Facing -import husaynhakeem.io.facedetector.models.Orientation +import androidx.core.content.ContextCompat /** - * A view that renders the results of a face detection process. It contains a list of faces - * bounds which it draws using a set of attributes provided by the camera: Its width, height, - * orientation and facing. These attributes impact how the face bounds are drawn, especially - * the scaling factor between this view and the camera view, and the mirroring of coordinates. + * A [View] that renders the results of a face detection operation. It receives a list of face + * bounds (represented by a list of [RectF]) and draws them, along with their tracking ids. */ -class FaceBoundsOverlay @JvmOverloads constructor( - ctx: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : View(ctx, attrs, defStyleAttr) { - - private val facesBounds: MutableList = mutableListOf() +class FaceBoundsOverlay @JvmOverloads constructor(ctx: Context, attrs: AttributeSet? = null) : + View(ctx, attrs) { + private val facesBounds = mutableListOf() private val anchorPaint = Paint() private val idPaint = Paint() private val boundsPaint = Paint() - var cameraPreviewWidth: Float = 0f - var cameraPreviewHeight: Float = 0f - var cameraOrientation: Orientation = Orientation.ANGLE_270 - var cameraFacing: Facing = Facing.BACK - init { anchorPaint.color = ContextCompat.getColor(context, android.R.color.holo_blue_dark) @@ -45,10 +33,7 @@ class FaceBoundsOverlay @JvmOverloads constructor( boundsPaint.strokeWidth = 4f } - /** - * Repopulates the face bounds list, and calls for a redraw of the view. - */ - fun updateFaces(bounds: List) { + internal fun updateFaces(bounds: List) { facesBounds.clear() facesBounds.addAll(bounds) invalidate() @@ -56,91 +41,34 @@ class FaceBoundsOverlay @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - facesBounds.forEach { - val centerX = computeFaceBoundsCenterX(canvas.width.toFloat(), scaleX(it.box.exactCenterX(), canvas)) - val centerY = computeFaceBoundsCenterY(canvas.height.toFloat(), scaleY(it.box.exactCenterY(), canvas)) - drawAnchor(canvas, centerX, centerY) - drawId(canvas, it.id.toString(), centerX, centerY) - drawBounds(it.box, canvas, centerX, centerY) + facesBounds.forEach { faceBounds -> + canvas.drawAnchor(faceBounds.box.center()) + canvas.drawId(faceBounds.id.toString(), faceBounds.box.center()) + canvas.drawBounds(faceBounds.box) } } - /** - * Calculates the center of the face bounds's X coordinate depending on the camera's facing - * and orientation. A change in the facing results in mirroring the coordinate. - */ - private fun computeFaceBoundsCenterX(viewWidth: Float, scaledCenterX: Float) = when { - cameraFacing == Facing.FRONT && cameraOrientation == Orientation.ANGLE_270 -> viewWidth - scaledCenterX - cameraFacing == Facing.FRONT && cameraOrientation == Orientation.ANGLE_90 -> scaledCenterX - cameraFacing == Facing.BACK && cameraOrientation == Orientation.ANGLE_270 -> viewWidth - scaledCenterX - cameraFacing == Facing.BACK && cameraOrientation == Orientation.ANGLE_90 -> scaledCenterX - else -> scaledCenterX - } - - /** - * Calculates the center of the face bounds's Y coordinate depending on the camera's facing - * and orientation. A change in the facing results in mirroring the coordinate. - */ - private fun computeFaceBoundsCenterY(viewHeight: Float, scaledCenterY: Float) = when { - cameraFacing == Facing.FRONT && cameraOrientation == Orientation.ANGLE_270 -> scaledCenterY - cameraFacing == Facing.FRONT && cameraOrientation == Orientation.ANGLE_90 -> viewHeight - scaledCenterY - cameraFacing == Facing.BACK && cameraOrientation == Orientation.ANGLE_270 -> viewHeight - scaledCenterY - cameraFacing == Facing.BACK && cameraOrientation == Orientation.ANGLE_90 -> scaledCenterY - else -> scaledCenterY + /** Draws an anchor (dot) at the center of a face. */ + private fun Canvas.drawAnchor(center: PointF) { + drawCircle(center.x, center.y, ANCHOR_RADIUS, anchorPaint) } - /** - * Draws an anchor/dot at the center of a face - */ - private fun drawAnchor(canvas: Canvas, centerX: Float, centerY: Float) { - canvas.drawCircle( - centerX, - centerY, - ANCHOR_RADIUS, - anchorPaint) + /** Draws (Writes) the face's id. */ + private fun Canvas.drawId(faceId: String, center: PointF) { + drawText("face id $faceId", center.x - ID_OFFSET, center.y + ID_OFFSET, idPaint) } - /** - * Draws/Writes the face's id - */ - private fun drawId(canvas: Canvas, id: String, centerX: Float, centerY: Float) { - canvas.drawText( - "face id $id", - centerX - ID_OFFSET, - centerY + ID_OFFSET, - idPaint) + /** Draws bounds around a face as a rectangle. */ + private fun Canvas.drawBounds(box: RectF) { + drawRect(box, boundsPaint) } - /** - * Draws bounds around a face as a rectangle - */ - private fun drawBounds(box: Rect, canvas: Canvas, centerX: Float, centerY: Float) { - val xOffset = scaleX(box.width() / 2.0f, canvas) - val yOffset = scaleY(box.height() / 2.0f, canvas) - val left = centerX - xOffset - val right = centerX + xOffset - val top = centerY - yOffset - val bottom = centerY + yOffset - canvas.drawRect( - left, - top, - right, - bottom, - boundsPaint) + private fun RectF.center(): PointF { + val centerX = left + (right - left) / 2 + val centerY = top + (bottom - top) / 2 + return PointF(centerX, centerY) } - /** - * Adjusts a horizontal value x from the camera preview scale to the view scale - */ - private fun scaleX(x: Float, canvas: Canvas) = - x * (canvas.width.toFloat() / cameraPreviewWidth) - - /** - * Adjusts a vertical value y from the camera preview scale to the view scale - */ - private fun scaleY(y: Float, canvas: Canvas) = - y * (canvas.height.toFloat() / cameraPreviewHeight) - companion object { private const val ANCHOR_RADIUS = 10f private const val ID_OFFSET = 50f diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlayHandler.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlayHandler.kt deleted file mode 100644 index 9cc9004..0000000 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceBoundsOverlayHandler.kt +++ /dev/null @@ -1,45 +0,0 @@ -package husaynhakeem.io.facedetector - -import husaynhakeem.io.facedetector.models.Facing -import husaynhakeem.io.facedetector.models.Orientation -import husaynhakeem.io.facedetector.models.convertToFacing -import husaynhakeem.io.facedetector.models.convertToOrientation - - -internal class FaceBoundsOverlayHandler { - - private var previousMin: Float = -1f - private var previousMax: Float = -1f - private var previousOrientation: Orientation = Orientation.ANGLE_0 - private var previousFacing: Facing = Facing.BACK - - fun updateOverlayAttributes(overlayWidth: Int, - overlayHeight: Int, - rotation: Int, - isCameraFacingBack: Boolean, - callback: (Float, Float, Orientation, Facing) -> Unit) { - - val min = Math.min(overlayWidth, overlayHeight).toFloat() - val max = Math.max(overlayWidth, overlayHeight).toFloat() - val orientation = rotation.convertToOrientation() - val facing = isCameraFacingBack.convertToFacing() - - if (previousMin == min && previousMax == max && previousOrientation == orientation && facing == previousFacing) { - return - } - - previousMin = min - previousMax = max - previousOrientation = orientation - previousFacing = facing - - when (orientation) { - Orientation.ANGLE_0, Orientation.ANGLE_180 -> { - callback(max, min, orientation, facing) - } - Orientation.ANGLE_90, Orientation.ANGLE_270 -> { - callback(min, max, orientation, facing) - } - } - } -} \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceDetector.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceDetector.kt index 5a3deb0..a84e55d 100644 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceDetector.kt +++ b/facedetector/src/main/java/husaynhakeem/io/facedetector/FaceDetector.kt @@ -1,64 +1,175 @@ package husaynhakeem.io.facedetector -import android.widget.Toast -import com.google.firebase.ml.vision.common.FirebaseVisionImage -import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata -import com.google.firebase.ml.vision.face.FirebaseVisionFace -import husaynhakeem.io.facedetector.models.FaceBounds -import husaynhakeem.io.facedetector.models.Frame +import android.graphics.RectF +import android.os.Looper +import android.util.Log +import android.view.View +import androidx.annotation.GuardedBy +import com.google.android.gms.common.util.concurrent.HandlerExecutor +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.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors class FaceDetector(private val faceBoundsOverlay: FaceBoundsOverlay) { - private val faceBoundsOverlayHandler = FaceBoundsOverlayHandler() - private val firebaseFaceDetectorWrapper = FirebaseFaceDetectorWrapper() + private val mlkitFaceDetector = FaceDetection.getClient( + FaceDetectorOptions.Builder() + .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE) + .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) + .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) + .setMinFaceSize(MIN_FACE_SIZE) + .enableTracking() + .build() + ) - fun process(frame: Frame) { - updateOverlayAttributes(frame) - detectFacesIn(frame) + /** Listener that gets notified when a face detection result is ready. */ + private var onFaceDetectionResultListener: OnFaceDetectionResultListener? = null + + /** [Executor] used to run the face detection on a background thread. */ + private lateinit var faceDetectionExecutor: ExecutorService + + /** [Executor] used to trigger the rendering of the detected face bounds on the UI thread. */ + private val mainExecutor = HandlerExecutor(Looper.getMainLooper()) + + /** Controls access to [isProcessing], since it can be accessed from different threads. */ + private val lock = Object() + + @GuardedBy("lock") + private var isProcessing = false + + init { + faceBoundsOverlay.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View?) { + faceDetectionExecutor = Executors.newSingleThreadExecutor() + } + + override fun onViewDetachedFromWindow(view: View?) { + if (::faceDetectionExecutor.isInitialized) { + faceDetectionExecutor.shutdown() + } + } + }) } - private fun updateOverlayAttributes(frame: Frame) { - faceBoundsOverlayHandler.updateOverlayAttributes( - overlayWidth = frame.size.width, - overlayHeight = frame.size.height, - rotation = frame.rotation, - isCameraFacingBack = frame.isCameraFacingBack, - callback = { newWidth, newHeight, newOrientation, newFacing -> - faceBoundsOverlay.cameraPreviewWidth = newWidth - faceBoundsOverlay.cameraPreviewHeight = newHeight - faceBoundsOverlay.cameraOrientation = newOrientation - faceBoundsOverlay.cameraFacing = newFacing - }) + /** Sets a listener to receive face detection result callbacks. */ + fun setonFaceDetectionFailureListener(listener: OnFaceDetectionResultListener) { + onFaceDetectionResultListener = listener } - private fun detectFacesIn(frame: Frame) { - frame.data?.let { - firebaseFaceDetectorWrapper.process( - image = convertFrameToImage(frame), - onSuccess = { - faceBoundsOverlay.updateFaces(convertToListOfFaceBounds(it)) - }, - onError = { - Toast.makeText(faceBoundsOverlay.context, "Error processing images: $it", Toast.LENGTH_LONG).show() - }) + /** + * Kick-starts a face detection operation on a camera frame. If a previous face detection + * operation is still ongoing, the frame is dropped until the face detector is no longer busy. + */ + fun process(frame: Frame) { + synchronized(lock) { + if (!isProcessing) { + isProcessing = true + if (!::faceDetectionExecutor.isInitialized) { + val exception = + IllegalStateException("Cannot run face detection. Make sure the face " + + "bounds overlay is attached to the current window.") + onError(exception) + } else { + faceDetectionExecutor.execute { frame.detectFaces() } + } + } } } - private fun convertFrameToImage(frame: Frame) = - FirebaseVisionImage.fromByteArray(frame.data!!, extractFrameMetadata(frame)) + private fun Frame.detectFaces() { + val data = data ?: return + val inputImage = InputImage.fromByteArray(data, size.width, size.height, rotation, format) + mlkitFaceDetector.process(inputImage) + .addOnSuccessListener { faces -> + synchronized(lock) { + isProcessing = false + } + + // Correct the detected faces so that they're correctly rendered on the UI, then + // pass them to [faceBoundsOverlay] to be drawn. + val faceBounds = faces.map { face -> face.toFaceBounds(this) } + mainExecutor.execute { faceBoundsOverlay.updateFaces(faceBounds) } + } + .addOnFailureListener { exception -> + synchronized(lock) { + isProcessing = false + } + onError(exception) + } + } - private fun extractFrameMetadata(frame: Frame): FirebaseVisionImageMetadata = - FirebaseVisionImageMetadata.Builder() - .setWidth(frame.size.width) - .setHeight(frame.size.height) - .setFormat(frame.format) - .setRotation(frame.rotation / RIGHT_ANGLE) - .build() + /** + * Converts a [Face] to an instance of [FaceBounds] while correctly transforming the face's + * bounding box by scaling it to match the overlay and mirroring it when the lens facing + * represents the front facing camera. + */ + private fun Face.toFaceBounds(frame: Frame): FaceBounds { + // In order to correctly display the face bounds, the orientation of the processed image + // (frame) and that of the overlay have to match. Which is why the dimensions of + // the analyzed image are reversed if its rotation is 90 or 270. + val reverseDimens = frame.rotation == 90 || frame.rotation == 270 + val width = if (reverseDimens) frame.size.height else frame.size.width + val height = if (reverseDimens) frame.size.width else frame.size.height - private fun convertToListOfFaceBounds(faces: MutableList): List = - faces.map { FaceBounds(it.trackingId, it.boundingBox) } + // Since the analyzed image (frame) probably has a different resolution (width and height) + // compared to the overlay view, we compute by how much we have to scale the bounding box + // so that it is displayed correctly on the overlay. + val scaleX = faceBoundsOverlay.width.toFloat() / width + val scaleY = faceBoundsOverlay.height.toFloat() / height + + // If the front camera lens is being used, reverse the right/left coordinates + val isFrontLens = frame.lensFacing == LensFacing.FRONT + val flippedLeft = if (isFrontLens) width - boundingBox.right else boundingBox.left + val flippedRight = if (isFrontLens) width - boundingBox.left else boundingBox.right + + // Scale all coordinates to match the overlay + val scaledLeft = scaleX * flippedLeft + val scaledTop = scaleY * boundingBox.top + val scaledRight = scaleX * flippedRight + val scaledBottom = scaleY * boundingBox.bottom + val scaledBoundingBox = RectF(scaledLeft, scaledTop, scaledRight, scaledBottom) + + // Return the scaled bounding box and a tracking id of the detected face. The tracking id + // remains the same as long as the same face continues to be detected. + return FaceBounds( + trackingId, + scaledBoundingBox + ) + } + + private fun onError(exception: Exception) { + onFaceDetectionResultListener?.onFailure(exception) + Log.e(TAG, "An error occurred while running a face detection", exception) + } + + /** + * Interface containing callbacks that are invoked when the face detection process succeeds or + * fails. + */ + interface OnFaceDetectionResultListener { + /** + * Signals that the face detection process has successfully completed for a camera frame. + * It also provides the result of the face detection for further potential processing. + * + * @param faceBounds Detected faces from a camera frame + */ + fun onSuccess(faceBounds: List) {} + + /** + * Invoked when an error is encountered while attempting to detect faces in a camera frame. + * + * @param exception Encountered [Exception] while attempting to detect faces in a camera + * frame. + */ + fun onFailure(exception: Exception) {} + } companion object { - private const val RIGHT_ANGLE = 90 + private const val TAG = "FaceDetector" + private const val MIN_FACE_SIZE = 0.15F } } \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/FirebaseFaceDetectorWrapper.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/FirebaseFaceDetectorWrapper.kt deleted file mode 100644 index 5d29cb0..0000000 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/FirebaseFaceDetectorWrapper.kt +++ /dev/null @@ -1,44 +0,0 @@ -package husaynhakeem.io.facedetector - -import android.util.Log -import com.google.firebase.ml.vision.FirebaseVision -import com.google.firebase.ml.vision.common.FirebaseVisionImage -import com.google.firebase.ml.vision.face.FirebaseVisionFace -import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetector -import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions - - -internal class FirebaseFaceDetectorWrapper { - - private val faceDetectorOptions: FirebaseVisionFaceDetectorOptions by lazy { - FirebaseVisionFaceDetectorOptions.Builder() - .setPerformanceMode(FirebaseVisionFaceDetectorOptions.ACCURATE) - .setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS) - .setClassificationMode(FirebaseVisionFaceDetectorOptions.NO_CLASSIFICATIONS) - .setMinFaceSize(MIN_FACE_SIZE) - .enableTracking() - .build() - } - - private val faceDetector: FirebaseVisionFaceDetector by lazy { - FirebaseVision.getInstance().getVisionFaceDetector(faceDetectorOptions) - } - - fun process(image: FirebaseVisionImage, - onSuccess: (MutableList) -> Unit, - onError: (Exception) -> Unit) { - faceDetector.detectInImage(image) - .addOnSuccessListener { - onSuccess(it) - } - .addOnFailureListener { - onError(it) - Log.e(TAG, "Error processing images: $it") - } - } - - companion object { - private val TAG = FirebaseFaceDetectorWrapper::class.java.simpleName - private const val MIN_FACE_SIZE = 0.15f - } -} \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/LensFacing.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/LensFacing.kt new file mode 100644 index 0000000..4c7e924 --- /dev/null +++ b/facedetector/src/main/java/husaynhakeem/io/facedetector/LensFacing.kt @@ -0,0 +1,3 @@ +package husaynhakeem.io.facedetector + +enum class LensFacing { BACK, FRONT } \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/models.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/models.kt new file mode 100644 index 0000000..a8607bd --- /dev/null +++ b/facedetector/src/main/java/husaynhakeem/io/facedetector/models.kt @@ -0,0 +1,11 @@ +package husaynhakeem.io.facedetector + +import android.util.Size + +data class Frame( + @Suppress("ArrayInDataClass") val data: ByteArray?, + val rotation: Int, + val size: Size, + val format: Int, + val lensFacing: LensFacing +) \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Facing.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Facing.kt deleted file mode 100644 index 112afc2..0000000 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Facing.kt +++ /dev/null @@ -1,12 +0,0 @@ -package husaynhakeem.io.facedetector.models - - -enum class Facing(val value: Int) { - BACK(0), - FRONT(1) -} - -internal fun Boolean.convertToFacing() = when (this) { - true -> Facing.BACK - false -> Facing.FRONT -} \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Orientation.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Orientation.kt deleted file mode 100644 index e84c5f7..0000000 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/Orientation.kt +++ /dev/null @@ -1,17 +0,0 @@ -package husaynhakeem.io.facedetector.models - - -enum class Orientation { - ANGLE_0, - ANGLE_90, - ANGLE_180, - ANGLE_270 -} - -internal fun Int.convertToOrientation() = when (this) { - 0 -> Orientation.ANGLE_0 - 90 -> Orientation.ANGLE_90 - 180 -> Orientation.ANGLE_180 - 270 -> Orientation.ANGLE_270 - else -> Orientation.ANGLE_270 -} \ No newline at end of file diff --git a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/models.kt b/facedetector/src/main/java/husaynhakeem/io/facedetector/models/models.kt deleted file mode 100644 index 3e3d94f..0000000 --- a/facedetector/src/main/java/husaynhakeem/io/facedetector/models/models.kt +++ /dev/null @@ -1,15 +0,0 @@ -package husaynhakeem.io.facedetector.models - -import android.graphics.Rect - - -data class FaceBounds(val id: Int, val box: Rect) - -data class Frame( - val data: ByteArray?, - val rotation: Int, - val size: Size, - val format: Int, - val isCameraFacingBack: Boolean) - -data class Size(val width: Int, val height: Int) \ No newline at end of file