diff --git a/processing/post/perceptualUniformColorMap.py b/processing/post/perceptualUniformColorMap.py deleted file mode 100755 index 8e432536d..000000000 --- a/processing/post/perceptualUniformColorMap.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 - -import os -import sys -from optparse import OptionParser - -import damask - - -scriptName = os.path.splitext(os.path.basename(__file__))[0] -scriptID = ' '.join([scriptName,damask.version]) - - -# -------------------------------------------------------------------- -# MAIN -# -------------------------------------------------------------------- -#Borland, D., & Taylor, R. M. (2007). Rainbow Color Map (Still) Considered Harmful. Computer Graphics and Applications, IEEE, 27(2), 14--17. -#Moreland, K. (2009). Diverging Color Maps for Scientific Visualization. In Proc. 5th Int. Symp. Visual Computing (pp. 92--103). -outtypes = ['paraview','gmsh','raw','GOM'] -extensions = ['.json','.msh','.txt','.legend'] -colormodels = ['RGB','HSL','XYZ','CIELAB','MSH'] - -parser = OptionParser(option_class=damask.extendableOption, usage='%prog options [file[s]]', description = """ -Produces perceptually linear diverging and sequential colormaps in formats suitable for visualization software -or simply as a list of interpolated colors. - -""", version = scriptID) - -parser.add_option('-N','--steps', dest='N', type='int', nargs=1, metavar='int', - help='number of interpolation steps [%default]') -parser.add_option('-t','--trim', dest='trim', type='float', nargs=2, metavar='float float', - help='relative trim of colormap range [%default]') -parser.add_option('-l','--left', dest='left', type='float', nargs=3, metavar='float float float', - help='left color [%default]') -parser.add_option('-r','--right', dest='right', type='float', nargs=3, metavar='float float float', - help='right color [%default]') -parser.add_option('-c','--colormodel', dest='colormodel', metavar='string', - help='colormodel: '+', '.join(colormodels)+' [%default]') -parser.add_option('-p','--predefined', dest='predefined', metavar='string', - help='predefined colormap') -parser.add_option('-f','--format', dest='format', metavar='string', - help='output format: '+', '.join(outtypes)+' [%default]') -parser.set_defaults(colormodel = 'RGB') -parser.set_defaults(predefined = None) -parser.set_defaults(basename = None) -parser.set_defaults(format = 'paraview') -parser.set_defaults(N = 10) -parser.set_defaults(trim = (-1.0,1.0)) -parser.set_defaults(left = (1.0,1.0,1.0)) -parser.set_defaults(right = (0.0,0.0,0.0)) - -(options,filename) = parser.parse_args() - -if options.format not in outtypes: - parser.error('invalid format: "{}" (choices: {}).'.format(options.format,', '.join(outtypes))) - -if options.N < 2: - parser.error('too few steps (need at least 2).') - -if options.trim[0] < -1.0 or \ - options.trim[1] > 1.0 or \ - options.trim[0] >= options.trim[1]: - parser.error('invalid trim range (-1 +1).') - -name = options.format if filename == [] \ - else filename[0] -output = sys.stdout if filename == [] \ - else open(os.path.basename(filename[0])+extensions[outtypes.index(options.format)],'w') - -colorLeft = damask.Color(options.colormodel.upper(), list(options.left)) -colorRight = damask.Color(options.colormodel.upper(), list(options.right)) -colormap = damask.Colormap(colorLeft, colorRight, predefined=options.predefined) - -output.write(colormap.export(name,options.format,options.N,list(options.trim))) -output.close() diff --git a/python/damask/__init__.py b/python/damask/__init__.py index 6d65f7cd1..b2b94b6a5 100644 --- a/python/damask/__init__.py +++ b/python/damask/__init__.py @@ -10,7 +10,7 @@ with open(_Path(__file__).parent/_Path('VERSION')) as _f: from ._environment import Environment # noqa from ._table import Table # noqa from ._vtk import VTK # noqa -from ._colormaps import Colormap, Color # noqa +from ._colormap import Colormap # noqa from ._rotation import Rotation # noqa from ._lattice import Symmetry, Lattice# noqa from ._orientation import Orientation # noqa diff --git a/python/damask/_colormap.py b/python/damask/_colormap.py new file mode 100644 index 000000000..20b1a015f --- /dev/null +++ b/python/damask/_colormap.py @@ -0,0 +1,482 @@ +import json +import functools + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib import cm + +from damask import Table + +_eps = 216./24389. +_kappa = 24389./27. +_ref_white = np.array([.95047, 1.00000, 1.08883]) # Observer = 2, Illuminant = D65 + + +class Colormap(mpl.colors.ListedColormap): + + + @staticmethod + def from_bounds(low,high,N=256,name='DAMASK colormap',model='rgb'): + """ + Create a perceptually uniform colormap. + + Parameters + ---------- + low : numpy.ndarray of shape (3) + high : numpy.ndarray of shape (3) + N : integer, optional + Number of discrete color values. Defaults to 256. + model : str + Colormodel used for low and high. + + """ + low_,high_ = map(np.array,[low,high]) + if model.lower() == 'rgb': + # ToDo: Sanity check + low_,high_ = map(Colormap._rgb2msh,[low_,high_]) + elif model.lower() == 'hsv': + # ToDo: Sanity check + low_,high_ = map(Colormap._hsv2msh,[low_,high_]) + elif model.lower() == 'hsl': + # ToDo: Sanity check + low_,high_ = map(Colormap._hsl2msh,[low_,high_]) + elif model.lower() == 'xyz': + # ToDo: Sanity check + low_,high_ = map(Colormap._xyz2msh,[low_,high_]) + elif model.lower() == 'lab': + # ToDo: Sanity check + low_,high_ = map(Colormap._lab2msh,[low_,high_]) + elif model.lower() == 'msh': + # ToDo: Sanity check + pass + else: + raise ValueError(f'Invalid color model: {model}.') + + msh = map(functools.partial(Colormap._interpolate_msh,low=low_,high=high_),np.linspace(0,1,N)) + rgb = np.array(list(map(Colormap._msh2rgb,msh))) + + return Colormap(rgb,name=name) + + + @staticmethod + def from_predefined(name,N=256): + """ + Select from set of predefined colormaps. + + Predefined colormaps include matplotlib-native colormaps + and common DAMASK colormaps. + + Parameters + ---------- + name : str + N : int, optional + Number of discrete color values. Defaults to 256. + This parameter is not used for matplotlib colormaps + that are of type `ListedColormap`. + + """ + # matplotlib presets + for cat in Colormap._predefined_mpl: + for n in cat[1]: + if n == name: + colormap = cm.__dict__[name] + if isinstance(colormap,mpl.colors.LinearSegmentedColormap): + return Colormap(np.array(list(map(colormap,np.linspace(0,1,N)))),name=name) + else: + return Colormap(colormap.colors,name=name) + + # DAMASK presets + definition = Colormap._predefined_DAMASK[name] + return Colormap.from_bounds(definition['left'],definition['right'],N,name) + + + @staticmethod + def list_predefined(): + """List predefined colormaps by category.""" + print('DAMASK colormaps') + print(' '+', '.join(Colormap._predefined_DAMASK.keys())) + for cat in Colormap._predefined_mpl: + print(f'{cat[0]}') + print(' '+', '.join(cat[1])) + + + def show(self): + fig, ax = plt.subplots(figsize=(10,1)) + ax.set_axis_off() + im = ax.imshow(np.broadcast_to(np.linspace(0,1,640).reshape(1,-1),(64,640)),cmap=self) # noqa + fig.canvas.set_window_title(self.name) + plt.show() + + + def to_file(self,fname=None,format='paraview'): + if fname is not None: + try: + f = open(fname,'w') + except TypeError: + f = fname + else: + f = None + + if format.lower() == 'paraview': + Colormap._export_paraview(self,f) + elif format.lower() == 'ascii': + Colormap._export_ASCII(self,f) + + + @staticmethod + def _export_paraview(colormap,fhandle=None): + colors = [] + for i,c in enumerate(np.round(colormap.colors,6).tolist()): + colors+=[i]+c + + out = [{ + 'ColorSpace':'RGB', + 'Name':colormap.name, + 'DefaultMap':True, + 'RGBPoints':colors + }] + if fhandle is None: + with open(colormap.name.replace(' ','_')+'.json', 'w') as f: + json.dump(out, f,indent=4) + else: + json.dump(out,fhandle,indent=4) + + @staticmethod + def _export_ASCII(colormap,fhandle=None): + labels = {'R':(1,),'G':(1,),'B':(1,)} + if colormap.colors.shape[1] == 4: labels['alpha']=(1,) + + t = Table(colormap.colors,labels) + if fhandle is None: + with open(colormap.name.replace(' ','_')+'.txt', 'w') as f: + t.to_ASCII(f) + else: + t.to_ASCII(fhandle) + + @staticmethod + def _export_GOM(colormap,fhandle=None): + pass + # a = f'1 1 {name.replace(" ","_"} 9 {name.replace(" ","_"} ' + # f' 0 1 0 3 0 0 -1 9 \\ 0 0 0 255 255 255 0 0 255 ' + # f'30 NO_UNIT 1 1 64 64 64 255 1 0 0 0 0 0 0 3 0 ' + str(len(colors)) + # f' '.join([' 0 %s 255 1'%(' '.join([str(int(x*255.0)) for x in color])) for color in reversed(colors)])] + + @staticmethod + def _export_gmsh(colormap,fname=None): + colors = colormap.colors + colormap = ['View.ColorTable = {'] \ + + [',\n'.join(['{%s}'%(','.join([str(x*255.0) for x in color])) for color in colors])] \ + + ['}'] + + @staticmethod + def _interpolate_msh(frac,low, high): + + def rad_diff(a,b): + return abs(a[2]-b[2]) + + def adjust_hue(msh_sat, msh_unsat): + """If saturation of one of the two colors is much less than the other, hue of the less.""" + if msh_sat[0] >= msh_unsat[0]: + return msh_sat[2] + else: + hSpin = msh_sat[1]/np.sin(msh_sat[1])*np.sqrt(msh_unsat[0]**2.0-msh_sat[0]**2)/msh_sat[0] + if msh_sat[2] < - np.pi/3.0: hSpin *= -1.0 + return msh_sat[2] + hSpin + + + lo = np.array(low) + hi = np.array(high) + + if (lo[1] > 0.05 and hi[1] > 0.05 and rad_diff(lo,hi) > np.pi/3.0): + M_mid = max(lo[0],hi[0],88.0) + if frac < 0.5: + hi = np.array([M_mid,0.0,0.0]) + frac *= 2.0 + else: + lo = np.array([M_mid,0.0,0.0]) + frac = 2.0*frac - 1.0 + if lo[1] < 0.05 and hi[1] > 0.05: + lo[2] = adjust_hue(hi,lo) + elif lo[1] > 0.05 and hi[1] < 0.05: + hi[2] = adjust_hue(lo,hi) + + return (1.0 - frac) * lo + frac * hi + + + _predefined_mpl= [('Perceptually Uniform Sequential', [ + 'viridis', 'plasma', 'inferno', 'magma', 'cividis']), + ('Sequential', [ + 'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', + 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', + 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']), + ('Sequential (2)', [ + 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', + 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', + 'hot', 'afmhot', 'gist_heat', 'copper']), + ('Diverging', [ + 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', + 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']), + ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), + ('Qualitative', [ + 'Pastel1', 'Pastel2', 'Paired', 'Accent', + 'Dark2', 'Set1', 'Set2', 'Set3', + 'tab10', 'tab20', 'tab20b', 'tab20c']), + ('Miscellaneous', [ + 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern', + 'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', + 'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'])] + + _predefined_DAMASK = {'orientation': {'low': [0.933334,0.878432,0.878431], + 'right': [0.250980,0.007843,0.000000]}, + 'strain': {'left': [0.941177,0.941177,0.870588], + 'right': [0.266667,0.266667,0.000000]}, + 'stress': {'left': [0.878432,0.874511,0.949019], + 'right': [0.000002,0.000000,0.286275]}} + + @staticmethod + def _hsv2rgb(hsv): + """ + H(ue) S(aturation) V(alue) to R(red) G(reen) B(lue). + + References + ---------- + https://www.rapidtables.com/convert/color/hsv-to-rgb.html + + """ + sextant = np.clip(int(hsv[0]/60.),0,5) + c = hsv[1]*hsv[2] + x = c*(1.0 - abs((hsv[0]/60.)%2 - 1.)) + + return np.array([ + [c, x, 0], + [x, c, 0], + [0, c, x], + [0, x, c], + [x, 0, c], + [c, 0, x], + ])[sextant] + hsv[2] - c + + @staticmethod + def _rgb2hsv(rgb): + """ + R(ed) G(reen) B(lue) to H(ue) S(aturation) V(alue). + + References + ---------- + https://www.rapidtables.com/convert/color/rgb-to-hsv.html + + """ + C_max = rgb.max() + C_min = rgb.min() + Delta = C_max - C_min + + v = C_max + s = 0. if np.isclose(C_max,0.) else Delta/C_max + if np.isclose(Delta,0.): + h = 0. + elif rgb.argmax() == 0: + h = (rgb[1]-rgb[2])/Delta%6 + elif rgb.argmax() == 1: + h = (rgb[2]-rgb[0])/Delta + 2. + elif rgb.argmax() == 2: + h = (rgb[0]-rgb[1])/Delta + 4. + + h = np.clip(h,0.,6.) * 60. + + return np.array([h,s,v]) + + + @staticmethod + def _hsl2rgb(hsl): + """ + H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue). + + References + ---------- + https://www.rapidtables.com/convert/color/hsl-to-rgb.html + + """ + sextant = np.clip(int(hsl[0]/60.),0,5) + c = (1.0 - abs(2.0 * hsl[2] - 1.))*hsl[1] + x = c*(1.0 - abs((hsl[0]/60.)%2 - 1.)) + m = hsl[2] - 0.5*c + + return np.array([ + [c+m, x+m, m], + [x+m, c+m, m], + [m, c+m, x+m], + [m, x+m, c+m], + [x+m, m, c+m], + [c+m, m, x+m], + ])[sextant] + + @staticmethod + def _rgb2hsl(rgb): + """ + R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance). + + References + ---------- + https://www.rapidtables.com/convert/color/rgb-to-hsl.html + + """ + C_max = rgb.max() + C_min = rgb.min() + Delta = C_max - C_min + + l = np.clip((C_max + C_min)*.5,0.,1.) # noqa + s = 0. if np.isclose(C_max,C_min) else Delta/(1.-np.abs(2*l-1.)) + if np.isclose(Delta,0.): + h = 0. + elif rgb.argmax() == 0: + h = (rgb[1]-rgb[2])/Delta%6 + elif rgb.argmax() == 1: + h = (rgb[2]-rgb[0])/Delta + 2. + elif rgb.argmax() == 2: + h = (rgb[0]-rgb[1])/Delta + 4. + + h = np.clip(h,0.,6.) * 60. + + return np.array([h,s,l]) + + + @staticmethod + def _xyz2rgb(xyz): + """ + CIE Xyz to R(ed) G(reen) B(lue). + + References + ---------- + http://www.ryanjuckett.com/programming/rgb-color-space-conversion + + """ + rgb_lin = np.dot(np.array([ + [ 3.240969942,-1.537383178,-0.498610760], + [-0.969243636, 1.875967502, 0.041555057], + [ 0.055630080,-0.203976959, 1.056971514] + ]),xyz) + with np.errstate(invalid='ignore'): + rgb = np.where(rgb_lin>0.0031308,rgb_lin**(1.0/2.4)*1.0555-0.0555,rgb_lin*12.92) + + return np.clip(rgb,0.,1.) + + @staticmethod + def _rgb2xyz(rgb): + """ + R(ed) G(reen) B(lue) to CIE Xyz. + + References + ---------- + http://www.ryanjuckett.com/programming/rgb-color-space-conversion + + """ + rgb_lin = np.where(rgb>0.04045,((rgb+0.0555)/1.0555)**2.4,rgb/12.92) + return np.dot(np.array([ + [0.412390799,0.357584339,0.180480788], + [0.212639006,0.715168679,0.072192315], + [0.019330819,0.119194780,0.950532152] + ]),rgb_lin) + + + @staticmethod + def _lab2xyz(lab,ref_white=None): + """ + CIE Lab to CIE Xyz. + + References + ---------- + http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + + """ + f_x = (lab[0]+16.)/116. + lab[1]/500. + f_z = (lab[0]+16.)/116. - lab[2]/200. + + return np.array([ + f_x**3. if f_x**3. > _eps else (116.*f_x-16.)/_kappa, + ((lab[0]+16.)/116.)**3 if lab[0]>_kappa*_eps else lab[0]/_kappa, + f_z**3. if f_z**3. > _eps else (116.*f_z-16.)/_kappa + ])*(ref_white if ref_white is not None else _ref_white) + + @staticmethod + def _xyz2lab(xyz,ref_white=None): + """ + CIE Xyz to CIE Lab. + + References + ---------- + http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + + """ + ref_white = ref_white if ref_white is not None else _ref_white + f = np.where(xyz/ref_white > _eps,(xyz/ref_white)**(1./3.),(_kappa*xyz/ref_white+16.)/116.) + + return np.array([ + 116.0 * f[1] - 16.0, + 500.0 * (f[0] - f[1]), + 200.0 * (f[1] - f[2]) + ]) + + + @staticmethod + def _lab2msh(lab): + """ + CIE Lab to Msh. + + References + ---------- + https://www.kennethmoreland.com/color-maps/ColorMapsExpanded.pdf + https://www.kennethmoreland.com/color-maps/diverging_map.py + + """ + M = np.linalg.norm(lab) + return np.array([ + M, + np.arccos(lab[0]/M) if M>1e-8 else 0., + np.arctan2(lab[2],lab[1]) if M>1e-8 else 0., + ]) + + @staticmethod + def _msh2lab(msh): + """ + Msh to CIE Lab. + + References + ---------- + https://www.kennethmoreland.com/color-maps/ColorMapsExpanded.pdf + https://www.kennethmoreland.com/color-maps/diverging_map.py + + """ + return np.array([ + msh[0] * np.cos(msh[1]), + msh[0] * np.sin(msh[1]) * np.cos(msh[2]), + msh[0] * np.sin(msh[1]) * np.sin(msh[2]) + ]) + + @staticmethod + def _lab2rgb(lab): + return Colormap._xyz2rgb(Colormap._lab2xyz(lab)) + + @staticmethod + def _rgb2lab(rgb): + return Colormap._xyz2lab(Colormap._rgb2xyz(rgb)) + + @staticmethod + def _msh2rgb(msh): + return Colormap._lab2rgb(Colormap._msh2lab(msh)) + + @staticmethod + def _rgb2msh(rgb): + return Colormap._lab2msh(Colormap._rgb2lab(rgb)) + + @staticmethod + def _hsv2msh(hsv): + return Colormap._rgb2msh(Colormap._hsv2rgb(hsv)) + + @staticmethod + def _hsl2msh(hsl): + return Colormap._rgb2msh(Colormap._hsl2rgb(hsl)) + + @staticmethod + def _xyz2msh(xyz): + return Colormap._lab2msh(Colormap._xyz2lab(xyz)) diff --git a/python/damask/_colormaps.py b/python/damask/_colormaps.py deleted file mode 100644 index 6c6f82604..000000000 --- a/python/damask/_colormaps.py +++ /dev/null @@ -1,541 +0,0 @@ -import numpy as np -from . import util - -class Color: - """Color representation in and conversion between different color-spaces.""" - - __slots__ = [ - 'model', - 'color', - '__dict__', - ] - - - def __init__(self, - model = 'RGB', - color = np.zeros(3)): - """ - Create a Color object. - - Parameters - ---------- - model : string - color model - color : numpy.ndarray - vector representing the color according to the selected model - - """ - self.__transforms__ = \ - {'HSV': {'index': 0, 'next': self._HSV2HSL}, - 'HSL': {'index': 1, 'next': self._HSL2RGB, 'prev': self._HSL2HSV}, - 'RGB': {'index': 2, 'next': self._RGB2XYZ, 'prev': self._RGB2HSL}, - 'XYZ': {'index': 3, 'next': self._XYZ2CIELAB, 'prev': self._XYZ2RGB}, - 'CIELAB': {'index': 4, 'next': self._CIELAB2MSH, 'prev': self._CIELAB2XYZ}, - 'MSH': {'index': 5, 'prev': self._MSH2CIELAB}, - } - - model = model.upper() - if model not in list(self.__transforms__.keys()): model = 'RGB' - if model == 'RGB' and max(color) > 1.0: # are we RGB255 ? - for i in range(3): - color[i] /= 255.0 # rescale to RGB - - if model == 'HSL': # are we HSL ? - if abs(color[0]) > 1.0: color[0] /= 360.0 # with angular hue? - while color[0] >= 1.0: color[0] -= 1.0 # rewind to proper range - while color[0] < 0.0: color[0] += 1.0 # rewind to proper range - - self.model = model - self.color = np.array(color,'d') - - - def __repr__(self): - """Color model and values.""" - return 'Model: %s Color: %s'%(self.model,str(self.color)) - - - def __str__(self): - """Color model and values.""" - return self.__repr__() - - - def convert_to(self,toModel = 'RGB'): - """ - Change the color model permanently. - - Parameters - ---------- - toModel : string - color model - - """ - toModel = toModel.upper() - if toModel not in list(self.__transforms__.keys()): return - - sourcePos = self.__transforms__[self.model]['index'] - targetPos = self.__transforms__[toModel]['index'] - - while sourcePos < targetPos: - self.__transforms__[self.model]['next']() - sourcePos += 1 - - while sourcePos > targetPos: - self.__transforms__[self.model]['prev']() - sourcePos -= 1 - return self - - - def express_as(self,asModel = 'RGB'): - """ - Return the color in a different model. - - Parameters - ---------- - asModel : string - color model - - """ - return self.__class__(self.model,self.color).convert_to(asModel) - - - def _HSV2HSL(self): - """ - Convert H(ue) S(aturation) V(alue or brightness) to H(ue) S(aturation) L(uminance). - - All values are in the range [0,1] - http://codeitdown.com/hsl-hsb-hsv-color - """ - if self.model != 'HSV': return - - converted = Color('HSL',np.array([ - self.color[0], - 1. if self.color[2] == 0.0 or (self.color[1] == 0.0 and self.color[2] == 1.0) \ - else self.color[1]*self.color[2]/(1.-abs(self.color[2]*(2.-self.color[1])-1.)), - 0.5*self.color[2]*(2.-self.color[1]), - ])) - - self.model = converted.model - self.color = converted.color - - - def _HSL2HSV(self): - """ - Convert H(ue) S(aturation) L(uminance) to H(ue) S(aturation) V(alue or brightness). - - All values are in the range [0,1] - http://codeitdown.com/hsl-hsb-hsv-color - """ - if self.model != 'HSL': return - - h = self.color[0] - b = self.color[2]+0.5*(self.color[1]*(1.-abs(2*self.color[2]-1))) - s = 1.0 if b == 0.0 else 2.*(b-self.color[2])/b - - converted = Color('HSV',np.array([h,s,b])) - - self.model = converted.model - self.color = converted.color - - - def _HSL2RGB(self): - """ - Convert H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue). - - All values are in the range [0,1] - from http://en.wikipedia.org/wiki/HSL_and_HSV - """ - if self.model != 'HSL': return - - sextant = self.color[0]*6.0 - c = (1.0 - abs(2.0 * self.color[2] - 1.0))*self.color[1] - x = c*(1.0 - abs(sextant%2 - 1.0)) - m = self.color[2] - 0.5*c - - converted = Color('RGB',np.array([ - [c+m, x+m, m], - [x+m, c+m, m], - [m, c+m, x+m], - [m, x+m, c+m], - [x+m, m, c+m], - [c+m, m, x+m], - ][int(sextant)])) - self.model = converted.model - self.color = converted.color - - - def _RGB2HSL(self): - """ - Convert R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance). - - All values are in the range [0,1] - from http://130.113.54.154/~monger/hsl-rgb.html - """ - if self.model != 'RGB': return - - HSL = np.zeros(3) - maxcolor = self.color.max() - mincolor = self.color.min() - HSL[2] = (maxcolor + mincolor)/2.0 - if(mincolor == maxcolor): - HSL[0] = 0.0 - HSL[1] = 0.0 - else: - if (HSL[2]<0.5): - HSL[1] = (maxcolor - mincolor)/(maxcolor + mincolor) - else: - HSL[1] = (maxcolor - mincolor)/(2.0 - maxcolor - mincolor) - if (maxcolor == self.color[0]): - HSL[0] = 0.0 + (self.color[1] - self.color[2])/(maxcolor - mincolor) - elif (maxcolor == self.color[1]): - HSL[0] = 2.0 + (self.color[2] - self.color[0])/(maxcolor - mincolor) - elif (maxcolor == self.color[2]): - HSL[0] = 4.0 + (self.color[0] - self.color[1])/(maxcolor - mincolor) - HSL[0] = HSL[0]*60.0 # scaling to 360 might be dangerous for small values - if (HSL[0] < 0.0): - HSL[0] = HSL[0] + 360.0 - HSL[1:] = np.clip(HSL[1:],0.0,1.0) - - converted = Color('HSL', HSL) - self.model = converted.model - self.color = converted.color - - - def _RGB2XYZ(self): - """ - Convert R(ed) G(reen) B(lue) to CIE XYZ. - - All values are in the range [0,1] - from http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'RGB': return - - XYZ = np.zeros(3) - RGB_lin = np.zeros(3) - convert = np.array([[0.412453,0.357580,0.180423], - [0.212671,0.715160,0.072169], - [0.019334,0.119193,0.950227]]) - - for i in range(3): - if (self.color[i] > 0.04045): RGB_lin[i] = ((self.color[i]+0.0555)/1.0555)**2.4 - else: RGB_lin[i] = self.color[i] /12.92 - XYZ = np.dot(convert,RGB_lin) - XYZ = np.clip(XYZ,0.0,None) - - converted = Color('XYZ', XYZ) - self.model = converted.model - self.color = converted.color - - - def _XYZ2RGB(self): - """ - Convert CIE XYZ to R(ed) G(reen) B(lue). - - All values are in the range [0,1] - from http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'XYZ': return - - convert = np.array([[ 3.240479,-1.537150,-0.498535], - [-0.969256, 1.875992, 0.041556], - [ 0.055648,-0.204043, 1.057311]]) - RGB_lin = np.dot(convert,self.color) - RGB = np.zeros(3) - - for i in range(3): - if (RGB_lin[i] > 0.0031308): RGB[i] = ((RGB_lin[i])**(1.0/2.4))*1.0555-0.0555 - else: RGB[i] = RGB_lin[i] *12.92 - - RGB = np.clip(RGB,0.0,1.0) - - maxVal = max(RGB) # clipping colors according to the display gamut - if (maxVal > 1.0): RGB /= maxVal - - converted = Color('RGB', RGB) - self.model = converted.model - self.color = converted.color - - - def _CIELAB2XYZ(self): - """ - Convert CIE Lab to CIE XYZ. - - All values are in the range [0,1] - from http://www.easyrgb.com/index.php?X=MATH&H=07#text7 - """ - if self.model != 'CIELAB': return - - ref_white = np.array([.95047, 1.00000, 1.08883]) # Observer = 2, Illuminant = D65 - XYZ = np.zeros(3) - - XYZ[1] = (self.color[0] + 16.0 ) / 116.0 - XYZ[0] = XYZ[1] + self.color[1]/ 500.0 - XYZ[2] = XYZ[1] - self.color[2]/ 200.0 - - for i in range(len(XYZ)): - if (XYZ[i] > 6./29. ): XYZ[i] = XYZ[i]**3. - else: XYZ[i] = 108./841. * (XYZ[i] - 4./29.) - - converted = Color('XYZ', XYZ*ref_white) - self.model = converted.model - self.color = converted.color - - - def _XYZ2CIELAB(self): - """ - Convert CIE XYZ to CIE Lab. - - All values are in the range [0,1] - from http://en.wikipedia.org/wiki/Lab_color_space, - http://www.cs.rit.edu/~ncs/color/t_convert.html - """ - if self.model != 'XYZ': return - - ref_white = np.array([.95047, 1.00000, 1.08883]) # Observer = 2, Illuminant = D65 - XYZ = self.color/ref_white - - for i in range(len(XYZ)): - if (XYZ[i] > 216./24389 ): XYZ[i] = XYZ[i]**(1.0/3.0) - else: XYZ[i] = (841./108. * XYZ[i]) + 16.0/116.0 - - converted = Color('CIELAB', np.array([ 116.0 * XYZ[1] - 16.0, - 500.0 * (XYZ[0] - XYZ[1]), - 200.0 * (XYZ[1] - XYZ[2]) ])) - self.model = converted.model - self.color = converted.color - - - def _CIELAB2MSH(self): - """ - Convert CIE Lab to Msh colorspace. - - from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls - """ - if self.model != 'CIELAB': return - - Msh = np.zeros(3) - Msh[0] = np.sqrt(np.dot(self.color,self.color)) - if (Msh[0] > 0.001): - Msh[1] = np.arccos(self.color[0]/Msh[0]) - if (self.color[1] != 0.0): - Msh[2] = np.arctan2(self.color[2],self.color[1]) - - converted = Color('MSH', Msh) - self.model = converted.model - self.color = converted.color - - - def _MSH2CIELAB(self): - """ - Convert Msh colorspace to CIE Lab. - - with s,h in radians - from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls - """ - if self.model != 'MSH': return - - Lab = np.zeros(3) - Lab[0] = self.color[0] * np.cos(self.color[1]) - Lab[1] = self.color[0] * np.sin(self.color[1]) * np.cos(self.color[2]) - Lab[2] = self.color[0] * np.sin(self.color[1]) * np.sin(self.color[2]) - - converted = Color('CIELAB', Lab) - self.model = converted.model - self.color = converted.color - - -class Colormap: - """Perceptually uniform diverging or sequential colormap.""" - - __slots__ = [ - 'left', - 'right', - 'interpolate', - ] - __predefined__ = { - 'gray': {'left': Color('HSL',[0,1,1]), - 'right': Color('HSL',[0,0,0.15]), - 'interpolate': 'perceptualuniform'}, - 'grey': {'left': Color('HSL',[0,1,1]), - 'right': Color('HSL',[0,0,0.15]), - 'interpolate': 'perceptualuniform'}, - 'red': {'left': Color('HSL',[0,1,0.14]), - 'right': Color('HSL',[0,0.35,0.91]), - 'interpolate': 'perceptualuniform'}, - 'green': {'left': Color('HSL',[0.33333,1,0.14]), - 'right': Color('HSL',[0.33333,0.35,0.91]), - 'interpolate': 'perceptualuniform'}, - 'blue': {'left': Color('HSL',[0.66,1,0.14]), - 'right': Color('HSL',[0.66,0.35,0.91]), - 'interpolate': 'perceptualuniform'}, - 'seaweed': {'left': Color('HSL',[0.78,1.0,0.1]), - 'right': Color('HSL',[0.40000,0.1,0.9]), - 'interpolate': 'perceptualuniform'}, - 'bluebrown': {'left': Color('HSL',[0.65,0.53,0.49]), - 'right': Color('HSL',[0.11,0.75,0.38]), - 'interpolate': 'perceptualuniform'}, - 'redgreen': {'left': Color('HSL',[0.97,0.96,0.36]), - 'right': Color('HSL',[0.33333,1.0,0.14]), - 'interpolate': 'perceptualuniform'}, - 'bluered': {'left': Color('HSL',[0.65,0.53,0.49]), - 'right': Color('HSL',[0.97,0.96,0.36]), - 'interpolate': 'perceptualuniform'}, - 'blueredrainbow':{'left': Color('HSL',[2.0/3.0,1,0.5]), - 'right': Color('HSL',[0,1,0.5]), - 'interpolate': 'linear' }, - 'orientation': {'left': Color('RGB',[0.933334,0.878432,0.878431]), - 'right': Color('RGB',[0.250980,0.007843,0.000000]), - 'interpolate': 'perceptualuniform'}, - 'strain': {'left': Color('RGB',[0.941177,0.941177,0.870588]), - 'right': Color('RGB',[0.266667,0.266667,0.000000]), - 'interpolate': 'perceptualuniform'}, - 'stress': {'left': Color('RGB',[0.878432,0.874511,0.949019]), - 'right': Color('RGB',[0.000002,0.000000,0.286275]), - 'interpolate': 'perceptualuniform'}, - } - - - def __init__(self, - left = Color('RGB',[1,1,1]), - right = Color('RGB',[0,0,0]), - interpolate = 'perceptualuniform', - predefined = None - ): - """ - Create a Colormap object. - - Parameters - ---------- - left : Color - left color (minimum value) - right : Color - right color (maximum value) - interpolate : str - interpolation scheme (either 'perceptualuniform' or 'linear') - predefined : bool - ignore other arguments and use predefined definition - - """ - if predefined is not None: - left = self.__predefined__[predefined.lower()]['left'] - right= self.__predefined__[predefined.lower()]['right'] - interpolate = self.__predefined__[predefined.lower()]['interpolate'] - - if left.__class__.__name__ != 'Color': - left = Color() - if right.__class__.__name__ != 'Color': - right = Color() - - self.left = left - self.right = right - self.interpolate = interpolate - - - def __repr__(self): - """Left and right value of colormap.""" - return 'Left: %s Right: %s'%(self.left,self.right) - - - def invert(self): - """Switch left/minimum with right/maximum.""" - (self.left, self.right) = (self.right, self.left) - return self - - - def show_predefined(self): - """Show the labels of the predefined colormaps.""" - print('\n'.join(self.__predefined__.keys())) - - def color(self,fraction = 0.5): - - def interpolate_Msh(lo, hi, frac): - - def rad_diff(a,b): - return abs(a[2]-b[2]) - - def adjust_hue(Msh_sat, Msh_unsat): - """If saturation of one of the two colors is too less than the other, hue of the less.""" - if Msh_sat[0] >= Msh_unsat[0]: - return Msh_sat[2] - else: - hSpin = Msh_sat[1]/np.sin(Msh_sat[1])*np.sqrt(Msh_unsat[0]**2.0-Msh_sat[0]**2)/Msh_sat[0] - if Msh_sat[2] < - np.pi/3.0: hSpin *= -1.0 - return Msh_sat[2] + hSpin - - Msh1 = np.array(lo[:]) - Msh2 = np.array(hi[:]) - - if (Msh1[1] > 0.05 and Msh2[1] > 0.05 and rad_diff(Msh1,Msh2) > np.pi/3.0): - M_mid = max(Msh1[0],Msh2[0],88.0) - if frac < 0.5: - Msh2 = np.array([M_mid,0.0,0.0]) - frac *= 2.0 - else: - Msh1 = np.array([M_mid,0.0,0.0]) - frac = 2.0*frac - 1.0 - if Msh1[1] < 0.05 and Msh2[1] > 0.05: Msh1[2] = adjust_hue(Msh2,Msh1) - elif Msh1[1] > 0.05 and Msh2[1] < 0.05: Msh2[2] = adjust_hue(Msh1,Msh2) - Msh = (1.0 - frac) * Msh1 + frac * Msh2 - - return Color('MSH',Msh) - - def interpolate_linear(lo, hi, frac): - """Linear interpolation between lo and hi color at given fraction; output in model of lo color.""" - interpolation = (1.0 - frac) * np.array(lo.color[:]) \ - + frac * np.array(hi.express_as(lo.model).color[:]) - - return Color(lo.model,interpolation) - - if self.interpolate == 'perceptualuniform': - return interpolate_Msh(self.left.express_as('MSH').color, - self.right.express_as('MSH').color,fraction) - elif self.interpolate == 'linear': - return interpolate_linear(self.left,self.right,fraction) - else: - raise NameError('unknown color interpolation method') - - - def export(self,name = 'uniformPerceptualColorMap',\ - format = 'paraview',\ - steps = 2,\ - crop = [-1.0,1.0], - model = 'RGB'): - """ - [RGB] colormap for use in paraview or gmsh, or as raw string, or array. - - Arguments: name, format, steps, crop. - Format is one of (paraview, gmsh, gom, raw, list). - Crop selects a (sub)range in [-1.0,1.0]. - Generates sequential map if one limiting color is either white or black, - diverging map otherwise. - """ - format = format.lower() # consistent comparison basis - frac = 0.5*(np.array(crop) + 1.0) # rescale crop range to fractions - colors = [self.color(float(i)/(steps-1)*(frac[1]-frac[0])+frac[0]).express_as(model).color for i in range(steps)] - if format == 'paraview': - colormap = [f'[\n {{\n "ColorSpace": "RGB", "Name": "{name}", "DefaultMap": true,\n "RGBPoints" : ['] \ - + [f' {i:4d},{color[0]:8.6f},{color[1]:8.6f},{color[2]:8.6f}{"," if i+1