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

Commit590b226

Browse files
authored
Merge pull request#1251 from effigies/enh/pointset
ENH: Add pointset data structures [BIAP9]
2 parents9c568dc +5ded851 commit590b226

File tree

3 files changed

+381
-3
lines changed

3 files changed

+381
-3
lines changed

‎.pre-commit-config.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
exclude:".*/data/.*"
22
repos:
33
-repo:https://github.com/pre-commit/pre-commit-hooks
4-
rev:v4.1.0
4+
rev:v4.4.0
55
hooks:
66
-id:trailing-whitespace
77
-id:end-of-file-fixer
@@ -21,12 +21,12 @@ repos:
2121
hooks:
2222
-id:isort
2323
-repo:https://github.com/pycqa/flake8
24-
rev:6.0.0
24+
rev:6.1.0
2525
hooks:
2626
-id:flake8
2727
exclude:"^(doc|nisext|tools)/"
2828
-repo:https://github.com/pre-commit/mirrors-mypy
29-
rev:v0.991
29+
rev:v1.5.1
3030
hooks:
3131
-id:mypy
3232
# Sync with project.optional-dependencies.typing

‎nibabel/pointset.py‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Point-set structures
2+
3+
Imaging data are sampled at points in space, and these points
4+
can be described by coordinates.
5+
These structures are designed to enable operations on sets of
6+
points, as opposed to the data sampled at those points.
7+
8+
Abstractly, a point set is any collection of points, but there are
9+
two types that warrant special consideration in the neuroimaging
10+
context: grids and meshes.
11+
12+
A *grid* is a collection of regularly-spaced points. The canonical
13+
examples of grids are the indices of voxels and their affine
14+
projection into a reference space.
15+
16+
A *mesh* is a collection of points and some structure that enables
17+
adjacent points to be identified. A *triangular mesh* in particular
18+
uses triplets of adjacent vertices to describe faces.
19+
"""
20+
from __future__importannotations
21+
22+
importmath
23+
importtypingasty
24+
fromdataclassesimportdataclass,replace
25+
26+
importnumpyasnp
27+
28+
fromnibabel.castingimportable_int_type
29+
fromnibabel.filesliceimportstrided_scalar
30+
fromnibabel.spatialimagesimportSpatialImage
31+
32+
ifty.TYPE_CHECKING:# pragma: no cover
33+
fromtyping_extensionsimportSelf
34+
35+
_DType=ty.TypeVar('_DType',bound=np.dtype[ty.Any])
36+
37+
38+
classCoordinateArray(ty.Protocol):
39+
ndim:int
40+
shape:tuple[int,int]
41+
42+
@ty.overload
43+
def__array__(self,dtype:None= ...,/)->np.ndarray[ty.Any,np.dtype[ty.Any]]:
44+
...# pragma: no cover
45+
46+
@ty.overload
47+
def__array__(self,dtype:_DType,/)->np.ndarray[ty.Any,_DType]:
48+
...# pragma: no cover
49+
50+
51+
@dataclass
52+
classPointset:
53+
"""A collection of points described by coordinates.
54+
55+
Parameters
56+
----------
57+
coords : array-like
58+
(*N*, *n*) array with *N* being points and columns their *n*-dimensional coordinates
59+
affine : :class:`numpy.ndarray`
60+
Affine transform to be applied to coordinates array
61+
homogeneous : :class:`bool`
62+
Indicate whether the provided coordinates are homogeneous,
63+
i.e., homogeneous 3D coordinates have the form ``(x, y, z, 1)``
64+
"""
65+
66+
coordinates:CoordinateArray
67+
affine:np.ndarray
68+
homogeneous:bool=False
69+
70+
# Force use of __rmatmul__ with numpy arrays
71+
__array_priority__=99
72+
73+
def__init__(
74+
self,
75+
coordinates:CoordinateArray,
76+
affine:np.ndarray|None=None,
77+
homogeneous:bool=False,
78+
):
79+
self.coordinates=coordinates
80+
self.homogeneous=homogeneous
81+
82+
ifaffineisNone:
83+
self.affine=np.eye(self.dim+1)
84+
else:
85+
self.affine=np.asanyarray(affine)
86+
87+
ifself.affine.shape!= (self.dim+1,)*2:
88+
raiseValueError(f'Invalid affine for{self.dim}D coordinates:\n{self.affine}')
89+
ifnp.any(self.affine[-1, :-1]!=0)orself.affine[-1,-1]!=1:
90+
raiseValueError(f'Invalid affine matrix:\n{self.affine}')
91+
92+
@property
93+
defn_coords(self)->int:
94+
"""Number of coordinates
95+
96+
Subclasses should override with more efficient implementations.
97+
"""
98+
returnself.coordinates.shape[0]
99+
100+
@property
101+
defdim(self)->int:
102+
"""The dimensionality of the space the coordinates are in"""
103+
returnself.coordinates.shape[1]-self.homogeneous
104+
105+
def__rmatmul__(self,affine:np.ndarray)->Self:
106+
"""Apply an affine transformation to the pointset
107+
108+
This will return a new pointset with an updated affine matrix only.
109+
"""
110+
returnreplace(self,affine=np.asanyarray(affine) @self.affine)
111+
112+
def_homogeneous_coords(self):
113+
ifself.homogeneous:
114+
returnnp.asanyarray(self.coordinates)
115+
116+
ones=strided_scalar(
117+
shape=(self.coordinates.shape[0],1),
118+
scalar=np.array(1,dtype=self.coordinates.dtype),
119+
)
120+
returnnp.hstack((self.coordinates,ones))
121+
122+
defget_coords(self,*,as_homogeneous:bool=False):
123+
"""Retrieve the coordinates
124+
125+
Parameters
126+
----------
127+
as_homogeneous : :class:`bool`
128+
Return homogeneous coordinates if ``True``, or Cartesian
129+
coordiantes if ``False``.
130+
131+
name : :class:`str`
132+
Select a particular coordinate system if more than one may exist.
133+
By default, `None` is equivalent to `"world"` and corresponds to
134+
an RAS+ coordinate system.
135+
"""
136+
ident=np.allclose(self.affine,np.eye(self.affine.shape[0]))
137+
ifself.homogeneous==as_homogeneousandident:
138+
returnnp.asanyarray(self.coordinates)
139+
coords=self._homogeneous_coords()
140+
ifnotident:
141+
coords= (self.affine @coords.T).T
142+
ifnotas_homogeneous:
143+
coords=coords[:, :-1]
144+
returncoords
145+
146+
147+
classGrid(Pointset):
148+
r"""A regularly-spaced collection of coordinates
149+
150+
This class provides factory methods for generating Pointsets from
151+
:class:`~nibabel.spatialimages.SpatialImage`\s and generating masks
152+
from coordinate sets.
153+
"""
154+
155+
@classmethod
156+
deffrom_image(cls,spatialimage:SpatialImage)->Self:
157+
returncls(coordinates=GridIndices(spatialimage.shape[:3]),affine=spatialimage.affine)
158+
159+
@classmethod
160+
deffrom_mask(cls,mask:SpatialImage)->Self:
161+
mask_arr=np.bool_(mask.dataobj)
162+
returncls(
163+
coordinates=np.c_[np.nonzero(mask_arr)].astype(able_int_type(mask.shape)),
164+
affine=mask.affine,
165+
)
166+
167+
defto_mask(self,shape=None)->SpatialImage:
168+
ifshapeisNone:
169+
shape=tuple(np.max(self.coordinates,axis=0)[:self.dim]+1)
170+
mask_arr=np.zeros(shape,dtype='bool')
171+
mask_arr[tuple(np.asanyarray(self.coordinates)[:, :self.dim].T)]=True
172+
returnSpatialImage(mask_arr,self.affine)
173+
174+
175+
classGridIndices:
176+
"""Class for generating indices just-in-time"""
177+
178+
__slots__= ('gridshape','dtype','shape')
179+
ndim=2
180+
181+
def__init__(self,shape,dtype=None):
182+
self.gridshape=shape
183+
self.dtype=dtypeorable_int_type(shape)
184+
self.shape= (math.prod(self.gridshape),len(self.gridshape))
185+
186+
def__repr__(self):
187+
returnf'<{self.__class__.__name__}{self.gridshape}>'
188+
189+
def__array__(self,dtype=None):
190+
ifdtypeisNone:
191+
dtype=self.dtype
192+
193+
axes= [np.arange(s,dtype=dtype)forsinself.gridshape]
194+
returnnp.reshape(np.meshgrid(*axes,copy=False,indexing='ij'), (len(axes),-1)).T

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp