siibra 1.0a14__py3-none-any.whl → 1.0.1a0__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 (80) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +15 -5
  3. siibra/commons.py +3 -48
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +1 -1
  6. siibra/configuration/factory.py +164 -127
  7. siibra/core/__init__.py +1 -1
  8. siibra/core/assignment.py +1 -1
  9. siibra/core/atlas.py +24 -17
  10. siibra/core/concept.py +18 -9
  11. siibra/core/parcellation.py +76 -55
  12. siibra/core/region.py +163 -183
  13. siibra/core/space.py +3 -1
  14. siibra/core/structure.py +1 -2
  15. siibra/exceptions.py +17 -1
  16. siibra/experimental/contour.py +6 -6
  17. siibra/experimental/patch.py +2 -2
  18. siibra/experimental/plane3d.py +8 -8
  19. siibra/explorer/__init__.py +1 -1
  20. siibra/explorer/url.py +15 -0
  21. siibra/explorer/util.py +1 -1
  22. siibra/features/__init__.py +1 -1
  23. siibra/features/anchor.py +13 -14
  24. siibra/features/connectivity/__init__.py +1 -1
  25. siibra/features/connectivity/functional_connectivity.py +1 -1
  26. siibra/features/connectivity/regional_connectivity.py +7 -5
  27. siibra/features/connectivity/streamline_counts.py +1 -1
  28. siibra/features/connectivity/streamline_lengths.py +1 -1
  29. siibra/features/connectivity/tracing_connectivity.py +1 -1
  30. siibra/features/dataset/__init__.py +1 -1
  31. siibra/features/dataset/ebrains.py +1 -1
  32. siibra/features/feature.py +50 -28
  33. siibra/features/image/__init__.py +1 -1
  34. siibra/features/image/image.py +18 -13
  35. siibra/features/image/sections.py +1 -1
  36. siibra/features/image/volume_of_interest.py +1 -1
  37. siibra/features/tabular/__init__.py +1 -1
  38. siibra/features/tabular/bigbrain_intensity_profile.py +2 -2
  39. siibra/features/tabular/cell_density_profile.py +102 -66
  40. siibra/features/tabular/cortical_profile.py +5 -3
  41. siibra/features/tabular/gene_expression.py +1 -1
  42. siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
  43. siibra/features/tabular/layerwise_cell_density.py +8 -25
  44. siibra/features/tabular/receptor_density_fingerprint.py +5 -3
  45. siibra/features/tabular/receptor_density_profile.py +5 -3
  46. siibra/features/tabular/regional_timeseries_activity.py +7 -5
  47. siibra/features/tabular/tabular.py +5 -3
  48. siibra/livequeries/__init__.py +1 -1
  49. siibra/livequeries/allen.py +46 -20
  50. siibra/livequeries/bigbrain.py +9 -9
  51. siibra/livequeries/ebrains.py +1 -1
  52. siibra/livequeries/query.py +1 -2
  53. siibra/locations/__init__.py +10 -10
  54. siibra/locations/boundingbox.py +77 -38
  55. siibra/locations/location.py +12 -4
  56. siibra/locations/point.py +14 -9
  57. siibra/locations/{pointset.py → pointcloud.py} +69 -27
  58. siibra/retrieval/__init__.py +1 -1
  59. siibra/retrieval/cache.py +1 -1
  60. siibra/retrieval/datasets.py +1 -1
  61. siibra/retrieval/exceptions/__init__.py +1 -1
  62. siibra/retrieval/repositories.py +10 -27
  63. siibra/retrieval/requests.py +20 -3
  64. siibra/vocabularies/__init__.py +1 -1
  65. siibra/volumes/__init__.py +2 -2
  66. siibra/volumes/parcellationmap.py +121 -94
  67. siibra/volumes/providers/__init__.py +1 -1
  68. siibra/volumes/providers/freesurfer.py +1 -1
  69. siibra/volumes/providers/gifti.py +1 -1
  70. siibra/volumes/providers/neuroglancer.py +68 -42
  71. siibra/volumes/providers/nifti.py +18 -28
  72. siibra/volumes/providers/provider.py +2 -2
  73. siibra/volumes/sparsemap.py +128 -247
  74. siibra/volumes/volume.py +252 -65
  75. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/METADATA +17 -4
  76. siibra-1.0.1a0.dist-info/RECORD +84 -0
  77. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
  78. siibra-1.0a14.dist-info/RECORD +0 -84
  79. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
  80. {siibra-1.0a14.dist-info → siibra-1.0.1a0.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");
@@ -19,12 +19,12 @@ from .query import LiveQuery
19
19
  from ..core import space as _space, structure
20
20
  from ..features import anchor as _anchor
21
21
  from ..features.tabular.gene_expression import GeneExpressions
22
- from ..commons import logger, Species, MapType
23
- from ..locations import point, pointset
22
+ from ..commons import logger, Species
23
+ from ..locations import point, pointcloud
24
24
  from ..retrieval import HttpRequest
25
25
  from ..vocabularies import GENE_NAMES
26
26
 
27
- from typing import Iterable
27
+ from typing import List
28
28
  from xml.etree import ElementTree
29
29
  import numpy as np
30
30
  import json
@@ -32,6 +32,24 @@ import json
32
32
 
33
33
  BASE_URL = "http://api.brain-map.org/api/v2/data"
34
34
 
35
+ LOCATION_PRECISION_MM = 2. # the assumed spatial precision of the probe locations in MNI space
36
+
37
+
38
+ def is_allen_api_microarray_service_available():
39
+ import requests
40
+
41
+ # see https://community.brain-map.org/t/human-brain-atlas-api/2876
42
+ microarray_test_url = "http://api.brain-map.org/api/v2/data/query.json?criteria= service::human_microarray_expression[probes$eq1023146,1023147][donors$eq15496][structures$eq9148]"
43
+ try:
44
+ response = requests.get(microarray_test_url).json()
45
+ except requests.RequestException:
46
+ return False
47
+ return response["success"]
48
+
49
+
50
+ class InvalidAllenAPIResponseException(Exception):
51
+ pass
52
+
35
53
 
36
54
  class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions):
37
55
  """
@@ -101,9 +119,6 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
101
119
  """
102
120
  LiveQuery.__init__(self, **kwargs)
103
121
  gene = kwargs.get('gene')
104
- self.maptype = kwargs.get("maptype", None)
105
- if isinstance(self.maptype, str):
106
- self.maptype = MapType[self.maptype.upper()]
107
122
 
108
123
  def parse_gene(spec):
109
124
  if isinstance(spec, str):
@@ -120,7 +135,14 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
120
135
 
121
136
  self.genes = parse_gene(gene)
122
137
 
123
- def query(self, concept: structure.BrainStructure) -> Iterable[GeneExpressions]:
138
+ def query(self, concept: structure.BrainStructure) -> List[GeneExpressions]:
139
+ if not is_allen_api_microarray_service_available():
140
+ raise InvalidAllenAPIResponseException(
141
+ 'The service "web API of the Allen Brain Atlas for the human microarray expression" '
142
+ 'is not available at the moment, therefore siibra is not able to fetch '
143
+ 'gene expression features. This is a known issue which we are investigating: '
144
+ 'https://github.com/FZJ-INM1-BDA/siibra-python/issues/636.'
145
+ )
124
146
 
125
147
  mnispace = _space.Space.registry().get('mni152')
126
148
 
@@ -128,20 +150,21 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
128
150
  # Record matched instances and their locations.
129
151
  measurements = []
130
152
  coordinates = []
131
- points_inside = dict()
132
153
  for measurement in self:
