前言:图像的变换处理离不开矩阵,在Android里面也一样,本文将从原理出发,介绍了Android里面 view的变换原理以及对应的相关API的使用,还有Android里面图像的颜色变换处理等等,其原理大多适用在其他平台,譬如Windows(c++)
什么是矩阵
对图形进行变换时,其本质就是矩阵的一些运算,文中讲到原理的地方都涉及到了矩阵的一些知识以及运算,所以在开始讲下文时,有必要先大概的介绍下矩阵(相信大家在大学的数学中都有学过矩阵,这里就当做一个简单的回顾,对矩阵记忆尤深的同学可以跳过)。
-
矩阵的定义
由 m × n 个数aij排成的m行n列的数表称为m行n列的矩阵,简称m × n矩阵。记作:
只有一行的矩阵称之为行矩阵或行向量,只有一列的矩阵称之为列矩阵或列向量,行数与列数都相等的n介矩阵又称之为n介方阵,对角线上的元素都是1,其它元素都是0的n介方阵叫单位矩阵
-
矩阵的简单运算
矩阵的运算大概有:矩阵的加法、数与矩阵相乘 、矩阵与矩阵相乘 、矩阵的置换 等等,这里我们只要了解数与矩阵的相乘,矩阵与矩阵的相乘就足够了。
- 数与矩阵相乘
常数c与矩阵A的乘积记作cA或Ac,规定为
-
矩阵与矩阵相乘
设A是一个 m x s的矩阵, B是一个s x n的矩阵, 那么定义矩阵A与矩阵B的乘积是一个m x n的矩阵C,其中矩阵C的第i行第j列元素可表示为
如下所示:
图像变换-Matrix
Android 里面定义了一个矩阵,用作来做图形的变换的,包括图形的平移、缩放、错切(倾斜)还有旋转等等,只要修改矩阵里对应的值,然后再跟原图形做乘法运算就可以得到变换后的效果了。这个矩阵的定义大概如下:
其中
MSCALE_X
MSCALE_X
用作图像的缩放,MSKEW_X
MSKEW_Y
用作图像的错切(倾斜),MTRANS_X
MTRANS_Y
用作图像的平移,矩阵里没有直接控制图像旋转的元素,图像的旋转变换是通过MSCALE
跟MSKEW
来一起完成的。
我们新建一个Matrix对象时,它的矩阵默认长成这样的
val matrix = Matrix()
Log.d(TAG, "$matrix")
输出:
MainActivity: Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}
根据前面介绍的知识我们可知,这是一个3介的单位方阵,单位方阵乘一个矩阵A,结果依然是方阵A,因此当我们创建一个Matrix时,默认是不会对图像进行任何修改。
-
缩放(Scale)
屏幕坐标系:每个像素点在屏幕上都会有一个像素坐标,这个坐标是个3行1列的矩阵,如下:
x ,y分别代表屏幕上面x, y轴的坐标,1是在计算三维坐标时用到,这里我们先不对三维图像展开讨论。
点阵图:我们肉眼所看见的图形,实际上在计算机里面是以一个点阵图的方式来表达的,所谓的点阵图就是由一个个的像素点为元素构成的像素矩阵,举个简单的例子,3X3 的256级灰度图,也就是高为3个像素,宽也是3个像素的图像,每个像素的取值可以是 0-255,代表该像素的亮度,255代表最亮,也就是白色,0代表最暗,即黑色 。像素矩阵如下所示:
图像中每个像素点都会有一个对应的屏幕坐标,缩放的本质就是对图像的每一个像素点坐标做矢量缩放,譬如对于单个像素点来讲,宽度缩放 k1 倍,高度缩放 k2 倍,该点坐标为 x0、y0,缩放后坐标为 x、y,那么缩放后的坐标可以用下面线性方程来表示:
用矩阵来描述就是:
这种给出原坐标,通过运算得出目标坐标的映射过程叫向前映射,反过来由输出图像坐标,来反推算出该像素在源图像中的坐标位置的映射叫向后映射。
缩放的原理就是这样,我们不需要自己去实现一个矩阵,在Android里面提供Matrix类来构造矩阵,Matrix提供了setScale
方法可以让我们很方便的去修改矩阵里面的MSCALE_X
MSCALE_X
元素,方法的定义如下:
//sx 代表水平方向的缩放系数
//sy 代表垂直方向的缩放系数
//py 缩放枢轴
void setScale(float sx, float sy);
void setScale(float sx, float sy, float px, float py);
举个例子
init {
mtx.setScale(2.0f, 2.0f)
paint.color = Color.BLUE
Log.d("MatrixDemoView", "$mtx")
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(bitmap, mtx, paint)
//canvas.scale(2.0f, 2.0f)
//canvas.drawRect(rect,paint)
}
输出日志:MatrixDemoView: Matrix{[2.0, 0.0, 0.0][0.0, 2.0, 0.0][0.0, 0.0, 1.0]}
效果如下:
-
平移(Translate)
前面已经介绍过图像的缩放原理了,相比起图像的缩放,图像的平移就更加简单。跟缩放原理一样,图像的平移本质上就是对图像的所有像素点进行矢量平移,假设平移前的像素坐标点是(x0, y0),在水平方向平移了x的距离,在垂直方式平移y距离,如图所示:
那么平移后的像素点坐标可以表示为:
用矩阵来描述就是:
Android里面通过Matrix提供的
setTranslate
可以设置平移,还是用上面的例子,我们把代码修改为setTranslate
init {
mtx.setTranslate(100f, 200f)
paint.color = Color.BLUE
Log.d("MatrixDemoView", "$mtx")
}
日志输出:MatrixDemoView: Matrix{[1.0, 0.0, 100.0][0.0, 1.0, 200.0][0.0, 0.0, 1.0]}
从日志里可以看出来,平移的本质其实就是修改矩阵里面的MTRANS_X
,MTRANS_Y
。
-
错切(Skew)
错切变换(skew)在数学上又称为Shear mapping(可译为“剪切变换”)或者Transvection(缩并),它是一种比较特殊的线性变换。错切变换的效果就是让所有点的x坐标(或者y坐标)保持不变,而对应的y坐标(或者x坐标)则按比例发生平移,且平移的大小和该点到x轴(或y轴)的垂直距离成正比。文字看起来比较难理解,下面给出一个映射图方便大家理解
下图各点的y坐标保持不变,但其x坐标则按比例发生了平移,这种情况将水平错切
下图各点的x坐标保持不变,但其y坐标则按比例发生了平移,这种情况叫垂直错切
假如有一个点(x0, y0),在水平方向上的错切是k1,在垂直方向上的错切是k1,错切变换后的坐标是(x, y),那么错切后的像素点坐标可以表示为
用矩阵来标识就是:
Matrix里面提供了setSkew
方法来设置错切,还是直接用上面的例子,我们把代码修改为setSkew
init {
//设置水平方向的错切系数是0.3
mtx.setSkew(0.3f, 0f)
paint.color = Color.BLUE
Log.d("MatrixDemoView", "$mtx")
}
日志输出:MatrixDemoView: Matrix{[1.0, 0.3, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}
效果大概如下:
从输出的日志里面我们也可以看得出来setSkew
方法实际修改的就是矩阵的MSKEW_X
MSKEW_Y
-
旋转(Rotate)
我们回顾下这个Matrix矩阵,发现它里面并没有直接设置旋转的元素的,那么矩阵的旋转又是怎么实现的呢?
在讲旋转的原理前,我们先回顾下三角函数里面的和差角公式,如下:
旋转的本质就是图像的像素点绕某个点旋转,假设有一个点P坐标是(x0, y0),绕坐标原点O旋转度后坐标变成(x, y),同时假设P点到坐标原点O的距离是r
那么就有:
换做矩阵运算就如下图:
原理大概就是这样了。
Matrix里面提供了几个设置旋转的接口
//degrees 旋转角度,以原点坐标(0,0)为中心做旋转
// px,py 设置一个旋转中心坐标
void setRotate(float degrees)
void setRotate(float degrees, float px, float py)
void setSinCos(float sinValue, float cosValue)
void setSinCos(float sinValue, float cosValue, float px, float py)
知道了旋转的原理后,我们就知道Matrix里面setSinCos
这个方法怎么用了,
下面还是用上面的例子来演示下,修改代码为
init {
// 设置旋转角度为90度,旋转中心是图片中心
mtx.setRotate(90f, bitmap.width / 2f, bitmap.height / 2f)
paint.color = Color.BLUE
Log.d("MatrixDemoView", "$mtx")
}
日志输出:Matrix{[0.0, -1.0, 315.0][1.0, 0.0, 0.0][0.0, 0.0, 1.0]}
效果如下
我们把代码修改为
mtx.setSinCos(1f, 0f, bitmap.width / 2f, bitmap.height / 2f)
sin的值设置为1 cos的值设置0,出来的效果跟直接调用setRotate
设置旋转角度的效果是一样的。
复合变换
在使用Matrix的set系列方法的时候会注意到,还有其他方法名类似只不过加了pre或者post前缀的,譬如除了setTranslate
,还会有preTranslate
跟postTranslate
,那么这两个方法又是怎么用的呢?
当使用set方法设置Matrix的时候会摸掉Matrix原先的所有值,只保留当前的设置,譬如同时调用了setScale
setSkew
setTranslate
,最后的setTranslate
会覆盖掉最开始的缩放跟错切设置,想既设置缩放又设置平移,那么就需要借助pre跟post前缀的方法了。
-
前乘(pre)
前乘相当于矩阵右乘:
譬如前面的例子代码我们把
setScale
修改为preScale
(其实修改成postScale
效果也是一样,因为初始化的时候Matrix是个单位矩阵,单位矩阵前乘后乘结果都一样)代码如下:
mtx.preScale(1.3f, 1.3f)
输出日志:MatrixDemoView: Matrix{[1.3, 0.0, 0.0][0.0, 1.3, 0.0][0.0, 0.0, 1.0]}
就会得到以下矩阵:
接着我们用pre设置一个平移量,代码如下:
mtx.preScale(1.3f, 1.3f)
mtx.preTranslate(100f, 100f)
输出日志:MatrixDemoView: Matrix{[1.3, 0.0, 130.0][0.0, 1.3, 130.0][0.0, 0.0, 1.0]}
变换过程如下:
虽然我们只设置了平移100个单位,但是实际平移是130,由于前面有个放大的变换,所以平移距离也对应的被放大了。
通过这个例子,我们能清楚得看到,通过pre(或post)前缀的方法去进行矩阵变换,并不是先放大后平移,变换后的图像是严格按照矩阵的运算来执行的
-
后乘(post)
后乘相当于矩阵左乘:
我们把前面的
preTranslate
修改为postTranslate
,代码如下:
mtx.preScale(1.3f, 1.3f)
mtx.postTranslate(100f, 100f)
输出日志:MatrixDemoView: Matrix{[1.3, 0.0, 100.0][0.0, 1.3, 100.0][0.0, 0.0, 1.0]}
变换过程如下:
这里我们也能清楚得看出来,前乘跟后乘的结果是不一样的,这是由于矩阵的乘法运算是不满足交换律的
图像的几何变换
前面介绍了如何通过Matrix进行图像变换以及其原理,但是细心的读者心中肯定会有一些疑问了。
疑问一:假设有像素坐标点A(1, 1) 水平垂直方向上平移1.3之后对应的坐标就是(1.3, 1.3)了,可是屏幕坐标里,x, y都是整数,没有小数的,那么经过平移变换后,A点的输出坐标到底是多少?
-
疑问二:假设现在有2x2大小的图片,像素点有4个,放大一倍后,变成了4x4了,像素点有16个,剩余的16-4=12个像素点又去哪里找?如下图所示:
上图只有(0,0),(0,2),(2,0),(2,2)四个坐标根据映射关系在原图像中找到了相对应的像素,其余的12个坐标没有有效值。
-
疑问三:假设现在有4x4大小的图片,像素点有16个,缩小一倍后,变成了2x2大小了,像素点有4个,根据映射关系,原图像的多个像素点都会映射到目标图像的同一个像素上,如下图所示:
上图左上角的四个像素(0,0),(0,1),(1,0),(1,1)都会映射到输出图像的(0,0)上,那么(0,0)究竟取那个像素值呢?
这就是向前映射通常会遇到的问题:浮点坐标、映射不完全、映射重叠。先说说向前映射的实现步骤:遍历源图像素, 通过矩阵运算得出原像素点在目标图中对应的坐标(这个过程可能会出现浮点坐标问题),然后进行逐点的像素拷贝(当放大图片的时候这个过程会出现空缺,缩小图片的时候会出现多余的计算量)。
要解决上述两个问题可以使用向后映射,使用输出图像的坐标反过来推算改坐标对应于原图像中的坐标位置。这样,输出图像的每个像素都可以通过映射关系在原图像找到唯一对应的像素,而不会出现映射不完全和映射重叠。
但是显然向后映射的方式只能解决映射不完全,映射重叠的问题,浮点坐标的问题依然在向后映射里面存着,那么要怎么解决这个问题呢?这个问题只能通过图像算法去处理了,常见的有“最临近插值”,“双线性内插值算法”等等,图像处理算法不在本文的讨论范围内,这里只做大概的介绍
-
最邻近插值法(Nearest Interpolation)
这是最简单的一种插值方法,不需要计算。在待求像素的四邻像素中,将距离待求像素最近的邻接像素灰度值赋予待求像素。设i+u, j+v(i, j为正整数, u, v为大于零小于1的小数,下同)为待求象素坐标,则待求象素灰度的值 f(i+u, j+v) 如下图所示:
如果(i+u, j+v)落在A区,即u<0.5, v<0.5,则将左上角象素的灰度值赋给待求象素,同理,落在B区则赋予右上角的象素灰度值,落在C区则赋予左下角象素的灰度值,落在D区则赋予右下角象素的灰度值。
最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能出现明显的锯齿状。
-
双线性内插法(Bilinear Interpolation)
双线性内插法是利用待求象素四个邻象素的灰度在两个方向上作线性内插,如下图所示:
对于 (i, j+v),f(i, j) 到 f(i, j+1) 的灰度变化为线性关系,则有:
f(i, j+v) = [f(i, j+1) - f(i, j)] * v + f(i, j)
同理对于 (i+1, j+v) 则有:
f(i+1, j+v) = [f(i+1, j+1) - f(i+1, j)] * v + f(i+1, j)
从f(i, j+v) 到 f(i+1, j+v) 的灰度变化也为线性关系,由此可推导出待求象素灰度的计算式如下:
f(i+u, j+v) = (1-u) * (1-v) * f(i, j) + (1-u) * v * f(i, j+1) + u * (1-v) * f(i+1, j) + u * v * f(i+1, j+1)
双线性内插法的计算比最邻近点法复杂,计算量较大,但没有灰度不连续的缺点,结果基本令人满意。它具有低通滤波性质,使高频分量受损,图像轮廓可能会有一点模糊。
颜色变换-ColorMatrix
-
原理介绍
-
灰度效果
-
图像反转效果