def apply_poly_model_at_hue_x(poly_model, pmodel, dCHoverC_res, \ hx = None, Cxr = 40, sig = _VF_SIG): """ Applies base color shift model at (hue,chroma) coordinates Args: :poly_model: | function handle to model :pmodel: | ndarray with model parameters. :dCHoverC_res: | ndarray with residuals between 'dCoverC,dH' of samples | and 'dCoverC,dH' predicted by the model. | Note: dCoverC = (Ct - Cr)/Cr and dH = ht - hr | (predicted from model, see notes luxpy.cri.get_poly_model()) :hx: | None or ndarray, optional | None defaults to np.arange(np.pi/10.0,2*np.pi,2*np.pi/10.0) :Cxr: | 40, optional :sig: | _VF_SIG or float, optional | Determines smooth transition between hue-bin-boundaries (no hard cutoff at hue bin boundary). Returns: :returns: | ndarrays with dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig | Note '_sig' denotes the uncertainty: | e.g. dH_x_sig is the uncertainty of dH at input (hue/chroma). """ if hx is None: dh = 2*np.pi/10.0; hx = np.arange(dh/2,2*np.pi,dh) #hue angles at which to apply model, i.e. calculate 'average' measures # A calculate reference coordinates: axr = Cxr*np.cos(hx) bxr = Cxr*np.sin(hx) # B apply model at reference coordinates to obtain test coordinates: axt,bxt,Cxt,hxt,axr,bxr,Cxr,hxr = apply_poly_model_at_x(poly_model, pmodel,axr,bxr) # C Calculate dC/C, dH for test and ref at fixed hues: dCoverC_x = (Cxt-Cxr)/(np.hstack((Cxr+Cxt)).max()) dH_x = (180/np.pi)*(hxt-hxr) # dCoverC_x = np.round(dCoverC_x,decimals = 2) # dH_x = np.round(dH_x,decimals = 0) # D calculate 'average' noise measures using sig-value: href = dCHoverC_res[:,0:1] dCoverC_res = dCHoverC_res[:,1:2] dHoverC_res = dCHoverC_res[:,2:3] dHsigi = np.exp((np.dstack((np.abs(hx-href),np.abs((hx-href-2*np.pi)),np.abs(hx-href-2*np.pi))).min(axis=2)**2)/(-2)/sig) dH_x_sig = (180/np.pi)*(np.sqrt((dHsigi*(dHoverC_res**2)).sum(axis=0,keepdims=True)/dHsigi.sum(axis=0,keepdims=True))) #dH_x_sig_avg = np.sqrt(np.sum(dH_x_sig**2,axis=1)/hx.shape[0]) dCoverC_x_sig = (np.sqrt((dHsigi*(dCoverC_res**2)).sum(axis=0,keepdims=True)/dHsigi.sum(axis=0,keepdims=True))) #dCoverC_x_sig_avg = np.sqrt(np.sum(dCoverC_x_sig**2,axis=1)/hx.shape[0]) return dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig
def psy_scale(data, scale_factor=[1.0 / 55.0, 3.0 / 2.0, 2.0], scale_max=100.0): # defaults for cri2012 """ Psychometric based color rendering index scale from CRI2012: | Rfi,a = 100 * (2 / (exp(c1*abs(DEi,a)**(c2) + 1))) ** c3. Args: :data: | float or list[floats] or ndarray :scale_factor: | [1/55, 3/2, 2.0] or list[float] or ndarray, optional | Rescales color differences before subtracting them from :scale_max: | Note that the default value is the one from (Smet et al. 2013, LRT). :scale_max: | 100.0, optional | Maximum value of linear scale Returns: :returns: | float or list[floats] or ndarray References: 1. `Smet, K., Schanda, J., Whitehead, L., & Luo, R. (2013). CRI2012: A proposal for updating the CIE colour rendering index. Lighting Research and Technology, 45, 689–709. <http://lrt.sagepub.com/content/45/6/689>`_ """ return scale_max * np.power( 2.0 / (np.exp(scale_factor[0] * np.power(np.abs(data), scale_factor[1])) + 1.0), scale_factor[2])
def get_xyz(self, *args): theta, phi, r = args x = r * np.sin(theta) * np.cos(phi) y = r * np.sin(theta) * np.sin(phi) z = r * np.cos(theta) z[np.abs(z) < self._TINY] = 0.0 return x, y, z
def naka_rushton(data, sig=2.0, n=0.73, scaling=1.0, noise=0.0, forward=True): """ Apply a Naka-Rushton response compression (n) and an adaptive shift (sig). | NK(x) = sign(x) * scaling * ((abs(x)**n) / ((abs(x)**n) + (sig**n))) + noise Args: :data: | float or ndarray :sig: | 2.0, optional | Semi-saturation constant. Value for which NK(:data:) is 1/2 :n: | 0.73, optional | Compression power. :scaling: | 1.0, optional | Maximum value of NK-function. :noise: | 0.0, optional | Cone excitation noise. :forward: | True, optional | True: do NK(x) | False: do NK(x)**(-1). Returns: :returns: | float or ndarray with NK-(de)compressed input :x: """ if forward: return np.sign(data) * scaling * ((np.abs(data)**n) / ((np.abs(data)**n) + (sig**n))) + noise elif forward == False: Ip = sig * (((np.abs(np.abs(data) - noise)) / (scaling - np.abs(np.abs(data) - noise))))**(1 / n) if not np.isscalar(Ip): p = np.where(np.abs(data) < noise) Ip[p] = -Ip[p] else: if np.abs(data) < noise: Ip = -Ip return Ip
def polyarea(x,y): """ Calculates area of polygon. | First coordinate should also be last. Args: :x: | ndarray of x-coordinates of polygon vertices. :y: | ndarray of x-coordinates of polygon vertices. Returns: :returns: | float (area or polygon) """ return 0.5*np.abs(np.dot(x,np.roll(y,1).T)-np.dot(y,np.roll(x,1).T))
def ipt_to_xyz(ipt, cieobs=_CIEOBS, xyzw=None, M=None, **kwargs): """ Convert XYZ tristimulus values to IPT color coordinates. | I: Lightness axis, P, red-green axis, T: yellow-blue axis. Args: :ipt: | ndarray with IPT color coordinates :xyzw: | None or ndarray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating xyzw for rescaling Mxyz2lms (only when not None). :M: | None, optional | None defaults to xyz to lms conversion matrix determined by:cieobs: Returns: :xyz: | ndarray with tristimulus values Note: :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation ! Reference: 1. `Ebner F, and Fairchild MD (1998). Development and testing of a color space (IPT) with improved hue uniformity. In IS&T 6th Color Imaging Conference, (Scottsdale, Arizona, USA), pp. 8–13. <http://www.ingentaconnect.com/content/ist/cic/1998/00001998/00000001/art00003?crawler=true>`_ """ ipt = np2d(ipt) # get M to convert xyz to lms and apply normalization to matrix or input your own: if M is None: M = _IPT_M['xyz2lms'][cieobs].copy( ) # matrix conversions from xyz to lms if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs, out=1) / 100.0 else: xyzw = xyzw / 100.0 M = math.normalize_3x3_matrix(M, xyzw) # convert from ipt to lms': if len(ipt.shape) == 3: lmsp = np.einsum('ij,klj->kli', np.linalg.inv(_IPT_M['lms2ipt']), ipt) else: lmsp = np.einsum('ij,lj->li', np.linalg.inv(_IPT_M['lms2ipt']), ipt) # reverse response compression: lms' to lms lms = lmsp**(1.0 / 0.43) p = np.where(lmsp < 0.0) lms[p] = -np.abs(lmsp[p])**(1.0 / 0.43) # convert from lms to xyz: if np.ndim(M) == 2: if len(ipt.shape) == 3: xyz = np.einsum('ij,klj->kli', np.linalg.inv(M), lms) else: xyz = np.einsum('ij,lj->li', np.linalg.inv(M), lms) else: if len( ipt.shape ) == 3: # second dim of lms must match dim of 1st of M and 1st dim of xyzw xyz = np.concatenate([ np.einsum('ij,klj->kli', np.linalg.inv(M[i]), lms[:, i:i + 1, :]) for i in range(M.shape[0]) ], axis=1) else: # first dim of lms must match dim of 1st of M and 1st dim of xyzw xyz = np.concatenate([ np.einsum('ij,lj->li', np.linalg.inv(M[i]), lms[i:i + 1, :]) for i in range(M.shape[0]) ], axis=0) #xyz = np.dot(np.linalg.inv(M),lms.T).T xyz = xyz * 100.0 xyz[np.where(xyz < 0.0)] = 0.0 return xyz
def plot_cri_graphics(data, cri_type = None, hbins = 16, start_hue = 0.0, scalef = 100, \ plot_axis_labels = False, bin_labels = None, plot_edge_lines = True, \ plot_center_lines = False, plot_bin_colors = True, \ axtype = 'polar', ax = None, force_CVG_layout = True, vf_model_type = _VF_MODEL_TYPE, vf_pcolorshift = _VF_PCOLORSHIFT, vf_color = 'k', \ vf_bin_labels = _VF_PCOLORSHIFT['labels'], vf_plot_bin_colors = True, \ scale_vf_chroma_to_sample_chroma = False,\ plot_VF = True, plot_CF = False, plot_SF = False): """ Plot graphical information on color rendition properties. Args: :data: | ndarray with spectral data or dict with pre-computed metrics. :cri_type: | None, optional | If None: defaults to cri_type = 'iesrf'. | :hbins:, :start_hue: and :scalef: are ignored if cri_type not None | and values are replaced by those in cri_type['rg_pars'] :hbins: | 16 or ndarray with sorted hue bin centers (°), optional :start_hue: | 0.0, optional :scalef: | 100, 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. :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. :force_CVG_layout: | False or True, optional | True: Force plot of basis of CVG. :vf_model_type: | _VF_MODEL_TYPE or 'M6' or 'M5', optional | Type of polynomial vector field model to use for the calculation of base color shift and metameric uncertainty. :vf_pcolorshift: | _VF_PCOLORSHIFT or user defined dict, optional | The polynomial models of degree 5 and 6 can be fully specified or summarized by the model parameters themselved OR by calculating the dCoverC and dH at resp. 5 and 6 hues. :VF_pcolorshift: specifies these hues and chroma level. :vf_color: | 'k', optional | For plotting the vector fields. :vf_plot_bin_colors: | True, optional | Colorize hue bins of VF graph. :scale_vf_chroma_to_sample_chroma: | False, optional | Scale chroma of reference and test vf fields such that average of binned reference chroma equals that of the binned sample chroma before calculating hue bin metrics. :vf_bin_labels: | see :bin_labels: | Set VF model hue-bin labels. :plot_CF: | False, optional | Plot circle fields. :plot_VF: | True, optional | Plot vector fields. :plot_SF: | True, optional | Plot sample shifts. Returns: :returns: | (data, | [plt.gcf(),ax_spd, ax_CVG, ax_locC, ax_locH, ax_VF], | cmap ) | | :data: dict with color rendering data | with keys: | - 'SPD' : ndarray test SPDs | - 'bjabt': ndarray with binned jab data under test SPDs | - 'bjabr': ndarray with binned jab data under reference SPDs | - 'cct' : ndarray with CCT of test SPD | - 'duv' : ndarray with distance to blackbody locus of test SPD | - 'Rf' : ndarray with general color fidelity indices | - 'Rg' : ndarray with gamut area indices | - 'Rfi' : ndarray with specific color fidelity indices | - 'Rfhi' : ndarray with local (hue binned) fidelity indices | - 'Rcshi': ndarray with local chroma shifts indices | - 'Rhshi': ndarray with local hue shifts indices | - 'Rt' : ndarray with general metameric uncertainty index Rt | - 'Rti' : ndarray with specific metameric uncertainty indices Rti | - 'Rfhi_vf' : ndarray with local (hue binned) fidelity indices | obtained from VF model predictions at color space | pixel coordinates | - 'Rcshi_vf': ndarray with local chroma shifts indices | (same as above) | - 'Rhshi_vf': ndarray with local hue shifts indices | (same as above) | | :[...]: list with handles to current figure and 5 axes. | | :cmap: list with rgb colors for hue bins (for use in other plotting fcns) """ if not isinstance(data,dict): data = spd_to_ies_tm30_metrics(data, cri_type = cri_type, hbins = hbins, start_hue = start_hue, scalef = scalef, vf_model_type = vf_model_type, vf_pcolorshift = vf_pcolorshift, scale_vf_chroma_to_sample_chroma = scale_vf_chroma_to_sample_chroma) Rcshi, Rf, Rfchhi_vf, Rfhi, Rfhi_vf, Rfhshi_vf, Rfi, Rg, Rhshi, Rt, Rti, SPD, bjabr, bjabt, cct, cri_type, dataVF, duv = [data[x] for x in sorted(data.keys())] hbins = cri_type['rg_pars']['nhbins'] start_hue = cri_type['rg_pars']['start_hue'] scalef = cri_type['rg_pars']['normalized_chroma_ref'] #layout = np.array([[3,3,0,0],[1,0,2,2],[0,0,2,1],[2,2,1,1],[0,2,1,1],[1,2,1,1]]) #layout = np.array([[6,6,0,0],[0,3,3,3],[3,3,3,3],[0,0,3,2],[2,2,2,2],[2,0,2,2],[4,0,2,2]]) layout = np.array([[6,7,0,0],[0,4,3,3],[3,4,3,3],[0,0,4,2],[2,0,2,2],[4,2,2,2],[4,0,2,2],[2,2,2,2]]) def create_subplot(layout,n, polar = False, frameon = True): ax = plt.subplot2grid(layout[0,0:2], layout[n,0:2], colspan = layout[n,2], rowspan = layout[n,3], polar = polar, frameon = frameon) return ax for i in range(cct.shape[0]): fig = plt.figure(figsize=(10, 6), dpi=144) # Plot CVG: ax_CVG = create_subplot(layout,1, polar = True, frameon = False) figCVG, ax, cmap = plot_ColorVectorGraphic(bjabt[...,i,:], bjabr[...,i,:], hbins = hbins, axtype = axtype, ax = ax_CVG, 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 = '#') # Plot VF: ax_VF = create_subplot(layout,2, polar = True, frameon = False) if i == 0: hbin_cmap = None ax_VF, hbin_cmap = plot_VF_PX_models([dataVF[i]], dataPX = None, plot_VF = plot_VF, plot_PX = None, axtype = 'polar', ax = ax_VF, \ plot_circle_field = plot_CF, plot_sample_shifts = plot_SF, plot_bin_colors = vf_plot_bin_colors, \ plot_samples_shifts_at_pixel_center = False, jabp_sampled = None, \ plot_VF_colors = [vf_color], plot_PX_colors = ['r'], hbin_cmap = hbin_cmap, force_CVG_layout = True, bin_labels = vf_bin_labels) # Plot test SPD: ax_spd = create_subplot(layout,3) ax_spd.plot(SPD[0],SPD[i+1]/SPD[i+1].max(),'r-') ax_spd.text(730,0.9,'CCT = {:1.0f} K'.format(cct[i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3) ax_spd.text(730,0.8,'Duv = {:1.4f}'.format(duv[i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3) ax_spd.text(730,0.7,'IES Rf = {:1.0f}'.format(Rf[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3) ax_spd.text(730,0.6,'IES Rg = {:1.0f}'.format(Rg[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3) ax_spd.text(730,0.5,'Rt = {:1.0f}'.format(Rt[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3) ax_spd.set_xlabel('Wavelength (nm)', fontsize = 9) ax_spd.set_ylabel('Rel. spectral intensity', fontsize = 9) ax_spd.set_xlim([360,830]) # Plot local color fidelity, Rfhi: ax_Rfi = create_subplot(layout,4) for j in range(hbins): ax_Rfi.bar(range(hbins)[j],Rfhi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4) ax_Rfi.text(range(hbins)[j],Rfhi[j,i]*1.1, '{:1.0f}'.format(Rfhi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',color = np.array([1,1,1])*0.3) ax_Rfi.set_ylim([0,120]) xticks = np.arange(hbins) xtickslabels = ['{:1.0f}'.format(ii+1) for ii in range(hbins)] ax_Rfi.set_xticks(xticks) ax_Rfi.set_xticklabels(xtickslabels, fontsize = 8) ax_Rfi.set_ylabel(r'Local color fidelity $R_{f,hi}$') ax_Rfi.set_xlabel('Hue bin #') # Plot local chroma shift, Rcshi: ax_locC = create_subplot(layout,5) for j in range(hbins): ax_locC.bar(range(hbins)[j],Rcshi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4) ax_locC.text(range(hbins)[j],-np.sign(Rcshi[j,i])*0.1, '{:1.0f}%'.format(100*Rcshi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',rotation = 90, color = np.array([1,1,1])*0.3) ylim = np.array([np.abs(Rcshi.min()),np.abs(Rcshi.min()),0.2]).max()*1.5 ax_locC.set_ylim([-ylim,ylim]) ax_locC.set_ylabel(r'Local chroma shift, $R_{cs,hi}$') ax_locC.set_xticklabels([]) ax_locC.set_yticklabels(['{:1.2f}'.format(ii) for ii in ax_locC.set_ylim()], color = 'white') # Plot local hue shift, Rhshi: ax_locH = create_subplot(layout,6) for j in range(hbins): ax_locH.bar(range(hbins)[j],Rhshi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4) ax_locH.text(range(hbins)[j],-np.sign(Rhshi[j,i])*0.2, '{:1.3f}'.format(Rhshi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',rotation = 90, color = np.array([1,1,1])*0.3) ylim = np.array([np.abs(Rhshi.min()),np.abs(Rhshi.min()),0.2]).max()*1.5 ax_locH.set_ylim([-ylim,ylim]) ax_locH.set_ylabel(r'Local hue shift, $R_{hs,hi}$') ax_locH.set_xticklabels([]) ax_locH.set_yticklabels(['{:1.2f}'.format(ii) for ii in ax_locH.set_ylim()], color = 'white') # Plot local color fidelity of VF, vfRfhi: ax_vfRfi = create_subplot(layout,7) for j in range(hbins): ax_vfRfi.bar(range(hbins)[j],Rfhi_vf[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4) ax_vfRfi.text(range(hbins)[j],Rfhi_vf[j,i]*1.1, '{:1.0f}'.format(Rfhi_vf[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',color = np.array([1,1,1])*0.3) ax_vfRfi.set_ylim([0,120]) xticks = np.arange(hbins) xtickslabels = ['{:1.0f}'.format(ii+1) for ii in range(hbins)] ax_vfRfi.set_xticks(xticks) ax_vfRfi.set_xticklabels(xtickslabels, fontsize = 8) ax_vfRfi.set_ylabel(r'Local VF color fidelity $vfR_{f,hi}$') ax_vfRfi.set_xlabel('Hue bin #') plt.tight_layout() return data, [plt.gcf(),ax_spd, ax_CVG, ax_locC, ax_locH, ax_VF], cmap
def plot_hue_bins(hbins = 16, start_hue = 0.0, scalef = 100, \ plot_axis_labels = False, bin_labels = '#', plot_edge_lines = True, \ plot_center_lines = False, plot_bin_colors = True, \ axtype = 'polar', ax = None, force_CVG_layout = False): """ Makes basis plot for Color Vector Graphic (CVG). Args: :hbins: | 16 or ndarray with sorted hue bin centers (°), optional :start_hue: | 0.0, optional :scalef: | 100, 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. :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. :force_CVG_layout: | False or True, optional | True: Force plot of basis of CVG on first encounter. Returns: :returns: | gcf(), gca(), list with rgb colors for hue bins (for use in other plotting fcns) """ # Setup hbincenters and hsv_hues: if isinstance(hbins, float) | isinstance(hbins, int): nhbins = hbins dhbins = 360 / (nhbins) # hue bin width hbincenters = np.arange(start_hue + dhbins / 2, 360, dhbins) hbincenters = np.sort(hbincenters) else: hbincenters = hbins idx = np.argsort(hbincenters) if isinstance(bin_labels, list) | isinstance(bin_labels, np.ndarray): bin_labels = bin_labels[idx] hbincenters = hbincenters[idx] nhbins = hbincenters.shape[0] hbincenters = hbincenters * np.pi / 180 # Setup hbin labels: if bin_labels is '#': bin_labels = ['#{:1.0f}'.format(i + 1) for i in range(nhbins)] # initializing the figure cmap = None if (ax == None) or (ax == 'new'): fig = plt.figure() newfig = True else: newfig = False rect = [0.1, 0.1, 0.8, 0.8] # setting the axis limits in [left, bottom, width, height] if axtype == 'polar': # the polar axis: if newfig == True: ax = fig.add_axes(rect, polar=True, frameon=False) else: #cartesian axis: if newfig == True: ax = fig.add_axes(rect) if (newfig == True) | (force_CVG_layout == True): # Calculate hue-bin boundaries: r = np.vstack( (np.zeros(hbincenters.shape), scalef * np.ones(hbincenters.shape))) theta = np.vstack((np.zeros(hbincenters.shape), hbincenters)) #t = hbincenters.copy() dU = np.roll(hbincenters.copy(), -1) dL = np.roll(hbincenters.copy(), 1) dtU = dU - hbincenters dtL = hbincenters - dL dtU[dtU < 0] = dtU[dtU < 0] + 2 * np.pi dtL[dtL < 0] = dtL[dtL < 0] + 2 * np.pi dL = hbincenters - dtL / 2 dU = hbincenters + dtU / 2 dt = (dU - dL) dM = dL + dt / 2 # Setup color for plotting hue bins: hsv_hues = hbincenters - 30 * np.pi / 180 hsv_hues = hsv_hues / hsv_hues.max() edges = np.vstack( (np.zeros(hbincenters.shape), dL)) # setup hue bin edges array if axtype == 'cart': if plot_center_lines == True: hx = r * np.cos(theta) hy = r * np.sin(theta) if bin_labels is not None: hxv = np.vstack((np.zeros(hbincenters.shape), 1.3 * scalef * np.cos(hbincenters))) hyv = np.vstack((np.zeros(hbincenters.shape), 1.3 * scalef * np.sin(hbincenters))) if plot_edge_lines == True: hxe = np.vstack( (np.zeros(hbincenters.shape), 1.2 * scalef * np.cos(dL))) hye = np.vstack( (np.zeros(hbincenters.shape), 1.2 * scalef * np.sin(dL))) # Plot hue-bins: for i in range(nhbins): # Create color from hue angle: c = np.abs(np.array(colorsys.hsv_to_rgb(hsv_hues[i], 0.84, 0.9))) #c = [abs(c[0]),abs(c[1]),abs(c[2])] # ensure all positive elements if i == 0: cmap = [c] else: cmap.append(c) if axtype == 'polar': if plot_edge_lines == True: ax.plot(edges[:, i], r[:, i] * 1.2, color='grey', marker='None', linestyle=':', linewidth=3, markersize=2) if plot_center_lines == True: if np.mod(i, 2) == 1: ax.plot(theta[:, i], r[:, i], color=c, marker=None, linestyle='--', linewidth=2) else: ax.plot(theta[:, i], r[:, i], color=c, marker='o', linestyle='-', linewidth=3, markersize=10) if plot_bin_colors == True: bar = ax.bar(dM[i], r[1, i], width=dt[i], color=c, alpha=0.15) if bin_labels is not None: ax.text(hbincenters[i], 1.3 * scalef, bin_labels[i], fontsize=12, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.3) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: if plot_edge_lines == True: ax.plot(hxe[:, i], hye[:, i], color='grey', marker='None', linestyle=':', linewidth=3, markersize=2) if plot_center_lines == True: if np.mod(i, 2) == 1: ax.plot(hx[:, i], hy[:, i], color=c, marker=None, linestyle='--', linewidth=2) else: ax.plot(hx[:, i], hy[:, i], color=c, marker='o', linestyle='-', linewidth=3, markersize=10) if bin_labels is not None: ax.text(hxv[1, i], hyv[1, i], bin_labels[i], fontsize=12, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.3) ax.axis(1.1 * np.array( [hxv.min(), hxv.max(), hyv.min(), hyv.max()])) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: plt.xlabel("a'") plt.ylabel("b'") plt.plot(0, 0, color='k', marker='o', linestyle=None) return plt.gcf(), plt.gca(), cmap
def DE2000(xyzt, xyzr, dtype = 'xyz', DEtype = 'jab', avg = None, avg_axis = 0, out = 'DEi', xyzwt = None, xyzwr = None, KLCH = None): """ Calculate DE2000 color difference. Args: :xyzt: | ndarray with tristimulus values of test data. :xyzr: | ndarray with tristimulus values of reference data. :dtype: | 'xyz' or 'lab', optional | Specifies data type in :xyzt: and :xyzr:. :xyzwt: | None or ndarray, optional | White point tristimulus values of test data | None defaults to the one set in lx.xyz_to_lab() :xyzwr: | None or ndarray, optional | Whitepoint tristimulus values of reference data | None defaults to the one set in lx.xyz_to_lab() :DEtype: | 'jab' or str, optional | Options: | - 'jab' : calculates full color difference over all 3 dimensions. | - 'ab' : calculates chromaticity difference. | - 'j' : calculates lightness or brightness difference | (depending on :outin:). | - 'j,ab': calculates both 'j' and 'ab' options and returns them as a tuple. :KLCH: | None, optional | Weigths for L, C, H | None: default to [1,1,1] :avg: | None, optional | None: don't calculate average DE, | otherwise use function handle in :avg:. :avg_axis: | axis to calculate average over, optional :out: | 'DEi' or str, optional | Requested output. Note: For the other input arguments, see specific color space used. Returns: :returns: | ndarray with DEi [, DEa] or other as specified by :out: References: 1. `Sharma, G., Wu, W., & Dalal, E. N. (2005). The CIEDE2000 color‐difference formula: Implementation notes, supplementary test data, and mathematical observations. Color Research & Application, 30(1), 21–30. <https://doi.org/10.1002/col.20070>`_ """ if KLCH is None: KLCH = [1,1,1] if dtype == 'xyz': labt = xyz_to_lab(xyzt, xyzw = xyzwt) labr = xyz_to_lab(xyzr, xyzw = xyzwr) else: labt = xyzt labr = xyzr Lt = labt[...,0:1] at = labt[...,1:2] bt = labt[...,2:3] Ct = np.sqrt(at**2 + bt**2) #ht = cam.hue_angle(at,bt,htype = 'rad') Lr = labr[...,0:1] ar = labr[...,1:2] br = labr[...,2:3] Cr = np.sqrt(ar**2 + br**2) #hr = cam.hue_angle(at,bt,htype = 'rad') # Step 1: Cavg = (Ct + Cr)/2 G = 0.5*(1 - np.sqrt((Cavg**7.0)/((Cavg**7.0) + (25.0**7)))) apt = (1 + G)*at apr = (1 + G)*ar Cpt = np.sqrt(apt**2 + bt**2) Cpr = np.sqrt(apr**2 + br**2) Cpprod = Cpt*Cpr hpt = cam.hue_angle(apt,bt, htype = 'deg') hpr = cam.hue_angle(apr,br, htype = 'deg') hpt[(apt==0)*(bt==0)] = 0 hpr[(apr==0)*(br==0)] = 0 # Step 2: dL = np.abs(Lr - Lt) dCp = np.abs(Cpr - Cpt) dhp_ = hpr - hpt dhp = dhp_.copy() dhp[np.where(np.abs(dhp_) > 180)] = dhp[np.where(np.abs(dhp_) > 180)] - 360 dhp[np.where(np.abs(dhp_) < -180)] = dhp[np.where(np.abs(dhp_) < -180)] + 360 dhp[np.where(Cpprod == 0)] = 0 #dH = 2*np.sqrt(Cpprod)*np.sin(dhp/2*np.pi/180) dH = deltaH(dhp, Cpprod, htype = 'deg') # Step 3: Lp = (Lr + Lt)/2 Cp = (Cpr + Cpt)/2 hps = hpt + hpr hp = (hpt + hpr)/2 hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] + 180 hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] - 180 hp[np.where(Cpprod == 0)] = 0 T = 1 - 0.17*np.cos((hp - 30)*np.pi/180) + 0.24*np.cos(2*hp*np.pi/180) +\ 0.32*np.cos((3*hp + 6)*np.pi/180) - 0.20*np.cos((4*hp - 63)*np.pi/180) dtheta = 30*np.exp(-((hp-275)/25)**2) RC = 2*np.sqrt((Cp**7)/((Cp**7) + (25**7))) SL = 1 + ((0.015*(Lp-50)**2)/np.sqrt(20 + (Lp - 50)**2)) SC = 1 + 0.045*Cp SH = 1 + 0.015*Cp*T RT = -np.sin(2*dtheta*np.pi/180)*RC kL, kC, kH = KLCH DEi = ((dL/(kL*SL))**2 , (dCp/(kC*SC))**2 + (dH/(kH*SH))**2 + RT*(dCp/(kC*SC))*(dH/(kH*SH))) return _process_DEi(DEi, DEtype = DEtype, avg = avg, avg_axis = avg_axis, out = out)
def __eq__(self, other): return np.abs(self - other) < self._TINY
def __pow__(self, x): return (self * self) if x == 2 else np.abs(self)**x
def apply(data, catmode = '1>0>2', cattype = 'vonkries', xyzw1 = None, xyzw2 = None, xyzw0 = None,\ D = None, mcat = ['cat02'], normxyz0 = None, outtype = 'xyz', La = None, F = None, Dtype = None): """ Calculate corresponding colors by applying a von Kries chromatic adaptation transform (CAT), i.e. independent rescaling of 'sensor sensitivity' to data to adapt from current adaptation conditions (1) to the new conditions (2). Args: :data: | ndarray of tristimulus values (can be NxMx3) :catmode: | '1>0>2, optional | -'1>0>2': Two-step CAT | from illuminant 1 to baseline illuminant 0 to illuminant 2. | -'1>0': One-step CAT | from illuminant 1 to baseline illuminant 0. | -'0>2': One-step CAT | from baseline illuminant 0 to illuminant 2. :cattype: | 'vonkries' (others: 'rlab', see Farchild 1990), optional :xyzw1: | None, depending on :catmode: optional (can be Mx3) :xyzw2: | None, depending on :catmode: optional (can be Mx3) :xyzw0: | None, depending on :catmode: optional (can be Mx3) :D: | None, optional | Degrees of adaptation. Defaults to [1.0, 1.0]. :La: | None, optional | Adapting luminances. | If None: xyz values are absolute or relative. | If not None: xyz are relative. :F: | None, optional | Surround parameter(s) for CAT02/CAT16 calculations (:Dtype: == 'cat02' or 'cat16') | Defaults to [1.0, 1.0]. :Dtype: | None, optional | Type of degree of adaptation function from literature | See luxpy.cat.get_degree_of_adaptation() :mcat: | ['cat02'], optional | List[str] or List[ndarray] of sensor space matrices for each condition pair. If len(:mcat:) == 1, the same matrix is used. :normxyz0: | None, optional | Set of xyz tristimulus values to normalize the sensor space matrix to. :outtype: | 'xyz' or 'lms', optional | - 'xyz': return corresponding tristimulus values | - 'lms': return corresponding sensor space excitation values | (e.g. for further calculations) Returns: :returns: | ndarray with corresponding colors """ if (xyzw1 is None) & (xyzw2 is None): return data # do nothing else: # Make data 2d: data = np2d(data) data_original_shape = data.shape if data.ndim < 3: target_shape = np.hstack((1, data.shape)) data = data * np.ones(target_shape) else: target_shape = data.shape target_shape = data.shape # initialize xyzw0: if (xyzw0 is None): # set to iLL.E xyzw0 = np2d([100.0, 100.0, 100.0]) xyzw0 = np.ones(target_shape) * xyzw0 La0 = xyzw0[..., 1, None] # Determine cat-type (1-step or 2-step) + make input same shape as data for block calculations: expansion_axis = np.abs(1 * (len(data_original_shape) == 2) - 1) if ((xyzw1 is not None) & (xyzw2 is not None)): xyzw1 = xyzw1 * np.ones(target_shape) xyzw2 = xyzw2 * np.ones(target_shape) default_La12 = [xyzw1[..., 1, None], xyzw2[..., 1, None]] elif (xyzw2 is None) & (xyzw1 is not None): # apply one-step CAT: 1-->0 catmode = '1>0' #override catmode input xyzw1 = xyzw1 * np.ones(target_shape) default_La12 = [xyzw1[..., 1, None], La0] elif (xyzw1 is None) & (xyzw2 is not None): raise Exception( "von_kries(): cat transformation '0>2' not supported, use '1>0' !" ) # Get or set La (La == None: xyz are absolute or relative, La != None: xyz are relative): target_shape_1 = tuple(np.hstack((target_shape[:-1], 1))) La1, La2 = parse_x1x2_parameters(La, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis, default=default_La12) # Set degrees of adaptation, D10, D20: (note D20 is degree of adaptation for 2-->0!!) D10, D20 = parse_x1x2_parameters(D, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis) # Set F surround in case of Dtype == 'cat02': F1, F2 = parse_x1x2_parameters(F, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis) # Make xyz relative to go to relative xyz0: if La is None: data = 100 * data / La1 xyzw1 = 100 * xyzw1 / La1 xyzw0 = 100 * xyzw0 / La0 if (catmode == '1>0>2') | (catmode == '1>2'): xyzw2 = 100 * xyzw2 / La2 # transform data (xyz) to sensor space (lms) and perform cat: xyzc = np.ones(data.shape) * np.nan mcat = np.array(mcat) if (mcat.shape[0] != data.shape[1]) & (mcat.shape[0] == 1): mcat = np.repeat(mcat, data.shape[1], axis=0) elif (mcat.shape[0] != data.shape[1]) & (mcat.shape[0] > 1): raise Exception( 'von_kries(): mcat.shape[0] > 1 and does not match data.shape[0]!' ) for i in range(xyzc.shape[1]): # get cat sensor matrix: if mcat[i].dtype == np.float64: mcati = mcat[i] else: mcati = _MCATS[mcat[i]] # normalize sensor matrix: if normxyz0 is not None: mcati = math.normalize_3x3_matrix(mcati, xyz0=normxyz0) # convert from xyz to lms: lms = np.dot(mcati, data[:, i].T).T lmsw0 = np.dot(mcati, xyzw0[:, i].T).T if (catmode == '1>0>2') | (catmode == '1>0'): lmsw1 = np.dot(mcati, xyzw1[:, i].T).T Dpar1 = dict(D=D10[:, i], F=F1[:, i], La=La1[:, i], La0=La0[:, i], order='1>0') D10[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar1) #get degree of adaptation depending on Dtype lmsw2 = None # in case of '1>0' if (catmode == '1>0>2'): lmsw2 = np.dot(mcati, xyzw2[:, i].T).T Dpar2 = dict(D=D20[:, i], F=F2[:, i], La=La2[:, i], La0=La0[:, i], order='0>2') D20[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar2) #get degree of adaptation depending on Dtype if (catmode == '1>2'): lmsw1 = np.dot(mcati, xyzw1[:, i].T).T lmsw2 = np.dot(mcati, xyzw2[:, i].T).T Dpar12 = dict(D=D10[:, i], F=F1[:, i], La=La1[:, i], La2=La2[:, i], order='1>2') D10[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar12) #get degree of adaptation depending on Dtype # Determine transfer function Dt: Dt = get_transfer_function(cattype=cattype, catmode=catmode, lmsw1=lmsw1, lmsw2=lmsw2, lmsw0=lmsw0, D10=D10[:, i], D20=D20[:, i], La1=La1[:, i], La2=La2[:, i]) # Perform cat: lms = np.dot(np.diagflat(Dt[0]), lms.T).T # Make xyz, lms 'absolute' again: if (catmode == '1>0>2'): lms = (La2[:, i] / La1[:, i]) * lms elif (catmode == '1>0'): lms = (La0[:, i] / La1[:, i]) * lms elif (catmode == '1>2'): lms = (La2[:, i] / La1[:, i]) * lms # transform back from sensor space to xyz (or not): if outtype == 'xyz': xyzci = np.dot(np.linalg.inv(mcati), lms.T).T xyzci[np.where(xyzci < 0)] = _EPS xyzc[:, i] = xyzci else: xyzc[:, i] = lms # return data to original shape: if len(data_original_shape) == 2: xyzc = xyzc[0] return xyzc
def plot_spectrum_colors(spd = None, spdmax = None,\ wavelength_height = -0.05, wavelength_opacity = 1.0, wavelength_lightness = 1.0,\ cieobs = _CIEOBS, show = True, axh = None,\ show_grid = True,ylabel = 'Spectral intensity (a.u.)',xlim=None,\ **kwargs): """ Plot the spectrum colors. Args: :spd: | None, optional | Spectrum :spdmax: | None, optional | max ylim is set at 1.05 or (1+abs(wavelength_height)*spdmax) :wavelength_opacity: | 1.0, optional | Sets opacity of wavelength rectangle. :wavelength_lightness: | 1.0, optional | Sets lightness of wavelength rectangle. :wavelength_height: | -0.05 or 'spd', optional | Determine wavelength bar height | if not 'spd': x% of spd.max() :axh: | None or axes handle, optional | Determines axes to plot data in. | None: make new figure. :show: | True or False, optional | Invoke matplotlib.pyplot.show() right after plotting :cieobs: | luxpy._CIEOBS or str, optional | Determines CMF set to calculate spectrum locus or other. :show_grid: | True, optional | Show grid (True) or not (False) :ylabel: | 'Spectral intensity (a.u.)' or str, optional | Set y-axis label. :xlim: | None, optional | list or ndarray with xlimits. :kwargs: | additional keyword arguments for use with matplotlib.pyplot. Returns: """ cmfs = _CMF[cieobs]['bar'] wavs = cmfs[0:1].T SL = cmfs[1:4].T srgb = xyz_to_srgb(wavelength_lightness*100*SL) srgb = srgb/srgb.max() if show == True: if axh is None: fig = plt.figure() axh = fig.add_subplot(111) if (wavelength_height == 'spd') & (spd is not None): if spdmax is None: spdmax = np.nanmax(spd[1:,:]) y_min, y_max = 0.0, spdmax*(1.05) if xlim is None: x_min, x_max = spd[0,:].min(), spd[0,:].max() else: x_min, x_max = xlim SLrect = np.vstack([ (x_min, 0.0), spd.T, (x_max, 0.0), ]) wavelength_height = y_max spdmax = 1 else: if (spdmax is None) & (spd is not None): spdmax = np.nanmax(spd[1:,:]) y_min, y_max = wavelength_height*spdmax, spdmax*(1 + np.abs(wavelength_height)) elif (spdmax is None) & (spd is None): spdmax = 1 y_min, y_max = wavelength_height, 0 elif (spdmax is not None): y_min, y_max = wavelength_height*spdmax, spdmax#*(1 + np.abs(wavelength_height)) if xlim is None: x_min, x_max = wavs.min(), wavs.max() else: x_min, x_max = xlim SLrect = np.vstack([ (x_min, 0.0), (x_min, wavelength_height*spdmax), (x_max, wavelength_height*spdmax), (x_max, 0.0), ]) axh.set_xlim([x_min,x_max]) axh.set_ylim([y_min,y_max]) polygon = Polygon(SLrect, facecolor=None, edgecolor=None) axh.add_patch(polygon) padding = 0.1 axh.bar(x = wavs - padding, height = wavelength_height*spdmax, width = 1 + padding, color = srgb, align = 'edge', linewidth = 0, clip_path = polygon) if spd is not None: axh.plot(spd[0:1,:].T,spd[1:,:].T, color = 'k', label = 'spd') if show_grid == True: plt.grid() axh.set_xlabel('Wavelength (nm)',kwargs) axh.set_ylabel(ylabel, kwargs) #plt.show() return axh else: return None
def xyz_to_ipt(xyz, cieobs = _CIEOBS, xyzw = None, M = None, **kwargs): """ Convert XYZ tristimulus values to IPT color coordinates. | I: Lightness axis, P, red-green axis, T: yellow-blue axis. Args: :xyz: | ndarray with tristimulus values :xyzw: | None or ndarray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating xyzw for rescaling M (only when not None). :M: | None, optional | None defaults to xyz to lms conversion matrix determined by :cieobs: Returns: :ipt: | ndarray with IPT color coordinates Note: :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation ! Reference: 1. `Ebner F, and Fairchild MD (1998). Development and testing of a color space (IPT) with improved hue uniformity. In IS&T 6th Color Imaging Conference, (Scottsdale, Arizona, USA), pp. 8–13. <http://www.ingentaconnect.com/content/ist/cic/1998/00001998/00000001/art00003?crawler=true>`_ """ xyz = np2d(xyz) # get M to convert xyz to lms and apply normalization to matrix or input your own: if M is None: M = _IPT_M['xyz2lms'][cieobs].copy() # matrix conversions from xyz to lms if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'],cieobs = cieobs, out = 1)[0]/100.0 else: xyzw = xyzw/100.0 M = math.normalize_3x3_matrix(M,xyzw) # get xyz and normalize to 1: xyz = xyz/100.0 # convert xyz to lms: if len(xyz.shape) == 3: lms = np.einsum('ij,klj->kli', M, xyz) else: lms = np.einsum('ij,lj->li', M, xyz) #lms = np.dot(M,xyz.T).T #response compression: lms to lms' lmsp = lms**0.43 p = np.where(lms<0.0) lmsp[p] = -np.abs(lms[p])**0.43 # convert lms' to ipt coordinates: if len(xyz.shape) == 3: ipt = np.einsum('ij,klj->kli', _IPT_M['lms2ipt'], lmsp) else: ipt = np.einsum('ij,lj->li', _IPT_M['lms2ipt'], lmsp) return ipt
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 get_pixel_coordinates(jab, jab_ranges=None, jab_deltas=None, limit_grid_radius=0): """ Get pixel coordinates corresponding to array of jab color coordinates. Args: :jab: | ndarray of color coordinates :jab_ranges: | None or ndarray, optional | Specifies the pixelization of color space. | (ndarray.shape = (3,3), with first axis: J,a,b, and second axis: min, max, delta) :jab_deltas: | float or ndarray, optional | Specifies the sampling range. | A float uses jab_deltas as the maximum Euclidean distance to select samples around each pixel center. A ndarray of 3 deltas, uses a city block sampling around each pixel center. :limit_grid_radius: | 0, optional | A value of zeros keeps grid as specified by axr,bxr. | A value > 0 only keeps (a,b) coordinates within :limit_grid_radius: Returns: :returns: | gridp, idxp, jabp, samplenrs, samplesIDs | - :gridp: ndarray with coordinates of all pixel centers. | - :idxp: list[int] with pixel index for each non-empty pixel | - :jabp: ndarray with center color coordinates of non-empty pixels | - :samplenrs: list[list[int]] with sample numbers belong to each | non-empty pixel | - :sampleIDs: summarizing list, | with column order: 'idxp, jabp, samplenrs' """ if jab_deltas is None: jab_deltas = np.array([_VF_DELTAR, _VF_DELTAR, _VF_DELTAR]) if jab_ranges is None: jab_ranges = np.vstack( ([0, 100, jab_deltas[0] ], [-_VF_MAXR, _VF_MAXR + jab_deltas[1], jab_deltas[1]], [-_VF_MAXR, _VF_MAXR + jab_deltas[2], jab_deltas[2]])) # Get pixel grid: gridp = generate_grid(jab_ranges=jab_ranges, limit_grid_radius=limit_grid_radius) # determine pixel coordinates of each sample in jab: samplesIDs = [] for idx in range(gridp.shape[0]): # get pixel coordinates: jp = gridp[idx, 0] ap = gridp[idx, 1] bp = gridp[idx, 2] #Cp = np.sqrt(ap**2+bp**2) if type(jab_deltas) == np.ndarray: sampleID = np.where( ((np.abs(jab[..., 0] - jp) <= jab_deltas[0] / 2) & (np.abs(jab[..., 1] - ap) <= jab_deltas[1] / 2) & (np.abs(jab[..., 2] - bp) <= jab_deltas[2] / 2))) else: sampleID = np.where( (np.sqrt((jab[..., 0] - jp)**2 + (jab[..., 1] - ap)**2 + (jab[..., 2] - bp)**2) <= jab_deltas / 2)) if (sampleID[0].shape[0] > 0): samplesIDs.append( np.hstack((idx, np.array([jp, ap, bp]), sampleID[0]))) idxp = [np.int(samplesIDs[i][0]) for i in range(len(samplesIDs))] jabp = np.vstack([samplesIDs[i][1:4] for i in range(len(samplesIDs))]) samplenrs = [ np.array(samplesIDs[i][4:], dtype=int).tolist() for i in range(len(samplesIDs)) ] return gridp, idxp, jabp, samplenrs, samplesIDs
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 run(data, xyzw, out = 'J,aM,bM', conditions = None, forward = True): """ Run CIECAM02 color appearance model in forward or backward modes. Args: :data: | ndarray with relative sample xyz values (forward mode) or J'a'b' coordinates (inverse mode) :xyzw: | ndarray with relative white point tristimulus values :conditions: | None, optional | Dictionary with viewing conditions. | None results in: | {'La':100, 'Yb':20, 'D':1, 'surround':'avg'} | For more info see luxpy.cam.ciecam02()? :forward: | True, optional | If True: run in CAM in forward mode, else: inverse mode. :out: | 'J,aM,bM', optional | String with requested output (e.g. "J,aM,bM,M,h") [Forward mode] | String with inputs in data. | Input must have data.shape[-1]==3 and last dim of data must have | the following structure: | * data[...,0] = J or Q, | * data[...,1:] = (aM,bM) or (aC,bC) or (aS,bS) Returns: :camout: | ndarray with Jab coordinates or whatever correlates requested in out. Note: * This is a simplified, less flexible, but faster version than the main ciecam02(). References: 1. `N. Moroney, M. D. Fairchild, R. W. G. Hunt, C. Li, M. R. Luo, and T. Newman, (2002), "The CIECAM02 color appearance model,” IS&T/SID Tenth Color Imaging Conference. p. 23, 2002. <http://rit-mcsl.org/fairchild/PDFs/PRO19.pdf>`_ """ outin = out.split(',') if isinstance(out,str) else out #-------------------------------------------- # Get/ set conditions parameters: if conditions is not None: surround_parameters = {'surrounds': ['avg', 'dim', 'dark'], 'avg' : {'c':0.69, 'Nc':1.0, 'F':1.0,'FLL': 1.0}, 'dim' : {'c':0.59, 'Nc':0.9, 'F':0.9,'FLL':1.0} , 'dark' : {'c':0.525, 'Nc':0.8, 'F':0.8,'FLL':1.0}} La = conditions['La'] Yb = conditions['Yb'] D = conditions['D'] surround = conditions['surround'] if isinstance(surround, str): surround = surround_parameters[conditions['surround']] F, FLL, Nc, c = [surround[x] for x in sorted(surround.keys())] else: # set defaults: La, Yb, D, F, FLL, Nc, c = 100, 20, 1, 1, 1, 1, 0.69 #-------------------------------------------- # Define sensor space and cat matrices: mhpe = np.array([[0.38971,0.68898,-0.07868], [-0.22981,1.1834,0.04641], [0.0,0.0,1.0]]) # Hunt-Pointer-Estevez sensors (cone fundamentals) mcat = np.array([[0.7328, 0.4296, -0.1624], [ -0.7036, 1.6975, 0.0061], [ 0.0030, 0.0136, 0.9834]]) # CAT02 sensor space #-------------------------------------------- # pre-calculate some matrices: invmcat = np.linalg.inv(mcat) mhpe_x_invmcat = np.dot(mhpe,invmcat) if not forward: mcat_x_invmhpe = np.dot(mcat,np.linalg.inv(mhpe)) #-------------------------------------------- # calculate condition dependent parameters: Yw = xyzw[...,1:2].T k = 1.0 / (5.0*La + 1.0) FL = 0.2*(k**4.0)*(5.0*La) + 0.1*((1.0 - k**4.0)**2.0)*((5.0*La)**(1.0/3.0)) # luminance adaptation factor n = Yb/Yw Nbb = 0.725*(1/n)**0.2 Ncb = Nbb z = 1.48 + FLL*n**0.5 if D is None: D = F*(1.0-(1.0/3.6)*np.exp((-La-42.0)/92.0)) #=================================================================== # WHITE POINT transformations (common to forward and inverse modes): #-------------------------------------------- # transform from xyzw to cat sensor space: rgbw = mcat @ xyzw.T #-------------------------------------------- # apply von Kries cat: rgbwc = ((D*Yw/rgbw) + (1 - D))*rgbw # factor 100 from ciecam02 is replaced with Yw[i] in cam16, but see 'note' in Fairchild's "Color Appearance Models" (p291 ni 3ed.) #-------------------------------------------- # convert from cat02 sensor space to cone sensors (hpe): rgbwp = (mhpe_x_invmcat @ rgbwc).T #-------------------------------------------- # apply Naka_rushton repsonse compression to white: NK = lambda x, forward: naka_rushton(x, scaling = 400, n = 0.42, sig = 27.13**(1/0.42), noise = 0.1, forward = forward) rgbwpa = NK(FL*rgbwp/100.0, True) pw = np.where(rgbwp<0) rgbwpa[pw] = 0.1 - (NK(FL*np.abs(rgbwp[pw])/100.0, True) - 0.1) #-------------------------------------------- # Calculate achromatic signal of white: Aw = (2.0*rgbwpa[...,0] + rgbwpa[...,1] + (1.0/20.0)*rgbwpa[...,2] - 0.305)*Nbb # massage shape of data for broadcasting: if data.ndim == 2: data = data[:,None] #=================================================================== # STIMULUS transformations if forward: #-------------------------------------------- # transform from xyz to cat sensor space: rgb = math.dot23(mcat, data.T) #-------------------------------------------- # apply von Kries cat: rgbc = ((D*Yw/rgbw)[...,None] + (1 - D))*rgb # factor 100 from ciecam02 is replaced with Yw[i] in cam16, but see 'note' in Fairchild's "Color Appearance Models" (p291 ni 3ed.) #-------------------------------------------- # convert from cat02 sensor space to cone sensors (hpe): rgbp = math.dot23(mhpe_x_invmcat,rgbc).T #-------------------------------------------- # apply Naka_rushton repsonse compression: rgbpa = NK(FL*rgbp/100.0, forward) p = np.where(rgbp<0) rgbpa[p] = 0.1 - (NK(FL*np.abs(rgbp[p])/100.0, forward) - 0.1) #-------------------------------------------- # Calculate achromatic signal: A = (2.0*rgbpa[...,0] + rgbpa[...,1] + (1.0/20.0)*rgbpa[...,2] - 0.305)*Nbb #-------------------------------------------- # calculate initial opponent channels: a = rgbpa[...,0] - 12.0*rgbpa[...,1]/11.0 + rgbpa[...,2]/11.0 b = (1.0/9.0)*(rgbpa[...,0] + rgbpa[...,1] - 2.0*rgbpa[...,2]) #-------------------------------------------- # calculate hue h and eccentricity factor, et: h = hue_angle(a,b, htype = 'deg') et = (1.0/4.0)*(np.cos(h*np.pi/180 + 2.0) + 3.8) #-------------------------------------------- # calculate Hue quadrature (if requested in 'out'): if 'H' in outin: H = hue_quadrature(h, unique_hue_data = 'ciecam02') else: H = None #-------------------------------------------- # calculate lightness, J: if ('J' in outin) | ('Q' in outin) | ('C' in outin) | ('M' in outin) | ('s' in outin) | ('aS' in outin) | ('aC' in outin) | ('aM' in outin): J = 100.0* (A / Aw)**(c*z) #-------------------------------------------- # calculate brightness, Q: if ('Q' in outin) | ('s' in outin) | ('aS' in outin): Q = (4.0/c)* ((J/100.0)**0.5) * (Aw + 4.0)*(FL**0.25) #-------------------------------------------- # calculate chroma, C: if ('C' in outin) | ('M' in outin) | ('s' in outin) | ('aS' in outin) | ('aC' in outin) | ('aM' in outin): t = ((50000.0/13.0)*Nc*Ncb*et*((a**2.0 + b**2.0)**0.5)) / (rgbpa[...,0] + rgbpa[...,1] + (21.0/20.0*rgbpa[...,2])) C = (t**0.9)*((J/100.0)**0.5) * (1.64 - 0.29**n)**0.73 #-------------------------------------------- # calculate colorfulness, M: if ('M' in outin) | ('s' in outin) | ('aM' in outin) | ('aS' in outin): M = C*FL**0.25 #-------------------------------------------- # calculate saturation, s: if ('s' in outin) | ('aS' in outin): s = 100.0* (M/Q)**0.5 #-------------------------------------------- # calculate cartesian coordinates: if ('aS' in outin): aS = s*np.cos(h*np.pi/180.0) bS = s*np.sin(h*np.pi/180.0) if ('aC' in outin): aC = C*np.cos(h*np.pi/180.0) bC = C*np.sin(h*np.pi/180.0) if ('aM' in outin): aM = M*np.cos(h*np.pi/180.0) bM = M*np.sin(h*np.pi/180.0) #-------------------------------------------- if outin != ['J','aM','bM']: camout = eval('ajoin(('+','.join(outin)+'))') else: camout = ajoin((J,aM,bM)) if camout.shape[1] == 1: camout = camout[:,0,:] return camout elif forward == False: #-------------------------------------------- # Get Lightness J from data: if ('J' in outin): J = data[...,0].copy() elif ('Q' in outin): Q = data[...,0].copy() J = 100.0*(Q / ((Aw + 4.0)*(FL**0.25)*(4.0/c)))**2.0 else: raise Exception('No lightness or brightness values in data. Inverse CAM-transform not possible!') #-------------------------------------------- # calculate hue h: h = hue_angle(data[...,1],data[...,2], htype = 'deg') #-------------------------------------------- # calculate Colorfulness M or Chroma C or Saturation s from a,b: MCs = (data[...,1]**2.0 + data[...,2]**2.0)**0.5 if ('aS' in outin): Q = (4.0/c)* ((J/100.0)**0.5) * (Aw + 4.0)*(FL**0.25) M = Q*(MCs/100.0)**2.0 C = M/(FL**0.25) if ('aM' in outin): # convert M to C: C = MCs/(FL**0.25) if ('aC' in outin): C = MCs #-------------------------------------------- # calculate t from J, C: t = (C / ((J/100.0)**(1.0/2.0) * (1.64 - 0.29**n)**0.73))**(1.0/0.9) #-------------------------------------------- # calculate eccentricity factor, et: et = (np.cos(h*np.pi/180.0 + 2.0) + 3.8) / 4.0 #-------------------------------------------- # calculate achromatic signal, A: A = Aw*(J/100.0)**(1.0/(c*z)) #-------------------------------------------- # calculate temporary cart. co. at, bt and p1,p2,p3,p4,p5: at = np.cos(h*np.pi/180.0) bt = np.sin(h*np.pi/180.0) p1 = (50000.0/13.0)*Nc*Ncb*et/t p2 = A/Nbb + 0.305 p3 = 21.0/20.0 p4 = p1/bt p5 = p1/at #-------------------------------------------- #q = np.where(np.abs(bt) < np.abs(at))[0] q = (np.abs(bt) < np.abs(at)) b = p2*(2.0 + p3) * (460.0/1403.0) / (p4 + (2.0 + p3) * (220.0/1403.0) * (at/bt) - (27.0/1403.0) + p3*(6300.0/1403.0)) a = b * (at/bt) a[q] = p2[q]*(2.0 + p3) * (460.0/1403.0) / (p5[q] + (2.0 + p3) * (220.0/1403.0) - ((27.0/1403.0) - p3*(6300.0/1403.0)) * (bt[q]/at[q])) b[q] = a[q] * (bt[q]/at[q]) #-------------------------------------------- # calculate post-adaptation values rpa = (460.0*p2 + 451.0*a + 288.0*b) / 1403.0 gpa = (460.0*p2 - 891.0*a - 261.0*b) / 1403.0 bpa = (460.0*p2 - 220.0*a - 6300.0*b) / 1403.0 #-------------------------------------------- # join values: rgbpa = ajoin((rpa,gpa,bpa)) #-------------------------------------------- # decompress signals: rgbp = (100.0/FL)*NK(rgbpa, forward) #-------------------------------------------- # convert from to cone sensors (hpe) cat02 sensor space: rgbc = math.dot23(mcat_x_invmhpe,rgbp.T) #-------------------------------------------- # apply inverse von Kries cat: rgb = rgbc / ((D*Yw/rgbw)[...,None] + (1.0 - D)) #-------------------------------------------- # transform from cat sensor space to xyz: xyz = math.dot23(invmcat,rgb).T return xyz
def cct_to_xyz(ccts, duv=None, cieobs=_CIEOBS, wl=None, mode='lut', out=None, accuracy=0.1, force_out_of_lut=True, upper_cct_max=10.0 * 20, approx_cct_temp=True): """ Convert correlated color temperature (CCT) and Duv (distance above (>0) or below (<0) the Planckian locus) to XYZ tristimulus values. | Finds xyzw_estimated by minimization of: | | F = numpy.sqrt(((100.0*(cct_min - cct)/(cct))**2.0) | + (((duv_min - duv)/(duv))**2.0)) | | with cct,duv the input values and cct_min, duv_min calculated using | luxpy.xyz_to_cct(xyzw_estimated,...). Args: :ccts: | ndarray of cct values :duv: | None or ndarray of duv values, optional | Note that duv can be supplied together with cct values in :ccts: as ndarray with shape (N,2) :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :mode: | 'lut' or 'search', optional | Determines what method to use. :out: | None (or 1), optional | If not None or 1: output a ndarray that contains estimated xyz and minimization results: | (cct_min, duv_min, F_min (objective fcn value)) :wl: | None, optional | Wavelengths used when calculating Planckian radiators. :accuracy: | float, optional | Stop brute-force search when cct :accuracy: is reached. :upper_cct_max: | 10.0**20, optional | Limit brute-force search to this cct. :approx_cct_temp: | True, optional | If True: use xyz_to_cct_HA() to get a first estimate of cct to speed up search. :force_out_of_lut: | True, optional | If True and cct is out of range of the LUT, then switch to brute-force search method, else return numpy.nan values. Returns: :returns: | ndarray with estimated XYZ tristimulus values Note: If duv is not supplied (:ccts:.shape is (N,1) and :duv: is None), source is assumed to be on the Planckian locus. """ # make ccts a min. 2d np.array: if isinstance(ccts, list): ccts = np2dT(np.array(ccts)) else: ccts = np2d(ccts) if len(ccts.shape) > 2: raise Exception('cct_to_xyz(): Input ccts.shape must be <= 2 !') # get cct and duv arrays from :ccts: cct = np2d(ccts[:, 0, None]) if (duv is None) & (ccts.shape[1] == 2): duv = np2d(ccts[:, 1, None]) elif duv is not None: duv = np2d(duv) #get estimates of approximate xyz values in case duv = None: BB = cri_ref(ccts=cct, wl3=wl, ref_type=['BB']) xyz_est = spd_to_xyz(data=BB, cieobs=cieobs, out=1) results = np.ones([ccts.shape[0], 3]) * np.nan if duv is not None: # optimization/minimization setup: def objfcn(uv_offset, uv0, cct, duv, out=1): #, cieobs = cieobs, wl = wl, mode = mode): uv0 = np2d(uv0 + uv_offset) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) cct_min, duv_min = xyz_to_cct(Yuv_to_xyz(Yuv0), cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) F = np.sqrt(((100.0 * (cct_min[0] - cct[0]) / (cct[0]))**2.0) + (((duv_min[0] - duv[0]) / (duv[0]))**2.0)) if out == 'F': return F else: return np.concatenate((cct_min, duv_min, np2d(F)), axis=1) # loop through each xyz_est: for i in range(xyz_est.shape[0]): xyz0 = xyz_est[i] cct_i = cct[i] duv_i = duv[i] cct_min, duv_min = xyz_to_cct(xyz0, cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) if np.abs(duv[i]) > _EPS: # find xyz: Yuv0 = xyz_to_Yuv(xyz0) uv0 = Yuv0[0][1:3] OptimizeResult = minimize(fun=objfcn, x0=np.zeros((1, 2)), args=(uv0, cct_i, duv_i, 'F'), method='Nelder-Mead', options={ "maxiter": np.inf, "maxfev": np.inf, 'xatol': 0.000001, 'fatol': 0.000001 }) betas = OptimizeResult['x'] #betas = np.zeros(uv0.shape) if out is not None: results[i] = objfcn(betas, uv0, cct_i, duv_i, out=3) uv0 = np2d(uv0 + betas) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) xyz_est[i] = Yuv_to_xyz(Yuv0) else: xyz_est[i] = xyz0 if (out is None) | (out == 1): return xyz_est else: # Also output results of minimization: return np.concatenate((xyz_est, results), axis=1)
def get_macadam_ellipse(xy=None, k_neighbours=3, nsteps=10, average_cik=True): """ Estimate n-step MacAdam ellipse at CIE x,y coordinates xy by calculating average inverse covariance ellipse of the k_neighbours closest ellipses. Args: :xy: | None or ndarray, optional | If None: output Macadam ellipses, if not None: xy are the | CIE xy coordinates for which ellipses will be estimated. :k_neighbours: | 3, optional | Number of nearest ellipses to use to calculate ellipse at xy :nsteps: | 10, optional | Set number of MacAdam steps of ellipse. :average_cik: | True, optional | If True: take distance weighted average of inverse | 'covariance ellipse' elements cik. | If False: average major & minor axis lengths and | ellipse orientation angles directly. Returns: :v_mac_est: | estimated MacAdam ellipse(s) in v-format [Rmax,Rmin,xc,yc,theta] """ # list of MacAdam ellipses (x10) v_mac = np.atleast_2d([[0.16, 0.057, 0.0085, 0.0035, 62.5], [0.187, 0.118, 0.022, 0.0055, 77], [0.253, 0.125, 0.025, 0.005, 55.5], [0.15, 0.68, 0.096, 0.023, 105], [0.131, 0.521, 0.047, 0.02, 112.5], [0.212, 0.55, 0.058, 0.023, 100], [0.258, 0.45, 0.05, 0.02, 92], [0.152, 0.365, 0.038, 0.019, 110], [0.28, 0.385, 0.04, 0.015, 75.5], [0.38, 0.498, 0.044, 0.012, 70], [0.16, 0.2, 0.021, 0.0095, 104], [0.228, 0.25, 0.031, 0.009, 72], [0.305, 0.323, 0.023, 0.009, 58], [0.385, 0.393, 0.038, 0.016, 65.5], [0.472, 0.399, 0.032, 0.014, 51], [0.527, 0.35, 0.026, 0.013, 20], [0.475, 0.3, 0.029, 0.011, 28.5], [0.51, 0.236, 0.024, 0.012, 29.5], [0.596, 0.283, 0.026, 0.013, 13], [0.344, 0.284, 0.023, 0.009, 60], [0.39, 0.237, 0.025, 0.01, 47], [0.441, 0.198, 0.028, 0.0095, 34.5], [0.278, 0.223, 0.024, 0.0055, 57.5], [0.3, 0.163, 0.029, 0.006, 54], [0.365, 0.153, 0.036, 0.0095, 40]]) # convert to v-format ([a,b, xc, yc, theta]): v_mac = v_mac[:, [2, 3, 0, 1, 4]] # convert last column to rad.: v_mac[:, -1] = v_mac[:, -1] * np.pi / 180 # convert to desired number of MacAdam-steps: v_mac[:, 0:2] = v_mac[:, 0:2] / 10 * nsteps if xy is not None: #calculate inverse covariance matrices: cik = math.v_to_cik(v_mac, inverse=True) if average_cik == True: cik_long = np.hstack((cik[:, 0, :], cik[:, 1, :])) # Calculate k_neighbours closest ellipses to xy: tree = cKDTree(v_mac[:, 2:4], copy_data=True) d, inds = tree.query(xy, k=k_neighbours) if k_neighbours > 1: pd = 1 w = (1.0 / np.abs(d)**pd)[:, :, None] # inverse distance weigthing if average_cik == True: cik_long_est = np.sum(w * cik_long[inds, :], axis=1) / np.sum( w, axis=1) else: v_mac_est = np.sum(w * v_mac[inds, :], axis=1) / np.sum( w, axis=1) # for average xyc else: v_mac_est = v_mac[inds, :].copy() # convert cik back to v: if (average_cik == True) & (k_neighbours > 1): cik_est = np.dstack((cik_long_est[:, 0:2], cik_long_est[:, 2:4])) v_mac_est = math.cik_to_v(cik_est, inverse=True) v_mac_est[:, 2:4] = xy else: v_mac_est = v_mac return v_mac_est