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.

Files changed (80) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +15 -5
  3. siibra/commons.py +3 -48
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +1 -1
  6. siibra/configuration/factory.py +164 -127
  7. siibra/core/__init__.py +1 -1
  8. siibra/core/assignment.py +1 -1
  9. siibra/core/atlas.py +24 -17
  10. siibra/core/concept.py +18 -9
  11. siibra/core/parcellation.py +76 -55
  12. siibra/core/region.py +163 -183
  13. siibra/core/space.py +3 -1
  14. siibra/core/structure.py +1 -2
  15. siibra/exceptions.py +17 -1
  16. siibra/experimental/contour.py +6 -6
  17. siibra/experimental/patch.py +2 -2
  18. siibra/experimental/plane3d.py +8 -8
  19. siibra/explorer/__init__.py +1 -1
  20. siibra/explorer/url.py +15 -0
  21. siibra/explorer/util.py +1 -1
  22. siibra/features/__init__.py +1 -1
  23. siibra/features/anchor.py +13 -14
  24. siibra/features/connectivity/__init__.py +1 -1
  25. siibra/features/connectivity/functional_connectivity.py +1 -1
  26. siibra/features/connectivity/regional_connectivity.py +7 -5
  27. siibra/features/connectivity/streamline_counts.py +1 -1
  28. siibra/features/connectivity/streamline_lengths.py +1 -1
  29. siibra/features/connectivity/tracing_connectivity.py +1 -1
  30. siibra/features/dataset/__init__.py +1 -1
  31. siibra/features/dataset/ebrains.py +1 -1
  32. siibra/features/feature.py +50 -28
  33. siibra/features/image/__init__.py +1 -1
  34. siibra/features/image/image.py +18 -13
  35. siibra/features/image/sections.py +1 -1
  36. siibra/features/image/volume_of_interest.py +1 -1
  37. siibra/features/tabular/__init__.py +1 -1
  38. siibra/features/tabular/bigbrain_intensity_profile.py +2 -2
  39. siibra/features/tabular/cell_density_profile.py +102 -66
  40. siibra/features/tabular/cortical_profile.py +5 -3
  41. siibra/features/tabular/gene_expression.py +1 -1
  42. siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
  43. siibra/features/tabular/layerwise_cell_density.py +8 -25
  44. siibra/features/tabular/receptor_density_fingerprint.py +5 -3
  45. siibra/features/tabular/receptor_density_profile.py +5 -3
  46. siibra/features/tabular/regional_timeseries_activity.py +7 -5
  47. siibra/features/tabular/tabular.py +5 -3
  48. siibra/livequeries/__init__.py +1 -1
  49. siibra/livequeries/allen.py +46 -20
  50. siibra/livequeries/bigbrain.py +9 -9
  51. siibra/livequeries/ebrains.py +1 -1
  52. siibra/livequeries/query.py +1 -2
  53. siibra/locations/__init__.py +10 -10
  54. siibra/locations/boundingbox.py +77 -38
  55. siibra/locations/location.py +12 -4
  56. siibra/locations/point.py +14 -9
  57. siibra/locations/{pointset.py → pointcloud.py} +69 -27
  58. siibra/retrieval/__init__.py +1 -1
  59. siibra/retrieval/cache.py +1 -1
  60. siibra/retrieval/datasets.py +1 -1
  61. siibra/retrieval/exceptions/__init__.py +1 -1
  62. siibra/retrieval/repositories.py +10 -27
  63. siibra/retrieval/requests.py +20 -3
  64. siibra/vocabularies/__init__.py +1 -1
  65. siibra/volumes/__init__.py +2 -2
  66. siibra/volumes/parcellationmap.py +121 -94
  67. siibra/volumes/providers/__init__.py +1 -1
  68. siibra/volumes/providers/freesurfer.py +1 -1
  69. siibra/volumes/providers/gifti.py +1 -1
  70. siibra/volumes/providers/neuroglancer.py +68 -42
  71. siibra/volumes/providers/nifti.py +18 -28
  72. siibra/volumes/providers/provider.py +2 -2
  73. siibra/volumes/sparsemap.py +128 -247
  74. siibra/volumes/volume.py +252 -65
  75. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/METADATA +17 -4
  76. siibra-1.0.1a0.dist-info/RECORD +84 -0
  77. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
  78. siibra-1.0a14.dist-info/RECORD +0 -84
  79. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
  80. {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-2023
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, point, pointset
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._CACHED_REGION_SEARCHES = {}
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, int, Region
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
- MEM[key] = (
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 get_regional_map(
416
+ def get_regional_mask(
439
417
  self,
440
418
  space: Union[str, _space.Space],
441
- maptype: MapType = SIIBRA_DEFAULT_MAPTYPE,
442
- threshold: float = SIIBRA_DEFAULT_MAP_THRESHOLD,
443
- via_space: Union[str, _space.Space] = None
444
- ) -> volume.Volume:
419
+ maptype: MapType = MapType.LABELLED,
420
+ threshold: float = 0.0,
421
+ ) -> volume.FilteredVolume:
445
422
  """
446
- Attempts to build a binary mask of this region in the given space,
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, optional
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
- via_space: Space or str
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
- # check for a cached object
440
+ if isinstance(maptype, str):
441
+ maptype = MapType[maptype.upper()]
480
442
 
481
- getmap_hash = hash(f"{self.id}{space}{maptype}{threshold}{via_space}")
482
- if getmap_hash in self._GETMAP_CACHE:
483
- return self._GETMAP_CACHE[getmap_hash]
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 instances
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(fetch_space)
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
- region_img = m.fetch(region=self, format='image')
507
- imgdata = np.asanyarray(region_img.dataobj)
508
- if maptype == MapType.STATISTICAL: # compute thresholded statistical map, default is 0.0
509
- logger.info(f"Thresholding statistical map at {threshold}")
510
- imgdata = (imgdata > threshold).astype('uint8')
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.get_regional_map(space=other.space)
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.get_regional_map(other.space)
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 space in self.supported_spaces:
628
+ for targetspace in self.supported_spaces:
651
629
  try:
652
- other_warped = other.warp(space)
653
- regionmap = self.get_regional_map(space)
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
- regionbbox_warped = self.get_boundingbox(
658
- space, restrict_space=True
659
- ).warp(other.space)
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=None,
701
- restrict_space=False,
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, or None
716
- if not None, masks will be preferably constructed by thresholding
717
- statistical maps with the given value.
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.get_regional_map(
709
+ mask = self.get_regional_mask(
729
710
  spaceobj, maptype=maptype, threshold=threshold_statistical
730
711
  )
731
- return mask.get_boundingbox(clip=True, background=0.0, **fetch_kwargs)
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.get_regional_map(
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.warning(
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(self, space: _space.Space) -> pointset.PointSet:
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
- PointSet
770
- Found centroids (as Point objects) in a PointSet
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(space)
778
- return pointset.PointSet(
779
- [c.centroid for c in props.components],
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=None,
788
- ) -> SpatialProp:
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, or None
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
- Dict
809
- Dictionary of region's spatial properties
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
- result = SpatialProp(space=space)
817
-
818
- if not self.mapped_in_space(space):
819
- logger.warning(
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 result
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.get_regional_map(other.space)
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.get_regional_map(space)
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-2021
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
@@ -1,5 +1,4 @@
1
-
2
- # Copyright 2018-2023
1
+ # Copyright 2018-2024
3
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
4
3
 
5
4
  # Licensed under the Apache License, Version 2.0 (the "License");
siibra/exceptions.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
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