krk island - pinezici Cross-compiling OpenCV from sources and using it in my Android apps has long time been on my todo list.

I like to play with images and graphics stuff and OpenCV offers a lot of features related to image processing. Recently I found some free time and created my Pano Stitch & Crop app which uses OpenCV to create panoramic photos.

There's a lot of documentation and tutorials that show how to use OpenCV on Android. But I did have some specific requirements that forced me to use OpenCV in my Android app in a bit different way than usual.

As with most open source projects, I prefer to build the libraries from source, instead of downloading any prebuild binaries.

Libraries written in C++ often provide a JNI wrapper to access all possible APIs from Java/Kotlin code. Here I prefer to create my own wrapper that accesses only the minimal functionality I need. Therefore I decided to only use the native C++ part of OpenCV and did not build the Java wrapper.

Additionally, OpenCV on Android is usually used with OpenCVManager. With OpenCVManager you have the advantage that multiple apps can use one library to save space and you also get automatic updates with bugfixes. I still prefer when the user doesn't have to download and install something additional besides my app, so I decided to use OpenCV without OpenCVManager.

In this post I'll describe my experience on building and using OpenCV on Android.

If you you're an Android enthusiast that likes to learn more about Android internals, I highly recommend to check out my Bugjaeger app. It allows you to connect 2 Android devices through USB OTG and perform many of the tasks that are normally only accessible from a developer machine via ADB directly from your Android phone/tablet.

Cross-Compiling OpenCV from Source Code

The main requirements for building OpenCV from source is to have the NDK and CMake installed on your machine.

I cross-compiled the sources on Ubuntu 16. It might not be a requirement, but I only had good results cross-compiling on the Linux platform.

First thing you need to do is downloading the sources

git clone https://github.com/opencv/opencv.git opencv

Next you should create an output directory (inside of the directory where you've cloned OpenCV into) because OpenCV doesn't allow in-source builds

mkdir android_build
cd android_build

To create the necessary build structure with cmake, you need to provide the path to your NDK in ANDROID_NDK variable. Maybe this has already been fixed, but the .cmake file used by OpenCV wasn't working properly for Android. Luckily, the NDK provides android.toolchain.cmake file located somewhere in build/cmake. I forced cmake to use this file instead by defining the -DCMAKE_TOOLCHAIN_FILE variable.

Some additional tweaks were that I disabled building of the Java stuff and I also specified which STL runtime I want to use (although it's possible that the same one is already set by default).

I also specified the install prefix. This will be the directory where all the output files (including .so libraries and header files) will be copied.

One additional variable that you should specify is ANDROID_ABI, otherwise the deprecated armeabi might get selected by default. For other projects you would normally use CMAKE_ANDROID_ARCH_ABI, but this flag seems to be ignored when using the .cmake file provided by NDK.

Your cmake shell command should look something like this

cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake -DANDROID_NDK=/path/to/ndk -DANDROID_NATIVE_API_LEVEL=android-21 -DBUILD_JAVA=OFF -DBUILD_ANDROID_EXAMPLES=OFF -DBUILD_ANDROID_PROJECTS=OFF -DANDROID_STL=c++_shared -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX:PATH=/absolute/path/to/opencv/android_build/out -DANDROID_ABI=arm64-v8a

Note here that I first changed current working directory to the directory we created in our previous step (android_build/) and then I executed the shell command.

In some cases you require only a small subset of the functionality specific to some modules. To reduce build time and final size, OpenCV's build system allows you to disable some modules and functionality during build

cmake .. \
-DBUILD_opencv_ittnotify=OFF -DBUILD_ITT=OFF -DCV_DISABLE_OPTIMIZATION=ON -DWITH_CUDA=OFF -DWITH_OPENCL=OFF -DWITH_OPENCLAMDFFT=OFF -DWITH_OPENCLAMDBLAS=OFF -DWITH_VA_INTEL=OFF -DCPU_BASELINE_DISABLE=ON -DENABLE_SSE=OFF -DENABLE_SSE2=OFF -DBUILD_TESTING=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=RELEASE -DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF -DBUILD_opencv_apps=OFF -DBUILD_SHARED_LIBS=OFF -DOpenCV_STATIC=ON -DWITH_1394=OFF -DWITH_ARITH_DEC=OFF -DWITH_ARITH_ENC=OFF -DWITH_CUBLAS=OFF -DWITH_CUFFT=OFF -DWITH_FFMPEG=OFF -DWITH_GDAL=OFF -DWITH_GSTREAMER=OFF -DWITH_GTK=OFF -DWITH_HALIDE=OFF -DWITH_JASPER=OFF -DWITH_NVCUVID=OFF -DWITH_OPENEXR=OFF -DWITH_PROTOBUF=OFF -DWITH_PTHREADS_PF=OFF -DWITH_QUIRC=OFF -DWITH_V4L=OFF -DWITH_WEBP=OFF \
-DBUILD_LIST=core,features2d,flann,imgcodecs,imgproc,stitching \
-DANDROID_NDK=/path/to/ndk -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake -DANDROID_NATIVE_API_LEVEL=android-21 -DBUILD_JAVA=OFF -DBUILD_ANDROID_EXAMPLES=OFF -DBUILD_ANDROID_PROJECTS=OFF -DANDROID_STL=c++_shared -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX:PATH=/absolute/path/to/opencv/android_build/out -DANDROID_ABI=arm64-v8a

This is how I disabled some of the modules and functionality when compiling OpenCV for my Pano Stitch & Crop App. Note how I disabled certain functionality with OFF and then I used -DBUILD_LIST to specify the modules I needed.

There might be some additional options that you want to change for your specific situation. You can use the following command to check which variables are available

cmake -LA

In case there are some errors and you changed some options to fix these errors, you might also need to first delete CMakeCache.txt file before you'll be able to successfully run cmake.

To finally build the project execute

make
make install

The .so libraries that we will use in our Android app should be located inside of the out/sdk/native/libs/ directory.

CMake makes it really easy to cross-compile projects for Android. You can use the same approach I showed above to cross-compile another related library - dlib.

Preparing Android Studio Project for Build

Here I assume that you've already created a new Android Studio project with C++ support enabled.

To properly link our native code with OpenCV libraries, you should change the CMakeLists.txt file inside of your app module folder.

# Configure path to include directories
include_directories(SYSTEM $ENV{VENDOR}/opencv/include )

# Set up OpenCV shared .so library so that it can
# be linked to your app
add_library( cv_core-lib SHARED IMPORTED)
set_target_properties( cv_core-lib
                      PROPERTIES IMPORTED_LOCATION
                      $ENV{VENDOR}/opencv/lib/${ANDROID_ABI}/libopencv_core.so )

add_library( cv_imgproc-lib SHARED IMPORTED)
set_target_properties( cv_imgproc-lib
        PROPERTIES IMPORTED_LOCATION
        $ENV{VENDOR}/opencv/lib/${ANDROID_ABI}/libopencv_imgproc.so )

add_library( cv_imgcodecs-lib SHARED IMPORTED)
set_target_properties( cv_imgcodecs-lib
        PROPERTIES IMPORTED_LOCATION
        $ENV{VENDOR}/opencv/lib/${ANDROID_ABI}/libopencv_imgcodecs.so )

...

# jnigraphics lib from NDK is used for Bitmap manipulation in native code
find_library( jnigraphics-lib jnigraphics )

# Link to your native app library
target_link_libraries( my_native-lib ${jnigraphics-lib} cv_core-lib cv_imgproc-lib cv_imgcodecs-lib other-libs...)

Here I linked with core, imgproc, and imgcodecs modules. You might need to add other OpenCV libraries depending on what functionality you use.

I decided to use the shared libraries in this case, but static linking should also work.

I also linked with jnigraphics library from the NDK. In my example code I use some Bitmap manipulation methods, so this is required.

Next you should change your module level build.gradle file

android {
    ...
    defaultConfig {
        ...
        sourceSets {
            main {
                jniLibs.srcDirs = [
                        System.getenv('VENDOR') + '/opencv/lib'
                ]
            }
        }
    }

This will will ensure that gradle packages the OpenCV .so libraries located in lib folder into the final APK. The lib/ folder should contain subfolders with each supported architecture, e.g., armeabi, armeabi-v7a,... I usually include at least armeabi-v7a and x86 to the final APK for publishing, but for testing you can only include the ABI supported by you testing devices. If you're not sure how to find the the supported ABIs for your test device, check out my Bugjaeger app which will show you the supported ABIs in Device section.

I stored the path to the folder with external libraries inside of VENDOR environment variable and I used System.getenv('VENDOR') to get the path to OpenCV. You should replace the path with wherever you store the previously built OpenCV libraries.

Additionally, in the module level build.gradle file I also usually specify which ABIs to build for, which C++ runtime to use, C++ exception support, and some other stuff

android {
    ...
    defaultConfig {
    ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -fexceptions"

                arguments "-DANDROID_TOOLCHAIN=clang",
                          "-DANDROID_STL=c++_shared",
                          "-DANDROID_PLATFORM=android-21"
            }
        }

        ndk {
            abiFilters 'armeabi-v7a'//, 'x86'
        }

        ...

Using OpenCV from Kotlin and Native C++ Code

In the following code I'll use Canny edge detection on one of my vacation photos from Krk Island. First I load the JPEG image into a Bitmap in Kotlin, then I'll use the pixels from the Bitmap in native code to perform the Canny edge detection.

This example might seem a bit lame considering all the stuff you can do with OpenCV, but I was trying to keep this simple. If you are interested in some low-latency/realtime Camera image processing on Android, you might also check out my previous post about Android Native Camera API.

It is always possible to open an image file directly in native C++ code. However, Android's Java API uses the Bitmap class in many other image processing and graphics related functions. Therefore I think it could be useful to show how to access Bitmap's pixel buffer in native code without doing unnecessary copies.

First we create a native C++ function that will be called from our Kotlin class via JNI. The function takes an input bitmap which will be used for canny edge detection. The function will write the detected edges into the given destination file.

#include <jni.h>
#include <string>

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <android/bitmap.h>

using namespace cv;

extern "C" {
JNIEXPORT void JNICALL
Java_eu_sisik_opencvsample_MainActivity_canny(
        JNIEnv *env,
        jobject /* this */,
        jobject bitmap,
        jstring destination) {

    // Get information about format and size
    AndroidBitmapInfo info;
    AndroidBitmap_getInfo(env, bitmap, &info);

    // Get pointer to pixel buffer
    void *pixels = 0;
    AndroidBitmap_lockPixels(env, bitmap, &pixels);

    // I create separate scope for input Mat here
    // to make sure it is destroyed before unlocking
    // pixels
    {
        // Check the format info before you pick the CV_ type
        // for OpenCV Mat
        // info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 -> CV_8UC4

        // Now create the mat
        Mat input(info.height, info.width, CV_8UC4, pixels);

        // Perform canny edge detection
        Mat edges;
        Canny(input, edges, 200.0, 600.0, 600.0);

        // Save to destination
        const char *dest = env->GetStringUTFChars(destination, 0);
        imwrite(dest, edges);
        env->ReleaseStringUTFChars(destination, dest);
    }

    // Release the Bitmap buffer once we have it inside our Mat
    AndroidBitmap_unlockPixels(env, bitmap);
}

}

In the code above I construct an OpenCV Mat from Bitmap pixels buffer. I create a separate scope for the input Mat object to make sure it gets destructed before I call AndroidBitmap_unlockPixels().

In Kotlin code, you should make sure that you load your native .so library System.loadLibrary(). The native method is declared with external keyword

class MainActivity : AppCompatActivity() {
...
    external fun canny(src: Bitmap?, destinationPath: String): Void

    companion object {

        init {
            System.loadLibrary("native-lib")
        }
    }
...

To use our native C++ function from Kotlin

// Load my JPEG from assets
var bitmap: Bitmap? = null
assets.open("pinezici_krk_island.JPG").use {
    bitmap = BitmapFactory.decodeStream(it)
}

// Store result inside of app's cache folder
var dest = cacheDir.absolutePath + "/canny.JPG"

// Pass the bitmap to native C++ code and perform canny edge detection
canny(bitmap, dest)

// Show the processed image
ivPhoto.setImageBitmap(BitmapFactory.decodeFile(dest))

The resulting image looks like this cannied image

You can find the code for this blog post also on github.

OpenCV in My Projects

OpenCV offers a lot of cool features and using it on Android is really simple.

For now I only used OpenCV in one Android app - my Pano Stitch & Crop. If you're interested in photography, you might find this app useful.

I also created a simplified online version of this panorama tool - http://sisik.eu/pano. This web app uses OpenCV compiled into WebAssembly. Although this online version is not as optimized for mobile devices, I still think it's cool that you can embed OpenCV into web apps that easily.

As the next thing, I would like to use OpenCV for some object detection and real-time processing of camera frames. Maybe I'll publish some more info about this in my next posts.

More Useful Resources

I wrote another blogpost that uses the devices camera directly from Android NDK (C/C++). Combining it with the information you've found in this blogpost might be useful in some of your camera app projects.

OpenCV is often used for various image processing. I also show how to do efficient image processing by using C/C++ and RenderScript in this blogpost .

Even though it's only Java/Kotlin (no C++), you can process videos very efficiently by using OpengGL and MediaCodec API. I wrote another blogpost that shows how to do this.

Previous Post

Add a comment

Comments

My problem was just to use (OpenCV 4.x version), if I use (OpenCV 3.x version and NDK r19c), then everything works! Thanks!
Written on Wed, 04 Mar 2020 15:40:40 by Mack
@Mack, since then I deleted the OpenCV source that I've cloned and installed multiple new versions of Android NDK, so I cannot tell you the exact versions. But I'd expect that it'll work with newer versions too.
Written on Wed, 04 Mar 2020 15:22:18 by Roman
Roman? hello again! What version (OpenCV) and (NDK) did you use? Information from the library in your android application says: OpenCV - 4.0.0-pre, NDK - r17b This is true?
Written on Wed, 04 Mar 2020 15:13:06 by Mack
@Mack, If you want to reduce the size, try to play with the cmake build variables that I'm showing in my post (e.g. -DBUILD_opencv_ittnotify=OFF) and also try to build only the minimal modules that you need with "-DBUILD_LIST" (but there might be interdependencies, so you probably won't be able to build only one module). Also try to check the official suggestions on how to reduce build size here - https://github.com/opencv/opencv/wiki/Compact-build-advice. Regarding the undefined references...double check your paths to the libraries in build.gradle and CMakeLists.txt and make sure they are packaged into the APK. If theyre missing inside of the APK (lib/yourabi/yourlibs.so), you probably don't have the correct path jniLibs.srcDirs. Also check that there's no mismatch between the ABI that you cross-compiled opencv into and the ABI for the APK.
Written on Wed, 04 Mar 2020 13:05:39 by Roman
I try to compile OpenCv 4.0.0 from source, according to your instructions and as a result I get large size libraries(~21Mb - libopencv-core.so), while you have (~1.5Mb - libopencv-code.so). What am I doing wrong? You can get a link to the archive (build\opencv-4.0.0-pre). And in the android application, then I get errors like: error: undefined reference to cv :: findContours. I will be very grateful if you help me!
Written on Wed, 04 Mar 2020 12:34:14 by Mack
Hi Anish, 1. My understanding is that it's kind of both. It will restrict the minimal API level you're app can reliably run on. It will also restrict which native C API's you'll have available (e.g. native Camera API only available since API level 24) 2. Yes, compile for all ABIs you're planning to support. However, compiling only for armeabi-v7 should already be sufficient to make it run on most of the devices (including x86/Intel). Additionally compiling for arm64-v8a will allow you to upload it to google play (there's now a requirement for 64bit platform).
Written on Mon, 16 Dec 2019 03:43:06 by Roman
1. What is "-DANDROID_NATIVE_API_LEVEL" ?? This is minSdkVersion or targetSdkVersion?? 2. Your mentioned "DANDROID_ABI=arm64-v8a". So, does this means i have to use the same process for other ABIs??
Written on Mon, 16 Dec 2019 03:43:06 by Anish Kumar Dubey