Repository: PMEAL/porespy

Issue:
The color map in the xray visualization function should be reversed
The brightness does not match the stated behavior (solids attenuate xray transmission).

Also, the docstring links to the wrong page.

Hints from issue comments:

Solution patch:
diff --git a/src/porespy/visualization/_funcs.py b/src/porespy/visualization/_funcs.py
index a984b0ec4..b0a1c8de0 100644
--- a/src/porespy/visualization/_funcs.py
+++ b/src/porespy/visualization/_funcs.py
@@ -63,9 +63,17 @@ def set_mpl_style():  # pragma: no cover
     matplotlib_inline.backend_inline.set_matplotlib_formats('retina')
 
 
-def satn_to_movie(im, satn, cmap='viridis',
-                  c_under='grey', c_over='white',
-                  v_under=1e-3, v_over=1.0, fps=10, repeat=True):
+def satn_to_movie(
+    im,
+    satn,
+    cmap='viridis',
+    c_under='grey',
+    c_over='white',
+    v_under=1e-3,
+    v_over=1.0,
+    fps=10,
+    repeat=True,
+):
     r"""
     Converts a saturation map into an animation that can be saved
 
@@ -80,12 +88,12 @@ def satn_to_movie(im, satn, cmap='viridis',
     cmap : str
         The name of the matplotlib color map to use. These are listed on
         matplotlib's website
-        `here <https://matplotlib.org/stable/gallery/color/colormap_reference.html>`__
+        `here <https://matplotlib.org/stable/gallery/color/colormap_reference.html>`_
     c_under, c_over : str
         The color to insert for values that are less than `v_under`
         (greater than `v_over`).  The string value of colors are given on
         matplotlib's website
-        `here <https://matplotlib.org/stable/gallery/color/named_colors.html>`__
+        `here <https://matplotlib.org/stable/gallery/color/named_colors.html>`_
     v_under, v_over : scalar
         The values in ``satn`` that should be considered the lower and upper
         threshold, beyond which the colors given in `c_under` and `c_over`
@@ -134,7 +142,14 @@ def satn_to_movie(im, satn, cmap='viridis',
     return ani
 
 
-def satn_to_panels(satn, im, bins=None, axis=0, slice=None, **kwargs):
+def satn_to_panels(
+    satn,
+    im,
+    bins=None,
+    axis=0,
+    slice=None,
+    **kwargs,
+):
     r"""
     Produces a set of images with each panel containing one saturation
 
@@ -209,7 +224,12 @@ def factors(n):
     return fig, ax
 
 
-def prep_for_imshow(im, mask=None, axis=0, slice=None):
+def prep_for_imshow(
+    im,
+    mask=None,
+    axis=0,
+    slice=None,
+):
     r"""
     Adjusts the range of greyscale values in an image to improve visualization
     by ``matplotlib.pyplot.imshow``
@@ -242,7 +262,7 @@ def prep_for_imshow(im, mask=None, axis=0, slice=None):
         ``plt.imshow(\*\*data)``).  It contains the following key-value pairs:
 
         =============== =======================================================
-        key               value
+        key             value
         =============== =======================================================
         'X'             The adjusted image with ``+inf`` replaced by
                         ``vmax + 1``, and all solid voxels replacd by
@@ -283,6 +303,11 @@ def prep_for_imshow(im, mask=None, axis=0, slice=None):
     im[(im == np.inf)] = vmax + 1
     vmin = np.amin((im*(im > -np.inf))[mask])
     im[(im == -np.inf)] = vmin - 1
-
-    return {'X': im, 'vmin': vmin, 'vmax': vmax,
-            'interpolation': 'none', 'origin': 'lower'}
+    d = {
+        'X': im,
+        'vmin': vmin,
+        'vmax': vmax,
+        'interpolation': 'none',
+        'origin': 'lower',
+    }
+    return d
diff --git a/src/porespy/visualization/_plots.py b/src/porespy/visualization/_plots.py
index 1bbfe9a13..7972d7761 100644
--- a/src/porespy/visualization/_plots.py
+++ b/src/porespy/visualization/_plots.py
@@ -54,27 +54,27 @@ def show_panels(im, rc=[3, 3], axis=0):
 
 def bar(results, h='pdf', **kwargs):  # pragma: no cover
     r"""
