diff --git a/libs/qimage2ndarray/LICENSE.txt b/libs/qimage2ndarray/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..96d599dac8fc563eb78b9305e428df72944a6678
--- /dev/null
+++ b/libs/qimage2ndarray/LICENSE.txt
@@ -0,0 +1,30 @@
+Copyright (c) 2009, Hans Meine <hans_meine@gmx.net>
+All rights reserved. 
+ 
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+ 
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+ 
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the
+   distribution.
+ 
+3. Neither the name of the University of Hamburg nor the names of its
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+ 
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/libs/qimage2ndarray/__init__.py b/libs/qimage2ndarray/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e56e0e06dd75127d37a7b5918acf431b9824c32
--- /dev/null
+++ b/libs/qimage2ndarray/__init__.py
@@ -0,0 +1,403 @@
+import sys as _sys
+import numpy as _np
+
+from .dynqt import QtGui as _qt
+
+from .dynqt import qt as _qt_driver
+if _qt_driver.name() == 'PythonQt':
+    from .qimageview import QImage2ndarray as _temp
+    _qimageview = _temp.qimageview
+else:
+    from .qimageview_python import qimageview as _qimageview
+
+__version__ = "1.5"
+
+if _sys.byteorder == 'little':
+    _bgra = (0, 1, 2, 3)
+else:
+    _bgra = (3, 2, 1, 0)
+
+_bgra_fields = {'b': (_np.uint8, _bgra[0], 'blue'),
+                'g': (_np.uint8, _bgra[1], 'green'),
+                'r': (_np.uint8, _bgra[2], 'red'),
+                'a': (_np.uint8, _bgra[3], 'alpha')}
+
+bgra_dtype = _np.dtype(_bgra_fields)
+"""Complex dtype offering the named fields 'r','g','b', and 'a' and
+corresponding long names, conforming to QImage_'s 32-bit memory layout."""
+
+try:
+    _basestring = basestring
+except NameError:
+    # 'basestring' undefined, must be Python 3
+    _basestring = str
+
+def _qimage_or_filename_view(qimage):
+    if isinstance(qimage, _basestring):
+        qimage = _qt.QImage(qimage)
+    return _qimageview(qimage)
+
+def raw_view(qimage):
+    """Returns raw 2D view of the given QImage_'s memory.  The result
+    will be a 2-dimensional numpy.ndarray with an appropriately sized
+    integral dtype.  (This function is not intented to be used
+    directly, but used internally by the other -- more convenient --
+    view creation functions.)
+
+    :param qimage: image whose memory shall be accessed via NumPy
+    :type qimage: QImage_
+    :rtype: numpy.ndarray_ with shape (height, width)"""
+    return _qimage_or_filename_view(qimage)
+
+
+def byte_view(qimage, byteorder = 'little'):
+    """Returns raw 3D view of the given QImage_'s memory.  This will
+    always be a 3-dimensional numpy.ndarray with dtype numpy.uint8.
+    
+    Note that for 32-bit images, the last dimension will be in the
+    [B,G,R,A] order (if little endian) due to QImage_'s memory layout
+    (the alpha channel will be present for Format_RGB32 images, too).
+
+    For 8-bit (indexed) images, the array will still be 3-dimensional,
+    i.e. shape will be (height, width, 1).
+
+    The order of channels in the last axis depends on the `byteorder`,
+    which defaults to 'little', i.e. BGRA order.  You may set the
+    argument `byteorder` to 'big' to get ARGB, or use None which means
+    sys.byteorder here, i.e. return native order for the machine the
+    code is running on.
+
+    For your convenience, `qimage` may also be a filename, see
+    `Loading and Saving Images`_ in the documentation.
+
+    :param qimage: image whose memory shall be accessed via NumPy
+    :type qimage: QImage_
+    :param byteorder: specify order of channels in last axis
+    :rtype: numpy.ndarray_ with shape (height, width, 1 or 4) and dtype uint8"""
+    raw = _qimage_or_filename_view(qimage)
+    result = raw.view(_np.uint8).reshape(raw.shape + (-1, ))
+    if byteorder and byteorder != _sys.byteorder:
+        result = result[...,::-1]
+    return result
+
+
+def rgb_view(qimage, byteorder = 'big'):
+    """Returns RGB view of a given 32-bit color QImage_'s memory.
+    Similarly to byte_view(), the result is a 3D numpy.uint8 array,
+    but reduced to the rgb dimensions (without alpha), and reordered
+    (using negative strides in the last dimension) to have the usual
+    [R,G,B] order.  The image must have 32 bit pixel size, i.e. be
+    RGB32, ARGB32, or ARGB32_Premultiplied.  (Note that in the latter
+    case, the values are of course premultiplied with alpha.)
+
+    The order of channels in the last axis depends on the `byteorder`,
+    which defaults to 'big', i.e. RGB order.  You may set the argument
+    `byteorder` to 'little' to get BGR, or use None which means
+    sys.byteorder here, i.e. return native order for the machine the
+    code is running on.
+
+    For your convenience, `qimage` may also be a filename, see
+    `Loading and Saving Images`_ in the documentation.
+
+    :param qimage: image whose memory shall be accessed via NumPy
+    :type qimage: QImage_ with 32-bit pixel type
+    :param byteorder: specify order of channels in last axis
+    :rtype: numpy.ndarray_ with shape (height, width, 3) and dtype uint8"""
+    if byteorder is None:
+        byteorder = _sys.byteorder
+    bytes = byte_view(qimage, byteorder)
+    if bytes.shape[2] != 4:
+        raise ValueError("For rgb_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)")
+
+    if byteorder == 'little':
+        return bytes[...,:3] # strip A off BGRA
+    else:
+        return bytes[...,1:] # strip A off ARGB
+
+
+def alpha_view(qimage):
+    """Returns alpha view of a given 32-bit color QImage_'s memory.
+    The result is a 2D numpy.uint8 array, equivalent to
+    byte_view(qimage)[...,3].  The image must have 32 bit pixel size,
+    i.e. be RGB32, ARGB32, or ARGB32_Premultiplied.  Note that it is
+    not enforced that the given qimage has a format that actually
+    *uses* the alpha channel -- for Format_RGB32, the alpha channel
+    usually contains 255 everywhere.
+
+    For your convenience, `qimage` may also be a filename, see
+    `Loading and Saving Images`_ in the documentation.
+
+    :param qimage: image whose memory shall be accessed via NumPy
+    :type qimage: QImage_ with 32-bit pixel type
+    :rtype: numpy.ndarray_ with shape (height, width) and dtype uint8"""
+    bytes = byte_view(qimage, byteorder = None)
+    if bytes.shape[2] != 4:
+        raise ValueError("For alpha_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)")
+    return bytes[...,_bgra[3]]
+
+
+def recarray_view(qimage):
+    """Returns recarray_ view of a given 32-bit color QImage_'s
+    memory.
+
+    The result is a 2D array with a complex record dtype, offering the
+    named fields 'r','g','b', and 'a' and corresponding long names.
+    Thus, each color components can be accessed either via string
+    indexing or via attribute lookup (through numpy.recarray_):
+
+    For your convenience, `qimage` may also be a filename, see
+    `Loading and Saving Images`_ in the documentation.
+
+    >>> from PyQt4.QtGui import QImage, qRgb
+    >>> qimg = QImage(320, 240, QImage.Format_ARGB32)
+    >>> qimg.fill(qRgb(12,34,56))
+    >>>
+    >>> import qimage2ndarray
+    >>> v = qimage2ndarray.recarray_view(qimg)
+    >>>
+    >>> red = v["r"]
+    >>> red[10,10]
+    12
+    >>> pixel = v[10,10]
+    >>> pixel["r"]
+    12
+    >>> (v.g == v["g"]).all()
+    True
+    >>> (v.alpha == 255).all()
+    True
+
+    :param qimage: image whose memory shall be accessed via NumPy
+    :type qimage: QImage_ with 32-bit pixel type
+    :rtype: numpy.ndarray_ with shape (height, width) and dtype :data:`bgra_dtype`"""
+    raw = _qimage_or_filename_view(qimage)
+    if raw.itemsize != 4:
+        raise ValueError("For rgb_view, the image must have 32 bit pixel size (use RGB32, ARGB32, or ARGB32_Premultiplied)")
+    return raw.view(bgra_dtype, _np.recarray)
+
+
+
+def _normalize255(array, normalize, clip = (0, 255)):
+    if normalize:
+        if normalize is True:
+            normalize = array.min(), array.max()
+            if clip == (0, 255):
+                clip = None
+        elif _np.isscalar(normalize):
+            normalize = (0, normalize)
+
+        nmin, nmax = normalize
+
+        if nmin:
+            array = array - nmin
+
+        if nmax != nmin:
+            scale = 255. / (nmax - nmin)
+            if scale != 1.0:
+                array = array * scale
+
+    if clip:
+        low, high = clip
+        _np.clip(array, low, high, array)
+
+    return array
+
+
+def gray2qimage(gray, normalize = False):
+    """Convert the 2D numpy array `gray` into a 8-bit, indexed QImage_
+    with a gray colormap.  The first dimension represents the vertical
+    image axis.
+
+    The parameter `normalize` can be used to normalize an image's
+    value range to 0..255:
+
+    `normalize` = (nmin, nmax):
+      scale & clip image values from nmin..nmax to 0..255
+
+    `normalize` = nmax:
+      lets nmin default to zero, i.e. scale & clip the range 0..nmax
+      to 0..255
+
+    `normalize` = True:
+      scale image values to 0..255 (same as passing (gray.min(),
+      gray.max()))
+
+    If the source array `gray` contains masked values, the result will
+    have only 255 shades of gray, and one color map entry will be used
+    to make the corresponding pixels transparent.
+
+    A full alpha channel cannot be supported with indexed images;
+    instead, use `array2qimage` to convert into a 32-bit QImage.
+
+    :param gray: image data which should be converted (copied) into a QImage_
+    :type gray: 2D or 3D numpy.ndarray_ or `numpy.ma.array <masked arrays>`_
+    :param normalize: normalization parameter (see above, default: no value changing)
+    :type normalize: bool, scalar, or pair
+    :rtype: QImage_ with RGB32 or ARGB32 format"""
+    if _np.ndim(gray) != 2:
+        raise ValueError("gray2QImage can only convert 2D arrays" +
+                         " (try using array2qimage)" if _np.ndim(gray) == 3 else "")
+
+    h, w = gray.shape
+    result = _qt.QImage(w, h, _qt.QImage.Format_Indexed8)
+
+    if not _np.ma.is_masked(gray):
+        for i in range(256):
+            result.setColor(i, _qt.qRgb(i,i,i))
+
+        _qimageview(result)[:] = _normalize255(gray, normalize)
+    else:
+        # map gray value 1 to gray value 0, in order to make room for
+        # transparent colormap entry:
+        result.setColor(0, _qt.qRgb(0,0,0))
+        for i in range(2, 256):
+            result.setColor(i-1, _qt.qRgb(i,i,i))
+
+        _qimageview(result)[:] = _normalize255(gray, normalize, clip = (1, 255)) - 1
+
+        result.setColor(255, 0)
+        _qimageview(result)[gray.mask] = 255
+
+    return result
+
+
+def array2qimage(array, normalize = False):
+    """Convert a 2D or 3D numpy array into a 32-bit QImage_.  The
+    first dimension represents the vertical image axis; the optional
+    third dimension is supposed to contain 1-4 channels:
+
+    ========= ===================
+    #channels interpretation
+    ========= ===================
+            1 scalar/gray
+            2 scalar/gray + alpha
+            3 RGB
+            4 RGB + alpha
+    ========= ===================
+
+    Scalar data will be converted into corresponding gray RGB triples;
+    if you want to convert to an (indexed) 8-bit image instead, use
+    `gray2qimage` (which cannot support an alpha channel though).
+
+    The parameter `normalize` can be used to normalize an image's
+    value range to 0..255:
+
+    `normalize` = (nmin, nmax):
+      scale & clip image values from nmin..nmax to 0..255
+
+    `normalize` = nmax:
+      lets nmin default to zero, i.e. scale & clip the range 0..nmax
+      to 0..255
+
+    `normalize` = True:
+      scale image values to 0..255 (same as passing (array.min(),
+      array.max()))
+
+    If `array` contains masked values, the corresponding pixels will
+    be transparent in the result.  Thus, the result will be of
+    QImage.Format_ARGB32 if the input already contains an alpha
+    channel (i.e. has shape (H,W,4)) or if there are masked pixels,
+    and QImage.Format_RGB32 otherwise.
+
+    :param array: image data which should be converted (copied) into a QImage_
+    :type array: 2D or 3D numpy.ndarray_ or `numpy.ma.array <masked arrays>`_
+    :param normalize: normalization parameter (see above, default: no value changing)
+    :type normalize: bool, scalar, or pair
+    :rtype: QImage_ with RGB32 or ARGB32 format"""
+    if _np.ndim(array) == 2:
+        array = array[...,None]
+    elif _np.ndim(array) != 3:
+        raise ValueError("array2qimage can only convert 2D or 3D arrays (got %d dimensions)" % _np.ndim(array))
+    if array.shape[2] not in (1, 2, 3, 4):
+        raise ValueError("array2qimage expects the last dimension to contain exactly one (scalar/gray), two (gray+alpha), three (R,G,B), or four (R,G,B,A) channels")
+
+    h, w, channels = array.shape
+
+    hasAlpha = _np.ma.is_masked(array) or channels in (2, 4)
+    fmt = _qt.QImage.Format_ARGB32 if hasAlpha else _qt.QImage.Format_RGB32
+
+    result = _qt.QImage(w, h, fmt)
+
+    array = _normalize255(array, normalize)
+
+    if channels >= 3:
+        rgb_view(result)[:] = array[...,:3]
+    else:
+        rgb_view(result)[:] = array[...,:1] # scalar data
+
+    alpha = alpha_view(result)
+
+    if channels in (2, 4):
+        alpha[:] = array[...,-1]
+    else:
+        alpha[:] = 255
+
+    if _np.ma.is_masked(array):
+        alpha[:]  *= _np.logical_not(_np.any(array.mask, axis = -1))
+
+    return result
+
+
+def imread(filename, masked = False):
+    """Convenience function that uses the QImage_ constructor to read an
+    image from the given file and return an `rgb_view` of the result.
+    This is intentionally similar to scipy.ndimage.imread (which uses
+    PIL), scipy.misc.imread, or matplotlib.pyplot.imread (using PIL
+    for non-PNGs).
+
+    For grayscale images, return 2D array (even if it comes from a 32-bit
+    representation; this is a consequence of the QImage API).
+
+    For images with an alpha channel, the resulting number of channels
+    will be 2 (grayscale+alpha) or 4 (RGB+alpha).  Alternatively, one may
+    pass `masked = True' in order to get `numpy.ma.array <masked
+    arrays>`_ back.  Note that only fully transparent pixels are masked
+    (and that masked arrays only support binary masks).  The value of
+    `masked` is ignored when the loaded image has no alpha channel
+    (i.e., one would not get a masked array in that case).
+
+    This function has been added in version 1.3.
+
+    """
+    qImage = _qt.QImage(filename)
+
+    isGray = qImage.isGrayscale()
+    if isGray and qImage.depth() == 8:
+        return byte_view(qImage)[...,0]
+
+    hasAlpha = qImage.hasAlphaChannel()
+
+    if hasAlpha:
+        targetFormat = _qt.QImage.Format_ARGB32
+    else:
+        targetFormat = _qt.QImage.Format_RGB32
+    if qImage.format() != targetFormat:
+        qImage = qImage.convertToFormat(targetFormat)
+
+    result = rgb_view(qImage)
+    if isGray:
+        result = result[...,0]
+    if hasAlpha:
+        if masked:
+            mask = (alpha_view(qImage) == 0)
+            if _np.ndim(result) == 3:
+                mask = _np.repeat(mask[...,None], 3, axis = 2)
+            result = _np.ma.masked_array(result, mask)
+        else:
+            result = _np.dstack((result, alpha_view(qImage)))
+    return result
+
+
+def imsave(filename, image, normalize = False, format = None, quality = -1):
+    """Convenience function that uses QImage.save to save an image to the
+    given file.  This is intentionally similar to scipy.misc.imsave.
+    However, it supports different optional arguments:
+
+    :param normalize: see :func:`array2qimage` (which is used internally)
+    :param format: image filetype (e.g. 'PNG'),  (default: check filename's suffix)
+    :param quality: see QImage.save (0 = small .. 100 = uncompressed, -1 = default compression)
+    :returns: boolean success, see QImage.save
+    
+    This function has been added in version 1.4.
+    """
+    qImage = array2qimage(image, normalize = normalize)
+    return qImage.save(filename, format, quality)
diff --git a/libs/qimage2ndarray/dynqt.py b/libs/qimage2ndarray/dynqt.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4f1a18bce006d81d848dfd98d881901e3af5bda
--- /dev/null
+++ b/libs/qimage2ndarray/dynqt.py
@@ -0,0 +1,4 @@
+from .qt_driver import QtDriver
+
+qt = QtDriver()
+QtGui = qt.QtGui
diff --git a/libs/qimage2ndarray/qimageview_python.py b/libs/qimage2ndarray/qimageview_python.py
new file mode 100644
index 0000000000000000000000000000000000000000..83efb2ddf0f9351992aef016f7ce1f4ac2eb83c9
--- /dev/null
+++ b/libs/qimage2ndarray/qimageview_python.py
@@ -0,0 +1,60 @@
+import numpy as np
+from qimage2ndarray.dynqt import qt, QtGui
+
+def PyQt_data(image):
+    # PyQt4/PyQt5's QImage.bits() returns a sip.voidptr that supports
+    # conversion to string via asstring(size) or getting its base
+    # address via int(...):
+    return (int(image.bits()), False)
+
+def _re_buffer_address_match(buf_repr):
+    import re
+    _re_buffer_address = re.compile('<read-write buffer ptr 0x([0-9a-fA-F]*),')
+    global _re_buffer_address_match
+    _re_buffer_address_match = _re_buffer_address.match
+    return _re_buffer_address_match(buf_repr)
+
+def PySide_data(image):
+    # PySide's QImage.bits() returns a buffer object like this:
+    # <read-write buffer ptr 0x7fc3f4821600, size 76800 at 0x111269570>
+    ma = _re_buffer_address_match(repr(image.bits()))
+    assert ma, 'could not parse address from %r' % (image.bits(), )
+    return (int(ma.group(1), 16), False)
+
+getdata = dict(
+    PyQt4 = PyQt_data,
+    PyQt5 = PyQt_data,
+    PySide = PySide_data,
+)[qt.name()]
+
+
+def qimageview(image):
+    if not isinstance(image, QtGui.QImage):
+        raise TypeError("image argument must be a QImage instance")
+
+    shape = image.height(), image.width()
+    strides0 = image.bytesPerLine()
+
+    format = image.format()
+    if format == QtGui.QImage.Format_Indexed8:
+        dtype = "|u1"
+        strides1 = 1
+    elif format in (QtGui.QImage.Format_RGB32, QtGui.QImage.Format_ARGB32, QtGui.QImage.Format_ARGB32_Premultiplied):
+        dtype = "|u4"
+        strides1 = 4
+    elif format == QtGui.QImage.Format_Invalid:
+        raise ValueError("qimageview got invalid QImage")
+    else:
+        raise ValueError("qimageview can only handle 8- or 32-bit QImages")
+
+    image.__array_interface__ = {
+        'shape': shape,
+        'typestr': dtype,
+        'data': getdata(image),
+        'strides': (strides0, strides1),
+        'version': 3,
+    }
+
+    result = np.asarray(image)
+    del image.__array_interface__
+    return result
diff --git a/libs/qimage2ndarray/qt_driver.py b/libs/qimage2ndarray/qt_driver.py
new file mode 100644
index 0000000000000000000000000000000000000000..483cd287af1976d6c665860fa84d3304ece7273d
--- /dev/null
+++ b/libs/qimage2ndarray/qt_driver.py
@@ -0,0 +1,125 @@
+#  Copyright 2014-2014 Hans Meine <hans_meine@gmx.net>
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+"""This module contains a wrapper around three different Qt python bindings.
+It will dynamically decide which one to use:
+
+* First, the environment variable QT_DRIVER is checked
+  (may be one of 'PyQt5', 'PyQt4', 'PySide', 'PythonQt').
+* If unset, previously imported binding modules are detected (in sys.modules).
+* If no bindings are loaded, the environment variable QT_API is checked
+  (used by ETS and ipython, may be 'pyside' or 'pyqt').
+
+In order to have compatible behavior between the different bindings,
+PyQt4 (if used) is configured as follows::
+
+    sip.setapi("QString", 2)
+    sip.setapi("QVariant", 2)
+
+Furthermore, there is a 'getprop' function that solves the following
+problem: PythonQt exports Qt properties as Python properties *and*
+gives the precedence over getters with the same name.  Instead of
+calling getters with parentheses (which must not be used in PythonQt,
+but are required in PyQt and PySide), one may e.g. write
+`getprop(widget.width)`.
+"""
+
+import sys, os
+
+def getprop_PythonQt(prop):
+    """getprop(property_or_getter)
+
+    Used on getters that have the same name as a corresponding
+    property.  For PythonQt, this version will just return the
+    argument, which is assumed to be (the value of) a python property
+    through which PythonQt exposes Qt properties."""
+    return prop
+
+def getprop_other(getter):
+    """getprop(property_or_getter)
+
+    Used on getters that have the same name as a corresponding
+    property.  For Qt bindings other than PythonQt, this version will
+    return the result of calling the argument, which is assumed to be
+    a Qt getter function.  (With PythonQt, properties override getters
+    and no calling must be done.)"""
+    return getter()
+
+class QtDriver(object):
+    DRIVERS = ('PyQt5', 'PyQt4', 'PySide', 'PythonQt')
+    DEFAULT = 'PyQt4'
+    
+    @classmethod
+    def detect_qt(cls):
+        for drv in cls.DRIVERS:
+            if drv in sys.modules:
+                return drv
+        if '_PythonQt' in sys.modules:
+            return 'PythonQt'
+        return None
+
+    def name(self):
+        return self._drv
+
+    def getprop(self):
+        return getprop_PythonQt if self._drv == 'PythonQt' else getprop_other
+
+    def __init__(self, drv = os.environ.get('QT_DRIVER')):
+        """Supports QT_API (used by ETS and ipython)"""
+        if drv is None:
+            drv = self.detect_qt()
+        if drv is None:
+            drv = os.environ.get('QT_API')
+        if drv is None:
+            drv = self.DEFAULT
+        drv = {'pyside' : 'PySide', 'pyqt' : 'PyQt4', 'pyqt5' : 'PyQt5'}.get(drv, drv) # map ETS syntax
+        assert drv in self.DRIVERS
+        self._drv = drv
+
+    @staticmethod
+    def _initPyQt4():
+        """initialize PyQt4 to be compatible with PySide"""
+        if 'PyQt4.QtCore' in sys.modules:
+            # too late to configure API
+            pass
+        else:
+            import sip
+            sip.setapi("QString", 2)
+            sip.setapi("QVariant", 2)
+
+    @staticmethod
+    def requireCompatibleAPI():
+        """If PyQt4's API should be configured to be compatible with PySide's
+        (i.e. QString and QVariant should not be explicitly exported,
+        cf. documentation of sip.setapi()), call this function to check that
+        the PyQt4 was properly imported.  (It will always be configured this
+        way by this module, but it could have been imported before we got a
+        hand on doing so.)
+        """
+        if 'PyQt4.QtCore' in sys.modules:
+            import sip
+            for api in ('QVariant', 'QString'):
+                if sip.getapi(api) != 2:
+                    raise RuntimeError('%s API already set to V%d, but should be 2' % (api, sip.getapi(api)))
+            
+    def importMod(self, mod):
+        if self._drv == 'PyQt4':
+            self._initPyQt4()
+        qt = __import__('%s.%s' % (self._drv, mod))
+        return getattr(qt, mod)
+
+    def __getattr__(self, name):
+        if name.startswith('Qt'):
+            return self.importMod(name)
+        return super(QtDriver, self).__getattr__(name)
diff --git a/libs/qrangeslider-0.1.1/PKG-INFO b/libs/qrangeslider-0.1.1/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..5374908bfa8050c16057076ebc3c909efc4b6441
--- /dev/null
+++ b/libs/qrangeslider-0.1.1/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: qrangeslider
+Version: 0.1.1
+Summary: The QRangeSlider class implements a horizontal PyQt range slider widget.
+Home-page: http://github.com/rsgalloway/qrangeslider
+Author: Ryan Galloway
+Author-email: ryan@rsgalloway.com
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/libs/qrangeslider-0.1.1/README b/libs/qrangeslider-0.1.1/README
new file mode 100644
index 0000000000000000000000000000000000000000..8048e6bbc834e6f1fca94eccc5fb61b1c9bf0790
--- /dev/null
+++ b/libs/qrangeslider-0.1.1/README
@@ -0,0 +1,34 @@
+QRangeSlider
+
+Thu Apr 28 00:08:07 PDT 2011
+
+
+1 Overview
+~~~~~~~~~~
+
+The QRangeSlider class implements a horizontal PyQt range slider widget.
+
+  * README	    	this file
+  * LICENSE	    	the license under which QRangeSlider is released
+  * qrangeslider.py	qrangeslider python module
+  * examples.py		some usage examples
+
+
+2 Installation
+~~~~~~~~~~~~~~
+
+  $ sudo easy_install qrangeslider
+
+or download the source and run
+
+  $ sudo python setup.py install
+
+
+3 Basic Usage
+~~~~~~~~~~~~~
+
+  >>> from qrangeslider import QRangeSlider
+  >>> app = QtGui.QApplication(sys.argv)
+  >>> slider = QRangeSlider()
+  >>> slider.show()
+  >>> app.exec_()
\ No newline at end of file
diff --git a/libs/qrangeslider-0.1.1/qrangeslider.py b/libs/qrangeslider-0.1.1/qrangeslider.py
new file mode 100644
index 0000000000000000000000000000000000000000..1267a1a459b364bab0196f53979fa351ec40dff6
--- /dev/null
+++ b/libs/qrangeslider-0.1.1/qrangeslider.py
@@ -0,0 +1,521 @@
+#!/usr/bin/env python
+# ---------------------------------------------------------------------------------------------
+# Copyright (c) 2011-2014, Ryan Galloway (ryan@rsgalloway.com)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#  - Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+#  - Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+#  - Neither the name of the software nor the names of its contributors
+#    may be used to endorse or promote products derived from this software
+#    without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# ---------------------------------------------------------------------------------------------
+# docs and latest version available for download at
+#   http://rsgalloway.github.com/qrangeslider
+# ---------------------------------------------------------------------------------------------
+
+__author__ = "Ryan Galloway <ryan@rsgalloway.com>"
+__version__ = "0.1.1"
+
+
+# ---------------------------------------------------------------------------------------------
+# SUMMARY
+# ---------------------------------------------------------------------------------------------
+"""The QRangeSlider class implements a horizontal range slider widget.
+
+"""
+
+# ---------------------------------------------------------------------------------------------
+# TODO
+# ---------------------------------------------------------------------------------------------
+
+"""
+  - smoother mouse move event handler
+  - support splits and joins
+  - verticle sliders
+  - ticks
+  
+"""
+
+# ---------------------------------------------------------------------------------------------
+# IMPORTS
+# ---------------------------------------------------------------------------------------------
+import os
+import sys
+
+from PyQt4 import QtCore
+from PyQt4 import QtGui
+from PyQt4 import uic
+
+try:
+    _fromUtf8 = QtCore.QString.fromUtf8
+except AttributeError:
+    _fromUtf8 = lambda s: s
+
+__all__ = ['QRangeSlider']
+
+DEFAULT_CSS = """
+QRangeSlider * {
+    border: 0px;
+    padding: 0px;
+}
+QRangeSlider #Head {
+    background: #222;
+}
+QRangeSlider #Span {
+    background: #393;
+}
+QRangeSlider #Span:active {
+    background: #282;
+}
+QRangeSlider #Tail {
+    background: #222;
+}
+QRangeSlider > QSplitter::handle {
+    background: #393;
+}
+QRangeSlider > QSplitter::handle:vertical {
+    height: 4px;
+}
+QRangeSlider > QSplitter::handle:pressed {
+    background: #ca5;
+}
+
+"""
+
+def scale(val, src, dst):
+    """
+    Scale the given value from the scale of src to the scale of dst.
+    """
+    return int(((val - src[0]) / float(src[1]-src[0])) * (dst[1]-dst[0]) + dst[0])
+
+class Ui_Form(object):
+    """default range slider form"""
+    
+    def setupUi(self, Form):
+        Form.setObjectName(_fromUtf8("QRangeSlider"))
+        Form.resize(300, 30)
+        Form.setStyleSheet(_fromUtf8(DEFAULT_CSS))
+        self.gridLayout = QtGui.QGridLayout(Form)
+        self.gridLayout.setMargin(0)
+        self.gridLayout.setSpacing(0)
+        self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
+        self._splitter = QtGui.QSplitter(Form)
+        self._splitter.setMinimumSize(QtCore.QSize(0, 0))
+        self._splitter.setMaximumSize(QtCore.QSize(16777215, 16777215))
+        self._splitter.setOrientation(QtCore.Qt.Horizontal)
+        self._splitter.setObjectName(_fromUtf8("splitter"))
+        self._head = QtGui.QGroupBox(self._splitter)
+        self._head.setTitle(_fromUtf8(""))
+        self._head.setObjectName(_fromUtf8("Head"))
+        self._handle = QtGui.QGroupBox(self._splitter)
+        self._handle.setTitle(_fromUtf8(""))
+        self._handle.setObjectName(_fromUtf8("Span"))
+        self._tail = QtGui.QGroupBox(self._splitter)
+        self._tail.setTitle(_fromUtf8(""))
+        self._tail.setObjectName(_fromUtf8("Tail"))
+        self.gridLayout.addWidget(self._splitter, 0, 0, 1, 1)
+
+        self.retranslateUi(Form)
+        QtCore.QMetaObject.connectSlotsByName(Form)
+
+    def retranslateUi(self, Form):
+        encoding = QtGui.QApplication.UnicodeUTF8
+        Form.setWindowTitle(QtGui.QApplication.translate("QRangeSlider", 
+                                                         "QRangeSlider",
+                                                         None, encoding))
+
+
+class Element(QtGui.QGroupBox):
+    
+    def __init__(self, parent, main):
+        super(Element, self).__init__(parent)
+        self.main = main
+        
+    def setStyleSheet(self, style):
+        """redirect style to parent groupbox"""
+        self.parent().setStyleSheet(style)
+
+    def textColor(self):
+        """text paint color"""
+        return getattr(self, '__textColor', QtGui.QColor(125, 125, 125))
+
+    def setTextColor(self, color):
+        """set the text paint color"""
+        if type(color) == tuple and len(color) == 3:
+            color = QtGui.QColor(color[0], color[1], color[2])
+        elif type(color) == int:
+            color = QtGui.QColor(color, color, color)
+        setattr(self, '__textColor', color)
+
+    def paintEvent(self, event):
+        """overrides paint event to handle text"""
+        qp = QtGui.QPainter()
+        qp.begin(self)
+        if self.main.drawValues():
+            self.drawText(event, qp)
+        qp.end()
+
+
+class Head(Element):
+    """area before the handle"""
+    
+    def __init__(self, parent, main):
+        super(Head, self).__init__(parent, main)
+
+    def drawText(self, event, qp):
+        qp.setPen(self.textColor())
+        qp.setFont(QtGui.QFont('Arial', 10))
+        qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.min()))
+
+
+class Tail(Element):
+    """area after the handle"""
+    
+    def __init__(self, parent, main):
+        super(Tail, self).__init__(parent, main)
+        
+    def drawText(self, event, qp):
+        qp.setPen(self.textColor())
+        qp.setFont(QtGui.QFont('Arial', 10))
+        qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.max()))
+
+
+class Handle(Element):
+    """handle area"""
+    
+    def __init__(self, parent, main):
+        super(Handle, self).__init__(parent, main)
+        
+    def drawText(self, event, qp):
+        qp.setPen(self.textColor())
+        qp.setFont(QtGui.QFont('Arial', 10))
+        qp.drawText(event.rect(), QtCore.Qt.AlignLeft, str(self.main.start()))
+        qp.drawText(event.rect(), QtCore.Qt.AlignRight, str(self.main.end()))
+
+    def mouseMoveEvent(self, event):
+        event.accept()
+        mx = event.globalX()
+        _mx = getattr(self, '__mx', None)
+        
+        if not _mx:  
+            setattr(self, '__mx', mx)
+            dx = 0
+        else:
+            dx = mx - _mx
+
+        setattr(self, '__mx', mx)
+
+        if dx == 0:
+            event.ignore()
+            return
+        elif dx > 0:
+            dx = 1
+        elif dx < 0:
+            dx = -1
+        
+        s = self.main.start() + dx
+        e = self.main.end() + dx
+        if s >= self.main.min() and e <= self.main.max():
+            self.main.setRange(s, e)
+
+
+class QRangeSlider(QtGui.QWidget, Ui_Form):
+    """
+    The QRangeSlider class implements a horizontal range slider widget.
+
+    Inherits QWidget.
+
+    Methods
+
+        * __init__ (self, QWidget parent = None)
+        * bool drawValues (self)
+        * int end (self)
+        * (int, int) getRange (self)
+        * int max (self)
+        * int min (self)
+        * int start (self)
+        * setBackgroundStyle (self, QString styleSheet)
+        * setDrawValues (self, bool draw)
+        * setEnd (self, int end)
+        * setStart (self, int start)
+        * setRange (self, int start, int end)
+        * setSpanStyle (self, QString styleSheet)
+
+    Signals
+
+        * endValueChanged (int)
+        * maxValueChanged (int)
+        * minValueChanged (int)
+        * startValueChanged (int)
+
+    Customizing QRangeSlider
+
+    You can style the range slider as below:
+    ::
+        QRangeSlider * {
+            border: 0px;
+            padding: 0px;
+        }
+        QRangeSlider #Head {
+            background: #222;
+        }
+        QRangeSlider #Span {
+            background: #393;
+        }
+        QRangeSlider #Span:active {
+            background: #282;
+        }
+        QRangeSlider #Tail {
+            background: #222;
+        }
+
+    Styling the range slider handles follows QSplitter options:
+    ::
+        QRangeSlider > QSplitter::handle {
+            background: #393;
+        }
+        QRangeSlider > QSplitter::handle:vertical {
+            height: 4px;
+        }
+        QRangeSlider > QSplitter::handle:pressed {
+            background: #ca5;
+        }
+        
+    """
+    endValueChanged = QtCore.pyqtSignal(int)
+    maxValueChanged = QtCore.pyqtSignal(int)
+    minValueChanged = QtCore.pyqtSignal(int)
+    startValueChanged = QtCore.pyqtSignal(int)
+
+    # define splitter indices
+    _SPLIT_START = 1
+    _SPLIT_END = 2
+
+    # signals
+    minValueChanged = QtCore.pyqtSignal(int)
+    maxValueChanged = QtCore.pyqtSignal(int)
+    startValueChanged = QtCore.pyqtSignal(int)
+    endValueChanged = QtCore.pyqtSignal(int)
+
+    def __init__(self, parent=None):
+        """Create a new QRangeSlider instance.
+        
+            :param parent: QWidget parent
+            :return: New QRangeSlider instance.
+        
+        """
+        super(QRangeSlider, self).__init__(parent)
+        self.setupUi(self)
+        self.setMouseTracking(False)
+
+        #self._splitter.setChildrenCollapsible(False)
+        self._splitter.splitterMoved.connect(self._handleMoveSplitter)
+
+        # head layout
+        self._head_layout = QtGui.QHBoxLayout()
+        self._head_layout.setSpacing(0)
+        self._head_layout.setMargin(0)
+        self._head.setLayout(self._head_layout)
+        self.head = Head(self._head, main=self)
+        self._head_layout.addWidget(self.head)
+
+        # handle layout
+        self._handle_layout = QtGui.QHBoxLayout()
+        self._handle_layout.setSpacing(0)
+        self._handle_layout.setMargin(0)
+        self._handle.setLayout(self._handle_layout)
+        self.handle = Handle(self._handle, main=self)
+        self.handle.setTextColor((150, 255, 150))
+        self._handle_layout.addWidget(self.handle)
+
+        # tail layout
+        self._tail_layout = QtGui.QHBoxLayout()
+        self._tail_layout.setSpacing(0)
+        self._tail_layout.setMargin(0)
+        self._tail.setLayout(self._tail_layout)
+        self.tail = Tail(self._tail, main=self)
+        self._tail_layout.addWidget(self.tail)
+
+        # defaults
+        self.setMin(0)
+        self.setMax(99)
+        self.setStart(0)
+        self.setEnd(99)
+        self.setDrawValues(True)
+
+    def min(self):
+        """:return: minimum value"""
+        return getattr(self, '__min', None)
+
+    def max(self):
+        """:return: maximum value"""
+        return getattr(self, '__max', None)
+    
+    def setMin(self, value):
+        """sets minimum value"""
+        assert type(value) is int
+        setattr(self, '__min', value)
+        self.minValueChanged.emit(value)
+
+    def setMax(self, value):
+        """sets maximum value"""
+        assert type(value) is int
+        setattr(self, '__max', value)
+        self.maxValueChanged.emit(value)
+    
+    def start(self):
+        """:return: range slider start value"""
+        return getattr(self, '__start', None)
+
+    def end(self):
+        """:return: range slider end value"""
+        return getattr(self, '__end', None)
+
+    def _setStart(self, value):
+        """stores the start value only"""
+        setattr(self, '__start', value)
+        self.startValueChanged.emit(value)
+    
+    def setStart(self, value):
+        """sets the range slider start value"""
+        assert type(value) is int
+        v = self._valueToPos(value)
+        self._splitter.splitterMoved.disconnect()
+        self._splitter.moveSplitter(v, self._SPLIT_START)
+        self._splitter.splitterMoved.connect(self._handleMoveSplitter)
+        self._setStart(value)
+
+    def _setEnd(self, value):
+        """stores the end value only"""
+        setattr(self, '__end', value)
+        self.endValueChanged.emit(value)
+    
+    def setEnd(self, value):
+        """set the range slider end value"""
+        assert type(value) is int
+        v = self._valueToPos(value)
+        self._splitter.splitterMoved.disconnect()
+        self._splitter.moveSplitter(v, self._SPLIT_END)
+        self._splitter.splitterMoved.connect(self._handleMoveSplitter)
+        self._setEnd(value)
+
+    def drawValues(self):
+        """:return: True if slider values will be drawn"""
+        return getattr(self, '__drawValues', None)
+    
+    def setDrawValues(self, draw):
+        """sets draw values boolean to draw slider values"""
+        assert type(draw) is bool
+        setattr(self, '__drawValues', draw)
+
+    def getRange(self):
+        """:return: the start and end values as a tuple"""
+        return (self.start(), self.end())
+
+    def setRange(self, start, end):
+        """set the start and end values"""
+        self.setStart(start)
+        self.setEnd(end)
+        
+    def keyPressEvent(self, event):
+        """overrides key press event to move range left and right"""
+        key = event.key()
+        if key == QtCore.Qt.Key_Left:
+            s = self.start()-1
+            e = self.end()-1
+        elif key == QtCore.Qt.Key_Right:
+            s = self.start()+1
+            e = self.end()+1
+        else:
+            event.ignore()
+            return
+        event.accept()
+        if s >= self.min() and e <= self.max():
+            self.setRange(s, e)
+
+    def setBackgroundStyle(self, style):
+        """sets background style"""
+        self._tail.setStyleSheet(style)
+        self._head.setStyleSheet(style)
+
+    def setSpanStyle(self, style):
+        """sets range span handle style"""
+        self._handle.setStyleSheet(style)
+
+    def _valueToPos(self, value):
+        """converts slider value to local pixel x coord"""
+        return scale(value, (self.min(), self.max()), (0, self.width()))
+
+    def _posToValue(self, xpos):
+        """converts local pixel x coord to slider value"""
+        return scale(xpos, (0, self.width()), (self.min(), self.max()))
+
+    def _handleMoveSplitter(self, xpos, index):
+        """private method for handling moving splitter handles"""
+        hw = self._splitter.handleWidth()
+        
+        def _lockWidth(widget):
+            width = widget.size().width()
+            widget.setMinimumWidth(width)
+            widget.setMaximumWidth(width)
+            
+        def _unlockWidth(widget):
+            widget.setMinimumWidth(0)
+            widget.setMaximumWidth(16777215)
+        
+        v = self._posToValue(xpos)
+        
+        if index == self._SPLIT_START:
+            _lockWidth(self._tail)
+            if v >= self.end():
+                return
+            
+            offset = -20
+            w = xpos + offset
+            self._setStart(v)
+            
+        elif index == self._SPLIT_END:
+            _lockWidth(self._head)
+            if v <= self.start():
+                return
+            
+            offset = -40
+            w = self.width() - xpos + offset
+            self._setEnd(v)
+
+        _unlockWidth(self._tail)
+        _unlockWidth(self._head)
+        _unlockWidth(self._handle)
+
+
+#-------------------------------------------------------------------------------
+# MAIN
+#-------------------------------------------------------------------------------
+
+if __name__ == '__main__':
+    app = QtGui.QApplication(sys.argv)
+    rs = QRangeSlider()
+    rs.show()
+    rs.setRange(15, 35)
+    rs.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
+    rs.handle.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
+    app.exec_()
diff --git a/libs/qrangeslider-0.1.1/setup.py b/libs/qrangeslider-0.1.1/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..76fd96a6274e1b39ae0bcd207f0d4dac5ca4d190
--- /dev/null
+++ b/libs/qrangeslider-0.1.1/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# 
+# Copyright (C) 2011-2014 Ryan Galloway (ryan@rsgalloway.com)
+#
+# This module is part of Shotman and is released under
+# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+
+from distutils.core import setup
+from qrangeslider import __version__
+setup(name='qrangeslider',
+      version=__version__,
+      description='The QRangeSlider class implements a horizontal PyQt range slider widget.',
+      author='Ryan Galloway',
+      author_email='ryan@rsgalloway.com',
+      url='http://github.com/rsgalloway/qrangeslider',
+      py_modules=['qrangeslider']
+      )
diff --git a/sensecarbon_tsv.py b/sensecarbon_tsv.py
index e673346fe15607c2ba7ba52d28d4c57d79215da4..703708a510dd4b0263a5ae7febfe110825f8b84d 100644
--- a/sensecarbon_tsv.py
+++ b/sensecarbon_tsv.py
@@ -55,9 +55,12 @@ if os.path.exists(path):
 
 pluginDir = os.path.dirname(__file__)
 sys.path.append(pluginDir)
-sys.path.append(os.path.join(pluginDir, 'qimage2ndarray'))
+sys.path.append(os.path.join(pluginDir, 'libs'))
+#sys.path.append(os.path.join(pluginDir, *['libs','qimage2ndarray']))
+sys.path.append(os.path.join(pluginDir, *['libs','qrangeslider-0.1.1']))
 
 import qimage2ndarray
+import qrangeslider
 
 from PyQt4.QtCore import *
 from PyQt4.QtGui import *
@@ -1296,6 +1299,11 @@ class SenseCarbon_TSV:
             TS = TimeSeries.loadFromFile(path)
             self.init_TimeSeries(TS)
             self.ua_datumAdded()
+
+            if len(self.BAND_VIEWS) == 0:
+                self.ua_addBandView([3, 2, 1])
+                self.ua_addBandView([4, 5, 3])
+
         self.check_enabled()
 
     def ua_saveTSFile(self):