Android圖像處理之色彩特效處理(學習筆記)

彩色特效處理

1.色彩矩陣分析

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

  • 色調——物體傳播的顏色
  • 飽和度——顏色的純度,從0(灰)到100%(飽和)來進行描述
  • 亮度——顏色的相對明暗程度
    在Android中,系統使用一個顏色矩陣——ColorMatrix,來處理圖像的這些色彩效果。Android中的顏色矩陣是一個4×5的數字矩陣,它用來對圖片的色彩進行處理。而對於每個像素點,都有一個顏色分量矩陣用來保存顏色的RGBA值,如下圖所示:
    A=afkpbglqchmrdinsejot      C=RGBA1

    在這個4×5的顏色矩陣中按以下方式劃分。
  • 第一行的abcde值用來決定新的顏色值中的R——紅色
  • 第二行的fghij值用來決定新的顏色值中的G——綠色
  • 第三行的klmno值用來決定新的顏色值中的B——藍色
  • 第四行的pqrst值用來決定新的顏色值中的A——透明度
  • 矩陣A中的第五列——ejot值分別用來決定每個分量重的offset,即偏移量。

1.1 改變偏移量

A=10000100001000010000      A=100001000010000110010000

在這個矩陣中修改了R,G所對應的顏色偏移量,那麼最後的處理結果就是圖像的紅色,綠色分量增加了100.而我們知道,紅色混合綠色會得到黃色,所以使得整個圖像的色調偏黃色。

1.2 改變顏色系數

A=10000200001000010000

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

1.3 改變色光屬性

圖像的色調,飽和度,亮度這三個屬性在圖像處理中的使用非常多,因此顏色矩陣中,也封裝了一些API來快速調用這些參數,而不用每次都去計算矩陣的值。
在Android中,系統封裝了一個類——ColorMatrix,也就是說前面的顏色矩陣。通過這個類,可以很方便地改變矩陣值來處理顏色效果。

ColorMatrix colorMatrix = new ColorMatrix();
  • 色調
    Android系統提供了setRotate(int axis, float degree)來幫助我們設置顏色的色調。第一個參數,系統分別使用0、1、2來代表Red、Green、Blue三種顏色的處理;而第二個參數,就是需要處理的值,代碼如下:
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix .setRotate(0,hue0);
hueMatrix .setRotate(1,hue1);
hueMatrix .setRotate(2,hue2);

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

  • 飽和度
    Android系統提供了setSaturation(float sat)方法來設置顏色的飽和度,參數代表設置顏色飽和度的值,當飽和度爲0時,圖像就變成灰度圖像了。
ColorMatrix saturationMatrix=new ColorMatrix();
saturationMatrix.setSaturation(saturation);
  • 亮度
    當三原色以相同的比例進行混合的時候,就會顯示出白色,系統正式使用這個原理來改變一個圖像的亮度的,代碼如下,當亮度爲0時,圖像就變成全黑了。
ColorMatrix lumMatrix=new ColorMatrix();
lumMatrix.setScale(lum,lum,lum,1);

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

ColorMatrix imageMatrix=new ColorMatrix();
imageMatrix.posConcat(hueMatrix);
imageMatrix.posConcat(saturationMatrix);
imageMatrix.posConcat(lumMatrix);

最後通過paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix))設置給paint,並使用這個畫筆來繪製原來的圖像,從而將顏色矩陣作用到原圖上。

paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
canvas.drawBitmap(bitmap,0,0,paint);

2. 常用圖像顏色矩陣處理效果

2.1 灰度效果

0.33f,0.59f,0.11f,0, 0,
0.33f,0.59f,0.11f,0, 0,
0.33f,0.59f,0.11f,0, 0,
0,    0,    0,    1, 0,

2.2 圖像反轉

-1, 0, 0, 1, 1,
 0,-1, 0, 1, 1,
 0, 0,-1, 1, 1,
 0, 0, 0, 1, 0,

2.3 懷舊效果

0.393f,0.769f,0.189f,0, 0,
0.349f,0.686f.0.168f,0, 0,
0.242f,0.534f,0.131f,0, 0,
0,     0,     0,     0, 0,

2.4 去色效果

1.5f, 1.5f, 1.5f, 0, -1,
1.5f, 1.5f, 1.5f, 0, -1,
1.5f, 1.5f, 1.5f, 0, -1,
0,    0,    0,    1,  0,

