def check_dimensions(data, xyzw, caller='cat.apply()'): """ Check if dimensions of data and xyzw match. | Does nothing when they do, but raises error if dimensions don't match. Args: :data: | ndarray with color data. :xyzw: | ndarray with white point tristimulus values. :caller: | str with caller function for error handling, optional Returns: :returns: | ndarray with input color data, | Raises error if dimensions don't match. """ xyzw = np2d(xyzw) data = np2d(data) if ((xyzw.shape[0] > 1) & (data.shape[0] != xyzw.shape[0]) & (data.ndim == 2)): raise Exception( '{}: Cannot match dim of xyzw with data: xyzw.shape[0]>1 & != data.shape[0]' .format(caller))
def spd_to_power(data, ptype='ru', cieobs=_CIEOBS): """ Calculate power of spectral data in radiometric, photometric or quantal energy units. Args: :data: | ndarray with spectral data :ptype: | 'ru' or str, optional | str: - 'ru': in radiometric units | - 'pu': in photometric units | - 'pusa': in photometric units with Km corrected | to standard air (cfr. CIE TN003-2015) | - 'qu': in quantal energy units :cieobs: | _CIEOBS or str, optional | Type of cmf set to use for photometric units. Returns: returns: | ndarray with normalized spectral data (SI units) """ # get wavelength spacing: dl = getwld(data[0]) if ptype == 'ru': #normalize to radiometric units p = np2d(np.dot(data[1:], dl * np.ones(data.shape[1]))).T elif ptype == 'pusa': # normalize in photometric units with correction of Km to standard air # Calculate correction factor for Km in standard air: na = _BB['na'] # n for standard air c = _BB['c'] # m/s light speed lambdad = c / (na * 54 * 1e13) / (1e-9 ) # 555 nm lambda in standard air Km_correction_factor = 1 / ( 1 - (1 - 0.9998567) * (lambdad - 555)) # correction factor for Km in standard air # Get Vlambda and Km (for E): Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2) Km *= Km_correction_factor p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T elif ptype == 'pu': # normalize in photometric units # Get Vlambda and Km (for E): Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2) p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T elif ptype == 'qu': # normalize to quantual units # Get Quantal conversion factor: fQ = ((1e-9) / (_BB['h'] * _BB['c'])) p = np2d(fQ * np.dot(data[1:], dl * data[0])).T return p
def xyz_to_lms(xyz, cieobs=_CIEOBS, M=None, **kwargs): """ Convert XYZ tristimulus values to LMS cone fundamental responses. Args: :xyz: | ndarray with tristimulus values :cieobs: | _CIEOBS or str, optional :M: | None, optional | Conversion matrix for xyz to lms. | If None: use the one defined by :cieobs: Returns: :lms: | ndarray with LMS cone fundamental responses """ xyz = np2d(xyz) if M is None: M = _CMF[cieobs]['M'] # convert xyz to lms: if len(xyz.shape) == 3: lms = np.einsum('ij,klj->kli', M, xyz) else: lms = np.einsum('ij,lj->li', M, xyz) return lms
def lms_to_xyz(lms, cieobs=_CIEOBS, M=None, **kwargs): """ Convert LMS cone fundamental responses to XYZ tristimulus values. Args: :lms: | ndarray with LMS cone fundamental responses :cieobs: | _CIEOBS or str, optional :M: | None, optional | Conversion matrix for xyz to lms. | If None: use the one defined by :cieobs: Returns: :xyz: | ndarray with tristimulus values """ lms = np2d(lms) if M is None: M = _CMF[cieobs]['M'] # convert from lms to xyz: if len(lms.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) return xyz
def parse_x1x2_parameters(x, target_shape, catmode, expand_2d_to_3d=None, default=[1.0, 1.0]): """ Parse input parameters x and make them the target_shape for easy calculation. | Input in main function can now be a single value valid for all xyzw or an array with a different value for each xyzw. Args: :x: | list[float, float] or ndarray :target_shape: | tuple with shape information :catmode: | '1>0>2, optional | -'1>0>2': Two-step CAT | from illuminant 1 to baseline illuminant 0 to illuminant 2. | -'1>0': One-step CAT | from illuminant 1 to baseline illuminant 0. | -'0>2': One-step CAT | from baseline illuminant 0 to illuminant 2. :expand_2d_to_3d: | None, optional | [will be removed in future, serves no purpose] | Expand :x: from 2 to 3 dimensions. :default: | [1.0,1.0], optional | Default values for :x: Returns: :returns: | (ndarray, ndarray) for x10 and x20 """ if x is None: x10 = np.ones(target_shape) * default[0] if (catmode == '1>0>2') | (catmode == '1>2'): x20 = np.ones(target_shape) * default[1] else: x20 = np.zeros(target_shape) x20.fill(np.nan) else: x = np2d(x) if (catmode == '1>0>2') | (catmode == '1>2'): if x.shape[-1] == 2: x10 = np.ones(target_shape) * x[..., 0] x20 = np.ones(target_shape) * x[..., 1] else: x10 = np.ones(target_shape) * x x20 = x10.copy() elif catmode == '1>0': x10 = np.ones(target_shape) * x[..., 0] x20 = np.zeros(target_shape) x20.fill(np.nan) return x10, x20
def daylightlocus(cct, force_daylight_below4000K = False, cieobs = None, daylight_locus = None): """ Calculates daylight chromaticity (xD,yD) from correlated color temperature (cct). Args: :cct: | int or float or list of int/floats or ndarray :force_daylight_below4000K: | False or True, optional | Daylight locus approximation is not defined below 4000 K, | but by setting this to True, the calculation can be forced to | calculate it anyway. :cieobs: | CMF set corresponding to xD, yD output. | If None: use default CIE15-20xx locus for '1931_2' | Else: use the locus specified in :daylight_locus: :daylight_locus: | None, optional | dict with xD(T) and yD(xD) parameters to calculate daylight locus | for specified cieobs. | If None: use pre-calculated values. | If 'calc': calculate them on the fly. Returns: :(xD, yD): | (ndarray of x-coordinates, ndarray of y-coordinates) References: 1. `CIE15:2018, “Colorimetry,” CIE, Vienna, Austria, 2018. <https://doi.org/10.25039/TR.015.2018>`_ """ cct = np2d(cct) if np.any((cct < 4000.0) & (force_daylight_below4000K == False)): raise Exception('spectral.daylightlocus(): Daylight locus approximation not defined below 4000 K') if (cieobs is None): # use default values for '1931_2' reported in CIE15-20xx xD = -4.607*((1e3/cct)**3.0)+2.9678*((1e3/cct)**2.0)+0.09911*(1000.0/cct)+0.244063 p = cct>=7000.0 xD[p] = -2.0064*((1.0e3/cct[p])**3.0)+1.9018*((1.0e3/cct[p])**2.0)+0.24748*(1.0e3/cct[p])+0.23704 yD = -3.0*xD**2.0+2.87*xD-0.275 else: if isinstance(cieobs, str): if daylight_locus is None: daylight_locus = _DAYLIGHT_LOCI_PARAMETERS[cieobs] else: if isinstance(daylight_locus,str): if daylight_locus == 'calc': daylight_locus = get_daylightloci_parameters(cieobs = [cieobs])[cieobs] else: daylight_locus = get_daylightloci_parameters(cieobs = cieobs)['cmf_0'] pxy, pxT_l7, pxT_L7 = daylight_locus['pxy'], daylight_locus['pxT_l7k'], daylight_locus['pxT_L7k'] xD = np.polyval(pxT_l7, 1000/cct) p = cct>=7000.0 xD[p] = np.polyval(pxT_L7, 1000/cct[p]) yD = np.polyval(pxy, xD) return xD,yD
def Vrb_mb_to_xyz(Vrb, cieobs=_CIEOBS, scaling=[1, 1], M=None, Minverted=False, **kwargs): """ Convert V,r,b (Macleod-Boynton) color coordinates to XYZ tristimulus values. | Macleod Boynton: V = R+G, r = R/V, b = B/V | Note that R,G,B ~ L,M,S Args: :Vrb: | ndarray with V,r,b (Macleod-Boynton) color coordinates :cieobs: | luxpy._CIEOBS, optional | CMF set to use when getting the default M, which is | the xyz to lms conversion matrix. :scaling: | list of scaling factors for r and b dimensions. :M: | None, optional | Conversion matrix for going from XYZ to RGB (LMS) | If None, :cieobs: determines the M (function does inversion) :Minverted: | False, optional | Bool that determines whether M should be inverted. Returns: :xyz: | ndarray with tristimulus values Reference: 1. `MacLeod DI, and Boynton RM (1979). Chromaticity diagram showing cone excitation by stimuli of equal luminance. J. Opt. Soc. Am. 69, 1183–1186. <https://www.osapublishing.org/josa/abstract.cfm?uri=josa-69-8-1183>`_ """ Vrb = np2d(Vrb) RGB = np.empty(Vrb.shape) RGB[..., 0] = Vrb[..., 1] * Vrb[..., 0] / scaling[0] RGB[..., 2] = Vrb[..., 2] * Vrb[..., 0] / scaling[1] RGB[..., 1] = Vrb[..., 0] - RGB[..., 0] if M is None: M = _CMF[cieobs]['M'] if Minverted == False: M = np.linalg.inv(M) if len(RGB.shape) == 3: return np.einsum('ij,klj->kli', M, RGB) else: return np.einsum('ij,lj->li', M, RGB)
def xyz_to_xyz(xyz, **kwargs): """ Convert XYZ tristimulus values to XYZ tristimulus values. Args: :xyz: | ndarray with tristimulus values Returns: :xyz: | ndarray with tristimulus values """ return np2d(xyz)
def xyz_to_wuv(xyz, xyzw=_COLORTF_DEFAULT_WHITE_POINT, **kwargs): """ Convert XYZ tristimulus values CIE 1964 U*V*W* color space. Args: :xyz: | ndarray with tristimulus values :xyzw: | ndarray with tristimulus values of white point, optional | (Defaults to luxpy._COLORTF_DEFAULT_WHITE_POINT) Returns: :wuv: | ndarray with W*U*V* values """ Yuv = xyz_to_Yuv(np2d(xyz)) # convert to cie 1976 u'v' Yuvw = xyz_to_Yuv(np2d(xyzw)) wuv = np.empty(xyz.shape) wuv[..., 0] = 25.0 * (Yuv[..., 0]**(1 / 3)) - 17.0 wuv[..., 1] = 13.0 * wuv[..., 0] * (Yuv[..., 1] - Yuvw[..., 1]) wuv[..., 2] = 13.0 * wuv[..., 0] * (Yuv[..., 2] - Yuvw[..., 2]) * ( 2.0 / 3.0) #*(2/3) to convert to cie 1960 u, v return wuv
def xyz_to_srgb(xyz, gamma=2.4, **kwargs): """ Calculates IEC:61966 sRGB values from xyz. Args: :xyz: | ndarray with relative tristimulus values. :gamma: | 2.4, optional | compression in sRGB Returns: :rgb: | ndarray with R,G,B values (uint8). """ xyz = np2d(xyz) # define 3x3 matrix M = np.array([[3.2404542, -1.5371385, -0.4985314], [-0.9692660, 1.8760108, 0.0415560], [0.0556434, -0.2040259, 1.0572252]]) if len(xyz.shape) == 3: srgb = np.einsum('ij,klj->kli', M, xyz / 100) else: srgb = np.einsum('ij,lj->li', M, xyz / 100) # perform clipping: srgb[np.where(srgb > 1)] = 1 srgb[np.where(srgb < 0)] = 0 # test for the dark colours in the non-linear part of the function: dark = np.where(srgb <= 0.0031308) # apply gamma function: g = 1 / gamma # and scale to range 0-255: rgb = srgb.copy() rgb = (1.055 * rgb**g - 0.055) * 255 # non-linear bit for dark colours rgb[dark] = (srgb[dark].copy() * 12.92) * 255 # clip to range: rgb[rgb > 255] = 255 rgb[rgb < 0] = 0 return rgb
def xyz_to_Vrb_mb(xyz, cieobs=_CIEOBS, scaling=[1, 1], M=None, **kwargs): """ Convert XYZ tristimulus values to V,r,b (Macleod-Boynton) color coordinates. | Macleod Boynton: V = R+G, r = R/V, b = B/V | Note that R,G,B ~ L,M,S Args: :xyz: | ndarray with tristimulus values :cieobs: | luxpy._CIEOBS, optional | CMF set to use when getting the default M, which is the xyz to lms conversion matrix. :scaling: | list of scaling factors for r and b dimensions. :M: | None, optional | Conversion matrix for going from XYZ to RGB (LMS) | If None, :cieobs: determines the M (function does inversion) Returns: :Vrb: | ndarray with V,r,b (Macleod-Boynton) color coordinates Reference: 1. `MacLeod DI, and Boynton RM (1979). Chromaticity diagram showing cone excitation by stimuli of equal luminance. J. Opt. Soc. Am. 69, 1183–1186. <https://www.osapublishing.org/josa/abstract.cfm?uri=josa-69-8-1183>`_ """ xyz = np2d(xyz) if M is None: M = _CMF[cieobs]['M'] if len(xyz.shape) == 3: RGB = np.einsum('ij,klj->kli', M, xyz) else: RGB = np.einsum('ij,lj->li', M, xyz) Vrb = np.empty(xyz.shape) Vrb[..., 0] = RGB[..., 0] + RGB[..., 1] Vrb[..., 1] = RGB[..., 0] / Vrb[..., 0] * scaling[0] Vrb[..., 2] = RGB[..., 2] / Vrb[..., 0] * scaling[1] return Vrb
def Yxy_to_xyz(Yxy, **kwargs): """ Convert CIE Yxy chromaticity values to XYZ tristimulus values. Args: :Yxy: | ndarray with Yxy chromaticity values | (Y value refers to luminance or luminance factor) Returns: :xyz: | ndarray with tristimulus values """ Yxy = np2d(Yxy) xyz = np.empty(Yxy.shape) xyz[..., 1] = Yxy[..., 0] xyz[..., 0] = Yxy[..., 0] * Yxy[..., 1] / Yxy[..., 2] xyz[..., 2] = Yxy[..., 0] * (1.0 - Yxy[..., 1] - Yxy[..., 2]) / Yxy[..., 2] return xyz
def xyz_to_lab(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert XYZ tristimulus values to CIE 1976 L*a*b* (CIELAB) coordinates. 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 xyzw. Returns: :lab: | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates """ xyz = np2d(xyz) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # get and normalize (X,Y,Z) to white point: XYZr = xyz / xyzw # Apply cube-root compression: fXYZr = XYZr**(1.0 / 3.0) # Check for T/Tn <= 0.008856: (Note (24/116)**3 = 0.008856) pqr = XYZr <= (24 / 116)**3 # calculate f(T) for T/Tn <= 0.008856: (Note:(1/3)*((116/24)**2) = 841/108 = 7.787) fXYZr[pqr] = ((841 / 108) * XYZr[pqr] + 16.0 / 116.0) # calculate L*, a*, b*: Lab = np.empty(xyz.shape) Lab[..., 0] = 116.0 * (fXYZr[..., 1]) - 16.0 Lab[pqr[..., 1], 0] = 841 / 108 * 116 * XYZr[pqr[..., 1], 1] Lab[..., 1] = 500.0 * (fXYZr[..., 0] - fXYZr[..., 1]) Lab[..., 2] = 200.0 * (fXYZr[..., 1] - fXYZr[..., 2]) return Lab
def geomean(data, axis = 0, keepdims = False): """ Calculate geometric mean along axis. Args: :data: | list of values or ndarray :axis: | 0, optional | Axis along which to calculate geomean. :keepdims: | False or True, optional | Keep original dimensions of array. Returns: :returns: | ndarray with geomean values. """ data = np2d(data) return np.power(data.prod(axis=axis, keepdims = keepdims),1/data.shape[axis])
def xyz_to_Yxy(xyz, **kwargs): """ Convert XYZ tristimulus values CIE Yxy chromaticity values. Args: :xyz: | ndarray with tristimulus values Returns: :Yxy: | ndarray with Yxy chromaticity values | (Y value refers to luminance or luminance factor) """ xyz = np2d(xyz) Yxy = np.empty(xyz.shape) sumxyz = xyz[..., 0] + xyz[..., 1] + xyz[..., 2] Yxy[..., 0] = xyz[..., 1] Yxy[..., 1] = xyz[..., 0] / sumxyz Yxy[..., 2] = xyz[..., 1] / sumxyz return Yxy
def rms(data,axis = 0, keepdims = False): """ Calculate root-mean-square along axis. Args: :data: | list of values or ndarray :axis: | 0, optional | Axis along which to calculate rms. :keepdims: | False or True, optional | Keep original dimensions of array. Returns: :returns: | ndarray with rms values. """ data = np2d(data) return np.sqrt(np.power(data,2).mean(axis=axis, keepdims = keepdims))
def Yuv_to_xyz(Yuv, **kwargs): """ Convert CIE 1976 Yu'v' chromaticity values to XYZ tristimulus values. Args: :Yuv: | ndarray with CIE 1976 Yu'v' chromaticity values | (Y value refers to luminance or luminance factor) Returns: :xyz: | ndarray with tristimulus values """ Yuv = np2d(Yuv) xyz = np.empty(Yuv.shape) xyz[..., 1] = Yuv[..., 0] xyz[..., 0] = Yuv[..., 0] * (9.0 * Yuv[..., 1]) / (4.0 * Yuv[..., 2]) xyz[..., 2] = Yuv[..., 0] * (12.0 - 3.0 * Yuv[..., 1] - 20.0 * Yuv[..., 2]) / (4.0 * Yuv[..., 2]) return xyz
def xyz_to_Yuv(xyz, **kwargs): """ Convert XYZ tristimulus values CIE 1976 Yu'v' chromaticity values. Args: :xyz: | ndarray with tristimulus values Returns: :Yuv: | ndarray with CIE 1976 Yu'v' chromaticity values | (Y value refers to luminance or luminance factor) """ xyz = np2d(xyz) Yuv = np.empty(xyz.shape) denom = xyz[..., 0] + 15.0 * xyz[..., 1] + 3.0 * xyz[..., 2] Yuv[..., 0] = xyz[..., 1] Yuv[..., 1] = 4.0 * xyz[..., 0] / denom Yuv[..., 2] = 9.0 * xyz[..., 1] / denom return Yuv
def lab_to_xyz(lab, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert CIE 1976 L*a*b* (CIELAB) color coordinates to XYZ tristimulus values. Args: :lab: | ndarray with CIE 1976 L*a*b* (CIELAB) 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. Returns: :xyz: | ndarray with tristimulus values """ lab = np2d(lab) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # make xyzw same shape as data: xyzw = xyzw * np.ones(lab.shape) # get L*, a*, b* and Xw, Yw, Zw: fXYZ = np.empty(lab.shape) fXYZ[..., 1] = (lab[..., 0] + 16.0) / 116.0 fXYZ[..., 0] = lab[..., 1] / 500.0 + fXYZ[..., 1] fXYZ[..., 2] = fXYZ[..., 1] - lab[..., 2] / 200.0 # apply 3rd power: xyz = (fXYZ**3.0) * xyzw # Now calculate T where T/Tn is below the knee point: pqr = fXYZ <= (24 / 116) #(24/116)**3**(1/3) xyz[pqr] = np.squeeze(xyzw[pqr] * ((fXYZ[pqr] - 16.0 / 116.0) / (841 / 108))) return xyz
def srgb_to_xyz(rgb, gamma=2.4, **kwargs): """ Calculates xyz from IEC:61966 sRGB values. Args: :rgb: | ndarray with srgb values (uint8). :gamma: | 2.4, optional | compression in sRGB Returns: :xyz: | ndarray with relative tristimulus values. """ rgb = np2d(rgb) # define 3x3 matrix M = np.array([[0.4124564, 0.3575761, 0.1804375], [0.2126729, 0.7151522, 0.0721750], [0.0193339, 0.1191920, 0.9503041]]) # scale device coordinates: sRGB = rgb / 255 # test for non-linear part of conversion nonlin = np.where((rgb / 255) < 0.0031308) #0.03928) # apply gamma function to convert to sRGB srgb = sRGB.copy() srgb = ((srgb + 0.055) / 1.055)**gamma srgb[nonlin] = sRGB[nonlin] / 12.92 if len(srgb.shape) == 3: xyz = np.einsum('ij,klj->kli', M, srgb) * 100 else: xyz = np.einsum('ij,lj->li', M, srgb) * 100 return xyz
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 wuv_to_xyz(wuv, xyzw=_COLORTF_DEFAULT_WHITE_POINT, **kwargs): """ Convert CIE 1964 U*V*W* color space coordinates to XYZ tristimulus values. Args: :wuv: | ndarray with W*U*V* values :xyzw: | ndarray with tristimulus values of white point, optional | (Defaults to luxpy._COLORTF_DEFAULT_WHITE_POINT) Returns: :xyz: | ndarray with tristimulus values """ wuv = np2d(wuv) Yuvw = xyz_to_Yuv(xyzw) # convert to cie 1976 u'v' Yuv = np.empty(wuv.shape) Yuv[..., 0] = ((wuv[..., 0] + 17.0) / 25.0)**3.0 Yuv[..., 1] = Yuvw[..., 1] + wuv[..., 1] / (13.0 * wuv[..., 0]) Yuv[..., 2] = Yuvw[..., 2] + wuv[..., 2] / (13.0 * wuv[..., 0]) * ( 3.0 / 2.0) # convert to cie 1960 u, v return Yuv_to_xyz(Yuv)
def xyz_to_luv(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert XYZ tristimulus values to CIE 1976 L*u*v* (CIELUV) coordinates. 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 xyzw. Returns: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates """ xyz = np2d(xyz) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # Calculate u',v' of test and white: Yuv = xyz_to_Yuv(xyz) Yuvw = xyz_to_Yuv(todim(xyzw, xyz.shape)) # todim: make xyzw same shape as xyz #uv1976 to CIELUV luv = np.empty(xyz.shape) YdivYw = Yuv[..., 0] / Yuvw[..., 0] luv[..., 0] = 116.0 * YdivYw**(1.0 / 3.0) - 16.0 p = np.where(YdivYw <= (6.0 / 29.0)**3.0) luv[..., 0][p] = ((29.0 / 3.0)**3.0) * YdivYw[p] luv[..., 1] = 13.0 * luv[..., 0] * (Yuv[..., 1] - Yuvw[..., 1]) luv[..., 2] = 13.0 * luv[..., 0] * (Yuv[..., 2] - Yuvw[..., 2]) return luv
def luv_to_xyz(luv, xyzw=None, cieobs=_CIEOBS, **kwargs): """ Convert CIE 1976 L*u*v* (CIELUVB) coordinates to XYZ tristimulus values. Args: :luv: | ndarray with CIE 1976 L*u*v* (CIELUV) 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. Returns: :xyz: | ndarray with tristimulus values """ luv = np2d(luv) if xyzw is None: xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs) # Make xyzw same shape as luv and convert to Yuv: Yuvw = todim(xyz_to_Yuv(xyzw), luv.shape, equal_shape=True) # calculate u'v' from u*,v*: Yuv = np.empty(luv.shape) Yuv[..., 1:3] = (luv[..., 1:3] / (13 * luv[..., :1])) + Yuvw[..., 1:3] Yuv[Yuv[..., 0] == 0, 1:3] = 0 Yuv[..., 0] = Yuvw[..., 0] * (((luv[..., 0] + 16.0) / 116.0)**3.0) p = np.where((Yuv[..., 0] / Yuvw[..., 0]) < ((6.0 / 29.0)**3.0)) Yuv[..., 0][p] = Yuvw[..., 0][p] * (luv[..., 0][p] / ((29.0 / 3.0)**3.0)) return Yuv_to_xyz(Yuv)
def jabz_to_xyz(jabz, ztype='jabz', **kwargs): """ Convert Jz,az,bz color coordinates to XYZ tristimulus values. Args: :jabz: | ndarray with Jz,az,bz color coordinates :ztype: | 'jabz', optional | String with requested return: | Options: 'jabz', 'iabz' Returns: :xyz: | ndarray with tristimulus values Note: | 1. :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation! | | 2a. Jz represents the 'lightness' relative to a D65 white with luminance = 10000 cd/m² | (note that Jz that not exactly equal 1 for this high value, but rather for 102900 cd/m2) | 2b. az, bz represent respectively a red-green and a yellow-blue opponent axis | (but note that a D65 shows a small offset from (0,0)) Reference: 1. `Safdar, M., Cui, G., Kim,Y. J., and Luo, M. R. (2017). Perceptually uniform color space for image signals including high dynamic range and wide gamut. Opt. Express, vol. 25, no. 13, pp. 15131–15151, June, 2017. <http://www.opticsexpress.org/abstract.cfm?URI=oe-25-13-15131>`_ """ jabz = np2d(jabz) # Convert Jz to Iz: jabz[..., 0] = (jabz[..., 0] + 1.6295499532821566e-11) / ( 1 - 0.56 * (1 - (jabz[..., 0] + 1.6295499532821566e-11))) # Convert Iabz to lmsp: M = np.linalg.inv( np.array([[0.5, 0.5, 0], [3.524000, -4.066708, 0.542708], [0.199076, 1.096799, -1.295875]])) if len(jabz.shape) == 3: lmsp = np.einsum('ij,klj->kli', M, jabz) else: lmsp = np.einsum('ij,lj->li', M, jabz) # Convert lmsp to lms: lms = 10000 * (((3424 / 2**12) - lmsp**(1 / (1.7 * 2523 / 2**5))) / (((2392 / 2**7) * lmsp**(1 / (1.7 * 2523 / 2**5))) - (2413 / 2**7)))**(1 / (2610 / (2**14))) # Convert lms to xyz: # Setup X',Y',Z' from X,Y,Z transform as matrix: b = 1.15 g = 0.66 M_to_xyzp = np.array([[b, 0, 1 - b], [1 - g, g, 0], [0, 0, 1]]) # Define X',Y',Z' to L,M,S conversion matrix: M_to_lms = np.array([[0.41478972, 0.579999, 0.0146480], [-0.2015100, 1.120649, 0.0531008], [-0.0166008, 0.264800, 0.6684799]]) # Premultiply M_to_xyzp and M_to_lms and invert: M = M_to_lms @ M_to_xyzp M = np.linalg.inv(M) # Transform L,M,S to X,Y,Z: if len(jabz.shape) == 3: xyz = np.einsum('ij,klj->kli', M, lms) else: xyz = np.einsum('ij,lj->li', M, lms) return xyz
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 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())] hue_bin_data = _get_hue_bin_data(ipt, ipt_mc, start_hue = start_hue, nhbins = nhbins, normalized_chroma_ref = normalized_chroma_ref) Rg = _hue_bin_data_to_rg(hue_bin_data) if (out != 'Rm'): return eval(out) else: return Rm
def _massage_input_and_init_output(data, dataw, inputtype='xyz', direction='forward', n_out=3): """ Redimension input data to ensure most they have the appropriate sizes for easy and efficient looping. | | 1. Convert data and dataw to atleast_2d ndarrays | 2. Make axis 1 of dataw have 'same' dimensions as data | 3. Make dataw have same lights source axis size as data | 4. Flip light source axis to axis=0 for efficient looping | 5. Initialize output array camout to 'same' shape as data but with camout.shape[-1] == n_out Args: :data: | ndarray with input tristimulus values | or spectral data | or input color appearance correlates | Can be of shape: (N [, xM], x 3), whereby: | N refers to samples and M refers to light sources. | Note that for spectral input shape is (N x (M+1) x wl) :dataw: | None or ndarray, optional | Input tristimulus values or spectral data of white point. | None defaults to the use of CIE illuminant C. :inputtype: | '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 -> cam | -'inverse': cam -> xyz :n_out: | 3, optional | output size of last dimension of camout | (e.g. n_out=3 for j,a,b output or n_out = 5 for J,M,h,a,b output) Returns: :data: | ndarray with reshaped data :dataw: | ndarray with reshaped dataw :camout: | NaN filled ndarray for output of CAMv (camout.shape[-1] == Nout) :originalshape: | original shape of data Notes: For an example on the use, see code _simple_cam() (type: _simple_cam??) """ # Convert data and dataw to atleast_2d ndarrays: data = np2d(data).copy( ) # stimulus data (can be upto NxMx3 for xyz, or [N x (M+1) x wl] for spd)) dataw = np2d(dataw).copy( ) # white point (can be upto Nx3 for xyz, or [(N+1) x wl] for spd) originalshape = data.shape # to restore output to same shape # Make axis 1 of dataw have 'same' dimensions as data: if (data.ndim == 2): data = np.expand_dims(data, axis=1) # add light source axis 1 # Flip light source dim to axis 0: data = np.transpose(data, axes=(1, 0, 2)) dataw = np.expand_dims( dataw, axis=1) # add extra axis to move light source to axis 0 # Make dataw have same lights source dimension size as data: if inputtype == 'xyz': if dataw.shape[0] == 1: dataw = np.repeat(dataw, data.shape[0], axis=0) if (data.shape[0] == 1) & (dataw.shape[0] > 1): data = np.repeat(data, dataw.shape[0], axis=0) else: dataw = np.array([ np.vstack((dataw[:1, 0, :], dataw[i + 1:i + 2, 0, :])) for i in range(dataw.shape[0] - 1) ]) if (data.shape[0] == 1) & (dataw.shape[0] > 1): data = np.repeat(data, dataw.shape[0], axis=0) # Initialize output array: if n_out is not None: dshape = list((data).shape) dshape[-1] = n_out # requested number of correlates: e.g. j,a,b 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) else: camout = None return data, dataw, camout, originalshape
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