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.

Files changed (83) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +20 -12
  3. siibra/commons.py +145 -90
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +22 -17
  6. siibra/configuration/factory.py +177 -128
  7. siibra/core/__init__.py +1 -8
  8. siibra/core/{relation_qualification.py → assignment.py} +17 -14
  9. siibra/core/atlas.py +66 -35
  10. siibra/core/concept.py +81 -39
  11. siibra/core/parcellation.py +83 -67
  12. siibra/core/region.py +569 -263
  13. siibra/core/space.py +7 -39
  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 +16 -0
  22. siibra/explorer/url.py +112 -52
  23. siibra/explorer/util.py +31 -9
  24. siibra/features/__init__.py +73 -8
  25. siibra/features/anchor.py +75 -196
  26. siibra/features/connectivity/__init__.py +1 -1
  27. siibra/features/connectivity/functional_connectivity.py +2 -2
  28. siibra/features/connectivity/regional_connectivity.py +99 -10
  29. siibra/features/connectivity/streamline_counts.py +1 -1
  30. siibra/features/connectivity/streamline_lengths.py +1 -1
  31. siibra/features/connectivity/tracing_connectivity.py +1 -1
  32. siibra/features/dataset/__init__.py +1 -1
  33. siibra/features/dataset/ebrains.py +3 -3
  34. siibra/features/feature.py +219 -110
  35. siibra/features/image/__init__.py +1 -1
  36. siibra/features/image/image.py +21 -13
  37. siibra/features/image/sections.py +1 -1
  38. siibra/features/image/volume_of_interest.py +1 -1
  39. siibra/features/tabular/__init__.py +1 -1
  40. siibra/features/tabular/bigbrain_intensity_profile.py +24 -13
  41. siibra/features/tabular/cell_density_profile.py +111 -69
  42. siibra/features/tabular/cortical_profile.py +82 -16
  43. siibra/features/tabular/gene_expression.py +117 -6
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +7 -9
  45. siibra/features/tabular/layerwise_cell_density.py +9 -24
  46. siibra/features/tabular/receptor_density_fingerprint.py +11 -6
  47. siibra/features/tabular/receptor_density_profile.py +12 -15
  48. siibra/features/tabular/regional_timeseries_activity.py +74 -18
  49. siibra/features/tabular/tabular.py +17 -8
  50. siibra/livequeries/__init__.py +1 -7
  51. siibra/livequeries/allen.py +139 -77
  52. siibra/livequeries/bigbrain.py +104 -128
  53. siibra/livequeries/ebrains.py +7 -4
  54. siibra/livequeries/query.py +1 -2
  55. siibra/locations/__init__.py +32 -25
  56. siibra/locations/boundingbox.py +153 -127
  57. siibra/locations/location.py +45 -80
  58. siibra/locations/point.py +97 -83
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +1 -1
  61. siibra/retrieval/cache.py +107 -13
  62. siibra/retrieval/datasets.py +9 -14
  63. siibra/retrieval/exceptions/__init__.py +2 -1
  64. siibra/retrieval/repositories.py +147 -53
  65. siibra/retrieval/requests.py +64 -29
  66. siibra/vocabularies/__init__.py +2 -2
  67. siibra/volumes/__init__.py +7 -9
  68. siibra/volumes/parcellationmap.py +396 -253
  69. siibra/volumes/providers/__init__.py +20 -0
  70. siibra/volumes/providers/freesurfer.py +113 -0
  71. siibra/volumes/{gifti.py → providers/gifti.py} +29 -18
  72. siibra/volumes/{neuroglancer.py → providers/neuroglancer.py} +204 -92
  73. siibra/volumes/{nifti.py → providers/nifti.py} +64 -44
  74. siibra/volumes/providers/provider.py +107 -0
  75. siibra/volumes/sparsemap.py +159 -260
  76. siibra/volumes/volume.py +720 -152
  77. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/METADATA +25 -28
  78. siibra-1.0.0a1.dist-info/RECORD +84 -0
  79. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/WHEEL +1 -1
  80. siibra/locations/pointset.py +0 -198
  81. siibra-0.5a2.dist-info/RECORD +0 -74
  82. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/LICENSE +0 -0
  83. {siibra-0.5a2.dist-info → siibra-1.0.0a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
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 volume
16
+ from . import provider as _provider
18
17
 
19
- from ..commons import logger
20
- from ..retrieval import requests
21
- from ..locations import pointset, boundingbox as _boundingbox
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(volume.VolumeProvider, srctype="nii"):
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
- volume.VolumeProvider.__init__(self)
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
- @property
66
- def boundingbox(self):
68
+ def get_boundingbox(self, **fetch_kwargs) -> "_boundingbox.BoundingBox":
67
69
  """
68
- Return the bounding box in physical coordinates
69
- of the union of fragments in this nifti volume.
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((0, 0, 0), shape, space=None) \
81
- .transform(img.affine)
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
- # TODO this only performs nearest neighbor interpolation, optimized for float types.
87
- bbox = self.boundingbox
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(bbox.transform(result_affine).shape, dtype="int")
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
- arr = np.asanyarray(img.dataobj)
103
- Xs, Ys, Zs = np.where(arr != 0)
104
- Xt, Yt, Zt, _ = np.split(
105
- (np.dot(
106
- np.linalg.inv(result_affine),
107
- np.dot(img.affine, np.c_[Xs, Ys, Zs, Zs * 0 + 1].T)
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(f"Merging fragments required to overwrite {num_conflicts} conflicting voxels ({num_conflicts/num_voxels*100.:2.1f}%).")
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 all(s is not None for s in [fragment, fragment_name]):
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
- bb_vox = voi.transform_bbox(np.linalg.inv(result.affine))
169
- (x0, y0, z0), (x1, y1, z1) = bb_vox.minpoint, bb_vox.maxpoint
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
- return self.image.shape
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.image)} of image for {self} {self.name}"
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 self.image.dataobj.dtype.kind == "f"
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
- PointSet
232
+ PointCloud
213
233
  """
214
234
 
215
235
  from skimage.feature.peak import peak_local_max
216
- from ..commons import affine_scaling
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
- pointset.PointSet(
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
- volume.VolumeProvider.__init__(self)
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