Android Bitmap像素排列與JNI操作

圖像的數(shù)值表示

RGB

RGB顏色模型即紅綠藍(lán)顏色模型劳景。由模仿生物視網(wǎng)膜三種視錐細(xì)胞產(chǎn)生配紫,之后通過三原色疊加來進(jìn)行彩色圖像顯示视搏。通過在黑色上不斷疊加三原色來顯示不同的顏色审孽。在RGB顏色空間中,分別將RGB作為笛卡爾坐標(biāo)系中XYZ坐標(biāo)系產(chǎn)生浑娜。每一個顏色取值范圍為[0,256)

RGB是從顏色發(fā)光的原理來設(shè)計定的佑力,通俗點(diǎn)說它的顏色混合方式就好像有紅、綠筋遭、藍(lán)三盞燈打颤,當(dāng)它們的光相互疊合的時候暴拄,色彩相混,而亮度卻等于兩者亮度之總和编饺,越混合亮度越高乖篷,即加法混合。

紅透且、綠撕蔼、藍(lán)三個顏色通道每種色各分為256階亮度,在0時“燈”最弱——是關(guān)掉的秽誊,而在255時“燈”最亮鲸沮。當(dāng)三色灰度數(shù)值相同時,產(chǎn)生不同灰度值的灰色調(diào)锅论,即三色灰度都為0時讼溺,是最暗的黑色調(diào);三色灰度都為255時最易,是最亮的白色調(diào)怒坯。

對一種顏色進(jìn)行編碼的方法統(tǒng)稱為顏色空間或色域。

用最簡單的話說藻懒,世界上任何一種顏色的“顏色空間”都可定義成一個固定的數(shù)字或變量剔猿。RGB(紅、綠束析、藍(lán))只是眾多顏色空間的一種艳馒。采用這種編碼方法憎亚,每種顏色都可用三個變量來表示-紅色綠色以及藍(lán)色的強(qiáng)度员寇。記錄及顯示彩色圖像時,RGB是最常見的一種方案第美。

所以每一個圖像都可以由RGB組成蝶锋,那么一個像素點(diǎn)的RGB該如何表示呢?音頻里面的每一個采樣(sample)均使用16個比特來表示什往,那么像素里面的子像素又該如何表示呢扳缕?常用的表示方式有以下幾種。

  • 浮點(diǎn)表示:取值范圍為0.0~1.0别威,比如躯舔,在OpenGL ES中對每一個子像素點(diǎn)的表示使用的就是這種表達(dá)方式。

  • 整數(shù)表示:取值范圍為0~255或者00~FF,8個比特表示一個子像素省古,32個比特表示一個像素

Android平臺上RGB_565的表示方法為16比特模式表示一個像素粥庄,R用5個比特來表示,G用6個比特來表示豺妓,B用5個比特來表示惜互。

對于一幅圖像布讹,一般使用整數(shù)表示方法來進(jìn)行描述,比如計算一張1280×720的RGBA_8888圖像的大小训堆,可采用如下方式:

1280 * 720 * 4 = 3.516MB

這也是位圖(bitmap)在內(nèi)存中所占用的大小描验,所以每一張圖像的裸數(shù)據(jù)都是很大的。對于圖像的裸數(shù)據(jù)來講坑鱼,直接在網(wǎng)絡(luò)上進(jìn)行傳輸也是不太可能的膘流,所以就有了圖像的壓縮格式。

安卓圖像引擎解碼的規(guī)則鲁沥,在JNI中解析出來的是ABGR順序睡扬,獲取RGB數(shù)據(jù)的時候要注意。

Android中的顏色值通常遵循RGB/ARGB標(biāo)準(zhǔn)黍析,使用時通常以“ # ”字符開頭的8位16進(jìn)制表示卖怜。前綴0x表示十六進(jìn)制(基數(shù)為16),其中ARGB 依次代表透明度(Alpha)阐枣、紅色(Red)马靠、綠色(Green)、藍(lán)色(Blue)蔼两,取值范圍為0 ~ 255(即16進(jìn)制的0x00 ~ 0xff)甩鳄。
A0x000xff表示從透明到不透明,RGB0x000xff表示顏色從淺到深额划。當(dāng)RGB全取最小值(0或0x000000)時顏色為黑色妙啃,全取最大值(255或0xffffff)時顏色為白色。

  • 紅色:(255,0,0)或0x00FF0000
  • 綠色:(0,255,0)或0x0000FF00
  • 藍(lán)色:(255,255,255)或0x00FFFFFF

大小端字節(jié)序

這是在Android中使用RGB數(shù)據(jù)的時候面臨的問題俊戳。

以內(nèi)存中0x0A0B0C0D(0x前綴代表十六進(jìn)制)的存放方式為例揖赴,分別有以下幾種方式:

小端序

  • (little-endian)又稱小尾序

數(shù)據(jù)以8bit為單位:

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

最低位字節(jié)是0x0D 存儲在最低的內(nèi)存地址處抑胎。后面字節(jié)依次存在后面的地址處燥滑。

大端序

  • (big-endian)又稱大尾序

數(shù)據(jù)以8bit為單位:

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

最高位字節(jié)是0x0A`存儲在最低的內(nèi)存地址處阿逃。下一個字節(jié)0x0B存在后面的地址處铭拧。正類似于十六進(jìn)制字節(jié)從左到右的閱讀順序。

混合序

  • (middle-endian)具有更復(fù)雜的順序恃锉。

PDP-11為例搀菩,0x0A0B0C0D被存儲為:

32bit在PDP-11的存儲方式

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

可以看作高16bit和低16bit以大端序存儲,但16bit內(nèi)部以小端存儲破托。

Bitmap像素排列

Android中Java/Kotlin默認(rèn)使用大端字節(jié)序肪跋,所見即所得,NDK 中C/C++默認(rèn)使用小端字節(jié)序。

這個很容易驗(yàn)證:

import java.nio.ByteOrder
......
// 調(diào)用
ByteOrder.nativeOrder()
....
// 得到
LITTLE_ENDIAN

我們在Android平臺下創(chuàng)建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);

這里的字節(jié)順應(yīng)該為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);
}

最終調(diào)用native的方法nativeGetPixels,我們先不管Native是如何處理的澎嚣。

這里將Bitmap中的像素數(shù)據(jù)將copy到pixels數(shù)組中疏尿,pixels數(shù)組是按照ColorSpace.Named#SRGB規(guī)則排列的。

即每一個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沒有改變順序

我們簡單驗(yàn)證下:

 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=$")
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 取絕對值二進(jìn)制反碼+1后的16進(jìn)制即為FF晤郑。

JNI取值順序

之前說 Bitmap.config.ARGB_8888對應(yīng)的Bitmap字節(jié)序?yàn)?strong>ABRG.

那么JNI中ANDROID_BITMAP_FORMAT_RGBA_8888也是如此敌呈。

簡單驗(yàn)證下:

同樣以上面的一個像素0X112233為例:

這里注意下我們使用paint.color = Color.rgb(0x11, 0x22, 0x33)alpha的值是默認(rèn)的。

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

對應(yīng)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 內(nèi)存 addrPtr 中讀取 BGRA 數(shù)據(jù)
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {
        LOGE("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
    }

    // 執(zhí)行圖片操作的邏輯
    // 獲取寬高
    uint32_t mWidth = bitmapInfo.width;
    uint32_t mHeight = bitmapInfo.height;
    // 獲取原生數(shù)據(jù)
    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存儲序列取值  獲取指針對應(yīng)的值
            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轉(zhuǎn)成二進(jìn)制:

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的處理經(jīng)過:

Java層函數(shù)——Native層函數(shù)——Skia庫函數(shù)——對應(yīng)第三方庫函數(shù)(libjpeg)

所有Bitmap.createBitmap()對應(yīng)的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) {
    // 轉(zhuǎn)換色域
    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強(qiáng)轉(zhuǎn)成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的創(chuàng)建內(nèi)存分配都是在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轉(zhuǎn)成skia域的顏色類型:

 SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);

看下GraphicsJNI.h中對應(yīng)的方法定義:

/*
 *  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中看下實(shí)現(xiàn):

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

因?yàn)槲覀冊趈ava層傳入的Bitmap.Config.ARGB_8888值為ARGB_8888(5)

與之對應(yīng)的就是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根據(jù)字節(jié)序決定kN32_SkColorTypef的值磕洪,用到的宏SK_PMCOLOR_BYTE_ORDERSkPostConfig.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

所以小端字節(jié)序?qū)?yīng)就是:

#  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

所以小端字節(jié)序處理:

#    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
// 表達(dá)式返回
true 

綜上:

這意味著Bitmap.Config.ARGB_8888 會被轉(zhuǎn)成Skia域中的顏色類型 kRGBA_8888_SkColorType并以此格式內(nèi)部存儲。在將RGBA寫入到小端字節(jié)序的內(nèi)存中签赃,就變成了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

準(zhǔn)備

Android 通過 JNI 調(diào)用 Bitmap,通過 CMake 去編 so 動態(tài)鏈接庫的時候需要添加 jnigraphics 圖像庫锦聊。

target_link_libraries(
        #自己的需要生成的動態(tài)庫
        TTNative
        # 操作bitmap
        jnigraphics
        # 鏈接log 庫
        ${log-lib})

然后 導(dǎo)入頭文件:

#include <android/bitmap.h>

創(chuàng)建Bitmap

JNI創(chuàng)建bitmap只能調(diào)用Java或者kotlin的方法歹嘹。

第一種,直接在Bitmap中

jclass bitmapCls;
jmethodID createBitmapFunction;
jmethodID getBitmapFunction;

// 創(chuàng)建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 對象信息

頭文件中定義的函數(shù)允許原生代碼檢索 Bitmap 對象信息孔庭,如它的大小尺上、像素格式等,函數(shù)簽名:

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

第一個參數(shù)就是 JNI 接口指針圆到,第二個參數(shù)是 Bitmap 對象的引用怎抛,第三個參數(shù)是指向 AndroidBitmapInfo 結(jié)構(gòu)體的指針。

AndroidBitmapInfo 結(jié)構(gòu)體如下:

/** 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 就是每一行的字節(jié)數(shù)。

圖像的格式有如下支持:

/** 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 執(zhí)行成功的話河绽,會返回 0 己单,否則返回一個負(fù)數(shù),代表執(zhí)行的錯誤碼列表如下:

/** 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 函數(shù)對圖片進(jìn)行解碼并獲取解碼后像素保存在內(nèi)存中的地址指針addrPtr,鎖定了像素緩存以確保像素的內(nèi)存不會被移動耙饰。

如果 Native 層想要訪問像素數(shù)據(jù)并操作它纹笼,該方法返回了像素緩存的一個原生指針:

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

前兩個參數(shù)同上,第三個參數(shù)是指向像素緩存地址的二維指針苟跪。

該函數(shù)拿到所有像素的緩存地址廷痘,然后對每個像素值進(jìn)行操作蔓涧,從而更改 Bitmap 信息。

函數(shù)執(zhí)行成功的話返回 0 笋额,否則返回一個負(fù)數(shù)元暴,錯誤碼列表同上。

釋放

調(diào)用完 AndroidBitmap_lockPixels 之后都應(yīng)該對應(yīng)調(diào)用一次 AndroidBitmap_unlockPixels 用來釋放原生像素緩存兄猩。

當(dāng)完成對原生像素緩存的讀寫之后茉盏,就應(yīng)該釋放它,一旦釋放后枢冤,Bitmap 的Java 對象就可以在 Java 層使用了鸠姨,函數(shù)簽名:

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

如果執(zhí)行成功返回 0,否則返回 1淹真。

旋轉(zhuǎn)讶迁、鏡像

我們不管在kotlin還是在jni中定義 Bitmap 圖像時,都需要定義寬和高核蘸,這就相對于是一個二維的

圖像是二維數(shù)據(jù)添瓷,但數(shù)據(jù)在內(nèi)存中只能一維存儲

二維轉(zhuǎn)一維有不同的對應(yīng)方式值纱,比較常見的只有兩種方式:

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

Bitmap 在Android中的的像素是按照行進(jìn)行排列的鳞贷,而且行的排列是從左往右,列的排列是從上往下虐唠。

起始點(diǎn)就和屏幕坐標(biāo)原點(diǎn)一樣搀愧,位于左上角。

舉個例子:

如果我們得到的原始bitmap像素信息展開為二位數(shù)組是這個樣子:

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

那像素數(shù)據(jù)存儲即為:

123 456 789

我們要將 Bitmap 進(jìn)行旋轉(zhuǎn)可以創(chuàng)建一個新的 Bitmap 對象疆偿,然后將像素值填充到新的 Bitmap 對象中

根據(jù)上述的像素排列規(guī)則咱筛,如果我們需要順時針旋轉(zhuǎn)90度 的話,我們需要讓像素存儲的循序?yàn)?

[
  [ 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;
// 獲取原生數(shù)據(jù)
auto pixelArr =((uint32_t *) addrPtr);
// 創(chuàng)建一個新的數(shù)組指針填充像素值
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個字節(jié),一個像素占4字節(jié)能存儲32位ARGB值

二進(jìn)制:2^32=16777216 (真彩色)

// 指針偏移
int tmp = 0;
// 按照順時針90度旋轉(zhuǎn)順序掃描
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軸龄砰。以此類推

如果是旋轉(zhuǎn)90度注意需要在創(chuàng)建bitmap時候?qū)捀咝枰獡Q一下?

 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 內(nèi)存 addrPtr 中讀取像素數(shù)據(jù)
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {
        LOGD("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
        return nullptr;
    }

    // 執(zhí)行圖片操作的邏輯
    // 獲取寬高
    int mWidth = bitmapInfo.width;
    int mHeight = bitmapInfo.height;
    // 獲取原生數(shù)據(jù)
    auto pixelArr = ((uint32_t *) addrPtr);
    // 矩陣 創(chuàng)建一個新的數(shù)組指針填充像素值
    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:
            // 上下翻轉(zhuǎn)
            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 注意這里 因?yàn)榉D(zhuǎn)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);
        // 釋放內(nèi)存
        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

或者加權(quán)平均值法:

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

對應(yīng)jni函數(shù):

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 內(nèi)存 addrPtr 中讀取 BGRA 數(shù)據(jù)
    void *addrPtr;
    ret = AndroidBitmap_lockPixels(env, bitmap, &addrPtr);
    if (ANDROID_BITMAP_RESULT_SUCCESS != ret) {
        LOGD("AndroidBitmap_lockPixels() bitmap failed ! error=%d", ret)
    }

    // 執(zhí)行圖片操作的邏輯
    // 獲取寬高
    uint32_t mWidth = bitmapInfo.width;
    uint32_t mHeight = bitmapInfo.height;
    // 矩陣 創(chuàng)建一個新的數(shù)組指針填充像素值
    // auto *newBitmapPixels = new uint32_t[mWidth * mHeight];
    LOGD("bitmap width = %d", mWidth)
    LOGD("bitmap height = %d", mHeight)
    LOGD("bitmap format = %d", bitmapInfo.format)

    // 獲取原生數(shù)據(jù)
    auto pixelArr = ((uint32_t *) addrPtr);

    int a, r, g, b;
    // 不操作A
    // 遍歷從 Bitmap 內(nèi)存 addrPtr 中讀取 BGRA 數(shù)據(jù), 然后向 data 內(nèi)存存儲 BGR 數(shù)據(jù)


    switch (ops) {
        // 灰度圖
        case 1: {
            for (int y = 0; y < mHeight; ++y) {
                for (int x = 0; x < mWidth; ++x) {
                    // 這里定義成void,方便后續(xù)操作
                    void *pixel = nullptr;
                    // 24位
                    if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
                        // 移動像素指針
                        pixel = pixelArr + y * mWidth + x;
                        //按照ABGR存儲序列取值  獲取指針對應(yīng)的值
                        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;
                        //或者加權(quán)平均值法
                        int sum = (int) (r * 0.3 + g * 0.59 + b * 0.11);
                        *((uint32_t *) pixel) = MAKE_ABGR(a, sum, sum, sum);
                    }
                }
            }
            break;
        }
            // 浮雕圖
        case 2: {
            // 
            // 用當(dāng)前點(diǎn)的RGB值減去相鄰點(diǎn)的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濾鏡娘汞。

效果:

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颇蜡,隨后出現(xiàn)的幾起案子价说,更是在濱河造成了極大的恐慌,老刑警劉巖风秤,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳖目,死亡現(xiàn)場離奇詭異,居然都是意外死亡缤弦,警方通過查閱死者的電腦和手機(jī)领迈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碍沐,“玉大人狸捅,你說我怎么就攤上這事±厶幔” “怎么了尘喝?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斋陪。 經(jīng)常有香客問我朽褪,道長,這世上最難降的妖魔是什么无虚? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任缔赠,我火速辦了婚禮,結(jié)果婚禮上友题,老公的妹妹穿的比我還像新娘嗤堰。我一直安慰自己,他們只是感情好度宦,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布踢匣。 她就那樣靜靜地躺著,像睡著了一般斗埂。 火紅的嫁衣襯著肌膚如雪符糊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天呛凶,我揣著相機(jī)與錄音,去河邊找鬼行贪。 笑死漾稀,一個胖子當(dāng)著我的面吹牛模闲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崭捍,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼尸折,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了殷蛇?” 一聲冷哼從身側(cè)響起实夹,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎粒梦,沒想到半個月后亮航,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匀们,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年缴淋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泄朴。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡重抖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祖灰,到底是詐尸還是另有隱情钟沛,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布局扶,位于F島的核電站恨统,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏详民。R本人自食惡果不足惜延欠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沈跨。 院中可真熱鬧由捎,春花似錦、人聲如沸饿凛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涧窒。三九已至心肪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纠吴,已是汗流浹背硬鞍。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人固该。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓锅减,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伐坏。 傳聞我的和親對象是個殘疾皇子怔匣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361