diff --git a/README.md b/README.md index 398f6f2..be8a353 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Defacing tool for nifti images. +Defacing tool for nifti images. ### Requirements: - [FSL](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FSL) @@ -14,5 +14,10 @@ python setup.py install ### To use: ``` -pydeface.py infile.nii.gz +pydeface infile.nii.gz +``` + +Also see the help for additional options: +``` +pydeface --help ``` diff --git a/pydeface/__init__.py b/pydeface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pydeface/__main__.py b/pydeface/__main__.py new file mode 100644 index 0000000..4c55a55 --- /dev/null +++ b/pydeface/__main__.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +"""Defacing utility for MRI images.""" + +# Copyright 2011, Russell Poldrack. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY RUSSELL POLDRACK ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL RUSSELL POLDRACK OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +import os +import tempfile +from nipype.interfaces import fsl +from nibabel import load, Nifti1Image +from pkg_resources import require +from pydeface.utils import initial_checks, output_checks + + +def main(): + """Command line call argument parsing.""" + parser = argparse.ArgumentParser() + parser.add_argument( + 'infile', metavar='path', + help="Path to input nifti.") + + parser.add_argument( + "--outfile", metavar='path', required=False, + help="If not provided adds '_defaced' suffix.") + + parser.add_argument( + "--force", action='store_true', + help="Force to rewrite the output even if it exists.") + + parser.add_argument( + '--applyto', nargs='+', required=False, metavar='', + help="Apply the created face mask to other images. Can take multiple " + "arguments.") + + parser.add_argument( + "--cost", metavar='mutualinfo', required=False, default='mutualinfo', + help="FSL-FLIRT cost function. Default is 'mutualinfo'.") + + parser.add_argument( + "--template", metavar='path', required=False, + help=("Optional template image that will be used as the registration " + "target instead of the default.")) + + parser.add_argument( + "--facemask", metavar='path', required=False, + help="Optional face mask image that will be used instead of the " + "default.") + + parser.add_argument( + "--nocleanup", action='store_true', + help="Do not cleanup temporary files. Off by default.") + + parser.add_argument( + "--verbose", action='store_true', + help="Show additional status prints. Off by default.") + + welcome_str = 'pydeface ' + require("pydeface")[0].version + welcome_decor = '-' * len(welcome_str) + print(welcome_decor + '\n' + welcome_str + '\n' + welcome_decor) + + args = parser.parse_args() + template, facemask = initial_checks(args.template, args.facemask) + infile = args.infile + outfile = output_checks(infile, args.outfile, args.force) + + # temporary files + _, tmpmat = tempfile.mkstemp() + tmpmat = tmpmat + '.mat' + _, tmpfile = tempfile.mkstemp() + tmpfile = tmpfile + '.nii.gz' + if args.verbose: + print("Temporary files:\n %s\n %s" % (tmpmat, tmpfile)) + _, tmpfile2 = tempfile.mkstemp() + _, tmpmat2 = tempfile.mkstemp() + + print('Defacing...\n %s' % args.infile) + + # register template to infile + flirt = fsl.FLIRT() + flirt.inputs.cost_func = args.cost + flirt.inputs.in_file = template + flirt.inputs.out_matrix_file = tmpmat + flirt.inputs.out_file = tmpfile2 + flirt.inputs.reference = infile + flirt.run() + + # warp facemask to infile + flirt = fsl.FLIRT() + flirt.inputs.in_file = facemask + flirt.inputs.in_matrix_file = tmpmat + flirt.inputs.apply_xfm = True + flirt.inputs.reference = infile + flirt.inputs.out_file = tmpfile + flirt.inputs.out_matrix_file = tmpmat2 + flirt.run() + + # multiply mask by infile and save + infile_img = load(infile) + tmpfile_img = load(tmpfile) + outdata = infile_img.get_data() * tmpfile_img.get_data() + outfile_img = Nifti1Image(outdata, infile_img.get_affine(), + infile_img.get_header()) + outfile_img.to_filename(outfile) + print("Defaced image saved as:\n %s" % outfile) + + # apply mask to other given images + if args.applyto is not None: + print("Defacing mask also applied to:") + for applyfile in args.applyto: + applyfile_img = load(applyfile) + outdata = applyfile_img.get_data() * tmpfile_img.get_data() + applyfile_img = Nifti1Image(outdata, applyfile_img.get_affine(), + applyfile_img.get_header()) + outfile = output_checks(applyfile) + applyfile_img.to_filename(outfile) + print(' %s' % applyfile) + + if args.nocleanup: + pass + else: + os.remove(tmpfile) + os.remove(tmpfile2) + os.remove(tmpmat) + + print('Finished.') + + +if __name__ == "__main__": + main() diff --git a/pydeface/utils.py b/pydeface/utils.py index bd14c7f..c9c31c4 100644 --- a/pydeface/utils.py +++ b/pydeface/utils.py @@ -1,22 +1,43 @@ """Utility scripts for pydeface.""" +import os import sys -import subprocess +from pkg_resources import resource_filename, Requirement -def run_shell_cmd(cmd, cwd=[]): - """Run a command in the shell using Popen.""" - if cwd: - process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, - cwd=cwd) - else: - process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) - for line in process.stdout: - print(line.strip()) - process.wait() +def initial_checks(template=None, facemask=None): + """Initial sanity checks.""" + if template is None: + template = resource_filename(Requirement.parse("pydeface"), + "pydeface/data/mean_reg2mean.nii.gz") + if facemask is None: + facemask = resource_filename(Requirement.parse("pydeface"), + "pydeface/data/facemask.nii.gz") + + if not os.path.exists(template): + raise Exception('Missing template: %s' % template) + if not os.path.exists(facemask): + raise Exception('Missing face mask: %s' % facemask) + + if 'FSLDIR' not in os.environ: + raise Exception("FSL must be installed and " + "FSLDIR environment variable must be defined.") + sys.exit(2) + return template, facemask -def usage(): - """Print the docstring and exit.""" - sys.stdout.write(__doc__) - sys.exit(2) +def output_checks(infile, outfile=None, force=False): + """Determine output file name.""" + if force is None: + force = False + if outfile is None: + outfile = infile.replace('.nii', '_defaced.nii') + + if os.path.exists(outfile) and force: + print('Previous output will be overwritten.') + elif os.path.exists(outfile): + raise Exception("%s already exists. Remove it first or use '--force' " + "flag to overwrite." % outfile) + else: + pass + return outfile diff --git a/scripts/pydeface.py b/scripts/pydeface.py deleted file mode 100755 index 8d3d1c9..0000000 --- a/scripts/pydeface.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python -"""Deface an image using FSL. - -Usage: ------- -pydeface.py - -""" - -# Copyright 2011, Russell Poldrack. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY RUSSELL POLDRACK ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -# EVENT SHALL RUSSELL POLDRACK OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import nibabel -import os -import sys -import tempfile -from nipype.interfaces import fsl -from pkg_resources import resource_filename, Requirement, require - - -def main(): - cleanup, verbose = True, False - - template = resource_filename(Requirement.parse("pydeface"), - "pydeface/data/mean_reg2mean.nii.gz") - facemask = resource_filename(Requirement.parse("pydeface"), - "pydeface/data/facemask.nii.gz") - - if not os.path.exists(template): - raise Exception('Missing template: %s' % template) - if not os.path.exists(facemask): - raise Exception('Missing face mask: %s' % facemask) - - if len(sys.argv) < 2: - sys.stdout.write(__doc__) - sys.exit(2) - else: - infile = sys.argv[1] - - if len(sys.argv) > 2: - outfile = sys.argv[2] - else: - outfile = infile.replace('.nii', '_defaced.nii') - - if os.path.exists(outfile): - raise Exception('%s already exists, remove it first.' % outfile) - - if 'FSLDIR' not in os.environ: - raise Exception("FSL must be installed and " - "FSLDIR environment variable must be defined.") - sys.exit(2) - - _, tmpmat = tempfile.mkstemp() - tmpmat = tmpmat + '.mat' - _, tmpfile = tempfile.mkstemp() - tmpfile = tmpfile + '.nii.gz' - if verbose: - print(tmpmat) - print(tmpfile) - _, tmpfile2 = tempfile.mkstemp() - _, tmpmat2 = tempfile.mkstemp() - - print('Defacing...\n%s' % infile) - - # register template to infile - flirt = fsl.FLIRT() - flirt.inputs.cost_func = 'mutualinfo' - flirt.inputs.in_file = template - flirt.inputs.out_matrix_file = tmpmat - flirt.inputs.out_file = tmpfile2 - flirt.inputs.reference = infile - flirt.run() - - # warp facemask to infile - flirt = fsl.FLIRT() - flirt.inputs.in_file = facemask - flirt.inputs.in_matrix_file = tmpmat - flirt.inputs.apply_xfm = True - flirt.inputs.reference = infile - flirt.inputs.out_file = tmpfile - flirt.inputs.out_matrix_file = tmpmat2 - flirt.run() - - # multiply mask by infile and save - infile_img = nibabel.load(infile) - tmpfile_img = nibabel.load(tmpfile) - outdata = infile_img.get_data() * tmpfile_img.get_data() - outfile_img = nibabel.Nifti1Image(outdata, infile_img.get_affine(), - infile_img.get_header()) - outfile_img.to_filename(outfile) - - if cleanup: - os.remove(tmpfile) - os.remove(tmpfile2) - os.remove(tmpmat) - - print('Output saved as:\n%s' % outfile) - - -if __name__ == "__main__": - welcome_str = 'pydeface ' + require("pydeface")[0].version - welcome_decor = '-' * len(welcome_str) - print(welcome_decor + '\n' + welcome_str + '\n' + welcome_decor) - main() diff --git a/setup.py b/setup.py index f5aeabd..b94816e 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ # # Some portions were borrowed from: # https://github.com/mwaskom/lyman/blob/master/setup.py +# and: +# https://chriswarrick.com/blog/2014/09/15/python-apps-the-right-way-entry_points-and-scripts/ import os from setuptools import setup @@ -15,7 +17,7 @@ LICENSE = 'MIT' URL = 'http://poldracklab.org' DOWNLOAD_URL = 'https://github.com/poldracklab/pydeface/' -VERSION = '1.1' +VERSION = '2.0' if os.path.exists('MANIFEST'): os.remove('MANIFEST') @@ -33,7 +35,6 @@ download_url=DOWNLOAD_URL, packages=['pydeface'], package_data=datafiles, - scripts=['scripts/pydeface.py'], classifiers=['Intended Audience :: Science/Research', 'Programming Language :: Python :: 2.7', 'License :: OSI Approved :: BSD License', @@ -41,4 +42,8 @@ 'Operating System :: Unix', 'Operating System :: MacOS'], install_requires=['numpy', 'nibabel', 'nipype'], + entry_points={ + 'console_scripts': [ + 'pydeface = pydeface.__main__:main' + ]}, )