Android中图像变换Matrix的原理、代码验证和应用

首先复习下大学的线性代数矩阵的公式

举个例子来说:

  \begin{bmatrix}     1 & 0 & 2 \\     -1 & 3 & 1  \end{bmatrix}\cdot  \begin{bmatrix}    3 & 1 \\    2 & 1 \\    1 & 0  \end{bmatrix}=\begin{bmatrix}   1 \begin{bmatrix} 3 & 1 \end{bmatrix} + 0 \begin{bmatrix} 2 & 1 \end{bmatrix} + 2 \begin{bmatrix} 1 & 0 \end{bmatrix} \\   -1 \begin{bmatrix} 3 & 1 \end{bmatrix} + 3 \begin{bmatrix} 2 & 1 \end{bmatrix} + 1 \begin{bmatrix} 1 & 0 \end{bmatrix}\end{bmatrix}=\begin{bmatrix}   \begin{bmatrix} 3 & 1 \end{bmatrix} +   \begin{bmatrix} 0 & 0 \end{bmatrix} +   \begin{bmatrix} 2 & 0 \end{bmatrix} \\   \begin{bmatrix} -3 & -1 \end{bmatrix} + \begin{bmatrix} 6 & 3 \end{bmatrix} +   \begin{bmatrix} 1 & 0 \end{bmatrix}\end{bmatrix}
=\begin{bmatrix}    5 & 1 \\    4 & 2\end{bmatrix}

第一部分 Matrix的数学原理

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

 

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

Translate           平移变换

Rotate                旋转变换

Scale                  缩放变换

Skew                  错切变换

 

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

 

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

set用于设置Matrix中的值。

pre是先乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。先乘相当于矩阵运算中的右乘。

post是后乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。后乘相当于矩阵运算中的左乘。

 

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

 

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

 

一、 平移变换

假定有一个点的坐标是 ,将其移动到 ,再假定在x轴和y轴方向移动的大小分别为:


如下图所示:


不难知道:


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

 


二、 旋转变换

 

2.1    围绕坐标原点旋转:

假定有一个点 ,相对坐标原点顺时针旋转后的情形,同时假定P点离坐标原点的距离为r,如下图:


那么,


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


 

2.2    围绕某个点旋转

如果是围绕某个点顺时针旋转,那么可以用矩阵表示为:


可以化为:


很显然,

1.   

  是将坐标原点移动到点后, 的新坐标。

2.     


是将上一步变换后的,围绕新的坐标原点顺时针旋转 。

3.     


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

 

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

 

三、 缩放变换

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


用矩阵表示就是:


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

 

四、 错切变换

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

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


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

 

假定一个点经过错切变换后得到,对于水平错切而言,应该有如下关系:


用矩阵表示就是:


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

 

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


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


 

五、 对称变换

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

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


用矩阵表示就是:


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


用矩阵表示就是:


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


那么,


很容易可以解得:


用矩阵表示就是:


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

 

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


那么,


很容易可解得:


用矩阵表示就是:


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的对称变换,再然后将坐标原点移回到原来的坐标原点即可。用矩阵表示大致是这样的:


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


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

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

 

 

第二部分 代码验证

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


其尺寸为162 x 251。

 

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
<span style= "font-size:13px;" ></span><pre name= "code" class = "java" > package com.pat.testtransformmatrix;
 
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 ;
     }
}


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

1.     平移

输出的结果:

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

 

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

输出的结果:

它实际上是

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);

所产生的转换矩阵就是:

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

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

 

所以它实际上就是:

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

 

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

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

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)对应的矩阵是:

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

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

 

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

这次由于是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);

对应的矩阵就是:

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

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

 

4.     缩放变换

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

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

 

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

 

5.     错切变换(水平错切)

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

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

 

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

 

6.     错切变换(垂直错切)

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

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

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

 

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

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

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

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

 

8.     对称变换(水平对称)

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

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

 

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

 

9.     对称变换(垂直对称)

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

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

 

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

 

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

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

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

 

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

 

11.   关于先乘和后乘的问题

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

 

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

 

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

 

所有这些,其实都是很自然的事情。

 

第三部分 应用

在这一部分,我们会将前面两部分所了解到的内容和Android手势结合起来,利用各种不同的手势对图像进行平移、缩放和旋转,前面两项都是在实践中经常需要用到的功能,后一项据说苹果也是最近才加上的,而实际上在Android中,咱们通过自己的双手,也可以很轻松地实现之。

 

首先创建一个Android项目PatImageView,同时创建一个Activity:PatImageViewActivity。完成这一步后, 记得在AndroidManifest.xml中增加如下许可:

<uses-permissionandroid:name="android.permission.VIBRATE"/>

因为我们将要通过短按还是长按,来确定将图片到底是缩放还是旋转。

 

现在来创建一个ImageView的派生类:PatImageView,其代码(PatImageView.java)如下(2011-11-22 revised):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package com.pat.imageview; 
   
import android.app.Service; 
import android.content.Context; 
import android.graphics.Matrix; 
import android.graphics.PointF; 
import android.os.Vibrator; 
import android.util.FloatMath; 
import android.view.GestureDetector; 
import android.view.MotionEvent; 
import android.view.View; 
import android.widget.ImageView; 
   
public class PatImageView extends ImageView 
     private Matrix matrix; 
     private Matrix savedMatrix; 
       
     private boolean long_touch = false
     private static int NONE = 0
     private static int DRAG = 1 ;    // 拖动 
     private static int ZOOM = 2 ;    // 缩放 
     private static int ROTA = 3 ;    // 旋转 
     private int mode = NONE; 
       
     private PointF startPoint; 
     private PointF middlePoint; 
       
     private float oldDistance; 
     private float oldAngle; 
   
     private Vibrator vibrator; 
       
     private GestureDetector gdetector; 
       
     public PatImageView( final Context context) 
    
         super (context); 
   
         matrix = new Matrix(); 
         savedMatrix = new Matrix(); 
           
         matrix.setTranslate(0f, 0f); 
         setScaleType(ScaleType.MATRIX); 
         setImageMatrix(matrix); 
           
         startPoint = new PointF(); 
         middlePoint = new PointF(); 
           
         oldDistance = 1f; 
           
         gdetector = new GestureDetector(context, new GestureDetector.OnGestureListener() 
        
             @Override 
             public boolean onSingleTapUp(MotionEvent e) 
            
                 return true
            
               
             @Override 
             public void onShowPress(MotionEvent e) 
            
            
               
             @Override 
             public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 
            
                 return true
            
               
             @Override 
             public void onLongPress(MotionEvent e) 
            
                 long_touch = true
                 vibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE); 
                 // 振动50ms,提示后续的操作将是旋转图片,而非缩放图片 
                 vibrator.vibrate( 50 ); 
            
               
             @Override 
             public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) 
            
                 return true
            
               
             @Override 
             public boolean onDown(MotionEvent e) 
            
                 return true
            
         }); 
           
         setOnTouchListener( new OnTouchListener() 
        
             public boolean onTouch(View view, MotionEvent event) 
            
                 switch (event.getAction() & MotionEvent.ACTION_MASK) 
                
                 case MotionEvent.ACTION_DOWN:           // 第一个手指touch 
                     savedMatrix.set(matrix); 
                     startPoint.set(event.getX(), event.getY()); 
                     mode = DRAG; 
                     long_touch = false
                     break
                 case MotionEvent.ACTION_POINTER_DOWN:   // 第二个手指touch 
                     oldDistance = getDistance(event);   // 计算第二个手指touch时,两指之间的距离 
                     oldAngle = getDegree(event);        // 计算第二个手指touch时,两指所形成的直线和x轴的角度 
                     if (oldDistance > 10f) 
                    
                         savedMatrix.set(matrix); 
                         middlePoint = midPoint(event); 
                         if (!long_touch) 
                        
                             mode = ZOOM; 
                        
                         else 
                        
                             mode = ROTA; 
                        
                    
                     break
                 case MotionEvent.ACTION_UP: 
                     mode = NONE; 
                     break
                 case MotionEvent.ACTION_POINTER_UP: 
                     mode = NONE; 
                     break
                 case MotionEvent.ACTION_MOVE: 
                     if (vibrator != null )    vibrator.cancel(); 
                     if (mode == DRAG) 
                    
                         matrix.set(savedMatrix); 
                         matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y); 
                    
                       
                     if (mode == ZOOM) 
                    
                         float newDistance = getDistance(event); 
                           
                         if (newDistance > 10f) 
                        
                             matrix.set(savedMatrix); 
                             float scale = newDistance / oldDistance; 
                             matrix.postScale(scale, scale, middlePoint.x, middlePoint.y); 
                        
                    
                       
                     if (mode == ROTA) 
                    
                         float newAngle = getDegree(event); 
                         matrix.set(savedMatrix); 
                         float degrees = newAngle - oldAngle; 
                         matrix.postRotate(degrees, middlePoint.x, middlePoint.y); 
                    
                     break
                
                 setImageMatrix(matrix); 
                 invalidate(); 
                 gdetector.onTouchEvent(event); 
                 return true
            
         }); 
    
   
     // 计算两个手指之间的距离 
         private float getDistance(MotionEvent event) 
        
             float x = event.getX( 0 ) - event.getX( 1 ); 
             float y = event.getY( 0 ) - event.getY( 1 ); 
             return FloatMath.sqrt(x * x + y * y); 
        
       
         // 计算两个手指所形成的直线和x轴的角度 
         private float getDegree(MotionEvent event) 
        
             return ( float )(Math.atan((event.getY( 1 ) - event.getY( 0 )) / (event.getX( 1 ) - event.getX( 0 ))) * 180f); 
        
   
         // 计算两个手指之间,中间点的坐标 
         private PointF midPoint( MotionEvent event) 
        
             PointF point = new PointF(); 
             float x = event.getX( 0 ) + event.getX( 1 ); 
             float y = event.getY( 0 ) + event.getY( 1 ); 
             point.set(x / 2 , y / 2 ); 
           
             return point; 
        
}

下面完善PatImageViewActivity.java的代码,使之如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.pat.imageview; 
   
import android.app.Activity; 
import android.graphics.Bitmap; 
import android.graphics.BitmapFactory; 
import android.os.Bundle; 
import android.view.Window; 
import android.view.WindowManager; 
   
public class PatImageViewActivity extends Activity 
     @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); 
           
         PatImageView piv = new PatImageView( this ); 
         Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.sophie); 
   
         piv.setImageBitmap(bmp); 
           
         setContentView(piv); 
    
}



 

由于有些手势在模拟器上无法模拟,所以就不上运行结果的图片了。本人在真机上运行后(照片就不拍了,有点累啦),可以轻松做到:

1.     很方便地拖动图片(比如,单指按住屏幕进行拖动)

2.     很方便地缩放图片(比如,双指按住屏幕进行分开或者并拢操作,可分别实现放大或者缩小图片的功能)

3.     长按出现振动后,可以很方便地旋转图片(一个手指固定,另外一个手指围绕那个固定的手指运动)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值