Face Detector Box in Face Recognition Attendance Application Is Inaccurate, How to Fix It?

I am developing a face recognition attendance application. The application successfully displays a face detector box, but the accuracy of the face detection is not satisfactory. The detector often misidentifies faces or fails to detect faces in certain conditions. I am looking for ways to improve the accuracy of the face detection in my application. Here are some specifics:

The face detector sometimes detects non-facial objects as faces.
The face detector box is directed towards the face, but it only detects half of the face.
I am using TensorFlow and ML Kit Face Detector for face detection.

I would appreciate any advice or solutions on how to enhance the accuracy of the face detection feature in my application.

I have tried implementing the following code to improve face detection accuracy:

Here is the XML:

        <!-- PreviewView for camera -->
        <androidx.camera.view.PreviewView
            android:id="@+id/previewView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.example.absensiapp.Drawing.OverlayView
            android:id="@+id/tracking_overlay"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/transparent" />

Here is the backend code:

class RegistrasiFaceActivity : AppCompatActivity() {

    private lateinit var previewView: PreviewView
    private lateinit var registerButton: Button
    private lateinit var switchCameraButton: Button
    private lateinit var overlayView: OverlayView
    private lateinit var cameraExecutor: ExecutorService
    private var isUsingFrontCamera = true

    private val detector: FaceDetector by lazy {
        val highAccuracyOpts = FaceDetectorOptions.Builder()
            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
            .build()
        FaceDetection.getClient(highAccuracyOpts)
    }

    private var faceClassifier: FaceClassifier? = null
    private var registerFace = false
    private var isProcessingFrame = false
    private lateinit var tracker: MultiBoxTracker

    companion object {
        private const val PERMISSIONS_REQUEST_CODE = 121
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_registrasi_face)

        previewView = findViewById(R.id.previewView)
        overlayView = findViewById(R.id.tracking_overlay)
        registerButton = findViewById(R.id.registerButton)
        switchCameraButton = findViewById(R.id.switchCameraButton)

        cameraExecutor = Executors.newSingleThreadExecutor()
        tracker = MultiBoxTracker(this)

        // Initialize FACE Recognition
        CoroutineScope(Dispatchers.Main).launch {
            try {
                faceClassifier = TFLiteFaceRecognition.create(
                    assets,
                    "mobile_face_net.tflite",
                    112, // pastikan input size sesuai
                    false,
                    applicationContext
                )
            } catch (e: IOException) {
                e.printStackTrace()
                Toast.makeText(applicationContext, "Classifier could not be initialized", Toast.LENGTH_SHORT).show()
                finish()
            }
        }

        checkAndRequestPermissions()

        registerButton.setOnClickListener {
            registerFace = true
        }

        switchCameraButton.setOnClickListener {
            isUsingFrontCamera = !isUsingFrontCamera
            bindCameraUseCases()
        }
    }

    private fun checkAndRequestPermissions() {
        val permissionsNeeded = mutableListOf<String>()
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            permissionsNeeded.add(Manifest.permission.CAMERA)
        }
        if (permissionsNeeded.isNotEmpty()) {
            ActivityCompat.requestPermissions(this, permissionsNeeded.toTypedArray(), PERMISSIONS_REQUEST_CODE)
        } else {
            bindCameraUseCases()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
                bindCameraUseCases()
            } else {
                Toast.makeText(this, "Permissions denied", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun bindCameraUseCases() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(if (isUsingFrontCamera) CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK)
                .build()

            val imageAnalyzer = ImageAnalysis.Builder()
                .setTargetResolution(Size(640, 480))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor, { imageProxy -> processImageProxy(imageProxy) })
                }

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))
    }

    @SuppressLint("UnsafeOptInUsageError")
    private fun processImageProxy(imageProxy: ImageProxy) {
        if (isProcessingFrame) {
            imageProxy.close()
            return
        }
        isProcessingFrame = true

        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        val image = imageProxy.image ?: run {
            imageProxy.close()
            isProcessingFrame = false
            return
        }

        val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val paint = android.graphics.Paint().apply {
            color = android.graphics.Color.RED
            style = android.graphics.Paint.Style.STROKE
            strokeWidth = 5.0f
        }
        canvas.drawBitmap(bitmap, 0f, 0f, paint)

        val inputImage = InputImage.fromMediaImage(image, rotationDegrees)
        detector.process(inputImage)
            .addOnSuccessListener { faces ->
                processFaces(faces, bitmap, image.width, image.height)
                imageProxy.close()
                isProcessingFrame = false
            }
            .addOnFailureListener { e ->
                e.printStackTrace()
                imageProxy.close()
                isProcessingFrame = false
            }
    }

    private fun processFaces(faces: List<Face>, bitmap: Bitmap, imageWidth: Int, imageHeight: Int) {
        if (faces.isEmpty()) {
            return
        }

        val face = faces.first()
        val bounds = face.boundingBox

        // Pastikan kotak batas berada dalam dimensi bitmap
        val left = bounds.left.coerceAtLeast(0)
        val top = bounds.top.coerceAtLeast(0)
        val right = bounds.right.coerceAtMost(bitmap.width)
        val bottom = bounds.bottom.coerceAtMost(bitmap.height)

        val width = right - left
        val height = bottom - top

        if (width <= 0 || height <= 0) {
            return
        }

        // Potong wajah dari bitmap asli
        val crop = Bitmap.createBitmap(bitmap, left, top, width, height)

        // Ubah ukuran bitmap sesuai dengan ukuran yang diharapkan oleh model (112x112 dalam kasus ini)
        val scaledBitmap = Bitmap.createScaledBitmap(crop, 112, 112, false)

        // Gunakan bitmap yang telah diubah ukurannya langsung dengan fungsi recognizeImage
        val result = faceClassifier?.recognizeImage(scaledBitmap, registerFace)

        val rectF = RectF(bounds)

        // Calculate scale to transform bounding box to screen coordinates
        val scaleX = overlayView.width / imageWidth.toFloat()
        val scaleY = overlayView.height / imageHeight.toFloat()

        // Apply scale to bounding box
        rectF.left *= scaleX
        rectF.top *= scaleY
        rectF.right *= scaleX
        rectF.bottom *= scaleY

        val recognition = FaceClassifier.Recognition(face.trackingId.toString(), "", 0f, rectF)

        tracker.setFrameConfiguration(imageWidth, imageHeight, 0)

        // Bersihkan callback sebelumnya sebelum menambahkan yang baru
        overlayView.clearCallbacks()

        tracker.trackResults(listOf(recognition), System.currentTimeMillis())
        overlayView.addCallback(object : OverlayView.DrawCallback {
            override fun drawCallback(canvas: Canvas) {
                tracker.draw(canvas)
            }
        })
        overlayView.postInvalidate()

        if (registerFace) {
            showRegisterDialog(scaledBitmap, result!!)
        }
    }

    private fun showRegisterDialog(bitmap: Bitmap, recognition: FaceClassifier.Recognition) {
        val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
            .setView(R.layout.dialog_register)
            .create()

        dialog.setOnShowListener {
            val imageView: ImageView = dialog.findViewById(R.id.dialog_image)!!
            val registerButton: Button = dialog.findViewById(R.id.dialog_register_button)!!

            imageView.setImageBitmap(bitmap)
            registerButton.setOnClickListener {
                CoroutineScope(Dispatchers.IO).launch {
                    faceClassifier?.register("Face", recognition)
                    withContext(Dispatchers.Main) {
                        Toast.makeText(this@RegistrasiFaceActivity, "Face Registered Successfully", Toast.LENGTH_SHORT).show()
                        dialog.dismiss()
                    }
                }
            }
        }

        dialog.show()
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
        detector.close()
    }
}

This is the TFLiteFaceRecognition code :

class TFLiteFaceRecognition private constructor(context: Context) : FaceClassifier {

    companion object {
        private const val OUTPUT_SIZE = 192
        private const val IMAGE_MEAN = 127.5f
        private const val IMAGE_STD = 127.5f
        private const val WIDTH = 112
        private const val HEIGHT = 112

        @Throws(IOException::class)
        private fun loadModelFile(assets: AssetManager, modelFilename: String): MappedByteBuffer {
            val fileDescriptor = assets.openFd(modelFilename)
            val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
            val fileChannel = inputStream.channel
            val startOffset = fileDescriptor.startOffset
            val declaredLength = fileDescriptor.declaredLength
            return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
        }

        @Throws(IOException::class)
        suspend fun create(
            assetManager: AssetManager,
            modelFilename: String,
            inputSize: Int,
            isQuantized: Boolean,
            context: Context
        ): FaceClassifier {
            return TFLiteFaceRecognition(context).apply {
                this.inputSize = inputSize

                try {
                    tfLite = Interpreter(loadModelFile(assetManager, modelFilename))
                } catch (e: Exception) {
                    throw RuntimeException(e)
                }

                this.isModelQuantized = isQuantized
                val numBytesPerChannel = if (isQuantized) 1 else 4
                imgData = ByteBuffer.allocateDirect(1 * inputSize * inputSize * 3 * numBytesPerChannel)
                imgData.order(ByteOrder.nativeOrder())
                intValues = IntArray(inputSize * inputSize)
            }
        }
    }

    private var isModelQuantized = false
    private var inputSize = 0
    private lateinit var intValues: IntArray
    private lateinit var embeddings: Array<FloatArray>
    private lateinit var imgData: ByteBuffer
    private lateinit var tfLite: Interpreter
    private val registered = HashMap<String, FaceClassifier.Recognition>()

    interface ApiService {
        @POST("api/update-profile")
        suspend fun insertFace(@Body requestBody: FaceRequest)
    }

    data class FaceRequest(val name: String, val embedding: String)

    private val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    private val apiService: ApiService = retrofit.create(ApiService::class.java)

    override fun register(name: String, rec: FaceClassifier.Recognition) {
        CoroutineScope(Dispatchers.IO).launch {
            insertFaceToDatabase(name, rec.embedding as FloatArray)
            registered[name] = rec
        }
    }

    private fun findNearest(emb: FloatArray): Pair<String, Float>? {
        var ret: Pair<String, Float>? = null
        for ((name, value) in registered) {
            val knownEmb = (value.embedding as Array<FloatArray>)[0]
            var distance = 0f
            for (i in emb.indices) {
                val diff = emb[i] - knownEmb[i]
                distance += diff * diff
            }
            distance = kotlin.math.sqrt(distance)
            if (ret == null || distance < ret.second) {
                ret = Pair(name, distance)
            }
        }
        return ret
    }

    private fun imageToArray(bitmap: Bitmap): Array<Array<Array<FloatArray>>> {
        val resizedBitmap = Bitmap.createScaledBitmap(bitmap, WIDTH, HEIGHT, true)
        val intValues = IntArray(WIDTH * HEIGHT)
        resizedBitmap.getPixels(intValues, 0, WIDTH, 0, 0, WIDTH, HEIGHT)
        val floatValues = FloatArray(WIDTH * HEIGHT * 3)
        for (i in intValues.indices) {
            val value = intValues[i]
            floatValues[i * 3 + 0] = ((value shr 16 and 0xFF) - IMAGE_MEAN) / IMAGE_STD
            floatValues[i * 3 + 1] = ((value shr 8 and 0xFF) - IMAGE_MEAN) / IMAGE_STD
            floatValues[i * 3 + 2] = ((value and 0xFF) - IMAGE_MEAN) / IMAGE_STD
        }
        return Array(1) { Array(WIDTH) { Array(HEIGHT) { FloatArray(3) } }.also { array ->
            for (y in 0 until HEIGHT) {
                for (x in 0 until WIDTH) {
                    for (c in 0 until 3) {
                        array[y][x][c] = floatValues[(y * WIDTH + x) * 3 + c]
                    }
                }
            }
        }}
    }

    override fun recognizeImage(bitmap: Bitmap, storeExtra: Boolean): FaceClassifier.Recognition {
        val inputArray = imageToArray(bitmap)
        val outputArray = Array(1) { FloatArray(OUTPUT_SIZE) }

        tfLite.run(inputArray, outputArray)

        var distance = Float.MAX_VALUE
        var id = "0"
        var label = "?"

        if (registered.isNotEmpty()) {
            val nearest = findNearest(outputArray[0])
            if (nearest != null) {
                val name = nearest.first
                label = name
                distance = nearest.second
            }
        }

        val rec = FaceClassifier.Recognition(
            id,
            label,
            distance,
            RectF()
        )

        if (storeExtra) {
            rec.embedding = outputArray
        }

        return rec
    }

