#!/usr/bin/env python #------------------------------------------------------------------------------ # Name: colorMapCreator.py # Purpose: Generate reasonable diverging colormaps using the technique # presented in "Diverging Color Maps for Scientific Visualization # (Expanded)" by Kenneth Moreland. # # Author: Carlo Barth # # Created: 22.10.2013 # Copyright: (c) 2013 #------------------------------------------------------------------------------ # Imports import numpy as np # ============================================================================= # ====================== The Class ColorMapCreator ============================ # ============================================================================= class ColorMapCreator: """ Class ColorMapCreator: Create diverging colormaps from RGB1 to RGB2 using the method of Moreland or a simple CIELAB-interpolation. numColors controls the number of color values to output (odd number) and divide gives the possibility to output RGB-values from 0.0-1.0 instead of 0-255. If a filename different than "" is given, the colormap will be saved to this file, otherwise a simple output using print will be given. """ # ======================== Global Variables =============================== # Reference white-point D65 Xn, Yn, Zn = [95.047, 100.0, 108.883] # from Adobe Cookbook # Transfer-matrix for the conversion of RGB to XYZ color space transM = np.array([[0.4124564, 0.2126729, 0.0193339], [0.3575761, 0.7151522, 0.1191920], [0.1804375, 0.0721750, 0.9503041]]) # ============================= Functions ================================= def __init__(self, RGB1, RGB2, numColors = 33., divide = 255., method = "moreland", filename = ""): # create a class variable for the number of colors self.numColors = numColors # assert an odd number of points assert np.mod(numColors,2) == 1, \ "For diverging colormaps odd numbers of colors are desireable!" # assert a known method was specified knownMethods = ["moreland", "lab"] assert method in knownMethods, "Unknown method was specified!" if method == knownMethods[0]: #generate the Msh diverging colormap colorMap = self.generateColorMap(RGB1, RGB2, divide) elif method == knownMethods[1]: # generate the Lab diverging colormap colorMap = self.generateColorMapLab(RGB1, RGB2, divide) # print out the colormap of save it to file named filename if filename == "": for c in colorMap: print "{0}, {1}, {2}".format(c[0], c[1], c[2]) else: with open(filename, "w") as f: for c in colorMap: f.write("{0}, {1}, {2}\n".format(c[0], c[1], c[2])) #- def rgblinear(self, RGB): """ Conversion from the sRGB components to RGB components with physically linear properties. """ # initialize the linear RGB array RGBlinear = np.zeros((3,)) # calculate the linear RGB values for i,value in enumerate(RGB): value = float(value) / 255. if value > 0.04045 : value = ( ( value + 0.055 ) / 1.055 ) ** 2.4 else : value = value / 12.92 RGBlinear[i] = value * 100. return RGBlinear #- def sRGB(self, RGBlinear): """ Back conversion from linear RGB to sRGB. """ # initialize the sRGB array RGB = np.zeros((3,)) # calculate the sRGB values for i,value in enumerate(RGBlinear): value = float(value) / 100. if value > 0.00313080495356037152: value = (1.055 * np.power(value,1./2.4) ) - 0.055 else : value = value * 12.92 RGB[i] = round(value * 255.) return RGB #- def rgb2xyz(self, RGB): """ Conversion of RGB to XYZ using the transfer-matrix """ return np.dot(self.rgblinear(RGB), self.transM) #- def xyz2rgb(self, XYZ): """ Conversion of RGB to XYZ using the transfer-matrix """ #return np.round(np.dot(XYZ, np.array(np.matrix(transM).I))) return self.sRGB(np.dot(XYZ, np.array(np.matrix(self.transM).I))) #- def rgb2Lab(self, RGB): """ Conversion of RGB to CIELAB """ # convert RGB to XYZ X, Y, Z = (self.rgb2xyz(RGB)).tolist() # helper function def f(x): limit = 0.008856 if x> limit: return np.power(x, 1./3.) else: return 7.787*x + 16./116. # calculation of L, a and b L = 116. * ( f(Y/self.Yn) - (16./116.) ) a = 500. * ( f(X/self.Xn) - f(Y/self.Yn) ) b = 200. * ( f(Y/self.Yn) - f(Z/self.Zn) ) return np.array([L, a, b]) #- def Lab2rgb(self, Lab): """ Conversion of CIELAB to RGB """ # unpack the Lab-array L, a, b = Lab.tolist() # helper function def finverse(x): xlim = 0.008856 a = 7.787 b = 16./116. ylim = a*xlim+b if x > ylim: return np.power(x, 3) else: return ( x - b ) / a # calculation of X, Y and Z X = self.Xn * finverse( (a/500.) + (L+16.)/116. ) Y = self.Yn * finverse( (L+16.)/116. ) Z = self.Zn * finverse( (L+16.)/116. - (b/200.) ) # conversion of XYZ to RGB return self.xyz2rgb(np.array([X,Y,Z])) #- def Lab2Msh(self, Lab): """ Conversion of CIELAB to Msh """ # unpack the Lab-array L, a, b = Lab.tolist() # calculation of M, s and h M = np.sqrt(np.sum(np.power(Lab, 2))) s = np.arccos(L/M) h = np.arctan2(b,a) return np.array([M,s,h]) #- def Msh2Lab(self, Msh): """ Conversion of Msh to CIELAB """ # unpack the Msh-array M, s, h = Msh.tolist() # calculation of L, a and b L = M*np.cos(s) a = M*np.sin(s)*np.cos(h) b = M*np.sin(s)*np.sin(h) return np.array([L,a,b]) #- def rgb2Msh(self, RGB): """ Direct conversion of RGB to Msh. """ return self.Lab2Msh(self.rgb2Lab(RGB)) #- def Msh2rgb(self, Msh): """ Direct conversion of Msh to RGB. """ return self.Lab2rgb(self.Msh2Lab(Msh)) #- def adjustHue(self, MshSat, Munsat): """ Function to provide an adjusted hue when interpolating to an unsaturated color in Msh space. """ # unpack the saturated Msh-array Msat, ssat, hsat = MshSat.tolist() if Msat >= Munsat: return hsat else: hSpin = ssat * np.sqrt(Munsat**2 - Msat**2) / \ (Msat * np.sin(ssat)) if hsat > -np.pi/3: return hsat + hSpin else: return hsat - hSpin #- def interpolateColor(self, RGB1, RGB2, interp): """ Interpolation algorithm to automatically create continuous diverging color maps. """ # convert RGB to Msh and unpack Msh1 = self.rgb2Msh(RGB1) M1, s1, h1 = Msh1.tolist() Msh2 = self.rgb2Msh(RGB2) M2, s2, h2 = Msh2.tolist() # If points saturated and distinct, place white in middle if (s1>0.05) and (s2>0.05) and ( np.abs(h1-h2) > np.pi/3. ): Mmid = max([M1, M2, 88.]) if interp < 0.5: M2 = Mmid s2 = 0. h2 = 0. interp = 2*interp else: M1 = Mmid s1 = 0. h1 = 0. interp = 2*interp-1. # Adjust hue of unsaturated colors if (s1 < 0.05) and (s2 > 0.05): h1 = self.adjustHue(np.array([M2,s2,h2]), M1) elif (s2 < 0.05) and (s1 > 0.05): h2 = self.adjustHue(np.array([M1,s1,h1]), M2) # Linear interpolation on adjusted control points MshMid = (1-interp)*np.array([M1,s1,h1]) + \ interp*np.array([M2,s2,h2]) return self.Msh2rgb(MshMid) #- def generateColorMap(self, RGB1, RGB2, divide): """ Generate the complete diverging color map using the Moreland-technique from RGB1 to RGB2, placing "white" in the middle. The number of points given by "numPoints" controls the resolution of the colormap. The optional parameter "divide" gives the possibility to scale the whole colormap, for example to have float values from 0 to 1. """ # calculate scalars = np.linspace(0., 1., self.numColors) RGBs = np.zeros((self.numColors, 3)) for i,s in enumerate(scalars): RGBs[i,:] = self.interpolateColor(RGB1, RGB2, s) return RGBs/divide #- def generateColorMapLab(self, RGB1, RGB2, divide): """ Generate the complete diverging color map using a transition from Lab1 to Lab2, transitioning true RGB-white. The number of points given by "numPoints" controls the resolution of the colormap. The optional parameter "divide" gives the possibility to scale the whole colormap, for example to have float values from 0 to 1. """ # convert to Lab-space Lab1 = self.rgb2Lab(RGB1) Lab2 = self.rgb2Lab(RGB2) LabWhite = np.array([100., 0., 0.]) # initialize the resulting arrays Lab = np.zeros((self.numColors ,3)) RGBs = np.zeros((self.numColors ,3)) N2 = np.floor(self.numColors/2.) # calculate for i in range(3): Lab[0:N2+1, i] = np.linspace(Lab1[i], LabWhite[i], N2+1) Lab[N2:, i] = np.linspace(LabWhite[i], Lab2[i], N2+1) for i,l in enumerate(Lab): RGBs[i,:] = self.Lab2rgb(l) return RGBs/divide #- # ============================================================================= # ========================== The Main-Function ================================ # ============================================================================= def main(): # define the initial and final RGB-colors (low and high end of the diverging # colormap RGB1 = np.array([59, 76, 192]) RGB2 = np.array([180, 4, 38]) # create a new instance of the ColorMapCreator-class using the desired # options # simple printout colormap = ColorMapCreator(RGB1, RGB2) # save to specific file ## import os ## current_directory = os.getcwd() ## fname = "RedBlueColormap.dat" ## filename = os.path.join(current_directory, fname) ## colormap = ColorMapCreator(RGB1, RGB2, filename = filename) if __name__ == '__main__': main()