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, z = None, mu = None, sigmainv = None): """ Evaluate the squared mahalanobis distance 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. :z: | None or scalar or list or ndarray (.ndim = 1) with z-coordinates at which to evaluate the mahalanobis distance squared, optional. | If :z: is None & :y: is None, then :x: should be a 2d array. :mu: | None or ndarray (.ndim = 1) with center coordinates of the mahalanobis ellipse, optional. | None defaults to zeros(2) or zeros(3). :sigmainv: | None or ndarray with 'inverse covariance matrix', optional | Determines the shape and orientation of the PD. | None default to np.eye(2) or eye(3). Returns: :returns: | ndarray with magnitude of mahalanobis2(x,y[,z]) """ if (y is None) & (z is None): p = x.shape[-1] elif (z is None): p = x.shape[-1] if (y is None) else 2 elif (z is not None): p = 3 if (y is not None) else 2 if mu is None: mu = np.zeros(p) if sigmainv is None: sigmainv = np.eye(p) x = np2d(x) mu = np2d(mu) if (y is None) & (z is None): x = x - mu if p == 2: x, y = asplit(x) elif p==3: x, y, z = asplit(x) elif (z is None): if y is None: x = x - mu x, y = asplit(x) else: x = x - mu[...,0] # center data on mu y = np2d(y) - mu[...,1] # center data on mu elif (z is not None): if (y is not None): x = x - mu[0] # center data on mu y = np2d(y) - mu[...,1] # center data on mu z = np2d(z) - mu[...,2] # center data on mu else: x = x - mu[...,0] # center data on mu y = np2d(z) - mu[...,1] # center data on mu if p == 2: return (sigmainv[0,0] * (x**2.0) + sigmainv[1,1] * (y**2.0) + 2.0*sigmainv[0,1]*(x*y)) else: return (sigmainv[0,0] * (x**2.0) + sigmainv[1,1] * (y**2.0) + 2.0*sigmainv[0,1]*(x*y) + sigmainv[2,2] * (z**2.0) + 2.0*sigmainv[0,2]*(x*z) + 2.0*sigmainv[1,2]*(y*z))
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
def cam18sl(data, datab=None, Lb=[100], fov=10.0, inputtype='xyz', direction='forward', outin='Q,aS,bS', 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,aS,bS' or str, optional | 'Q,aS,bS' (brightness and opponent signals for saturation) | other options: 'Q,aM,bM' (colorfulness) | (Note that 'Q,aW,bW' would lead to a Cartesian | a,b-coordinate system centered at (1,0)) | 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) | * Default output was 'Q,aW,bW' prior to March 2020, but since this | is an a,b Cartesian system centered on (1,0), the default output | has been changed to 'Q,aS,bS'. 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, cieobs, k, naka, unique_hue_data = [ parameters[x] for x in sorted(parameters.keys()) ] # precomputations: Mlms2xyz = np.linalg.inv(_CMF[cieobs]['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=cieobs, 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=cieobs, 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=cieobs, relative=False) # 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': datar = spd_to_xyz(datar, cieobs=cieobs, relative=False) # convert to 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.zeros(dshape) camout.fill(np.nan) 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=cieobs, relative=False) xyzr = spd_to_xyz(np.vstack((datar[0], datar[i + 1:i + 2, :])), cieobs=cieobs, relative=False) else: xyzb = datab[i:i + 1, :] xyzr = datar[i:i + 1, :] lmsb = np.dot(_CMF[cieobs]['M'], xyzb.T).T # convert to l,m,s rgbb = (lmsb / _CMF[cieobs]['K']) * k # convert to rho, gamma, beta #lmsr = np.dot(_CMF[cieobs]['M'],xyzr.T).T # convert to l,m,s #rgbr = (lmsr / _CMF[cieobs]['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=cieobs, relative=False) elif (inputtype == 'xyz'): xyz = data[i] lms = np.dot(_CMF[cieobs]['M'], xyz.T).T # convert to l,m,s rgb = (lms / _CMF[cieobs]['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 S = s # make extra variable, jsut in case 'S' is called # 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', 'as', 'bs']): camout[i] = eval('ajoin((' + ','.join(outin) + '))') else: camout[i] = ajoin((Q, aS, bS)) 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[cieobs]['K'] xyz = np.dot(Mlms2xyz, lms.T).T camout[i] = xyz camout = np.transpose(camout, axes=(1, 0, 2)) if camout.shape[1] == 1: camout = np.squeeze(camout, axis=1) return camout