# NiBabel

### Access a cacophony of neuro-imaging file formats

# BIAP5 - A streamlines converter

Author:

  Marc-Alexandre Côté

Status:

  Draft

Type:

  Standards

Created:

  2013-09-03

The first objective of this proposal is to add support to other streamlines format. The second objective is to be able to easily convert from one file format to another.

## Motivation

There are a couple of different formats for saving streamlines to a file. Currently, NiBabel only support one of them: TRK from Trackvis. NiBabel could greatly benefit from supporting other formats: TCK (MRtrix), VTK (Camino, MITK) and more. Moreover, being able to move from one format to another would be convenient. To ease the conversion process, a generic format from which to inherit and some common header fields would be necessary. This is similar to what NiBabel already has for neuroimages.

After implementing this proposal, users could load and use streamlines file like this:

    >>> import nibabel as nib
    >>> f = nib.streamlines.load('my_trk.trk', lazy_load=False)
    >>> type(f)
    nibabel.streamlines.base_format.Streamlines
    >>> f.points
    [array([ [1, 1, 1],
             [2, 2, 2],
             [3, 3, 3] ]),
     array([ [4, 4, 4],
             [5, 5, 5] ])]
    >>> nib.streamlines.convert('my_trk.trk', 'my_tck.tck')
    >>> f2 = nib.streamlines.load('my_trk.tck', lazy_load=False)
    >>> type(f2)
    nibabel.streamlines.base_format.Streamlines
    >>> f2.points
    [array([ [1, 1, 1],
             [2, 2, 2],
             [3, 3, 3] ]),
     array([ [4, 4, 4],
             [5, 5, 5] ])]

Of course, similar functions will be available for ‘scalars’ (per point) and ‘properties’ (per streamline) as defined in the TrackVis format. A simple example to save three streamlines with no scalars nor properties would look like this:

    >>> import nibabel as nib
    >>> points = [np.arange(1*3).reshape((1,3)),
                  np.arange(2*3).reshape((2,3)),
                  np.arange(5*3).reshape((5,3))]
    >>> streamlines = nib.streamlines.Streamlines(points)
    >>> nib.streamlines.save(streamlines, 'data1.trk')  # Default TRK header is used but updated with streamlines information.

    >>> FA = nib.load('FA.nii')
    >>> streamlines.header = nib.streamlines.header.from_nifti(FA)  # Uses information of the FA to create an header.
    >>> nib.streamlines.save(streamlines, 'data2.trk')  # Streamlines' header is used but also updated with streamlines information.

    >>> from nib.streamlines.header import VOXEL_ORDER, VOXEL_SIZES
    >>> hdr = nib.streamlines.TrkFile.get_empty_header()  # Default TRK header
    >>> hdr[VOXEL_ORDER] = "LAS"
    >>> hdr[VOXEL_SIZES] = (2, 2, 2)
    >>> streamlines.header = hdr
    >>> nib.streamlines.save(streamlines, 'data3.trk')  # Uses hdr to create a TRK header.

## Overview

All code related to managing streamlines should be kept in a separate folder: `nibabel.streamlines`. A first file, `base_format.py`, would contain base classes acting as general interfaces from which new streamlines file format will inherit.

Streamlines would be represented by its own class `Streamlines` which will have three main properties: `points`, `scalars` and `properties`. Streamlines objects can be iterate over producing tuple of `points`, `scalars` and `properties` for each streamline.

The generic class `StreamlinesFile` would look like this:

    class StreamlinesFile:
        @classmethod
        def get_magic_number(cls):
            raise NotImplementedError()

        @classmethod
        def is_correct_format(cls, fileobj):
            raise NotImplementedError()

        @classmethod
        def get_empty_header(cls):
            raise NotImplementedError()

        @classmethod
        def load(cls, fileobj, lazy_load=True):
            raise NotImplementedError()

        @classmethod
        def save(cls, streamlines, fileobj):
            raise NotImplementedError()

        @staticmethod
        def pretty_print(streamlines):
            raise NotImplementedError()

When inheriting from a base class, a specific streamline format class should know how to do its i/o, in particular how to iterate through the streamlines without loading the whole file into memory.

Once, the right interface is in place, the conversion part should be quite easy. Moreover, the conversion could be done without loading the input file entirely into memory thanks to generators. Actually, the convert function should looks like this:

    def convert(in_fileobj, out_filename):
        # Loading part
        streamlines_file = detect_format(in_fileobj)
        streamlines = streamlines_file.load(in_fileobj, lazy_load=True)

        # Saving part
        streamlines_file = detect_format(out_filename)
        streamlines_file.save(streamlines, out_filename)

Of course, this implies some sort of general header compatibility between every format.

## Issues

### Header

Like it is done in NiBabel, headers should be defined using the `numpy.dtype`. This consists of a list of tuples, each one containing information (name, datatype and shape) about one field of the header. Once loaded, the header will acted as a dictionary using the name of each field as the key. Ideally, header of different formats would be the same, but it is not. To avoid manually writing each possible conversion between header formats, a general architecture should be put in place.

One solution is to define some sort of `CommonHeader` containing an enum of the most common field (i.e. NB_FIBERS, VOXEL_SIZES, DIMENSIONS, etc). Like that, instead of specifying a field’s name in the header definition, the suited enum constant should be used if there is one, otherwise the name is hard coded to a string representing the field. It should be make clear, in the documentation of `CommonHeader`, what is the expected value of a common field.

## Future Work

A first interesting subclass would be the `DynamicStreamlineFile` offering a way to append streamlines to an existing file when format permits it.