Ближайшая точка на кубической кривой Безье? - PullRequest
32 голосов
/ 30 апреля 2010

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

Ответы [ 3 ]

17 голосов
/ 01 мая 2010

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

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

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

11 голосов
/ 09 июля 2017

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

Демонстрация: 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 для эффективного перехода между векторами (в 2D, 3D или 4D), но было бы тривиально заменить вызов lerp() в bézierPoint() вашим собственным кодом.

НастройкаАлгоритм

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

  • Сначала рассчитайте точки по всей кривой (равномерно расположенные значения параметра t ).Запишите, какое значение t имеет наименьшее расстояние до точки.
  • Затем используйте функцию localMinimum() для поиска области наименьшего расстояния, используя двоичный поиск, чтобы найти t и точку, которая дает истинное наименьшее расстояние.

Значение scans в closestPoint() определяет, сколько сэмплов использовать в первом проходе.Меньшее количество сканирований быстрее, но увеличивает шансы пропустить истинную минимальную точку.

Предел ε, переданный функции localMinimum(), контролирует, как долго она продолжает искать лучшее значение.Значение 1e-2 квантовает кривую на ~ 100 точек, и, таким образом, вы можете увидеть точки, возвращенные из closestPoint(), всплывающими вдоль линии.Каждая дополнительная десятичная точка точности - 1e-3, 1e-4,… - стоит около 6-8 дополнительных вызовов к bézierPoint().

9 голосов
/ 30 декабря 2015

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

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);
}

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

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% по кривой). Затем вы индексируете различные точки на стороне пирамиды и на другой стороне пирамиды, построенной из основания. Так, например, в куб.

   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, а затем продолжаем подразделять кривые до тех пор, пока не узнаем, что самая дальняя точка одной кривой находится ближе, чем самая близкая точка другой кривой. Мы просто сравниваем эту точку P со всеми контрольными и опорными точками кривых. И отбросьте любую кривую из нашего активного списка, ближайшая точка которой (привязка или элемент управления) находится дальше, чем самая дальняя точка другой кривой. Затем мы подразделяем все активные кривые и делаем это снова. В конце концов у нас будут очень подразделенные кривые, отбрасывающие примерно половину каждого шага (то есть, это должно быть O (n log n)), пока наша ошибка в основном ничтожна. В этой точке мы называем наши активные кривые ближайшей точкой к этой точке (их может быть больше одной) и отмечаем, что ошибка в этом сильно подразделенном бите кривой в основном равна точке. Или просто решите проблему, сказав, какая из двух опорных точек ближе всего находится к нашей точке P. И мы знаем ошибку в очень определенной степени.

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

...