def _compute_s_W_S(sample_size, num_groups, tri_idxs, distances, group_sizes, grouping, subjects, paired): """Compute PERMANOVA Within & Subjects Sum-of-Squares.""" # Create a matrix where objects in the same group are marked with the group # index (e.g. 0, 1, 2, etc.). objects that are not in the same group are # marked with -1. If paired == True: Do similar for test subjects: grouping_matrix = -1 * np.ones((sample_size, sample_size), dtype=int) for group_idx in range(num_groups): within_indices = _index_combinations( np.where(grouping == group_idx)[0]) grouping_matrix[within_indices] = group_idx # Extract upper triangle (in same order as distances were extracted # from full distance matrix). grouping_tri = grouping_matrix[tri_idxs] # Calculate s_WG for each group, accounting for different group sizes. s_WG = 0 for i in range(num_groups): s_WG += (distances[grouping_tri == i]**2).sum() / group_sizes[i] # for pseudo-F2: Calculate s_WG_V for each group, accounting for different group sizes. s_WG_V = 0 for i in range(num_groups): s_WG_V += (1 - group_sizes[i] / sample_size) * ( (1 / (group_sizes[i] * (group_sizes[i] - 1))) * distances[grouping_tri == i]**2).sum() if paired == True: num_subjects = sample_size // num_groups subjects_matrix = -1 * np.ones((sample_size, sample_size), dtype=int) for subject_idx in range(num_subjects): subject_indices = _index_combinations( np.where(subjects == subject_idx)[0]) subjects_matrix[subject_indices] = subject_idx # Extract upper triangle (in same order as distances were extracted # from full distance matrix). subjects_tri = subjects_matrix[tri_idxs] # Calculate s_WS for each subject, accounting for number of groups. s_WS = 0 for i in range(num_subjects): s_WS += (distances[subjects_tri == i]**2).sum() / num_groups else: s_WS = 0 return s_WG, s_WS, s_WG_V
def __init__(self, *args, argtype='xyz', vtype='xyz', _TINY=1e-15): """ Initialize 3-dimensional vector. Args: :`*args`: | x,y,z coordinates :vtype: | 'xyz', optional | if 'xyz': cartesian coordinate input | if 'tpr': spherical coordinates input (t: theta, p: phi, r: radius) :_TINY: | Set smallest value considered still different from zero. """ self._TINY = _TINY self.vtype = vtype if len(args) == 0: args = [0.0, 0.0, 0.0] args = [np.atleast_1d(args[i]) for i in range(len(args))] # make atleast_1d ndarray if vtype == 'xyz': self.x = args[0] self.y = args[1] self.z = args[2] elif vtype == 'tpr': if len(args) == 2: args.append(np.ones(args[0].shape)) self.set_tpr(*args) self.shape = self.x.shape
def spd_to_power(data, ptype='ru', cieobs=_CIEOBS): """ Calculate power of spectral data in radiometric, photometric or quantal energy units. Args: :data: | ndarray with spectral data :ptype: | 'ru' or str, optional | str: - 'ru': in radiometric units | - 'pu': in photometric units | - 'pusa': in photometric units with Km corrected | to standard air (cfr. CIE TN003-2015) | - 'qu': in quantal energy units :cieobs: | _CIEOBS or str, optional | Type of cmf set to use for photometric units. Returns: returns: | ndarray with normalized spectral data (SI units) """ # get wavelength spacing: dl = getwld(data[0]) if ptype == 'ru': #normalize to radiometric units p = np2d(np.dot(data[1:], dl * np.ones(data.shape[1]))).T elif ptype == 'pusa': # normalize in photometric units with correction of Km to standard air # Calculate correction factor for Km in standard air: na = _BB['na'] # n for standard air c = _BB['c'] # m/s light speed lambdad = c / (na * 54 * 1e13) / (1e-9 ) # 555 nm lambda in standard air Km_correction_factor = 1 / ( 1 - (1 - 0.9998567) * (lambdad - 555)) # correction factor for Km in standard air # Get Vlambda and Km (for E): Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2) Km *= Km_correction_factor p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T elif ptype == 'pu': # normalize in photometric units # Get Vlambda and Km (for E): Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2) p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T elif ptype == 'qu': # normalize to quantual units # Get Quantal conversion factor: fQ = ((1e-9) / (_BB['h'] * _BB['c'])) p = np2d(fQ * np.dot(data[1:], dl * data[0])).T return p
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 _hue_bin_data_to_ellipsefit(hue_bin_data): # use get chroma-normalized jabtn_hj: jabt = hue_bin_data['jabtn_hj'] ecc = np.ones((1, jabt.shape[1])) * np.nan theta = np.ones((1, jabt.shape[1])) * np.nan v = np.ones((jabt.shape[1], 5)) * np.nan for i in range(jabt.shape[1]): try: v[i, :] = math.fit_ellipse(jabt[:, i, 1:]) a, b = v[i, 0], v[i, 1] # major and minor ellipse axes ecc[0, i] = a / b theta[0, i] = np.rad2deg(v[i, 4]) # orientation angle if theta[0, i] > 180: theta[0, i] = theta[0, i] - 180 except: v[i, :] = np.nan * np.ones((1, 5)) ecc[0, i] = np.nan theta[0, i] = np.nan # orientation angle return {'v': v, 'a/b': ecc, 'thetad': theta}
def get_discrimination_ellipse(Yxy = np.array([[100,1/3,1/3]]), etype = 'fmc2', nsteps = 10, k_neighbours = 3, average_cik = True, Y = None): """ Get discrimination ellipse(s) in v-format (R,r, xc, yc, theta) for Yxy using an interpolation of the MacAdam ellipses or using FMC-1 or FMC-2. Args: :Yxy: | 2D ndarray with [Y,]x,y coordinate centers. | If Yxy.shape[-1]==2: Y is added using the value from the Y-input argument. :etype: | 'fmc2', optional | Type color discrimination ellipse estimation to use. | options: 'macadam', 'fmc1', 'fmc2' | - 'macadam': interpolate covariance matrices of closest MacAdam ellipses (see: get_macadam_ellipse?). | - 'fmc1': use FMC-1 from ref 2 (see get_fmc_discrimination_ellipse?). | - 'fmc2': use FMC-1 from ref 3 (see get_fmc_discrimination_ellipse?). :nsteps: | 10, optional | Set multiplication factor for ellipses | (nsteps=1 corresponds to approximately 1 MacAdam step, | for FMC-2, Y also has to be 10.69, see note below). :k_neighbours: | 3, optional | Only for option 'macadam'. | Number of nearest ellipses to use to calculate ellipse at xy :average_cik: | True, optional | Only for option 'macadam'. | If True: take distance weighted average of inverse | 'covariance ellipse' elements cik. | If False: average major & minor axis lengths and | ellipse orientation angles directly. :Y: | None, optional | Only for option 'fmc2'(see note below). | If not None: Y = 10.69 and overrides values in Yxy. Note: 1. FMC-2 is almost identical to FMC-1 is Y = 10.69!; see [3] References: 1. MacAdam DL. Visual Sensitivities to Color Differences in Daylight*. J Opt Soc Am. 1942;32(5):247-274. 2. Chickering, K.D. (1967), Optimization of the MacAdam-Modified 1965 Friele Color-Difference Formula, 57(4):537-541 3. Chickering, K.D. (1971), FMC Color-Difference Formulas: Clarification Concerning Usage, 61(1):118-122 """ if Yxy.shape[-1] == 2: Yxy = np.hstack((100*np.ones((Yxy.shape[0],1)),Yxy)) if Y is not None: Yxy[...,0] = Y if etype == 'macadam': return get_macadam_ellipse(xy = Yxy[...,1:], k_neighbours = k_neighbours, nsteps = nsteps, average_cik = average_cik) else: return get_fmc_discrimination_ellipse(Yxy = Yxy, etype = etype, nsteps = nsteps, Y = Y)
def parse_x1x2_parameters(x, target_shape, catmode, expand_2d_to_3d=None, default=[1.0, 1.0]): """ Parse input parameters x and make them the target_shape for easy calculation. | Input in main function can now be a single value valid for all xyzw or an array with a different value for each xyzw. Args: :x: | list[float, float] or ndarray :target_shape: | tuple with shape information :catmode: | '1>0>2, optional | -'1>0>2': Two-step CAT | from illuminant 1 to baseline illuminant 0 to illuminant 2. | -'1>0': One-step CAT | from illuminant 1 to baseline illuminant 0. | -'0>2': One-step CAT | from baseline illuminant 0 to illuminant 2. :expand_2d_to_3d: | None, optional | [will be removed in future, serves no purpose] | Expand :x: from 2 to 3 dimensions. :default: | [1.0,1.0], optional | Default values for :x: Returns: :returns: | (ndarray, ndarray) for x10 and x20 """ if x is None: x10 = np.ones(target_shape) * default[0] if (catmode == '1>0>2') | (catmode == '1>2'): x20 = np.ones(target_shape) * default[1] else: x20 = np.zeros(target_shape) x20.fill(np.nan) else: x = np2d(x) if (catmode == '1>0>2') | (catmode == '1>2'): if x.shape[-1] == 2: x10 = np.ones(target_shape) * x[..., 0] x20 = np.ones(target_shape) * x[..., 1] else: x10 = np.ones(target_shape) * x x20 = x10.copy() elif catmode == '1>0': x10 = np.ones(target_shape) * x[..., 0] x20 = np.zeros(target_shape) x20.fill(np.nan) return x10, x20
def plot_rgb_color_patches(rgb, patch_shape=(100, 100), patch_layout=None, ax=None, show=True): """ Create (and plot) an image with patches with specified rgb values. Args: :rgb: | ndarray with rgb values for each of the patches :patch_shape: | (100,100), optional | shape of each of the patches in the image :patch_layout: | None, optional | If None: layout is calculated automatically to give a 'good' aspect ratio :ax: | None, optional | Axes to plot the image in. If None: a new axes is created. :show: | True, optional | If True: plot image in axes and return axes handle; else: return ndarray with image. Return: :ax: or :imagae: | Axes is returned if show == True, else: ndarray with rgb image is returned. """ if ax is None: fig, ax = plt.subplots(1, 1) if patch_layout is None: patch_layout = get_subplot_layout(rgb.shape[0]) image = np.zeros( np.hstack((np.array(patch_shape) * np.array(patch_layout), 3))) for i in range(rgb.shape[0]): r, c = np.unravel_index(i, patch_layout) R = int(r * patch_shape[0]) C = int(c * patch_shape[1]) image[R:R + patch_shape[0], C:C + patch_shape[1], :] = np.ones(np.hstack( (patch_shape, 3))) * rgb[i, None, :] if show == False: return image else: ax.imshow(image.astype('uint8')) ax.axis('off') return ax
def get_gij_fmc(Yxy, etype = 'fmc2', ellipsoid = True, Y = None, cspace = 'Yxy'): """ Get gij matrices describing the discrimination ellipses/ellipsoids for Yxy or xyz using FMC-1 or FMC-2. Args: :Yxy: | 2D ndarray with [Y,]x,y coordinate centers. | If Yxy.shape[-1]==2: Y is added using the value from the Y-input argument. :etype: | 'fmc2', optional | Type of FMC color discrimination equations to use (see references below). | options: 'fmc1', fmc2' :Y: | None, optional | Only affects FMC-2 (see note below). | If not None: Y = 10.69 and overrides values in Yxy. :ellipsoid: | True, optional | If True: return ellipsoids, else return ellipses (only if cspace == 'Yxy')! :cspace: | 'Yxy', optional | Return coefficients for Yxy-ellipses/ellipsoids ('Yxy') or XYZ ellipsoids ('xyz') Note: 1. FMC-2 is almost identical to FMC-1 is Y = 10.69!; see [2] References: 1. Chickering, K.D. (1967), Optimization of the MacAdam-Modified 1965 Friele Color-Difference Formula, 57(4), p.537-541 2. Chickering, K.D. (1971), FMC Color-Difference Formulas: Clarification Concerning Usage, 61(1), p.118-122 """ if Yxy.shape[-1] == 2: Yxy = np.hstack((100*np.ones((Yxy.shape[0],1)),Yxy)) if Y is not None: Yxy[...,0] = Y xyz = Yxy_to_xyz(Yxy) if etype == 'fmc2': gij = _get_gij_fmc_2(xyz, cspace = cspace) else: gij = _get_gij_fmc_1(xyz, cspace = cspace) if ellipsoid == True: return gij else: if cspace.lower()=='xyz': return gij else: return gij[:,1:,1:]
def stress_F_test(stressA, stressB, N, alpha = 0.05): """ Perform F-test on significance of difference between STRESS A and STRESS B. Args: :stressA, stressB: | ndarray with stress(es) values for A and B :N: | int or ndarray with number of samples used to determine stress values. :alpha: | 0.05, optional | significance level Returns: :Fstats: | Dictionary with keys: | - 'p': p-values | - 'F': F-values | - 'Fc': critcal values | - 'H': string reporting on significance of A compared to B. """ N = N*np.ones(stressA.shape[0]) Fvs = np.nan*np.ones_like(stressA) ps = Fvs.copy() Fcs = Fvs.copy() H = [] i = 0 for stA, stB in zip(stressA,stressB): Ni = N[i] Fvs[i] = stA**2/stB**2 ps[i] = stats.f.sf(Fvs[i], Ni-1, Ni-1) Fcs[i] = stats.f.ppf(q = alpha/2, dfn = Ni - 1, dfd = Ni-1) if Fvs[i] < Fcs[i]: H_ = "A significantly better than B" elif Fvs[i] > 1/Fcs[i]: H_ = "A significantly poorer than B" elif (Fcs[i] <= Fvs[i]) & (Fvs[i] < 1): H_ = "A insignificantly better than B" elif (1 < Fvs[i]) & (Fvs[i] <= 1/Fcs[i]): H_ = "A insignificanty poorer than B" elif (Fvs[i] == 1): H_ = "A equals B" H.append(H_) i+=1 Fstats = {'p': ps, 'F': Fvs, 'Fc': Fcs, 'H': H} return Fstats
def getwld(wl): """ Get wavelength spacing. Args: :wl: | ndarray with wavelengths Returns: :returns: | - float: for equal wavelength spacings | - ndarray (.shape = (n,)): for unequal wavelength spacings """ d = np.diff(wl) dl = (np.hstack((d[0], d[0:-1] / 2.0, d[-1])) + np.hstack( (0.0, d[1:] / 2.0, 0.0))) if np.array_equal(dl, dl.mean() * np.ones(dl.shape)): dl = dl[0] return dl
def lab_to_xyz(lab, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert CIE 1976 L*a*b* (CIELAB) color coordinates to XYZ tristimulus values. Args: :lab: | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates :xyzw: | None or ndarray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating xyzw. Returns: :xyz: | ndarray with tristimulus values """ lab = np2d(lab) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # make xyzw same shape as data: xyzw = xyzw * np.ones(lab.shape) # get L*, a*, b* and Xw, Yw, Zw: fXYZ = np.empty(lab.shape) fXYZ[..., 1] = (lab[..., 0] + 16.0) / 116.0 fXYZ[..., 0] = lab[..., 1] / 500.0 + fXYZ[..., 1] fXYZ[..., 2] = fXYZ[..., 1] - lab[..., 2] / 200.0 # apply 3rd power: xyz = (fXYZ**3.0) * xyzw # Now calculate T where T/Tn is below the knee point: pqr = fXYZ <= (24 / 116) #(24/116)**3**(1/3) xyz[pqr] = np.squeeze(xyzw[pqr] * ((fXYZ[pqr] - 16.0 / 116.0) / (841 / 108))) return xyz
def dtlz_range_(fname, M): """ Returns the decision range of a DTLZ function | The range is simply [0,1] for all variables. What varies is the number | of decision variables in each problem. The equation for that is | n = (M-1) + k | wherein k = 5 for DTLZ1, 10 for DTLZ2-6, and 20 for DTLZ7. Args: :fname: | a string with the name of the function ('dtlz1', 'dtlz2' etc.) :M: | a scalar with the number of objectives Returns: :lim: | a n x 2 matrix wherein the first column is the lower limit |(0), and the second column, the upper limit of search (1) """ #Checks if the string has or not the prefix 'dtlz', or if the number later #is greater than 7: fname = fname.lower() if (len(fname) < 5) or (fname[:4] != 'dtlz') or (float(fname[4]) > 7) : raise Exception('Sorry, the function {:s} is not implemented.'.format(fname)) # If the name is o.k., defines the value of k if fname == 'dtlz1': k = 5 elif fname == 'dtlz7': k = 20 else: #any other function k = 10; n = (M-1) + k #number of decision variables lim = np.hstack((np.zeros((n,1)), np.ones((n,1)))) return lim
def plot_chromaticity_diagram_colors(diagram_samples = 256, diagram_opacity = 1.0, diagram_lightness = 0.25,\ cieobs = _CIEOBS, cspace = 'Yxy', cspace_pars = {},\ show = True, axh = None,\ show_grid = False, label_fontname = 'Times New Roman', label_fontsize = 12,\ **kwargs): """ Plot the chromaticity diagram colors. Args: :diagram_samples: | 256, optional | Sampling resolution of color space. :diagram_opacity: | 1.0, optional | Sets opacity of chromaticity diagram :diagram_lightness: | 0.25, optional | Sets lightness of chromaticity diagram :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: :cspace_pars: | {} or dict, optional | Dict with parameters required by color space specified in :cspace: | (for use with luxpy.colortf()) :show_grid: | False, optional | Show grid (True) or not (False) :label_fontname: | 'Times New Roman', optional | Sets font type of axis labels. :label_fontsize: | 12, optional | Sets font size of axis labels. :kwargs: | additional keyword arguments for use with matplotlib.pyplot. Returns: """ if isinstance(cieobs, str): SL = _CMF[cieobs]['bar'][1:4].T else: SL = cieobs[1:4].T SL = 100.0 * SL / (SL[:, 1, None] + _EPS) SL = SL[SL.sum(axis=1) > 0, :] # avoid div by zero in xyz-to-Yxy conversion SL = colortf(SL, tf=cspace, tfa0=cspace_pars) plambdamax = SL[:, 1].argmax() SL = np.vstack( (SL[:(plambdamax + 1), :], SL[0]) ) # add lowest wavelength data and go to max of gamut in x (there is a reversal for some cmf set wavelengths >~700 nm!) Y, x, y = asplit(SL) SL = np.vstack((x, y)).T # create grid for conversion to srgb offset = _EPS min_x = min(offset, x.min()) max_x = max(1, x.max()) min_y = min(offset, y.min()) max_y = max(1, y.max()) ii, jj = np.meshgrid( np.linspace(min_x - offset, max_x + offset, int(diagram_samples)), np.linspace(max_y + offset, min_y - offset, int(diagram_samples))) ij = np.dstack((ii, jj)) ij[ij == 0] = offset ij2D = ij.reshape((diagram_samples**2, 2)) ij2D = np.hstack((diagram_lightness * 100 * np.ones( (ij2D.shape[0], 1)), ij2D)) xyz = colortf(ij2D, tf=cspace + '>xyz', tfa0=cspace_pars) xyz[xyz < 0] = 0 xyz[np.isinf(xyz.sum(axis=1)), :] = np.nan xyz[np.isnan(xyz.sum(axis=1)), :] = offset srgb = xyz_to_srgb(xyz) srgb = srgb / srgb.max() srgb = srgb.reshape((diagram_samples, diagram_samples, 3)) if show == True: if axh is None: fig = plt.figure() axh = fig.add_subplot(111) polygon = Polygon(SL, facecolor='none', edgecolor='none') axh.add_patch(polygon) image = axh.imshow(srgb, interpolation='bilinear', extent=(min_x, max_x, min_y - 0.05, max_y), clip_path=None, alpha=diagram_opacity) image.set_clip_path(polygon) axh.plot(x, y, color='darkgray') if (cspace == 'Yxy') & (isinstance(cieobs, str)): axh.set_xlim([0, 1]) axh.set_ylim([0, 1]) elif (cspace == 'Yuv') & (isinstance(cieobs, str)): axh.set_xlim([0, 0.6]) axh.set_ylim([0, 0.6]) if (cspace is not None): xlabel = _CSPACE_AXES[cspace][1] ylabel = _CSPACE_AXES[cspace][2] if (label_fontname is not None) & (label_fontsize is not None): axh.set_xlabel(xlabel, fontname=label_fontname, fontsize=label_fontsize) axh.set_ylabel(ylabel, fontname=label_fontname, fontsize=label_fontsize) if show_grid == True: axh.grid(True) #plt.show() return axh else: return None
def plotellipse(v, cspace_in = 'Yxy', cspace_out = None, nsamples = 100, \ show = True, axh = None, \ line_color = 'darkgray', line_style = ':', line_width = 1, line_marker = '', line_markersize = 4,\ plot_center = False, center_marker = 'o', center_color = 'darkgray', center_markersize = 4,\ show_grid = False, llabel = '', label_fontname = 'Times New Roman', label_fontsize = 12,\ out = None): """ Plot ellipse(s) given in v-format [Rmax,Rmin,xc,yc,theta]. Args: :v: | (Nx5) ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] :cspace_in: | 'Yxy', optional | Color space of v. | If None: no color space assumed. Axis labels assumed ('x','y'). :cspace_out: | None, optional | Color space to plot ellipse(s) in. | If None: plot in cspace_in. :nsamples: | 100 or int, optional | Number of points (samples) in ellipse boundary :show: | True or boolean, optional | Plot ellipse(s) (True) or not (False) :axh: | None, optional | Ax-handle to plot ellipse(s) in. | If None: create new figure with axes. :line_color: | 'darkgray', optional | Color to plot ellipse(s) in. :line_style: | ':', optional | Linestyle of ellipse(s). :line_width': | 1, optional | Width of ellipse boundary line. :line_marker: | 'none', optional | Marker for ellipse boundary. :line_markersize: | 4, optional | Size of markers in ellipse boundary. :plot_center: | False, optional | Plot center of ellipse: yes (True) or no (False) :center_color: | 'darkgray', optional | Color to plot ellipse center in. :center_marker: | 'o', optional | Marker for ellipse center. :center_markersize: | 4, optional | Size of marker of ellipse center. :show_grid: | False, optional | Show grid (True) or not (False) :llabel: | None,optional | Legend label for ellipse boundary. :label_fontname: | 'Times New Roman', optional | Sets font type of axis labels. :label_fontsize: | 12, optional | Sets font size of axis labels. :out: | None, optional | Output of function | If None: returns None. Can be used to output axh of newly created | figure axes or to return Yxys an ndarray with coordinates of | ellipse boundaries in cspace_out (shape = (nsamples,3,N)) Returns: :returns: None, or whatever set by :out:. """ Yxys = np.zeros((nsamples, 3, v.shape[0])) ellipse_vs = np.zeros((v.shape[0], 5)) for i, vi in enumerate(v): # Set sample density of ellipse boundary: t = np.linspace(0, 2 * np.pi, int(nsamples)) a = vi[0] # major axis b = vi[1] # minor axis xyc = vi[2:4, None] # center theta = vi[-1] # rotation angle # define rotation matrix: R = np.hstack((np.vstack((np.cos(theta), np.sin(theta))), np.vstack((-np.sin(theta), np.cos(theta))))) # Calculate ellipses: Yxyc = np.vstack((1, xyc)).T Yxy = np.vstack( (np.ones((1, nsamples)), xyc + np.dot(R, np.vstack((a * np.cos(t), b * np.sin(t)))))).T Yxys[:, :, i] = Yxy # Convert to requested color space: if (cspace_out is not None) & (cspace_in is not None): Yxy = colortf(Yxy, cspace_in + '>' + cspace_out) Yxyc = colortf(Yxyc, cspace_in + '>' + cspace_out) Yxys[:, :, i] = Yxy # get ellipse parameters in requested color space: ellipse_vs[i, :] = math.fit_ellipse(Yxy[:, 1:]) #de = np.sqrt((Yxy[:,1]-Yxyc[:,1])**2 + (Yxy[:,2]-Yxyc[:,2])**2) #ellipse_vs[i,:] = np.hstack((de.max(),de.min(),Yxyc[:,1],Yxyc[:,2],np.nan)) # nan because orientation is xy, but request is some other color space. Change later to actual angle when fitellipse() has been implemented # plot ellipses: if show == True: if (axh is None) & (i == 0): fig = plt.figure() axh = fig.add_subplot(111) if (cspace_in is None): xlabel = 'x' ylabel = 'y' else: xlabel = _CSPACE_AXES[cspace_in][1] ylabel = _CSPACE_AXES[cspace_in][2] if (cspace_out is not None): xlabel = _CSPACE_AXES[cspace_out][1] ylabel = _CSPACE_AXES[cspace_out][2] if plot_center == True: axh.plot(Yxyc[:, 1], Yxyc[:, 2], color=center_color, linestyle='none', marker=center_marker, markersize=center_markersize) if llabel is None: axh.plot(Yxy[:, 1], Yxy[:, 2], color=line_color, linestyle=line_style, linewidth=line_width, marker=line_marker, markersize=line_markersize) else: axh.plot(Yxy[:, 1], Yxy[:, 2], color=line_color, linestyle=line_style, linewidth=line_width, marker=line_marker, markersize=line_markersize, label=llabel) axh.set_xlabel(xlabel, fontname=label_fontname, fontsize=label_fontsize) axh.set_ylabel(ylabel, fontname=label_fontname, fontsize=label_fontsize) if show_grid == True: plt.grid(True) #plt.show() Yxys = np.transpose(Yxys, axes=(0, 2, 1)) if out is not None: return eval(out) else: return None
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) xD, yD = daylightlocus(ccts, cieobs=cieobs, 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 apply(data, n_step = 2, catmode = None, cattype = 'vonkries', xyzw1 = None, xyzw2 = None, xyzw0 = None,\ D = None, mcat = [_MCAT_DEFAULT], normxyz0 = None, outtype = 'xyz', La = None, F = None, Dtype = None): """ Calculate corresponding colors by applying a von Kries chromatic adaptation transform (CAT), i.e. independent rescaling of 'sensor sensitivity' to data to adapt from current adaptation conditions (1) to the new conditions (2). Args: :data: | ndarray of tristimulus values (can be NxMx3) :n_step: | 2, optional | Number of step in CAT (1: 1-step, 2: 2-step) :catmode: | None, optional | - None: use :n_step: to set mode: 1 = '1>2', 2:'1>0>2' | -'1>0>2': Two-step CAT | from illuminant 1 to baseline illuminant 0 to illuminant 2. | -'1>2': One-step CAT | from illuminant 1 to illuminant 2. | -'1>0': One-step CAT | from illuminant 1 to baseline illuminant 0. | -'0>2': One-step CAT | from baseline illuminant 0 to illuminant 2. :cattype: | 'vonkries' (others: 'rlab', see Farchild 1990), optional :xyzw1: | None, depending on :catmode: optional (can be Mx3) :xyzw2: | None, depending on :catmode: optional (can be Mx3) :xyzw0: | None, depending on :catmode: optional (can be Mx3) :D: | None, optional | Degrees of adaptation. Defaults to [1.0, 1.0]. :La: | None, optional | Adapting luminances. | If None: xyz values are absolute or relative. | If not None: xyz are relative. :F: | None, optional | Surround parameter(s) for CAT02/CAT16 calculations | (:Dtype: == 'cat02' or 'cat16') | Defaults to [1.0, 1.0]. :Dtype: | None, optional | Type of degree of adaptation function from literature | See luxpy.cat.get_degree_of_adaptation() :mcat: | [_MCAT_DEFAULT], optional | List[str] or List[ndarray] of sensor space matrices for each | condition pair. If len(:mcat:) == 1, the same matrix is used. :normxyz0: | None, optional | Set of xyz tristimulus values to normalize the sensor space matrix to. :outtype: | 'xyz' or 'lms', optional | - 'xyz': return corresponding tristimulus values | - 'lms': return corresponding sensor space excitation values | (e.g. for further calculations) Returns: :returns: | ndarray with corresponding colors Reference: 1. `Smet, K. A. G., & Ma, S. (2020). Some concerns regarding the CAT16 chromatic adaptation transform. Color Research & Application, 45(1), 172–177. <https://doi.org/10.1002/col.22457>`_ """ if (xyzw1 is None) & (xyzw2 is None): return data # do nothing else: # Set catmode: if catmode is None: if n_step == 2: catmode = '1>0>2' elif n_step == 1: catmode = '1>2' else: raise Exception( 'cat.apply(n_step = {:1.0f}, catmode = None): Unknown requested n-step CAT mode !' .format(n_step)) # Make data 2d: data = np2d(data) data_original_shape = data.shape if data.ndim < 3: target_shape = np.hstack((1, data.shape)) data = data * np.ones(target_shape) else: target_shape = data.shape target_shape = data.shape # initialize xyzw0: if (xyzw0 is None): # set to iLL.E xyzw0 = np2d([100.0, 100.0, 100.0]) xyzw0 = np.ones(target_shape) * xyzw0 La0 = xyzw0[..., 1, None] # Determine cat-type (1-step or 2-step) + make input same shape as data for block calculations: expansion_axis = np.abs(1 * (len(data_original_shape) == 2) - 1) if ((xyzw1 is not None) & (xyzw2 is not None)): xyzw1 = xyzw1 * np.ones(target_shape) xyzw2 = xyzw2 * np.ones(target_shape) default_La12 = [xyzw1[..., 1, None], xyzw2[..., 1, None]] elif (xyzw2 is None) & (xyzw1 is not None): # apply one-step CAT: 1-->0 catmode = '1>0' #override catmode input xyzw1 = xyzw1 * np.ones(target_shape) default_La12 = [xyzw1[..., 1, None], La0] elif (xyzw1 is None) & (xyzw2 is not None): raise Exception( "von_kries(): cat transformation '0>2' not supported, use '1>0' !" ) # Get or set La (La == None: xyz are absolute or relative, La != None: xyz are relative): target_shape_1 = tuple(np.hstack((target_shape[:-1], 1))) La1, La2 = parse_x1x2_parameters(La, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis, default=default_La12) # Set degrees of adaptation, D10, D20: (note D20 is degree of adaptation for 2-->0!!) D10, D20 = parse_x1x2_parameters(D, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis) # Set F surround in case of Dtype == 'cat02': F1, F2 = parse_x1x2_parameters(F, target_shape=target_shape_1, catmode=catmode, expand_2d_to_3d=expansion_axis) # Make xyz relative to go to relative xyz0: if La is None: data = 100 * data / La1 xyzw1 = 100 * xyzw1 / La1 xyzw0 = 100 * xyzw0 / La0 if (catmode == '1>0>2') | (catmode == '1>2'): xyzw2 = 100 * xyzw2 / La2 # transform data (xyz) to sensor space (lms) and perform cat: xyzc = np.zeros(data.shape) xyzc.fill(np.nan) mcat = np.array(mcat) if (mcat.shape[0] != data.shape[1]) & (mcat.shape[0] == 1): mcat = np.repeat(mcat, data.shape[1], axis=0) elif (mcat.shape[0] != data.shape[1]) & (mcat.shape[0] > 1): raise Exception( 'von_kries(): mcat.shape[0] > 1 and does not match data.shape[0]!' ) for i in range(xyzc.shape[1]): # get cat sensor matrix: if mcat[i].dtype == np.float64: mcati = mcat[i] else: mcati = _MCATS[mcat[i]] # normalize sensor matrix: if normxyz0 is not None: mcati = math.normalize_3x3_matrix(mcati, xyz0=normxyz0) # convert from xyz to lms: lms = np.dot(mcati, data[:, i].T).T lmsw0 = np.dot(mcati, xyzw0[:, i].T).T if (catmode == '1>0>2') | (catmode == '1>0'): lmsw1 = np.dot(mcati, xyzw1[:, i].T).T Dpar1 = dict(D=D10[:, i], F=F1[:, i], La=La1[:, i], La0=La0[:, i], order='1>0') D10[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar1) #get degree of adaptation depending on Dtype lmsw2 = None # in case of '1>0' if (catmode == '1>0>2'): lmsw2 = np.dot(mcati, xyzw2[:, i].T).T Dpar2 = dict(D=D20[:, i], F=F2[:, i], La=La2[:, i], La0=La0[:, i], order='0>2') D20[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar2) #get degree of adaptation depending on Dtype if (catmode == '1>2'): lmsw1 = np.dot(mcati, xyzw1[:, i].T).T lmsw2 = np.dot(mcati, xyzw2[:, i].T).T Dpar12 = dict(D=D10[:, i], F=F1[:, i], La=La1[:, i], La2=La2[:, i], order='1>2') D10[:, i] = get_degree_of_adaptation( Dtype=Dtype, **Dpar12) #get degree of adaptation depending on Dtype # Determine transfer function Dt: Dt = get_transfer_function(cattype=cattype, catmode=catmode, lmsw1=lmsw1, lmsw2=lmsw2, lmsw0=lmsw0, D10=D10[:, i], D20=D20[:, i], La1=La1[:, i], La2=La2[:, i]) # Perform cat: lms = np.dot(np.diagflat(Dt[0]), lms.T).T # Make xyz, lms 'absolute' again: if (catmode == '1>0>2'): lms = (La2[:, i] / La1[:, i]) * lms elif (catmode == '1>0'): lms = (La0[:, i] / La1[:, i]) * lms elif (catmode == '1>2'): lms = (La2[:, i] / La1[:, i]) * lms # transform back from sensor space to xyz (or not): if outtype == 'xyz': xyzci = np.dot(np.linalg.inv(mcati), lms.T).T xyzci[np.where(xyzci < 0)] = _EPS xyzc[:, i] = xyzci else: xyzc[:, i] = lms # return data to original shape: if len(data_original_shape) == 2: xyzc = xyzc[0] return xyzc
def 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] = sp.interpolate.interp1d(wl_shifted[0], LMSa[0], kind=kind, bounds_error=False, fill_value="extrapolate")(_WL) LMSa_shft[1] = sp.interpolate.interp1d(wl_shifted[1], LMSa[1], kind=kind, bounds_error=False, fill_value="extrapolate")(_WL) LMSa_shft[2] = sp.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 _tm30_process_spd(spd, cri_type = 'ies-tm30',**kwargs): """ Calculate all required parameters for plotting from spd using cri.spd_to_cri() Args: :spd: | ndarray or dict | If ndarray: single spectral power distribution. | If dict: dictionary with pre-computed parameters. | 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. :kwargs: | Additional optional keyword arguments, | the same as in cri.spd_to_cri() Returns: :data: | dictionary with required parameters for plotting functions. """ out = 'Rf,Rg,cct,duv,Sr,cri_type,xyzri,xyzrw,binnrs,Rfi,Rfhi,Rcshi,Rhshi,jabt_binned,jabr_binned,nhbins,start_hue,normalize_gamut,normalized_chroma_ref' if not isinstance(spd,dict): tpl = spd_to_cri(spd, cri_type = cri_type, out = out, **kwargs) data = {'spd':spd} for i,key in enumerate(out.split(',')): if key == 'normalized_chroma_ref': key = 'scalef' # rename if key == 'binnrs': key = 'hbinnrs' # rename data[key] = tpl[i] # Normalize chroma to scalef and fit ellipse to gamut: scalef = data['scalef'] jabt = data['jabt_binned'].copy() jabr = data['jabr_binned'].copy() Cr = (jabr[...,1]**2 + jabr[...,2]**2)**0.5 Ct = ((jabt[...,1]**2 + jabt[...,2]**2)**0.5)/Cr*scalef ht = math.positive_arctan(jabt[...,1],jabt[...,2], htype = 'rad') hr = math.positive_arctan(jabr[...,1],jabr[...,2], htype = 'rad') jabt[...,1] = Ct*np.cos(ht) jabt[...,2] = Ct*np.sin(ht) jabr[...,1] = scalef*np.cos(hr) jabr[...,2] = scalef*np.sin(hr) ecc = np.ones((1,jabt.shape[1]))*np.nan theta = np.ones((1,jabt.shape[1]))*np.nan v = np.ones((jabt.shape[1],5))*np.nan data['hue_bin_data'] = {'jabt_hj_closed':data['jabt_binned'],'jabr_hj_closed':data['jabr_binned'], 'jabtn_hj_closed':jabt,'jabrn_hj_closed':jabr} for i in range(jabt.shape[1]): try: v[i,:] = math.fit_ellipse(jabt[:,i,1:]) a,b = v[i,0], v[i,1] # major and minor ellipse axes ecc[0,i] = a/b theta[0,i] = np.rad2deg(v[i,4]) # orientation angle if theta[0,i]>180: theta[0,i] -= 180 except: v[i,:] = np.nan*np.ones((1,5)) ecc[0,i] = np.nan theta[0,i] = np.nan # orientation angle data['gamut_ellipse_fit'] = {'v':v, 'a/b':ecc,'thetad': theta} else: data = spd return data
_CMF_M_1931_2_JUDDVOS1978_Vos78 = np.array([ # definition of 3x3 matrices to convert from Judd-Vos xyz to lms (from Vos 1978) [0.15514,0.54312,-0.0370161], [-0.15514,0.45684, 0.0296946], [0.0,0.00801,0.0073215] ]) _CMF_N_1931_2_JUDDVOS1978 = _CMF_N_1931_2.copy() # not defined, so take the one for CIE 1931 2° #------------------------------------------------------------------------------ # 2015 CMFs based on 2006 cone fundamentals (2°): _CMF_M_2006_2 = np.linalg.inv(np.array([[1.94735469, -1.41445123, 0.36476327], # from CIE15:2018 [0.68990272, 0.34832189, 0], [0, 0, 1.93485343]])) _CMF_N_2006_2 = np.ones((3,3))*np.nan # N : definition of 3x3 matrices to convert from rgb to xyz (not defined, so NaNs) #------------------------------------------------------------------------------ # 2015 CMFs based on 2006 cone fundamentals (2°): _CMF_M_2006_10 = np.linalg.inv(np.array([[1.93986443, -1.34664359, 0.43044935], # from CIE15:2018 [0.69283932, 0.34967567, 0], [0, 0, 2.14687945]])) _CMF_N_2006_10 = np.array([ # N : definition of 3x3 matrices to convert from rgb to xyz (calculated from published xyz-to-lms and rgb-to-lms matrices, CIE TC1-36) [3.161850764,-0.698441888,-0.572538921], [-0.522270801,1.29543215,0.046547295], [0.005536941,-0.01373374,0.469326311] ])
def box_m(*X, ni = None, verbosity = 0, robust = False, robust_alpha = 0.01): """ Perform Box's M test (p>=2) to check equality of covariance matrices or Bartlett's test (p==1) for equality of variances. Args: :X: | A number (k groups) or list of 2d-ndarrays (rows: samples, cols: variables) with data. | or a number of 2d-ndarrays with covariance matrices (supply ni!) :ni: | None, optional | If None: X contains data, else, X contains covariance matrices. :verbosity: | 0, optional | If 1: print results. :robust: | False, optional | If True: remove outliers beyond the confidence ellipsoid before calculating | the covariances. :robust_alpha: | 0.01, optional | Significance level of confidence ellipsoid marking the boundary for outliers. Returns: :statistic: | F or chi2 value (see len(dfs)) :pval: | p-value :df: | degrees of freedom. | if len(dfs) == 2: F-test was used. | if len(dfs) == 1: chi2 approx. was used. Notes: 1. If p==1: Reduces to Bartlett's test for equal variances. 2. If (ni>20).all() & (p<6) & (k<6): then a more appropriate chi2 test is used in a some cases. """ k = len(X) # groups p = np.atleast_2d(X[0]).shape[1] # variables if p == 1: # for p == 1: only variance! det = lambda x: np.array(x) else: det = lambda x: np.linalg.det(x) if ni is None: # samples in each group # remove outliers before calculation of box M: if robust == True: X = [remove_outliers(Xi, alpha = robust_alpha) for Xi in X] ni = np.array([Xi.shape[0] for Xi in X]) Si = np.array([np.cov(Xi.T) for Xi in X]) if p == 1: Si = np.atleast_2d(Si).T else: Si = np.array([Xi for Xi in X]) # input are already cov matrices! ni = np.array(ni) if ni.shape[0] == 1: ni = ni*np.ones((k,)) N = ni.sum() S = np.array([(ni[i]-1)*Si[i] for i in range(len(ni))]).sum(axis=0)/(N - k) M = (N-k)*np.log(det(S)) - ((ni-1)*np.log(det(Si))).sum() if p == 1: M = M[0] A1 = (2*p**2 + 3*p -1)/(6*(p+1)*(k-1))*((1/(ni-1)) - 1/(N - k)).sum() v1 = p*(p+1)*(k-1)/2 A2 = (p-1)*(p+2)/(6*(k-1))*((1/(ni-1)**2) - 1/(N - k)**2).sum() if (A2 - A1**2) > 0: v2 = (v1 + 2)/(A2 - A1**2) b = v1/(1 - A1 -(v1/v2)) Fv1v2 = M/b statistic = Fv1v2 pval = 1.0 - sp.stats.f.cdf(Fv1v2,v1,v2) dfs = [v1,v2] if verbosity == 1: print('M = {:1.4f}, F = {:1.4f}, df1 = {:1.1f}, df2 = {:1.1f}, p = {:1.4f}'.format(M,Fv1v2,v1,v2,pval)) else: v2 = (v1 + 2)/(A1**2 - A2) b = v2/(1 - A1 + (2/v2)) Fv1v2 = v2*M/(v1*(b - M)) statistic = Fv1v2 pval = 1.0 - sp.stats.f.cdf(Fv1v2,v1,v2) dfs = [v1,v2] if (ni>20).all() & (p<6) & (k<6): #use Chi2v1 chi2v1 = M*(1-A1) statistic = chi2v1 pval = 1.0 - sp.stats.chi2.cdf(chi2v1,v1) dfs = [v1] if verbosity == 1: print('M = {:1.4f}, chi2 = {:1.4f}, df1 = {:1.1f}, p = {:1.4f}'.format(M,chi2v1,v1,pval)) else: if verbosity == 1: print('M = {:1.4f}, F = {:1.4f}, df1 = {:1.1f}, df2 = {:1.1f}, p = {:1.4f}'.format(M,Fv1v2,v1,v2,pval)) return statistic, pval, dfs
def spd_to_mcri(SPD, D = 0.9, E = None, Yb = 20.0, out = 'Rm', wl = None): """ Calculates the MCRI or Memory Color Rendition Index, Rm Args: :SPD: | ndarray with spectral data (can be multiple SPDs, | first axis are the wavelengths) :D: | 0.9, optional | Degree of adaptation. :E: | None, optional | Illuminance in lux | (used to calculate La = (Yb/100)*(E/pi) to then calculate D | following the 'cat02' model). | If None: the degree is determined by :D: | If (:E: is not None) & (:Yb: is None): :E: is assumed to contain | the adapting field luminance La (cd/m²). :Yb: | 20.0, optional | Luminance factor of background. (used when calculating La from E) | If None, E contains La (cd/m²). :out: | 'Rm' or str, optional | Specifies requested output (e.g. 'Rm,Rmi,cct,duv') :wl: | None, optional | Wavelengths (or [start, end, spacing]) to interpolate the SPDs to. | None: default to no interpolation Returns: :returns: | float or ndarray with MCRI Rm for :out: 'Rm' | Other output is also possible by changing the :out: str value. References: 1. `K.A.G. Smet, W.R. Ryckaert, M.R. Pointer, G. Deconinck, P. Hanselaer,(2012) “A memory colour quality metric for white light sources,” Energy Build., vol. 49, no. C, pp. 216–225. <http://www.sciencedirect.com/science/article/pii/S0378778812000837>`_ """ SPD = np2d(SPD) if wl is not None: SPD = spd(data = SPD, interpolation = _S_INTERP_TYPE, kind = 'np', wl = wl) # unpack metric default values: avg, catf, cieobs, cri_specific_pars, cspace, ref_type, rg_pars, sampleset, scale = [_MCRI_DEFAULTS[x] for x in sorted(_MCRI_DEFAULTS.keys())] similarity_ai = cri_specific_pars['similarity_ai'] Mxyz2lms = cspace['Mxyz2lms'] scale_fcn = scale['fcn'] scale_factor = scale['cfactor'] sampleset = eval(sampleset) # A. calculate xyz: xyzti, xyztw = spd_to_xyz(SPD, cieobs = cieobs['xyz'], rfl = sampleset, out = 2) if 'cct' in out.split(','): cct, duv = xyz_to_cct(xyztw, cieobs = cieobs['cct'], out = 'cct,duv',mode = 'lut') # B. perform chromatic adaptation to adopted whitepoint of ipt color space, i.e. D65: if catf is not None: Dtype_cat, F, Yb_cat, catmode_cat, cattype_cat, mcat_cat, xyzw_cat = [catf[x] for x in sorted(catf.keys())] # calculate degree of adaptationn D: if E is not None: if Yb is not None: La = (Yb/100.0)*(E/np.pi) else: La = E D = cat.get_degree_of_adaptation(Dtype = Dtype_cat, F = F, La = La) else: Dtype_cat = None # direct input of D if (E is None) and (D is None): D = 1.0 # set degree of adaptation to 1 ! if D > 1.0: D = 1.0 if D < 0.6: D = 0.6 # put a limit on the lowest D # apply cat: xyzti = cat.apply(xyzti, cattype = cattype_cat, catmode = catmode_cat, xyzw1 = xyztw,xyzw0 = None, xyzw2 = xyzw_cat, D = D, mcat = [mcat_cat], Dtype = Dtype_cat) xyztw = cat.apply(xyztw, cattype = cattype_cat, catmode = catmode_cat, xyzw1 = xyztw,xyzw0 = None, xyzw2 = xyzw_cat, D = D, mcat = [mcat_cat], Dtype = Dtype_cat) # C. convert xyz to ipt and split: ipt = xyz_to_ipt(xyzti, cieobs = cieobs['xyz'], M = Mxyz2lms) #input matrix as published in Smet et al. 2012, Energy and Buildings I,P,T = asplit(ipt) # D. calculate specific (hue dependent) similarity indicators, Si: if len(xyzti.shape) == 3: ai = np.expand_dims(similarity_ai, axis = 1) else: ai = similarity_ai a1,a2,a3,a4,a5 = asplit(ai) mahalanobis_d2 = (a3*np.power((P - a1),2.0) + a4*np.power((T - a2),2.0) + 2.0*a5*(P-a1)*(T-a2)) if (len(mahalanobis_d2.shape)==3) & (mahalanobis_d2.shape[-1]==1): mahalanobis_d2 = mahalanobis_d2[:,:,0].T Si = np.exp(-0.5*mahalanobis_d2) # E. calculate general similarity indicator, Sa: Sa = avg(Si, axis = 0,keepdims = True) # F. rescale similarity indicators (Si, Sa) with a 0-1 scale to memory color rendition indices (Rmi, Rm) with a 0 - 100 scale: Rmi = scale_fcn(np.log(Si),scale_factor = scale_factor) Rm = np2d(scale_fcn(np.log(Sa),scale_factor = scale_factor)) # G. calculate Rg (polyarea of test / polyarea of memory colours): if 'Rg' in out.split(','): I = I[...,None] #broadcast_shape(I, target_shape = None,expand_2d_to_3d = 0) a1 = a1[:,None]*np.ones(I.shape)#broadcast_shape(a1, target_shape = None,expand_2d_to_3d = 0) a2 = a2[:,None]*np.ones(I.shape) #broadcast_shape(a2, target_shape = None,expand_2d_to_3d = 0) a12 = np.concatenate((a1,a2),axis=2) #broadcast_shape(np.hstack((a1,a2)), target_shape = ipt.shape,expand_2d_to_3d = 0) ipt_mc = np.concatenate((I,a12),axis=2) nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [rg_pars[x] for x in sorted(rg_pars.keys())] hue_bin_data = _get_hue_bin_data(ipt, ipt_mc, start_hue = start_hue, nhbins = nhbins, normalized_chroma_ref = normalized_chroma_ref) Rg = _hue_bin_data_to_rg(hue_bin_data) if (out != 'Rm'): return eval(out) else: return Rm
def apply_vonkries2(xyz, xyzw1, xyzw2, xyzw0=None, D=1, mcat=None, invmcat=None, in_='xyz', out_='xyz', use_Yw=False): """ Apply a 2-step von kries chromatic adaptation transform. Args: :xyz: | ndarray with sample tristimulus or cat-sensor values :xyzw1: | ndarray with white point tristimulus or cat-sensor values of illuminant 1 :xyzw2: | ndarray with white point tristimulus or cat-sensor values of illuminant 2 :xyzw0: | None, optional | ndarray with white point tristimulus or cat-sensor values of baseline illuminant 0 | None: defaults to EEW. :D: | [1,1], optional | Degree of chromatic adaptations (Ill.1-->Ill.0, Ill.2.-->Ill.0) :mcat: | None, optional | Specifies CAT sensor space. | - options: | - None defaults to luxpy.cat._MCAT_DEFAULT | - str: see see luxpy.cat._MCATS.keys() for options | (details on type, ?luxpy.cat) | - ndarray: matrix with sensor primaries :invmcat: | None,optional | Pre-calculated inverse mcat. | If None: calculate inverse of mcat. :in_: | 'xyz', optional | Input type ('xyz', 'rgb') of data in xyz, xyzw1, xyzw2 :out_: | 'xyz', optional | Output type ('xyz', 'rgb') of corresponding colors :use_Yw: | False, optional | Use CAT version with Yw factors included (but this results in | potential wrong predictions, see Smet & Ma (2020)). Returns: :xyzc: | ndarray with corresponding colors. Reference: 1. `Smet, K. A. G., & Ma, S. (2020). Some concerns regarding the CAT16 chromatic adaptation transform. Color Research & Application, 45(1), 172–177. <https://doi.org/10.1002/col.22457>`_ """ # Define cone/chromatic adaptation sensor space: if (in_ == 'xyz') | (out_ == 'xyz'): if not isinstance(mcat, np.ndarray): if (mcat is None): mcat = _MCATS[_MCAT_DEFAULT] elif isinstance(mcat, str): mcat = _MCATS[mcat] if invmcat is None: invmcat = np.linalg.inv(mcat) D = D * np.ones((2, )) # ensure there are two D's available! #-------------------------------------------- # Define default baseline illuminant: if xyzw0 is None: xyzw0 = np.array([[100., 100., 100.]]) #-------------------------------------------- # transform from xyz to cat sensor space: if in_ == 'xyz': rgb = math.dot23(mcat, xyz.T) rgbw1 = math.dot23(mcat, xyzw1.T) rgbw2 = math.dot23(mcat, xyzw2.T) rgbw0 = math.dot23(mcat, xyzw0.T) elif (in_ == 'xyz') & (use_Yw == False): rgb = xyz rgbw1 = xyzw1 rgbw2 = xyzw2 rgbw0 = xyzw0 else: raise Exception('Use of Yw requires xyz input.') #-------------------------------------------- # apply 1-step von Kries cat from 1->0: vk_w_ratio10 = rgbw0 / rgbw1 yw_ratio10 = 1.0 if use_Yw == False else xyzw1[..., 1] / xyzw0[..., 1] if rgb.ndim == 3: vk_w_ratio10 = vk_w_ratio10[..., None] if use_Yw: yw_ratio10 = yw_ratio10[..., None] rgbc = (D[0] * yw_ratio10 * vk_w_ratio10 + (1 - D[0])) * rgb #-------------------------------------------- # apply inverse 1-step von Kries cat from 2->0: vk_w_ratio20 = rgbw0 / rgbw2 yw_ratio20 = 1.0 if use_Yw == False else xyzw2[..., 1] / xyzw0[..., 1] if rgbc.ndim == 3: vk_w_ratio20 = vk_w_ratio20[..., None] if use_Yw: yw_ratio20 = yw_ratio20[..., None] rgbc = ((D[1] * yw_ratio20 * vk_w_ratio20 + (1 - D[1]))**(-1)) * rgbc #-------------------------------------------- # convert from cat16 sensor space to xyz: if out_ == 'xyz': return math.dot23(invmcat, rgbc).T else: return rgbc.T
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
def initialize_VF_hue_angles(hx = None, Cxr = _VF_MAXR, \ cri_type = _VF_CRI_DEFAULT, \ modeltype = _VF_MODEL_TYPE,\ determine_hue_angles = _DETERMINE_HUE_ANGLES): """ Initialize the hue angles that will be used to 'summarize' the VF model fitting parameters. Args: :hx: | None or ndarray, optional | None defaults to Munsell H5 hues. :Cxr: | _VF_MAXR, optional :cri_type: | _VF_CRI_DEFAULT or str or dict, optional, | Cri_type parameters for cri and VF model. :modeltype: | _VF_MODEL_TYPE or 'M5' or 'M6', optional | Determines the type of polynomial model. :determine_hue_angles: | _DETERMINE_HUE_ANGLES or True or False, optional | True: determines the 10 primary / secondary Munsell hues ('5..'). | Note that for 'M6', an additional Returns: :pcolorshift: | {'href': href, | 'Cref' : _VF_MAXR, | 'sig' : _VF_SIG, | 'labels' : list[str]} """ ########################################### # Get Munsell H5 hues: ########################################### rflM = _MUNSELL['R'] hn = _MUNSELL['H'] # all Munsell hues rH5 = np.where([ _MUNSELL['H'][:, 0][x][0] == '5' for x in range(_MUNSELL['H'][:, 0].shape[0]) ])[0] #all Munsell H5 hues hns5 = np.unique(_MUNSELL['H'][rH5]) #------------------------------------------------------------------------------ # Determine Munsell hue angles in cam02ucs: pool = False IllC = _CIE_ILLUMINANTS[ 'C'] # for determining Munsell hue angles in cam02ucs outM = VF_colorshift_model(IllC, cri_type=cri_type, sampleset=rflM, vfcolor='g', pool=pool) #------------------------------------------------------------------------------ if (determine_hue_angles == True) | (hx is None): # find samples at major Munsell hue angles: all_h5_Munsell_cam02ucs = np.ones(hns5.shape) Jabt_IllC = outM[0]['Jab']['Jabt'] for i, v in enumerate(hns5): hm = np.where(hn == v)[0] all_h5_Munsell_cam02ucs[i] = math.positive_arctan( [Jabt_IllC[hm, 0, 1].mean()], [Jabt_IllC[hm, 0, 2].mean()], htype='rad')[0] hx = all_h5_Munsell_cam02ucs #------------------------------------------------------------------------------ # Setp color shift parameters: pcolorshift = {'href': hx, 'Cref': Cxr, 'sig': _VF_SIG, 'labels': hns5} return pcolorshift
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 render_image(img = None, spd = None, rfl = None, out = 'img_hyp', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'xyz', cspace_tf = {}, CSF = None,\ interp_type = 'nd', 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 float (max = 1) rgb image. | None load a default image. :spd: | ndarray, optional | Light source spectrum for rendering | If None: use CIE illuminant F4 :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: | '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. :CSF: | None, optional | RGB camera response functions. | If None: input :xyz: contains raw rgb values. Override :cspace: | argument and perform estimation directly in raw rgb space!!! :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 :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 float 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) if isinstance(img, np.uint8): img = img / 255 elif isinstance(img, np.uint16): img = img / (2**16 - 1) # Convert to 2D format: rgb = img.reshape(img.shape[0] * img.shape[1], 3) # *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 rfl set: if rfl is None: # use IESTM30['4880'] set rfl = _CRI_RFL['ies-tm30']['4880']['5nm'] wlr = rfl[ 0] # spectral reflectance set determines wavelength range for estimation (xyz_to_rfl()) # 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 # Convert rgb_u to xyz and lab-type values under assumed refspd: if CSF is None: xyz_wr = spd_to_xyz(refspd, cieobs=cieobs, relative=True) xyz_ur = colortf(rgb_u * 255, tf='srgb>xyz') else: xyz_ur = rgb_u # for input in xyz_to_rfl (when CSF is not None: this functions assumes input is indeed rgb !!!) # 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, CSF = CSF,\ interp_type = interp_type, k_neighbours = k_neighbours, verbosity = verbosity) # Get default test spd if none supplied: if spd is None: spd = _CIE_ILLUMINANTS['F4'] if CSF is None: # 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 else: # Calculate rgb coordinates from camera sensitivity functions under spd: rgbti = rfl_to_rgb(rfl_est, spd=spd, CSF=CSF, wl=None) # Chromatic adaptation from test spd to refspd: if D is not None: white = np.ones_like(spd) white[0] = spd[0] rgbwr = rfl_to_rgb(white, spd=refspd, CSF=CSF, wl=None) rgbwt = rfl_to_rgb(white, spd=spd, CSF=CSF, wl=None) rgbti = cat.apply_vonkries2(rgbti, rgbwt, rgbwr, xyzw0=np.array([[1.0, 1.0, 1.0]]), in_='rgb', out_='rgb', D=1) # Reconstruct original locations for rendered image rgbs: img_ren = rgbti[rgb_indices] img_ren.shape = img.shape # reshape back to 3D size of original img_ren = img_ren # For output: if show_ref_img == True: rgb_ref = colortf(xyzri, tf='srgb') / 255 if ( CSF is None ) else xyzri # if CSF not None: xyzri contains rgbri !!! 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 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) 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 cam_sww16(data, dataw = None, Yb = 20.0, Lw = 400.0, Ccwb = None, relative = True, \ parameters = None, inputtype = 'xyz', direction = 'forward', \ cieobs = '2006_10'): """ A simple principled color appearance model based on a mapping of the Munsell color system. | This function implements the JOSA A (parameters = 'JOSA') published model. Args: :data: | ndarray with input tristimulus values | or spectral data | or input color appearance correlates | Can be of shape: (N [, xM], x 3), whereby: | N refers to samples and M refers to light sources. | Note that for spectral input shape is (N x (M+1) x wl) :dataw: | None or ndarray, optional | Input tristimulus values or spectral data of white point. | None defaults to the use of CIE illuminant C. :Yb: | 20.0, optional | Luminance factor of background (perfect white diffuser, Yw = 100) :Lw: | 400.0, optional | Luminance (cd/m²) of white point. :Ccwb: | None, optional | Degree of cognitive adaptation (white point balancing) | If None: use [..,..] from parameters dict. :relative: | True or False, optional | True: xyz tristimulus values are relative (Yw = 100) :parameters: | None or str or dict, optional | Dict with model parameters. | - None: defaults to luxpy.cam._CAM_SWW_2016_PARAMETERS['JOSA'] | - str: 'best-fit-JOSA' or 'best-fit-all-Munsell' | - dict: user defined model parameters | (dict should have same structure) :inputtype: | '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 -> cam_sww_2016 | -'inverse': cam_sww_2016 -> xyz :cieobs: | '2006_10', optional | CMF set to use to perform calculations where spectral data is involved (inputtype == 'spd'; dataw = None) | Other options: see luxpy._CMF['types'] Returns: :returns: | ndarray with color appearance correlates (:direction: == 'forward') | or | XYZ tristimulus values (:direction: == 'inverse') Notes: | This function implements the JOSA A (parameters = 'JOSA') published model. | With: | 1. A correction for the parameter | in Eq.4 of Fig. 11: 0.952 --> -0.952 | | 2. The delta_ac and delta_bc white-balance shifts in Eq. 5e & 5f | should be: -0.028 & 0.821 | | (cfr. Ccwb = 0.66 in: | ab_test_out = ab_test_int - Ccwb*ab_gray_adaptation_field_int)) References: 1. `Smet, K. A. G., Webster, M. A., & Whitehead, L. A. (2016). A simple principled approach for modeling and understanding uniform color metrics. Journal of the Optical Society of America A, 33(3), A319–A331. <https://doi.org/10.1364/JOSAA.33.00A319>`_ """ # get model parameters args = locals().copy() if parameters is None: parameters = _CAM_SWW16_PARAMETERS['JOSA'] if isinstance(parameters, str): parameters = _CAM_SWW16_PARAMETERS[parameters] parameters = put_args_in_db( parameters, args) #overwrite parameters with other (not-None) args input #unpack model parameters: Cc, Ccwb, Cf, Mxyz2lms, cLMS, cab_int, cab_out, calpha, cbeta, cga1, cga2, cgb1, cgb2, cl_int, clambda, lms0 = [ parameters[x] for x in sorted(parameters.keys()) ] # setup default adaptation field: if (dataw is None): dataw = _CIE_ILLUMINANTS['C'].copy() # get illuminant C xyzw = spd_to_xyz(dataw, cieobs=cieobs, relative=False) # get abs. tristimulus values if relative == False: #input is expected to be absolute dataw[1:] = Lw * dataw[ 1:] / xyzw[:, 1:2] #dataw = Lw*dataw # make absolute else: dataw = dataw # make relative (Y=100) if inputtype == 'xyz': dataw = spd_to_xyz(dataw, cieobs=cieobs, relative=relative) # precomputations: Mxyz2lms = np.dot( np.diag(cLMS), math.normalize_3x3_matrix(Mxyz2lms, np.array([[1, 1, 1]])) ) # normalize matrix for xyz-> lms conversion to ill. E weighted with cLMS invMxyz2lms = np.linalg.inv(Mxyz2lms) MAab = np.array([clambda, calpha, cbeta]) invMAab = np.linalg.inv(MAab) #initialize data and camout: data = np2d(data).copy( ) # stimulus data (can be upto NxMx3 for xyz, or [N x (M+1) x wl] for spd)) dataw = np2d(dataw).copy( ) # white point (can be upto Nx3 for xyz, or [(N+1) x wl] for spd) # make axis 1 of dataw 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 dataw.shape[ 0] == 1: #make dataw have same lights source dimension size as data dataw = np.repeat(dataw, data.shape[1], axis=0) else: if dataw.shape[0] == 2: dataw = np.vstack( (dataw[0], np.repeat(dataw[1:], data.shape[1], axis=0))) # Flip light source dim to axis 0: data = np.transpose(data, axes=(1, 0, 2)) # Initialize output array: dshape = list(data.shape) dshape[-1] = 3 # requested number of correlates: l_int, a_int, b_int 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.zeros(dshape) camout.fill(np.nan) # apply forward/inverse model for each row in data: for i in range(data.shape[0]): # stage 1: calculate photon rates of stimulus and adapting field, lmst & lmsf: if (inputtype != 'xyz'): if relative == True: xyzw_abs = spd_to_xyz(np.vstack((dataw[0], dataw[i + 1])), cieobs=cieobs, relative=False) dataw[i + 1] = Lw * dataw[i + 1] / xyzw_abs[0, 1] # make absolute xyzw = spd_to_xyz(np.vstack((dataw[0], dataw[i + 1])), cieobs=cieobs, relative=False) lmsw = 683.0 * np.dot(Mxyz2lms, xyzw.T).T / _CMF[cieobs]['K'] lmsf = (Yb / 100.0 ) * lmsw # calculate adaptation field and convert to l,m,s if (direction == 'forward'): if relative == True: data[i, 1:, :] = Lw * data[i, 1:, :] / xyzw_abs[ 0, 1] # make absolute xyzt = spd_to_xyz(data[i], cieobs=cieobs, relative=False) / _CMF[cieobs]['K'] lmst = 683.0 * np.dot(Mxyz2lms, xyzt.T).T # convert to l,m,s else: lmst = lmsf # put lmsf in lmst for inverse-mode elif (inputtype == 'xyz'): if relative == True: dataw[i] = Lw * dataw[i] / 100.0 # make absolute lmsw = 683.0 * np.dot( Mxyz2lms, dataw[i].T).T / _CMF[cieobs]['K'] # convert to lms lmsf = (Yb / 100.0) * lmsw if (direction == 'forward'): if relative == True: data[i] = Lw * data[i] / 100.0 # make absolute lmst = 683.0 * np.dot( Mxyz2lms, data[i].T).T / _CMF[cieobs]['K'] # convert to lms else: lmst = lmsf # put lmsf in lmst for inverse-mode # stage 2: calculate cone outputs of stimulus lmstp lmstp = math.erf(Cc * (np.log(lmst / lms0) + Cf * np.log(lmsf / lms0))) lmsfp = math.erf(Cc * (np.log(lmsf / lms0) + Cf * np.log(lmsf / lms0))) lmstp = np.vstack( (lmsfp, lmstp) ) # add adaptation field lms temporarily to lmsp for quick calculation # stage 3: calculate optic nerve signals, lam*, alphp, betp: lstar, alph, bet = asplit(np.dot(MAab, lmstp.T).T) alphp = cga1[0] * alph alphp[alph < 0] = cga1[1] * alph[alph < 0] betp = cgb1[0] * bet betp[bet < 0] = cgb1[1] * bet[bet < 0] # stage 4: calculate recoded nerve signals, alphapp, betapp: alphpp = cga2[0] * (alphp + betp) betpp = cgb2[0] * (alphp - betp) # stage 5: calculate conscious color perception: lstar_int = cl_int[0] * (lstar + cl_int[1]) alph_int = cab_int[0] * (np.cos(cab_int[1] * np.pi / 180.0) * alphpp - np.sin(cab_int[1] * np.pi / 180.0) * betpp) bet_int = cab_int[0] * (np.sin(cab_int[1] * np.pi / 180.0) * alphpp + np.cos(cab_int[1] * np.pi / 180.0) * betpp) lstar_out = lstar_int if direction == 'forward': if Ccwb is None: alph_out = alph_int - cab_out[0] bet_out = bet_int - cab_out[1] else: Ccwb = Ccwb * np.ones((2)) Ccwb[Ccwb < 0.0] = 0.0 Ccwb[Ccwb > 1.0] = 1.0 alph_out = alph_int - Ccwb[0] * alph_int[ 0] # white balance shift using adaptation gray background (Yb=20%), with Ccw: degree of adaptation bet_out = bet_int - Ccwb[1] * bet_int[0] camout[i] = np.vstack( (lstar_out[1:], alph_out[1:], bet_out[1:]) ).T # stack together and remove adaptation field from vertical stack elif direction == 'inverse': labf_int = np.hstack((lstar_int[0], alph_int[0], bet_int[0])) # get lstar_out, alph_out & bet_out for data: lstar_out, alph_out, bet_out = asplit(data[i]) # stage 5 inverse: # undo cortical white-balance: if Ccwb is None: alph_int = alph_out + cab_out[0] bet_int = bet_out + cab_out[1] else: Ccwb = Ccwb * np.ones((2)) Ccwb[Ccwb < 0.0] = 0.0 Ccwb[Ccwb > 1.0] = 1.0 alph_int = alph_out + Ccwb[0] * alph_int[ 0] # inverse white balance shift using adaptation gray background (Yb=20%), with Ccw: degree of adaptation bet_int = bet_out + Ccwb[1] * bet_int[0] lstar_int = lstar_out alphpp = (1.0 / cab_int[0]) * ( np.cos(-cab_int[1] * np.pi / 180.0) * alph_int - np.sin(-cab_int[1] * np.pi / 180.0) * bet_int) betpp = (1.0 / cab_int[0]) * ( np.sin(-cab_int[1] * np.pi / 180.0) * alph_int + np.cos(-cab_int[1] * np.pi / 180.0) * bet_int) lstar_int = lstar_out lstar = (lstar_int / cl_int[0]) - cl_int[1] # stage 4 inverse: alphp = 0.5 * (alphpp / cga2[0] + betpp / cgb2[0] ) # <-- alphpp = (Cga2.*(alphp+betp)); betp = 0.5 * (alphpp / cga2[0] - betpp / cgb2[0] ) # <-- betpp = (Cgb2.*(alphp-betp)); # stage 3 invers: alph = alphp / cga1[0] bet = betp / cgb1[0] sa = np.sign(cga1[1]) sb = np.sign(cgb1[1]) alph[(sa * alphp) < 0.0] = alphp[(sa * alphp) < 0] / cga1[1] bet[(sb * betp) < 0.0] = betp[(sb * betp) < 0] / cgb1[1] lab = ajoin((lstar, alph, bet)) # stage 2 inverse: lmstp = np.dot(invMAab, lab.T).T lmstp[lmstp < -1.0] = -1.0 lmstp[lmstp > 1.0] = 1.0 lmstp = math.erfinv(lmstp) / Cc - Cf * np.log(lmsf / lms0) lmst = np.exp(lmstp) * lms0 # stage 1 inverse: xyzt = np.dot(invMxyz2lms, lmst.T).T if relative == True: xyzt = (100.0 / Lw) * xyzt camout[i] = xyzt # if flipaxis0and1 == True: # loop over shortest dim. # camout = np.transpose(camout, axes = (1,0,2)) # Flip light source dim back to axis 1: camout = np.transpose(camout, axes=(1, 0, 2)) if camout.shape[0] == 1: camout = np.squeeze(camout, axis=0) return camout
def rosenbrock_with_args(x, a, b, c=0): f = (a - x[:, 0])**2 + b * (x[:, 1] - x[:, 0]**2)**2 + c return f if __name__ == '__main__': from pyswarms.utils.functions import single_obj as fx #-------------------------------------------------------------------------- # 1: Rastrigin example: objfcn = fx.rastrigin fargs = {} dimensions = 2 max_bound = 5.12 * np.ones(2) min_bound = -max_bound res1 = particleswarm(objfcn, dimensions, args=fargs, use_bnds=True, bounds=(min_bound, max_bound), iters=100, n_particles=10, ftol=-np.inf, options={ 'c1': 0.5, 'c2': 0.3, 'w': 0.9 },
def discrimination_hotelling_t2(Yxy1, Yxy2, etype = 'fmc2', ellipsoid = True, Y1 = None, Y2 = None, cspace = 'Yxy'): """ Check 'significance' of difference using Hotelling's T2 test on the centers Yxy1 and Yxy2 and their associate FMC-1/2 discrimination ellipses. Args: :Yxy1, Yxy2: | 2D ndarrays with [Y,]x,y coordinate centers. | If Yxy.shape[-1]==2: Y is added using the value from the Y-input argument. :etype: | 'fmc2', optional | Type of FMC color discrimination equations to use (see references below). | options: 'fmc1', fmc2' :Y1, Y2: | None, optional | Only affects FMC-2 (see note below). | If not None: Yi = 10.69 and overrides values in Yxyi. :ellipsoid: | True, optional | If True: return ellipsoids, else return ellipses (only if cspace == 'Yxy')! :cspace: | 'Yxy', optional | Return coefficients for Yxy-ellipses/ellipsoids ('Yxy') or XYZ ellipsoids ('xyz') Returns: :p: | Chi-square based p-value :T2: | T2 test statistic (= mahalanobis distance on summed standard error cov. matrices) Steps: 1. For each center coordinate, the standard error covariance matrix gij^-1 = Si/ni is determined using the FMC-1 or FMC-2 equations (see refs. 1 & 2). 2. Calculate sum of covariance matrices: SIG = S1/n1 + S2/n2 = gij1^-1 + gij2^-1 3. These are then used in Hotelling's T2 test: T2 = (xy1 - xy2).T*(SIG^-1)*(xy1_xy2) 4. The T2 statistic is then tested against a Chi-square distribution with 2 or 3 degrees of freedom. References: 1. Chickering, K.D. (1967), Optimization of the MacAdam-Modified 1965 Friele Color-Difference Formula, 57(4):537-541 2. Chickering, K.D. (1971), FMC Color-Difference Formulas: Clarification Concerning Usage, 61(1):118-122 """ if Yxy1.shape[-1] == 2: Yxy1 = np.hstack((100*np.ones((Yxy1.shape[0],1)),Yxy1)) if Y1 is not None: Yxy1[...,0] = Y1 if Yxy2.shape[-1] == 2: Yxy2 = np.hstack((100*np.ones((Yxy2.shape[0],1)),Yxy2)) if Y2 is not None: Yxy2[...,0] = Y2 # Get gij matrices (i.e. inverse covariance matrices): gij1 = get_gij_fmc(Yxy1, etype = etype, ellipsoid=ellipsoid, Y = Y1, cspace = cspace) gij2 = get_gij_fmc(Yxy2, etype = etype, ellipsoid=ellipsoid, Y = Y2, cspace = cspace) df = gij1.shape[1] # get degrees of freedom for Chi2 # Calculate T2 statistic: D12 = (Yxy1[...,(3-df):] - Yxy2[...,(3-df):]) SIG12 = np.linalg.inv(np.linalg.inv(gij1) + np.linalg.inv(gij2)) T2 = np.einsum('ki,ki->k',D12,np.einsum('kij,kj->ki',SIG12,D12)) #T2 = np.atleast_2d(T2).T # Get p-value: p = sp.stats.distributions.chi2.sf(T2, df) return p, T2