    private suspend fun insertFaceToDatabase(name: String, embedding: FloatArray) {
        withContext(Dispatchers.IO) {
            val embeddingString = embedding.joinToString(",")
            val request = FaceRequest(name, embeddingString)
            apiService.insertFace(request)
        }
    }
}

This is the ImageUtils code

/** Utility class for manipulating images. */
object ImageUtils {
    // This value is 2 ^ 18 - 1, and is used to clamp the RGB values before their ranges
    // are normalized to eight bits.
    private const val kMaxChannelValue = 262143

    /**
     * Utility method to compute the allocated size in bytes of a YUV420SP image of the given
     * dimensions.
     */
    fun getYUVByteSize(width: Int, height: Int): Int {
        // The luminance plane requires 1 byte per pixel.
        val ySize = width * height

        // The UV plane works on 2x2 blocks, so dimensions with odd size must be rounded up.
        // Each 2x2 block takes 2 bytes to encode, one each for U and V.
        val uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2

        return ySize + uvSize
    }

    /**
     * Saves a Bitmap object to disk for analysis.
     *
     * @param bitmap The bitmap to save.
     */
    fun saveBitmap(bitmap: Bitmap) {
        saveBitmap(bitmap, "preview.png")
    }

    /**
     * Saves a Bitmap object to disk for analysis.
     *
     * @param bitmap The bitmap to save.
     * @param filename The location to save the bitmap to.
     */
    fun saveBitmap(bitmap: Bitmap, filename: String) {
        val root = Environment.getExternalStorageDirectory().absolutePath + File.separator + "tensorflow"
        //LOGGER.i("Saving %dx%d bitmap to %s.", bitmap.getWidth(), bitmap.getHeight(), root)
        val myDir = File(root)

        if (!myDir.mkdirs()) {
            //LOGGER.i("Make dir failed")
        }

        val fname = filename
        val file = File(myDir, fname)
        if (file.exists()) {
            file.delete()
        }
        try {
            val out = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.PNG, 99, out)
            out.flush()
            out.close()
        } catch (e: Exception) {
            //LOGGER.e(e, "Exception!")
        }
    }

    fun convertYUV420SPToARGB8888(input: ByteArray, width: Int, height: Int, output: IntArray) {
        val frameSize = width * height
        for (j in 0 until height) {
            var uvp = frameSize + (j shr 1) * width
            var u = 0
            var v = 0

            for (i in 0 until width) {
                val y = 0xff and input[j * width + i].toInt()
                if (i and 1 == 0) {
                    v = 0xff and input[uvp++].toInt()
                    u = 0xff and input[uvp++].toInt()
                }

                output[j * width + i] = YUV2RGB(y, u, v)
            }
        }
    }

    private fun YUV2RGB(y: Int, u: Int, v: Int): Int {
        var y = y
        var u = u
        var v = v
        y = if (y - 16 < 0) 0 else y - 16
        u -= 128
        v -= 128

        val y1192 = 1192 * y
        var r = y1192 + 1634 * v
        var g = y1192 - 833 * v - 400 * u
        var b = y1192 + 2066 * u

        r = if (r > kMaxChannelValue) kMaxChannelValue else if (r < 0) 0 else r
        g = if (g > kMaxChannelValue) kMaxChannelValue else if (g < 0) 0 else g
        b = if (b > kMaxChannelValue) kMaxChannelValue else if (b < 0) 0 else b

        return -0x1000000 or (r shl 6 and 0xff0000) or (g shr 2 and 0xff00) or (b shr 10 and 0xff)
    }

    fun convertYUV420ToARGB8888(
        yData: ByteArray,
        uData: ByteArray,
        vData: ByteArray,
        width: Int,
        height: Int,
        yRowStride: Int,
        uvRowStride: Int,
        uvPixelStride: Int,
        out: IntArray
    ) {
        var yp = 0
        for (j in 0 until height) {
            val pY = yRowStride * j
            val pUV = uvRowStride * (j shr 1)

            for (i in 0 until width) {
                val uvOffset = pUV + (i shr 1) * uvPixelStride

                out[yp++] = YUV2RGB(
                    0xff and yData[pY + i].toInt(),
                    0xff and uData[uvOffset].toInt(),
                    0xff and vData[uvOffset].toInt()
                )
            }
        }
    }

    fun getTransformationMatrix(
        srcWidth: Int,
        srcHeight: Int,
        dstWidth: Int,
        dstHeight: Int,
        applyRotation: Int,
        maintainAspectRatio: Boolean
    ): Matrix {
        val matrix = Matrix()

        if (applyRotation != 0) {
            if (applyRotation % 90 != 0) {
                //LOGGER.w("Rotation of %d % 90 != 0", applyRotation)
            }

            // Translate so center of image is at origin.
            matrix.postTranslate(-srcWidth / 2.0f, -srcHeight / 2.0f)

            // Rotate around origin.
            matrix.postRotate(applyRotation.toFloat())
        }

        val transpose = (Math.abs(applyRotation) + 90) % 180 == 0

        val inWidth = if (transpose) srcHeight else srcWidth
        val inHeight = if (transpose) srcWidth else srcHeight

        if (inWidth != dstWidth || inHeight != dstHeight) {
            val scaleFactorX = dstWidth / inWidth.toFloat()
            val scaleFactorY = dstHeight / inHeight.toFloat()

            if (maintainAspectRatio) {
                val scaleFactor = Math.max(scaleFactorX, scaleFactorY)
                matrix.postScale(scaleFactor, scaleFactor)
            } else {
                matrix.postScale(scaleFactorX, scaleFactorY)
            }
        }

        if (applyRotation != 0) {
            matrix.postTranslate(dstWidth / 2.0f, dstHeight / 2.0f)
        }

        return matrix
    }
}

