Ближайшая точка на кубической кривой Безье?

Как я могу найти точку B (t) вдоль кубической кривой Безье, которая ближе всего к произвольной точке P в плоскости?

3 ответов


после долгих поисков я нашел статью, в которой обсуждается метод поиска ближайшей точки На кривой Безье к данной точке:

Улучшенный Алгебраический Алгоритм На Точке Проекция Кривых Безье, by Сяо-Диао Чэнь, Инь Чжоу, Чжэнью Шу, Хуа Су и Жан-Клод Поль.

кроме того, я нашел Википедия и MathWorld описания последовательностей штурма, полезных для понимания первая часть алгоритма, так как сама статья не очень понятна в своем собственном описании.


в зависимости от вашей толерантности. Грубая сила и принятие ошибки. Этот алгоритм может быть неправильным в некоторых редких случаях. Но в большинстве из них он найдет точку, очень близкую к правильному ответу, и результаты улучшатся тем выше, чем вы установите срезы. Он просто пробует каждую точку вдоль кривой через регулярные промежутки времени и возвращает лучшую из найденных.

public double getClosestPointToCubicBezier(double fx, double fy, int slices, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3)  {
    double tick = 1d / (double) slices;
    double x;
    double y;
    double t;
    double best = 0;
    double bestDistance = Double.POSITIVE_INFINITY;
    double currentDistance;
    for (int i = 0; i <= slices; i++) {
        t = i * tick;
        //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
        x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3;
        y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3;

        currentDistance = Point.distanceSq(x,y,fx,fy);
        if (currentDistance < bestDistance) {
            bestDistance = currentDistance;
            best = t;
        }
    }
    return best;
}

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

public double getClosestPointToCubicBezier(double fx, double fy, int slices, int iterations, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
    return getClosestPointToCubicBezier(iterations, fx, fy, 0, 1d, slices, x0, y0, x1, y1, x2, y2, x3, y3);
}

private double getClosestPointToCubicBezier(int iterations, double fx, double fy, double start, double end, int slices, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
    if (iterations <= 0) return (start + end) / 2;
    double tick = (end - start) / (double) slices;
    double x, y, dx, dy;
    double best = 0;
    double bestDistance = Double.POSITIVE_INFINITY;
    double currentDistance;
    double t = start;
    while (t <= end) {
        //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
        x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3;
        y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3;


        dx = x - fx;
        dy = y - fy;
        dx *= dx;
        dy *= dy;
        currentDistance = dx + dy;
        if (currentDistance < bestDistance) {
            bestDistance = currentDistance;
            best = t;
        }
        t += tick;
    }
    return getClosestPointToCubicBezier(iterations - 1, fx, fy, Math.max(best - tick, 0d), Math.min(best + tick, 1d), slices, x0, y0, x1, y1, x2, y2, x3, y3);
}

в обоих случаях вы можете сделать quad так же легко:

x = (1 - t) * (1 - t) * x0 + 2 * (1 - t) * t * x1 + t * t * x2; //quad.
y = (1 - t) * (1 - t) * y0 + 2 * (1 - t) * t * y1 + t * t * y2; //quad.

выключив уравнение там.

в то время как принятый ответ правильный, и вы действительно можете выяснить корни и сравнивать эти вещи. Если вам действительно просто нужно найти ближайшую точку на Кривой, это сделает это.


в отношении Бена в комментариях. Вы не можете коротко передать формулу во многих сотнях диапазонов контрольных точек, как это сделал я для кубических и четырехъядерных форм. Потому что сумма, требуемая каждым новым добавлением кривой Безье, означает, что вы строите для них пирамиды Пифагора, а мы имеем дело с еще более массивными строками чисел. Для квадроцикла вы идете 1, 2, 1, для кубического вы идете 1, 3, 3, 1. Вы в конечном итоге строите все большие и большие пирамиды и в конечном итоге разбиваете их алгоритмом Кастельяу (я написал это для твердой скорости):

/**
 * Performs deCasteljau's algorithm for a bezier curve defined by the given control points.
 *
 * A cubic for example requires four points. So it should get at least an array of 8 values
 *
 * @param controlpoints (x,y) coord list of the Bezier curve.
 * @param returnArray Array to store the solved points. (can be null)
 * @param t Amount through the curve we are looking at.
 * @return returnArray
 */
public static float[] deCasteljau(float[] controlpoints, float[] returnArray, float t) {
    int m = controlpoints.length;
    int sizeRequired = (m/2) * ((m/2) + 1);
    if (returnArray == null) returnArray = new float[sizeRequired];
    if (sizeRequired > returnArray.length) returnArray = Arrays.copyOf(controlpoints, sizeRequired); //insure capacity
    else System.arraycopy(controlpoints,0,returnArray,0,controlpoints.length);
    int index = m; //start after the control points.
    int skip = m-2; //skip if first compare is the last control point.
    for (int i = 0, s = returnArray.length - 2; i < s; i+=2) {
        if (i == skip) {
            m = m - 2;
            skip += m;
            continue;
        }
        returnArray[index++] = (t * (returnArray[i + 2] - returnArray[i])) + returnArray[i];
        returnArray[index++] = (t * (returnArray[i + 3] - returnArray[i + 1])) + returnArray[i + 1];
    }
    return returnArray;
}

вам в основном нужно использовать алгоритм напрямую, а не просто для вычисления x, y, которые происходят на самой кривой, но вам также нужно выполнить фактический и правильный алгоритм подразделения Безье (есть и другие, но это то, что я бы рекомендовал), чтобы вычислить не просто приближение, как я даю, разделив его на отрезки линии, но фактических кривых. Вернее полигона КАСКО, которые наверняка содержат кривая.

вы делаете это, используя приведенный выше алгоритм для разделения кривых в заданном t. Так T=0.5 для того чтобы отрезать кривые в половине (Примечание 0.2 сократило бы его на 20% 80% через Кривую). Затем вы индексируете различные точки на стороне пирамиды и на другой стороне пирамиды, построенной из основания. Так, например, в cubic:

   9
  7 8
 4 5 6
0 1 2 3

вы подадите алгоритм 0 1 2 3 в качестве контрольных точек, затем вы индексируете две идеально разделенные кривые на 0, 4, 7, 9 и 9, 8, 6, 3. Обратите особое внимание на то, что эти кривые начинаются и заканчиваются в одной точке. и окончательный индекс 9, который является точкой кривая используется в качестве другой новой точки привязки. Учитывая это, вы можете прекрасно разделить кривую Безье.

затем, чтобы найти ближайшую точку, вы хотели бы продолжать подразделять кривую на разные части, отмечая, что это так, что вся кривая кривой Безье содержится в корпусе контрольных точек. То есть, если мы повернем точки 0, 1, 2, 3 в замкнутый путь, соединяющий 0,3 этой кривой должны полностью упасть в корпус многоугольника. Так что же нам делать? затем мы продолжаем подразделять кривые до тех пор, пока не узнаем, что самая дальняя точка одной кривой ближе, чем самая близкая точка другой кривой. Мы просто сравниваем эту точку P со всеми контрольными и якорными точками кривых. И отбросьте любую кривую из нашего активного списка, чья ближайшая точка (якорь или элемент управления) находится дальше, чем самая дальняя точка другой кривой. Затем мы разделяем все активные кривые и делаем это снова. В конце концов, у нас будет очень разделенные кривые отбрасывают около половины каждого шага(это означает, что он должен быть O (N log n)), пока наша ошибка в основном незначительна. В этот момент мы называем наши активные кривые ближайшей точкой к этой точке (их может быть несколько) и отмечаем, что ошибка в этом сильно разделенном бите кривой в основном равна точке. Или просто решите проблему, сказав, что какая из двух опорных точек ближе всего к нашей точке P. И мы знаем, что ошибка очень специфична степень.

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


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

демо:http://phrogz.net/svg/closest-point-on-bezier.html

/** Find the ~closest point on a Bézier curve to a point you supply.
 * out    : A vector to modify to be the point on the curve
 * curve  : Array of vectors representing control points for a Bézier curve
 * pt     : The point (vector) you want to find out to be near
 * tmps   : Array of temporary vectors (reduces memory allocations)
 * returns: The parameter t representing the location of `out`
 */
function closestPoint(out, curve, pt, tmps) {
    let mindex, scans=25; // More scans -> better chance of being correct
    const vec=vmath['w' in curve[0]?'vec4':'z' in curve[0]?'vec3':'vec2'];
    for (let min=Infinity, i=scans+1;i--;) {
        let d2 = vec.squaredDistance(pt, bézierPoint(out, curve, i/scans, tmps));
        if (d2<min) { min=d2; mindex=i }
    }
    let t0 = Math.max((mindex-1)/scans,0);
    let t1 = Math.min((mindex+1)/scans,1);
    let d2ForT = t => vec.squaredDistance(pt, bézierPoint(out,curve,t,tmps));
    return localMinimum(t0, t1, d2ForT, 1e-4);
}

/** Find a minimum point for a bounded function. May be a local minimum.
 * minX   : the smallest input value
 * maxX   : the largest input value
 * ƒ      : a function that returns a value `y` given an `x`
 * ε      : how close in `x` the bounds must be before returning
 * returns: the `x` value that produces the smallest `y`
 */
function localMinimum(minX, maxX, ƒ, ε) {
    if (ε===undefined) ε=1e-10;
    let m=minX, n=maxX, k;
    while ((n-m)>ε) {
        k = (n+m)/2;
        if (ƒ(k-ε)<ƒ(k+ε)) n=k;
        else               m=k;
    }
    return k;
}

/** Calculate a point along a Bézier segment for a given parameter.
 * out    : A vector to modify to be the point on the curve
 * curve  : Array of vectors representing control points for a Bézier curve
 * t      : Parameter [0,1] for how far along the curve the point should be
 * tmps   : Array of temporary vectors (reduces memory allocations)
 * returns: out (the vector that was modified)
 */
function bézierPoint(out, curve, t, tmps) {
    if (curve.length<2) console.error('At least 2 control points are required');
    const vec=vmath['w' in curve[0]?'vec4':'z' in curve[0]?'vec3':'vec2'];
    if (!tmps) tmps = curve.map( pt=>vec.clone(pt) );
    else tmps.forEach( (pt,i)=>{ vec.copy(pt,curve[i]) } );
    for (var degree=curve.length-1;degree--;) {
        for (var i=0;i<=degree;++i) vec.lerp(tmps[i],tmps[i],tmps[i+1],t);
    }
    return vec.copy(out,tmps[0]);
}

код выше использует vmath библиотека эффективно lerp между векторами (в 2D, 3D или 4D), но было бы тривиально заменить lerp() вызов в bézierPoint() С собственный код.

настройки алгоритма

на closestPoint() функция работает в два этапа:

  • во-первых, вычислить точки по всей кривой (равномерно разнесенные значения t