Skip to content

Commit 519bc7f

Browse files
committed
TimeSeriesVolume and assignment
1 parent c7310d2 commit 519bc7f

4 files changed

Lines changed: 114 additions & 13 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import numpy as np
2+
import nibabel
3+
import siibra
4+
5+
6+
def create_synthetic_data():
7+
julich_pmaps = siibra.get_map(
8+
parcellation="julich 2.9",
9+
space="mni152",
10+
maptype="statistical"
11+
)
12+
length = 20
13+
template_img = julich_pmaps.space.get_template().fetch()
14+
arr = np.zeros(list(template_img.shape) + [length])
15+
for i, img in enumerate(julich_pmaps.fetch_iter()):
16+
if i == length:
17+
break
18+
arr[:, :, :, i] += img.dataobj
19+
return siibra.volumes.from_nifti(
20+
nibabel.nifti1.Nifti1Image(arr, affine=template_img.affine),
21+
time_index=np.asanyarray(range(length)),
22+
space='mni152',
23+
name="synthetic timeseries volume"
24+
)
25+
26+
27+
def test_timeseries_volume_assignment():
28+
difumo128 = siibra.get_map(
29+
parcellation="difumo 128",
30+
space="mni152",
31+
maptype="statistical"
32+
)
33+
synthetic_vol = create_synthetic_data()
34+
assignments = difumo128.assign(synthetic_vol, split_components=False)
35+
print(assignments)

siibra/volumes/parcellationmap.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class MapAssignment:
5656
volume: int
5757
fragment: str
5858
map_value: np.ndarray
59+
time_index: Union[int, None]
5960

6061

