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.
- siibra/VERSION +1 -1
- siibra/__init__.py +2 -0
- siibra/commons.py +53 -8
- siibra/configuration/configuration.py +21 -17
- siibra/configuration/factory.py +95 -19
- siibra/core/atlas.py +11 -8
- siibra/core/concept.py +41 -8
- siibra/core/parcellation.py +94 -43
- siibra/core/region.py +160 -187
- siibra/core/space.py +44 -39
- siibra/features/__init__.py +19 -19
- siibra/features/anchor.py +9 -6
- siibra/features/connectivity/__init__.py +0 -8
- siibra/features/connectivity/functional_connectivity.py +11 -3
- siibra/features/{basetypes → connectivity}/regional_connectivity.py +46 -33
- siibra/features/connectivity/streamline_counts.py +3 -2
- siibra/features/connectivity/streamline_lengths.py +3 -2
- siibra/features/{basetypes → dataset}/__init__.py +2 -0
- siibra/features/{external → dataset}/ebrains.py +3 -3
- siibra/features/feature.py +420 -0
- siibra/{samplers → features/image}/__init__.py +7 -1
- siibra/features/{basetypes/volume_of_interest.py → image/image.py} +12 -7
- siibra/features/{external/__init__.py → image/sections.py} +8 -5
- siibra/features/image/volume_of_interest.py +70 -0
- siibra/features/{cellular → tabular}/__init__.py +7 -11
- siibra/features/{cellular → tabular}/bigbrain_intensity_profile.py +5 -2
- siibra/features/{cellular → tabular}/cell_density_profile.py +6 -2
- siibra/features/{basetypes → tabular}/cortical_profile.py +48 -41
- siibra/features/{molecular → tabular}/gene_expression.py +5 -2
- siibra/features/{cellular → tabular}/layerwise_bigbrain_intensities.py +6 -2
- siibra/features/{cellular → tabular}/layerwise_cell_density.py +9 -3
- siibra/features/{molecular → tabular}/receptor_density_fingerprint.py +3 -2
- siibra/features/{molecular → tabular}/receptor_density_profile.py +6 -2
- siibra/features/tabular/regional_timeseries_activity.py +213 -0
- siibra/features/{basetypes → tabular}/tabular.py +14 -9
- siibra/livequeries/allen.py +1 -1
- siibra/livequeries/bigbrain.py +2 -3
- siibra/livequeries/ebrains.py +3 -9
- siibra/livequeries/query.py +1 -1
- siibra/locations/location.py +4 -3
- siibra/locations/point.py +21 -17
- siibra/locations/pointset.py +2 -2
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +8 -2
- siibra/retrieval/datasets.py +149 -29
- siibra/retrieval/repositories.py +19 -8
- siibra/retrieval/requests.py +98 -116
- siibra/volumes/gifti.py +26 -11
- siibra/volumes/neuroglancer.py +35 -19
- siibra/volumes/nifti.py +8 -9
- siibra/volumes/parcellationmap.py +341 -184
- siibra/volumes/sparsemap.py +67 -53
- siibra/volumes/volume.py +25 -13
- {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/METADATA +4 -3
- siibra-0.4a46.dist-info/RECORD +69 -0
- {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/WHEEL +1 -1
- siibra/features/basetypes/feature.py +0 -248
- siibra/features/fibres/__init__.py +0 -14
- siibra/features/functional/__init__.py +0 -14
- siibra/features/molecular/__init__.py +0 -26
- siibra/samplers/bigbrain.py +0 -181
- siibra-0.4a33.dist-info/RECORD +0 -71
- {siibra-0.4a33.dist-info → siibra-0.4a46.dist-info}/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
|
92
|
+
identifier: str
|
|
71
93
|
Unique identifier of the parcellation
|
|
72
|
-
name
|
|
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
|
|
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
|
|
108
|
+
volumes: list[Volume]
|
|
87
109
|
parcellation volumes
|
|
88
|
-
shortname: str
|
|
89
|
-
Shortform of human-readable name
|
|
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
|
|
114
|
+
modality: str, default: None
|
|
93
115
|
Specification of the modality used for creating the parcellation
|
|
94
116
|
publications: list
|
|
95
|
-
List of
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
179
|
-
which match the given
|
|
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.
|
|
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 =
|
|
197
|
-
"""
|
|
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.
|
|
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}
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
640
|
+
region_specs: iterable(regions), optional
|
|
546
641
|
Optional parameter to only color the desired regions.
|
|
547
642
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
681
|
+
The probability distribution is approximated from the region mask based
|
|
682
|
+
on the squared distance transform.
|
|
588
683
|
|
|
589
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
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,
|
|
690
|
-
A spatial object defined in the same physical reference space as
|
|
691
|
-
parcellation map, which could be a point, set of points, or
|
|
692
|
-
If it is an image, it will be resampled to the same voxel
|
|
693
|
-
|
|
694
|
-
Resampling will use linear interpolation for float
|
|
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
|
|
700
|
-
this threshold will be excluded from the assignment computation.
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
assignments
|
|
705
|
-
A table of associated regions and their scores per component found
|
|
706
|
-
or per coordinate
|
|
707
|
-
|
|
708
|
-
- Value: Maximum value of the voxels in the map covered by an
|
|
709
|
-
|
|
710
|
-
- Pearson correlation coefficient between the brain region map
|
|
711
|
-
|
|
712
|
-
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
923
|
-
|
|
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
|