Multidimensional Readers
========================

.. ipython:: python
   :suppress:

   from pims.tests.test_common import save_dummy_png, clean_dummy_png
   from itertools import product
   filenames = ['img-t{0}z{1}c{2}.png'.format(i, j, z) for i, j, z in product(range(4), range(10), range(2))]
   save_dummy_png('.', filenames, (64, 128))
   from pims import ImageSequenceND
   images = ImageSequenceND('img-*.png', axes_identifiers='tzc')

Multidimensional files are files that have for instance an extra z-axis (making
the image 3D, an extra channel axis, or a multipoint axis. PIMS provides a
flexible and uniform interface for working with these kind of files through the
base class ``FramesSequenceND``. The readers ``ImageSequenceND`` and ``Bioformats``
(see :doc:`image_sequence` or :doc:`bioformats`).
are based on this baseclass and provide the axis-aware methods describe below.

To avoid ambiguity, we call a point along one axis a *coordinate*. The word
*index* refers to the working of ``frames[index]``. The index is equal to the
coordinate along the iterating axis (normally, `t`), except when the iterating
axis is nested (see below).

The names and sizes of each axis is provided with the ``sizes`` property:

.. ipython:: python

   images.sizes


Axes bundling
-------------

The ``bundle_axes`` property defines which axes will be present in a single frame.
The ``frame_shape`` property is changed accordingly:

.. ipython:: python

   images.bundle_axes = 'czyx'
   images.frame_shape

   images.bundle_axes = 'yx'
   images.frame_shape

Currently, the last two axes have to be ``'yx'``. For multi-symbol axis names,
provide a list like this: ``images.bundle_axes = ['one', 'two']``.


Axes iteration
--------------

The ``iter_axes`` property defines which axis will be used as the index axis. The
reader length is updated accordingly:

.. ipython:: python

   images.iter_axes = 't'
   len(images)

When multiple axes are provided to ``iter_axes``, a nested iteration will be
performed in which the last element will iterate fastest:

.. ipython:: python

   images.iter_axes = 'tz'
   len(images)  # 4 * 10
   images[12];  # returns the image at t == 1 and z = 2

Default coordinates
-------------------

What if an axis is not present in ``bundle_axes`` and ``iter_axes``? Then the
*default coordinate* is returned, as defined in the dictionary ``default_coords``:

.. ipython:: python

   images.bundle_axes = 'zyx'
   images.iter_axes = 't'
   images.default_coords['c'] = 1

   images[2];  # returns the 3D image at t == 2 and c = 1

.. ipython:: python
   :suppress:

   clean_dummy_png('.', filenames)

Make your own multidimensional reader
-------------------------------------

Making a multidimensional reader class yourself is simple. The following
example is already a fully-functioning multidimensional reader. The crucial
method here is ``_register_get_frame``, that registers a ``get_frame`` method
and tells the reader which axes to expect from that method. You can also define
multiple ``get_frame`` methods to increase the reader performance.

The reader then figures out how to efficiently use this function, to present
the image in the shape that corresponds with the ``bundle_axes`` settings.

.. code-block:: python

   from pims import FramesSequenceND
   import numpy as np

   class IndexReturningReader(FramesSequenceND):
      @property
      def pixel_type(self):
          return np.uint8  # the pixel datatype

      def __init__(self, size_c, size_t, size_z):
          # first call the baseclass initialization
          super(IndexReturningReader, self).__init__()
          self._init_axis('x', 3)
          self._init_axis('y', 1)
          self._init_axis('c', size_c)
          self._init_axis('t', size_t)
          self._init_axis('z', size_z)
          # register the get_frame function
          self._register_get_frame(self.get_frame_func, 'yx')

      def get_frame_func(self, c, t, z):
          return np.array([[c, t, z]], dtype=np.uint8)