图片和视频编辑之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的介绍到此就结束了,关键仍是要多实践、实践、实践!!!

相关文章
相关标签/搜索