Changing hue of rgba color - matplotlib

I'm plotting a bunch of data on a logscale as a scatter plot in matplotlib (just in case the medium is relevant) using RGBA colors. What I would like to be able to do is once I plotted everything, I want to pick out individual scatter points and change their hue to the hue of some RGB color, but preserving the old alpha value. The way I currently do it is this:
oldHSV = rgb_to_hsv(oldRGBA[:3])
newHSV = rgb_to_hsv(newRGB)
oldHSV[0] = newHSV[0]
newRGBA = hsv_to_rgb(oldHSV).tolist() + [oldRGBA[3]]
i.e. I take the RGB part of the old RGBA value, turn it to HSV, do the same for the new intended RGB color, then replace the hue, turn it back to RGB and add on the old alpha value.
Since I'm doing this thousands of times, this can take significantly longer than I would like to spend here. One possible fix would be to dig into the conversion between RGB and HSV and figure out how to do this in one go, but I was hoping that folks who know how to handle color (I really don't) have figured out simple and efficient ways to do this.
How do I change the hue of a given RGBA color A to that of a given RGB color B while preserving the alpha value of A? Would using a different color model (HSL for example) simplify the task, and if so, which would help?

Here is the solution to do all the replacement in one go :
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
N = 100000
x = 1.2 + 800.0 * np.random.rand(N)
y = 1.2 + 800.0 * np.random.rand(N)
# Generate random colors of the form (r, g, b, a) where r = 0.0
colors = np.random.rand(4 * N).reshape((N, 4))
colors[:, 0] = 0.0
area = np.pi * (5 * np.random.rand(N))**2
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
pcol = ax.scatter(x, y, s=area, c=colors)
ax.set_xscale('log')
ax.set_yscale('log')
# Save initial figure
plt.savefig("hue.jpg")
oldRGBA = pcol.get_facecolors().reshape((N, 1, 4))
oldRGB = oldRGBA[:, :, :3]
newRGB = oldRGB
newRGB[:, :, 0] = 1.0 # Set red component to 1.0
oldHSV = clr.rgb_to_hsv(oldRGB)
newHSV = clr.rgb_to_hsv(newRGB)
oldHSV[:, :, 0] = newHSV[:, :, 0]
newRGBA = np.copy(oldRGBA)
newRGBA[:, :, :3] = clr.rgb_to_hsv(oldHSV)
pcol.set_facecolors(newRGBA[:, 0, :])
# Save modified figure
plt.savefig("hue_bis.jpg")
plt.close()
As you can see, this code attempts to plot 100000 points and in fact it managed to do this in about 2 seconds. Here are the figures produced :
and :
With regard to your last two questions :
How do I change the hue of a given RGBA color A to that of a given RGB color B while preserving the alpha value of A ?
and :
Would using a different color model (HSL for example) simplify the task, and if so, which would help
I think that your approach to do such a modification is appreciable, it avoids making calculations by hand (see HSL and HSV). Using a different color model is possible, both HSL and HSV allow to change the hue without affecting other parameters, but that is only an other way to do it and not a better one.
Hope this will help.

It should be possible to extract the alpha from oldRGBA and apply it to newRGB directly:
newRGBA = colors.to_rgba(newRGB, alpha=oldRGBA[3])
Note: I never used matplotlib, I assume the alpha component is the 4th element of the color

Related

Show class probabilities from Numpy array

