OpenCV基础(13)基于OpenCV的轮廓检测

利用轮廓检测,可以检测出目标的边界,并在图像中方便地定位目标。它通常是许多有趣应用的第一步,如图像前景提取,简单的图像分割,检测和识别。

因此,让我们学习使用OpenCV的轮廓和轮廓检测,并自己看看如何使用它们来构建各种应用程序。

1.轮廓在计算机视觉中的应用

已经存使用轮廓进行运动检测或分割的应用程序。下面是一些例子:

  • 运动检测 :在监控视频中,运动检测技术有许多应用,包括室内和室外的安全环境、交通控制、体育活动中的行为检测、无人看管的物体检测,甚至视频压缩。在下面的图中,我们可以看到在视频流中检测人的移动在监控应用中是多么有用。请注意,静止在图像左侧的一组人是如何不被检测到的。
    在这里插入图片描述
  • 无人看管的对象检测:公共场所任何无人看管的物体通常被认为是可疑物体。一种有效而安全的解决方案是:(利用背景消除法以及轮廓检测实现无人值勤目标检测)。
    在这里插入图片描述
  • 前景\背景分割:要用另一幅图像替换背景,需要执行图像前景提取(类似于图像分割)。使用轮廓是一种可以用于执行分割的方法。下面的图片展示了这样一个应用程序的简单示例:
    在这里插入图片描述

2.什么是轮廓

当我们把物体边界上的所有点连接起来时,就得到一条轮廓线。通常,特定的轮廓是指具有相同颜色和强度的边界像素。OpenCV使得在图像中寻找和绘制轮廓变得非常容易。它提供了两个简单的功能:

  • findContours()
  • drawContours()

同时,它有两种不同的轮廓检测算法:

  • CHAIN_APPROX_SIMPLE
  • CHAIN_APPROX_NONE

既然已经介绍了轮廓,现在让我们讨论检测轮廓所涉及的步骤。

3.OpenCV中轮廓检测和绘制的步骤

OpenCV使这一任务变得相当简单。只需遵循以下步骤:

  • 1.读取图像并将其转换为灰度格式:读取图像并将图像转换为灰度格式。将图像转换为灰度是非常重要的,因为它为下一步准备图像。将图像转换为单通道灰度图像是实现阈值化的重要步骤,而阈值化是轮廓检测算法正常工作的必要条件。
  • 2.应用二值化:在寻找轮廓时,首先对灰度图像进行二值化或Canny边缘检测。在这里,我们将应用二值化。
    这将图像转换为黑白,突出感兴趣的目标,使轮廓检测算法更容易。阈值化使图像中物体的边界完全变白,所有像素具有相同的强度。该算法现在可以从这些白色像素中检测出物体的边界。
    注意:值为0的黑色像素被视为背景像素而忽略。
    在这一点上,可能会出现一个问题。如果我们使用单一通道,如R(红色),G(绿色),或B(蓝色),而不是灰度(阈值)图像会怎样?在这种情况下,轮廓检测算法就不能很好地工作。如前所述,该算法寻找边界和类似强度的像素来检测轮廓。二值图像提供的信息比单一(RGB)彩色通道图像要好得多。在博客的后面部分,我们得到了仅使用单个R、G或B通道而不是灰度和阈值图像时的结果图像。
  • 3.找到轮廓:使用findContours()函数检测图像中的轮廓。
  • 4.在原始RGB图像上绘制轮廓:一旦轮廓被确定,使用drawContours()函数将轮廓覆盖在原始RGB图像上。

4.使用OpenCV二值化图像

(1)Python

# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image = cv2.imread('0.jpg')
# 将图像转换为灰度格式
img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化
ret, thresh = cv2.threshold(img_gray, 150, 255, cv2.THRESH_BINARY)
# 显示
cv2.imshow('Binary image', thresh)
cv2.waitKey(0)
cv2.imwrite('image_thres1.jpg', thresh)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 读取
    Mat image = imread("0.jpg");
	// 将图像转换为灰度格式
	Mat img_gray;
	cvtColor(image, img_gray, COLOR_BGR2GRAY);
	// 二值化
	Mat thresh;
	threshold(img_gray, thresh, 150, 255, THRESH_BINARY);
	imshow("Binary mage", thresh);
	waitKey(0);
	imwrite("image_thres1.jpg", thresh);
	destroyAllWindows();

请添加图片描述在这里插入图片描述

5.使用CHAIN_APPROX_NONE绘制轮廓

现在,让我们使用CHAIN_APPROX_NONE方法找到并绘制轮廓。

findContours()函数开始。它有三个必需参数,如下所示。

  • image:上一步获取的二值输入图像。
  • mode:这是轮廓检索模式。我们使用RETR_TREE,这意味着该算法将从二值图像中检索所有可能的轮廓。更多的轮廓检索模式是可用的,我们也将讨论它们。
  • method:这定义了轮廓近似法。在本例中,我们将使用CHAIN_APPROX_NONE。虽然略慢于CHAIN_APPROX_SIMPLE,但我们将在这里使用这个方法来存储所有轮廓点。

这里需要强调的是,模式是指要检索的轮廓的类型,而方法是指存储轮廓中的哪些点。我们将在下面对此进行更详细的讨论。

接下来,使用drawContours()函数在RGB图像上覆盖轮廓。这个函数有四个必选参数和几个可选参数。下面的前四个参数是必需的。

  • image:这是你想在上面画轮廓的输入RGB图像。
  • contours:从findContours()函数获得的轮廓
  • contourIdx:在得到的所有轮廓中列出轮廓索引。使用此参数,您可以指定此列表中的索引位置,准确指示要绘制的轮廓。提供一个负值将绘制所有轮廓。
  • color:这表示要绘制的轮廓的颜色。我们用绿色标出这些轮廓。
  • thickness:这是轮廓的厚度。

(1)Python

# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image = cv2.imread('0.jpg')
# 将图像转换为灰度格式
img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化
ret, thresh = cv2.threshold(img_gray, 150, 255, cv2.THRESH_BINARY)
# 显示
cv2.imshow('Binary image', thresh)
cv2.waitKey(0)
# 使用cv2.CHAIN_APPROX_NONE检测二值图像上的轮廓。
contours, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
                                     
# 在原始图像上绘制轮廓
image_copy = image.copy()
cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
               
# 显示结果
cv2.imshow('None approximation', image_copy)
cv2.waitKey(0)
# cv2.imwrite('contours_none_image1.jpg', image_copy)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 读取
    Mat image = imread("0.jpg");
	// 将图像转换为灰度格式
	Mat img_gray;
	cvtColor(image, img_gray, COLOR_BGR2GRAY);
	// 二值化
	Mat thresh;
	threshold(img_gray, thresh, 150, 255, THRESH_BINARY);
	imshow("Binary mage", thresh);
	waitKey(0);
	// 使用cv2.CHAIN_APPROX_NONE检测二值图像上的轮廓
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE);
	// 在原始图像上绘制轮廓
	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();

在这里插入图片描述
正如上图所示,算法生成的轮廓很好地识别了每个对象的边界。然而,如果你仔细看手机,你会发现它包含不止一个轮廓线。对于与相机镜头和光线相关的圆形区域,已经确定了单独的轮廓。沿着手机边缘的部分也有“次级”轮廓。

请记住,轮廓算法的准确性和质量很大程度上取决于所提供的二值图像的质量(再次查看上一节中的二值图像,您可以看到与这些次级轮廓相关的线)。有些应用需要高质量的轮廓。在这种情况下,在创建二值图像时,使用不同的阈值进行实验,看看是否会改善结果轮廓。

在轮廓生成之前,还可以使用其他方法来消除二值图中不需要的轮廓。您还可以使用与我们将在这里讨论的轮廓算法相关的更高级的特征。

6.使用单通道:红色、绿色或蓝色

下面是分别使用红色、绿色和蓝色通道检测轮廓时的一些结果。我们在前面的轮廓检测步骤中讨论过这个问题。下面是与上面相同的图像的Python和c++代码。
(1)Python

import cv2

# 读取图像
image = cv2.imread('input/image_1.jpg')

# B, G, R 通道分离
blue, green, red = cv2.split(image)

# 使用蓝色通道和无阈值检测轮廓
contours1, hierarchy1 = cv2.findContours(image=blue, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)

# 在原始图像上绘制轮廓
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)
# 查看结果
cv2.imshow('Contour detection using blue channels only', image_contour_blue)
cv2.waitKey(0)
cv2.imwrite('blue_channel.jpg', image_contour_blue)
cv2.destroyAllWindows()

# 使用绿色通道和无阈值检测轮廓
contours2, hierarchy2 = cv2.findContours(image=green, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
# 在原始图像上绘制轮廓
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)
# 查看结果
cv2.imshow('Contour detection using green channels only', image_contour_green)
cv2.waitKey(0)
cv2.imwrite('green_channel.jpg', image_contour_green)
cv2.destroyAllWindows()

# 使用红色通道和无阈值检测轮廓
contours3, hierarchy3 = cv2.findContours(image=red, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_NONE)
# 在原始图像上绘制轮廓
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)
# 查看结果
cv2.imshow('Contour detection using red channels only', image_contour_red)
cv2.waitKey(0)
cv2.imwrite('red_channel.jpg', image_contour_red)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main() {
   // 读取图片
   Mat image = imread("input/image_1.jpg");

   // B, G, R 通道分离
   Mat channels[3];
   split(image, channels);

   // 使用蓝色通道检测轮廓而不使用阈值
   vector<vector<Point>> contours1;
   vector<Vec4i> hierarchy1;
   findContours(channels[0], contours1, hierarchy1, RETR_TREE, CHAIN_APPROX_NONE);
   // 在原始图像上绘制轮廓
   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();

   // 使用绿色通道和无阈值检测轮廓
   vector<vector<Point>> contours2;
   vector<Vec4i> hierarchy2;
   findContours(channels[1], contours2, hierarchy2, RETR_TREE, CHAIN_APPROX_NONE);
   // 在原始图像上绘制轮廓
   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();

   // 使用红色通道和无阈值检测轮廓
   vector<vector<Point>> contours3;
   vector<Vec4i> hierarchy3;
   findContours(channels[2], contours3, hierarchy3, RETR_TREE, CHAIN_APPROX_NONE);
   // 在原始图像上绘制轮廓
   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();
}

在这里插入图片描述
从上图中我们可以看出,轮廓检测算法并不能很好地找到轮廓。这是因为它不能正确地检测物体的边界,也没有很好地定义像素之间的强度差。这就是为什么我们更喜欢使用灰度和二值阈值图像来检测轮廓。

7.使用CHAIN_APPROX_SIMPLE绘制轮廓

(1)Python

# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image = cv2.imread('0.jpg')
# 将图像转换为灰度格式
img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化
ret, thresh = cv2.threshold(img_gray, 150, 255, cv2.THRESH_BINARY)
# 显示
cv2.imshow('Binary image', thresh)
cv2.waitKey(0)
# 使用cv2.CHAIN_APPROX_NONE检测二值图像上的轮廓。
contours, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE)
                                     
# 在原始图像上绘制轮廓
image_copy = image.copy()
cv2.drawContours(image=image_copy, contours=contours, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
               
# 显示结果
cv2.imshow('None approximation', image_copy)
cv2.waitKey(0)
# cv2.imwrite('contours_none_image1.jpg', image_copy)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 读取
    Mat image = imread("0.jpg");
	// 将图像转换为灰度格式
	Mat img_gray;
	cvtColor(image, img_gray, COLOR_BGR2GRAY);
	// 二值化
	Mat thresh;
	threshold(img_gray, thresh, 150, 255, THRESH_BINARY);
	imshow("Binary mage", thresh);
	waitKey(0);
	// 使用cv2.CHAIN_APPROX_NONE检测二值图像上的轮廓
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
	// 在原始图像上绘制轮廓
	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();

CHAIN_APPROX_SIMPLE算法压缩沿轮廓的水平、垂直和对角线段,只留下它们的端点。这意味着沿着直线路径的任何点都将被忽略,只剩下端点。例如,考虑一个矩形的轮廓。除四个角点外,所有轮廓点将被剔除。这种方法比CHAIN_APPROX_NONE更快,因为算法不存储所有的点,使用更少的内存,因此执行时间更少。
在这里插入图片描述
为什么呢?
功劳归于drawContours()函数。虽然CHAIN_APPROX_SIMPLE方法通常会导致更少的点,但drawContours()函数会自动连接相邻的点,将它们连接起来,即使它们不在轮廓列表中。

那么,我们如何确认CHAIN_APPROX_SIMPLE算法实际上是有效的呢?

  • 最直接的方法是手动循环轮廓点,并使用OpenCV在检测到的轮廓坐标上画一个圆。
  • 同时,我们使用不同的图像来帮助我们可视化算法的结果。

下面的代码使用上面的图像来可视化CHAIN_APPROX_SIMPLE算法的效果。除了两个额外的for循环和一些变量名之外,几乎所有内容都与前面的代码示例相同。

  • 第一个for循环遍历在轮廓列表中的每个轮廓区域。
  • 第二个循环是遍历在这个区域的每个坐标。
  • 然后我们使用OpenCV的circle()函数在每个坐标点上画一个绿色的圆。
  • 最后,我们可视化结果并将其保存到磁盘中。

(1)Python

# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
# 为了真正可视化“CHAIN_APPROX_SIMPLE”的效果,我们需要一个合适的图像
image1 = cv2.imread('new.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): # 遍历轮廓
   for j, contour_point in enumerate(contour): # 遍历轮廓点
       # 在当前轮廓点坐标上画一个圆
       cv2.circle(image_copy3, ((contour_point[0][0], contour_point[0][1])), 2, (0, 255, 0), 2, cv2.LINE_AA)
# 可视化
cv2.imshow('CHAIN_APPROX_SIMPLE Point only', image_copy3)
cv2.waitKey(0)
cv2.imwrite('contour_point_simple.jpg', image_copy3)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 使用合适的图像来可视化CHAIN APPROX SIMPLE
	Mat image1 = imread("new.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();
}

在这里插入图片描述
使用CHAIN_APPROX_SIMPLE进行轮廓检测时,可以看到书的四个角上只有四个轮廓点。书中垂直和水平的直线完全被忽略了。
观察输出图像,它在上图的右手边。请注意,书的垂直和水平边只包含四个角。还要注意字母和鸟是用离散的点而不是线段表示的。

8.轮廓层次结构

层次表示轮廓之间的父子关系。您将看到每种轮廓检索模式如何影响图像中的轮廓检测,并产生分层结果。

(1)亲子关系

图像中轮廓检测算法检测到的物体可以是:

  • 在图像中分散的单个物体(如第一个例子),或
  • 物体和形状在彼此内部

在大多数情况下,当一个形状包含更多的形状时,我们可以有把握地得出结论,外部形状是内部形状的父形状。
看一下下图,它包含了几个简单的形状,可以帮助演示轮廓层次结构。
在这里插入图片描述
图像与简单的线条和形状。我们将使用这张图片来学习更多关于轮廓层次结构的知识。现在请看下图,与上图中的每个形状相关联的轮廓已经被识别出来了。下图中的每个数字都有重要意义。

  • 根据轮廓层次和父子关系,所有的个体数字,即1、2、3、4,都是独立的对象。
  • 我们可以说 3a 是 3 的孩子。 注意 3a 代表 3 的内部部分。
  • 轮廓1、2和4都是父形状,没有任何关联的子形状,因此它们的编号是任意的。换句话说,轮廓2可以被标记为1,反之亦然。
    在这里插入图片描述

(2)轮廓的关系表示

您已经看到findContours()函数返回两个输出:contours列表和层次结构。现在让我们详细了解轮廓层次结构输出。
轮廓层次结构表示为数组,数组又包含四个值。它表示为:[Next, Previous, First_Child, Parent]
那么,所有这些值意味着什么?
Next:表示图像中的下一个轮廓,处于同一层次。所以,

  • 对于轮廓1,同一层次下一个轮廓为2。这里,Next是2。
  • 因此,轮廓3没有与它本身相同层次的下一个轮廓。所以下一个值是-1。

Previous:在同一层次上的前一个轮廓。这意味着轮廓1的Previous值始终为-1。
First_Child:表示我们当前考虑的轮廓的第一个子轮廓。

  • 轮廓1和2根本没有子轮廓。因此,First_Child的索引值将是-1。
  • 但是轮廓3有个子轮廓。因此,对于轮廓3,First_Child的位置值将是索引位置3a。

Parent:表示当前轮廓的父轮廓索引位置。

  • 很明显,轮廓1和2没有任何父轮廓。
  • 对于3a轮廓,它的父轮廓是3
  • 轮廓4的父轮廓是轮廓3a

上面的解释是有意义的,但是我们如何实际可视化这些层次结构数组呢?最好的方法是:

  • 使用一个有线条和形状组成的简单图像
  • 使用不同的检索模式检测轮廓和层次结构
  • 然后打印这些值以可视化它们

(3)不同的轮廓检索技术

到目前为止,我们使用了一种特定的检索技术RETR TREE来查找和绘制轮廓,但是OpenCV中还有另外三种轮廓检索技术,即RETR LIST、RETR EXTERNAL和RETR CCOMP
在这里插入图片描述
因此,现在让我们使用上图中的图像来检查这四个方法中的每一个,以及它们的相关代码来获得轮廓。

  • RETR_LIST
    RETR_LIST轮廓检索方法不会在提取的轮廓之间创建任何父-子关系。因此,对于所有检测到的轮廓区域,First_ChildParent索引位置值始终为-1。
    如上所述,所有轮廓都有相应的上一轮廓和下一轮廓。
    (1)Python
# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image1 = cv2.imread('simple.png')
img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
ret, thresh1= cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY)
contours1, hierarchy1 = cv2.findContours(thresh1, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
image_copy1 = image1.copy()
cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
# 显示
cv2.imshow('LIST', image_copy1)
print(f"LIST: {hierarchy1}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_list.jpg', image_copy1)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 使用合适的图像来可视化
	Mat image1 = imread('simple.png');
	Mat img_gray1;
	cvtColor(image1, img_gray1, COLOR_BGR2GRAY);
	Mat thresh1;
	threshold(img_gray1, thresh1, 150, 255, THRESH_BINARY);
	vector<vector<Point>> contours1;
	vector<Vec4i> hierarchy1;
	findContours(thresh1, contours1, hierarchy1, RETR_LIST, CHAIN_APPROX_NONE);
	Mat image_copy1 = image1.clone();
	drawContours(image_copy1, contours1, -1, Scalar(0, 255, 0), 2);
	imshow("LIST", image_copy1);
	waitKey(0);
	imwrite("contours_retr_list.jpg", image_copy1);
	destroyAllWindows();
}

在这里插入图片描述在这里插入图片描述
可以清楚地看到,所有检测到的轮廓区域的第3和第4个索引位置都是-1,正如预期的那样。

  • RETR_EXTERNAL
    RETR_EXTERNAL轮廓检索方法是一种非常有趣的方法。它只检测父轮廓,而忽略任何子轮廓。所有的内轮廓,比如3a和4上面都没有点。
    (1)Python
# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image1 = cv2.imread('simple.png')
img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
ret, thresh1= cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY)
contours1, hierarchy1 = cv2.findContours(thresh1, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
image_copy1 = image1.copy()
cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
# 显示
cv2.imshow('EXTERNAL', image_copy1)
print(f"EXTERNAL: {hierarchy1}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_external.jpg', image_copy1)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 使用合适的图像来可视化
	Mat image1 = imread('simple.png');
	Mat img_gray1;
	cvtColor(image1, img_gray1, COLOR_BGR2GRAY);
	Mat thresh1;
	threshold(img_gray1, thresh1, 150, 255, THRESH_BINARY);
	vector<vector<Point>> contours1;
	vector<Vec4i> hierarchy1;
	findContours(thresh1, contours1, hierarchy1, RETR_EXTERNAL, CHAIN_APPROX_NONE);
	Mat image_copy1 = image1.clone();
	drawContours(image_copy1, contours1, -1, Scalar(0, 255, 0), 2);
	imshow("EXTERNAL", image_copy1);
	waitKey(0);
	imwrite("contours_retr_external.jpg", image_copy1);
	destroyAllWindows();
}

在这里插入图片描述在这里插入图片描述
上面的输出图像只显示了在轮廓1、2和3上绘制的点。轮廓3a和4省略,因为它们是子轮廓。

  • RETR_CCOMP
    RETR_EXTERNAL不同,RETR_CCOMP检索图像中的所有轮廓。除此之外,它还对图像中的所有形状或对象应用2级层次结构。

    • 所有的外部轮廓将有层次1级
    • 所有的内部轮廓将有层次2级

    但是如果我们有一个轮廓在另一个层次2级轮廓内部呢?就像轮廓4在3a内部。

    • 同样,轮廓4将有层次1级。
    • 如果轮廓4内有轮廓,则此轮廓等级为2。
      在下面的图像中,轮廓已经根据其层次级别进行了编号,如上所述。
      在这里插入图片描述
      上图显示了第1级和第2级的层次级别分别为HL-1和HL-2。现在,让我们看看代码和输出层次数组。
      (1)Python
# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image1 = cv2.imread('simple.png')
img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
ret, thresh1= cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY)
contours1, hierarchy1 = cv2.findContours(thresh1, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
image_copy1 = image1.copy()
cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
# 显示
cv2.imshow('CCOMP', image_copy1)
print(f"CCOMP: {hierarchy1}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_ccomp.jpg', image_copy1)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 使用合适的图像来可视化
	Mat image1 = imread('simple.png');
	Mat img_gray1;
	cvtColor(image1, img_gray1, COLOR_BGR2GRAY);
	Mat thresh1;
	threshold(img_gray1, thresh1, 150, 255, THRESH_BINARY);
	vector<vector<Point>> contours1;
	vector<Vec4i> hierarchy1;
	findContours(thresh1, contours1, hierarchy1, RETR_CCOMP, CHAIN_APPROX_NONE);
	Mat image_copy1 = image1.clone();
	drawContours(image_copy1, contours1, -1, Scalar(0, 255, 0), 2);
	imshow("EXTERNAL", image_copy1);
	// cout << "EXTERNAL:" << hierarchy1;
	waitKey(0);
	imwrite("contours_retr_ccomp.jpg", image_copy1);
	destroyAllWindows();
}

在这里插入图片描述在这里插入图片描述
在这里,我们看到,根据轮廓检索方法,所有的Next、Previous、First Child和Parent关系都得到了维护,因为所有的轮廓都被检测到了。如预期,第一个轮廓区域的Previous为-1。而没有任何父元素的轮廓,值也是-1

  • RETR_TREE
    就像RETR_CCOMP一样,RETR_TREE也检索所有的轮廓。它还创建了一个完整的层次结构,级别不限于1或2。每个轮廓可以有自己的层次结构,与它所处的级别一致,以及它所具有的相应的父子关系。
    在这里插入图片描述
    从上图可以看出:
  • 轮廓 1、2 和 3 处于同一级别,即级别 0
  • 轮廓3a等级1,因为它是轮廓3的子轮廓。
  • 轮廓4是一种新的轮廓区域,其等级为2。

(1)Python

# 首先导入OpenCV,并读取输入图像。
import cv2
# 读取图像
image1 = cv2.imread('simple.png')
img_gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
ret, thresh1= cv2.threshold(img_gray1, 150, 255, cv2.THRESH_BINARY)
contours1, hierarchy1 = cv2.findContours(thresh1, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
image_copy1 = image1.copy()
cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
# 显示
cv2.imshow('TREE', image_copy1)
print(f"TREE: {hierarchy1}")
cv2.waitKey(0)
cv2.imwrite('contours_retr_tree.jpg', image_copy1)
cv2.destroyAllWindows()

(2)C++

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    // 使用合适的图像来可视化
	Mat image1 = imread('simple.png');
	Mat img_gray1;
	cvtColor(image1, img_gray1, COLOR_BGR2GRAY);
	Mat thresh1;
	threshold(img_gray1, thresh1, 150, 255, THRESH_BINARY);
	vector<vector<Point>> contours1;
	vector<Vec4i> hierarchy1;
	findContours(thresh1, contours1, hierarchy1, RETR_TREE, CHAIN_APPROX_NONE);
	Mat image_copy1 = image1.clone();
	drawContours(image_copy1, contours1, -1, Scalar(0, 255, 0), 2);
	imshow("EXTERNAL", image_copy1);
	// cout << "EXTERNAL:" << hierarchy1;
	waitKey(0);
	imwrite("contours_retr_tree.jpg", image_copy1);
	destroyAllWindows();
}

在这里插入图片描述
所有的轮廓都按照预期绘制,轮廓区域清晰可见。你还推断轮廓3和3a是两个独立的轮廓,因为它们有不同的轮廓边界和区域。同时,很明显轮廓3a是轮廓3的产物。

(4)不同轮廓检索方法的运行时比较

仅仅知道轮廓检索方法是不够的。您还应该了解它们的相对处理时间。下表比较了上面讨论的每个方法的运行时。
在这里插入图片描述
比较了不同方法的推理速度
从上表中可以得出一些有趣的结论:

  • RETR_LISTRETR_EXTERNAL的执行时间最少,因为RETR_LIST没有定义任何层次结构,而RETR_EXTERNAL只检索父轮廓
  • RETR_CCOMP的执行时间是第二快的。它检索所有轮廓并定义一个两级层次结构。
  • RETR_TREE执行时间最长,因为它检索了所有的轮廓,并为每个父子关系定义了独立的层次结构级别。

虽然上述时间可能看起来不重要,但重要的是要意识到可能需要大量轮廓处理的应用程序的差异。同样值得注意的是,这个处理时间可能会有所不同,这取决于它们提取的轮廓的程度以及它们定义的层次级别。

9.局限性

到目前为止,我们探索的所有例子似乎都很有趣,结果也令人鼓舞。然而,在某些情况下,轮廓算法可能无法提供有意义和有用的结果。让我们也考虑这样一个例子。

  • 当图像中的物体与它们的背景形成强烈对比时,你可以清楚地识别出与每个物体相关的轮廓。但是,如果您有一个图像,如下面的图所示,该怎么办呢?它不仅有一个明亮的对象(小狗),而且还有一个与感兴趣的对象(小狗)相同值(亮度)的背景。你会发现右图中的轮廓甚至不完整。此外,在背景区域,有多个不需要的轮廓。
    在这里插入图片描述
  • 当图像中物体的背景充满了线条时,轮廓检测也可能失败。

总结

您从轮廓检测开始,并学习在OpenCV中实现它。了解了应用程序如何使用轮廓来进行移动检测和分割。接下来,我们演示了四种不同的检索模式和两种轮廓近似方法的使用。你还学会了画轮廓。最后,我们讨论了轮廓层次结构,以及不同的轮廓检索模式对图像轮廓绘制的影响。
关键点:

  • OpenCV中的轮廓检测算法在图像有黑色背景和明确的感兴趣目标时工作得非常好。
  • 但是,当输入图像的背景是杂乱的,或者与感兴趣的目标像素强度相同时,算法的效果就不那么好了。

参考文件

https://learnopencv.com/contour-detection-using-opencv-python-c/

  • 8
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值