Bitmap 圖像灰度變換原理淺析

上篇文章《擁抱 C/C++ : Android JNI 的使用》里提到調(diào)用 native 方法直接修改 bitmap 像素緩沖區(qū),從而實(shí)現(xiàn)將彩色圖片顯示為灰度圖片的方法。這篇文章將介紹該操作的實(shí)現(xiàn)原理。

開始先不講關(guān)于 Bitmap 的相關(guān)細(xì)節(jié),先從計(jì)算機(jī)底層存儲與運(yùn)算原理講起。總所周知,計(jì)算機(jī)只識別 0 和 1,無論是八進(jìn)制、十進(jìn)制、十六進(jìn)制,在底層都會被轉(zhuǎn)換為二進(jìn)制。有幾個單位與概念要提及一下:

計(jì)量單位

bit(位)

計(jì)算機(jī)表示信息的最小單位,也是最小的存儲單位,只有兩種狀態(tài):0 和 1。即二進(jìn)制位。

平時常見的 32 位出流程就是一次最多能處理 32 位的數(shù)據(jù),也就是 4 個 byte(字節(jié))。同理,64 位處理器一次最多能處理 64 位的數(shù)據(jù),即 8 個字節(jié)。

byte(字節(jié))

  • 1 KB = 1024 Byte
  • 1 MB = 1024 KB
  • 1 GB = 1024 MB

通常一個字節(jié)由 8 個二進(jìn)制位(bit)組成。

一個十六進(jìn)制數(shù)需要由 4 個二進(jìn)制組成,即一個字節(jié)可以標(biāo)識 2 個十六進(jìn)制數(shù)。

基本數(shù)據(jù)類型的長度

對 C/C++ 而言,不同的操作平臺分配給基本數(shù)據(jù)類型的長度(字節(jié))是不一樣的,比如 char* 指針變量在 32 位編譯器里是 4 個字節(jié)(32 位的尋址空間是 2^32, 即 32 個 bit,也就是 4 個字節(jié)。64 位編譯器同理),在 64 位編譯器里是 8 個字節(jié)。

而 Java 是跨平臺語言,JVM 里的基礎(chǔ)數(shù)據(jù)類型的字節(jié)長度是一致的。各基本數(shù)據(jù)類型長度如下:

int:4 個字節(jié)
short:2 個字節(jié)。
long:8 個字節(jié)。
byte:1 個字節(jié)。
float:4 個字節(jié)。
double:8 個字節(jié)。
char:2 個字節(jié)。
boolean:boolean 屬于布爾類型,在存儲的時候不使用字節(jié),僅使用 1 位來存儲,范圍僅為 0 和 1,其字面量為 true 和 false。

基本數(shù)據(jù)類型的取值范圍

以最常見的 int 為例,Java 中 int 是 4 個字節(jié),那 int 的取值范圍是多少呢?熟悉 api 的同學(xué)都知道,Integer 類里定義了 MAX_VALUE = 0x7fffffff,那就來推算一下 Java 定義的這個值對不對(大霧

int 占 4 個字節(jié) 32 位,因此就是 8 位數(shù)的十六進(jìn)制。因?yàn)?int 值有正負(fù)之分,所以最高位表示符號,0 代表正數(shù),1 代表負(fù)數(shù)。顯而易見,int 能表示的最大值的二進(jìn)制為 0111 1111 1111 1111 1111 1111 1111 1111 ,最高位 0,后面跟 31 個 1。換算成十六進(jìn)制就是 0x7FFFFFFF,該值與 Jdk 中定義的相同,可見 Jdk 還是很嚴(yán)謹(jǐn)?shù)模?333),Java 大法好!同理,最小值的二進(jìn)制為 1111 1111 1111 1111 1111 1111 1111,換算成十六進(jìn)制就是 0xFFFFFFFF,再對照一下 Jdk 中定義的最小值 MIN_VALUE = 0x80000000。納尼?Jdk 有 bug!(2333)

想都不用想,肯定是我自己有 bug,那為什么推算出的和 Jdk 中定義的不符呢。其實(shí)是二進(jìn)制表示方法不對而已。二進(jìn)制除了上述可直觀計(jì)算得出的逢二進(jìn)一的原碼外,另外還有幾種表示方法。

原碼 反碼 補(bǔ)碼

原碼很直觀易懂,但也有其缺點(diǎn),就比如最高位為符號位為這個槽點(diǎn),就誕生了 0000 ~ 0000,1000 ~ 000,分別代表 +0 和 -0。至于數(shù)學(xué)里有沒有 +0 和 -0,二者參與運(yùn)算是怎么個計(jì)算法,我讀書少我也不清楚。但這說明了一個問題,使用原碼存儲和運(yùn)算會存在二義性。計(jì)算機(jī)在運(yùn)算時使用的并非原碼而是補(bǔ)碼。補(bǔ)碼和反碼的計(jì)算公式如下:

  • 正數(shù)
    原碼、反碼、補(bǔ)碼都相同

  • 負(fù)數(shù)
    反碼:原碼保留符號位,其他位取反
    補(bǔ)碼:反碼+1

  • 補(bǔ)碼轉(zhuǎn)原碼
    如果符號位為1,其余各位取反,然后再整個數(shù)加1。

上面提到的 +0 (0000 ~ 0000),其補(bǔ)碼也為 000 ~ 0000,而 -0(1000 ~ 0000),其反碼為 1111 ~ 1111,補(bǔ)碼為反碼 + 1 ,為 0000 ~ 0000,可見補(bǔ)碼消除了關(guān)于 0 的二義性,使用補(bǔ)碼并不會存在兩個 0。

回到上面推算的 int 值得最小值 1111 ~ 1111,其反碼為 1000 ~ 0000,補(bǔ)碼為 1000 ~ 0001,轉(zhuǎn)換為十六進(jìn)制為 0x80000001。而這與 Jdk 規(guī)定的最小值 MIN_VALUE = 0x80000000 并不相同,說明還遺漏了什么。再回看補(bǔ)碼,除了消除二義性,還有個好處是可以把減法當(dāng)做加法。都知道 01111 ~ 1111 代表正數(shù)的最大值,最高位只代表符號,那么將其由 0 變 1,用 1111 ~ 1111 來代表負(fù)數(shù)的最大值從某種角度上也說得通,補(bǔ)碼(1111 ~ 1111) = 十進(jìn)制(-1),將 補(bǔ)碼(1111 ~ 1111) 往前迭代 1 位(做 + 1 的運(yùn)算),舍棄溢出位,得到 補(bǔ)碼(0000 ~ 0000) = 十進(jìn)制(0),符合 -1 + 1 = 0 的運(yùn)算結(jié)果。將 補(bǔ)碼(1111 ~ 1111) 往后迭代 1 位,得到 補(bǔ)碼(1111 ~ 1110) = 原碼(1000~ 0010) = 十進(jìn)制(-2),符合 -1 - 1 = -2 的運(yùn)算結(jié)果。則同理,將負(fù)數(shù)最大值 補(bǔ)碼(1111 ~ 1111) 一直往后迭代,直到無法再小,則最小值應(yīng)為 補(bǔ)碼(1000 ~ 000) = 原碼(1000 ~ 000) = 十進(jìn)制(-0) = 十六進(jìn)制(0x80000000)。也就是原碼空出來的那個代表 -0 的數(shù),被計(jì)算機(jī)用來表示 int 的最小值。

Bitmap 像素

提及 Bitmap ,先介紹一下 Android 中Bitmap 類中定義的枚舉類 Config 里的幾個值,也是比較多見的 Android 中的 Biamap 顯示參數(shù)。

Bitmap 參數(shù)

  • ARGB_4444
    四個通道 A(透明度)、R(紅色)、G(綠色)、B(藍(lán)色)各占 4 位,總共 16 位,即每個像素占用 2 個字節(jié)。

  • ARGB_8888
    四個通道各占 8 位,總共 32 位,每個像素占用 4 個字節(jié)。因?yàn)?RGB 通道精度更高,所以顏色顯示更豐富,同時占用內(nèi)存也更大。

  • RGB_565
    沒有透明度信息,RGB 通道各占用 5 位、6 位、5 位,總共 16 位,每個像素占用 2 個字節(jié)。

知道了每個像素占用的字節(jié)長度,就可以計(jì)算一張圖片顯示時所占用的內(nèi)存大小,以 ARGB_8888 為例,一張像素為 16 * 16 的圖片占用的內(nèi)存為:16 * 16 * 4 = 1024 byte,即 1 KB。

輕松愉快又簡單!可夢想很美好,顯示很骨感。在 Android 中,在不壓縮計(jì)算的情況下(例如顯示 assets 目錄下的圖片),內(nèi)存大小就是上面計(jì)算所得,但因?yàn)?Android 中的圖片一般存放在不同的資源目錄:

資源目錄對應(yīng)的 dpi
mdpi -> 120 dpi
mdpi -> 160 dpi
hdpi -> 240 dpi
xdpi -> 320 dpi
xxdpi -> 480 dpi
xxxdpi -> 640 dpi

Android 中顯示不同的資源目錄圖片時,會對圖片做縮放處理,縮放比例為 設(shè)備dpi / 資源目錄對應(yīng) dpi,以 小米8SE 為例,設(shè)備屏幕密度為 440 dpi,該設(shè)備顯示存放在 xxdpi(480dpi)目錄中的像素為 300 * 300 的圖片時,實(shí)際顯示圖片的寬和高將換算為 440 / 480 * 300 (結(jié)果四舍五入),計(jì)算得到圖片在手機(jī)顯示的寬高為 275,再根據(jù)計(jì)算所得實(shí)際的圖片寬高計(jì)算所占內(nèi)存:

275 * 275 * 4 = 302500(byte)

可以調(diào)用 Bitmap 類自帶的方法 getByteCount() 方法驗(yàn)證一下。

順帶提一下,Android 中 Bitmap 的占用內(nèi)存大小與顯示圖片的容器(例如 Android 上的 ImageView)尺寸無關(guān)。

Bitmap 像素的定義

介紹完 Bitmap 內(nèi)存占用大小后,回到 Bitmap 本身來。Bitmap 將圖像定義為由像素組成,以 ARGB_8888 為例,上面提到過,A/R/G/B 各占 8 位,各由兩個十六進(jìn)制數(shù)表示,依次排列,比如常見的色值 #FF234567,即各通道值為:透明度 alpha 0xFF,紅色 red 0x23,綠色 green 0x45,藍(lán)色 blue 0x67。

因此一張分辨率 100 * 100 的彩色圖片,無非就是 100 * 100 個像素,每個像素顯示對應(yīng)的顏色,所有像素組合在一起便成了彩色的圖片。所以只要拿到了 Bitmap,想要如何修改圖像的顯示,只要對各個像素顯示的顏色做相應(yīng)的處理就好了。

彩色轉(zhuǎn)換為灰色的計(jì)算方式暫且不提。要改變圖像的顯示,首要任務(wù)是獲取到各像素點(diǎn)的顏色。

Android 中可以調(diào)用 Bitmap 類自帶的方法獲取到具體某個點(diǎn)的像素顏色:

int color = bitmap.getPixel(200, 300);

那么問題來了,如何才能從一個 int 值中獲取各個通道(RGB)的顏色呢?

從像素中提取各通道色值

老司機(jī)們可能秒懂,這個簡單,Color 類自帶的方法就可以做到:

int redColor = Color.red(color);

再看一下該方法的實(shí)現(xiàn):

@IntRange(from = 0, to = 255)
public static int red(int color) {
    return (color >> 16) & 0xFF;
}

其實(shí)計(jì)算方法也很簡單,用到了位運(yùn)算,那就順帶回顧一下位運(yùn)算。

位運(yùn)算符

從最低位到最高位一一對齊,每一位都做運(yùn)算(也是對補(bǔ)碼做運(yùn)算),各運(yùn)算符含義如下:

  • &
    都是 1,則結(jié)果為1。否則為 0。
  • |
    都是 0,則結(jié)果為0。否則為 1。
  • ~ 取反
    對數(shù)的每一位取反。
  • ^ 異或
    數(shù)值相同,則結(jié)果為 0,不為 1。
  • >>右移
    從 0 位起整體向右移動,空出的高位正數(shù)補(bǔ) 0,負(fù)數(shù)補(bǔ)1。
  • >>> 無符號右移
    從 0 位起(連符號位)整體向右移動,空出的高位一律補(bǔ) 0。
    對于正數(shù)而言,>>和>>>沒區(qū)別。
  • << 左移
    整體向左移動,右邊的空位一律補(bǔ) 0。

現(xiàn)在再來回看上面提到的取色方法:

// Color
public static int red(int color) {
    return (color >> 16) & 0xFF;
}

還以 #FF234567 為例,轉(zhuǎn)換為二進(jìn)制為
1111 1111 | 0010 0011 | 0100 0101 | 0110 0111 (這里我用了 | 符號方便劃分),其中 第二陣列 0010 0011,即右起第 17 ~25 位代表紅色色值。將二進(jìn)制右移 16位,等同于舍棄了紅色右邊 的 16 位用于存儲綠色、藍(lán)色的色值,得到 0000 0000 | 0000 0000 | 1111 1111 | 0010 0011,再與 0xFF 即二進(jìn)制 1111 1111 做與運(yùn)算,運(yùn)算時高位為空則補(bǔ)0,與 0 做 &與運(yùn)算結(jié)果必為0,等同于與舍棄了右邊代表透明度的高八位,最終得到紅色的色值 0010 0011

取紅色色值也還有另一種解法:

(color & 0x00FF0000) >> 16

先和 0x00FF0000 做與運(yùn)算,舍棄除紅色外所有色值,再右移 16 位得到該值。這種解法與上述的只不過是運(yùn)算順序不同,殊途同歸。

至此,獲取到了色值,想要怎么改變圖片的顯示就是算法上的事了,各憑本事各顯神通。

今天的分享就到這,如有紕漏歡迎指正,下篇博客見。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容