Source code for mriqc.workflows.anatomical

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# @Author: oesteban
# @Date:   2016-01-05 11:24:05
# @Email:  code@oscaresteban.es
# @Last modified by:   oesteban
# @Last Modified time: 2016-03-04 13:51:59
""" A QC workflow for anatomical MRI """
import os.path as op
from nipype.pipeline import engine as pe
from nipype.algorithms import misc as nam
from nipype.interfaces import io as nio
from nipype.interfaces import utility as niu
from nipype.interfaces import fsl
from nipype.interfaces import ants
from nipype.interfaces.afni import preprocess as afp

from ..interfaces.qc import StructuralQC
from ..interfaces.viz import Report, PlotMosaic
from ..utils.misc import reorder_csv


SLICE_MASK_POINTS = [(78., -110., -72.),
                     (-78., -110., -72.),
                     (-1., 91., -29.)]

[docs]def anat_qc_workflow(name='aMRIQC', settings=None, sub_list=None): """ The anatomical quality control workflow """ if settings is None: settings = {} if sub_list is None: sub_list = [] # Define workflow, inputs and outputs workflow = pe.Workflow(name=name) inputnode = pe.Node(niu.IdentityInterface(fields=['data']), name='inputnode') datasource = pe.Node(niu.IdentityInterface( fields=['anatomical_scan', 'subject_id', 'session_id', 'scan_id', 'site_name']), name='datasource') if sub_list: inputnode.iterables = [('data', [list(s) for s in sub_list])] dsplit = pe.Node(niu.Split(splits=[1, 1, 1, 1], squeeze=True), name='datasplit') workflow.connect([ (inputnode, dsplit, [('data', 'inlist')]), (dsplit, datasource, [('out1', 'subject_id'), ('out2', 'session_id'), ('out3', 'scan_id'), ('out4', 'anatomical_scan')]) ]) outputnode = pe.Node(niu.IdentityInterface( fields=['qc', 'mosaic', 'out_csv', 'out_group']), name='outputnode') measures = pe.Node(StructuralQC(), 'measures') mergqc = pe.Node(niu.Function( input_names=['in_qc', 'subject_id', 'metadata', 'fwhm'], output_names=['out_qc'], function=_merge_dicts), name='merge_qc') arw = mri_reorient_wf() # 1. Reorient anatomical image n4itk = pe.Node(ants.N4BiasFieldCorrection(dimension=3, bias_image='output_bias.nii.gz'), name='Bias') asw = skullstrip_wf() # 2. Skull-stripping (afni) qmw = brainmsk_wf() # 3. Brain mask (template & 2.) # Brain tissue segmentation segment = pe.Node(fsl.FAST( img_type=1, segments=True, out_basename='segment'), name='segmentation') # AFNI check smoothing fwhm = pe.Node(afp.FWHMx(combine=True, detrend=True), name='smoothness') # fwhm.inputs.acf = True # add when AFNI >= 16 # Plot mosaic plot = pe.Node(PlotMosaic(), name='plot_mosaic') merg = pe.Node(niu.Merge(3), name='plot_metadata') workflow.connect([ (datasource, arw, [('anatomical_scan', 'inputnode.in_file')]), (arw, n4itk, [('outputnode.out_file', 'input_image')]), (n4itk, asw, [('output_image', 'inputnode.in_file')]), (n4itk, qmw, [('output_image', 'inputnode.in_file')]), (asw, qmw, [('outputnode.out_file', 'inputnode.in_brain')]), (asw, segment, [('outputnode.out_file', 'in_files')]), (n4itk, measures, [('output_image', 'in_file')]), (n4itk, fwhm, [('output_image', 'in_file')]), (qmw, fwhm, [('outputnode.out_mask', 'mask')]), (fwhm, mergqc, [('fwhm', 'fwhm')]), (segment, measures, [('tissue_class_map', 'in_segm'), ('partial_volume_files', 'in_pvms')]), (n4itk, measures, [('bias_image', 'in_bias')]), (arw, plot, [('outputnode.out_file', 'in_file')]), (datasource, plot, [('subject_id', 'subject')]), (datasource, merg, [('session_id', 'in1'), ('scan_id', 'in2'), ('site_name', 'in3')]), (datasource, mergqc, [('subject_id', 'subject_id')]), (merg, mergqc, [('out', 'metadata')]), (merg, plot, [('out', 'metadata')]), (measures, mergqc, [('out_qc', 'in_qc')]), (mergqc, outputnode, [('out_qc', 'qc')]), (plot, outputnode, [('out_file', 'mosaic')]), ]) if settings.get('mask_mosaic', False): workflow.connect(qmw, 'outputnode.out_file', plot, 'in_mask') # Save mosaic to well-formed path mvplot = pe.Node(niu.Rename( format_string='anatomical_%(subject_id)s_%(session_id)s_%(scan_id)s', keep_ext=True), name='rename_plot') dsplot = pe.Node(nio.DataSink( base_directory=settings['work_dir'], parameterization=False), name='ds_plot') workflow.connect([ (datasource, mvplot, [('subject_id', 'subject_id'), ('session_id', 'session_id'), ('scan_id', 'scan_id')]), (plot, mvplot, [('out_file', 'in_file')]), (mvplot, dsplot, [('out_file', '@mosaic')]) ]) # Export to CSV out_csv = op.join(settings['output_dir'], 'aMRIQC.csv') to_csv = pe.Node(nam.AddCSVRow(in_file=out_csv), name='write_csv') re_csv0 = pe.JoinNode(niu.Function(input_names=['csv_file'], output_names=['out_file'], function=reorder_csv), joinsource='inputnode', joinfield='csv_file', name='reorder_anat') report0 = pe.Node( Report(qctype='anatomical', settings=settings), name='AnatomicalReport') if sub_list: report0.inputs.sub_list = sub_list workflow.connect([ (mergqc, to_csv, [('out_qc', '_outputs')]), (to_csv, re_csv0, [('csv_file', 'csv_file')]), (re_csv0, outputnode, [('out_file', 'out_csv')]), (re_csv0, report0, [('out_file', 'in_csv')]), (report0, outputnode, [('out_group', 'out_group')]) ]) return workflow
[docs]def mri_reorient_wf(name='ReorientWorkflow'): """A workflow to reorient images to 'RPI' orientation""" workflow = pe.Workflow(name=name) inputnode = pe.Node(niu.IdentityInterface(fields=['in_file']), name='inputnode') outputnode = pe.Node(niu.IdentityInterface( fields=['out_file']), name='outputnode') deoblique = pe.Node(afp.Refit(deoblique=True), name='deoblique') reorient = pe.Node(afp.Resample( orientation='RPI', outputtype='NIFTI_GZ'), name='reorient') workflow.connect([ (inputnode, deoblique, [('in_file', 'in_file')]), (deoblique, reorient, [('out_file', 'in_file')]), (reorient, outputnode, [('out_file', 'out_file')]) ]) return workflow
[docs]def brainmsk_wf(name='BrainMaskWorkflow'): """Computes a brain mask from the original T1 and the skull-stripped""" import pkg_resources as p from nipype.interfaces.fsl.maths import MathsCommand from nipype.interfaces.fsl.utils import WarpPointsFromStd def _default_template(in_file): from nipype.interfaces.fsl.base import Info from os.path import isfile from nipype.interfaces.base import isdefined if isdefined(in_file) and isfile(in_file): return in_file return Info.standard_image('MNI152_T1_2mm.nii.gz') def _post_maskav(in_file): with open(in_file, 'r') as fdesc: avg_out = fdesc.readlines() avg = int(float(avg_out[-1].split(" ")[0])) return int(avg * 3) def _post_hist(in_file): with open(in_file, 'r') as fdesc: hist_out = fdesc.readlines() bins = {} for line in hist_out: if "*" in line and not line.startswith("*"): vox_bin = line.replace(" ", "").split(":")[0] voxel_value = int(float(vox_bin.split(",")[0])) bins[int(vox_bin.split(",")[1])] = voxel_value return bins[min(bins.keys())] workflow = pe.Workflow(name=name) inputnode = pe.Node(niu.IdentityInterface( fields=['in_file', 'in_brain', 'in_template']), name='inputnode') outputnode = pe.Node(niu.IdentityInterface( fields=['out_mask', 'out_matrix_file']), name='outputnode') # Compute threshold from histogram and generate mask maskav = pe.Node(afp.Maskave(), 'mask_average') hist = pe.Node(afp.Hist(nbin=10, showhist=True), 'brain_hist') binarize = pe.Node(fsl.Threshold(args='-bin'), name='binarize') dilate = pe.Node(MathsCommand(args=' '.join(['-dilM']*6)), name='dilate') erode = pe.Node(MathsCommand(args=' '.join(['-eroF']*6)), name='erode') msk_coords = pe.Node(WarpPointsFromStd(coord_vox=True), name='msk_coords') msk_coords.inputs.in_coords = p.resource_filename('mriqc', 'data/slice_mask_points.txt') slice_msk = pe.Node(niu.Function( input_names=['in_file', 'in_coords'], output_names=['out_file'], function=slice_head_mask), name='slice_msk') combine = pe.Node(fsl.BinaryMaths( operation='add', args='-bin'), name='headmask_combine_masks') # Get linear mapping to normalized (template) space flirt = pe.Node(fsl.FLIRT(cost='corratio'), name='spatial_normalization') workflow.connect([ (inputnode, msk_coords, [(('in_template', _default_template), 'std_file')]), (inputnode, msk_coords, [('in_file', 'img_file')]), (inputnode, slice_msk, [('in_file', 'in_file')]), (inputnode, maskav, [('in_file', 'in_file')]), (maskav, hist, [(('out_file', _post_maskav), 'max_value')]), (inputnode, hist, [('in_file', 'in_file')]), (inputnode, binarize, [('in_file', 'in_file')]), (inputnode, flirt, [ ('in_brain', 'in_file'), (('in_template', _default_template), 'reference')]), (flirt, msk_coords, [('out_matrix_file', 'xfm_file')]), (msk_coords, slice_msk, [('out_file', 'in_coords')]), (hist, binarize, [(('out_show', _post_hist), 'thresh')]), (binarize, dilate, [('out_file', 'in_file')]), (dilate, erode, [('out_file', 'in_file')]), (erode, combine, [('out_file', 'in_file')]), (slice_msk, combine, [('out_file', 'operand_file')]), (combine, outputnode, [('out_file', 'out_mask')]), (flirt, outputnode, [('out_matrix_file', 'out_matrix_file')]), ]) return workflow
[docs]def skullstrip_wf(name='SkullStripWorkflow'): """ Skull-stripping workflow """ workflow = pe.Workflow(name=name) inputnode = pe.Node(niu.IdentityInterface(fields=['in_file']), name='inputnode') outputnode = pe.Node(niu.IdentityInterface(fields=['out_file']), name='outputnode') sstrip = pe.Node(afp.SkullStrip(outputtype='NIFTI_GZ'), name='skullstrip') sstrip_orig_vol = pe.Node(afp.Calc( expr='a*step(b)', outputtype='NIFTI_GZ'), name='sstrip_orig_vol') workflow.connect([ (inputnode, sstrip, [('in_file', 'in_file')]), (inputnode, sstrip_orig_vol, [('in_file', 'in_file_a')]), (sstrip, sstrip_orig_vol, [('out_file', 'in_file_b')]), (sstrip_orig_vol, outputnode, [('out_file', 'out_file')]) ]) return workflow
def _merge_dicts(in_qc, subject_id, metadata, fwhm): in_qc['subject'] = subject_id in_qc['session'] = metadata[0] in_qc['scan'] = metadata[1] try: in_qc['site_name'] = metadata[2] except IndexError: pass # No site_name defined in_qc.update({'fwhm_x': fwhm[0], 'fwhm_y': fwhm[1], 'fwhm_z': fwhm[2], 'fwhm': fwhm[3]}) in_qc['snr'] = in_qc.pop('snr_total') try: in_qc['tr'] = in_qc['spacing_tr'] except KeyError: pass # TR is not defined return in_qc
[docs]def slice_head_mask(in_file, in_coords, out_file=None): import os.path as op import numpy as np import nibabel as nb # get file info in_nii = nb.load(in_file) in_header = in_nii.get_header() in_aff = in_nii.get_affine() in_dims = in_header.get_data_shape() coords = [] for vox in np.loadtxt(in_coords): # pylint: disable=no-member vox = [int(v) for v in vox] for i in range(0, 3): vox[i] = np.clip(vox[i], 1, in_dims[i] - 1) coords.append(np.array(vox)) # get the vectors connecting the points uvector = [] for a_pt, c_pt in zip(coords[0], coords[2]): uvector.append(int(a_pt - c_pt)) vvector = [] for b_pt, c_pt in zip(coords[1], coords[2]): vvector.append(int(b_pt - c_pt)) # vector cross product nvector = np.cross(uvector, vvector) # normalize the vector nvector = nvector / np.linalg.norm(nvector, 2) constant = np.dot(nvector, np.asarray(coords[0])) # now determine the z-coordinate for each pair of x,y plane_dict = {} for yvox in range(0, in_dims[1]): for xvox in range(0, in_dims[0]): zvox = (constant - (nvector[0] * xvox + nvector[1] * yvox)) / nvector[2] zvox = np.floor(zvox) # pylint: disable=no-member if zvox < 1: zvox = 1 elif zvox > in_dims[2]: zvox = in_dims[2] plane_dict[(xvox, yvox)] = zvox # create the mask mask_array = np.zeros(in_dims) for i in range(0, in_dims[0]): for j in range(0, in_dims[1]): for k in range(0, in_dims[2]): if plane_dict[(i, j)] > k: mask_array[i, j, k] = 1 if out_file is None: fname, ext = op.splitext(op.basename(in_file)) if ext == '.gz': fname, ext2 = op.splitext(fname) ext = ext2 + ext out_file = op.abspath('%s_slice_mask%s' % (fname, ext)) nb.Nifti1Image(mask_array, in_aff, in_header).to_filename(out_file) return out_file