def spd_to_aopicE(sid, Ee = None, E = None, Q = None, cieobs = _CIEOBS, sid_units = 'W/m2', out = 'Eeas,Eas'):
    """
    Calculate alpha-opic irradiance (Ee,α) and equivalent luminance (Eα) values
    for the l-cone, m-cone, s-cone, rod and iprgc (α) photoreceptor cells 
    following CIE technical note TN 003:2015.
    
    Args:
        :sid: 
            | numpy.ndarray with retinal spectral irradiance in :sid_units: 
            | (if 'uW/cm2', sid will be converted to SI units 'W/m2')
        :Ee: 
            | None, optional
            | If not None: normalize :sid: to an irradiance of :Ee:
        :E: 
            | None, optional
            | If not None: normalize :sid: to an illuminance of :E:
            | Note that E is calculate using a Km factor corrected to standard air.
        :Q: 
            | None, optional
            | If not None: nNormalize :sid: to a quantal energy of :Q:
        :cieobs:
            | _CIEOBS or str, optional
            | Type of cmf set to use for photometric units.
        :sid_units:
            | 'W/m2', optional
            | Other option 'uW/m2', input units of :sid:
        :out: 
            | 'Eeas, Eas' or str, optional
            | Determines values to return.
            
    Returns:
        :returns: 
            | (Eeas, Eas) with Eeas and Eas resp. numpy.ndarrays with the 
              α-opic irradiance and equivalent illuminance values 
              of all spectra in :sid: in SI-units. 
         
            | (other choice can be set using :out:)
    """
    outlist = out.split(',')
    
    # Convert to Watt/m²:
    if sid_units == 'uW/cm2':
        sid[1:] = sid[1:]/100

    elif sid_units == 'W/m2':
        pass
    else:
        raise Exception("spd_to_aopicE(): {} unsupported units for SID.".format(sid_units))
    
    
    # Normalize sid to Ee:
    if Ee is not None:
        sid = spd_normalize(sid, norm_type = 'ru', norm_f = Ee)  
    elif E is not None:
        sid = spd_normalize(sid, norm_type = 'pusa', norm_f = E) 
    elif Q is not None:
        sid = spd_normalize(sid, norm_type = 'qu', norm_f = Q) 
    
    
    # Get sid irradiance (W/m²):
    if 'Ee' in outlist:
        Ee = spd_to_power(sid, cieobs = cieobs, ptype = 'ru')
    
    # Get sid illuminance (lx):
    if 'E' in outlist:
        E = spd_to_power(sid, cieobs = cieobs, ptype = 'pusa') #photometric units (Km corrected to standard air)
    
    # Get sid quantal energy (photons/m²/s):
    if 'Q' in outlist:
        Q = spd_to_power(sid, cieobs = cieobs, ptype = 'qu')


    # get SI actinic action spectra, sa:
    sa = spd(_ACTIONSPECTRA, wl = sid[0], interpolation = 'cmf', norm_type = 'max')
    
    # get wavelength spacing:
    dl = getwld(sid[0])
    
    # Calculate all alpha-opics Ee's:
    Eeas = (np.dot((sa[1:]*dl),sid[1:].T)).T

    # Calculate equivalent alpha-opic E's:
    Vl, Km = vlbar(cieobs = cieobs, wl_new = sid[0], out = 2)
    Eas = Km*Km_correction_factor*Eeas*(Vl[1].sum()/sa[1:].sum(axis = 1))

    #Prepare output:
    if out == 'Eeas,Eas':
        return Eeas,Eas
    elif out == 'Eeas':
        return Eeas
    elif out == 'Eas':
        return Eas
    else:
        eval(out)
Beispiel #2
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
def cie2006cmfsEx(age = 32,fieldsize = 10, wl = None,\
                  var_od_lens = 0, var_od_macula = 0, \
                  var_od_L = 0, var_od_M = 0, var_od_S = 0,\
                  var_shft_L = 0, var_shft_M = 0, var_shft_S = 0,\
                  out = 'LMS', allow_negative_values = False):
    """
    Generate Individual Observer CMFs (cone fundamentals) 
    based on CIE2006 cone fundamentals and published literature 
    on observer variability in color matching and in physiological parameters.
    
    Args:
        :age: 
            | 32 or float or int, optional
            | Observer age
        :fieldsize:
            | 10, optional
            | Field size of stimulus in degrees (between 2° and 10°).
        :wl: 
            | None, optional
            | Interpolation/extraplation of :LMS: output to specified wavelengths.
            | None: output original _WL = np.array([390,780,5])
        :var_od_lens:
            | 0, optional
            | Std Dev. in peak optical density [%] of lens.
        :var_od_macula:
            | 0, optional
            | Std Dev. in peak optical density [%] of macula.
        :var_od_L:
            | 0, optional
            | Std Dev. in peak optical density [%] of L-cone.
        :var_od_M:
            | 0, optional
            | Std Dev. in peak optical density [%] of M-cone.
        :var_od_S:
            | 0, optional
            | Std Dev. in peak optical density [%] of S-cone.
        :var_shft_L:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of L-cone. 
        :var_shft_L:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of M-cone.  
        :var_shft_S:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of S-cone. 
        :out: 
            | 'LMS' or , optional
            | Determines output.
        :allow_negative_values:
            | False, optional
            | Cone fundamentals or color matching functions 
              should not have negative values.
            |     If False: X[X<0] = 0.
            
    Returns:
        :returns: 
            | - 'LMS' : ndarray with individual observer area-normalized 
            |           cone fundamentals. Wavelength have been added.
                
            | [- 'trans_lens': ndarray with lens transmission 
            |      (no wavelengths added, no interpolation)
            |  - 'trans_macula': ndarray with macula transmission 
            |      (no wavelengths added, no interpolation)
            |  - 'sens_photopig' : ndarray with photopigment sens. 
            |      (no wavelengths added, no interpolation)]
            
    References:
         1. `Asano Y, Fairchild MD, and Blondé L (2016). 
         Individual Colorimetric Observer Model. 
         PLoS One 11, 1–19. 
         <http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0145671>`_
        
         2. `Asano Y, Fairchild MD, Blondé L, and Morvan P (2016). 
         Color matching experiment for highlighting interobserver variability. 
         Color Res. Appl. 41, 530–539. 
         <https://onlinelibrary.wiley.com/doi/abs/10.1002/col.21975>`_
         
         3. `CIE, and CIE (2006). 
         Fundamental Chromaticity Diagram with Physiological Axes - Part I 
         (Vienna: CIE). 
         <http://www.cie.co.at/publications/fundamental-chromaticity-diagram-physiological-axes-part-1>`_ 
         
         4. `Asano's Individual Colorimetric Observer Model 
         <https://www.rit.edu/cos/colorscience/re_AsanoObserverFunctions.php>`_
    """
    fs = fieldsize
    rmd = _INDVCMF_DATA['rmd'].copy()
    LMSa = _INDVCMF_DATA['LMSa'].copy()
    docul = _INDVCMF_DATA['docul'].copy()

    # field size corrected macular density:
    pkOd_Macula = 0.485 * np.exp(-fs / 6.132) * (
        1 + var_od_macula / 100)  # varied peak optical density of macula
    corrected_rmd = rmd * pkOd_Macula

    # age corrected lens/ocular media density:
    if (age <= 60):
        correct_lomd = docul[:1] * (1 + 0.02 * (age - 32)) + docul[1:2]
    else:
        correct_lomd = docul[:1] * (1.56 + 0.0667 * (age - 60)) + docul[1:2]
    correct_lomd = correct_lomd * (1 + var_od_lens / 100
                                   )  # varied overall optical density of lens

    # Peak Wavelength Shift:
    wl_shifted = np.empty(LMSa.shape)
    wl_shifted[0] = _WL + var_shft_L
    wl_shifted[1] = _WL + var_shft_M
    wl_shifted[2] = _WL + var_shft_S

    LMSa_shft = np.empty(LMSa.shape)
    kind = 'cubic'
    LMSa_shft[0] = interpolate.interp1d(wl_shifted[0],
                                        LMSa[0],
                                        kind=kind,
                                        bounds_error=False,
                                        fill_value="extrapolate")(_WL)
    LMSa_shft[1] = interpolate.interp1d(wl_shifted[1],
                                        LMSa[1],
                                        kind=kind,
                                        bounds_error=False,
                                        fill_value="extrapolate")(_WL)
    LMSa_shft[2] = interpolate.interp1d(wl_shifted[2],
                                        LMSa[2],
                                        kind=kind,
                                        bounds_error=False,
                                        fill_value="extrapolate")(_WL)
    #    LMSa[2,np.where(_WL >= _WL_CRIT)] = 0 #np.nan # Not defined above 620nm
    #    LMSa_shft[2,np.where(_WL >= _WL_CRIT)] = 0

    ssw = np.hstack(
        (0, np.sign(np.diff(LMSa_shft[2, :]))
         ))  #detect poor interpolation (sign switch due to instability)
    LMSa_shft[2, np.where((ssw >= 0) & (_WL > 560))] = np.nan

    # corrected LMS (no age correction):
    pkOd_L = (0.38 + 0.54 * np.exp(-fs / 1.333)) * (
        1 + var_od_L / 100)  # varied peak optical density of L-cone
    pkOd_M = (0.38 + 0.54 * np.exp(-fs / 1.333)) * (
        1 + var_od_M / 100)  # varied peak optical density of M-cone
    pkOd_S = (0.30 + 0.45 * np.exp(-fs / 1.333)) * (
        1 + var_od_S / 100)  # varied peak optical density of S-cone

    alpha_lms = 0. * LMSa_shft
    alpha_lms[0] = 1 - 10**(-pkOd_L * (10**LMSa_shft[0]))
    alpha_lms[1] = 1 - 10**(-pkOd_M * (10**LMSa_shft[1]))
    alpha_lms[2] = 1 - 10**(-pkOd_S * (10**LMSa_shft[2]))

    # this fix is required because the above math fails for alpha_lms[2,:]==0
    alpha_lms[2, np.where(_WL >= _WL_CRIT)] = 0

    # Corrected to Corneal Incidence:
    lms_barq = alpha_lms * (10**(-corrected_rmd - correct_lomd)) * np.ones(
        alpha_lms.shape)

    # Corrected to Energy Terms:
    lms_bar = lms_barq * _WL

    # Set NaN values to zero:
    lms_bar[np.isnan(lms_bar)] = 0

    # normalized:
    LMS = 100 * lms_bar / np.nansum(lms_bar, axis=1, keepdims=True)

    # Output extra:
    trans_lens = 10**(-correct_lomd)
    trans_macula = 10**(-corrected_rmd)
    sens_photopig = alpha_lms * _WL

    # Add wavelengths:
    LMS = np.vstack((_WL, LMS))

    if ('xyz' in out.lower().split(',')):
        LMS = lmsb_to_xyzb(LMS,
                           fieldsize,
                           out='xyz',
                           allow_negative_values=allow_negative_values)
        out = out.replace('xyz', 'LMS').replace('XYZ', 'LMS')
    if ('lms' in out.lower().split(',')):
        out = out.replace('lms', 'LMS')

    # Interpolate/extrapolate:
    if wl is None:
        interpolation = None
    else:
        interpolation = 'cubic'
    LMS = spd(LMS, wl=wl, interpolation=interpolation, norm_type='area')

    if (out == 'LMS'):
        return LMS
    elif (out == 'LMS,trans_lens,trans_macula,sens_photopig'):
        return LMS, trans_lens, trans_macula, sens_photopig
    elif (out == 'LMS,trans_lens,trans_macula,sens_photopig,LMSa'):
        return LMS, trans_lens, trans_macula, sens_photopig, LMSa
    else:
        return eval(out)