OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标

1.使用 Python 和 OpenCV 顺时针排序坐标

这篇博文的目标有两个:

  • 1、主要目的是学习如何按照左上、右上、右下和左下的顺序排列与旋转边界框相关联的(x, y)坐标。按照这样的顺序组织边界框坐标是执行透视转换或匹配对象角点(例如计算对象之间的距离)等操作的先决条件。
  • 2、第二个目的是解决 imutils 包的 order_points 方法中一个微妙的、难以发现的错误。

话虽如此,让我们通过回顾按顺时针顺序排列边界框坐标的原始的、有缺陷的方法来开始这篇博文。

2.原始(有缺陷的)方法

在我们学习如何按(1)顺时针顺序(2)左上、右上、右下和左下顺序排列一组边界框坐标之前,我们应该首先回顾一下最初的4点getPerspectiveTransform博客文章中详细介绍的order_points方法。

我已将(有缺陷的)order_points 方法重命名为 order_points_old,以便我们可以比较原始方法和更新后的方法。首先,打开一个新文件并将其命名为 order_coordinates.py

# 导入包
from __future__ import print_function
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2
def order_points_old(pts):
	# 初始化将被排序的坐标列表,这样,列表中的第一个条目是左上,第二个条目是右上,第三个条目是右下,第四个条目是左下
	rect = np.zeros((4, 2), dtype="float32")
	# 左上点的总和最小,而右下点的总和最大
	s = pts.sum(axis=1)
	rect[0] = pts[np.argmin(s)]
	rect[2] = pts[np.argmax(s)]
	# 现在,计算点之间的差值,右上角的差值最小,而左下角的差值最大
	diff = np.diff(pts, axis=1)
	rect[1] = pts[np.argmin(diff)]
	rect[3] = pts[np.argmax(diff)]
	# 返回有序坐标
	return rect

导入本示例所需的 Python 包。我们将在本博文稍后使用 imutils 包。
定义我们的 order_points_old 函数。该方法只需要一个参数,即我们将按左上、右上、右下和左下顺序排列的一组点;虽然,正如我们将看到的,这种方法有一些缺陷。
我们定义一个形状为 (4, 2) 的 NumPy 数组,该数组将用于存储我们的四个 (x, y) 坐标集。
给定这些 pts ,我们将 x 和 y 值加在一起,然后找到最小和最大的和。这些值分别为我们提供了左上角和右下角坐标。
然后我们取 x 和 y 值之间的差异,其中右上角的差异最小,左下角的差异最大。
最后,将有序的 (x, y) 坐标返回给调用函数。

说了这么多,你能发现我们逻辑中的缺陷吗?
当两点的和或差相同时会发生什么?如果数组 和 或数组 diff 有相同的值,我们有选择不正确索引的风险,这会对我们的排序产生严重影响。

选择错误的索引意味着我们从 pts 列表中选择了错误的点。如果我们从 pts 中取错误的点,那么我们顺时针的左上、右上、右下、左下的顺序将被破坏。

为了解决这个问题,我们需要使用更可靠的数学原理设计一个更好的 order_points 函数。这正是我们将在下一节中介绍的内容。

3.使用 OpenCV 和 Python 顺时针排序坐标的更好方法

现在我们已经查看了 order_points 函数的一个有缺陷的版本,让我们回顾一下更新的、正确的实现。

# import the necessary packages
from scipy.spatial import distance as dist
import numpy as np
import cv2
def order_points(pts):
	# 根据点的 x 坐标对点进行排序
	xSorted = pts[np.argsort(pts[:, 0]), :]
	# 从根据点的 x 坐标排序的坐标点中获取最左和最右的点
	leftMost = xSorted[:2, :]
	rightMost = xSorted[2:, :]
	# 现在,根据y坐标对最左边的坐标排序,这样我们就可以分别获取左上角和左下角的点
	leftMost = leftMost[np.argsort(leftMost[:, 1]), :]
	(tl, bl) = leftMost
	# 现在我们有了左上角的坐标,用它作为锚点来计算左上角和右下角点之间的欧氏距离;根据勾股定理,距离最大的点就是右下点
	D = dist.cdist(tl[np.newaxis], rightMost, "euclidean")[0]
	(br, tr) = rightMost[np.argsort(D)[::-1], :]
	# 按左上、右上、右下和左下顺序返回坐标
	return np.array([tl, tr, br, bl], dtype="float32")

同样,导入所需的 Python 包。然后定义我们的 order_points 函数,它只需要一个参数——我们想要排序的点列表。
然后根据它们的 x 值对这些点进行排序。给定已排序的 xSorted 列表,我们应用数组切片来获取最左边的两个点和最右边的两个点。
因此,最左边的点将对应于左上角和左下角的点,而最右边的点将是我们的右上角和右下角的点——诀窍是弄清楚哪个是哪个。
幸运的是,这并不太具有挑战性。 如果我们根据 y 值对 leftMost 点进行排序,我们可以分别推导出左上角和左下角的点。
然后,为了确定右下角和左下角的点,我们可以应用一些几何知识。
使用左上角的点作为锚点,我们可以应用勾股定理并计算左上角和rightMost之间的欧几里得距离。根据三角形的定义,斜边将是直角三角形的最大边。
因此,通过将左上角点作为我们的锚点,右下角点将具有最大的欧几里德距离,从而允许我们提取右下角和右上角的点。
最后,返回一个 NumPy 数组,表示我们按左上、右上、右下和左下顺序排列的有序坐标。

4.实现我们的坐标排序

现在我们有了 order_points 的原始版本和更新版本,让我们继续实现我们的 order_coordinates.py 脚本并尝试一下:

# 导入包
from __future__ import print_function
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2
def order_points_old(pts):
	# 初始化将被排序的坐标列表,这样,列表中的第一个条目是左上,第二个条目是右上,第三个条目是右下,第四个条目是左下
	rect = np.zeros((4, 2), dtype="float32")
	# 左上点的总和最小,而右下点的总和最大
	s = pts.sum(axis=1)
	rect[0] = pts[np.argmin(s)]
	rect[2] = pts[np.argmax(s)]
	# 现在,计算点之间的差值,右上角的差值最小,而左下角的差值最大
	diff = np.diff(pts, axis=1)
	rect[1] = pts[np.argmin(diff)]
	rect[3] = pts[np.argmax(diff)]
	# 返回有序坐标
	return rect
def order_points(pts):
	# 根据点的 x 坐标对点进行排序
	xSorted = pts[np.argsort(pts[:, 0]), :]
	# 从根据点的 x 坐标排序的坐标点中获取最左和最右的点
	leftMost = xSorted[:2, :]
	rightMost = xSorted[2:, :]
	# 现在,根据y坐标对最左边的坐标排序,这样我们就可以分别获取左上角和左下角的点
	leftMost = leftMost[np.argsort(leftMost[:, 1]), :]
	(tl, bl) = leftMost
	# 现在我们有了左上角的坐标,用它作为锚点来计算左上角和右下角点之间的欧氏距离;根据勾股定理,距离最大的点就是右下点
	D = dist.cdist(tl[np.newaxis], rightMost, "euclidean")[0]
	(br, tr) = rightMost[np.argsort(D)[::-1], :]
	# 按左上、右上、右下和左下顺序返回坐标
	return np.array([tl, tr, br, bl], dtype="float32")
# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--new", type=int, default=-1,
	help="whether or not the new order points should should be used")
args = vars(ap.parse_args())
# 加载我们的输入图像,将其转换为灰度,并稍微模糊它
image = cv2.imread("example.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
# 执行边缘检测,然后执行膨胀+腐蚀以缩小对象边缘之间的间隙
edged = cv2.Canny(gray, 50, 100)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)

解析我们的命令行参数。我们只需要一个参数 --new ,它用于指示是否应该使用新的或原始的 order_points 函数。我们将默认使用原始实现。 从那里,我们从磁盘加载 example.png 并通过将图像转换为灰度并使用高斯滤波器对其进行平滑来执行一些预处理。 我们通过应用 Canny 边缘检测器继续处理我们的图像,然后进行膨胀 + 腐蚀以缩小边缘图中轮廓之间的任何间隙。 执行完边缘检测过程后,我们的图像应该是这样的:
OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标_第1张图片
计 算 输 入 图 像 的 边 缘 图 计算输入图像的边缘图
如您所见,我们已经能够确定图像中对象的轮廓。 现在我们有了边缘图的轮廓,我们可以应用 cv2.findContours 函数来提取对象的轮廓:

# 在边缘图中找到轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
	cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# 从左到右对轮廓进行排序并初始化边界框点颜色
(cnts, _) = contours.sort_contours(cnts)
colors = ((0, 0, 255), (240, 0, 159), (255, 0, 0), (255, 255, 0))

然后我们从左到右对对象轮廓进行排序,这不是必需的,但可以更轻松地查看脚本的输出。下一步是单独循环每个轮廓:

# 分别在轮廓上循环
for (i, c) in enumerate(cnts):
	# 如果轮廓不够大,则忽略它
	if cv2.contourArea(c) < 100:
		continue
	# 计算轮廓的旋转边界框,然后绘制轮廓
	box = cv2.minAreaRect(c)
	box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
	box = np.array(box, dtype="int")
	cv2.drawContours(image, [box], -1, (0, 255, 0), 2)
	# 显示原始坐标
	print("Object #{}:".format(i + 1))
	print(box)

开始在我们的轮廓上循环。如果轮廓不够大(由于边缘检测过程中的“噪声”),我们丢弃轮廓区域。否则,计算轮廓的旋转边界框(注意使用 cv2.cv.BoxPoints [如果我们使用 OpenCV 2.4] 或 cv2.boxPoints [如果我们使用 OpenCV 3])并在图像上绘制轮廓。
我们还将打印原始旋转边界框,以便我们可以在对坐标进行排序后比较结果。
我们现在准备按顺时针排列排列边界框坐标:

# 对轮廓中的点进行排序,使它们以左上、右上、右下和左下的顺序出现,然后绘制旋转边界框的轮廓
rect = order_points_old(box)
# 检查是否应使用新方法对坐标进行排序
if args["new"] > 0:
	rect = perspective.order_points(box)
# 显示重新排序的坐标
print(rect.astype("int"))
print("")

应用原始(即有缺陷的)order_points_old 函数以左上、右上、右下和左下的顺序排列我们的边界框坐标。
如果 --new 1 标志已传递给我们的脚本,那么我们将应用我们更新的 order_points 函数。
就像我们将原始边界框打印到控制台一样,我们还将打印有序点,以确保我们的功能正常工作。
最后,我们可以可视化我们的结果:

# 遍历原始点并绘制它们
for ((x, y), color) in zip(rect, colors):
	cv2.circle(image, (int(x), int(y)), 5, color, -1)
# 在左上角绘制对象编号
cv2.putText(image, "Object #{}".format(i + 1),
(int(rect[0][0] - 15), int(rect[0][1] - 15)),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 2)
# 显示
cv2.imshow("Image", image)
cv2.waitKey(0)

我们开始遍历我们(希望如此)有序的坐标并将它们绘制在我们的图像上。 根据颜色列表,左上角应该是红色,右上角应该是紫色,右下角应该是蓝色,最后是左下角应该是蓝绿色。 最后,在我们的图像上绘制对象编号并显示输出结果。
OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标_第2张图片

OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标_第3张图片
正如我们所看到的,我们的输出是按顺时针方向按左上角、右上角、右下角和左下角排列的点——除了对象 #6! 注意:看看输出圆圈——注意怎么没有蓝色圆圈? 查看对象 #6 的终端输出,我们可以看到原因:
OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标_第4张图片
我们得到和:
520 + 255 = 775
491 + 226 = 717
520 + 197 = 717
549 + 226 = 775
而我们得到差:
520 – 255 = 265
491 – 226 = 265
520 – 197 = 323
549 – 226 = 323
如您所见,我们最终得到了重复的值!
并且由于存在重复值,argmin()argmax() 函数无法像我们期望的那样工作,从而为我们提供了一组不正确的“有序”坐标。
为了解决这个问题,我们可以在 imutils 包中使用我们更新的 order_points 函数。
OpenCV基础(26)使用 Python 和 OpenCV 顺时针排序坐标_第5张图片

5.完整代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/10/17 0:20
# @File    : order_coordinates.py.py
# @Software: PyCharm
# 导入包
from __future__ import print_function
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2


def order_points_old(pts):
    # 初始化将被排序的坐标列表,这样,列表中的第一个条目是左上,第二个条目是右上,第三个条目是右下,第四个条目是左下
    rect = np.zeros((4, 2), dtype="float32")
    # 左上点的总和最小,而右下点的总和最大
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    # 现在,计算点之间的差值,右上角的差值最小,而左下角的差值最大
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    # 返回有序坐标
    return rect


# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--new", type=int, default=1,
                help="whether or not the new order points should should be used")
args = vars(ap.parse_args())
# 加载我们的输入图像,将其转换为灰度,并稍微模糊它
image = cv2.imread("example_01.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
# 执行边缘检测,然后执行膨胀+腐蚀以缩小对象边缘之间的间隙
edged = cv2.Canny(gray, 50, 100)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)
# 在边缘图中找到轮廓
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
                        cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# 从左到右对轮廓进行排序并初始化边界框点颜色
(cnts, _) = contours.sort_contours(cnts)
colors = ((0, 0, 255), (240, 0, 159), (255, 0, 0), (255, 255, 0))
# 分别在轮廓上循环
for (i, c) in enumerate(cnts):
    # 如果轮廓不够大,则忽略它
    if cv2.contourArea(c) < 100:
        continue
    # 计算轮廓的旋转边界框,然后绘制轮廓
    box = cv2.minAreaRect(c)
    box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
    box = np.array(box, dtype="int")
    cv2.drawContours(image, [box], -1, (0, 255, 0), 2)
    # 显示原始坐标
    print("Object #{}:".format(i + 1))
    print(box)

    # 对轮廓中的点进行排序,使它们以左上、右上、右下和左下的顺序出现,然后绘制旋转边界框的轮廓
    rect = order_points_old(box)
    # 检查是否应使用新方法对坐标进行排序
    if args["new"] > 0:
        rect = perspective.order_points(box)
    # 显示重新排序的坐标
    print(rect.astype("int"))
    print("")
    # 遍历原始点并绘制它们
    for ((x, y), color) in zip(rect, colors):
        cv2.circle(image, (int(x), int(y)), 5, color, -1)
    # 在左上角绘制对象编号
    cv2.putText(image, "Object #{}".format(i + 1),
                (int(rect[0][0] - 15), int(rect[0][1] - 15)),
                cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 2)
    # 显示
    cv2.imshow("Image", image)
    cv2.waitKey(0)

总结

我们已经在之前的博文中实现了对与每个对象的旋转边界框相关联的 4 个点进行排序功能。 然而,正如我们发现的,这个实现有一个致命的缺陷——它可以在非常特殊的情况下返回错误的坐标。 为了解决这个问题,我们定义了一个新的、更新的 order_points 函数并将它放在 imutils 包中。此实现可确保我们的点始终正确排序。 现在我们可以以可靠的方式对 (x, y) 坐标进行排序,我们可以继续测量图像中对象的大小,这正是我将在以后博文中讨论的内容。

参考目录

https://www.pyimagesearch.com/2016/03/21/ordering-coordinates-clockwise-with-python-and-opencv/?_ga=2.125978829.13056945.1634300872-1609619485.1590463372

你可能感兴趣的:(OpenCV,opencv,python)