Add alpha to an existing matplotlib colormap

Solution 1:

I'm not quite sure if this qualifies within "not knowing the inner structure of the colormap", but perhaps something like this would work to add a linear alpha to an existing colormap?

import numpy as np
import matplotlib.pylab as pl
from matplotlib.colors import ListedColormap

# Random data
data1 = np.random.random((4,4))

# Choose colormap
cmap = pl.cm.RdBu

# Get the colormap colors
my_cmap = cmap(np.arange(cmap.N))

# Set alpha
my_cmap[:,-1] = np.linspace(0, 1, cmap.N)

# Create new colormap
my_cmap = ListedColormap(my_cmap)

pl.figure()
pl.subplot(121)
pl.pcolormesh(data1, cmap=pl.cm.RdBu)
pl.colorbar()

pl.subplot(122)
pl.pcolormesh(data1, cmap=my_cmap)
pl.colorbar()

enter image description here

Solution 2:

I'd like to extend the answer by Bart by a fix, that eliminates the line artifacts in the colorbar. Some history: as of today, these line artifacts still persist, and are not well solved (see Matplotlib: Add a custom colorbar that runs from full transparent to full color (remove artifacts), why does my colorbar have lines in it?). However, every color with an alpha channel is nothing but a mixture of the color with its background. Therefore, if you know the background, you can calculate the corresponding non-alpha color (see https://www.viget.com/articles/equating-color-and-transparency/).

The following solution assumes, that actual transparency is not necessary for the figure. If one uses true alpha in the figure and an own colormap with calculated non-alpha color values if desired.

import numpy as np
import matplotlib.pylab as pl
from matplotlib.colors import ListedColormap

# Random data
data1 = np.random.random((4,4))

# Choose colormap which will be mixed with the alpha values
cmap = pl.cm.RdBu

# Get the colormap colors
my_cmap = cmap(np.arange(cmap.N))
# Define the alphas in the range from 0 to 1
alphas = np.linspace(0, 1, cmap.N)
# Define the background as white
BG = np.asarray([1., 1., 1.,])
# Mix the colors with the background
for i in range(cmap.N):
    my_cmap[i,:-1] = my_cmap[i,:-1] * alphas[i] + BG * (1.-alphas[i])
# Create new colormap which mimics the alpha values
my_cmap = ListedColormap(my_cmap)

# Plot
f, axs = pl.subplots(1,2, figsize=(8,3))
h = axs[0].pcolormesh(data1, cmap=pl.cm.RdBu)
cb = f.colorbar(h, ax=axs[0])

h = axs[1].pcolormesh(data1, cmap=my_cmap)
cb = pl.colorbar(h, ax=axs[1])
f.show()

image wo artifacts

Solution 3:

The solution provided by Bart certainly does add alpha channel transparency to the figure. The problem with the method is it doesn't work very well with sequential colormaps or divergent colormaps. Sequential colormaps try increase their lightness approximately linearly, from a pale color to a saturated color, so that the intensity of the colors increases as the intensity of the values increase. Adding the alpha channel channel on top of this means the lighter shades are mostly transparent. If you render the colormap on top of a pale background (which I think is quite typical), the colormap is no longer perceptually uniform, with the smaller values appearing even smaller.

An alternative way to go about converting an existing colormap to have an alpha channel is to demix white from the RGB colors of the original colormap as much as possible. By converting the "white component" into transparency, and rescaling the RGB values to correct for the alpha value, we can ensure that the new transparent colormap will render exactly the same as the original colormap, if it is presented against a white background.

import matplotlib.pyplot as plt
import matplotlib.colors
import matplotlib.cm
import numpy as np

def rgb_white2alpha(rgb, ensure_increasing=False):
    """
    Convert a set of RGB colors to RGBA with maximum transparency.
    
    The transparency is maximised for each color individually, assuming
    that the background is white.
    
    Parameters
    ----------
    rgb : array_like shaped (N, 3)
        Original colors.
    ensure_increasing : bool, default=False
        Ensure that alpha values are strictly increasing.
    
    Returns
    -------
    rgba : numpy.ndarray shaped (N, 4)
        Colors with maximum possible transparency, assuming a white
        background.
    """
    # The most transparent alpha we can use is given by the min of RGB
    # Convert it from saturation to opacity
    alpha = 1. - np.min(rgb, axis=1)
    if ensure_increasing:
        # Let's also ensure the alpha value is monotonically increasing
        a_max = alpha[0]
        for i, a in enumerate(alpha):
            alpha[i] = a_max = np.maximum(a, a_max)
    alpha = np.expand_dims(alpha, -1)
    # Rescale colors to discount the white that will show through from transparency
    rgb = (rgb + alpha - 1) / alpha
    # Concatenate our alpha channel
    return np.concatenate((rgb, alpha), axis=1)
    

def cmap_white2alpha(name, ensure_increasing=False, register=True):
    """
    Convert colormap to have the most transparency possible, assuming white background.
    
    Parameters
    ----------
    name : str
        Name of builtin (or registered) colormap.
    ensure_increasing : bool, default=False
        Ensure that alpha values are strictly increasing.
    register : bool, default=True
        Whether to register the new colormap.

    Returns
    -------
    cmap : matplotlib.colors.ListedColormap
        Colormap with alpha set as low as possible.
    """
    # Fetch the cmap callable
    cmap = plt.get_cmap(name)
    # Get the colors out from the colormap LUT
    rgb = cmap(np.arange(cmap.N))[:, :3]  # N-by-3
    # Convert white to alpha
    rgba = rgb_white2alpha(rgb, ensure_increasing=ensure_increasing)
    # Create a new Colormap object
    cmap_alpha = matplotlib.colors.ListedColormap(rgba, name=name + "_alpha")
    if register:
        matplotlib.cm.register_cmap(name=name + "_alpha", cmap=cmap_alpha)
    return cmap_alpha

# Get original Reds colormap
cmap_og = plt.get_cmap("Reds")

# Get our new version of the colormap with white2alpha
cmap_w2a = cmap_white2alpha("Reds")

# Replicate Bart's method, which adds linear alpha without rescaling RGB values
rgba_lin = cmap(np.arange(cmap_og.N))
rgba_lin[:,-1] = np.linspace(0, 1, cmap_og.N)
cmap_lin_alpha = ListedColormap(rgba_lin)

# Generate some data to plot
data1 = np.random.random((5, 5))
data2 = np.expand_dims(np.arange(25), axis=0)

# Plot the colormap scale bars
plt.figure(figsize=(18, 5), facecolor=[.7, .7, .7])
# Original Reds colormap
plt.subplot(3, 1, 1, facecolor="w")
plt.pcolormesh(data2, cmap=cmap_og)
plt.title("Reds cmap", fontsize=12, rotation=0)
# White converted to alpha
plt.subplot(3, 1, 2, facecolor="w")
plt.pcolormesh(data2, cmap=cmap_w2a)
plt.title("white2alpha", fontsize=12, rotation=0)
# Linear alpha added
plt.subplot(3, 1, 3, facecolor="w")
plt.pcolormesh(data2, cmap=cmap_lin_alpha)
plt.title("linear alpha", fontsize=12, rotation=0)
plt.show()

# Plot randomly generated data
for bg in ["w", "g"]:
    plt.figure(figsize=(20, 5), facecolor=[.7, .7, .7])
    # Plot original Reds colormap
    plt.subplot(1, 3, 1, facecolor=bg)
    plt.pcolormesh(data1, cmap=cmap_og)
    plt.title("Reds cmap")
    plt.colorbar()
    # Plot Reds colormap with white converted to alpha
    plt.subplot(1, 3, 2, facecolor=bg)
    plt.pcolormesh(data1, cmap=cmap_w2a)
    plt.title("Reds white2alpha cmap")
    plt.colorbar()
    # Plot Reds colormap with linear alpha channel
    plt.subplot(1, 3, 3, facecolor=bg)
    plt.pcolormesh(data1, cmap=cmap_lin_alpha)
    plt.title("Reds + linear alpha")
    plt.colorbar()
    # Render
    plt.show()

The resulting colormaps look as follows: Colorbars

And with random values rendered on white axes and green axes, respectively: Random values on white bg Random values on green bg

As the figures demonstrate, converting white to alpha produces a result which is more perceptually linear and has richer colors, compared with adding a linearly increasing alpha channel without rescaling the RGB values.

The downside to converting white to alpha is that the alpha value does not increase linearly, which may be important to you.

# Plot the alpha values of each of the new colormaps
plt.figure(figsize=(10, 6))
plt.plot(cmap_w2a.colors[:, -1], label="white2alpha")
plt.plot(cmap_lin_alpha.colors[:, -1], label="linear_alpha")
plt.xlabel("Index", fontsize=12)
plt.ylabel("Alpha", fontsize=12)
plt.grid()
plt.legend(fontsize=12)
plt.show()

Comparison of alpha values

If you do need the alpha values to increase linearly, you can use the linearly increasing alpha value, but rescale the RGB values to correct for the transparency as best as you can. The results won't be perfect because the colormap is too transparent to render the original colors, so there's some trade-offs to consider. I implemented this and related options in this colab notebook.