Matplotlib applies a colormap to your image and, under certain circumstances also scales the values to the full colour range. PIL doesn't do that, so if you give it a greyscale image (single channel) with all the values in the range 20..30, it will come out black, because it really expects values in the range 0..255 and it doesn't colorise the grey for you either.
Here's how you can convert a Matplotlib cmap
into a palette and push it into a PIL Image
:
#!/usr/bin/env python3
import matplotlib.pyplot as plt
from PIL import Image, ImageMath
def cmap2palette(cmapName='viridis'):
"""Convert a Matplotlib colormap to a PIL Palette"""
cmap = plt.get_cmap(cmapName)
palette = [int(x*255) for entry in cmap.colors for x in entry]
return palette
# Generate a linear ramp
im = Image.linear_gradient('L')
im.save('DEBUG-ramp.png') # just debug, not required
# Generate the viridis Matplotlib colourmap push into our PIL Image
pal = cmap2palette('viridis')
im.putpalette(pal)
im.save('DEBUG-viridis.png')
# Just for fun...
# Generate the inferno Matplotlib colourmap push into our PIL Image
pal = cmap2palette('inferno')
im.putpalette(pal)
im.save('DEBUG-inferno.png')
# Now with your image
im = Image.open('w9jUR.png')
# Contrast-stretch to full range
gMin, gMax = im.getextrema()
print(F'Greyscale min: {gMin}, greyscale max: {gMax}')
im = ImageMath.eval('int(255*(g-gMin)/(gMax-gMin))', g=im, gMin=gMin, gMax=gMax).convert('L')
# Generate the viridis Matplotlib colourmap push into our PIL Image
pal = cmap2palette('viridis')
im.putpalette(pal)
im.save('DEBUG-result.png')
That gives this as the input greyscale image:

and this as the result with the viridis
and inferno
colormaps:

Note also that you don't need to make 2 passes over your image to clip it to the range 20..30 like this:
cimage = np.where(cimage < 20, 20, cimage)
cimage = np.where(cimage > 30, 30, cimage)
You should be able to use np.clip()
:
np.clip(cimage, 20, 30, out=cimage)
If we apply the code above to your image:
