使用OpenCV Hough Transform在2D点云中进行直线检测

6

我已经尽力去寻找如何使用OpenCV进行直线检测的方法。然而,我无法找到我需要的示例。我想要用它来在简单的二维点云中找到直线。作为测试,我想使用以下点:

enter image description here

import random
import numpy as np
import matplotlib.pyplot as plt

a = np.random.randint(1,101,400)  # Random points.
b = np.random.randint(1,101,400)  # Random points.

for i in range(0, 90, 2):  # A line to detect
    a = np.append(a, [i+5])
    b = np.append(b, [0.5*i+30])

plt.plot(a, b, '.')
plt.show()

我已经找到了许多Hough变换的初始示例。但是,当涉及到代码示例时,我只能发现使用了图像。
是否有一种方法可以使用OpenCV Hough变换来检测一组点中的线条,或者您可以推荐任何其他方法或库?
---- 编辑 ----
阅读了一些很好的答案后,我觉得我应该更好地描述一下我的意图。我有一个高分辨率2D LiDAR,需要从数据中提取墙壁。典型的扫描可能如下所示:enter image description here 其中“正确的输出”将类似于:enter image description here 在我做了更多的研究之后,我怀疑在这种情况下使用Hough变换不是最佳选择。有什么提示我应该寻找什么?
(如果有人感兴趣,LiDAR和墙面提取用于生成地图和导航机器人。)
谢谢,Jakob

请详细说明您认为霍夫变换不够优秀的原因?膨胀和腐蚀作为预处理可以用于连接相邻的点。在这里,您可以做一些有趣的事情,例如将霍夫空间限制为水平和垂直线。 - Nikolas Rieble
霍夫变换用于图像处理,我不想被迫将收集到的数据离散化到低于1毫米,否则在这个例子中会产生一个50,000 x 40,000像素的图像。然而,你建议的代码确实在我应用到收集到的数据时成功地提取了一些线条。但还不够好(目前)。我认为有一个方面可以帮助解决问题,那就是收集到的数据是按顺序(逆时针)排列的。我觉得应该有一种方法利用这些信息来提取线条。 - JakobVinkas
你能提供原始数据吗?或者任何原始数据的摘录? - Nikolas Rieble
这里是一个驱动器链接,链接到一个文本文件,文件中的两个数组分别表示x和y坐标,其中元素按索引对应。https://drive.google.com/file/d/1EnSOr2FYjIdqG1RdFgTkgsoEhVcG7Tl_/view?usp=sharing - JakobVinkas
嗨,我需要做和你一样的事情。你已经成功地完成了墙面跟踪吗?我需要为我的论文完成这个任务。请问我可以与您联系吗?@JakobVinkas - Kazi
@Kazi 我正在开发一个大型导航项目,而墙壁检测只是其中的一小部分。我仍然没有找到一个合适的解决方案,但我很愿意进一步讨论并交流我们可能有用的见解。你可以在这里讨论,或通过jakobvinkas@gmail.com联系我。 - JakobVinkas
4个回答

2
一种方法是按照这些幻灯片的步骤实现Hough变换,跳过边缘检测部分,以此来实现。幻灯片链接如下:slides
另一种方法是从您的点列表中创建图像,例如:
#create an image from list of points
x_shape = int(np.max(a) - np.min(a))
y_shape = int(np.max(b) - np.min(b))

im = np.zeros((x_shape+1, y_shape+1))

indices = np.stack([a-1,b-1], axis =1).astype(int)
im[indices[:,0], indices[:,1]] = 1

plt.imshow(im)

#feed to opencv as usual

根据这个问题的回答

编辑:不要使用OpenCV,而是使用skimage,例如在文档中所述:

Original Answer翻译成"最初的回答"

import numpy as np

from skimage.transform import (hough_line, hough_line_peaks,
                               probabilistic_hough_line)
from skimage.feature import canny
from skimage import data

import matplotlib.pyplot as plt
from matplotlib import cm


# Constructing test image
#image = np.zeros((100, 100))
#idx = np.arange(25, 75)
#image[idx[::-1], idx] = 255
#image[idx, idx] = 255

image = im

# Classic straight-line Hough transform
h, theta, d = hough_line(image)

# Generating figure 1
fig, axes = plt.subplots(1, 3, figsize=(15, 6))
ax = axes.ravel()

ax[0].imshow(image, cmap=cm.gray)
ax[0].set_title('Input image')
ax[0].set_axis_off()

ax[1].imshow(np.log(1 + h),
             extent=[np.rad2deg(theta[-1]), np.rad2deg(theta[0]), d[-1], d[0]],
             cmap=cm.gray, aspect=1/1.5)
ax[1].set_title('Hough transform')
ax[1].set_xlabel('Angles (degrees)')
ax[1].set_ylabel('Distance (pixels)')
ax[1].axis('image')

ax[2].imshow(image, cmap=cm.gray)
for _, angle, dist in zip(*hough_line_peaks(h, theta, d)):
    y0 = (dist - 0 * np.cos(angle)) / np.sin(angle)
    y1 = (dist - image.shape[1] * np.cos(angle)) / np.sin(angle)
    ax[2].plot((0, image.shape[1]), (y0, y1), '-r')
ax[2].set_xlim((0, image.shape[1]))
ax[2].set_ylim((image.shape[0], 0))
ax[2].set_axis_off()
ax[2].set_title('Detected lines')

plt.tight_layout()
plt.show()

# Line finding using the Probabilistic Hough Transform
image = data.camera()
edges = canny(image, 2, 1, 25)
lines = probabilistic_hough_line(edges, threshold=10, line_length=5,
                                 line_gap=3)

# Generating figure 2
fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharex=True, sharey=True)
ax = axes.ravel()

ax[0].imshow(image, cmap=cm.gray)
ax[0].set_title('Input image')

ax[1].imshow(edges, cmap=cm.gray)
ax[1].set_title('Canny edges')

ax[2].imshow(edges * 0)
for line in lines:
    p0, p1 = line
    ax[2].plot((p0[0], p1[0]), (p0[1], p1[1]))
ax[2].set_xlim((0, image.shape[1]))
ax[2].set_ylim((image.shape[0], 0))
ax[2].set_title('Probabilistic Hough')

for a in ax:
    a.set_axis_off()

plt.tight_layout()
plt.show()

result


Hough算法无法处理不连续的点,它需要实际的线段,每个点都必须相邻。 - lenik
这些点不需要是相邻的。有关算法的详细信息,请查看此处 - 它特别包含两个不相交点的图像以及霍夫在该情况下的工作方式。http://me.umn.edu/courses/me5286/vision/Notes/2015/ME5286-Lecture9.pdf - Nikolas Rieble
2
请问您能否用一个 POC 代码替换掉 #feed to opencv as usual,以证明您的方法在这种情况下确实有效? - lenik
如果您有更多问题,我很乐意为您提供更多信息。 - Nikolas Rieble
如果你有时间,请看一下我对帖子进行的编辑,我在其中更详细地解释了我的问题。 - JakobVinkas

2
这是一种方法:
  • 将图像转换为灰度
  • 阈值处理以获取二值图像
  • 进行形态学操作以连接轮廓并平滑图像
  • 查找线条

将图像转换为灰度后,我们对其进行阈值处理以获得二进制图像。

import cv2
import numpy as np

image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255,cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

接下来,我们执行形态学操作来连接轮廓。

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)

最后,我们使用 cv2.HoughLinesP() 找到了这条线。

minLineLength = 550
maxLineGap = 70
lines = cv2.HoughLinesP(close,1,np.pi/180,100,minLineLength,maxLineGap)
for line in lines:
    for x1,y1,x2,y2 in line:
        cv2.line(image,(x1,y1),(x2,y2),(36,255,12),3)

不使用cv2.HoughLinesP()的替代方法是找到轮廓并使用cv2.contourArea()进行过滤。最大的轮廓将成为我们的线条。
完整代码
import cv2
import numpy as np

image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255,cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=3)

minLineLength = 550
maxLineGap = 70
lines = cv2.HoughLinesP(close,1,np.pi/180,100,minLineLength,maxLineGap)
for line in lines:
    for x1,y1,x2,y2 in line:
        cv2.line(image,(x1,y1),(x2,y2),(36,255,12),3)

cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.imshow('image', image)
cv2.waitKey()

1
您很可能无法使用Hough变换来检测一组点中的线条。Hough变换适用于图像,最好是将边缘标记为1的二值化图像,背景保持为0。所以,请忘记Hough变换。
对于您的特定情况,我建议使用某种RANSAC算法,该算法按照某些规则查找特定点,并忽略其他所有内容。尽管在您的情况下存在大量(=太多)噪音。如果您可以将噪点保持在50%以下,则RANSAC将起作用。您可以在此处阅读详细信息:OpenCV-Ransac拟合线 或者这里是最通用解释的维基百科页面:https://en.wikipedia.org/wiki/RANSAC

谢谢您的回答。如果您查看我所做的编辑,您可以看到实际实现中存在的噪声量。一个“扫描”会产生8400个测量点。 - JakobVinkas

0

在花费了很多时间后,我最终找到了一个令我满意的解决方案。

我的解决方案是通过扫描数据(作为旋转扫描仪)并迭代地查看数据的最小部分,然后运行自定义的RANSAC算法来适应当前段的线条。

然后,如果该段满足可能成为一条直线的标准,则会扩展该段并再次进行检查。这将以不同的方式重复所有小数据段。简而言之,我使用了自定义的、从头开始编写的迭代RANSAC线拟合。

如果我们以最初给出的类似示例为例: enter image description here

现在算法生成了以下结果: enter image description here

与环境的实际(墙)地图进行比较,我们可以看到以下比较:

enter image description here

我认为这已经足够好了。另一个重要的注意点是,该算法可以在多次数据扫描中运行,而所有扫描之间环境自然会略微变化(执行时间相当长):

enter image description here

可以看到地图上有一些额外的墙壁,这是可以预料的,还有一些错误的发现,可以通过过滤来排除它们,因为它们是明显的异常值。

现在来看代码... 最终解决方案是一个300行长的Python脚本,通过.pyx文件转换并使用Cython编译,以提高时间复杂度。如果需要代码或者更重要的是伪代码(因为我的实现是根据我的特定需求进行调整的),我可以提供,只要有人喜欢使用/阅读它 :)


我对伪代码很感兴趣。 - user3520616
@jakobvinkas,你能提供一下你的代码吗? - youssef

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