Reputation: 41
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.
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
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
I also cropped your swirl thing off the left side of your image - please post images separately in future.
swirl.png
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
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
That makes three segments each with a linear gradient:
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
That leads to this:
Upvotes: 7
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
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:
Upvotes: 1
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