A formula for the area enclosed by a planar Bezier curve of order n
mathematics
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
The curve
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
The divergence theorem in 2D
Let
Let
By definition of
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:
The Beta function has the following useful properties:
We can combine these equalities to obtain two new equalities which we’ll use later on:
Solution
Let
Note that we can obtain the tangent vector easily by a
Now, let’s consider the vector field
And the area of
Let
Using the definition
Now, substituting this expression into
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
Using
Now, substituting this into
Note that we have
So they exist, are of same magnitude and opposite sign. Interesting. Now, note that the binomial coefficients are equal when we set
Additionally,
These remarks lead us to the final form:
What a beast! Notice how
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
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 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()
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
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
I hope you found this as interesting as I did! Maybe you came up with a simpler proof?