Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use precalculated bounding boxes of image-features to speed up image feature query #583

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
31 changes: 31 additions & 0 deletions e2e/volumes/test_preconfigured_boundingbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import siibra
from siibra.features.image.image import Image
from siibra.volumes.volume import Volume
import pytest

space_vols = [v for s in siibra.spaces for v in s.volumes]
map_vols = [v for m in siibra.maps for v in m.volumes]
imagefeatures = [
feat
for ftype in siibra.features.Feature._SUBCLASSES[Image]
for feat in ftype._get_instances()
]
volumes = space_vols + map_vols + imagefeatures


@pytest.mark.parametrize("volume", volumes)
def test_onthefly_and_preconfig_bboxes(volume: Volume):
configured_bbox = volume._boundingbox
if configured_bbox is None:
pytest.skip(f"No preconfigured BoundingBox for {volume} is found.")
volume._boundingbox = None
kwargs = {"clip": True}
if "neuroglancer/precomputed" in volume.providers:
kwargs.update({
"clip": False,
"resolution_mm": -1,
"format": "neuroglancer/precomputed",
"max_bytes": 2 * 1024**3
})
bbox = volume.get_boundingbox(**kwargs)
assert configured_bbox == bbox, f'{volume}'
2 changes: 1 addition & 1 deletion siibra/configuration/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Configuration:
conn(
server_or_owner,
project_or_repo,
reftag="siibra-{}".format(__version__),
reftag="enh_use_precalc_img_bboxes",
skip_branchtest=True
)
for conn, server_or_owner, project_or_repo in CONFIG_REPOS
Expand Down
47 changes: 31 additions & 16 deletions siibra/configuration/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
from ..features.image import sections, volume_of_interest
from ..core import atlas, parcellation, space, region
from ..locations import point, pointset
from ..locations import point, pointset, boundingbox
from ..retrieval import datasets, repositories
from ..volumes import volume, sparsemap, parcellationmap
from ..volumes.providers.provider import VolumeProvider
Expand Down Expand Up @@ -253,11 +253,9 @@ def build_parcellation(cls, spec):
return p

@classmethod
@build_type("siibra/volume/v0.0.1")
def build_volume(cls, spec):
providers: List[volume.VolumeProvider] = []

for srctype, provider_spec in spec.get("providers", {}).items():
def build_volumeproviders(cls, provider_specs: Dict) -> List["VolumeProvider"]:
providers: List[VolumeProvider] = []
for srctype, provider_spec in provider_specs.items():
for ProviderType in VolumeProvider._SUBCLASSES:
if srctype == ProviderType.srctype:
providers.append(ProviderType(provider_spec))
Expand All @@ -266,15 +264,22 @@ def build_volume(cls, spec):
if srctype not in cls._warnings_issued:
logger.warning(f"No provider defined for volume Source type {srctype}")
cls._warnings_issued.append(srctype)

assert all([isinstance(p, VolumeProvider) for p in providers])
return providers

@classmethod
@build_type("siibra/volume/v0.0.1")
def build_volume(cls, spec):
result = volume.Volume(
space_spec=spec.get("space", {}),
providers=providers,
providers=cls.build_volumeproviders(spec.get("providers")),
name=spec.get("name", ""),
variant=spec.get("variant"),
datasets=cls.extract_datasets(spec),
bbox=cls.build_boundingbox(spec)
)
if result._boundingbox is not None:
assert result._boundingbox._space_spec == result._space_spec, "BoundingBox of a volume cannot be in a different space than the volume's space."

return result

Expand Down Expand Up @@ -349,6 +354,16 @@ def build_pointset(cls, spec):
coords = [tuple(c) for c in spec.get("coordinates")]
return pointset.PointSet(coords, space=space_id)

@classmethod
@build_type("siibra/location/boundingbox/v0.1")
def build_boundingbox(cls, spec):
bboxspec = spec.get("boundingbox", None)
if bboxspec is None:
return None
space_spec = bboxspec.get("space")
coords = [tuple(c) for c in bboxspec.get("coordinates")]
return boundingbox.BoundingBox(coords[0], coords[1], space=space_spec)

