图像的数值表示
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)。
A 从0x00到0xff表示从透明到不透明,RGB 从0x00到0xff表示颜色从浅到深。当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() 取值顺序
看下具体方法:
/**
* 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.
* 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.
* @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 MAKE_ABGR(a, b, g, r) (((a&0xff)<<24) | ((b & 0xff) << 16) | ((g & 0xff) << 8 ) | (r & 0xff))
#define BGR_8888_A(p) ((p & (0xff<<24)) >> 24 )
#define BGR_8888_B(p) ((p & (0xff << 16)) >> 16 )
#define BGR_8888_G(p) ((p & (0xff << 8)) >> 8 )
#define BGR_8888_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 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 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(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::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp(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(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;
}
#include
#include
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
创建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.
*
* Two bits are used to encode alpha. Use {@link ANDROID_BITMAP_FLAGS_ALPHA_MASK}
* and {@link ANDROID_BITMAP_FLAGS_ALPHA_SHIFT} to retrieve them.
*
* One bit is used to encode whether the Bitmap uses the HARDWARE Config. Use
* {@link ANDROID_BITMAP_FLAGS_IS_HARDWARE} to know.
*
* These flags were introduced in API level 30.
*/
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滤镜。
效果: