1""" Validate image API
2
3What is the image API?
4
5* ``img.dataobj``
6
7    * Returns ``np.ndarray`` from ``np.array(img.databj)``
8    * Has attribute ``shape``
9
10* ``img.header`` (image metadata) (changes in the image metadata should not
11  change any of ``dataobj``, ``affine``, ``shape``)
12* ``img.affine`` (4x4 float ``np.ndarray`` relating spatial voxel coordinates
13  to world space)
14* ``img.shape`` (shape of data as read with ``np.array(img.dataobj)``
15* ``img.get_fdata()`` (returns floating point data as read with
16  ``np.array(img.dataobj)`` and the cast to float);
17* ``img.uncache()`` (``img.get_fdata()`` (recommended) and ``img.get_data()``
18  (deprecated) are allowed to cache the result of the array creation.  If they
19  do, this call empties that cache.  Implement this as a no-op if
20  ``get_fdata()``, ``get_data()`` do not cache.)
21* ``img[something]`` generates an informative TypeError
22* ``img.in_memory`` is True for an array image, and for a proxy image that is
23  cached, but False otherwise.
24"""
25
26import warnings
27from functools import partial
28from itertools import product
29import pathlib
30
31import numpy as np
32
33from ..optpkg import optional_package
34_, have_scipy, _ = optional_package('scipy')
35from .._h5py_compat import have_h5py
36
37from .. import (AnalyzeImage, Spm99AnalyzeImage, Spm2AnalyzeImage,
38                Nifti1Pair, Nifti1Image, Nifti2Pair, Nifti2Image,
39                GiftiImage,
40                MGHImage, Minc1Image, Minc2Image, is_proxy)
41from ..spatialimages import SpatialImage
42from .. import minc1, minc2, parrec, brikhead
43
44import unittest
45import pytest
46
47from numpy.testing import assert_almost_equal, assert_array_equal, assert_warns, assert_allclose
48from nibabel.testing import (bytesio_round_trip, bytesio_filemap, assert_data_similar,
49                             clear_and_catch_warnings, nullcontext)
50from ..tmpdirs import InTemporaryDirectory
51from ..deprecator import ExpiredDeprecationError
52
53from .test_api_validators import ValidateAPI
54from .test_minc1 import EXAMPLE_IMAGES as MINC1_EXAMPLE_IMAGES
55from .test_minc2 import EXAMPLE_IMAGES as MINC2_EXAMPLE_IMAGES
56from .test_parrec import EXAMPLE_IMAGES as PARREC_EXAMPLE_IMAGES
57from .test_brikhead import EXAMPLE_IMAGES as AFNI_EXAMPLE_IMAGES
58
59
60def maybe_deprecated(meth_name):
61    return pytest.deprecated_call() if meth_name == 'get_data' else nullcontext()
62
63
64class GenericImageAPI(ValidateAPI):
65    """ General image validation API """
66    # Whether this image type can do scaling of data
67    has_scaling = False
68    # Whether the image can be saved to disk / file objects
69    can_save = False
70    # Filename extension to which to save image; only used if `can_save` is
71    # True
72    standard_extension = '.img'
73
74    def obj_params(self):
75        """ Return generator returning (`img_creator`, `img_params`) tuples
76
77        ``img_creator`` is a function taking no arguments and returning a fresh
78        image.  We need to return this ``img_creator`` function rather than an
79        image instance so we can recreate the images fresh for each of multiple
80        tests run from the ``validate_xxx`` autogenerated test methods.  This
81        allows the tests to modify the image without having an effect on the
82        later tests in the same function, because each test will create a fresh
83        image with ``img_creator``.
84
85        Returns
86        -------
87        func_params_gen : generator
88            Generator returning tuples with:
89
90            * img_creator : callable
91              Callable returning a fresh image for testing
92            * img_params : mapping
93              Expected properties of image returned from ``img_creator``
94              callable.  Key, value pairs should include:
95
96              * ``data`` : array returned from ``get_fdata()`` on image - OR -
97                ``data_summary`` : dict with data ``min``, ``max``, ``mean``;
98              * ``shape`` : shape of image;
99              * ``affine`` : shape (4, 4) affine array for image;
100              * ``dtype`` : dtype of data returned from ``np.asarray(dataobj)``;
101              * ``is_proxy`` : bool, True if image data is proxied;
102
103        Notes
104        -----
105        Passing ``data_summary`` instead of ``data`` allows you gentle user to
106        avoid having to have a saved copy of the entire data array from example
107        images for testing.
108        """
109        raise NotImplementedError
110
111    def validate_header(self, imaker, params):
112        # Check header API
113        img = imaker()
114        hdr = img.header  # we can fetch it
115        # Read only
116        with pytest.raises(AttributeError):
117            img.header = hdr
118
119    def validate_header_deprecated(self, imaker, params):
120        # Check deprecated header API
121        img = imaker()
122        with pytest.deprecated_call():
123            hdr = img.get_header()
124        assert hdr is img.header
125
126    def validate_filenames(self, imaker, params):
127        # Validate the filename, file_map interface
128
129        if not self.can_save:
130            raise unittest.SkipTest
131        img = imaker()
132        img.set_data_dtype(np.float32)  # to avoid rounding in load / save
133        # Make sure the object does not have a file_map
134        img.file_map = None
135        # The bytesio_round_trip helper tests bytesio load / save via file_map
136        rt_img = bytesio_round_trip(img)
137        assert_array_equal(img.shape, rt_img.shape)
138        assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
139        assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
140        # Give the image a file map
141        klass = type(img)
142        rt_img.file_map = bytesio_filemap(klass)
143        # This object can now be saved and loaded from its own file_map
144        rt_img.to_file_map()
145        rt_rt_img = klass.from_file_map(rt_img.file_map)
146        assert_almost_equal(img.get_fdata(), rt_rt_img.get_fdata())
147        assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
148        # get_ / set_ filename
149        fname = 'an_image' + self.standard_extension
150        for path in (fname, pathlib.Path(fname)):
151            img.set_filename(path)
152            assert img.get_filename() == str(path)
153            assert img.file_map['image'].filename == str(path)
154        # to_ / from_ filename
155        fname = 'another_image' + self.standard_extension
156        for path in (fname, pathlib.Path(fname)):
157          with InTemporaryDirectory():
158              # Validate that saving or loading a file doesn't use deprecated methods internally
159              with clear_and_catch_warnings() as w:
160                  warnings.filterwarnings('error',
161                                          category=DeprecationWarning,
162                                          module=r"nibabel.*")
163                  img.to_filename(path)
164                  rt_img = img.__class__.from_filename(path)
165              assert_array_equal(img.shape, rt_img.shape)
166              assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
167              assert_almost_equal(np.asanyarray(img.dataobj), np.asanyarray(rt_img.dataobj))
168              del rt_img  # to allow windows to delete the directory
169
170    def validate_no_slicing(self, imaker, params):
171        img = imaker()
172        with pytest.raises(TypeError):
173            img['string']
174        with pytest.raises(TypeError):
175            img[:]
176
177    def validate_get_data_deprecated(self, imaker, params):
178        # Check deprecated header API
179        img = imaker()
180        with pytest.deprecated_call():
181            data = img.get_data()
182        assert_array_equal(np.asanyarray(img.dataobj), data)
183
184
185class GetSetDtypeMixin(object):
186    """ Adds dtype tests
187
188    Add this one if your image has ``get_data_dtype`` and ``set_data_dtype``.
189    """
190
191    def validate_dtype(self, imaker, params):
192        # data / storage dtype
193        img = imaker()
194        # Need to rename this one
195        assert img.get_data_dtype().type == params['dtype']
196        # dtype survives round trip
197        if self.has_scaling and self.can_save:
198            with np.errstate(invalid='ignore'):
199                rt_img = bytesio_round_trip(img)
200            assert rt_img.get_data_dtype().type == params['dtype']
201        # Setting to a different dtype
202        img.set_data_dtype(np.float32)  # assumed supported for all formats
203        assert img.get_data_dtype().type == np.float32
204        # dtype survives round trip
205        if self.can_save:
206            rt_img = bytesio_round_trip(img)
207            assert rt_img.get_data_dtype().type == np.float32
208
209
210class DataInterfaceMixin(GetSetDtypeMixin):
211    """ Test dataobj interface for images with array backing
212
213    Use this mixin if your image has a ``dataobj`` property that contains an
214    array or an array-like thing.
215    """
216    meth_names = ('get_fdata', 'get_data')
217
218    def validate_data_interface(self, imaker, params):
219        # Check get data returns array, and caches
220        img = imaker()
221        assert img.shape == img.dataobj.shape
222        assert img.ndim == len(img.shape)
223        assert_data_similar(img.dataobj, params)
224        for meth_name in self.meth_names:
225            if params['is_proxy']:
226                self._check_proxy_interface(imaker, meth_name)
227            else:  # Array image
228                self._check_array_interface(imaker, meth_name)
229            method = getattr(img, meth_name)
230            # Data shape is same as image shape
231            with maybe_deprecated(meth_name):
232                assert img.shape == method().shape
233            # Data ndim is same as image ndim
234            with maybe_deprecated(meth_name):
235                assert img.ndim == method().ndim
236            # Values to get_data caching parameter must be 'fill' or
237            # 'unchanged'
238            with maybe_deprecated(meth_name), pytest.raises(ValueError):
239                method(caching='something')
240        # dataobj is read only
241        fake_data = np.zeros(img.shape).astype(img.get_data_dtype())
242        with pytest.raises(AttributeError):
243            img.dataobj = fake_data
244        # So is in_memory
245        with pytest.raises(AttributeError):
246            img.in_memory = False
247
248    def _check_proxy_interface(self, imaker, meth_name):
249        # Parameters assert this is an array proxy
250        img = imaker()
251        # Does is_proxy agree?
252        assert is_proxy(img.dataobj)
253        # Confirm it is not a numpy array
254        assert not isinstance(img.dataobj, np.ndarray)
255        # Confirm it can be converted to a numpy array with asarray
256        proxy_data = np.asarray(img.dataobj)
257        proxy_copy = proxy_data.copy()
258        # Not yet cached, proxy image: in_memory is False
259        assert not img.in_memory
260        # Load with caching='unchanged'
261        method = getattr(img, meth_name)
262        with maybe_deprecated(meth_name):
263            data = method(caching='unchanged')
264        # Still not cached
265        assert not img.in_memory
266        # Default load, does caching
267        with maybe_deprecated(meth_name):
268            data = method()
269        # Data now cached. in_memory is True if either of the get_data
270        # or get_fdata caches are not-None
271        assert img.in_memory
272        # We previously got proxy_data from disk, but data, which we
273        # have just fetched, is a fresh copy.
274        assert not proxy_data is data
275        # asarray on dataobj, applied above, returns same numerical
276        # values.  This might not be true get_fdata operating on huge
277        # integers, but lets assume that's not true here.
278        assert_array_equal(proxy_data, data)
279        # Now caching='unchanged' does nothing, returns cached version
280        with maybe_deprecated(meth_name):
281            data_again = method(caching='unchanged')
282        assert data is data_again
283        # caching='fill' does nothing because the cache is already full
284        with maybe_deprecated(meth_name):
285            data_yet_again = method(caching='fill')
286        assert data is data_yet_again
287        # changing array data does not change proxy data, or reloaded
288        # data
289        data[:] = 42
290        assert_array_equal(proxy_data, proxy_copy)
291        assert_array_equal(np.asarray(img.dataobj), proxy_copy)
292        # It does change the result of get_data
293        with maybe_deprecated(meth_name):
294            assert_array_equal(method(), 42)
295        # until we uncache
296        img.uncache()
297        # Which unsets in_memory
298        assert not img.in_memory
299        with maybe_deprecated(meth_name):
300            assert_array_equal(method(), proxy_copy)
301        # Check caching='fill' does cache data
302        img = imaker()
303        method = getattr(img, meth_name)
304        assert not img.in_memory
305        with maybe_deprecated(meth_name):
306            data = method(caching='fill')
307        assert img.in_memory
308        with maybe_deprecated(meth_name):
309            data_again = method()
310        assert data is data_again
311        # Check the interaction of caching with get_data, get_fdata.
312        # Caching for `get_data` should have no effect on caching for
313        # get_fdata, and vice versa.
314        # Modify the cached data
315        data[:] = 43
316        # Load using the other data fetch method
317        other_name = set(self.meth_names).difference({meth_name}).pop()
318        other_method = getattr(img, other_name)
319        with maybe_deprecated(other_name):
320            other_data = other_method()
321        # We get the original data, not the modified cache
322        assert_array_equal(proxy_data, other_data)
323        assert not np.all(data == other_data)
324        # We can modify the other cache, without affecting the first
325        other_data[:] = 44
326        with maybe_deprecated(other_name):
327            assert_array_equal(other_method(), 44)
328        with pytest.deprecated_call():
329            assert not np.all(method() == other_method())
330        if meth_name != 'get_fdata':
331            return
332        # Check that caching refreshes for new floating point type.
333        img.uncache()
334        fdata = img.get_fdata()
335        assert fdata.dtype == np.float64
336        fdata[:] = 42
337        fdata_back = img.get_fdata()
338        assert_array_equal(fdata_back, 42)
339        assert fdata_back.dtype == np.float64
340        # New data dtype, no caching, doesn't use or alter cache
341        fdata_new_dt = img.get_fdata(caching='unchanged', dtype='f4')
342        # We get back the original read, not the modified cache
343        # Allow for small rounding error when the data is scaled with 32-bit
344        # factors, rather than 64-bit factors and then cast to float-32
345        # Use rtol/atol from numpy.allclose
346        assert_allclose(fdata_new_dt, proxy_data.astype('f4'), rtol=1e-05, atol=1e-08)
347        assert fdata_new_dt.dtype == np.float32
348        # The original cache stays in place, for default float64
349        assert_array_equal(img.get_fdata(), 42)
350        # And for not-default float32, because we haven't cached
351        fdata_new_dt[:] = 43
352        fdata_new_dt = img.get_fdata(caching='unchanged', dtype='f4')
353        assert_allclose(fdata_new_dt, proxy_data.astype('f4'), rtol=1e-05, atol=1e-08)
354        # Until we reset with caching='fill', at which point we
355        # drop the original float64 cache, and have a float32 cache
356        fdata_new_dt = img.get_fdata(caching='fill', dtype='f4')
357        assert_allclose(fdata_new_dt, proxy_data.astype('f4'), rtol=1e-05, atol=1e-08)
358        # We're using the cache, for dtype='f4' reads
359        fdata_new_dt[:] = 43
360        assert_array_equal(img.get_fdata(dtype='f4'), 43)
361        # We've lost the cache for float64 reads (no longer 42)
362        assert_array_equal(img.get_fdata(), proxy_data)
363
364    def _check_array_interface(self, imaker, meth_name):
365        for caching in (None, 'fill', 'unchanged'):
366            self._check_array_caching(imaker, meth_name, caching)
367
368    def _check_array_caching(self, imaker, meth_name, caching):
369        img = imaker()
370        method = getattr(img, meth_name)
371        get_data_func = (method if caching is None else
372                         partial(method, caching=caching))
373        assert isinstance(img.dataobj, np.ndarray)
374        assert img.in_memory
375        with maybe_deprecated(meth_name):
376            data = get_data_func()
377        # Returned data same object as underlying dataobj if using
378        # old ``get_data`` method, or using newer ``get_fdata``
379        # method, where original array was float64.
380        arr_dtype = img.dataobj.dtype
381        dataobj_is_data = arr_dtype == np.float64 or method == img.get_data
382        # Set something to the output array.
383        data[:] = 42
384        with maybe_deprecated(meth_name):
385            get_result_changed = np.all(get_data_func() == 42)
386        assert get_result_changed == (dataobj_is_data or caching != 'unchanged')
387        if dataobj_is_data:
388            assert data is img.dataobj
389            # Changing array data changes
390            # data
391            assert_array_equal(np.asarray(img.dataobj), 42)
392            # Uncache has no effect
393            img.uncache()
394            with maybe_deprecated(meth_name):
395                assert_array_equal(get_data_func(), 42)
396        else:
397            assert not data is img.dataobj
398            assert not np.all(np.asarray(img.dataobj) == 42)
399            # Uncache does have an effect
400            img.uncache()
401            with maybe_deprecated(meth_name):
402                assert not np.all(get_data_func() == 42)
403        # in_memory is always true for array images, regardless of
404        # cache state.
405        img.uncache()
406        assert img.in_memory
407        if meth_name != 'get_fdata':
408            return
409        # Return original array from get_fdata only if the input array is the
410        # requested dtype.
411        float_types = np.sctypes['float']
412        if arr_dtype not in float_types:
413            return
414        for float_type in float_types:
415            with maybe_deprecated(meth_name):
416                data = get_data_func(dtype=float_type)
417            assert (data is img.dataobj) == (arr_dtype == float_type)
418
419    def validate_data_deprecated(self, imaker, params):
420        # Check _data property still exists, but raises warning
421        img = imaker()
422        with pytest.deprecated_call():
423            assert_data_similar(img._data, params)
424        # Check setting _data raises error
425        fake_data = np.zeros(img.shape).astype(img.get_data_dtype())
426        with pytest.raises(AttributeError):
427            img._data = fake_data
428
429    def validate_shape(self, imaker, params):
430        # Validate shape
431        img = imaker()
432        # Same as expected shape
433        assert img.shape == params['shape']
434        # Same as array shape if passed
435        if 'data' in params:
436            assert img.shape == params['data'].shape
437        # Read only
438        with pytest.raises(AttributeError):
439            img.shape = np.eye(4)
440
441    def validate_ndim(self, imaker, params):
442        # Validate shape
443        img = imaker()
444        # Same as expected ndim
445        assert img.ndim == len(params['shape'])
446        # Same as array ndim if passed
447        if 'data' in params:
448            assert img.ndim == params['data'].ndim
449        # Read only
450        with pytest.raises(AttributeError):
451            img.ndim = 5
452
453    def validate_shape_deprecated(self, imaker, params):
454        # Check deprecated get_shape API
455        img = imaker()
456        with pytest.raises(ExpiredDeprecationError):
457            img.get_shape()
458
459    def validate_mmap_parameter(self, imaker, params):
460        img = imaker()
461        fname = img.get_filename()
462        with InTemporaryDirectory():
463            # Load test files with mmap parameters
464            # or
465            # Save a generated file so we can test it
466            if fname is None:
467                # Skip only formats we can't write
468                if not img.rw or not img.valid_exts:
469                    return
470                fname = 'image' + img.valid_exts[0]
471                img.to_filename(fname)
472            rt_img = img.__class__.from_filename(fname, mmap=True)
473            assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
474            rt_img = img.__class__.from_filename(fname, mmap=False)
475            assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
476            rt_img = img.__class__.from_filename(fname, mmap='c')
477            assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
478            rt_img = img.__class__.from_filename(fname, mmap='r')
479            assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
480            # r+ is specifically not valid for images
481            with pytest.raises(ValueError):
482                img.__class__.from_filename(fname, mmap='r+')
483            with pytest.raises(ValueError):
484                img.__class__.from_filename(fname, mmap='invalid')
485            del rt_img  # to allow windows to delete the directory
486
487
488class HeaderShapeMixin(object):
489    """ Tests that header shape can be set and got
490
491    Add this one of your header supports ``get_data_shape`` and
492    ``set_data_shape``.
493    """
494
495    def validate_header_shape(self, imaker, params):
496        # Change shape in header, check this changes img.header
497        img = imaker()
498        hdr = img.header
499        shape = hdr.get_data_shape()
500        new_shape = (shape[0] + 1,) + shape[1:]
501        hdr.set_data_shape(new_shape)
502        assert img.header is hdr
503        assert img.header.get_data_shape() == new_shape
504
505
506class AffineMixin(object):
507    """ Adds test of affine property, method
508
509    Add this one if your image has an ``affine`` property.  If so, it should
510    (for now) also have a ``get_affine`` method returning the same result.
511    """
512
513    def validate_affine(self, imaker, params):
514        # Check affine API
515        img = imaker()
516        assert_almost_equal(img.affine, params['affine'], 6)
517        assert img.affine.dtype == np.float64
518        img.affine[0, 0] = 1.5
519        assert img.affine[0, 0] == 1.5
520        # Read only
521        with pytest.raises(AttributeError):
522            img.affine = np.eye(4)
523
524    def validate_affine_deprecated(self, imaker, params):
525        # Check deprecated affine API
526        img = imaker()
527        with pytest.deprecated_call():
528            assert_almost_equal(img.get_affine(), params['affine'], 6)
529            assert img.get_affine().dtype == np.float64
530            aff = img.get_affine()
531            aff[0, 0] = 1.5
532            assert aff is img.get_affine()
533
534
535class SerializeMixin(object):
536    def validate_to_bytes(self, imaker, params):
537        img = imaker()
538        serialized = img.to_bytes()
539        with InTemporaryDirectory():
540            fname = 'img' + self.standard_extension
541            img.to_filename(fname)
542            with open(fname, 'rb') as fobj:
543                file_contents = fobj.read()
544        assert serialized == file_contents
545
546    def validate_from_bytes(self, imaker, params):
547        img = imaker()
548        klass = getattr(self, 'klass', img.__class__)
549        with InTemporaryDirectory():
550            fname = 'img' + self.standard_extension
551            img.to_filename(fname)
552
553            all_images = list(getattr(self, 'example_images', [])) + [{'fname': fname}]
554            for img_params in all_images:
555                img_a = klass.from_filename(img_params['fname'])
556                with open(img_params['fname'], 'rb') as fobj:
557                    img_b = klass.from_bytes(fobj.read())
558
559                assert self._header_eq(img_a.header, img_b.header)
560                assert np.array_equal(img_a.get_fdata(), img_b.get_fdata())
561                del img_a
562                del img_b
563
564    def validate_to_from_bytes(self, imaker, params):
565        img = imaker()
566        klass = getattr(self, 'klass', img.__class__)
567        with InTemporaryDirectory():
568            fname = 'img' + self.standard_extension
569            img.to_filename(fname)
570
571            all_images = list(getattr(self, 'example_images', [])) + [{'fname': fname}]
572            for img_params in all_images:
573                img_a = klass.from_filename(img_params['fname'])
574                bytes_a = img_a.to_bytes()
575
576                img_b = klass.from_bytes(bytes_a)
577
578                assert img_b.to_bytes() == bytes_a
579                assert self._header_eq(img_a.header, img_b.header)
580                assert np.array_equal(img_a.get_fdata(), img_b.get_fdata())
581                del img_a
582                del img_b
583
584    @staticmethod
585    def _header_eq(header_a, header_b):
586        """ Header equality check that can be overridden by a subclass of this test
587
588        This allows us to retain the same tests above when testing an image that uses an
589        abstract class as a header, namely when testing the FileBasedImage API, which
590        raises a NotImplementedError for __eq__
591        """
592        return header_a == header_b
593
594
595
596class LoadImageAPI(GenericImageAPI,
597                   DataInterfaceMixin,
598                   AffineMixin,
599                   GetSetDtypeMixin,
600                   HeaderShapeMixin):
601    # Callable returning an image from a filename
602    loader = None
603    # Sequence of dictionaries, where dictionaries have keys
604    # 'fname" in addition to keys for ``params`` (see obj_params docstring)
605    example_images = ()
606    # Class of images to be tested
607    klass = None
608
609    def obj_params(self):
610        for img_params in self.example_images:
611            yield lambda: self.loader(img_params['fname']), img_params
612
613    def validate_path_maybe_image(self, imaker, params):
614        for img_params in self.example_images:
615            test, sniff = self.klass.path_maybe_image(img_params['fname'])
616            assert isinstance(test, bool)
617            if sniff is not None:
618                assert isinstance(sniff[0], bytes)
619                assert isinstance(sniff[1], str)
620
621
622class MakeImageAPI(LoadImageAPI):
623    """ Validation for images we can make with ``func(data, affine, header)``
624    """
625    # A callable returning an image from ``image_maker(data, affine, header)``
626    image_maker = None
627    # A callable returning a header from ``header_maker()``
628    header_maker = None
629    # Example shapes for created images
630    example_shapes = ((2,), (2, 3), (2, 3, 4), (2, 3, 4, 5))
631    # Supported dtypes for storing to disk
632    storable_dtypes = (np.uint8, np.int16, np.float32)
633
634    def obj_params(self):
635        # Return any obj_params from superclass
636        for func, params in super(MakeImageAPI, self).obj_params():
637            yield func, params
638        # Create new images
639        aff = np.diag([1, 2, 3, 1])
640
641        def make_imaker(arr, aff, header=None):
642            return lambda: self.image_maker(arr, aff, header)
643
644        def make_prox_imaker(arr, aff, hdr):
645
646            def prox_imaker():
647                img = self.image_maker(arr, aff, hdr)
648                rt_img = bytesio_round_trip(img)
649                return self.image_maker(rt_img.dataobj, aff, rt_img.header)
650
651            return prox_imaker
652
653        for shape, stored_dtype in product(self.example_shapes,
654                                           self.storable_dtypes):
655            # To make sure we do not trigger scaling, always use the
656            # stored_dtype for the input array.
657            arr = np.arange(np.prod(shape), dtype=stored_dtype).reshape(shape)
658            hdr = self.header_maker()
659            hdr.set_data_dtype(stored_dtype)
660            func = make_imaker(arr.copy(), aff, hdr)
661            params = dict(
662                dtype=stored_dtype,
663                affine=aff,
664                data=arr,
665                shape=shape,
666                is_proxy=False)
667            yield make_imaker(arr.copy(), aff, hdr), params
668            if not self.can_save:
669                continue
670            # Create proxy images from these array images, by storing via BytesIO.
671            # We assume that loading from a fileobj creates a proxy image.
672            params['is_proxy'] = True
673            yield make_prox_imaker(arr.copy(), aff, hdr), params
674
675
676class ImageHeaderAPI(MakeImageAPI):
677    """ When ``self.image_maker`` is an image class, make header from class
678    """
679
680    def header_maker(self):
681        return self.image_maker.header_class()
682
683
684class TestAnalyzeAPI(ImageHeaderAPI):
685    """ General image validation API instantiated for Analyze images
686    """
687    klass = image_maker = AnalyzeImage
688    has_scaling = False
689    can_save = True
690    standard_extension = '.img'
691    # Supported dtypes for storing to disk
692    storable_dtypes = (np.uint8, np.int16, np.int32, np.float32, np.float64)
693
694
695class TestSpatialImageAPI(TestAnalyzeAPI):
696    klass = image_maker = SpatialImage
697    can_save = False
698
699
700class TestSpm99AnalyzeAPI(TestAnalyzeAPI):
701    # SPM-type analyze need scipy for mat file IO
702    klass = image_maker = Spm99AnalyzeImage
703    has_scaling = True
704    can_save = have_scipy
705
706
707class TestSpm2AnalyzeAPI(TestSpm99AnalyzeAPI):
708    klass = image_maker = Spm2AnalyzeImage
709
710
711class TestNifti1PairAPI(TestSpm99AnalyzeAPI):
712    klass = image_maker = Nifti1Pair
713    can_save = True
714
715
716class TestNifti1API(TestNifti1PairAPI, SerializeMixin):
717    klass = image_maker = Nifti1Image
718    standard_extension = '.nii'
719
720
721class TestNifti2PairAPI(TestNifti1PairAPI):
722    klass = image_maker = Nifti2Pair
723
724
725class TestNifti2API(TestNifti1API):
726    klass = image_maker = Nifti2Image
727
728
729class TestMinc1API(ImageHeaderAPI):
730    klass = image_maker = Minc1Image
731    loader = minc1.load
732    example_images = MINC1_EXAMPLE_IMAGES
733
734
735class TestMinc2API(TestMinc1API):
736
737    def setup(self):
738        if not have_h5py:
739            raise unittest.SkipTest('Need h5py for these tests')
740
741    klass = image_maker = Minc2Image
742    loader = minc2.load
743    example_images = MINC2_EXAMPLE_IMAGES
744
745
746class TestPARRECAPI(LoadImageAPI):
747
748    def loader(self, fname):
749        return parrec.load(fname)
750
751    klass = parrec.PARRECImage
752    example_images = PARREC_EXAMPLE_IMAGES
753
754
755# ECAT is a special case and needs more thought
756# class TestEcatAPI(TestAnalyzeAPI):
757#     image_maker = ecat.EcatImage
758#     has_scaling = True
759#     can_save = True
760#    standard_extension = '.v'
761
762
763class TestMGHAPI(ImageHeaderAPI, SerializeMixin):
764    klass = image_maker = MGHImage
765    example_shapes = ((2, 3, 4), (2, 3, 4, 5))  # MGH can only do >= 3D
766    has_scaling = True
767    can_save = True
768    standard_extension = '.mgh'
769
770
771class TestGiftiAPI(LoadImageAPI, SerializeMixin):
772    klass = image_maker = GiftiImage
773    can_save = True
774    standard_extension = '.gii'
775
776
777class TestAFNIAPI(LoadImageAPI):
778    loader = brikhead.load
779    klass = image_maker = brikhead.AFNIImage
780    example_images = AFNI_EXAMPLE_IMAGES
781