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