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.
@@ -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