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.
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:
- Create a new folder named jni beneath your project file.
- Within the jni folder, create a file named Android.mk, which contains the makefile instructions to properly build and name your library.
- 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:
- Compile the ibmphotophun.c source file into a shared library.
- Name the shared library. By default, the shared library naming convention is lib<modulename>.so. So, the resulting file here is named libibmphotophun.so.
- 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:
- The
AndroidBitmapInfostructure, defined in bitmap.h, is helpful for learning about aBitmapobject. - The
AndroidBitmap_getInfofunction, found in the jnigraphics library, obtains information about a specificBitmapobject. - The next step is to ensure that the bitmaps passed into the
convertToGrayfunction are of the expected format. - The
AndroidBitmap_lockPixelsfunction locks down the image data so you can perform operations directly on the data. - The
AndroidBitmap_unlockPixelsfunction unlocks previously locked pixel data. These functions should be called as a "lock/unlock pair". - Sandwiched between the lock and unlock functions you see the pixel operations.
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:
- When the image data is "locked," the base address of the image data
is referenced by a pointer named
pixelscolorfor the input color image andpixelsgrayfor the output grayscale image. - Two
for-nextloops allow you to iterate over the entire image.- First, you iterate over the height of the image, one pass
per "row." Use the
infocolor.heightvalue to get the count of the rows. - 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.
- 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.
- 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.
- First, you iterate over the height of the image, one pass
per "row." Use the
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:
- This function requires only a single grayscale bitmap. The image passed in is modified in place.
- 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.
- 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
changeBrightnessfunction 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 namedvand 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:
- Like the
convertToGrayfunction, this function takes two bitmap parameters, but in this case, both are grayscale. - The bitmaps are interrogated to ensure that they are of the expected format.
- The bitmap pixels are locked and unlocked appropriately.
- 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.



