Search code examples
matplotlibcolorshsvcolor-mapping

Matplotlib darker hsv colormap


I'm using the HSV colormap from matplotlib to plot some vector fields. Is there a way to darken or make smoother the HSV colours so they look more like this

enter image description here

than my original plot colours, which are too bright:

enter image description here


Solution

  • Introduction

    Assuming you're trying to plot a pcolor image like this:

    import numpy as np
    import matplotlib.pyplot as plt
    
    y, x = np.mgrid[slice(-3, 3 + 0.05, 0.05),
                    slice(-3, 3 + 0.15, 0.15)]
    z = (1 - x / 2. + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)
    # x and y are bounds, so z should be the value *inside* those bounds.
    # Therefore, remove the last value from the z array.
    z = z[:-1, :-1]
    
    fig = plt.figure(1)
    fig.clf()
    ax = plt.gca()
    pcol = ax.pcolormesh(x, y, z, cmap=plt.get_cmap('hsv'), )
    plt.colorbar(pcol)
    ax.set_xlim([-3, 3])
    ax.set_ylim([-3, 3])
    

    Your image will be:

    HSV colormap.

    Methods

    I've written an alternate implementation of the MPL cookbook cmap_map function that modifies colormaps. In addition to support for kwargs and pep8 compliance, this version handles discontinuities in a colormap:

    import numpy as np
    from matplotlib.colors import LinearSegmentedColormap as lsc
    
    
    def cmap_map(function, cmap, name='colormap_mod', N=None, gamma=None):
        """
        Modify a colormap using `function` which must operate on 3-element
        arrays of [r, g, b] values.
    
        You may specify the number of colors, `N`, and the opacity, `gamma`,
        value of the returned colormap. These values default to the ones in
        the input `cmap`.
    
        You may also specify a `name` for the colormap, so that it can be
        loaded using plt.get_cmap(name).
        """
        if N is None:
            N = cmap.N
        if gamma is None:
            gamma = cmap._gamma
        cdict = cmap._segmentdata
        # Cast the steps into lists:
        step_dict = {key: map(lambda x: x[0], cdict[key]) for key in cdict}
        # Now get the unique steps (first column of the arrays):
        step_list = np.unique(sum(step_dict.values(), []))
        # 'y0', 'y1' are as defined in LinearSegmentedColormap docstring:
        y0 = cmap(step_list)[:, :3]
        y1 = y0.copy()[:, :3]
        # Go back to catch the discontinuities, and place them into y0, y1
        for iclr, key in enumerate(['red', 'green', 'blue']):
            for istp, step in enumerate(step_list):
                try:
                    ind = step_dict[key].index(step)
                except ValueError:
                    # This step is not in this color
                    continue
                y0[istp, iclr] = cdict[key][ind][1]
                y1[istp, iclr] = cdict[key][ind][2]
        # Map the colors to their new values:
        y0 = np.array(map(function, y0))
        y1 = np.array(map(function, y1))
        # Build the new colormap (overwriting step_dict):
        for iclr, clr in enumerate(['red', 'green', 'blue']):
            step_dict[clr] = np.vstack((step_list, y0[:, iclr], y1[:, iclr])).T
        return lsc(name, step_dict, N=N, gamma=gamma)
    

    Implementation

    To use it, simply define a function that will modify your RGB colors as you like (values from 0 to 1) and supply it as input to cmap_map. To get colors close to the ones in the images you provided, for example, you could define:

    def darken(x, ):
       return x * 0.8
    
    dark_hsv = cmap_map(darken, plt.get_cmap('hsv'))
    

    And then modify the call to pcolormesh:

    pcol = ax.pcolormesh(x, y, z, cmap=dark_hsv)
    

    Darker HSV.

    If you only wanted to darken the greens in the image, you could do (now all in one line):

    pcol = ax.pcolormesh(x, y, z,
                         cmap=cmap_map(lambda x: x * [1, 0.7, 1],
                                       plt.get_cmap('hsv'))
                        )
    

    Darken green only