这儿是 RenderDemo 的第三篇:用 OpenGL 完成高斯含糊。咱们分别在 iOS 和 Android 平台完成了用 OpenGL 对图画进行高斯含糊处理并烘托出来。作用图如下:

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

高斯含糊处理图片作用

到目前为止咱们已经在咱们的付费知识星球中供给了这些音视频 Demo 和烘托 Demo 的工程源码,均可直接下载运转:

  • iOS AVDemo(1):音频收集
  • iOS AVDemo(2):音频编码
  • iOS AVDemo(3):音频封装
  • iOS AVDemo(4):音频解封装
  • iOS AVDemo(5):音频解码
  • iOS AVDemo(6):音频烘托
  • iOS AVDemo(7):视频收集
  • iOS AVDemo(8):视频编码
  • iOS AVDemo(9):视频封装
  • iOS AVDemo(10):视频解封装
  • iOS AVDemo(11):视频转封装
  • iOS AVDemo(12):视频编码
  • iOS AVDemo(13):视频烘托
  • Android AVDemo(1):音频收集
  • Android AVDemo(2):音频编码
  • Android AVDemo(3):音频封装
  • Android AVDemo(4):音频解封装
  • Android AVDemo(5):音频解码
  • Android AVDemo(6):音频烘托
  • Android AVDemo(7):视频收集
  • Android AVDemo(8):视频编码
  • Android AVDemo(9):视频封装
  • Android AVDemo(10):视频解封装
  • Android AVDemo(11):视频转封装
  • Android AVDemo(12):视频解码
  • Android AVDemo(13):视频烘托
  • RenderDemo(1):用 OpenGL 画一个三角形(iOS+Android)
  • RenderDemo(2):用 OpenGL 烘托视频(iOS+Android)
  • RenderDemo(3):用 OpenGL 完成高斯含糊(iOS+Android)

这些源码关于学习和了解 iOS/Android 音视频开发非常容易上手。vx 搜索『gjzkeyframe』 重视『关键帧Keyframe』。发送音讯『知识星球』来获得一切源码


高斯含糊是一种柔软含糊的图画作用,含糊后的图画能够被更杂乱的算法用来发生例如炫光、景深、热浪或许毛玻璃的作用。本文将会给大家介绍高斯含糊的数学原理,以及用 OpenGL 完成高斯含糊的代码完成。

1、高斯含糊基础知识

高斯含糊(Gaussian Blur),也叫高斯滑润,是在图画处理中广泛运用的处理作用,一般用它来削减图画噪声以及下降细节层次。由于其视觉作用就像是经过一个半透明屏幕在观察图画,所以常用于生成毛玻璃作用。

从数学的视点来看,图画的高斯含糊进程便是图画与正态分布做卷积,由于正态分布又叫作高斯分布,所以这项技术就叫作高斯含糊。

1.1、基本原理

让咱们先看一个直观的例子来了解含糊这个概念。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

数据滑润

上图中,中心点是 2,周边点都是 1。

假设现在咱们想让中心点和周围的点数值上更加接近来达到咱们含糊中心点和周围点边界的目的。咱们能够让中心点取周围点的平均值,那么中心点就会从 2 变成 1,中心点就会接近周围的值,这便是数值上的滑润,也便是含糊。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

数据滑润

咱们将这个主意应用到图画上,对图画中的每一个像素点,取周围像素的平均值,自然而然就会让这幅图发生含糊作用。

当咱们取周围点的时候,所参考的范围呈现一个圆形,圆形半径越大,含糊作用就会越激烈。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

高斯含糊作用图

假如运用简略平均,明显不是很合理,由于图画都是接连的,越接近的点联系越密切,越远离的点联系越疏远。因而,加权平均更合理,间隔越近的点权重越大,间隔越远的点权重越小。

高斯含糊便是一种加权平均的含糊作用。

1.2、高斯函数的数学表达

正态分布的密度函数叫做高斯函数(Gaussian function)

在图形上,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。核算平均值的时候,咱们只需求将中心点作为原点,其他点按照其在正态曲线上的方位,分配权重,就能够得到一个加权平均值。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

一维高斯函数的图画表明

由于咱们处理的是图画,而图画能够表明为二维矩阵,其间每个元素为 ARGB 像素值,因而咱们在这儿需求延伸到二维高斯函数。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

二维高斯函数的图画表明

高斯函数的一维方式是:

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

高斯函数一维函数方程

高斯函数的二维方式是:

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

高斯函数二维函数方程

假设目前有一张宽高为 1024×1024 的图画,咱们运用上述所说的方法对这个图画上的每个点核算二维正态分布的加权,参考当时坐标邻近间隔半径为 33 的一切像素。那么能够得知,咱们需求进行的核算次数为1024 * 1024 * 33 * 33 ≈ 11.4 亿次,这明显是一个不行承受的算法,咱们需求对算法的效率进行优化。

由于二维高斯函数具有分离性,所以二维高斯函数能够拆分为两个一维高斯函数来核算(证明进程参考:二维高斯卷积核拆分红两个一维的高斯卷积核[1]),所以咱们能够将算法优化为先核算水平方向的加权函数再核算笔直方向的加权函数。这样咱们的算法核算量就从之前的1024 * 1024 * 33 * 33 ≈ 11.4 亿次 下降为1024 * 1024 * 33 * 2 ≈ 6900 万次。

RenderDemo(3):用 OpenGL 实现高斯模糊丨音视频工程示例

优化后的算法

所以,咱们的算法优化为水平方向运转一次着色器后再在笔直方向运转一次着色器。

2、iOS Demo

2.1、烘托模块

烘托模块与OpenGL 烘托视频中讲到的一致,最终是封装出一个烘托视图KFOpenGLView用于展现最后的烘托成果。这儿就不再细讲,只贴一下主要的类和类详细的功用:

  • KFOpenGLView:运用 OpenGL 完成的烘托 View,供给了设置画面填充形式的接口和烘托一帧纹路的接口。
  • KFGLFilter:完成 shader 的加载、编译和着色器程序链接,以及 FBO 的办理。同时作为烘托处理节点,供给给了接口支撑多级烘托。
  • KFGLProgram:封装了运用 GL 程序的部分 API。
  • KFGLFrameBuffer:封装了运用 FBO 的 API。
  • KFTextureFrame:表明一帧纹路目标。
  • KFFrame:表明一帧,类型能够是数据缓冲或纹路。
  • KFGLTextureAttributes:对纹路 Texture 属性的封装。
  • KFGLBase:界说了默认的 VertexShader 和 FragmentShader。

2.2、高斯含糊 Shader 完成

咱们运用KFGLFilter为它设置高斯含糊的 Shader 来完成咱们高斯含糊作用,对应的极点着色器和片段着色器的代码如下:

KFGLGaussianBlur.h

#import<Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#defineSTRINGIZE(x)#x
#defineSTRINGIZE2(x)STRINGIZE(x)
#defineSHADER_STRING(text)@STRINGIZE2(text)
externNSString*constKFGLGaussianBlurVertexShader;
externNSString*constKFGLGaussianBlurFragmentShader;
NS_ASSUME_NONNULL_END

KFGLGaussianBlur.m

