def _get_daylightphase_Mi_values(xD,yD, Mcoeffs = None, cieobs = None, S012_daylightphase = None): """ Get daylight phase coefficients M1, M2 following Judd et al. (1964) Args: :xD,yD: | ndarray of x-coordinates, ndarray of y-coordinates of daylight phase :Mcoeffs: | Coefficients in M1 & M2 weights for specific cieobs. | If None and cieobs is not None: they will be calculated. :cieobs: | CMF set to use when calculating coefficients in M1, M2 weights. | If None: Mcoeffs must be supplied. :S012_daylightphase: | ndarray with CIE S0, S1, S2 daylight phase component functions Returns: :M1,M2: | daylight phase coefficients M1, M2 Reference: 1. `Judd et al.(1964). Spectral Distribution of Typical Daylight as a Function of Correlated Color Temperature. JOSA A, 54(8), pp. 1031-1040 (1964) <https://doi.org/10.1364/JOSA.54.001031>`_ """ if (Mcoeffs is None) & (cieobs is None): raise Exception("_get_daylightphase_Mi_values(): Mcoeffs and cieobs can't both be None") if (Mcoeffs is None) & isinstance(cieobs,str): # use pre-calculated coeffs. Mcoeffs = _DAYLIGHT_M12_COEFFS[cieobs] if isinstance(Mcoeffs,str) & (cieobs is not None): # calculate coeffs. if (Mcoeffs == 'calc'): Mcoeffs = get_daylightphase_Mi_coeffs(cieobs = cieobs, S012_daylightphase = S012_daylightphase) Mcoeffs = Mcoeffs[cieobs] if isinstance(cieobs,str) else Mcoeffs['cmf_0'] else: raise Exception("Mcoeffs is a string, but not 'calc': unknown option.") if Mcoeffs is not None: c = Mcoeffs # Calculate M1, M2 and round to 3 decimals (CIE recommendation): denom = c['i'] + c['j']*xD + c['k']*yD rr = 3 # rounding of Mi M1 = np.round((c['i1'] + c['j1']*xD + c['k1']*yD) / denom, rr) M2 = np.round((c['i2'] + c['j2']*xD + c['k2']*yD) / denom, rr) # denom = (xyz[2,0]*xyz[1,1] - xyz[1,0]*xyz[2,1]) + (xyz[2,1]*S[1] - xyz[1,1]*S[2])*xD + (xyz[1,0]*S[2] - xyz[2,0]*S[1])*yD # M1 = np.round(((xyz[0,0]*xyz[2,1] - xyz[2,0]*xyz[0,1]) + (xyz[0,1]*S[2] - xyz[2,1]*S[0])*xD + (xyz[2,0]*S[0] - xyz[0,0]*S[2])*yD) / denom, 3) # M2 = np.round(((xyz[1,0]*xyz[0,1] - xyz[0,0]*xyz[1,1]) + (xyz[1,1]*S[0] - xyz[0,1]*S[1])*xD + (xyz[0,0]*S[1] - xyz[1,0]*S[0])*yD) / denom, 3) return M1, M2, c
def xyz_to_rgb(xyz,N,tr, xyz_black, tr_type = 'lut'): """ Convert xyz to input rgb. Args: :xyz: | ndarray [Nx3] with XYZ tristimulus values :N: | xyz to linear rgb conversion matrix :tr: | Tone Response function parameters or lut :xyz_black: | ndarray with XYZ tristimulus values of black :tr_type: | 'lut', optional | Type of Tone Response in tr input argument | options: | - 'lut': Tone-Response as a look-up-table | - 'gog': Tone-Response as a gain-offset-gamma function Returns: :rgb: | ndarray [Nx3] of display RGB values """ rgblin = _clamp0(np.dot(N,(xyz - xyz_black).T).T) # calculate rgblin and clamp to zero (on separate line for speed) return np.round(_rgb_delinearizer(rgblin,tr, tr_type = tr_type)) # delinearize rgblin
def _get_daylightlocus_parameters(ccts, spds, cieobs): """ Get daylight locus parameters for a single cieobs from daylight phase spectra determined based on parameters for '1931_2' as reported in CIE15-20xx. """ #------------------------------------------------ # Get locus parameters: #====================== # get xy coordinates for new cieobs: xyz_ = spd_to_xyz(spds, cieobs = cieobs) xy = xyz_[...,:2]/xyz_.sum(axis=-1,keepdims=True) # Fit 3e order polynomal xD(1/T) [4000 K < T <= 7000 K]: l7 = ccts<7000 pxT_l7 = np.polyfit((1000/ccts[l7]), xy[l7,0],3) # Fit 3e order polynomal xD(1/T) [T > 7000 K]: L7 = ccts>=7000 pxT_L7 = np.polyfit((1000/ccts[L7]), xy[L7,0],3) # Fit 2nd order polynomal yD(xD): pxy = np.round(np.polyfit(xy[:,0],xy[:,1],2),3) #pxy = np.hstack((0,pxy)) # make also 3e order for easy stacking return (xy, pxy, pxT_l7, pxT_L7, l7, L7)
def getUSCensusAgeDist(): """ Get US Census Age Distribution """ t_num = _INDVCMF_DATA['USCensus2010population'] list_AgeCensus = t_num[0] freq_AgeCensus = np.round( t_num[1] / 1000 ) # Reduce # of populations to manageable number, this doesn't change probability # Remove age < 10 and 70 < age: freq_AgeCensus[:10] = 0 freq_AgeCensus[71:] = 0 list_Age = [] for k in range(len(list_AgeCensus)): list_Age = np.hstack( (list_Age, np.repeat(list_AgeCensus[k], freq_AgeCensus[k]))) return list_Age
def xyz_to_rfl(xyz, CSF = None, rfl = None, out = 'rfl_est', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'xyz', cspace_tf = {},\ interp_type = 'nd', k_neighbours = 4, verbosity = 0): """ Approximate spectral reflectance of xyz values based on nd-dimensional linear interpolation or k nearest neighbour interpolation of samples from a standard reflectance set. Args: :xyz: | ndarray with xyz values of target points. :CSF: | None, optional | RGB camera response functions. | If None: input :xyz: contains raw rgb (float) values. Override :cspace: | argument and perform estimation directly in raw rgb space!!! :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: | 'xyz', optional | Color space for color coordinate to rfl mapping. | Tip: Use linear space (e.g. 'xyz', 'Yuv',...) for (interp_type == 'nd'), | and perceptually uniform space (e.g. 'ipt') for (interp_type == 'nearest') :cspace_tf: | {}, optional | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform. :interp_type: | 'nd', optional | Options: | - 'nd': perform n-dimensional linear interpolation using Delaunay triangulation. | - 'nearest': perform nearest neighbour interpolation. :k_neighbours: | 4 or int, optional | Number of nearest neighbours for reflectance spectrum interpolation. | Neighbours are found using scipy.spatial.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'] wlr = rfl[0] # get Ref spd: if refspd is None: refspd = _CIE_ILLUMINANTS['D65'].copy() refspd = cie_interp( refspd, wlr, kind='linear') # force spd to same wavelength range as rfl # Calculate rgb values of standard rfl set under refspd: if CSF is None: # Calculate lab coordinates: 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, :] else: # Calculate rgb coordinates from camera sensitivity functions rgb_rr = rfl_to_rgb(rfl, spd=refspd, CSF=CSF, wl=None) lab_rr = rgb_rr xyz = xyz lab_rr = np.round(lab_rr, _ROUNDING) # speed up search # Convert xyz to lab-type values under refspd: if CSF is None: lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy) else: lab = xyz # xyz contained rgb values !!! rgb = xyz lab = np.round(lab, _ROUNDING) # speed up search if interp_type == 'nearest': # 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 = sp.spatial.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: d += _EPS 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() elif interp_type == 'nd': rfl_est = math.ndinterp1_scipy(lab_rr, rfl[1:], lab) _isnan = np.isnan(rfl_est[:, 0]) if ( _isnan.any() ): #do nearest neigbour method for those that fail using Delaunay (i.e. ndinterp1_scipy) # 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 = sp.spatial.cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab[_isnan, ...], k=k_neighbours) if k_neighbours > 1: d += _EPS w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est_isnan = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum( w, axis=1) else: rfl_est_isnan = rfl[inds + 1, :].copy() rfl_est[_isnan, :] = rfl_est_isnan else: raise Exception('xyz_to_rfl(): unsupported interp_type!') rfl_est[ rfl_est < 0] = 0 #can occur for points outside convexhull of standard rfl set. 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(','))) & (CSF is None): 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() elif ((verbosity > 0) | ('xyz_est' in out.split(',')) | ('rgb_est' in out.split(',')) | ('DEi_rgb' in out.split(',')) | ('DEa_rgb' in out.split(','))) & (CSF is not None): rgb_est = rfl_to_rgb(rfl_est[1:], spd=refspd, CSF=CSF, wl=wlr) xyz_est = rgb_est DEi_rgb = np.sqrt(((rgb_est - rgb)**2).sum(axis=1)) DEa_rgb = DEi_rgb.mean() if verbosity > 0: if CSF is None: 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') else: n = 100 #min(rfl.shape[0]-1,rfl_est.shape[0]-1) s = np.random.permutation(rfl.shape[0] - 1)[:min(n, rfl.shape[0] - 1)] st = np.random.permutation(rfl_est.shape[0] - 1)[:min(n, rfl_est.shape[0] - 1)] fig = plt.figure() ax = np.zeros((3, ), dtype=np.object) ax[0] = fig.add_subplot(131) ax[1] = fig.add_subplot(132) ax[2] = fig.add_subplot(133, projection='3d') ax[0].plot(rfl[0], rfl[1:][s].T, linestyle='-') ax[0].set_title('Original RFL set (random selection of all)') ax[0].set_ylim([0, 1]) ax[1].plot(rfl_est[0], rfl_est[1:][st].T, linestyle='--') ax[0].set_title('Estimated RFL set (random selection of targets)') ax[1].set_ylim([0, 1]) ax[2].plot(rgb[st, 0], rgb[st, 1], rgb[st, 2], 'ro', label='Original') ax[2].plot(rgb_est[st, 0], rgb_est[st, 1], rgb_est[st, 2], 'bd', label='Rendered') ax[2].legend() if out == 'rfl_est': return rfl_est elif out == 'rfl_est,xyz_est': return rfl_est, xyz_est else: return eval(out)
#-------------------------------------------------------------------------- # read calibration data: #-------------------------------------------------------------------------- xyzcal = pd.read_csv(_PATH_DATA + 'XYZcal.csv',sep=',', header=None).values # read measured xyz data rgbcal = pd.read_csv(_PATH_DATA + 'RGBcal.csv',sep=',', header=None).values # read rgb data #-------------------------------------------------------------------------- # Apply functions as an example: #-------------------------------------------------------------------------- print('\nFunctional example:') # Get calibration gog-parameters/lut and conversion matrices M, N as well as xyz-black: M, N, tr, xyz_black, xyz_white = calibrate(rgbcal, xyzcal, L_type = L_type, tr_type = tr_type, avg = avg, cspace = cspace) # get calibration parameters if tr_type == 'gog': print("Calibration parameters :\nTR(gamma,offset,gain)=\n", np.round(tr,5),'\nM=\n',np.round(M,5),'\nN=\n',np.round(N,5)) # Check calibration performance: DElabi,DEli, DEabi = calibration_performance(rgbcal, xyzcal, M, N, tr, xyz_black, xyz_white, cspace=cspace, tr_type = tr_type, avg = avg, verbosity = 1, is_verification_data = False) # calculate calibration performance in cspace='lab' # define a test xyz for converion to rgb: xyz_test = np.array([[100.0,100.0,100.0]])*0.5 print('\nTest calibration for user defined xyz:\n xyz_test_est:', xyz_test) # print chosen test xyz # for a test xyz calculate the estimated rgb: rgb_test_est = xyz_to_rgb(xyz_test, N, tr, xyz_black, tr_type = tr_type) print(' rgb_test_est:', rgb_test_est) # print estimated rgb
def genMonteCarloObs(n_obs=1, fieldsize=10, list_Age=[32], out='LMS', wl=None, allow_negative_values=False): """ Monte-Carlo generation of individual observer cone fundamentals. Args: :n_obs: | 1, optional | Number of observer CMFs to generate. :list_Age: | list of observer ages or str, optional | Defaults to 32 (cfr. CIE2006 CMFs) | If 'us_census': use US population census of 2010 to generate list_Age. :fieldsize: | fieldsize in degrees (between 2° and 10°), optional | Defaults to 10°. :out: | 'LMS' or str, optional | Determines output. :wl: | None, optional | Interpolation/extraplation of :LMS: output to specified wavelengths. | None: output original _WL = np.array([390,780,5]) :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 [,var_age, vAll] | - LMS: ndarray with population LMS functions. | - var_age: ndarray with population observer ages. | - vAll: dict with population physiological factors (see .keys()) 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>`_ """ # Scale down StdDev by scalars optimized using Asano's 75 observers # collected in Germany: stdDevAllParam = _INDVCMF_STD_DEV_ALL_PARAM.copy() scale_factors = [0.98, 0.98, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] scale_factors = dict(zip(list(stdDevAllParam.keys()), scale_factors)) stdDevAllParam = { k: v * scale_factors[k] for (k, v) in stdDevAllParam.items() } # Get Normally-distributed Physiological Factors: vAll = getMonteCarloParam(n_obs=n_obs) if list_Age is 'us_census': list_Age = getUSCensusAgeDist() # Generate Random Ages with the same probability density distribution # as color matching experiment: sz_interval = 1 list_AgeRound = np.round(np.array(list_Age) / sz_interval) * sz_interval h = math.histogram(list_AgeRound, bins=np.unique(list_AgeRound), bin_center=True)[0] p = h / h.sum() # probability density distribution var_age = np.random.choice(np.unique(list_AgeRound), \ size = n_obs, replace = True,\ p = p) # Set requested wavelength range: if wl is not None: wl = getwlr(wl3=wl) else: wl = _WL LMS_All = np.zeros((3 + 1, wl.shape[0], n_obs)) LMS_All.fill(np.nan) for k in range(n_obs): t_LMS, t_trans_lens, t_trans_macula, t_sens_photopig = cie2006cmfsEx(age = var_age[k], fieldsize = fieldsize, wl = wl,\ var_od_lens = vAll['od_lens'][k], var_od_macula = vAll['od_macula'][k], \ var_od_L = vAll['od_L'][k], var_od_M = vAll['od_M'][k], var_od_S = vAll['od_S'][k],\ var_shft_L = vAll['shft_L'][k], var_shft_M = vAll['shft_M'][k], var_shft_S = vAll['shft_S'][k],\ out = 'LMS,trans_lens,trans_macula,sens_photopig') LMS_All[:, :, k] = t_LMS # listout = out.split(',') # if ('trans_lens' in listout) | ('trans_macula' in listout) | ('trans_photopig' in listout): # trans_lens[:,k] = t_trans_lens # trans_macula[:,k] = t_trans_macula # sens_photopig[:,:,k] = t_sens_photopig if n_obs == 1: LMS_All = np.squeeze(LMS_All, axis=2) if ('xyz' in out.lower().split(',')): LMS_All = lmsb_to_xyzb(LMS_All, 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') if (out == 'LMS'): return LMS_All elif (out == 'LMS,var_age,vAll'): return LMS_All, var_age, vAll else: return eval(out)
def get_daylightphase_Mi_coeffs(cieobs = None, wl3 = None, S012_daylightphase = None): """ Get coefficients of Mi weights of daylight phase for specific cieobs Args: :cieobs: | None or str or ndarray or list of str or list of ndarrays, optional | CMF set to get coefficients for. | If None: get coeffs for all CMFs in _CMF :wl3: | None, optional | Wavelength range to interpolate S012_daylightphase to. :S012_daylightphase: | None, optional | Daylight phase component functions. | If None: use _S012_DAYLIGHTPHASE Returns: :Mcoeffs: | Dictionary with i,j,k,i1,j1,k1,i2,j2,k2 for each cieobs in :cieobs: | If cieobs contains ndarrays, then keys in dict will be | labeled 'cmf_0', 'cmf_1', ... """ #------------------------------------------------- # Get Mi coefficients: #===================== # Get tristimulus values of daylight phase component functions: if S012_daylightphase is None: S012_daylightphase = _S012_DAYLIGHTPHASE if wl3 is not None: S012_daylightphase = cie_interp(S012_daylightphase,wl_new = wl3, kind='linear',negative_values_allowed = True) if cieobs is None: cieobs = _CMF['types'] if not isinstance(cieobs, list): cieobs = [cieobs] Mcoeffs = {} i = 0 for cieobs_ in cieobs: if isinstance(cieobs_,str): if 'scotopic' in cieobs_: continue if 'std_dev_obs' in cieobs_: continue key = cieobs_ else: key = 'cmf_{:1.0f}'.format(i) xyz = spd_to_xyz(S012_daylightphase, cieobs = cieobs_, relative = False, K = 1) S = xyz.sum(axis=-1) # Get coefficients in Mi: f = 1000/S[0]**2 r = 4 # rounding of i,j,k,i1,j1,k1,i2,j2,k2 c = {'i': np.round(f*(xyz[2,0]*xyz[1,1] - xyz[1,0]*xyz[2,1]),r)} c['j'] = np.round(f*(xyz[2,1]*S[1] - xyz[1,1]*S[2]),r) c['k'] = np.round(f*(xyz[1,0]*S[2] - xyz[2,0]*S[1]),r) c['i1'] = np.round(f*(xyz[0,0]*xyz[2,1] - xyz[2,0]*xyz[0,1]),r) c['j1'] = np.round(f*(xyz[0,1]*S[2] - xyz[2,1]*S[0]),r) c['k1'] = np.round(f*(xyz[2,0]*S[0] - xyz[0,0]*S[2]),r) c['i2'] = np.round(f*(xyz[1,0]*xyz[0,1] - xyz[0,0]*xyz[1,1]),r) c['j2'] = np.round(f*(xyz[1,1]*S[0] - xyz[0,1]*S[1]),r) c['k2']= np.round(f*(xyz[0,0]*S[1] - xyz[1,0]*S[0]),r) Mcoeffs[key] = c i+=1 return Mcoeffs