GPUImage源码解读(四) - 图像锐化

边缘模糊是图像中经常出现的质量问题,由此造成的轮廓不清晰,线条不鲜明,使图像特征提取、识别和理解难以进行。增强图像边缘和线条,使图像边缘变得清晰的处理就是我们所说的图像锐化。

在移动设备上使用GPU做图像锐化,一般就是利用空域滤波器对图像做模板卷积处理,主要步骤如下:
1)、用模板遍历图像,使模板中心分别与图中像素重合
2)、将模板上各点位的系数分别与图像中对应像素相乘
3)、将所有乘积相加
4)、将和赋值给对应中心像素

图像锐化中常用的方法主要有梯度运算、拉普拉斯算子等。

梯度算法

梯度算法的结果值与相邻像素的灰度差值成正比,图像经过梯度运算后,留下灰度值急剧变化的边沿处的点。GPUImage中可以找到Sobel算子和Prewitt算子的具体实现,Sobel和Prewitt都是3x3模板的梯度运算,其模板表示如下:

GPUImageSobelEdgeDetectionFilter是GPUImage利用Sobel算子实现的一种边缘检测器,其运行效果如下:

接下来我们深入源码,看一下这样一个滤镜在GPUImage中具体是怎样实现的。

@interface GPUImageSobelEdgeDetectionFilter : GPUImageTwoPassFilter

首先GPUImageSobelEdgeDetectionFilter是继承自GPUImageTwoPassFilter的。GPUImageTwoPassFilter内部会管理2个GLProgram,每个GLProgram代表一個GPU处理过程。这里之所以需要2个GLProgram是因为梯度运算基于灰度计算灰度变化,因此第一个GLProgram用于将图像转换成灰度(具体使用的是GPUImageGrayscaleFilter.h中的shader),第二个GLProgram才是真正用于处理sobel算法的shader。

if (!(self = [super initWithFirstStageVertexShaderFromString:kGPUImageVertexShaderString firstStageFragmentShaderFromString:kGPUImageLuminanceFragmentShaderString secondStageVertexShaderFromString:kGPUImageNearbyTexelSamplingVertexShaderString secondStageFragmentShaderFromString:fragmentShaderString]))
{
    return nil;
}
...

初始化过程非常简单,直接调用父类的初始化接口,分别传入2个GLProgram需要的shader即可。灰度变换的shader就不在这里细看了,我们重点看一下sobel算法的vertex shader和fragment shader。

NSString *const kGPUImageNearbyTexelSamplingVertexShaderString = SHADER_STRING
(
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 
 uniform float texelWidth;
 uniform float texelHeight; 
 
 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;
 
 void main()
 {
     gl_Position = position;
     
     vec2 widthStep = vec2(texelWidth, 0.0);
     vec2 heightStep = vec2(0.0, texelHeight);
     vec2 widthHeightStep = vec2(texelWidth, texelHeight);
     vec2 widthNegativeHeightStep = vec2(texelWidth, -texelHeight);
     
     textureCoordinate = inputTextureCoordinate.xy;
     leftTextureCoordinate = inputTextureCoordinate.xy - widthStep;
     rightTextureCoordinate = inputTextureCoordinate.xy + widthStep;
     
     topTextureCoordinate = inputTextureCoordinate.xy - heightStep;
     topLeftTextureCoordinate = inputTextureCoordinate.xy - widthHeightStep;
     topRightTextureCoordinate = inputTextureCoordinate.xy + widthNegativeHeightStep;
     
     bottomTextureCoordinate = inputTextureCoordinate.xy + heightStep;
     bottomLeftTextureCoordinate = inputTextureCoordinate.xy - widthNegativeHeightStep;
     bottomRightTextureCoordinate = inputTextureCoordinate.xy + widthHeightStep;
 }
);

fragment shader的计算资源比较珍贵,可以看到这里有个比较取巧的做法,把每个像素点四周的坐标点计算放在了vertex shader,然后利用varying变量传给fragment shader。这样在fragment shader中要取四周的像素值时只要直接把穿过来的坐标点拿来用就可以了。

NSString *const kGPUImageSobelEdgeDetectionFragmentShaderString = SHADER_STRING
(
 precision mediump float;

 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;
 uniform float edgeStrength;
 
 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 = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;
    float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
    
    float mag = length(vec2(h, v)) * edgeStrength;
    
    gl_FragColor = vec4(vec3(mag), 1.0);
 }
);

在fragment shader中,获取四周的像素值,再分别按照sobel算子计算横向和纵向的乘积。最后以横向和纵向差值为坐标计算向量长度,并且把最终的长度值作为结果。

filter中的其它代码主要是一些属性设置,几乎不需要额外的GL代码调用,GPU调用的部分基本都被GPUImage封裝好了,扩展起来还是非常方便的。filter的使用也非常简单:

GPUImagePicture *gpuPic = [[GPUImagePicture alloc] initWithImage:sampleImage];
GPUImageSobelEdgeDetectionFilter *sobelFilter = [[GPUImageSobelEdgeDetectionFilter alloc] init];
[gpuPic addTarget:sobelFilter];
[gpuPic processImageUpToFilter:sobelFilter withCompletionHandler:^(UIImage *processedImage) {
    // do something
}];

拉普拉斯算子

拉普拉斯算法比较适合用于改善图像模糊,是比较常用的边缘增强处理算子,其模板表示有如下几种:

GPUImage中同样可以找到关于拉普拉斯算法的具体实现GPUImageLaplacianFilter、GPUImageSharpenFilter。GPUImageSharpenFilter是基于拉普拉斯算子的一种拉氏锐化,其运行效果如下:

深入源码,看一下GPUImageSharpenFilter的具体实现

@interface GPUImageSharpenFilter : GPUImageFilter

GPUImageSharpenFilter比较简单,直接继承自GPUImageFilter,只有一个GLProgram。

if (!(self = [super initWithVertexShaderFromString:kGPUImageSharpenVertexShaderString fragmentShaderFromString:kGPUImageSharpenFragmentShaderString]))
{
    return nil;
}
...

初始化过程同样是调用父类的初始化接口,传入shader即可。

NSString *const kGPUImageSharpenVertexShaderString = SHADER_STRING
(
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 
 uniform float imageWidthFactor; 
 uniform float imageHeightFactor; 
 uniform float sharpness;
 
 varying vec2 textureCoordinate;
 varying vec2 leftTextureCoordinate;
 varying vec2 rightTextureCoordinate; 
 varying vec2 topTextureCoordinate;
 varying vec2 bottomTextureCoordinate;
 
 varying float centerMultiplier;
 varying float edgeMultiplier;
 
 void main()
 {
     gl_Position = position;
     
     vec2 widthStep = vec2(imageWidthFactor, 0.0);
     vec2 heightStep = vec2(0.0, imageHeightFactor);
     
     textureCoordinate = inputTextureCoordinate.xy;
     leftTextureCoordinate = inputTextureCoordinate.xy - widthStep;
     rightTextureCoordinate = inputTextureCoordinate.xy + widthStep;
     topTextureCoordinate = inputTextureCoordinate.xy + heightStep;     
     bottomTextureCoordinate = inputTextureCoordinate.xy - heightStep;
     
     centerMultiplier = 1.0 + 4.0 * sharpness;
     edgeMultiplier = sharpness;
 }
);

同样的技巧,相邻坐标的计算放在了vertex shader。

NSString *const kGPUImageSharpenFragmentShaderString = SHADER_STRING
(
 precision highp float;
 
 varying highp vec2 textureCoordinate;
 varying highp vec2 leftTextureCoordinate;
 varying highp vec2 rightTextureCoordinate; 
 varying highp vec2 topTextureCoordinate;
 varying highp vec2 bottomTextureCoordinate;
 
 varying highp float centerMultiplier;
 varying highp float edgeMultiplier;

 uniform sampler2D inputImageTexture;
 
 void main()
 {
     mediump vec3 textureColor = texture2D(inputImageTexture, textureCoordinate).rgb;
     mediump vec3 leftTextureColor = texture2D(inputImageTexture, leftTextureCoordinate).rgb;
     mediump vec3 rightTextureColor = texture2D(inputImageTexture, rightTextureCoordinate).rgb;
     mediump vec3 topTextureColor = texture2D(inputImageTexture, topTextureCoordinate).rgb;
     mediump vec3 bottomTextureColor = texture2D(inputImageTexture, bottomTextureCoordinate).rgb;

     gl_FragColor = vec4((textureColor * centerMultiplier - (leftTextureColor * edgeMultiplier + rightTextureColor * edgeMultiplier + topTextureColor * edgeMultiplier + bottomTextureColor * edgeMultiplier)), texture2D(inputImageTexture, bottomTextureCoordinate).w);
 }
);

fragment shader分别获取上下左右的像素值,按照模板系数计算乘积。可以注意到,这里的模板系数其实不是固定的,而是根据外部设置的sharpness动态调节的,这样可以针对不同的图片,不同的需要设置不同的锐化强度。上面的运行效果图设置的sharpness值是1.0,不同的强度值,甚至不同的锐化模板,大家都可以自己动手尝试哦。

作者简介:Bill, 天天P图 iOS 工程师


文章后记
天天P图是由腾讯公司开发的业内领先的图像处理,相机美拍的APP。欢迎扫码或搜索关注我们的微信公众号:“天天P图攻城狮”,那上面将陆续公开分享我们的技术实践,期待一起交流学习!

加入我们
天天P图技术团队长期招聘:
(1) 深度学习(图像处理)研发工程师(上海)
工作职责

  • 开展图像/视频的深度学习相关领域研究和开发工作;
  • 负责图像/视频深度学习算法方案的设计与实现;
  • 支持社交平台部产品前沿深度学习相关研究。

工作要求

  • 计算机等相关专业硕士及以上学历,计算机视觉等方向优先;
  • 掌握主流计算机视觉和机器学习/深度学习等相关知识,有相关的研究经历或开发经验;
  • 具有较强的编程能力,熟悉C/C++、python;
  • 在人脸识别,背景分割,体态跟踪等技术方向上有研究经历者优先,熟悉主流和前沿的技术方案优先;
  • 宽泛的技术视野,创造性思维,富有想象力;
  • 思维活跃,能快速学习新知识,对技术研发富有激情。

(2) AND / iOS 开发工程师 
(3) 图像处理算法工程师
期待对我们感兴趣或者有推荐的技术牛人加入我们(base 上海)!联系方式:ttpic_dev@qq.com

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
GPUImage源码解读(四) - 图像锐化
边缘模糊是图像中经常出现的质量问题,由此造成的轮廓不清晰,线条不鲜明,使图像特征提取、识别和理解难以进行。
<<上一篇
下一篇>>