Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b55a363

Browse files
committed
ENH: Add validation when saving CIFTI2 images
- Enabled by default, validation will parse the output filename for a valid CIFTI2 extension. - If found, the intent code of the image will be set. Also, the CIFTI2Header will be check for compliant index maps for the intent code
1 parent d0bbcc7 commit b55a363

File tree

2 files changed

+131
-38
lines changed

2 files changed

+131
-38
lines changed

‎nibabel/cifti2/cifti2.py‎

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ..dataobj_images import DataobjImage
2525
from ..nifti2 import Nifti2Image, Nifti2Header
2626
from ..arrayproxy import reshape_dataobj
27+
from ..volumeutils import Recoder
2728
from warnings import warn
2829

2930

@@ -90,20 +91,50 @@ class Cifti2HeaderError(Exception):
9091

9192
# "Standard CIFTI Mapping Combinations" within CIFTI-2 spec
9293
# https://www.nitrc.org/forum/attachment.php?attachid=341&group_id=454&forum_id=1955
93-
CIFTI_EXTENSIONS_TO_INTENTS = {
94-
'.dconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE',
95-
'.dtseries': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
96-
'.pconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED',
97-
'.ptseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES',
98-
'.dscalar': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS',
99-
'.dlabel': 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS',
100-
'.pscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR',
101-
'.pdconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE',
102-
'.dpconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED',
103-
'.pconnseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES',
104-
'.pconnscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR',
105-
'.dfan': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
106-
}
94+
CIFTI_CODES = Recoder((
95+
('dconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE', (
96+
'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
97+
)),
98+
('dtseries', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
99+
'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
100+
)),
101+
('pconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED', (
102+
'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS',
103+
)),
104+
('ptseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES', (
105+
'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_PARCELS',
106+
)),
107+
('dscalar', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS', (
108+
'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
109+
)),
110+
('dlabel', 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS', (
111+
'CIFTI_INDEX_TYPE_LABELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
112+
)),
113+
('pscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR', (
114+
'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_PARCELS',
115+
)),
116+
('pdconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE', (
117+
'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_PARCELS',
118+
)),
119+
('dpconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED', (
120+
'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
121+
)),
122+
('pconnseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES', (
123+
'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SERIES',
124+
)),
125+
('pconnscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR', (
126+
'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SCALARS',
127+
)),
128+
('dfan', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
129+
'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
130+
)),
131+
('dfibersamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
132+
'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
133+
)),
134+
('dfansamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
135+
'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
136+
)),
137+
), fields=('extension', 'niistring', 'map_types'))
107138

108139

109140
def _value_if_klass(val, klass):
@@ -1503,32 +1534,52 @@ def get_data_dtype(self):
15031534
def set_data_dtype(self, dtype):
15041535
self._nifti_header.set_data_dtype(dtype)
15051536

1506-
def to_filename(self, filename, infer_intent=False):
1537+
def to_filename(self, filename, validate=True):
15071538
"""
15081539
Ensures NIfTI header intent code is set prior to saving.
15091540
15101541
Parameters
15111542
----------
1512-
infer_intent : boolean, optional
1513-
If ``True``, attempt to infer and set intent code based on filename suffix.
1543+
validate : boolean, optional
1544+
If ``True``, infer and validate CIFTI type based on filename suffix.
1545+
This includes the setting of the NIfTI intent code and checking the ``CIFTI2Matrix``
1546+
for the expected IndicesMaps attributes.
1547+
If validation fails, an error will be raised instead.
15141548
"""
1515-
header = self._nifti_header
1516-
if infer_intent:
1517-
# try to infer intent code based on filename suffix
1518-
intent = _infer_intent_from_filename(filename)
1519-
if intent is not None:
1520-
header.set_intent(intent)
1549+
nheader = self._nifti_header
1550+
# try to infer intent code based on filename suffix
1551+
if validate:
1552+
ext = _extract_cifti_extension(filename)
1553+
try:
1554+
CIFTI_CODES.extension[ext]
1555+
except KeyError as err:
1556+
raise KeyError(
1557+
f"Validation failed: No information for extension {ext} available"
1558+
) from err
1559+
intent = CIFTI_CODES.niistring[ext]
1560+
nheader.set_intent(intent)
1561+
# validate matrix indices
1562+
for idx, mtype in enumerate(CIFTI_CODES.map_types[ext]):
1563+
try:
1564+
assert self.header.matrix.get_index_map(idx).indices_map_to_data_type == mtype
1565+
except Exception:
1566+
raise Cifti2HeaderError(
1567+
f"Validation failed: Cifti2Matrix index map {idx} does "
1568+
f"not match expected type {mtype}"
1569+
)
15211570
# if intent code is not set, default to unknown
1522-
if header.get_intent()[0] == 'none':
1523-
header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
1571+
if nheader.get_intent()[0] == 'none':
1572+
nheader.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
15241573
super().to_filename(filename)
15251574

15261575

1527-
def _infer_intent_from_filename(filename):
1576+
def _extract_cifti_extension(filename):
15281577
"""Parses output filename for common suffixes and fetches corresponding intent code"""
15291578
from pathlib import Path
1530-
ext = Path(filename).suffixes[0]
1531-
return CIFTI_EXTENSIONS_TO_INTENTS.get(ext)
1579+
_suf = Path(filename).suffixes
1580+
# select second to last if possible (.<suffix>.nii)
1581+
ext = _suf[-2] if len(_suf) >= 2 else _suf[0]
1582+
return ext.lstrip('.')
15321583

15331584

15341585
load = Cifti2Image.from_filename

‎nibabel/cifti2/tests/test_new_cifti2.py‎

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def test_dtseries():
239239
img = ci.Cifti2Image(data, hdr)
240240

241241
with InTemporaryDirectory():
242-
ci.save(img, 'test.dtseries.nii', infer_intent=True)
242+
ci.save(img, 'test.dtseries.nii')
243243
img2 = nib.load('test.dtseries.nii')
244244
assert img2.nifti_header.get_intent()[0] == 'ConnDenseSeries'
245245
assert isinstance(img2, ci.Cifti2Image)
@@ -282,7 +282,7 @@ def test_dlabel():
282282
img = ci.Cifti2Image(data, hdr)
283283

284284
with InTemporaryDirectory():
285-
ci.save(img, 'test.dlabel.nii', infer_intent=True)
285+
ci.save(img, 'test.dlabel.nii')
286286
img2 = nib.load('test.dlabel.nii')
287287
assert img2.nifti_header.get_intent()[0] == 'ConnDenseLabel'
288288
assert isinstance(img2, ci.Cifti2Image)
@@ -301,7 +301,7 @@ def test_dconn():
301301
img = ci.Cifti2Image(data, hdr)
302302

303303
with InTemporaryDirectory():
304-
ci.save(img, 'test.dconn.nii', infer_intent=True)
304+
ci.save(img, 'test.dconn.nii')
305305
img2 = nib.load('test.dconn.nii')
306306
assert img2.nifti_header.get_intent()[0] == 'ConnDense'
307307
assert isinstance(img2, ci.Cifti2Image)
@@ -322,7 +322,7 @@ def test_ptseries():
322322
img = ci.Cifti2Image(data, hdr)
323323

324324
with InTemporaryDirectory():
325-
ci.save(img, 'test.ptseries.nii', infer_intent=True)
325+
ci.save(img, 'test.ptseries.nii')
326326
img2 = nib.load('test.ptseries.nii')
327327
assert img2.nifti_header.get_intent()[0] == 'ConnParcelSries'
328328
assert isinstance(img2, ci.Cifti2Image)
@@ -343,7 +343,7 @@ def test_pscalar():
343343
img = ci.Cifti2Image(data, hdr)
344344

345345
with InTemporaryDirectory():
346-
ci.save(img, 'test.pscalar.nii', infer_intent=True)
346+
ci.save(img, 'test.pscalar.nii')
347347
img2 = nib.load('test.pscalar.nii')
348348
assert img2.nifti_header.get_intent()[0] == 'ConnParcelScalr'
349349
assert isinstance(img2, ci.Cifti2Image)
@@ -364,7 +364,7 @@ def test_pdconn():
364364
img = ci.Cifti2Image(data, hdr)
365365

366366
with InTemporaryDirectory():
367-
ci.save(img, 'test.pdconn.nii', infer_intent=True)
367+
ci.save(img, 'test.pdconn.nii')
368368
img2 = ci.load('test.pdconn.nii')
369369
assert img2.nifti_header.get_intent()[0] == 'ConnParcelDense'
370370
assert isinstance(img2, ci.Cifti2Image)
@@ -385,7 +385,7 @@ def test_dpconn():
385385
img = ci.Cifti2Image(data, hdr)
386386

387387
with InTemporaryDirectory():
388-
ci.save(img, 'test.dpconn.nii', infer_intent=True)
388+
ci.save(img, 'test.dpconn.nii')
389389
img2 = ci.load('test.dpconn.nii')
390390
assert img2.nifti_header.get_intent()[0] == 'ConnDenseParcel'
391391
assert isinstance(img2, ci.Cifti2Image)
@@ -425,7 +425,7 @@ def test_pconn():
425425
img = ci.Cifti2Image(data, hdr)
426426

427427
with InTemporaryDirectory():
428-
ci.save(img, 'test.pconn.nii', infer_intent=True)
428+
ci.save(img, 'test.pconn.nii')
429429
img2 = ci.load('test.pconn.nii')
430430
assert img.nifti_header.get_intent()[0] == 'ConnParcels'
431431
assert isinstance(img2, ci.Cifti2Image)
@@ -447,7 +447,7 @@ def test_pconnseries():
447447
img = ci.Cifti2Image(data, hdr)
448448

449449
with InTemporaryDirectory():
450-
ci.save(img, 'test.pconnseries.nii', infer_intent=True)
450+
ci.save(img, 'test.pconnseries.nii')
451451
img2 = ci.load('test.pconnseries.nii')
452452
assert img.nifti_header.get_intent()[0] == 'ConnPPSr'
453453
assert isinstance(img2, ci.Cifti2Image)
@@ -470,7 +470,7 @@ def test_pconnscalar():
470470
img = ci.Cifti2Image(data, hdr)
471471

472472
with InTemporaryDirectory():
473-
ci.save(img, 'test.pconnscalar.nii', infer_intent=True)
473+
ci.save(img, 'test.pconnscalar.nii')
474474
img2 = ci.load('test.pconnscalar.nii')
475475
assert img.nifti_header.get_intent()[0] == 'ConnPPSc'
476476
assert isinstance(img2, ci.Cifti2Image)
@@ -509,3 +509,45 @@ def test_wrong_shape():
509509
with pytest.raises(ValueError):
510510
img.to_file_map()
511511

512+
513+
def test_cifti_validation():
514+
# flip label / brain_model index maps
515+
geometry_map = create_geometry_map((0, ))
516+
label_map = create_label_map((1, ))
517+
matrix = ci.Cifti2Matrix()
518+
matrix.append(label_map)
519+
matrix.append(geometry_map)
520+
hdr = ci.Cifti2Header(matrix)
521+
data = np.random.randn(10, 2)
522+
img = ci.Cifti2Image(data, hdr)
523+
524+
# attempt to save and validate with an invalid extension
525+
with pytest.raises(KeyError):
526+
ci.save(img, 'test.dlabelz.nii')
527+
# even with a proper extension, flipped index maps will fail
528+
with pytest.raises(ci.Cifti2HeaderError):
529+
ci.save(img, 'test.dlabel.nii')
530+
531+
label_map = create_label_map((0, ))
532+
geometry_map = create_geometry_map((1, ))
533+
matrix = ci.Cifti2Matrix()
534+
matrix.append(label_map)
535+
matrix.append(geometry_map)
536+
hdr = ci.Cifti2Header(matrix)
537+
data = np.random.randn(2, 10)
538+
img = ci.Cifti2Image(data, hdr)
539+
540+
with InTemporaryDirectory():
541+
# still fail with invalid extension and validation
542+
with pytest.raises(KeyError):
543+
ci.save(img, 'test.dlabelz.nii')
544+
# but removing validation should work (though intent code will be unknown)
545+
ci.save(img, 'test.dlabelz.nii', validate=False)
546+
547+
img2 = nib.load('test.dlabelz.nii')
548+
assert img2.nifti_header.get_intent()[0] == 'ConnUnknown'
549+
assert isinstance(img2, ci.Cifti2Image)
550+
assert_array_equal(img2.get_fdata(), data)
551+
check_label_map(img2.header.matrix.get_index_map(0))
552+
check_geometry_map(img2.header.matrix.get_index_map(1))
553+
del img2

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /