siibra 0.5a2__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of siibra might be problematic. Click here for more details.
- siibra/VERSION +1 -1
- siibra/__init__.py +20 -12
- siibra/commons.py +145 -90
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +22 -17
- siibra/configuration/factory.py +177 -128
- siibra/core/__init__.py +1 -8
- siibra/core/{relation_qualification.py → assignment.py} +17 -14
- siibra/core/atlas.py +66 -35
- siibra/core/concept.py +81 -39
- siibra/core/parcellation.py +83 -67
- siibra/core/region.py +569 -263
- siibra/core/space.py +7 -39
- siibra/core/structure.py +111 -0
- siibra/exceptions.py +63 -0
- siibra/experimental/__init__.py +19 -0
- siibra/experimental/contour.py +61 -0
- siibra/experimental/cortical_profile_sampler.py +57 -0
- siibra/experimental/patch.py +98 -0
- siibra/experimental/plane3d.py +256 -0
- siibra/explorer/__init__.py +16 -0
- siibra/explorer/url.py +112 -52
- siibra/explorer/util.py +31 -9
- siibra/features/__init__.py +73 -8
- siibra/features/anchor.py +75 -196
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +2 -2
- siibra/features/connectivity/regional_connectivity.py +99 -10
- siibra/features/connectivity/streamline_counts.py +1 -1
- siibra/features/connectivity/streamline_lengths.py +1 -1
- siibra/features/connectivity/tracing_connectivity.py +1 -1
- siibra/features/dataset/__init__.py +1 -1
- siibra/features/dataset/ebrains.py +3 -3
- siibra/features/feature.py +219 -110
- siibra/features/image/__init__.py +1 -1
- siibra/features/image/image.py +21 -13
- siibra/features/image/sections.py +1 -1
- siibra/features/image/volume_of_interest.py +1 -1
- siibra/features/tabular/__init__.py +1 -1
- siibra/features/tabular/bigbrain_intensity_profile.py +24 -13
- siibra/features/tabular/cell_density_profile.py +111 -69
- siibra/features/tabular/cortical_profile.py +82 -16
- siibra/features/tabular/gene_expression.py +117 -6
- siibra/features/tabular/layerwise_bigbrain_intensities.py +7 -9
- siibra/features/tabular/layerwise_cell_density.py +9 -24
- siibra/features/tabular/receptor_density_fingerprint.py +11 -6
- siibra/features/tabular/receptor_density_profile.py +12 -15
- siibra/features/tabular/regional_timeseries_activity.py +74 -18
- siibra/features/tabular/tabular.py +17 -8
- siibra/livequeries/__init__.py +1 -7
- siibra/livequeries/allen.py +139 -77
- siibra/livequeries/bigbrain.py +104 -128
- siibra/livequeries/ebrains.py +7 -4
- siibra/livequeries/query.py +1 -2
- siibra/locations/__init__.py +32 -25
- siibra/locations/boundingbox.py +153 -127
- siibra/locations/location.py +45 -80
- siibra/locations/point.py +97 -83
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +107 -13
- siibra/retrieval/datasets.py +9 -14
- siibra/retrieval/exceptions/__init__.py +2 -1
- siibra/retrieval/repositories.py +147 -53
- siibra/retrieval/requests.py +64 -29
- siibra/vocabularies/__init__.py +2 -2
- siibra/volumes/__init__.py +7 -9
- siibra/volumes/parcellationmap.py +396 -253
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/{gifti.py → providers/gifti.py} +29 -18
- siibra/volumes/{neuroglancer.py → providers/neuroglancer.py} +204 -92
- siibra/volumes/{nifti.py → providers/nifti.py} +64 -44
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +159 -260
- siibra/volumes/volume.py +720 -152
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/METADATA +25 -28
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/WHEEL +1 -1
- siibra/locations/pointset.py +0 -198
- siibra-0.5a2.dist-info/RECORD +0 -74
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/LICENSE +0 -0
- {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2024
|
|
2
2
|
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
3
|
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -14,29 +14,30 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Provides spatial representations for parcellations and regions."""
|
|
16
16
|
|
|
17
|
-
from . import volume as _volume
|
|
18
|
-
from
|
|
17
|
+
from . import volume as _volume
|
|
18
|
+
from .providers import provider
|
|
19
|
+
from .. import logger, QUIET, exceptions
|
|
19
20
|
from ..commons import (
|
|
20
21
|
MapIndex,
|
|
21
22
|
MapType,
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
compare_arrays,
|
|
24
|
+
resample_img_to_img,
|
|
25
|
+
connected_components,
|
|
24
26
|
clear_name,
|
|
25
27
|
create_key,
|
|
26
28
|
create_gaussian_kernel,
|
|
27
29
|
siibra_tqdm,
|
|
28
30
|
Species,
|
|
29
|
-
CompareMapsResult
|
|
31
|
+
CompareMapsResult,
|
|
32
|
+
generate_uuid
|
|
30
33
|
)
|
|
31
34
|
from ..core import concept, space, parcellation, region as _region
|
|
32
|
-
from ..locations import point,
|
|
33
|
-
from ..retrieval import requests
|
|
35
|
+
from ..locations import location, point, pointcloud
|
|
34
36
|
|
|
35
37
|
import numpy as np
|
|
36
38
|
from typing import Union, Dict, List, TYPE_CHECKING, Iterable, Tuple
|
|
37
39
|
from scipy.ndimage import distance_transform_edt
|
|
38
40
|
from collections import defaultdict
|
|
39
|
-
from nibabel import Nifti1Image
|
|
40
41
|
from nilearn import image
|
|
41
42
|
import pandas as pd
|
|
42
43
|
from dataclasses import dataclass, asdict
|
|
@@ -45,31 +46,18 @@ if TYPE_CHECKING:
|
|
|
45
46
|
from ..core.region import Region
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
class ExcessiveArgumentException(ValueError): pass
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class InsufficientArgumentException(ValueError): pass
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class ConflictingArgumentException(ValueError): pass
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class NonUniqueIndexError(RuntimeError): pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class NoVolumeFound(RuntimeError): pass
|
|
61
|
-
|
|
62
49
|
@dataclass
|
|
63
|
-
class
|
|
50
|
+
class MapAssignment:
|
|
64
51
|
input_structure: int
|
|
65
|
-
centroid: Union[Tuple[np.ndarray], point.Point]
|
|
52
|
+
centroid: Union[Tuple[np.ndarray], point.Point]
|
|
66
53
|
volume: int
|
|
67
54
|
fragment: str
|
|
68
55
|
map_value: np.ndarray
|
|
69
56
|
|
|
70
57
|
|
|
71
58
|
@dataclass
|
|
72
|
-
class AssignImageResult(CompareMapsResult,
|
|
59
|
+
class AssignImageResult(CompareMapsResult, MapAssignment):
|
|
60
|
+
pass
|
|
73
61
|
|
|
74
62
|
|
|
75
63
|
class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
@@ -80,13 +68,14 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
80
68
|
name: str,
|
|
81
69
|
space_spec: dict,
|
|
82
70
|
parcellation_spec: dict,
|
|
83
|
-
indices: Dict[str, Dict],
|
|
71
|
+
indices: Dict[str, List[Dict]],
|
|
84
72
|
volumes: list = [],
|
|
85
73
|
shortname: str = "",
|
|
86
74
|
description: str = "",
|
|
87
75
|
modality: str = None,
|
|
88
76
|
publications: list = [],
|
|
89
77
|
datasets: list = [],
|
|
78
|
+
prerelease: bool = False,
|
|
90
79
|
):
|
|
91
80
|
"""
|
|
92
81
|
Constructs a new parcellation object.
|
|
@@ -131,8 +120,11 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
131
120
|
description=description,
|
|
132
121
|
publications=publications,
|
|
133
122
|
datasets=datasets,
|
|
134
|
-
modality=modality
|
|
123
|
+
modality=modality,
|
|
124
|
+
prerelease=prerelease,
|
|
135
125
|
)
|
|
126
|
+
self._space_spec = space_spec
|
|
127
|
+
self._parcellation_spec = parcellation_spec
|
|
136
128
|
|
|
137
129
|
# Since the volumes might include 4D arrays, where the actual
|
|
138
130
|
# volume index points to a z coordinate, we create subvolume
|
|
@@ -165,15 +157,12 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
165
157
|
duplicates = {x for x in all_indices if x in seen or seen.add(x)}
|
|
166
158
|
if len(duplicates) > 0:
|
|
167
159
|
logger.warning(f"Non unique indices encountered in {self}: {duplicates}")
|
|
168
|
-
|
|
169
|
-
self._space_spec = space_spec
|
|
170
|
-
self._parcellation_spec = parcellation_spec
|
|
171
160
|
self._affine_cached = None
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def key(self):
|
|
164
|
+
_id = self.id
|
|
165
|
+
return create_key(_id[len("siibra-map-v0.0.1"):])
|
|
177
166
|
|
|
178
167
|
@property
|
|
179
168
|
def species(self) -> Species:
|
|
@@ -205,12 +194,18 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
205
194
|
"""
|
|
206
195
|
matches = self.find_indices(region)
|
|
207
196
|
if len(matches) > 1:
|
|
208
|
-
|
|
197
|
+
# if there is an exact match, we still use it. If not, we cannot proceed.
|
|
198
|
+
regionname = region.name if isinstance(region, _region.Region) \
|
|
199
|
+
else region
|
|
200
|
+
for index, matched_name in matches.items():
|
|
201
|
+
if matched_name == regionname:
|
|
202
|
+
return index
|
|
203
|
+
raise exceptions.NonUniqueIndexError(
|
|
209
204
|
f"The specification '{region}' matches multiple mapped "
|
|
210
205
|
f"structures in {str(self)}: {list(matches.values())}"
|
|
211
206
|
)
|
|
212
207
|
elif len(matches) == 0:
|
|
213
|
-
raise NonUniqueIndexError(
|
|
208
|
+
raise exceptions.NonUniqueIndexError(
|
|
214
209
|
f"The specification '{region}' does not match to any structure mapped in {self}"
|
|
215
210
|
)
|
|
216
211
|
else:
|
|
@@ -328,69 +323,27 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
328
323
|
def regions(self):
|
|
329
324
|
return list(self._indices)
|
|
330
325
|
|
|
331
|
-
def
|
|
326
|
+
def get_volume(
|
|
332
327
|
self,
|
|
333
|
-
|
|
328
|
+
region: Union[str, "Region"] = None,
|
|
334
329
|
*,
|
|
335
330
|
index: MapIndex = None,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
):
|
|
339
|
-
"""
|
|
340
|
-
Fetches one particular volume of this parcellation map.
|
|
341
|
-
|
|
342
|
-
If there's only one volume, this is the default, otherwise further
|
|
343
|
-
specification is requested:
|
|
344
|
-
- the volume index,
|
|
345
|
-
- the MapIndex (which results in a regional map being returned)
|
|
346
|
-
|
|
347
|
-
You might also consider fetch_iter() to iterate the volumes, or
|
|
348
|
-
compress() to produce a single-volume parcellation map.
|
|
349
|
-
|
|
350
|
-
Parameters
|
|
351
|
-
----------
|
|
352
|
-
region_or_index: str, Region, MapIndex
|
|
353
|
-
Lazy match the specification.
|
|
354
|
-
index: MapIndex
|
|
355
|
-
Explicit specification of the map index, typically resulting
|
|
356
|
-
in a regional map (mask or statistical map) to be returned.
|
|
357
|
-
Note that supplying 'region' will result in retrieving the map index of that region
|
|
358
|
-
automatically.
|
|
359
|
-
region: str, Region
|
|
360
|
-
Specification of a region name, resulting in a regional map
|
|
361
|
-
(mask or statistical map) to be returned.
|
|
362
|
-
**kwargs
|
|
363
|
-
- resolution_mm: resolution in millimeters
|
|
364
|
-
- format: the format of the volume, like "mesh" or "nii"
|
|
365
|
-
- voi: a BoundingBox of interest
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
Note
|
|
369
|
-
----
|
|
370
|
-
Not all keyword arguments are supported for volume formats. Format
|
|
371
|
-
is restricted by available formats (check formats property).
|
|
372
|
-
|
|
373
|
-
Returns
|
|
374
|
-
-------
|
|
375
|
-
An image or mesh
|
|
376
|
-
"""
|
|
331
|
+
**kwargs,
|
|
332
|
+
) -> Union[_volume.Volume, _volume.FilteredVolume, _volume.Subvolume]:
|
|
377
333
|
try:
|
|
378
|
-
length = len([arg for arg in [
|
|
334
|
+
length = len([arg for arg in [region, index] if arg is not None])
|
|
379
335
|
assert length == 1
|
|
380
336
|
except AssertionError:
|
|
381
337
|
if length > 1:
|
|
382
|
-
raise ExcessiveArgumentException(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if isinstance(region_or_index, MapIndex):
|
|
386
|
-
index = region_or_index
|
|
387
|
-
|
|
388
|
-
if isinstance(region_or_index, (str, _region.Region)):
|
|
389
|
-
region = region_or_index
|
|
390
|
-
|
|
338
|
+
raise exceptions.ExcessiveArgumentException(
|
|
339
|
+
"One and only one of region or index can be defined for `get_volume`."
|
|
340
|
+
)
|
|
391
341
|
mapindex = None
|
|
392
342
|
if region is not None:
|
|
393
|
-
|
|
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)}")
|
|
394
347
|
mapindex = self.get_index(region)
|
|
395
348
|
if index is not None:
|
|
396
349
|
assert isinstance(index, MapIndex)
|
|
@@ -399,34 +352,21 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
399
352
|
if len(self) == 1:
|
|
400
353
|
mapindex = MapIndex(volume=0, label=None)
|
|
401
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."
|
|
402
356
|
logger.info(
|
|
403
357
|
"Map provides multiple volumes and no specification is"
|
|
404
|
-
"provided. Resampling all volumes to the space."
|
|
405
|
-
)
|
|
406
|
-
resolution = kwargs.get("resolution_mm")
|
|
407
|
-
template = self.space.get_template().fetch(
|
|
408
|
-
resolution_mm=resolution
|
|
358
|
+
" provided. Resampling all volumes to the space."
|
|
409
359
|
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
unit=" volume", desc="Fetching", total=len(self)
|
|
414
|
-
):
|
|
415
|
-
regionlabel = i + 1
|
|
416
|
-
regionmap = image.resample_to_img(
|
|
417
|
-
self.fetch(region=region, resolution_mm=resolution),
|
|
418
|
-
template,
|
|
419
|
-
interpolation='nearest'
|
|
420
|
-
)
|
|
421
|
-
aggregated_volume[regionmap.get_fdata() > 0] = regionlabel
|
|
422
|
-
return Nifti1Image(aggregated_volume, affine=template.affine)
|
|
360
|
+
labels = list(range(len(self.volumes)))
|
|
361
|
+
merged_volume = _volume.merge(self.volumes, labels, **kwargs)
|
|
362
|
+
return merged_volume
|
|
423
363
|
else:
|
|
424
|
-
raise NoVolumeFound("Map provides no volumes.")
|
|
364
|
+
raise exceptions.NoVolumeFound("Map provides no volumes.")
|
|
425
365
|
|
|
426
366
|
kwargs_fragment = kwargs.pop("fragment", None)
|
|
427
367
|
if kwargs_fragment is not None:
|
|
428
368
|
if (mapindex.fragment is not None) and (kwargs_fragment != mapindex.fragment):
|
|
429
|
-
raise ConflictingArgumentException(
|
|
369
|
+
raise exceptions.ConflictingArgumentException(
|
|
430
370
|
"Conflicting specifications for fetching volume fragment: "
|
|
431
371
|
f"{mapindex.fragment} / {kwargs_fragment}"
|
|
432
372
|
)
|
|
@@ -438,28 +378,74 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
438
378
|
raise IndexError(
|
|
439
379
|
f"{self} provides {len(self)} mapped volumes, but #{mapindex.volume} was requested."
|
|
440
380
|
)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
447
422
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
423
|
+
|
|
424
|
+
Note
|
|
425
|
+
----
|
|
426
|
+
Not all keyword arguments are supported for volume formats. Format
|
|
427
|
+
is restricted by available formats (check formats property).
|
|
428
|
+
|
|
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)
|
|
451
435
|
|
|
452
436
|
def fetch_iter(self, **kwargs):
|
|
453
437
|
"""
|
|
454
438
|
Returns an iterator to fetch all mapped volumes sequentially.
|
|
455
439
|
|
|
456
|
-
All arguments are passed on to function Map.fetch().
|
|
440
|
+
All arguments are passed on to function Map.fetch(). By default, it
|
|
441
|
+
will go through all fragments as well.
|
|
457
442
|
"""
|
|
458
|
-
|
|
443
|
+
fragments = {kwargs.pop('fragment', None)} or self.fragments or {None}
|
|
459
444
|
return (
|
|
460
445
|
self.fetch(
|
|
461
|
-
index=MapIndex(volume=i, label=None, fragment=
|
|
446
|
+
index=MapIndex(volume=i, label=None, fragment=frag), **kwargs
|
|
462
447
|
)
|
|
448
|
+
for frag in fragments
|
|
463
449
|
for i in range(len(self))
|
|
464
450
|
)
|
|
465
451
|
|
|
@@ -529,10 +515,10 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
529
515
|
raise RuntimeError("The map cannot be merged since there are no multiple volumes or fragments.")
|
|
530
516
|
|
|
531
517
|
# initialize empty volume according to the template
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
518
|
+
template_img = self.space.get_template().fetch(**kwargs)
|
|
519
|
+
result_arr = np.zeros_like(np.asanyarray(template_img.dataobj))
|
|
520
|
+
result_affine = template_img.affine
|
|
521
|
+
voxelwise_max = np.zeros_like(result_arr)
|
|
536
522
|
interpolation = 'nearest' if self.is_labelled else 'linear'
|
|
537
523
|
next_labelindex = 1
|
|
538
524
|
region_indices = defaultdict(list)
|
|
@@ -548,11 +534,14 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
548
534
|
disable=(len(self.fragments) == 1 or self.fragments is None)
|
|
549
535
|
):
|
|
550
536
|
mapindex = MapIndex(volume=volidx, fragment=frag)
|
|
551
|
-
img = self.fetch(mapindex)
|
|
552
|
-
if np.
|
|
537
|
+
img = self.fetch(index=mapindex)
|
|
538
|
+
if np.allclose(img.affine, result_affine):
|
|
539
|
+
img_data = np.asanyarray(img.dataobj)
|
|
540
|
+
else:
|
|
553
541
|
logger.debug(f"Compression requires to resample volume {volidx} ({interpolation})")
|
|
554
|
-
|
|
555
|
-
|
|
542
|
+
img_data = np.asanyarray(
|
|
543
|
+
resample_img_to_img(img, template_img).dataobj
|
|
544
|
+
)
|
|
556
545
|
|
|
557
546
|
if self.is_labelled:
|
|
558
547
|
labels = set(np.unique(img_data)) - {0}
|
|
@@ -571,7 +560,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
571
560
|
update_voxels = (img_data > voxelwise_max)
|
|
572
561
|
else:
|
|
573
562
|
update_voxels = (img_data == label)
|
|
574
|
-
|
|
563
|
+
result_arr[update_voxels] = next_labelindex
|
|
575
564
|
voxelwise_max[update_voxels] = img_data[update_voxels]
|
|
576
565
|
next_labelindex += 1
|
|
577
566
|
|
|
@@ -581,43 +570,64 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
581
570
|
space_spec=self._space_spec,
|
|
582
571
|
parcellation_spec=self._parcellation_spec,
|
|
583
572
|
indices=region_indices,
|
|
584
|
-
volumes=[
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
providers=[nifti.NiftiProvider(result_nii)]
|
|
588
|
-
)
|
|
589
|
-
]
|
|
573
|
+
volumes=[_volume.from_array(
|
|
574
|
+
result_arr, result_affine, self._space_spec, name=self.name + " compressed"
|
|
575
|
+
)]
|
|
590
576
|
)
|
|
591
577
|
|
|
592
|
-
def compute_centroids(self) -> Dict[str,
|
|
578
|
+
def compute_centroids(self, split_components: bool = True, **fetch_kwargs) -> Dict[str, pointcloud.PointCloud]:
|
|
593
579
|
"""
|
|
594
|
-
Compute a dictionary of
|
|
580
|
+
Compute a dictionary of all regions in this map to their centroids.
|
|
581
|
+
By default, the regional masks will be split to connected components
|
|
582
|
+
and each point in the PointCloud corresponds to a region component.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
split_components: bool, default: True
|
|
587
|
+
If True, finds the spatial properties for each connected component
|
|
588
|
+
found by skimage.measure.label.
|
|
595
589
|
|
|
596
590
|
Returns
|
|
597
591
|
-------
|
|
598
592
|
Dict[str, point.Point]
|
|
599
593
|
Region names as keys and computed centroids as items.
|
|
600
594
|
"""
|
|
601
|
-
|
|
602
|
-
|
|
595
|
+
assert self.provides_image, "Centroid computation for meshes is not supported yet."
|
|
596
|
+
centroids = dict()
|
|
603
597
|
for regionname, indexlist in siibra_tqdm(
|
|
604
598
|
self._indices.items(), unit="regions", desc="Computing centroids"
|
|
605
599
|
):
|
|
606
|
-
assert len(indexlist) == 1
|
|
607
|
-
index = indexlist[0]
|
|
608
|
-
if index.label == 0:
|
|
609
|
-
continue
|
|
610
|
-
with QUIET:
|
|
611
|
-
mapimg = self.fetch(index=index) # returns a mask of the region
|
|
612
|
-
maparr = np.asanyarray(mapimg.dataobj)
|
|
613
|
-
centroid_vox = np.mean(np.where(maparr == 1), axis=1)
|
|
614
600
|
assert regionname not in centroids
|
|
615
|
-
|
|
616
|
-
|
|
601
|
+
# get the mask of the region in this map
|
|
602
|
+
with QUIET:
|
|
603
|
+
if len(indexlist) >= 1:
|
|
604
|
+
merged_volume = _volume.merge(
|
|
605
|
+
[
|
|
606
|
+
_volume.from_nifti(
|
|
607
|
+
self.fetch(index=index, **fetch_kwargs),
|
|
608
|
+
self.space,
|
|
609
|
+
f"{self.name} - {index}"
|
|
610
|
+
)
|
|
611
|
+
for index in indexlist
|
|
612
|
+
],
|
|
613
|
+
labels=[1] * len(indexlist)
|
|
614
|
+
)
|
|
615
|
+
mapimg = merged_volume.fetch()
|
|
616
|
+
elif len(indexlist) == 1:
|
|
617
|
+
index = indexlist[0]
|
|
618
|
+
mapimg = self.fetch(index=index, **fetch_kwargs) # returns a mask of the region
|
|
619
|
+
props = _volume.ComponentSpatialProperties.compute_from_image(
|
|
620
|
+
img=mapimg,
|
|
621
|
+
space=self.space,
|
|
622
|
+
split_components=split_components,
|
|
617
623
|
)
|
|
624
|
+
try:
|
|
625
|
+
centroids[regionname] = pointcloud.from_points([c.centroid for c in props])
|
|
626
|
+
except exceptions.EmptyPointCloudError:
|
|
627
|
+
centroids[regionname] = None
|
|
618
628
|
return centroids
|
|
619
629
|
|
|
620
|
-
def get_resampled_template(self, **fetch_kwargs) ->
|
|
630
|
+
def get_resampled_template(self, **fetch_kwargs) -> _volume.Volume:
|
|
621
631
|
"""
|
|
622
632
|
Resample the reference space template to fetched map image. Uses
|
|
623
633
|
nilearn.image.resample_to_img to resample the template.
|
|
@@ -628,17 +638,19 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
628
638
|
|
|
629
639
|
Returns
|
|
630
640
|
-------
|
|
631
|
-
|
|
641
|
+
Volume
|
|
632
642
|
"""
|
|
633
643
|
source_template = self.space.get_template().fetch()
|
|
634
644
|
map_image = self.fetch(**fetch_kwargs)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
645
|
+
img = image.resample_to_img(source_template, map_image, interpolation='continuous')
|
|
646
|
+
return _volume.from_array(
|
|
647
|
+
data=img.dataobj,
|
|
648
|
+
affine=img.affine,
|
|
649
|
+
space=self.space,
|
|
650
|
+
name=f"{source_template} resampled to coordinate system of {self}"
|
|
639
651
|
)
|
|
640
652
|
|
|
641
|
-
def colorize(self, values: dict, **kwargs) ->
|
|
653
|
+
def colorize(self, values: dict, **kwargs) -> _volume.Volume:
|
|
642
654
|
"""Colorize the map with the provided regional values.
|
|
643
655
|
|
|
644
656
|
Parameters
|
|
@@ -671,7 +683,12 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
671
683
|
else:
|
|
672
684
|
result[img == index.label] = value
|
|
673
685
|
|
|
674
|
-
return
|
|
686
|
+
return _volume.from_array(
|
|
687
|
+
data=result,
|
|
688
|
+
affine=affine,
|
|
689
|
+
space=self.space,
|
|
690
|
+
name=f"Custom colorization of {self}"
|
|
691
|
+
)
|
|
675
692
|
|
|
676
693
|
def get_colormap(self, region_specs: Iterable = None):
|
|
677
694
|
"""
|
|
@@ -732,7 +749,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
732
749
|
|
|
733
750
|
Returns
|
|
734
751
|
-------
|
|
735
|
-
|
|
752
|
+
PointCloud
|
|
736
753
|
Sample points in physcial coordinates corresponding to this
|
|
737
754
|
parcellationmap
|
|
738
755
|
"""
|
|
@@ -750,7 +767,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
750
767
|
np.unravel_index(np.random.choice(len(p), numpoints, p=p), W.shape)
|
|
751
768
|
).T
|
|
752
769
|
XYZ = np.dot(mask.affine, np.c_[XYZ_, np.ones(numpoints)].T)[:3, :].T
|
|
753
|
-
return
|
|
770
|
+
return pointcloud.PointCloud(XYZ, space=self.space)
|
|
754
771
|
|
|
755
772
|
def to_sparse(self):
|
|
756
773
|
"""
|
|
@@ -810,44 +827,52 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
810
827
|
|
|
811
828
|
def _assign(
|
|
812
829
|
self,
|
|
813
|
-
item:
|
|
830
|
+
item: location.Location,
|
|
814
831
|
minsize_voxel=1,
|
|
815
832
|
lower_threshold=0.0,
|
|
816
833
|
**kwargs
|
|
817
|
-
) -> List[Union[
|
|
834
|
+
) -> List[Union[MapAssignment, AssignImageResult]]:
|
|
818
835
|
"""
|
|
819
836
|
For internal use only. Returns a dataclass, which provides better static type checking.
|
|
820
837
|
"""
|
|
821
838
|
|
|
822
839
|
if isinstance(item, point.Point):
|
|
823
|
-
return self._assign_points(
|
|
824
|
-
|
|
840
|
+
return self._assign_points(
|
|
841
|
+
pointcloud.PointCloud([item], item.space, sigma_mm=item.sigma),
|
|
842
|
+
lower_threshold
|
|
843
|
+
)
|
|
844
|
+
if isinstance(item, pointcloud.PointCloud):
|
|
825
845
|
return self._assign_points(item, lower_threshold)
|
|
826
|
-
if isinstance(item,
|
|
827
|
-
return self.
|
|
828
|
-
|
|
846
|
+
if isinstance(item, _volume.Volume):
|
|
847
|
+
return self._assign_volume(
|
|
848
|
+
queryvolume=item,
|
|
849
|
+
lower_threshold=lower_threshold,
|
|
850
|
+
minsize_voxel=minsize_voxel,
|
|
851
|
+
**kwargs
|
|
852
|
+
)
|
|
853
|
+
|
|
829
854
|
raise RuntimeError(
|
|
830
855
|
f"Items of type {item.__class__.__name__} cannot be used for region assignment."
|
|
831
856
|
)
|
|
832
857
|
|
|
833
858
|
def assign(
|
|
834
859
|
self,
|
|
835
|
-
item:
|
|
860
|
+
item: location.Location,
|
|
836
861
|
minsize_voxel=1,
|
|
837
862
|
lower_threshold=0.0,
|
|
838
863
|
**kwargs
|
|
839
|
-
):
|
|
840
|
-
"""Assign an input
|
|
864
|
+
) -> "pd.DataFrame":
|
|
865
|
+
"""Assign an input Location to brain regions.
|
|
841
866
|
|
|
842
|
-
The input
|
|
867
|
+
The input is assumed to be defined in the same coordinate space
|
|
843
868
|
as this parcellation map.
|
|
844
869
|
|
|
845
870
|
Parameters
|
|
846
871
|
----------
|
|
847
|
-
item:
|
|
872
|
+
item: Location
|
|
848
873
|
A spatial object defined in the same physical reference space as
|
|
849
874
|
this parcellation map, which could be a point, set of points, or
|
|
850
|
-
image. If it is an image, it will be resampled to the same voxel
|
|
875
|
+
image volume. If it is an image, it will be resampled to the same voxel
|
|
851
876
|
space if its affine transformation differs from that of the
|
|
852
877
|
parcellation map. Resampling will use linear interpolation for float
|
|
853
878
|
image types, otherwise nearest neighbor.
|
|
@@ -859,7 +884,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
859
884
|
|
|
860
885
|
Returns
|
|
861
886
|
-------
|
|
862
|
-
|
|
887
|
+
pandas.DataFrame
|
|
863
888
|
A table of associated regions and their scores per component found
|
|
864
889
|
in the input image, or per coordinate provided. The scores are:
|
|
865
890
|
|
|
@@ -876,11 +901,6 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
876
901
|
masks as the ratio between the volume of their intersection and
|
|
877
902
|
the volume of the input image signal component (NaN for exact
|
|
878
903
|
coordinates)
|
|
879
|
-
components: Nifti1Image or None
|
|
880
|
-
If the input was an image, this is a labelled volume mapping the
|
|
881
|
-
detected components in the input image, where pixel values correspond
|
|
882
|
-
to the "component" column of the assignment table. If the input was
|
|
883
|
-
a Point or PointSet, returns None.
|
|
884
904
|
"""
|
|
885
905
|
|
|
886
906
|
assignments = self._assign(item, minsize_voxel, lower_threshold, **kwargs)
|
|
@@ -952,7 +972,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
952
972
|
"input containedness": a.intersection_over_second,
|
|
953
973
|
}
|
|
954
974
|
}
|
|
955
|
-
elif isinstance(a,
|
|
975
|
+
elif isinstance(a, MapAssignment):
|
|
956
976
|
item_to_append = {
|
|
957
977
|
**item_to_append,
|
|
958
978
|
**{
|
|
@@ -969,18 +989,18 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
969
989
|
raise RuntimeError("assignments must be of type Assignment or AssignImageResult!")
|
|
970
990
|
|
|
971
991
|
dataframe_list.append(item_to_append)
|
|
972
|
-
df = pd.DataFrame(dataframe_list)
|
|
973
992
|
return (
|
|
974
|
-
|
|
993
|
+
pd.DataFrame(dataframe_list)
|
|
975
994
|
.convert_dtypes() # convert will guess numeric column types
|
|
976
995
|
.reindex(columns=columns)
|
|
996
|
+
.dropna(axis='columns', how='all')
|
|
977
997
|
)
|
|
978
998
|
|
|
979
|
-
def _assign_points(self, points:
|
|
999
|
+
def _assign_points(self, points: pointcloud.PointCloud, lower_threshold: float) -> List[MapAssignment]:
|
|
980
1000
|
"""
|
|
981
|
-
assign a
|
|
1001
|
+
assign a PointCloud to this parcellation map.
|
|
982
1002
|
|
|
983
|
-
Parameters
|
|
1003
|
+
Parameters
|
|
984
1004
|
-----------
|
|
985
1005
|
lower_threshold: float, default: 0
|
|
986
1006
|
Lower threshold on values in the statistical map. Values smaller than
|
|
@@ -1010,7 +1030,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
1010
1030
|
if value > lower_threshold:
|
|
1011
1031
|
position = pts_warped[pointindex].coordinate
|
|
1012
1032
|
assignments.append(
|
|
1013
|
-
|
|
1033
|
+
MapAssignment(
|
|
1014
1034
|
input_structure=pointindex,
|
|
1015
1035
|
centroid=tuple(np.array(position).round(2)),
|
|
1016
1036
|
volume=vol,
|
|
@@ -1025,7 +1045,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
1025
1045
|
# of the coordinates.
|
|
1026
1046
|
for pointindex, pt in siibra_tqdm(
|
|
1027
1047
|
enumerate(points.warp(self.space.id)),
|
|
1028
|
-
total=len(points), desc="
|
|
1048
|
+
total=len(points), desc="Assigning points",
|
|
1029
1049
|
):
|
|
1030
1050
|
sigma_vox = pt.sigma / scaling
|
|
1031
1051
|
if sigma_vox < 3:
|
|
@@ -1037,7 +1057,7 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
1037
1057
|
for _, vol, frag, value in values:
|
|
1038
1058
|
if value > lower_threshold:
|
|
1039
1059
|
assignments.append(
|
|
1040
|
-
|
|
1060
|
+
MapAssignment(
|
|
1041
1061
|
input_structure=pointindex,
|
|
1042
1062
|
centroid=tuple(pt),
|
|
1043
1063
|
volume=vol,
|
|
@@ -1046,91 +1066,214 @@ class Map(concept.AtlasConcept, configuration_folder="maps"):
|
|
|
1046
1066
|
)
|
|
1047
1067
|
)
|
|
1048
1068
|
else:
|
|
1049
|
-
logger.
|
|
1069
|
+
logger.debug(
|
|
1050
1070
|
f"Assigning uncertain coordinate {tuple(pt)} to {len(self)} maps."
|
|
1051
1071
|
)
|
|
1052
1072
|
kernel = create_gaussian_kernel(sigma_vox, 3)
|
|
1053
1073
|
r = int(kernel.shape[0] / 2) # effective radius
|
|
1054
|
-
|
|
1074
|
+
assert pt.homogeneous.shape[0] == 1
|
|
1075
|
+
xyz_vox = (np.dot(phys2vox, pt.homogeneous.T) + 0.5).astype("int")
|
|
1055
1076
|
shift = np.identity(4)
|
|
1056
|
-
shift[:3, -1] = xyz_vox[:3] - r
|
|
1077
|
+
shift[:3, -1] = xyz_vox[:3, 0] - r
|
|
1057
1078
|
# build niftiimage with the Gaussian blob,
|
|
1058
1079
|
# then recurse into this method with the image input
|
|
1059
|
-
|
|
1060
|
-
|
|
1080
|
+
gaussian_kernel = _volume.from_array(
|
|
1081
|
+
data=kernel,
|
|
1082
|
+
affine=np.dot(self.affine, shift),
|
|
1083
|
+
space=self.space,
|
|
1084
|
+
name=f"Gaussian kernel of {pt}"
|
|
1085
|
+
)
|
|
1086
|
+
for entry in self._assign(
|
|
1087
|
+
item=gaussian_kernel,
|
|
1088
|
+
lower_threshold=lower_threshold,
|
|
1089
|
+
split_components=False
|
|
1090
|
+
):
|
|
1061
1091
|
entry.input_structure = pointindex
|
|
1062
1092
|
entry.centroid = tuple(pt)
|
|
1063
1093
|
assignments.append(entry)
|
|
1064
1094
|
return assignments
|
|
1065
1095
|
|
|
1066
|
-
def
|
|
1096
|
+
def _assign_volume(
|
|
1097
|
+
self,
|
|
1098
|
+
queryvolume: "_volume.Volume",
|
|
1099
|
+
lower_threshold: float,
|
|
1100
|
+
split_components: bool = True,
|
|
1101
|
+
**kwargs
|
|
1102
|
+
) -> List[AssignImageResult]:
|
|
1067
1103
|
"""
|
|
1068
1104
|
Assign an image volume to this parcellation map.
|
|
1069
1105
|
|
|
1070
|
-
Parameters
|
|
1106
|
+
Parameters
|
|
1071
1107
|
-----------
|
|
1108
|
+
queryvolume: Volume
|
|
1109
|
+
the volume to be compared with maps
|
|
1072
1110
|
minsize_voxel: int, default: 1
|
|
1073
1111
|
Minimum voxel size of image components to be taken into account.
|
|
1074
1112
|
lower_threshold: float, default: 0
|
|
1075
1113
|
Lower threshold on values in the statistical map. Values smaller than
|
|
1076
1114
|
this threshold will be excluded from the assignment computation.
|
|
1115
|
+
split_components: bool, default: True
|
|
1116
|
+
Whether to split the query volume into disjoint components.
|
|
1077
1117
|
"""
|
|
1078
|
-
|
|
1118
|
+
# TODO: split_components is not known to `assign`
|
|
1119
|
+
# TODO: `minsize_voxel` is not used here. Consider the implementation of `assign` again.
|
|
1120
|
+
if kwargs:
|
|
1121
|
+
logger.info(f"The keywords {[k for k in kwargs]} are not passed on during volume assignment.")
|
|
1079
1122
|
|
|
1080
|
-
|
|
1081
|
-
# resample query image into this image's voxel space, if required
|
|
1082
|
-
if (img.affine - affine).sum() == 0:
|
|
1083
|
-
return img
|
|
1084
|
-
else:
|
|
1085
|
-
interp = "nearest" \
|
|
1086
|
-
if issubclass(np.asanyarray(img.dataobj).dtype.type, np.integer) \
|
|
1087
|
-
else "linear"
|
|
1088
|
-
return image.resample_img(
|
|
1089
|
-
img,
|
|
1090
|
-
target_affine=affine,
|
|
1091
|
-
target_shape=shape,
|
|
1092
|
-
interpolation=interp,
|
|
1093
|
-
)
|
|
1123
|
+
assert queryvolume.space == self.space, ValueError("Assigned volume must be in the same space as the map.")
|
|
1094
1124
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
)
|
|
1125
|
+
if split_components:
|
|
1126
|
+
iter_components = lambda arr: connected_components(arr)
|
|
1127
|
+
else:
|
|
1128
|
+
iter_components = lambda arr: [(0, arr)]
|
|
1129
|
+
|
|
1130
|
+
queryimg = queryvolume.fetch()
|
|
1131
|
+
assignments = []
|
|
1132
|
+
all_indices = [
|
|
1133
|
+
index
|
|
1134
|
+
for regionindices in self._indices.values()
|
|
1135
|
+
for index in regionindices
|
|
1136
|
+
]
|
|
1137
|
+
with QUIET and provider.SubvolumeProvider.UseCaching():
|
|
1138
|
+
for index in siibra_tqdm(
|
|
1139
|
+
all_indices,
|
|
1140
|
+
desc=f"Assigning {queryvolume} to {self}",
|
|
1141
|
+
disable=len(all_indices) < 5,
|
|
1142
|
+
unit="map",
|
|
1143
|
+
leave=False
|
|
1144
|
+
):
|
|
1145
|
+
region_map = self.fetch(index=index)
|
|
1146
|
+
region_map_arr = np.asanyarray(region_map.dataobj)
|
|
1147
|
+
# the shape and affine are checked by `nilearn.image.resample_to_img()`
|
|
1148
|
+
# and returns the original data if resampling is not necessary.
|
|
1149
|
+
queryimgarr_res = np.asanyarray(
|
|
1150
|
+
resample_img_to_img(queryimg, region_map).dataobj
|
|
1151
|
+
)
|
|
1152
|
+
for compmode, voxelmask in iter_components(queryimgarr_res):
|
|
1153
|
+
scores = compare_arrays(
|
|
1154
|
+
voxelmask,
|
|
1155
|
+
region_map.affine, # after resampling, both should have the same affine
|
|
1156
|
+
region_map_arr,
|
|
1157
|
+
region_map.affine
|
|
1158
|
+
)
|
|
1159
|
+
component_position = np.array(np.where(voxelmask)).T.mean(0)
|
|
1160
|
+
if scores.intersection_over_union > lower_threshold:
|
|
1161
|
+
assignments.append(
|
|
1162
|
+
AssignImageResult(
|
|
1163
|
+
input_structure=compmode,
|
|
1164
|
+
centroid=tuple(component_position.round(2)),
|
|
1165
|
+
volume=index.volume,
|
|
1166
|
+
fragment=index.fragment,
|
|
1167
|
+
map_value=index.label,
|
|
1168
|
+
**asdict(scores)
|
|
1169
|
+
)
|
|
1170
|
+
)
|
|
1135
1171
|
|
|
1136
1172
|
return assignments
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def from_volume(
|
|
1176
|
+
name: str,
|
|
1177
|
+
volume: Union[_volume.Volume, List[_volume.Volume]],
|
|
1178
|
+
regionnames: List[str],
|
|
1179
|
+
regionlabels: List[int],
|
|
1180
|
+
parcellation_spec: Union[str, "parcellation.Parcellation"] = None
|
|
1181
|
+
) -> 'Map':
|
|
1182
|
+
"""
|
|
1183
|
+
Add a custom labelled parcellation map to siibra from a labelled NIfTI file.
|
|
1184
|
+
|
|
1185
|
+
Parameters
|
|
1186
|
+
------------
|
|
1187
|
+
name: str
|
|
1188
|
+
Human-readable name of the parcellation.
|
|
1189
|
+
volume: Volume, or a list of Volumes.
|
|
1190
|
+
space_spec: str, Space
|
|
1191
|
+
Specification of the reference space (space object, name, keyword, or id - e.g. 'mni152').
|
|
1192
|
+
regionnames: list[str]
|
|
1193
|
+
List of human-readable names of the mapped regions.
|
|
1194
|
+
regionlabels: list[int]
|
|
1195
|
+
List of integer labels in the nifti file corresponding to the list of regions.
|
|
1196
|
+
parcellation: str or Parcellation. Optional.
|
|
1197
|
+
If the related parcellation already defined or preconfigured in siibra.
|
|
1198
|
+
"""
|
|
1199
|
+
# providers and map indices
|
|
1200
|
+
providers = []
|
|
1201
|
+
volumes = volume if isinstance(volume, list) else [volume]
|
|
1202
|
+
map_space = volumes[0].space
|
|
1203
|
+
assert all(v.space == map_space for v in volumes), "Volumes have to be in the same space"
|
|
1204
|
+
for vol_idx, vol in enumerate(volumes):
|
|
1205
|
+
image = vol.fetch()
|
|
1206
|
+
arr = np.asanyarray(image.dataobj)
|
|
1207
|
+
labels_in_volume = np.unique(arr)[1:].astype('int')
|
|
1208
|
+
|
|
1209
|
+
# populate region indices from given name/label lists
|
|
1210
|
+
indices = dict()
|
|
1211
|
+
for label, regionname in zip(regionlabels, regionnames):
|
|
1212
|
+
if label not in labels_in_volume:
|
|
1213
|
+
logger.warning(
|
|
1214
|
+
f"Label {label} not mapped in the provided NIfTI volume -> "
|
|
1215
|
+
f"region '{regionname} will not be in the map."
|
|
1216
|
+
)
|
|
1217
|
+
elif label in [v[0]['label'] for v in indices.values() if v[0]['volume'] == vol_idx]:
|
|
1218
|
+
logger.warning(f"Label {label} already defined in the same volume; will not map it to '{regionname}'.")
|
|
1219
|
+
else:
|
|
1220
|
+
assert regionname not in indices, f"'{regionname}' must be unique in `regionnames`."
|
|
1221
|
+
indices[regionname] = [{'volume': vol_idx, 'label': label}]
|
|
1222
|
+
|
|
1223
|
+
# check for any remaining labels in the NIfTI volume
|
|
1224
|
+
unnamed_labels = list(set(labels_in_volume) - set(regionlabels))
|
|
1225
|
+
if unnamed_labels:
|
|
1226
|
+
logger.warning(
|
|
1227
|
+
f"The following labels appear in the NIfTI volume {vol_idx}, but not in "
|
|
1228
|
+
f"the specified regions: {', '.join(str(lb) for lb in unnamed_labels)}. "
|
|
1229
|
+
"They will be removed from the nifti volume."
|
|
1230
|
+
)
|
|
1231
|
+
for label in unnamed_labels:
|
|
1232
|
+
arr[arr == label] = 0
|
|
1233
|
+
providers.extend(vol._providers.values())
|
|
1234
|
+
|
|
1235
|
+
# parcellation
|
|
1236
|
+
if parcellation_spec is None:
|
|
1237
|
+
parcellation_spec = name
|
|
1238
|
+
try:
|
|
1239
|
+
parcobj = parcellation.Parcellation.registry().get(parcellation_spec)
|
|
1240
|
+
logger.info(f"Using '{parcellation_spec}', siibra decoded the parcellation as '{parcobj}'")
|
|
1241
|
+
except Exception:
|
|
1242
|
+
logger.info(
|
|
1243
|
+
f"Using '{parcellation_spec}', siibra could not decode the "
|
|
1244
|
+
" parcellation. Building a new parcellation."
|
|
1245
|
+
)
|
|
1246
|
+
# build a new parcellation
|
|
1247
|
+
parcobj = parcellation.Parcellation(
|
|
1248
|
+
identifier=generate_uuid(','.join(regionnames)),
|
|
1249
|
+
name=name,
|
|
1250
|
+
species=vol.space.species,
|
|
1251
|
+
regions=list(map(_region.Region, regionnames)),
|
|
1252
|
+
)
|
|
1253
|
+
if parcobj.key not in list(parcellation.Parcellation.registry()):
|
|
1254
|
+
parcellation.Parcellation.registry().add(parcobj.key, parcobj)
|
|
1255
|
+
|
|
1256
|
+
for region in siibra_tqdm(
|
|
1257
|
+
indices.keys(),
|
|
1258
|
+
desc="Checking if provided regions are defined in the parcellation."
|
|
1259
|
+
):
|
|
1260
|
+
try:
|
|
1261
|
+
_ = parcobj.get_region(region)
|
|
1262
|
+
except Exception:
|
|
1263
|
+
logger.warning(f"'{region}' is missing in the parcellation.")
|
|
1264
|
+
|
|
1265
|
+
# build the parcellation map object
|
|
1266
|
+
parcmap = Map(
|
|
1267
|
+
identifier=generate_uuid(name),
|
|
1268
|
+
name=f"{name} map in {map_space.name}",
|
|
1269
|
+
space_spec={"@id": map_space.id},
|
|
1270
|
+
parcellation_spec={'name': parcobj.name},
|
|
1271
|
+
indices=indices,
|
|
1272
|
+
volumes=volumes
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
# add it to siibra's registry
|
|
1276
|
+
Map.registry().add(parcmap.key, parcmap)
|
|
1277
|
+
|
|
1278
|
+
# return the map - note that it has a pointer to the parcellation
|
|
1279
|
+
return parcmap
|