图像处理 空间卷积原理

2017-11-14 13:55:12 akadiao 阅读数 7691

空间卷积-Spatial convolution

译自Spatial convolution

卷积是对f和g两个函数的运算,产生了第三个函数,可以理解为f的一个修改(“过滤”)版本。 在这个解释中,我们称之为过滤器。 如果f定义在像x这样的空间变量而不是像t这样的时间变量上,我们称之为空间卷积运算。 卷积是执行平滑或锐化的任何物理设备或计算过程的核心。 应用于像图像这样的二维函数,对于边缘寻找,特征检测,运动检测,图像匹配以及无数其他任务也是有用的。 形式上,对于连续变量x的函数f(x)和g(x),卷积定义为:

这里写图片描述

其中*表示卷积,并且意味着普通的乘法。 对于离散变量x的函数,即数组数组,其定义如下:

这里写图片描述

最后,对于两个变量x和y(例如图像)的函数,这些定义变成:

这里写图片描述

以及

这里写图片描述

在数码摄影中,镜头产生的图像是一个连续的函数f(x,y)。在传感器前放置一个抗混叠滤波器,用平滑滤波器g(x,y)来对图像进行卷积。就是上面的第三个方程式。一旦图像被传感器记录并存储在文件中,将文件加载到Photoshop中并使用滤波器g [x,y]对其进行锐化就是第四个方程式。

尽管定义很简单,但卷积是一个难以获得直觉的概念,并且通过对特定函数应用特定的过滤器而获得的效果并不总是显而易见的。在这个小程序中,我们探讨连续一维函数(第一个方程)和离散二维函数(第四个方程)的卷积。

这里写图片描述

Applet参见Applet链接

一维函数的卷积

小程序的左侧是一维函数(“信号”)。这是f。可以借助该功能进行更改,但暂时保留原有功能。在此之下是一维滤镜菜单。这是g。你可以选择“custom/自定义”,也可以使用过滤功能,选择后离开。底部是由g卷积f的结果。点击几个过滤器。请注意,“big rect”比“rect”模糊不清,但是很多地方会产生kinks(尖锐的转折点)。还要注意,“gaussian”模糊不及“big rect”,但不会产生kinks(尖锐的转折点)。

这两个函数(f和g)都被描绘成好像它们是连续变量x的函数,所以看起来这个可视化显示了连续函数的卷积(上面的第一个方程)。在实践中,这两个函数被精细地采样并用一维数组表示。这些数字在绘制时使用线连接,呈现出连续的功能。实际上在小程序脚本中执行的卷积是两个离散函数(上面的第二个方程)。

无论我们将卷积视为连续的还是离散的,其解释都是相同的:对于输出函数中的每个位置x,我们将滤波函数g向左或向右移动直到它在该位置居中,我们从左到右翻转,我们将f上的每一点乘以移位的g上的相应点,并且将这些乘积相加(或集成)在一起。从左到右的翻转是因为不明原因,卷积方程定义为g [x-k],而不是g [x + k](以第二个方程为例)。

另一种思考卷积的方法

如果这个过程有点困难,可以用下面的方法来描述它,这个过程可能更容易看出来:在输出函数的每个位置x,我们放置一个过滤器g的副本,在该位置左右,从左到右翻转,并根据该位置处的信号f的值放大或缩小。放下这些副本之后,如果我们把它们全部加在一起,我们就可以得到正确的答案!

要看到这种理解卷积的另一种方法,点击“animate”,然后点击“big rect”。动画从原始信号f开始,然后将过滤器g的副本放置在沿着f的位置,根据该位置处的f的高度将它们垂直拉伸,然后将这些副本相加在一起以制作厚输出曲线。尽管动画只显示了几十个过滤器的副本,但实际上每个位置x需要一个副本。另外,对于这个过程来说,复制的总和必须除以过滤函数下的区域,称为标准化。否则,输出将比输入更高或更低,而不是简单地被平滑或锐化。对于除“custom/自定义”之外的所有过滤器,在绘制厚输出曲线之前,会为您执行标准化。对于“custom/自定义”过滤器,请参阅下文。

一旦你明白这是如何工作,请尝试“sharpen”或“shift”过滤器。锐化滤波器用它的直接邻居的加权和代替f的每个值,但是将邻居的值减去一点。这些减法的作用是增强原始信号中的特征。在Photoshop中的“Sharpen”过滤器做到这一点;就像视网膜中的某些神经元层。移位滤波器将f的每个值替换为距离右边一定距离的邻居的f值。 (是的,在右边,尽管滤波器的尖峰在左边,请记住,卷积滤波器函数在应用之前从左向右翻转)。

最后,点击“custom/自定义”,然后尝试绘制自己的过滤器。如果过滤器的面积大于或小于1.0,则输出功能将分别向上或向下跳跃。为了避免这种情况,请点击“normalize”,这会使过滤器向上或向下缩放,直到其面积恰好为1.0。顺便说一下,如果你为自定义过滤器的应用设置了动画效果,那么如果你的自定义过滤器在其x = 0位置达到y = 1.0,缩放的副本将仅触碰原始函数上的相应点。无论如何,如果你的过滤器是标准化的,输出功能将是正确的高度。

二维函数卷积

在applet的右侧,我们将这些思想扩展到二维离散函数,特别是普通的摄影图像。最初的2D信号在顶部,2d滤波器位于中间,被描绘成一组数字,输出在底部。单击不同的过滤函数并观察结果。“sharpen”和“edges”之间的唯一区别是中间过滤值从9到8之间的变化。然而,这一变化是至关重要的,正如你所看到的。特别是,“edges”中所有非零滤波值之和为零。因此,原始信号中平滑的位置(如背景),卷积的输出为零(即黑色)。这种“hand shake”类似于长曝光照片中发生的情况,在这种情况下,相机的目标在曝光时从左上方移动到右下方。

最后,单击“identity”,将中间筛选器抽头(有时称为筛选器中的位置)设置为1,其余的设置为0。毫不奇怪,这仅仅是对原始信号的复制。现在点击“自定义”,然后点击单独的抽头,为他们输入新的值。当你进入每一个值,重新计算的卷积。尝试创建平滑滤波或锐化滤波。或从“identity”开始,将中间抽头改为0.5或2。图像缩小或放大了吗?Applet是裁剪输出0(黑色)和255(白色),所以若增加强度,则图像将饱和-就像一个相机曝光的时间太长。试着在左上角和右下角放置1个,把其他的设置为0。会有双像吗?与自定义1D过滤器一样,如果过滤值不等于1,则可能需要按“normalize”。除非你正在寻找边缘,在这种情况下,它们应该等于0。


卷积示意:

这里写图片描述

演示:

# #!/usr/bin/python
# # coding:utf-8

import cv2
import numpy as np

img = cv2.imread('daibola.jpg', 1)
# # 卷积核
rect = np.array([[0, 0, 0, 0, 0],
                 [0, 0.11, 0.11, 0.11, 0],
                 [0, 0.11, 0.11, 0.11, 0],
                 [0, 0.11, 0.11, 0.11, 0],
                 [0, 0, 0, 0, 0]], np.float32)

bigrect = np.array([[0.04, 0.04, 0.04, 0.04, 0.04],
                    [0.04, 0.04, 0.04, 0.04, 0.04],
                    [0.04, 0.04, 0.04, 0.04, 0.04],
                    [0.04, 0.04, 0.04, 0.04, 0.04],
                    [0.04, 0.04, 0.04, 0.04, 0.04]], np.float32)

gaussian = np.array([[0.01, 0.02, 0.03, 0.02, 0.01],
                     [0.02, 0.06, 0.08, 0.06, 0.02],
                     [0.03, 0.08, 0.11, 0.08, 0.03],
                     [0.02, 0.06, 0.08, 0.06, 0.02],
                     [0.01, 0.02, 0.03, 0.02, 0.01]], np.float32)

sharpen = np.array([[0, 0, 0, 0, 0],
                    [0.02, 0.06, 0.08, 0.06, 0.02],
                    [0.03, 0.08, 0.11, 0.08, 0.03],
                    [0.02, 0.06, 0.08, 0.06, 0.02],
                    [0.01, 0.02, 0.03, 0.02, 0.01]], np.float32)

edges = np.array([[0, 0, 0, 0, 0],
                  [0, 0, -2, 0, 0],
                  [0, -2, 8, -2, 0],
                  [0, 0, -2, 0, 0],
                  [0, 0, 0, 0, 0]], np.float32)

shift = np.array([[0, 0, 0, 0, 0],
                  [0, 0, 0, 0, 0],
                  [1, 0, 0, 0, 0],
                  [0, 0, 0, 0, 0],
                  [0, 0, 0, 0, 0]], np.float32)

handshake = np.array([[0.2, 0, 0, 0, 0],
                      [0, 0.2, 0, 0, 0],
                      [0, 0, 0.2, 0, 0],
                      [0, 0, 0, 0.2, 0],
                      [0, 0, 0, 0, 0.2]], np.float32)


# 该函数实际上计算相关性,而不是卷积.  ddepth:目标图像的所需深度
rectimage = cv2.filter2D(img, -1, rect)
bigrectimage = cv2.filter2D(img, -1, bigrect)
gaussianimage = cv2.filter2D(img, -1, gaussian)
sharpenimage = cv2.filter2D(img, -1, sharpen)
edgesimage = cv2.filter2D(img, -1, edges)
shiftimage = cv2.filter2D(img, -1, shift)
handshakeimage = cv2.filter2D(img, -1, handshake)


cv2.imshow('original', img)
cv2.imshow('rect', rectimage)
cv2.imshow('bigrect', bigrectimage)
cv2.imshow('gaussian', gaussianimage)
cv2.imshow('sharpen', sharpenimage)
cv2.imshow('edges', edgesimage)
cv2.imshow('shift', shiftimage)
cv2.imshow('handshake', handshakeimage)

cv2.imwrite('map/rect.jpg',rectimage)
cv2.imwrite('map/bigrect.jpg',bigrectimage)
cv2.imwrite('map/gaussian.jpg',gaussianimage)
cv2.imwrite('map/sharpen.jpg',sharpenimage)
cv2.imwrite('map/edges.jpg',edgesimage)
cv2.imwrite('map/shift.jpg',shiftimage)
cv2.imwrite('map/handshake.jpg',handshakeimage)

cv2.waitKey(0)

输出:

这里写图片描述这里写图片描述

这里写图片描述这里写图片描述

这里写图片描述这里写图片描述

这里写图片描述这里写图片描述

2015-07-09 21:39:56 u012786955 阅读数 4590

在执行线性空间滤波时,经常会遇到两个概念相关和卷积
二者基本相似,在进行图像匹配是一个非常重要的方法。
相关是滤波器模板移过图像并计算计算每个位置乘积之和的处理
卷积的机理相似,但滤波器首先要旋转180度
相关的计算步骤:
(1)移动相关核的中心元素,使它位于输入图像待处理像素的正上方
(2)将输入图像的像素值作为权重,乘以相关核
(3)将上面各步得到的结果相加做为输出
卷积的计算步骤:
(1)卷积核绕自己的核心元素顺时针旋转180度
(2)移动卷积核的中心元素,使它位于输入图像待处理像素的正上方
(3)在旋转后的卷积核中,将输入图像的像素值作为权重相乘
(4)第三步各结果的和做为该输入像素对应的输出像素
超出边界时要补充像素,一般是添加0或者添加原始边界像素的值
    可以看出他们的主要区别在于计算卷积的时候,卷积核要先做旋转。
而计算相关过程中不需要旋转相关核。

离散单位冲击:我们将包含单个1而其余全是0的函数成为离散单位冲击。
重要性质:一个函数与离散单位冲击相关,在冲击位置产生这个函数的一
个翻转版本。
f 函数
w 滤波器模板
eg:
f(x,y)
  0 0 0 0 0
  0 0 0 0 0 
  0 0 1 0 0
  0 0 0 0 0
  0 0 0 0 0
w(x,y)
  1 2 3 
  4 5 6 
  7 8 9
相关 f*w = 
     0     0     0     0     0
     0     9     8     7     0
     0     6     5     4     0
     0     3     2     1     0
     0     0     0     0     0
卷积f*w=
     0     0     0     0     0
     0     1     2     3     0
     0     4     5     6     0
     0     7     8     9     0
     0     0     0     0     0
相关的用途:图象的匹配
假如函数f中存在w的一个复制版本,即f:
  0 0 0 0 0
  0 1 2 3 0 
  0 4 5 6 0
  0 7 8 9 0
  0 0 0 0 0
f*w是多少呢?
    9    26    50    38    21
    42    94   154   106    54
    90   186   285   186    90
    54   106   154    94    42
    21    38    50    26     9
是不是会发现w与f中w的复制版本重合时,该点的值最大。最大值为
1^2+2^2+……+9^2 = 285
这就是用相关进行图像匹配的基本原理。当然了,在图像匹配时还要进行
相关函数的归一化等操作。
2018-06-07 15:39:46 u013539952 阅读数 38055

原理

卷积,有时也叫算子。用一个模板去和另一个图片对比,进行卷积运算。目的是使目标与目标之间的差距变得更大。卷积在数字图像处理中最常见的应用为锐化和边缘提取。
边缘提取,假如目标像素点和它周边的值(上下左右前后的临点,具体的比邻范围依赖于算子的大小,3*3的算子比邻范围为1,5*5的为2,以此类推)得有较大差异,那么就可以通过这个算子对原图矩阵中的这个位置进行卷积运算,得出的值和该像素点原来的灰度值会产生显著的差异。当这种前后差异超过我们预设的范围后,就将这个像素点标记为0(白色),其余点标记为255(黑色),这样就得到了一黑色为背景,白色线条作为边缘或形状的边缘提取效果图。
锐化算子:通过卷积运算,可以增大矩阵每一个元素与周边元素的方差,轻则起到锐化作用,重则成了边缘提取。反之,则是去噪过程。这种矩阵运算 可以在空间域上,图像经过量化其实就是以矩阵的形式存在的。

运算方法

假设卷积核h为
这里写图片描述
待处理矩阵x为
这里写图片描述
求x*h

  1. 将卷积核旋转180°,即
    这里写图片描述

  2. 将卷积核h的中心对准x的第一个元素,然后h和x重叠的元素相乘,h中不与x重叠的地方x用0代替,再将相乘后h对应的元素相加,得到结果矩阵中Y的第一个元素。如:
    这里写图片描述
    所以结果矩阵中的第一个元素Y11 = -1 * 0 + -2 * 0 + -1 * 0 + 0 * 0 + 0 * 1 + 0 * 2 + 1 * 0 + 2 * 5 + 1 * 6 = 16

  3. x中的每一个元素都用这样的方法来计算,得到的卷积结果矩阵为
    这里写图片描述

2017-12-12 01:19:00 weixin_34160277 阅读数 278

一:什么是卷积

离散卷积的数学公式可以表示为如下形式:

f(x) =  - 其中C(k)代表卷积操作数,g(i)代表样本数据, f(x)代表输出结果。

举例如下:

假设g(i)是一个一维的函数,而且代表的样本数为G = [1,2,3,4,5,6,7,8,9]

假设C(k)是一个一维的卷积操作数, 操作数为C=[-1,0,1]

则输出结果f(x)可以表示为 F=[1,2,2,2,2,2,2,2,1]  //边界数据未处理

 

以上只是一维的情况下,当对一幅二维数字图像加以卷积时,其数学意义可以解释如下:

源图像是作为输入源数据,处理以后要的图像是卷积输出结果,卷积操作数作为Filter

在XY两个方向上对源图像的每个像素点实施卷积操作。如图所示:

 

粉红色的方格每次在X/Y前进一个像素方格,就会产生一个新的输出像素,图中深蓝色的代

表要输出的像素方格,走完全部的像素方格,就得到了所有输出像素。

 

图中,粉红色的矩阵表示卷积操作数矩阵,黑色表示源图像– 每个方格代表一个像素点。

 

二:卷积在数字图像处理中应用

一副数字图像可以看作一个二维空间的离散函数可以表示为f(x, y), 假设有对于二维卷积操

作函数C(u, v) ,则会产生输出图像g(x, y) = f(x, y) *C(u,v), 利用卷积可以实现对图像模糊处理,边缘检测,产生轧花效果的图像。

 

一个简单的数字图像卷积处理流程可以如下:

1.      读取源图像像素

2.      应用卷积操作数矩阵产生目标图像

3.      对目标图像进行归一化处理

4.      处理边界像素

使用模板处理图像相关概念:     

      模板:矩阵方块,其数学含义是一种卷积运算。
      卷积运算:可看作是加权求和的过程,使用到的图像区域中的每个像素分别于卷积核(权矩阵)的每个元素对应相
                乘,所有乘积之和作为区域中心像素的新值。
      卷积核:卷积时使用到的权用一个矩阵表示,该矩阵与使用的图像区域大小相同,其行、列都是奇数,
              是一个权矩阵。
      卷积示例:
              3 * 3 的像素区域R与卷积核G的卷积运算:
              R5(中心像素)=R1G1 + R2G2 + R3G3 + R4G4 + R5G5 + R6G6 + R7G7 + R8G8 + R9G9
            

四使用模板处理图像的问题:
       边界问题:当处理图像边界像素时,卷积核与图像使用区域不能匹配,卷积核的中心与边界像素点对应,
                 卷积运算将出现问题。
       处理办法:
              A. 忽略边界像素,即处理后的图像将丢掉这些像素。
              B. 保留原边界像素,即copy边界像素到处理后的图像。

五.常用模板:


 

六其他

 

-----------------------------------------------------------------------------------------------------------

 以下用$符号表示从负无穷大到正无穷大的积分。   
    
  一维卷积:   
  y(t)=g(k)*x(k)=$g(k)x(t-k)   
  先把函数x(k)相对于原点反折,然后向右移动距离t,然后两个函数相乘再积分,就得到了在t处的输出。对每个t值重复上述过程,就得到了输出曲线。   
    
  二维卷积:   
  h(x,y)=f(u,v)*g(u,v)=$$f(u,v)g(x-u,y-v)   
  先将g(u,v)绕其原点旋转180度,然后平移其原点,u轴上像上平移x,   v轴上像上平移y。然后两个函数相乘积分,得到一个点处的输出。   

在图像中卷积是什么意思呢,就是图像就是图像f(x),模板是g(x),然后将模版g(x)在模版中移动,每到一个位置,就把f(x)与g(x)的定义域相交的元素进行乘积并且求和,得出新的图像一点,就是被卷积后的图像.模版又称为卷积核.卷积核做一个矩阵的形状。由于大多数模板都是对称的,所以模板不旋转。 

 

二维图像卷积运算

import numpy as np
from scipy import signal
from scipy import misc
import matplotlib.pyplot as plt
face=misc.face(gray=True) #创建一个灰度图像
scharr=np.array([[-3-3j,0-10j,+3-3j],
        [-10+0j,0+0j,+10+0j],
         [-3+3j,0+10j,+3+3j]]) #设置一个特殊的卷积和
grad=signal.convolve2d(face,scharr,boundary='symm',mode='same') #把图像的face数组和设计好的卷积和作二维卷积运算,设计边界处理方式为symm
fig,(ax1,ax2)=plt.subplots(1,2,figsize=(10,6)) #建立1行2列的图fig
ax1.imshow(face,cmap='gray') #显示原始的图
<matplotlib.image.AxesImage object at 0x00000000078FC198>
 ax1.set_axis_off() #不显示坐标轴
 ax2.imshow(np.absolute(grad),cmap='gray') #显示卷积后的图
<matplotlib.image.AxesImage object at 0x00000000078FCE48>
 ax2.set_axis_off() #不显示坐标轴
 fig.show() #显示绘制好的画布

 

转载于:https://www.cnblogs.com/yanghelin/p/8025820.html

2017-11-07 18:13:51 qq283980619 阅读数 6343

链接: 原文出处
作者: FreeBlues


概述

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

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

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

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

图像卷积计算示意图:

图像卷积计算示意图

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

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


以 Sobel 算子为例

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

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

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

sobel滤波器

注意:这里列出的这两个梯度矩阵对应于横向从左到右, 纵向从上到下的坐标轴, 也就是这种:

原点
O ——-> x轴
|
|
|
V y轴

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

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

计算展开

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

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

那么用 Gx 计算展开为:
横向新值 = (-1) x [左上] + (-2) x [左] + (-1) x [左下] + 1 x [右上] + 2 x [右] + 1 x [右下]

Gy 计算展开为:
纵向新值 = (-1) x [左上] + (-2) x [上] + (-1) x [右] + 1 x [左下] + 2 x [下] + 1 x [右下]

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

卷积核翻转

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

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

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

假设原始卷积核为:

a b c
d e f
g h i

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

先沿左下角到右上角的对角线翻转, 也就是 a和i, b和f, d和h交换位置, 结果为:

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);

}
]]
}

参考
图像卷积与滤波的一些知识点
Sobel Derivatives
Wiki:Kernel (image processing)