GPU处理图像 Shader的入门

Shader着色器

Shader出现在OpenGL ES 2.0中,允许创建自己的Shader。必须同时创建两个Shader,分别是Vertex shader和Fragment shader.

Shader工具

Shader会有很多坑,不过一些工具能够帮助你跳过这些坑

Shader使用范例

Vertex shader

attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
     gl_position = position;
     textureCoordinate = inputTextureCoordinate.xy;
}

Fragment shader

直通滤镜

varying highp vec2 textureCoordinate; //highp属性负责变量精度,这个被加入可以提高效率
uniform sampler2D inputImageTexture; //接收一个图片的引用,当做2D的纹理,这个数据类型就是smpler2D。
void main()
{
     gl_FragColor = texture2D(inputImageTexture, textureCoordinate); //texture是GLSL(着色语言)特有的方法
}

GLSL着色语言

GLSL的官方快速入门指导

变量赋值

三个可以赋值给我们的变量的标签

  • Uniforms:在渲染循环里作为不变的输入值
  • Attributes:随顶点位置不同会变的输入值
  • Varyings:用来在Vertex shader和Fragment shader之间传递信息的,比如在Vertex shader中写入varying值,然后就可以在Fragment shader中读取和处理

向量

有很多种向量,但是有三种会经常看到

  • vec2:两个浮点数,适合在Fragment shader中保存X和Y坐标的情况
  • vec3:三个浮点数
  • vec4:四个浮点数,在图像处理中持续追踪每个像素的R,G,V,A这四个值。

矩阵

是浮点数组的数组。三个经常处理的矩阵对象

  • mat2:相当于保存了两个vec2对象的值或四个浮点数。
  • mat3
  • mat4

向量和矩阵运算

线性代数发挥作用的地方。想知道线性代数如何工作可以看这个资源站:http://betterexplained.com/articles/linear-algebra-guide/

线性代数可以一次在很多值上进行并行操作,so,正好适合需求,GLSL内建了很多函数可以处理庞大的计算转换

GLSL特有函数

GLSL内建的函数可以在Shaderific网站上找到:http://www.shaderific.com/glsl-functions。很多C语言数学库基本数学运算都有对应的函数。

  • step():GPU处理条件逻辑不是很好。step()允许在不产生分支的前提下实现条件逻辑。传入step()函数的值小于阈值就返回0.0,大于等于阈值就返回1.0。
  • mix():将两个颜色值混合为一个。
  • clamp():可以确保值在一个区间内。

复杂的Shader的例子

一个饱和度调节的Fragment shader的例子,出自《图形着色器:理论和实践》这本书。

varying highp vec2 textureCoordinate; //

uniform sampler2D inputImageTexture;
uniform lowp float saturation;

const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721); //光亮度里三个值相加要为1,各个值代表着颜色的百分比,中间是绿色的值,70%的比重会让效果更好点。

void main()
{
     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate); //根据坐标取样图片颜色信息
     lowp float luminance = dot(textureColor.rgb, luminanceWeighting); //GLSL中的点乘运算,线性代数的点运算符相乘两个数字。点乘计算需要将纹理颜色信息和相对应的亮度权重相乘。然后取出所有的三个值相加到一起计算得到这个像素的中和亮度值。
     lowp vec3 greyScaleColor = vec3(luminance); //创建一个三个值都是亮度信息的vec3,如果只指定一个值,编译器会将其它的都设置成这个值
     gl_FragColor = vec4(mix(greyScaleColor, textureColor.rgb, saturation), textureColor.w); //用mix函数把计算的灰度值,初识的纹理颜色和得到的饱和度信息结合起来。
}

球形滤镜示例

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

uniform highp vec2 center;
uniform highp float radius;
uniform highp float aspectRatio;
uniform highp float refractiveIndex;

void main()
{
     highp vec2 textureCoordinateToUse = vec2(textureCoordinate.x, (textureCoordinate.y * aspectRatio + 0.5 - 0.5 * aspectRatio)); //归一化坐标空间需要考虑屏幕是一个单位宽和一个单位长。
     highp float distanceFromCenter = distance(center, textureCoordinateToUse); //计算特定像素点距离球形的中心有多远。使用GLSL内建的distance()函数,用勾股定律计算出中心坐标和长宽比矫正过的纹理坐标的距离
     lowp float checkForPresenceWithinSphere = step(distanceFromCenter, radius); //计算片段是否在球体内。

     distanceFromCenter = distanceFromCenter / radius;  //标准化到球心的距离,重新设置distanceFromCenter

     highp float normalizedDepth = radius * sqrt(1.0 - distanceFromCenter * distanceFromCenter); //模拟一个玻璃球,需要计算球的“深度”是多少。
     highp vec3 sphereNormal = normalize(vec3(textureCoordinateToUse - center, normalizedDepth)); //归一化

     highp vec3 refractedVector = refract(vec3(0.0, 0.0, -1.0), sphereNormal, refractiveIndex); //GLSL的refract()函数以刚才创建的球法线和折射率来计算当光线通过球时从任意一个点看起来如何。

     gl_FragColor = texture2D(inputImageTexture, (refractedVector.xy + 1.0) * 0.5) * checkForPresenceWithinSphere; //最后凑齐所有计算需要的颜色信息。
}

调试Shader

使用gl_FragColor调试代码。GPUImage是个开源的资源有些很酷的shader,非常好的学习shader的方式,可以拿一个你觉得很有意思的shader对着源码一点点看下去。GPUImage还有一个shader设计器https://github.com/BradLarson/GPUImage/tree/master/examples/Mac/ShaderDesigner 的Mac应用,可以测试shader而不用准备OpenGL代码。

性能调优

简单的方法达到调优目的,可以用下载Imagination Technologies PowerVR SDKhttp://community.imgtec.com/developers/powervr/这个工具帮助分析shader

  • 消除条件逻辑:使用step()这样的函数
  • 减少依赖纹理的读取:如果希望从附近像素取样而不是计算Fragment shader相邻像素的偏差,最好在Vertex shader中计算然后把结果以varying的方式传入Fragment shader里。
  • 计算尽量简单:能够得到一个近似值就尽量用,不要用类似sin(),cos(),tan()的比较消耗的操作。
  • 尽可能的将计算放到Vertex上:如果计算在图片上会有相同的结果或线性变化最好这样做。因为Vertex shader对每个顶点运行一次,而Fragment shader会在每个像素上运行一次。
  • 移动设备使用合适的精度:在向量上使用低精度的值会变得更快。两个lowp vec4相加可以在一个时钟周期内完成,两个highp vec4相加则需要四个时钟周期。

边界探测

基于OpenCV库,不过这些步骤在GPUImage中都有完整的实现

Sobel边界探测

这种操作在滤镜方面比机器视觉方面多。Sobel边界探测用于探测边界的出现位置,边界是由明变暗或者由暗变明的区域。在被处理的图片中一个像素的亮度反映了这个像素周围边界的强度。

  • 第一步,将彩色图片弄成灰阶图,这个过程就是将每个像素的红绿蓝部分合一代表亮度的值。如是果YUV而不是RGB格式的可以省略这步,因为YUV是将亮度信息和色度信息分开的。如果简化到只剩亮度的话一个像素周围的边界强度就可以由周围3*3个临近像素计算得到。这个计算涉及Convolution Matrix(卷积矩阵),每个像素都要与这个矩阵计算出一个数值,因为没有顺序要求所以可以采取并行运算。
  • Sobel的水平处理矩阵

-1 0 +1
-2 0 +2
-1 0 +1

  • Sobel的垂直矩阵

