点击此处下载源代码:https://jbox.sjtu.edu.cn/l/85O9ur
本教程是我们关于形状检测和分析的三部分系列文章的第二篇。
上周我们学习了如何使用OpenCV计算轮廓的中心。
今天,我们将利用轮廓属性来实际标记和识别图像中的形状,就像本文顶部的图中一样。
在开始学习本教程之前,让我们快速回顾一下我们的项目结构:·
| --- pyimagesearch
| | --- __init__.py
| | --- shapedetector.py
| --- detect_shapes.py
| --- shapes_and_colors.png
如您所见,我们已经定义了一个pyimagesearch
模块。在这个模块中,我们有shapedetector.py
,它将存储我们ShapeDetector
类的实现。
最后,我们有detect_shapes.py
驱动程序脚本,我们将用它来从磁盘加载图像,分析它的形状,然后通过ShapeDetector
类执行形状检测和识别。
在我们开始之前,请确保您的系统上安装了imutils package,我们将在本教程后面使用一系列OpenCV便捷函数:
$ pip install imutils
构建形状检测器的第一步是编写一些代码来封装形状识别逻辑。
让我们继续定义我们的ShapeDetecto
r。打开shapedetector.py
文件并插入以下代码:
# import the necessary packages
import cv2
class ShapeDetector:
def __init__(self):
pass
def detect(self, c):
# initialize the shape name and approximate the contour
shape = "unidentified"
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.04 * peri, True)
第4行开始我们的ShapeDetector
类的定义。我们将在这里跳过__init__
构造函数,因为没有什么需要初始化。
然后我们在第8行有我们的检测方法,它只需要一个参数c
,即我们试图识别的形状的轮廓。
为了进行形状检测,我们将使用轮廓近似,顾名思义,轮廓近似是使用较少的点集去表示原曲线(由更多点组成)的一种算法。
该算法通常称为Ramer-Douglas-Peucker
算法,或简称为split-and-merge
算法。
轮廓近似是基于曲线可以通过一系列短线段近似的假设来预测的,因此得到的近似曲线所包含的点集是用于定义原始曲线的点集的子集。
实际上OpenCV
已经通过```cv2.approxPolyDP``方法实现了轮廓近似。
为了执行轮廓近似,我们首先计算轮廓的周长(第11行),然后构造实际的轮廓近似(第12行)。
cv2.approxPolyDP
的第二个参数的常用值通常在原始轮廓周长的1-5%范围内。
得到我们想要的近似轮廓后,我们可以继续进行形状检测:
# import the necessary packages
import cv2
class ShapeDetector:
def __init__(self):
pass
def detect(self, c):
# initialize the shape name and approximate the contour
shape = "unidentified"
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.04 * peri, True)
# if the shape is a triangle, it will have 3 vertices
if len(approx) == 3:
shape = "triangle"
# if the shape has 4 vertices, it is either a square or
# a rectangle
elif len(approx) == 4:
# compute the bounding box of the contour and use the
# bounding box to compute the aspect ratio
(x, y, w, h) = cv2.boundingRect(approx)
ar = w / float(h)
# a square will have an aspect ratio that is approximately
# equal to one, otherwise, the shape is a rectangle
shape = "square" if ar >= 0.95 and ar <= 1.05 else "rectangle"
# if the shape is a pentagon, it will have 5 vertices
elif len(approx) == 5:
shape = "pentagon"
# otherwise, we assume the shape is a circle
else:
shape = "circle"
# return the name of the shape
return shape
轮廓由顶点列表组成。我们可以检查此列表中的顶点数目以确定对象的形状。
例如,如果近似轮廓有三个顶点,则它一定是三角形(第15和16行)。
如果轮廓有四个顶点,则它一定是正方形或矩形(第20行)。为了确定具体是哪个,我们计算形状的纵横比,这就是轮廓边界框的宽度除以高度(第23和24行)。如果纵横比是~1.0,那么我们正在检测的图形是一个正方形(因为所有边的长度大致相等)。否则,形状是矩形。
如果轮廓有五个顶点,我们可以将其标记为五边形(第31行和第32行)。
否则,通过排除过程(当然,仅限于在这个例子的上下文中),我们可以假设我们正在检查的形状是一个圆(第35和36行)。
最后,我们将识别的形状返回给调用方法。
现在已经定义了我们的ShapeDetector
类,让我们创建detect_shapes.py
驱动程序脚本:
# import the necessary packages
from pyimagesearch.shapedetector import ShapeDetector
import argparse
import imutils
import cv2
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path to the input image")
args = vars(ap.parse_args())
# import the necessary packages
from pyimagesearch.shapedetector import ShapeDetector
import argparse
import imutils
import cv2
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path to the input image")
args = vars(ap.parse_args())
# load the image and resize it to a smaller factor so that
# the shapes can be approximated better
image = cv2.imread(args["image"])
resized = imutils.resize(image, width=300)
ratio = image.shape[0] / float(resized.shape[0])
# convert the resized image to grayscale, blur it slightly,
# and threshold it
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.threshold(blurred, 60, 255, cv2.THRESH_BINARY)[1]
# find contours in the thresholded image and initialize the
# shape detector
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
sd = ShapeDetector()
我们从第2-5行开始,导入我们所需的包。注意我们如何从pyimagesearch
的shapedetector
子模块导入ShapeDetector
类的实现。
第8-11行处理解析我们的命令行参数。我们这里只需要一个开关, -- image
,这是我们想要处理的图像保存在磁盘上的路径。
接下来,让我们预处理我们的图像。
首先,我们从第15行的磁盘加载图像并在第16行调整大小。然后我们跟踪第17行的旧高度与新调整大小的高度的比率 ——我们将在后面教程里解释我们为什么这样做。
第21-23行处理将调整大小的图像转换为灰度,平滑它以减少高频噪声,最后对其进行阈值处理以显示图像中的形状。
阈值处理后,我们的图像应如下所示:
注意我们的图像是如何被二值化的 —— 形状在黑色背景下显示为白色前景。
最后,我们在二进制图像中找到轮廓,根据我们的OpenCV
版本处理从cv2.findContours
获取正确的元组值,最后初始化ShapeDetector
(第27-30行)。
最后一步是识别每个轮廓:
# import the necessary packages
from pyimagesearch.shapedetector import ShapeDetector
import argparse
import imutils
import cv2
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
help="path to the input image")
args = vars(ap.parse_args())
# load the image and resize it to a smaller factor so that
# the shapes can be approximated better
image = cv2.imread(args["image"])
resized = imutils.resize(image, width=300)
ratio = image.shape[0] / float(resized.shape[0])
# convert the resized image to grayscale, blur it slightly,
# and threshold it
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.threshold(blurred, 60, 255, cv2.THRESH_BINARY)[1]
# find contours in the thresholded image and initialize the
# shape detector
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
sd = ShapeDetector()
# loop over the contours
for c in cnts:
# compute the center of the contour, then detect the name of the
# shape using only the contour
M = cv2.moments(c)
cX = int((M["m10"] / M["m00"]) * ratio)
cY = int((M["m01"] / M["m00"]) * ratio)
shape = sd.detect(c)
# multiply the contour (x, y)-coordinates by the resize ratio,
# then draw the contours and the name of the shape on the image
c = c.astype("float")
c *= ratio
c = c.astype("int")
cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
cv2.putText(image, shape, (cX, cY), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (255, 255, 255), 2)
# show the output image
cv2.imshow("Image", image)
cv2.waitKey(0)
在第33行,我们开始在每个轮廓上循环。对于它们中的每一个,我们计算轮廓的中心,然后执行形状检测和标记。
由于我们正在处理从调整大小的图像(而不是原始图像)中提取的轮廓,我们需要将轮廓和中心(x,y)坐标乘以我们的调整大小比率(第43-45行)。这将为我们提供原始图像的轮廓和质心的正确(x,y)坐标。
最后,我们在图像上绘制轮廓和标记的形状(第44-48行),然后显示我们的结果(第51和52行)。
要查看我们的形状检测器,请执行以下命令:
$ python detect_shapes.py --image shapes_and_colors.png
从上面的动画中可以看出,我们的脚本分别遍历每个形状,对每个形状执行形状检测,然后在对象上绘制形状的名称。
在今天的博客中,我们学习了如何使用OpenCV
和Python
执行形状检测。
为实现这一目标,我们利用轮廓近似,即将曲线上的点数减少到更简单的近似版本。
然后,基于该轮廓近似,我们检查了每个形状具有的顶点数。给定顶点计数,我们能够准确地标记每个形状。
本课程是关于形状检测和分析的三部分系列的一部分。上周我们介绍了如何计算轮廓的中心。今天我们介绍了OpenCV
的形状检测。下周我们将讨论如何使用颜色通道统计信息标记形状的实际颜色。