def _plot_tm30_report_top(axh, source = '', manufacturer = '', date = '', model = ''): """ Print source name, source model, manufacturer and date in an empty axes. Args: :axh: | Plot on specified axes. :source: | string with source name. :manufacturer: | string with source manufacturer. :model: | string with source model. :date: | string with source measurement date. Returns: :axh: | handle to figure axes. """ axh.set_xticks(np.arange(10)) axh.set_xticklabels(['' for i in np.arange(10)]) axh.set_yticks(np.arange(2)) axh.set_yticklabels(['' for i in np.arange(4)]) axh.set_axis_off() axh.set_xlabel([]) axh.text(0,1, 'Source: ' + source, fontsize = 10, horizontalalignment='left',verticalalignment='center',color = 'k') axh.text(0,0, ' Date: ' + date, fontsize = 10, horizontalalignment='left',verticalalignment='center',color = 'k') axh.text(5,1, 'Manufacturer: ' + manufacturer, fontsize = 10, horizontalalignment='left',verticalalignment='center',color = 'k') axh.text(5,0, 'Model: ' + model, fontsize = 10, horizontalalignment='left',verticalalignment='center',color = 'k') return axh
def get_subset(self, idx_R=None, idx_S=None): """ Get spectral data related to specific light source and reflectance data | (cfr. axis = 1 and axis = 0 in xyz ndarrays). Args: :idx_S: | None, optional | Index of light source related spectral data. | None: selects all. :idx_R: | None, optional | Index of reflectance sample related spectral data. | None selects all. Returns: :returns: | luxpy.CDATA instance with only selected spectral data. Note: If ndim < 3: selection is based on :idx_R: """ if idx_S is None: idx_S = np.arange(self.value.shape[1]) if idx_R is None: idx_R = np.arange(self.value.shape[0]) if self.value.ndim == 3: self.value = self.value[idx_R, idx_S, :] else: self.value = self.value[idx_R, ...] self.shape = self.value.shape return self
def 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), \ make_grid = True, limit_grid_radius = 0,color = 'k'): """ Generates a field of vectors using the base color shift model. | Has the option to plot vector field. Args: :poly_model: | function handle to model :pmodel: | ndarray with model parameters. :axr: | np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), optional | Ndarray specifying the a-coordinates at which to apply the model. :bxr: | np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), optional | Ndarray specifying the b-coordinates at which to apply the model. :make_grid: | True, optional | True: generate a 2d-grid from :axr:, :bxr:. :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: :color: | 'k', optional | For plotting the vector field. | If :color: == 0, no plot will be generated. Returns: :returns: | If :color: == 0: ndarray of axt,bxt,axr,bxr | Else: handle to axes used for plotting. """ # Generate grid from axr, bxr: if make_grid == True: axr, bxr = generate_grid(ax=axr, bx=bxr, out='ax,bx', limit_grid_radius=limit_grid_radius) # Apply model at ref. coordinates: axt, bxt, Cxt, hxt, axr, bxr, Cxr, hxr = apply_poly_model_at_x( poly_model, pmodel, axr, bxr) # Plot vectorfield: if color is not 0: #plt.plot(axr, bxr,'ro',markersize=2) plt.quiver(axr, bxr, axt - axr, bxt - bxr, headlength=1, color=color) plt.xlabel("a'") plt.ylabel("b'") return plt.gca() #plt.show(plot1) else: return axt, bxt, axr, bxr
def _plot_tm30_report_bottom(axh, spd, notes = '', max_len_notes_line = 40): """ Print some notes, the CIE x, y, u',v' and Ra, R9 values of the source in some empty axes. Args: :axh: | None, optional | Plot on specified axes. :spd: | ndarray or dict | If ndarray: single spectral power distribution. :notes: | string to be split :max_len_notes_line: | 40, optional | Maximum length of a single line when splitting the string. Returns: :axh: | handle to figure axes. """ ciera = spd_to_cri(spd, cri_type = 'ciera') cierai = spd_to_cri(spd, cri_type = 'ciera-14', out = 'Rfi') xyzw = spd_to_xyz(spd, cieobs = '1931_2', relative = True) Yxyw = xyz_to_Yxy(xyzw) Yuvw = xyz_to_Yuv(xyzw) notes_ = _split_notes(notes, max_len_notes_line = max_len_notes_line) axh.set_xticks(np.arange(10)) axh.set_xticklabels(['' for i in np.arange(10)]) axh.set_yticks(np.arange(4)) axh.set_yticklabels(['' for i in np.arange(4)]) axh.set_axis_off() axh.set_xlabel([]) axh.text(0,2.8, 'Notes: ', fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(0.75,2.8, notes_, fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,2.8, "x {:1.4f}".format(Yxyw[0,1]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,2.2, "y {:1.4f}".format(Yxyw[0,2]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,1.6, "u' {:1.4f}".format(Yuvw[0,1]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(6,1.0, "v' {:1.4f}".format(Yuvw[0,2]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,2.8, "CIE 13.3-1995", fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,2.2, " (CRI) ", fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,1.6, " $R_a$ {:1.0f}".format(ciera[0,0]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') axh.text(7.5,1.0, " $R_9$ {:1.0f}".format(cierai[9,0]), fontsize = 9, horizontalalignment='left',verticalalignment='top',color = 'k') # Create a Rectangle patch rect = patches.Rectangle((7.2,0.5),1.7,2.5,linewidth=1,edgecolor='k',facecolor='none') # Add the patch to the Axes axh.add_patch(rect) return axh
def _create_subjects_index_arr(subjects=None, grouping=None): """ Create subjects indexing array""" if subjects is None: if grouping is None: raise Exception('Grouping must be supplied!') groups = np.unique(grouping) for i, group in enumerate(groups): if i == 0: subjects = np.arange(((grouping == group) * 1).sum()) else: subjects = np.hstack( (subjects, np.arange(((grouping == group) * 1).sum()))) return subjects
def _hue_bin_data_to_Rxhj(hue_bin_data, scale_factor): nhbins = hue_bin_data['nhbins'] start_hue = hue_bin_data['start_hue'] # A. Local color fidelity, Rfhj: #DEhj = ((hue_bin_data['jabt_hj']-hue_bin_data['jabr_hj'])**2).sum(axis=-1)**0.5 DEhj = hue_bin_data[ 'DE_hj'] #TM30 specifies average of DEi per hue bin, not DE of average jabt, jabr Rfhj = log_scale(DEhj, scale_factor=scale_factor) # B.Local chroma shift and hue shift, [Rcshi, Rhshi]: # B.1 relative paths: dab = (hue_bin_data['jabt_hj'] - hue_bin_data['jabr_hj'])[..., 1:] / ( hue_bin_data['Cr_hj'][..., None]) # B.2 Reference unit circle: hbincenters = np.arange(start_hue + np.pi / nhbins, 2 * np.pi, 2 * np.pi / nhbins)[..., None] arc = np.cos(hbincenters) brc = np.sin(hbincenters) # B.3 calculate local chroma shift, Rcshi: Rcshi = dab[..., 0] * arc + dab[..., 1] * brc # B.4 calculate local hue shift, Rcshi: Rhshi = dab[..., 1] * arc - dab[..., 0] * brc return Rcshi, Rhshi, Rfhj, DEhj
def mutation(Xp, options): """ Performs mutation in the individuals. | The mutation is one of the operators responsible for random changes in | the individuals. Each parent x will have a new individual, called trial | vector u, after the mutation. | To do that, pick up two random individuals from the population, x2 and | x3, and creates a difference vector v = x2 - x3. Then, chooses another | point, called base vector, xb, and creates the trial vector by | | u = xb + F*v = xb + F*(x2 - x3) | | wherein F is an internal parameter, called scale factor. Args: :Xp: | a n x mu ndarray with mu "parents" and of dimension n :options: | the dict with the internal parameters Returns: :Xo: | a n x mu ndarray with the mu mutated individuals (of dimension n) """ # Creates a mu x mu matrix of 1:n elements on each row A = np.arange(options['mu']).repeat(options['mu']).reshape(options['mu'],options['mu']).T # Now, one removes the diagonal of A, because it contains indexes that repeat # the current i-th individual A = np.reshape(A[(np.eye(A.shape[0]))==False],(options['mu'],options['mu']-1)) # Now, creates a matrix that permutes the elements of A randomly J = np.argsort(np.random.rand(*A.shape), axis = 1) # J = getdata('J.txt')-1 Ilin = J*options['mu'] + np.arange(options['mu'])[:,None] A = A.T.flatten()[Ilin].reshape(A.shape) # Chooses three random points (for each row) xbase = Xp[:, A[:,0]] #base vectors v = Xp[:, A[:,1]] - Xp[:, A[:,2]] #difference vector # Performs the mutation Xo = xbase + options['F']*v return Xo
def plotcircle(center=np.array([[0., 0.]]), radii=np.arange(0, 60, 10), angles=np.arange(0, 350, 10), color='k', linestyle='--', out=None, axh=None, **kwargs): """ Plot one or more concentric circles. Args: :center: | np.array([[0.,0.]]) or ndarray with center coordinates, optional :radii: | np.arange(0,60,10) or ndarray with radii of circle(s), optional :angles: | np.arange(0,350,10) or ndarray with angles (°), optional :color: | 'k', optional | Color for plotting. :linestyle: | '--', optional | Linestyle of circles. :out: | None, optional | If None: plot circles, return (x,y) otherwise. """ xs = np.array([0]) ys = xs.copy() if ((out != 'x,y') & (axh is None)): fig, axh = plt.subplots(rows=1, ncols=1) for ri in radii: x = center[:, 0] + ri * np.cos(angles * np.pi / 180) y = center[:, 1] + ri * np.sin(angles * np.pi / 180) xs = np.hstack((xs, x)) ys = np.hstack((ys, y)) if (out != 'x,y'): axh.plot(x, y, color=color, linestyle=linestyle, **kwargs) if out == 'x,y': return xs, ys elif out == 'axh': return axh
def _rgb_delinearizer(rgblin, tr, tr_type = 'lut'): """ De-linearize linear rgblin using tr tone response function or lut """ if tr_type == 'gog': return np.array([TRi(rgblin[:,i],*tr[i]) for i in range(3)]).T elif tr_type == 'lut': maxv = (tr.shape[0] - 1) bins = np.vstack((tr-np.diff(tr,axis=0,prepend=0)/2,tr[-1,:]+0.01)) # create bins idxs = np.array([(np.digitize(rgblin[:,i],bins[:,i]) - 1) for i in range(3)]).T # find bin indices idxs[idxs>maxv] = maxv rgb = np.arange(tr.shape[0])[idxs] return rgb
def _permutate_grouping(grouping, subjects, paired=False): """ permutate grouping and subjects indexing arrays""" if paired == False: perm_idx = np.arange(grouping.shape[0], dtype=int) perm_idx = np.random.permutation(perm_idx) perm_grouping = grouping[perm_idx] perm_subjects = subjects[perm_idx] else: groups = np.unique(grouping) o = 10**((grouping.reshape(len(groups), len(grouping) // len(groups))) + 1) if subjects is not None: s = subjects.reshape(len(groups), len(grouping) // len(groups)) for i in range(o.shape[-1]): perm_idx = np.arange(o.shape[0], dtype=int) perm_idx = np.random.permutation(perm_idx) o[:, i] = o[perm_idx, i] s[:, i] = s[perm_idx, i] o = np.log10(o) - 1 perm_grouping = o.flatten() if subjects is not None: perm_subjects = s.flatten() return perm_grouping, perm_subjects
def plotcircle(radii = np.arange(0,60,10), \ angles = np.arange(0,350,10),\ color = 'k',linestyle = '--', out = None): """ Plot one or more concentric circles around (0,0). Args: :radii: | np.arange(0,60,10) or ndarray with radii of circle(s), optional :angles: | np.arange(0,350,10) or ndarray with angles (°), optional :color: | 'k', optional | Color for plotting. :linestyle: | '--', optional | Linestyle of circles. :out: | None, optional | If None: plot circles, return (x,y) otherwise. Returns: :x,y: | ndarrays with circle coordinates (only returned if out is 'x,y') """ x = np.array([0]) y = x.copy() for ri in radii: xi = ri * np.cos(angles * np.pi / 180) yi = ri * np.sin(angles * np.pi / 180) x = np.hstack((x, xi)) y = np.hstack((y, yi)) if out != 'x,y': plt.plot(xi, yi, color=color, linestyle=linestyle) if out == 'x,y': return x, y
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 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.zeros((len(idxp), R.shape[1])) rflsampled.fill(np.nan) 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 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.zeros((len(dataVF), 1)) delta_SvsVF_vshift_ab_mean.fill(np.nan) delta_SvsVF_vshift_ab_mean_normalized = delta_SvsVF_vshift_ab_mean.copy() delta_PXvsVF_vshift_ab_mean = np.zeros((len(dataVF), 1)) delta_PXvsVF_vshift_ab_mean.fill(np.nan) 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
import warnings from imageio import imsave __all__ = [ '_HYPSPCIM_PATH', '_HYPSPCIM_DEFAULT_IMAGE', 'render_image', 'xyz_to_rfl', 'get_superresolution_hsi', 'hsi_to_rgb', 'rfl_to_rgb' ] _HYPSPCIM_PATH = _PKG_PATH + _SEP + 'hypspcim' + _SEP _HYPSPCIM_DEFAULT_IMAGE = _PKG_PATH + _SEP + 'toolboxes' + _SEP + 'hypspcim' + _SEP + 'data' + _SEP + 'testimage1.jpg' _ROUNDING = 6 # to speed up xyz_to_rfl search algorithm # Nikon D700 camera sensitivity functions: _CSF_NIKON_D700 = np.vstack( (np.arange(400, 710, 10), np.array([[ 0.005, 0.007, 0.012, 0.015, 0.023, 0.025, 0.030, 0.026, 0.024, 0.019, 0.010, 0.004, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 ], [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.001, 0.002, 0.003, 0.005, 0.007, 0.012, 0.013, 0.015, 0.016, 0.017, 0.020, 0.013, 0.011, 0.009, 0.005, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.002, 0.002, 0.003 ], [ 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
def calibrate(rgbcal, xyzcal, L_type = 'lms', tr_type = 'lut', cieobs = '1931_2', nbit = 8, cspace = 'lab', avg = lambda x: ((x**2).mean()**0.5), ensure_increasing_lut_at_low_rgb = 0.2, verbosity = 1, sep=',',header=None): """ Calculate TR parameters/lut and conversion matrices. Args: :rgbcal: | ndarray [Nx3] or string with filename of RGB values | rgcal must contain at least the following type of settings: | - pure R,G,B: e.g. for pure R: (R != 0) & (G==0) & (B == 0) | - white(s): R = G = B = 2**nbit-1 | - gray(s): R = G = B | - black(s): R = G = B = 0 | - binary colors: cyan (G = B, R = 0), yellow (G = R, B = 0), magenta (R = B, G = 0) :xyzcal: | ndarray [Nx3] or string with filename of measured XYZ values for | the RGB settings in rgbcal. :L_type: | 'lms', optional | Type of response to use in the derivation of the Tone-Response curves. | options: | - 'lms': use cone fundamental responses: L vs R, M vs G and S vs B | (reduces noise and generally leads to more accurate characterization) | - 'Y': use the luminance signal: Y vs R, Y vs G, Y vs B :tr_type: | 'lut', optional | options: | - 'lut': Derive/specify Tone-Response as a look-up-table | - 'gog': Derive/specify Tone-Response as a gain-offset-gamma function :cieobs: | '1931_2', optional | CIE CMF set used to determine the XYZ tristimulus values | (needed when L_type == 'lms': determines the conversion matrix to | convert xyz to lms values) :nbit: | 8, optional | RGB values in nbit format (e.g. 8, 16, ...) :cspace: | color space or chromaticity diagram to calculate color differences in | when optimizing the xyz_to_rgb and rgb_to_xyz conversion matrices. :avg: | lambda x: ((x**2).mean()**0.5), optional | Function used to average the color differences of the individual RGB settings | in the optimization of the xyz_to_rgb and rgb_to_xyz conversion matrices. :ensure_increasing_lut_at_low_rgb: | 0.2 or float (max = 1.0) or None, optional | Ensure an increasing lut by setting all values below the RGB with the maximum | zero-crossing of np.diff(lut) and RGB/RGB.max() values of :ensure_increasing_lut_at_low_rgb: | (values of 0.2 are a good rule of thumb value) | Non-strictly increasing lut values can be caused at low RGB values due | to noise and low measurement signal. | If None: don't force lut, but keep as is. :verbosity: | 1, optional | > 0: print and plot optimization results :sep: | ',', optional | separator in files with rgbcal and xyzcal data :header: | None, optional | header specifier for files with rgbcal and xyzcal data | (see pandas.read_csv) Returns: :M: | linear rgb to xyz conversion matrix :N: | xyz to linear rgb conversion matrix :tr: | Tone Response function parameters or lut :xyz_black: | ndarray with XYZ tristimulus values of black :xyz_white: | ndarray with tristimlus values of white """ # process rgb, xyzcal inputs: rgbcal, xyzcal = _parse_rgbxyz_input(rgbcal, xyz = xyzcal, sep = sep, header=header) # get black-positions and average black xyz (flare): p_blacks = (rgbcal[:,0]==0) & (rgbcal[:,1]==0) & (rgbcal[:,2]==0) xyz_black = xyzcal[p_blacks,:].mean(axis=0,keepdims=True) # Calculate flare corrected xyz: xyz_fc = xyzcal - xyz_black # get positions of pure r, g, b values: p_pure = [(rgbcal[:,1]==0) & (rgbcal[:,2]==0), (rgbcal[:,0]==0) & (rgbcal[:,2]==0), (rgbcal[:,0]==0) & (rgbcal[:,1]==0)] # set type of L-response to use: Y for R,G,B or L,M,S for R,G,B: if L_type == 'Y': L = np.array([xyz_fc[:,1] for i in range(3)]).T elif L_type == 'lms': lms = (math.normalize_3x3_matrix(_CMF[cieobs]['M'].copy()) @ xyz_fc.T).T L = np.array([lms[:,i] for i in range(3)]).T # Get rgb linearizer parameters or lut and apply to all rgb's: if tr_type == 'gog': par = np.array([sp.optimize.curve_fit(TR, rgbcal[p_pure[i],i], L[p_pure[i],i]/L[p_pure[i],i].max(), p0=[1,0,1])[0] for i in range(3)]) # calculate parameters of each TR tr = par elif tr_type == 'lut': dac = np.arange(2**nbit) # lut = np.array([cie_interp(np.vstack((rgbcal[p_pure[i],i],L[p_pure[i],i]/L[p_pure[i],i].max())), dac, kind ='cubic')[1,:] for i in range(3)]).T lut = np.array([sp.interpolate.PchipInterpolator(rgbcal[p_pure[i],i],L[p_pure[i],i]/L[p_pure[i],i].max())(dac) for i in range(3)]).T # use this one to avoid potential overshoot with cubic spline interpolation (but slightly worse performance) lut[lut<0] = 0 # ensure monotonically increasing lut values for low signal: if ensure_increasing_lut_at_low_rgb is not None: #ensure_increasing_lut_at_low_rgb = 0.2 # anything below that has a zero-crossing for diff(lut) will be set to zero for i in range(3): p0 = np.where((np.diff(lut[dac/dac.max() < ensure_increasing_lut_at_low_rgb,i])<=0))[0] if p0.any(): p0 = range(0,p0[-1]) lut[p0,i] = 0 tr = lut # plot: if verbosity > 0: colors = 'rgb' linestyles = ['-','--',':'] rgball = np.repeat(np.arange(2**8)[:,None],3,axis=1) Lall = _rgb_linearizer(rgball, tr, tr_type = tr_type) plt.figure() for i in range(3): plt.plot(rgbcal[p_pure[i],i],L[p_pure[i],i]/L[p_pure[i],i].max(),colors[i]+'o') plt.plot(rgball[:,i],Lall[:,i],colors[i]+linestyles[i],label=colors[i]) plt.xlabel('Display RGB') plt.ylabel('Linear RGB') plt.legend() plt.title('Tone response curves') # linearize all rgb values and clamp to 0 rgblin = _rgb_linearizer(rgbcal, tr, tr_type = tr_type) # get rgblin to xyz_fc matrix: M = np.linalg.lstsq(rgblin, xyz_fc, rcond=None)[0].T # get xyz_fc to rgblin matrix: N = np.linalg.inv(M) # get better approximation for conversion matrices: p_grays = (rgbcal[:,0] == rgbcal[:,1]) & (rgbcal[:,0] == rgbcal[:,2]) p_whites = (rgbcal[:,0] == (2**nbit-1)) & (rgbcal[:,1] == (2**nbit-1)) & (rgbcal[:,2] == (2**nbit-1)) xyz_white = xyzcal[p_whites,:].mean(axis=0,keepdims=True) # get xyzw for input into xyz_to_lab() or colortf() def optfcn(x, rgbcal, xyzcal, tr, xyz_black, cspace, p_grays, p_whites,out,verbosity): M = x.reshape((3,3)) xyzest = rgb_to_xyz(rgbcal, M, tr, xyz_black, tr_type) xyzw = xyzcal[p_whites,:].mean(axis=0) # get xyzw for input into xyz_to_lab() or colortf() labcal, labest = colortf(xyzcal,tf=cspace,xyzw=xyzw), colortf(xyzest,tf=cspace,xyzw=xyzw) # calculate lab coord. of cal. and est. DEs = ((labcal-labest)**2).sum(axis=1)**0.5 DEg = DEs[p_grays] DEw = DEs[p_whites] F = (avg(DEs)**2 + avg(DEg)**2 + avg(DEw**2))**0.5 if verbosity > 1: print('\nPerformance of TR + rgb-to-xyz conversion matrix M:') print('all: DE(jab): avg = {:1.4f}, std = {:1.4f}'.format(avg(DEs),np.std(DEs))) print('grays: DE(jab): avg = {:1.4f}, std = {:1.4f}'.format(avg(DEg),np.std(DEg))) print('whites(s) DE(jab): avg = {:1.4f}, std = {:1.4f}'.format(avg(DEw),np.std(DEw))) if out == 'F': return F else: return eval(out) x0 = M.ravel() res = math.minimizebnd(optfcn, x0, args =(rgbcal, xyzcal, tr, xyz_black, cspace, p_grays, p_whites,'F',0), use_bnd=False) xf = res['x_final'] M = optfcn(xf, rgbcal, xyzcal, tr, xyz_black, cspace, p_grays, p_whites,'M',verbosity) N = np.linalg.inv(M) return M, N, tr, xyz_black, xyz_white
def get_daylightloci_parameters(ccts=None, cieobs=None, wl3=[300, 830, 10], verbosity=0): """ Get parameters for the daylight loci functions xD(1000/CCT) and yD(xD). Args: :ccts: | None, optional | ndarray with CCTs, if None: ccts = np.arange(4000,25000,250) :cieobs: | None or list, optional | CMF sets to determine parameters for. | If None: get for all CMFs sets in _CMF (except scoptopic and deviate observer) :wl3: | [300,830,10], optional | Wavelength range and spacing of daylight phases to be determined | from '1931_2'. The default setting results in parameters very close | to that in CIE15-2004/2018. :verbosity: | 0, optional | print parameters and make plots. Returns: :dayloci: | dict with parameters for each cieobs """ if ccts is None: ccts = np.arange(4000, 25000, 250) # Get daylight phase spds using cieobs '1931_2': # wl3 = [300,830,10] # results in Judd's (1964) coefficients for the function yD(xD)x; other show slight deviations for i, cct in enumerate(ccts): spd = daylightphase(cct, wl3=wl3, force_daylight_below4000K=False) if i == 0: spds = spd else: spds = np.vstack((spds, spd[1:])) if verbosity > 0: fig, axs = plt.subplots( nrows=2, ncols=len(_CMF['types']) - 2) # -2: don't include scoptopic and dev observers dayloci = {} i = 0 for cieobs in _CMF['types']: if 'scotopic' in cieobs: continue if 'std_dev_obs' in cieobs: continue # get parameters for cieobs: xy, pxy, pxT_l7, pxT_L7, l7, L7 = _get_daylightlocus_parameters( ccts, spds, cieobs) dayloci[cieobs] = {'pxT_l7k': pxT_l7, 'pxT_L7k': pxT_L7, 'pxy': pxy} if verbosity > 0: print('\n cieobs:', cieobs) print('pxT_l7 (Tcp<=7000K):', pxT_l7) print('pxT_L7 (Tcp>7000K):', pxT_L7) print('p:xy', pxy) axs[0, i].plot(ccts, xy[:, 0], 'r-', label='Data') axs[0, i].plot(ccts[l7], np.polyval(pxT_l7, 1000 / ccts[l7]), 'b--', label='Fit (Tcp<=7000K)') axs[0, i].plot(ccts[L7], np.polyval(pxT_L7, 1000 / ccts[L7]), 'c--', label='Fit (Tcp>7000K)') axs[0, i].set_title(cieobs) axs[0, i].set_xlabel('Tcp (K)') axs[0, i].set_ylabel('xD') axs[0, i].legend() #plotSL(cieobs = cieobs, cspace = 'Yxy', DL = False, axh = axs[1,i]) axs[1, i].plot(xy[:, 0], xy[:, 1], 'r-', label='Data') axs[1, i].plot(xy[:, 0], np.polyval(pxy, xy[:, 0]), 'b--', label='Fit') # axs[1,i].plot(xy[:,0],np.polyval(pxy_31,xy[:,0]),'g:') axs[1, i].set_xlabel('xD') axs[1, i].set_ylabel('yD') axs[1, i].legend() i += 1 return dayloci
def _get_hue_map(hbins = 16, start_hue = 0.0, hbinnrs = None, xyzri = None, xyzrw = None, cri_type = None): """ Generate color map for hue bins. Args: :hbins: | 16 or ndarray with sorted hue bin centers (°), optional :start_hue: | 0.0, optional :hbinnrs: | None, optional | ndarray with hue bin number of each sample. | If hbinnrs, xyzri, xyzrw and cri_type are all not-None: | use these to calculate color map, otherwise just use number of | hue bins :hbins: and :start_hue: :xyzri: | None, optional | relative xyz tristimulus values of samples under ref. illuminant. | see :hbinnrs: for more info when this is used. :xyzrw: | None, optional | relative xyz tristimulus values of ref. illuminant white point. | see :hbinnrs: for more info when this is used. :cri_type: | None, optional | Specifies dict with default cri model parameters | (needed to get correct :cieobs:) | see :hbinnrs: for more info when this is used. Returns: :cmap: | list with rgb values (one for each hue bin) for plotting. """ # Setup hbincenters and hsv_hues: if isinstance(hbins,float) | isinstance(hbins,int): nhbins = hbins dhbins = 360/(nhbins) # hue bin width hbincenters = np.arange(start_hue + dhbins/2, 360, dhbins) hbincenters = np.sort(hbincenters) else: hbincenters = hbins idx = np.argsort(hbincenters) hbincenters = hbincenters[idx] nhbins = hbincenters.shape[0] cmap = [] if (hbinnrs is not None) & (xyzri is not None) & (xyzrw is not None) & (cri_type is not None): xyzw = spd_to_xyz(_CIE_D65, relative = True, cieobs = cri_type['cieobs']['xyz']) xyzri = cat.apply(xyzri[:,0,:],xyzw1 = xyzrw, xyzw2 = xyzw) # Create color from xyz average: for i in range(nhbins): xyzrhi = xyzri[hbinnrs[:,0] == i,:].mean(axis=0,keepdims=True) rgbrhi = xyz_to_srgb(xyzrhi)/255 cmap.append(rgbrhi) else: # Create color from hue angle: # Setup color for plotting hue bins: hbincenters = hbincenters*np.pi/180 hsv_hues = hbincenters - 30*np.pi/180 hsv_hues = hsv_hues/hsv_hues.max() for i in range(nhbins): #c = np.abs(np.array(colorsys.hsv_to_rgb(hsv_hues[i], 0.75, 0.85))) c = np.abs(np.array(colorsys.hls_to_rgb(hsv_hues[i], 0.45, 0.5))) cmap.append(c) return cmap
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 plot_tm30_Rfi(spd, cri_type = 'ies-tm30', axh = None, font_size = _TM30_FONT_SIZE, **kwargs): """ Plot Sample Color Fidelity values (Rfi). Args: :spd: | ndarray or dict | If ndarray: single spectral power distribution. | If dict: dictionary with pre-computed parameters (using _tm30_process_spd()). | required keys: | 'Rf','Rg','cct','duv','Sr','cri_type','xyzri','xyzrw', | 'hbinnrs','Rfi','Rfhi','Rcshi','Rhshi', | 'jabt_binned','jabr_binned', | 'nhbins','start_hue','normalize_gamut','normalized_chroma_ref' | see cri.spd_to_cri() for more info on parameters. :cri_type: | _CRI_TYPE_DEFAULT or str or dict, optional | -'str: specifies dict with default cri model parameters | (for supported types, see luxpy.cri._CRI_DEFAULTS['cri_types']) | - dict: user defined model parameters | (see e.g. luxpy.cri._CRI_DEFAULTS['cierf'] | for required structure) | Note that any non-None input arguments (in kwargs) | to the function will override default values in cri_type dict. :axh: | None, optional | If None: create new figure with single axes, else plot on specified axes. :font_size: | _TM30_FONT_SIZE, optional | Font size of text, axis labels and axis values. :kwargs: | Additional optional keyword arguments, | the same as in cri.spd_to_cri() Returns: :axh: | handle to figure axes. :data: | dictionary with required parameters for plotting functions. """ data = _tm30_process_spd(spd, cri_type = 'ies-tm30',**kwargs) Rfi = data['Rfi'] # get rgb values representing each sample: N = data['xyzri'].shape[0] xyzw = spd_to_xyz(_CIE_D65, relative = True, cieobs = data['cri_type']['cieobs']['xyz']) xyzri = cat.apply(data['xyzri'][:,0,:],xyzw1 = data['xyzrw'], xyzw2 = xyzw) rgbri = xyz_to_srgb(xyzri)/255 # Create color map: cmap = [] for i in range(N): cmap.append(rgbri[i,...]) # Plot sample color fidelity, Rfi: if axh is None: fig, axh = plt.subplots(nrows = 1, ncols = 1) for j in range(N): axh.bar(j,Rfi[j,0], color = cmap[j], width = 1,edgecolor = None, alpha = 0.9) #axh.text(j,Rfi[j,0]*1.1, '{:1.0f}'.format(Rfi[j,0]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',color = np.array([1,1,1])*0.3) xticks = np.arange(0,N,step=2) xtickslabels = ['CES{:1.0f}'.format(ii+1) for ii in range(0,N,2)] axh.set_xticks(xticks) axh.set_xticklabels(xtickslabels, fontsize = font_size, rotation = 90) axh.set_ylabel(r'Color Sample Fidelity $(R_{f,CESi})$', fontsize = font_size) axh.set_ylim([0,100]) axh.set_xlim([-0.5,N-0.5]) return axh, data
def apply_poly_model_at_hue_x(poly_model, pmodel, dCHoverC_res, \ hx = None, Cxr = 40, sig = _VF_SIG): """ Applies base color shift model at (hue,chroma) coordinates Args: :poly_model: | function handle to model :pmodel: | ndarray with model parameters. :dCHoverC_res: | ndarray with residuals between 'dCoverC,dH' of samples | and 'dCoverC,dH' predicted by the model. | Note: dCoverC = (Ct - Cr)/Cr and dH = ht - hr | (predicted from model, see notes luxpy.cri.get_poly_model()) :hx: | None or ndarray, optional | None defaults to np.arange(np.pi/10.0,2*np.pi,2*np.pi/10.0) :Cxr: | 40, optional :sig: | _VF_SIG or float, optional | Determines smooth transition between hue-bin-boundaries (no hard | cutoff at hue bin boundary). Returns: :returns: | ndarrays with dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig | Note '_sig' denotes the uncertainty: | e.g. dH_x_sig is the uncertainty of dH at input (hue/chroma). """ if hx is None: dh = 2 * np.pi / 10.0 hx = np.arange( dh / 2, 2 * np.pi, dh ) #hue angles at which to apply model, i.e. calculate 'average' measures # A calculate reference coordinates: axr = Cxr * np.cos(hx) bxr = Cxr * np.sin(hx) # B apply model at reference coordinates to obtain test coordinates: axt, bxt, Cxt, hxt, axr, bxr, Cxr, hxr = apply_poly_model_at_x( poly_model, pmodel, axr, bxr) # C Calculate dC/C, dH for test and ref at fixed hues: dCoverC_x = (Cxt - Cxr) / (np.hstack((Cxr + Cxt)).max()) dH_x = (180 / np.pi) * (hxt - hxr) # dCoverC_x = np.round(dCoverC_x,decimals = 2) # dH_x = np.round(dH_x,decimals = 0) # D calculate 'average' noise measures using sig-value: href = dCHoverC_res[:, 0:1] dCoverC_res = dCHoverC_res[:, 1:2] dHoverC_res = dCHoverC_res[:, 2:3] dHsigi = np.exp((np.dstack( (np.abs(hx - href), np.abs((hx - href - 2 * np.pi)), np.abs(hx - href - 2 * np.pi))).min(axis=2)**2) / (-2) / sig) dH_x_sig = (180 / np.pi) * (np.sqrt( (dHsigi * (dHoverC_res**2)).sum(axis=0, keepdims=True) / dHsigi.sum(axis=0, keepdims=True))) #dH_x_sig_avg = np.sqrt(np.sum(dH_x_sig**2,axis=1)/hx.shape[0]) dCoverC_x_sig = (np.sqrt( (dHsigi * (dCoverC_res**2)).sum(axis=0, keepdims=True) / dHsigi.sum(axis=0, keepdims=True))) #dCoverC_x_sig_avg = np.sqrt(np.sum(dCoverC_x_sig**2,axis=1)/hx.shape[0]) return dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig
Opt. Express, vol. 23, no. 9, pp. 12045–12064, 2015. <https://www.osapublishing.org/oe/abstract.cfm?uri=oe-23-9-12045&origin=search>`_ | `M. Withouck, K. A. G. Smet, and P. Hanselaer, (2015), “Brightness prediction of different sized unrelated self-luminous stimuli,” Opt. Express, vol. 23, no. 10, pp. 13455–13466. <https://www.osapublishing.org/oe/abstract.cfm?uri=oe-23-10-13455&origin=search>`_ """ from luxpy import _CMF, _CIE_ILLUMINANTS, _MUNSELL, spd_to_xyz, cie_interp from luxpy.utils import np, np2d, asplit, ajoin from luxpy.color.cam.colorappearancemodels import hue_angle, hue_quadrature _CAM15U_AXES = {'qabW_cam15u': ["Q (cam15u)", "aW (cam15u)", "bW (cam15u)"]} _CAM15U_UNIQUE_HUE_DATA = { 'hues': 'red yellow green blue red'.split(), 'i': np.arange(5.0), 'hi': [20.14, 90.0, 164.25, 237.53, 380.14], 'ei': [0.8, 0.7, 1.0, 1.2, 0.8], 'Hi': [0.0, 100.0, 200.0, 300.0, 400.0] } _CAM15U_PARAMETERS = { 'k': [666.7, 782.3, 1444.6], 'cp': 1.0 / 3, 'cA': 3.22, 'cAlms': [2.0, 1.0, 1 / 20], 'ca': 1.0, 'calms': [1.0, -12 / 11, 1 / 11],
def __init__(self, spd = None, wl = None, ax0iswl = True, dtype = 'S', \ wl_new = None, interp_method = 'auto', negative_values_allowed = False, extrap_values = None,\ norm_type = None, norm_f = 1,\ header = None, sep = ','): """ Initialize instance of SPD. Args: :spd: | None or ndarray or str, optional | If None: self.value is initialized with zeros. | If str: spd contains filename. | If ndarray: ((wavelength, spectra)) or (spectra). | If latter, :wl: should contain the wavelengths. :wl: | None or ndarray, optional | Wavelengths. | Either specified as a 3-vector ([start, stop, spacing]) | or as full wavelength array. :a0iswl: | True, optional | Signals that first axis of :spd: contains wavelengths. :dtype: | 'S', optional | Type of spectral object (e.g. 'S' for source spectrum, 'R' for reflectance spectra, etc.) | See SPD._INTERP_TYPES for more options. | This is used to automatically determine the correct kind of interpolation method according to CIE15-2018. :wl_new: | None or ndarray with wavelength range, optional | If None: don't interpolate, else perform interpolation. :interp_method: | - 'auto', optional | If 'auto': method is determined based on :dtype: :negative-values_allowed: | False, optional (for cie_interp()) | Spectral data can not be negative. Values < 0 are therefore clipped when set to False. :extrap_values: | None, optional | float or list or ndarray with values to extrapolate | If None: use CIE recommended 'closest value' approach. :norm_type: | None or str, optional | - 'lambda': make lambda in norm_f equal to 1 | - 'area': area-normalization times norm_f | - 'max': max-normalization times norm_f :norm_f: | 1, optional | Normalization factor determines size of normalization | for 'max' and 'area' | or which wavelength is normalized to 1 for 'lambda' option. """ if spd is not None: if isinstance(spd, str): spd = SPD.read_csv_(self, file=spd, header=header, sep=sep) if ax0iswl == True: self.wl = spd[0] self.value = spd[1:] else: self.wl = wl if (self.wl.size == 3): self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2]) self.value = spd if self.value.shape[1] != self.wl.shape[0]: raise Exception( 'SPD.__init__(): Dimensions of wl and spd do not match.') else: if (wl is None): self.wl = SPD._WL3 else: self.wl = wl if (self.wl.size == 3): self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2]) self.value = np.zeros((1, self.wl.size)) self.wl = self.wl self.dtype = dtype self.shape = self.value.shape self.N = self.shape[0] if wl_new is not None: if interp_method == 'auto': interp_method = dtype self.cie_interp(wl_new, kind=interp_method, negative_values_allowed=negative_values_allowed, extrap_values=extrap_values) if norm_type is not None: self.normalize(norm_type=norm_type, norm_f=norm_f)
def plot_hue_bins(hbins = 16, start_hue = 0.0, scalef = 100, \ plot_axis_labels = False, bin_labels = '#', plot_edge_lines = True, \ plot_center_lines = False, plot_bin_colors = True, \ plot_10_20_circles = False,\ axtype = 'polar', ax = None, force_CVG_layout = False): """ Makes basis plot for Color Vector Graphic (CVG). Args: :hbins: | 16 or ndarray with sorted hue bin centers (°), optional :start_hue: | 0.0, optional :scalef: | 100, optional | Scale factor for graphic. :plot_axis_labels: | False, optional | Turns axis ticks on/off (True/False). :bin_labels: | None or list[str] or '#', optional | Plots labels at the bin center hues. | - None: don't plot. | - list[str]: list with str for each bin. | (len(:bin_labels:) = :nhbins:) | - '#': plots number. :plot_edge_lines: | True or False, optional | Plot grey bin edge lines with '--'. :plot_center_lines: | False or True, optional | Plot colored lines at 'center' of hue bin. :plot_bin_colors: | True, optional | Colorize hue bins. :plot_10_20_circles: | False, optional | If True and :axtype: == 'cart': Plot white circles at | 80%, 90%, 100%, 110% and 120% of :scalef: :axtype: | 'polar' or 'cart', optional | Make polar or Cartesian plot. :ax: | None or 'new' or 'same', optional | - None or 'new' creates new plot | - 'same': continue plot on same axes. | - axes handle: plot on specified axes. :force_CVG_layout: | False or True, optional | True: Force plot of basis of CVG on first encounter. Returns: :returns: | gcf(), gca(), list with rgb colors for hue bins (for use in other plotting fcns) """ # Setup hbincenters and hsv_hues: if isinstance(hbins, float) | isinstance(hbins, int): nhbins = hbins dhbins = 360 / (nhbins) # hue bin width hbincenters = np.arange(start_hue + dhbins / 2, 360, dhbins) hbincenters = np.sort(hbincenters) else: hbincenters = hbins idx = np.argsort(hbincenters) if isinstance(bin_labels, list) | isinstance(bin_labels, np.ndarray): bin_labels = bin_labels[idx] hbincenters = hbincenters[idx] nhbins = hbincenters.shape[0] hbincenters = hbincenters * np.pi / 180 # Setup hbin labels: if bin_labels is '#': bin_labels = ['#{:1.0f}'.format(i + 1) for i in range(nhbins)] elif isinstance(bin_labels, str): bin_labels = [ bin_labels + '{:1.0f}'.format(i + 1) for i in range(nhbins) ] # initializing the figure cmap = None if (ax is None) or (ax == 'new'): fig = plt.figure() newfig = True else: fig = plt.gcf() newfig = False rect = [0.1, 0.1, 0.8, 0.8] # setting the axis limits in [left, bottom, width, height] if axtype == 'polar': # the polar axis: if newfig == True: ax = fig.add_axes(rect, polar=True, frameon=False) else: #cartesian axis: if newfig == True: ax = fig.add_axes(rect) if (newfig == True) | (force_CVG_layout == True): # Calculate hue-bin boundaries: r = np.vstack((np.zeros(hbincenters.shape), 1. * scalef * np.ones(hbincenters.shape))) theta = np.vstack((np.zeros(hbincenters.shape), hbincenters)) #t = hbincenters.copy() dU = np.roll(hbincenters.copy(), -1) dL = np.roll(hbincenters.copy(), 1) dtU = dU - hbincenters dtL = hbincenters - dL dtU[dtU < 0] = dtU[dtU < 0] + 2 * np.pi dtL[dtL < 0] = dtL[dtL < 0] + 2 * np.pi dL = hbincenters - dtL / 2 dU = hbincenters + dtU / 2 dt = (dU - dL) dM = dL + dt / 2 # Setup color for plotting hue bins: hsv_hues = hbincenters - 30 * np.pi / 180 hsv_hues = hsv_hues / hsv_hues.max() edges = np.vstack( (np.zeros(hbincenters.shape), dL)) # setup hue bin edges array if axtype == 'cart': if plot_center_lines == True: hx = r * np.cos(theta) * 1.2 hy = r * np.sin(theta) * 1.2 if bin_labels is not None: hxv = np.vstack((np.zeros(hbincenters.shape), 1.4 * scalef * np.cos(hbincenters))) hyv = np.vstack((np.zeros(hbincenters.shape), 1.4 * scalef * np.sin(hbincenters))) if plot_edge_lines == True: #hxe = np.vstack((np.zeros(hbincenters.shape),1.2*scalef*np.cos(dL))) #hye = np.vstack((np.zeros(hbincenters.shape),1.2*scalef*np.sin(dL))) hxe = np.vstack( (0.1 * scalef * np.cos(dL), 1.5 * scalef * np.cos(dL))) hye = np.vstack( (0.1 * scalef * np.sin(dL), 1.5 * scalef * np.sin(dL))) # Plot hue-bins: for i in range(nhbins): # Create color from hue angle: #c = np.abs(np.array(colorsys.hsv_to_rgb(hsv_hues[i], 0.75, 0.85))) c = np.abs(np.array(colorsys.hls_to_rgb(hsv_hues[i], 0.45, 0.5))) if i == 0: cmap = [c] else: cmap.append(c) if axtype == 'polar': if plot_edge_lines == True: ax.plot(edges[:, i], r[:, i] * 1., color='grey', marker='None', linestyle='--', linewidth=1, markersize=2) if plot_center_lines == True: if np.mod(i, 2) == 1: ax.plot(theta[:, i], r[:, i], color=c, marker=None, linestyle='--', linewidth=1) else: ax.plot(theta[:, i], r[:, i], color=c, marker=None, linestyle='--', linewidth=1, markersize=10) if plot_bin_colors == True: bar = ax.bar(dM[i], r[1, i], width=dt[i], color=c, alpha=0.25) if bin_labels is not None: ax.text(hbincenters[i], 1.3 * scalef, bin_labels[i], fontsize=10, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.45) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: axis_ = 1. * np.array( [-scalef * 1.5, scalef * 1.5, -scalef * 1.5, scalef * 1.5]) if plot_edge_lines == True: ax.plot(hxe[:, i], hye[:, i], color='grey', marker='None', linestyle='--', linewidth=1, markersize=2) if plot_center_lines == True: if np.mod(i, 2) == 1: ax.plot(hx[:, i], hy[:, i], color=c, marker=None, linestyle='--', linewidth=1) else: ax.plot(hx[:, i], hy[:, i], color=c, marker=None, linestyle='--', linewidth=1, markersize=10) if bin_labels is not None: ax.text(hxv[1, i], hyv[1, i], bin_labels[i], fontsize=10, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.45) ax.axis(axis_) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: ax.set_xlabel("a'") ax.set_ylabel("b'") ax.plot(0, 0, color='grey', marker='+', linestyle=None, markersize=6) if (axtype != 'polar') & (plot_10_20_circles == True): r = np.array([ 0.8, 0.9, 1.1, 1.2 ]) * scalef # plot circles at 80, 90, 100, 110, 120 % of scale f plotcircle(radii=r, angles=np.arange(0, 365, 5), color='w', linestyle='-', axh=ax, linewidth=0.5) plotcircle(radii=[scalef], angles=np.arange(0, 365, 5), color='k', linestyle='-', axh=ax, linewidth=1) ax.text(0, -0.75 * scalef, '-20%', fontsize=8, horizontalalignment='center', verticalalignment='center', color='w') ax.text(0, -1.25 * scalef, '+20%', fontsize=8, horizontalalignment='center', verticalalignment='center', color='w') if (axtype != 'polar') & (plot_bin_colors == True) & (_CVG_BG is not None): ax.imshow(_CVG_BG, origin='upper', extent=axis_) return fig, ax, cmap