Canvas系列(八)之Canvas中Matrix的使用

  • 内容
  • 评论
  • 相关

本来Canvas系列就剩一篇了,想尽快结束的,结果由于最近很忙很忙,导致这篇文章距离上一篇文章有一个月之久,现在终于可以抽空把这篇文章写完了。这篇文章写完,Canvas绘图也就告一段落了,好了,废话不多说了,开始正篇。

本来Matrix和canvas是不着边际的,但是既然canvas可以通过matrix的设置来绘图:

public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint)

那么就可以把matrix直接在这里讲解说明了。

对于Matrix,实际上一篇已经接触到了,ColorFilter对图片的操作就是一个图片色值的矩阵操作的过程。接下来所说的Matrix则是对图片的像素的位置即像素坐标的操作。直接表现上来看就是图片的操作,比如缩放、错切、旋转、移动、透视。Matrix是一个3*3的矩阵,即:

qq%e6%88%aa%e5%9b%be20160901172016

通过这个矩阵,我们也能想到代表像素位置坐标的矩阵是这样的:

qq%e6%88%aa%e5%9b%be20160901173233

有人会想到上次的ColorFilter,代表像素不用三个值居然用四个值,同样,这次代表坐标怎么是用三个数代表,而不是简单的两个数,这个疑问我们会在后面慢慢的解答。

两者进行操作的过程是这样的:

qq%e6%88%aa%e5%9b%be20160901173208

这个关系的计算是我们整篇文章的计算基础,所以这个大家一定要弄明白。因为我这个大学里唯一挂掉线性代数的人都明白了,那么大家都能明白了(好讽刺啊,唯一挂的科却是唯一能用到的科)。有人好奇,我们是怎么知道Matrix是3*3的,实际直接看Matrix的源码就知道了。我们先来看一下Matrix类里的字段:

qq%e6%88%aa%e5%9b%be20160901152117

可以看到有9个特殊的字段,这9个字段正好组成了3*3的矩阵,按它给的名字就是如下展示:

qq%e6%88%aa%e5%9b%be20160926183626

可以看到,通过这9个字段的名字我们也能猜到它们分别代表的含义,图中已经由不同颜色标出了。第0和第4个值可以操作缩放值;第1和第3个值可以操作错切值;第0和第1和第3和第4个值可以操作旋转值;第2和第5个值可以操作位移值;第6和第7和第8个值可以操作透视值。接下来我们会对每一个的作用一一进行讲解。

先来展示一下我们今天使用的图片:

img

在进行讲解之前,我们先来看一下代码是如何操作的。同样的在View的onDraw里:

    Matrix matrix = new Matrix();
    matrix.setValues(new float[]{
        1,0,0,
        0,1,0,
        0,0,1
    });
    canvas.drawBitmap(mBitmap, matrix, mPaint);

执行结果如下:

qq%e6%88%aa%e5%9b%be20160926101649

这样就可以用自己定义的Matrix作用到Bitmap上了。好了,讲解开始。用公式表示就是:(x y) 代表原坐标的点,(x',y')代表新坐标的点。

首先是缩放值的操作:

缩放值很好理解,x或y方向按固定缩放值变大变小就可以了。

matrix1

matrix2

如果用矩阵来表示就是:

matrix3

我们分别进行两个测试,X方向测试矩阵如下:

matrix21

效果如下:

qq%e6%88%aa%e5%9b%be20160926101749

可以看到,图像在X方向上进行了缩放,变成了之前的0.5倍。

再进行Y方向的矩阵测试:

matrix22

效果如下:

qq%e6%88%aa%e5%9b%be20160926101854

没有问题,Y方向变成原来的0.5倍。

接着是错切值的操作:

如果懂PS那么错切应该也好理解,x或y方向随另一方向线性拉伸就可以了。

matrix4

matrix6

或者

matrix5

matrix7

如果用矩阵表示就是:

matrix8

matrix9

当然你也可以一起用。

我们分别进行两个测试,X方向测试矩阵如下:

matrix23

效果如下:

qq%e6%88%aa%e5%9b%be20160926105910

可以看到,图像在X方向上随着y的不断变大位移逐渐变大。

再进行Y方向的矩阵测试:

matrix24

效果如下:

qq%e6%88%aa%e5%9b%be20160927093914

同样,图像在Y方向上随着x的不断变大位移逐渐变大。

然后是旋转值的操作:

由于旋转涉及到了角度,那么我们来一张图来演示:

qq%e6%88%aa%e5%9b%be20160927141733

如上图,假定一个点 A(x, y) ,距离原点距离为 r, 与水平轴夹角为 α 度, 绕原点旋转 θ 度, 旋转后为点 B(x', y') 那么A点的坐标计算为:

matrix10

matrix11

旋转后的B点的坐标计算为:

matrix12

matrix13

用矩阵表示就是:

matrix15

我们来进行一个90度旋转的测试,cos90=0,sin90=1,所以矩阵应该写成(90是度,我的小圈没有写!):

matrix27

不过经过实验我们发现没有图显示,为什么呢?因为如果从(0,0)点绘制一个旋转的图,那么肯定已经转到屏幕外面了,所以我们给它加一个偏移试试(位移下面马上讲):

matrix28

结果图是:

qq%e6%88%aa%e5%9b%be20160927094549

可以看到,确实是旋转了90度。

接下来说一下位移值的操作:

说到这里,我们可以回看一下上面的三个操作,缩放、错切、旋转,他们都操作的是3*3矩阵的左上角的四个数据,如果只有这三个操作,那么2*2的矩阵完全可以,不过2*2的矩阵就完全限制了图像只能在它原本的位置上进行操作,如果我们想对它进行一个平面内的移动那么2*2是远远不够的,这就是Matrix是3*3的其中一个理由了。想要进行位移操作,那么公式表示就是:

matrix16

matrix17

如果用矩阵表示就是:

matrix18

我们先来测试一下X方向的,测试矩阵如下:

matrix25

效果就是:

qq%e6%88%aa%e5%9b%be20160927094027

再来测试一下Y方向的,测试矩阵如下:

matrix26

效果就是:

qq%e6%88%aa%e5%9b%be20160927094103

最后,我们来说的是透视操作。

透视操作,也可以叫做是远景操作。这个不是很好理解。由上一个操作-位移操作我们知道,想实现位移操作那么必须从2*2的矩阵变成3*3的矩阵,那么从2*2变成3*3,不仅多出了右边的两个控制位移的数据,而且还多出了第三行的三个数据,这三个数据就是控制透视的。同样的,由于变换矩阵是3*3的,那么代表原图片像素位置的坐标也应该是三个数,即[x,y,1]而非[x,y]。到这里我们解释了为什么是三个数,而不是两个数。说到这里我们将引入一个重要的概念-齐次坐标系。

例如,二维点(x,y)的齐次坐标表示为(hx,hy,h)。由此可以看出,一个向量的齐次表示是不唯一的,齐次坐标的h取不同的值都表示的是同一个点,比如齐次坐标(8,4,2)、(4,2,1)表示的都是二维点(4,2)。给出点的齐次表达式[X Y H],就可求得其二维笛卡尔坐标,即[X Y H]→= [x y 1], 这个过程称为归一化处理。在几何意义上,相当于把发生在三维空间的变换限制在H=1的平面内。

那么引进齐次坐标有什么必要,它有什么优点呢?

许多图形应用涉及到几何变换,主要包括平移、旋转、缩放。以矩阵表达式来计算这些变换时,平移是矩阵相加,旋转和缩放则是矩阵相乘,综合起来可以表示为p' = m1*p+ m2(注:因为习惯的原因,实际使用时一般使用变化矩阵左乘向量)(m1旋转缩放矩阵, m2为平移矩阵, p为原向量 ,p'为变换后的向量)。引入齐次坐标的目的主要是合并矩阵运算中的乘法和加法,表示为p' = p*M的形式。即它提供了用矩阵运算把二维、三维甚至高维空间中的一个点集从一个坐标系变换到另一个坐标系的有效方法。

在欧几里得几何空间里,两条平行线永远都不会相交。但是在投影空间中,如右图中的两条铁轨在地平线处却是会相交的,因为在无限远处它们看起来相交于一点。

index

在欧几里得(或称笛卡尔)空间里描述2D/3D 几何物体是很理想的,但在投影空间里面却并不见得。 我们用表示笛卡尔空间中的一个 2D 点,而处于无限远处的点在笛卡尔空间里是没有意义的。投影空间里的两条平行线会在无限远处相交于一点,但笛卡尔空间里面无法搞定这个问题(因为无限远处的点在笛卡尔空间里是没有意义的),因此数学家想出齐次坐标这个点子来了。

所以说我们有了[X,Y,1]这样的齐次坐标就能够把图片从三维投影到二维显示了。

有了上面的只是铺垫,我们就可以明白透视是什么意思了,说白了就是控制物体与我们之间的距离,从而改变其在我们实现内的投影,这样我们看到的物体大小就有变化了。(想象一下,手里拿一本书,向前向后的移动,是不是看到书的大小在变化。)这个远近的值实际就是[x,y,1],里面的第三个值。而这第三个值直接由Matrix的3*3矩阵的第三行决定,我们先来看一下第三行第三个值对其的影响。直接用矩阵表示就是:

matrix19

我们用一个例子进行测试:

matrix29

执行结果就是:

qq%e6%88%aa%e5%9b%be20160927094704

可以看到,图像缩小了,这是怎么造成的呢?可以想像一下,手机屏幕是三维的,图片离屏幕表面越来越远,就是说Z轴的数值越来越大,这样我们的图片,也就是投影到屏幕上的像,看起来就越来越小。了解到这里我们也知道了为什么有很多人误以为第三行第三列的这个数据是控制缩放的,实际效果上是缩放,但是概念上却是透视远近的改变。

说了第三行的第三个数据,那么再来看一下第一个和第二个数,提前说一下,这两个数我并没有研究。我先举一个最简单的例子看一下:

matrix30

看上图,为了研究第三行第一个数,我把它改成了1,为了不影响第一个数的作用,我把第三个数改成了0.我们来看一下矩阵是如何变化的:

matrix20

可以看到最后的矩阵变成了[1,y/x,1]。这能代表什么意思呢?实际什么都不是,举个例子,现在图像的点的坐标如下:

matrix31

如果执行了刚才的矩阵,那么每个点的新坐标是:

matrix32

实际不用看都知道,X的坐标已经全部变成了1,这意味这什么,意味着所有图像的像素点都会跑到一条线上,那么在展示上这个图片根本就不会显示!!!所以说,我们单纯的改变第三行的第一个第二个数是无法看到任何效果的,这两者,甚至三者之间肯定有关系。不过这个关系我不想研究了。我感觉这个东西应该是和投影几何有关系的:

%e6%8a%95%e5%bd%b1%e5%87%a0%e4%bd%95

感兴趣的可以自己研究。

以上都是对这个矩阵9个数的直接操作,实际呢,我们也可以不这样操作,Android系统已经封装好了这些操作的方法,我们来看看都有哪些方法:

第一部分就是对缩放、错切、旋转、位移的操作的设置,之前讲解了原理,所以参数也很好理解,并且除了位移,其他的都有重载方法,可以通过px,px指定中心点:

    void setTranslate(float dx, float dy)
    void setScale(float sx, float sy, float px, float py)
    void setScale(float sx, float sy)
    void setRotate(float degrees, float px, float py)
    void setRotate(float degrees)
    void setSinCos(float sinValue, float cosValue, float px, float py)
    void setSinCos(float sinValue, float cosValue)
    void setSkew(float kx, float ky, float px, float py)
    void setSkew(float kx, float ky)

并且我们也可以看到里面并没有透视的相关方法,我们也可以猜测,可能官方引入3*3的矩阵就是为了位移,并没有考虑透视,我们使用的透视完全是出于我们自己基于Matrix对图像的操作的理解。

第二部分就是对pre和post的理解,我们知道矩阵之间的乘法是不满足交换率的,即矩阵A*矩阵B!=矩阵B*矩阵A。所以多个矩阵对图像的作用和他们之间的顺序是有关系的。那么上面的方法就衍生出了preXXX和postXXX这样的方法(但是没有SinCos!!!我也不知道为什么!!!),例如preTranslate、postScale等等。其中pre表示在队头插入一个方法(相当于矩阵中的右乘),post表示在队尾插入一个方法(相当于矩阵中的左乘)。我们来演示一下pre和post的区别。

    Matrix matrix = new Matrix();
    matrix.setTranslate(300, 300);
    matrix.preScale(0.5f, 0.5f);
    //matrix.postScale(0.5f, 0.5f);
    canvas.drawBitmap(mBitmap, matrix, mPaint);

使用pre的话效果如下:

matrix33

使用post的话效果如下(代码中pre换成注释的post):

matrix34

可以看到,前乘和后乘的结果不同,为什么呢?实际没有为什么,矩阵的乘法就是这样,看一下相乘的过程就明白了:

matrix35

matrix36

上面的两个图分别是代码里的前乘和后乘的运算过程,可以看到最终的到的矩阵本来就不同,所以展示出来的图像也不同了。

第三部分就是剩余的函数方法了。

boolean isIdentity()

判断是否是单位矩阵,就是说是不是[1,0,0,0,1,0,0,0,1]。

boolean isAffine()

判断是否是仿射矩阵(貌似高版本才有这个方法),官方的说明是:

An affine matrix preserves straight lines and has no perspective.

就是说如果一个矩阵操作图像只是线性变化,那么它即使仿射矩阵,实际就是没有透视的影响,即第三行是不是[0,0,1]。

boolean rectStaysRect()

官方说明:

Returns true if will map a rectangle to another rectangle. This can be true if the matrix is identity, scale-only, or rotates a multiple of 90 degrees.

判断该矩阵是否可以将一个矩形依然变换为一个矩形。当矩阵是单位矩阵,或者只进行平移,缩放,以及旋转90度的倍数的时候,返回true。

void set(Matrix src)

void reset()

上面的俩就不用说了,设置,重置。

boolean setConcat(Matrix a, Matrix b)

将当前matrix的值变为a和b的乘积,同样有pre和post方法:

boolean preConcat(Matrix other)

boolean postConcat(Matrix other)

以上三个方法我都没有测试。

boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf)

这是一个比较有意思的方法,它将图像以能够使原rect变为目标rect这样的矩阵变换。什么意思呢?比如src的矩形是(0,0,10,10),目标矩形是(0,0,20,20),那么从原矩形到目标矩形需要什么矩阵呢?需要一个XY都放大2倍的矩阵,即{2,0,0,0,2,0,0,0,1},就是说如果setRectToRect的前两个参数是new Rect(0,0,10,10)和new Rect(0,0,20,20),那么图像将会执行{2,0,0,0,2,0,0,0,1}矩阵,使得图片XY方向都变大2倍。同时可以看到这个方法接受一个ScaleToFit参数指定变化的方式。可以在Matrix源码中找到这个ScaleToFit枚举类。其有四个值FILL、START、CENTER、END。含义分别如下:

FILL: 可能会变换矩形的长宽比,保证变换和目标矩阵长宽一致,即充满目标矩形。

START:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。左上对齐。

CENTER: 保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。居中对齐。

END:保持坐标变换前矩形的长宽比,并最大限度的填充变换后的矩形。至少有一边和目标矩形重叠。右下对齐。

盗来一张图来说明(张图也是官方demo的,所以算不算盗呢,呵呵。)

matrix37

boolean setPolyToPoly(float[] src, int srcIndex,float[] dst, int dstIndex,int pointCount)

这个方法是一个很牛的方法啊,作用非常多。不过通过这个名字,我还真看不出到底是啥意思!Poly是聚的意思,是说把哪些点聚集?不懂。

Set the matrix such that the specified src points would map to the specified dst points. The "points" are represented as an array of floats,order [x0, y0, x1, y1, ...], where each "point" is 2 float values.

首先目的还是获得矩阵,和前两个一样。这个矩阵的作用是将指定的点绘制到目标点。提供指定变换前(src)和变换后(dst)的坐标对,Matrix自动帮你计算出实现这些坐标变换对于的Matrix。每个坐标的格式为[x0,y0,x1,y1 ...]两个float值代表一个点,并且这些点的坐标数组的长度必须是2的倍数。如果指定0个点则没有效果。1 个点 (偏移变换) 2个点(旋转/缩放) ,3个点(旋转/剪切),4个点(透视变换)。接下来我们分别来演示指定1-4个点的作用。

一个点时:(以下例子只展示setPolyToPoly的参数,并且width代表图片的宽度,height代表图片的高度。下同)

(new float[]{0,0}, 0, new float[]{100,100}, 0, 1)

效果如下:

matrix38

执行的作用就是将图片的(0,0)点移动到(100,100)点,所以图片整个移动了(100,100)。为什么呢?可以想象一下,你打开一个有图片查看的App应用,用一个手指按住图片的左上角,然后移动(100,100),是不是整个图片都移动了。因为一个手指能进行的操作就只能是移动了。同理,如果我们写(50,20)到(150,120)也是一样的效果。

两个点时:

(new float[]{width/2,height/2,0,0}, 0,new float[]{width/2,height/2,0,height}, 0, 2)

效果如下:

matrix39

这次我们操作了两个点,第一个是图片的中心点,可以对比前后的数值,发现图片中心点没有变化,第二个点是图片的左上角,变化后成了图片的左下角,就有了上图的效果。可以想像一下,你一个手指按住图片中心不动,另一个手指把图片的左上角滑到了左下角,图片是不是就转成了上图的样子。

同理,我们知道,两个手指的操作不仅仅是图片的旋转,还能进行图片的缩放。

(new float[]{width/2,height/2,0,0}, 0,new float[]{height/2,width/4,height/4}, 0, 2)

效果如下:

matrix40

还是中心不动,图片左上角向中心靠拢,移动到图片宽高的1/4处,可以想象一下你的两个手指按住图片减小两个手指的距离,是不是图片变小了了。

接下来操作三个点:

(new float[]{0,0,width,0,0,height}, 0,new float[]{0,0,width,100,100,height/4*5}, 0, 3)

效果如下:

matrix41

三个点的操作就不能用手指类比了,因为大部分程序都没提供这个功能,或者说对人而言,三个手指操作图片太诡异了。。。上图的效果就是左上角没有变化,右上角和左下角都做了相应的移动,导致了图片有了错切效果。

最后是四个点的操作:

(new float[]{0,0,width,0,width,height,0,mBitmap.getHeight()}, 0,new float[]{100,0,width-

100,0,width,height+100,0,height+100}, 0, 4)

效果如下:

matrix42

有了上面三个的讲解,这个也好理解了,四个点都变化了,就出现了上面的效果。说是透视效果,实际就那么回事,看自己怎么理解了。在这个方法介绍一开始我们说1个点是偏移变换,2个点是旋转/缩放,3个点是旋转/错切,4个点是透视变换。实际我们可以发现,点的数量越多,它的效果越多,而且多个点的操作的效果总是能包含少个点的效果。所以说上面的总结是一个片面的总结。我们能理解它的操作原理就好了。如果还是不是很明白,建议你去看看PhotoShop中的图片的自由变换,你一下子就会明白这个方法函数的设计目的了。同时我们也会发现,Matrix中这么多方法,setPolyToPoly是一个非常直观的对图片的操作,其他方法不太容易实现的效果,这个方法能够轻松达到。

boolean invert(Matrix inverse)

反转当前矩阵,如果能反转就返回true并将反转后的值写入inverse,否则返回false。我们需要了解的是:当前矩阵*反转矩阵=单位矩阵。就是说如果我们对图片执行了矩阵A操作,显示了一个新的图片,这时候再对这个新图片进行反转矩阵A,那么就会得到原来的图片。即反转矩阵是对原矩阵的一个逆效果。例如

matrix.setScale(2,2);

matrix.invert(matrix);

这时候matrix的效果就不是XY各放大2倍了,而是正好相反,XY各缩小到原来的0.5倍。剩下的方法就是mapXXX了,这些方法实际搞了很久才明白。它们是对已有矩阵的实际测量值计算。怎么理解呢?比如说有这么一个规则y=2x,这个公式的意思就是y是x的2倍,例如当x=5时y=10。同理mapXXX的含义就是,已经有了Matrix(即y=2x的规则,Matrix是图片变换的规则),那么如果我给你一组数据(即x=2的数据,Matrix是给定坐标的数据),给我计算出变换后的值(即y=10的结果,Matrix是计算坐标变换后的值)。

    void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex,int pointCount)
    void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex,int vectorCount)
    void mapPoints(float[] dst, float[] src)
    void mapVectors(float[] dst, float[] src)
    void mapPoints(float[] pts)
    void mapVectors(float[] vecs)
    boolean mapRect(RectF dst, RectF src)
    boolean mapRect(RectF rect)
    float mapRadius(float radius)

对于上面的这一堆方法,我不想一一说,我就验证一个最简单的:

    Matrix matrix=new Matrix();
    matrix.setScale(2,2);
    float[] dst=new float[2];
    matrix.mapPoints(dst,new float[]{100,100});
    Log.e("velsharoon",dst[0]+" "+dst[1]);

输出结果就是200,200。就是说这个Matrix是将图片XY扩大到2倍。所以如果我给它一组(100,100)的值,那么他将告诉我(100,100)这样的坐标变换后会变成(200,200)。对于最后一个方法我要单独说一下float mapRadius(float radius):将一个半径为radius的圆的所有点坐标用matrix进行变换后,计算出该圆的半径并且返回该值。要得到正确的值的前提是该圆默认是有中心的。

好了,至此,Canvas篇终于结束了!!!

最后推荐一个在线公式编辑器:http://latex.codecogs.com/eqneditor/editor.php

评论

1条评论
  1. Gravatar 头像

    游客 回复

    很好,很强,很有用,解释得很清楚

游客进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注