Skip to content

Commit 42a891f

Browse files
committed
DOC: document adding an image format
Notes on how you might go about adding a new image format to nibabel. Yes, it's a little complicated.
1 parent 3687553 commit 42a891f

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

doc/source/devel/add_image_format.rst

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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 best if you can) or on the mailing list
157+
or via github issues.
158+
159+
.. include:: ../links_names.txt

doc/source/devel/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ Developer documentation page
1111

1212
devguide
1313
add_test_data
14+
add_image_format
1415
devdiscuss
1516
make_release

0 commit comments

Comments
 (0)