#import"KFGLGaussianBlur.h"
NSString*constKFGLGaussianBlurVertexShader=SHADER_STRING
(
attributevec4position;//经过attribute通道获取极点信息。4维向量。
attributevec4inputTextureCoordinate;//经过attribute通道获取纹路坐标信息。4维向量。
varyingvec2textureCoordinate;//用于vertexshader和fragmentshader间传递纹路坐标。2维向量。
constintGAUSSIAN_SAMPLES=9;//被参考的点数目。
uniformfloatwOffset;//水平方向单位偏移。Offset越大成果越含糊。
uniformfloathOffset;//笔直方向单位偏移。Offset越大成果越含糊。
varyingvec2blurCoordinates[GAUSSIAN_SAMPLES];//被参考点的纹路坐标数组,将在vertexshader和fragmentshader间传递。2维向量数组。
voidmain()
{
gl_Position=position;
textureCoordinate=inputTextureCoordinate.xy;//将经过attribute通道获取的纹路坐标数据中的2维分量传给fragmentshader。
intmultiplier=0;
vec2blurStep;
vec2singleStepOffset=vec2(hOffset,wOffset);
for(inti=0;i<GAUSSIAN_SAMPLES;i++)
{
multiplier=(i-((GAUSSIAN_SAMPLES-1)/2));//每一个被参考点间隔当时纹路坐标的偏移乘数
blurStep=float(multiplier)*singleStepOffset;//每一个被参考点间隔当时纹路坐标的偏移
blurCoordinates[i]=inputTextureCoordinate.xy+blurStep;//每一个被参考点的纹路坐标
}
}
);
NSString*constKFGLGaussianBlurFragmentShader=SHADER_STRING
(
varyinghighpvec2textureCoordinate;//从vertexshader传递来的纹路坐标。
uniformsampler2DinputImageTexture;//经过uniform通道获取纹路信息。2D纹路。
constlowpintGAUSSIAN_SAMPLES=9;//被参考的点数目。
varyinghighpvec2blurCoordinates[GAUSSIAN_SAMPLES];//从vertexshader传递来的被参考点的纹路坐标数组。
voidmain()
{
lowpvec4sum=vec4(0.0);
//根据间隔当时点间隔远近分配权重。分配原则越近权重越大。
sum+=texture2D(inputImageTexture,blurCoordinates[0])*0.05;
sum+=texture2D(inputImageTexture,blurCoordinates[1])*0.09;
sum+=texture2D(inputImageTexture,blurCoordinates[2])*0.12;
sum+=texture2D(inputImageTexture,blurCoordinates[3])*0.15;
sum+=texture2D(inputImageTexture,blurCoordinates[4])*0.18;
sum+=texture2D(inputImageTexture,blurCoordinates[5])*0.15;
sum+=texture2D(inputImageTexture,blurCoordinates[6])*0.12;
sum+=texture2D(inputImageTexture,blurCoordinates[7])*0.09;
sum+=texture2D(inputImageTexture,blurCoordinates[8])*0.05;
//加权。
gl_FragColor=sum;
}
);

2.3、图画转纹路

咱们还需求完成一个KFUIImageConvertTexture类用于完成图片转纹路,之后再对纹路运用 OpenGL 进行处理。代码如下:

KFUIImageConvertTexture.h

#import<Foundation/Foundation.h>
#import<OpenGLES/EAGL.h>
#import<UIKit/UIKit.h>
#import"KFTextureFrame.h"
@interfaceKFUIImageConvertTexture:NSObject
+(KFTextureFrame*)renderImage:(UIImage*)image;
@end

KFUIImageConvertTexture.m

#import"KFUIImageConvertTexture.h"
@implementationKFUIImageConvertTexture
+(KFTextureFrame*)renderImage:(UIImage*)image{
CGImageRefcgImageRef=[imageCGImage];
GLuintwidth=(GLuint)CGImageGetWidth(cgImageRef);
GLuintheight=(GLuint)CGImageGetHeight(cgImageRef);
CGRectrect=CGRectMake(0,0,width,height);

CGColorSpaceRefcolorSpace=CGColorSpaceCreateDeviceRGB();

void*imageData=malloc(width*height*4);
CGContextRefcontext=CGBitmapContextCreate(imageData,width,height,8,width*4,colorSpace,kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big);

CGColorSpaceRelease(colorSpace);
CGContextClearRect(context,rect);
CGContextDrawImage(context,rect,cgImageRef);

glEnable(GL_TEXTURE_2D);

GLuinttextureID;
glGenTextures(1,&textureID);
glBindTexture(GL_TEXTURE_2D,textureID);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,imageData);

//结束后要做清理
glBindTexture(GL_TEXTURE_2D,0);//解绑
CGContextRelease(context);
free(imageData);

KFTextureFrame*inputFrame=[[KFTextureFramealloc]initWithTextureId:textureIDtextureSize:CGSizeMake(width,height)time:kCMTimeZero];
returninputFrame;
}

2.4、展现高斯含糊烘托成果

咱们在一个 ViewController 串联对图片进行高斯含糊处理的逻辑,并展现最后的作用。代码如下:

