Skip to content
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

Unable to save extra information to the 'extra' field of a nifti1 image #1326

Open
lxmedai opened this issue Jun 7, 2024 · 1 comment
Open

Comments

@lxmedai
Copy link

lxmedai commented Jun 7, 2024

I tried to put some extra information to the field 'extra' (which is just an empty dict by default) by using
img_nii.extra = {'Test': 'See what happens'} # img_nii is my loaded nifti1 image.

This assignment worked perfectly fine. Then I tried to save it by calling:
img_nii.to_filename('myTestFilename.nii.gz')
, which also worked with no errors reported.
But if I tried to load the nifti1 image again by calling
img_nii_new = nib.load('myTestFilename.nii.gz'),
what I got in the field 'extra' of img_nii_new was simply an empty dict. What should I do in order to save extra information there? I plan to save some numpy arrays to this 'extra' field. How can I solve this problem? Any suggestions?

@effigies
Copy link
Member

effigies commented Jun 9, 2024

The extra dictionary is purely for passing around images in memory. You could write a NIfTI extension to pack anything you want in the header, though:

nibabel/nibabel/nifti1.py

Lines 286 to 417 in e6ccec4

class Nifti1Extension:
"""Baseclass for NIfTI1 header extensions.
This class is sufficient to handle very simple text-based extensions, such
as `comment`. More sophisticated extensions should/will be supported by
dedicated subclasses.
"""
def __init__(self, code, content):
"""
Parameters
----------
code : int or str
Canonical extension code as defined in the NIfTI standard, given
either as integer or corresponding label
(see :data:`~nibabel.nifti1.extension_codes`)
content : str
Extension content as read from the NIfTI file header. This content is
converted into a runtime representation.
"""
try:
self._code = extension_codes.code[code]
except KeyError:
# XXX or fail or at least complain?
self._code = code
self._content = self._unmangle(content)
def _unmangle(self, value):
"""Convert the extension content into its runtime representation.
The default implementation does nothing at all.
Parameters
----------
value : str
Extension content as read from file.
Returns
-------
The same object that was passed as `value`.
Notes
-----
Subclasses should reimplement this method to provide the desired
unmangling procedure and may return any type of object.
"""
return value
def _mangle(self, value):
"""Convert the extension content into NIfTI file header representation.
The default implementation does nothing at all.
Parameters
----------
value : str
Extension content in runtime form.
Returns
-------
str
Notes
-----
Subclasses should reimplement this method to provide the desired
mangling procedure.
"""
return value
def get_code(self):
"""Return the canonical extension type code."""
return self._code
def get_content(self):
"""Return the extension content in its runtime representation."""
return self._content
def get_sizeondisk(self):
"""Return the size of the extension in the NIfTI file."""
# need raw value size plus 8 bytes for esize and ecode
size = len(self._mangle(self._content))
size += 8
# extensions size has to be a multiple of 16 bytes
if size % 16 != 0:
size += 16 - (size % 16)
return size
def __repr__(self):
try:
code = extension_codes.label[self._code]
except KeyError:
# deal with unknown codes
code = self._code
s = f"Nifti1Extension('{code}', '{self._content}')"
return s
def __eq__(self, other):
return (self._code, self._content) == (other._code, other._content)
def __ne__(self, other):
return not self == other
def write_to(self, fileobj, byteswap):
"""Write header extensions to fileobj
Write starts at fileobj current file position.
Parameters
----------
fileobj : file-like object
Should implement ``write`` method
byteswap : boolean
Flag if byteswapping the data is required.
Returns
-------
None
"""
extstart = fileobj.tell()
rawsize = self.get_sizeondisk()
# write esize and ecode first
extinfo = np.array((rawsize, self._code), dtype=np.int32)
if byteswap:
extinfo = extinfo.byteswap()
fileobj.write(extinfo.tobytes())
# followed by the actual extension content
# XXX if mangling upon load is implemented, it should be reverted here
fileobj.write(self._mangle(self._content))
# be nice and zero out remaining part of the extension till the
# next 16 byte border
fileobj.write(b'\x00' * (extstart + rawsize - fileobj.tell()))

The basic procedure is:

ext = nb.nifti1.Nifti1Extension(0, bytestring)  # 0 for unknown, and you need some bytestring
img.header.extensions.append(ext)

Now you can save the image, and then retrieve it on a received image with img.header.extensions[0].get_content().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants