Android Matrix

2017/2/10 posted in  Android

Matrix的数学原理

在Android中,如果你用Matrix进行过图像处理,那么一定知道Matrix这个类。Android中的Matrix是一个3 x 3的矩阵,其内容如下:

1.png
1.png

Matrix的对图像的处理可分为四类基本变换:

  • Translate ———— 平移变换
  • Rotate ———— 旋转变换
  • Scale ———— 缩放变换
  • Skew ———— 错切变换

从字面上理解,矩阵中的MSCALE用于处理缩放变换,MSKEW用于处理错切变换,MTRANS用于处理平移变换,MPERSP用于处理透视变换。实际中当然不能完全按照字面上的说法去理解Matrix。同时,在Android的文档中,未见到用Matrix进行透视变换的相关说明,所以本文也不讨论这方面的问题。这里需要把矩阵根据他们的作用划分为4块:

20170923150616374765871.png
20170923150616374765871.png

如上图所示,这四块区域各有作用。后面会详细讲解各个作用,先来看看这个矩阵是如何影响图像的。先看看屏幕的坐标系:

20170923150616379323870.png
20170923150616379323870.png

看上图,这里表示了屏幕的坐标系,其中的x,y轴是大家所熟知的,但是其实,一个物体他是存在于一个三维空间的,所以必然会有z轴。我们的屏幕,就像是一个窗口,透过它,我们看到了屏幕后面的世界,那里面有各种物体,我们看到的是映射在x,y平面上的一个投射图像。屏幕就像是一个镜头一样,将里面的物体映射到x,y平面上,成为一个二维的图像。那么如果,我们把屏幕这个镜头沿着z轴,拉远或者拉进,那么图像会有什么变化呢,肯定会变小或者变大。就好比坐在飞机上透过窗口看地面的汽车,和在地面上看到的大小是不同的。

20170923150616386136695.png
20170923150616386136695.png

x,y分别代表x,y轴上的坐标,而1代表屏幕在z轴上的坐标为默认的。如果将1变大,那么屏幕会拉远, 图形会变小。

针对每种变换,Android提供了pre、set和post三种操作方式。其中

  • set用于设置Matrix中的值。
  • pre是先乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。先乘相当于矩阵运算中的右乘。
  • post是后乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。后乘相当于矩阵运算中的左乘。

除平移变换(Translate)外,旋转变换(Rotate)、缩放变换(Scale)和错切变换(Skew)都可以围绕一个中心点来进行,如果不指定,在默认情况下是围绕(0, 0)来进行相应的变换的。

下面我们来看看四种变换的具体情形。由于所有的图形都是有点组成,因此我们只需要考察一个点相关变换即可。

平移变换

假定有一个点的坐标是 P(\(x_{0}\),\(y_{0}\)) ,将其移动到 P(x,y) ,再假定在x轴和y轴方向移动的大小分别为: \(\Delta\)x = x - \(x_{0}\) and \(\Delta\)y = y - \(y_{0}\)

如下图所示:

20170301148833900863943.png
20170301148833900863943.png

不难知道:

x = \(x_{0}\) + \(\Delta\)x

y = \(y_{0}\) + \(\Delta\)y

如果用矩阵来表示的话,就可以写成:

20170301148833910576102.png
20170301148833910576102.png

旋转变换

围绕坐标原点旋转:

假定有一个点 P(\(x_{0}\),\(y_{0}\)) ,相对坐标原点 \(\theta\) 顺时针旋转后的情形,同时假定P点离坐标原点的距离为r,如下图:

20170301148833915872714.png
20170301148833915872714.png

那么,

20170301148833921797142.png
20170301148833921797142.png

如果用矩阵,就可以表示为:

20170301148833923074578.png
20170301148833923074578.png

围绕某个点旋转

如果是围绕某个点 P(\(x_{p}\),\(y_{p}\)) 顺时针旋转 \(\theta\) ,那么可以用矩阵表示为:

2017030114883392576953.png
2017030114883392576953.png


可以化为:

20170301148833927146486.png
20170301148833927146486.png


很显然

1、

20170301148833928967061.png
20170301148833928967061.png
是将坐标原点移动到点 P(\(x_{p}\),\(y_{p}\)) 后, P(\(x_{0}\),\(y_{0}\)) 的新坐标。

2、