This is FaceClassifier code :

interface FaceClassifier {

    fun register(name: String, recognition: Recognition)

    fun recognizeImage(bitmap: Bitmap, getExtra: Boolean): Recognition

    class Recognition(
        val id: String? = null,
        val title: String?,
        val distance: Float? = null,
        var location: RectF? = null,
        var embedding: Any? = null,
        var crop: Bitmap? = null
    ) {

        constructor(title: String?, embedding: Any?) : this(
            id = null,
            title = title,
            distance = null,
            location = null,
            embedding = embedding,
            crop = null
        )

        fun updateEmbedding(extra: Any?) {
            this.embedding = extra
        }

        override fun toString(): String {
            var resultString = ""
            if (id != null) {
                resultString += "[$id] "
            }

            if (title != null) {
                resultString += "$title "
            }

            if (distance != null) {
                resultString += String.format("(%.1f%%) ", distance * 100.0f)
            }

            if (location != null) {
                resultString += "$location "
            }

            return resultString.trim()
        }
    }
}

This is Overlay code :

class OverlayView(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private val callbacks = mutableListOf<DrawCallback>()

    fun addCallback(callback: DrawCallback) {
        callbacks.add(callback)
    }

    fun clearCallbacks() {
        callbacks.clear()
    }

    override fun draw(canvas: Canvas) {
        super.draw(canvas)
        for (callback in callbacks) {
            callback.drawCallback(canvas)
        }
    }

    /** Interface defining the callback for client classes. */
    interface DrawCallback {
        fun drawCallback(canvas: Canvas)
    }
}

This is MultiboxTracker code :

class MultiBoxTracker(context: Context) {
    private val screenRects = mutableListOf<Pair<Float, RectF>>()
    private val availableColors = LinkedList<Int>()
    private val trackedObjects = mutableListOf<TrackedRecognition>()
    private val boxPaint = Paint()
    private var frameToCanvasMatrix: Matrix? = null
    private var frameWidth = 0
    private var frameHeight = 0
    private var sensorOrientation = 0

    init {
        for (color in COLORS) {
            availableColors.add(color)
        }

        boxPaint.color = Color.RED
        boxPaint.style = Paint.Style.STROKE
        boxPaint.strokeWidth = 10.0f
        boxPaint.strokeCap = Paint.Cap.ROUND
        boxPaint.strokeJoin = Paint.Join.ROUND
        boxPaint.strokeMiter = 100f
    }

