Repository: vispy/vispy

Issue:
VisPy isocurve incompatible with matplotlib 2.2.0
Matplotlib 2.2 is now out and the private contour api `from matplotlib import _cntr` has been removed. We knew this day would come. @QuLogic pointed this thread out on gitter: https://mail.python.org/pipermail/matplotlib-users/2018-March/001313.html

The TL;DR of that is that we can either copy the source from old mpl releases or we could try using skimage. I'm not a huge fan of adding such a huge (optional) dependency on to vispy, but as `isocurve.py` sits right now it uses the `_cntr` classes as an optional dependency so it shouldn't be a huge deal.

http://scikit-image.org/docs/dev/auto_examples/edges/plot_contours.html

As a quick hack I will try to make a PR in the next couple days to switch the import of `_cntr` to a try/except and use the slow contouring method if it isn't found. That should at least let our tests pass and give github users working code.


Discussion:
Note that there is GPU only solution but I don't know to what extent "real" contour lines are needed.
Yes, we also have the IsolineFilter which can be attached to eg. Image Visual. 
Ah, ok. Sorry for the noise.
@rougier Did you mean with GPU-method something like in IsolineFilter?
I'm not sure actually. I've made them for glumpy (https://github.com/glumpy/glumpy/blob/master/examples/isocurves.py) but I don't remember if I've ported them to vispy...

![isocurves](https://user-images.githubusercontent.com/327203/37111152-48d41e4c-223f-11e8-8211-456ccffca40d.png)

@rougier Actually we, means you, @larsoner and I discussed this some years ago. This was implemented when we did the spatial-filters using packed floats stuff. Here is the example https://github.com/vispy/vispy/blob/master/examples/basics/scene/contour.py
You surely remember the eye of Mona Lisa. 
I'm currently working on replacing the mpl cntr usage with skimage. Is everyone (@larsoner, @kmuehlbauer, @rougier) ok with me completely removing the mpl usage?

My thinking is if someone wants new vispy features then they should be ok making updates to all packages including MPL. Then again, they may not have skimage installed.
Yes mpl 1.2 is too old to bother with a `try`/`except`. Feel free to do a `try/except` with `skimage` to make it fast if you want
@larsoner my message on gitter had the wrong version. This is mpl 2.2 which is the latest release.
I'm fine with a `try`/`except` for `skimage`. Just make sure similar functionality isn't available in `scipy` as it seems like a more common thing to have.
Google shows nothing from scipy (but that could be me messing up my google cache with skimage results). The mailing list I linked to at the top suggests skimage from the mpl devs: https://mail.python.org/pipermail/matplotlib-users/2018-March/001313.html

Resolving patch:
diff --git a/vispy/visuals/isocurve.py b/vispy/visuals/isocurve.py
--- a/vispy/visuals/isocurve.py
+++ b/vispy/visuals/isocurve.py
@@ -10,18 +10,16 @@
 from ..color import ColorArray
 from ..color.colormap import _normalize, get_colormap
 from ..geometry.isocurve import isocurve
-from ..testing import has_matplotlib
+from ..testing import has_skimage
 
-# checking for matplotlib
-_HAS_MPL = has_matplotlib()
-if _HAS_MPL:
+# checking for scikit-image
+_HAS_SKI = has_skimage()
+if _HAS_SKI:
     try:
-        from matplotlib import _cntr as cntr
+        from skimage.measure import find_contours
     except ImportError:
-        import warnings
-        warnings.warn("VisPy is not yet compatible with matplotlib 2.2+")
-        _HAS_MPL = False
-        cntr = None
+        _HAS_SKI = False
+        find_contours = None
 
 
 class IsocurveVisual(LineVisual):
@@ -55,9 +53,6 @@ def __init__(self, data=None, levels=None, color_lev=None, clim=None,
         self._need_color_update = True
         self._need_level_update = True
         self._need_recompute = True
-        self._X = None
-        self._Y = None
-        self._iso = None
         self._level_min = None
         self._data_is_uniform = False
         self._lc = None
@@ -107,14 +102,6 @@ def set_data(self, data):
         """
         self._data = data
 
-        # if using matplotlib isoline algorithm we have to check for meshgrid
-        # and we can setup the tracer object here
-        if _HAS_MPL:
-            if self._X is None or self._X.shape != data.shape:
-                self._X, self._Y = np.meshgrid(np.arange(data.shape[1]),
-                                               np.arange(data.shape[0]))
-            self._iso = cntr.Cntr(self._X, self._Y, self._data.astype(float))
-
         if self._clim is None:
             self._clim = (data.min(), data.max())
 
@@ -156,13 +143,15 @@ def _compute_iso_line(self):
         self._level_min = choice[0][0]
 
         for level in levels_to_calc:
-            # if we use matplotlib isoline algorithm we need to add half a
+            # if we use skimage isoline algorithm we need to add half a
             # pixel in both (x,y) dimensions because isolines are aligned to
             # pixel centers
-            if _HAS_MPL:
-                nlist = self._iso.trace(level, level, 0)
-                paths = nlist[:len(nlist)//2]
-                v, c = self._get_verts_and_connect(paths)
+            if _HAS_SKI:
+                contours = find_contours(self._data, level,
+                                         positive_orientation='high')
+                v, c = self._get_verts_and_connect(contours)
+                # swap row, column to column, row (x, y)
+                v[:, [0, 1]] = v[:, [1, 0]]
                 v += np.array([0.5, 0.5])
             else:
                 paths = isocurve(self._data.astype(float).T, level,


Test patch:
diff --git a/vispy/testing/__init__.py b/vispy/testing/__init__.py
--- a/vispy/testing/__init__.py
+++ b/vispy/testing/__init__.py
@@ -43,7 +43,7 @@
 from ._testing import (SkipTest, requires_application, requires_ipython,  # noqa
                       requires_img_lib,  # noqa
                       has_backend, requires_pyopengl,  # noqa
-                      requires_scipy, has_matplotlib,  # noqa
+                      requires_scipy, has_matplotlib, has_skimage,  # noqa
                       save_testing_image, TestingCanvas, has_pyopengl,  # noqa
                       run_tests_if_main,
                       assert_is, assert_in, assert_not_in, assert_equal,
diff --git a/vispy/testing/_testing.py b/vispy/testing/_testing.py
--- a/vispy/testing/_testing.py
+++ b/vispy/testing/_testing.py
@@ -294,6 +294,16 @@ def has_matplotlib(version='1.2'):
     return has_mpl
 
 
+def has_skimage(version='0.11'):
+    """Determine if scikit-image is a usable version"""
+    try:
+        import skimage
+    except ImportError:
+        return False
+    sk_version = LooseVersion(skimage.__version__)
+    return sk_version >= LooseVersion(version)
+
+
 ###############################################################################
 # Visuals stuff