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 dot23(A,B, keepdims = False): """ Dot product of a 2-d ndarray with a (N x K x L) 3-d ndarray using einsum(). Args: :A: | ndarray (.shape = (M,N)) :B: | ndarray (.shape = (N,K,L)) Returns: :returns: | ndarray (.shape = (M,K,L)) """ if (len(A.shape)==2) & (len(B.shape)==3): dotAB = np.einsum('ij,jkl->ikl',A,B) if (len(B.shape)==3) & (keepdims == True): dotAB = np.expand_dims(dotAB,axis=1) elif (len(A.shape)==2) & (len(B.shape)==2): dotAB = np.einsum('ij,jk->ik',A,B) if (len(B.shape)==2) & (keepdims == True): dotAB = np.expand_dims(dotAB,axis=1) return dotAB
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 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 ndinterp1(X, Y, Xnew): """ Perform nd-dimensional linear interpolation using Delaunay triangulation. Args: :X: | ndarray with n-dimensional coordinates (last axis represents dimension). :Y: | ndarray with values at coordinates in X. :Xnew: | ndarray of new coordinates (last axis represents dimension). | When outside of the convex hull of X, then a best estimate is | given based on the closest vertices. Returns: :Ynew: | ndarray with new values at coordinates in Xnew. """ #get dimensions: n = Xnew.shape[-1] # create an object with triangulation tri = sp.spatial.Delaunay(X) # find simplexes that contain interpolated points s = tri.find_simplex(Xnew) # get the vertices for each simplex v = tri.vertices[s] # get transform matrices for each simplex (see explanation bellow) m = tri.transform[s] # for each interpolated point p, mutliply the transform matrix by # vector p-r, where r=m[:,n,:] is one of the simplex vertices to which # the matrix m is related to (again, see below) b = np.einsum('ijk,ik->ij', m[:,:n,:n], Xnew-m[:,n,:]) # get the weights for the vertices; `b` contains an n-dimensional vector # with weights for all but the last vertices of the simplex # (note that for n-D grid, each simplex consists of n+1 vertices); # the remaining weight for the last vertex can be copmuted from # the condition that sum of weights must be equal to 1 w = np.c_[b, 1-b.sum(axis=1)] # normalize weigths: w = w/w.sum(axis=1, keepdims=True) # interpolate: if Y[v].ndim == 3: Ynew = np.einsum('ijk,ij->ik', Y[v], w) else: Ynew = np.einsum('ij,ij->i', Y[v], w) return Ynew
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 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 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
def jabz_to_xyz(jabz, **kwargs): """ Convert Jz,az,bz color coordinates to XYZ tristimulus values. Args: :jabz: | ndarray with Jz,az,bz color coordinates 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, Jun. 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 xyz_to_ipt(xyz, 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: :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 for rescaling M (only when not None). :M: | None, optional | None defaults to xyz to lms conversion matrix determined by :cieobs: Returns: :ipt: | ndarray with IPT color coordinates 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>`_ """ xyz = np2d(xyz) # 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)[0]/100.0 else: xyzw = xyzw/100.0 M = math.normalize_3x3_matrix(M,xyzw) # get xyz and normalize to 1: xyz = xyz/100.0 # 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) #lms = np.dot(M,xyz.T).T #response compression: lms to lms' lmsp = lms**0.43 p = np.where(lms<0.0) lmsp[p] = -np.abs(lms[p])**0.43 # convert lms' to ipt coordinates: if len(xyz.shape) == 3: ipt = np.einsum('ij,klj->kli', _IPT_M['lms2ipt'], lmsp) else: ipt = np.einsum('ij,lj->li', _IPT_M['lms2ipt'], lmsp) return ipt