Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit0dbef92

Browse files
committed
code to compute bezier segment / path lengths
1 parenta568413 commit0dbef92

File tree

4 files changed

+141
-5
lines changed

4 files changed

+141
-5
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
importmath
66
importwarnings
7+
fromcollectionsimportdeque
78

89
importnumpyasnp
910

@@ -219,6 +220,96 @@ def point_at_t(self, t):
219220
"""Evaluate curve at a single point *t*. Returns a Tuple[float*d]."""
220221
returntuple(self(t))
221222

223+
defsplit_at_t(self,t):
224+
"""Split into two Bezier curves using de casteljau's algorithm.
225+
226+
Parameters
227+
----------
228+
t : float
229+
Point in [0,1] at which to split into two curves
230+
231+
Returns
232+
-------
233+
B1, B2 : BezierSegment
234+
The two sub-curves.
235+
"""
236+
new_cpoints=split_de_casteljau(self._cpoints,t)
237+
returnBezierSegment(new_cpoints[0]),BezierSegment(new_cpoints[1])
238+
239+
defcontrol_net_length(self):
240+
"""Sum of lengths between control points"""
241+
L=0
242+
N,d=self._cpoints.shape
243+
foriinrange(N-1):
244+
L+=np.linalg.norm(self._cpoints[i+1]-self._cpoints[i])
245+
returnL
246+
247+
defarc_length(self,rtol=None,atol=None):
248+
"""Estimate the length using iterative refinement.
249+
250+
Our estimate is just the average between the length of the chord and
251+
the length of the control net.
252+
253+
Since the chord length and control net give lower and upper bounds
254+
(respectively) on the length, this maximum possible error is tested
255+
against an absolute tolerance threshold at each subdivision.
256+
257+
However, sometimes this estimator converges much faster than this error
258+
esimate would suggest. Therefore, the relative change in the length
259+
estimate between subdivisions is compared to a relative error tolerance
260+
after each set of subdivisions.
261+
262+
Parameters
263+
----------
264+
rtol : float, default 1e-4
265+
If :code:`abs(est[i+1] - est[i]) <= rtol * est[i+1]`, we return
266+
:code:`est[i+1]`.
267+
atol : float, default 1e-6
268+
If the distance between chord length and control length at any
269+
point falls below this number, iteration is terminated.
270+
"""
271+
ifrtolisNone:
272+
rtol=1e-4
273+
ifatolisNone:
274+
atol=1e-6
275+
276+
chord=np.linalg.norm(self._cpoints[-1]-self._cpoints[0])
277+
net=self.control_net_length()
278+
max_err= (net-chord)/2
279+
curr_est=chord+max_err
280+
# early exit so we don't try to "split" paths of zero length
281+
ifmax_err<atol:
282+
returncurr_est
283+
284+
prev_est=np.inf
285+
curves=deque([self])
286+
errs=deque([max_err])
287+
lengths=deque([curr_est])
288+
whilenp.abs(curr_est-prev_est)>rtol*curr_est:
289+
# subdivide the *whole* curve before checking relative convergence
290+
# again
291+
prev_est=curr_est
292+
num_curves=len(curves)
293+
foriinrange(num_curves):
294+
curve=curves.popleft()
295+
new_curves=curve.split_at_t(0.5)
296+
max_err-=errs.popleft()
297+
curr_est-=lengths.popleft()
298+
forncurveinnew_curves:
299+
chord=np.linalg.norm(
300+
ncurve._cpoints[-1]-ncurve._cpoints[0])
301+
net=ncurve.control_net_length()
302+
nerr= (net-chord)/2
303+
nlength=chord+nerr
304+
max_err+=nerr
305+
curr_est+=nlength
306+
curves.append(ncurve)
307+
errs.append(nerr)
308+
lengths.append(nlength)
309+
ifmax_err<atol:
310+
returncurr_est
311+
returncurr_est
312+
222313
defarc_area(self):
223314
r"""
224315
(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+
deflength(self,rtol=None,atol=None,**kwargs):
646+
r"""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+
returnnp.sum([B.arc_length(rtol,atol)
664+
forB,codeinself.iter_bezier(**kwargs)])
665+
645666
defsigned_area(self,**kwargs):
646667
"""
647668
Get signed area of the filled path.

‎lib/matplotlib/tests/test_bezier.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
_test_curves= [list(tc.path.iter_bezier())[-1][0]fortcin_test_curves]
1111

1212

13+
def_integral_arc_length(B):
14+
dB=B.differentiate(B)
15+
defintegrand(t):
16+
returnnp.linalg.norm(dB(t))
17+
returnscipy.integrate.quad(integrand,0,1)[0]
18+
19+
1320
def_integral_arc_area(B):
1421
"""(Signed) area swept out by ray from origin to curve."""
1522
dB=B.differentiate(B)
@@ -23,3 +30,10 @@ def integrand(t):
2330
@pytest.mark.parametrize("B",_test_curves)
2431
deftest_area_formula(B):
2532
assertnp.isclose(_integral_arc_area(B),B.arc_area())
33+
34+
35+
@pytest.mark.parametrize("B",_test_curves)
36+
deftest_length_iteration(B):
37+
assertnp.isclose(_integral_arc_length(B),
38+
B.arc_length(rtol=1e-5,atol=1e-8),
39+
rtol=1e-5,atol=1e-8)

‎lib/matplotlib/tests/test_path.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,24 @@ def test_contains_points_negative_radius():
4949
np.testing.assert_equal(result, [True,False,False])
5050

5151

52-
_ExampleCurve=namedtuple('ExampleCurve', ['path','extents','area'])
52+
_ExampleCurve=namedtuple('ExampleCurve',
53+
['path','extents','area','length'])
5354
_test_curves= [
5455
# interior extrema determine extents and degenerate derivative
5556
_ExampleCurve(Path([[0,0], [1,0], [1,1], [0,1]],
5657
[Path.MOVETO,Path.CURVE4,Path.CURVE4,Path.CURVE4]),
57-
extents=(0.,0.,0.75,1.),area=0.6),
58+
extents=(0.,0.,0.75,1.),area=0.6,length=2.0),
5859
# a quadratic curve, clockwise
5960
_ExampleCurve(Path([[0,0], [0,1], [1,0]], [Path.MOVETO,Path.CURVE3,
60-
Path.CURVE3]),extents=(0.,0.,1.,0.5),area=-1/3),
61+
Path.CURVE3]),extents=(0.,0.,1.,0.5),area=-1/3,
62+
length=(1/25)*(10+15*np.sqrt(2)+np.sqrt(5) \
63+
* (np.arcsinh(2)+np.arcsinh(3)))),
6164
# a linear curve, degenerate vertically
6265
_ExampleCurve(Path([[0,1], [1,1]], [Path.MOVETO,Path.LINETO]),
63-
extents=(0.,1.,1.,1.),area=0.),
66+
extents=(0.,1.,1.,1.),area=0.,length=1.0),
6467
# a point
6568
_ExampleCurve(Path([[1,2]], [Path.MOVETO]),extents=(1.,2.,1.,2.),
66-
area=0.),
69+
area=0.,length=0.0),
6770
]
6871

6972

@@ -105,6 +108,13 @@ def test_signed_area_unit_circle():
105108
assertnp.isclose(circ.signed_area(),3.1415935732517166)
106109

107110

111+
@pytest.mark.parametrize('precomputed_curve',_test_curves)
112+
deftest_length_curve(precomputed_curve):
113+
path,length=precomputed_curve.path,precomputed_curve.length
114+
assertnp.isclose(path.length(rtol=1e-5,atol=1e-8),length,rtol=1e-5,
115+
atol=1e-8)
116+
117+
108118
deftest_point_in_path_nan():
109119
box=np.array([[0,0], [1,0], [1,1], [0,1], [0,0]])
110120
p=Path(box)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp