コード例 #1
0
ファイル: utils.py プロジェクト: simongr2/luxpy
def hue_angle(a, b, htype='deg'):
    """
    Calculate positive hue angle (0°-360° or 0 - 2*pi rad.) from opponent signals a and b.
    
    Args:
        :a: 
            | ndarray of a-coordinates
        :b: 
            | ndarray of b-coordinates
        :htype: 
            | 'deg' or 'rad', optional
            |   - 'deg': hue angle between 0° and 360°
            |   - 'rad': hue angle between 0 and 2pi radians
    Returns:
        :returns:
            | ndarray of positive hue angles.
    """
    return math.positive_arctan(a, b, htype=htype)
コード例 #2
0
ファイル: helpers.py プロジェクト: simongr2/luxpy
def _simple_cam(
        data,
        dataw=None,
        Lw=100.0,
        relative=True,
        inputtype='xyz',
        direction='forward',
        cie_illuminant='D65',
        parameters={
            'cA': 1,
            'ca': np.array([1, -1, 0]),
            'cb': (1 / 3) * np.array([0.5, 0.5, -1]),
            'n': 1 / 3,
            'Mxyz2lms': _CMF['1931_2']['M'].copy()
        },
        cieobs='2006_10',
        match_to_conversionmatrix_to_cieobs=True):
    """
    An example CAM illustration the usage of the functions in luxpy.cam.helpers 
    
    | Note that this example uses NO chromatic adaptation 
    | and SIMPLE compression, opponent and correlate processing.
    | THIS IS ONLY FOR ILLUSTRATION PURPOSES !!!

    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:
        :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.
        :relative:
            | True or False, optional
            | True: data and dataw input is relative (i.e. Yw = 100)
        :parameters:
            | {'cA': 1, 'ca':np.array([1,-1,0]), 'cb':(1/3)*np.array([0.5,0.5,-1]),
            |  'n': 1/3, 'Mxyz2lms': _CMF['1931_2']['M'].copy()}
            | Dict with model parameters 
            | (For illustration purposes of match_conversionmatrix_to_cieobs, 
            |  the conversion matrix luxpy._CMF['1931_2']['M'] does NOT match
            |  the default observer specification of the input data in :cieobs: !!!)
        :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 
        :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']
        :match_conversionmatrix_to_cieobs:
            | True, optional
            | When changing to a different CIE observer, change the xyz_to_lms
            | matrix to the one corresponding to that observer. 
            | Set to False to keep the one in the parameter dict!
    
    Returns:
        :returns: 
            | ndarray with:
            | - color appearance correlates (:direction: == 'forward')
            |  or 
            | - XYZ tristimulus values (:direction: == 'inverse')
    """
    #--------------------------------------------------------------------------
    # Get model parameters:
    #--------------------------------------------------------------------------
    args = locals().copy(
    )  # gets all local variables (i.e. the function arguments)

    parameters = _update_parameter_dict(
        args,
        parameters=parameters,
        cieobs=cieobs,
        match_conversionmatrix_to_cieobs=match_to_conversionmatrix_to_cieobs,
        Mxyz2lms_whitepoint=np.array([[1, 1, 1]]))

    #unpack model parameters:
    (Mxyz2lms, cA, ca, cb,
     n) = [parameters[x] for x in sorted(parameters.keys())]

    #--------------------------------------------------------------------------
    # Setup default white point / adaptation field:
    #--------------------------------------------------------------------------
    dataw = _setup_default_adaptation_field(dataw=dataw,
                                            Lw=Lw,
                                            cie_illuminant='C',
                                            inputtype=inputtype,
                                            relative=relative,
                                            cieobs=cieobs)

    #--------------------------------------------------------------------------
    # Redimension input data to ensure most appropriate sizes
    # for easy and efficient looping and initialize output array:
    #--------------------------------------------------------------------------
    n_out = 5  # this example outputs 5 'correlates': J, a, b, C, h
    (data, dataw, camout,
     originalshape) = _massage_input_and_init_output(data,
                                                     dataw,
                                                     inputtype=inputtype,
                                                     direction=direction,
                                                     n_out=n_out)

    #--------------------------------------------------------------------------
    # Do precomputations needed for both the forward and inverse model,
    # and which do not depend on sample or light source data:
    #--------------------------------------------------------------------------
    # Create matrix with scale factors for L, M, S
    # for quick matrix multiplications to obtain neural signals:
    MAab = np.array([[cA, cA, cA], ca, cb])

    if direction == 'inverse':
        invMxyz2lms = np.linalg.inv(
            Mxyz2lms)  # Calculate the inverse lms-to-xyz conversion matrix
        invMAab = np.linalg.inv(
            MAab)  # Pre-calculate its inverse to avoid repeat in loop.

    #--------------------------------------------------------------------------
    # Apply forward/inverse model by looping over each row (=light source dim.)
    # in data:
    #--------------------------------------------------------------------------
    N = data.shape[0]
    for i in range(N):
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        #  START FORWARD MODE and common part of inverse mode
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        #-----------------------------------------------------------------------------
        # Get tristimulus values for stimulus field and white point for row i:
        #-----------------------------------------------------------------------------
        # Note that xyzt will contain a None in case of inverse mode !!!
        xyzt, xyzw, xyzw_abs = _get_absolute_xyz_xyzw(data,
                                                      dataw,
                                                      i=i,
                                                      Lw=Lw,
                                                      direction=direction,
                                                      cieobs=cieobs,
                                                      inputtype=inputtype,
                                                      relative=relative)

        #---------------------------------------------------------------------
        # stage 1 (white point): calculate lms values of white:
        #----------------------------------------------------------------------
        lmsw = np.dot(Mxyz2lms, xyzw.T).T

        #------------------------------------------------------------------
        # stage 2 (white): apply simple chromatic adaptation:
        #------------------------------------------------------------------
        lmsw_a = lmsw / lmsw

        #----------------------------------------------------------------------
        # stage 3 (white point): apply simple compression to lms values
        #----------------------------------------------------------------------
        lmsw_ac = lmsw_a**n

        #----------------------------------------------------------------------
        # stage 4 (white point): calculate achromatic A, and opponent signals a,b):
        #----------------------------------------------------------------------
        Aabw = np.dot(MAab, lmsw_ac.T).T

        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        # SPLIT CALCULATION STEPS IN FORWARD AND INVERSE MODES:
        #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        if direction == 'forward':
            #------------------------------------------------------------------
            # stage 1 (stimulus): calculate lms values
            #------------------------------------------------------------------
            lms = np.dot(Mxyz2lms, xyzt.T).T

            #------------------------------------------------------------------
            # stage 2 (stimulus): apply simple chromatic adaptation:
            #------------------------------------------------------------------
            lms_a = lms / lmsw

            #------------------------------------------------------------------
            # stage 3 (stimulus): apply simple compression to lms values
            #------------------------------------------------------------------
            lms_ac = lms_a**n

            #------------------------------------------------------------------
            # stage 3 (stimulus): calculate achromatic A, and opponent signals a,b:
            #------------------------------------------------------------------
            Aab = np.dot(MAab, lms_ac.T).T

            #------------------------------------------------------------------
            # stage 4 (stimulus): calculate J, C, h
            #------------------------------------------------------------------
            J = Aab[..., 0] / Aabw[..., 0]
            C = (Aab[..., 1]**2 + Aab[..., 2]**2)**0.5
            h = math.positive_arctan(Aab[..., 1], Aab[..., 2])

            # # stack together:
            camout[i] = np.vstack((J, Aab[..., 1], Aab[..., 2], C, h)).T

        #--------------------------------------
        # INVERSE MODE FROM PERCEPTUAL SIGNALS:
        #--------------------------------------
        elif direction == 'inverse':
            pass

    return _massage_output_data_to_original_shape(camout, originalshape)
コード例 #3
0
def initialize_VF_hue_angles(hx = None, Cxr = _VF_MAXR, \
                             cri_type = _VF_CRI_DEFAULT, \
                             modeltype = _VF_MODEL_TYPE,\
                             determine_hue_angles = _DETERMINE_HUE_ANGLES):
    """
    Initialize the hue angles that will be used to 'summarize' 
    the VF model fitting parameters.
    
    Args:       
        :hx: 
            | None or ndarray, optional
            | None defaults to Munsell H5 hues.
        :Cxr: 
            | _VF_MAXR, optional
        :cri_type: 
            | _VF_CRI_DEFAULT or str or dict, optional,
            | Cri_type parameters for cri and VF model.
        :modeltype:
            | _VF_MODEL_TYPE or 'M5' or 'M6', optional
            | Determines the type of polynomial model.
        :determine_hue_angles:
            | _DETERMINE_HUE_ANGLES or True or False, optional
            | True: determines the 10 primary / secondary Munsell hues ('5..').
            | Note that for 'M6', an additional 
            
    Returns:
        :pcolorshift: 
            | {'href': href,
            |           'Cref' : _VF_MAXR, 
            |           'sig' : _VF_SIG, 
            |           'labels' : list[str]}
    """
    
    ###########################################
    # Get Munsell H5 hues:
    ###########################################

    rflM = _MUNSELL['R']
    hn = _MUNSELL['H'] # all Munsell hues
    rH5 = np.where([_MUNSELL['H'][:,0][x][0]=='5' for x in range(_MUNSELL['H'][:,0].shape[0])])[0] #all Munsell H5 hues
    hns5 = np.unique(_MUNSELL['H'][rH5]) 
    #------------------------------------------------------------------------------
    # Determine Munsell hue angles in cam02ucs:
    pool = False  
    IllC = _CIE_ILLUMINANTS['C'] # for determining Munsell hue angles in cam02ucs
    outM = VF_colorshift_model(IllC, cri_type = cri_type, sampleset = rflM, vfcolor = 'g',pool = pool)
    #------------------------------------------------------------------------------
    if (determine_hue_angles == True) | (hx is None):
        # find samples at major Munsell hue angles:
        all_h5_Munsell_cam02ucs = np.ones(hns5.shape)
        Jabt_IllC = outM[0]['Jab']['Jabt']
        for i,v in enumerate(hns5):
            hm = np.where(hn == v)[0]
            all_h5_Munsell_cam02ucs[i] = math.positive_arctan([Jabt_IllC[hm,0,1].mean()],[Jabt_IllC[hm,0,2].mean()],htype = 'rad')[0]
        hx = all_h5_Munsell_cam02ucs
        

    #------------------------------------------------------------------------------
    # Setp color shift parameters:
    pcolorshift = {'href': hx,'Cref' : Cxr, 'sig' : _VF_SIG, 'labels' : hns5}
    return pcolorshift
コード例 #4
0
def plot_shift_data(data, fieldtype = 'vectorfield', scalef = _VF_MAXR, color = 'k', \
                    axtype = 'polar', ax = None, \
                    hbins = 10,  start_hue = 0.0, bin_labels = '#', plot_center_lines = True,  \
                    plot_axis_labels = False, plot_edge_lines = False, plot_bin_colors = True, \
                    force_CVG_layout = True):
     
    """
    Plots vector or circle fields generated by VFcolorshiftmodel() 
    or PXcolorshiftmodel().
     
    Args:
        :data: 
            | dict generated by VFcolorshiftmodel() or PXcolorshiftmodel()
            | Must contain 'fielddata'- key, which is a dict with possible keys:
            |     - key: 'vectorfield': ndarray with vector field data
            |     - key: 'circlefield': ndarray with circle field data
        :color: 
            | 'k', optional
            | Color for plotting the vector-fields.
        :axtype:
            | 'polar' or 'cart', optional
            | Make polar or Cartesian plot.
        :ax: 
            | None or 'new' or 'same', optional
            |   - None or 'new' creates new plot
            |   - 'same': continue plot on same axes.
            |   - axes handle: plot on specified axes.
        :hbins:
            | 16 or ndarray with sorted hue bin centers (°), optional
        :start_hue:
            | _VF_MAXR, optional
            | Scale factor for graphic.
        :plot_axis_labels:
            | False, optional
            | Turns axis ticks on/off (True/False).
        :bin_labels:
            | None or list[str] or '#', optional
            | Plots labels at the bin center hues.
            |   - None: don't plot.
            |   - list[str]: list with str for each bin. 
            |                (len(:bin_labels:) = :nhbins:)
            |   - '#': plots number.
        :plot_edge_lines:
            | True or False, optional
            | Plot grey bin edge lines with '--'.
        :plot_center_lines:
            | False or True, optional
            | Plot colored lines at 'center' of hue bin.
        :plot_bin_colors:
            | True, optional
            | Colorize hue-bins.
        :force_CVG_layout: 
            | False or True, optional
            | True: Force plot of basis of CVG.
    
    Returns:
        :returns:
            | figCVG, hax, cmap
        
            |   :figCVG: handle to CVG figure
            |   :hax: handle to CVG axes
            |   :cmap: list with rgb colors for hue bins 
            |          (for use in other plotting fcns)
   
    """
       
    # Plot basis of CVG:
    figCVG, hax, cmap = plot_hue_bins(hbins = hbins, axtype = axtype, ax = ax, plot_center_lines = plot_center_lines, plot_edge_lines = plot_edge_lines, plot_bin_colors = plot_bin_colors, scalef = scalef, force_CVG_layout = force_CVG_layout, bin_labels = bin_labels)
    
    # plot vector field:
    if data is not None:
        if fieldtype is not None:
            vf = data['fielddata'][fieldtype]
            if axtype == 'polar':
                if fieldtype == 'vectorfield':
                    vfrtheta = math.positive_arctan(vf['axr'], vf['bxr'],htype = 'rad')
                    vfrr = np.sqrt(vf['axr']**2 + vf['bxr']**2)
                    hax.quiver(vfrtheta, vfrr, vf['axt'] - vf['axr'], vf['bxt'] - vf['bxr'],  headlength=3,color = color,angles='uv', scale_units='y', scale = 2,linewidth = 0.5)
                else:
                    vfttheta = math.positive_arctan(vf['axt'], vf['bxt'],htype = 'rad')
                    vfrtheta = math.positive_arctan(vf['axr'], vf['bxr'],htype = 'rad')
                    vftr = np.sqrt(vf['axt']**2 + vf['bxt']**2)
                    dh = (math.angle_v1v2(np.hstack((vf['axt'],vf['bxt'])),np.hstack((vf['axr'],vf['bxr'])),htype='deg')[:,None]) #hue shift
                    dh = dh/np.nanmax(dh)
                    plt.set_cmap('jet')
                    hax.scatter(vfttheta, vftr, s = 100*dh, c = dh, linestyle = 'None', marker = 'o',norm = None)
                hax.set_ylim([0, 1.1*scalef])     
            else:
                if fieldtype == 'vectorfield':
                    hax.quiver(vf['axr'], vf['bxr'], vf['axt'] - vf['axr'], vf['bxt'] - vf['bxr'],  headlength=1,color = color,angles='uv', scale_units='xy', scale = 1,linewidth = 0.5)
                else:
                    hax.plot(vf['axr'], vf['bxr'], color = color, marker = '.',linestyle = 'None')
    
    return figCVG, hax, cmap
コード例 #5
0
def Ydlep_to_xyz(Ydlep,
                 cieobs=_CIEOBS,
                 xyzw=_COLORTF_DEFAULT_WHITE_POINT,
                 flip_axes=False,
                 **kwargs):
    """
    Convert Y, dominant (complementary) wavelength and excitation purity to XYZ
    tristimulus values.

    Args:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
        :xyzw: 
            | None or narray with tristimulus values of a single (!) native white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.
        :flip_axes:
            | False, optional
            | If True: flip axis 0 and axis 1 in Ydelep to increase speed of loop in function.
            |          (single xyzw with is not flipped!)
    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """

    Ydlep3 = np3d(Ydlep).copy().astype(np.float)

    # flip axis so that longest dim is on first axis  (save time in looping):
    if (Ydlep3.shape[0] < Ydlep3.shape[1]) & (flip_axes == True):
        axes12flipped = True
        Ydlep3 = Ydlep3.transpose((1, 0, 2))
    else:
        axes12flipped = False

    # convert xyzw to Yxyw:
    Yxyw = xyz_to_Yxy(xyzw)
    Yxywo = Yxyw.copy()

    # get spectrum locus Y,x,y and wavelengths:
    SL = _CMF[cieobs]['bar']
    wlsl = SL[0, None].T
    Yxysl = xyz_to_Yxy(SL[1:4].T)[:, None]

    # center on xyzw:
    Yxysl = Yxysl - Yxyw
    Yxyw = Yxyw - Yxyw

    #split:
    Y, dom, pur = asplit(Ydlep3)
    Yw, xw, yw = asplit(Yxyw)
    Ywo, xwo, ywo = asplit(Yxywo)
    Ysl, xsl, ysl = asplit(Yxysl)

    # loop over longest dim:
    x = np.empty(Y.shape)
    y = np.empty(Y.shape)
    for i in range(Ydlep3.shape[1]):

        # find closest wl's to dom:
        #wlslb,wlib = meshblock(wlsl,np.abs(dom[i,:])) #abs because dom<0--> complemtary wl
        wlib, wlslb = np.meshgrid(np.abs(dom[:, i]), wlsl)

        dwl = np.abs(wlslb - wlib)
        q1 = dwl.argmin(axis=0)  # index of closest wl
        dwl[q1] = 10000.0
        q2 = dwl.argmin(axis=0)  # index of second closest wl

        # calculate x,y of dom:
        x_dom_wl = xsl[q1, 0] + (xsl[q2, 0] - xsl[q1, 0]) * (
            np.abs(dom[:, i]) - wlsl[q1, 0]) / (wlsl[q2, 0] - wlsl[q1, 0]
                                                )  # calculate x of dom. wl
        y_dom_wl = ysl[q1, 0] + (ysl[q2, 0] - ysl[q1, 0]) * (
            np.abs(dom[:, i]) - wlsl[q1, 0]) / (wlsl[q2, 0] - wlsl[q1, 0]
                                                )  # calculate y of dom. wl

        # calculate x,y of test:
        d_wl = (x_dom_wl**2.0 +
                y_dom_wl**2.0)**0.5  # distance from white point to dom
        d = pur[:, i] * d_wl
        hdom = math.positive_arctan(x_dom_wl, y_dom_wl, htype='deg')
        x[:, i] = d * np.cos(hdom * np.pi / 180.0)
        y[:, i] = d * np.sin(hdom * np.pi / 180.0)

        # complementary:
        pc = np.where(dom[:, i] < 0.0)
        hdom[pc] = hdom[pc] - np.sign(dom[:, i][pc] -
                                      180.0) * 180.0  # get positive hue angle

        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x_dom_wl, y_dom_wl)).T
        xyw = np.vstack((xw, yw)).T
        xypl1 = np.vstack((xsl[0, None], ysl[0, None])).T
        xypl2 = np.vstack((xsl[-1, None], ysl[-1, None])).T
        da = (xy - xyw)
        db = (xypl2 - xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da, T)
        denom = np.sum(dap * db, axis=1, keepdims=True)
        num = np.sum(dap * dp, axis=1, keepdims=True)
        xy_linecross = (num / denom) * db + xypl1
        d_linecross = np.atleast_2d(
            (xy_linecross[:, 0]**2.0 + xy_linecross[:, 1]**2.0)**0.5).T[:, 0]
        x[:, i][pc] = pur[:, i][pc] * d_linecross[pc] * np.cos(
            hdom[pc] * np.pi / 180)
        y[:, i][pc] = pur[:, i][pc] * d_linecross[pc] * np.sin(
            hdom[pc] * np.pi / 180)
    Yxy = np.dstack((Ydlep3[:, :, 0], x + xwo, y + ywo))
    if axes12flipped == True:
        Yxy = Yxy.transpose((1, 0, 2))
    else:
        Yxy = Yxy.transpose((0, 1, 2))
    return Yxy_to_xyz(Yxy).reshape(Ydlep.shape)
コード例 #6
0
def xyz_to_Ydlep(xyz,
                 cieobs=_CIEOBS,
                 xyzw=_COLORTF_DEFAULT_WHITE_POINT,
                 flip_axes=False,
                 **kwargs):
    """
    Convert XYZ tristimulus values to Y, dominant (complementary) wavelength
    and excitation purity.

    Args:
        :xyz:
            | ndarray with tristimulus values
        :xyzw:
            | None or ndarray with tristimulus values of a single (!) native white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.
        :flip_axes:
            | False, optional
            | If True: flip axis 0 and axis 1 in Ydelep to increase speed of loop in function.
            |          (single xyzw with is not flipped!)
    Returns:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
    """

    xyz3 = np3d(xyz).copy().astype(np.float)

    # flip axis so that shortest dim is on axis0 (save time in looping):
    if (xyz3.shape[0] < xyz3.shape[1]) & (flip_axes == True):
        axes12flipped = True
        xyz3 = xyz3.transpose((1, 0, 2))
    else:
        axes12flipped = False

    # convert xyz to Yxy:
    Yxy = xyz_to_Yxy(xyz3)
    Yxyw = xyz_to_Yxy(xyzw)

    # get spectrum locus Y,x,y and wavelengths:
    SL = _CMF[cieobs]['bar']

    wlsl = SL[0]
    Yxysl = xyz_to_Yxy(SL[1:4].T)[:, None]

    # center on xyzw:
    Yxy = Yxy - Yxyw
    Yxysl = Yxysl - Yxyw
    Yxyw = Yxyw - Yxyw

    #split:
    Y, x, y = asplit(Yxy)
    Yw, xw, yw = asplit(Yxyw)
    Ysl, xsl, ysl = asplit(Yxysl)

    # calculate hue:
    h = math.positive_arctan(x, y, htype='deg')

    hsl = math.positive_arctan(xsl, ysl, htype='deg')

    hsl_max = hsl[0]  # max hue angle at min wavelength
    hsl_min = hsl[-1]  # min hue angle at max wavelength

    dominantwavelength = np.empty(Y.shape)
    purity = np.empty(Y.shape)
    for i in range(xyz3.shape[1]):

        # find index of complementary wavelengths/hues:
        pc = np.where(
            (h[:, i] >= hsl_max) & (h[:, i] <= hsl_min + 360.0)
        )  # hue's requiring complementary wavelength (purple line)
        h[:, i][pc] = h[:, i][pc] - np.sign(
            h[:, i][pc] - 180.0
        ) * 180.0  # add/subtract 180° to get positive complementary wavelength

        # find 2 closest hues in sl:
        #hslb,hib = meshblock(hsl,h[:,i:i+1])
        hib, hslb = np.meshgrid(h[:, i:i + 1], hsl)
        dh = np.abs(hslb - hib)
        q1 = dh.argmin(axis=0)  # index of closest hue
        dh[q1] = 1000.0
        q2 = dh.argmin(axis=0)  # index of second closest hue

        dominantwavelength[:, i] = wlsl[q1] + np.divide(
            np.multiply((wlsl[q2] - wlsl[q1]),
                        (h[:, i] - hsl[q1, 0])), (hsl[q2, 0] - hsl[q1, 0])
        )  # calculate wl corresponding to h: y = y1 + (y2-y1)*(x-x1)/(x2-x1)
        dominantwavelength[:, i][pc] = -dominantwavelength[:, i][
            pc]  #complementary wavelengths are specified by '-' sign

        # calculate excitation purity:
        x_dom_wl = xsl[q1, 0] + (xsl[q2, 0] - xsl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate x of dom. wl
        y_dom_wl = ysl[q1, 0] + (ysl[q2, 0] - ysl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate y of dom. wl
        d_wl = (x_dom_wl**2.0 +
                y_dom_wl**2.0)**0.5  # distance from white point to sl
        d = (x[:, i]**2.0 +
             y[:, i]**2.0)**0.5  # distance from white point to test point
        purity[:, i] = d / d_wl

        # correct for those test points that have a complementary wavelength
        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x[:, i], y[:, i])).T
        xyw = np.hstack((xw, yw))
        xypl1 = np.hstack((xsl[0, None], ysl[0, None]))
        xypl2 = np.hstack((xsl[-1, None], ysl[-1, None]))
        da = (xy - xyw)
        db = (xypl2 - xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da, T)
        denom = np.sum(dap * db, axis=1, keepdims=True)
        num = np.sum(dap * dp, axis=1, keepdims=True)
        xy_linecross = (num / denom) * db + xypl1
        d_linecross = np.atleast_2d(
            (xy_linecross[:, 0]**2.0 + xy_linecross[:, 1]**2.0)**0.5).T  #[0]
        purity[:, i][pc] = d[pc] / d_linecross[pc][:, 0]
    Ydlep = np.dstack((xyz3[:, :, 1], dominantwavelength, purity))

    if axes12flipped == True:
        Ydlep = Ydlep.transpose((1, 0, 2))
    else:
        Ydlep = Ydlep.transpose((0, 1, 2))
    return Ydlep.reshape(xyz.shape)
