siibra 1.0a1__1-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 (84) hide show
  1. siibra/VERSION +1 -0
  2. siibra/__init__.py +164 -0
  3. siibra/commons.py +823 -0
  4. siibra/configuration/__init__.py +17 -0
  5. siibra/configuration/configuration.py +189 -0
  6. siibra/configuration/factory.py +589 -0
  7. siibra/core/__init__.py +16 -0
  8. siibra/core/assignment.py +110 -0
  9. siibra/core/atlas.py +239 -0
  10. siibra/core/concept.py +308 -0
  11. siibra/core/parcellation.py +387 -0
  12. siibra/core/region.py +1223 -0
  13. siibra/core/space.py +131 -0
  14. siibra/core/structure.py +111 -0
  15. siibra/exceptions.py +63 -0
  16. siibra/experimental/__init__.py +19 -0
  17. siibra/experimental/contour.py +61 -0
  18. siibra/experimental/cortical_profile_sampler.py +57 -0
  19. siibra/experimental/patch.py +98 -0
  20. siibra/experimental/plane3d.py +256 -0
  21. siibra/explorer/__init__.py +17 -0
  22. siibra/explorer/url.py +222 -0
  23. siibra/explorer/util.py +87 -0
  24. siibra/features/__init__.py +117 -0
  25. siibra/features/anchor.py +224 -0
  26. siibra/features/connectivity/__init__.py +33 -0
  27. siibra/features/connectivity/functional_connectivity.py +57 -0
  28. siibra/features/connectivity/regional_connectivity.py +494 -0
  29. siibra/features/connectivity/streamline_counts.py +27 -0
  30. siibra/features/connectivity/streamline_lengths.py +27 -0
  31. siibra/features/connectivity/tracing_connectivity.py +30 -0
  32. siibra/features/dataset/__init__.py +17 -0
  33. siibra/features/dataset/ebrains.py +90 -0
  34. siibra/features/feature.py +970 -0
  35. siibra/features/image/__init__.py +27 -0
  36. siibra/features/image/image.py +115 -0
  37. siibra/features/image/sections.py +26 -0
  38. siibra/features/image/volume_of_interest.py +88 -0
  39. siibra/features/tabular/__init__.py +24 -0
  40. siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
  41. siibra/features/tabular/cell_density_profile.py +298 -0
  42. siibra/features/tabular/cortical_profile.py +322 -0
  43. siibra/features/tabular/gene_expression.py +257 -0
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
  45. siibra/features/tabular/layerwise_cell_density.py +95 -0
  46. siibra/features/tabular/receptor_density_fingerprint.py +192 -0
  47. siibra/features/tabular/receptor_density_profile.py +110 -0
  48. siibra/features/tabular/regional_timeseries_activity.py +294 -0
  49. siibra/features/tabular/tabular.py +139 -0
  50. siibra/livequeries/__init__.py +19 -0
  51. siibra/livequeries/allen.py +352 -0
  52. siibra/livequeries/bigbrain.py +197 -0
  53. siibra/livequeries/ebrains.py +145 -0
  54. siibra/livequeries/query.py +49 -0
  55. siibra/locations/__init__.py +91 -0
  56. siibra/locations/boundingbox.py +454 -0
  57. siibra/locations/location.py +115 -0
  58. siibra/locations/point.py +344 -0
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +27 -0
  61. siibra/retrieval/cache.py +233 -0
  62. siibra/retrieval/datasets.py +389 -0
  63. siibra/retrieval/exceptions/__init__.py +27 -0
  64. siibra/retrieval/repositories.py +769 -0
  65. siibra/retrieval/requests.py +659 -0
  66. siibra/vocabularies/__init__.py +45 -0
  67. siibra/vocabularies/gene_names.json +29176 -0
  68. siibra/vocabularies/receptor_symbols.json +210 -0
  69. siibra/vocabularies/region_aliases.json +460 -0
  70. siibra/volumes/__init__.py +23 -0
  71. siibra/volumes/parcellationmap.py +1279 -0
  72. siibra/volumes/providers/__init__.py +20 -0
  73. siibra/volumes/providers/freesurfer.py +113 -0
  74. siibra/volumes/providers/gifti.py +165 -0
  75. siibra/volumes/providers/neuroglancer.py +736 -0
  76. siibra/volumes/providers/nifti.py +266 -0
  77. siibra/volumes/providers/provider.py +107 -0
  78. siibra/volumes/sparsemap.py +468 -0
  79. siibra/volumes/volume.py +892 -0
  80. siibra-1.0.0a1.dist-info/LICENSE +201 -0
  81. siibra-1.0.0a1.dist-info/METADATA +160 -0
  82. siibra-1.0.0a1.dist-info/RECORD +84 -0
  83. siibra-1.0.0a1.dist-info/WHEEL +5 -0
  84. siibra-1.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,266 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from . import provider as _provider
17
+
18
+ from ...commons import logger, resample_img_to_img
19
+ from ...retrieval import requests
20
+ from ...locations import pointcloud, boundingbox as _boundingbox
21
+
22
+ from typing import Union, Dict, Tuple
23
+ import nibabel as nib
24
+ import os
25
+ import numpy as np
26
+
27
+
28
+ class NiftiProvider(_provider.VolumeProvider, srctype="nii"):
29
+
30
+ def __init__(self, src: Union[str, Dict[str, str], nib.Nifti1Image, Tuple[np.ndarray, np.ndarray]]):
31
+ """
32
+ Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
33
+ """
34
+ _provider.VolumeProvider.__init__(self)
35
+
36
+ self._init_url: Union[str, Dict[str, str]] = None
37
+
38
+ def loader(url):
39
+ if os.path.isfile(url):
40
+ return lambda fn=url: nib.load(fn)
41
+ else:
42
+ req = requests.HttpRequest(url)
43
+ return lambda req=req: req.data
44
+
45
+ if isinstance(src, nib.Nifti1Image):
46
+ self._img_loaders = {None: lambda img=src: img}
47
+ elif isinstance(src, str): # one single image to load
48
+ self._img_loaders = {None: loader(src)}
49
+ elif isinstance(src, dict): # assuming multiple for fragment images
50
+ self._img_loaders = {lbl: loader(url) for lbl, url in src.items()}
51
+ elif isinstance(src, tuple):
52
+ assert len(src) == 2
53
+ assert all(isinstance(_, np.ndarray) for _ in src)
54
+ self._img_loaders = {None: lambda data=src[0], affine=src[1]: nib.Nifti1Image(data, affine)}
55
+ else:
56
+ raise ValueError(f"Invalid source specification for {self.__class__}: {src}")
57
+ if not isinstance(src, (nib.Nifti1Image, tuple)):
58
+ self._init_url = src
59
+
60
+ @property
61
+ def _url(self) -> Union[str, Dict[str, str]]:
62
+ return self._init_url
63
+
64
+ @property
65
+ def fragments(self):
66
+ return [k for k in self._img_loaders if k is not None]
67
+
68
+ def get_boundingbox(self, **fetch_kwargs) -> "_boundingbox.BoundingBox":
69
+ """
70
+ Return the bounding box in physical coordinates of the union of
71
+ fragments in this nifti volume.
72
+
73
+ Parameters
74
+ ----------
75
+ fetch_kwargs:
76
+ Not used
77
+ """
78
+ bbox = None
79
+ for loader in self._img_loaders.values():
80
+ img = loader()
81
+ if len(img.shape) > 3:
82
+ logger.warning(
83
+ f"N-D NIfTI volume has shape {img.shape}, but "
84
+ f"bounding box considers only {img.shape[:3]}"
85
+ )
86
+ shape = img.shape[:3]
87
+ next_bbox = _boundingbox.BoundingBox(
88
+ (0, 0, 0), shape, space=None
89
+ ).transform(img.affine)
90
+ bbox = next_bbox if bbox is None else bbox.union(next_bbox)
91
+ return bbox
92
+
93
+ def _merge_fragments(self) -> nib.Nifti1Image:
94
+ """
95
+ Merge all fragments this volume contains into one Nifti1Image.
96
+ """
97
+ bbox = self.get_boundingbox(clip=False, background=0.0)
98
+ num_conflicts = 0
99
+ result = None
100
+ for loader in self._img_loaders.values():
101
+ img = loader()
102
+ if result is None:
103
+ # build the empty result image with its own affine and voxel space
104
+ s0 = np.identity(4)
105
+ s0[:3, -1] = list(bbox.minpoint.transform(np.linalg.inv(img.affine)))
106
+ result_affine = np.dot(img.affine, s0) # adjust global bounding box offset to get global affine
107
+ voxdims = np.asanyarray(np.ceil(
108
+ bbox.transform(np.linalg.inv(result_affine)).shape
109
+ ), dtype="int")
110
+ result_arr = np.zeros(voxdims, dtype=img.dataobj.dtype)
111
+ result = nib.Nifti1Image(dataobj=result_arr, affine=result_affine)
112
+
113
+ # resample to merge template and update it
114
+ resampled_img = resample_img_to_img(source_img=img, target_img=result)
115
+ arr = np.asanyarray(resampled_img.dataobj)
116
+ nonzero_voxels = arr != 0
117
+ num_conflicts += np.count_nonzero(result_arr[nonzero_voxels])
118
+ result_arr[nonzero_voxels] = arr[nonzero_voxels]
119
+
120
+ if num_conflicts > 0:
121
+ num_voxels = np.count_nonzero(result_arr)
122
+ logger.warning(
123
+ f"Merging fragments required to overwrite {num_conflicts} "
124
+ f"conflicting voxels ({num_conflicts / num_voxels * 100.:2.1f}%)."
125
+ )
126
+
127
+ return result
128
+
129
+ def fetch(
130
+ self,
131
+ fragment: str = None,
132
+ voi: _boundingbox.BoundingBox = None,
133
+ label: int = None
134
+ ):
135
+ """
136
+ Loads and returns a Nifti1Image object
137
+
138
+ Parameters
139
+ ----------
140
+ fragment: str
141
+ Optional name of a fragment volume to fetch, if any.
142
+ For example, some volumes are split into left and right hemisphere fragments.
143
+ see :func:`~siibra.volumes.Volume.fragments`
144
+ voi : BoundingBox
145
+ optional specification of a volume of interest to fetch.
146
+ label: int, default: None
147
+ Optional: a label index can be provided. Then the mask of the
148
+ 3D volume will be returned, where voxels matching this label
149
+ are marked as "1".
150
+ """
151
+
152
+ result = None
153
+ if len(self._img_loaders) > 1:
154
+ if fragment is None:
155
+ logger.info(
156
+ f"Merging fragments [{', '.join(self._img_loaders.keys())}]. "
157
+ f"You can select one using {self.__class__.__name__}.fetch(fragment=<name>)."
158
+ )
159
+ result = self._merge_fragments()
160
+ else:
161
+ matched_names = [n for n in self._img_loaders if fragment.lower() in n.lower()]
162
+ if len(matched_names) != 1:
163
+ raise ValueError(
164
+ f"Requested fragment '{fragment}' could not be matched uniquely "
165
+ f"to [{', '.join(self._img_loaders)}]"
166
+ )
167
+ else:
168
+ result = self._img_loaders[matched_names[0]]()
169
+ else:
170
+ assert len(self._img_loaders) > 0
171
+ fragment_name, loader = next(iter(self._img_loaders.items()))
172
+ if (fragment_name is not None) and (fragment is not None):
173
+ assert fragment.lower() in fragment_name.lower()
174
+ result = loader()
175
+
176
+ if voi is not None:
177
+ zoom_xyz = np.array(result.header.get_zooms()) # voxel dimensions in xyzt_units
178
+ bb_vox = voi.transform(np.linalg.inv(result.affine))
179
+ x0, y0, z0 = np.floor(np.array(bb_vox.minpoint.coordinate) / zoom_xyz).astype(int)
180
+ x1, y1, z1 = np.ceil(np.array(bb_vox.maxpoint.coordinate) / zoom_xyz).astype(int)
181
+ shift = np.identity(4)
182
+ shift[:3, -1] = bb_vox.minpoint
183
+ result = nib.Nifti1Image(
184
+ dataobj=result.dataobj[x0:x1, y0:y1, z0:z1],
185
+ affine=np.dot(result.affine, shift),
186
+ dtype=result.header.get_data_dtype(),
187
+ )
188
+
189
+ if label is not None:
190
+ result = nib.Nifti1Image(
191
+ (result.get_fdata() == label).astype('uint8'),
192
+ result.affine,
193
+ dtype='uint8'
194
+ )
195
+
196
+ return result
197
+
198
+ def get_shape(self, resolution_mm=None):
199
+ if resolution_mm is not None:
200
+ raise NotImplementedError(
201
+ "NiftiVolume does not support to specify different image resolutions"
202
+ )
203
+ try:
204
+ loader_shapes = {loader().shape for loader in self._img_loaders.values()}
205
+ if len(loader_shapes) == 1:
206
+ return next(iter(loader_shapes))
207
+ else:
208
+ raise RuntimeError(f"Fragments have different shapes: {loader_shapes}")
209
+ except AttributeError as e:
210
+ logger.error(
211
+ f"Invalid object type/s {[type(loader()) for loader in self._img_loaders.values()]} of image for {self}."
212
+ )
213
+ raise (e)
214
+
215
+ def is_float(self):
216
+ return all(
217
+ loader().dataobj.dtype.kind == "f"
218
+ for loader in self._img_loaders.values()
219
+ )
220
+
221
+ def find_peaks(self, min_distance_mm=5):
222
+ """
223
+ Find peaks in the image data.
224
+
225
+ Parameters
226
+ ----------
227
+ min_distance_mm : float
228
+ Minimum distance between peaks in mm
229
+
230
+ Returns:
231
+ --------
232
+ PointCloud
233
+ """
234
+
235
+ from skimage.feature.peak import peak_local_max
236
+ from ...commons import affine_scaling
237
+
238
+ img = self.fetch()
239
+ dist = int(min_distance_mm / affine_scaling(img.affine) + 0.5)
240
+ voxels = peak_local_max(
241
+ img.get_fdata(),
242
+ exclude_border=False,
243
+ min_distance=dist,
244
+ )
245
+ return (
246
+ pointcloud.PointCloud(
247
+ [np.dot(img.affine, [x, y, z, 1])[:3] for x, y, z in voxels],
248
+ space=self.space,
249
+ ),
250
+ img,
251
+ )
252
+
253
+
254
+ class ZipContainedNiftiProvider(NiftiProvider, srctype="zip/nii"):
255
+
256
+ def __init__(self, src: str):
257
+ """
258
+ Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
259
+ """
260
+ _provider.VolumeProvider.__init__(self)
261
+ zipurl, zipped_file = src.split(" ")
262
+ req = requests.ZipfileRequest(zipurl, zipped_file)
263
+ self._img_loaders = {None: lambda req=req: req.data}
264
+
265
+ # required for self._url property
266
+ self._init_url = src
@@ -0,0 +1,107 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+
18
+ from abc import ABC, abstractmethod
19
+ from typing import TYPE_CHECKING, Union, Dict, List
20
+ from nibabel import Nifti1Image
21
+ import json
22
+ if TYPE_CHECKING:
23
+ from ...locations.boundingbox import BoundingBox
24
+
25
+ # TODO add mesh primitive. Check nibabel implementation? Use trimesh? Do we want to add yet another dependency?
26
+ VolumeData = Union[Nifti1Image, Dict]
27
+
28
+
29
+ class VolumeProvider(ABC):
30
+
31
+ _SUBCLASSES: List[VolumeProvider] = []
32
+
33
+ def __init_subclass__(cls, srctype: str) -> None:
34
+ cls.srctype = srctype
35
+ VolumeProvider._SUBCLASSES.append(cls)
36
+ return super().__init_subclass__()
37
+
38
+ @abstractmethod
39
+ def get_boundingbox(self, clip=True, background=0.0) -> "BoundingBox":
40
+ raise NotImplementedError
41
+
42
+ @property
43
+ def fragments(self) -> List[str]:
44
+ return []
45
+
46
+ @abstractmethod
47
+ def fetch(self, *args, **kwargs) -> VolumeData:
48
+ raise NotImplementedError
49
+
50
+ @property
51
+ @abstractmethod
52
+ def _url(self) -> Union[str, Dict[str, str]]:
53
+ """
54
+ This is needed to provide urls to applications that can utilise such resources directly.
55
+ e.g. siibra-api
56
+ """
57
+ return {}
58
+
59
+
60
+ class SubvolumeProvider(VolumeProvider, srctype="subvolume"):
61
+ """
62
+ This provider wraps around an existing volume provider,
63
+ but is preconfigured to always fetch a fixed subvolume.
64
+ The primary use is to provide a fixed z coordinate
65
+ of a 4D volume provider as a 3D volume under the
66
+ interface of a normal volume provider.
67
+ """
68
+
69
+ _USE_CACHING = False
70
+ _FETCHED_VOLUMES = {}
71
+
72
+ class UseCaching:
73
+ def __enter__(self):
74
+ SubvolumeProvider._USE_CACHING = True
75
+
76
+ def __exit__(self, et, ev, tb):
77
+ SubvolumeProvider._USE_CACHING = False
78
+ SubvolumeProvider._FETCHED_VOLUMES = {}
79
+
80
+ def __init__(self, parent_provider: VolumeProvider, z: int):
81
+ VolumeProvider.__init__(self)
82
+ self.provider = parent_provider
83
+ self.srctype = parent_provider.srctype
84
+ self.z = z
85
+
86
+ def get_boundingbox(self, clip=True, background=0.0, **fetch_kwargs) -> "BoundingBox":
87
+ return self.provider.get_boundingbox(clip=clip, background=background, **fetch_kwargs)
88
+
89
+ def fetch(self, **kwargs):
90
+ # activate caching at the caller using "with SubvolumeProvider.UseCaching():""
91
+ if self.__class__._USE_CACHING:
92
+ data_key = json.dumps(self.provider._url, sort_keys=True) \
93
+ + json.dumps(kwargs, sort_keys=True)
94
+ if data_key not in self.__class__._FETCHED_VOLUMES:
95
+ vol = self.provider.fetch(**kwargs)
96
+ self.__class__._FETCHED_VOLUMES[data_key] = vol
97
+ vol = self.__class__._FETCHED_VOLUMES[data_key]
98
+ else:
99
+ vol = self.provider.fetch(**kwargs)
100
+ return vol.slicer[:, :, :, self.z]
101
+
102
+ def __getattr__(self, attr):
103
+ return self.provider.__getattribute__(attr)
104
+
105
+ @property
106
+ def _url(self) -> Union[str, Dict[str, str]]:
107
+ return super()._url