#import"KFGaussianBlurViewController.h"
#import"KFUIImageConvertTexture.h"
#import"KFOpenGLView.h"
#import"KFGLFilter.h"
#import"KFGLGaussianBlur.h"
@interfaceKFGaussianBlurViewController()
@property(nonatomic,strong)KFOpenGLView*glView;
@property(nonatomic,strong)KFUIImageConvertTexture*imageConvertTexture;
@property(nonatomic,strong)EAGLContext*context;
@property(nonatomic,strong)KFGLFilter*verticalGaussianBlurFilter;
@property(nonatomic,strong)KFGLFilter*horizonalGaussianBlurFilter;
@end
@implementationKFGaussianBlurViewController
#pragmamark-Property
-(EAGLContext*)context{
if(!_context){
_context=[[EAGLContextalloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
}

return_context;
}
-(KFUIImageConvertTexture*)imageConvertTexture{
if(!_imageConvertTexture){
_imageConvertTexture=[[KFUIImageConvertTexturealloc]init];
}
return_imageConvertTexture;
}
-(KFGLFilter*)verticalGaussianBlurFilter{
if(!_verticalGaussianBlurFilter){
_verticalGaussianBlurFilter=[[KFGLFilteralloc]initWithCustomFBO:NOvertexShader:KFGLGaussianBlurVertexShaderfragmentShader:KFGLGaussianBlurFragmentShader];
[_verticalGaussianBlurFiltersetFloatUniformValue:@"hOffset"floatValue:0.00390625f];
}
return_verticalGaussianBlurFilter;
}
-(KFGLFilter*)horizonalGaussianBlurFilter{
if(!_horizonalGaussianBlurFilter){
_horizonalGaussianBlurFilter=[[KFGLFilteralloc]initWithCustomFBO:NOvertexShader:KFGLGaussianBlurVertexShaderfragmentShader:KFGLGaussianBlurFragmentShader];
[_horizonalGaussianBlurFiltersetFloatUniformValue:@"wOffset"floatValue:0.00390625f];
}
return_horizonalGaussianBlurFilter;
}
#pragmamark-Lifecycle
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetupUI];

[selfapplyGaussianBlurEffect];
}
-(void)viewWillLayoutSubviews{
[superviewWillLayoutSubviews];
self.glView.frame=self.view.bounds;
}
-(void)setupUI{
self.edgesForExtendedLayout=UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars=YES;
self.title=@"GaussianBlur";
self.view.backgroundColor=[UIColorwhiteColor];

//烘托view。
_glView=[[KFOpenGLViewalloc]initWithFrame:self.view.boundscontext:self.context];
_glView.fillMode=KFGLViewContentModeFit;
[self.viewaddSubview:self.glView];
}
-(void)applyGaussianBlurEffect{
[EAGLContextsetCurrentContext:self.context];
UIImage*baseImage=[UIImageimageNamed:@"KeyframeLogo"];
KFTextureFrame*textureFrame=[KFUIImageConvertTexturerenderImage:baseImage];

//笔直方向做一次高斯含糊。
KFTextureFrame*verticalTexture=[self.verticalGaussianBlurFilterrender:textureFrame];

//水平方向做一次高斯含糊。
KFTextureFrame*horizonalTexture=[self.horizonalGaussianBlurFilterrender:verticalTexture];

[self.glViewdisplayFrame:horizonalTexture];
[EAGLContextsetCurrentContext:nil];
}
@end

经过上面的代码,能够看到咱们是用KFGLFilter来封装一次 OpenGL 的处理节点,它能够接收一个KFTextureFrame目标,加载 Shader 对其进行烘托处理,处理完后输出处理后的KFTextureFrame,然后能够接着交给下一个KFGLFilter来处理,就像一条烘托链。

这儿咱们把高斯含糊用到的二维高斯卷积核拆成两个一维的高斯卷积核,所以是分别做了一次笔直方向和一次水平风向的处理。

3、Android Demo

Android 完成高斯含糊的 Demo 咱们是在OpenGL 烘托视频 Demo的基础上在相机返回的视频帧被烘托前添加了高斯含糊的处理。对应视频收集含糊和视频烘托模块这儿就不再细讲,只贴一下主要的类和类详细的功用:

  • KFGLContext:担任创立 OpenGL 环境,担任办理和组装 EGLDisplay、EGLSurface、EGLContext。
  • KFGLFilter:完成 shader 的加载、编译和着色器程序链接,以及 FBO 的办理。同时作为烘托处理节点,供给给了接口支撑多级烘托。
  • KFGLProgram:担任加载和编译着色器,创立着色器程序容器。
  • KFGLBase:界说了默认的 VertexShader 和 FragmentShader。
  • KFSurfaceView:KFSurfaceView 承继自 SurfaceView 来完成烘托。
  • KFTextureView:KFTextureView 承继自 TextureView 来完成烘托。
  • KFFrame:表明一帧,类型能够是数据缓冲或纹路。
  • KFRenderView:KFRenderView 是一个容器,能够选择运用 KFSurfaceView 或 KFTextureView 作为实践的烘托视图。

完成高斯含糊的极点着色器代码和片段着色器代码如下:

publicstaticStringdefaultGaussianVertexShader=
"attributevec4position;\n"+
"attributevec4inputTextureCoordinate;\n"+
"varyingvec2textureCoordinate;\n"+
"constintGAUSSIAN_SAMPLES=9;\n"+
"uniformfloatwOffset;\n"+
"uniformfloathOffset;\n"+
"varyingvec2blurCoordinates[GAUSSIAN_SAMPLES];\n"+
"voidmain()\n"+
"{\n"+
"gl_Position=position;\n"+
"textureCoordinate=inputTextureCoordinate.xy;\n"+
"intmultiplier=0;\n"+
"vec2blurStep;\n"+
"vec2singleStepOffset=vec2(hOffset,wOffset);\n"+
"for(inti=0;i<GAUSSIAN_SAMPLES;i++)\n"+
"{\n"+
"multiplier=(i-((GAUSSIAN_SAMPLES-1)/2));\n"+
"blurStep=float(multiplier)*singleStepOffset;\n"+
"blurCoordinates[i]=inputTextureCoordinate.xy+blurStep;\n"+
"}\n"+
"}\n";
publicstaticStringdefaultGaussianFragmentShader=
"varyinghighpvec2textureCoordinate;\n"+
"uniformsampler2DinputImageTexture;\n"+
"constlowpintGAUSSIAN_SAMPLES=9;\n"+
"varyinghighpvec2blurCoordinates[GAUSSIAN_SAMPLES];\n"+
"voidmain()\n"+
"{\n"+
"lowpvec4sum=vec4(0.0);\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[0])*0.05;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[1])*0.09;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[2])*0.12;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[3])*0.15;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[4])*0.18;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[5])*0.15;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[6])*0.12;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[7])*0.09;\n"+
"sum+=texture2D(inputImageTexture,blurCoordinates[8])*0.05;\n"+
"gl_FragColor=sum;\n"+
"}\n";

MainActivity的改动则主要是在KFVideoCaptureListeneronFrameAvailable回调中添加对图画帧做高斯含糊的处理逻辑,再进行烘托即可。代码如下:

publicvoidonFrameAvailable(KFFrameframe){
mGLContext.bind();
if(mVerticalGLFilter==null){
mVerticalGLFilter=newKFGLFilter(false,defaultGaussianVertexShader,defaultGaussianFragmentShader);
mVerticalGLFilter.setFloatUniformValue("hOffset",0.00390625f);
}
if(mHoritizalGLFilter==null){
mHoritizalGLFilter=newKFGLFilter(false,defaultGaussianVertexShader,defaultGaussianFragmentShader);
mHoritizalGLFilter.setFloatUniformValue("wOffset",0.00390625f);
}
KFFramefilterFrame=mVerticalGLFilter.render((KFTextureFrame)frame);
KFFramehFilterFrame=mHoritizalGLFilter.render((KFTextureFrame)filterFrame);
mRenderView.render((KFTextureFrame)hFilterFrame);
mGLContext.unbind();
}

可见,当咱们用KFGLFilter将 OpenGL 烘托才能封装起来,并能够像添加烘托处理节点相同往现有烘托链中添加新的图画处理功用时,相关改动就变得很方便了。

参考:

  • 高斯含糊的分离性[2]
  • 高斯含糊的原理[3]
  • Efficient Gaussian blur with linear sampling[4]
  • 高斯含糊算法[5]
  • 高斯含糊 OpenGL 代码完成[6]

参考资料

[1]

二维高斯卷积核拆分红两个一维的高斯卷积核:www.zhihu.com/question/36…

[2]

高斯含糊的分离性:www.zhihu.com/question/36…

[3]

高斯含糊的原理:zhuanlan.zhihu.com/p/493628728

[4]

Efficient Gaussian blur with linear sampling:www.rastergrid.com/blog/2010/0…

[5]

高斯含糊算法:www.ruanyifeng.com/blog/2012/1…

[6]

高斯含糊OpenGL代码完成:cloud.tencent.com/developer/a…

– 完 –