def add_to_cmf_dict(bar=None, cieobs='indv', K=683, M=np.eye(3)): """ Add set of cmfs to _CMF dict. Args: :bar: | None, optional | Set of CMFs. None: initializes to empty ndarray. :cieobs: | 'indv' or str, optional | Name of CMF set. :K: | 683 (lm/W), optional | Conversion factor from radiometric to photometric quantity. :M: | np.eye, optional | Matrix for lms to xyz conversion. """ if bar is None: wl3 = getwlr(_WL3) bar = np.vstack((wl3, np.empty((3, wl3.shape[0])))) _CMF['types'].append(cieobs) _CMF[cieobs] = {'bar': bar} _CMF[cieobs]['K'] = K _CMF[cieobs]['M'] = M
def v_to_cik(v, inverse=False): """ Calculate 2x2 '(covariance matrix)^-1' elements cik Args: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] :inverse: | If True: return inverse of cik. Returns: :cik: 'Nx2x2' (covariance matrix)^-1 Notes: | cik is not actually a covariance matrix, | only for a Gaussian or normal distribution! """ v = np.atleast_2d(v) g11 = (1 / v[:, 0] * np.cos(v[:, 4]))**2 + (1 / v[:, 1] * np.sin(v[:, 4]))**2 g22 = (1 / v[:, 0] * np.sin(v[:, 4]))**2 + (1 / v[:, 1] * np.cos(v[:, 4]))**2 g12 = (1 / v[:, 0]**2 - 1 / v[:, 1]**2) * np.sin(v[:, 4]) * np.cos(v[:, 4]) cik = np.zeros((g11.shape[0], 2, 2)) for i in range(g11.shape[0]): cik[i, :, :] = np.vstack((np.hstack( (g11[i], g12[i])), np.hstack((g12[i], g22[i])))) if inverse == True: cik[i, :, :] = np.linalg.inv(cik[i, :, :]) return cik
def _complete_ldt_lid(LDT, Isym=4): """ Convert LDT LID map with Isym symmetry to a 'full' map with phi: [0,360] and theta: [0,180]. """ cangles = LDT['h_angs'] tangles = LDT['v_angs'] candela_2d = LDT['candela_2d'] if Isym == 4: # complete cangles: a = candela_2d.copy().T b = np.hstack((a, a[:, (a.shape[1] - 2)::-1])) c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1])) candela_2d_0C360 = np.hstack((c, c[:, :1])) cangles = np.hstack( (cangles, cangles[1:] + 90, cangles[1:] + 180, cangles[1:] + 270)) # complete tangles: a = candela_2d_0C360.copy() b = np.vstack((a, np.zeros(a.shape)[1:, :])) tangles = np.hstack((tangles, tangles[1:] + 90)) candela_2d = b elif Isym == -4: # complete cangles: a = candela_2d.copy().T b = np.hstack((a, a[:, (a.shape[1] - 2)::-1])) c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1])) candela_2d_0C360 = np.hstack((c, c[:, :1])) cangles = np.hstack( (cangles, -cangles[(cangles.shape[0] - 2)::-1] + 180)) cangles = np.hstack( (cangles, -cangles[(cangles.shape[0] - 2):0:-1] + 360)) cangles = np.hstack((cangles, cangles[:1])) # complete tangles: a = candela_2d_0C360.copy() b = np.vstack((a, np.zeros(a.shape)[1:, :])) tangles = np.hstack( (tangles, -tangles[(tangles.shape[0] - 2)::-1] + 180)) candela_2d = b else: raise Exception( 'complete_ldt_lid(): Other "Isym" than "4", not yet implemented (31/10/2018).' ) LDT['map'] = {'thetas': tangles} LDT['map']['phis'] = cangles LDT['map']['values'] = candela_2d.T return LDT
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 normalize_to_Lw(Ill, Lw, cieobs, rflM): xyzw = lx.spd_to_xyz(Ill, cieobs = cieobs, relative = False) for i in range(Ill.shape[0]-1): Ill[i+1] = Lw*Ill[i+1]/xyzw[i,1] IllM = [] for i in range(Ill.shape[0]-1): IllM.append(np.vstack((Ill1[0],Ill[i+1]*rflM[1:,:]))) IllM = np.transpose(np.array(IllM),(1,0,2)) return Ill, IllM
def plotceruleanline(cieobs=_CIEOBS, cspace=_CSPACE, axh=None, formatstr='ko-', cspace_pars={}): """ Plot cerulean (yellow (577 nm) - blue (472 nm)) line | Kuehni, CRA, 2014: | Table II: spectral lights. Args: :axh: | None or axes handle, optional | Determines axes to plot data in. | None: make new figure. :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: | handle to cerulean line References: 1. `Kuehni, R. G. (2014). Unique hues and their stimuli—state of the art. Color Research & Application, 39(3), 279–287. <https://doi.org/10.1002/col.21793>`_ (see Table II, IV) """ cmf = _CMF[cieobs]['bar'] p_y = cmf[0] == 577.0 #Kuehni, CRA 2013 (mean, table IV) p_b = cmf[0] == 472.0 #Kuehni, CRA 2013 (mean, table IV) xyz_y = cmf[1:, p_y].T xyz_b = cmf[1:, p_b].T lab = colortf(np.vstack((xyz_b, xyz_y)), tf=cspace, tfa0=cspace_pars) if axh is None: axh = plt.gca() hcerline = axh.plot(lab[:, 1], lab[:, 2], formatstr, label='Cerulean line') return hcerline
def interpolate_efficiency_functions(wl, cs_cl_lrs): """ Interpolate all spectral data in dict cs_cl_lrs to new wavelength range. """ for key in cs_cl_lrs: if key[-1] == 'l': #signifies l for spectral data temp = np.vstack((cs_cl_lrs['WL'],cs_cl_lrs[key])) # construct [wl,S] data cs_cl_lrs[key] = cie_interp(temp,wl, kind = 'cmf')[1:] # interpolate and store in dict cs_cl_lrs['WL'] = wl # store new wavelength range return cs_cl_lrs
def cik_to_v(cik, xyc=None, inverse=False): """ Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik Args: :cik: | 'Nx2x2' (covariance matrix)^-1 :inverse: | If True: input is inverse of cik. Returns: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] Notes: | cik is not actually the inverse covariance matrix, | only for a Gaussian or normal distribution! """ if cik.ndim < 3: cik = cik[None, ...] if inverse == True: for i in range(cik.shape[0]): cik[i, :, :] = np.linalg.inv(cik[i, :, :]) g11 = cik[:, 0, 0] g22 = cik[:, 1, 1] g12 = cik[:, 0, 1] theta = 0.5 * np.arctan2(2 * g12, (g11 - g22)) + (np.pi / 2) * (g12 < 0) #theta = theta2 + (np.pi/2)*(g12<0) #theta2 = theta cottheta = np.cos(theta) / np.sin(theta) #np.cot(theta) cottheta[np.isinf(cottheta)] = 0 a = 1 / np.sqrt((g22 + g12 * cottheta)) b = 1 / np.sqrt((g11 - g12 * cottheta)) # ensure largest ellipse axis is first (correct angle): c = b > a a[c], b[c], theta[c] = b[c], a[c], theta[c] + np.pi / 2 v = np.vstack((a, b, np.zeros(a.shape), np.zeros(a.shape), theta)).T # add center coordinates: if xyc is not None: v[:, 2:4] = xyc return v
def lmsb_to_xyzb(lms, fieldsize=10, out='XYZ', allow_negative_values=False): """ Convert from LMS cone fundamentals to XYZ color matching functions. Args: :lms: | ndarray with lms cone fundamentals, optional :fieldsize: | fieldsize in degrees, optional | Defaults to 10°. :out: | 'xyz' or str, optional | Determines output. :allow_negative_values: | False, optional | XYZ color matching functions should not have negative values. | If False: xyz[xyz<0] = 0. Returns: :returns: | LMS | - LMS: ndarray with population XYZ color matching functions. Note: For intermediate field sizes (2° < fieldsize < 10°) a conversion matrix is calculated by linear interpolation between the _INDVCMF_M_2d and _INDVCMF_M_10d matrices. """ wl = lms[None, 0] #store wavelengths M = get_lms_to_xyz_matrix(fieldsize=fieldsize) if lms.ndim > 2: xyz = np.vstack((wl, math.dot23(M, lms[1:, ...], keepdims=False))) else: xyz = np.vstack((wl, np.dot(M, lms[1:, ...]))) if allow_negative_values == False: xyz[np.where(xyz < 0)] = 0 return xyz
def crowdingdistance(F): """ Computes the crowding distance of a nondominated front. The crowding distance gives a measure of how close the individuals are with regard to its neighbors. The higher this value, the greater the spacing. This is used to promote better diversity in the population. Args: F: | an m x mu ndarray with mu individuals and m objectives Returns: cdist: | a m-length column vector """ m, mu = F.shape #gets the size of F if mu == 2: cdist = np.vstack((np.inf, np.inf)) return cdist #[Fs, Is] = sort(F,2); #sorts the objectives by individuals Is = F.argsort(axis=1) Fs = np.sort(F, axis=1) # Creates the numerator C = Fs[:, 2:] - Fs[:, :-2] C = np.hstack((np.inf * np.ones((m, 1)), C, np.inf * np.ones( (m, 1)))) #complements with inf in the extremes # Indexing to permute the C matrix in the right ordering Aux = np.arange(m).repeat(mu).reshape(m, mu) ind = np.ravel_multi_index( (Aux.flatten(), Is.flatten()), (m, mu )) #converts to lin. indexes # ind = sub2ind([m, mu], Aux(:), Is(:)); C2 = C.flatten().copy() C2[ind] = C2.flatten() C = C2.reshape((m, mu)) # Constructs the denominator den = np.repeat((Fs[:, -1] - Fs[:, 0])[:, None], mu, axis=1) # Calculates the crowding distance cdist = (C / den).sum(axis=0) cdist = cdist.flatten() #assures a column vector return cdist
def cik_to_v(cik, xyc=None, inverse=False): """ Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik Args: :cik: '2x2xN' (covariance matrix)^-1 Returns: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] Notes: | cik is not actually the inverse covariance matrix, | only for a Gaussian or normal distribution! """ if inverse == True: for i in np.arange(cik.shape[0]): cik[i, :, :] = np.linalg.inv(cik[i, :, :]) g11 = cik[:, 0, 0] g22 = cik[:, 1, 1] g12 = cik[:, 0, 1] theta2 = 1 / 2 * np.arctan2(2 * g12, (g11 - g22)) theta = theta2 + (np.pi / 2) * (g12 < 0) theta2 = theta cottheta = np.cos(theta) / np.sin(theta) #np.cot(theta) cottheta[np.isinf(cottheta)] = 0 a = 1 / np.sqrt((g22 + g12 * cottheta)) b = 1 / np.sqrt((g11 - g12 * cottheta)) v = np.vstack((a, b, np.zeros(a.shape), np.zeros(a.shape), theta)).T # add center coordinates: if xyc is not None: v[:, 2:4] = xyc return v
def rgb_to_spec_smits(rgb, intent='rfl', bitdepth=8, wlr=_WL3, rgb2spec=None): """ Convert an array of RGB values to a spectrum using a Smits like conversion as implemented in Mitsuba. Args: :rgb: | ndarray of list of rgb values :intent: | 'rfl' (or 'spd'), optional | type of requested spectrum conversion . :bitdepth: | 8, optional | bit depth of rgb values :wlr: | _WL3, optional | desired wavelength (nm) range of spectrum. :rgb2spec: | None, optional | Dict with base spectra for white, cyan, magenta, yellow, blue, green and red for each intent. | If None: use _BASESPEC_SMITS. Returns: :spec: | ndarray with spectrum or spectra (one for each rgb value, first row are the wavelengths) """ if isinstance(rgb, list): rgb = np.atleast_2d(rgb) if rgb.max() > 1: rgb = rgb / (2**bitdepth - 1) if rgb2spec is None: rgb2spec = _BASESPEC_SMITS if not np.array_equal(rgb2spec['wlr'], getwlr(wlr)): rgb2spec = _convert_to_wlr(entries=copy.deepcopy(rgb2spec), wlr=wlr) spec = np.zeros((rgb.shape[0], rgb2spec['wlr'].shape[0])) for i in range(rgb.shape[0]): spec[i, :] = _fromLinearRGB(rgb[i, :], intent=intent, rgb2spec=rgb2spec, wlr=wlr) return np.vstack((rgb2spec['wlr'], spec))
def get_poly_model(jabt, jabr, modeltype = _VF_MODEL_TYPE): """ Setup base color shift model (delta_a, delta_b), determine model parameters and accuracy. | Calculates a base color shift (delta) from the ref. chromaticity ar, br. Args: :jabt: | ndarray with jab color coordinates under the test SPD. :jabr: | ndarray with jab color coordinates under the reference SPD. :modeltype: | _VF_MODEL_TYPE or 'M6' or 'M5', optional | Specifies degree 5 or degree 6 polynomial model in ab-coordinates. (see notes below) Returns: :returns: | (poly_model, | pmodel, | dab_model, | dab_res, | dCHoverC_res, | dab_std, | dCHoverC_std) | | :poly_model: function handle to model | :pmodel: ndarray with model parameters | :dab_model: ndarray with ab model predictions from ar, br. | :dab_res: ndarray with residuals between 'da,db' of samples and | 'da,db' predicted by the model. | :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 below) | :dab_std: ndarray with std of :dab_res: | :dCHoverC_std: ndarray with std of :dCHoverC_res: Notes: 1. Model types: | poly5_model = lambda a,b,p: p[0]*a + p[1]*b + p[2]*(a**2) + p[3]*a*b + p[4]*(b**2) | poly6_model = lambda a,b,p: p[0] + p[1]*a + p[2]*b + p[3]*(a**2) + p[4]*a*b + p[5]*(b**2) 2. Calculation of dCoverC and dH: | dCoverC = (np.cos(hr)*da + np.sin(hr)*db)/Cr | dHoverC = (np.cos(hr)*db - np.sin(hr)*da)/Cr """ at = jabt[...,1] bt = jabt[...,2] ar = jabr[...,1] br = jabr[...,2] # A. Calculate da, db: da = at - ar db = bt - br # B.1 Calculate model matrix: # 5-parameter model: M5 = np.array([[np.sum(ar*ar), np.sum(ar*br), np.sum(ar*ar**2),np.sum(ar*ar*br),np.sum(ar*br**2)], [np.sum(br*ar), np.sum(br*br), np.sum(br*ar**2),np.sum(br*ar*br),np.sum(br*br**2)], [np.sum((ar**2)*ar), np.sum((ar**2)*br), np.sum((ar**2)*ar**2),np.sum((ar**2)*ar*br),np.sum((ar**2)*br**2)], [np.sum(ar*br*ar), np.sum(ar*br*br), np.sum(ar*br*ar**2),np.sum(ar*br*ar*br),np.sum(ar*br*br**2)], [np.sum((br**2)*ar), np.sum((br**2)*br), np.sum((br**2)*ar**2),np.sum((br**2)*ar*br),np.sum((br**2)*br**2)]]) #6-parameters model M6 = np.array([[ar.size,np.sum(1.0*ar), np.sum(1.0*br), np.sum(1.0*ar**2),np.sum(1.0*ar*br),np.sum(1.0*br**2)], [np.sum(ar*1.0),np.sum(ar*ar), np.sum(ar*br), np.sum(ar*ar**2),np.sum(ar*ar*br),np.sum(ar*br**2)], [np.sum(br*1.0),np.sum(br*ar), np.sum(br*br), np.sum(br*ar**2),np.sum(br*ar*br),np.sum(br*br**2)], [np.sum((ar**2)*1.0),np.sum((ar**2)*ar), np.sum((ar**2)*br), np.sum((ar**2)*ar**2),np.sum((ar**2)*ar*br),np.sum((ar**2)*br**2)], [np.sum(ar*br*1.0),np.sum(ar*br*ar), np.sum(ar*br*br), np.sum(ar*br*ar**2),np.sum(ar*br*ar*br),np.sum(ar*br*br**2)], [np.sum((br**2)*1.0),np.sum((br**2)*ar), np.sum((br**2)*br), np.sum((br**2)*ar**2),np.sum((br**2)*ar*br),np.sum((br**2)*br**2)]]) # B.2 Define model function: poly5_model = lambda a,b,p: p[0]*a + p[1]*b + p[2]*(a**2) + p[3]*a*b + p[4]*(b**2) poly6_model = lambda a,b,p: p[0] + p[1]*a + p[2]*b + p[3]*(a**2) + p[4]*a*b + p[5]*(b**2) if modeltype == 'M5': M = M5 poly_model = poly5_model else: M = M6 poly_model = poly6_model M = np.linalg.inv(M) # C.1 Data a,b analysis output: if modeltype == 'M5': da_model_parameters = np.dot(M, np.array([np.sum(da*ar), np.sum(da*br), np.sum(da*ar**2),np.sum(da*ar*br),np.sum(da*br**2)])) db_model_parameters = np.dot(M, np.array([np.sum(db*ar), np.sum(db*br), np.sum(db*ar**2),np.sum(db*ar*br),np.sum(db*br**2)])) else: da_model_parameters = np.dot(M, np.array([np.sum(da*1.0),np.sum(da*ar), np.sum(da*br), np.sum(da*ar**2),np.sum(da*ar*br),np.sum(da*br**2)])) db_model_parameters = np.dot(M, np.array([np.sum(db*1.0),np.sum(db*ar), np.sum(db*br), np.sum(db*ar**2),np.sum(db*ar*br),np.sum(db*br**2)])) pmodel = np.vstack((da_model_parameters,db_model_parameters)) # D.1 Calculate model da, db: da_model = poly_model(ar,br,pmodel[0]) db_model = poly_model(ar,br,pmodel[1]) dab_model = np.hstack((da_model,db_model)) # D.2 Calculate residuals for da & db: da_res = da - da_model db_res = db - db_model dab_res = np.hstack((da_res,db_res)) dab_std = np.vstack((np.std(da_res,axis=0),np.std(db_res,axis=0))) # E Calculate href, Cref: href = np.arctan2(br,ar) Cref = (ar**2 + br**2)**0.5 # F Calculate dC/C, dH/C for data and model and calculate residuals: dCoverC = (np.cos(href)*da + np.sin(href)*db)/Cref dHoverC = (np.cos(href)*db - np.sin(href)*da)/Cref dCoverC_model = (np.cos(href)*da_model + np.sin(href)*db_model)/Cref dHoverC_model = (np.cos(href)*db_model - np.sin(href)*da_model)/Cref dCoverC_res = dCoverC - dCoverC_model dHoverC_res = dHoverC - dHoverC_model dCHoverC_std = np.vstack((np.std(dCoverC_res,axis = 0),np.std(dHoverC_res,axis = 0))) dCHoverC_res = np.hstack((href,dCoverC_res,dHoverC_res)) return poly_model, pmodel, dab_model, dab_res, dCHoverC_res, dab_std, dCHoverC_std
def VF_colorshift_model(S, cri_type = _VF_CRI_DEFAULT, model_type = _VF_MODEL_TYPE, \ cspace = _VF_CSPACE, sampleset = None, pool = False, \ pcolorshift = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10),'Cref' : _VF_MAXR, 'sig' : _VF_SIG}, \ vfcolor = 'k',verbosity = 0): """ Applies full vector field model calculations to spectral data. Args: :S: | nump.ndarray with spectral data. :cri_type: | _VF_CRI_DEFAULT or str or dict, optional | Specifies type of color fidelity model to use. | Controls choice of ref. ill., sample set, averaging, scaling, etc. | See luxpy.cri.spd_to_cri for more info. :modeltype: | _VF_MODEL_TYPE or 'M6' or 'M5', optional | Specifies degree 5 or degree 6 polynomial model in ab-coordinates. :cspace: | _VF_CSPACE or dict, optional | Specifies color space. See _VF_CSPACE_EXAMPLE for example structure. :sampleset: | None or str or ndarray, optional | Sampleset to be used when calculating vector field model. :pool: | False, optional | If :S: contains multiple spectra, True pools all jab data before modeling the vector field, while False models a different field for each spectrum. :pcolorshift: | default dict (see below) or user defined dict, optional | Dict containing the specification input for apply_poly_model_at_hue_x(). | Default dict = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10), | 'Cref' : _VF_MAXR, | 'sig' : _VF_SIG, | 'labels' : '#'} | 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. :vfcolor: | 'k', optional | For plotting the vector fields. :verbosity: | 0, optional | Report warnings or not. Returns: :returns: | list[dict] (each list element refers to a different test SPD) | with the following keys: | - 'Source': dict with ndarrays of the S, cct and duv of source spd. | - 'metrics': dict with ndarrays for: | * Rf (color fidelity: base + metameric shift) | * Rt (metameric uncertainty index) | * Rfi (specific color fidelity indices) | * Rti (specific metameric uncertainty indices) | * cri_type (str with cri_type) | - 'Jab': dict with with ndarrays for Jabt, Jabr, DEi | - 'dC/C_dH_x_sig' : | np.vstack((dCoverC_x,dCoverC_x_sig,dH_x,dH_x_sig)).T | See get_poly_model() for more info. | - 'fielddata': dict with dicts containing data on the calculated | vector-field and circle-fields: | * 'vectorfield' : {'axt': vfaxt, 'bxt' : vfbxt, | 'axr' : vfaxr, 'bxr' : vfbxr}, | * 'circlefield' : {'axt': cfaxt, 'bxt' : cfbxt, | 'axr' : cfaxr, 'bxr' : cfbxr}}, | - 'modeldata' : dict with model info: | {'pmodel': pmodel, | 'pcolorshift' : pcolorshift, | 'dab_model' : dab_model, | 'dab_res' : dab_res, | 'dab_std' : dab_std, | 'modeltype' : modeltype, | 'fmodel' : poly_model, | 'Jabtm' : Jabtm, | 'Jabrm' : Jabrm, | 'DEim' : DEim}, | - 'vshifts' :dict with various vector shifts: | * 'Jabshiftvector_r_to_t' : ndarray with difference vectors | between jabt and jabr. | * 'vshift_ab_s' : vshift_ab_s: ab-shift vectors of samples | * 'vshift_ab_s_vf' : vshift_ab_s_vf: ab-shift vectors of | VF model predictions of samples. | * 'vshift_ab_vf' : vshift_ab_vf: ab-shift vectors of VF | model predictions of vector field grid. """ if type(cri_type) == str: cri_type_str = cri_type else: cri_type_str = None # Calculate Rf, Rfi and Jabr, Jabt: Rf, Rfi, Jabt, Jabr,cct,duv,cri_type = spd_to_cri(S, cri_type= cri_type,out='Rf,Rfi,jabt,jabr,cct,duv,cri_type', sampleset=sampleset) # In case of multiple source SPDs, pool: if (len(Jabr.shape) == 3) & (Jabr.shape[1]>1) & (pool == True): #Nsamples = Jabr.shape[0] Jabr = np.transpose(Jabr,(1,0,2)) # set lamps on first dimension Jabt = np.transpose(Jabt,(1,0,2)) Jabr = Jabr.reshape(Jabr.shape[0]*Jabr.shape[1],3) # put all lamp data one after the other Jabt = Jabt.reshape(Jabt.shape[0]*Jabt.shape[1],3) Jabt = Jabt[:,None,:] # add dim = 1 Jabr = Jabr[:,None,:] out = [{} for _ in range(Jabr.shape[1])] #initialize empty list of dicts if pool == False: N = Jabr.shape[1] else: N = 1 for i in range(N): Jabr_i = Jabr[:,i,:].copy() Jabr_i = Jabr_i[:,None,:] Jabt_i = Jabt[:,i,:].copy() Jabt_i = Jabt_i[:,None,:] DEi = np.sqrt((Jabr_i[...,0] - Jabt_i[...,0])**2 + (Jabr_i[...,1] - Jabt_i[...,1])**2 + (Jabr_i[...,2] - Jabt_i[...,2])**2) # Determine polynomial model: poly_model, pmodel, dab_model, dab_res, dCHoverC_res, dab_std, dCHoverC_std = get_poly_model(Jabt_i, Jabr_i, modeltype = _VF_MODEL_TYPE) # Apply model at fixed hues: href = pcolorshift['href'] Cref = pcolorshift['Cref'] sig = pcolorshift['sig'] dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig = apply_poly_model_at_hue_x(poly_model, pmodel, dCHoverC_res, hx = href, Cxr = Cref, sig = sig) # Calculate deshifted a,b values on original samples: Jt = Jabt_i[...,0].copy() at = Jabt_i[...,1].copy() bt = Jabt_i[...,2].copy() Jr = Jabr_i[...,0].copy() ar = Jabr_i[...,1].copy() br = Jabr_i[...,2].copy() ar = ar + dab_model[:,0:1] # deshift reference to model prediction br = br + dab_model[:,1:2] # deshift reference to model prediction Jabtm = np.hstack((Jt,at,bt)) Jabrm = np.hstack((Jr,ar,br)) # calculate color differences between test and deshifted ref: # DEim = np.sqrt((Jr - Jt)**2 + (at - ar)**2 + (bt - br)**2) DEim = np.sqrt(0*(Jr - Jt)**2 + (at - ar)**2 + (bt - br)**2) # J is not used # Apply scaling function to convert DEim to Rti: scale_factor = cri_type['scale']['cfactor'] scale_fcn = cri_type['scale']['fcn'] avg = cri_type['avg'] Rfi_deshifted = scale_fcn(DEim,scale_factor) Rf_deshifted = scale_fcn(avg(DEim,axis = 0),scale_factor) rms = lambda x: np.sqrt(np.sum(x**2,axis=0)/x.shape[0]) Rf_deshifted_rms = scale_fcn(rms(DEim),scale_factor) # Generate vector field: vfaxt,vfbxt,vfaxr,vfbxr = generate_vector_field(poly_model, pmodel,axr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), bxr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), limit_grid_radius = _VF_MAXR,color = 0) vfaxt,vfbxt,vfaxr,vfbxr = generate_vector_field(poly_model, pmodel,axr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), bxr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), limit_grid_radius = _VF_MAXR,color = 0) # Calculate ab-shift vectors of samples and VF model predictions: vshift_ab_s = calculate_shiftvectors(Jabt_i, Jabr_i, average = False, vtype = 'ab')[:,0,0:3] vshift_ab_s_vf = calculate_shiftvectors(Jabtm,Jabrm, average = False, vtype = 'ab') # Calculate ab-shift vectors using vector field model: Jabt_vf = np.hstack((np.zeros((vfaxt.shape[0],1)), vfaxt, vfbxt)) Jabr_vf = np.hstack((np.zeros((vfaxr.shape[0],1)), vfaxr, vfbxr)) vshift_ab_vf = calculate_shiftvectors(Jabt_vf,Jabr_vf, average = False, vtype = 'ab') # Generate circle field: x,y = plotcircle(radii = np.arange(0,_VF_MAXR+_VF_DELTAR,10), angles = np.arange(0,359,1), out = 'x,y') cfaxt,cfbxt,cfaxr,cfbxr = generate_vector_field(poly_model, pmodel,make_grid = False,axr = x[:,None], bxr = y[:,None], limit_grid_radius = _VF_MAXR,color = 0) out[i] = {'Source' : {'S' : S, 'cct' : cct[i] , 'duv': duv[i]}, 'metrics' : {'Rf':Rf[:,i], 'Rt': Rf_deshifted, 'Rt_rms' : Rf_deshifted_rms, 'Rfi':Rfi[:,i], 'Rti': Rfi_deshifted, 'cri_type' : cri_type_str}, 'Jab' : {'Jabt' : Jabt_i, 'Jabr' : Jabr_i, 'DEi' : DEi}, 'dC/C_dH_x_sig' : np.vstack((dCoverC_x,dCoverC_x_sig,dH_x,dH_x_sig)).T, 'fielddata': {'vectorfield' : {'axt': vfaxt, 'bxt' : vfbxt, 'axr' : vfaxr, 'bxr' : vfbxr}, 'circlefield' : {'axt': cfaxt, 'bxt' : cfbxt, 'axr' : cfaxr, 'bxr' : cfbxr}}, 'modeldata' : {'pmodel': pmodel, 'pcolorshift' : pcolorshift, 'dab_model' : dab_model, 'dab_res' : dab_res,'dab_std' : dab_std, 'model_type' : model_type, 'fmodel' : poly_model, 'Jabtm' : Jabtm, 'Jabrm' : Jabrm, 'DEim' : DEim}, 'vshifts' : {'Jabshiftvector_r_to_t' : np.hstack((Jt-Jr,at-ar,bt-br)), 'vshift_ab_s' : vshift_ab_s, 'vshift_ab_s_vf' : vshift_ab_s_vf, 'vshift_ab_vf' : vshift_ab_vf}} return out
def generate_grid(jab_ranges = None, out = 'grid', \ ax = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR),\ bx = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \ jx = None, limit_grid_radius = 0): """ Generate a grid of color coordinates. Args: :out: | 'grid' or 'vectors', optional | - 'grid': outputs a single 2d numpy.nd-vector with the grid coordinates | - 'vector': outputs each dimension seperately. :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) :ax: | default ndarray or user defined ndarray, optional | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) :bx: | default ndarray or user defined ndarray, optional | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) :jx: | None, optional | Note that not-None :jab_ranges: override :ax:, :bx: and :jx input. :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: | single ndarray with ax,bx [,jx] | or | seperate ndarrays for each dimension specified. """ # generate grid from jab_ranges array input, otherwise use ax, bx, jx input: if jab_ranges is not None: if jab_ranges.shape[0] == 3: jx = np.arange(jab_ranges[0][0],jab_ranges[0][1],jab_ranges[0][2]) ax = np.arange(jab_ranges[1][0],jab_ranges[1][1],jab_ranges[1][2]) bx = np.arange(jab_ranges[2][0],jab_ranges[2][1],jab_ranges[2][2]) else: jx = None ax = np.arange(jab_ranges[0][0],jab_ranges[0][1],jab_ranges[0][2]) bx = np.arange(jab_ranges[1][0],jab_ranges[1][1],jab_ranges[1][2]) # Generate grid from (jx), ax, bx: Ax,Bx = np.meshgrid(ax,bx) grid = np.dstack((Ax,Bx)) grid = np.reshape(grid,(np.array(grid.shape[:-1]).prod(),grid.ndim-1)) if jx is not None: for i,v in enumerate(jx): gridi = np.hstack((np.ones((grid.shape[0],1))*v,grid)) if i == 0: gridwithJ = gridi else: gridwithJ = np.vstack((gridwithJ,gridi)) grid = gridwithJ if jx is None: ax = grid[:,0:1] bx = grid[:,1:2] else: jx = grid[:,0:1] ax = grid[:,1:2] bx = grid[:,2:3] if limit_grid_radius > 0:# limit radius of grid: Cr = (ax**2+bx**2)**0.5 ax = ax[Cr<=limit_grid_radius,None] bx = bx[Cr<=limit_grid_radius,None] if jx is not None: jx = jx[Cr<=limit_grid_radius,None] # create output: if out == 'grid': if jx is None: return np.hstack((ax,bx)) else: return np.hstack((jx,ax,bx)) else: if jx is None: return ax, bx else: return jx, ax, bx
def xyz_to_rfl(xyz, rfl = None, out = 'rfl_est', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'ipt', cspace_tf = {},\ k_neighbours = 4, verbosity = 0): """ Approximate spectral reflectance of xyz based on k nearest neighbour interpolation of samples from a standard reflectance set. Args: :xyz: | ndarray with tristimulus values of target points. :rfl: | ndarray, optional | Reflectance set for color coordinate to rfl mapping. :out: | 'rfl_est' or str, optional :refspd: | None, optional | Refer ence spectrum for color coordinate to rfl mapping. | None defaults to D65. :cieobs: | _CIEOBS, optional | CMF set used for calculation of xyz from spectral data. :cspace: | 'ipt', optional | Color space for color coordinate to rfl mapping. :cspace_tf: | {}, optional | Dict with parameters for xyz_to_... and ..._to_xyz transform. :k_neighbours: | 4 or int, optional | Number of nearest neighbours for reflectance spectrum interpolation. | Neighbours are found using scipy.cKDTree :verbosity: | 0, optional | If > 0: make a plot of the color coordinates of original and rendered image pixels. Returns: :returns: | :rfl_est: | ndarrays with estimated reflectance spectra. """ # get rfl set: if rfl is None: # use IESTM30['4880'] set rfl = _CRI_RFL['ies-tm30']['4880']['5nm'] # get Ref spd: if refspd is None: refspd = _CIE_ILLUMINANTS['D65'].copy() # Calculate lab-type coordinates of standard rfl set under refspd: xyz_rr, xyz_wr = spd_to_xyz(refspd, relative=True, rfl=rfl, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy['xyzw'] = xyz_wr # put correct white point in param. dict lab_rr = colortf(xyz_rr, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy)[:, 0, :] # Convert xyz to lab-type values under refspd: lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy) # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric # color coordinates for each value in lab_ur (i.e. smallest DE): # Construct cKDTree: tree = cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab, k=k_neighbours) if k_neighbours > 1: w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(w, axis=1) else: rfl_est = rfl[inds + 1, :].copy() rfl_est = np.vstack((rfl[0], rfl_est)) if (verbosity > 0) | ('xyz_est' in out.split(',')) | ( 'lab_est' in out.split(',')) | ('DEi_ab' in out.split(',')) | ( 'DEa_ab' in out.split(',')): xyz_est, _ = spd_to_xyz(refspd, rfl=rfl_est, relative=True, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy[ 'xyzw'] = xyz_wr # put correct white point in param. dict lab_est = colortf(xyz_est, tf=cspace, fwtf=cspace_tf_copy)[:, 0, :] DEi_ab = np.sqrt(((lab_est[:, 1:3] - lab[:, 1:3])**2).sum(axis=1)) DEa_ab = DEi_ab.mean() if verbosity > 0: ax = plot_color_data(lab[...,1], lab[...,2], z = lab[...,0], \ show = False, cieobs = cieobs, cspace = cspace, \ formatstr = 'ro', label = 'Original') plot_color_data(lab_est[...,1], lab_est[...,2], z = lab_est[...,0], \ show = True, axh = ax, cieobs = cieobs, cspace = cspace, \ formatstr = 'bd', label = 'Rendered') if out == 'rfl_est': return rfl_est elif out == 'rfl_est,xyz_est': return rfl_est, xyz_est else: return eval(out)
def render_image(img = None, spd = None, rfl = None, out = 'img_hyp', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'ipt', cspace_tf = {},\ k_neighbours = 4, show = True, verbosity = 0, show_ref_img = True,\ stack_test_ref = 12,\ write_to_file = None): """ Render image under specified light source spd. Args: :img: | None or str or ndarray with uint8 rgb image. | None load a default image. :spd: | ndarray, optional | Light source spectrum for rendering :rfl: | ndarray, optional | Reflectance set for color coordinate to rfl mapping. :out: | 'img_hyp' or str, optional | (other option: 'img_ren': rendered image under :spd:) :refspd: | None, optional | Reference spectrum for color coordinate to rfl mapping. | None defaults to D65 (srgb has a D65 white point) :D: | None, optional | Degree of (von Kries) adaptation from spd to refspd. :cieobs: | _CIEOBS, optional | CMF set for calculation of xyz from spectral data. :cspace: | 'ipt', optional | Color space for color coordinate to rfl mapping. :cspace_tf: | {}, optional | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform. :k_neighbours: | 4 or int, optional | Number of nearest neighbours for reflectance spectrum interpolation. | Neighbours are found using scipy.cKDTree :show: | True, optional | Show images. :verbosity: | 0, optional | If > 0: make a plot of the color coordinates of original and rendered image pixels. :show_ref_img: | True, optional | True: shows rendered image under reference spd. False: shows original image. :write_to_file: | None, optional | None: do nothing, else: write to filename(+path) in :write_to_file: :stack_test_ref: | 12, optional | - 12: left (test), right (ref) format for show and imwrite | - 21: top (test), bottom (ref) | - 1: only show/write test | - 2: only show/write ref | - 0: show both, write test Returns: :returns: | img_hyp, img_ren, | ndarrays with hyperspectral image and rendered images """ # Get image: #imread = lambda x: plt.imread(x) #matplotlib.pyplot if img is not None: if isinstance(img, str): img = plt.imread(img) # use matplotlib.pyplot's imread else: img = plt.imread(_HYPSPCIM_DEFAULT_IMAGE) # Convert to 2D format: rgb = img.reshape(img.shape[0] * img.shape[1], 3) * 1.0 # *1.0: make float rgb[rgb == 0] = _EPS # avoid division by zero for pure blacks. # Get unique rgb values and positions: rgb_u, rgb_indices = np.unique(rgb, return_inverse=True, axis=0) # get Ref spd: if refspd is None: refspd = _CIE_ILLUMINANTS['D65'].copy() # Convert rgb_u to xyz and lab-type values under assumed refspd: xyz_wr = spd_to_xyz(refspd, cieobs=cieobs, relative=True) xyz_ur = colortf(rgb_u, tf='srgb>xyz') # Estimate rfl's for xyz_ur: rfl_est, xyzri = xyz_to_rfl(xyz_ur, rfl = rfl, out = 'rfl_est,xyz_est', \ refspd = refspd, D = D, cieobs = cieobs, \ cspace = cspace, cspace_tf = cspace_tf,\ k_neighbours = k_neighbours, verbosity = verbosity) # Get default test spd if none supplied: if spd is None: spd = _CIE_ILLUMINANTS['F4'] # calculate xyz values under test spd: xyzti, xyztw = spd_to_xyz(spd, rfl=rfl_est, cieobs=cieobs, out=2) # Chromatic adaptation from test spd to refspd: if D is not None: xyzti = cat.apply(xyzti, xyzw1=xyztw, xyzw2=xyz_wr, D=D) # Convert xyzti under test spd to srgb: rgbti = colortf(xyzti, tf='srgb') / 255 # Reconstruct original locations for rendered image rgbs: img_ren = rgbti[rgb_indices] img_ren.shape = img.shape # reshape back to 3D size of original # For output: if show_ref_img == True: rgb_ref = colortf(xyzri, tf='srgb') / 255 img_ref = rgb_ref[rgb_indices] img_ref.shape = img.shape # reshape back to 3D size of original img_str = 'Rendered (under ref. spd)' img = img_ref else: img_str = 'Original' img = img / 255 if (stack_test_ref > 0) | show == True: if stack_test_ref == 21: img_original_rendered = np.vstack( (img_ren, np.ones((4, img.shape[1], 3)), img)) img_original_rendered_str = 'Rendered (under test spd)\n ' + img_str elif stack_test_ref == 12: img_original_rendered = np.hstack( (img_ren, np.ones((img.shape[0], 4, 3)), img)) img_original_rendered_str = 'Rendered (under test spd) | ' + img_str elif stack_test_ref == 1: img_original_rendered = img_ren img_original_rendered_str = 'Rendered (under test spd)' elif stack_test_ref == 2: img_original_rendered = img img_original_rendered_str = img_str elif stack_test_ref == 0: img_original_rendered = img_ren img_original_rendered_str = 'Rendered (under test spd)' if write_to_file is not None: # Convert from RGB to BGR formatand write: #print('Writing rendering results to image file: {}'.format(write_to_file)) with warnings.catch_warnings(): warnings.simplefilter("ignore") imsave(write_to_file, img_original_rendered) if show == True: # show images using pyplot.show(): plt.figure() plt.imshow(img_original_rendered) plt.title(img_original_rendered_str) plt.gca().get_xaxis().set_ticklabels([]) plt.gca().get_yaxis().set_ticklabels([]) if stack_test_ref == 0: plt.figure() plt.imshow(img_str) plt.title(img_str) plt.axis('off') if 'img_hyp' in out.split(','): # Create hyper_spectral image: rfl_image_2D = rfl_est[ rgb_indices + 1, :] # create array with all rfls required for each pixel img_hyp = rfl_image_2D.reshape(img.shape[0], img.shape[1], rfl_image_2D.shape[1]) # Setup output: if out == 'img_hyp': return img_hyp elif out == 'img_ren': return img_ren else: return eval(out)
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 cie2006cmfsEx(age = 32,fieldsize = 10, wl = None,\ var_od_lens = 0, var_od_macula = 0, \ var_od_L = 0, var_od_M = 0, var_od_S = 0,\ var_shft_L = 0, var_shft_M = 0, var_shft_S = 0,\ out = 'LMS', allow_negative_values = False): """ Generate Individual Observer CMFs (cone fundamentals) based on CIE2006 cone fundamentals and published literature on observer variability in color matching and in physiological parameters. Args: :age: | 32 or float or int, optional | Observer age :fieldsize: | 10, optional | Field size of stimulus in degrees (between 2° and 10°). :wl: | None, optional | Interpolation/extraplation of :LMS: output to specified wavelengths. | None: output original _WL = np.array([390,780,5]) :var_od_lens: | 0, optional | Std Dev. in peak optical density [%] of lens. :var_od_macula: | 0, optional | Std Dev. in peak optical density [%] of macula. :var_od_L: | 0, optional | Std Dev. in peak optical density [%] of L-cone. :var_od_M: | 0, optional | Std Dev. in peak optical density [%] of M-cone. :var_od_S: | 0, optional | Std Dev. in peak optical density [%] of S-cone. :var_shft_L: | 0, optional | Std Dev. in peak wavelength shift [nm] of L-cone. :var_shft_L: | 0, optional | Std Dev. in peak wavelength shift [nm] of M-cone. :var_shft_S: | 0, optional | Std Dev. in peak wavelength shift [nm] of S-cone. :out: | 'LMS' or , optional | Determines output. :allow_negative_values: | False, optional | Cone fundamentals or color matching functions should not have negative values. | If False: X[X<0] = 0. Returns: :returns: | - 'LMS' : ndarray with individual observer area-normalized | cone fundamentals. Wavelength have been added. | [- 'trans_lens': ndarray with lens transmission | (no wavelengths added, no interpolation) | - 'trans_macula': ndarray with macula transmission | (no wavelengths added, no interpolation) | - 'sens_photopig' : ndarray with photopigment sens. | (no wavelengths added, no interpolation)] References: 1. `Asano Y, Fairchild MD, and Blondé L (2016). Individual Colorimetric Observer Model. PLoS One 11, 1–19. <http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0145671>`_ 2. `Asano Y, Fairchild MD, Blondé L, and Morvan P (2016). Color matching experiment for highlighting interobserver variability. Color Res. Appl. 41, 530–539. <https://onlinelibrary.wiley.com/doi/abs/10.1002/col.21975>`_ 3. `CIE, and CIE (2006). Fundamental Chromaticity Diagram with Physiological Axes - Part I (Vienna: CIE). <http://www.cie.co.at/publications/fundamental-chromaticity-diagram-physiological-axes-part-1>`_ 4. `Asano's Individual Colorimetric Observer Model <https://www.rit.edu/cos/colorscience/re_AsanoObserverFunctions.php>`_ """ fs = fieldsize rmd = _INDVCMF_DATA['rmd'].copy() LMSa = _INDVCMF_DATA['LMSa'].copy() docul = _INDVCMF_DATA['docul'].copy() # field size corrected macular density: pkOd_Macula = 0.485 * np.exp(-fs / 6.132) * ( 1 + var_od_macula / 100) # varied peak optical density of macula corrected_rmd = rmd * pkOd_Macula # age corrected lens/ocular media density: if (age <= 60): correct_lomd = docul[:1] * (1 + 0.02 * (age - 32)) + docul[1:2] else: correct_lomd = docul[:1] * (1.56 + 0.0667 * (age - 60)) + docul[1:2] correct_lomd = correct_lomd * (1 + var_od_lens / 100 ) # varied overall optical density of lens # Peak Wavelength Shift: wl_shifted = np.empty(LMSa.shape) wl_shifted[0] = _WL + var_shft_L wl_shifted[1] = _WL + var_shft_M wl_shifted[2] = _WL + var_shft_S LMSa_shft = np.empty(LMSa.shape) kind = 'cubic' LMSa_shft[0] = interpolate.interp1d(wl_shifted[0], LMSa[0], kind=kind, bounds_error=False, fill_value="extrapolate")(_WL) LMSa_shft[1] = interpolate.interp1d(wl_shifted[1], LMSa[1], kind=kind, bounds_error=False, fill_value="extrapolate")(_WL) LMSa_shft[2] = interpolate.interp1d(wl_shifted[2], LMSa[2], kind=kind, bounds_error=False, fill_value="extrapolate")(_WL) # LMSa[2,np.where(_WL >= _WL_CRIT)] = 0 #np.nan # Not defined above 620nm # LMSa_shft[2,np.where(_WL >= _WL_CRIT)] = 0 ssw = np.hstack( (0, np.sign(np.diff(LMSa_shft[2, :])) )) #detect poor interpolation (sign switch due to instability) LMSa_shft[2, np.where((ssw >= 0) & (_WL > 560))] = np.nan # corrected LMS (no age correction): pkOd_L = (0.38 + 0.54 * np.exp(-fs / 1.333)) * ( 1 + var_od_L / 100) # varied peak optical density of L-cone pkOd_M = (0.38 + 0.54 * np.exp(-fs / 1.333)) * ( 1 + var_od_M / 100) # varied peak optical density of M-cone pkOd_S = (0.30 + 0.45 * np.exp(-fs / 1.333)) * ( 1 + var_od_S / 100) # varied peak optical density of S-cone alpha_lms = 0. * LMSa_shft alpha_lms[0] = 1 - 10**(-pkOd_L * (10**LMSa_shft[0])) alpha_lms[1] = 1 - 10**(-pkOd_M * (10**LMSa_shft[1])) alpha_lms[2] = 1 - 10**(-pkOd_S * (10**LMSa_shft[2])) # this fix is required because the above math fails for alpha_lms[2,:]==0 alpha_lms[2, np.where(_WL >= _WL_CRIT)] = 0 # Corrected to Corneal Incidence: lms_barq = alpha_lms * (10**(-corrected_rmd - correct_lomd)) * np.ones( alpha_lms.shape) # Corrected to Energy Terms: lms_bar = lms_barq * _WL # Set NaN values to zero: lms_bar[np.isnan(lms_bar)] = 0 # normalized: LMS = 100 * lms_bar / np.nansum(lms_bar, axis=1, keepdims=True) # Output extra: trans_lens = 10**(-correct_lomd) trans_macula = 10**(-corrected_rmd) sens_photopig = alpha_lms * _WL # Add wavelengths: LMS = np.vstack((_WL, LMS)) if ('xyz' in out.lower().split(',')): LMS = lmsb_to_xyzb(LMS, fieldsize, out='xyz', allow_negative_values=allow_negative_values) out = out.replace('xyz', 'LMS').replace('XYZ', 'LMS') if ('lms' in out.lower().split(',')): out = out.replace('lms', 'LMS') # Interpolate/extrapolate: if wl is None: interpolation = None else: interpolation = 'cubic' LMS = spd(LMS, wl=wl, interpolation=interpolation, norm_type='area') if (out == 'LMS'): return LMS elif (out == 'LMS,trans_lens,trans_macula,sens_photopig'): return LMS, trans_lens, trans_macula, sens_photopig elif (out == 'LMS,trans_lens,trans_macula,sens_photopig,LMSa'): return LMS, trans_lens, trans_macula, sens_photopig, LMSa else: return eval(out)
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)
Wrapper function for cam15u inverse mode with 'Q,aW,bW' input. | For help on parameter details: ?luxpy.cam.cam15u """ return cam15u(qab, fov=fov, direction='inverse', inputtype='xyz', outin='Q,aW,bW', parameters=parameters) #------------------------------------------------------------------------------ if __name__ == '__main__': C = _CIE_ILLUMINANTS['C'].copy() C = np.vstack((C, cie_interp(_CIE_ILLUMINANTS['D65'], C[0], kind='spd')[1:])) M = _MUNSELL.copy() rflM = M['R'] cieobs = '2006_10' # Normalize to Lw: Lw = 100 xyzw2 = spd_to_xyz(C, cieobs=cieobs, relative=False) for i in range(C.shape[0] - 1): C[i + 1] = Lw * C[i + 1] / xyzw2[i, 1] xyz, xyzw = spd_to_xyz(C, cieobs=cieobs, relative=True, rfl=rflM, out=2) qab = xyz_to_qabW_cam15u(xyzw, fov=10.0) qab2 = cam15u(C, fov=10.0, direction='forward',
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
""" return cam18sl(xyz, datab = xyzb, Lb = Lb, fov = fov, direction = 'forward', inputtype = 'xyz', outin = 'Q,aS,bS', parameters = parameters) def qabS_cam18sl_to_xyz(qab, xyzb = None, Lb = [100], fov = 10.0, parameters = None, **kwargs): """ Wrapper function for cam18sl inverse mode with 'Q,aS,bS' input. | For help on parameter details: ?luxpy.cam.cam18sl """ return cam18sl(qab, datab = xyzb, Lb = Lb, fov = fov, direction = 'inverse', inputtype = 'xyz', outin = 'Q,aS,bS', parameters = parameters) #------------------------------------------------------------------------------ if __name__ == '__main__': C = _CIE_ILLUMINANTS['C'].copy() C = np.vstack((C,cie_interp(_CIE_ILLUMINANTS['D65'],C[0],kind='spd')[1:])) M = _MUNSELL.copy() rflM = M['R'] cieobs = '2006_10' # Normalize to Lw: Lw = 100 xyzw2 = spd_to_xyz(C, cieobs = cieobs, relative = False) for i in range(C.shape[0]-1): C[i+1] = Lw*C[i+1]/xyzw2[i,1] xyz, xyzw = spd_to_xyz(C, cieobs = cieobs, relative = True, rfl = rflM, out = 2) qab = xyz_to_qabW_cam18sl(xyzw, xyzb = None, Lb = [100], fov = 10.0) print('qab: ',qab) qab2 = cam18sl(C, datab = None, Lb = [100], fov = 10.0, direction = 'forward', inputtype = 'spd', outin = 'Q,aW,bW', parameters = None)
def get_spd(dvc = 0, Tint = 0.0, autoTint_max = _TINT_MAX, Nscans = 1, wlstep = 1, wlstart = 360, wlend = 830, twait = _TWAIT_STATUS, out = "spd", close_device = True, laser_on = 0, laser_intensity = 1000, verbosity = _VERBOSITY): """ Measure spectral radiance (W/nm.sr.m²). Args: :dvc: | 0 or Int or ctypes.wintypes.LP_c_ulong, optional | Number of the spectrometer device to load (0 = 1st) or handle (ctypes) to pre_initialized device. :Tint: | 0 or Float, optional | Integration time in seconds. (if 0: find best integration time, but < autoTint_max). :autoTint_max: | Limit Tint to this value when Tint = 0. :Nscans: | 1 or Int, optional | Number of scans to average. :wlstep: | 1 or Int, optional | Wavelength step size in nm. :wlstart: | 360 or Int, optional | Start wavelength in nm. (min = 350 nm) :wlend: | 830 or Int, optional | Start wavelength in nm. (max = 1000 nm) :twait: | 0.1 or Float, optional | Time in seconds to wait before checking status of device. | (If 0: wait :Tint: seconds, unless :Tint: == 0, then wait _TWAIT_STATUS seconds) :out: | "spd" [",dvc, Errors"], optional | Requested return. If "spd" in out.split(","):do spectral measurement, else: initialize dvc handle [and turn laser ON or OFF]. :close_device: | True or False, optional | Close device at the end of the measurement. | If 'dvc' not in out.split(','): always close!!! :laser_on: | 0: OFF, >0: ON -> 1: PWM 7Hz, 2: PWM 28 Hz, 3: 255 Hz, optional | True (>0): turn laser on to select measurement area; False (0): turn off. | (Can only be ON when "spd" is not in out.split(",") | if Tint is None) :laser_intensity: | 1000.0, optional | Laser intensity in ‰ (pro-mille). :verbosity: | 1, optional | 0: no printed error message output. Returns: :returns: | spd [,dvc, Errors] (as specified in :out:) | - "spd": ndarray with wavelengths (1st row) and spectral radiance (2nd row). | - "dvc": ctypes handle to device (if open) or nan (if closed). | - "Errors": dict with error message returned by device during various steps of the spectral measurement process. """ # Initialize dict with errors messages for each of the different measurement steps: Errors = {} Errors["get_spd"] = None out = out.replace(' ','') # Get wavelength range: wls = np.arange(np.int(wlstart), np.int(wlend)+np.int(wlstep), np.int(wlstep), dtype=np.float32) # Initialize spd filled with nan's: spd = np.vstack((wls, np.nan*np.ones(wls.shape))) try: # Initialize device : dvc, Errors = dvc_open(dvc = dvc, Errors = Errors, out = "dvc,Errors", verbosity = verbosity) if (_check_dvc_open(dvc)) & (("spd" in out.split(",")) & (Tint is not None)): # Turn off laser before starting measurement: Errors = set_laser(dvc = dvc, laser_on = False, laser_intensity = laser_intensity, Errors = Errors, verbosity = verbosity) # Start measurement: Tint, Errors = start_meas(dvc, Tint = Tint, autoTint_max = autoTint_max, Nscans = Nscans, wlstep = wlstep, Errors = Errors, out = "Tint, Errors", verbosity = verbosity) # wait until measurement is finished (check intermediate status every twait seconds): status, Errors = wait_until_meas_is_finished(dvc, Tint = Tint, twait = twait, out = "status,Errors", Errors = Errors, verbosity = verbosity) if status == False: # Read measured spectral radiance from device: spd, Errors = read_spectral_radiance(dvc, wlstart = wlstart, wlend = wlend, wlstep = wlstep, out = "spd,Errors", Errors = Errors, verbosity = verbosity) elif (("spd" not in out.split(",")) | (Tint is None)): # only dvc handle was requested or to turn laser ON. Errors = set_laser(dvc = dvc, laser_on = laser_on, laser_intensity = laser_intensity, Errors = Errors, verbosity = verbosity) # Close device: dvc, Errors = dvc_close(dvc, Errors = Errors, close_device = (close_device) | ('dvc' not in out.split(',')), out = "dvc,Errors", verbosity = verbosity) Errors["get_spd"] = int(np.sum([int(bool(x)) for x in Errors.values() if (x is not None)]) > 0) except: Errors["get_spd"] = "get_spd fails." finally: # Generate requested return: if out == "spd": return spd elif out == "dvc": return dvc elif out == "Errors": return Errors elif out == "spd,Errors": return spd, Errors elif out == "spd,dvc": return spd, dvc elif out == "spd,Errors,dvc": return spd, Errors, dvc elif out == "spd,dvc,Errors": return spd, dvc, Errors else: raise Exception("Requested output error.")
def read_spectral_radiance(dvc, wlstart = 360, wlend = 830, wlstep = 1, out = "spd,Errors", Errors = {}, verbosity = _VERBOSITY): """ Read measured spectral radiance (W/m².sr.nm) from device. Args: :dvc: | Device handle (of class ctypes). :wlstart: | 360 or Int, optional | Start wavelength in nm. (min = 350 nm) :wlend: | 830 or Int, optional | Start wavelength in nm. (max = 1000 nm) :out: | "status,Errors", optional | Requested return. :Errors: | Dict with error messages. :verbosity: | 1, optional | 0: no printed error message output. Returns: :spd: | ndarray with wavelengths (1st row) and spectral radiance (2nd row; nan's if error). :Errors: | Dict with error messages. """ out = out.replace(' ','') # Get wavelength range: wls = np.arange(np.int(wlstart), np.int(wlend)+np.int(wlstep), np.int(wlstep), dtype=np.float32) # Initialize spd filled with nan's: spd = np.vstack((wls, np.nan*np.ones(wls.shape))) # try: Errors["SpecRadEx"] = None # Convert measurement parameters to ctypes: dwBeg = DWORD(np.int(wlstart)) # wavelength start in nm dwEnd = DWORD(np.int(wlend)) # wavelength end in nm # create buffer for spectral radiance data: fSprad = (FLOAT * wls.shape[0])() # get pointer to start of spectral radiance dwError = jtre.JETI_SpecRadEx(dvc, dwBeg, dwEnd, ctypes.byref(fSprad)) Errors["SpecRadEx"] = dwError if (dwError != 0): if (verbosity == 1): print("Could not read spectral radiance data from device. Error code = {}".format(dwError)) else: # Read spectral radiance from buffer: Sprad= np.frombuffer(fSprad, np.float32) # Overwrite 2nd row of spd array with measured spectral radiance values: spd[1,:] = Sprad # except: # Errors["SpecRadEx"] = "read_spectral_radiance() fails." # finally: # Generate requested return: if out == "spd,Errors": return spd, Errors elif out == "spd": return spd elif out == "Errors": return Errors else: raise Exception("Requested output error.")
def calculate_VF_PX_models(S, cri_type = _VF_CRI_DEFAULT, sampleset = None, pool = False, \ pcolorshift = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10),\ 'Cref' : _VF_MAXR, 'sig' : _VF_SIG, 'labels' : '#'},\ vfcolor = 'k', verbosity = 0): """ Calculate Vector Field and Pixel color shift models. Args: :cri_type: | _VF_CRI_DEFAULT or str or dict, optional | Specifies type of color fidelity model to use. | Controls choice of ref. ill., sample set, averaging, scaling, etc. | See luxpy.cri.spd_to_cri for more info. :sampleset: | None or str or ndarray, optional | Sampleset to be used when calculating vector field model. :pool: | False, optional | If :S: contains multiple spectra, True pools all jab data before modeling the vector field, while False models a different field for each spectrum. :pcolorshift: | default dict (see below) or user defined dict, optional | Dict containing the specification input for apply_poly_model_at_hue_x(). | Default dict = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10), | 'Cref' : _VF_MAXR, | 'sig' : _VF_SIG, | 'labels' : '#'} | 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. :vfcolor: | 'k', optional | For plotting the vector fields. :verbosity: | 0, optional | Report warnings or not. Returns: :returns: | :dataVF:, :dataPX: | Dicts, for more info, see output description of resp.: luxpy.cri.VF_colorshift_model() and luxpy.cri.PX_colorshift_model() """ # calculate VectorField cri_color_shift model: dataVF = VF_colorshift_model(S, cri_type = cri_type, sampleset = sampleset, vfcolor = vfcolor, pcolorshift = pcolorshift, pool = pool, verbosity = verbosity) # Set jab_ranges and _deltas for PX-model pixel calculations: PX_jab_deltas = np.array([_VF_DELTAR,_VF_DELTAR,_VF_DELTAR]) #set same as for vectorfield generation PX_jab_ranges = np.vstack(([0,100,_VF_DELTAR],[-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR], [-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR]))#IES4880 gamut # Calculate shift vectors using vectorfield and pixel methods: delta_SvsVF_vshift_ab_mean = np.nan*np.ones((len(dataVF),1)) delta_SvsVF_vshift_ab_mean_normalized = delta_SvsVF_vshift_ab_mean.copy() delta_PXvsVF_vshift_ab_mean = np.nan*np.ones((len(dataVF),1)) delta_PXvsVF_vshift_ab_mean_normalized = delta_PXvsVF_vshift_ab_mean.copy() dataPX = [[] for k in range(len(dataVF))] for Snr in range(len(dataVF)): # Calculate shifts using pixel method, PX: dataPX[Snr] = PX_colorshift_model(dataVF[Snr]['Jab']['Jabt'][:,0,:],dataVF[Snr]['Jab']['Jabr'][:,0,:], jab_ranges = PX_jab_ranges, jab_deltas = PX_jab_deltas,limit_grid_radius = _VF_MAXR) # Calculate shift difference between Samples (S) and VectorField model predictions (VF): delta_SvsVF_vshift_ab = dataVF[Snr]['vshifts']['vshift_ab_s'] - dataVF[Snr]['vshifts']['vshift_ab_s_vf'] delta_SvsVF_vshift_ab_mean[Snr] = np.nanmean(np.sqrt((delta_SvsVF_vshift_ab[...,1:3]**2).sum(axis = delta_SvsVF_vshift_ab[...,1:3].ndim-1)), axis=0) delta_SvsVF_vshift_ab_mean_normalized[Snr] = delta_SvsVF_vshift_ab_mean[Snr]/dataVF[Snr]['Jab']['DEi'].mean(axis=0) # Calculate shift difference between PiXel method (PX) and VectorField (VF): delta_PXvsVF_vshift_ab = dataPX[Snr]['vshifts']['vectorshift_ab_J0'] - dataVF[Snr]['vshifts']['vshift_ab_vf'] delta_PXvsVF_vshift_ab_mean[Snr] = np.nanmean(np.sqrt((delta_PXvsVF_vshift_ab[...,1:3]**2).sum(axis = delta_PXvsVF_vshift_ab[...,1:3].ndim-1)), axis=0) delta_PXvsVF_vshift_ab_mean_normalized[Snr] = delta_PXvsVF_vshift_ab_mean[Snr]/dataVF[Snr]['Jab']['DEi'].mean(axis=0) dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean'] = delta_PXvsVF_vshift_ab_mean[Snr] dataVF[Snr]['vshifts']['delta_SvsVF_vshift_ab_mean'] = delta_SvsVF_vshift_ab_mean[Snr] dataVF[Snr]['vshifts']['delta_SvsVF_vshift_ab_mean_normalized'] = delta_SvsVF_vshift_ab_mean_normalized[Snr] dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized'] = delta_PXvsVF_vshift_ab_mean_normalized[Snr] dataPX[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean'] = dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean'] dataPX[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized'] = dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized'] return dataVF, dataPX
def cam18sl(data, datab = None, Lb = [100], fov = 10.0, inputtype = 'xyz', direction = 'forward', outin = 'Q,aW,bW', parameters = None): """ Convert between CIE 2006 10° XYZ tristimulus values (or spectral data) and CAM18sl color appearance correlates. Args: :data: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data or color appearance attributes of stimulus :datab: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data of stimulus background :Lb: | [100], optional | Luminance (cd/m²) value(s) of background(s) calculated using the CIE 2006 10° CMFs | (only used in case datab == None and the background is assumed to be an Equal-Energy-White) :fov: | 10.0, optional | Field-of-view of stimulus (for size effect on brightness) :inputtpe: | '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 -> cam18sl | -'inverse': cam18sl -> xyz :outin: | 'Q,aW,bW' or str, optional | 'Q,aW,bW' (brightness and opponent signals for amount-of-neutral) | other options: 'Q,aM,bM' (colorfulness) and 'Q,aS,bS' (saturation) | Str specifying the type of | input (:direction: == 'inverse') and | output (:direction: == 'forward') :parameters: | None or dict, optional | Set of model parameters. | - None: defaults to luxpy.cam._CAM18SL_PARAMETERS | (see references below) Returns: :returns: | ndarray with color appearance correlates (:direction: == 'forward') | or | XYZ tristimulus values (:direction: == 'inverse') Notes: | * Instead of using the CIE 1964 10° CMFs in some places of the model, | the CIE 2006 10° CMFs are used througout, making it more self_consistent. | This has an effect on the k scaling factors (now different those in CAM15u) | and the illuminant E normalization for use in the chromatic adaptation transform. | (see future erratum to Hermans et al., 2018) | * The paper also used an equation for the amount of white W, which is | based on a Q value not expressed in 'bright' ('cA' = 0.937 instead of 123). | This has been corrected for in the luxpy version of the model, i.e. | _CAM18SL_PARAMETERS['cW'][0] has been changed from 2.29 to 1/11672. | (see future erratum to Hermans et al., 2018) References: 1. `Hermans, S., Smet, K. A. G., & Hanselaer, P. (2018). "Color appearance model for self-luminous stimuli." Journal of the Optical Society of America A, 35(12), 2000–2009. <https://doi.org/10.1364/JOSAA.35.002000>`_ """ if parameters is None: parameters = _CAM18SL_PARAMETERS outin = outin.split(',') #unpack model parameters: cA, cAlms, cHK, cM, cW, ca, calms, cb, cblms, cfov, k, naka, unique_hue_data = [parameters[x] for x in sorted(parameters.keys())] # precomputations: Mlms2xyz = np.linalg.inv(_CMF['2006_10']['M']) MAab = np.array([cAlms,calms,cblms]) invMAab = np.linalg.inv(MAab) #------------------------------------------------- # setup EEW reference field and default background field (Lr should be equal to Lb): # Get Lb values: if datab is not None: if inputtype != 'xyz': Lb = spd_to_xyz(datab, cieobs = '2006_10', relative = False)[...,1:2] else: Lb = datab[...,1:2] else: if isinstance(Lb,list): Lb = np2dT(Lb) # Setup EEW ref of same luminance as datab: if inputtype == 'xyz': wlr = getwlr(_CAM18SL_WL3) else: if datab is None: wlr = data[0] # use wlr of stimulus data else: wlr = datab[0] # use wlr of background data datar = np.vstack((wlr,np.ones((Lb.shape[0], wlr.shape[0])))) # create eew xyzr = spd_to_xyz(datar, cieobs = '2006_10', relative = False) # get abs. tristimulus values datar[1:] = datar[1:]/xyzr[...,1:2]*Lb # Create datab if None: if (datab is None): if inputtype != 'xyz': datab = datar.copy() else: datab = spd_to_xyz(datar, cieobs = '2006_10', relative = False) datar = datab.copy() # prepare data and datab for loop over backgrounds: # make axis 1 of datab have 'same' dimensions as data: if (data.ndim == 2): data = np.expand_dims(data, axis = 1) # add light source axis 1 if inputtype == 'xyz': if datab.shape[0] == 1: #make datab and datar have same lights source dimension (used to store different backgrounds) size as data datab = np.repeat(datab,data.shape[1],axis=0) datar = np.repeat(datar,data.shape[1],axis=0) else: if datab.shape[0] == 2: datab = np.vstack((datab[0],np.repeat(datab[1:], data.shape[1], axis = 0))) if datar.shape[0] == 2: datar = np.vstack((datar[0],np.repeat(datar[1:], data.shape[1], axis = 0))) # Flip light source/ background dim to axis 0: data = np.transpose(data, axes = (1,0,2)) #------------------------------------------------- #initialize camout: dshape = list(data.shape) dshape[-1] = len(outin) # requested number of correlates if (inputtype != 'xyz') & (direction == 'forward'): dshape[-2] = dshape[-2] - 1 # wavelength row doesn't count & only with forward can the input data be spectral camout = np.nan*np.ones(dshape) for i in range(data.shape[0]): # get rho, gamma, beta of background and reference white: if (inputtype != 'xyz'): xyzb = spd_to_xyz(np.vstack((datab[0], datab[i+1:i+2,:])), cieobs = '2006_10', relative = False) xyzr = spd_to_xyz(np.vstack((datar[0], datar[i+1:i+2,:])), cieobs = '2006_10', relative = False) else: xyzb = datab[i:i+1,:] xyzr = datar[i:i+1,:] lmsb = np.dot(_CMF['2006_10']['M'],xyzb.T).T # convert to l,m,s rgbb = (lmsb / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #lmsr = np.dot(_CMF['2006_10']['M'],xyzr.T).T # convert to l,m,s #rgbr = (lmsr / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #rgbr = rgbr/rgbr[...,1:2]*Lb[i] # calculated EEW cone excitations at same luminance values as background rgbr = np.ones(xyzr.shape)*Lb[i] # explicitely equal EEW cone excitations at same luminance values as background if direction == 'forward': # get rho, gamma, beta of stimulus: if (inputtype != 'xyz'): xyz = spd_to_xyz(data[i], cieobs = '2006_10', relative = False) elif (inputtype == 'xyz'): xyz = data[i] lms = np.dot(_CMF['2006_10']['M'],xyz.T).T # convert to l,m,s rgb = (lms / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta # apply von-kries cat with D = 1: if (rgbb == 0).any(): Mcat = np.eye(3) else: Mcat = np.diag((rgbr/rgbb)[0]) rgba = np.dot(Mcat,rgb.T).T # apply naka-rushton compression: rgbc = naka_rushton(rgba, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling']) #rgbc = np.ones(rgbc.shape)*rgbc.mean() # test if eew ends up at origin # calculate achromatic and color difference signals, A, a, b: Aab = np.dot(MAab, rgbc.T).T A,a,b = asplit(Aab) a = ca*a b = cb*b # calculate colorfullness like signal M: M = cM*((a**2.0 + b**2.0)**0.5) # calculate brightness Q: Q = cA*(A + cHK[0]*M**cHK[1]) # last term is contribution of Helmholtz-Kohlrausch effect on brightness # calculate saturation, s: s = M / Q # calculate amount of white, W: W = 1 / (1.0 + cW[0]*(s**cW[1])) # adjust Q for size (fov) of stimulus (matter of debate whether to do this before or after calculation of s or W, there was no data on s, M or W for different sized stimuli: after) Q = Q*(fov/10.0)**cfov # calculate hue, h and Hue quadrature, H: h = hue_angle(a,b, htype = 'deg') if 'H' in outin: H = hue_quadrature(h, unique_hue_data = unique_hue_data) else: H = None # calculate cart. co.: if 'aM' in outin: aM = M*np.cos(h*np.pi/180.0) bM = M*np.sin(h*np.pi/180.0) if 'aS' in outin: aS = s*np.cos(h*np.pi/180.0) bS = s*np.sin(h*np.pi/180.0) if 'aW' in outin: aW = W*np.cos(h*np.pi/180.0) bW = W*np.sin(h*np.pi/180.0) if (outin != ['Q','aW','bW']): camout[i] = eval('ajoin(('+','.join(outin)+'))') else: camout[i] = ajoin((Q,aW,bW)) elif direction == 'inverse': # get Q, M and a, b depending on input type: if 'aW' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref W = (a**2.0 + b**2.0)**0.5 s = (((1.0 / W) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q if 'aM' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref M = (a**2.0 + b**2.0)**0.5 if 'aS' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref s = (a**2.0 + b**2.0)**0.5 M = s*Q if 'h' in outin: Q, WsM, h = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref if 'W' in outin: s = (((1.0 / WsM) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q elif 's' in outin: M = WsM*Q elif 'M' in outin: M = WsM # calculate achromatic signal, A from Q and M: A = Q/cA - cHK[0]*M**cHK[1] # calculate hue angle: h = hue_angle(a,b, htype = 'rad') # calculate a,b from M and h: a = (M/cM)*np.cos(h) b = (M/cM)*np.sin(h) a = a/ca b = b/cb # create Aab: Aab = ajoin((A,a,b)) # calculate rgbc: rgbc = np.dot(invMAab, Aab.T).T # decompress rgbc to (adapted) rgba : rgba = naka_rushton(rgbc, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling'], direction = 'inverse') # apply inverse von-kries cat with D = 1: rgb = np.dot(np.diag((rgbb/rgbr)[0]),rgba.T).T # convert rgb to lms to xyz: lms = rgb/k*_CMF['2006_10']['K'] xyz = np.dot(Mlms2xyz,lms.T).T camout[i] = xyz if camout.shape[0] == 1: camout = np.squeeze(camout,axis = 0) return camout
def subsample_RFL_set(rfl, rflpath = '', samplefcn = 'rand', S = _CIE_ILLUMINANTS['E'], \ jab_ranges = None, jab_deltas = None, cieobs = _VF_CIEOBS, cspace = _VF_CSPACE, \ ax = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \ bx = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \ jx = None, limit_grid_radius = 0): """ Sub-samples a spectral reflectance set by pixelization of color space. Args: :rfl: | ndarray or str | Array with of str referring to a set of spectral reflectance functions to be subsampled. | If str to file: file must contain data as columns, with first column the wavelengths. :rflpath: | '' or str, optional | Path to folder with rfl-set specified in a str :rfl: filename. :samplefcn: | 'rand' or 'mean', optional | -'rand': selects a random sample from the samples within each pixel | -'mean': returns the mean spectral reflectance in each pixel. :S: | _CIE_ILLUMINANTS['E'], optional | Illuminant used to calculate the color coordinates of the spectral reflectance samples. :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. :cspace: | _VF_CSPACE or dict, optional | Specifies color space. See _VF_CSPACE_EXAMPLE for example structure. :cieobs: | _VF_CIEOBS or str, optional | Specifies CMF set used to calculate color coordinates. :ax: | default ndarray or user defined ndarray, optional | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) :bx: | default ndarray or user defined ndarray, optional | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) :jx: | None, optional | Note that not-None :jab_ranges: override :ax:, :bx: and :jx input. :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: | rflsampled, jabp | ndarrays with resp. the subsampled set of spectral reflectance functions and the pixel coordinate centers. """ # Testing effects of sample set, pixel size and gamut size: if type(rfl) == str: rfl = pd.read_csv(os.path.join(rflpath,rfl),header = None).get_values().T # Calculate Jab coordinates of samples: xyz,xyzw = spd_to_xyz(S, cieobs = cieobs, rfl = rfl.copy(), out = 2) cspace_pars = cspace.copy() cspace_pars.pop('type') cspace_pars['xyzw'] = xyzw jab = colortf(xyz,tf = cspace['type'],fwtf = cspace_pars) # Generate grid and get samples in each grid: gridp,idxp, jabp, pixelsamplenrs, pixelIDs = get_pixel_coordinates(jab, jab_ranges = jab_ranges, jab_deltas = jab_deltas, limit_grid_radius = limit_grid_radius) # Get rfls from set using sampling function (mean or rand): W = rfl[:1] R = rfl[1:] rflsampled = np.nan*np.ones((len(idxp),R.shape[1])) for i in range(len(idxp)): if samplefcn == 'mean': rfl_i = np.nanmean(rfl[pixelsamplenrs[i],:],axis = 0) else: samplenr_i = np.random.randint(len(pixelsamplenrs[i])) rfl_i = rfl[pixelsamplenrs[i][samplenr_i],:] rflsampled[i,:] = rfl_i rflsampled = np.vstack((W,rflsampled)) return rflsampled, jabp
def xyz_to_cct_ohno(xyzw, cieobs=_CIEOBS, out='cct', wl=None, accuracy=0.1, force_out_of_lut=True, 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) using Ohno's method. 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. :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: | cct: out == 'cct' (or 1) | duv: out == 'duv' (or -1) | cct, duv: out == 'cct,duv' (or 2) | [cct,duv]: out == "[cct,duv]" (or -2) Note: LUTs are stored in ./data/cctluts/ Reference: 1. `Ohno Y. Practical use and calculation of CCT and Duv. Leukos. 2014 Jan 2;10(1):47-55. <http://www.tandfonline.com/doi/abs/10.1080/15502724.2014.839020>`_ """ xyzw = np2d(xyzw) if len(xyzw.shape) > 2: raise Exception('xyz_to_cct_ohno(): Input xyzwa.ndim must be <= 2 !') # get 1960 u,v of test source: Yuv = xyz_to_Yuv( xyzw) # remove possible 1-dim + convert xyzw 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 uv = np2d(np.concatenate((u, v), axis=axis_of_v3)) # load cct & uv from LUT: if cieobs not in _CCT_LUT: _CCT_LUT[cieobs] = calculate_lut(ccts=None, cieobs=cieobs, add_to_lut=False) cct_LUT = _CCT_LUT[cieobs][:, 0, None] uv_LUT = _CCT_LUT[cieobs][:, 1:3] # calculate CCT of each uv: CCT = np.ones(uv.shape[0]) * np.nan # initialize with NaN's Duv = CCT.copy() # initialize with NaN's idx_m = 0 idx_M = uv_LUT.shape[0] - 1 for i in range(uv.shape[0]): out_of_lut = False delta_uv = (((uv_LUT - uv[i])**2.0).sum( axis=1))**0.5 # calculate distance of uv with uv_LUT idx_min = delta_uv.argmin() # find index of minimum distance # find Tm, delta_uv and u,v for 2 points surrounding uv corresponding to idx_min: if idx_min == idx_m: idx_min_m1 = idx_min out_of_lut = True else: idx_min_m1 = idx_min - 1 if idx_min == idx_M: idx_min_p1 = idx_min out_of_lut = True else: idx_min_p1 = idx_min + 1 if (out_of_lut == True) & (force_out_of_lut == True): # calculate using search-function cct_i, Duv_i = xyz_to_cct_search(xyzw[i], cieobs=cieobs, wl=wl, accuracy=accuracy, out='cct,duv', upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) CCT[i] = cct_i Duv[i] = Duv_i continue elif (out_of_lut == True) & (force_out_of_lut == False): CCT[i] = np.nan Duv[i] = np.nan cct_m1 = cct_LUT[idx_min_m1] # - 2*_EPS delta_uv_m1 = delta_uv[idx_min_m1] uv_m1 = uv_LUT[idx_min_m1] cct_p1 = cct_LUT[idx_min_p1] delta_uv_p1 = delta_uv[idx_min_p1] uv_p1 = uv_LUT[idx_min_p1] cct_0 = cct_LUT[idx_min] delta_uv_0 = delta_uv[idx_min] # calculate uv distance between Tm_m1 & Tm_p1: delta_uv_p1m1 = ((uv_p1[0] - uv_m1[0])**2.0 + (uv_p1[1] - uv_m1[1])**2.0)**0.5 # Triangular solution: x = ((delta_uv_m1**2) - (delta_uv_p1**2) + (delta_uv_p1m1**2)) / (2 * delta_uv_p1m1) Tx = cct_m1 + ((cct_p1 - cct_m1) * (x / delta_uv_p1m1)) #uBB = uv_m1[0] + (uv_p1[0] - uv_m1[0]) * (x / delta_uv_p1m1) vBB = uv_m1[1] + (uv_p1[1] - uv_m1[1]) * (x / delta_uv_p1m1) Tx_corrected_triangular = Tx * 0.99991 signDuv = np.sign(uv[i][1] - vBB) Duv_triangular = signDuv * np.atleast_1d( ((delta_uv_m1**2.0) - (x**2.0))**0.5) # Parabolic solution: a = delta_uv_m1 / (cct_m1 - cct_0 + _EPS) / (cct_m1 - cct_p1 + _EPS) b = delta_uv_0 / (cct_0 - cct_m1 + _EPS) / (cct_0 - cct_p1 + _EPS) c = delta_uv_p1 / (cct_p1 - cct_0 + _EPS) / (cct_p1 - cct_m1 + _EPS) A = a + b + c B = -(a * (cct_p1 + cct_0) + b * (cct_p1 + cct_m1) + c * (cct_0 + cct_m1)) C = (a * cct_p1 * cct_0) + (b * cct_p1 * cct_m1) + (c * cct_0 * cct_m1) Tx = -B / (2 * A + _EPS) Tx_corrected_parabolic = Tx * 0.99991 Duv_parabolic = signDuv * (A * np.power(Tx_corrected_parabolic, 2) + B * Tx_corrected_parabolic + C) Threshold = 0.002 if Duv_triangular < Threshold: CCT[i] = Tx_corrected_triangular Duv[i] = Duv_triangular else: CCT[i] = Tx_corrected_parabolic Duv[i] = Duv_parabolic # Regulate output: if (out == 'cct') | (out == 1): return np2dT(CCT) elif (out == 'duv') | (out == -1): return np2dT(Duv) elif (out == 'cct,duv') | (out == 2): return np2dT(CCT), np2dT(Duv) elif (out == "[cct,duv]") | (out == -2): return np.vstack((CCT, Duv)).T
0.00080179, 0.00080258, 0.00080346, 0.00080447, 0.00080565, 0.00080703, 0.00080865, 0.00081053, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272, 0.00081272 ]]) F1, F2, F3, F4_, F5, F6, F7, F8, F9, F10, F11, F12 = [ np.vstack((_IESTM30['S']['data'][0], _IESTM30['S']['data'][i + 1])) for i in range(12) ] _CIE_ILLUMINANTS = { 'types': [ 'E', 'D65', 'A', 'C', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12' ], 'E': E, 'D65': D65, 'A': A, 'C': C,