siibra 1.0a19__py3-none-any.whl → 1.0.1a1__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 (82) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +7 -7
  3. siibra/commons.py +8 -53
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +1 -1
  6. siibra/configuration/factory.py +11 -21
  7. siibra/core/__init__.py +1 -1
  8. siibra/core/assignment.py +1 -1
  9. siibra/core/atlas.py +21 -15
  10. siibra/core/concept.py +3 -3
  11. siibra/core/parcellation.py +69 -54
  12. siibra/core/region.py +178 -158
  13. siibra/core/space.py +1 -1
  14. siibra/core/structure.py +2 -2
  15. siibra/exceptions.py +13 -1
  16. siibra/experimental/__init__.py +1 -1
  17. siibra/experimental/contour.py +8 -8
  18. siibra/experimental/cortical_profile_sampler.py +1 -1
  19. siibra/experimental/patch.py +3 -3
  20. siibra/experimental/plane3d.py +12 -12
  21. siibra/explorer/__init__.py +1 -1
  22. siibra/explorer/url.py +2 -2
  23. siibra/explorer/util.py +1 -1
  24. siibra/features/__init__.py +1 -1
  25. siibra/features/anchor.py +14 -15
  26. siibra/features/connectivity/__init__.py +1 -1
  27. siibra/features/connectivity/functional_connectivity.py +1 -1
  28. siibra/features/connectivity/regional_connectivity.py +4 -4
  29. siibra/features/connectivity/streamline_counts.py +1 -1
  30. siibra/features/connectivity/streamline_lengths.py +1 -1
  31. siibra/features/connectivity/tracing_connectivity.py +1 -1
  32. siibra/features/dataset/__init__.py +1 -1
  33. siibra/features/dataset/ebrains.py +1 -1
  34. siibra/features/feature.py +24 -26
  35. siibra/features/image/__init__.py +1 -1
  36. siibra/features/image/image.py +2 -2
  37. siibra/features/image/sections.py +1 -1
  38. siibra/features/image/volume_of_interest.py +1 -1
  39. siibra/features/tabular/__init__.py +1 -1
  40. siibra/features/tabular/bigbrain_intensity_profile.py +2 -2
  41. siibra/features/tabular/cell_density_profile.py +98 -64
  42. siibra/features/tabular/cortical_profile.py +3 -3
  43. siibra/features/tabular/gene_expression.py +1 -1
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
  45. siibra/features/tabular/layerwise_cell_density.py +4 -23
  46. siibra/features/tabular/receptor_density_fingerprint.py +13 -10
  47. siibra/features/tabular/receptor_density_profile.py +1 -1
  48. siibra/features/tabular/regional_timeseries_activity.py +4 -4
  49. siibra/features/tabular/tabular.py +7 -5
  50. siibra/livequeries/__init__.py +1 -1
  51. siibra/livequeries/allen.py +42 -19
  52. siibra/livequeries/bigbrain.py +21 -12
  53. siibra/livequeries/ebrains.py +1 -1
  54. siibra/livequeries/query.py +2 -3
  55. siibra/locations/__init__.py +11 -11
  56. siibra/locations/boundingbox.py +30 -29
  57. siibra/locations/location.py +1 -1
  58. siibra/locations/point.py +7 -7
  59. siibra/locations/{pointset.py → pointcloud.py} +36 -33
  60. siibra/retrieval/__init__.py +1 -1
  61. siibra/retrieval/cache.py +1 -1
  62. siibra/retrieval/datasets.py +4 -4
  63. siibra/retrieval/exceptions/__init__.py +1 -1
  64. siibra/retrieval/repositories.py +13 -30
  65. siibra/retrieval/requests.py +25 -8
  66. siibra/vocabularies/__init__.py +1 -1
  67. siibra/volumes/__init__.py +2 -2
  68. siibra/volumes/parcellationmap.py +119 -91
  69. siibra/volumes/providers/__init__.py +1 -1
  70. siibra/volumes/providers/freesurfer.py +3 -3
  71. siibra/volumes/providers/gifti.py +1 -1
  72. siibra/volumes/providers/neuroglancer.py +67 -41
  73. siibra/volumes/providers/nifti.py +12 -26
  74. siibra/volumes/providers/provider.py +1 -1
  75. siibra/volumes/sparsemap.py +125 -246
  76. siibra/volumes/volume.py +150 -61
  77. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/METADATA +26 -4
  78. siibra-1.0.1a1.dist-info/RECORD +84 -0
  79. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/WHEEL +1 -1
  80. siibra-1.0a19.dist-info/RECORD +0 -84
  81. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/LICENSE +0 -0
  82. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -19,7 +19,8 @@ from .requests import (
19
19
  EbrainsRequest,
20
20
  SiibraHttpRequestError,
21
21
  find_suitiable_decoder,
22
- DECODERS
22
+ DECODERS,
23
+ FileLoader
23
24
  )
24
25
  from .cache import CACHE
25
26
 
@@ -124,34 +125,16 @@ class LocalFileRepository(RepositoryConnector):
124
125
  self._folder = pathlib.Path(folder)
125
126
  assert pathlib.Path.is_dir(self._folder)
126
127
 
127
- def _build_url(self, folder: str, filename: str):
128
- return pathlib.Path.joinpath(self._folder, folder, filename)
129
-
130
- class FileLoader:
131
- """
132
- Just a loads a local file, but mimics the behaviour
133
- of cached http requests used in other connectors.
134
- """
135
- def __init__(self, file_url, decode_func):
136
- self.url = file_url
137
- self.func = decode_func
138
- self.cached = True
139
-
140
- @property
141
- def data(self):
142
- with open(self.url, 'rb') as f:
143
- return self.func(f.read())
128
+ def _build_url(self, folder: str, filename: str) -> str:
129
+ return pathlib.Path.joinpath(self._folder, folder, filename).as_posix()
144
130
 
145
131
  def get_loader(self, filename, folder="", decode_func=None):
146
132
  """Get a lazy loader for a file, for loading data
147
133
  only once loader.data is accessed."""
148
- url = self._build_url(folder, filename)
149
- if url is None:
150
- raise RuntimeError(f"Cannot build url for ({folder}, {filename})")
151
- if decode_func is None:
152
- return self.FileLoader(url, lambda b: self._decode_response(b, filename))
153
- else:
154
- return self.FileLoader(url, decode_func)
134
+ filepath = self._build_url(folder, filename)
135
+ if not pathlib.Path(filepath).is_file():
136
+ raise RuntimeError(f"No file is found in {filepath}")
137
+ return FileLoader(filepath, decode_func)
155
138
 
156
139
  def search_files(self, folder="", suffix=None, recursive=False):
157
140
  results = []
@@ -165,7 +148,7 @@ class LocalFileRepository(RepositoryConnector):
165
148
  def __str__(self):
166
149
  return f"{self.__class__.__name__} at {self._folder}"
167
150
 
168
- def __eq__(self, other):
151
+ def __eq__(self, other: "LocalFileRepository"):
169
152
  return self._folder == other._folder
170
153
 
171
154
 
@@ -213,7 +196,7 @@ class GithubConnector(RepositoryConnector):
213
196
  if len(matched_reftags) == 1:
214
197
  self._want_commit_cached = matched_reftags[0]["commit"]
215
198
  else:
216
- raise RuntimeError(f"Found {len(matched_reftags)} mathces to {reftag}")
199
+ raise RuntimeError(f"Found {len(matched_reftags)} matches to {reftag}")
217
200
  self._tag_checked = True
218
201
  except Exception:
219
202
  logger.warning("Could not connect to GitHub repository.", exc_info=1)
@@ -275,7 +258,7 @@ class GitlabConnector(RepositoryConnector):
275
258
  n.b. only archive_mode should only be set for trusted domains. Extraction of archive can result in files created outside the path
276
259
  see https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
277
260
  """
278
- # TODO: the query builder needs to check wether the reftag is a branch, and then not cache.
261
+ # TODO: the query builder needs to check whether the reftag is a branch, and then not cache.
279
262
  assert server.startswith("http")
280
263
  RepositoryConnector.__init__(
281
264
  self, base_url=f"{server}/api/v4/projects/{project}/repository"
@@ -601,7 +584,7 @@ class EbrainsPublicDatasetConnector(RepositoryConnector):
601
584
  Part of dataset title as an alternative dataset specification (will ignore dataset_id then)
602
585
  in_progress: bool (default:False)
603
586
  If true, will request datasets that are still under curation.
604
- Will only work when autenticated with an appropriately privileged
587
+ Will only work when authenticated with an appropriately privileged
605
588
  user account.
606
589
  """
