siibra 1.0a19__py3-none-any.whl → 1.0.1a1__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 +7 -7
- siibra/commons.py +8 -53
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +1 -1
- siibra/configuration/factory.py +11 -21
- siibra/core/__init__.py +1 -1
- siibra/core/assignment.py +1 -1
- siibra/core/atlas.py +21 -15
- siibra/core/concept.py +3 -3
- siibra/core/parcellation.py +69 -54
- siibra/core/region.py +178 -158
- siibra/core/space.py +1 -1
- siibra/core/structure.py +2 -2
- siibra/exceptions.py +13 -1
- siibra/experimental/__init__.py +1 -1
- siibra/experimental/contour.py +8 -8
- siibra/experimental/cortical_profile_sampler.py +1 -1
- siibra/experimental/patch.py +3 -3
- siibra/experimental/plane3d.py +12 -12
- siibra/explorer/__init__.py +1 -1
- siibra/explorer/url.py +2 -2
- siibra/explorer/util.py +1 -1
- siibra/features/__init__.py +1 -1
- siibra/features/anchor.py +14 -15
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +1 -1
- siibra/features/connectivity/regional_connectivity.py +4 -4
- 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 +24 -26
- siibra/features/image/__init__.py +1 -1
- siibra/features/image/image.py +2 -2
- 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 +98 -64
- siibra/features/tabular/cortical_profile.py +3 -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 +4 -23
- siibra/features/tabular/receptor_density_fingerprint.py +13 -10
- siibra/features/tabular/receptor_density_profile.py +1 -1
- siibra/features/tabular/regional_timeseries_activity.py +4 -4
- siibra/features/tabular/tabular.py +7 -5
- siibra/livequeries/__init__.py +1 -1
- siibra/livequeries/allen.py +42 -19
- siibra/livequeries/bigbrain.py +21 -12
- siibra/livequeries/ebrains.py +1 -1
- siibra/livequeries/query.py +2 -3
- siibra/locations/__init__.py +11 -11
- siibra/locations/boundingbox.py +30 -29
- siibra/locations/location.py +1 -1
- siibra/locations/point.py +7 -7
- siibra/locations/{pointset.py → pointcloud.py} +36 -33
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +1 -1
- siibra/retrieval/datasets.py +4 -4
- siibra/retrieval/exceptions/__init__.py +1 -1
- siibra/retrieval/repositories.py +13 -30
- siibra/retrieval/requests.py +25 -8
- siibra/vocabularies/__init__.py +1 -1
- siibra/volumes/__init__.py +2 -2
- siibra/volumes/parcellationmap.py +119 -91
- siibra/volumes/providers/__init__.py +1 -1
- siibra/volumes/providers/freesurfer.py +3 -3
- siibra/volumes/providers/gifti.py +1 -1
- siibra/volumes/providers/neuroglancer.py +67 -41
- siibra/volumes/providers/nifti.py +12 -26
- siibra/volumes/providers/provider.py +1 -1
- siibra/volumes/sparsemap.py +125 -246
- siibra/volumes/volume.py +150 -61
- {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/METADATA +26 -4
- siibra-1.0.1a1.dist-info/RECORD +84 -0
- {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/WHEEL +1 -1
- siibra-1.0a19.dist-info/RECORD +0 -84
- {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/LICENSE +0 -0
- {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/top_level.txt +0 -0
siibra/core/region.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -18,7 +18,7 @@ from . import concept, structure, space as _space, parcellation as _parcellation
|
|
|
18
18
|
from .assignment import Qualification, AnatomicalAssignment
|
|
19
19
|
|
|
20
20
|
from ..retrieval.cache import cache_user_fn
|
|
21
|
-
from ..locations import location,
|
|
21
|
+
from ..locations import location, pointcloud, boundingbox as _boundingbox
|
|
22
22
|
from ..volumes import parcellationmap, volume
|
|
23
23
|
from ..commons import (
|
|
24
24
|
logger,
|
|
@@ -26,20 +26,18 @@ from ..commons import (
|
|
|
26
26
|
create_key,
|
|
27
27
|
clear_name,
|
|
28
28
|
InstanceTable,
|
|
29
|
-
SIIBRA_DEFAULT_MAPTYPE,
|
|
30
|
-
SIIBRA_DEFAULT_MAP_THRESHOLD
|
|
31
29
|
)
|
|
32
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, Union, Iterable, Dict, Callable, Tuple
|
|
34
|
+
from typing import List, Union, Iterable, Dict, Callable, Tuple, Set
|
|
38
35
|
from difflib import SequenceMatcher
|
|
39
36
|
from ebrains_drive import BucketApiClient
|
|
40
37
|
import json
|
|
41
38
|
from functools import wraps, reduce
|
|
42
39
|
from concurrent.futures import ThreadPoolExecutor
|
|
40
|
+
from functools import lru_cache
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
REGEX_TYPE = type(re.compile("test"))
|
|
@@ -55,8 +53,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
55
53
|
_regex_re = re.compile(r'^\/(?P<expression>.+)\/(?P<flags>[a-zA-Z]*)$')
|
|
56
54
|
_accepted_flags = "aiLmsux"
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
_GETMASK_CACHE = {}
|
|
57
|
+
_GETMASK_CACHE_MAX_ENTRIES = 1
|
|
60
58
|
|
|
61
59
|
def __init__(
|
|
62
60
|
self,
|
|
@@ -113,20 +111,20 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
113
111
|
)
|
|
114
112
|
|
|
115
113
|
# anytree node will take care to use this appropriately
|
|
116
|
-
self.parent = parent
|
|
117
|
-
self.children = children
|
|
114
|
+
self.parent: "Region" = parent
|
|
115
|
+
self.children: List["Region"] = children
|
|
118
116
|
# convert hex to int tuple if rgb is given
|
|
119
117
|
self.rgb = (
|
|
120
118
|
None if rgb is None
|
|
121
119
|
else tuple(int(rgb[p:p + 2], 16) for p in [1, 3, 5])
|
|
122
120
|
)
|
|
123
|
-
self._supported_spaces = None # computed on 1st call of self.supported_spaces
|
|
121
|
+
self._supported_spaces: Set[_space.Space] = None # computed on 1st call of self.supported_spaces
|
|
124
122
|
self._str_aliases = None
|
|
125
|
-
self.
|
|
123
|
+
self.find = lru_cache(maxsize=3)(self.find)
|
|
126
124
|
|
|
127
125
|
def get_related_regions(self) -> Iterable["RegionRelationAssessments"]:
|
|
128
126
|
"""
|
|
129
|
-
Get
|
|
127
|
+
Get assessments on relations of this region to others defined on EBRAINS.
|
|
130
128
|
|
|
131
129
|
Yields
|
|
132
130
|
------
|
|
@@ -135,8 +133,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
135
133
|
Example
|
|
136
134
|
-------
|
|
137
135
|
>>> region = siibra.get_region("monkey", "PG")
|
|
138
|
-
>>> for
|
|
139
|
-
>>> print(
|
|
136
|
+
>>> for assessment in region.get_related_regions():
|
|
137
|
+
>>> print(assessment)
|
|
140
138
|
'PG' is homologous to 'Area PGa (IPL)'
|
|
141
139
|
'PG' is homologous to 'Area PGa (IPL) left'
|
|
142
140
|
'PG' is homologous to 'Area PGa (IPL) right'
|
|
@@ -265,7 +263,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
265
263
|
|
|
266
264
|
Parameters
|
|
267
265
|
----------
|
|
268
|
-
regionspec: str, regex,
|
|
266
|
+
regionspec: str, regex, Region
|
|
269
267
|
- a string with a possibly inexact name (matched both against the name and the identifier key)
|
|
270
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)
|
|
271
269
|
- a regex applied to region names
|
|
@@ -286,11 +284,6 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
286
284
|
---
|
|
287
285
|
See example 01-003, find regions.
|
|
288
286
|
"""
|
|
289
|
-
key = (regionspec, filter_children, find_topmost)
|
|
290
|
-
MEM = self._CACHED_REGION_SEARCHES
|
|
291
|
-
if key in MEM:
|
|
292
|
-
return MEM[key]
|
|
293
|
-
|
|
294
287
|
if isinstance(regionspec, str):
|
|
295
288
|
# convert the specified string into a regex for matching
|
|
296
289
|
regex_match = self._regex_re.match(regionspec)
|
|
@@ -352,7 +345,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
352
345
|
found_regions = sorted(set(candidates), key=lambda r: r.depth)
|
|
353
346
|
|
|
354
347
|
# reverse is set to True, since SequenceMatcher().ratio(), higher == better
|
|
355
|
-
|
|
348
|
+
return (
|
|
356
349
|
sorted(
|
|
357
350
|
found_regions,
|
|
358
351
|
reverse=True,
|
|
@@ -361,8 +354,6 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
361
354
|
if isinstance(regionspec, str) else found_regions
|
|
362
355
|
)
|
|
363
356
|
|
|
364
|
-
return MEM[key]
|
|
365
|
-
|
|
366
357
|
def matches(self, regionspec):
|
|
367
358
|
"""
|
|
368
359
|
Checks whether this region matches the given region specification.
|
|
@@ -422,15 +413,14 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
422
413
|
|
|
423
414
|
return self._CACHED_MATCHES[regionspec]
|
|
424
415
|
|
|
425
|
-
def
|
|
416
|
+
def get_regional_mask(
|
|
426
417
|
self,
|
|
427
418
|
space: Union[str, _space.Space],
|
|
428
|
-
maptype: MapType =
|
|
429
|
-
threshold: float =
|
|
430
|
-
|
|
431
|
-
) -> volume.Volume:
|
|
419
|
+
maptype: MapType = MapType.LABELLED,
|
|
420
|
+
threshold: float = 0.0,
|
|
421
|
+
) -> volume.FilteredVolume:
|
|
432
422
|
"""
|
|
433
|
-
|
|
423
|
+
Get a binary mask of this region in the given space,
|
|
434
424
|
using the specified MapTypes.
|
|
435
425
|
|
|
436
426
|
Parameters
|
|
@@ -439,128 +429,135 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
439
429
|
The requested reference space
|
|
440
430
|
maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
|
|
441
431
|
The type of map to be used ('labelled' or 'statistical')
|
|
442
|
-
threshold: float,
|
|
432
|
+
threshold: float, default: 0.0
|
|
443
433
|
When fetching a statistical map, use this threshold to convert
|
|
444
|
-
it to a binary mask
|
|
445
|
-
|
|
446
|
-
If specified, fetch the map in this space first, and then perform
|
|
447
|
-
a linear warping from there to the requested space.
|
|
448
|
-
|
|
449
|
-
Tip
|
|
450
|
-
---
|
|
451
|
-
You might want to use this if a map in the requested space
|
|
452
|
-
is not available.
|
|
453
|
-
|
|
454
|
-
Note
|
|
455
|
-
----
|
|
456
|
-
This linear warping is an affine approximation of the
|
|
457
|
-
nonlinear deformation, computed from the warped corner points
|
|
458
|
-
of the bounding box (see siibra.locations.BoundingBox.estimate_affine()).
|
|
459
|
-
It does not require voxel resampling, just replaces the affine
|
|
460
|
-
matrix, but is less accurate than a full nonlinear warping,
|
|
461
|
-
which is currently not supported in siibra-python for images.
|
|
434
|
+
it to a binary mask.
|
|
435
|
+
|
|
462
436
|
Returns
|
|
463
437
|
-------
|
|
464
438
|
Volume (use fetch() to get a NiftiImage)
|
|
465
439
|
"""
|
|
466
|
-
|
|
440
|
+
if isinstance(maptype, str):
|
|
441
|
+
maptype = MapType[maptype.upper()]
|
|
467
442
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
443
|
+
threshold_info = "" if maptype == MapType.LABELLED else f"(threshold: {threshold}) "
|
|
444
|
+
# check cache
|
|
445
|
+
getmask_hash = hash(f"{self.id} - {space} - {maptype}{threshold_info}")
|
|
446
|
+
if getmask_hash in self._GETMASK_CACHE:
|
|
447
|
+
return self._GETMASK_CACHE[getmask_hash]
|
|
471
448
|
|
|
449
|
+
name = f"Mask {threshold_info}of '{self.name} ({self.parcellation})' in "
|
|
450
|
+
try:
|
|
451
|
+
regional_map = self.get_regional_map(space=space, maptype=maptype)
|
|
452
|
+
if maptype == MapType.LABELLED:
|
|
453
|
+
assert threshold == 0.0, f"threshold can only be set for {MapType.STATISTICAL} maps."
|
|
454
|
+
result = volume.FilteredVolume(
|
|
455
|
+
parent_volume=regional_map,
|
|
456
|
+
label=regional_map.label,
|
|
457
|
+
fragment=regional_map.fragment
|
|
458
|
+
)
|
|
459
|
+
result._boundingbox = None
|
|
460
|
+
if maptype == MapType.STATISTICAL:
|
|
461
|
+
result = volume.FilteredVolume(
|
|
462
|
+
parent_volume=regional_map,
|
|
463
|
+
threshold=threshold
|
|
464
|
+
)
|
|
465
|
+
if threshold == 0.0:
|
|
466
|
+
result._boundingbox = regional_map._boundingbox
|
|
467
|
+
name += f"'{result.space}'"
|
|
468
|
+
except NoMapAvailableError as e:
|
|
469
|
+
# This region is not mapped directly in any map in the registry.
|
|
470
|
+
# Try building a map from the child regions
|
|
471
|
+
if (len(self.children) > 0) and self.mapped_in_space(space, recurse=True):
|
|
472
|
+
mapped_descendants: List[Region] = [
|
|
473
|
+
d for d in self.descendants if d.mapped_in_space(space, recurse=False)
|
|
474
|
+
]
|
|
475
|
+
logger.info(f"{self.name} is not mapped in {space}. Merging the masks of its {len(mapped_descendants)} map descendants.")
|
|
476
|
+
descendant_volumes = [
|
|
477
|
+
descendant.get_regional_mask(space=space, maptype=maptype, threshold=threshold)
|
|
478
|
+
for descendant in mapped_descendants
|
|
479
|
+
]
|
|
480
|
+
result = volume.FilteredVolume(
|
|
481
|
+
volume.merge(descendant_volumes),
|
|
482
|
+
label=1
|
|
483
|
+
)
|
|
484
|
+
name += f"'{result.space}' (built by merging the mask {threshold_info} of its descendants)"
|
|
485
|
+
else:
|
|
486
|
+
raise e
|
|
487
|
+
result._name = name
|
|
488
|
+
|
|
489
|
+
while len(self._GETMASK_CACHE) > self._GETMASK_CACHE_MAX_ENTRIES:
|
|
490
|
+
self._GETMASK_CACHE.pop(next(iter(self._GETMASK_CACHE)))
|
|
491
|
+
self._GETMASK_CACHE[getmask_hash] = result
|
|
492
|
+
return result
|
|
493
|
+
|
|
494
|
+
def get_regional_map(
|
|
495
|
+
self,
|
|
496
|
+
space: Union[str, _space.Space],
|
|
497
|
+
maptype: MapType = MapType.LABELLED,
|
|
498
|
+
) -> Union[volume.FilteredVolume, volume.Volume, volume.Subvolume]:
|
|
499
|
+
"""
|
|
500
|
+
Get a volume representing this region in the given space and MapType.
|
|
501
|
+
|
|
502
|
+
Note
|
|
503
|
+
----
|
|
504
|
+
If a region is not mapped in any of the `Map`s in the registry, then
|
|
505
|
+
siibra will get the maps of its children recursively and merge them.
|
|
506
|
+
If no map is available this way as well, an exception is raised.
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
space: Space or str
|
|
511
|
+
The requested reference space
|
|
512
|
+
maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
|
|
513
|
+
The type of map to be used ('labelled' or 'statistical')
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
Volume (use fetch() to get a NiftiImage)
|
|
518
|
+
"""
|
|
472
519
|
if isinstance(maptype, str):
|
|
473
520
|
maptype = MapType[maptype.upper()]
|
|
474
521
|
|
|
475
|
-
# prepare space
|
|
522
|
+
# prepare space instance
|
|
476
523
|
if isinstance(space, str):
|
|
477
524
|
space = _space.Space.get_instance(space)
|
|
478
|
-
fetch_space = space if via_space is None else via_space
|
|
479
|
-
if isinstance(fetch_space, str):
|
|
480
|
-
fetch_space = _space.Space.get_instance(fetch_space)
|
|
481
|
-
|
|
482
|
-
result = None # try to replace this with the actual regionmap volume
|
|
483
525
|
|
|
484
526
|
# see if we find a map supporting the requested region
|
|
485
527
|
for m in parcellationmap.Map.registry():
|
|
486
528
|
if (
|
|
487
|
-
m.space.matches(
|
|
529
|
+
m.space.matches(space)
|
|
488
530
|
and m.parcellation == self.parcellation
|
|
489
531
|
and m.provides_image
|
|
490
532
|
and m.maptype == maptype
|
|
491
533
|
and self.name in m.regions
|
|
492
534
|
):
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
name = f"Statistical mask of {self} on {fetch_space}{f' thresholded by {threshold}' if threshold else ''}"
|
|
499
|
-
else: # compute region mask from labelled parcellation map
|
|
500
|
-
name = f"Mask of {self} in {m.parcellation} on {fetch_space}"
|
|
501
|
-
result = volume.from_array(
|
|
502
|
-
data=imgdata,
|
|
503
|
-
affine=region_img.affine,
|
|
504
|
-
space=fetch_space,
|
|
505
|
-
name=name,
|
|
506
|
-
)
|
|
507
|
-
if result is not None:
|
|
508
|
-
break
|
|
509
|
-
|
|
510
|
-
if result is None:
|
|
511
|
-
# No region map available. Then see if we can build a map from the child regions
|
|
512
|
-
if (len(self.children) > 0) and all(c.mapped_in_space(fetch_space) for c in self.children):
|
|
513
|
-
logger.debug(f"Building regional map of {self.name} in {self.parcellation} from {len(self.children)} child regions.")
|
|
514
|
-
child_volumes = [
|
|
515
|
-
child.get_regional_map(fetch_space, maptype, threshold, via_space)
|
|
516
|
-
for child in self.children
|
|
517
|
-
]
|
|
518
|
-
result = volume.merge(child_volumes)
|
|
519
|
-
result._name = f"Subtree {'mask' if maptype == MapType.LABELLED else 'statistical map of'} built from {self.name}"
|
|
520
|
-
|
|
521
|
-
if result is None:
|
|
522
|
-
raise NoMapAvailableError(f"Cannot build region map for {self.name} from {str(maptype)} maps in {fetch_space}")
|
|
523
|
-
|
|
524
|
-
if via_space is not None:
|
|
525
|
-
# the map volume is taken from an intermediary reference space
|
|
526
|
-
# provided by 'via_space'. Now transform the affine to match the
|
|
527
|
-
# desired target space.
|
|
528
|
-
intermediary_result = result
|
|
529
|
-
transform = intermediary_result.get_boundingbox(clip=True, background=0.0).estimate_affine(space)
|
|
530
|
-
result = volume.from_array(
|
|
531
|
-
imgdata,
|
|
532
|
-
np.dot(transform, region_img.affine),
|
|
533
|
-
space,
|
|
534
|
-
f"{result.name} fetched from {fetch_space} and linearly corrected to match {space}"
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
while len(self._GETMAP_CACHE) > self._GETMAP_CACHE_MAX_ENTRIES:
|
|
538
|
-
self._GETMAP_CACHE.pop(next(iter(self._GETMAP_CACHE)))
|
|
539
|
-
self._GETMAP_CACHE[getmap_hash] = result
|
|
540
|
-
return result
|
|
535
|
+
return m.get_volume(region=self)
|
|
536
|
+
raise NoMapAvailableError(
|
|
537
|
+
f"{self.name} is not mapped in {space} as a {str(maptype)} map."
|
|
538
|
+
" Please try getting the children or getting the mask."
|
|
539
|
+
)
|
|
541
540
|
|
|
542
|
-
def mapped_in_space(self, space, recurse: bool =
|
|
541
|
+
def mapped_in_space(self, space, recurse: bool = False) -> bool:
|
|
543
542
|
"""
|
|
544
|
-
Verifies
|
|
543
|
+
Verifies whether this region is defined by an explicit map in the given space.
|
|
545
544
|
|
|
546
545
|
Parameters
|
|
547
546
|
----------
|
|
548
547
|
space: Space or str
|
|
549
548
|
reference space
|
|
550
|
-
recurse: bool, default:
|
|
551
|
-
If True, check if all child regions are mapped instead
|
|
549
|
+
recurse: bool, default: False
|
|
550
|
+
If True, check if itself or all child regions are mapped instead recursively.
|
|
552
551
|
Returns
|
|
553
552
|
-------
|
|
554
553
|
bool
|
|
555
554
|
"""
|
|
556
555
|
from ..volumes.parcellationmap import Map
|
|
557
556
|
for m in Map.registry():
|
|
558
|
-
# Use and operant for efficiency (short circuiting logic)
|
|
559
|
-
# Put the most inexpensive logic first
|
|
560
557
|
if (
|
|
561
|
-
|
|
562
|
-
and m.space.matches(space)
|
|
558
|
+
m.space.matches(space)
|
|
563
559
|
and m.parcellation.matches(self.parcellation)
|
|
560
|
+
and self.name in m.regions
|
|
564
561
|
):
|
|
565
562
|
return True
|
|
566
563
|
if recurse and not self.is_leaf:
|
|
@@ -571,21 +568,16 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
571
568
|
@property
|
|
572
569
|
def supported_spaces(self) -> List[_space.Space]:
|
|
573
570
|
"""
|
|
574
|
-
The
|
|
575
|
-
|
|
571
|
+
The list of spaces for which a mask could be extracted from an existing
|
|
572
|
+
map or combination of masks of its children.
|
|
576
573
|
"""
|
|
577
574
|
if self._supported_spaces is None:
|
|
578
|
-
self._supported_spaces = sorted(
|
|
579
|
-
|
|
580
|
-
|
|
575
|
+
self._supported_spaces = sorted({
|
|
576
|
+
s for s in _space.Space.registry()
|
|
577
|
+
if self.mapped_in_space(s, recurse=True)
|
|
578
|
+
})
|
|
581
579
|
return self._supported_spaces
|
|
582
580
|
|
|
583
|
-
def supports_space(self, space: _space.Space):
|
|
584
|
-
"""
|
|
585
|
-
Return true if this region supports the given space, else False.
|
|
586
|
-
"""
|
|
587
|
-
return any(s.matches(space) for s in self.supported_spaces)
|
|
588
|
-
|
|
589
581
|
@property
|
|
590
582
|
def spaces(self):
|
|
591
583
|
return InstanceTable(
|
|
@@ -598,7 +590,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
598
590
|
return len(self.find(other)) > 0
|
|
599
591
|
else:
|
|
600
592
|
try:
|
|
601
|
-
regionmap = self.
|
|
593
|
+
regionmap = self.get_regional_mask(space=other.space)
|
|
602
594
|
return regionmap.__contains__(other)
|
|
603
595
|
except NoMapAvailableError:
|
|
604
596
|
return False
|
|
@@ -627,9 +619,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
627
619
|
return None
|
|
628
620
|
return self._ASSIGNMENT_CACHE[other, self].invert()
|
|
629
621
|
|
|
630
|
-
if isinstance(other, location.Location):
|
|
631
|
-
if self.mapped_in_space(other.space):
|
|
632
|
-
regionmap = self.
|
|
622
|
+
if isinstance(other, (location.Location, volume.Volume)):
|
|
623
|
+
if self.mapped_in_space(other.space, recurse=True):
|
|
624
|
+
regionmap = self.get_regional_mask(other.space)
|
|
633
625
|
self._ASSIGNMENT_CACHE[self, other] = regionmap.assign(other)
|
|
634
626
|
return self._ASSIGNMENT_CACHE[self, other]
|
|
635
627
|
|
|
@@ -646,7 +638,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
646
638
|
for targetspace in self.supported_spaces:
|
|
647
639
|
try:
|
|
648
640
|
other_warped = other.warp(targetspace)
|
|
649
|
-
regionmap = self.
|
|
641
|
+
regionmap = self.get_regional_mask(targetspace)
|
|
650
642
|
assignment_result = regionmap.assign(other_warped)
|
|
651
643
|
except SpaceWarpingFailedError:
|
|
652
644
|
try:
|
|
@@ -696,10 +688,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
696
688
|
self,
|
|
697
689
|
space: _space.Space,
|
|
698
690
|
maptype: MapType = MapType.LABELLED,
|
|
699
|
-
threshold_statistical=
|
|
700
|
-
restrict_space=True,
|
|
691
|
+
threshold_statistical: float = 0.0,
|
|
692
|
+
restrict_space: bool = True,
|
|
701
693
|
**fetch_kwargs
|
|
702
|
-
):
|
|
694
|
+
) -> Union[_boundingbox.BoundingBox, None]:
|
|
703
695
|
"""
|
|
704
696
|
Compute the bounding box of this region in the given space.
|
|
705
697
|
|
|
@@ -711,9 +703,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
711
703
|
Type of map to build ('labelled' will result in a binary mask,
|
|
712
704
|
'statistical' attempts to build a statistical mask, possibly by
|
|
713
705
|
elementwise maximum of statistical maps of children)
|
|
714
|
-
threshold_statistical: float,
|
|
715
|
-
|
|
716
|
-
|
|
706
|
+
threshold_statistical: float, default: 0.0
|
|
707
|
+
When masking a statistical map, use this threshold to convert
|
|
708
|
+
it to a binary mask before finding its bounding box.
|
|
717
709
|
restrict_space: bool, default: False
|
|
718
710
|
If True, it will not try to fetch maps from other spaces and warp
|
|
719
711
|
its boundingbox to requested space.
|
|
@@ -724,16 +716,20 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
724
716
|
"""
|
|
725
717
|
spaceobj = _space.Space.get_instance(space)
|
|
726
718
|
try:
|
|
727
|
-
mask = self.
|
|
719
|
+
mask = self.get_regional_mask(
|
|
728
720
|
spaceobj, maptype=maptype, threshold=threshold_statistical
|
|
729
721
|
)
|
|
730
|
-
return mask.get_boundingbox(
|
|
722
|
+
return mask.get_boundingbox(
|
|
723
|
+
clip=True,
|
|
724
|
+
background=0.0,
|
|
725
|
+
**fetch_kwargs
|
|
726
|
+
)
|
|
731
727
|
except (RuntimeError, ValueError):
|
|
732
728
|
if restrict_space:
|
|
733
729
|
return None
|
|
734
730
|
for other_space in self.parcellation.spaces - spaceobj:
|
|
735
731
|
try:
|
|
736
|
-
mask = self.
|
|
732
|
+
mask = self.get_regional_mask(
|
|
737
733
|
other_space,
|
|
738
734
|
maptype=maptype,
|
|
739
735
|
threshold=threshold_statistical,
|
|
@@ -754,7 +750,14 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
754
750
|
logger.error(f"Could not compute bounding box for {self.name}.")
|
|
755
751
|
return None
|
|
756
752
|
|
|
757
|
-
def compute_centroids(
|
|
753
|
+
def compute_centroids(
|
|
754
|
+
self,
|
|
755
|
+
space: _space.Space,
|
|
756
|
+
maptype: MapType = MapType.LABELLED,
|
|
757
|
+
threshold_statistical: float = 0.0,
|
|
758
|
+
split_components: bool = True,
|
|
759
|
+
**fetch_kwargs,
|
|
760
|
+
) -> pointcloud.PointCloud:
|
|
758
761
|
"""
|
|
759
762
|
Compute the centroids of the region in the given space.
|
|
760
763
|
|
|
@@ -762,19 +765,32 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
762
765
|
----------
|
|
763
766
|
space: Space
|
|
764
767
|
reference space in which the computation will be performed
|
|
768
|
+
maptype: MapType, default: MapType.LABELLED
|
|
769
|
+
Type of map to build ('labelled' will result in a binary mask,
|
|
770
|
+
'statistical' attempts to build a statistical mask, possibly by
|
|
771
|
+
elementwise maximum of statistical maps of children)
|
|
772
|
+
threshold_statistical: float, default: 0.0
|
|
773
|
+
When masking a statistical map, use this threshold to convert
|
|
774
|
+
it to a binary mask before finding its centroids.
|
|
765
775
|
|
|
766
776
|
Returns
|
|
767
777
|
-------
|
|
768
|
-
|
|
769
|
-
Found centroids (as Point objects) in a
|
|
778
|
+
PointCloud
|
|
779
|
+
Found centroids (as Point objects) in a PointCloud
|
|
770
780
|
|
|
771
781
|
Note
|
|
772
782
|
----
|
|
773
783
|
A region can generally have multiple centroids if it has multiple
|
|
774
784
|
connected components in the map.
|
|
775
785
|
"""
|
|
776
|
-
props = self.spatial_props(
|
|
777
|
-
|
|
786
|
+
props = self.spatial_props(
|
|
787
|
+
space=space,
|
|
788
|
+
maptype=maptype,
|
|
789
|
+
threshold_statistical=threshold_statistical,
|
|
790
|
+
split_components=split_components,
|
|
791
|
+
**fetch_kwargs,
|
|
792
|
+
)
|
|
793
|
+
return pointcloud.PointCloud(
|
|
778
794
|
[c.centroid for c in props],
|
|
779
795
|
space=space
|
|
780
796
|
)
|
|
@@ -783,7 +799,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
783
799
|
self,
|
|
784
800
|
space: _space.Space,
|
|
785
801
|
maptype: MapType = MapType.LABELLED,
|
|
786
|
-
threshold_statistical=
|
|
802
|
+
threshold_statistical: float = 0.0,
|
|
803
|
+
split_components: bool = True,
|
|
804
|
+
**fetch_kwargs,
|
|
787
805
|
):
|
|
788
806
|
"""
|
|
789
807
|
Compute spatial properties for connected components of this region in the given space.
|
|
@@ -796,21 +814,21 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
796
814
|
Type of map to build ('labelled' will result in a binary mask,
|
|
797
815
|
'statistical' attempts to build a statistical mask, possibly by
|
|
798
816
|
elementwise maximum of statistical maps of children)
|
|
799
|
-
threshold_statistical: float,
|
|
817
|
+
threshold_statistical: float, default: 0.0
|
|
800
818
|
if not None, masks will be preferably constructed by thresholding
|
|
801
819
|
statistical maps with the given value.
|
|
802
820
|
|
|
803
821
|
Returns
|
|
804
822
|
-------
|
|
805
|
-
|
|
806
|
-
|
|
823
|
+
List
|
|
824
|
+
List of region's component spatial properties
|
|
807
825
|
"""
|
|
808
826
|
if not isinstance(space, _space.Space):
|
|
809
827
|
space = _space.Space.get_instance(space)
|
|
810
828
|
|
|
811
829
|
# build binary mask of the image
|
|
812
830
|
try:
|
|
813
|
-
region_vol = self.
|
|
831
|
+
region_vol = self.get_regional_mask(
|
|
814
832
|
space, maptype=maptype, threshold=threshold_statistical
|
|
815
833
|
)
|
|
816
834
|
except NoMapAvailableError:
|
|
@@ -820,7 +838,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
820
838
|
f"{', '.join(s.name for s in self.supported_spaces)}"
|
|
821
839
|
)
|
|
822
840
|
|
|
823
|
-
return region_vol.compute_spatial_props(
|
|
841
|
+
return region_vol.compute_spatial_props(
|
|
842
|
+
split_components=split_components, **fetch_kwargs
|
|
843
|
+
)
|
|
824
844
|
|
|
825
845
|
def __iter__(self):
|
|
826
846
|
"""
|
|
@@ -832,9 +852,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
832
852
|
def intersection(self, other: "location.Location") -> "location.Location":
|
|
833
853
|
"""Use this region for filtering a location object."""
|
|
834
854
|
|
|
835
|
-
if self.
|
|
855
|
+
if self.mapped_in_space(other.space, recurse=True):
|
|
836
856
|
try:
|
|
837
|
-
volume = self.
|
|
857
|
+
volume = self.get_regional_mask(other.space)
|
|
838
858
|
if volume is not None:
|
|
839
859
|
return volume.intersection(other)
|
|
840
860
|
except NotImplementedError:
|
|
@@ -844,7 +864,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
844
864
|
for space in self.supported_spaces:
|
|
845
865
|
if space.provides_image:
|
|
846
866
|
try:
|
|
847
|
-
volume = self.
|
|
867
|
+
volume = self.get_regional_mask(space)
|
|
848
868
|
if volume is not None:
|
|
849
869
|
intersection = volume.intersection(other)
|
|
850
870
|
logger.info(f"Warped {other} to {space} to find the intersection.")
|
|
@@ -898,7 +918,7 @@ def get_peid_from_region(region: Region) -> str:
|
|
|
898
918
|
|
|
899
919
|
def get_related_regions(region: Region) -> Iterable["RegionRelationAssessments"]:
|
|
900
920
|
"""
|
|
901
|
-
Get
|
|
921
|
+
Get assessments on relations of a region to others defined on EBRAINS.
|
|
902
922
|
|
|
903
923
|
Parameters
|
|
904
924
|
----------
|
|
@@ -911,8 +931,8 @@ def get_related_regions(region: Region) -> Iterable["RegionRelationAssessments"]
|
|
|
911
931
|
Example
|
|
912
932
|
-------
|
|
913
933
|
>>> region = siibra.get_region("monkey", "PG")
|
|
914
|
-
>>> for
|
|
915
|
-
>>> print(
|
|
934
|
+
>>> for assessment in siibra.core.region.get_related_regions(region):
|
|
935
|
+
>>> print(assessment)
|
|
916
936
|
'PG' is homologous to 'Area PGa (IPL)'
|
|
917
937
|
'PG' is homologous to 'Area PGa (IPL) left'
|
|
918
938
|
'PG' is homologous to 'Area PGa (IPL) right'
|
siibra/core/space.py
CHANGED
siibra/core/structure.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -76,7 +76,7 @@ class BrainStructure(ABC):
|
|
|
76
76
|
# Two cases:
|
|
77
77
|
# 1) self is location, other is location -> look at spatial intersection/relationship, do it here
|
|
78
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,
|
|
79
|
+
# If self is region -> Region overwrite this method, addressed there
|
|
80
80
|
|
|
81
81
|
assert not isinstance(self, _region.Region) # method is overwritten by Region!
|
|
82
82
|
if (self, other) in self._ASSIGNMENT_CACHE:
|
siibra/exceptions.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -53,3 +53,15 @@ class ZeroVolumeBoundingBox(Exception):
|
|
|
53
53
|
|
|
54
54
|
class NoneCoordinateSuppliedError(ValueError):
|
|
55
55
|
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MapNotFound(ValueError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EmptyPointCloudError(ValueError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NoPredifinedColormapException(ValueError):
|
|
67
|
+
pass
|
siibra/experimental/__init__.py
CHANGED