找到使用 houghlines opencv 绘制的两条线的交点

find intersection point of two lines drawn using houghlines opencv

如何使用opencv霍夫线算法获取直线的交点?

这是我的代码:

import cv2
import numpy as np
import imutils

im = cv2.imread('../data/test1.jpg')
gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 60, 150, apertureSize=3)

img = im.copy()
lines = cv2.HoughLines(edges,1,np.pi/180,200)

for line in lines:
    for rho,theta in line:
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        x1 = int(x0 + 3000*(-b))
        y1 = int(y0 + 3000*(a))
        x2 = int(x0 - 3000*(-b))
        y2 = int(y0 - 3000*(a))
        cv2.line(img,(x1,y1),(x2,y2),(0,255,0),10)

cv2.imshow('houghlines',imutils.resize(img, height=650))
cv2.waitKey(0)
cv2.destroyAllWindows()

输出:

我要获取所有的交点

如果您已经有了线段,只需将它们代入线方程...

x = x1 + u * (x2-x1)
y = y1 + u * (y2-y1)

可以使用以下任何一种方法找到您...

u = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))
u = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1))

你不想得到平行线的交点;只有垂直线与水平线的交点。此外,由于您有垂直线,计算斜率可能会导致斜率爆炸或 inf 斜率,因此您不应使用 y = mx+b 方程。你需要做两件事:

  1. 根据角度将您的线条分成两条 class。
  2. 计算一个 class 中每条线与另一个 class 中的线的交点。

使用 HoughLines,您已经得到了 rho, theta 的结果,因此您可以轻松地用 theta 分割成两个 class 的角度。您可以使用例如cv2.kmeans()theta 作为您要拆分的数据。

然后,要计算交点,可以使用calculating intersections given two points from each line的公式。您已经从每行计算了两个点:(x1, y1), (x2, y2) 因此您可以简单地存储并使用它们。编辑:实际上,如下面我的代码所示,有一个公式可用于计算 HoughLines 给出的 rho, theta 形式的线的交点。

我之前已经用一些 python 代码回答了 ,您可以查看这些代码;请注意,这是使用 HoughLinesP,它只给你线段。


代码示例

您没有提供原始图片,所以我无法使用它。相反,我将使用 OpenCV 在其 Hough 变换和阈值教程中使用的标准数独图像:

首先,我们将只读取此图像并使用自适应阈值将其二值化,就像 this OpenCV tutorial:

中使用的那样
import cv2
import numpy as np

img = cv2.imread('sudoku.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.medianBlur(gray, 5)
adapt_type = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
thresh_type = cv2.THRESH_BINARY_INV
bin_img = cv2.adaptiveThreshold(blur, 255, adapt_type, thresh_type, 11, 2)

然后我们会找到 cv2.HoughLines():

的霍夫线
rho, theta, thresh = 2, np.pi/180, 400
lines = cv2.HoughLines(bin_img, rho, theta, thresh)

现在,如果我们想找到交点,实际上我们只想找到垂直线的交点。我们不想要大部分平行线的交点。所以我们需要分割我们的线。在这个特定的例子中,你可以很容易地根据一个简单的测试来检查这条线是水平的还是垂直的;垂直线的 theta 约为 0 或 180;水平线的 theta 约为 90。但是,如果您想根据任意数量的角度自动分割它们,而无需定义这些角度,我认为最好的办法是使用 cv2.kmeans().

要做好一件棘手的事情。 HoughLines returns行rho, theta形式(Hesse normal form),返回的theta在0度到180度之间,180度和0度左右的行类似(它们都接近水平线),所以我们需要一些方法来获得 kmeans.

中的这种周期性

如果我们把这个角度画在单位圆上,但是把这个角度乘以,那么原来大约180度的角度会变得接近360度,因此会有x, y 单位圆上的值对于 0 的角度接近相同。因此我们可以通过绘制 2*angle 和单位圆上的坐标来获得一些不错的“接近度”。然后我们可以 运行 cv2.kmeans() 在这些点上,并根据我们想要的数量自动分割。

那么让我们构建一个函数来进行分割:

from collections import defaultdict
def segment_by_angle_kmeans(lines, k=2, **kwargs):
    """Groups lines based on angle with k-means.

    Uses k-means on the coordinates of the angle on the unit circle 
    to segment `k` angles inside `lines`.
    """

    # Define criteria = (type, max_iter, epsilon)
    default_criteria_type = cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
    criteria = kwargs.get('criteria', (default_criteria_type, 10, 1.0))
    flags = kwargs.get('flags', cv2.KMEANS_RANDOM_CENTERS)
    attempts = kwargs.get('attempts', 10)

    # returns angles in [0, pi] in radians
    angles = np.array([line[0][1] for line in lines])
    # multiply the angles by two and find coordinates of that angle
    pts = np.array([[np.cos(2*angle), np.sin(2*angle)]
                    for angle in angles], dtype=np.float32)

    # run kmeans on the coords
    labels, centers = cv2.kmeans(pts, k, None, criteria, attempts, flags)[1:]
    labels = labels.reshape(-1)  # transpose to row vec

    # segment lines based on their kmeans label
    segmented = defaultdict(list)
    for i, line in enumerate(lines):
        segmented[labels[i]].append(line)
    segmented = list(segmented.values())
    return segmented

现在要使用它,我们可以简单地调用:

segmented = segment_by_angle_kmeans(lines)

这里的好处是我们可以通过指定可选参数 k 来指定任意数量的组(默认情况下,k = 2 所以我没有在这里指定它)。

如果我们用不同的颜色绘制每组的线:

现在剩下的就是找到第一组中每条线与第二组中每条线的交点。由于这些线是 Hesse 范式,因此有一个很好的线性代数公式用于计算这种形式的线的交点。参见 here。让我们在这里创建两个函数;一个只找到两条线的交点,一个函数循环遍历组中的所有线并对两条线使用该更简单的函数:

def intersection(line1, line2):
    """Finds the intersection of two lines given in Hesse normal form.

    Returns closest integer pixel locations.
    See 
    """
    rho1, theta1 = line1[0]
    rho2, theta2 = line2[0]
    A = np.array([
        [np.cos(theta1), np.sin(theta1)],
        [np.cos(theta2), np.sin(theta2)]
    ])
    b = np.array([[rho1], [rho2]])
    x0, y0 = np.linalg.solve(A, b)
    x0, y0 = int(np.round(x0)), int(np.round(y0))
    return [[x0, y0]]


def segmented_intersections(lines):
    """Finds the intersections between groups of lines."""

    intersections = []
    for i, group in enumerate(lines[:-1]):
        for next_group in lines[i+1:]:
            for line1 in group:
                for line2 in next_group:
                    intersections.append(intersection(line1, line2)) 

    return intersections

那么要使用它,很简单:

intersections = segmented_intersections(segmented)

绘制所有交叉点,我们得到:


如上所述,这段代码也可以将线分割成两组以上的角。这是在手绘三角形上 运行ning,并计算检测到的线与 k=3:

的交点

首先,您需要细化 Hough 变换的输出(我通常通过 k-means 基于某些标准的聚类来完成此操作,例如斜率 and/or 线段的质心)。比如你的问题,似乎所有直线的斜率通常都在0、180、90度附近,所以你可以在此基础上进行聚类。

接下来,您可以通过两种不同的方式获取交点(技术上是相同的):

  1. Bhupen 回答中的方程式。
  2. 使用像 Shapely or SymPy 这样的几何库。使用几何库执行此操作的好处是,您可以访问以后在开发中可能需要的各种工具(交集、插值、凸包等)

P.S。 Shapely 是一个强大的 C++ 几何库的包装器,但 SymPy 是纯粹的 Python。如果您的应用程序时间紧迫,您可能需要考虑这一点。

这里我用一些方法处理了我的图像;

1.Grayscale

2.Either按位转换或边缘检测,这取决于我猜的图像,这里我已经进行了按位转换。 首先将所有检测到的行执行到一个列表中。

listOflines = cv2.HoughLines(mask_inv,1,np.pi/180,200)

我们将获得 'rho' 和 'theta' 的值, 我在这里做的是创建两个空列表,一个用于垂直线,一个用于水平线,并将两行的值附加到各自的列表中。

rowsValue = []
columnValue = []

这里是竖线和横线的逻辑。

for line in listOflines:
if line[0][1] == 0:
    columnValue.append(line[0][0])
else:
    rowsValue.append(line[0][0])

现在重要的部分来了, 当每条线穿过并彼此相交时,它在特定像素值上与该线相交。 我们根据 'rho'.

得到该像素值

现在让我们创建元组作为坐标传递给 'cv2' 函数,即以 (x,y) 的形式。

tupsList = [(r,c) for r in rowsValue for c in columnValue]
for tups in tupsList:
     cv2.circle(image, tups, 1,(0,0,255), 2)
cv2.imshow('image',image)
cv2.waitKey(0)
cv2.destroyAllWindows()

就是这样!! 下面是之前和之后的图片。

Original Image

Intersection Image

这是使用 OpenCV 2.4 在 python 2.7.x 中编写的完整解决方案。

它使用了此线程中 alkasm 的解决方案,该解决方案不完整。此外,HoughLines() 的返回值和 kmeans() 的语法已从 OpenCV 2.x 更改为 3.x

结果一:桌上一张纸
https://i.ibb.co/VBSY7V7/paper-on-desk-intersection-points.jpg
这回答了最初的问题,但是使用 k-means k = 2,3,4 的聚类并不能分割这张纸。你需要一种不同的方法来找到纸的角
例如过滤平行线。

结果2:数独格子
https://i.ibb.co/b6thfgr/sudoku-intersection-points.jpg

代码: https://pastiebin.com/5f36425b7ae3d

"""
Find the intersection points of lines.
"""

import numpy as np
import cv2
from collections import defaultdict
import sys


img = cv2.imread("paper_on_desk.jpg")
#img = cv2.imread("sudoku.jpg")


def segment_by_angle_kmeans(lines, k=2, **kwargs):
    """
    Group lines by their angle using k-means clustering.

    Code from here:
    
    """

    # Define criteria = (type, max_iter, epsilon)
    default_criteria_type = cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
    criteria = kwargs.get('criteria', (default_criteria_type, 10, 1.0))

    flags = kwargs.get('flags', cv2.KMEANS_RANDOM_CENTERS)
    attempts = kwargs.get('attempts', 10)

    # Get angles in [0, pi] radians
    angles = np.array([line[0][1] for line in lines])

    # Multiply the angles by two and find coordinates of that angle on the Unit Circle
    pts = np.array([[np.cos(2*angle), np.sin(2*angle)] for angle in angles], dtype=np.float32)

    # Run k-means
    if sys.version_info[0] == 2:
        # python 2.x
        ret, labels, centers = cv2.kmeans(pts, k, criteria, attempts, flags)
    else: 
        # python 3.x, syntax has changed.
        labels, centers = cv2.kmeans(pts, k, None, criteria, attempts, flags)[1:]

    labels = labels.reshape(-1) # Transpose to row vector

    # Segment lines based on their label of 0 or 1
    segmented = defaultdict(list)
    for i, line in zip(range(len(lines)), lines):
        segmented[labels[i]].append(line)

    segmented = list(segmented.values())
    print("Segmented lines into two groups: %d, %d" % (len(segmented[0]), len(segmented[1])))

    return segmented


def intersection(line1, line2):
    """
    Find the intersection of two lines 
    specified in Hesse normal form.

    Returns closest integer pixel locations.

    See here:
    
    """

    rho1, theta1 = line1[0]
    rho2, theta2 = line2[0]
    A = np.array([[np.cos(theta1), np.sin(theta1)],
                  [np.cos(theta2), np.sin(theta2)]])
    b = np.array([[rho1], [rho2]])
    x0, y0 = np.linalg.solve(A, b)
    x0, y0 = int(np.round(x0)), int(np.round(y0))

    return [[x0, y0]]


def segmented_intersections(lines):
    """
    Find the intersection between groups of lines.
    """

    intersections = []
    for i, group in enumerate(lines[:-1]):
        for next_group in lines[i+1:]:
            for line1 in group:
                for line2 in next_group:
                    intersections.append(intersection(line1, line2)) 

    return intersections


def drawLines(img, lines, color=(0,0,255)):
    """
    Draw lines on an image
    """
    for line in lines:
        for rho,theta in line:
            a = np.cos(theta)
            b = np.sin(theta)
            x0 = a*rho
            y0 = b*rho
            x1 = int(x0 + 1000*(-b))
            y1 = int(y0 + 1000*(a))
            x2 = int(x0 - 1000*(-b))
            y2 = int(y0 - 1000*(a))
            cv2.line(img, (x1,y1), (x2,y2), color, 1)


gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

blur = cv2.medianBlur(gray, 5)

# Make binary image
adapt_type = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
thresh_type = cv2.THRESH_BINARY_INV
bin_img = cv2.adaptiveThreshold(blur, 255, adapt_type, thresh_type, 11, 2)
cv2.imshow("binary", bin_img)
cv2.waitKey()

# Detect lines
rho = 2
theta = np.pi/180
thresh = 350
lines = cv2.HoughLines(bin_img, rho, theta, thresh)

if sys.version_info[0] == 2:
    # python 2.x
    # Re-shape from 1xNx2 to Nx1x2
    temp_lines = []
    N = lines.shape[1]
    for i in range(N):
        rho = lines[0,i,0]
        theta = lines[0,i,1]
        temp_lines.append( np.array([[rho,theta]]) )
    lines = temp_lines

print("Found lines: %d" % (len(lines)))

# Draw all Hough lines in red
img_with_all_lines = np.copy(img)
drawLines(img_with_all_lines, lines)
cv2.imshow("Hough lines", img_with_all_lines)
cv2.waitKey()
cv2.imwrite("all_lines.jpg", img_with_all_lines)

# Cluster line angles into 2 groups (vertical and horizontal)
segmented = segment_by_angle_kmeans(lines, 2)

# Find the intersections of each vertical line with each horizontal line
intersections = segmented_intersections(segmented)

img_with_segmented_lines = np.copy(img)

# Draw vertical lines in green
vertical_lines = segmented[1]
img_with_vertical_lines = np.copy(img)
drawLines(img_with_segmented_lines, vertical_lines, (0,255,0))

# Draw horizontal lines in yellow
horizontal_lines = segmented[0]
img_with_horizontal_lines = np.copy(img)
drawLines(img_with_segmented_lines, horizontal_lines, (0,255,255))

# Draw intersection points in magenta
for point in intersections:
    pt = (point[0][0], point[0][1])
    length = 5
    cv2.line(img_with_segmented_lines, (pt[0], pt[1]-length), (pt[0], pt[1]+length), (255, 0, 255), 1) # vertical line
    cv2.line(img_with_segmented_lines, (pt[0]-length, pt[1]), (pt[0]+length, pt[1]), (255, 0, 255), 1)

cv2.imshow("Segmented lines", img_with_segmented_lines)
cv2.waitKey()
cv2.imwrite("intersection_points.jpg", img_with_segmented_lines)

这里有一个更直接的解决方案,适应this answer。它应该比 Bhupen 的答案在数值上更稳定

首先你应该聚类这些线,这样你就不会像其他答案中提到的那样试图找到平行线的交点(否则,你会得到不一致的结果 and/or 计算错误)

然后你可以找到一对线的交点:

def hough_inter(theta1, rho1, theta2, rho2):
    A = np.array([[cos(theta1), sin(theta1)], 
                  [cos(theta2), sin(theta2)]])
    b = np.array([rho1, rho2])
    return np.linalg.lstsq(A, b)[0] # use lstsq to solve Ax = b, not inv() which is unstable

我的数据结果:

解释:

hough (rho/theta) space 中的线在 x-y space:

中表示如下
rho = x cosθ + y sinθ

因此交集 (x, y) 必然求解

x cos θ1 + y sin θ1 = r1
x cos θ2 + y sin θ2 = r2

即AX=b,其中

A = [cos θ1  sin θ1]   b = |r1|   X = |x|
    [cos θ2  sin θ2]       |r2|       |y|

因此如果你在python中有两条线,你可以这样找到它们的交点。