siibra 1.0a1__1-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 -0
- siibra/__init__.py +164 -0
- siibra/commons.py +823 -0
- siibra/configuration/__init__.py +17 -0
- siibra/configuration/configuration.py +189 -0
- siibra/configuration/factory.py +589 -0
- siibra/core/__init__.py +16 -0
- siibra/core/assignment.py +110 -0
- siibra/core/atlas.py +239 -0
- siibra/core/concept.py +308 -0
- siibra/core/parcellation.py +387 -0
- siibra/core/region.py +1223 -0
- siibra/core/space.py +131 -0
- 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 +17 -0
- siibra/explorer/url.py +222 -0
- siibra/explorer/util.py +87 -0
- siibra/features/__init__.py +117 -0
- siibra/features/anchor.py +224 -0
- siibra/features/connectivity/__init__.py +33 -0
- siibra/features/connectivity/functional_connectivity.py +57 -0
- siibra/features/connectivity/regional_connectivity.py +494 -0
- siibra/features/connectivity/streamline_counts.py +27 -0
- siibra/features/connectivity/streamline_lengths.py +27 -0
- siibra/features/connectivity/tracing_connectivity.py +30 -0
- siibra/features/dataset/__init__.py +17 -0
- siibra/features/dataset/ebrains.py +90 -0
- siibra/features/feature.py +970 -0
- siibra/features/image/__init__.py +27 -0
- siibra/features/image/image.py +115 -0
- siibra/features/image/sections.py +26 -0
- siibra/features/image/volume_of_interest.py +88 -0
- siibra/features/tabular/__init__.py +24 -0
- siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
- siibra/features/tabular/cell_density_profile.py +298 -0
- siibra/features/tabular/cortical_profile.py +322 -0
- siibra/features/tabular/gene_expression.py +257 -0
- siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
- siibra/features/tabular/layerwise_cell_density.py +95 -0
- siibra/features/tabular/receptor_density_fingerprint.py +192 -0
- siibra/features/tabular/receptor_density_profile.py +110 -0
- siibra/features/tabular/regional_timeseries_activity.py +294 -0
- siibra/features/tabular/tabular.py +139 -0
- siibra/livequeries/__init__.py +19 -0
- siibra/livequeries/allen.py +352 -0
- siibra/livequeries/bigbrain.py +197 -0
- siibra/livequeries/ebrains.py +145 -0
- siibra/livequeries/query.py +49 -0
- siibra/locations/__init__.py +91 -0
- siibra/locations/boundingbox.py +454 -0
- siibra/locations/location.py +115 -0
- siibra/locations/point.py +344 -0
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +27 -0
- siibra/retrieval/cache.py +233 -0
- siibra/retrieval/datasets.py +389 -0
- siibra/retrieval/exceptions/__init__.py +27 -0
- siibra/retrieval/repositories.py +769 -0
- siibra/retrieval/requests.py +659 -0
- siibra/vocabularies/__init__.py +45 -0
- siibra/vocabularies/gene_names.json +29176 -0
- siibra/vocabularies/receptor_symbols.json +210 -0
- siibra/vocabularies/region_aliases.json +460 -0
- siibra/volumes/__init__.py +23 -0
- siibra/volumes/parcellationmap.py +1279 -0
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/providers/gifti.py +165 -0
- siibra/volumes/providers/neuroglancer.py +736 -0
- siibra/volumes/providers/nifti.py +266 -0
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +468 -0
- siibra/volumes/volume.py +892 -0
- siibra-1.0.0a1.dist-info/LICENSE +201 -0
- siibra-1.0.0a1.dist-info/METADATA +160 -0
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- siibra-1.0.0a1.dist-info/WHEEL +5 -0
- siibra-1.0.0a1.dist-info/top_level.txt +1 -0
siibra/core/space.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
"""A particular brain reference space."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .concept import AtlasConcept
|
|
19
|
+
|
|
20
|
+
from ..commons import logger, Species
|
|
21
|
+
|
|
22
|
+
from typing import List, TYPE_CHECKING, Union
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ..volumes import volume
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Space(AtlasConcept, configuration_folder="spaces"):
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
identifier: str,
|
|
33
|
+
name: str,
|
|
34
|
+
species: Union[str, Species],
|
|
35
|
+
volumes: List['volume.Volume'] = [],
|
|
36
|
+
shortname: str = "",
|
|
37
|
+
description: str = "",
|
|
38
|
+
modality: str = "",
|
|
39
|
+
publications: list = [],
|
|
40
|
+
datasets: list = [],
|
|
41
|
+
prerelease: bool = False,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Constructs a new parcellation object.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
identifier : str
|
|
49
|
+
Unique identifier of the space
|
|
50
|
+
name : str
|
|
51
|
+
Human-readable name of the space
|
|
52
|
+
species: str or Species
|
|
53
|
+
Specification of the species
|
|
54
|
+
volumes: list[Volume]
|
|
55
|
+
list of template volumes
|
|
56
|
+
shortname: str, optional
|
|
57
|
+
Shortform of human-readable name
|
|
58
|
+
description: str, optional
|
|
59
|
+
Textual description of the parcellation
|
|
60
|
+
modality : str or None
|
|
61
|
+
Specification of the modality representing this reference space
|
|
62
|
+
publications: list
|
|
63
|
+
List of associated publications, each a dictionary with "doi" and/or "citation" fields
|
|
64
|
+
ebrains_ids : dict
|
|
65
|
+
Identifiers of EBRAINS entities corresponding to this Parcellation.
|
|
66
|
+
Key: EBRAINS KG schema, value: EBRAINS KG @id
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
AtlasConcept.__init__(
|
|
70
|
+
self,
|
|
71
|
+
identifier=identifier,
|
|
72
|
+
name=name,
|
|
73
|
+
species=species,
|
|
74
|
+
shortname=shortname,
|
|
75
|
+
description=description,
|
|
76
|
+
modality=modality,
|
|
77
|
+
publications=publications,
|
|
78
|
+
datasets=datasets,
|
|
79
|
+
prerelease=prerelease,
|
|
80
|
+
)
|
|
81
|
+
self.volumes = volumes
|
|
82
|
+
for v in self.volumes:
|
|
83
|
+
v.space_info = {"@id": self.id}
|
|
84
|
+
|
|
85
|
+
def get_template(self, variant: str = None):
|
|
86
|
+
"""
|
|
87
|
+
Get the volumetric reference template for this space.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
variant: str, optional
|
|
92
|
+
Some templates are provided in different variants, e.g.
|
|
93
|
+
freesurfer is available as either white matter, pial or
|
|
94
|
+
inflated surface for left and right hemispheres (6 variants).
|
|
95
|
+
This field could be used to request a specific variant.
|
|
96
|
+
Per default, the first found variant is returned.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
Volume
|
|
101
|
+
representing the reference template, or None if not available.
|
|
102
|
+
"""
|
|
103
|
+
tests = []
|
|
104
|
+
if variant is not None:
|
|
105
|
+
tests.append(lambda v: hasattr(v, 'variant') and variant.lower() in v.variant.lower())
|
|
106
|
+
candidates = [v for v in self.volumes if all(t(v) for t in tests)]
|
|
107
|
+
|
|
108
|
+
if len(candidates) == 0:
|
|
109
|
+
msg = f"Volume variant {variant} not available for '{self.name}'. " \
|
|
110
|
+
if variant else f"No volumes available for '{self.name}'. "
|
|
111
|
+
raise RuntimeError(msg)
|
|
112
|
+
|
|
113
|
+
if len(candidates) > 1:
|
|
114
|
+
logger.info(
|
|
115
|
+
f"Multiple template variants available for '{self.name}': "
|
|
116
|
+
f"{', '.join(c.variant for c in candidates)}. "
|
|
117
|
+
f"'{candidates[0].variant}' is chosen, but you might specify another with the 'variant' parameter."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
template = candidates[0]
|
|
121
|
+
if len(template.datasets) == 0:
|
|
122
|
+
template.datasets = self.datasets
|
|
123
|
+
return template
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def provides_mesh(self):
|
|
127
|
+
return any(v.provides_mesh for v in self.volumes)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def provides_image(self):
|
|
131
|
+
return any(v.provides_image for v in self.volumes)
|
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
|
+
)
|