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