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)
def join(self, data): """ Join data along last axis and return instance. """ if data[0].ndim == 2: #faster implementation self.value = np.transpose( np.concatenate(data, axis=0).reshape((np.hstack( (len(data), data[0].shape)))), (1, 2, 0)) elif data[0].ndim == 1: self.value = np.concatenate(data, axis=0).reshape((np.hstack( (len(data), data[0].shape)))).T else: self.value = np.hstack(data)[0] return self
def normalize_3x3_matrix(M, xyz0=np.array([[1.0, 1.0, 1.0]])): """ Normalize 3x3 matrix M to xyz0 -- > [1,1,1] If M.shape == (1,9): M is reshaped to (3,3) Args: :M: | ndarray((3,3) or ndarray((1,9)) :xyz0: | 2darray, optional Returns: :returns: | normalized matrix such that M*xyz0 = [1,1,1] """ M = np2d(M) if M.shape[-1] == 9: M = M.reshape(3, 3) if xyz0.shape[0] == 1: return np.dot(np.diagflat(1 / (np.dot(M, xyz0.T))), M) else: return np.concatenate([ np.dot(np.diagflat(1 / (np.dot(M, xyz0[1].T))), M) for i in range(xyz0.shape[0]) ], axis=0).reshape(xyz0.shape[0], 3, 3)
def ajoin(data): """ Join data on last axis. Args: :data: | tuple (ndarray, ndarray, ...) Returns: :returns: | ndarray (shape[-1] is equal to tuple length) """ if data[0].ndim == 2: #faster implementation return np.transpose(np.concatenate(data,axis=0).reshape((np.hstack((len(data),data[0].shape)))),(1,2,0)) elif data[0].ndim == 1: return np.concatenate(data,axis=0).reshape((np.hstack((len(data),data[0].shape)))).T else: return np.hstack(data)[0]
def spd_to_mcri(SPD, D=0.9, E=None, Yb=20.0, out='Rm', wl=None): """ Calculates the MCRI or Memory Color Rendition Index, Rm Args: :SPD: | ndarray with spectral data (can be multiple SPDs, first axis are the wavelengths) :D: | 0.9, optional | Degree of adaptation. :E: | None, optional | Illuminance in lux | (used to calculate La = (Yb/100)*(E/pi) to then calculate D | following the 'cat02' model). | If None: the degree is determined by :D: | If (:E: is not None) & (:Yb: is None): :E: is assumed to contain the adapting field luminance La (cd/m²). :Yb: | 20.0, optional | Luminance factor of background. (used when calculating La from E) | If None, E contains La (cd/m²). :out: | 'Rm' or str, optional | Specifies requested output (e.g. 'Rm,Rmi,cct,duv') :wl: | None, optional | Wavelengths (or [start, end, spacing]) to interpolate the SPDs to. | None: default to no interpolation Returns: :returns: | float or ndarray with MCRI Rm for :out: 'Rm' | Other output is also possible by changing the :out: str value. References: 1. `K.A.G. Smet, W.R. Ryckaert, M.R. Pointer, G. Deconinck, P. Hanselaer,(2012) “A memory colour quality metric for white light sources,” Energy Build., vol. 49, no. C, pp. 216–225. <http://www.sciencedirect.com/science/article/pii/S0378778812000837>`_ """ SPD = np2d(SPD) if wl is not None: SPD = spd(data=SPD, interpolation=_S_INTERP_TYPE, kind='np', wl=wl) # unpack metric default values: avg, catf, cieobs, cri_specific_pars, cspace, ref_type, rg_pars, sampleset, scale = [ _MCRI_DEFAULTS[x] for x in sorted(_MCRI_DEFAULTS.keys()) ] similarity_ai = cri_specific_pars['similarity_ai'] Mxyz2lms = cspace['Mxyz2lms'] scale_fcn = scale['fcn'] scale_factor = scale['cfactor'] sampleset = eval(sampleset) # A. calculate xyz: xyzti, xyztw = spd_to_xyz(SPD, cieobs=cieobs['xyz'], rfl=sampleset, out=2) if 'cct' in out.split(','): cct, duv = xyz_to_cct(xyztw, cieobs=cieobs['cct'], out='cct,duv', mode='lut') # B. perform chromatic adaptation to adopted whitepoint of ipt color space, i.e. D65: if catf is not None: Dtype_cat, F, Yb_cat, catmode_cat, cattype_cat, mcat_cat, xyzw_cat = [ catf[x] for x in sorted(catf.keys()) ] # calculate degree of adaptationn D: if E is not None: if Yb is not None: La = (Yb / 100.0) * (E / np.pi) else: La = E D = cat.get_degree_of_adaptation(Dtype=Dtype_cat, F=F, La=La) else: Dtype_cat = None # direct input of D if (E is None) and (D is None): D = 1.0 # set degree of adaptation to 1 ! if D > 1.0: D = 1.0 if D < 0.6: D = 0.6 # put a limit on the lowest D # apply cat: xyzti = cat.apply(xyzti, cattype=cattype_cat, catmode=catmode_cat, xyzw1=xyztw, xyzw0=None, xyzw2=xyzw_cat, D=D, mcat=[mcat_cat], Dtype=Dtype_cat) xyztw = cat.apply(xyztw, cattype=cattype_cat, catmode=catmode_cat, xyzw1=xyztw, xyzw0=None, xyzw2=xyzw_cat, D=D, mcat=[mcat_cat], Dtype=Dtype_cat) # C. convert xyz to ipt and split: ipt = xyz_to_ipt( xyzti, cieobs=cieobs['xyz'], M=Mxyz2lms ) #input matrix as published in Smet et al. 2012, Energy and Buildings I, P, T = asplit(ipt) # D. calculate specific (hue dependent) similarity indicators, Si: if len(xyzti.shape) == 3: ai = np.expand_dims(similarity_ai, axis=1) else: ai = similarity_ai a1, a2, a3, a4, a5 = asplit(ai) mahalanobis_d2 = (a3 * np.power((P - a1), 2.0) + a4 * np.power( (T - a2), 2.0) + 2.0 * a5 * (P - a1) * (T - a2)) if (len(mahalanobis_d2.shape) == 3) & (mahalanobis_d2.shape[-1] == 1): mahalanobis_d2 = mahalanobis_d2[:, :, 0].T Si = np.exp(-0.5 * mahalanobis_d2) # E. calculate general similarity indicator, Sa: Sa = avg(Si, axis=0, keepdims=True) # F. rescale similarity indicators (Si, Sa) with a 0-1 scale to memory color rendition indices (Rmi, Rm) with a 0 - 100 scale: Rmi = scale_fcn(np.log(Si), scale_factor=scale_factor) Rm = np2d(scale_fcn(np.log(Sa), scale_factor=scale_factor)) # G. calculate Rg (polyarea of test / polyarea of memory colours): if 'Rg' in out.split(','): I = I[ ..., None] #broadcast_shape(I, target_shape = None,expand_2d_to_3d = 0) a1 = a1[:, None] * np.ones( I.shape ) #broadcast_shape(a1, target_shape = None,expand_2d_to_3d = 0) a2 = a2[:, None] * np.ones( I.shape ) #broadcast_shape(a2, target_shape = None,expand_2d_to_3d = 0) a12 = np.concatenate( (a1, a2), axis=2 ) #broadcast_shape(np.hstack((a1,a2)), target_shape = ipt.shape,expand_2d_to_3d = 0) ipt_mc = np.concatenate((I, a12), axis=2) nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [ rg_pars[x] for x in sorted(rg_pars.keys()) ] Rg = jab_to_rg(ipt, ipt_mc, ordered_and_sliced=False, nhbins=nhbins, start_hue=start_hue, normalize_gamut=normalize_gamut) if (out != 'Rm'): return eval(out) else: return Rm
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 xyz_to_cct_ohno(xyzw, cieobs=_CIEOBS, out='cct', wl=None, accuracy=0.1, force_out_of_lut=True, upper_cct_max=10.0**20, approx_cct_temp=True): """ Convert XYZ tristimulus values to correlated color temperature (CCT) and Duv (distance above (>0) or below (<0) the Planckian locus) using Ohno's method. Args: :xyzw: | ndarray of tristimulus values :cieobs: | luxpy._CIEOBS, optional | CMF set used to calculated xyzw. :out: | 'cct' (or 1), optional | Determines what to return. | Other options: 'duv' (or -1), 'cct,duv'(or 2), "[cct,duv]" (or -2) :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: | cct: out == 'cct' (or 1) | duv: out == 'duv' (or -1) | cct, duv: out == 'cct,duv' (or 2) | [cct,duv]: out == "[cct,duv]" (or -2) Note: LUTs are stored in ./data/cctluts/ Reference: 1. `Ohno Y. Practical use and calculation of CCT and Duv. Leukos. 2014 Jan 2;10(1):47-55. <http://www.tandfonline.com/doi/abs/10.1080/15502724.2014.839020>`_ """ xyzw = np2d(xyzw) if len(xyzw.shape) > 2: raise Exception('xyz_to_cct_ohno(): Input xyzwa.ndim must be <= 2 !') # get 1960 u,v of test source: Yuv = xyz_to_Yuv( xyzw) # remove possible 1-dim + convert xyzw to CIE 1976 u',v' axis_of_v3 = len(Yuv.shape) - 1 # axis containing color components u = Yuv[:, 1, None] # get CIE 1960 u v = (2.0 / 3.0) * Yuv[:, 2, None] # get CIE 1960 v uv = np2d(np.concatenate((u, v), axis=axis_of_v3)) # load cct & uv from LUT: if cieobs not in _CCT_LUT: _CCT_LUT[cieobs] = calculate_lut(ccts=None, cieobs=cieobs, add_to_lut=False) cct_LUT = _CCT_LUT[cieobs][:, 0, None] uv_LUT = _CCT_LUT[cieobs][:, 1:3] # calculate CCT of each uv: CCT = np.ones(uv.shape[0]) * np.nan # initialize with NaN's Duv = CCT.copy() # initialize with NaN's idx_m = 0 idx_M = uv_LUT.shape[0] - 1 for i in range(uv.shape[0]): out_of_lut = False delta_uv = (((uv_LUT - uv[i])**2.0).sum( axis=1))**0.5 # calculate distance of uv with uv_LUT idx_min = delta_uv.argmin() # find index of minimum distance # find Tm, delta_uv and u,v for 2 points surrounding uv corresponding to idx_min: if idx_min == idx_m: idx_min_m1 = idx_min out_of_lut = True else: idx_min_m1 = idx_min - 1 if idx_min == idx_M: idx_min_p1 = idx_min out_of_lut = True else: idx_min_p1 = idx_min + 1 if (out_of_lut == True) & (force_out_of_lut == True): # calculate using search-function cct_i, Duv_i = xyz_to_cct_search(xyzw[i], cieobs=cieobs, wl=wl, accuracy=accuracy, out='cct,duv', upper_cct_max=upper_cct_max, approx_cct_temp=approx_cct_temp) CCT[i] = cct_i Duv[i] = Duv_i continue elif (out_of_lut == True) & (force_out_of_lut == False): CCT[i] = np.nan Duv[i] = np.nan cct_m1 = cct_LUT[idx_min_m1] # - 2*_EPS delta_uv_m1 = delta_uv[idx_min_m1] uv_m1 = uv_LUT[idx_min_m1] cct_p1 = cct_LUT[idx_min_p1] delta_uv_p1 = delta_uv[idx_min_p1] uv_p1 = uv_LUT[idx_min_p1] cct_0 = cct_LUT[idx_min] delta_uv_0 = delta_uv[idx_min] # calculate uv distance between Tm_m1 & Tm_p1: delta_uv_p1m1 = ((uv_p1[0] - uv_m1[0])**2.0 + (uv_p1[1] - uv_m1[1])**2.0)**0.5 # Triangular solution: x = ((delta_uv_m1**2) - (delta_uv_p1**2) + (delta_uv_p1m1**2)) / (2 * delta_uv_p1m1) Tx = cct_m1 + ((cct_p1 - cct_m1) * (x / delta_uv_p1m1)) #uBB = uv_m1[0] + (uv_p1[0] - uv_m1[0]) * (x / delta_uv_p1m1) vBB = uv_m1[1] + (uv_p1[1] - uv_m1[1]) * (x / delta_uv_p1m1) Tx_corrected_triangular = Tx * 0.99991 signDuv = np.sign(uv[i][1] - vBB) Duv_triangular = signDuv * np.atleast_1d( ((delta_uv_m1**2.0) - (x**2.0))**0.5) # Parabolic solution: a = delta_uv_m1 / (cct_m1 - cct_0 + _EPS) / (cct_m1 - cct_p1 + _EPS) b = delta_uv_0 / (cct_0 - cct_m1 + _EPS) / (cct_0 - cct_p1 + _EPS) c = delta_uv_p1 / (cct_p1 - cct_0 + _EPS) / (cct_p1 - cct_m1 + _EPS) A = a + b + c B = -(a * (cct_p1 + cct_0) + b * (cct_p1 + cct_m1) + c * (cct_0 + cct_m1)) C = (a * cct_p1 * cct_0) + (b * cct_p1 * cct_m1) + (c * cct_0 * cct_m1) Tx = -B / (2 * A + _EPS) Tx_corrected_parabolic = Tx * 0.99991 Duv_parabolic = signDuv * (A * np.power(Tx_corrected_parabolic, 2) + B * Tx_corrected_parabolic + C) Threshold = 0.002 if Duv_triangular < Threshold: CCT[i] = Tx_corrected_triangular Duv[i] = Duv_triangular else: CCT[i] = Tx_corrected_parabolic Duv[i] = Duv_parabolic # Regulate output: if (out == 'cct') | (out == 1): return np2dT(CCT) elif (out == 'duv') | (out == -1): return np2dT(Duv) elif (out == 'cct,duv') | (out == 2): return np2dT(CCT), np2dT(Duv) elif (out == "[cct,duv]") | (out == -2): return np.vstack((CCT, Duv)).T
def ipt_to_xyz(ipt, cieobs=_CIEOBS, xyzw=None, M=None, **kwargs): """ Convert XYZ tristimulus values to IPT color coordinates. | I: Lightness axis, P, red-green axis, T: yellow-blue axis. Args: :ipt: | ndarray with IPT color coordinates :xyzw: | None or ndarray with tristimulus values of white point, optional | None defaults to xyz of CIE D65 using the :cieobs: observer. :cieobs: | luxpy._CIEOBS, optional | CMF set to use when calculating xyzw for rescaling Mxyz2lms (only when not None). :M: | None, optional | None defaults to xyz to lms conversion matrix determined by:cieobs: Returns: :xyz: | ndarray with tristimulus values Note: :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation ! Reference: 1. `Ebner F, and Fairchild MD (1998). Development and testing of a color space (IPT) with improved hue uniformity. In IS&T 6th Color Imaging Conference, (Scottsdale, Arizona, USA), pp. 8–13. <http://www.ingentaconnect.com/content/ist/cic/1998/00001998/00000001/art00003?crawler=true>`_ """ ipt = np2d(ipt) # get M to convert xyz to lms and apply normalization to matrix or input your own: if M is None: M = _IPT_M['xyz2lms'][cieobs].copy( ) # matrix conversions from xyz to lms if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs, out=1) / 100.0 else: xyzw = xyzw / 100.0 M = math.normalize_3x3_matrix(M, xyzw) # convert from ipt to lms': if len(ipt.shape) == 3: lmsp = np.einsum('ij,klj->kli', np.linalg.inv(_IPT_M['lms2ipt']), ipt) else: lmsp = np.einsum('ij,lj->li', np.linalg.inv(_IPT_M['lms2ipt']), ipt) # reverse response compression: lms' to lms lms = lmsp**(1.0 / 0.43) p = np.where(lmsp < 0.0) lms[p] = -np.abs(lmsp[p])**(1.0 / 0.43) # convert from lms to xyz: if np.ndim(M) == 2: if len(ipt.shape) == 3: xyz = np.einsum('ij,klj->kli', np.linalg.inv(M), lms) else: xyz = np.einsum('ij,lj->li', np.linalg.inv(M), lms) else: if len( ipt.shape ) == 3: # second dim of lms must match dim of 1st of M and 1st dim of xyzw xyz = np.concatenate([ np.einsum('ij,klj->kli', np.linalg.inv(M[i]), lms[:, i:i + 1, :]) for i in range(M.shape[0]) ], axis=1) else: # first dim of lms must match dim of 1st of M and 1st dim of xyzw xyz = np.concatenate([ np.einsum('ij,lj->li', np.linalg.inv(M[i]), lms[i:i + 1, :]) for i in range(M.shape[0]) ], axis=0) #xyz = np.dot(np.linalg.inv(M),lms.T).T xyz = xyz * 100.0 xyz[np.where(xyz < 0.0)] = 0.0 return xyz