Setting Transparency Based on Pixel Values in Matplotlib - python

I am attempting to use matplotlib to plot some figures for a paper I am working on. I have two sets of data in 2D numpy arrays: An ascii hillshade raster which I can happily plot and tweak using:
import matplotlib.pyplot as pp
import numpy as np
hillshade = np.genfromtxt('hs.asc', delimiter=' ', skip_header=6)[:,:-1]
pp.imshow(hillshade, vmin=0, vmax=255)
pp.gray()
pp.show()
Which gives:
And a second ascii raster which delineates properties of a river flowing across the landscape. This data can be plotted in the same manner as above, however values in the array which do not correspond to the river network are assigned a no data value of -9999. The aim is to have the no data values set to be transparent so the river values overlie the hillshade.
This is the river data, ideally every pixel represented here as 0 would be completely transparent.
Having done some research on this it seems I may be able to convert my data into an RGBA array and set the alpha values to only make the unwanted cells transparent. However, the values in the river array are floats and cannot be transformed (as the original values are the whole point of the figure) and I believe the imshow function can only take unsigned integers if using the RGBA format.
Is there any way around this limitation? I had hoped I could simply create a tuple with the pixel value and the alpha value and plot them like that, but this does not seem possible.
I have also had a play with PIL to attempt to create a PNG file of the river data with the no data value transparent, however this seems to automatically scale the pixel values to 0-255, thereby losing the values I need to preserve.
I would welcome any insight anyone has on this problem.

Just mask your "river" array.
e.g.
rivers = np.ma.masked_where(rivers == 0, rivers)
As a quick example of overlaying two plots in this manner:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# Generate some data...
gray_data = np.arange(10000).reshape(100, 100)
masked_data = np.random.random((100,100))
masked_data = np.ma.masked_where(masked_data < 0.9, masked_data)
# Overlay the two images
fig, ax = plt.subplots()
ax.imshow(gray_data, cmap=cm.gray)
ax.imshow(masked_data, cmap=cm.jet, interpolation='none')
plt.show()
Also, on a side note, imshow will happily accept floats for its RGBA format. It just expects everything to be in a range between 0 and 1.

An alternate way to do this with out using masked arrays is to set how the color map deals with clipping values below the minimum of clim (shamelessly using Joe Kington's example):
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# Generate some data...
gray_data = np.arange(10000).reshape(100, 100)
masked_data = np.random.random((100,100))
my_cmap = cm.jet
my_cmap.set_under('k', alpha=0)
# Overlay the two images
fig, ax = plt.subplots()
ax.imshow(gray_data, cmap=cm.gray)
im = ax.imshow(masked_data, cmap=my_cmap,
interpolation='none',
clim=[0.9, 1])
plt.show()
There as also a set_over for clipping off the top and a set_bad for setting how the color map handles 'bad' values in the data.
An advantage of doing it this way is you can change your threshold by just adjusting clim with im.set_clim([bot, top])

Another option is to set all cells which shall remain transparent to np.nan (not sure what's more efficient here, I guess tacaswell's answer based on clim will be the fastet). Example adapting Joe Kington's answer:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# Generate some data...
gray_data = np.arange(10000).reshape(100, 100)
masked_data = np.random.random((100,100))
masked_data[np.where(masked_data < 0.9)] = np.nan
# Overlay the two images
fig, ax = plt.subplots()
ax.imshow(gray_data, cmap=cm.gray)
ax.imshow(masked_data, cmap=cm.jet, interpolation='none')
plt.show()
Note that for arrays of dtype=bool you should not follow your IDE's advice to compare masked_data is True for the sake of PEP 8 (E712) but stick with masked_data == True for element-wise comparison, otherwise the masking will fail:

Related

Altered image array when plotting with pyplot's imshow

So i noticed for me and a few of my colleagues that when we display a binary array using matplotlib.pyplot's imshow function the edges of the displayed image seems altered. For a while i thought it was just a visual artifact, but ran into further trouble with it today.
By the way i am running with matplotlib: 3.2.2 and numpy: 1.19.1
If i create a small binary array and plot it it you can see a small "halo" to the binary box in the image. It is not very obvious but it is there:
import matplotlib.pyplot as plt
import numpy as np
img=np.zeros((100,100))
img[25:60,25:60]=50
plt.imshow(img)
It will become more apparent if i change the cmap for the plot.
my_cmap = plt.cm.get_cmap('prism')
my_cmap.set_under('black')
plt.imshow(img,cmap=my_cmap, vmin=1)
The displayed array should only have 0's as background and 1's in the box, but the box is displayed as a green box with a red/yellow border.
With previous versions of pyplot i have not had this issue and it does become a problem when i do object detection and i want to display them and my other wise binary objects end up like this:
I hope you can help me with this
imshow doesn't know about your data being discrete or even binary. Default it adds some interpolation. You can suppress the smooth interpolation using imshow(...., interpolation='none') (or interpolation='nearest').
Note that the default mode is 'antialiased' for which the effect is different depending on the number of screen pixels occupied by an image pixel.
See the official matplotlib documentation for more details.
Here is some test code comparing the default and the 'none' interpolation mode for different image sizes:
from matplotlib import pyplot as plt
import numpy as np
x = np.round(10 + np.random.uniform(-.1, .1, (100, 100)).cumsum(axis=0).cumsum(axis=1))
x[x % 2 == 0] = x.max() + 1
fig, axes = plt.subplots(2, 6, figsize=(14, 5))
for i, axrow in enumerate(axes):
for j, ax in enumerate(axrow):
k = 10 * (j + 5)
ax.imshow(x[-k:, -k:], cmap='Dark2', interpolation=None if i == 0 else 'none')
ax.set_title("size={}\ninterpolation={}".format(k, 'None' if i == 0 else "'none'"))
plt.tight_layout()
plt.show()
Here is another example, using the 'seismic' colormap and only two data values. This colormap has dark blue and red at the extremes and white near the center, which shows the interpolation much more pronunciated:

Retrieving Matplotlib Heatmap Colors

I am trying to retrieve the colors of each cell on a matplotlib heatmap, generated by the imshow() function, such as performed by the magic_function below:
import matplotlib.pyplot as plt
import numpy as np
hm = plt.imshow(np.random.rand(10, 10))
color_matrix = hm.magic_function() #returns matrix containing the RGB/Hex values of each cell
You are looking for the colormap that is used by the image created via imshow. Now of course you can reverse engineer how that colormap got into the image in the first place like the other answer suggests. That seems cumbersome and often enough isn't even possible.
So given an AxesImage (the object returned by imshow) or for that matter any other ScalarMappable, you get the colormap in use via .cmap. Since the data values are normalized to the range between 0..1, you need a normalization, which you get from the .norm attribute. Finally, you need the data, which you get from the .get_array() method.
The magic_function is hence a chain of three functions.
im = plt.imshow(np.random.rand(10, 10))
color_matrix = im.cmap(im.norm(im.get_array()))
color_matrix is now the (10, 10, 4)-shaped RGBA array of colors corresponding to the pixels in the image.
Building upon this answer, you need to understand the default color map chosen by matplotlib since you didn't provide one. The documentation states that it is the value of plt.rcParams["image.cmap"], so we use that.
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
from matplotlib.colors import Normalize
data = np.random.rand(10, 10)
cmap = cm.get_cmap(plt.rcParams["image.cmap"])
hm = plt.imshow(data)
norm = Normalize(vmin=data.min(), vmax=data.max())
rgba_values = cmap(norm(data))
The RGBA value of the upper left cell would then be rgba_values[0,0]

RGB polar plot in Python

I am trying to produce RGB polar plots in Python and I was expecting matplotlib.pyplot.imshow to be able to do it. However, whenever I try plotting data using this method I obtain a blank output.
import matplotlib.pyplot as plt
import numpy as np
data = np.array([[[0,0,1],[0,1,0],[1,0,0]],[[0,0,0.5],[0,0.5,0],[0.5,0,0]]])
# Sample, any N,M,3 data should work
ax = plt.subplot(111,polar=True)
ax.imshow(data,extent=[0,2*np.pi,0,1]) # Produces a white circle
Is there a good way to accomplish this using the aforementioned method or another ?
Thanks.
EDIT: I managed to make a single quadrant by using extent=[0,np.pi/2,0,1] but its use is clearly bugged for polar plots. since anything but a full quadrant doesn't produce the expected outcome.
Using imshow on a polar plot is unfortunately not possible, because the imshow grid is necessarily quadratic in its pixels. You may however use pcolormesh and apply a trick (similar to this one), namely to provide the colors as color argument to pcolormesh, as it would usually just take 2D input.
import matplotlib.pyplot as plt
import numpy as np
data = np.array([[[0,0,1],[0,1,0],[1,0,0]],
[[0,0,0.5],[0,0.5,0],[0.5,0,0]]])
ax = plt.subplot(111, polar=True)
#get coordinates:
phi = np.linspace(0,2*np.pi,data.shape[1]+1)
r = np.linspace(0,1,data.shape[0]+1)
Phi,R = np.meshgrid(phi, r)
# get color
color = data.reshape((data.shape[0]*data.shape[1],data.shape[2]))
# plot colormesh with Phi, R as coordinates,
# and some 2D array of the same shape as the image, except the last dimension
# provide colors as `color` argument
m = plt.pcolormesh(Phi,R,data[:,:,0], color=color, linewidth=0)
# This is necessary to let the `color` argument determine the color
m.set_array(None)
plt.show()
The result is not a circle because you do not have enough points. Repeating the data, data = np.repeat(data, 25, axis=1) would then allow to get a circle.

Displaying multiple masks in different colours in pylab

I've an array that includes decent observations, irrelevant observations (that I would like to mask out), and areas where there are no observations (that i would also like to mask out). I want to display this array as an image (using pylab.imshow) with two separate masks, where each mask is shown in a different colour.
I've found code for a single mask (here) in a certain colour, but nothing for two different masks:
masked_array = np.ma.array (a, mask=np.isnan(a))
cmap = matplotlib.cm.jet
cmap.set_bad('w',1.)
ax.imshow(masked_array, interpolation='nearest', cmap=cmap)
If possible, I'd like to avoid having to use a heavily distorted colour map but accept that that is an option.
You might simply replace values in you array with some fixed value depending on some conditions. For example, if you want to mask elements larger than 1 and smaller than -1:
val1, val2 = 0.5, 1
a[a<-1]= val1
a[a>1] = val2
ax.imshow(a, interpolation='nearest')
val1 and val2 can be modified to obtain colors you wish.
You can also set the colors explicitly, but it requires more work:
import matplotlib.pyplot as plt
from matplotlib import colors, cm
a = np.random.randn(10,10)
norm = colors.normalize()
cmap = cm.hsv
a_colors = cmap(norm(a))
col1 = colors.colorConverter.to_rgba('w')
col2 = colors.colorConverter.to_rgba('k')
a_colors[a<-0.1,:] = col1
a_colors[a>0.1,:] = col2
plt.imshow(a_colors, interpolation='nearest')
plt.show()
For me the simplest way is plotting directly the masks with imshow, passing different colormaps. Max and min of a colormap are used for True and False values:
mask1=np.isnan(a)
mask2=np.logical_not(mask1)
plt.imshow(mask1,cmap='gray')
plt.imshow(mask2,cmap='rainbow')
However this (and other approaches suggested) plot also False values overplotting previous plots. If you want to avoid plotting False values, it can be done by replacing them with np.nan, after converting the array to float (np.nan is of type float and cannot be contained in a boolean mask). nan values are not plotted:
mmm=mask.astype(np.float)
mmm[np.where(mmm==0)]=np.nan
#the substitution can be done also in one line with:
#mmm=np.where(mask,mask.astype(np.float),np.nan)
plt.imshow(mmm,cmap='rainbow',vmin=0,vmax=1)) #will use only the top color: red. vmin and vmax are needed if there are only one value (1.0=True) in the array.
plt.colorbar()
#repeat for other masks...
And i hope I am not going too much off topic, but same technique can be used to plot different part of the data with different colormaps, by replacing the plotting command with:
plt.imshow(mmm*data,cmap='rainbow')
In order to color some pixels red and others green, as appears in the the following image, I used the code below. (See code comments for details.)
import numpy as np #Used for holding and manipulating data
import numpy.random #Used to generate random data
import matplotlib as mpl #Used for controlling color
import matplotlib.colors #Used for controlling color as well
import matplotlib.pyplot as plt #Use for plotting
#Generate random data
a = np.random.random(size=(10,10))
#This 30% of the data will be red
am1 = a<0.3 #Find data to colour special
am1 = np.ma.masked_where(am1 == False, am1) #Mask the data we are not colouring
#This 10% of the data will be green
am2 = np.logical_and(a>=0.3,a<0.4) #Find data to colour special
am2 = np.ma.masked_where(am2 == False, am2) #Mask the data we are not colouring
#Colourmaps for each special colour to place. The left-hand colour (black) is
#not used because all black pixels are masked. The right-hand colour (red or
#green) is used because it represents the highest z-value of the mask matrices
cm1 = mpl.colors.ListedColormap(['black','red'])
cm2 = mpl.colors.ListedColormap(['black','green'])
fig = plt.figure() #Make a new figure
ax = fig.add_subplot(111) #Add subplot to that figure, get ax
#Plot the original data. We'll overlay the specially-coloured data
ax.imshow(a, aspect='auto', cmap='Greys', vmin=0, vmax=1)
#Plot the first mask. Values we wanted to colour (`a<0.3`) are masked, so they
#do not show up. The values that do show up are coloured using the `cm1` colour
#map. Since the range is constrained to `vmin=0, vmax=1` and a value of
#`cm2==True` corresponds to a 1, the top value of `cm1` is applied to all such
#pixels, thereby colouring them red.
ax.imshow(am1, aspect='auto', cmap=cm1, vmin=0, vmax=1);
ax.imshow(am2, aspect='auto', cmap=cm2, vmin=0, vmax=1);
plt.show()
I don't know what the values are in your array, but you could convert the masked areas so that the X values become RGB(A) values (tuples of (R,G,B,A)), in which case the cmap is ignored, according to the documentation at least.
• cmap: [ None | Colormap ]
A matplotlib.colors.Colormap instance, eg. cm.jet. If None, default to
rc image.cmap value. cmap is ignored when X has RGB(A) information

How can I plot NaN values as a special color with imshow in matplotlib?

I am trying to use imshow in matplotlib to plot data as a heatmap, but some of the values are NaNs. I'd like the NaNs to be rendered as a special color not found in the colormap.
example:
import numpy as np
import matplotlib.pyplot as plt
f = plt.figure()
ax = f.add_subplot(111)
a = np.arange(25).reshape((5,5)).astype(float)
a[3,:] = np.nan
ax.imshow(a, interpolation='nearest')
f.canvas.draw()
The resultant image is unexpectedly all blue (the lowest color in the jet colormap). However, if I do the plotting like this:
ax.imshow(a, interpolation='nearest', vmin=0, vmax=24)
--then I get something better, but the NaN values are drawn the same color as vmin... Is there a graceful way that I can set NaNs to be drawn with a special color (eg: gray or transparent)?
Hrm, it appears I can use a masked array to do this:
masked_array = np.ma.array (a, mask=np.isnan(a))
cmap = matplotlib.cm.jet
cmap.set_bad('white',1.)
ax.imshow(masked_array, interpolation='nearest', cmap=cmap)
This should suffice, though I'm still open to suggestions. :]
With newer versions of Matplotlib, it is not necessary to use a masked array anymore.
For example, let’s generate an array where every 7th value is a NaN:
arr = np.arange(100, dtype=float).reshape(10, 10)
arr[~(arr % 7).astype(bool)] = np.nan
We can modify the current colormap and plot the array with the following lines:
current_cmap = matplotlib.cm.get_cmap()
current_cmap.set_bad(color='red')
plt.imshow(arr)

Categories