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
Example #2
0
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)
Example #5
0
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
Example #10
0
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
Example #11
0
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