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

Commit08faaf5

Browse files
committed
add function to get center of mass of path
1 parent7c72a61 commit08faaf5

File tree

2 files changed

+211
-2
lines changed

2 files changed

+211
-2
lines changed

‎lib/matplotlib/bezier.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,73 @@ def point_at_t(self, t):
218218
defarc_length(self,tol=1e-6):
219219
pass
220220

221+
defarc_center_of_mass(self):
222+
r"""
223+
Center of mass of the (even-odd-rendered) area swept out by the ray
224+
from the origin to the path.
225+
226+
Summing this vector for each segment along a closed path will produce
227+
that area's center of mass.
228+
229+
Returns
230+
-------
231+
r_cm : (2,) np.array<float>
232+
the "arc's center of mass"
233+
234+
Notes
235+
-----
236+
A simple analytical form can be derived for general Bezier curves.
237+
Suppose the curve was closed, so :math:`B(0) = B(1)`. Call the area
238+
enclosed by :math:`B(t)` :math:`B_\text{int}`. The center of mass of
239+
:math:`B_\text{int}` is defined by the expected value of the position
240+
vector `\vec{r}`
241+
242+
.. math::
243+
244+
\vec{R}_\text{cm} = \int_{B_\text{int}} \vec{r} \left( \frac{1}{
245+
\int_{B_\text{int}}} d\vec{r} \right) d\vec{r}
246+
247+
where :math:`(1/\text{Area}(B_\text{int})` can be interpreted as a
248+
probability density.
249+
250+
In order to compute this integral, we choose two functions
251+
:math:`F_0(x,y) = [x^2/2, 0]` and :math:`F_1(x,y) = [0, y^2/2]` such
252+
that :math:`[\div \cdot F_0, \div \cdot F_1] = \vec{r}`. Then, applying
253+
the divergence integral (componentwise), we get that
254+
255+
.. math::
256+
\vec{R}_\text{cm} &= \oint_{B(t)} F \cdot \vec{n} dt \\
257+
&= \int_0^1 \left[ \begin{array}{1}
258+
B^{(0)}(t) \frac{dB^{(1)}(t)}{dt} \\
259+
- B^{(1)}(t) \frac{dB^{(0)}(t)}{dt} \end{array} \right] dt
260+
261+
After expanding in Berstein polynomials and moving the integral inside
262+
all the sums, we get that
263+
264+
.. math::
265+
\vec{R}_\text{cm} = \frac{1}{6} \sum_{i,j=0}^n\sum_{k=0}^{n-1}
266+
\frac{{n \choose i}{n \choose j}{{n-1} \choose k}}
267+
{{3n - 1} \choose {i + j + k}}
268+
\left(\begin{array}{1}
269+
P^{(0)}_i P^{(0)}_j (P^{(1)}_{k+1} - P^{(1)}_k)
270+
- P^{(1)}_i P^{(1)}_j (P^{(0)}_{k+1} - P^{(0)}_k)
271+
\right) \end{array}
272+
273+
where :math:`P_i = [P^{(0)}_i, P^{(1)}_i]` is the :math:`i`'th control
274+
point of the curve and :math:`n` is the degree of the curve.
275+
"""
276+
n=self.degree
277+
r_cm=np.zeros(2)
278+
P=self.control_points
279+
dP=np.diff(P,axis=0)
280+
Pn=np.array([[1,-1]])*dP[:, ::-1]# n = [y, -x]
281+
foriinrange(n+1):
282+
forjinrange(n+1):
283+
forkinrange(n):
284+
r_cm+=_comb(n,i)*_comb(n,j)*_comb(n-1,k) \
285+
*P[i]*P[j]*Pn[k]/_comb(3*n-1,i+j+k)
286+
returnr_cm/6
287+
221288
defarc_area(self):
222289
r"""
223290
(Signed) area swept out by ray from origin to curve.
@@ -306,6 +373,11 @@ def arc_area(self):
306373
* (P[j,0]*dP[k,1]-P[j,1]*dP[k,0])
307374
return (1/4)*area
308375

376+
defcenter_of_mass(self):
377+
# return np.array([scipy.integrate.quad(lambda t: B(t)[0], 0, 1)[0],
378+
# scipy.integrate.quad(lambda t: B(t)[1], 0, 1)[0]])
379+
pass
380+
309381
@classmethod
310382
defdifferentiate(cls,B):
311383
"""Return the derivative of a BezierSegment, itself a BezierSegment"""

‎lib/matplotlib/path.py

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,10 +690,147 @@ def signed_area(self, **kwargs):
690690
# add final implied CLOSEPOLY, if necessary
691691
ifstart_pointisnotNone \
692692
andnotnp.all(np.isclose(start_point,prev_point)):
693-
B=BezierSegment(np.array([prev_point,start_point]))
694-
area+=B.arc_area()
693+
Bclose=BezierSegment(np.array([prev_point,start_point]))
694+
area+=Bclose.arc_area()
695695
returnarea
696696

697+
defcenter_of_mass(self,dimension=None,**kwargs):
698+
r"""
699+
Center of mass of the path, assuming constant density.
700+
701+
The center of mass is defined to be the expected value of a vector
702+
located uniformly within either the filled area of the path
703+
(:code:`dimension=2`) or the along path's edge (:code:`dimension=1`) or
704+
along isolated points of the path (:code:`dimension=0`). Notice in
705+
particular that for this definition, if the filled area is used, then
706+
any 0- or 1-dimensional components of the path will not contribute to
707+
the center of mass. Similarly, for if *dimension* is 1, then isolated
708+
points in the path (i.e. "0-dimensional" strokes made up of only
709+
:code:`Path.MOVETO`'s) will not contribute to the center of mass.
710+
711+
For the 2d case, the center of mass is computed using the same
712+
filling strategy as `signed_area`. So, if a path is self-intersecting,
713+
the drawing rule "even-odd" is used and only the filled area is
714+
counted, and all sub paths are treated as if they had been closed. That
715+
is, if there is a MOVETO without a preceding CLOSEPOLY, one is added.
716+
717+
For the 1d measure, the curve is averaged as-is (the implied CLOSEPOLY
718+
is not added).
719+
720+
For the 0d measure, any non-isolated points are ignored.
721+
722+
Parameters
723+
----------
724+
dimension : 2, 1, or 0 (optional)
725+
Whether to compute the center of mass by taking the expected value
726+
of a position uniformly distributed within the filled path
727+
(2D-measure), the path's edge (1D-measure), or between the
728+
discrete, isolated points of the path (0D-measure), respectively.
729+
By default, the intended dimension of the path is inferred by
730+
checking first if `Path.signed_area` is non-zero (implying a
731+
*dimension* of 2), then if the `Path.arc_length` is non-zero
732+
(implying a *dimension* of 1), and finally falling back to the
733+
counting measure (*dimension* of 0).
734+
kwargs : Dict[str, object]
735+
Passed thru to `Path.cleaned` via `Path.iter_bezier`.
736+
737+
Returns
738+
-------
739+
r_cm : (2,) np.array<float>
740+
The center of mass of the path.
741+
742+
Raises
743+
------
744+
ValueError
745+
An empty path has no well-defined center of mass.
746+
747+
In addition, if a specific *dimension* is requested and that
748+
dimension is not well-defined, an error is raised. This can happen
749+
if::
750+
751+
1) 2D expected value was requested but the path has zero area
752+
2) 1D expected value was requested but the path has only
753+
`Path.MOVETO` directives
754+
3) 0D expected value was requested but the path has NO
755+
subsequent `Path.MOVETO` directives.
756+
757+
This error cannot be raised if the function is allowed to infer
758+
what *dimension* to use.
759+
"""
760+
area=None
761+
cleaned=self.cleaned(**kwargs)
762+
move_codes=cleaned.codes==Path.MOVETO
763+
iflen(cleaned.codes)==0:
764+
raiseValueError("An empty path has no center of mass.")
765+
ifdimensionisNone:
766+
dimension=2
767+
area=cleaned.signed_area()
768+
ifnotnp.isclose(area,0):
769+
dimension-=1
770+
ifnp.all(move_codes):
771+
dimension=0
772+
ifdimension==2:
773+
# area computation can be expensive, make sure we don't repeat it
774+
ifareaisNone:
775+
area=cleaned.signed_area()
776+
ifnp.isclose(area,0):
777+
raiseValueError("2d expected value over empty area is "
778+
"ill-defined.")
779+
returncleaned._2d_center_of_mass(area)
780+
ifdimension==1:
781+
ifnp.all(move_codes):
782+
raiseValueError("1d expected value over empty arc-length is "
783+
"ill-defined.")
784+
returncleaned._1d_center_of_mass()
785+
ifdimension==0:
786+
adjacent_moves= (move_codes[1:]+move_codes[:-1])==2
787+
iflen(move_codes)>1andnotnp.any(adjacent_moves):
788+
raiseValueError("0d expected value with no isolated points "
789+
"is ill-defined.")
790+
returncleaned._0d_center_of_mass()
791+
792+
def_2d_center_of_mass(self,normalization=None):
793+
#TODO: refactor this and signed_area (and maybe others, with
794+
# close= parameter)?
795+
ifnormalizationisNone:
796+
normalization=self.signed_area()
797+
r_cm=np.zeros(2)
798+
prev_point=None
799+
prev_code=None
800+
start_point=None
801+
forB,codeinself.iter_bezier():
802+
ifcode==Path.MOVETO:
803+
ifprev_codeisnotNoneandprev_codeisnotPath.CLOSEPOLY:
804+
Bclose=BezierSegment(np.array([prev_point,start_point]))
805+
r_cm+=Bclose.arc_center_of_mass()
806+
start_point=B.control_points[0]
807+
r_cm+=B.arc_center_of_mass()
808+
prev_point=B.control_points[-1]
809+
prev_code=code
810+
# add final implied CLOSEPOLY, if necessary
811+
ifstart_pointisnotNone \
812+
andnotnp.all(np.isclose(start_point,prev_point)):
813+
Bclose=BezierSegment(np.array([prev_point,start_point]))
814+
r_cm+=Bclose.arc_center_of_mass()
815+
returnr_cm/normalization
816+
817+
def_1d_center_of_mass(self):
818+
r_cm=np.zeros(2)
819+
Bs=list(self.iter_bezier())
820+
arc_lengths=np.array([B.arc_length()forBinBs])
821+
r_cms=np.array([B.center_of_mass()forBinBs])
822+
total_length=np.sum(arc_lengths)
823+
returnnp.sum(r_cms*arc_lengths)/total_length
824+
825+
def_0d_center_of_mass(self):
826+
move_verts=self.codes
827+
isolated_verts=move_verts.copy()
828+
iflen(move_verts)>1:
829+
isolated_verts[:-1]= (move_verts[:-1]+move_verts[1:])==2
830+
isolated_verts[-1]=move_verts[-1]
831+
num_verts=np.sum(isolated_verts)
832+
returnnp.sum(self.vertices[isolated_verts],axis=0)/num_verts
833+
697834
definterpolated(self,steps):
698835
"""
699836
Returns a new path resampled to length N x steps. Does not

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp