File indexing completed on 2026-04-09 07:48:46
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
0020
0021 """
0022 cie.py: converts wavelength spectra into XYZ and RGB colorspaces
0023 ===================================================================
0024
0025 Conversion of the binned wavelength spectra into XYZ (using
0026 CIE weighting functions) and then RGB produces a spectrum
0027
0028 [FIXED] Unphysical color repetition
0029 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0030
0031 Uniform scaling by maximal single X,Y,Z or R,G,B
0032 prior to clipping gets rid of the unphysical color repetition
0033 but theres kinda a between the green and the blue, where cyan
0034 should be
0035
0036 #hRGB_raw /= hRGB_raw[0,:,0].max() # scaling by maximal red, results in muted spectrum
0037 #hRGB_raw /= hRGB_raw[0,:,1].max() # scaling by maximal green, OK
0038 #hRGB_raw /= hRGB_raw[0,:,2].max() # scaling by maximal blue, similar to green by pumps the blues and nice yellow
0039
0040 The entire spectral locus is outside sRGB gamut (the triangle),
0041 so all bins are being clipped.
0042
0043 Not clipping produces a psychedelic mess.
0044
0045
0046 [ISSUE] Blue/Green transition looks unphysical
0047 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0048
0049 Need better way to handle out of gamut ?
0050
0051 Raw numbers show that green ramps up thru 430..480 nm but
0052 its all negative, so that info is clipped.
0053
0054 ::
0055
0056 In [68]: np.set_printoptions(linewidth=150)
0057
0058 In [75]: np.hstack([wd[:-1,None],c.raw[0],c.xyz[0],c.rgb[0]])
0059 Out[75]:
0060 array([[ 350. , 0. , 0.016, 0.102, 0. , 0. , 0. , -0. , 0. , 0. ],
0061 [ 370. , 0.015, 0.105, 1.922, 0. , 0. , 0.001, -0.001, 0. , 0.001],
0062 [ 390. , 1.873, 0.582, 20.444, 0.001, 0. , 0.011, -0.003, 0. , 0.012],
0063 [ 410. , 49.306, 2.691, 205.061, 0.028, 0.002, 0.115, 0.03 , -0.019, 0.123],
0064 [ 430. , 273.393, 10.384, 1386.823, 0.153, 0.006, 0.779, 0.1 , -0.105, 0.83 ],
0065 [ 450. , 343.75 , 33.415, 1781.385, 0.193, 0.019, 1. , 0.098, -0.11 , 1.064],
0066 [ 470. , 191.832, 89.944, 1294.473, 0.108, 0.05 , 0.727, -0.091, 0.021, 0.764],
0067 [ 490. , 32.012, 213.069, 465.525, 0.018, 0.12 , 0.261, -0.256, 0.218, 0.253],
0068 [ 510. , 16.48 , 500.611, 155.962, 0.009, 0.281, 0.088, -0.446, 0.522, 0.036],
0069 [ 530. , 159.607, 869.052, 43.036, 0.09 , 0.488, 0.024, -0.472, 0.829, -0.069],
0070 [ 550. , 433.715, 994.463, 8.758, 0.243, 0.558, 0.005, -0.072, 0.812, -0.095],
0071 [ 570. , 772.904, 950.107, 1.308, 0.434, 0.533, 0.001, 0.586, 0.58 , -0.084],
0072 [ 590. , 1021.039, 762.587, 0.143, 0.573, 0.428, 0. , 1.199, 0.248, -0.055],
0073 [ 610. , 1000.205, 500.338, 0.012, 0.561, 0.281, 0. , 1.388, -0.017, -0.026],
0074 [ 630. , 656.21 , 263.667, 0.001, 0.368, 0.148, 0. , 0.966, -0.079, -0.01 ],
0075 [ 650. , 283.632, 110.045, 0. , 0.159, 0.062, 0. , 0.421, -0.038, -0.004],
0076 [ 670. , 80.766, 36.117, 0. , 0.045, 0.02 , 0. , 0.116, -0.006, -0.002],
0077 [ 690. , 17.024, 11.172, 0. , 0.01 , 0.006, 0. , 0.021, 0.003, -0.001]])
0078
0079
0080
0081 Chromatic Adaption
0082 ~~~~~~~~~~~~~~~~~~~~
0083
0084 * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
0085
0086
0087
0088 Refs
0089 ~~~~~
0090
0091 https://github.com/colour-science/colour/issues/191
0092 http://www.scipy-lectures.org/advanced/image_processing/index.html
0093 http://www.scipy-lectures.org/packages/scikit-image/index.html#scikit-image
0094
0095 http://www.scipy.org/scikits.html
0096 separate from scipy, but under the "brand"
0097
0098 """
0099
0100 import os, logging, numpy as np
0101 log = logging.getLogger(__name__)
0102 np.set_printoptions(linewidth=150)
0103
0104 import matplotlib.pyplot as plt
0105 import ciexyz.ciexyz as _cie
0106 from env.graphics.ciexyz.XYZ import Spectrum
0107 from env.graphics.ciexyz.RGB import RGB
0108
0109
0110 class CIE(object):
0111 def __init__(self, colorspace="sRGB/D65", whitepoint=None):
0112 cs = RGB(colorspace)
0113 self.x2r = cs.x2r
0114 self.whitepoint = whitepoint
0115
0116 def hist0d_XYZ(self,w, nb=100):
0117
0118 X = np.sum(_cie.X(w))
0119 Y = np.sum(_cie.Y(w))
0120 Z = np.sum(_cie.Z(w))
0121
0122 hX = np.repeat(X, nb)
0123 hY = np.repeat(Y, nb)
0124 hZ = np.repeat(Z, nb)
0125
0126 raw = np.dstack([hX,hY,hZ])
0127 self.raw = np.copy(raw)
0128 return raw
0129
0130 def hist1d_XYZ(self,w,x,xb):
0131 hX, hXx = np.histogram(x,bins=xb, weights=_cie.X(w))
0132 hY, hYx = np.histogram(x,bins=xb, weights=_cie.Y(w))
0133 hZ, hZx = np.histogram(x,bins=xb, weights=_cie.Z(w))
0134 assert np.all(hXx == xb) & np.all(hYx == xb ) & np.all(hZx == xb)
0135 raw = np.dstack([hX,hY,hZ])
0136 self.raw = np.copy(raw)
0137 return raw
0138
0139 def hist2d_XYZ(self,w,x,y,xb,yb):
0140 bins = [xb,yb]
0141 hX, hXx, hXy = np.histogram2d(x,y,bins=bins, weights=_cie.X(w))
0142 hY, hYx, hYy = np.histogram2d(x,y,bins=bins, weights=_cie.Y(w))
0143 hZ, hZx, hZy = np.histogram2d(x,y,bins=bins, weights=_cie.Z(w))
0144 assert np.all(hXx == xb) & np.all(hYx == xb ) & np.all(hZx == xb)
0145 assert np.all(hXy == yb) & np.all(hYy == yb ) & np.all(hZy == yb)
0146 return np.dstack([hX,hY,hZ])
0147
0148 def norm_XYZ(self, hXYZ, norm=2, scale=1):
0149 """
0150 Trying to find an appropriate way to normalize XYZ values
0151
0152 0,1,2
0153 scale by maximal of X,Y,Z
0154 3
0155 scale by maximal X+Y+Z
0156 4
0157 scale by Yint of an externally determined whitepoint
0158 (problem is that is liable to be with very much more light
0159 than are looking at...)
0160 5
0161 scale by Yint of the spectrum provided, this
0162 is also yielding very small X,Y,Z
0163
0164 >50
0165 scale is used, for normalization with Y value
0166 obtained from the histogram norm identified bin
0167
0168
0169 Hmm, some adhoc exposure factor seems unavoidable given the
0170 range of intensities so perhaps the adhoc techniques are appropriate after all.
0171
0172 Initial thinking was that the out-of-gamut problem was tied up with the
0173 exposure problem, but they are kinda orthogonal: think vectors in XYZ space,
0174 the length of the vector doesnt change the hue.
0175 """
0176 if norm in [0,1,2]:
0177 nscale = hXYZ[:,:,norm].max()
0178 elif norm == 3:
0179 nscale = np.sum(hXYZ, axis=2).max()
0180 elif norm == 4:
0181 assert not self.whitepoint is None
0182 nscale = self.whitepoint[4]
0183 elif norm == 5 or norm > 50:
0184 nscale = scale
0185 else:
0186 nscale = 1
0187 pass
0188
0189 hXYZ /= nscale
0190 self.scale = nscale
0191 self.xyz = np.copy(hXYZ)
0192 return hXYZ
0193
0194 def XYZ_to_RGB(self, hXYZ):
0195 return np.dot( hXYZ, self.x2r.T )
0196
0197 def hist0d(self, w, norm=2, nb=100):
0198 hXYZ_raw = self.hist0d_XYZ(w, nb=nb)
0199 hXYZ = self.norm_XYZ(hXYZ_raw, norm=norm)
0200 hRGB = self.XYZ_to_RGB(hXYZ)
0201 self.rgb = np.copy(hRGB)
0202 return hRGB,hXYZ,None
0203
0204 def hist1d(self, w, x, xb, norm=2):
0205 hXYZ_raw = self.hist1d_XYZ(w,x,xb)
0206
0207 if norm == 5:
0208 scale = np.sum(_cie.Y(w))
0209 elif norm > 50:
0210
0211 scale = hXYZ_raw[0,norm,1]
0212 else:
0213 scale = 1
0214 pass
0215
0216 hXYZ = self.norm_XYZ(hXYZ_raw, norm=norm, scale=scale)
0217 hRGB = self.XYZ_to_RGB(hXYZ)
0218 self.rgb = np.copy(hRGB)
0219 return hRGB,hXYZ,xb
0220
0221 def hist2d(self, w, x, y, xb, yb, norm=2):
0222 hXYZ_raw = self.hist2d_XYZ(w,x,y,xb,yb)
0223 self.raw = hXYZ_raw
0224 hXYZ = self.norm_XYZ(hXYZ_raw, norm=norm)
0225 hRGB = self.XYZ_to_RGB(hXYZ)
0226 extent = [xb[0], xb[-1], yb[0], yb[-1]]
0227 return hRGB,hXYZ,extent
0228
0229 def spectral_plot(self, ax, wd, norm=2):
0230
0231 ndupe = 1000
0232 w = np.tile(wd, ndupe)
0233 x = np.tile(wd, ndupe)
0234 xb = wd
0235
0236 hRGB_raw, hXYZ_raw, bx = self.hist1d(w, x, xb, norm=norm)
0237
0238 hRGB_1d = np.clip(hRGB_raw, 0, 1)
0239 ntile = 100
0240 hRGB = np.tile(hRGB_1d, ntile ).reshape(-1,ntile,3)
0241 extent = [0,2,bx[0],bx[-1]]
0242
0243
0244
0245
0246 interpolation = 'gaussian'
0247
0248 ax.imshow(hRGB, origin="lower", extent=extent, alpha=1, vmin=0, vmax=1, aspect='auto', interpolation=interpolation)
0249 ax.yaxis.set_visible(True)
0250 ax.xaxis.set_visible(False)
0251
0252 def swatch_plot(self, wd, norm=2):
0253 ndupe = 1000
0254 w = np.tile(wd, ndupe)
0255 hRGB_raw, hXYZ_raw, bx = self.hist0d(w, norm=norm)
0256
0257 hRGB_1d = np.clip(hRGB_raw, 0, 1)
0258 ntile = 100
0259 hRGB = np.tile(hRGB_1d, ntile ).reshape(-1,ntile,3)
0260 extent = [0,2,0,1]
0261 ax.imshow(hRGB, origin="lower", extent=extent, alpha=1, vmin=0, vmax=1, aspect='auto')
0262 ax.yaxis.set_visible(True)
0263 ax.xaxis.set_visible(False)
0264
0265
0266
0267 def cie_hist1d(w, x, xb, norm=1, colorspace="sRGB/D65", whitepoint=None):
0268 c = CIE(colorspace, whitepoint=whitepoint)
0269 return c.hist1d(w,x,xb,norm)
0270
0271 def cie_hist2d(w, x, y, xb, yb, norm=1, colorspace="sRGB/D65", whitepoint=None):
0272 c = CIE(colorspace, whitepoint=whitepoint)
0273 return c.hist2d(w,x,y,xb,yb,norm)
0274
0275
0276
0277 class Whitepoint(object):
0278 def __init__(self, w):
0279 """
0280 For spectra close to original (think perfect diffuse reflector)
0281 this is expected to yield the characteristic of the illuminant.
0282
0283 XYZ values must be normalized as clearly simulating more photons
0284 will give larger values...
0285
0286 The Yint is hoped to provide a less adhoc way of doing the
0287 normalization.
0288 """
0289 assert w is not None
0290
0291 X = np.sum(_cie.X(w))
0292 Y = np.sum(_cie.Y(w))
0293 Z = np.sum(_cie.Z(w))
0294
0295 Yint = Y
0296
0297 X /= Yint
0298 Y /= Yint
0299 Z /= Yint
0300
0301 x = X/(X+Y+Z)
0302 y = Y/(X+Y+Z)
0303
0304 self.wp = np.array([X,Y,Z,Yint,x,y])
0305
0306 def __repr__(self):
0307 return str(self.wp)
0308
0309
0310
0311
0312 def whitepoint(wd):
0313 bb = _cie.BB6K(wd)
0314 bb /= bb.max()
0315
0316 X = np.sum( _cie.X(wd)*bb )
0317 Y = np.sum( _cie.Y(wd)*bb )
0318 Z = np.sum( _cie.Z(wd)*bb )
0319
0320 norm = Y
0321
0322
0323 X /= norm
0324 Y /= norm
0325 Z /= norm
0326
0327 return [X,Y,Z], norm
0328
0329
0330
0331 def compare_norms(wdom):
0332 """
0333 norm 0:X,1:Y
0334 look almost identical
0335
0336 norm 2:Z, 3:X+Y+Z
0337 also look the same
0338
0339 0,1 have better yellow and less of a murky gap between green and blue
0340 """
0341 c = CIE()
0342
0343 nplot = 4
0344
0345 for i, norm in enumerate([0,1,2,3]):
0346 ax = fig.add_subplot(1,nplot,i+1)
0347 c.spectral_plot(ax, wdom, norm)
0348
0349
0350
0351
0352
0353 if __name__ == '__main__':
0354 pass
0355 logging.basicConfig(level=logging.INFO)
0356
0357 plt.close()
0358 plt.ion()
0359
0360 wdom = np.arange(350,720,20)
0361
0362
0363 fig = plt.figure()
0364
0365 compare_norms(wdom)
0366
0367 plt.show()
0368
0369
0370 wp = whitepoint(wdom)
0371
0372
0373
0374
0375
0376
0377
0378
0379
0380
0381