Bezier和b样条弧长算法给我带来了问题

时间:2014-10-19 23:16:02

标签: swift bezier numerical-integration

我在计算贝塞尔曲线和b样条曲线的弧长时遇到了一些问题。几天来,我一直在反对这一点,我想我几乎就在那里,但似乎无法让它完全正确。我在Swift中开发,但我认为它的语法很清楚,任何知道C / C ++的人都能读懂它。如果没有,请告诉我,我会尝试将其翻译成C / C ++。

我已经一遍又一遍地检查了我对几个来源的实现,并且,就算法而言,它们似乎是正确的,尽管我对B样条算法不太确定。一些教程使用学位,有些使用曲线的顺序计算,我真的很困惑。另外,在使用Gauss-Legendre积分时,我理解我应该总结跨度的积分,但我不确定我是否理解如何正确地做到这一点。根据我的理解,我应该在每个结跨度上进行整合。这是对的吗?

当我使用以下控制多边形计算贝塞尔曲线的长度时,我得到28.2842712474619,而3D软件(Cinema 4D和Maya)告诉我长度应为30.871。

let bezierControlPoints = [
    Vector(-10.0, -10.0),
    Vector(0.0, -10.0),
    Vector(0.0, 10.0),
    Vector(10.0, 10.0)
]

b样条的长度同样关闭。我的算法产生5.6062782185353,而它应该是7.437。

let splineControlPoints = [
    Vector(-2.0, -1.0),
    Vector(-1.0, 1.0),
    Vector(-0.25, 1.0),
    Vector(0.25, -1.0),
    Vector(1.0, -1.0),
    Vector(2.0, 1.0)
]

我不是数学家,所以我在数学方面苦苦挣扎,但我认为我有它的主旨。

Vector类非常简单,但我为了方便/易读而使一些操作符超载,这使得代码非常冗长,所以我不在这里发布它。我还没有包括Gauss-Legendre重量和横坐标。您可以从here(53K)下载源代码和Xcode项目。

这是我的bezier曲线课程:

class Bezier
{
    var c0:Vector
    var c1:Vector
    var c2:Vector
    var c3:Vector

    init(ic0 _ic0:Vector, ic1 _ic1:Vector, ic2 _ic2:Vector, ic3 _ic3:Vector) {
        c0 = _ic0
        c1 = _ic1
        c2 = _ic2
        c3 = _ic3
    }

    // Calculate curve length using Gauss-Legendre quadrature
    func curveLength()->Double {
        let gl = GaussLegendre()
        gl.order = 3    // Good enough for a quadratic polynomial
        let xprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dx(atTime:t) })
        let yprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dy(atTime:t) })

        return sqrt(xprime*xprime + yprime*yprime)
    }

    // I could vectorize this, but correctness > efficiency

    // The derivative of the x-component
    func dx(atTime t:Double)->Double {
        let tc = (1.0-t)
        let r0 = (3.0 * tc*tc) * (c1.x - c0.x)
        let r1 = (6.0 * tc*t) * (c2.x - c1.x)
        let r2 = (3.0 * t*t) * (c3.x - c2.x)
        return r0 + r1 + r2
    }

    // The derivative of the y-component
    func dy(atTime t:Double)->Double {
        let tc = (1.0-t)
        let r0 = (3.0 * tc*tc) * (c1.y - c0.y)
        let r1 = (6.0 * tc*t) * (c2.y - c1.y)
        let r2 = (3.0 * t*t) * (c3.y - c2.y)
        return r0 + r1 + r2
    }
}

这是我的b-spline类:

class BSpline
{
    var spanLengths:[Double]! = nil
    var totalLength:Double = 0.0
    var cp:[Vector]
    var knots:[Double]! = nil
    var o:Int = 4

    init(controlPoints:[Vector]) {
        cp = controlPoints
        calcKnots()
    }

    // Method to return length of the curve using Gauss-Legendre numerical integration
    func cacheSpanLengths() {
        spanLengths = [Double]()

        totalLength = 0.0

        let gl = GaussLegendre()
        gl.order = o-1  // The derivative should be quadratic, so o-2 would suffice?

        // Am I doing this right? Piece-wise integration?
        for i in o-1 ..< knots.count-o {
            let t0 = knots[i]
            let t1 = knots[i+1]
            let xprime = gl.integrate(a:t0, b:t1, closure:self.dx)
            let yprime = gl.integrate(a:t0, b:t1, closure:self.dy)
            let spanLength = sqrt(xprime*xprime + yprime*yprime)
            spanLengths.append(spanLength)
            totalLength += spanLength
        }
    }

    // The b-spline basis function
    func basis(i:Int, _ k:Int, _ x:Double)->Double {
        var r:Double = 0.0
        switch k {
            case 0:
                if (knots[i] <= x) && (x <= knots[i+1]) {
                    r = 1.0
                } else {
                    r = 0.0
                }
            default:
                var n0 = x - knots[i]
                var d0 = knots[i+k]-knots[i]
                var b0 = basis(i,k-1,x)

                var n1 = knots[i+k+1] - x
                var d1 = knots[i+k+1]-knots[i+1]
                var b1 = basis(i+1,k-1,x)

                var left = Double(0.0)
                var right = Double(0.0)
                if b0 != 0 && d0 != 0 { left = n0 * b0 / d0 }
                if b1 != 0 && d1 != 0 { right = n1 * b1 / d1 }
                r = left + right
        }
        return r
    }

    // Method to calculate and store the knot vector
    func calcKnots() {

        // The number of knots in the knot vector = number of control points + order (i.e. degree + 1)
        let knotCount = cp.count + o

        knots = [Double]()

        // For an open b-spline where the ends are incident on the first and last control points,
        // the first o knots are the same and the last o knots are the same, where o is the order
        // of the curve.
        var k = 0
        for i in 0 ..< o {
            knots.append(0.0)
        }
        for i in o ..< cp.count {
            k++
            knots.append(Double(k))
        }
        k++
        for i in cp.count ..< knotCount {
            knots.append(Double(k))
        }
    }

    // I could vectorize this, but correctness > efficiency

    // Derivative of the x-component
    func dx(t:Double)->Double {
        var p = Double(0.0)

        let n = o
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].x - cp[i].x) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }

    // Derivative of the y-component
    func dy(t:Double)->Double {
        var p = Double(0.0)

        let n = o
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].y - cp[i].y) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }
}

这是我的高斯 - 勒让德实施:

class GaussLegendre
{
    var order:Int = 5

    init() {
    }

    // Numerical integration of arbitrary function
    func integrate(a _a:Double, b _b:Double, closure f:(Double)->Double)->Double {
        var result = 0.0
        let wgts = gl_weights[order-2]
        let absc = gl_abscissae[order-2]
        for i in 0..<order {
            let a0 = absc[i]
            let w0 = wgts[i]
            result += w0 * f(0.5 * (_b + _a + a0 * (_b - _a)))
        }
        return 0.5 * (_b - _a) * result
    }
}

我的主要逻辑是:

let bezierControlPoints = [
    Vector(-10.0, -10.0),
    Vector(0.0, -10.0),
    Vector(0.0, 10.0),
    Vector(10.0, 10.0)
]

let splineControlPoints = [
    Vector(-2.0, -1.0),
    Vector(-1.0, 1.0),
    Vector(-0.25, 1.0),
    Vector(0.25, -1.0),
    Vector(1.0, -1.0),
    Vector(2.0, 1.0)
]

var bezier = Bezier(controlPoints:bezierControlPoints)
println("Bezier curve length: \(bezier.curveLength())\n")

var spline:BSpline = BSpline(controlPoints:splineControlPoints)
spline.cacheSpanLengths()
println("B-Spline curve length: \(spline.totalLength)\n")



更新:问题(部分)已解决

感谢Mike的回答!

我验证了我正确地将数值积分从间隔a..b重新映射到-1..1,以用于勒让德 - 高斯求积分。数学是here(对任何真正的数学家道歉,这是我用我长期遗忘的微积分所做的最好的事情)。

我建议将勒让德 - 高斯积分的阶数从5增加到32。

然后经过Mathematica的大量挣扎后,我回来重新阅读Mike的代码,发现我的代码 NOT 等同于他。

我取得了导数分量的平方积分之和的平方根:

WRONG

当我应该采用导数向量的积分时:

RIGHT

在代码方面,在我的Bezier类中,而不是:

// INCORRECT
func curveLength()->Double {
    let gl = GaussLegendre()
    gl.order = 3    // Good enough for a quadratic polynomial
    let xprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dx(atTime:t) })
    let yprime = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in return self.dy(atTime:t) })

    return sqrt(xprime*xprime + yprime*yprime)
}

我应该写这个:

// CORRECT
func curveLength()->Double {
    let gl = GaussLegendre()
    gl.order = 32
    return = gl.integrate(a:0.0, b:1.0, closure:{ (t:Double)->Double in
        let x = self.dx(atTime:t)
        let y = self.dy(atTime:t)
        return sqrt(x*x + y*y)
    })
}

我的代码计算弧长为:3.59835872777095
Mathematica:3.598358727834686

所以,我的结果非常接近。有趣的是,我的测试贝塞尔曲线的Mathematica图与Cinema 4D渲染的图之间存在差异,这可以解释为什么Mathematica和Cinema 4D计算的弧长也不同。不过,我认为我相信Mathematica会更正确。

在我的B-Spline课程中,而不是:

// INCORRECT
func cacheSpanLengths() {
    spanLengths = [Double]()

    totalLength = 0.0

    let gl = GaussLegendre()
    gl.order = o-1  // The derivative should be quadratic, so o-2 would suffice?

    // Am I doing this right? Piece-wise integration?
    for i in o-1 ..< knots.count-o {
        let t0 = knots[i]
        let t1 = knots[i+1]
        let xprime = gl.integrate(a:t0, b:t1, closure:self.dx)
        let yprime = gl.integrate(a:t0, b:t1, closure:self.dy)
        let spanLength = sqrt(xprime*xprime + yprime*yprime)
        spanLengths.append(spanLength)
        totalLength += spanLength
    }
}

我应该写这个:

// CORRECT
func cacheSpanLengths() {
    spanLengths = [Double]()

    totalLength = 0.0

    let gl = GaussLegendre()
    gl.order = 32

    // Am I doing this right? Piece-wise integration?
    for i in o-1 ..< knots.count-o {
        let t0 = knots[i]
        let t1 = knots[i+1]
        let spanLength = gl.integrate(a:t0, b:t1, closure:{ (t:Double)->Double in
            let x = self.dx(atTime:t)
            let y = self.dy(atTime:t)
            return sqrt(x*x + y*y)
        })
        spanLengths.append(spanLength)
        totalLength += spanLength
    }
}

不幸的是,B-Spline数学并不是那么直截了当,我还没有像Bezier数学那样在Mathematica中测试它,所以我不能完全确定我的代码是工作,即使有上述变化。我在验证时会发布另一个更新。



更新2:已解决的问题

Eureka,我在代码中发现了一个错误的计算B-Spline导数的错误。

而不是

    // Derivative of the x-component
    func dx(t:Double)->Double {
        var p = Double(0.0)

        let n = o // INCORRECT (should be one less)
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].x - cp[i].x) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }

    // Derivative of the y-component
    func dy(t:Double)->Double {
        var p = Double(0.0)

        let n = o // INCORRECT (should be one less_
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].y - cp[i].y) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }

我应该写

    // Derivative of the x-component
    func dx(t:Double)->Double {
        var p = Double(0.0)

        let n = o-1 // CORRECT
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].x - cp[i].x) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }

    // Derivative of the y-component
    func dy(t:Double)->Double {
        var p = Double(0.0)

        let n = o-1 // CORRECT
        for i in 0 ..< cp.count-1 {
            let u0 = knots[i + n + 1]
            let u1 = knots[i + 1]
            let fn = Double(n) / (u0 - u1)
            let thePoint = (cp[i+1].y - cp[i].y) * fn
            let b = basis(i+1, n-1, Double(t))
            p += thePoint * b
        }

        return Double(p)
    }

我的代码现在计算B样条曲线的长度为6.87309971722132。 Mathematica:6.87309884638438。

它可能在科学上并不精确,但对我来说足够好。

1 个答案:

答案 0 :(得分:2)

Legendre-Gauss程序专门为区间[-1,1]定义,而Beziers和B-Splines在[0,1]上定义,所以这是一个简单的转换,至少在你尝试时确保你的代码做正确的事情,轻松烘焙而不是提供动态间隔(正如你所说,准确性超过效率。一旦它工作,我们可以担心优化)

所以,给定权重W和横向A(两者长度相同n),您可以这样做:

z = 0.5
for i in 1..n
  w = W[i]
  a = A[i]
  t = z * a + z
  sum += w * arcfn(t, xpoints, ypoints)
return z * sum

伪代码假设列表从1开始索引。arcfn将被定义为:

arcfn(t, xpoints, ypoints):
  x = derive(xpoints, t)
  y = derive(ypoints, t)
  c = x*x + y*y
  return sqrt(c)

但那部分看起来已经是正确的了。

你的衍生物看起来也是正确的,所以主要问题是:“你在勒让德高斯求积法中使用了足够多的切片吗?”。您的代码建议您仅使用5个切片,这远远不足以获得良好的结果。使用http://pomax.github.io/bezierinfo/legendre-gauss.html作为术语数据,通常需要n为16或更高的集合(对于三次贝塞尔曲线,24通常是安全的,但对于具有尖点或大量变形的曲线仍然不太合适)。 / p>

我建议在这里采用“单元测试”方法:测试bezier和bspline代码(单独)以获取已知的基本值和派生值。退房吗?排除了一个问题。在您的LG代码上:如果您使用以下参数函数对直线执行Legendre-Gauss:

fx(t) = t
fy(t) = t
fx'(t) = 1
fy'(t) = 1

超过间隔t = [0,1],我们知道长度应该正好 2的平方根,并且导数是最简单的。如果这些有效,请使用以下方法进行非线性测试:

fx(t) = sin(t)
fy(t) = cos(t)
fx'(t) = cos(t)
fy'(t) = -sin(t)

过度间隔t = [0,1];我们知道长度应该正好 1.您的LG实现是否会产生正确的值?排除了另一个问题。如果没有,检查你的重量和横纹。它们是否与链接页面中的那些匹配(使用可验证的正确Mathematica程序生成,所以几乎保证是正确的)?你用的是足够的切片吗?将数字提高到10,16,24,32;增加切片数量将显示稳定求和,其中添加更多切片不会在第2,第3,第4,第5等小数点之前更改数字,因为您增加了计数。

您正在测试的曲线是否是有问题的曲线?绘制它们,它们是否有尖点或许多变形?对于LG来说,这将是一个问题,尝试更简单的曲线,看看你回来的价值是否至少是正确的。

最后,检查您的类型:您使用的是精度最高的数据类型吗?当使用合理数量的切片进行LG时,32位浮点数将会出现神秘消失的FPU以及我们需要使用的值的奇妙舍入误差。