Skip to content

Commit b729779

Browse files
committed
RF: Factor SerializableImage
1 parent 38fd9fc commit b729779

File tree

4 files changed

+102
-38
lines changed

4 files changed

+102
-38
lines changed

nibabel/filebasedimages.py

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -260,21 +260,6 @@ def from_filename(klass, filename):
260260
file_map = klass.filespec_to_file_map(filename)
261261
return klass.from_file_map(file_map)
262262

263-
@classmethod
264-
def from_bytes(klass, bstring):
265-
""" Construct image from a byte string
266-
267-
Class method
268-
269-
Parameters
270-
----------
271-
bstring : bytes
272-
Byte string containing the on-disk representation of an image
273-
"""
274-
bio = io.BytesIO(bstring)
275-
file_map = self.make_file_map({'image': bio, 'header': bio})
276-
return klass.from_file_map(file_map)
277-
278263
@classmethod
279264
def from_file_map(klass, file_map):
280265
raise NotImplementedError
@@ -349,24 +334,6 @@ def to_filename(self, filename):
349334
self.file_map = self.filespec_to_file_map(filename)
350335
self.to_file_map()
351336

352-
def to_bytes(self):
353-
""" Return a ``bytes`` object with the contents of the file that would
354-
be written if the image were saved.
355-
356-
Parameters
357-
----------
358-
None
359-
360-
Returns
361-
-------
362-
bytes
363-
Serialized image
364-
"""
365-
bio = io.BytesIO()
366-
file_map = self.make_file_map({'image': bio, 'header': bio})
367-
self.to_file_map(file_map)
368-
return bio.getvalue()
369-
370337
@deprecate_with_version('to_filespec method is deprecated.\n'
371338
'Please use the "to_filename" method instead.',
372339
'1.0', '3.0')
@@ -545,3 +512,79 @@ def path_maybe_image(klass, filename, sniff=None, sniff_max=1024):
545512
if sniff is None or len(sniff[0]) < klass._meta_sniff_len:
546513
return False, sniff
547514
return klass.header_class.may_contain_header(sniff[0]), sniff
515+
516+
517+
class SerializableImage(FileBasedImage):
518+
'''
519+
Abstract image class for (de)serializing images to/from byte strings.
520+
521+
The class doesn't define any image properties.
522+
523+
It has:
524+
525+
methods:
526+
527+
* .to_bytes() - serialize image to byte string
528+
529+
classmethods:
530+
531+
* from_bytes(bytestring) - make instance by deserializing a byte string
532+
533+
The following properties should hold:
534+
535+
* ``klass.from_bytes(bstr).to_bytes() == bstr``
536+
* if ``img = orig.__class__.from_bytes(orig.to_bytes())``, then
537+
``img.header == orig.header`` and ``img.get_data() == orig.get_data()``
538+
539+
Further, for images that are single files on disk, the following methods of loading
540+
the image must be equivalent:
541+
542+
img = klass.from_filename(fname)
543+
544+
with open(fname, 'rb') as fobj:
545+
img = klass.from_bytes(fobj.read())
546+
547+
And the following methods of saving a file must be equivalent:
548+
549+
img.to_filename(fname)
550+
551+
with open(fname, 'wb') as fobj:
552+
fobj.write(img.to_bytes())
553+
554+
Images that consist of separate header and data files will generally
555+
place the header with the data, but if the header is not of fixed
556+
size and does not define its own size, a new format may need to be
557+
defined.
558+
'''
559+
@classmethod
560+
def from_bytes(klass, bytestring):
561+
""" Construct image from a byte string
562+
563+
Class method
564+
565+
Parameters
566+
----------
567+
bstring : bytes
568+
Byte string containing the on-disk representation of an image
569+
"""
570+
bio = io.BytesIO(bstring)
571+
file_map = klass.make_file_map({'image': bio, 'header': bio})
572+
return klass.from_file_map(file_map)
573+
574+
def to_bytes(self):
575+
""" Return a ``bytes`` object with the contents of the file that would
576+
be written if the image were saved.
577+
578+
Parameters
579+
----------
580+
None
581+
582+
Returns
583+
-------
584+
bytes
585+
Serialized image
586+
"""
587+
bio = io.BytesIO()
588+
file_map = self.make_file_map({'image': bio, 'header': bio})
589+
self.to_file_map(file_map)
590+
return bio.getvalue()

nibabel/freesurfer/mghformat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from ..affines import voxel_sizes, from_matvec
1717
from ..volumeutils import (array_to_file, array_from_file, endian_codes,
1818
Recoder)
19+
from ..filebasedimages import SerializableImage
1920
from ..spatialimages import HeaderDataError, SpatialImage
2021
from ..fileholders import FileHolder
2122
from ..arrayproxy import ArrayProxy, reshape_dataobj
@@ -503,7 +504,7 @@ def __setitem__(self, item, value):
503504
super(MGHHeader, self).__setitem__(item, value)
504505

505506

506-
class MGHImage(SpatialImage):
507+
class MGHImage(SpatialImage, SerializableImage):
507508
""" Class for MGH format image
508509
"""
509510
header_class = MGHHeader

nibabel/nifti1.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import numpy.linalg as npl
2020

2121
from .py3k import asstr
22+
from .filebasedimages import SerializableImage
2223
from .volumeutils import Recoder, make_dt_codes, endian_codes
2324
from .spatialimages import HeaderDataError, ImageFileError
2425
from .batteryrunners import Report
@@ -1758,7 +1759,7 @@ class Nifti1PairHeader(Nifti1Header):
17581759
is_single = False
17591760

17601761

1761-
class Nifti1Pair(analyze.AnalyzeImage):
1762+
class Nifti1Pair(analyze.AnalyzeImage, SerializableImage):
17621763
""" Class for NIfTI1 format image, header pair
17631764
"""
17641765
header_class = Nifti1PairHeader

nibabel/tests/test_image_api.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,17 +494,36 @@ def validate_affine_deprecated(self, imaker, params):
494494

495495

496496
class SerializeMixin(object):
497-
498-
def validate_serialize(self, imaker, params):
497+
def validate_to_bytes(self, imaker, params):
499498
img = imaker()
500-
serialized = img.serialize()
499+
serialized = img.to_bytes()
501500
with InTemporaryDirectory():
502501
fname = 'img' + self.standard_extension
503502
img.to_filename(fname)
504503
with open(fname, 'rb') as fobj:
505504
file_contents = fobj.read()
506505
assert serialized == file_contents
507506

507+
def validate_from_bytes(self, imaker, params):
508+
for img_params in self.example_images:
509+
img_a = self.klass.from_filename(img_params['fname'])
510+
with open(img_params['fname'], 'rb') as fobj:
511+
img_b = self.klass.from_bytes(fobj.read())
512+
513+
assert img_a.header == img_b.header
514+
assert np.array_equal(img_a.get_data(), img_b.get_data())
515+
516+
def validate_round_trip(self, imaker, params):
517+
for img_params in self.example_images:
518+
img_a = self.klass.from_filename(img_params['fname'])
519+
bytes_a = img_a.to_bytes()
520+
521+
img_b = self.klass.from_bytes(bytes_a)
522+
523+
assert img_b.to_bytes() == bytes_a
524+
assert img_a.header == img_b.header
525+
assert np.array_equal(img_a.get_data(), img_b.get_data())
526+
508527

509528
class LoadImageAPI(GenericImageAPI,
510529
DataInterfaceMixin,

0 commit comments

Comments
 (0)