1. 程式人生 > >淺談矩陣變換——Matrix

淺談矩陣變換——Matrix

hit res ngs com 之間 map wid can 位置

矩陣變換在圖形學上經常用到。基本的常用矩陣變換操作包括平移、縮放、旋轉、斜切。

技術分享

每種變換都對應一個變換矩陣,通過矩陣乘法,可以把多個變換矩陣相乘得到復合變換矩陣。

技術分享

矩陣乘法不支持交換律,因此不同的變換順序得到的變換矩陣也是不相同的。

事實上,圖像處理時,矩陣的運算是從右邊往左邊方向進行運算的。這就形成了越在右邊(右乘)的矩陣,越先運算(先乘),反之亦然。所以,右乘就是先乘,左乘就是後乘。

復合變換矩陣T = 變換矩陣T1 x 變換矩陣T2 x 變換矩陣T3。

圖形是由一個個點組成的,得到變換矩陣T後,左乘以變換前的圖形像素矩陣M,即可達到變換後像素矩陣M’,即M‘ = T x M。

在Android中,用Matrix這個類代表矩陣。Matrix是一個3x3的矩陣,

技術分享

Matrix提供了基本的變換,translate、scale、rotate、skew,針對每種變換,Android提供了set、pre和post三種操作方式。

  • set用於設置單位矩陣中的值。我們通過new Matrix()得到的是一個單位矩陣,後續的矩陣變換都是針對這個單位矩陣進行變換。如Matrix.setRotate(90)、Matrix.setTranslate(10,20)等。
  • pre指先乘,相當於矩陣運算中的右乘。如Matrix.setRotate(90),表示M‘ = M * R(90)。
  • post指後乘,相當於矩陣運算中的左乘,如Matrix.setRotate(90),表示M‘ = R(90) * M。
矩陣乘法不支持交換律,所以區分先乘和後乘是非常有必要的!在實際開發中中,通常先new Matrix()獲取一個單位矩陣,再通過set操作設置初始矩陣,那麽後續的變換到底是pre(先乘)還是post(後乘)運算,都是相對這個矩陣而言的(pre在初始矩陣的右邊,post在初始矩陣的左邊)。最後得到的復合變換矩陣再左乘以原圖矩陣。 [java] view plain copy
  1. matrix.setRotate(θ);
  2. matrix.preTranslate(-10, -10); // 先乘
  3. matrix.postTranslate(10, 10); // 後乘
如上面的矩陣變換,實際中的運算如下,M‘ = T(10,10) x R(θ) x T(-10,-10) x M。 技術分享

點(x0,y0)經過矩陣變換後得到(x,y),如果對圖形中的所有點應用該變換矩陣,則產生的效果就是整個圖都變換了。那麽如何理解上面的變換呢?它是先平移(10,10)還是先平移(-10,-10)?

首先我們得明白上面變換的效果是什麽——讓圖形圍繞點(10,10)順時針旋轉角度θ。

按照我們上面說的,實際運算時,是從右邊往左開始運算,那麽這時的變換順序是,T(-10,-10)->R(θ)->T(10,10),

技術分享

把所有的頂點(坐標)位置平移(-10,-10),也就是分別沿x軸y軸的負方向平移10個單位,然後沿著原點(0,0)把頂點旋轉角度θ,最後再把頂點的位置平移(10,10).

可見這裏變換的是坐標(也就是頂點)位置,坐標系不變。

Android自定義view時,往往在onDraw(canvas)方法裏實現繪圖,canvas表示畫布,我們可以在代碼裏對畫布進行矩陣變換,如下面的代碼,

[java] view plain copy
  1. canvas.translate(10, 10);
  2. canvas.rotate(θ);
  3. canvas.translate(-10, -10);

效果也是讓畫布圍繞點(10,10)旋轉θ度。我們在看看Canvas中translate()方法的註釋。

[java] view plain copy
  1. /**
  2. * Preconcat the current matrix with the specified translation
  3. *
  4. * @param dx The distance to translate in X
  5. * @param dy The distance to translate in Y
  6. */
  7. public void translate(float dx, float dy);

可見canvas.translate()方法實現的操作是先乘(preconcat),等同於Matrix.preTranslate()。其實canvas中的矩陣變換方法rotate()、scale()、skew(),也是先乘操作。按照先乘的定義,先乘操作在初始矩陣的右邊,那麽多個先乘操作時,後面的先乘在前面的先乘右邊。那麽這時候你會發現,實際的運算式子剛剛好跟代碼中的順序一樣,即M‘ = T(10,10) x R(θ) x T(-10,-10) x M,M表示初始矩陣。然後問題又來了,按照前面說的變換順序,T(-10,-10)->R(θ)->T(10,10),又是跟代碼相反的!難道我們要把代碼反過來理解嗎?

其實這裏有兩種方式,第一種,把運算式子寫出來如M‘ = T(10,10) x R(θ) x T(-10,-10) x M,然後在按照從右邊到左邊的順序(T(-10,-10)->R(θ)->T(10,10))去理解,改變的是坐標位置,坐標系不變。第二種,索性就從左邊開始理解,這樣既跟代碼的順序一致,也符合我們平時的閱讀習慣,從左往右。

如果采用第二種方式去理解矩陣變換,就得改變變換的空間想象,這個時候改變得是坐標系,不變的是坐標位置,即坐標位置相對於它所在的坐標系裏一直是不變的。如下是采用變換坐標系的空間想象去理解一開始的圖形矩陣變換(灰色的是初始的坐標系)。

技術分享

坐標系先平移了(10,10),然後把平移後的坐標系繞它的原點(0,0)旋轉角度θ,再把變換後的坐標系沿著它的坐標軸方向平移(-10,10),最後在最終得到的坐標系裏面繪出圖形,這個過程中圖形相對於它的坐標系的坐標位置一直保持不變。

可見最後實現的效果是一樣的!對於一組矩陣變換操作,可以分別使用變換坐標位置和變換坐標系的空間想象去理解,沒有哪個更優之說,無論采取哪種變換思想,首先第一步都是得明確實際的變換運算式子,然後再決定采取從左往右的變換坐標系的空間想象,還是采取從右往左的變換坐標位置的空間想象。

在這裏,個人推薦使用變換坐標系的空間想象,因為這樣可以做到通用,canvas和openGL裏面的圖形運算的矩陣操作都是先乘的,這樣我們就可以按照代碼的順序去理解變換。像前面的Matrix的代碼,我們可以讓代碼跟采用變換坐標系的空間想象的理解順序一樣。

[java] view plain copy
  1. matrix.preTranslate(10, 10);
  2. matrix.preRotate(θ);
  3. matrix.preTranslate(-10, -10);

其實無論代碼怎麽寫,只要運算式子是一樣的即M‘ = T(10,10) x R(θ) x T(-10,-10) x M,實現的效果其實都是一樣的!(上面的代碼沒有調用set方法,所以變換操作都是針對單位矩陣的,任何矩陣無論是左乘還是右乘以單位矩陣,都等於該矩陣,相當於數字乘法中的1的效果,所以這裏表示運算順序的式子中把單位矩陣忽略掉了。)

所以代碼還可以這樣寫,剛好跟先乘的代碼相反.

[java] view plain copy
  1. matrix.postTranslate(-10, -10);
  2. matrix.postRotate(θ);
  3. matrix.postTranslate(10, 10);

所以重要的是知道運算式子,下面給出一個例子。

[java] view plain copy
  1. matrix.preScale(0.5f, 1);
  2. matrix.preTranslate(10, 0);
  3. matrix.postScale(0.8f, 1);
  4. matrix.postTranslate(15, 0);

那麽上面變換的實際運算式子是什麽呢?先嘗試自己寫出來,再看下面的答案。(註意:後調用的pre操作更靠右,而後調用的post操作更靠左)

運算式子為:M = T(15,0) x S(0.8f,1) x S(0.5f,1) x T(10,0)

再寫一段代碼,在畫布上畫出一段文字,對其做一些旋轉平移操作。

[java] view plain copy
  1. canvas.translate(100, 200);
  2. canvas.rotate(90, 0, 0);
  3. canvas.drawText("hello,world", 0, 0, mPaint);

試著畫出最終的效果。

技術分享

說了那麽多矩陣變換的例子,似乎還沒涉及到縮放變換,好,現在就給一個。

技術分享

上面是原圖,分別說出下面兩段代碼的變換效果。

[java] view plain copy
  1. matrix.preScale(2,2);
  2. matrix.preTranslate(0,bitmapHeight);

[java] view plain copy
  1. matrix.preTranslate(0, bitmapHeight * 2);
  2. matrix.preScale(2, 2);


其實上面的兩個變換效果都是一樣的!效果如下。板面的做法和配料

技術分享

按照變換坐標系的空間想象,第一段代碼,首先把坐標系放大兩倍,然後把放大後的坐標系向下平移了一個圖片高度(由於坐標系放大了,這個時候的高度實際是初始圖片高度的兩倍!)。第二段代碼,首先將坐標系向下平移了兩個圖片高度,然後再把坐標系放大兩倍。仔細想想,雖然它們的運算式子不一樣,但它們的變換效果卻是一樣的!

最後,再說一個有趣的地方。其實View的onDraw(canvas)方法裏的canvas(畫布),在最初從根布局傳下來時的原點就在屏幕的左上角,但傳到當前view時,已經經過過裁剪(clip)和平移。裁剪的作用就是為了防止畫出的內容超出的view的範圍,而平移則是通過canvas.translate()實現,讓畫布的坐標系平移到當前view的原點,接下來在畫布上的操作都是相對於這個原點的。所以就可以明白為什麽當我們在view中繪圖時,如果繪制的坐標是(0,0),圖形出現在view的左上角,而不是屏幕的左上角。

Canvas還有兩個常用的方法,save()和restore()。

[java] view plain copy
  1. /**
  2. * Saves the current matrix and clip onto a private stack.
  3. * <p>
  4. * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
  5. * clipPath will all operate as usual, but when the balancing call to
  6. * restore() is made, those calls will be forgotten, and the settings that
  7. * existed before the save() will be reinstated.
  8. *
  9. * @return The value to pass to restoreToCount() to balance this save()
  10. */
  11. public int save()

[java] view plain copy
  1. /**
  2. * This call balances a previous call to save(), and is used to remove all
  3. * modifications to the matrix/clip state since the last save call. It is
  4. * an error to call restore() more times than save() was called.
  5. */
  6. public void restore()


save()方法就是保存當前的矩陣/裁剪狀態。restore()就是把當前的矩陣/裁剪狀態恢復到save()方法保存起來的那個狀態下。也就是說 在save()和restore()方法之間做的矩陣變換或裁剪操作,在調用restore()方法後都不生效,畫布恢復到save()方法之前的狀態。

[java] view plain copy
  1. canvas.save(); // 保存狀態(入棧)
  2. canvas.translate(50, 0);
  3. canvas.scale(2f, 2f);
  4. mPaint.setColor(Color.BLUE); // 繪制藍色方塊
  5. canvas.drawRect(0, 0, 50, 50, mPaint);
  6. canvas.restore(); // 恢復狀態(出棧)
  7. mPaint.setColor(Color.GREEN); // 繪制綠色方塊
  8. canvas.drawRect(0, 0, 50, 50, mPaint);


上面的代碼效果如下。

技術分享

可見在save()和restore()方法之間的變換操作並沒有影響到綠色方塊的繪制,它還是相對於save()之前的畫布繪制自己。

好的,矩陣變換就這麽多了!上面的所述並沒有多少需要自己計算的地方,主要是靠理解矩陣在空間中如何變換的,空間形象力很重要。理解了之後,要實現一個圖形的變換效果,那就容易多了!加油吧。

淺談矩陣變換——Matrix