20170301148833934213059.png
20170301148833934213059.png
是将上一步变换后的 P(\(x_{0}\),\(y_{0}\)) ,围绕新的坐标原点顺时针旋转 \(\theta\) 。

3、

20170301148833940633954.png
20170301148833940633954.png


经过上一步旋转变换后,再将坐标原点移回到原来的坐标原点。

所以,围绕某一点进行旋转变换,可以分成3个步骤,即首先将坐标原点移至该点,然后围绕新的坐标原点进行旋转变换,再然后将坐标原点移回到原先的坐标原点。

同时,可以看到,上面的矩阵四块区域的切分也是因为矩阵乘法的操作决定的,由于这里的乘法运算中,左上角的四个值,可以和x,y值做乘法运算,所以可以影响到旋转等操作,而右上角的模块,只能做加法,所以只能影响到平移。右下角的模块主要管z轴,自然就可以进行等比的缩放了,左下角的模块一般不去动他,否则会把x,y值加入到z轴中来,会不可控。

缩放变换

理论上而言,一个点是不存在什么缩放变换的,但考虑到所有图像都是由点组成,因此,如果图像在x轴和y轴方向分别放大k1和k2倍的话,那么图像中的所有点的x坐标和y坐标均会分别放大k1和k2倍,即

x = \(k_{1}\)\(x_{0}\)

y = \(k_{2}\)\(y_{0}\)

用矩阵表示就是:

20170301148833944726575.png
20170301148833944726575.png


缩放变换比较好理解,就不多说了。

错切变换

错切变换(skew)在数学上又称为Shear mapping(可译为“剪切变换”)或者Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的x坐标(或者y坐标)保持不变,而对应的y坐标(或者x坐标)则按比例发生平移,且平移的大小和该点到x轴(或y轴)的垂直距离成正比。错切变换,属于等面积变换,即一个形状在错切变换的前后,其面积是相等的。

比如下图,各点的y坐标保持不变,但其x坐标则按比例发生了平移。这种情况将水平错切。

20170301148833947362086.png
20170301148833947362086.png


下图各点的x坐标保持不变,但其y坐标则按比例发生了平移。这种情况叫垂直错切。

20170301148833948852458.png
20170301148833948852458.png

假定一个点 P(\(x_{0}\),\(y_{0}\)) 经过错切变换后得到 P(x,y) ,对于水平错切而言,应该有如下关系:

x = \(x_{0}\) + k\(y_{0}\)

y = \(y_{0}\)

用矩阵表示就是:

20170301148833957987910.png
20170301148833957987910.png


扩展到3 x 3的矩阵就是下面这样的形式:

20170301148833959011272.png
20170301148833959011272.png


同理,对于垂直错切,可以有:

20170301148833960187699.png
20170301148833960187699.png


在数学上严格的错切变换就是上面这样的。在Android中除了有上面说到的情况外,还可以同时进行水平、垂直错切,那么形式上就是:

20170301148833961263046.png
20170301148833961263046.png

对称变换

除了上面讲到的4中基本变换外,事实上,我们还可以利用Matrix,进行对称变换。所谓对称变换,就是经过变化后的图像和原图像是关于某个对称轴是对称的。比如,某点 P(\(x_{0}\),\(y_{0}\)) 经过对称变换后得到 P(x,y) ,

如果对称轴是x轴,那么,

x = \(x_{0}\)

y = -\(y_{0}\)

用矩阵表示就是:

20170301148834373561338.png
20170301148834373561338.png


如果对称轴是y轴,那么,

x = -\(x_{0}\)

y = \(y_{0}\)

用矩阵表示就是:

20170301148834375645689.png
20170301148834375645689.png


如果对称轴是y = x,如图:

20170301148834376584200.png
20170301148834376584200.png


那么,

20170301148834377595408.png
20170301148834377595408.png


很容易可以解得:

x = \(y_{0}\)

y = \(x_{0}\)

用矩阵表示就是:

2017030114883437925984.png
2017030114883437925984.png


同样的道理,如果对称轴是y = -x,那么用矩阵表示就是:

20170301148834380928745.png
20170301148834380928745.png


特殊地,如果对称轴是y = kx,如下图:

20170301148834381851581.png
20170301148834381851581.png


那么,

20170301148834382842810.png
20170301148834382842810.png


很容易可解得:

20170301148834383638888.png
20170301148834383638888.png


用矩阵表示就是:

201703011488343844739.png
201703011488343844739.png

当k = 0时,即y = 0,也就是对称轴为x轴的情况;当k趋于无穷大时,即x = 0,也就是对称轴为y轴的情况;当k =1时,即y = x,也就是对称轴为y = x的情况;当k = -1时,即y = -x,也就是对称轴为y = -x的情况。不难验证,这和我们前面说到的4中具体情况是相吻合的。

如果对称轴是y = kx + b这样的情况,只需要在上面的基础上增加两次平移变换即可,即先将坐标原点移动到(0, b),然后做上面的关于y = kx的对称变换,再然后将坐标原点移回到原来的坐标原点即可。用矩阵表示大致是这样的:

20170301148834385747500.png
20170301148834385747500.png


需要特别注意:在实际编程中,我们知道屏幕的y坐标的正向和数学中y坐标的正向刚好是相反的,所以在数学上y = x和屏幕上的y = -x才是真正的同一个东西,反之亦然。也就是说,如果要使图片在屏幕上看起来像按照数学意义上y = x对称,那么需使用这种转换:

20170301148834387269783.png
20170301148834387269783.png


要使图片在屏幕上看起来像按照数学意义上y = -x对称,那么需使用这种转换:

2017030114883438827302.png
2017030114883438827302.png


关于对称轴为y = kx 或y = kx + b的情况,同样需要考虑这方面的问题。

三角函数

public void setSinCos(float sinValue, float cosValue, float px, float py)
public void setSinCos(float sinValue, float cosValue)

这个方法乍一看可能有点蒙,其实在前面的原理中,我们讲解了一个旋转的例子,他最终的矩阵效果是这样的:

20170923150616425357007.jpg
20170923150616425357007.jpg

其实旋转,就是使用了这样的matrix,显而易见,这里的参数就清晰了。

sinValue:对应图中的sin值

cosValue:对应cos值

px:中心的x坐标

py:中心的y坐标

看一个示例,我们把图像旋转90度,那么90度对应的sin和cos分别是1和0。

20170923150616431122661.png
20170923150616431122661.png

看代码如下:

Matrixmatrix = new Matrix();
matrix.setSinCos(1, 0, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
canvas.drawBitmap(bitmap, matrix, paint);123123

数值操作

数值操作这一组方法可以帮助我们直接控制Matrix里面的数值。

set

void set (Matrix src)没有返回值,有一个参数,作用是将参数Matrix的数值复制到当前Matrix中。如果参数为空,则重置当前Matrix,相当于reset()

reset

void reset ()重置当前Matrix(将当前Matrix重置为单位矩阵)。

setValues

void setValues (float[] values)setValues的参数是浮点型的一维数组,长度需要大于9,拷贝数组中的前9位数值赋值给当前Matrix。

getValues

void getValues (float[] values)很显然,getValues和setValues是一对方法,参数也是浮点型的一维数组,长度需要大于9,将Matrix中的数值拷贝进参数的前9位中。

基本方法解析

讲解完了matrix作用于像素点的原理之后,我们逐个讲解它的方法。

public Matrix()
public Matrix(Matrix src)

构造函数有两个,第一个是直接创建一个单位矩阵,第二个是根据提供的矩阵创建一个新的矩阵(采用deep copy)
单位矩阵如下:

20170923150616404162667.png
20170923150616404162667.png

public boolean isIdentity()//判断是否是单位矩阵
public boolean isAffine()//判断是否是仿射矩阵

是否是单位矩阵很简单,就不做讲解了,这里是否是仿射矩阵可能大家不好理解。
首先来看看什么是仿射变换。仿射变换其实就是二维坐标到二维坐标的线性变换,保持二维图形的“平直性”(即变换后直线还是直线不会打弯,圆弧还是圆弧)和“平行性”(指保持二维图形间的相对位置关系不变,平行线还是平行线,而直线上点的位置顺序不变),可以通过一系列的原子变换的复合来实现,原子变换就包括:平移、缩放、翻转、旋转和错切。这里除了透视可以改变z轴以外,其他的变换基本都是上述的原子变换,所以,只要最后一行是0,0,1则是仿射矩阵。public boolean rectStaysRect()判断该矩阵是否可以将一个矩形依然变换为一个矩形。当矩阵是单位矩阵,或者只进行平移,缩放,以及旋转90度的倍数的时候,返回true。public void reset()重置矩阵为单位矩阵。
public void setTranslate(float dx, float dy)设置平移效果,参数分别是x,y上的平移量。
效果图如下:

20170923150616415154198.png
20170923150616415154198.png

代码如下:

Matrix matrix = new Matrix();
canvas.drawBitmap(bitmap, matrix, paint);

matrix.setTranslate(100, 1000);
canvas.drawBitmap(bitmap, matrix, paint);1234512345

代码验证

在第一部分中讲到的各种图像变换的验证代码如下,一共列出了10种情况。如果要验证其中的某一种情况,只需将相应的代码反注释即可。试验中用到的图片:

2017030114883438981398.png
2017030114883438981398.png


其尺寸为162 x 251。

每种变换的结果,请见代码之后的说明。

import android.app.Activity;  
import android.content.Context;  
import android.graphics.Bitmap;  
import android.graphics.BitmapFactory;  
import android.graphics.Canvas;  
import android.graphics.Matrix;  
import android.os.Bundle;  
import android.util.Log;  
import android.view.MotionEvent;  
import android.view.View;  
import android.view.Window;  
import android.view.WindowManager;  
import android.view.View.OnTouchListener;  
import android.widget.ImageView;  
  
public class TestTransformMatrixActivity extends Activity  
implements  
OnTouchListener  
{  
    private TransformMatrixView view;  
    @Override  
    public void onCreate(Bundle savedInstanceState)  
    {  
        super.onCreate(savedInstanceState);  
        requestWindowFeature(Window.FEATURE_NO_TITLE);  
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);  
  
        view = new TransformMatrixView(this);  
        view.setScaleType(ImageView.ScaleType.MATRIX);  
        view.setOnTouchListener(this);  
          
        setContentView(view);  
    }  
      
    class TransformMatrixView extends ImageView  
    {  
        private Bitmap bitmap;  
        private Matrix matrix;  
        public TransformMatrixView(Context context)  
        {  
            super(context);  
            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sophie);  
            matrix = new Matrix();  
        }  
  
        @Override  
        protected void onDraw(Canvas canvas)  
        {  
            // 画出原图像  
            canvas.drawBitmap(bitmap, 0, 0, null);  
            // 画出变换后的图像  
            canvas.drawBitmap(bitmap, matrix, null);  
            super.onDraw(canvas);  
        }  
  
        @Override  
        public void setImageMatrix(Matrix matrix)  
        {  
            this.matrix.set(matrix);  
            super.setImageMatrix(matrix);  
        }  
          
        public Bitmap getImageBitmap()  
        {  
            return bitmap;  
        }  
    }  
  
    public boolean onTouch(View v, MotionEvent e)  
    {  
        if(e.getAction() == MotionEvent.ACTION_UP)  
        {  
            Matrix matrix = new Matrix();  
            // 输出图像的宽度和高度(162 x 251)  
            Log.e("TestTransformMatrixActivity", "image size: width x height = " +  view.getImageBitmap().getWidth() + " x " + view.getImageBitmap().getHeight());  
            // 1. 平移  
            matrix.postTranslate(view.getImageBitmap().getWidth(), view.getImageBitmap().getHeight());  
            // 在x方向平移view.getImageBitmap().getWidth(),在y轴方向view.getImageBitmap().getHeight()  
            view.setImageMatrix(matrix);  
              
            // 下面的代码是为了查看matrix中的元素  
            float[] matrixValues = new float[9];  
            matrix.getValues(matrixValues);  
            for(int i = 0; i < 3; ++i)  
            {  
                String temp = new String();  
                for(int j = 0; j < 3; ++j)  
                {  
                    temp += matrixValues[3 * i + j ] + "\t";  
                }  
                Log.e("TestTransformMatrixActivity", temp);  
            }  
              
  
//          // 2. 旋转(围绕图像的中心点)  
//          matrix.setRotate(45f, view.getImageBitmap().getWidth() / 2f, view.getImageBitmap().getHeight() / 2f);  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠  
//          matrix.postTranslate(view.getImageBitmap().getWidth() * 1.5f, 0f);  
//          view.setImageMatrix(matrix);  
//  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
              
              
//          // 3. 旋转(围绕坐标原点) + 平移(效果同2)  
//          matrix.setRotate(45f);  
//          matrix.preTranslate(-1f * view.getImageBitmap().getWidth() / 2f, -1f * view.getImageBitmap().getHeight() / 2f);  
//          matrix.postTranslate((float)view.getImageBitmap().getWidth() / 2f, (float)view.getImageBitmap().getHeight() / 2f);  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠  
//          matrix.postTranslate((float)view.getImageBitmap().getWidth() * 1.5f, 0f);  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }             
              
//          // 4. 缩放  
//          matrix.setScale(2f, 2f);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠  
//          matrix.postTranslate(view.getImageBitmap().getWidth(), view.getImageBitmap().getHeight());  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
  
              
//          // 5. 错切 - 水平  
//          matrix.setSkew(0.5f, 0f);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠           
//          matrix.postTranslate(view.getImageBitmap().getWidth(), 0f);  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
              
//          // 6. 错切 - 垂直  
//          matrix.setSkew(0f, 0.5f);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠               
//          matrix.postTranslate(0f, view.getImageBitmap().getHeight());  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }             
              
//          7. 错切 - 水平 + 垂直  
//          matrix.setSkew(0.5f, 0.5f);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠               
//          matrix.postTranslate(0f, view.getImageBitmap().getHeight());  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
              
//          // 8. 对称 (水平对称)  
//          float matrix_values[] = {1f, 0f, 0f, 0f, -1f, 0f, 0f, 0f, 1f};  
//          matrix.setValues(matrix_values);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠   
//          matrix.postTranslate(0f, view.getImageBitmap().getHeight() * 2f);  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }             
              
//          // 9. 对称 - 垂直  
//          float matrix_values[] = {-1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f};  
//          matrix.setValues(matrix_values);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }     
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠   
//          matrix.postTranslate(view.getImageBitmap().getWidth() * 2f, 0f);  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
  
              
//          // 10. 对称(对称轴为直线y = x)  
//          float matrix_values[] = {0f, -1f, 0f, -1f, 0f, 0f, 0f, 0f, 1f};  
//          matrix.setValues(matrix_values);  
//          // 下面的代码是为了查看matrix中的元素  
//          float[] matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
//            
//          // 做下面的平移变换,纯粹是为了让变换后的图像和原图像不重叠               
//          matrix.postTranslate(view.getImageBitmap().getHeight() + view.getImageBitmap().getWidth(),   
//                  view.getImageBitmap().getHeight() + view.getImageBitmap().getWidth());  
//          view.setImageMatrix(matrix);  
//            
//          // 下面的代码是为了查看matrix中的元素  
//          matrixValues = new float[9];  
//          matrix.getValues(matrixValues);  
//          for(int i = 0; i < 3; ++i)  
//          {  
//              String temp = new String();  
//              for(int j = 0; j < 3; ++j)  
//              {  
//                  temp += matrixValues[3 * i + j ] + "\t";  
//              }  
//              Log.e("TestTransformMatrixActivity", temp);  
//          }  
              
            view.invalidate();  
        }  
        return true;  
    }  
}

下面给出上述代码中,各种变换的具体结果及其对应的相关变换矩阵

平移

20170301148834397841218.png
20170301148834397841218.png


输出的结果:

20170301148834399578654.png
20170301148834399578654.png


请对照第一部分中的“一、平移变换”所讲的情形,考察上述矩阵的正确性。

旋转(围绕图像的中心点)

20170301148834401523528.png
20170301148834401523528.png

输出的结果:

20170301148834402755650.png
20170301148834402755650.png

它实际上是
matrix.setRotate(45f,view.getImageBitmap().getWidth() / 2f, view.getImageBitmap().getHeight() / 2f);

matrix.postTranslate(view.getImageBitmap().getWidth()* 1.5f, 0f);

这两条语句综合作用的结果。根据第一部分中“二、旋转变换”里面关于围绕某点旋转的公式,matrix.setRotate(45f,view.getImageBitmap().getWidth() / 2f, view.getImageBitmap().getHeight() / 2f);

所产生的转换矩阵就是:

20170301148834407336074.png
20170301148834407336074.png


matrix.postTranslate(view.getImageBitmap().getWidth()* 1.5f, 0f);的意思就是在上述矩阵的左边再乘以下面的矩阵:

2017030114883440946965.png
2017030114883440946965.png


关于post是左乘这一点,我们在前面的理论部分曾经提及过,后面我们还会专门讨论这个问题。

所以它实际上就是:

20170301148834415832436.png
20170301148834415832436.png


出去计算上的精度误差,我们可以看到我们计算出来的结果,和程序直接输出的结果是一致的。

旋转(围绕坐标原点旋转,在加上两次平移,效果同2)

20170301148834419441235.png
20170301148834419441235.png


根据第一部分中“二、旋转变换”里面关于围绕某点旋转的解释,不难知道:

matrix.setRotate(45f,view.getImageBitmap().getWidth() / 2f, view.getImageBitmap().getHeight() / 2f);

等价于

matrix.setRotate(45f);

matrix.preTranslate(-1f* view.getImageBitmap().getWidth() / 2f, -1f *view.getImageBitmap().getHeight() / 2f);

matrix.postTranslate((float)view.getImageBitmap().getWidth()/ 2f, (float)view.getImageBitmap().getHeight() / 2f);

其中matrix.setRotate(45f)对应的矩阵是:

20170301148834424050901.png
20170301148834424050901.png

matrix.preTranslate(-1f* view.getImageBitmap().getWidth() / 2f, -1f * view.getImageBitmap().getHeight()/ 2f)对应的矩阵是:

20170301148834425298280.png
20170301148834425298280.png

由于是preTranslate,是先乘,也就是右乘,即它应该出现在matrix.setRotate(45f)所对应矩阵的右侧。

matrix.postTranslate((float)view.getImageBitmap().getWidth()/ 2f, (float)view.getImageBitmap().getHeight() / 2f)对应的矩阵是:

20170301148834426293354.png
20170301148834426293354.png


这次由于是postTranslate,是后乘,也就是左乘,即它应该出现在matrix.setRotate(45f)所对应矩阵的左侧。

所以综合起来,

matrix.setRotate(45f);

matrix.preTranslate(-1f* view.getImageBitmap().getWidth() / 2f, -1f *view.getImageBitmap().getHeight() / 2f);

matrix.postTranslate((float)view.getImageBitmap().getWidth()/ 2f, (float)view.getImageBitmap().getHeight() / 2f);

对应的矩阵就是:

20170301148834427273695.png
20170301148834427273695.png


这和下面这个矩阵(围绕图像中心顺时针旋转45度)其实是一样的:

20170301148834428250094.png
20170301148834428250094.png


因此,此处变换后的图像和2中变换后的图像时一样的。

缩放变换

20170301148834429635851.png
20170301148834429635851.png


程序所输出的两个矩阵分别是:

2017030114883443077762.png
2017030114883443077762.png


其中第二个矩阵,其实是下面两个矩阵相乘的结果:

20170301148834431597151.png
20170301148834431597151.png


大家可以对照第一部分中的“三、缩放变换”和“一、平移变换”说法,自行验证结果。

错切变换(水平错切)

2017030114883443394822.png
2017030114883443394822.png


代码所输出的两个矩阵分别是:

20170301148834434942666.png
20170301148834434942666.png


其中,第二个矩阵其实是下面两个矩阵相乘的结果:

2017030114883443583409.png
2017030114883443583409.png


大家可以对照第一部分中的“四、错切变换”和“一、平移变换”的相关说法,自行验证结果。

错切变换(垂直错切)

20170301148834436740060.png
20170301148834436740060.png

代码所输出的两个矩阵分别是:

20170301148834438464295.png
20170301148834438464295.png


其中,第二个矩阵其实是下面两个矩阵相乘的结果:

20170301148834439581151.png
20170301148834439581151.png


大家可以对照第一部分中的“四、错切变换”和“一、平移变换”的相关说法,自行验证结果。

错切变换(水平+垂直错切)

20170301148834440579717.png
20170301148834440579717.png


代码所输出的两个矩阵分别是:

20170301148834441593278.png
20170301148834441593278.png


其中,后者是下面两个矩阵相乘的结果:

2017030114883444261979.png
2017030114883444261979.png


大家可以对照第一部分中的“四、错切变换”和“一、平移变换”的相关说法,自行验证结果。

对称变换(水平对称)

20170301148834443981714.png
20170301148834443981714.png

代码所输出的两个各矩阵分别是:

2017030114883444616294.png
2017030114883444616294.png


其中,后者是下面两个矩阵相乘的结果:

20170301148834447285645.png
20170301148834447285645.png


大家可以对照第一部分中的“五、对称变换”和“一、平移变换”的相关说法,自行验证结果。

对称变换(垂直对称)

20170301148834448061705.png
20170301148834448061705.png

代码所输出的两个矩阵分别是:

20170301148834449886306.png
20170301148834449886306.png


其中,后者是下面两个矩阵相乘的结果:

20170301148834450718549.png
20170301148834450718549.png


大家可以对照第一部分中的“五、对称变换”和“一、平移变换”的相关说法,自行验证结果。

对称变换(对称轴为直线y = x)

20170301148834451545673.png
20170301148834451545673.png


代码所输出的两个矩阵分别是:

20170301148834452594988.png
20170301148834452594988.png


其中,后者是下面两个矩阵相乘的结果:

20170301148834453180954.png
20170301148834453180954.png


大家可以对照第一部分中的“五、对称变换”和“一、平移变换”的相关说法,自行验证结果。

关于先乘和后乘的问题

由于矩阵的乘法运算不满足交换律,我们在前面曾经多次提及先乘、后乘的问题,即先乘就是矩阵运算中右乘,后乘就是矩阵运算中的左乘。其实先乘、后乘的概念是针对变换操作的时间先后而言的,左乘、右乘是针对矩阵运算的左右位置而言的。以第一部分“二、旋转变换”中围绕某点旋转的情况为例:

20170301148834454434026.png
20170301148834454434026.png


越靠近原图像中像素的矩阵,越先乘,越远离原图像中像素的矩阵,越后乘。事实上,图像处理时,矩阵的运算是从右边往左边方向进行运算的。这就形成了越在右边的矩阵(右乘),越先运算(先乘),反之亦然。

当然,在实际中,如果首先指定了一个matrix,比如我们先setRotate(\(\theta\)),即指定了上面变换矩阵中,中间的那个矩阵,那么后续的矩阵到底是pre还是post运算,都是相对这个中间矩阵而言的。

进阶方法解析

上面的基本方法中,有关于变换的set方法都可以带来不同的效果,但是每个set都会把上个效果清除掉,例如依次调用了setSkew,setTranslate,那么最终只有setTranslate会起作用,那么如何才和将两种效果复合呢。Matrix给我们提供了很多方法。但是主要都是2类:
preXXXX:以pre开头,例如preTranslate
postXXXX:以post开头,例如postScale
他们分别代表了前乘,和后乘。看一段代码:

Matrix matrix = new Matrix();
matrix.setTranslate(100, 1000);
matrix.preScale(0.5f, 0.5f);

这里matrix前乘了一个scale矩阵,换算成数学式如下:

2017092315061645158599.png
2017092315061645158599.png

从上面可以看出,最终得出的matrix既包含了缩放信息也有平移信息。
后乘自然就是matrix在后面,而缩放矩阵在前面,由于矩阵前后乘并不等价,也就导致了他们的效果不同。我们来看看后乘的结果:

20170923150616454160642.png
20170923150616454160642.png

可以看到,结果跟上面不同,并且这也不是我们想要的结果,这里缩放没有更改,但是平移被减半了,换句话说,平移的距离也被缩放了。所以需要注意前后乘法的关系。
来看看他们对应的效果图:

前乘:

20170923150616460519787.png
20170923150616460519787.png

后乘:

20170923150616463626763.png
20170923150616463626763.png

可以明显看到,后乘的平移距离受了影响。
了解清除了前后乘的意义,在使用的过程中,多个效果的叠加时,一样要注意,否则效果达不到预期。

其他方法解析

matrix除了上面的方法外,还有一些其他的方法,这里依次解析

setRectToRect

public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf)将rect变换成rect,上面的rectStaysRect已经说过,要保持rect只能做缩放平移和选择90度的倍数,那么这里其实也是一样,只是这几种变化,这里通过stf参数来控制。

ScaleToFit 有如下四个值:

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

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

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

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

这里使用谷歌的api demo的图片作为例子:

20170923150616472114007.png
20170923150616472114007.png

setPolyToPoly

public boolean setPolyToPoly(float[] src, int srcIndex,float[] dst, int dstIndex,int pointCount)通过指定的0-4个点,原始坐标以及变化后的坐标,来得到一个变换矩阵。如果指定0个点则没有效果。

