From 328d3bb9d3bb3c1768c3ce81d5a3496d8db0e9d3 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 3 Nov 2017 11:56:44 -0400 Subject: [PATCH 001/142] RF: moved all the functionality from nib-ls under nibabel.cmdline --- bin/nib-ls | 271 +----------------------------------- nibabel/cmdline/__init__.py | 12 ++ nibabel/cmdline/ls.py | 170 ++++++++++++++++++++++ nibabel/cmdline/utils.py | 127 +++++++++++++++++ 4 files changed, 310 insertions(+), 270 deletions(-) create mode 100644 nibabel/cmdline/__init__.py create mode 100755 nibabel/cmdline/ls.py create mode 100644 nibabel/cmdline/utils.py diff --git a/bin/nib-ls b/bin/nib-ls index fe8007d917..067efb0533 100755 --- a/bin/nib-ls +++ b/bin/nib-ls @@ -10,277 +10,8 @@ """ Output a summary table for neuroimaging files (resolution, dimensionality, etc.) """ -from __future__ import division, print_function, absolute_import - -import re -import sys - -import numpy as np -import nibabel as nib - -from math import ceil -from optparse import OptionParser, Option -from io import StringIO -from nibabel.py3k import asunicode - -__author__ = 'Yaroslav Halchenko' -__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ - 'and NiBabel contributors' -__license__ = 'MIT' - - -# global verbosity switch -verbose_level = 0 -MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts - -def _err(msg=None): - """To return a string to signal "error" in output table""" - if msg is None: - msg = 'error' - return '!' + msg - -def verbose(l, msg): - """Print `s` if `l` is less than the `verbose_level` - """ - # TODO: consider using nibabel's logger - if l <= int(verbose_level): - print("%s%s" % (' ' * l, msg)) - - -def error(msg, exit_code): - print >> sys.stderr, msg - sys.exit(exit_code) - - -def table2string(table, out=None): - """Given list of lists figure out their common widths and print to out - - Parameters - ---------- - table : list of lists of strings - What is aimed to be printed - out : None or stream - Where to print. If None -- will print and return string - - Returns - ------- - string if out was None - """ - - print2string = out is None - if print2string: - out = StringIO() - - # equalize number of elements in each row - nelements_max = \ - len(table) and \ - max(len(x) for x in table) - - for i, table_ in enumerate(table): - table[i] += [''] * (nelements_max - len(table_)) - - # figure out lengths within each column - atable = np.asarray(table) - # eat whole entry while computing width for @w (for wide) - markup_strip = re.compile('^@([lrc]|w.*)') - col_width = [max([len(markup_strip.sub('', x)) - for x in column]) for column in atable.T] - string = "" - for i, table_ in enumerate(table): - string_ = "" - for j, item in enumerate(table_): - item = str(item) - if item.startswith('@'): - align = item[1] - item = item[2:] - if align not in ['l', 'r', 'c', 'w']: - raise ValueError('Unknown alignment %s. Known are l,r,c' % - align) - else: - align = 'c' - - nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) - nspacesr = max(col_width[j] - nspacesl - len(item), 0) - - if align in ['w', 'c']: - pass - elif align == 'l': - nspacesl, nspacesr = 0, nspacesl + nspacesr - elif align == 'r': - nspacesl, nspacesr = nspacesl + nspacesr, 0 - else: - raise RuntimeError('Should not get here with align=%s' % align) - - string_ += "%%%ds%%s%%%ds " \ - % (nspacesl, nspacesr) % ('', item, '') - string += string_.rstrip() + '\n' - out.write(asunicode(string)) - - if print2string: - value = out.getvalue() - out.close() - return value - - -def ap(l, format_, sep=', '): - """Little helper to enforce consistency""" - if l == '-': - return l - ls = [format_ % x for x in l] - return sep.join(ls) - - -def safe_get(obj, name): - """A getattr which would return '-' if getattr fails - """ - try: - f = getattr(obj, 'get_' + name) - return f() - except Exception as e: - verbose(2, "get_%s() failed -- %s" % (name, e)) - return '-' - - -def get_opt_parser(): - # use module docstring for help output - p = OptionParser( - usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, - version="%prog " + nib.__version__) - - p.add_options([ - Option("-v", "--verbose", action="count", - dest="verbose", default=0, - help="Make more noise. Could be specified multiple times"), - - Option("-H", "--header-fields", - dest="header_fields", default='', - help="Header fields (comma separated) to be printed as well (if present)"), - - Option("-s", "--stats", - action="store_true", dest='stats', default=False, - help="Output basic data statistics"), - - Option("-c", "--counts", - action="store_true", dest='counts', default=False, - help="Output counts - number of entries for each numeric value " - "(useful for int ROI maps)"), - - Option("--all-counts", - action="store_true", dest='all_counts', default=False, - help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), - - Option("-z", "--zeros", - action="store_true", dest='stats_zeros', default=False, - help="Include zeros into output basic data statistics (--stats, --counts)"), - ]) - - return p - - -def proc_file(f, opts): - verbose(1, "Loading %s" % f) - - row = ["@l%s" % f] - try: - vol = nib.load(f) - h = vol.header - except Exception as e: - row += ['failed'] - verbose(2, "Failed to gather information -- %s" % str(e)) - return row - - row += [str(safe_get(h, 'data_dtype')), - '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), - '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] - # Slope - if hasattr(h, 'has_data_slope') and \ - (h.has_data_slope or h.has_data_intercept) and \ - not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: - row += ['@l*%.3g+%.3g' % h.get_slope_inter()] - else: - row += [''] - - if hasattr(h, 'extensions') and len(h.extensions): - row += ['@l#exts: %d' % len(h.extensions)] - else: - row += [''] - - if opts.header_fields: - # signals "all fields" - if opts.header_fields == 'all': - # TODO: might vary across file types, thus prior sensing - # would be needed - header_fields = h.keys() - else: - header_fields = opts.header_fields.split(',') - - for f in header_fields: - if not f: # skip empty - continue - try: - row += [str(h[f])] - except (KeyError, ValueError): - row += [_err()] - - try: - if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and - (h.get_qform() != h.get_sform()).any()): - row += ['sform'] - else: - row += [''] - except Exception as e: - verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) - if isinstance(h, nib.AnalyzeHeader): - row += [''] - else: - row += [_err()] - - if opts.stats or opts.counts: - # We are doomed to load data - try: - d = vol.get_data() - if not opts.stats_zeros: - d = d[np.nonzero(d)] - else: - # at least flatten it -- functionality below doesn't - # depend on the original shape, so let's use a flat view - d = d.reshape(-1) - if opts.stats: - # just # of elements - row += ["@l[%d]" % np.prod(d.shape)] - # stats - row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] - if opts.counts: - items, inv = np.unique(d, return_inverse=True) - if len(items) > 1000 and not opts.all_counts: - counts = _err("%d uniques. Use --all-counts" % len(items)) - else: - freq = np.bincount(inv) - counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) - row += ["@l" + counts] - except IOError as e: - verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) - row += [_err()] - return row - - -def main(): - """Show must go on""" - - parser = get_opt_parser() - (opts, files) = parser.parse_args() - - global verbose_level - verbose_level = opts.verbose - - if verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - - rows = [proc_file(f, opts) for f in files] - - print(table2string(rows)) +from nibabel.cmdline.ls import main if __name__ == '__main__': main() diff --git a/nibabel/cmdline/__init__.py b/nibabel/cmdline/__init__.py new file mode 100644 index 0000000000..a5f3e38228 --- /dev/null +++ b/nibabel/cmdline/__init__.py @@ -0,0 +1,12 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +"""Functionality to be exposed in the command line +""" + + diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py new file mode 100755 index 0000000000..b302b67e5b --- /dev/null +++ b/nibabel/cmdline/ls.py @@ -0,0 +1,170 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get + +__author__ = 'Yaroslav Halchenko' +__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ + 'and NiBabel contributors' +__license__ = 'MIT' + + +MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + + Option("-s", "--stats", + action="store_true", dest='stats', default=False, + help="Output basic data statistics"), + + Option("-c", "--counts", + action="store_true", dest='counts', default=False, + help="Output counts - number of entries for each numeric value " + "(useful for int ROI maps)"), + + Option("--all-counts", + action="store_true", dest='all_counts', default=False, + help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), + + Option("-z", "--zeros", + action="store_true", dest='stats_zeros', default=False, + help="Include zeros into output basic data statistics (--stats, --counts)"), + ]) + + return p + + +def proc_file(f, opts): + verbose(1, "Loading %s" % f) + + row = ["@l%s" % f] + try: + vol = nib.load(f) + h = vol.header + except Exception as e: + row += ['failed'] + verbose(2, "Failed to gather information -- %s" % str(e)) + return row + + row += [str(safe_get(h, 'data_dtype')), + '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), + '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] + # Slope + if hasattr(h, 'has_data_slope') and \ + (h.has_data_slope or h.has_data_intercept) and \ + not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: + row += ['@l*%.3g+%.3g' % h.get_slope_inter()] + else: + row += [''] + + if hasattr(h, 'extensions') and len(h.extensions): + row += ['@l#exts: %d' % len(h.extensions)] + else: + row += [''] + + if opts.header_fields: + # signals "all fields" + if opts.header_fields == 'all': + # TODO: might vary across file types, thus prior sensing + # would be needed + header_fields = h.keys() + else: + header_fields = opts.header_fields.split(',') + + for f in header_fields: + if not f: # skip empty + continue + try: + row += [str(h[f])] + except (KeyError, ValueError): + row += [_err()] + + try: + if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and + (h.get_qform() != h.get_sform()).any()): + row += ['sform'] + else: + row += [''] + except Exception as e: + verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) + if isinstance(h, nib.AnalyzeHeader): + row += [''] + else: + row += [_err()] + + if opts.stats or opts.counts: + # We are doomed to load data + try: + d = vol.get_data() + if not opts.stats_zeros: + d = d[np.nonzero(d)] + else: + # at least flatten it -- functionality below doesn't + # depend on the original shape, so let's use a flat view + d = d.reshape(-1) + if opts.stats: + # just # of elements + row += ["@l[%d]" % np.prod(d.shape)] + # stats + row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] + if opts.counts: + items, inv = np.unique(d, return_inverse=True) + if len(items) > 1000 and not opts.all_counts: + counts = _err("%d uniques. Use --all-counts" % len(items)) + else: + freq = np.bincount(inv) + counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) + row += ["@l" + counts] + except IOError as e: + verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) + row += [_err()] + return row + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) \ No newline at end of file diff --git a/nibabel/cmdline/utils.py b/nibabel/cmdline/utils.py new file mode 100644 index 0000000000..ae96c99a13 --- /dev/null +++ b/nibabel/cmdline/utils.py @@ -0,0 +1,127 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Helper utilities to be used in cmdline applications +""" + + +# global verbosity switch +import re +from io import StringIO +from math import ceil + +import numpy as np + +from nibabel.py3k import asunicode + +verbose_level = 0 + + +def _err(msg=None): + """To return a string to signal "error" in output table""" + if msg is None: + msg = 'error' + return '!' + msg + + +def verbose(l, msg): + """Print `s` if `l` is less than the `verbose_level` + """ + # TODO: consider using nibabel's logger + if l <= int(verbose_level): + print("%s%s" % (' ' * l, msg)) + + +def table2string(table, out=None): + """Given list of lists figure out their common widths and print to out + + Parameters + ---------- + table : list of lists of strings + What is aimed to be printed + out : None or stream + Where to print. If None -- will print and return string + + Returns + ------- + string if out was None + """ + + print2string = out is None + if print2string: + out = StringIO() + + # equalize number of elements in each row + nelements_max = \ + len(table) and \ + max(len(x) for x in table) + + for i, table_ in enumerate(table): + table[i] += [''] * (nelements_max - len(table_)) + + # figure out lengths within each column + atable = np.asarray(table) + # eat whole entry while computing width for @w (for wide) + markup_strip = re.compile('^@([lrc]|w.*)') + col_width = [max([len(markup_strip.sub('', x)) + for x in column]) for column in atable.T] + string = "" + for i, table_ in enumerate(table): + string_ = "" + for j, item in enumerate(table_): + item = str(item) + if item.startswith('@'): + align = item[1] + item = item[2:] + if align not in ['l', 'r', 'c', 'w']: + raise ValueError('Unknown alignment %s. Known are l,r,c' % + align) + else: + align = 'c' + + nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) + nspacesr = max(col_width[j] - nspacesl - len(item), 0) + + if align in ['w', 'c']: + pass + elif align == 'l': + nspacesl, nspacesr = 0, nspacesl + nspacesr + elif align == 'r': + nspacesl, nspacesr = nspacesl + nspacesr, 0 + else: + raise RuntimeError('Should not get here with align=%s' % align) + + string_ += "%%%ds%%s%%%ds " \ + % (nspacesl, nspacesr) % ('', item, '') + string += string_.rstrip() + '\n' + out.write(asunicode(string)) + + if print2string: + value = out.getvalue() + out.close() + return value + + +def ap(l, format_, sep=', '): + """Little helper to enforce consistency""" + if l == '-': + return l + ls = [format_ % x for x in l] + return sep.join(ls) + + +def safe_get(obj, name): + """A getattr which would return '-' if getattr fails + """ + try: + f = getattr(obj, 'get_' + name) + return f() + except Exception as e: + verbose(2, "get_%s() failed -- %s" % (name, e)) + return '-' \ No newline at end of file From d293a2025f665eaa392e46caeaf153432275a56b Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 3 Nov 2017 12:07:19 -0400 Subject: [PATCH 002/142] ENH: added a skeleton for nib-diff command --- bin/nib-diff | 17 ++++++++ nibabel/cmdline/diff.py | 75 +++++++++++++++++++++++++++++++++++ nibabel/tests/test_scripts.py | 4 ++ 3 files changed, 96 insertions(+) create mode 100755 bin/nib-diff create mode 100755 nibabel/cmdline/diff.py diff --git a/bin/nib-diff b/bin/nib-diff new file mode 100755 index 0000000000..2ae66dda9d --- /dev/null +++ b/bin/nib-diff @@ -0,0 +1,17 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Quick diff summary for a set of neuroimaging files +""" + +from nibabel.cmdline.diff import main + +if __name__ == '__main__': + main() diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py new file mode 100755 index 0000000000..27370f11d4 --- /dev/null +++ b/nibabel/cmdline/diff.py @@ -0,0 +1,75 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get + +__author__ = 'Yaroslav Halchenko & Christopher Cheng' +__copyright__ = 'Copyright (c) 2017 NiBabel contributors' +__license__ = 'MIT' + + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + ]) + + return p + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number + + # load the files headers + # see which fields differ + # call proc_file from ls, with opts.header_fields set to the fields which + # differ between files + opts.header_fields = [] # TODO #1 + from .ls import proc_file + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) + + # Later TODO #2 + # if opts.header_fields are specified, then limit comparison only to those + # fields \ No newline at end of file diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index 941e2271b0..18e1a92e94 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -128,6 +128,10 @@ def test_nib_ls_multiple(): ) +@script_test +def test_nib_diff(): + raise NotImplementedError("TODO") + @script_test def test_nib_nifti_dx(): From 5491af4fc41fe8e44364296f008bbd6ad3a696ac Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Mon, 6 Nov 2017 15:59:37 -0500 Subject: [PATCH 003/142] TODO1 attempt 1: processed data type, data shape, and data offset of two different headers --- nibabel/nibabel/cmdline/__init__.py | 12 ++ nibabel/nibabel/cmdline/diff.py | 98 ++++++++++++++++ nibabel/nibabel/cmdline/ls.py | 170 ++++++++++++++++++++++++++++ nibabel/nibabel/cmdline/utils.py | 127 +++++++++++++++++++++ 4 files changed, 407 insertions(+) create mode 100644 nibabel/nibabel/cmdline/__init__.py create mode 100755 nibabel/nibabel/cmdline/diff.py create mode 100755 nibabel/nibabel/cmdline/ls.py create mode 100644 nibabel/nibabel/cmdline/utils.py diff --git a/nibabel/nibabel/cmdline/__init__.py b/nibabel/nibabel/cmdline/__init__.py new file mode 100644 index 0000000000..a5f3e38228 --- /dev/null +++ b/nibabel/nibabel/cmdline/__init__.py @@ -0,0 +1,12 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +"""Functionality to be exposed in the command line +""" + + diff --git a/nibabel/nibabel/cmdline/diff.py b/nibabel/nibabel/cmdline/diff.py new file mode 100755 index 0000000000..bd01ce5349 --- /dev/null +++ b/nibabel/nibabel/cmdline/diff.py @@ -0,0 +1,98 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import fileinput + +__author__ = 'Yaroslav Halchenko & Christopher Cheng' +__copyright__ = 'Copyright (c) 2017 NiBabel contributors' +__license__ = 'MIT' + + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + ]) + + return p + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number + + # load the files headers + # see which fields differ + # call proc_file from ls, with opts.header_fields set to the fields which + # differ between files + + from nibabel.analyze import AnalyzeHeader + + img1 = AnalyzeHeader(nibabel.load(files[0])) # load first image + img2 = AnalyzeHeader(nibabel.load(files[1])) # load second image + + if img1.get_data_dtype() != img2.get_data_dtype(): + data_type = (img1.get_data_dtype(), img2.get_data_dtype()) + else: + return "Same" + + if img1.get_data_shape() != img2.get_data_shape(): + data_shape = (img1.get_data_shape(), img2.get_data_shape()) + else: + return "Same" + + if img1.get_data_offset() != img2.get_data_offset(): + data_offset = (img1.get_data_offset(), img2.get_data_offset()) + else: + return "Same" + + opts.header_fields = [data_type, data_shape, data_offset] # TODO #1 + + from .ls import proc_file + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) + + # Later TODO #2 + # if opts.header_fields are specified, then limit comparison only to those + # fields \ No newline at end of file diff --git a/nibabel/nibabel/cmdline/ls.py b/nibabel/nibabel/cmdline/ls.py new file mode 100755 index 0000000000..b302b67e5b --- /dev/null +++ b/nibabel/nibabel/cmdline/ls.py @@ -0,0 +1,170 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get + +__author__ = 'Yaroslav Halchenko' +__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ + 'and NiBabel contributors' +__license__ = 'MIT' + + +MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + + Option("-s", "--stats", + action="store_true", dest='stats', default=False, + help="Output basic data statistics"), + + Option("-c", "--counts", + action="store_true", dest='counts', default=False, + help="Output counts - number of entries for each numeric value " + "(useful for int ROI maps)"), + + Option("--all-counts", + action="store_true", dest='all_counts', default=False, + help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), + + Option("-z", "--zeros", + action="store_true", dest='stats_zeros', default=False, + help="Include zeros into output basic data statistics (--stats, --counts)"), + ]) + + return p + + +def proc_file(f, opts): + verbose(1, "Loading %s" % f) + + row = ["@l%s" % f] + try: + vol = nib.load(f) + h = vol.header + except Exception as e: + row += ['failed'] + verbose(2, "Failed to gather information -- %s" % str(e)) + return row + + row += [str(safe_get(h, 'data_dtype')), + '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), + '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] + # Slope + if hasattr(h, 'has_data_slope') and \ + (h.has_data_slope or h.has_data_intercept) and \ + not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: + row += ['@l*%.3g+%.3g' % h.get_slope_inter()] + else: + row += [''] + + if hasattr(h, 'extensions') and len(h.extensions): + row += ['@l#exts: %d' % len(h.extensions)] + else: + row += [''] + + if opts.header_fields: + # signals "all fields" + if opts.header_fields == 'all': + # TODO: might vary across file types, thus prior sensing + # would be needed + header_fields = h.keys() + else: + header_fields = opts.header_fields.split(',') + + for f in header_fields: + if not f: # skip empty + continue + try: + row += [str(h[f])] + except (KeyError, ValueError): + row += [_err()] + + try: + if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and + (h.get_qform() != h.get_sform()).any()): + row += ['sform'] + else: + row += [''] + except Exception as e: + verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) + if isinstance(h, nib.AnalyzeHeader): + row += [''] + else: + row += [_err()] + + if opts.stats or opts.counts: + # We are doomed to load data + try: + d = vol.get_data() + if not opts.stats_zeros: + d = d[np.nonzero(d)] + else: + # at least flatten it -- functionality below doesn't + # depend on the original shape, so let's use a flat view + d = d.reshape(-1) + if opts.stats: + # just # of elements + row += ["@l[%d]" % np.prod(d.shape)] + # stats + row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] + if opts.counts: + items, inv = np.unique(d, return_inverse=True) + if len(items) > 1000 and not opts.all_counts: + counts = _err("%d uniques. Use --all-counts" % len(items)) + else: + freq = np.bincount(inv) + counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) + row += ["@l" + counts] + except IOError as e: + verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) + row += [_err()] + return row + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) \ No newline at end of file diff --git a/nibabel/nibabel/cmdline/utils.py b/nibabel/nibabel/cmdline/utils.py new file mode 100644 index 0000000000..ae96c99a13 --- /dev/null +++ b/nibabel/nibabel/cmdline/utils.py @@ -0,0 +1,127 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Helper utilities to be used in cmdline applications +""" + + +# global verbosity switch +import re +from io import StringIO +from math import ceil + +import numpy as np + +from nibabel.py3k import asunicode + +verbose_level = 0 + + +def _err(msg=None): + """To return a string to signal "error" in output table""" + if msg is None: + msg = 'error' + return '!' + msg + + +def verbose(l, msg): + """Print `s` if `l` is less than the `verbose_level` + """ + # TODO: consider using nibabel's logger + if l <= int(verbose_level): + print("%s%s" % (' ' * l, msg)) + + +def table2string(table, out=None): + """Given list of lists figure out their common widths and print to out + + Parameters + ---------- + table : list of lists of strings + What is aimed to be printed + out : None or stream + Where to print. If None -- will print and return string + + Returns + ------- + string if out was None + """ + + print2string = out is None + if print2string: + out = StringIO() + + # equalize number of elements in each row + nelements_max = \ + len(table) and \ + max(len(x) for x in table) + + for i, table_ in enumerate(table): + table[i] += [''] * (nelements_max - len(table_)) + + # figure out lengths within each column + atable = np.asarray(table) + # eat whole entry while computing width for @w (for wide) + markup_strip = re.compile('^@([lrc]|w.*)') + col_width = [max([len(markup_strip.sub('', x)) + for x in column]) for column in atable.T] + string = "" + for i, table_ in enumerate(table): + string_ = "" + for j, item in enumerate(table_): + item = str(item) + if item.startswith('@'): + align = item[1] + item = item[2:] + if align not in ['l', 'r', 'c', 'w']: + raise ValueError('Unknown alignment %s. Known are l,r,c' % + align) + else: + align = 'c' + + nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) + nspacesr = max(col_width[j] - nspacesl - len(item), 0) + + if align in ['w', 'c']: + pass + elif align == 'l': + nspacesl, nspacesr = 0, nspacesl + nspacesr + elif align == 'r': + nspacesl, nspacesr = nspacesl + nspacesr, 0 + else: + raise RuntimeError('Should not get here with align=%s' % align) + + string_ += "%%%ds%%s%%%ds " \ + % (nspacesl, nspacesr) % ('', item, '') + string += string_.rstrip() + '\n' + out.write(asunicode(string)) + + if print2string: + value = out.getvalue() + out.close() + return value + + +def ap(l, format_, sep=', '): + """Little helper to enforce consistency""" + if l == '-': + return l + ls = [format_ % x for x in l] + return sep.join(ls) + + +def safe_get(obj, name): + """A getattr which would return '-' if getattr fails + """ + try: + f = getattr(obj, 'get_' + name) + return f() + except Exception as e: + verbose(2, "get_%s() failed -- %s" % (name, e)) + return '-' \ No newline at end of file From 949762c99e3995db981685c7c962df099f73c11f Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 16 Nov 2017 17:01:18 -0500 Subject: [PATCH 004/142] tweaked to remove AnalyzeHeader but currently still has problems --- nibabel/nibabel/cmdline/diff.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/nibabel/nibabel/cmdline/diff.py b/nibabel/nibabel/cmdline/diff.py index bd01ce5349..b12a7fe6c2 100755 --- a/nibabel/nibabel/cmdline/diff.py +++ b/nibabel/nibabel/cmdline/diff.py @@ -63,30 +63,29 @@ def main(): # load the files headers # see which fields differ - # call proc_file from ls, with opts.header_fields set to the fields which - # differ between files + # call proc_file from ls, with opts.header_fields set to the fields which differ between files - from nibabel.analyze import AnalyzeHeader + img1 = nibabel.load(files[0]) # load first image + img2 = nibabel.load(files[1]) # load second image - img1 = AnalyzeHeader(nibabel.load(files[0])) # load first image - img2 = AnalyzeHeader(nibabel.load(files[1])) # load second image - - if img1.get_data_dtype() != img2.get_data_dtype(): - data_type = (img1.get_data_dtype(), img2.get_data_dtype()) + if img1.header.get_data_dtype() != img2.header.get_data_dtype(): + data_dtype = (img1.header.get_data_dtype(), img2.header.get_data_dtype()) else: - return "Same" + return "Same data type" - if img1.get_data_shape() != img2.get_data_shape(): - data_shape = (img1.get_data_shape(), img2.get_data_shape()) + if img1.header.get_data_shape() != img2.header.get_data_shape(): + data_shape = (img1.header.get_data_shape(), img2.header.get_data_shape()) else: - return "Same" + return "Same data shape" - if img1.get_data_offset() != img2.get_data_offset(): - data_offset = (img1.get_data_offset(), img2.get_data_offset()) + if img1.header.get_zooms() != img2.header.get_zooms(): + zooms = (img1.header.get_zooms(), img2.header.get_zooms()) else: - return "Same" + return "Same voxel sizes" + + # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? - opts.header_fields = [data_type, data_shape, data_offset] # TODO #1 + opts.header_fields = [data_dtype, data_shape, zooms] # TODO #1 from .ls import proc_file rows = [proc_file(f, opts) for f in files] From 93b5e09b3188d719477d209c75dc11548dfd7cea Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 17 Nov 2017 14:24:16 -0500 Subject: [PATCH 005/142] added nib-diff to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a0f98c4395..2ed772e8e1 100755 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ def main(**extra_args): pjoin('bin', 'nib-ls'), pjoin('bin', 'nib-dicomfs'), pjoin('bin', 'nib-nifti-dx'), + pjoin('bin', 'nib-diff'), ], cmdclass = cmdclass, **extra_args From 22804f1ae3a98556c7f37351b2d93deee1af9448 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 17 Nov 2017 14:32:36 -0500 Subject: [PATCH 006/142] first attempt at nib-diff that doesnt work --- nibabel/cmdline/diff.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 27370f11d4..b12a7fe6c2 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -20,6 +20,7 @@ import nibabel as nib import nibabel.cmdline.utils from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import fileinput __author__ = 'Yaroslav Halchenko & Christopher Cheng' __copyright__ = 'Copyright (c) 2017 NiBabel contributors' @@ -62,9 +63,30 @@ def main(): # load the files headers # see which fields differ - # call proc_file from ls, with opts.header_fields set to the fields which - # differ between files - opts.header_fields = [] # TODO #1 + # call proc_file from ls, with opts.header_fields set to the fields which differ between files + + img1 = nibabel.load(files[0]) # load first image + img2 = nibabel.load(files[1]) # load second image + + if img1.header.get_data_dtype() != img2.header.get_data_dtype(): + data_dtype = (img1.header.get_data_dtype(), img2.header.get_data_dtype()) + else: + return "Same data type" + + if img1.header.get_data_shape() != img2.header.get_data_shape(): + data_shape = (img1.header.get_data_shape(), img2.header.get_data_shape()) + else: + return "Same data shape" + + if img1.header.get_zooms() != img2.header.get_zooms(): + zooms = (img1.header.get_zooms(), img2.header.get_zooms()) + else: + return "Same voxel sizes" + + # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? + + opts.header_fields = [data_dtype, data_shape, zooms] # TODO #1 + from .ls import proc_file rows = [proc_file(f, opts) for f in files] From f81a78b5c695442334beab8542977820919a7573 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 17 Nov 2017 14:44:49 -0500 Subject: [PATCH 007/142] removed incorrectly committed changes --- nibabel/nibabel/cmdline/__init__.py | 12 -- nibabel/nibabel/cmdline/diff.py | 97 ---------------- nibabel/nibabel/cmdline/ls.py | 170 ---------------------------- nibabel/nibabel/cmdline/utils.py | 127 --------------------- 4 files changed, 406 deletions(-) delete mode 100644 nibabel/nibabel/cmdline/__init__.py delete mode 100755 nibabel/nibabel/cmdline/diff.py delete mode 100755 nibabel/nibabel/cmdline/ls.py delete mode 100644 nibabel/nibabel/cmdline/utils.py diff --git a/nibabel/nibabel/cmdline/__init__.py b/nibabel/nibabel/cmdline/__init__.py deleted file mode 100644 index a5f3e38228..0000000000 --- a/nibabel/nibabel/cmdline/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -"""Functionality to be exposed in the command line -""" - - diff --git a/nibabel/nibabel/cmdline/diff.py b/nibabel/nibabel/cmdline/diff.py deleted file mode 100755 index b12a7fe6c2..0000000000 --- a/nibabel/nibabel/cmdline/diff.py +++ /dev/null @@ -1,97 +0,0 @@ -#!python -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Output a summary table for neuroimaging files (resolution, dimensionality, etc.) -""" -from __future__ import division, print_function, absolute_import - -import sys -from optparse import OptionParser, Option - -import numpy as np - -import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get -import fileinput - -__author__ = 'Yaroslav Halchenko & Christopher Cheng' -__copyright__ = 'Copyright (c) 2017 NiBabel contributors' -__license__ = 'MIT' - - - -def get_opt_parser(): - # use module docstring for help output - p = OptionParser( - usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, - version="%prog " + nib.__version__) - - p.add_options([ - Option("-v", "--verbose", action="count", - dest="verbose", default=0, - help="Make more noise. Could be specified multiple times"), - - Option("-H", "--header-fields", - dest="header_fields", default='', - help="Header fields (comma separated) to be printed as well (if present)"), - ]) - - return p - - -def main(): - """Show must go on""" - - parser = get_opt_parser() - (opts, files) = parser.parse_args() - - nibabel.cmdline.utils.verbose_level = opts.verbose - - if nibabel.cmdline.utils.verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - - assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number - - # load the files headers - # see which fields differ - # call proc_file from ls, with opts.header_fields set to the fields which differ between files - - img1 = nibabel.load(files[0]) # load first image - img2 = nibabel.load(files[1]) # load second image - - if img1.header.get_data_dtype() != img2.header.get_data_dtype(): - data_dtype = (img1.header.get_data_dtype(), img2.header.get_data_dtype()) - else: - return "Same data type" - - if img1.header.get_data_shape() != img2.header.get_data_shape(): - data_shape = (img1.header.get_data_shape(), img2.header.get_data_shape()) - else: - return "Same data shape" - - if img1.header.get_zooms() != img2.header.get_zooms(): - zooms = (img1.header.get_zooms(), img2.header.get_zooms()) - else: - return "Same voxel sizes" - - # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? - - opts.header_fields = [data_dtype, data_shape, zooms] # TODO #1 - - from .ls import proc_file - rows = [proc_file(f, opts) for f in files] - - print(table2string(rows)) - - # Later TODO #2 - # if opts.header_fields are specified, then limit comparison only to those - # fields \ No newline at end of file diff --git a/nibabel/nibabel/cmdline/ls.py b/nibabel/nibabel/cmdline/ls.py deleted file mode 100755 index b302b67e5b..0000000000 --- a/nibabel/nibabel/cmdline/ls.py +++ /dev/null @@ -1,170 +0,0 @@ -#!python -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Output a summary table for neuroimaging files (resolution, dimensionality, etc.) -""" -from __future__ import division, print_function, absolute_import - -import sys -from optparse import OptionParser, Option - -import numpy as np - -import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get - -__author__ = 'Yaroslav Halchenko' -__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ - 'and NiBabel contributors' -__license__ = 'MIT' - - -MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts - - -def get_opt_parser(): - # use module docstring for help output - p = OptionParser( - usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, - version="%prog " + nib.__version__) - - p.add_options([ - Option("-v", "--verbose", action="count", - dest="verbose", default=0, - help="Make more noise. Could be specified multiple times"), - - Option("-H", "--header-fields", - dest="header_fields", default='', - help="Header fields (comma separated) to be printed as well (if present)"), - - Option("-s", "--stats", - action="store_true", dest='stats', default=False, - help="Output basic data statistics"), - - Option("-c", "--counts", - action="store_true", dest='counts', default=False, - help="Output counts - number of entries for each numeric value " - "(useful for int ROI maps)"), - - Option("--all-counts", - action="store_true", dest='all_counts', default=False, - help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), - - Option("-z", "--zeros", - action="store_true", dest='stats_zeros', default=False, - help="Include zeros into output basic data statistics (--stats, --counts)"), - ]) - - return p - - -def proc_file(f, opts): - verbose(1, "Loading %s" % f) - - row = ["@l%s" % f] - try: - vol = nib.load(f) - h = vol.header - except Exception as e: - row += ['failed'] - verbose(2, "Failed to gather information -- %s" % str(e)) - return row - - row += [str(safe_get(h, 'data_dtype')), - '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), - '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] - # Slope - if hasattr(h, 'has_data_slope') and \ - (h.has_data_slope or h.has_data_intercept) and \ - not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: - row += ['@l*%.3g+%.3g' % h.get_slope_inter()] - else: - row += [''] - - if hasattr(h, 'extensions') and len(h.extensions): - row += ['@l#exts: %d' % len(h.extensions)] - else: - row += [''] - - if opts.header_fields: - # signals "all fields" - if opts.header_fields == 'all': - # TODO: might vary across file types, thus prior sensing - # would be needed - header_fields = h.keys() - else: - header_fields = opts.header_fields.split(',') - - for f in header_fields: - if not f: # skip empty - continue - try: - row += [str(h[f])] - except (KeyError, ValueError): - row += [_err()] - - try: - if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and - (h.get_qform() != h.get_sform()).any()): - row += ['sform'] - else: - row += [''] - except Exception as e: - verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) - if isinstance(h, nib.AnalyzeHeader): - row += [''] - else: - row += [_err()] - - if opts.stats or opts.counts: - # We are doomed to load data - try: - d = vol.get_data() - if not opts.stats_zeros: - d = d[np.nonzero(d)] - else: - # at least flatten it -- functionality below doesn't - # depend on the original shape, so let's use a flat view - d = d.reshape(-1) - if opts.stats: - # just # of elements - row += ["@l[%d]" % np.prod(d.shape)] - # stats - row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] - if opts.counts: - items, inv = np.unique(d, return_inverse=True) - if len(items) > 1000 and not opts.all_counts: - counts = _err("%d uniques. Use --all-counts" % len(items)) - else: - freq = np.bincount(inv) - counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) - row += ["@l" + counts] - except IOError as e: - verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) - row += [_err()] - return row - - -def main(): - """Show must go on""" - - parser = get_opt_parser() - (opts, files) = parser.parse_args() - - nibabel.cmdline.utils.verbose_level = opts.verbose - - if nibabel.cmdline.utils.verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - - rows = [proc_file(f, opts) for f in files] - - print(table2string(rows)) \ No newline at end of file diff --git a/nibabel/nibabel/cmdline/utils.py b/nibabel/nibabel/cmdline/utils.py deleted file mode 100644 index ae96c99a13..0000000000 --- a/nibabel/nibabel/cmdline/utils.py +++ /dev/null @@ -1,127 +0,0 @@ -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Helper utilities to be used in cmdline applications -""" - - -# global verbosity switch -import re -from io import StringIO -from math import ceil - -import numpy as np - -from nibabel.py3k import asunicode - -verbose_level = 0 - - -def _err(msg=None): - """To return a string to signal "error" in output table""" - if msg is None: - msg = 'error' - return '!' + msg - - -def verbose(l, msg): - """Print `s` if `l` is less than the `verbose_level` - """ - # TODO: consider using nibabel's logger - if l <= int(verbose_level): - print("%s%s" % (' ' * l, msg)) - - -def table2string(table, out=None): - """Given list of lists figure out their common widths and print to out - - Parameters - ---------- - table : list of lists of strings - What is aimed to be printed - out : None or stream - Where to print. If None -- will print and return string - - Returns - ------- - string if out was None - """ - - print2string = out is None - if print2string: - out = StringIO() - - # equalize number of elements in each row - nelements_max = \ - len(table) and \ - max(len(x) for x in table) - - for i, table_ in enumerate(table): - table[i] += [''] * (nelements_max - len(table_)) - - # figure out lengths within each column - atable = np.asarray(table) - # eat whole entry while computing width for @w (for wide) - markup_strip = re.compile('^@([lrc]|w.*)') - col_width = [max([len(markup_strip.sub('', x)) - for x in column]) for column in atable.T] - string = "" - for i, table_ in enumerate(table): - string_ = "" - for j, item in enumerate(table_): - item = str(item) - if item.startswith('@'): - align = item[1] - item = item[2:] - if align not in ['l', 'r', 'c', 'w']: - raise ValueError('Unknown alignment %s. Known are l,r,c' % - align) - else: - align = 'c' - - nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) - nspacesr = max(col_width[j] - nspacesl - len(item), 0) - - if align in ['w', 'c']: - pass - elif align == 'l': - nspacesl, nspacesr = 0, nspacesl + nspacesr - elif align == 'r': - nspacesl, nspacesr = nspacesl + nspacesr, 0 - else: - raise RuntimeError('Should not get here with align=%s' % align) - - string_ += "%%%ds%%s%%%ds " \ - % (nspacesl, nspacesr) % ('', item, '') - string += string_.rstrip() + '\n' - out.write(asunicode(string)) - - if print2string: - value = out.getvalue() - out.close() - return value - - -def ap(l, format_, sep=', '): - """Little helper to enforce consistency""" - if l == '-': - return l - ls = [format_ % x for x in l] - return sep.join(ls) - - -def safe_get(obj, name): - """A getattr which would return '-' if getattr fails - """ - try: - f = getattr(obj, 'get_' + name) - return f() - except Exception as e: - verbose(2, "get_%s() failed -- %s" % (name, e)) - return '-' \ No newline at end of file From fe9c052d16a701dd9160ab75fec96beca71c7bf1 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 17 Nov 2017 15:07:35 -0500 Subject: [PATCH 008/142] BK: stab at the test_dict_diff --- nibabel/tests/test_diff.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 nibabel/tests/test_diff.py diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py new file mode 100644 index 0000000000..17be7ef849 --- /dev/null +++ b/nibabel/tests/test_diff.py @@ -0,0 +1,47 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" Test scripts + +Test running scripts +""" +from __future__ import division, print_function, absolute_import + +import sys +import os +from os.path import (dirname, join as pjoin, abspath, splitext, basename, + exists) +import csv +from glob import glob + +import numpy as np + +from ..tmpdirs import InTemporaryDirectory +from ..loadsave import load +from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff + +from nose.tools import assert_true, assert_false, assert_equal +from nose import SkipTest + +from numpy.testing import assert_almost_equal + +from .scriptrunner import ScriptRunner +from .nibabel_data import needs_nibabel_data +from ..testing import assert_dt_equal, assert_re_in +from .test_parrec import (DTI_PAR_BVECS, DTI_PAR_BVALS, + EXAMPLE_IMAGES as PARREC_EXAMPLES) +from .test_parrec_data import BALLS, AFF_OFF +from .test_helpers import assert_data_similar + + + +DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) + +from nibabel.cmdline.diff import diff_dicts + + +def test_diff_dicts(): + assert_equal(diff_dicts({}, {}), {}) + assert_equal(diff_dicts({'dtype': int}, {'dtype': float}), {'dtype': (int, float)}) + assert_equal(diff_dicts({1: 2}, {1: 2}), {}) + # TODO: mixed cases + # TODO: arrays From 5eb44772cd6c3b2d777900f1791f712aeff9cfa3 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Mon, 27 Nov 2017 23:50:44 -0500 Subject: [PATCH 009/142] first attempt at diff_dicts method and diff testing file --- cmdline/__init__.py | 12 ++++ cmdline/diff.py | 91 ++++++++++++++++++++++++ cmdline/ls.py | 170 ++++++++++++++++++++++++++++++++++++++++++++ cmdline/utils.py | 127 +++++++++++++++++++++++++++++++++ nibabel/diff.py | 47 ++++++++++++ 5 files changed, 447 insertions(+) create mode 100644 cmdline/__init__.py create mode 100755 cmdline/diff.py create mode 100755 cmdline/ls.py create mode 100644 cmdline/utils.py create mode 100644 nibabel/diff.py diff --git a/cmdline/__init__.py b/cmdline/__init__.py new file mode 100644 index 0000000000..a5f3e38228 --- /dev/null +++ b/cmdline/__init__.py @@ -0,0 +1,12 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +"""Functionality to be exposed in the command line +""" + + diff --git a/cmdline/diff.py b/cmdline/diff.py new file mode 100755 index 0000000000..0ba6732e37 --- /dev/null +++ b/cmdline/diff.py @@ -0,0 +1,91 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import fileinput + +__author__ = 'Yaroslav Halchenko & Christopher Cheng' +__copyright__ = 'Copyright (c) 2017 NiBabel contributors' +__license__ = 'MIT' + + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + ]) + + return p + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number + + # load the files headers + # see which fields differ + # call proc_file from ls, with opts.header_fields set to the fields which differ between files + + img1 = nibabel.load(files[0]) # load first image + img2 = nibabel.load(files[1]) # load second image + + def diff_dicts(compare1, compare2): + if {i: compare1.header[i] for i in compare1.header.keys()} == {i: compare2.header[i] for i in + compare2.header.keys()}: + return + else: + for i in compare1.header.keys(): + if compare1.header[i] != compare2.header[i]: + print({i: (compare1.header[i], compare2.header[i])}) + + # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? + + opts.header_fields = [] # TODO #1 + + from .ls import proc_file + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) + + # Later TODO #2 + # if opts.header_fields are specified, then limit comparison only to those + # fields \ No newline at end of file diff --git a/cmdline/ls.py b/cmdline/ls.py new file mode 100755 index 0000000000..b302b67e5b --- /dev/null +++ b/cmdline/ls.py @@ -0,0 +1,170 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +""" +from __future__ import division, print_function, absolute_import + +import sys +from optparse import OptionParser, Option + +import numpy as np + +import nibabel as nib +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get + +__author__ = 'Yaroslav Halchenko' +__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ + 'and NiBabel contributors' +__license__ = 'MIT' + + +MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts + + +def get_opt_parser(): + # use module docstring for help output + p = OptionParser( + usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, + version="%prog " + nib.__version__) + + p.add_options([ + Option("-v", "--verbose", action="count", + dest="verbose", default=0, + help="Make more noise. Could be specified multiple times"), + + Option("-H", "--header-fields", + dest="header_fields", default='', + help="Header fields (comma separated) to be printed as well (if present)"), + + Option("-s", "--stats", + action="store_true", dest='stats', default=False, + help="Output basic data statistics"), + + Option("-c", "--counts", + action="store_true", dest='counts', default=False, + help="Output counts - number of entries for each numeric value " + "(useful for int ROI maps)"), + + Option("--all-counts", + action="store_true", dest='all_counts', default=False, + help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), + + Option("-z", "--zeros", + action="store_true", dest='stats_zeros', default=False, + help="Include zeros into output basic data statistics (--stats, --counts)"), + ]) + + return p + + +def proc_file(f, opts): + verbose(1, "Loading %s" % f) + + row = ["@l%s" % f] + try: + vol = nib.load(f) + h = vol.header + except Exception as e: + row += ['failed'] + verbose(2, "Failed to gather information -- %s" % str(e)) + return row + + row += [str(safe_get(h, 'data_dtype')), + '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), + '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] + # Slope + if hasattr(h, 'has_data_slope') and \ + (h.has_data_slope or h.has_data_intercept) and \ + not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: + row += ['@l*%.3g+%.3g' % h.get_slope_inter()] + else: + row += [''] + + if hasattr(h, 'extensions') and len(h.extensions): + row += ['@l#exts: %d' % len(h.extensions)] + else: + row += [''] + + if opts.header_fields: + # signals "all fields" + if opts.header_fields == 'all': + # TODO: might vary across file types, thus prior sensing + # would be needed + header_fields = h.keys() + else: + header_fields = opts.header_fields.split(',') + + for f in header_fields: + if not f: # skip empty + continue + try: + row += [str(h[f])] + except (KeyError, ValueError): + row += [_err()] + + try: + if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and + (h.get_qform() != h.get_sform()).any()): + row += ['sform'] + else: + row += [''] + except Exception as e: + verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) + if isinstance(h, nib.AnalyzeHeader): + row += [''] + else: + row += [_err()] + + if opts.stats or opts.counts: + # We are doomed to load data + try: + d = vol.get_data() + if not opts.stats_zeros: + d = d[np.nonzero(d)] + else: + # at least flatten it -- functionality below doesn't + # depend on the original shape, so let's use a flat view + d = d.reshape(-1) + if opts.stats: + # just # of elements + row += ["@l[%d]" % np.prod(d.shape)] + # stats + row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] + if opts.counts: + items, inv = np.unique(d, return_inverse=True) + if len(items) > 1000 and not opts.all_counts: + counts = _err("%d uniques. Use --all-counts" % len(items)) + else: + freq = np.bincount(inv) + counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) + row += ["@l" + counts] + except IOError as e: + verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) + row += [_err()] + return row + + +def main(): + """Show must go on""" + + parser = get_opt_parser() + (opts, files) = parser.parse_args() + + nibabel.cmdline.utils.verbose_level = opts.verbose + + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 + + rows = [proc_file(f, opts) for f in files] + + print(table2string(rows)) \ No newline at end of file diff --git a/cmdline/utils.py b/cmdline/utils.py new file mode 100644 index 0000000000..ae96c99a13 --- /dev/null +++ b/cmdline/utils.py @@ -0,0 +1,127 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Helper utilities to be used in cmdline applications +""" + + +# global verbosity switch +import re +from io import StringIO +from math import ceil + +import numpy as np + +from nibabel.py3k import asunicode + +verbose_level = 0 + + +def _err(msg=None): + """To return a string to signal "error" in output table""" + if msg is None: + msg = 'error' + return '!' + msg + + +def verbose(l, msg): + """Print `s` if `l` is less than the `verbose_level` + """ + # TODO: consider using nibabel's logger + if l <= int(verbose_level): + print("%s%s" % (' ' * l, msg)) + + +def table2string(table, out=None): + """Given list of lists figure out their common widths and print to out + + Parameters + ---------- + table : list of lists of strings + What is aimed to be printed + out : None or stream + Where to print. If None -- will print and return string + + Returns + ------- + string if out was None + """ + + print2string = out is None + if print2string: + out = StringIO() + + # equalize number of elements in each row + nelements_max = \ + len(table) and \ + max(len(x) for x in table) + + for i, table_ in enumerate(table): + table[i] += [''] * (nelements_max - len(table_)) + + # figure out lengths within each column + atable = np.asarray(table) + # eat whole entry while computing width for @w (for wide) + markup_strip = re.compile('^@([lrc]|w.*)') + col_width = [max([len(markup_strip.sub('', x)) + for x in column]) for column in atable.T] + string = "" + for i, table_ in enumerate(table): + string_ = "" + for j, item in enumerate(table_): + item = str(item) + if item.startswith('@'): + align = item[1] + item = item[2:] + if align not in ['l', 'r', 'c', 'w']: + raise ValueError('Unknown alignment %s. Known are l,r,c' % + align) + else: + align = 'c' + + nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) + nspacesr = max(col_width[j] - nspacesl - len(item), 0) + + if align in ['w', 'c']: + pass + elif align == 'l': + nspacesl, nspacesr = 0, nspacesl + nspacesr + elif align == 'r': + nspacesl, nspacesr = nspacesl + nspacesr, 0 + else: + raise RuntimeError('Should not get here with align=%s' % align) + + string_ += "%%%ds%%s%%%ds " \ + % (nspacesl, nspacesr) % ('', item, '') + string += string_.rstrip() + '\n' + out.write(asunicode(string)) + + if print2string: + value = out.getvalue() + out.close() + return value + + +def ap(l, format_, sep=', '): + """Little helper to enforce consistency""" + if l == '-': + return l + ls = [format_ % x for x in l] + return sep.join(ls) + + +def safe_get(obj, name): + """A getattr which would return '-' if getattr fails + """ + try: + f = getattr(obj, 'get_' + name) + return f() + except Exception as e: + verbose(2, "get_%s() failed -- %s" % (name, e)) + return '-' \ No newline at end of file diff --git a/nibabel/diff.py b/nibabel/diff.py new file mode 100644 index 0000000000..17be7ef849 --- /dev/null +++ b/nibabel/diff.py @@ -0,0 +1,47 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" Test scripts + +Test running scripts +""" +from __future__ import division, print_function, absolute_import + +import sys +import os +from os.path import (dirname, join as pjoin, abspath, splitext, basename, + exists) +import csv +from glob import glob + +import numpy as np + +from ..tmpdirs import InTemporaryDirectory +from ..loadsave import load +from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff + +from nose.tools import assert_true, assert_false, assert_equal +from nose import SkipTest + +from numpy.testing import assert_almost_equal + +from .scriptrunner import ScriptRunner +from .nibabel_data import needs_nibabel_data +from ..testing import assert_dt_equal, assert_re_in +from .test_parrec import (DTI_PAR_BVECS, DTI_PAR_BVALS, + EXAMPLE_IMAGES as PARREC_EXAMPLES) +from .test_parrec_data import BALLS, AFF_OFF +from .test_helpers import assert_data_similar + + + +DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) + +from nibabel.cmdline.diff import diff_dicts + + +def test_diff_dicts(): + assert_equal(diff_dicts({}, {}), {}) + assert_equal(diff_dicts({'dtype': int}, {'dtype': float}), {'dtype': (int, float)}) + assert_equal(diff_dicts({1: 2}, {1: 2}), {}) + # TODO: mixed cases + # TODO: arrays From 5e3a7670899c5ff088a4d93a80ef49c7131c7f9d Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Wed, 29 Nov 2017 21:26:11 -0500 Subject: [PATCH 010/142] tried something else with header fields --- cmdline/diff.py | 8 ++++++-- nibabel/diff.py | 47 ----------------------------------------------- 2 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 nibabel/diff.py diff --git a/cmdline/diff.py b/cmdline/diff.py index 0ba6732e37..43f37dd716 100755 --- a/cmdline/diff.py +++ b/cmdline/diff.py @@ -26,6 +26,12 @@ __copyright__ = 'Copyright (c) 2017 NiBabel contributors' __license__ = 'MIT' +# these fields are processed by the __get method +header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', + 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', + 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', + 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', + 'srow_y', 'srow_z', 'intent_name', 'magic'] def get_opt_parser(): @@ -77,8 +83,6 @@ def diff_dicts(compare1, compare2): if compare1.header[i] != compare2.header[i]: print({i: (compare1.header[i], compare2.header[i])}) - # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? - opts.header_fields = [] # TODO #1 from .ls import proc_file diff --git a/nibabel/diff.py b/nibabel/diff.py deleted file mode 100644 index 17be7ef849..0000000000 --- a/nibabel/diff.py +++ /dev/null @@ -1,47 +0,0 @@ -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" Test scripts - -Test running scripts -""" -from __future__ import division, print_function, absolute_import - -import sys -import os -from os.path import (dirname, join as pjoin, abspath, splitext, basename, - exists) -import csv -from glob import glob - -import numpy as np - -from ..tmpdirs import InTemporaryDirectory -from ..loadsave import load -from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff - -from nose.tools import assert_true, assert_false, assert_equal -from nose import SkipTest - -from numpy.testing import assert_almost_equal - -from .scriptrunner import ScriptRunner -from .nibabel_data import needs_nibabel_data -from ..testing import assert_dt_equal, assert_re_in -from .test_parrec import (DTI_PAR_BVECS, DTI_PAR_BVALS, - EXAMPLE_IMAGES as PARREC_EXAMPLES) -from .test_parrec_data import BALLS, AFF_OFF -from .test_helpers import assert_data_similar - - - -DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) - -from nibabel.cmdline.diff import diff_dicts - - -def test_diff_dicts(): - assert_equal(diff_dicts({}, {}), {}) - assert_equal(diff_dicts({'dtype': int}, {'dtype': float}), {'dtype': (int, float)}) - assert_equal(diff_dicts({1: 2}, {1: 2}), {}) - # TODO: mixed cases - # TODO: arrays From 7febf65b8ba5ae870b99f690c54847b1ce4ed0ea Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 30 Nov 2017 01:40:41 -0500 Subject: [PATCH 011/142] latest attempt: restructured diff_dicts() method, troubleshooting ensuing errors --- bin/nib-diff | 2 +- cmdline/diff.py | 26 +++++++++++--------------- cmdline/ls.py | 8 ++++---- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/bin/nib-diff b/bin/nib-diff index 2ae66dda9d..76b46c318d 100755 --- a/bin/nib-diff +++ b/bin/nib-diff @@ -11,7 +11,7 @@ Quick diff summary for a set of neuroimaging files """ -from nibabel.cmdline.diff import main +from cmdline.diff import main if __name__ == '__main__': main() diff --git a/cmdline/diff.py b/cmdline/diff.py index 43f37dd716..99efc8295e 100755 --- a/cmdline/diff.py +++ b/cmdline/diff.py @@ -18,8 +18,8 @@ import numpy as np import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import cmdline.utils +from cmdline.utils import _err, verbose, table2string, ap, safe_get import fileinput __author__ = 'Yaroslav Halchenko & Christopher Cheng' @@ -59,9 +59,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - nibabel.cmdline.utils.verbose_level = opts.verbose + cmdline.utils.verbose_level = opts.verbose - if nibabel.cmdline.utils.verbose_level < 3: + if cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 @@ -71,19 +71,15 @@ def main(): # see which fields differ # call proc_file from ls, with opts.header_fields set to the fields which differ between files - img1 = nibabel.load(files[0]) # load first image - img2 = nibabel.load(files[1]) # load second image + img1 = nib.load(files[0]) # load first image + img2 = nib.load(files[1]) # load second image def diff_dicts(compare1, compare2): - if {i: compare1.header[i] for i in compare1.header.keys()} == {i: compare2.header[i] for i in - compare2.header.keys()}: - return - else: - for i in compare1.header.keys(): - if compare1.header[i] != compare2.header[i]: - print({i: (compare1.header[i], compare2.header[i])}) - - opts.header_fields = [] # TODO #1 + for i in header_fields: + if {i: compare1.header[i]} == {i: compare2.header[i]}: + return + else: + opts.header_fields.append((compare1.header[i],compare2.header[i])) from .ls import proc_file rows = [proc_file(f, opts) for f in files] diff --git a/cmdline/ls.py b/cmdline/ls.py index b302b67e5b..c5c0d7be42 100755 --- a/cmdline/ls.py +++ b/cmdline/ls.py @@ -18,8 +18,8 @@ import numpy as np import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import cmdline.utils +from cmdline.utils import _err, verbose, table2string, ap, safe_get __author__ = 'Yaroslav Halchenko' __copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ @@ -159,9 +159,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - nibabel.cmdline.utils.verbose_level = opts.verbose + cmdline.utils.verbose_level = opts.verbose - if nibabel.cmdline.utils.verbose_level < 3: + if cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 From 23a43ba71693f2f4487500f4b5aa226a541bb164 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 1 Dec 2017 00:45:31 -0500 Subject: [PATCH 012/142] corrected misplacement of cmdline files and latest attempt at diff_dicts method --- cmdline/__init__.py | 12 --- cmdline/diff.py | 91 -------------------- cmdline/ls.py | 170 ------------------------------------- cmdline/utils.py | 127 --------------------------- nibabel/cmdline/diff.py | 43 ++++------ nibabel/cmdline/ls.py | 8 +- nibabel/tests/test_diff.py | 2 +- 7 files changed, 23 insertions(+), 430 deletions(-) delete mode 100644 cmdline/__init__.py delete mode 100755 cmdline/diff.py delete mode 100755 cmdline/ls.py delete mode 100644 cmdline/utils.py diff --git a/cmdline/__init__.py b/cmdline/__init__.py deleted file mode 100644 index a5f3e38228..0000000000 --- a/cmdline/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -"""Functionality to be exposed in the command line -""" - - diff --git a/cmdline/diff.py b/cmdline/diff.py deleted file mode 100755 index 99efc8295e..0000000000 --- a/cmdline/diff.py +++ /dev/null @@ -1,91 +0,0 @@ -#!python -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Output a summary table for neuroimaging files (resolution, dimensionality, etc.) -""" -from __future__ import division, print_function, absolute_import - -import sys -from optparse import OptionParser, Option - -import numpy as np - -import nibabel as nib -import cmdline.utils -from cmdline.utils import _err, verbose, table2string, ap, safe_get -import fileinput - -__author__ = 'Yaroslav Halchenko & Christopher Cheng' -__copyright__ = 'Copyright (c) 2017 NiBabel contributors' -__license__ = 'MIT' - -# these fields are processed by the __get method -header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', - 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', - 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', - 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', - 'srow_y', 'srow_z', 'intent_name', 'magic'] - - -def get_opt_parser(): - # use module docstring for help output - p = OptionParser( - usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, - version="%prog " + nib.__version__) - - p.add_options([ - Option("-v", "--verbose", action="count", - dest="verbose", default=0, - help="Make more noise. Could be specified multiple times"), - - Option("-H", "--header-fields", - dest="header_fields", default='', - help="Header fields (comma separated) to be printed as well (if present)"), - ]) - - return p - - -def main(): - """Show must go on""" - - parser = get_opt_parser() - (opts, files) = parser.parse_args() - - cmdline.utils.verbose_level = opts.verbose - - if cmdline.utils.verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - - assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number - - # load the files headers - # see which fields differ - # call proc_file from ls, with opts.header_fields set to the fields which differ between files - - img1 = nib.load(files[0]) # load first image - img2 = nib.load(files[1]) # load second image - - def diff_dicts(compare1, compare2): - for i in header_fields: - if {i: compare1.header[i]} == {i: compare2.header[i]}: - return - else: - opts.header_fields.append((compare1.header[i],compare2.header[i])) - - from .ls import proc_file - rows = [proc_file(f, opts) for f in files] - - print(table2string(rows)) - - # Later TODO #2 - # if opts.header_fields are specified, then limit comparison only to those - # fields \ No newline at end of file diff --git a/cmdline/ls.py b/cmdline/ls.py deleted file mode 100755 index c5c0d7be42..0000000000 --- a/cmdline/ls.py +++ /dev/null @@ -1,170 +0,0 @@ -#!python -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Output a summary table for neuroimaging files (resolution, dimensionality, etc.) -""" -from __future__ import division, print_function, absolute_import - -import sys -from optparse import OptionParser, Option - -import numpy as np - -import nibabel as nib -import cmdline.utils -from cmdline.utils import _err, verbose, table2string, ap, safe_get - -__author__ = 'Yaroslav Halchenko' -__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ - 'and NiBabel contributors' -__license__ = 'MIT' - - -MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts - - -def get_opt_parser(): - # use module docstring for help output - p = OptionParser( - usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__, - version="%prog " + nib.__version__) - - p.add_options([ - Option("-v", "--verbose", action="count", - dest="verbose", default=0, - help="Make more noise. Could be specified multiple times"), - - Option("-H", "--header-fields", - dest="header_fields", default='', - help="Header fields (comma separated) to be printed as well (if present)"), - - Option("-s", "--stats", - action="store_true", dest='stats', default=False, - help="Output basic data statistics"), - - Option("-c", "--counts", - action="store_true", dest='counts', default=False, - help="Output counts - number of entries for each numeric value " - "(useful for int ROI maps)"), - - Option("--all-counts", - action="store_true", dest='all_counts', default=False, - help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE), - - Option("-z", "--zeros", - action="store_true", dest='stats_zeros', default=False, - help="Include zeros into output basic data statistics (--stats, --counts)"), - ]) - - return p - - -def proc_file(f, opts): - verbose(1, "Loading %s" % f) - - row = ["@l%s" % f] - try: - vol = nib.load(f) - h = vol.header - except Exception as e: - row += ['failed'] - verbose(2, "Failed to gather information -- %s" % str(e)) - return row - - row += [str(safe_get(h, 'data_dtype')), - '@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'), - '@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')] - # Slope - if hasattr(h, 'has_data_slope') and \ - (h.has_data_slope or h.has_data_intercept) and \ - not h.get_slope_inter() in [(1.0, 0.0), (None, None)]: - row += ['@l*%.3g+%.3g' % h.get_slope_inter()] - else: - row += [''] - - if hasattr(h, 'extensions') and len(h.extensions): - row += ['@l#exts: %d' % len(h.extensions)] - else: - row += [''] - - if opts.header_fields: - # signals "all fields" - if opts.header_fields == 'all': - # TODO: might vary across file types, thus prior sensing - # would be needed - header_fields = h.keys() - else: - header_fields = opts.header_fields.split(',') - - for f in header_fields: - if not f: # skip empty - continue - try: - row += [str(h[f])] - except (KeyError, ValueError): - row += [_err()] - - try: - if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and - (h.get_qform() != h.get_sform()).any()): - row += ['sform'] - else: - row += [''] - except Exception as e: - verbose(2, "Failed to obtain qform or sform -- %s" % str(e)) - if isinstance(h, nib.AnalyzeHeader): - row += [''] - else: - row += [_err()] - - if opts.stats or opts.counts: - # We are doomed to load data - try: - d = vol.get_data() - if not opts.stats_zeros: - d = d[np.nonzero(d)] - else: - # at least flatten it -- functionality below doesn't - # depend on the original shape, so let's use a flat view - d = d.reshape(-1) - if opts.stats: - # just # of elements - row += ["@l[%d]" % np.prod(d.shape)] - # stats - row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-'] - if opts.counts: - items, inv = np.unique(d, return_inverse=True) - if len(items) > 1000 and not opts.all_counts: - counts = _err("%d uniques. Use --all-counts" % len(items)) - else: - freq = np.bincount(inv) - counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq)) - row += ["@l" + counts] - except IOError as e: - verbose(2, "Failed to obtain stats/counts -- %s" % str(e)) - row += [_err()] - return row - - -def main(): - """Show must go on""" - - parser = get_opt_parser() - (opts, files) = parser.parse_args() - - cmdline.utils.verbose_level = opts.verbose - - if cmdline.utils.verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - - rows = [proc_file(f, opts) for f in files] - - print(table2string(rows)) \ No newline at end of file diff --git a/cmdline/utils.py b/cmdline/utils.py deleted file mode 100644 index ae96c99a13..0000000000 --- a/cmdline/utils.py +++ /dev/null @@ -1,127 +0,0 @@ -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Helper utilities to be used in cmdline applications -""" - - -# global verbosity switch -import re -from io import StringIO -from math import ceil - -import numpy as np - -from nibabel.py3k import asunicode - -verbose_level = 0 - - -def _err(msg=None): - """To return a string to signal "error" in output table""" - if msg is None: - msg = 'error' - return '!' + msg - - -def verbose(l, msg): - """Print `s` if `l` is less than the `verbose_level` - """ - # TODO: consider using nibabel's logger - if l <= int(verbose_level): - print("%s%s" % (' ' * l, msg)) - - -def table2string(table, out=None): - """Given list of lists figure out their common widths and print to out - - Parameters - ---------- - table : list of lists of strings - What is aimed to be printed - out : None or stream - Where to print. If None -- will print and return string - - Returns - ------- - string if out was None - """ - - print2string = out is None - if print2string: - out = StringIO() - - # equalize number of elements in each row - nelements_max = \ - len(table) and \ - max(len(x) for x in table) - - for i, table_ in enumerate(table): - table[i] += [''] * (nelements_max - len(table_)) - - # figure out lengths within each column - atable = np.asarray(table) - # eat whole entry while computing width for @w (for wide) - markup_strip = re.compile('^@([lrc]|w.*)') - col_width = [max([len(markup_strip.sub('', x)) - for x in column]) for column in atable.T] - string = "" - for i, table_ in enumerate(table): - string_ = "" - for j, item in enumerate(table_): - item = str(item) - if item.startswith('@'): - align = item[1] - item = item[2:] - if align not in ['l', 'r', 'c', 'w']: - raise ValueError('Unknown alignment %s. Known are l,r,c' % - align) - else: - align = 'c' - - nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0) - nspacesr = max(col_width[j] - nspacesl - len(item), 0) - - if align in ['w', 'c']: - pass - elif align == 'l': - nspacesl, nspacesr = 0, nspacesl + nspacesr - elif align == 'r': - nspacesl, nspacesr = nspacesl + nspacesr, 0 - else: - raise RuntimeError('Should not get here with align=%s' % align) - - string_ += "%%%ds%%s%%%ds " \ - % (nspacesl, nspacesr) % ('', item, '') - string += string_.rstrip() + '\n' - out.write(asunicode(string)) - - if print2string: - value = out.getvalue() - out.close() - return value - - -def ap(l, format_, sep=', '): - """Little helper to enforce consistency""" - if l == '-': - return l - ls = [format_ % x for x in l] - return sep.join(ls) - - -def safe_get(obj, name): - """A getattr which would return '-' if getattr fails - """ - try: - f = getattr(obj, 'get_' + name) - return f() - except Exception as e: - verbose(2, "get_%s() failed -- %s" % (name, e)) - return '-' \ No newline at end of file diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index b12a7fe6c2..c86ab1fc2c 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -18,14 +18,20 @@ import numpy as np import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import cmdline.utils +from cmdline.utils import _err, verbose, table2string, ap, safe_get import fileinput __author__ = 'Yaroslav Halchenko & Christopher Cheng' __copyright__ = 'Copyright (c) 2017 NiBabel contributors' __license__ = 'MIT' +# these fields are processed by the __get method +header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', + 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', + 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', + 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', + 'srow_y', 'srow_z', 'intent_name', 'magic'] def get_opt_parser(): @@ -46,6 +52,13 @@ def get_opt_parser(): return p +def diff_dicts(compare1, compare2): + """Returns the header fields with differing values between two files""" + for i in header_fields: + if {i: compare1.header[i]} == {i: compare2.header[i]}: + return + else: + opts.header_fields.append((compare1.header[i],compare2.header[i])) def main(): """Show must go on""" @@ -53,9 +66,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - nibabel.cmdline.utils.verbose_level = opts.verbose + cmdline.utils.verbose_level = opts.verbose - if nibabel.cmdline.utils.verbose_level < 3: + if cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 @@ -65,27 +78,7 @@ def main(): # see which fields differ # call proc_file from ls, with opts.header_fields set to the fields which differ between files - img1 = nibabel.load(files[0]) # load first image - img2 = nibabel.load(files[1]) # load second image - - if img1.header.get_data_dtype() != img2.header.get_data_dtype(): - data_dtype = (img1.header.get_data_dtype(), img2.header.get_data_dtype()) - else: - return "Same data type" - - if img1.header.get_data_shape() != img2.header.get_data_shape(): - data_shape = (img1.header.get_data_shape(), img2.header.get_data_shape()) - else: - return "Same data shape" - - if img1.header.get_zooms() != img2.header.get_zooms(): - zooms = (img1.header.get_zooms(), img2.header.get_zooms()) - else: - return "Same voxel sizes" - - # MAIN QUESTION: HOW TO GET 1. properly load files and 2. replace with adjusted header fields? - - opts.header_fields = [data_dtype, data_shape, zooms] # TODO #1 + diff_dicts(files[0], files[1]) from .ls import proc_file rows = [proc_file(f, opts) for f in files] diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py index b302b67e5b..c5c0d7be42 100755 --- a/nibabel/cmdline/ls.py +++ b/nibabel/cmdline/ls.py @@ -18,8 +18,8 @@ import numpy as np import nibabel as nib -import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get +import cmdline.utils +from cmdline.utils import _err, verbose, table2string, ap, safe_get __author__ = 'Yaroslav Halchenko' __copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ @@ -159,9 +159,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - nibabel.cmdline.utils.verbose_level = opts.verbose + cmdline.utils.verbose_level = opts.verbose - if nibabel.cmdline.utils.verbose_level < 3: + if cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index 17be7ef849..6c67944895 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -36,7 +36,7 @@ DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) -from nibabel.cmdline.diff import diff_dicts +from cmdline.diff import diff_dicts def test_diff_dicts(): From fae491db886f40cf7f763130c05b6932f896dfd9 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 1 Dec 2017 01:45:03 -0500 Subject: [PATCH 013/142] progress! tweaked bugs, corrected rookie mistakes like cmdline placement, implemented yarik suggestions --- bin/nib-diff | 2 +- nibabel/cmdline/diff.py | 21 +++++++++++---------- nibabel/cmdline/ls.py | 8 ++++---- nibabel/tests/test_diff.py | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/bin/nib-diff b/bin/nib-diff index 76b46c318d..2ae66dda9d 100755 --- a/bin/nib-diff +++ b/bin/nib-diff @@ -11,7 +11,7 @@ Quick diff summary for a set of neuroimaging files """ -from cmdline.diff import main +from nibabel.cmdline.diff import main if __name__ == '__main__': main() diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index c86ab1fc2c..6df88feb23 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -18,8 +18,8 @@ import numpy as np import nibabel as nib -import cmdline.utils -from cmdline.utils import _err, verbose, table2string, ap, safe_get +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get import fileinput __author__ = 'Yaroslav Halchenko & Christopher Cheng' @@ -54,11 +54,9 @@ def get_opt_parser(): def diff_dicts(compare1, compare2): """Returns the header fields with differing values between two files""" - for i in header_fields: - if {i: compare1.header[i]} == {i: compare2.header[i]}: - return - else: - opts.header_fields.append((compare1.header[i],compare2.header[i])) + for i in header_fields: + if np.any(compare1.header[i] != compare2.header[i]): + return {i:(compare1.header[i],compare2.header[i])} def main(): """Show must go on""" @@ -66,9 +64,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - cmdline.utils.verbose_level = opts.verbose + nibabel.cmdline.utils.verbose_level = opts.verbose - if cmdline.utils.verbose_level < 3: + if nibabel.cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 @@ -78,7 +76,10 @@ def main(): # see which fields differ # call proc_file from ls, with opts.header_fields set to the fields which differ between files - diff_dicts(files[0], files[1]) + img1 = nib.load(files[0]) + img2 = nib.load(files[1]) + + opts.header_fields = [diff_dicts(img1, img2)] from .ls import proc_file rows = [proc_file(f, opts) for f in files] diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py index c5c0d7be42..b302b67e5b 100755 --- a/nibabel/cmdline/ls.py +++ b/nibabel/cmdline/ls.py @@ -18,8 +18,8 @@ import numpy as np import nibabel as nib -import cmdline.utils -from cmdline.utils import _err, verbose, table2string, ap, safe_get +import nibabel.cmdline.utils +from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get __author__ = 'Yaroslav Halchenko' __copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ @@ -159,9 +159,9 @@ def main(): parser = get_opt_parser() (opts, files) = parser.parse_args() - cmdline.utils.verbose_level = opts.verbose + nibabel.cmdline.utils.verbose_level = opts.verbose - if cmdline.utils.verbose_level < 3: + if nibabel.cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index 6c67944895..17be7ef849 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -36,7 +36,7 @@ DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) -from cmdline.diff import diff_dicts +from nibabel.cmdline.diff import diff_dicts def test_diff_dicts(): From 3e87d81ad406606caf361d7c1a4f1d6e236d4907 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 2 Dec 2017 02:24:12 -0500 Subject: [PATCH 014/142] got rid of proc file and function works at a basic level --- nibabel/cmdline/diff.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 6df88feb23..3c422187db 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -54,7 +54,7 @@ def get_opt_parser(): def diff_dicts(compare1, compare2): """Returns the header fields with differing values between two files""" - for i in header_fields: + for i in compare1.header.keys(): if np.any(compare1.header[i] != compare2.header[i]): return {i:(compare1.header[i],compare2.header[i])} @@ -79,12 +79,13 @@ def main(): img1 = nib.load(files[0]) img2 = nib.load(files[1]) - opts.header_fields = [diff_dicts(img1, img2)] +# opts.header_fields = [diff_dicts(img1, img2)] - from .ls import proc_file - rows = [proc_file(f, opts) for f in files] +# from .ls import proc_file +# rows = [proc_file(f, opts) for f in files] - print(table2string(rows)) +# print(table2string(rows)) + print(diff_dicts(img1, img2)) # Later TODO #2 # if opts.header_fields are specified, then limit comparison only to those From a3b35d9c14415066f5a79a3be05fce66c2555bcc Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sun, 3 Dec 2017 02:11:49 -0500 Subject: [PATCH 015/142] tweaked diff_dicts to be compatible with tests --- nibabel/cmdline/diff.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 3c422187db..9a64727ade 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -27,11 +27,11 @@ __license__ = 'MIT' # these fields are processed by the __get method -header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', - 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', - 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', - 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', - 'srow_y', 'srow_z', 'intent_name', 'magic'] +# header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', +# 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', +# 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', +# 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', +# 'srow_y', 'srow_z', 'intent_name', 'magic'] def get_opt_parser(): @@ -52,8 +52,18 @@ def get_opt_parser(): return p -def diff_dicts(compare1, compare2): - """Returns the header fields with differing values between two files""" + +def diff_dicts(key, compare1, compare2): + """Returns the differences between two dicts""" + if np.any(compare1 != compare2): + return {key: (compare1,compare2)} + elif compare1 or compare2 is None: + return {key: "Information for this header does not exist for both files"} + else: + return {key: None} + + +def diff_dicts2(compare1, compare2): for i in compare1.header.keys(): if np.any(compare1.header[i] != compare2.header[i]): return {i:(compare1.header[i],compare2.header[i])} @@ -85,7 +95,8 @@ def main(): # rows = [proc_file(f, opts) for f in files] # print(table2string(rows)) - print(diff_dicts(img1, img2)) + for i in img1.header.keys(): + print(diff_dicts(i, img1.header[i], img2.header[i])) # Later TODO #2 # if opts.header_fields are specified, then limit comparison only to those From 1491c61f9cad4d603dbe06bbf5f190ecbbcd4912 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 9 Dec 2017 01:23:12 -0500 Subject: [PATCH 016/142] got rid of None, troubleshot tests --- nibabel/cmdline/diff.py | 36 ++++++------------------------------ nibabel/tests/test_diff.py | 13 +++++++------ 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 9a64727ade..f34a43d72a 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -26,13 +26,6 @@ __copyright__ = 'Copyright (c) 2017 NiBabel contributors' __license__ = 'MIT' -# these fields are processed by the __get method -# header_fields = ['sizeof_hdr', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3', 'intent_code', 'datatype', -# 'bitpix', 'slice_start', 'pixdim', 'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code', -# 'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset', 'descrip', 'aux_file', 'qform_code', -# 'sform_code', 'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y', 'qoffset_z', 'srow_x', -# 'srow_y', 'srow_z', 'intent_name', 'magic'] - def get_opt_parser(): # use module docstring for help output @@ -57,16 +50,9 @@ def diff_dicts(key, compare1, compare2): """Returns the differences between two dicts""" if np.any(compare1 != compare2): return {key: (compare1,compare2)} - elif compare1 or compare2 is None: - return {key: "Information for this header does not exist for both files"} else: - return {key: None} - + pass -def diff_dicts2(compare1, compare2): - for i in compare1.header.keys(): - if np.any(compare1.header[i] != compare2.header[i]): - return {i:(compare1.header[i],compare2.header[i])} def main(): """Show must go on""" @@ -80,24 +66,14 @@ def main(): # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 - assert len(files) == 2, "ATM we can work only with two files" # TODO #3 -- make it work for any number - - # load the files headers - # see which fields differ - # call proc_file from ls, with opts.header_fields set to the fields which differ between files + assert len(files) == 2, "Please enter two files" + # TODO #3 -- make it work for any number img1 = nib.load(files[0]) img2 = nib.load(files[1]) -# opts.header_fields = [diff_dicts(img1, img2)] - -# from .ls import proc_file -# rows = [proc_file(f, opts) for f in files] - -# print(table2string(rows)) for i in img1.header.keys(): - print(diff_dicts(i, img1.header[i], img2.header[i])) + if diff_dicts(i, img1.header[i], img2.header[i]) is not None: + print(diff_dicts(i, img1.header[i], img2.header[i])) - # Later TODO #2 - # if opts.header_fields are specified, then limit comparison only to those - # fields \ No newline at end of file + # TODO #2 -- limit comparison only to certain fields diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index 17be7ef849..1ab6b344b4 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -15,7 +15,7 @@ import numpy as np -from ..tmpdirs import InTemporaryDirectory +# from ..tmpdirs import InTemporaryDirectory from ..loadsave import load from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff @@ -40,8 +40,9 @@ def test_diff_dicts(): - assert_equal(diff_dicts({}, {}), {}) - assert_equal(diff_dicts({'dtype': int}, {'dtype': float}), {'dtype': (int, float)}) - assert_equal(diff_dicts({1: 2}, {1: 2}), {}) - # TODO: mixed cases - # TODO: arrays + assert_equal(diff_dicts('key', None, None), None) + assert_equal(diff_dicts('dtype', int, float), {'dtype': (int, float)}) + assert_equal(diff_dicts(1, 2, 2), None) + assert_equal(diff_dicts('hdr', 1, None), {'hdr': (1, None)}) + assert_equal(diff_dicts('array', np.array([[-3., 0., 0., 3.]]), np.array([[-2., 0., 0., 3.]])), {'array': (array([[-3., 0., 0., 3.]]), array([[-2., 0., 0., 3.]]))}) + # TODO: mixed cases \ No newline at end of file From 397bc036ebbef5f31188265e960713af51c131c6 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 15 Dec 2017 03:18:16 -0500 Subject: [PATCH 017/142] introduced hypothesis to use for testing with pretty sexy results --- nibabel/cmdline/diff.py | 4 ++- nibabel/tests/test_diff.py | 56 +++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index f34a43d72a..30d7dbc27d 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -49,7 +49,9 @@ def get_opt_parser(): def diff_dicts(key, compare1, compare2): """Returns the differences between two dicts""" if np.any(compare1 != compare2): - return {key: (compare1,compare2)} + return {key: (compare1, compare2)} + elif type(compare1) != type(compare2): + return {key: (compare1, compare2)} else: pass diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index 1ab6b344b4..b51ebaa268 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -14,6 +14,7 @@ from glob import glob import numpy as np +from numpy import array # from ..tmpdirs import InTemporaryDirectory from ..loadsave import load @@ -32,6 +33,8 @@ from .test_parrec_data import BALLS, AFF_OFF from .test_helpers import assert_data_similar +from hypothesis import given +import hypothesis.strategies as st DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) @@ -39,10 +42,49 @@ from nibabel.cmdline.diff import diff_dicts -def test_diff_dicts(): - assert_equal(diff_dicts('key', None, None), None) - assert_equal(diff_dicts('dtype', int, float), {'dtype': (int, float)}) - assert_equal(diff_dicts(1, 2, 2), None) - assert_equal(diff_dicts('hdr', 1, None), {'hdr': (1, None)}) - assert_equal(diff_dicts('array', np.array([[-3., 0., 0., 3.]]), np.array([[-2., 0., 0., 3.]])), {'array': (array([[-3., 0., 0., 3.]]), array([[-2., 0., 0., 3.]]))}) - # TODO: mixed cases \ No newline at end of file +@given(st.data()) +def test_diff_dicts_int(data): + x = data.draw(st.integers(), label='x') + y = data.draw(st.integers(min_value = x + 1), label='x+1') + z = data.draw(st.integers(max_value = x - 1), label='x-1') + + assert diff_dicts('key', x, x) is None + assert diff_dicts('key', x, y) == {'key': (x, y)} + assert diff_dicts('key', x, z) == {'key': (x, z)} + assert diff_dicts('key', y, z) == {'key': (y, z)} + + +@given(st.data()) +def test_diff_dicts_float(data): + x = data.draw(st.just(0), label='x') + y = data.draw(st.floats(min_value = 1e8), label='y') + z = data.draw(st.floats(max_value = -1e8), label='z') + + assert diff_dicts('key', x, x) is None + assert diff_dicts('key', x, y) == {'key': (x, y)} + assert diff_dicts('key', x, z) == {'key': (x, z)} + assert diff_dicts('key', y, z) == {'key': (y, z)} + + +@given(st.data()) +def test_diff_dicts_mixed(data): + type_float = data.draw(st.floats(), label='float') + type_int = data.draw(st.integers(), label='int') + type_none = data.draw(st.none(), label='none') + + assert diff_dicts('key', type_float, type_int) == {'key': (type_float, type_int)} + assert diff_dicts('key', type_float, type_none) == {'key': (type_float, type_none)} + assert diff_dicts('key', type_int, type_none) == {'key': (type_int, type_none)} + assert diff_dicts('key', type_none, type_none) is None + + +@given(st.data()) +def test_diff_dicts_array(data): + a = data.draw(st.lists(elements=st.integers(min_value=0), min_size=1)) + b = data.draw(st.lists(elements=st.integers(max_value=-1), min_size=1)) + c = data.draw(st.lists(elements=st.floats(min_value=1e8), min_size=1)) + d = data.draw(st.lists(elements=st.floats(max_value=-1e8), min_size=1)) + # TODO: Figure out a way to include 0 in lists (arrays) + + assert diff_dicts('key', a, b) == {'key': (a, b)} + assert diff_dicts('key', c, d) == {'key': (c, d)} \ No newline at end of file From f192f65582188bc25138be6b08fb986ca9b3cfe5 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 16 Dec 2017 09:37:02 -0500 Subject: [PATCH 018/142] noted hypothesis need for tests, refactored diff_dicts name --- dev-requirements.txt | 1 + nibabel/cmdline/diff.py | 4 ++-- nibabel/tests/test_diff.py | 41 ++++++++++++++++++-------------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index f63af96cf4..014b7a9d01 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,3 +2,4 @@ -r requirements.txt nose mock +hypothesis \ No newline at end of file diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 30d7dbc27d..86bf7afc5f 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -46,7 +46,7 @@ def get_opt_parser(): return p -def diff_dicts(key, compare1, compare2): +def diff_values(key, compare1, compare2): """Returns the differences between two dicts""" if np.any(compare1 != compare2): return {key: (compare1, compare2)} @@ -75,7 +75,7 @@ def main(): img2 = nib.load(files[1]) for i in img1.header.keys(): - if diff_dicts(i, img1.header[i], img2.header[i]) is not None: + if diff_values(i, img1.header[i], img2.header[i]) is not None: print(diff_dicts(i, img1.header[i], img2.header[i])) # TODO #2 -- limit comparison only to certain fields diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index b51ebaa268..a6a8362721 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -13,9 +13,6 @@ import csv from glob import glob -import numpy as np -from numpy import array - # from ..tmpdirs import InTemporaryDirectory from ..loadsave import load from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff @@ -39,52 +36,52 @@ DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) -from nibabel.cmdline.diff import diff_dicts +from nibabel.cmdline.diff import diff_values @given(st.data()) -def test_diff_dicts_int(data): +def test_diff_values_int(data): x = data.draw(st.integers(), label='x') y = data.draw(st.integers(min_value = x + 1), label='x+1') z = data.draw(st.integers(max_value = x - 1), label='x-1') - assert diff_dicts('key', x, x) is None - assert diff_dicts('key', x, y) == {'key': (x, y)} - assert diff_dicts('key', x, z) == {'key': (x, z)} - assert diff_dicts('key', y, z) == {'key': (y, z)} + assert diff_values('key', x, x) is None + assert diff_values('key', x, y) == {'key': (x, y)} + assert diff_values('key', x, z) == {'key': (x, z)} + assert diff_values('key', y, z) == {'key': (y, z)} @given(st.data()) -def test_diff_dicts_float(data): +def test_diff_values_float(data): x = data.draw(st.just(0), label='x') y = data.draw(st.floats(min_value = 1e8), label='y') z = data.draw(st.floats(max_value = -1e8), label='z') - assert diff_dicts('key', x, x) is None - assert diff_dicts('key', x, y) == {'key': (x, y)} - assert diff_dicts('key', x, z) == {'key': (x, z)} - assert diff_dicts('key', y, z) == {'key': (y, z)} + assert diff_values('key', x, x) is None + assert diff_values('key', x, y) == {'key': (x, y)} + assert diff_values('key', x, z) == {'key': (x, z)} + assert diff_values('key', y, z) == {'key': (y, z)} @given(st.data()) -def test_diff_dicts_mixed(data): +def test_diff_values_mixed(data): type_float = data.draw(st.floats(), label='float') type_int = data.draw(st.integers(), label='int') type_none = data.draw(st.none(), label='none') - assert diff_dicts('key', type_float, type_int) == {'key': (type_float, type_int)} - assert diff_dicts('key', type_float, type_none) == {'key': (type_float, type_none)} - assert diff_dicts('key', type_int, type_none) == {'key': (type_int, type_none)} - assert diff_dicts('key', type_none, type_none) is None + assert diff_values('key', type_float, type_int) == {'key': (type_float, type_int)} + assert diff_values('key', type_float, type_none) == {'key': (type_float, type_none)} + assert diff_values('key', type_int, type_none) == {'key': (type_int, type_none)} + assert diff_values('key', type_none, type_none) is None @given(st.data()) -def test_diff_dicts_array(data): +def test_diff_values_array(data): a = data.draw(st.lists(elements=st.integers(min_value=0), min_size=1)) b = data.draw(st.lists(elements=st.integers(max_value=-1), min_size=1)) c = data.draw(st.lists(elements=st.floats(min_value=1e8), min_size=1)) d = data.draw(st.lists(elements=st.floats(max_value=-1e8), min_size=1)) # TODO: Figure out a way to include 0 in lists (arrays) - assert diff_dicts('key', a, b) == {'key': (a, b)} - assert diff_dicts('key', c, d) == {'key': (c, d)} \ No newline at end of file + assert diff_values('key', a, b) == {'key': (a, b)} + assert diff_values('key', c, d) == {'key': (c, d)} \ No newline at end of file From 92553a23ea6d3cf8cdbf639f044c9647545d65a9 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Mon, 18 Dec 2017 08:08:10 -0500 Subject: [PATCH 019/142] attempt at TODO#2: allowing specification of header fields --- nibabel/cmdline/diff.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 86bf7afc5f..d6c12262dd 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -39,7 +39,7 @@ def get_opt_parser(): help="Make more noise. Could be specified multiple times"), Option("-H", "--header-fields", - dest="header_fields", default='', + dest="header_fields", default='all', help="Header fields (comma separated) to be printed as well (if present)"), ]) @@ -56,6 +56,29 @@ def diff_values(key, compare1, compare2): pass +def proc_file(f1, f2, opts): + + vol = nib.load(f1) + vol2 = nib.load(f2) + h = vol.header + h2 = vol2.header + + if opts.header_fields: + # signals "all fields" + if opts.header_fields == 'all': + # TODO: might vary across file types, thus prior sensing + # would be needed + header_fields = h.keys() + else: + header_fields = opts.header_fields.split(',') + + for f in header_fields: + # if not f: # skip empty + # continue + if diff_values(f, h[f], h2[f]) is not None: + print(diff_values(f, h[f], h2[f])) + + def main(): """Show must go on""" @@ -64,18 +87,13 @@ def main(): nibabel.cmdline.utils.verbose_level = opts.verbose - if nibabel.cmdline.utils.verbose_level < 3: - # suppress nibabel format-compliance warnings - nib.imageglobals.logger.level = 50 - assert len(files) == 2, "Please enter two files" # TODO #3 -- make it work for any number - img1 = nib.load(files[0]) - img2 = nib.load(files[1]) + if nibabel.cmdline.utils.verbose_level < 3: + # suppress nibabel format-compliance warnings + nib.imageglobals.logger.level = 50 - for i in img1.header.keys(): - if diff_values(i, img1.header[i], img2.header[i]) is not None: - print(diff_dicts(i, img1.header[i], img2.header[i])) + rows = [proc_file(files[0], files[1], opts)] - # TODO #2 -- limit comparison only to certain fields + print(rows) From 7a70d56d2214f3a8b56b03d3b989f767336653b2 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 21 Dec 2017 02:19:28 -0500 Subject: [PATCH 020/142] now functional for several header files. --- nibabel/cmdline/diff.py | 85 ++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index d6c12262dd..d99556ae40 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -8,7 +8,7 @@ # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ -Output a summary table for neuroimaging files (resolution, dimensionality, etc.) +Quick summary of the differences among a set of neuroimaging files """ from __future__ import division, print_function, absolute_import @@ -46,54 +46,79 @@ def get_opt_parser(): return p -def diff_values(key, compare1, compare2): - """Returns the differences between two dicts""" - if np.any(compare1 != compare2): - return {key: (compare1, compare2)} - elif type(compare1) != type(compare2): - return {key: (compare1, compare2)} - else: - pass - - -def proc_file(f1, f2, opts): - - vol = nib.load(f1) - vol2 = nib.load(f2) - h = vol.header - h2 = vol2.header +def diff_values(key, inputs): + diffs = [] + + for i in range(len(inputs)): + if i != len(inputs)-1: + temp_input_1 = inputs[i] + temp_input_2 = inputs[i+1] + if np.any(temp_input_1[key] != temp_input_2[key]): + if i != 0 and temp_input_2 != diffs: + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + else: + diffs.append(temp_input_1[key]) + elif type(temp_input_1[key]) != type(temp_input_2[key]): + if i != 0 and temp_input_2 != diffs: + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + else: + diffs.append(temp_input_1[key]) + else: + pass + + # TODO: figure out a way to not have these erroneous outputs occur in the above loop + for a in range(len(diffs)-1): + for b in range(len(diffs)-1): + try: + if np.all(np.isnan(diffs[a])) and np.all(np.isnan(diffs[a+1])): + del diffs[a] + except TypeError: + pass + if a and b != len(diffs)-1: + if np.any(diffs[a] == diffs[b]): + del diffs[a] + + if len(diffs) > 1: + return {key: diffs} + + +def process_file(files, opts): + + file_list = [] + header_list = [] + + for f in range(len(files)): + file_list.append(nib.load(files[f])) + for h in range(len(files)): + header_list.append(file_list[f].header) if opts.header_fields: # signals "all fields" if opts.header_fields == 'all': - # TODO: might vary across file types, thus prior sensing - # would be needed - header_fields = h.keys() + # TODO: header fields might vary across file types, thus prior sensing would be needed + header_fields = header_list[0].keys() else: header_fields = opts.header_fields.split(',') for f in header_fields: - # if not f: # skip empty - # continue - if diff_values(f, h[f], h2[f]) is not None: - print(diff_values(f, h[f], h2[f])) + if diff_values(f, header_list) is not None: + print(diff_values(f, header_list)) def main(): - """Show must go on""" + """NO DAYS OFF""" parser = get_opt_parser() (opts, files) = parser.parse_args() nibabel.cmdline.utils.verbose_level = opts.verbose - assert len(files) == 2, "Please enter two files" - # TODO #3 -- make it work for any number + assert len(files) >= 2, "Please enter at least two files" if nibabel.cmdline.utils.verbose_level < 3: # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 - rows = [proc_file(files[0], files[1], opts)] - - print(rows) + process_file(files, opts) From f5e930d54c65ec272148b01202c6c926db3361d4 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 23 Dec 2017 16:52:15 +0800 Subject: [PATCH 021/142] tweak to make hypothesis work with a list, but problem above has not been addressed --- nibabel/tests/test_diff.py | 79 ++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index a6a8362721..cf89ab2be5 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -6,29 +6,7 @@ """ from __future__ import division, print_function, absolute_import -import sys -import os -from os.path import (dirname, join as pjoin, abspath, splitext, basename, - exists) -import csv -from glob import glob - -# from ..tmpdirs import InTemporaryDirectory -from ..loadsave import load -from ..orientations import flip_axis, aff2axcodes, inv_ornt_aff - -from nose.tools import assert_true, assert_false, assert_equal -from nose import SkipTest - -from numpy.testing import assert_almost_equal - -from .scriptrunner import ScriptRunner -from .nibabel_data import needs_nibabel_data -from ..testing import assert_dt_equal, assert_re_in -from .test_parrec import (DTI_PAR_BVECS, DTI_PAR_BVALS, - EXAMPLE_IMAGES as PARREC_EXAMPLES) -from .test_parrec_data import BALLS, AFF_OFF -from .test_helpers import assert_data_similar +from os.path import (dirname, join as pjoin, abspath) from hypothesis import given import hypothesis.strategies as st @@ -38,17 +16,26 @@ from nibabel.cmdline.diff import diff_values +# TODO: MAJOR TO DO IS TO FIGURE OUT HOW TO USE HYPOTHESIS FOR LONGER LIST LENGTHS WHILE STILL CONTROLLING FOR OUTCOMES @given(st.data()) def test_diff_values_int(data): x = data.draw(st.integers(), label='x') y = data.draw(st.integers(min_value = x + 1), label='x+1') z = data.draw(st.integers(max_value = x - 1), label='x-1') + list_1 = [x, x, x] + list_2 = [x, y, x] + list_3 = [x, y, z] + list_4 = [x, z, x] + list_5 = [y, z, y] + list_6 = [y, x, y] - assert diff_values('key', x, x) is None - assert diff_values('key', x, y) == {'key': (x, y)} - assert diff_values('key', x, z) == {'key': (x, z)} - assert diff_values('key', y, z) == {'key': (y, z)} + assert diff_values('key', list_1) is None + assert diff_values('key', list_2) == {'key': [x, y]} + assert diff_values('key', list_3) == {'key': [x, y, z]} + assert diff_values('key', list_4) == {'key': [x, z]} + assert diff_values('key', list_5) == {'key': [y, z]} + assert diff_values('key', list_6) == {'key': [y, x]} @given(st.data()) @@ -56,11 +43,19 @@ def test_diff_values_float(data): x = data.draw(st.just(0), label='x') y = data.draw(st.floats(min_value = 1e8), label='y') z = data.draw(st.floats(max_value = -1e8), label='z') + list_1 = [x, x, x] + list_2 = [x, y, x] + list_3 = [x, y, z] + list_4 = [x, z, x] + list_5 = [y, z, y] + list_6 = [y, x, y] - assert diff_values('key', x, x) is None - assert diff_values('key', x, y) == {'key': (x, y)} - assert diff_values('key', x, z) == {'key': (x, z)} - assert diff_values('key', y, z) == {'key': (y, z)} + assert diff_values('key', list_1) is None + assert diff_values('key', list_2) == {'key': [x, y]} + assert diff_values('key', list_3) == {'key': [x, y, z]} + assert diff_values('key', list_4) == {'key': [x, z]} + assert diff_values('key', list_5) == {'key': [y, z]} + assert diff_values('key', list_6) == {'key': [y, x]} @given(st.data()) @@ -68,11 +63,13 @@ def test_diff_values_mixed(data): type_float = data.draw(st.floats(), label='float') type_int = data.draw(st.integers(), label='int') type_none = data.draw(st.none(), label='none') + list_1 = [type_float, type_int] + list_2 = [type_none, type_none, type_none] + list_3 = [type_float, type_none, type_int] - assert diff_values('key', type_float, type_int) == {'key': (type_float, type_int)} - assert diff_values('key', type_float, type_none) == {'key': (type_float, type_none)} - assert diff_values('key', type_int, type_none) == {'key': (type_int, type_none)} - assert diff_values('key', type_none, type_none) is None + assert diff_values('key', list_1) == {'key': [type_float, type_int]} + assert diff_values('key', list_2) is None + assert diff_values('key', list_3) == {'key': [type_float, type_none, type_int]} @given(st.data()) @@ -82,6 +79,14 @@ def test_diff_values_array(data): c = data.draw(st.lists(elements=st.floats(min_value=1e8), min_size=1)) d = data.draw(st.lists(elements=st.floats(max_value=-1e8), min_size=1)) # TODO: Figure out a way to include 0 in lists (arrays) + # TODO: Figure out a way to test for NaN - assert diff_values('key', a, b) == {'key': (a, b)} - assert diff_values('key', c, d) == {'key': (c, d)} \ No newline at end of file + list_1 = [a, a, b] + list_2 = [a, b, c, d] + list_3 = [c, d] + list_4 = [b, b, c] + + assert diff_values('key', list_1) == {'key': [a, b]} + assert diff_values('key', list_2) == {'key': [a, b, c, d]} + assert diff_values('key', list_3) == {'key': [c, d]} + assert diff_values('key', list_1) == {'key': [b, c]} \ No newline at end of file From df82a517f8846d51919cd11d247c0653f2fcb278 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sun, 24 Dec 2017 22:22:37 +0800 Subject: [PATCH 022/142] tweaked names and code as suggested! --- nibabel/cmdline/diff.py | 17 +++++++--------- nibabel/tests/test_diff.py | 40 +++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index d99556ae40..93ed9c5e0f 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -46,7 +46,7 @@ def get_opt_parser(): return p -def diff_values(key, inputs): +def diff_files(key, inputs): diffs = [] for i in range(len(inputs)): @@ -84,15 +84,12 @@ def diff_values(key, inputs): return {key: diffs} -def process_file(files, opts): +def process_files(files, opts): - file_list = [] header_list = [] - for f in range(len(files)): - file_list.append(nib.load(files[f])) - for h in range(len(files)): - header_list.append(file_list[f].header) + for f in files: + header_list.append(nib.load(f).header) if opts.header_fields: # signals "all fields" @@ -103,8 +100,8 @@ def process_file(files, opts): header_fields = opts.header_fields.split(',') for f in header_fields: - if diff_values(f, header_list) is not None: - print(diff_values(f, header_list)) + if diff_files(f, header_list) is not None: + print(diff_files(f, header_list)) def main(): @@ -121,4 +118,4 @@ def main(): # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 - process_file(files, opts) + process_files(files, opts) diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index cf89ab2be5..e05eb7dfcb 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -14,7 +14,7 @@ DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) -from nibabel.cmdline.diff import diff_values +from nibabel.cmdline.diff import diff_files # TODO: MAJOR TO DO IS TO FIGURE OUT HOW TO USE HYPOTHESIS FOR LONGER LIST LENGTHS WHILE STILL CONTROLLING FOR OUTCOMES @@ -30,12 +30,12 @@ def test_diff_values_int(data): list_5 = [y, z, y] list_6 = [y, x, y] - assert diff_values('key', list_1) is None - assert diff_values('key', list_2) == {'key': [x, y]} - assert diff_values('key', list_3) == {'key': [x, y, z]} - assert diff_values('key', list_4) == {'key': [x, z]} - assert diff_values('key', list_5) == {'key': [y, z]} - assert diff_values('key', list_6) == {'key': [y, x]} + assert diff_files('key', list_1) is None + assert diff_files('key', list_2) == {'key': [x, y]} + assert diff_files('key', list_3) == {'key': [x, y, z]} + assert diff_files('key', list_4) == {'key': [x, z]} + assert diff_files('key', list_5) == {'key': [y, z]} + assert diff_files('key', list_6) == {'key': [y, x]} @given(st.data()) @@ -50,12 +50,12 @@ def test_diff_values_float(data): list_5 = [y, z, y] list_6 = [y, x, y] - assert diff_values('key', list_1) is None - assert diff_values('key', list_2) == {'key': [x, y]} - assert diff_values('key', list_3) == {'key': [x, y, z]} - assert diff_values('key', list_4) == {'key': [x, z]} - assert diff_values('key', list_5) == {'key': [y, z]} - assert diff_values('key', list_6) == {'key': [y, x]} + assert diff_files('key', list_1) is None + assert diff_files('key', list_2) == {'key': [x, y]} + assert diff_files('key', list_3) == {'key': [x, y, z]} + assert diff_files('key', list_4) == {'key': [x, z]} + assert diff_files('key', list_5) == {'key': [y, z]} + assert diff_files('key', list_6) == {'key': [y, x]} @given(st.data()) @@ -67,9 +67,9 @@ def test_diff_values_mixed(data): list_2 = [type_none, type_none, type_none] list_3 = [type_float, type_none, type_int] - assert diff_values('key', list_1) == {'key': [type_float, type_int]} - assert diff_values('key', list_2) is None - assert diff_values('key', list_3) == {'key': [type_float, type_none, type_int]} + assert diff_files('key', list_1) == {'key': [type_float, type_int]} + assert diff_files('key', list_2) is None + assert diff_files('key', list_3) == {'key': [type_float, type_none, type_int]} @given(st.data()) @@ -86,7 +86,7 @@ def test_diff_values_array(data): list_3 = [c, d] list_4 = [b, b, c] - assert diff_values('key', list_1) == {'key': [a, b]} - assert diff_values('key', list_2) == {'key': [a, b, c, d]} - assert diff_values('key', list_3) == {'key': [c, d]} - assert diff_values('key', list_1) == {'key': [b, c]} \ No newline at end of file + assert diff_files('key', list_1) == {'key': [a, b]} + assert diff_files('key', list_2) == {'key': [a, b, c, d]} + assert diff_files('key', list_3) == {'key': [c, d]} + assert diff_files('key', list_1) == {'key': [b, c]} \ No newline at end of file From 6d706f5edec1adc3eed3eed4c165c0196189be80 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Wed, 27 Dec 2017 15:43:49 +0800 Subject: [PATCH 023/142] bug fix --- nibabel/cmdline/diff.py | 52 +++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 93ed9c5e0f..85e87f077d 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -49,24 +49,39 @@ def get_opt_parser(): def diff_files(key, inputs): diffs = [] - for i in range(len(inputs)): - if i != len(inputs)-1: - temp_input_1 = inputs[i] - temp_input_2 = inputs[i+1] - if np.any(temp_input_1[key] != temp_input_2[key]): - if i != 0 and temp_input_2 != diffs: - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) + if len(inputs) > 2: + for i in range(len(inputs)): + if i != len(inputs)-1: + temp_input_1 = inputs[i] + temp_input_2 = inputs[i+1] + if np.any(temp_input_1[key] != temp_input_2[key]): + if i != 0 and temp_input_2 not in diffs: + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + else: + diffs.append(temp_input_1[key]) + elif type(temp_input_1[key]) != type(temp_input_2[key]): + if i != 0 and temp_input_2 not in diffs: + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + else: + diffs.append(temp_input_1[key]) else: - diffs.append(temp_input_1[key]) - elif type(temp_input_1[key]) != type(temp_input_2[key]): - if i != 0 and temp_input_2 != diffs: - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) - else: - diffs.append(temp_input_1[key]) - else: - pass + pass + + + else: + temp_input_1 = inputs[0] + temp_input_2 = inputs[1] + + if np.any(temp_input_1[key] != temp_input_2[key]): + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + elif type(temp_input_1[key]) != type(temp_input_2[key]): + diffs.append(temp_input_1[key]) + diffs.append(temp_input_2[key]) + else: + pass # TODO: figure out a way to not have these erroneous outputs occur in the above loop for a in range(len(diffs)-1): @@ -76,9 +91,6 @@ def diff_files(key, inputs): del diffs[a] except TypeError: pass - if a and b != len(diffs)-1: - if np.any(diffs[a] == diffs[b]): - del diffs[a] if len(diffs) > 1: return {key: diffs} From 774ce3b8d5393dd0568018071001dd67891bc6ea Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 5 Jan 2018 17:05:57 -0500 Subject: [PATCH 024/142] cosmetic tweaks --- nibabel/cmdline/diff.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 85e87f077d..0dd8b79e20 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -46,7 +46,8 @@ def get_opt_parser(): return p -def diff_files(key, inputs): +def diff_header_fields(key, inputs): + """Iterates over a single header field of multiple files""" diffs = [] if len(inputs) > 2: @@ -96,12 +97,9 @@ def diff_files(key, inputs): return {key: diffs} -def process_files(files, opts): +def get_headers_diff(files, opts): - header_list = [] - - for f in files: - header_list.append(nib.load(f).header) + header_list = [nib.load(f).header for f in files] if opts.header_fields: # signals "all fields" @@ -112,8 +110,8 @@ def process_files(files, opts): header_fields = opts.header_fields.split(',') for f in header_fields: - if diff_files(f, header_list) is not None: - print(diff_files(f, header_list)) + if diff_header_fields(f, header_list) is not None: + print(diff_header_fields(f, header_list)) def main(): @@ -130,4 +128,4 @@ def main(): # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 - process_files(files, opts) + get_headers_diff(files, opts) From 911d7812474f08509936b7f45551d93780575345 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sun, 7 Jan 2018 15:49:19 -0500 Subject: [PATCH 025/142] cleaned up code --- nibabel/cmdline/diff.py | 54 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 0dd8b79e20..d1cd34c12e 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -51,38 +51,36 @@ def diff_header_fields(key, inputs): diffs = [] if len(inputs) > 2: - for i in range(len(inputs)): - if i != len(inputs)-1: - temp_input_1 = inputs[i] - temp_input_2 = inputs[i+1] - if np.any(temp_input_1[key] != temp_input_2[key]): - if i != 0 and temp_input_2 not in diffs: - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) + for input_1, input_2 in zip(inputs, inputs[1:]): + key_1 = input_1[key] + key_2 = input_2[key] + + if type(key_1) != type(key_2): + if input_1 != inputs[0] and input_2 not in diffs: + diffs.append(key_1) + diffs.append(key_2) else: - diffs.append(temp_input_1[key]) - elif type(temp_input_1[key]) != type(temp_input_2[key]): - if i != 0 and temp_input_2 not in diffs: - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) + diffs.append(key_1) + elif np.any(key_1 != key_2): + if input_1 != inputs[0] and input_2 not in diffs: + diffs.append(key_1) + diffs.append(key_2) else: - diffs.append(temp_input_1[key]) - else: - pass - + diffs.append(key_1) else: - temp_input_1 = inputs[0] - temp_input_2 = inputs[1] - - if np.any(temp_input_1[key] != temp_input_2[key]): - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) - elif type(temp_input_1[key]) != type(temp_input_2[key]): - diffs.append(temp_input_1[key]) - diffs.append(temp_input_2[key]) - else: - pass + input_1 = inputs[0] + input_2 = inputs[1] + + key_1 = input_1[key] + key_2 = input_2[key] + + if np.any(key_1 != key_2): + diffs.append(key_1) + diffs.append(key_2) + elif type(key_1) != type(key_2): + diffs.append(key_1) + diffs.append(key_2) # TODO: figure out a way to not have these erroneous outputs occur in the above loop for a in range(len(diffs)-1): From 0458694fc82778e55663279cefe20a3b7bdc1e14 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 13 Jan 2018 17:19:39 -0500 Subject: [PATCH 026/142] promoted generic programming and got test to work again --- nibabel/cmdline/diff.py | 17 +++++++----- nibabel/tests/test_diff.py | 56 ++++++++++---------------------------- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index d1cd34c12e..0535b1d769 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -46,6 +46,15 @@ def get_opt_parser(): return p +def diff_values(compare1, compare2): + if np.any(compare1 != compare2): + return compare1, compare2 + elif type(compare1) != type(compare2): + return compare1, compare2 + else: + pass + + def diff_header_fields(key, inputs): """Iterates over a single header field of multiple files""" diffs = [] @@ -55,13 +64,7 @@ def diff_header_fields(key, inputs): key_1 = input_1[key] key_2 = input_2[key] - if type(key_1) != type(key_2): - if input_1 != inputs[0] and input_2 not in diffs: - diffs.append(key_1) - diffs.append(key_2) - else: - diffs.append(key_1) - elif np.any(key_1 != key_2): + if diff_values(key_1, key_2): if input_1 != inputs[0] and input_2 not in diffs: diffs.append(key_1) diffs.append(key_2) diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index e05eb7dfcb..225c17c17e 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -14,7 +14,7 @@ DATA_PATH = abspath(pjoin(dirname(__file__), 'data')) -from nibabel.cmdline.diff import diff_files +from nibabel.cmdline.diff import diff_values # TODO: MAJOR TO DO IS TO FIGURE OUT HOW TO USE HYPOTHESIS FOR LONGER LIST LENGTHS WHILE STILL CONTROLLING FOR OUTCOMES @@ -23,19 +23,11 @@ def test_diff_values_int(data): x = data.draw(st.integers(), label='x') y = data.draw(st.integers(min_value = x + 1), label='x+1') z = data.draw(st.integers(max_value = x - 1), label='x-1') - list_1 = [x, x, x] - list_2 = [x, y, x] - list_3 = [x, y, z] - list_4 = [x, z, x] - list_5 = [y, z, y] - list_6 = [y, x, y] - assert diff_files('key', list_1) is None - assert diff_files('key', list_2) == {'key': [x, y]} - assert diff_files('key', list_3) == {'key': [x, y, z]} - assert diff_files('key', list_4) == {'key': [x, z]} - assert diff_files('key', list_5) == {'key': [y, z]} - assert diff_files('key', list_6) == {'key': [y, x]} + assert diff_values(x, x) is None + assert diff_values(x, y) == (x, y) + assert diff_values(x, z) == (x, z) + assert diff_values(y, z) == (y, z) @given(st.data()) @@ -43,19 +35,11 @@ def test_diff_values_float(data): x = data.draw(st.just(0), label='x') y = data.draw(st.floats(min_value = 1e8), label='y') z = data.draw(st.floats(max_value = -1e8), label='z') - list_1 = [x, x, x] - list_2 = [x, y, x] - list_3 = [x, y, z] - list_4 = [x, z, x] - list_5 = [y, z, y] - list_6 = [y, x, y] - assert diff_files('key', list_1) is None - assert diff_files('key', list_2) == {'key': [x, y]} - assert diff_files('key', list_3) == {'key': [x, y, z]} - assert diff_files('key', list_4) == {'key': [x, z]} - assert diff_files('key', list_5) == {'key': [y, z]} - assert diff_files('key', list_6) == {'key': [y, x]} + assert diff_values(x, x) is None + assert diff_values(x, y) == (x, y) + assert diff_values(x, z) == (x, z) + assert diff_values(y, z) == (y, z) @given(st.data()) @@ -63,13 +47,11 @@ def test_diff_values_mixed(data): type_float = data.draw(st.floats(), label='float') type_int = data.draw(st.integers(), label='int') type_none = data.draw(st.none(), label='none') - list_1 = [type_float, type_int] - list_2 = [type_none, type_none, type_none] - list_3 = [type_float, type_none, type_int] - assert diff_files('key', list_1) == {'key': [type_float, type_int]} - assert diff_files('key', list_2) is None - assert diff_files('key', list_3) == {'key': [type_float, type_none, type_int]} + assert diff_values(type_float, type_int) == (type_float, type_int) + assert diff_values(type_float, type_none) == (type_float, type_none) + assert diff_values(type_int, type_none) == (type_int, type_none) + assert diff_values(type_none, type_none) is None @given(st.data()) @@ -79,14 +61,6 @@ def test_diff_values_array(data): c = data.draw(st.lists(elements=st.floats(min_value=1e8), min_size=1)) d = data.draw(st.lists(elements=st.floats(max_value=-1e8), min_size=1)) # TODO: Figure out a way to include 0 in lists (arrays) - # TODO: Figure out a way to test for NaN - list_1 = [a, a, b] - list_2 = [a, b, c, d] - list_3 = [c, d] - list_4 = [b, b, c] - - assert diff_files('key', list_1) == {'key': [a, b]} - assert diff_files('key', list_2) == {'key': [a, b, c, d]} - assert diff_files('key', list_3) == {'key': [c, d]} - assert diff_files('key', list_1) == {'key': [b, c]} \ No newline at end of file + assert diff_values(a, b) == (a, b) + assert diff_values(c, d) == (c, d) \ No newline at end of file From fed70e916a4faaa34be585ff72401c183d369f2d Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 13 Jan 2018 17:42:49 -0500 Subject: [PATCH 027/142] tried to clean code but couldnt get comprehension going --- nibabel/cmdline/diff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 0535b1d769..10e8118c0c 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -111,8 +111,10 @@ def get_headers_diff(files, opts): header_fields = opts.header_fields.split(',') for f in header_fields: - if diff_header_fields(f, header_list) is not None: - print(diff_header_fields(f, header_list)) + output = diff_header_fields(f, header_list) + + if output is not None: + print(output) def main(): From 0b59dfbf6fb8818ebc2dcedc116528e4daf4404a Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Mon, 15 Jan 2018 18:29:30 -0500 Subject: [PATCH 028/142] comment and docstring --- nibabel/cmdline/diff.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 10e8118c0c..a3a3b0767f 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -47,6 +47,7 @@ def get_opt_parser(): def diff_values(compare1, compare2): + """Generically compares two values and, if different, returns both as a tuple""" if np.any(compare1 != compare2): return compare1, compare2 elif type(compare1) != type(compare2): @@ -59,6 +60,7 @@ def diff_header_fields(key, inputs): """Iterates over a single header field of multiple files""" diffs = [] + # for comparing more than 2 files at once if len(inputs) > 2: for input_1, input_2 in zip(inputs, inputs[1:]): key_1 = input_1[key] @@ -71,6 +73,7 @@ def diff_header_fields(key, inputs): else: diffs.append(key_1) + # for comparing 2 files else: input_1 = inputs[0] input_2 = inputs[1] @@ -85,6 +88,7 @@ def diff_header_fields(key, inputs): diffs.append(key_1) diffs.append(key_2) + # fixing some erroneous outputs generated by the above # TODO: figure out a way to not have these erroneous outputs occur in the above loop for a in range(len(diffs)-1): for b in range(len(diffs)-1): From 2920abf8b64e6432d61928799873e6e2589847e7 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sun, 21 Jan 2018 15:15:12 -0500 Subject: [PATCH 029/142] added options for text, json, yaml but still have to implement --- nibabel/cmdline/diff.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index a3a3b0767f..715ef81cbe 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -41,6 +41,18 @@ def get_opt_parser(): Option("-H", "--header-fields", dest="header_fields", default='all', help="Header fields (comma separated) to be printed as well (if present)"), + + Option("-t", "--text", + dest="text", + help="Print output in a very nice-looking way"), + + Option("-j", "--json", + dest="json", + help="Print output in a json way"), + + Option("-y", "--yaml", + dest="yaml", + help="Print output in a yaml way"), ]) return p From 1e57409a1e8787c4c597616778d06f710e499ff3 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 3 Feb 2018 16:44:31 -0500 Subject: [PATCH 030/142] work in progress with all outputs --- nibabel/cmdline/diff.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 715ef81cbe..bf43908e71 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -14,9 +14,12 @@ import sys from optparse import OptionParser, Option +from six import binary_type import numpy as np +import json_tricks + import nibabel as nib import nibabel.cmdline.utils from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get @@ -74,16 +77,18 @@ def diff_header_fields(key, inputs): # for comparing more than 2 files at once if len(inputs) > 2: - for input_1, input_2 in zip(inputs, inputs[1:]): - key_1 = input_1[key] + input_1 = inputs[0] + key_1 = input_1[key] + for input_2 in inputs[1:]: key_2 = input_2[key] if diff_values(key_1, key_2): - if input_1 != inputs[0] and input_2 not in diffs: - diffs.append(key_1) - diffs.append(key_2) - else: + if key_1 not in diffs: diffs.append(key_1) + diffs.append(key_2) + + elif len(diffs) != 0: + diffs.append(key_2) # for comparing 2 files else: @@ -111,7 +116,7 @@ def diff_header_fields(key, inputs): pass if len(diffs) > 1: - return {key: diffs} + return diffs def get_headers_diff(files, opts): @@ -126,11 +131,15 @@ def get_headers_diff(files, opts): else: header_fields = opts.header_fields.split(',') + output = {} + for f in header_fields: - output = diff_header_fields(f, header_list) + val = diff_header_fields(f, header_list) + + if val is not None: + output[f] = val - if output is not None: - print(output) + return output def main(): @@ -147,4 +156,13 @@ def main(): # suppress nibabel format-compliance warnings nib.imageglobals.logger.level = 50 - get_headers_diff(files, opts) + diff = get_headers_diff(files, opts) + + if opts.text: + print(diff) + + if opts.json: + if isinstance(diff, binary_type): + print(json_tricks.dumps(diff.decode("utf-8"))) + else: + print(json_tricks.dumps(diff)) From fd6c47465afef6ee4c24f2a42f0172d7f7cd7e5c Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Wed, 14 Mar 2018 15:10:07 -0400 Subject: [PATCH 031/142] simplified code but now bizarrely doesnt run with two files --- nibabel/cmdline/diff.py | 73 +++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 47 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index bf43908e71..9a4e385961 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -19,6 +19,7 @@ import numpy as np import json_tricks +import yaml import nibabel as nib import nibabel.cmdline.utils @@ -62,61 +63,39 @@ def get_opt_parser(): def diff_values(compare1, compare2): - """Generically compares two values and, if different, returns both as a tuple""" + """Generically compares two values, returns true if different""" if np.any(compare1 != compare2): - return compare1, compare2 + return True elif type(compare1) != type(compare2): - return compare1, compare2 + return True else: - pass + return False def diff_header_fields(key, inputs): """Iterates over a single header field of multiple files""" - diffs = [] - # for comparing more than 2 files at once - if len(inputs) > 2: - input_1 = inputs[0] - key_1 = input_1[key] - for input_2 in inputs[1:]: - key_2 = input_2[key] + keyed_inputs = [] - if diff_values(key_1, key_2): - if key_1 not in diffs: - diffs.append(key_1) - diffs.append(key_2) + for i in inputs: + if isinstance(i[key], binary_type): + i[key] = i[key].decode() - elif len(diffs) != 0: - diffs.append(key_2) + try: + if np.all(np.isnan(i[key])): + continue + except TypeError: + pass - # for comparing 2 files - else: - input_1 = inputs[0] - input_2 = inputs[1] - - key_1 = input_1[key] - key_2 = input_2[key] + keyed_inputs.append(i[key]) - if np.any(key_1 != key_2): - diffs.append(key_1) - diffs.append(key_2) - elif type(key_1) != type(key_2): - diffs.append(key_1) - diffs.append(key_2) + if keyed_inputs: - # fixing some erroneous outputs generated by the above - # TODO: figure out a way to not have these erroneous outputs occur in the above loop - for a in range(len(diffs)-1): - for b in range(len(diffs)-1): - try: - if np.all(np.isnan(diffs[a])) and np.all(np.isnan(diffs[a+1])): - del diffs[a] - except TypeError: - pass + comparison_input = keyed_inputs[0] - if len(diffs) > 1: - return diffs + for i in keyed_inputs[1:]: + if diff_values(comparison_input, i): + return keyed_inputs def get_headers_diff(files, opts): @@ -136,7 +115,7 @@ def get_headers_diff(files, opts): for f in header_fields: val = diff_header_fields(f, header_list) - if val is not None: + if val: output[f] = val return output @@ -161,8 +140,8 @@ def main(): if opts.text: print(diff) - if opts.json: - if isinstance(diff, binary_type): - print(json_tricks.dumps(diff.decode("utf-8"))) - else: - print(json_tricks.dumps(diff)) + elif opts.json: + print(json_tricks.dumps(diff)) + + elif opts.yaml: + print(yaml.dump(diff)) From 497ad2aed8541a91c5d7c1dadcd0175c1393ac1d Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 15 Mar 2018 23:20:28 -0400 Subject: [PATCH 032/142] beautified text output, next up json and yaml --- nibabel/cmdline/diff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 9a4e385961..c812e78d23 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -138,7 +138,8 @@ def main(): diff = get_headers_diff(files, opts) if opts.text: - print(diff) + for x in diff: + print(x, diff[x]) elif opts.json: print(json_tricks.dumps(diff)) From df0aa7972168715c72fb204bbef6df40d7f380c2 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Fri, 16 Mar 2018 21:55:47 -0400 Subject: [PATCH 033/142] modified the tests for the new diff_values --- nibabel/tests/test_diff.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/nibabel/tests/test_diff.py b/nibabel/tests/test_diff.py index 225c17c17e..5ac422e956 100644 --- a/nibabel/tests/test_diff.py +++ b/nibabel/tests/test_diff.py @@ -24,10 +24,10 @@ def test_diff_values_int(data): y = data.draw(st.integers(min_value = x + 1), label='x+1') z = data.draw(st.integers(max_value = x - 1), label='x-1') - assert diff_values(x, x) is None - assert diff_values(x, y) == (x, y) - assert diff_values(x, z) == (x, z) - assert diff_values(y, z) == (y, z) + assert diff_values(x, x) is False + assert diff_values(x, y) == True + assert diff_values(x, z) == True + assert diff_values(y, z) == True @given(st.data()) @@ -36,10 +36,10 @@ def test_diff_values_float(data): y = data.draw(st.floats(min_value = 1e8), label='y') z = data.draw(st.floats(max_value = -1e8), label='z') - assert diff_values(x, x) is None - assert diff_values(x, y) == (x, y) - assert diff_values(x, z) == (x, z) - assert diff_values(y, z) == (y, z) + assert diff_values(x, x) is False + assert diff_values(x, y) == True + assert diff_values(x, z) == True + assert diff_values(y, z) == True @given(st.data()) @@ -48,10 +48,10 @@ def test_diff_values_mixed(data): type_int = data.draw(st.integers(), label='int') type_none = data.draw(st.none(), label='none') - assert diff_values(type_float, type_int) == (type_float, type_int) - assert diff_values(type_float, type_none) == (type_float, type_none) - assert diff_values(type_int, type_none) == (type_int, type_none) - assert diff_values(type_none, type_none) is None + assert diff_values(type_float, type_int) == True + assert diff_values(type_float, type_none) == True + assert diff_values(type_int, type_none) == True + assert diff_values(type_none, type_none) is False @given(st.data()) @@ -62,5 +62,6 @@ def test_diff_values_array(data): d = data.draw(st.lists(elements=st.floats(max_value=-1e8), min_size=1)) # TODO: Figure out a way to include 0 in lists (arrays) - assert diff_values(a, b) == (a, b) - assert diff_values(c, d) == (c, d) \ No newline at end of file + assert diff_values(a, b) == True + assert diff_values(c, d) == True + assert diff_values(a, a) == False From c23143c8d07e5d168a3c4bfd5ae33fdf1cdf6a90 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Sat, 17 Mar 2018 22:35:21 -0400 Subject: [PATCH 034/142] commenting out json and yaml, cosmetic mod to ls.py --- nibabel/cmdline/diff.py | 21 ++++++++++----------- nibabel/cmdline/ls.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index c812e78d23..6a075e0112 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -77,9 +77,9 @@ def diff_header_fields(key, inputs): keyed_inputs = [] - for i in inputs: - if isinstance(i[key], binary_type): - i[key] = i[key].decode() + for i in inputs: # stores each file's respective header files + if isinstance(i[key], (bytes, bytearray)): + i[key] = str(i[key][2:]) try: if np.all(np.isnan(i[key])): @@ -89,8 +89,7 @@ def diff_header_fields(key, inputs): keyed_inputs.append(i[key]) - if keyed_inputs: - + if keyed_inputs: # sometimes keyed_inputs is empty lol comparison_input = keyed_inputs[0] for i in keyed_inputs[1:]: @@ -139,10 +138,10 @@ def main(): if opts.text: for x in diff: - print(x, diff[x]) - - elif opts.json: - print(json_tricks.dumps(diff)) + print(x+":", diff[x]) - elif opts.yaml: - print(yaml.dump(diff)) + # elif opts.json: + # print(json_tricks.dumps(diff, conv_str_byte=True)) + # + # elif opts.yaml: + # print(yaml.dump(diff)) diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py index b302b67e5b..98f75e21dc 100755 --- a/nibabel/cmdline/ls.py +++ b/nibabel/cmdline/ls.py @@ -167,4 +167,4 @@ def main(): rows = [proc_file(f, opts) for f in files] - print(table2string(rows)) \ No newline at end of file + print(table2string(rows)) From db16d8520935a57c91fe1fec3ddc73311f9c6575 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 26 Mar 2018 14:27:36 -0400 Subject: [PATCH 035/142] BF: all those flags are just boolean flags and no need to repeat their identical names in dest --- nibabel/cmdline/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 6a075e0112..6ddbf7176d 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -47,15 +47,15 @@ def get_opt_parser(): help="Header fields (comma separated) to be printed as well (if present)"), Option("-t", "--text", - dest="text", + action='store_true', help="Print output in a very nice-looking way"), Option("-j", "--json", - dest="json", + action='store_true', help="Print output in a json way"), Option("-y", "--yaml", - dest="yaml", + action='store_true', help="Print output in a yaml way"), ]) From feca4390bd62d75fe55f9ef4c3564886b2892437 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 29 Mar 2018 12:07:55 -0400 Subject: [PATCH 036/142] progress towards table formatting --- nibabel/cmdline/diff.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 6ddbf7176d..fa997e555a 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -78,8 +78,6 @@ def diff_header_fields(key, inputs): keyed_inputs = [] for i in inputs: # stores each file's respective header files - if isinstance(i[key], (bytes, bytearray)): - i[key] = str(i[key][2:]) try: if np.all(np.isnan(i[key])): @@ -87,7 +85,12 @@ def diff_header_fields(key, inputs): except TypeError: pass - keyed_inputs.append(i[key]) + if i[key].ndim < 1: + keyed_inputs.append("{}@{}".format(i[key], i[key].dtype)) + elif i[key].ndim == 1: + keyed_inputs.append("{}@{}".format(list(i[key]), i[key].dtype)) + else: + pass if keyed_inputs: # sometimes keyed_inputs is empty lol comparison_input = keyed_inputs[0] @@ -137,8 +140,9 @@ def main(): diff = get_headers_diff(files, opts) if opts.text: + print("{:<10} {:<40}".format('Field', ' '.join(files))) for x in diff: - print(x+":", diff[x]) + print("{:<10} {:<40}".format(x, ' '.join(str(e) for e in diff[x]))) # elif opts.json: # print(json_tricks.dumps(diff, conv_str_byte=True)) From acf667bf2e4161fbcfc09d8706c919a76dc28006 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 29 Mar 2018 14:06:21 -0400 Subject: [PATCH 037/142] table now works and looks p good --- nibabel/cmdline/diff.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index fa997e555a..ad4bd2ffcd 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -140,9 +140,19 @@ def main(): diff = get_headers_diff(files, opts) if opts.text: - print("{:<10} {:<40}".format('Field', ' '.join(files))) + + print("{:<11}".format('Field'), end="", flush=True) + + for f in files: + print("{:<40}".format(f), end="", flush=True) + print("\n") + for x in diff: - print("{:<10} {:<40}".format(x, ' '.join(str(e) for e in diff[x]))) + print("{:<11}".format(x), end="", flush=True) + + for e in diff[x]: + print("{:<40}".format(e), end="", flush=True) + print("\n") # elif opts.json: # print(json_tricks.dumps(diff, conv_str_byte=True)) From bb3fbf0d4ce634bc58fcbf2f77becff9f2efd328 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 29 Mar 2018 14:33:43 -0400 Subject: [PATCH 038/142] slight tweak to table formatting --- nibabel/cmdline/diff.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index ad4bd2ffcd..7a3a8f18ae 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -144,14 +144,15 @@ def main(): print("{:<11}".format('Field'), end="", flush=True) for f in files: - print("{:<40}".format(f), end="", flush=True) + print("{:<45}".format(f), end="", flush=True) print("\n") for x in diff: print("{:<11}".format(x), end="", flush=True) for e in diff[x]: - print("{:<40}".format(e), end="", flush=True) + print("{:<45}".format(e), end="", flush=True) + print("\n") # elif opts.json: From 8a920101a84bf4763e6ee55f547fa7bd55a0801c Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Wed, 4 Apr 2018 19:45:07 -0400 Subject: [PATCH 039/142] very inefficient but successfully removed dtype when its identical --- nibabel/cmdline/diff.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 7a3a8f18ae..7fdc8d8b5f 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -78,19 +78,30 @@ def diff_header_fields(key, inputs): keyed_inputs = [] for i in inputs: # stores each file's respective header files + field_value = i[key] try: - if np.all(np.isnan(i[key])): + if np.all(np.isnan(field_value)): continue except TypeError: pass - if i[key].ndim < 1: - keyed_inputs.append("{}@{}".format(i[key], i[key].dtype)) - elif i[key].ndim == 1: - keyed_inputs.append("{}@{}".format(list(i[key]), i[key].dtype)) + for x in inputs[1:]: + data_diff = diff_values(str(x[key].dtype), str(field_value.dtype)) + + if data_diff: + break + + if data_diff: + if field_value.ndim < 1: + keyed_inputs.append("{}@{}".format(field_value, field_value.dtype)) + elif field_value.ndim == 1: + keyed_inputs.append("{}@{}".format(list(field_value), field_value.dtype)) else: - pass + if field_value.ndim < 1: + keyed_inputs.append("{}".format(field_value)) + elif field_value.ndim == 1: + keyed_inputs.append("{}".format(list(field_value))) if keyed_inputs: # sometimes keyed_inputs is empty lol comparison_input = keyed_inputs[0] @@ -139,8 +150,7 @@ def main(): diff = get_headers_diff(files, opts) - if opts.text: - + if opts.text: # using string formatting to print a table of the results print("{:<11}".format('Field'), end="", flush=True) for f in files: From a9a572aa3a50947e9ac7d7a3dbf56564881babaf Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 5 Apr 2018 10:45:34 -0400 Subject: [PATCH 040/142] removed extra blank line lol --- nibabel/cmdline/diff.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index 7fdc8d8b5f..d44ed03066 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -151,19 +151,20 @@ def main(): diff = get_headers_diff(files, opts) if opts.text: # using string formatting to print a table of the results - print("{:<11}".format('Field'), end="", flush=True) + print("{:<11}".format('Field'), end="") for f in files: - print("{:<45}".format(f), end="", flush=True) - print("\n") + print("{:<45}".format(f), end="") + print() for x in diff: - print("{:<11}".format(x), end="", flush=True) + print("{:<11}".format(x), end="") for e in diff[x]: - print("{:<45}".format(e), end="", flush=True) + print("{:<45}".format(e), end="") - print("\n") + print() + raise SystemExit(1) # elif opts.json: # print(json_tricks.dumps(diff, conv_str_byte=True)) From 3290a66cefe48721865f10456a8e5b0017860092 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 5 Apr 2018 10:59:38 -0400 Subject: [PATCH 041/142] boosting coverage --- nibabel/cmdline/ls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py index 98f75e21dc..f599b576c5 100755 --- a/nibabel/cmdline/ls.py +++ b/nibabel/cmdline/ls.py @@ -153,11 +153,11 @@ def proc_file(f, opts): return row -def main(): +def main(args=None): """Show must go on""" parser = get_opt_parser() - (opts, files) = parser.parse_args() + (opts, files) = parser.parse_args(args=args) nibabel.cmdline.utils.verbose_level = opts.verbose From 06e8dd76d3d3762ed335c6acd11d9e43a4fd614a Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 12 Apr 2018 11:56:20 -0400 Subject: [PATCH 042/142] data comparison function added --- nibabel/cmdline/diff.py | 75 +++++++++++++++++++---------------------- nibabel/cmdline/ls.py | 5 --- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index d44ed03066..cfcafa8427 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -14,21 +14,13 @@ import sys from optparse import OptionParser, Option -from six import binary_type import numpy as np -import json_tricks -import yaml - import nibabel as nib import nibabel.cmdline.utils -from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get -import fileinput - -__author__ = 'Yaroslav Halchenko & Christopher Cheng' -__copyright__ = 'Copyright (c) 2017 NiBabel contributors' -__license__ = 'MIT' +import itertools +import hashlib def get_opt_parser(): @@ -45,18 +37,6 @@ def get_opt_parser(): Option("-H", "--header-fields", dest="header_fields", default='all', help="Header fields (comma separated) to be printed as well (if present)"), - - Option("-t", "--text", - action='store_true', - help="Print output in a very nice-looking way"), - - Option("-j", "--json", - action='store_true', - help="Print output in a json way"), - - Option("-y", "--yaml", - action='store_true', - help="Print output in a yaml way"), ]) return p @@ -68,6 +48,8 @@ def diff_values(compare1, compare2): return True elif type(compare1) != type(compare2): return True + elif compare1 != compare2: + return True else: return False @@ -80,19 +62,19 @@ def diff_header_fields(key, inputs): for i in inputs: # stores each file's respective header files field_value = i[key] - try: + try: # filter numpy arrays if np.all(np.isnan(field_value)): continue except TypeError: pass - for x in inputs[1:]: + for x in inputs[1:]: # compare different values, print all as soon as diff is found data_diff = diff_values(str(x[key].dtype), str(field_value.dtype)) if data_diff: break - if data_diff: + if data_diff: # prints data types if they're different and not if they're not if field_value.ndim < 1: keyed_inputs.append("{}@{}".format(field_value, field_value.dtype)) elif field_value.ndim == 1: @@ -134,6 +116,15 @@ def get_headers_diff(files, opts): return output +def get_data_diff(files): + + data_list = [nib.load(f).get_data() for f in files] + + for a, b in itertools.combinations(data_list, 2): + return diff_values(hashlib.md5(repr(a).encode('utf-8')).hexdigest(), hashlib.md5( + repr(b).encode('utf-8')).hexdigest()) + + def main(): """NO DAYS OFF""" @@ -149,25 +140,27 @@ def main(): nib.imageglobals.logger.level = 50 diff = get_headers_diff(files, opts) + diff2 = get_data_diff(files) - if opts.text: # using string formatting to print a table of the results - print("{:<11}".format('Field'), end="") + print("{:<11}".format('Field'), end="") - for f in files: - print("{:<45}".format(f), end="") - print() + for f in files: + print("{:<45}".format(f), end="") + print() - for x in diff: - print("{:<11}".format(x), end="") + for x in diff: + print("{:<11}".format(x), end="") - for e in diff[x]: - print("{:<45}".format(e), end="") + for e in diff[x]: + print("{:<45}".format(e), end="") - print() - raise SystemExit(1) + print() + + print("DATA: ", end="") + + if diff2: + print("These files are different.") + else: + print("These files are identical!") - # elif opts.json: - # print(json_tricks.dumps(diff, conv_str_byte=True)) - # - # elif opts.yaml: - # print(yaml.dump(diff)) + raise SystemExit(1) diff --git a/nibabel/cmdline/ls.py b/nibabel/cmdline/ls.py index f599b576c5..4e5174ff4f 100755 --- a/nibabel/cmdline/ls.py +++ b/nibabel/cmdline/ls.py @@ -21,11 +21,6 @@ import nibabel.cmdline.utils from nibabel.cmdline.utils import _err, verbose, table2string, ap, safe_get -__author__ = 'Yaroslav Halchenko' -__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \ - 'and NiBabel contributors' -__license__ = 'MIT' - MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts From 8fd6995c4ef7c91e099cacba05f6ce4919d4a4c4 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Tue, 24 Apr 2018 16:00:25 -0400 Subject: [PATCH 043/142] implemented test-nib-diff --- nibabel/cmdline/diff.py | 46 +++++++++++++++++++++++------------ nibabel/tests/test_scripts.py | 12 ++++++++- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/nibabel/cmdline/diff.py b/nibabel/cmdline/diff.py index cfcafa8427..2881a7786d 100755 --- a/nibabel/cmdline/diff.py +++ b/nibabel/cmdline/diff.py @@ -60,7 +60,10 @@ def diff_header_fields(key, inputs): keyed_inputs = [] for i in inputs: # stores each file's respective header files - field_value = i[key] + try: + field_value = i[key] + except ValueError: + continue try: # filter numpy arrays if np.all(np.isnan(field_value)): @@ -69,21 +72,27 @@ def diff_header_fields(key, inputs): pass for x in inputs[1:]: # compare different values, print all as soon as diff is found - data_diff = diff_values(str(x[key].dtype), str(field_value.dtype)) + try: + data_diff = diff_values(str(x[key].dtype), str(field_value.dtype)) - if data_diff: - break + if data_diff: + break + except ValueError: + continue - if data_diff: # prints data types if they're different and not if they're not - if field_value.ndim < 1: - keyed_inputs.append("{}@{}".format(field_value, field_value.dtype)) - elif field_value.ndim == 1: - keyed_inputs.append("{}@{}".format(list(field_value), field_value.dtype)) - else: - if field_value.ndim < 1: - keyed_inputs.append("{}".format(field_value)) - elif field_value.ndim == 1: - keyed_inputs.append("{}".format(list(field_value))) + try: + if data_diff: # prints data types if they're different and not if they're not + if field_value.ndim < 1: + keyed_inputs.append("{}@{}".format(field_value, field_value.dtype)) + elif field_value.ndim == 1: + keyed_inputs.append("{}@{}".format(list(field_value), field_value.dtype)) + else: + if field_value.ndim < 1: + keyed_inputs.append("{}".format(field_value)) + elif field_value.ndim == 1: + keyed_inputs.append("{}".format(list(field_value))) + except UnboundLocalError: + continue if keyed_inputs: # sometimes keyed_inputs is empty lol comparison_input = keyed_inputs[0] @@ -119,10 +128,15 @@ def get_headers_diff(files, opts): def get_data_diff(files): data_list = [nib.load(f).get_data() for f in files] + temp_bool = False for a, b in itertools.combinations(data_list, 2): - return diff_values(hashlib.md5(repr(a).encode('utf-8')).hexdigest(), hashlib.md5( - repr(b).encode('utf-8')).hexdigest()) + if diff_values(hashlib.md5(repr(a).encode('utf-8')).hexdigest(), hashlib.md5( + repr(b).encode('utf-8')).hexdigest()): + temp_bool = True + break + + return temp_bool def main(): diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index 18e1a92e94..42fcd4fff8 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -65,6 +65,16 @@ def check_nib_ls_example4d(opts=[], hdrs_str="", other_str=""): assert_equal(fname, stdout[:len(fname)]) assert_re_in(expected_re, stdout[len(fname):]) + +def check_nib_diff_examples(opts=[], hdrs_str="", other_str=""): + # test nib-diff script + fnames = ['spmT_0001.nii.gz', 'spmT_0001_2.nii.gz'] + code, stdout, stderr = run_command(['nib-diff'] + fnames, check_code=False) + assert_equal(stdout, "Field spmT_0001.nii.gz spmT_0001_2.nii.gz " + " " + "\n" + "descrip b'SPM{T_[1066.0]} - contrast 1: AA-GG' b'SPM{T_[1092.0]}" + " - contrast 1: gg-tt' " + "\n" + "DATA: These files are identical!") + + @script_test def test_nib_ls(): yield check_nib_ls_example4d @@ -130,7 +140,7 @@ def test_nib_ls_multiple(): @script_test def test_nib_diff(): - raise NotImplementedError("TODO") + yield check_nib_diff_examples @script_test From df8bc04323ffef05f2ca1cd8b5b93f5570641e96 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Tue, 24 Apr 2018 22:02:36 -0400 Subject: [PATCH 044/142] added another test --- nibabel/tests/test_scripts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index 42fcd4fff8..5f56281b1f 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -69,11 +69,17 @@ def check_nib_ls_example4d(opts=[], hdrs_str="", other_str=""): def check_nib_diff_examples(opts=[], hdrs_str="", other_str=""): # test nib-diff script fnames = ['spmT_0001.nii.gz', 'spmT_0001_2.nii.gz'] + fnames2 = ['spmT_0001.nii.gz', 'spmT_0001.nii.gz'] code, stdout, stderr = run_command(['nib-diff'] + fnames, check_code=False) assert_equal(stdout, "Field spmT_0001.nii.gz spmT_0001_2.nii.gz " " " + "\n" + "descrip b'SPM{T_[1066.0]} - contrast 1: AA-GG' b'SPM{T_[1092.0]}" " - contrast 1: gg-tt' " + "\n" + "DATA: These files are identical!") + code, stdout, stderr = run_command(['nib-diff'] + fnames2, check_code=False) + assert_equal(stdout, "Field spmT_0001.nii.gz spmT_0001.nii.gz " + " " + "\n" + "DATA: These files are identical!") + + @script_test def test_nib_ls(): From 45d3fbf39382a6a554e1b6b9dfebf39c11283f69 Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Tue, 24 Apr 2018 22:39:57 -0400 Subject: [PATCH 045/142] changed travis appveyor utils per test errors --- .travis.yml | 4 ++-- appveyor.yml | 1 + nibabel/cmdline/utils.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28ac4fa5f4..f2d22fb02a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ cache: - $HOME/.cache/pip env: global: - - DEPENDS="six numpy scipy matplotlib h5py pillow pydicom" + - DEPENDS="six numpy scipy matplotlib h5py pillow pydicom hypothesis" - OPTIONAL_DEPENDS="" - INSTALL_TYPE="setup" - EXTRA_WHEELS="https://5cf40426d9f06eb7461d-6fe47d9331aba7cd62fc36c7196769e4.ssl.cf2.rackcdn.com" @@ -95,7 +95,7 @@ before_install: - source venv/bin/activate - python --version # just to check - pip install -U pip wheel # needed at one point - - retry pip install nose flake8 mock # always + - retry pip install nose flake8 mock hypothesis # always - pip install $EXTRA_PIP_FLAGS $DEPENDS $OPTIONAL_DEPENDS - if [ "${COVERAGE}" == "1" ]; then pip install coverage; diff --git a/appveyor.yml b/appveyor.yml index e41aee90c8..177eacc3be 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,6 +21,7 @@ install: # Install the dependencies of the project. - pip install numpy scipy matplotlib nose h5py mock + - pip install hypothesis - pip install pydicom - pip install . - SET NIBABEL_DATA_DIR=%CD%\nibabel-data diff --git a/nibabel/cmdline/utils.py b/nibabel/cmdline/utils.py index 697b122f58..b65db794b6 100644 --- a/nibabel/cmdline/utils.py +++ b/nibabel/cmdline/utils.py @@ -10,7 +10,6 @@ Helper utilities to be used in cmdline applications """ - # global verbosity switch import re from io import StringIO @@ -49,7 +48,7 @@ def table2string(table, out=None): Where to print. If None -- will print and return string Returns - ------- + ------- string if out was None """ From 1cbf5b3ab82a206031dba9a340c0990434b265db Mon Sep 17 00:00:00 2001 From: "Christopher P. Cheng" Date: Thu, 26 Apr 2018 10:36:02 -0400 Subject: [PATCH 046/142] test files --- nibabel/tests/spmT_0001_2.nii.gz | Bin 0 -> 1431006 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 nibabel/tests/spmT_0001_2.nii.gz diff --git a/nibabel/tests/spmT_0001_2.nii.gz b/nibabel/tests/spmT_0001_2.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..32771c00095c69b3fbd134b35c9357923b6cb26a GIT binary patch literal 1431006 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%0Abv^fNX^8alC6k2r@ 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