乐趣区

关于android:Android-Bitmap像素排列与JNI操作

图像的数值示意

RGB

RGB 色彩模型即红绿蓝色彩模型。由模拟生物视网膜三种视锥细胞产生,之后通过三原色叠加来进行彩色图像显示。通过在彩色上一直叠加三原色来显示不同的色彩。在 RGB 色彩空间中,别离将 RGB 作为笛卡尔坐标系中 XYZ 坐标系产生。每一个色彩取值范畴为[0,256)

RGB 是从色彩发光的原理来设计定的,艰深点说它的颜色混合形式就如同有红、绿、蓝三盏灯,当它们的光互相叠合的时候,色调相混,而亮度却等于两者亮度之总和,越混合亮度越高,即加法混合。

红、绿、蓝三个色彩通道每种色各分为 256 阶亮度,在 0 时“灯”最弱——是关掉的,而在 255 时“灯”最亮。当三色灰度数值雷同时,产生不同灰度值的灰色调,即三色灰度都为 0 时,是最暗的彩色调;三色灰度都为 255 时,是最亮的红色调。

对一种色彩进行编码的办法统称为色彩空间或色域。

用最简略的话说,世界上任何一种色彩的“色彩空间”都可定义成一个固定的数字或变量。RGB(红、绿、蓝)只是泛滥色彩空间的一种。采纳这种编码方法,每种色彩都可用三个变量来示意 - 红色绿色以及蓝色的强度。记录及显示彩色图像时,RGB 是最常见的一种计划。

所以每一个图像都能够由 RGB 组成,那么一个像素点的 RGB 该如何示意呢?音频外面的每一个采样(sample)均应用 16 个比特来示意,那么像素外面的子像素又该如何示意呢?罕用的示意形式有以下几种。

  • 浮点示意:取值范畴为 0.0~1.0,比方,在 OpenGL ES 中对每一个子像素点的示意应用的就是这种表达方式。
  • 整数示意:取值范畴为 0~255 或者 00~FF,8 个比特示意一个子像素,32 个比特示意一个像素

Android 平台上 RGB_565 的示意办法为 16 比特模式示意一个像素,R 用 5 个比特来示意,G 用 6 个比特来示意,B 用 5 个比特来示意。

对于一幅图像,个别应用整数示意办法来进行形容,比方计算一张 1280×720 的 RGBA_8888 图像的大小,可采纳如下形式:

1280 * 720 * 4 = 3.516MB

这也是位图(bitmap)在内存中所占用的大小,所以每一张图像的裸数据都是很大的。对于图像的裸数据来讲,间接在网络上进行传输也是不太可能的,所以就有了图像的压缩格局。

安卓图像引擎解码的规定,在 JNI 中解析进去的是 ABGR 程序,获取 RGB 数据的时候要留神。

Android 中的色彩值通常遵循 RGB/ARGB 规范,应用时通常以 “#” 字符结尾的 8 位 16 进制示意。前缀 0x 示意十六进制(基数为 16),其中 ARGB 顺次代表透明度(Alpha)、红色(Red)、绿色(Green)、蓝色(Blue),取值范畴为0 ~ 255(即 16 进制的0x00 ~ 0xff)。
A0x000xff示意从通明到不通明,RGB0x000xff示意色彩从浅到深。当 RGB 全取最小值 ( 0 或 0x000000) 时色彩为彩色,全取最大值 (255 或 0xffffff) 时色彩为红色。

  • 红色:(255,0,0)或 0x00FF0000
  • 绿色:(0,255,0)或 0x0000FF00
  • 蓝色:(255,255,255)或 0x00FFFFFF

大小端字节序

这是在 Android 中应用 RGB 数据的时候面临的问题。

以内存中0x0A0B0C0D(0x 前缀代表十六进制)的寄存形式为例,别离有以下几种形式:

小端序

  • (little-endian)又称 小尾序

数据以 8bit 为单位:

地址增长方向
0x0D 0x0C 0x0B 0x0A ...

最低位字节是0x0D 存储在最低的内存地址处。前面字节顺次存在前面的地址处。

大端序

  • (big-endian)又称 大尾序

数据以 8bit 为单位:

地址增长方向
0x0A 0x0B 0x0C 0x0D

