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.

Files changed (38) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +3 -3
  3. siibra/commons.py +0 -46
  4. siibra/configuration/factory.py +10 -20
  5. siibra/core/atlas.py +20 -14
  6. siibra/core/parcellation.py +67 -52
  7. siibra/core/region.py +133 -123
  8. siibra/exceptions.py +8 -0
  9. siibra/experimental/contour.py +6 -6
  10. siibra/experimental/patch.py +2 -2
  11. siibra/experimental/plane3d.py +8 -8
  12. siibra/features/anchor.py +12 -13
  13. siibra/features/connectivity/regional_connectivity.py +2 -2
  14. siibra/features/feature.py +14 -16
  15. siibra/features/tabular/bigbrain_intensity_profile.py +1 -1
  16. siibra/features/tabular/cell_density_profile.py +97 -63
  17. siibra/features/tabular/layerwise_cell_density.py +3 -22
  18. siibra/features/tabular/regional_timeseries_activity.py +2 -2
  19. siibra/livequeries/allen.py +39 -16
  20. siibra/livequeries/bigbrain.py +8 -8
  21. siibra/livequeries/query.py +0 -1
  22. siibra/locations/__init__.py +9 -9
  23. siibra/locations/boundingbox.py +29 -24
  24. siibra/locations/point.py +4 -4
  25. siibra/locations/{pointset.py → pointcloud.py} +30 -22
  26. siibra/retrieval/repositories.py +9 -26
  27. siibra/retrieval/requests.py +19 -2
  28. siibra/volumes/__init__.py +1 -1
  29. siibra/volumes/parcellationmap.py +88 -81
  30. siibra/volumes/providers/neuroglancer.py +62 -36
  31. siibra/volumes/providers/nifti.py +11 -25
  32. siibra/volumes/sparsemap.py +124 -245
  33. siibra/volumes/volume.py +141 -52
  34. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/METADATA +16 -3
  35. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/RECORD +38 -38
  36. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
  37. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
  38. {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, pointset, boundingbox as _boundingbox
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._CACHED_REGION_SEARCHES = {}
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, int, Region
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
- MEM[key] = (
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 get_regional_map(
416
+ def get_regional_mask(
426
417
  self,
427
418
  space: Union[str, _space.Space],
428
- maptype: MapType = SIIBRA_DEFAULT_MAPTYPE,
429
- threshold: float = SIIBRA_DEFAULT_MAP_THRESHOLD,
430
- via_space: Union[str, _space.Space] = None
431
- ) -> volume.Volume:
419
+ maptype: MapType = MapType.LABELLED,
420
+ threshold: float = 0.0,
421
+ ) -> volume.FilteredVolume:
432
422
  """
433
- 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,
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, optional
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
- via_space: Space or str
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
- # check for a cached object
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
- getmap_hash = hash(f"{self.id}{space}{maptype}{threshold}{via_space}")
469
- if getmap_hash in self._GETMAP_CACHE:
470
- return self._GETMAP_CACHE[getmap_hash]
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 instances
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(fetch_space)
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
- region_img = m.fetch(region=self, format='image')
494
- imgdata = np.asanyarray(region_img.dataobj)
495
- if maptype == MapType.STATISTICAL: # compute thresholded statistical map, default is 0.0
496
- logger.info(f"Thresholding statistical map at {threshold}")
497
- imgdata = (imgdata > threshold).astype('uint8')
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.get_regional_map(space=other.space)
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.get_regional_map(other.space)
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.get_regional_map(targetspace)
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=None,
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, or None
715
- if not None, masks will be preferably constructed by thresholding
716
- 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.
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.get_regional_map(
709
+ mask = self.get_regional_mask(
728
710
  spaceobj, maptype=maptype, threshold=threshold_statistical
729
711
  )
730
- 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
+ )
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.get_regional_map(
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(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:
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
- PointSet
769
- Found centroids (as Point objects) in a PointSet
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(space)
777
- return pointset.PointSet(
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=None,
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, or None
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
- Dict
806
- Dictionary of region's spatial properties
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.get_regional_map(
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.get_regional_map(other.space)
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.get_regional_map(space)
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
@@ -53,3 +53,11 @@ class ZeroVolumeBoundingBox(Exception):
53
53
 
54
54
  class NoneCoordinateSuppliedError(ValueError):
55
55
  pass
56
+
57
+
58
+ class NoMapMatchingValues(ValueError):
59
+ pass
60
+
61
+
62
+ class EmptyPointCloudError(ValueError):
63
+ pass
@@ -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, pointset, boundingbox
16
+ from ..locations import point, pointcloud, boundingbox
17
17
 
18
18
  import numpy as np
19
19
 
20
20
 
21
- class Contour(pointset.PointSet):
21
+ class Contour(pointcloud.PointCloud):
22
22
  """
23
- A PointSet that represents a contour line.
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, PointSet assumes order as well, but no connections between points.
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
- pointset.PointSet.__init__(self, coordinates, space, sigma_mm, labels)
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, pointset.PointSet)
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])
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
 
16
16
  from ..volumes import volume
17
- from ..locations import pointset, boundingbox
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: pointset.PointSet):
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."""
@@ -15,7 +15,7 @@
15
15
 
16
16
  from . import contour
17
17
  from . import patch
18
- from ..locations import point, pointset
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 PointSet
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" PointSet hold the index of the face in the
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: pointset.PointSet):
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 pointset.PointSet(
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: pointset.PointSet, margin=[0.5, 0.5]):
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 = pointset.PointSet(
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 = pointset.PointSet(
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 Parcellation.find_regions(self._regionspec)
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 Parcellation.find_regions(alias_regionspec):
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, restrict_space: bool = False) -> AnatomicalAssignment:
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
- restrict_space
166
- and self.location is not None
167
- and isinstance(concept, Location)
168
- and not self.location.space.matches(concept.space)
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, restrict_space: bool = False) -> bool:
188
- return len(self.assign(concept, restrict_space)) > 0
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
  """