2.2 数字图像处理
随着数码相片逐渐替代传统胶卷相片成为主流,数字图像处理与我们生活的关系也越来越密切了。例如经常用来进行相片后期制作的PhotoShop中就带有很多特效滤镜,如:模糊、浮雕、锐化等,这些都是基于数字图像处理技术开发的。
传统的数字图像处理应用大部分是基于CPU开发的,由于CPU运算能力的限制,处理一幅图片需要较长的时间。本节将介绍的数字图像处理应用是基于GPU开发的,具有开发简便,速度快等优势。
2.2.1 卷积的基本知识
数字图像处理中用到的很多滤镜都是基于卷积计算开发的,因此本小节将对卷积计算的原理进行简要的介绍。卷积是一种很常见的数字图像处理操作,其可以用来过滤一幅图像。实现过滤的方法是计算源图像与卷积内核之间的积,所谓卷积内核是指一个n×n的矩阵,n一般为奇数。进行卷积计算时将卷积内核对待处理图像中的每个像素都应用一次,具体的计算思路如图2-4所示。
▲图2-4 卷积基本原理
从图2-4中可以看出,卷积计算将需要处理的图片中的每个像素计算一次,具体的计算方法如下。
● 首先将卷积内核中心的元素对准当前待处理的像素,此时卷积内核中的其他元素也各自对应到了一个像素。
● 然后将卷积内核中的各元素与其对应的像素的颜色值相乘。
● 最后将所有的乘积加权求和即可得到处理后图片中此位置像素的颜色值。
根据卷积内核中个各元素值的不同,可以轻松地实现模糊、边缘检测、锐化、浮雕等滤镜的特效,下面的小节将陆续给出这方面的案例。
提示
请读者注意的是,卷积内核的尺寸并不一定是3×3,也可以是5×5、7×7等。但卷积内核越大,计算量成几何级数增长,因此采用3×3的情况较多。
2.2.2 平滑过滤
平滑过滤可以将过于锐利的照片变得平滑些,实现起来也非常简单,只需要将卷积内核的所有元素值都设为1即可,下面的表2-1给出了本节案例所采用的3×3平滑过滤的卷积内核。
表2-1 实现平滑过滤的卷积内核
了解了平滑过滤的卷积内核后,下面请读者进一步了解一下本小节案例Sample2_3的运行效果,如图2-5所示。
▲图2-5 案例Sample2_3的运行效果图
说明
图2-5中左侧为处理前的原图,右侧为经过平滑过滤的图像。细致比对左右两幅图可以看出,右侧的图片比左侧的要平滑了不少。由于插图灰度印刷的原因,读者也可能看得不是很清楚,此时请读者自行在真机上运行本案例观察。
了解了案例所采用的卷积内核与案例的运行效果后,就可以进行案例的开发了。由于本案例中的大部分代码在前面章节的许多案例中都出现过,故这里仅给出实现卷积的片元着色器,其代码如下。
代码位置:见随书光盘中源代码/第2章/Sample2_3/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec2 vTextureCoord; //从顶点着色器传递过来的纹理坐标 3 uniform sampler2D sTexture; //纹理内容数据 4 void main(){ 5 //给出卷积内核中各个元素对应像素相对于待处理像素的纹理坐标偏移量 6 vec2 offset0=vec2(-1.0,-1.0); vec2 offset1=vec2(0.0,-1.0); vec2 offset2=vec2 (1.0,-1.0); 7 vec2 offset3=vec2(-1.0,0.0); vec2 offset4=vec2(0.0,0.0); vec2 offset5=vec2(1.0, 0.0); 8 vec2 offset6=vec2(-1.0,1.0); vec2 offset7=vec2(0.0,1.0); vec2 offset8=vec2(1.0, 1.0); 9 const float scaleFactor = 1.0/9.0;//给出最终求和时的加权因子(为调整亮度) 10 //卷积内核中各个位置的值 11 float kernelValue0 = 1.0; float kernelValue1 = 1.0; float kernelValue2 = 1.0; 12 float kernelValue3 = 1.0; float kernelValue4 = 1.0; float kernelValue5 = 1.0; 13 float kernelValue6 = 1.0; float kernelValue7 = 1.0; float kernelValue8 = 1.0; 14 vec4 sum;//最终的颜色和 15 //获取卷积内核中各个元素对应像素的颜色值 16 vec4 cTemp0,cTemp1,cTemp2,cTemp3,cTemp4,cTemp5,cTemp6,cTemp7,cTemp8; 17 cTemp0=texture2D(sTexture, vTextureCoord.st + offset0.xy/512.0);
18 cTemp1=texture2D(sTexture, vTextureCoord.st + offset1.xy/512.0); 19 cTemp2=texture2D(sTexture, vTextureCoord.st + offset2.xy/512.0); 20 cTemp3=texture2D(sTexture, vTextureCoord.st + offset3.xy/512.0); 21 cTemp4=texture2D(sTexture, vTextureCoord.st + offset4.xy/512.0); 22 cTemp5=texture2D(sTexture, vTextureCoord.st + offset5.xy/512.0); 23 cTemp6=texture2D(sTexture, vTextureCoord.st + offset6.xy/512.0); 24 cTemp7=texture2D(sTexture, vTextureCoord.st + offset7.xy/512.0); 25 cTemp8=texture2D(sTexture, vTextureCoord.st + offset8.xy/512.0); 26 //颜色求和 27 sum =kernelValue0*cTemp0+kernelValue1*cTemp1+kernelValue2*cTemp2+ 28 kernelValue3*cTemp3+kernelValue4*cTemp4+kernelValue5*cTemp5+ 29 kernelValue6*cTemp6+kernelValue7*cTemp7+kernelValue8*cTemp8; 30 gl_FragColor = sum * scaleFactor; //进行亮度加权后将最终颜色传递给管线 31 }
提示
上述片元着色器的代码使用平滑过滤的卷积内核进行卷积计算,实现了平滑过滤效果的滤镜。同时上述代码中使用的加权因子是用来调节结果图像亮度的,读者可以根据具体情况进行修改,其值越大结果图像亮度越大。
2.2.3 边缘检测
通过卷积不但可以对图像进行平滑处理,还可以进行边缘检测。实现边缘检测的卷积内核各元素的值如表2-2所示。
表2-2 实现边缘检测的卷积内核
了解了边缘检测的卷积内核后,下面请读者进一步了解一下本小节案例Sample2_4的运行效果,如图2-6所示。
▲图2-6 案例Sample2_4的运行效果图
说明
图2-6中左图为处理前的原图,中间为经过边缘检测后的图像。对比两幅图可以看出,中间的图像中仅保留了左侧原图中物体边缘的位置内容。由于插图灰度印刷的原因,读者也可能看的不是很清楚,因此最右边还给出了将结果图像颜色置反后的参考图像,这样看起来会清楚很多。
了解了案例所采用的卷积内核与案例的运行效果后,就可以进行案例的开发了。由于本案例中的大部分代码与上一小节案例中的基本一致,有区别的部分主要就是片元着色器中卷积内核各个元素的值以及控制亮度的加权因子。故这里仅给出有区别部分的代码,具体内容如下。
代码位置:见随书光盘中源代码/第2章/Sample2_4/assets目录下的frag.sh。
1 const float scaleFactor = 0.9;//给出最终求和时的加权因子(为调整亮度) 2 //卷积内核中各个位置的值 3 float kernelValue0 = 0.0; float kernelValue1 = 1.0; float kernelValue2 = 0.0;
4 float kernelValue3 = 1.0; float kernelValue4 = -4.0; float kernelValue5 = 1.0; 5 float kernelValue6 = 0.0; float kernelValue7 = 1.0; float kernelValue8 = 0.0;
2.2.4 锐化处理
对于过于平滑已经显得模糊的图像也可以使用卷积进行锐化处理,使得图像看起来清晰些。实现锐化处理的卷积内核各元素的值如表2-3所列。
表2-3 实现锐化的卷积内核
了解了锐化处理的卷积内核后,下面请读者进一步了解一下本小节案例Sample2_5的运行效果,如图2-7所示。
▲图2-7 案例Sample2_5的运行效果图
说明
图2-7中左图为处理前的原图,右图为经过锐化处理后的图像。细致比对左右两幅图可以看出,右侧的图片比左侧的要清晰了一些。由于插图灰度印刷的原因,读者也可能看的不是很清楚,此时请读者自行在真机上运行本案例观察。
看完案例的运行效果后,接下来对本案例的具体开发过程进行介绍。由于本案例只是将案例Sample2_4的片元着色器做了简单的修改,因此这里仅给出修改部分的代码。
代码位置:见随书光盘中源代码/第2章/Sample2_5/assets目录下的frag.sh。
1 const float scaleFactor = 1.0;//给出最终求和时的加权因子(为调整亮度) 2 //卷积内核中各个位置的值 3 float kernelValue0 = 0.0; float kernelValue1 = -1.0; float kernelValue2 = 0.0; 4 float kernelValue3=-1.0;float kernelValue4=5.0;float kernelValue5=-1.0; 5 float kernelValue6 = 0.0; float kernelValue7 = -1.0; float kernelValue8 = 0.0;
提示
从上述代码中可以看出,主要是修改了片元着色器中卷积内核的元素值以及亮度加权因子,其他部分基本没有变化。
2.2.5 浮雕效果
采用卷积计算还可以产生浮雕效果,实现浮雕效果的卷积内核各元素的值如表2-4所列。
表2-4 实现浮雕效果的卷积内核
了解了浮雕效果的卷积内核后,下面请读者进一步了解一下本小节案例Sample2_5A的运行效果,如图2-8所示。
▲图2-8 案例Sample2_5A的运行效果图
说明
图2-8中左图为处理前的原图,右侧为经过浮雕处理后的图像。对比左右两幅图可以明显地看出,左图的原图给人的感觉是平面的,而右图经过浮雕处理后的结果图像有明显的雕刻的凹凸感。
看完案例的运行效果后,接下来对本案例的具体开发过程进行介绍。由于本案例只是将案例Sample2_5的片元着色器做了简单的修改,因此这里仅给出修改后片元着色器的代码。
代码位置:见随书光盘中源代码/第2章/Sample2_5A/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec2 vTextureCoord; //从顶点着色器传递过来的纹理坐标 3 uniform sampler2D sTexture; //纹理内容数据 4 void main(){ 5 //给出卷积内核中各个元素对应像素相对于待处理像素的纹理坐标偏移量 6 vec2 offset0=vec2(-1.0,-1.0); vec2 offset1=vec2(0.0,-1.0); vec2 offset2=vec2 (1.0,-1.0); 7 vec2 offset3=vec2(-1.0,0.0); vec2 offset4=vec2(0.0,0.0); vec2 offset5=vec2 (1.0,0.0); 8 vec2 offset6=vec2(-1.0,1.0); vec2 offset7=vec2(0.0,1.0); vec2 offset8=vec2 (1.0,1.0); 9 const float scaleFactor = 1.0;//给出最终求和时的加权因子(为调整亮度) 10 //卷积内核中各个位置的值 11 float kernelValue0 = 2.0; float kernelValue1 = 0.0; float kernelValue2 = 2.0; 12 float kernelValue3 = 0.0; float kernelValue4 = 0.0; float kernelValue5 = 0.0; 13 float kernelValue6 = 3.0; float kernelValue7 = 0.0; float kernelValue8 = -6.0; 14 vec4 sum;//最终的颜色和 15 //获取卷积内核中各个元素对应像素的颜色值 16 vec4 cTemp0,cTemp1,cTemp2,cTemp3,cTemp4,cTemp5,cTemp6,cTemp7,cTemp8; 17 cTemp0=texture2D(sTexture, vTextureCoord.st + offset0.xy/512.0); 18 cTemp1=texture2D(sTexture, vTextureCoord.st + offset1.xy/512.0); 19 cTemp2=texture2D(sTexture, vTextureCoord.st + offset2.xy/512.0); 20 cTemp3=texture2D(sTexture, vTextureCoord.st + offset3.xy/512.0); 21 cTemp4=texture2D(sTexture, vTextureCoord.st + offset4.xy/512.0); 22 cTemp5=texture2D(sTexture, vTextureCoord.st + offset5.xy/512.0); 23 cTemp6=texture2D(sTexture, vTextureCoord.st + offset6.xy/512.0); 24 cTemp7=texture2D(sTexture, vTextureCoord.st + offset7.xy/512.0); 25 cTemp8=texture2D(sTexture, vTextureCoord.st + offset8.xy/512.0); 26 //颜色求和 27 sum =kernelValue0*cTemp0+kernelValue1*cTemp1+kernelValue2*cTemp2+ 28 kernelValue3*cTemp3+kernelValue4*cTemp4+kernelValue5*cTemp5+ 29 kernelValue6*cTemp6+kernelValue7*cTemp7+kernelValue8*cTemp8; 30 //灰度化 31 float hd=(sum.r+sum.g+sum.b)/3.0; 32 gl_FragColor = vec4(hd)* scaleFactor; //进行亮度加权后将最终颜色传递给管线 33 }
● 第11-13行中卷积内核各个元素的值与修改前案例Sample2_5中的有很大的不同。
● 第31行对滤镜处理后的颜色进行了灰度化处理,将3个色彩通道值的平均值作为结果颜色各个色彩通道的值。
提示
到这里为止本书需要介绍的基于卷积计算的数字图像处理滤镜就介绍完了。但实际可用的滤镜千变万化,远不止这几个,有兴趣的读者可以查阅数字图像处理相关的技术资料,仿造上述几个案例自行实现更加酷炫的滤镜。
2.2.6 图像渐变
采用片元着色器不但可以轻松开发出各种滤镜,还可以开发出很多有趣的应用。如本小节将给出的图像渐变的例子就是如此,此应用运行时从一幅照片平滑地过渡为另一幅照片,非常有意思。在介绍案例的开发之前,请读者先了解一下案例的运行效果,如图2-9所示。
▲图2-9 案例Sample2_6的运行效果图
说明
图2-9中从左至右为从一幅照片向另一幅照片平滑过渡的过程。从最左边我们开发团队仇磊的照片平滑过渡到了我们团队夏学良的照片,实现的基本思路就是在不同的时间点采用不同的加权因子对两幅照片进行混合。
了解了案例的运行效果后,下面来介绍本小节案例的开发。由于本小节案例中的大部分代码与前面很多案例中的基本一致,因此这里仅给出本案例中有代表性和特色的部分,具体如下所列。
(1)首先需要在应用程序中定时将连续变化的混合比例因子以及两幅纹理图传入渲染管线,以备片元着色器使用。由于将混合比例因子传入渲染管线的代码与将其他数据传入渲染管线的代码基本相同,因此这里不再赘述,需要的读者请参考随书光盘中的源代码。
(2)本案例的顶点着色器与普通的纹理映射顶点着色器基本相同,但片元着色器有所不同。因此下面给出本案例中的片元着色器,其代码如下。
代码位置:见随书光盘中源代码/第2章/Sample2_6/assets目录下的frag.sh。
1 precision mediump float; //给出默认的浮点精度 2 varying vec2 vTextureCoord; //从顶点着色器传递过来的纹理坐标 3 uniform sampler2D sTexture1; //纹理内容数据1(仇磊照片) 4 uniform sampler2D sTexture2; //纹理内容数据2(夏学良照片) 5 uniform float uT; //混合比例因子 6 void main(){ 7 vec4 color1 = texture2D(sTexture1, vTextureCoord); //从纹理1中采样出颜色值1 8 vec4 color2 = texture2D(sTexture2, vTextureCoord); //从纹理2中采样出颜色值2 9 gl_FragColor = color1*(1.0-uT)+ color2*uT; //按比例混合两个颜色值 10 }
说明
上述片元着色器其实非常简单,根据传入的混合比例因子将从两幅纹理图中采样得到的颜色按比例进行混合。只要混合比例因子定时变化,就自然会产生平滑过渡的效果了。