Skip to content

WIP: Migrate load, save, class_map, ext_map to class properties and methods #329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Oct 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
200bac0
RF: Begin refactoring load into image classes
effigies Jun 25, 2015
2cb5c95
Deprecate the use of class_map and ext_map.
Jul 16, 2015
f3373ca
Migrating imageclasses, away from IMAGE_MAP
Jul 16, 2015
45f50e3
Improve error handling, efficiency, and search more broadly over head…
Jul 16, 2015
20ad347
Modify save, remove vestigates of class_map / ext_map
Jul 16, 2015
91202cf
Code cleanup after self code review, fix for Python 3 'filter' issue.
Jul 20, 2015
6f40ad8
Remove references to BinOpener
Sep 1, 2015
7635ed2
Linting, cleaning up logic.
Sep 1, 2015
fab693e
Adding tests for loading each image type.
Sep 1, 2015
887423f
Add back analyze header tests.
Sep 1, 2015
e2a6fd8
sniff_size => sizeof_hdr
Sep 1, 2015
17b8253
Simplify is_image test.
Sep 1, 2015
4eec23d
Relegate slicing to is_header, remove _minctest
effigies Aug 4, 2015
64e9ac1
TST: Remove Minc1Header exception
effigies Sep 2, 2015
4d77eb4
STY: Unnecessary try block, comment lines too long
effigies Sep 4, 2015
afc9c25
TST: Test ValueErrors raised by is_header
effigies Sep 4, 2015
1971efb
RF: Rename is_header to may_contain_header
effigies Sep 4, 2015
4ddcbd2
RF: Restore and deprecate loadsave helpers
effigies Sep 4, 2015
5570d36
TST: Kill extra test; doc string to comment
effigies Sep 4, 2015
01af20f
RF: Prefer alternate_exts over abusing files_types
effigies Sep 6, 2015
0c06464
RF: refactor using valid_exts, _meta_sniff_len
matthew-brett Sep 19, 2015
fce4cad
RF: rename compressed_exts to compressed_suffxes
matthew-brett Sep 19, 2015
2ad5621
BF: isolate ImageOpener extension list
matthew-brett Sep 19, 2015
5600d42
DOC: add docstrings for image loading classmethods
matthew-brett Sep 19, 2015
d8f1be9
RF: go back to using decorator for .mgz extension
matthew-brett Sep 19, 2015
ecee561
DOC: Deprecation warnings to stacklevel 2
effigies Sep 21, 2015
c3518d1
STY: Remove misleading 'sizeof_hdr' from Minc1/2
effigies Sep 22, 2015
5814435
RF: Tag sniffs with file name
effigies Oct 6, 2015
76440f2
RF: Fail quietly on too-short header blocks
effigies Oct 6, 2015
8a926a4
TEST: path_maybe_image part of image API
effigies Oct 6, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nibabel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
from .orientations import (io_orientation, orientation_affine,
flip_axis, OrientationError,
apply_orientation, aff2axcodes)
from .imageclasses import class_map, ext_map
from .imageclasses import class_map, ext_map, all_image_classes
from . import trackvis
from . import mriutils

Expand Down
17 changes: 16 additions & 1 deletion nibabel/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,13 +892,28 @@ def _chk_pixdims(hdr, fix=False):
rep.fix_msg = ' and '.join(fmsgs)
return hdr, rep

@classmethod
def may_contain_header(klass, binaryblock):
if len(binaryblock) < klass.sizeof_hdr:
return False

hdr_struct = np.ndarray(shape=(), dtype=header_dtype,
buffer=binaryblock[:klass.sizeof_hdr])
bs_hdr_struct = hdr_struct.byteswap()
return 348 in (hdr_struct['sizeof_hdr'], bs_hdr_struct['sizeof_hdr'])


class AnalyzeImage(SpatialImage):
""" Class for basic Analyze format image
"""
header_class = AnalyzeHeader
_meta_sniff_len = header_class.sizeof_hdr
files_types = (('image', '.img'), ('header', '.hdr'))
_compressed_exts = ('.gz', '.bz2')
valid_exts = ('.img', '.hdr')
_compressed_suffixes = ('.gz', '.bz2')

makeable = True
rw = True

ImageArrayProxy = ArrayProxy

Expand Down
1 change: 1 addition & 0 deletions nibabel/ecat.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ class EcatImage(SpatialImage):
"""
_header = EcatHeader
header_class = _header
valid_exts = ('.v',)
_subheader = EcatSubHeader
files_types = (('image', '.v'), ('header', '.v'))

Expand Down
7 changes: 6 additions & 1 deletion nibabel/freesurfer/mghformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,18 @@ def writeftr_to(self, fileobj):
fileobj.write(ftr_nd.tostring())


# Register .mgz extension as compressed
@ImageOpener.register_ext_from_image('.mgz', ImageOpener.gz_def)
class MGHImage(SpatialImage):
""" Class for MGH format image
"""
header_class = MGHHeader
valid_exts = ('.mgh',)
files_types = (('image', '.mgh'),)
_compressed_exts = (('.gz',))
_compressed_suffixes = ()

makeable = True
rw = True

ImageArrayProxy = ArrayProxy

Expand Down
2 changes: 1 addition & 1 deletion nibabel/freesurfer/tests/test_mghformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def test_filename_exts():
# and the default affine matrix (Note the "None")
img = MGHImage(v, None)
# Check if these extensions allow round trip
for ext in ('.mgh', '.mgz', '.mgh.gz'):
for ext in ('.mgh', '.mgz'):
with InTemporaryDirectory():
fname = 'tmpname' + ext
save(img, fname)
Expand Down
119 changes: 71 additions & 48 deletions nibabel/imageclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,92 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
''' Define supported image classes and names '''
import warnings

from .analyze import AnalyzeImage
from .spm99analyze import Spm99AnalyzeImage
from .spm2analyze import Spm2AnalyzeImage
from .nifti1 import Nifti1Pair, Nifti1Image
from .nifti2 import Nifti2Pair, Nifti2Image
from .minc1 import Minc1Image
from .minc2 import Minc2Image
from .freesurfer import MGHImage
from .parrec import PARRECImage
from .volumeutils import Recoder
from .optpkg import optional_package
_, have_scipy, _ = optional_package('scipy')


# mapping of names to classes and class functionality
# Ordered by the load/save priority.
all_image_classes = [Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image,
Spm2AnalyzeImage, Spm99AnalyzeImage, AnalyzeImage,
Minc1Image, Minc2Image, MGHImage,
PARRECImage]


# DEPRECATED: mapping of names to classes and class functionality
class ClassMapDict(dict):
def __getitem__(self, *args, **kwargs):
warnings.warn("class_map is deprecated.", DeprecationWarning,
stacklevel=2)
return super(ClassMapDict, self).__getitem__(*args, **kwargs)

class_map = ClassMapDict(
analyze={'class': AnalyzeImage, # Image class
'ext': '.img', # characteristic image extension
'has_affine': False, # class can store an affine
'makeable': True, # empty image can be easily made in memory
'rw': True}, # image can be written
spm99analyze={'class': Spm99AnalyzeImage,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': have_scipy},
spm2analyze={'class': Spm2AnalyzeImage,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': have_scipy},
nifti_pair={'class': Nifti1Pair,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': True},
nifti_single={'class': Nifti1Image,
'ext': '.nii',
'has_affine': True,
'makeable': True,
'rw': True},
minc={'class': Minc1Image,
'ext': '.mnc',
'has_affine': True,
'makeable': True,
'rw': False},
mgh={'class': MGHImage,
'ext': '.mgh',
'has_affine': True,
'makeable': True,
'rw': True},
mgz={'class': MGHImage,
'ext': '.mgz',
'has_affine': True,
'makeable': True,
'rw': True},
par={'class': PARRECImage,
'ext': '.par',
'has_affine': True,
'makeable': False,
'rw': False})


class_map = {
'analyze': {'class': AnalyzeImage, # Image class
'ext': '.img', # characteristic image extension
'has_affine': False, # class can store an affine
'makeable': True, # empty image can be easily made in memory
'rw': True}, # image can be written
'spm99analyze': {'class': Spm99AnalyzeImage,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': have_scipy},
'spm2analyze': {'class': Spm2AnalyzeImage,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': have_scipy},
'nifti_pair': {'class': Nifti1Pair,
'ext': '.img',
'has_affine': True,
'makeable': True,
'rw': True},
'nifti_single': {'class': Nifti1Image,
'ext': '.nii',
'has_affine': True,
'makeable': True,
'rw': True},
'minc': {'class': Minc1Image,
'ext': '.mnc',
'has_affine': True,
'makeable': True,
'rw': False},
'mgh': {'class': MGHImage,
'ext': '.mgh',
'has_affine': True,
'makeable': True,
'rw': True},
'mgz': {'class': MGHImage,
'ext': '.mgz',
'has_affine': True,
'makeable': True,
'rw': True},
'par': {'class': PARRECImage,
'ext': '.par',
'has_affine': True,
'makeable': False,
'rw': False}}
class ExtMapRecoder(Recoder):
def __getitem__(self, *args, **kwargs):
warnings.warn("ext_map is deprecated.", DeprecationWarning,
stacklevel=2)
return super(ExtMapRecoder, self).__getitem__(*args, **kwargs)

# mapping of extensions to default image class names
ext_map = Recoder((
ext_map = ExtMapRecoder((
('nifti_single', '.nii'),
('nifti_pair', '.img', '.hdr'),
('minc', '.mnc'),
Expand Down
106 changes: 51 additions & 55 deletions nibabel/loadsave.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
""" Utilities to load and save image objects """

import numpy as np
import warnings

from .filename_parser import types_filenames, splitext_addext
from .filename_parser import splitext_addext
from .openers import ImageOpener
from .analyze import AnalyzeImage
from .spm2analyze import Spm2AnalyzeImage
from .nifti1 import Nifti1Image, Nifti1Pair, header_dtype as ni1_hdr_dtype
from .nifti2 import Nifti2Image, Nifti2Pair
from .minc1 import Minc1Image
from .minc2 import Minc2Image
from .freesurfer import MGHImage
from .spatialimages import ImageFileError
from .imageclasses import class_map, ext_map
from .imageclasses import all_image_classes
from .arrayproxy import is_proxy


Expand All @@ -40,9 +34,17 @@ def load(filename, **kwargs):
img : ``SpatialImage``
Image of guessed type
'''
return guessed_image_type(filename).from_filename(filename, **kwargs)
sniff = None
for image_klass in all_image_classes:
is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
if is_valid:
return image_klass.from_filename(filename, **kwargs)

raise ImageFileError('Cannot work out file type of "%s"' %
filename)


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave a stub here? I guess someone may have used this function, given it's got a public API name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think a deprecation warning or return an image class like before, but using the new mechanism? (Or both?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both, I think. Or do you think someone would want this function independent of the load mechanism?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem obvious that they would. If you say deprecate, that works for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just be a stub that tries load and return the class of the result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load is only 7 lines. My inclination is to just replicate it without the .from_filename bit. If we weren't deprecating, I'd have load call it like before, but might as well make the process of removing it easy.

@np.deprecate
def guessed_image_type(filename):
""" Guess image type from file `filename`

