+1 (415) 360-7596

OpenCV on Android in 2026

If your last Android project with OpenCV used "OpenCV4Android", OpenCV Manager and a hand-copied sdk/ folder, almost everything has changed. In 2026 the official OpenCV Android SDK is a Maven Central artifact, camera access goes through CameraX, and the deep-learning half of the app often belongs in LiteRT rather than cv::dnn. This guide is the modern workflow.

1. Get OpenCV from Maven Central

Stop vendoring the SDK. The official artifact is org.opencv:opencv on Maven Central:

// app/build.gradle.kts
dependencies {
    implementation("org.opencv:opencv:5.0.0")
    implementation("androidx.camera:camera-core:1.4.2")
    implementation("androidx.camera:camera-camera2:1.4.2")
    implementation("androidx.camera:camera-lifecycle:1.4.2")
    implementation("androidx.camera:camera-view:1.4.2")
}

Check Maven Central for the newest OpenCV and CameraX versions before copying those numbers. If you prefer the downloadable SDK zip from the GitHub release, note the 5.0.0 release note: the original Android SDK package was built with an old NDK whose bundled C++ standard library is not aligned for 16 KB memory pages, and Google Play now requires 16 KB page-size support. Use the package with the 16kb-page-fix suffix for Play releases, or the Maven artifact.

Initialize OpenCV once, in your Application or first Activity. OpenCVLoader.initLocal() replaced the old initAsync / OpenCV Manager dance years ago:

import org.opencv.android.OpenCVLoader

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        check(OpenCVLoader.initLocal()) { "OpenCV failed to load" }
    }
}

2. Watch the OpenCV 5 Java package renames

OpenCV 5 moved several modules, and Java is the one binding where that requires code changes:

// OpenCV 4.x
import org.opencv.calib3d.Calib3d
import org.opencv.features2d.ORB
// OpenCV 5.x
import org.opencv.geometry.Geometry      // findHomography, solvePnP
import org.opencv.calib.Calib            // calibrateCamera, stereoCalibrate
import org.opencv.features.ORB

Imgproc.convexHull and friends moved to Geometry too. Haar cascades (CascadeClassifier) and HOG now live in opencv_contrib, which the standard Android artifact does not include; use FaceDetectorYN from objdetect instead for faces.

3. Camera frames with CameraX

Do not use the legacy JavaCameraView. CameraX's ImageAnalysis use case hands you YUV_420_888 frames on a background executor, which is exactly what a vision pipeline wants:

import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.opencv.imgproc.Imgproc

fun ImageProxy.toBgrMat(): Mat {
    val y = planes[0].buffer
    val u = planes[1].buffer
    val v = planes[2].buffer
    val ySize = y.remaining(); val uSize = u.remaining(); val vSize = v.remaining()
    val nv21 = ByteArray(ySize + uSize + vSize)
    y.get(nv21, 0, ySize)
    v.get(nv21, ySize, vSize)          // NV21 is Y then interleaved VU
    u.get(nv21, ySize + vSize, uSize)
    val yuv = Mat(height + height / 2, width, CvType.CV_8UC1)
    yuv.put(0, 0, nv21)
    val bgr = Mat()
    Imgproc.cvtColor(yuv, bgr, Imgproc.COLOR_YUV2BGR_NV21)
    yuv.release()
    return bgr
}

val analysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
    .build()
    .also { ia ->
        ia.setAnalyzer(analysisExecutor) { image ->
            val bgr = image.toBgrMat()
            try { process(bgr, image.imageInfo.rotationDegrees) } finally {
                bgr.release(); image.close()
            }
        }
    }

ProcessCameraProvider.getInstance(context).get()
    .bindToLifecycle(lifecycleOwner, cameraSelector, preview, analysis)

The interleaved-VU copy above assumes the common pixel-stride-2 layout; check planes[1].pixelStride and fall back to a per-pixel copy (or ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) on devices that report stride 1. STRATEGY_KEEP_ONLY_LATEST is the Android equivalent of appsink drop=true: never queue frames behind slow processing. Apply rotationDegrees with Core.rotate before you run anything orientation-sensitive.

4. Where the model runs: cv::dnn or LiteRT?

OpenCV 5's new DNN engine is excellent on desktop CPUs, but on a phone you usually want the NPU or GPU, and that is LiteRT's job. LiteRT is the renamed TensorFlow Lite; the Ultralytics LiteRT export produces a .tflite for YOLO26 that runs with the GPU or NNAPI/vendor delegates.

The practical split we use:

  • OpenCV for capture conversion, letterboxing, color, undistortion, tracking, ArUco, drawing, and any classical pipeline.
  • LiteRT for the neural network when you need hardware acceleration or a model under ~50 MB.
  • cv::dnn on Android only for small ONNX models where a single dependency matters more than peak speed.

Feed LiteRT from the same Mat you pre-processed with OpenCV and hand the [1, N, 6] end-to-end output straight back into OpenCV for drawing; YOLO26's NMS-free head means there is no post-processing to port.

5. JNI performance pitfalls

The Java bindings are fine for calling OpenCV functions; they are expensive for moving pixels. Rules that hold up in profiling:

  • Never loop over pixels in Kotlin/Java. Mat.get/Mat.put per element crosses JNI each call. Use whole-buffer put(row, col, ByteArray) or move the loop into C++.
  • Pass native addresses across the boundary, not copies. Mat.getNativeObjAddr() gives you a cv::Mat* to use in your own JNI function:
external fun processFrame(matAddr: Long): Int
processFrame(bgr.nativeObjAddr)
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Vision_processFrame(JNIEnv*, jobject, jlong addr) {
    cv::Mat& frame = *reinterpret_cast<cv::Mat*>(addr);
    cv::cvtColor(frame, frame, cv::COLOR_BGR2GRAY);   // in place, zero copy
    return frame.rows;
}
  • Release Mats deterministically. The finalizer runs late; a camera loop that allocates a fresh Mat per frame and relies on GC will stutter. use {}-style helpers or explicit release() in finally.
  • Keep the analysis executor single-threaded and drop frames; parallel analyzers fight for the same cores the GPU delegate needs.
  • Build OpenCV with only the modules you use (-DBUILD_LIST=core,imgproc,imgcodecs,objdetect,features,geometry) if APK size matters; the full SDK adds tens of megabytes per ABI.

6. Checklist

  1. org.opencv:opencv from Maven Central, or the 16kb-page-fix SDK package for Play.
  2. OpenCVLoader.initLocal() at startup; no OpenCV Manager.
  3. Update calib3d/features2d imports for OpenCV 5.
  4. CameraX ImageAnalysis with KEEP_ONLY_LATEST, YUV to BGR via cvtColor.
  5. LiteRT for the accelerated model, OpenCV around it.
  6. No per-pixel JNI, native addresses across the boundary, explicit release().

We build and modernize Android vision apps; get in touch if yours is still on OpenCV4Android.