siibra 0.5a2__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of siibra might be problematic. Click here for more details.
- siibra/VERSION +1 -1
- siibra/__init__.py +20 -12
- siibra/commons.py +145 -90
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +22 -17
- siibra/configuration/factory.py +177 -128
- siibra/core/__init__.py +1 -8
- siibra/core/{relation_qualification.py → assignment.py} +17 -14
- siibra/core/atlas.py +66 -35
- siibra/core/concept.py +81 -39
- siibra/core/parcellation.py +83 -67
- siibra/core/region.py +569 -263
- siibra/core/space.py +7 -39
- siibra/core/structure.py +111 -0
- siibra/exceptions.py +63 -0
- siibra/experimental/__init__.py +19 -0
- siibra/experimental/contour.py +61 -0
- siibra/experimental/cortical_profile_sampler.py +57 -0
- siibra/experimental/patch.py +98 -0
- siibra/experimental/plane3d.py +256 -0
- siibra/explorer/__init__.py +16 -0
- siibra/explorer/url.py +112 -52
- siibra/explorer/util.py +31 -9
- siibra/features/__init__.py +73 -8
- siibra/features/anchor.py +75 -196
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +2 -2
- siibra/features/connectivity/regional_connectivity.py +99 -10
- siibra/features/connectivity/streamline_counts.py +1 -1
- siibra/features/connectivity/streamline_lengths.py +1 -1
- siibra/features/connectivity/tracing_connectivity.py +1 -1
- siibra/features/dataset/__init__.py +1 -1
- siibra/features/dataset/ebrains.py +3 -3
- siibra/features/feature.py +219 -110
- siibra/features/image/__init__.py +1 -1
- siibra/features/image/image.py +21 -13
- siibra/features/image/sections.py +1 -1
- siibra/features/image/volume_of_interest.py +1 -1
- siibra/features/tabular/__init__.py +1 -1
- siibra/features/tabular/bigbrain_intensity_profile.py +24 -13
- siibra/features/tabular/cell_density_profile.py +111 -69
- siibra/features/tabular/cortical_profile.py +82 -16
- siibra/features/tabular/gene_expression.py +117 -6
- siibra/features/tabular/layerwise_bigbrain_intensities.py +7 -9
- siibra/features/tabular/layerwise_cell_density.py +9 -24
- siibra/features/tabular/receptor_density_fingerprint.py +11 -6
- siibra/features/tabular/receptor_density_profile.py +12 -15
- siibra/features/tabular/regional_timeseries_activity.py +74 -18
- siibra/features/tabular/tabular.py +17 -8
- siibra/livequeries/__init__.py +1 -7
- siibra/livequeries/allen.py +139 -77
- siibra/livequeries/bigbrain.py +104 -128
- siibra/livequeries/ebrains.py +7 -4
- siibra/livequeries/query.py +1 -2
- siibra/locations/__init__.py +32 -25
- siibra/locations/boundingbox.py +153 -127
- siibra/locations/location.py +45 -80
- siibra/locations/point.py +97 -83
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +107 -13
- siibra/retrieval/datasets.py +9 -14
- siibra/retrieval/exceptions/__init__.py +2 -1
- siibra/retrieval/repositories.py +147 -53
- siibra/retrieval/requests.py +64 -29
- siibra/vocabularies/__init__.py +2 -2
- siibra/volumes/__init__.py +7 -9
- siibra/volumes/parcellationmap.py +396 -253
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/{gifti.py → providers/gifti.py} +29 -18
- siibra/volumes/{neuroglancer.py → providers/neuroglancer.py} +204 -92
- siibra/volumes/{nifti.py → providers/nifti.py} +64 -44
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +159 -260
- siibra/volumes/volume.py +720 -152
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/METADATA +25 -28
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/WHEEL +1 -1
- siibra/locations/pointset.py +0 -198
- siibra-0.5a2.dist-info/RECORD +0 -74
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/LICENSE +0 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/top_level.txt +0 -0
siibra/core/space.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2024
|
|
2
2
|
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
3
|
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
from .concept import AtlasConcept
|
|
19
19
|
|
|
20
|
-
from ..locations import Point, BoundingBox
|
|
21
20
|
from ..commons import logger, Species
|
|
22
21
|
|
|
23
22
|
from typing import List, TYPE_CHECKING, Union
|
|
@@ -39,6 +38,7 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
39
38
|
modality: str = "",
|
|
40
39
|
publications: list = [],
|
|
41
40
|
datasets: list = [],
|
|
41
|
+
prerelease: bool = False,
|
|
42
42
|
):
|
|
43
43
|
"""
|
|
44
44
|
Constructs a new parcellation object.
|
|
@@ -76,6 +76,7 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
76
76
|
modality=modality,
|
|
77
77
|
publications=publications,
|
|
78
78
|
datasets=datasets,
|
|
79
|
+
prerelease=prerelease,
|
|
79
80
|
)
|
|
80
81
|
self.volumes = volumes
|
|
81
82
|
for v in self.volumes:
|
|
@@ -116,7 +117,10 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
116
117
|
f"'{candidates[0].variant}' is chosen, but you might specify another with the 'variant' parameter."
|
|
117
118
|
)
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
template = candidates[0]
|
|
121
|
+
if len(template.datasets) == 0:
|
|
122
|
+
template.datasets = self.datasets
|
|
123
|
+
return template
|
|
120
124
|
|
|
121
125
|
@property
|
|
122
126
|
def provides_mesh(self):
|
|
@@ -125,39 +129,3 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
125
129
|
@property
|
|
126
130
|
def provides_image(self):
|
|
127
131
|
return any(v.provides_image for v in self.volumes)
|
|
128
|
-
|
|
129
|
-
def __getitem__(self, slices):
|
|
130
|
-
"""
|
|
131
|
-
Get a volume of interest specification from this space.
|
|
132
|
-
|
|
133
|
-
Parameters
|
|
134
|
-
----------
|
|
135
|
-
slices: triple of slice
|
|
136
|
-
Defines the x, y and z range
|
|
137
|
-
"""
|
|
138
|
-
if len(slices) != 3:
|
|
139
|
-
raise TypeError(
|
|
140
|
-
"Slice access to spaces needs to define x,y and z ranges (e.g. Space[10:30,0:10,200:300])"
|
|
141
|
-
)
|
|
142
|
-
point1 = [0 if s.start is None else s.start for s in slices]
|
|
143
|
-
point2 = [s.stop for s in slices]
|
|
144
|
-
if None in point2:
|
|
145
|
-
# fill upper bounds with maximum physical coordinates
|
|
146
|
-
T = self.get_template()
|
|
147
|
-
shape = Point(T.get_shape(-1), None).transform(T.build_affine(-1))
|
|
148
|
-
point2 = [shape[i] if v is None else v for i, v in enumerate(point2)]
|
|
149
|
-
return self.get_bounding_box(point1, point2)
|
|
150
|
-
|
|
151
|
-
def get_bounding_box(self, point1, point2):
|
|
152
|
-
"""
|
|
153
|
-
Get a volume of interest specification from this space.
|
|
154
|
-
|
|
155
|
-
Parameters
|
|
156
|
-
----------
|
|
157
|
-
point1: 3D tuple defined in physical coordinates of this reference space
|
|
158
|
-
point2: 3D tuple defined in physical coordinates of this reference space
|
|
159
|
-
Returns
|
|
160
|
-
-------
|
|
161
|
-
BoundingBox
|
|
162
|
-
"""
|
|
163
|
-
return BoundingBox(point1, point2, self)
|
siibra/core/structure.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
Abstract base class for any kind of brain structure.
|
|
17
|
+
A brain structure is more general than a brain region. It refers to any object
|
|
18
|
+
defining a spatial extent in one or more reference spaces, and can thus be
|
|
19
|
+
used to compute intersections with other structures in space. For example,
|
|
20
|
+
a brain region is a structure which is at the same time an AtlasConcept. A
|
|
21
|
+
bounding box in MNI space is a structure, but not an AtlasConcept.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from . import assignment, region as _region
|
|
25
|
+
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from typing import Tuple, Dict
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BrainStructure(ABC):
|
|
31
|
+
"""Abstract base class for types who can act as a location filter."""
|
|
32
|
+
|
|
33
|
+
# cache assignment results at class level
|
|
34
|
+
_ASSIGNMENT_CACHE: Dict[
|
|
35
|
+
Tuple["BrainStructure", "BrainStructure"],
|
|
36
|
+
"assignment.AnatomicalAssignment"
|
|
37
|
+
] = {}
|
|
38
|
+
|
|
39
|
+
def intersects(self, other: "BrainStructure") -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Whether or not two BrainStructures have any intersection.
|
|
42
|
+
"""
|
|
43
|
+
return self.intersection(other) is not None
|
|
44
|
+
|
|
45
|
+
def __contains__(self, other: "BrainStructure") -> bool:
|
|
46
|
+
return self.intersection(other) == other
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash(self.__repr__())
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def intersection(self, other: "BrainStructure") -> "BrainStructure":
|
|
57
|
+
"""
|
|
58
|
+
Return the intersection of two BrainStructures,
|
|
59
|
+
ie. the other BrainStructure filtered by this BrainStructure.
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def assign(self, other: "BrainStructure") -> assignment.AnatomicalAssignment:
|
|
64
|
+
"""
|
|
65
|
+
Compute assignment of a BrainStructure to this filter.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
other : Location or Region
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
assignment.AnatomicalAssignment or None
|
|
74
|
+
None if there is no AssignmentQualification found.
|
|
75
|
+
"""
|
|
76
|
+
# Two cases:
|
|
77
|
+
# 1) self is location, other is location -> look at spatial intersection/relationship, do it here
|
|
78
|
+
# 2) self is location, other is region -> get region map, then call again. do it here
|
|
79
|
+
# If self is region -> Region overwrite this method, adressed there
|
|
80
|
+
|
|
81
|
+
assert not isinstance(self, _region.Region) # method is overwritten by Region!
|
|
82
|
+
if (self, other) in self._ASSIGNMENT_CACHE:
|
|
83
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
84
|
+
if (other, self) in self._ASSIGNMENT_CACHE:
|
|
85
|
+
return self._ASSIGNMENT_CACHE[other, self].invert()
|
|
86
|
+
|
|
87
|
+
if isinstance(other, _region.Region):
|
|
88
|
+
inverse_assignment = other.assign(self)
|
|
89
|
+
if inverse_assignment is None:
|
|
90
|
+
self._ASSIGNMENT_CACHE[self, other] = None
|
|
91
|
+
else:
|
|
92
|
+
self._ASSIGNMENT_CACHE[self, other] = inverse_assignment.invert()
|
|
93
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
94
|
+
else: # other is a location object, just check spatial relationships
|
|
95
|
+
qualification = None
|
|
96
|
+
if self == other:
|
|
97
|
+
qualification = assignment.Qualification.EXACT
|
|
98
|
+
else:
|
|
99
|
+
intersection = self.intersection(other)
|
|
100
|
+
if intersection is not None:
|
|
101
|
+
if intersection == other:
|
|
102
|
+
qualification = assignment.Qualification.CONTAINS
|
|
103
|
+
elif intersection == self:
|
|
104
|
+
qualification = assignment.Qualification.CONTAINED
|
|
105
|
+
else:
|
|
106
|
+
qualification = assignment.Qualification.OVERLAPS
|
|
107
|
+
if qualification is None:
|
|
108
|
+
self._ASSIGNMENT_CACHE[self, other] = None
|
|
109
|
+
else:
|
|
110
|
+
self._ASSIGNMENT_CACHE[self, other] = assignment.AnatomicalAssignment(self, other, qualification)
|
|
111
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
siibra/exceptions.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
"""Siibra specific exceptions"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExcessiveArgumentException(ValueError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InsufficientArgumentException(ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConflictingArgumentException(ValueError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NonUniqueIndexError(RuntimeError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NoMapAvailableError(RuntimeError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SpaceWarpingFailedError(RuntimeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NoVolumeFound(RuntimeError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WarmupRegException(Exception):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ZeroVolumeBoundingBox(Exception):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NoneCoordinateSuppliedError(ValueError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NoMapMatchingValues(ValueError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EmptyPointCloudError(ValueError):
|
|
63
|
+
pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from .plane3d import Plane3D
|
|
17
|
+
from .contour import Contour
|
|
18
|
+
from .cortical_profile_sampler import CorticalProfileSampler
|
|
19
|
+
from .patch import Patch
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from ..locations import point, pointcloud, boundingbox
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Contour(pointcloud.PointCloud):
|
|
22
|
+
"""
|
|
23
|
+
A PointCloud that represents a contour line.
|
|
24
|
+
The only difference is that the point order is relevant,
|
|
25
|
+
and consecutive points are thought as being connected by an edge.
|
|
26
|
+
|
|
27
|
+
In fact, PointCloud assumes order as well, but no connections between points.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, coordinates, space=None, sigma_mm=0, labels: list = None):
|
|
31
|
+
pointcloud.PointCloud.__init__(self, coordinates, space, sigma_mm, labels)
|
|
32
|
+
|
|
33
|
+
def crop(self, voi: boundingbox.BoundingBox):
|
|
34
|
+
"""
|
|
35
|
+
Crop the contour with a volume of interest.
|
|
36
|
+
Since the contour might be split from the cropping,
|
|
37
|
+
returns a set of contour segments.
|
|
38
|
+
"""
|
|
39
|
+
segments = []
|
|
40
|
+
|
|
41
|
+
# set the contour point labels to a linear numbering
|
|
42
|
+
# so we can use them after the intersection to detect splits.
|
|
43
|
+
old_labels = self.labels
|
|
44
|
+
self.labels = list(range(len(self)))
|
|
45
|
+
cropped = self.intersection(voi)
|
|
46
|
+
|
|
47
|
+
if cropped is not None and not isinstance(cropped, point.Point):
|
|
48
|
+
assert isinstance(cropped, pointcloud.PointCloud)
|
|
49
|
+
# Identifiy contour splits are by discontinuouities ("jumps")
|
|
50
|
+
# of their labels, which denote positions in the original contour
|
|
51
|
+
jumps = np.diff([self.labels.index(lb) for lb in cropped.labels])
|
|
52
|
+
splits = [0] + list(np.where(jumps > 1)[0] + 1) + [len(cropped)]
|
|
53
|
+
for i, j in zip(splits[:-1], splits[1:]):
|
|
54
|
+
segments.append(
|
|
55
|
+
self.__class__(cropped.coordinates[i:j, :], space=cropped.space)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# reset labels of the input contour points.
|
|
59
|
+
self.labels = old_labels
|
|
60
|
+
|
|
61
|
+
return segments
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from . import contour
|
|
17
|
+
from ..locations import point
|
|
18
|
+
from ..core import parcellation
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CorticalProfileSampler:
|
|
24
|
+
"""Samples cortical profiles from the cortical layer maps."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.layermap = parcellation.Parcellation.get_instance(
|
|
28
|
+
"cortical layers"
|
|
29
|
+
).get_map(space="bigbrain", maptype="labelled")
|
|
30
|
+
|
|
31
|
+
def query(self, query_point: point.Point):
|
|
32
|
+
q = query_point.warp(self.layermap.space)
|
|
33
|
+
smallest_dist = np.inf
|
|
34
|
+
best_match = None
|
|
35
|
+
for layername in self.layermap.regions:
|
|
36
|
+
vertices = self.layermap.fetch(region=layername, format="mesh")["verts"]
|
|
37
|
+
dists = np.sqrt(((vertices - q.coordinate) ** 2).sum(1))
|
|
38
|
+
best = np.argmin(dists)
|
|
39
|
+
if dists[best] < smallest_dist:
|
|
40
|
+
best_match = (layername, best)
|
|
41
|
+
smallest_dist = dists[best]
|
|
42
|
+
|
|
43
|
+
best_vertex = best_match[1]
|
|
44
|
+
hemisphere = "left" if "left" in best_match[0] else "right"
|
|
45
|
+
print(f"Best match is vertex #{best_match[1]} in {best_match[0]}.")
|
|
46
|
+
|
|
47
|
+
profile = [
|
|
48
|
+
(_, self.layermap.fetch(region=_, format="mesh")["verts"][best_vertex])
|
|
49
|
+
for _ in self.layermap.regions
|
|
50
|
+
if hemisphere in _
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
return contour.Contour(
|
|
54
|
+
[p[1] for p in profile],
|
|
55
|
+
space=self.layermap.space,
|
|
56
|
+
labels=[p[0] for p in profile],
|
|
57
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from ..volumes import volume
|
|
17
|
+
from ..locations import pointcloud, boundingbox
|
|
18
|
+
from ..commons import translation_matrix, y_rotation_matrix
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import math
|
|
22
|
+
from nilearn import image
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Patch:
|
|
26
|
+
|
|
27
|
+
def __init__(self, corners: pointcloud.PointCloud):
|
|
28
|
+
"""Construct a patch in physical coordinates.
|
|
29
|
+
As of now, only patches aligned in the y plane of the physical space
|
|
30
|
+
are supported."""
|
|
31
|
+
# TODO: need to ensure that the points are planar, if more than 3
|
|
32
|
+
assert len(corners) == 4
|
|
33
|
+
assert len(np.unique(corners.coordinates[:, 1])) == 1
|
|
34
|
+
self.corners = corners
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def space(self):
|
|
38
|
+
return self.corners.space
|
|
39
|
+
|
|
40
|
+
def flip(self):
|
|
41
|
+
"""Flips the patch. """
|
|
42
|
+
self.corners._coordinates = self.corners.coordinates[[2, 3, 0, 1]]
|
|
43
|
+
|
|
44
|
+
def extract_volume(self, image_volume: volume.Volume, resolution_mm: float):
|
|
45
|
+
"""
|
|
46
|
+
fetches image data in a planar patch.
|
|
47
|
+
TODO The current implementation only covers patches which are strictly
|
|
48
|
+
define in the y plane. A future implementation should accept arbitrary
|
|
49
|
+
oriented patches.accept arbitrary oriented patches.
|
|
50
|
+
"""
|
|
51
|
+
assert image_volume.space == self.space
|
|
52
|
+
|
|
53
|
+
# Extend the 2D patch into a 3D structure
|
|
54
|
+
# this is only valid if the patch plane lies within the image canvas.
|
|
55
|
+
canvas = image_volume.get_boundingbox()
|
|
56
|
+
assert canvas.minpoint[1] <= self.corners.coordinates[0, 1]
|
|
57
|
+
assert canvas.maxpoint[1] >= self.corners.coordinates[0, 1]
|
|
58
|
+
XYZ = self.corners.coordinates
|
|
59
|
+
voi = boundingbox.BoundingBox(
|
|
60
|
+
XYZ.min(0)[:3], XYZ.max(0)[:3], space=image_volume.space
|
|
61
|
+
)
|
|
62
|
+
# enforce the patch to have the same y dimensions
|
|
63
|
+
voi.minpoint[1] = canvas.minpoint[1]
|
|
64
|
+
voi.maxpoint[1] = canvas.maxpoint[1]
|
|
65
|
+
patch = image_volume.fetch(voi=voi, resolution_mm=resolution_mm)
|
|
66
|
+
assert patch is not None
|
|
67
|
+
|
|
68
|
+
# patch rotation defined in physical space
|
|
69
|
+
vx, vy, vz = XYZ[1] - XYZ[0]
|
|
70
|
+
alpha = -math.atan2(-vz, -vx)
|
|
71
|
+
cx, cy, cz = XYZ.mean(0)
|
|
72
|
+
rot_phys = np.linalg.multi_dot(
|
|
73
|
+
[
|
|
74
|
+
translation_matrix(cx, cy, cz),
|
|
75
|
+
y_rotation_matrix(alpha),
|
|
76
|
+
translation_matrix(-cx, -cy, -cz),
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# rotate the patch in physical space
|
|
81
|
+
affine_rot = np.dot(rot_phys, patch.affine)
|
|
82
|
+
|
|
83
|
+
# crop in the rotated space
|
|
84
|
+
pixels = (
|
|
85
|
+
np.dot(np.linalg.inv(affine_rot), self.corners.homogeneous.T)
|
|
86
|
+
.astype("int")
|
|
87
|
+
.T
|
|
88
|
+
)
|
|
89
|
+
# keep a pixel distance to avoid black border pixels
|
|
90
|
+
xmin, ymin, zmin = pixels.min(0)[:3] + 1
|
|
91
|
+
xmax, ymax, zmax = pixels.max(0)[:3] - 1
|
|
92
|
+
h, w = xmax - xmin, zmax - zmin
|
|
93
|
+
affine = np.dot(affine_rot, translation_matrix(xmin, 0, zmin))
|
|
94
|
+
return volume.from_nifti(
|
|
95
|
+
image.resample_img(patch, target_affine=affine, target_shape=[h, 1, w]),
|
|
96
|
+
space=image_volume.space,
|
|
97
|
+
name=f"Rotated patch with corner points {self.corners} sampled from {image_volume.name}",
|
|
98
|
+
)
|