许多时候我们在数学课本上学的可能更多是知其然而不知其所以然,然而要理解一个概念可能只需要一个观点才不至于被搞懵,比如从应用的角度出发把线性代数当作是一种人为设计的领域特定语言(domain specific language)可能更容易被程序员们所接受。具体参考:程序观点下的线性代数。
下面先简单介绍(复习)矩阵的定义、矩阵的运算,然后才是在android中使用矩阵
矩阵的定义
由 m×n 个数 aij(i=1,2,…,m;j=1,2,…,n) 排成的m行n列的数表:
a11a21⋮am1a12a22⋮am2………a1na2n⋮amn
称为m行n列矩阵。
人们常常使用大写字母来表示矩阵,如
aij(i=1,2,⋯,m;j=1,2,⋯,n) 即为矩阵 A中第i行第j列的元素。
矩阵的运算
一、矩阵的加法
设有两个 m×n 矩阵A=( aij )和B=( bij ),那么矩阵A与矩阵B的和记为A + B,规定为
A+B=⎡⎣⎢⎢⎢⎢a11+b11a21+b21⋮am1+bm1a12+b12a22+b22⋮am2+bm2⋯⋯⋯a1n+b1na2n+b2n⋮amn+bmn⎤⎦⎥⎥⎥⎥
,即 A+ B=( aij+bij )
矩阵的加法满足以下规律
- 交换律:A + B = B + A
- 结合律:A + (B + C) = (A + B) + C
- 存在零元:A + 0 = 0 + A
- 存在负元:A + (-A) =(-A) + A = 0
二、数与矩阵相乘
数λ与矩阵A的乘积记作λA或Aλ,规定为
λA=Aλ=⎡⎣⎢⎢⎢⎢λa11λa21⋮λam1λa12λa22⋮λam2⋯⋯⋯λa1nλa2n⋮λamn⎤⎦⎥⎥⎥⎥
数乘矩阵满足以下运算规律(设A,B为 m×n 矩阵,λ,μ为数)
- 交换律:λA = Aλ
- 结合律:(λμ)A = λ(μA)
- 分配律:(λ+μ)A = λA + μA;λ(A + B) = λA + λB
三、矩阵与矩阵相乘
设A=( aij )是一个 m×s 矩阵,B=( bij )是一个 s×n 矩阵,
那么规定矩阵A与矩阵B的乘积是一个 m×n 矩阵C=( cij ),其中
cij=ai1b1j+ai2b2j+⋯+aisbsj=∑k=1saikbkj,(i=1,2,…,m;j=1,2,⋯,n),
并把此乘积记作
C=AB
如矩阵C为矩阵A与矩阵B的乘积,其中
可以看到矩阵 A的列数刚好等于矩阵 B的行数,根据定义矩阵 A左乘矩阵 B,即 A × B有意义,
而矩阵 B的列数不等于矩阵 A的行数,所以矩阵 A右乘矩阵 B,即 B × A无意义,
可以得出结论:矩阵与矩阵相乘不满足交换律,即 AB= BA不一定成立。根据定义我们可以得出
矩阵与矩阵相乘虽不满足交换律,但仍满足以下规律(前提:两个矩阵相乘有意义):
- 结合律:(AB)C=A(BC);
- 数与矩阵相乘的交换律:λ(AB)=(λA)B=A(λB)
- 分配律:A(B+C)=AB+AC;(B+C)A=BA+CA
Android中的矩阵
android的graphic包下有两个与矩阵运算直接相关的类:ColorMatrix与Matrix。
简单的说,ColorMatrix负责图像色彩特效的处理,Matrix负责图像图形特效处理。
一、ColorMatrix
android中使用一个列矩阵来保存图片每个像素点的RGBA值
而对图片的色彩进行处理android是使用一个 4×5 的矩阵与图片每个像素由RBGA值表示的列矩阵进行运算,如
再规定
这个新的列矩阵就能表示变化后像素的RGBA值
显而易见,当我们使用一个a=g=m=s=1,其余皆为0的矩阵去处理图片时必定不会有任何效果上的变化,我们可以称这样的矩阵为初始矩阵,
在它之上修改相关的值也很方便讲解。如图:
初始状态
如果设置RGBA中R的系数为3,则整个图片将偏红,如果设置R和G的系数为3,即红色和绿色同比例增加其效果将偏黄
若R,G,B的系数均为3,则图片变亮,且系数越大图片越亮
具体实现
用GridView存放
4×5
个EditText,每次点击change时获取EditText里的数值,20个值以数组的形式设置到一个colorMatrix中,
通过new ColorMatrixColorFilter(colorMatrix)
构造出滤镜再设置给paint,使用设置了滤镜的paint进行绘画时就能出现相应的效
果了,具体代码如下:
public class FmRGBAMatrix extends Fragment implements View.OnClickListener{
private Button changeMatrix, resetMatrix;
private GridView gridView;
private ImageView imageView;
private Bitmap picture;
private float[] mMatrix;
private List<EditText> mEts;
private GridAdapter gridAdapter;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mMatrix = new float[20];
mEts = new ArrayList<>();
View view = inflater.inflate(R.layout.fm_rgba_matrix, container, false);
gridView = (GridView) view.findViewById(R.id.group);
imageView = (ImageView) view.findViewById(R.id.imageview);
changeMatrix = (Button) view.findViewById(R.id.change);
resetMatrix = (Button) view.findViewById(R.id.reset);
picture = BitmapFactory.decodeResource(getResources(), R.drawable.cat);
imageView.setImageBitmap(picture);
gridView.setNumColumns(5);
for (int i = 0; i < 20; i++) {
EditText ed = new EditText(getContext());
mEts.add(ed);
}
gridAdapter = new GridAdapter(mEts);
gridView.setAdapter(gridAdapter);
initMatrix();
changeMatrix.setOnClickListener(this);
resetMatrix.setOnClickListener(this);
return view;
}
private void initMatrix() {
for (int i = 0; i < 20; i++) {
if (i % 6 == 0) {
mEts.get(i).setText(String.valueOf(1));
} else {
mEts.get(i).setText(String.valueOf(0));
}
}
gridAdapter.notifyDataSetChanged();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.change:
getMatrix();
setImageMatrix();
break;
case R.id.reset:
initMatrix();
getMatrix();
setImageMatrix();
}
}
private void setImageMatrix() {
Bitmap bmp = Bitmap.createBitmap(picture.getWidth(),picture.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.set(mMatrix);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(picture,0,0,paint);
imageView.setImageBitmap(bmp);
}
private void getMatrix() {
for (int i = 0; i < 20; i++){
mMatrix[i] = Float.valueOf(mEts.get(i).getText().toString());
}
}
}
效果叠加
假设有两个矩阵分别都能对图片产生效果,那么两个矩阵的乘法并得出新的矩阵可以用来表示叠加效果,如:
查看源码,可以发现在
ColorMatrix
的中的
setConcat
方法就实现了两个矩阵
matA
与
matB
的相乘,
preConcat
与
postConcat
只是调用
setConcat
方法。
preConcat
表示当前矩阵左乘
prematrix
,
postConcat
表示当前矩阵右乘
postmatrix
。
public void preConcat(ColorMatrix prematrix) {
setConcat(this, prematrix);
}
public void postConcat(ColorMatrix postmatrix) {
setConcat(postmatrix, this);
}
public void setConcat(ColorMatrix matA, ColorMatrix matB) {
float[] tmp;
if (matA == this || matB == this) {
tmp = new float[20];
} else {
tmp = mArray;
}
final float[] a = matA.mArray;
final float[] b = matB.mArray;
int index = 0;
for (int j = 0; j < 20; j += 5) {
for (int i = 0; i < 4; i++) {
tmp[index++] = a[j + 0] * b[i + 0] + a[j + 1] * b[i + 5] +
a[j + 2] * b[i + 10] + a[j + 3] * b[i + 15];
}
tmp[index++] = a[j + 0] * b[4] + a[j + 1] * b[9] +
a[j + 2] * b[14] + a[j + 3] * b[19] +
a[j + 4];
}
if (tmp != mArray) {
System.arraycopy(tmp, 0, mArray, 0, 20);
}
}
例子
用三个SeekBar分别调用ColorMatrix内置的方法setRotate(int axis, float degree)
、setSaturation(float sat)
、
setScale(float rScale, float gScale, float bScale,float aScale)
来控制图片的色调、饱和度和亮度。
setRotate(int axis, float degree)
:
第一个参数只能为0,1,2,否则抛出运行时异常,0,1,2分别代表R,G,B;第二个参数是角度,在该方法内部中被转化为弧度进行计算。
setSaturation(float sat)
:
设置图片饱和度,参数为0时为灰度图像。
setScale(float rScale, float gScale, float bScale,float aScale)
:
四个参数分别为RGBA的系数,让RGB的系数同比例变化可表现出变亮或变暗的效果。
初始状态
调节饱和度
同时调节饱和度和亮度
核心代码:
public static Bitmap handleEffect(Bitmap bm, float hue, float saturation, float lum) {
Bitmap bmp = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
Paint paint = new Paint();
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0, hue);
hueMatrix.setRotate(1, hue);
hueMatrix.setRotate(2, hue);
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum, lum, lum, 1);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.postConcat(hueMatrix);
colorMatrix.postConcat(saturationMatrix);
colorMatrix.postConcat(lumMatrix);
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(bm, 0, 0, paint);
return bmp;
}
界面代码:
public class FmRGBASeekbar extends Fragment {
private SeekBar seekbarHue, seekbarSaturation, seekbarLum;
private float mHue, mSaturation = 1.0f, mLum = 1.0f;
private Bitmap picture;
private ImageView imageView;
private int MID_VALUE = 50;
private SeekBar.OnSeekBarChangeListener listener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
switch (seekBar.getId()){
case R.id.seekbarHue:
mHue = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180;
break;
case R.id.seekbarSaturation:
mSaturation = progress * 1.0F / MID_VALUE;
break;
case R.id.seekbarLum:
mLum = progress * 1.0F / MID_VALUE;
break;
}
imageView.setImageBitmap(ImageUtil.handleEffect(picture,mHue,mSaturation,mLum));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fm_rgba_seekbar, container, false);
imageView = (ImageView) view.findViewById(R.id.imageview);
seekbarHue = (SeekBar) view.findViewById(R.id.seekbarHue);
seekbarSaturation = (SeekBar) view.findViewById(R.id.seekbarSaturation);
seekbarLum = (SeekBar) view.findViewById(R.id.seekbarLum);
seekbarHue.setMax(2 * MID_VALUE);
seekbarSaturation.setMax(2 * MID_VALUE);
seekbarLum.setMax(2 * MID_VALUE);
seekbarHue.setProgress(MID_VALUE);
seekbarSaturation.setProgress(MID_VALUE);
seekbarLum.setProgress(MID_VALUE);
seekbarHue.setOnSeekBarChangeListener(listener);
seekbarSaturation.setOnSeekBarChangeListener(listener);
seekbarLum.setOnSeekBarChangeListener(listener);
picture = BitmapFactory.decodeResource(getResources(),R.drawable.cat);
imageView.setImageBitmap(picture);
return view;
}
}
二、Matrix
图片的每个像素点的位置信息同样可以用一个列矩阵来表示
对图片进行图形处理是用一个 3×3 的矩阵去左乘一个列矩阵
根据矩阵乘法规则可知:当矩阵的scaleX=scaleY=1,skewX=skewY=translateX=translateY=0时图片不会有效果上的变化。
初始状态
- 平移
图片的所有点沿X轴方向移动Δx,沿Y轴方向移动ΔY就能实现整个图片的平移。
如图点
P0(x0,y0)
移动到点
P1(x0+Δx,y1+Δy)
使用矩阵表示即为:
使用graphic包下的Matrix类提供的
setTranslate(float Δx, float Δy)
方法就能很简便的配置Matrix实例。
Matrix translateMatrix = new Matrix();
translateMatrix.setTranslate(100.0f, 100.0f);
右移100px下移100px
- 旋转
选取图片中的一点作为中心,图片所有点均绕中心点旋转到新的点就能实现整个图片的旋转。
如图点
P0
绕原点旋转θ°到达
P1
,点
P0
坐标可以表示为OP与X轴正方向夹角之间的函数表达式:
使用矩阵表示即为:
setRotate(float degrees, float px, float py)
:第一个参数表示选转的角度,后两个参数确定旋转中心。
Matrix rotateMatrix = new Matrix();
rotateMatrix.setRotate(60.0f, picture.getWidth() / 2 + imageView.getPaddingLeft(),
picture.getHeight() / 2 + imageView.getPaddingTop());
绕图片中心顺时针旋转60°
- 缩放
单个的像素点没有缩放的概念,但图片所有像素点按照一定比例计算出新的坐标点,就能实现图片整体上的缩放。
如坐标点
(x0,y0)
经过缩放后得到新的坐标点
(x1,y1)
。
使用矩阵表示:
setScale(float sx, float sy)
:参数分别确定x轴和y轴方向上的缩放比例。
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(0.5f, 0.5f);
x轴y轴方向均缩放为原来的一半
- 错切
垂直错切(或水平错切):像素点保持X轴坐标(或y轴坐标)不变,y轴坐标(或x轴坐标)按一定比例发生改变,且改变的大小与x轴坐标(或y轴坐标)成正比。
如图点
P0
保持x轴坐标不变移动到
P1
,其中有:
使用矩阵表示:
k1=0,k2≠0 为垂直错切; k1≠0,k2=0 为水平错切;当然 k1,k2 可以均不为0,此时水平、垂直方向同时错切。
setSkew(float kx, float ky)
:参数分别确定x轴坐标变化大小与原始y轴坐标的比例,y轴坐标变化大小与原始x轴坐标的比例。
y轴方向的错切变换
public class FmMatrix extends Fragment implements View.OnClickListener {
private Button translateBtn, skewBtn, rotateBtn, scaleBtn;
private ImageView imageView;
private Bitmap picture;
private Bitmap product;
private Canvas mCanvas;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fm_matrix, container, false);
translateBtn = (Button) view.findViewById(R.id.translate);
skewBtn = (Button) view.findViewById(R.id.skew);
rotateBtn = (Button) view.findViewById(R.id.rotate);
scaleBtn = (Button) view.findViewById(R.id.scale);
imageView = (ImageView) view.findViewById(R.id.imageview);
picture = BitmapFactory.decodeResource(getResources(), R.drawable.cat);
product = Bitmap.createBitmap(picture.getWidth() * 2, picture.getHeight() * 2,
Bitmap.Config.ARGB_8888);
imageView.setImageBitmap(picture);
mCanvas = new Canvas(product);
translateBtn.setOnClickListener(this);
skewBtn.setOnClickListener(this);
rotateBtn.setOnClickListener(this);
scaleBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
mCanvas.drawColor(Color.WHITE);
switch (v.getId()) {
case R.id.translate:
Matrix translateMatrix = new Matrix();
translateMatrix.setTranslate(100.0f, 100.0f);
mCanvas.drawBitmap(picture, translateMatrix, null);
break;
case R.id.skew:
Matrix skewMatrix = new Matrix();
skewMatrix.setSkew(0.0f, 0.8f);
mCanvas.drawBitmap(picture, skewMatrix, null);
break;
case R.id.rotate:
Matrix rotateMatrix = new Matrix();
rotateMatrix.setRotate(60.0f, picture.getWidth() / 2 + imageView.getPaddingLeft(),
picture.getHeight() / 2 + imageView.getPaddingTop());
mCanvas.drawBitmap(picture, rotateMatrix, null);
break;
case R.id.scale:
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(0.5f, 0.5f);
mCanvas.drawBitmap(picture, scaleMatrix, null);
break;
}
imageView.setImageBitmap(product);
}
}