Android繪圖機制與處理技巧(二)——Android影象處理之色彩特效處理

Android繪圖機制與處理技巧(二)——Android影象處理之色彩特效處理

Android對於圖片處理,最常使用到的資料結構是點陣圖——Bitmap,它包含了一張圖片所有的資料。整個圖片都是由點陣和顏色值組成的,所謂點陣就是一個包含畫素的矩陣,每一個元素對應著圖片的一個畫素。而顏色值——ARGB,分別對應透明圖、紅、綠、藍這四個通道分量,它們共同決定了每個畫素點顯示的顏色。

色彩矩陣分析

在色彩處理中,通常使用以下三個角度來描述一個影象。

  • 色調——物體傳播的顏色
  • 飽和度——顏色的純度,從0(灰)到100%(飽和)來進行描述
  • 亮度——顏色的相對明暗程度

而在Android中,系統使用一個顏色矩陣——ColorMatrix,來處理影象的這些色彩效果。Android中顏色矩陣是一個4×5的數字矩陣,它用來對圖片的色彩進行處理。而對於每個畫素點,都有一個顏色分量矩陣用來儲存顏色的RGBA值,如下圖所示:

⎡⎣⎢⎢⎢afkpbglqchmrdinsejot⎤⎦⎥⎥⎥顏色矩陣A

\begin{bmatrix}
a & b & c & d & e \\
f & g & h & i & j \\
k & l & m & n & o\\
p & q & r& s& t
\end{bmatrix}顏色矩陣A

⎡⎣⎢⎢⎢⎢⎢⎢RGBA1⎤⎦⎥⎥⎥⎥⎥⎥顏色分量矩陣C

\begin{bmatrix}
R\\
G\\
B\\
A\\
1
\end{bmatrix}顏色分量矩陣C

圖中矩陣A就是一個4×5的顏色矩陣。在Android中,它會以一維陣列的形式來儲存[a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t],而C則是一個顏色分量矩陣。在處理影象時,使用矩陣乘法運算AC來處理顏色分量矩陣,如下圖所示:

⎡⎣⎢⎢⎢R1G1B1A1⎤⎦⎥⎥⎥矩陣乘法運算D=AC

\begin{bmatrix}
R1\\
G1\\
B1\\
A1
\end{bmatrix}矩陣乘法運算D=AC

其中計算過程如下:

R1 = axR bxG cxB dxA e;
G1 = fxR gxG hxB ixA j;
B1 = kxR lxG mxB nxA o;
A1 = pxR qxG rxB sxA t;

從這個公式可以發現,對於顏色矩陣A是按以下方式劃分的。

  • 第一行的abcde值決定新的顏色值中的R——紅色
  • 第二行的fghij值決定新的顏色值中的G——綠色
  • 第一行的klmno值決定新的顏色值中的B——藍色
  • 第一行的pqrst值決定新的顏色值中的A——透明度
  • 矩陣A中的第五列——ejot值分別用來決定每個分量中的offset,即偏移量

現在我們重新看一下矩陣變換的計算公式,以R分量為例。如果令a=1,b、c、d、e都等於0,那麼計算結果為R1=R。因此,可以構造出一個矩陣,如下圖所示:

⎡⎣⎢⎢⎢10000100001000010000⎤⎦⎥⎥⎥初始矩陣

\begin{bmatrix}
1 & 0 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0\\
0 & 0 & 0& 1& 0
\end{bmatrix}初始矩陣

如果把這個矩陣帶入公式D=AC,那麼根據矩陣乘法運演算法則,可以得到R1=R。因此這個矩陣通常被用來作為初始的顏色矩陣來使用,它不會對原有顏色值進行任何變化。

當要變換顏色值的時候,通常有兩種方法。一種是直接改變顏色的offset,即偏移量的值來修改顏色分量,另一種方法是直接改變對應RGBA值的係數來調整顏色分量的值。

改變偏移量

要修改R1的值,只要將第五列的值進行修改即可。即改變顏色的偏移量,其他值保持初始矩陣的值,如下圖所示:

⎡⎣⎢⎢⎢100001000010000110010000⎤⎦⎥⎥⎥改變顏色偏移量

\begin{bmatrix}
1 & 0 & 0 & 0 & 100 \\
0 & 1 & 0 & 0 & 100 \\
0 & 0 & 1 & 0 & 0\\
0 & 0 & 0& 1& 0
\end{bmatrix}改變顏色偏移量

在上面這個矩陣中,修改了R、G所對應的顏色偏移量,那麼最後的處理結果就是影象的紅色、綠色分量增加了100。而紅色混合綠色會得到黃色,所以最終的顏色處理結果就是讓整個影象的色調偏黃色。

改變顏色系數

如果修改顏色分量中的某個係數值,而其他值依然保持初始矩陣的值,如下圖所示:

⎡⎣⎢⎢⎢10000200001000010000⎤⎦⎥⎥⎥改變顏色系數

\begin{bmatrix}
1 & 0 & 0 & 0 & 0 \\
0 & 2 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0\\
0 & 0 & 0& 1& 0
\end{bmatrix}改變顏色系數

在上面這個矩陣中,改變了G分量所對應的係數g,這樣在矩陣運算後G分量會變為以前的兩倍,最終結果就是影象的色調更加偏綠。

改變色光屬性

影象的色調、飽和度、亮度這三個屬性在影象處理中的使用非常之多。在Android中,系統封裝了一個類——ColorMatrix,也就是顏色矩陣。通過這個類,可以很方便地通過改變矩陣值來處理顏色效果。建立一個ColorMatrix物件非常簡單,程式碼如下:

ColorMatrix colorMatrix = new ColorMatrix();

下面處理不同的色光屬性。

  • 色調

Android提供了setRotate(int axis, float degrees)來設定顏色的色調。第一個引數,系統分別使用0、1、2來代表Red、Green、Blue三種顏色的處理;而第二個引數,就是需要處理的值,程式碼如下:

        ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0, hue1);
hueMatrix.setRotate(1, hue2);
hueMatrix.setRotate(2, hue3);

通過這樣的方法,可以為RGB三種顏色分量分別重新設定了不同的色調值。

  • 飽和度

Android系統提供了setSaturation(float sat)方法來設定顏色的飽和度值,程式碼如下:

        //設定顏色的飽和度
ColorMatrix saturationMatrix = new ColorMatrix();
//saturation引數即代表設定顏色的飽和度的值,當飽和度為0時,影象就變成灰度影象了
saturationMatrix.setSaturation(saturation);
  • 亮度

Android系統提供了setScale(float rScale, float gScale, float bScale, float aScale)方法來設定顏色的亮度值。當三原色以相同的比例進行混合時,就會顯示出白色。系統也正是根據這個原理來改變一個影象的亮度的。當亮度為0時,影象就變為全黑了,程式碼如下所示:

        //設定顏色的亮度
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum, lum, lum, 1);

除了單獨使用上面三種方式進行顏色效果的處理之外,Android系統還封裝了矩陣的乘法運算。它提供了postConcat(ColorMatrix postmatrix)方法來將矩陣的作用效果混合,從而疊加處理效果,程式碼如下所示:

        //將矩陣的作用效果混合,從而疊加處理效果
ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);

在下面的例子中,通過滑動三個SeekBar來改變不同色光屬性的數值,並將這些數值作用到對應的矩陣中。最後通過postConcat()方法來顯示混合的處理效果。

滑動SeekBar獲取輸入值的程式碼如下所示:

package com.huangfei.example;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.SeekBar;
/**
* 通過滑動三個SeekBar來改變不同色光屬性的數值,並將這些數值作用到對應的矩陣中
*/
public class PrimaryColorActivity extends Activity implements SeekBar.OnSeekBarChangeListener {
private static final int MAX_VALUE = 255;
private static final int MID_VALUE = 127;
private ImageView mImageView;
private float mHue, mSaturation, mLum;
private Bitmap mBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_primary_color);
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test3);
mImageView = (ImageView) findViewById(R.id.imageview);
SeekBar seekBarHue = (SeekBar) findViewById(R.id.seekbarHue);
SeekBar seekBarSaturation = (SeekBar) findViewById(R.id.seekbarSaturation);
SeekBar seekBarLum = (SeekBar) findViewById(R.id.seekbarLum);
seekBarHue.setOnSeekBarChangeListener(this);
seekBarSaturation.setOnSeekBarChangeListener(this);
seekBarLum.setOnSeekBarChangeListener(this);
//設定最大值
seekBarHue.setMax(MAX_VALUE);
seekBarSaturation.setMax(MAX_VALUE);
seekBarLum.setMax(MAX_VALUE);
//設定進度
seekBarHue.setProgress(MID_VALUE);
seekBarSaturation.setProgress(MID_VALUE);
seekBarLum.setProgress(MID_VALUE);
mImageView.setImageBitmap(mBitmap);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
switch (seekBar.getId()) {
case R.id.seekbarHue://色調
mHue = (progress - MID_VALUE) * 1.0f / MID_VALUE * 180;
break;
case R.id.seekbarSaturation://飽和度
mSaturation = progress * 1.0f / MID_VALUE;
break;
case R.id.seekbarLum://亮度
mLum = progress * 1.0f / MID_VALUE;
break;
}
mImageView.setImageBitmap(ImageHelper.handleImageEffect(mBitmap, mHue, mSaturation, mLum));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}

設定影象矩陣的程式碼如下:

    /**
*改變影象色光屬性
* @param bm
* @param hue 色調
* @param saturation 飽和度
* @param lum 亮度
* @return
*/
public static Bitmap handleImageEffect(Bitmap bm, float hue, float saturation, float lum) {
//設定顏色的色調
ColorMatrix hueMatrix = new ColorMatrix();
//第一個引數,系統分別使用0、1、2來代表Red、Green、Blue三種顏色的處理;而第二個引數,就是需要處理的值
hueMatrix.setRotate(0, hue);
hueMatrix.setRotate(1, hue);
hueMatrix.setRotate(2, hue);
//設定顏色的飽和度
ColorMatrix saturationMatrix = new ColorMatrix();
//saturation引數即代表設定顏色的飽和度的值,當飽和度為0時,影象就變成灰度影象了
saturationMatrix.setSaturation(saturation);
//設定顏色的亮度
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum, lum, lum, 1);
//將矩陣的作用效果混合,從而疊加處理效果
ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);
/**
* 設定好處理的顏色矩陣後,通過使用Paint類的setColorFilter()方法,將通過imageMatrix構造的
* ColorMatrixColorFilter物件傳遞進去,並使用這個畫筆來繪製原來的影象,從而將顏色矩陣作用到原圖中
*/
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
/**
* Android系統也不允許直接修改原圖,類似Photoshop中的鎖定,必須通過原圖建立一個同樣大小的Bitmap,並將
* 原圖繪製到該Bitmap中,以一個副本的形式來修改影象。
*/
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(bm, 0, 0 ,paint);
return bitmap;
}

效果圖如下:

這裡寫圖片描述

Android顏色矩陣——ColorMatrix

通過調整顏色矩陣可以改變一副影象的色彩效果,影象處理很大程度上就是在尋找處理影象的顏色矩陣。不僅僅可以通過Android系統提供的API來進行ColorMatrix的修改,同樣可以精確地修改矩陣的值來實現顏色效果的改變。下面模擬一個4×5的顏色矩陣,通過修改矩陣中的值,來觀察圖片的效果,程式執行後的效果如下圖:

這裡寫圖片描述

通過GridLayout來進行佈局,動態新增20個EditText來建立4×5的矩陣,GridLayout佈局程式碼如下:

    <GridLayout
android:id="@ id/group"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:columnCount="5"
android:rowCount="4" />

動態建立EditText,新增到GridLayout並初始化矩陣的程式碼如下所示:

package com.huangfei.example;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.GridLayout;
import android.widget.ImageView;
/**
* Created by Administrator on 2016/5/26.
* 模擬一個4x5的顏色矩陣
*/
public class ColorMatrixActivity extends Activity {
private ImageView mImageView;
private GridLayout mGroup;
private Bitmap bitmap;
private int mEtWidth, mEtHeight;
private EditText[] mEts = new EditText[20];
private float[] mColorMatrix = new float[20];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color_matrix);
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test1);
mImageView = (ImageView) findViewById(R.id.imageview);
mGroup = (GridLayout) findViewById(R.id.group);
mImageView.setImageBitmap(bitmap);
mGroup.post(new Runnable() {
@Override
public void run() {
// 獲取寬高資訊
mEtWidth = mGroup.getWidth() / 5;
mEtHeight = mGroup.getHeight() / 4;
addEts();
initMatrix();
}
});
}
// 初始化顏色矩陣為初始狀態
private void initMatrix() {
for (int i = 0; i < 20; i  ) {
if (i % 6 == 0)
mEts[i].setText(String.valueOf(1));
else
mEts[i].setText(String.valueOf(0));
}
}
// 新增EditText
private void addEts() {
for (int i = 0; i < 20; i  ) {
mEts[i] = new EditText(this);
mGroup.addView(mEts[i], mEtWidth, mEtHeight);
}
}
// 獲取矩陣值
private void getMatrix() {
for (int i = 0; i < 20; i  ) {
mColorMatrix[i] = Float.valueOf(mEts[i].getText().toString());
}
}
// 將矩陣值設定到影象
private void setImageMatrix() {
Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.set(mColorMatrix);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(bitmap, 0, 0, paint);
mImageView.setImageBitmap(bmp);
}
// 作用矩陣效果
public void btnChange(View view) {
getMatrix();
setImageMatrix();
}
// 重置矩陣效果
public void btnReset(View view) {
initMatrix();
getMatrix();
setImageMatrix();
}
}

常用影象顏色矩陣處理效果

灰度效果

顏色矩陣如下所示:

⎡⎣⎢⎢⎢0.330.330.3300.590.590.5900.110.110.11000010000⎤⎦⎥⎥⎥灰度效果

\begin{bmatrix}
0.33 & 0.59 & 0.11 & 0 & 0 \\
0.33 & 0.59 & 0.11 & 0 & 0 \\
0.33 & 0.59 & 0.11 & 0 & 0\\
0 & 0 & 0& 1& 0
\end{bmatrix}灰度效果

處理效果如下所示:

這裡寫圖片描述

影象反轉

顏色矩陣如下所示:

⎡⎣⎢⎢⎢−10000−10000−1011111110⎤⎦⎥⎥⎥影象反轉

\begin{bmatrix}
-1 & 0 & 0 & 1 & 1 \\
0 & -1 & 0 & 1& 1 \\
0 & 0 & -1 & 1 & 1\\
0 & 0 & 0& 1& 0
\end{bmatrix}影象反轉

處理效果如下所示:

這裡寫圖片描述

懷舊效果

顏色矩陣如下所示:

⎡⎣⎢⎢⎢0.3930.3490.27200.7690.6860.53400.1890.1680.131000010000⎤⎦⎥⎥⎥懷舊效果

\begin{bmatrix}
0.393 & 0.769 & 0.189 & 0 & 0 \\
0.349 & 0.686 & 0.168 & 0 & 0 \\
0.272 & 0.534 & 0.131 & 0 & 0\\
0 & 0 & 0& 1 & 0
\end{bmatrix}懷舊效果

處理效果如下所示:

這裡寫圖片描述

去色效果

顏色矩陣如下所示:

⎡⎣⎢⎢⎢1.51.51.501.51.51.501.51.51.500001−1−1−10⎤⎦⎥⎥⎥去色效果

\begin{bmatrix}
1.5 & 1.5 & 1.5 & 0 & -1 \\
1.5 & 1.5 & 1.5 & 0 & -1 \\
1.5 & 1.5 & 1.5 & 0 & -1 \\
0 & 0 & 0& 1& 0
\end{bmatrix}去色效果

處理效果如下所示:

這裡寫圖片描述

高飽和度

顏色矩陣如下所示:

⎡⎣⎢⎢⎢1.438−0.062−0.0620−0.1221.378−0.1220−0.016−0.0161.48300001−0.03−0.05−0.020⎤⎦⎥⎥⎥高飽和度

\begin{bmatrix}
1.438 & -0.122 & -0.016 & 0 & -0.03 \\
-0.062 & 1.378 & -0.016 & 0 & -0.05\\
-0.062 & -0.122 & 1.483 & 0 & -0.02\\
0 & 0 & 0& 1& 0
\end{bmatrix}高飽和度

處理效果如下所示:

這裡寫圖片描述

畫素點分析

可以通過改變每個畫素點的具體ARGB值,來達到處理一張影象效果的目的。但要注意的是,傳遞進來的原始圖片是不能修改的,一般根據原始圖片生成一張新的圖片來修改。

在Android中,系統提供了Bitmap.getPixels()方法來幫我們提取整個Bitmap中的畫素點,並儲存到一個陣列中,該方法如下所示:

getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height)

引數含義如下:

  • pixels——接收點陣圖顏色值的陣列
  • offset——寫入到pixels[]中的第一個畫素索引值
  • stride——用來表示pixels[]陣列中每行的畫素個數,用於行與行之間區分,絕對值必須大於引數width,但不必大於所要讀取圖片的寬度w(在width < w 時成立).(stride負數有何作用不知,存疑).另,pixels.length >= stride * height,否則會丟擲ArrayIndexOutOfBoundsException異常。 stride > width時,可以在pixels[]陣列中新增每行的附加資訊,可做它用.
  • x——從點陣圖中讀取的第一個畫素的x座標值
  • y——從點陣圖中讀取的第一個畫素的y座標值
  • width——從每一行中讀取的畫素寬度
  • height——讀取的行數

通常情況下,可以使用如下程式碼:

        int width = bm.getWidth();
int height = bm.getHeight();   
int[] oldPx = new int[width * height];
bm.getPixels(oldPx, 0, width, 0, 0, width, height);

接下來,就可以獲取到每個畫素具體的ARGB值了,程式碼如下所示:

            color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);

當獲取到具體的顏色值之後,就可以通過相應的演算法來修改它的ARGB值,從而來重構一張新的影象,例如進行如下處理:

            r1 = (int) (0.393 * r   0.769 * g   0.189 * b);
g1 = (int) (0.349 * r   0.686 * g   0.168 * b);
b1 = (int) (0.272 * r   0.534 * g   0.131 * b);

再通過如下所示程式碼將新的RGBA值合成畫素點:

newPx[i] = Color.argb(a, r1, g1, b1);

最後將處理之後的畫素點陣列重新set給Bitmap。從而達到影象處理的目的:

bmp.setPixels(newPx, 0, width, 0, 0, width, height);

常用影象畫素點處理效果

底片效果

若存在ABC3個畫素點,要求B點對應的底片效果演算法,程式碼如下:

B.r = 255 – B.r;
B.g = 255 – B.g;
B.b = 255 – B.b;

實現程式碼如下:

    //底片效果
public static Bitmap handleImageNegative(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int color;
int r, g, b, a;
Bitmap btm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];
int[] newPx = new int[width * height];
bitmap.getPixels(oldPx, 0, width, 0, 0, width, height);
for (int i = 0; i < width * height; i  ) {
color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);
r = 255 - r;
g = 255 - g;
b = 255 - b;
if (r > 255)
r = 255;
else if (r < 0)
r = 0;
if (g > 255)
g = 255;
else if (g < 0)
g = 0;
if (b > 255)
b = 255;
else if (b < 0)
b = 0;
newPx[i] = Color.argb(a, r, g, b);
}
btm.setPixels(newPx, 0, width, 0, 0, width, height);
return btm;
}

老照片效果

求某畫素點的老照片效果演算法,程式碼如下:

for (int i = 0; i < width * height; i  ) {
color = oldPx[i];
a = Color.alpha(color);
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
r1 = (int) (0.393 * r   0.769 * g   0.189 * b);
g1 = (int) (0.349 * r   0.686 * g   0.168 * b);
b1 = (int) (0.272 * r   0.534 * g   0.131 * b);
if (r1 > 255) {
r1 = 255;
}
if (g1 > 255) {
g1 = 255;
}
if (b1 > 255) {
b1 = 255;
}
newPx[i] = Color.argb(a, r1, g1, b1);
}

浮雕效果

求某畫素點的浮雕效果演算法,程式碼如下:

        for (int i = 1; i < width * height; i  ) {
colorBefore = oldPx[i - 1];
a = Color.alpha(colorBefore);
r = Color.red(colorBefore);
g = Color.green(colorBefore);
b = Color.blue(colorBefore);
color = oldPx[i];
r1 = Color.red(color);
g1 = Color.green(color);
b1 = Color.blue(color);
r = r - r1   127;
g = g - g1   127;
b = b - b1   127;
if (r > 255) {
r = 255;
}
if (g > 255) {
g = 255;
}
if (b > 255) {
b = 255;
}
newPx[i] = Color.argb(a, r, g, b);
}

效果如下所示:

這裡寫圖片描述

程式碼地址