1D Cubic Bezier Curve Fitting

How to apply the Least Squares method in details

1. Intention

This blog post goes through all the math behind fitting a one-dimensional cubic Bezier curve to a data set, using the least squares regression method.

There are multiple implementation of this algorithm in MathLab, Python, C++, etc… but they don’t cover the reasoning behind the formulas, which makes generalization to higher degree curves impossible for the non math savvy.

TL,DR : If you are coming back and just need the final answer, feel free to jump to the recap or check out the C# code snippet!

2. Bezier Curves Equation

A Bezier curve going through 4 control points \(P_0,P_1,P_2,P_3\) of any dimension can be expressed as:

$$Q\left(t\right)=\left(1-t\right)^3P_0+{3t\left(1-t\right)}^2P_1+\ 3t^2\left(1-t\right)P_2+\ t^3P_3$$

Noting \(B_k\left(t\right)\) as the Bernstein polynomial and \(C_3^k\) as the binomial coefficient for cubic Bezier curves, we get:

$$C_n^k=\frac{n!}{k!\left(n-k\right)!}$$

$$C_3^k=\frac{6}{k!\left(3-k\right)!}$$

$$B_k\left(t\right)={C_3^kt^k\left(1-t\right)}^{3-k}$$

We can rewrite the equation in a simpler form, which will simplify differentiation later:

$$\fbox{$Q\left(t\right)=B_0(t)P_0+B_1(t)P_1+\ B_2(t)P_2+\ B_3(t)P_3$}$$

3. One-dimensional cubic Bezier

The idea of 1D Bezier might sound counter intuitive at first (hard to think in one dimension), but it’s useful to compress any continuous signal, or a portion of it, creating easing curves for animations etc…

The smoothstep() function, used in graphics shading language amongst other things, can be interpreted as a 1D Bezier. $$smoothstep(t) = 3t^2 – 2t^3 $$ $$smoothstep(t) = 3t^2 – 3t^3 + t^3$$ $$smoothstep(t) = t^2(3 – 3t) + t^3$$ $$smoothstep(t) = 3t^2(1 – t) + t^3$$

Assuming the general form of a cubic Bezier equation:

$$Q\left(t\right)=\left(1-t\right)^3P_0+{3t\left(1-t\right)}^2P_1+\ 3t^2\left(1-t\right)P_2+\ t^3P_3$$

We can rewrite the smoothstep function as:

\( smoothstep(t) = Q(t)\) with \( P_0=0, P_1=0, P_2=1, P_3=1\)

For more information about 1D Bezier curve, I encourage you to read this Awesome blog from Alan Wolfe

4. Data set fitting with a 1D Bezier Curve

Let’s assume we have a data set in the form \(\\{\left(x_1,\ y_1\right),\ \left(x_2,\ y_2\right),\ \ldots\ \left(x_n,\ y_n\right)\\}\).

Let the following function be our fitting one dimensional cubic Bezier curve: $$T^\prime(x)=B_0(x)C_0+B_1(x)C_1+\ B_2(x)C_2+\ B_3(x)C_3$$

We can define the error function over all the samples as:

$$E(C_0{,C}_{1,}C_{2,}C_3)=\sum_{i=1}^{n}{(y_i-T^\prime(x_i))}^2$$

$$E(C_0{,C}_{1,}C_{2,}C_3)=\sum_{i=1}^{n}{(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3)}^2$$

The goal is to find the values for the constants \((C_0{,C}_{1,}C_{2,}C_3)\) that will minimize the error function defined above.

Although it’s not a requirement, in a lot of cases it is desirable that the fitting curve exactly goes through the first and last points. This is a feature of Bezier splines that make them so interesting when it comes to 3D paths, but also signal compression.

Let’s assume our output fitting curve goes through \(\left(x_1,\ y_1\right)\) and \(\left(x_n,\ y_n\right)\), the first and last sample points. We can deduce:

$$C_0=y_1, C_3=y_n$$

As we are dealing with multiple variables, minimizing the function requires us to find the values such as the following is true:

$$\frac{\partial E}{\partial C_0}=0,\ \ \frac{\partial E}{\partial C_1}=0,\ \ \frac{\partial E}{\partial C_2}=0,\ \ \frac{\partial E}{\partial C_3}=0$$

Since the sample series is continuous for \(x_i\) within the range \([0,1]\), we can safely differentiate and get the following:

$$\frac{\partial E}{\partial C_0}=\sum_{i=1}^{n}{\frac{\partial}{\partial C_0}\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right)}^2=0$$

$$\frac{\partial E}{\partial C_1}=\sum_{i=1}^{n}{\frac{\partial}{\partial C_1}\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right)^2}=0$$

$$\frac{\partial E}{\partial C_2}=\sum_{i=1}^{n}{\frac{\partial}{\partial C_2}\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right)^2}=0$$

$$\frac{\partial E}{\partial C_3}=\sum_{i=1}^{n}{\frac{\partial}{\partial C_3}(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3)}^2=0$$

Using the chain rule, we know that for 2 differentiable functions \(f(x)\) and \(g(x)\) , if we define:

$$F\left(x\right)=(f\ o\ g)(x)$$

then the derivative of \(F(x)\) is

$$F^\prime\left(x\right)=f^\prime(g\left(x\right)){\ g}^\prime(x)$$

in our case: $$f\left(x\right)=x^2$$ $$g\left(x\right)=y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3$$

Which gives:

$$\frac{\partial E}{\partial C_0}=\sum_{i=1}^{n}2\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right).\left(-B_0\left(x_i\right)\right)=0$$

$$\frac{\partial E}{\partial C_1}=\sum_{i=1}^{n}2\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right).\left(-B_1\left(x_i\right)\right)=0$$

$$\frac{\partial E}{\partial C_2}=\sum_{i=1}^{n}2\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right).\left(-B_2\left(x_i\right)\right)=0$$

$$\frac{\partial E}{\partial C_3}=\sum_{i=1}^{n}2\left(y_{i-\ }B_0\left(x_i\right)C_0-B_1\left(x_i\right)C_1-\ B_2\left(x_i\right)C_2-\ B_3\left(x_i\right)C_3\right).\left(-B_3\left(x_i\right)\right)=0$$

We can divide both sides by 2, and drop the – sign on the \(\left(-B_n\left(x_i\right)\right)\), then factor them into get a solvable system of equation:

$$\small{(1)\sum_{i=1}^{n}{{B_0\left(x_i\right)y}_{i\ }=\sum_{i=1}^{n}{{B_0}^2\left(x_i\right)C_0}+\sum_{i=1}^{n}{{B_0\left(x_i\right)B}_1\left(x_i\right)C_1}+\sum_{i=1}^{n}{{B_0\left(x_i\right)B}_2\left(x_i\right)C_2}+\ \sum_{i=1}^{n}{{B_0\left(x_i\right)B}_3\left(x_i\right)C_3}}}$$

$$\small{(2)\sum_{i=1}^{n}{{B_1\left(x_i\right)y}_{i\ }=\sum_{i=1}^{n}{B_0\left(x_i\right)B_1\left(x_i\right)C_0}+\sum_{i=1}^{n}{{B_1}^2\left(x_i\right)C_1}+\ \sum_{i=1}^{n}{{B_1\left(x_i\right)B}_2\left(x_i\right)C_2}+\ \sum_{i=1}^{n}{{B_1\left(x_i\right)B}_3\left(x_i\right)C_3}}}$$

$$\small{(3)\sum_{i=1}^{n}{{B_2\left(x_i\right)y}_{i\ }=\sum_{i=1}^{n}{B_0\left(x_i\right)B_2\left(x_i\right)C_0}+\sum_{i=1}^{n}{B_1{\left(x_i\right)B}_2\left(x_i\right)C_1}+\ \sum_{i=1}^{n}{{B_2}^2\left(x_i\right)C_2}+\ \sum_{i=1}^{n}{{B_2\left(x_i\right)B}_3\left(x_i\right)C_3}}}$$

$$\small{(4)\sum_{i=1}^{n}{{B_3\left(x_i\right)y}_{i\ }=\sum_{i=1}^{n}{B_0{\left(x_i\right)B}_3\left(x_i\right)C_0}+\sum_{i=1}^{n}{B_1{\left(x_i\right)B}_3\left(x_i\right)C_1}+\ \sum_{i=1}^{n}{B_2{\left(x_i\right)B}_3\left(x_i\right)C_2}+\ \sum_{i=1}^{n}{{B_3}^2\left(x_i\right)C_3}}}$$

Since we already know \(C_0\) and \(C_3\), we just need to figure out \(C_1\) and \(C_2\). We can focus on line 2 and 3 above, and reorder this way:

$$C_1\sum_{i=1}^{n}{{B_1}^2\left(x_i\right)}+\ C_2\sum_{i=1}^{n}{{B_1\left(x_i\right)B}_2\left(x_i\right)}=\sum_{i=1}^{n}B_1(x_i)[y_i -B_0(x_i)C_0- B_3(x_i)C_3]$$

$$C_1\sum_{i=1}^{n}{B_1{\left(x_i\right)B}_2\left(x_i\right)}+\ C_2\sum_{i=1}^{n}{{B_2}^2\left(x_i\right)}=\sum_{i=1}^{n}{{B_2\left(x_i\right)[y}_{i\ }-B_0\left(x_i\right)C_0-B_3\left(x_i\right)C_3}]$$

All the sums above can be computed by looping through the samples and accumulating into different variables. If we name these variables the following way:

$$a=\sum_{i=1}^{n}{{B_1}^2\left(x_i\right)},\ \ b=\ \sum_{i=1}^{n}{{B_1\left(x_i\right)B}_2\left(x_i\right)},\ \ c=\sum_{i=1}^{n}B_1(x_i)[y_i -B_0(x_i)C_0- B_3(x_i)C_3]$$

$$e=\sum_{i=1}^{n}{{B_2}^2\left(x_i\right)},\ \ f=\sum_{i=1}^{n}{{B_2\left(x_i\right)[y}_{i\ }-B_0\left(x_i\right)C_0-B_3\left(x_i\right)C_3}]$$

We can rewrite our equations:

$$\binom{aC_1+bC_2}{bC_1+eC_2}=\binom{c}{f}$$

Multiplying first line by \(e\) and the second line by \(b\) we get:

$$\binom{aeC_1+b{eC}_2}{b^2C_1+beC_2}=\binom{ce}{bf}$$

We can then remove the second line from the first to get \(C_1\)

$$aeC_1-\ b^2C_1=ce-bf$$

$$C_1(ae-\ b^2)=ce-bf$$

$$\fbox{$C_1=\frac{ce-bf}{(ae-\ b^2)}$}$$

Using the same logic, we can solve for \(C_2\)

$$\binom{abC_1+b^2C_2}{abC_1+aeC_2}=\binom{bc}{af}$$

$$aeC_2{-b}^2C_2=af-cb$$

$$\fbox{$C_2=\frac{af-bc}{(ae-\ b^2)}$}$$

5. Recap

$$a=\sum_{i=1}^{n}{{B_1}^2\left(x_i\right)},\ \ b=\ \sum_{i=1}^{n}{{B_1\left(x_i\right)B}_2\left(x_i\right)},\ \ c=\sum_{i=1}^{n}B_1x_i[y_i -B_0x_iC_0- B_3x_iC_3]$$

$$e=\sum_{i=1}^{n}{{B_2}^2\left(x_i\right)},\ \ f=\sum_{i=1}^{n}{{B_2\left(x_i\right)[y}_{i\ }-B_0\left(x_i\right)C_0-B_3\left(x_i\right)C_3}]$$

$$C_0=y_1, C_3=y_n$$

$$\fbox{$C_1=\frac{ce-bf}{(ae-\ b^2)}$}$$ $$\fbox{$C_2=\frac{af-bc}{(ae-\ b^2)}$}$$

6. Unity C# Implementation

Below is a C# implementation for Unity, that fits a one-dimensional cubic Bezier to a list of \(n\) Vector2 samples \(\\{\left(x_1,\ y_1\right),\ \left(x_2,\ y_2\right),\ \ldots\ \left(x_n,\ y_n\right)\\}\). We assume that \(x_1=0\) and \(x_n=1\), in order for the resulting fitting curve to go through the first and last samples (at \(t=0\) and \(t=1\) respectively).

We also assume that the samples array contains at least 4 values, and for each Vector2 entry, the \(.x\) value corresponds to a normalized \(t\) within \([0,1]\), and the \(.y\) value corresponds to the actual value for that \(t\).When the data set can be sampled at a fixed frequency, the input \(t\) values (\(x\) component of each Vector2 entry) can be filled in as: $$ \{0, \frac{1}{(n-1)}, \frac{2}{(n-1)}, … ,1\}$$

public static void LeastSquareFit_Bezier(Vector2[] samples,
                                   out float c0,
                                   out float c1,
                                   out float c2,
                                   out float c3)
{
    // C0 and C1 are set right away, since the Bezier
    // goes through them
    double C0 = samples[0].y;
    double C3 = samples[samples.Length-1].y;

    // B0 = (1-x)³
    // B1 = 3x(1-x)²
    // B2 = 3x²(1-x)
    // B3 = x³
    //
    // B1² = 9x²(1-x)^4
    // B2² = 9x^4(1-x)²
    // B1 x B2 => 9x³(1-x)³
    //
    // a = SUM(i, 1, n) [ B1(x)² ]
    // b = SUM(i, 1, n) [ B1(x)B2(x) ]
    // c = SUM(i, 1, n) [ B1(x)(y - B0(x)C0 - B3(x)C3) ]
    // e = SUM(i, 1, n) [ B2(x)² ]
    // f = SUM(i, 1, n) [ B2(x)(y - B0(x)C0 - B3(x)C3)) ]
    //
    double a = 0, b = 0, c = 0, e = 0, f = 0;
    for (int i = 0; i < samples.Length; ++i)
    {
        double x = samples[i].x;
        double x_pow2 = x * x;
        double x_pow3 = x_pow2 * x;
        double x_pow4 = x_pow3 * x;
        double one_minus_x = 1 - x;
        double one_minus_x_pow2 = one_minus_x * one_minus_x;
        double one_minus_x_pow3 = one_minus_x_pow2 * one_minus_x;
        double one_minus_x_pow4 = one_minus_x_pow3 * one_minus_x;

        // Update SUM(i, 1, n) [ B1(x)² ]
        a += 9 * x_pow2 * one_minus_x_pow4;

        // Update SUM(i, 1, n) [ B2(x)² ]
        e += 9 * x_pow4 * one_minus_x_pow2;

        // Update SUM(i, 1, n) [ B1(x)B2(x) ]
        b += 9 * x_pow3 * one_minus_x_pow3;

        double r = (samples[i].y - one_minus_x_pow3 * C0 - x_pow3 * C3);

        // Update SUM(i, 1, n) [ B1(x)(y - B0(x)C0 - B3(x)C3) ]
        c += 3 * x * one_minus_x_pow2 * r;

        // Update SUM(i, 1, n) [ B2(x)(y - B0(x)C0 - B3(x)C3)) ]
        f += 3 * x_pow2 * one_minus_x * r;    
    }
    // the easy ones we already know
    c0 = (float)C0;
    c3 = (float)C3;

    // Solve for the other 2
    double denom = (a * e - b * b);
    c1 = (float)((c * e - b * f) / denom);
    c2 = (float)((a * f - b * c) / denom);
}

7. Closing notes

Hope you found this blog interesting! I might continue on this topic one day in a future blog entry, exploring the concrete case of fitting one-dimensional Bezier to arc-length reparameterization curves, in order to optimize speed and storage for 3D Bezier paths, with the added benefit of getting increased precision, compared to typical solutions like splitting the curve in \(n\) segments of length \(l\).