使用端点和凸出距离绘制圆弧。在 OpenCV 或 PIL 中
Draw an arc by using end-points and bulge distance. In OpenCV or PIL
正在编写将 dxf 转换为 png 的脚本,我需要绘制只有三个参数的圆弧,即圆弧起点、圆弧终点和凸起距离。
OpenCV和PIL我都检查过了,他们需要开始和结束角度来画这个弧。我可以使用一些几何图形找出这些角度,但想知道是否还有我遗漏的其他解决方案。
您拥有定义圆弧的三个信息:圆上的两个点(定义该圆的 弦 )和凸起距离(称为 圆弧的sagitta).
见下图:
这里s是矢形,l是半弦长,r 当然是半径。其他重要的未标记位置是弦与圆相交的点、矢状与圆相交的点和圆心,半径从这里延伸。
对于 OpenCV 的 ellipse()
函数,我们将使用以下原型:
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]]) → img
其中大部分参数如下图所示:
由于我们绘制的是圆形而不是椭圆弧,major/minor 轴将具有相同的大小并且旋转它没有区别,因此轴将只是 (radius, radius)
和 angle
应该为零以简化。那么我们需要的参数就只有圆心,半径,以及绘制的起始角和结束角,对应弦的点。角度很容易计算(它们只是圆上的一些角度)。所以最终我们需要找到圆的半径和圆心。
求半径和圆心就像求圆的方程一样,所以有很多种方法。但是由于我们在这里编程,IMO 最简单的方法是在矢状面接触圆的位置定义圆上的第三个点,然后从这三个点求解圆。
所以首先我们需要得到弦的中点,得到一条垂直于该中点的线,然后将它延伸到矢状面的长度以到达第三点,但这很容易。我将开始给定 pt1 = (x1, y1)
和 pt2 = (x2, y2)
作为我在圆上的两个点,sagitta
是 'bulge depth'(即您拥有的参数):
# 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
现在我们得到了圆上的第三个点。请注意,sagitta 只是一些长度,所以它可以向任一方向移动——如果 sagitta 为负,它会从弦向一个方向移动,如果它是正的,它会向另一个方向移动。不确定这是否是给你的距离。
那我们就可以简单的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
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.')
这里报错是因为我们需要得到一个单位向量,所以需要除以不能为零的长度。当然,这只有在 pt1
和 pt2
是同一点时才会发生,因此您可以只在函数顶部检查它们是否唯一,而不是在此处检查。
第二个:
if np.isclose(M11, 0):
# catch error here, the points are collinear (sagitta ~ 0)
print('Error: The third point is collinear.')
这只有在三个点共线时才会发生,这只有在 sagitta 为 0 时才会发生。因此,您可以再次在函数的顶部检查它(也许会说,好的,如果它为 0,则只需从 pt1
到 pt2
画一条线或任何您想做的事情)。
正在编写将 dxf 转换为 png 的脚本,我需要绘制只有三个参数的圆弧,即圆弧起点、圆弧终点和凸起距离。
OpenCV和PIL我都检查过了,他们需要开始和结束角度来画这个弧。我可以使用一些几何图形找出这些角度,但想知道是否还有我遗漏的其他解决方案。
您拥有定义圆弧的三个信息:圆上的两个点(定义该圆的 弦 )和凸起距离(称为 圆弧的sagitta).
见下图:
这里s是矢形,l是半弦长,r 当然是半径。其他重要的未标记位置是弦与圆相交的点、矢状与圆相交的点和圆心,半径从这里延伸。
对于 OpenCV 的 ellipse()
函数,我们将使用以下原型:
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color[, thickness[, lineType[, shift]]]) → img
其中大部分参数如下图所示:
由于我们绘制的是圆形而不是椭圆弧,major/minor 轴将具有相同的大小并且旋转它没有区别,因此轴将只是 (radius, radius)
和 angle
应该为零以简化。那么我们需要的参数就只有圆心,半径,以及绘制的起始角和结束角,对应弦的点。角度很容易计算(它们只是圆上的一些角度)。所以最终我们需要找到圆的半径和圆心。
求半径和圆心就像求圆的方程一样,所以有很多种方法。但是由于我们在这里编程,IMO 最简单的方法是在矢状面接触圆的位置定义圆上的第三个点,然后从这三个点求解圆。
所以首先我们需要得到弦的中点,得到一条垂直于该中点的线,然后将它延伸到矢状面的长度以到达第三点,但这很容易。我将开始给定 pt1 = (x1, y1)
和 pt2 = (x2, y2)
作为我在圆上的两个点,sagitta
是 'bulge depth'(即您拥有的参数):
# 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
现在我们得到了圆上的第三个点。请注意,sagitta 只是一些长度,所以它可以向任一方向移动——如果 sagitta 为负,它会从弦向一个方向移动,如果它是正的,它会向另一个方向移动。不确定这是否是给你的距离。
那我们就可以简单的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
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.')
这里报错是因为我们需要得到一个单位向量,所以需要除以不能为零的长度。当然,这只有在 pt1
和 pt2
是同一点时才会发生,因此您可以只在函数顶部检查它们是否唯一,而不是在此处检查。
第二个:
if np.isclose(M11, 0):
# catch error here, the points are collinear (sagitta ~ 0)
print('Error: The third point is collinear.')
这只有在三个点共线时才会发生,这只有在 sagitta 为 0 时才会发生。因此,您可以再次在函数的顶部检查它(也许会说,好的,如果它为 0,则只需从 pt1
到 pt2
画一条线或任何您想做的事情)。