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

[MRG][ENH]: Add Ability to export STC files as GIFTI #12309

Merged
merged 30 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b4d4bf4
add functions to export STC as GIFTI
pmolfese Dec 18, 2023
0e4a0a8
added more attribution
pmolfese Dec 18, 2023
7c5b675
move gifti export functionality to source_estimate.py
pmolfese Dec 19, 2023
42bdda6
Merge branch 'main' into exportstc_gifti
pmolfese Dec 19, 2023
c5ab7aa
Merge branch 'main' of https://github.com/mne-tools/mne-python into e…
pmolfese Dec 19, 2023
dab0639
devel doc update
pmolfese Dec 19, 2023
a0e19ab
Merge branch 'exportstc_gifti' of https://github.com/pmolfese/mne-pyt…
pmolfese Dec 19, 2023
1be89f1
add test to write and check save_as_surface
pmolfese Dec 19, 2023
5ed43f3
Merge branch 'main' into exportstc_gifti
pmolfese Dec 19, 2023
0e8960e
Reusing existing get_decimated_surfaces from source_space, missed tha…
pmolfese Dec 19, 2023
0994c0e
Merge branch 'exportstc_gifti' of https://github.com/pmolfese/mne-pyt…
pmolfese Dec 19, 2023
1784e66
styling fixes
pmolfese Dec 19, 2023
7eaafcc
verbose breakage
pmolfese Dec 19, 2023
070f9bb
Update doc/changes/devel.rst
pmolfese Dec 20, 2023
8b8f766
Update mne/source_estimate.py
pmolfese Dec 20, 2023
8613719
Update mne/source_estimate.py
pmolfese Dec 20, 2023
f21209e
Update mne/tests/test_source_estimate.py
pmolfese Dec 20, 2023
e10f5d7
TST: Ping
larsoner Dec 20, 2023
d97ab1e
Merge remote-tracking branch 'upstream/main' into exportstc_gifti
larsoner Dec 20, 2023
1587da1
STY: pre-commit
larsoner Dec 20, 2023
f205937
STY: Caps and dots
larsoner Dec 20, 2023
bca2773
FIX: Name
larsoner Dec 20, 2023
dffd5a9
FIX: No nest
larsoner Dec 20, 2023
11d77df
MAINT: DRY code
larsoner Dec 20, 2023
e6b7b0a
Update mne/tests/test_source_estimate.py
pmolfese Dec 20, 2023
86a3429
Update doc/changes/devel/12309.newfeature.rst
pmolfese Dec 20, 2023
a0df09d
Update mne/source_estimate.py
pmolfese Dec 20, 2023
8a0b922
Update mne/source_estimate.py
pmolfese Dec 20, 2023
d78b121
Apply suggestions from code review
larsoner Dec 21, 2023
c3d0075
Update mne/source_estimate.py
larsoner Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Enhancements
- Add ``method="polyphase"`` to :meth:`mne.io.Raw.resample` and related functions to allow resampling using :func:`scipy.signal.upfirdn` (:gh:`12268` by `Eric Larson`_)
- The package build backend was switched from ``setuptools`` to ``hatchling``. This will only affect users who build and install MNE-Python from source. (:gh:`12269`, :gh:`12281` by `Richard Höchenberger`_)
- :meth:`mne.Annotations.to_data_frame` can now output different formats for the ``onset`` column: seconds, milliseconds, datetime objects, and timedelta objects. (:gh:`12289` by `Daniel McCloy`_)
- Add ``save_as_surface`` method to allow saving GIFTI files from STC SourceEstimates in :class:`~mne._BaseSurfaceSourceEstimate` (:gh:`12309` by `Peter Molfese`_)
pmolfese marked this conversation as resolved.
Show resolved Hide resolved

Bugs
~~~~
Expand Down
92 changes: 92 additions & 0 deletions mne/source_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,98 @@ def in_label(self, label):
subject=self.subject,
)
return label_stc

def save_as_surface(self, fname, src, scale=1, scale_rr=1e3):
"""Save a surface source estimate (stc) as a GIFTI file.

Parameters
----------
fname : path-like
filename basename to save files as
Will write anatomical gifti plus time series gifti for both lh/rh
ex:
"basename" --> "basename.lh.gii" and "basename.lh.time.gii"
--> "basename.rh.gii" and "basename.rh.time.gii"
pmolfese marked this conversation as resolved.
Show resolved Hide resolved
src : source solution (surface) object
the source space of the forward solution
scale : int
scale of functional values
scale_rr : float
Value to scale source solution

Notes
----------
Creates gifti files for source solution and time courses of STC
.. versionadded:: 1.7
pmolfese marked this conversation as resolved.
Show resolved Hide resolved
"""
nib = _import_nibabel()
from .source_space import get_decimated_surfaces

ss = get_decimated_surfaces(src)
stc = self

# Create lists to put DataArrays into
lh = []
rh = []

# Coerce rr to be in mm (MNE uses meters)
ss[0]['rr'] *= scale_rr
ss[1]['rr'] *= scale_rr

lh.append(nib.gifti.gifti.GiftiDataArray(
data=ss[0]['rr'],
intent='NIFTI_INTENT_POINTSET',
datatype='NIFTI_TYPE_FLOAT32'))
rh.append(nib.gifti.gifti.GiftiDataArray(
data=ss[1]['rr'],
intent='NIFTI_INTENT_POINTSET',
datatype='NIFTI_TYPE_FLOAT32'))

# Make the topology DataArray
lh.append(nib.gifti.gifti.GiftiDataArray(
data=ss[0]['tris'],
intent='NIFTI_INTENT_TRIANGLE',
datatype='NIFTI_TYPE_INT32'))
rh.append(nib.gifti.gifti.GiftiDataArray(
data=ss[1]['tris'],
intent='NIFTI_INTENT_TRIANGLE',
datatype='NIFTI_TYPE_INT32'))

# Make the output GIFTI for anatomicals
topo_gi_lh = nib.gifti.gifti.GiftiImage(darrays=lh)
topo_gi_rh = nib.gifti.gifti.GiftiImage(darrays=rh)

#actually save the files
nib.save(topo_gi_lh, f"{fname}-lh.gii")
nib.save(topo_gi_rh, f"{fname}-rh.gii")

# Make the Time Series data arrays
lh_ts = []
rh_ts = []

for t in range(stc.shape[1]):
lh_ts.append(
nib.gifti.gifti.GiftiDataArray(
data=stc.lh_data[:, t] * scale,
intent='NIFTI_INTENT_POINTSET',
datatype='NIFTI_TYPE_FLOAT32'
)
)

rh_ts.append(
nib.gifti.gifti.GiftiDataArray(
data=stc.rh_data[:, t] * scale,
intent='NIFTI_INTENT_POINTSET',
datatype='NIFTI_TYPE_FLOAT32'
)
)

#save the time series
ts_gi_lh = nib.gifti.gifti.GiftiImage(darrays=lh_ts)
ts_gi_rh = nib.gifti.gifti.GiftiImage(darrays=rh_ts)
nib.save(ts_gi_lh, f"{fname}-lh.time.gii")
nib.save(ts_gi_rh, f"{fname}-rh.time.gii")


def expand(self, vertices):
"""Expand SourceEstimate to include more vertices.
Expand Down
28 changes: 28 additions & 0 deletions mne/tests/test_source_estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,34 @@ def test_volume_stc(tmp_path):
assert_array_equal(stc.vertices[0], stc_new.vertices[0])
assert_array_almost_equal(stc.data, stc_new.data)

@testing.requires_testing_data
pmolfese marked this conversation as resolved.
Show resolved Hide resolved
def test_save_stc_as_gifti(tmp_path):
"""Save the stc as a gifti file and export."""
pmolfese marked this conversation as resolved.
Show resolved Hide resolved
nib = pytest.importorskip("nibabel")
surfpath_src = bem_path / "sample-oct-6-src.fif"
surfpath_stc = data_path / "MEG" / "sample" / "sample_audvis_trunc-meg"
src = read_source_spaces(surfpath_src) #need source space
stc = read_source_estimate(surfpath_stc) #need stc
assert isinstance(src, SourceSpaces)
assert isinstance(stc, SourceEstimate)

surf_fname = tmp_path / "stc_write"

stc.save_as_surface(surf_fname, src)

#did structural get written?
img_lh = nib.load(f'{surf_fname}-lh.gii')
img_rh = nib.load(f'{surf_fname}-rh.gii')
assert isinstance(img_lh, nib.gifti.gifti.GiftiImage)
assert isinstance(img_rh, nib.gifti.gifti.GiftiImage)

#did time series get written?
img_timelh = nib.load(f'{surf_fname}-lh.time.gii')
img_timerh = nib.load(f'{surf_fname}-rh.time.gii')
assert isinstance(img_timelh, nib.gifti.gifti.GiftiImage)
assert isinstance(img_timerh, nib.gifti.gifti.GiftiImage)



@testing.requires_testing_data
def test_stc_as_volume():
Expand Down
Loading