Rotating a bitmap using JNI & NDK
Asked Answered
T

2

13

Background:

I've decided that since bitmaps take a lot of memory which can cause out-of-memory errors easily, I will put the hard, memory consuming work on C/C++ code .

The steps I use for rotating a bitmap are:

  1. read bitmap info (width,height)
  2. store bitmap pixels into an array.
  3. recycle the bitmap.
  4. create a new bitmap of opposite size.
  5. put the pixels into the new bitmap.
  6. free the pixels and return the bitmap.

The problem:

Even though everything seems to run without any errors, the output image is not a rotation of the original. In fact, it ruins it completely.

The rotation should be counter clock wise, 90 degrees.

Example (screenshot is zoomed in) of what I get:

enter image description here

So as you can see, not only the colors became weirder, but the size doesn't match what I've set to it. Something is really weird here.

Maybe I don't read/put the data correctly?

Of course this is just an example. The code should work fine on any bitmap, as long as the device has enough memory to hold it. Also, I might want to do other operations on the bitmap other than rotating it.

Code I've created :

Android.mk file:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := JniTest
LOCAL_SRC_FILES := JniTest.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDFLAGS += -ljnigraphics
include $(BUILD_SHARED_LIBRARY)
APP_OPTIM := debug
LOCAL_CFLAGS := -g

cpp file:

#include <jni.h>
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <android/bitmap.h>
#include <cstring>
#include <unistd.h>

#define  LOG_TAG    "DEBUG"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

extern "C"
  {
  JNIEXPORT jobject JNICALL Java_com_example_jnitest_MainActivity_rotateBitmapCcw90(JNIEnv * env, jobject obj, jobject bitmap);
  }

JNIEXPORT jobject JNICALL Java_com_example_jnitest_MainActivity_rotateBitmapCcw90(JNIEnv * env, jobject obj, jobject bitmap)
  {
  //
  //getting bitmap info:
  //
  LOGD("reading bitmap info...");
  AndroidBitmapInfo info;
  int ret;
  if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0)
    {
    LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
    return NULL;
    }
  LOGD("width:%d height:%d stride:%d", info.width, info.height, info.stride);
  if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
    {
    LOGE("Bitmap format is not RGBA_8888!");
    return NULL;
    }
  //
  //read pixels of bitmap into native memory :
  //
  LOGD("reading bitmap pixels...");
  void* bitmapPixels;
  if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0)
    {
    LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    return NULL;
    }
  uint32_t* src = (uint32_t*) bitmapPixels;
  uint32_t* tempPixels = new uint32_t[info.height * info.width];
  int stride = info.stride;
  int pixelsCount = info.height * info.width;
  memcpy(tempPixels, src, sizeof(uint32_t) * pixelsCount);
  AndroidBitmap_unlockPixels(env, bitmap);
  //
  //recycle bitmap - using bitmap.recycle()
  //
  LOGD("recycling bitmap...");
  jclass bitmapCls = env->GetObjectClass(bitmap);
  jmethodID recycleFunction = env->GetMethodID(bitmapCls, "recycle", "()V");
  if (recycleFunction == 0)
    {
    LOGE("error recycling!");
    return NULL;
    }
  env->CallVoidMethod(bitmap, recycleFunction);
  //
  //creating a new bitmap to put the pixels into it - using Bitmap Bitmap.createBitmap (int width, int height, Bitmap.Config config) :
  //
  LOGD("creating new bitmap...");
  jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
  jstring configName = env->NewStringUTF("ARGB_8888");
  jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
  jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
  jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass, valueOfBitmapConfigFunction, configName);
  jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, info.height, info.width, bitmapConfig);
  //
  // putting the pixels into the new bitmap:
  //
  if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &bitmapPixels)) < 0)
    {
    LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    return NULL;
    }
  uint32_t* newBitmapPixels = (uint32_t*) bitmapPixels;
  int whereToPut = 0;    
  for (int x = info.width - 1; x >= 0; --x)
    for (int y = 0; y < info.height; ++y)
      {
      uint32_t pixel = tempPixels[info.width * y + x];
      newBitmapPixels[whereToPut++] = pixel;
      }
  AndroidBitmap_unlockPixels(env, newBitmap);
  //
  // freeing the native memory used to store the pixels
  //
  delete[] tempPixels;
  return newBitmap;
  }

java file:

  static
    {
    System.loadLibrary("JniTest");
    }

  /**
   * rotates a bitmap by 90 degrees counter-clockwise . <br/>
   * notes:<br/>
   * -the input bitmap will be recycled and shouldn't be used anymore <br/>
   * -returns the rotated bitmap . <br/>
   * -could take some time , so do the operation in a new thread
   */
  public native Bitmap rotateBitmapCcw90(Bitmap bitmap);

...
  Bitmap rotatedImage=rotateBitmapCcw90(bitmapToRotate);

EDIT: after I got my answer, I wish to share this code and notes about it to everyone:

  • in order for it to work, i've replaced in the code every instance of "uint16_t" with "uint32_t" (that's the bug on my code I've asked about).

  • input and output bitmap must be with 8888 config (which is ARGB )

  • input bitmap will be recycled during the process.

  • the code rotates the image 90 degrees counter clock wise. Of course you can change it depending on your needs.


better solution

i've made a nice post having this functionality and others, here .

Thermodynamics answered 18/1, 2013 at 12:10 Comment(10)
Since you're using ARGB_8888 format should you be using uint32_t for creating rotated Bitmap?Dragonfly
What makes you think it'll take less memory to hold a bitmap in C than it does in Java?Chouest
harism , changed it and it worked !!! thank you . please put an answer so that i could tick it . @Chouest : it doesn't . it removes the limitation of max heap size , which might be too low on some cases .for example , an 8MP camera image could take 30MB or RAM , and using the normal rotation technique will make it use double this amount of memory .Thermodynamics
@Dragonfly , do you know by any chance how i could also use other bitmap configs , and if i will need to use uint16_t for them ?Thermodynamics
@androiddeveloper unfortunately I'm not exactly sure how e.g RGB_565 is packed into memory. I would guess they pack them as uint16_t to save memory but it's best to verify this first.Dragonfly
There are much easier, more efficient ways of rotating a bitmap in android. Try using a matrix post rotateExteroceptor
@Exteroceptor if you do it, you will use 2 bitmaps and it can cause OOM , right? that's the whole point of why I made this code. using this code, you use only the needed size of the bitmap, plus you have full control of what to put into it.Thermodynamics
@androiddeveloper : Please enlighten me how to achieve like these : #26405064.Valency
@VikalpPatel Sorry, I'm not an expert in opencv4android, and as I remember, it's quite a large library with tons of features. However, as I've read your question, this looks like something that's not related to bitmaps... I'm pretty sure there is a solution for this already. do you want me to check it out?Thermodynamics
@androiddeveloper : I would be much grateful if you check out how to achieve something like Virtual Make Over application do these. As I'm much far away Image Processing keeps baffling as zero output comes EOD. :/Valency
D
6

Since you're using ARGB_8888 format every pixel is an uint32_t not uint16_t. Try changing your rotated Bitmap creation to use uint32_t for source and destination arrays and it should work better.

Dragonfly answered 18/1, 2013 at 12:48 Comment(2)
thank you . it worked great . i can't believe i had so much work and i didn't do this thing right .Thermodynamics
do you have any nice speed optimizations tips for this code ?Thermodynamics
H
0

I tried Samsung S5 (Android 6, API 21) and didn't see the difference between using your JNI code and Bitmap.createBitmap():

val bitmapHolder = JniBitmapHolder()
bitmapHolder.storeBitmap(bitmap)
bitmapHolder.rotateBitmapCw90()
bitmapHolder.bitmapAndFree

Throwing OutOfMemoryError "Failed to allocate a 63489036 byte allocation with 16776704 free bytes and 39MB until OOM" AndroidBitmap_lockPixels() failed ! error=-1 java.lang.OutOfMemoryError: Failed to allocate a 63489036 byte allocation with 16776704 free bytes and 39MB until OOM at dalvik.system.VMRuntime.newNonMovableArray(Native Method) at android.graphics.Bitmap.nativeCreate(Native Method) at android.graphics.Bitmap.createBitmap(Bitmap.java:975) at android.graphics.Bitmap.createBitmap(Bitmap.java:946) at android.graphics.Bitmap.createBitmap(Bitmap.java:913) at com.jni.bitmap_operations.JniBitmapHolder.jniGetBitmapFromStoredBitmapData(Native Method) at com.jni.bitmap_operations.JniBitmapHolder.getBitmap(JniBitmapHolder.java:88) at com.jni.bitmap_operations.JniBitmapHolder.getBitmapAndFree(JniBitmapHolder.java:93)

vs

Bitmap.createBitmap(
   bitmap,
    0,
    0,
     bitmap.width,
     bitmap.height,
     matrix,
   true
)

Throwing OutOfMemoryError "Failed to allocate a 63489036 byte allocation with 16765968 free bytes and 39MB until OOM" java.lang.OutOfMemoryError: Failed to allocate a 63489036 byte allocation with 16765968 free bytes and 39MB until OOM at dalvik.system.VMRuntime.newNonMovableArray(Native Method) at android.graphics.Bitmap.nativeCreate(Native Method) at android.graphics.Bitmap.createBitmap(Bitmap.java:975) at android.graphics.Bitmap.createBitmap(Bitmap.java:946) at android.graphics.Bitmap.createBitmap(Bitmap.java:877)

So only adding largeHeap helps, without it doesn't work on such device at all

p.s. I was trying to rotate bitmap of 2988x5312 (16 megapixels camera) which I got from Camera (capture photo with highest available resolution)

Updated

Added bitmap.recycle() after bitmapHolder.storeBitmap(bitmap) to make it work!

val bitmapHolder = JniBitmapHolder()
bitmapHolder.storeBitmap(bitmap)
bitmap.recycle()
bitmapHolder.rotateBitmapCw90()
bitmapHolder.bitmapAndFree
Haiti answered 20/12, 2023 at 12:51 Comment(6)
First, things have changed and this library isn't needed anymore as Bitmaps don't cause OOM by being on the heap (because they are not there anymore). Second, the point of the library is to avoid having 2 Bitmaps on the heap at the same time (and cache if you wish). So, if you have max of 10MB heap, and you have a bitmap that takes 9MB, using the normal API would cause OOM as you need a new bitmap that holds the rotated content while you read from the original. So after you store the bitmap on JNI side you can release it on the Java side, rotate, and get the new one.Thermodynamics
So your order of operations is incorrect. You should do as such: store using JNI, release the Java Bitmap, rotate in JNI, get back a new Java (rotated) Bitmap from JNI. The changes of how Bitmaps are stored are talked about here: https://mcmap.net/q/138747/-how-does-bitmap-allocation-work-on-oreo-and-how-to-investigate-their-memory/878126Thermodynamics
@androiddeveloper thanks. The library is not needed from specific version of Android - Oreo? But for Android 6-7 is still needed?Haiti
Yes. From Oreo things have changed a lot. Very hard to reach an issue there. I don't get though how to detect an issue of OOM in this case and try to overcome it (such as releasing Bitmaps) though, because according to my tests it's not this exception anymore.Thermodynamics
@androiddeveloper understood. Yep, I tried to add bitmap.recycle() right after bitmapHolder.storeBitmap(bitmap) and now it works on old Samsung S5 even without largeHeap=true. Thanks!Haiti
I think that it's time to move to Oreo though. People shouldn't need this library anymore, except maybe for educational purposes or something...Thermodynamics

© 2022 - 2024 — McMap. All rights reserved.