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/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");
|
|
@@ -18,30 +18,26 @@ 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,
|
|
25
25
|
MapType,
|
|
26
|
-
affine_scaling,
|
|
27
26
|
create_key,
|
|
28
27
|
clear_name,
|
|
29
28
|
InstanceTable,
|
|
30
|
-
SIIBRA_DEFAULT_MAPTYPE,
|
|
31
|
-
SIIBRA_DEFAULT_MAP_THRESHOLD
|
|
32
29
|
)
|
|
33
30
|
from ..exceptions import NoMapAvailableError, SpaceWarpingFailedError
|
|
34
31
|
|
|
35
|
-
import numpy as np
|
|
36
32
|
import re
|
|
37
33
|
import anytree
|
|
38
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
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,19 +45,6 @@ REGEX_TYPE = type(re.compile("test"))
|
|
|
49
45
|
THRESHOLD_STATISTICAL_MAPS = None
|
|
50
46
|
|
|
51
47
|
|
|
52
|
-
@dataclass
|
|
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
48
|
class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
66
49
|
"""
|
|
67
50
|
Representation of a region with name and more optional attributes
|
|
@@ -85,6 +68,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
85
68
|
datasets: list = [],
|
|
86
69
|
rgb: str = None,
|
|
87
70
|
spec=None,
|
|
71
|
+
prerelease: bool = False,
|
|
88
72
|
):
|
|
89
73
|
"""
|
|
90
74
|
Constructs a new Region object.
|
|
@@ -122,7 +106,8 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
122
106
|
modality=modality,
|
|
123
107
|
publications=publications,
|
|
124
108
|
datasets=datasets,
|
|
125
|
-
spec=spec
|
|
109
|
+
spec=spec,
|
|
110
|
+
prerelease=prerelease,
|
|
126
111
|
)
|
|
127
112
|
|
|
128
113
|
# anytree node will take care to use this appropriately
|
|
@@ -135,7 +120,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
135
120
|
)
|
|
136
121
|
self._supported_spaces = None # computed on 1st call of self.supported_spaces
|
|
137
122
|
self._str_aliases = None
|
|
138
|
-
self.
|
|
123
|
+
self.find = lru_cache(maxsize=3)(self.find)
|
|
139
124
|
|
|
140
125
|
def get_related_regions(self) -> Iterable["RegionRelationAssessments"]:
|
|
141
126
|
"""
|
|
@@ -278,7 +263,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
278
263
|
|
|
279
264
|
Parameters
|
|
280
265
|
----------
|
|
281
|
-
regionspec: str, regex,
|
|
266
|
+
regionspec: str, regex, Region
|
|
282
267
|
- a string with a possibly inexact name (matched both against the name and the identifier key)
|
|
283
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)
|
|
284
269
|
- a regex applied to region names
|
|
@@ -299,11 +284,6 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
299
284
|
---
|
|
300
285
|
See example 01-003, find regions.
|
|
301
286
|
"""
|
|
302
|
-
key = (regionspec, filter_children, find_topmost)
|
|
303
|
-
MEM = self._CACHED_REGION_SEARCHES
|
|
304
|
-
if key in MEM:
|
|
305
|
-
return MEM[key]
|
|
306
|
-
|
|
307
287
|
if isinstance(regionspec, str):
|
|
308
288
|
# convert the specified string into a regex for matching
|
|
309
289
|
regex_match = self._regex_re.match(regionspec)
|
|
@@ -365,7 +345,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
365
345
|
found_regions = sorted(set(candidates), key=lambda r: r.depth)
|
|
366
346
|
|
|
367
347
|
# reverse is set to True, since SequenceMatcher().ratio(), higher == better
|
|
368
|
-
|
|
348
|
+
return (
|
|
369
349
|
sorted(
|
|
370
350
|
found_regions,
|
|
371
351
|
reverse=True,
|
|
@@ -374,8 +354,6 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
374
354
|
if isinstance(regionspec, str) else found_regions
|
|
375
355
|
)
|
|
376
356
|
|
|
377
|
-
return MEM[key]
|
|
378
|
-
|
|
379
357
|
def matches(self, regionspec):
|
|
380
358
|
"""
|
|
381
359
|
Checks whether this region matches the given region specification.
|
|
@@ -435,15 +413,14 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
435
413
|
|
|
436
414
|
return self._CACHED_MATCHES[regionspec]
|
|
437
415
|
|
|
438
|
-
def
|
|
416
|
+
def get_regional_mask(
|
|
439
417
|
self,
|
|
440
418
|
space: Union[str, _space.Space],
|
|
441
|
-
maptype: MapType =
|
|
442
|
-
threshold: float =
|
|
443
|
-
|
|
444
|
-
) -> volume.Volume:
|
|
419
|
+
maptype: MapType = MapType.LABELLED,
|
|
420
|
+
threshold: float = 0.0,
|
|
421
|
+
) -> volume.FilteredVolume:
|
|
445
422
|
"""
|
|
446
|
-
|
|
423
|
+
Get a binary mask of this region in the given space,
|
|
447
424
|
using the specified MapTypes.
|
|
448
425
|
|
|
449
426
|
Parameters
|
|
@@ -452,106 +429,98 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
452
429
|
The requested reference space
|
|
453
430
|
maptype: MapType, default: SIIBRA_DEFAULT_MAPTYPE
|
|
454
431
|
The type of map to be used ('labelled' or 'statistical')
|
|
455
|
-
threshold: float,
|
|
432
|
+
threshold: float, default: 0.0
|
|
456
433
|
When fetching a statistical map, use this threshold to convert
|
|
457
|
-
it to a binary mask
|
|
458
|
-
|
|
459
|
-
If specified, fetch the map in this space first, and then perform
|
|
460
|
-
a linear warping from there to the requested space.
|
|
461
|
-
|
|
462
|
-
Tip
|
|
463
|
-
---
|
|
464
|
-
You might want to use this if a map in the requested space
|
|
465
|
-
is not available.
|
|
466
|
-
|
|
467
|
-
Note
|
|
468
|
-
----
|
|
469
|
-
This linear warping is an affine approximation of the
|
|
470
|
-
nonlinear deformation, computed from the warped corner points
|
|
471
|
-
of the bounding box (see siibra.locations.BoundingBox.estimate_affine()).
|
|
472
|
-
It does not require voxel resampling, just replaces the affine
|
|
473
|
-
matrix, but is less accurate than a full nonlinear warping,
|
|
474
|
-
which is currently not supported in siibra-python for images.
|
|
434
|
+
it to a binary mask.
|
|
435
|
+
|
|
475
436
|
Returns
|
|
476
437
|
-------
|
|
477
438
|
Volume (use fetch() to get a NiftiImage)
|
|
478
439
|
"""
|
|
479
|
-
|
|
440
|
+
if isinstance(maptype, str):
|
|
441
|
+
maptype = MapType[maptype.upper()]
|
|
480
442
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
484
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
|
+
"""
|
|
485
501
|
if isinstance(maptype, str):
|
|
486
502
|
maptype = MapType[maptype.upper()]
|
|
487
503
|
|
|
488
|
-
# prepare space
|
|
504
|
+
# prepare space instance
|
|
489
505
|
if isinstance(space, str):
|
|
490
506
|
space = _space.Space.get_instance(space)
|
|
491
|
-
fetch_space = space if via_space is None else via_space
|
|
492
|
-
if isinstance(fetch_space, str):
|
|
493
|
-
fetch_space = _space.Space.get_instance(fetch_space)
|
|
494
|
-
|
|
495
|
-
result = None # try to replace this with the actual regionmap volume
|
|
496
507
|
|
|
497
508
|
# see if we find a map supporting the requested region
|
|
498
509
|
for m in parcellationmap.Map.registry():
|
|
499
510
|
if (
|
|
500
|
-
m.space.matches(
|
|
511
|
+
m.space.matches(space)
|
|
501
512
|
and m.parcellation == self.parcellation
|
|
502
513
|
and m.provides_image
|
|
503
514
|
and m.maptype == maptype
|
|
504
515
|
and self.name in m.regions
|
|
505
516
|
):
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
name = f"Statistical mask of {self} on {fetch_space}{f' thresholded by {threshold}' if threshold else ''}"
|
|
512
|
-
else: # compute region mask from labelled parcellation map
|
|
513
|
-
name = f"Mask of {self} in {m.parcellation} on {fetch_space}"
|
|
514
|
-
result = volume.from_array(
|
|
515
|
-
data=imgdata,
|
|
516
|
-
affine=region_img.affine,
|
|
517
|
-
space=fetch_space,
|
|
518
|
-
name=name,
|
|
519
|
-
)
|
|
520
|
-
if result is not None:
|
|
521
|
-
break
|
|
522
|
-
|
|
523
|
-
if result is None:
|
|
524
|
-
# No region map available. Then see if we can build a map from the child regions
|
|
525
|
-
if (len(self.children) > 0) and all(c.mapped_in_space(fetch_space) for c in self.children):
|
|
526
|
-
logger.debug(f"Building regional map of {self.name} in {self.parcellation} from {len(self.children)} child regions.")
|
|
527
|
-
child_volumes = [
|
|
528
|
-
child.get_regional_map(fetch_space, maptype, threshold, via_space)
|
|
529
|
-
for child in self.children
|
|
530
|
-
]
|
|
531
|
-
result = volume.merge(child_volumes)
|
|
532
|
-
result._name = f"Subtree {'mask' if maptype == MapType.LABELLED else 'statistical map of'} built from {self.name}"
|
|
533
|
-
|
|
534
|
-
if result is None:
|
|
535
|
-
raise NoMapAvailableError(f"Cannot build region map for {self.name} from {str(maptype)} maps in {fetch_space}")
|
|
536
|
-
|
|
537
|
-
if via_space is not None:
|
|
538
|
-
# the map volume is taken from an intermediary reference space
|
|
539
|
-
# provided by 'via_space'. Now transform the affine to match the
|
|
540
|
-
# desired target space.
|
|
541
|
-
intermediary_result = result
|
|
542
|
-
transform = intermediary_result.get_boundingbox(clip=True, background=0.0).estimate_affine(space)
|
|
543
|
-
result = volume.from_array(
|
|
544
|
-
imgdata,
|
|
545
|
-
np.dot(transform, region_img.affine),
|
|
546
|
-
space,
|
|
547
|
-
f"{result.name} fetched from {fetch_space} and linearly corrected to match {space}"
|
|
517
|
+
return m.get_volume(region=self)
|
|
518
|
+
else:
|
|
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."
|
|
548
522
|
)
|
|
549
523
|
|
|
550
|
-
while len(self._GETMAP_CACHE) > self._GETMAP_CACHE_MAX_ENTRIES:
|
|
551
|
-
self._GETMAP_CACHE.pop(next(iter(self._GETMAP_CACHE)))
|
|
552
|
-
self._GETMAP_CACHE[getmap_hash] = result
|
|
553
|
-
return result
|
|
554
|
-
|
|
555
524
|
def mapped_in_space(self, space, recurse: bool = True) -> bool:
|
|
556
525
|
"""
|
|
557
526
|
Verifies wether this region is defined by an explicit map in the given space.
|
|
@@ -611,7 +580,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
611
580
|
return len(self.find(other)) > 0
|
|
612
581
|
else:
|
|
613
582
|
try:
|
|
614
|
-
regionmap = self.
|
|
583
|
+
regionmap = self.get_regional_mask(space=other.space)
|
|
615
584
|
return regionmap.__contains__(other)
|
|
616
585
|
except NoMapAvailableError:
|
|
617
586
|
return False
|
|
@@ -640,23 +609,35 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
640
609
|
return None
|
|
641
610
|
return self._ASSIGNMENT_CACHE[other, self].invert()
|
|
642
611
|
|
|
643
|
-
if isinstance(other, location.Location):
|
|
612
|
+
if isinstance(other, (location.Location, volume.Volume)):
|
|
644
613
|
if self.mapped_in_space(other.space):
|
|
645
|
-
regionmap = self.
|
|
614
|
+
regionmap = self.get_regional_mask(other.space)
|
|
646
615
|
self._ASSIGNMENT_CACHE[self, other] = regionmap.assign(other)
|
|
647
616
|
return self._ASSIGNMENT_CACHE[self, other]
|
|
648
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
|
+
|
|
649
627
|
assignment_result = None
|
|
650
|
-
for
|
|
628
|
+
for targetspace in self.supported_spaces:
|
|
651
629
|
try:
|
|
652
|
-
other_warped = other.warp(
|
|
653
|
-
regionmap = self.
|
|
630
|
+
other_warped = other.warp(targetspace)
|
|
631
|
+
regionmap = self.get_regional_mask(targetspace)
|
|
654
632
|
assignment_result = regionmap.assign(other_warped)
|
|
655
633
|
except SpaceWarpingFailedError:
|
|
656
634
|
try:
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
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)
|
|
660
641
|
except SpaceWarpingFailedError:
|
|
661
642
|
continue
|
|
662
643
|
assignment_result = regionbbox_warped.assign(other)
|
|
@@ -697,10 +678,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
697
678
|
self,
|
|
698
679
|
space: _space.Space,
|
|
699
680
|
maptype: MapType = MapType.LABELLED,
|
|
700
|
-
threshold_statistical=
|
|
701
|
-
restrict_space=
|
|
681
|
+
threshold_statistical: float = 0.0,
|
|
682
|
+
restrict_space: bool = True,
|
|
702
683
|
**fetch_kwargs
|
|
703
|
-
):
|
|
684
|
+
) -> Union[_boundingbox.BoundingBox, None]:
|
|
704
685
|
"""
|
|
705
686
|
Compute the bounding box of this region in the given space.
|
|
706
687
|
|
|
@@ -712,9 +693,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
712
693
|
Type of map to build ('labelled' will result in a binary mask,
|
|
713
694
|
'statistical' attempts to build a statistical mask, possibly by
|
|
714
695
|
elementwise maximum of statistical maps of children)
|
|
715
|
-
threshold_statistical: float,
|
|
716
|
-
|
|
717
|
-
|
|
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.
|
|
718
699
|
restrict_space: bool, default: False
|
|
719
700
|
If True, it will not try to fetch maps from other spaces and warp
|
|
720
701
|
its boundingbox to requested space.
|
|
@@ -725,16 +706,20 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
725
706
|
"""
|
|
726
707
|
spaceobj = _space.Space.get_instance(space)
|
|
727
708
|
try:
|
|
728
|
-
mask = self.
|
|
709
|
+
mask = self.get_regional_mask(
|
|
729
710
|
spaceobj, maptype=maptype, threshold=threshold_statistical
|
|
730
711
|
)
|
|
731
|
-
return mask.get_boundingbox(
|
|
712
|
+
return mask.get_boundingbox(
|
|
713
|
+
clip=True,
|
|
714
|
+
background=0.0,
|
|
715
|
+
**fetch_kwargs
|
|
716
|
+
)
|
|
732
717
|
except (RuntimeError, ValueError):
|
|
733
718
|
if restrict_space:
|
|
734
719
|
return None
|
|
735
720
|
for other_space in self.parcellation.spaces - spaceobj:
|
|
736
721
|
try:
|
|
737
|
-
mask = self.
|
|
722
|
+
mask = self.get_regional_mask(
|
|
738
723
|
other_space,
|
|
739
724
|
maptype=maptype,
|
|
740
725
|
threshold=threshold_statistical,
|
|
@@ -745,7 +730,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
745
730
|
bbox_warped = bbox.warp(spaceobj)
|
|
746
731
|
except SpaceWarpingFailedError:
|
|
747
732
|
continue
|
|
748
|
-
logger.
|
|
733
|
+
logger.debug(
|
|
749
734
|
f"No bounding box for {self.name} defined in {spaceobj.name}, "
|
|
750
735
|
f"warped the bounding box from {other_space.name} instead."
|
|
751
736
|
)
|
|
@@ -755,7 +740,14 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
755
740
|
logger.error(f"Could not compute bounding box for {self.name}.")
|
|
756
741
|
return None
|
|
757
742
|
|
|
758
|
-
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:
|
|
759
751
|
"""
|
|
760
752
|
Compute the centroids of the region in the given space.
|
|
761
753
|
|
|
@@ -763,20 +755,33 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
763
755
|
----------
|
|
764
756
|
space: Space
|
|
765
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.
|
|
766
765
|
|
|
767
766
|
Returns
|
|
768
767
|
-------
|
|
769
|
-
|
|
770
|
-
Found centroids (as Point objects) in a
|
|
768
|
+
PointCloud
|
|
769
|
+
Found centroids (as Point objects) in a PointCloud
|
|
771
770
|
|
|
772
771
|
Note
|
|
773
772
|
----
|
|
774
773
|
A region can generally have multiple centroids if it has multiple
|
|
775
774
|
connected components in the map.
|
|
776
775
|
"""
|
|
777
|
-
props = self.spatial_props(
|
|
778
|
-
|
|
779
|
-
|
|
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],
|
|
780
785
|
space=space
|
|
781
786
|
)
|
|
782
787
|
|
|
@@ -784,13 +789,13 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
784
789
|
self,
|
|
785
790
|
space: _space.Space,
|
|
786
791
|
maptype: MapType = MapType.LABELLED,
|
|
787
|
-
threshold_statistical=
|
|
788
|
-
|
|
792
|
+
threshold_statistical: float = 0.0,
|
|
793
|
+
split_components: bool = True,
|
|
794
|
+
**fetch_kwargs,
|
|
795
|
+
):
|
|
789
796
|
"""
|
|
790
797
|
Compute spatial properties for connected components of this region in the given space.
|
|
791
798
|
|
|
792
|
-
TODO: this should go to the Volume class and just be called from here.
|
|
793
|
-
|
|
794
799
|
Parameters
|
|
795
800
|
----------
|
|
796
801
|
space: Space
|
|
@@ -799,58 +804,33 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
799
804
|
Type of map to build ('labelled' will result in a binary mask,
|
|
800
805
|
'statistical' attempts to build a statistical mask, possibly by
|
|
801
806
|
elementwise maximum of statistical maps of children)
|
|
802
|
-
threshold_statistical: float,
|
|
807
|
+
threshold_statistical: float, default: 0.0
|
|
803
808
|
if not None, masks will be preferably constructed by thresholding
|
|
804
809
|
statistical maps with the given value.
|
|
805
810
|
|
|
806
811
|
Returns
|
|
807
812
|
-------
|
|
808
|
-
|
|
809
|
-
|
|
813
|
+
List
|
|
814
|
+
List of region's component spatial properties
|
|
810
815
|
"""
|
|
811
|
-
from skimage import measure
|
|
812
|
-
|
|
813
816
|
if not isinstance(space, _space.Space):
|
|
814
817
|
space = _space.Space.get_instance(space)
|
|
815
818
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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(
|
|
820
826
|
f"Spatial properties of {self.name} cannot be computed in {space.name}. "
|
|
821
827
|
"This region is only mapped in these spaces: "
|
|
822
828
|
f"{', '.join(s.name for s in self.supported_spaces)}"
|
|
823
829
|
)
|
|
824
|
-
return result
|
|
825
|
-
|
|
826
|
-
# build binary mask of the image
|
|
827
|
-
pimg = self.get_regional_map(
|
|
828
|
-
space, maptype=maptype, threshold=threshold_statistical
|
|
829
|
-
).fetch()
|
|
830
|
-
|
|
831
|
-
# determine scaling factor from voxels to cube mm
|
|
832
|
-
scale = affine_scaling(pimg.affine)
|
|
833
|
-
|
|
834
|
-
# compute properties of labelled volume
|
|
835
|
-
A = np.asarray(pimg.get_fdata(), dtype=np.int32).squeeze()
|
|
836
|
-
C = measure.label(A)
|
|
837
|
-
|
|
838
|
-
# compute spatial properties of each connected component
|
|
839
|
-
for label in range(1, C.max() + 1):
|
|
840
|
-
nonzero = np.c_[np.nonzero(C == label)]
|
|
841
|
-
result.components.append(
|
|
842
|
-
SpatialPropCmpt(
|
|
843
|
-
centroid=point.Point(
|
|
844
|
-
np.dot(pimg.affine, np.r_[nonzero.mean(0), 1])[:3], space=space
|
|
845
|
-
),
|
|
846
|
-
volume=nonzero.shape[0] * scale,
|
|
847
|
-
)
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
# sort by volume
|
|
851
|
-
result.components.sort(key=lambda cmp: cmp.volume, reverse=True)
|
|
852
830
|
|
|
853
|
-
return
|
|
831
|
+
return region_vol.compute_spatial_props(
|
|
832
|
+
split_components=split_components, **fetch_kwargs
|
|
833
|
+
)
|
|
854
834
|
|
|
855
835
|
def __iter__(self):
|
|
856
836
|
"""
|
|
@@ -864,7 +844,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
864
844
|
|
|
865
845
|
if self.supports_space(other.space):
|
|
866
846
|
try:
|
|
867
|
-
volume = self.
|
|
847
|
+
volume = self.get_regional_mask(other.space)
|
|
868
848
|
if volume is not None:
|
|
869
849
|
return volume.intersection(other)
|
|
870
850
|
except NotImplementedError:
|
|
@@ -874,7 +854,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
874
854
|
for space in self.supported_spaces:
|
|
875
855
|
if space.provides_image:
|
|
876
856
|
try:
|
|
877
|
-
volume = self.
|
|
857
|
+
volume = self.get_regional_mask(space)
|
|
878
858
|
if volume is not None:
|
|
879
859
|
intersection = volume.intersection(other)
|
|
880
860
|
logger.info(f"Warped {other} to {space} to find the intersection.")
|
siibra/core/space.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2024
|
|
2
2
|
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
3
|
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -38,6 +38,7 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
38
38
|
modality: str = "",
|
|
39
39
|
publications: list = [],
|
|
40
40
|
datasets: list = [],
|
|
41
|
+
prerelease: bool = False,
|
|
41
42
|
):
|
|
42
43
|
"""
|
|
43
44
|
Constructs a new parcellation object.
|
|
@@ -75,6 +76,7 @@ class Space(AtlasConcept, configuration_folder="spaces"):
|
|
|
75
76
|
modality=modality,
|
|
76
77
|
publications=publications,
|
|
77
78
|
datasets=datasets,
|
|
79
|
+
prerelease=prerelease,
|
|
78
80
|
)
|
|
79
81
|
self.volumes = volumes
|
|
80
82
|
for v in self.volumes:
|
siibra/core/structure.py
CHANGED
siibra/exceptions.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");
|
|
@@ -45,3 +45,19 @@ class NoVolumeFound(RuntimeError):
|
|
|
45
45
|
|
|
46
46
|
class WarmupRegException(Exception):
|
|
47
47
|
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ZeroVolumeBoundingBox(Exception):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NoneCoordinateSuppliedError(ValueError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NoMapMatchingValues(ValueError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EmptyPointCloudError(ValueError):
|
|
63
|
+
pass
|