-1 -2 -1
0 0 0
+1 +2 +1

  • 和Sobel类似的变体,Prewitt边界探测。这个变体会在横向竖向矩阵中用不同的矩阵,但是运作过程差不多。
  • OpenGL ES代码

precision mediump float;

//varying的都是在Vertex shader上定义了
varying vec2 textureCoordinate;
varying vec2 leftTextureCoordinate;
varying vec2 rightTextureCoordinate;

varying vec2 topTextureCoordinate;
varying vec2 topLeftTextureCoordinate;
varying vec2 topRightTextureCoordinate;

varying vec2 bottomTextureCoordinate;
varying vec2 bottomLeftTextureCoordinate;
varying vec2 bottomRightTextureCoordinate;

uniform sampler2D inputImageTexture;

void main()
{
     float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
     float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;
     float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
     float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
     float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
     float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
     float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
     float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;

     float h = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
     float v = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;
     float mag = length(vec2(h, v)); //length()函数计算出水平和垂直矩阵转化后值的平方和的平方根的值,这个值会被拷贝进输出像素的红绿蓝通道中,这样就可以来代表边界的明显程度了。

     gl_FragColor = vec4(vec3(mag), 1.0);
}

Canny边界探测

Canny探测会比Sobel复杂些,这样做会得到一条物体边界的干净线条。

探测过程:

  • 先用Sobel矩阵得到边界梯度的强度。这个和Sobel比就是最后一个计算有些不同
vec2 gradientDirection;
gradientDirection.x = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
gradientDirection.y = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;

float gradientMagnitude = length(gradientDirection);
vec2 normalizedDirection = normalize(gradientDirection);
normalizedDirection = sign(normalizedDirection) * floor(abs(normalizedDirection) + 0.617316); // Offset by 1-sin(pi/8) to set to 0 if near axis, 1 if away
normalizedDirection = (normalizedDirection + 1.0) * 0.5; // Place -1.0 - 1.0 within 0 - 1.0

gl_FragColor = vec4(gradientMagnitude, normalizedDirection.x, normalizedDirection.y, 1.0);
  • 着色器步骤
precision mediump float;

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;
uniform highp float texelWidth; //要处理的图片中临近像素之间的距离。
uniform highp float texelHeight; //同上
uniform mediump float upperThreshold; //预期边界强度上下限
uniform mediump float lowerThreshold; 

void main()
{
     vec3 currentGradientAndDirection = texture2D(inputImageTexture, textureCoordinate).rgb;
     vec2 gradientDirection = ((currentGradientAndDirection.gb * 2.0) - 1.0) * vec2(texelWidth, texelHeight);

     float firstSampledGradientMagnitude = texture2D(inputImageTexture, textureCoordinate + gradientDirection).r;
     float secondSampledGradientMagnitude = texture2D(inputImageTexture, textureCoordinate - gradientDirection).r;

     float multiplier = step(firstSampledGradientMagnitude, currentGradientAndDirection.r);
     multiplier = multiplier * step(secondSampledGradientMagnitude, currentGradientAndDirection.r);

     float thresholdCompliance = smoothstep(lowerThreshold, upperThreshold, currentGradientAndDirection.r);
     multiplier = multiplier * thresholdCompliance;

     gl_FragColor = vec4(multiplier, multiplier, multiplier, 1.0);
}

Harris边角探测

多步骤的方法来探测场景中的边角。

  • 先弄得只有亮度信息,再通过Sobel矩阵,普里维特矩阵或者其它相关的矩阵计算出一个像素X和Y方向的梯度值,计算的结果会将X梯度传入红色部分,Y梯度传入绿色部分,X与Y梯度的乘积传入蓝色部分。
  • 对计算的结果进行高斯模糊。将模糊后图中取出红绿蓝编码的值带到计算边角点可能性公式:

R = Ix2 × Iy2 − Ixy × Ixy − k × (Ix2 + Iy2)2

资料

相关数学

相关GLSL

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容