siibra 1.0a19__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 +3 -3
- siibra/commons.py +0 -46
- siibra/configuration/factory.py +10 -20
- siibra/core/atlas.py +20 -14
- siibra/core/parcellation.py +67 -52
- siibra/core/region.py +133 -123
- siibra/exceptions.py +8 -0
- siibra/experimental/contour.py +6 -6
- siibra/experimental/patch.py +2 -2
- siibra/experimental/plane3d.py +8 -8
- siibra/features/anchor.py +12 -13
- siibra/features/connectivity/regional_connectivity.py +2 -2
- siibra/features/feature.py +14 -16
- siibra/features/tabular/bigbrain_intensity_profile.py +1 -1
- siibra/features/tabular/cell_density_profile.py +97 -63
- siibra/features/tabular/layerwise_cell_density.py +3 -22
- siibra/features/tabular/regional_timeseries_activity.py +2 -2
- siibra/livequeries/allen.py +39 -16
- siibra/livequeries/bigbrain.py +8 -8
- siibra/livequeries/query.py +0 -1
- siibra/locations/__init__.py +9 -9
- siibra/locations/boundingbox.py +29 -24
- siibra/locations/point.py +4 -4
- siibra/locations/{pointset.py → pointcloud.py} +30 -22
- siibra/retrieval/repositories.py +9 -26
- siibra/retrieval/requests.py +19 -2
- siibra/volumes/__init__.py +1 -1
- siibra/volumes/parcellationmap.py +88 -81
- siibra/volumes/providers/neuroglancer.py +62 -36
- siibra/volumes/providers/nifti.py +11 -25
- siibra/volumes/sparsemap.py +124 -245
- siibra/volumes/volume.py +141 -52
- {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/METADATA +16 -3
- {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/RECORD +38 -38
- {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
- {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
- {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/top_level.txt +0 -0
siibra/core/region.py
CHANGED
|
@@ -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,12 +26,9 @@ 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
34
|
from typing import List, Union, Iterable, Dict, Callable, Tuple
|
|
@@ -40,6 +37,7 @@ 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"))
|
|
@@ -122,7 +120,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
122
120
|
)
|
|
123
121
|
self._supported_spaces = 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
|
"""
|
|
@@ -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,106 +429,98 @@ 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()]
|
|
442
|
+
|
|
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.
|
|
467
483
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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.
|
|
471
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
|
+
"""
|
|
472
501
|
if isinstance(maptype, str):
|
|
473
502
|
maptype = MapType[maptype.upper()]
|
|
474
503
|
|
|
475
|
-
# prepare space
|
|
504
|
+
# prepare space instance
|
|
476
505
|
if isinstance(space, str):
|
|
477
506
|
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
507
|
|
|
484
508
|
# see if we find a map supporting the requested region
|
|
485
509
|
for m in parcellationmap.Map.registry():
|
|
486
510
|
if (
|
|
487
|
-
m.space.matches(
|
|
511
|
+
m.space.matches(space)
|
|
488
512
|
and m.parcellation == self.parcellation
|
|
489
513
|
and m.provides_image
|
|
490
514
|
and m.maptype == maptype
|
|
491
515
|
and self.name in m.regions
|
|
492
516
|
):
|
|
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}"
|
|
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."
|
|
535
522
|
)
|
|
536
523
|
|
|
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
|
|
541
|
-
|
|
542
524
|
def mapped_in_space(self, space, recurse: bool = True) -> bool:
|
|
543
525
|
"""
|
|
544
526
|
Verifies wether this region is defined by an explicit map in the given space.
|
|
@@ -598,7 +580,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
598
580
|
return len(self.find(other)) > 0
|
|
599
581
|
else:
|
|
600
582
|
try:
|
|
601
|
-
regionmap = self.
|
|
583
|
+
regionmap = self.get_regional_mask(space=other.space)
|
|
602
584
|
return regionmap.__contains__(other)
|
|
603
585
|
except NoMapAvailableError:
|
|
604
586
|
return False
|
|
@@ -627,9 +609,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
627
609
|
return None
|
|
628
610
|
return self._ASSIGNMENT_CACHE[other, self].invert()
|
|
629
611
|
|
|
630
|
-
if isinstance(other, location.Location):
|
|
612
|
+
if isinstance(other, (location.Location, volume.Volume)):
|
|
631
613
|
if self.mapped_in_space(other.space):
|
|
632
|
-
regionmap = self.
|
|
614
|
+
regionmap = self.get_regional_mask(other.space)
|
|
633
615
|
self._ASSIGNMENT_CACHE[self, other] = regionmap.assign(other)
|
|
634
616
|
return self._ASSIGNMENT_CACHE[self, other]
|
|
635
617
|
|
|
@@ -646,7 +628,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
646
628
|
for targetspace in self.supported_spaces:
|
|
647
629
|
try:
|
|
648
630
|
other_warped = other.warp(targetspace)
|
|
649
|
-
regionmap = self.
|
|
631
|
+
regionmap = self.get_regional_mask(targetspace)
|
|
650
632
|
assignment_result = regionmap.assign(other_warped)
|
|
651
633
|
except SpaceWarpingFailedError:
|
|
652
634
|
try:
|
|
@@ -696,10 +678,10 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
696
678
|
self,
|
|
697
679
|
space: _space.Space,
|
|
698
680
|
maptype: MapType = MapType.LABELLED,
|
|
699
|
-
threshold_statistical=
|
|
700
|
-
restrict_space=True,
|
|
681
|
+
threshold_statistical: float = 0.0,
|
|
682
|
+
restrict_space: bool = True,
|
|
701
683
|
**fetch_kwargs
|
|
702
|
-
):
|
|
684
|
+
) -> Union[_boundingbox.BoundingBox, None]:
|
|
703
685
|
"""
|
|
704
686
|
Compute the bounding box of this region in the given space.
|
|
705
687
|
|
|
@@ -711,9 +693,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
711
693
|
Type of map to build ('labelled' will result in a binary mask,
|
|
712
694
|
'statistical' attempts to build a statistical mask, possibly by
|
|
713
695
|
elementwise maximum of statistical maps of children)
|
|
714
|
-
threshold_statistical: float,
|
|
715
|
-
|
|
716
|
-
|
|
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.
|
|
717
699
|
restrict_space: bool, default: False
|
|
718
700
|
If True, it will not try to fetch maps from other spaces and warp
|
|
719
701
|
its boundingbox to requested space.
|
|
@@ -724,16 +706,20 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
724
706
|
"""
|
|
725
707
|
spaceobj = _space.Space.get_instance(space)
|
|
726
708
|
try:
|
|
727
|
-
mask = self.
|
|
709
|
+
mask = self.get_regional_mask(
|
|
728
710
|
spaceobj, maptype=maptype, threshold=threshold_statistical
|
|
729
711
|
)
|
|
730
|
-
return mask.get_boundingbox(
|
|
712
|
+
return mask.get_boundingbox(
|
|
713
|
+
clip=True,
|
|
714
|
+
background=0.0,
|
|
715
|
+
**fetch_kwargs
|
|
716
|
+
)
|
|
731
717
|
except (RuntimeError, ValueError):
|
|
732
718
|
if restrict_space:
|
|
733
719
|
return None
|
|
734
720
|
for other_space in self.parcellation.spaces - spaceobj:
|
|
735
721
|
try:
|
|
736
|
-
mask = self.
|
|
722
|
+
mask = self.get_regional_mask(
|
|
737
723
|
other_space,
|
|
738
724
|
maptype=maptype,
|
|
739
725
|
threshold=threshold_statistical,
|
|
@@ -754,7 +740,14 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
754
740
|
logger.error(f"Could not compute bounding box for {self.name}.")
|
|
755
741
|
return None
|
|
756
742
|
|
|
757
|
-
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:
|
|
758
751
|
"""
|
|
759
752
|
Compute the centroids of the region in the given space.
|
|
760
753
|
|
|
@@ -762,19 +755,32 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
762
755
|
----------
|
|
763
756
|
space: Space
|
|
764
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
765
|
|
|
766
766
|
Returns
|
|
767
767
|
-------
|
|
768
|
-
|
|
769
|
-
Found centroids (as Point objects) in a
|
|
768
|
+
PointCloud
|
|
769
|
+
Found centroids (as Point objects) in a PointCloud
|
|
770
770
|
|
|
771
771
|
Note
|
|
772
772
|
----
|
|
773
773
|
A region can generally have multiple centroids if it has multiple
|
|
774
774
|
connected components in the map.
|
|
775
775
|
"""
|
|
776
|
-
props = self.spatial_props(
|
|
777
|
-
|
|
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(
|
|
778
784
|
[c.centroid for c in props],
|
|
779
785
|
space=space
|
|
780
786
|
)
|
|
@@ -783,7 +789,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
783
789
|
self,
|
|
784
790
|
space: _space.Space,
|
|
785
791
|
maptype: MapType = MapType.LABELLED,
|
|
786
|
-
threshold_statistical=
|
|
792
|
+
threshold_statistical: float = 0.0,
|
|
793
|
+
split_components: bool = True,
|
|
794
|
+
**fetch_kwargs,
|
|
787
795
|
):
|
|
788
796
|
"""
|
|
789
797
|
Compute spatial properties for connected components of this region in the given space.
|
|
@@ -796,21 +804,21 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
796
804
|
Type of map to build ('labelled' will result in a binary mask,
|
|
797
805
|
'statistical' attempts to build a statistical mask, possibly by
|
|
798
806
|
elementwise maximum of statistical maps of children)
|
|
799
|
-
threshold_statistical: float,
|
|
807
|
+
threshold_statistical: float, default: 0.0
|
|
800
808
|
if not None, masks will be preferably constructed by thresholding
|
|
801
809
|
statistical maps with the given value.
|
|
802
810
|
|
|
803
811
|
Returns
|
|
804
812
|
-------
|
|
805
|
-
|
|
806
|
-
|
|
813
|
+
List
|
|
814
|
+
List of region's component spatial properties
|
|
807
815
|
"""
|
|
808
816
|
if not isinstance(space, _space.Space):
|
|
809
817
|
space = _space.Space.get_instance(space)
|
|
810
818
|
|
|
811
819
|
# build binary mask of the image
|
|
812
820
|
try:
|
|
813
|
-
region_vol = self.
|
|
821
|
+
region_vol = self.get_regional_mask(
|
|
814
822
|
space, maptype=maptype, threshold=threshold_statistical
|
|
815
823
|
)
|
|
816
824
|
except NoMapAvailableError:
|
|
@@ -820,7 +828,9 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
820
828
|
f"{', '.join(s.name for s in self.supported_spaces)}"
|
|
821
829
|
)
|
|
822
830
|
|
|
823
|
-
return region_vol.compute_spatial_props(
|
|
831
|
+
return region_vol.compute_spatial_props(
|
|
832
|
+
split_components=split_components, **fetch_kwargs
|
|
833
|
+
)
|
|
824
834
|
|
|
825
835
|
def __iter__(self):
|
|
826
836
|
"""
|
|
@@ -834,7 +844,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
834
844
|
|
|
835
845
|
if self.supports_space(other.space):
|
|
836
846
|
try:
|
|
837
|
-
volume = self.
|
|
847
|
+
volume = self.get_regional_mask(other.space)
|
|
838
848
|
if volume is not None:
|
|
839
849
|
return volume.intersection(other)
|
|
840
850
|
except NotImplementedError:
|
|
@@ -844,7 +854,7 @@ class Region(anytree.NodeMixin, concept.AtlasConcept, structure.BrainStructure):
|
|
|
844
854
|
for space in self.supported_spaces:
|
|
845
855
|
if space.provides_image:
|
|
846
856
|
try:
|
|
847
|
-
volume = self.
|
|
857
|
+
volume = self.get_regional_mask(space)
|
|
848
858
|
if volume is not None:
|
|
849
859
|
intersection = volume.intersection(other)
|
|
850
860
|
logger.info(f"Warped {other} to {space} to find the intersection.")
|
siibra/exceptions.py
CHANGED
siibra/experimental/contour.py
CHANGED
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
from ..locations import point,
|
|
16
|
+
from ..locations import point, pointcloud, boundingbox
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class Contour(
|
|
21
|
+
class Contour(pointcloud.PointCloud):
|
|
22
22
|
"""
|
|
23
|
-
A
|
|
23
|
+
A PointCloud that represents a contour line.
|
|
24
24
|
The only difference is that the point order is relevant,
|
|
25
25
|
and consecutive points are thought as being connected by an edge.
|
|
26
26
|
|
|
27
|
-
In fact,
|
|
27
|
+
In fact, PointCloud assumes order as well, but no connections between points.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
def __init__(self, coordinates, space=None, sigma_mm=0, labels: list = None):
|
|
31
|
-
|
|
31
|
+
pointcloud.PointCloud.__init__(self, coordinates, space, sigma_mm, labels)
|
|
32
32
|
|
|
33
33
|
def crop(self, voi: boundingbox.BoundingBox):
|
|
34
34
|
"""
|
|
@@ -45,7 +45,7 @@ class Contour(pointset.PointSet):
|
|
|
45
45
|
cropped = self.intersection(voi)
|
|
46
46
|
|
|
47
47
|
if cropped is not None and not isinstance(cropped, point.Point):
|
|
48
|
-
assert isinstance(cropped,
|
|
48
|
+
assert isinstance(cropped, pointcloud.PointCloud)
|
|
49
49
|
# Identifiy contour splits are by discontinuouities ("jumps")
|
|
50
50
|
# of their labels, which denote positions in the original contour
|
|
51
51
|
jumps = np.diff([self.labels.index(lb) for lb in cropped.labels])
|
siibra/experimental/patch.py
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
16
|
from ..volumes import volume
|
|
17
|
-
from ..locations import
|
|
17
|
+
from ..locations import pointcloud, boundingbox
|
|
18
18
|
from ..commons import translation_matrix, y_rotation_matrix
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
@@ -24,7 +24,7 @@ from nilearn import image
|
|
|
24
24
|
|
|
25
25
|
class Patch:
|
|
26
26
|
|
|
27
|
-
def __init__(self, corners:
|
|
27
|
+
def __init__(self, corners: pointcloud.PointCloud):
|
|
28
28
|
"""Construct a patch in physical coordinates.
|
|
29
29
|
As of now, only patches aligned in the y plane of the physical space
|
|
30
30
|
are supported."""
|
siibra/experimental/plane3d.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
from . import contour
|
|
17
17
|
from . import patch
|
|
18
|
-
from ..locations import point,
|
|
18
|
+
from ..locations import point, pointcloud
|
|
19
19
|
from ..volumes import volume
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
@@ -93,9 +93,9 @@ class Plane3D:
|
|
|
93
93
|
and an Mx3 array "faces" of face definitions.
|
|
94
94
|
Each row in the face array corresponds to the three indices of vertices making up the
|
|
95
95
|
triangle.
|
|
96
|
-
The result is a list of contour segments, each represented as a
|
|
96
|
+
The result is a list of contour segments, each represented as a PointCloud
|
|
97
97
|
holding the ordered list of contour points.
|
|
98
|
-
The point labels in each "contour"
|
|
98
|
+
The point labels in each "contour" PointCloud hold the index of the face in the
|
|
99
99
|
mesh which made up each contour point.
|
|
100
100
|
"""
|
|
101
101
|
|
|
@@ -185,17 +185,17 @@ class Plane3D:
|
|
|
185
185
|
|
|
186
186
|
return result
|
|
187
187
|
|
|
188
|
-
def project_points(self, points:
|
|
188
|
+
def project_points(self, points: pointcloud.PointCloud):
|
|
189
189
|
"""projects the given points onto the plane."""
|
|
190
190
|
assert self.space == points.space
|
|
191
191
|
XYZ = points.coordinates
|
|
192
192
|
N = XYZ.shape[0]
|
|
193
193
|
dists = np.dot(self._n, XYZ.T) - self._d
|
|
194
|
-
return
|
|
194
|
+
return pointcloud.PointCloud(
|
|
195
195
|
XYZ - np.tile(self._n, (N, 1)) * dists[:, np.newaxis], space=self.space
|
|
196
196
|
)
|
|
197
197
|
|
|
198
|
-
def get_enclosing_patch(self, points:
|
|
198
|
+
def get_enclosing_patch(self, points: pointcloud.PointCloud, margin=[0.5, 0.5]):
|
|
199
199
|
"""
|
|
200
200
|
Computes the enclosing patch in the given plane
|
|
201
201
|
which contains the projections of the given points.
|
|
@@ -225,7 +225,7 @@ class Plane3D:
|
|
|
225
225
|
|
|
226
226
|
m0, m1 = margin
|
|
227
227
|
w = np.linalg.norm(p3 - p2)
|
|
228
|
-
corners =
|
|
228
|
+
corners = pointcloud.PointCloud(
|
|
229
229
|
[
|
|
230
230
|
p1 + (w / 2 + m1) * v2 + m0 * v1,
|
|
231
231
|
p0 + (w / 2 + m1) * v2 - m0 * v1,
|
|
@@ -249,7 +249,7 @@ class Plane3D:
|
|
|
249
249
|
assert isinstance(image, volume.Volume)
|
|
250
250
|
im_lowres = image.fetch(resolution_mm=1)
|
|
251
251
|
plane_dims = np.where(np.argsort(im_lowres.shape) < 2)[0]
|
|
252
|
-
voxels =
|
|
252
|
+
voxels = pointcloud.PointCloud(
|
|
253
253
|
np.vstack(([0, 0, 0], np.identity(3)[plane_dims])), space=None
|
|
254
254
|
)
|
|
255
255
|
points = voxels.transform(im_lowres.affine, space=image.space)
|
siibra/features/anchor.py
CHANGED
|
@@ -19,7 +19,7 @@ from ..commons import Species, logger
|
|
|
19
19
|
from ..core.structure import BrainStructure
|
|
20
20
|
from ..core.assignment import AnatomicalAssignment, Qualification
|
|
21
21
|
from ..locations.location import Location
|
|
22
|
-
from ..core.parcellation import Parcellation
|
|
22
|
+
from ..core.parcellation import Parcellation, find_regions
|
|
23
23
|
from ..core.region import Region
|
|
24
24
|
from ..core.space import Space
|
|
25
25
|
from ..exceptions import SpaceWarpingFailedError
|
|
@@ -126,13 +126,13 @@ class AnatomicalAnchor:
|
|
|
126
126
|
# decode the region specification into a dict of region objects and assignment qualifications
|
|
127
127
|
regions = {
|
|
128
128
|
region: Qualification.EXACT
|
|
129
|
-
for region in
|
|
129
|
+
for region in find_regions(self._regionspec, filter_children=True, find_topmost=False)
|
|
130
130
|
if region.species in self.species
|
|
131
131
|
}
|
|
132
132
|
# add more regions from possible aliases of the region spec
|
|
133
133
|
for alt_species, aliases in self.region_aliases.items():
|
|
134
134
|
for alias_regionspec, qualificationspec in aliases.items():
|
|
135
|
-
for r in
|
|
135
|
+
for r in find_regions(alias_regionspec, filter_children=True, find_topmost=False):
|
|
136
136
|
if r.species != alt_species:
|
|
137
137
|
continue
|
|
138
138
|
if r not in regions:
|
|
@@ -156,18 +156,17 @@ class AnatomicalAnchor:
|
|
|
156
156
|
else:
|
|
157
157
|
return region + separator + location
|
|
158
158
|
|
|
159
|
-
def assign(self, concept: BrainStructure,
|
|
159
|
+
def assign(self, concept: Union[BrainStructure, Space]) -> AnatomicalAssignment:
|
|
160
160
|
"""
|
|
161
161
|
Match this anchor to a query concept. Assignments are cached at runtime,
|
|
162
162
|
so repeated assignment with the same concept will be cheap.
|
|
163
163
|
"""
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return []
|
|
164
|
+
if isinstance(concept, Space):
|
|
165
|
+
if self.location is not None and self.location.space.matches(concept):
|
|
166
|
+
return [AnatomicalAssignment(concept, self.location, Qualification.CONTAINED)]
|
|
167
|
+
else:
|
|
168
|
+
return []
|
|
169
|
+
|
|
171
170
|
if concept not in self._assignments:
|
|
172
171
|
assignments: List[AnatomicalAssignment] = []
|
|
173
172
|
if self.location is not None:
|
|
@@ -184,8 +183,8 @@ class AnatomicalAnchor:
|
|
|
184
183
|
else None
|
|
185
184
|
return self._assignments[concept]
|
|
186
185
|
|
|
187
|
-
def matches(self, concept: BrainStructure,
|
|
188
|
-
return len(self.assign(concept
|
|
186
|
+
def matches(self, concept: Union[BrainStructure, Space]) -> bool:
|
|
187
|
+
return len(self.assign(concept)) > 0
|
|
189
188
|
|
|
190
189
|
def represented_parcellations(self) -> List[Parcellation]:
|
|
191
190
|
"""
|