Practical Python and OpenCV,3rd Edition 14
Contours
OpenCV提供了在图像中查找“曲线”的方法,称为轮廓。轮廓是点的曲线,曲线中没有间隙。轮廓对形状近似和分析等方面非常有用。
为了在图像中找到轮廓,您需要首先使用边缘检测方法或阈值处理来获得图像的二值化。在下面的例子中,我们将使用Canny边缘检测器找到硬币的轮廓,然后找到硬币的实际轮廓。
新建一个counting_coins.py文件
from __future__ import print_function
import numpy as np
import argparse
import cv2
# Construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required = True,
help = "Path to the image")
args = vars(ap.parse_args())
# Load the image, convert it to grayscale, and blur it slightly
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
cv2.imshow("Image", image)
# The first thing we are going to do is apply edge detection to
# the image to reveal the outlines of the coins
edged = cv2.Canny(blurred, 30, 150)
cv2.imshow("Edges", edged)
正如前一章讨论的边缘检测方法一样,我们将把图像转换为灰度,然后应用高斯模糊,使边缘检测器更容易找到硬币的轮廓。我们这次使用了更大的模糊大小,σ=11.
然后,我们通过应用Canny边缘检测器来获得边缘图像。再次,正如在先前的边缘检测示例中,任何低于30的梯度值被认为是非边缘,而高于150的任何值被认为是确定的边缘。

# Find contours in the edged image.
# NOTE: The cv2.findContours method is DESTRUCTIVE to the image
# you pass in. If you intend on reusing your edged image, be
# sure to copy it before calling cv2.findContours
(_,cnts,_) = cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# How many contours did we find?
print("I count {} coins in this image".format(len(cnts)))
# Let's highlight the coins in the original image by drawing a
# green circle around them
coins = image.copy()
cv2.drawContours(coins,cnts,-1,(0,255,0),2)
cv2.imshow("Coins",coins)
cv2.waitKey(0)

现在我们有了硬币的轮廓,我们可以找到the contours of the outlines。我们使用cv2.findContours函数来实现。此方法返回一个3元组:(1)应用轮廓检测后的图像(经过修改并基本上被破坏),(2)轮廓本身,cnts以及(3)轮廓的层次结构(见下文)。
Note:
The return tuple of cv2. findContours has changed in OpenCV 3.0. Originally in OpenCV 2.4.X, this tuple was only a 2-tuple, consisting of just the contours themselves and the associated hierarchy. However, in OpenCV 3.0, we have a third value added to the return tuple: the image itself after applying the contour detection algorithm. This is a small, minor change (and one that I’m personally not crazy about since it breaks backwards compatibility with so many scripts), but something that can definitely trip you up when working with both OpenCV 2.4.X and OpenCV 3.0. Be sure to take special care when using the cv2. findContours function if you intend for your code to be cross-version portable.
cv2.findContours的第一个参数是我们的边缘图像。重要的是要注意,此函数对您传入的图像具有破坏性。如果您打算稍后在代码中使用该图像,最好使用NumPy复制方法复制它。
第二个参数是我们想要的轮廓类型。我们使用cv2.RETR_EXTERNAL来仅检索最外面的轮廓(即,跟随硬币轮廓的轮廓)。我们也可以通过cv2.RETR_LIST来获取所有轮廓。其他方法包括使用cv2.RETR_COMP和cv2.RETR_TREE的层次轮廓,但层次结构轮廓不在本文的范围。
我们的最后一个参数是我们想要近似轮廓。我们使用cv2.CHAIN_APPROX_SIMPLE仅将水平,垂直和对角线段压缩到其端点。 这节省了计算和内存。如果我们想要轮廓上的所有点,没有压缩,我们可以传入cv2.CHAIN_APPROX_NONE; 但是,使用此功能时要非常谨慎。 沿轮廓检索所有点通常是不必要的并且浪费资源。
我们的轮廓cnts只是一个Python列表。我们可以使用它上面的len函数来计算返回的轮廓数。
当我们执行我们的脚本时,我们将输出“我在此图像中计算9个硬币”打印到我们的控制台
F:\20181116\Practical Python and OpenCV, 3rd Edition>python counting_coins.py -i coins.png
I count 9 coins in this image
现在,我们可以绘制轮廓。为了不在我们的原始图像上绘制,我们制作原始图像的副本,赋值给coins。
对cv2.drawContours的调用会在我们的图像上绘制实际轮廓。 函数的第一个参数是我们想要绘制的图像。第二个是我们的轮廓列表。 接下来,我们有轮廓指数。通过指定负值-1,我们指示我们要绘制所有轮廓。但是,我们也会提供一个索引i,这将是cnts中的第i个轮廓。这将允许我们只绘制一个轮廓而不是所有轮廓。
例如,以下是分别绘制第一,第二和第三轮廓的一些代码:
cv2.drawContours(coins, cnts, 0, (0, 255, 0), 2)
cv2.drawContours(coins, cnts, 1, (0, 255, 0), 2)
cv2.drawContours(coins, cnts, 2, (0, 255, 0), 2)
cv2.drawContours函数的第四个参数是我们要绘制的线的颜色。在这里,我们使用绿色。
最后,我们的最后一个参数是我们绘制的线的粗细。我们将绘制厚度为两个像素的轮廓。
让我们从图像中裁剪每个硬币:
# Now, let's loop over each contour
for (i,c) in enumerate(cnts):
# We can compute the 'bounding box' for each contour, which is
# the rectangle that encloses the contour
(x,y,w,h) = cv2.boundingRect(c)
# Now that we have the contour, let's extract it using array
# slices
print("Coin #{}".format(i+1))
coin = image[y:y + h,x:x + w]
cv2.imshow("Coin",coin)
# Just for fun, let's construct a mask for the coin by finding
# The minumum enclosing circle of the contour
mask = np.zeros(image.shape[:2],dtype="uint8")
((centerX,centerY),radius) = cv2.minEnclosingCircle(c)
cv2.circle(mask,(int(centerX),int(centerY)),int(radius),255,-1)
mask = mask[y:y + h,x:x + w]
cv2.imshow("Masked Coin",cv2.bitwise_and(coin,coin,mask=mask))
cv2.waitKey(0)
然后,我们在当前轮廓上使用cv2.boundingRect函数。此方法找到我们的轮廓适合的“封闭框”,允许我们从图像中裁剪它。该函数采用单个参数,一个轮廓,然后返回矩形开始的x和y位置的元组,后跟矩形的宽度和高度。
然后我们使用我们的边界框坐标和NumPy阵列切片从图像中裁剪硬币。
如果我们能找到轮廓的边界框,为什么不在轮廓上加一个圆?毕竟,硬币是圆圈。
我们首先将mask初始化为NumPy零数组,其原始图像的宽度和高度相同。
我们调用cv2.minEnclosingCircle方法来fit我们轮廓的圆。 我们传入一个圆形变量,即当前轮廓,并给出圆的x和y坐标及其半径。
使用(x,y)坐标和半径,我们可以在我们的mask上画一个圆圈,代表硬币。
然后我们将与裁剪硬币完全相同的方式裁剪mask。
为了仅显示硬币的前景并忽略背景,我们使用硬币图像和硬币的mask来调用我们可靠的按位AND函数。

用到的函数
cv2.findContours
np.uint8
cv2.drawContours
cv2.Canny
更多的参考:
Target acquired: Finding targets in drone and quadcopter video streams using Python and OpenCV
Measuring size of objects in an image with OpenCV
Detecting machine-readable zones in passport images
Detecting Barcodes in Images with Python and OpenCV
How to Build a Kick-Ass Mobile Document Scanner in Just 5 Minutes
;