【问题标题】:Draw an arc by using end-points and bulge distance. In OpenCV or PIL使用端点和凸出距离绘制圆弧。在 OpenCV 或 PIL 中
【发布时间】:2018-01-09 06:37:44
【问题描述】:

使用脚本将 dxf 转换为 png,我需要绘制只有三个参数的圆弧,即圆弧起点、圆弧终点和凸出距离。

我已经检查过 OpenCV 和 PIL,它们需要开始和结束角度来绘制这条弧线。我可以使用一些几何形状找出这些角度,但想知道是否还有其他我错过的解决方案。

【问题讨论】:

  • 不给出任何理由就否决一个问题是很糟糕的。让我知道原因,如果需要,我可以更新问题。
  • 使用几何有什么问题?这不是需要任何大量处理的东西,它只需要一些操作就可以完成。请注意,至少对于 OpenCV,这个想法是您绘制一个椭圆,但仅从一个起始角度到结束角度(这毕竟是弧)。您如何发布尝试上述解决方案的代码(使用您喜欢的任何库)并发布您遇到的问题?
  • 取一个向量(长度为给定的喇叭距离)从端点形成的线的中间垂直延伸,以获得圆上的第三个点。众所周知,从这三个点,您可以计算通过点(即圆弧)using determinants 的圆的中心和半径。然后使用中心,您可以简单地计算与原始端点的角度,为您提供角度、中心、半径等,以使用 OpenCV 或 PIL 绘制弧线。
  • 感谢@AlexanderReynolds 指出这个解决方案。我正在使用椭圆曲线解决它,但是将三个点放在一个圆上(具有不同的半径和中心)的想法要好得多。

标签: opencv image-processing python-imaging-library dxf


【解决方案1】:

你有三个信息来定义你的圆弧:圆上的两个点(定义那个圆的 )和凸出距离(称为 sagitta圆弧)。

见下图:

这里s是矢状面,l是弦长的一半,r当然是半径。其他重要的未标记位置是弦与圆相交的点、矢状面与圆相交的点以及半径延伸的圆心。

对于 OpenCV 的 ellipse() 函数,我们将使用以下原型:

cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]]) → img

其中大部分参数由下图描述:

由于我们绘制的是圆形而非椭圆弧,因此主轴/次轴将具有相同的大小并且旋转它没有区别,因此轴将只是 (radius, radius) 并且 angle 应该为零到简化。那么我们唯一需要的参数就是圆心,半径,以及绘图的开始角度和结束角度,对应于弦的点。角度很容易计算(它们只是圆上的一些角度)。所以最终我们需要找到圆的半径和圆心。

求半径和圆心与求圆的方程是一样的,所以有很多方法可以做到。但由于我们在这里编程,IMO 最简单的方法是通过矢状面接触圆的位置在圆上定义第三个点,然后从这三个点求解圆。

所以首先我们需要得到弦的中点,得到一条与该中点垂直的线,然后将它延伸到矢状面的长度以到达第三点,但这很容易。我将开始给出pt1 = (x1, y1)pt2 = (x2, y2) 作为我在圆上的两个点,sagitta 是“凸起深度”(即您拥有的参数):

# extract point coordinates
x1, y1 = pt1
x2, y2 = pt2

# find normal from midpoint, follow by length sagitta
n = np.array([y2 - y1, x1 - x2])
n_dist = np.sqrt(np.sum(n**2))

if np.isclose(n_dist, 0):
    # catch error here, d(pt1, pt2) ~ 0
    print('Error: The distance between pt1 and pt2 is too small.')

n = n/n_dist
x3, y3 = (np.array(pt1) + np.array(pt2))/2 + sagitta * n

现在我们得到了圆圈上的第三个点。请注意,矢状面只是一些长度,所以它可以向任何一个方向移动——如果矢状面是负数,它会从和弦向一个方向移动,如果它是正数,它会向另一个方向移动。不确定这是否是给你距离的方式。

那么我们可以简单地use determinants to solve for the radius and center

# calculate the circle from three points
# see https://math.stackexchange.com/a/1460096/246399
A = np.array([
    [x1**2 + y1**2, x1, y1, 1],
    [x2**2 + y2**2, x2, y2, 1],
    [x3**2 + y3**2, x3, y3, 1]])
M11 = np.linalg.det(A[:, (1, 2, 3)])
M12 = np.linalg.det(A[:, (0, 2, 3)])
M13 = np.linalg.det(A[:, (0, 1, 3)])
M14 = np.linalg.det(A[:, (0, 1, 2)])

if np.isclose(M11, 0):
    # catch error here, the points are collinear (sagitta ~ 0)
    print('Error: The third point is collinear.')

cx = 0.5 * M12/M11
cy = -0.5 * M13/M11
radius = np.sqrt(cx**2 + cy**2 + M14/M11)

最后,由于我们需要开始和结束角度来用 OpenCV 绘制椭圆,我们可以使用 atan2() 来获取从中心到初始点的角度:

# calculate angles of pt1 and pt2 from center of circle
pt1_angle = 180*np.arctan2(y1 - cy, x1 - cx)/np.pi
pt2_angle = 180*np.arctan2(y2 - cy, x2 - cx)/np.pi

所以我把这一切打包成一个函数:

def convert_arc(pt1, pt2, sagitta):

    # extract point coordinates
    x1, y1 = pt1
    x2, y2 = pt2

    # find normal from midpoint, follow by length sagitta
    n = np.array([y2 - y1, x1 - x2])
    n_dist = np.sqrt(np.sum(n**2))

    if np.isclose(n_dist, 0):
        # catch error here, d(pt1, pt2) ~ 0
        print('Error: The distance between pt1 and pt2 is too small.')

    n = n/n_dist
    x3, y3 = (np.array(pt1) + np.array(pt2))/2 + sagitta * n

    # calculate the circle from three points
    # see https://math.stackexchange.com/a/1460096/246399
    A = np.array([
        [x1**2 + y1**2, x1, y1, 1],
        [x2**2 + y2**2, x2, y2, 1],
        [x3**2 + y3**2, x3, y3, 1]])
    M11 = np.linalg.det(A[:, (1, 2, 3)])
    M12 = np.linalg.det(A[:, (0, 2, 3)])
    M13 = np.linalg.det(A[:, (0, 1, 3)])
    M14 = np.linalg.det(A[:, (0, 1, 2)])

    if np.isclose(M11, 0):
        # catch error here, the points are collinear (sagitta ~ 0)
        print('Error: The third point is collinear.')

    cx = 0.5 * M12/M11
    cy = -0.5 * M13/M11
    radius = np.sqrt(cx**2 + cy**2 + M14/M11)

    # calculate angles of pt1 and pt2 from center of circle
    pt1_angle = 180*np.arctan2(y1 - cy, x1 - cx)/np.pi
    pt2_angle = 180*np.arctan2(y2 - cy, x2 - cx)/np.pi

    return (cx, cy), radius, pt1_angle, pt2_angle

使用这些值,您可以使用 OpenCV 的 ellipse() 函数创建弧。但是,这些都是浮点值。 ellipse() 确实允许您使用 shift 参数绘制浮点值,但是如果您不熟悉它,那会有点奇怪,因此我们可以借用 this answer 的解决方案来定义一个函数

def draw_ellipse(
        img, center, axes, angle,
        startAngle, endAngle, color,
        thickness=1, lineType=cv2.LINE_AA, shift=10):
    # uses the shift to accurately get sub-pixel resolution for arc
    # taken from https://stackoverflow.com/a/44892317/5087436
    center = (
        int(round(center[0] * 2**shift)),
        int(round(center[1] * 2**shift))
    )
    axes = (
        int(round(axes[0] * 2**shift)),
        int(round(axes[1] * 2**shift))
    )
    return cv2.ellipse(
        img, center, axes, angle,
        startAngle, endAngle, color,
        thickness, lineType, shift)

那么使用这些函数就这么简单:

img = np.zeros((500, 500), dtype=np.uint8)
pt1 = (50, 50)
pt2 = (350, 250)
sagitta = 50

center, radius, start_angle, end_angle = convert_arc(pt1, pt2, sagitta)
axes = (radius, radius)
draw_ellipse(img, center, axes, 0, start_angle, end_angle, 255)
cv2.imshow('', img)
cv2.waitKey()

再次注意,负矢状面使弧线指向另一个方向:

center, radius, start_angle, end_angle = convert_arc(pt1, pt2, sagitta)
axes = (radius, radius)
draw_ellipse(img, center, axes, 0, start_angle, end_angle, 255)
center, radius, start_angle, end_angle = convert_arc(pt1, pt2, -sagitta)
axes = (radius, radius)
draw_ellipse(img, center, axes, 0, start_angle, end_angle, 127)
cv2.imshow('', img)
cv2.waitKey()


最后只是为了扩展,我在convert_arc() 函数中爆发了两个错误案例。第一:

if np.isclose(n_dist, 0):
    # catch error here, d(pt1, pt2) ~ 0
    print('Error: The distance between pt1 and pt2 is too small.')

这里的错误捕获是因为我们需要得到一个单位向量,所以我们需要除以不能为零的长度。当然,这只会发生在pt1pt2 是同一点的情况下,因此您可以在函数顶部检查它们是否唯一,而不是在此处检查。

第二:

if np.isclose(M11, 0):
    # catch error here, the points are collinear (sagitta ~ 0)
    print('Error: The third point is collinear.')

只有当三个点共线时才会发生这种情况,只有当矢状位为 0 时才会发生这种情况。所以再一次,你可以在函数的顶部检查这个(也许说,好的,如果它是 0,那么就画从pt1pt2 的一行或任何你想做的)。

【讨论】:

  • 感谢您的精确解决方案。
猜你喜欢
  • 1970-01-01
  • 2014-04-11
  • 1970-01-01
  • 2011-05-10
  • 1970-01-01
  • 1970-01-01
  • 2018-04-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多