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/region.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");
|
|
@@ -13,35 +13,31 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Representation of a brain region."""
|
|
16
|
-
from . import concept, space as _space, parcellation as _parcellation
|
|
17
|
-
from .relation_qualification import Qualification as RegionRelationship, RelationAssignment
|
|
18
16
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
17
|
+
from . import concept, structure, space as _space, parcellation as _parcellation
|
|
18
|
+
from .assignment import Qualification, AnatomicalAssignment
|
|
21
19
|
|
|
20
|
+
from ..retrieval.cache import cache_user_fn
|
|
21
|
+
from ..locations import location, pointcloud, boundingbox as _boundingbox
|
|
22
|
+
from ..volumes import parcellationmap, volume
|
|
22
23
|
from ..commons import (
|
|
23
24
|
logger,
|
|
24
25
|
MapType,
|
|
25
|
-
affine_scaling,
|
|
26
26
|
create_key,
|
|
27
27
|
clear_name,
|
|
28
|
-
siibra_tqdm,
|
|
29
28
|
InstanceTable,
|
|
30
|
-
SIIBRA_DEFAULT_MAPTYPE,
|
|
31
|
-
SIIBRA_DEFAULT_MAP_THRESHOLD
|
|
32
29
|
)
|
|
30
|
+
from ..exceptions import NoMapAvailableError, SpaceWarpingFailedError
|
|
33
31
|
|
|
34
|
-
import numpy as np
|
|
35
32
|
import re
|
|
36
33
|
import anytree
|
|
37
|
-
from typing import List,
|
|
38
|
-
from nibabel import Nifti1Image
|
|
34
|
+
from typing import List, Union, Iterable, Dict, Callable, Tuple
|
|
39
35
|
from difflib import SequenceMatcher
|
|
40
|
-
from dataclasses import dataclass, field
|
|
41
36
|
from ebrains_drive import BucketApiClient
|
|
42
37
|
import json
|
|
43
|
-
from functools import wraps
|
|
38
|
+
from functools import wraps, reduce
|
|
44
39
|
from concurrent.futures import ThreadPoolExecutor
|
|
40
|
+
from functools import lru_cache
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
REGEX_TYPE = type(re.compile("test"))
|
|
@@ -49,20 +45,7 @@ REGEX_TYPE = type(re.compile("test"))
|
|
|
49
45
|
THRESHOLD_STATISTICAL_MAPS = None
|
|
50
46
|
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
class SpatialPropCmpt:
|
|
54
|
-
centroid: point.Point
|
|
55
|
-
volume: int
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass
|
|
59
|
-
class SpatialProp:
|
|
60
|
-
cog: SpatialPropCmpt = None
|
|
61
|
-
components: List[SpatialPropCmpt] = field(default_factory=list)
|
|
62
|
-
space: _space.Space = None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
48
|
+
class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
66
49
|
"""
|
|
67
50
|
Representation of a region with name and more optional attributes
|
|
68
51
|
"""
|
|
@@ -70,6 +53,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
70
53
|
_regex_re = re.compile(r'^\/(?P<expression>.+)\/(?P<flags>[a-zA-Z]*)$')
|
|
71
54
|
_accepted_flags = "aiLmsux"
|
|
72
55
|
|
|
56
|
+
_GETMAP_CACHE = {}
|
|
57
|
+
_GETMAP_CACHE_MAX_ENTRIES = 1
|
|
58
|
+
|
|
73
59
|
def __init__(
|
|
74
60
|
self,
|
|
75
61
|
name: str,
|
|
@@ -82,6 +68,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
82
68
|
datasets: list = [],
|
|
83
69
|
rgb: str = None,
|
|
84
70
|
spec=None,
|
|
71
|
+
prerelease: bool = False,
|
|
85
72
|
):
|
|
86
73
|
"""
|
|
87
74
|
Constructs a new Region object.
|
|
@@ -119,7 +106,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
119
106
|
modality=modality,
|
|
120
107
|
publications=publications,
|
|
121
108
|
datasets=datasets,
|
|
122
|
-
spec=spec
|
|
109
|
+
spec=spec,
|
|
110
|
+
prerelease=prerelease,
|
|
123
111
|
)
|
|
124
112
|
|
|
125
113
|
# anytree node will take care to use this appropriately
|
|
@@ -131,8 +119,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
131
119
|
else tuple(int(rgb[p:p + 2], 16) for p in [1, 3, 5])
|
|
132
120
|
)
|
|
133
121
|
self._supported_spaces = None # computed on 1st call of self.supported_spaces
|
|
134
|
-
self._CACHED_REGION_SEARCHES = {}
|
|
135
122
|
self._str_aliases = None
|
|
123
|
+
self.find = lru_cache(maxsize=3)(self.find)
|
|
136
124
|
|
|
137
125
|
def get_related_regions(self) -> Iterable["RegionRelationAssessments"]:
|
|
138
126
|
"""
|
|
@@ -140,11 +128,11 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
140
128
|
|
|
141
129
|
Yields
|
|
142
130
|
------
|
|
143
|
-
|
|
131
|
+
Qualification
|
|
144
132
|
|
|
145
133
|
Example
|
|
146
134
|
-------
|
|
147
|
-
>>> region = siibra.get_region("monkey", "PG")
|
|
135
|
+
>>> region = siibra.get_region("monkey", "PG")
|
|
148
136
|
>>> for assesment in region.get_related_regions():
|
|
149
137
|
>>> print(assesment)
|
|
150
138
|
'PG' is homologous to 'Area PGa (IPL)'
|
|
@@ -255,6 +243,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
255
243
|
Parameters
|
|
256
244
|
----------
|
|
257
245
|
region: Region
|
|
246
|
+
|
|
258
247
|
Returns
|
|
259
248
|
-------
|
|
260
249
|
bool
|
|
@@ -274,9 +263,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
274
263
|
|
|
275
264
|
Parameters
|
|
276
265
|
----------
|
|
277
|
-
regionspec: str, regex,
|
|
266
|
+
regionspec: str, regex, Region
|
|
278
267
|
- a string with a possibly inexact name (matched both against the name and the identifier key)
|
|
279
|
-
- a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux)
|
|
268
|
+
- a string in '/pattern/flags' format to use regex search (acceptable flags: aiLmsux, see at https://docs.python.org/3/library/re.html#flags)
|
|
280
269
|
- a regex applied to region names
|
|
281
270
|
- a Region object
|
|
282
271
|
filter_children : bool, default: False
|
|
@@ -285,20 +274,18 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
285
274
|
If True (requires `filter_children=True`), will return parent
|
|
286
275
|
structures if all children are matched, even though the parent
|
|
287
276
|
itself might not match the specification.
|
|
277
|
+
|
|
288
278
|
Returns
|
|
289
279
|
-------
|
|
290
280
|
list[Region]
|
|
291
281
|
list of regions matching to the regionspec
|
|
282
|
+
|
|
292
283
|
Tip
|
|
293
284
|
---
|
|
294
285
|
See example 01-003, find regions.
|
|
295
286
|
"""
|
|
296
|
-
key = (regionspec, filter_children, find_topmost)
|
|
297
|
-
MEM = self._CACHED_REGION_SEARCHES
|
|
298
|
-
if key in MEM:
|
|
299
|
-
return MEM[key]
|
|
300
|
-
|
|
301
287
|
if isinstance(regionspec, str):
|
|
288
|
+
# convert the specified string into a regex for matching
|
|
302
289
|
regex_match = self._regex_re.match(regionspec)
|
|
303
290
|
if regex_match:
|
|
304
291
|
flags = regex_match.group('flags')
|
|
@@ -306,7 +293,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
306
293
|
|
|
307
294
|
for flag in flags or []: # catch if flags is nullish
|
|
308
295
|
if flag not in self._accepted_flags:
|
|
309
|
-
raise Exception(f"only accepted flag are in {
|
|
296
|
+
raise Exception(f"only accepted flag are in {self._accepted_flags}. {flag} is not within them")
|
|
310
297
|
search_regex = (f"(?{flags})" if flags else "") + expression
|
|
311
298
|
regionspec = re.compile(search_regex)
|
|
312
299
|
|
|
@@ -358,18 +345,15 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
358
345
|
found_regions = sorted(set(candidates), key=lambda r: r.depth)
|
|
359
346
|
|
|
360
347
|
# reverse is set to True, since SequenceMatcher().ratio(), higher == better
|
|
361
|
-
|
|
348
|
+
return (
|
|
362
349
|
sorted(
|
|
363
350
|
found_regions,
|
|
364
351
|
reverse=True,
|
|
365
352
|
key=lambda region: SequenceMatcher(None, str(region), regionspec).ratio(),
|
|
366
353
|
)
|
|
367
|
-
if
|
|
368
|
-
else found_regions
|
|
354
|
+
if isinstance(regionspec, str) else found_regions
|
|
369
355
|
)
|
|
370
356
|
|
|
371
|
-
return MEM[key]
|
|
372
|
-
|
|
373
357
|
def matches(self, regionspec):
|
|
374
358
|
"""
|
|
375
359
|
Checks whether this region matches the given region specification.
|
|
@@ -386,53 +370,57 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
386
370
|
bool
|
|
387
371
|
If the regionspec matches to the Region.
|
|
388
372
|
"""
|
|
389
|
-
if regionspec
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
373
|
+
if regionspec not in self._CACHED_MATCHES:
|
|
374
|
+
def splitstr(s):
|
|
375
|
+
return [w for w in re.split(r"[^a-zA-Z0-9.\-]", s) if len(w) > 0]
|
|
376
|
+
|
|
377
|
+
if regionspec is None:
|
|
378
|
+
self._CACHED_MATCHES[regionspec] = False
|
|
379
|
+
|
|
380
|
+
elif isinstance(regionspec, Region):
|
|
381
|
+
self._CACHED_MATCHES[regionspec] = self == regionspec
|
|
382
|
+
|
|
383
|
+
elif isinstance(regionspec, str):
|
|
384
|
+
# string is given, perform lazy string matching
|
|
385
|
+
q = regionspec.lower().strip()
|
|
386
|
+
if q == self.key.lower().strip():
|
|
387
|
+
self._CACHED_MATCHES[regionspec] = True
|
|
388
|
+
elif q == self.id.lower().strip():
|
|
389
|
+
self._CACHED_MATCHES[regionspec] = True
|
|
390
|
+
elif q == self.name.lower().strip():
|
|
391
|
+
self._CACHED_MATCHES[regionspec] = True
|
|
392
|
+
else:
|
|
393
|
+
# match if all words of the query are also included in the region name
|
|
394
|
+
W = splitstr(clear_name(self.name.lower()))
|
|
395
|
+
Q = splitstr(clear_name(regionspec))
|
|
396
|
+
self._CACHED_MATCHES[regionspec] = all([any(
|
|
397
|
+
q.lower() == w or 'v' + q.lower() == w
|
|
398
|
+
for w in W
|
|
399
|
+
) for q in Q])
|
|
400
|
+
|
|
401
|
+
# TODO since dropping 3.6 support, maybe reimplement as re.Pattern ?
|
|
402
|
+
elif isinstance(regionspec, REGEX_TYPE):
|
|
403
|
+
# match regular expression
|
|
404
|
+
self._CACHED_MATCHES[regionspec] = any(regionspec.search(s) is not None for s in [self.name, self.key])
|
|
405
|
+
|
|
406
|
+
elif isinstance(regionspec, (list, tuple)):
|
|
407
|
+
self._CACHED_MATCHES[regionspec] = any(self.matches(_) for _ in regionspec)
|
|
397
408
|
|
|
398
|
-
elif isinstance(regionspec, str):
|
|
399
|
-
# string is given, perform lazy string matching
|
|
400
|
-
q = regionspec.lower().strip()
|
|
401
|
-
if q == self.key.lower().strip():
|
|
402
|
-
return True
|
|
403
|
-
elif q == self.id.lower().strip():
|
|
404
|
-
return True
|
|
405
|
-
elif q == self.name.lower().strip():
|
|
406
|
-
return True
|
|
407
409
|
else:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
for w in W
|
|
414
|
-
) for q in Q])
|
|
415
|
-
|
|
416
|
-
# TODO since dropping 3.6 support, maybe reimplement as re.Pattern ?
|
|
417
|
-
elif isinstance(regionspec, REGEX_TYPE):
|
|
418
|
-
# match regular expression
|
|
419
|
-
return any(regionspec.search(s) is not None for s in [self.name, self.key])
|
|
420
|
-
elif isinstance(regionspec, (list, tuple)):
|
|
421
|
-
return any(self.matches(_) for _ in regionspec)
|
|
422
|
-
else:
|
|
423
|
-
raise TypeError(
|
|
424
|
-
f"Cannot interpret region specification of type '{type(regionspec)}'"
|
|
425
|
-
)
|
|
410
|
+
raise TypeError(
|
|
411
|
+
f"Cannot interpret region specification of type '{type(regionspec)}'"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return self._CACHED_MATCHES[regionspec]
|
|
426
415
|
|
|
427
|
-
def
|
|
416
|
+
def get_regional_mask(
|
|
428
417
|
self,
|
|
429
418
|
space: Union[str, _space.Space],
|
|
430
|
-
maptype: MapType =
|
|
431
|
-
threshold: float =
|
|
432
|
-
|
|
433
|
-
):
|
|
419
|
+
maptype: MapType = MapType.LABELLED,
|
|
420
|
+
threshold: float = 0.0,
|
|
421
|
+
) -> volume.FilteredVolume:
|
|
434
422
|
"""
|
|
435
|
-
|
|
423
|
+
Get a binary mask of this region in the given space,
|
|
436
424
|
using the specified MapTypes.
|
|
437
425
|
|
|
438
426
|
Parameters
|
|
@@ -441,100 +429,98 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
441
429
|
The requested reference space
|
|
442
430
|
maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
|
|
443
431
|
The type of map to be used ('labelled' or 'statistical')
|
|
444
|
-
threshold: float,
|
|
432
|
+
threshold: float, default: 0.0
|
|
445
433
|
When fetching a statistical map, use this threshold to convert
|
|
446
|
-
it to a binary mask
|
|
447
|
-
|
|
448
|
-
If specified, fetch the map in this space first, and then perform
|
|
449
|
-
a linear warping from there to the requested space.
|
|
450
|
-
|
|
451
|
-
Tip
|
|
452
|
-
---
|
|
453
|
-
You might want to use this if a map in the requested space
|
|
454
|
-
is not available.
|
|
455
|
-
|
|
456
|
-
Note
|
|
457
|
-
----
|
|
458
|
-
This linear warping is an affine approximation of the
|
|
459
|
-
nonlinear deformation, computed from the warped corner points
|
|
460
|
-
of the bounding box (see siibra.locations.BoundingBox.estimate_affine()).
|
|
461
|
-
It does not require voxel resampling, just replaces the affine
|
|
462
|
-
matrix, but is less accurate than a full nonlinear warping,
|
|
463
|
-
which is currently not supported in siibra-python for images.
|
|
434
|
+
it to a binary mask.
|
|
435
|
+
|
|
464
436
|
Returns
|
|
465
437
|
-------
|
|
466
|
-
|
|
438
|
+
Volume (use fetch() to get a NiftiImage)
|
|
467
439
|
"""
|
|
468
440
|
if isinstance(maptype, str):
|
|
469
441
|
maptype = MapType[maptype.upper()]
|
|
470
|
-
result = None
|
|
471
442
|
|
|
472
|
-
|
|
443
|
+
threshold_info = "" if maptype == MapType.LABELLED else f"(threshold: {threshold}) "
|
|
444
|
+
name = f"Mask {threshold_info}of '{self.name} ({self.parcellation})' in "
|
|
445
|
+
try:
|
|
446
|
+
regional_map = self.get_regional_map(space=space, maptype=maptype)
|
|
447
|
+
if maptype == MapType.LABELLED:
|
|
448
|
+
assert threshold == 0.0, f"threshold can only be set for {MapType.STATISTICAL} maps."
|
|
449
|
+
result = regional_map
|
|
450
|
+
result._boundingbox = None
|
|
451
|
+
if maptype == MapType.STATISTICAL:
|
|
452
|
+
result = volume.FilteredVolume(
|
|
453
|
+
parent_volume=regional_map,
|
|
454
|
+
threshold=threshold
|
|
455
|
+
)
|
|
456
|
+
if threshold == 0.0:
|
|
457
|
+
result._boundingbox = regional_map._boundingbox
|
|
458
|
+
name += f"'{result.space}'"
|
|
459
|
+
except NoMapAvailableError:
|
|
460
|
+
# This region is not mapped directly in any map in the registry.
|
|
461
|
+
# Try building a map from the child regions
|
|
462
|
+
if (len(self.children) > 0) and all(c.mapped_in_space(space) for c in self.children):
|
|
463
|
+
logger.info(f"{self.name} is not mapped in {space}. Merging the masks of its {len(self.children)} child regions.")
|
|
464
|
+
child_volumes = [
|
|
465
|
+
child.get_regional_mask(space=space, maptype=maptype, threshold=threshold)
|
|
466
|
+
for child in self.children
|
|
467
|
+
]
|
|
468
|
+
result = volume.FilteredVolume(
|
|
469
|
+
volume.merge(child_volumes),
|
|
470
|
+
label=1
|
|
471
|
+
)
|
|
472
|
+
name += f"'{result.space}' (built by merging the mask {threshold_info} of its decendants)"
|
|
473
|
+
result._name = name
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
def get_regional_map(
|
|
477
|
+
self,
|
|
478
|
+
space: Union[str, _space.Space],
|
|
479
|
+
maptype: MapType = MapType.LABELLED,
|
|
480
|
+
) -> Union[volume.FilteredVolume, volume.Volume, volume.Subvolume]:
|
|
481
|
+
"""
|
|
482
|
+
Get a volume reprsenting this region in the given space and MapType.
|
|
483
|
+
|
|
484
|
+
Note
|
|
485
|
+
----
|
|
486
|
+
If a region is not mapped in any of the `Map`s in the registry, then
|
|
487
|
+
siibra will get the maps of its children recursively and merge them.
|
|
488
|
+
If no map is available this way as well, an exception is raised.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
space: Space or str
|
|
493
|
+
The requested reference space
|
|
494
|
+
maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
|
|
495
|
+
The type of map to be used ('labelled' or 'statistical')
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
Volume (use fetch() to get a NiftiImage)
|
|
500
|
+
"""
|
|
501
|
+
if isinstance(maptype, str):
|
|
502
|
+
maptype = MapType[maptype.upper()]
|
|
503
|
+
|
|
504
|
+
# prepare space instance
|
|
473
505
|
if isinstance(space, str):
|
|
474
506
|
space = _space.Space.get_instance(space)
|
|
475
|
-
fetch_space = space if via_space is None else via_space
|
|
476
|
-
if isinstance(fetch_space, str):
|
|
477
|
-
fetch_space = _space.Space.get_instance(fetch_space)
|
|
478
507
|
|
|
508
|
+
# see if we find a map supporting the requested region
|
|
479
509
|
for m in parcellationmap.Map.registry():
|
|
480
510
|
if (
|
|
481
|
-
m.space.matches(
|
|
511
|
+
m.space.matches(space)
|
|
482
512
|
and m.parcellation == self.parcellation
|
|
483
513
|
and m.provides_image
|
|
484
514
|
and m.maptype == maptype
|
|
485
515
|
and self.name in m.regions
|
|
486
516
|
):
|
|
487
|
-
|
|
488
|
-
if (maptype == MapType.STATISTICAL) and (threshold is not None):
|
|
489
|
-
logger.info(f"Thresholding statistical map at {threshold}")
|
|
490
|
-
result = Nifti1Image(
|
|
491
|
-
(result.get_fdata() > threshold).astype('uint8'),
|
|
492
|
-
result.affine
|
|
493
|
-
)
|
|
494
|
-
break
|
|
517
|
+
return m.get_volume(region=self)
|
|
495
518
|
else:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if all(c.mapped_in_space(fetch_space) for c in self.children):
|
|
500
|
-
for c in siibra_tqdm(
|
|
501
|
-
self.children,
|
|
502
|
-
desc=f"Building mask of {self.name}",
|
|
503
|
-
leave=False,
|
|
504
|
-
unit=" child region"
|
|
505
|
-
):
|
|
506
|
-
mask = c.fetch_regional_map(fetch_space, maptype, threshold)
|
|
507
|
-
if dataobj is None:
|
|
508
|
-
dataobj = np.asanyarray(mask.dataobj)
|
|
509
|
-
affine = mask.affine
|
|
510
|
-
else:
|
|
511
|
-
if np.linalg.norm(mask.affine - affine) > 1e-12:
|
|
512
|
-
raise NotImplementedError(
|
|
513
|
-
f"Child regions of {self.name} have different voxel spaces "
|
|
514
|
-
"and the aggregated subtree mask is not supported. "
|
|
515
|
-
f"Try fetching masks of the children: {self.children}"
|
|
516
|
-
)
|
|
517
|
-
updates = mask.get_fdata() > dataobj
|
|
518
|
-
dataobj[updates] = mask.get_fdata()[updates]
|
|
519
|
-
if dataobj is not None:
|
|
520
|
-
result = Nifti1Image(dataobj, affine)
|
|
521
|
-
|
|
522
|
-
if result is None:
|
|
523
|
-
raise RuntimeError(f"Cannot build mask for {self.name} from {maptype} maps in {fetch_space}")
|
|
524
|
-
|
|
525
|
-
if via_space is not None:
|
|
526
|
-
# fetch used an intermediate reference space provided by 'via_space'.
|
|
527
|
-
# We will now transform the affine to match the desired target space.
|
|
528
|
-
bbox = boundingbox.BoundingBox.from_image(result, fetch_space)
|
|
529
|
-
transform = bbox.estimate_affine(space)
|
|
530
|
-
result = Nifti1Image(result.dataobj, np.dot(transform, result.affine))
|
|
531
|
-
logger.info(
|
|
532
|
-
f"Regional map was fetched from {fetch_space.name}, "
|
|
533
|
-
f"then linearly corrected to match {space.name}."
|
|
519
|
+
raise NoMapAvailableError(
|
|
520
|
+
f"{self.name} is not mapped in {space} as a {str(maptype)} map."
|
|
521
|
+
" Please try getting the children or getting the mask."
|
|
534
522
|
)
|
|
535
523
|
|
|
536
|
-
return result
|
|
537
|
-
|
|
538
524
|
def mapped_in_space(self, space, recurse: bool = True) -> bool:
|
|
539
525
|
"""
|
|
540
526
|
Verifies wether this region is defined by an explicit map in the given space.
|
|
@@ -565,13 +551,15 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
565
551
|
return False
|
|
566
552
|
|
|
567
553
|
@property
|
|
568
|
-
def supported_spaces(self) ->
|
|
554
|
+
def supported_spaces(self) -> List[_space.Space]:
|
|
569
555
|
"""
|
|
570
556
|
The set of spaces for which a mask could be extracted.
|
|
571
557
|
Overwrites the corresponding method of AtlasConcept.
|
|
572
558
|
"""
|
|
573
559
|
if self._supported_spaces is None:
|
|
574
|
-
self._supported_spaces =
|
|
560
|
+
self._supported_spaces = sorted(
|
|
561
|
+
{s for s in _space.Space.registry() if self.mapped_in_space(s)}
|
|
562
|
+
)
|
|
575
563
|
return self._supported_spaces
|
|
576
564
|
|
|
577
565
|
def supports_space(self, space: _space.Space):
|
|
@@ -587,11 +575,92 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
587
575
|
elements={s.key: s for s in self.supported_spaces},
|
|
588
576
|
)
|
|
589
577
|
|
|
590
|
-
def
|
|
591
|
-
|
|
578
|
+
def __contains__(self, other: Union[location.Location, 'Region']) -> bool:
|
|
579
|
+
if isinstance(other, Region):
|
|
580
|
+
return len(self.find(other)) > 0
|
|
581
|
+
else:
|
|
582
|
+
try:
|
|
583
|
+
regionmap = self.get_regional_mask(space=other.space)
|
|
584
|
+
return regionmap.__contains__(other)
|
|
585
|
+
except NoMapAvailableError:
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
def assign(self, other: structure.BrainStructure) -> AnatomicalAssignment:
|
|
589
|
+
"""
|
|
590
|
+
Compute assignment of a location to this region.
|
|
591
|
+
|
|
592
|
+
Two cases:
|
|
593
|
+
1) other is location -> get region map, call regionmap.assign(other)
|
|
594
|
+
2) other is region -> just do a semantic check for the regions
|
|
595
|
+
|
|
596
|
+
Parameters
|
|
597
|
+
----------
|
|
598
|
+
other : Location or Region
|
|
592
599
|
|
|
593
|
-
|
|
594
|
-
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
AnatomicalAssignment or None
|
|
603
|
+
None if there is no Qualification found.
|
|
604
|
+
"""
|
|
605
|
+
if (self, other) in self._ASSIGNMENT_CACHE:
|
|
606
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
607
|
+
if (other, self) in self._ASSIGNMENT_CACHE:
|
|
608
|
+
if self._ASSIGNMENT_CACHE[other, self] is None:
|
|
609
|
+
return None
|
|
610
|
+
return self._ASSIGNMENT_CACHE[other, self].invert()
|
|
611
|
+
|
|
612
|
+
if isinstance(other, (location.Location, volume.Volume)):
|
|
613
|
+
if self.mapped_in_space(other.space):
|
|
614
|
+
regionmap = self.get_regional_mask(other.space)
|
|
615
|
+
self._ASSIGNMENT_CACHE[self, other] = regionmap.assign(other)
|
|
616
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
617
|
+
|
|
618
|
+
if isinstance(other, _boundingbox.BoundingBox): # volume.intersection(bbox) gets boundingbox anyway
|
|
619
|
+
try:
|
|
620
|
+
regionbbox_otherspace = self.get_boundingbox(other.space, restrict_space=False)
|
|
621
|
+
if regionbbox_otherspace is not None:
|
|
622
|
+
self._ASSIGNMENT_CACHE[self, other] = regionbbox_otherspace.assign(other)
|
|
623
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.debug(e)
|
|
626
|
+
|
|
627
|
+
assignment_result = None
|
|
628
|
+
for targetspace in self.supported_spaces:
|
|
629
|
+
try:
|
|
630
|
+
other_warped = other.warp(targetspace)
|
|
631
|
+
regionmap = self.get_regional_mask(targetspace)
|
|
632
|
+
assignment_result = regionmap.assign(other_warped)
|
|
633
|
+
except SpaceWarpingFailedError:
|
|
634
|
+
try:
|
|
635
|
+
regionbbox_targetspace = self.get_boundingbox(
|
|
636
|
+
targetspace, restrict_space=True
|
|
637
|
+
)
|
|
638
|
+
if regionbbox_targetspace is None:
|
|
639
|
+
continue
|
|
640
|
+
regionbbox_warped = regionbbox_targetspace.warp(other.space)
|
|
641
|
+
except SpaceWarpingFailedError:
|
|
642
|
+
continue
|
|
643
|
+
assignment_result = regionbbox_warped.assign(other)
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.debug(e)
|
|
646
|
+
continue
|
|
647
|
+
break
|
|
648
|
+
self._ASSIGNMENT_CACHE[self, other] = assignment_result
|
|
649
|
+
else: # other is a Region
|
|
650
|
+
assert isinstance(other, Region)
|
|
651
|
+
if self == other:
|
|
652
|
+
qualification = Qualification.EXACT
|
|
653
|
+
elif self.__contains__(other):
|
|
654
|
+
qualification = Qualification.CONTAINS
|
|
655
|
+
elif other.__contains__(self):
|
|
656
|
+
qualification = Qualification.CONTAINED
|
|
657
|
+
else:
|
|
658
|
+
qualification = None
|
|
659
|
+
if qualification is None:
|
|
660
|
+
self._ASSIGNMENT_CACHE[self, other] = None
|
|
661
|
+
else:
|
|
662
|
+
self._ASSIGNMENT_CACHE[self, other] = AnatomicalAssignment(self, other, qualification)
|
|
663
|
+
return self._ASSIGNMENT_CACHE[self, other]
|
|
595
664
|
|
|
596
665
|
def tree2str(self):
|
|
597
666
|
"""Render region-tree as a string"""
|
|
@@ -605,13 +674,16 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
605
674
|
"""Prints the tree representation of the region"""
|
|
606
675
|
print(self.tree2str())
|
|
607
676
|
|
|
608
|
-
def
|
|
677
|
+
def get_boundingbox(
|
|
609
678
|
self,
|
|
610
679
|
space: _space.Space,
|
|
611
680
|
maptype: MapType = MapType.LABELLED,
|
|
612
|
-
threshold_statistical=
|
|
613
|
-
|
|
614
|
-
|
|
681
|
+
threshold_statistical: float = 0.0,
|
|
682
|
+
restrict_space: bool = True,
|
|
683
|
+
**fetch_kwargs
|
|
684
|
+
) -> Union[_boundingbox.BoundingBox, None]:
|
|
685
|
+
"""
|
|
686
|
+
Compute the bounding box of this region in the given space.
|
|
615
687
|
|
|
616
688
|
Parameters
|
|
617
689
|
----------
|
|
@@ -621,40 +693,61 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
621
693
|
Type of map to build ('labelled' will result in a binary mask,
|
|
622
694
|
'statistical' attempts to build a statistical mask, possibly by
|
|
623
695
|
elementwise maximum of statistical maps of children)
|
|
624
|
-
threshold_statistical: float,
|
|
625
|
-
|
|
626
|
-
|
|
696
|
+
threshold_statistical: float, default: 0.0
|
|
697
|
+
When masking a statistical map, use this threshold to convert
|
|
698
|
+
it to a binary mask before finding its bounding box.
|
|
699
|
+
restrict_space: bool, default: False
|
|
700
|
+
If True, it will not try to fetch maps from other spaces and warp
|
|
701
|
+
its boundingbox to requested space.
|
|
702
|
+
|
|
627
703
|
Returns
|
|
628
704
|
-------
|
|
629
705
|
BoundingBox
|
|
630
706
|
"""
|
|
631
707
|
spaceobj = _space.Space.get_instance(space)
|
|
632
708
|
try:
|
|
633
|
-
mask = self.
|
|
709
|
+
mask = self.get_regional_mask(
|
|
634
710
|
spaceobj, maptype=maptype, threshold=threshold_statistical
|
|
635
711
|
)
|
|
636
|
-
return
|
|
712
|
+
return mask.get_boundingbox(
|
|
713
|
+
clip=True,
|
|
714
|
+
background=0.0,
|
|
715
|
+
**fetch_kwargs
|
|
716
|
+
)
|
|
637
717
|
except (RuntimeError, ValueError):
|
|
718
|
+
if restrict_space:
|
|
719
|
+
return None
|
|
638
720
|
for other_space in self.parcellation.spaces - spaceobj:
|
|
639
721
|
try:
|
|
640
|
-
mask = self.
|
|
722
|
+
mask = self.get_regional_mask(
|
|
641
723
|
other_space,
|
|
642
724
|
maptype=maptype,
|
|
643
725
|
threshold=threshold_statistical,
|
|
644
726
|
)
|
|
645
|
-
|
|
646
|
-
f"No bounding box for {self.name} defined in {spaceobj.name}, "
|
|
647
|
-
f"will warp the bounding box from {other_space.name} instead."
|
|
648
|
-
)
|
|
649
|
-
bbox = boundingbox.BoundingBox.from_image(mask, space=other_space)
|
|
727
|
+
bbox = mask.get_boundingbox(clip=True, background=0.0, **fetch_kwargs)
|
|
650
728
|
if bbox is not None:
|
|
651
|
-
|
|
729
|
+
try:
|
|
730
|
+
bbox_warped = bbox.warp(spaceobj)
|
|
731
|
+
except SpaceWarpingFailedError:
|
|
732
|
+
continue
|
|
733
|
+
logger.debug(
|
|
734
|
+
f"No bounding box for {self.name} defined in {spaceobj.name}, "
|
|
735
|
+
f"warped the bounding box from {other_space.name} instead."
|
|
736
|
+
)
|
|
737
|
+
return bbox_warped
|
|
652
738
|
except RuntimeError:
|
|
653
739
|
continue
|
|
654
740
|
logger.error(f"Could not compute bounding box for {self.name}.")
|
|
655
741
|
return None
|
|
656
742
|
|
|
657
|
-
def compute_centroids(
|
|
743
|
+
def compute_centroids(
|
|
744
|
+
self,
|
|
745
|
+
space: _space.Space,
|
|
746
|
+
maptype: MapType = MapType.LABELLED,
|
|
747
|
+
threshold_statistical: float = 0.0,
|
|
748
|
+
split_components: bool = True,
|
|
749
|
+
**fetch_kwargs,
|
|
750
|
+
) -> pointcloud.PointCloud:
|
|
658
751
|
"""
|
|
659
752
|
Compute the centroids of the region in the given space.
|
|
660
753
|
|
|
@@ -662,18 +755,33 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
662
755
|
----------
|
|
663
756
|
space: Space
|
|
664
757
|
reference space in which the computation will be performed
|
|
758
|
+
maptype: MapType, default: MapType.LABELLED
|
|
759
|
+
Type of map to build ('labelled' will result in a binary mask,
|
|
760
|
+
'statistical' attempts to build a statistical mask, possibly by
|
|
761
|
+
elementwise maximum of statistical maps of children)
|
|
762
|
+
threshold_statistical: float, default: 0.0
|
|
763
|
+
When masking a statistical map, use this threshold to convert
|
|
764
|
+
it to a binary mask before finding its centroids.
|
|
765
|
+
|
|
665
766
|
Returns
|
|
666
767
|
-------
|
|
667
|
-
|
|
668
|
-
Found centroids (as Point objects) in a
|
|
768
|
+
PointCloud
|
|
769
|
+
Found centroids (as Point objects) in a PointCloud
|
|
770
|
+
|
|
669
771
|
Note
|
|
670
772
|
----
|
|
671
773
|
A region can generally have multiple centroids if it has multiple
|
|
672
774
|
connected components in the map.
|
|
673
775
|
"""
|
|
674
|
-
props = self.spatial_props(
|
|
675
|
-
|
|
676
|
-
|
|
776
|
+
props = self.spatial_props(
|
|
777
|
+
space=space,
|
|
778
|
+
maptype=maptype,
|
|
779
|
+
threshold_statistical=threshold_statistical,
|
|
780
|
+
split_components=split_components,
|
|
781
|
+
**fetch_kwargs,
|
|
782
|
+
)
|
|
783
|
+
return pointcloud.PointCloud(
|
|
784
|
+
[c.centroid for c in props],
|
|
677
785
|
space=space
|
|
678
786
|
)
|
|
679
787
|
|
|
@@ -681,8 +789,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
681
789
|
self,
|
|
682
790
|
space: _space.Space,
|
|
683
791
|
maptype: MapType = MapType.LABELLED,
|
|
684
|
-
threshold_statistical=
|
|
685
|
-
|
|
792
|
+
threshold_statistical: float = 0.0,
|
|
793
|
+
split_components: bool = True,
|
|
794
|
+
**fetch_kwargs,
|
|
795
|
+
):
|
|
686
796
|
"""
|
|
687
797
|
Compute spatial properties for connected components of this region in the given space.
|
|
688
798
|
|
|
@@ -694,59 +804,34 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
694
804
|
Type of map to build ('labelled' will result in a binary mask,
|
|
695
805
|
'statistical' attempts to build a statistical mask, possibly by
|
|
696
806
|
elementwise maximum of statistical maps of children)
|
|
697
|
-
threshold_statistical: float,
|
|
807
|
+
threshold_statistical: float, default: 0.0
|
|
698
808
|
if not None, masks will be preferably constructed by thresholding
|
|
699
809
|
statistical maps with the given value.
|
|
700
810
|
|
|
701
811
|
Returns
|
|
702
812
|
-------
|
|
703
|
-
|
|
704
|
-
|
|
813
|
+
List
|
|
814
|
+
List of region's component spatial properties
|
|
705
815
|
"""
|
|
706
|
-
from skimage import measure
|
|
707
|
-
|
|
708
816
|
if not isinstance(space, _space.Space):
|
|
709
817
|
space = _space.Space.get_instance(space)
|
|
710
818
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
819
|
+
# build binary mask of the image
|
|
820
|
+
try:
|
|
821
|
+
region_vol = self.get_regional_mask(
|
|
822
|
+
space, maptype=maptype, threshold=threshold_statistical
|
|
823
|
+
)
|
|
824
|
+
except NoMapAvailableError:
|
|
825
|
+
raise ValueError(
|
|
715
826
|
f"Spatial properties of {self.name} cannot be computed in {space.name}. "
|
|
716
827
|
"This region is only mapped in these spaces: "
|
|
717
828
|
f"{', '.join(s.name for s in self.supported_spaces)}"
|
|
718
829
|
)
|
|
719
|
-
return result
|
|
720
830
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
space, maptype=maptype, threshold=threshold_statistical
|
|
831
|
+
return region_vol.compute_spatial_props(
|
|
832
|
+
split_components=split_components, **fetch_kwargs
|
|
724
833
|
)
|
|
725
834
|
|
|
726
|
-
# determine scaling factor from voxels to cube mm
|
|
727
|
-
scale = affine_scaling(pimg.affine)
|
|
728
|
-
|
|
729
|
-
# compute properties of labelled volume
|
|
730
|
-
A = np.asarray(pimg.get_fdata(), dtype=np.int32).squeeze()
|
|
731
|
-
C = measure.label(A)
|
|
732
|
-
|
|
733
|
-
# compute spatial properties of each connected component
|
|
734
|
-
for label in range(1, C.max() + 1):
|
|
735
|
-
nonzero = np.c_[np.nonzero(C == label)]
|
|
736
|
-
result.components.append(
|
|
737
|
-
SpatialPropCmpt(
|
|
738
|
-
centroid=point.Point(
|
|
739
|
-
np.dot(pimg.affine, np.r_[nonzero.mean(0), 1])[:3], space=space
|
|
740
|
-
),
|
|
741
|
-
volume=nonzero.shape[0] * scale,
|
|
742
|
-
)
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
# sort by volume
|
|
746
|
-
result.components.sort(key=lambda cmp: cmp.volume, reverse=True)
|
|
747
|
-
|
|
748
|
-
return result
|
|
749
|
-
|
|
750
835
|
def __iter__(self):
|
|
751
836
|
"""
|
|
752
837
|
Returns an iterator that goes through all regions in this subtree
|
|
@@ -754,6 +839,110 @@ class Region(anytree.NodeMixin, concept.AtlasConcept):
|
|
|
754
839
|
"""
|
|
755
840
|
return anytree.PreOrderIter(self)
|
|
756
841
|
|
|
842
|
+
def intersection(self, other: "location.Location") -> "location.Location":
|
|
843
|
+
"""Use this region for filtering a location object."""
|
|
844
|
+
|
|
845
|
+
if self.supports_space(other.space):
|
|
846
|
+
try:
|
|
847
|
+
volume = self.get_regional_mask(other.space)
|
|
848
|
+
if volume is not None:
|
|
849
|
+
return volume.intersection(other)
|
|
850
|
+
except NotImplementedError:
|
|
851
|
+
intersections = [child.intersection(other) for child in self.children]
|
|
852
|
+
return reduce(lambda a, b: a.union(b), intersections)
|
|
853
|
+
|
|
854
|
+
for space in self.supported_spaces:
|
|
855
|
+
if space.provides_image:
|
|
856
|
+
try:
|
|
857
|
+
volume = self.get_regional_mask(space)
|
|
858
|
+
if volume is not None:
|
|
859
|
+
intersection = volume.intersection(other)
|
|
860
|
+
logger.info(f"Warped {other} to {space} to find the intersection.")
|
|
861
|
+
return intersection
|
|
862
|
+
except SpaceWarpingFailedError:
|
|
863
|
+
continue
|
|
864
|
+
|
|
865
|
+
return None
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
@cache_user_fn
|
|
869
|
+
def _get_related_regions_str(pe_id: str) -> Tuple[Tuple[str, str, str, str], ...]:
|
|
870
|
+
logger.info("LONG CALC...", pe_id)
|
|
871
|
+
return_val = []
|
|
872
|
+
region_relation_assessments = RegionRelationAssessments.translate_pes(pe_id, pe_id)
|
|
873
|
+
for asgmt in region_relation_assessments:
|
|
874
|
+
assert isinstance(asgmt, RegionRelationAssessments), f"Expecting type to be of RegionRelationAssessments, but is {type(asgmt)}"
|
|
875
|
+
assert isinstance(asgmt.assigned_structure, Region), f"Expecting assigned structure to be of type Region, but is {type(asgmt.assigned_structure)}"
|
|
876
|
+
return_val.append((
|
|
877
|
+
asgmt.assigned_structure.parcellation.id,
|
|
878
|
+
asgmt.assigned_structure.name,
|
|
879
|
+
asgmt.qualification.name,
|
|
880
|
+
asgmt.explanation
|
|
881
|
+
))
|
|
882
|
+
return tuple(return_val)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def get_peid_from_region(region: Region) -> str:
|
|
886
|
+
"""
|
|
887
|
+
Given a region, obtain the Parcellation Entity ID.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
region : Region
|
|
892
|
+
|
|
893
|
+
Returns
|
|
894
|
+
-------
|
|
895
|
+
str
|
|
896
|
+
"""
|
|
897
|
+
if region._spec:
|
|
898
|
+
region_peid = region._spec.get("ebrains", {}).get("openminds/ParcellationEntity")
|
|
899
|
+
if region_peid:
|
|
900
|
+
return region_peid
|
|
901
|
+
# In some cases (e.g. Julich Brain, PE is defined on the parent leaf nodes)
|
|
902
|
+
if region.parent and region.parent._spec:
|
|
903
|
+
parent_peid = region.parent._spec.get("ebrains", {}).get("openminds/ParcellationEntity")
|
|
904
|
+
if parent_peid:
|
|
905
|
+
return parent_peid
|
|
906
|
+
return None
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def get_related_regions(region: Region) -> Iterable["RegionRelationAssessments"]:
|
|
910
|
+
"""
|
|
911
|
+
Get assements on relations of a region to others defined on EBRAINS.
|
|
912
|
+
|
|
913
|
+
Parameters
|
|
914
|
+
----------
|
|
915
|
+
region: Region
|
|
916
|
+
|
|
917
|
+
Yields
|
|
918
|
+
------
|
|
919
|
+
Qualification
|
|
920
|
+
|
|
921
|
+
Example
|
|
922
|
+
-------
|
|
923
|
+
>>> region = siibra.get_region("monkey", "PG")
|
|
924
|
+
>>> for assesment in siibra.core.region.get_related_regions(region):
|
|
925
|
+
>>> print(assesment)
|
|
926
|
+
'PG' is homologous to 'Area PGa (IPL)'
|
|
927
|
+
'PG' is homologous to 'Area PGa (IPL) left'
|
|
928
|
+
'PG' is homologous to 'Area PGa (IPL) right'
|
|
929
|
+
'PG' is homologous to 'Area PGa (IPL)'
|
|
930
|
+
'PG' is homologous to 'Area PGa (IPL) left'
|
|
931
|
+
'PG' is homologous to 'Area PGa (IPL) right'
|
|
932
|
+
'PG' is homologous to 'Area PGa (IPL)'
|
|
933
|
+
'PG' is homologous to 'Area PGa (IPL) right'
|
|
934
|
+
'PG' is homologous to 'Area PGa (IPL) left'
|
|
935
|
+
"""
|
|
936
|
+
logger.info("get related region called")
|
|
937
|
+
pe_id = get_peid_from_region(region)
|
|
938
|
+
if not pe_id:
|
|
939
|
+
return []
|
|
940
|
+
|
|
941
|
+
for parc_id, region_name, qual, explanation in _get_related_regions_str(pe_id):
|
|
942
|
+
parc = _parcellation.Parcellation.get_instance(parc_id)
|
|
943
|
+
found_region = parc.get_region(region_name)
|
|
944
|
+
yield RegionRelationAssessments(region, found_region, qual, explanation)
|
|
945
|
+
|
|
757
946
|
|
|
758
947
|
_get_reg_relation_asmgt_types: Dict[str, Callable] = {}
|
|
759
948
|
|
|
@@ -769,12 +958,33 @@ def _register_region_reference_type(ebrain_type: str):
|
|
|
769
958
|
return outer
|
|
770
959
|
|
|
771
960
|
|
|
772
|
-
class RegionRelationAssessments(
|
|
961
|
+
class RegionRelationAssessments(AnatomicalAssignment[Region]):
|
|
962
|
+
"""
|
|
963
|
+
A collection of methods on finding related regions and the quantification
|
|
964
|
+
of the relationship.
|
|
965
|
+
"""
|
|
773
966
|
|
|
774
967
|
anony_client = BucketApiClient()
|
|
775
968
|
|
|
776
969
|
@staticmethod
|
|
777
|
-
def get_uuid(long_id: Union[str, Dict]):
|
|
970
|
+
def get_uuid(long_id: Union[str, Dict]) -> str:
|
|
971
|
+
"""
|
|
972
|
+
Returns the uuid portion of either a fully formed openminds id, or get
|
|
973
|
+
the 'id' property first, and extract the uuid portion of the id.
|
|
974
|
+
|
|
975
|
+
Parameters
|
|
976
|
+
----------
|
|
977
|
+
long_id: str, dict[str, str]
|
|
978
|
+
|
|
979
|
+
Returns
|
|
980
|
+
-------
|
|
981
|
+
str
|
|
982
|
+
|
|
983
|
+
Raises
|
|
984
|
+
------
|
|
985
|
+
AssertionError
|
|
986
|
+
RuntimeError
|
|
987
|
+
"""
|
|
778
988
|
if isinstance(long_id, str):
|
|
779
989
|
pass
|
|
780
990
|
elif isinstance(long_id, dict):
|
|
@@ -788,6 +998,22 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
788
998
|
|
|
789
999
|
@staticmethod
|
|
790
1000
|
def parse_id_arg(_id: Union[str, List[str]]) -> List[str]:
|
|
1001
|
+
"""
|
|
1002
|
+
Normalizes the ebrains id property. The ebrains id field can be either
|
|
1003
|
+
a str or list[str]. This method normalizes it to always be list[str].
|
|
1004
|
+
|
|
1005
|
+
Parameters
|
|
1006
|
+
----------
|
|
1007
|
+
_id: strl, list[str]
|
|
1008
|
+
|
|
1009
|
+
Returns
|
|
1010
|
+
-------
|
|
1011
|
+
list[str]
|
|
1012
|
+
|
|
1013
|
+
Raises
|
|
1014
|
+
------
|
|
1015
|
+
RuntimeError
|
|
1016
|
+
"""
|
|
791
1017
|
if isinstance(_id, list):
|
|
792
1018
|
assert all(isinstance(_i, str) for _i in _id), "all instances of pev should be str"
|
|
793
1019
|
elif isinstance(_id, str):
|
|
@@ -798,11 +1024,34 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
798
1024
|
|
|
799
1025
|
@classmethod
|
|
800
1026
|
def get_object(cls, obj: str):
|
|
1027
|
+
"""
|
|
1028
|
+
Gets given a object (path), loads the content and serializes to json.
|
|
1029
|
+
Relative to the bucket 'reference-atlas-data'.
|
|
1030
|
+
|
|
1031
|
+
Parameters
|
|
1032
|
+
----------
|
|
1033
|
+
obj: str
|
|
1034
|
+
|
|
1035
|
+
Returns
|
|
1036
|
+
-------
|
|
1037
|
+
dict
|
|
1038
|
+
"""
|
|
801
1039
|
bucket = cls.anony_client.buckets.get_bucket("reference-atlas-data")
|
|
802
1040
|
return json.loads(bucket.get_file(obj).get_content())
|
|
803
1041
|
|
|
804
1042
|
@classmethod
|
|
805
1043
|
def get_snapshot_factory(cls, type_str: str):
|
|
1044
|
+
"""
|
|
1045
|
+
Factory method for given type.
|
|
1046
|
+
|
|
1047
|
+
Parameters
|
|
1048
|
+
----------
|
|
1049
|
+
type_str: str
|
|
1050
|
+
|
|
1051
|
+
Returns
|
|
1052
|
+
-------
|
|
1053
|
+
Callable[[str|list[str]], dict]
|
|
1054
|
+
"""
|
|
806
1055
|
def get_objects(_id: Union[str, List[str]]):
|
|
807
1056
|
_id = cls.parse_id_arg(_id)
|
|
808
1057
|
with ThreadPoolExecutor() as ex:
|
|
@@ -815,7 +1064,19 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
815
1064
|
|
|
816
1065
|
@classmethod
|
|
817
1066
|
def parse_relationship_assessment(cls, src: "Region", assessment):
|
|
1067
|
+
"""
|
|
1068
|
+
Given a region, and the fetched assessment json, yield
|
|
1069
|
+
RegionRelationAssignment object.
|
|
818
1070
|
|
|
1071
|
+
Parameters
|
|
1072
|
+
----------
|
|
1073
|
+
src: Region
|
|
1074
|
+
assessment: dict
|
|
1075
|
+
|
|
1076
|
+
Returns
|
|
1077
|
+
-------
|
|
1078
|
+
Iterable[RegionRelationAssessments]
|
|
1079
|
+
"""
|
|
819
1080
|
all_regions = [
|
|
820
1081
|
region
|
|
821
1082
|
for p in _parcellation.Parcellation.registry()
|
|
@@ -836,7 +1097,11 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
836
1097
|
]
|
|
837
1098
|
|
|
838
1099
|
for found_target in found_targets:
|
|
839
|
-
yield cls(
|
|
1100
|
+
yield cls(
|
|
1101
|
+
query_structure=src,
|
|
1102
|
+
assigned_structure=found_target,
|
|
1103
|
+
qualification=Qualification.parse_relation_assessment(overlap)
|
|
1104
|
+
)
|
|
840
1105
|
|
|
841
1106
|
if "https://openminds.ebrains.eu/sands/ParcellationEntity" in target.get("type"):
|
|
842
1107
|
pev_uuids = [
|
|
@@ -846,28 +1111,47 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
846
1111
|
]
|
|
847
1112
|
for reg in all_regions:
|
|
848
1113
|
if reg in pev_uuids:
|
|
849
|
-
yield cls(
|
|
1114
|
+
yield cls(
|
|
1115
|
+
query_structure=src,
|
|
1116
|
+
assigned_structure=reg,
|
|
1117
|
+
qualification=Qualification.parse_relation_assessment(overlap)
|
|
1118
|
+
)
|
|
850
1119
|
|
|
851
1120
|
@classmethod
|
|
852
1121
|
@_register_region_reference_type("openminds/CustomAnatomicalEntity")
|
|
853
1122
|
def translate_cae(cls, src: "Region", _id: Union[str, List[str]]):
|
|
1123
|
+
"""Register how CustomAnatomicalEntity should be parsed
|
|
1124
|
+
|
|
1125
|
+
Parameters
|
|
1126
|
+
----------
|
|
1127
|
+
src: Region
|
|
1128
|
+
_id: str|list[str]
|
|
1129
|
+
|
|
1130
|
+
Returns
|
|
1131
|
+
-------
|
|
1132
|
+
Iterable[RegionRelationAssessments]
|
|
1133
|
+
"""
|
|
854
1134
|
caes = cls.get_snapshot_factory("CustomAnatomicalEntity")(_id)
|
|
855
1135
|
for cae in caes:
|
|
856
1136
|
for ass in cae.get("relationAssessment", []):
|
|
857
1137
|
yield from cls.parse_relationship_assessment(src, ass)
|
|
858
1138
|
|
|
859
1139
|
@classmethod
|
|
860
|
-
@_register_region_reference_type("openminds/
|
|
861
|
-
def
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1140
|
+
@_register_region_reference_type("openminds/ParcellationEntity")
|
|
1141
|
+
def translate_pes(cls, src: "Region", _id: Union[str, List[str]]):
|
|
1142
|
+
"""
|
|
1143
|
+
Register how ParcellationEntity should be parsed
|
|
1144
|
+
|
|
1145
|
+
Parameters
|
|
1146
|
+
----------
|
|
1147
|
+
src: Region
|
|
1148
|
+
_id: str|list[str]
|
|
1149
|
+
|
|
1150
|
+
Returns
|
|
1151
|
+
-------
|
|
1152
|
+
Iterable[RegionRelationAssessments]
|
|
1153
|
+
"""
|
|
1154
|
+
pes = cls.get_snapshot_factory("ParcellationEntity")(_id)
|
|
871
1155
|
|
|
872
1156
|
all_regions = [
|
|
873
1157
|
region
|
|
@@ -876,30 +1160,15 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
876
1160
|
]
|
|
877
1161
|
|
|
878
1162
|
for pe in pes:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
has_versions = pe.get("hasVersion", [])
|
|
882
|
-
for has_version in has_versions:
|
|
883
|
-
uuid = cls.get_uuid(has_version)
|
|
884
|
-
|
|
885
|
-
# ignore if uuid is referring to src region
|
|
886
|
-
if uuid == src:
|
|
1163
|
+
for region in all_regions:
|
|
1164
|
+
if region is src:
|
|
887
1165
|
continue
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
region
|
|
891
|
-
for region in all_regions
|
|
892
|
-
if region == uuid
|
|
893
|
-
]
|
|
894
|
-
if len(found_targets) == 0:
|
|
895
|
-
logger.warning(f"other version with uuid {uuid} not found")
|
|
896
|
-
continue
|
|
897
|
-
|
|
898
|
-
for found_target in found_targets:
|
|
1166
|
+
region_peid = get_peid_from_region(region)
|
|
1167
|
+
if region_peid and (region_peid in pe.get("id")):
|
|
899
1168
|
yield cls(
|
|
900
1169
|
query_structure=src,
|
|
901
|
-
assigned_structure=
|
|
902
|
-
qualification=
|
|
1170
|
+
assigned_structure=region,
|
|
1171
|
+
qualification=Qualification.OTHER_VERSION
|
|
903
1172
|
)
|
|
904
1173
|
|
|
905
1174
|
# homologuous
|
|
@@ -907,8 +1176,45 @@ class RegionRelationAssessments(RelationAssignment[Region]):
|
|
|
907
1176
|
for relation in relations:
|
|
908
1177
|
yield from cls.parse_relationship_assessment(src, relation)
|
|
909
1178
|
|
|
1179
|
+
@classmethod
|
|
1180
|
+
@_register_region_reference_type("openminds/ParcellationEntityVersion")
|
|
1181
|
+
def translate_pevs(cls, src: "Region", _id: Union[str, List[str]]):
|
|
1182
|
+
"""
|
|
1183
|
+
Register how ParcellationEntityVersion should be parsed
|
|
1184
|
+
|
|
1185
|
+
Parameters
|
|
1186
|
+
----------
|
|
1187
|
+
src: Region
|
|
1188
|
+
_id: str|list[str]
|
|
1189
|
+
|
|
1190
|
+
Returns
|
|
1191
|
+
-------
|
|
1192
|
+
Iterable[RegionRelationAssessments]
|
|
1193
|
+
"""
|
|
1194
|
+
pe_uuids = [
|
|
1195
|
+
uuid for uuid in
|
|
1196
|
+
{
|
|
1197
|
+
cls.get_uuid(pe)
|
|
1198
|
+
for pev in cls.get_snapshot_factory("ParcellationEntityVersion")(_id)
|
|
1199
|
+
for pe in pev.get("isVersionOf")
|
|
1200
|
+
}
|
|
1201
|
+
]
|
|
1202
|
+
yield from cls.translate_pes(src, pe_uuids)
|
|
1203
|
+
|
|
910
1204
|
@classmethod
|
|
911
1205
|
def parse_from_region(cls, region: "Region") -> Iterable["RegionRelationAssessments"]:
|
|
1206
|
+
"""
|
|
1207
|
+
Main entry on how related regions should be retrieved. Given a region,
|
|
1208
|
+
retrieves all RegionRelationAssessments
|
|
1209
|
+
|
|
1210
|
+
Parameters
|
|
1211
|
+
----------
|
|
1212
|
+
region: Region
|
|
1213
|
+
|
|
1214
|
+
Returns
|
|
1215
|
+
-------
|
|
1216
|
+
Iterable[RegionRelationAssessments]
|
|
1217
|
+
"""
|
|
912
1218
|
if not region._spec:
|
|
913
1219
|
return None
|
|
914
1220
|
for ebrain_type, ebrain_ref in region._spec.get("ebrains", {}).items():
|