如何找到最接近平面中任意点P的三次贝塞尔曲线上的点B(t)?
答案 0 :(得分:17)
经过大量的搜索后,我发现了一篇论文,讨论了在贝塞尔曲线上找到最近点到给定点的方法:
Improved Algebraic Algorithm On Point Projection For Bezier Curves,by 陈小貂,周寅,舒振宇, 华苏和让 - 克劳德保罗。
此外,我发现Sturm序列的Wikipedia和MathWorld's描述对于理解算法的第一部分很有用,因为论文本身在其描述中并不十分清楚。
答案 1 :(得分:11)
我写了一些快速而肮脏的代码,可以估算出任何程度的Bézier曲线。 (注意:这是伪暴力,而不是封闭形式的解决方案。)
/** 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 library来有效地在向量之间进行缩放(在2D,3D或4D中),但用lerp()
替换bézierPoint()
中的closestPoint()
调用是微不足道的。自己的代码。
localMinimum()
功能分为两个阶段:
scans
函数搜索最小距离周围的区域,使用二进制搜索找到 t 和产生真实最小距离的点。 closestPoint()
中ε
的值决定了第一遍中使用的样本数量。更少的扫描速度更快,但增加了错过真正最小点的机会。
传递给localMinimum()
函数的1e-2
限制会控制它继续寻找最佳值的时间。值closestPoint()
将曲线量化为~100个点,因此您可以看到从1e-3
返回的点沿线弹出。每增加一个小数点精确度 - 1e-4
,bézierPoint()
,...... - 大约有6到8个额外的<script type="text/javascript">
<?php
$latest = 1;
include_once 'dbh.inc.php';
$sql = "SELECT * FROM videos";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
while ($row = mysqli_fetch_assoc($result)) {
echo '
$("' . $row['vid_class'] . '").click(function() {
$(".video-display iframe").attr("src", "' . $row['vid_link'] . '");
$(".video-display").toggle();
});';
}
}
?>
</script>
来电。
答案 2 :(得分:9)
取决于您的公差。蛮力和接受错误。对于一些罕见的情况,该算法可能是错误的。但是,在大多数情况下,它会找到一个非常接近正确答案的点,结果会提高您设置切片的位置。它只是定期尝试沿曲线的每个点,并返回它找到的最佳点。
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.
通过在那里切换等式。
虽然接受的答案是正确的,但你真的可以找出根源并比较那些东西。如果你真的只需要在曲线上找到最近的点,那就可以了。
关于Ben的评论。你不能在数百个控制点范围内缩短公式,就像我对立方和四元形式所做的那样。因为每次添加贝塞尔曲线所需的数量意味着你为他们建造毕达哥拉斯金字塔,而我们基本上处理的是更多更庞大的数字串。对于四边形你去1,2,1,对于立方你去1,3,3,1。你最终建造越来越大的金字塔,并最终用Casteljau算法打破它(我为此写了这个速度稳定):
/**
* 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,但你还需要它来执行实际和正确的Bezier细分算法(还有其他算法,但这就是我推荐),不仅要计算一个近似值,而是将其除以线段,而不是实际曲线。或者更确切地说是确定包含曲线的多边形船体。
您可以使用上述算法细分给定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的闭合路径,则必须完全落入该多边形船体内。所以我们做的是定义给定的点P,然后我们继续细分曲线,直到我们知道一条曲线的最远点比另一条曲线的最近点更近。我们只需将此点P与曲线的所有控制点和锚点进行比较。并且从我们的活动列表中丢弃任何曲线,其最近点(无论是锚点还是控制点)远离另一条曲线的最远点。然后我们细分所有活动曲线并再次执行此操作。最终我们将有非常细分的曲线丢弃大约每一步的一半(意味着它应该是O(n log n)),直到我们的错误基本上可以忽略不计。此时我们将活动曲线称为该点的最近点(可能有多个),并注意曲线的高度细分位中的误差基本上等于一个点。或者简单地通过说出两个锚点中最接近的点是我们的点P的最近点来确定问题。我们知道错误到非常特定的程度。
但是,这要求我们实际上有一个强大的解决方案,并且做一个肯定正确的算法,并正确找到一小部分曲线,它肯定是我们观点的最接近点。它应该相对较快。
答案 3 :(得分:0)
由于该页面上的其他方法似乎是近似的,因此此答案将提供一个简单的数值解决方案。这是一个Python实现,具体取决于numpy
库来提供Bezier
类。在我的测试中,这种方法的性能要比蛮力实施(使用示例和细分)高出三倍。
看看interactive example here。
点击放大。
我用基本代数解决了这个最小的问题。
从贝塞尔曲线方程开始。
B(t) = (1 - t)^3 * p0 + 3 * (1 - t)^2 * t * p1 + 3 * (1 - t) * t^2 * p2 + t^3 * p3
由于我使用的是numpy,所以我的点表示为numpy向量(矩阵)。这意味着p0
是一维的,例如(1, 4.2)
。如果要处理两个浮点变量,则可能需要多重方程式(对于x
和y
):Bx(t) = (1-t)^3*px_0 + ...
将其转换为具有四个系数的标准形式。
您可以通过扩展原始方程式来写出四个系数。
从点 p 到曲线 B(t) 的距离可以使用勾股法计算定理。
此处 a 和 b 是我们的点x
和{{ 1}}。这意味着 D(t) 的平方距离是:
我现在不算平方根,因为如果我们比较相对平方距离就足够了。以下所有等式均指平方距离。
此函数 D(t) 描述图形与点之间的距离。我们对y
范围内的最小值感兴趣。要找到它们,我们必须两次导出函数。距离函数的一阶导数是5阶多项式:
二阶导数是:
desmos图让我们检查一下不同的功能。
D(t) 具有其局部最小值,其中 d'(t)= 0 和 d''(t)> = 0 。这意味着,我们必须为 d'(t)= 0'找到 t 。
黑色 :贝塞尔曲线,绿色:d(t),紫色:d '(t),红色:d''(t)
找到 d'(t)的根。我使用numpy库,该库采用多项式的系数。
t in [0, 1]
删除虚根(仅保留实根),并删除dcoeffs = np.stack([da, db, dc, dd, de, df])
roots = np.roots(dcoeffs)
或< 0
的任何根。如果使用三次方贝塞尔曲线,则可能剩下约0-3个根。
接下来,为每个> 1
检查每个|B(t) - pt|
的距离。还应检查t in roots
和B(0)
的距离,因为Bezier曲线的起点和终点可能是最接近的点(尽管它们不是距离函数的局部最小值)。
返回最接近的点。
我在python中附加了Bezier的类。查看github link中的用法示例。
B(1)
答案 4 :(得分:0)
Mike Bostock的最接近点算法还具有 DOM SVG特定实现: