如何在opencv中合并轮廓?

7

大家好,我已经花了相当长的时间在这个项目上。我正在构建一个可以玩Chrome恐龙游戏的机器人。因此,我尝试了其他方法来检测字符,例如matchTemplate,甚至自己编写了算法来定位对象,但我最喜欢的是findcontours。

这是我的成果:

What my program sees

有谁能帮我找出如何合并仙人掌的两个矩形?

img = screen_cap()
roi = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(roi,127, 255, 0)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
first = True
for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > 200: #filtering contours
        x,y,w,h = cv2.boundingRect(cnt)
        if w/h < 4: # filtering even more
            cv2.rectangle(img,(x,y),(x+w,y+h),(255,0,0),2)

有什么阻止你检查边界框的像素位置是否靠近其他边界框的位置,如果是,则扩展最左边的边界框的宽度和高度呢? - alkasm
@AlexanderReynolds 他说合并轮廓,所以如果矩形偏移或有多个轮廓,我不确定他们是只想让边界框消失还是实际的轮廓线也要消失(这样它们将成为任意轴对齐的多边形)。 - Krupip
@AlexanderReynolds 很抱歉问题不够清晰。我想做的是让计算机将仙人掌视为一个单一实体,并在一个边界框内,必须是一个矩形。现在我清楚了吗?我也在尝试自己实现它。只是在这里问一下是否有本地opencv方法。 - rjpj1998
@snb 查找... - rjpj1998
我理解您想将它们合并为一个对象,但不清楚您希望该对象是什么。它应该是二进制图像吗?它应该是仅包含仙人掌像素位置而非周围或之间所有像素的集合吗?还是只需要一个围绕它们的矩形(即您只需要左上角点和宽度或高度)? - alkasm
显示剩余4条评论
3个回答

14
抱歉晚到了一点。不过如果我在谷歌上搜索“合并OpenCV轮廓”,我找到了这个,我想这里应该有一个答案。
你可以通过以下其中一种方法合并任意两个轮廓:
- 获取每个轮廓的点列表 - 将它们连接起来 - 强制将它们转换为cv2轮廓格式 - 如果你不太关心细节,可以对其进行cv2.convexHull处理
如果你不喜欢convexHull的结果,因为轮廓的凹部很重要,那么请按照以下方法操作:
- 获取每个轮廓的点列表 - 将它们连接起来 - 获取一个共同的中心点 - 按照顺时针顺序将所有点围绕中心点排序 - 强制将它们转换为cv2轮廓格式
如果两个轮廓中有很多凹形,这可能会导致一个锯齿状的模式,因为该方法会忽略它们的原始结构。如果是这种情况,你需要按照第三种方法操作:
  • 获取每个轮廓的点列表
  • 获取一个公共中心
  • 删除每个轮廓中位于另一个轮廓内部的点
  • 在每个轮廓中找到离公共中心最近的点
  • 按照列表中的顺序遍历第一个轮廓,直到遇到最近的点
  • 然后切换到另一个列表,从最近的点开始,顺时针遍历另一个轮廓直到用完
  • 切换回第一个轮廓,并添加剩余的点
  • 将它们强制转换为cv2轮廓格式

下一个更复杂的情况是,如果轮廓之间有多个交叉点,并且您想保留两者之间的空洞。那么最好创建一个黑色图像,并通过cv2.fillPoly()将轮廓绘制为白色;然后通过cv2.findContours()获取轮廓。

我在这里为前两个步骤画了一些示意图。

获取每个轮廓的点列表:

import cv2
list_of_pts = [] 
for ctr in ctrs_to_merge:
    list_of_pts += [pt[0] for pt in ctr]

顺时针排序点

我使用MSeifert的这篇非常棒的帖子中的函数来按顺时针顺序排序点。

class clockwise_angle_and_distance():
    '''
    A class to tell if point is clockwise from origin or not.
    This helps if one wants to use sorted() on a list of points.

    Parameters
    ----------
    point : ndarray or list, like [x, y]. The point "to where" we g0
    self.origin : ndarray or list, like [x, y]. The center around which we go
    refvec : ndarray or list, like [x, y]. The direction of reference

    use: 
        instantiate with an origin, then call the instance during sort
    reference: 
    https://dev59.com/11gR5IYBdhLWcg3wBZW3

    Returns
    -------
    angle
    
    distance
    

    '''
    def __init__(self, origin):
        self.origin = origin

    def __call__(self, point, refvec = [0, 1]):
        if self.origin is None:
            raise NameError("clockwise sorting needs an origin. Please set origin.")
        # Vector between point and the origin: v = p - o
        vector = [point[0]-self.origin[0], point[1]-self.origin[1]]
        # Length of vector: ||v||
        lenvector = np.linalg.norm(vector[0] - vector[1])
        # If length is zero there is no angle
        if lenvector == 0:
            return -pi, 0
        # Normalize vector: v/||v||
        normalized = [vector[0]/lenvector, vector[1]/lenvector]
        dotprod  = normalized[0]*refvec[0] + normalized[1]*refvec[1] # x1*x2 + y1*y2
        diffprod = refvec[1]*normalized[0] - refvec[0]*normalized[1] # x1*y2 - y1*x2
        angle = atan2(diffprod, dotprod)
        # Negative angles represent counter-clockwise angles so we need to 
        # subtract them from 2*pi (360 degrees)
        if angle < 0:
            return 2*pi+angle, lenvector
        # I return first the angle because that's the primary sorting criterium
        # but if two vectors have the same angle then the shorter distance 
        # should come first.
        return angle, lenvector

center_pt = np.array(list_of_pts).mean(axis = 0) # get origin
clock_ang_dist = clockwise_angle_and_distance(origin) # set origin
list_of_pts = sorted(list_of_pts, key=clock_ang_dist) # use to sort

将一个点列表强制转换为cv2格式。
import numpy as np
ctr = np.array(list_of_pts).reshape((-1,1,2)).astype(np.int32)

使用cv2.convexHull来合并它们。
如果你使用这个方法,就不需要按顺时针顺序排列点了。然而,由于它无法保留轮廓的凹角,所以可能会丢失一些轮廓的特性。
# get a list of points
# force the list of points into cv2 format and then
ctr = cv2.convexHull(ctr) # done.

我认为合并两个轮廓的功能应该是opencv库的内容。这个方法非常简单明了,可惜很多使用opencv的程序员都不得不自己编写这段代码。

2
谢谢您,Anderas,这非常有帮助! - fermi
这里有一个非常棒且非常紧凑的函数实现,可以按顺时针顺序排序点:https://dev59.com/Krbna4cB1Zd3GeqPiuRS#58874392 - Marcos

7
最简单的合并轮廓的方法是将它们堆叠在一起。
contours = np.vstack(contours)

在您的情况下(例如将轮廓5和6叠加):
contours = np.vstack([contours[5], contours[6]])

5

这似乎是一个老问题,看起来还没有得到正确的答案(尽管向那些在评论中部分回答的SOer表示歉意)。对我而言,这个问题似乎有两个部分:

  1. 是否有一个opencv函数可以合并两个矩形?

对于这个问题的答案是“是”和“否”。让我明确一下;如果您使用的是opencv C++绑定,则是。Simple()函数可用于取两个矩形的联合,|运算符可用于计算两个矩形的交集。但Python绑定缺少这些函数。

  1. How to then do it in Python?

    def union(a,b):
        x = min(a[0], b[0])
        y = min(a[1], b[1])
        w = max(a[0]+a[2], b[0]+b[2]) - x
        h = max(a[1]+a[3], b[1]+b[3]) - y
        return (x, y, w, h)
    
    def intersection(a,b):
        x = max(a[0], b[0])
        y = max(a[1], b[1])
        w = min(a[0]+a[2], b[0]+b[2]) - x
        h = min(a[1]+a[3], b[1]+b[3]) - y
        if w<0 or h<0: return () # or (0,0,0,0) ?
        return (x, y, w, h)
        # Please remember a and b are rects.
    

源代码来源:OpenCV矩形的并集和交集


如果有多个轮廓并且它们非常接近,有没有一种方法将它们合并成一个轮廓? - Abdelsalam Hamdi
@abss,您只需要迭代它们来合并它们。或者,您可以在找到轮廓之前对二进制图像本身进行形态学膨胀,具体取决于您的使用情况哪种更有效。 - Knight Forked
谢谢你的回答。问题是我想合并的轮廓非常接近,以至于它们看起来像一个轮廓。但是它们之间有一个非常小的间隙,并且周围有一些噪点,因此使用膨胀会将噪点包含在轮廓中。我已经尽可能地过滤了图像,但无法完全删除它们。 - Abdelsalam Hamdi
@abss,要处理你所拥有的图像,直到你将它们发布供他人查看,否则很难告诉你该怎么做。如果与噪声相比,前景轮廓较大,则可以使用cv2.ContourArea()函数过滤掉较小的轮廓,重新绘制前景轮廓,然后运行形态学膨胀。 - Knight Forked
是的,我明白了。我目前还在尝试进行一些筛选,如果我没有更多的想法,我会发布带有图片的问题。感谢您的回复。 - Abdelsalam Hamdi
@abss 通常的处理过程是使用形态学“闭运算”,该操作会将掩模膨胀以使它们合并,然后再侵蚀它们,使其收缩回到原始大小(但现在已经填充了孔洞/间隙)。 - alkasm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接