astropy/imageutils — pull request #24 by astrofrog (2014-09-11 19:32:53+00:00)
Title: Proposal for new scaling/stretching API

### About

@ejeschke @cdeil @larrybradley @ChrisBeamont - here is a proposal for a new API for the image stretching/scaling. I tried to write something inspired by @ejeschke's approach, but also trying to make it compatible with @ChrisBeaumont's approach in glue and my approach in APLpy. This is by no means complete in terms of different stretch functions, but at this point I just wanted to start a discussion on the API based on some code.

### Approach

Following's @ejeschke's approach, I've separated the problem into (a) determining vmin/vmax intervals, and (b) stretching values in the range [0:1] to other values in the range [0:1]

### Intervals

The interval classes can be initialized with zero or more options depending on the class, and for now have a single ``get_limits`` method that returns ``vmin`` and ``vmax``:

```
In [1]: import numpy as np

In [2]: data = np.random.random((128))

In [3]: from imageutils.scaling import *

In [4]: i = ManualInterval(0.2, 0.8)

In [5]: i.get_limits(data)
Out[5]: (0.2, 0.8)

In [6]: i = MinMaxInterval()

In [7]: i.get_limits(data)
Out[7]: (0.0041587738556735365, 0.97469338355296631)

In [8]: i = PercentileInterval(95)

In [9]: i.get_limits(data)
Out[9]: (0.041784080796265728, 0.94854907673291367)
```

Of course we could also define ``__call__`` on the base function so as to be able to do e.g. ``i(data)`` and get back values in the range [0:1].

### Stretching

Stretching is also defined with classes, which take zero or more arguments, and then are used as functions to transform the values. The ``inverted()`` method takes no arguments and returns the inverse of the transform:

```
In [11]: data = np.array([0., 0.33, 0.66, 1.])

In [12]: s = LinearStretch()

In [13]: s(data)
Out[13]: array([ 0. , 0.33, 0.66, 1. ])

In [14]: s = SqrtStretch()

In [15]: s(data)
Out[15]: array([ 0. , 0.57445626, 0.81240384, 1. ])

In [16]: si = s.inverted()

In [17]: si(s(data))
Out[17]: array([ 0. , 0.33, 0.66, 1. ])
```

Transformations can be chained:

```
In [18]: c = s + s

In [19]: c
Out[19]:

In [20]: c(data)
Out[20]: array([ 0. , 0.75792893, 0.90133448, 1. ])
```

And we can check that this is equivalent to ``x**0.25``:

```
In [21]: p = PowerStretch(0.25)

In [22]: p(data)
Out[22]: array([ 0. , 0.75792893, 0.90133448, 1. ])
```

There is also a ``ContrastBiasStretch`` stretch and inverse one which can be used for ds9-like behavior, and this can of course be chained with another transformation.

### Normalization class

We can bring these two together to make a ``Normalize`` class for use with Matplotlib. At the moment one has to first get the ``vmin`` and ``vmax`` then initialize the normalization class:

```python
import numpy as np
import matplotlib.pyplot as plt
from imageutils.scaling import ImageNormalize, SqrtStretch, PercentileInterval

data = np.arange(900).reshape((30, 30))

p = PercentileInterval(90.)
vmin, vmax = p.get_limits(data)

normalizer = ImageNormalize(vmin=vmin, vmax=vmax, stretch=SqrtStretch())

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
im = ax.imshow(data, norm=normalizer, interpolation='nearest')
fig.colorbar(im)
```

which gives:

![test](https://cloud.githubusercontent.com/assets/314716/4240341/d072930c-39e9-11e4-85e3-26ca4e8214e9.png)

As you can see, the values are correctly placed on the colorbar, non-linearly.

We could also have the normalization class take the interval instance too, but the issue is that some intervals depend on the data, so we'd have to make it that when passing it to imshow, one has to do ``norm=normalizer(data)``. However, the issue is that the user can technically update the data without updating the normalization class, so things could end up in an inconsistent state. I think the current API is fine given that this is going to be mostly used in tools rather than directly by users.

### Summary

This is by far not complete and needs tests, docs, etc. and more classes, but at this point I would be interested to know:

* Do you think this is the right approach?
* What else is needed before you can consider using something like this in your tool? The aim here really is to avoid duplication and I personally plan to switch APLpy to use this once it is complete, so I'm hoping that we can make it general enough so that glue and ginga can also switch

I should say that we can also still provide a functional interface for users if we want, but the point here is to generalize the backend.