最高位字节是 0x0A` 存储在最低的内存地址处。下一个字节 0x0B 存在前面的地址处。正相似于十六进制字节从左到右的浏览程序。

混合序

  • (middle-endian)具备更简单的程序。

以 PDP-11 为例,0x0A0B0C0D 被存储为:

32bit 在 PDP-11 的存储形式

地址增长方向
0x0B 0x0A 0x0D 0x0C

能够看作高 16bit 和低 16bit 以大端序存储,但 16bit 外部以小端存储。

Bitmap 像素排列

Android 中 Java/Kotlin 默认应用大端字节序,所见即所得,NDK 中 C /C++ 默认应用小端字节序。

这个很容易验证:

import java.nio.ByteOrder
......
// 调用
ByteOrder.nativeOrder()
....
// 失去
LITTLE_ENDIAN

咱们在 Android 平台下创立 Bitmap 时:

Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)

Bitmap.config.ARGB_8888的正文中就指明了:

int color = (A & 0xff) << 24 | (B & 0xff) << 16 | (G & 0xff) << 8 | (R & 0xff);

这里的字节顺应该为 ABGR.

然而咱们在 Android 中读取 bitmap 中的像素值有两种形式并不是依照这个程序取值的, 这是为什么?

getPixel() 取值程序

办法:

public void getPixels(@ColorInt int[] pixels, int offset, int stride,
                          int x, int y, int width, int height) {
  .......
nativeGetPixels(mNativePtr, pixels, offset, stride,
                        x, y, width, height);
}

最终调用 native 的办法nativeGetPixels, 咱们先不论 Native 是如何解决的。

这里将 Bitmap 中的像素数据将 copy 到 pixels 数组中,pixels 数组是依照 ColorSpace.Named#SRGB 规定排列的。

即每一个 pixel 都是按 ARGB 四个重量 8 位排列压缩而成的一个 int 值。

像素组装:

int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);

获取单个像素值:

 int A = (color >> 24) & 0xff; // or color >>> 24
 int R = (color >> 16) & 0xff;
 int G = (color >>  8) & 0xff;
 int B = (color) & 0xff;

copyPixelsToBuffer() 取值程序

看下具体方法:

/**
     * <p>Copy the pixels from the buffer, beginning at the current position,
     * overwriting the bitmap's pixels. The data in the buffer is not changed
     * in any way (unlike setPixels(), which converts from unpremultipled 32bit
     * to whatever the bitmap's native format is. The pixels in the source
     * buffer are assumed to be in the bitmap's color space.</p>
     * <p>After this method returns, the current position of the buffer is
     * updated: the position is incremented by the number of elements read from
     * the buffer. If you need to read the bitmap from the buffer again you must
     * first rewind the buffer.</p>
     * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
     */
public void copyPixelsFromBuffer(Buffer src) {
    .....
    
    nativeCopyPixelsFromBuffer(mNativePtr, src);
     
    .....
}

这里说The data in the buffer is not changed

也就是说 native 层的操作将 bitmap 的排列就变成了 RGBA ,buffer 没有扭转程序

咱们简略验证下:

 val tempBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
val canvas = Canvas(tempBitmap)
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.rgb(0x11, 0x22, 0x33)
canvas.drawRect(0f, 0f, tempBitmap.width.toFloat(), tempBitmap.height.toFloat(), paint)

val byteSize = tempBitmap.allocationByteCount
val byteBuffer: ByteBuffer = ByteBuffer.allocateDirect(byteSize)
tempBitmap.copyPixelsToBuffer(byteBuffer)
byteBuffer.rewind()
val out = ByteArray(4)
byteBuffer[out, 0, out.size]
val pixel = tempBitmap.getPixel(0,0)
val a = Color.alpha(pixel)
val r = Color.red(pixel)
val g = Color.green(pixel)
val b = Color.blue(pixel)
Log.d("pixel =", "${pixel}")
Log.d("pixel =", "a= ${a},r= ${r},g=${g}, b=${b}")
Log.d("pixel 16 =", "a= ${a.toString(16)},r= ${r.toString(16)},g=${g.toString(16)}, b=${b.toString(16)}")
for(element in out){Log.d("out =", element.toString(16))
}

查看打印的的值

pixel =:        {-15654349} 

pixel =:      {a= 255,r= 17,g=34, b=51}
// ARGB
pixel 16=:  {a= ff,r= 11,g=22, b=33}
// RGBA
out   =          {11, 22 ,33 , -1}

-1 取绝对值二进制反码 + 1 后的 16 进制即为 FF。

JNI 取值程序

之前说 Bitmap.config.ARGB_8888对应的 Bitmap 字节序为ABRG.

那么 JNI 中 ANDROID_BITMAP_FORMAT_RGBA_8888 也是如此。

简略验证下:

同样以下面的一个像素 0X112233 为例:

这里留神下咱们应用paint.color = Color.rgb(0x11, 0x22, 0x33)alpha 的值是默认的。

0xff000000 | (red << 16) | (green << 8) | blue;

kotlin:

external fun handleBitmapForSinglePixel(bitmap: Bitmap)

定义宏, 依照 ABGR 的程序取值:

#define RGB8888_A(p) ((p & (0xff<<24))      >> 24 )
#define RGB8888_B(p) ((p & (0xff << 16)) >> 16 )
#define RGB8888_G(p) ((p & (0xff << 8))  >> 8 )
#define RGB8888_R(p) (p & (0xff) )

对应 JNI 办法:

extern "C"
JNIEXPORT void JNICALL
Java_tt_reducto_ndksample_BitmapOps_handleBitmapForSinglePixel(JNIEnv *env, jobject thiz,
                                                               jobject bitmap) {
    AndroidBitmapInfo bitmapInfo;
//    memset(&bitmapInfo , 0 , sizeof(bitmapInfo));
    int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGE("AndroidBitmap_getInfo() bitmap failed ! error=%d", ret)
    }
    // 取得 Bitmap 的像素缓存指针:遍历从 Bitmap 内存 addrPtr 中读取 BGRA 数据
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGE("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
    }

    // 执行图片操作的逻辑
    // 获取宽高
    uint32_t mWidth = bitmapInfo.width;
    uint32_t mHeight = bitmapInfo.height;
    // 获取原生数据
    auto pixelArr = ((uint32_t *) addrPtr);

    LOGE("bitmap width = %d", mWidth)
    LOGE("bitmap height = %d", mHeight)
    LOGE("bitmap format = %d", bitmapInfo.format)
    int a,r, g, b;
    for (int x = 0; x < mWidth; ++x) {for (int y = 0; y < mHeight; ++y) {LOGE("handleBitmapForSinglePixel %d", pixelArr[0])
            void *pixel = nullptr;
            // 挪动像素指针
            pixel = pixelArr + y * mWidth + x;
            // 依照 ABGR 存储序列取值  获取指针对应的值
            uint32_t v = *((uint32_t *) pixel);
            // 
            a = RGB8888_A(v);
            r = RGB8888_R(v);
            g = RGB8888_G(v);
            b = RGB8888_B(v);
            //
            LOGD("bitmapInfo a %d", a)
            LOGD("bitmapInfo r %d", r)
            LOGD("bitmapInfo g %d", g)
            LOGD("bitmapInfo b %d", b)

        }
    }
    // 开释缓存指针
    AndroidBitmap_unlockPixels(env, bitmap);
}

查看打印值:

2020-08-19 16:58:55.374 9562-9562/tt.reducto.ndksample E/TTNative: handleBitmapForSinglePixel -13426159
2020-08-19 16:58:55.374 9562-9562/tt.reducto.ndksample D/TTNative: bitmapInfo a 255
2020-08-19 16:58:55.374 9562-9562/tt.reducto.ndksample D/TTNative: bitmapInfo r 17
2020-08-19 16:58:55.374 9562-9562/tt.reducto.ndksample D/TTNative: bitmapInfo g 34
2020-08-19 16:58:55.374 9562-9562/tt.reducto.ndksample D/TTNative: bitmapInfo b 51

-13426159 转成二进制:

1100 1100 1101 1101 1110 1111    
----------------------------- 取反
0011 0011 0010 0010 0001 0000
----------------------------- +1 
0011 0011 0010 0010 0001 0001  
        b                    g                     r

Skia 解决

Android 中 bitmap 的解决通过:

Java 层函数——Native 层函数——Skia 库函数——对应第三方库函数(libjpeg)

所有 Bitmap.createBitmap() 对应的 native 操作在 ..android/graphics/Bitmap.cpp 中:

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jlong colorSpacePtr) {
    // 转换色域
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
    if (NULL != jColors) {size_t n = env->GetArrayLength(jColors);
        if (n < SkAbs32(stride) * (size_t)height) {doThrowAIOOBE(env);
            return NULL;
        }
    }
    // ARGB_4444 is a deprecated format, convert automatically to 8888
    if (colorType == kARGB_4444_SkColorType) {
        // 将 ARGB_4444 强转成 kN32_SkColorType
        colorType = kN32_SkColorType;
    }
    sk_sp<SkColorSpace> colorSpace;
    if (colorType == kAlpha_8_SkColorType) {colorSpace = nullptr;} else {colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr);
    }
    // 
    SkBitmap bitmap;
    bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType,
                colorSpace));
    // 8.0 当前 bitmap 的创立内存调配都是在 native 上
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
    if (!nativeBitmap) {ALOGE("OOM allocating Bitmap with dimensions %i x %i", width, height);
        doThrowOOME(env);
        return NULL;
    }
    // 填充色值
    if (jColors != NULL) {GraphicsJNI::SetPixels(env, jColors, offset, stride, 0, 0, width, height, &bitmap);
    }
    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}

这里第一步就是将 Bitmap.Config.ARGB_8888 转成 skia 域的色彩类型:

 SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);

看下 GraphicsJNI.h 中对应的办法定义:

/*
 *  LegacyBitmapConfig is the old enum in Skia that matched the enum int values
 *  in Bitmap.Config. Skia no longer supports this config, but has replaced it
 *  with SkColorType. These routines convert between the two.
 */
static SkColorType legacyBitmapConfigToColorType(jint legacyConfig);

再去看下 GraphicsJNI.cpp 中看下实现:

SkColorType GraphicsJNI::legacyBitmapConfigToColorType(jint legacyConfig) {const uint8_t gConfig2ColorType[] = {
        kUnknown_SkColorType,
        kAlpha_8_SkColorType,
        kUnknown_SkColorType, // Previously kIndex_8_SkColorType,
        kRGB_565_SkColorType,
        kARGB_4444_SkColorType,
        kN32_SkColorType,
        kRGBA_F16_SkColorType,
        kN32_SkColorType
    };
    if (legacyConfig < 0 || legacyConfig > kLastEnum_LegacyBitmapConfig) {legacyConfig = kNo_LegacyBitmapConfig;}
    return static_cast<SkColorType>(gConfig2ColorType[legacyConfig]);
}

因为咱们在 java 层传入的 Bitmap.Config.ARGB_8888 值为ARGB_8888(5)

与之对应的就是kN32_SkColorType

接下来咱们在 SkImageInfo.h 中看下 SkColorType:

/** \enum SkImageInfo::SkColorType
    Describes how pixel bits encode color. A pixel may be an alpha mask, a
    grayscale, RGB, or ARGB.
    kN32_SkColorType selects the native 32-bit ARGB format. On little endian
    processors, pixels containing 8-bit ARGB components pack into 32-bit
    kBGRA_8888_SkColorType. On big endian processors, pixels pack into 32-bit
    kRGBA_8888_SkColorType.
*/
enum SkColorType {
    kUnknown_SkColorType,      //!< uninitialized
    kAlpha_8_SkColorType,      //!< pixel with alpha in 8-bit byte
    kRGB_565_SkColorType,      //!< pixel with 5 bits red, 6 bits green, 5 bits blue, in 16-bit word
    kARGB_4444_SkColorType,    //!< pixel with 4 bits for alpha, red, green, blue; in 16-bit word
    kRGBA_8888_SkColorType,    //!< pixel with 8 bits for red, green, blue, alpha; in 32-bit word
    kRGB_888x_SkColorType,     //!< pixel with 8 bits each for red, green, blue; in 32-bit word
    kBGRA_8888_SkColorType,    //!< pixel with 8 bits for blue, green, red, alpha; in 32-bit word
    kRGBA_1010102_SkColorType, //!< 10 bits for red, green, blue; 2 bits for alpha; in 32-bit word
    kRGB_101010x_SkColorType,  //!< pixel with 10 bits each for red, green, blue; in 32-bit word
    kGray_8_SkColorType,       //!< pixel with grayscale level in 8-bit byte
    kRGBA_F16Norm_SkColorType, //!< pixel with half floats in [0,1] for red, green, blue, alpha; in 64-bit word
    kRGBA_F16_SkColorType,     //!< pixel with half floats for red, green, blue, alpha; in 64-bit word
    kRGBA_F32_SkColorType,     //!< pixel using C float for red, green, blue, alpha; in 128-bit word
    kLastEnum_SkColorType     = kRGBA_F32_SkColorType,//!< last valid value
#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType          = kBGRA_8888_SkColorType,//!< native ARGB 32-bit encoding
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType          = kRGBA_8888_SkColorType,//!< native ARGB 32-bit encoding
#else
    #error "SK_*32_SHIFT values must correspond to BGRA or RGBA byte order"
#endif
};

接着看上面

kN32_SkColorType依据字节序决定 kN32_SkColorTypef 的值,用到的宏 SK_PMCOLOR_BYTE_ORDER 在 SkPostConfig.h 中定义:


/**
 * SK_PMCOLOR_BYTE_ORDER can be used to query the byte order of SkPMColor at compile time. The
 * relationship between the byte order and shift values depends on machine endianness. If the shift
 * order is R=0, G=8, B=16, A=24 then ((char*)&pmcolor)[0] will produce the R channel on a little
 * endian machine and the A channel on a big endian machine. Thus, given those shifts values,
 * SK_PMCOLOR_BYTE_ORDER(R,G,B,A) will be true on a little endian machine and
 * SK_PMCOLOR_BYTE_ORDER(A,B,G,R) will be true on a big endian machine.
 */
#ifdef SK_CPU_BENDIAN
#  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
        (SK_ ## C3 ## 32_SHIFT == 0  &&             \
         SK_ ## C2 ## 32_SHIFT == 8  &&             \
         SK_ ## C1 ## 32_SHIFT == 16 &&             \
         SK_ ## C0 ## 32_SHIFT == 24)
#else
#  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
        (SK_ ## C0 ## 32_SHIFT == 0  &&             \
         SK_ ## C1 ## 32_SHIFT == 8  &&             \
         SK_ ## C2 ## 32_SHIFT == 16 &&             \
         SK_ ## C3 ## 32_SHIFT == 24)
#endif

所以小端字节序对应就是:

#  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
        (SK_ ## C0 ## 32_SHIFT == 0  &&             \
         SK_ ## C1 ## 32_SHIFT == 8  &&             \
         SK_ ## C2 ## 32_SHIFT == 16 &&             \
         SK_ ## C3 ## 32_SHIFT == 24)

这里用到了 SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT 这几个宏:

/**
 *  We check to see if the SHIFT value has already been defined.
 *  if not, we define it ourself to some default values. We default to OpenGL
 *  order (in memory: r,g,b,a)
 */
#ifndef SK_A32_SHIFT
#  ifdef SK_CPU_BENDIAN
#    define SK_R32_SHIFT    24
#    define SK_G32_SHIFT    16
#    define SK_B32_SHIFT    8
#    define SK_A32_SHIFT    0
#  else
#    define SK_R32_SHIFT    0
#    define SK_G32_SHIFT    8
#    define SK_B32_SHIFT    16
#    define SK_A32_SHIFT    24
#  endif
#endif

所以小端字节序解决:

#    define SK_R32_SHIFT    0
#    define SK_G32_SHIFT    8
#    define SK_B32_SHIFT    16
#    define SK_A32_SHIFT    24

回到 SkColorType 中:

#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType = kBGRA_8888_SkColorType,
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType = kRGBA_8888_SkColorType,
    
// SK_PMCOLOR_BYTE_ORDER(R,G,B,A) 开展后如下
SK_R32_SHIFT == 0 && SK_G32_SHIFT == 8 && SK_B32_SHIFT == 16 && SK_A32_SHIFT == 24
// 表达式返回
true 

综上:

这意味着 Bitmap.Config.ARGB_8888 会被转成 Skia 域中的色彩类型 kRGBA_8888_SkColorType 并以此格局外部存储。在将 RGBA 写入到小端字节序的内存中,就变成了ABGR.

ABGR也是咱们在 JNI 中获取 bitmap 像素值得程序。

接着往下看:

typedef uint32_t     SkPMColor
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {void* addr = calloc(size, 1);
    if (!addr) {return nullptr;}
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}
bool GraphicsJNI::SetPixels(JNIEnv* env, jintArray srcColors, int srcOffset, int srcStride,
        int x, int y, int width, int height, const SkBitmap& dstBitmap) {SkAutoLockPixels alp(dstBitmap);
    void* dst = dstBitmap.getPixels();
    FromColorProc proc = ChooseFromColorProc(dstBitmap);
    if (NULL == dst || NULL == proc) {return false;}
    const jint* array = env->GetIntArrayElements(srcColors, NULL);
    const SkColor* src = (const SkColor*)array + srcOffset;
    // reset to to actual choice from caller
    dst = dstBitmap.getAddr(x, y);
    // now copy/convert each scanline
    for (int y = 0; y < height; y++) {proc(dst, src, width, x, y);
        src += srcStride;
        dst = (char*)dst + dstBitmap.rowBytes();}
    dstBitmap.notifyPixelsChanged();
    env->ReleaseIntArrayElements(srcColors, const_cast<jint*>(array),
                                 JNI_ABORT);
    return true;
}

ChooseFromColorProc:

// can return NULL
static FromColorProc ChooseFromColorProc(const SkBitmap& bitmap) {switch (bitmap.colorType()) {
        case kN32_SkColorType:
            return bitmap.alphaType() == kPremul_SkAlphaType ? FromColor_D32 : FromColor_D32_Raw;
        case kARGB_4444_SkColorType:
            return bitmap.alphaType() == kPremul_SkAlphaType ? FromColor_D4444 :
                    FromColor_D4444_Raw;
        case kRGB_565_SkColorType:
            return FromColor_D565;
        default:
            break;
    }
    return NULL;
}

cpp 代码

#include <android/bitmap.h>
#include <android/graphics/Bitmap.h>
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
                          AndroidBitmapInfo* info) {if (NULL == env || NULL == jbitmap) {return ANDROID_BITMAP_RESULT_BAD_PARAMETER;}
    if (info) {android::bitmap::imageInfo(env, jbitmap, info);
    }
    return ANDROID_BITMAP_RESULT_SUCCESS;
}
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr) {if (NULL == env || NULL == jbitmap) {return ANDROID_BITMAP_RESULT_BAD_PARAMETER;}
    void* addr = android::bitmap::lockPixels(env, jbitmap);
    if (!addr) {return ANDROID_BITMAP_RESULT_JNI_EXCEPTION;}
    if (addrPtr) {*addrPtr = addr;}
    return ANDROID_BITMAP_RESULT_SUCCESS;
}
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap) {if (NULL == env || NULL == jbitmap) {return ANDROID_BITMAP_RESULT_BAD_PARAMETER;}
    bool unlocked = android::bitmap::unlockPixels(env, jbitmap);
    if (!unlocked) {return ANDROID_BITMAP_RESULT_JNI_EXCEPTION;}
    return ANDROID_BITMAP_RESULT_SUCCESS;
}

JNI 操作 Bitmap

筹备

Android 通过 JNI 调用 Bitmap,通过 CMake 去编 so 动态链接库的时候须要增加 jnigraphics 图像库。

target_link_libraries(
        #本人的须要生成的动静库
        TTNative
        # 操作 bitmap
        jnigraphics
        # 链接 log 库
        ${log-lib})

而后 导入头文件:

#include <android/bitmap.h>

创立 Bitmap

JNI 创立 bitmap 只能调用 Java 或者 kotlin 的办法。

第一种,间接在 Bitmap 中

jclass bitmapCls;
jmethodID createBitmapFunction;
jmethodID getBitmapFunction;

// 创立 bitmap public static Bitmap createBitmap (int width,int height,  Bitmap.Config config)

jobject createBitmap(JNIEnv *env, uint32_t width, uint32_t height) {bitmapCls = env->FindClass("android/graphics/Bitmap");
    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");
    getBitmapFunction = env->GetStaticMethodID(
            bitmapConfigClass, "valueOf",
            "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");

    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
                                                       getBitmapFunction, configName);

    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction,
                                                    width, height, bitmapConfig);
    return newBitmap;
}

检索 Bitmap 对象信息

头文件中定义的函数容许原生代码检索 Bitmap 对象信息,如它的大小、像素格局等,函数签名:

/**
 * Given a java bitmap object, fill out the {@link AndroidBitmapInfo} struct for it.
 * If the call fails, the info parameter will be ignored.
 */
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
                          AndroidBitmapInfo* info);

第一个参数就是 JNI 接口指针,第二个参数是 Bitmap 对象的援用,第三个参数是指向 AndroidBitmapInfo 构造体的指针。

AndroidBitmapInfo 构造体如下:

/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {
    /** The bitmap width in pixels. */
    uint32_t    width;
    /** The bitmap height in pixels. */
    uint32_t    height;
    /** The number of byte per row. */
    uint32_t    stride;
    /** The bitmap pixel format. See {@link AndroidBitmapFormat} */
    int32_t     format;
    /** Bitfield containing information about the bitmap.
     *
     * <p>Two bits are used to encode alpha. Use {@link ANDROID_BITMAP_FLAGS_ALPHA_MASK}
     * and {@link ANDROID_BITMAP_FLAGS_ALPHA_SHIFT} to retrieve them.</p>
     *
     * <p>One bit is used to encode whether the Bitmap uses the HARDWARE Config. Use
     * {@link ANDROID_BITMAP_FLAGS_IS_HARDWARE} to know.</p>
     *
     * <p>These flags were introduced in API level 30.</p>
     */
    uint32_t    flags;
} AndroidBitmapInfo;

其中,width 就是 Bitmap 的宽,height 就是高,format 就是图像的格局,而 stride 就是每一行的字节数。

图像的格局有如下反对:

/** Bitmap pixel format. */
enum AndroidBitmapFormat {
    /** No format. */
    ANDROID_BITMAP_FORMAT_NONE      = 0,
    /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/
    ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
    /** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/
    ANDROID_BITMAP_FORMAT_RGB_565   = 4,
    /** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/
    ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
    /** Alpha: 8 bits. */
    ANDROID_BITMAP_FORMAT_A_8       = 8,
    /** Each component is stored as a half float. **/
    ANDROID_BITMAP_FORMAT_RGBA_F16  = 9,
};

如果 AndroidBitmap_getInfo 执行胜利的话,会返回 0,否则返回一个正数,代表执行的错误码列表如下:

/** AndroidBitmap functions result code. */
enum {
    /** Operation was successful. */
    ANDROID_BITMAP_RESULT_SUCCESS           = 0,
    /** Bad parameter. */
    ANDROID_BITMAP_RESULT_BAD_PARAMETER     = -1,
    /** JNI exception occured. */
    ANDROID_BITMAP_RESULT_JNI_EXCEPTION     = -2,
    /** Allocation failed. */
    ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
};

操作原生像素缓存

拜访

在头文件中AndroidBitmap_lockPixels 函数对图片进行解码并获取解码后像素保留在内存中的地址指针 addrPtr, 锁定了像素缓存以确保像素的内存不会被挪动。

如果 Native 层想要拜访像素数据并操作它,该办法返回了像素缓存的一个原生指针:

/**
 * Given a java bitmap object, attempt to lock the pixel address.
 * Locking will ensure that the memory for the pixels will not move
 * until the unlockPixels call, and ensure that, if the pixels had been
 * previously purged, they will have been restored.
 *
 * If this call succeeds, it must be balanced by a call to
 * AndroidBitmap_unlockPixels, after which time the address of the pixels should
 * no longer be used.
 *
 * If this succeeds, *addrPtr will be set to the pixel address. If the call
 * fails, addrPtr will be ignored.
 */
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);

前两个参数同上,第三个参数是指向像素缓存地址的二维指针。

该函数拿到所有像素的缓存地址,而后对每个像素值进行操作,从而更改 Bitmap 信息。

函数执行胜利的话返回 0,否则返回一个正数,错误码列表同上。

开释

调用完 AndroidBitmap_lockPixels 之后都应该对应调用一次 AndroidBitmap_unlockPixels 用来开释原生像素缓存。

当实现对原生像素缓存的读写之后,就应该开释它,一旦开释后,Bitmap 的 Java 对象就能够在 Java 层应用了,函数签名:

/**
 * Call this to balance a successful call to AndroidBitmap_lockPixels.
 */
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

如果执行胜利返回 0,否则返回 1。

旋转、镜像

咱们不论在 kotlin 还是在 jni 中定义 Bitmap 图像时,都须要定义宽和高,这就绝对于是一个二维的

图像是二维数据,但数据在内存中只能一维存储

二维转一维有不同的对应形式,比拟常见的只有两种形式:

按像素“行排列”从上往下或者从下往上;

Bitmap 在 Android 中的的像素是依照行进行排列的,而且行的排列是从左往右,列的排列是从上往下。

起始点就和屏幕坐标原点一样,位于左上角。

举个例子:

如果咱们失去的原始 bitmap 像素信息开展为二位数组是这个样子:

[[ 1, 2, 3]
  [4, 5, 6]
  [7, 8, 9]
]

那像素数据存储即为:

123 456 789

咱们要将 Bitmap 进行旋转能够创立一个新的 Bitmap 对象,而后将像素值填充到新的 Bitmap 对象中

根据上述的像素排列规定,如果咱们须要顺时针旋转 90 度 的话,咱们须要让像素存储的循序为:

[[ 7, 4, 1]
  [8, 5, 2]
  [9, 6, 3]
]

// 贮存程序
741 852 963

万物基于矩阵。

然而咱们这里只须要依照须要操作的程序去矩阵中取值再写入就能够了。

通过 AndroidBitmap_lockPixels 办法,*addrPtr 指针就指向了 Bitmap 的像素地址,它的长度就是 Bitmap 的宽和高的乘积。

uint32_t mWidth = bitmapInfo.width;
uint32_t mHeight = bitmapInfo.height;
// 获取原生数据
auto pixelArr =((uint32_t *) addrPtr);
// 创立一个新的数组指针填充像素值
auto *newBitmapPixels = new uint32_t[mWidth * mHeight];
LOGE("bitmap width = %d", (uint32_t)mWidth)
LOGE("bitmap height = %d", mHeight)
LOGE("bitmap format = %d", bitmapInfo.format)

咱们这里解决 RGBA_8888 格局,A、R、G、B 重量各占 8 位,8 位是 1 个字节, 一个像素占 4 字节能存储 32 位 ARGB 值

二进制:2^32=16777216 (真彩色)

// 指针偏移
int tmp = 0;
// 依照顺时针 90 度旋转程序扫描
for (int x =0 ; x < mWidth; x++) {for (int y = mHeight-1; y >=0 ; --y) {
              // 从原左下角开始
            uint32_t pixel = pixelArr[mWidth * y+x];
              // 写入
            newBitmapPixels[tmp++] =pixel;
        }
}

首先从原矩阵左下角开始依 y 轴从下向上扫描,再从左向右扫描 x 轴。以此类推

如果是旋转 90 度留神须要在创立 bitmap 时候宽高须要换一下

 jobject newBitmap = createBitmap(env, mHeight, mWidth);

残缺代码:

extern "C"
JNIEXPORT jobject JNICALL
Java_tt_reducto_ndksample_jni_BitmapOps_rotateBitmap(JNIEnv *env, jobject thiz, jobject bitmap,
                                                     jint ops) {if (bitmap == nullptr) {LOGD("rotateBitmap - the  bitmap is null")
        return nullptr;
    }

    // 检索获取 bitmap 信息
    AndroidBitmapInfo bitmapInfo;
    int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGD("AndroidBitmap_getInfo() bitmap failed ! error=%d", ret)
        return nullptr;
    }
    // 取得 Bitmap 的像素缓存指针:遍历从 Bitmap 内存 addrPtr 中读取像素数据
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGD("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
        return nullptr;
    }

    // 执行图片操作的逻辑
    // 获取宽高
    int mWidth = bitmapInfo.width;
    int mHeight = bitmapInfo.height;
    // 获取原生数据
    auto pixelArr = ((uint32_t *) addrPtr);
    // 矩阵 创立一个新的数组指针填充像素值
    auto *newBitmapPixels = new uint32_t[mWidth * mHeight];
    LOGD("bitmap width = %d", mWidth)
    LOGD("bitmap height = %d", mHeight)
    LOGD("bitmap format = %d", bitmapInfo.format)
    int temp = 0;
    switch (ops) {
        case 0:
            // 遍历矩阵,依照顺时针 90 度程序扫描
            for (int x = 0; x < mWidth; x++) {for (int y = mHeight - 1; y >= 0; --y) {newBitmapPixels[temp++] = pixelArr[mWidth * y + x];
                }
            }

            break;
        case 1:
            // 高低翻转
            for (int y = 0; y < mHeight; ++y) {for (int x = 0; x < mWidth; x++) {uint32_t pixel = pixelArr[temp++];
                    newBitmapPixels[mWidth * (mHeight - 1 - y) + x] = pixel;
                }
            }
            break;
        case 2:
            // 镜像
            for (int y = 0; y < mHeight; ++y) {for (int x = mWidth - 1; x >= 0; x--) {uint32_t pixel = pixelArr[temp++];
                    newBitmapPixels[mWidth * y + x] = pixel;
                }
            }
            break;
        default:
            break;
    }


    // 新建 bitmap 留神这里 因为翻转 90 度后,矩阵即 bitmap 的宽高也要扭转
    jobject newBitmap;
    int size = mWidth * mHeight;
    if (ops == 0) {newBitmap = createBitmap(env, mHeight, mWidth);
        void *resultBitmapPixels;
        //
        ret = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels);
        if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGD("AndroidBitmap_lockPixels() newBitmap failed ! error=%d", ret)
            return nullptr;
        }

        // 写入新值
        memcpy((uint32_t *) resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * size);
        // 开释缓存指针
        AndroidBitmap_unlockPixels(env, newBitmap);
        // 开释内存
        delete[] newBitmapPixels;

        return newBitmap;
    } else {memcpy((uint32_t *) addrPtr, newBitmapPixels, sizeof(uint32_t) * size);
        delete[] newBitmapPixels;
        // 开释缓存指针
        AndroidBitmap_unlockPixels(env, bitmap);
        return bitmap;
    }


}

灰度、浮雕

平均值法:即新的色彩值

R=G=B=(R+G+B)/3

或者加权平均值法:

 (r * 0.3 + g * 0.59 + b * 0.11)

对应 jni 函数:

extern "C"
JNIEXPORT void JNICALL
Java_tt_reducto_ndksample_jni_BitmapOps_addBitmapFilter(JNIEnv *env, jobject thiz, jobject bitmap,
                                                        jint ops) {if (bitmap == nullptr) {LOGD("addBitmapFilter - the  bitmap is null")
    }

    // 检索获取 bitmap 信息
    AndroidBitmapInfo bitmapInfo;
//    memset(&bitmapInfo , 0 , sizeof(bitmapInfo));
    int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGD("AndroidBitmap_getInfo() bitmap failed ! error=%d", ret)
    }
    // 取得 Bitmap 的像素缓存指针:遍历从 Bitmap 内存 addrPtr 中读取 BGRA 数据
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {LOGD("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
    }

    // 执行图片操作的逻辑
    // 获取宽高
    uint32_t mWidth = bitmapInfo.width;
    uint32_t mHeight = bitmapInfo.height;
    // 矩阵 创立一个新的数组指针填充像素值
    // auto *newBitmapPixels = new uint32_t[mWidth * mHeight];
    LOGD("bitmap width = %d", mWidth)
    LOGD("bitmap height = %d", mHeight)
    LOGD("bitmap format = %d", bitmapInfo.format)

    // 获取原生数据
    auto pixelArr = ((uint32_t *) addrPtr);

    int a, r, g, b;
    // 不操作 A
    // 遍历从 Bitmap 内存 addrPtr 中读取 BGRA 数据, 而后向 data 内存存储 BGR 数据


    switch (ops) {
        // 灰度图
        case 1: {for (int y = 0; y < mHeight; ++y) {for (int x = 0; x < mWidth; ++x) {
                    // 这里定义成 void, 不便后续操作
                    void *pixel = nullptr;
                    // 24 位
                    if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
                        // 挪动像素指针
                        pixel = pixelArr + y * mWidth + x;
                        // 依照 ABGR 存储序列取值  获取指针对应的值
                        uint32_t v = *((uint32_t *) pixel);
                        a = BGR_8888_A(v);
                        r = BGR_8888_R(v);
                        g = BGR_8888_G(v);
                        b = BGR_8888_B(v);
                        // 平均值法
                        // int sum = (r + g + b) / 3;
                        // 或者加权平均值法
                        int sum = (int) (r * 0.3 + g * 0.59 + b * 0.11);
                        *((uint32_t *) pixel) = MAKE_ABGR(a, sum, sum, sum);
                    }
                }
            }
            break;
        }
            // 浮雕图
        case 2: {
            // 
            // 用以后点的 RGB 值减去相邻点的 RGB 值并加上 128 作为新的 RGB 值
            void *pixel = nullptr;
            void *pixelBefore = nullptr;
            int  r1, g1, b1;
            for (int i = 1; i < mWidth * mHeight; ++i) {
                uint32_t color, colorBefore;

                pixel = pixelArr+i;
                pixelBefore = pixelArr+i - 1;
                color = *((uint32_t *) pixel);
                colorBefore =  *((uint32_t *) pixelBefore);
                a = BGR_8888_A(color);
                r = BGR_8888_R(color);
                g = BGR_8888_G(color);
                b = BGR_8888_B(color);

                r1 = BGR_8888_R(colorBefore);
                g1 = BGR_8888_G(colorBefore);
                b1 = BGR_8888_B(colorBefore);


                r = r - r1 + 128;
                g = g - g1+ 128;
                b = b - b1 + 128;
                // 再一次灰度解决
                int sum = (int) (r * 0.3 + g * 0.59 + b * 0.11);
                *((uint32_t *)pixelBefore) = MAKE_ABGR(a, sum, sum, sum);
            }
            break;
        }

        default:
            break;
    }

    // 开释缓存指针
    AndroidBitmap_unlockPixels(env, bitmap);
}

以上,比较简单的 R、G、B 滤镜。

成果:

退出移动版