Expand All @@ -56,39 +58,16 @@ def guessed_image_type(filename):
image_class : class
Class corresponding to guessed image type
"""
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
lext = ext.lower()
try:
img_type = ext_map[lext]
except KeyError:
raise ImageFileError('Cannot work out file type of "%s"' %
filename)
if lext in ('.mgh', '.mgz', '.par'):
klass = class_map[img_type]['class']
elif lext == '.mnc':
# Look for HDF5 signature for MINC2
# https://www.hdfgroup.org/HDF5/doc/H5.format.html
with ImageOpener(filename) as fobj:
signature = fobj.read(4)
klass = Minc2Image if signature == b'\211HDF' else Minc1Image
elif lext == '.nii':
with ImageOpener(filename) as fobj:
binaryblock = fobj.read(348)
ft = which_analyze_type(binaryblock)
klass = Nifti2Image if ft == 'nifti2' else Nifti1Image
else: # might be nifti 1 or 2 pair or analyze of some sort
files_types = (('image', '.img'), ('header', '.hdr'))
filenames = types_filenames(filename, files_types)
with ImageOpener(filenames['header']) as fobj:
binaryblock = fobj.read(348)
ft = which_analyze_type(binaryblock)
if ft == 'nifti2':
klass = Nifti2Pair
elif ft == 'nifti1':
klass = Nifti1Pair
else:
klass = Spm2AnalyzeImage
return klass
warnings.warn('guessed_image_type is deprecated', DeprecationWarning,
stacklevel=2)
sniff = None
for image_klass in all_image_classes:
is_valid, sniff = image_klass.path_maybe_image(filename, sniff)
if is_valid:
return image_klass

raise ImageFileError('Cannot work out file type of "%s"' %
filename)


def save(img, filename):
Expand All @@ -105,25 +84,38 @@ def save(img, filename):
-------
None
'''

# Save the type as expected
try:
img.to_filename(filename)
except ImageFileError:
pass
else:
return

# Be nice to users by making common implicit conversions
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
lext = ext.lower()

# Special-case Nifti singles and Pairs
if type(img) == Nifti1Image and ext in ('.img', '.hdr'):
# Inline imports, as this module really shouldn't reference any image type
from .nifti1 import Nifti1Image, Nifti1Pair
from .nifti2 import Nifti2Image, Nifti2Pair
if type(img) == Nifti1Image and lext in ('.img', '.hdr'):
klass = Nifti1Pair
elif type(img) == Nifti2Image and ext in ('.img', '.hdr'):
elif type(img) == Nifti2Image and lext in ('.img', '.hdr'):
klass = Nifti2Pair
elif type(img) == Nifti1Pair and ext == '.nii':
elif type(img) == Nifti1Pair and lext == '.nii':
klass = Nifti1Image
elif type(img) == Nifti2Pair and ext == '.nii':
elif type(img) == Nifti2Pair and lext == '.nii':
klass = Nifti2Image
else:
img_type = ext_map[ext]
klass = class_map[img_type]['class']
else: # arbitrary conversion
valid_klasses = [klass for klass in all_image_classes
if ext in klass.valid_exts]
if not valid_klasses: # if list is empty
raise ImageFileError('Cannot work out file type of "%s"' %
filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next(iter(filter(...))) is awkward. Alternatives:

valid_klasses = list(filter(lambda klass: klass.is_valid_extension(ext), all_image_classes))
valid_klasses = [klass for klass in all_image_classes if klass.is_valid_extension(ext)]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go for the second, since we want a list anyway...

klass = valid_klasses[0]
converted = klass.from_image(img)
converted.to_filename(filename)

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


@np.deprecate
def which_analyze_type(binaryblock):
""" Is `binaryblock` from NIfTI1, NIfTI2 or Analyze header?

Expand Down Expand Up @@ -241,13 +234,16 @@ def which_analyze_type(binaryblock):
* if ``sizeof_hdr`` is 348 or byteswapped 348 assume Analyze
* Return None
"""
hdr = np.ndarray(shape=(), dtype=ni1_hdr_dtype, buffer=binaryblock)
bs_hdr = hdr.byteswap()
sizeof_hdr = hdr['sizeof_hdr']
bs_sizeof_hdr = bs_hdr['sizeof_hdr']
warnings.warn('which_analyze_type is deprecated', DeprecationWarning,
stacklevel=2)
from .nifti1 import header_dtype
hdr_struct = np.ndarray(shape=(), dtype=header_dtype, buffer=binaryblock)
bs_hdr_struct = hdr_struct.byteswap()
sizeof_hdr = hdr_struct['sizeof_hdr']
bs_sizeof_hdr = bs_hdr_struct['sizeof_hdr']
if 540 in (sizeof_hdr, bs_sizeof_hdr):
return 'nifti2'
if hdr['magic'] in (b'ni1', b'n+1'):
if hdr_struct['magic'] in (b'ni1', b'n+1'):
return 'nifti1'
if 348 in (sizeof_hdr, bs_sizeof_hdr):
return 'analyze'
Expand Down
Loading