Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active February 21, 2021 22:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endolith/74275dc8fa2bb9a78266 to your computer and use it in GitHub Desktop.
Save endolith/74275dc8fa2bb9a78266 to your computer and use it in GitHub Desktop.
smoothed hot python colormap

This isn't meant to be the end-all be-all thermal colormap; it's just an attempt at improving on the default hot built into matlab/matplotlib/etc, which has nasty bands at the yellow and red corners. Something based on perceptual color space would probably be even better.

Code is ugly because it's not that great of a colormap so I'm not going to bother cleaning it up. :) Initially it had a radius parameter but I decided 0.5 was the only value that made sense, so the code could be simplified with that.

You can see in the MRI image that the default hot colormap obscures some detail in the cheek(?) region, because saturated red and orange are not so distinguishable from each other, while the smoothed version shows it more clearly.

Built-in thermal-like maps gist_heat, afmhot, hot, and copper all have some banding/flaws, but ColorBrewer maps OrRd, YlOrRd, YlOrBr, Oranges are pretty good.

# -*- coding: utf-8 -*-
"""
Created on Fri Feb 28 23:09:08 2014
Thermal colormap in RGB space with rounded curve
"""
from __future__ import division
from numpy import pi, arange, zeros, ones, cos, sin, dstack, vstack
from matplotlib import cm
def smooth_hot(lutsize=1025):
"""
Same as Matlab `hot`, but rounds the corners of the curve to avoid bright
bands
"""
r = 0.5
some_len = 1-r
# simplified:
total_arc_length = pi*r - 4*r + 3
dt = total_arc_length / (lutsize-1)
b2r_t = arange(0, some_len, dt)
b2r_rgbs = dstack((b2r_t, 0*b2r_t, 0*b2r_t))[0]
remainder = some_len - b2r_t[-1]
offset = dt - remainder
dtheta = dt / r
red_theta = arange(offset/r, pi/2, dtheta)
red_rgbs = dstack(( r*sin(red_theta) + 1 - r,
-r*cos(red_theta) + r,
zeros(len(red_theta))))[0]
remainder = r*(pi/2 - red_theta[-1])
offset2 = dt - remainder
r2y_t = arange(r+offset2, 1-r, dt)
r2y_rgbs = dstack(( ones(len(r2y_t)),
r2y_t,
zeros(len(r2y_t))))[0]
y_theta = arange(offset/r, pi/2, dtheta)
y_rgbs = dstack(( ones(len(y_theta)),
r*cos(y_theta) + 1 - r,
-r*sin(y_theta) + r,
))[0][::-1]
y2w_t = arange(1, 1-some_len, -dt)[::-1]
y2w_rgbs = dstack((ones(len(y2w_t)), ones(len(y2w_t)), y2w_t))[0]
rgbs = vstack((b2r_rgbs, red_rgbs, r2y_rgbs, y_rgbs, y2w_rgbs))
return cm.colors.LinearSegmentedColormap.from_list('smooth_hot', rgbs,
lutsize)
if __name__ == "__main__":
import matplotlib.pyplot as plt
from numpy import meshgrid
# make these smaller to increase the resolution
dx, dy = 0.01, 0.01
lutsize = 1025 # Lower produces visible steps in slow-changing gradients
cmap = cm.get_cmap(smooth_hot(lutsize=lutsize))
def func3(x, y):
# Sinusoid clearly shows edges, bands, or halos in the colormap
return sin(x) + sin(y)
x = arange(-4.0, 4.0001, dx)
y = arange(-4.0, 4.0001, dy)
X, Y = meshgrid(x, y)
Z = func3(X, Y)
plt.subplot(1, 2, 1)
im = plt.imshow(Z, vmax=abs(Z).max(), vmin=-abs(Z).max(),
origin='lower',
extent=[-3, 3, -3, 3],
cmap=cmap,
)
plt.colorbar()
plt.show()
def relative_luminance((R, G, B)):
Y = 0.2126 * R + 0.7152 * G + 0.0722 * B
return Y
rgbs = cmap(arange(lutsize))
import matplotlib.pyplot as plt
plt.subplot(1, 2, 2)
plt.plot(rgbs[:, 0], color='r')
plt.plot(rgbs[:, 1], color='g')
plt.plot(rgbs[:, 2], color='b')
plt.plot([relative_luminance(c) for c in rgbs[:, :3]], color='k')
plt.margins(0, 0.1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment