【问题标题】:How to use Opencv contours to describe line points in a unidirectional way如何使用Opencv轮廓单向描述线点
【发布时间】:2020-07-06 11:02:03
【问题描述】:

我正在使用 opencvs findContour 来查找点来描述由线(不是多边形)组成的图像,如下所示: cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

【问题讨论】:

  • 轮廓是闭合的形状。你不会从findContours 中得到线路。
  • 有没有办法删除这些点?我做的一件事就是去掉轮廓的后半部分,这似乎没问题。我也在想 approxPolyDP 可能会有所帮助。除了轮廓之外还有什么可以做我想要的吗?
  • 你能提供示例图片吗?
  • 我附上了一个可能的样本输入。它可能不那么复杂
  • 由于@alkasm 提到的原因,我认为您不能使用轮廓。你的最终目标是什么?我们也许可以帮助找到另一种方法。如果您可以发布一个您期望输出的示例,这也会有所帮助。

标签: c++ opencv hierarchy contour


【解决方案1】:

如果我理解正确,"cv2.connectedComponents" 方法可以满足您的需求。它为图像中的每个点分配一个标签,如果点连接,标签是相同的。通过执行此分配,不会发生重复。因此,如果您的线条是 1 个像素宽(例如边缘检测器或细化算子的输出),则每个位置都会得到一个点。

编辑:

根据 OP 要求,行应为 1 像素宽。为了实现这一点,在找到连接的组件之前应用了细化操作。还添加了步骤图像。

请注意,每个连接的组件点按 y 线的升序排序。

img_path = "D:/_temp/fig.png"
output_dir = 'D:/_temp/'

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

_, img = cv2.threshold(img, 128, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)

total_white_pixels = cv2.countNonZero(img)
print ("Total White Pixels Before Thinning = ", total_white_pixels)

cv2.imwrite(output_dir + '1-thresholded.png', img)

#apply thinning -> each line is one-pixel wide
img = cv2.ximgproc.thinning(img)
cv2.imwrite(output_dir + '2-thinned.png', img)

total_white_pixels = cv2.countNonZero(img)
print ("Total White Pixels After Thinning = ", total_white_pixels)

no_ccs, labels = cv2.connectedComponents(img)

label_pnts_dic = {}

colored = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

i = 1 # skip label 0 as it corresponds to the backgground points
sum_of_cc_points = 0 
while i < no_ccs:
    label_pnts_dic[i] = np.where(labels == i) #where return tuple(list of x cords, list of y cords)
    colored[label_pnts_dic[i]] = (random.randint(100, 255), random.randint(100, 255), random.randint(100, 255))
    i +=1

cv2.imwrite(output_dir + '3-colored.png', colored)    


print ("First ten points of label-1 cc: ")
for i in range(10):
    print ("x: ", label_pnts_dic[1][1][i], "y: ", label_pnts_dic[1][0][i])

输出:

Total White Pixels Before Thinning =  6814
Total White Pixels After Thinning =  2065
First ten points of label-1 cc: 
x:  312 y:  104
x:  313 y:  104
x:  314 y:  104
x:  315 y:  104
x:  316 y:  104
x:  317 y:  104
x:  318 y:  104
x:  319 y:  104
x:  320 y:  104
x:  321 y:  104

图片:

1.阈值

  1. 变薄

  1. 彩色组件

编辑2:

在与 OP 讨论后,我了解到拥有(分散的)点列表是不够的。应对点进行排序,以便对其进行追踪。为了实现这一点,应该在对图像应用细化后引入新的逻辑。

  1. 查找极值点(具有单个 8 连通性邻居的点)
  2. 查找连接器点(具有 3 路连接性的点)
  3. 查找简单点(所有其他点)
  4. 从一个极值点开始跟踪,直到到达另一个极值点或连接器。
  5. 提取行进路径。
  6. 检查连接器点是否已变成简单点并更新其状态。
  7. 重复
  8. 检查是否存在从任何极值点未到达的简单点的闭环,提取每个闭环作为附加路点。

极值点/连接点/简单点分类代码

def filter_neighbors(ns):    
    i = 0
    while i < len(ns):
        j = i + 1
        while j < len(ns):
            if (ns[i][0] == ns[j][0] and abs(ns[i][1] - ns[j][1]) <= 1) or (ns[i][1] == ns[j][1] and abs(ns[i][0] - ns[j][0]) <= 1):
                del ns[j]
                break                                    
            j += 1
        i += 1    

def sort_points_types(pnts):
    extremes = []
    connections = []
    simple = []

    for i in range(pnts.shape[0]):
        neighbors = []
        for j in range (pnts.shape[0]):
            if i == j: continue
            if abs(pnts[i, 0] - pnts[j, 0]) <= 1 and abs(pnts[i, 1] - pnts[j, 1]) <= 1:#8-connectivity check
                neighbors.append(pnts[j])
        filter_neighbors(neighbors)
        if len(neighbors) == 1:
            extremes.append(pnts[i])
        elif len(neighbors) == 2:
            simple.append(pnts[i])
        elif len(neighbors) > 2:
            connections.append(pnts[i])
    return extremes, connections, simple


img_path = "D:/_temp/fig.png"
output_dir = 'D:/_temp/'

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

_, img = cv2.threshold(img, 128, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY_INV)
img = cv2.ximgproc.thinning(img)

pnts = cv2.findNonZero(img)
pnts = np.squeeze(pnts)


ext, conn, simple = sort_points_types(pnts)

for p in conn:
    cv2.circle(img, (p[0], p[1]), 5, 128)

for p in ext:
    cv2.circle(img, (p[0], p[1]), 5, 128)

cv2.imwrite(output_dir + "6-both.png", img)

print (len(ext), len(conn), len(simple))

编辑3:

感谢eldesgraciado

注意:在调用此方法之前,应为图像填充一个像素,以避免边界检查或在边界处产生等效的黑色像素。

def sort_points_types(pnts, img):
    extremes = []
    connections = []
    simple = []

    for p in pnts:
        x = p[0]
        y = p[1]
        n = []
        if img[y - 1,x] > 0: n.append((y-1, x))
        if img[y - 1,x - 1] > 0: n.append((y-1, x - 1))
        if img[y - 1,x + 1] > 0: n.append((y-1, x + 1))
        if img[y,x - 1] > 0: n.append((y, x - 1))
        if img[y,x + 1] > 0: n.append((y, x + 1))
        if img[y + 1,x] > 0: n.append((y+1, x))
        if img[y + 1,x - 1] > 0: n.append((y+1, x - 1))
        if img[y + 1,x + 1] > 0: n.append((y+1, x + 1))
        filter_neighbors(n)
        if len(n) == 1:
            extremes.append(p)
        elif len(n) == 2:
            simple.append(p)
        elif len(n) > 2:
            connections.append(p)
    return extremes, connections, simple

显示极值点和连接点的图像:

【讨论】:

  • 问题是我的线条不是 1 像素厚。为了更好地连接,我使用了扩张。所以我认为这会给太多分。另外,对于我上面展示的示例,这不会只给出一行吗?我宁愿有几个。并且每一行应该是一个有序的点序列
  • 对于上面的例子,它给出了三个组件。 1. 背部。 2. 脸部和左手拇指。 3.手的外侧。所以我想这种方法在这方面应该很好。在 OpenCV 贡献中,有一个实现的细化/骨架算法(使线宽 1 点)我将添加一个编辑,包括细化代码和输出(每行应该有更少的点)。请看一下。
  • @Baraa 非常好的解决方案,我的朋友。这只是一个查找连接点的想法 - Hit-or-Miss 操作 来查找终点/起点怎么样? OP 的轮廓为 1 像素宽,因此所需的内核应该足够简单。我自己没有测试过,但它可以产生替代/补充解决方案。不过,到目前为止,工作非常酷!
  • @Baraa 啊!我知道了。可悲的是,我不能说哪种方法更快。使用命中/未命中方法,需要多次通过图像。但是,这是另一个想法:您的方法涉及应用 3x3 的互补内核来确定一个点是否是“目标”点。可以做的是获取每个内核“像素”的逻辑表达式 - 您检查内核,确定在哪些情况下,例如,pixel(0,0) 必须返回 true。您将为每个像素获得一组布尔函数。您一次评估一组“逻辑规则”并确定哪个输入像素是目标点。
  • @eldesgraciado 这是个好主意。我编辑了代码。现在速度更快了
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-04
  • 2021-09-07
  • 2021-07-26
  • 1970-01-01
  • 2021-10-06
  • 2020-05-11
相关资源
最近更新 更多