Skip to content

Commit 1baf9c9

Browse files
authored
Merge pull request #52 from yhuang43/master
support gifti time-series
2 parents 00c769a + 1b12ae6 commit 1baf9c9

File tree

7 files changed

+146
-1
lines changed

7 files changed

+146
-1
lines changed

surfa/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .mesh import Mesh
2424
from .mesh import Overlay
2525
from .mesh import sphere
26+
from .mesh import TimeSeries
2627

2728
from .io import load_volume
2829
from .io import load_slice
@@ -31,6 +32,7 @@
3132
from .io import load_label_lookup
3233
from .io import load_mesh
3334
from .io import load_warp
35+
from .io import load_timeseries
3436

3537
from . import vis
3638
from . import freesurfer

surfa/core/framed.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class FramedArrayIntents:
1515
shape = 2
1616
warpmap = 3
1717
warpmap_inv = 4
18+
timeseries = 5
1819

1920

2021
class FramedArray:

surfa/io/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
from .framed import load_warp
77
from .labels import load_label_lookup
88
from .mesh import load_mesh
9+
from .timeseries import load_timeseries

surfa/io/framed.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from surfa import Slice
88
from surfa import Overlay
99
from surfa import Warp
10+
from surfa import TimeSeries
1011
from surfa.core.array import pad_vector_length
1112
from surfa.core.framed import FramedArray
1213
from surfa.core.framed import FramedArrayIntents
@@ -185,7 +186,7 @@ def framed_array_from_4d(atype, data):
185186
Squeezed framed array.
186187
"""
187188
# this code is a bit ugly - it does the job but should probably be cleaned up
188-
if atype == Volume:
189+
if atype == Volume or atype == TimeSeries:
189190
return atype(data)
190191
if atype == Warp:
191192
if data.ndim == 4 and data.shape[-1] == 2:

surfa/io/timeseries.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import os
2+
import numpy as np
3+
4+
from surfa.io import protocol
5+
from surfa.io.utils import check_file_readability
6+
from surfa.io.framed import framed_array_from_4d
7+
8+
from surfa import TimeSeries
9+
10+
def load_timeseries(filename, fmt=None):
11+
"""
12+
Generic loader for `FramedArray` objects.
13+
14+
Parameters
15+
----------
16+
filename : str or Path
17+
File path to read.
18+
fmt : str, optional
19+
Explicit file format. If None, we extrapolate from the file extension.
20+
21+
Returns
22+
-------
23+
TimeSeries
24+
A TimeSeries object loaded file.
25+
"""
26+
check_file_readability(filename)
27+
28+
if fmt is None:
29+
iop = find_timeseries_protocol_by_extension(filename)
30+
else:
31+
iop = protocol.find_protocol_by_name(timeseries_io_protocols, fmt)
32+
if iop is None:
33+
raise ValueError(f'unknown file format {fmt}')
34+
35+
return iop().load(filename)
36+
37+
38+
def find_timeseries_protocol_by_extension(filename):
39+
"""
40+
Find timeseries IO protocol from file extension.
41+
42+
Parameters
43+
----------
44+
filename : str
45+
File path to read.
46+
47+
Returns
48+
-------
49+
protocol : IOProtocol
50+
Matched timeseries IO protocol class.
51+
"""
52+
53+
# find matching protocol
54+
iop = protocol.find_protocol_by_extension(timeseries_io_protocols, filename)
55+
if iop is None:
56+
basename = os.path.basename(filename).lower()
57+
raise ValueError(f'timeseries file type {basename} is not supported.')
58+
59+
return iop
60+
61+
62+
class GiftiIO(protocol.IOProtocol):
63+
"""
64+
GIFTI IO protocol for time-series files.
65+
"""
66+
name = 'time-series'
67+
extensions = ('.gii')
68+
69+
def __init__(self):
70+
try:
71+
import nibabel as nib
72+
except ImportError:
73+
raise ImportError('the `nibabel` python package must be installed for gifti IO')
74+
self.nib = nib
75+
76+
def load(self, filename):
77+
"""
78+
Read time-series from a gifti file.
79+
80+
Parameters
81+
----------
82+
filename : str or Path
83+
File path read.
84+
85+
Returns
86+
-------
87+
TimeSeries
88+
TimeSeries object loaded from file.
89+
"""
90+
91+
giftiImage = self.nib.load(filename)
92+
data = np.expand_dims(giftiImage.agg_data(), axis=(1,2))
93+
94+
arr = framed_array_from_4d(TimeSeries, data)
95+
96+
return arr
97+
98+
99+
def save(self, arr, filename):
100+
"""
101+
Write time-series to a gifti file.
102+
103+
Parameters
104+
----------
105+
arr : array
106+
Array to save as gifti time-series
107+
filename : str or Path
108+
Target file path.
109+
"""
110+
111+
raise ValueError(f'ERROR: surfa.io.timeseries.GiftiIO.save() not implemented!')
112+
113+
114+
# enabled TimeSeries IO protocol classes
115+
timeseries_io_protocols = [
116+
GiftiIO, # '.gii'
117+
]

surfa/mesh/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .overlay import Overlay
33
from .overlay import cast_overlay
44
from .distance import surface_distance
5+
from .timeseries import TimeSeries

surfa/mesh/timeseries.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import surfa as sf
2+
from surfa.image.framed import FramedImage
3+
4+
class TimeSeries(FramedImage):
5+
6+
def __init__(self, data):
7+
"""
8+
Time-Series class defining an array with data frames.
9+
10+
Parameters
11+
----------
12+
data : array_like
13+
Image data array.
14+
"""
15+
16+
basedim = data.ndim - 1
17+
super().__init__(basedim=basedim, data=data)
18+
19+
20+
def save(self, filename, fmt=None):
21+
super().save(filename, fmt=fmt, intent=sf.core.framed.FramedArrayIntents.timeseries)
22+

0 commit comments

Comments
 (0)