# Copyright (C) Frank Zaverl, Jr.
# See file LICENSE for license information.
'''
Auxiliary functions for use with Matplotlib.
'''
# These functions use the colorspacious module for
# creating Lab color space colormaps.
import string
import random
import numpy as np
import matplotlib as mpl
from matplotlib import cm,colors # update for v_1.2.0
from matplotlib.colors import ListedColormap
from colorspacious import cspace_converter
import s3dlib.cmap_utilities as cmu
[docs]def Lab_cmap_gradient(lowColor='k', highColor='w', name=None, mirrored=False) :
"""
A linear-in-Lab-space Colormap, with option of registering the map.
Parameters
----------
lowColor : RGB color, optional, default: 'black'
Color at the low end of the Colormap range.
highColor : RGB color, optional, default: 'white'
Color at the high end of the Colormap range.
name : str, optional
The registered name to identify the colormap.
If it's None, the name will be a string of 8 random
characters.
mirrored : bool
If True, colormap is divided into two linear
segments with the lowColor at the low and high
values, the highColor in the middle.
Returns
-------
ListedColormap
An instance of a colormap.
"""
if name is not None :
if not isinstance(name, str ):
raise ValueError('Invalid name argument (str required): ' + str(name))
if isinstance(lowColor, str ): lowColor = colors.to_rgb(lowColor)
if isinstance(highColor, str ): highColor= colors.to_rgb(highColor)
if not isinstance(lowColor,(list, tuple, np.ndarray)) :
raise ValueError('Invalid lowColor argument: ' + str(name))
if not isinstance(highColor,(list, tuple, np.ndarray)) :
raise ValueError('Invalid highColor argument: ' + str(name))
lowColor, highColor = list(lowColor), list(highColor)
if len(lowColor) == 3 : lowColor.append(1.0)
if len(highColor) == 3 : highColor.append(1.0)
if len(lowColor) != 4 or len(highColor) != 4 :
raise ValueError('Invalid HSVarg argument list length ')
lowLab = cspace_converter("sRGB1", "CAM02-UCS")(lowColor[:3])
hghLab = cspace_converter("sRGB1", "CAM02-UCS")(highColor[:3])
deltaL = np.subtract(hghLab,lowLab)
deltaA = highColor[3] - lowColor[3]
numbSegs = 256
x = np.linspace(0.0,1.0,num=numbSegs)
if mirrored : x = abs( 2.0*x - 1.0 )
# DevNote: insignificant time spent in loop. no need to opt.
clist = []
for n in x :
lab = np.add(lowLab, np.multiply(deltaL,n) )
alf = lowColor[3] + n*deltaA
rgb = cspace_converter("CAM02-UCS", "sRGB1")(lab)
rgb = np.clip(rgb,0,1)
rgba = np.append(rgb,alf)
clist.append(rgba)
cmap = ListedColormap(clist)
if name is None :
name = ''.join(random.choices(string.ascii_uppercase , k = 8))
mpl.colormaps.register(cmap,name=name) # update for v_1.2.0
cmap.name = name
return cmap
[docs]def Hue_Lab_gradient( color, lowL=0.0, hiL=1.0, name=None ) :
"""
A linear-in-Lab-space Colormap with a constant hue, with option of registering the map.
Parameters
----------
color : RGB color(str,list,tuple) or number in the domain[0,1]
Vaule is used to
lowL : number, optional, default: 0.0
The minimum L* lightness value in range 0 to <1
hiL : number, optional, default: 1.0
The maximum L* lightness value in range >0 to 1.
name : str, optional, default: None
The registered name to identify the colormap.
If it's None and color is a string, name is assigned to the string_L.
Otherwize, if it's None, name is assigned a string of 8 random characters.
Returns
-------
ListedColormap
An instance of a colormap.
"""
# ...............................................................
def get_arrays(hue,N) :
hsv_0 = np.array([hue,1.0,1.0])
rgb_0 = np.array(colors.hsv_to_rgb(hsv_0.T)).T
L_0,_,_ = cspace_converter("sRGB1", "CAM02-UCS" )(rgb_0.T).T
L_0 = L_0/100.0
L = np.linspace(0.0,1.0,N)
endS = (1.0 -L)/(1.0 - L_0)
sttV = L/L_0
ones = np.ones(len(L))
L0vals = np.full(len(L),L_0)
H = np.full(len(L),hue)
S = np.where( L>L0vals, endS, ones )
V = np.where( L>L0vals, ones, sttV )
hsv = np.array( [H,S,V] )
rgb = np.array(colors.hsv_to_rgb(hsv.T)).T
Lact,_,_ = cspace_converter("sRGB1", "CAM02-UCS" )(rgb.T).T
Lact = Lact/100.0
Lact[0]=0.0
Lact[len(Lact)-1] = 1.000001
return Lact,S,V
# ...............................................................
def linIntrp(xVal,xArr,yArr) :
i = np.where(xArr>xVal)[0][0]
x0,x1 = xArr[i-1], xArr[i]
y0,y1 = yArr[i-1], yArr[i]
m = (y1-y0)/(x1-x0)
yVal = (xVal-x0)*m + y0
return yVal
# ...............................................................
if isinstance(color, str ):
hue,_,_ = colors.rgb_to_hsv(colors.to_rgb(color))
if name is None : name = color+'_L'
elif isinstance(color,(list,tuple)) :
hue,_,_ = colors.rgb_to_hsv(colors.to_rgb(color))
else :
hue = color
if hue<0.0 or hue>1.0 :
raise ValueError('Error: required that 0.0 <= hue <= 1.0. , found {}'.format(hue))
Lval,Sval,Vval = get_arrays(hue,100)
N=256
L = np.linspace(lowL,hiL,N)
H = np.full(len(L),hue)
S = [ linIntrp(Lst,Lval,Sval) for Lst in L ]
V = [ linIntrp(Lst,Lval,Vval) for Lst in L ]
hsv = np.array( [H,S,V] )
rgb = np.array(colors.hsv_to_rgb(hsv.T)).T
rgba = np.vstack( (rgb, np.ones(len(L)) ) ).T
cmap = ListedColormap(rgba)
if name is None :
name = ''.join(random.choices(string.ascii_uppercase , k = 8))
mpl.colormaps.register(cmap,name=name) # update for v_1.2.0
cmap.name = name
return cmap
def _Cmap_Lab_gradient( cmap, lowL=0.0, hiL=1.0, name=None ) :
"""
A linear-in-Lab-space Colormap with a hue matched to cmap and maximum saturation.
Parameters
----------
cmap : colormap
lowL : number, optional, default: 0.0
The minimum L* lightness value in range 0 to <1
hiL : number, optional, default: 1.0
The maximum L* lightness value in range >0 to 1.
name : str, optional, default: None
The registered name to identify the colormap.
If it's None and color is a string, name is assigned to the string_L.
Otherwize, if it's None, name is assigned a string of 8 random characters.
Returns
-------
ListedColormap
An instance of a colormap.
"""
# ...............................................................
def get_arrays(cmap,lw,hi,N) :
cols = cmap(np.linspace(0,1,N))
hsv = colors.rgb_to_hsv(cols[:,:3])
hue = hsv[:,0]
hsv_0 = np.ones((N,3))
hsv_0[:,0] = hue
rgb_0 = np.array(colors.hsv_to_rgb(hsv_0)).T
L_0,a,b = cspace_converter("sRGB1", "CAM02-UCS" )(rgb_0.T).T
L_0 = L_0/100.0
L = np.linspace(lw,hi,N)
endS = (1.0 -L)/(1.0 - L_0)
sttV = L/L_0
ones = np.ones(len(L))
L0vals = np.full(len(L),L_0)
H = hue
S = np.where( L>L0vals, endS, ones )
V = np.where( L>L0vals, ones, sttV )
hsv = np.array( [H,S,V] )
rgb = np.array(colors.hsv_to_rgb(hsv.T)).T
Lact,a,b = cspace_converter("sRGB1", "CAM02-UCS" )(rgb.T).T
Lact = Lact/100.0
Lact[0]=lw
Lact[len(Lact)-1] = 1.000001*hi
return Lact,a,b #<<<<< linear in lab space.
# ...............................................................
def linIntrp(xVal,xArr,yArr) :
i = np.where(xArr>xVal)[0][0]
x0,x1 = xArr[i-1], xArr[i]
y0,y1 = yArr[i-1], yArr[i]
m = (y1-y0)/(x1-x0)
yVal = (xVal-x0)*m + y0
return yVal
# ...............................................................
if isinstance(cmap,str) :
cmap = mpl.colormaps[cmap] # update for v_1.2.0
if name is None :
name = cmap.name + '_L'
Lval,aval,bval = get_arrays(cmap,lowL,hiL,100)
N=256
L = np.linspace(lowL,hiL,N)
a = [ linIntrp(Lst,Lval,aval) for Lst in L ]
b = [ linIntrp(Lst,Lval,bval) for Lst in L ]
lab = np.array( [100*L,a,b]).T
rgb = cspace_converter("CAM02-UCS", "sRGB1")(lab)
rgba = np.vstack( (rgb.T, np.ones(len(L)) ) ).T
rgba = np.clip(rgba, 0.0,1.0)
cmap = ListedColormap(rgba)
if name is None :
name = ''.join(random.choices(string.ascii_uppercase , k = 8))
mpl.colormaps.register(cmap,name=name) # update for v_1.2.0
cmap.name = name
return cmap
[docs]def Cmap_Lab_gradient(cmap, lowL=None, hiL=None, name=None) :
"""
A linear-in-Lab-space Colormap with a hue matched to cmap at maximum saturation.
Parameters
----------
cmap : colormap
lowL : number, optional, default: cmap min L* @ boundaries
The minimum L* lightness value in range 0 to <1
hiL : number, optional, default: cmap max L* @ boundaries
The maximum L* lightness value in range >0 to 1.
name : str, optional, default: None
The registered name to identify the colormap.
If None, ā_Lā will be appended to the colormap name.
Returns
-------
ListedColormap
An instance of a colormap.
"""
# DevNote: easier to just call the previous version of this function
# with reset defaults than to edit the previous function.
if isinstance(cmap,str) :
cmap = mpl.colormaps[cmap] # update for v_1.2.0
if lowL is None:
strRGB = cm.colors.to_rgb(cmap(0.0))
strLab = cspace_converter("sRGB1", "CAM02-UCS" )(strRGB)
sLab = strLab[0]/100
else : sLab = 0.0
if hiL is None :
endRGB = cm.colors.to_rgb(cmap(1.0))
endLab = cspace_converter("sRGB1", "CAM02-UCS" )(endRGB)
eLab = endLab[0]/100
else : eLab = 1.0
if name is None :
name = cmap.name + '_L'
if sLab>eLab : # then L* decrease from start to finish.
sLab, eLab = eLab, sLab
# don't want to auto-register the colormap name, use a tempName
# that's random so not to register an already given name.
tempName = ''.join(random.choices(string.ascii_uppercase , k=8))
cmap = cmu.reversed_cmap(cmap,name=tempName)
cmap = _Cmap_Lab_gradient(cmap, lowL=sLab, hiL=eLab)
newCmap = cmu.reversed_cmap(cmap,name=name)
else :
newCmap = _Cmap_Lab_gradient(cmap, lowL=sLab, hiL=eLab, name=name)
return newCmap
[docs]def show_cmaps( plt, cmaps, onlyColormaps=True, show=True) :
"""
Construct and show Matplotlib figures of colormaps.
Parameters
----------
plt : matplotlib.pyplot
cmaps : List of colormaps.
List contains colormaps or strings of registered
colormap names.
onlyColormaps : bool {True,False}, optional, default: True
If True, only a color scale figure is shown. If False,
the L* gray scale and L* line plot figures are also shown.
show : bool {True,False}, optional, default: True
If True, the plt method 'show()' will be executed.
"""
# ===================================================================
def show_trans_cmap(ax,cmap,Lstr=None,aspect=24) :
# ...............................................
def over_color(lower_color, upper_color) :
opaque, transparent = 0.99, 0.01
alpha_A, alpha_B = upper_color[3], lower_color[3]
alpha_0 = alpha_A + alpha_B*( 1 - alpha_A )
if alpha_A >= opaque : return upper_color
if alpha_B <= transparent : return lower_color
if alpha_0 <= transparent : return np.array([0,0,0,0])
C_A, C_B = np.array(upper_color[:3]), np.array(lower_color[:3])
c0 = C_A*alpha_A + C_B*alpha_B*( 1 - alpha_A)
C_0 = np.divide(c0,alpha_0)
return np.append(C_0,alpha_0)
# ...............................................
sqrsz, sph = 4, 3 # pixels per square, squares per height
bgrd, blck = [1.0,1.0,1.0,1.0], [0.8,0.8,0.8,1.0]
if Lstr is not None :
bgrd, blck = [1.0,0.7,0.7,1.0], [0.6,1.0,0.6,1.0]
hdim = sph*sqrsz
wdim = aspect*hdim
i_ck = True
X = np.empty([hdim, wdim,4])
X[::] = np.array(bgrd)
for i in range(hdim) :
if i%sqrsz == 0 : i_ck = not i_ck
j_ck = i_ck
for j in range(wdim) :
if j%sqrsz == 0 : j_ck = not j_ck
bottom = X[i,j]
if j_ck : bottom = blck
# ---------------------
if Lstr is not None :
val = Lstr[int(255*j/wdim)]/100.0
top = [val,val,val,cmap(j/wdim)[3] ]
else :
top = cmap(j/wdim)
X[i,j] = over_color(bottom, top)
ax.imshow(X, aspect='auto')
return
# ...................................................................
def hasTransparency(cmap) :
if isinstance(cmap,str) : cmap = mpl.colormaps[cmap] # update for v_1.2.0
N = 256
alpha = cmap(np.linspace(0, 1, N))[:,3:4]
return np.any( np.where(alpha<1,True,False) )
# ...................................................................
def Lstar(cmap) :
# x,y arrays for ploting L* of a colormap.
if isinstance(cmap,str) : cmap = mpl.colormaps[cmap] # update for v_1.2.0
N = 256
rgb = cmap(np.linspace(0, 1, N))[:,0:3]
lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
L = np.transpose(lab)[0]
x = np.linspace(0, 1, N)
return x,L,cmap.name
# ===================================================================
# Figure 1: Visual color scale. -------------------------------------
numb =len(cmaps)
w, h = 6, numb*0.25
fig, axes = plt.subplots(nrows=numb, figsize=(w,h))
fig.subplots_adjust(top=0.95, bottom=0.01, left=0.2, right=0.99)
if numb==1 : axes = [axes]
for ax in axes :
ax.set_xticks([])
ax.set_yticks([])
grad = np.linspace(0, 1, 256)
grad = np.vstack((grad, grad))
for i in range(len(cmaps)):
ax = axes[i]
cmap=cmaps[i]
if isinstance(cmap,str) : cmap = mpl.colormaps[cmap] # update for v_1.2.0
if hasTransparency(cmap) :
show_trans_cmap(ax,cmap)
else :
ax.imshow(grad, aspect='auto', cmap=cmap)
pos = list(ax.get_position().bounds)
x_text = pos[0] - 0.01
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, cmap.name, va='center', ha='right', fontsize=10)
if not onlyColormaps :
# Figure 2: Visual L* gray scale. -----------------------------------
numb =len(cmaps)
w, h = 6, numb*0.25
fig, axes = plt.subplots(nrows=numb, figsize=(w,h))
fig.subplots_adjust(top=0.95, bottom=0.01, left=0.2, right=0.99)
if numb==1 : axes = [axes]
for ax in axes :
ax.set_xticks([])
ax.set_yticks([])
for i in range(len(cmaps)):
cmap = cmaps[i]
if isinstance(cmap,str) : cmap = mpl.colormaps[cmap] # update for v_1.2.0
x,y,name = Lstar(cmap)
grad = np.float32(np.vstack((y, y)))
ax = axes[i]
if hasTransparency(cmap) :
show_trans_cmap(ax,cmap,y)
else :
ax.imshow(grad, aspect='auto', cmap='binary_r', vmin=0., vmax=100.)
pos = list(ax.get_position().bounds)
x_text = pos[0] - 0.01
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, name, va='center', ha='right', fontsize=10)
# Figure 3: x,L* line plot. -----------------------------------------
colors = ['k','r','g','b','y','m','c']
linestyles = ['-', '--', '-.', ':']
fig = plt.figure(figsize=plt.figaspect(0.6))
ax = fig.add_axes([0.1, 0.1, 0.65, 0.85])
ax.set(xlim=(0,1), ylim=(0,100))
ax.set_ylabel('L* value')
Lcol, Lstyles = len(colors), len(linestyles)
for i in range(len(cmaps)) :
linestyle = linestyles[i%Lstyles]
color = colors[i%Lcol]
x,y,name = Lstar(cmaps[i])
ax.plot(x,y, label=name, color=color, linestyle=linestyle)
ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0.)
# ===================================================================
if show : plt.show()
return