下面通过例子分别说明1到4个点的可以达到的效果:

1个点,平移

只指定一个点,可以达到平移效果:

20170923150616478770235.png
20170923150616478770235.png

代码如下:

float[] src = {0, 0};
int DX = 300;
float[] dst = {0 + DX, 0 + DX};
matrix.setPolyToPoly(src, 0, dst, 0, 1);
canvas.drawBitmap(bitmap, matrix, paint);1234512345

2个点,旋转或者缩放

两个点,可以达到旋转效果或者缩放效果,缩放比较简单,这里我们来看旋转效果,一个点指定中心,一点指出旋转的效果

20170923150616485313647.png
20170923150616485313647.png

代码如下

int bw = bitmap.getWidth();
int bh = bitmap.getHeight();
float[] src = {bw / 2, bh / 2, bw, 0};
float[] dst = {bw / 2, bh / 2, bw / 2 + bh / 2, bh / 2 + bw / 2};
matrix.setPolyToPoly(src, 0, dst, 0, 2);
canvas.drawBitmap(bitmap, matrix, paint);123456123456

图片的中心点作为旋转的中心,前后不变,右上角变化到了下方,所以导致图片旋转了90度。

3个点,错切

使用3个点,可以产生错切效果,指定3个顶点,一个固定,另外两个移动。

看图:

20170923150616492474600.png
20170923150616492474600.png

代码如下:

Matrix matrix = new Matrix();
int bw = bitmap.getWidth();
int bh = bitmap.getHeight();
float[] src = {0,0, 0, bh,bw,bh};
float[] dst = {0, 0, 200, bh, bw + 200, bh};
matrix.setPolyToPoly(src, 0, dst, 0, 3);
canvas.drawBitmap(bitmap, matrix, paint);12345671234567

4个点,透视

透视就是观察的角度变化了。导致投射到平面上的二维图像变化了。
我们看下面的例子,更容易理解:

20170923150616497319034.png
20170923150616497319034.png

图片看起来好像倾斜了,实现特别简单:

Matrix matrix = new Matrix();
int bw = bitmap.getWidth();
int bh = bitmap.getHeight();
float[] src = {0, 0, 0, bh, bw, bh, bw, 0};
int DX = 100;
float[] dst = {0 + DX, 0, 0, bh, bw, bh, bw - DX, 0};
matrix.setPolyToPoly(src, 0, dst, 0, 4);
canvas.drawBitmap(bitmap, matrix, paint);1234567812345678

可以看到,只是把左右两个顶点往里面收拢了,这样就得出了一个有3d效果的透视图。

invert

public boolean invert(Matrix inverse)反转当前矩阵,如果能反转就返回true并将反转后的值写入inverse,否则返回false。当前矩阵*inverse=单位矩阵。

反转前后有什么效果,我们来看看示例:

20170923150616503373152.png
20170923150616503373152.png

可以看到,反转之后,其实是对效果的一种反转。

mapPoints

public void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex,int pointCount)
public void mapPoints(float[] dst, float[] src)
public void mapPoints(float[] pts)

映射点的值到指定的数组中,这个方法可以在矩阵变换以后,给出指定点的值。

  • dst:指定写入的数组
  • dstIndex:写入的起始索引,x,y两个坐标算作一对,索引的单位是对,也就是经过两个值才加1
  • src:指定要计算的点
  • srcIndex:要计算的点的索引
  • pointCount:需要计算的点的个数,每个点有两个值,x和y。

mapVectors

public void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex,int vectorCount)
public void mapVectors(float[] dst, float[] src)
public void mapVectors(float[] vecs)

与上面的mapPoionts基本类似,这里是将一个矩阵作用于一个向量,由于向量的平移前后是相等的,所以这个方法不会对translate相关的方法产生反应,如果只是调用了translate相关的方法,那么得到的值和原本的一致。

mapRect

public boolean mapRect(RectF dst, RectF src)
public boolean mapRect(RectF rect)

返回值即是调用的rectStaysRect(),这个方法前面有讲过,这里把src中指定的矩形的左上角和右下角的两个点的坐标,写入dst中。

mapRadius

public float mapRadius(float radius)返回一个圆圈半径的平均值,将matrix作用于一个指定radius半径的圆,随后返回的平均半径。