axe
axe

Reputation: 41

Apply a color map / gradient map to an image using Python

Most image editing software has gradient map functions. Gradient maps take the brightness of a given pixel and apply a gradient of colors according to brightness. Photoshop and other software have ways to automate this, but they can't do exactly what I want, so I thought Python might do the trick. Unfortunately I'm having a very hard time understanding or applying any of the results that come up when I search for gradient maps or color maps with Python.

Gradient maps

All the potential solution threads I found used numpy or matplotlib which have lots of mathy lines that go right over my head... I would love some help with this. Initially I had something going with Processing, but I found the task of exporting tons of images with Processing to be weird and hacky. Plus I like Python, and want to learn how to edit and generate art with it.

This is what I'm working with right now.

from PIL import Image

myPalette = ['#1A1423', '#684756', '#AB8476']

def colorMap(pixel, palette): 
    # Calculate the brightness of pixel 
    R, G, B = pixel 
    brightness = sum([R, G, B])/3
 
    # Map gradient of colors to brightness
    # ???...
    
    return mappedColor

 
img = Image.open('image_input.png') 
pixels = img.load()
 
for x in range(img.size[0]): 
    for y in range(img.size[1]): 
        pixel = img.getpixel((x, y)) 
        pixels[x, y] = colorMap(pixel, myPalette)
 
img.save('image_output.png')

Loading, calculating brightness, and saving are easy. I just have no idea how to apply a gradient of my palette to the pixel.

Upvotes: 4

Views: 6267

Answers (4)

Mark Setchell
Mark Setchell

Reputation: 207540

You can do that quite easily with ImageMagick or with PIL/Numpy/OpenCV.

The first thing is to get hold of the colormap - the vertical bar down the right side of your image. I don't have or know Clip Studio, so maybe it will let you export the colormap, or create a greyscale gradient and apply the colormap to it, then save the result as a PNG. In my case, I loaded your image into Photoshop, cut out the gradient and rotated it to make an image exactly 256-pixels wide by 1-pixel high. Enlarged, that looks like this:

colourmap.png

colormap going from navy to salmon, to yellow, to white

I also cropped your swirl thing off the left side of your image - please post images separately in future.

swirl.png

abstract grayscale image of a swirl


Now for applying it. First, just with ImageMagick in Terminal. I loaded your swirl image and separated it into its constituent RGB channels then averaged the channels and applied the colourmap, also known as CLUT or "Colour Lookup Table":

magick swirl.png -channel RGB -separate -evaluate-sequence mean colourmap.png -clut result.png

the previous swirl image with the previous colormap applied to it, giving it a navy-salmon-yellow color scheme


Next, same thing with PIL/Numpy:

#!/usr/bin/env python3

import numpy as np
from PIL import Image

# Load image, make into Numpy array and average RGB channels
im = Image.open('swirl.png').convert('RGB')
na = np.array(im)
grey = np.mean(na, axis=2).astype(np.uint8)
Image.fromarray(grey).save('DEBUG-grey.png')   # DEBUG only

# Load colourmap
cmap = Image.open('colourmap.png').convert('RGB')
# cmap must be resized to have a width of 256
# since grey's scaled from 0-255, so np.take will select from indices 0-255 only
cmap = cmap.resize((256, 1))

# Make output image same height and width as grey image, but 3-channel RGB
result = np.zeros((*grey.shape,3), dtype=np.uint8)

# Take entries from RGB colourmap according to greyscale values in image
np.take(cmap.getdata(), grey, axis=0, out=result)

# Save result
Image.fromarray(result).save('result.png')

You can also generate piece-wise linear colormaps like this:

 magick -size 160x1 gradient:navy-"rgb(220,110,110)"   \
        -size  60x1 gradient:"rgb(220,110,110)"-yellow \
        -size  35x1 gradient:yellow-white              \
        +append colourmap.png

colormap similar to first colormap, but with greater height and less width

That makes three segments each with a linear gradient:

  • 160x1 in navy to salmon,
  • 60x1 in salmon to yellow and
  • 35x1 in yellow to white

then appends them together.


If you make all the segments of the colour map the same length, you will get a different interpretation:

magick -size 85x1 \
   gradient:navy-"rgb(220,110,110)"   \
   gradient:"rgb(220,110,110)"-yellow \
   gradient:yellow-white +append -resize 256x1\! colourmap.png

colormap similar to previous colormap, but with longer segments of yellow and white, and less navy and salmon

That leads to this:

the provided swirl image with the previous colormap applied to it; it has more yellow and orange than the first colormapped swirl

Upvotes: 7

nathancy
nathancy

Reputation: 46620

Here are two methods, one using Matplotlib and one using only OpenCV

Method #1: OpenCV + matplotlib.pyplot.get_cmap

We first load in the image as grayscale. By default, OpenCV reads in an image as 3-channel, 8-bit BGR. We can directly load in an image as grayscale using cv2.imread() with the cv2.IMREAD_GRAYSCALE parameter or use cv2.cvtColor() to convert a BGR image to grayscale with the cv2.COLOR_BGR2GRAY parameter. Once we load in the image, we throw this grayscale image into Matplotlib to obtain our heatmap image. Matplotlib returns a RGB format so we must convert back to Numpy format and switch to BGR colorspace for use with OpenCV. Here's a example using the inferno colormap. See choosing color maps in Matplotlib for available built-in colormaps depending on your desired use case.

Input image -> Heatmap image

Code

import matplotlib.pyplot as plt
import numpy as np
import cv2

image = cv2.imread('1.png', 0)
colormap = plt.get_cmap('inferno')
heatmap = (colormap(image) * 2**16).astype(np.uint16)[:,:,:3]
heatmap = cv2.cvtColor(heatmap, cv2.COLOR_RGB2BGR)

cv2.imshow('image', image)
cv2.imshow('heatmap', heatmap)
cv2.waitKey()

Method #2: cv2.applyColorMap()

We can use OpenCV's built in heatmap function. Here's the result using the cv2.COLORMAP_HOT heatmap

Code

import cv2

image = cv2.imread('1.png', 0)
heatmap = cv2.applyColorMap(image, cv2.COLORMAP_HOT)

cv2.imshow('heatmap', heatmap)
cv2.waitKey()

Note: Although OpenCV's built-in implementation is short and quick, I recommend using Method #1 since there is a larger colormap selection. Matplotlib has hundreds of various colormaps and allows you to create your own custom color maps while OpenCV only has 12 to choose from. Here's the built in OpenCV colormap selection:

Upvotes: 1

user1196549
user1196549

Reputation:

The trick is to define a mapping table that associates a color to every gray value in the range [0, 255] (in your case, you can even map [0, 255 x 3] by not taking the average).

Now to get a smooth effect, you need to change the color components regularly. The options are unlimited. Here is a typical example:

enter image description here

enter image description here

Upvotes: 1

pasnik
pasnik

Reputation: 347

I know you said you didn't want matplotlib, but I don't think you need mathy lines to make it work. I tested it with a PNG of my own, so hopefully it works for yours as well. Here's an example that draws heavily on this documentation.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

# Read PNG and take average:
img = plt.imread('image_input.png')
Z = img.mean(-1)

#   Kestrel
color_list = [np.array([207,117,80])/255,
              np.array([219,179,127])/255,
              np.array([248,249,251])/255,
              np.array([185,191,205])/255,
              np.array([88,104,127])/255]

cmap_name = 'Kestrel'
N_bin=100
# Create the colormap
cmap = LinearSegmentedColormap.from_list(cmap_name, color_list, N=N_bin)
# Fewer bins will result in "coarser" colomap interpolation
fig, ax = plt.subplots()
im = ax.imshow(Z, cmap=cmap)
fig.colorbar(im, ax=ax)

plt.show()

Upvotes: 0

Related Questions