|
| 1 | +######################################## |
| 2 | +How to add a new image format to nibabel |
| 3 | +######################################## |
| 4 | + |
| 5 | +These are some work-in-progress notes in the hope that they will help adding a |
| 6 | +new image format to NiBabel. |
| 7 | + |
| 8 | +********** |
| 9 | +Philosophy |
| 10 | +********** |
| 11 | + |
| 12 | +As usual, the general idea is to make your image as explicit and transparent |
| 13 | +as possible. |
| 14 | + |
| 15 | +From the Zen of Python (``import this``), these guys spring to mind: |
| 16 | + |
| 17 | +* Explicit is better than implicit. |
| 18 | +* Errors should never pass silently. |
| 19 | +* In the face of ambiguity, refuse the temptation to guess. |
| 20 | +* Now is better than never. |
| 21 | +* If the implementation is hard to explain, it's a bad idea. |
| 22 | + |
| 23 | +So far we have tried to make the nibabel version of the image as close as |
| 24 | +possible to the way the user of the particular format is expecting to see it. |
| 25 | + |
| 26 | +For example, the NIfTI format documents describe the image with the first |
| 27 | +dimension of the image data array being the fastest varying in memory (and on |
| 28 | +disk). Numpy defaults to having the last dimension of the array being the |
| 29 | +fastest varying in memory. We chose to have the first dimension vary fastest |
| 30 | +in memory to match the conventions in the NIfTI specification. |
| 31 | + |
| 32 | +****************************** |
| 33 | +Helping us to review your code |
| 34 | +****************************** |
| 35 | + |
| 36 | +You are likely to know the image format much much better than the rest of us |
| 37 | +do, but to help you with the code, we will need to learn. The following will |
| 38 | +really help us get up to speed: |
| 39 | + |
| 40 | +#. Links in the code or in the docs to the information on the file format. |
| 41 | + For example, you'll see the canonical links for the NIfTI 2 format at the |
| 42 | + top of the :mod:`.nifti2` file, in the module docstring; |
| 43 | +#. Example files in the format; see :doc:`add_test_data`; |
| 44 | +#. Good test coverage. The tests help us see how you are expecting the code |
| 45 | + and the format to be used. We recommend writing the tests first; the tests |
| 46 | + do an excellent job in helping us and you see how the API is going to work. |
| 47 | + |
| 48 | +*************************** |
| 49 | +The format can be read-only |
| 50 | +*************************** |
| 51 | + |
| 52 | +Read-only access to a format is better than no access to a format, and often |
| 53 | +much better. For example, we can read but not write PAR / REC and MINC files. |
| 54 | +Having the code to read the files makes it easier to work with these files in |
| 55 | +Python, and easier for someone else to add the ability to write the format |
| 56 | +later. |
| 57 | + |
| 58 | +************* |
| 59 | +The image API |
| 60 | +************* |
| 61 | + |
| 62 | +An image should conform to the image API. See the module docstring for |
| 63 | +:mod:`.spatialimages` for a description of the API. |
| 64 | + |
| 65 | +You should test whether your image does conform to the API by adding a test |
| 66 | +class for your image in :mod:`nibabel.tests.test_image_api`. For example, the |
| 67 | +API test for the PAR / REC image format looks like:: |
| 68 | + |
| 69 | + class TestPARRECAPI(LoadImageAPI): |
| 70 | + def loader(self, fname): |
| 71 | + return parrec.load(fname) |
| 72 | + |
| 73 | + example_images = PARREC_EXAMPLE_IMAGES |
| 74 | + |
| 75 | +where your work is to define the ``EXAMPLE_IMAGES`` list |--| see the |
| 76 | +:mod:`nibabel.tests.test_parrec` file for the PAR / REC example images |
| 77 | +definition. |
| 78 | + |
| 79 | +**************************** |
| 80 | +Where to start with the code |
| 81 | +**************************** |
| 82 | + |
| 83 | +There is no API requirement that a new image format inherit from the general |
| 84 | +:class:`.SpatialImage` class, but in fact all our image |
| 85 | +formats do inherit from this class. We strongly suggest you do the same, to |
| 86 | +get many simple methods implemented for free. You can always override the |
| 87 | +ones you don't want. |
| 88 | + |
| 89 | +There is also a generic header class you might consider building on to contain |
| 90 | +your image metadata |--| :class:`.Header`. See that |
| 91 | +class for the header API. |
| 92 | + |
| 93 | +The API does not require it, but if it is possible, it may be good to |
| 94 | +implement the image data as loaded from disk as an array proxy. See the |
| 95 | +docstring of :mod:`.arrayproxy` for a description of the API, and see the |
| 96 | +module code for an implementation of the API. You may be able to use the |
| 97 | +unmodified :class:`.ArrayProxy` class for your image type. |
| 98 | + |
| 99 | +If you write a new array proxy class, add tests for the API of the class in |
| 100 | +:mod:`nibabel.tests.test_proxy_api`. See |
| 101 | +:class:`.TestPARRECAPI` for an example. |
| 102 | + |
| 103 | +A nibabel image is the association of: |
| 104 | + |
| 105 | +#. The image array data (as implemented by an array proxy or a numpy array); |
| 106 | +#. An affine relating the image array coordinates to an RAS+ world (see |
| 107 | + :doc:`../coordinate_systems`); |
| 108 | +#. Image metadata in the form of a header. |
| 109 | + |
| 110 | +Your new image constructor may well be the default from |
| 111 | +:class:`.SpatialImage`, which looks like this:: |
| 112 | + |
| 113 | + def __init__(self, dataobj, affine, header=None, |
| 114 | + extra=None, file_map=None): |
| 115 | + |
| 116 | +Your job when loading a file is to create: |
| 117 | + |
| 118 | +#. ``dataobj`` - an array or array proxy; |
| 119 | +#. ``affine`` - 4 by 4 array relating array coordinates to world coordinates; |
| 120 | +#. ``header`` - a metadata container implementing at least ``get_data_dtype``, |
| 121 | + ``get_data_shape``. |
| 122 | + |
| 123 | +You will likely implement this logic in the ``from_file_map`` method of the |
| 124 | +image class. See :class:`.PARRECImage` for an example. |
| 125 | + |
| 126 | +*************************************** |
| 127 | +A recipe for writing a new image format |
| 128 | +*************************************** |
| 129 | + |
| 130 | +#. Find one or more examples images; |
| 131 | +#. Put them in ``nibabel/tests/data`` or a data submodule (see |
| 132 | + :doc:`add_test_data`); |
| 133 | +#. Create a file ``nibabel/tests/test_my_format_name_here.py``; |
| 134 | +#. Use some program that can read the format correctly to fill out the needed |
| 135 | + fields for an ``EXAMPLE_IMAGES`` list (see |
| 136 | + :mod:`nibabel.tests.test_parrec.py` for example); |
| 137 | +#. Add a test class using your ``EXAMPLE_IMAGES`` to |
| 138 | + :mod:`nibabel.tests.test_image_api`, using the PARREC image test class as |
| 139 | + an example. Now you have some failing tests |--| good job!; |
| 140 | +#. If you can, extract the metadata information from the test file, so it is |
| 141 | + small enough to fit as a small test file into ``nibabel/tests/data`` (don't |
| 142 | + forget the license); |
| 143 | +#. Write small maybe private functions to extract the header metadata from |
| 144 | + your new test file, testing these functions in |
| 145 | + ``test_my_format_name_here.py``. See :mod:`.parrec` for examples; |
| 146 | +#. When that is working, try sub-classing :class:`.Header`, and working out how |
| 147 | + to make the ``__init__`` and ``from_fileboj`` methods for that class. Test |
| 148 | + in ``test_my_format_name_here.py``; |
| 149 | +#. When that is working, try sub-classing :class:`.SpatialImage` and working |
| 150 | + out how to load the file with the ``from_file_map`` class; |
| 151 | +#. Now try seeing if you can get your ``test_image_api.py`` tests to pass; |
| 152 | +#. Consider adding more test data files, maybe to a test data repository |
| 153 | + submodule (:doc:`add_test_data`). Check you can read these files correctly |
| 154 | + (see :mod:`nibabel.tests.test_parrec_data` for an example). |
| 155 | +#. Ask for advice as early and as often as you can, either with a |
| 156 | + work-in-progress pull request (the easiest way for us to review) or on |
| 157 | + the mailing list or via github issues. |
| 158 | + |
| 159 | +.. include:: ../links_names.txt |
0 commit comments