607
590
  self.dataset_id = dataset_id
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -94,8 +94,8 @@ DECODERS = {
94
94
  def find_suitiable_decoder(url: str) -> Callable:
95
95
  """
96
96
  By supplying a url or a filename, obtain a suitable decoder function
97
- for siibra to digest based on predifined DECODERS. An extra layer of
98
- gzip decompresser automatically added for gzipped files.
97
+ for siibra to digest based on predefined DECODERS. An extra layer of
98
+ gzip decompressor automatically added for gzipped files.
99
99
 
100
100
  Parameters
101
101
  ----------
@@ -117,8 +117,7 @@ def find_suitiable_decoder(url: str) -> Callable:
117
117
  suitable_decoders = [
118
118
  dec for sfx, dec in DECODERS.items() if urlpath.endswith(sfx)
119
119
  ]
120
- if len(suitable_decoders) > 0:
121
- assert len(suitable_decoders) == 1
120
+ if len(suitable_decoders) == 1:
122
121
  return suitable_decoders[0]
123
122
  else:
124
123
  return None
@@ -194,7 +193,7 @@ class HttpRequest:
194
193
  """
195
194
  Populates the file cache with the data from http if required.
196
195
  noop if 1/ data is already cached and 2/ refresh flag not set
197
- The caller should load the cachefile after _retrieve successfuly executes
196
+ The caller should load the cachefile after _retrieve successfully executes
198
197
  """
199
198
  if self.cached and not self.refresh:
200
199
  return
@@ -270,6 +269,24 @@ class HttpRequest:
270
269
  return self.get()
271
270
 
272
271
 
272
+ class FileLoader(HttpRequest):
273
+ """
274
+ Just a loads a local file, but mimics the behaviour
275
+ of cached http requests used in other connectors.
276
+ """
277
+ def __init__(self, filepath, func=None):
278
+ HttpRequest.__init__(
279
+ self, filepath, refresh=False,
280
+ func=func or find_suitiable_decoder(filepath)
281
+ )
282
+ self.cachefile = filepath
283
+
284
+ def _retrieve(self, **kwargs):
285
+ if kwargs:
286
+ logger.info(f"Keywords {list(kwargs.keys())} are supplied but won't be used.")
287
+ assert os.path.isfile(self.cachefile)
288
+
289
+
273
290
  class ZipfileRequest(HttpRequest):
274
291
  def __init__(self, url, filename, func=None, refresh=False):
275
292
  HttpRequest.__init__(
@@ -440,9 +457,9 @@ class EbrainsRequest(HttpRequest):
440
457
 
441
458
  if resp.status_code == 200:
442
459
  json_resp = resp.json()
443
- logger.debug("Device flow sucessful:", json_resp)
460
+ logger.debug("Device flow successful:", json_resp)
444
461
  cls._KG_API_TOKEN = json_resp.get("access_token")
445
- print("ebrains token successfuly set.")
462
+ print("ebrains token successfully set.")
446
463
  break
447
464
 
448
465
  if resp.status_code == 400:
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -16,7 +16,7 @@
16
16
 
17
17
  from .parcellationmap import Map
18
18
  from .providers import provider
19
- from .volume import from_array, from_file, from_pointset, from_nifti, Volume
19
+ from .volume import from_array, from_file, from_pointcloud, from_nifti, Volume
20
20
 
21
21
  from ..commons import logger
22
22
  from typing import List, Union
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -32,8 +32,7 @@ from ..commons import (
32
32
  generate_uuid
33
33
  )
34
34
  from ..core import concept, space, parcellation, region as _region
35
- from ..locations import location, point, pointset
36
- from ..retrieval import requests
35
+ from ..locations import location, point, pointcloud
37
36
 
38
37
  import numpy as np
39
38
  from typing import Union, Dict, List, TYPE_CHECKING, Iterable, Tuple
@@ -160,6 +159,11 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
160
159
  logger.warning(f"Non unique indices encountered in {self}: {duplicates}")
161
160
  self._affine_cached = None
162
161
 
162
+ @property
163
+ def key(self):
164
+ _id = self.id
165
+ return create_key(_id[len("siibra-map-v0.0.1"):])
166
+
163
167
  @property
164
168
  def species(self) -> Species:
165
169
  # lazy implementation
@@ -319,69 +323,27 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
319
323
  def regions(self):
320
324
  return list(self._indices)
321
325
 
322
- def fetch(
326
+ def get_volume(
323
327
  self,
324
- region_or_index: Union[str, "Region", MapIndex] = None,
328
+ region: Union[str, "Region"] = None,
325
329
  *,
326
330
  index: MapIndex = None,
327
- region: Union[str, "Region"] = None,
328
- **kwargs
329
- ):
330
- """
331
- Fetches one particular volume of this parcellation map.
332
-
333
- If there's only one volume, this is the default, otherwise further
334
- specification is requested:
335
- - the volume index,
336
- - the MapIndex (which results in a regional map being returned)
337
-
338
- You might also consider fetch_iter() to iterate the volumes, or
339
- compress() to produce a single-volume parcellation map.
340
-
341
- Parameters
342
- ----------
343
- region_or_index: str, Region, MapIndex
344
- Lazy match the specification.
345
- index: MapIndex
346
- Explicit specification of the map index, typically resulting
347
- in a regional map (mask or statistical map) to be returned.
348
- Note that supplying 'region' will result in retrieving the map index of that region
349
- automatically.
350
- region: str, Region
351
- Specification of a region name, resulting in a regional map
352
- (mask or statistical map) to be returned.
353
- **kwargs
354
- - resolution_mm: resolution in millimeters
355
- - format: the format of the volume, like "mesh" or "nii"
356
- - voi: a BoundingBox of interest
357
-
358
-
359
- Note
360
- ----
361
- Not all keyword arguments are supported for volume formats. Format
362
- is restricted by available formats (check formats property).
363
-
364
- Returns
365
- -------
366
- An image or mesh
367
- """
331
+ **kwargs,
332
+ ) -> Union[_volume.Volume, _volume.FilteredVolume, _volume.Subvolume]:
368
333
  try:
369
- length = len([arg for arg in [region_or_index, region, index] if arg is not None])
334
+ length = len([arg for arg in [region, index] if arg is not None])
370
335
  assert length == 1
371
336
  except AssertionError:
372
337
  if length > 1:
373
- raise exceptions.ExcessiveArgumentException("One and only one of region_or_index, region, index can be defined for fetch")
374
- # user can provide no arguments, which assumes one and only one volume present
375
-
376
- if isinstance(region_or_index, MapIndex):
377
- index = region_or_index
378
-
379
- if isinstance(region_or_index, (str, _region.Region)):
380
- region = region_or_index
381
-
338
+ raise exceptions.ExcessiveArgumentException(
339
+ "One and only one of region or index can be defined for `get_volume`."
340
+ )
382
341
  mapindex = None
383
342
  if region is not None:
384
- assert isinstance(region, (str, _region.Region))
343
+ try:
344
+ assert isinstance(region, (str, _region.Region))
345
+ except AssertionError:
346
+ raise TypeError(f"Please provide a region name or region instance, not a {type(region)}")
385
347
  mapindex = self.get_index(region)
386
348
  if index is not None:
387
349
  assert isinstance(index, MapIndex)
@@ -390,19 +352,17 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
390
352
  if len(self) == 1:
391
353
  mapindex = MapIndex(volume=0, label=None)
392
354
  elif len(self) > 1:
355
+ assert self.maptype == MapType.LABELLED, f"Cannot merge multiple volumes of map type {self.maptype}. Please specify a region or index."
393
356
  logger.info(
394
357
  "Map provides multiple volumes and no specification is"
395
358
  " provided. Resampling all volumes to the space."
396
359
  )
397
360
  labels = list(range(len(self.volumes)))
398
361
  merged_volume = _volume.merge(self.volumes, labels, **kwargs)
399
- return merged_volume.fetch()
362
+ return merged_volume
400
363
  else:
401
364
  raise exceptions.NoVolumeFound("Map provides no volumes.")
402
365
 
403
- if "resolution_mm" in kwargs and kwargs.get("format") is None:
404
- kwargs["format"] = 'neuroglancer/precomputed'
405
-
406
366
  kwargs_fragment = kwargs.pop("fragment", None)
407
367
  if kwargs_fragment is not None:
408
368
  if (mapindex.fragment is not None) and (kwargs_fragment != mapindex.fragment):
@@ -418,17 +378,60 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
418
378
  raise IndexError(
419
379
  f"{self} provides {len(self)} mapped volumes, but #{mapindex.volume} was requested."
420
380
  )
421
- try:
422
- result = self.volumes[mapindex.volume or 0].fetch(
423
- fragment=mapindex.fragment, label=mapindex.label, **kwargs
424
- )
425
- except requests.SiibraHttpRequestError as e:
426
- print(str(e))
381
+ if mapindex.label is None and mapindex.fragment is None:
382
+ return self.volumes[mapindex.volume]
383
+
384
+ return _volume.FilteredVolume(
385
+ parent_volume=self.volumes[mapindex.volume],
386
+ label=mapindex.label,
387
+ fragment=mapindex.fragment,
388
+ )
389
+
390
+ def fetch(
391
+ self,
392
+ region: Union[str, "Region"] = None,
393
+ *,
394
+ index: MapIndex = None,
395
+ **fetch_kwargs
396
+ ):
397
+ """
398
+ Fetches one particular volume of this parcellation map.
399
+
400
+ If there's only one volume, this is the default, otherwise further
401
+ specification is requested:
402
+ - the volume index,
403
+ - the MapIndex (which results in a regional map being returned)
404
+
405
+ You might also consider fetch_iter() to iterate the volumes, or
406
+ compress() to produce a single-volume parcellation map.
407
+
408
+ Parameters
409
+ ----------
410
+ region: str, Region
411
+ Specification of a region name, resulting in a regional map
412
+ (mask or statistical map) to be returned.
413
+ index: MapIndex
414
+ Explicit specification of the map index, typically resulting
415
+ in a regional map (mask or statistical map) to be returned.
416
+ Note that supplying 'region' will result in retrieving the map index of that region
417
+ automatically.
418
+ **fetch_kwargs
419
+ - resolution_mm: resolution in millimeters
420
+ - format: the format of the volume, like "mesh" or "nii"
421
+ - voi: a BoundingBox of interest
422
+
427
423
 
428
- if result is None:
429
- raise RuntimeError(f"Error fetching {mapindex} from {self} as {kwargs.get('format', f'{self.formats}')}.")
424
+ Note
425
+ ----
426
+ Not all keyword arguments are supported for volume formats. Format
427
+ is restricted by available formats (check formats property).
430
428
 
431
- return result
429
+ Returns
430
+ -------
431
+ An image or mesh
432
+ """
433
+ vol = self.get_volume(region=region, index=index, **fetch_kwargs)
434
+ return vol.fetch(**fetch_kwargs)
432
435
 
433
436
  def fetch_iter(self, **kwargs):
434
437
  """
@@ -500,7 +503,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
500
503
  taking the voxelwise maximum across the mapped volumes and fragments,
501
504
  and re-labelling regions sequentially.
502
505
 
503
- Paramaters
506
+ Parameters
504
507
  ----------
505
508
  **kwargs: Takes the fetch arguments of its space's template.
506
509
 
@@ -531,7 +534,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
531
534
  disable=(len(self.fragments) == 1 or self.fragments is None)
532
535
  ):
533
536
  mapindex = MapIndex(volume=volidx, fragment=frag)
534
- img = self.fetch(mapindex)
537
+ img = self.fetch(index=mapindex)
535
538
  if np.allclose(img.affine, result_affine):
536
539
  img_data = np.asanyarray(img.dataobj)
537
540
  else:
@@ -572,11 +575,11 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
572
575
  )]
573
576
  )
574
577
 
575
- def compute_centroids(self, split_components: bool = True) -> Dict[str, pointset.PointSet]:
578
+ def compute_centroids(self, split_components: bool = True, **fetch_kwargs) -> Dict[str, pointcloud.PointCloud]:
576
579
  """
577
580
  Compute a dictionary of all regions in this map to their centroids.
578
581
  By default, the regional masks will be split to connected components
579
- and each point in the PointSet corresponds to a region component.
582
+ and each point in the PointCloud corresponds to a region component.
580
583
 
581
584
  Parameters
582
585
  ----------
@@ -589,6 +592,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
589
592
  Dict[str, point.Point]
590
593
  Region names as keys and computed centroids as items.
591
594
  """
595
+ assert self.provides_image, "Centroid computation for meshes is not supported yet."
592
596
  centroids = dict()
593
597
  for regionname, indexlist in siibra_tqdm(
594
598
  self._indices.items(), unit="regions", desc="Computing centroids"
@@ -600,7 +604,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
600
604
  merged_volume = _volume.merge(
601
605
  [
602
606
  _volume.from_nifti(
603
- self.fetch(index=index),
607
+ self.fetch(index=index, **fetch_kwargs),
604
608
  self.space,
605
609
  f"{self.name} - {index}"
606
610
  )
@@ -611,13 +615,16 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
611
615
  mapimg = merged_volume.fetch()
612
616
  elif len(indexlist) == 1:
613
617
  index = indexlist[0]
614
- mapimg = self.fetch(index=index) # returns a mask of the region
618
+ mapimg = self.fetch(index=index, **fetch_kwargs) # returns a mask of the region
615
619
  props = _volume.ComponentSpatialProperties.compute_from_image(
616
620
  img=mapimg,
617
621
  space=self.space,
618
622
  split_components=split_components,
619
623
  )
620
- centroids[regionname] = pointset.from_points([c.centroid for c in props])
624
+ try:
625
+ centroids[regionname] = pointcloud.from_points([c.centroid for c in props])
626
+ except exceptions.EmptyPointCloudError:
627
+ centroids[regionname] = None
621
628
  return centroids
622
629
 
623
630
  def get_resampled_template(self, **fetch_kwargs) -> _volume.Volume:
@@ -683,7 +690,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
683
690
  name=f"Custom colorization of {self}"
684
691
  )
685
692
 
686
- def get_colormap(self, region_specs: Iterable = None):
693
+ def get_colormap(self, region_specs: Iterable = None, *, allow_random_colors: bool = False):
687
694
  """
688
695
  Generate a matplotlib colormap from known rgb values of label indices.
689
696
 
@@ -691,13 +698,23 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
691
698
  ----------
692
699
  region_specs: iterable(regions), optional
693
700
  Optional parameter to only color the desired regions.
701
+ allow_random_colors: bool , optional
694
702
 
695
703
  Returns
696
704
  -------
697
705
  ListedColormap
698
706
  """
699
- from matplotlib.colors import ListedColormap
700
- import numpy as np
707
+ try:
708
+ from matplotlib.colors import ListedColormap
709
+ except ImportError as e:
710
+ logger.error(
711
+ "matplotlib not available. Please install matplotlib to create a matplotlib colormap."
712
+ )
713
+ raise e
714
+ if allow_random_colors:
715
+ seed = len(self.regions)
716
+ np.random.seed(seed)
717
+ logger.info(f"Random colors are allowed for regions without preconfgirued colors. Random seee: {seed}.")
701
718
 
702
719
  colors = {}
703
720
  if region_specs is not None:
@@ -718,14 +735,25 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
718
735
  region = self.get_region(index=index)
719
736
  if region.rgb is not None:
720
737
  colors[index.label] = region.rgb
738
+ elif allow_random_colors:
739
+ random_clr = [np.random.randint(0, 255) for r in range(3)]
740
+ while random_clr in list(colors.values()):
741
+ random_clr = [np.random.randint(0, 255) for r in range(3)]
742
+ colors[index.label] = random_clr
743
+
744
+ if len(colors) == 0:
745
+ raise exceptions.NoPredifinedColormapException(
746
+ f"There is no predefined/preconfigured colormap for '{self}'."
747
+ "Set `allow_random_colors=True` to a colormap with random values"
748
+ )
721
749
 
722
- pallette = np.array(
750
+ palette = np.array(
723
751
  [
724
752
  list(colors[i]) + [1] if i in colors else [0, 0, 0, 0]
725
753
  for i in range(max(colors.keys()) + 1)
726
754
  ]
727
755
  ) / [255, 255, 255, 1]
728
- return ListedColormap(pallette)
756
+ return ListedColormap(palette)
729
757
 
730
758
  def sample_locations(self, regionspec, numpoints: int):
731
759
  """ Sample 3D locations inside a given region.
@@ -742,8 +770,8 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
742
770
 
743
771
  Returns
744
772
  -------
745
- PointSet
746
- Sample points in physcial coordinates corresponding to this
773
+ PointCloud
774
+ Sample points in physical coordinates corresponding to this
747
775
  parcellationmap
748
776
  """
749
777
  index = self.get_index(regionspec)
@@ -760,7 +788,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
760
788
  np.unravel_index(np.random.choice(len(p), numpoints, p=p), W.shape)
761
789
  ).T
762
790
  XYZ = np.dot(mask.affine, np.c_[XYZ_, np.ones(numpoints)].T)[:3, :].T
763
- return pointset.PointSet(XYZ, space=self.space)
791
+ return pointcloud.PointCloud(XYZ, space=self.space)
764
792
 
765
793
  def to_sparse(self):
766
794
  """
@@ -831,10 +859,10 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
831
859
 
832
860
  if isinstance(item, point.Point):
833
861
  return self._assign_points(
834
- pointset.PointSet([item], item.space, sigma_mm=item.sigma),
862
+ pointcloud.PointCloud([item], item.space, sigma_mm=item.sigma),
835
863
  lower_threshold
836
864
  )
837
- if isinstance(item, pointset.PointSet):
865
+ if isinstance(item, pointcloud.PointCloud):
838
866
  return self._assign_points(item, lower_threshold)
839
867
  if isinstance(item, _volume.Volume):
840
868
  return self._assign_volume(
@@ -916,7 +944,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
916
944
  if len(assignments) == 0:
917
945
  return pd.DataFrame(columns=columns)
918
946
  # determine the unique set of observed indices in order to do region lookups
919
- # only once for each map index occuring in the point list
947
+ # only once for each map index occurring in the point list
920
948
  labelled = self.is_labelled # avoid calling this in a loop
921
949
  observed_indices = { # unique set of observed map indices. NOTE: len(observed_indices) << len(assignments)
922
950
  (
@@ -989,9 +1017,9 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
989
1017
  .dropna(axis='columns', how='all')
990
1018
  )
991
1019
 
992
- def _assign_points(self, points: pointset.PointSet, lower_threshold: float) -> List[MapAssignment]:
1020
+ def _assign_points(self, points: pointcloud.PointCloud, lower_threshold: float) -> List[MapAssignment]:
993
1021
  """
994
- assign a PointSet to this parcellation map.
1022
+ assign a PointCloud to this parcellation map.
995
1023
 
996
1024
  Parameters
997
1025
  -----------
@@ -1025,7 +1053,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
1025
1053
  assignments.append(
1026
1054
  MapAssignment(
1027
1055
  input_structure=pointindex,
1028
- centroid=tuple(np.array(position).round(2)),
1056
+ centroid=tuple(position),
1029
1057
  volume=vol,
1030
1058
  fragment=frag,
1031
1059
  map_value=value
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");
@@ -54,7 +54,7 @@ class FreesurferAnnot(_provider.VolumeProvider, srctype="freesurfer-annot"):
54
54
  frag_labels[selected_label] = 1
55
55
  frag_labels[~selected_label] = 0
56
56
  else:
57
- frag_labels[frag_labels == -1] = 0 # annot files store backgorund as -1 while siibra uses 0
57
+ frag_labels[frag_labels == -1] = 0 # annot files store background as -1 while siibra uses 0
58
58
  vertex_labels.append(frag_labels)
59
59
 
60
60
  return {"labels": np.hstack(vertex_labels)}
@@ -98,7 +98,7 @@ class ZippedFreesurferAnnot(_provider.VolumeProvider, srctype="zip/freesurfer-an
98
98
  frag_labels[selected_label] = 1
99
99
  frag_labels[~selected_label] = 0
100
100
  else:
101
- frag_labels[frag_labels == -1] = 0 # annot files store backgorund as -1 while siibra uses 0
101
+ frag_labels[frag_labels == -1] = 0 # annot files store background as -1 while siibra uses 0
102
102
  vertex_labels.append(frag_labels)
103
103
 
104
104
  return {"labels": np.hstack(vertex_labels)}
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
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");