Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

Reuse existing C code with the Android NDK

Learn how to use the Android Native Developer's Kit

Frank Ableson, Entrepreneur, Navitend
W. Frank Ableson is an entrepreneur living in northern New Jersey with his wife Nikki and their children. His professional interests include mobile software and embedded design. He is the author of Unlocking Android (Manning Publications, 2009) and Android in Action (Manning Publications, 2011) and he is the mobile editor for Linux Magazine.

Summary:  The Android Software Developer Kit (SDK) used by the majority of Android application developers requires the use of the Java™ programming language. However, there is a large body of C language code available online. The Android Native Developer Kit (NDK) permits an Android developer to reuse existing C source code within an Android application. In this tutorial, you will create an image processing application in the Java programming language that uses C code to perform basic image processing operations.

Date:  12 Apr 2011
Level:  Intermediate PDF:  A4 and Letter (773 KB | 42 pages)Get Adobe® Reader®

Activity:  57808 views
Comments:  

Creating the NDK files

Now that the Android application's UI and application logic are in place, you need to implement the image processing functions. In order to do this, you need to create a Java-native library with the NDK. In this case, you will use some public domain C code to implement the image processing functions and package them into a library usable by the Android application.

Building the native library

The NDK creates shared libraries and relies on a makefile system. To build the native library for this project, you need to perform the following steps:

  1. Create a new folder named jni beneath your project file.
  2. Within the jni folder, create a file named Android.mk, which contains the makefile instructions to properly build and name your library.
  3. Within the jni folder, create the source file, which is referenced in the Android.mk file. The name of the C source file for this tutorial is ibmphotophun.c.

Listing 3 contains the Android.mk file contents.


Listing 3. Android.mk file

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := ibmphotophun
LOCAL_SRC_FILES := ibmphotophun.c
LOCAL_LDLIBS    := -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)

Among other things, this makefile (snippet) instructs the NDK to:

  1. Compile the ibmphotophun.c source file into a shared library.
  2. Name the shared library. By default, the shared library naming convention is lib<modulename>.so. So, the resulting file here is named libibmphotophun.so.
  3. Specify the required "input" libraries. The shared library relies upon two built-in library files for logging (liblog.so) and jni graphics (libjnigraphics.so). The logging library permits you to add entries to the LogCat, which is helpful during the development phase of your project. The graphics library provides routines for working with Android bitmaps and their image data.

The ibmphotophun.c source file contains a few C include statements and the definition of the argb type, which corresponds to the Color data type in the Android SDK. Listing 4 shows ibmphotophun.c without the image routines, which are presented next.


Listing 4. Ibmphotophun.c macros and includes

/*
 * ibmphotophun.c
 * 
 * Author: Frank Ableson
 * Contact Info: fableson@msiservices.com
 */

#include <jni.h>
#include <android/log.h>
#include <android/bitmap.h>

#define  LOG_TAG    "libibmphotophun"
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

typedef struct 
{
    uint8_t alpha;
    uint8_t red;
    uint8_t green;
    uint8_t blue;
} argb;

The LOGI and LOGE macros make calls to the Logging facility and are equivalent in functionality to Log.i() and Log.e() respectively in the Android SDK. The argb data type defined with the typedef struct keywords allows the C code to access the four data elements of a single pixel stored in a 32-bit integer. The three include statements provide the necessary declarations to the C compiler for jni glue, logging, and bitmap handling, respectively.

It is now time for you to implement some image processing routines, but before we examine the code itself, you need to understand the naming convention of JNI functions.

When Java code calls a native function, it maps the function name to an expanded, or decorated, function, which is exported by the JNI shared library. Here is the convention: Java_fully_qualified_classname_functionname.

For example, the convertToGray function is implemented in the C code as Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray.

The first two arguments to the JNI functions include a pointer to the JNI environment and to the calling-class object instance. For more information about JNI, please see the Resources section.

Building the library is quite simple. Open a terminal (or DOS) window and change directory to the jni folder where you have stored these files. Make sure that the NDK is in your path and execute the ndk-build script. This script contains all the glue necessary to build the library. The resulting library is placed in the libs folder on the same level as the jni folder (<project folder>/libs/, for example).

When the Android application is packaged by the ADT plug-in for Eclipse, the library files are included and "wired up" automatically for you. One library file is generated for each supported hardware platform. The correct library is loaded at runtime.

Let's look at how the image processing algorithms are implemented.


Implementing the image processing algorithms

The image processing routines used by this application were adapted from a variety of public domain and academic routines, along with my own experience as an image processing hobbyist. Two of the functions use pixel operations and the third utilizes a minimal matrix approach. Let's look first at the convertToGray function in Listing 5.


Listing 5. convertToGray function

/*
convertToGray
Pixel operation
*/
JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_convertToGray(JNIEnv 
* env, jobject  obj, jobject bitmapcolor,jobject bitmapgray)
{
    AndroidBitmapInfo  infocolor;
    void*              pixelscolor; 
    AndroidBitmapInfo  infogray;
    void*              pixelsgray;
    int                ret;
    int             y;
    int             x;

     
    LOGI("convertToGray");
    if ((ret = AndroidBitmap_getInfo(env, bitmapcolor, &infocolor)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }

    
    if ((ret = AndroidBitmap_getInfo(env, bitmapgray, &infogray)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }

    
    LOGI("color image :: width is %d; height is %d; stride is %d; format is %d;flags is 
%d",infocolor.width,infocolor.height,infocolor.stride,infocolor.format,infocolor.flags);
    if (infocolor.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        LOGE("Bitmap format is not RGBA_8888 !");
        return;
    }

    
    LOGI("gray image :: width is %d; height is %d; stride is %d; format is %d;flags is 
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
    if (infogray.format != ANDROID_BITMAP_FORMAT_A_8) {
        LOGE("Bitmap format is not A_8 !");
        return;
    }
  
    
    if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor, &pixelscolor)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    if ((ret = AndroidBitmap_lockPixels(env, bitmapgray, &pixelsgray)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    // modify pixels with image processing algorithm
    
    for (y=0;y<infocolor.height;y++) {
        argb * line = (argb *) pixelscolor;
        uint8_t * grayline = (uint8_t *) pixelsgray;
        for (x=0;x<infocolor.width;x++) {
            grayline[x] = 0.3 * line[x].red + 0.59 * line[x].green + 0.11*line[x].blue;
        }
        
        pixelscolor = (char *)pixelscolor + infocolor.stride;
        pixelsgray = (char *) pixelsgray + infogray.stride;
    }
    
    LOGI("unlocking pixels");
    AndroidBitmap_unlockPixels(env, bitmapcolor);
    AndroidBitmap_unlockPixels(env, bitmapgray);

    
}

This function takes two arguments from the calling Java code: a color Bitmap in the ARGB format and an 8-bit grayscale Bitmap that receives a grayscale version of the color image. Here is a walk-through of the code:

  1. The AndroidBitmapInfo structure, defined in bitmap.h, is helpful for learning about a Bitmap object.
  2. The AndroidBitmap_getInfo function, found in the jnigraphics library, obtains information about a specific Bitmap object.
  3. The next step is to ensure that the bitmaps passed into the convertToGray function are of the expected format.
  4. The AndroidBitmap_lockPixels function locks down the image data so you can perform operations directly on the data.
  5. The AndroidBitmap_unlockPixels function unlocks previously locked pixel data. These functions should be called as a "lock/unlock pair".
  6. Sandwiched between the lock and unlock functions you see the pixel operations.

Pointer fun

Image processing applications in C typically involve the use of pointers. Pointers are variables that "point" to a memory address. The data type of a variable specifies the type and size of memory you are working with. For example a char represents a signed 8-bit value, so a char pointer (char *) allows you to reference an 8-bit value and perform operations through that pointer. The image data is represented as uint8_t, which means an unsigned 8-bit value, where each byte holds a value ranging from 0 to 255. A collection of three 8-bit unsigned values represents a pixel of image data for a 24-bit image.

Working through an image involves working on the individual rows of data and moving across the columns. The Bitmap structure contains a member known as the stride. The stride represents the width, in bytes, of a row of image data. For example, a 24-bit color plus alpha channel image has 32 bits, or 4 bytes, per pixel. So an image with a width of 320 pixels has a stride of 320*4 or 1,280 bytes. An 8-bit grayscale image has 8 bits, or 1 byte, per pixel. A grayscale bitmap with a width of 320 pixels has a stride of 320*1 or simply 320 bytes. With this information in mind, let's look at the image processing algorithm for converting a color image to a grayscale image:

  1. When the image data is "locked," the base address of the image data is referenced by a pointer named pixelscolor for the input color image and pixelsgray for the output grayscale image.
  2. Two for-next loops allow you to iterate over the entire image.
    1. First, you iterate over the height of the image, one pass per "row." Use the infocolor.height value to get the count of the rows.
    2. On each pass through the rows a pointer is set up to the memory location corresponding to the first "column" of image data for the row.
    3. As you iterate over the columns for a particular row, you convert each pixel of color data to a single value representing the grayscale value.
    4. When the complete row is converted you need to advance the pointers to the next row. This is done by jumping forward in memory by the stride value.

For all pixel-oriented image processing operations, you follow the above format. For example, consider the changeBrightness function shown in Listing 6.


Listing 6. changeBrightness function

/*
changeBrightness
Pixel Operation
*/
JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_changeBrightness(JNIEnv 
* env, jobject  obj, int direction,jobject bitmap)
{
    AndroidBitmapInfo  infogray;
    void*              pixelsgray;
    int                ret;
    int             y;
    int             x;
    uint8_t save;

    
    
    
    if ((ret = AndroidBitmap_getInfo(env, bitmap, &infogray)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }

    
    LOGI("gray image :: width is %d; height is %d; stride is %d; format is %d;flags is 
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
    if (infogray.format != ANDROID_BITMAP_FORMAT_A_8) {
        LOGE("Bitmap format is not A_8 !");
        return;
    }
  
    
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixelsgray)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    // modify pixels with image processing algorithm
    
    
    LOGI("time to modify pixels....");    
    for (y=0;y<infogray.height;y++) {
        uint8_t * grayline = (uint8_t *) pixelsgray;
        int v;
        for (x=0;x<infogray.width;x++) {
            v = (int) grayline[x];
            
            if (direction == 1)
                v -=5;
            else
                v += 5;
            if (v >= 255) {
                grayline[x] = 255;
            } else if (v <= 0) {
                grayline[x] = 0;
            } else {
                grayline[x] = (uint8_t) v;
            }
        }
        
        pixelsgray = (char *) pixelsgray + infogray.stride;
    }
    
    AndroidBitmap_unlockPixels(env, bitmap);

    
}

This function operates in a manner very similar to the convertToGray function with the following distinctions:

  1. This function requires only a single grayscale bitmap. The image passed in is modified in place.
  2. The function adds or subtracts a value of 5 from each pixel on each pass. This constant may be changed. I used 5 because it made the image change noticeably with each pass without having to press the plus or minus buttons excessively.
  3. The pixel values are constrained between 0 and 255. Be careful when performing these operations with unsigned variables directly as it is easy to "wrap around." My initial effort on the changeBrightness function resulted in adding 5 to a value such as 252 and winding up with 2. The effect was fun to watch, but not what I was after. That is why I am using the integer named v and casting the pixel data to the signed integer and then comparing that value to 0 and 255.

There remains one more image processing algorithm to examine: the findEdges function, which works a little bit differently from the prior two pixel-oriented functions. Listing 7 shows the findEdges function.


Listing 7. The findEdges function detects outlines within an image

/*
findEdges
Matrix operation
*/
JNIEXPORT void JNICALL Java_com_msi_ibm_ndk_IBMPhotoPhun_findEdges(JNIEnv 
* env, jobject  obj, jobject bitmapgray,jobject bitmapedges)
{
    AndroidBitmapInfo  infogray;
    void*              pixelsgray;
    AndroidBitmapInfo  infoedges;
    void*              pixelsedge;
    int                ret;
    int             y;
    int             x;
    int             sumX,sumY,sum;
    int             i,j;
    int                Gx[3][3];
    int                Gy[3][3];
    uint8_t            *graydata;
    uint8_t            *edgedata;
    

    LOGI("findEdges running");    
    
    Gx[0][0] = -1;Gx[0][1] = 0;Gx[0][2] = 1;
    Gx[1][0] = -2;Gx[1][1] = 0;Gx[1][2] = 2;
    Gx[2][0] = -1;Gx[2][1] = 0;Gx[2][2] = 1;
    
    
    
    Gy[0][0] = 1;Gy[0][1] = 2;Gy[0][2] = 1;
    Gy[1][0] = 0;Gy[1][1] = 0;Gy[1][2] = 0;
    Gy[2][0] = -1;Gy[2][1] = -2;Gy[2][2] = -1;


    if ((ret = AndroidBitmap_getInfo(env, bitmapgray, &infogray)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }

    
    if ((ret = AndroidBitmap_getInfo(env, bitmapedges, &infoedges)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }

    
    
    LOGI("gray image :: width is %d; height is %d; stride is %d; format is %d;flags is
%d",infogray.width,infogray.height,infogray.stride,infogray.format,infogray.flags);
    if (infogray.format != ANDROID_BITMAP_FORMAT_A_8) {
        LOGE("Bitmap format is not A_8 !");
        return;
    }
  
    LOGI("color image :: width is %d; height is %d; stride is %d; format is %d;flags is
%d",infoedges.width,infoedges.height,infoedges.stride,infoedges.format,infoedges.flags);
    if (infoedges.format != ANDROID_BITMAP_FORMAT_A_8) {
        LOGE("Bitmap format is not A_8 !");
        return;
    }

    

    if ((ret = AndroidBitmap_lockPixels(env, bitmapgray, &pixelsgray)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    if ((ret = AndroidBitmap_lockPixels(env, bitmapedges, &pixelsedge)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }

    
    // modify pixels with image processing algorithm
    
    
    LOGI("time to modify pixels....");    
    
    graydata = (uint8_t *) pixelsgray;
    edgedata = (uint8_t *) pixelsedge;
    
    for (y=0;y<=infogray.height - 1;y++) {
        for (x=0;x<infogray.width -1;x++) {
            sumX = 0;
            sumY = 0;
            // check boundaries
            if (y==0 || y == infogray.height-1) {
                sum = 0;
            } else if (x == 0 || x == infogray.width -1) {
                sum = 0;
            } else {
                // calc X gradient
                for (i=-1;i<=1;i++) {
                    for (j=-1;j<=1;j++) {
                        sumX += (int) ( (*(graydata + x + i + (y + j) 
* infogray.stride)) * Gx[i+1][j+1]);
                    }
                }
                
                // calc Y gradient
                for (i=-1;i<=1;i++) {
                    for (j=-1;j<=1;j++) {
                        sumY += (int) ( (*(graydata + x + i + (y + j) 
* infogray.stride)) * Gy[i+1][j+1]);
                    }
                }
                
                sum = abs(sumX) + abs(sumY);
                
            }
            
            if (sum>255) sum = 255;
            if (sum<0) sum = 0;
            
            *(edgedata + x + y*infogray.width) = 255 - (uint8_t) sum;
            
            
            
        }
    }
    
    AndroidBitmap_unlockPixels(env, bitmapgray);
    AndroidBitmap_unlockPixels(env, bitmapedges);

    
}

The findEdges routine shares much in common with the prior two functions:

  1. Like the convertToGray function, this function takes two bitmap parameters, but in this case, both are grayscale.
  2. The bitmaps are interrogated to ensure that they are of the expected format.
  3. The bitmap pixels are locked and unlocked appropriately.
  4. The algorithm iterates over the source image's rows and columns.

Unlike the prior two functions, this function compares each pixel to the pixels around it, rather than simply performing a mathematical operation on the pixel value itself. The algorithm implemented in this function is a variant of the Sobel Edge Detection algorithm. In this implementation, I am comparing each pixel to its neighbors with a border of one pixel in each direction. Variants of this and other algorithms may use larger "borders" to obtain different results. Comparing each pixel to its neighbors accentuates the contrast between pixels and in doing so highlights the "edges."

I am not going to go into the math involved in this algorithm for two reasons. First, it is beyond the scope of this tutorial to care about the math itself. And second, the exact purpose of this tutorial — to (re)use existing C source code — is demonstrated by using an existing image processing algorithm. You are able to obtain the desired results without reinventing the wheel or having to port this code to Java technology. C is an ideal environment for working with image data, thanks to pointer arithmetic.

For more information about image processing algorithms, please see Resources.

5 of 10 | Previous | Next

Comments



static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=646128
TutorialTitle=Reuse existing C code with the Android NDK
publish-date=04122011
author1-email=fableson@navitend.com
author1-email-cc=