def spd_to_fci(spd, use_cielab=True): """ Calculate Feeling of Contrast Index (FCI). Args: :spd: | ndarray with spectral power distribution(s) of the test light source(s). :use_cielab: | True, optional | True: use original formulation of FCI, which adopts a CIECAT94 | chromatic adaptation transform followed by a conversion to | CIELAB coordinates before calculating the gamuts. | False: use CIECAM02 coordinates and embedded CAT02 transform. Returns: :fci: | ndarray with FCI values. References: 1. `Hashimoto, K., Yano, T., Shimizu, M., & Nayatani, Y. (2007). New method for specifying color-rendering properties of light sources based on feeling of contrast. Color Research and Application, 32(5), 361–371. <http://dx.doi.org/10.1002/col.20338>`_ """ # get xyz: xyz, xyzw = spd_to_xyz(spd, cieobs='1931_2', relative=True, rfl=_RFL_FCI, out=2) # set condition parameters: D = 1 Yb = 20 La = Yb * 1000 / np.pi / 100 if use_cielab: # apply ciecat94 chromatic adaptation transform: xyzc = cat.apply_ciecat94( xyz, xyzw=xyzw, E=1000, Yb=20, D=D, cat94_old=True ) # there is apparently an updated version with an alpha incomplete adaptation factor and noise = 0.1; However, FCI doesn't use that version. # convert to cielab: lab = xyz_to_lab(xyzc, xyzw=_XYZW_D65_REF) labd65 = np.repeat(xyz_to_lab(_XYZ_D65_REF, xyzw=_XYZW_D65_REF), lab.shape[1], axis=1) else: f = lambda xyz, xyzw: cam.xyz_to_jabC_ciecam02( xyz, xyzw=xyzw, La=1000 * 20 / np.pi / 100, Yb=20, surround='avg') lab = f(xyz, xyzw) labd65 = np.repeat(f(_XYZ_D65_REF, _XYZW_D65_REF), lab.shape[1], axis=1) fci = 100 * (_polyarea3D(lab) / _polyarea3D(labd65))**1.5 return fci
def test_lab(self, getvars): S, spds, rfls, xyz, xyzw = getvars labw = lx.xyz_to_lab(xyzw, xyzw=xyzw[:1, :]) lab = lx.xyz_to_lab(xyz, xyzw=xyzw[:1, :]) xyzw_ = lx.lab_to_xyz(labw, xyzw=xyzw[:1, :]) xyz_ = lx.lab_to_xyz(lab, xyzw=xyzw[:1, :]) assert np.isclose(xyz, xyz_).all() assert np.isclose(xyzw, xyzw_).all()
def to_lab(self, xyzw=None, cieobs=_CIEOBS): """ Convert XYZ tristimulus values to CIE 1976 L*a*b* (CIELAB) coordinates. Args: :xyzw: | None or ndarray with xyz values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating xyzw. Returns: :lab: | luxpy.LAB with .value field that is a ndarray | with CIE 1976 L*a*b* (CIELAB) color coordinates """ return LAB(value=xyz_to_lab(self.value, xyzw=xyzw, cieobs=cieobs), relative=self.relative, dtype='lab', xyzw=xyzw, cieobs=cieobs)
def DE2000(xyzt, xyzr, dtype='xyz', DEtype='jab', avg=None, avg_axis=0, out='DEi', xyzwt=None, xyzwr=None, KLCH=None): """ Calculate DE2000 color difference. Args: :xyzt: | ndarray with tristimulus values of test data. :xyzr: | ndarray with tristimulus values of reference data. :dtype: | 'xyz' or 'lab', optional | Specifies data type in :xyzt: and :xyzr:. :xyzwt: | None or ndarray, optional | White point tristimulus values of test data | None defaults to the one set in lx.xyz_to_lab() :xyzwr: | None or ndarray, optional | Whitepoint tristimulus values of reference data | None defaults to the one set in lx.xyz_to_lab() :DEtype: | 'jab' or str, optional | Options: | - 'jab' : calculates full color difference over all 3 dimensions. | - 'ab' : calculates chromaticity difference. | - 'j' : calculates lightness or brightness difference | (depending on :outin:). | - 'j,ab': calculates both 'j' and 'ab' options | and returns them as a tuple. :KLCH: | None, optional | Weigths for L, C, H | None: default to [1,1,1] :avg: | None, optional | None: don't calculate average DE, | otherwise use function handle in :avg:. :avg_axis: | axis to calculate average over, optional :out: | 'DEi' or str, optional | Requested output. Note: For the other input arguments, see specific color space used. Returns: :returns: | ndarray with DEi [, DEa] or other as specified by :out: References: 1. `Sharma, G., Wu, W., & Dalal, E. N. (2005). The CIEDE2000 color‐difference formula: Implementation notes, supplementary test data, and mathematical observations. Color Research & Application, 30(1), 21–30. <https://doi.org/10.1002/col.20070>`_ """ if KLCH is None: KLCH = [1, 1, 1] if dtype == 'xyz': labt = xyz_to_lab(xyzt, xyzw=xyzwt) labr = xyz_to_lab(xyzr, xyzw=xyzwr) else: labt = xyzt labr = xyzr Lt = labt[..., 0:1] at = labt[..., 1:2] bt = labt[..., 2:3] Ct = np.sqrt(at**2 + bt**2) #ht = cam.hue_angle(at,bt,htype = 'rad') Lr = labr[..., 0:1] ar = labr[..., 1:2] br = labr[..., 2:3] Cr = np.sqrt(ar**2 + br**2) #hr = cam.hue_angle(at,bt,htype = 'rad') # Step 1: Cavg = (Ct + Cr) / 2 G = 0.5 * (1 - np.sqrt((Cavg**7.0) / ((Cavg**7.0) + (25.0**7)))) apt = (1 + G) * at apr = (1 + G) * ar Cpt = np.sqrt(apt**2 + bt**2) Cpr = np.sqrt(apr**2 + br**2) Cpprod = Cpt * Cpr hpt = cam.hue_angle(apt, bt, htype='deg') hpr = cam.hue_angle(apr, br, htype='deg') hpt[(apt == 0) * (bt == 0)] = 0 hpr[(apr == 0) * (br == 0)] = 0 # Step 2: dL = np.abs(Lr - Lt) dCp = np.abs(Cpr - Cpt) dhp_ = hpr - hpt dhp = dhp_.copy() dhp[np.where(np.abs(dhp_) > 180)] = dhp[np.where(np.abs(dhp_) > 180)] - 360 dhp[np.where( np.abs(dhp_) < -180)] = dhp[np.where(np.abs(dhp_) < -180)] + 360 dhp[np.where(Cpprod == 0)] = 0 #dH = 2*np.sqrt(Cpprod)*np.sin(dhp/2*np.pi/180) dH = deltaH(dhp, Cpprod, htype='deg') # Step 3: Lp = (Lr + Lt) / 2 Cp = (Cpr + Cpt) / 2 hps = hpt + hpr hp = (hpt + hpr) / 2 hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] + 180 hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] - 180 hp[np.where(Cpprod == 0)] = 0 T = 1 - 0.17*np.cos((hp - 30)*np.pi/180) + 0.24*np.cos(2*hp*np.pi/180) +\ 0.32*np.cos((3*hp + 6)*np.pi/180) - 0.20*np.cos((4*hp - 63)*np.pi/180) dtheta = 30 * np.exp(-((hp - 275) / 25)**2) RC = 2 * np.sqrt((Cp**7) / ((Cp**7) + (25**7))) SL = 1 + ((0.015 * (Lp - 50)**2) / np.sqrt(20 + (Lp - 50)**2)) SC = 1 + 0.045 * Cp SH = 1 + 0.015 * Cp * T RT = -np.sin(2 * dtheta * np.pi / 180) * RC kL, kC, kH = KLCH DEi = ((dL / (kL * SL))**2, (dCp / (kC * SC))**2 + (dH / (kH * SH))**2 + RT * (dCp / (kC * SC)) * (dH / (kH * SH))) return _process_DEi(DEi, DEtype=DEtype, avg=avg, avg_axis=avg_axis, out=out)
# check xyz_to_cct(): cct, duv = lx.xyz_to_cct(xyzw, cieobs='1931_2', out=2) # check xyz_to_..., ..._to_xyz: labw = lx.xyz_to_Yxy(xyzw) lab = lx.xyz_to_Yxy(xyz) xyzw_ = lx.Yxy_to_xyz(labw) xyz_ = lx.Yxy_to_xyz(lab) labw = lx.xyz_to_Yuv(xyzw) lab = lx.xyz_to_Yuv(xyz) xyzw_ = lx.Yuv_to_xyz(labw) xyz_ = lx.Yuv_to_xyz(lab) labw = lx.xyz_to_lab(xyzw, xyzw=xyzw[:1, :]) lab = lx.xyz_to_lab(xyz, xyzw=xyzw[:1, :]) xyzw_ = lx.lab_to_xyz(labw, xyzw=xyzw[:1, :]) xyz_ = lx.lab_to_xyz(lab, xyzw=xyzw[:1, :]) labw = lx.xyz_to_luv(xyzw, xyzw=xyzw) lab = lx.xyz_to_luv(xyz, xyzw=xyzw) xyzw_ = lx.luv_to_xyz(labw, xyzw=xyzw) xyz_ = lx.luv_to_xyz(lab, xyzw=xyzw) labw = lx.xyz_to_ipt(xyzw) lab = lx.xyz_to_ipt(xyz) xyzw_ = lx.ipt_to_xyz(labw) xyz_ = lx.ipt_to_xyz(lab) labw = lx.xyz_to_wuv(xyzw, xyzw=xyzw)
def apply_ciecat94(xyz, xyzw, xyzwr=None, E=1000, Er=1000, Yb=20, D=1, cat94_old=True): """ Calculate corresponding color tristimulus values using the CIECAT94 chromatic adaptation transform. Args: :xyz: | ndarray with sample 1931 2° XYZ tristimulus values under the test illuminant :xyzw: | ndarray with white point tristimulus values of the test illuminant :xyzwr: | None, optional | ndarray with white point tristimulus values of the reference illuminant | None defaults to D65. :E: | 100, optional | Illuminance (lx) of test illumination :Er: | 63.66, optional | Illuminance (lx) of the reference illumination :Yb: | 20, optional | Relative luminance of the adaptation field (background) :D: | 1, optional | Degree of chromatic adaptation. | For object colours D = 1, | and for luminous colours (typically displays) D=0 Returns: :xyzc: | ndarray with corresponding tristimlus values. Reference: 1. CIE160-2004. (2004). A review of chromatic adaptation transforms (Vols. CIE160-200). CIE. """ #-------------------------------------------- # Define cone/chromatic adaptation sensor space: mcat = _MCATS['kries'] invmcat = np.linalg.inv(mcat) #-------------------------------------------- # Define default ref. white point: if xyzwr is None: xyzwr = np.array( [[9.5047e+01, 1.0000e+02, 1.0888e+02]] ) #spd_to_xyz(_CIE_D65, cieobs = '1931_2', relative = True, rfl = None) #-------------------------------------------- # Calculate Y,x,y of white: Yxyw = xyz_to_Yxy(xyzw) Yxywr = xyz_to_Yxy(xyzwr) #-------------------------------------------- # Calculate La, Lar: La = Yb * E / np.pi / 100 Lar = Yb * Er / np.pi / 100 #-------------------------------------------- # Calculate CIELAB L* of samples: Lstar = xyz_to_lab(xyz, xyzw)[..., 0] #-------------------------------------------- # Define xi_, eta_ and zeta_ functions: xi_ = lambda Yxy: (0.48105 * Yxy[..., 1] + 0.78841 * Yxy[..., 2] - 0.080811 ) / Yxy[..., 2] eta_ = lambda Yxy: (-0.27200 * Yxy[..., 1] + 1.11962 * Yxy[..., 2] + 0.04570) / Yxy[..., 2] zeta_ = lambda Yxy: 0.91822 * (1 - Yxy[..., 1] - Yxy[..., 2]) / Yxy[..., 2] #-------------------------------------------- # Calculate intermediate values for test and ref. illuminants: xit, etat, zetat = xi_(Yxyw), eta_(Yxyw), zeta_(Yxyw) xir, etar, zetar = xi_(Yxywr), eta_(Yxywr), zeta_(Yxywr) #-------------------------------------------- # Calculate alpha: if cat94_old == False: alpha = 0.1151 * np.log10(La) + 0.0025 * (Lstar - 50) + (0.22 * D + 0.510) alpha[alpha > 1] = 1 else: alpha = 1 #-------------------------------------------- # Calculate adapted intermediate xip, etap zetap: xip = alpha * xit - (1 - alpha) * xir etap = alpha * etat - (1 - alpha) * etar zetap = alpha * zetat - (1 - alpha) * zetar #-------------------------------------------- # Calculate effective adapting response Rw, Gw, Bw and Rwr, Gwr, Bwr: #Rw, Gw, Bw = La*xit, La*etat, La*zetat # according to westland's book: Computational Colour Science wirg Matlab Rw, Gw, Bw = La * xip, La * etap, La * zetap # according to CIE160-2004 Rwr, Gwr, Bwr = Lar * xir, Lar * etar, Lar * zetar #-------------------------------------------- # Calculate beta1_ and beta2_ exponents for (R,G) and B: beta1_ = lambda x: (6.469 + 6.362 * x**0.4495) / (6.469 + x**0.4495) beta2_ = lambda x: 0.7844 * (8.414 + 8.091 * x**0.5128) / (8.414 + x** 0.5128) b1Rw, b1Rwr, b1Gw, b1Gwr = beta1_(Rw), beta1_(Rwr), beta1_(Gw), beta1_(Gwr) b2Bw, b2Bwr = beta2_(Bw), beta2_(Bwr) #-------------------------------------------- # Noise term: n = 1 if cat94_old else 0.1 #-------------------------------------------- # K factor = p/q (for correcting the difference between # the illuminance of the test and references conditions) # calculate q: p = ((Yb * xip + n) / (20 * xip + n))**(2 / 3 * b1Rw) * ((Yb * etap + n) / (20 * etap + n))**(1 / 3 * b1Gw) q = ((Yb * xir + n) / (20 * xir + n))**(2 / 3 * b1Rwr) * ((Yb * etar + n) / (20 * etar + n))**(1 / 3 * b1Gwr) K = p / q #-------------------------------------------- # transform sample xyz to cat sensor space: rgb = math.dot23(mcat, xyz.T).T #-------------------------------------------- # Calculate corresponding colors: Rc = (Yb * xir + n) * K**(1 / b1Rwr) * ((rgb[..., 0] + n) / (Yb * xip + n))**(b1Rw / b1Rwr) - n Gc = (Yb * etar + n) * K**(1 / b1Gwr) * ( (rgb[..., 1] + n) / (Yb * etap + n))**(b1Gw / b1Gwr) - n Bc = (Yb * zetar + n) * K**(1 / b2Bwr) * ( (rgb[..., 2] + n) / (Yb * zetap + n))**(b2Bw / b2Bwr) - n #-------------------------------------------- # transform to xyz and return: xyzc = math.dot23(invmcat, ajoin((Rc, Gc, Bc)).T).T return xyzc