android位圖顏色模式的問題

【譯】Android位圖顏色模式的問題

  原文http://android.nakatome.net/

     最近開始了android上的編程之旅,在瞭解2D圖形編程時,令人蛋疼的發覺android上僅支持ARGB8888、ARGB4444、RGB565以及Alpha 8這麼幾種顏色模式,而不支持RGB888這種格式。原本以爲即使不支持RGB888我用ARGB8888總行吧,但後來瞭解到,即使我在內存中用ARGB888顏色模型表示圖像,在該圖像拷貝到屏幕幀緩衝區的過程中,它也會變成RGB565顏色模式。我們知道,RGB565最多隻能表示2^16=65536種圖像,這對於RGB888所能表示的2^24=16777216種顏色來說顯然在表現力上要略遜一籌。這集中表現在顯示某些帶有漸變效果的圖片時,出現了一條條的顏色帶,而不是原始的平滑的漸變效果。後來得知android使用了Dither(抖動)這種技術,以欺騙人類眼球的方式加以補償。

  當然,以這個問題爲出發點,後來又引發了諸多問題,而這篇文章解決了我不少問題,特在這裏翻譯出來供大家分享之。

======

  在學習AvoidXfermode類的時候,我遇到了圖片顏色不能正確顯示的問題。我畫了一個平滑漸變的色譜圖片,然後用AvoidXfermode的Target功能將替換該圖片裏一種顏色替換爲另外的顏色。但問題是,我想要替換的顏色並沒有按照我所預想的一樣被AvoidXfermode替換掉。原來,我的PNG圖片被自動從24位的RGB888顏色模式轉換爲了16位的RGB565顏色模式,這使得圖片中顏色的原始數值被改變了。

  現在市面上幾乎所有的設備都是16位色的屏幕,這意味着不管你在代碼裏使用什麼顏色格式的圖片,在某個時候它們都會被轉化爲16位圖片,這樣它們才能被顯示在屏幕上。這種轉換會佔用處理器資源,會以時間和電池壽命的形式給用戶造成損失。所以,我們並不期望這種轉換在繪圖時會發生,相反的,我們希望它還要在儘可能早的階段裏被完成。Android是一個智能的系統,在有些情況下Android會自動幫你完成這些轉換工作,所以你並不需要擔心這些問題。在大部分情況下,這個特性是相當棒的,它大大減少了開發者的工作量,也減少你的資源大小同時節省了處理器時間和電池壽命。但是,如果你希望在程序裏將圖片以一種指定的顏色格式處理,這種自動轉化機制會讓你頭大的。幸運的是,我們有辦法阻止自動轉換的發生,讓你的圖片以你期望的顏色格式儲存。

  不過,既然這種自動轉換既節省內存和處理器時間又保護電池壽命,那麼爲什麼有人會不希望這種轉換髮生呢?Android程序中大部分的圖片都只需要簡單的載入並顯示就行了。但是在一些教程以及我之前提到的例子中,我們需要在圖片顯示之前對它們做一些操作。爲了得到最佳的效果,我們希望載入的圖片在處理時盡能可能的保持最好的質量,因爲在處理過程中過早的降低質量將對最終效果造成不良影響。

  好了,這就是說在圖片處理好之前,我們不希望被Android橫插一腳。但是,Android系統將在什麼時候替我們執行這些轉換呢?答案是轉換將在3個地方發生:

  • 編譯時圖片資源被編譯到軟件包裏去時
  • 當圖片被從資源中載入爲Bitmap時
  • 當圖片被繪製時

  我們將看看這三類情況,並且瞭解如何避免這些轉換。

程序編譯時

  當你在項目“res/drawable”文件夾下放置圖片的時候,意味着你告訴Android:如果需要的話,在構建程序的時候將圖片轉換爲16位圖片。該轉換髮生的必要條件有哪些?無論你圖片的原始格式是什麼,如果你的圖片沒有alpha通道,在你的軟件構建的時候Android會將它轉換爲本地16位色圖。你有兩種方法阻止轉換髮生:

  • 給圖片添加alpha通道
  • 將圖片放置到“res/raw”目錄下而不是“res/drawable”

  通過給圖片添加alpha通道,Android將不會嘗試將圖片轉換爲16位色圖,這是因爲RGB565顏色模式不帶alpha通道。將一張帶有alpha通道的圖片轉換爲RGB565顏色格式會使半透明信息丟失,所以任何帶有alpha通道的圖片將被儲存爲32位的ARGB8888圖片資源。通過將圖片放置到“res/raw”目錄下,我們告訴Android這個資源包含原始數據,它不應該在構建時更改。所以,對於我們放置到該目錄下的任何圖片都不會發生自動轉換。但不幸的是,如果我們放在raw目錄下的圖片不帶有alpha通道的話,這個方法還是有問題。這個問題在我們載入圖片時顯露了出來:

圖片載入時

  當使用BitmapFactory從你程序的資源中載入一張圖片時,同樣的自動轉換機制會發生。如果你要載入的圖片沒有alpha通道,Android會將其轉換爲16位RGB565圖片。不幸的是,即使我們將圖片放置在“res/raw”目錄下,這種轉換依然會發生。根據在這個帖子裏一個叫Romain Guy的Android開發者所說,有一個解決之道:

  你需要做的就是將圖片直接載入爲ARGB888模式。當你調用BitmapFactory.decode*()的某一個重載方法時,你能夠指定一系列的BitmapFactory.Option對象。你需要將Option對象中的inDither設置爲false。這樣將會使BitmapFactory不去嘗試將24位色圖轉換爲16位色圖。”

  當時,我發覺這個方法不起作用,至少在Android 1.6下都不起作用。傳入一系列參數使得inDither爲false的確使得圖片不被抖動(Dither)處理,但是這種方法並不能阻止顏色模式的轉換。圖片依然會被轉換成16位的RGB565,這將會使得轉換後的漸變圖片出現顏色條帶的現象。

  既然這個推薦的解決方案並不能滿足我們的要求,那麼在這個過程中唯一能保持你圖片原貌的方法就是讓你的圖片帶上alpha通道。當BitmapFactory發覺圖片資源帶有alpha通道,它便只能將圖片解碼爲32位的ARGB888位圖。

繪製時

  假設我們有兩張圖片。圖片A是ARGB888位圖,圖片B是RGB565位圖。如果我們緊挨那個圖片A繪製到B上,那麼圖片A將需要被轉換爲B的顏色格式。辛運的是,這種轉換是由Canvas的drawBitmap方法替我們包辦了。這對我們來說是個好消息,因爲這正是我們想要的。在我們要釋放位圖資源的時候,我們已經完成調整和操作圖片,並且圖片將被轉換和顯示。然而,由於我們從32位轉換爲16位顏色深度,這將會造成圖像的失真。爲了減小失真對圖片的影響,我們能夠控制將圖片從32位轉換爲16位的時機。當將一個高色深的圖片繪製到低色深的圖片上時,默認是不會進行抖動處理的。對於包含漸變的圖片而言,這會使得圖片出現衆所周知的“色帶”問題,這是非常難看的。爲了克服這個問題,我們需要告訴Android我們想要對結果進行抖動處理。抖動是這麼一種處理,它將原始顏色做出一些改變,以騙過我們的眼睛,讓我們在低色深圖片中以爲自己看到了一個平滑的漸變。

  需要記住的是,當我們處理圖片的時候,圖片必須總是32位ARGB8888模式。只有當我們完成處理圖片後,他們才應該被轉換成16位色的圖片。由於任何不帶alpha通道的圖片,再被BitmapFactory載入的時候將被轉換爲RGB565,不管它們是在"res/drawable"還是在"res/raw"目錄下。而確保他們被解碼爲32色ARGB8888的唯一手段就是保證你的圖片帶有alpha通道。

例子

  爲了證明我所說,然我們做一個簡單的測試。首先讓我們寫一個載入並以默認option顯示兩張圖片的activity。兩張圖片都是平滑漸變的色譜,但是他們將被保存爲不同的格式。第一張圖將被保存爲24位色RGB888的PNG圖,第二張圖片被保存爲32位帶有alpha通道的ARGB8888的PNG圖片。兩張圖片有着完全一致的顏色數據,惟一的區別是一張帶有alpha通道而另一張沒有。保存兩張圖片,並把他們放置到你項目的"res/raw"目錄下。然後,在你的activity中使用下列代碼:

 1 @Override
