Skip to content

Commit 72a3684

Browse files
committed
Maint provider.get_boundingbox(), volume.boundingbox -> volume.get_boundingbox()
1 parent ccd4547 commit 72a3684

File tree

9 files changed

+139
-52
lines changed

9 files changed

+139
-52
lines changed

e2e/core/test_region.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,5 +121,5 @@ def test_boundingbox(space):
121121

122122
v_l = hoc1_l.get_regional_map(space)
123123
v_r = hoc1_r.get_regional_map(space)
124-
assert bbox_l == v_l.boundingbox, "Boundingbox of regional mask should be the same as bouding mask of the regions"
125-
assert bbox_r == v_r.boundingbox, "Boundingbox of regional mask should be the same as bouding mask of the regions"
124+
assert bbox_l == v_l.get_boundingbox(clip=True, background=0.0), "Boundingbox of regional mask should be the same as bouding mask of the regions"
125+
assert bbox_r == v_r.get_boundingbox(clip=True, background=0.0), "Boundingbox of regional mask should be the same as bouding mask of the regions"

siibra/core/region.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def get_regional_map(
535535
# provided by 'via_space'. Now transform the affine to match the
536536
# desired target space.
537537
intermediary_result = result
538-
transform = intermediary_result.boundingbox.estimate_affine(space)
538+
transform = intermediary_result.get_boundingbox(clip=True, background=0.0).estimate_affine(space)
539539
result = volume.from_array(
540540
imgdata,
541541
np.dot(transform, region_img.affine),
@@ -682,6 +682,7 @@ def get_bounding_box(
682682
space: _space.Space,
683683
maptype: MapType = MapType.LABELLED,
684684
threshold_statistical=None,
685+
**fetch_kwargs
685686
):
686687
"""Compute the bounding box of this region in the given space.
687688
@@ -705,7 +706,7 @@ def get_bounding_box(
705706
mask = self.get_regional_map(
706707
spaceobj, maptype=maptype, threshold=threshold_statistical
707708
)
708-
return mask.boundingbox
709+
return mask.get_boundingbox(clip=True, background=0.0, **fetch_kwargs)
709710
except (RuntimeError, ValueError):
710711
for other_space in self.parcellation.spaces - spaceobj:
711712
try:
@@ -718,7 +719,7 @@ def get_bounding_box(
718719
f"No bounding box for {self.name} defined in {spaceobj.name}, "
719720
f"will warp the bounding box from {other_space.name} instead."
720721
)
721-
bbox = mask.boundingbox
722+
bbox = mask.get_boundingbox(clip=True, background=0.0, **fetch_kwargs)
722723
if bbox is not None:
723724
return bbox.warp(spaceobj)
724725
except RuntimeError:

siibra/features/image/image.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def location(self):
4242
Loads the bounding box only if required, since it demands image data access.
4343
"""
4444
if self._location_cached is None:
45-
self._location_cached = self.volume.boundingbox
45+
self._location_cached = self.volume.get_boundingbox(clip=False) # use unclipped to preseve exisiting behaviour
4646
return self._location_cached
4747

4848
@property

siibra/locations/boundingbox.py

+42-20
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121

2222
import hashlib
2323
import numpy as np
24-
from typing import TYPE_CHECKING
24+
from typing import TYPE_CHECKING, Union
2525
if TYPE_CHECKING:
2626
from ..core.structure import BrainStructure
2727
from nibabel import Nifti1Image
28+
from ..core.space import Space
2829

2930

3031
class BoundingBox(location.Location):
@@ -35,7 +36,14 @@ class BoundingBox(location.Location):
3536
from the two corner points.
3637
"""
3738

38-
def __init__(self, point1, point2, space=None, minsize: float = None, sigma_mm=None):
39+
def __init__(
40+
self,
41+
point1,
42+
point2,
43+
space: Union[str, 'Space'] = None,
44+
minsize: float = None,
45+
sigma_mm=None
46+
):
3947
"""
4048
Construct a new bounding box spanned by two 3D coordinates
4149
in the given reference space.
@@ -99,24 +107,6 @@ def shape(self) -> float:
99107
def is_planar(self) -> bool:
100108
return any(d == 0 for d in self.shape)
101109

102-
@staticmethod
103-
def _determine_bounds(A, threshold=0):
104-
"""
105-
Bounding box of nonzero values in a 3D array.
106-
https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
107-
"""
108-
x = np.any(A > threshold, axis=(1, 2))
109-
y = np.any(A > threshold, axis=(0, 2))
110-
z = np.any(A > threshold, axis=(0, 1))
111-
nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
112-
if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
113-
# empty array
114-
return None
115-
xmin, xmax = nzx[0][[0, -1]]
116-
ymin, ymax = nzy[0][[0, -1]]
117-
zmin, zmax = nzz[0][[0, -1]]
118-
return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
119-
120110
def __str__(self):
121111
if self.space is None:
122112
return (
@@ -390,3 +380,35 @@ def __eq__(self, other: 'BoundingBox'):
390380

391381
def __hash__(self):
392382
return super().__hash__()
383+
384+
385+
def _determine_bounds(array: np.ndarray, threshold=0):
386+
"""
387+
Bounding box of nonzero values in a 3D array.
388+
https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
389+
"""
390+
x = np.any(array > threshold, axis=(1, 2))
391+
y = np.any(array > threshold, axis=(0, 2))
392+
z = np.any(array > threshold, axis=(0, 1))
393+
nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
394+
if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
395+
# empty array
396+
return None
397+
xmin, xmax = nzx[0][[0, -1]]
398+
ymin, ymax = nzy[0][[0, -1]]
399+
zmin, zmax = nzz[0][[0, -1]]
400+
return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
401+
402+
403+
def from_array(array: np.ndarray, threshold=0, space: "Space" = None) -> "BoundingBox":
404+
"""
405+
Find the bounding box of an array.
406+
407+
Parameters
408+
----------
409+
array : np.ndarray
410+
threshold : int, default: 0
411+
space : Space, default: None
412+
"""
413+
bounds = _determine_bounds(array, threshold)
414+
return BoundingBox(bounds[:3, 0], bounds[:3, 1], space=space)

siibra/volumes/providers/gifti.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ def __init__(self, url: Union[str, Dict[str, str]], volume=None):
4444
def _url(self) -> Union[str, Dict[str, str]]:
4545
return self._init_url
4646

47-
def get_boundingbox(self, clip=False, background=0.0) -> '_boundingbox.BoundingBox':
47+
def get_boundingbox(self, clip=False, background=0.0, **fetch_kwargs) -> '_boundingbox.BoundingBox':
48+
"""
49+
Bounding box calculation is not yet implemented for meshes.
50+
"""
4851
raise NotImplementedError(
4952
f"Bounding box access to {self.__class__.__name__} objects not yet implemented."
5053
)

siibra/volumes/providers/neuroglancer.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,23 @@ def fetch(
118118

119119
return result
120120

121-
def get_boundingbox(self, clip=False, background=0) -> "_boundingbox.BoundingBox":
121+
def get_boundingbox(self, clip=False, background=0, **fetch_kwargs) -> "_boundingbox.BoundingBox":
122122
"""
123-
Return the bounding box in physical coordinates
124-
of the union of fragments in this neuroglancer volume.
123+
Return the bounding box in physical coordinates of the union of
124+
fragments in this neuroglancer volume.
125+
126+
Parameters
127+
----------
128+
clip: bool, default: True
129+
Whether to clip the background of the volume.
130+
background: float, default: 0.0
131+
The background value to clip.
132+
Note
133+
----
134+
To use it, clip must be True.
135+
fetch_kwargs:
136+
key word arguments that are used for fetchin volumes,
137+
such as voi or resolution_mm.
125138
"""
126139
bbox = None
127140
for frag in self._fragments.values():
@@ -131,11 +144,10 @@ def get_boundingbox(self, clip=False, background=0) -> "_boundingbox.BoundingBox
131144
f"bounding box considers only {frag.shape[:3]}"
132145
)
133146
if clip:
134-
img = frag.fetch()
135-
bounds = _boundingbox.BoundingBox._determine_bounds(np.asanyarray(img.dataobj), background)
136-
next_bbox = _boundingbox.BoundingBox(
137-
bounds[:3, 0], bounds[:3, 1], space=None
138-
)
147+
img = frag.fetch(**fetch_kwargs)
148+
next_bbox = _boundingbox.from_array(
149+
np.asanyarray(img.dataobj), threshold=background, space=None
150+
).transform(img.affine) # use the affine of the image matching fetch_kwargs
139151
else:
140152
shape = frag.shape[:3]
141153
next_bbox = _boundingbox.BoundingBox(
@@ -499,7 +511,10 @@ def __init__(self, resource: Union[str, dict], volume=None):
499511
def _url(self) -> Union[str, Dict[str, str]]:
500512
return self._init_url
501513

502-
def get_boundingbox(self, clip=False, background=0.0) -> '_boundingbox.BoundingBox':
514+
def get_boundingbox(self, clip=False, background=0.0, **fetch_kwargs) -> '_boundingbox.BoundingBox':
515+
"""
516+
Bounding box calculation is not yet implemented for meshes.
517+
"""
503518
raise NotImplementedError(
504519
f"Bounding box access to {self.__class__.__name__} objects not yet implemented."
505520
)

siibra/volumes/providers/nifti.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,28 @@ def _url(self) -> Union[str, Dict[str, str]]:
6565
def fragments(self):
6666
return [k for k in self._img_loaders if k is not None]
6767

68-
def get_boundingbox(self, clip=True, background=0) -> "_boundingbox.BoundingBox":
68+
def get_boundingbox(self, clip=True, background=0, **fetch_kwargs) -> "_boundingbox.BoundingBox":
6969
"""
70-
Return the bounding box in physical coordinates
71-
of the union of fragments in this nifti volume.
70+
Return the bounding box in physical coordinates of the union of
71+
fragments in this nifti volume.
72+
73+
Parameters
74+
----------
75+
clip : bool, default: True
76+
Whether to clip the background of the volume.
77+
background : float, default: 0.0
78+
The background value to clip.
79+
Note
80+
----
81+
To use it, clip must be True.
82+
fetch_kwargs:
83+
Not used
7284
"""
85+
if fetch_kwargs:
86+
logger.warning(
87+
"`volume.fetch()` keyword arguments supplied. Nifti volumes"
88+
" cannot pass them for bounding box calculation."
89+
)
7390
bbox = None
7491
for loader in self._img_loaders.values():
7592
img = loader()
@@ -79,10 +96,9 @@ def get_boundingbox(self, clip=True, background=0) -> "_boundingbox.BoundingBox"
7996
f"bounding box considers only {img.shape[:3]}"
8097
)
8198
if clip:
82-
bounds = _boundingbox.BoundingBox._determine_bounds(np.asanyarray(img.dataobj), background)
83-
next_bbox = _boundingbox.BoundingBox(
84-
bounds[:3, 0], bounds[:3, 1], space=None
85-
)
99+
next_bbox = _boundingbox.from_array(
100+
np.asanyarray(img.dataobj), threshold=background, space=None
101+
).transform(img.affine)
86102
else:
87103
shape = img.shape[:3]
88104
next_bbox = _boundingbox.BoundingBox(
@@ -93,7 +109,7 @@ def get_boundingbox(self, clip=True, background=0) -> "_boundingbox.BoundingBox"
93109

94110
def _merge_fragments(self) -> nib.Nifti1Image:
95111
# TODO this only performs nearest neighbor interpolation, optimized for float types.
96-
bbox = self.get_boundingbox(clip=True, background=0.0)
112+
bbox = self.get_boundingbox(clip=False, background=0.0)
97113
num_conflicts = 0
98114
result = None
99115

@@ -174,7 +190,7 @@ def fetch(
174190
result = loader()
175191

176192
if voi is not None:
177-
bb_vox = voi.transform_bbox(np.linalg.inv(result.affine))
193+
bb_vox = voi.transform(np.linalg.inv(result.affine))
178194
(x0, y0, z0), (x1, y1, z1) = bb_vox.minpoint, bb_vox.maxpoint
179195
shift = np.identity(4)
180196
shift[:3, -1] = bb_vox.minpoint

siibra/volumes/providers/provider.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ def __init__(self, parent_provider: VolumeProvider, z: int):
8383
self.srctype = parent_provider.srctype
8484
self.z = z
8585

86-
def get_boundingbox(self, clip=True, background=0.0) -> "BoundingBox":
87-
return self.provider.get_boundingbox(clip=clip, background=background)
86+
def get_boundingbox(self, clip=True, background=0.0, **fetch_kwargs) -> "BoundingBox":
87+
return self.provider.get_boundingbox(clip=clip, background=background, **fetch_kwargs)
8888

8989
def fetch(self, **kwargs):
9090
# activate caching at the caller using "with SubvolumeProvider.UseCaching():""

siibra/volumes/volume.py

+35-5
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,41 @@ def concat(url: Union[str, Dict[str, str]], concat: str):
115115
for srctype, prov in self._providers.items()
116116
}
117117

118-
@property
119-
def boundingbox(self):
120-
for provider in self._providers.values():
118+
def get_boundingbox(self, clip: bool = True, background: float = 0.0, **fetch_kwargs) -> "boundingbox.BoundingBox":
119+
"""
120+
Obtain the bounding box in physical coordinates of this volume.
121+
122+
Parameters
123+
----------
124+
clip : bool, default: True
125+
Whether to clip the background of the volume.
126+
background : float, default: 0.0
127+
The background value to clip.
128+
Note
129+
----
130+
To use it, clip must be True.
131+
fetch_kwargs:
132+
key word arguments that are used for fetchin volumes,
133+
such as voi or resolution_mm. Currently, only possible for
134+
Neuroglancer volumes except for `format`.
135+
136+
Raises
137+
------
138+
RuntimeError
139+
If the volume provider does not have a bounding box calculator.
140+
"""
141+
fmt = fetch_kwargs.get("format")
142+
if fmt in self._providers:
143+
raise ValueError(
144+
f"Requested format {fmt} is not available as provider of "
145+
"this volume. See `volume.formats` for possible options."
146+
)
147+
providers = [self._providers[fmt]] if fmt else self._providers.values()
148+
for provider in providers:
121149
try:
122-
bbox = provider.get_boundingbox()
150+
bbox = provider.get_boundingbox(
151+
clip=clip, background=background, **fetch_kwargs
152+
)
123153
if bbox.space is None: # provider does usually not know the space!
124154
bbox._space_cached = self.space
125155
bbox.minpoint._space_cached = self.space
@@ -230,7 +260,7 @@ def intersection(self, other: structure.BrainStructure, **kwargs) -> structure.B
230260
return None # BrainStructure.intersects check for not None
231261
return result[0] if len(result) == 1 else result # if PointSet has single point return as a Point
232262
elif isinstance(other, boundingbox.BoundingBox):
233-
return self.boundingbox.intersection(other)
263+
return self.get_boundingbox(clip=True, background=0.0, **kwargs).intersection(other)
234264
elif isinstance(other, Volume):
235265
format = kwargs.pop('format', 'image')
236266
v1 = self.fetch(format=format, **kwargs)

0 commit comments

Comments
 (0)