圖片和視頻編輯之Matrix大法好

最近在作圖片和視頻編輯時,大量使用了Matrix,這裏記錄下相關知識點,但願能夠起到拋磚引玉的做用。java

概述

Matrix的使用範圍很是普遍,咱們平時使用的Tween Animation,其在進行位移、縮放、旋轉時,都是經過Matrix來實現的。除此以外,在進行圖像變換操做時,Matrix也是最佳選擇。數組

Matrix是一個3*3的矩陣,以下所示:post

Matrix =
\begin{bmatrix} 
MSCALE_X & MSKEW_X & MTRANS_X \\ 
MSKEW_Y & MSCALE_Y & MTRANS_Y \\ 
MPERSP_0 & MPERSP_1 & MPERSP_2 \\ 
\end{bmatrix}

咱們能夠直接經過Matrix.getValues方法獲取Matrix的矩陣值(浮點型數組類型),而後修改矩陣值(Matrix類爲每個矩陣值提供了固定索引,如:MSCALE_X、MSKEW_X等),最後經過Matrix.setValues方法從新設置Matrix值,以達到修改Matrix的目的。這種方式要求咱們對Matrix每個值的做用都要十分了解,操做起來比較繁瑣,但倒是最靈活、最完全的操做方式。spa

具體要修改哪些Matrix值,則取決於要實現什麼效果,從本質上這是一個數學問題,這裏給出幾種比較常見的方案:.net

  1. 實現Translate操做 位移操做在Matrix中對應是MTRANS_XMTRANS_Y值,分別表示X和Y軸上的位移量,假設在X和Y軸上分別位移100px,那麼對應的Matrix就是
\begin{bmatrix} 
1 & 0 & 100 \\ 
0 & 1 & 100 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}
  1. 實現Scale操做 縮放操做在Matrix中對應的是MSCALE_XMSCALE_Y值,分別表示X和Y軸上的縮放比例,假設在X和Y軸上分別放大2倍,那麼對應的Matrix就是
\begin{bmatrix} 
2 & 0 & 0 \\ 
0 & 2 & 0 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}
  1. 實現Rotate操做 旋轉操做在Matrix中對應是MSCALE_XMSCALE_YMSKEW_XMSKEW_Y值,假設咱們要以座標原點爲中心,旋轉A度(順時針),那麼對應的Matrix就是
\begin{bmatrix} 
\cos(A) & -\sin(A) & 0 \\ 
\sin(A) & \cos(A) & 0 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}
  1. 實現Skew操做 錯切操做在Matrix中對應的是MSKEW_XMSKEW_Y,分別表示X和Y軸上的錯切係數,假設在X軸上錯切係數爲0.5,Y軸上爲2,那麼對應的Matrix就是
\begin{bmatrix} 
1 & 0.5 & 0 \\ 
2 & 1 & 0 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

其餘3種操做都比較常見,可是錯切操做咱們可能不是很熟悉。設計

錯切可分爲水平錯切和垂直錯切。 水平錯切表示變換後,Y座標不變,X座標則按比例發平生移,且平移的大小和Y座標成正比,即新的座標爲(X+Matrix[MSKEW_X] * Y,Y)。 垂直錯切表示變換後,X座標不變,Y座標則按比例發平生移,且平移的大小和X座標成正比,即新的座標爲(X,Y+Matrix[MSKEW_Y] * X)。 固然,咱們也能夠同時實現水平錯切和垂直錯切。code

關於爲何修改Matrix的這些值後,就實現了位移、縮放、旋轉和錯切操做,就主要是數學推導過程了,能夠參考這篇文章—— Android中圖像變換Matrix的原理,講解的很是詳細,強烈推薦。cdn

實踐

除了能夠直接修改Matrix值,Matrix類還提供了一些API來操做Matrix。這裏主要介紹幾類比較經常使用的API。視頻

setXXXpreXXXpostXXX

XXX能夠是Translate、Rotate、Scale、Skew和Concat(表示直接操做Matrix矩陣)。咱們主要搞清楚這3種API的區別就OK了。blog

  1. setXXX,首先會將該Matrix設置爲單位矩陣,即至關於調用reset()方法,而後再設置該Matrix的值。
  2. preXXX,不會重置Matrix,而是被當前Matrix左乘(矩陣運算中,A左乘B等於A * B),即M' = M * S(XXX)。
  3. postXXX,不會重置Matrix,而是被當前Matrix右乘(矩陣運算中,A右乘B等於B * A),即M' = S(XXX) * M。

當這些API同時使用時,又會出現什麼效果那,咱們來看個例子:

Matrix matrix = new Matrix();
float[] points = new float[] { 10.0f, 10.0f };
matrix.postScale(2.0f, 3.0f);// 第1步
matrix.preRotate(90);// 第2步
matrix.setScale(2f, 3f);// 第3步
matrix.preTranslate(8.0f, 7.0f);// 第5步
matrix.postTranslate(18.0f, 17.0f);// 第4步
matrix.mapPoints(points);
Log.i("test", points[0] + " : " + points[1]);
複製代碼

最後獲得的結果是:54.0 : 68.0 能夠發現,在第3步setScale以前的第一、2步根本就沒有用,直接被第3步setScale覆蓋了。因此最終的矩陣運算爲

Translate(18,17) * Scale(2,3) * Translate(8,7) * (10,10)

這樣,就很容易得出最後的結果了。

這裏也許會有一個疑問,爲何座標點(10,10)會被結果矩陣(矩陣運算雖然不知足交換律,可是知足結合律)左乘,而不是右乘。這一點咱們看一下矩陣運算就會明白。

\begin{bmatrix} x \\ y \\ 1 \\ \end{bmatrix}  
 = 
\begin{bmatrix} 
1 & 0 & Translate_X \\ 
0 & 1 & Translate_Y \\ 
0 & 0 & 1 \\ 
\end{bmatrix}  
 *  
\begin{bmatrix} x_0 \\ y_0 \\ 1  \\ \end{bmatrix}

等號左邊是變換後的座標點,等號右邊是Matrix矩陣左乘原始座標點。由於Matrix是3行3列,座標點是3行1列,因此正好能夠相乘,但若是反過來,就不知足矩陣相乘的條件了(左邊矩陣的列數等於右邊矩陣的行數)。因此,就能夠理解爲何是結果矩陣左乘原始座標點了。

也正由於這一點以及矩陣的結合律,因此咱們能夠理解上面矩陣運算的流程:

先對原始座標點(10,10)進行Translate(8,7)位移,而後再對中間座標點(18,17)進行Scale(2,3)放大,最後再次對中間座標點(36,51)進行Translate(18,17)操做,就獲得了最後的座標點(54,68)。

這裏還有一個小Tips: 當須要對Matrix矩陣進行比較複雜的設置時,能夠把這些複雜的設置,拆分爲多個步驟,每個步驟都是一個簡單的Matrix,而後再依據這些步驟的前後順序,決定是經過左乘 or 右乘獲得結果矩陣,最後經過結果矩陣左乘原始座標就OK了(設計時,能夠拆分以後理解,但最終運算時仍是要獲得一個結果矩陣,再去操做原始座標)。

還有一點須要瞭解:Canvas裏的scale、translate、rotate和concat都是preXXX方法,若是要進行更多的變換能夠先從Canvas得到Matrix, 變換後再設置回Canvas.

mapPoints mapRect mapVectors

這些API很簡單,主要是根據當前Matrix矩陣對點、矩形區域和向量進行變換,以獲得變換後的點、矩形區域和向量。常常和下面的invert方法結合使用。

invert

經過上面的mapXXX方法,能夠獲取變換後的座標或者矩形。但假設咱們知道了變換後的座標,如何計算Matrix變換前的座標那?! 此時經過invert方法獲取的逆矩陣就派上用場了。所謂逆矩陣,就是Matrix旋轉了30度,逆Matrix就反向旋轉30度,Matrix放大n倍,逆Matrix就縮小n倍。 假設逆矩陣是invertMatrix,那麼Matrix.preConcat(invertMatrix) 和 Matrix.postConcat(invertMatrix) 都應該等於單位矩陣(但實際上會有一些偏差)。 因此,經過Matrix和invertMatrix對座標進行變換的規則可總結以下:

InvertMatrix

逆矩陣在進行自定義View Touch事件處理時頗有用,假設咱們在自定義View中,經過Matrix(包含了旋轉、縮放和位移操做)繪製了Bitmap,如今想要判斷Touch事件是否在變換後的Bitmap範圍內,應該如何操做那?! 首先想到的多是下面的方案:

RectF rect = new RectF(bitmap.getWidth(),bitmap.getHeight());
//假設matrix就是對bitmap進行變換的矩陣
matrix.mapRect(rect);
boolean isTouchBitmap = rect.contains(touchX,touchY);
複製代碼

可是這種方式實際上不是很是的準確,經過matrix變換後的矩形區域並非真實的Bitmap區域,而是包含bitmap的矩形區域(很難描述啊),看下圖就知道了:

Matrix正向操做
圖中的綠色矩形區域就是咱們進行判斷的rect區域,很明顯偏差很大哈。既然正向操做不可行,那就只能試下逆向操做了:

RectF rect = new RectF(bitmap.getWidth(),bitmap.getHeight());
float eventFloat[] = new float[]{touchX,touchY};
//假設invertMatrix是matrix的逆矩陣,這裏對Touch座標進行逆向操做。
invertMatrix.mapPoints(eventFloat);
boolean isTouchBitmap = rect.contains(eventFloat[0],eventFloat[1]);
複製代碼

經過這種方式,首先會對Touch座標進行逆矩陣操做,而後再判斷是否落在原始bitmap矩形區域內(上圖中的小企鵝),就比較精確了。精妙哈!!!

典型問題

此次在實現以雙指中心爲中心點進行縮放時,遇到一個問題:由於用戶每次的雙指中心都是不一樣的,可是最後Bitmap上屏時,只能有一個Matrix,那最終怎麼處理縮放的中心點那?

這個問題能夠簡化成下面的模型:定義一個矩形區域:

val originRectF = RectF(0f, 0f, 4f, 4f)
複製代碼

依次實現下面的Scale變換,獲得最終的矩形區域。

先以(2,1)爲中心點,放大2倍,再以(2,3)爲中心點,放大2倍

實際上有兩種方式,均可以實現上述的變換:指定中心點的縮放不指定中心點的縮放

指定中心點的縮放

首先,以(2,1)爲中心點,放大2倍,即:

FirstScaleMatrix.setScale(2f, 2f, 2f, 1f)
複製代碼

獲得的Matrix以下所示:

FirstScaleMatrix =
\begin{bmatrix} 
2 & 0 & -2 \\ 
0 & 2 & -1 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

而後,以(2,3)爲中心點,放大2倍,即:

SecondScaleMatrix.setScale(2f, 2f, 2f, 3f)
複製代碼

獲得的Matrix以下所示:

SecondScaleMatrix =
\begin{bmatrix} 
2 & 0 & -2 \\ 
0 & 2 & -3 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

最後,獲得的效果就是:先以(2,1)爲中心點,放大2倍,再以(2,3)爲中心點,放大2倍,即:

ResultMatrix = SecondScaleMatrix * FirstScaleMatrix
複製代碼

獲得的Matrix以下所示:

ResultMatrix =
\begin{bmatrix} 
4 & 0 & -6 \\ 
0 & 4 & -5 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

最後,經過ResultMatrix矩陣實現對上述矩形區域的變換:

ResultMatrix.mapRect(tempRectf, originRectF)
複製代碼

獲得最後的矩形區域:

tempRectf = RectF(-6, -5, 10, 11)
複製代碼

不指定中心點的縮放

經過不帶中心點的縮放 + 位移來模擬指定中心點的縮放

具體的映射公式以下所示:

TranslateX = TranslateX * ScaleX + PivotX * (1 - ScaleX)
TranslateY = TranslateY * ScaleY + PivotY * (1 - ScaleY)
複製代碼

仍是針對上面的變換:先以(2,1)爲中心點,放大2倍,再以(2,3)爲中心點,放大2倍。按照不指定中心點的縮放,以下所示:

var translateX = 0f
var translateY = 0f

// 1. 先以(2,1)爲中心點,放大2倍
translateX = translateX * 2f + 2 * (1f - 2f)
translateY = translateY * 2f + 1 * (1f - 2f)
// 2. 再以(2,3)爲中心點,放大2倍
translateX = translateX * 2f + 2 * (1f - 2f)
translateY = translateY * 2f + 3 * (1f - 2f)

// 3. 獲得最後的Matrix
val resultMatrix = Matrix()
resultMatrix.setScale(4f, 4f)
resultMatrix.postTranslate(translateX, translateY)
resultMatrix.mapRect(tempRectf, originRectF)
複製代碼

其中,resultMatrix以下所示:

ResultMatrix =
\begin{bmatrix} 
4 & 0 & -6 \\ 
0 & 4 & -5 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

獲得最後的矩形區域:

tempRectf = RectF(-6, -5, 10, 11)
複製代碼

可見,經過上述指定中心點的縮放和不指定中心點的縮放+位移,最後的Matrix都是相同的。

錯誤樣例

由於在Canvas中Draw Bitmap時,是不考慮過程的,只考慮結果:最終生成的Matrix。因此上述先以(2,1)爲中心點,放大2倍,再以(2,3)爲中心點,放大2倍,其中是有(2,1)(2,3)兩個中心點的。若是咱們單純以最後一箇中心點縮放累計的倍數,是不行的。

仍是以上述的縮放過程爲例:

val resultMatrix = Matrix()
// 累積的倍數是4f,最後的中心點是(2,3)
resultMatrix.setScale(4f, 4f, 2f, 3f)
resultMatrix.mapRect(tempRectf, originRectF)
複製代碼

其中,resultMatrix以下所示:

ResultMatrix =
\begin{bmatrix} 
4 & 0 & -6 \\ 
0 & 4 & -9 \\ 
0 & 0 & 1 \\ 
\end{bmatrix}

獲得最後的矩形區域:

tempRectf = RectF(-6, -9, 10, 7)
複製代碼

可見,經過這種方式計算出的ResultMatrix和以前計算出來的是不一樣的,在界面上的現象就是Bitmap會跳動。

小結

總之,就是經過不帶中心點的縮放 + 位移,能夠實現指定中心點的縮放。 例如:對下圖Bitmap,以它的中心點(width/2,height/2)爲縮放中心,對X軸放大必定的倍數。能夠經過如下兩種方式實現:

圖片編輯問題

總結

關於Matrix的介紹到此就結束了,關鍵仍是要多實踐、實踐、實踐!!!

相關文章
相關標籤/搜索