Image processing android app with Java Native Access (JNA)

in #utopian-io7 years ago (edited)

Repository

https://github.com/java-native-access/jna

Overview

In this tutorial we will create simple image processing app in which user will capture image from camera and then color image will be converted to gray scale image using C++. Image processing in java is expensive task, using C++ we can greatly improve the performance. App will use Native Access(JNA) library to call native code from Java side.
We will use some techniques that we learnt in previous tutorials on Java Native Access(JNA) library. For more information on Java Native Access(JNA) library please read previous tutorials series

Requirements

  • Android Studio 3.0 or higher
  • Android NDK

Difficulty

  • Intermediate

Algorithm

  • Get individual color channels from pixel
  • Calculate gray value
  • Create gray pixel

Example below uses red pixel

         ALPHA    RED      GREEN    BLUE
PIXEL = 11111111 11111111 00000000 00000000

// Right shift by 24 moves Byte 0(ALPHA) to Byte 4 position
// If MSB (most significant bit) is 1 then 1's will be appended from left side
// if MSB is 0 then 0's will be appended from left side
// & operation with 0xFF makes Byte 0, 1 and 2  = 0 and we get ALPHA value

ALPHA = (PIXEL >> 24) & 0xFF
11111111 11111111 11111111 11111111
00000000 00000000 00000000 11111111 &
--------------------------------------
00000000 00000000 00000000 11111111

// Right shift by 16 moves Byte 1(RED) to Byte 4 position
// If MSB (most significant bit) is 1 then 1's will be appended from left side
// if MSB is 0 then 0's will be appended from left side
// & operation with 0xFF makes Byte 0, 1 and 2  = 0 and we get RED value

RED = (PIXEL >> 16) & 0xFF
11111111 11111111 11111111 11111111
00000000 00000000 00000000 11111111 &
--------------------------------------
00000000 00000000 00000000 11111111

// Right shift by 8 moves Byte 2(GREEN) to Byte 4 position
// If MSB (most significant bit) is 1 then 1's will be appended from left side
// if MSB is 0 then 0's will be appended from left side
// & operation with 0xFF makes Byte 0, 1 and 2  = 0 and we get GREEN value

GREEN = (PIXEL >> 8) & 0xFF
11111111 11111111 11111111 00000000
00000000 00000000 00000000 11111111 &
--------------------------------------
00000000 00000000 00000000 00000000

// BLUE byte is already on Byte 4 position no need to shift
// & operation with 0xFF makes Byte 0, 1 and 2  = 0 and we get BLUE value

BLUE = (PIXEL) & 0xFF
11111111 11111111 00000000 00000000
00000000 00000000 00000000 11111111 &
--------------------------------------
00000000 00000000 00000000 00000000

// Average of RED, GREEN and BLUE is GRAY

GRAY = (RED + GREEN + BLUE) / 3

//We need to assign same GRAY value to RED,GREEN and BLUE
// ALPHA remains same

GRAY_PIXEL =  (ALPHA << 24) | (GRAY << 16) | (GRAY << 8) | GRAY

Tutorial covers

  • Creating Android Studio project
  • Configuring JNA AAR library
  • Creating UI
  • Capture image and showing in ImageView
  • Convert bitmap image to pixels
  • Convert color pixels to gray scale in C++
  • Loading and mapping C++ shared library in Java
  • Calling native method and showing gray scale image

Guide

1. Creating Android Studio project

Create new android project and change Application name you can also change Company domain and Package name according to requirements, select Include C++ support and click next

p1.png

Select minimum SDK version and click next

p2.png

Select Empty Activity and click next

p3.png

Change Activity Name and Layout Name according to requirements and click Next

p4.png

Select C++ Standard as Toolchain Default and click Finish to create project

p5.png

If you get NDK not configured error in Messages window then click on File menu and select Project Structure and set Android NDK location.

p6.png

2. Configuring JNA AAR library

Download jna.aar and create New Module and select Import JAR/AAR Package and click Next

p7.png

Select jna.aar file from file system and click Finish

p8.png

jna module added to project, open build.gradle file and add jna module under dependencies

dependencies {
    implementation project(':jna')
    ....
    ....
}

3. Creating UI

Our app will use camera so lets add camera permission in AndroidManifest.xml file

<uses-feature android:name="android.hardware.camera" android:required="true" />



p10.png

Main layout contains only Button and ImageView user will click on Button to capture image from camera and resulting gray scale image will be shown in ImageView. Here is layout xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Capture Image"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:background="#e5e5e5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>

Main screen should look like this

p11.png

To access Button and ImageView in Java

public class MainActivity extends AppCompatActivity {
    private Button button;
    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageView);
    }
}

4. Capture image and showing in ImageView

In this section first we will add click listener on button in onCreate() method, click to this button will call captureImage() method

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        captureImage();
    }
});

Call to captureImage() method will starts new camera activity with image capture intent

//will open camera activity
private void captureImage() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
    }
}

Result of camera activity will be handled in onActivityResult() method. We need to override this method, in this method we will get image result and assign it to ImageView

//result of camera activity handled here
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        //getting mutable bitmap from intent and set it to ImageView
        Bundle extras = data.getExtras();
        Bitmap bitmap = ((Bitmap) extras.get("data"))
                .copy(Bitmap.Config.ARGB_8888, true);
        imageView.setImageBitmap(bitmap);
    }
}

Here is complete code for image capture and showing captured image in ImageView

public class MainActivity extends AppCompatActivity {
    private Button button;
    private ImageView imageView;
    private static final int REQUEST_IMAGE_CAPTURE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageView);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                captureImage();
            }
        });
    }

    //will open camera activity
    private void captureImage() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
        }
    }

    //result of camera activity handled here
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            //getting mutable bitmap from intent and set it to ImageView
            Bundle extras = data.getExtras();
            Bitmap bitmap = ((Bitmap) extras.get("data"))
                    .copy(Bitmap.Config.ARGB_8888, true);
            imageView.setImageBitmap(bitmap);
        }
    }
}

5. Convert bitmap image to pixels

We now have bitmap image, to convert bitmap image to pixels

int width = bitmap.getWidth();
int height = bitmap.getHeight();

int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

6. Convert color pixels to gray scale in C++

Open native-lib.cpp file in cpp folder and remove everything. Method we want to export is marked with extern "C" to avoid name mangling. It accepts two arguments one is pixels array and other one is length of pixels array.
For more information of passing Java arrays to C++ please read tutorial T5.

extern "C"
void toGrayScale(int *pixels, int len) {
    for (int i = 0; i < len; i++) {

        //getting individual color values from each pixel
        int A = (pixels[i] >> 24) & 0xFF;
        int R = (pixels[i] >> 16) & 0xFF;
        int G = (pixels[i] >> 8) & 0xFF;
        int B = pixels[i] & 0xFF;

        //averaging Red, Green and Blue value to get gray scale value
        int gray = (R + G + B) / 3;

        //assign same gray value to Red, Green and Blue.
        //alpha value is unchanged
        pixels[i] = (A << 24) | (gray << 16) | (gray << 8) | gray;
    }
}

7. Loading and mapping C++ shared library in Java

static {
    Native.register(MainActivity.class, "native-lib");
}

In static block of a class we load our shared library, first argument to Native.register() method is the class in which we defined our native methods and 2nd argument is the name of native shared library. Name of shared library is native-lib we can change this default name in CMakeLists.txt file which is available in app folder. Loading should be done in static block which will load shared library during class loading time.
To map native method void toGrayScale(int *pixels, int len) in Java we need to add native keyword which tells compiler that method is implemented in native code.

public native void toGrayScale(int pixels[], int len);

To call native method in Java is same as calling other Java methods

toGrayScale(pixels, pixels.length);

8. Calling native method and showing gray scale image

//calling native method
toGrayScale(pixels, pixels.length);

//updating grayscale pixels of bitmap and showing in ImageView
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
imageView.setImageBitmap(bitmap);

Complete code

Layout xml file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Capture Image"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:background="#e5e5e5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>
C++ code
extern "C"
void toGrayScale(int *pixels, int len) {
    for (int i = 0; i < len; i++) {
        //getting individual color values from each pixel
        int A = (pixels[i] >> 24) & 0xFF;
        int R = (pixels[i] >> 16) & 0xFF;
        int G = (pixels[i] >> 8) & 0xFF;
        int B = pixels[i] & 0xFF;

        //averaging Red, Green and Blue value to get gray scale value
        int gray = (R + G + B) / 3;

        //assign same gray value to Red, Green and Blue.
        //alpha value is unchanged
        pixels[i] = (A << 24) | (gray << 16) | (gray << 8) | gray;
    }
}
Java code
public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    private Button button;
    private ImageView imageView;
    private static final int REQUEST_IMAGE_CAPTURE = 1;

    public native void toGrayScale(int pixels[], int len);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageView);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                captureImage();
            }
        });
    }

    //will open camera activity
    private void captureImage() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
        }
    }

    //result of camera activity handled here
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            //getting mutable bitmap from intent and set it to ImageView
            Bundle extras = data.getExtras();
            Bitmap bitmap = ((Bitmap) extras.get("data"))
                    .copy(Bitmap.Config.ARGB_8888, true);

            //getting pixels from bitmap
            int width = bitmap.getWidth();
            int height = bitmap.getHeight();

            int[] pixels = new int[width * height];
            bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

            //calling native method
            toGrayScale(pixels, pixels.length);

            //updating grayscale pixels of bitmap and showing in ImageView
            bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
            imageView.setImageBitmap(bitmap);
        }
    }
}

Output

p12.png

Github

Complete project available on github. Clone repo and open project name T6.

Sort:  

Hey @kabooom
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Congratulation kabooom! Your post has appeared on the hot page after 68min with 9 votes.

Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend few advices for your upcoming contributions:

  • In the images where you have code, put the most visible code. In your photos it is difficult to see the code.
  • Work on improving your post for English mistakes and incorrectly written words and sentences, as those could lead your author to lose track of your purpose.

Looking forward to your upcoming tutorials.

Link to the Answers of the Questionnaire -

Click here


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Loading...