Real-Time Image Analysis with CameraX

AndroidKotlinCameraXComputer Vision
Share on LinkedIn

The core of any camera feature that "understands" what it sees — a document scanner, a live translator, a barcode reader — is a CameraX ImageAnalysis use case delivering frames to your code in real time. The whole game is keeping up: the camera produces frames faster than most analysis can process them, so the difference between a smooth feature and a laggy one is how you handle backpressure and buffer lifecycle. Get those two right and everything else is detail.

I've built several live-camera features on CameraX. The mistakes are always the same, and they're always about frame flow, not the analysis itself.

The three use cases, bound together

CameraX gives you Preview, ImageCapture, and ImageAnalysis. For a live feature you bind Preview (what the user sees) and ImageAnalysis (what your code sees) to the same lifecycle:

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

val analysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
    .setTargetResolution(Size(1280, 720))
    .build()
    .also {
        it.setAnalyzer(cameraExecutor) { proxy -> analyze(proxy) }
    }

cameraProvider.bindToLifecycle(
    lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
)

Bind to a LifecycleOwner and CameraX starts and stops the camera with the lifecycle automatically — no manual open/release, which removes a whole category of camera-leak bugs.

Backpressure is the whole ballgame

The camera pushes ~30 frames a second. If your analyzer takes 60ms per frame, you can only process ~16 of them. What happens to the other 14? That's the backpressure strategy:

For anything user-facing, KEEP_ONLY_LATEST. Chasing "process every frame" in a live viewfinder is how you get a feature that feels broken.

Close the ImageProxy or everything freezes

Each frame arrives as an ImageProxy backed by one buffer from a small pool. Hold onto it and you starve the pool; the analyzer simply stops getting called and the feature "freezes" with no crash and no log. The fix is disciplined closing:

private fun analyze(proxy: ImageProxy) {
    try {
        val rotation = proxy.imageInfo.rotationDegrees
        // ... run detection on proxy ...
    } finally {
        proxy.close()   // non-negotiable, even on exception
    }
}

If you hand the frame to an async consumer (like ML Kit's process() which returns a Task), close the proxy in the task's completion listener, not synchronously — closing while ML Kit still reads the buffer corrupts the frame. This ordering bug is the single most common CameraX + ML Kit defect I see.

Rotation and format

Two details that silently ruin accuracy:

Throttle when you don't need every frame

Many features don't need 30fps analysis. A document-edge detector at 5fps feels instant and saves battery and heat — and phones throttle the camera when they get hot, so less work can mean a more stable frame rate. A simple time gate:

private var lastRun = 0L
private val minIntervalMs = 200L   // ~5 fps

private fun analyze(proxy: ImageProxy) {
    val now = SystemClock.elapsedRealtime()
    if (now - lastRun < minIntervalMs) { proxy.close(); return }
    lastRun = now
    try { /* detect */ } finally { proxy.close() }
}

Feeding fewer, well-chosen frames to something like on-device ML Kit vision usually beats hammering it at full frame rate.

Keep heavy work off the main thread

Pass a dedicated single-thread executor to setAnalyzer. The analysis runs there, off the UI thread, so even a slow detector never janks the preview. When you have a result to draw (a bounding box overlay), post only that small result back to the main thread — never the frame. Marshaling whole frames to the UI thread is a classic self-inflicted jank source.

What I'd take away

A real-time CameraX pipeline lives or dies on frame flow, not cleverness. Use STRATEGY_KEEP_ONLY_LATEST so you always analyze fresh frames, close every ImageProxy (in a finally, or after your async consumer finishes) or the feed freezes, honor rotationDegrees so your detector sees an upright world, keep the default YUV format when feeding ML Kit, throttle to the frame rate your feature actually needs, and run analysis on a dedicated executor. Nail those and you get a live camera feature that stays smooth, cool, and accurate.

Common production mistakes

Teams get camerax image analysis wrong in predictable ways:

Shipping camerax image analysis on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.

Resources

Frequently asked questions

What backpressure strategy should I use for CameraX ImageAnalysis?

For real-time analysis use STRATEGY_KEEP_ONLY_LATEST, which drops intermediate frames and always hands you the newest one. This keeps your analyzer from falling behind and building latency when processing is slower than the camera frame rate. Use STRATEGY_BLOCK_PRODUCER only when you must process every single frame and can tolerate the camera stalling.

Why must I call ImageProxy.close() in the analyzer?

CameraX uses a small pool of image buffers, and each ImageProxy holds one. If you don't close it, the pool exhausts and the analyzer stops receiving new frames — playback appears to freeze. Always close the ImageProxy when you're done, ideally in a finally block, even if your analysis throws.

Should I use YUV_420_888 or RGBA_8888 output from ImageAnalysis?

Use YUV_420_888 (the default) when feeding ML Kit or any consumer that accepts YUV, because it avoids a conversion and is what the camera produces natively. Request RGBA_8888 output only when your analysis code specifically needs RGB, since CameraX then does the conversion for you at some CPU cost.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →