使用JNI和NDK旋转位图

12

背景:

我决定由于位图占用大量内存,很容易导致内存溢出错误,因此将耗费大量内存的工作放在C/C++代码上。

旋转位图的步骤如下:

  1. 读取位图信息(宽度、高度)
  2. 将位图像素存储到数组中。
  3. 回收位图。
  4. 创建一个大小相反的新位图。
  5. 将像素放入新位图中。
  6. 释放像素并返回位图。

问题:

尽管一切似乎没有出现任何错误,但输出的图像并不是原始图像的旋转。实际上,它完全破坏了它。

旋转应该是逆时针旋转90度。

示例(截图已放大):

enter image description here

所以你可以看到,不仅颜色变得奇怪,而且大小也不符合我的设定。这里真的很奇怪。

也许我没有正确地读取/放置数据?

当然,这只是一个例子。只要设备有足够的内存来保存它,代码应该可以在任何位图上正常工作。此外,我可能还想对位图进行除旋转以外的其他操作。

我创建的代码:

Android.mk文件:

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文件:

#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文件:

  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);

编辑:在得到答案后,我希望与大家分享这段代码以及关于它的笔记:

  • 为了使其工作正常,我已经将代码中每个"uint16_t"实例替换为"uint32_t"(这是我所问的代码错误)。

  • 输入和输出位图必须是8888配置(即ARGB)。

  • 在过程中,输入位图将被回收利用。

  • 该代码将图像逆时针旋转90度。当然,您可以根据需要进行更改。


更好的解决方案

我已经发布了一个很好的帖子,其中包括此功能和其他功能,请点击这里


由于您正在使用“ARGB_8888”格式,因此在创建旋转的位图时应该使用“uint32_t”吗? - harism
1
哈里斯姆,更改了它,它工作了!!!谢谢。请提供一个答案,以便我可以勾选它。@Ridcully:它不会。它消除了最大堆大小的限制,在某些情况下可能太低。例如,8MP相机图像可能需要30MB内存,并使用正常旋转技术将使其使用双倍的内存。 - android developer
@androiddeveloper 很抱歉,我不确定例如RGB_565是如何打包到内存中的。我猜他们会将它们打包成“uint16_t”以节省内存,但最好先验证一下。 - harism
在Android中,有更简单、更高效的方法来旋转位图。尝试使用矩阵后旋转。 - Jameo
@Jameo 如果你这样做,会使用两个位图,并可能导致OOM,这就是我编写此代码的原因。使用此代码,您仅使用所需的位图大小,而且可以完全控制将什么放入其中。 - android developer
显示剩余5条评论
1个回答

6

由于您正在使用ARGB_8888格式,因此每个像素都是uint32_t而不是uint16_t。尝试更改旋转位图创建以使用uint32_t作为源和目标数组,应该可以更好地工作。


谢谢。它运行得很好。我简直不敢相信我有这么多工作,却没有做对这件事情。 - android developer
你有没有针对这段代码的优化建议?可以提供一些速度优化的技巧吗? - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接