Android影象處理之圖形特效處理
上一篇部落格說到了Android影象的色彩處理,使用的是ColorMatrix矩陣;本篇部落格說Android圖形的特效處理,使用的是Matrix這個類。
一、Android變形矩陣——Matricx:
跟Android影象的色彩處理基本一樣,只是將ColorMatrix換成了Matrix,ColorMatrix是4*5的矩陣,Matrix是3*3的。每個畫素點表達了其座標的X、Y資訊:
當使用變換矩陣去處理每一個畫素點的時候,與顏色矩陣的矩陣乘法一樣,計算公式如下所示:
通常情況下,會讓g=h=0,i=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