siibra 1.0.1a1__py3-none-any.whl → 1.0.1a5__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 +7 -16
- siibra/commons.py +19 -8
- siibra/configuration/configuration.py +5 -6
- siibra/configuration/factory.py +13 -8
- siibra/core/__init__.py +1 -1
- siibra/core/assignment.py +19 -7
- siibra/core/atlas.py +3 -3
- siibra/core/concept.py +4 -2
- siibra/core/parcellation.py +5 -5
- siibra/core/region.py +24 -25
- siibra/core/space.py +4 -6
- siibra/core/structure.py +2 -2
- siibra/explorer/url.py +2 -2
- siibra/features/anchor.py +3 -7
- siibra/features/connectivity/regional_connectivity.py +51 -40
- siibra/features/dataset/ebrains.py +1 -1
- siibra/features/feature.py +29 -20
- siibra/features/image/__init__.py +6 -3
- siibra/features/image/image.py +2 -4
- siibra/features/image/sections.py +81 -2
- siibra/features/image/volume_of_interest.py +8 -7
- siibra/features/tabular/__init__.py +1 -1
- siibra/features/tabular/bigbrain_intensity_profile.py +2 -1
- siibra/features/tabular/cell_density_profile.py +8 -9
- siibra/features/tabular/cortical_profile.py +6 -6
- siibra/features/tabular/gene_expression.py +34 -16
- siibra/features/tabular/layerwise_bigbrain_intensities.py +4 -3
- siibra/features/tabular/layerwise_cell_density.py +83 -24
- siibra/features/tabular/receptor_density_fingerprint.py +34 -9
- siibra/features/tabular/receptor_density_profile.py +1 -2
- siibra/features/tabular/regional_timeseries_activity.py +7 -7
- siibra/features/tabular/tabular.py +14 -7
- siibra/livequeries/allen.py +23 -22
- siibra/livequeries/bigbrain.py +239 -51
- siibra/livequeries/ebrains.py +13 -10
- siibra/livequeries/query.py +3 -3
- siibra/locations/__init__.py +17 -8
- siibra/locations/boundingbox.py +10 -8
- siibra/{experimental/plane3d.py → locations/experimental.py} +113 -13
- siibra/locations/location.py +17 -13
- siibra/locations/point.py +14 -19
- siibra/locations/pointcloud.py +57 -12
- siibra/retrieval/cache.py +1 -0
- siibra/retrieval/datasets.py +19 -13
- siibra/retrieval/repositories.py +10 -11
- siibra/retrieval/requests.py +26 -24
- siibra/vocabularies/__init__.py +1 -2
- siibra/volumes/__init__.py +4 -3
- siibra/volumes/parcellationmap.py +33 -17
- siibra/volumes/providers/freesurfer.py +4 -4
- siibra/volumes/providers/gifti.py +4 -4
- siibra/volumes/providers/neuroglancer.py +19 -22
- siibra/volumes/providers/nifti.py +6 -6
- siibra/volumes/providers/provider.py +3 -2
- siibra/volumes/sparsemap.py +19 -26
- siibra/volumes/volume.py +21 -28
- {siibra-1.0.1a1.dist-info → siibra-1.0.1a5.dist-info}/METADATA +37 -17
- siibra-1.0.1a5.dist-info/RECORD +80 -0
- {siibra-1.0.1a1.dist-info → siibra-1.0.1a5.dist-info}/WHEEL +1 -1
- siibra/experimental/__init__.py +0 -19
- siibra/experimental/contour.py +0 -61
- siibra/experimental/cortical_profile_sampler.py +0 -57
- siibra/experimental/patch.py +0 -98
- siibra-1.0.1a1.dist-info/RECORD +0 -84
- {siibra-1.0.1a1.dist-info → siibra-1.0.1a5.dist-info/licenses}/LICENSE +0 -0
- {siibra-1.0.1a1.dist-info → siibra-1.0.1a5.dist-info}/top_level.txt +0 -0
|
@@ -13,18 +13,113 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from ..locations import point, pointcloud
|
|
19
|
-
from ..volumes import volume
|
|
16
|
+
from typing import List
|
|
17
|
+
from math import atan2
|
|
20
18
|
|
|
21
19
|
import numpy as np
|
|
20
|
+
from nilearn import image
|
|
21
|
+
|
|
22
|
+
from . import point, pointcloud, boundingbox
|
|
23
|
+
from ..volumes import volume
|
|
24
|
+
from ..commons import translation_matrix, y_rotation_matrix
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AxisAlignedPatch(pointcloud.PointCloud):
|
|
28
|
+
|
|
29
|
+
def __init__(self, corners: pointcloud.PointCloud):
|
|
30
|
+
"""Construct a patch in physical coordinates.
|
|
31
|
+
As of now, only patches aligned in the y plane of the physical space
|
|
32
|
+
are supported."""
|
|
33
|
+
# TODO: need to ensure that the points are planar, if more than 3
|
|
34
|
+
assert len(corners) == 4
|
|
35
|
+
assert len(np.unique(corners.coordinates[:, 1])) == 1
|
|
36
|
+
pointcloud.PointCloud.__init__(
|
|
37
|
+
self,
|
|
38
|
+
coordinates=corners.coordinates,
|
|
39
|
+
space=corners.space,
|
|
40
|
+
sigma_mm=corners.sigma_mm,
|
|
41
|
+
labels=corners.labels
|
|
42
|
+
)
|
|
43
|
+
# self.corners = corners
|
|
44
|
+
|
|
45
|
+
def __str__(self):
|
|
46
|
+
return f"Patch with boundingbox {self.boundingbox}"
|
|
47
|
+
|
|
48
|
+
def flip(self):
|
|
49
|
+
"""Returns a flipped version of the patch."""
|
|
50
|
+
new_corners = self.coordinates.copy()[[2, 3, 0, 1]]
|
|
51
|
+
return AxisAlignedPatch(pointcloud.PointCloud(new_corners, self.space))
|
|
52
|
+
|
|
53
|
+
def extract_volume(
|
|
54
|
+
self,
|
|
55
|
+
image_volume: volume.Volume,
|
|
56
|
+
resolution_mm: float,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
fetches image data in a planar patch.
|
|
60
|
+
TODO The current implementation only covers patches which are strictly
|
|
61
|
+
define in the y plane. A future implementation should accept arbitrary
|
|
62
|
+
oriented patches.accept arbitrary oriented patches.
|
|
63
|
+
"""
|
|
64
|
+
assert image_volume.space == self.space
|
|
65
|
+
|
|
66
|
+
# Extend the 2D patch into a 3D structure
|
|
67
|
+
# this is only valid if the patch plane lies within the image canvas.
|
|
68
|
+
canvas = image_volume.get_boundingbox()
|
|
69
|
+
assert canvas.minpoint[1] <= self.coordinates[0, 1]
|
|
70
|
+
assert canvas.maxpoint[1] >= self.coordinates[0, 1]
|
|
71
|
+
XYZ = self.coordinates
|
|
72
|
+
voi = boundingbox.BoundingBox(
|
|
73
|
+
XYZ.min(0)[:3], XYZ.max(0)[:3], space=image_volume.space
|
|
74
|
+
)
|
|
75
|
+
# enforce the patch to have the same y dimensions
|
|
76
|
+
voi.minpoint[1] = canvas.minpoint[1]
|
|
77
|
+
voi.maxpoint[1] = canvas.maxpoint[1]
|
|
78
|
+
patch = image_volume.fetch(voi=voi, resolution_mm=resolution_mm)
|
|
79
|
+
assert patch is not None
|
|
22
80
|
|
|
81
|
+
# patch rotation defined in physical space
|
|
82
|
+
vx, vy, vz = XYZ[1] - XYZ[0]
|
|
83
|
+
alpha = -atan2(-vz, -vx)
|
|
84
|
+
cx, cy, cz = XYZ.mean(0)
|
|
85
|
+
rot_phys = np.linalg.multi_dot(
|
|
86
|
+
[
|
|
87
|
+
translation_matrix(cx, cy, cz),
|
|
88
|
+
y_rotation_matrix(alpha),
|
|
89
|
+
translation_matrix(-cx, -cy, -cz),
|
|
90
|
+
]
|
|
91
|
+
)
|
|
23
92
|
|
|
24
|
-
|
|
93
|
+
# rotate the patch in physical space
|
|
94
|
+
affine_rot = np.dot(rot_phys, patch.affine)
|
|
95
|
+
|
|
96
|
+
# crop in the rotated space
|
|
97
|
+
pixels = (
|
|
98
|
+
np.dot(np.linalg.inv(affine_rot), self.homogeneous.T)
|
|
99
|
+
.astype("int")
|
|
100
|
+
.T
|
|
101
|
+
)
|
|
102
|
+
# keep a pixel distance to avoid black border pixels
|
|
103
|
+
xmin, ymin, zmin = pixels.min(0)[:3] + 1
|
|
104
|
+
xmax, ymax, zmax = pixels.max(0)[:3] - 1
|
|
105
|
+
h, w = xmax - xmin, zmax - zmin
|
|
106
|
+
affine = np.dot(affine_rot, translation_matrix(xmin, 0, zmin))
|
|
107
|
+
return volume.from_nifti(
|
|
108
|
+
image.resample_img(
|
|
109
|
+
patch,
|
|
110
|
+
target_affine=affine,
|
|
111
|
+
target_shape=[h, 1, w],
|
|
112
|
+
force_resample=True
|
|
113
|
+
),
|
|
114
|
+
space=image_volume.space,
|
|
115
|
+
name=f"Rotated patch with corner points {self.coordinates} sampled from {image_volume.name}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Plane:
|
|
25
120
|
"""
|
|
26
121
|
A 3D plane in reference space.
|
|
27
|
-
This shall eventually be derived from siibra.Location
|
|
122
|
+
TODO This shall eventually be derived from siibra.Location
|
|
28
123
|
"""
|
|
29
124
|
|
|
30
125
|
def __init__(self, point1: point.Point, point2: point.Point, point3: point.Point):
|
|
@@ -64,8 +159,6 @@ class Plane3D:
|
|
|
64
159
|
Returns the set of intersection points.
|
|
65
160
|
The line segments are given by two Nx3 arrays of their start- and endpoints.
|
|
66
161
|
The result is an Nx3 list of intersection coordinates.
|
|
67
|
-
TODO This returns an intersection even if the line segment intersects the plane,
|
|
68
|
-
|
|
69
162
|
"""
|
|
70
163
|
directions = endpoints - startpoints
|
|
71
164
|
lengths = np.linalg.norm(directions, axis=1)
|
|
@@ -83,7 +176,7 @@ class Plane3D:
|
|
|
83
176
|
)
|
|
84
177
|
return result
|
|
85
178
|
|
|
86
|
-
def intersect_mesh(self, mesh: dict):
|
|
179
|
+
def intersect_mesh(self, mesh: dict) -> List[pointcloud.Contour]:
|
|
87
180
|
"""
|
|
88
181
|
Intersects a 3D surface mesh with the plane.
|
|
89
182
|
Returns a set of split 2D contours, represented by ordered coordinate lists.
|
|
@@ -148,7 +241,7 @@ class Plane3D:
|
|
|
148
241
|
# should include the exact same set of points. Verify this now.
|
|
149
242
|
sortrows = lambda A: A[np.lexsort(A.T[::-1]), :]
|
|
150
243
|
err = (sortrows(fwd_intersections) - sortrows(bwd_intersections)).sum()
|
|
151
|
-
assert err == 0
|
|
244
|
+
assert err == 0, f"intersection inconsistency: {err}"
|
|
152
245
|
|
|
153
246
|
# Due to the above property, we can construct closed contours in the
|
|
154
247
|
# intersection plane by following the interleaved fwd/bwd roles of intersection
|
|
@@ -175,7 +268,7 @@ class Plane3D:
|
|
|
175
268
|
|
|
176
269
|
# finish the current contour.
|
|
177
270
|
result.append(
|
|
178
|
-
|
|
271
|
+
pointcloud.Contour(np.array(points), labels=labels, space=self.space)
|
|
179
272
|
)
|
|
180
273
|
if len(face_indices) > 0:
|
|
181
274
|
# prepare to process another contour segment
|
|
@@ -236,8 +329,15 @@ class Plane3D:
|
|
|
236
329
|
)
|
|
237
330
|
err = (self.project_points(corners).coordinates - corners.coordinates).sum()
|
|
238
331
|
if err > 1e-5:
|
|
239
|
-
print(
|
|
240
|
-
|
|
332
|
+
print(
|
|
333
|
+
f"WARNING: patch coordinates were not exactly in-plane (error={err})."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
patch = AxisAlignedPatch(self.project_points(corners))
|
|
338
|
+
except AssertionError:
|
|
339
|
+
patch = None
|
|
340
|
+
return patch
|
|
241
341
|
|
|
242
342
|
@classmethod
|
|
243
343
|
def from_image(cls, image: volume.Volume):
|
siibra/locations/location.py
CHANGED
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from typing import Union, Dict
|
|
20
|
+
from abc import abstractmethod
|
|
20
21
|
|
|
21
22
|
import numpy as np
|
|
22
|
-
from abc import abstractmethod
|
|
23
23
|
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
from siibra.core.space import Space
|
|
24
|
+
from ..core.structure import BrainStructure
|
|
25
|
+
from ..core import space as _space
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class Location(BrainStructure):
|
|
@@ -46,29 +45,34 @@ class Location(BrainStructure):
|
|
|
46
45
|
_MASK_MEMO = {} # cache region masks for Location._assign_region()
|
|
47
46
|
_ASSIGNMENT_CACHE = {} # caches assignment results, see Region.assign()
|
|
48
47
|
|
|
49
|
-
def __init__(self, spacespec: Union[str, Dict[str, str],
|
|
48
|
+
def __init__(self, spacespec: Union[str, Dict[str, str], _space.Space]):
|
|
50
49
|
self._space_spec = spacespec
|
|
51
50
|
self._space_cached = None
|
|
52
51
|
|
|
53
52
|
@property
|
|
54
|
-
def space(self):
|
|
53
|
+
def space(self) -> _space.Space:
|
|
55
54
|
if self._space_cached is None:
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if self._space_spec is None:
|
|
56
|
+
return None
|
|
57
|
+
elif isinstance(self._space_spec, _space.Space):
|
|
58
|
+
self._space_cached = self._space_spec
|
|
59
|
+
elif isinstance(self._space_spec, dict):
|
|
58
60
|
spec = self._space_spec.get("@id") or self._space_spec.get("name")
|
|
59
|
-
self._space_cached = Space.get_instance(spec)
|
|
61
|
+
self._space_cached = _space.Space.get_instance(spec)
|
|
62
|
+
elif isinstance(self._space_spec, str):
|
|
63
|
+
self._space_cached = _space.Space.get_instance(self._space_spec)
|
|
60
64
|
else:
|
|
61
|
-
|
|
65
|
+
raise ValueError(f"Invalid space spec type: '{type(self._space_spec)}'")
|
|
62
66
|
return self._space_cached
|
|
63
67
|
|
|
64
68
|
@abstractmethod
|
|
65
|
-
def warp(self, space):
|
|
69
|
+
def warp(self, space: Union[str, "_space.Space"]):
|
|
66
70
|
"""Generates a new location by warping the
|
|
67
71
|
current one into another reference space."""
|
|
68
72
|
pass
|
|
69
73
|
|
|
70
74
|
@abstractmethod
|
|
71
|
-
def transform(self, affine: np.ndarray, space=None):
|
|
75
|
+
def transform(self, affine: np.ndarray, space: Union[str, "_space.Space", None] = None):
|
|
72
76
|
"""Returns a new location obtained by transforming the
|
|
73
77
|
reference coordinates of this one with the given affine matrix.
|
|
74
78
|
|
siibra/locations/point.py
CHANGED
|
@@ -14,19 +14,22 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Singular coordinate defined on a space, possibly with an uncertainty."""
|
|
16
16
|
|
|
17
|
-
from . import location, boundingbox, pointcloud
|
|
18
|
-
|
|
19
|
-
from ..commons import logger
|
|
20
|
-
from ..retrieval.requests import HttpRequest
|
|
21
|
-
from ..exceptions import SpaceWarpingFailedError, NoneCoordinateSuppliedError
|
|
22
|
-
|
|
23
17
|
from urllib.parse import quote
|
|
24
18
|
import re
|
|
25
|
-
import numpy as np
|
|
26
19
|
import json
|
|
27
20
|
import numbers
|
|
28
21
|
import hashlib
|
|
29
|
-
from typing import Tuple, Union
|
|
22
|
+
from typing import Tuple, Union, Dict, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from . import location, pointcloud
|
|
27
|
+
from ..commons import logger
|
|
28
|
+
from ..retrieval.requests import HttpRequest
|
|
29
|
+
from ..exceptions import SpaceWarpingFailedError, NoneCoordinateSuppliedError
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from ..core.space import Space
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
class Point(location.Location):
|
|
@@ -115,18 +118,17 @@ class Point(location.Location):
|
|
|
115
118
|
def intersection(self, other: location.Location) -> "Point":
|
|
116
119
|
if isinstance(other, Point):
|
|
117
120
|
return self if self == other else None
|
|
118
|
-
elif isinstance(other, pointcloud.PointCloud):
|
|
119
|
-
return self if self in other else None
|
|
120
121
|
else:
|
|
121
122
|
return self if other.intersection(self) else None
|
|
122
123
|
|
|
123
|
-
def warp(self, space):
|
|
124
|
+
def warp(self, space: Union[str, Dict, "Space"]):
|
|
124
125
|
"""
|
|
125
126
|
Creates a new point by warping this point to another space
|
|
126
127
|
TODO this needs to maintain the sigma parameter!
|
|
127
128
|
"""
|
|
128
129
|
from ..core.space import Space
|
|
129
|
-
|
|
130
|
+
|
|
131
|
+
spaceobj = space if isinstance(space, Space) else Space.get_instance(space)
|
|
130
132
|
if spaceobj == self.space:
|
|
131
133
|
return self
|
|
132
134
|
if any(_ not in location.Location.SPACEWARP_IDS for _ in [self.space.id, spaceobj.id]):
|
|
@@ -305,13 +307,6 @@ class Point(location.Location):
|
|
|
305
307
|
assert 0 <= index < 3
|
|
306
308
|
return self.coordinate[index]
|
|
307
309
|
|
|
308
|
-
@property
|
|
309
|
-
def boundingbox(self):
|
|
310
|
-
w = max(self.sigma or 0, 1e-6) # at least a micrometer
|
|
311
|
-
return boundingbox.BoundingBox(
|
|
312
|
-
self - w, self + w, self.space, self.sigma
|
|
313
|
-
)
|
|
314
|
-
|
|
315
310
|
def bigbrain_section(self):
|
|
316
311
|
"""
|
|
317
312
|
Estimate the histological section number of BigBrain
|
siibra/locations/pointcloud.py
CHANGED
|
@@ -14,15 +14,10 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A set of coordinates on a reference space."""
|
|
16
16
|
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
from ..retrieval.requests import HttpRequest
|
|
20
|
-
from ..commons import logger
|
|
21
|
-
from ..exceptions import SpaceWarpingFailedError, EmptyPointCloudError
|
|
22
|
-
|
|
23
|
-
from typing import List, Union, Tuple
|
|
17
|
+
from typing import List, Union, Tuple, Dict, TYPE_CHECKING
|
|
24
18
|
import numbers
|
|
25
19
|
import json
|
|
20
|
+
|
|
26
21
|
import numpy as np
|
|
27
22
|
try:
|
|
28
23
|
from sklearn.cluster import HDBSCAN
|
|
@@ -30,10 +25,14 @@ try:
|
|
|
30
25
|
except ImportError:
|
|
31
26
|
import sklearn
|
|
32
27
|
_HAS_HDBSCAN = False
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
|
|
29
|
+
from . import location, point, boundingbox as _boundingbox
|
|
30
|
+
from ..retrieval.requests import HttpRequest
|
|
31
|
+
from ..commons import logger
|
|
32
|
+
from ..exceptions import SpaceWarpingFailedError, EmptyPointCloudError
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from ..core.space import Space
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
def from_points(points: List["point.Point"], newlabels: List[Union[int, float, tuple]] = None) -> "PointCloud":
|
|
@@ -122,6 +121,8 @@ class PointCloud(location.Location):
|
|
|
122
121
|
|
|
123
122
|
if isinstance(other, PointCloud):
|
|
124
123
|
intersecting_points = [p for p in self if p.coordinate in other.coordinates]
|
|
124
|
+
elif isinstance(other, point.Point):
|
|
125
|
+
return other if other in self else None
|
|
125
126
|
else:
|
|
126
127
|
intersecting_points = [p for p in self if p.intersects(other)]
|
|
127
128
|
if len(intersecting_points) == 0:
|
|
@@ -142,9 +143,10 @@ class PointCloud(location.Location):
|
|
|
142
143
|
def has_constant_sigma(self) -> bool:
|
|
143
144
|
return len(set(self.sigma)) == 1
|
|
144
145
|
|
|
145
|
-
def warp(self, space, chunksize=1000):
|
|
146
|
+
def warp(self, space: Union[str, Dict, "Space"], chunksize: int = 1000):
|
|
146
147
|
"""Creates a new point set by warping its points to another space"""
|
|
147
148
|
from ..core.space import Space
|
|
149
|
+
|
|
148
150
|
spaceobj = space if isinstance(space, Space) else Space.get_instance(space)
|
|
149
151
|
if spaceobj == self.space:
|
|
150
152
|
return self
|
|
@@ -342,3 +344,46 @@ class PointCloud(location.Location):
|
|
|
342
344
|
logger.error("Matplotlib is not available. Label colors is disabled.")
|
|
343
345
|
return None
|
|
344
346
|
return colormaps.rainbow(np.linspace(0, 1, max(self.labels) + 1))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Contour(PointCloud):
|
|
350
|
+
"""
|
|
351
|
+
A PointCloud that represents a contour line.
|
|
352
|
+
The only difference is that the point order is relevant,
|
|
353
|
+
and consecutive points are thought as being connected by an edge.
|
|
354
|
+
|
|
355
|
+
In fact, PointCloud assumes order as well, but no connections between points.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(self, coordinates, space=None, sigma_mm=0, labels: list = None):
|
|
359
|
+
PointCloud.__init__(self, coordinates, space, sigma_mm, labels)
|
|
360
|
+
|
|
361
|
+
def crop(self, voi: "_boundingbox.BoundingBox") -> List["Contour"]:
|
|
362
|
+
"""
|
|
363
|
+
Crop the contour with a volume of interest.
|
|
364
|
+
Since the contour might be split from the cropping,
|
|
365
|
+
returns a set of contour segments.
|
|
366
|
+
"""
|
|
367
|
+
segments = []
|
|
368
|
+
|
|
369
|
+
# set the contour point labels to a linear numbering
|
|
370
|
+
# so we can use them after the intersection to detect splits.
|
|
371
|
+
old_labels = self.labels
|
|
372
|
+
self.labels = list(range(len(self)))
|
|
373
|
+
cropped = self.intersection(voi)
|
|
374
|
+
|
|
375
|
+
if cropped is not None and not isinstance(cropped, point.Point):
|
|
376
|
+
assert isinstance(cropped, PointCloud)
|
|
377
|
+
# Identify contour splits are by discontinuouities ("jumps")
|
|
378
|
+
# of their labels, which denote positions in the original contour
|
|
379
|
+
jumps = np.diff([self.labels.index(lb) for lb in cropped.labels])
|
|
380
|
+
splits = [0] + list(np.where(jumps > 1)[0] + 1) + [len(cropped)]
|
|
381
|
+
for i, j in zip(splits[:-1], splits[1:]):
|
|
382
|
+
segments.append(
|
|
383
|
+
self.__class__(cropped.coordinates[i:j, :], space=cropped.space)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# reset labels of the input contour points.
|
|
387
|
+
self.labels = old_labels
|
|
388
|
+
|
|
389
|
+
return segments
|
siibra/retrieval/cache.py
CHANGED
|
@@ -23,6 +23,7 @@ from enum import Enum
|
|
|
23
23
|
from typing import Callable, List, NamedTuple, Union
|
|
24
24
|
from concurrent.futures import ThreadPoolExecutor
|
|
25
25
|
from pathlib import Path
|
|
26
|
+
|
|
26
27
|
from filelock import FileLock as Lock
|
|
27
28
|
|
|
28
29
|
from ..commons import logger, SIIBRA_CACHEDIR, SKIP_CACHEINIT_MAINTENANCE, siibra_tqdm
|
siibra/retrieval/datasets.py
CHANGED
|
@@ -14,19 +14,18 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Metadata connection to EBRAINS datasets."""
|
|
16
16
|
|
|
17
|
-
from .requests import MultiSourcedRequest, GitlabProxy, GitlabProxyEnum
|
|
18
|
-
|
|
19
17
|
import re
|
|
20
|
-
from
|
|
21
|
-
from abc import ABC, abstractproperty
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
22
19
|
from hashlib import md5
|
|
23
|
-
|
|
20
|
+
from typing import Union, List
|
|
24
21
|
try:
|
|
25
22
|
from typing import TypedDict
|
|
26
23
|
except ImportError:
|
|
27
24
|
# support python 3.7
|
|
28
25
|
from typing_extensions import TypedDict
|
|
29
26
|
|
|
27
|
+
from .requests import MultiSourcedRequest, GitlabProxy, GitlabProxyEnum
|
|
28
|
+
|
|
30
29
|
|
|
31
30
|
class EbrainsDatasetUrl(TypedDict):
|
|
32
31
|
url: str
|
|
@@ -48,31 +47,38 @@ EbrainsDatasetEmbargoStatus = TypedDict("EbrainsDatasetEmbargoStatus", {
|
|
|
48
47
|
|
|
49
48
|
|
|
50
49
|
class EbrainsBaseDataset(ABC):
|
|
51
|
-
@
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
52
|
def id(self) -> str:
|
|
53
53
|
raise NotImplementedError
|
|
54
54
|
|
|
55
|
-
@
|
|
55
|
+
@property
|
|
56
|
+
@abstractmethod
|
|
56
57
|
def name(self) -> str:
|
|
57
58
|
raise NotImplementedError
|
|
58
59
|
|
|
59
|
-
@
|
|
60
|
+
@property
|
|
61
|
+
@abstractmethod
|
|
60
62
|
def urls(self) -> List[EbrainsDatasetUrl]:
|
|
61
63
|
raise NotImplementedError
|
|
62
64
|
|
|
63
|
-
@
|
|
65
|
+
@property
|
|
66
|
+
@abstractmethod
|
|
64
67
|
def description(self) -> str:
|
|
65
68
|
raise NotImplementedError
|
|
66
69
|
|
|
67
|
-
@
|
|
70
|
+
@property
|
|
71
|
+
@abstractmethod
|
|
68
72
|
def contributors(self) -> List[EbrainsDatasetPerson]:
|
|
69
73
|
raise NotImplementedError
|
|
70
74
|
|
|
71
|
-
@
|
|
75
|
+
@property
|
|
76
|
+
@abstractmethod
|
|
72
77
|
def ebrains_page(self) -> str:
|
|
73
78
|
raise NotImplementedError
|
|
74
79
|
|
|
75
|
-
@
|
|
80
|
+
@property
|
|
81
|
+
@abstractmethod
|
|
76
82
|
def custodians(self) -> List[EbrainsDatasetPerson]:
|
|
77
83
|
raise NotImplementedError
|
|
78
84
|
|
|
@@ -342,7 +348,7 @@ class EbrainsV3Dataset(EbrainsBaseDataset):
|
|
|
342
348
|
return [version.get("id") for version in self._detail.get("versions", [])]
|
|
343
349
|
|
|
344
350
|
|
|
345
|
-
class GenericDataset
|
|
351
|
+
class GenericDataset:
|
|
346
352
|
|
|
347
353
|
def __init__(
|
|
348
354
|
self,
|
siibra/retrieval/repositories.py
CHANGED
|
@@ -14,25 +14,24 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Connect to repositories to browse and pull files within."""
|
|
16
16
|
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from urllib.parse import quote
|
|
19
|
+
import pathlib
|
|
20
|
+
import os
|
|
21
|
+
from zipfile import ZipFile
|
|
22
|
+
from typing import List
|
|
23
|
+
|
|
24
|
+
from .cache import CACHE
|
|
17
25
|
from .requests import (
|
|
18
26
|
HttpRequest,
|
|
19
27
|
EbrainsRequest,
|
|
20
28
|
SiibraHttpRequestError,
|
|
21
|
-
|
|
29
|
+
find_suitable_decoder,
|
|
22
30
|
DECODERS,
|
|
23
31
|
FileLoader
|
|
24
32
|
)
|
|
25
|
-
from .cache import CACHE
|
|
26
|
-
|
|
27
33
|
from ..commons import logger, siibra_tqdm
|
|
28
34
|
|
|
29
|
-
from abc import ABC, abstractmethod
|
|
30
|
-
from urllib.parse import quote
|
|
31
|
-
import pathlib
|
|
32
|
-
import os
|
|
33
|
-
from zipfile import ZipFile
|
|
34
|
-
from typing import List
|
|
35
|
-
|
|
36
35
|
|
|
37
36
|
class RepositoryConnector(ABC):
|
|
38
37
|
"""
|
|
@@ -67,7 +66,7 @@ class RepositoryConnector(ABC):
|
|
|
67
66
|
pass
|
|
68
67
|
|
|
69
68
|
def _decode_response(self, response, filename: str):
|
|
70
|
-
decoder =
|
|
69
|
+
decoder = find_suitable_decoder(filename)
|
|
71
70
|
return decoder(response) if decoder else response
|
|
72
71
|
|
|
73
72
|
def get(self, filename, folder="", decode_func=None):
|
siibra/retrieval/requests.py
CHANGED
|
@@ -14,35 +14,37 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Request files with decoders, lazy loading, and caching."""
|
|
16
16
|
|
|
17
|
-
from .cache import CACHE, cache_user_fn
|
|
18
|
-
from .exceptions import EbrainsAuthenticationError
|
|
19
|
-
from ..commons import (
|
|
20
|
-
logger,
|
|
21
|
-
HBP_AUTH_TOKEN,
|
|
22
|
-
KEYCLOAK_CLIENT_ID,
|
|
23
|
-
KEYCLOAK_CLIENT_SECRET,
|
|
24
|
-
siibra_tqdm,
|
|
25
|
-
SIIBRA_USE_LOCAL_SNAPSPOT,
|
|
26
|
-
)
|
|
27
|
-
from .. import __version__
|
|
28
|
-
|
|
29
17
|
import json
|
|
30
18
|
from zipfile import ZipFile
|
|
31
19
|
import requests
|
|
32
20
|
import os
|
|
33
|
-
from nibabel import Nifti1Image, GiftiImage, streamlines, freesurfer
|
|
34
|
-
from skimage import io as skimage_io
|
|
35
21
|
import gzip
|
|
36
22
|
from io import BytesIO
|
|
37
23
|
import urllib.parse
|
|
38
|
-
import pandas as pd
|
|
39
|
-
import numpy as np
|
|
40
24
|
from typing import List, Callable, TYPE_CHECKING
|
|
41
25
|
from enum import Enum
|
|
42
26
|
from functools import wraps
|
|
43
27
|
from time import sleep
|
|
44
28
|
import sys
|
|
29
|
+
|
|
45
30
|
from filelock import FileLock as Lock
|
|
31
|
+
import numpy as np
|
|
32
|
+
import pandas as pd
|
|
33
|
+
from skimage import io as skimage_io
|
|
34
|
+
from nibabel import Nifti1Image, GiftiImage, streamlines, freesurfer
|
|
35
|
+
|
|
36
|
+
from . import exceptions as _exceptions
|
|
37
|
+
from .cache import CACHE, cache_user_fn
|
|
38
|
+
from .. import __version__
|
|
39
|
+
from ..commons import (
|
|
40
|
+
logger,
|
|
41
|
+
HBP_AUTH_TOKEN,
|
|
42
|
+
KEYCLOAK_CLIENT_ID,
|
|
43
|
+
KEYCLOAK_CLIENT_SECRET,
|
|
44
|
+
siibra_tqdm,
|
|
45
|
+
SIIBRA_USE_LOCAL_SNAPSPOT,
|
|
46
|
+
)
|
|
47
|
+
|
|
46
48
|
if TYPE_CHECKING:
|
|
47
49
|
from .repositories import GitlabConnector
|
|
48
50
|
|
|
@@ -91,7 +93,7 @@ DECODERS = {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
|
|
94
|
-
def
|
|
96
|
+
def find_suitable_decoder(url: str) -> Callable:
|
|
95
97
|
"""
|
|
96
98
|
By supplying a url or a filename, obtain a suitable decoder function
|
|
97
99
|
for siibra to digest based on predefined DECODERS. An extra layer of
|
|
@@ -108,7 +110,7 @@ def find_suitiable_decoder(url: str) -> Callable:
|
|
|
108
110
|
"""
|
|
109
111
|
urlpath = urllib.parse.urlsplit(url).path
|
|
110
112
|
if urlpath.endswith(".gz"):
|
|
111
|
-
dec =
|
|
113
|
+
dec = find_suitable_decoder(urlpath[:-3])
|
|
112
114
|
if dec is None:
|
|
113
115
|
return lambda b: gzip.decompress(b)
|
|
114
116
|
else:
|
|
@@ -183,7 +185,7 @@ class HttpRequest:
|
|
|
183
185
|
----------
|
|
184
186
|
func : Callable, default: None
|
|
185
187
|
"""
|
|
186
|
-
self.func = func or
|
|
188
|
+
self.func = func or find_suitable_decoder(self.url)
|
|
187
189
|
|
|
188
190
|
@property
|
|
189
191
|
def cached(self):
|
|
@@ -277,7 +279,7 @@ class FileLoader(HttpRequest):
|
|
|
277
279
|
def __init__(self, filepath, func=None):
|
|
278
280
|
HttpRequest.__init__(
|
|
279
281
|
self, filepath, refresh=False,
|
|
280
|
-
func=func or
|
|
282
|
+
func=func or find_suitable_decoder(filepath)
|
|
281
283
|
)
|
|
282
284
|
self.cachefile = filepath
|
|
283
285
|
|
|
@@ -291,7 +293,7 @@ class ZipfileRequest(HttpRequest):
|
|
|
291
293
|
def __init__(self, url, filename, func=None, refresh=False):
|
|
292
294
|
HttpRequest.__init__(
|
|
293
295
|
self, url, refresh=refresh,
|
|
294
|
-
func=func or
|
|
296
|
+
func=func or find_suitable_decoder(filename)
|
|
295
297
|
)
|
|
296
298
|
self.filename = filename
|
|
297
299
|
|
|
@@ -388,7 +390,7 @@ class EbrainsRequest(HttpRequest):
|
|
|
388
390
|
), # if explicitly enabled by env var, do not raise
|
|
389
391
|
]
|
|
390
392
|
):
|
|
391
|
-
raise EbrainsAuthenticationError(
|
|
393
|
+
raise _exceptions.EbrainsAuthenticationError(
|
|
392
394
|
"sys.stdout is not tty, SIIBRA_ENABLE_DEVICE_FLOW is not set,"
|
|
393
395
|
"and not running in a notebook. Are you running in batch mode?"
|
|
394
396
|
)
|
|
@@ -444,7 +446,7 @@ class EbrainsRequest(HttpRequest):
|
|
|
444
446
|
f"exceeded max attempts: {cls._IAM_DEVICE_MAXTRIES}, aborting..."
|
|
445
447
|
)
|
|
446
448
|
logger.error(message)
|
|
447
|
-
raise EbrainsAuthenticationError(message)
|
|
449
|
+
raise _exceptions.EbrainsAuthenticationError(message)
|
|
448
450
|
attempt_number += 1
|
|
449
451
|
resp = requests.post(
|
|
450
452
|
url=cls._IAM_TOKEN_ENDPOINT,
|
|
@@ -470,7 +472,7 @@ class EbrainsRequest(HttpRequest):
|
|
|
470
472
|
logger.debug(f"400 error: {resp.content}")
|
|
471
473
|
continue
|
|
472
474
|
|
|
473
|
-
raise EbrainsAuthenticationError(resp.content)
|
|
475
|
+
raise _exceptions.EbrainsAuthenticationError(resp.content)
|
|
474
476
|
|
|
475
477
|
@classmethod
|
|
476
478
|
def set_token(cls, token):
|
siibra/vocabularies/__init__.py
CHANGED
siibra/volumes/__init__.py
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Package handling variety of volumes and volume operations"""
|
|
16
16
|
|
|
17
|
+
from typing import List, Union
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
17
21
|
from .parcellationmap import Map
|
|
18
22
|
from .providers import provider
|
|
19
23
|
from .volume import from_array, from_file, from_pointcloud, from_nifti, Volume
|
|
20
|
-
|
|
21
24
|
from ..commons import logger
|
|
22
|
-
from typing import List, Union
|
|
23
|
-
import numpy as np
|