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}
Example #2
0
 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)
Example #3
0
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
Example #4
0
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
Example #5
0
 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
Example #6
0
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
Example #7
0
 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
Example #8
0
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))
Example #9
0
 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()
Example #10
0
 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()
Example #11
0
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
Example #13
0
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)
Example #14
0
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))
Example #15
0
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
Example #16
0
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)))
Example #17
0
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
Example #18
0
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
Example #19
0
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
Example #20
0
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))
Example #21
0
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
Example #22
0
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)
Example #23
0
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
Example #24
0
        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]))
Example #26
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)
Example #27
0
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
Example #28
0
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
Example #29
0
    | 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])
Example #30
0
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