简析OpenGL ES 3.0 Sobel Filter实现边缘
文档iOS OpenGL ES 3.0 数据可视化 4:纹理映射实现2维图像与视频渲染简介使用索贝尔算子(Sobel Operator)实现了边缘检测(Edge Detection),相关代码在该文档没作分析。
本文档描述了有关GPU实现索贝尔算子(Sobel Operator)中Fragment Shader部分代码的简单分析。同时,包括了通过Xcode在线调试GPU对比OpenGL ES着色器语言内置函数与普通乘法计算的性能差异、启用着色器调试模式及其资源消耗等内容。
1、sobel_filter操作
float sobel_filter()
{
float dx = 1.0 / float(u_screen_width); // e.g. 1920
float dy = 1.0 / float(u_screen_height); // e.g. 1080
float s00 = pixel_operator(-dx, dy);
float s10 = pixel_operator(-dx, 0.0);
float s20 = pixel_operator(-dx, -dy);
float s01 = pixel_operator(0.0, dy);
float s21 = pixel_operator(0.0, -dy);
float s02 = pixel_operator(dx, dy);
float s12 = pixel_operator(dx, 0.0);
float s22 = pixel_operator(dx, -dy);
float sx = s00 + 2.0 * s10 + s20 - (s02 + 2.0 * s12 + s22);
float sy = s00 + 2.0 * s01 + s02 - (s20 + 2.0 * s21 + s22);
float dist = sx * sx + sy * sy;
return dist;
}
dx = 1.0 / float(u_screen_width);
、dy = 1.0 / float(u_screen_height);
分别取出指定宽高(即,屏幕的宽高)上的最小单位,在此是一个像素。同时,确保坐标值在纹理坐标[0, 1]区间内。
s00、s10等变量的坐标由下图所示,在此按纹理坐标的定义理解即可,原点在左下角,故在待计算的像素点的左边则是x轴少一个单位。
坐标的表示
@琨君对sobel的解释如下:
sx = 1*(s00 - s01 + s01 - s02) + 2*(s10 - s11 + s11 - s12) + 1*(s20 - s21 + s21 - s22)
,sy是纵向的,相当于是加权的差分。
sx对应横向的情况,矩阵为[1, 0, -1; 2, 0, -2; 1, 0, -1]。
sy对应纵向求差分,矩阵为[1, 2, 1; 0, 0, 0, -1, -2, -1]。
可看成是一个梯度向量(sx, sy),sx平方 + sy平方就是求模。
2、采样操作pixel_operator
// in vec2 v_TexCoord;
float pixel_operator(float dx, float dy) {
vec4 rgba_color = texture( u_sampler, v_TexCoord + vec2(dx,dy) );
return rgb2gray(rgba_color.rgb);
}
前面的sobel_filter()
只是得到待计算的像素点周围的像素的坐标,实现了算法骨架,然而,对于待计算的像素点本身的坐标却没处理。在此,只需提供待计算的像素点的坐标,即可完成指定像素点的处理,这就是pixel_operator()
的功能。
通过接收顶点着色器传递过来的纹理坐标v_TexCoord,加上坐标偏移vec2(dx,dy),完成邻近像素点的采样。
3、Sobel Operator计算规则问题
在分析rgb2gray()
前,对Sobel Operator作简单说明。虽然,Sobel Operator定义为3x3矩阵,在计算sx、sy的平方里却没按矩阵乘法进行。在此,将其理解为Sobel Operator特定的计算规则即可。
4、RGB亮度转换至梯度值
float rgb2gray(vec3 color) {
return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
}
实现了RGB(三维颜色空间)向梯度(一维颜色空间)的转换,从而以黑、白及其过渡颜色表达了RGB图像的亮度。
5、代码优化
为简化代码,将float rgb2gray(vec3 color)
修改为float rgb2gray(vec4 color)
,提高pixel_operator(float dx, float dy)
的调用代码可读性。同时,使用内置函数dot实现向量点乘计算。另外,dx、dy由CPU计算好再上传至GPU。
// predefine var brightness
vec3 brightness = vec3(0.2126, 0.7152, 0.0722);
float rgb2gray(vec4 color) {
return dot(color.rgb, brightness);
}
float pixel_operator(float dx, float dy) {
return rgb2gray( texture( u_sampler, v_TexCoord + vec2(dx,dy)) );
}
6、dot运算与直接相乘的性能比较
在不使用VBO的情况下,验证替换成dot计算RGB亮度是否提升性能。原始实现的运行情况如下图所示。
原始实现的资源消耗 原始实现的Fragment耗时替换成dot计算的资源利用情况。
dot计算的资源消耗 dot计算RGB亮度的耗时直接相乘与dot运算的比较。
baseline为直接相乘可见,dot运算与直接相乘在性能几乎没区别。那么,dot运算由底层图形硬件加速的观点并没在iPad Air 2真机上体现。
7、不同阈值下的边缘检测效果
在Fragment Shader中加上阈值判断,代码如下所示。
// predefine var
float gradientThreshold = 0.005;
// void main()
if (graylevel > gradientThreshold) {
o_Color = vec4(0.0, 0.0, 0.0, 1.0);
} else {
o_Color = vec4(1.0);
}
依次修改gradientThreshold观察运行结果。
gradientThreshold = 0.5 gradientThreshold = 0.05 gradientThreshold = 0.005 gradientThreshold = 0.00058、关闭Shader代码优化及开启调试功能
#pragma debug(on)
#pragma optimize(off)
启用调试并关闭着色器代码优化的情况下,资源消耗比默认情况(开启着色器代码优化)略有提升,如下图所示。这里调试与非调试状态资源消耗对比不明显的可能原因是计算简单及顶点数据量小。
启用调试并关闭着色器代码优化的资源消耗致谢:
感谢@琨君提出的见解。