文档章节

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

FreeBlues
 FreeBlues
发布于 2016/08/05 00:20
字数 2128
阅读 495
收藏 3

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

  • 作者:FreeBlues
  • 修订记录
    • 2016.08.04 初稿完成

概述

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

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

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

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

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

图像卷积计算示意图:

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

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

以 Sobel 算子为例

Sobel 算子 也叫 Sobel 滤波, 是两个 3*3 的矩阵, 主要用来计算图像中某一点在横向/纵向上的梯度, 看了不少网络上讲解 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 度, 就看到翻转后的卷积核了.

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

假设原始卷积核为:

abc
def
ghi

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

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

ifc
heb
gda

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

ihg
fed
cba

方法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 互换位置, 结果为:

ghi
def
abc

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

ihg
fed
cba

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)

© 著作权归作者所有

共有 人打赏支持
FreeBlues
粉丝 98
博文 280
码字总数 493678
作品 0
其它
程序员
私信 提问
Unity Shader 学习笔记(20) 卷积、卷积核、边缘检测算子、边缘检测

Unity Shader 学习笔记(20) 卷积、卷积核、边缘检测算子、边缘检测 参考书籍:《Unity Shader 入门精要》 书中算子Bug Unity Shader 学习笔记(26) 边缘检测(深度和法线纹理) 彻底理解数...

l773575310
2017/11/22
0
0
7- OpenCV+TensorFlow 入门人工智能图像处理-彩色反转&边缘检测

彩色图像的颜色反转 不管是灰度图像的颜色反转,还是彩色颜色反转。都是255减去当前值 马赛克效果 理解马赛克效果中的关键指标 马赛克效果的窗体范围。如: 从100行开始到300行结束,列从100...

天涯明月笙
2018/05/06
0
0
Matlab图像处理-长杆在太阳下的影长度处理方法

首先来看原图像 原图像中光干扰较大,因此我们后面的处理会使用下图红框这样只有杆影的局部区域进行计算。 图像切割拆分 视频的图像帧数为25帧长度为从8:54:06到9:34:46共40min,把图像每隔3...

pulsar-v
2018/08/29
0
0
教你在真实图像数据上应用线性滤波器

雷锋网按:本文为AI研习社编译的技术博客,原标题 Learning to perform linear filtering using natural image data,作者为 Josef Taher 。 翻译 | 张泽坤 王霏 校对 | 余杭 整理 | MY 卷积...

雷锋字幕组
2018/08/14
0
0
OpenCV3 imgproc模块的基本方法

本文用于实战OpenCV3 imgproc模块的基本方法!!! 具体函数定义可参考OpenCV3 Library API参考网站: https://docs.opencv.org/3.0-beta/modules/refman.html / main.c / include include ...

attitude_yu
2018/04/16
0
0

没有更多内容

加载失败,请刷新页面

加载更多

nginx反向代理配置去除前缀

使用nginx做反向代理的时候,可以简单的直接把请求原封不动的转发给下一个服务。设置proxy_pass请求只会替换域名,如果要根据不同的url后缀来访问不同的服务,则需要通过如下方法: 方法一:...

架构师springboot
21分钟前
1
0
QianBill API 开发笔记

JWT

BeanHo
32分钟前
3
0
Elasticsearch实战篇——Spring Boot整合ElasticSearch

当前Spring Boot很是流行,包括我自己,也是在用Spring Boot集成其他框架进行项目开发,所以这一节,我们一起来探讨Spring Boot整合ElasticSearch的问题。 本文主要讲以下内容: 第一部分,通...

JAVA_冯文议
41分钟前
3
0
不错的linux下通用的java程序启动脚本

#!/bin/sh#该脚本为Linux下启动java程序的通用脚本。即可以作为开机自启动service脚本被调用,#也可以作为启动java程序的独立脚本来使用。##Author: tudaxia.com, Date: 2011/6/7...

sprouting
今天
3
0
Linux manjaro系统安装后无法连接wifi,解决方案

笔记本为联想 thinkpad E480 首先通过命令lspci -k看一下原因是否为缺少wifi驱动,如下,如果没有Kernel driver in use,说明缺少驱动。 05:00.0 Network controller: Realtek Semiconducto...

bluecoffee
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部