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)
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)
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
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
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)
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)
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)
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
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