Android OpenGL ES - 反相、曝光、对比度、饱和度、色调滤镜

经过前几篇的博客,我们应该队OpenGL有了基础的认识,那么我们就该来点实践了,它来了,它来了,真正的滤镜它来了

先放效果图

image.png

滤镜的学习是个循序渐进的过程,我们本章先说一下简单的滤镜,也让读者揭开一角相机滤镜神秘面纱

OpenGL ES - 简单滤镜

经过前几篇的博客,我们应该能实现如上图中的原图的滤镜了吧,我对前面的工程也做了一些代码重构,类图如下所示,完整代码请查看文末链接。

我们后面的滤镜代码将只关注于核心的Shader Code部分,因为GL环境部分我们前面几篇博客已经做了非常详细的讲解了。如果有读者不太熟悉,请查看前面的博客。

image.png

默认滤镜

vertex shader

attribute vec4 position;
attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main() {
   gl_Position = position;
   textureCoordinate = inputTextureCoordinate.xy;
}

fragment shader

precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
    gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
};

默认的滤镜我们前面也说过了,使用texture2D函数对纹理进行取样,既OpenGL展示图片

反相

关于图片反相的定义还是很容易理解的,我们已经知道在GL中颜色是用r,g,b,a表示的,r,g,b,a 的范围是0.0f~1.0f,若染色color = vec4(r,g,b,a),则反相的颜色
invert_color = vec4((1.0-color.rgb), color.a);

所以反相的fragment shader 自然而然就是下面了

fragment shader

precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    // textureColor.w其实就是textureColor.a 透明度,这里不参与反相,因为我们透明度本来是1(完全不透明),参与反相的话就是0了,完全就是透明了,
     gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);
};

反相效果图如下
image.png

亮度(luminance)

PS
亮度(luminance)是表示人眼对发光体或被照射物体表面的发光或反射光强度实际感受的物理量,亮度和光强这两个量在一般的日常用语中往往被混淆使用。简而言之,当任两个物体表面在照相时被拍摄出的最终结果是一样亮、或被眼睛看起来两个表面一样亮,它们就是亮度相同。

亮度(明度)反应了色彩的明暗程度,它和色相(H)、饱和度(S)共同构成HSL色彩空间。调整亮度只需要RGB色彩空间里面同时加上一个程度值。

fragment shader

varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp float brightness;
void main()
{
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
};

这里我们定义了一个uniform lowp float brightness,我们通过界面Seekbar的拖动来动态修改这个值,还记得GLSL中uniform修饰的变量该如何赋值嘛,glUnifrom*系列函数
效果图来了,

image

曝光度(Exposure)

PS
曝光度和亮度的原理基本上是一致的,亮度是全方位的线性增加色值,而曝光度是基于原色值的指数型叠加(红的会更红,绿的会更绿,蓝的会更蓝,白光的会更光)

fragment shader

varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp float exposure;
void main()
{
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    gl_FragColor = vec4(textureColor.rgb * pow(2.0, exposure), textureColor.w);// rgb * 2^效果值
};

这里用到了GLSL的内置函数pow进行指数运算

对比度(Contrast)

接触了上面的亮度(luminance)曝光度(Exposure),我们来看一个稍微复杂点的对比度(Contrast)

PS
对比度是画面黑与白的比值,也就是从黑到白的渐变层次。比值越大,从黑到白的渐变层次就越多,从而色彩表现越丰富。

对比度对视觉效果的影响非常关键,一般来说对比度越大,图像越清晰醒目,色彩也越鲜明艳丽;而对比度小,则会让整个画面都灰蒙蒙的。

简单的说,对比度是像素颜色和某个中值的差,它可以让明亮的颜色更明亮,让灰暗的颜色更灰暗。

这里实现个简单的线性对比度算法:

结果=中值差*对比度+中值

fragment shader

varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp float m_contrast;
void main()
{
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    gl_FragColor = vec4(((textureColor.rgb - vec3(0.5)) * m_contrast + vec3(0.5)), textureColor.w);
}

image

饱和度(Saturation)

PS
色彩饱和度:饱和度是指色彩的鲜艳程度,也称色彩的纯度。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。纯的颜色都是高度饱和的,如鲜红,鲜绿。混杂上白色,灰色或其他色调的颜色,是不饱和的颜色,如绛紫,粉红,黄褐等,完全不饱和的颜色根本没有色调,如黑白之间的各种灰色。

概念总结:饱和度 = X·原色 + Y·灰度值,其中(x+y=1)

其中使用Luma算法求算灰度:Gray = R0.2125 + G0.7154 + B*0.0721

fragment shader

varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
uniform lowp float saturation;
void main()
{
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
    lowp vec3 greyScaleColor = vec3(luminance);
    gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w);
     // GLSL内置函数 mix(x,y,a) = x*(1-a)+y*a,刚好满足饱和度的公式定义。
}

image

色相(色调)

