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.

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.

Now 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.

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.

Previous Post

Add a comment