I've had a look through and I don't think stack has an answer for this, I am fairly new at this though any help is appreciated.
I'm using an AWS Sagemaker endpoint to return a png mask and I'm trying to display the probability as a whole of each class.
So first stab does this:
np.set_printoptions(threshold=np.inf)
pred_map = np.argmax(mask, axis=0)
non_zero_mask = pred_map[pred_map != 0]) # get everything but background
# print(np.bincount(pred_map[pred_map != 0]).argmax()) # Ignore this line as it just shows the most probable
num_classes = 6
plt.imshow(pred_map, vmin=0, vmax=num_classes-1, cmap='jet')
plt.show()
As you can see I'm removing the background pixels, now I need to show class 1,2,3,4,5 have X probability based on the number of pixels they occupy - I'm unsure if I'll reinvent the wheel by simply taking the total number of elements from the original mask then looping and counting each pixel/class number etc - are there inbuilt methods for this please?
Update:
So after typing this out had a little think and reworded some of searches and came across this.
unique_elements, counts_elements = np.unique(pred_map[pred_map != 0], return_counts=True)
print(np.asarray((unique_elements, counts_elements)))
#[[ 2 3]
#[87430 2131]]
So then I'd just calculate the % based on this or is there a better way? For example I'd do
87430 / 89561(total number of pixels in the mask) * 100
Giving 2 in this case a 97% probability.
Update for Joe's comment below:
rec = Record()
recordio = mx.recordio.MXRecordIO(results_file, 'r')
protobuf = rec.ParseFromString(recordio.read())
values = list(rec.features["target"].float32_tensor.values)
shape = list(rec.features["shape"].int32_tensor.values)
shape = np.squeeze(shape)
mask = np.reshape(np.array(values), shape)
mask = np.squeeze(mask, axis=0)
My first thought was to use np.digitize and write a nice solution.
But then I realized how you can hack it in 10 lines:
import numpy as np
import matplotlib.pyplot as plt
size = (10, 10)
x = np.random.randint(0, 7, size) # your classes, seven excluded.
# empty array, filled with mask and number of occurrences.
x_filled = np.zeros_like(x)
for i in range(1, 7):
mask = x == i
count_mask = np.count_nonzero(mask)
x_filled[mask] = count_mask
print(x_filled)
plt.imshow(x_filled)
plt.colorbar()
plt.show()
I am not sure about the axis convention with imshow
at the moment, you might have to flip the y axis so up is up.
SageMaker does not provide in-built methods for this.

Dynamically scaling axes during a matplotlib ArtistAnimation

It appears to be impossible to change the y and x axis view limits during an ArtistAnimation, and have the frames replayed with different axis limits.
The limits seem to fixed to those set last before the animation function is called.
In the code below, I have two plotting stages. The input data in the second plot is a much smaller subset of the data in the 1st frame. The data in the 1st stage has a much wider range.
So, I need to "zoom in" when displaying the second plot (otherwise the plot would be very tiny if the axis limits remain the same).
The two plots are overlaid on two different images (that are of the same size, but different content).
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.image as mpimg
import random
# sample 640x480 image. Actual frame loops through
# many different images, but of same size
image = mpimg.imread('image_demo.png')
fig = plt.figure()
plt.axis('off')
ax = fig.gca()
artists = []
def plot_stage_1():
# both x, y axis limits automatically set to 0 - 100
# when we call ax.imshow with this extent
im_extent = (0, 100, 0, 100) # (xmin, xmax, ymin, ymax)
im = ax.imshow(image, extent=im_extent, animated=True)
# y axis is a list of 100 random numbers between 0 and 100
p, = ax.plot(range(100), random.choices(range(100), k=100))
# Text label at 90, 90
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 1")
artists.append([im, t, p])
def plot_stage_2():
# axes remain at the the 0 - 100 limit from the previous
# imshow extent so both the background image and plot are tiny
im_extent = (0, 10, 0, 10)
# so let's update the x, y axis limits
ax.set_xlim(im_extent[0], im_extent[1])
ax.set_ylim(im_extent[0], im_extent[3])
im = ax.imshow(image, extent=im_extent, animated=True)
p, = ax.plot(range(10), random.choices(range(10), k=10))
# Text label at 9, 9
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 2")
artists.append([im, t, p])
plot_stage_1()
plot_stage_2()
# clear white space around plot
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)
# set figure size
fig.set_size_inches(6.67, 5.0, True)
anim = animation.ArtistAnimation(fig, artists, interval=2000, repeat=False, blit=False)
plt.show()
If I call just one of the two functions above, the plot is fine. However, if I call both, the axis limits in both frames will be 0 - 10, 0 - 10. So frame 1 will be super zoomed in.
Also calling ax.set_xlim(0, 100), ax.set_ylim(0, 100) in plot_stage_1() doesn't help. The last set_xlim(), set_ylim() calls fix the axis limits throughout all frames in the animation.
I could keep the axis bounds fixed and apply a scaling function to the input data.
However, I'm curious to know whether I can simply change the axis limits -- my code will be better this way, because the actual code is complicated with multiple stages, zooming plots across many different ranges.
Or perhaps I have to rejig my code to use FuncAnimation, instead of ArtistAnimation?
FuncAnimation appears to result in the expected behavior. So I'm changing my code to use that instead of ArtistAnimation.
Still curious to know though, whether this can at all be done using ArtistAnimation.

Plot axvline from Point to Point in Matplotlib Python 3.6

I am reading Data from a Simulation out of an Excel File. Out of this Data I generated two DataFrames containing 200 values. Now i want to plot all the Values from DataFrame one in blue and all Values from DataFrame two in purple. Therefore I have following code:
df = pd.read_excel("###CENSORED####.xlsx", sheetname="Data")
unpatched = df["Unpatched"][:-800]
patched = df["Patched"][:-800]
x = range(0,len(unpatched))
fig = plt.figure(figsize=(10, 5))
plt.scatter(x, unpatched, zorder=10, )
plt.scatter(x, patched, c="purple",zorder=19,)
This results in following Graph:
But now i want to draw in some lines that visualize the difference between the blue and purple dots. I thought about an orange line going from blue dot at simulation-run x to the purple dot at simulation-run x. I've tried to "cheat" with following code, since I'm pretty new to matplotlib.
scale_factor = 300
for a in x:
plt.axvline(a, patched[a]/scale_factor, unpatched[a]/scale_factor, c="orange")
But this resulted in a inaccuracy as seen seen below:
So is there a smarter way to do this? I've realized that the axvline documentation only says that ymin, ymax can only be scalars. Can I somehow turn my given values into fitting scalars?

Is there a convenient way to add a scale indicator to a plot in matplotlib?

I want to add a scale indicator to a plot like the one labelled '10kpc' in the (otherwise) empty plot below. So basically, the axis use one unit of measure and I want to indicate a length in the plot in a different unit. It has to have the same style as below, i.e. a |----| bar with text above.
Is there a convenient way in matplotlib to do that or do I have to draw three lines (two small vertical, one horizontal) and add the text? An ideal solution would not even require me to set coordinates in the data dimensions, i.e. I just say something along the line of horizontalalignment='left', verticalalignment='bottom', transform=ax.transAxes and specify only the width in data coordinates.
I fought with annotate() and arrow() and their documentations for quiet a bit until I concluded, they were not exactly useful, but I might be wrong.
Edit:
The code below is the closest, I have come so far. I still don't like having to specify the x-coordinates in the data coordinate system. The only thing I want to specify in data is the width of the bar. The rest should be placed in the plot system and ideally the bar should be placed relative to the text (a few pixels above).
import matplotlib.pyplot as plt
import matplotlib.transforms as tfrms
plt.imshow(somedata)
plt.colorbar()
ax = plt.gca()
trans = tfrms.blended_transform_factory( ax.transData, ax.transAxes )
plt.errorbar( 5, 0.06, xerr=10*arcsecperkpc/2, color='k', capsize=5, transform=trans )
plt.text( 5, 0.05, '10kpc', horizontalalignment='center', verticalalignment='top', transform=trans )
Here is a code that adds a horizontal scale bar (or scale indicator or scalebar) to a plot. The bar's width is given in data units, while the height of the edges is in fraction of axes units.
The solution is based on an AnchoredOffsetbox, which contains a VPacker. The VPacker has a label in its lower row, and an AuxTransformBox in its upper row.
The key here is that the AnchoredOffsetbox is positioned relative to the axes, using the loc argument similar to the legend positioning (e.g. loc=4 denotes the lower right corner). However, the AuxTransformBox contains a set of elements, which are positioned inside the box using a transformation. As transformation we can choose a blended transform which transforms x coordinates according to the data transform of the axes and y coordinates according to the axes transform. A tranformation which does this is actually the xaxis_transform of the axes itself. Supplying this transform to the AuxTransformBox allows us to specify the artists within (which are Line2Ds in this case) in a useful way, e.g. the line of the bar will be Line2D([0,size],[0,0]).
All of this can be packed into a class, subclassing the AnchoredOffsetbox, such that it is easy to be used in an existing code.
import matplotlib.pyplot as plt
import matplotlib.offsetbox
from matplotlib.lines import Line2D
import numpy as np; np.random.seed(42)
x = np.linspace(-6,6, num=100)
y = np.linspace(-10,10, num=100)
X,Y = np.meshgrid(x,y)
Z = np.sin(X)/X+np.sin(Y)/Y
fig, ax = plt.subplots()
ax.contourf(X,Y,Z, alpha=.1)
ax.contour(X,Y,Z, alpha=.4)
class AnchoredHScaleBar(matplotlib.offsetbox.AnchoredOffsetbox):
""" size: length of bar in data units
extent : height of bar ends in axes units """
def __init__(self, size=1, extent = 0.03, label="", loc=2, ax=None,
pad=0.4, borderpad=0.5, ppad = 0, sep=2, prop=None,
frameon=True, linekw={}, **kwargs):
if not ax:
ax = plt.gca()
trans = ax.get_xaxis_transform()
size_bar = matplotlib.offsetbox.AuxTransformBox(trans)
line = Line2D([0,size],[0,0], **linekw)
vline1 = Line2D([0,0],[-extent/2.,extent/2.], **linekw)
vline2 = Line2D([size,size],[-extent/2.,extent/2.], **linekw)
size_bar.add_artist(line)
size_bar.add_artist(vline1)
size_bar.add_artist(vline2)
txt = matplotlib.offsetbox.TextArea(label, minimumdescent=False)
self.vpac = matplotlib.offsetbox.VPacker(children=[size_bar,txt],
align="center", pad=ppad, sep=sep)
matplotlib.offsetbox.AnchoredOffsetbox.__init__(self, loc, pad=pad,
borderpad=borderpad, child=self.vpac, prop=prop, frameon=frameon,
**kwargs)
ob = AnchoredHScaleBar(size=3, label="3 units", loc=4, frameon=True,
pad=0.6,sep=4, linekw=dict(color="crimson"),)
ax.add_artist(ob)
plt.show()
In order to achieve a result as desired in the question, you can set the frame off and adjust the linewidth. Of course the transformation from the units you want to show (kpc) into data units (km?) needs to be done by yourself.
ikpc = lambda x: x*3.085e16 #x in kpc, return in km
ob = AnchoredHScaleBar(size=ikpc(10), label="10kpc", loc=4, frameon=False,
pad=0.6,sep=4, linekw=dict(color="k", linewidth=0.8))

Problems with zeros in matplotlib.colors.LogNorm

I am plotting a histogram using
plt.imshow(hist2d, norm = LogNorm(), cmap = gray)
where hist2d is a matrix of histogram values. This works fine except for elements in hist2d that are zero. In particular, I obtain the following image
but would like the white patches to be black.
Thank you!
Here's an alternative method that does not require you to muck with your data by setting a rgb value for bad pixels.
import copy
data = np.arange(25).reshape((5,5))
my_cmap = copy.copy(matplotlib.cm.get_cmap('gray')) # copy the default cmap
my_cmap.set_bad((0,0,0))
plt.imshow(data,
norm=matplotlib.colors.LogNorm(),
interpolation='nearest',
cmap=my_cmap)
The problem is that bins with 0 can not be properly log normalized so they are flagged as 'bad', which are mapped to differently. The default behavior is to not draw anything on those pixels. You can also specify what color to draw pixels that are over or under the limits of the color map (the default is to draw them as the highest/lowest color).
If you're happy with the colour scaling as is, and simply want the 0 values to be black, I'd simply change the input matrix so that the 0s are replaced by the next smallest value:
import matplotlib.pyplot as plt
import matplotlib.cm, matplotlib.colors
import numpy
hist2d = numpy.arange(9).reshape(3,3)
plt.imshow(numpy.maximum(hist2d, sorted(hist2d.flat)[1]),
interpolation='nearest',
norm = matplotlib.colors.LogNorm(),
cmap = matplotlib.cm.gray)
produces
Was this generated with the matplotlib hist2d function?
All you need to do is go through the matrix and set some arbitrary floor value, then make sure to plot this with fixed limits
for f in hist2d:
f += 1e-3
then when you show the figure, all of the whitespace will now be at the floor value, and will show up on the lognormal plot . However, if you are letting hist2d automatically pick the scaling for you, it will want to use the 1e-3 floor value as the minimum. To avoid this, you need to set vmin and vmax values in hist2d
hist2d(x,y,bins=40, norm=LogNorm(), vmin=1, vmax=1e4)