Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e2f403a

Browse files
committed
code to compute bezier segment / path lengths
1 parent 0037abd commit e2f403a

File tree

4 files changed

+129
-1
lines changed

4 files changed

+129
-1
lines changed

lib/matplotlib/bezier.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import math
66
import warnings
7+
from collections import deque
78

89
import numpy as np
910

@@ -206,6 +207,96 @@ def point_at_t(self, t):
206207
"""Return the point on the Bezier curve for parameter *t*."""
207208
return tuple(self(t))
208209

210+
def split_at_t(self, t):
211+
"""Split into two Bezier curves using de casteljau's algorithm.
212+
213+
Parameters
214+
----------
215+
t : float
216+
Point in [0,1] at which to split into two curves
217+
218+
Returns
219+
-------
220+
B1, B2 : BezierSegment
221+
The two sub-curves.
222+
"""
223+
new_cpoints = split_de_casteljau(self._cpoints, t)
224+
return BezierSegment(new_cpoints[0]), BezierSegment(new_cpoints[1])
225+
226+
def control_net_length(self):
227+
"""Sum of lengths between control points"""
228+
L = 0
229+
N, d = self._cpoints.shape
230+
for i in range(N - 1):
231+
L += np.linalg.norm(self._cpoints[i+1] - self._cpoints[i])
232+
return L
233+
234+
def arc_length(self, rtol=None, atol=None):
235+
"""Estimate the length using iterative refinement.
236+
237+
Our estimate is just the average between the length of the chord and
238+
the length of the control net.
239+
240+
Since the chord length and control net give lower and upper bounds
241+
(respectively) on the length, this maximum possible error is tested
242+
against an absolute tolerance threshold at each subdivision.
243+
244+
However, sometimes this estimator converges much faster than this error
245+
esimate would suggest. Therefore, the relative change in the length
246+
estimate between subdivisions is compared to a relative error tolerance
247+
after each set of subdivisions.
248+
249+
Parameters
250+
----------
251+
rtol : float, default 1e-4
252+
If :code:`abs(est[i+1] - est[i]) <= rtol * est[i+1]`, we return
253+
:code:`est[i+1]`.
254+
atol : float, default 1e-6
255+
If the distance between chord length and control length at any
256+
point falls below this number, iteration is terminated.
257+
"""
258+
if rtol is None:
259+
rtol = 1e-4
260+
if atol is None:
261+
atol = 1e-6
262+
263+
chord = np.linalg.norm(self._cpoints[-1] - self._cpoints[0])
264+
net = self.control_net_length()
265+
max_err = (net - chord)/2
266+
curr_est = chord + max_err
267+
# early exit so we don't try to "split" paths of zero length
268+
if max_err < atol:
269+
return curr_est
270+
271+
prev_est = np.inf
272+
curves = deque([self])
273+
errs = deque([max_err])
274+
lengths = deque([curr_est])
275+
while np.abs(curr_est - prev_est) > rtol * curr_est:
276+
# subdivide the *whole* curve before checking relative convergence
277+
# again
278+
prev_est = curr_est
279+
num_curves = len(curves)
280+
for i in range(num_curves):
281+
curve = curves.popleft()
282+
new_curves = curve.split_at_t(0.5)
283+
max_err -= errs.popleft()
284+
curr_est -= lengths.popleft()
285+
for ncurve in new_curves:
286+
chord = np.linalg.norm(
287+
ncurve._cpoints[-1] - ncurve._cpoints[0])
288+
net = ncurve.control_net_length()
289+
nerr = (net - chord)/2
290+
nlength = chord + nerr
291+
max_err += nerr
292+
curr_est += nlength
293+
curves.append(ncurve)
294+
errs.append(nerr)
295+
lengths.append(nlength)
296+
if max_err < atol:
297+
return curr_est
298+
return curr_est
299+
209300
def arc_area(self):
210301
r"""
211302
(Signed) area swept out by ray from origin to curve.

lib/matplotlib/path.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,27 @@ def intersects_bbox(self, bbox, filled=True):
642642
return _path.path_intersects_rectangle(
643643
self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
644644

645+
def length(self, rtol=None, atol=None, **kwargs):
646+
"""Get length of Path.
647+
648+
Equivalent to (but not computed as)
649+
650+
.. math::
651+
652+
\sum_{j=1}^N \int_0^1 ||B'_j(t)|| dt
653+
654+
where the sum is over the :math:`N` Bezier curves that comprise the
655+
Path. Notice that this measure of length will assign zero weight to all
656+
isolated points on the Path.
657+
658+
Returns
659+
-------
660+
length : float
661+
The path length.
662+
"""
663+
return np.sum([B.arc_length(rtol, atol)
664+
for B, code in self.iter_bezier(**kwargs)])
665+
645666
def signed_area(self, **kwargs):
646667
"""
647668
Get signed area filled by path.

lib/matplotlib/tests/test_bezier.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
]
2121

2222

23+
def _integral_arc_length(B):
24+
dB = B.differentiate(B)
25+
def integrand(t):
26+
return np.linalg.norm(dB(t))
27+
return scipy.integrate.quad(integrand, 0, 1)[0]
28+
29+
2330
def _integral_arc_area(B):
2431
"""(Signed) area swept out by ray from origin to curve."""
2532
dB = B.differentiate(B)
@@ -33,3 +40,9 @@ def integrand(t):
3340
def test_area_formula():
3441
for B in test_curves:
3542
assert(np.isclose(_integral_arc_area(B), B.arc_area()))
43+
44+
45+
def test_length_iteration():
46+
for B in test_curves:
47+
assert(np.isclose(_integral_arc_length(B), B.arc_length(
48+
rtol=1e-5, atol=1e-8), rtol=1e-5, atol=1e-8))

lib/matplotlib/tests/test_path.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import copy
22

33
import numpy as np
4-
54
from numpy.testing import assert_array_equal
65
import pytest
76

@@ -72,6 +71,10 @@ def test_signed_area_unit_rectangle():
7271
assert(np.isclose(rect.signed_area(), 1))
7372

7473

74+
def test_length_curve():
75+
assert(np.isclose(_hard_curve.length(), 2.0))
76+
77+
7578
def test_point_in_path_nan():
7679
box = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
7780
p = Path(box)

0 commit comments

Comments
 (0)