计算视觉

您所在的位置:网站首页 图像旋转公式 计算视觉

计算视觉

2023-03-11 10:42| 来源: 网络整理| 查看: 265

写在前面

       在我很多期博客中,如基于内容的图像缩放、图像手绘/素描风格转换、图像的局部特征信息及全景图像拼接,都有涉及到图像梯度的概念,因为梯度确实是一种具有高信息熵且易于提取的信息,在图像处理、计算机视觉、机器视觉领域研究中应用广泛,我在很多完全不同的图像研究方向的文献中见到过图像梯度的身影。可以说,绝大多数图像相关的研究都可以利用图像的梯度去实现特定效果的优化,只是一些情况下该优化的作用很小。但是,这并不影响图像梯度的价值。而图像边缘、图像几何特征更是当前计算机视觉、模式识别研究的重点关注对象,图像边缘信息对行人检测、人脸识别等人工智能主流研究方向有着不可替代的作用。

       在本期博客中,我们将探讨如下问题:

图像梯度与图像边缘 图像几何检测原理及其实现

       Let's begin!!

1.图像梯度与图像边缘

       我们来看一下维基百科对于Image Gradient(图像梯度)的解释。

       "An image gradient is a directional change in the intensity or color in an image. The gradient of the image is one of the fundamental building blocks in image processing. For example, the Canny edge detector uses image gradient for edge detection. In graphics software for digital image editing, the term gradient or color gradient is also used for a gradual blend of color which can be considered as an even gradation from low to high values, as used from white to black in the images to the right. Mathematically, the gradient of a two-variable function (here the image intensity function) at each image point is a 2D vector with the components given by the derivatives in the horizontal and vertical directions. At each image point, the gradient vector points in the direction of largest possible intensity increase, and the length of the gradient vector corresponds to the rate of change in that direction."

       "图像梯度是指图像强度或颜色的方向变化。图像梯度是图像处理的基础框架之一。例如,Canny边缘检测器使用图像梯度进行边缘检测。在用于数字图像编辑的图形软件中,梯度或颜色梯度一词也用于表示颜色的逐渐混合,可以认为是从低到高值的均匀渐变,如下图中从白色到黑色所使用的渐变。数学上,一个双变量函数(这里是图像强度函数)在每个图像点处的梯度是一个二维向量,其分量由水平和垂直方向上的导数给出。在每个图像点上,梯度向量指向可能最大强度增加的方向,梯度向量的长度对应于该方向的变化率。"

       上述内容非常严谨地定义了图像和视觉中的图像梯度,但是仍然需要指出,蓝字部分的内容存在一些难以理解的部分,如颜色的逐渐混合,其实这里指的是Photoshop(或其他的图形编辑软件)中渐变的效果,当然,这些渐变也属于梯度。

       维基百科用了较通俗的语言去描述图像梯度,相比之下,百度百科对图像梯度的解释就显得更专业,更容易理解,如下:        "图像梯度可以把图像看作二维离散函数,图像梯度其实就是这个二维离散函数的求导: 图像梯度:

G(x,y)=dx(i,j)+dy(i,j) dx(i,j)=I(i+1,j)-I(i,j) dy(i,j)=I(i,j+1)-I(i,j)

其中,I(i,j)为灰度图像I在第i行第j个像素上的像素值。        上面说的是简单的梯度定义,其实还有更多更复杂的梯度公式。"

       我更喜欢百度百科更接近本质的解释,简单来说,图像梯度就是图像在不同方向上的变化趋势,可以将图像视为只存在水平和垂直两方向存在梯度变化,进行临近像素的梯度提取。当然,这么做会存在一定的问题,那就是这样求解得到的梯度信息对噪声敏感,即对噪声鲁棒性差。

       接下来,将以实际的图像为例,更进一步的解释 1.1 图像梯度、图像边缘及梯度提取算子,并介绍目前最广泛应用的 1.2 canny边缘检测算法原理及实现。

1.1图像梯度、图像边缘及梯度提取算子

       说起图像梯度,其实我们得好好谈一谈图像边缘,从许多曾经的视觉研究实验中,我们可以发现,边缘是很特殊的一种信息,因为人眼识别物体,靠的正是边缘,而非颜色、纹理。在绘画中我们可以通过线条去绘制任何一样物体,但若该手绘物体缺失了部分边缘,我们就难以识别。但相反的,我们对缺失了纹理、色彩的物体,却依然具备强大的辨别能力。

       Walther等人在2011年进行了这样一个视觉研究,让视觉受试者观看海滩、城市街道、森林、高速公路、山脉和办公室的照片和线条图时收集功能磁共振成像数据。尽管在场景统计数据上有明显的差异,但研究者们还是能够通过PPA(the parahippocampal place area,海马旁区和RSC(the ret-rosplenial cortex,脾叶皮层)解码初级视觉皮层中线条图的fMRI(Functional magnetic resonance imaging,功能磁共振成像)数据和彩色照片的活动数据的场景类别。更值得注意的是,在PPA和RSC中,从线条图识别的错误模式与从彩色照片识别的错误模式非常相似。这些数据表明,在这些区域,用于区分场景类别的信息对于线条图和照片来说是相似的。

       而我们在谈论图像梯度的时候,很多情况下关注的是图像边缘。因此,图像的边缘检测的目的,即为确定图像突变(不连续)的地方

显然,图像内高信息熵信息隐藏在边缘信息当中 边缘传递的信息多余像素点本身

边缘的由来主要源于:

表面的不连续

深度(距离)的不连续

表面颜色的不连续

光照的不连续

       已经介绍完梯度与边缘,我们现在就先来看看梯度的计算方式。在先前度娘百科(百度百科)对于图像梯度已经有了一个数理上的定义,当然,对于图像梯度的表示和计算方式有很多,对于一幅给定的数字图像,我们将其视作二维离散函数f(x,y),那么常见的梯度表征和计算方式有:

梯度向量 \triangledown f(x,y)=\begin{bmatrix} \frac{\partial f(x,y)}{\partial x} \\ \frac{\partial f(x,y)}{\partial y} \end{bmatrix}=\begin{bmatrix} f_x \\ f_y \end{bmatrix} 梯度幅值 \begin{vmatrix} \triangledown f(x,y) \end{vmatrix}=\sqrt{f^2_x+f^2_y} 梯度方向 \theta=tan^{-1}\frac{f_y}{f_x}

       而在实际编程运算时,我们通常采用算子通过卷积获得一幅图像的梯度信息,如果不明白何为图像中的卷积运算,可以看看下图。

       实际上,上图中的原图像大小为5\times 5(左侧),而卷积核为3\times 3(中间),因此得到的卷积结果是3\times 3的(右侧)。原图像大小为m\times m(m为奇数)的图像,经过k\times k(k为奇数)的卷积核卷积后,得到的卷积结果图像大小为(m-k+1)\times (m-k+1)

       以最简单的Prewitt算子为例,利用算子卷积的图像梯度结果如下:

       不难发现,在梯度图像中,值越高的部分,越接近人眼视觉所观察到的边缘。因此,我们可以用梯度更直接的解释边缘:边缘是图像能量急剧变化的地方,是图像梯度高的地方。

       但这也存在一个问题,图像的梯度是对邻域像素值变化敏感的,而图像内的噪声则会导致部分非边缘区域同样具有较高的梯度,这会导致边缘检测的噪声鲁棒性差。

       为了提高噪声鲁棒性,Canny在1986年提出了一种边缘检测算法,该算法是边缘检测中最经典,最具影响力的方法,至今还有非常广泛的应用,是计算机视觉领域的基础框架组成之一,论文被引超20000余次。

1.2 Canny边缘检测算法原理

       Canny边缘检测算法可以分为四步:

噪声抑制 计算图像梯度及方向 非极大值抑制 双阈值检测 边缘连接

       对于噪声抑制和梯度及梯度方向的计算,Canny边缘检测算法并没有进行创新,只是做了单纯的高斯核卷积平滑,以此进行噪声抑制,并通过差分运算得到梯度及梯度方向。Canny边缘检测突破性的创新是集中在后三步上的。

非极大值抑制

       非极大值抑制的思想在于:当图像的梯度到达局部(特定大小的邻域)最大值的时候,我们认为它是边缘;对于非极大值的点,即使它超过了我们所定义的“边缘梯度阈值”,仍不认为它是边缘。并且,预定义了八个可能的方向(左上、上、右上、右、右下、下、左下、左),对于该方向上的梯度值,将非极大值归零。

       具体操作:1.筛选幅值大于阈值的像素点,设为待验证点;2.遍历待验证点,若为局部最大值,保留,反之去归零;3.保留下来的点就是非极大值抑制后的边缘点。

双阈值检测

       说到双阈值检测,我们得先说采用单一阈值的缺陷,如下图所示。

       显而易见,采用单一阈值会有两种问题随之产生:1.阈值过高,会导致部分梯度较低的边缘被当成噪声剔除,导致边缘不连续;2.阈值过低,会有过多不应该作为边缘的梯度信息被误判为边缘而包含进边缘图。

       而双阈值解决了单阈值的上述问题。双阈值定义了两种阈值,低阈值与高阈值:若梯度小于低阈值,则不是边缘;若梯度大于高阈值,则为强边缘;若梯度在高低阈值间,则为弱边缘。

       至此,就是双阈值检测的部分,乍一看仿佛与单一阈值没有区别,只要高于低阈值就能作为弱边缘,那这么一来好像跟单阈值就没什么区别了。诚然,如果缺少了“边缘连接”这下一步骤,双阈值和单阈值的本质确实是一样的。

边缘连接

       在开始解释“边缘连接”这一步骤前,我们不妨来仔细观察一下“单阈值”与“双阈值”的图像,这对于“边缘连接”原理的理解非常有帮助。

       在单阈值图像中,存在部分“离群”的线条,其中有一部分其实并非真实的边缘,而是被误判的高梯度位置;其中也有一部分不连通的真实边缘,这又导致了边缘的不连续。

       而在双阈值图像中,较亮的线条为强边缘,稍暗一些的线条为弱边缘。不难发现,一条不与强边缘相连的弱边缘,基本都是被误判为边缘的区域。相反,与强边缘相连的弱边缘,则囊括了绝大多数单阈值所没有判定包含的“真实边缘”。

       那么现在,我们可以明白边缘连接的原理了:利用高阈值来确定主要边缘轮廓(强边缘),并利用与强边缘连接的低阈值所得弱边缘来填补缺失边缘,并防止引入噪声(不与强边缘相连的弱边缘就归零)。

       由此,完整地解释了Canny边缘检测算法的原理,我们进入代码部分。

1.3 代码实战部分 ### edge.py import numpy as np import math def conv(image, kernel): """卷积的一个实现. 对于任意一个像素点,该本版采用了点积运算以及 np.sum()来进行快速的加权求和 Args: 图像: 尺寸为(Hi, Wi)的numpy数组. 卷积核(kernel): 尺寸为(Hk, Wk)的numpy数组. Returns: out: 尺寸为(Hi, Wi)的numpy数组. """ Hi, Wi = image.shape Hk, Wk = kernel.shape out = np.zeros((Hi, Wi)) # 对于本次作业, 我们将使用边界值来对图像进行填充. # 这是因为,如果使用0进行填充,会使得在图像边界上的导数非常大, # 但通常而言,我们希望忽略图像边界上的边缘(图像边界永远不会被认为是边缘). pad_width0 = Hk // 2 pad_width1 = Wk // 2 pad_width = ((pad_width0,pad_width0),(pad_width1,pad_width1)) padded = np.pad(image, pad_width, mode='edge') ### YOUR CODE HERE for i in range(Hi): for j in range(Wi): out[i][j] = (padded[i:i + Hk,j:j + Wk] * kernel).sum() ### END YOUR CODE return out def gaussian_kernel(size, sigma): """ 生成高斯卷积核. 该函数按照高斯核的公式,生成了一个卷积核矩阵。 提示: - 利用 np.pi 以及 np.exp 当计算 pi 以及 exp()函数. 参数: size: 一个整数,表示输出的kernel的尺寸. sigma: 一个float,对应于高斯公式中的sigma,用来控制权重的分配. 返回值: kernel: 尺寸为(size, size)的numpy数组. """ kernel = np.zeros((size, size)) ### YOUR CODE HERE k=(size-1)/2 for i in range(size): for j in range(size): kernel[i][j]=1/(2*np.pi*sigma**2)*np.exp(((i-k)**2+(j-k)**2)/(-2*sigma**2)) #kenel[i][j]=1/(2*np.pi*power(sigma,2))*np.exp(((power(i-k,2)+(power(j-k,2))/(-2*power(sigma,2))) ### END YOUR CODE return kernel def partial_x(img): """ 计算输入图像水平方向的偏导. 提示: - 你可以利用你在前面完成的conv函数. 参数: img: 尺寸为(Hi, Wi)的numpy数组. 输出: out: 水平方向的梯度图像 """ Hi, Wi = img.shape out = None padded = np.pad(img, 1, mode='edge') padded1 = np.zeros(padded.shape) ### YOUR CODE HERE for i in range(1,Hi+1): for j in range(1,Wi+1): padded1[i][j]=(padded[i][j+1]-padded[i][j-1])/2 ### END YOUR CODE out=padded1[1:Hi+1,1:Wi+1] return out def partial_y(img): """ 计算输入图像竖直方向的偏导. 提示: - 你可以利用你在前面完成的conv函数/或者用前面刚开发的partial_x函数. 参数: img: 尺寸为(Hi, Wi)的numpy数组. 输出: out: 竖直方向的梯度图像 """ Hi, Wi = img.shape out = None padded = np.pad(img, 1, mode='edge') padded1 = np.zeros(padded.shape) ### YOUR CODE HERE for i in range(1,Hi+1): for j in range(1,Wi+1): padded1[i][j]=(padded[i+1][j]-padded[i-1][j])/2 ### END YOUR CODE out=padded1[1:Hi+1,1:Wi+1] return out def gradient(img): """ 计算输入图像的梯度大小和方向. 参数: img: 灰度图. 尺寸为 (H, W) 的Numpy数组. 返回值: G: 输入图像的梯度值图像。它的每个像素点值都是该像素点的梯度值. 尺寸为 (H, W) 的Numpy数组. theta: 输入图像的梯度方向图像(角度, 0 = 0 and i < H 2. j >= 0 and j < W 3. (i, j) != (y, x) 参数: y, x: 像素点的位置 H, W: 图像的尺寸 返回值: neighbors: 该像素点的邻居的索引值[(i, j)]所组成的list . """ neighbors = [] for i in (y-1, y, y+1): for j in (x-1, x, x+1): if i >= 0 and i < H and j >= 0 and j < W: if (i == y and j == x): continue neighbors.append((i, j)) return neighbors def link_edges(strong_edges, weak_edges): """ 找出与真实的边缘相连接的弱边缘,并将它们连接到一起. 对于每个强边缘点,它们都是真实的边缘。 我们需要遍历每个真实的边缘点,然后在弱边缘中找出与之相邻的像素点,并把他们连接起来。 在这里,我们认为如果像素点(a, b)与像素点(c, d)相连,只要(a, b)位于(c, d)的八邻域内。 Args: strong_edges: 尺寸为 (H, W)的二值图像. weak_edges: 尺寸为 (H, W)的二值图像. Returns: edges: 尺寸为 (H, W)的二值图像. 提示: 弱边缘一旦与强边缘相连,那它就是真实的边缘,与强边缘的地位一样。 这句话的意思是,一旦一个弱边缘像素点被检测成为了一个真实的边缘点,所有与它相连的其它弱边缘也应该被归为真实的边缘。 所以在编程的时候,你只遍历一遍强边缘是不够的,因为此时可能有新的弱边缘点被标记为真实的边缘点。 """ H, W = strong_edges.shape indices = np.stack(np.nonzero(strong_edges)).T edges = np.zeros((H, W), dtype=np.bool) # 生成新的拷贝 weak_edges = np.copy(weak_edges) edges = np.copy(strong_edges) g=0; padded_weak = np.pad(weak_edges, 1, mode='edge') edges1=np.pad(strong_edges, 1, mode='edge') ### YOUR CODE HERE while(g!=-1): g=0; k=0; for i in range(H): for j in range(W): #if strong_edges[i][j]!=0: if edges1[i+1][j+1]!=0: if padded_weak[i][j]!=0 and edges1[i][j]!=padded_weak[i][j]: edges1[i][j]=padded_weak[i][j] k=k+1 if padded_weak[i][j+1]!=0 and edges1[i][j+1]!=padded_weak[i][j+1]: edges1[i][j+1]=padded_weak[i][j+1] k=k+1 if padded_weak[i][j+2]!=0 and edges1[i][j+2]!=padded_weak[i][j+2]: edges1[i][j+2]=padded_weak[i][j+2] k=k+1 if padded_weak[i+1][j]!=0 and edges1[i+1][j]!=padded_weak[i+1][j]: edges1[i+1][j]=padded_weak[i+1][j] k=k+1 if padded_weak[i+1][j+2]!=0 and edges1[i+1][j+2]!=padded_weak[i+1][j+2]: edges1[i+1][j+2]=padded_weak[i+1][j+2] k=k+1 if padded_weak[i+2][j]!=0 and edges1[i+2][j]!=padded_weak[i+2][j]: edges1[i+2][j]=padded_weak[i+2][j] k=k+1 if padded_weak[i+2][j+1]!=0 and edges1[i+2][j+1]!=padded_weak[i+2][j+1]: edges1[i+2][j+1]=padded_weak[i+2][j+1] k=k+1 if padded_weak[i+2][j+2]!=0 and edges1[i+2][j+2]!=padded_weak[i+2][j+2]: edges1[i+2][j+2]=padded_weak[i+2][j+2] k=k+1 if k==0: g=-1; else: strong_egdes=np.copy(edges1[1:H+1,1:W+1]) edges=edges1[1:H+1,1:W+1] ### END YOUR CODE return edges def canny(img, kernel_size=5, sigma=1.4, high=20, low=15): """ 将上面所完成的函数组合到一起,完成canny边缘检测器. Args: img: 输入图像 kernel_size: int, 表示kernel的大小 sigma: float, 用来计算kernel. high: 为强边缘而设的高阈值. low: 为弱边缘而设的低阈值. Returns: edge: 输出的边缘图像 """ ### YOUR CODE HERE kernel = gaussian_kernel(kernel_size, sigma) smoothed = conv(img, kernel) G,theta=gradient(smoothed) nms = non_maximum_suppression(G, theta) strong_edges, weak_edges = double_thresholding(nms, high, low) edge = link_edges(strong_edges, weak_edges) ### END YOUR CODE return edge 复制代码1.4 实施例

       初始化

# 初始化配置 import numpy as np import matplotlib.pyplot as plt from time import time from skimage import io %matplotlib inline plt.rcParams['figure.figsize'] = (15.0, 12.0) # 设置默认尺寸 plt.rcParams['image.interpolation'] = 'nearest' plt.rcParams['image.cmap'] = 'gray' # 自动重装外部模块 %load_ext autoreload %autoreload 2 复制代码

       首先利用高斯滤波器来对图片进行平滑处理。高斯核大小为 (2k+1)\times (2k+1) 的高斯滤波器,该滤波器可以由以下方程给出

h_{ij}=\frac{1}{2\pi \sigma^2}\exp\bigg(-\frac{(i-k)^2+(ji-k)^2}{2\sigma^2}\bigg),0\le i,j\le 2k+1 from edge import conv, gaussian_kernel # 定义一个 3x3 的高斯 kernel,并将其sigma值设为 1 kernel = gaussian_kernel(3, 1) kernel_test = np.array( [[ 0.05854983, 0.09653235, 0.05854983], [ 0.09653235, 0.15915494, 0.09653235], [ 0.05854983, 0.09653235, 0.05854983]] ) print(kernel) # 检测生成的高斯kernel是否正确 if not np.allclose(kernel, kernel_test): print('Incorrect values! Please check your implementation.') 复制代码

       输出结果:

[[0.05854983 0.09653235 0.05854983] [0.09653235 0.15915494 0.09653235] [0.05854983 0.09653235 0.05854983]] 复制代码

       找一幅图像进行测试。

# 用不同的尺寸以及sigma值来进行测试 kernel_size = 5 sigma = 1.4 # 载入图片 img = io.imread('iguana.png', as_grey=True) # 生成高斯kernel kernel = gaussian_kernel(kernel_size, sigma) # 利用kernel来对图片进行平滑 smoothed = conv(img, kernel) plt.subplot(1,2,1) plt.imshow(img) plt.title('Original image') plt.axis('off') plt.subplot(1,2,2) plt.imshow(smoothed) plt.title('Smoothed image') plt.axis('off') plt.show() 复制代码

       这里有个小问答:改变kernel_size 以及 sigma分别具有什么效果?

改变核的大小:

       会导致中心像素点受临域像素值的影响更大,从到导致高斯滤波的模糊效果更佳明显。

改变sigma的大小:

       若减小,会使高斯滤波核中心权重占比变大,中心位置临域的权重占比都减小,若sigma趋于0,则高斯滤波基本没有效果;若增大,会使高斯滤波核的中心位置权重占比减小,中心位置领域的权重占比增大,若sigma趋于无穷,则高斯滤波等于均值滤波。

       我们回到实施例部分,找出求取图像差分所对应的kernelD_xD_y

from edge import partial_x, partial_y # 测试案例 I = np.array( [[0, 0, 0], [0, 1, 0], [0, 0, 0]] ) # 希望的输出结果 I_x_test = np.array( [[ 0, 0, 0], [ 0.5, 0, -0.5], [ 0, 0, 0]] ) I_y_test = np.array( [[ 0, 0.5, 0], [ 0, 0, 0], [ 0, -0.5, 0]] ) # 计算梯度 I_x = partial_x(I) I_y = partial_y(I) print(I_x) print(I_y) # 确定partial_x and partial_y是否编写正确 if not np.all(I_x == I_x_test): print('partial_x incorrect') if not np.all(I_y == I_y_test): print('partial_y incorrect') 复制代码

       输出结果:

[[ 0. 0. 0. ] [ 0.5 0. -0.5] [ 0. 0. 0. ]] [[ 0. 0.5 0. ] [ 0. 0. 0. ] [ 0. -0.5 0. ]] 复制代码

       计算x、y方向上的梯度

# 计算平滑后的图像的差分 Gx = partial_x(smoothed) Gy = partial_y(smoothed) plt.subplot(1,2,1) plt.imshow(Gx) plt.title('Derivative in x direction') plt.axis('off') plt.subplot(1,2,2) plt.imshow(Gy) plt.title('Derivative in y direction') plt.axis('off') plt.show() 复制代码

       结果如下

       现在,让我们用两个方向的差分来计算图片的梯度大小以及方向,完成edge.py所定义的梯度和梯度方向的求解。

from edge import gradient G, theta = gradient(smoothed) if not np.all(G >= 0): print('Magnitude of gradients should be non-negative.') if not np.all((theta >= 0) * (theta < 360)): print('Direction of gradients should be in range 0


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3