Skip to content

Commit ec7926a

Browse files
committed
Merge pull request #329 from bcipolli/reload
MRG: Migrate load, save, class_map, ext_map to class properties and methods This is to address #317 and #323 (and aims to supercede PR #319), both issues around image-specific code being distributed across files, rather than localized in the class definition. Changes here include: * Removing image-specific code from `nib.load` and `nib.save` functions, moving the logic into `ImageKlass.is_image` and `HeaderKlass.is_header` class functions. * Deprecating `class_map` and `ext_map` objects, and moving necessary info to properties defined on the image classes themselves.
2 parents 3bc31e9 + 8a926a4 commit ec7926a

23 files changed

+527
-172
lines changed

nibabel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
from .orientations import (io_orientation, orientation_affine,
6262
flip_axis, OrientationError,
6363
apply_orientation, aff2axcodes)
64-
from .imageclasses import class_map, ext_map
64+
from .imageclasses import class_map, ext_map, all_image_classes
6565
from . import trackvis
6666
from . import mriutils
6767

nibabel/analyze.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,13 +892,28 @@ def _chk_pixdims(hdr, fix=False):
892892
rep.fix_msg = ' and '.join(fmsgs)
893893
return hdr, rep
894894

895+
@classmethod
896+
def may_contain_header(klass, binaryblock):
897+
if len(binaryblock) < klass.sizeof_hdr:
898+
return False
899+
900+
hdr_struct = np.ndarray(shape=(), dtype=header_dtype,
901+
buffer=binaryblock[:klass.sizeof_hdr])
902+
bs_hdr_struct = hdr_struct.byteswap()
903+
return 348 in (hdr_struct['sizeof_hdr'], bs_hdr_struct['sizeof_hdr'])
904+
895905

896906
class AnalyzeImage(SpatialImage):
897907
""" Class for basic Analyze format image
898908
"""
899909
header_class = AnalyzeHeader
910+
_meta_sniff_len = header_class.sizeof_hdr
900911
files_types = (('image', '.img'), ('header', '.hdr'))
901-
_compressed_exts = ('.gz', '.bz2')
912+
valid_exts = ('.img', '.hdr')
913+
_compressed_suffixes = ('.gz', '.bz2')
914+
915+
makeable = True
916+
rw = True
902917

903918
ImageArrayProxy = ArrayProxy
904919

nibabel/ecat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ class EcatImage(SpatialImage):
732732
"""
733733
_header = EcatHeader
734734
header_class = _header
735+
valid_exts = ('.v',)
735736
_subheader = EcatSubHeader
736737
files_types = (('image', '.v'), ('header', '.v'))
737738

nibabel/freesurfer/mghformat.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,18 @@ def writeftr_to(self, fileobj):
454454
fileobj.write(ftr_nd.tostring())
455455

456456

457+
# Register .mgz extension as compressed
457458
@ImageOpener.register_ext_from_image('.mgz', ImageOpener.gz_def)
458459
class MGHImage(SpatialImage):
459460
""" Class for MGH format image
460461
"""
461462
header_class = MGHHeader
463+
valid_exts = ('.mgh',)
462464
files_types = (('image', '.mgh'),)
463-
_compressed_exts = (('.gz',))
465+
_compressed_suffixes = ()
466+
467+
makeable = True
468+
rw = True
464469

465470
ImageArrayProxy = ArrayProxy
466471

nibabel/freesurfer/tests/test_mghformat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_filename_exts():
145145
# and the default affine matrix (Note the "None")
146146
img = MGHImage(v, None)
147147
# Check if these extensions allow round trip
148-
for ext in ('.mgh', '.mgz', '.mgh.gz'):
148+
for ext in ('.mgh', '.mgz'):
149149
with InTemporaryDirectory():
150150
fname = 'tmpname' + ext
151151
save(img, fname)

nibabel/imageclasses.py

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,69 +7,92 @@
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
''' Define supported image classes and names '''
10+
import warnings
11+
1012
from .analyze import AnalyzeImage
1113
from .spm99analyze import Spm99AnalyzeImage
1214
from .spm2analyze import Spm2AnalyzeImage
1315
from .nifti1 import Nifti1Pair, Nifti1Image
16+
from .nifti2 import Nifti2Pair, Nifti2Image
1417
from .minc1 import Minc1Image
18+
from .minc2 import Minc2Image
1519
from .freesurfer import MGHImage
1620
from .parrec import PARRECImage
1721
from .volumeutils import Recoder
1822
from .optpkg import optional_package
1923
_, have_scipy, _ = optional_package('scipy')
2024

2125

22-
# mapping of names to classes and class functionality
26+
# Ordered by the load/save priority.
27+
all_image_classes = [Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image,
28+
Spm2AnalyzeImage, Spm99AnalyzeImage, AnalyzeImage,
29+
Minc1Image, Minc2Image, MGHImage,
30+
PARRECImage]
31+
32+
33+
# DEPRECATED: mapping of names to classes and class functionality
34+
class ClassMapDict(dict):
35+
def __getitem__(self, *args, **kwargs):
36+
warnings.warn("class_map is deprecated.", DeprecationWarning,
37+
stacklevel=2)
38+
return super(ClassMapDict, self).__getitem__(*args, **kwargs)
39+
40+
class_map = ClassMapDict(
41+
analyze={'class': AnalyzeImage, # Image class
42+
'ext': '.img', # characteristic image extension
43+
'has_affine': False, # class can store an affine
44+
'makeable': True, # empty image can be easily made in memory
45+
'rw': True}, # image can be written
46+
spm99analyze={'class': Spm99AnalyzeImage,
47+
'ext': '.img',
48+
'has_affine': True,
49+
'makeable': True,
50+
'rw': have_scipy},
51+
spm2analyze={'class': Spm2AnalyzeImage,
52+
'ext': '.img',
53+
'has_affine': True,
54+
'makeable': True,
55+
'rw': have_scipy},
56+
nifti_pair={'class': Nifti1Pair,
57+
'ext': '.img',
58+
'has_affine': True,
59+
'makeable': True,
60+
'rw': True},
61+
nifti_single={'class': Nifti1Image,
62+
'ext': '.nii',
63+
'has_affine': True,
64+
'makeable': True,
65+
'rw': True},
66+
minc={'class': Minc1Image,
67+
'ext': '.mnc',
68+
'has_affine': True,
69+
'makeable': True,
70+
'rw': False},
71+
mgh={'class': MGHImage,
72+
'ext': '.mgh',
73+
'has_affine': True,
74+
'makeable': True,
75+
'rw': True},
76+
mgz={'class': MGHImage,
77+
'ext': '.mgz',
78+
'has_affine': True,
79+
'makeable': True,
80+
'rw': True},
81+
par={'class': PARRECImage,
82+
'ext': '.par',
83+
'has_affine': True,
84+
'makeable': False,
85+
'rw': False})
86+
2387

24-
class_map = {
25-
'analyze': {'class': AnalyzeImage, # Image class
26-
'ext': '.img', # characteristic image extension
27-
'has_affine': False, # class can store an affine
28-
'makeable': True, # empty image can be easily made in memory
29-
'rw': True}, # image can be written
30-
'spm99analyze': {'class': Spm99AnalyzeImage,
31-
'ext': '.img',
32-
'has_affine': True,
33-
'makeable': True,
34-
'rw': have_scipy},
35-
'spm2analyze': {'class': Spm2AnalyzeImage,
36-
'ext': '.img',
37-
'has_affine': True,
38-
'makeable': True,
39-
'rw': have_scipy},
40-
'nifti_pair': {'class': Nifti1Pair,
41-
'ext': '.img',
42-
'has_affine': True,
43-
'makeable': True,
44-
'rw': True},
45-
'nifti_single': {'class': Nifti1Image,
46-
'ext': '.nii',
47-
'has_affine': True,
48-
'makeable': True,
49-
'rw': True},
50-
'minc': {'class': Minc1Image,
51-
'ext': '.mnc',
52-
'has_affine': True,
53-
'makeable': True,
54-
'rw': False},
55-
'mgh': {'class': MGHImage,
56-
'ext': '.mgh',
57-
'has_affine': True,
58-
'makeable': True,
59-
'rw': True},
60-
'mgz': {'class': MGHImage,
61-
'ext': '.mgz',
62-
'has_affine': True,
63-
'makeable': True,
64-
'rw': True},
65-
'par': {'class': PARRECImage,
66-
'ext': '.par',
67-
'has_affine': True,
68-
'makeable': False,
69-
'rw': False}}
88+
class ExtMapRecoder(Recoder):
89+
def __getitem__(self, *args, **kwargs):
90+
warnings.warn("ext_map is deprecated.", DeprecationWarning,
91+
stacklevel=2)
92+
return super(ExtMapRecoder, self).__getitem__(*args, **kwargs)
7093

7194
# mapping of extensions to default image class names
72-
ext_map = Recoder((
95+
ext_map = ExtMapRecoder((
7396
('nifti_single', '.nii'),
7497
('nifti_pair', '.img', '.hdr'),
7598
('minc', '.mnc'),

nibabel/loadsave.py

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,12 @@
1010
""" Utilities to load and save image objects """
1111

1212
import numpy as np
13+
import warnings
1314

14-
from .filename_parser import types_filenames, splitext_addext
15+
from .filename_parser import splitext_addext
1516
from .openers import ImageOpener
16-
from .analyze import AnalyzeImage
17-
from .spm2analyze import Spm2AnalyzeImage
18-
from .nifti1 import Nifti1Image, Nifti1Pair, header_dtype as ni1_hdr_dtype
19-
from .nifti2 import Nifti2Image, Nifti2Pair
20-
from .minc1 import Minc1Image
21-
from .minc2 import Minc2Image
22-
from .freesurfer import MGHImage
2317
from .spatialimages import ImageFileError
24-
from .imageclasses import class_map, ext_map
18+
from .imageclasses import all_image_classes
2519
from .arrayproxy import is_proxy
2620

2721

@@ -40,9 +34,17 @@ def load(filename, **kwargs):
4034
img : ``SpatialImage``
4135
Image of guessed type
4236
'''
43-
return guessed_image_type(filename).from_filename(filename, **kwargs)
37+
sniff = None
38+
for image_klass in all_image_classes:
39+
is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
40+
if is_valid:
41+
return image_klass.from_filename(filename, **kwargs)
4442

43+
raise ImageFileError('Cannot work out file type of "%s"' %
44+
filename)
4545

46+
47+
@np.deprecate
4648
def guessed_image_type(filename):
4749
""" Guess image type from file `filename`
4850
@@ -56,39 +58,16 @@ def guessed_image_type(filename):
5658
image_class : class
5759
Class corresponding to guessed image type
5860
"""
59-
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
60-
lext = ext.lower()
61-
try:
62-
img_type = ext_map[lext]
63-
except KeyError:
64-
raise ImageFileError('Cannot work out file type of "%s"' %
65-
filename)
66-
if lext in ('.mgh', '.mgz', '.par'):
67-
klass = class_map[img_type]['class']
68-
elif lext == '.mnc':
69-
# Look for HDF5 signature for MINC2
70-
# https://www.hdfgroup.org/HDF5/doc/H5.format.html
71-
with ImageOpener(filename) as fobj:
72-
signature = fobj.read(4)
73-
klass = Minc2Image if signature == b'\211HDF' else Minc1Image
74-
elif lext == '.nii':
75-
with ImageOpener(filename) as fobj:
76-
binaryblock = fobj.read(348)
77-
ft = which_analyze_type(binaryblock)
78-
klass = Nifti2Image if ft == 'nifti2' else Nifti1Image
79-
else: # might be nifti 1 or 2 pair or analyze of some sort
80-
files_types = (('image', '.img'), ('header', '.hdr'))
81-
filenames = types_filenames(filename, files_types)
82-
with ImageOpener(filenames['header']) as fobj:
83-
binaryblock = fobj.read(348)
84-
ft = which_analyze_type(binaryblock)
85-
if ft == 'nifti2':
86-
klass = Nifti2Pair
87-
elif ft == 'nifti1':
88-
klass = Nifti1Pair
89-
else:
90-
klass = Spm2AnalyzeImage
91-
return klass
61+
warnings.warn('guessed_image_type is deprecated', DeprecationWarning,
62+
stacklevel=2)
63+
sniff = None
64+
for image_klass in all_image_classes:
65+
is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
66+
if is_valid:
67+
return image_klass
68+
69+
raise ImageFileError('Cannot work out file type of "%s"' %
70+
filename)
9271

9372

9473
def save(img, filename):
@@ -105,25 +84,38 @@ def save(img, filename):
10584
-------
10685
None
10786
'''
87+
88+
# Save the type as expected
10889
try:
10990
img.to_filename(filename)
11091
except ImageFileError:
11192
pass
11293
else:
11394
return
95+
96+
# Be nice to users by making common implicit conversions
11497
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
98+
lext = ext.lower()
99+
115100
# Special-case Nifti singles and Pairs
116-
if type(img) == Nifti1Image and ext in ('.img', '.hdr'):
101+
# Inline imports, as this module really shouldn't reference any image type
102+
from .nifti1 import Nifti1Image, Nifti1Pair
103+
from .nifti2 import Nifti2Image, Nifti2Pair
104+
if type(img) == Nifti1Image and lext in ('.img', '.hdr'):
117105
klass = Nifti1Pair
118-
elif type(img) == Nifti2Image and ext in ('.img', '.hdr'):
106+
elif type(img) == Nifti2Image and lext in ('.img', '.hdr'):
119107
klass = Nifti2Pair
120-
elif type(img) == Nifti1Pair and ext == '.nii':
108+
elif type(img) == Nifti1Pair and lext == '.nii':
121109
klass = Nifti1Image
122-
elif type(img) == Nifti2Pair and ext == '.nii':
110+
elif type(img) == Nifti2Pair and lext == '.nii':
123111
klass = Nifti2Image
124-
else:
125-
img_type = ext_map[ext]
126-
klass = class_map[img_type]['class']
112+
else: # arbitrary conversion
113+
valid_klasses = [klass for klass in all_image_classes
114+
if ext in klass.valid_exts]
115+
if not valid_klasses: # if list is empty
116+
raise ImageFileError('Cannot work out file type of "%s"' %
117+
filename)
118+
klass = valid_klasses[0]
127119
converted = klass.from_image(img)
128120
converted.to_filename(filename)
129121

@@ -214,6 +206,7 @@ def read_img_data(img, prefer='scaled'):
214206
return hdr.raw_data_from_fileobj(fileobj)
215207

216208

209+
@np.deprecate
217210
def which_analyze_type(binaryblock):
218211
""" Is `binaryblock` from NIfTI1, NIfTI2 or Analyze header?
219212
@@ -241,13 +234,16 @@ def which_analyze_type(binaryblock):
241234
* if ``sizeof_hdr`` is 348 or byteswapped 348 assume Analyze
242235
* Return None
243236
"""
244-
hdr = np.ndarray(shape=(), dtype=ni1_hdr_dtype, buffer=binaryblock)
245-
bs_hdr = hdr.byteswap()
246-
sizeof_hdr = hdr['sizeof_hdr']
247-
bs_sizeof_hdr = bs_hdr['sizeof_hdr']
237+
warnings.warn('which_analyze_type is deprecated', DeprecationWarning,
238+
stacklevel=2)
239+
from .nifti1 import header_dtype
240+
hdr_struct = np.ndarray(shape=(), dtype=header_dtype, buffer=binaryblock)
241+
bs_hdr_struct = hdr_struct.byteswap()
242+
sizeof_hdr = hdr_struct['sizeof_hdr']
243+
bs_sizeof_hdr = bs_hdr_struct['sizeof_hdr']
248244
if 540 in (sizeof_hdr, bs_sizeof_hdr):
249245
return 'nifti2'
250-
if hdr['magic'] in (b'ni1', b'n+1'):
246+
if hdr_struct['magic'] in (b'ni1', b'n+1'):
251247
return 'nifti1'
252248
if 348 in (sizeof_hdr, bs_sizeof_hdr):
253249
return 'analyze'

0 commit comments

Comments
 (0)