Skip to content

Commit ac705eb

Browse files
committed
Merge remote-tracking branch 'upstream/maint/1.1.x'
2 parents 15c9161 + 8350a26 commit ac705eb

File tree

6 files changed

+89
-39
lines changed

6 files changed

+89
-39
lines changed

CHANGES.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
1.1.8 (February 26, 2020)
2+
=========================
3+
Bug-fix release in the 1.1.x series.
4+
5+
This release includes some minor improvements to formatting of reports and derivative metadata.
6+
7+
* FIX: Check fo valid qform before calculating change (#466) @effigies
8+
* ENH: Display errors as summary/details elements (#464) @effigies
9+
* ENH: Add a pure-Python ApplyMask interface, based on NiBabel (#463) @oesteban
10+
* RF: Replace ``os`` operations with ``pathlib``, indent JSON sidecars (#467) @mgxd
11+
112
1.1.7 (February 14, 2020)
213
=========================
314
Minor improvements to enable fMRIPrep 20.0.0 release.

niworkflows/interfaces/cifti.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""Handling connectivity: combines FreeSurfer surfaces with subcortical volumes."""
4-
import os
5-
from glob import glob
4+
from pathlib import Path
65
import json
76
import warnings
87

@@ -209,12 +208,7 @@ def _get_cifti_data(surface, volume, subjects_dir=None, density=None):
209208
# fMRIPrep grayordinates
210209
if volume == "MNI152NLin2009cAsym":
211210
tpl_kwargs.update({'resolution': '2', 'desc': 'DKT31'})
212-
annotation_files = sorted(glob(os.path.join(
213-
subjects_dir,
214-
surface,
215-
'label',
216-
'*h.aparc.annot'
217-
)))
211+
annotation_files = sorted((subjects_dir / surface / 'label').glob('*h.aparc.annot'))
218212
# HCP grayordinates
219213
elif volume == 'MNI152NLin6Asym':
220214
# templateflow specific resolutions (2mm, 1.6mm)
@@ -253,7 +247,7 @@ def _get_cifti_variant(surface, volume, density=None):
253247
Examples
254248
--------
255249
>>> metafile, variant, _ = _get_cifti_variant('fsaverage5', 'MNI152NLin2009cAsym')
256-
>>> metafile # doctest: +ELLIPSIS
250+
>>> str(metafile) # doctest: +ELLIPSIS
257251
'.../dtseries_variant.json'
258252
>>> variant
259253
'fMRIPrep grayordinates'
@@ -279,7 +273,7 @@ def _get_cifti_variant(surface, volume, density=None):
279273
)
280274

281275
grayords = None
282-
out_metadata = os.path.abspath('dtseries_variant.json')
276+
out_metadata = Path.cwd() / 'dtseries_variant.json'
283277
out_json = {
284278
'space': variant,
285279
'surface': surface,
@@ -290,8 +284,7 @@ def _get_cifti_variant(surface, volume, density=None):
290284
grayords = {'32k': '91k', '59k': '170k'}[density]
291285
out_json['grayordinates'] = grayords
292286

293-
with open(out_metadata, 'w') as fp:
294-
json.dump(out_json, fp)
287+
out_metadata.write_text(json.dumps(out_json, indent=2))
295288
return out_metadata, variant, grayords
296289

297290

@@ -428,4 +421,4 @@ def _create_cifti_image(bold_file, label_file, bold_surfs, annotation_files, tr,
428421

429422
out_file = "{}.dtseries.nii".format(split_filename(bold_file)[1])
430423
ci.save(img, out_file)
431-
return os.path.join(os.getcwd(), out_file)
424+
return Path.cwd() / out_file

niworkflows/interfaces/images.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -420,25 +420,28 @@ def _run_interface(self, runtime):
420420
elif (valid_sform and sform_code > 0) and (not matching_affines or qform_code == 0):
421421
img.set_qform(sform, sform_code)
422422
new_qform = img.get_qform()
423-
if np.allclose(new_qform, qform) and qform_code > 0:
424-
# False alarm
425-
self._results['out_file'] = self.inputs.in_file
426-
open(out_report, 'w').close()
427-
self._results['out_report'] = out_report
428-
return runtime
429-
diff = np.linalg.inv(qform) @ new_qform
430-
trans, rot, _, _ = transforms3d.affines.decompose44(diff)
431-
angle = transforms3d.axangles.mat2axangle(rot)[1]
432-
total_trans = np.sqrt(np.sum(trans * trans)) # Add angle and total_trans to report
433-
warning_txt = 'Note on orientation: qform matrix overwritten'
434-
description = """\
423+
if valid_qform:
424+
# False alarm - the difference is due to precision loss of qform
425+
if np.allclose(new_qform, qform) and qform_code > 0:
426+
self._results['out_file'] = self.inputs.in_file
427+
open(out_report, 'w').close()
428+
self._results['out_report'] = out_report
429+
return runtime
430+
# Replacing an existing, valid qform. Report magnitude of change.
431+
diff = np.linalg.inv(qform) @ new_qform
432+
trans, rot, _, _ = transforms3d.affines.decompose44(diff)
433+
angle = transforms3d.axangles.mat2axangle(rot)[1]
434+
total_trans = np.sqrt(np.sum(trans * trans)) # Add angle and total_trans to report
435+
warning_txt = 'Note on orientation: qform matrix overwritten'
436+
description = """\
435437
<p class="elem-desc">
436438
The qform has been copied from sform.
437439
The difference in angle is {angle:.02g}.
438440
The difference in translation is {total_trans:.02g}.
439441
</p>
440442
""".format(angle=angle, total_trans=total_trans)
441-
if not valid_qform and qform_code > 0:
443+
elif qform_code > 0:
444+
# qform code indicates the qform is supposed to be valid. Use more stridency.
442445
warning_txt = 'WARNING - Invalid qform information'
443446
description = """\
444447
<p class="elem-desc">
@@ -448,6 +451,10 @@ def _run_interface(self, runtime):
448451
by the scanner is advised.
449452
</p>
450453
"""
454+
else: # qform_code == 0
455+
# qform is not expected to be valids. Simple note.
456+
warning_txt = 'Note on orientation: qform matrix overwritten'
457+
description = '<p class="elem-desc">The qform has been copied from sform.</p>'
451458
# Rows 5-6:
452459
else:
453460
affine = img.header.get_base_affine()

niworkflows/interfaces/tests/test_images.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,51 @@ def test_qformsform_warning(tmp_path, qform_add, sform_add, expectation):
3434
validate = pe.Node(im.ValidateImage(), name='validate', base_dir=str(tmp_path))
3535
validate.inputs.in_file = fname
3636
res = validate.run()
37+
out_report = Path(res.outputs.out_report).read_text()
3738
if expectation == 'warn':
38-
assert "Note on" in Path(res.outputs.out_report).read_text()
39-
assert len(Path(res.outputs.out_report).read_text()) > 0
39+
assert "Note on" in out_report
4040
elif expectation == 'no_warn':
41-
assert len(Path(res.outputs.out_report).read_text()) == 0
41+
assert len(out_report) == 0
42+
43+
44+
@pytest.mark.parametrize('qform_code, warning_text', [
45+
(0, "Note on orientation"),
46+
(1, "WARNING - Invalid qform"),
47+
])
48+
def test_bad_qform(tmp_path, qform_code, warning_text):
49+
fname = str(tmp_path / 'test.nii')
50+
51+
# make a random image
52+
random_data = np.random.random(size=(5, 5, 5) + (5,))
53+
img = nb.Nifti1Image(random_data, np.eye(4))
54+
55+
# Some magic terms from a bad qform in the wild
56+
img.header['qform_code'] = qform_code
57+
img.header['quatern_b'] = 0
58+
img.header['quatern_c'] = 0.998322
59+
img.header['quatern_d'] = -0.0579125
60+
img.to_filename(fname)
61+
62+
validate = pe.Node(im.ValidateImage(), name='validate', base_dir=str(tmp_path))
63+
validate.inputs.in_file = fname
64+
res = validate.run()
65+
assert warning_text in Path(res.outputs.out_report).read_text()
66+
67+
68+
def test_no_good_affines(tmp_path):
69+
fname = str(tmp_path / 'test.nii')
70+
71+
# make a random image
72+
random_data = np.random.random(size=(5, 5, 5) + (5,))
73+
img = nb.Nifti1Image(random_data, None)
74+
img.header['qform_code'] = 0
75+
img.header['sform_code'] = 0
76+
img.to_filename(fname)
77+
78+
validate = pe.Node(im.ValidateImage(), name='validate', base_dir=str(tmp_path))
79+
validate.inputs.in_file = fname
80+
res = validate.run()
81+
assert 'WARNING - Missing orientation information' in Path(res.outputs.out_report).read_text()
4282

4383

4484
@pytest.mark.parametrize('nvols, nmasks, ext, factor', [

niworkflows/reports/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ class Report(object):
234234
>>> robj.generate_report()
235235
0
236236
>>> len((testdir / 'out' / 'fmriprep' / 'sub-01.html').read_text())
237-
36425
237+
36450
238238
239239
.. testcleanup::
240240

niworkflows/reports/report.tpl

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ div#boilerplate pre {
4949
background-color: #F8F9FA;
5050
}
5151
52+
#errors div, #errors p {
53+
padding-left: 1em;
54+
}
5255
</style>
5356
</head>
5457
<body>
@@ -127,12 +130,10 @@ div#boilerplate pre {
127130

128131
<div id="errors">
129132
<h1 class="sub-report-title">Errors</h1>
130-
<ul>
131133
{% for error in errors %}
132-
<li>
133-
<div class="nipype_error">
134-
Node Name: <a target="_self" onclick="toggle('{{error.file|replace('.', '')}}_details_id');">{{ error.node }}</a><br>
135-
<div id="{{error.file|replace('.', '')}}_details_id" style="display:none">
134+
<details>
135+
<summary>Node Name: {{ error.node }}</summary><br>
136+
<div>
136137
File: <code>{{ error.file }}</code><br>
137138
Working Directory: <code>{{ error.node_dir }}</code><br>
138139
Inputs: <br>
@@ -142,13 +143,11 @@ div#boilerplate pre {
142143
{% endfor %}
143144
</ul>
144145
<pre>{{ error.traceback }}</pre>
145-
</div>
146146
</div>
147-
</li>
147+
</details>
148148
{% else %}
149-
<li>No errors to report!</li>
149+
<p>No errors to report!</p>
150150
{% endfor %}
151-
</ul>
152151
</div>
153152

154153

0 commit comments

Comments
 (0)