Skip to content

Commit 53a8606

Browse files
committed
Merge pull request #437 from yarikoptic/enh-ls-counts
MRG: nib-ls -c/--counts to report counts for each value (useful for ROI maps) Add option to output counts of unique values
2 parents 1a434a7 + 5d1b19a commit 53a8606

File tree

2 files changed

+103
-58
lines changed

2 files changed

+103
-58
lines changed

bin/nib-ls

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,34 @@ Output a summary table for neuroimaging files (resolution, dimensionality, etc.)
1212
"""
1313
from __future__ import division, print_function, absolute_import
1414

15-
__author__ = 'Yaroslav Halchenko'
16-
__copyright__ = 'Copyright (c) 2011-2015 Yaroslav Halchenko ' \
17-
'and NiBabel contributors'
18-
__license__ = 'MIT'
19-
2015
import re
2116
import sys
17+
18+
import numpy as np
19+
import nibabel as nib
20+
2221
from math import ceil
22+
from collections import defaultdict
2323
from optparse import OptionParser, Option
2424
from io import StringIO
25+
from nibabel.py3k import asunicode
26+
from nibabel.externals.six.moves import xrange
2527

26-
import numpy as np
28+
__author__ = 'Yaroslav Halchenko'
29+
__copyright__ = 'Copyright (c) 2011-2016 Yaroslav Halchenko ' \
30+
'and NiBabel contributors'
31+
__license__ = 'MIT'
2732

28-
import nibabel as nib
29-
from nibabel.py3k import asunicode
3033

3134
# global verbosity switch
3235
verbose_level = 0
36+
MAX_UNIQUE = 1000 # maximal number of unique values to report for --counts
37+
38+
def _err(msg=None):
39+
"""To return a string to signal "error" in output table"""
40+
if msg is None:
41+
msg = 'error'
42+
return '!' + msg
3343

3444
def verbose(l, msg):
3545
"""Print `s` if `l` is less than the `verbose_level`
@@ -40,11 +50,10 @@ def verbose(l, msg):
4050

4151

4252
def error(msg, exit_code):
43-
print >> sys.stderr, msg
53+
print >> sys.stderr, msg
4454
sys.exit(exit_code)
4555

4656

47-
4857
def table2string(table, out=None):
4958
"""Given list of lists figure out their common widths and print to out
5059
@@ -65,18 +74,19 @@ def table2string(table, out=None):
6574
out = StringIO()
6675

6776
# equalize number of elements in each row
68-
Nelements_max = len(table) \
69-
and max(len(x) for x in table)
77+
nelements_max = \
78+
len(table) and \
79+
max(len(x) for x in table)
7080

7181
for i, table_ in enumerate(table):
72-
table[i] += [''] * (Nelements_max - len(table_))
82+
table[i] += [''] * (nelements_max - len(table_))
7383

7484
# figure out lengths within each column
7585
atable = np.asarray(table)
7686
# eat whole entry while computing width for @w (for wide)
7787
markup_strip = re.compile('^@([lrc]|w.*)')
78-
col_width = [ max( [len(markup_strip.sub('', x))
79-
for x in column] ) for column in atable.T ]
88+
col_width = [max([len(markup_strip.sub('', x))
89+
for x in column]) for column in atable.T]
8090
string = ""
8191
for i, table_ in enumerate(table):
8292
string_ = ""
@@ -85,26 +95,26 @@ def table2string(table, out=None):
8595
if item.startswith('@'):
8696
align = item[1]
8797
item = item[2:]
88-
if not align in ['l', 'r', 'c', 'w']:
98+
if align not in ['l', 'r', 'c', 'w']:
8999
raise ValueError('Unknown alignment %s. Known are l,r,c' %
90100
align)
91101
else:
92102
align = 'c'
93103

94-
NspacesL = max(ceil((col_width[j] - len(item))/2.0), 0)
95-
NspacesR = max(col_width[j] - NspacesL - len(item), 0)
104+
nspacesl = max(ceil((col_width[j] - len(item)) / 2.0), 0)
105+
nspacesr = max(col_width[j] - nspacesl - len(item), 0)
96106

97107
if align in ['w', 'c']:
98108
pass
99109
elif align == 'l':
100-
NspacesL, NspacesR = 0, NspacesL + NspacesR
110+
nspacesl, nspacesr = 0, nspacesl + nspacesr
101111
elif align == 'r':
102-
NspacesL, NspacesR = NspacesL + NspacesR, 0
112+
nspacesl, nspacesr = nspacesl + nspacesr, 0
103113
else:
104114
raise RuntimeError('Should not get here with align=%s' % align)
105115

106116
string_ += "%%%ds%%s%%%ds " \
107-
% (NspacesL, NspacesR) % ('', item, '')
117+
% (nspacesl, nspacesr) % ('', item, '')
108118
string += string_.rstrip() + '\n'
109119
out.write(asunicode(string))
110120

@@ -113,15 +123,17 @@ def table2string(table, out=None):
113123
out.close()
114124
return value
115125

116-
def ap(l, format, sep=', '):
126+
127+
def ap(l, format_, sep=', '):
117128
"""Little helper to enforce consistency"""
118129
if l == '-':
119130
return l
120-
ls = [format % x for x in l]
131+
ls = [format_ % x for x in l]
121132
return sep.join(ls)
122133

134+
123135
def safe_get(obj, name):
124-
"""
136+
"""A getattr which would return '-' if getattr fails
125137
"""
126138
try:
127139
f = getattr(obj, 'get_' + name)
@@ -130,11 +142,12 @@ def safe_get(obj, name):
130142
verbose(2, "get_%s() failed -- %s" % (name, e))
131143
return '-'
132144

145+
133146
def get_opt_parser():
134147
# use module docstring for help output
135148
p = OptionParser(
136-
usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__,
137-
version="%prog " + nib.__version__)
149+
usage="%s [OPTIONS] [FILE ...]\n\n" % sys.argv[0] + __doc__,
150+
version="%prog " + nib.__version__)
138151

139152
p.add_options([
140153
Option("-v", "--verbose", action="count",
@@ -149,13 +162,23 @@ def get_opt_parser():
149162
action="store_true", dest='stats', default=False,
150163
help="Output basic data statistics"),
151164

165+
Option("-c", "--counts",
166+
action="store_true", dest='counts', default=False,
167+
help="Output counts - number of entries for each numeric value "
168+
"(useful for int ROI maps)"),
169+
170+
Option("--all-counts",
171+
action="store_true", dest='all_counts', default=False,
172+
help="Output all counts, even if number of unique values > %d" % MAX_UNIQUE),
173+
152174
Option("-z", "--zeros",
153175
action="store_true", dest='stats_zeros', default=False,
154-
help="Include zeros into output basic data statistics (--stats)"),
155-
])
176+
help="Include zeros into output basic data statistics (--stats, --counts)"),
177+
])
156178

157179
return p
158180

181+
159182
def proc_file(f, opts):
160183
verbose(1, "Loading %s" % f)
161184

@@ -168,21 +191,21 @@ def proc_file(f, opts):
168191
verbose(2, "Failed to gather information -- %s" % str(e))
169192
return row
170193

171-
row += [ str(safe_get(h, 'data_dtype')),
172-
'@l[%s]' %ap(safe_get(h, 'data_shape'), '%3g'),
173-
'@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x') ]
194+
row += [str(safe_get(h, 'data_dtype')),
195+
'@l[%s]' % ap(safe_get(h, 'data_shape'), '%3g'),
196+
'@l%s' % ap(safe_get(h, 'zooms'), '%.2f', 'x')]
174197
# Slope
175-
if (hasattr(h, 'has_data_slope')
176-
and (h.has_data_slope or h.has_data_intercept)) \
177-
and not h.get_slope_inter() in [(1.0, 0.0), (None, None)]:
198+
if hasattr(h, 'has_data_slope') and \
199+
(h.has_data_slope or h.has_data_intercept) and \
200+
not h.get_slope_inter() in [(1.0, 0.0), (None, None)]:
178201
row += ['@l*%.3g+%.3g' % h.get_slope_inter()]
179202
else:
180-
row += [ '' ]
203+
row += ['']
181204

182-
if (hasattr(h, 'extensions') and len(h.extensions)):
205+
if hasattr(h, 'extensions') and len(h.extensions):
183206
row += ['@l#exts: %d' % len(h.extensions)]
184207
else:
185-
row += [ '' ]
208+
row += ['']
186209

187210
if opts.header_fields:
188211
# signals "all fields"
@@ -194,16 +217,16 @@ def proc_file(f, opts):
194217
header_fields = opts.header_fields.split(',')
195218

196219
for f in header_fields:
197-
if not f: # skip empty
220+
if not f: # skip empty
198221
continue
199222
try:
200223
row += [str(h[f])]
201224
except (KeyError, ValueError):
202-
row += [ 'error' ]
225+
row += [_err()]
203226

204227
try:
205-
if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform')
206-
and (h.get_qform() != h.get_sform()).any()):
228+
if (hasattr(h, 'get_qform') and hasattr(h, 'get_sform') and
229+
(h.get_qform() != h.get_sform()).any()):
207230
row += ['sform']
208231
else:
209232
row += ['']
@@ -212,21 +235,34 @@ def proc_file(f, opts):
212235
if isinstance(h, nib.AnalyzeHeader):
213236
row += ['']
214237
else:
215-
row += ['error']
238+
row += [_err()]
216239

217-
if opts.stats:
240+
if opts.stats or opts.counts:
218241
# We are doomed to load data
219242
try:
220243
d = vol.get_data()
221244
if not opts.stats_zeros:
222245
d = d[np.nonzero(d)]
223-
# just # of elements
224-
row += ["[%d] " % np.prod(d.shape)]
225-
# stats
226-
row += [len(d) and '%.2g:%.2g' % (np.min(d), np.max(d)) or '-']
227-
except Exception as e:
228-
verbose(2, "Failed to obtain stats -- %s" % str(e))
229-
row += ['error']
246+
else:
247+
# at least flatten it -- functionality below doesn't
248+
# depend on the original shape, so let's use a flat view
249+
d = d.reshape(-1)
250+
if opts.stats:
251+
# just # of elements
252+
row += ["@l[%d]" % np.prod(d.shape)]
253+
# stats
254+
row += [len(d) and '@l[%.2g, %.2g]' % (np.min(d), np.max(d)) or '-']
255+
if opts.counts:
256+
items, inv = np.unique(d, return_inverse=True)
257+
if len(items) > 1000 and not opts.all_counts:
258+
counts = _err("%d uniques. Use --all-counts" % len(items))
259+
else:
260+
freq = np.bincount(inv)
261+
counts = " ".join("%g:%d" % (i, f) for i, f in zip(items, freq))
262+
row += ["@l" + counts]
263+
except IOError as e:
264+
verbose(2, "Failed to obtain stats/counts -- %s" % str(e))
265+
row += [_err()]
230266
return row
231267

232268

nibabel/tests/test_scripts.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ def script_test(func):
5353
DATA_PATH = abspath(pjoin(dirname(__file__), 'data'))
5454

5555

56-
def check_nib_ls_example4d(opts=[], hdrs_str=""):
56+
def check_nib_ls_example4d(opts=[], hdrs_str="", other_str=""):
5757
# test nib-ls script
5858
fname = pjoin(DATA_PATH, 'example4d.nii.gz')
5959
expected_re = (" (int16|[<>]i2) \[128, 96, 24, 2\] "
60-
"2.00x2.00x2.20x2000.00 #exts: 2%s sform$"
61-
% hdrs_str)
60+
"2.00x2.00x2.20x2000.00 #exts: 2%s sform%s$"
61+
% (hdrs_str, other_str))
6262
cmd = ['nib-ls'] + opts + [fname]
6363
code, stdout, stderr = run_command(cmd)
6464
assert_equal(fname, stdout[:len(fname)])
@@ -68,7 +68,16 @@ def check_nib_ls_example4d(opts=[], hdrs_str=""):
6868
def test_nib_ls():
6969
yield check_nib_ls_example4d
7070
yield check_nib_ls_example4d, \
71-
['-H', 'dim,bitpix'], " \[ 4 128 96 24 2 1 1 1\] 16"
71+
['-H', 'dim,bitpix'], " \[ 4 128 96 24 2 1 1 1\] 16"
72+
yield check_nib_ls_example4d, ['-c'], "", " !1030 uniques. Use --all-counts"
73+
yield check_nib_ls_example4d, ['-c', '--all-counts'], "", " 2:3 3:2 4:1 5:1.*"
74+
# both stats and counts
75+
yield check_nib_ls_example4d, \
76+
['-c', '-s', '--all-counts'], "", " \[229725\] \[2, 1.2e\+03\] 2:3 3:2 4:1 5:1.*"
77+
# and must not error out if we allow for zeros
78+
yield check_nib_ls_example4d, \
79+
['-c', '-s', '-z', '--all-counts'], "", " \[589824\] \[0, 1.2e\+03\] 0:360099 2:3 3:2 4:1 5:1.*"
80+
7281

7382
@script_test
7483
def test_nib_ls_multiple():
@@ -109,10 +118,10 @@ def test_nib_ls_multiple():
109118
assert_equal(
110119
[l[l.index('['):] for l in stdout_lines],
111120
[
112-
'[128, 96, 24, 2] 2.00x2.00x2.20x2000.00 #exts: 2 sform [229725] 2:1.2e+03',
113-
'[ 32, 20, 12, 2] 2.00x2.00x2.20x2000.00 #exts: 2 sform [15360] 46:7.6e+02',
114-
'[ 18, 28, 29] 9.00x8.00x7.00 [14616] 0.12:93',
115-
'[ 91, 109, 91] 2.00x2.00x2.00 error'
121+
'[128, 96, 24, 2] 2.00x2.00x2.20x2000.00 #exts: 2 sform [229725] [2, 1.2e+03]',
122+
'[ 32, 20, 12, 2] 2.00x2.00x2.20x2000.00 #exts: 2 sform [15360] [46, 7.6e+02]',
123+
'[ 18, 28, 29] 9.00x8.00x7.00 [14616] [0.12, 93]',
124+
'[ 91, 109, 91] 2.00x2.00x2.00 !error'
116125
]
117126
)
118127

0 commit comments

Comments
 (0)