siibra 1.0.1a0__py3-none-any.whl → 1.0.1a2__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 +11 -20
- siibra/commons.py +17 -14
- siibra/configuration/__init__.py +1 -1
- siibra/configuration/configuration.py +6 -6
- siibra/configuration/factory.py +10 -9
- siibra/core/__init__.py +2 -2
- siibra/core/assignment.py +2 -1
- siibra/core/atlas.py +4 -4
- siibra/core/concept.py +7 -5
- siibra/core/parcellation.py +10 -10
- siibra/core/region.py +82 -73
- siibra/core/space.py +5 -7
- siibra/core/structure.py +4 -4
- siibra/exceptions.py +6 -2
- siibra/explorer/__init__.py +1 -1
- siibra/explorer/url.py +2 -2
- siibra/explorer/util.py +1 -1
- siibra/features/__init__.py +1 -1
- siibra/features/anchor.py +4 -6
- siibra/features/connectivity/__init__.py +1 -1
- siibra/features/connectivity/functional_connectivity.py +1 -1
- siibra/features/connectivity/regional_connectivity.py +12 -15
- 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 +2 -2
- siibra/features/feature.py +31 -28
- siibra/features/image/__init__.py +5 -3
- siibra/features/image/image.py +4 -6
- siibra/features/image/sections.py +82 -3
- siibra/features/image/volume_of_interest.py +1 -9
- siibra/features/tabular/__init__.py +2 -2
- siibra/features/tabular/bigbrain_intensity_profile.py +3 -2
- siibra/features/tabular/cell_density_profile.py +10 -11
- siibra/features/tabular/cortical_profile.py +9 -9
- siibra/features/tabular/gene_expression.py +7 -6
- siibra/features/tabular/layerwise_bigbrain_intensities.py +5 -4
- siibra/features/tabular/layerwise_cell_density.py +5 -7
- siibra/features/tabular/receptor_density_fingerprint.py +47 -19
- siibra/features/tabular/receptor_density_profile.py +2 -3
- siibra/features/tabular/regional_timeseries_activity.py +9 -9
- siibra/features/tabular/tabular.py +10 -9
- siibra/livequeries/__init__.py +1 -1
- siibra/livequeries/allen.py +23 -25
- siibra/livequeries/bigbrain.py +252 -55
- siibra/livequeries/ebrains.py +14 -11
- siibra/livequeries/query.py +5 -5
- siibra/locations/__init__.py +19 -10
- siibra/locations/boundingbox.py +10 -13
- siibra/{experimental/plane3d.py → locations/experimental.py} +117 -17
- siibra/locations/location.py +11 -13
- siibra/locations/point.py +10 -19
- siibra/locations/pointcloud.py +59 -23
- siibra/retrieval/__init__.py +1 -1
- siibra/retrieval/cache.py +2 -1
- siibra/retrieval/datasets.py +23 -17
- siibra/retrieval/exceptions/__init__.py +1 -1
- siibra/retrieval/repositories.py +14 -15
- siibra/retrieval/requests.py +32 -30
- siibra/vocabularies/__init__.py +2 -3
- siibra/volumes/__init__.py +5 -4
- siibra/volumes/parcellationmap.py +55 -20
- siibra/volumes/providers/__init__.py +1 -1
- siibra/volumes/providers/freesurfer.py +7 -7
- siibra/volumes/providers/gifti.py +5 -5
- siibra/volumes/providers/neuroglancer.py +25 -28
- siibra/volumes/providers/nifti.py +7 -7
- siibra/volumes/providers/provider.py +4 -3
- siibra/volumes/sparsemap.py +8 -7
- siibra/volumes/volume.py +33 -40
- {siibra-1.0.1a0.dist-info → siibra-1.0.1a2.dist-info}/METADATA +21 -8
- siibra-1.0.1a2.dist-info/RECORD +80 -0
- {siibra-1.0.1a0.dist-info → siibra-1.0.1a2.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.1a0.dist-info/RECORD +0 -84
- {siibra-1.0.1a0.dist-info → siibra-1.0.1a2.dist-info}/LICENSE +0 -0
- {siibra-1.0.1a0.dist-info → siibra-1.0.1a2.dist-info}/top_level.txt +0 -0
siibra/livequeries/query.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Handles feature queries that rely on live or on-the-fly calculations."""
|
|
16
16
|
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import List
|
|
19
|
+
|
|
17
20
|
from ..commons import logger
|
|
18
21
|
from ..features.feature import Feature
|
|
19
22
|
from ..core.concept import AtlasConcept
|
|
20
23
|
|
|
21
|
-
from abc import ABC, abstractmethod
|
|
22
|
-
from typing import List
|
|
23
|
-
|
|
24
24
|
|
|
25
25
|
class LiveQuery(ABC):
|
|
26
26
|
|
|
@@ -46,4 +46,4 @@ class LiveQuery(ABC):
|
|
|
46
46
|
|
|
47
47
|
@abstractmethod
|
|
48
48
|
def query(self, concept: AtlasConcept, **kwargs) -> List[Feature]:
|
|
49
|
-
raise NotImplementedError(f"
|
|
49
|
+
raise NotImplementedError(f"Derived class {self.__class__} needs to implement query()")
|
siibra/locations/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -13,26 +13,34 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""Handles spatial concepts and spatial operation like warping between spaces."""
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
from functools import reduce
|
|
16
18
|
|
|
17
19
|
from .location import Location
|
|
18
20
|
from .point import Point
|
|
19
|
-
from .pointcloud import PointCloud, from_points
|
|
21
|
+
from .pointcloud import PointCloud, Contour, from_points
|
|
22
|
+
from .experimental import AxisAlignedPatch, Plane
|
|
20
23
|
from .boundingbox import BoundingBox
|
|
21
24
|
|
|
22
25
|
|
|
23
|
-
def reassign_union(
|
|
26
|
+
def reassign_union(*args: Iterable["Location"]) -> "Location":
|
|
27
|
+
return reduce(pairwise_union, args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pairwise_union(loc0: "Location", loc1: "Location") -> "Location":
|
|
24
31
|
"""
|
|
25
|
-
Add two locations of same or
|
|
32
|
+
Add two locations of same or different type to find their union as a
|
|
26
33
|
Location object.
|
|
34
|
+
|
|
27
35
|
Note
|
|
28
36
|
----
|
|
29
37
|
`loc1` will be warped to `loc0` they are not in the same space.
|
|
38
|
+
|
|
30
39
|
Parameters
|
|
31
40
|
----------
|
|
32
41
|
loc0 : Location
|
|
33
|
-
_description_
|
|
34
42
|
loc1 : Location
|
|
35
|
-
|
|
43
|
+
|
|
36
44
|
Returns
|
|
37
45
|
-------
|
|
38
46
|
Location
|
|
@@ -58,12 +66,13 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
|
|
|
58
66
|
try:
|
|
59
67
|
return loc1.union(loc0)
|
|
60
68
|
except Exception:
|
|
61
|
-
raise NotImplementedError(
|
|
69
|
+
raise NotImplementedError(
|
|
70
|
+
f"There are no union method for {(loc0.__class__.__name__, loc1.__class__.__name__)}"
|
|
71
|
+
)
|
|
62
72
|
|
|
63
73
|
# convert Points to PointClouds
|
|
64
74
|
loc0, loc1 = [
|
|
65
|
-
from_points([loc]) if isinstance(loc, Point) else loc
|
|
66
|
-
for loc in [loc0, loc1]
|
|
75
|
+
from_points([loc]) if isinstance(loc, Point) else loc for loc in [loc0, loc1]
|
|
67
76
|
]
|
|
68
77
|
|
|
69
78
|
# adopt the space of the first location
|
|
@@ -82,7 +91,7 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
|
|
|
82
91
|
point1=[min(p[i] for p in coordinates) for i in range(3)],
|
|
83
92
|
point2=[max(p[i] for p in coordinates) for i in range(3)],
|
|
84
93
|
space=loc0.space,
|
|
85
|
-
sigma_mm=[loc0.minpoint.sigma, loc0.maxpoint.sigma]
|
|
94
|
+
sigma_mm=[loc0.minpoint.sigma, loc0.maxpoint.sigma],
|
|
86
95
|
)
|
|
87
96
|
|
|
88
97
|
return reassign_union(loc1_w, loc0)
|
siibra/locations/boundingbox.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A box defined by two farthest corner coordinates on a specific space."""
|
|
16
16
|
|
|
17
|
-
from
|
|
17
|
+
from itertools import product
|
|
18
|
+
import hashlib
|
|
19
|
+
from typing import TYPE_CHECKING, Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
18
22
|
|
|
23
|
+
from . import location, point, pointcloud
|
|
19
24
|
from ..commons import logger
|
|
20
25
|
from ..exceptions import SpaceWarpingFailedError
|
|
21
26
|
|
|
22
|
-
from itertools import product
|
|
23
|
-
import hashlib
|
|
24
|
-
import numpy as np
|
|
25
|
-
from typing import TYPE_CHECKING, Union
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from ..core.structure import BrainStructure
|
|
28
29
|
from nibabel import Nifti1Image
|
|
@@ -85,7 +86,7 @@ class BoundingBox(location.Location):
|
|
|
85
86
|
self.maxpoint[d] = self.minpoint[d] + minsize
|
|
86
87
|
|
|
87
88
|
if self.volume == 0:
|
|
88
|
-
logger.
|
|
89
|
+
logger.debug(f"Zero-volume bounding box from points {point1} and {point2} in {self.space} space.")
|
|
89
90
|
|
|
90
91
|
@property
|
|
91
92
|
def id(self) -> str:
|
|
@@ -148,11 +149,7 @@ class BoundingBox(location.Location):
|
|
|
148
149
|
points_inside = [p for p in other if self.intersects(p)]
|
|
149
150
|
if len(points_inside) == 0:
|
|
150
151
|
return None
|
|
151
|
-
result = pointcloud.
|
|
152
|
-
points_inside,
|
|
153
|
-
space=other.space,
|
|
154
|
-
sigma_mm=[p.sigma for p in points_inside]
|
|
155
|
-
)
|
|
152
|
+
result = pointcloud.from_points(points_inside)
|
|
156
153
|
return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
|
|
157
154
|
|
|
158
155
|
return other.intersection(self)
|
|
@@ -213,7 +210,7 @@ class BoundingBox(location.Location):
|
|
|
213
210
|
X, Y, Z = np.where(mask.get_fdata() > threshold)
|
|
214
211
|
h = np.ones(len(X))
|
|
215
212
|
|
|
216
|
-
# array of
|
|
213
|
+
# array of homogeneous physical nonzero voxel coordinates
|
|
217
214
|
coords = np.dot(mask.affine, np.vstack((X, Y, Z, h)))[:3, :].T
|
|
218
215
|
minpoint = [min(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
|
|
219
216
|
maxpoint = [max(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -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):
|
|
@@ -33,7 +128,7 @@ class Plane3D:
|
|
|
33
128
|
The plane's reference space is defined by the first point.
|
|
34
129
|
"""
|
|
35
130
|
self.space = point1.space
|
|
36
|
-
# normal is the cross product of two
|
|
131
|
+
# normal is the cross product of two arbitrary in-plane vectors
|
|
37
132
|
n = np.cross(
|
|
38
133
|
(point2.warp(self.space) - point1).coordinate,
|
|
39
134
|
(point3.warp(self.space) - point1).coordinate,
|
|
@@ -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.
|
|
@@ -107,7 +200,7 @@ class Plane3D:
|
|
|
107
200
|
)[0]
|
|
108
201
|
faces = mesh["faces"][face_indices]
|
|
109
202
|
|
|
110
|
-
# for each of N selected faces, indicate
|
|
203
|
+
# for each of N selected faces, indicate whether we cross the plane
|
|
111
204
|
# as we go from vertex 2->0, 0->1, 1->2, respectively.
|
|
112
205
|
# This gives us an Nx3 array, where forward crossings are identified by 1,
|
|
113
206
|
# and backward crossings by -1.
|
|
@@ -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
|
|
@@ -160,7 +253,7 @@ class Plane3D:
|
|
|
160
253
|
face_id = 0 # index of the mesh face to consider
|
|
161
254
|
while len(face_indices) > 0:
|
|
162
255
|
|
|
163
|
-
# continue the contour with the next
|
|
256
|
+
# continue the contour with the next forward edge intersection
|
|
164
257
|
p = fwd_intersections[face_id]
|
|
165
258
|
points.append(p)
|
|
166
259
|
# Remember the ids of the face and start-/end vertices for the point
|
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -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,28 @@ 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], "Space"]):
|
|
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
|
-
from ..core.space import Space
|
|
57
55
|
if isinstance(self._space_spec, dict):
|
|
58
56
|
spec = self._space_spec.get("@id") or self._space_spec.get("name")
|
|
59
|
-
self._space_cached = Space.get_instance(spec)
|
|
57
|
+
self._space_cached = _space.Space.get_instance(spec)
|
|
60
58
|
else:
|
|
61
|
-
self._space_cached = Space.get_instance(self._space_spec)
|
|
59
|
+
self._space_cached = _space.Space.get_instance(self._space_spec)
|
|
62
60
|
return self._space_cached
|
|
63
61
|
|
|
64
62
|
@abstractmethod
|
|
65
|
-
def warp(self, space):
|
|
63
|
+
def warp(self, space: Union[str, "_space.Space"]):
|
|
66
64
|
"""Generates a new location by warping the
|
|
67
65
|
current one into another reference space."""
|
|
68
66
|
pass
|
|
69
67
|
|
|
70
68
|
@abstractmethod
|
|
71
|
-
def transform(self, affine: np.ndarray, space=None):
|
|
69
|
+
def transform(self, affine: np.ndarray, space: Union[str, "_space.Space", None] = None):
|
|
72
70
|
"""Returns a new location obtained by transforming the
|
|
73
71
|
reference coordinates of this one with the given affine matrix.
|
|
74
72
|
|
siibra/locations/point.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -14,20 +14,20 @@
|
|
|
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
22
|
from typing import Tuple, Union
|
|
30
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
31
|
|
|
32
32
|
class Point(location.Location):
|
|
33
33
|
"""A single 3D point in reference space."""
|
|
@@ -108,15 +108,13 @@ class Point(location.Location):
|
|
|
108
108
|
|
|
109
109
|
@property
|
|
110
110
|
def homogeneous(self):
|
|
111
|
-
"""The
|
|
111
|
+
"""The homogeneous coordinate of this point as a 4-tuple,
|
|
112
112
|
obtained by appending '1' to the original 3-tuple."""
|
|
113
113
|
return np.atleast_2d(self.coordinate + (1,))
|
|
114
114
|
|
|
115
115
|
def intersection(self, other: location.Location) -> "Point":
|
|
116
116
|
if isinstance(other, Point):
|
|
117
117
|
return self if self == other else None
|
|
118
|
-
elif isinstance(other, pointcloud.PointCloud):
|
|
119
|
-
return self if self in other else None
|
|
120
118
|
else:
|
|
121
119
|
return self if other.intersection(self) else None
|
|
122
120
|
|
|
@@ -157,7 +155,7 @@ class Point(location.Location):
|
|
|
157
155
|
return self.sigma**3 * np.pi * 4. / 3.
|
|
158
156
|
|
|
159
157
|
def __sub__(self, other):
|
|
160
|
-
"""
|
|
158
|
+
"""Subtract the coordinates of two points to get
|
|
161
159
|
a new point representing the offset vector. Alternatively,
|
|
162
160
|
subtract an integer from the all coordinates of this point
|
|
163
161
|
to create a new one.
|
|
@@ -305,13 +303,6 @@ class Point(location.Location):
|
|
|
305
303
|
assert 0 <= index < 3
|
|
306
304
|
return self.coordinate[index]
|
|
307
305
|
|
|
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
306
|
def bigbrain_section(self):
|
|
316
307
|
"""
|
|
317
308
|
Estimate the histological section number of BigBrain
|
siibra/locations/pointcloud.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -14,15 +14,10 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
"""A set of coordinates on a reference space."""
|
|
16
16
|
|
|
17
|
-
from . import location, point, boundingbox as _boundingbox
|
|
18
|
-
|
|
19
|
-
from ..retrieval.requests import HttpRequest
|
|
20
|
-
from ..commons import logger
|
|
21
|
-
from ..exceptions import SpaceWarpingFailedError, EmptyPointCloudError
|
|
22
|
-
|
|
23
17
|
from typing import List, Union, Tuple
|
|
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,11 @@ 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
|
|
37
33
|
|
|
38
34
|
|
|
39
35
|
def from_points(points: List["point.Point"], newlabels: List[Union[int, float, tuple]] = None) -> "PointCloud":
|
|
@@ -120,18 +116,15 @@ class PointCloud(location.Location):
|
|
|
120
116
|
if not isinstance(other, (point.Point, PointCloud, _boundingbox.BoundingBox)):
|
|
121
117
|
return other.intersection(self)
|
|
122
118
|
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
if isinstance(other, PointCloud):
|
|
120
|
+
intersecting_points = [p for p in self if p.coordinate in other.coordinates]
|
|
121
|
+
elif isinstance(other, point.Point):
|
|
122
|
+
return other if other in self else None
|
|
123
|
+
else:
|
|
124
|
+
intersecting_points = [p for p in self if p.intersects(other)]
|
|
125
|
+
if len(intersecting_points) == 0:
|
|
125
126
|
return None
|
|
126
|
-
|
|
127
|
-
labels = None if self.labels is None else [self.labels[i] for i in ids]
|
|
128
|
-
sigma = [p.sigma for p in points]
|
|
129
|
-
intersection = PointCloud(
|
|
130
|
-
points,
|
|
131
|
-
space=self.space,
|
|
132
|
-
sigma_mm=sigma,
|
|
133
|
-
labels=labels
|
|
134
|
-
)
|
|
127
|
+
intersection = from_points(intersecting_points)
|
|
135
128
|
return intersection[0] if len(intersection) == 1 else intersection
|
|
136
129
|
|
|
137
130
|
@property
|
|
@@ -320,7 +313,7 @@ class PointCloud(location.Location):
|
|
|
320
313
|
if not _HAS_HDBSCAN:
|
|
321
314
|
raise RuntimeError(
|
|
322
315
|
f"HDBSCAN is not available with your version {sklearn.__version__} "
|
|
323
|
-
"of sckit-learn. `PointCloud.find_clusters()` will not be
|
|
316
|
+
"of sckit-learn. `PointCloud.find_clusters()` will not be available."
|
|
324
317
|
)
|
|
325
318
|
points = np.array(self.as_list())
|
|
326
319
|
N = points.shape[0]
|
|
@@ -347,3 +340,46 @@ class PointCloud(location.Location):
|
|
|
347
340
|
logger.error("Matplotlib is not available. Label colors is disabled.")
|
|
348
341
|
return None
|
|
349
342
|
return colormaps.rainbow(np.linspace(0, 1, max(self.labels) + 1))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class Contour(PointCloud):
|
|
346
|
+
"""
|
|
347
|
+
A PointCloud that represents a contour line.
|
|
348
|
+
The only difference is that the point order is relevant,
|
|
349
|
+
and consecutive points are thought as being connected by an edge.
|
|
350
|
+
|
|
351
|
+
In fact, PointCloud assumes order as well, but no connections between points.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def __init__(self, coordinates, space=None, sigma_mm=0, labels: list = None):
|
|
355
|
+
PointCloud.__init__(self, coordinates, space, sigma_mm, labels)
|
|
356
|
+
|
|
357
|
+
def crop(self, voi: "_boundingbox.BoundingBox"):
|
|
358
|
+
"""
|
|
359
|
+
Crop the contour with a volume of interest.
|
|
360
|
+
Since the contour might be split from the cropping,
|
|
361
|
+
returns a set of contour segments.
|
|
362
|
+
"""
|
|
363
|
+
segments = []
|
|
364
|
+
|
|
365
|
+
# set the contour point labels to a linear numbering
|
|
366
|
+
# so we can use them after the intersection to detect splits.
|
|
367
|
+
old_labels = self.labels
|
|
368
|
+
self.labels = list(range(len(self)))
|
|
369
|
+
cropped = self.intersection(voi)
|
|
370
|
+
|
|
371
|
+
if cropped is not None and not isinstance(cropped, point.Point):
|
|
372
|
+
assert isinstance(cropped, PointCloud)
|
|
373
|
+
# Identify contour splits are by discontinuouities ("jumps")
|
|
374
|
+
# of their labels, which denote positions in the original contour
|
|
375
|
+
jumps = np.diff([self.labels.index(lb) for lb in cropped.labels])
|
|
376
|
+
splits = [0] + list(np.where(jumps > 1)[0] + 1) + [len(cropped)]
|
|
377
|
+
for i, j in zip(splits[:-1], splits[1:]):
|
|
378
|
+
segments.append(
|
|
379
|
+
self.__class__(cropped.coordinates[i:j, :], space=cropped.space)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# reset labels of the input contour points.
|
|
383
|
+
self.labels = old_labels
|
|
384
|
+
|
|
385
|
+
return segments
|
siibra/retrieval/__init__.py
CHANGED
siibra/retrieval/cache.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2018-
|
|
1
|
+
# Copyright 2018-2025
|
|
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");
|
|
@@ -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
|