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 plot(v, origin=None, ax=None, color='k', marker='.', linestyle='-', **kwargs): """ Plot a vector from origin. Args: :v: | vec3 vector. :origin: | vec3 vector with same size attributes as in :v:. :ax: | None, optional | axes handle. | If None, create new figure with axes ax. :color: | 'k', optional | color specifier. :marker: | '.', optional | marker specifier. :linestyle: | '-', optional | linestyle specifier :**kwargs: | other keyword specifiers for plot. Returns: :ax: | handle to figure axes. """ if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') if origin is None: origin = vec3(np.zeros(v.x.shape), np.zeros(v.x.shape), np.zeros(v.x.shape)) ax.plot(np.hstack([origin.x, v.x]), np.hstack([origin.y, v.y]), np.hstack([origin.z, v.z]), color=color, marker=marker, **kwargs) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('z') return ax
def v_to_cik(v, inverse=False): """ Calculate 2x2 '(covariance matrix)^-1' elements cik Args: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] :inverse: | If True: return inverse of cik. Returns: :cik: 'Nx2x2' (covariance matrix)^-1 Notes: | cik is not actually a covariance matrix, | only for a Gaussian or normal distribution! """ v = np.atleast_2d(v) g11 = (1 / v[:, 0] * np.cos(v[:, 4]))**2 + (1 / v[:, 1] * np.sin(v[:, 4]))**2 g22 = (1 / v[:, 0] * np.sin(v[:, 4]))**2 + (1 / v[:, 1] * np.cos(v[:, 4]))**2 g12 = (1 / v[:, 0]**2 - 1 / v[:, 1]**2) * np.sin(v[:, 4]) * np.cos(v[:, 4]) cik = np.zeros((g11.shape[0], 2, 2)) for i in range(g11.shape[0]): cik[i, :, :] = np.vstack((np.hstack( (g11[i], g12[i])), np.hstack((g12[i], g22[i])))) if inverse == True: cik[i, :, :] = np.linalg.inv(cik[i, :, :]) return cik
def _complete_ldt_lid(LDT, Isym=4): """ Convert LDT LID map with Isym symmetry to a 'full' map with phi: [0,360] and theta: [0,180]. """ cangles = LDT['h_angs'] tangles = LDT['v_angs'] candela_2d = LDT['candela_2d'] if Isym == 4: # complete cangles: a = candela_2d.copy().T b = np.hstack((a, a[:, (a.shape[1] - 2)::-1])) c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1])) candela_2d_0C360 = np.hstack((c, c[:, :1])) cangles = np.hstack( (cangles, cangles[1:] + 90, cangles[1:] + 180, cangles[1:] + 270)) # complete tangles: a = candela_2d_0C360.copy() b = np.vstack((a, np.zeros(a.shape)[1:, :])) tangles = np.hstack((tangles, tangles[1:] + 90)) candela_2d = b elif Isym == -4: # complete cangles: a = candela_2d.copy().T b = np.hstack((a, a[:, (a.shape[1] - 2)::-1])) c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1])) candela_2d_0C360 = np.hstack((c, c[:, :1])) cangles = np.hstack( (cangles, -cangles[(cangles.shape[0] - 2)::-1] + 180)) cangles = np.hstack( (cangles, -cangles[(cangles.shape[0] - 2):0:-1] + 360)) cangles = np.hstack((cangles, cangles[:1])) # complete tangles: a = candela_2d_0C360.copy() b = np.vstack((a, np.zeros(a.shape)[1:, :])) tangles = np.hstack( (tangles, -tangles[(tangles.shape[0] - 2)::-1] + 180)) candela_2d = b else: raise Exception( 'complete_ldt_lid(): Other "Isym" than "4", not yet implemented (31/10/2018).' ) LDT['map'] = {'thetas': tangles} LDT['map']['phis'] = cangles LDT['map']['values'] = candela_2d.T return LDT
def cik_to_v(cik, xyc=None, inverse=False): """ Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik Args: :cik: '2x2xN' (covariance matrix)^-1 Returns: :v: | (Nx5) np.ndarray | ellipse parameters [Rmax,Rmin,xc,yc,theta] Notes: | cik is not actually the inverse covariance matrix, | only for a Gaussian or normal distribution! """ if inverse == True: for i in np.arange(cik.shape[0]): cik[i, :, :] = np.linalg.inv(cik[i, :, :]) g11 = cik[:, 0, 0] g22 = cik[:, 1, 1] g12 = cik[:, 0, 1] theta2 = 1 / 2 * np.arctan2(2 * g12, (g11 - g22)) theta = theta2 + (np.pi / 2) * (g12 < 0) theta2 = theta cottheta = np.cos(theta) / np.sin(theta) #np.cot(theta) cottheta[np.isinf(cottheta)] = 0 a = 1 / np.sqrt((g22 + g12 * cottheta)) b = 1 / np.sqrt((g11 - g12 * cottheta)) v = np.vstack((a, b, np.zeros(a.shape), np.zeros(a.shape), theta)).T # add center coordinates: if xyc is not None: v[:, 2:4] = xyc return v
def fit_ellipse(xy): """ Fit an ellipse to supplied data points. Args: :xy: | coordinates of points to fit (Nx2 array) Returns: :v: | vector with ellipse parameters [Rmax,Rmin, xc,yc, theta] """ # 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] # 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 xc = ((c * d - b * f) / num) + center[0] yc = ((a * f - b * d) / num) + center[1] # get ellipse orientation: theta = np.arctan2(np.array(2 * b), np.array((a - c))) / 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) return np.hstack((a, b, xc, yc, theta))
def ndset(F): """ Finds the nondominated set of a set of objective points. Args: F: | a m x mu ndarray with mu points and m objectives Returns: :ispar: | a mu-length vector with true in the nondominated points """ mu = F.shape[1] #number of points # The idea is to compare each point with the other ones f1 = np.transpose(F[..., None], axes=[0, 2, 1]) #puts in the 3D direction f1 = np.repeat(f1, mu, axis=1) f2 = np.repeat(F[..., None], mu, axis=2) # Now, for the ii-th slice, the ii-th individual is compared with all of the # others at once. Then, the usual operations of domination are checked # Checks where f1 dominates f2 aux1 = (f1 <= f2).all(axis=0, keepdims=True) aux2 = (f1 < f2).any(axis=0, keepdims=True) auxf1 = np.logical_and(aux1, aux2) # Checks where f1 is dominated by f2 aux1 = (f1 >= f2).all(axis=0, keepdims=True) aux2 = (f1 > f2).any(axis=0, keepdims=True) auxf2 = np.logical_and(aux1, aux2) # dom will be a 3D matrix (1 x mu x mu) such that, for the ii-th slice, it # will contain +1 if fii dominates the current point, -1 if it is dominated # by it, and 0 if they are incomparable dom = np.zeros((1, mu, mu), dtype=int) dom[auxf1] = 1 dom[auxf2] = -1 # Finally, the slices with no -1 are nondominated ispar = (dom != -1).all(axis=1) ispar = ispar.flatten() return ispar
def xtransform(x, params): """ Converts unconstrained variables into their original domains. """ xtrans = np.zeros((params['n'])) # k allows some variables to be fixed, thus dropped from the optimization. k = 0 for i in np.arange(params['n']): if params['BoundClass'][i] == 1: # lower bound only xtrans[i] = params['LB'][i] + x[k]**2 elif params['BoundClass'][i] == 2: # upper bound only xtrans[i] = params['UB'][i] - x[k]**2 elif params['BoundClass'][i] == 3: # lower and upper bounds xtrans[i] = (np.sin(x[k]) + 1) / 2 xtrans[i] = xtrans[i] * (params['UB'][i] - params['LB'][i]) + params['LB'][i] # just in case of any floating point problems xtrans[i] = np.hstack( (params['LB'][i], np.hstack( (params['UB'][i], xtrans[i])).min())).max() elif params['BoundClass'][i] == 4: # fixed variable, bounds are equal, set it at either bound xtrans[i] = params['LB'][i] elif params['BoundClass'][i] == 0: # unconstrained variable. xtrans[i] = x[k] if params['BoundClass'][i] != 4: k += 1 return xtrans
def mahalanobis2(x, y=None, mu=None, sigmainv=None): """ Evaluate the squared mahalanobis distance with center mu and shape and orientation determined by sigmainv. Args: :x: | scalar or list or ndarray (.ndim = 1 or 2) with x(y)-coordinates at which to evaluate the mahalanobis distance squared. :y: | None or scalar or list or ndarray (.ndim = 1) with y-coordinates at which to evaluate the mahalanobis distance squared, optional. | If :y: is None, :x: should be a 2d array. :mu: | None or ndarray (.ndim = 2) with center coordinates of the mahalanobis ellipse, optional. | None defaults to ndarray([0,0]). :sigmainv: | None or ndarray with 'inverse covariance matrix', optional | Determines the shape and orientation of the PD. | None default to np.eye(2). Returns: :returns: | ndarray with magnitude of mahalanobis2(x,y) """ if mu is None: mu = np.zeros(2) if sigmainv is None: sigmainv = np.eye(2) x = np2d(x) if y is not None: x = x - mu[0] # center data on mu y = np2d(y) - mu[1] # center data on mu else: x = x - mu # center data on mu x, y = asplit(x) return (sigmainv[0, 0] * (x**2.0) + sigmainv[1, 1] * (y**2.0) + 2.0 * sigmainv[0, 1] * (x * y))
def rgb_to_spec_smits(rgb, intent='rfl', bitdepth=8, wlr=_WL3, rgb2spec=None): """ Convert an array of RGB values to a spectrum using a Smits like conversion as implemented in Mitsuba. Args: :rgb: | ndarray of list of rgb values :intent: | 'rfl' (or 'spd'), optional | type of requested spectrum conversion . :bitdepth: | 8, optional | bit depth of rgb values :wlr: | _WL3, optional | desired wavelength (nm) range of spectrum. :rgb2spec: | None, optional | Dict with base spectra for white, cyan, magenta, yellow, blue, green and red for each intent. | If None: use _BASESPEC_SMITS. Returns: :spec: | ndarray with spectrum or spectra (one for each rgb value, first row are the wavelengths) """ if isinstance(rgb, list): rgb = np.atleast_2d(rgb) if rgb.max() > 1: rgb = rgb / (2**bitdepth - 1) if rgb2spec is None: rgb2spec = _BASESPEC_SMITS if not np.array_equal(rgb2spec['wlr'], getwlr(wlr)): rgb2spec = _convert_to_wlr(entries=copy.deepcopy(rgb2spec), wlr=wlr) spec = np.zeros((rgb.shape[0], rgb2spec['wlr'].shape[0])) for i in range(rgb.shape[0]): spec[i, :] = _fromLinearRGB(rgb[i, :], intent=intent, rgb2spec=rgb2spec, wlr=wlr) return np.vstack((rgb2spec['wlr'], spec))
def 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 _fromLinearRGB(rgb, intent='rfl', rgb2spec=_BASESPEC_SMITS, wlr=_WL3): r, g, b = rgb result = np.zeros((rgb2spec['wlr'].shape[0], )) if (r <= g) & (r <= b): # Compute reflectance spectrum with 'r' as minimum result += r * rgb2spec[intent]['white'] if (g <= b): result += (g - r) * rgb2spec[intent]['cyan'] result += (b - g) * rgb2spec[intent]['blue'] else: result += (b - r) * rgb2spec[intent]['cyan'] result += (g - b) * rgb2spec[intent]['green'] elif (g <= r) & (g <= b): # Compute reflectance spectrum with 'g' as minimum result += g * rgb2spec[intent]['white'] if (r <= b): result += (r - g) * rgb2spec[intent]['magenta'] result += (b - r) * rgb2spec[intent]['blue'] else: result += (b - g) * rgb2spec[intent]['magenta'] result += (r - b) * rgb2spec[intent]['red'] else: # Compute reflectance spectrum with 'b' as minimum result += b * rgb2spec[intent]['white'] if (r <= g): result += (r - b) * rgb2spec[intent]['yellow'] result += (g - r) * rgb2spec[intent]['green'] else: result += (g - b) * rgb2spec[intent]['yellow'] result += (r - g) * rgb2spec[intent]['red'] result *= rgb2spec[intent]['scalefactor'] return np.clip(result, 0, None) # no negative values allowed
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.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 = vec_to_dict(dic=x0, vsize=x0_vsize, keys=x0_keys) if use_bnd == False: res = minimize(fun, x0, args=args, 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 = vec_to_dict(dic=LB, vsize=x0_vsize, keys=x0_keys) if isinstance(LB, dict): UB = 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 np.arange(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 np.arange(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] = sqrt(UB[i] - x0[i]) elif params['BoundClass'][i] == 2: # 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.asin( 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['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 = 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
def Ydlep_to_xyz(Ydlep, cieobs = _CIEOBS, xyzw = _COLORTF_DEFAULT_WHITE_POINT, **kwargs): """ Convert Y, dominant (complementary) wavelength and excitation purity to XYZ tristimulus values. Args: :Ydlep: | ndarray with Y, dominant (complementary) wavelength and excitation purity :xyzw: | None or narray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating spectrum locus coordinates. Returns: :xyz: | ndarray with tristimulus values """ Ydlep3 = np3d(Ydlep).copy() # flip axis so that shortest dim is on axis0 (save time in looping): if Ydlep3.shape[0] < Ydlep3.shape[1]: axes12flipped = True Ydlep3 = Ydlep3.transpose((1,0,2)) else: axes12flipped = False # convert xyzw to Yxyw: Yxyw = xyz_to_Yxy(xyzw) Yxywo = Yxyw.copy() # get spectrum locus Y,x,y and wavelengths: SL = _CMF[cieobs]['bar'] wlsl = SL[0,None].T Yxysl = xyz_to_Yxy(SL[1:4].T)[:,None] # center on xyzw: Yxysl = Yxysl - Yxyw Yxyw = Yxyw - Yxyw #split: Y, dom, pur = asplit(Ydlep3) Yw,xw,yw = asplit(Yxyw) Ywo,xwo,ywo = asplit(Yxywo) Ysl,xsl,ysl = asplit(Yxysl) # loop over longest dim: x = np.zeros(Y.shape) y = x.copy() for i in range(Ydlep3.shape[1]): # find closest wl's to dom: #wlslb,wlib = meshblock(wlsl,np.abs(dom[i,:])) #abs because dom<0--> complemtary wl wlib,wlslb = np.meshgrid(np.abs(dom[:,i]),wlsl) dwl = np.abs(wlslb-wlib) q1 = dwl.argmin(axis=0) # index of closest wl dwl[q1] = 10000.0 q2 = dwl.argmin(axis=0) # index of second closest wl # calculate x,y of dom: x_dom_wl = xsl[q1,0] + (xsl[q2,0] - xsl[q1,0])*(np.abs(dom[:,i]) - wlsl[q1,0])/(wlsl[q2,0] - wlsl[q1,0]) # calculate x of dom. wl y_dom_wl = ysl[q1,0] + (ysl[q2,0] - ysl[q1,0])*(np.abs(dom[:,i]) - wlsl[q1,0])/(wlsl[q2,0] - wlsl[q1,0]) # calculate y of dom. wl # calculate x,y of test: d_wl = (x_dom_wl**2.0 + y_dom_wl**2.0)**0.5 # distance from white point to dom d = pur[:,i]*d_wl hdom = math.positive_arctan(x_dom_wl,y_dom_wl,htype = 'deg') x[:,i] = d*np.cos(hdom*np.pi/180.0) y[:,i] = d*np.sin(hdom*np.pi/180.0) # complementary: pc = np.where(dom[:,i] < 0.0) hdom[pc] = hdom[pc] - np.sign(dom[:,i][pc] - 180.0)*180.0 # get positive hue angle # calculate intersection of line through white point and test point and purple line: xy = np.vstack((x_dom_wl,y_dom_wl)).T xyw = np.vstack((xw,yw)).T xypl1 = np.vstack((xsl[0,None],ysl[0,None])).T xypl2 = np.vstack((xsl[-1,None],ysl[-1,None])).T da = (xy-xyw) db = (xypl2-xypl1) dp = (xyw - xypl1) T = np.array([[0.0, -1.0], [1.0, 0.0]]) dap = np.dot(da,T) denom = np.sum(dap * db,axis=1,keepdims=True) num = np.sum(dap * dp,axis=1,keepdims=True) xy_linecross = (num/denom) *db + xypl1 d_linecross = np.atleast_2d((xy_linecross[:,0]**2.0 + xy_linecross[:,1]**2.0)**0.5).T[:,0] x[:,i][pc] = pur[:,i][pc]*d_linecross[pc]*np.cos(hdom[pc]*np.pi/180) y[:,i][pc] = pur[:,i][pc]*d_linecross[pc]*np.sin(hdom[pc]*np.pi/180) Yxy = np.dstack((Ydlep3[:,:,0],x + xwo, y + ywo)) if axes12flipped == True: Yxy = Yxy.transpose((1,0,2)) else: Yxy = Yxy.transpose((0,1,2)) return Yxy_to_xyz(Yxy).reshape(Ydlep.shape)
def 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, \ 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. :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)] # initializing the figure cmap = None if (ax == None) or (ax == 'new'): fig = plt.figure() newfig = True else: 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), 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) hy = r * np.sin(theta) if bin_labels is not None: hxv = np.vstack((np.zeros(hbincenters.shape), 1.3 * scalef * np.cos(hbincenters))) hyv = np.vstack((np.zeros(hbincenters.shape), 1.3 * 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))) # 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.84, 0.9))) #c = [abs(c[0]),abs(c[1]),abs(c[2])] # ensure all positive elements if i == 0: cmap = [c] else: cmap.append(c) if axtype == 'polar': if plot_edge_lines == True: ax.plot(edges[:, i], r[:, i] * 1.2, color='grey', marker='None', linestyle=':', linewidth=3, 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=2) else: ax.plot(theta[:, i], r[:, i], color=c, marker='o', linestyle='-', linewidth=3, markersize=10) if plot_bin_colors == True: bar = ax.bar(dM[i], r[1, i], width=dt[i], color=c, alpha=0.15) if bin_labels is not None: ax.text(hbincenters[i], 1.3 * scalef, bin_labels[i], fontsize=12, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.3) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: if plot_edge_lines == True: ax.plot(hxe[:, i], hye[:, i], color='grey', marker='None', linestyle=':', linewidth=3, 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=2) else: ax.plot(hx[:, i], hy[:, i], color=c, marker='o', linestyle='-', linewidth=3, markersize=10) if bin_labels is not None: ax.text(hxv[1, i], hyv[1, i], bin_labels[i], fontsize=12, horizontalalignment='center', verticalalignment='center', color=np.array([1, 1, 1]) * 0.3) ax.axis(1.1 * np.array( [hxv.min(), hxv.max(), hyv.min(), hyv.max()])) if plot_axis_labels == False: ax.set_xticklabels([]) ax.set_yticklabels([]) else: plt.xlabel("a'") plt.ylabel("b'") plt.plot(0, 0, color='k', marker='o', linestyle=None) return plt.gcf(), plt.gca(), cmap
def __init__(self, spd = None, wl = None, ax0iswl = True, dtype = 'S', \ wl_new = None, interp_method = 'auto', negative_values_allowed = False, \ norm_type = None, norm_f = 1,\ header = None, sep = ','): """ Initialize instance of SPD. Args: :spd: | None or ndarray or str, optional | If None: self.value is initialized with zeros. | If str: spd contains filename. | If ndarray: ((wavelength, spectra)) or (spectra). | If latter, :wl: should contain the wavelengths. :wl: | None or ndarray, optional | Wavelengths. | Either specified as a 3-vector ([start, stop, spacing]) | or as full wavelength array. :a0iswl: | True, optional | Signals that first axis of :spd: contains wavelengths. :dtype: | 'S', optional | Type of spectral object (e.g. 'S' for source spectrum, 'R' for reflectance spectra, etc.) | See SPD._INTERP_TYPES for more options. | This is used to automatically determine the correct kind of interpolation method according to CIE15-2004. :wl_new: | None or ndarray with wavelength range, optional | If None: don't interpolate, else perform interpolation. :interp_method: | - 'auto', optional | If 'auto': method is determined based on :dtype: :negative-values_allowed: | False, optional (for cie_interp()) | Spectral data can not be negative. Values < 0 are therefore clipped when set to False. :norm_type: | None or str, optional | - 'lambda': make lambda in norm_f equal to 1 | - 'area': area-normalization times norm_f | - 'max': max-normalization times norm_f :norm_f: | 1, optional | Normalization factor determines size of normalization | for 'max' and 'area' | or which wavelength is normalized to 1 for 'lambda' option. """ if spd is not None: if isinstance(spd, str): spd = SPD.read_csv_(self, file=spd, header=header, sep=sep) if ax0iswl == True: self.wl = spd[0] self.value = spd[1:] else: self.wl = wl if (self.wl.size == 3): self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2]) self.value = spd if self.value.shape[1] != self.wl.shape[0]: raise Exception( 'SPD.__init__(): Dimensions of wl and spd do not match.') else: if (wl is None): self.wl = SPD._WL3 else: self.wl = wl if (self.wl.size == 3): self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2]) self.value = np.zeros((1, self.wl.size)) self.wl = self.wl self.dtype = dtype self.shape = self.value.shape self.N = self.shape[0] if wl_new is not None: if interp_method == 'auto': interp_method = dtype self.cie_interp(wl_new, kind=interp_method, negative_values_allowed=negative_values_allowed) if norm_type is not None: self.normalize(norm_type=norm_type, norm_f=norm_f)
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] 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_Ydlep(xyz, cieobs = _CIEOBS, xyzw = _COLORTF_DEFAULT_WHITE_POINT, **kwargs): """ Convert XYZ tristimulus values to Y, dominant (complementary) wavelength and excitation purity. Args: :xyz: | ndarray with tristimulus values :xyzw: | None or ndarray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating spectrum locus coordinates. Returns: :Ydlep: | ndarray with Y, dominant (complementary) wavelength and excitation purity """ xyz3 = np3d(xyz).copy() # flip axis so that shortest dim is on axis0 (save time in looping): if xyz3.shape[0] < xyz3.shape[1]: axes12flipped = True xyz3 = xyz3.transpose((1,0,2)) else: axes12flipped = False # convert xyz to Yxy: Yxy = xyz_to_Yxy(xyz3) Yxyw = xyz_to_Yxy(xyzw) # get spectrum locus Y,x,y and wavelengths: SL = _CMF[cieobs]['bar'] wlsl = SL[0] Yxysl = xyz_to_Yxy(SL[1:4].T)[:,None] # center on xyzw: Yxy = Yxy - Yxyw Yxysl = Yxysl - Yxyw Yxyw = Yxyw - Yxyw #split: Y, x, y = asplit(Yxy) Yw,xw,yw = asplit(Yxyw) Ysl,xsl,ysl = asplit(Yxysl) # calculate hue: h = math.positive_arctan(x,y, htype = 'deg') hsl = math.positive_arctan(xsl,ysl, htype = 'deg') hsl_max = hsl[0] # max hue angle at min wavelength hsl_min = hsl[-1] # min hue angle at max wavelength dominantwavelength = np.zeros(Y.shape) purity = dominantwavelength.copy() for i in range(xyz3.shape[1]): # find index of complementary wavelengths/hues: pc = np.where((h[:,i] >= hsl_max) & (h[:,i] <= hsl_min + 360.0)) # hue's requiring complementary wavelength (purple line) h[:,i][pc] = h[:,i][pc] - np.sign(h[:,i][pc] - 180.0)*180.0 # add/subtract 180° to get positive complementary wavelength # find 2 closest hues in sl: #hslb,hib = meshblock(hsl,h[:,i:i+1]) hib,hslb = np.meshgrid(h[:,i:i+1],hsl) dh = np.abs(hslb-hib) q1 = dh.argmin(axis=0) # index of closest hue dh[q1] = 1000.0 q2 = dh.argmin(axis=0) # index of second closest hue dominantwavelength[:,i] = wlsl[q1] + np.divide(np.multiply((wlsl[q2] - wlsl[q1]),(h[:,i] - hsl[q1,0])),(hsl[q2,0] - hsl[q1,0])) # calculate wl corresponding to h: y = y1 + (y2-y1)*(x-x1)/(x2-x1) dominantwavelength[:,i][pc] = - dominantwavelength[:,i][pc] #complementary wavelengths are specified by '-' sign # calculate excitation purity: x_dom_wl = xsl[q1,0] + (xsl[q2,0] - xsl[q1,0])*(h[:,i] - hsl[q1,0])/(hsl[q2,0] - hsl[q1,0]) # calculate x of dom. wl y_dom_wl = ysl[q1,0] + (ysl[q2,0] - ysl[q1,0])*(h[:,i] - hsl[q1,0])/(hsl[q2,0] - hsl[q1,0]) # calculate y of dom. wl d_wl = (x_dom_wl**2.0 + y_dom_wl**2.0)**0.5 # distance from white point to sl d = (x[:,i]**2.0 + y[:,i]**2.0)**0.5 # distance from white point to test point purity[:,i] = d/d_wl # correct for those test points that have a complementary wavelength # calculate intersection of line through white point and test point and purple line: xy = np.vstack((x[:,i],y[:,i])).T xyw = np.hstack((xw,yw)) xypl1 = np.hstack((xsl[0,None],ysl[0,None])) xypl2 = np.hstack((xsl[-1,None],ysl[-1,None])) da = (xy-xyw) db = (xypl2-xypl1) dp = (xyw - xypl1) T = np.array([[0.0, -1.0], [1.0, 0.0]]) dap = np.dot(da,T) denom = np.sum(dap * db,axis=1,keepdims=True) num = np.sum(dap * dp,axis=1,keepdims=True) xy_linecross = (num/denom) *db + xypl1 d_linecross = np.atleast_2d((xy_linecross[:,0]**2.0 + xy_linecross[:,1]**2.0)**0.5).T#[0] purity[:,i][pc] = d[pc]/d_linecross[pc][:,0] Ydlep = np.dstack((xyz3[:,:,1],dominantwavelength,purity)) if axes12flipped == True: Ydlep = Ydlep.transpose((1,0,2)) else: Ydlep = Ydlep.transpose((0,1,2)) return Ydlep.reshape(xyz.shape)
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
Ct_hj = ((jabt_hj[..., 1]**2 + jabt_hj[..., 2]**2))**0.5 Cr_hj = ((jabr_hj[..., 1]**2 + jabr_hj[..., 2]**2))**0.5 Ctn_hj = normalized_chroma_ref * Ct_hj / ( Cr_hj + 1e-308) # calculate normalized chroma for samples under test Ctn_hj[Cr_hj == 0.0] = np.inf jabtn_hj = jabt_hj.copy() jabrn_hj = jabr_hj.copy() jabtn_hj[..., 1], jabtn_hj[..., 2] = Ctn_hj * np.cos(ht_hj), Ctn_hj * np.sin(ht_hj) jabrn_hj[..., 1], jabrn_hj[..., 2] = normalized_chroma_ref * np.cos( hr_hj), normalized_chroma_ref * np.sin(hr_hj) # calculate normalized versions of jabt, jabr: jabtn = jabt.copy() jabrn = jabr.copy() Ctn = np.zeros((jabt.shape[0], jabt.shape[1])) Crn = Ctn.copy() for j in range(nhbins): Ctn = Ctn + (Ct / Cr_hj[j, ...]) * (hr_idx == j) Crn = Crn + (Cr / Cr_hj[j, ...]) * (hr_idx == j) Ctn *= normalized_chroma_ref Crn *= normalized_chroma_ref jabtn[..., 1] = (Ctn * np.cos(ht)) jabtn[..., 2] = (Ctn * np.sin(ht)) jabrn[..., 1] = (Crn * np.cos(hr)) jabrn[..., 2] = (Crn * np.sin(hr)) # closed jabt_hj, jabr_hj for Rg: jabt_hj_closed = np.vstack((jabt_hj, jabt_hj[:1, ...])) jabr_hj_closed = np.vstack((jabr_hj, jabr_hj[:1, ...]))
def rotate(v, vecA=None, vecB=None, rot_axis=None, rot_angle=None, deg=True, norm=False): """ Rotate vector around rotation axis over angle. Args: :v: | vec3 vector. :rot_axis: | None, optional | vec3 vector specifying rotation axis. :rot_angle: | None, optional | float or int rotation angle. :deg: | True, optional | If False, rot_angle is in radians. :vecA:, :vecB: | None, optional | vec3 vectors defining a normal direction (cross(vecA, vecB)) around | which to rotate the vector in :v:. If rot_angle is None: rotation | angle is defined by the in-plane angle between vecA and vecB. :norm: | False, optional | Normalize rotated vector. """ if (vecA is not None) & (vecB is not None): rot_axis = cross(vecA, vecB) # rotation axis if rot_angle is None: costheta = dot(vecA, vecB, norm=True) # rotation angle costheta[costheta > 1] = 1 costheta[costheta < -1] = -1 rot_angle = np.arccos(costheta) elif (rot_angle is not None): if deg == True: rot_angle = np.deg2rad(rot_angle) else: raise Exception('vec3.rotate: insufficient not-None input args.') # normalize rot_axis rot_axis = rot_axis / rot_axis.norm() # Create short-hand variables: u = rot_axis cost = np.cos(rot_angle) sint = np.sin(rot_angle) # Setup rotation matrix: R = np.asarray([[np.zeros(u.x.shape) for j in range(3)] for i in range(3)]) R[0, 0] = cost + u.x * u.x * (1 - cost) R[0, 1] = u.x * u.y * (1 - cost) - u.z * sint R[0, 2] = u.x * u.z * (1 - cost) + u.y * sint R[1, 0] = u.x * u.y * (1 - cost) + u.z * sint R[1, 1] = cost + u.y * u.y * (1 - cost) R[1, 2] = u.y * u.z * (1 - cost) - u.x * sint R[2, 0] = u.z * u.x * (1 - cost) - u.y * sint R[2, 1] = u.z * u.y * (1 - cost) + u.x * sint R[2, 2] = cost + u.z * u.z * (1 - cost) # calculate dot product of matrix M with vector v: v3 = vec3(R[0,0]*v.x + R[0,1]*v.y + R[0,2]*v.z, \ R[1,0]*v.x + R[1,1]*v.y + R[1,2]*v.z, \ R[2,0]*v.x + R[2,1]*v.y + R[2,2]*v.z) if norm == True: v3 = v3 / v3.norm() return v3
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 = True, 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: | True, 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. :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, 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: plt.plot(Yxyc[:,1],Yxyc[:,2],color = center_color, linestyle = 'none', marker = center_marker, markersize = center_markersize) plt.plot(Yxy[:,1],Yxy[:,2],color = line_color, linestyle = line_style, linewidth = line_width, marker = line_marker, markersize = line_markersize) plt.xlabel(xlabel, fontname = label_fontname, fontsize = label_fontsize) plt.ylabel(ylabel, fontname = label_fontname, fontsize = label_fontsize) if show_grid == True: plt.grid() #plt.show() Yxys = np.transpose(Yxys,axes=(0,2,1)) if out is not None: return eval(out) else: return None
def cct_to_xyz(ccts, duv=None, cieobs=_CIEOBS, wl=None, mode='lut', out=None, accuracy=0.1, force_out_of_lut=True, upper_cct_max=10.0 * 20, approx_cct_temp=True): """ Convert correlated color temperature (CCT) and Duv (distance above (>0) or below (<0) the Planckian locus) to XYZ tristimulus values. | Finds xyzw_estimated by minimization of: | | F = numpy.sqrt(((100.0*(cct_min - cct)/(cct))**2.0) | + (((duv_min - duv)/(duv))**2.0)) | | with cct,duv the input values and cct_min, duv_min calculated using | luxpy.xyz_to_cct(xyzw_estimated,...). Args: :ccts: | ndarray of cct values :duv: | None or ndarray of duv values, optional | Note that duv can be supplied together with cct values in :ccts: as ndarray with shape (N,2) :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :mode: | 'lut' or 'search', optional | Determines what method to use. :out: | None (or 1), optional | If not None or 1: output a ndarray that contains estimated xyz and minimization results: | (cct_min, duv_min, F_min (objective fcn value)) :wl: | None, optional | Wavelengths used when calculating Planckian radiators. :accuracy: | float, optional | Stop brute-force search when cct :accuracy: is reached. :upper_cct_max: | 10.0**20, optional | Limit brute-force search to this cct. :approx_cct_temp: | True, optional | If True: use xyz_to_cct_HA() to get a first estimate of cct to speed up search. :force_out_of_lut: | True, optional | If True and cct is out of range of the LUT, then switch to brute-force search method, else return numpy.nan values. Returns: :returns: | ndarray with estimated XYZ tristimulus values Note: If duv is not supplied (:ccts:.shape is (N,1) and :duv: is None), source is assumed to be on the Planckian locus. """ # make ccts a min. 2d np.array: if isinstance(ccts, list): ccts = np2dT(np.array(ccts)) else: ccts = np2d(ccts) if len(ccts.shape) > 2: raise Exception('cct_to_xyz(): Input ccts.shape must be <= 2 !') # get cct and duv arrays from :ccts: cct = np2d(ccts[:, 0, None]) if (duv is None) & (ccts.shape[1] == 2): duv = np2d(ccts[:, 1, None]) elif duv is not None: duv = np2d(duv) #get estimates of approximate xyz values in case duv = None: BB = cri_ref(ccts=cct, wl3=wl, ref_type=['BB']) xyz_est = spd_to_xyz(data=BB, cieobs=cieobs, out=1) results = np.ones([ccts.shape[0], 3]) * np.nan if duv is not None: # optimization/minimization setup: def objfcn(uv_offset, uv0, cct, duv, out=1): #, cieobs = cieobs, wl = wl, mode = mode): uv0 = np2d(uv0 + uv_offset) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) cct_min, duv_min = xyz_to_cct(Yuv_to_xyz(Yuv0), cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) F = np.sqrt(((100.0 * (cct_min[0] - cct[0]) / (cct[0]))**2.0) + (((duv_min[0] - duv[0]) / (duv[0]))**2.0)) if out == 'F': return F else: return np.concatenate((cct_min, duv_min, np2d(F)), axis=1) # loop through each xyz_est: for i in range(xyz_est.shape[0]): xyz0 = xyz_est[i] cct_i = cct[i] duv_i = duv[i] cct_min, duv_min = xyz_to_cct(xyz0, cieobs=cieobs, out='cct,duv', wl=wl, mode=mode, accuracy=accuracy, force_out_of_lut=force_out_of_lut, upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) if np.abs(duv[i]) > _EPS: # find xyz: Yuv0 = xyz_to_Yuv(xyz0) uv0 = Yuv0[0][1:3] OptimizeResult = minimize(fun=objfcn, x0=np.zeros((1, 2)), args=(uv0, cct_i, duv_i, 'F'), method='Nelder-Mead', options={ "maxiter": np.inf, "maxfev": np.inf, 'xatol': 0.000001, 'fatol': 0.000001 }) betas = OptimizeResult['x'] #betas = np.zeros(uv0.shape) if out is not None: results[i] = objfcn(betas, uv0, cct_i, duv_i, out=3) uv0 = np2d(uv0 + betas) Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1) xyz_est[i] = Yuv_to_xyz(Yuv0) else: xyz_est[i] = xyz0 if (out is None) | (out == 1): return xyz_est else: # Also output results of minimization: return np.concatenate((xyz_est, results), axis=1)
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)