1. 程式人生 > >Android影象處理之圖形特效處理

Android影象處理之圖形特效處理

上一篇部落格說到了Android影象的色彩處理,使用的是ColorMatrix矩陣;本篇部落格說Android圖形的特效處理,使用的是Matrix這個類。

一、Android變形矩陣——Matricx:

跟Android影象的色彩處理基本一樣,只是將ColorMatrix換成了Matrix,ColorMatrix是4*5的矩陣,Matrix是3*3的。每個畫素點表達了其座標的X、Y資訊:



當使用變換矩陣去處理每一個畫素點的時候,與顏色矩陣的矩陣乘法一樣,計算公式如下所示:




通常情況下,會讓g=h=0i=1,這樣就使1=gX+hY+i    恆成立。因此,只需著重關注上面幾個引數即可。

與色彩變換矩陣的初始矩陣一樣,圖形變換矩陣也有一個初始矩陣。就是對角線元素a、e、i為1,其他元素為0的矩陣,如下圖所示:



影象的變形處理通常包含以下四類基本變換:
  • Translate——平移變換
  • Rotate——旋轉變換
  • Scale——縮放變換
  • Skew——錯切變換

1、平移變換

平移變換的座標值變換過程就是將每個畫素點都進行平移變換,當從P(x0,y0)P(x1,y1)時,所需的平移矩陣如下所示:


2、旋轉變換

旋轉變換即指一個點圍繞一箇中心旋轉到一個新的點。當從P(x0,y0)點,以座標原點O為旋轉中心旋轉到P(x1,y1)時,可以將點的座標都表達成OP與X軸正方向夾角的函式表示式(其中r為線段OP的長度,αOP(x0,y0)XθOP(x0,y0)OP(x1,y1)),如下所示:

x

0=rcosα
y0=rsinα
x1=rcos(α+θ)=rcosαcosθrsinαsinθ=x0cosθy0sinθ
y1=rsin(α+θ)=rsinαcosθ+rcosαsinθ=y0cosθ+x0sinθ

矩陣形式如下圖所示:



前面是以座標原點為旋轉中心的旋轉變換,如果以任意點O為旋轉中心來進行旋轉變換,通常需要以下三個步驟:
  • 將座標原點平移到O點
  • 使用前面講的以座標原點為中心的旋轉方法進行旋轉變換
  • 將座標原點還原

3、縮放變換

一個畫素點是不存在縮放的概念的,但是由於影象是由很多個畫素點組成的,如果將每個點的座標都進行相同比例的縮放,最終就會形成讓整個影象縮放的效果,縮放效果的公式如下

x

1=K1x0
y1=K2y0

矩陣形式如下圖所示:


4、錯切變換

錯切變換(skew)在數學上又稱為Shear mapping(可譯為“剪下變換“)或者Transvection(縮並),它是一種比較特殊的線性變換。錯切變換的效果就是讓所有點的X座標(或者Y座標)保持不變,而對應的Y座標(或者X座標)則按比例發生平移,且平移的大小和該點到Y軸(或者X軸)的距離成正比。錯切變換通常包含兩種——水平錯切與垂直錯切。

錯切變換的計算公式如下:

  • 水平錯切

x1=x0+K1y0
y1=y0

  • 垂直錯切

x1=x0
y1=K2x0+y0

矩陣形式如下圖


由上面的分析可以發現,這個圖形變換3x3的矩陣與色彩變換矩陣一樣,每個位置的元素所表示的功能是有規律的,總結如下:



可以發現,a、b、c、d、e、f這六個矩陣元素分別對應以下變換:
  • a和e控制Scale——縮放變換
  • b和d控制Skew——錯切變換
  • a和e控制Trans——平移變換
  • a、b、d、e共同控制Rotate——旋轉變換

通過類似色彩矩陣中模擬矩陣的例子來模擬變形矩陣。在圖形變換矩陣中,同樣是通過一個一維陣列來模擬矩陣,並通過setValues()方法將一個一維陣列轉換為圖形變換矩陣,程式碼如下所示:

   private float[] mImageMatrix = new float[9];


   Matrix matrix = new Matrix();
   matrix.setValues(mImageMatrix);

當獲得了變換矩陣後,就可以通過以下程式碼將一個影象以這個變換矩陣的形式繪製出來。

   canvas.drawBitmap(mBitmap, mMatrix, null);

示例程式碼:

activity:

package com.example.androidmatrix;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.GridLayout;
import android.widget.ImageView;

public class TestMatrixActivity extends Activity {
	//定義元件
	private ImageView imageView;
	private GridLayout group;
	
	private Bitmap bitmap;
	private EditText[] edits = new EditText[9];
	private float[] edittexts = new float[9];
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.testmatrix);
		
		imageView = (ImageView) findViewById(R.id.imageView);
		group = (GridLayout) findViewById(R.id.group);
		
		bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
		imageView.setImageBitmap(bitmap);
		
		group.post(new Runnable() {
			public void run() {
				setNineEdits();
				fillNineEdits();
			}
		});
	}
	
	//建立9個編輯框
	private void setNineEdits(){
		int width = group.getWidth();
		int height = group.getHeight();
		for (int i = 0; i < edits.length; i++) {
			edits[i] = new EditText(TestMatrixActivity.this);
			edits[i].setWidth(width / 3);
			edits[i].setHeight(height / 3);
			group.addView(edits[i]);
		}
	}
	//給九個編輯框賦值
	private void fillNineEdits(){
		for (int i = 0; i < edits.length; i++) {
			if(i % 4 == 0){
				edits[i].setText(String.valueOf(1));
			}else{
				edits[i].setText(String.valueOf(0));
			}
		}
	}
	
	//重新獲取九個編輯框的值
	private void getNineEdits(){
		for (int i = 0; i < edits.length; i++) {
			edittexts[i] = Float.valueOf(edits[i].getText().toString().trim());
		}
	}
	
	private void change(){
		Matrix matrix = new Matrix();
		Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
		matrix.setValues(edittexts);
		Canvas canvas = new Canvas(bmp);
		canvas.drawBitmap(bitmap, matrix, null);
		imageView.setImageBitmap(bmp);
	}
	
	public void onChange(View view){
		getNineEdits();
		change();
	}
	
	public void onReset(View view){
		fillNineEdits();
		change();
	}
}

介面:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="2" />

    <GridLayout
        android:id="@+id/group"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_weight="3"
        android:columnCount="3"
        android:rowCount="3" >
    </GridLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onChange"
            android:text="生效" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onReset"
            android:text="重置" />
    </LinearLayout>

</LinearLayout>

效果圖:


Android系統同樣提供了一些API來簡化矩陣的運算,我們不必每次都去設定矩陣的每一個元素值。Android中使用Matrix類來封裝矩陣,並提供了以下幾個操作方法來實現上面的四中變換方式:

  • matrix.setRotate()——旋轉變換
  • matrix.setTranslate()——平移變換
  • matrix.setScale()——縮放變換
  • matrix.setSkew()——錯切變換
  • matrix.preX和matrix.postY——提供矩陣的前乘和後乘運算

Matrix類的set方法會重置矩陣中的值,而post和pre方法不會,這兩個方法常用來實現矩陣的混合作用。不過要注意的是,矩陣運算不滿足乘法的交換律,所以矩陣乘法的前乘和後乘是兩種不同的運算方式。舉例說明,比如需要實現以下效果:

  • 先旋轉45度
  • 再平移到(200, 200)

如果使用後乘運算,表示當前矩陣乘上引數代表的矩陣,程式碼如下所示:

        matrix.setRotate(45);
        matrix.postTranslate(200, 200);

如果使用前乘運算,表示引數代表的矩陣乘上當前矩陣,程式碼如下所示:

        matrix.setTranslate(200, 200);
        matrix.preRotate(45);

示例程式碼:

介面程式碼(就一個ImageView)省略...

private ImageView imageView;
	private Bitmap bitmap;
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.testmatrixmethod);
		
		imageView = (ImageView) findViewById(R.id.imageView);
		
		bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
		imageView.setImageBitmap(changeImage());
	}
	
	private Bitmap changeImage(){

		Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
		Matrix matrix = new Matrix();
		//旋轉變換,引數是順時針旋轉角度
		matrix.setRotate(45);
		//平移變化,引數是要平移到的座標
		//matrix.setTranslate(50, 50);
		//縮放變化
		//matrix.setScale(10, 10, 10, 10);
		//錯切變換
		//matrix.setSkew(10, 10, 10, 10);
		Canvas canvas = new Canvas(bmp);
		canvas.drawBitmap(bitmap, matrix, null);
		return bmp;
	}

效果圖:


二、畫素塊分析

影象的特效處理有兩種方式,即使用矩陣來進行影象變換和使用drawBitmapMesh()方法來進行處理。drawBitmapMesh()與操縱畫素點來改變色彩的原理類似,只不過是把影象分成了一個個的小塊,然後通過改變每一個影象塊來修改整個影象。

drawBitmapMesh()方法程式碼如下:

public void 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)座標對的數目

要使用drawBitmapMesh()方法就需先將圖片分割為若干個影象塊。所以,在影象上橫縱各畫N條線,而這橫縱各N條線就交織成了NxN個點,而每個點的座標則以x1,y1,x2,y2,...,xn,yn的形式儲存在verts陣列中。也就是說verts陣列的每兩位用來儲存一個交織點,第一個是橫座標,第二個是縱座標。而整個drawBitmapMesh()方法改變影象的方式,就是靠這些座標值的改變來重新定義每一個影象塊,從而達到影象效果處理的功能。

drawBitmapMesh()方法的功能非常強大,基本上可以實現所有的影象特效,但使用起來也非常複雜,其關鍵就是在於計算、確定新的交叉點的座標。下面舉例說明如何使用drawBitmapMesh()方法來實現一個旗幟飛揚的效果。

要想達到旗幟飛揚的效果,只需要讓圖片中每個交叉點的橫座標較之前不發生變化,而縱座標較之前座標呈現一個三角函式的週期性變化即可。

首先獲取交叉點的座標,並將座標儲存到orig陣列中,其獲取交叉點座標的原理就是通過迴圈遍歷所有的交叉線,並按比例獲取其座標,程式碼如下所示:

        mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.test);
        float bitmapWidth = mBitmap.getWidth();
        float bitmapHeight = mBitmap.getHeight();
        int index = 0;
        for (int y = 0; y <= HEIGHT ; y++) {
            float fy = bitmapHeight * y / HEIGHT;
            for (int x = 0; x <= WIDTH; x++) {
                float fx = bitmapWidth * x / WIDTH;
                orig[index * 2] = verts[ index * 2] = fx;
                //這裡人為將座標+100是為了讓影象下移,避免扭曲後被螢幕遮擋
                orig[index * 2 + 1] = verts[ index * 2 + 1] = fy + 100;
                index++;
            }
        }

接下來,在onDraw()方法中改變交叉點的縱座標的值,為了實現旗幟飄揚的效果,使用一個正弦函式sinx來改變交叉點縱座標的值,而橫座標不變,並將變化後的值儲存到verts陣列中,程式碼如下所示:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        flagWave();
        K += 0.1f;//將K的值增加
        canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, verts, 0, null, 0, null);
        invalidate();
    }

    /**
     * 按當前點所在的橫座標的位置來確定縱座標的偏移量,其中A代表正弦函式中的振幅大小
     */
    private void flagWave() {
        for (int j = 0; j <= HEIGHT; j++) {
            for (int i = 0; i <= WIDTH; i++) {
                //在獲取縱座標的偏移量時,利用正弦函式的週期性給函式增加一個週期K * Math.PI,就是為了讓影象能夠動起來
                float offsetY = (float) Math.sin(2 * Math.PI * i / WIDTH + K * Math.PI);
                verts[(j * (WIDTH + 1) + i) * 2 + 1] = orig[(j * (WIDTH + 1) + i) * 2 + 1] + offsetY * A;
            }
        }
    }

這樣,每次在重繪時,通過改變相位來改變偏移量,從而造成一個動態的效果,就好象旗幟在風中飄揚一樣,效果圖如下(這裡應該是動態的,似乎一個飄揚的旗幟)。

主要程式碼:

package com.mfc.myview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

import com.example.androidmatrix.R;

public class FlagBitmapMeshView extends View {

    private final int WIDTH = 200;
    private final int HEIGHT = 200;
    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);
    private float[] verts = new float[COUNT * 2];
    private float[] orig = new float[COUNT * 2];
    private Bitmap bitmap;
    private float A;
    private float k = 1;

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

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

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

    private void initView(Context context) {
        setFocusable(true);
        bitmap = BitmapFactory.decodeResource(context.getResources(),
                R.drawable.we);
        float bitmapWidth = bitmap.getWidth();
        float bitmapHeight = bitmap.getHeight();
        int index = 0;
        for (int y = 0; y <= HEIGHT; y++) {
            float fy = bitmapHeight * y / HEIGHT;
            for (int x = 0; x <= WIDTH; x++) {
                float fx = bitmapWidth * x / WIDTH;
                orig[index * 2 + 0] = verts[index * 2 + 0] = fx;
                orig[index * 2 + 1] = verts[index * 2 + 1] = fy + 100;
                index += 1;
            }
        }
        A = 50;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        flagWave();
        k += 0.1F;
        canvas.drawBitmapMesh(bitmap, WIDTH, HEIGHT,
                verts, 0, null, 0, null);
        invalidate();
    }

    private void flagWave() {
        for (int j = 0; j <= HEIGHT; j++) {
            for (int i = 0; i <= WIDTH; i++) {
                verts[(j * (WIDTH + 1) + i) * 2 + 0] += 0;
                float offsetY =
                        (float) Math.sin((float) i / WIDTH * 2 * Math.PI +
                                Math.PI * k);
                verts[(j * (WIDTH + 1) + i) * 2 + 1] =
                        orig[(j * WIDTH + i) * 2 + 1] + offsetY * A;
            }
        }
    }
}

使用drawBitmapMesh()方法可以建立很多複雜的影象效果,但是對它的使用也相對複雜,需要我們對影象處理有很深厚的功底。同時,對演算法的要求也比較高,需要計算各種特效下不同的座標點變化規律,從而設計出不同的特效。

原始碼下載:http://download.csdn.net/detail/fancheng614/9922173