@classmethod
@build_type("siibra/feature/fingerprint/receptor/v0.1")
def build_receptor_density_fingerprint(cls, spec):
Expand Down Expand Up @@ -392,13 +407,13 @@ def build_cell_density_profile(cls, spec):
@classmethod
@build_type("siibra/feature/section/v0.1")
def build_section(cls, spec):
vol = cls.build_volume(spec)
kwargs = {
"name": spec.get('name', ""),
"name": spec.get("name"),
"region": spec.get('region', None),
"space_spec": vol._space_spec,
"providers": vol._providers.values(),
"space_spec": spec.get("space"),
"providers": cls.build_volumeproviders(spec.get("providers")),
"datasets": cls.extract_datasets(spec),
"bbox": cls.build_boundingbox(spec)
}
modality = spec.get('modality', "")
if modality == "cell body staining":
Expand All @@ -409,13 +424,13 @@ def build_section(cls, spec):
@classmethod
@build_type("siibra/feature/voi/v0.1")
def build_volume_of_interest(cls, spec):
vol = cls.build_volume(spec)
kwargs = {
"name": spec.get('name', ""),
"name": spec.get("name"),
"region": spec.get('region', None),
"space_spec": vol._space_spec,
"providers": vol._providers.values(),
"space_spec": spec.get("space"),
"providers": cls.build_volumeproviders(spec.get("providers")),
"datasets": cls.extract_datasets(spec),
"bbox": cls.build_boundingbox(spec)
}
modality = spec.get('modality', "")
if modality == "cell body staining":
Expand Down
10 changes: 7 additions & 3 deletions siibra/features/image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
from .. import anchor as _anchor

from ...volumes import volume as _volume
from ...volumes.providers import provider

from typing import List
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from ...locations.boundingbox import BoundingBox
from ...volumes.providers import provider


class ImageAnchor(_anchor.AnatomicalAnchor):
Expand Down Expand Up @@ -60,9 +62,10 @@ def __init__(
name: str,
modality: str,
space_spec: dict,
providers: List[provider.VolumeProvider],
providers: List["provider.VolumeProvider"],
region: str = None,
datasets: List = [],
bbox: "BoundingBox" = None
):
feature.Feature.__init__(
self,
Expand All @@ -78,6 +81,7 @@ def __init__(
providers=providers,
name=name,
datasets=datasets,
bbox=bbox
)

self._anchor_cached = ImageAnchor(self, region=region)
Expand Down
14 changes: 11 additions & 3 deletions siibra/locations/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import numpy as np
from abc import abstractmethod

from typing import TYPE_CHECKING, Union, Dict
if TYPE_CHECKING:
from siibra.core.space import Space


class Location(BrainStructure):
"""
Expand All @@ -42,15 +46,19 @@ class Location(BrainStructure):
_MASK_MEMO = {} # cache region masks for Location._assign_region()
_ASSIGNMENT_CACHE = {} # caches assignment results, see Region.assign()

def __init__(self, space):
self._space_spec = space
def __init__(self, spacespec: Union[str, Dict[str, str], "Space"]):
self._space_spec = spacespec
self._space_cached = None

@property
def space(self):
if self._space_cached is None:
from ..core.space import Space
self._space_cached = Space.get_instance(self._space_spec)
if isinstance(self._space_spec, dict):
spec = self._space_spec.get("@id") or self._space_spec.get("name")
self._space_cached = Space.get_instance(spec)
else:
self._space_cached = Space.get_instance(self._space_spec)
return self._space_cached

@abstractmethod
Expand Down
2 changes: 1 addition & 1 deletion siibra/volumes/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

class VolumeProvider(ABC):

_SUBCLASSES = []
_SUBCLASSES: List[VolumeProvider] = []

def __init_subclass__(cls, srctype: str) -> None:
cls.srctype = srctype
Expand Down
5 changes: 5 additions & 0 deletions siibra/volumes/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ def __init__(
name: str = "",
variant: str = None,
datasets: List['TypeDataset'] = [],
bbox: "boundingbox.BoundingBox" = None
):
self._name = name
self._space_spec = space_spec
self.variant = variant
self._providers: Dict[str, _provider.VolumeProvider] = {}
self.datasets = datasets
self._boundingbox = bbox
for provider in providers:
srctype = provider.srctype
assert srctype not in self._providers
Expand Down Expand Up @@ -140,6 +142,9 @@ def get_boundingbox(self, clip: bool = True, background: float = 0.0, **fetch_kw
RuntimeError
If the volume provider does not have a bounding box calculator.
"""
if self._boundingbox is not None and len(fetch_kwargs) == 0:
return self._boundingbox

fmt = fetch_kwargs.get("format")
if (fmt is not None) and (fmt not in self.formats):
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion test/core/test_parcellation.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ def test_get_region(self, regionspec, find_topmost, allow_tuple, result):


@pytest.mark.parametrize('space_id,parc_id,map_type', [
('waxholm', 'v4', 'labelled')
('waxholm', 'waxholm v4', 'labelled')
])
def test_should_be_able_to_fetch_map(space_id, parc_id, map_type):

Expand Down
Loading