利用OpenCV进行图像的轮廓检测

简 介: 本文对于OpenCV中的轮廓检测算法进行了讨论,可以看到一些基于轮廓检测的应用。接着对四种不同的提取方式的结果进行了讨论。你还了解了如何将轮廓进行绘制的方法。

关键词 轮廓检测二值化

 

§00   言

本文根据 Contour Detection using OpenCV (Python/C++) 中的内容整理而得。

  使用轮廓检测可以获得物体的边界,方便在图像中对他们进行定位。通常在一些有趣的应用中轮廓检测是第一处理环节。,比如图像前景提取,简单的图像分割,检测以及识别等。

  下面让我们认识一下轮廓以及在OpenCV中实现轮廓检测算法,自己考虑一下如何基于此形成更加有趣的应用。

0.1 机器视觉中的轮廓

  基于轮廓信息完成移动物体检测和分割可以构建很多有趣的应用,下面给出一些例子:

  • 运动检测: 在视频监视中,移动物体检测技术有着广泛的应用:室内外安全环境监测、交通控制、在体育活动中的行为检测、监测非法入侵物体,甚至在视频压缩中都有着应用。下图中,可以看到视频流中的移动人去在监控中有着重要的应用。在左侧站立静止不动的人群则没有被标注出来。只有那些运动的才被抓拍。基于本文方法细节进行研究。

▲ 图1.1  运动检测的应用示例。对于移动中的人体进行检测。对于站立静止不动的人群没有检测

▲ 图1.1 运动检测的应用示例。对于移动中的人体进行检测。对于站立静止不动的人群没有检测

  • 可疑物体检测: 在公共场合的非法侵入物品被当做可疑物品。 一个有效安全的方法:通过轮廓建模和背景减除进行入侵物体检测。

▲ 图1.2  入侵物体检测

▲ 图1.2 入侵物体检测

  从上述引用的文章来看,背景图片以及带有非法入侵物体图片可以找出并标注出可疑物体。

  • 背景前景分割: 将图像中的背景进行替换,你需要执行图像的前景提取(类似于图像分割)。使用轮廓信息可以帮助我们完成前景的分割。本文中将会给出更爱的细节。下面的图展示了种种应用的简单示例。

▲ 图1.3   图镶前景提取的示例,添加新的背景

▲ 图1.3 图镶前景提取的示例,添加新的背景

0.2 什么是轮廓

  当我们把物体边缘所有的点连接在一起可以获得轮廓。通常,对于特定的轮廓是指那些具有相同颜色和亮度的边界点像素。 OpenCV使得寻找和绘制图像中物体轮廓变得很容易了。它提供了一下两个简单函数:
  1. findContours();
  2. drawContours();

  还有,它还提供了不同的轮廓检测算法:
  1. CHAIN_APPOROX_SIMPLE
  2. CAHIN_APPROX_NONE

  在下面的例子中我们将详细讨论。下面图片显示了这些算法是如何能够检测到简单物体的轮廓的。

▲ 图2.1  轮廓检测结果

▲ 图2.1 轮廓检测结果

  上面解释了什么是物体的轮廓,下面我们讨论轮廓检测中的步骤。

 

§01 廓检测和绘制


  OpenCV使得轮廓检测和绘制变得十分容易。只需一下步骤:

1.1 读取图片转换成灰度格式

  将图像读取并转换成灰度格式。将图像转换成灰度图是进行下一步操作前的重要步骤。将图像转换成单通道的灰度值可以用于后面的阈值处理,这是后面进行轮廓检测的必须步骤。

1.2 进行二值化阈值处理

  为了检测轮廓,需要先对灰度图进行二值化结果,或者Canny边缘检测结果上进行。这里我们采用二值化结果。

  将图像转换成黑白图像,吧感兴趣物体加亮凸显出来便于轮廓检测算法。阈值处理将图片中的物体区域转换成白色,所有的物体像素都具有相同的亮度值。从这些白色像素中算法获得物体的轮廓。

  注意:黑色像素,取值为0。代表着背景,在轮廓检测中被忽略。

  此时,会产生一个问题,是否我们可以使用彩色图像中的一个彩色通道,比如 R,G,B通道,取代灰度图呢?这种情况下, 轮廓检测可能工作不好。前面我们讨论过,检测边界的算法,也就是检测相同亮度像素以发现轮廓。在灰度图上建立的黑白图比起单个颜色通道(R,G,B)效果会更好。本文后半部分,我们会给出仅仅基于R,G,B单个彩色通道下测轮廓检测结果。

1.3 检测轮廓

  使用 findContours()函数来检测图像中的所有的轮廓。

1.4 在原图中显示轮廓

  当轮廓被确定之后,可以使用 drawContours()函数来在原始图像上重合上轮廓标注曲线。

  通过代码可以使得上面各个步骤的含义更加确切和明了。

 

§02 OpenCV轮廓检测


2.1 图像读入和转换

  先以OpenCV的导入开始,然后读取图像。

  • Python
import cv2

# read the image
image = cv2.imread('input/image_1.jpg')

  上面代码是假设图像文件在当前工程目录中。下面是将图像转换成灰度图像(单通道格式)

  • C++
#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
   // read the image
   Mat image = imread("input/image_1.jpg");

  接下来使用 cvtColor() 函数将原来的RGB图像转换成灰度图像。

  • Python
# convert the image to grayscale format
img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  • C++
// convert the image to grayscale format
Mat img_gray;
cvtColor(image, img_gray, COLOR_BGR2GRAY);

2.2 阈值处理

  使用threshold()函数将图像进行二值化。对于所有像素值大于150 将被转换成255(白色),其余的像素则转换成(0)黑色。参数150是一个可以调整的参数,你可以通过实验来确定它。

  经过阈值处理之后可以通过 imshow观察到二值化图像。函数在下面代码中演示:

  • Python
# apply binary thresholding
ret, thresh = cv2.threshold(img_gray, 150, 255, cv2.THRESH_BINARY)
# visualize the binary image
cv2.imshow('Binary image', thresh)
cv2.waitKey(0)
cv2.imwrite('image_thres1.jpg', thresh)
cv2.destroyAllWindows()
  • C++
// apply binary thresholding
Mat thresh;
threshold(img_gray, thresh, 150, 255, THRESH_BINARY);
imshow("Binary mage", thresh);
waitKey(0);
imwrite("image_thres1.jpg", thresh);
destroyAllWindows();

  查看下面的图像。这是对原来的RGB彩色图像的二值化。你可以缺口处看到笔、手写笔以及手机的边缘都变成了白色。 轮廓算法将这些白色当成物体,来寻找这些物体的轮廓。

  注意,所有的背景都是黑色,包括手机的背面。这些区域将会被算法忽略。 每个物体边缘的白色像素都被当做具有相同亮度的像素,算法通过相似度量来把它们结合成物体的轮廓。
▲ 图2.2.1  经过阈值处理之后的二值化图像

▲ 图2.2.1 经过阈值处理之后的二值化图像

2.3 利用CHAIN_APPROX_NONE绘制轮廓

  下面通过 CHAIN_APPROX_NONE方法来发现和绘制轮廓。

  首先使用 findContours() 函数。它需要三个输入参数,下面给出具体定义。对于可选参数可以参考这个 链接中的文档

  • image: The binary input image obtained in the previous step.
  • ode: This is the contour-retrieval mode. We provided this as RETR_TREE, which means the algorithm will retrieve all possible contours from the binary image. More contour retrieval modes are available, we will be discussing them too. You can learn more details on these options here.
  • thod: This defines the contour-approximation method. In this example, we will use CHAIN_APPROX_NONE.Though slightly slower than CHAIN_APPROX_SIMPLE, we will use this method here tol store ALL contour points.

  值得说明的是, mode 参数是指提取轮廓的类型, method参数则是指轮廓中的哪些点将被存储。后面我们将会详细解释。

  对于相同的图像施加不同的方法的结果差异可以通过观察很容易进行理解。

  下面代码中,我们先对原始图像进行拷贝,然后展示处理方法。之所以拷贝是不想算法对原始图像进行改动。

  截止,使用 drawContours()函数了在RGB图像上绘制轮廓。这个函数具有四个必须的参数以及一些可选的参数。前面四个参数在下面列出。可选参数可以参考 这个链接中的文档

  • image: This is the input RGB image on which you want to draw the contour.

  • contours: Indicates the contours obtained from the findContours() function.

  • contourIdx: The pixel coordinates of the contour points are listed in the obtained contours. Using this argument, you can specify the index position from this list, indicating exactly which contour point you want to draw. Providing a negative value will draw all the contour points.

  • color: This indicates the color of the contour points you want to draw. We are drawing the points in green.

  • thickness: This is the thickness of contour points.

  • Python

# detect the contours on the binary image using cv2.CHAIN_APPROX_NONE
contours, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
                                     
# draw contours on the original image
image_copy = image.copy()
cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
               
# see the results
cv2.imshow('None approximation', image_copy)
cv2.waitKey(0)
cv2.imwrite('contours_none_image1.jpg', image_copy)
cv2.destroyAllWindows()
  • C++
// detect the contours on the binary image using cv2.CHAIN_APPROX_NONE
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE);
// draw contours on the original image
Mat image_copy = image.clone();
drawContours(image_copy, contours, -1, Scalar(0, 255, 0), 2);
imshow("None approximation", image_copy);
waitKey(0);
imwrite("contours_none_image1.jpg", image_copy);
destroyAllWindows();

  上面代码执行完之后将会产生和显示下面的图像,我们将结果存储在磁盘中。

▲ 图2.3.1  使用 CHINA_APPROX_NONE检测轮廓结果

▲ 图2.3.1 使用 CHINA_APPROX_NONE检测轮廓结果

  下图给出了原始图像(左)以及附盖有检测轮廓的图像(右)。

▲ 图2.3.2 原始图像以及有轮廓标注的图像

▲ 图2.3.2 原始图像以及有轮廓标注的图像

  你所看的结果显示了算法所产生的轮廓细腻找出了每个物体的边界 。然而,如果你放大仔细看可以发现每个物体存在着不止一个轮廓。对于手机摄像头镜头和补光灯前面的圆形区域,就有许多分离的边界。在手机边缘也存在着第二边界。

  需要知道,轮廓算法的检测精度和检测治理一类与所提供的二值图的质量(重新观察前面得到的图像二值图可以看到与第二轮廓之间的联系)。对于一些要求轮廓检测质量高的应用,需要通过不同的 阈值来处理灰度图以便获取质量好的二值图,对于结果查看以便确定最优的阈值。

  存在其他方法可以利用前面所产生的二值图来去除不需要的轮廓。你也能使用更好的轮廓特征算法,可以看如下的连接。

2.4 使用单个通道:R,G,B

  只是为了测试一个想法,下面代码使用单个R,G,B通道来进行轮廓检测。前面对此提到过,下面是相应的代码实现。

  • Python
import cv2

# read the image
image = cv2.imread('input/image_1.jpg')

# B, G, R channel splitting
blue, green, red = cv2.split(image)

# detect contours using blue channel and without thresholding
contours1, hierarchy1 = cv2.findContours(image=blue, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)

# draw contours on the original image
image_contour_blue = image.copy()
cv2.drawContours(image=image_contour_blue, contours=contours1, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
# see the results
cv2.imshow('Contour detection using blue channels only', image_contour_blue)
cv2.waitKey(0)
cv2.imwrite('blue_channel.jpg', image_contour_blue)
cv2.destroyAllWindows()

# detect contours using green channel and without thresholding
contours2, hierarchy2 = cv2.findContours(image=green, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
# draw contours on the original image
image_contour_green = image.copy()
cv2.drawContours(image=image_contour_green, contours=contours2, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
# see the results
cv2.imshow('Contour detection using green channels only', image_contour_green)
cv2.waitKey(0)
cv2.imwrite('green_channel.jpg', image_contour_green)
cv2.destroyAllWindows()

# detect contours using red channel and without thresholding
contours3, hierarchy3 = cv2.findContours(image=red, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
# draw contours on the original image
image_contour_red = image.copy()
cv2.drawContours(image=image_contour_red, contours=contours3, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
# see the results
cv2.imshow('Contour detection using red channels only', image_contour_red)
cv2.waitKey(0)
cv2.imwrite('red_channel.jpg', image_contour_red)
cv2.destroyAllWindows()
  • C++
#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
   // read the image
   Mat image = imread("input/image_1.jpg");

   // B, G, R channel splitting
   Mat channels[3];
   split(image, channels);

   // detect contours using blue channel and without thresholding
   vector<vector<Point>> contours1;
   vector<Vec4i> hierarchy1;
   findContours(channels[0], contours1, hierarchy1, RETR_TREE, CHAIN_APPROX_NONE);
   // draw contours on the original image
   Mat image_contour_blue = image.clone();
   drawContours(image_contour_blue, contours1, -1, Scalar(0, 255, 0), 2);
   imshow("Contour detection using blue channels only", image_contour_blue);
   waitKey(0);
   imwrite("blue_channel.jpg", image_contour_blue);
   destroyAllWindows();

   // detect contours using green channel and without thresholding
   vector<vector<Point>> contours2;
   vector<Vec4i> hierarchy2;
   findContours(channels[1], contours2, hierarchy2, RETR_TREE, CHAIN_APPROX_NONE);
   // draw contours on the original image
   Mat image_contour_green = image.clone();
   drawContours(image_contour_green, contours2, -1, Scalar(0, 255, 0), 2);
   imshow("Contour detection using green channels only", image_contour_green);
   waitKey(0);
   imwrite("green_channel.jpg", image_contour_green);
   destroyAllWindows();

   // detect contours using red channel and without thresholding
   vector<vector<Point>> contours3;
   vector<Vec4i> hierarchy3;
   findContours(channels[2], contours3, hierarchy3, RETR_TREE, CHAIN_APPROX_NONE);
   // draw contours on the original image
   Mat image_contour_red = image.clone();
   drawContours(image_contour_red, contours3, -1, Scalar(0, 255, 0), 2);
   imshow("Contour detection using red channels only", image_contour_red);
   waitKey(0);
   imwrite("red_channel.jpg", image_contour_red);
   destroyAllWindows();
}

  下面的图片显示了使用三个不同的参赛通道检测的结果。

▲ 图2.4.1 使用三个不同的彩色通道检测的轮廓

▲ 图2.4.1 使用三个不同的彩色通道检测的轮廓

  上面结果显示只使用单个颜色通道来进行轮廓检测结果并不理想。这是因为基于单个颜色通道无法正确确定物体的编辑,对于像素亮度差异也没有很好的定义。这就是为什么我们使用灰度图来进行二值化并检测轮廓的原因。

2.5 使用CHAIN_APPROX_SIMPLE绘制轮廓

  下面对比一的CHAIN_APPROX_SIMPLE 与 CHAIN_APPROX_NONE之间的差异。

  下面就是对应的代码:

  • Python
"""
Now let's try with `cv2.CHAIN_APPROX_SIMPLE`
"""
# detect the contours on the binary image using cv2.ChAIN_APPROX_SIMPLE
contours1, hierarchy1 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# draw contours on the original image for `CHAIN_APPROX_SIMPLE`
image_copy1 = image.copy()
cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
# see the results
cv2.imshow('Simple approximation', image_copy1)
cv2.waitKey(0)
cv2.imwrite('contours_simple_image1.jpg', image_copy1)
cv2.destroyAllWindows()
  • C++
// Now let us try with CHAIN_APPROX_SIMPLE`
// detect the contours on the binary image using cv2.CHAIN_APPROX_NONE
vector<vector<Point>> contours1;
vector<Vec4i> hierarchy1;
findContours(thresh, contours1, hierarchy1, RETR_TREE, CHAIN_APPROX_SIMPLE);
// draw contours on the original image
Mat image_copy1 = image.clone();
drawContours(image_copy1, contours1, -1, Scalar(0, 255, 0), 2);
imshow("Simple approximation", image_copy1);
waitKey(0);
imwrite("contours_simple_image1.jpg", image_copy1);
destroyAllWindows();

  这两个参数用在fondContours()中的method取值中。

  CHAIN_APPROX_SIMPLE算法对水平、垂直以及对角线方向的片段轮廓进行了省略,仅仅保存了他们的端点。这意味着所有直线上的点都会消失,只保存端点信息。比如,对于举行的轮廓,除了四个角落的点其他的点都被省略。这种模式先处理速度要比CHAIN_APPROX_NONE更快,所需要的内存更少,因此执行时间就会被节省。

  下面给出了处理结果。

  hf[CSDN-ZHUOQINGJOKING-97298]▲ 图1  利用 CHAIN_APPROX_SIMPLE

▲ 图1 利用 CHAIN_APPROX_SIMPLE所得到的结果

  如果你自己观察,在输出结果上 CHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE并没有任何差异。

  那么,为什么是这样?

  这就是 drawContours() 函数的 优点。虽然CHAIN_APPROX_SIMPLE 方法只保存了少了的角点,但drawContours()自动把中间的相邻点都补齐了,尽管他们并没有在输出轮廓结果列表中。

  因此,我们如何确定 CHAIN_APPROX_SIMPLE所带来的优点的确有效果呢?

  • 可以通过OpenCV中的画圆函数对于检测结果中所有的点进行标注;
  • 更换一个不同的图像来帮助我们看到算法的结果。

▲ 图2.5.1  用于展示CHAIN_APPROX_SIMPLE方法的图像

▲ 图2.5.1 用于展示CHAIN_APPROX_SIMPLE方法的图像

  下面代码使用上面的图像来显示CHAIN_APPROX_SIMPLE方法算法的特性。几乎所有的步骤都是相同的,只是添加了for循环以及一些变量的名称。

  • 第一个for巡回是对contours列表中进行循环;
  • 迪若各则是对区域中的每个坐标进行循环;
  • 使用circle() 对于每个坐标绘制相应的圆圈;
  • 最后可以看到相应的结果并存盘。
  • Python
# to actually visualize the effect of `CHAIN_APPROX_SIMPLE`, we need a proper image
image1 = cv2.imread('input/image_2.jpg')
img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)

ret, thresh1 = cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY)
contours2, hierarchy2 = cv2.findContours(thresh1, cv2.RETR_TREE,
                                               cv2.CHAIN_APPROX_SIMPLE)
image_copy2 = image1.copy()
cv2.drawContours(image_copy2, contours2, -1, (0, 255, 0), 2, cv2.LINE_AA)
cv2.imshow('SIMPLE Approximation contours', image_copy2)
cv2.waitKey(0)
image_copy3 = image1.copy()
for i, contour in enumerate(contours2): # loop over one contour area
   for j, contour_point in enumerate(contour): # loop over the points
       # draw a circle on the current contour coordinate
       cv2.circle(image_copy3, ((contour_point[0][0], contour_point[0][1])), 2, (0, 255, 0), 2, cv2.LINE_AA)
# see the results
cv2.imshow('CHAIN_APPROX_SIMPLE Point only', image_copy3)
cv2.waitKey(0)
cv2.imwrite('contour_point_simple.jpg', image_copy3)
cv2.destroyAllWindows()
  • C++
// using a proper image for visualizing CHAIN_APPROX_SIMPLE
Mat image1 = imread("input/image_2.jpg");
Mat img_gray1;
cvtColor(image1, img_gray1, COLOR_BGR2GRAY);
Mat thresh1;
threshold(img_gray1, thresh1, 150, 255, THRESH_BINARY);
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(thresh1, contours2, hierarchy2, RETR_TREE, CHAIN_APPROX_NONE);
Mat image_copy2 = image1.clone();
drawContours(image_copy2, contours2, -1, Scalar(0, 255, 0), 2);
imshow("None approximation", image_copy2);
waitKey(0);
imwrite("contours_none_image1.jpg", image_copy2);
destroyAllWindows();
Mat image_copy3 = image1.clone();
for(int i=0; i<contours2.size(); i=i+1){
   for (int j=0; j<contours2[i].size(); j=j+1){
       circle(image_copy3, (contours2[i][0], contours2[i][1]), 2, Scalar(0, 255, 0), 2);
       }
   }
   imshow("CHAIN_APPROX_SIMPLE Point only", image_copy3);
   waitKey(0);
   imwrite("contour_point_simple.jpg", image_copy3);
   destroyAllWindows();

  执行上面代码可以获得下面的处理结果。
▲ 图2.5.1  处理结果

▲ 图2.5.1 处理结果

  在上面的处理结果中你可以看到在书籍的边缘只有四个角落中的点被标注出来。水平和垂直轮廓线上的点都被消除了。这些现象在一些具有水平和垂直笔画的字幕上也能够看到。

 

§03 廓层级


  谓层级表明了轮廓中父子关系。你可以看到每一种轮廓检测模式会影响图像中的轮廓检测结果,产生层级关系。

3.1 父子关系

  在图像轮廓检测中的物体可能是:

  • 在图像中分布的单个物体(就像前面例子所示)
  • 或者共享相同边缘的物体;

  在多数情况下,如果一个形状中包含有其它形状,我们安全地认为外边形状是出内部形状的父亲。

  下面图表中,存在许多简单的形状,可以说明父子关系。

▲ 图2.5.1  带有很多简单形状的图片,用于说明层级关系

▲ 图2.5.1 带有很多简单形状的图片,用于说明层级关系

  现在看一下下面的图像中,这是上面图像对应的轮廓,他们被找出并标记出来。

  • 一些单个数字的边界都是分离的物体,这可以根据轮廓层级he父子关系来判定;
  • 轮廓3a则是轮廓3的子轮廓,这是因为它表示了轮廓3的内边缘;
  • 轮廓1,2,4都是父轮廓,没有子嗣。他们的标号实际上是任意的。也就是2,1轮廓的标号可以互换。

▲ 图2.5.1  数字显示了不同形状轮廓之间的父子关系

▲ 图2.5.1 数字显示了不同形状轮廓之间的父子关系

3.2 轮廓关系表示

  你已经看到 findContours()函数返回两个输出。轮廓列表以及层级关系。下面讨论层级关系的细节。

  轮廓层级关系表述成一个数组,具有四个元素值。格式如下:

[Next, Previous, First_Child, Parent] 

  这些数值的含义是啥?

  • Next: Denotes the next contour in an image, which is at the same hierarchical level. So,

  • For contour 1, the next contour at the same hierarchical level is 2. Here, Next will be 2.

  • Accordingly, contour 3 has no contour at the same hierarchical level as itself. So, it’s Next value will be -1.

  • Previous: Denotes the previous contour at the same hierarchical level. This means that contour 1 will always have its Previous value as -1.

  • First_Child: Denotes the first child contour of the contour we are currently considering.

  • Contours 1 and 2 have no children at all. So, the index values for their First_Child will be -1.

  • But contour 3 has a child. So, for contour 3, the First_Child position value will be the index position of 3a.

  • Parent: Denotes the parent contour’s index position for the current contour.

  • Contours 1 and 2, as is obvious, do not have any Parent contour.

  • For the contour 3a, its Parent is going to be contour 3

  • For contour 4, the parent is contour 3a

  根据上面的描述,我们如何能够将这些关系可视化呢?最好的方法就是:

  • 像前面举例那样使用带有直线和形状的简单图像;
  • 检测轮廓以及层级关系,使用不同的提取方法;
  • 打印取值来显示;

3.3 不同的轮廓获取技术

  至此,我们使用一个特定的获取技术, RET_TREE来找出绘制轮廓,但还有另外三个获取方法: RETR_LIST, RETR_EXTERNAL以及RETR_CCOMP。

  因此我们使用前面给出的图像来观察者四种方法产生的结果。

  下面的代码是读取磁盘上的图像转换成灰度图并进行阈值二值化。

  • Python
"""
Contour detection and drawing using different extraction modes to complement
the understanding of hierarchies
"""
image2 = cv2.imread('input/custom_colors.jpg')
img_gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
ret, thresh2 = cv2.threshold(img_gray2, 150, 255, cv2.THRESH_BINARY)
  • C++
/*
Contour detection and drawing using different extraction modes to complement the understanding of hierarchies
*/
Mat image2 = imread("input/custom_colors.jpg");
Mat img_gray2;
cvtColor(image2, img_gray2, COLOR_BGR2GRAY);
Mat thresh2;
threshold(img_gray2, thresh2, 150, 255, THRESH_BINARY);

3.3.1 RET_LIST

  RETR_LIST轮廓获取方法不创建任何父子关系,因此所有的轮廓面积都被监测到, First_Child, Parant索引都是-1.

  前面说过,所有的轮廓都是存在Previous, Next 。

  下面是代码中显示了RETR_LISTF得到的结果。

  • Python
contours3, hierarchy3 = cv2.findContours(thresh2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
image_copy4 = image2.copy()
cv2.drawContours(image_copy4, contours3, -1, (0, 255, 0), 2, cv2.LINE_AA)
# see the results
cv2.imshow('LIST', image_copy4)
print(f"LIST: {hierarchy3}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_list.jpg', image_copy4)
cv2.destroyAllWindows()
  • C++
vector<vector<Point>> contours3;
vector<Vec4i> hierarchy3;
findContours(thresh2, contours3, hierarchy3, RETR_LIST, CHAIN_APPROX_NONE);
Mat image_copy4 = image2.clone();
drawContours(image_copy4, contours3, -1, Scalar(0, 255, 0), 2);
imshow("LIST", image_copy4);
waitKey(0);
imwrite("contours_retr_list.jpg", image_copy4);
destroyAllWindows();

  下面是代码运行的结果。

LIST: [[[ 1 -1 -1 -1]
[ 2  0 -1 -1]
[ 3  1 -1 -1]
[ 4  2 -1 -1]
[-1  3 -1 -1]]]

  很显然前面的LIST表格显示了轮廓检测区域结果中的第三个和第四个位置数值都是-1.

3.3.2 RETR_EXTERNAL

  RETR_EXTERNAL方法仅仅检测父亲轮廓,因此所有内部的子轮廓都被忽略。

  • Python
contours4, hierarchy4 = cv2.findContours(thresh2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
image_copy5 = image2.copy()
cv2.drawContours(image_copy5, contours4, -1, (0, 255, 0), 2, cv2.LINE_AA)
# see the results
cv2.imshow('EXTERNAL', image_copy5)
print(f"EXTERNAL: {hierarchy4}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_external.jpg', image_copy5)
cv2.destroyAllWindows()
  • C++
vector<vector<Point>> contours4;
vector<Vec4i> hierarchy4;
findContours(thresh2, contours4, hierarchy4, RETR_EXTERNAL, CHAIN_APPROX_NONE);
Mat image_copy5 = image2.clone();
drawContours(image_copy5, contours4, -1, Scalar(0, 255, 0), 2);
imshow("EXTERNAL", image_copy5);
waitKey(0);
imwrite("contours_retr_external.jpg", image_copy4);
destroyAllWindows();

  前面的代码运行结果为:

EXTERNAL: [[[ 1 -1 -1 -1]
[ 2  0 -1 -1]
[-1  1 -1 -1]]]

▲ 图2.5.1  利用RETR_EXTERNAL检测结果

▲ 图2.5.1 利用RETR_EXTERNAL检测结果

  可以看到在RETR_EXTERNAL检测方法中,只有外部的轮廓才被检测出来。

3.3.3 RETR_CCOMP

  RETR_CCOMP检测方法将会把图像中的所有轮廓都检测出来,它使用一个2-层的层级关系来描述图像中物体形状。

  这要求降e是:

  • 所有的外部轮廓都是层级1;
  • 所有的内部轮廓都是层级2;

  如果我们在层级2中的轮廓中还有其他物体怎么办?就像前面给出的 轮廓4 那样。

此时:

  • 轮廓4 继续具有层级1
  • 轮廓4中如何还有轮廓则为层级2

  下图给出了 层级结果:

▲ 图2.5.1  利用 RETR_CCOMP检测的层级结果

▲ 图2.5.1 利用 RETR_CCOMP检测的层级结果

  上面给出了层级定义,下面通过代码来看一下实际检测输出的列表。

  • Python
contours5, hierarchy5 = cv2.findContours(thresh2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
image_copy6 = image2.copy()
cv2.drawContours(image_copy6, contours5, -1, (0, 255, 0), 2, cv2.LINE_AA)

# see the results
cv2.imshow('CCOMP', image_copy6)
print(f"CCOMP: {hierarchy5}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_ccomp.jpg', image_copy6)
cv2.destroyAllWindows()
  • C++
vector<vector<Point>> contours5;
vector<Vec4i> hierarchy5;
findContours(thresh2, contours5, hierarchy5, RETR_CCOMP, CHAIN_APPROX_NONE);
Mat image_copy6 = image2.clone();
drawContours(image_copy6, contours5, -1, Scalar(0, 255, 0), 2);
imshow("EXTERNAL", image_copy6);
// cout << "EXTERNAL:" << hierarchy5;
waitKey(0);
imwrite("contours_retr_ccomp.jpg", image_copy6);
destroyAllWindows();

  执行上面的代码,产生如下的输出。

CCOMP: [[[ 1 -1 -1 -1]
[ 3  0  2 -1]
[-1 -1 -1  1]
[ 4  1 -1 -1]
[-1  3 -1 -1]]]

3.3.4 RETR_TREE

  RETR_TREE获得所有的轮廓并建立复杂的层级关系,层级关系不再限定在1,或者2. 每个轮廓都会有自己的层级关系。根据他所在的层级对应的具体的父子关系。

▲ 图2.5.1  应用 RETR_TREE检测结果

▲ 图2.5.1 应用 RETR_TREE检测结果

  从上图来看:

  • 轮廓 1,2,3具有相同的层级,都是0级;
  • 3a层级为1,是3的子集;
  • 4 是新的轮廓面积,所以它的层级为2

  下面是对应的检测代码:

  • Python
contours6, hierarchy6 = cv2.findContours(thresh2, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
image_copy7 = image2.copy()
cv2.drawContours(image_copy7, contours6, -1, (0, 255, 0), 2, cv2.LINE_AA)
# see the results
cv2.imshow('TREE', image_copy7)
print(f"TREE: {hierarchy6}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_tree.jpg', image_copy7)
cv2.destroyAllWindows()
  • C++
vector<vector<Point>> contours6;
vector<Vec4i> hierarchy6;
findContours(thresh2, contours6, hierarchy6, RETR_TREE, CHAIN_APPROX_NONE);
Mat image_copy7 = image2.clone();
drawContours(image_copy7, contours6, -1, Scalar(0, 255, 0), 2);
imshow("EXTERNAL", image_copy7);
// cout << "EXTERNAL:" << hierarchy6;
waitKey(0);
imwrite("contours_retr_tree.jpg", image_copy7);
destroyAllWindows();

  代码执行结果:

TREE: [[[ 3 -1  1 -1]
[-1 -1  2  0]
[-1 -1 -1  1]
[ 4  0 -1 -1]
[-1  3 -1 -1]]]

▲ 图2.5.1  利用 RETR_TREE获得的轮廓结果

▲ 图2.5.1 利用 RETR_TREE获得的轮廓结果

  上图中可以看到所有的轮廓都被检测出。 3,3a是两个独立的轮廓,他们具有不同的区域面积。同时他们是父子关系。

  现在你对于OpenCV中的轮廓检测算法更熟悉了,包括对应的输入参数和配置。下面就利用它来解决你自己的工作吧。

 

  结 ※


  文对于OpenCV中的轮廓检测算法进行了讨论,可以看到一些基于轮廓检测的应用。接着对四种不同的提取方式的结果进行了讨论。你还了解了如何将轮廓进行绘制的方法。

4.1 不同检测算法对应的时间

  为了补充对于轮廓检测方法的说明下表给出了不同方法对应的检测时间。

Contour Retrieval MethodTime Take (in seconds)
RETR LIST0.000382
RETR EXTERNAL0.000554
RETR CCOMP0.001845
RETR TREE0.005594

  对于具有大量边缘的图像,上述处理速度的不同会影响算法运行的性能。

4.2 局限性

  至此,我们所探讨的例子所产生的结构都有趣而且令人满意。但还有些情况,findContours并不能够给出令人满意有用的结果。比如:

  • 当物体与背景具有很强的差异时,可以清楚确定物体边缘像素。这种情况效果很好。但有些情况,不如下面给出的这个例子,这个小狗与背景之间就不是那么容易区分,所以可以看到所检测到的结果就非常令人沮丧。

▲ 图2.5.1  一个具有很困难轮廓检测的图片

▲ 图2.5.1 一个具有很困难轮廓检测的图片

  • 对于背景充满着线段的情况轮廓检测往往也会失败。


■ 相关文献链接:

● 相关图表链接:

  • 49
    点赞
  • 318
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Python的OpenCV(Open Source Computer Vision)库是一个用于计算机视觉图像处理的强大工具。它提供了许多功能和算法,其中包括识别轮廓。 在OpenCV中,轮廓是由一组连接在一起的点组成的曲线,它描述了图像中的对象边缘。识别轮廓在许多图像处理和计算机视觉应用中非常常见,比如形状分析、对象检测和跟踪等。 要识别轮廓,首先我们需要把图像转换成灰度图像。这可以通过使用OpenCV的cv2.cvtColor()函数将图像从BGR格式(默认)转换为灰度格式来实现。接下来,我们需要使用cv2.threshold()或cv2.Canny()函数将图像转换为二值图像。这将使得轮廓更明显并且更容易识别。 一旦我们得到了二值图像,我们可以使用cv2.findContours()函数来查找图像中的轮廓。这个函数返回一个由轮廓点组成的列表,每个轮廓都表示为一个Numpy数组。我们还可以通过传递适当的参数来控制轮廓的检测和过滤。 接下来,我们可以使用cv2.drawContours()函数将找到的轮廓绘制到原始图像上,以便我们能够可视化和分析它们。我们可以选择绘制所有的轮廓或者仅绘制特定的轮廓。 最后,我们可以对识别到的轮廓进行进一步的分析和处理。OpenCV提供了很多函数来计算轮廓的形状属性,比如周长、面积、凸包等。我们可以利用这些属性来进行对象分类或者执行其他图像处理任务。 总结来说,通过使用Python的OpenCV库,我们可以轻松地识别并处理图像中的轮廓。这为我们提供了一个强大的工具来进行形状分析、对象检测和跟踪等计算机视觉任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卓晴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值