图像特效

图像特效按照处理对象一般可以分为颜色特效和图形特效两类,下面分别进行介绍。

颜色特效

在Android系统中,通常使用Bitmap(位图)的数据结构来表示一张图片,它包含了图片的所有数据。Bitmap由点阵和颜色值组成,点阵是图片像素的矩阵,其中每个元素对应图片的一个像素;颜色值(ARGB)分别对应透明度、红、绿、蓝四个分量,它们共同决定每个像素点的颜色。

进行颜色处理时,通常使用以下三个角度来描述一个图像:

  • 色调——物体的颜色
  • 饱和度——颜色的纯度,从0(灰)到100%(饱和)进行描述
  • 亮度——颜色的明暗程度

颜色矩阵——ColorMatrix

Android中使用颜色矩阵(ColorMatrix)处理图像的颜色效果,ColorMatrix是一个4x5的矩阵,对于图像的每个像素点,都有一个4x1的颜色分量矩阵保存颜色的RGBA值。

具体对图像的颜色进行处理时,会通过一定的算法得到一个对应效果的ColorMatrix,接着把这个矩阵分别与每个像素点的颜色分量矩阵进行乘法运算得到新的颜色分量矩阵,最后为每个像素点设置新的颜色分量矩阵,从而达到颜色特效的处理效果,具体过程如下所示:

颜色矩阵处理过程

根据上述过程,可以得到:

颜色处理结果

根据处理结果可以知道,在4x5的ColorMatrix中:

  • 第一行r1 g1 b1 a1 o1的值决定新颜色值中的R(红色)
  • 第二行r2 g2 b2 a2 o2的值决定新颜色值中的G(绿色)
  • 第三行r3 g3 b3 a3 o3的值决定新颜色值中的B(蓝色)
  • 第四行r4 g4 b4 a4 o4的值决定新颜色值中的A(透明度)
  • 第五列o1 o2 o3 o4的值决定新颜色值中每个分量的偏移量(offset)

因此,对图像的颜色进行处理时,通常有两种方法:一个是改变颜色的offset;另一个是改变对应RGBA值的系数。

通过颜色矩阵进行颜色处理

改变ColorMatrix方式通常有两种:一种是通过系统API修改;另一种是利用经典算法直接创建。

通过系统API修改

通过系统API修改颜色矩阵可以改变图像的色调、饱和度和亮度。

  • 色调
    使用setRotate(int axis, float degrees)方法设置颜色矩阵的色调。axis使用0,1,2来表示Red、Green、Blue三种颜色的处理;degrees表示处理的具体值。

  • 饱和度
    使用setSaturation(float sat)方法设置颜色矩阵的饱和度。当饱和度为0时,图像会变为灰色。

  • 亮度
    使用setScale(float rScale, float gScale, float bScale, float aScale)方法设置颜色矩阵的亮度。其本质是利用三原色以同比例混合会显示出白色的原理。当亮度为0时,图像会变为黑色。

  • 效果叠加
    除了上面三种处理方法外,还可以使用postConcat()方法将不同效果的矩阵进行混合,从而产生叠加效果。

下面的示例代码展示了如何使用上面介绍的方法:

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
public static Bitmap handleImageEffect(Bitmap bm, float hue, 
float saturation, float lum) {
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
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 imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);

paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
canvas.drawBitmap(bm, 0, 0, paint);
return bitmap;
}

利用经典算法创建

图像色彩处理,通常就是研究如何通过某种算法创建颜色矩阵,将其作用到图像上,形成新的色彩风格的图像。下面介绍一些经典算法对应的颜色矩阵。

  • 灰度效果
1
2
3
4
5
6
7
// 灰度效果
float[] colorMatrix = new float[] {
0.33f, 0.59f, 0.11f, 0, 0,
0.33f, 0.59f, 0.11f, 0, 0,
0.33f, 0.59f, 0.11f, 0, 0,
0, 0, 0, 1, 0,
};
  • 图像反转
1
2
3
4
5
6
7
// 图像反转
float[] colorMatrix = new float[] {
-1, 0, 0, 1, 1,
0, -1, 0, 1, 1,
0, 0, -1, 1, 1,
0, 0, 0, 1, 0,
};
  • 怀旧效果
1
2
3
4
5
6
7
// 怀旧效果
float[] colorMatrix = new float[] {
0.393f, 0.769f, 0.189f, 0, 0,
0.349f, 0.686f, 0.168f, 0, 0,
0.272f, 0.534f, 0.131f, 0, 0,
0, 0, 0, 1, 0,
};
  • 去色效果
1
2
3
4
5
6
7
// 去色效果
float[] colorMatrix = new float[] {
1.5f, 1.5f, 1.5f, 0, -1,
1.5f, 1.5f, 1.5f, 0, -1,
1.5f, 1.5f, 1.5f, 0, -1,
0, 0, 0, 1, 0,
};
  • 高饱和度
1
2
3
4
5
6
7
// 高饱和度
float[] colorMatrix = new float[] {
1.438f, -0.122f, -0.016f, 0, -0.03f,
-0.062f, 1.378f, -0.016f, 0, 0.05f,
-0.062f, -0.122f, 1.483f, 0, -0.02f,
0, 0, 0, 1, 0,
};

像素点分析

除了通过ColorMatrix改变图像的颜色,还可以直接改变每个像素点的ARGB值来改变图像的颜色,这种处理方式相对更加精确。用这种方式处理图片时,需要注意原始图片是不可变的(mutable),需要复制原始图片进行处理。

Android中使用Bitmap.getPixels(int[] pixels, int offset, int stride,int x, int y, int width, int height)方法获取图片的像素点的颜色值:

  • pixels: 接收位图像素点颜色值的数组
  • offset: 写入到pixels[]中的第一个像素索引值
  • stride: pixels[]的行间距
  • x: 从bitmap中读取的第一个像素的x坐标值
  • y: 从bitmap中读取的第一个像素的y坐标值
  • width: 每行的像素个数
  • height: 读取的行数

修改图像像素点的颜色值的关键代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取图片像素点的颜色值数组
bitmap.getPixels(oldPx, 0, bitmap.getWidth(),
0, 0, bitmap.getWidth(), bitmap.getHeight());

// 获取每个像素点具体的ARGB值
int color = oldPx[i];
a = Color.alpha(color);
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);

// 根据图像处理算法修改像素点的ARGB值
rn = (int) (0.393 * r + 0.322 * g + 0.189 * b);
gn = (int) (0.393 * r + 0.322 * g + 0.189 * b);
bn = (int) (0.393 * r + 0.322 * g + 0.189 * b);
newPx[i] = Color.argb(a, rn, gn, bn);

// 将修改后的颜色数组设置到位图上,完成颜色处理
bitmap.setPixels(newPx, 0, bitmap.getWidth(),
0, 0, bitmap.getWidth(), bitmap.getHeight());

通过像素点进行颜色处理

通过Pixels进行颜色处理,就是通过特定的算法改变每个像素点的颜色值,从而得到相应的处理效果。下面介绍一些常用的处理算法。

  • 底片效果

处理算法:

1
2
3
r = 255 - r;
g = 255 - g;
b = 255 - b;

示例代码:

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
public static Bitmap handleImagePixels(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int color;
int a, r, g, b;

Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];
int[] newPx = new int[width * height];

// 获取图片像素点的颜色值数组
bitmap.getPixels(oldPx, 0, width, 0, 0, width, height);

for (int i = 0; i < width * height; i++) {
// 获取每个像素点具体的ARGB值
color = oldPx[i];
a = Color.alpha(color);
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);

// 根据图像处理算法修改像素点的ARGB值
r = 255 - r;
g = 255 - g;
b = 255 - b;
if (r > 255) r = 255;
if (r < 0) r = 0;
if (g > 255) g = 255;
if (g < 0) g = 0;
if (b > 255) b = 255;
if (b < 0) b = 0;
newPx[i] = Color.argb(a, r, g, b);
}

// 将修改后的颜色数组设置到位图上,完成颜色处理
bmp.setPixels(newPx, 0, width, 0, 0, width, height);
return bmp;
}
  • 老照片效果

处理算法:

1
2
3
rn = (int) (0.393 * r + 0.769 * g + 0.189 * b);
gn = (int) (0.349 * r + 0.686 * g + 0.168 * b);
bn = (int) (0.272 * r + 0.534 * g + 0.131 * b);

示例代码和上面类似。

图形特效

可以将一张图像的形状进行处理,达到某种图形上的效果。Android中提供的常用处理方式有:平移、旋转、缩放和错切(skew),除了常用的处理方式,还可以通过变形矩阵自定义图形变换。

变形矩阵——Matrix

类似于颜色矩阵,Android中使用变形矩阵(Matrix)处理图像的图形变换,变形矩阵是一个3x3的矩阵,对于图像的每个像素点,都有一个3x1的位置矩阵保存其X,Y的坐标值。

具体进行图形变换时,会将变形矩阵与每个像素的位置矩阵相乘,为每个像素设置新的位置,从而达到图像变换效果,具体过程如下所示:

变形矩阵处理过程

根据上述过程,可以得到:

变形处理结果

通常情况下为了保证 1 = gX + hY + i 成立,会令 g = h = 0,i = 1,这样,在处理图形变换时,只需要关注其它几个参数即可。

  • 平移变换(Translate)

平移变换就是将所有像素点的坐标值进行平移,其变换过程如下:

平移变换过程

  • 旋转变换(Rotate)

旋转变换是将像素点围绕一个中心点进行选择,以原点为中心旋转一定角度的变换过程如下:

旋转变换过程

述变换过程是以坐标原点为中心旋转的,如果以任意点O为中心旋转通常需要:将坐标原点平移到O点;以原点为中心旋转;将坐标原点还原。

  • 缩放变换(Scale)

缩放变换是对于多个像素点才会有效果,将图像沿X轴和Y轴按一定比例缩放的变换过程如下:

缩放变换过程

  • 错切变换(Skew)

错切变换是将所有像素点的X坐标(或Y坐标)保持不变,而对应的Y坐标(或X坐标)按比例发生平移,并且平移的大小和该点到X轴(或Y轴)的垂直距离成正比。

错切变换的示意图如下所示:

错切变换示意图

错切变换过程如下:

错切变换过程

通过变形矩阵进行图形处理

与颜色矩阵一样,变形矩阵也提供了相关API简化图形变换,其中Matrix提供的方法是进行2D变换的,而Camera提供的方法还可以进行3D变换。

通过Matrix进行2D变换

Matrix提供的变换API有:

  • Matrix.setTranslate():平移变换
  • Matrix.setRotate():旋转变换
  • Matrix.setScale():缩放变换
  • Matrix.setSkew():错切变换
  • Matrix.setPolyToPoly() setRectToRect() setSinCos():自定义变换
  • Matrix.preXXX()和Matrix.postXXX():叠加变换

当设置完Matrix后,把Matrix设置到Canvas上有两个方法:

  • Canvas.setMatrix(matrix):用Matrix直接替换Canvas当前的变换矩阵,即抛弃Canvas当前的变换,改用Matrix的变换
  • Canvas.concat(matrix):用Canvas当前的变换矩阵和Matrix相乘,即基于Canvas当前的变换,叠加上Matrix中的变换

通过Camera进行3D变换

Camera提供的3D变换有三类:

  • Camera.rotateXXX():将虚拟相机的坐标轴沿X、Y、Z轴三个方向进行旋转,可以用来实现翻转效果。同时要注意,虚拟相机旋转的轴心是坐标原点。
  • Camera.translate(x, y, z):将虚拟相机的坐标轴沿X、Y、Z轴三个方向进行移动,可以使用Canvas的translate()和scale()方法代替。
  • Camera.setLocation(x, y, z):设置虚拟相机的位置,可以用来控制投影的图像大小。

当设置完Camera后,把Camera的Matrix设置到Canvas需要使用Camera.applyToCanvas(canvas)方法。

示例代码如下:

1
2
3
4
5
6
7
8
canvas.translate(centerX, centerY); // 旋转之前把绘制内容移动到轴心(原点)

camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore(); // 恢复 Camera 的状态

canvas.translate(-centerX, -centerY); // 旋转之后把投绘制内容动回来

像素块分析

类似于颜色处理,除了使用矩阵的方式,还可以使用基于像素的方式进行图形处理。具体处理的时候需要使用drawBitmapMesh()方法,该方法像一张网格,把图像分成一个个像素块,通过改变像素块间节点的坐标位置,使整个图像的图形发生变化。

该方法具体形式如下:
drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)

其中关键参数有:

  • bitmap:需要形变的图像
  • meshWidth:横向网格数目
  • meshHeight:纵向网格数目
  • verts:网格交叉点坐标数组
  • vertOffset:绘制的时候,verts数组跳过坐标对数目

其中最重要的参数是verts数组,drawBitmapMesh()方法绘制前会将图像分成多个像素块,假如在图像上的横向和纵向各画N(N > 1,线条从图像边缘开始)条线,这些线会交叉组成NxN个的点,那么,每个点的坐标值以 x1, y1, x2, y2, … , xn, yn 的形式保存在verts数组中,drawBitmapMesh()方法就是通过改变这些坐标值,重新定位每个像素块,从而改变图像形状。

drawBitmapMesh()方法基本上可以实现所有的图像特效,其使用关键在于计算去,确定新的交叉点坐标。

通过像素块进行图形处理

这里使用drawBitmapMesh()方法使一张图片产生“旗帜飞扬”的效果。

要达到这个效果,需要保持交叉点的横坐标不变,并且交叉点纵坐标呈现一个三角函数的周期变化。

  • 获取交叉点坐标

基本原理是通过遍历所有的交叉线,按比例获取交叉点坐标。

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
// 网线数目 = 网格数目 + 1
private static int HEIGHT = 19; // 纵向网格数目
private static int WIDTH = 29; // 横向网格数目

/**
* 获取原始交叉点坐标
*/
private float[] getBitmapVerts(Bitmap bitmap) {
float[] verts = new float[(WIDTH + 1) * (HEIGHT + 1) * 2]; // 坐标值数目 = 2 * 交点数目
float bitmapHeight = bitmap.getHeight();
float bitmapWidth = bitmap.getWidth();
int index = 0;

for (int y = 0; y <= HEIGHT; y++) {
float fy = bitmapHeight * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = bitmapWidth * x / WIDTH;
verts[index * 2] = fx;
verts[index * 2 + 1] = fy + 100; // 为了避免图像偏移后被遮挡,将纵坐标+100,使图像下移
index++;
}
}

return verts;
}
  • 改变交叉点坐标值

横坐标不变,使用正弦函数改变纵坐标值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据正选函数修改交叉点坐标
*
* @param verts 交叉点坐标
* @param K 相位,用于动态改变偏移量,实现动态效果
* @param A 振幅,用于改变偏移幅度
*/
private void flagWave(float[] verts, float K, float A) {
for (int j = 0; j <= HEIGHT; j++) {
for (int i = 0; i <= WIDTH; i++) {
float offsetY = (float) Math.sin(((float) i / WIDTH + K) * 2 * Math.PI);
int index = 2 * (j * (WIDTH + 1) + i);
verts[index] += 0;
verts[index + 1] += offsetY * A;
}
}
}
  • 根据交叉点绘制图像

这样绘制可以得到一个静态的效果。

1
2
3
4
5
6
7
8
9
10
private float mVerts[] = getBitmapVerts(mBitmap);
private static float K = 0f, A = 50f;

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

flagWave(mVerts, K, A);
canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, mVerts, 0, null, 0, null);
}
  • 重绘产生动态效果

每次重绘时,通过改变正弦函数的相位来改变纵坐标的偏移量。

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

flagWave(mVerts, K, A);
canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, mVerts, 0, null, 0, null);

// 改变相位后重绘,产生飘扬效果
K += 0.1f;
postInvalidateDelayed(50); // 延时重绘,避免飘扬太快
}
坚持原创技术分享,您的支持将鼓励我继续创作!