def spd_to_tm30(St): # calculate CIE 1931 2° white point xyz: xyzw_cct, _ = spd_to_xyz(St, cieobs = '1931_2', relative = True, out = 2) # calculate cct, duv: cct, duv = xyz_to_cct(xyzw_cct, cieobs = '1931_2', out = 'cct,duv') # calculate ref illuminant: Sr = _cri_ref(cct, mix_range = [4000, 5000], cieobs = '1931_2', wl3 = St[0]) # calculate CIE 1964 10° sample and white point xyz under test and ref. illuminants: xyz, xyzw = spd_to_xyz(np.vstack((St,Sr[1:])), cieobs = '1964_10', rfl = _TM30_SAMPLE_SET, relative = True, out = 2) N = St.shape[0]-1 xyzt, xyzr = xyz[:,:N,:], xyz[:,N:,:] xyzwt, xyzwr = xyzw[:N,:], xyzw[N:,:] # calculate CAM02-UCS coordinates # (standard conditions = {'La':100.0,'Yb':20.0,'surround':'avg','D':1.0): jabt = _xyz_to_jab_cam02ucs(xyzt, xyzw = xyzwt) jabr = _xyz_to_jab_cam02ucs(xyzr, xyzw = xyzwr) # calculate DEi, Rfi: DEi = (((jabt-jabr)**2).sum(axis=-1,keepdims=True)**0.5)[...,0] Rfi = log_scale(DEi, scale_factor = [6.73]) # calculate Rf DEa = DEi.mean(axis = 0,keepdims = True) Rf = log_scale(DEa, scale_factor = [6.73]) # calculate hue-bin data: hue_bin_data = _get_hue_bin_data(jabt, jabr, start_hue = 0, nhbins = 16) # calculate Rg: Rg = _hue_bin_data_to_rg(hue_bin_data) # calculate local color fidelity values, Rfhj, # local hue shift, Rhshj and local chroma shifts, Rcshj: Rcshj, Rhshj, Rfhj, DEhj = _hue_bin_data_to_Rxhj(hue_bin_data, scale_factor = [6.73]) # Fit ellipse to gamut shape of samples under test source: gamut_ellipse_fit = _hue_bin_data_to_ellipsefit(hue_bin_data) hue_bin_data['gamut_ellipse_fit'] = gamut_ellipse_fit # return output dict: return {'St' : St, 'Sr' : Sr, 'xyzw_cct' : xyzw_cct, 'xyzwt' : xyzwt, 'xyzwr' : xyzwr, 'xyzt' : xyzt, 'xyzr' : xyzr, 'cct': cct.T, 'duv': duv.T, 'jabt' : jabt, 'jabr' : jabr, 'DEi' : DEi, 'DEa' : DEa, 'Rfi' : Rfi, 'Rf' : Rf, 'hue_bin_data' : hue_bin_data, 'Rg' : Rg, 'DEhj' : DEhj, 'Rfhj' : Rfhj, 'Rcshj': Rcshj,'Rhshj':Rhshj, 'hue_bin_data' : hue_bin_data}
def test_spd_to_xyz(self, getvars): # check spd_to_xyz: S, spds, rfls, xyz, xyzw = getvars xyz = lx.spd_to_xyz(spds) xyz = lx.spd_to_xyz(spds, relative=False) xyz = lx.spd_to_xyz(spds, rfl=rfls) xyz, xyzw = lx.spd_to_xyz(spds, rfl=rfls, cieobs='1931_2', out=2)
def _setup_default_adaptation_field(dataw=None, Lw=100, cie_illuminant='D65', inputtype='xyz', relative=True, cieobs=_CAM_DEFAULT_CIEOBS): """ Setup a default illuminant adaptation field with Lw = 100 cd/m² for selected CIE observer. Args: :dataw: | None or ndarray, optional | Input tristimulus values or spectral data of white point. | None defaults to the use of the illuminant specified in :cie_illuminant:. :cie_illuminant: | 'D65', optional | String corresponding to one of the illuminants (keys) | in luxpy._CIE_ILLUMINANT | If ndarray, then use this one. | This is ONLY USED WHEN dataw is NONE !!! :Lw: | 100.0, optional | Luminance (cd/m²) of white point. :inputtype: | 'xyz' or 'spd', optional | Specifies the type of input: | tristimulus values or spectral data for the forward mode. :relative: | True or False, optional | True: xyz tristimulus values are relative (Yw = 100) :cieobs: | _CAM_DEFAULT_CIEOBS, optional | CMF set to use to perform calculations where spectral data | is involved (inputtype == 'spd'; dataw = None) | Other options: see luxpy._CMF['types'] Returns: :dataw: | Ndarray with default adaptation field data (spectral or xyz) Notes: For an example on the use, see code _simple_cam() (type: _simple_cam??) """ if (dataw is None): if cie_illuminant is None: cie_illuminant = 'D65' if isinstance(cie_illuminant, str): cie_illuminant = _CIE_ILLUMINANTS[cie_illuminant] dataw = cie_illuminant.copy() # get illuminant xyzw = spd_to_xyz(dataw, cieobs=cieobs, relative=False) # get abs. tristimulus values if relative == False: #input is expected to be absolute dataw[1:] = Lw * dataw[ 1:] / xyzw[:, 1:2] # dataw = Lw*dataw # make absolute else: dataw = dataw # make relative (Y=100) if inputtype == 'xyz': dataw = spd_to_xyz(dataw, cieobs=cieobs, relative=relative) return dataw
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 getvars(scope="module"): # get some spectral data: S = lx._CIE_ILLUMINANTS['F4'] spds = lx._IESTM30['S']['data'][:5] rfls = lx._IESTM30['R']['99']['5nm'][:6] xyz, xyzw = lx.spd_to_xyz(spds, rfl=rfls, cieobs='1931_2', out=2) return S, spds, rfls, xyz, xyzw
def _setup_default_adaptation_field(dataw=None, Lw=400, inputtype='xyz', relative=True, cieobs='2006_10'): """ Setup theh default illuminant C adaptation field with Lw = 400 cd/m² for selected CIE observer. Args: :dataw: | None or ndarray, optional | Input tristimulus values or spectral data of white point. | None defaults to the use of CIE illuminant C. :Lw: | 400.0, optional | Luminance (cd/m²) of white point. :inputtype: | 'xyz' or 'spd', optional | Specifies the type of input: | tristimulus values or spectral data for the forward mode. :relative: | True or False, optional | True: xyz tristimulus values are relative (Yw = 100) :cieobs: | '2006_10', optional | CMF set to use to perform calculations where spectral data is involved (inputtype == 'spd'; dataw = None) | Other options: see luxpy._CMF['types'] Returns: :dataw: | Ndarray with default adaptation field data (spectral or xyz) """ if (dataw is None): dataw = _CIE_ILLUMINANTS['C'].copy() # get illuminant C xyzw = spd_to_xyz(dataw, cieobs=cieobs, relative=False) # get abs. tristimulus values if relative == False: #input is expected to be absolute dataw[1:] = Lw * dataw[ 1:] / xyzw[:, 1:2] # dataw = Lw*dataw # make absolute else: dataw = dataw # make relative (Y=100) if inputtype == 'xyz': dataw = spd_to_xyz(dataw, cieobs=cieobs, relative=relative) return dataw
def normalize_to_Lw(Ill, Lw, cieobs, rflM): xyzw = lx.spd_to_xyz(Ill, cieobs=cieobs, relative=False) for i in range(Ill.shape[0] - 1): Ill[i + 1] = Lw * Ill[i + 1] / xyzw[i, 1] IllM = [] for i in range(Ill.shape[0] - 1): IllM.append(np.vstack((Ill1[0], Ill[i + 1] * rflM[1:, :]))) IllM = np.transpose(np.array(IllM), (1, 0, 2)) return Ill, IllM
def hsi_to_rgb(hsi, spd=None, cieobs=_CIEOBS, srgb=False, linear_rgb=False, CSF=None, wl=[380, 780, 1]): """ Convert HyperSpectral Image to rgb. Args: :hsi: | ndarray with hyperspectral image [M,N,L] :spd: | None, optional | ndarray with illumination spectrum :cieobs: | _CIEOBS, optional | CMF set to convert spectral data to xyz tristimulus values. :srgb: | False, optional | If False: Use xyz_to_srgb(spd_to_xyz(...)) to convert to srgb values | If True: use camera sensitivity functions. :linear_rgb: | False, optional | If False: use gamma = 2.4 in xyz_to_srgb, if False: use gamma = 1. :CSF: | None, optional | ndarray with camera sensitivity functions | If None: use Nikon D700 :wl: | [380,780,1], optional | Wavelength range and spacing or ndarray with wavelengths of HSI image. Returns: :rgb: | ndarray with rgb image [M,N,3] """ if spd is None: spd = _CIE_E.copy() wlr = getwlr(wl) spd = cie_interp(spd, wl, kind='linear') hsi_2d = np.reshape(hsi, (hsi.shape[0] * hsi.shape[1], hsi.shape[2])) if srgb: xyz = spd_to_xyz(spd, cieobs=cieobs, relative=True, rfl=np.vstack((wlr, hsi_2d))) gamma = 1 if linear_rgb else 2.4 rgb = xyz_to_srgb(xyz, gamma=gamma) / 255 else: if CSF is None: CSF = _CSF_NIKON_D700 rgb = rfl_to_rgb(hsi_2d, spd=spd, CSF=CSF, wl=wl) return np.reshape(rgb, (hsi.shape[0], hsi.shape[1], 3))
def test_cam15u(self, getvars): # cam15u: S, spds, rfls, xyz, xyzw = getvars xyz, xyzw = lx.spd_to_xyz(spds, rfl=rfls, cieobs='2006_10', out=2) qabw = lx.xyz_to_qabW_cam15u(xyzw, fov=10) qab = lx.xyz_to_qabW_cam15u(xyz, fov=10) xyzw_ = lx.qabW_cam15u_to_xyz(qabw, fov=10) xyz_ = lx.qabW_cam15u_to_xyz(qab, fov=10) assert np.isclose(xyz, xyz_).all() assert np.isclose(xyzw, xyzw_).all()
def test_cam_sww16(self, getvars): # cam_sww16: S, spds, rfls, xyz, xyzw = getvars xyz, xyzw = lx.spd_to_xyz(S, rfl=rfls, cieobs='2006_10', out=2) jabw = lx.xyz_to_lab_cam_sww16(xyzw, xyzw=xyzw.copy()) jab = lx.xyz_to_lab_cam_sww16(xyz, xyzw=xyzw) xyzw_ = lx.lab_cam_sww16_to_xyz(jabw, xyzw=xyzw) xyz_ = lx.lab_cam_sww16_to_xyz(jab, xyzw=xyzw) assert np.isclose(xyz, xyz_, atol=1e-1).all() assert np.isclose(xyzw, xyzw_, atol=1e-1).all()
def _plot_tm30_report_bottom(axh, spd, notes = '', max_len_notes_line = 40): """ Print some notes, the CIE x, y, u',v' and Ra, R9 values of the source in some empty axes. Args: :axh: | None, optional | Plot on specified axes. :spd: | ndarray or dict | If ndarray: single spectral power distribution. :notes: | string to be split :max_len_notes_line: | 40, optional | Maximum length of a single line when splitting the string. Returns: :axh: | handle to figure axes. """ ciera = spd_to_cri(spd, cri_type = 'ciera') cierai = spd_to_cri(spd, cri_type = 'ciera-14', out = 'Rfi') xyzw = spd_to_xyz(spd, cieobs = '1931_2', relative = True) Yxyw = xyz_to_Yxy(xyzw) Yuvw = xyz_to_Yuv(xyzw) notes_ = _split_notes(notes, max_len_notes_line = max_len_notes_line) axh.set_xticks(np.arange(10)) axh.set_xticklabels(['' for i in np.arange(10)]) axh.set_yticks(np.arange(4)) axh.set_yticklabels(['' for i in np.arange(4)]) axh.set_axis_off() axh.set_xlabel([]) axh.text(0,2.8, 'Notes: ', fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(0.75,2.8, notes_, fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,2.8, "x {:1.4f}".format(Yxyw[0,1]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,2.2, "y {:1.4f}".format(Yxyw[0,2]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,1.6, "u' {:1.4f}".format(Yuvw[0,1]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,1.0, "v' {:1.4f}".format(Yuvw[0,2]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,2.8, "CIE 13.3-1995", fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,2.2, " (CRI) ", fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,1.6, " $R_a$ {:1.0f}".format(ciera[0,0]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,1.0, " $R_9$ {:1.0f}".format(cierai[9,0]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') # Create a Rectangle patch rect = patches.Rectangle((7.2,0.5),1.7,2.5,linewidth=1,edgecolor='k',facecolor='none') # Add the patch to the Axes axh.add_patch(rect) return axh
def _spd_to_tm30(spd, cieobs = '1931_2', mixer_type = '3mixer'): # Call function that calculates ref.illuminant and jabt & jabr only once to obtain Rf & Rg: data = lx.cri._tm30_process_spd(spd, cri_type = _CRI_TYPE_TM30) # use 1nm samples to avoid interpolation Rf, Rg = data['Rf'], data['Rg'] thetad = data['hue_bin_data']['gamut_ellipse_fit']['thetad'] ecc = data['hue_bin_data']['gamut_ellipse_fit']['a/b'] xyzw = lx.spd_to_xyz(spd, cieobs = cieobs, relative = False) # set K = 1 to avoid overflow when _FLOAT_TYPE = np.float16 data['xyzw'] = xyzw return data
def plot_rfl_color_patches(rfl, spd=None, cieobs='1931_2', patch_shape=(100, 100), patch_layout=None, ax=None, show=True): """ Create (and plot) an image with colored patches representing a set of reflectance spectra illuminated by a specified illuminant. Args: :rfl: | ndarray with reflectance spectra :spd: | None, optional | ndarray with illuminant spectral power distribution | If None: _CIE_D65 is used. :cieobs: | '1931_2', optional | CIE standard observer to use when converting rfl to xyz. :patch_shape: | (100,100), optional | shape of each of the patches in the image :patch_layout: | None, optional | If None: layout is calculated automatically to give a 'good' aspect ratio :ax: | None, optional | Axes to plot the image in. If None: a new axes is created. :show: | True, optional | If True: plot image in axes and return axes handle; else: return ndarray with image. Return: :ax: or :imagae: | Axes is returned if show == True, else: ndarray with rgb image is returned. """ if spd is None: spd = _CIE_D65 xyz = spd_to_xyz(spd, rfl=rfl, cieobs=cieobs)[:, 0, :] rgb = xyz_to_srgb(xyz).astype('uint8') return plot_rgb_color_patches(rgb, ax=ax, patch_shape=patch_shape, patch_layout=patch_layout, show=show)
def lab_to_xyz(lab, xyzw = None, cieobs = _CIEOBS, **kwargs): """ Convert CIE 1976 L*a*b* (CIELAB) color coordinates to XYZ tristimulus values. Args: :lab: | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates :xyzw: | None or ndarray with tristimulus 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: :xyz: | ndarray with tristimulus values """ lab = np2d(lab) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'],cieobs = cieobs) # make xyzw same shape as data: xyzw = xyzw*np.ones(lab.shape) # set knee point of function: k=(24/116) #(24/116)**3**(1/3) # get L*, a*, b* and Xw, Yw, Zw: L,a,b = asplit(lab) Xw,Yw,Zw = asplit(xyzw) fy = (L + 16.0) / 116.0 fx = a / 500.0 + fy fz = fy - b/200.0 # apply 3rd power: X,Y,Z = [xw*(x**3.0) for (x,xw) in ((fx,Xw),(fy,Yw),(fz,Zw))] # Now calculate T where T/Tn is below the knee point: p,q,r = [np.where(x<k) for x in (fx,fy,fz)] X[p],Y[q],Z[r] = [np.squeeze(xw[xp]*((x[xp] - 16.0/116.0) / (841/108))) for (x,xw,xp) in ((fx,Xw,p),(fy,Yw,q),(fz,Zw,r))] return ajoin((X,Y,Z))
def xyz_to_lab(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert XYZ tristimulus values to CIE 1976 L*a*b* (CIELAB) coordinates. Args: :xyz: | ndarray with tristimulus values :xyzw: | None or ndarray with tristimulus 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: | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates """ xyz = np2d(xyz) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # get and normalize (X,Y,Z) to white point: XYZr = xyz / xyzw # Apply cube-root compression: fXYZr = XYZr**(1.0 / 3.0) # Check for T/Tn <= 0.008856: (Note (24/116)**3 = 0.008856) pqr = XYZr <= (24 / 116)**3 # calculate f(T) for T/Tn <= 0.008856: (Note:(1/3)*((116/24)**2) = 841/108 = 7.787) fXYZr[pqr] = ((841 / 108) * XYZr[pqr] + 16.0 / 116.0) # calculate L*, a*, b*: Lab = np.empty(xyz.shape) Lab[..., 0] = 116.0 * (fXYZr[..., 1]) - 16.0 Lab[pqr[..., 1], 0] = 903.3 * XYZr[pqr[..., 1], 1] Lab[..., 1] = 500.0 * (fXYZr[..., 0] - fXYZr[..., 1]) Lab[..., 2] = 200.0 * (fXYZr[..., 1] - fXYZr[..., 2]) return Lab
def luv_to_xyz(luv, xyzw = None, cieobs = _CIEOBS, **kwargs): """ Convert CIE 1976 L*u*v* (CIELUVB) coordinates to XYZ tristimulus values. Args: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates :xyzw: | None or ndarray with tristimulus 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: :xyz: | ndarray with tristimulus values """ luv = np2d(luv) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'],cieobs = cieobs) # Make xyzw same shape as luv: Yuvw = todim(xyz_to_Yuv(xyzw), luv.shape, equal_shape = True) # Get Yw, uw,vw: Yw,uw,vw = asplit(Yuvw) # calculate u'v' from u*,v*: L,u,v = asplit(luv) up,vp = [(x / (13*L)) + xw for (x,xw) in ((u,uw),(v,vw))] up[np.where(L == 0.0)] = 0.0 vp[np.where(L == 0.0)] = 0.0 fy = (L + 16.0) / 116.0 Y = Yw*(fy**3.0) p = np.where((Y/Yw) < ((6.0/29.0)**3.0)) Y[p] = Yw[p]*(L[p]/((29.0/3.0)**3.0)) return Yuv_to_xyz(ajoin((Y,up,vp)))
def lab_to_xyz(lab, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert CIE 1976 L*a*b* (CIELAB) color coordinates to XYZ tristimulus values. Args: :lab: | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates :xyzw: | None or ndarray with tristimulus 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: :xyz: | ndarray with tristimulus values """ lab = np2d(lab) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # make xyzw same shape as data: xyzw = xyzw * np.ones(lab.shape) # get L*, a*, b* and Xw, Yw, Zw: fXYZ = np.empty(lab.shape) fXYZ[..., 1] = (lab[..., 0] + 16.0) / 116.0 fXYZ[..., 0] = lab[..., 1] / 500.0 + fXYZ[..., 1] fXYZ[..., 2] = fXYZ[..., 1] - lab[..., 2] / 200.0 # apply 3rd power: xyz = (fXYZ**3.0) * xyzw # Now calculate T where T/Tn is below the knee point: pqr = fXYZ <= (24 / 116) #(24/116)**3**(1/3) xyz[pqr] = np.squeeze(xyzw[pqr] * ((fXYZ[pqr] - 16.0 / 116.0) / (841 / 108))) return xyz
def _get_daylightlocus_parameters(ccts, spds, cieobs): """ Get daylight locus parameters for a single cieobs from daylight phase spectra determined based on parameters for '1931_2' as reported in CIE15-20xx. """ # get xy coordinates for new cieobs: xyz = spd_to_xyz(spds, cieobs=cieobs) xy = xyz[..., :2] / xyz.sum(axis=-1, keepdims=True) # Fit 3e order polynomal xD(1/T) [4000 K < T <= 7000 K]: l7 = ccts <= 7000 pxT_l7 = np.polyfit((1000 / ccts[l7]), xy[l7, 0], 3) # Fit 3e order polynomal xD(1/T) [T > 7000 K]: L7 = ccts > 7000 pxT_L7 = np.polyfit((1000 / ccts[L7]), xy[L7, 0], 3) # Fit 2nd order polynomal yD(xD): pxy = np.round(np.polyfit(xy[:, 0], xy[:, 1], 2), 3) #pxy = np.hstack((0,pxy)) # make also 3e order for easy stacking return xy, pxy, pxT_l7, pxT_L7, l7, L7
def calculate_lut(ccts=None, cieobs=None, add_to_lut=True): """ Function that calculates LUT for the ccts stored in ./data/cctluts/cct_lut_cctlist.dat or given as input argument. Calculation is performed for CMF set specified in cieobs. Adds a new (temprorary) field to the _CCT_LUT dict. Args: :ccts: | ndarray or str, optional | list of ccts for which to (re-)calculate the LUTs. | If str, ccts contains path/filename.dat to list. :cieobs: | None or str, optional | str specifying cmf set. Returns: :returns: | ndarray with cct and duv. Note: Function changes the global variable: _CCT_LUT! """ if ccts is None: ccts = getdata('{}cct_lut_cctlist.dat'.format(_CCT_LUT_PATH)) elif isinstance(ccts, str): ccts = getdata(ccts) Yuv = np.ones((ccts.shape[0], 2)) * np.nan for i, cct in enumerate(ccts): Yuv[i, :] = xyz_to_Yuv( spd_to_xyz(blackbody(cct, wl3=[360, 830, 1]), cieobs=cieobs))[:, 1:3] u = Yuv[:, 0, None] # get CIE 1960 u v = (2.0 / 3.0) * Yuv[:, 1, None] # get CIE 1960 v cctuv = np.hstack((ccts, u, v)) if add_to_lut == True: _CCT_LUT[cieobs] = cctuv return cctuv
def xyz_to_luv(xyz, xyzw = None, cieobs = _CIEOBS, **kwargs): """ Convert XYZ tristimulus values to CIE 1976 L*u*v* (CIELUV) coordinates. Args: :xyz: | ndarray with tristimulus values :xyzw: | None or ndarray with tristimulus 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: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates """ xyz = np2d(xyz) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'],cieobs = cieobs) # make xyzw same shape as xyz: xyzw = todim(xyzw, xyz.shape) # Calculate u',v' of test and white: Y,u,v = asplit(xyz_to_Yuv(xyz)) Yw,uw,vw = asplit(xyz_to_Yuv(xyzw)) #uv1976 to CIELUV YdivYw = Y / Yw L = 116.0*YdivYw**(1.0/3.0) - 16.0 p = np.where(YdivYw <= (6.0/29.0)**3.0) L[p] = ((29.0/3.0)**3.0)*YdivYw[p] u = 13.0*L*(u-uw) v = 13.0*L*(v-vw) return ajoin((L,u,v))
def xyz_to_luv(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert XYZ tristimulus values to CIE 1976 L*u*v* (CIELUV) coordinates. Args: :xyz: | ndarray with tristimulus values :xyzw: | None or ndarray with tristimulus 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: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates """ xyz = np2d(xyz) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # Calculate u',v' of test and white: Yuv = xyz_to_Yuv(xyz) Yuvw = xyz_to_Yuv(todim(xyzw, xyz.shape)) # todim: make xyzw same shape as xyz #uv1976 to CIELUV luv = np.empty(xyz.shape) YdivYw = Yuv[..., 0] / Yuvw[..., 0] luv[..., 0] = 116.0 * YdivYw**(1.0 / 3.0) - 16.0 p = np.where(YdivYw <= (6.0 / 29.0)**3.0) luv[..., 0][p] = ((29.0 / 3.0)**3.0) * YdivYw[p] luv[..., 1] = 13.0 * luv[..., 0] * (Yuv[..., 1] - Yuvw[..., 1]) luv[..., 2] = 13.0 * luv[..., 0] * (Yuv[..., 2] - Yuvw[..., 2]) return luv
def luv_to_xyz(luv, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert CIE 1976 L*u*v* (CIELUVB) coordinates to XYZ tristimulus values. Args: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates :xyzw: | None or ndarray with tristimulus 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: :xyz: | ndarray with tristimulus values """ luv = np2d(luv) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # Make xyzw same shape as luv and convert to Yuv: Yuvw = todim(xyz_to_Yuv(xyzw), luv.shape, equal_shape=True) # calculate u'v' from u*,v*: Yuv = np.empty(luv.shape) Yuv[..., 1:3] = (luv[..., 1:3] / (13 * luv[..., 0])) + Yuvw[..., 1:3] Yuv[Yuv[..., 0] == 0, 1:3] = 0 Yuv[..., 0] = Yuvw[..., 0] * (((luv[..., 0] + 16.0) / 116.0)**3.0) p = np.where((Yuv[..., 0] / Yuvw[..., 0]) < ((6.0 / 29.0)**3.0)) Yuv[..., 0][p] = Yuvw[..., 0][p] * (luv[..., 0][p] / ((29.0 / 3.0)**3.0)) return Yuv_to_xyz(Yuv)
def spd_to_mcri(SPD, D=0.9, E=None, Yb=20.0, out='Rm', wl=None): """ Calculates the MCRI or Memory Color Rendition Index, Rm Args: :SPD: | ndarray with spectral data (can be multiple SPDs, first axis are the wavelengths) :D: | 0.9, optional | Degree of adaptation. :E: | None, optional | Illuminance in lux | (used to calculate La = (Yb/100)*(E/pi) to then calculate D | following the 'cat02' model). | If None: the degree is determined by :D: | If (:E: is not None) & (:Yb: is None): :E: is assumed to contain the adapting field luminance La (cd/m²). :Yb: | 20.0, optional | Luminance factor of background. (used when calculating La from E) | If None, E contains La (cd/m²). :out: | 'Rm' or str, optional | Specifies requested output (e.g. 'Rm,Rmi,cct,duv') :wl: | None, optional | Wavelengths (or [start, end, spacing]) to interpolate the SPDs to. | None: default to no interpolation Returns: :returns: | float or ndarray with MCRI Rm for :out: 'Rm' | Other output is also possible by changing the :out: str value. References: 1. `K.A.G. Smet, W.R. Ryckaert, M.R. Pointer, G. Deconinck, P. Hanselaer,(2012) “A memory colour quality metric for white light sources,” Energy Build., vol. 49, no. C, pp. 216–225. <http://www.sciencedirect.com/science/article/pii/S0378778812000837>`_ """ SPD = np2d(SPD) if wl is not None: SPD = spd(data=SPD, interpolation=_S_INTERP_TYPE, kind='np', wl=wl) # unpack metric default values: avg, catf, cieobs, cri_specific_pars, cspace, ref_type, rg_pars, sampleset, scale = [ _MCRI_DEFAULTS[x] for x in sorted(_MCRI_DEFAULTS.keys()) ] similarity_ai = cri_specific_pars['similarity_ai'] Mxyz2lms = cspace['Mxyz2lms'] scale_fcn = scale['fcn'] scale_factor = scale['cfactor'] sampleset = eval(sampleset) # A. calculate xyz: xyzti, xyztw = spd_to_xyz(SPD, cieobs=cieobs['xyz'], rfl=sampleset, out=2) if 'cct' in out.split(','): cct, duv = xyz_to_cct(xyztw, cieobs=cieobs['cct'], out='cct,duv', mode='lut') # B. perform chromatic adaptation to adopted whitepoint of ipt color space, i.e. D65: if catf is not None: Dtype_cat, F, Yb_cat, catmode_cat, cattype_cat, mcat_cat, xyzw_cat = [ catf[x] for x in sorted(catf.keys()) ] # calculate degree of adaptationn D: if E is not None: if Yb is not None: La = (Yb / 100.0) * (E / np.pi) else: La = E D = cat.get_degree_of_adaptation(Dtype=Dtype_cat, F=F, La=La) else: Dtype_cat = None # direct input of D if (E is None) and (D is None): D = 1.0 # set degree of adaptation to 1 ! if D > 1.0: D = 1.0 if D < 0.6: D = 0.6 # put a limit on the lowest D # apply cat: xyzti = cat.apply(xyzti, cattype=cattype_cat, catmode=catmode_cat, xyzw1=xyztw, xyzw0=None, xyzw2=xyzw_cat, D=D, mcat=[mcat_cat], Dtype=Dtype_cat) xyztw = cat.apply(xyztw, cattype=cattype_cat, catmode=catmode_cat, xyzw1=xyztw, xyzw0=None, xyzw2=xyzw_cat, D=D, mcat=[mcat_cat], Dtype=Dtype_cat) # C. convert xyz to ipt and split: ipt = xyz_to_ipt( xyzti, cieobs=cieobs['xyz'], M=Mxyz2lms ) #input matrix as published in Smet et al. 2012, Energy and Buildings I, P, T = asplit(ipt) # D. calculate specific (hue dependent) similarity indicators, Si: if len(xyzti.shape) == 3: ai = np.expand_dims(similarity_ai, axis=1) else: ai = similarity_ai a1, a2, a3, a4, a5 = asplit(ai) mahalanobis_d2 = (a3 * np.power((P - a1), 2.0) + a4 * np.power( (T - a2), 2.0) + 2.0 * a5 * (P - a1) * (T - a2)) if (len(mahalanobis_d2.shape) == 3) & (mahalanobis_d2.shape[-1] == 1): mahalanobis_d2 = mahalanobis_d2[:, :, 0].T Si = np.exp(-0.5 * mahalanobis_d2) # E. calculate general similarity indicator, Sa: Sa = avg(Si, axis=0, keepdims=True) # F. rescale similarity indicators (Si, Sa) with a 0-1 scale to memory color rendition indices (Rmi, Rm) with a 0 - 100 scale: Rmi = scale_fcn(np.log(Si), scale_factor=scale_factor) Rm = np2d(scale_fcn(np.log(Sa), scale_factor=scale_factor)) # G. calculate Rg (polyarea of test / polyarea of memory colours): if 'Rg' in out.split(','): I = I[ ..., None] #broadcast_shape(I, target_shape = None,expand_2d_to_3d = 0) a1 = a1[:, None] * np.ones( I.shape ) #broadcast_shape(a1, target_shape = None,expand_2d_to_3d = 0) a2 = a2[:, None] * np.ones( I.shape ) #broadcast_shape(a2, target_shape = None,expand_2d_to_3d = 0) a12 = np.concatenate( (a1, a2), axis=2 ) #broadcast_shape(np.hstack((a1,a2)), target_shape = ipt.shape,expand_2d_to_3d = 0) ipt_mc = np.concatenate((I, a12), axis=2) nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [ rg_pars[x] for x in sorted(rg_pars.keys()) ] Rg = jab_to_rg(ipt, ipt_mc, ordered_and_sliced=False, nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut) if (out != 'Rm'): return eval(out) else: return Rm
xyzw = lx.spd_to_xyz(Ill, cieobs=cieobs, relative=False) for i in range(Ill.shape[0] - 1): Ill[i + 1] = Lw * Ill[i + 1] / xyzw[i, 1] IllM = [] for i in range(Ill.shape[0] - 1): IllM.append(np.vstack((Ill1[0], Ill[i + 1] * rflM[1:, :]))) IllM = np.transpose(np.array(IllM), (1, 0, 2)) return Ill, IllM Ill1, Ill1M = normalize_to_Lw(Ill1, Lw, cieobs, rflM) Ill2, Ill2M = normalize_to_Lw(Ill2, Lw, cieobs, rflM) n = 6 xyz1, xyzw1 = lx.spd_to_xyz(Ill1, cieobs=cieobs, relative=True, rfl=rflM, out=2) xyz1 = xyz1[:n, 0, :] Ill1M = Ill1M[:(n + 1), 0, :] xyz2, xyzw2 = lx.spd_to_xyz(Ill2, cieobs=cieobs, relative=True, rfl=rflM, out=2) xyz2 = xyz2[:n, :, :] Ill2M = Ill2M[:(n + 1), :, :] # Single data for sample and illuminant: # test input to _simple_cam():
LMS_All_CatObs, var_age_CatObs, vAll_CatObs = getCatObs(n_cat=10, fieldsize=2, out=out) plt.figure() plt.plot(LMS_All_CatObs[0], LMS_All_CatObs[1], color='r', linestyle='-') plt.plot(LMS_All_CatObs[0], LMS_All_CatObs[2], color='g', linestyle='-') plt.plot(LMS_All_CatObs[0], LMS_All_CatObs[3], color='b', linestyle='-') plt.title('getCatObs(...)') plt.show() # XYZ_All_CatObs = lmsb_to_xyzb(LMS_All_CatObs, fieldsize = 3) # plt.figure() # plt.plot(wl[:,None],XYZ_All_CatObs[0,:,:], color ='r', linestyle='-') # plt.plot(wl[:,None],XYZ_All_CatObs[1,:,:], color ='g', linestyle='-') # plt.plot(wl[:,None],XYZ_All_CatObs[2,:,:], color ='b', linestyle='-') # plt.title('getCatObs XYZ') # plt.show() # Calculate new set of CMFs and calculate xyzw and cct, duv: from luxpy import spd_to_xyz, _CIE_ILLUMINANTS, xyz_to_cct_ohno XYZb_All_CatObs, _, _ = getCatObs(n_cat=1, fieldsize=10, out=out) add_to_cmf_dict(bar=XYZb_All_CatObs, cieobs='CatObs1', K=683) xyz2 = spd_to_xyz(_CIE_ILLUMINANTS['F4'], cieobs='1931_2') xyz1 = spd_to_xyz(_CIE_ILLUMINANTS['F4'], cieobs='CatObs1') cct2, duv2 = xyz_to_cct_ohno(xyz2, cieobs='1931_2', out='cct,duv') cct1, duv1 = xyz_to_cct_ohno(xyz1, cieobs='CatObs1', out='cct,duv') print('cct,duv using 1931_2: {:1.0f} K, {:1.4f}'.format( cct2[0, 0], duv2[0, 0])) print('cct,duv using CatObs1: {:1.0f} K, {:1.4f}'.format( cct1[0, 0], duv1[0, 0]))
def cct_to_xyz(ccts, duv=None, cieobs=_CIEOBS, wl=None, mode='lut', out=None, accuracy=0.1, force_out_of_lut=True, upper_cct_max=10.0 * 20, approx_cct_temp=True): """ Convert correlated color temperature (CCT) and Duv (distance above (>0) or below (<0) the Planckian locus) to XYZ tristimulus values. | Finds xyzw_estimated by minimization of: | | F = numpy.sqrt(((100.0*(cct_min - cct)/(cct))**2.0) | + (((duv_min - duv)/(duv))**2.0)) | | with cct,duv the input values and cct_min, duv_min calculated using | luxpy.xyz_to_cct(xyzw_estimated,...). Args: :ccts: | ndarray of cct values :duv: | None or ndarray of duv values, optional | Note that duv can be supplied together with cct values in :ccts: as ndarray with shape (N,2) :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :mode: | 'lut' or 'search', optional | Determines what method to use. :out: | None (or 1), optional | If not None or 1: output a ndarray that contains estimated xyz and minimization results: | (cct_min, duv_min, F_min (objective fcn value)) :wl: | None, optional | Wavelengths used when calculating Planckian radiators. :accuracy: | float, optional | Stop brute-force search when cct :accuracy: is reached. :upper_cct_max: | 10.0**20, optional | Limit brute-force search to this cct. :approx_cct_temp: | True, optional | If True: use xyz_to_cct_HA() to get a first estimate of cct to speed up search. :force_out_of_lut: | True, optional | If True and cct is out of range of the LUT, then switch to brute-force search method, else return numpy.nan values. Returns: :returns: | ndarray with estimated XYZ tristimulus values Note: If duv is not supplied (:ccts:.shape is (N,1) and :duv: is None), source is assumed to be on the Planckian locus. """ # make ccts a min. 2d np.array: if isinstance(ccts, list): ccts = np2dT(np.array(ccts)) else: ccts = np2d(ccts) if len(ccts.shape) > 2: raise Exception('cct_to_xyz(): Input ccts.shape must be <= 2 !') # get cct and duv arrays from :ccts: cct = np2d(ccts[:, 0, None]) if (duv is None) & (ccts.shape[1] == 2): duv = np2d(ccts[:, 1, None]) elif duv is not None: duv = np2d(duv) #get estimates of approximate xyz values in case duv = None: BB = cri_ref(ccts=cct, wl3=wl, ref_type=['BB']) xyz_est = spd_to_xyz(data=BB, cieobs=cieobs, out=1) results = np.ones([ccts.shape[0], 3]) * np.nan if duv is not None: # optimization/minimization setup: def objfcn(uv_offset, uv0, cct, duv, out=1): #, cieobs = cieobs, wl = wl, mode = mode): uv0 = np2d(uv0 + uv_offset) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) cct_min, duv_min = xyz_to_cct(Yuv_to_xyz(Yuv0), cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) F = np.sqrt(((100.0 * (cct_min[0] - cct[0]) / (cct[0]))**2.0) + (((duv_min[0] - duv[0]) / (duv[0]))**2.0)) if out == 'F': return F else: return np.concatenate((cct_min, duv_min, np2d(F)), axis=1) # loop through each xyz_est: for i in range(xyz_est.shape[0]): xyz0 = xyz_est[i] cct_i = cct[i] duv_i = duv[i] cct_min, duv_min = xyz_to_cct(xyz0, cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) if np.abs(duv[i]) > _EPS: # find xyz: Yuv0 = xyz_to_Yuv(xyz0) uv0 = Yuv0[0][1:3] OptimizeResult = minimize(fun=objfcn, x0=np.zeros((1, 2)), args=(uv0, cct_i, duv_i, 'F'), method='Nelder-Mead', options={ "maxiter": np.inf, "maxfev": np.inf, 'xatol': 0.000001, 'fatol': 0.000001 }) betas = OptimizeResult['x'] #betas = np.zeros(uv0.shape) if out is not None: results[i] = objfcn(betas, uv0, cct_i, duv_i, out=3) uv0 = np2d(uv0 + betas) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) xyz_est[i] = Yuv_to_xyz(Yuv0) else: xyz_est[i] = xyz0 if (out is None) | (out == 1): return xyz_est else: # Also output results of minimization: return np.concatenate((xyz_est, results), axis=1)
def xyz_to_cct_search(xyzw, cieobs=_CIEOBS, out='cct', wl=None, accuracy=0.1, upper_cct_max=10.0**20, approx_cct_temp=True): """ Convert XYZ tristimulus values to correlated color temperature (CCT) and Duv(distance above (> 0) or below ( < 0) the Planckian locus) by a brute-force search. | The algorithm uses an approximate cct_temp (HA approx., see xyz_to_cct_HA) as starting point or uses the middle of the allowed cct-range (1e2 K - 1e20 K, higher causes overflow) on a log-scale, then constructs a 4-step section of the blackbody (Planckian) locus on which to find the minimum distance to the 1960 uv chromaticity of the test source. Args: :xyzw: | ndarray of tristimulus values :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :out: | 'cct' (or 1), optional | Determines what to return. | Other options: 'duv' (or -1), 'cct,duv'(or 2), "[cct,duv]" (or -2) :wl: | None, optional | Wavelengths used when calculating Planckian radiators. :accuracy: | float, optional | Stop brute-force search when cct :accuracy: is reached. :upper_cct_max: | 10.0**20, optional | Limit brute-force search to this cct. :approx_cct_temp: | True, optional | If True: use xyz_to_cct_HA() to get a first estimate of cct to speed up search. Returns: :returns: | ndarray with: | cct: out == 'cct' (or 1) | duv: out == 'duv' (or -1) | cct, duv: out == 'cct,duv' (or 2) | [cct,duv]: out == "[cct,duv]" (or -2) Notes: This program is more accurate, but slower than xyz_to_cct_ohno! Note that cct must be between 1e3 K - 1e20 K (very large cct take a long time!!!) """ xyzw = np2d(xyzw) if len(xyzw.shape) > 2: raise Exception('xyz_to_cct_search(): Input xyzw.shape must be <= 2 !') # get 1960 u,v of test source: Yuvt = xyz_to_Yuv(np.squeeze( xyzw)) # remove possible 1-dim + convert xyzw to CIE 1976 u',v' #axis_of_v3t = len(Yuvt.shape)-1 # axis containing color components ut = Yuvt[:, 1, None] #.take([1],axis = axis_of_v3t) # get CIE 1960 u vt = (2 / 3) * Yuvt[:, 2, None] #.take([2],axis = axis_of_v3t) # get CIE 1960 v # Initialize arrays: ccts = np.ones((xyzw.shape[0], 1)) * np.nan duvs = ccts.copy() #calculate preliminary solution(s): if (approx_cct_temp == True): ccts_est = xyz_to_cct_HA(xyzw) procent_estimates = np.array([[3000.0, 100000.0, 0.05], [100000.0, 200000.0, 0.1], [200000.0, 300000.0, 0.25], [300000.0, 400000.0, 0.4], [400000.0, 600000.0, 0.4], [600000.0, 800000.0, 0.4], [800000.0, np.inf, 0.25]]) else: upper_cct = np.array(upper_cct_max) lower_cct = np.array(10.0**2) cct_scale_fun = lambda x: np.log10(x) cct_scale_ifun = lambda x: np.power(10.0, x) dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2 ccttemp = np.array([cct_scale_ifun(cct_scale_fun(lower_cct) + dT)]) ccts_est = np2d(ccttemp * np.ones((xyzw.shape[0], 1))) dT_approx_cct_False = dT.copy() # Loop through all ccts: for i in range(xyzw.shape[0]): #initialize CCT search parameters: cct = np.nan duv = np.nan ccttemp = ccts_est[i].copy() # Take care of (-1, NaN)'s from xyz_to_cct_HA signifying (CCT < lower, CCT > upper) bounds: approx_cct_temp_temp = approx_cct_temp if (approx_cct_temp == True): cct_scale_fun = lambda x: x cct_scale_ifun = lambda x: x if (ccttemp != -1) & ( np.isnan(ccttemp) == False ): # within validity range of CCT estimator-function for ii in range(procent_estimates.shape[0]): if (ccttemp >= (1.0 - 0.05 * (ii == 0)) * procent_estimates[ii, 0]) & ( ccttemp < (1.0 + 0.05 * (ii == 0)) * procent_estimates[ii, 1]): procent_estimate = procent_estimates[ii, 2] break dT = np.multiply( ccttemp, procent_estimate ) # determines range around CCTtemp (25% around estimate) or 100 K elif (ccttemp == -1) & (np.isnan(ccttemp) == False): ccttemp = np.array([procent_estimates[0, 0] / 2]) procent_estimate = 1 # cover 0 K to min_CCT of estimator dT = np.multiply(ccttemp, procent_estimate) elif (np.isnan(ccttemp) == True): upper_cct = np.array(upper_cct_max) lower_cct = np.array(10.0**2) cct_scale_fun = lambda x: np.log10(x) cct_scale_ifun = lambda x: np.power(10.0, x) dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2 ccttemp = np.array( [cct_scale_ifun(cct_scale_fun(lower_cct) + dT)]) approx_cct_temp = False else: dT = dT_approx_cct_False nsteps = 3 signduv = 1.0 ccttemp = ccttemp[0] delta_cct = dT while ((delta_cct > accuracy)): # keep converging on CCT #generate range of ccts: ccts_i = cct_scale_ifun( np.linspace( cct_scale_fun(ccttemp) - dT, cct_scale_fun(ccttemp) + dT, nsteps + 1)) ccts_i[ccts_i < 100.0] = 100.0 # avoid nan's in calculation # Generate BB: BB = cri_ref(ccts_i, wl3=wl, ref_type=['BB'], cieobs=cieobs) # Calculate xyz: xyz = spd_to_xyz(BB, cieobs=cieobs) # Convert to CIE 1960 u,v: Yuv = xyz_to_Yuv(np.squeeze( xyz)) # remove possible 1-dim + convert xyz to CIE 1976 u',v' #axis_of_v3 = len(Yuv.shape)-1 # axis containing color components u = Yuv[:, 1, None] # get CIE 1960 u v = (2.0 / 3.0) * Yuv[:, 2, None] # get CIE 1960 v # Calculate distance between list of uv's and uv of test source: dc = ((ut[i] - u)**2 + (vt[i] - v)**2)**0.5 if np.isnan(dc.min()) == False: #eps = _EPS q = dc.argmin() if np.size( q ) > 1: #to minimize calculation time: only calculate median when necessary cct = np.median(ccts[q]) duv = np.median(dc[q]) q = np.median(q) q = int(q) #must be able to serve as index else: cct = ccts_i[q] duv = dc[q] if (q == 0): ccttemp = cct_scale_ifun( np.array(cct_scale_fun([cct])) + 2 * dT / nsteps) #dT = 2.0*dT/nsteps continue # look in higher section of planckian locus if (q == np.size(ccts_i)): ccttemp = cct_scale_ifun( np.array(cct_scale_fun([cct])) - 2 * dT / nsteps) #dT = 2.0*dT/nsteps continue # look in lower section of planckian locus if (q > 0) & (q < np.size(ccts_i) - 1): dT = 2 * dT / nsteps # get Duv sign: d_p1m1 = ((u[q + 1] - u[q - 1])**2.0 + (v[q + 1] - v[q - 1])**2.0)**0.5 x = (dc[q - 1]**2.0 - dc[q + 1]**2.0 + d_p1m1**2.0) / 2.0 * d_p1m1 vBB = v[q - 1] + ((v[q + 1] - v[q - 1]) * (x / d_p1m1)) signduv = np.sign(vt[i] - vBB) #calculate difference with previous intermediate solution: delta_cct = abs(cct - ccttemp) ccttemp = np.array(cct) #%set new intermediate CCT approx_cct_temp = approx_cct_temp_temp else: ccttemp = np.nan cct = np.nan duv = np.nan duvs[i] = signduv * abs(duv) ccts[i] = cct # Regulate output: if (out == 'cct') | (out == 1): return np2d(ccts) elif (out == 'duv') | (out == -1): return np2d(duvs) elif (out == 'cct,duv') | (out == 2): return np2d(ccts), np2d(duvs) elif (out == "[cct,duv]") | (out == -2): return np.vstack((ccts, duvs)).T
def cam18sl(data, datab = None, Lb = [100], fov = 10.0, inputtype = 'xyz', direction = 'forward', outin = 'Q,aW,bW', parameters = None): """ Convert between CIE 2006 10° XYZ tristimulus values (or spectral data) and CAM18sl color appearance correlates. Args: :data: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data or color appearance attributes of stimulus :datab: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data of stimulus background :Lb: | [100], optional | Luminance (cd/m²) value(s) of background(s) calculated using the CIE 2006 10° CMFs | (only used in case datab == None and the background is assumed to be an Equal-Energy-White) :fov: | 10.0, optional | Field-of-view of stimulus (for size effect on brightness) :inputtpe: | 'xyz' or 'spd', optional | Specifies the type of input: | tristimulus values or spectral data for the forward mode. :direction: | 'forward' or 'inverse', optional | -'forward': xyz -> cam18sl | -'inverse': cam18sl -> xyz :outin: | 'Q,aW,bW' or str, optional | 'Q,aW,bW' (brightness and opponent signals for amount-of-neutral) | other options: 'Q,aM,bM' (colorfulness) and 'Q,aS,bS' (saturation) | Str specifying the type of | input (:direction: == 'inverse') and | output (:direction: == 'forward') :parameters: | None or dict, optional | Set of model parameters. | - None: defaults to luxpy.cam._CAM18SL_PARAMETERS | (see references below) Returns: :returns: | ndarray with color appearance correlates (:direction: == 'forward') | or | XYZ tristimulus values (:direction: == 'inverse') Notes: | * Instead of using the CIE 1964 10° CMFs in some places of the model, | the CIE 2006 10° CMFs are used througout, making it more self_consistent. | This has an effect on the k scaling factors (now different those in CAM15u) | and the illuminant E normalization for use in the chromatic adaptation transform. | (see future erratum to Hermans et al., 2018) | * The paper also used an equation for the amount of white W, which is | based on a Q value not expressed in 'bright' ('cA' = 0.937 instead of 123). | This has been corrected for in the luxpy version of the model, i.e. | _CAM18SL_PARAMETERS['cW'][0] has been changed from 2.29 to 1/11672. | (see future erratum to Hermans et al., 2018) References: 1. `Hermans, S., Smet, K. A. G., & Hanselaer, P. (2018). "Color appearance model for self-luminous stimuli." Journal of the Optical Society of America A, 35(12), 2000–2009. <https://doi.org/10.1364/JOSAA.35.002000>`_ """ if parameters is None: parameters = _CAM18SL_PARAMETERS outin = outin.split(',') #unpack model parameters: cA, cAlms, cHK, cM, cW, ca, calms, cb, cblms, cfov, k, naka, unique_hue_data = [parameters[x] for x in sorted(parameters.keys())] # precomputations: Mlms2xyz = np.linalg.inv(_CMF['2006_10']['M']) MAab = np.array([cAlms,calms,cblms]) invMAab = np.linalg.inv(MAab) #------------------------------------------------- # setup EEW reference field and default background field (Lr should be equal to Lb): # Get Lb values: if datab is not None: if inputtype != 'xyz': Lb = spd_to_xyz(datab, cieobs = '2006_10', relative = False)[...,1:2] else: Lb = datab[...,1:2] else: if isinstance(Lb,list): Lb = np2dT(Lb) # Setup EEW ref of same luminance as datab: if inputtype == 'xyz': wlr = getwlr(_CAM18SL_WL3) else: if datab is None: wlr = data[0] # use wlr of stimulus data else: wlr = datab[0] # use wlr of background data datar = np.vstack((wlr,np.ones((Lb.shape[0], wlr.shape[0])))) # create eew xyzr = spd_to_xyz(datar, cieobs = '2006_10', relative = False) # get abs. tristimulus values datar[1:] = datar[1:]/xyzr[...,1:2]*Lb # Create datab if None: if (datab is None): if inputtype != 'xyz': datab = datar.copy() else: datab = spd_to_xyz(datar, cieobs = '2006_10', relative = False) datar = datab.copy() # prepare data and datab for loop over backgrounds: # make axis 1 of datab have 'same' dimensions as data: if (data.ndim == 2): data = np.expand_dims(data, axis = 1) # add light source axis 1 if inputtype == 'xyz': if datab.shape[0] == 1: #make datab and datar have same lights source dimension (used to store different backgrounds) size as data datab = np.repeat(datab,data.shape[1],axis=0) datar = np.repeat(datar,data.shape[1],axis=0) else: if datab.shape[0] == 2: datab = np.vstack((datab[0],np.repeat(datab[1:], data.shape[1], axis = 0))) if datar.shape[0] == 2: datar = np.vstack((datar[0],np.repeat(datar[1:], data.shape[1], axis = 0))) # Flip light source/ background dim to axis 0: data = np.transpose(data, axes = (1,0,2)) #------------------------------------------------- #initialize camout: dshape = list(data.shape) dshape[-1] = len(outin) # requested number of correlates if (inputtype != 'xyz') & (direction == 'forward'): dshape[-2] = dshape[-2] - 1 # wavelength row doesn't count & only with forward can the input data be spectral camout = np.nan*np.ones(dshape) for i in range(data.shape[0]): # get rho, gamma, beta of background and reference white: if (inputtype != 'xyz'): xyzb = spd_to_xyz(np.vstack((datab[0], datab[i+1:i+2,:])), cieobs = '2006_10', relative = False) xyzr = spd_to_xyz(np.vstack((datar[0], datar[i+1:i+2,:])), cieobs = '2006_10', relative = False) else: xyzb = datab[i:i+1,:] xyzr = datar[i:i+1,:] lmsb = np.dot(_CMF['2006_10']['M'],xyzb.T).T # convert to l,m,s rgbb = (lmsb / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #lmsr = np.dot(_CMF['2006_10']['M'],xyzr.T).T # convert to l,m,s #rgbr = (lmsr / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #rgbr = rgbr/rgbr[...,1:2]*Lb[i] # calculated EEW cone excitations at same luminance values as background rgbr = np.ones(xyzr.shape)*Lb[i] # explicitely equal EEW cone excitations at same luminance values as background if direction == 'forward': # get rho, gamma, beta of stimulus: if (inputtype != 'xyz'): xyz = spd_to_xyz(data[i], cieobs = '2006_10', relative = False) elif (inputtype == 'xyz'): xyz = data[i] lms = np.dot(_CMF['2006_10']['M'],xyz.T).T # convert to l,m,s rgb = (lms / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta # apply von-kries cat with D = 1: if (rgbb == 0).any(): Mcat = np.eye(3) else: Mcat = np.diag((rgbr/rgbb)[0]) rgba = np.dot(Mcat,rgb.T).T # apply naka-rushton compression: rgbc = naka_rushton(rgba, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling']) #rgbc = np.ones(rgbc.shape)*rgbc.mean() # test if eew ends up at origin # calculate achromatic and color difference signals, A, a, b: Aab = np.dot(MAab, rgbc.T).T A,a,b = asplit(Aab) a = ca*a b = cb*b # calculate colorfullness like signal M: M = cM*((a**2.0 + b**2.0)**0.5) # calculate brightness Q: Q = cA*(A + cHK[0]*M**cHK[1]) # last term is contribution of Helmholtz-Kohlrausch effect on brightness # calculate saturation, s: s = M / Q # calculate amount of white, W: W = 1 / (1.0 + cW[0]*(s**cW[1])) # adjust Q for size (fov) of stimulus (matter of debate whether to do this before or after calculation of s or W, there was no data on s, M or W for different sized stimuli: after) Q = Q*(fov/10.0)**cfov # calculate hue, h and Hue quadrature, H: h = hue_angle(a,b, htype = 'deg') if 'H' in outin: H = hue_quadrature(h, unique_hue_data = unique_hue_data) else: H = None # calculate cart. co.: if 'aM' in outin: aM = M*np.cos(h*np.pi/180.0) bM = M*np.sin(h*np.pi/180.0) if 'aS' in outin: aS = s*np.cos(h*np.pi/180.0) bS = s*np.sin(h*np.pi/180.0) if 'aW' in outin: aW = W*np.cos(h*np.pi/180.0) bW = W*np.sin(h*np.pi/180.0) if (outin != ['Q','aW','bW']): camout[i] = eval('ajoin(('+','.join(outin)+'))') else: camout[i] = ajoin((Q,aW,bW)) elif direction == 'inverse': # get Q, M and a, b depending on input type: if 'aW' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref W = (a**2.0 + b**2.0)**0.5 s = (((1.0 / W) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q if 'aM' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref M = (a**2.0 + b**2.0)**0.5 if 'aS' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref s = (a**2.0 + b**2.0)**0.5 M = s*Q if 'h' in outin: Q, WsM, h = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref if 'W' in outin: s = (((1.0 / WsM) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q elif 's' in outin: M = WsM*Q elif 'M' in outin: M = WsM # calculate achromatic signal, A from Q and M: A = Q/cA - cHK[0]*M**cHK[1] # calculate hue angle: h = hue_angle(a,b, htype = 'rad') # calculate a,b from M and h: a = (M/cM)*np.cos(h) b = (M/cM)*np.sin(h) a = a/ca b = b/cb # create Aab: Aab = ajoin((A,a,b)) # calculate rgbc: rgbc = np.dot(invMAab, Aab.T).T # decompress rgbc to (adapted) rgba : rgba = naka_rushton(rgbc, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling'], direction = 'inverse') # apply inverse von-kries cat with D = 1: rgb = np.dot(np.diag((rgbb/rgbr)[0]),rgba.T).T # convert rgb to lms to xyz: lms = rgb/k*_CMF['2006_10']['K'] xyz = np.dot(Mlms2xyz,lms.T).T camout[i] = xyz if camout.shape[0] == 1: camout = np.squeeze(camout,axis = 0) return camout
| For help on parameter details: ?luxpy.cam.cam18sl """ return cam18sl(qab, datab = xyzb, Lb = Lb, fov = fov, direction = 'inverse', inputtype = 'xyz', outin = 'Q,aS,bS', parameters = parameters) #------------------------------------------------------------------------------ if __name__ == '__main__': C = _CIE_ILLUMINANTS['C'].copy() C = np.vstack((C,cie_interp(_CIE_ILLUMINANTS['D65'],C[0],kind='spd')[1:])) M = _MUNSELL.copy() rflM = M['R'] cieobs = '2006_10' # Normalize to Lw: Lw = 100 xyzw2 = spd_to_xyz(C, cieobs = cieobs, relative = False) for i in range(C.shape[0]-1): C[i+1] = Lw*C[i+1]/xyzw2[i,1] xyz, xyzw = spd_to_xyz(C, cieobs = cieobs, relative = True, rfl = rflM, out = 2) qab = xyz_to_qabW_cam18sl(xyzw, xyzb = None, Lb = [100], fov = 10.0) print('qab: ',qab) qab2 = cam18sl(C, datab = None, Lb = [100], fov = 10.0, direction = 'forward', inputtype = 'spd', outin = 'Q,aW,bW', parameters = None) print('qab2: ',qab2) xyz_ = qabW_cam18sl_to_xyz(qab, xyzb = None, Lb = [100], fov = 10.0) print('delta: ', xyzw-xyz_) # test 2: cieobs = '2006_10' Lb = np2d([100])
def _get_absolute_xyz_xyzw(data, dataw, i=0, Lw=100, direction='forward', cieobs=_CAM_DEFAULT_CIEOBS, inputtype='xyz', relative=True): """ Calculate absolute xyz tristimulus values of stimulus and white point from spectral input or convert relative xyz values to absolute ones. Args: :data: | ndarray with input tristimulus values | or spectral data | or input color appearance correlates | Can be of shape: (N [, xM], x 3), whereby: | N refers to samples and M refers to light sources. | Note that for spectral input shape is (N x (M+1) x wl) :dataw: | None or ndarray, optional | Input tristimulus values or spectral data of white point. | None defaults to the use of CIE illuminant C. :i: | 0, optional | row number in data and dataw ndarrays | (for loops across illuminant dimension after dimension reshape | with _massage_output_data_to_original_shape). :Lw: | 100.0, optional | Luminance (cd/m²) of white point. :inputtype: | 'xyz' or 'spd', optional | Specifies the type of input: | tristimulus values or spectral data for the forward mode. :direction: | 'forward' or 'inverse', optional | -'forward': xyz -> cam | -'inverse': cam -> xyz :relative: | True or False, optional | True: xyz tristimulus values are relative (Yw = 100) :cieobs: | _CAM_DEFAULT_CIEOBS, optional | CMF set to use to perform calculations where spectral data is involved (inputtype == 'spd'; dataw = None) | Other options: see luxpy._CMF['types'] Returns: :xyzti: | in forward mode : ndarray with relative or absolute sample xyz for data[i] | in inverse mode: None :xyzwi: | ndarray with relative or absolute white point for dataw[i] :xyzw_abs: | ndarray with absolute xyz for white point for dataw[i] Notes: For an example on the use, see code _simple_cam() (type: _simple_cam??) """ xyzw_abs = None # Spectral input: if (inputtype != 'xyz'): # make spectral data in `dataw` absolute: if relative == True: xyzw_abs = spd_to_xyz(dataw[i], cieobs=cieobs, relative=False) dataw[i, 1:, :] = Lw * dataw[i, 1:, :] / xyzw_abs[0, 1] # Calculate absolute xyzw: xyzwi = spd_to_xyz(dataw[i], cieobs=cieobs, relative=False) # make spectral data in `data` absolute: if (direction == 'forward' ): # no xyz data or spectra in data if == 'inverse'!!! if relative == True: data[i, 1:, :] = Lw * data[i, 1:, :] / xyzw_abs[0, 1] # Calculate absolute xyz of test field: xyzti = spd_to_xyz(data[i, ...], cieobs=cieobs, relative=False) else: xyzti = None # XYZ input: elif (inputtype == 'xyz'): # make xyz data in `dataw` absolute: if relative == True: xyzw_abs = dataw[i].copy() dataw[i] = Lw * dataw[i] / xyzw_abs[:, 1] xyzwi = dataw[i] if (direction == 'forward'): if relative == True: # make xyz data in `data` absolute: data[i] = Lw * data[i] / xyzw_abs[:, 1] # make absolute xyzti = data[i] else: xyzti = None # not needed in inverse model return xyzti, xyzwi, xyzw_abs