DEV Community

vmodal_ai
vmodal_ai

Posted on

Real-Time Object Detection and Tracking with Kotlin and YOLO on Android

Real-Time Object Detection and Tracking with Kotlin and YOLO on Android

Real-time computer vision is one of the most useful applications of machine learning on mobile devices. Android phones provide cameras, GPU acceleration, and enough processing power to run optimized detection models locally.

In this tutorial, we will build the architecture for a Kotlin application that captures camera frames, runs a YOLO-style object detector, and tracks detected objects across frames.

Architecture

CameraX
   |
ImageAnalysis
   |
Frame Conversion
   |
YOLO Detector
   |
Non-Maximum Suppression
   |
Object Tracker
   |
UI Overlay
Enter fullscreen mode Exit fullscreen mode

The important part is to keep camera capture, inference, tracking, and rendering separate.

CameraX Setup

Add the CameraX dependencies compatible with your project:

dependencies {
    implementation("androidx.camera:camera-camera2:<version>")
    implementation("androidx.camera:camera-lifecycle:<version>")
    implementation("androidx.camera:camera-view:<version>")
}
Enter fullscreen mode Exit fullscreen mode

Create an ImageAnalysis use case:

val analysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(
        ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
    )
    .build()
Enter fullscreen mode Exit fullscreen mode

KEEP_ONLY_LATEST is important for real-time applications. If inference is slower than the camera, processing every old frame creates a growing queue and increases latency.

Analyzer

Implement an analyzer that receives camera frames:

class ObjectAnalyzer(
    private val detector: YoloDetector
) : ImageAnalysis.Analyzer {

    override fun analyze(image: ImageProxy) {
        try {
            detector.detect(image)
        } finally {
            image.close()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Always close ImageProxy when processing is complete.

YOLO Detector

Your detector should convert camera input into the tensor format expected by your model.

Conceptually:

class YoloDetector {

    fun detect(image: ImageProxy): List<Detection> {
        val input = preprocess(image)
        val output = runModel(input)

        return postprocess(output)
    }
}
Enter fullscreen mode Exit fullscreen mode

A detection normally contains a bounding box, class ID, confidence score, and optionally additional metadata.

data class Detection(
    val classId: Int,
    val confidence: Float,
    val box: RectF
)
Enter fullscreen mode Exit fullscreen mode

Preprocessing

Most object detection models expect a fixed input size.

For example:

Camera frame
    ↓
Rotate
    ↓
Crop / resize
    ↓
Normalize
    ↓
Tensor
Enter fullscreen mode Exit fullscreen mode

Be careful with aspect ratio. Incorrect scaling can make objects appear distorted and reduce detection accuracy.

Postprocessing

YOLO-style models can return many candidate boxes. Filter low-confidence detections:

val filtered = detections.filter {
    it.confidence >= 0.5f
}
Enter fullscreen mode Exit fullscreen mode

Then apply non-maximum suppression (NMS) to remove overlapping boxes representing the same object.

The exact threshold should be tuned using your target model and dataset.

Tracking Objects Across Frames

Detection tells you what is visible in a frame. Tracking attempts to maintain an object's identity across multiple frames.

A simplified tracked object could be:

data class TrackedObject(
    val id: Int,
    var box: RectF,
    var classId: Int,
    var confidence: Float
)
Enter fullscreen mode Exit fullscreen mode

A basic tracker can associate the current detection with an existing object using Intersection over Union (IoU).

fun iou(a: RectF, b: RectF): Float {
    val left = maxOf(a.left, b.left)
    val top = maxOf(a.top, b.top)
    val right = minOf(a.right, b.right)
    val bottom = minOf(a.bottom, b.bottom)

    if (right <= left || bottom <= top) return 0f

    val intersection =
        (right - left) * (bottom - top)

    val union =
        a.width() * a.height() +
        b.width() * b.height() -
        intersection

    return intersection / union
}
Enter fullscreen mode Exit fullscreen mode

For more robust tracking, consider established algorithms such as SORT or ByteTrack.

Drawing Bounding Boxes

Use a custom Android view to draw detections over the camera preview.

class DetectionOverlay : View(context) {

    var detections: List<Detection> = emptyList()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        for (detection in detections) {
            canvas.drawRect(
                detection.box,
                paint
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You will need coordinate transformation because model coordinates and preview coordinates may differ.

Improving FPS

Real-time inference requires careful optimization.

Useful techniques include:

  • Resize frames before inference
  • Process only the latest frame
  • Avoid unnecessary bitmap allocations
  • Reuse buffers
  • Use hardware acceleration where supported
  • Quantize the model
  • Reduce model input resolution
  • Run inference off the UI thread

Do not optimize only for FPS. A smaller model with slightly lower accuracy may provide a much better mobile user experience.

Measuring Performance

Track at least:

Camera FPS
Inference latency
End-to-end latency
Memory usage
CPU/GPU utilization
Enter fullscreen mode Exit fullscreen mode

For example:

val start = System.nanoTime()

val detections = detector.detect(image)

val elapsedMs =
    (System.nanoTime() - start) / 1_000_000
Enter fullscreen mode Exit fullscreen mode

Use real devices for benchmarking because emulator performance does not represent typical mobile hardware.

Handling Rotation

Camera frames can have different orientations depending on the device.

Pass rotation information into preprocessing:

val rotation = image.imageInfo.rotationDegrees
Enter fullscreen mode Exit fullscreen mode

Failing to handle rotation correctly can produce incorrect bounding boxes or significantly reduce accuracy.

Production Considerations

A production application should also handle:

  • Camera permission failures
  • Model initialization errors
  • Unsupported hardware
  • Low-memory devices
  • App backgrounding
  • Camera lifecycle changes
  • Device rotation

Conclusion

Combining Kotlin, CameraX, YOLO, and object tracking creates a powerful on-device computer vision pipeline.

The key to a production-ready implementation is not simply running a model. You must also control frame backpressure, coordinate transformations, memory allocations, inference latency, and camera lifecycle.

This foundation can be extended into applications such as traffic monitoring, retail analytics, industrial inspection, sports analysis, and smart navigation.

Useful Links

SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter

SDK Android: https://github.com/v-modal/vmodal_sdk_android

Discord: https://discord.gg/K72z28KUx

Top comments (0)