Graphics 三次贝塞尔曲线上的最近点?

Graphics 三次贝塞尔曲线上的最近点?,graphics,geometry,bezier,spline,curve,Graphics,Geometry,Bezier,Spline,Curve,如何沿三次贝塞尔曲线找到最接近平面中任意点p的点B(t)?经过大量搜索,我找到了一篇文章,讨论了在贝塞尔曲线上找到最接近给定点的方法: ,由 陈晓刁,尹舟,舒振宇, 华苏和让·克劳德·保罗 此外,我发现Sturm序列的定义和描述有助于理解算法的第一部分,因为论文本身的描述并不十分清楚。取决于您的容差。暴力和接受错误。这种算法在某些罕见的情况下可能是错误的。但是,在大多数情况下,它会找到一个非常接近正确答案的点,并且设置的切片越高,结果就会越好。它只是以固定的间隔尝试曲线上的每个点,然后返回找到的

如何沿三次贝塞尔曲线找到最接近平面中任意点p的点B(t)?

经过大量搜索,我找到了一篇文章,讨论了在贝塞尔曲线上找到最接近给定点的方法:

,由 陈晓刁,尹舟,舒振宇, 华苏和让·克劳德·保罗


此外,我发现Sturm序列的定义和描述有助于理解算法的第一部分,因为论文本身的描述并不十分清楚。

取决于您的容差。暴力和接受错误。这种算法在某些罕见的情况下可能是错误的。但是,在大多数情况下,它会找到一个非常接近正确答案的点,并且设置的切片越高,结果就会越好。它只是以固定的间隔尝试曲线上的每个点,然后返回找到的最佳点

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;
}
把方程换掉

虽然公认的答案是正确的,但你真的可以找出根源并进行比较。如果你真的只需要找到曲线上最近的点,这就可以了


关于本的评论。你不能在数百个控制点范围内简化公式,就像我对立方体和四边形所做的那样。因为每增加一条贝塞尔曲线所需要的数量意味着你要为它们建立一个毕达哥拉斯金字塔,而我们基本上要处理的是越来越多的庞大的数字串。对于四边形,是1,2,1,对于立方形,是1,3,3,1。你最终建立了越来越大的金字塔,并最终用Casteljau的算法将其分解(我为solid speed写了这篇文章):

将算法0 1 2 3作为控制点输入,然后在0、4、7、9和9、8、6、3处对两条完全细分的曲线进行索引。请特别注意,这些曲线在同一点开始和结束。最后的索引9(曲线上的点)用作另一个新的锚定点。鉴于此,您可以完美地细分贝塞尔曲线

然后,为了找到最近的点,您需要不断将曲线细分为不同的部分,注意贝塞尔曲线的整个曲线都包含在控制点的外壳内。也就是说,如果我们把点0,1,2,3变成一条连接0,3的闭合路径,那么曲线必须完全落在多边形外壳内。所以我们要做的是定义给定的点P,然后我们继续细分曲线,直到我们知道一条曲线的最远点比另一条曲线的最近点更近。我们只是将该点P与曲线的所有控制点和锚定点进行比较。并从活动列表中丢弃其最近点(锚定点或控制点)比另一条曲线的最远点更远的任何曲线。然后,我们细分所有活动曲线,然后再次执行此操作。最终,我们将有非常细分的曲线,丢弃大约一半的每一步(这意味着它应该是O(n logn)),直到我们的误差基本上可以忽略不计。在这一点上,我们将活动曲线称为离该点最近的点(可能有多个),并注意高度细分的曲线位中的误差基本上等于一个点。或者简单地说,两个锚定点中最接近的一个就是离我们的点P最近的点,我们知道误差的具体程度


然而,这需要我们有一个稳健的解决方案,并使用正确的算法,正确地找到曲线的一小部分,它肯定是离我们的点最近的点。它应该还是比较快的。

我已经编写了一些快速而肮脏的代码,对任何程度的贝塞尔曲线进行了估计。(注意:这是伪暴力,不是封闭形式的解决方案。)

演示:
/**找到贝塞尔曲线上与您提供的点最近的点。
*out:要修改为曲线上的点的向量
*曲线:表示Bézier曲线控制点的向量数组
*pt:你想要找到的点(向量)在附近
*tmps:临时向量数组(减少内存分配)
*返回:表示`out'位置的参数t`
*/
功能闭合点(输出、曲线、pt、tmps){
让mindex,scans=25;//更多的扫描->更大的正确率
const vec=vmath[0]曲线中的'w'?'vec4':'z'曲线[0]?'vec3':'vec2'];
for(设min=无穷大,i=scans+1;i--;){
设d2=向量平方距离(pt,bézierPoint(out,curve,i/scans,tmps));
if(d2向量平方距离(pt,bézierPoint(out,curve,t,tmps));
返回本地最小值(t0、t1、d2ForT、1e-4);
}
/**求一个有界函数的最小点。可以是局部最小值。
*minX:最小的输入值
*maxX:最大的输入值
*ƒ:给定一个'x'返回一个'y'值的函数`
*ε:返回之前,边界在'x'中必须有多近
*返回:产生最小'y'的'x'值`
*/
函数localMinimum(minX,maxX,ƒ,ε){
如果(ε===未定义)ε=1e-10;
设m=minX,n=maxX,k;
而((n-m)>ε){
k=(n+m)/2;
if(k-ε){vec.copy(pt,曲线[i]);
对于(变量阶数=曲线长度-1;阶数--;){

对于(var i=0;i,鉴于本页上的其他方法似乎是近似的,这个答案将提供一个简单的数值解。这是一个python实现,它依赖于
numpy
库来提供
Bezier
类。在我的测试中,这种方法的性能大约是我的蛮力实现的三倍(使用样本和细分)

看这张照片。

点击放大

我用基础代数来解决这个极小的问题


从贝塞尔曲线方程开始

B(t) = (1 - t)^3 * p0 + 3 * (1 - t)^2 * t * p1 + 3 * (1 - t) * t^2 * p2 + t^3 * p3
由于我使用numpy,我的点被表示为numpy向量(矩阵)。这意味着
p0
是一维的,例如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.
/**
 * 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;
}
   9
  7 8
 4 5 6
0 1 2 3
B(t) = (1 - t)^3 * p0 + 3 * (1 - t)^2 * t * p1 + 3 * (1 - t) * t^2 * p2 + t^3 * p3
dcoeffs = np.stack([da, db, dc, dd, de, df])
roots = np.roots(dcoeffs)
import numpy as np

# Bezier Class representing a CUBIC bezier defined by four
# control points.
# 
# at(t):            gets a point on the curve at t
# distance2(pt)      returns the closest distance^2 of
#                   pt and the curve
# closest(pt)       returns the point on the curve
#                   which is closest to pt
# maxes(pt)         plots the curve using matplotlib
class Bezier(object):
    exp3 = np.array([[3, 3], [2, 2], [1, 1], [0, 0]], dtype=np.float32)
    exp3_1 = np.array([[[3, 3], [2, 2], [1, 1], [0, 0]]], dtype=np.float32)
    exp4 = np.array([[4], [3], [2], [1], [0]], dtype=np.float32)
    boundaries = np.array([0, 1], dtype=np.float32)

    # Initialize the curve by assigning the control points.
    # Then create the coefficients.
    def __init__(self, points):
        assert isinstance(points, np.ndarray)
        assert points.dtype == np.float32
        self.points = points
        self.create_coefficients()
    
    # Create the coefficients of the bezier equation, bringing
    # the bezier in the form:
    # f(t) = a * t^3 + b * t^2 + c * t^1 + d
    #
    # The coefficients have the same dimensions as the control
    # points.
    def create_coefficients(self):
        points = self.points
        a = - points[0] + 3*points[1] - 3*points[2] + points[3]
        b = 3*points[0] - 6*points[1] + 3*points[2]
        c = -3*points[0] + 3*points[1]
        d = points[0]
        self.coeffs = np.stack([a, b, c, d]).reshape(-1, 4, 2)

    # Return a point on the curve at the parameter t.
    def at(self, t):
        if type(t) != np.ndarray:
            t = np.array(t)
        pts = self.coeffs * np.power(t, self.exp3_1)
        return np.sum(pts, axis = 1)

    # Return the closest DISTANCE (squared) between the point pt
    # and the curve.
    def distance2(self, pt):
        points, distances, index = self.measure_distance(pt)
        return distances[index]

    # Return the closest POINT between the point pt
    # and the curve.
    def closest(self, pt):
        points, distances, index = self.measure_distance(pt)
        return points[index]

    # Measure the distance^2 and closest point on the curve of 
    # the point pt and the curve. This is done in a few steps:
    # 1     Define the distance^2 depending on the pt. I am 
    #       using the squared distance because it is sufficient
    #       for comparing distances and doesn't have the over-
    #       head of an additional root operation.
    #       D(t) = (f(t) - pt)^2
    # 2     Get the roots of D'(t). These are the extremes of 
    #       D(t) and contain the closest points on the unclipped
    #       curve. Only keep the minima by checking if
    #       D''(roots) > 0 and discard imaginary roots.
    # 3     Calculate the distances of the pt to the minima as
    #       well as the start and end of the curve and return
    #       the index of the shortest distance.
    #
    # This desmos graph is a helpful visualization.
    # https://www.desmos.com/calculator/ktglugn1ya
    def measure_distance(self, pt):
        coeffs = self.coeffs

        # These are the coefficients of the derivatives d/dx and d/(d/dx).
        da = 6*np.sum(coeffs[0][0]*coeffs[0][0])
        db = 10*np.sum(coeffs[0][0]*coeffs[0][1])
        dc = 4*(np.sum(coeffs[0][1]*coeffs[0][1]) + 2*np.sum(coeffs[0][0]*coeffs[0][2]))
        dd = 6*(np.sum(coeffs[0][0]*(coeffs[0][3]-pt)) + np.sum(coeffs[0][1]*coeffs[0][2]))
        de = 2*(np.sum(coeffs[0][2]*coeffs[0][2])) + 4*np.sum(coeffs[0][1]*(coeffs[0][3]-pt))
        df = 2*np.sum(coeffs[0][2]*(coeffs[0][3]-pt))

        dda = 5*da
        ddb = 4*db
        ddc = 3*dc
        ddd = 2*dd
        dde = de
        dcoeffs = np.stack([da, db, dc, dd, de, df])
        ddcoeffs = np.stack([dda, ddb, ddc, ddd, dde]).reshape(-1, 1)
        
        # Calculate the real extremes, by getting the roots of the first
        # derivativ of the distance function.
        extrema = np_real_roots(dcoeffs)
        # Remove the roots which are out of bounds of the clipped range [0, 1].
        # [future reference] https://stackoverflow.com/questions/47100903/deleting-every-3rd-element-of-a-tensor-in-tensorflow
        dd_clip = (np.sum(ddcoeffs * np.power(extrema, self.exp4)) >= 0) & (extrema > 0) & (extrema < 1)
        minima = extrema[dd_clip]

        # Add the start and end position as possible positions.
        potentials = np.concatenate((minima, self.boundaries))

        # Calculate the points at the possible parameters t and 
        # get the index of the closest
        points = self.at(potentials.reshape(-1, 1, 1))
        distances = np.sum(np.square(points - pt), axis = 1)
        index = np.argmin(distances)

        return points, distances, index


    # Point the curve to a matplotlib figure.
    # maxes         ... the axes of a matplotlib figure
    def plot(self, maxes):
        import matplotlib.path as mpath
        import matplotlib.patches as mpatches
        Path = mpath.Path
        pp1 = mpatches.PathPatch(
            Path(self.points, [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]),
            fc="none")#, transform=ax.transData)
        pp1.set_alpha(1)
        pp1.set_color('#00cc00')
        pp1.set_fill(False)
        pp2 = mpatches.PathPatch(
            Path(self.points, [Path.MOVETO, Path.LINETO , Path.LINETO , Path.LINETO]),
            fc="none")#, transform=ax.transData)
        pp2.set_alpha(0.2)
        pp2.set_color('#666666')
        pp2.set_fill(False)

        maxes.scatter(*zip(*self.points), s=4, c=((0, 0.8, 1, 1), (0, 1, 0.5, 0.8), (0, 1, 0.5, 0.8),
                                                  (0, 0.8, 1, 1)))
        maxes.add_patch(pp2)
        maxes.add_patch(pp1)

# Wrapper around np.roots, but only returning real
# roots and ignoring imaginary results.
def np_real_roots(coefficients, EPSILON=1e-6):
    r = np.roots(coefficients)
    return r.real[abs(r.imag) < EPSILON]