OpenCV Recipes:边缘检测与图像过滤

In this post, we are going to see how to apply cool visual effects to images.

二维卷积

卷积是图像处理的基本操作。我们将核矩阵中的每个值与图像中的相应值相乘,然后将其求和。得到输出矩阵此位置的值。

这里,核被称为图像滤镜,将核应用于给定图像的过程称为图像过滤。根据核值的不同,它执行不同的功能,如模糊、检测边缘等。下图可帮助你可视化图像的过滤操作:

Screenshot from 2018-08-04 11-51-20

让我们从最简单的情况开始,即 identity kernel。这个核并没有改变输入图像。考虑一个 3 x 3 的 identity kernel,它看起来如下所示:

模糊

模糊是指对邻域内的像素值求平均值。这也称为低通滤镜。低通滤镜是一种通低频、阻高频的滤镜。现在,下一个问题是:频率在图像中意味着什么?在这种情况下,频率是指像素值的变化率。所以我们可以说锐利的边缘是高频成分,因为像素值在那个区域变化很快。

构建低通滤镜的一个简单方法是对像素附近的值进行均匀地平均。让我们看看 3 x 3 低通滤镜核的样子:

我们将矩阵除以 9,因为我们希望这些值总和为 1。这称为归一化。这很重要,因为我们不想人为地增加该位置像素的强度。

下面是应用低通滤镜到图像的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import cv2
import numpy as np

img = cv2.imread('./test.jpg')
rows, cols = img.shape[:2]

kernel_identity = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]])

kernel_3x3 = np.ones((3, 3), np.float32) / 9
kernel_5x5 = np.ones((5, 5), np.float32) / 25

cv2.imshow('Original', img)

# value -1 is to maintain source image depth
output = cv2.filter2D(img, -1, kernel_identity)
cv2.imshow('Identity filter', output)

output = cv2.filter2D(img, -1, kernel_3x3)
cv2.imshow('3x3 filter', output)

output = cv2.filter2D(img, -1, kernel_5x5)
cv2.imshow('5x5 filter', output)

cv2.waitKey(0)

核大小与模糊度

在前面的代码中,我们生成了不同的核。我们使用函数 filter2d 将这些核应用于输入图像。如果你仔细观察这些图片,会发现随着核尺寸的增加,它们会变得越来越模糊。这是因为当增加核大小时,会在更大的范围内平均。这往往具有更大的模糊效果。

另一种方法是使用 OpenCV 的 blur 函数。如果不想自己生成核,可以直接使用这个函数。我们可以使用以下代码行来调用它:

1
output = cv2.blur(img, (3,3))

运动模糊

当我们应用运动模糊效果时,它看起来就像是你在特定方向移动时捕捉到了图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2
import numpy as np

img = cv2.imread('./test.jpg')
cv2.imshow('Original', img)

size = 15

# generating the kernel
kernel_motion_blur = np.zeros((size, size))
kernel_motion_blur[int((size - 1)/2), :] = np.ones(size)
kernel_motion_blur = kernel_motion_blur / size

output = cv2.filter2D(img, -1, kernel_motion_blur)

cv2.imshow('Motion Blur', output)
cv2.waitKey(0)

内部细节

运动模糊核对特定方向上的像素值进行平均,就像一个定向低通滤镜。一个 3 x 3 水平运动模糊核看起来是这样的:

这将在水平方向模糊图像。你也可以选择任何方向。

锐化

应用锐化滤镜将锐化图像中的边缘。当我们想要增强不够清晰的图像边缘时,这个滤镜非常有用。

正如在上图中看到的,锐化程度取决于我们所使用核的类型。我们可以自由地定制核,每个核都是一种不同的锐化。上图右上角的图像中,我们使用了这样一个核:

如果我们想进行过度锐化,如左下角的图像,我们使用以下核:

但是这两个核的问题在于其输出图像看起来是人为增强的。 如果我们希望图像看起来更自然,则需要使用边缘增强滤镜。其基本概念保持不变,但使用近似高斯核来构建此滤镜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import cv2
import numpy as np

img = cv2.imread('./test.jpg')
cv2.imshow('Original', img)

# generating the kernels
kernel_sharpen_1 = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
kernel_sharpen_2 = np.array([[1, 1, 1], [1, -7, 1], [1, 1, 1]])
kernel_sharpen_3 = np.array([
[-1, -1, -1, -1, -1],
[-1, 2, 2, 2, -1],
[-1, 2, 8, 2, -1],
[-1, 2, 2, 2, -1],
[-1, -1, -1, -1, -1]]) / 8

output = cv2.filter2D(img, -1, kernel_sharpen_1)
cv2.imshow('Sharpening', output)

output = cv2.filter2D(img, -1, kernel_sharpen_2)
cv2.imshow('Excessive Sharpening', output)

output = cv2.filter2D(img, -1, kernel_sharpen_3)
cv2.imshow('Edge Enhancement', output)

cv2.waitKey(0)

浮雕

浮雕滤镜使图像具有浮雕效果。如果某个特定区域有很大的对比度,我们根据浮雕的方向,用白色像素或黑色像素来代替它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import cv2
import numpy as np

img = cv2.imread('./test.jpg')

kernel_emboss_1 = np.array([[0, -1, -1],
[1, 0, -1],
[1, 1, 0]])

kernel_emboss_2 = np.array([[-1, -1, 0],
[-1, 0, 1],
[0, 1, 1]])

kernel_emboss_3 = np.array([[1, 0, 0],
[0, 0, 0],
[0, 0, -1]])

# converting the image to grayscale
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

cv2.imshow('Input', img)

output = cv2.filter2D(gray_img, -1, kernel_emboss_1) + 128
cv2.imshow('Embossing - South West', output)

output = cv2.filter2D(gray_img, -1, kernel_emboss_2) + 128
cv2.imshow('Embossing - South East', output)

output = cv2.filter2D(gray_img, -1, kernel_emboss_3) + 128
cv2.imshow('Embossing - North West', output)

cv2.waitKey(0)

边缘检测

边缘检测过程包括检测图像中的尖锐边缘,并产生二值图像作为输出。通常,在黑色背景上画白线来表示这些边缘。我们可以将边缘检测视为高通滤波操作。高通滤镜通高频并阻低频。正如我们前面讨论的,边缘是高频内容。在边缘检测中,我们希望保留这些边缘并丢弃所有其他东西。因此,我们应该构建一个相当于高通滤镜的核。

让我们从一个被称为 Sobel 滤波器的简单边缘检测滤波器开始。由于边缘可以出现在水平和垂直方向,Sobel 滤波器由以下两个核组成:

OpenCV 提供了直接将 Sobel 滤波器应用于给定图像的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2
import numpy as np

img = cv2.imread('./test.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape

# It is used depth of cv2.CV_64F
sobel_horizontal = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)

# Kernel size can be: 1, 3, 5 or 7
sobel_vertical = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)

cv2.imshow('Original', img)
cv2.imshow('Sobel horizontal', sobel_horizontal)
cv2.imshow('Sobel Vertical', sobel_vertical)

cv2.waitKey(0)

除此以外,还有 Laplacian 和 Canny 等边缘检测器。

侵蚀和膨胀

腐蚀和膨胀是形态学图像处理操作。形态学图像处理基本上涉及修改图像中的几何结构。这些操作主要是为二值图像定义的,但是我们也可以在灰度图像上使用它们。腐蚀基本上去除了结构中最外层的像素,而膨胀为结构增加了额外的像素层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2
import numpy as np

img = cv2.imread('./test.jpg', 0)

kernel = np.ones((5, 5), np.uint8)

img_erosion = cv2.erode(img, kernel, iterations=1)
img_dilation = cv2.dilate(img, kernel, iterations=1)

cv2.imshow('Input', img)
cv2.imshow('Erosion', img_erosion)
cv2.imshow('Dilation', img_dilation)

cv2.waitKey(0)

创建晕影滤镜

使用我们拥有的所有信息,让我们看看我们是否可以创建一个漂亮的晕影滤镜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cv2
import numpy as np

img = cv2.imread('./test01.jpg')
rows, cols = img.shape[:2]

# generating vignette mask using Gaussian kernels
kernel_x = cv2.getGaussianKernel(cols, 200)
kernel_y = cv2.getGaussianKernel(rows, 200)
kernel = kernel_y * kernel_x.T
mask = 255 * kernel / np.linalg.norm(kernel)
output = np.copy(img)

for i in range(3):
output[:, :, i] = output[:, :, i] * mask

cv2.imshow('Original', img)
cv2.imshow('vignette', output)
cv2.waitKey(0)

内部原理

晕影滤镜将亮度聚焦在图像的特定部分上,而其他部分看起来褪色。为了实现这一点,我们需要使用高斯核过滤图像中的每个通道。OpenCV 提供了一个执行此操作的函数,getGaussianKernelgetGaussianKernel函数的第二个参数非常有趣。它是高斯的标准差,它控制明亮中心区域的半径。

一旦我们构建了 2D 核,我们需要通过归一化这个核并将其扩展为一个 mask,如下所示:

1
mask = 255 * kernel / np.linalg.norm(kernel)

这是一个重要的步骤,因为如果不进行缩放,图像将显示为黑色。因为在输入图像上叠加蒙版后,所有像素值都将接近于零。在此之后,我我们遍历所有颜色通道并将 mask 应用于每个通道。

如何将焦点转移到周围?

我们现在知道如何创建一个聚焦于图像中心的晕影滤镜。 假设我们想要实现相同的晕影效果,但我们希望关注图像中的不同区域,我们应该如何做?

我们需要做的就是构建一个更大的高斯核,并确保峰值与感兴趣的区域一致。 以下是实现此目的的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2
import numpy as np

img = cv2.imread('./test.jpg')
rows, cols = img.shape[:2]

# generating vignette mask using Gaussian kernels
kernel_x = cv2.getGaussianKernel(int(1.5*cols), 200)
kernel_y = cv2.getGaussianKernel(int(1.5*rows), 200)
kernel = kernel_y * kernel_x.T
mask = 255 * kernel / np.linalg.norm(kernel)
mask = mask[int(0.5*rows):, int(0.5*cols):]
output = np.copy(img)

for i in range(3):
output[:, :, i] = output[:, :, i] * mask

cv2.imshow('Original', img)
cv2.imshow('vignette with shifted foucus', output)
cv2.waitKey(0)

增强图像的对比度

每当我们在光线不足的条件下拍摄图像时,图像都会变暗。发生这种情况的原因是因为当我们在这样的条件下捕获图像时,像素值倾向于集中在接近零的位置。

当发生这种情况时,人眼无法清楚地看到图像中的许多细节。 人眼喜欢对比度,因此我们需要调整对比度以使图像看起来美观和愉悦。 许多相机和照片应用已经隐含地这样做了。 我们使用一个称为直方图均衡的过程来实现这一目标。

1
2
3
4
5
6
7
8
9
10
11
12
import cv2
import numpy as np

img = cv2.imread('./test.jpg', 0)

# equalize the histogram of the input image
histeq = cv2.equalizeHist(img)

cv2.imshow('Input' ,img)
cv2.imshow('histogram equalized', histeq)

cv2.waitKey(0)

如何处理彩色图像

现在我们知道如何均衡灰度图像的直方图,您可能想知道如何处理彩色图像。 直方图均衡是一个非线性过程。 因此,我们不能分离出 RGB 图像中的三个通道,分别均衡直方图,然后将它们组合以形成输出图像。 直方图均衡的概念仅适用于图像中的强度值。 因此,我们必须确保在执行此操作时不要修改颜色信息。

为了处理彩色图像的直方图均衡,我们需要将其转换到一个强度与颜色信息分开的颜色空间。 YUV 空间是一个很好的例子,因为 YUV 模型根据亮度(Y)和色度(UV)分量定义的色彩空间。 一旦我们将其转换为 YUV,我们只需要均衡 Y 通道并将其与其他两个通道组合就可以获得输出图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cv2
import numpy as np

img = cv2.imread('./test01.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

# equalize the histogram of the input image
# histeq = cv2.equalizeHist(img)
# equalize the histogram of the Y channel
img_yuv[:, :, 0] = cv2.equalizeHist(img_yuv[:, :, 0])

# convert the YUV image back to RGB format
img_out = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)

cv2.imshow('Color input image' ,img)
cv2.imshow('histogram equalized', img_out)

cv2.waitKey(0)
GreatX wechat
Subscribe to my blog by scanning my public wechat account