OpenCv笔记

OpenCV In Python

为什么学习 OpenCV?

电子设计中使用OpenCV进行图像处理的仿真

使用工具:

python

安装OpenCV:

1
pip install opencv-python

例程展示

官网链接

Getting Started with Images

图片入门——读取和显示图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#作为第一步,导入OpenCV python库。正确的方法是给它附加赋一个名称cv,下面将使用它来引用这个库。
#(不知道为什么要加别称,但是实践是不加别称也可以使用)
import cv2 as cv
import sys
#调用cv::imread()来读入图片(在官网的示例中使用了绝对路径,在我实践中我的电脑上需要使用绝对路径,具体原因不清楚。)
img = cv.imread(cv.samples.findFile(r'C:\Users\qjy\Desktop\2.jpg'))
#如果图像打开失败,退出并显示“不能读取图片。”
if img is None:
sys.exit("不能读取图片。")
#使用cv::imshow()显示图片,cv::waitkey()用于维持图片显示:cv::waitKey函数唯一的参数是等待用户输入的时间(以毫秒为单位)。零意味着永远等待。返回值是被按下的键。
cv.imshow("显示窗口", img)
k = cv.waitKey(0)
#如果按下的键是“s”键,图像将被写入一个文件。为此,cv::imwrite()函数被调用,该函数具有文件路径和cv::Mat对象作为参数。
if k == ord("s"):
cv.imwrite("另存为图片名", img)
k = cv.waitKey(0)
  1. 读取图像函数:cv::imread

    第一个参数指定的文件路径来加载图像。第二个参数是可选的,它指定图像的格式:

    • IMREAD_COLOR 以BGR 8位格式加载图像。这是这里使用的默认

    • IMREAD_UNCHANGED 按原样加载图像(包括alpha通道)。

    • IMREAD_GRAYSCALE 以灰度值加载图像。

    1
    2
    #具体写法
    img = cv.imread(cv.samples.findFile(r'C:\Users\qjy\Desktop\2.jpg'),cv.IMREAD_GRAYSCALE)

    备注:

    • 对于彩色图像,解码后的图像将以B G R顺序存储通道。

    • 当使用IMREAD_GRAYSCALE时,如果可用,将使用编解码器的内部灰度转换。结果可能与cvtColor()的输出不同

Capture Video from Camera

视频入门——调用电脑摄像头并获取视频

有时我们需要用摄像头捕获实时流,OpenCV提供了一个非常简单的接口来做到这一点。本次任务使用电脑内置的摄像头捕获视频并且显示。

要捕获视频,您需要创建一个VideoCapture对象。它的参数可以是设备索引或视频文件的名称。设备索引只是指定哪个摄像机的数字。通常会连接一个摄像头。所以我只是传递0(或 -1)。你可以通过传递1来选择第二个摄像机,以此类推。之后,您可以逐帧捕捉。但在最后,不要忘记释放Capture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
import cv2 as cv
cap = cv.VideoCapture(0)
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
# Capture frame-by-frame
ret, frame = cap.read()
# if frame is read correctly ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# Our operations on the frame come here

# Display the resulting frame
cv.imshow('frame', frame)
if cv.waitKey(1) == ord('q'):
break
# When everything done, release the capture
cap.release()
cv.destroyAllWindows()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
import cv2 as cv
cap = cv.VideoCapture(r'C:\Users\qjy\Desktop\a.mp4')
while cap.isOpened():
ret, frame = cap.read()
# if frame is read correctly ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break
cap.release()
cv.destroyAllWindows()

Basic Operations on Images

OpenCV-Python教程

OpenCV中的图像处理

色彩空间转换

目标

  • 在本教程中,您将学习如何将图像从一个颜色空间转换到另一个颜色空间,如 \(BGR↔Gray\)\(BGR↔HSV\) 等。

  • 除此之外,我们将创建一个应用程序来提取视频中的彩色对象

  • 你将学习以下函数: cv.cvtColor()cv.inRange() 等。

改变颜色空间

\(OpenCV\) 中有超过150种颜色空间转换方法。但我们只看两种使用最广泛的一种: \(BGR↔Gray\)\(BGR↔HSV\)

对于颜色转换,我们使用函数 cv.cvtColor(input_image, flag),其中 \(flag\) 决定转换的类型。

对于 \(BGR→Gray\) 转换,我们使用标志cv.COLOR_BGR2GRAY。类似地,对于 \(BGR→HSV\),我们使用标志cv.COLOR_BGR2HSV。要获取其他标志,只需在 \(Python\) 终端中运行以下命令:

1
2
3
>>> import cv2 as cv
>>> flags = [i for i in dir(cv) if i.startswith('COLOR_')]
>>> print( flags )

Note

对于HSV,色相范围为[0,179],饱和度范围为[0,255],取值范围为[0,255]。不同的软件使用不同的尺度。所以如果你在比较 \(OpenCV\) 值和它们,你需要标准化这些范围。

跟踪对象

现在我们知道了如何将 \(BGR\) 图像转换为 \(HSV\),我们可以使用它来提取有颜色的对象。在 $ HSV$ 中,比在 \(BGR\) 颜色空间中更容易表示颜色。在我们的应用程序中,我们将尝试提取一个蓝色的对象。方法如下:

  • 取视频的每一帧。

  • \(BGR\) 转换到 \(HSV\) 颜色空间。

  • 我们阈值的 \(HSV\) 图像范围的蓝色。

  • 现在单独提取蓝色物体,我们可以对图像做任何我们想做的事情。

下面是详细注释的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cv2 as cv
import numpy as np
cap = cv.VideoCapture(0)
while(1):
# Take each frame
_, frame = cap.read()
# Convert BGR to HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
# define range of blue color in HSV
lower_blue = np.array([110,50,50])
upper_blue = np.array([130,255,255])
# Threshold the HSV image to get only blue colors
mask = cv.inRange(hsv, lower_blue, upper_blue)
# Bitwise-AND mask and original image
res = cv.bitwise_and(frame,frame, mask= mask)
cv.imshow('frame',frame)
cv.imshow('mask',mask)
cv.imshow('res',res)
k = cv.waitKey(5) & 0xFF
if k == 27:
break
cv.destroyAllWindows()

下图显示了对蓝色物体的跟踪:

Note

图像中有一些噪声。我们将在后面的章节中看到如何删除它。

这是目标跟踪中最简单的方法。一旦你学会了轮廓函数,你就可以做很多事情,比如找到物体的质心并使用它来跟踪物体,通过在摄像机前移动你的手来绘制图表,以及其他有趣的事情。

如何找到HSV值跟踪?

这是在 stackoverflow.com 中发现的一个常见问题。它非常简单,你可以使用相同的函数cv.cvtColor()。你只需传递你想要的 \(BGR\) 值,而不是传递一个图像。例如,要找到绿色的 \(HSV\) 值,请在 $ Python$ 终端中尝试以下命令:

1
2
3
4
>>> green = np.uint8([[[0,255,0 ]]])
>>> hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
>>> print( hsv_green )
[[[ 60 255 255]]]

现在分别取 \([H-10, 100,100]\)\([H+10, 255,255]\) 为下界和上界。除了这种方法之外,您还可以使用任何图像编辑工具(如 \(GIMP\))或任何在线转换器来查找这些值,但不要忘记调整 \(HSV\) 范围。

附加资源

练习:

试着找出一种方法来提取不止一种颜色的对象,例如,同时提取红色、蓝色和绿色的对象。

图像的几何变换

图像阈值化

目标

简单的阈值

在这里,事情很简单。对于每个像素,应用相同的阈值。如果像素值小于阈值,则设置为 \(0\),否则设置为最大值。函数的简历。\(Threshold\) 用于应用阈值。第一个参数是源图像,它应该是一个灰度图像。第二个参数是用于对像素值进行分类的阈值。第三个参数是分配给超过阈值的像素值的最大值。\(OpenCV\) 提供了由第四个参数给出的不同类型的阈值

请参阅这些类型的文档以了解其区别。

该方法返回两个输出。第一个是所使用的阈值,第二个输出是阈值图像。

这段代码比较了不同的简单阈值类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('gradient.png',0)
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray',vmin=0,vmax=255)
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()

Note

为了绘制多幅图像,我们使用了 plt.subplot() 函数。详情请查看 \(matplotlib\) 文档。

代码产生如下结果:

自适应阈值

在前一节中,我们使用一个全局值作为阈值。但这并不是在所有情况下都是好的,例如,如果一个图像在不同的区域有不同的光照条件。在这种情况下,自适应阈值可以有所帮助。在这里,算法根据像素周围的小区域确定阈值。因此,我们对同一幅图像的不同区域采用不同的阈值,对不同光照条件下的图像有较好的处理效果。

除上述参数外,方法 cv.adaptiveThreshold 接受三个输入参数:

adaptivmethod决定如何计算阈值:

cv.ADAPTIVE_THRESH_MEAN_C:阈值是邻近区域的平均值减去常数C。

cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域值减去常数C的高斯加权和。

blockSize 决定了邻域区域的大小,\(C\) 是一个常数,从邻域像素的平均值或加权和中减去。

下面的代码比较了全局阈值和自适应阈值对不同光照的图像的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread(r'C:\Users\qjy\Desktop\11.jpg',0)
img = cv.medianBlur(img,5)
ret,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
th2 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,11,2)
th3 = cv.adaptiveThreshold(img,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,11,2)
titles = ['Original Image', 'Global Thresholding (v = 127)','Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]
for i in range(4):
plt.subplot(2,2,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()

结果:

1
2
3
4
5
6
7
8
import cv2 as cv
import numpy as np
img = cv.imread(r'C:\Users\qjy\Desktop\65ce8a75cfc0dc33feddd6d4632ab1a.jpg',0)
kernel = np.ones((2,1),np.uint8)
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
#erosion = cv.erode(img,kernel,iterations = 1)
cv.imshow('res',closing)
k = cv.waitKey(0)

Otsu 二值法

在全局阈值中,我们使用任意选择的值作为阈值。相比之下,\(Otsu\) 的方法避免了必须选择一个值,并自动确定它。

考虑一个只有两个不同图像值的图像(双峰图像),其中直方图只包含两个峰。一个合适的阈值应该在这两个值之间。类似地,\(Otsu\) 的方法从图像直方图中确定一个最优的全局阈值。

为此,我们使用了 cv.threshold() 函数,其中 cv.threshold()cv.THRESH_OTSU 被作为一个额外的标志传递。阈值可以任意选择。

看看下面的例子。输入图像是一个有噪声的图像。在第一种情况下,应用值为 \(127\) 的全局阈值。在第二种情况下,直接应用 \(Otsu\) 的阈值。在第三种情况下,首先用 \(5x5\) 高斯核滤波去除噪声,然后应用 \(Otsu\) 阈值。看看噪声滤波是如何改善结果的。

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 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('noisy2.png',0)
# global thresholding
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# Otsu's thresholding
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# Otsu's thresholding after Gaussian filtering
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# plot all the images and their histograms
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
for i in range(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()

结果:

Otsu 二值化是如何工作的?

本节演示了 \(Otsu\) 二值化的 \(Python\) 实现,以展示它实际上是如何工作的。如果你不感兴趣,你可以跳过这个。

由于我们使用的是双峰图像,\(Otsu\) 的算法试图找到一个阈值\((t)\),使由关系给出的weighted within-class variance最小化: \[ \sigma_w^2(t) = q_1(t)\sigma_1^2(t)+q_2(t)\sigma_2^2(t) \] 其中: \[ \begin{gather*} \mu_1(t) = \sum_{i=1}^{t} \frac{iP(i)}{q_1(t)} \quad \& \quad \mu_2(t) = \sum_{i=t+1}^{I} \frac{iP(i)}{q_2(t)}\\ \mu_1(t) = \sum_{i=1}^{t} \frac{iP(i)}{q_1(t)} \quad \& \quad \mu_2(t) = \sum_{i=t+1}^{I} \frac{iP(i)}{q_2(t)}\\ \sigma_1^2(t) = \sum_{i=1}^{t} [i-\mu_1(t)]^2 \frac{P(i)}{q_1(t)} \quad \& \quad \sigma_2^2(t) = \sum_{i=t+1}^{I} [i-\mu_2(t)]^2 \frac{P(i)}{q_2(t)} \end{gather*} \] 它实际上找到了一个 \(t\) 的值,它位于两个峰值之间,使得这两个类的方差都是最小的。它可以简单地在 \(Python\) 中实现如下:

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
img = cv.imread('noisy2.png',0)
blur = cv.GaussianBlur(img,(5,5),0)
# find normalized_histogram, and its cumulative distribution function
hist = cv.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel()/hist.sum()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in range(1,256):
p1,p2 = np.hsplit(hist_norm,[i]) # probabilities
q1,q2 = Q[i],Q[255]-Q[i] # cum sum of classes
if q1 < 1.e-6 or q2 < 1.e-6:
continue
b1,b2 = np.hsplit(bins,[i]) # weights
# finding means and variances
m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2
# calculates the minimization function
fn = v1*q1 + v2*q2
if fn < fn_min:
fn_min = fn
thresh = i
# find otsu's threshold value with OpenCV function
ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
print( "{} {}".format(thresh,ret) )

附加资源

  1. Digital Image Processing, Rafael C. Gonzalez

Exercises

  1. There are some optimizations available for Otsu's binarization. You can search and implement it.

平滑图像

目标

学习:

  • 用各种低通滤波器模糊图像

  • 为图像应用定制滤镜(2D卷积)

二维卷积(图像滤波)

和一维信号一样,图像也可以用各种低通滤波器(\(LPF\))、高通滤波器(\(HPF\))等进行滤波。\(LPF\) 有助于去除噪声、模糊图像等。高频滤波器有助于在图像中找到边缘。

$OpenCV $ 提供了一个函数 cv.filter2D() 来将内核与图像进行卷积。例如,我们将尝试在图像上使用平均滤镜。一个 \(5x5\) 平均的过滤器内核看起来如下所示: \[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \] 操作是这样的:将该内核保持在一个像素之上,将该内核之下的所有25个像素相加,取平均值,并用新的平均值替换中心像素。对图像中的所有像素继续执行此操作。尝试以下代码并检查结果:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('opencv_logo.png')
kernel = np.ones((5,5),np.float32)/25
dst = cv.filter2D(img,-1,kernel)
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()

结果:

图像模糊(图像平滑)

图像模糊是通过卷积图像与低通滤波核。它对消除噪声很有用。它实际上从图像中去除高频内容(如噪声和边缘)。所以在这个操作中,边缘会模糊一些(也有一些模糊技术不会模糊边缘)\(OpenCV\) 提供了四种主要的模糊技术。

  1. 平均值

    这是通过卷积图像与一个标准化的盒子过滤器。它只是取内核区域下所有像素的平均值,然后替换中心元素。这是由函数 cv.blur()cv.boxFilter() 完成的。查看文档了解更多关于内核的细节。我们应该指定内核的宽度和高度。一个 \(3x3\) 标准化的框状滤波器看起来如下所示: \[ K = \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} \]

    Note

    如果您不想使用规范化的框过滤器,请使用 cv.boxFilter()。传递一个参数 normalize=False 给函数。

    查看下面一个 \(5x5\) 内核大小的示例演示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import cv2 as cv
    import numpy as np
    from matplotlib import pyplot as plt
    img = cv.imread('opencv-logo-white.png')
    blur = cv.blur(img,(5,5))
    plt.subplot(121),plt.imshow(img),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()

    结果:

  2. 高斯模糊

    在此方法中,使用高斯核代替盒形滤波器。这是通过函数 cv.GaussianBlur() 完成的。我们应该指定内核的宽度和高度,它们应该是正的和奇数的。我们还应该指定X和Y方向的标准差,分别是 $ sigmaX$ 和 \(sigmaY\)。如果只指定了 \(sigmaX\),则 \(sigmaY\) 与 $ sigmaX$ 取相同的值。如果两者都是 \(0\),则从内核大小计算。高斯模糊是去除图像高斯噪声的一种有效方法。

    如果你愿意,你可以用函数 cv.getGaussianKernel() 创建一个高斯核。

    以上代码可以修改为高斯模糊:

    1
    blur = cv.GaussianBlur(img,(5,5),0)

    结果:

  3. 模糊中值

    在这里,函数 cv.medianBlur() 取内核区域下所有像素的中值,并将中心元素替换为这个中值。这对于图像中的椒盐噪声是非常有效的。有趣的是,在上面的过滤器中,中心元素是一个新计算的值,它可能是图像中的像素值,也可能是一个新值。但在中值模糊中,中心元素往往被图像中的某个像素值所替代。有效地降低了噪声。它的内核大小应该是一个正奇数。2

    在这个演示中,我给原始图像添加了 \(50%\) 的噪声,并应用了中间值模糊。检查结果:

    1
    median = cv.medianBlur(img,5)

    结果:

  4. 双边滤波

    cv.bilateralFilter() 在去除噪声的同时保持边缘锋利是非常有效的。但与其他滤波器相比,其运算速度较慢。我们已经知道高斯滤波器取像素周围的邻域并求其高斯加权平均值。该高斯滤波器是一个单独的空间函数,即在滤波时考虑附近的像素。它不考虑像素是否有几乎相同的强度。它不考虑一个像素是否是边缘像素。它也会模糊边缘,这是我们不想做的。

    双边滤波也在空间中采用高斯滤波器,但多了一个高斯滤波器,它是像素差的函数。空间高斯函数保证了只考虑附近像素进行模糊处理,而强度差高斯函数保证了只考虑那些与中心像素强度相似的像素进行模糊处理。所以它保留了边缘,因为边缘上的像素会有很大的强度变化。

    下面的示例展示了双边过滤器的使用(关于参数的详细信息,请访问docs)。

    1
    blur = cv.bilateralFilter(img,9,75,75)

    结果:

    看,表面的纹理消失了,但边缘还保留着。

附加资源

  1. Details about the bilateral filtering

Exercises

形态学变换

1
2
3
4
5
6
7
8
9
10
import cv2 as cv
import numpy as np
img = cv.imread(r'C:\Users\qjy\Desktop\65ce8a75cfc0dc33feddd6d4632ab1a.jpg',0)
kernel = np.ones((2,2),np.uint8)
#closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
#erosion = cv.erode(img,kernel,iterations = 1)
#dilation = cv.dilate(img,kernel,iterations = 1)
opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
cv.imshow('res',opening)
k = cv.waitKey(0)

目标

在这一章

理论

形态变换是一种基于图像形状的简单操作。它通常在二值图像上执行。它需要两个输入,一个是原始图像,另一个是决定运算性质的结构元素或核。两个基本的形态运算符是腐蚀和膨胀。然后它的变体形式,如开操作,闭操作和梯度等也会发挥作用。我们将在下图的帮助下一个一个地看到它们:

  1. 腐蚀

    腐蚀的基本概念就像土壤腐蚀一样,它腐蚀了前景对象的边界(尽量保持前景为白色)。那么它有什么作用呢?核在图像中滑动(就像在二维卷积中一样)。只有当核下的所有像素都是 \(1\) 时,原始图像中的一个像素(\(1\) 或 $ 0$)才会被认为是 \(1\),否则它会被腐蚀(变成 \(0\))。

    所以进行腐蚀操作时发生的是,边界附近的所有像素都会被丢弃,这取决于内核的大小。因此前景物体的厚度或尺寸减小,或者图像中的白色区域减小。它可以用来去除小的白色噪音(正如我们在色彩空间章节中看到的),分离两个连接的物体等等。

    在这里,作为一个例子,我将使用一个充满1的5x5内核。让我们看看它是如何工作的:

    1
    2
    3
    4
    5
    import cv2 as cv
    import numpy as np
    img = cv.imread('j.png',0)
    kernel = np.ones((5,5),np.uint8)
    erosion = cv.erode(img,kernel,iterations = 1)

    结果:

  2. 膨胀

    膨胀与腐蚀正好相反,在膨胀操作中,如果核下至少有一个像素为“1”,则像素元素为“1”。因此,它增加了图像中的白色区域,或增加了前景对象的大小。通常情况下,在去除噪音的情况下,腐蚀之后是膨胀。因为侵蚀去除了白噪音,但也缩小了我们的目标。所以我们把它放大。因为噪音消失了,它们不会回来,但是我们的物体面积增加了。它在连接物体的破碎部分时也很有用。

    1
    dilation = cv.dilate(img,kernel,iterations = 1)

    结果:

  3. 开操作

    开操作只是腐蚀和膨胀的另一个名称。它在去除噪音方面很有用,正如我们上面解释的那样。这里我们使用函数cv.morphologyEx()

    1
    opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)

    结果:

    image
  4. 闭操作

    闭操作是开操作的反向,即先膨胀后腐蚀。它在删除前景物体内部的小洞或物体上的小黑点时很有用。

    1
    closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)

    结果:

  5. 形态学梯度

    它是图像膨胀和侵蚀的区别,梯度的结果将看起来像对象的轮廓。

    1
    gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)

    结果:

  6. Top Hat

    它是输入图像和开操作处理后的输出图像之间的区别。下面的例子是针对9x9内核的。

    1
    tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)

  7. Black Hat

    它是输入图像的闭操作与输入图像之间的区别。

    1
    blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)

结构元素

在前面的例子中,我们在 \(Numpy\) 的帮助下手工创建了一个结构化元素。它是长方形的。但在某些情况下,您可能需要椭圆形/圆形的核。为此,\(OpenCV\) 有一个函数 cv.getStructuringElement()。你只需要传递内核的形状和大小,就可以得到想要的内核。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Rectangular Kernel
>>> cv.getStructuringElement(cv.MORPH_RECT,(5,5))
array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8)
# Elliptical Kernel
>>> cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
array([[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
# Cross-shaped Kernel
>>> cv.getStructuringElement(cv.MORPH_CROSS,(5,5))
array([[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)

额外的资源

  1. Morphological Operations at HIPR2

练习

图像的梯度

Canny 边缘检测

图像金字塔

OpenCV 的轮廓

OpenCV 的直方图

OpenCV 中的图像变换

模板匹配

Hough 直线检测

基于分水岭算法的图像分割

基于 GrabCut 算法的交互式前景提取