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

本文分享自微信公众号 - 天天P图攻城狮(ttpic_dev)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-12-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏机器学习AI算法工程

如何通过数据挖掘手段分析网民的评价内容?

近年来微博等用户自媒体的爆炸式增长,使得利用计算机挖掘网民意见不但变得可行,而且变得必须。这其中很重要的一项任务就是挖掘网民意见所讨论的对象,即评价对象。本文...

77280
来自专栏数说工作室

异常值检测

之前发过一篇讨论文章——异常值怎么整。 在原文评论区里(戳此→异常值怎么整?| 讨论)得到了各位大大的指教,数说君也受益匪浅,现在整理一下供大家参考: 聚类 ...

38450
来自专栏牛客网

等了大半个月的b站offer,附算法岗三面面经

9月底第一批的面试,算法岗 终于等到你== 发一波面经,回馈一下牛客 一面 1.自我介绍 2.项目介绍 非常细致深入的讨论了项目 3.索引 一维 红...

59460
来自专栏人工智能快报

美国MIT研究人员揭示神经网络运行机制

美国麻省理工学院(MIT)官网报道该校在通用神经网络方面的研究进展可用于揭示神经网络的运行机制。理解神经网络的运行机制可以帮助研究人员增强其性能,并将从中获得的...

36450
来自专栏超智能体

好多人一辈子都没搞清什么是学习

老师和家长总是告诉我们要好好学习,可从没有人告诉过我们什么是学习,学习和记忆的区别又是什么。以至于很多人误以为记忆就是学习。更讽刺的是,市面上有一大堆学习方法,...

24340
来自专栏机器学习算法与Python学习

tweet情感分析流程

关键字全网搜索最新排名 【机器学习算法】:排名第一 【机器学习】:排名第二 【Python】:排名第三 【算法】:排名第四 前言 自然语言处理(NLP)中一个很...

39880
来自专栏AI科技大本营的专栏

探索 | 神经网络到底是如何思考的?MIT精英们做了这么一个实验室来搞清楚

作者 | Larry Hardesty等 编译 | ziqi Zhang 没错!人工智能是很火,神经网络也很火,但你真的懂它吗?神经网络到底是怎么工作的?没有...

34790
来自专栏机器之心

谷歌微软等科技巨头数据科学面试107道真题:你能答出多少?

选自Learndatasci 机器之心编译 参与:李泽南 来自 Glassdoor 的最新数据可以告诉我们各大科技公司最近在招聘面试时最喜欢向候选人提什么问题。...

30770
来自专栏专知

谷歌上线机器学习速成课程:中文配音+中文字幕+完全免费!

【导读】3月1日,Google上线了AI学习网站——Learn with Google AI,并重磅推出了机器学习速成课程MLCC,该课程基于TensorFlo...

56590
来自专栏CreateAMind

深入理解 RNN-神经图灵机(代码)

在写《深度学习与神经科学相遇》的过程中开始谈到了RNNs(Recurrent Neural Networks),我想很有必要暂停下来先对RNNs进行一些更深入的...

20130

扫码关注云+社区

领取腾讯云代金券