colourspace-explorer 1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- colourspace/__init__.py +12 -0
- colourspace/convert.py +165 -0
- colourspace/gamut/Cmax_res10_full.npy +0 -0
- colourspace/gamut/Cmax_res10_sRGB.npy +0 -0
- colourspace/gamut/Cmax_res1_full.npy +0 -0
- colourspace/gamut/Cmax_res1_sRGB.npy +0 -0
- colourspace/gamut/Cmax_res2_full.npy +0 -0
- colourspace/gamut/Cmax_res2_sRGB.npy +0 -0
- colourspace/gamut.py +367 -0
- colourspace/limits/limits_360-780-10_XYZ.npy +0 -0
- colourspace/limits/limits_360-780-1_XYZ.npy +0 -0
- colourspace/limits/limits_360-780-20_XYZ.npy +0 -0
- colourspace/limits/limits_360-780-5_XYZ.npy +0 -0
- colourspace/limits.py +304 -0
- colourspace/maps.py +607 -0
- colourspace/slices.py +251 -0
- colourspace/tutorials/1. convert.ipynb +613 -0
- colourspace/tutorials/2. sRGB gamut.ipynb +738 -0
- colourspace/tutorials/3. full gamut.ipynb +1294 -0
- colourspace/tutorials/4. slices.ipynb +1162 -0
- colourspace/tutorials/5. cmaps.ipynb +1828 -0
- colourspace/tutorials/5.1 cmaps helix.ipynb +691 -0
- colourspace/tutorials/copy_tutorials.py +38 -0
- colourspace_explorer-1.5.dist-info/LICENSE +29 -0
- colourspace_explorer-1.5.dist-info/METADATA +79 -0
- colourspace_explorer-1.5.dist-info/RECORD +28 -0
- colourspace_explorer-1.5.dist-info/WHEEL +5 -0
- colourspace_explorer-1.5.dist-info/top_level.txt +1 -0
colourspace/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
__author__ = "Gilles Ferrand (U. of Manitoba / RIKEN)"
|
|
4
|
+
__version__ = "1.5"
|
|
5
|
+
|
|
6
|
+
from . import convert
|
|
7
|
+
from . import gamut
|
|
8
|
+
from . import limits
|
|
9
|
+
from . import slices
|
|
10
|
+
from . import maps
|
|
11
|
+
|
|
12
|
+
from .tutorials.copy_tutorials import copy_tutorials
|
colourspace/convert.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Conversion between colour spaces: CIE LCH <-> CIE Lab <-> CIE XYZ <->sRGB
|
|
4
|
+
Can use either:
|
|
5
|
+
- custom formulas taken from http://www.easyrgb.com/en/math.php
|
|
6
|
+
- wrapper to "colorspacious" package from https://pypi.python.org/pypi/colorspacious/
|
|
7
|
+
- wrapper to "colour" package from http://colour-science.org
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import print_function
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
convertor = None
|
|
14
|
+
illuminant = None
|
|
15
|
+
LCH2RGB = lambda L,C,H: [np.nan, np.nan, np.nan]
|
|
16
|
+
RGB2LCH = lambda R,G,B: [np.nan, np.nan, np.nan]
|
|
17
|
+
|
|
18
|
+
def set_convertor(name, ill='D65'):
|
|
19
|
+
""" Binds the conversion functions LCH2RGB() and RGB2LCH() to the choosen colour package
|
|
20
|
+
"""
|
|
21
|
+
global LCH2RGB, RGB2LCH, convertor, illuminant
|
|
22
|
+
if name not in ['custom', 'colorspacious', 'colourscience']:
|
|
23
|
+
print("Unknown conversion module")
|
|
24
|
+
return
|
|
25
|
+
convertor = name
|
|
26
|
+
illuminant = ill
|
|
27
|
+
if name=='custom':
|
|
28
|
+
LCH2RGB = lambda L,C,H: XYZ2RGB(Lab2XYZ(LCH2Lab((L,C,H))))
|
|
29
|
+
RGB2LCH = lambda R,G,B: Lab2LCH(XYZ2Lab(RGB2XYZ((R,G,B))))
|
|
30
|
+
if name=='colorspacious':
|
|
31
|
+
from colorspacious import cspace_convert
|
|
32
|
+
func_LCH2RGB = lambda L,C,H: cspace_convert([L,C,H], {"name": "CIELCh", "XYZ100_w": ill}, "sRGB1")
|
|
33
|
+
func_RGB2LCH = lambda R,G,B: cspace_convert([R,G,B], "sRGB1", {"name": "CIELCh", "XYZ100_w": ill})
|
|
34
|
+
if name=='colourscience':
|
|
35
|
+
import colour
|
|
36
|
+
if hasattr(colour,'ILLUMINANTS' ): cs_ill = colour.ILLUMINANTS ['CIE 1931 2 Degree Standard Observer'][ill]
|
|
37
|
+
if hasattr(colour,'CCS_ILLUMINANTS'): cs_ill = colour.CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer'][ill]
|
|
38
|
+
func_LCH2RGB = lambda L,C,H: colour.XYZ_to_sRGB(colour.Lab_to_XYZ(colour.LCHab_to_Lab([L,C,H]), illuminant=cs_ill))
|
|
39
|
+
func_RGB2LCH = lambda R,G,B: colour.Lab_to_LCHab(colour.XYZ_to_Lab(colour.sRGB_to_XYZ([R,G,B]), illuminant=cs_ill))
|
|
40
|
+
if name=='colorspacious' or name=='colourscience':
|
|
41
|
+
def LCH2RGB(L,C,H):
|
|
42
|
+
if hasattr(L, '__iter__'):
|
|
43
|
+
RGB = np.array(list(map(func_LCH2RGB,L,C,H)))
|
|
44
|
+
R = RGB[:,0]
|
|
45
|
+
G = RGB[:,1]
|
|
46
|
+
B = RGB[:,2]
|
|
47
|
+
else:
|
|
48
|
+
R,G,B = func_LCH2RGB(L,C,H)
|
|
49
|
+
return R, G, B
|
|
50
|
+
def RGB2LCH(R,G,B):
|
|
51
|
+
if hasattr(R, '__iter__'):
|
|
52
|
+
LCH = np.array(list(map(func_RGB2LCH,R,G,B)))
|
|
53
|
+
L = LCH[:,0]
|
|
54
|
+
C = LCH[:,1]
|
|
55
|
+
H = LCH[:,2]
|
|
56
|
+
else:
|
|
57
|
+
L,C,H = func_RGB2LCH(R,G,B)
|
|
58
|
+
return L, C, H
|
|
59
|
+
print("convertor = '%s' (illuminant = '%s')"%(name,illuminant))
|
|
60
|
+
|
|
61
|
+
set_convertor('custom')
|
|
62
|
+
|
|
63
|
+
# cartesian CIE Lab <-> cylindrical CIE LCH
|
|
64
|
+
|
|
65
|
+
def LCH2Lab(LCH):
|
|
66
|
+
L,C,H = LCH
|
|
67
|
+
#L = np.float(L)
|
|
68
|
+
a = C * np.cos(H*2*np.pi/360.)
|
|
69
|
+
b = C * np.sin(H*2*np.pi/360.)
|
|
70
|
+
return (L, a, b)
|
|
71
|
+
|
|
72
|
+
def Lab2LCH(Lab):
|
|
73
|
+
L,a,b = Lab
|
|
74
|
+
#L = np.float(L)
|
|
75
|
+
C = np.sqrt(a**2+b**2)
|
|
76
|
+
H = np.arctan2(b,a) * 360/(2*np.pi)
|
|
77
|
+
H = np.where(H<0, H+360, H)
|
|
78
|
+
return (L, C, H+0.)
|
|
79
|
+
|
|
80
|
+
# perceptual CIE XYZ <-> uniform CIE Lab
|
|
81
|
+
|
|
82
|
+
# standard illuminants for 2° observer
|
|
83
|
+
Xn = {}
|
|
84
|
+
Yn = {}
|
|
85
|
+
Zn = {}
|
|
86
|
+
# D65
|
|
87
|
+
Xn['D65'] = 95.047
|
|
88
|
+
Yn['D65'] = 100.000
|
|
89
|
+
Zn['D65'] = 108.883
|
|
90
|
+
# D50
|
|
91
|
+
Xn['D50'] = 96.422
|
|
92
|
+
Yn['D50'] = 100.000
|
|
93
|
+
Zn['D50'] = 82.521
|
|
94
|
+
|
|
95
|
+
def XYZ2Lab(XYZ):
|
|
96
|
+
X,Y,Z = XYZ
|
|
97
|
+
L = 116 * f_forward(Y/Yn[illuminant]) - 16
|
|
98
|
+
a = 500 * (f_forward(X/Xn[illuminant]) - f_forward(Y/Yn[illuminant]))
|
|
99
|
+
b = 200 * (f_forward(Y/Yn[illuminant]) - f_forward(Z/Zn[illuminant]))
|
|
100
|
+
return (L,a,b)
|
|
101
|
+
|
|
102
|
+
def Lab2XYZ(Lab):
|
|
103
|
+
L,a,b = Lab
|
|
104
|
+
X = Xn[illuminant] * f_reverse((L+16)/116.+a/500.)
|
|
105
|
+
Y = Yn[illuminant] * f_reverse((L+16)/116.)
|
|
106
|
+
Z = Zn[illuminant] * f_reverse((L+16)/116.-b/200.)
|
|
107
|
+
return (X,Y,Z)
|
|
108
|
+
|
|
109
|
+
def f_forward(x):
|
|
110
|
+
return np.where(x > (6/29.)**3, x**(1/3.), 1/3.*(29/6.)**2*x+4/29.)
|
|
111
|
+
|
|
112
|
+
def f_reverse(x):
|
|
113
|
+
return np.where(x > 6/29., x**3, 3*(6/29.)**2*(x-4/29.))
|
|
114
|
+
|
|
115
|
+
# human CIE XYZ <-> machine sRGB
|
|
116
|
+
|
|
117
|
+
RGB_max = 1.
|
|
118
|
+
|
|
119
|
+
def XYZ2RGB(XYZ):
|
|
120
|
+
X,Y,Z = XYZ
|
|
121
|
+
R = RGB_max * gamma_forward( +3.2406 * X/100. -1.5372 * Y/100. -0.4986 * Z/100. )
|
|
122
|
+
G = RGB_max * gamma_forward( -0.9689 * X/100. +1.8758 * Y/100. +0.0415 * Z/100. )
|
|
123
|
+
B = RGB_max * gamma_forward( +0.0557 * X/100. -0.2040 * Y/100. +1.0570 * Z/100. )
|
|
124
|
+
return (R,G,B)
|
|
125
|
+
|
|
126
|
+
def RGB2XYZ(RGB):
|
|
127
|
+
R,G,B = RGB
|
|
128
|
+
X = 100 * ( +0.4124 * gamma_reverse(R/RGB_max) +0.3576 * gamma_reverse(G/RGB_max) +0.1805 * gamma_reverse(B/RGB_max) )
|
|
129
|
+
Y = 100 * ( +0.2126 * gamma_reverse(R/RGB_max) +0.7152 * gamma_reverse(G/RGB_max) +0.0722 * gamma_reverse(B/RGB_max) )
|
|
130
|
+
Z = 100 * ( +0.0193 * gamma_reverse(R/RGB_max) +0.1192 * gamma_reverse(G/RGB_max) +0.9505 * gamma_reverse(B/RGB_max) )
|
|
131
|
+
return (X,Y,Z)
|
|
132
|
+
|
|
133
|
+
def gamma_forward(Cln):
|
|
134
|
+
mask = Cln <= 0.0031308
|
|
135
|
+
g = np.empty_like(Cln)
|
|
136
|
+
g[ mask] = 12.92*Cln[mask]
|
|
137
|
+
g[~mask] = 1.055*(Cln[~mask]**(1/2.4))-0.055
|
|
138
|
+
return g
|
|
139
|
+
|
|
140
|
+
def gamma_reverse(Cnl):
|
|
141
|
+
mask = Cnl <= 0.04045
|
|
142
|
+
g = np.empty_like(Cnl)
|
|
143
|
+
g[ mask] = Cnl[mask]/12.92
|
|
144
|
+
g[~mask] = ((Cnl[~mask]+0.055)/1.055)**2.4
|
|
145
|
+
return g
|
|
146
|
+
|
|
147
|
+
# RGB gamut check
|
|
148
|
+
|
|
149
|
+
def crop1(R,min=0,max=1):
|
|
150
|
+
R_crop = np.where(R < min, np.nan, R )
|
|
151
|
+
R_crop = np.where(R_crop > max, np.nan, R_crop)
|
|
152
|
+
return R_crop
|
|
153
|
+
|
|
154
|
+
def crop3(RGB,min=0,max=1):
|
|
155
|
+
R,G,B = RGB
|
|
156
|
+
return np.stack((crop1(R,min,max),crop1(G,min,max),crop1(B,min,max)),axis=-1)
|
|
157
|
+
|
|
158
|
+
def clip1(R,min=0,max=1):
|
|
159
|
+
R_clip = np.where(R < min, min, R )
|
|
160
|
+
R_clip = np.where(R_clip > max, max, R_clip)
|
|
161
|
+
return R_clip
|
|
162
|
+
|
|
163
|
+
def clip3(RGB,min=0,max=1):
|
|
164
|
+
R,G,B = RGB
|
|
165
|
+
return np.stack((clip1(R,min,max),clip1(G,min,max),clip1(B,min,max)),axis=-1)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
colourspace/gamut.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Limits of the full human gamut, or the sRGB gamut, in CIE LCH space: Cmax(L,H)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import print_function
|
|
7
|
+
import sys
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pylab as plt
|
|
10
|
+
import matplotlib.ticker as ticker
|
|
11
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
12
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
13
|
+
from scipy import interpolate
|
|
14
|
+
from . import convert
|
|
15
|
+
from . import limits
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
this_dir, this_filename = os.path.split(__file__)
|
|
19
|
+
this_dir += "/gamut"
|
|
20
|
+
|
|
21
|
+
# the gamut is cached
|
|
22
|
+
Cmax = {}
|
|
23
|
+
|
|
24
|
+
# ranges for L and H
|
|
25
|
+
# (several functions need to know this)
|
|
26
|
+
L_min = 0 ; L_max = 100
|
|
27
|
+
H_min = 0 ; H_max = 360
|
|
28
|
+
|
|
29
|
+
#---------------------------
|
|
30
|
+
# find the gamut boundary
|
|
31
|
+
# method 1: sample the LH plane, for each point convert to native space and check if within limits
|
|
32
|
+
#---------------------------
|
|
33
|
+
|
|
34
|
+
def valid_LCH_full(L,C,H):
|
|
35
|
+
""" Checks if a LCH colour is in the human gamut """
|
|
36
|
+
X,Y,Z = convert.Lab2XYZ(convert.LCH2Lab((L,C,H)))
|
|
37
|
+
XYZ = np.stack((X,Y,Z),axis=-1)
|
|
38
|
+
return limits.within_limits(XYZ,'XYZ',kind='cmp') # assumes limits have been set
|
|
39
|
+
|
|
40
|
+
def valid_LCH_sRGB(L,C,H):
|
|
41
|
+
""" Checks if a LCH colour is in the sRGB gamut """
|
|
42
|
+
R,G,B = convert.LCH2RGB(L,C,H)
|
|
43
|
+
valid = lambda x: np.logical_and(0 <= x, x <= 1)
|
|
44
|
+
return valid(R)*valid(G)*valid(B)
|
|
45
|
+
#return np.logical_and(np.logical_and(valid(R),valid(G)),valid(B))
|
|
46
|
+
|
|
47
|
+
valid_LCH = {"full": valid_LCH_full, \
|
|
48
|
+
"sRGB": valid_LCH_sRGB}
|
|
49
|
+
|
|
50
|
+
def find_Cmax_forward(res, gmt, save=False, plot=True, version='mapping'):
|
|
51
|
+
""" Finds the maximum Cmax for each (L,H) pair with a precision of res points per unit of L,C,H """
|
|
52
|
+
if res not in Cmax.keys(): Cmax[res] = {}
|
|
53
|
+
if gmt=='full' and not 'XYZ' in limits.triangulation['cmp'].keys():
|
|
54
|
+
limits.set_limits(l_step=10, l_min=360, l_max=780)
|
|
55
|
+
limits.triangulate('XYZ')
|
|
56
|
+
L = np.linspace(L_min,L_max,int((L_max-L_min)*res+1))
|
|
57
|
+
H = np.linspace(H_min,H_max,int((H_max-H_min)*res+1))
|
|
58
|
+
Cmax[res][gmt] = np.zeros((len(L),len(H)),dtype=np.float32)
|
|
59
|
+
# version with loops
|
|
60
|
+
if version == 'looping':
|
|
61
|
+
for i in range(len(L)):
|
|
62
|
+
for k in range(len(H)):
|
|
63
|
+
Cmax[res][gmt][i,k] = find_Cmax_for_LH(L=L[i], H=H[k], Cres=1./res, gmt=gmt)
|
|
64
|
+
sys.stdout.write("L = %.2f, H = %.2f, Cmax = %.2f"%(L[i],H[k],Cmax[res][gmt][i,k]))
|
|
65
|
+
sys.stdout.write("\r")
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
sys.stdout.write("\n")
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
# version with mapping
|
|
70
|
+
if version == 'mapping':
|
|
71
|
+
LL, HH = np.meshgrid(L,H,indexing='ij')
|
|
72
|
+
Cmax[res][gmt][:,:] = find_Cmax_for_LH(LL, HH, Cres=1./res, gmt=gmt)
|
|
73
|
+
if version == 'mapping3D':
|
|
74
|
+
C = np.linspace(0, 200, int(200.*res)+1)
|
|
75
|
+
LLL, CCC, HHH = np.meshgrid(L,C,H,indexing='ij')
|
|
76
|
+
valid = valid_LCH[gmt](LLL,CCC,HHH)
|
|
77
|
+
for i in range(len(L)):
|
|
78
|
+
for k in range(len(H)):
|
|
79
|
+
j_valid = np.where(valid[i,:,k])[0]
|
|
80
|
+
Cmax[res][gmt][i,k] = C[j_valid[-1]] if len(j_valid)>0 else 0
|
|
81
|
+
if save: save_Cmax_npy(res=res, gmt=gmt)
|
|
82
|
+
if plot: plot_Cmax(res=res, gmt=gmt)
|
|
83
|
+
|
|
84
|
+
def find_Cmax_for_LH(L, H, Cres, gmt):
|
|
85
|
+
"""Finds the maximum C for a given (L,H) at a given resolution in a given gamut"""
|
|
86
|
+
edge_detector = lambda L,H: find_edge_by_dichotomy(lambda c: valid_LCH[gmt](L,c,H), xmin=0, xmax=200, dx=Cres)
|
|
87
|
+
Cmax = np.vectorize(edge_detector)(L,H)
|
|
88
|
+
Cmax = np.where(L<= 0, 0,Cmax)
|
|
89
|
+
Cmax = np.where(L>=100, 0,Cmax)
|
|
90
|
+
Cmax = np.where(L< 0,np.nan,Cmax)
|
|
91
|
+
Cmax = np.where(L> 100,np.nan,Cmax)
|
|
92
|
+
#Cmax = np.where(np.logical_or(L<=0,L>=100), 0,Cmax)
|
|
93
|
+
#Cmax = np.where(np.logical_or(L< 0,L> 100),np.nan,Cmax)
|
|
94
|
+
return Cmax
|
|
95
|
+
|
|
96
|
+
def find_edge_by_dichotomy(func, xmin, xmax, dx=1., iter_max=100):
|
|
97
|
+
"""Returns the point `x` (within resolution `dx`) where boolean function `func` changes value
|
|
98
|
+
`func` is assumed to switch from True to False between `xmin` and `xmax`
|
|
99
|
+
"""
|
|
100
|
+
xleft = xmin
|
|
101
|
+
xright = xmax
|
|
102
|
+
xmid = 0.5*(xright-xleft)
|
|
103
|
+
i = 0
|
|
104
|
+
delta = dx
|
|
105
|
+
while delta >= dx and i<iter_max:
|
|
106
|
+
i += 1
|
|
107
|
+
#print("i = %4i: x = [%6.2f, %6.2f]: func(%6.2f) = %i"%(i,xleft,xright,xmid,func(xmid)),end='')
|
|
108
|
+
if func(xmid): xleft = xmid
|
|
109
|
+
else: xright = xmid
|
|
110
|
+
xmid_old = xmid
|
|
111
|
+
xmid = 0.5*(xleft+xright)
|
|
112
|
+
delta = abs(xmid_old-xmid)
|
|
113
|
+
#print("-> x = [%6.2f, %6.2f], delta=%f"%(xleft,xright,delta))
|
|
114
|
+
if i >= iter_max: print("edge not found at precision ",dx,"in ",iter_max," iterations")
|
|
115
|
+
return np.around(xmid,int(np.ceil(np.log10(1./dx))))
|
|
116
|
+
|
|
117
|
+
#---------------------------
|
|
118
|
+
# find the gamut boundary
|
|
119
|
+
# method 2: discretize the gamut boundary in the native space, project it back to the LH plane
|
|
120
|
+
#---------------------------
|
|
121
|
+
|
|
122
|
+
def get_RGB_faces(num=10):
|
|
123
|
+
""" Samples the faces of the RGB cube with `num` points per axis """
|
|
124
|
+
array = {}
|
|
125
|
+
for coord in ['R','G','B']: array[coord] = np.array([])
|
|
126
|
+
block = {}
|
|
127
|
+
block['0'] = np.zeros(num**2)
|
|
128
|
+
block['1'] = np.ones( num**2)
|
|
129
|
+
x = np.linspace(0,1,int(num))
|
|
130
|
+
X,Y = np.meshgrid(x,x)
|
|
131
|
+
block['x'] = X.flatten()
|
|
132
|
+
block['y'] = Y.flatten()
|
|
133
|
+
for x,y,z in [('R','G','B'), ('G','B','R'), ('B','R','G')]:
|
|
134
|
+
for side in ['0','1']:
|
|
135
|
+
array[x] = np.concatenate((array[x], block['x']))
|
|
136
|
+
array[y] = np.concatenate((array[y], block['y']))
|
|
137
|
+
array[z] = np.concatenate((array[z], block[side]))
|
|
138
|
+
return array['R'],array['G'],array['B']
|
|
139
|
+
|
|
140
|
+
def get_edges_LCH_sRGB(res):
|
|
141
|
+
R,G,B = get_RGB_faces(num=res)
|
|
142
|
+
L,C,H = convert.RGB2LCH(R,G,B)
|
|
143
|
+
return np.stack((L,C,H),axis=-1)
|
|
144
|
+
|
|
145
|
+
def get_edges_LCH_full(res):
|
|
146
|
+
limits.set_limits(l_step=res, l_min=360, l_max=780)
|
|
147
|
+
return limits.limits['cmp']['LCH']
|
|
148
|
+
|
|
149
|
+
get_edges_LCH = {"full": get_edges_LCH_full, \
|
|
150
|
+
"sRGB": get_edges_LCH_sRGB}
|
|
151
|
+
|
|
152
|
+
def find_Cmax_backward(res_native, res_LH, gmt, save=False, plot=True):
|
|
153
|
+
""" Finds the maximum Cmax(L,H) by discretizing the gamut boundary in its native space
|
|
154
|
+
for sRGB gamut: res_native = number of points along R, G, B
|
|
155
|
+
for full gamut: res_native = delta_Lambda in nm
|
|
156
|
+
the LH plane will be re-sampled regularly at resolution res_LH
|
|
157
|
+
"""
|
|
158
|
+
if res_LH not in Cmax.keys(): Cmax[res_LH] = {}
|
|
159
|
+
# get edges in LCH space
|
|
160
|
+
LCH_max = get_edges_LCH[gmt](res_native)
|
|
161
|
+
# interpolate the implicit function C(L,H)
|
|
162
|
+
L = LCH_max[:,0]
|
|
163
|
+
C = LCH_max[:,1]
|
|
164
|
+
H = LCH_max[:,2]
|
|
165
|
+
L_grid, H_grid = np.mgrid[L_min:L_max:1j*(L_max-L_min+1)*res_LH, H_min:H_max:1j*(H_max-H_min+1)*res_LH]
|
|
166
|
+
C_grid = interpolate.griddata((L,H),C,(L_grid,H_grid),method='linear')
|
|
167
|
+
C_grid[np.where(np.isnan(C_grid))] = 0
|
|
168
|
+
# fix the edges
|
|
169
|
+
C_grid[:, 0] = 0.5*(C_grid[:,1]+C_grid[:,-2])
|
|
170
|
+
C_grid[:,-1] = 0.5*(C_grid[:,1]+C_grid[:,-2])
|
|
171
|
+
|
|
172
|
+
Cmax[res_LH][gmt] = np.zeros(C_grid.shape,dtype=np.float32)
|
|
173
|
+
Cmax[res_LH][gmt][:,:] = C_grid[:,:]
|
|
174
|
+
if save: save_Cmax_npy(res=res_LH, gmt=gmt)
|
|
175
|
+
if plot: plot_Cmax(res=res_LH, gmt=gmt)
|
|
176
|
+
|
|
177
|
+
#-------------------
|
|
178
|
+
# display the gamut
|
|
179
|
+
#-------------------
|
|
180
|
+
|
|
181
|
+
def get_extremum(res, gmt):
|
|
182
|
+
""" Prints the LCH value of the colour of highest C """
|
|
183
|
+
C = Cmax[res][gmt].max()
|
|
184
|
+
iL,iH = np.unravel_index(Cmax[res][gmt].argmax(),Cmax[res][gmt].shape)
|
|
185
|
+
nL = len(Cmax[res][gmt][:,0])
|
|
186
|
+
L = L_min + iL/(nL-1.) * (L_max-L_min)
|
|
187
|
+
nH = len(Cmax[res][gmt][0,:])
|
|
188
|
+
H = H_min + iH/(nH-1.) * (H_max-H_min)
|
|
189
|
+
return np.array((L,C,H))
|
|
190
|
+
|
|
191
|
+
def plot_Cmax(res, gmt, vmax=200, fig=1, figsize=None, dpi=None, dir=this_dir, fname="Cmax", axes=['on','off']):
|
|
192
|
+
""" Plots Cmax(H,L) """
|
|
193
|
+
plot2D(Cmax[res][gmt], name=gmt, vmax=vmax, fname='%s_res%i_%s'%(fname,res,gmt), fig=fig, figsize=figsize, dpi=dpi, dir=dir, axes=axes)
|
|
194
|
+
|
|
195
|
+
def plot2D(array, marker='', colour='', vmin=0, vmax=200, cbar=3, fig=1, figsize=None, dpi=None, aspect="equal", name="", fname="Cmax", dir=this_dir, axes=['on','off']):
|
|
196
|
+
""" Plots a surface represented explicitly by a 2D array XY or implicitly by a set of 3D points XYZ """
|
|
197
|
+
cmap = "Greys_r"
|
|
198
|
+
norm = plt.Normalize(vmin=vmin, vmax=vmax)
|
|
199
|
+
if dir != "":
|
|
200
|
+
fname='%s/%s'%(dir,fname)
|
|
201
|
+
ext = ".png"
|
|
202
|
+
if fig != 0:
|
|
203
|
+
plt.figure(fig,figsize=figsize)
|
|
204
|
+
plt.title("%s gamut"%name)
|
|
205
|
+
plt.xlabel("H")
|
|
206
|
+
plt.ylabel("L", rotation='horizontal')
|
|
207
|
+
plt.xlim([0,360])
|
|
208
|
+
plt.ylim([0,100])
|
|
209
|
+
if array.shape[1]==3:
|
|
210
|
+
# array is the 3D surface of a 2D function
|
|
211
|
+
L = array[:,0]
|
|
212
|
+
C = array[:,1]
|
|
213
|
+
H = array[:,2]
|
|
214
|
+
plt.tricontourf(H,L,C, cmap=cmap, norm=norm)
|
|
215
|
+
if marker != '':
|
|
216
|
+
ax = plt.gca()
|
|
217
|
+
if len(colour)>0:
|
|
218
|
+
ax.plot(H,L,marker,c=colour)
|
|
219
|
+
else:
|
|
220
|
+
#ax.plot(H,L,marker,color=convert.clip3(convert.LCH2RGB(L,C,H)).tolist())
|
|
221
|
+
for h, l, c in zip(H, L, array): ax.plot(h,l,marker,color=convert.clip3(convert.LCH2RGB(c[0],c[1],c[2])))
|
|
222
|
+
plt.gca().set_aspect(aspect)
|
|
223
|
+
else:
|
|
224
|
+
# array is a 2D map
|
|
225
|
+
plt.imshow(array, origin='lower', extent=[H_min, H_max, L_min, L_max], aspect=aspect, interpolation='nearest', cmap=cmap, norm=norm)
|
|
226
|
+
locator = ticker.MultipleLocator(60)
|
|
227
|
+
plt.gca().xaxis.set_major_locator(locator)
|
|
228
|
+
if cbar>0:
|
|
229
|
+
cax = make_axes_locatable(plt.gca()).append_axes("right", size="%.f%%"%cbar, pad=0.10)
|
|
230
|
+
cb = plt.colorbar(plt.gci(), cax=cax)
|
|
231
|
+
cb.locator = ticker.MultipleLocator(50)
|
|
232
|
+
cb.update_ticks()
|
|
233
|
+
cb.set_label("Cmax")
|
|
234
|
+
if dir != "" and 'on' in axes:
|
|
235
|
+
print("writing %s"%(fname+"_axon"+ext))
|
|
236
|
+
plt.savefig(fname+"_axon"+ext, dpi=dpi, bbox_inches='tight')
|
|
237
|
+
if dir != "" and 'off' in axes and array.shape[1] > 3:
|
|
238
|
+
print("writing %s"%(fname+"_axoff"+ext))
|
|
239
|
+
plt.imsave(arr=array, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax, fname=fname+"_axoff"+ext, dpi=dpi if dpi!=None else 200)
|
|
240
|
+
#plt.imsave(fname+"_axoff"+ext, plt.get_cmap(cmap)(norm(np.flipud(array))), dpi=dpi if dpi!=None else 200)
|
|
241
|
+
|
|
242
|
+
def plot3D(RGB, angle=(0,0), fig=0, figsize=None, dpi=None, dir="", fname="RGB"):
|
|
243
|
+
""" Plots a set of (R,G,B) points in 3D
|
|
244
|
+
(beware: mplot3d does not composite colours correctly, and cannot handle large sets)
|
|
245
|
+
"""
|
|
246
|
+
# figure
|
|
247
|
+
fg = plt.figure("RGB",figsize=figsize)
|
|
248
|
+
ax = fg.add_subplot(111, projection='3d')
|
|
249
|
+
ax.set_xlabel("R")
|
|
250
|
+
ax.set_ylabel("G")
|
|
251
|
+
ax.set_zlabel("B")
|
|
252
|
+
ax.xaxis.pane.fill = False
|
|
253
|
+
ax.yaxis.pane.fill = False
|
|
254
|
+
ax.zaxis.pane.fill = False
|
|
255
|
+
ax.view_init(angle[0],angle[1])
|
|
256
|
+
#ax.grid(False)
|
|
257
|
+
# plot
|
|
258
|
+
R,G,B = RGB
|
|
259
|
+
RGB_list = np.stack((R,G,B),axis=-1)
|
|
260
|
+
print(len(RGB_list)," points")
|
|
261
|
+
ax.scatter(R,G,B,color=RGB_list,marker='o',depthshade=False)
|
|
262
|
+
# save
|
|
263
|
+
if dir != "":
|
|
264
|
+
fname = "%s/%s_%s.png"%(dir,fname,space)
|
|
265
|
+
print("writing %s"%(fname))
|
|
266
|
+
plt.savefig(fname, dpi=dpi, bbox_inches='tight')
|
|
267
|
+
if fig<0: plt.close(fg)
|
|
268
|
+
|
|
269
|
+
#-------------------------
|
|
270
|
+
# save and load the gamut
|
|
271
|
+
#-------------------------
|
|
272
|
+
|
|
273
|
+
def save_Cmax_npy(res, gmt, dir=this_dir):
|
|
274
|
+
""" Saves a gamut as a numpy binary file """
|
|
275
|
+
global Cmax
|
|
276
|
+
fname = '%s/Cmax_res%.0f_%s.npy'%(dir,res,gmt)
|
|
277
|
+
print("saving gamut to %s"%fname)
|
|
278
|
+
np.save(fname, Cmax[res][gmt])
|
|
279
|
+
|
|
280
|
+
def load_Cmax_npy(res, gmt, dir=this_dir):
|
|
281
|
+
""" Loads a gamut from a numpy binary file """
|
|
282
|
+
global Cmax
|
|
283
|
+
fname = '%s/Cmax_res%.0f_%s.npy'%(dir,res,gmt)
|
|
284
|
+
print("loading gamut from %s"%fname)
|
|
285
|
+
if res not in Cmax.keys(): Cmax[res] = {}
|
|
286
|
+
Cmax[res][gmt] = np.load(fname)
|
|
287
|
+
|
|
288
|
+
def save_Cmax_txt(res, gmt, dir=this_dir):
|
|
289
|
+
""" Saves a gamut as a text file """
|
|
290
|
+
global Cmax
|
|
291
|
+
fname = '%s/Cmax_res%.0f_%s.txt'%(dir,res,gmt)
|
|
292
|
+
file = open(fname, 'w')
|
|
293
|
+
print("saving gamut to %s"%fname)
|
|
294
|
+
L = np.linspace(L_min,L_max,int((L_max-L_min)*res+1))
|
|
295
|
+
H = np.linspace(H_min,H_max,int((H_max-H_min)*res+1))
|
|
296
|
+
digits = np.ceil(np.log10(res))
|
|
297
|
+
format = "%%%i.%if"%(4+digits,digits)
|
|
298
|
+
formats = format+"\t"+format+"\t"+format+"\n"
|
|
299
|
+
for i in range(len(L)):
|
|
300
|
+
for k in range(len(H)):
|
|
301
|
+
file.write(formats%(L[i],H[k],Cmax[res][gmt][i,k]))
|
|
302
|
+
file.close()
|
|
303
|
+
|
|
304
|
+
def load_Cmax_txt(res, gmt, dir=this_dir):
|
|
305
|
+
""" Loads a gamut from a text file (as written by save_Cmax_txt()) """
|
|
306
|
+
global Cmax
|
|
307
|
+
Cmax[res] = {}
|
|
308
|
+
fname = '%s/Cmax_res%.0f_%s.txt'%(dir,res,gmt)
|
|
309
|
+
file = open(fname, 'r')
|
|
310
|
+
print("loading gamut from %s"%fname)
|
|
311
|
+
L = np.linspace(L_min,L_max,int((L_max-L_min)*res+1))
|
|
312
|
+
H = np.linspace(H_min,H_max,int((H_max-H_min)*res+1))
|
|
313
|
+
Cmax[res][gmt] = np.zeros((len(L),len(H)),dtype=np.float32)
|
|
314
|
+
for i in range(len(L)):
|
|
315
|
+
for k in range(len(H)):
|
|
316
|
+
Cmax[res][gmt][i,k] = float(file.readline().strip("\n").split("\t")[-1])
|
|
317
|
+
file.close()
|
|
318
|
+
|
|
319
|
+
#---------------
|
|
320
|
+
# use the gamut
|
|
321
|
+
#---------------
|
|
322
|
+
|
|
323
|
+
def set_Cmax(res,gmt):
|
|
324
|
+
""" Loads or computes a gamut as needed (only needed once) """
|
|
325
|
+
global Cmax
|
|
326
|
+
if res in Cmax.keys() and gmt in Cmax[res].keys(): return
|
|
327
|
+
try:
|
|
328
|
+
load_Cmax_npy(res,gmt)
|
|
329
|
+
except:
|
|
330
|
+
print("couldn't load gamut '%s' at res=%f, computing it"%(gmt,res))
|
|
331
|
+
find_Cmax_forward(res, gmt)
|
|
332
|
+
|
|
333
|
+
def Cmax_for_LH(L,H,res=1,gmt='full'):
|
|
334
|
+
""" Returns the maximum C for a given pair (L,H)
|
|
335
|
+
at a given resolution in a given gamut """
|
|
336
|
+
global Cmax
|
|
337
|
+
set_Cmax(res,gmt) # the gamut array is cached
|
|
338
|
+
H = H%360
|
|
339
|
+
L_valid = np.logical_and(L>=0, L<=100)
|
|
340
|
+
C = np.where(L_valid,interpolate_Cmax_for_LH(L,H,Cmax[res][gmt]),np.nan)
|
|
341
|
+
return C
|
|
342
|
+
|
|
343
|
+
def interpolate_Cmax_for_LH(L,H,Cmax):
|
|
344
|
+
""" Bi-linearly interpolates tabulated Cmax(L,H) at given L,H
|
|
345
|
+
(expects L in [L_min,L_max] = [0,100] and H in [H_min,Hmax] = [0,360])
|
|
346
|
+
"""
|
|
347
|
+
# L
|
|
348
|
+
nL = Cmax.shape[0]
|
|
349
|
+
i = (L-L_min)/float(L_max-L_min) * (nL-1)
|
|
350
|
+
i0 = (np.floor(i)).astype(int)
|
|
351
|
+
i0 = np.maximum(np.minimum(i0,nL-1),0)
|
|
352
|
+
i1 = np.where(i0 < nL-1, i0 + 1, i0)
|
|
353
|
+
x = i - i0
|
|
354
|
+
# H
|
|
355
|
+
nH = Cmax.shape[1]
|
|
356
|
+
j = (H-H_min)/float(H_max-H_min) * (nH-1)
|
|
357
|
+
j = np.where(np.isnan(j),0,j) # to avoid casting error
|
|
358
|
+
j0 = (np.floor(j)).astype(int)
|
|
359
|
+
j0 = np.maximum(np.minimum(j0,nH-1),0)
|
|
360
|
+
j1 = np.where(j0 < nH-1, j0 + 1, j0)
|
|
361
|
+
y = j - j0
|
|
362
|
+
# C (bilinear interpolation)
|
|
363
|
+
C = Cmax[i0,j0] * (1-x)*(1-y) \
|
|
364
|
+
+ Cmax[i0,j1] * (1-x)* y \
|
|
365
|
+
+ Cmax[i1,j0] * x *(1-y) \
|
|
366
|
+
+ Cmax[i1,j1] * x * y
|
|
367
|
+
return C
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|