2121# https://www.nipreps.org/community/licensing/
2222#
2323"""Visualization tools."""
24+ from collections import defaultdict
2425import numpy as np
26+ import nibabel as nb
2527
2628from nipype .utils .filemanip import fname_presuffix
2729from nipype .interfaces .base import (
3739
3840class _FMRISummaryInputSpec (BaseInterfaceInputSpec ):
3941 in_func = File (exists = True , mandatory = True , desc = "" )
40- in_mask = File (exists = True , desc = "" )
42+ in_spikes_bg = File (exists = True , desc = "" )
43+ fd = File (exists = True , desc = "" )
44+ dvars = File (exists = True , desc = "" )
45+ outliers = File (exists = True , desc = "" )
4146 in_segm = File (exists = True , desc = "" )
42- in_spikes_bg = File (exists = True , mandatory = True , desc = "" )
43- fd = File (exists = True , mandatory = True , desc = "" )
44- fd_thres = traits .Float (0.2 , usedefault = True , desc = "" )
45- dvars = File (exists = True , mandatory = True , desc = "" )
46- outliers = File (exists = True , mandatory = True , desc = "" )
4747 tr = traits .Either (None , traits .Float , usedefault = True , desc = "the TR" )
48+ fd_thres = traits .Float (0.2 , usedefault = True , desc = "" )
49+ drop_trs = traits .Int (0 , usedefault = True , desc = "dummy scans" )
4850
4951
5052class _FMRISummaryOutputSpec (TraitedSpec ):
@@ -67,28 +69,44 @@ def _run_interface(self, runtime):
6769 newpath = runtime .cwd ,
6870 )
6971
70- dataframe = pd .DataFrame (
71- {
72- "outliers" : np .loadtxt (self .inputs .outliers , usecols = [0 ]).tolist (),
73- # Pick non-standardize dvars (col 1)
74- # First timepoint is NaN (difference)
75- "DVARS" : [np .nan ]
76- + np .loadtxt (self .inputs .dvars , skiprows = 1 , usecols = [1 ]).tolist (),
77- # First timepoint is zero (reference volume)
78- "FD" : [0.0 ]
79- + np .loadtxt (self .inputs .fd , skiprows = 1 , usecols = [0 ]).tolist (),
80- }
72+ dataframe = pd .DataFrame ({
73+ "outliers" : np .loadtxt (self .inputs .outliers , usecols = [0 ]).tolist (),
74+ # Pick non-standardize dvars (col 1)
75+ # First timepoint is NaN (difference)
76+ "DVARS" : [np .nan ]
77+ + np .loadtxt (self .inputs .dvars , skiprows = 1 , usecols = [1 ]).tolist (),
78+ # First timepoint is zero (reference volume)
79+ "FD" : [0.0 ]
80+ + np .loadtxt (self .inputs .fd , skiprows = 1 , usecols = [0 ]).tolist (),
81+ }) if (
82+ isdefined (self .inputs .outliers )
83+ and isdefined (self .inputs .dvars )
84+ and isdefined (self .inputs .fd )
85+ ) else None
86+
87+ input_data = nb .load (self .inputs .in_func )
88+ seg_file = self .inputs .in_segm if isdefined (self .inputs .in_segm ) else None
89+ dataset , segments = (
90+ _cifti_timeseries (input_data )
91+ if isinstance (input_data , nb .Cifti2Image ) else
92+ _nifti_timeseries (input_data , seg_file )
8193 )
8294
8395 fig = fMRIPlot (
84- self .inputs .in_func ,
85- mask_file = self .inputs .in_mask if isdefined (self .inputs .in_mask ) else None ,
86- seg_file = self .inputs .in_segm if isdefined (self .inputs .in_segm ) else None ,
87- spikes_files = [self .inputs .in_spikes_bg ],
88- tr = self .inputs .tr ,
89- data = dataframe [["outliers" , "DVARS" , "FD" ]],
96+ dataset ,
97+ segments = segments ,
98+ spikes_files = (
99+ [self .inputs .in_spikes_bg ]
100+ if isdefined (self .inputs .in_spikes_bg ) else None
101+ ),
102+ tr = (
103+ self .inputs .tr if isdefined (self .inputs .tr ) else
104+ _get_tr (self .inputs .in_func )
105+ ),
106+ confounds = dataframe ,
90107 units = {"outliers" : "%" , "FD" : "mm" },
91108 vlines = {"FD" : [self .inputs .fd_thres ]},
109+ nskip = self .inputs .drop_trs ,
92110 ).plot ()
93111 fig .savefig (self ._results ["out_file" ], bbox_inches = "tight" )
94112 return runtime
@@ -203,3 +221,78 @@ def _run_interface(self, runtime):
203221 reference = self .inputs .reference_column ,
204222 )
205223 return runtime
224+
225+
226+ def _cifti_timeseries (dataset ):
227+ """Extract timeseries from CIFTI2 dataset."""
228+ dataset = nb .load (dataset ) if isinstance (dataset , str ) else dataset
229+
230+ if dataset .nifti_header .get_intent ()[0 ] != "ConnDenseSeries" :
231+ raise ValueError ("Not a dense timeseries" )
232+
233+ matrix = dataset .header .matrix
234+ seg = defaultdict (list )
235+ for bm in matrix .get_index_map (1 ).brain_models :
236+ label = bm .brain_structure .replace ("CIFTI_STRUCTURE_" , "" ).replace ("_" , " " ).title ()
237+ if "CORTEX" not in bm .brain_structure and "CEREBELLUM" not in bm .brain_structure :
238+ label = "Other"
239+
240+ seg [label ] += list (range (
241+ bm .index_offset , bm .index_offset + bm .index_count
242+ ))
243+
244+ return dataset .get_fdata (dtype = "float32" ).T , seg
245+
246+
247+ def _nifti_timeseries (
248+ dataset ,
249+ segmentation = None ,
250+ lut = None ,
251+ labels = ("CSF" , "WM" , "Cerebellum" , "Cortex" )
252+ ):
253+ """Extract timeseries from NIfTI1/2 datasets."""
254+ dataset = nb .load (dataset ) if isinstance (dataset , str ) else dataset
255+ data = dataset .get_fdata (dtype = "float32" ).reshape ((- 1 , dataset .shape [- 1 ]))
256+
257+ if segmentation is None :
258+ return data , None
259+
260+ segmentation = nb .load (segmentation ) if isinstance (segmentation , str ) else segmentation
261+ # Map segmentation
262+ if lut is None :
263+ lut = np .zeros ((256 ,), dtype = "int" )
264+ lut [1 :11 ] = 4
265+ lut [255 ] = 3
266+ lut [30 :99 ] = 2
267+ lut [100 :201 ] = 1
268+ # Apply lookup table
269+ seg = lut [np .asanyarray (segmentation .dataobj , dtype = int )].reshape (- 1 )
270+ fgmask = seg > 0
271+ seg = seg [fgmask ]
272+ seg_dict = {}
273+ for i in np .unique (seg ):
274+ seg_dict [labels [i - 1 ]] = np .argwhere (seg == i ).squeeze ()
275+
276+ return data [fgmask ], seg_dict
277+
278+
279+ def _get_tr (img ):
280+ """
281+ Attempt to extract repetition time from NIfTI/CIFTI header
282+
283+ Examples
284+ --------
285+ >>> _get_tr(nb.load(Path(test_data) /
286+ ... 'sub-ds205s03_task-functionallocalizer_run-01_bold_volreg.nii.gz'))
287+ 2.2
288+ >>> _get_tr(nb.load(Path(test_data) /
289+ ... 'sub-01_task-mixedgamblestask_run-02_space-fsLR_den-91k_bold.dtseries.nii'))
290+ 2.0
291+
292+ """
293+
294+ try :
295+ return img .header .matrix .get_index_map (0 ).series_step
296+ except AttributeError :
297+ return img .header .get_zooms ()[- 1 ]
298+ raise RuntimeError ("Could not extract TR - unknown data structure type" )
0 commit comments