Skip to content

Commit 4b83294

Browse files
authored
Add tflite for face-mask-detection (#1)
1 parent f6f627b commit 4b83294

File tree

10 files changed

+401
-27
lines changed

10 files changed

+401
-27
lines changed

Diff for: app/build.gradle

+13-1
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,21 @@ android {
3333
kotlinOptions {
3434
jvmTarget = '1.8'
3535
}
36+
buildFeatures {
37+
mlModelBinding true
38+
}
3639
}
3740

3841
dependencies {
39-
42+
// Kotlin
4043
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
4144
implementation 'androidx.core:core-ktx:1.3.0'
45+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4'
46+
// Android UI
4247
implementation 'androidx.appcompat:appcompat:1.1.0'
4348
implementation 'com.google.android.material:material:1.1.0'
4449
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
50+
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
4551
// The following line is optional, as the core library is included indirectly by camera-camera2
4652
implementation "androidx.camera:camera-core:${camerax_version}"
4753
implementation "androidx.camera:camera-camera2:${camerax_version}"
@@ -51,6 +57,12 @@ dependencies {
5157
implementation "androidx.camera:camera-view:${camerax_view_version}"
5258
// If you want to additionally use the CameraX Extensions library
5359
implementation "androidx.camera:camera-extensions:${camerax_view_version}"
60+
// If you want to use tflite and work with image detection
61+
implementation "org.tensorflow:tensorflow-lite-support:${tflite}"
62+
implementation "org.tensorflow:tensorflow-lite-metadata:${tflite}"
63+
// If you want to get GPU support
64+
implementation 'org.tensorflow:tensorflow-lite-gpu:2.2.0'
65+
5466
testImplementation 'junit:junit:4.13'
5567
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
5668
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

Diff for: app/src/main/java/io/face/mask/detection/MainActivity.kt

+100-25
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package io.face.mask.detection
22

33
import android.Manifest
4+
import android.annotation.SuppressLint
5+
import android.content.Context
46
import android.content.pm.PackageManager
57
import android.content.res.Configuration
8+
import android.graphics.Bitmap
9+
import android.graphics.Matrix
10+
import android.media.Image
611
import android.os.Bundle
712
import android.util.DisplayMetrics
813
import android.util.Log
@@ -13,18 +18,24 @@ import androidx.camera.core.*
1318
import androidx.camera.lifecycle.ProcessCameraProvider
1419
import androidx.core.app.ActivityCompat
1520
import androidx.core.content.ContextCompat
21+
import androidx.lifecycle.lifecycleScope
1622
import com.google.common.util.concurrent.ListenableFuture
23+
import io.face.mask.detection.ml.FackMaskDetection
1724
import kotlinx.android.synthetic.main.activity_main.*
18-
import java.nio.ByteBuffer
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.launch
27+
import org.tensorflow.lite.support.image.TensorImage
28+
import org.tensorflow.lite.support.label.Category
29+
import org.tensorflow.lite.support.model.Model
1930
import java.util.concurrent.ExecutorService
2031
import java.util.concurrent.Executors
2132
import kotlin.math.abs
2233
import kotlin.math.max
2334
import kotlin.math.min
2435

25-
typealias AnalyzerListener = (pixel: Double) -> Unit
36+
typealias CameraBitmapOutputListener = (bitmap: Bitmap) -> Unit
2637

27-
class MainActivity : AppCompatActivity() {
38+
class MainActivity : AppCompatActivity(R.layout.activity_main) {
2839
private var preview: Preview? = null
2940
private var imageAnalyzer: ImageAnalysis? = null
3041
private var cameraProvider: ProcessCameraProvider? = null
@@ -37,7 +48,7 @@ class MainActivity : AppCompatActivity() {
3748

3849
override fun onCreate(savedInstanceState: Bundle?) {
3950
super.onCreate(savedInstanceState)
40-
setContentView(R.layout.activity_main)
51+
setupML()
4152

4253
setupCameraThread()
4354
setupCameraControllers()
@@ -49,7 +60,6 @@ class MainActivity : AppCompatActivity() {
4960
}
5061
}
5162

52-
5363
private fun setupCameraThread() {
5464
cameraExecutor = Executors.newSingleThreadExecutor()
5565
}
@@ -72,7 +82,7 @@ class MainActivity : AppCompatActivity() {
7282
CameraSelector.LENS_FACING_FRONT
7383
}
7484
setLensButtonIcon()
75-
bindCameraUseCases()
85+
setupCameraUseCases()
7686
}
7787

7888
try {
@@ -105,7 +115,7 @@ class MainActivity : AppCompatActivity() {
105115
}
106116

107117

108-
private fun bindCameraUseCases() {
118+
private fun setupCameraUseCases() {
109119
// Lens-facing selector
110120
val cameraSelector: CameraSelector =
111121
CameraSelector.Builder().requireLensFacing(lensFacing)
@@ -127,8 +137,8 @@ class MainActivity : AppCompatActivity() {
127137
.setTargetRotation(rotation)
128138
.build()
129139
.also {
130-
it.setAnalyzer(cameraExecutor, AppImageAnalysis { luma ->
131-
Log.d(TAG, "Average luminosity: $luma")
140+
it.setAnalyzer(cameraExecutor, BitmapOutputAnalysis(applicationContext) { bitmap ->
141+
setupMLOutput(bitmap)
132142
})
133143
}
134144

@@ -146,6 +156,7 @@ class MainActivity : AppCompatActivity() {
146156
}
147157
}
148158

159+
149160
private fun setupCamera() {
150161
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
151162
ProcessCameraProvider.getInstance(this)
@@ -154,13 +165,13 @@ class MainActivity : AppCompatActivity() {
154165
cameraProvider = cameraProviderFuture.get()
155166

156167
lensFacing = when {
157-
hasBackCamera -> CameraSelector.LENS_FACING_BACK
158168
hasFrontCamera -> CameraSelector.LENS_FACING_FRONT
169+
hasBackCamera -> CameraSelector.LENS_FACING_BACK
159170
else -> throw IllegalStateException("No cameras on this devices")
160171
}
161172

162173
setupCameraControllers()
163-
bindCameraUseCases()
174+
setupCameraUseCases()
164175
}, ContextCompat.getMainExecutor(this))
165176
}
166177

@@ -204,6 +215,45 @@ class MainActivity : AppCompatActivity() {
204215
return AspectRatio.RATIO_16_9
205216
}
206217

218+
private lateinit var faceMaskDetection: FackMaskDetection
219+
220+
private fun setupML() {
221+
val options: Model.Options =
222+
Model.Options.Builder().setDevice(Model.Device.GPU).setNumThreads(5).build()
223+
faceMaskDetection = FackMaskDetection.newInstance(applicationContext, options)
224+
}
225+
226+
private fun setupMLOutput(bitmap: Bitmap) {
227+
val tensorImage: TensorImage = TensorImage.fromBitmap(bitmap)
228+
val result: FackMaskDetection.Outputs = faceMaskDetection.process(tensorImage)
229+
val output: List<Category> =
230+
result.probabilityAsCategoryList.apply {
231+
sortByDescending { res -> res.score }
232+
}
233+
lifecycleScope.launch(Dispatchers.Main) {
234+
output.firstOrNull()?.let { category ->
235+
tv_output.text = category.label
236+
tv_output.setTextColor(
237+
ContextCompat.getColor(
238+
applicationContext,
239+
if (category.label == "without_mask") R.color.red else R.color.green
240+
)
241+
)
242+
overlay.background = getDrawable(
243+
if (category.label == "without_mask") R.drawable.red_border else R.drawable.green_border
244+
)
245+
246+
pb_output.progressTintList = AppCompatResources.getColorStateList(
247+
applicationContext,
248+
if (category.label == "without_mask") R.color.red else R.color.green
249+
)
250+
pb_output.progress = (category.score * 100).toInt()
251+
252+
Log.d(TAG, output.toString())
253+
}
254+
}
255+
}
256+
207257
companion object {
208258
private const val TAG = "Face-Mask-Detection"
209259
private const val REQUEST_CODE_PERMISSIONS = 0x98
@@ -213,23 +263,48 @@ class MainActivity : AppCompatActivity() {
213263
}
214264
}
215265

216-
private class AppImageAnalysis(private val listener: AnalyzerListener) :
266+
private class BitmapOutputAnalysis(
267+
context: Context,
268+
private val listener: CameraBitmapOutputListener
269+
) :
217270
ImageAnalysis.Analyzer {
218271

219-
private fun ByteBuffer.toByteArray(): ByteArray {
220-
rewind() // Rewind the buffer to zero
221-
val data = ByteArray(remaining())
222-
get(data) // Copy the buffer into a byte array
223-
return data // Return the byte array
224-
}
272+
private val yuvToRgbConverter = YuvToRgbConverter(context)
273+
private lateinit var bitmapBuffer: Bitmap
274+
private lateinit var rotationMatrix: Matrix
275+
276+
@SuppressLint("UnsafeExperimentalUsageError")
277+
private fun ImageProxy.toBitmap(): Bitmap? {
225278

226-
override fun analyze(image: ImageProxy) {
227-
val buffer: ByteBuffer = image.planes[0].buffer
228-
val data: ByteArray = buffer.toByteArray()
229-
val pixels: List<Int> = data.map { it.toInt() and 0xFF }
230-
val pixelsAverage: Double = pixels.average()
279+
val image: Image = this.image ?: return null
231280

232-
listener(pixelsAverage)
233-
image.close()
281+
if (!::bitmapBuffer.isInitialized) {
282+
rotationMatrix = Matrix()
283+
rotationMatrix.postRotate(this.imageInfo.rotationDegrees.toFloat())
284+
bitmapBuffer = Bitmap.createBitmap(
285+
this.width, this.height, Bitmap.Config.ARGB_8888
286+
)
287+
}
288+
289+
// Pass image to an image analyser
290+
yuvToRgbConverter.yuvToRgb(image, bitmapBuffer)
291+
292+
// Create the Bitmap in the correct orientation
293+
return Bitmap.createBitmap(
294+
bitmapBuffer,
295+
0,
296+
0,
297+
bitmapBuffer.width,
298+
bitmapBuffer.height,
299+
rotationMatrix,
300+
false
301+
)
302+
}
303+
304+
override fun analyze(imageProxy: ImageProxy) {
305+
imageProxy.toBitmap()?.let {
306+
listener(it)
307+
}
308+
imageProxy.close()
234309
}
235310
}

0 commit comments

Comments
 (0)