Нарисуйте дугу, используя конечные точки и расстояние выпуклости. В OpenCV или PIL

Работая над скриптом для преобразования dxf в png, мне нужно нарисовать дугу, которая имеет только три параметра, то есть начальную точку дуги, конечную точку дуги и расстояние до выпуклости.

Я проверил OpenCV и PIL, и им требуется начальный и конечный угол для рисования этой дуги. Я могу узнать эти углы, используя некоторую геометрию, но хотел бы знать, есть ли какое-либо другое решение, которое я упускаю.


person Mohammed Jamali    schedule 08.01.2018    source источник
comment
Голосование против вопроса без объяснения причин ужасно. Сообщите мне причину, и я могу обновить вопрос, если это необходимо.   -  person Mohammed Jamali    schedule 08.01.2018
comment
Что плохого в использовании геометрии? Это не то, что потребует какой-либо значительной обработки, это будет просто несколько операций и все будет готово. Обратите внимание, что, по крайней мере, для OpenCV идея состоит в том, что вы рисуете эллипс, но только от одного начального угла до конечного угла (в конце концов, это и есть дуга). Как насчет того, чтобы опубликовать код, пытающийся использовать вышеуказанное решение (с любыми библиотеками, которые вам нравятся), и опубликовать, какая у вас проблема с ним?   -  person alkasm    schedule 08.01.2018
comment
Возьмите вектор (с длиной заданного расстояния от горна), идущий перпендикулярно от середины линии, образованной вашими конечными точками, чтобы получить третью точку на вашем круге. По этим трем точкам, как известно, можно вычислить центр и радиус окружности, проходящей через точки (которая будет дугой) с помощью определителей. Затем с центром вы можете просто вычислить угол к исходным конечным точкам, давая вам углы, центр, радиус и т. д., чтобы рисовать дуги с помощью OpenCV или PIL.   -  person alkasm    schedule 08.01.2018
comment
Спасибо @AlexanderReynolds за указание на это решение. Я решал это с помощью эллиптической кривой, но идея с тремя точками, лежащими на окружности (с разным радиусом и центром), намного лучше.   -  person Mohammed Jamali    schedule 08.01.2018


Ответы (1)


У вас есть три элемента информации, определяющие дугу окружности: две точки на окружности (определяющие хорду этой окружности) и расстояние по выпуклости (называемое стрелкой дуги окружности). ).

См. следующий рисунок:

Обозначение дуги окружности

Здесь s — сагитта, l — половина длины хорды, а r — конечно же, радиус. Другими важными неотмеченными позициями являются точки, в которых хорда пересекает окружность, точка, в которой стрела пересекает окружность, и центр окружности, от которого отходит радиус.

Для функции OpenCV ellipse() мы будем использовать следующий прототип:

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

где большинство параметров описываются следующим графиком:

параметры cv2.ellipse

Поскольку мы рисуем дугу окружности, а не эллипса, большая и малая оси будут иметь одинаковый размер, и нет никакой разницы при вращении, поэтому оси будут просто (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

Теперь у нас есть третья точка на окружности. Обратите внимание, что сагитта — это всего лишь некоторая длина, поэтому она может идти в любом направлении — если бы сагитта была отрицательной, она шла бы в одном направлении от хорды, а если бы она была положительной, она шла бы в другом направлении. Не уверен, так ли это расстояние дается вам или нет.

Затем мы можем просто использовать определители для определения радиуса и центра.

# 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

С этими значениями вы можете создать дугу с помощью функции ellipse() OpenCV. Однако это все значения с плавающей запятой. ellipse() позволяет отображать значения с плавающей запятой с аргументом shift, но если вы не знакомы с ним, это немного странно, поэтому вместо этого мы можем позаимствовать решение из этот ответ для определения функции

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.')

Ошибка здесь в том, что нам нужно получить единичный вектор, поэтому нам нужно разделить на длину, которая не может быть равна нулю. Конечно, это произойдет только в том случае, если pt1 и pt2 являются одной и той же точкой, поэтому вы можете просто проверить их уникальность в начале функции, а не здесь.

Второй:

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

Это происходит только в том случае, если три точки лежат на одной прямой, что происходит только в том случае, если сагитта равна 0. Опять же, вы можете проверить это в верхней части своей функции (и, возможно, сказать, хорошо, если это 0, тогда просто нарисуйте линию от от pt1 до pt2 или как хотите).

person alkasm    schedule 08.01.2018
comment
Спасибо за точное решение. - person Mohammed Jamali; 09.01.2018