Skip to content

Commit a806a53

Browse files
committed
Implement Android Facial Recognition
Inject it
1 parent f5af5ef commit a806a53

3 files changed

Lines changed: 100 additions & 3 deletions

File tree

app/src/main/kotlin/com/darkrockstudios/app/securecamera/AppModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import com.darkrockstudios.app.securecamera.camera.ThumbnailCache
88
import com.darkrockstudios.app.securecamera.gallery.GalleryViewModel
99
import com.darkrockstudios.app.securecamera.import.ImportPhotosViewModel
1010
import com.darkrockstudios.app.securecamera.introduction.IntroductionViewModel
11+
import com.darkrockstudios.app.securecamera.obfuscation.FacialDetection
12+
import com.darkrockstudios.app.securecamera.obfuscation.MlFacialDetection
1113
import com.darkrockstudios.app.securecamera.obfuscation.ObfuscatePhotoViewModel
1214
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
1315
import com.darkrockstudios.app.securecamera.security.DeviceInfo
@@ -52,6 +54,7 @@ val appModule = module {
5254
factoryOf(::VerifyPinUseCase)
5355
factoryOf(::CreatePinUseCase)
5456
factoryOf(::PinSizeUseCase)
57+
factoryOf(::MlFacialDetection) bind FacialDetection::class
5558

5659
viewModelOf(::ObfuscatePhotoViewModel)
5760
viewModelOf(::ViewPhotoViewModel)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.darkrockstudios.app.securecamera.obfuscation
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.PointF
5+
import android.graphics.Rect
6+
import android.media.FaceDetector
7+
import androidx.core.graphics.createBitmap
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.withContext
10+
import timber.log.Timber
11+
12+
class AndroidFacialDetection : FacialDetection {
13+
override suspend fun processForFaces(bitmap: Bitmap): List<FacialDetection.FoundFace> =
14+
withContext(Dispatchers.Default) {
15+
// Android's FaceDetector requires RGB_565 format
16+
val bitmapForDetection = if (bitmap.config != Bitmap.Config.RGB_565) {
17+
try {
18+
createBitmap(bitmap.width, bitmap.height, Bitmap.Config.RGB_565).also { targetBitmap ->
19+
val canvas = android.graphics.Canvas(targetBitmap)
20+
canvas.drawBitmap(bitmap, 0f, 0f, null)
21+
}
22+
} catch (e: Exception) {
23+
Timber.e(e, "Failed to convert bitmap to RGB_565")
24+
return@withContext emptyList<FacialDetection.FoundFace>()
25+
}
26+
} else {
27+
bitmap
28+
}
29+
30+
// Maximum number of faces to detect
31+
val maxFaces = 100
32+
33+
try {
34+
val detector = FaceDetector(bitmapForDetection.width, bitmapForDetection.height, maxFaces)
35+
val faces = arrayOfNulls<FaceDetector.Face>(maxFaces)
36+
37+
// Find faces in the bitmap
38+
val facesFound = detector.findFaces(bitmapForDetection, faces)
39+
Timber.d("Found $facesFound faces using Android FaceDetector")
40+
41+
// Convert the detected faces to our FoundFace format
42+
val foundFaces = mutableListOf<FacialDetection.FoundFace>()
43+
44+
for (i in 0 until facesFound) {
45+
faces[i]?.let { face ->
46+
val midPoint = PointF()
47+
face.getMidPoint(midPoint)
48+
49+
// Calculate the bounding box based on the face's position and confidence
50+
val eyeDistance = face.eyesDistance()
51+
val confidence = face.confidence()
52+
53+
// Use eye distance to estimate face size
54+
// The multipliers are approximations and may need adjustment
55+
val faceWidth = (eyeDistance * 2.5f).toInt()
56+
val faceHeight = (eyeDistance * 3.0f).toInt()
57+
58+
val left = (midPoint.x - faceWidth / 2).toInt().coerceAtLeast(0)
59+
val top = (midPoint.y - faceHeight / 2).toInt().coerceAtLeast(0)
60+
val right = (midPoint.x + faceWidth / 2).toInt().coerceAtMost(bitmap.width)
61+
val bottom = (midPoint.y + faceHeight / 2).toInt().coerceAtMost(bitmap.height)
62+
63+
val boundingBox = Rect(left, top, right, bottom)
64+
65+
// Calculate approximate eye positions based on the midpoint and eye distance
66+
val leftEyeX = midPoint.x - eyeDistance / 2
67+
val rightEyeX = midPoint.x + eyeDistance / 2
68+
val eyeY = midPoint.y - eyeDistance / 8 // Slight adjustment for eye height
69+
70+
val eyes = FacialDetection.FoundFace.Eyes(
71+
left = PointF(leftEyeX, eyeY),
72+
right = PointF(rightEyeX, eyeY)
73+
)
74+
75+
foundFaces.add(
76+
FacialDetection.FoundFace(
77+
boundingBox = boundingBox,
78+
eyes = eyes
79+
)
80+
)
81+
}
82+
}
83+
84+
// Clean up if we created a new bitmap
85+
if (bitmapForDetection != bitmap) {
86+
bitmapForDetection.recycle()
87+
}
88+
89+
foundFaces
90+
} catch (e: Exception) {
91+
Timber.e(e, "Error detecting faces with Android FaceDetector")
92+
emptyList()
93+
}
94+
}
95+
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/obfuscation/ObfuscatePhotoViewModel.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ import timber.log.Timber
1818

1919
class ObfuscatePhotoViewModel(
2020
private val appContext: Context,
21-
private val imageManager: SecureImageRepository
21+
private val imageManager: SecureImageRepository,
22+
private val facialDetection: FacialDetection,
2223
) : BaseViewModel<ObfuscatePhotoUiState>() {
2324

24-
private val facialDetection = MlFacialDetection()
25-
2625
override fun createState() = ObfuscatePhotoUiState()
2726

2827
fun toggleRegionObfuscation(index: Int) {

0 commit comments

Comments
 (0)