Skip to content

Commit

Permalink
on-device face recognizer prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
solrudev committed Apr 17, 2024
0 parents commit 3486bed
Show file tree
Hide file tree
Showing 84 changed files with 2,384 additions and 0 deletions.
78 changes: 78 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# 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
.idea/

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
keystore.properties

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# 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

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

# Android Profiling
*.hprof
2 changes: 2 additions & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build
/release
134 changes: 134 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.util.Locale
import java.util.Properties

val packageName = "ru.solrudev.facerecognizer"

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.ksp)
alias(dagger.plugins.hilt.plugin)
alias(androidx.plugins.navigation.safeargs)
alias(libs.plugins.android.cachefix)
}

kotlin {
jvmToolchain(17)
}

base {
archivesName.set(packageName)
}

android {
compileSdk = 34
buildToolsVersion = "34.0.0"
namespace = packageName

defaultConfig {
applicationId = packageName
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}

val releaseSigningConfig by signingConfigs.registering {
initWith(signingConfigs["debug"])
val keystorePropertiesFile = rootProject.file("keystore.properties")
if (keystorePropertiesFile.exists()) {
val keystoreProperties = Properties().apply {
keystorePropertiesFile.inputStream().use(::load)
}
keyAlias = keystoreProperties["keyAlias"] as? String
keyPassword = keystoreProperties["keyPassword"] as? String
storeFile = keystoreProperties["storeFile"]?.let(::file)
storePassword = keystoreProperties["storePassword"] as? String
enableV3Signing = true
enableV4Signing = true
}
}

buildTypes {
named("debug") {
multiDexEnabled = true
}
named("release") {
isMinifyEnabled = true
isShrinkResources = true
signingConfig = releaseSigningConfig.get()
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

splits {
abi {
isEnable = true
reset()
//noinspection ChromeOsAbiSupport
include("arm64-v8a")
isUniversalApk = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

buildFeatures {
mlModelBinding = true
viewBinding = true
}
}

tasks.withType<KotlinJvmCompile>().configureEach {
compilerOptions {
freeCompilerArgs.add("-Xjvm-default=all")
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

androidComponents {
onVariants(selector().all()) { variant ->
afterEvaluate {
val variantName = variant.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
tasks.getByName<KotlinCompile>("ksp${variantName}Kotlin") {
setSource(tasks.getByName("generate${variantName}MlModelClass").outputs)
}
}
}
}

dependencies {
ksp(dagger.bundles.hilt.compilers)

implementation(dagger.hilt.android)
implementation(androidx.activity.ktx)
implementation(androidx.fragment.ktx)
implementation(androidx.bundles.navigation)
implementation(androidx.bundles.camera)
implementation(libs.materialcomponents)
implementation(libs.kotlinx.coroutines)
implementation(libs.viewbindingpropertydelegate)
implementation(libs.insetter)
implementation(libs.mlkit.facedetection)
implementation(libs.bundles.tensorflow)

debugImplementation(androidx.multidex)

testImplementation(libs.kotlin.test)
testImplementation(libs.bundles.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(androidx.test.ext.junit)
androidTestImplementation(androidx.espresso.core)
}
6 changes: 6 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute Source

#### Miscellaneous
-keep,allowobfuscation,allowshrinking class org.tensorflow.lite.gpu.GpuDelegateFactory$Options$GpuBackend
-keep,allowobfuscation,allowshrinking class org.tensorflow.lite.gpu.GpuDelegateFactory$Options
35 changes: 35 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".MainApplication"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FaceRecognizer"
tools:ignore="UnusedAttribute">

<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.FaceRecognizer.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>
</manifest>
56 changes: 56 additions & 0 deletions app/src/main/kotlin/ru/solrudev/facerecognizer/FaceDetector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@file:OptIn(TransformExperimental::class)

package ru.solrudev.facerecognizer

import android.graphics.Bitmap
import android.graphics.RectF
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import androidx.camera.view.TransformExperimental
import androidx.camera.view.transform.CoordinateTransform
import com.google.mlkit.vision.common.InputImage
import ru.solrudev.facerecognizer.util.await
import ru.solrudev.facerecognizer.util.crop
import ru.solrudev.facerecognizer.util.rotateCounterclockwise
import javax.inject.Inject
import com.google.mlkit.vision.face.FaceDetector as MlKitVisionFaceDetector

interface FaceDetector : AutoCloseable {
suspend fun detect(
image: ImageProxy,
rotationDegrees: Int,
previewTransform: CoordinateTransform
): List<DetectedFace>
}

class MlKitFaceDetector @Inject constructor(private val faceDetector: MlKitVisionFaceDetector) : FaceDetector {

@OptIn(ExperimentalGetImage::class)
override suspend fun detect(
image: ImageProxy,
rotationDegrees: Int,
previewTransform: CoordinateTransform
): List<DetectedFace> {
val inputImage = InputImage.fromMediaImage(image.image!!, rotationDegrees)
return faceDetector
.process(inputImage)
.await()
.mapNotNull { face ->
val rotatedFaceBoundingBox = face.boundingBox
val faceBoundingBox = rotatedFaceBoundingBox.rotateCounterclockwise(
image.width, image.height, rotationDegrees
)
val faceBitmap = image.toBitmap().crop(faceBoundingBox, rotationDegrees) ?: return@mapNotNull null
previewTransform.mapRect(faceBoundingBox)
val faceId = face.trackingId ?: error("Face tracking is disabled")
DetectedFace(faceId, faceBitmap, faceBoundingBox)
}
}

override fun close() {
faceDetector.close()
}
}

data class DetectedFace(val id: Int, val bitmap: Bitmap, val boundingBox: RectF)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ru.solrudev.facerecognizer

import javax.inject.Inject
import javax.inject.Singleton

interface FaceEmbeddingsRepository {
suspend fun addFaceEmbeddings(faceEmbeddings: FaceEmbeddings)
suspend fun getAllFaceEmbeddings(): List<FaceEmbeddings>
}

@Singleton
class InMemoryFaceEmbeddingsRepository @Inject constructor() : FaceEmbeddingsRepository {

private val faceEmbeddingsList = mutableListOf<FaceEmbeddings>()

override suspend fun addFaceEmbeddings(faceEmbeddings: FaceEmbeddings) {
faceEmbeddingsList += faceEmbeddings
}

override suspend fun getAllFaceEmbeddings(): List<FaceEmbeddings> {
return faceEmbeddingsList
}
}

data class FaceEmbeddings(val name: String, val embeddings: FloatArray) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FaceEmbeddings
if (name != other.name) return false
return embeddings.contentEquals(other.embeddings)
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + embeddings.contentHashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.solrudev.facerecognizer

import android.graphics.Bitmap
import android.graphics.Rect

data class FaceRecognition(val face: RecognizedFace, val boundingBox: Rect, val bitmap: Bitmap)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ru.solrudev.facerecognizer

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import ru.solrudev.facerecognizer.di.DefaultDispatcher
import javax.inject.Inject

interface FaceRecognitionCoroutineScope : CoroutineScope

class FaceRecognitionCoroutineScopeImpl @Inject constructor(
@DefaultDispatcher dispatcher: CoroutineDispatcher
) : FaceRecognitionCoroutineScope, CoroutineScope by CoroutineScope(dispatcher)
Loading

0 comments on commit 3486bed

Please sign in to comment.