彻底理解数字图像处理中的卷积-以Sobel算子为例

  • 作者:FreeBlues
  • 修订记录

    - 2016.08.04 初稿完成

概述

卷积在信号处理领域有极其广泛的应用, 也有严格的物理和数学定义. 本文只讨论卷积在数字图像处理中的应用.

在数字图像处理中, 有一种基本的处理方法:线性滤波. 待处理的平面数字图像可被看做一个大矩阵, 图像的每个像素对应着矩阵的每个元素, 假设我们平面的分辨率是 1024*768, 那么对应的大矩阵的行数= 1024, 列数=768.

用于滤波的是一个滤波器小矩阵(也叫卷积核), 滤波器小矩阵一般是个方阵, 也就是 行数列数 相同, 比如常见的用于边缘检测的 Sobel 算子 就是两个 3*3 的小矩阵.

进行滤波就是对于大矩阵中的每个像素, 计算它周围像素和滤波器矩阵对应位置元素的乘积, 然后把结果相加到一起, 最终得到的值就作为该像素的新值, 这样就完成了一次滤波.

上面的处理过程可以参考这个示意图:

图像卷积计算示意图:

对图像大矩阵和滤波小矩阵对应位置元素相乘再求和的操作就叫卷积(Convolution)或协相关(Correlation).

协相关(Correlation)和卷积(Convolution)很类似, 两者唯一的差别就是卷积在计算前需要翻转卷积核, 而协相关则不需要翻转.

以 Sobel 算子为例

Sobel 算子 也叫 Sobel 滤波, 是两个 3*3 的矩阵, 主要用来计算图像中某一点在横向/纵向上的梯度, 看了不少网络上讲解 Sobel 算子 的文章, 发现人们常常把它的横向梯度矩阵和纵向梯度矩阵混淆. 这可能与 Sobel 算子 在它的两个主要应用场景中的不同用法有关.

Sobel 算子的两个梯度矩阵: Gx 和 Gy

这里以 Wiki 资料为准, Sobel 算子 有两个滤波矩阵: GxGy, Gx 用来计算横向的梯度, Gy 用来计算纵向的梯度, 下图就是具体的滤波器:

  • 注意:这里列出的这两个梯度矩阵对应于横向从左到右, 纵向从上到下的坐标轴, 也就是这种:
原点
O -------> x轴
|
|
|
V y轴

Sobel 算子的用途

它可以用来对图像进行边缘检测, 或者用来计算某个像素点的法线向量. 这里需要注意的是:

  • 边缘检测时: Gx 用于检测纵向边缘, Gy 用于检测横向边缘.
  • 计算法线时: Gx 用于计算法线的横向偏移, Gy 用于计算法线的纵向偏移.

计算展开

假设待处理图像的某个像素点周围的像素如下:

左上 右上
中心像素
左下 右下

那么用 Gx 计算展开为:

横向新值 = (-1)*[左上] + (-2)*[左] + (-1)*[左下] + 1*[右上] + 2*[右] + 1*[右下]

Gy 计算展开为:

纵向新值 = (-1)*[左上] + (-2)*[上] + (-1)*[右] + 1*[左下] + 2*[下] + 1*[右下]

前面说过, 做图像卷积时需要翻转卷积核, 但是我们上面的计算过程没有显式翻转, 这是因为 Sobel 算子 绕中心元素旋转 180 度后跟原来一样. 不过有些 卷积核 翻转后就变了, 下面我们详细说明如何翻转卷积核.

卷积核翻转

前面说过, 图像卷积计算, 需要先翻转卷积核, 也就是绕卷积核中心旋转 180度, 也可以分别沿两条对角线翻转两次, 还可以同时翻转行和列, 这3种处理都可以得到同样的结果.

对于第一种卷积核翻转方法, 一个简单的演示方法是把卷积核写在一张纸上, 用笔尖固定住中心元素, 旋转 180 度, 就看到翻转后的卷积核了.

下面演示后两种翻转方法, 示例如下:

假设原始卷积核为:

a b c
d e f
g h i

方法2:沿两条对角线分别翻转两次

先沿左下角到右上角的对角线翻转, 也就是 ai, bf, dh交换位置, 结果为:

i f c
h e b
g d a

再沿左上角到右下角的对角线翻转, 最终用于计算的卷积核为:

i h g
f e d
c b a

方法3:同时翻转行和列

Wiki 中对这种翻转的描述:

convolution is the process of flipping both the rows and columns of the kernel and then multiplying locationally similar entries and summing.

也是把卷积核的行列同时翻转, 我们可以先翻转行, 把 a b cg h i 互换位置, 结果为:

g h i
d e f
a b c

再翻转列, 把 g d ai f c 互换位置, 结果为:

i h g
f e d
c b a

Wiki 中有一个计算展开式, 也说明了这种翻转:

  • 注意:这里要跟矩阵乘法区分开, 这里只是借用了矩阵符号, 实际做的是对应项相乘, 再求和.

图像边缘像素的处理

以上都默认待处理的像素点周围都有像素, 但是实际上图像边缘的像素点周围的像素就不完整, 比如顶部的像素在它上方就没有像素点了, 而图像的四个角的像素点的相邻像素更少, 我们以一个图像矩阵为例:

左上角 ... ... 右上角
... ... ... ... ...
左侧 ... ... ... 右侧
... ... ... ... ...
左下角 ... ... 右下角

位于左上角的像素点的周围就只有右侧和下方有相邻像素, 遇到这种情况, 就需要补全它所缺少的相邻像素, 具体补全方法请参考下一节的代码.

用GPU进行图像卷积

如果在 CPU 上实现图像卷积算法需要进行4重循环, 效率比较差, 所以我们试着把这些卷积计算放到 GPU 上, 用 shader 实现, 结果发现性能相当好, 而且因为顶点着色器片段着色器 本质就是一个循环结构, 我们甚至不需要显式的循环, 代码也清晰了很多.

图像卷积在代码中的实际应用, 下面是一个 GLSL 形式的着色器, 它可以根据纹理贴图生成对应的法线图:

-- 用 sobel 算子生成法线图    generate normal map with sobel operator
genNormal1 = {
vertexShader = [[
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord; varying vec2 vTexCoord;
varying vec4 vColor;
varying vec4 vPosition; uniform mat4 modelViewProjection; void main()
{
vColor = color;
vTexCoord = texCoord;
vPosition = position;
gl_Position = modelViewProjection * position;
}
]], fragmentShader = [[
precision highp float; varying vec2 vTexCoord;
varying vec4 vColor;
varying vec4 vPosition; // 纹理贴图
uniform sampler2D tex;
uniform sampler2D texture; //图像横向长度-宽度, 图像纵向长度-高度
uniform float w;
uniform float h; float clamp1(float, float);
float intensity(vec4); float clamp1(float pX, float pMax) {
if (pX > pMax)
return pMax;
else if (pX < 0.0)
return 0.0;
else
return pX;
} float intensity(vec4 col) {
// 计算像素点的灰度值
return 0.3*col.x + 0.59*col.y + 0.11*col.z;
} void main() {
// 横向步长-每像素点宽度,纵向步长-每像素点高度
float ws = 1.0/w ;
float hs = 1.0/h ;
float c[10];
vec2 p = vTexCoord;
lowp vec4 col = texture2D( texture, p ); // sobel operator
// position. Gx. Gy
// 1 2 3 |-1. 0. 1.| |-1. -2. -1.|
// 4 5 6 |-2. 0. 2.| | 0. 0. 0.|
// 7 8 9 |-1. 0. 1.| | 1. 2. 1.|
// 右上角,右,右下角
c[3] = intensity(texture2D( texture, vec2(clamp(p.x+ws,0.,w), clamp(p.y+hs,0.,h) )));
c[6] = intensity(texture2D( texture, vec2(clamp1(p.x+ws,w), clamp1(p.y,h))));
c[9] = intensity(texture2D( texture, vec2(clamp1(p.x+ws,w), clamp1(p.y-hs,h)))); // 上, 下
c[2] = intensity(texture2D( texture, vec2(clamp1(p.x,w), clamp1(p.y+hs,h))));
c[8] = intensity(texture2D( texture, vec2(clamp1(p.x,w), clamp1(p.y-hs,h)))); // 左上角, 左, 左下角
c[1] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y+hs,h))));
c[4] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y,h))));
c[7] = intensity(texture2D( texture, vec2(clamp1(p.x-ws,w), clamp1(p.y-hs,h)))); // 先进行 sobel 滤波, 再把范围从 [-1,1] 调整到 [0,1]
// 注意: 比较方向要跟坐标轴方向一致, 横向从左到右, 纵向从下到上
float dx = (c[3]+2.*c[6]+c[9]-(c[1]+2.*c[4]+c[7]) + 1.0) / 2.0;
float dy = (c[7]+2.*c[8]+c[9]-(c[1]+2.*c[2]+c[3]) + 1.0) / 2.0;
float dz = (1.0 + 1.0) / 2.0; gl_FragColor = vec4(vec3(dx,dy,dz), col.a); }
]]
}

后续有时间的话考虑写一个 APP 来用动画过程模拟图像卷积的计算过程.

参考

图像卷积与滤波的一些知识点

Sobel Derivatives

Wiki:Kernel (image processing)

彻底理解数字图像处理中的卷积-以Sobel算子为例的更多相关文章

  1. 对于Sobel算子的学习

    本来想说很多目前对于 Sobel 算子的认识,但最终还是觉得对于其掌握程度太低,没有一个系统的理解,远不足以写博客,但为了12月不至于零输出,还是决定把自己学习过程中找到的相关资料进行分享. 等到一月 ...

  2. 初探FFT在数字图像处理中的应用(fft2函数的用法)

    初探FFT在数字图像处理中的应用 一般FFT在通信等领域都做的一维变换就能够了.可是在图像处理方面,须要做二维变换,这个时候就须要用到FFT2. 在利用Octave(或者matlab)里面的fft2( ...

  3. 理解NLP中的卷积神经网络(CNN)

    此篇文章是Denny Britz关于CNN在NLP中应用的理解,他本人也曾在Google Brain项目中参与多项关于NLP的项目. · 翻译不周到的地方请大家见谅. 阅读完本文大概需要7分钟左右的时 ...

  4. 我对sobel算子的理解

    转自:http://blog.csdn.net/yanmy2012/article/details/8110316 索贝尔算子(Sobeloperator)主要用作边缘检测,在技术上,它是一离散性差分 ...

  5. EasyPR--开发详解(3)高斯模糊、灰度化和Sobel算子

    在上篇文章中我们了解了PlateLocate的过程中的所有步骤.在本篇文章中我们对前3个步骤,分别是高斯模糊.灰度化和Sobel算子进行分析. 一.高斯模糊 1.目标 对图像去噪,为边缘检测算法做准备 ...

  6. sobel算子

    #1,个人理解 网上查了很多资料,都说sobel算子是用来检测边缘的,分别给了两个方向上的卷积核,然后说明做法,就说这就是sobel算子.对于我个人来说,还有很多不明白的地方,所以理清下思路. #2, ...

  7. 【OpenCV新手教程之十二】OpenCV边缘检測:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/25560901 作者:毛星云(浅墨) ...

  8. [OpenCV入门教程之十二】OpenCV边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑

    http://blog.csdn.net/poem_qianmo/article/details/25560901 本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog ...

  9. 学习 opencv---(11)OpenC 边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器

    本篇文章中,我们将一起学习OpenCV中边缘检测的各种算子和滤波器——Canny算子,Sobel算子,Laplace算子以及Scharr滤波器.文章中包含了五个浅墨为大家准备的详细注释的博文配套源代码 ...

随机推荐

  1. Git.Framework 框架随手记--ORM新增操作

    本篇主要记录具体如何新增数据,废话不多说,开始进入正文. 一. 生成工程结构 上一篇已经说到了如何生成工程结构,这里在累述一次. 1. 新建项目总体结构 使用VS新建项目结构,分层结构可以随意.我们使 ...

  2. WebGame开发总结

    不知不觉我们的项目开发有2年了,这两年来走了很多弯路,也收获了很多,今天在这里做一个总结. 项目基本情况: 服务器端采用c++和c#混合开发,网络层采用c++开发,业务逻辑用c#开发.客户端采用sil ...

  3. 编写高质量代码改善C#程序的157个建议[10-12]

    前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议10.创建对象时需要考虑是否实现比较器 建议11.区别对待 ...

  4. 团队项目NABCD模型的需求分析

    团队项目NABCD模型的需求分析 NABCD模型的介绍 Need(需求)-现在市场上未被满足但又急需满足的客户需求是什么?Approach(方法)-要满足这种需求,我能够提出什么独特的方法吗?Bene ...

  5. Git代码管理心得

    一.概述: 这次按照要求进行了看似复杂,实则非常复杂并且麻烦(网上教程众多且啰嗦)的对git使用的学习,从星期六晚18:48我准备这次作业开始,直到了晚上22:44才结束电脑上的操作···(导致这篇随 ...

  6. 使用PowerDesigner创建表并导入到数据库

    使用PowerDesigner创建表并导入到数据库 刚刚学习使用PowerDesigner进行数据库的创建,下面我就分享一下如何创建表并导入到数据库. 1.首先到网上下载一下PowerDesigner ...

  7. 改Bug

    一:新闻查询失败 1.velocity:R对象里的变量不区分大小写?  哦,应该是的! 2.表单的button是默认就有提交功能的哦! 3.velocity变量在页面上的解析:  为什么会出错呢? 难 ...

  8. java操作word

    一个使用Apache POI写word文档的实例: 1 package apache.poi; 2 3 import java.io.ByteArrayInputStream; 4 import ja ...

  9. 使用Struts2和jQuery EasyUI实现简单CRUD系统(转载汇总)

    使用Struts2和jQuery EasyUI实现简单CRUD系统(一)——从零开始,ajax与Servlet的交互 使用Struts2和jQuery EasyUI实现简单CRUD系统(二)——aja ...

  10. 【CodeForces 621C】Wet Shark and Flowers

    题 There are n sharks who grow flowers for Wet Shark. They are all sitting around the table, such tha ...