siibra 0.4a33__py3-none-any.whl → 0.4a46__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 (64) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +2 -0
  3. siibra/commons.py +53 -8
  4. siibra/configuration/configuration.py +21 -17
  5. siibra/configuration/factory.py +95 -19
  6. siibra/core/atlas.py +11 -8
  7. siibra/core/concept.py +41 -8
  8. siibra/core/parcellation.py +94 -43
  9. siibra/core/region.py +160 -187
  10. siibra/core/space.py +44 -39
  11. siibra/features/__init__.py +19 -19
  12. siibra/features/anchor.py +9 -6
  13. siibra/features/connectivity/__init__.py +0 -8
  14. siibra/features/connectivity/functional_connectivity.py +11 -3
  15. siibra/features/{basetypes → connectivity}/regional_connectivity.py +46 -33
  16. siibra/features/connectivity/streamline_counts.py +3 -2
  17. siibra/features/connectivity/streamline_lengths.py +3 -2
  18. siibra/features/{basetypes → dataset}/__init__.py +2 -0
  19. siibra/features/{external → dataset}/ebrains.py +3 -3
  20. siibra/features/feature.py +420 -0
  21. siibra/{samplers → features/image}/__init__.py +7 -1
  22. siibra/features/{basetypes/volume_of_interest.py → image/image.py} +12 -7
  23. siibra/features/{external/__init__.py → image/sections.py} +8 -5
  24. siibra/features/image/volume_of_interest.py +70 -0
  25. siibra/features/{cellular → tabular}/__init__.py +7 -11
  26. siibra/features/{cellular → tabular}/bigbrain_intensity_profile.py +5 -2
  27. siibra/features/{cellular → tabular}/cell_density_profile.py +6 -2
  28. siibra/features/{basetypes → tabular}/cortical_profile.py +48 -41
  29. siibra/features/{molecular → tabular}/gene_expression.py +5 -2
  30. siibra/features/{cellular → tabular}/layerwise_bigbrain_intensities.py +6 -2
  31. siibra/features/{cellular → tabular}/layerwise_cell_density.py +9 -3
  32. siibra/features/{molecular → tabular}/receptor_density_fingerprint.py +3 -2
  33. siibra/features/{molecular → tabular}/receptor_density_profile.py +6 -2
  34. siibra/features/tabular/regional_timeseries_activity.py +213 -0
  35. siibra/features/{basetypes → tabular}/tabular.py +14 -9
  36. siibra/livequeries/allen.py +1 -1
  37. siibra/livequeries/bigbrain.py +2 -3
  38. siibra/livequeries/ebrains.py +3 -9
  39. siibra/livequeries/query.py +1 -1
  40. siibra/locations/location.py +4 -3
  41. siibra/locations/point.py +21 -17
  42. siibra/locations/pointset.py +2 -2
  43. siibra/retrieval/__init__.py +1 -1
  44. siibra/retrieval/cache.py +8 -2
  45. siibra/retrieval/datasets.py +149 -29
  46. siibra/retrieval/repositories.py +19 -8
  47. siibra/retrieval/requests.py +98 -116
  48. siibra/volumes/gifti.py +26 -11
  49. siibra/volumes/neuroglancer.py +35 -19
  50. siibra/volumes/nifti.py +8 -9
  51. siibra/volumes/parcellationmap.py +341 -184
  52. siibra/volumes/sparsemap.py +67 -53
  53. siibra/volumes/volume.py +25 -13
  54. {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/METADATA +4 -3
  55. siibra-0.4a46.dist-info/RECORD +69 -0
  56. {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/WHEEL +1 -1
  57. siibra/features/basetypes/feature.py +0 -248
  58. siibra/features/fibres/__init__.py +0 -14
  59. siibra/features/functional/__init__.py +0 -14
  60. siibra/features/molecular/__init__.py +0 -26
  61. siibra/samplers/bigbrain.py +0 -181
  62. siibra-0.4a33.dist-info/RECORD +0 -71
  63. {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/LICENSE +0 -0
  64. {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/top_level.txt +0 -0
@@ -16,36 +16,58 @@
16
16
 
17
17
  from . import volume as _volume, nifti
18
18
  from .. import logger, QUIET
19
- from ..commons import MapIndex, MapType, compare_maps, clear_name, create_key, create_gaussian_kernel, Species
19
+ from ..commons import (
20
+ MapIndex,
21
+ MapType,
22
+ compare_maps,
23
+ iterate_connected_components,
24
+ clear_name,
25
+ create_key,
26
+ create_gaussian_kernel,
27
+ siibra_tqdm,
28
+ Species,
29
+ CompareMapsResult
30
+ )
20
31
  from ..core import concept, space, parcellation, region as _region
21
32
  from ..locations import point, pointset
22
33
  from ..retrieval import requests
23
34
 
24
35
  import numpy as np
25
- from tqdm import tqdm
26
- from typing import Union, Dict, List, TYPE_CHECKING, Iterable
36
+ from typing import Union, Dict, List, TYPE_CHECKING, Iterable, Tuple
27
37
  from scipy.ndimage import distance_transform_edt
28
38
  from collections import defaultdict
29
39
  from nibabel import Nifti1Image
30
40
  from nilearn import image
31
41
  import pandas as pd
42
+ from dataclasses import dataclass, asdict
32
43
 
33
44
  if TYPE_CHECKING:
34
45
  from ..core.region import Region
35
46
 
36
47
 
37
- class ExcessiveArgumentException(ValueError):
38
- pass
48
+ class ExcessiveArgumentException(ValueError): pass
39
49
 
40
50
 
41
- class InsufficientArgumentException(ValueError):
42
- pass
51
+ class InsufficientArgumentException(ValueError): pass
43
52
 
44
53
 
45
- class ConflictingArgumentException(ValueError):
46
- pass
54
+ class ConflictingArgumentException(ValueError): pass
47
55
 
48
56
 
57
+ class NonUniqueIndexError(RuntimeError): pass
58
+
59
+ @dataclass
60
+ class Assignment:
61
+ input_structure: int
62
+ centroid: Union[Tuple[np.ndarray], point.Point]
63
+ volume: int
64
+ fragment: str
65
+ map_value: np.ndarray
66
+
67
+
68
+ @dataclass
69
+ class AssignImageResult(CompareMapsResult, Assignment): pass
70
+
49
71
  class Map(concept.AtlasConcept, configuration_folder="maps"):
50
72
 
51
73
  def __init__(
@@ -67,9 +89,9 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
67
89
 
68
90
  Parameters
69
91
  ----------
70
- identifier : str
92
+ identifier: str
71
93
  Unique identifier of the parcellation
72
- name : str
94
+ name: str
73
95
  Human-readable name of the parcellation
74
96
  space_spec: dict
75
97
  Specification of the space (use @id or name fields)
@@ -81,18 +103,18 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
81
103
  Per region name, a list of dictionaries with fields "volume" and "label" is expected,
82
104
  where "volume" points to the index of the Volume object where this region is mapped,
83
105
  and optional "label" is the voxel label for that region.
84
- For contiuous / probability maps, the "label" can be null or omitted.
106
+ For continuous / probability maps, the "label" can be null or omitted.
85
107
  For single-volume labelled maps, the "volume" can be null or omitted.
86
- volumes: list of Volume
108
+ volumes: list[Volume]
87
109
  parcellation volumes
88
- shortname: str
89
- Shortform of human-readable name (optional)
90
- description: str
110
+ shortname: str, optional
111
+ Shortform of human-readable name
112
+ description: str, optional
91
113
  Textual description of the parcellation
92
- modality : str or None
114
+ modality: str, default: None
93
115
  Specification of the modality used for creating the parcellation
94
116
  publications: list
95
- List of ssociated publications, each a dictionary with "doi" and/or "citation" fields
117
+ List of associated publications, each a dictionary with "doi" and/or "citation" fields
96
118
  datasets : list
97
119
  datasets associated with this concept
98
120
  """
@@ -144,6 +166,9 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
144
166
  self._parcellation_spec = parcellation_spec
145
167
  self._affine_cached = None
146
168
  for v in self.volumes:
169
+ # allow the providers to query their parcellation map if needed
170
+ for p in v._providers.values():
171
+ p.parcellation_map = self
147
172
  v._space_spec = space_spec
148
173
 
149
174
  @property
@@ -155,28 +180,53 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
155
180
 
156
181
  def get_index(self, region: Union[str, "Region"]):
157
182
  """
158
- Returns the unique index corresponding to the specified region,
159
- assuming that the specification matches one unique region
160
- defined in this parcellation map.
161
- If not unique, or not defined, an exception will be thrown.
162
- See find_indices() for a less strict search returning all matches.
183
+ Returns the unique index corresponding to the specified region.
184
+
185
+ Tip
186
+ ----
187
+ Use find_indices() method for a less strict search returning all matches.
188
+
189
+ Parameters
190
+ ----------
191
+ region: str or Region
192
+
193
+ Returns
194
+ -------
195
+ MapIndex
196
+
197
+ Raises
198
+ ------
199
+ NonUniqueIndexError
200
+ If not unique or not defined in this parcellation map.
163
201
  """
164
202
  matches = self.find_indices(region)
165
203
  if len(matches) > 1:
166
- raise RuntimeError(
204
+ raise NonUniqueIndexError(
167
205
  f"The specification '{region}' matches multiple mapped "
168
206
  f"structures in {str(self)}: {list(matches.values())}"
169
207
  )
170
208
  elif len(matches) == 0:
171
- raise RuntimeError(
209
+ raise NonUniqueIndexError(
172
210
  f"The specification '{region}' does not match to any structure mapped in {self}"
173
211
  )
174
212
  else:
175
213
  return next(iter(matches))
176
214
 
177
215
  def find_indices(self, region: Union[str, "Region"]):
178
- """ Returns the volume/label indices in this map
179
- which match the given region specification"""
216
+ """
217
+ Returns the volume/label indices in this map which match the given
218
+ region specification.
219
+
220
+ Parameters
221
+ ----------
222
+ region: str or Region
223
+
224
+ Returns
225
+ -------
226
+ dict
227
+ - keys: MapIndex
228
+ - values: region name
229
+ """
180
230
  if region in self._indices:
181
231
  return {
182
232
  idx: region
@@ -186,15 +236,34 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
186
236
  matched_region_names = set(_.name for _ in (self.parcellation.find(regionname)))
187
237
  matches = matched_region_names & self._indices.keys()
188
238
  if len(matches) == 0:
189
- logger.warn(f"Region {regionname} not defined in {self}")
239
+ logger.warning(f"Region {regionname} not defined in {self}")
190
240
  return {
191
241
  idx: regionname
192
242
  for regionname in matches
193
243
  for idx in self._indices[regionname]
194
244
  }
195
245
 
196
- def get_region(self, label: int = None, volume: int = None, index: MapIndex = None):
197
- """ Returns the region mapped by the given index, if any. """
246
+ def get_region(self, label: int = None, volume: int = 0, index: MapIndex = None):
247
+ """
248
+ Returns the region mapped by the given index, if any.
249
+
250
+ Tip
251
+ ----
252
+ Use get_index() or find_indices() methods to obtain the MapIndex.
253
+
254
+ Parameters
255
+ ----------
256
+ label: int, default: None
257
+ volume: int, default: 0
258
+ index: MapIndex, default: None
259
+
260
+ Returns
261
+ -------
262
+ Region
263
+ A region object defined in the parcellation map.
264
+ """
265
+ if isinstance(label, MapIndex) and index is None:
266
+ raise TypeError(f"Specify MapIndex with index keyword.")
198
267
  if index is None:
199
268
  index = MapIndex(volume, label)
200
269
  matches = [
@@ -203,13 +272,13 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
203
272
  if index in indexlist
204
273
  ]
205
274
  if len(matches) == 0:
206
- logger.warn(f"Index {index} not defined in {self}")
275
+ logger.warning(f"Index {index} not defined in {self}")
207
276
  return None
208
277
  elif len(matches) == 1:
209
278
  return self.parcellation.get_region(matches[0])
210
279
  else:
211
280
  # this should not happen, already tested in constructor
212
- raise RuntimeError(f"Index {index} is not unique in {self}")
281
+ raise RuntimeError(f"Index {index} is not unique in {self}")
213
282
 
214
283
  @property
215
284
  def space(self):
@@ -223,7 +292,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
223
292
  for key in ["@id", "name"]:
224
293
  if key in self._parcellation_spec:
225
294
  return parcellation.Parcellation.get_instance(self._parcellation_spec[key])
226
- logger.warn(
295
+ logger.warning(
227
296
  f"Cannot determine parcellation of {self.__class__.__name__} "
228
297
  f"{self.name} from {self._parcellation_spec}"
229
298
  )
@@ -232,8 +301,8 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
232
301
  @property
233
302
  def labels(self):
234
303
  """
235
- The set of all label indices defined in this map,
236
- including "None" if not defined for one or more regions.
304
+ The set of all label indices defined in this map, including "None" if
305
+ not defined for one or more regions.
237
306
  """
238
307
  return {d.label for v in self._indices.values() for d in v}
239
308
 
@@ -265,26 +334,41 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
265
334
  ):
266
335
  """
267
336
  Fetches one particular volume of this parcellation map.
337
+
268
338
  If there's only one volume, this is the default, otherwise further
269
- specication is requested:
339
+ specification is requested:
270
340
  - the volume index,
271
341
  - the MapIndex (which results in a regional map being returned)
272
342
 
273
- You might also consider fetch_iter() to iterate the volumes, or compress()
274
- to produce a single-volume parcellation map.
343
+ You might also consider fetch_iter() to iterate the volumes, or
344
+ compress() to produce a single-volume parcellation map.
275
345
 
276
346
  Parameters
277
347
  ----------
278
- region_or_index: Union[str, Region, MapIndex]
348
+ region_or_index: str, Region, MapIndex
279
349
  Lazy match the specification.
280
350
  index: MapIndex
281
351
  Explicit specification of the map index, typically resulting
282
352
  in a regional map (mask or statistical map) to be returned.
283
353
  Note that supplying 'region' will result in retrieving the map index of that region
284
354
  automatically.
285
- region: Union[str, Region]
355
+ region: str, Region
286
356
  Specification of a region name, resulting in a regional map
287
357
  (mask or statistical map) to be returned.
358
+ **kwargs
359
+ - resolution_mm: resolution in millimeters
360
+ - format: the format of the volume, like "mesh" or "nii"
361
+ - voi: a BoundingBox of interest
362
+
363
+
364
+ Note
365
+ ----
366
+ Not all keyword arguments are supported for volume formats. Format
367
+ is restricted by available formats (check formats property).
368
+
369
+ Returns
370
+ -------
371
+ An image or mesh
288
372
  """
289
373
  try:
290
374
  length = len([arg for arg in [region_or_index, region, index] if arg is not None])
@@ -340,13 +424,14 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
340
424
  print(str(e))
341
425
 
342
426
  if result is None:
343
- raise RuntimeError(f"Error fetching {mapindex} from {self} as {kwargs['format']}.")
427
+ raise RuntimeError(f"Error fetching {mapindex} from {self} as {kwargs.get('format', f'{self.formats}')}.")
344
428
  return result
345
429
 
346
430
  def fetch_iter(self, **kwargs):
347
431
  """
348
432
  Returns an iterator to fetch all mapped volumes sequentially.
349
- All arguments are passed on to func:`~siibra.Map.fetch`
433
+
434
+ All arguments are passed on to function Map.fetch().
350
435
  """
351
436
  fragment = kwargs.pop('fragment') if 'fragment' in kwargs else None
352
437
  return (
@@ -403,9 +488,17 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
403
488
 
404
489
  def compress(self, **kwargs):
405
490
  """
406
- Converts this map into a labelled 3D parcellation map, obtained
407
- by taking the voxelwise maximum across the mapped volumes, and
408
- re-labelling regions sequentially.
491
+ Converts this map into a labelled 3D parcellation map, obtained by
492
+ taking the voxelwise maximum across the mapped volumes and fragments,
493
+ and re-labelling regions sequentially.
494
+
495
+ Paramaters
496
+ ----------
497
+ **kwargs: Takes the fetch arguments of its space's template.
498
+
499
+ Returns
500
+ -------
501
+ parcellationmap.Map
409
502
  """
410
503
  if len(self.volumes) == 1 and (self.fragments is None):
411
504
  raise RuntimeError("The map cannot be merged since there are no multiple volumes or fragments.")
@@ -419,12 +512,12 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
419
512
  next_labelindex = 1
420
513
  region_indices = defaultdict(list)
421
514
 
422
- for volidx in tqdm(
515
+ for volidx in siibra_tqdm(
423
516
  range(len(self.volumes)), total=len(self.volumes), unit='maps',
424
517
  desc=f"Compressing {len(self.volumes)} {self.maptype.name.lower()} volumes into single-volume parcellation",
425
518
  disable=(len(self.volumes) == 1)
426
519
  ):
427
- for frag in tqdm(
520
+ for frag in siibra_tqdm(
428
521
  self.fragments, total=len(self.fragments), unit='maps',
429
522
  desc=f"Compressing {len(self.fragments)} {self.maptype.name.lower()} fragments into single-fragment parcellation",
430
523
  disable=(len(self.fragments) == 1 or self.fragments is None)
@@ -446,7 +539,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
446
539
  mapindex.__setattr__("label", int(label))
447
540
  region = self.get_region(index=mapindex)
448
541
  if region is None:
449
- logger.warn(f"Label index {label} is observed in map volume {self}, but no region is defined for it.")
542
+ logger.warning(f"Label index {label} is observed in map volume {self}, but no region is defined for it.")
450
543
  continue
451
544
  region_indices[region.name].append({"volume": 0, "label": next_labelindex})
452
545
  if label is None:
@@ -474,13 +567,18 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
474
567
  def compute_centroids(self) -> Dict[str, point.Point]:
475
568
  """
476
569
  Compute a dictionary of the centroids of all regions in this map.
570
+
571
+ Returns
572
+ -------
573
+ Dict[str, point.Point]
574
+ Region names as keys and computed centroids as items.
477
575
  """
478
576
  centroids = {}
479
577
  # list of regions sorted by mapindex
480
578
  regions = sorted(self._indices.items(), key=lambda v: min(_.volume for _ in v[1]))
481
579
  current_vol_index = MapIndex(volume=0)
482
580
  maparr = None
483
- for regionname, indexlist in tqdm(regions, unit="regions", desc="Computing centroids"):
581
+ for regionname, indexlist in siibra_tqdm(regions, unit="regions", desc="Computing centroids"):
484
582
  assert len(indexlist) == 1
485
583
  index = indexlist[0]
486
584
  if index.label == 0:
@@ -498,7 +596,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
498
596
  )
499
597
  return centroids
500
598
 
501
- def colorize(self, values: dict):
599
+ def colorize(self, values: dict, **kwargs):
502
600
  """Colorize the map with the provided regional values.
503
601
 
504
602
  Parameters
@@ -512,7 +610,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
512
610
  """
513
611
 
514
612
  result = None
515
- for volidx, vol in enumerate(self.fetch_iter()):
613
+ for volidx, vol in enumerate(self.fetch_iter(**kwargs)):
516
614
  if isinstance(vol, dict):
517
615
  raise NotImplementedError("Map colorization not yet implemented for meshes.")
518
616
  img = np.asanyarray(vol.dataobj)
@@ -537,17 +635,14 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
537
635
  """
538
636
  Generate a matplotlib colormap from known rgb values of label indices.
539
637
 
540
- The probability distribution is approximated from the region mask
541
- based on the squared distance transform.
542
-
543
638
  Parameters
544
639
  ----------
545
- region_specs: An iterable selection of regions
640
+ region_specs: iterable(regions), optional
546
641
  Optional parameter to only color the desired regions.
547
642
 
548
- Return
549
- ------
550
- samples : PointSet in physcial coordinates corresponding to this parcellationmap.
643
+ Returns
644
+ -------
645
+ ListedColormap
551
646
  """
552
647
  from matplotlib.colors import ListedColormap
553
648
  import numpy as np
@@ -583,18 +678,21 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
583
678
  def sample_locations(self, regionspec, numpoints: int):
584
679
  """ Sample 3D locations inside a given region.
585
680
 
586
- The probability distribution is approximated from the region mask
587
- based on the squared distance transform.
681
+ The probability distribution is approximated from the region mask based
682
+ on the squared distance transform.
588
683
 
589
- regionspec: valid region specification
684
+ Parameters
685
+ ----------
686
+ regionspec: Region or str
590
687
  Region to be used
591
688
  numpoints: int
592
689
  Number of samples to draw
593
690
 
594
- Return
595
- ------
596
- samples : PointSet in physcial coordinates corresponding to this parcellationmap.
597
-
691
+ Returns
692
+ -------
693
+ PointSet
694
+ Sample points in physcial coordinates corresponding to this
695
+ parcellationmap
598
696
  """
599
697
  index = self.get_index(regionspec)
600
698
  mask = self.fetch(index=index)
@@ -612,23 +710,14 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
612
710
  XYZ = np.dot(mask.affine, np.c_[XYZ_, np.ones(numpoints)].T)[:3, :].T
613
711
  return pointset.PointSet(XYZ, space=self.space)
614
712
 
615
- @staticmethod
616
- def iterate_connected_components(img: Nifti1Image):
617
- """
618
- Provide an iterator over masks of connected components in the given image.
713
+ def to_sparse(self):
619
714
  """
620
- from skimage import measure
621
- imgdata = np.asanyarray(img.dataobj).squeeze()
622
- components = measure.label(imgdata > 0)
623
- component_labels = np.unique(components)
624
- assert component_labels[0] == 0
625
- return (
626
- (label, Nifti1Image((components == label).astype('uint8'), img.affine))
627
- for label in component_labels[1:]
628
- )
715
+ Creates a SparseMap object from this parcellation map object.
629
716
 
630
- def to_sparse(self):
631
- """ Creates a sparse parcellation map from this object. """
717
+ Returns
718
+ -------
719
+ SparseMap
720
+ """
632
721
  from .sparsemap import SparseMap
633
722
  indices = {
634
723
  regionname: [
@@ -672,6 +761,27 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
672
761
  for pointindex, value
673
762
  in enumerate(np.asanyarray(volimg.dataobj)[x, y, z])
674
763
  ]
764
+
765
+ def _assign(
766
+ self,
767
+ item: Union[point.Point, pointset.PointSet, Nifti1Image],
768
+ minsize_voxel=1,
769
+ lower_threshold=0.0
770
+ ) -> List[Union[Assignment,AssignImageResult]]:
771
+ """
772
+ For internal use only. Returns a dataclass, which provides better static type checking.
773
+ """
774
+
775
+ if isinstance(item, point.Point):
776
+ return self._assign_points(pointset.PointSet([item], item.space, sigma_mm=item.sigma), lower_threshold)
777
+ if isinstance(item, pointset.PointSet):
778
+ return self._assign_points(item, lower_threshold)
779
+ if isinstance(item, Nifti1Image):
780
+ return self._assign_image(item, minsize_voxel, lower_threshold)
781
+
782
+ raise RuntimeError(
783
+ f"Items of type {item.__class__.__name__} cannot be used for region assignment."
784
+ )
675
785
 
676
786
  def assign(
677
787
  self,
@@ -686,99 +796,139 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
686
796
 
687
797
  Parameters
688
798
  ----------
689
- item: Point, PointSet, or Nifti1Image
690
- A spatial object defined in the same physical reference space as this
691
- parcellation map, which could be a point, set of points, or image.
692
- If it is an image, it will be resampled to the same voxel space if its affine
693
- transforation differs from that of the parcellation map.
694
- Resampling will use linear interpolation for float image types,
695
- otherwise nearest neighbor.
799
+ item: Point, PointSet, Nifti1Image
800
+ A spatial object defined in the same physical reference space as
801
+ this parcellation map, which could be a point, set of points, or
802
+ image. If it is an image, it will be resampled to the same voxel
803
+ space if its affine transformation differs from that of the
804
+ parcellation map. Resampling will use linear interpolation for float
805
+ image types, otherwise nearest neighbor.
696
806
  minsize_voxel: int, default: 1
697
807
  Minimum voxel size of image components to be taken into account.
698
808
  lower_threshold: float, default: 0
699
- Lower threshold on values in the statistical map. Values smaller than
700
- this threshold will be excluded from the assignment computation.
701
-
702
- Return
703
- ------
704
- assignments : pandas Dataframe
705
- A table of associated regions and their scores per component found in the input image,
706
- or per coordinate provived.
707
- The scores are:
708
- - Value: Maximum value of the voxels in the map covered by an input coordinate or
709
- input image signal component.
710
- - Pearson correlation coefficient between the brain region map and an input image signal
711
- component (NaN for exact coordinates)
712
- - "Contains": Percentage of the brain region map contained in an input image signal component,
713
- measured from their binarized masks as the ratio between the volume of their interesection
714
- and the volume of the brain region (NaN for exact coordinates)
715
- - "Contained"": Percentage of an input image signal component contained in the brain region map,
716
- measured from their binary masks as the ratio between the volume of their interesection
717
- and the volume of the input image signal component (NaN for exact coordinates)
718
- components: Nifti1Image, or None
719
- If the input was an image, this is a labelled volume mapping the detected components
720
- in the input image, where pixel values correspond to the "component" column of the
721
- assignment table. If the input was a Point or PointSet, this is None.
809
+ Lower threshold on values in the statistical map. Values smaller
810
+ than this threshold will be excluded from the assignment computation.
811
+
812
+ Returns
813
+ -------
814
+ assignments: pandas.DataFrame
815
+ A table of associated regions and their scores per component found
816
+ in the input image, or per coordinate provided. The scores are:
817
+
818
+ - Value: Maximum value of the voxels in the map covered by an
819
+ input coordinate or input image signal component.
820
+ - Pearson correlation coefficient between the brain region map
821
+ and an input image signal component (NaN for exact coordinates)
822
+ - Contains: Percentage of the brain region map contained in an
823
+ input image signal component, measured from their binarized
824
+ masks as the ratio between the volume of their intersection
825
+ and the volume of the brain region (NaN for exact coordinates)
826
+ - Contained: Percentage of an input image signal component
827
+ contained in the brain region map, measured from their binary
828
+ masks as the ratio between the volume of their intersection and
829
+ the volume of the input image signal component (NaN for exact
830
+ coordinates)
831
+ components: Nifti1Image or None
832
+ If the input was an image, this is a labelled volume mapping the
833
+ detected components in the input image, where pixel values correspond
834
+ to the "component" column of the assignment table. If the input was
835
+ a Point or PointSet, returns None.
722
836
  """
723
837
 
724
- if isinstance(item, point.Point):
725
- assignments = self._assign_points(pointset.PointSet([item], item.space, sigma_mm=item.sigma), lower_threshold)
726
- elif isinstance(item, pointset.PointSet):
727
- assignments = self._assign_points(item, lower_threshold)
728
- elif isinstance(item, Nifti1Image):
729
- assignments = self._assign_image(item, minsize_voxel, lower_threshold)
730
- else:
731
- raise RuntimeError(
732
- f"Items of type {item.__class__.__name__} cannot be used for region assignment."
733
- )
838
+ assignments = self._assign(item, minsize_voxel, lower_threshold)
734
839
 
735
840
  # format assignments as pandas dataframe
841
+ columns = [
842
+ "input structure",
843
+ "centroid",
844
+ "volume",
845
+ "fragment",
846
+ "region",
847
+ "correlation",
848
+ "intersection over union",
849
+ "map value",
850
+ "map weighted mean",
851
+ "map containedness",
852
+ "input weighted mean",
853
+ "input containedness"
854
+ ]
736
855
  if len(assignments) == 0:
737
- df = pd.DataFrame(
738
- columns=["Structure", "Centroid", "Volume", "Fragment", "Region", "Value", "Correlation", "IoU", "Contains", "Contained"]
856
+ return pd.DataFrame(columns=columns)
857
+ # determine the unique set of observed indices in order to do region lookups
858
+ # only once for each map index occuring in the point list
859
+ labelled = self.is_labelled # avoid calling this in a loop
860
+ observed_indices = { # unique set of observed map indices. NOTE: len(observed_indices) << len(assignments)
861
+ (
862
+ a.volume,
863
+ a.fragment,
864
+ a.map_value if labelled else None
739
865
  )
740
- else:
741
- result = np.array(assignments, dtype=object)
742
- ind = np.lexsort((-result[:, -1].astype('float'), result[:, 0].astype('float')))
743
-
744
- # determine the unique set of observed indices in order to do region lookups
745
- # only once for each map index occuring in the point list
746
- labelled = self.is_labelled # avoid calling this in a long loop
747
-
748
- def format_label(label):
749
- return int(label) if (label != 'nan') and (labelled) else None
750
-
751
- observed_indices = {
752
- (v, f, format_label(l))
753
- for _, _, v, f, l, _, _, _, _ in result[ind]
754
- }
755
- region_lut = {
756
- (v, f, l): self.get_region(
757
- index=MapIndex(volume=int(v), label=format_label(l), fragment=f)
866
+ for a in assignments
867
+ }
868
+ region_lut = { # lookup table of observed region objects
869
+ (v, f, l): self.get_region(
870
+ index=MapIndex(
871
+ volume=int(v),
872
+ label=l if l is None else int(l),
873
+ fragment=f
758
874
  )
759
- for v, f, l in observed_indices
760
- }
761
- regions = [
762
- region_lut[v, f, format_label(l)] for _, _, v, f, l, _, _, _, _ in result[ind]
763
- ]
764
- df = pd.DataFrame(
765
- {
766
- "Structure": result[ind, 0].astype("int"),
767
- "Centroid": result[ind, 1],
768
- "Volume": result[ind, 2].astype("int"),
769
- "Fragment": result[ind, 3],
770
- "Region": regions,
771
- "Value": result[ind, 4],
772
- "Correlation": result[ind, 8],
773
- "IoU": result[ind, 5],
774
- "Contains": result[ind, 7],
775
- "Contained": result[ind, 6],
776
- }
777
875
  )
876
+ for v, f, l in observed_indices
877
+ }
778
878
 
779
- return df.convert_dtypes() # convert will guess numeric column types
879
+ dataframe_list = []
880
+ for a in assignments:
881
+ item_to_append = {
882
+ "input structure": a.input_structure,
883
+ "centroid": a.centroid,
884
+ "volume": a.volume,
885
+ "fragment": a.fragment,
886
+ "region": region_lut[
887
+ a.volume,
888
+ a.fragment,
889
+ a.map_value if labelled else None
890
+ ],
891
+ }
892
+ # because AssignImageResult is a subclass of Assignment
893
+ # need to check for isinstance AssignImageResult first
894
+ if isinstance(a, AssignImageResult):
895
+ item_to_append = {
896
+ **item_to_append,
897
+ **{
898
+ "correlation": a.correlation,
899
+ "intersection over union": a.intersection_over_union,
900
+ "map value": a.map_value,
901
+ "map weighted mean": a.weighted_mean_of_first,
902
+ "map containedness": a.intersection_over_first,
903
+ "input weighted mean": a.weighted_mean_of_second,
904
+ "input containedness": a.intersection_over_second,
905
+ }
906
+ }
907
+ elif isinstance(a, Assignment):
908
+ item_to_append = {
909
+ **item_to_append,
910
+ **{
911
+ "correlation": None,
912
+ "intersection over union": None,
913
+ "map value": None,
914
+ "map weighted mean": None,
915
+ "map containedness": None,
916
+ "input weighted mean": None,
917
+ "input containedness": None,
918
+ }
919
+ }
920
+ else:
921
+ raise RuntimeError(f"assignments must be of type Assignment or AssignImageResult!")
922
+
923
+ dataframe_list.append(item_to_append)
924
+ df = pd.DataFrame(dataframe_list)
925
+ return (
926
+ df
927
+ .convert_dtypes() # convert will guess numeric column types
928
+ .reindex(columns=columns)
929
+ )
780
930
 
781
- def _assign_points(self, points, lower_threshold: float):
931
+ def _assign_points(self, points:pointset.PointSet, lower_threshold: float) -> List[Assignment]:
782
932
  """
783
933
  assign a PointSet to this parcellation map.
784
934
 
@@ -812,14 +962,20 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
812
962
  if value > lower_threshold:
813
963
  position = pts_warped[pointindex].coordinate
814
964
  assignments.append(
815
- [pointindex, tuple(np.array(position).round(2)), vol, frag, value, np.nan, np.nan, np.nan, np.nan]
965
+ Assignment(
966
+ input_structure=pointindex,
967
+ centroid=tuple(np.array(position).round(2)),
968
+ volume=vol,
969
+ fragment=frag,
970
+ map_value=value
971
+ )
816
972
  )
817
973
  return assignments
818
974
 
819
975
  # if we get here, we need to handle each point independently.
820
976
  # This is much slower but more precise in dealing with the uncertainties
821
977
  # of the coordinates.
822
- for pointindex, pt in tqdm(
978
+ for pointindex, pt in siibra_tqdm(
823
979
  enumerate(points.warp(self.space.id)),
824
980
  total=len(points), desc="Warping points",
825
981
  ):
@@ -833,7 +989,13 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
833
989
  for _, vol, frag, value in values:
834
990
  if value > lower_threshold:
835
991
  assignments.append(
836
- [pointindex, tuple(pt), vol, frag, value, np.nan, np.nan, np.nan, np.nan]
992
+ Assignment(
993
+ input_structure=pointindex,
994
+ centroid=tuple(pt),
995
+ volume=vol,
996
+ fragment=frag,
997
+ map_value=value
998
+ )
837
999
  )
838
1000
  else:
839
1001
  logger.info(
@@ -847,16 +1009,13 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
847
1009
  # build niftiimage with the Gaussian blob,
848
1010
  # then recurse into this method with the image input
849
1011
  W = Nifti1Image(dataobj=kernel, affine=np.dot(self.affine, shift))
850
- T = self.assign(W, lower_threshold=lower_threshold)
851
- assignments.extend(
852
- [
853
- [pointindex, tuple(pt), volume, fragment, value, iou, contained, contains, rho]
854
- for (_, _, volume, fragment, _, value, rho, iou, contains, contained) in T.values
855
- ]
856
- )
1012
+ for entry in self._assign(W, lower_threshold=lower_threshold):
1013
+ entry.input_structure=pointindex
1014
+ entry.centroid=tuple(pt)
1015
+ assignments.append(entry)
857
1016
  return assignments
858
1017
 
859
- def _assign_image(self, queryimg: Nifti1Image, minsize_voxel: int, lower_threshold: float):
1018
+ def _assign_image(self, queryimg: Nifti1Image, minsize_voxel: int, lower_threshold: float) -> List[AssignImageResult]:
860
1019
  """
861
1020
  Assign an image volume to this parcellation map.
862
1021
 
@@ -890,7 +1049,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
890
1049
  # but only if the sequence is long.
891
1050
  seqlen = N or len(it)
892
1051
  return iter(it) if seqlen < min_elements \
893
- else tqdm(it, desc=desc, total=N)
1052
+ else siibra_tqdm(it, desc=desc, total=N)
894
1053
 
895
1054
  with QUIET and _volume.SubvolumeProvider.UseCaching():
896
1055
  for frag in self.fragments or {None}:
@@ -900,7 +1059,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
900
1059
  desc=f"Assigning to {len(self)} volumes"
901
1060
  ):
902
1061
  queryimg_res = resample(queryimg, vol_img.affine, vol_img.shape)
903
- for mode, maskimg in Map.iterate_connected_components(queryimg_res):
1062
+ for mode, maskimg in iterate_connected_components(queryimg_res):
904
1063
  vol_data = np.asanyarray(vol_img.dataobj)
905
1064
  position = np.array(np.where(maskimg.get_fdata())).T.mean(0)
906
1065
  labels = {v.label for L in self._indices.values() for v in L if v.volume == vol}
@@ -911,18 +1070,16 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
911
1070
  targetimg = vol_img if label is None \
912
1071
  else Nifti1Image((vol_data == label).astype('uint8'), vol_img.affine)
913
1072
  scores = compare_maps(maskimg, targetimg)
914
- if scores["IoU"] > 0:
1073
+ if scores.intersection_over_union > 0:
915
1074
  assignments.append(
916
- [
917
- mode,
918
- tuple(position.round(2)),
919
- vol,
920
- frag,
921
- label,
922
- scores["IoU"],
923
- scores["contained"],
924
- scores["contains"],
925
- scores["correlation"]]
1075
+ AssignImageResult(
1076
+ input_structure=mode,
1077
+ centroid=tuple(position.round(2)),
1078
+ volume=vol,
1079
+ fragment=frag,
1080
+ map_value=label,
1081
+ **asdict(scores)
1082
+ )
926
1083
  )
927
1084
 
928
1085
  return assignments