Example #1
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
Example #2
0
def _dot_M_xyz(M,xyz):
    """
    Perform matrix multiplication between M and xyz (M*xyz) using einsum.
    
    Args:
        :xyz:
            | 2D or 3D ndarray
        :M:
            | 2D or 3D ndarray
            | if 2D: use same matrix M for each xyz, 
            | if 3D: use M[i] for xyz[i,:] (2D xyz) or xyz[:,i,:] (3D xyz)
            
    Returns:
        :M*xyz:
            | ndarray with same shape as xyz containing dot product of M and xyz.
    """
    # convert xyz to ...:
    if np.ndim(M)==2:
        if len(xyz.shape) == 3:
            return np.einsum('ij,klj->kli', M, xyz)
        else:
            return np.einsum('ij,lj->li', M, xyz)
    else:
        if len(xyz.shape) == 3: # second dim of x must match dim of 1st of M and 1st dim of xyzw
            return np.concatenate([np.einsum('ij,klj->kli', M[i], xyz[:,i:i+1,:]) for i in range(M.shape[0])],axis=1)
        else: # first dim of xyz must match dim of 1st of M and 1st dim of xyzw
            return np.concatenate([np.einsum('ij,lj->li', M[i], xyz[i:i+1,:]) for i in range(M.shape[0])],axis=0)
Example #3
0
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 #4
0
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
Example #5
0
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 #6
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
Example #7
0
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
Example #8
0
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
Example #9
0
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
Example #10
0
def _cij_to_gij(xyz,C):
    """ Convert from matrix elements describing the discrimination ellipses from Cij (XYZ) to gij (Yxy)"""
    SIG = xyz[...,0] + xyz[...,1] + xyz[...,2]
    M1 = np.array([SIG, -SIG*xyz[...,0]/xyz[...,1], xyz[...,0]/xyz[...,1]])
    M2 = np.array([np.zeros_like(SIG), np.zeros_like(SIG), np.ones_like(SIG)])
    M3 = np.array([-SIG, -SIG*(xyz[...,1] + xyz[...,2])/xyz[...,1], xyz[...,2]/xyz[...,1]])
    M = np.array((M1,M2,M3))
    
    M = _transpose_02(M) # move stimulus dimension to axis = 0
    
    C = _transpose_02(C) # move stimulus dimension to axis = 0
    
    # convert Cij (XYZ) to gij' (xyY):
    AM = np.einsum('ij,kjl->kil', _M_XYZ_TO_PQS, M)
    CAM = np.einsum('kij,kjl->kil', C, AM) 
#    ATCAM = np.einsum('ij,kjl->kil', _M_XYZ_TO_PQS.T, CAM)
#    gij = np.einsum('kij,kjl->kil', np.transpose(M,(0,2,1)), ATCAM) # gij = M.T*A.T*C**A*M = (AM).T*C*A*M
    gij = np.einsum('kij,kjl->kil', np.transpose(AM,(0,2,1)), CAM) # gij = M.T*A.T*C**A*M = (AM).T*C*A*M

    # convert gij' (xyY) to gij (Yxy):
    gij = np.roll(np.roll(gij,1,axis=2),1,axis=1)
    
    return gij
Example #11
0
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
Example #12
0
def discrimination_hotelling_t2(Yxy1, Yxy2, etype = 'fmc2', ellipsoid = True, Y1 = None, Y2 = None, cspace = 'Yxy'):
    """
    Check 'significance' of difference using Hotelling's T2 test on the centers Yxy1 and Yxy2 and their associate FMC-1/2 discrimination ellipses.
    
    Args:
        :Yxy1, Yxy2:
            | 2D ndarrays with [Y,]x,y coordinate centers. 
            | If Yxy.shape[-1]==2: Y is added using the value from the Y-input argument.
        :etype:
            | 'fmc2', optional
            | Type of FMC color discrimination equations to use (see references below).
            | options: 'fmc1', fmc2'
        :Y1, Y2:
            | None, optional
            | Only affects FMC-2 (see note below).
            | If not None: Yi = 10.69 and overrides values in Yxyi. 
        :ellipsoid:
            | True, optional
            | If True: return ellipsoids, else return ellipses (only if cspace == 'Yxy')!
        :cspace:
            | 'Yxy', optional
            | Return coefficients for Yxy-ellipses/ellipsoids ('Yxy') or XYZ ellipsoids ('xyz')

    Returns:
        :p:
            | Chi-square based p-value
        :T2:
            | T2 test statistic (= mahalanobis distance on summed standard error cov. matrices)
    
    Steps:
        1. For each center coordinate, the standard error covariance matrix gij^-1 = Si/ni
        is determined using the FMC-1 or FMC-2 equations (see refs. 1 & 2).
        2. Calculate sum of covariance matrices: SIG = S1/n1 + S2/n2 = gij1^-1 + gij2^-1
        3. These are then used in Hotelling's T2 test: T2 = (xy1 - xy2).T*(SIG^-1)*(xy1_xy2)
        4. The T2 statistic is then tested against a Chi-square distribution with 2 or 3 degrees of freedom.
    
    References:
       1. Chickering, K.D. (1967), Optimization of the MacAdam-Modified 1965 Friele Color-Difference Formula, 57(4):537-541
       2. Chickering, K.D. (1971), FMC Color-Difference Formulas: Clarification Concerning Usage, 61(1):118-122

    """    
    if Yxy1.shape[-1] == 2:
        Yxy1 = np.hstack((100*np.ones((Yxy1.shape[0],1)),Yxy1))
    if Y1 is not None:
        Yxy1[...,0] = Y1
    if Yxy2.shape[-1] == 2:
        Yxy2 = np.hstack((100*np.ones((Yxy2.shape[0],1)),Yxy2))
    if Y2 is not None:
        Yxy2[...,0] = Y2
    
    # Get gij matrices (i.e. inverse covariance matrices):
    gij1 = get_gij_fmc(Yxy1, etype = etype, ellipsoid=ellipsoid, Y = Y1, cspace = cspace)
    gij2 = get_gij_fmc(Yxy2, etype = etype, ellipsoid=ellipsoid, Y = Y2, cspace = cspace)
    df = gij1.shape[1] # get degrees of freedom for Chi2
    
    # Calculate T2 statistic:
    D12 = (Yxy1[...,(3-df):] - Yxy2[...,(3-df):])
    SIG12 = np.linalg.inv(np.linalg.inv(gij1) + np.linalg.inv(gij2))
    T2 = np.einsum('ki,ki->k',D12,np.einsum('kij,kj->ki',SIG12,D12))
    #T2 = np.atleast_2d(T2).T
    
    # Get p-value:
    p = sp.stats.distributions.chi2.sf(T2, df)
    
    return p, T2
Example #13
0
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 #14
0
def jabz_to_xyz(jabz, ztype='jabz', use_zcam_parameters=False, **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'
        :use_zcam_parameters:
            | False, optional
            | ZCAM uses a slightly different values (see notes)
            
    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))
     | 3. ZCAM: uses a slightly different value for b (=0.7 instead of 0.66)
     |      and calculates Iz as M' -epsilon (instead L'/2 + M'/2).

    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>`_
        
        2. Safdar, M., Hardeberg, J.Y., Luo, M.R. (2021) 
        "ZCAM, a psychophysical model for colour appearance prediction", 
        Optics Express.
    """
    jabz = np2d(jabz)

    # Convert Jz to Iz:
    if ztype == 'jabz':
        jabz[..., 0] = (jabz[..., 0] + 1.6295499532821566e-11) / (
            1 - 0.56 * (1 - (jabz[..., 0] + 1.6295499532821566e-11)))

    # Convert Iabz to lmsp:
    # Transform L',M',S' to Iabz:
    if use_zcam_parameters:
        epsilon = 3.70352262101900054e-11
        M = _M_LMSP_TO_IAB_ZCAM
    else:
        epsilon = 0
        M = _M_LMSP_TO_IAB_JABZ
    jabz[..., 0] += epsilon
    if len(jabz.shape) == 3:
        lmsp = np.einsum('ij,klj->kli', np.linalg.inv(M), jabz)
    else:
        lmsp = np.einsum('ij,lj->li', np.linalg.inv(M), jabz)

    # Convert lmsp to lms:
    c1, c2, c3, n, p = [
        _PQ_PARAMETERS[x] for x in sorted(_PQ_PARAMETERS.keys())
    ]
    lms = 10000 * ((c1 - lmsp**(1 / p)) / ((c3 * lmsp**(1 / p)) - c2))**(1 / n)

    # 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]])

    # Premultiply M_to_xyzp and M_to_lms and invert:
    M = np.linalg.inv(_M_XYZP_TO_LMS @ M_to_xyzp)

    # 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 #15
0
def xyz_to_jabz(xyz, ztype='jabz', use_zcam_parameters=False, **kwargs):
    """
    Convert XYZ tristimulus values to Jz,az,bz color coordinates.

    Args:
        :xyz: 
            | ndarray with absolute tristimulus values (Y in cd/m²!)
        :ztype:
            | 'jabz', optional
            | String with requested return:
            | Options: 'jabz', 'iabz'
        :use_zcam_parameters:
            | False, optional
            | ZCAM uses a slightly different values (see notes)
    
    Returns:
        :jabz: 
            | ndarray with Jz (or Iz), az, bz color coordinates

    Notes:
     | 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))
     | 3. ZCAM: uses a slightly different value for b (=0.7 instead of 0.66)
     |      and calculates Iz as M' -epsilon (instead L'/2 + M'/2).

    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>`_
        
        2. Safdar, M., Hardeberg, J.Y., Luo, M.R. (2021) 
        "ZCAM, a psychophysical model for colour appearance prediction", 
        Optics Express.
    """
    xyz = np2d(xyz)

    # Setup X,Y,Z to 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]])

    # Premultiply _M_XYZP_TO_LMS and M_to_lms:
    M = _M_XYZP_TO_LMS @ M_to_xyzp

    # Transform X,Y,Z to L,M,S:
    if len(xyz.shape) == 3:
        lms = np.einsum('ij,klj->kli', M, xyz)
    else:
        lms = np.einsum('ij,lj->li', M, xyz)

    # response compression: lms to lms'
    c1, c2, c3, n, p = [
        _PQ_PARAMETERS[x] for x in sorted(_PQ_PARAMETERS.keys())
    ]
    lmsp = ((c1 + c2 * (lms / 10000)**n) / (1 + c3 * (lms / 10000)**n))**p

    # Transform L',M',S' to Iabz:
    if use_zcam_parameters:
        epsilon = 3.70352262101900054e-11
        M = _M_LMSP_TO_IAB_ZCAM
    else:
        epsilon = 0
        M = _M_LMSP_TO_IAB_JABZ

    if len(lms.shape) == 3:
        Iabz = np.einsum('ij,klj->kli', M, lmsp)
    else:
        Iabz = np.einsum('ij,lj->li', M, lmsp)

    Iabz[..., 0] -= epsilon  # correct to ensure zero output

    # convert Iabz' to Jabz coordinates:
    if ztype == 'jabz':
        Iabz[..., 0] = ((1 - 0.56) * Iabz[..., 0] /
                        (1 - 0.56 * Iabz[..., 0])) - 1.6295499532821566e-11
    return Iabz