IBigerBiger的成长之路

移动端滤镜开发(五)普通滤镜开发

上一篇文章对简单滤镜实现有一定的讲解,那么这一篇则是对图像处理更加深层次的说明,对于一张图片怎么处理起来效果会看起来更好呢?我想大部分人首先就会想到PS软件,确实对于图像的处理PS有很多的功能,一般处理图片呢会用到下面这些工具


图1 图片处理

通过这些工具可以对图片各种修改,当然除了这种处理之外难免还会出现加上水印或者边框这种类型的需求,那么这种修改一般用PS中的图层混合,ps中的图层混合如下


图2 图层混合模式

接下来就来分别说明下关于图层混合与图片处理

一.图层混合

所谓图层混合模式就是指一个层与其下图层的色彩叠加方式,在这之前我们所使用的是正常模式,除了正常以外,还有很多种混合模式,它们都可以产生迥异的合成效果。

首先看正常模式


图2 正常模式

接下来试下不同的效果,如柔光模式


图3 柔光模式

大家也可以对其他的效果也可以进行更多的尝试与了解

1.图片混合模式介绍

上面的Ps图层混合的图我们看到很多混合模式,那么这里就一一介绍下这些混合模式


1.溶解模式

溶解模式下混合色的不透明度及填充都是100%的话,我们就看不到基色图层。降低混合色图层的不透明度后,我们就会发现结果色中出现了很多细小的颗粒。这些颗粒会随着混合色的不透明度变化。不透明度越低混合色图层就被溶解的越多。剩下的部分就越少。不透明度越高混合色图层被溶解的部分就越少,剩下的部分就越多,结果色就越接近混合色。

2.变暗模式

变暗混合模式下,它会把混合色与基色进行对比,分别选择R,G,B三组数值中最小的数值,也就是最暗的颜色作为结果色的数值。

B<=A: C=B
B>=A: C=A

3.正片叠底

正片叠底混合原理:它是按照混合色与基色中的各R,G,B值计算,计算公式:结果色R=混合色R * 基色R / 255,G值与B值同样的方法计算。最后得到的R,G,B值就是结果色的颜色。

4.颜色加深

颜色加深可以快速增加图片的暗部。它的计算公式:结果色 = (基色 + 混合色 - 255)* 255 / 混合色。其中(基色 + 混合色 - 255)如果出现负数就直接归0。因此在基色与混合色都较暗的时候都是直接变成黑色的。这样结果色的暗部就会增加。整体效果看上去对比较为强烈。

5.线性加深

线性加深的计算公式是:结果色 = 基色 + 混合色 - 255,如果基色 + 混合色的数值小于255,结果色就为0。由这个公式可以看出,画面暗部会直接变成黑色。因此画面整体会更暗。白色与基色混合得到基色,黑色与基色混合得到黑色。

6.深色模式

深色模式是通过计算混合色与基色的所有通道的数值,然后选择数值较小的作为结果色。因此结果色只跟混合色或基色相同,不会产生出另外的颜色。白色与基色混合色得到基色,黑色与基色混合得到黑色。深色模式中,混合色与基色的数值是固定的,我们颠倒位置后,混合色出来的结果色是没有变化的。

7.变亮模式

变亮模式跟变暗模式是相对的,它是通过混合色与基色的相关数值进行比较,选择较大的数值作为结果色。因此结果色会更亮,同时颜色也会变化。

8.滤色模式

滤色模式与正片叠底模式相对。它的计算公式是:255 - 混合色的补色 * 基色补色 / 255。得到的数据会比混合及基色更大,因此结果色会更亮。从计算公式也可以看出基色或混合色任何一项为255也就是白色,结果色数值就是255为白色。任何一项数值为0,也就是为黑色的话,结果色就跟数值不为0的一致。

9.颜色减淡

颜色减淡是通过混合色及基色的各通道颜色值进行对比,减少二者的对比度使基色的变亮来反映混合色。它的计算公式:结果色 = 基色 + (混合色 * 基色) / (255 - 混合色)。混合色为黑色,结果色就等于基色,混合色为白色结果色就为白色。基色为黑色结果色就为黑色。

10.线性减淡

线性减淡是通过查看每个通道的颜色信息,并通过增加亮度使基色变亮以反映混合色。它的计算公式:结果色 = 基色 +混合色,其中基色与混合色的数值大于255,系统就默认为最大值也就是255。
由公式可以分析出混合色为黑色结果色就等于基色,混合色为白色结果色就为白色。基色也一样。我们颠倒混合色及基色的位置,结果色也不会变化。

11.浅色模式

浅色模式是通过计算混合色与基色所有通道的数值总和,哪个数值大就选为结果色。因此结果色只能在混合色与基色中选择,不会产生第三种颜色。与深色模式刚好相反。

12.叠加模式

叠加模式比较特别,它是通过分析基色个通道的数值,对颜色进行正片叠加或滤色混合,结果色保留基色的明暗对比,因此结果色以基色为主导。
计算公式:
基色 < = 128:结果色 = 混合色 基色 / 128;基色 > 128:结果色 = 255 - (255 - 混合色) (255 - 基色) / 128。从公式可以看出,结果色会根据基色的颜色数值选择不同的计算公式。

13.柔光模式

柔光模式是根据混合色的通道数值选择不同的公式计算混合色。数值大于128的时候,结果色就比基色稍亮;数值小于或等于128,结果色就比基色稍暗。柔光模式是以基色为主导,混合色只相应改变局部明暗。其中混合色为黑色,结果色不会为黑色,只比结果色稍暗,混合色为中性色,结果色跟基色一样。
计算公式:
混合色 <=128:结果色 = 基色 + (2 混合色 - 255) (基色 - 基色 基色 / 255) / 255;
混合色 >128: 结果色 = 基色 + (2
混合色 - 255) (Sqrt(基色/255)255 - 基色)/255。

14.强光模式

强光模式跟叠加模式十分类似,只是在计算的时候需要通过混合色来控制,混合色的数值小于或等于128的时候,颜色会变暗;混合色的数值大于128的时候,颜色会变亮。混合色为白色,结果色就为白色;混合色为黑色,结果为黑色。混合色起到主导作用。
计算公式:
混合色 <= 128:结果色 = 混合色 基色 / 128;
混合色 > 128 :结果色 = 255 - (255 - 混合色)
(255 - 基色) / 128.

15.亮光模式

亮光模式是通过增加或减少对比度是颜色变暗或变亮,具体取决于混合色的数值。混合色比中性灰色暗,结果色就相应的变暗,混合色比中性灰色亮,结果色就相应的变亮。有点类似颜色加深或颜色减淡。
计算公式:
A—基色;B—混合色
C=A-(255-A)(255-2B)/2B 当混合色>128时
C=A+[A
(2B-255)]/[255-(2B-255)

16.线性光模式

线性光模式通过减少或增加亮度,来使颜色加深或减淡。具体取决于混合色的数值。混合色数值比中性灰色暗的时候进行相应的加深混合;混合色的数值比中性灰色亮的时候进行减淡混合。这里的加深及减淡时线性加深或线性减淡。
计算公式:结果色 = 2 * 混合色 + 基色 -255。数值大于255取255。

17.点光模式

点光模式会根据混合色的颜色数值替换相应的颜色。如果混合色数值小于中性灰色,那么就替换比混合色亮的像素;相反混合色的数值大于中性灰色,则替换比混合色暗的像素,因此混合出来的颜色对比较大。
计算公式:
基色 < 2 混合色 - 255:结果色 = 2 混合色 - 255;
2 混合色 - 255 < 基色 < 2 混合色 :结果色 = 基色;
基色 > 2 混合色:结果色 = 2 混合色。

18.实色混合

实色混合是把混合色颜色中的红、绿、蓝通道数值,添加到基色的RGB值中。结果色的R、G、B通道的数值只能是255或0。因此结构色只有一下八种可能:红、绿、蓝、黄、青、洋红、白、黑。由此看以看出结果色是非常纯的颜色。
计算公式:
混合色 + 基色 < 255:结果色 = 0 ;混合色 + 基色 >= 255:结果色 = 255。

19.差值模式

差值模式通过查看每个通道的数值,用基色减去混合色或用混合色减去基色。具体取决于混合色与基色那个通道的数值更大。白色与任何颜色混合得到反相色,黑色与任何颜色混合颜色不变。
计算公式:
结果色 = 绝对值(混合色 - 基色)

20.排除模式

排除模式是跟差值模式非常类似的混合模式,只是排除模式的结果色对比度没有差值模式强。白色与基色混合得到基色补色,黑色与基色混合得到基色。
计算公式:
结果色 = (混合色 + 基色) - 混合色 * 基色 / 128。

21.减去模式

减去模式查看各通道的颜色信息,并从基色中减去混合色。如果出现负数就剪切为零;与基色相同的颜色混合得到黑色;白色与基色混合得到黑色;黑色与基色混合得到基色。
计算公式:
结果色 = 基色 - 混合色。

22.划分模式

划分模式查看每个通道的颜色信息,并用基色分割混合色。基色数值大于或等于混合色数值,混合出的颜色为白色。基色数值小于混合色,结果色比基色更暗。因此结果色对比非常强。白色与基色混合得到基色,黑色与基色混合得到白色。
计算公式:
结果色 = (基色 / 混合色) * 255。

23.色相模式

输出图像的色调为上层,饱和度和亮度保持为下层。对于灰色上层,结果为去色的下层。

24.饱和度模式

输出图像的饱和度为上层,色调和亮度保持为下层。

25.颜色模式

输出图像的亮度为下层,色调和饱和度保持为上层。

26.明度模式

输出图像的亮度为上层,色调和饱和度保持为下层。

2.OpenGl实现混合模式


这里还是以之前的OpenGl显示图片为例子,但是由于图层混合模式需要两张图片,所以对之前的代码进行小的修改

首先增加新的图片纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int[] mTexNames2 = new int[2];
GLES20.glGenTextures(1, mTexNames2, 1);
bitmap = BitmapFactory.decodeResource(mResources, R.drawable.p_300px2);
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexNames2[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_REPEAT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();

注意我这里glActiveTexture与之前不同,为一个新的纹理,后面绘制新的图片也需要用这个纹理

接下来获取 shader 代码中的新的变量索引

1
2
3
4
5
6
7
8
9
private int mTexSamplerHandle2;
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
...
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
mTexSamplerHandle2 = GLES20.glGetUniformLocation(mProgram, "s_texture2");
...
}

最后绘制

1
2
3
4
5
6
@Override
public void onDrawFrame(GL10 gl) {
...
GLES20.glUniform1i(mTexSamplerHandle2, 1);
...
}

接下来就是需要修改片段着色器程序了根据不同的模式来创建不同的程序

这里我们选取几个效果实现

(1).正常模式

正常模式下,则为第二张叠加在第一张图片上面,对于我们输出的颜色需要从图片一和图片二共同获取,所以这时候第二张图片透明部分则显示为第一张图片,不透明部分显示第二张图片

接下来上片段着色器程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
varying highp vec2 v_texCoord;
uniform sampler2D s_texture;
uniform sampler2D s_texture2;
void main() {
mediump vec4 textureColor = texture2D(s_texture, v_texCoord);
mediump vec4 textureColor2 = texture2D(s_texture2, v_texCoord);
vec4 outputColor;
outputColor.r = textureColor2.r + textureColor.r * textureColor.a * (1.0 - textureColor2.a);
outputColor.g = textureColor2.g + textureColor.g * textureColor.a * (1.0 - textureColor2.a);
outputColor.b = textureColor2.b + textureColor.b * textureColor.a * (1.0 - textureColor2.a);
outputColor.a = textureColor2.a + textureColor.a * (1.0 - textureColor2.a);
gl_FragColor = outputColor;
}

代码就不做过多解释了,接下来运行如下


图5 正常模式

(2).柔光模式

柔光模式是根据混合色的通道数值选择不同的公式计算混合色。数值大于128的时候,结果色就比基色稍亮;数值小于或等于128,结果色就比基色稍暗。柔光模式是以基色为主导,混合色只相应改变局部明暗。其中混合色为黑色,结果色不会为黑色,只比结果色稍暗,混合色为中性色,结果色跟基色一样。
计算公式:
混合色 <=128:结果色 = 基色 + (2 混合色 - 255) (基色 - 基色 基色 / 255) / 255;
混合色 >128: 结果色 = 基色 + (2
混合色 - 255) (Sqrt(基色/255)255 - 基色)/255。

接下来上片段着色器程序代码

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
varying highp vec2 v_texCoord;
uniform sampler2D s_texture;
uniform sampler2D s_texture2;
void main() {
mediump vec4 textureColor = texture2D(s_texture, v_texCoord);
mediump vec4 textureColor2 = texture2D(s_texture2, v_texCoord);
float ra;
if(textureColor2.r <= 0.5){
ra = textureColor.r + (2.0 * textureColor2.r - 1.0) * (textureColor.r - textureColor.r * textureColor.r);
}else{
ra = textureColor.r + (2.0 * textureColor2.r - 1.0) * (sqrt(textureColor.r) - textureColor.r);
}
float ga;
if(textureColor2.g <= 0.5){
ga = textureColor.g + (2.0 * textureColor2.g - 1.0) * (textureColor.g - textureColor.g * textureColor.g);
}else{
ga = textureColor.g + (2.0 * textureColor2.g - 1.0) * (sqrt(textureColor.g) - textureColor.g);
}
float ba;
if(textureColor2.b <= 0.5){
ba = textureColor.b + (2.0 * textureColor2.b - 1.0) * (textureColor.b - textureColor.b * textureColor.b);
}else{
ba = textureColor.b + (2.0 * textureColor2.b - 1.0) * (sqrt(textureColor.b) - textureColor.b);
}
gl_FragColor = vec4(ra, ga, ba, 1.0);
}

运行如下


图6 柔光模式

(3).线性加深模式

线性加深的计算公式是:结果色 = 基色 + 混合色 - 255,如果基色 + 混合色的数值小于255,结果色就为0。由这个公式可以看出,画面暗部会直接变成黑色。因此画面整体会更暗。白色与基色混合得到基色,黑色与基色混合得到黑色。

接下来上片段着色器程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
varying highp vec2 v_texCoord;
uniform sampler2D s_texture;
uniform sampler2D s_texture2;
void main() {
mediump vec4 textureColor = texture2D(s_texture, v_texCoord);
mediump vec4 textureColor2 = texture2D(s_texture2, v_texCoord);
gl_FragColor = textureColor + textureColor2 - vec4(1.0);
}

运行如下:


Paste_Image.png

更多的效果大家可以去自己尝试

二.图片处理

关于图层混合我们已经用GLSL语言去实现并运行实现了需要的效果了,接下来就是关于图片处理了,上面的图我们看到图片处理的方法太多了,我们这里首先先挑两个处理方法来说明

(1).曲线调整

曲线调整是Photoshop的最常用的重要功能之一,所以了解他的实现与原理是很有必要的。

首先看下PhotoShop中的曲线调整


图1 曲线调整框


图2 曲线调整

图3 曲线调整对应图片的变化图片
1.曲线调整原理


对于一个RGB图像, 可以对R, G, B 通道进行独立的曲线调整,即,对三个通道分别使用三条曲线(Curve)。还可以再增加一条曲线对 三个通道进行整体调整。 因此,对一个图像,可以用四条曲线调整。最终的结果,是四条曲线调整后合并产生的结果。

我们首先对单一的通道调整,如图


图4 单通道曲线调整

图中,横轴是输入,比左到右分别表示0到255. 纵轴是输出,从下到上分别表示0到255.

该曲线由三个点定义,座标分别为: 点1(0,0), 点2(85,143),点3(255,255)
点1和点3是默认产生的,点2是我们新增加的。这样这三个点就生成了一条曲线

所以图片中红色通道的值都要按这条曲线来生成新的值,蓝绿通道的值不发生改变

比如之前一个点的RGB值为(85,125,125),那么经过红色通道的调整后,这个点的RGB值为(143,125,125)

同理于蓝色通道,绿色通道以及整个RGB通道的曲线调整

到这里大家基本了解了曲线调整的流程了,那么接下来就需要知道这条曲线是如何生成的

2.曲线生产

结合上面的图,我们可以发现这条曲线的特点,仅需要定义几个控制点,就可以定义一条平滑的曲线,且曲线同时通过所有控制点。根据这个特性我们可以判断这里的曲线是三次样条插值Cubic Spline Interpolation(简称Spline插值),生成曲线时,只需要给出几个控制点,调用曲线生成函数即可,然后可以根据这个函数来生成新的RGB值。

三次样条函数:

定义:函数S(x)∈C2[a,b] ,且在每个小区间[ xj,xj+1 ]上是三次多项式,其中a =x0 <x1<…< xn= b 是给定节点,则称S(x)是节点x0,x1,…xn上的三次样条函数。若在节点x j 上给定函数值Yj= f (Xj).( j =0, 1, , n) ,并成立S(xj ) =yj .( j= 0, 1, , n) ,则称S(x)为三次样条插值函数。
实际计算时还需要引入边界条件才能完成计算。边界通常有自然边界(边界点的二阶导为0),夹持边界(边界点导数给定),非扭结边界(使两端点的三阶导与这两端点的邻近点的三阶导相等)。

3.曲线调整模拟

要模拟出曲线调整的效果,首先要实现三次样条函数

这个我就不做过多的介绍了,网上也有不少例子

上代码

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
public class Chazhi{
int[][] range = new int[50][2];
int number = 0;
int choice = 0;
public double Thrice(int inputx)//三次自然样条插值算法
{
double a[] = new double[50];
double b[] = new double[50];
double c[] = new double[50];
double d[] = new double[50];
double h[] = new double[50];
double s = 0;
double x = 0;
int temp = 0;
double s1[] = new double[50];
double s2[] = new double[50];
for(int i = 0; i < number - 1; i ++)
{
for(int j = number - 2; j >= i; j --)
{
if(range[j+1][0]<range[j][0])
{
temp = range[j + 1][0];
range[j + 1][0] = range[j][0];
range[j][0] = temp;
temp = range[j + 1][1];
range[j + 1][1] = range[j][1];
range[j][1] = temp;
}
}
}
for(int k = 0; k <= number - 2; k ++)
{
h[k] = range[k+1][0] - range[k][0];
}
a[1] = 2 * (h[0] + h[1]);
for(int k = 2; k <= number - 2; k ++)
{
a[k] = 2 * (h[k-1] + h[k]) - h[k-1] * h[k-1] / a[k-1];
}
for(int k = 1; k <= number - 1; k ++)
{
c[k] = (range[k][1] - range[k - 1][1])/h[k-1];
}
for(int k = 1; k <= number - 2; k ++)
{
d[k] = 6*(c[k+1]-c[k]);
}
b[1] = d[1];
for(int k = 2; k <= number - 2; k ++)
{
b[k] = d[k]-b[k-1]*h[k-1]/a[k];
}
s2[number-2] = b[number - 2]/a[number - 2];
for(int k = number - 3; k >=1; k --)
{
s2[k] = (b[k]-h[k]*s2[k+1])/a[k];
}
s2[0]=0;
s2[number - 1]=0;
if(inputx == 255 || inputx == 0){
return inputx;
}
for(int k = 0; k <= number - 2; k ++)
{
if(inputx < range[k + 1][0]){
s1[k] = c[k + 1] - s2[k + 1] * h[k] / 6 - s2[k] * h[k] / 3;
s = range[k][1] + s1[k] * (inputx - range[k][0]) + s2[k] * (inputx - range[k][0]) * (inputx - range[k][0]) / 2 +
(s2[k + 1]-s2[k]) * (inputx - range[k][0]) * (inputx - range[k][0]) * (inputx - range[k][0]) / 6 / h[k];
break;
}
}
return s;
}
}

使用如下

1
2
3
4
5
6
7
8
9
10
11
12
Chazhi chazhi = new Chazhi();
chazhi.range[chazhi.number][0] = 0;
chazhi.range[chazhi.number][1] = 0;
chazhi.number ++;
chazhi.range[chazhi.number][0] = 114;
chazhi.range[chazhi.number][1] = 176;
chazhi.number ++;
chazhi.range[chazhi.number][0] = 255;
chazhi.range[chazhi.number][1] = 255;
chazhi.number ++;
r = Math.abs((int) chazhi.Thrice(r));

这里只是对红色通道进行了曲线模拟并且选取的点为(0,0),(114,176),(255,255),模拟后,我们看生成的图片是什么效果


图5 模拟效果

所以经过我们模拟后的效果与Photoshop中的效果一致

(2).色阶调整

上面说了曲线调整,接下来看看PhotoShop的色阶调整


图1 色阶调整

我们尝试下去修改输入色阶

如下


图2 色阶调整输入色阶

可以看到图片的变化如下


图3 色阶调整对应图片的变化

所以我们来了解下色阶调整带来的变化

1.色阶调整的原理

色阶是什么:色阶就是用直方图描述出的整张图片的明暗信息

我们继续看色阶调整的图


图4 色阶调整

从左至右是从暗到亮的像素分布,黑色三角代表最暗地方(纯黑),白色三角代表最亮地方(纯白)。灰色三角代表中间调。

每一个色阶定义有两组值:
一组是输入色阶值,包含黑灰白三个值, 上图中:黑点值为0,灰点为1.00,白点为255
另一组是输入色阶值,包含黑白两个值,上图中:输出色阶黑为0,白为255

对于一个RGB图像, 可以对R, G, B 通道进行独立的色阶调整,即,对三个通道分别使用三个色阶定义值。还可以再对 三个通道进行整体色阶调整。 因此,对一个图像,可以用四次色阶调整。最终的结果,是四次调整后合并产生的结果。

我们以红色通道为例,如下


图5 红色通道色阶调整

这里:输入色阶值为:黑15,灰1.5,白200   输出色阶值为:黑12,白233

则色阶调整的实现是:
当输入值<黑点值(15)时,全部变为输出色阶的黑值(12)。 
当输入值>白点(200)时,全部变为输出色阶的白值(233)。
当输入值介于黑值与白值之间(15-200)时,则结合灰度系数,按比例重新计算,变为一个新的值。

同理于蓝色通道,绿色通道以及整个RGB通道的曲线调整

2.色阶调整模拟

上面已经说明了原理,这里就直接把模拟的代码贴上来了

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
public class LevlesAdjustment{
Levles[] levles;
class Levles{
int Shadow = 0;
double Midtones = 1.00;
int Highlight = 255;
int OutputShadow = 0;
int OutputHighlight = 255;
boolean defined = false;
public int doChange(int index){
int diff = Highlight - Shadow;
int outDiff = (int)(OutputHighlight - OutputShadow);
double coef = 255.0 / diff;
double outCoef = outDiff / 255.0;
double exponent = 1.0 / Midtones;
int v;
if ( index <= Shadow ) {
v = 0;
} else {
v = (int)((index - Shadow) * coef + 0.5);
if (v > 255) v = 255;
}
v = (int)( Math.pow(v / 255.0, exponent) * 255.0 + 0.5 );
return (int) (v * outCoef + OutputShadow + 0.5);
}
}
public LevlesAdjustment(){
levles = new Levles[4];
for(int i = 0; i < levles.length; i ++){
levles[i] = new Levles();
}
levles[0].Shadow = 10;
levles[0].Highlight = 24;
}
public int[] doChange(int[] rgb){
for(int i = 0; i < levles.length; i ++){
calcDefined(levles[i]);
}
for (int i = 0; i < levles.length; i++) {
if ( levles[i].defined ) {
switch (i){
case 0:
rgb[0] = levles[i].doChange(rgb[0]);
break;
case 1:
rgb[1] = levles[i].doChange(rgb[1]);
break;
case 2:
rgb[2] = levles[i].doChange(rgb[2]);
break;
case 3:
rgb[0] = levles[i].doChange(rgb[0]);
rgb[1] = levles[i].doChange(rgb[1]);
rgb[2] = levles[i].doChange(rgb[2]);
break;
}
}
}
return rgb;
}
private void calcDefined(Levles levles){
if ( levles.Shadow != 0 || levles.Midtones != 1.0 || levles.Highlight != 255 ) {
levles.defined = true;
return;
}
levles.defined = false;
}
}

这里是将红色通道的Shadow设置为10,Highlight = 244,接下来看一下运行的效果图


图6 运行效果图

通过与ps处理的效果图对比,显示效果一致

前面几个小点都是对于PhotoShop中常用的处理的图片的方法的原理进行了分析,那么接下来自然就要考虑一下怎么用OpenGl去实现这样的效果呢?

那么第一种方法自然就是模拟出这些处理方法,用glsl语言来实现这些方法,这么做当然是可以的,但是目前这些处理方法大多都是用C来实现的,glsl语言尚未有可以用的轮子,那么自然我们就要去造这个轮子,那我们不禁要思考一下这么做的成本和回报到底是否让我们满意,假设影像的大小为1024x768,那么对于其中的某一项的处理则总共要786432次运算,而且这些运动大多比较复杂,所以这么做其实相对来说并没有那么讨喜,成本相对较大,并且结果并没有达到很令人满意的效果,所以要思考下怎么用别的方式去实现。

看了这么多的处理原理,我相信大家都应该了解每一个像素的点都将原来的RGB值替换了新的RGB值,所以我们可以建一张表,把所有色彩值经过处理之后的结果记录起来,然后把每个像素的色彩值拿去查表,得到处理之后的色彩值,那么我们只要做786432查表动作,这样确实会比上面的运算快上许多。但是问题还是伴随会出现,这个表怎么去获取又是一个问题,毕竟美工不可能一点一点给我们计算这个表,我们自己去算也是不现实的,所以有没有更加高效的方式呢?

答案自然是肯定的,而这一解决方案就是Color Lookup Table(ColorLUT)技术

(3).Color Lookup Table(ColorLUT)技术

上文提到查表的方法时候提到了怎么去获取这个表是一个问题,那其实ColorLUT就是为了解决这个问题的,对于一个处理RGB值的表,我们不需要将所有都记录下来,而是只记下部分的色彩,其他不在表内的色彩则用内插法取得处理后的结果。

因为每个像素的色彩都是由RGB三种颜色组成,因此我们会以三维阵列的方式来储存这张表。如果把三维阵列中的每一个像素想像成三度空间中的一个点,而R、G、B分别代表X、Y、Z的座标,则阵列中的所有像素可以构成一个正立方体。以RGB 24bits为例,由于R、G、B的值为0~255,因此正方体的长宽高为255x255x255。当我们要查某个像素经过处理之后的色彩,只要将该像素处理前的RGB值当做X、Y、Z座标,位于那个位置上的像素则为处理后的颜色。

如图所示


图1 三维空间图

这个图是以4x4x4的正方体X、Y、Z三个方向都只有在0、85、170、255这些位置有资料,如果座标不在这些点上,则必须跟周围的点做内差来得到色彩的近似值。

而这些已有的颜色的值当我们通过PhotoShop进行各种操作时,则会将这些值替换成新的值,而这些新的值则就是上面所述的数组里面的关键值之一了。

我们继续看上面的图毕竟是三维图,我们无法进行处理,所以把z轴的面去出来拼接在一起则会成为一个8x8的图片,这个图片是包含4个4x4的正方形,配上颜色如下图所示:


图2 颜色二维图

以右上角正方形为例,这代表Z=0的平面,而X轴由左至右,Y轴为由上至下,左上角第一个像素代表位于(0,0,0)的点,第二个像素代表位于(85,0,0)的点,以此类推,由于这些像素代表的是未处理前的颜色,因此第一个像素的RGB值为0,0,0,第二的像素的RGB值为85,0,0

接着我们将这张图经过Photoshop降低色彩饱和度,结果如下:


图3 颜色二维图

这张图内的每个像素,代表每个座标点处理之后的色彩。也就是说,如果我们想知道一个RGB值为85,0,0的像素降低饱和度之后的颜色,可从正立方体中位于(85,0,0)的点得知,也就是这张图中左上角第二个像素的颜色,RGB值为68,16,16。

所以我们可以将这一技术运用到我们的滤镜开发中,从而避免上面的尴尬问题,接下来就是实现上面的ColorLUT技术

#####ColorLUT实现

ColorLUT的实现无非就是用GLSL的语言来实现识别上面的颜色二维图方法,这个我就不做过多的介绍了,这个在GPUImage里面已经有实现,我就不重复造轮子了,这里直接拿过来使用,代码如下,自己理解

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
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture2;
uniform lowp float intensity;
void main() {
highp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
highp float blueColor = textureColor.b * 63.0;
highp vec2 quad1;
quad1.y = floor(floor(blueColor) / 8.0);
quad1.x = floor(blueColor) - (quad1.y * 8.0);
highp vec2 quad2;
quad2.y = floor(ceil(blueColor) / 8.0);
quad2.x = ceil(blueColor) - (quad2.y * 8.0);
highp vec2 texPos1;
texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
highp vec2 texPos2;
texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
lowp vec4 newColor1 = texture2D(inputImageTexture2, texPos1);
lowp vec4 newColor2 = texture2D(inputImageTexture2, texPos2);
lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
gl_FragColor = vec4(newColor.rgb, textureColor.w);
}

下面这一张则是初始的颜色二维图


图4 颜色初始二维图

接下来就是来验证下ColorLUT的实际情况了


图5 验证图

用上面的图来进行PS的调整

调整如下:

曲线调整: 红色通道调整 –> 曝光度调整

最后显示效果如下


图6 PS处理图

接下来对上面的初始颜色二维图进行同样的调整,如下


图7 颜色二维图处理

接下来用上面的GLSL语言来验证,运行效果如下


图8 ColorLUT验证

通过运行图与前面的PS效果图对比,我们发现基本一致,所以通过ColorLUT技术解决了滤镜中关于滤镜生产这一大问题,所以只需要视觉同学对图片进行美化后用同样的流程处理一遍颜色二维图,就可以快速的生产大量优质的滤镜了

写在后面的话

这一大幅篇章主要是介绍了如何使用图层混合技术与ColorLUT技术解决了生产力的大问题,这样确实普通的滤镜开发都可以通过这种方式来实现,但是一些特殊的滤镜,比如马赛克滤镜,油画滤镜,粉笔画滤镜,这些效果的实现用ColorLUT技术不能实现,所以我们需要使用其他的方法,那么对于这些特殊滤镜的实现,我们在后面再进行说明,peace~~~