How to add a new image format to nibabel

These are some work-in-progress notes in the hope that they will help adding a new image format to NiBabel.

Philosophy

As usual, the general idea is to make your image as explicit and transparent as possible.

From the Zen of Python (import this), these guys spring to mind:

  • Explicit is better than implicit.

  • Errors should never pass silently.

  • In the face of ambiguity, refuse the temptation to guess.

  • Now is better than never.

  • If the implementation is hard to explain, it’s a bad idea.

So far we have tried to make the nibabel version of the image as close as possible to the way the user of the particular format is expecting to see it.

For example, the NIfTI format documents describe the image with the first dimension of the image data array being the fastest varying in memory (and on disk). Numpy defaults to having the last dimension of the array being the fastest varying in memory. We chose to have the first dimension vary fastest in memory to match the conventions in the NIfTI specification.

Helping us to review your code

You are likely to know the image format much much better than the rest of us do, but to help you with the code, we will need to learn. The following will really help us get up to speed:

  1. Links in the code or in the docs to the information on the file format. For example, you’ll see the canonical links for the NIfTI 2 format at the top of the nifti2 file, in the module docstring;

  2. Example files in the format; see Adding test data;

  3. Good test coverage. The tests help us see how you are expecting the code and the format to be used. We recommend writing the tests first; the tests do an excellent job in helping us and you see how the API is going to work.

The format can be read-only

Read-only access to a format is better than no access to a format, and often much better. For example, we can read but not write PAR / REC and MINC files. Having the code to read the files makes it easier to work with these files in Python, and easier for someone else to add the ability to write the format later.

The image API

An image should conform to the image API. See the module docstring for spatialimages for a description of the API.

You should test whether your image does conform to the API by adding a test class for your image in nibabel.tests.test_image_api. For example, the API test for the PAR / REC image format looks like:

class TestPARRECAPI(LoadImageAPI):
    def loader(self, fname):
        return parrec.load(fname)

    example_images = PARREC_EXAMPLE_IMAGES

where your work is to define the EXAMPLE_IMAGES list — see the nibabel.tests.test_parrec file for the PAR / REC example images definition.

Where to start with the code

There is no API requirement that a new image format inherit from the general SpatialImage class, but in fact all our image formats do inherit from this class. We strongly suggest you do the same, to get many simple methods implemented for free. You can always override the ones you don’t want.

There is also a generic header class you might consider building on to contain your image metadata — Header. See that class for the header API.

The API does not require it, but if it is possible, it may be good to implement the image data as loaded from disk as an array proxy. See the docstring of arrayproxy for a description of the API, and see the module code for an implementation of the API. You may be able to use the unmodified ArrayProxy class for your image type.

If you write a new array proxy class, add tests for the API of the class in nibabel.tests.test_proxy_api. See TestPARRECAPI for an example.

A nibabel image is the association of:

  1. The image array data (as implemented by an array proxy or a numpy array);

  2. An affine relating the image array coordinates to an RAS+ world (see Coordinate systems and affines);

  3. Image metadata in the form of a header.

Your new image constructor may well be the default from SpatialImage, which looks like this:

def __init__(self, dataobj, affine, header=None,
             extra=None, file_map=None):

Your job when loading a file is to create:

  1. dataobj - an array or array proxy;

  2. affine - 4 by 4 array relating array coordinates to world coordinates;

  3. header - a metadata container implementing at least get_data_dtype, get_data_shape.

You will likely implement this logic in the from_file_map method of the image class. See PARRECImage for an example.

A recipe for writing a new image format

  1. Find one or more examples images;

  2. Put them in nibabel/tests/data or a data submodule (see Adding test data);

  3. Create a file nibabel/tests/test_my_format_name_here.py;

  4. Use some program that can read the format correctly to fill out the needed fields for an EXAMPLE_IMAGES list (see nibabel.tests.test_parrec.py for example);

  5. Add a test class using your EXAMPLE_IMAGES to nibabel.tests.test_image_api, using the PARREC image test class as an example. Now you have some failing tests — good job!;

  6. If you can, extract the metadata information from the test file, so it is small enough to fit as a small test file into nibabel/tests/data (don’t forget the license);

  7. Write small maybe private functions to extract the header metadata from your new test file, testing these functions in test_my_format_name_here.py. See parrec for examples;

  8. When that is working, try sub-classing Header, and working out how to make the __init__ and from_fileboj methods for that class. Test in test_my_format_name_here.py;

  9. When that is working, try sub-classing SpatialImage and working out how to load the file with the from_file_map class;

  10. Now try seeing if you can get your test_image_api.py tests to pass;

  11. Consider adding more test data files, maybe to a test data repository submodule (Adding test data). Check you can read these files correctly (see nibabel.tests.test_parrec_data for an example).

  12. Ask for advice as early and as often as you can, either with a work-in-progress pull request (the easiest way for us to review) or on the mailing list or via github issues.