2.5 高飽和度

 1.438f,-0.122f,-0.016f, 0,-0.03f,
-0.062f, 1.378f,-0.016f, 0, 0.05f,
-0.062f,-0.122f, 1.483f, 0,-0.02f
 0,      0,      0,      1, 0,

3. 像素點分析

作爲更加精確的圖像處理方式,可以通過改變每個像素點的具體ARGB值,來達到處理一張圖像效果的目的。需要注意的是,傳遞進來的原始圖片是不能修改的,一般根據原始圖片生成一張新的圖片來修改。
在Android中,系統提供了Bitmap.getPixels()方法來幫我們提取整個Bitmap中的像素點,並保存到一個數組中。

bitmap.getPixels(pixels,offset,stride,x,y,width,height);

這幾個參數的含義如下:

  • pixels——接收位圖顏色值的數組
  • offset——寫入到pixels[]中的第一個像素索引值
  • stride——pixels[]中的行間距
  • x——從位圖中讀取的第一個像素的x座標值
  • y——從位圖中讀取的第一個像素的y座標值
  • width——從每一行中讀取的像素寬度
  • height——讀取的行數
    通常情況下,可以使用如下代碼:
bitmap.getPixels(oldPx,0,bm.getWidth(),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,從而達到圖像處理的目的。

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

4. 常用像素像素點處理效果

4.1 底片效果

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

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

實現代碼如下:

public static Bitmap handleImageNegative(Bitmap bm){
    int width = bm.getWidth();
    int height - bm.getHeight();
    int color;
    int r,g,b,a;

    Bitmap bmp=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);

    int[]oldPx=new int[width * height];
    int[]newPx=new int[width * height];
    bm.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);
    }
    bmp.setPixels(newPx,0,width,0,0,widht,height);
    return bmp;
}

4.2 老照片效果

求某像素點的老照片效果算法,代碼如下:

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

4.3 浮雕效果

若存在ABC3個像素點,要求B點對應的浮雕效果算法,代碼如下:

B.r=C.r-B.r+127;
B.g=C.g-B.g+127;
B.b=C.b-B.b+127;

5. 圖形特效處理

5.1 Android變形矩陣——Matrix

對於圖像的圖形變換,Android系統也通過矩陣來進行處理。Android的圖形變換矩陣是一個3×3的矩陣,如下:

A=adgbehcfi   C=XY1   R=X1Y11=AC

當使用變換矩陣去處理每一個像素點的時候,與顏色矩陣的矩陣乘法一樣,計算公式如下:
X1=a×X+b×Y+cY1=d×X+e×Y+f1=g×X+h×Y+i

通常情況下,會讓g=h=0,i=1,這樣使1=g×X+h×Y+i恆成立。因此只需要着重關注上面幾個參數就可以了。
圖形初始矩陣和色彩變換初始矩陣一樣:
100010001

圖像的變形處理通常包含以下四類基本變換。
  • Translate——平移變換
  • Rotate——旋轉變換
  • Scale——縮放變換
  • Skew——錯切變換
5.1.1 平移變換
5.1.2 旋轉變換
5.1.3 縮放變換
5.1.4 錯切變換

矩陣變換規律:

ADGBEHCFI        Scale_XSkew_Y0Skew_XScale_Y0Trans_XTrans_Y1
  • A和E控制Scale——縮放變換
  • B和D控制Skew——錯切變換
  • C和F控制Trans——平移變換
  • A、B、D、E共同控制Rotate——旋轉變換

在圖形變換矩陣中,同樣是通過一個一維數組來模擬矩陣,並通過setValues()方法將一個一維數組轉換爲圖形變換矩陣,代碼如下:

float[] mImageMatrix=new float[9];
Matrix matrix=new Matrix();
matrix.setValues(mImageMatrix);

當獲得變換矩陣後,就可以通過以下代碼將一個圖形以這個變換矩陣的形式繪製出來

canvas.drawBitmpa(mBitmap,matrix,null);

Android系統同樣提供了一些API來簡化矩陣的運算。Android中使用Matri類來封裝矩陣,並提供了以下幾個方法:

  • matri X.setRoate()——旋轉變換
  • matri X.setTranslate()——平移變換
  • matri X.setScale()——縮放變換
  • matri X.setSkew()——錯切變換
  • pre()和post()——提供矩陣的前乘和後乘運算

Matri類的set方法會重置矩陣中的所有值,而post和pre方法不會,這兩個方法常用來實現矩陣的混合作用。不過要注意的是,矩陣運算並不滿足交換律,所以矩陣乘法的前乘和後乘是兩種不同的運算方式。例如:
(1)先平移到(300,100)
(2)再旋轉45度
(3)最後平移到(200,200)
如果使用後乘運算,表示當前矩陣乘上參數代表的矩陣

matri X.setRotate(45);
matri X.postTranslate(200,200);

如果使用前乘運算,表示參數代表的矩陣乘上當前矩陣

matri X.setTranslate(200,200);
matri X.preRotate(45);

5.2 像素塊分析

drawBitmapMesh()與操縱像素點來改變色彩的原理類似,只不過是把圖像分成一個個的小塊,然後通過改變每一個圖像塊來修改整個圖像。代碼如下:

drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[]verts, int vertOffset, int[]colors, int colorOffset, Paint paint)

關鍵參數如下:

  • bitmap:將要扭曲的圖像
  • meshWidth:需要的橫向網格數目
  • meshHeight:需要的縱向網格數目
  • verts:網格交叉點座標數組
  • vertOffset:verts數組中開始跳過的(x,y)座標對的數目

6. Android圖像處理之畫筆特效處理

6.1 PorterDuffXfermode

首先看一下API Demo中一張經典的圖:
這裏寫圖片描述
ProterDuffXfermode設置的是兩個圖層交集區域的顯示方式,dst是先畫的圖形,而src是後畫的圖形。
經常用到DST_IN、SRC_IN模式來實現將一個矩形圖片變成圓角或者圓形圖片的效果。
如下:先用一個普通畫筆畫畫一個遮罩層,再用帶ProterDuffXfermode的畫筆將圖形畫在罩層上,這樣就可以通過上面所說的效果來混合兩個圖像了。

mBitmap=BitmapFactory.decodeResourse(getResoures(),R.drawable.test1);
mOut=Bitmap.createBitmap(mBitmap.getWidth(),mBitmap.getHeight(),Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas();
mPaint=new Paint();
mPaint.setAntiAlias(true);
canvas.drawRoundRect(0,0,mBitmap.getWidth(),mBitmap.getHeight(),80,80,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(mBitmap,0,0,mPaint);

6.2 Shader

Shader又被稱之爲着色器、渲染器,它用來實現一系列的漸變,渲染效果。Android中的Shader包括以下幾種。

  • BitmapShader——位圖Shader
  • LinearGradient——線性Shader
  • RadialGradient——光束Shader
  • SweepGradient——梯度Shader
  • ComposeShader——混合Shader

除了第一個Shader以外,其他的Shader都比較正常,實現了名副其實的漸變,渲染效果。而與其他的Shader所產生的漸變不同,BitmapShader產生的是一個圖像,這有點像Photoshop中的圖像填充漸變,他的作用就是通過Paint對畫布進行指定Binmap的填充,填充模式有三種:

  • CLAMP拉伸——拉伸的是圖片最後的那一個像素,不斷重複
  • REPEAT重複——橫向,縱向不斷重複
  • MIRROR鏡像——橫向不斷翻轉重複,縱向不斷翻轉重複
mBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.test);
mBitmapShader=new BitmapShader(mBitmap,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);
mPaint=new Paint();
mPaint.setShader(mBitmapShader);
canvas.drawCircle(500,250,200,mPaint);
paint.setShader(new LinearGradient(0,0,400,400,Color.BLUE,Color.YELLOW,Shader.TileMode.REPEAT));
canvas.drawRect(0,0,400,400,paint);

6.3 PathEffect

  • CornerPathEffect——拐角處變得圓滑
  • DiscretePathEffect——線段上產生許多雜點
  • DashPathEffect——虛線
  • PathDashPathEffect——和DashPathEffect類似,在它的基礎上,可以設置顯示點的圖形,即方形點的虛線,圓形點的虛線
  • ComposePathEffect——將任意兩種路徑特性組合起來形成新的效果
mEffects[0]=null;
mEffects[1]=new CornerPathEffect();
mEffects[2]=new DiscretePathEffect(); 
mEffects[3]=new DashPathEffect(); 
Path path=new Path();
path.addRect(0,0,8,8,Path.Direction.CWW);
mEffects[4]=new PathDashPathEffect(path,12,0,PathDashPathEffect.Style.TPTATE);
mEffects[5]=ComposePathEffect(mEffect[3],mEffects[1]);
for(int i=0;i<mEffects.length;i++){
    mPaint.setPathEffect(mEffects[i]);
    canvas.drawPath(mPath,mPaint);
    canvas.translate(0,200);
}

7. View的孿生兄弟——SurfaceView

7.1 SurfaceView和View的區別

View通過刷新來重繪視圖,Android系統通過發出VSYNC信號來進行屏幕的重繪,刷新的間隔時間爲16ms。如果在16ms內View完成你所需要執行的所有操作,那麼用戶在視覺上,就不會產生卡頓的感覺;而如果執行的操作邏輯太多,特別是需要頻繁刷新的界面上,例如遊戲界面,那麼就不會不斷阻塞主線程,從而導致畫面卡頓。很多時候在自定義View的Log中經常會看到如下警告:

“Skipped 47 frames!The application may be doing too much work on its main thread"

這些警告的產生很多情況下就是因爲在繪製過程中,處理的邏輯太多造成的。
因此,Android系統提供了SurfaceView組建來解決這個問題。SurfaceView可以說是View的孿生兄弟,但它與View還是有所不同的,它們的區別主要體現在一下幾點:

  • View主要適用於主動更新的情況下,而SurfaceView主要使用與被動更新,例如頻繁的刷新。
  • View在主線程中對畫面進行刷新,而SurfaceView通常會通過一個子線程來進行頁面的刷新。
  • View在繪圖時沒有使用雙緩衝機制,而SurfaceView在底層實現機制中就已經實現了雙緩衝機制。

7.2 SurfaceView的使用

● 創建SurfaceView

創建自定義的SurfaceView繼承自SurfaceView,並實現兩個接口——SurfaceHolder。Callback和Runnable,代碼如下:

public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable

通過實現這兩個接口沒就需要在自定義的SurfaceView中實現接口的方法:

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

分別對應SurfaceView的創建,改變和銷燬過程。

對於Runnable接口,需要實現run()方法,代碼:

    @Override
    public void run() {
    }

●初始化SurfaceView

通常需要定義以下三個成員變量,代碼如下:

//SurfaceHolder
private SurfaceHolder mHolder;
//用於繪圖的Canvas
private Canvas mCanvas;
//子線程標誌位
private boolean mIsDrawing;

初始化方法中對SurfaceHolder初始化,並註冊SurfaceHolder的回調方法。

mHolder = getHolder();
        mHolder.addCallback(this);

在SurfaceView中沒我們也通過Canvas來進行繪圖,而另一個標誌位,則是用來控制子線程的。

●使用SurfaceView

通過SurfaceHolder對象的lockCanvas()方法,就可以獲得當前的Canvas繪圖對象。獲取到的Canvas對象還是繼續上次的Canvas對象,而不是一個新的對象。因此之前的繪圖操作都將被保留,如果需要擦除,則可以在繪製前,通過drawColor()方法進行清屏操作。
繪製的時候,充分利用SurfaceView的三個回調方法,在SurfaceCreate()方法中開啓子線程進行繪製,而子線程使用一個while(mIsDrawing)的循環來不停的進行繪製,而在繪製的具體邏輯中,通過lockCanvas()方法獲取Canvas對象進行繪製,並通過unlockCanvasAndPost(mCanvas)方法對畫布內容進行提交。整個SurfaceView的末班代碼如下:

package com.example.hp.dragviewgroup;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * Created by hp on 2016/1/20.
 */
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    //SurfaceHolder
    private SurfaceHolder mHolder;
    //用於繪圖的Canvas
    private Canvas mCanvas;
    //子線程標誌位
    private boolean mIsDrawing;

    public SurfaceViewTemplate(Context context) {
        super(context);
        initView();
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        mHolder = getHolder();
        mHolder.addCallback(this);
        setFocusable(true);
        setFocusableInTouchMode(true);
        //控制是否在屏幕應保持修改的值。
        this.setKeepScreenOn(true);
//        mHolder.setFormat(PixelFormat.OPAQUE);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            draw();
        }
    }

    private void draw() {
        try {
            mCanvas = mHolder.lockCanvas();
            //draw something
        } catch (Exception e) {

        } finally {
            if (mCanvas != null) {
                mHolder.unlockCanvasAndPost(mCanvas);
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章