Skip to content

WIP: loadsave refactoring #319

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

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions nibabel/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,9 @@ class AnalyzeImage(SpatialImage):
files_types = (('image', '.img'), ('header', '.hdr'))
_compressed_exts = ('.gz', '.bz2')

makeable = True
rw = True

ImageArrayProxy = ArrayProxy

def __init__(self, dataobj, affine, header=None,
Expand Down
2 changes: 2 additions & 0 deletions nibabel/filename_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def types_filenames(template_fname, types_exts,
elif found_ext == found_ext.lower():
proc_ext = lambda s: s.lower()
for name, ext in types_exts:
if name in tfns: # Allow multipe definitions of image, header, etc,
continue # giving priority to those found first.
if name == direct_set_name:
tfns[name] = template_fname
continue
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,14 +454,19 @@ def writeftr_to(self, fileobj):
fileobj.write(ftr_nd.tostring())


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

makeable = True
rw = True

ImageArrayProxy = ArrayProxy

@classmethod
Expand Down
116 changes: 68 additions & 48 deletions nibabel/imageclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,69 +7,89 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
''' 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)
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)
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
134 changes: 30 additions & 104 deletions nibabel/loadsave.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,10 @@

import numpy as np

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,55 +33,15 @@ 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.is_image(filename, sniff)
if is_valid:
return image_klass.from_filename(filename, **kwargs)

def guessed_image_type(filename):
""" Guess image type from file `filename`

Parameters
----------
filename : str
File name containing an image

Returns
-------
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
raise ImageFileError('Cannot work out file type of "%s"' %
filename)


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

# Save the type as expected
try:
img.to_filename(filename)
except ImageFileError:
pass
else:
return
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))

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

# Special-case Nifti singles and Pairs
if type(img) == Nifti1Image and ext in ('.img', '.hdr'):
from .nifti1 import Nifti1Image, Nifti1Pair # Inline imports, as this module
from .nifti2 import Nifti2Image, Nifti2Pair # really shouldn't reference any image type
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 klass.is_valid_extension(ext)]
try:
klass = valid_klasses[0]
except IndexError: # if list is empty
raise ImageFileError('Cannot work out file type of "%s"' %
filename)
converted = klass.from_image(img)
converted.to_filename(filename)

Expand Down Expand Up @@ -212,43 +178,3 @@ def read_img_data(img, prefer='scaled'):
if prefer == 'scaled':
return hdr.data_from_fileobj(fileobj)
return hdr.raw_data_from_fileobj(fileobj)


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

Parameters
----------
binaryblock : bytes
The `binaryblock` is 348 bytes that might be NIfTI1, NIfTI2, Analyze,
or None of the the above.

Returns
-------
hdr_type : str
* a nifti1 header (pair or single) -> return 'nifti1'
* a nifti2 header (pair or single) -> return 'nifti2'
* an Analyze header -> return 'analyze'
* None of the above -> return None

Notes
-----
Algorithm:

* read in the first 4 bytes from the file as 32-bit int ``sizeof_hdr``
* if ``sizeof_hdr`` is 540 or byteswapped 540 -> assume nifti2
* Check for 'ni1', 'n+1' magic -> assume nifti1
* 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']
if 540 in (sizeof_hdr, bs_sizeof_hdr):
return 'nifti2'
if hdr['magic'] in (b'ni1', b'n+1'):
return 'nifti1'
if 348 in (sizeof_hdr, bs_sizeof_hdr):
return 'analyze'
return None
Loading