6162
@dataclass
@@ -893,12 +894,20 @@ def _assign(
893894
if isinstance(item, pointcloud.PointCloud):
894895
return self._assign_points(item, lower_threshold)
895896
if isinstance(item, _volume.Volume):
896-
return self._assign_volume(
897-
queryvolume=item,
898-
lower_threshold=lower_threshold,
899-
minsize_voxel=minsize_voxel,
900-
**kwargs
901-
)
897+
if isinstance(item, _volume.TimeSeriesVolume):
898+
return self._assign_timeseries_volume(
899+
queryvolume=item,
900+
lower_threshold=lower_threshold,
901+
minsize_voxel=minsize_voxel,
902+
**kwargs
903+
)
904+
else:
905+
return self._assign_volume(
906+
queryvolume=item,
907+
lower_threshold=lower_threshold,
908+
minsize_voxel=minsize_voxel,
909+
**kwargs
910+
)
902911

903912
raise RuntimeError(
904913
f"Items of type {item.__class__.__name__} cannot be used for region assignment."
@@ -957,6 +966,7 @@ def assign(
957966
# format assignments as pandas dataframe
958967
columns = [
959968
"input structure",
969+
"time_index",
960970
"centroid",
961971
"volume",
962972
"fragment",
@@ -970,7 +980,7 @@ def assign(
970980
"input containedness"
971981
]
972982
if len(assignments) == 0:
973-
return pd.DataFrame(columns=columns)
983+
return pd.DataFrame(columns=columns).dropna(axis='columns', how='all')
974984
# determine the unique set of observed indices in order to do region lookups
975985
# only once for each map index occurring in the point list
976986
labelled = self.is_labelled # avoid calling this in a loop
@@ -1147,7 +1157,8 @@ def _assign_volume(
11471157
queryvolume: "_volume.Volume",
11481158
lower_threshold: float,
11491159
split_components: bool = True,
1150-
**kwargs
1160+
time_index: int = None,
1161+
**kwargs,
11511162
) -> List[AssignImageResult]:
11521163
"""
11531164
Assign an image volume to this parcellation map.
@@ -1214,12 +1225,32 @@ def _assign_volume(
12141225
volume=index.volume,
12151226
fragment=index.fragment,
12161227
map_value=index.label,
1228+
time_index=time_index,
12171229
**asdict(scores)
12181230
)
12191231
)
12201232

12211233
return assignments
12221234

1235+
def _assign_timeseries_volume(
1236+
self,
1237+
queryvolume: "_volume.TimeSeriesVolume",
1238+
lower_threshold: float,
1239+
split_components: bool = True,
1240+
**kwargs
1241+
) -> List[AssignImageResult]:
1242+
assignments = []
1243+
for v_t in siibra_tqdm(queryvolume, unit='time_index'):
1244+
assignments_t = self._assign_volume(
1245+
v_t,
1246+
lower_threshold=lower_threshold,
1247+
split_components=split_components,
1248+
time_index=v_t.time_index,
1249+
**kwargs
1250+
)
1251+
assignments.extend(assignments_t)
1252+
return assignments
1253+
12231254

12241255
def from_volume(
12251256
name: str,

siibra/volumes/sparsemap.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ def _assign_volume(
335335
queryvolume: "_volume.Volume",
336336
minsize_voxel: int,
337337
lower_threshold: float,
338-
split_components: bool = True
338+
split_components: bool = True,
339+
time_index: int = None,
339340
) -> List[parcellationmap.AssignImageResult]:
340341
"""
341342
Assign an image volume to this sparse map.
@@ -444,6 +445,7 @@ def _assign_volume(
444445

445446
assignments.append(
446447
parcellationmap.AssignImageResult(
448+
time_index=time_index,
447449
input_structure=mode,
448450
centroid=tuple(position.round(2)),
449451
volume=volume,

siibra/volumes/volume.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
"""A specific mesh or 3D array."""
1616

17-
from typing import List, Dict, Union, Set, TYPE_CHECKING
17+
from typing import Iterable, List, Dict, Union, Set, TYPE_CHECKING
1818
from dataclasses import dataclass
1919
from time import sleep
2020
import json
@@ -633,6 +633,7 @@ def __init__(
633633
label: int = None,
634634
fragment: str = None,
635635
threshold: float = None,
636+
time_index: int = None,
636637
):
637638
"""
638639
A prescribed Volume to fetch specified label and fragment.
@@ -647,6 +648,8 @@ def __init__(
647648
If a volume is fragmented, get a specified one.
648649
threshold : float, default None
649650
Provide a float value to threshold the image.
651+
time_index: int = None,
652+
If parent volume is a timeseries Nifti, filter a time index without fetching the full image.
650653
"""
651654
name = parent_volume.name
652655
if label:
@@ -655,6 +658,8 @@ def __init__(
655658
name += f" - fragment: {fragment}"
656659
if threshold:
657660
name += f" - threshold: {threshold}"
661+
if time_index:
662+
name += f" - time index: {time_index}"
658663
Volume.__init__(
659664
self,
660665
space_spec=parent_volume._space_spec,
@@ -664,6 +669,7 @@ def __init__(
664669
self.fragment = fragment
665670
self.label = label
666671
self.threshold = threshold
672+
self.time_index = time_index
667673

668674
def fetch(
669675
self,
@@ -680,7 +686,8 @@ def fetch(
680686
kwargs["label"] = self.label
681687

682688
result = super().fetch(format=format, **kwargs)
683-
689+
if self.time_index is not None:
690+
result = result.slicer[:, :, :, self.time_index]
684691
if self.threshold is not None:
685692
assert self.label is None
686693
if not isinstance(result, Nifti1Image):
@@ -710,6 +717,29 @@ def get_boundingbox(
710717
)
711718

712719

720+
class TimeSeriesVolume(Volume):
721+
def __init__(
722+
self,
723+
time_index: np.ndarray,
724+
**kwargs,
725+
):
726+
Volume.__init__(self, **kwargs)
727+
self.time_index = time_index
728+
729+
def __iter__(self) -> Iterable[FilteredVolume]:
730+
yield from (
731+
FilteredVolume(parent_volume=self, time_index=t)
732+
for t in self.time_index
733+
)
734+
735+
def get_index(self, time_index: int):
736+
return FilteredVolume(parent_volume=self, time_index=time_index)
737+
738+
def fetch(self, format: str = None, time_index: int = None, **kwargs):
739+
img = super().fetch(format, **kwargs)
740+
return img.slicer[:, :, :, time_index] if time_index else img
741+
742+
713743
class ReducedVolume(Volume):
714744
def __init__(
715745
self,
@@ -832,14 +862,17 @@ def from_file(filename: str, space: str, name: str) -> Volume:
832862
)
833863

834864

835-
def from_nifti(nifti: Nifti1Image, space: str, name: str) -> Volume:
865+
def from_nifti(nifti: Nifti1Image, space: str, name: str, time_index: np.ndarray = None) -> Union[Volume, TimeSeriesVolume]:
836866
"""Builds a nifti volume from a Nifti image."""
837867
spaceobj = get_registry("Space").get(space)
838-
return Volume(
868+
kwargs = dict(
839869
space_spec={"@id": spaceobj.id},
840870
providers=[_providers.NiftiProvider((np.asanyarray(nifti.dataobj), nifti.affine))],
841871
name=name
842872
)
873+
if time_index is None:
874+
return Volume(**kwargs)
875+
return TimeSeriesVolume(time_index=time_index, **kwargs)
843876

844877

845878
def from_array(

0 commit comments

Comments
 (0)