2 public void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4
5 // Load both of our images from our application's resources.
6 Resources r = getResources();
7 Bitmap resource= BitmapFactory.decodeResource(r, R.raw.spectrum_gray_nodither_;
8 Bitmap resource= BitmapFactory.decodeResource(r, R.raw.spectrum_gray_nodither_;
9
10 // Print some log statements to show what pixel format these images were decoded with.
11 Log.d("FormatTest","Resource " + resourcegetConfig()); // Resource RGB_
12 Log.d("FormatTest","Resource " + resourcegetConfig()); // Resource ARGB_8
13
14 // Create two image views to show these bitmaps in.
15 ImageView image= new ImageView(this);
16 ImageView image= new ImageView(this);
17 imagesetImageBitmap(resource;
18 imagesetImageBitmap(resource;
19
20
21
22 // Create a simple layout to show these two image views side-by-side.
23 LayoutParams wrap = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
24 LayoutParams fill = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
25 RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(wrap);
26 paramsaddRule(RelativeLayout.CENTER_VERTICAL);
27 paramsaddRule(RelativeLayout.ALIGN_PARENT_LEFT);
28 RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(wrap);
29 paramsaddRule(RelativeLayout.CENTER_VERTICAL);
30 paramsaddRule(RelativeLayout.ALIGN_PARENT_RIGHT);
31 RelativeLayout layout = new RelativeLayout(this);
32 layout.addView(image params;
33 layout.addView(image params;
34 layout.setBackgroundColor(Color.BLACK);
35
36 // Show this layout in our activity.
37 setContentView(layout, fill);
38
39 }

  編譯此項目並將它部署到你的設備上查看結果。我們看到24位色圖片在左邊,爲32位色圖片在右邊。但是等一下!32位的圖片看上去有一些色帶出現,然而24位圖片看上去卻很平滑。到底是怎麼了?讓我們仔細看看圖片,我們能發現:

  仔細檢查後,我們發覺24位圖被抖動處理了,而32位色圖沒有。考慮到對於任何顯示到Android設備屏幕的圖片,都將被轉換爲16位色格式,所以這兩張圖都將發生這個轉換。由於24位色圖不帶有alpha通道,並且它被放置在"res/raw"目錄下,所以它將在我們載入activity中的資源時被自動轉換爲16位色圖。我們能通過檢查Logcat裏的消息來驗證這一點。我們從BitmapFactory裏得到的Bitmap對象實際上是RGB565格式的,而BitmapFactory足夠聰明會幫我們抖動處理圖片。而我們帶有alpha通道的的32位色圖,則被載入爲了ARGB8888位圖。在該圖片對應的ImageView被繪製到屏幕時,它將被轉換爲本地16位色格式。這裏我們看到的是繪製時發生的轉換不會進行抖動處理。讓我們看一下,如果我們指明轉換時進行抖動處理,情況是否會好一些。在上例中19行添加如下幾行代碼:

// Enable dithering when our 32-bit image gets drawn.
Drawable drawable32 = image32.getDrawable();
drawable32.setDither(true);

  編譯後上傳至設備然後查看結果:


  啊,好了。現在我們的32位圖也已經被抖動處理好了。這裏我們簡單地告訴ImageView我們想要Bitmap被被繪製時進行抖動處理。不幸的是這將影響繪圖速度,所以這並非一個理想的解決方案。但我們能夠消除這個性能問題,通過在我們將圖片送至ImageView之前預先進行抖動處理:

 1 @Override
2 public void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4
5 // Load both of our images from our application's resources.
6 Resources r = getResources();
7 Bitmap resource24 = BitmapFactory.decodeResource(r, R.raw.spectrum_gray_nodither_24);
8 Bitmap resource32 = BitmapFactory.decodeResource(r, R.raw.spectrum_gray_nodither_32);
9
10 // Print some log statements to show what pixel format these images were decoded with.
11 Log.d("FormatTest","Resource24: " + resource24.getConfig()); // Resource24: RGB_565
12 Log.d("FormatTest","Resource32: " + resource32.getConfig()); // Resource32: ARGB_8888
13
14 // Save the dimensions of these images.
15 int width = resource24.getWidth();
16 int height = resource24.getHeight();
17
18 // Create a 16-bit RGB565 bitmap that we will draw our 32-bit image to with dithering.
19 Bitmap final32 = Bitmap.createBitmap(width, height, Config.RGB_565);
20
21 // Create a new paint object we will use to draw our bitmap with. This is how we tell
22 // Android that we want to dither the 32-bit image when it gets drawn to our 16-bit final
23 // bitmap.
24 Paint ditherPaint = new Paint();
25 ditherPaint.setDither(true);
26
27 // Create a new canvas for our 16-bit final bitmap, and draw our 32-bit image to it with
28 // the paint object we just created.
29 Canvas canvas = new Canvas();
30 canvas.setBitmap(final32);
31 canvas.drawBitmap(resource32, 0, 0, ditherPaint);
32
33 // Create two image views to show these bitmaps in.
34 ImageView image24 = new ImageView(this);
35 ImageView image32 = new ImageView(this);
36 image24.setImageBitmap(resource24);
37 image32.setImageBitmap(final32);
38
39 // Create a simple layout to show these two image views side-by-side.
40 LayoutParams wrap = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
41 LayoutParams fill = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
42 RelativeLayout.LayoutParams params24 = new RelativeLayout.LayoutParams(wrap);
43 params24.addRule(RelativeLayout.CENTER_VERTICAL);
44 params24.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
45 RelativeLayout.LayoutParams params32 = new RelativeLayout.LayoutParams(wrap);
46 params32.addRule(RelativeLayout.CENTER_VERTICAL);
47 params32.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
48 RelativeLayout layout = new RelativeLayout(this);
49 layout.addView(image24, params24);
50 layout.addView(image32, params32);
51 layout.setBackgroundColor(Color.BLACK);
52
53 // Show this layout in our activity.
54 setContentView(layout, fill);
55 }

  運行這段代碼將會產生之前一樣的效果,但是通過提前抖動處理,我們避免了性能瓶頸。如果你只是簡單地載入和顯示你的圖片到你的activity一次,這種處理並不是必要的。但是如果需要頻繁地繪製,則需要提前抖動處理以避免浪費處理時間和電池壽命。

         這時候你可能會想了,爲什麼我們要做那麼多額外的工作來確保我們的圖片被解碼爲32位色圖像,然而其結果卻只是還要在我們繪圖時做更多的工作來確保圖片被抖動處理了呢?既然24位色被自動轉換爲了效果不錯的16位色圖而不需要你的任何干預,那麼我們使用32位色圖的理由是什麼呢?實際上,對於上面的例子,我們還沒有對這些位圖做任何的處理和操作,所以被自動轉換的24位色圖片看上去和我們手動轉換的32位色圖片效果一樣。他們最終都有着一樣的像素數據。但是若是我們想要在顯示之前對圖片進行一些處理呢?咱們先試試看。在你的設備上運行下述代碼:

  

 1 @Override
2 public void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4
5 // Load both of our images from our application's resources.
6 Resources r = getResources();
7 Bitmap resource24 = BitmapFactory.decodeResource(r, R.raw.spectrum_gray_nodither_24);
8 Bitmap resource32 = BitmapFactory.decodeResource(r, R.raw.spectrum_gray_dithered_32);
9
10 Log.d("FormatTest","Resource24: " + resource24.getConfig()); // Resource24: RGB_565
11 Log.d("FormatTest","Resource32: " + resource32.getConfig()); // Resource32: ARGB_8888
12
13 // Sadly, the images we have decoded from our resources are immutable. Since we want to
14 // change them, we need to copy them into new mutable bitmaps, giving each of them the same
15 // pixel format as their source.
16 Bitmap bitmap24 = resource24.copy(resource24.getConfig(), true);
17 Bitmap bitmap32 = resource32.copy(resource32.getConfig(), true);
18
19 // Save the dimensions of these images.
20 int width = bitmap24.getWidth();
21 int height = bitmap24.getHeight();
22
23 // Create a new paint object that we will use to manipulate our images. This will tell
24 // Android that we want to replace any color in our image that is even remotely similar to
25 // 0xFF307070 (a dark teal) with 0xFF000000 (black).
26 Paint avoid1Paint = new Paint();
27 avoid1Paint.setColor(0xFF000000);
28 avoid1Paint.setXfermode(new AvoidXfermode(0xFF307070, 255, AvoidXfermode.Mode.TARGET));
29
30 // Make another paint object, but this one will replace any color that is similar to a
31 // 0xFF00C000 (green) with 0xFF0070D0 (skyish blue) instead.
32 Paint avoid2Paint = new Paint();
33 avoid2Paint.setColor(0xFF0070D0);
34 avoid2Paint.setXfermode(new AvoidXfermode(0xFF00C000, 245, AvoidXfermode.Mode.TARGET));
35
36 Paint fadePaint = new Paint();
37 int[] fadeColors = {0x00000000, 0xFF000000, 0xFF000000, 0x00000000};
38 fadePaint.setShader(new LinearGradient(0, 0, 0, height, fadeColors, null,
39 LinearGradient.TileMode.CLAMP));
40 fadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
41
42 // Create a new canvas for our bitmaps, and draw a full-sized rectangle to each of them
43 // which will apply the paint object we just created.
44 Canvas canvas = new Canvas();
45 canvas.setBitmap(bitmap24);
46 canvas.drawRect(0, 0, width, height, avoid1Paint);
47 canvas.drawRect(0, 0, width, height, avoid2Paint);
48 canvas.drawRect(0, 0, width, height, fadePaint);
49 canvas.setBitmap(bitmap32);
50 canvas.drawRect(0, 0, width, height, avoid1Paint);
51 canvas.drawRect(0, 0, width, height, avoid2Paint);
52 canvas.drawRect(0, 0, width, height, fadePaint);
53
54 // Create a 16-bit RGB565 bitmap that we will draw our 32-bit image to with dithering. We
55 // only need to do this for our 32-bit image, and not our 24-bit image, because it is
56 // already in the RGB565 format.
57 Bitmap final32 = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
58
59 // Create a new paint object that we will use to draw our bitmap with. This is how we tell
60 // Android that we want to dither the 32-bit image when it gets drawn to our 16-bit final
61 // bitmap.
62 Paint ditherPaint = new Paint();
63 ditherPaint.setDither(true);
64
65 // Using our canvas from above, draw our 32-bit image to it with the paint object we just
66 // created.
67 canvas.setBitmap(final32);
68 canvas.drawBitmap(bitmap32, 0, 0, ditherPaint);
69
70 // Create two image views to show these bitmaps in.
71 ImageView image24 = new ImageView(this);
72 ImageView image32 = new ImageView(this);
73 image24.setImageBitmap(bitmap24);
74 image32.setImageBitmap(final32);
75
76 // Create a simple layout to show these two image views side-by-side.
77 LayoutParams wrap = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
78 LayoutParams fill = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
79 RelativeLayout.LayoutParams params24 = new RelativeLayout.LayoutParams(wrap);
80 params24.addRule(RelativeLayout.CENTER_VERTICAL);
81 params24.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
82 RelativeLayout.LayoutParams params32 = new RelativeLayout.LayoutParams(wrap);
83 params32.addRule(RelativeLayout.CENTER_VERTICAL);
84 params32.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
85 RelativeLayout layout = new RelativeLayout(this);
86 layout.addView(image24, params24);
87 layout.addView(image32, params32);
88 layout.setBackgroundColor(Color.BLACK);
89
90 // Show this layout in our activity.
91 setContentView(layout, fill);
92 }

         這裏我們載入了和之前例子一樣的圖片,但我們現在使用Android內置的圖形函數對他們預先進行了一些處理,而不是直接將它們添加到ImageView中顯示。我們在顯示兩張圖片前分別將它們都使用了三個不同的濾鏡進行處理。要是你不明白23-52行代碼是幹嘛的,別擔心,我會在以後的文章中詳盡的介紹它們。而對於現在而言,你只需要知道我們的原始圖片在運行時被我們的程序進行了巨大的變換。下圖是最終的效果:

  正如你所看到的,在經過了許多操作後,24位色圖有了巨大的失真,而32位色圖的效果則依然很不錯。除了”條帶”和失真,你可以發現24位色圖的顏色根本都不正確。那麼,爲什麼24位色圖會出現這種現象而32位色圖卻沒有呢?原因就在於24位色圖被Android自動轉換爲了RGB565格式圖像。一張16位色圖片僅能夠顯示65,535種不同顏色,而32位色圖卻能夠顯示16,777,215種顏色,還帶有255級半透明效果。這意味着當我們處理16位色圖時,處理過後的顏色不能夠被16位色所支持的65,535種顏色精確地表示出來。所以處理後的顏色被截取到了最臨近相似的顏色值去了。每次我們對圖片進行操作時,這種截取都會發生,所以圖片將變得更加不精確。而當時用32位色時,這種現象雖然依然會有,但是由於有着將近17兆的顏色可以表示,這種副作用對於大部分程序來說將可以被忽略。

         我希望我的這篇文章能幫到各位。我希望各位能從我的文章中學會的主要思想是:

  1. 意識到Android的自動圖片轉換和抖動處理機制。當事情不照你所想發展時,知道Android是如何儲存、載入以及繪製你的圖像的話將大大減輕你的頭痛。
  2. 如果你要對你的圖像被載入後進行任何的處理,確保你的圖片是ARGB8888格式的。
  3. 通過使用某種圖形處理軟件將你的圖像保存爲32位帶alpha通道的PNG圖片,你可以強制這張圖片被載入爲32位色。
  4. 當你處理完你的32位色圖片後,記得啓用抖動處理後再將該圖片轉換爲16位RGB565色圖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章