add HSV support

This commit is contained in:
Chen 2017-08-28 15:39:35 -04:00
parent adc7c9d5b1
commit a9f4213952
1 changed files with 136 additions and 94 deletions

View File

@ -9,28 +9,29 @@ class Color():
Conversion of colors between different color-spaces. Conversion of colors between different color-spaces.
Colors should be given in the form Color('model',[vector]). Colors should be given in the form Color('model',[vector]).
To convert or copy color from one space to other, use the methods To convert or copy color from one space to other, use the methods
convertTo('model') or expressAs('model'), respectively. convertTo('model') or expressAs('model'), respectively.
""" """
__slots__ = [ __slots__ = [
'model', 'model',
'color', 'color',
'__dict__', '__dict__',
] ]
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def __init__(self, def __init__(self,
model = 'RGB', model = 'RGB',
color = np.zeros(3,'d')): color = np.zeros(3,'d')):
self.__transforms__ = \ self.__transforms__ = \
{'HSL': {'index': 0, 'next': self._HSL2RGB}, {'HSV': {'index': 0, 'next': self._HSV2HSL},
'RGB': {'index': 1, 'next': self._RGB2XYZ, 'prev': self._RGB2HSL}, 'HSL': {'index': 1, 'next': self._HSL2RGB, 'prev': self._HSL2HSV},
'XYZ': {'index': 2, 'next': self._XYZ2CIELAB, 'prev': self._XYZ2RGB}, 'RGB': {'index': 2, 'next': self._RGB2XYZ, 'prev': self._RGB2HSL},
'CIELAB': {'index': 3, 'next': self._CIELAB2MSH, 'prev': self._CIELAB2XYZ}, 'XYZ': {'index': 3, 'next': self._XYZ2CIELAB, 'prev': self._XYZ2RGB},
'MSH': {'index': 4, 'prev': self._MSH2CIELAB}, 'CIELAB': {'index': 4, 'next': self._CIELAB2MSH, 'prev': self._CIELAB2XYZ},
'MSH': {'index': 5, 'prev': self._MSH2CIELAB},
} }
model = model.upper() model = model.upper()
@ -46,24 +47,24 @@ class Color():
self.model = model self.model = model
self.color = np.array(color,'d') self.color = np.array(color,'d')
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def __repr__(self): def __repr__(self):
"""Color model and values""" """Color model and values"""
return 'Model: %s Color: %s'%(self.model,str(self.color)) return 'Model: %s Color: %s'%(self.model,str(self.color))
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def __str__(self): def __str__(self):
"""Color model and values""" """Color model and values"""
return self.__repr__() return self.__repr__()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def convertTo(self,toModel = 'RGB'): def convertTo(self,toModel = 'RGB'):
toModel = toModel.upper() toModel = toModel.upper()
if toModel not in list(self.__transforms__.keys()): return if toModel not in list(self.__transforms__.keys()): return
sourcePos = self.__transforms__[self.model]['index'] sourcePos = self.__transforms__[self.model]['index']
targetPos = self.__transforms__[toModel]['index'] targetPos = self.__transforms__[toModel]['index']
@ -76,23 +77,64 @@ class Color():
self.__transforms__[self.model]['prev']() self.__transforms__[self.model]['prev']()
sourcePos -= 1 sourcePos -= 1
return self return self
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def expressAs(self,asModel = 'RGB'): def expressAs(self,asModel = 'RGB'):
return self.__class__(self.model,self.color).convertTo(asModel) return self.__class__(self.model,self.color).convertTo(asModel)
def _HSV2HSL(self):
"""
Convert H(ue) S(aturation) V(alue or brightness) to H(ue) S(aturation) L(uminance)
with all values in the range of 0 to 1
http://codeitdown.com/hsl-hsb-hsv-color/
"""
if self.model != 'HSV': return
print "HSV: ",self.color
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)
with all values in the range of 0 to 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): def _HSL2RGB(self):
""" """
Convert H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue) Convert H(ue) S(aturation) L(uminance) to R(red) G(reen) B(lue)
with all values in the range of 0 to 1 with all values in the range of 0 to 1
from http://en.wikipedia.org/wiki/HSL_and_HSV from http://en.wikipedia.org/wiki/HSL_and_HSV
""" """
if self.model != 'HSL': return if self.model != 'HSL': return
sextant = self.color[0]*6.0 sextant = self.color[0]*6.0
c = (1.0 - abs(2.0 * self.color[2] - 1.0))*self.color[1] c = (1.0 - abs(2.0 * self.color[2] - 1.0))*self.color[1]
x = c*(1.0 - abs(sextant%2 - 1.0)) x = c*(1.0 - abs(sextant%2 - 1.0))
@ -108,15 +150,15 @@ class Color():
][int(sextant)],'d')) ][int(sextant)],'d'))
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
def _RGB2HSL(self): def _RGB2HSL(self):
""" """
Convert R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance) Convert R(ed) G(reen) B(lue) to H(ue) S(aturation) L(uminance)
with all values in the range of 0 to 1 with all values in the range of 0 to 1
from http://130.113.54.154/~monger/hsl-rgb.html from http://130.113.54.154/~monger/hsl-rgb.html
""" """
if self.model != 'RGB': return if self.model != 'RGB': return
HSL = np.zeros(3,'d') HSL = np.zeros(3,'d')
@ -141,43 +183,43 @@ class Color():
if (HSL[0] < 0.0): if (HSL[0] < 0.0):
HSL[0] = HSL[0] + 360.0 HSL[0] = HSL[0] + 360.0
for i in range(2): for i in range(2):
HSL[i+1] = min(HSL[i+1],1.0) HSL[i+1] = min(HSL[i+1],1.0)
HSL[i+1] = max(HSL[i+1],0.0) HSL[i+1] = max(HSL[i+1],0.0)
converted = Color('HSL', HSL) converted = Color('HSL', HSL)
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
def _RGB2XYZ(self): def _RGB2XYZ(self):
""" """
Convert R(ed) G(reen) B(lue) to CIE XYZ Convert R(ed) G(reen) B(lue) to CIE XYZ
with all values in the range of 0 to 1 with all values in the range of 0 to 1
from http://www.cs.rit.edu/~ncs/color/t_convert.html from http://www.cs.rit.edu/~ncs/color/t_convert.html
""" """
if self.model != 'RGB': return if self.model != 'RGB': return
XYZ = np.zeros(3,'d') XYZ = np.zeros(3,'d')
RGB_lin = np.zeros(3,'d') RGB_lin = np.zeros(3,'d')
convert = np.array([[0.412453,0.357580,0.180423], convert = np.array([[0.412453,0.357580,0.180423],
[0.212671,0.715160,0.072169], [0.212671,0.715160,0.072169],
[0.019334,0.119193,0.950227]]) [0.019334,0.119193,0.950227]])
for i in range(3): for i in range(3):
if (self.color[i] > 0.04045): RGB_lin[i] = ((self.color[i]+0.0555)/1.0555)**2.4 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 else: RGB_lin[i] = self.color[i] /12.92
XYZ = np.dot(convert,RGB_lin) XYZ = np.dot(convert,RGB_lin)
for i in range(3): for i in range(3):
XYZ[i] = max(XYZ[i],0.0)
converted = Color('XYZ', XYZ) XYZ[i] = max(XYZ[i],0.0)
converted = Color('XYZ', XYZ)
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
def _XYZ2RGB(self): def _XYZ2RGB(self):
""" """
@ -199,17 +241,17 @@ class Color():
if (RGB_lin[i] > 0.0031308): RGB[i] = ((RGB_lin[i])**(1.0/2.4))*1.0555-0.0555 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 else: RGB[i] = RGB_lin[i] *12.92
for i in range(3): for i in range(3):
RGB[i] = min(RGB[i],1.0) RGB[i] = min(RGB[i],1.0)
RGB[i] = max(RGB[i],0.0) RGB[i] = max(RGB[i],0.0)
maxVal = max(RGB) # clipping colors according to the display gamut maxVal = max(RGB) # clipping colors according to the display gamut
if (maxVal > 1.0): RGB /= maxVal if (maxVal > 1.0): RGB /= maxVal
converted = Color('RGB', RGB) converted = Color('RGB', RGB)
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
def _CIELAB2XYZ(self): def _CIELAB2XYZ(self):
""" """
@ -219,19 +261,19 @@ class Color():
from http://www.easyrgb.com/index.php?X=MATH&H=07#text7 from http://www.easyrgb.com/index.php?X=MATH&H=07#text7
""" """
if self.model != 'CIELAB': return if self.model != 'CIELAB': return
ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65
XYZ = np.zeros(3,'d') XYZ = np.zeros(3,'d')
XYZ[1] = (self.color[0] + 16.0 ) / 116.0 XYZ[1] = (self.color[0] + 16.0 ) / 116.0
XYZ[0] = XYZ[1] + self.color[1]/ 500.0 XYZ[0] = XYZ[1] + self.color[1]/ 500.0
XYZ[2] = XYZ[1] - self.color[2]/ 200.0 XYZ[2] = XYZ[1] - self.color[2]/ 200.0
for i in range(len(XYZ)): for i in range(len(XYZ)):
if (XYZ[i] > 6./29. ): XYZ[i] = XYZ[i]**3. if (XYZ[i] > 6./29. ): XYZ[i] = XYZ[i]**3.
else: XYZ[i] = 108./841. * (XYZ[i] - 4./29.) else: XYZ[i] = 108./841. * (XYZ[i] - 4./29.)
converted = Color('XYZ', XYZ*ref_white) converted = Color('XYZ', XYZ*ref_white)
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
@ -244,30 +286,30 @@ class Color():
http://www.cs.rit.edu/~ncs/color/t_convert.html http://www.cs.rit.edu/~ncs/color/t_convert.html
""" """
if self.model != 'XYZ': return if self.model != 'XYZ': return
ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65 ref_white = np.array([.95047, 1.00000, 1.08883],'d') # Observer = 2, Illuminant = D65
XYZ = self.color/ref_white XYZ = self.color/ref_white
for i in range(len(XYZ)): for i in range(len(XYZ)):
if (XYZ[i] > 216./24389 ): XYZ[i] = XYZ[i]**(1.0/3.0) if (XYZ[i] > 216./24389 ): XYZ[i] = XYZ[i]**(1.0/3.0)
else: XYZ[i] = (841./108. * XYZ[i]) + 16.0/116.0 else: XYZ[i] = (841./108. * XYZ[i]) + 16.0/116.0
converted = Color('CIELAB', np.array([ 116.0 * XYZ[1] - 16.0, converted = Color('CIELAB', np.array([ 116.0 * XYZ[1] - 16.0,
500.0 * (XYZ[0] - XYZ[1]), 500.0 * (XYZ[0] - XYZ[1]),
200.0 * (XYZ[1] - XYZ[2]) ])) 200.0 * (XYZ[1] - XYZ[2]) ]))
self.model = converted.model self.model = converted.model
self.color = converted.color self.color = converted.color
def _CIELAB2MSH(self): def _CIELAB2MSH(self):
""" """
Convert CIE Lab to Msh colorspace Convert CIE Lab to Msh colorspace
from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls
""" """
if self.model != 'CIELAB': return if self.model != 'CIELAB': return
Msh = np.zeros(3,'d') Msh = np.zeros(3,'d')
Msh[0] = math.sqrt(np.dot(self.color,self.color)) Msh[0] = math.sqrt(np.dot(self.color,self.color))
if (Msh[0] > 0.001): if (Msh[0] > 0.001):
Msh[1] = math.acos(self.color[0]/Msh[0]) Msh[1] = math.acos(self.color[0]/Msh[0])
@ -287,8 +329,8 @@ class Color():
from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls from http://www.cs.unm.edu/~kmorel/documents/ColorMaps/DivergingColorMapWorkshop.xls
""" """
if self.model != 'MSH': return if self.model != 'MSH': return
Lab = np.zeros(3,'d') Lab = np.zeros(3,'d')
Lab[0] = self.color[0] * math.cos(self.color[1]) Lab[0] = self.color[0] * math.cos(self.color[1])
Lab[1] = self.color[0] * math.sin(self.color[1]) * math.cos(self.color[2]) Lab[1] = self.color[0] * math.sin(self.color[1]) * math.cos(self.color[2])
Lab[2] = self.color[0] * math.sin(self.color[1]) * math.sin(self.color[2]) Lab[2] = self.color[0] * math.sin(self.color[1]) * math.sin(self.color[2])
@ -305,7 +347,7 @@ class Colormap():
'left', 'left',
'right', 'right',
'interpolate', 'interpolate',
] ]
__predefined__ = { __predefined__ = {
'gray': {'left': Color('HSL',[0,1,1]), 'gray': {'left': Color('HSL',[0,1,1]),
'right': Color('HSL',[0,0,0.15]), 'right': Color('HSL',[0,0,0.15]),
@ -329,7 +371,7 @@ class Colormap():
'right': Color('HSL',[0.11,0.75,0.38]), 'right': Color('HSL',[0.11,0.75,0.38]),
'interpolate': 'perceptualuniform'}, 'interpolate': 'perceptualuniform'},
'redgreen': {'left': Color('HSL',[0.97,0.96,0.36]), 'redgreen': {'left': Color('HSL',[0.97,0.96,0.36]),
'right': Color('HSL',[0.33333,1.0,0.14]), 'right': Color('HSL',[0.33333,1.0,0.14]),
'interpolate': 'perceptualuniform'}, 'interpolate': 'perceptualuniform'},
'bluered': {'left': Color('HSL',[0.65,0.53,0.49]), 'bluered': {'left': Color('HSL',[0.65,0.53,0.49]),
'right': Color('HSL',[0.97,0.96,0.36]), 'right': Color('HSL',[0.97,0.96,0.36]),
@ -347,8 +389,8 @@ class Colormap():
'right': Color('RGB',[0.000002,0.000000,0.286275]), 'right': Color('RGB',[0.000002,0.000000,0.286275]),
'interpolate': 'perceptualuniform'}, 'interpolate': 'perceptualuniform'},
} }
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def __init__(self, def __init__(self,
left = Color('RGB',[1,1,1]), left = Color('RGB',[1,1,1]),
@ -366,32 +408,32 @@ class Colormap():
left = Color() left = Color()
if right.__class__.__name__ != 'Color': if right.__class__.__name__ != 'Color':
right = Color() right = Color()
self.left = left self.left = left
self.right = right self.right = right
self.interpolate = interpolate self.interpolate = interpolate
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def __repr__(self): def __repr__(self):
"""Left and right value of colormap""" """Left and right value of colormap"""
return 'Left: %s Right: %s'%(self.left,self.right) return 'Left: %s Right: %s'%(self.left,self.right)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def invert(self): def invert(self):
(self.left, self.right) = (self.right, self.left) (self.left, self.right) = (self.right, self.left)
return self return self
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def color(self,fraction = 0.5): def color(self,fraction = 0.5):
def interpolate_Msh(lo, hi, frac): def interpolate_Msh(lo, hi, frac):
def rad_diff(a,b): def rad_diff(a,b):
return abs(a[2]-b[2]) return abs(a[2]-b[2])
# if saturation of one of the two colors is too less than the other, hue of the less # if saturation of one of the two colors is too less than the other, hue of the less
def adjust_hue(Msh_sat, Msh_unsat): def adjust_hue(Msh_sat, Msh_unsat):
if Msh_sat[0] >= Msh_unsat[0]: if Msh_sat[0] >= Msh_unsat[0]:
return Msh_sat[2] return Msh_sat[2]
@ -399,11 +441,11 @@ class Colormap():
hSpin = Msh_sat[1]/math.sin(Msh_sat[1])*math.sqrt(Msh_unsat[0]**2.0-Msh_sat[0]**2)/Msh_sat[0] hSpin = Msh_sat[1]/math.sin(Msh_sat[1])*math.sqrt(Msh_unsat[0]**2.0-Msh_sat[0]**2)/Msh_sat[0]
if Msh_sat[2] < - math.pi/3.0: hSpin *= -1.0 if Msh_sat[2] < - math.pi/3.0: hSpin *= -1.0
return Msh_sat[2] + hSpin return Msh_sat[2] + hSpin
Msh1 = np.array(lo[:]) Msh1 = np.array(lo[:])
Msh2 = np.array(hi[:]) Msh2 = np.array(hi[:])
if (Msh1[1] > 0.05 and Msh2[1] > 0.05 and rad_diff(Msh1,Msh2) > math.pi/3.0): if (Msh1[1] > 0.05 and Msh2[1] > 0.05 and rad_diff(Msh1,Msh2) > math.pi/3.0):
M_mid = max(Msh1[0],Msh2[0],88.0) M_mid = max(Msh1[0],Msh2[0],88.0)
if frac < 0.5: if frac < 0.5:
Msh2 = np.array([M_mid,0.0,0.0],'d') Msh2 = np.array([M_mid,0.0,0.0],'d')
@ -414,16 +456,16 @@ class Colormap():
if Msh1[1] < 0.05 and Msh2[1] > 0.05: Msh1[2] = adjust_hue(Msh2,Msh1) 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) elif Msh1[1] > 0.05 and Msh2[1] < 0.05: Msh2[2] = adjust_hue(Msh1,Msh2)
Msh = (1.0 - frac) * Msh1 + frac * Msh2 Msh = (1.0 - frac) * Msh1 + frac * Msh2
return Color('MSH',Msh) return Color('MSH',Msh)
def interpolate_linear(lo, hi, frac): def interpolate_linear(lo, hi, frac):
"""Linear interpolation between lo and hi color at given fraction; output in model of lo color.""" """Linear interpolation between lo and hi color at given fraction; output in model of lo color."""
interpolation = (1.0 - frac) * np.array(lo.color[:]) \ interpolation = (1.0 - frac) * np.array(lo.color[:]) \
+ frac * np.array(hi.expressAs(lo.model).color[:]) + frac * np.array(hi.expressAs(lo.model).color[:])
return Color(lo.model,interpolation) return Color(lo.model,interpolation)
if self.interpolate == 'perceptualuniform': if self.interpolate == 'perceptualuniform':
return interpolate_Msh(self.left.expressAs('MSH').color, return interpolate_Msh(self.left.expressAs('MSH').color,
self.right.expressAs('MSH').color,fraction) self.right.expressAs('MSH').color,fraction)
@ -432,8 +474,8 @@ class Colormap():
self.right,fraction) self.right,fraction)
else: else:
raise NameError('unknown color interpolation method') raise NameError('unknown color interpolation method')
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def export(self,name = 'uniformPerceptualColorMap',\ def export(self,name = 'uniformPerceptualColorMap',\
format = 'paraview',\ format = 'paraview',\
steps = 2,\ steps = 2,\
@ -461,21 +503,21 @@ class Colormap():
colormap = ['View.ColorTable = {'] \ colormap = ['View.ColorTable = {'] \
+ [',\n'.join(['{%s}'%(','.join([str(x*255.0) for x in color])) for color in colors])] \ + [',\n'.join(['{%s}'%(','.join([str(x*255.0) for x in color])) for color in colors])] \
+ ['}'] + ['}']
elif format == 'gom': elif format == 'gom':
colormap = ['1 1 ' + str(name) \ colormap = ['1 1 ' + str(name) \
+ ' 9 ' + str(name) \ + ' 9 ' + str(name) \
+ ' 0 1 0 3 0 0 -1 9 \ 0 0 0 255 255 255 0 0 255 ' \ + ' 0 1 0 3 0 0 -1 9 \ 0 0 0 255 255 255 0 0 255 ' \
+ '30 NO_UNIT 1 1 64 64 64 255 1 0 0 0 0 0 0 3 0 ' + str(len(colors)) \ + '30 NO_UNIT 1 1 64 64 64 255 1 0 0 0 0 0 0 3 0 ' + str(len(colors)) \
+ ' '.join([' 0 %s 255 1'%(' '.join([str(int(x*255.0)) for x in color])) for color in reversed(colors)])] + ' '.join([' 0 %s 255 1'%(' '.join([str(int(x*255.0)) for x in color])) for color in reversed(colors)])]
elif format == 'raw': elif format == 'raw':
colormap = ['\t'.join(map(str,color)) for color in colors] colormap = ['\t'.join(map(str,color)) for color in colors]
elif format == 'list': elif format == 'list':
colormap = colors colormap = colors
else: else:
raise NameError('unknown color export format') raise NameError('unknown color export format')
return '\n'.join(colormap) + '\n' if type(colormap[0]) is str else colormap return '\n'.join(colormap) + '\n' if type(colormap[0]) is str else colormap