Skip to content

Commit 2741f88

Browse files
committed
RF: Begin refactoring load into image classes
1 parent cc52223 commit 2741f88

File tree

11 files changed

+111
-156
lines changed

11 files changed

+111
-156
lines changed

nibabel/freesurfer/mghformat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from os.path import splitext
1414
import numpy as np
1515

16+
from ..imageglobals import valid_exts
1617
from ..volumeutils import (array_to_file, array_from_file, Recoder)
1718
from ..spatialimages import HeaderDataError, SpatialImage
1819
from ..fileholders import FileHolder, copy_file_map
@@ -454,6 +455,7 @@ def writeftr_to(self, fileobj):
454455
fileobj.write(ftr_nd.tostring())
455456

456457

458+
@valid_exts('.mgh', '.mgz')
457459
class MGHImage(SpatialImage):
458460
""" Class for MGH format image
459461
"""

nibabel/imageglobals.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ def __enter__(self):
5858
def __exit__(self, exc, value, tb):
5959
for handler in self.orig_handlers:
6060
logger.addHandler(handler)
61+
62+
IMAGE_MAP = {}
63+
64+
65+
def valid_exts(*exts):
66+
def decorate(klass):
67+
for ext in exts:
68+
IMAGE_MAP.setdefault(ext, []).append(klass)
69+
return klass
70+
return decorate

nibabel/loadsave.py

Lines changed: 15 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,13 @@
1111

1212
import numpy as np
1313

14-
from .filename_parser import types_filenames, splitext_addext
15-
from .volumeutils import BinOpener, Opener
16-
from .analyze import AnalyzeImage
17-
from .spm2analyze import Spm2AnalyzeImage
18-
from .nifti1 import Nifti1Image, Nifti1Pair, header_dtype as ni1_hdr_dtype
14+
from .filename_parser import splitext_addext
15+
from .volumeutils import BinOpener
16+
from .nifti1 import Nifti1Image, Nifti1Pair
1917
from .nifti2 import Nifti2Image, Nifti2Pair
20-
from .minc1 import Minc1Image
21-
from .minc2 import Minc2Image
22-
from .freesurfer import MGHImage
23-
from .fileholders import FileHolderError
2418
from .spatialimages import ImageFileError
2519
from .imageclasses import class_map, ext_map
20+
from .imageglobals import IMAGE_MAP
2621
from .arrayproxy import is_proxy
2722

2823

@@ -41,55 +36,20 @@ def load(filename, **kwargs):
4136
img : ``SpatialImage``
4237
Image of guessed type
4338
'''
44-
return guessed_image_type(filename).from_filename(filename, **kwargs)
4539

46-
47-
def guessed_image_type(filename):
48-
""" Guess image type from file `filename`
49-
50-
Parameters
51-
----------
52-
filename : str
53-
File name containing an image
54-
55-
Returns
56-
-------
57-
image_class : class
58-
Class corresponding to guessed image type
59-
"""
6040
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
6141
lext = ext.lower()
62-
try:
63-
img_type = ext_map[lext]
64-
except KeyError:
65-
raise ImageFileError('Cannot work out file type of "%s"' %
66-
filename)
67-
if lext in ('.mgh', '.mgz', '.par'):
68-
klass = class_map[img_type]['class']
69-
elif lext == '.mnc':
70-
# Look for HDF5 signature for MINC2
71-
# https://www.hdfgroup.org/HDF5/doc/H5.format.html
72-
with Opener(filename) as fobj:
73-
signature = fobj.read(4)
74-
klass = Minc2Image if signature == b'\211HDF' else Minc1Image
75-
elif lext == '.nii':
76-
with BinOpener(filename) as fobj:
77-
binaryblock = fobj.read(348)
78-
ft = which_analyze_type(binaryblock)
79-
klass = Nifti2Image if ft == 'nifti2' else Nifti1Image
80-
else: # might be nifti 1 or 2 pair or analyze of some sort
81-
files_types = (('image','.img'), ('header','.hdr'))
82-
filenames = types_filenames(filename, files_types)
83-
with BinOpener(filenames['header']) as fobj:
84-
binaryblock = fobj.read(348)
85-
ft = which_analyze_type(binaryblock)
86-
if ft == 'nifti2':
87-
klass = Nifti2Pair
88-
elif ft == 'nifti1':
89-
klass = Nifti1Pair
90-
else:
91-
klass = Spm2AnalyzeImage
92-
return klass
42+
potential_classes = IMAGE_MAP[lext]
43+
44+
if len(potential_classes) == 1:
45+
return potential_classes[0].from_filename(filename, **kwargs)
46+
47+
# Allow image tests to cache data
48+
sniff = None
49+
for img_type in IMAGE_MAP[lext]:
50+
is_valid, sniff = img_type.is_image(filename, sniff)
51+
if is_valid:
52+
return img_type.from_filename(filename, **kwargs)
9353

9454

9555
def save(img, filename):
@@ -213,43 +173,3 @@ def read_img_data(img, prefer='scaled'):
213173
if prefer == 'scaled':
214174
return hdr.data_from_fileobj(fileobj)
215175
return hdr.raw_data_from_fileobj(fileobj)
216-
217-
218-
def which_analyze_type(binaryblock):
219-
""" Is `binaryblock` from NIfTI1, NIfTI2 or Analyze header?
220-
221-
Parameters
222-
----------
223-
binaryblock : bytes
224-
The `binaryblock` is 348 bytes that might be NIfTI1, NIfTI2, Analyze, or
225-
None of the the above.
226-
227-
Returns
228-
-------
229-
hdr_type : str
230-
* a nifti1 header (pair or single) -> return 'nifti1'
231-
* a nifti2 header (pair or single) -> return 'nifti2'
232-
* an Analyze header -> return 'analyze'
233-
* None of the above -> return None
234-
235-
Notes
236-
-----
237-
Algorithm:
238-
239-
* read in the first 4 bytes from the file as 32-bit int ``sizeof_hdr``
240-
* if ``sizeof_hdr`` is 540 or byteswapped 540 -> assume nifti2
241-
* Check for 'ni1', 'n+1' magic -> assume nifti1
242-
* if ``sizeof_hdr`` is 348 or byteswapped 348 assume Analyze
243-
* Return None
244-
"""
245-
hdr = np.ndarray(shape=(), dtype=ni1_hdr_dtype, buffer=binaryblock)
246-
bs_hdr = hdr.byteswap()
247-
sizeof_hdr = hdr['sizeof_hdr']
248-
bs_sizeof_hdr = bs_hdr['sizeof_hdr']
249-
if 540 in (sizeof_hdr, bs_sizeof_hdr):
250-
return 'nifti2'
251-
if hdr['magic'] in (b'ni1', b'n+1'):
252-
return 'nifti1'
253-
if 348 in (sizeof_hdr, bs_sizeof_hdr):
254-
return 'analyze'
255-
return None

nibabel/minc1.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414

1515
from .externals.netcdf import netcdf_file
1616

17+
from .filename_parser import splitext_addext
18+
from .imageglobals import valid_exts
1719
from .spatialimages import Header, SpatialImage
1820
from .fileslice import canonical_slicers
21+
from .volumeutils import BinOpener
1922

2023
from .deprecated import FutureWarningMixin
2124

@@ -280,6 +283,7 @@ def data_from_fileobj(self, fileobj):
280283
raise NotImplementedError
281284

282285

286+
@valid_exts('.mnc')
283287
class Minc1Image(SpatialImage):
284288
''' Class for MINC1 format images
285289
@@ -307,6 +311,26 @@ def from_file_map(klass, file_map):
307311
data = klass.ImageArrayProxy(minc_file)
308312
return klass(data, affine, header, extra=None, file_map=file_map)
309313

314+
@classmethod
315+
def is_image(klass, filename, sniff=None):
316+
ftypes = dict(klass.files_types)
317+
froot, ext, trailing = splitext_addext(filename, klass._compressed_exts)
318+
lext = ext.lower()
319+
320+
if lext not in ftypes.values():
321+
return False, sniff
322+
323+
fname = froot + ftypes['header'] if 'header' in ftypes else filename
324+
if not sniff:
325+
with BinOpener(fname, 'rb') as fobj:
326+
sniff = fobj.read(4)
327+
328+
return klass._minctest(sniff), sniff
329+
330+
@classmethod
331+
def _minctest(klass, binaryblock):
332+
return binaryblock != b'\211HDF'
333+
310334

311335
load = Minc1Image.load
312336

nibabel/minc2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .optpkg import optional_package
3131
h5py, have_h5py, setup_module = optional_package('h5py')
3232

33+
from .imageglobals import valid_exts
3334
from .minc1 import Minc1File, Minc1Image, MincError
3435

3536

@@ -134,6 +135,7 @@ def get_scaled_data(self, sliceobj=()):
134135
return self._normalize(raw_data, sliceobj)
135136

136137

138+
@valid_exts('.mnc')
137139
class Minc2Image(Minc1Image):
138140
''' Class for MINC2 images
139141
@@ -160,5 +162,9 @@ def from_file_map(klass, file_map):
160162
data = klass.ImageArrayProxy(minc_file)
161163
return klass(data, affine, header, extra=None, file_map=file_map)
162164

165+
@classmethod
166+
def _minctest(klass, binaryblock):
167+
return binaryblock[:4] == b'\211HDF'
168+
163169

164170
load = Minc2Image.load

nibabel/nifti1.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from .py3k import asstr
2020
from .volumeutils import Recoder, make_dt_codes, endian_codes
21+
from .imageglobals import valid_exts
2122
from .spatialimages import HeaderDataError, ImageFileError
2223
from .batteryrunners import Report
2324
from .quaternions import fillpositive, quat2mat, mat2quat
@@ -1592,13 +1593,20 @@ def _chk_xform_code(klass, code_type, hdr, fix):
15921593
rep.fix_msg = 'setting to 0'
15931594
return hdr, rep
15941595

1596+
@classmethod
1597+
def is_header(klass, binaryblock):
1598+
hdr = np.ndarray(shape=(), dtype=header_dtype,
1599+
buffer=binaryblock[:348])
1600+
return hdr['magic'] in (b'ni1', b'n+1')
1601+
15951602

15961603
class Nifti1PairHeader(Nifti1Header):
15971604
''' Class for NIfTI1 pair header '''
15981605
# Signal whether this is single (header + data) file
15991606
is_single = False
16001607

16011608

1609+
@valid_exts('.img', '.hdr')
16021610
class Nifti1Pair(analyze.AnalyzeImage):
16031611
""" Class for NIfTI1 format image, header pair
16041612
"""
@@ -1822,6 +1830,7 @@ def set_sform(self, affine, code=None, **kwargs):
18221830
self._affine[:] = self._header.get_best_affine()
18231831

18241832

1833+
@valid_exts('.nii')
18251834
class Nifti1Image(Nifti1Pair):
18261835
""" Class for single file NIfTI1 format image
18271836
"""

nibabel/nifti2.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import numpy as np
2121

2222
from .analyze import AnalyzeHeader
23+
from .imageglobals import valid_exts
2324
from .batteryrunners import Report
2425
from .spatialimages import HeaderDataError, ImageFileError
2526
from .nifti1 import Nifti1Header, Nifti1Pair, Nifti1Image
@@ -221,19 +222,32 @@ def _chk_eol_check(hdr, fix=False):
221222
rep.fix_msg = 'setting EOL check to 13, 10, 26, 10'
222223
return hdr, rep
223224

225+
@classmethod
226+
def is_header(klass, binaryblock):
227+
if len(binaryblock) < 540:
228+
return False
229+
230+
hdr = np.ndarray(shape=(), dtype=header_dtype,
231+
buffer=binaryblock[:540])
232+
bs_hdr = hdr.byteswap()
233+
return 540 in (hdr['sizeof_hdr'], bs_hdr['sizeof_hdr'])
234+
235+
224236

225237
class Nifti2PairHeader(Nifti2Header):
226238
''' Class for NIfTI2 pair header '''
227239
# Signal whether this is single (header + data) file
228240
is_single = False
229241

230242

243+
@valid_exts('.img', '.hdr')
231244
class Nifti2Pair(Nifti1Pair):
232245
""" Class for NIfTI2 format image, header pair
233246
"""
234247
header_class = Nifti2PairHeader
235248

236249

250+
@valid_exts('.nii')
237251
class Nifti2Image(Nifti1Image):
238252
""" Class for single file NIfTI2 format image
239253
"""

nibabel/parrec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
from .spatialimages import SpatialImage, Header
101101
from .eulerangles import euler2mat
102102
from .volumeutils import Recoder, array_from_file, BinOpener
103+
from .imageglobals import valid_exts
103104
from .affines import from_matvec, dot_reduce, apply_affine
104105
from .nifti1 import unit_codes
105106
from .fileslice import fileslice, strided_scalar
@@ -975,6 +976,7 @@ def get_sorted_slice_indices(self):
975976
return np.lexsort(keys)[:n_used]
976977

977978

979+
@valid_exts('.par', '.rec')
978980
class PARRECImage(SpatialImage):
979981
"""PAR/REC image"""
980982
header_class = PARRECHeader

nibabel/spatialimages.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,10 @@
141141

142142
import numpy as np
143143

144-
from .filename_parser import types_filenames, TypesFilenamesError
144+
from .filename_parser import types_filenames, TypesFilenamesError, \
145+
splitext_addext
145146
from .fileholders import FileHolder
146-
from .volumeutils import shape_zoom_affine
147+
from .volumeutils import shape_zoom_affine, BinOpener
147148

148149

149150
class HeaderDataError(Exception):
@@ -866,6 +867,22 @@ def from_image(klass, img):
866867
klass.header_class.from_header(img.header),
867868
extra=img.extra.copy())
868869

870+
@classmethod
871+
def is_image(klass, filename, sniff=None):
872+
ftypes = dict(klass.files_types)
873+
froot, ext, trailing = splitext_addext(filename, ('.gz', '.bz2'))
874+
lext = ext.lower()
875+
876+
if lext not in ftypes.values():
877+
return False, sniff
878+
879+
fname = froot + ftypes['header'] if 'header' in ftypes else filename
880+
if not sniff:
881+
with BinOpener(fname, 'rb') as fobj:
882+
sniff = fobj.read(1024)
883+
884+
return klass.header_class.is_header(sniff), sniff
885+
869886
def __getitem__(self):
870887
''' No slicing or dictionary interface for images
871888
'''

nibabel/spm2analyze.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import numpy as np
1111

1212
from .spatialimages import HeaderDataError
13+
from .imageglobals import valid_exts
1314
from .batteryrunners import Report
1415
from . import spm99analyze as spm99 # module import
1516

@@ -114,7 +115,16 @@ def get_slope_inter(self):
114115
return slope, inter
115116
return None, None
116117

118+
@classmethod
119+
def is_header(klass, binaryblock):
120+
hdr = np.ndarray(shape=(), dtype=header_dtype,
121+
buffer=binaryblock[:348])
122+
bs_hdr = hdr.byteswap()
123+
return (binaryblock[344:348] not in (b'ni1\x00', b'n+1\x00') and
124+
348 in (hdr['sizeof_hdr'], bs_hdr['sizeof_hdr']))
117125

126+
127+
@valid_exts('.img', '.hdr')
118128
class Spm2AnalyzeImage(spm99.Spm99AnalyzeImage):
119129
""" Class for SPM2 variant of basic Analyze image
120130
"""

0 commit comments

Comments
 (0)