Skip to content

Commit

Permalink
Face detector (#5)
Browse files Browse the repository at this point in the history
* Add face detection sample using CameraX + MLKit

* Resize art

* Update README.md
  • Loading branch information
husaynhakeem authored Jun 30, 2020
1 parent 1a294e8 commit 3729180
Show file tree
Hide file tree
Showing 23 changed files with 833 additions and 2 deletions.
9 changes: 9 additions & 0 deletions FaceDetectorSample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/*
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
1 change: 1 addition & 0 deletions FaceDetectorSample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
38 changes: 38 additions & 0 deletions FaceDetectorSample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"

android {
compileSdkVersion 30
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.husaynhakeem.facedetectorsample"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
}

// CameraX uses java8 features
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:1.3.0"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.activity:activity-ktx:1.2.0-alpha06"
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha06"

// CameraX
implementation "androidx.camera:camera-camera2:1.0.0-beta06"
implementation "androidx.camera:camera-lifecycle:1.0.0-beta06"
implementation "androidx.camera:camera-view:1.0.0-alpha13"

// MLKit
implementation "com.google.mlkit:face-detection:16.0.0"
}
21 changes: 21 additions & 0 deletions FaceDetectorSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.husaynhakeem.facedetectorsample">

<!-- For using the camera -->
<uses-permission android:name="android.permission.CAMERA" />

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.husaynhakeem.facedetectorsample

import android.annotation.SuppressLint
import android.graphics.Rect
import android.graphics.RectF
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
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.FaceDetector

/** CameraX Analyzer that wraps MLKit's FaceDetector. */
internal class AnalysisFaceDetector(
private val previewWidth: Int,
private val previewHeight: Int,
private val isFrontLens: Boolean
) : ImageAnalysis.Analyzer {

private val faceDetector: FaceDetector = FaceDetection.getClient()

/** Listener to receive callbacks for when faces are detected, or an error occurs. */
var listener: Listener? = null

@SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(imageProxy: ImageProxy) {
val image = imageProxy.image
if (image == null) {
imageProxy.close()
return
}

val rotation = imageProxy.imageInfo.rotationDegrees
val inputImage = InputImage.fromMediaImage(image, rotation)
faceDetector
.process(inputImage)
.addOnSuccessListener { faces: List<Face> ->
val listener = listener ?: return@addOnSuccessListener

// In order to correctly display the face bounds, the orientation of the analyzed
// image and that of the viewfinder have to match. Which is why the dimensions of
// the analyzed image are reversed if its rotation information is 90 or 270.
val reverseDimens = rotation == 90 || rotation == 270
val width = if (reverseDimens) imageProxy.height else imageProxy.width
val height = if (reverseDimens) imageProxy.width else imageProxy.height

val faceBounds = faces.map { it.boundingBox.transform(width, height) }
listener.onFacesDetected(faceBounds)
imageProxy.close()
}
.addOnFailureListener { exception ->
listener?.onError(exception)
imageProxy.close()
}
}

private fun Rect.transform(width: Int, height: Int): RectF {
val scaleX = previewWidth / width.toFloat()
val scaleY = previewHeight / height.toFloat()

// If the front camera lens is being used, reverse the right/left coordinates
val flippedLeft = if (isFrontLens) width - right else left
val flippedRight = if (isFrontLens) width - left else right

// Scale all coordinates to match preview
val scaledLeft = scaleX * flippedLeft
val scaledTop = scaleY * top
val scaledRight = scaleX * flippedRight
val scaledBottom = scaleY * bottom
return RectF(scaledLeft, scaledTop, scaledRight, scaledBottom)
}

/**
* Interface to register callbacks for when the face detector provides detected face bounds, or
* when it encounters an error.
*/
internal interface Listener {
/** Callback that receives face bounds that can be drawn on top of the viewfinder. */
fun onFacesDetected(faceBounds: List<RectF>)

/** Invoked when an error is encounter during face detection. */
fun onError(exception: Exception)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.husaynhakeem.facedetectorsample

import android.app.Application
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import java.util.concurrent.ExecutionException

/** ViewModel that provides a [ProcessCameraProvider] to interact with the camera. */
class CameraViewModel(application: Application) : AndroidViewModel(application) {

private val _getProcessCameraProvider by lazy {
MutableLiveData<ProcessCameraProvider>().apply {
val cameraProviderFuture = ProcessCameraProvider.getInstance(getApplication())
cameraProviderFuture.addListener(
Runnable {
try {
value = cameraProviderFuture.get()
} catch (exception: ExecutionException) {
throw IllegalStateException(
"Failed to retrieve a ProcessCameraProvider instance", exception
)
} catch (exception: InterruptedException) {
throw IllegalStateException(
"Failed to retrieve a ProcessCameraProvider instance", exception
)
}
},
ContextCompat.getMainExecutor(getApplication())
)
}
}
val processCameraProvider: LiveData<ProcessCameraProvider>
get() = _getProcessCameraProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.husaynhakeem.facedetectorsample

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat

/** Overlay where face bounds are drawn. */
class FaceBoundsOverlay constructor(context: Context?, attributeSet: AttributeSet?) :
View(context, attributeSet) {

private val faceBounds: MutableList<RectF> = mutableListOf()
private val paint = Paint().apply {
style = Paint.Style.STROKE
color = ContextCompat.getColor(context!!, android.R.color.black)
strokeWidth = 10f
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
faceBounds.forEach { canvas.drawRect(it, paint) }
}

fun drawFaceBounds(faceBounds: List<RectF>) {
this.faceBounds.clear()
this.faceBounds.addAll(faceBounds)
invalidate()
}
}
Loading

0 comments on commit 3729180

Please sign in to comment.