def add_to_cmf_dict(bar=None, cieobs='indv', K=683, M=np.eye(3)): """ Add set of cmfs to _CMF dict. Args: :bar: | None, optional | Set of CMFs. None: initializes to empty ndarray. :cieobs: | 'indv' or str, optional | Name of CMF set. :K: | 683 (lm/W), optional | Conversion factor from radiometric to photometric quantity. :M: | np.eye, optional | Matrix for lms to xyz conversion. """ if bar is None: wl3 = getwlr(_WL3) bar = np.vstack((wl3, np.empty((3, wl3.shape[0])))) _CMF['types'].append(cieobs) _CMF[cieobs] = {'bar': bar} _CMF[cieobs]['K'] = K _CMF[cieobs]['M'] = M
def mutation(Xp, options): """ Performs mutation in the individuals. The mutation is one of the operators responsible for random changes in the individuals. Each parent x will have a new individual, called trial vector u, after the mutation. To do that, pick up two random individuals from the population, x2 and x3, and creates a difference vector v = x2 - x3. Then, chooses another point, called base vector, xb, and creates the trial vector by u = xb + F*v = xb + F*(x2 - x3) wherein F is an internal parameter, called scale factor. Args: :Xp: | a n x mu ndarray with mu "parents" and of dimension n :options: | the dict with the internal parameters Returns: :Xo: | a n x mu ndarray with the mu mutated individuals (of dimension n) """ # Creates a mu x mu matrix of 1:n elements on each row A = np.arange(options['mu']).repeat(options['mu']).reshape( options['mu'], options['mu']).T # Now, one removes the diagonal of A, because it contains indexes that repeat # the current i-th individual A = np.reshape(A[(np.eye(A.shape[0])) == False], (options['mu'], options['mu'] - 1)) # Now, creates a matrix that permutes the elements of A randomly J = np.argsort(np.random.rand(*A.shape), axis=1) # J = getdata('J.txt')-1 Ilin = J * options['mu'] + np.arange(options['mu'])[:, None] A = A.T.flatten()[Ilin].reshape(A.shape) # Chooses three random points (for each row) xbase = Xp[:, A[:, 0]] #base vectors v = Xp[:, A[:, 1]] - Xp[:, A[:, 2]] #difference vector # Performs the mutation Xo = xbase + options['F'] * v return Xo
def 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 cam18sl(data, datab = None, Lb = [100], fov = 10.0, inputtype = 'xyz', direction = 'forward', outin = 'Q,aW,bW', parameters = None): """ Convert between CIE 2006 10° XYZ tristimulus values (or spectral data) and CAM18sl color appearance correlates. Args: :data: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data or color appearance attributes of stimulus :datab: | ndarray of CIE 2006 10° absolute XYZ tristimulus values or spectral data of stimulus background :Lb: | [100], optional | Luminance (cd/m²) value(s) of background(s) calculated using the CIE 2006 10° CMFs | (only used in case datab == None and the background is assumed to be an Equal-Energy-White) :fov: | 10.0, optional | Field-of-view of stimulus (for size effect on brightness) :inputtpe: | 'xyz' or 'spd', optional | Specifies the type of input: | tristimulus values or spectral data for the forward mode. :direction: | 'forward' or 'inverse', optional | -'forward': xyz -> cam18sl | -'inverse': cam18sl -> xyz :outin: | 'Q,aW,bW' or str, optional | 'Q,aW,bW' (brightness and opponent signals for amount-of-neutral) | other options: 'Q,aM,bM' (colorfulness) and 'Q,aS,bS' (saturation) | Str specifying the type of | input (:direction: == 'inverse') and | output (:direction: == 'forward') :parameters: | None or dict, optional | Set of model parameters. | - None: defaults to luxpy.cam._CAM18SL_PARAMETERS | (see references below) Returns: :returns: | ndarray with color appearance correlates (:direction: == 'forward') | or | XYZ tristimulus values (:direction: == 'inverse') Notes: | * Instead of using the CIE 1964 10° CMFs in some places of the model, | the CIE 2006 10° CMFs are used througout, making it more self_consistent. | This has an effect on the k scaling factors (now different those in CAM15u) | and the illuminant E normalization for use in the chromatic adaptation transform. | (see future erratum to Hermans et al., 2018) | * The paper also used an equation for the amount of white W, which is | based on a Q value not expressed in 'bright' ('cA' = 0.937 instead of 123). | This has been corrected for in the luxpy version of the model, i.e. | _CAM18SL_PARAMETERS['cW'][0] has been changed from 2.29 to 1/11672. | (see future erratum to Hermans et al., 2018) References: 1. `Hermans, S., Smet, K. A. G., & Hanselaer, P. (2018). "Color appearance model for self-luminous stimuli." Journal of the Optical Society of America A, 35(12), 2000–2009. <https://doi.org/10.1364/JOSAA.35.002000>`_ """ if parameters is None: parameters = _CAM18SL_PARAMETERS outin = outin.split(',') #unpack model parameters: cA, cAlms, cHK, cM, cW, ca, calms, cb, cblms, cfov, k, naka, unique_hue_data = [parameters[x] for x in sorted(parameters.keys())] # precomputations: Mlms2xyz = np.linalg.inv(_CMF['2006_10']['M']) MAab = np.array([cAlms,calms,cblms]) invMAab = np.linalg.inv(MAab) #------------------------------------------------- # setup EEW reference field and default background field (Lr should be equal to Lb): # Get Lb values: if datab is not None: if inputtype != 'xyz': Lb = spd_to_xyz(datab, cieobs = '2006_10', relative = False)[...,1:2] else: Lb = datab[...,1:2] else: if isinstance(Lb,list): Lb = np2dT(Lb) # Setup EEW ref of same luminance as datab: if inputtype == 'xyz': wlr = getwlr(_CAM18SL_WL3) else: if datab is None: wlr = data[0] # use wlr of stimulus data else: wlr = datab[0] # use wlr of background data datar = np.vstack((wlr,np.ones((Lb.shape[0], wlr.shape[0])))) # create eew xyzr = spd_to_xyz(datar, cieobs = '2006_10', relative = False) # get abs. tristimulus values datar[1:] = datar[1:]/xyzr[...,1:2]*Lb # Create datab if None: if (datab is None): if inputtype != 'xyz': datab = datar.copy() else: datab = spd_to_xyz(datar, cieobs = '2006_10', relative = False) datar = datab.copy() # prepare data and datab for loop over backgrounds: # make axis 1 of datab have 'same' dimensions as data: if (data.ndim == 2): data = np.expand_dims(data, axis = 1) # add light source axis 1 if inputtype == 'xyz': if datab.shape[0] == 1: #make datab and datar have same lights source dimension (used to store different backgrounds) size as data datab = np.repeat(datab,data.shape[1],axis=0) datar = np.repeat(datar,data.shape[1],axis=0) else: if datab.shape[0] == 2: datab = np.vstack((datab[0],np.repeat(datab[1:], data.shape[1], axis = 0))) if datar.shape[0] == 2: datar = np.vstack((datar[0],np.repeat(datar[1:], data.shape[1], axis = 0))) # Flip light source/ background dim to axis 0: data = np.transpose(data, axes = (1,0,2)) #------------------------------------------------- #initialize camout: dshape = list(data.shape) dshape[-1] = len(outin) # requested number of correlates if (inputtype != 'xyz') & (direction == 'forward'): dshape[-2] = dshape[-2] - 1 # wavelength row doesn't count & only with forward can the input data be spectral camout = np.nan*np.ones(dshape) for i in range(data.shape[0]): # get rho, gamma, beta of background and reference white: if (inputtype != 'xyz'): xyzb = spd_to_xyz(np.vstack((datab[0], datab[i+1:i+2,:])), cieobs = '2006_10', relative = False) xyzr = spd_to_xyz(np.vstack((datar[0], datar[i+1:i+2,:])), cieobs = '2006_10', relative = False) else: xyzb = datab[i:i+1,:] xyzr = datar[i:i+1,:] lmsb = np.dot(_CMF['2006_10']['M'],xyzb.T).T # convert to l,m,s rgbb = (lmsb / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #lmsr = np.dot(_CMF['2006_10']['M'],xyzr.T).T # convert to l,m,s #rgbr = (lmsr / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta #rgbr = rgbr/rgbr[...,1:2]*Lb[i] # calculated EEW cone excitations at same luminance values as background rgbr = np.ones(xyzr.shape)*Lb[i] # explicitely equal EEW cone excitations at same luminance values as background if direction == 'forward': # get rho, gamma, beta of stimulus: if (inputtype != 'xyz'): xyz = spd_to_xyz(data[i], cieobs = '2006_10', relative = False) elif (inputtype == 'xyz'): xyz = data[i] lms = np.dot(_CMF['2006_10']['M'],xyz.T).T # convert to l,m,s rgb = (lms / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta # apply von-kries cat with D = 1: if (rgbb == 0).any(): Mcat = np.eye(3) else: Mcat = np.diag((rgbr/rgbb)[0]) rgba = np.dot(Mcat,rgb.T).T # apply naka-rushton compression: rgbc = naka_rushton(rgba, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling']) #rgbc = np.ones(rgbc.shape)*rgbc.mean() # test if eew ends up at origin # calculate achromatic and color difference signals, A, a, b: Aab = np.dot(MAab, rgbc.T).T A,a,b = asplit(Aab) a = ca*a b = cb*b # calculate colorfullness like signal M: M = cM*((a**2.0 + b**2.0)**0.5) # calculate brightness Q: Q = cA*(A + cHK[0]*M**cHK[1]) # last term is contribution of Helmholtz-Kohlrausch effect on brightness # calculate saturation, s: s = M / Q # calculate amount of white, W: W = 1 / (1.0 + cW[0]*(s**cW[1])) # adjust Q for size (fov) of stimulus (matter of debate whether to do this before or after calculation of s or W, there was no data on s, M or W for different sized stimuli: after) Q = Q*(fov/10.0)**cfov # calculate hue, h and Hue quadrature, H: h = hue_angle(a,b, htype = 'deg') if 'H' in outin: H = hue_quadrature(h, unique_hue_data = unique_hue_data) else: H = None # calculate cart. co.: if 'aM' in outin: aM = M*np.cos(h*np.pi/180.0) bM = M*np.sin(h*np.pi/180.0) if 'aS' in outin: aS = s*np.cos(h*np.pi/180.0) bS = s*np.sin(h*np.pi/180.0) if 'aW' in outin: aW = W*np.cos(h*np.pi/180.0) bW = W*np.sin(h*np.pi/180.0) if (outin != ['Q','aW','bW']): camout[i] = eval('ajoin(('+','.join(outin)+'))') else: camout[i] = ajoin((Q,aW,bW)) elif direction == 'inverse': # get Q, M and a, b depending on input type: if 'aW' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref W = (a**2.0 + b**2.0)**0.5 s = (((1.0 / W) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q if 'aM' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref M = (a**2.0 + b**2.0)**0.5 if 'aS' in outin: Q,a,b = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref s = (a**2.0 + b**2.0)**0.5 M = s*Q if 'h' in outin: Q, WsM, h = asplit(data[i]) Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref if 'W' in outin: s = (((1.0 / WsM) - 1.0)/cW[0])**(1.0/cW[1]) M = s*Q elif 's' in outin: M = WsM*Q elif 'M' in outin: M = WsM # calculate achromatic signal, A from Q and M: A = Q/cA - cHK[0]*M**cHK[1] # calculate hue angle: h = hue_angle(a,b, htype = 'rad') # calculate a,b from M and h: a = (M/cM)*np.cos(h) b = (M/cM)*np.sin(h) a = a/ca b = b/cb # create Aab: Aab = ajoin((A,a,b)) # calculate rgbc: rgbc = np.dot(invMAab, Aab.T).T # decompress rgbc to (adapted) rgba : rgba = naka_rushton(rgbc, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling'], direction = 'inverse') # apply inverse von-kries cat with D = 1: rgb = np.dot(np.diag((rgbb/rgbr)[0]),rgba.T).T # convert rgb to lms to xyz: lms = rgb/k*_CMF['2006_10']['K'] xyz = np.dot(Mlms2xyz,lms.T).T camout[i] = xyz if camout.shape[0] == 1: camout = np.squeeze(camout,axis = 0) return camout
def symmM_to_posdefM(A = None, atol = 1.0e-9, rtol = 1.0e-9, method = 'make', forcesymm = True): """ Convert a symmetric matrix to a positive definite one. Args: :A: | ndarray :atol: | float, optional | The absolute tolerance parameter (see Notes of numpy.allclose()) :rtol: | float, optional | The relative tolerance parameter (see Notes of numpy.allclose()) :method: | 'make' or 'nearest', optional (see notes for more info) :forcesymm: | True or False, optional | If A is not symmetric, force symmetry using: | A = numpy.triu(A) + numpy.triu(A).T - numpy.diag(numpy.diag(A)) Returns: :returns: | ndarray with positive-definite matrix. Notes on supported methods: 1. `'make': A Python/Numpy port of Muhammad Asim Mubeen's matlab function Spd_Mat.m <https://nl.mathworks.com/matlabcentral/fileexchange/45873-positive-definite-matrix>`_ 2. `'nearest': A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code. <https://stackoverflow.com/questions/43238173/python-convert-matrix-to-positive-semi-definite>`_ """ if A is not None: A = np2d(A) # Make sure matrix A is symmetric up to a certain tolerance: sn = check_symmetric(A, atol = atol, rtol = rtol) if ((A.shape[0] != A.shape[1]) | (sn != True)): if (forcesymm == True) & (A.shape[0] == A.shape[1]): A = np.triu(A) + np.triu(A).T - np.diag(np.diag(A)) else: raise Exception('symmM_to_posdefM(): matrix A not symmetric.') if check_posdef(A, atol = atol, rtol = rtol) == True: return A else: if method == 'make': # A Python/Numpy port of Muhammad Asim Mubeen's matlab function Spd_Mat.m # # See: https://nl.mathworks.com/matlabcentral/fileexchange/45873-positive-definite-matrix Val, Vec = np.linalg.eig(A) Val = np.real(Val) Vec = np.real(Vec) Val[np.where(Val==0)] = _EPS #making zero eigenvalues non-zero p = np.where(Val<0) Val[p] = -Val[p] #making negative eigenvalues positive return np.dot(Vec,np.dot(np.diag(Val) , Vec.T)) elif method == 'nearest': # A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code [1], which # credits [2]. # # [1] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd # # [2] N.J. Higham, "Computing a nearest symmetric positive semidefinite # matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6 # # See: https://stackoverflow.com/questions/43238173/python-convert-matrix-to-positive-semi-definite B = (A + A.T) / 2.0 _, s, V = np.linalg.svd(B) H = np.dot(V.T, np.dot(np.diag(s), V)) A2 = (B + H) / 2.0 A3 = (A2 + A2.T) / 2.0 if check_posdef(A3, atol = atol, rtol = rtol) == True: return A3 spacing = np.spacing(np.linalg.norm(A)) I = np.eye(A.shape[0]) k = 1 while not check_posdef(A3, atol = atol, rtol = rtol): mineig = np.min(np.real(np.linalg.eigvals(A3))) A3 += I * (-mineig * k**2.0+ spacing) k += 1 return A3
_CMF_M_2006_10 = np.array([[0.21701045, 0.83573367, -0.043510597], [-0.42997951, 1.2038895, 0.086210895], [0.0, 0.0, 0.46579234]]) # Note that for the following, no conversion has been defined, so the 1931 HPE matrix is used: _CMF_M_1964_10 = np.array([[0.38971, 0.68898, -0.07868], [-0.22981, 1.1834, 0.04641], [0.0, 0.0, 1.0]]) _CMF_M_1931_2_JUDD1951 = np.array([[0.38971, 0.68898, -0.07868], [-0.22981, 1.1834, 0.04641], [0.0, 0.0, 1.0]]) _CMF_M_1931_2_JUDDVOS1978 = np.array([[0.38971, 0.68898, -0.07868], [-0.22981, 1.1834, 0.04641], [0.0, 0.0, 1.0]]) # Scotopic conversion matrix has been set as the identity matrix (V' was replicated in the Xb,Yb,Zb columns) _CMF_M_1951_20_SCOTOPIC = np.eye(3) _CMF_M_cie_std_dev_obs_f1 = np.eye(3) _CMF_M_list = [ _CMF_M_1931_2, _CMF_M_1964_10, _CMF_M_2006_2, _CMF_M_2006_10, _CMF_M_1931_2_JUDD1951, _CMF_M_1931_2_JUDDVOS1978, _CMF_M_1951_20_SCOTOPIC, _CMF_M_cie_std_dev_obs_f1 ] #_CMF_K = _dictkv(keys = _CMF_TYPES, values = _CMF_K_VALUES, ordered = True) # K-factors for calculating absolute tristimulus values #_CMF_M = _dictkv(keys = _CMF_TYPES, values= _CMF_M_list, ordered = True) _CMF = {'types': _CMF_TYPES} for i, cmf_type in enumerate(_CMF_TYPES): # store all in single nested dict