    @Synchronized
    fun setFrameConfiguration(width: Int, height: Int, sensorOrientation: Int) {
        frameWidth = width
        frameHeight = height
        this.sensorOrientation = sensorOrientation
    }

    @Synchronized
    fun drawDebug(canvas: Canvas) {
        val boxPaint = Paint().apply {
            color = Color.RED
            alpha = 200
            style = Paint.Style.STROKE
        }

        for (detection in screenRects) {
            val rect = detection.second
            canvas.drawRect(rect, boxPaint)
        }
    }

    @Synchronized
    fun trackResults(results: List<FaceClassifier.Recognition>, timestamp: Long) {
        processResults(results)
    }

    private fun getFrameToCanvasMatrix(): Matrix {
        return frameToCanvasMatrix ?: Matrix()
    }

    @Synchronized
    fun draw(canvas: Canvas) {
        val rotated = sensorOrientation % 180 == 90
        val multiplier = minOf(
            canvas.height / (if (rotated) frameWidth else frameHeight).toFloat(),
            canvas.width / (if (rotated) frameHeight else frameWidth).toFloat()
        )
        frameToCanvasMatrix = ImageUtils.getTransformationMatrix(
            frameWidth,
            frameHeight,
            (multiplier * (if (rotated) frameHeight else frameWidth)).toInt(),
            (multiplier * (if (rotated) frameWidth else frameHeight)).toInt(),
            sensorOrientation,
            false
        )

        for (recognition in trackedObjects) {
            val trackedPos = RectF(recognition.location)

            getFrameToCanvasMatrix().mapRect(trackedPos)
            boxPaint.color = recognition.color

            // Gambar kotak deteksi wajah tanpa judul
            canvas.drawRect(trackedPos, boxPaint)
        }
    }

    private fun processResults(results: List<FaceClassifier.Recognition>) {
        val rectsToTrack = LinkedList<Pair<Float, FaceClassifier.Recognition>>()

        screenRects.clear()
        val rgbFrameToScreen = Matrix(getFrameToCanvasMatrix())

        for (result in results) {
            if (result.location == null) {
                continue
            }
            val detectionFrameRect = RectF(result.location)

            val detectionScreenRect = RectF()
            rgbFrameToScreen.mapRect(detectionScreenRect, detectionFrameRect)

            screenRects.add(Pair(result.distance ?: 0f, detectionScreenRect))

            if (detectionFrameRect.width() < MIN_SIZE || detectionFrameRect.height() < MIN_SIZE) {
                continue
            }

            rectsToTrack.add(Pair(result.distance ?: 0f, result))
        }

        trackedObjects.clear()
        if (rectsToTrack.isEmpty()) {
            return
        }

        for (potential in rectsToTrack) {
            val trackedRecognition = TrackedRecognition()
            trackedRecognition.detectionConfidence = potential.first
            trackedRecognition.location = RectF(potential.second.location)
            trackedRecognition.color = COLORS[trackedObjects.size]
            trackedObjects.add(trackedRecognition)

            if (trackedObjects.size >= COLORS.size) {
                break
            }
        }
    }

    private class TrackedRecognition {
        var location: RectF? = null
        var detectionConfidence: Float = 0f
        var color: Int = 0
    }

    companion object {
        private const val MIN_SIZE = 16.0f
        private val COLORS = intArrayOf(
            Color.BLUE,
            Color.RED,
            Color.GREEN,
            Color.YELLOW,
            Color.CYAN,
            Color.MAGENTA,
            Color.WHITE,
            Color.parseColor("#55FF55"),
            Color.parseColor("#FFA500"),
            Color.parseColor("#FF8888"),
            Color.parseColor("#AAAAFF"),
            Color.parseColor("#FFFFAA"),
            Color.parseColor("#55AAAA"),
            Color.parseColor("#AA33AA"),
            Color.parseColor("#0D0068")
        )
    }
}

Despite this, the accuracy has not improved as expected. I also recorded a screen capture of the application to illustrate the issue:

Click to watch my video

This is my project on GitHub

I expected the face detector to accurately detect the full face without misidentifying non-facial objects. However, the detector still only identifies half of the face and sometimes detects non-facial objects as faces.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật