A formula for the area enclosed by a planar Bezier curve of order n

mathematics
Here I present a closed-form expression for the area enclosed by an arbitrary order closed Bezier curve in the plane. The formula in itself is of limited interest given that you can trivially approximate any parametrized closed curve by a polygon and compute the polygon's area instead (say with the shoelace formula), but as always in maths, the real treasure is the friends we made along the way! The proof is a bit involved, but all we need is some basic vector calculus, combinatorics and some knowledge about the Beta function.

I solved this problem in 2019 because I couldn’t find a definite answer to it after a good deal of Internet research, but surely I can’t be the first one to tackle it. I don’t quite remember why I needed this, I think I went on a side quest when designing custom variable capacitor plates or something like that, which lead nowhere. But here you go.

The problem

Say we have a closed planar Bezier curve C described by n+1 control points Pi with i0,n. The curve being closed, we have the additional constraint that P0=Pn. Let t[0,1] be the curve parameter such that any point on the curve can be expressed as:

(1)g(t)=i=0n(ni)(1t)nitiPi

The curve C=Ω encloses a region Ω of the plane. We’re looking for an expression for the area AΩ of this region Ω.

Strategy

We’ll use a local tangent frame running along the Bezier curve together with an ad hoc vector field, to construct an integral expression of the area thanks to the divergence theorem. Then we’ll actually compute the integral to obtain a closed-form expression involving the curve’s parameters.

Maybe there’s a simpler way and I’m too braindead to notice, but that’s the path I chose.

Prerequisites

As mentioned above, we’ll need to use a fundamental result in vector calculus known as the divergence theorem. I don’t want to go into too much detail about this, you’re free to have a look at the Wikipedia article if you need a more rigorous approach, or completely skip this part if you’re already familiar with that stuff. But know that what I show here is just the tip of the iceberg!

However, do read the subsection on the Beta function, because we derive some useful equalities there.

Green’s theorem in 2D

Let Ω be a region in the plane, delimited by a simple closed curve C=Ω that is positively oriented and piecewise smooth. Then, for any function M and N with continuous partial derivatives and whose domains contain Ω, we have:

(2)ΩMdx+Ndy=Ω(NxMy)dxdy

The divergence theorem in 2D

Let ds=dx2+dy2 be a line element, and n^ the unit vector:

(3)n^=dyu^xdxu^ydx2+dy2=dyu^xdxu^yds

Let F(x,y)=P(x,y)u^x+Q(x,y)u^y be a vector field. Then:

(4)ΩFn^ds=ΩF(dyu^xdxu^y)=Ω(P(x,y)dyQ(x,y)dx)

By definition of F and n^. Using 2 we get:

(5)ΩFn^ds=Ω(Px+Qx)dxdy=ΩFdA

This last equation equates the circulation integral of a vector field around a closed curve to the flux of the divergence of the vector field through the surface enclosed by the curve. This very powerful result that is central in our proof is called the divergence theorem or Ostrogradsky’s theorem.

The Beta function

The Euler integral of the first kind (aka Beta function) is a hidden gem of mathematics, often overshadowed by its omnipresent sibling the Gamma function. It admits two complex arguments, has an integral expression, and can be expressed as a ratio of Gamma functions:

(6)B(x,y)01tx1(1t)y1dt=Γ(x)Γ(y)Γ(x+y)x,yC(x)>0,(y)>0

The Beta function has the following useful properties:

(7)B(x+1,y)=B(x,y)xx+yx,yC(8)B(x,y+1)=B(x,y)yx+yx,yC(9)B(nk+1,k+1)=1(n+1)(nk)(n,k)N×N

We can combine these equalities to obtain two new equalities which we’ll use later on:

(10)B(nk+1,k)=B(nk+1,k+1)n+1k=n+1k(n+1)(nk)=1k(nk)(n,k)N×N (11)B(nk,k+1)=B(nk+1,k+1)n+1nk=n+1(nk)(n+1)(nk)=1(nk)(nk)(n,k)N×N

Solution

Let (τ^,n^) represent a local basis along the curve C, τ^(t) being the unit vector tangent to the curve at parameter value t, and n^(t) the normal unit vector.

The local basis vectors and the Bezier curve with its control points

Note that we can obtain the tangent vector easily by a +π2 CCW rotation. Let R=[0110] be the corresponding rotation matrix. Then we have:

(12)τ^(t)=gt (13)n^(t)=Rτ^(t)=Rgt

Now, let’s consider the vector field F(x,y)=12[xy]. Then:

(14)F=1

And the area of Ω is thus:

(15)ΩFdA=ΩdA=AΩ

Let ds denote the line element on the curve. Using 5 we get:

(16)AΩ=ΩFn^ds=01Fg(t)Rgtdt=1201g(t)gtdt

Using the definition 1, let’s calculate the derivative:

(17)gt=i=0n(ni)Piddt((1t)niti)=i=0n(ni)(iti1(1t)ni(ni)ti(1t)ni1)Pi

Now, substituting this expression into 16 we get:

(18)AΩ=1201[i=0n(ni)(1t)nitiPi][j=0n(nj)(jtj1(1t)nj(nj)tj(1t)nj1)RPj]dt=12i=0nj=0n(ni)(nj)01dt[j(ti+j1(1t)2nij)(nj)(ti+j(1t)2nij1)]PiRPj

The sums are finite, so they pass through the integral, no worries there. In the middle, we can split the integral into two Euler integrals of the form 6, allowing us to rewrite the area expression like so:

(19)AΩ=12i=0nj=0n(ni)(nj)[jB(2nij+1,i+j)(nj)B(2nij,i+j+1)]PiRPj

Using 10 and 11 we can simplify things a bit:

(20)B(2nij+1,i+j)=1(i+j)(2ni+j)(21)B(2nij,i+j+1)=1(2nij)(2ni+j)

Now, substituting this into 19 we find:

(22)AΩ=12i,j=0n(ni)(nj)(2ni+j)[ji+jnj2nij]PiRPj

Note that we have 00 indeterminate forms for (i=0,j=0) and (i=n,j=n), so let’s see if we can evaluate limit points for these:

(23)limj0limi0[ji+jnj2nij]=limj0[1nj2nj]=1n2n=12 (24)limjnlimin[ji+jnj2nij]=limjn[jn+j1]=n2n1=12

So they exist, are of same magnitude and opposite sign. Interesting. Now, note that the binomial coefficients are equal when we set i and j to the two problematic configurations (i=0,j=0) and (i=n,j=n). And also note that P0RP0=PnRPn because the curve being closed we have P0=Pn. This means that the two end-chain contributions cancel, and we can ignore them in 22.

Additionally, 22 is symmetric under swapping i and j: the binomial coefficients are invariant, and both the bracketted difference factor and the dot product change sign under this operation. This means we are counting contributions twice, and then divide by two, which is wasteful.

These remarks lead us to the final form:

(25)AΩ=i=1j<in(ni)(nj)(2ni+j)[ji+jnj2nij]PiRPj

What a beast! Notice how i starts at 1, j at zero and stays strictly lower than i. This simultaneously ensures that the end-chain contributions are not evaluated, and that we are not overcounting the contributions, allowing us to drop the 12 factor.

Implementation

Here is a Python 3 implementation of the formula. Let’s start by importing numpy and scipy.special for the binomial coefficients:

import numpy as np
import scipy.special

Now, the function that computes 25. It is a straightforward nested loop with an accumulator:

def bezier_area(P):
    R = np.array([[0, 1],
                  [-1, 0]])

    N = P.shape[0]-1
    A = 0
    for ii in range(1, N+1):
        for jj in range(0, ii):
            B = scipy.special.binom(
                N, ii) * scipy.special.binom(N, jj) / scipy.special.binom(2*N, ii+jj)
            D = jj/(ii+jj) - (N-jj)/(2*N-ii-jj)
            S = np.dot(P[ii, :], np.matmul(R, P[jj, :]))
            A += B * D * S

    return A

Let’s also implement a shoelace algorithm so we can compare the two:

def approx_bezier_area(P, num_samples):
    def shoelace(x, y):
        return 0.5*np.abs(np.dot(x, np.roll(y, 1))-np.dot(y, np.roll(x, 1)))

    def bezier_interp(P, t):
        N = P.shape[0]-1
        val = np.zeros((t.shape[0], 2), dtype='float')
        for ii in range(0, N+1):
            b = scipy.special.binom(N, ii)
            c = b * np.power(t, ii) * np.power(1-t, N-ii)
            val += c[:, np.newaxis] * P[ii, :]
        return val

    N = P.shape[0]-1
    tt = np.linspace(0, 1, num_samples)
    pts = bezier_interp(P, tt)
    return shoelace(pts[:, 0], pts[:, 1])

A few things are going on. The bezier_interp() function computes the interpolated points on the curve for multiple values of the parameter t using the definition 1, and the shoelace algorithm was merely stolen from this Stack Overflow post. We compute num_samples values of the parameter t equally spaced between 0 and 1 with np.linspace(), and use bezier_interp() to get the interpolated points along the curve. These points form a polygon which -if the number of samples is high enough- will approximate closely the curve’s shape. Then we submit all these points to the shoelace algorithm which does its magick, and returns the polygon’s area.

Finally, we can define an arbitrary Bezier curve and run a simple test:

def main():
    P = np.array([[-0.5, -0.5], [1, -1.8], [1.5, -5],
                 [0.6, -2.3], [1, 1], [-1, 1], [-0.5, -0.5]])

    A_poly = approx_bezier_area(P, 100)
    print(f'the approximate area is {A_poly}')

    A_omega = bezier_area(P)
    print(f'the exact area is {A_omega}')


if __name__ == '__main__':
    main()

The Bezier curve under test

Which should output:

the approximate area is 1.3401359675890347
the exact area is 1.3405519480519479

I spare you the boring details, but we can also plot the approximation error |ApolyAΩ| for multiple numbers of polygon points for this specific case, and show that it vanishes exponentially as the number of points increases:

The approximation error plot

This concludes this article. One advantage of this approach -in addition to the fact that it is exact-, is that it is also faster than computing a polygon area for a few hundreds of sample points, as it only requires n(n+1)2 operations, with n the order of the Bezier curve.

I hope you found this as interesting as I did! Maybe you came up with a simpler proof?




The comment section requires the Utterances cookie in order to work properly. If you want to see people's comments or post a comment yourself, please enable the Utterances cookie here.