siibra 1.0a14__py3-none-any.whl → 1.0.1a0__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 +15 -5
- siibra/commons.py +3 -48
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +1 -1
- siibra/configuration/factory.py +164 -127
- siibra/core/__init__.py +1 -1
- siibra/core/assignment.py +1 -1
- siibra/core/atlas.py +24 -17
- siibra/core/concept.py +18 -9
- siibra/core/parcellation.py +76 -55
- siibra/core/region.py +163 -183
- siibra/core/space.py +3 -1
- siibra/core/structure.py +1 -2
- siibra/exceptions.py +17 -1
- siibra/experimental/contour.py +6 -6
- siibra/experimental/patch.py +2 -2
- siibra/experimental/plane3d.py +8 -8
- siibra/explorer/__init__.py +1 -1
- siibra/explorer/url.py +15 -0
- siibra/explorer/util.py +1 -1
- siibra/features/__init__.py +1 -1
- siibra/features/anchor.py +13 -14
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +1 -1
- siibra/features/connectivity/regional_connectivity.py +7 -5
- 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 +1 -1
- siibra/features/feature.py +50 -28
- siibra/features/image/__init__.py +1 -1
- siibra/features/image/image.py +18 -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 +2 -2
- siibra/features/tabular/cell_density_profile.py +102 -66
- siibra/features/tabular/cortical_profile.py +5 -3
- siibra/features/tabular/gene_expression.py +1 -1
- siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
- siibra/features/tabular/layerwise_cell_density.py +8 -25
- siibra/features/tabular/receptor_density_fingerprint.py +5 -3
- siibra/features/tabular/receptor_density_profile.py +5 -3
- siibra/features/tabular/regional_timeseries_activity.py +7 -5
- siibra/features/tabular/tabular.py +5 -3
- siibra/livequeries/__init__.py +1 -1
- siibra/livequeries/allen.py +46 -20
- siibra/livequeries/bigbrain.py +9 -9
- siibra/livequeries/ebrains.py +1 -1
- siibra/livequeries/query.py +1 -2
- siibra/locations/__init__.py +10 -10
- siibra/locations/boundingbox.py +77 -38
- siibra/locations/location.py +12 -4
- siibra/locations/point.py +14 -9
- siibra/locations/{pointset.py → pointcloud.py} +69 -27
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +1 -1
- siibra/retrieval/datasets.py +1 -1
- siibra/retrieval/exceptions/__init__.py +1 -1
- siibra/retrieval/repositories.py +10 -27
- siibra/retrieval/requests.py +20 -3
- siibra/vocabularies/__init__.py +1 -1
- siibra/volumes/__init__.py +2 -2
- siibra/volumes/parcellationmap.py +121 -94
- siibra/volumes/providers/__init__.py +1 -1
- siibra/volumes/providers/freesurfer.py +1 -1
- siibra/volumes/providers/gifti.py +1 -1
- siibra/volumes/providers/neuroglancer.py +68 -42
- siibra/volumes/providers/nifti.py +18 -28
- siibra/volumes/providers/provider.py +2 -2
- siibra/volumes/sparsemap.py +128 -247
- siibra/volumes/volume.py +252 -65
- {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/METADATA +17 -4
- siibra-1.0.1a0.dist-info/RECORD +84 -0
- {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
- siibra-1.0a14.dist-info/RECORD +0 -84
- {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
- {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/top_level.txt +0 -0
siibra/livequeries/allen.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");
|
|
@@ -19,12 +19,12 @@ from .query import LiveQuery
|
|
|
19
19
|
from ..core import space as _space, structure
|
|
20
20
|
from ..features import anchor as _anchor
|
|
21
21
|
from ..features.tabular.gene_expression import GeneExpressions
|
|
22
|
-
from ..commons import logger, Species
|
|
23
|
-
from ..locations import point,
|
|
22
|
+
from ..commons import logger, Species
|
|
23
|
+
from ..locations import point, pointcloud
|
|
24
24
|
from ..retrieval import HttpRequest
|
|
25
25
|
from ..vocabularies import GENE_NAMES
|
|
26
26
|
|
|
27
|
-
from typing import
|
|
27
|
+
from typing import List
|
|
28
28
|
from xml.etree import ElementTree
|
|
29
29
|
import numpy as np
|
|
30
30
|
import json
|
|
@@ -32,6 +32,24 @@ import json
|
|
|
32
32
|
|
|
33
33
|
BASE_URL = "http://api.brain-map.org/api/v2/data"
|
|
34
34
|
|
|
35
|
+
LOCATION_PRECISION_MM = 2. # the assumed spatial precision of the probe locations in MNI space
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_allen_api_microarray_service_available():
|
|
39
|
+
import requests
|
|
40
|
+
|
|
41
|
+
# see https://community.brain-map.org/t/human-brain-atlas-api/2876
|
|
42
|
+
microarray_test_url = "http://api.brain-map.org/api/v2/data/query.json?criteria= service::human_microarray_expression[probes$eq1023146,1023147][donors$eq15496][structures$eq9148]"
|
|
43
|
+
try:
|
|
44
|
+
response = requests.get(microarray_test_url).json()
|
|
45
|
+
except requests.RequestException:
|
|
46
|
+
return False
|
|
47
|
+
return response["success"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InvalidAllenAPIResponseException(Exception):
|
|
51
|
+
pass
|
|
52
|
+
|
|
35
53
|
|
|
36
54
|
class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions):
|
|
37
55
|
"""
|
|
@@ -101,9 +119,6 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
101
119
|
"""
|
|
102
120
|
LiveQuery.__init__(self, **kwargs)
|
|
103
121
|
gene = kwargs.get('gene')
|
|
104
|
-
self.maptype = kwargs.get("maptype", None)
|
|
105
|
-
if isinstance(self.maptype, str):
|
|
106
|
-
self.maptype = MapType[self.maptype.upper()]
|
|
107
122
|
|
|
108
123
|
def parse_gene(spec):
|
|
109
124
|
if isinstance(spec, str):
|
|
@@ -120,7 +135,14 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
120
135
|
|
|
121
136
|
self.genes = parse_gene(gene)
|
|
122
137
|
|
|
123
|
-
def query(self, concept: structure.BrainStructure) ->
|
|
138
|
+
def query(self, concept: structure.BrainStructure) -> List[GeneExpressions]:
|
|
139
|
+
if not is_allen_api_microarray_service_available():
|
|
140
|
+
raise InvalidAllenAPIResponseException(
|
|
141
|
+
'The service "web API of the Allen Brain Atlas for the human microarray expression" '
|
|
142
|
+
'is not available at the moment, therefore siibra is not able to fetch '
|
|
143
|
+
'gene expression features. This is a known issue which we are investigating: '
|
|
144
|
+
'https://github.com/FZJ-INM1-BDA/siibra-python/issues/636.'
|
|
145
|
+
)
|
|
124
146
|
|
|
125
147
|
mnispace = _space.Space.registry().get('mni152')
|
|
126
148
|
|
|
@@ -128,20 +150,21 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
128
150
|
# Record matched instances and their locations.
|
|
129
151
|
measurements = []
|
|
130
152
|
coordinates = []
|
|
131
|
-
points_inside = dict()
|
|
132
153
|
for measurement in self:
|
|
133
|
-
pt = point.Point(measurement['mni_xyz'], space=mnispace)
|
|
134
|
-
if pt
|
|
135
|
-
points_inside[pt] = pt in concept
|
|
136
|
-
if points_inside[pt]:
|
|
154
|
+
pt = point.Point(measurement['mni_xyz'], space=mnispace, sigma_mm=LOCATION_PRECISION_MM)
|
|
155
|
+
if pt in concept:
|
|
137
156
|
measurements.append(measurement)
|
|
138
157
|
coordinates.append(pt)
|
|
139
158
|
|
|
159
|
+
if len(coordinates) == 0:
|
|
160
|
+
logger.info(f"No probes found that lie within {concept}")
|
|
161
|
+
return []
|
|
162
|
+
|
|
140
163
|
# Build the anatomical anchor and assignment to the query concept.
|
|
141
164
|
# It will be attached to the returned feature, with the set of matched
|
|
142
165
|
# MNI coordinates as anchor's location.
|
|
143
166
|
anchor = _anchor.AnatomicalAnchor(
|
|
144
|
-
location=
|
|
167
|
+
location=pointcloud.from_points(coordinates),
|
|
145
168
|
species=self.species
|
|
146
169
|
)
|
|
147
170
|
explanation = f"MNI coordinates of tissue samples were filtered using {concept}"
|
|
@@ -153,7 +176,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
153
176
|
)]
|
|
154
177
|
anchor._last_matched_concept = concept
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
return [GeneExpressions(
|
|
157
180
|
anchor=anchor,
|
|
158
181
|
genes=[m['gene'] for m in measurements],
|
|
159
182
|
levels=[m['expression_level'] for m in measurements],
|
|
@@ -167,7 +190,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
167
190
|
"probe_id": [m['probe_id'] for m in measurements],
|
|
168
191
|
"donor_name": [m['donor_name'] for m in measurements],
|
|
169
192
|
}
|
|
170
|
-
)
|
|
193
|
+
)]
|
|
171
194
|
|
|
172
195
|
def __iter__(self):
|
|
173
196
|
|
|
@@ -261,7 +284,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
261
284
|
url = AllenBrainAtlasQuery._QUERY["specimen"].format(specimen_id=specimen_id)
|
|
262
285
|
response = HttpRequest(url).get()
|
|
263
286
|
if not response["success"]:
|
|
264
|
-
raise
|
|
287
|
+
raise InvalidAllenAPIResponseException(
|
|
265
288
|
"Invalid response when retrieving specimen information: {}".format(url)
|
|
266
289
|
)
|
|
267
290
|
# we ask for 1 specimen, so list should have length 1
|
|
@@ -278,7 +301,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
278
301
|
return specimen
|
|
279
302
|
|
|
280
303
|
@classmethod
|
|
281
|
-
def _retrieve_microarray(cls, donor_id: str, probe_ids: str)
|
|
304
|
+
def _retrieve_microarray(cls, donor_id: str, probe_ids: str):
|
|
282
305
|
"""
|
|
283
306
|
Retrieve microarray data for several probes of a given donor, and
|
|
284
307
|
compute the MRI position of the corresponding tissue block in the ICBM
|
|
@@ -292,9 +315,12 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
|
|
|
292
315
|
url = AllenBrainAtlasQuery._QUERY["microarray"].format(
|
|
293
316
|
probe_ids=",".join([str(id) for id in probe_ids]), donor_id=donor_id
|
|
294
317
|
)
|
|
295
|
-
|
|
318
|
+
try:
|
|
319
|
+
response = HttpRequest(url, json.loads).get()
|
|
320
|
+
except json.JSONDecodeError as e:
|
|
321
|
+
raise RuntimeError(f"Allen institute site produced an empty response - please try again later.\n{e}")
|
|
296
322
|
if not response["success"]:
|
|
297
|
-
raise
|
|
323
|
+
raise InvalidAllenAPIResponseException(
|
|
298
324
|
"Invalid response when retrieving microarray data: {}".format(url)
|
|
299
325
|
)
|
|
300
326
|
|
siibra/livequeries/bigbrain.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");
|
|
@@ -19,7 +19,7 @@ from . import query
|
|
|
19
19
|
from ..features.tabular import bigbrain_intensity_profile, layerwise_bigbrain_intensities
|
|
20
20
|
from ..features import anchor as _anchor
|
|
21
21
|
from ..commons import logger
|
|
22
|
-
from ..locations import point,
|
|
22
|
+
from ..locations import point, pointcloud
|
|
23
23
|
from ..core import structure
|
|
24
24
|
from ..retrieval import requests, cache
|
|
25
25
|
from ..retrieval.datasets import GenericDataset
|
|
@@ -119,11 +119,11 @@ class BigBrainProfileQuery(query.LiveQuery, args=[], FeatureType=bigbrain_intens
|
|
|
119
119
|
|
|
120
120
|
def query(self, concept: structure.BrainStructure, **kwargs) -> List[bigbrain_intensity_profile.BigBrainIntensityProfile]:
|
|
121
121
|
loader = WagstylProfileLoader()
|
|
122
|
-
mesh_vertices =
|
|
123
|
-
matched = concept.intersection(mesh_vertices) # returns a reduced
|
|
122
|
+
mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
|
|
123
|
+
matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels
|
|
124
124
|
if matched is None:
|
|
125
125
|
return []
|
|
126
|
-
assert isinstance(matched,
|
|
126
|
+
assert isinstance(matched, pointcloud.PointCloud)
|
|
127
127
|
indices = matched.labels
|
|
128
128
|
assert indices is not None
|
|
129
129
|
features = []
|
|
@@ -159,11 +159,11 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
|
|
|
159
159
|
def query(self, concept: structure.BrainStructure, **kwargs) -> List[layerwise_bigbrain_intensities.LayerwiseBigBrainIntensities]:
|
|
160
160
|
|
|
161
161
|
loader = WagstylProfileLoader()
|
|
162
|
-
mesh_vertices =
|
|
163
|
-
matched = concept.intersection(mesh_vertices) # returns a reduced
|
|
162
|
+
mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
|
|
163
|
+
matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels
|
|
164
164
|
if matched is None:
|
|
165
165
|
return []
|
|
166
|
-
assert isinstance(matched,
|
|
166
|
+
assert isinstance(matched, pointcloud.PointCloud)
|
|
167
167
|
indices = matched.labels
|
|
168
168
|
assert indices is not None
|
|
169
169
|
matched_profiles = loader._profiles[indices, :]
|
|
@@ -177,7 +177,7 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
|
|
|
177
177
|
]).reshape((-1, 200))
|
|
178
178
|
|
|
179
179
|
anchor = _anchor.AnatomicalAnchor(
|
|
180
|
-
location=
|
|
180
|
+
location=pointcloud.PointCloud(loader._vertices[indices, :], space='bigbrain'),
|
|
181
181
|
region=str(concept),
|
|
182
182
|
species='Homo sapiens'
|
|
183
183
|
)
|
siibra/livequeries/ebrains.py
CHANGED
siibra/livequeries/query.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");
|
|
@@ -47,4 +47,3 @@ class LiveQuery(ABC):
|
|
|
47
47
|
@abstractmethod
|
|
48
48
|
def query(self, concept: AtlasConcept, **kwargs) -> List[Feature]:
|
|
49
49
|
raise NotImplementedError(f"Dervied class {self.__class__} needs to implement query()")
|
|
50
|
-
pass
|
siibra/locations/__init__.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");
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
from .location import Location
|
|
18
18
|
from .point import Point
|
|
19
|
-
from .
|
|
19
|
+
from .pointcloud import PointCloud, from_points
|
|
20
20
|
from .boundingbox import BoundingBox
|
|
21
21
|
|
|
22
22
|
|
|
@@ -36,11 +36,11 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
|
|
|
36
36
|
Returns
|
|
37
37
|
-------
|
|
38
38
|
Location
|
|
39
|
-
- Point U Point =
|
|
40
|
-
- Point U
|
|
41
|
-
-
|
|
39
|
+
- Point U Point = PointCloud
|
|
40
|
+
- Point U PointCloud = PointCloud
|
|
41
|
+
- PointCloud U PointCloud = PointCloud
|
|
42
42
|
- BoundingBox U BoundingBox = BoundingBox
|
|
43
|
-
- BoundingBox U
|
|
43
|
+
- BoundingBox U PointCloud = BoundingBox
|
|
44
44
|
- BoundingBox U Point = BoundingBox
|
|
45
45
|
- WholeBrain U Location = NotImplementedError
|
|
46
46
|
(all operations are commutative)
|
|
@@ -53,14 +53,14 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
|
|
|
53
53
|
# location that has its own union method since it is not a part of locations
|
|
54
54
|
# module and to avoid importing Volume here.
|
|
55
55
|
if not all(
|
|
56
|
-
isinstance(loc, (Point,
|
|
56
|
+
isinstance(loc, (Point, PointCloud, BoundingBox)) for loc in [loc0, loc1]
|
|
57
57
|
):
|
|
58
58
|
try:
|
|
59
59
|
return loc1.union(loc0)
|
|
60
60
|
except Exception:
|
|
61
61
|
raise NotImplementedError(f"There are no union method for {(loc0.__class__.__name__, loc1.__class__.__name__)}")
|
|
62
62
|
|
|
63
|
-
# convert Points to
|
|
63
|
+
# convert Points to PointClouds
|
|
64
64
|
loc0, loc1 = [
|
|
65
65
|
from_points([loc]) if isinstance(loc, Point) else loc
|
|
66
66
|
for loc in [loc0, loc1]
|
|
@@ -69,8 +69,8 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
|
|
|
69
69
|
# adopt the space of the first location
|
|
70
70
|
loc1_w = loc1.warp(loc0.space)
|
|
71
71
|
|
|
72
|
-
if isinstance(loc0,
|
|
73
|
-
if isinstance(loc1_w,
|
|
72
|
+
if isinstance(loc0, PointCloud):
|
|
73
|
+
if isinstance(loc1_w, PointCloud):
|
|
74
74
|
points = list(dict.fromkeys([*loc0, *loc1_w]))
|
|
75
75
|
return from_points(points)
|
|
76
76
|
if isinstance(loc1_w, BoundingBox):
|
siibra/locations/boundingbox.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");
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A box defined by two farthest corner coordinates on a specific space."""
|
|
16
16
|
|
|
17
|
-
from . import point,
|
|
17
|
+
from . import point, pointcloud, location
|
|
18
18
|
|
|
19
19
|
from ..commons import logger
|
|
20
20
|
from ..exceptions import SpaceWarpingFailedError
|
|
21
21
|
|
|
22
|
+
from itertools import product
|
|
22
23
|
import hashlib
|
|
23
24
|
import numpy as np
|
|
24
25
|
from typing import TYPE_CHECKING, Union
|
|
@@ -75,6 +76,7 @@ class BoundingBox(location.Location):
|
|
|
75
76
|
s1, s2 = sigma_mm
|
|
76
77
|
else:
|
|
77
78
|
raise ValueError(f"Cannot interpret sigma_mm parameter value {sigma_mm} for bounding box")
|
|
79
|
+
self.sigma_mm = [s1, s2]
|
|
78
80
|
self.minpoint = point.Point([min(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s1)
|
|
79
81
|
self.maxpoint = point.Point([max(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s2)
|
|
80
82
|
if minsize is not None:
|
|
@@ -82,6 +84,9 @@ class BoundingBox(location.Location):
|
|
|
82
84
|
if self.shape[d] < minsize:
|
|
83
85
|
self.maxpoint[d] = self.minpoint[d] + minsize
|
|
84
86
|
|
|
87
|
+
if self.volume == 0:
|
|
88
|
+
logger.warning(f"Zero-volume bounding box from points {point1} and {point2} in {self.space} space.")
|
|
89
|
+
|
|
85
90
|
@property
|
|
86
91
|
def id(self) -> str:
|
|
87
92
|
return hashlib.md5(str(self).encode("utf-8")).hexdigest()
|
|
@@ -110,12 +115,12 @@ class BoundingBox(location.Location):
|
|
|
110
115
|
def __str__(self):
|
|
111
116
|
if self.space is None:
|
|
112
117
|
return (
|
|
113
|
-
f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})
|
|
114
|
-
f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})
|
|
118
|
+
f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
|
|
119
|
+
f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm"
|
|
115
120
|
)
|
|
116
121
|
else:
|
|
117
122
|
return (
|
|
118
|
-
f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})
|
|
123
|
+
f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
|
|
119
124
|
f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm in {self.space.name} space"
|
|
120
125
|
)
|
|
121
126
|
|
|
@@ -139,16 +144,16 @@ class BoundingBox(location.Location):
|
|
|
139
144
|
if isinstance(other, point.Point):
|
|
140
145
|
warped = other.warp(self.space)
|
|
141
146
|
return other if self.minpoint <= warped <= self.maxpoint else None
|
|
142
|
-
if isinstance(other,
|
|
147
|
+
if isinstance(other, pointcloud.PointCloud):
|
|
143
148
|
points_inside = [p for p in other if self.intersects(p)]
|
|
144
|
-
|
|
149
|
+
if len(points_inside) == 0:
|
|
150
|
+
return None
|
|
151
|
+
result = pointcloud.PointCloud(
|
|
145
152
|
points_inside,
|
|
146
153
|
space=other.space,
|
|
147
154
|
sigma_mm=[p.sigma for p in points_inside]
|
|
148
155
|
)
|
|
149
|
-
if len(result) ==
|
|
150
|
-
return None
|
|
151
|
-
return result[0] if len(result) == 1 else result # if PointSet has single point return as a Point
|
|
156
|
+
return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
|
|
152
157
|
|
|
153
158
|
return other.intersection(self)
|
|
154
159
|
|
|
@@ -183,12 +188,18 @@ class BoundingBox(location.Location):
|
|
|
183
188
|
result_minpt.append(A[dim])
|
|
184
189
|
result_maxpt.append(B[dim])
|
|
185
190
|
|
|
191
|
+
if result_minpt == result_maxpt:
|
|
192
|
+
return result_minpt
|
|
193
|
+
|
|
186
194
|
bbox = BoundingBox(
|
|
187
195
|
point1=point.Point(result_minpt, self.space),
|
|
188
196
|
point2=point.Point(result_maxpt, self.space),
|
|
189
197
|
space=self.space,
|
|
190
198
|
)
|
|
191
|
-
|
|
199
|
+
|
|
200
|
+
if bbox.volume == 0 and sum(cmin == cmax for cmin, cmax in zip(result_minpt, result_maxpt)) == 2:
|
|
201
|
+
return None
|
|
202
|
+
return bbox
|
|
192
203
|
|
|
193
204
|
def _intersect_mask(self, mask: 'Nifti1Image', threshold=0):
|
|
194
205
|
"""Intersect this bounding box with an image mask. Returns None if they do not intersect.
|
|
@@ -229,8 +240,8 @@ class BoundingBox(location.Location):
|
|
|
229
240
|
)
|
|
230
241
|
else:
|
|
231
242
|
return self._intersect_bbox(
|
|
232
|
-
|
|
233
|
-
.
|
|
243
|
+
pointcloud
|
|
244
|
+
.PointCloud(XYZ, space=self.space, sigma_mm=voxel_size.max())
|
|
234
245
|
.boundingbox
|
|
235
246
|
)
|
|
236
247
|
|
|
@@ -245,6 +256,35 @@ class BoundingBox(location.Location):
|
|
|
245
256
|
sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma]
|
|
246
257
|
)
|
|
247
258
|
|
|
259
|
+
@property
|
|
260
|
+
def corners(self):
|
|
261
|
+
"""
|
|
262
|
+
Returns all 8 corners of the box as a pointcloud.
|
|
263
|
+
|
|
264
|
+
Note
|
|
265
|
+
----
|
|
266
|
+
x0, y0, z0 = self.minpoint
|
|
267
|
+
x1, y1, z1 = self.maxpoint
|
|
268
|
+
all_corners = [
|
|
269
|
+
(x0, y0, z0),
|
|
270
|
+
(x1, y0, z0),
|
|
271
|
+
(x0, y1, z0),
|
|
272
|
+
(x1, y1, z0),
|
|
273
|
+
(x0, y0, z1),
|
|
274
|
+
(x1, y0, z1),
|
|
275
|
+
(x0, y1, z1),
|
|
276
|
+
(x1, y1, z1)
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
TODO: deal with sigma. Currently, returns the mean of min and max point.
|
|
280
|
+
"""
|
|
281
|
+
xs, ys, zs = zip(self.minpoint, self.maxpoint)
|
|
282
|
+
return pointcloud.PointCloud(
|
|
283
|
+
coordinates=[[x, y, z] for x, y, z in product(xs, ys, zs)],
|
|
284
|
+
space=self.space,
|
|
285
|
+
sigma_mm=np.mean([self.minpoint.sigma, self.maxpoint.sigma])
|
|
286
|
+
)
|
|
287
|
+
|
|
248
288
|
def warp(self, space):
|
|
249
289
|
"""Returns a new bounding box obtained by warping the
|
|
250
290
|
min- and maxpoint of this one into the new target space.
|
|
@@ -257,13 +297,10 @@ class BoundingBox(location.Location):
|
|
|
257
297
|
return self
|
|
258
298
|
else:
|
|
259
299
|
try:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
point2=self.maxpoint.warp(spaceobj),
|
|
263
|
-
space=spaceobj,
|
|
264
|
-
)
|
|
265
|
-
except ValueError:
|
|
300
|
+
warped_corners = self.corners.warp(spaceobj)
|
|
301
|
+
except SpaceWarpingFailedError:
|
|
266
302
|
raise SpaceWarpingFailedError(f"Warping {str(self)} to {spaceobj.name} not successful.")
|
|
303
|
+
return warped_corners.boundingbox
|
|
267
304
|
|
|
268
305
|
def transform(self, affine: np.ndarray, space=None):
|
|
269
306
|
"""Returns a new bounding box obtained by transforming the
|
|
@@ -282,12 +319,9 @@ class BoundingBox(location.Location):
|
|
|
282
319
|
"""
|
|
283
320
|
from ..core.space import Space
|
|
284
321
|
spaceobj = Space.get_instance(space)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
space=space,
|
|
289
|
-
sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma] # TODO: error propagation
|
|
290
|
-
)
|
|
322
|
+
result = self.corners.transform(affine, spaceobj).boundingbox
|
|
323
|
+
result.sigma_mm = [self.minpoint.sigma, self.maxpoint.sigma] # TODO: error propagation
|
|
324
|
+
return result
|
|
291
325
|
|
|
292
326
|
def shift(self, offset):
|
|
293
327
|
return self.__class__(
|
|
@@ -326,7 +360,7 @@ class BoundingBox(location.Location):
|
|
|
326
360
|
x1, y1, z1 = self.maxpoint
|
|
327
361
|
|
|
328
362
|
# set of 8 corner points in source space
|
|
329
|
-
corners1 =
|
|
363
|
+
corners1 = pointcloud.PointCloud(
|
|
330
364
|
[
|
|
331
365
|
(x0, y0, z0),
|
|
332
366
|
(x0, y0, z1),
|
|
@@ -383,14 +417,15 @@ class BoundingBox(location.Location):
|
|
|
383
417
|
return super().__hash__()
|
|
384
418
|
|
|
385
419
|
|
|
386
|
-
def _determine_bounds(
|
|
420
|
+
def _determine_bounds(masked_array: np.ndarray, background: float = 0.0):
|
|
387
421
|
"""
|
|
388
|
-
Bounding box of nonzero values in a 3D array.
|
|
422
|
+
Bounding box of nonzero (background) values in a 3D array.
|
|
423
|
+
|
|
389
424
|
https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
|
|
390
425
|
"""
|
|
391
|
-
x = np.any(
|
|
392
|
-
y = np.any(
|
|
393
|
-
z = np.any(
|
|
426
|
+
x = np.any(masked_array != background, axis=(1, 2))
|
|
427
|
+
y = np.any(masked_array != background, axis=(0, 2))
|
|
428
|
+
z = np.any(masked_array != background, axis=(0, 1))
|
|
394
429
|
nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
|
|
395
430
|
if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
|
|
396
431
|
# empty array
|
|
@@ -401,15 +436,19 @@ def _determine_bounds(array: np.ndarray, threshold=0):
|
|
|
401
436
|
return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
|
|
402
437
|
|
|
403
438
|
|
|
404
|
-
def from_array(
|
|
439
|
+
def from_array(
|
|
440
|
+
array: np.ndarray,
|
|
441
|
+
background: Union[int, float] = 0.0,
|
|
442
|
+
space: "Space" = None
|
|
443
|
+
) -> BoundingBox:
|
|
405
444
|
"""
|
|
406
|
-
Find the bounding box of
|
|
445
|
+
Find the bounding box of non-background values for any 3D array.
|
|
407
446
|
|
|
408
447
|
Parameters
|
|
409
448
|
----------
|
|
410
|
-
array
|
|
411
|
-
|
|
412
|
-
space
|
|
449
|
+
array: np.ndarray
|
|
450
|
+
background: int or float, default: 0.0
|
|
451
|
+
space: Space, default: None
|
|
413
452
|
"""
|
|
414
|
-
bounds = _determine_bounds(array,
|
|
415
|
-
return BoundingBox(bounds[:3, 0], bounds[:3, 1], space=space)
|
|
453
|
+
bounds = _determine_bounds(array, background)
|
|
454
|
+
return BoundingBox(point1=bounds[:3, 0], point2=bounds[:3, 1], space=space)
|
siibra/locations/location.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");
|
|
@@ -21,6 +21,10 @@ from ..core.structure import BrainStructure
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from abc import abstractmethod
|
|
23
23
|
|
|
24
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from siibra.core.space import Space
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class Location(BrainStructure):
|
|
26
30
|
"""
|
|
@@ -42,15 +46,19 @@ class Location(BrainStructure):
|
|
|
42
46
|
_MASK_MEMO = {} # cache region masks for Location._assign_region()
|
|
43
47
|
_ASSIGNMENT_CACHE = {} # caches assignment results, see Region.assign()
|
|
44
48
|
|
|
45
|
-
def __init__(self,
|
|
46
|
-
self._space_spec =
|
|
49
|
+
def __init__(self, spacespec: Union[str, Dict[str, str], "Space"]):
|
|
50
|
+
self._space_spec = spacespec
|
|
47
51
|
self._space_cached = None
|
|
48
52
|
|
|
49
53
|
@property
|
|
50
54
|
def space(self):
|
|
51
55
|
if self._space_cached is None:
|
|
52
56
|
from ..core.space import Space
|
|
53
|
-
|
|
57
|
+
if isinstance(self._space_spec, dict):
|
|
58
|
+
spec = self._space_spec.get("@id") or self._space_spec.get("name")
|
|
59
|
+
self._space_cached = Space.get_instance(spec)
|
|
60
|
+
else:
|
|
61
|
+
self._space_cached = Space.get_instance(self._space_spec)
|
|
54
62
|
return self._space_cached
|
|
55
63
|
|
|
56
64
|
@abstractmethod
|
siibra/locations/point.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");
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Singular coordinate defined on a space, possibly with an uncertainty."""
|
|
16
16
|
|
|
17
|
-
from . import location, boundingbox,
|
|
17
|
+
from . import location, boundingbox, pointcloud
|
|
18
18
|
|
|
19
19
|
from ..commons import logger
|
|
20
20
|
from ..retrieval.requests import HttpRequest
|
|
21
|
+
from ..exceptions import SpaceWarpingFailedError, NoneCoordinateSuppliedError
|
|
21
22
|
|
|
22
23
|
from urllib.parse import quote
|
|
23
24
|
import re
|
|
@@ -55,10 +56,14 @@ class Point(location.Location):
|
|
|
55
56
|
if len(digits) == 3:
|
|
56
57
|
return tuple(float(d) for d in digits)
|
|
57
58
|
elif isinstance(spec, (tuple, list)) and len(spec) in [3, 4]:
|
|
59
|
+
if any(v is None for v in spec):
|
|
60
|
+
raise NoneCoordinateSuppliedError("Cannot parse cooridantes containing None values.")
|
|
58
61
|
if len(spec) == 4:
|
|
59
62
|
assert spec[3] == 1
|
|
60
63
|
return tuple(float(v.item()) if isinstance(v, np.ndarray) else float(v) for v in spec[:3])
|
|
61
64
|
elif isinstance(spec, np.ndarray) and spec.size == 3:
|
|
65
|
+
if any(np.isnan(v) for v in spec):
|
|
66
|
+
raise NoneCoordinateSuppliedError("Cannot parse cooridantes containing NaN values.")
|
|
62
67
|
return tuple(float(v.item()) if isinstance(v, np.ndarray) else float(v) for v in spec[:3])
|
|
63
68
|
elif isinstance(spec, Point):
|
|
64
69
|
return spec.coordinate
|
|
@@ -110,7 +115,7 @@ class Point(location.Location):
|
|
|
110
115
|
def intersection(self, other: location.Location) -> "Point":
|
|
111
116
|
if isinstance(other, Point):
|
|
112
117
|
return self if self == other else None
|
|
113
|
-
elif isinstance(other,
|
|
118
|
+
elif isinstance(other, pointcloud.PointCloud):
|
|
114
119
|
return self if self in other else None
|
|
115
120
|
else:
|
|
116
121
|
return self if other.intersection(self) else None
|
|
@@ -125,7 +130,7 @@ class Point(location.Location):
|
|
|
125
130
|
if spaceobj == self.space:
|
|
126
131
|
return self
|
|
127
132
|
if any(_ not in location.Location.SPACEWARP_IDS for _ in [self.space.id, spaceobj.id]):
|
|
128
|
-
raise
|
|
133
|
+
raise SpaceWarpingFailedError(
|
|
129
134
|
f"Cannot convert coordinates between {self.space.id} and {spaceobj.id}"
|
|
130
135
|
)
|
|
131
136
|
url = "{server}/transform-point?source_space={src}&target_space={tgt}&x={x}&y={y}&z={z}".format(
|
|
@@ -137,9 +142,9 @@ class Point(location.Location):
|
|
|
137
142
|
z=self.coordinate[2],
|
|
138
143
|
)
|
|
139
144
|
response = HttpRequest(url, lambda b: json.loads(b.decode())).get()
|
|
140
|
-
if any(
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
if np.any(np.isnan(response['target_point'])):
|
|
146
|
+
raise SpaceWarpingFailedError(f'Warping {str(self)} to {spaceobj.name} resulted in NaN')
|
|
147
|
+
|
|
143
148
|
return self.__class__(
|
|
144
149
|
coordinatespec=tuple(response["target_point"]),
|
|
145
150
|
space=spaceobj.id,
|
|
@@ -185,8 +190,8 @@ class Point(location.Location):
|
|
|
185
190
|
return super().__hash__()
|
|
186
191
|
|
|
187
192
|
def __eq__(self, other: 'Point'):
|
|
188
|
-
if isinstance(other,
|
|
189
|
-
return other == self # implemented at
|
|
193
|
+
if isinstance(other, pointcloud.PointCloud):
|
|
194
|
+
return other == self # implemented at pointcloud
|
|
190
195
|
if not isinstance(other, Point):
|
|
191
196
|
return False
|
|
192
197
|
o = other if self.space is None else other.warp(self.space)
|