Android變形矩陣——Matrix
對於圖像的圖形變換,Android系統是通過矩陣來進行處理的,每個像素點都表達了其座標的X、Y信息。Android的圖形變換矩陣是一個3x3的矩陣,如下圖所示:
當使用變換矩陣去處理每一個像素點的時候,與顏色矩陣的矩陣乘法一樣,計算公式如下所示:
X1=aX+bY+c
Y1=dX+eY+f
1=gX+hY+i
通常情況下,會讓
與色彩變換矩陣的初始矩陣一樣,圖形變換矩陣也有一個初始矩陣。就是對角線元素a、e、i爲1,其他元素爲0的矩陣,如下圖所示:
圖像的變形處理通常包含以下四類基本變換:
- Translate——平移變換
- Rotate——旋轉變換
- Scale——縮放變換
- Skew——錯切變換
平移變換
平移變換的座標值變換過程就是將每個像素點都進行平移變換,當從
旋轉變換
旋轉變換即指一個點圍繞一箇中心旋轉到一個新的點。當從
x0=rcosα
y0=rsinα
x1=rcos(α+θ)=rcosαcosθ−rsinαsinθ=x0cosθ−y0sinθ
y1=rsin(α+θ)=rsinαcosθ+rcosαsinθ=y0cosθ+x0sinθ
矩陣形式如下圖所示:
前面是以座標原點爲旋轉中心的旋轉變換,如果以任意點O爲旋轉中心來進行旋轉變換,通常需要以下三個步驟:
- 將座標原點平移到O點
- 使用前面講的以座標原點爲中心的旋轉方法進行旋轉變換
- 將座標原點還原
縮放變換
一個像素點是不存在縮放的概念的,但是由於圖像是由很多個像素點組成的,如果將每個點的座標都進行相同比例的縮放,最終就會形成讓整個圖像縮放的效果,縮放效果的公式如下
x1=K1x0
y1=K2y0
矩陣形式如下圖所示:
錯切變換
錯切變換(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);
運行程序後,初始界面如下所示:
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);
像素塊分析
圖像的特效處理有兩種方式,即使用矩陣來進行圖像變換和使用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個點,而每個點的座標則以
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()方法中改變交叉點的縱座標的值,爲了實現旗幟飄揚的效果,使用一個正弦函數
@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;
}
}
}
這樣,每次在重繪時,通過改變相位來改變偏移量,從而造成一個動態的效果,就好象旗幟在風中飄揚一樣,效果圖如下。
使用drawBitmapMesh()方法可以創建很多複雜的圖像效果,但是對它的使用也相對複雜,需要我們對圖像處理有很深厚的功底。同時,對算法的要求也比較高,需要計算各種特效下不同的座標點變化規律,從而設計出不同的特效。