コード例 #7
0
def xyz_to_Ydlep(xyz,
                 cieobs=_CIEOBS,
                 xyzw=_COLORTF_DEFAULT_WHITE_POINT,
                 flip_axes=False,
                 SL_max_lambda=None,
                 **kwargs):
    """
    Convert XYZ tristimulus values to Y, dominant (complementary) wavelength
    and excitation purity.

    Args:
        :xyz:
            | ndarray with tristimulus values
        :xyzw:
            | None or ndarray with tristimulus values of a single (!) native white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.
        :flip_axes:
            | False, optional
            | If True: flip axis 0 and axis 1 in Ydelep to increase speed of loop in function.
            |          (single xyzw with is not flipped!)
        :SL_max_lambda:
            | None or float, optional
            | Maximum wavelength of spectrum locus before it turns back on itelf in the high wavelength range (~700 nm)
    Returns:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
            |  and excitation purity
    """

    xyz3 = np3d(xyz).copy().astype(np.float)

    # flip axis so that shortest dim is on axis0 (save time in looping):
    if (xyz3.shape[0] < xyz3.shape[1]) & (flip_axes == True):
        axes12flipped = True
        xyz3 = xyz3.transpose((1, 0, 2))
    else:
        axes12flipped = False

    # convert xyz to Yxy:
    Yxy = xyz_to_Yxy(xyz3)
    Yxyw = xyz_to_Yxy(xyzw)

    # get spectrum locus Y,x,y and wavelengths:
    SL = _CMF[cieobs]['bar']
    SL = SL[:, SL[1:].sum(axis=0) >
            0]  # avoid div by zero in xyz-to-Yxy conversion
    wlsl = SL[0]
    Yxysl = xyz_to_Yxy(SL[1:4].T)[:, None]

    # Get maximum wavelength of spectrum locus (before it turns back on itself)
    if SL_max_lambda is None:
        pmaxlambda = Yxysl[..., 1].argmax()  # lambda with largest x value
        dwl = np.diff(
            Yxysl[:, 0,
                  1])  # spectrumlocus in that range should have increasing x
        dwl[wlsl[:-1] < 600] = 10000
        pmaxlambda = np.where(
            dwl <= 0)[0][0]  # Take first element with zero or <zero slope
    else:
        pmaxlambda = np.abs(wlsl - SL_max_lambda).argmin()
    Yxysl = Yxysl[:(pmaxlambda + 1), :]
    wlsl = wlsl[:(pmaxlambda + 1)]

    # center on xyzw:
    Yxy = Yxy - Yxyw
    Yxysl = Yxysl - Yxyw
    Yxyw = Yxyw - Yxyw

    #split:
    Y, x, y = asplit(Yxy)
    Yw, xw, yw = asplit(Yxyw)
    Ysl, xsl, ysl = asplit(Yxysl)

    # calculate hue:
    h = math.positive_arctan(x, y, htype='deg')

    hsl = math.positive_arctan(xsl, ysl, htype='deg')

    hsl_max = hsl[0]  # max hue angle at min wavelength
    hsl_min = hsl[-1]  # min hue angle at max wavelength
    if hsl_min < hsl_max: hsl_min += 360

    dominantwavelength = np.empty(Y.shape)
    purity = np.empty(Y.shape)

    for i in range(xyz3.shape[1]):

        # find index of complementary wavelengths/hues:
        pc = np.where(
            (h[:, i] > hsl_max) & (h[:, i] < hsl_min)
        )  # hue's requiring complementary wavelength (purple line)
        h[:, i][pc] = h[:, i][pc] - np.sign(
            h[:, i][pc] - 180.0
        ) * 180.0  # add/subtract 180° to get positive complementary wavelength

        # find 2 closest enclosing hues in sl:
        #hslb,hib = meshblock(hsl,h[:,i:i+1])
        hib, hslb = np.meshgrid(h[:, i:i + 1], hsl)
        dh = (hslb - hib)
        q1 = np.abs(dh).argmin(axis=0)  # index of closest hue
        sign_q1 = np.sign(dh[q1])[0]
        dh[np.sign(dh) ==
           sign_q1] = 1000000  # set all dh on the same side as q1 to a very large value
        q2 = np.abs(dh).argmin(
            axis=0)  # index of second  closest (enclosing) hue

        # # Test changes to code:
        # print('wls',i, wlsl[q1],wlsl[q2])
        # import matplotlib.pyplot as plt
        # plt.figure()
        # plt.plot(wlsl[:-1],np.diff(xsl[:,0]),'k.-')
        # plt.figure()
        # plt.plot(x[0,i],y[0,i],'k.'); plt.plot(xsl,ysl,'r.-');plt.plot(xsl[q1],ysl[q1],'b.');plt.plot(xsl[q2],ysl[q2],'g.');plt.plot(xsl[-1],ysl[-1],'c+')

        dominantwavelength[:, i] = wlsl[q1] + np.multiply(
            (h[:, i] - hsl[q1, 0]),
            np.divide((wlsl[q2] - wlsl[q1]), (hsl[q2, 0] - hsl[q1, 0]))
        )  # calculate wl corresponding to h: y = y1 + (x-x1)*(y2-y1)/(x2-x1)
        dominantwavelength[:, i][pc] = -dominantwavelength[:, i][
            pc]  #complementary wavelengths are specified by '-' sign

        # calculate excitation purity:
        x_dom_wl = xsl[q1, 0] + (xsl[q2, 0] - xsl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate x of dom. wl
        y_dom_wl = ysl[q1, 0] + (ysl[q2, 0] - ysl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate y of dom. wl
        d_wl = (x_dom_wl**2.0 +
                y_dom_wl**2.0)**0.5  # distance from white point to sl
        d = (x[:, i]**2.0 +
             y[:, i]**2.0)**0.5  # distance from white point to test point
        purity[:, i] = d / d_wl

        # correct for those test points that have a complementary wavelength
        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x[:, i], y[:, i])).T
        xyw = np.hstack((xw, yw))
        xypl1 = np.hstack((xsl[0, None], ysl[0, None]))
        xypl2 = np.hstack((xsl[-1, None], ysl[-1, None]))
        da = (xy - xyw)
        db = (xypl2 - xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da, T)
        denom = np.sum(dap * db, axis=1, keepdims=True)
        num = np.sum(dap * dp, axis=1, keepdims=True)
        xy_linecross = (num / denom) * db + xypl1
        d_linecross = np.atleast_2d(
            (xy_linecross[:, 0]**2.0 + xy_linecross[:, 1]**2.0)**0.5).T  #[0]
        purity[:, i][pc] = d[pc] / d_linecross[pc][:, 0]
    Ydlep = np.dstack((xyz3[:, :, 1], dominantwavelength, purity))

    if axes12flipped == True:
        Ydlep = Ydlep.transpose((1, 0, 2))
    else:
        Ydlep = Ydlep.transpose((0, 1, 2))
    return Ydlep.reshape(xyz.shape)
コード例 #8
0
def _tm30_process_spd(spd, cri_type = 'ies-tm30',**kwargs):
    """
    Calculate all required parameters for plotting from spd using cri.spd_to_cri()
    
    Args:
        :spd:
            | ndarray or dict
            | If ndarray: single spectral power distribution.
            | If dict: dictionary with pre-computed parameters.
            |  required keys:
            |   'Rf','Rg','cct','duv','Sr','cri_type','xyzri','xyzrw',
            |   'hbinnrs','Rfi','Rfhi','Rcshi','Rhshi',
            |   'jabt_binned','jabr_binned',
            |   'nhbins','start_hue','normalize_gamut','normalized_chroma_ref'
            | see cri.spd_to_cri() for more info on parameters.
        :cri_type:
            | _CRI_TYPE_DEFAULT or str or dict, optional
            |   -'str: specifies dict with default cri model parameters 
            |     (for supported types, see luxpy.cri._CRI_DEFAULTS['cri_types'])
            |   - dict: user defined model parameters 
            |     (see e.g. luxpy.cri._CRI_DEFAULTS['cierf'] 
            |     for required structure)
            | Note that any non-None input arguments (in kwargs) 
            | to the function will override default values in cri_type dict.
        :kwargs:
            | Additional optional keyword arguments, 
            | the same as in cri.spd_to_cri()
            
    Returns:
        :data:
            | dictionary with required parameters for plotting functions.      
    """
    out = 'Rf,Rg,cct,duv,Sr,cri_type,xyzri,xyzrw,binnrs,Rfi,Rfhi,Rcshi,Rhshi,jabt_binned,jabr_binned,nhbins,start_hue,normalize_gamut,normalized_chroma_ref'
    if not isinstance(spd,dict):
        tpl = spd_to_cri(spd, cri_type = cri_type, out = out, **kwargs)
        data = {'spd':spd}
        for i,key in enumerate(out.split(',')):
            if key == 'normalized_chroma_ref': key = 'scalef' # rename
            if key == 'binnrs': key = 'hbinnrs' # rename
            data[key] = tpl[i]
            
        # Normalize chroma to scalef and fit ellipse to gamut:
        scalef = data['scalef']
        jabt = data['jabt_binned'].copy()
        jabr = data['jabr_binned'].copy()
        Cr = (jabr[...,1]**2 + jabr[...,2]**2)**0.5
        Ct = ((jabt[...,1]**2 + jabt[...,2]**2)**0.5)/Cr*scalef
        ht = math.positive_arctan(jabt[...,1],jabt[...,2], htype = 'rad')
        hr = math.positive_arctan(jabr[...,1],jabr[...,2], htype = 'rad')
        jabt[...,1] = Ct*np.cos(ht)
        jabt[...,2] = Ct*np.sin(ht)
        jabr[...,1] = scalef*np.cos(hr)
        jabr[...,2] = scalef*np.sin(hr) 
        ecc = np.ones((1,jabt.shape[1]))*np.nan
        theta = np.ones((1,jabt.shape[1]))*np.nan
        v = np.ones((jabt.shape[1],5))*np.nan
        data['hue_bin_data'] = {'jabt_hj_closed':data['jabt_binned'],'jabr_hj_closed':data['jabr_binned'],
                                'jabtn_hj_closed':jabt,'jabrn_hj_closed':jabr}
        for i in range(jabt.shape[1]):
            try:
                v[i,:] = math.fit_ellipse(jabt[:,i,1:])
                a,b = v[i,0], v[i,1] # major and minor ellipse axes
                ecc[0,i] = a/b
                theta[0,i] = np.rad2deg(v[i,4]) # orientation angle
                if theta[0,i]>180: theta[0,i] -= 180
            except:
                v[i,:] = np.nan*np.ones((1,5))
                ecc[0,i] = np.nan
                theta[0,i] = np.nan # orientation angle
        data['gamut_ellipse_fit'] = {'v':v, 'a/b':ecc,'thetad': theta}
    else:
        data = spd
    return data
コード例 #9
0
def plot_tm30_cvg(spd, cri_type = 'ies-tm30',  
                  gamut_line_color = 'r',
                  gamut_line_style = '-',
                  gamut_line_marker = 'o',
                  gamut_line_label = None,
                  plot_vectors = True,
                  plot_index_values = True,
                  axh = None, axtype = 'cart',
                  **kwargs):
    """
    Plot TM30 Color Vector Graphic (CVG).
    
    Args:
        :spd:
            | ndarray or dict
            | If ndarray: single spectral power distribution.
            | If dict: dictionary with pre-computed parameters (using _tm30_process_spd()).
            |  required keys:
            |   'Rf','Rg','cct','duv','Sr','cri_type','xyzri','xyzrw',
            |   'hbinnrs','Rfi','Rfhi','Rcshi','Rhshi',
            |   'jabt_binned','jabr_binned',
            |   'nhbins','start_hue','normalize_gamut','normalized_chroma_ref'
            | see cri.spd_to_cri() for more info on parameters.
        :cri_type:
            | _CRI_TYPE_DEFAULT or str or dict, optional
            |   -'str: specifies dict with default cri model parameters 
            |     (for supported types, see luxpy.cri._CRI_DEFAULTS['cri_types'])
            |   - dict: user defined model parameters 
            |     (see e.g. luxpy.cri._CRI_DEFAULTS['cierf'] 
            |     for required structure)
            | Note that any non-None input arguments (in kwargs) 
            | to the function will override default values in cri_type dict.
        :gamut_line_color:
            | 'r', optional
            | Plotting line style for the line connecting the 
            | average test chromaticity in the hue bins.
        :gamut_line_style:
            | 'r', optional
            | Plotting color for the line connecting the 
            | average test chromaticity in the hue bins.
        :gamut_line_marker:
            | '-', optional
            | Markers to plot the test color gamut points for each hue bin in 
            | (only used when plot_vectors = False).
        :gamut_line_label:
            | None, optional
            | Label for gamut line. (only used when plot_vectors = False).
        :plot_vectors:
            | True, optional
            | Plot color shift vectors in CVG (True) or not (False).
        :plot_index_values:
            | True, optional
            | Print Rf, Rg, CCT and Duv in corners of CVG (True) or not (False).
        :axh: 
            | None, optional
            | If None: create new figure with single axes, else plot on specified axes. 
        :axtype: 
            | 'cart' (or 'polar'), optional
            | Make Cartesian (default) or polar plot.    
        :kwargs:
            | Additional optional keyword arguments, 
            | the same as in cri.spd_to_cri()
            
    Returns:
        :axh: 
            | handle to figure axes.
        :data:
            | dictionary with required parameters for plotting functions. 
    """

    data = _tm30_process_spd(spd, cri_type = 'ies-tm30',**kwargs)
    
    # Normalize chroma to scalef:
    scalef = data['scalef']
    jabt = data['jabt_binned'][:,0,:]
    jabr = data['jabr_binned'][:,0,:]
    Cr = (jabr[...,1]**2 + jabr[...,2]**2)**0.5
    Ct = ((jabt[...,1]**2 + jabt[...,2]**2)**0.5)/Cr*scalef
    ht = math.positive_arctan(jabt[...,1],jabt[...,2], htype = 'rad')
    hr = math.positive_arctan(jabr[...,1],jabr[...,2], htype = 'rad')
    jabt[...,1] = Ct*np.cos(ht)
    jabt[...,2] = Ct*np.sin(ht)
    jabr[...,1] = scalef*np.cos(hr)
    jabr[...,2] = scalef*np.sin(hr)
    
    # Plot color vector graphic
    _, axh, _ = plot_ColorVectorGraphic(jabt = jabt, jabr = jabr, 
                                        hbins = data['nhbins'], start_hue = data['start_hue'], 
                                        bin_labels = '',
                                        gamut_line_color = gamut_line_color,
                                        gamut_line_style = gamut_line_style,
                                        gamut_line_marker = gamut_line_marker,
                                        gamut_line_label = gamut_line_label,
                                        plot_vectors = plot_vectors,
                                        ax = axh, axtype = axtype,
                                        force_CVG_layout = True,
                                        plot_axis_labels = False)
    
    # Print Rf, Rg, CCT and Duv in plot:
    if plot_index_values == True:
        Rf, Rg, cct, duv = data['Rf'], data['Rg'], data['cct'], data['duv']
        axh.text(-1.30*scalef,1.30*scalef,'{:1.0f}'.format(Rf[0,0]),fontsize = 15, fontweight='bold', horizontalalignment='center',verticalalignment='center',color = 'k')
        axh.text(-1.33*scalef,1.12*scalef,'$R_f$',fontsize = 13, style='italic', horizontalalignment='center',verticalalignment='center',color = 'k')
        axh.text(1.30*scalef,1.30*scalef,'{:1.0f}'.format(Rg[0,0]),fontsize = 15, fontweight='bold', horizontalalignment='center',verticalalignment='center',color = 'k')
        axh.text(1.33*scalef,1.12*scalef,'$R_g$',fontsize = 13, style='italic', horizontalalignment='center',verticalalignment='center',color = 'k')
        axh.text(-1.43*scalef,-1.45*scalef,'{:1.0f}'.format(cct[0,0]),fontsize = 15, fontweight='bold', horizontalalignment='left',verticalalignment='bottom',color = 'k')
        axh.text(-1.43*scalef,-1.25*scalef,'$CCT$',fontsize = 13, style='italic', horizontalalignment='left',verticalalignment='bottom',color = 'k')
        axh.text(1.43*scalef,-1.45*scalef,'{:1.4f}'.format(duv[0,0]),fontsize = 15, fontweight='bold', horizontalalignment='right',verticalalignment='bottom',color = 'k')
        axh.text(1.43*scalef,-1.25*scalef,'$D_{uv}$',fontsize = 13, style='italic', horizontalalignment='right',verticalalignment='bottom',color = 'k')
    axh.set_xticks([])
    axh.set_yticks([])
    return axh, data