133
- pt = point.Point(measurement['mni_xyz'], space=mnispace)
134
- if pt not in points_inside: # cache redundant intersection tests
135
- points_inside[pt] = pt in concept
136
- if points_inside[pt]:
154
+ pt = point.Point(measurement['mni_xyz'], space=mnispace, sigma_mm=LOCATION_PRECISION_MM)
155
+ if pt in concept:
137
156
  measurements.append(measurement)
138
157
  coordinates.append(pt)
139
158
 
159
+ if len(coordinates) == 0:
160
+ logger.info(f"No probes found that lie within {concept}")
161
+ return []
162
+
140
163
  # Build the anatomical anchor and assignment to the query concept.
141
164
  # It will be attached to the returned feature, with the set of matched
142
165
  # MNI coordinates as anchor's location.
143
166
  anchor = _anchor.AnatomicalAnchor(
144
- location=pointset.from_points(coordinates),
167
+ location=pointcloud.from_points(coordinates),
145
168
  species=self.species
146
169
  )
147
170
  explanation = f"MNI coordinates of tissue samples were filtered using {concept}"
@@ -153,7 +176,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
153
176
  )]
154
177
  anchor._last_matched_concept = concept
155
178
 
156
- yield GeneExpressions(
179
+ return [GeneExpressions(
157
180
  anchor=anchor,
158
181
  genes=[m['gene'] for m in measurements],
159
182
  levels=[m['expression_level'] for m in measurements],
@@ -167,7 +190,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
167
190
  "probe_id": [m['probe_id'] for m in measurements],
168
191
  "donor_name": [m['donor_name'] for m in measurements],
169
192
  }
170
- )
193
+ )]
171
194
 
172
195
  def __iter__(self):
173
196
 
@@ -261,7 +284,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
261
284
  url = AllenBrainAtlasQuery._QUERY["specimen"].format(specimen_id=specimen_id)
262
285
  response = HttpRequest(url).get()
263
286
  if not response["success"]:
