def pitman_morgan(X,Y, verbosity = 0): """ Pitman-Morgan Test for the difference between correlated variances with paired samples. Args: :X,Y: | ndarrays with data. :verbosity: | 0, optional | If 1: print results. Returns: :tval: | statistic :pval: | p-value :df: | degree of freedom. :ratio: | variance ratio var1/var2 (with var1 > var2). Note: 1. Based on Gardner, R.C. (2001). Psychological Statistics Using SPSS for Windows. New Jersey, Prentice Hall. 2. Python port from matlab code by Janne Kauttonen (https://nl.mathworks.com/matlabcentral/fileexchange/67910-pitmanmorgantest-x-y; accessed Sep 26, 2019) """ N = X.shape[0] var1, var2 = X.var(axis=0),Y.var(axis=0) cor = np.corrcoef(X,Y)[0,1] # must have var1 > var2: if var1 < var2: var1, var2 = var2, var1 ratio = var1/var2 # formulas from Garder (2001, p.57): numerator1_S1minusS2 = var1-var2 numerator2_SQRTnminus2 = np.sqrt(N-2) numerator3 = numerator1_S1minusS2*numerator2_SQRTnminus2 denominator1_4timesS1timesS2 = 4*var1*var2 denominator2_rSquared = cor**2 denominator3_1minusrSquared = 1.0 - denominator2_rSquared denominator4_4timesS1timesS2div1minusrSquared = denominator1_4timesS1timesS2*denominator3_1minusrSquared denominator5 = np.sqrt(denominator4_4timesS1timesS2div1minusrSquared) df = N-2 if denominator5 == 0: denominator5 = _EPS tval = numerator3/denominator5 # compute stats: p = 2*(1.0-sp.stats.t.cdf(tval,df)) if verbosity == 1: print('tval = {:1.4f}, df = {:1.1f}, p = {:1.4f}'.format(tval,df, p)) return tval, p, df, ratio
def cik_to_v(cik, xyc = None, inverse = False): """ Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik Args: :cik: | 'Nx2x2' (covariance matrix)^-1 :inverse: | If True: input is inverse of cik. Returns: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] Notes: | cik is not actually the inverse covariance matrix, | only for a Gaussian or normal distribution! """ if cik.ndim < 3: cik = cik[None,...] if inverse == True: for i in range(cik.shape[0]): cik[i,:,:] = np.linalg.inv(cik[i,:,:]) g11 = cik[:,0,0] g22 = cik[:,1,1] g12 = cik[:,0,1] theta = 0.5*np.arctan2(2*g12,(g11-g22)) + (np.pi/2)*(g12<0) #theta = theta2 + (np.pi/2)*(g12<0) #theta2 = theta cottheta = np.cos(theta)/np.sin(theta) #np.cot(theta) cottheta[np.isinf(cottheta)] = 0 a = 1/np.sqrt((g22 + g12*cottheta)) b = 1/np.sqrt((g11 - g12*cottheta)) # ensure largest ellipse axis is first (correct angle): c = b>a; a[c], b[c], theta[c] = b[c],a[c],theta[c]+np.pi/2 v = np.vstack((a, b, np.zeros(a.shape), np.zeros(a.shape), theta)).T # add center coordinates: if xyc is not None: v[:,2:4] = xyc return v
def cart2spher(x,y,z, deg = True): """ Convert cartesian to spherical coordinates. Args: :x, y, z: | tuple of floats, ints or ndarrays | Cartesian coordinates Returns: :theta: | Float, int or ndarray | Angle with positive z-axis. :phi: | Float, int or ndarray | Angle around positive z-axis starting from x-axis. :r: | 1, optional | Float, int or ndarray | radius """ r = np.sqrt(x*x + y*y + z*z) phi = np.arctan2(y,x) phi[phi<0.] = phi[phi<0.] + 2*np.pi zdr = z/r zdr[zdr > 1.] = 1. zdr[zdr<-1.] = -1 theta = np.arccos(zdr) if deg == True: theta = theta*180/np.pi phi = phi *180/np.pi return theta, phi, r
def apply_poly_model_at_x(poly_model, pmodel, axr, bxr): """ Applies base color shift model at cartesian coordinates axr, bxr. Args: :poly_model: | function handle to model :pmodel: | ndarray with model parameters. :axr: | ndarray with a-coordinates under the reference conditions :bxr: | ndarray with b-coordinates under the reference conditions Returns: :returns: | (axt,bxt,Cxt,hxt, | axr,bxr,Cxr,hxr) | | ndarrays with ab-coordinates, chroma and hue | predicted by the model (xt), under the reference (xr). """ # Calculate hxr and Cxr: Cxr = np.sqrt(axr**2 + bxr**2) hxr = np.arctan(bxr / (axr + _EPS)) #_eps avoid zero-division # B Set 2nd order color multipliers (shiftd parameters for a and b: pa & pb): pa = pmodel[0].copy() pb = pmodel[1].copy() isM6 = pa.shape[0] == 6 pa[0 + isM6 * 1] = 1 + pa[0 + isM6 * 1] pb[1 + isM6 * 1] = 1 + pb[1 + isM6 * 1] # C Apply model to reference hues using 2nd order multipliers: axt = poly_model(axr, bxr, pa) bxt = poly_model(axr, bxr, pb) Cxt = np.sqrt(axt**2 + bxt**2) #test chroma hxt = np.arctan(bxt / (axt + _EPS)) return axt, bxt, Cxt, hxt, axr, bxr, Cxr, hxr
def magnitude_v(v): """ Calculates magnitude of vector. Args: :v: | ndarray with vector Returns: :magnitude: | ndarray """ magnitude = np.sqrt(v[:,0]**2 + v[:,1]**2) return magnitude
def get_tpr(self, *args): """ get spherical coordinates tpr (theta, phi, radius) """ if len(args) > 0: x, y, z = args else: x, y, z = self.x, self.y, self.z r = np.sqrt(x * x + y * y + z * z) zdr = np.asarray(z / r) zdr[zdr > 1.0] = 1.0 zdr[zdr < -1.0] = -1.0 theta = np.arccos(zdr) phi = np.arctan2(y, x) phi[phi < 0.0] = phi[phi < 0.0] + 2 * np.pi phi[r < self._TINY] = 0.0 theta[r < self._TINY] = 0.0 return theta, phi, r
def _polyarea3D(xyz): x, y, z = asplit(xyz) RY = np.sqrt((x[0] - x[1])**2 + (y[0] - y[1])**2 + (z[0] - z[1])**2) YG = np.sqrt((x[1] - x[2])**2 + (y[1] - y[2])**2 + (z[1] - z[2])**2) GR = np.sqrt((x[2] - x[0])**2 + (y[2] - y[0])**2 + (z[2] - z[0])**2) RB = np.sqrt((x[0] - x[3])**2 + (y[0] - y[3])**2 + (z[0] - z[3])**2) BG = np.sqrt((x[2] - x[3])**2 + (y[2] - y[3])**2 + (z[2] - z[3])**2) S1 = (RY + YG + GR) / 2 S2 = (RB + BG + GR) / 2 GA1 = np.sqrt(S1 * (S1 - RY) * (S1 - YG) * (S1 - GR)) GA2 = np.sqrt(S2 * (S2 - RB) * (S2 - BG) * (S2 - GR)) GA = GA1 + GA2 return GA
def rms(data,axis = 0, keepdims = False): """ Calculate root-mean-square along axis. Args: :data: | list of values or ndarray :axis: | 0, optional | Axis along which to calculate rms. :keepdims: | False or True, optional | Keep original dimensions of array. Returns: :returns: | ndarray with rms values. """ data = np2d(data) return np.sqrt(np.power(data,2).mean(axis=axis, keepdims = keepdims))
def cart2pol(x,y = None, htype = 'deg'): """ Convert Cartesion to polar coordinates. Args: :x: | float or ndarray with x-coordinates :y: | None or float or ndarray with x-coordinates, optional | If None, y-coordinates are assumed to be in :x:. :htype: | 'deg' or 'rad, optional | Output type of theta. Returns: :returns: | (float or ndarray of theta, float or ndarray of r) values """ if y is None: y = x[...,1].copy() x = x[...,0].copy() return positive_arctan(x,y, htype = htype), np.sqrt(x**2 + y**2)
def _process_DEi(DEi, DEtype='jab', avg=None, avg_axis=0, out='DEi'): """ Process color difference input DEi for output (helper function). Args: :DEi: | tuple(J ndarray, ab ndarray). :DEtype: | 'jab' or str, optional | Options: | - 'jab' : calculates full color difference over all 3 dimensions. | - 'ab' : calculates chromaticity difference. | - 'j' : calculates lightness or brightness difference | (depending on :out:). | - 'j,ab': calculates both 'j' and 'ab' options | and returns them as a tuple. :avg: | None, optional | None: don't calculate average DE, | otherwise use function handle in :avg:. :avg_axis: | axis to calculate average over, optional :out: | 'DEi' or str, optional | Requested output. Note: For the other input arguments, see specific color space used. Returns: :returns: | ndarray with DEi [, DEa] or other as specified by :out: """ if (DEi[0].shape[-1] == 1) & (DEi[0].ndim == 3): DEi = tuple((map(lambda x: np.squeeze(x, axis=x.ndim - 1), DEi))) # Calculate correct type of DE: if DEtype == 'jab': DEi = np.sqrt(DEi[0] + DEi[1]) elif DEtype == 'ab': DEi = np.sqrt(DEi[1]) elif DEtype == 'j': DEi = np.sqrt(DEi[0]) # Calculate average when requested: if (avg is not None) & ('DEa' in out.split(',')): if isinstance(DEi, tuple): DEa = (avg(DEi[0], axis=avg_axis, keepdims=True), avg(DEi[1], axis=avg_axis, keepdims=True)) else: DEa = avg(DEi, axis=avg_axis, keepdims=True) if out == 'DEi': return DEi elif out == 'DEi,DEa': return DEi, DEa else: return eval(out)
def DE_cspace(xyzt, xyzr, dtype = 'xyz', tf = _CSPACE, DEtype = 'jab', avg = None, avg_axis = 0, out = 'DEi', xyzwt = None, xyzwr = None, fwtft = {}, fwtfr = {}, KLCH = None,\ camtype = cam._CAM_DEFAULT_TYPE, ucstype = 'ucs'): """ Calculate color difference DE in specific color space. Args: :xyzt: | ndarray with tristimulus values of test data. :xyzr: | ndarray with tristimulus values of reference data. :dtype: | 'xyz' or 'jab', optional | Specifies data type in :xyzt: and :xyzr:. :xyzwt: | None or ndarray, optional | White point tristimulus values of test data | None defaults to the one set in :fwtft: | or else to the default of cspace. :xyzwr: | None or ndarray, optional | Whitepoint tristimulus values of reference data | None defaults to the one set in non-empty :fwtfr: | or else to default of cspace. :tf: | _CSPACE, optional | Color space to use for color difference calculation. :fwtft: | {}, optional | Dict with parameters for forward transform from xyz to cspace for test data. :fwtfr: | {}, optional | Dict with parameters for forward transform | from xyz to cspace for reference data. :KLCH: | None, optional | Weigths for L, C, H | None: default to [1,1,1] | KLCH is not used when tf == 'camucs'. :DEtype: | 'jab' or str, optional | Options: | - 'jab' : calculates full color difference over all 3 dimensions. | - 'ab' : calculates chromaticity difference. | - 'j' : calculates lightness or brightness difference | (depending on :outin:). | - 'j,ab': calculates both 'j' and 'ab' options | and returns them as a tuple. :avg: | None, optional | None: don't calculate average DE, | otherwise use function handle in :avg:. :avg_axis: | axis to calculate average over, optional :out: | 'DEi' or str, optional | Requested output. :camtype: | luxpy.cam._CAM_DEFAULT_TYPE, optional | Str specifier for CAM type to use, options: 'ciecam02' or 'ciecam16'. | Only when DEtype == 'camucs'. :ucstype: | 'ucs' or 'lcd' or 'scd', optional | Str specifier for which type of color attribute compression | parameters to use: | -'ucs': uniform color space, | -'lcd', large color differences, | -'scd': small color differences | Only when DEtype == 'camucs'. Note: For the other input arguments, see specific color space used. Returns: :returns: | ndarray with DEi [, DEa] or other as specified by :out: """ # Get xyzw from dict if xyzw is None & dict is Not None if xyzwr is not None: fwtfr['xyzw'] = xyzwr else: if bool(fwtfr): xyzwr = fwtfr['xyzw'] if xyzwt is not None: fwtft['xyzw'] = xyzwt else: if bool(fwtft): xyzwt = fwtft['xyzw'] if tf == 'camucs': if dtype == 'xyz': if fwtfr['xyzw'] is None: fwtfr['xyzw'] = cam._CAM_DEFAULT_WHITE_POINT if fwtft['xyzw'] is None: fwtft['xyzw'] = cam._CAM_DEFAULT_WHITE_POINT jabt = cam.camXucs(xyzt, camtype=camtype, ucstype=ucstype, **fwtft) jabr = cam.camXucs(xyzr, camtype=camtype, ucstype=ucstype, **fwtfr) else: jabt = xyzt jabr = xyzr KL, c1, c2 = [ cam._CAM_UCS_PARAMETERS[camtype][ucstype][x] for x in sorted(cam._CAM_UCS_PARAMETERS[camtype][ucstype].keys()) ] # Calculate color difference and take account of KL: DEi = ((((jabt[...,0:1]-jabr[...,0:1])/KL)**2).sum(axis = jabt[...,0:1].ndim - 1, keepdims = True),\ ((jabt[...,1:3]-jabr[...,1:3])**2).sum(axis = jabt[...,1:3].ndim - 1, keepdims = True)) elif (tf == 'DE2000') | (tf == 'DE00'): return DE2000(xyzt, xyzr, dtype = 'xyz', DEtype = DEtype, avg = avg,\ avg_axis = avg_axis, out = out, xyzwt = xyzwt, xyzwr = xyzwr, KLCH = KLCH) else: if dtype == 'xyz': # Use colortf: jabt = colortf(xyzt, tf=tf, fwtf=fwtft) jabr = colortf(xyzr, tf=tf, fwtf=fwtfr) else: jabt = xyzt jabr = xyzr if (KLCH == None) | (KLCH == [1, 1, 1]): # Calculate color difference and take account of KL: DEi = (((jabt[...,0:1]-jabr[...,0:1])**2).sum(axis = jabt[...,0:1].ndim - 1, keepdims = True),\ ((jabt[...,1:3]-jabr[...,1:3])**2).sum(axis = jabt[...,1:3].ndim - 1, keepdims = True)) else: #using LCH specification for use with KLCH weights: Jt = jabt[..., 0:1] at = jabt[..., 1:2] bt = jabt[..., 2:3] Ct = np.sqrt(at**2 + bt**2) ht = cam.hue_angle(at, bt, htype='rad') Jr = jabr[..., 0:1] ar = jabr[..., 1:2] br = jabr[..., 2:3] Cr = np.sqrt(ar**2 + br**2) hr = cam.hue_angle(at, bt, htype='rad') dJ = Jt - Jr dC = Ct - Cr dH = ht - hr DEab2 = ((at - ar)**2 + (bt - br)**2) dH = np.sqrt(DEab2 - dC**2) DEi = ((dJ / KLCH[0])**2, (dC / KLCH[1])**2 + (dH / KLCH[2])**2) return _process_DEi(DEi, DEtype=DEtype, avg=avg, avg_axis=avg_axis, out=out)
def DE2000(xyzt, xyzr, dtype='xyz', DEtype='jab', avg=None, avg_axis=0, out='DEi', xyzwt=None, xyzwr=None, KLCH=None): """ Calculate DE2000 color difference. Args: :xyzt: | ndarray with tristimulus values of test data. :xyzr: | ndarray with tristimulus values of reference data. :dtype: | 'xyz' or 'lab', optional | Specifies data type in :xyzt: and :xyzr:. :xyzwt: | None or ndarray, optional | White point tristimulus values of test data | None defaults to the one set in lx.xyz_to_lab() :xyzwr: | None or ndarray, optional | Whitepoint tristimulus values of reference data | None defaults to the one set in lx.xyz_to_lab() :DEtype: | 'jab' or str, optional | Options: | - 'jab' : calculates full color difference over all 3 dimensions. | - 'ab' : calculates chromaticity difference. | - 'j' : calculates lightness or brightness difference | (depending on :outin:). | - 'j,ab': calculates both 'j' and 'ab' options | and returns them as a tuple. :KLCH: | None, optional | Weigths for L, C, H | None: default to [1,1,1] :avg: | None, optional | None: don't calculate average DE, | otherwise use function handle in :avg:. :avg_axis: | axis to calculate average over, optional :out: | 'DEi' or str, optional | Requested output. Note: For the other input arguments, see specific color space used. Returns: :returns: | ndarray with DEi [, DEa] or other as specified by :out: References: 1. `Sharma, G., Wu, W., & Dalal, E. N. (2005). The CIEDE2000 color‐difference formula: Implementation notes, supplementary test data, and mathematical observations. Color Research & Application, 30(1), 21–30. <https://doi.org/10.1002/col.20070>`_ """ if KLCH is None: KLCH = [1, 1, 1] if dtype == 'xyz': labt = xyz_to_lab(xyzt, xyzw=xyzwt) labr = xyz_to_lab(xyzr, xyzw=xyzwr) else: labt = xyzt labr = xyzr Lt = labt[..., 0:1] at = labt[..., 1:2] bt = labt[..., 2:3] Ct = np.sqrt(at**2 + bt**2) #ht = cam.hue_angle(at,bt,htype = 'rad') Lr = labr[..., 0:1] ar = labr[..., 1:2] br = labr[..., 2:3] Cr = np.sqrt(ar**2 + br**2) #hr = cam.hue_angle(at,bt,htype = 'rad') # Step 1: Cavg = (Ct + Cr) / 2 G = 0.5 * (1 - np.sqrt((Cavg**7.0) / ((Cavg**7.0) + (25.0**7)))) apt = (1 + G) * at apr = (1 + G) * ar Cpt = np.sqrt(apt**2 + bt**2) Cpr = np.sqrt(apr**2 + br**2) Cpprod = Cpt * Cpr hpt = cam.hue_angle(apt, bt, htype='deg') hpr = cam.hue_angle(apr, br, htype='deg') hpt[(apt == 0) * (bt == 0)] = 0 hpr[(apr == 0) * (br == 0)] = 0 # Step 2: dL = np.abs(Lr - Lt) dCp = np.abs(Cpr - Cpt) dhp_ = hpr - hpt dhp = dhp_.copy() dhp[np.where(np.abs(dhp_) > 180)] = dhp[np.where(np.abs(dhp_) > 180)] - 360 dhp[np.where( np.abs(dhp_) < -180)] = dhp[np.where(np.abs(dhp_) < -180)] + 360 dhp[np.where(Cpprod == 0)] = 0 #dH = 2*np.sqrt(Cpprod)*np.sin(dhp/2*np.pi/180) dH = deltaH(dhp, Cpprod, htype='deg') # Step 3: Lp = (Lr + Lt) / 2 Cp = (Cpr + Cpt) / 2 hps = hpt + hpr hp = (hpt + hpr) / 2 hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps < 360))] + 180 hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] = hp[np.where((np.abs(dhp_) > 180) & (hps >= 360))] - 180 hp[np.where(Cpprod == 0)] = 0 T = 1 - 0.17*np.cos((hp - 30)*np.pi/180) + 0.24*np.cos(2*hp*np.pi/180) +\ 0.32*np.cos((3*hp + 6)*np.pi/180) - 0.20*np.cos((4*hp - 63)*np.pi/180) dtheta = 30 * np.exp(-((hp - 275) / 25)**2) RC = 2 * np.sqrt((Cp**7) / ((Cp**7) + (25**7))) SL = 1 + ((0.015 * (Lp - 50)**2) / np.sqrt(20 + (Lp - 50)**2)) SC = 1 + 0.045 * Cp SH = 1 + 0.015 * Cp * T RT = -np.sin(2 * dtheta * np.pi / 180) * RC kL, kC, kH = KLCH DEi = ((dL / (kL * SL))**2, (dCp / (kC * SC))**2 + (dH / (kH * SH))**2 + RT * (dCp / (kC * SC)) * (dH / (kH * SH))) return _process_DEi(DEi, DEtype=DEtype, avg=avg, avg_axis=avg_axis, out=out)
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
def xyz_to_rfl(xyz, CSF = None, rfl = None, out = 'rfl_est', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'xyz', cspace_tf = {},\ interp_type = 'nd', k_neighbours = 4, verbosity = 0): """ Approximate spectral reflectance of xyz values based on nd-dimensional linear interpolation or k nearest neighbour interpolation of samples from a standard reflectance set. Args: :xyz: | ndarray with xyz values of target points. :CSF: | None, optional | RGB camera response functions. | If None: input :xyz: contains raw rgb (float) values. Override :cspace: | argument and perform estimation directly in raw rgb space!!! :rfl: | ndarray, optional | Reflectance set for color coordinate to rfl mapping. :out: | 'rfl_est' or str, optional :refspd: | None, optional | Refer ence spectrum for color coordinate to rfl mapping. | None defaults to D65. :cieobs: | _CIEOBS, optional | CMF set used for calculation of xyz from spectral data. :cspace: | 'xyz', optional | Color space for color coordinate to rfl mapping. | Tip: Use linear space (e.g. 'xyz', 'Yuv',...) for (interp_type == 'nd'), | and perceptually uniform space (e.g. 'ipt') for (interp_type == 'nearest') :cspace_tf: | {}, optional | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform. :interp_type: | 'nd', optional | Options: | - 'nd': perform n-dimensional linear interpolation using Delaunay triangulation. | - 'nearest': perform nearest neighbour interpolation. :k_neighbours: | 4 or int, optional | Number of nearest neighbours for reflectance spectrum interpolation. | Neighbours are found using scipy.spatial.cKDTree :verbosity: | 0, optional | If > 0: make a plot of the color coordinates of original and | rendered image pixels. Returns: :returns: | :rfl_est: | ndarrays with estimated reflectance spectra. """ # get rfl set: if rfl is None: # use IESTM30['4880'] set rfl = _CRI_RFL['ies-tm30']['4880']['5nm'] wlr = rfl[0] # get Ref spd: if refspd is None: refspd = _CIE_ILLUMINANTS['D65'].copy() refspd = cie_interp( refspd, wlr, kind='linear') # force spd to same wavelength range as rfl # Calculate rgb values of standard rfl set under refspd: if CSF is None: # Calculate lab coordinates: xyz_rr, xyz_wr = spd_to_xyz(refspd, relative=True, rfl=rfl, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy[ 'xyzw'] = xyz_wr # put correct white point in param. dict lab_rr = colortf(xyz_rr, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy)[:, 0, :] else: # Calculate rgb coordinates from camera sensitivity functions rgb_rr = rfl_to_rgb(rfl, spd=refspd, CSF=CSF, wl=None) lab_rr = rgb_rr xyz = xyz lab_rr = np.round(lab_rr, _ROUNDING) # speed up search # Convert xyz to lab-type values under refspd: if CSF is None: lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy) else: lab = xyz # xyz contained rgb values !!! rgb = xyz lab = np.round(lab, _ROUNDING) # speed up search if interp_type == 'nearest': # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric # color coordinates for each value in lab_ur (i.e. smallest DE): # Construct cKDTree: tree = sp.spatial.cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab, k=k_neighbours) if k_neighbours > 1: d += _EPS w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(w, axis=1) else: rfl_est = rfl[inds + 1, :].copy() elif interp_type == 'nd': rfl_est = math.ndinterp1_scipy(lab_rr, rfl[1:], lab) _isnan = np.isnan(rfl_est[:, 0]) if ( _isnan.any() ): #do nearest neigbour method for those that fail using Delaunay (i.e. ndinterp1_scipy) # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric # color coordinates for each value in lab_ur (i.e. smallest DE): # Construct cKDTree: tree = sp.spatial.cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab[_isnan, ...], k=k_neighbours) if k_neighbours > 1: d += _EPS w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est_isnan = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum( w, axis=1) else: rfl_est_isnan = rfl[inds + 1, :].copy() rfl_est[_isnan, :] = rfl_est_isnan else: raise Exception('xyz_to_rfl(): unsupported interp_type!') rfl_est[ rfl_est < 0] = 0 #can occur for points outside convexhull of standard rfl set. rfl_est = np.vstack((rfl[0], rfl_est)) if ((verbosity > 0) | ('xyz_est' in out.split(',')) | ('lab_est' in out.split(',')) | ('DEi_ab' in out.split(',')) | ('DEa_ab' in out.split(','))) & (CSF is None): xyz_est, _ = spd_to_xyz(refspd, rfl=rfl_est, relative=True, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy[ 'xyzw'] = xyz_wr # put correct white point in param. dict lab_est = colortf(xyz_est, tf=cspace, fwtf=cspace_tf_copy)[:, 0, :] DEi_ab = np.sqrt(((lab_est[:, 1:3] - lab[:, 1:3])**2).sum(axis=1)) DEa_ab = DEi_ab.mean() elif ((verbosity > 0) | ('xyz_est' in out.split(',')) | ('rgb_est' in out.split(',')) | ('DEi_rgb' in out.split(',')) | ('DEa_rgb' in out.split(','))) & (CSF is not None): rgb_est = rfl_to_rgb(rfl_est[1:], spd=refspd, CSF=CSF, wl=wlr) xyz_est = rgb_est DEi_rgb = np.sqrt(((rgb_est - rgb)**2).sum(axis=1)) DEa_rgb = DEi_rgb.mean() if verbosity > 0: if CSF is None: ax = plot_color_data(lab[...,1], lab[...,2], z = lab[...,0], \ show = False, cieobs = cieobs, cspace = cspace, \ formatstr = 'ro', label = 'Original') plot_color_data(lab_est[...,1], lab_est[...,2], z = lab_est[...,0], \ show = True, axh = ax, cieobs = cieobs, cspace = cspace, \ formatstr = 'bd', label = 'Rendered') else: n = 100 #min(rfl.shape[0]-1,rfl_est.shape[0]-1) s = np.random.permutation(rfl.shape[0] - 1)[:min(n, rfl.shape[0] - 1)] st = np.random.permutation(rfl_est.shape[0] - 1)[:min(n, rfl_est.shape[0] - 1)] fig = plt.figure() ax = np.zeros((3, ), dtype=np.object) ax[0] = fig.add_subplot(131) ax[1] = fig.add_subplot(132) ax[2] = fig.add_subplot(133, projection='3d') ax[0].plot(rfl[0], rfl[1:][s].T, linestyle='-') ax[0].set_title('Original RFL set (random selection of all)') ax[0].set_ylim([0, 1]) ax[1].plot(rfl_est[0], rfl_est[1:][st].T, linestyle='--') ax[0].set_title('Estimated RFL set (random selection of targets)') ax[1].set_ylim([0, 1]) ax[2].plot(rgb[st, 0], rgb[st, 1], rgb[st, 2], 'ro', label='Original') ax[2].plot(rgb_est[st, 0], rgb_est[st, 1], rgb_est[st, 2], 'bd', label='Rendered') ax[2].legend() if out == 'rfl_est': return rfl_est elif out == 'rfl_est,xyz_est': return rfl_est, xyz_est else: return eval(out)
def fit_ellipse(xy, center_on_mean_xy = False): """ Fit an ellipse to supplied data points. Args: :xy: | coordinates of points to fit (Nx2 array) :center_on_mean_xy: | False, optional | Center ellipse on mean of xy | (otherwise it might be offset due to solving | the contrained minization problem: aT*S*a, see ref below.) Returns: :v: | vector with ellipse parameters [Rmax,Rmin, xc,yc, theta (rad.)] Reference: 1. Fitzgibbon, A.W., Pilu, M., and Fischer R.B., Direct least squares fitting of ellipsees, Proc. of the 13th Internation Conference on Pattern Recognition, pp 253–257, Vienna, 1996. """ # remove centroid: # center = xy.mean(axis=0) # xy = xy - center # Fit ellipse: x, y = xy[:,0:1], xy[:,1:2] D = np.hstack((x * x, x * y, y * y, x, y, np.ones_like(x))) S, C = np.dot(D.T, D), np.zeros([6, 6]) C[0, 2], C[2, 0], C[1, 1] = 2, 2, -1 U, s, V = np.linalg.svd(np.dot(np.linalg.inv(S), C)) e = U[:, 0] # E, V = np.linalg.eig(np.dot(np.linalg.inv(S), C)) # n = np.argmax(np.abs(E)) # e = V[:,n] # get ellipse axis lengths, center and orientation: b, c, d, f, g, a = e[1] / 2, e[2], e[3] / 2, e[4] / 2, e[5], e[0] # get ellipse center: num = b * b - a * c if num == 0: xc = 0 yc = 0 else: xc = ((c * d - b * f) / num) yc = ((a * f - b * d) / num) # get ellipse orientation: theta = np.arctan2(np.array(2 * b), np.array((a - c))) / 2 # if b == 0: # if a > c: # theta = 0 # else: # theta = np.pi/2 # else: # if a > c: # theta = np.arctan2(2*b,(a-c))/2 # else: # theta = np.arctan2(2*b,(a-c))/2 + np.pi/2 # axis lengths: up = 2 * (a * f * f + c * d * d + g * b * b - 2 * b * d * f - a * c * g) down1 = (b * b - a * c) * ((c - a) * np.sqrt(1 + 4 * b * b / ((a - c) * (a - c))) - (c + a)) down2 = (b * b - a * c) * ((a - c) * np.sqrt(1 + 4 * b * b / ((a - c) * (a - c))) - (c + a)) a, b = np.sqrt((up / down1)), np.sqrt((up / down2)) # assert that a is the major axis (otherwise swap and correct angle) if(b > a): b, a = a, b # ensure the angle is betwen 0 and 2*pi theta = fmod(theta, 2.0 * np.pi) if center_on_mean_xy == True: xc,yc = xy.mean(axis=0) return np.hstack((a, b, xc, yc, theta))
def xyz_to_rfl(xyz, rfl = None, out = 'rfl_est', \ refspd = None, D = None, cieobs = _CIEOBS, \ cspace = 'xyz', cspace_tf = {},\ interp_type = 'nd', k_neighbours = 4, verbosity = 0): """ Approximate spectral reflectance of xyz based on nd-dimensional linear interpolation or k nearest neighbour interpolation of samples from a standard reflectance set. Args: :xyz: | ndarray with tristimulus values of target points. :rfl: | ndarray, optional | Reflectance set for color coordinate to rfl mapping. :out: | 'rfl_est' or str, optional :refspd: | None, optional | Refer ence spectrum for color coordinate to rfl mapping. | None defaults to D65. :cieobs: | _CIEOBS, optional | CMF set used for calculation of xyz from spectral data. :cspace: | 'xyz', optional | Color space for color coordinate to rfl mapping. | Tip: Use linear space (e.g. 'xyz', 'Yuv',...) for (interp_type == 'nd'), | and perceptually uniform space (e.g. 'ipt') for (interp_type == 'nearest') :cspace_tf: | {}, optional | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform. :interp_type: | 'nd', optional | Options: | - 'nd': perform n-dimensional linear interpolation using Delaunay triangulation. | - 'nearest': perform nearest neighbour interpolation. :k_neighbours: | 4 or int, optional | Number of nearest neighbours for reflectance spectrum interpolation. | Neighbours are found using scipy.spatial.cKDTree :verbosity: | 0, optional | If > 0: make a plot of the color coordinates of original and | rendered image pixels. Returns: :returns: | :rfl_est: | ndarrays with estimated reflectance spectra. """ # get rfl set: if rfl is None: # use IESTM30['4880'] set rfl = _CRI_RFL['ies-tm30']['4880']['5nm'] # get Ref spd: if refspd is None: refspd = _CIE_ILLUMINANTS['D65'].copy() # Calculate lab-type coordinates of standard rfl set under refspd: xyz_rr, xyz_wr = spd_to_xyz(refspd, relative=True, rfl=rfl, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy['xyzw'] = xyz_wr # put correct white point in param. dict lab_rr = colortf(xyz_rr, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy)[:, 0, :] # Convert xyz to lab-type values under refspd: lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy) if interp_type == 'nearest': # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric # color coordinates for each value in lab_ur (i.e. smallest DE): # Construct cKDTree: tree = sp.spatial.cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab, k=k_neighbours) if k_neighbours > 1: d += _EPS w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(w, axis=1) else: rfl_est = rfl[inds + 1, :].copy() elif interp_type == 'nd': rfl_est = math.ndinterp1_scipy(lab_rr, rfl[1:], lab) _isnan = np.isnan(rfl_est[:, 0]) if ( _isnan.any() ): #do nearest neigbour method for those that fail using Delaunay (i.e. ndinterp1_scipy) # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric # color coordinates for each value in lab_ur (i.e. smallest DE): # Construct cKDTree: tree = sp.spatial.cKDTree(lab_rr, copy_data=True) # Interpolate rfls using k nearest neightbours and inverse distance weigthing: d, inds = tree.query(lab[_isnan, ...], k=k_neighbours) if k_neighbours > 1: d += _EPS w = (1.0 / d**2)[:, :, None] # inverse distance weigthing rfl_est_isnan = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum( w, axis=1) else: rfl_est_isnan = rfl[inds + 1, :].copy() rfl_est[_isnan, :] = rfl_est_isnan else: raise Exception('xyz_to_rfl(): unsupported interp_type!') rfl_est[ rfl_est < 0] = 0 #can occur for points outside convexhull of standard rfl set. rfl_est = np.vstack((rfl[0], rfl_est)) if (verbosity > 0) | ('xyz_est' in out.split(',')) | ( 'lab_est' in out.split(',')) | ('DEi_ab' in out.split(',')) | ( 'DEa_ab' in out.split(',')): xyz_est, _ = spd_to_xyz(refspd, rfl=rfl_est, relative=True, cieobs=cieobs, out=2) cspace_tf_copy = cspace_tf.copy() cspace_tf_copy[ 'xyzw'] = xyz_wr # put correct white point in param. dict lab_est = colortf(xyz_est, tf=cspace, fwtf=cspace_tf_copy)[:, 0, :] DEi_ab = np.sqrt(((lab_est[:, 1:3] - lab[:, 1:3])**2).sum(axis=1)) DEa_ab = DEi_ab.mean() if verbosity > 0: ax = plot_color_data(lab[...,1], lab[...,2], z = lab[...,0], \ show = False, cieobs = cieobs, cspace = cspace, \ formatstr = 'ro', label = 'Original') plot_color_data(lab_est[...,1], lab_est[...,2], z = lab_est[...,0], \ show = True, axh = ax, cieobs = cieobs, cspace = cspace, \ formatstr = 'bd', label = 'Rendered') if out == 'rfl_est': return rfl_est elif out == 'rfl_est,xyz_est': return rfl_est, xyz_est else: return eval(out)
def spd_to_cqs(SPD, version='v9.0', out='Qa', wl=None): """ Calculates CQS Qa (Qai) or Qf (Qfi) or Qp (Qpi) for versions v9.0 or v7.5. Args: :SPD: | ndarray with spectral data (can be multiple SPDs, | first axis are the wavelengths) :version: | 'v9.0' or 'v7.5', optional :out: | 'Qa' or str, optional | Specifies requested output (e.g. 'Qa,Qai,Qf,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 CQS Qa for :out: 'Qa' | Other output is also possible by changing the :out: str value. References: 1. `W. Davis and Y. Ohno, “Color quality scale,” (2010), Opt. Eng., vol. 49, no. 3, pp. 33602–33616. <http://spie.org/Publications/Journal/10.1117/1.3360335>`_ """ outlist = out.split() if isinstance(version, str): cri_type = 'cqs-' + version elif isinstance(version, dict): cri_type = version # calculate DEI, labti, labri and get cspace_pars and rg_pars: DEi, labti, labri, cct, duv, cri_type = spd_to_DEi( SPD, cri_type=cri_type, out='DEi,jabt,jabr,cct,duv,cri_type', wl=wl) # further unpack cri_type: scale_fcn = cri_type['scale']['fcn'] scale_factor = cri_type['scale']['cfactor'] avg = cri_type['avg'] cri_specific_pars = cri_type['cri_specific_pars'] rg_pars = cri_type['rg_pars'] # get maxC: to limit chroma-enhancement: maxC = cri_specific_pars['maxC'] # make 3d: test_original_shape = labti.shape if len(test_original_shape) < 3: labti = labti[:, None] labri = labri[:, None] DEi = DEi[:, None] cct = cct[:, None] # calculate Rg for each spd: Qf = np.zeros((1, labti.shape[1])) Qfi = np.zeros((labti.shape[0], labti.shape[1])) if version == 'v7.5': GA = (9.2672 * (1.0e-11)) * cct**3.0 - ( 8.3959 * (1.0e-7)) * cct**2.0 + 0.00255 * cct - 1.612 elif version == 'v9.0': GA = np.ones(cct.shape) else: raise Exception('.cri.spd_to_cqs(): Unrecognized CQS version.') if ('Qf' in outlist) | ('Qfi' in outlist): # loop of light source spds for ii in range(labti.shape[1]): Qfi[:, ii] = GA[ii] * scale_fcn(DEi[:, ii], [scale_factor[0]]) Qf[:, ii] = GA[ii] * scale_fcn(avg(DEi[:, ii, None], axis=0), [scale_factor[0]]) if ('Qa' in outlist) | ('Qai' in outlist) | ('Qp' in outlist) | ( 'Qpi' in outlist): Qa = Qf.copy() Qai = Qfi.copy() Qp = Qf.copy() Qpi = Qfi.copy() # loop of light source spds for ii in range(labti.shape[1]): # calculate deltaC: deltaC = np.sqrt( np.power(labti[:, ii, 1:3], 2).sum( axis=1, keepdims=True)) - np.sqrt( np.power(labri[:, ii, 1:3], 2).sum(axis=1, keepdims=True)) # limit chroma increase: DEi_Climited = DEi[:, ii, None].copy() deltaC_Climited = deltaC.copy() if maxC is None: maxC = 10000.0 limitC = np.where(deltaC >= maxC)[0] deltaC_Climited[limitC] = maxC p_deltaC_pos = np.where(deltaC > 0.0)[0] DEi_Climited[p_deltaC_pos] = np.sqrt( DEi_Climited[p_deltaC_pos]**2.0 - deltaC_Climited[p_deltaC_pos] **2.0) # increase in chroma is not penalized! if ('Qa' in outlist) | ('Qai' in outlist): Qai[:, ii, None] = GA[ii] * scale_fcn(DEi_Climited, [scale_factor[1]]) Qa[:, ii] = GA[ii] * scale_fcn(avg(DEi_Climited, axis=0), [scale_factor[1]]) if ('Qp' in outlist) | ('Qpi' in outlist): deltaC_pos = deltaC_Climited * (deltaC_Climited >= 0.0) deltaCmu = np.mean(deltaC_Climited * (deltaC_Climited >= 0.0)) Qpi[:, ii, None] = GA[ii] * scale_fcn( (DEi_Climited - deltaC_pos), [scale_factor[2] ]) # or ?? np.sqrt(DEi_Climited**2 - deltaC_pos**2) ?? Qp[:, ii] = GA[ii] * scale_fcn( (avg(DEi_Climited, axis=0) - deltaCmu), [scale_factor[2]]) if ('Qg' in outlist): Qg = Qf.copy() for ii in range(labti.shape[1]): Qg[:, ii] = 100.0 * math.polyarea( labti[:, ii, 1], labti[:, ii, 2]) / math.polyarea( labri[:, ii, 1], labri[:, ii, 2] ) # calculate Rg = gamut area ratio of test and ref if out == 'Qa': return Qa else: return eval(out)
def spd_to_ies_tm30_metrics(SPD, cri_type = None, \ hbins = 16, start_hue = 0.0,\ scalef = 100, \ vf_model_type = _VF_MODEL_TYPE, \ vf_pcolorshift = _VF_PCOLORSHIFT,\ scale_vf_chroma_to_sample_chroma = False): """ Calculates IES TM30 metrics from spectral data. Args: :data: | numpy.ndarray with spectral data :cri_type: | None, optional | If None: defaults to cri_type = 'iesrf'. | Not none values of :hbins:, :start_hue: and :scalef: overwrite | input in cri_type['rg_pars'] :hbins: | None or numpy.ndarray with sorted hue bin centers (°), optional :start_hue: | None, optional :scalef: | None, optional | Scale factor for reference circle. :vf_pcolorshift: | _VF_PCOLORSHIFT or user defined dict, optional | 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. :VF_pcolorshift: specifies | these hues and chroma level. :scale_vf_chroma_to_sample_chroma: | False, optional | Scale chroma of reference and test vf fields such that average of | binned reference chroma equals that of the binned sample chroma | before calculating hue bin metrics. Returns: :data: | dict with color rendering data: | - 'SPD' : ndarray test SPDs | - 'bjabt': ndarray with binned jab data under test SPDs | - 'bjabr': ndarray with binned jab data under reference SPDs | - 'jabti': ndarray with individual jab data under test SPDs (scaled such that bjabr are on a circle) | - 'jabri': ndarray with individual jab data under reference SPDs (scaled such that bjabr are on a circle) | - 'hbinnr': ndarray with the hue bin number the samples belong to. | - 'cct' : ndarray with CCT of test SPD | - 'duv' : ndarray with distance to blackbody locus of test SPD | - 'Rf' : ndarray with general color fidelity indices | - 'Rg' : ndarray with gamut area indices | - 'Rfi' : ndarray with specific color fidelity indices | - 'Rfhi' : ndarray with local (hue binned) fidelity indices | - 'Rcshi': ndarray with local chroma shifts indices | - 'Rhshi': ndarray with local hue shifts indices | - 'Rt' : ndarray with general metameric uncertainty index Rt | - 'Rti' : ndarray with specific metameric uncertainty indices Rti | - 'Rfhi_vf' : ndarray with local (hue binned) fidelity indices | obtained from VF model predictions at color space | pixel coordinates | - 'Rcshi_vf': ndarray with local chroma shifts indices | (same as above) | - 'Rhshi_vf': ndarray with local hue shifts indices | (same as above) """ if cri_type is None: cri_type = 'iesrf' #Calculate color rendering measures for SPDs in data: out = 'Rf,Rg,cct,duv,Rfi,jabt,jabr,Rfhi,Rcshi,Rhshi,cri_type' if isinstance(cri_type, str): # get dict cri_type = copy.deepcopy(_CRI_DEFAULTS[cri_type]) if hbins is not None: cri_type['rg_pars']['nhbins'] = hbins if start_hue is not None: cri_type['rg_pars']['start_hue'] = start_hue if scalef is not None: cri_type['rg_pars']['normalized_chroma_ref'] = scalef Rf, Rg, cct, duv, Rfi, jabt, jabr, Rfhi, Rcshi, Rhshi, cri_type = spd_to_cri( SPD, cri_type=cri_type, out=out) rg_pars = cri_type['rg_pars'] #Calculate Metameric uncertainty and base color shifts: dataVF = VF_colorshift_model(SPD, cri_type=cri_type, model_type=vf_model_type, cspace=cri_type['cspace'], sampleset=eval(cri_type['sampleset']), pool=False, pcolorshift=vf_pcolorshift, vfcolor=0) Rf_ = np.array([dataVF[i]['metrics']['Rf'] for i in range(len(dataVF))]).T Rt = np.array([dataVF[i]['metrics']['Rt'] for i in range(len(dataVF))]).T Rti = np.array([dataVF[i]['metrics']['Rti'] for i in range(len(dataVF))][0]) # Get normalized and sliced sample data for plotting: rg_pars = cri_type['rg_pars'] nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [ rg_pars[x] for x in sorted(rg_pars.keys()) ] normalized_chroma_ref = scalef # np.sqrt((jabr[...,1]**2 + jabr[...,2]**2)).mean(axis = 0).mean() if scale_vf_chroma_to_sample_chroma == True: normalize_gamut = False bjabt, bjabr = gamut_slicer( jabt, jabr, out='jabt,jabr', nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut, normalized_chroma_ref=normalized_chroma_ref, close_gamut=True) Cr_s = (np.sqrt(bjabr[:-1, ..., 1]**2 + bjabr[:-1, ..., 2]**2)).mean( axis=0) # for rescaling vector field average reference chroma normalize_gamut = True #(for plotting) bjabt, bjabr, binnrs, jabti, jabri = gamut_slicer( jabt, jabr, out='jabt,jabr,binnr,jabti,jabri', nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut, normalized_chroma_ref=normalized_chroma_ref, close_gamut=True) Rfhi_vf = np.empty(Rfhi.shape) Rcshi_vf = np.empty(Rcshi.shape) Rhshi_vf = np.empty(Rhshi.shape) for i in range(cct.shape[0]): # Get normalized and sliced VF data for hue specific metrics: vfjabt = np.hstack( (np.ones(dataVF[i]['fielddata']['vectorfield']['axt'].shape), dataVF[i]['fielddata']['vectorfield']['axt'], dataVF[i]['fielddata']['vectorfield']['bxt'])) vfjabr = np.hstack( (np.ones(dataVF[i]['fielddata']['vectorfield']['axr'].shape), dataVF[i]['fielddata']['vectorfield']['axr'], dataVF[i]['fielddata']['vectorfield']['bxr'])) nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [ rg_pars[x] for x in sorted(rg_pars.keys()) ] vfbjabt, vfbjabr, vfbDEi = gamut_slicer( vfjabt, vfjabr, out='jabt,jabr,DEi', nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut, normalized_chroma_ref=normalized_chroma_ref, close_gamut=False) if scale_vf_chroma_to_sample_chroma == True: #rescale vfbjabt and vfbjabr to same chroma level as bjabr. Cr_vfb = np.sqrt(vfbjabr[..., 1]**2 + vfbjabr[..., 2]**2) Cr_vf = np.sqrt(vfjabr[..., 1]**2 + vfjabr[..., 2]**2) hr_vf = np.arctan2(vfjabr[..., 2], vfjabr[..., 1]) Ct_vf = np.sqrt(vfjabt[..., 1]**2 + vfjabt[..., 2]**2) ht_vf = np.arctan2(vfjabt[..., 2], vfjabt[..., 1]) fC = Cr_s.mean() / Cr_vfb.mean() vfjabr[..., 1] = fC * Cr_vf * np.cos(hr_vf) vfjabr[..., 2] = fC * Cr_vf * np.sin(hr_vf) vfjabt[..., 1] = fC * Ct_vf * np.cos(ht_vf) vfjabt[..., 2] = fC * Ct_vf * np.sin(ht_vf) vfbjabt, vfbjabr, vfbDEi = gamut_slicer( vfjabt, vfjabr, out='jabt,jabr,DEi', nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut, normalized_chroma_ref=normalized_chroma_ref, close_gamut=False) scale_factor = cri_type['scale']['cfactor'] scale_fcn = cri_type['scale']['fcn'] vfRfhi, vfRcshi, vfRhshi = jab_to_rhi( jabt=vfbjabt, jabr=vfbjabr, DEi=vfbDEi, cri_type=cri_type, scale_factor=scale_factor, scale_fcn=scale_fcn, use_bin_avg_DEi=True ) # [:-1,...] removes last row from jab as this was added to close the gamut. Rfhi_vf[:, i:i + 1] = vfRfhi Rhshi_vf[:, i:i + 1] = vfRhshi Rcshi_vf[:, i:i + 1] = vfRcshi # Create dict with CRI info: data = {'SPD' : SPD, 'cct' : cct, 'duv' : duv, 'bjabt' : bjabt, 'bjabr' : bjabr,\ 'jabti':jabti, 'jabri':jabri, 'hbinnr':binnrs,\ 'Rf' : Rf, 'Rg' : Rg, 'Rfi': Rfi, 'Rfhi' : Rfhi, 'Rcshi' : Rcshi, 'Rhshi' : Rhshi, \ 'Rt' : Rt, 'Rti' : Rti, 'Rfhi_vf' : Rfhi_vf, 'Rfcshi_vf' : Rcshi_vf, 'Rfhshi_vf' : Rhshi_vf, \ 'dataVF' : dataVF,'cri_type' : cri_type, # 'jabt_':jabt_,'jabr_':jabr_ } return data
def spd_to_ies_tm30_metrics(St, cri_type = None, \ hbins = 16, start_hue = 0.0,\ scalef = 100, \ vf_model_type = _VF_MODEL_TYPE, \ vf_pcolorshift = _VF_PCOLORSHIFT,\ scale_vf_chroma_to_sample_chroma = False): """ Calculates IES TM30 metrics from spectral data. Args: :St: | numpy.ndarray with spectral data :cri_type: | None, optional | If None: defaults to cri_type = 'iesrf'. | Not none values of :hbins:, :start_hue: and :scalef: overwrite | input in cri_type['rg_pars'] :hbins: | None or numpy.ndarray with sorted hue bin centers (°), optional :start_hue: | None, optional :scalef: | None, optional | Scale factor for reference circle. :vf_pcolorshift: | _VF_PCOLORSHIFT or user defined dict, optional | 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. :VF_pcolorshift: specifies | these hues and chroma level. :scale_vf_chroma_to_sample_chroma: | False, optional | Scale chroma of reference and test vf fields such that average of | binned reference chroma equals that of the binned sample chroma | before calculating hue bin metrics. Returns: :data: | Dictionary with color rendering data: | | - 'St, Sr' : ndarray of test SPDs and corresponding ref. illuminants. | - 'xyz_cct': xyz of white point calculate with cieobs defined for cct calculations in cri_type['cieobs'] | - 'cct, duv': CCT and Duv obtained with cieobs in cri_type['cieobs']['cct'] | - 'xyzti, xyzri': ndarray tristimulus values of test and ref. samples (obtained with with cieobs in cri_type['cieobs']['xyz']) | - 'xyztw, xyzrw': ndarray tristimulus values of test and ref. white points (obtained with with cieobs in cri_type['cieobs']['xyz']) | - 'DEi, DEa': ndarray with individual sample color differences DEi and average DEa between test and ref. | - 'Rf' : ndarray with general color fidelity index values | - 'Rg' : ndarray with color gamut area index values | - 'Rfi' : ndarray with specific (sample) color fidelity indices | - 'Rfhj' : ndarray with local (hue binned) fidelity indices | - 'DEhj' : ndarray with local (hue binned) color differences | - 'Rcshj': ndarray with local chroma shifts indices | - 'Rhshj': ndarray with local hue shifts indices | - 'hue_bin_data': dict with output from _get_hue_bin_data() [see its help for more info] | - 'cri_type': same as input (for reference purposes) | - 'vf' : dictionary with vector field measures and data. | Keys: | - 'Rt' : ndarray with general metameric uncertainty index Rt | - 'Rti' : ndarray with specific metameric uncertainty indices Rti | - 'Rfhj' : ndarray with local (hue binned) fidelity indices | obtained from VF model predictions at color space | pixel coordinates | - 'DEhj' : ndarray with local (hue binned) color differences | (same as above) | - 'Rcshj': ndarray with local chroma shifts indices for vectorfield coordinates | (same as above) | - 'Rhshj': ndarray with local hue shifts indicesfor vectorfield coordinates | (same as above) | - 'Rfi': ndarray with sample fidelity indices for vectorfield coordinates | (same as above) | - 'DEi': ndarray with sample color differences for vectorfield coordinates | (same as above) | - 'hue_bin_data': dict with output from _get_hue_bin_data() for vectorfield coordinates | - 'dataVF': dictionary with output of cri.VFPX.VF_colorshift_model() """ if cri_type is None: cri_type = 'iesrf' if isinstance(cri_type,str): # get dict cri_type = copy.deepcopy(_CRI_DEFAULTS[cri_type]) if hbins is not None: cri_type['rg_pars']['nhbins'] = hbins if start_hue is not None: cri_type['rg_pars']['start_hue'] = start_hue if scalef is not None: cri_type['rg_pars']['normalized_chroma_ref'] = scalef #Calculate color rendering measures for SPDs in St: data,_ = spd_to_cri(St, cri_type = cri_type, out = 'data,hue_bin_data', fit_gamut_ellipse = True) hdata = data['hue_bin_data'] Rfhj, Rcshj, Rhshj = data['Rfhj'], data['Rcshj'], data['Rhshj'] cct = data['cct'] #Calculate Metameric uncertainty and base color shifts: dataVF = VF_colorshift_model(St, cri_type = cri_type, model_type = vf_model_type, cspace = cri_type['cspace'], sampleset = eval(cri_type['sampleset']), pool = False, pcolorshift = vf_pcolorshift, vfcolor = 0) Rf_ = np.array([dataVF[i]['metrics']['Rf'] for i in range(len(dataVF))]).T Rt = np.array([dataVF[i]['metrics']['Rt'] for i in range(len(dataVF))]).T Rti = np.array([dataVF[i]['metrics']['Rti'] for i in range(len(dataVF))][0]) _data_vf = {'Rt' : Rt, 'Rti' : Rti, 'Rf_' : Rf_} # add to dict for output # Get normalized and sliced hue-bin _hj data for plotting: rg_pars = cri_type['rg_pars'] nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [rg_pars[x] for x in sorted(rg_pars.keys())] # Get chroma of samples: if scale_vf_chroma_to_sample_chroma == True: jabt_hj_closed, jabr_hj_closed = hdata['jabt_hj_closed'], hdata['jabr_hj_closed'] Cr_hj_s = (np.sqrt(jabr_hj_closed[:-1,...,1]**2 + jabr_hj_closed[:-1,...,2]**2)).mean(axis=0) # for rescaling vector field average reference chroma #jabtn_hj_closed, jabrn_hj_closed = hdata['jabtn_hj_closed'], hdata['jabrn_hj_closed'] # get vector field data for each source (must be on 2nd dim) jabt_vf = np.transpose(np.array([np.hstack((np.ones(dataVF[i]['fielddata']['vectorfield']['axt'].shape),dataVF[i]['fielddata']['vectorfield']['axt'],dataVF[i]['fielddata']['vectorfield']['bxt'])) for i in range(cct.shape[0])]),(1,0,2)) jabr_vf = np.transpose(np.array([np.hstack((np.ones(dataVF[i]['fielddata']['vectorfield']['axr'].shape),dataVF[i]['fielddata']['vectorfield']['axr'],dataVF[i]['fielddata']['vectorfield']['bxr'])) for i in range(cct.shape[0])]),(1,0,2)) # Get hue bin data for vector field data: hue_bin_data_vf = _get_hue_bin_data(jabt_vf, jabr_vf, start_hue = start_hue, nhbins = nhbins, normalized_chroma_ref = normalized_chroma_ref ) # Rescale chroma of vector field such that it is on average equal to that of the binned samples: if scale_vf_chroma_to_sample_chroma == True: Cr_vf_hj, Cr_vf, Ct_vf = hue_bin_data_vf['Cr_hj'], hue_bin_data_vf['Cr'], hue_bin_data_vf['Ct'] hr_vf, ht_vf = hue_bin_data_vf['hr'], hue_bin_data_vf['ht'] fC = np.nanmean(Cr_hj_s)/np.nanmean(Cr_vf_hj) jabr_vf[...,1], jabr_vf[...,2] = fC * Cr_vf*np.cos(hr_vf), fC * Cr_vf*np.sin(hr_vf) jabt_vf[...,1], jabt_vf[...,2] = fC * Ct_vf*np.cos(ht_vf), fC * Ct_vf*np.sin(ht_vf) # Get new hue bin data for rescaled vector field data: hue_bin_data_vf = _get_hue_bin_data(jabt_vf, jabr_vf, start_hue = start_hue, nhbins = nhbins, normalized_chroma_ref = normalized_chroma_ref ) # Get scale factor and scaling function for Rfx: scale_factor = cri_type['scale']['cfactor'] scale_fcn = cri_type['scale']['fcn'] # Calculate Local color fidelity, chroma and hue shifts for vector field data: (Rcshj_vf, Rhshj_vf, Rfhj_vf, DEhj_vf) = _hue_bin_data_to_rxhj(hue_bin_data_vf, cri_type = cri_type, scale_factor = scale_factor, scale_fcn = scale_fcn) # Get sample color fidelity for vector field data: (Rfi_vf, DEi_vf) = _hue_bin_data_to_rfi(hue_bin_data_vf, cri_type = cri_type, scale_factor = scale_factor, scale_fcn = scale_fcn) # Store in dict: _data_vf.update({'Rfi' : Rfi_vf, 'DEi' : DEi_vf, 'Rcshj' : Rcshj_vf, 'Rhshj' : Rhshj_vf, 'Rfhj' : Rfhj_vf, 'DEhj': DEhj_vf, 'dataVF' : dataVF, 'hue_bin_data' : hue_bin_data_vf}) # Add to main dictionary: data['vf'] = _data_vf return data
def PX_colorshift_model(Jabt, Jabr, jab_ranges=None, jab_deltas=None, limit_grid_radius=0): """ Pixelates the color space and calculates the color shifts in each pixel. Args: :Jabt: | ndarray with color coordinates under the (single) test SPD. :Jabr: | ndarray with color coordinates under the (single) reference SPD. :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. :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: | dict with the following keys: | - 'Jab': dict with with ndarrays for: | Jabt, Jabr, DEi, DEi_ab (only ab-coordinates), DEa (mean) | and DEa_ab | - 'vshifts': dict with: | * 'vectorshift': ndarray with vector shifts between average | Jabt and Jabr for each pixel | * 'vectorshift_ab': ndarray with vector shifts averaged | over J for each pixel | * 'vectorshift_ab_J0': ndarray with vector shifts averaged | over J for each pixel of J=0 plane. | * 'vectorshift_len': length of 'vectorshift' | * 'vectorshift_ab_len': length of 'vectorshift_ab' | * 'vectorshift_ab_J0_len': length of 'vectorshift_ab_J0' | * 'vectorshift_len_DEnormed': length of 'vectorshift' | normalized to 'DEa' | * 'vectorshift_ab_len_DEnormed': length of 'vectorshift_ab' | normalized to 'DEa_ab' | * 'vectorshift_ab_J0_len_DEnormed': length of 'vectorshift_ab_J0' | normalized to 'DEa_ab' | - 'pixeldata': dict with pixel info: | * 'grid' ndarray with coordinates of all pixel centers. | * 'idx': list[int] with pixel index for each non-empty pixel | * 'Jab': ndarray with center coordinates of non-empty pixels | * 'samplenrs': list[list[int]] with sample numbers belong to | each non-empty pixel | * 'IDs: summarizing list, | with column order: 'idxp, jabp, samplenrs' | - 'fielddata' : dict with dicts containing data on the calculated | vector-field and circle-fields | * 'vectorfield': dict with ndarrays for the ab-coordinates | under the ref. (axr, bxr) and test (axt, bxt) illuminants, | centered at the pixel centers corresponding to the ab-coordinates of the reference illuminant. """ # get pixelIDs of all samples under ref. conditions: gridp, idxp, jabp, pixelsamplenrs, pixelIDs = get_pixel_coordinates( Jabr, jab_ranges=jab_ranges, jab_deltas=jab_deltas, limit_grid_radius=limit_grid_radius) # get average Jab coordinates for each pixel: Npixels = len(idxp) # number of non-empty pixels Jabr_avg = np.zeros((gridp.shape[0], 3)) Jabr_avg.fill(np.nan) Jabt_avg = Jabr_avg.copy() for i in range(Npixels): Jabr_avg[idxp[i], :] = Jabr[pixelsamplenrs[i], :].mean(axis=0) Jabt_avg[idxp[i], :] = Jabt[pixelsamplenrs[i], :].mean(axis=0) #jabtemp = Jabr[pixelsamplenrs[i],:] #jabtempm = Jabr_avg[idxp[i],:] # calculate Jab vector shift: vectorshift = Jabt_avg - Jabr_avg # calculate ab vector shift: uabs = gridp[gridp[:, 0] == 0, 1:3] #np.unique(gridp[:,1:3],axis=0) vectorshift_ab_J0 = np.zeros((uabs.shape[0], 2)) vectorshift_ab_J0.fill(np.nan) vectorshift_ab = np.zeros((vectorshift.shape[0], 2)) vectorshift_ab.fill(np.nan) for i in range(uabs.shape[0]): cond = (gridp[:, 1:3] == uabs[i, :]).all(axis=1) if cond.any() & np.logical_not( np.isnan(vectorshift[cond, 1:3]).all() ): #last condition is to avoid warning of taking nanmean of empty slice when all are NaNs vectorshift_ab_J0[i, :] = np.nanmean(vectorshift[cond, 1:3], axis=0) vectorshift_ab[cond, :] = np.nanmean(vectorshift[cond, 1:3], axis=0) # Calculate length of shift vectors: vectorshift_len = np.sqrt((vectorshift**2).sum(axis=vectorshift.ndim - 1)) vectorshift_ab_len = np.sqrt( (vectorshift_ab**2).sum(axis=vectorshift_ab.ndim - 1)) vectorshift_ab_J0_len = np.sqrt( (vectorshift_ab_J0**2).sum(axis=vectorshift_ab_J0.ndim - 1)) # Calculate average DE for normalization of vectorshifts DEi_Jab_avg = np.sqrt(((Jabt - Jabr)**2).sum(axis=Jabr.ndim - 1)) DE_Jab_avg = DEi_Jab_avg.mean(axis=0) DEi_ab_avg = np.sqrt( ((Jabt[..., 1:3] - Jabr[..., 1:3])**2).sum(axis=Jabr[..., 1:3].ndim - 1)) DE_ab_avg = DEi_ab_avg.mean(axis=0) # calculate vectorfield: axr = uabs[:, 0, None] bxr = uabs[:, 1, None] axt = axr + vectorshift_ab_J0[:, 0, None] bxt = bxr + vectorshift_ab_J0[:, 1, None] data = { 'Jab': { 'Jabr': Jabr_avg, 'Jabt': Jabt_avg, 'DEi': DEi_Jab_avg, 'DEi_ab': DEi_ab_avg, 'DEa': DE_Jab_avg, 'DEa_ab': DE_ab_avg }, 'vshifts': { 'vectorshift': vectorshift, 'vectorshift_ab': vectorshift_ab, 'vectorshift_ab_J0': vectorshift_ab_J0, 'vectorshift_len': vectorshift_len, 'vectorshift_ab_len': vectorshift_ab_len, 'vectorshift_ab_J0_len': vectorshift_ab_J0_len, 'vectorshift_len_DEnormed': vectorshift_len / DE_Jab_avg, 'vectorshift_ab_len_DEnormed': vectorshift_ab_len / DE_ab_avg, 'vectorshift_ab_J0_len_DEnormed': vectorshift_ab_J0_len / DE_ab_avg }, 'pixeldata': { 'grid': gridp, 'idx': idxp, 'Jab': jabp, 'samplenrs': pixelsamplenrs, 'IDs': pixelIDs }, 'fielddata': { 'vectorfield': { 'axr': axr, 'bxr': bxr, 'axt': axt, 'bxt': bxt } } } return data
def plot_shift_data(data, fieldtype = 'vectorfield', scalef = _VF_MAXR, color = 'k', \ axtype = 'polar', ax = None, \ hbins = 10, start_hue = 0.0, bin_labels = '#', plot_center_lines = True, \ plot_axis_labels = False, plot_edge_lines = False, plot_bin_colors = True, \ force_CVG_layout = True): """ Plots vector or circle fields generated by VFcolorshiftmodel() or PXcolorshiftmodel(). Args: :data: | dict generated by VFcolorshiftmodel() or PXcolorshiftmodel() | Must contain 'fielddata'- key, which is a dict with possible keys: | - key: 'vectorfield': ndarray with vector field data | - key: 'circlefield': ndarray with circle field data :color: | 'k', optional | Color for plotting the vector-fields. :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. :hbins: | 16 or ndarray with sorted hue bin centers (°), optional :start_hue: | _VF_MAXR, 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. :force_CVG_layout: | False or True, optional | True: Force plot of basis of CVG. Returns: :returns: | figCVG, hax, cmap | :figCVG: handle to CVG figure | :hax: handle to CVG axes | :cmap: list with rgb colors for hue bins | (for use in other plotting fcns) """ # Plot basis of CVG: figCVG, hax, cmap = plot_hue_bins(hbins=hbins, axtype=axtype, ax=ax, plot_center_lines=plot_center_lines, plot_edge_lines=plot_edge_lines, plot_bin_colors=plot_bin_colors, scalef=scalef, force_CVG_layout=force_CVG_layout, bin_labels=bin_labels) # plot vector field: if data is not None: if fieldtype is not None: vf = data['fielddata'][fieldtype] if axtype == 'polar': if fieldtype == 'vectorfield': vfrtheta = math.positive_arctan(vf['axr'], vf['bxr'], htype='rad') vfrr = np.sqrt(vf['axr']**2 + vf['bxr']**2) hax.quiver(vfrtheta, vfrr, vf['axt'] - vf['axr'], vf['bxt'] - vf['bxr'], headlength=3, color=color, angles='uv', scale_units='y', scale=2, linewidth=0.5) else: vfttheta = math.positive_arctan(vf['axt'], vf['bxt'], htype='rad') vfrtheta = math.positive_arctan(vf['axr'], vf['bxr'], htype='rad') vftr = np.sqrt(vf['axt']**2 + vf['bxt']**2) dh = (math.angle_v1v2(np.hstack((vf['axt'], vf['bxt'])), np.hstack((vf['axr'], vf['bxr'])), htype='deg')[:, None]) #hue shift dh = dh / np.nanmax(dh) plt.set_cmap('jet') hax.scatter(vfttheta, vftr, s=100 * dh, c=dh, linestyle='None', marker='o', norm=None) hax.set_ylim([0, 1.1 * scalef]) else: if fieldtype == 'vectorfield': hax.quiver(vf['axr'], vf['bxr'], vf['axt'] - vf['axr'], vf['bxt'] - vf['bxr'], headlength=1, color=color, angles='uv', scale_units='xy', scale=1, linewidth=0.5) else: hax.plot(vf['axr'], vf['bxr'], color=color, marker='.', linestyle='None') return figCVG, hax, 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 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
def __abs__(self): return np.sqrt(self * self)
def get_pixel_coordinates(jab, jab_ranges=None, jab_deltas=None, limit_grid_radius=0): """ Get pixel coordinates corresponding to array of jab color coordinates. Args: :jab: | ndarray of color coordinates :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. :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: | gridp, idxp, jabp, samplenrs, samplesIDs | - :gridp: ndarray with coordinates of all pixel centers. | - :idxp: list[int] with pixel index for each non-empty pixel | - :jabp: ndarray with center color coordinates of non-empty pixels | - :samplenrs: list[list[int]] with sample numbers belong to each | non-empty pixel | - :sampleIDs: summarizing list, | with column order: 'idxp, jabp, samplenrs' """ if jab_deltas is None: jab_deltas = np.array([_VF_DELTAR, _VF_DELTAR, _VF_DELTAR]) if jab_ranges is None: jab_ranges = np.vstack( ([0, 100, jab_deltas[0] ], [-_VF_MAXR, _VF_MAXR + jab_deltas[1], jab_deltas[1]], [-_VF_MAXR, _VF_MAXR + jab_deltas[2], jab_deltas[2]])) # Get pixel grid: gridp = generate_grid(jab_ranges=jab_ranges, limit_grid_radius=limit_grid_radius) # determine pixel coordinates of each sample in jab: samplesIDs = [] for idx in range(gridp.shape[0]): # get pixel coordinates: jp = gridp[idx, 0] ap = gridp[idx, 1] bp = gridp[idx, 2] #Cp = np.sqrt(ap**2+bp**2) if type(jab_deltas) == np.ndarray: sampleID = np.where( ((np.abs(jab[..., 0] - jp) <= jab_deltas[0] / 2) & (np.abs(jab[..., 1] - ap) <= jab_deltas[1] / 2) & (np.abs(jab[..., 2] - bp) <= jab_deltas[2] / 2))) else: sampleID = np.where( (np.sqrt((jab[..., 0] - jp)**2 + (jab[..., 1] - ap)**2 + (jab[..., 2] - bp)**2) <= jab_deltas / 2)) if (sampleID[0].shape[0] > 0): samplesIDs.append( np.hstack((idx, np.array([jp, ap, bp]), sampleID[0]))) idxp = [np.int(samplesIDs[i][0]) for i in range(len(samplesIDs))] jabp = np.vstack([samplesIDs[i][1:4] for i in range(len(samplesIDs))]) samplenrs = [ np.array(samplesIDs[i][4:], dtype=int).tolist() for i in range(len(samplesIDs)) ] return gridp, idxp, jabp, samplenrs, samplesIDs
def minimizebnd(fun, x0, args=(), method = 'Nelder-Mead', use_bnd = True, \ bounds = (None,None) , options = None, \ x0_vsize = None, x0_keys = None, **kwargs): """ | Minimization function that allows for bounds on any type of method in | SciPy's minimize function by transforming the parameters values | (see Matlab's fminsearchbnd). | Starting values, and lower and upper bounds can also be provided as a dict. Args: :x0: | parameter starting values | If x0_keys is None then :x0: is vector else, :x0: is dict and | x0_size should be provided with length/size of values for each of | the keys in :x0: to convert it to a vector. :use_bnd: | True, optional | False: omits bounds and defaults to regular minimize function. :bounds: | (lower, upper), optional | Tuple of lists or dicts (x0_keys is None) of lower and upper bounds | for each of the parameters values. :kwargs: | allows input for other type of arguments (e.g. in OutputFcn) Note: For other input arguments, see ?scipy.optimize.minimize() Returns: :res: | dict with minimize() output. | Additionally, function value, fval, of solution is also in :res:, | as well as a vector or dict (if x0 was dict) | with final solutions (res['x']) """ # Convert dict to vec: if isinstance(x0, dict): x0, vsize = vec_to_dict(dic=x0, vsize=x0_vsize, keys=x0_keys) if use_bnd == False: res = sp.optimize.minimize(fun, x0, args=args, method=method, options=options, **kwargs) res['fval'] = fun(res['x'], *args) if x0_keys is None: res['x_final'] = res['x'] else: res['x_final'] = vec_to_dict(vec=res['x'], vsize=x0_vsize, keys=x0_keys) return res else: LB, UB = bounds # Convert dict to vec: if isinstance(LB, dict): LB, vsize = vec_to_dict(dic=LB, vsize=x0_vsize, keys=x0_keys) if isinstance(UB, dict): UB, vsize = vec_to_dict(dic=UB, vsize=x0_vsize, keys=x0_keys) #size checks xsize = x0.shape x0 = x0.flatten() n = x0.shape[0] if LB is None: LB = -np.inf * np.ones(n) else: LB = LB.flatten() if UB is None: UB = np.inf * np.ones(n) else: UB = UB.flatten() if (n != LB.shape[0]) | (n != UB.shape[0]): raise Exception( 'minimizebnd(): x0 is incompatible in size with either LB or UB.' ) #set default options if necessary if options is None: options = {} # stuff into a struct to pass around params = {} params['args'] = args params['LB'] = LB params['UB'] = UB params['fun'] = fun params['n'] = n params['OutputFcn'] = None # % 0 --> unconstrained variable # % 1 --> lower bound only # % 2 --> upper bound only # % 3 --> dual finite bounds # % 4 --> fixed variable params['BoundClass'] = np.zeros(n) for i in range(n): k = np.isfinite(LB[i]) + 2 * np.isfinite(UB[i]) params['BoundClass'][i] = k if (k == 3) & (LB[i] == UB[i]): params['BoundClass'][i] = 4 # transform starting values into their unconstrained # surrogates. Check for infeasible starting guesses. x0u = x0 k = 0 for i in range(n): if params['BoundClass'][i] == 1: # lower bound only if x0[i] <= LB[i]: # infeasible starting value. Use bound. x0u[k] = 0 else: x0u[k] = np.sqrt(x0[i] - LB[i]) elif params['BoundClass'][i] == 2: # upper bound only if x0[i] >= UB[i]: # infeasible starting value. use bound. x0u[k] = 0 else: x0u[k] = np.sqrt(UB[i] - x0[i]) elif params['BoundClass'][i] == 3: # lower and upper bounds if x0[i] <= LB[i]: # infeasible starting value x0u[k] = -np.pi / 2 elif x0[i] >= UB[i]: # infeasible starting value x0u[k] = np.pi / 2 else: x0u[k] = 2 * (x0[i] - LB[i]) / (UB[i] - LB[i]) - 1 # shift by 2*pi to avoid problems at zero in fminsearch #otherwise, the initial simplex is vanishingly small x0u[k] = 2 * np.pi + np.arcsin( np.hstack((-1, np.hstack((1, x0u[k])).min())).max()) elif params['BoundClass'][i] == 0: # unconstrained variable. x0u(i) is set. x0u[k] = x0[i] if params['BoundClass'][i] != 4: # increment k k += 1 else: # fixed variable. drop it before fminsearch sees it. # k is not incremented for this variable. pass # if any of the unknowns were fixed, then we need to shorten x0u now. if k <= n: x0u = x0u[:k + 1] # were all the variables fixed? if x0u.shape[0] == 0: # All variables were fixed. quit immediately, setting the # appropriate parameters, then return. # undo the variable transformations into the original space x = xtransform(x0u, params) # final reshape x = x.reshape(xsize) # stuff fval with the final value fval = params['fun'](x, *params['args']) # minimize was not called output = {'success': False} output['x'] = x output['x_final'] = x output['iterations'] = 0 output['funcount'] = 1 output['algorithm'] = method output[ 'message'] = 'All variables were held fixed by the applied bounds' output['status'] = 0 # return with no call at all to fminsearch return output # Check for an outputfcn. If there is any, then substitute my # own wrapper function. # Use a nested function as the OutputFcn wrapper def outfun_wrapper(x, **kwargs): # we need to transform x first xtrans = xtransform(x, params) # then call the user supplied OutputFcn stop = params['OutputFcn'](xtrans, **kwargs) return stop if 'OutputFcn' in options: if options['OutputFcn'] is not None: params['OutputFcn'] = options['OutputFcn'] options['OutputFcn'] = outfun_wrapper # now we can call minimize, but with our own # intra-objective function. res = sp.optimize.minimize(intrafun, x0u, args=params, method=method, options=options) #[xu,fval,exitflag,output] = fminsearch(@intrafun,x0u,options,params); # get function value: fval = intrafun(res['x'], params) # undo the variable transformations into the original space x = xtransform(res['x'], params) # final reshape x = x.reshape(xsize) res['fval'] = fval res['x'] = x #overwrite x in res to unconstrained format if x0_keys is None: res['x_final'] = res['x'] else: res['x_final'] = vec_to_dict(vec=res['x'], vsize=x0_vsize, keys=x0_keys) return res