def plotDL(ccts = None, cieobs =_CIEOBS, cspace = _CSPACE, axh = None, \ show = True, force_daylight_below4000K = False, cspace_pars = {}, \ formatstr = 'k-', **kwargs): """ Plot daylight locus. Args: :ccts: | None or list[float], optional | None defaults to [4000 K to 1e19 K] in 100 steps on a log10 scale. :force_daylight_below4000K: | False or True, optional | CIE daylight phases are not defined below 4000 K. | If True plot anyway. :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. :cspace: | luxpy._CSPACE or str, optional | Determines color space / chromaticity diagram to plot data in. | Note that data is expected to be in specified :cspace: :formatstr: | 'k-' or str, optional | Format str for plotting (see ?matplotlib.pyplot.plot) :cspace_pars: | {} or dict, optional | Dict with parameters required by color space specified in :cspace: (for use with luxpy.colortf()) :kwargs: | additional keyword arguments for use with matplotlib.pyplot. Returns: :returns: | None (:show: == True) | or | handle to current axes (:show: == False) """ if ccts is None: ccts = 10**np.linspace(np.log10(4000.0),np.log10(10.0**19.0),100.0) xD,yD = daylightlocus(ccts, force_daylight_below4000K = force_daylight_below4000K) Y = 100*np.ones(xD.shape) DL = Yxy_to_xyz(np.vstack((Y, xD,yD)).T) DL = colortf(DL, tf = cspace, tfa0 = cspace_pars) Y,x,y = asplit(DL) axh = plot_color_data(x,y,axh = axh, cieobs = cieobs, cspace = cspace, show=show, formatstr=formatstr, **kwargs) if show == False: return axh
def xyz_to_cct_search(xyzw, cieobs=_CIEOBS, out='cct', wl=None, accuracy=0.1, upper_cct_max=10.0**20, approx_cct_temp=True): """ Convert XYZ tristimulus values to correlated color temperature (CCT) and Duv(distance above (> 0) or below ( < 0) the Planckian locus) by a brute-force search. | The algorithm uses an approximate cct_temp (HA approx., see xyz_to_cct_HA) as starting point or uses the middle of the allowed cct-range (1e2 K - 1e20 K, higher causes overflow) on a log-scale, then constructs a 4-step section of the blackbody (Planckian) locus on which to find the minimum distance to the 1960 uv chromaticity of the test source. Args: :xyzw: | ndarray of tristimulus values :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :out: | 'cct' (or 1), optional | Determines what to return. | Other options: 'duv' (or -1), 'cct,duv'(or 2), "[cct,duv]" (or -2) :wl: | None, optional | Wavelengths used when calculating Planckian radiators. :accuracy: | float, optional | Stop brute-force search when cct :accuracy: is reached. :upper_cct_max: | 10.0**20, optional | Limit brute-force search to this cct. :approx_cct_temp: | True, optional | If True: use xyz_to_cct_HA() to get a first estimate of cct to speed up search. Returns: :returns: | ndarray with: | cct: out == 'cct' (or 1) | duv: out == 'duv' (or -1) | cct, duv: out == 'cct,duv' (or 2) | [cct,duv]: out == "[cct,duv]" (or -2) Notes: This program is more accurate, but slower than xyz_to_cct_ohno! Note that cct must be between 1e3 K - 1e20 K (very large cct take a long time!!!) """ xyzw = np2d(xyzw) if len(xyzw.shape) > 2: raise Exception('xyz_to_cct_search(): Input xyzw.shape must be <= 2 !') # get 1960 u,v of test source: Yuvt = xyz_to_Yuv(np.squeeze( xyzw)) # remove possible 1-dim + convert xyzw to CIE 1976 u',v' #axis_of_v3t = len(Yuvt.shape)-1 # axis containing color components ut = Yuvt[:, 1, None] #.take([1],axis = axis_of_v3t) # get CIE 1960 u vt = (2 / 3) * Yuvt[:, 2, None] #.take([2],axis = axis_of_v3t) # get CIE 1960 v # Initialize arrays: ccts = np.ones((xyzw.shape[0], 1)) * np.nan duvs = ccts.copy() #calculate preliminary solution(s): if (approx_cct_temp == True): ccts_est = xyz_to_cct_HA(xyzw) procent_estimates = np.array([[3000.0, 100000.0, 0.05], [100000.0, 200000.0, 0.1], [200000.0, 300000.0, 0.25], [300000.0, 400000.0, 0.4], [400000.0, 600000.0, 0.4], [600000.0, 800000.0, 0.4], [800000.0, np.inf, 0.25]]) else: upper_cct = np.array(upper_cct_max) lower_cct = np.array(10.0**2) cct_scale_fun = lambda x: np.log10(x) cct_scale_ifun = lambda x: np.power(10.0, x) dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2 ccttemp = np.array([cct_scale_ifun(cct_scale_fun(lower_cct) + dT)]) ccts_est = np2d(ccttemp * np.ones((xyzw.shape[0], 1))) dT_approx_cct_False = dT.copy() # Loop through all ccts: for i in range(xyzw.shape[0]): #initialize CCT search parameters: cct = np.nan duv = np.nan ccttemp = ccts_est[i].copy() # Take care of (-1, NaN)'s from xyz_to_cct_HA signifying (CCT < lower, CCT > upper) bounds: approx_cct_temp_temp = approx_cct_temp if (approx_cct_temp == True): cct_scale_fun = lambda x: x cct_scale_ifun = lambda x: x if (ccttemp != -1) & ( np.isnan(ccttemp) == False ): # within validity range of CCT estimator-function for ii in range(procent_estimates.shape[0]): if (ccttemp >= (1.0 - 0.05 * (ii == 0)) * procent_estimates[ii, 0]) & ( ccttemp < (1.0 + 0.05 * (ii == 0)) * procent_estimates[ii, 1]): procent_estimate = procent_estimates[ii, 2] break dT = np.multiply( ccttemp, procent_estimate ) # determines range around CCTtemp (25% around estimate) or 100 K elif (ccttemp == -1) & (np.isnan(ccttemp) == False): ccttemp = np.array([procent_estimates[0, 0] / 2]) procent_estimate = 1 # cover 0 K to min_CCT of estimator dT = np.multiply(ccttemp, procent_estimate) elif (np.isnan(ccttemp) == True): upper_cct = np.array(upper_cct_max) lower_cct = np.array(10.0**2) cct_scale_fun = lambda x: np.log10(x) cct_scale_ifun = lambda x: np.power(10.0, x) dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2 ccttemp = np.array( [cct_scale_ifun(cct_scale_fun(lower_cct) + dT)]) approx_cct_temp = False else: dT = dT_approx_cct_False nsteps = 3 signduv = 1.0 ccttemp = ccttemp[0] delta_cct = dT while ((delta_cct > accuracy)): # keep converging on CCT #generate range of ccts: ccts_i = cct_scale_ifun( np.linspace( cct_scale_fun(ccttemp) - dT, cct_scale_fun(ccttemp) + dT, nsteps + 1)) ccts_i[ccts_i < 100.0] = 100.0 # avoid nan's in calculation # Generate BB: BB = cri_ref(ccts_i, wl3=wl, ref_type=['BB'], cieobs=cieobs) # Calculate xyz: xyz = spd_to_xyz(BB, cieobs=cieobs) # Convert to CIE 1960 u,v: Yuv = xyz_to_Yuv(np.squeeze( xyz)) # remove possible 1-dim + convert xyz to CIE 1976 u',v' #axis_of_v3 = len(Yuv.shape)-1 # axis containing color components u = Yuv[:, 1, None] # get CIE 1960 u v = (2.0 / 3.0) * Yuv[:, 2, None] # get CIE 1960 v # Calculate distance between list of uv's and uv of test source: dc = ((ut[i] - u)**2 + (vt[i] - v)**2)**0.5 if np.isnan(dc.min()) == False: #eps = _EPS q = dc.argmin() if np.size( q ) > 1: #to minimize calculation time: only calculate median when necessary cct = np.median(ccts[q]) duv = np.median(dc[q]) q = np.median(q) q = int(q) #must be able to serve as index else: cct = ccts_i[q] duv = dc[q] if (q == 0): ccttemp = cct_scale_ifun( np.array(cct_scale_fun([cct])) + 2 * dT / nsteps) #dT = 2.0*dT/nsteps continue # look in higher section of planckian locus if (q == np.size(ccts_i)): ccttemp = cct_scale_ifun( np.array(cct_scale_fun([cct])) - 2 * dT / nsteps) #dT = 2.0*dT/nsteps continue # look in lower section of planckian locus if (q > 0) & (q < np.size(ccts_i) - 1): dT = 2 * dT / nsteps # get Duv sign: d_p1m1 = ((u[q + 1] - u[q - 1])**2.0 + (v[q + 1] - v[q - 1])**2.0)**0.5 x = (dc[q - 1]**2.0 - dc[q + 1]**2.0 + d_p1m1**2.0) / 2.0 * d_p1m1 vBB = v[q - 1] + ((v[q + 1] - v[q - 1]) * (x / d_p1m1)) signduv = np.sign(vt[i] - vBB) #calculate difference with previous intermediate solution: delta_cct = abs(cct - ccttemp) ccttemp = np.array(cct) #%set new intermediate CCT approx_cct_temp = approx_cct_temp_temp else: ccttemp = np.nan cct = np.nan duv = np.nan duvs[i] = signduv * abs(duv) ccts[i] = cct # Regulate output: if (out == 'cct') | (out == 1): return np2d(ccts) elif (out == 'duv') | (out == -1): return np2d(duvs) elif (out == 'cct,duv') | (out == 2): return np2d(ccts), np2d(duvs) elif (out == "[cct,duv]") | (out == -2): return np.vstack((ccts, duvs)).T
def get_degree_of_adaptation(Dtype=None, **kwargs): """ Calculates the degree of adaptation according to some function published in literature. Args: :Dtype: | None, optional | If None: kwargs should contain 'D' with value. | If 'manual: kwargs should contain 'D' with value. | If 'cat02' or 'cat16': kwargs should contain keys 'F' and 'La'. | Calculate D according to CAT02 or CAT16 model: | D = F*(1-(1/3.6)*numpy.exp((-La-42)/92)) | If 'cmc': kwargs should contain 'La', 'La0'(or 'La2') and 'order' | for 'order' = '1>0': 'La' is set La1 and 'La0' to La0. | for 'order' = '0>2': 'La' is set La0 and 'La0' to La1. | for 'order' = '1>2': 'La' is set La1 and 'La2' to La0. | D is calculated as follows: | D = 0.08*numpy.log10(La1+La0)+0.76-0.45*(La1-La0)/(La1+La0) | If 'smet2017': kwargs should contain 'xyzw' and 'Dmax' (see Smet2017_D for more details). | If "? user defined", then D is calculated by: | D = ndarray(eval(:Dtype:)) Returns: :D: | ndarray with degree of adaptation values. Notes: 1. D passes either right through or D is calculated following some D-function (Dtype) published in literature. 2. D is limited to values between zero and one 3. If kwargs do not contain the required parameters, an exception is raised. """ try: if Dtype is None: PAR = ["D"] D = np.array([kwargs['D']]) elif Dtype == 'manual': PAR = ["D"] D = np.array([kwargs['D']]) elif (Dtype == 'cat02') | (Dtype == 'cat16'): PAR = ["F, La"] F = kwargs['F'] if isinstance(F, str): #CIECAM02 / CAT02 surround based F values if (F == 'avg') | (F == 'average'): F = 1 elif (F == 'dim'): F = 0.9 elif (F == 'dark'): F = 0.8 elif (F == 'disp') | (F == 'display'): F = 0.0 else: F = eval(F) F = np.array([F]) La = np.array([kwargs['La']]) D = F * (1 - (1 / 3.6) * np.exp((-La - 42) / 92)) elif Dtype == 'cmc': PAR = ["La, La0, order"] order = np.array([kwargs['order']]) if order == '1>0': La1 = np.array([kwargs['La']]) La0 = np.array([kwargs['La0']]) elif order == '0>2': La0 = np.array([kwargs['La']]) La1 = np.array([kwargs['La0']]) elif order == '1>2': La1 = np.array([kwargs['La']]) La0 = np.array([kwargs['La2']]) D = 0.08 * np.log10(La1 + La0) + 0.76 - 0.45 * (La1 - La0) / (La1 + La0) elif 'smet2017': PAR = ['xyzw', 'Dmax'] xyzw = np.array([kwargs['xyzw']]) Dmax = np.array([kwargs['Dmax']]) D = smet2017_D(xyzw, Dmax=Dmax) else: PAR = ["? user defined"] D = np.array(eval(Dtype)) D[np.where(D < 0)] = 0 D[np.where(D > 1)] = 1 except: raise Exception( 'degree_of_adaptation_D(): **kwargs does not contain the necessary parameters ({}) for Dtype = {}' .format(PAR, Dtype)) return D
def plotBB(ccts = None, cieobs =_CIEOBS, cspace = _CSPACE, axh = None, cctlabels = True, show = True, cspace_pars = {}, formatstr = 'k-', **kwargs): """ Plot blackbody locus. Args: :ccts: | None or list[float], optional | None defaults to [1000 to 1e19 K]. | Range: | [1000,1500,2000,2500,3000,3500,4000,5000,6000,8000,10000] | + [15000 K to 1e19 K] in 100 steps on a log10 scale :cctlabels: | True or False, optional | Add cct text labels at various points along the blackbody locus. :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. :cspace: | luxpy._CSPACE or str, optional | Determines color space / chromaticity diagram to plot data in. | Note that data is expected to be in specified :cspace: :formatstr: | 'k-' or str, optional | Format str for plotting (see ?matplotlib.pyplot.plot) :cspace_pars: | {} or dict, optional | Dict with parameters required by color space specified in :cspace: (for use with luxpy.colortf()) :kwargs: | additional keyword arguments for use with matplotlib.pyplot. Returns: :returns: | None (:show: == True) | or | handle to current axes (:show: == False) """ if ccts is None: ccts1 = np.array([1000.0,1500.0,2000.0,2500.0,3000.0,3500.0,4000.0,5000.0,6000.0,8000.0,10000.0]) ccts2 = 10**np.linspace(np.log10(15000.0),np.log10(10.0**19.0),100.0) ccts = np.hstack((ccts1,ccts2)) else: ccts1 = None BB = cri_ref(ccts,ref_type='BB') xyz = spd_to_xyz(BB,cieobs = cieobs) Yxy = colortf(xyz, tf = cspace, tfa0 = cspace_pars) Y,x,y = asplit(Yxy) axh = plot_color_data(x,y,axh = axh, cieobs = cieobs, cspace = cspace, show=show, formatstr=formatstr, **kwargs) if (cctlabels == True) & (ccts1 is not None): for i in range(ccts1.shape[0]): if ccts1[i]>= 3000.0: if i%2 == 0.0: plt.plot(x[i],y[i],'k+', color = '0.5') plt.text(x[i]*1.05,y[i]*0.95,'{:1.0f}K'.format(ccts1[i]), color = '0.5') plt.plot(x[-1],y[-1],'k+', color = '0.5') plt.text(x[-1]*1.05,y[-1]*0.95,'{:1.0e}K'.format(ccts[-1]), color = '0.5') if show == False: return axh