Skip to main content
FaceNet Android offers several configuration options to balance accuracy and performance based on your needs.

Choosing the FaceNet model

The app provides two FaceNet models with different embedding dimensions:
  • facenet.tflite: Outputs a 128-dimensional embedding (smaller, faster)
  • facenet_512.tflite: Outputs a 512-dimensional embedding (larger, more accurate)

Switching models

1

Open FaceNet.kt

Navigate to app/src/main/java/com/ml/shubham0204/facenet_android/domain/embeddings/FaceNet.kt
2

Change the model file

Update the Interpreter initialization to load your preferred model:
FaceNet.kt
// For facenet (128-dimensional)
interpreter = Interpreter(
    FileUtil.loadMappedFile(context, "facenet.tflite"), 
    interpreterOptions
)

// For facenet-512 (512-dimensional)
interpreter = Interpreter(
    FileUtil.loadMappedFile(context, "facenet_512.tflite"), 
    interpreterOptions
)
3

Update the embedding dimension

Change the embeddingDim variable to match your model:
FaceNet.kt
// For facenet
private val embeddingDim = 128

// For facenet-512
private val embeddingDim = 512
4

Update the database schema

In app/src/main/java/com/ml/shubham0204/facenet_android/data/DataModels.kt, update the @HnswIndex annotation:
DataModels.kt
@Entity
data class FaceImageRecord(
    @Id var recordID: Long = 0,
    @Index var personID: Long = 0,
    var personName: String = "",
    // Update dimensions to match your model
    @HnswIndex(dimensions = 512) // or 128
    var faceEmbedding: FloatArray = floatArrayOf()
)
The 512-dimensional model generally provides better accuracy but requires more computation and storage.

Model sources

Both FaceNet models are sourced from the deepface library and converted to TFLite format:
from deepface import DeepFace
from deepface.models.facial_recognition.Facenet import scaling
import tensorflow as tf

model = DeepFace.build_model("Facenet512")
model.model.save("facenet512.keras")

model = tf.keras.models.load_model("facenet512.keras", custom_objects={
    "scaling": scaling
})
converter_fp16 = tf.lite.TFLiteConverter.from_keras_model(model)
converter_fp16.optimizations = [tf.lite.Optimize.DEFAULT]
converter_fp16.target_spec.supported_types = [tf.float16]
tflite_model_fp16 = converter_fp16.convert()

with open("facenet_512.tflite", "wb") as file:
    file.write(tflite_model_fp16)

Choosing the face detector

The app supports two face detection backends:
  • MLKit: Google’s MLKit Face Detection API
  • Mediapipe: Google’s Mediapipe Face Detection solution

Switching face detectors

1

Open AppModule.kt

Navigate to app/src/main/java/com/ml/shubham0204/facenet_android/di/AppModule.kt
2

Toggle the detector

Set the isMLKit variable:
AppModule.kt
@Module
@ComponentScan("com.ml.shubham0204.facenet_android")
class AppModule {

    // Set to true for MLKit, false for Mediapipe
    private var isMLKit = true

    @Single
    fun provideFaceDetector(context: Context): BaseFaceDetector = if (isMLKit) {
        MLKitFaceDetector(context)
    } else {
        MediapipeFaceDetector(context)
    }
}
Both detectors work well, but you may find one performs better in specific lighting conditions or with certain face angles.
ObjectBox provides two search modes for finding nearest neighbors:

HNSW search (default)

  • Type: Approximate Nearest Neighbor (ANN)
  • Performance: Fast, especially for large datasets
  • Accuracy: Approximate results, may not always return the true nearest neighbor
  • Best for: Datasets with many face embeddings (50+ people)

Flat search (precise)

  • Type: Exact Nearest Neighbor
  • Performance: Slower, performs linear search across all records
  • Accuracy: Precise, always returns the true nearest neighbor
  • Best for: Small datasets or when accuracy is critical
1

Open FaceDetectionOverlay.kt

Navigate to app/src/main/java/com/ml/shubham0204/facenet_android/presentation/components/FaceDetectionOverlay.kt
2

Enable flat search

Set the flatSearch variable to true:
FaceDetectionOverlay.kt
@SuppressLint("ViewConstructor")
@ExperimentalGetImage
class FaceDetectionOverlay(
    private val lifecycleOwner: LifecycleOwner,
    private val context: Context,
    private val viewModel: DetectScreenViewModel,
) : FrameLayout(context) {
    
    // Setting `flatSearch` to `true` enables precise calculation
    // of cosine similarity.
    // This is slower than ObjectBox's vector search, which approximates
    // nearest neighbor search
    private val flatSearch: Boolean = true // Change to true
    
    // ...
}
Flat search uses 4 parallel coroutines to speed up the linear scan, but it will still be slower than HNSW for large datasets.

Performance optimization

The FaceNet model uses several optimizations:

GPU acceleration

GPU delegation is enabled by default for faster inference:
FaceNet.kt
val interpreterOptions = Interpreter.Options().apply {
    if (useGpu) {
        if (CompatibilityList().isDelegateSupportedOnThisDevice) {
            addDelegate(GpuDelegate(CompatibilityList().bestOptionsForThisDevice))
        }
    } else {
        numThreads = 4
    }
    useXNNPACK = useXNNPack
    useNNAPI = true
}

XNNPACK

XNNPACK is a highly optimized library for neural network inference on mobile devices. It’s enabled by default.

NNAPI

Android’s Neural Networks API (NNAPI) is also enabled for hardware acceleration when available.

Choosing configuration options

Use caseRecommended configuration
High accuracy, small dataset (< 50 people)FaceNet-512 + Flat search
Balanced accuracy and speedFaceNet-512 + HNSW search
Maximum speed, large dataset (100+ people)FaceNet-128 + HNSW search
Low-end devicesFaceNet-128 + HNSW search + GPU disabled
Security-critical applicationsFaceNet-512 + Flat search + Spoof detection

Runtime configuration

Currently, configuration changes require modifying the source code and rebuilding the app. Future versions may support runtime configuration through a settings UI.

Next steps

Now that you understand the configuration options:

Build docs developers (and LLMs) love