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");
|
|
@@ -12,27 +12,26 @@
|
|
|
12
12
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
|
-
"""Handles reading and preparing nifti files."""
|
|
16
15
|
|
|
17
|
-
from . import
|
|
16
|
+
from . import provider as _provider
|
|
18
17
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
18
|
+
from ...commons import logger, resample_img_to_img
|
|
19
|
+
from ...retrieval import requests
|
|
20
|
+
from ...locations import pointcloud, boundingbox as _boundingbox
|
|
22
21
|
|
|
23
|
-
from typing import Union, Dict
|
|
22
|
+
from typing import Union, Dict, Tuple
|
|
24
23
|
import nibabel as nib
|
|
25
24
|
import os
|
|
26
25
|
import numpy as np
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
class NiftiProvider(
|
|
28
|
+
class NiftiProvider(_provider.VolumeProvider, srctype="nii"):
|
|
30
29
|
|
|
31
|
-
def __init__(self, src: Union[str, Dict[str, str], nib.Nifti1Image]):
|
|
30
|
+
def __init__(self, src: Union[str, Dict[str, str], nib.Nifti1Image, Tuple[np.ndarray, np.ndarray]]):
|
|
32
31
|
"""
|
|
33
32
|
Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
|
|
34
33
|
"""
|
|
35
|
-
|
|
34
|
+
_provider.VolumeProvider.__init__(self)
|
|
36
35
|
|
|
37
36
|
self._init_url: Union[str, Dict[str, str]] = None
|
|
38
37
|
|
|
@@ -49,9 +48,13 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
49
48
|
self._img_loaders = {None: loader(src)}
|
|
50
49
|
elif isinstance(src, dict): # assuming multiple for fragment images
|
|
51
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)}
|
|
52
55
|
else:
|
|
53
56
|
raise ValueError(f"Invalid source specification for {self.__class__}: {src}")
|
|
54
|
-
if not isinstance(src, nib.Nifti1Image):
|
|
57
|
+
if not isinstance(src, (nib.Nifti1Image, tuple)):
|
|
55
58
|
self._init_url = src
|
|
56
59
|
|
|
57
60
|
@property
|
|
@@ -62,11 +65,15 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
62
65
|
def fragments(self):
|
|
63
66
|
return [k for k in self._img_loaders if k is not None]
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
def boundingbox(self):
|
|
68
|
+
def get_boundingbox(self, **fetch_kwargs) -> "_boundingbox.BoundingBox":
|
|
67
69
|
"""
|
|
68
|
-
Return the bounding box in physical coordinates
|
|
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
|
|
70
77
|
"""
|
|
71
78
|
bbox = None
|
|
72
79
|
for loader in self._img_loaders.values():
|
|
@@ -77,17 +84,19 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
77
84
|
f"bounding box considers only {img.shape[:3]}"
|
|
78
85
|
)
|
|
79
86
|
shape = img.shape[:3]
|
|
80
|
-
next_bbox = _boundingbox.BoundingBox(
|
|
81
|
-
|
|
87
|
+
next_bbox = _boundingbox.BoundingBox(
|
|
88
|
+
(0, 0, 0), shape, space=None
|
|
89
|
+
).transform(img.affine)
|
|
82
90
|
bbox = next_bbox if bbox is None else bbox.union(next_bbox)
|
|
83
91
|
return bbox
|
|
84
92
|
|
|
85
93
|
def _merge_fragments(self) -> nib.Nifti1Image:
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
"""
|
|
95
|
+
Merge all fragments this volume contains into one Nifti1Image.
|
|
96
|
+
"""
|
|
97
|
+
bbox = self.get_boundingbox(clip=False, background=0.0)
|
|
88
98
|
num_conflicts = 0
|
|
89
99
|
result = None
|
|
90
|
-
|
|
91
100
|
for loader in self._img_loaders.values():
|
|
92
101
|
img = loader()
|
|
93
102
|
if result is None:
|
|
@@ -95,25 +104,25 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
95
104
|
s0 = np.identity(4)
|
|
96
105
|
s0[:3, -1] = list(bbox.minpoint.transform(np.linalg.inv(img.affine)))
|
|
97
106
|
result_affine = np.dot(img.affine, s0) # adjust global bounding box offset to get global affine
|
|
98
|
-
voxdims = np.asanyarray(
|
|
107
|
+
voxdims = np.asanyarray(np.ceil(
|
|
108
|
+
bbox.transform(np.linalg.inv(result_affine)).shape
|
|
109
|
+
), dtype="int")
|
|
99
110
|
result_arr = np.zeros(voxdims, dtype=img.dataobj.dtype)
|
|
100
111
|
result = nib.Nifti1Image(dataobj=result_arr, affine=result_affine)
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
) + .5).astype('int'),
|
|
109
|
-
4, axis=0
|
|
110
|
-
)
|
|
111
|
-
num_conflicts += np.count_nonzero(result_arr[Xt, Yt, Zt])
|
|
112
|
-
result_arr[Xt, Yt, Zt] = arr[Xs, Ys, Zs]
|
|
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]
|
|
113
119
|
|
|
114
120
|
if num_conflicts > 0:
|
|
115
121
|
num_voxels = np.count_nonzero(result_arr)
|
|
116
|
-
logger.warning(
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"Merging fragments required to overwrite {num_conflicts} "
|
|
124
|
+
f"conflicting voxels ({num_conflicts / num_voxels * 100.:2.1f}%)."
|
|
125
|
+
)
|
|
117
126
|
|
|
118
127
|
return result
|
|
119
128
|
|
|
@@ -160,24 +169,28 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
160
169
|
else:
|
|
161
170
|
assert len(self._img_loaders) > 0
|
|
162
171
|
fragment_name, loader = next(iter(self._img_loaders.items()))
|
|
163
|
-
if
|
|
172
|
+
if (fragment_name is not None) and (fragment is not None):
|
|
164
173
|
assert fragment.lower() in fragment_name.lower()
|
|
165
174
|
result = loader()
|
|
166
175
|
|
|
167
176
|
if voi is not None:
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
170
181
|
shift = np.identity(4)
|
|
171
182
|
shift[:3, -1] = bb_vox.minpoint
|
|
172
183
|
result = nib.Nifti1Image(
|
|
173
184
|
dataobj=result.dataobj[x0:x1, y0:y1, z0:z1],
|
|
174
185
|
affine=np.dot(result.affine, shift),
|
|
186
|
+
dtype=result.header.get_data_dtype(),
|
|
175
187
|
)
|
|
176
188
|
|
|
177
189
|
if label is not None:
|
|
178
190
|
result = nib.Nifti1Image(
|
|
179
191
|
(result.get_fdata() == label).astype('uint8'),
|
|
180
|
-
result.affine
|
|
192
|
+
result.affine,
|
|
193
|
+
dtype='uint8'
|
|
181
194
|
)
|
|
182
195
|
|
|
183
196
|
return result
|
|
@@ -188,15 +201,22 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
188
201
|
"NiftiVolume does not support to specify different image resolutions"
|
|
189
202
|
)
|
|
190
203
|
try:
|
|
191
|
-
|
|
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}")
|
|
192
209
|
except AttributeError as e:
|
|
193
210
|
logger.error(
|
|
194
|
-
f"Invalid object type {type(self.
|
|
211
|
+
f"Invalid object type/s {[type(loader()) for loader in self._img_loaders.values()]} of image for {self}."
|
|
195
212
|
)
|
|
196
213
|
raise (e)
|
|
197
214
|
|
|
198
215
|
def is_float(self):
|
|
199
|
-
return
|
|
216
|
+
return all(
|
|
217
|
+
loader().dataobj.dtype.kind == "f"
|
|
218
|
+
for loader in self._img_loaders.values()
|
|
219
|
+
)
|
|
200
220
|
|
|
201
221
|
def find_peaks(self, min_distance_mm=5):
|
|
202
222
|
"""
|
|
@@ -209,11 +229,11 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
209
229
|
|
|
210
230
|
Returns:
|
|
211
231
|
--------
|
|
212
|
-
|
|
232
|
+
PointCloud
|
|
213
233
|
"""
|
|
214
234
|
|
|
215
235
|
from skimage.feature.peak import peak_local_max
|
|
216
|
-
from
|
|
236
|
+
from ...commons import affine_scaling
|
|
217
237
|
|
|
218
238
|
img = self.fetch()
|
|
219
239
|
dist = int(min_distance_mm / affine_scaling(img.affine) + 0.5)
|
|
@@ -223,7 +243,7 @@ class NiftiProvider(volume.VolumeProvider, srctype="nii"):
|
|
|
223
243
|
min_distance=dist,
|
|
224
244
|
)
|
|
225
245
|
return (
|
|
226
|
-
|
|
246
|
+
pointcloud.PointCloud(
|
|
227
247
|
[np.dot(img.affine, [x, y, z, 1])[:3] for x, y, z in voxels],
|
|
228
248
|
space=self.space,
|
|
229
249
|
),
|
|
@@ -237,7 +257,7 @@ class ZipContainedNiftiProvider(NiftiProvider, srctype="zip/nii"):
|
|
|
237
257
|
"""
|
|
238
258
|
Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
|
|
239
259
|
"""
|
|
240
|
-
|
|
260
|
+
_provider.VolumeProvider.__init__(self)
|
|
241
261
|
zipurl, zipped_file = src.split(" ")
|
|
242
262
|
req = requests.ZipfileRequest(zipurl, zipped_file)
|
|
243
263
|
self._img_loaders = {None: lambda req=req: req.data}
|
|
@@ -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
|