-    Convenience wrapper for matplotlib's ``bar``.
+    Convenience wrapper for matplotlib's `bar`.
 
     This automatically:
 
-        * fetches the ``bin_centers``
-        * fetches the bin heights from the specified ``h``
-        * sets the bin widths
-        * sets the edges to black
+        * Fetches the `bin_centers`
+        * Fetches the bin heights from the specified `h`
+        * Sets the bin widths
+        * Sets the edges to black
 
     Parameters
     ----------
     results : object
         The objects returned by various functions in the
-        ``porespy.metrics`` submodule, such as ``chord_length_distribution``.
+        `porespy.metrics` submodule, such as `chord_length_distribution`.
     h : str
-        The value to use for bin heights.  The default is ``pdf``, but
-        ``cdf`` is another option. Depending on the function the named-tuple
+        The value to use for bin heights.  The default is `pdf`, but
+        `cdf` is another option. Depending on the function the named-tuple
         may have different options.
     kwargs : keyword arguments
-        All other keyword arguments are passed to ``bar``, including
-        ``edgecolor`` if you wish to overwrite the default black.
+        All other keyword arguments are passed to `bar`, including
+        `edgecolor` if you wish to overwrite the default black.
 
     Returns
     -------
@@ -88,17 +88,22 @@ def bar(results, h='pdf', **kwargs):  # pragma: no cover
     """
     if 'edgecolor' not in kwargs:
         kwargs['edgecolor'] = 'k'
-    fig = plt.bar(x=results.bin_centers, height=getattr(results, h),
-                  width=results.bin_widths, **kwargs)
+    fig, ax = plt.subplots()
+    ax.bar(
+        x=results.bin_centers,
+        height=getattr(results, h),
+        width=results.bin_widths,
+        **kwargs,
+    )
     xlab = [attr for attr in results.__dir__() if not attr.startswith('_')][0]
-    plt.xlabel(xlab)
-    plt.ylabel(h)
-    return fig
+    ax.set_xlabel(xlab)
+    ax.set_ylabel(h)
+    return fig, ax
 
 
-def imshow(*im, ind=None, axis=None, **kwargs):  # pragma: no cover
+def imshow(im, ind=None, axis=None, **kwargs):  # pragma: no cover
     r"""
-    Convenience wrapper for matplotlib's ``imshow``.
+    Convenience wrapper for matplotlib's `imshow`.
 
     This automatically:
 
@@ -110,22 +115,21 @@ def imshow(*im, ind=None, axis=None, **kwargs):  # pragma: no cover
     Parameters
     ----------
     im : ndarray
-        The 2D or 3D image (or images) to show.  If 2D then all other
-        arguments are ignored.
+        The 2D or 3D image to show.  If 2D then all other arguments are ignored.
     ind : int
-        The slice to show if ``im`` is 3D.  If not given then the middle of
+        The slice to show if `im` is 3D.  If not given then the middle of
         the image is used.
     axis : int
-        The axis to show if ``im`` is 3D.  If not given, then the last
+        The axis to show if `im` is 3D.  If not given, then the last
         axis of the image is used, so an 'lower' slice is shown.
 
     **kwargs
-        All other keyword arguments are passed to ``plt.imshow``
+        All other keyword arguments are passed to `plt.imshow`
 
     Note
     ----
-    ``im`` can also be a series of unnamed arguments, in which case all
-    received images will be shown using ``subplot``.
+    `im` can also be a series of unnamed arguments, in which case all
+    received images will be shown using `subplot`.
 
     Examples
     --------
@@ -137,30 +141,27 @@ def imshow(*im, ind=None, axis=None, **kwargs):  # pragma: no cover
         kwargs['origin'] = 'lower'
     if 'interpolation' not in kwargs.keys():
         kwargs['interpolation'] = 'none'
-    if not isinstance(im, tuple):
-        im = tuple([im])
-    for i, image in enumerate(im):
-        if image.ndim == 3:
-            if axis is None:
-                axis = 2
-            if ind is None:
-                ind = int(image.shape[axis]/2)
-            image = image.take(indices=ind, axis=axis)
-        image = np.ma.array(image, mask=image == 0)
-        fig = plt.subplot(1, len(im), i+1)
-        plt.imshow(image, **kwargs)
-    return fig
+    if im.ndim == 3:
+        if axis is None:
+            axis = 2
+        if ind is None:
+            ind = int(im.shape[axis]/2)
+        im = im.take(indices=ind, axis=axis)
+    im = np.ma.array(im, mask=(im == 0))
+    fig, ax = plt.subplots()
+    ax.imshow(im, **kwargs)
+    return fig, ax
 
 
 def show_mesh(mesh):  # pragma: no cover
     r"""
-    Visualizes the mesh of a region as obtained by ``get_mesh`` function in
-    the ``metrics`` submodule.
+    Visualizes the mesh of a region as obtained by `get_mesh` function in the
+    `metrics` submodule.
 
     Parameters
     ----------
     mesh : tuple
-        A mesh returned by ``skimage.measure.marching_cubes``
+        A mesh returned by `skimage.measure.marching_cubes`
 
     Returns
     -------
@@ -197,4 +198,4 @@ def show_mesh(mesh):  # pragma: no cover
     ax.set_ylim(lim_min[1], lim_max[1])
     ax.set_zlim(lim_min[2], lim_max[2])
 
-    return fig
+    return fig, ax
diff --git a/src/porespy/visualization/_views.py b/src/porespy/visualization/_views.py
index 99205b9f9..b52b61d08 100644
--- a/src/porespy/visualization/_views.py
+++ b/src/porespy/visualization/_views.py
@@ -2,8 +2,6 @@
 import scipy.ndimage as spim
 from numba import njit, prange
 
-# from mpl_toolkits.mplot3d.art3d import Poly3DCollection
-
 
 __all__ = [
     "show_3D",
@@ -17,8 +15,8 @@ def show_3D(im):  # pragma: no cover
     r"""
     Rotates a 3D image and creates an angled view for rough 2D visualization.
 
-    Because it rotates the image it can be slow for large images, so is
-    mostly meant for rough checking of small prototype images.
+    Because it rotates the image it can be slow for large images, so is mostly
+    meant for rough checking of small prototype images.
 
     Parameters
     ----------
@@ -33,7 +31,7 @@ def show_3D(im):  # pragma: no cover
     Notes
     -----
     Although this is meant to be *quick* it can still take on the order of
-    minutes to render very large images.  It uses ``scipy.ndimage.rotate``
+    minutes to render very large images.  It uses `scipy.ndimage.rotate`
     with no interpolation to view the 3D image from an angle, then casts the
     result into a 2D projection.
 
@@ -74,7 +72,7 @@ def show_planes(im, spacing=10):  # pragma: no cover
     -------
     image : ndarray
         A 2D array containing the views.  This single image can be viewed using
-        ``matplotlib.pyplot.imshow``.
+        `matplotlib.pyplot.imshow`.
 
     Examples
     --------
@@ -92,18 +90,16 @@ def show_planes(im, spacing=10):  # pragma: no cover
     im_yz = np.rot90(im[x, :, :])
 
     new_x = im_xy.shape[0] + im_yz.shape[0] + s
-
     new_y = im_xy.shape[1] + im_xz.shape[1] + s
-
     new_im = np.zeros([new_x + 2 * s, new_y + 2 * s], dtype=im.dtype)
 
     # Add xy image to upper left corner
-    new_im[s : im_xy.shape[0] + s, s : im_xy.shape[1] + s] = im_xy
+    new_im[s: im_xy.shape[0] + s, s: im_xy.shape[1] + s] = im_xy
     # Add xz image to lower left coner
     x_off = im_xy.shape[0] + 2 * s
     y_off = im_xy.shape[1] + 2 * s
-    new_im[s : s + im_xz.shape[0], y_off : y_off + im_xz.shape[1]] = im_xz
-    new_im[x_off : x_off + im_yz.shape[0], s : s + im_yz.shape[1]] = im_yz
+    new_im[s: s + im_xz.shape[0], y_off: y_off + im_xz.shape[1]] = im_xz
+    new_im[x_off: x_off + im_yz.shape[0], s: s + im_yz.shape[1]] = im_yz
 
     return new_im
 
@@ -126,7 +122,7 @@ def sem(im, axis=0):  # pragma: no cover
     Returns
     -------
     image : ndarray
-        A 2D greyscale image suitable for use in matplotlib's ``imshow``
+        A 2D greyscale image suitable for use in matplotlib's `imshow`
         function.
 
     Examples
@@ -172,8 +168,7 @@ def xray(im, axis=0):  # pragma: no cover
     Parameters
     ----------
     im : array_like
-        ndarray of the porous material with the solid phase marked as 1 or
-        True
+        ndarray of the porous material with the solid phase marked as 1 or `True`
 
     axis : int
         Specifes the axis along which the camera will point.
@@ -181,8 +176,7 @@ def xray(im, axis=0):  # pragma: no cover
     Returns
     -------
     image : ndarray
-        A 2D greyscale image suitable for use in matplotlib\'s ```imshow```
-        function.
+        A 2D greyscale image suitable for use in matplotlib's `imshow` function.
 
     Examples
     --------
@@ -196,5 +190,5 @@ def xray(im, axis=0):  # pragma: no cover
     if axis == 2:
         im = np.transpose(im, axes=[2, 1, 0])
     im = np.sum(im, axis=0, dtype=np.int64)
-    im = im / np.max(im)
+    im = 1 - im / np.max(im)
     return im


Test patch:
diff --git a/test/unit/test_visualization.py b/test/unit/test_visualization.py
index d33ebaf35..a24bbd52e 100644
--- a/test/unit/test_visualization.py
+++ b/test/unit/test_visualization.py
@@ -38,32 +38,26 @@ def test_xray_z(self):
 
     def test_imshow_single(self):
         im = ps.generators.blobs(shape=[10, 20, 30], seed=0)
-        fig = ps.visualization.imshow(im)
-        assert fig.get_gridspec().ncols == 1
-        assert fig.get_gridspec().nrows == 1
-
-    def test_imshow_multi(self):
-        im = ps.generators.blobs(shape=[10, 20, 30], seed=0)
-        fig = ps.visualization.imshow(im, im)
-        assert fig.get_gridspec().ncols == 2
-        assert fig.get_gridspec().nrows == 1
+        fig, ax = ps.visualization.imshow(im)
+        assert ax.get_gridspec().ncols == 1
+        assert ax.get_gridspec().nrows == 1
 
     def test_bar(self):
         im = ps.generators.blobs(shape=[101, 200], seed=0)
         chords = ps.filters.apply_chords(im)
         h = ps.metrics.chord_length_distribution(chords)
-        fig = ps.visualization.bar(h)
-        assert len(h.pdf) == len(fig.patches)
+        fig, ax = ps.visualization.bar(h)
+        assert len(h.pdf) == len(ax.patches)
 
     def test_show_planes(self):
-        fig = ps.visualization.show_planes(self.im)
-        assert fig.ndim == 2
-        assert fig.shape[0] > self.im.shape[0]
+        im = ps.visualization.show_planes(self.im)
+        assert im.ndim == 2
+        assert im.shape[0] > self.im.shape[0]
 
     def test_show_3D(self):
-        fig = ps.visualization.show_3D(self.im)
-        assert fig.ndim == 2
-        assert fig.shape[0] > self.im.shape[0]
+        im = ps.visualization.show_3D(self.im)
+        assert im.ndim == 2
+        assert im.shape[0] > self.im.shape[0]
 
     def test_satn_to_movie(self):
         im = ps.generators.lattice_spheres(shape=[107, 107],