264
- raise Exception(
287
+ raise InvalidAllenAPIResponseException(
265
288
  "Invalid response when retrieving specimen information: {}".format(url)
266
289
  )
267
290
  # we ask for 1 specimen, so list should have length 1
@@ -278,7 +301,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
278
301
  return specimen
279
302
 
280
303
  @classmethod
281
- def _retrieve_microarray(cls, donor_id: str, probe_ids: str) -> Iterable[GeneExpressions]:
304
+ def _retrieve_microarray(cls, donor_id: str, probe_ids: str):
282
305
  """
283
306
  Retrieve microarray data for several probes of a given donor, and
284
307
  compute the MRI position of the corresponding tissue block in the ICBM
@@ -292,9 +315,12 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
292
315
  url = AllenBrainAtlasQuery._QUERY["microarray"].format(
293
316
  probe_ids=",".join([str(id) for id in probe_ids]), donor_id=donor_id
294
317
  )
295
- response = HttpRequest(url, json.loads).get()
318
+ try:
319
+ response = HttpRequest(url, json.loads).get()
320
+ except json.JSONDecodeError as e:
321
+ raise RuntimeError(f"Allen institute site produced an empty response - please try again later.\n{e}")
296
322
  if not response["success"]:
297
- raise Exception(
323
+ raise InvalidAllenAPIResponseException(
298
324
  "Invalid response when retrieving microarray data: {}".format(url)
299
325
  )
300
326
 
@@ -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");
@@ -19,7 +19,7 @@ from . import query
19
19
  from ..features.tabular import bigbrain_intensity_profile, layerwise_bigbrain_intensities
20
20
  from ..features import anchor as _anchor
21
21
  from ..commons import logger
22
- from ..locations import point, pointset
22
+ from ..locations import point, pointcloud
23
23
  from ..core import structure
24
24
  from ..retrieval import requests, cache
25
25
  from ..retrieval.datasets import GenericDataset
@@ -119,11 +119,11 @@ class BigBrainProfileQuery(query.LiveQuery, args=[], FeatureType=bigbrain_intens
119
119
 
120
120
  def query(self, concept: structure.BrainStructure, **kwargs) -> List[bigbrain_intensity_profile.BigBrainIntensityProfile]:
121
121
  loader = WagstylProfileLoader()
122
- mesh_vertices = pointset.PointSet(loader._vertices, space='bigbrain')
123
- matched = concept.intersection(mesh_vertices) # returns a reduced PointSet with og indices as labels
122
+ mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
123
+ matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels
124
124
  if matched is None:
125
125
  return []
126
- assert isinstance(matched, pointset.PointSet)
126
+ assert isinstance(matched, pointcloud.PointCloud)
127
127
  indices = matched.labels
128
128
  assert indices is not None
129
129
  features = []
@@ -159,11 +159,11 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
159
159
  def query(self, concept: structure.BrainStructure, **kwargs) -> List[layerwise_bigbrain_intensities.LayerwiseBigBrainIntensities]:
160
160
 
161
161
  loader = WagstylProfileLoader()
162
- mesh_vertices = pointset.PointSet(loader._vertices, space='bigbrain')
163
- matched = concept.intersection(mesh_vertices) # returns a reduced PointSet with og indices as labels
162
+ mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
163
+ matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels
164
164
  if matched is None:
165
165
  return []
166
- assert isinstance(matched, pointset.PointSet)
166
+ assert isinstance(matched, pointcloud.PointCloud)
167
167
  indices = matched.labels
168
168
  assert indices is not None
169
169
  matched_profiles = loader._profiles[indices, :]
@@ -177,7 +177,7 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
177
177
  ]).reshape((-1, 200))
178
178
 
179
179
  anchor = _anchor.AnatomicalAnchor(
180
- location=pointset.PointSet(loader._vertices[indices, :], space='bigbrain'),
180
+ location=pointcloud.PointCloud(loader._vertices[indices, :], space='bigbrain'),
181
181
  region=str(concept),
182
182
  species='Homo sapiens'
183
183
  )
@@ -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");
@@ -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");
@@ -47,4 +47,3 @@ class LiveQuery(ABC):
47
47
  @abstractmethod
48
48
  def query(self, concept: AtlasConcept, **kwargs) -> List[Feature]:
49
49
  raise NotImplementedError(f"Dervied class {self.__class__} needs to implement query()")
50
- pass
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2023
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");
@@ -16,7 +16,7 @@
16
16
 
17
17
  from .location import Location
18
18
  from .point import Point
19
- from .pointset import PointSet, from_points
19
+ from .pointcloud import PointCloud, from_points
20
20
  from .boundingbox import BoundingBox
21
21
 
22
22
 
@@ -36,11 +36,11 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
36
36
  Returns
37
37
  -------
38
38
  Location
39
- - Point U Point = PointSet
40
- - Point U PointSet = PointSet
41
- - PointSet U PointSet = PointSet
39
+ - Point U Point = PointCloud
40
+ - Point U PointCloud = PointCloud
41
+ - PointCloud U PointCloud = PointCloud
42
42
  - BoundingBox U BoundingBox = BoundingBox
43
- - BoundingBox U PointSet = BoundingBox
43
+ - BoundingBox U PointCloud = BoundingBox
44
44
  - BoundingBox U Point = BoundingBox
45
45
  - WholeBrain U Location = NotImplementedError
46
46
  (all operations are commutative)
@@ -53,14 +53,14 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
53
53
  # location that has its own union method since it is not a part of locations
54
54
  # module and to avoid importing Volume here.
55
55
  if not all(
56
- isinstance(loc, (Point, PointSet, BoundingBox)) for loc in [loc0, loc1]
56
+ isinstance(loc, (Point, PointCloud, BoundingBox)) for loc in [loc0, loc1]
57
57
  ):
58
58
  try:
59
59
  return loc1.union(loc0)
60
60
  except Exception:
61
61
  raise NotImplementedError(f"There are no union method for {(loc0.__class__.__name__, loc1.__class__.__name__)}")
62
62
 
63
- # convert Points to PointSets
63
+ # convert Points to PointClouds
64
64
  loc0, loc1 = [
65
65
  from_points([loc]) if isinstance(loc, Point) else loc
66
66
  for loc in [loc0, loc1]
@@ -69,8 +69,8 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
69
69
  # adopt the space of the first location
70
70
  loc1_w = loc1.warp(loc0.space)
71
71
 
72
- if isinstance(loc0, PointSet):
73
- if isinstance(loc1_w, PointSet):
72
+ if isinstance(loc0, PointCloud):
73
+ if isinstance(loc1_w, PointCloud):
74
74
  points = list(dict.fromkeys([*loc0, *loc1_w]))
75
75
  return from_points(points)
76
76
  if isinstance(loc1_w, BoundingBox):
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2023
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");
@@ -14,11 +14,12 @@
14
14
  # limitations under the License.
15
15
  """A box defined by two farthest corner coordinates on a specific space."""
16
16
 
17
- from . import point, pointset, location
17
+ from . import point, pointcloud, location
18
18
 
19
19
  from ..commons import logger
20
20
  from ..exceptions import SpaceWarpingFailedError
21
21
 
22
+ from itertools import product
22
23
  import hashlib
23
24
  import numpy as np
24
25
  from typing import TYPE_CHECKING, Union
@@ -75,6 +76,7 @@ class BoundingBox(location.Location):
75
76
  s1, s2 = sigma_mm
76
77
  else:
77
78
  raise ValueError(f"Cannot interpret sigma_mm parameter value {sigma_mm} for bounding box")
79
+ self.sigma_mm = [s1, s2]
78
80
  self.minpoint = point.Point([min(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s1)
79
81
  self.maxpoint = point.Point([max(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s2)
80
82
  if minsize is not None:
@@ -82,6 +84,9 @@ class BoundingBox(location.Location):
82
84
  if self.shape[d] < minsize:
83
85
  self.maxpoint[d] = self.minpoint[d] + minsize
84
86
 
87
+ if self.volume == 0:
88
+ logger.warning(f"Zero-volume bounding box from points {point1} and {point2} in {self.space} space.")
89
+
85
90
  @property
86
91
  def id(self) -> str:
87
92
  return hashlib.md5(str(self).encode("utf-8")).hexdigest()
@@ -110,12 +115,12 @@ class BoundingBox(location.Location):
110
115
  def __str__(self):
111
116
  if self.space is None:
112
117
  return (
113
- f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)}) mm "
114
- f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)}) mm"
118
+ f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
119
+ f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm"
115
120
  )
116
121
  else:
117
122
  return (
118
- f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)}) mm "
123
+ f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
119
124
  f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm in {self.space.name} space"
120
125
  )
121
126
 
@@ -139,16 +144,16 @@ class BoundingBox(location.Location):
139
144
  if isinstance(other, point.Point):
140
145
  warped = other.warp(self.space)
141
146
  return other if self.minpoint <= warped <= self.maxpoint else None
142
- if isinstance(other, pointset.PointSet):
147
+ if isinstance(other, pointcloud.PointCloud):
143
148
  points_inside = [p for p in other if self.intersects(p)]
144
- result = pointset.PointSet(
149
+ if len(points_inside) == 0:
150
+ return None
151
+ result = pointcloud.PointCloud(
145
152
  points_inside,
146
153
  space=other.space,
147
154
  sigma_mm=[p.sigma for p in points_inside]
148
155
  )
149
- if len(result) == 0:
150
- return None
151
- return result[0] if len(result) == 1 else result # if PointSet has single point return as a Point
156
+ return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
152
157
 
153
158
  return other.intersection(self)
154
159
 
@@ -183,12 +188,18 @@ class BoundingBox(location.Location):
183
188
  result_minpt.append(A[dim])
184
189
  result_maxpt.append(B[dim])
185
190
 
191
+ if result_minpt == result_maxpt:
192
+ return result_minpt
193
+
186
194
  bbox = BoundingBox(
187
195
  point1=point.Point(result_minpt, self.space),
188
196
  point2=point.Point(result_maxpt, self.space),
189
197
  space=self.space,
190
198
  )
191
- return bbox if bbox.volume > 0 else None
199
+
200
+ if bbox.volume == 0 and sum(cmin == cmax for cmin, cmax in zip(result_minpt, result_maxpt)) == 2:
201
+ return None
202
+ return bbox
192
203
 
193
204
  def _intersect_mask(self, mask: 'Nifti1Image', threshold=0):
194
205
  """Intersect this bounding box with an image mask. Returns None if they do not intersect.
@@ -229,8 +240,8 @@ class BoundingBox(location.Location):
229
240
  )
230
241
  else:
231
242
  return self._intersect_bbox(
232
- pointset
233
- .PointSet(XYZ, space=self.space, sigma_mm=voxel_size.max())
243
+ pointcloud
244
+ .PointCloud(XYZ, space=self.space, sigma_mm=voxel_size.max())
234
245
  .boundingbox
235
246
  )
236
247
 
@@ -245,6 +256,35 @@ class BoundingBox(location.Location):
245
256
  sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma]
246
257
  )
247
258
 
259
+ @property
260
+ def corners(self):
261
+ """
262
+ Returns all 8 corners of the box as a pointcloud.
263
+
264
+ Note
265
+ ----
266
+ x0, y0, z0 = self.minpoint
267
+ x1, y1, z1 = self.maxpoint
268
+ all_corners = [
269
+ (x0, y0, z0),
270
+ (x1, y0, z0),
271
+ (x0, y1, z0),
272
+ (x1, y1, z0),
273
+ (x0, y0, z1),
274
+ (x1, y0, z1),
275
+ (x0, y1, z1),
276
+ (x1, y1, z1)
277
+ ]
278
+
279
+ TODO: deal with sigma. Currently, returns the mean of min and max point.
280
+ """
281
+ xs, ys, zs = zip(self.minpoint, self.maxpoint)
282
+ return pointcloud.PointCloud(
283
+ coordinates=[[x, y, z] for x, y, z in product(xs, ys, zs)],
284
+ space=self.space,
285
+ sigma_mm=np.mean([self.minpoint.sigma, self.maxpoint.sigma])
286
+ )
287
+
248
288
  def warp(self, space):
249
289
  """Returns a new bounding box obtained by warping the
250
290
  min- and maxpoint of this one into the new target space.
@@ -257,13 +297,10 @@ class BoundingBox(location.Location):
257
297
  return self
258
298
  else:
259
299
  try:
260
- return self.__class__(
261
- point1=self.minpoint.warp(spaceobj),
262
- point2=self.maxpoint.warp(spaceobj),
263
- space=spaceobj,
264
- )
265
- except ValueError:
300
+ warped_corners = self.corners.warp(spaceobj)
301
+ except SpaceWarpingFailedError:
266
302
  raise SpaceWarpingFailedError(f"Warping {str(self)} to {spaceobj.name} not successful.")
303
+ return warped_corners.boundingbox
267
304
 
268
305
  def transform(self, affine: np.ndarray, space=None):
269
306
  """Returns a new bounding box obtained by transforming the
@@ -282,12 +319,9 @@ class BoundingBox(location.Location):
282
319
  """
283
320
  from ..core.space import Space
284
321
  spaceobj = Space.get_instance(space)
285
- return self.__class__(
286
- point1=self.minpoint.transform(affine, spaceobj),
287
- point2=self.maxpoint.transform(affine, spaceobj),
288
- space=space,
289
- sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma] # TODO: error propagation
290
- )
322
+ result = self.corners.transform(affine, spaceobj).boundingbox
323
+ result.sigma_mm = [self.minpoint.sigma, self.maxpoint.sigma] # TODO: error propagation
324
+ return result
291
325
 
292
326
  def shift(self, offset):
293
327
  return self.__class__(
@@ -326,7 +360,7 @@ class BoundingBox(location.Location):
326
360
  x1, y1, z1 = self.maxpoint
327
361
 
328
362
  # set of 8 corner points in source space
329
- corners1 = pointset.PointSet(
363
+ corners1 = pointcloud.PointCloud(
330
364
  [
331
365
  (x0, y0, z0),
332
366
  (x0, y0, z1),
@@ -383,14 +417,15 @@ class BoundingBox(location.Location):
383
417
  return super().__hash__()
384
418
 
385
419
 
386
- def _determine_bounds(array: np.ndarray, threshold=0):
420
+ def _determine_bounds(masked_array: np.ndarray, background: float = 0.0):
387
421
  """
388
- Bounding box of nonzero values in a 3D array.
422
+ Bounding box of nonzero (background) values in a 3D array.
423
+
389
424
  https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
390
425
  """
391
- x = np.any(array > threshold, axis=(1, 2))
392
- y = np.any(array > threshold, axis=(0, 2))
393
- z = np.any(array > threshold, axis=(0, 1))
426
+ x = np.any(masked_array != background, axis=(1, 2))
427
+ y = np.any(masked_array != background, axis=(0, 2))
428
+ z = np.any(masked_array != background, axis=(0, 1))
394
429
  nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
395
430
  if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
396
431
  # empty array
@@ -401,15 +436,19 @@ def _determine_bounds(array: np.ndarray, threshold=0):
401
436
  return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
402
437
 
403
438
 
404
- def from_array(array: np.ndarray, threshold=0, space: "Space" = None) -> "BoundingBox":
439
+ def from_array(
440
+ array: np.ndarray,
441
+ background: Union[int, float] = 0.0,
442
+ space: "Space" = None
443
+ ) -> BoundingBox:
405
444
  """
406
- Find the bounding box of an array.
445
+ Find the bounding box of non-background values for any 3D array.
407
446
 
408
447
  Parameters
409
448
  ----------
410
- array : np.ndarray
411
- threshold : int, default: 0
412
- space : Space, default: None
449
+ array: np.ndarray
450
+ background: int or float, default: 0.0
451
+ space: Space, default: None
413
452
  """
414
- bounds = _determine_bounds(array, threshold)
415
- return BoundingBox(bounds[:3, 0], bounds[:3, 1], space=space)
453
+ bounds = _determine_bounds(array, background)
454
+ return BoundingBox(point1=bounds[:3, 0], point2=bounds[:3, 1], space=space)
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2023
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");
@@ -21,6 +21,10 @@ from ..core.structure import BrainStructure
21
21
  import numpy as np
22
22
  from abc import abstractmethod
23
23
 
24
+ from typing import TYPE_CHECKING, Union, Dict
25
+ if TYPE_CHECKING:
26
+ from siibra.core.space import Space
27
+
24
28
 
25
29
  class Location(BrainStructure):
26
30
  """
@@ -42,15 +46,19 @@ class Location(BrainStructure):
42
46
  _MASK_MEMO = {} # cache region masks for Location._assign_region()
43
47
  _ASSIGNMENT_CACHE = {} # caches assignment results, see Region.assign()
44
48
 
45
- def __init__(self, space):
46
- self._space_spec = space
49
+ def __init__(self, spacespec: Union[str, Dict[str, str], "Space"]):
50
+ self._space_spec = spacespec
47
51
  self._space_cached = None
48
52
 
49
53
  @property
50
54
  def space(self):
51
55
  if self._space_cached is None:
52
56
  from ..core.space import Space
53
- self._space_cached = Space.get_instance(self._space_spec)
57
+ if isinstance(self._space_spec, dict):
58
+ spec = self._space_spec.get("@id") or self._space_spec.get("name")
59
+ self._space_cached = Space.get_instance(spec)
60
+ else:
61
+ self._space_cached = Space.get_instance(self._space_spec)
54
62
  return self._space_cached
55
63
 
56
64
  @abstractmethod
siibra/locations/point.py CHANGED
@@ -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");
@@ -14,10 +14,11 @@
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, pointset
17
+ from . import location, boundingbox, pointcloud
18
18
 
19
19
  from ..commons import logger
20
20
  from ..retrieval.requests import HttpRequest
21
+ from ..exceptions import SpaceWarpingFailedError, NoneCoordinateSuppliedError
21
22
 
22
23
  from urllib.parse import quote
23
24
  import re
@@ -55,10 +56,14 @@ class Point(location.Location):
55
56
  if len(digits) == 3:
56
57
  return tuple(float(d) for d in digits)
57
58
  elif isinstance(spec, (tuple, list)) and len(spec) in [3, 4]:
59
+ if any(v is None for v in spec):
60
+ raise NoneCoordinateSuppliedError("Cannot parse cooridantes containing None values.")
58
61
  if len(spec) == 4:
59
62
  assert spec[3] == 1
60
63
  return tuple(float(v.item()) if isinstance(v, np.ndarray) else float(v) for v in spec[:3])
61
64
  elif isinstance(spec, np.ndarray) and spec.size == 3:
65
+ if any(np.isnan(v) for v in spec):
66
+ raise NoneCoordinateSuppliedError("Cannot parse cooridantes containing NaN values.")
62
67
  return tuple(float(v.item()) if isinstance(v, np.ndarray) else float(v) for v in spec[:3])
63
68
  elif isinstance(spec, Point):
64
69
  return spec.coordinate
@@ -110,7 +115,7 @@ class Point(location.Location):
110
115
  def intersection(self, other: location.Location) -> "Point":
111
116
  if isinstance(other, Point):
112
117
  return self if self == other else None
113
- elif isinstance(other, pointset.PointSet):
118
+ elif isinstance(other, pointcloud.PointCloud):
114
119
  return self if self in other else None
115
120
  else:
116
121
  return self if other.intersection(self) else None
@@ -125,7 +130,7 @@ class Point(location.Location):
125
130
  if spaceobj == self.space:
126
131
  return self
127
132
  if any(_ not in location.Location.SPACEWARP_IDS for _ in [self.space.id, spaceobj.id]):
128
- raise ValueError(
133
+ raise SpaceWarpingFailedError(
129
134
  f"Cannot convert coordinates between {self.space.id} and {spaceobj.id}"
130
135
  )
131
136
  url = "{server}/transform-point?source_space={src}&target_space={tgt}&x={x}&y={y}&z={z}".format(
@@ -137,9 +142,9 @@ class Point(location.Location):
137
142
  z=self.coordinate[2],
138
143
  )
139
144
  response = HttpRequest(url, lambda b: json.loads(b.decode())).get()
140
- if any(map(np.isnan, response['target_point'])):
141
- logger.debug(f'Warping {str(self)} to {spaceobj.name} resulted in NaN')
142
- return None
145
+ if np.any(np.isnan(response['target_point'])):
146
+ raise SpaceWarpingFailedError(f'Warping {str(self)} to {spaceobj.name} resulted in NaN')
147
+
143
148
  return self.__class__(
144
149
  coordinatespec=tuple(response["target_point"]),
145
150
  space=spaceobj.id,
@@ -185,8 +190,8 @@ class Point(location.Location):
185
190
  return super().__hash__()
186
191
 
187
192
  def __eq__(self, other: 'Point'):
188
- if isinstance(other, pointset.PointSet):
189
- return other == self # implemented at pointset
193
+ if isinstance(other, pointcloud.PointCloud):
194
+ return other == self # implemented at pointcloud
190
195
  if not isinstance(other, Point):
191
196
  return False
192
197
  o = other if self.space is None else other.warp(self.space)