我们最后来解释调整图像色调,算是入门以上的图像处理手法。理论部分较多且深,要慢慢理解。先来了解什么是色调,如何定义色调。

PS
色调不是指颜色的性质,是对一幅绘画作品的整体评价。一幅绘画作品虽然可能用了多种颜色,但总体有一种色调,是偏蓝或偏红,是偏暖或偏冷等等。

色彩模型

在理解色调之前,我们先来解释一下色彩模型,数字图像处理当中的三大色彩模型:RGB、HSI、CMYK(注意!这里不是格式,是色彩模型)

(1)最常用的RGB色彩模型。

RGB是依据人眼识别的颜色定义出的空间,可表示大部分颜色。是图像处理中最基本、最常用、面向硬件的颜色空间,是一种光混合的体系。

可以看到RGB颜色模式用三维空间中的一个点表示一种颜色,每个点有三个分量,分别表示红、绿、蓝的亮度值,亮度值限定为【0,1】。在RGB模型的立方体中,原点对应的颜色为黑色,它的三个分量值都为0;距离原点最远的顶点对应的颜色为白色,它的三个分量值都为1。从黑色到白色的灰度值分布在这两个点的连线上,该虚线称为灰度线;立方体的其余各点对应不同的颜色,即三原色红、绿、蓝及其混合色黄、品红、青色。

(2)HSI色彩模型,视觉传输传播使用

HSI色彩空间是从人的视觉系统出发,用色调(Hue)、饱和度(Saturation或Chroma)和亮度 (Intensity或Brightness)来描述色彩。

H——表示颜色的相位角。红、绿、蓝分别相隔120度;互补色分别相差180度,即颜色的类别。
S——表示成所选颜色的纯度和该颜色最大的纯度之间的比率,范围:[0,  1],即颜色的深浅程度。
I——表示色彩的明亮程度,范围:[0, 1],人眼对亮度很敏感!

可以看到HSI色彩空间和RGB色彩空间只是同一物理量的不同表示法,因而它们之间存在着转换关系:HSI颜色模式中的色调使用颜色类别表示,饱和度与颜色的白光光亮亮度刚好成反比,代表灰色与色调的比例,亮度是颜色的相对明暗程度。

(3)CMYK模型,用于印刷品依靠反光的色彩模式

CMYK是一种依靠反光的色彩模式,我们是怎样阅读报纸的内容呢?是由阳光或灯光照射到报纸上,再反射到我们的眼中,才看到内容。它需要有外界光源,如果你在黑暗房间内是无法阅读报纸的。只要在屏幕上显示的图像,就是RGB模式表现的。只要是在印刷品上看到的图像,就是CMYK模式表现的。大多数在纸上沉积彩色颜料的设备,如彩色打印机和复印机,要求输入CMY数据,在内部进行RGB到CMY的转换。

青色Cyan、品红色Magenta、黄色Yellow是光的二次色,是颜料的颜色。而K取的是black最后一个字母,之所以不取首字母,是为了避免与蓝色(Blue)混淆。当红绿蓝三原色被混合时,会产生白色,当混合青色、品红色、黄色三原色时会产生黑色。从理论上来说,只需要CMY三种油墨就足够了,但是由于目前制造工艺还不能造出高纯度的油墨,CMY相加的结果实际是一种暗红色。

色调(Hue)描述的是整体的颜色效果,在三大色彩模型当中,HSI色彩模型是能较好接近直觉概念上的色调,饱和度,亮度的联系。在这方面rgb格式不方便计算,这里建议切换到YIQ色彩空间进行计算

fragment shader

precision highp float;
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform mediump float hueAdjust;
const highp vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);
const highp vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);
const highp vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);
const highp vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);
const highp vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);
const highp vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);
void main ()
{
     // Sample the input pixel
     highp vec4 color = texture2D(inputImageTexture, textureCoordinate);
     // Convert to YIQ
     highp float YPrime = dot (color, kRGBToYPrime);
     highp float I = dot (color, kRGBToI);
     highp float Q = dot (color, kRGBToQ);
     // Calculate the hue and chroma
     highp float hue = atan (Q, I);
     highp float chroma = sqrt (I * I + Q * Q);
     // Make the user's adjustments
     hue += (-hueAdjust);
     // Convert back to YIQ Q = chroma * sin (hue);
     I = chroma * cos (hue);
     // Convert back to RGB
     highp vec4 yIQ = vec4 (YPrime, I, Q, 0.0);
     color.r = dot (yIQ, kYIQToR);
     color.g = dot (yIQ, kYIQToG);
     color.b = dot (yIQ, kYIQToB);
     // Save the result
     gl_FragColor = color;
};

image

小结

Github代码


Android中高级开发
本专栏面向Android中高级开发工程师,为各位开发者们提供一下我的Android开发之路的点点滴滴。
269 声望
920 粉丝
0 条评论
269 声望
920 粉丝
宣传栏