从0打造一个GPUImage(4)

330 阅读6分钟
原文链接: zhuanlan.zhihu.com

从0打造一个GPUImage(4)

用容易理解的语言讲一些不太容易理解的概念.

答疑

上一章提了个问题,就是如何写fragment shader绘制

这样的图像。

现在揭晓一下答案。

precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordOut;
varying vec4 a_position_out;

void main(void) {
    if (a_position_out.x + a_position_out.y <= 0.0) {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }else {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
}

ContentMode

UIImageView的contentMode经常使用的有3种。
1. scaleToFill
简单的填充整个屏幕,不管拉伸比例
2. scaleAspectFit
在保持长宽比的前提下,缩放图片,使得图片在容器内完整显示出来。
3. scaleAspectFill
在保持长宽比的前提下,缩放图片,使图片充满容器。

我们的纹理渲染出来之后,是变形的。如图。


帅气的吴彦祖被拉伸了。

我们的目标就是,让图片恢复比例。

scaleAspectFit原理

如果使用过AVFoundation框架的同学,应该会知道一个叫做AVMakeRectWithAspectRatioInsideRect(CGSize aspectRatio, CGRect boundingRect)
的方法。

这个方法有什么用呢?
这个方法就是计算在一个rect里如果需要保持一个size比例不变,这个size的真实位置。

比如。当前显示的View就是我们需要展示图片的容器。view的大小就是self.view.bounds.这张图片的大小是1000 × 1503。那么让我们来检测一下。把1000 * 1503大小的图片放在我们的screen大小的容器里,这张图片的大小和坐标是多少。
CGRect realRect = AVMakeRectWithAspectRatioInsideRect(image.size, self.view.bounds);


也就是说,如果我们的容器大小是0,0, 375, 667的话,那么我们的图片在保持宽高比例不变的情况下,坐标和大小会是(0, 51, 375, 563)

我们之前的篇章提到过,图片显示在屏幕上的过程其实就是把图片绘制在两个三角形拼成的矩形上。所以,想要保持图片比例不变的本质实际就是绘制一个大小和位置是(0, 51, 375, 563)的矩形,然后把图片贴在上面。

所以,代码就是

CGRect realRect = AVMakeRectWithAspectRatioInsideRect(image.size, self.view.bounds);
    CGFloat widthRatio = realRect.size.width/self.view.bounds.size.width;
    CGFloat heightRatio = realRect.size.height/self.view.bounds.size.height;
    
    const GLfloat vertices[] = {
        -widthRatio, -heightRatio, 0,   //左下
        widthRatio,  -heightRatio, 0,   //右下
        -widthRatio, heightRatio,  0,   //左上
        widthRatio,  heightRatio,  0 }; //右上

大家思考一下,为什么顶点是这样?(提示:顶点范围是-1 ~ 1)

运行一下,我们的效果如下。



现在,我们使用OpenGL显示一张图片就完成了。

滤镜的原理

接下来,我们的重点,放在解析滤镜原理上。

GPUImage的滤镜种类

GPUImage的滤镜主要有以下4类。

  • Color Processing
  • Image Processing
  • Blends
  • Effects

Color Processing主要用来处理图片的颜色。包括图片的对比度,亮度,灰度等。

Image Processing主要用来处理图片的变换,切割等。

Blends 主要处理多个纹理叠加的效果。例如著名的滤镜lomo和1973等就是基于2-3个纹理叠加的效果。

Effects 主要提供了一些现成的滤镜效果,包括卡通画,马赛克等。

这一张主要介绍一些Color Processing的滤镜。

Color Processing 之 GrayScale(1)

GrayScale应该是最简单的滤镜了。就是将一张图片转化为灰度图片。注意,这里是灰度,而不是黑白,因为用黑白图来形容是不准确的,因为一张黑白照片是由不同灰度的颜色组成的。

最常见的转灰度图的方法就是(color.r + color.g + color.b)/3.我们这里介绍一种新的方法。

先介绍一个常数。
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

那么如何把一张图片转化为灰度图呢?看一下我们的GrayScaleFragmentShader.

precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordOut;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

void main(void) {
    vec4 color = texture2D(u_Texture, v_TexCoordOut);
    float lumiance = dot(color.rgb, W);
    gl_FragColor = vec4(vec3(lumiance), 1.0);
    
}

原图

灰度图


解释一下这段代码的意思。

首先,利用texture2D函数通过目前的纹理坐标取出当前处理像素的RGBA, 然后利用dot函数计算RGB和我们声明的W的点积。

什么是点积?
设A = vec3(a, b, c); B = vec3(x, y, z);
则,dot(A, B) = a*x + b*y + c*z

然后我们在fragment shader中最终返回的gl_FragColor就等于我们通过dot计算的点积。

Color Processing 之 Levels

levels中文名叫色阶。
什么意思呢?
看一下Photoshop.


色阶在ps中主要用直方图描述出的整张图片的明暗信息。

大家注意吴彦祖这张图片,图片的直方图峰顶的位置大部分集中在0 - 255这个区间的前半段,也就是说,这张图的整体明暗度对比比较强烈。

而且整张图骗的明暗分布是比较全的。也就是0-255整个范围都有覆盖。我们可以挑选一张拍摄的特别暗的图片来看一下他的直方图。




这种图片在摄影师眼里看来就是属于比较差劲的作品,明暗分布不完整。图片的像素大部分比较暗。

色阶的算法如下。
输入值有5个。
minInput, maxInput, mid, minOutput, maxOutput.

Diff = maxInput - minInput
colorDiff = min((color - minInput), 0)
上句的意思是如果color中rgb的点小于最小输入值,那么为0

color = pow(colorDiff/Diff, 1/mid)

outputColor = color*(maxOutput - minOutput) + minOutput

比较复杂。大家可以对照GPUImageLevelsFilter来看。

Color Processing 之 Saturation

Saturation即饱和度的意思。
GPUImage如何调整饱和度呢?

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);
     
 }

前三句没什么好说的,根据图片的RGB信息,生成了一个灰度的greyScaleColor。
最后根据用户输入的saturation的值,调整颜色。
因为mix(greyScaleColor, textureColor.rgb, saturation) = greyScaleColor * (1-saturation ) + textureColor.rgb * saturation

也就是说,对比度越强,灰度就越低。所以变相增加了颜色的对比度。

Color Processing 之 Brightness(亮度)

我们通常接触到的颜色空间是RGB,其实常用的还有HSV又叫HSB。
HSV即Hue, Saturation, Value.
什么意思呢?
先看一张图。


HSV这个color space可以用上图的圆柱体来表示。
Hue代表从0°到360°的不同颜色.
Saturation指的是色彩的饱和度,它用0%至100%的值描述了相同色相、明度下色彩纯度的变化。数值越大,颜色中的灰色越少,颜色越鲜艳,呈现一种从理性(灰度)到感性(纯色)的变化
Value指的是色彩的明度,作用是控制色彩的明暗变化。它同样使用了0%至100%的取值范围。数值越小,色彩越暗,越接近于黑色;数值越大,色彩越亮,越接近于白色。

所以,我们在调整图片饱和度的时候,往往需要把RGB模式转换为HSV模式,然后调节Saturation的数值。

但是GPUImage在调整图片亮度的时候没有按照先把rgb -> hsv,然后调整V的值来调整图片亮度。
而是直接用了一种简单粗暴的方法。

void main()
 {
     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
     
     gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
 }

brightness是一个用户输入值。范围是(-1 ~ 1).
也就是说,当用户调高brightness的时候,直接把rgb的数值加上相应的值就可以了。

当然,这也不能说不对,因为颜色越靠近1.那么就越亮。

待续

下一章继续介绍几种简单的滤镜,我们就步入正式编写自己的OpenGL ES处理图片的库。

从封装AVFoundation开始。