到目前为止,在这一系列的文章中,我们已经涵盖了图像处理的基本概念。现在我们要深入探讨一些更高级的变换。没有它们,计算机视觉,一般来说,将是不可能的。大多数转换都严重依赖于数学,更确切地说,是线性代数。我们将尽最大努力,尝试在不使用太多数学的情况下解释它们。
在这篇文章中,我们包括
- 阈值处理
- 边缘检测
- 线条检测
- 轮廓检测
1.阈值处理
阈值处理是一种图像分割方法,用于从灰度或彩色图像中创建二进制图像。二元图像是一种只有2个值的图像,通常是黑色和白色,意味着像素的值是0或255。
这个过程主要用于将图像中的物体从其背景中分离出来。简单地说,我们确定一个阈值,然后创建一个逻辑矩阵,即所谓的掩码,用于图像的索引。有2种类型的阈值处理:全局和适应性。
1.1 全局阈值处理
在进行全局阈值处理时,我们将图像中的所有像素值与一个单一的阈值进行比较。如果像素值大于阈值,它就被设置为255,否则,我们就给它一个0的值。全局阈值处理只有在我们能在图像中完全不同的背景和物体,即它们有两个不同的像素值组的情况下才起作用。
全局阈值是根据直方图来确定的。图像的直方图告诉我们图像中有多少像素具有某个像素值,由于我们说全局阈值的理想图片是那些具有2个分离的像素值组的图片,所以理想的直方图看起来就像这样。
我们称它为双峰型。
现在让我们看看全局阈值处理的一个真实例子。
我们有这张肺部的X射线图像,我们的想法是将肺部与背景分开,这样我们就可以清楚地看到形状并检查一些异常情况。让我们深入了解一下代码。
import numpy as np
import cv2
from google.colab.patches import cv2_imshow
import matplotlib.pyplot as plt
img = cv2.imread('lungsxray.jpg')
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
histogram,bin_edges = np.histogram(img_gray,bins=256,range=(0,256))
fig = plt.plot(histogram)
plt.show()
threshold_value = 130
ret,imgt = cv2.threshold(img_gray,threshold_value,255,cv2.THRESH_BINARY)
cv2_imshow(imgt)
首先,我们要把图像转换成灰度,然后我们要用numpy函数numpy.histogram找到图像的直方图。它把图像作为第一个参数,把若干个bin以及图像中的像素值范围作为第二和第三个参数。当我们绘制直方图时,它看起来像这样。
这不是一个教科书式的完美的双峰直方图,但它已经足够好了。我们可以注意到,两个像素的值组在120-130左右分开。在这个例子中,我们将其设置为130。现在我们要使用OpenCV的函数,叫做cv2.threshold。它接收五个参数:我们要阈值的图像,阈值,阈值后的最大像素值,以及阈值的类型。
cv2.THRESH_BINARY代表基本的全局阈值处理。该函数返回两个参数,第一个是用于阈值处理的值,第二个是阈值处理后的图像。让我们看看它是什么样子的。
正如我们所看到的,我们已经把肺部与身体的其他部分完全分开。请记住,这只有在图像具有某种程度的双峰直方图时才能做到。
1.2 自适应(局部)阈值处理
与使用一个阈值的全局阈值不同,自适应阈值利用了一个独特的阈值,该阈值是基于从整个图像中获得的分区子图像。基本上,我们把图像分成许多小块(矩阵),并使用某种统计方法,来确定该子图像的阈值。现在让我们来看看这个图像。
img = cv2.imread('bookthreshold.jpg')
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
这个图像不会有双峰直方图,因为有许多不同的像素值组,简单地说就是有许多灰色的阴影。
hist,bin_edges = np.histogram(img,bins=256,range=(0,256))
plt.plot(hist)
plt.show()
直方图显然不是双峰的,所以让我们进行全局阈值处理,看看会发生什么。
ret,threshold = cv2.threshold(img,130,255,cv2.THRESH_BINARY)
cv2_imshow(threshold)
正如我们所见,全局阈值处理在这里显然不起作用。现在让我们来执行自适应阈值处理。我们可以使用OpenCV的函数cv2.adaptiveThreshold来做。它接收6个参数:图像、最大像素值、寻找阈值的统计方法、阈值的类型、子矩阵(掩码)的大小,以及一个帮助算法在整个过程中更新阈值的常数。
threshold2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,13,5)
cv2_imshow(threshold2)
ADAPTIVE_THRESH_MEAN_C表示阈值将被确定为子图像中所有像素的平均值。你可以在OpenCV的文档中找到其余的统计方法。通过THRESHOLD_BINARY,我们明确了我们想要一个基本的二进制图像作为结果,这意味着高于其阈值的像素将有255的值,而其余的将被设置为0。
在不深入研究数学的情况下,数字13决定了子图像的大小将是13×13,你需要记住的是,这个数字必须是一个奇数,而我们设置了一个常数为5。我们任意选择了这2个数字。结果如下。
这看起来比全局阈值的那个好得多。通过对参数的微调和对统计方法的实验,我们可以得到一个更好的图像,但这已经足够好了,可以显示出区别。
2.边缘检测
许多问题最常见的前提条件之一可能是寻找边缘。几乎所有应该在图像中找到某种对象的算法都是基于边缘检测的。 但是,边缘检测是如何进行的呢?有大量的算法用于这一目的,而且几乎所有的算法都有复杂的数学背景。在这篇文章中,我们将向你展示如何实现可能是寻找图像中所有边缘的最佳方法。
2.1 Canny 边缘检测
Canny Edge Detection一直是识别图像中的边缘的最常用的方法之一,但也是最复杂的方法之一。在这篇文章中,我们不打算介绍Canny Edge Detection背后的理论背景,我们只打算关注它的实现。因此,让我们深入了解它。
import cv2
image = cv2.imread(r'/content/drive/MyDrive/shapessm.jpg')
cv2.imshow(image)
image_edges = cv2.Canny(image,100,200)
cv2.imshow(image_edges)
正如你所看到的,OpenCV中Canny的实现非常简单。canny函数需要三个参数:我们要检测边缘的图像和两个我们要分离的阈值。你可以看到,用OpenCV进行边缘检测并不难,但如果我们想从图像中只得到那张CD,或者是那支铅笔呢?边缘检测只是物体检测的第一步。现在让我们转到第二步。
3.线条检测
在图像处理中,寻找图像中直线的方向和位置是非常重要的任务。解决这个问题时最常用的技术是Hough变换。Hough变换将图像从空间域转到另一个域,在这个域中,感兴趣的信息是以不同方式表示的。在这种情况下,我们从空间域到Hough域。
Hough变换的骨干是一些复杂的数学,需要注意的是,Hough变换不仅限于线条检测,还包括任何具有数学参数化的形状。
3.1 霍夫线变换
Hough线变换可以通过实现cv2.HougLinesP函数来完成。该函数需要5个参数。原始图像的边缘,累积器的距离分辨率(像素),累积器的距离分辨率(弧度),以及函数内部的阈值票数。
可选的参数是最小和最大的线长,以及线之间的间隙。因此,首先我们需要在图像上进行边缘检测。
img = cv2.imread('Highway.JPG')
#Converting the image into gray-scale
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2_imshow(img)
#Finding edges of the image
edge_image = cv2.Canny(img,250,200)
cv2_imshow(edge_image)
现在我们要在这个图像上进行Hough线检测。
lines = cv2.HoughLinesP(edge_image, 1, np.pi/180, 60, minLineLength=10, maxLineGap=250)
#Going through every line we found and drawing it in an image based on starting and ending point
for i in range(2):
print(lines[i])
#[[ 2 1 589 1]]
#[[336 4 542 297]]
这就是前两条线的样子。每条线由一条线的起点和终点的坐标组成。第二和第三个参数几乎都是1和Pi/180,它们与Hough变换的数学运算有关。
阈值、最小线长和最大线距都是通过实验选择的。现在我们已经得到了每条线的坐标,我们想继续在我们的原始图像上绘制这些线。
lines = cv2.HoughLinesP(edge_image, 1, np.pi/180, 60, minLineLength=10, maxLineGap=250)
#Going through every line we found and drawing it in an image based on starting and ending point
for i in range(2):
print(lines[i])
#[[ 2 1 589 1]]
#[[336 4 542 297]]
我们已经成功识别了图像中的大部分线条。现在让我们看看如何用Hough变换来检测圆圈。
3.2 Hough圆形变换
除了线条之外,Hough变换还能够进行圆的检测。实际上,如果你知道它们的数学方程,它能够找到任何形状。我们鼓励你了解更多关于这个图像处理的强大工具。但如果你只是需要快速的圆检测,让我们看看在OpenCV中是如何做到的。
import cv2
import numpy as np
from google.colab.patches import cv2_imshow
image = cv2.imread(r'/content/drive/MyDrive/shapessm.jpg')
cv2_imshow(image)
image_edges = cv2.Canny(image, 0,255)
cv2_imshow(image_edges)
circles_img = cv2.HoughCircles(image_edges,cv2.HOUGH_GRADIENT,1,20,
param1=50,param2=30,minRadius=40)
circles_img = np.uint16(np.around(circles_img))
for i in circles_img[0,:]:
image = cv2.circle(image,(i[0],i[1]),i[2],(0,255,0),2)
cv2_imshow(image)
正如我们所看到的,在图像中,有不止一个类似圆的形状,但我们只检测到了我们之前谈到的那个CD的外线。这与HoughCircle函数的参数和它的参数有关,所以让我们尽可能简单地解释一下它们。
首先,像往常一样,我们要在一张图片上找到圆。第二个参数告诉我们要使用哪种检测算法,你应该总是使用cv2.HOUGH_GRADIENT。第三个参数与变换本身有很大关系,但为了简单起见,它定义了如何检测一个完整的圆线。它被称为dp,它越大,圆线可以不那么完整,但仍然被检测到。然后我们有一个检测到的两个圆圈之间的最小距离。
参数一和二告诉gradient如何检测边缘,以及什么是圆状物体的阈值,阈值越小,返回的非圆状物体越多。最后,你可以设置你期望检测的圆的最小和最大半径,以像素为单位。
4.等高线检测
等高线被定义为连接沿图像边界的所有具有相同强度的点的线。使用等高线检测,我们可以检测图像中物体的边界。OpenCV提供了cv2.findContours函数,使我们能够很容易地识别所有的轮廓,这在许多不同的任务中是非常有用的。它在二进制图像上的效果最好,该函数需要4个参数。图像、轮廓线检索模式和近似方法。现在让我们来看看我们如何进行轮廓检测。
#Loading the image
img = cv2.imread('/content/drive/MyDrive/shapessm.jpg')
#Converting the image into gray-scale
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2_imshow(img)
#Output
现在我们要用Canny边缘检测法来寻找这个图像的边缘。
#Finding edges of the image
edge_image = cv2.Canny(img,250,200)
#showing Edged image
cv2_imshow(edge_image)
在我们自己得到这张图片后,我们将使用OpenCV的findContours函数。此外,我们还将展示如何使用cv2.drawContours函数在原始图像上绘制检测到的轮廓线。
# Finding all the lines in an image based on given parameters
contours, hierarchy = cv2.findContours(edge_image,
cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
#Reverting the original image back to BGR so we can draw in colors
img = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)
#parameter -1 specifies that we want to draw all the contours
cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
cv2_imshow(img)
现在让我们来解释一下我们所使用的参数。对于轮廓检索类型,我们有4个选项。这里我们使用了cv2.RETR_LIST,这意味着该函数将检索所有可能的轮廓,而不计算层次。所有其他的参数都会返回一定层次的轮廓,你可以在OpenCV官方文档中查看。最后一个参数代表近似的方法。我们在这里有两个选项。
第一个是cv.CHAIN_APPROX_NONE,这意味着该函数将返回轮廓中的所有点,并且不会进行任何形式的逼近。第二个参数是cv.CHAIN_APPROX_SIMPLE。使用这个参数,函数将对轮廓的关键点进行近似,并只提供给我们这些点。在下一篇文章中,我们将看到轮廓检测的重要性,以及我们如何操作这些参数。
总结
在这篇文章中,我们介绍了图像分割的一些核心原则。计算机视觉在很大程度上依赖于分割,它经常被用作某些种类的CNN模型的预处理方法。另外,一些基本的计数应用也可以用我们所涉及的一些算法来完成。在下一篇文章中,我们将向你展示一些现实生活中的项目,以及你如何使用到目前为止所获得的图像处理知识。