def plotcircle(center = np.array([0.,0.]),\
               radii = np.arange(0,60,10), \
               angles = np.arange(0,350,10),\
               color = 'k',linestyle = '--', out = None):
    """
    Plot one or more concentric circles.
    
    Args:
        :center: 
            | np.array([0.,0.]) or ndarray with center coordinates, optional
        :radii:
            | np.arange(0,60,10) or ndarray with radii of circle(s), optional
        :angles:
            | np.arange(0,350,10) or ndarray with angles (°), optional
        :color: 
            | 'k', optional
            | Color for plotting.
        :linestyle:
            | '--', optional
            | Linestyle of circles.
        :out: 
            | None, optional
            | If None: plot circles, return (x,y) otherwise.
    """
    xs = np.array([0])
    ys = xs.copy()
    for ri in radii:
        x = ri*np.cos(angles*np.pi/180)
        y = ri*np.sin(angles*np.pi/180)
        xs = np.hstack((xs,x))
        ys = np.hstack((ys,y))
        if out != 'x,y':
            plt.plot(x,y,color = color, linestyle = linestyle)
    if out == 'x,y':
        return xs,ys
Exemple #2
0
def fit_ellipse(xy):
    """
    Fit an ellipse to supplied data points.

    Args:
        :xy: 
            | coordinates of points to fit (Nx2 array)
            
    Returns:
        :v:
            | vector with ellipse parameters [Rmax,Rmin, xc,yc, theta]
    """
    # remove centroid:
    center = xy.mean(axis=0)
    xy = xy - center

    # Fit ellipse:
    x, y = xy[:, 0:1], xy[:, 1:2]
    D = np.hstack((x * x, x * y, y * y, x, y, np.ones_like(x)))
    S, C = np.dot(D.T, D), np.zeros([6, 6])
    C[0, 2], C[2, 0], C[1, 1] = 2, 2, -1
    U, s, V = np.linalg.svd(np.dot(np.linalg.inv(S), C))
    e = U[:, 0]

    # get ellipse axis lengths, center and orientation:
    b, c, d, f, g, a = e[1] / 2, e[2], e[3] / 2, e[4] / 2, e[5], e[0]

    # get ellipse center:
    num = b * b - a * c
    xc = ((c * d - b * f) / num) + center[0]
    yc = ((a * f - b * d) / num) + center[1]

    # get ellipse orientation:
    theta = np.arctan2(np.array(2 * b), np.array((a - c))) / 2

    # axis lengths:
    up = 2 * (a * f * f + c * d * d + g * b * b - 2 * b * d * f - a * c * g)
    down1 = (b * b - a * c) * ((c - a) * np.sqrt(1 + 4 * b * b / ((a - c) *
                                                                  (a - c))) -
                               (c + a))
    down2 = (b * b - a * c) * ((a - c) * np.sqrt(1 + 4 * b * b / ((a - c) *
                                                                  (a - c))) -
                               (c + a))
    a, b = np.sqrt(up / down1), np.sqrt(up / down2)

    # assert that a is the major axis (otherwise swap and correct angle)
    if (b > a):
        b, a = a, b

        # ensure the angle is betwen 0 and 2*pi
        theta = fmod(theta, 2.0 * np.pi)
    return np.hstack((a, b, xc, yc, theta))
Exemple #3
0
def xyz_to_cct_HA(xyzw):
    """
    Convert XYZ tristimulus values to correlated color temperature (CCT). 
    
    Args:
        :xyzw: 
            | ndarray of tristimulus values
        
    Returns:
        :cct: 
            | ndarray of correlated color temperatures estimates
    
    References:
        1. `Hernández-Andrés, Javier; Lee, RL; Romero, J (September 20, 1999). 
        Calculating Correlated Color Temperatures Across the Entire Gamut 
        of Daylight and Skylight Chromaticities.
        Applied Optics. 38 (27), 5703–5709. P
        <https://www.osapublishing.org/ao/abstract.cfm?uri=ao-38-27-5703>`_
            
    Notes: 
        According to paper small error from 3000 - 800 000 K, but a test with 
        Planckians showed errors up to 20% around 500 000 K; 
        e>0.05 for T>200 000, e>0.1 for T>300 000, ...
    """
    if len(xyzw.shape)>2:
        raise Exception('xyz_to_cct_HA(): Input xyzw.ndim must be <= 2 !')
        
    out_of_range_code = np.nan
    xe = [0.3366, 0.3356]
    ye = [0.1735, 0.1691]
    A0 = [-949.86315, 36284.48953]
    A1 = [6253.80338, 0.00228]
    t1 = [0.92159, 0.07861]
    A2 = [28.70599, 5.4535*1e-36]
    t2 = [0.20039, 0.01543]
    A3 = [0.00004, 0.0]
    t3 = [0.07125,1.0]
    cct_ranges = np.array([[3000.0,50000.0],[50000.0,800000.0]])
    
    Yxy = xyz_to_Yxy(xyzw)
    CCT = np.ones((1,Yxy.shape[0]))*out_of_range_code
    for i in range(2):
        n = (Yxy[:,1]-xe[i])/(Yxy[:,2]-ye[i])
        CCT_i = np2d(np.array(A0[i] + A1[i]*np.exp(np.divide(-n,t1[i])) + A2[i]*np.exp(np.divide(-n,t2[i])) + A3[i]*np.exp(np.divide(-n,t3[i]))))
        p = (CCT_i >= (1.0-0.05*(i == 0))*cct_ranges[i][0]) & (CCT_i < (1.0+0.05*(i == 0))*cct_ranges[i][1])
        CCT[p] = CCT_i[p]
        p = (CCT_i < (1.0-0.05)*cct_ranges[0][0]) #smaller than smallest valid CCT value
        CCT[p] = -1
   
    if (np.isnan(CCT.sum()) == True) | (np.any(CCT == -1)):
        print("Warning: xyz_to_cct_HA(): one or more CCTs out of range! --> (CCT < 3 kK,  CCT >800 kK) coded as (-1, NaN) 's")
    return CCT.T
Exemple #4
0
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)
Exemple #5
0
def line_intersect(a1, a2, b1, b2):
    """
    Line intersections of series of two line segments a and b. 
        
    Args:
        :a1: 
            | ndarray (.shape  = (N,2)) specifying end-point 1 of line a
        :a2: 
            | ndarray (.shape  = (N,2)) specifying end-point 2 of line a
        :b1: 
            | ndarray (.shape  = (N,2)) specifying end-point 1 of line b
        :b2: 
            | ndarray (.shape  = (N,2)) specifying end-point 2 of line b
    
    Note: 
        N is the number of line segments a and b.
    
    Returns:
        :returns: 
            | ndarray with line-intersections (.shape = (N,2))
    
    References:
        1. https://stackoverflow.com/questions/3252194/numpy-and-line-intersections
    """
    T = np.array([[0.0, -1.0], [1.0, 0.0]])
    da = np.atleast_2d(a2 - a1)
    db = np.atleast_2d(b2 - b1)
    dp = np.atleast_2d(a1 - b1)
    dap = np.dot(da, T)
    denom = np.sum(dap * db, axis=1)
    num = np.sum(dap * dp, axis=1)
    return np.atleast_2d(num / denom).T * db + b1
Exemple #6
0
def smet2017_D(xyzw, Dmax=None, cieobs='1964_10'):
    """
    Calculate the degree of adaptation based on chromaticity following 
    Smet et al. (2017) 
    
    Args:
        :xyzw: 
            | ndarray with white point data
        :Dmax:
            | None or float, optional
            | Defaults to 0.6539 (max D obtained under experimental conditions, 
              but probably too low due to dark surround leading to incomplete 
              chromatic adaptation even for neutral illuminants 
              resulting in background luminance (fov~50°) of 760 cd/m²))
        :cieobs:
            | '1964_10', optional
            | CMF set used in deriving model in cited paper.
            
    Returns:
        :D: 
            | ndarray with degrees of adaptation
    
    References: 
        1. `Smet, K.A.G.*, Zhai, Q., Luo, M.R., Hanselaer, P., (2017), 
        Study of chromatic adaptation using memory color matches, 
        Part II: colored illuminants, 
        Opt. Express, 25(7), pp. 8350-8365.
        <https://www.osapublishing.org/oe/abstract.cfm?uri=oe-25-7-8350&origin=search)>`_

    """

    # Convert xyzw to log-compressed Macleod_Boyton coordinates:
    Vl, rl, bl = asplit(np.log(xyz_to_Vrb_mb(xyzw, cieobs=cieobs)))

    # apply Dmodel (technically only for cieobs = '1964_10')
    pD = (1.0e7) * np.array([
        0.021081326530436, 4.751255762876845, -0.000000071025181,
        -0.000000063627042, -0.146952821492957, 3.117390441655821
    ])  #D model parameters for gaussian model in log(MB)-space (july 2016)
    if Dmax is None:
        Dmax = 0.6539  # max D obtained under experimental conditions (probably too low due to dark surround leading to incomplete chromatic adaptation even for neutral illuminants resulting in background luminance (fov~50°) of 760 cd/m²)
    return Dmax * math.bvgpdf(x=rl,
                              y=bl,
                              mu=pD[2:4],
                              sigmainv=np.linalg.inv(
                                  np.array([[pD[0], pD[4]], [pD[4], pD[1]]
                                            ])))**pD[5]
Exemple #7
0
 def normalize_to_Lw(Ill, Lw, cieobs, rflM):
     xyzw = lx.spd_to_xyz(Ill, cieobs = cieobs, relative = False)
     for i in range(Ill.shape[0]-1):
         Ill[i+1] = Lw*Ill[i+1]/xyzw[i,1]
     IllM = []
     for i in range(Ill.shape[0]-1):
         IllM.append(np.vstack((Ill1[0],Ill[i+1]*rflM[1:,:])))
     IllM = np.transpose(np.array(IllM),(1,0,2))
     return Ill, IllM
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
Exemple #9
0
def np3d(data):
    """
    Make a tuple, list or numpy array at least a 3d numpy array.
    
    Args:
        :data: 
            | tuple, list, ndarray
        
    Returns:
        :returns: 
            | ndarray with .ndim >= 3
    """
    if isinstance(data, np.ndarray):# assume already atleast_3d when nd.array (user has to ensure input is an array)
        if (len(data.shape)>=3):
            return data   
        else:
            return np.expand_dims(np.atleast_2d(data),axis=0)
    else:
        return np.expand_dims(np.atleast_2d(np.array(data)),axis=0)
Exemple #10
0
def np2dT(data):
    """
    Make a tuple, list or numpy array at least a 2D numpy array and transpose.
    
    Args:
        :data: 
            | tuple, list, ndarray
        
    Returns:
        :returns: 
            | ndarray with .ndim >= 2 and with transposed axes.
    """
    if isinstance(data, np.ndarray):# assume already atleast_2d when nd.array (user has to ensure input is an array)
        if (len(data.shape)>=2):
            return data.T   
        else:
            return np.atleast_2d(data).T
    else:
        return np.atleast_2d(np.array(data)).T
Exemple #11
0
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)
    return np.dot(np.diagflat(1/(np.dot(M,xyz0.T))),M)
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 generate_grid(jab_ranges = None, out = 'grid', \
                  ax = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR),\
                  bx = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \
                  jx = None, limit_grid_radius = 0):
    """
    Generate a grid of color coordinates.
    
    Args:
        :out:
            | 'grid' or 'vectors', optional
            |   - 'grid': outputs a single 2d numpy.nd-vector with the grid coordinates
            |   - 'vector': outputs each dimension seperately.
        :jab_ranges:
            | None or ndarray, optional
            | Specifies the pixelization of color space.
              (ndarray.shape = (3,3), with  first axis: J,a,b, and second 
              axis: min, max, delta)
        :ax:
            | default ndarray or user defined ndarray, optional
            | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) 
        :bx:
            | default ndarray or user defined ndarray, optional
            | default = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR) 
        :jx:
            | None, optional
            | Note that not-None :jab_ranges: override :ax:, :bx: and :jx input.
        :limit_grid_radius:
            | 0, optional
            | A value of zeros keeps grid as specified  by axr,bxr.
            | A value > 0 only keeps (a,b) coordinates within :limit_grid_radius:
            
    Returns:
        :returns: 
            | single ndarray with ax,bx [,jx] 
            |  or
            | seperate ndarrays for each dimension specified.
    """
    # generate grid from jab_ranges array input, otherwise use ax, bx, jx input:
    if jab_ranges is not None:
        if jab_ranges.shape[0] == 3:
            jx = np.arange(jab_ranges[0][0],jab_ranges[0][1],jab_ranges[0][2])
            ax = np.arange(jab_ranges[1][0],jab_ranges[1][1],jab_ranges[1][2])
            bx = np.arange(jab_ranges[2][0],jab_ranges[2][1],jab_ranges[2][2])
        else:
            jx = None
            ax = np.arange(jab_ranges[0][0],jab_ranges[0][1],jab_ranges[0][2])
            bx = np.arange(jab_ranges[1][0],jab_ranges[1][1],jab_ranges[1][2])
   
    # Generate grid from (jx), ax, bx:
    Ax,Bx = np.meshgrid(ax,bx)
    grid = np.dstack((Ax,Bx))
    grid = np.reshape(grid,(np.array(grid.shape[:-1]).prod(),grid.ndim-1))
    if jx is not None:
        for i,v in enumerate(jx):
            gridi = np.hstack((np.ones((grid.shape[0],1))*v,grid))
            if i == 0:
                gridwithJ = gridi
            else:
                gridwithJ = np.vstack((gridwithJ,gridi))
        grid = gridwithJ
    
    if jx is None:
        ax = grid[:,0:1]
        bx = grid[:,1:2]
    else:
        jx = grid[:,0:1]
        ax = grid[:,1:2]
        bx = grid[:,2:3] 
    
    if limit_grid_radius > 0:# limit radius of grid:
        Cr = (ax**2+bx**2)**0.5
        ax = ax[Cr<=limit_grid_radius,None]
        bx = bx[Cr<=limit_grid_radius,None]
        if jx is not None:
            jx = jx[Cr<=limit_grid_radius,None]
    
    # create output:
    if out == 'grid':
        if jx is None:
            return np.hstack((ax,bx))
        else:
            return np.hstack((jx,ax,bx))
    else:
        if jx is None:
            return ax, bx
        else:
            return jx, ax, bx
Exemple #14
0
def cam15u(data,
           fov=10.0,
           inputtype='xyz',
           direction='forward',
           outin='Q,aW,bW',
           parameters=None):
    """
    Convert between CIE 2006 10°  XYZ tristimulus values (or spectral data) 
    and CAM15u color appearance correlates.
    
    Args:
        :data: 
            | ndarray of CIE 2006 10°  XYZ tristimulus values or spectral data
              or color appearance attributes
        :fov: 
            | 10.0, optional
            | Field-of-view of stimulus (for size effect on brightness)
        :inputtpe:
            | '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 -> cam15u
            |   -'inverse': cam15u -> xyz 
        :outin:
            | 'Q,aW,bW' or str, optional
            | 'Q,aW,bW' (brightness and opponent signals for amount-of-neutral)
            |  other options: 'Q,aM,bM' (colorfulness) and 'Q,aS,bS' (saturation)
            | Str specifying the type of 
            |     input (:direction: == 'inverse') and 
            |     output (:direction: == 'forward')
        :parameters:
            | None or dict, optional
            | Set of model parameters.
            |   - None: defaults to luxpy.cam._CAM15U_PARAMETERS 
            |    (see references below)
    
    Returns:
        :returns: 
            | ndarray with color appearance correlates (:direction: == 'forward')
            |  or 
            | XYZ tristimulus values (:direction: == 'inverse')

    References: 
        1. `M. Withouck, K. A. G. Smet, W. R. Ryckaert, and P. Hanselaer, 
        “Experimental driven modelling of the color appearance of 
        unrelated self-luminous stimuli: CAM15u,” 
        Opt. Express, vol. 23, no. 9, pp. 12045–12064, 2015.
        <https://www.osapublishing.org/oe/abstract.cfm?uri=oe-23-9-12045&origin=search>`_
        2. `M. Withouck, K. A. G. Smet, and P. Hanselaer, (2015), 
        “Brightness prediction of different sized unrelated self-luminous stimuli,” 
        Opt. Express, vol. 23, no. 10, pp. 13455–13466. 
        <https://www.osapublishing.org/oe/abstract.cfm?uri=oe-23-10-13455&origin=search>`_  
     """

    if parameters is None:
        parameters = _CAM15U_PARAMETERS

    outin = outin.split(',')

    #unpack model parameters:
    Mxyz2rgb, cA, cAlms, cHK, cM, cW, ca, calms, cb, cblms, cfov, cp, k, unique_hue_data = [
        parameters[x] for x in sorted(parameters.keys())
    ]

    # precomputations:
    invMxyz2rgb = np.linalg.inv(Mxyz2rgb)
    MAab = np.array([cAlms, calms, cblms])
    invMAab = np.linalg.inv(MAab)

    #initialize data and camout:
    data = np2d(data)
    if len(data.shape) == 2:
        data = np.expand_dims(data, axis=0)  # avoid looping if not necessary

    if (data.shape[0] > data.shape[1]):  # loop over shortest dim.
        flipaxis0and1 = True
        data = np.transpose(data, axes=(1, 0, 2))
    else:
        flipaxis0and1 = False

    dshape = list(data.shape)
    dshape[-1] = len(outin)  # requested number of correlates
    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.nan * np.ones(dshape)

    for i in range(data.shape[0]):

        if (inputtype != 'xyz') & (direction == 'forward'):
            xyz = spd_to_xyz(data[i], cieobs='2006_10', relative=False)
            lms = np.dot(_CMF['2006_10']['M'], xyz.T).T  # convert to l,m,s
            rgb = (lms /
                   _CMF['2006_10']['K']) * k  # convert to rho, gamma, beta
        elif (inputtype == 'xyz') & (direction == 'forward'):
            rgb = np.dot(Mxyz2rgb, data[i].T).T

        if direction == 'forward':

            # apply cube-root compression:
            rgbc = rgb**(cp)

            # calculate achromatic and color difference signals, A, a, b:
            Aab = np.dot(MAab, rgbc.T).T
            A, a, b = asplit(Aab)
            A = cA * A
            a = ca * a
            b = cb * b

            # calculate colorfullness like signal M:
            M = cM * ((a**2.0 + b**2.0)**0.5)

            # calculate brightness Q:
            Q = A + cHK[0] * M**cHK[
                1]  # last term is contribution of Helmholtz-Kohlrausch effect on brightness

            # calculate saturation, s:
            s = M / Q

            # calculate amount of white, W:
            W = 100.0 / (1.0 + cW[0] * (s**cW[1]))

            #  adjust Q for size (fov) of stimulus (matter of debate whether to do this before or after calculation of s or W, there was no data on s, M or W for different sized stimuli: after)
            Q = Q * (fov / 10.0)**cfov

            # calculate hue, h and Hue quadrature, H:
            h = hue_angle(a, b, htype='deg')

            if 'H' in outin:
                H = hue_quadrature(h, unique_hue_data=unique_hue_data)
            else:
                H = None

            # calculate cart. co.:
            if 'aM' in outin:
                aM = M * np.cos(h * np.pi / 180.0)
                bM = M * np.sin(h * np.pi / 180.0)

            if 'aS' in outin:
                aS = s * np.cos(h * np.pi / 180.0)
                bS = s * np.sin(h * np.pi / 180.0)

            if 'aW' in outin:
                aW = W * np.cos(h * np.pi / 180.0)
                bW = W * np.sin(h * np.pi / 180.0)

            if (outin != ['Q', 'aW', 'bW']):
                camout[i] = eval('ajoin((' + ','.join(outin) + '))')
            else:
                camout[i] = ajoin((Q, aW, bW))

        elif direction == 'inverse':

            # get Q, M and a, b depending on input type:
            if 'aW' in outin:
                Q, a, b = asplit(data[i])
                Q = Q / (
                    (fov / 10.0)**cfov
                )  #adjust Q for size (fov) of stimulus back to that 10° ref
                W = (a**2.0 + b**2.0)**0.5
                s = (((100 / W) - 1.0) / cW[0])**(1.0 / cW[1])
                M = s * Q

            if 'aM' in outin:
                Q, a, b = asplit(data[i])
                Q = Q / (
                    (fov / 10.0)**cfov
                )  #adjust Q for size (fov) of stimulus back to that 10° ref
                M = (a**2.0 + b**2.0)**0.5

            if 'aS' in outin:
                Q, a, b = asplit(data[i])
                Q = Q / (
                    (fov / 10.0)**cfov
                )  #adjust Q for size (fov) of stimulus back to that 10° ref
                s = (a**2.0 + b**2.0)**0.5
                M = s * Q

            if 'h' in outin:
                Q, WsM, h = asplit(data[i])
                Q = Q / (
                    (fov / 10.0)**cfov
                )  #adjust Q for size (fov) of stimulus back to that 10° ref
                if 'W' in outin:
                    s = (((100.0 / WsM) - 1.0) / cW[0])**(1.0 / cW[1])
                    M = s * Q
                elif 's' in outin:
                    M = WsM * Q
                elif 'M' in outin:
                    M = WsM

            # calculate achromatic signal, A from Q and M:
            A = Q - cHK[0] * M**cHK[1]
            A = A / cA

            # calculate hue angle:
            h = hue_angle(a, b, htype='rad')

            # calculate a,b from M and h:
            a = (M / cM) * np.cos(h)
            b = (M / cM) * np.sin(h)
            a = a / ca
            b = b / cb

            # create Aab:
            Aab = ajoin((A, a, b))

            # calculate rgbc:
            rgbc = np.dot(invMAab, Aab.T).T

            # decompress rgbc to rgb:
            rgb = rgbc**(1 / cp)

            # convert rgb to xyz:
            xyz = np.dot(invMxyz2rgb, rgb.T).T

            camout[i] = xyz

    if flipaxis0and1 == True:  # loop over shortest dim.
        camout = np.transpose(camout, axes=(1, 0, 2))

    if camout.shape[0] == 1:
        camout = np.squeeze(camout, axis=0)

    return camout
def genMonteCarloObs(n_obs=1,
                     fieldsize=10,
                     list_Age=[32],
                     out='LMS',
                     wl=None,
                     allow_negative_values=False):
    """
    Monte-Carlo generation of individual observer cone fundamentals.
    
    Args: 
        :n_obs: 
            | 1, optional
            | Number of observer CMFs to generate.
        :list_Age:
            | list of observer ages or str, optional
            | Defaults to 32 (cfr. CIE2006 CMFs)
            | If 'us_census': use US population census of 2010 
              to generate list_Age.
        :fieldsize: 
            | fieldsize in degrees (between 2° and 10°), optional
            | Defaults to 10°.
        :out: 
            | 'LMS' or str, optional
            | Determines output.
        :wl: 
            | None, optional
            | Interpolation/extraplation of :LMS: output to specified wavelengths.
            | None: output original _WL = np.array([390,780,5])
        :allow_negative_values: 
            | False, optional
            | Cone fundamentals or color matching functions 
            |   should not have negative values.
            |     If False: X[X<0] = 0.
    
    Returns:
        :returns: 
            | LMS [,var_age, vAll] 
            |   - LMS: ndarray with population LMS functions.
            |   - var_age: ndarray with population observer ages.
            |   - vAll: dict with population physiological factors (see .keys()) 
            
    References:
         1. `Asano Y, Fairchild MD, and Blondé L (2016). 
         Individual Colorimetric Observer Model. 
         PLoS One 11, 1–19. 
         <http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0145671>`_
         
         2. `Asano Y, Fairchild MD, Blondé L, and Morvan P (2016). 
         Color matching experiment for highlighting interobserver variability. 
         Color Res. Appl. 41, 530–539. 
         <https://onlinelibrary.wiley.com/doi/abs/10.1002/col.21975>`_
         
         3. `CIE, and CIE (2006). 
         Fundamental Chromaticity Diagram with Physiological Axes - Part I 
         (Vienna: CIE). 
         <http://www.cie.co.at/publications/fundamental-chromaticity-diagram-physiological-axes-part-1>`_ 
         
         4. `Asano's Individual Colorimetric Observer Model 
         <https://www.rit.edu/cos/colorscience/re_AsanoObserverFunctions.php>`_
    """

    # Scale down StdDev by scalars optimized using Asano's 75 observers
    # collected in Germany:
    stdDevAllParam = _INDVCMF_STD_DEV_ALL_PARAM.copy()
    scale_factors = [0.98, 0.98, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
    scale_factors = dict(zip(list(stdDevAllParam.keys()), scale_factors))
    stdDevAllParam = {
        k: v * scale_factors[k]
        for (k, v) in stdDevAllParam.items()
    }

    # Get Normally-distributed Physiological Factors:
    vAll = getMonteCarloParam(n_obs=n_obs)

    if list_Age is 'us_census':
        list_Age = getUSCensusAgeDist()

    # Generate Random Ages with the same probability density distribution
    # as color matching experiment:
    sz_interval = 1
    list_AgeRound = np.round(np.array(list_Age) / sz_interval) * sz_interval
    h = math.histogram(list_AgeRound,
                       bins=np.unique(list_AgeRound),
                       bin_center=True)[0]
    p = h / h.sum()  # probability density distribution

    var_age = np.random.choice(np.unique(list_AgeRound), \
                               size = n_obs, replace = True,\
                               p = p)

    # Set requested wavelength range:
    if wl is not None:
        wl = getwlr(wl3=wl)
    else:
        wl = _WL

    LMS_All = np.nan * np.ones((3 + 1, wl.shape[0], n_obs))
    for k in range(n_obs):
        t_LMS, t_trans_lens, t_trans_macula, t_sens_photopig = cie2006cmfsEx(age = var_age[k], fieldsize = fieldsize, wl = wl,\
                                                                          var_od_lens = vAll['od_lens'][k], var_od_macula = vAll['od_macula'][k], \
                                                                          var_od_L = vAll['od_L'][k], var_od_M = vAll['od_M'][k], var_od_S = vAll['od_S'][k],\
                                                                          var_shft_L = vAll['shft_L'][k], var_shft_M = vAll['shft_M'][k], var_shft_S = vAll['shft_S'][k],\
                                                                          out = 'LMS,trans_lens,trans_macula,sens_photopig')
        LMS_All[:, :, k] = t_LMS


#        listout = out.split(',')
#        if ('trans_lens' in listout) | ('trans_macula' in listout) | ('trans_photopig' in listout):
#            trans_lens[:,k] = t_trans_lens
#            trans_macula[:,k] = t_trans_macula
#            sens_photopig[:,:,k] = t_sens_photopig

    if n_obs == 1:
        LMS_All = np.squeeze(LMS_All, axis=2)

    if ('xyz' in out.lower().split(',')):
        LMS_All = lmsb_to_xyzb(LMS_All,
                               fieldsize,
                               out='xyz',
                               allow_negative_values=allow_negative_values)
        out = out.replace('xyz', 'LMS').replace('XYZ', 'LMS')
    if ('lms' in out.lower().split(',')):
        out = out.replace('lms', 'LMS')

    if (out == 'LMS'):
        return LMS_All
    elif (out == 'LMS,var_age,vAll'):
        return LMS_All, var_age, vAll
    else:
        return eval(out)
#_INDVCMF_STD_DEV_ALL_PARAM_GE['shft_L'] = 2.0
#_INDVCMF_STD_DEV_ALL_PARAM_GE['shft_M'] = 1.5
#_INDVCMF_STD_DEV_ALL_PARAM_GE['shft_S'] = 1.3

# Define dict with Iteratively Derived Cat.Obs.:
t_data = getdata(_INDVCMF_DATA_PATH + 'CatObsPfctr.dat', header=None).T
dict_values = [t_data[:, i + 1] for i in range(t_data.shape[1] - 1)]
dict_keys = list(_INDVCMF_STD_DEV_ALL_PARAM.keys())
_INDVCMF_CATOBSPFCTR = dict(zip(dict_keys, dict_values))
_INDVCMF_CATOBSPFCTR['age'] = t_data[:, 0]

# Matrices for conversion from LMS cone fundamentals to XYZ CMFs:
# (https://www.rit.edu/cos/colorscience/re_AsanoObserverFunctions.php)
# For 2-degree, the 3x3 matrix is:

_INDVCMF_M_2d = np.array([[0.4151, -0.2424, 0.0425], [0.1355, 0.0833, -0.0043],
                          [-0.0093, 0.0125, 0.2136]])

# For 10-degree, the 3x3 matrix is:
_INDVCMF_M_10d = np.array([[0.4499, -0.2630,
                            0.0460], [0.1617, 0.0726, -0.0011],
                           [-0.0036, 0.0054, 0.2291]])

_WL_CRIT = 620  # Asano: 620 nm: wavelenght at which interpolation fails for S-cones
_WL = getwlr([390, 780,
              5])  # wavelength range of specrtal data in _INDVCMF_DATA

def cie2006cmfsEx(age = 32,fieldsize = 10, wl = None,\
                  var_od_lens = 0, var_od_macula = 0, \
                  var_od_L = 0, var_od_M = 0, var_od_S = 0,\
                  var_shft_L = 0, var_shft_M = 0, var_shft_S = 0,\
                  out = 'LMS', allow_negative_values = False):
Exemple #17
0
def cct_to_xyz(ccts,
               duv=None,
               cieobs=_CIEOBS,
               wl=None,
               mode='lut',
               out=None,
               accuracy=0.1,
               force_out_of_lut=True,
               upper_cct_max=10.0 * 20,
               approx_cct_temp=True):
    """
    Convert correlated color temperature (CCT) and Duv (distance above (>0) or 
    below (<0) the Planckian locus) to XYZ tristimulus values.
    
    | Finds xyzw_estimated by minimization of:
    |    
    |    F = numpy.sqrt(((100.0*(cct_min - cct)/(cct))**2.0) 
    |         + (((duv_min - duv)/(duv))**2.0))
    |    
    | with cct,duv the input values and cct_min, duv_min calculated using 
    | luxpy.xyz_to_cct(xyzw_estimated,...).
    
    Args:
        :ccts: 
            | ndarray of cct values
        :duv: 
            | None or ndarray of duv values, optional
            | Note that duv can be supplied together with cct values in :ccts: 
              as ndarray with shape (N,2)
        :cieobs: 
            | luxpy._CIEOBS, optional
            | CMF set used to calculated xyzw.
        :mode: 
            | 'lut' or 'search', optional
            | Determines what method to use.
        :out: 
            | None (or 1), optional
            | If not None or 1: output a ndarray that contains estimated 
              xyz and minimization results: 
            | (cct_min, duv_min, F_min (objective fcn value))
        :wl: 
            | None, optional
            | Wavelengths used when calculating Planckian radiators.
        :accuracy: 
            | float, optional
            | Stop brute-force search when cct :accuracy: is reached.
        :upper_cct_max: 
            | 10.0**20, optional
            | Limit brute-force search to this cct.
        :approx_cct_temp: 
            | True, optional
            | If True: use xyz_to_cct_HA() to get a first estimate of cct to 
              speed up search.
        :force_out_of_lut: 
            | True, optional
            | If True and cct is out of range of the LUT, then switch to 
              brute-force search method, else return numpy.nan values.
        
    Returns:
        :returns: 
            | ndarray with estimated XYZ tristimulus values
    
    Note:
        If duv is not supplied (:ccts:.shape is (N,1) and :duv: is None), 
        source is assumed to be on the Planckian locus.
	 """
    # make ccts a min. 2d np.array:
    if isinstance(ccts, list):
        ccts = np2dT(np.array(ccts))
    else:
        ccts = np2d(ccts)

    if len(ccts.shape) > 2:
        raise Exception('cct_to_xyz(): Input ccts.shape must be <= 2 !')

    # get cct and duv arrays from :ccts:
    cct = np2d(ccts[:, 0, None])

    if (duv is None) & (ccts.shape[1] == 2):
        duv = np2d(ccts[:, 1, None])
    elif duv is not None:
        duv = np2d(duv)

    #get estimates of approximate xyz values in case duv = None:
    BB = cri_ref(ccts=cct, wl3=wl, ref_type=['BB'])
    xyz_est = spd_to_xyz(data=BB, cieobs=cieobs, out=1)
    results = np.ones([ccts.shape[0], 3]) * np.nan

    if duv is not None:

        # optimization/minimization setup:
        def objfcn(uv_offset,
                   uv0,
                   cct,
                   duv,
                   out=1):  #, cieobs = cieobs, wl = wl, mode = mode):
            uv0 = np2d(uv0 + uv_offset)
            Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1)
            cct_min, duv_min = xyz_to_cct(Yuv_to_xyz(Yuv0),
                                          cieobs=cieobs,
                                          out='cct,duv',
                                          wl=wl,
                                          mode=mode,
                                          accuracy=accuracy,
                                          force_out_of_lut=force_out_of_lut,
                                          upper_cct_max=upper_cct_max,
                                          approx_cct_temp=approx_cct_temp)
            F = np.sqrt(((100.0 * (cct_min[0] - cct[0]) / (cct[0]))**2.0) +
                        (((duv_min[0] - duv[0]) / (duv[0]))**2.0))
            if out == 'F':
                return F
            else:
                return np.concatenate((cct_min, duv_min, np2d(F)), axis=1)

        # loop through each xyz_est:
        for i in range(xyz_est.shape[0]):
            xyz0 = xyz_est[i]
            cct_i = cct[i]
            duv_i = duv[i]
            cct_min, duv_min = xyz_to_cct(xyz0,
                                          cieobs=cieobs,
                                          out='cct,duv',
                                          wl=wl,
                                          mode=mode,
                                          accuracy=accuracy,
                                          force_out_of_lut=force_out_of_lut,
                                          upper_cct_max=upper_cct_max,
                                          approx_cct_temp=approx_cct_temp)

            if np.abs(duv[i]) > _EPS:
                # find xyz:
                Yuv0 = xyz_to_Yuv(xyz0)
                uv0 = Yuv0[0][1:3]

                OptimizeResult = minimize(fun=objfcn,
                                          x0=np.zeros((1, 2)),
                                          args=(uv0, cct_i, duv_i, 'F'),
                                          method='Nelder-Mead',
                                          options={
                                              "maxiter": np.inf,
                                              "maxfev": np.inf,
                                              'xatol': 0.000001,
                                              'fatol': 0.000001
                                          })
                betas = OptimizeResult['x']
                #betas = np.zeros(uv0.shape)
                if out is not None:
                    results[i] = objfcn(betas, uv0, cct_i, duv_i, out=3)

                uv0 = np2d(uv0 + betas)
                Yuv0 = np.concatenate((np2d([100.0]), uv0), axis=1)
                xyz_est[i] = Yuv_to_xyz(Yuv0)

            else:
                xyz_est[i] = xyz0

    if (out is None) | (out == 1):
        return xyz_est
    else:
        # Also output results of minimization:
        return np.concatenate((xyz_est, results), axis=1)
Exemple #18
0
def cam18sl(data, datab = None, Lb = [100], fov = 10.0, inputtype = 'xyz', direction = 'forward', outin = 'Q,aW,bW', parameters = None):
    """
    Convert between CIE 2006 10°  XYZ tristimulus values (or spectral data) 
    and CAM18sl color appearance correlates.
    
    Args:
        :data: 
            | ndarray of CIE 2006 10°  absolute XYZ tristimulus values or spectral data
              or color appearance attributes of stimulus
        :datab: 
            | ndarray of CIE 2006 10°  absolute XYZ tristimulus values or spectral data
              of stimulus background
        :Lb: 
            | [100], optional
            | Luminance (cd/m²) value(s) of background(s) calculated using the CIE 2006 10° CMFs 
            | (only used in case datab == None and the background is assumed to be an Equal-Energy-White)
        :fov: 
            | 10.0, optional
            | Field-of-view of stimulus (for size effect on brightness)
        :inputtpe:
            | '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 -> cam18sl
            |   -'inverse': cam18sl -> xyz 
        :outin:
            | 'Q,aW,bW' or str, optional
            | 'Q,aW,bW' (brightness and opponent signals for amount-of-neutral)
            |  other options: 'Q,aM,bM' (colorfulness) and 'Q,aS,bS' (saturation)
            | Str specifying the type of 
            |     input (:direction: == 'inverse') and 
            |     output (:direction: == 'forward')
        :parameters:
            | None or dict, optional
            | Set of model parameters.
            |   - None: defaults to luxpy.cam._CAM18SL_PARAMETERS 
            |    (see references below)
    
    Returns:
        :returns: 
            | ndarray with color appearance correlates (:direction: == 'forward')
            |  or 
            | XYZ tristimulus values (:direction: == 'inverse')
            
    Notes:
        | * Instead of using the CIE 1964 10° CMFs in some places of the model,
        |   the CIE 2006 10° CMFs are used througout, making it more self_consistent.
        |   This has an effect on the k scaling factors (now different those in CAM15u) 
        |   and the illuminant E normalization for use in the chromatic adaptation transform.
        |   (see future erratum to Hermans et al., 2018)
        | * The paper also used an equation for the amount of white W, which is
        |   based on a Q value not expressed in 'bright' ('cA' = 0.937 instead of 123). 
        |   This has been corrected for in the luxpy version of the model, i.e.
        |   _CAM18SL_PARAMETERS['cW'][0] has been changed from 2.29 to 1/11672.
        |   (see future erratum to Hermans et al., 2018)

    References: 
        1. `Hermans, S., Smet, K. A. G., & Hanselaer, P. (2018). 
        "Color appearance model for self-luminous stimuli."
        Journal of the Optical Society of America A, 35(12), 2000–2009. 
        <https://doi.org/10.1364/JOSAA.35.002000>`_ 
     """
    
    if parameters is None:
        parameters = _CAM18SL_PARAMETERS
        
    outin = outin.split(',')    
    
    #unpack model parameters:
    cA, cAlms, cHK, cM, cW, ca, calms, cb, cblms, cfov, k, naka, unique_hue_data = [parameters[x] for x in sorted(parameters.keys())]
    
    # precomputations:
    Mlms2xyz = np.linalg.inv(_CMF['2006_10']['M'])
    MAab = np.array([cAlms,calms,cblms])
    invMAab = np.linalg.inv(MAab)    
    
    #-------------------------------------------------
    # setup EEW reference field and default background field (Lr should be equal to Lb):
    # Get Lb values:
    if datab is not None:
        if inputtype != 'xyz':
            Lb = spd_to_xyz(datab, cieobs = '2006_10', relative = False)[...,1:2]
        else:
            Lb = datab[...,1:2]
    else:
        if isinstance(Lb,list):
            Lb = np2dT(Lb)

    # Setup EEW ref of same luminance as datab:
    if inputtype == 'xyz':
        wlr = getwlr(_CAM18SL_WL3)
    else:
        if datab is None:
            wlr = data[0] # use wlr of stimulus data
        else:
            wlr = datab[0] # use wlr of background data
    datar = np.vstack((wlr,np.ones((Lb.shape[0], wlr.shape[0])))) # create eew
    xyzr = spd_to_xyz(datar, cieobs = '2006_10', relative = False) # get abs. tristimulus values
    datar[1:] = datar[1:]/xyzr[...,1:2]*Lb
    # Create datab if None:
    if (datab is None):
        if inputtype != 'xyz':
            datab = datar.copy()
        else:
            datab = spd_to_xyz(datar, cieobs = '2006_10', relative = False)
            datar = datab.copy()

 
    # prepare data and datab for loop over backgrounds: 
    # make axis 1 of datab have 'same' dimensions as data:         
    if (data.ndim == 2): 
        data = np.expand_dims(data, axis = 1)  # add light source axis 1     

    if inputtype == 'xyz': 
        if datab.shape[0] == 1: #make datab and datar have same lights source dimension (used to store different backgrounds) size as data
            datab = np.repeat(datab,data.shape[1],axis=0)  
            datar = np.repeat(datar,data.shape[1],axis=0)               
    else:
        if datab.shape[0] == 2:
            datab = np.vstack((datab[0],np.repeat(datab[1:], data.shape[1], axis = 0)))
        if datar.shape[0] == 2:
            datar = np.vstack((datar[0],np.repeat(datar[1:], data.shape[1], axis = 0)))

    # Flip light source/ background dim to axis 0:
    data = np.transpose(data, axes = (1,0,2))

    #-------------------------------------------------
    
    #initialize camout:     
    dshape = list(data.shape)
    dshape[-1] = len(outin) # requested number of correlates
    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.nan*np.ones(dshape)
    
  
    for i in range(data.shape[0]):
       
        # get rho, gamma, beta of background and reference white:
        if (inputtype != 'xyz'):
            xyzb = spd_to_xyz(np.vstack((datab[0], datab[i+1:i+2,:])), cieobs = '2006_10', relative = False)
            xyzr = spd_to_xyz(np.vstack((datar[0], datar[i+1:i+2,:])), cieobs = '2006_10', relative = False)
        else:
            xyzb = datab[i:i+1,:] 
            xyzr = datar[i:i+1,:] 

        lmsb = np.dot(_CMF['2006_10']['M'],xyzb.T).T # convert to l,m,s
        rgbb = (lmsb / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta
        #lmsr = np.dot(_CMF['2006_10']['M'],xyzr.T).T # convert to l,m,s
        #rgbr = (lmsr / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta
        #rgbr = rgbr/rgbr[...,1:2]*Lb[i] # calculated EEW cone excitations at same luminance values as background
        rgbr = np.ones(xyzr.shape)*Lb[i] # explicitely equal EEW cone excitations at same luminance values as background

        if direction == 'forward':
            # get rho, gamma, beta of stimulus:
            if (inputtype != 'xyz'):
                xyz = spd_to_xyz(data[i], cieobs = '2006_10', relative = False)   
            elif (inputtype == 'xyz'):
                xyz = data[i]
            lms = np.dot(_CMF['2006_10']['M'],xyz.T).T # convert to l,m,s
            rgb = (lms / _CMF['2006_10']['K']) * k # convert to rho, gamma, beta

            # apply von-kries cat with D = 1:
            if (rgbb == 0).any():
                Mcat = np.eye(3)
            else:
                Mcat = np.diag((rgbr/rgbb)[0])
            rgba = np.dot(Mcat,rgb.T).T

            # apply naka-rushton compression:
            rgbc = naka_rushton(rgba, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling'])

            #rgbc = np.ones(rgbc.shape)*rgbc.mean() # test if eew ends up at origin
            
            # calculate achromatic and color difference signals, A, a, b:
            Aab = np.dot(MAab, rgbc.T).T
            A,a,b = asplit(Aab)
            a = ca*a
            b = cb*b

            # calculate colorfullness like signal M:
            M = cM*((a**2.0 + b**2.0)**0.5)

            # calculate brightness Q:
            Q = cA*(A + cHK[0]*M**cHK[1]) # last term is contribution of Helmholtz-Kohlrausch effect on brightness

            # calculate saturation, s:
            s = M / Q

            # calculate amount of white, W:
            W = 1 / (1.0 + cW[0]*(s**cW[1]))

            #  adjust Q for size (fov) of stimulus (matter of debate whether to do this before or after calculation of s or W, there was no data on s, M or W for different sized stimuli: after)
            Q = Q*(fov/10.0)**cfov

            # calculate hue, h and Hue quadrature, H:
            h = hue_angle(a,b, htype = 'deg')
            if 'H' in outin:
                H = hue_quadrature(h, unique_hue_data = unique_hue_data)
            else:
                H = None

            # calculate cart. co.:
            if 'aM' in outin:
                aM = M*np.cos(h*np.pi/180.0)
                bM = M*np.sin(h*np.pi/180.0)
            
            if 'aS' in outin:
                aS = s*np.cos(h*np.pi/180.0)
                bS = s*np.sin(h*np.pi/180.0)
            
            if 'aW' in outin:
                aW = W*np.cos(h*np.pi/180.0)
                bW = W*np.sin(h*np.pi/180.0)

            if (outin != ['Q','aW','bW']):
                camout[i] =  eval('ajoin(('+','.join(outin)+'))')
            else:
                camout[i] = ajoin((Q,aW,bW))
    
        
        elif direction == 'inverse':

            # get Q, M and a, b depending on input type:        
            if 'aW' in outin:
                Q,a,b = asplit(data[i])
                Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref
                W = (a**2.0 + b**2.0)**0.5
                s = (((1.0 / W) - 1.0)/cW[0])**(1.0/cW[1])
                M = s*Q
                
            
            if 'aM' in outin:
                Q,a,b = asplit(data[i])
                Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref
                M = (a**2.0 + b**2.0)**0.5
            
            if 'aS' in outin:
                Q,a,b = asplit(data[i])
                Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref
                s = (a**2.0 + b**2.0)**0.5
                M = s*Q
                      
            if 'h' in outin:
                Q, WsM, h = asplit(data[i])
                Q = Q / ((fov/10.0)**cfov) #adjust Q for size (fov) of stimulus back to that 10° ref
                if 'W' in outin:
                     s = (((1.0 / WsM) - 1.0)/cW[0])**(1.0/cW[1])
                     M = s*Q
                elif 's' in outin:
                     M = WsM*Q
                elif 'M' in outin:
                     M = WsM
            
            # calculate achromatic signal, A from Q and M:
            A = Q/cA - cHK[0]*M**cHK[1]

            # calculate hue angle:
            h = hue_angle(a,b, htype = 'rad')
            
            # calculate a,b from M and h:
            a = (M/cM)*np.cos(h)
            b = (M/cM)*np.sin(h)

            a = a/ca
            b = b/cb

            # create Aab:
            Aab = ajoin((A,a,b))    

            # calculate rgbc:
            rgbc = np.dot(invMAab, Aab.T).T    

            # decompress rgbc to (adapted) rgba :
            rgba = naka_rushton(rgbc, n = naka['n'], sig = naka['sig'](rgbr.mean()), noise = naka['noise'], scaling = naka['scaling'], direction = 'inverse')

            # apply inverse von-kries cat with D = 1:
            rgb = np.dot(np.diag((rgbb/rgbr)[0]),rgba.T).T

            # convert rgb to lms to xyz:
            lms = rgb/k*_CMF['2006_10']['K']  
            xyz = np.dot(Mlms2xyz,lms.T).T 
            
            camout[i] = xyz
    
    if camout.shape[0] == 1:
        camout = np.squeeze(camout,axis = 0)
    
    return camout
_CSPACE_AXES['xyz'] = ['X', 'Y', 'Z']
_CSPACE_AXES['lms'] = ['L', 'M', 'S']
_CSPACE_AXES['lab'] = ['L*', "a*", "b*"]
_CSPACE_AXES['luv'] = ['L*', "u*", "v*"]
_CSPACE_AXES['ipt'] = ['I', "P", "T"]
_CSPACE_AXES['wuv'] = ['W*', "U*", "V*"]
_CSPACE_AXES['Vrb_mb'] = [
    'V (Macleod-Boyton)', "r (Macleod-Boyton)", "b (Macleod-Boyton)"
]
_CSPACE_AXES['cct'] = ['', 'cct', 'duv']
_CSPACE_AXES['srgb'] = ['sR', 'sG', 'sB']

# pre-calculate matrices for conversion of xyz to lms and back for use in xyz_to_ipt() and ipt_to_xyz():
_IPT_M = {
    'lms2ipt':
    np.array([[0.4000, 0.4000, 0.2000], [4.4550, -4.8510, 0.3960],
              [0.8056, 0.3572, -1.1628]]),
    'xyz2lms': {
        x: math.normalize_3x3_matrix(
            _CMF[x]['M'], spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=x))
        for x in sorted(_CMF['types'])
    }
}
_COLORTF_DEFAULT_WHITE_POINT = np.array([[100.0, 100.0,
                                          100.0]])  # ill. E white point


#------------------------------------------------------------------------------
#---chromaticity coordinates---------------------------------------------------
#------------------------------------------------------------------------------
def xyz_to_Yxy(xyz, **kwargs):
    """
# load TM30 spd data base:
_IESTM30 = {
    'S': {
        'data': getdata(_S_PATH + 'IESTM30_Sspds.dat', kind='np').transpose()
    }
}
_IESTM30['S']['info'] = getdata(_S_PATH + 'IESTM30_Sinfo.txt',
                                kind='np',
                                header='infer',
                                verbosity=False)
_IESTM30_S = _IESTM30['S']

#------------------------------------------------------------------------------
# Illuminant library: set some typical CIE illuminants:
E = np.array([np.linspace(380, 780, 401), np.ones(401)])
D65 = np.array(
    [[
        380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
        394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407,
        408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421,
        422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435,
        436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449,
        450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463,
        464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477,
        478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491,
        492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505,
        506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519,
        520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533,
        534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547,
        548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561,
Exemple #21
0
def calculate_VF_PX_models(S, cri_type = _VF_CRI_DEFAULT, sampleset = None, pool = False, \
                           pcolorshift = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10),\
                                          'Cref' : _VF_MAXR, 'sig' : _VF_SIG, 'labels' : '#'},\
                           vfcolor = 'k', verbosity = 0):
    """
    Calculate Vector Field and Pixel color shift models.
    
    Args:
        :cri_type: 
            | _VF_CRI_DEFAULT or str or dict, optional
            | Specifies type of color fidelity model to use. 
            | Controls choice of ref. ill., sample set, averaging, scaling, etc.
            | See luxpy.cri.spd_to_cri for more info.
        :sampleset:
            | None or str or ndarray, optional
            | Sampleset to be used when calculating vector field model.
        :pool:
            | False, optional
            | If :S: contains multiple spectra, True pools all jab data before 
              modeling the vector field, while False models a different field 
              for each spectrum.
        :pcolorshift: 
            | default dict (see below) or user defined dict, optional
            | Dict containing the specification input 
              for apply_poly_model_at_hue_x().
            | Default dict = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10),
            |                'Cref' : _VF_MAXR, 
            |                'sig' : _VF_SIG, 
            |                'labels' : '#'} 
            | The polynomial models of degree 5 and 6 can be fully specified or 
              summarized by the model parameters themselved OR by calculating the
             dCoverC and dH at resp. 5 and 6 hues.
        :vfcolor:
            | 'k', optional
            | For plotting the vector fields.
        :verbosity: 
            | 0, optional
            | Report warnings or not.
    
    Returns:
        :returns:
            | :dataVF:, :dataPX: 
            | Dicts, for more info, see output description of resp.: 
              luxpy.cri.VF_colorshift_model() and luxpy.cri.PX_colorshift_model()
    """
    # calculate VectorField cri_color_shift model:
    dataVF = VF_colorshift_model(S, cri_type = cri_type, sampleset = sampleset, vfcolor = vfcolor, pcolorshift = pcolorshift, pool = pool, verbosity = verbosity)
    
    # Set jab_ranges and _deltas for PX-model pixel calculations:
    PX_jab_deltas = np.array([_VF_DELTAR,_VF_DELTAR,_VF_DELTAR]) #set same as for vectorfield generation
    PX_jab_ranges = np.vstack(([0,100,_VF_DELTAR],[-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR], [-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR]))#IES4880 gamut
   
    # Calculate shift vectors using vectorfield and pixel methods:
    delta_SvsVF_vshift_ab_mean = np.nan*np.ones((len(dataVF),1))
    delta_SvsVF_vshift_ab_mean_normalized = delta_SvsVF_vshift_ab_mean.copy()
    delta_PXvsVF_vshift_ab_mean = np.nan*np.ones((len(dataVF),1))
    delta_PXvsVF_vshift_ab_mean_normalized = delta_PXvsVF_vshift_ab_mean.copy()
    dataPX = [[] for k in range(len(dataVF))]
    for Snr in range(len(dataVF)):

        # Calculate shifts using pixel method, PX:
        dataPX[Snr] = PX_colorshift_model(dataVF[Snr]['Jab']['Jabt'][:,0,:],dataVF[Snr]['Jab']['Jabr'][:,0,:], jab_ranges = PX_jab_ranges, jab_deltas = PX_jab_deltas,limit_grid_radius = _VF_MAXR)
        
        # Calculate shift difference between Samples (S) and VectorField model predictions (VF):
        delta_SvsVF_vshift_ab = dataVF[Snr]['vshifts']['vshift_ab_s'] - dataVF[Snr]['vshifts']['vshift_ab_s_vf']
        delta_SvsVF_vshift_ab_mean[Snr] = np.nanmean(np.sqrt((delta_SvsVF_vshift_ab[...,1:3]**2).sum(axis = delta_SvsVF_vshift_ab[...,1:3].ndim-1)), axis=0)
        delta_SvsVF_vshift_ab_mean_normalized[Snr] = delta_SvsVF_vshift_ab_mean[Snr]/dataVF[Snr]['Jab']['DEi'].mean(axis=0)
        
        # Calculate shift difference between PiXel method (PX) and VectorField (VF):
        delta_PXvsVF_vshift_ab = dataPX[Snr]['vshifts']['vectorshift_ab_J0'] - dataVF[Snr]['vshifts']['vshift_ab_vf']
        delta_PXvsVF_vshift_ab_mean[Snr] = np.nanmean(np.sqrt((delta_PXvsVF_vshift_ab[...,1:3]**2).sum(axis = delta_PXvsVF_vshift_ab[...,1:3].ndim-1)), axis=0)
        delta_PXvsVF_vshift_ab_mean_normalized[Snr] = delta_PXvsVF_vshift_ab_mean[Snr]/dataVF[Snr]['Jab']['DEi'].mean(axis=0)

        dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean'] = delta_PXvsVF_vshift_ab_mean[Snr]
        dataVF[Snr]['vshifts']['delta_SvsVF_vshift_ab_mean'] = delta_SvsVF_vshift_ab_mean[Snr]
        dataVF[Snr]['vshifts']['delta_SvsVF_vshift_ab_mean_normalized'] = delta_SvsVF_vshift_ab_mean_normalized[Snr]
        dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized'] = delta_PXvsVF_vshift_ab_mean_normalized[Snr]
        dataPX[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean'] = dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean']
        dataPX[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized'] = dataVF[Snr]['vshifts']['delta_PXvsVF_vshift_ab_mean_normalized']

    return dataVF, dataPX
Exemple #22
0
def spd_to_ies_tm30_metrics(SPD, cri_type = None, \
                            hbins = 16, start_hue = 0.0,\
                            scalef = 100, \
                            vf_model_type = _VF_MODEL_TYPE, \
                            vf_pcolorshift = _VF_PCOLORSHIFT,\
                            scale_vf_chroma_to_sample_chroma = False):
    """
    Calculates IES TM30 metrics from spectral data.      
      
      Args:
        :data:
            | numpy.ndarray with spectral data 
        :cri_type:
            | None, optional
            | If None: defaults to cri_type = 'iesrf'.
            | Not none values of :hbins:, :start_hue: and :scalef: overwrite 
              input in cri_type['rg_pars'] 
        :hbins:
            | None or numpy.ndarray with sorted hue bin centers (°), optional
        :start_hue: 
            | None, optional
        :scalef:
            | None, optional
            | Scale factor for reference circle.
        :vf_pcolorshift:
            | _VF_PCOLORSHIFT or user defined dict, optional
            | The polynomial models of degree 5 and 6 can be fully specified or 
              summarized by the model parameters themselved OR by calculating the
              dCoverC and dH at resp. 5 and 6 hues. :VF_pcolorshift: specifies 
              these hues and chroma level.
        :scale_vf_chroma_to_sample_chroma: 
            | False, optional
            | Scale chroma of reference and test vf fields such that average of 
              binned reference chroma equals that of the binned sample chroma
              before calculating hue bin metrics.
            
    Returns:
        :data: 
            | dict with color rendering data:
            | - 'SPD'  : ndarray test SPDs
            | - 'bjabt': ndarray with binned jab data under test SPDs
            | - 'bjabr': ndarray with binned jab data under reference SPDs
            | - 'cct'  : ndarray with CCT of test SPD
            | - 'duv'  : ndarray with distance to blackbody locus of test SPD
            | - 'Rf'   : ndarray with general color fidelity indices
            | - 'Rg'   : ndarray with gamut area indices
            | - 'Rfi'  : ndarray with specific color fidelity indices
            | - 'Rfhi' : ndarray with local (hue binned) fidelity indices
            | - 'Rcshi': ndarray with local chroma shifts indices
            | - 'Rhshi': ndarray with local hue shifts indices
            | - 'Rt'  : ndarray with general metameric uncertainty index Rt
            | - 'Rti' : ndarray with specific metameric uncertainty indices Rti
            | - 'Rfhi_vf' : ndarray with local (hue binned) fidelity indices 
            |               obtained from VF model predictions at color space
            |               pixel coordinates
            | - 'Rcshi_vf': ndarray with local chroma shifts indices 
            |               (same as above)
            | - 'Rhshi_vf': ndarray with local hue shifts indices 
            |               (same as above)
    """
    if cri_type is None:
        cri_type = 'iesrf'

    #Calculate color rendering measures for SPDs in data:
    out = 'Rf,Rg,cct,duv,Rfi,jabt,jabr,Rfhi,Rcshi,Rhshi,cri_type'
    if isinstance(cri_type, str):  # get dict
        cri_type = _CRI_DEFAULTS[cri_type].copy()
    if hbins is not None:
        cri_type['rg_pars']['nhbins'] = hbins
    if start_hue is not None:
        cri_type['rg_pars']['start_hue'] = start_hue
    if scalef is not None:
        cri_type['rg_pars']['normalized_chroma_ref'] = scalef
    Rf, Rg, cct, duv, Rfi, jabt, jabr, Rfhi, Rcshi, Rhshi, cri_type = spd_to_cri(
        SPD, cri_type=cri_type, out=out)
    rg_pars = cri_type['rg_pars']

    #Calculate Metameric uncertainty and base color shifts:
    dataVF = VF_colorshift_model(SPD,
                                 cri_type=cri_type,
                                 model_type=vf_model_type,
                                 cspace=cri_type['cspace'],
                                 sampleset=eval(cri_type['sampleset']),
                                 pool=False,
                                 pcolorshift=vf_pcolorshift,
                                 vfcolor=0)
    Rf_ = np.array([dataVF[i]['metrics']['Rf'] for i in range(len(dataVF))]).T
    Rt = np.array([dataVF[i]['metrics']['Rt'] for i in range(len(dataVF))]).T
    Rti = np.array([dataVF[i]['metrics']['Rti']
                    for i in range(len(dataVF))][0])

    # Get normalized and sliced sample data for plotting:
    rg_pars = cri_type['rg_pars']
    nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [
        rg_pars[x] for x in sorted(rg_pars.keys())
    ]
    normalized_chroma_ref = scalef
    # np.sqrt((jabr[...,1]**2 + jabr[...,2]**2)).mean(axis = 0).mean()

    if scale_vf_chroma_to_sample_chroma == True:
        normalize_gamut = False
        bjabt, bjabr = gamut_slicer(
            jabt,
            jabr,
            out='jabt,jabr',
            nhbins=nhbins,
            start_hue=start_hue,
            normalize_gamut=normalize_gamut,
            normalized_chroma_ref=normalized_chroma_ref,
            close_gamut=True)
        Cr_s = (np.sqrt(bjabr[:-1, ..., 1]**2 + bjabr[:-1, ..., 2]**2)).mean(
            axis=0)  # for rescaling vector field average reference chroma

    normalize_gamut = True  #(for plotting)
    bjabt, bjabr = gamut_slicer(jabt,
                                jabr,
                                out='jabt,jabr',
                                nhbins=nhbins,
                                start_hue=start_hue,
                                normalize_gamut=normalize_gamut,
                                normalized_chroma_ref=normalized_chroma_ref,
                                close_gamut=True)

    Rfhi_vf = np.empty(Rfhi.shape)
    Rcshi_vf = np.empty(Rcshi.shape)
    Rhshi_vf = np.empty(Rhshi.shape)
    for i in range(cct.shape[0]):

        # Get normalized and sliced VF data for hue specific metrics:
        vfjabt = np.hstack(
            (np.ones(dataVF[i]['fielddata']['vectorfield']['axt'].shape),
             dataVF[i]['fielddata']['vectorfield']['axt'],
             dataVF[i]['fielddata']['vectorfield']['bxt']))
        vfjabr = np.hstack(
            (np.ones(dataVF[i]['fielddata']['vectorfield']['axr'].shape),
             dataVF[i]['fielddata']['vectorfield']['axr'],
             dataVF[i]['fielddata']['vectorfield']['bxr']))
        nhbins, normalize_gamut, normalized_chroma_ref, start_hue = [
            rg_pars[x] for x in sorted(rg_pars.keys())
        ]
        vfbjabt, vfbjabr, vfbDEi = gamut_slicer(
            vfjabt,
            vfjabr,
            out='jabt,jabr,DEi',
            nhbins=nhbins,
            start_hue=start_hue,
            normalize_gamut=normalize_gamut,
            normalized_chroma_ref=normalized_chroma_ref,
            close_gamut=False)

        if scale_vf_chroma_to_sample_chroma == True:
            #rescale vfbjabt and vfbjabr to same chroma level as bjabr.
            Cr_vfb = np.sqrt(vfbjabr[..., 1]**2 + vfbjabr[..., 2]**2)
            Cr_vf = np.sqrt(vfjabr[..., 1]**2 + vfjabr[..., 2]**2)
            hr_vf = np.arctan2(vfjabr[..., 2], vfjabr[..., 1])
            Ct_vf = np.sqrt(vfjabt[..., 1]**2 + vfjabt[..., 2]**2)
            ht_vf = np.arctan2(vfjabt[..., 2], vfjabt[..., 1])
            fC = Cr_s.mean() / Cr_vfb.mean()
            vfjabr[..., 1] = fC * Cr_vf * np.cos(hr_vf)
            vfjabr[..., 2] = fC * Cr_vf * np.sin(hr_vf)
            vfjabt[..., 1] = fC * Ct_vf * np.cos(ht_vf)
            vfjabt[..., 2] = fC * Ct_vf * np.sin(ht_vf)
            vfbjabt, vfbjabr, vfbDEi = gamut_slicer(
                vfjabt,
                vfjabr,
                out='jabt,jabr,DEi',
                nhbins=nhbins,
                start_hue=start_hue,
                normalize_gamut=normalize_gamut,
                normalized_chroma_ref=normalized_chroma_ref,
                close_gamut=False)

        scale_factor = cri_type['scale']['cfactor']
        scale_fcn = cri_type['scale']['fcn']
        vfRfhi, vfRcshi, vfRhshi = jab_to_rhi(
            jabt=vfbjabt,
            jabr=vfbjabr,
            DEi=vfbDEi,
            cri_type=cri_type,
            scale_factor=scale_factor,
            scale_fcn=scale_fcn,
            use_bin_avg_DEi=True
        )  # [:-1,...] removes last row from jab as this was added to close the gamut.

        Rfhi_vf[:, i:i + 1] = vfRfhi
        Rhshi_vf[:, i:i + 1] = vfRhshi
        Rcshi_vf[:, i:i + 1] = vfRcshi

    # Create dict with CRI info:
    data = {'SPD' : SPD, 'cct' : cct, 'duv' : duv, 'bjabt' : bjabt, 'bjabr' : bjabr,\
           'Rf' : Rf, 'Rg' : Rg, 'Rfi': Rfi, 'Rfhi' : Rfhi, 'Rchhi' : Rcshi, 'Rhshi' : Rhshi, \
           'Rt' : Rt, 'Rti' : Rti,  'Rfhi_vf' : Rfhi_vf, 'Rfcshi_vf' : Rcshi_vf, 'Rfhshi_vf' : Rhshi_vf, \
           'dataVF' : dataVF,'cri_type' : cri_type}
    return data
Exemple #23
0
    'ca':
    1.0,
    'calms': [1.0, -12 / 11, 1 / 11],
    'cb':
    0.117,
    'cblms': [1.0, 1.0, -2.0],
    'unique_hue_data':
    _CAM15U_UNIQUE_HUE_DATA,
    'cM':
    135.52,
    'cHK': [2.559, 0.561],
    'cW': [2.29, 2.68],
    'cfov':
    0.271,
    'Mxyz2rgb':
    np.array([[0.211831, 0.815789, -0.042472], [-0.492493, 1.378921, 0.098745],
              [0.0, 0.0, 0.985188]])
}

_CAM15U_NAKA_RUSHTON_PARAMETERS = {
    'n': None,
    'sig': None,
    'scaling': None,
    'noise': None
}

_CAM15U_SURROUND_PARAMETERS = {
    'surrounds': ['dark'],
    'dark': {
        'c': None,
        'Nc': None,
        'F': None,
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 get_poly_model(jabt, jabr, modeltype = _VF_MODEL_TYPE):
    """
    Setup base color shift model (delta_a, delta_b), 
    determine model parameters and accuracy.
    
    | Calculates a base color shift (delta) from the ref. chromaticity ar, br.
    
    Args:
        :jabt: 
            | ndarray with jab color coordinates under the test SPD.
        :jabr: 
            | ndarray with jab color coordinates under the reference SPD.
        :modeltype:
            | _VF_MODEL_TYPE or 'M6' or 'M5', optional
            | Specifies degree 5 or degree 6 polynomial model in ab-coordinates.
              (see notes below)
            
    Returns:
        :returns: 
            | (poly_model, 
            |       pmodel, 
            |       dab_model, 
            |        dab_res, 
            |        dCHoverC_res, 
            |        dab_std, 
            |        dCHoverC_std)
            |
            | :poly_model: function handle to model
            | :pmodel: ndarray with model parameters
            | :dab_model: ndarray with ab model predictions from ar, br.
            | :dab_res: ndarray with residuals between 'da,db' of samples and 
            |            'da,db' predicted by the model.
            | :dCHoverC_res: ndarray with residuals between 'dCoverC,dH' 
            |                 of samples and 'dCoverC,dH' predicted by the model.
            |     Note: dCoverC = (Ct - Cr)/Cr and dH = ht - hr 
            |         (predicted from model, see notes below)
            | :dab_std: ndarray with std of :dab_res:
            | :dCHoverC_std: ndarray with std of :dCHoverC_res: 

    Notes: 
        1. Model types:
            | poly5_model = lambda a,b,p:         p[0]*a + p[1]*b + p[2]*(a**2) + p[3]*a*b + p[4]*(b**2)
            | poly6_model = lambda a,b,p:  p[0] + p[1]*a + p[2]*b + p[3]*(a**2) + p[4]*a*b + p[5]*(b**2)
        
        2. Calculation of dCoverC and dH:
            | dCoverC = (np.cos(hr)*da + np.sin(hr)*db)/Cr
            | dHoverC = (np.cos(hr)*db - np.sin(hr)*da)/Cr    
    """
    at = jabt[...,1]
    bt = jabt[...,2]
    ar = jabr[...,1]
    br = jabr[...,2]
    
    # A. Calculate da, db:
    da = at - ar
    db = bt - br
    
    # B.1 Calculate model matrix:
    # 5-parameter model:
    M5 = np.array([[np.sum(ar*ar), np.sum(ar*br), np.sum(ar*ar**2),np.sum(ar*ar*br),np.sum(ar*br**2)],
            [np.sum(br*ar), np.sum(br*br), np.sum(br*ar**2),np.sum(br*ar*br),np.sum(br*br**2)],
            [np.sum((ar**2)*ar), np.sum((ar**2)*br), np.sum((ar**2)*ar**2),np.sum((ar**2)*ar*br),np.sum((ar**2)*br**2)],
            [np.sum(ar*br*ar), np.sum(ar*br*br), np.sum(ar*br*ar**2),np.sum(ar*br*ar*br),np.sum(ar*br*br**2)],
            [np.sum((br**2)*ar), np.sum((br**2)*br), np.sum((br**2)*ar**2),np.sum((br**2)*ar*br),np.sum((br**2)*br**2)]])
    #6-parameters model
    M6 = np.array([[ar.size,np.sum(1.0*ar), np.sum(1.0*br), np.sum(1.0*ar**2),np.sum(1.0*ar*br),np.sum(1.0*br**2)],
            [np.sum(ar*1.0),np.sum(ar*ar), np.sum(ar*br), np.sum(ar*ar**2),np.sum(ar*ar*br),np.sum(ar*br**2)],
            [np.sum(br*1.0),np.sum(br*ar), np.sum(br*br), np.sum(br*ar**2),np.sum(br*ar*br),np.sum(br*br**2)],
            [np.sum((ar**2)*1.0),np.sum((ar**2)*ar), np.sum((ar**2)*br), np.sum((ar**2)*ar**2),np.sum((ar**2)*ar*br),np.sum((ar**2)*br**2)],
            [np.sum(ar*br*1.0),np.sum(ar*br*ar), np.sum(ar*br*br), np.sum(ar*br*ar**2),np.sum(ar*br*ar*br),np.sum(ar*br*br**2)],
            [np.sum((br**2)*1.0),np.sum((br**2)*ar), np.sum((br**2)*br), np.sum((br**2)*ar**2),np.sum((br**2)*ar*br),np.sum((br**2)*br**2)]])
    
    # B.2 Define model function:
    poly5_model = lambda a,b,p: p[0]*a + p[1]*b + p[2]*(a**2) + p[3]*a*b + p[4]*(b**2)
    poly6_model = lambda a,b,p: p[0] + p[1]*a + p[2]*b + p[3]*(a**2) + p[4]*a*b + p[5]*(b**2)
    
    if modeltype == 'M5':
        M = M5
        poly_model = poly5_model
    else:
        M = M6
        poly_model = poly6_model

    M = np.linalg.inv(M)


    # C.1 Data a,b analysis output:
    if modeltype == 'M5':
        da_model_parameters = np.dot(M, np.array([np.sum(da*ar), np.sum(da*br), np.sum(da*ar**2),np.sum(da*ar*br),np.sum(da*br**2)]))
        db_model_parameters = np.dot(M, np.array([np.sum(db*ar), np.sum(db*br), np.sum(db*ar**2),np.sum(db*ar*br),np.sum(db*br**2)]))
    else:
        da_model_parameters = np.dot(M, np.array([np.sum(da*1.0),np.sum(da*ar), np.sum(da*br), np.sum(da*ar**2),np.sum(da*ar*br),np.sum(da*br**2)]))
        db_model_parameters = np.dot(M, np.array([np.sum(db*1.0),np.sum(db*ar), np.sum(db*br), np.sum(db*ar**2),np.sum(db*ar*br),np.sum(db*br**2)]))
    pmodel = np.vstack((da_model_parameters,db_model_parameters))

    # D.1 Calculate model da, db:
    da_model = poly_model(ar,br,pmodel[0])
    db_model = poly_model(ar,br,pmodel[1])
    dab_model = np.hstack((da_model,db_model))

    # D.2 Calculate residuals for da & db:
    da_res = da - da_model
    db_res = db - db_model
    dab_res = np.hstack((da_res,db_res))
    dab_std = np.vstack((np.std(da_res,axis=0),np.std(db_res,axis=0)))

    # E Calculate href, Cref:
    href = np.arctan2(br,ar)
    Cref = (ar**2 + br**2)**0.5
    
    # F Calculate dC/C, dH/C for data and model and calculate residuals:
    dCoverC = (np.cos(href)*da + np.sin(href)*db)/Cref
    dHoverC = (np.cos(href)*db - np.sin(href)*da)/Cref
    dCoverC_model = (np.cos(href)*da_model + np.sin(href)*db_model)/Cref
    dHoverC_model = (np.cos(href)*db_model - np.sin(href)*da_model)/Cref
    dCoverC_res = dCoverC - dCoverC_model
    dHoverC_res = dHoverC - dHoverC_model
    dCHoverC_std = np.vstack((np.std(dCoverC_res,axis = 0),np.std(dHoverC_res,axis = 0)))
    
    dCHoverC_res = np.hstack((href,dCoverC_res,dHoverC_res))

    return poly_model, pmodel, dab_model, dab_res, dCHoverC_res, dab_std, dCHoverC_std
def xyz_to_Ydlep(xyz,
                 cieobs=_CIEOBS,
                 xyzw=_COLORTF_DEFAULT_WHITE_POINT,
                 flip_axes=False,
                 **kwargs):
    """
    Convert XYZ tristimulus values to Y, dominant (complementary) wavelength
    and excitation purity.

    Args:
        :xyz:
            | ndarray with tristimulus values
        :xyzw:
            | None or ndarray with tristimulus values of a single (!) native white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.
        :flip_axes:
            | False, optional
            | If True: flip axis 0 and axis 1 in Ydelep to increase speed of loop in function.
            |          (single xyzw with is not flipped!)
    Returns:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
    """

    xyz3 = np3d(xyz).copy().astype(np.float)

    # flip axis so that shortest dim is on axis0 (save time in looping):
    if (xyz3.shape[0] < xyz3.shape[1]) & (flip_axes == True):
        axes12flipped = True
        xyz3 = xyz3.transpose((1, 0, 2))
    else:
        axes12flipped = False

    # convert xyz to Yxy:
    Yxy = xyz_to_Yxy(xyz3)
    Yxyw = xyz_to_Yxy(xyzw)

    # get spectrum locus Y,x,y and wavelengths:
    SL = _CMF[cieobs]['bar']

    wlsl = SL[0]
    Yxysl = xyz_to_Yxy(SL[1:4].T)[:, None]

    # center on xyzw:
    Yxy = Yxy - Yxyw
    Yxysl = Yxysl - Yxyw
    Yxyw = Yxyw - Yxyw

    #split:
    Y, x, y = asplit(Yxy)
    Yw, xw, yw = asplit(Yxyw)
    Ysl, xsl, ysl = asplit(Yxysl)

    # calculate hue:
    h = math.positive_arctan(x, y, htype='deg')

    hsl = math.positive_arctan(xsl, ysl, htype='deg')

    hsl_max = hsl[0]  # max hue angle at min wavelength
    hsl_min = hsl[-1]  # min hue angle at max wavelength

    dominantwavelength = np.empty(Y.shape)
    purity = np.empty(Y.shape)
    for i in range(xyz3.shape[1]):

        # find index of complementary wavelengths/hues:
        pc = np.where(
            (h[:, i] >= hsl_max) & (h[:, i] <= hsl_min + 360.0)
        )  # hue's requiring complementary wavelength (purple line)
        h[:, i][pc] = h[:, i][pc] - np.sign(
            h[:, i][pc] - 180.0
        ) * 180.0  # add/subtract 180° to get positive complementary wavelength

        # find 2 closest hues in sl:
        #hslb,hib = meshblock(hsl,h[:,i:i+1])
        hib, hslb = np.meshgrid(h[:, i:i + 1], hsl)
        dh = np.abs(hslb - hib)
        q1 = dh.argmin(axis=0)  # index of closest hue
        dh[q1] = 1000.0
        q2 = dh.argmin(axis=0)  # index of second closest hue

        dominantwavelength[:, i] = wlsl[q1] + np.divide(
            np.multiply((wlsl[q2] - wlsl[q1]),
                        (h[:, i] - hsl[q1, 0])), (hsl[q2, 0] - hsl[q1, 0])
        )  # calculate wl corresponding to h: y = y1 + (y2-y1)*(x-x1)/(x2-x1)
        dominantwavelength[:, i][pc] = -dominantwavelength[:, i][
            pc]  #complementary wavelengths are specified by '-' sign

        # calculate excitation purity:
        x_dom_wl = xsl[q1, 0] + (xsl[q2, 0] - xsl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate x of dom. wl
        y_dom_wl = ysl[q1, 0] + (ysl[q2, 0] - ysl[q1, 0]) * (h[:, i] - hsl[
            q1, 0]) / (hsl[q2, 0] - hsl[q1, 0])  # calculate y of dom. wl
        d_wl = (x_dom_wl**2.0 +
                y_dom_wl**2.0)**0.5  # distance from white point to sl
        d = (x[:, i]**2.0 +
             y[:, i]**2.0)**0.5  # distance from white point to test point
        purity[:, i] = d / d_wl

        # correct for those test points that have a complementary wavelength
        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x[:, i], y[:, i])).T
        xyw = np.hstack((xw, yw))
        xypl1 = np.hstack((xsl[0, None], ysl[0, None]))
        xypl2 = np.hstack((xsl[-1, None], ysl[-1, None]))
        da = (xy - xyw)
        db = (xypl2 - xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da, T)
        denom = np.sum(dap * db, axis=1, keepdims=True)
        num = np.sum(dap * dp, axis=1, keepdims=True)
        xy_linecross = (num / denom) * db + xypl1
        d_linecross = np.atleast_2d(
            (xy_linecross[:, 0]**2.0 + xy_linecross[:, 1]**2.0)**0.5).T  #[0]
        purity[:, i][pc] = d[pc] / d_linecross[pc][:, 0]
    Ydlep = np.dstack((xyz3[:, :, 1], dominantwavelength, purity))

    if axes12flipped == True:
        Ydlep = Ydlep.transpose((1, 0, 2))
    else:
        Ydlep = Ydlep.transpose((0, 1, 2))
    return Ydlep.reshape(xyz.shape)
Exemple #27
0
def xyz_to_cct_search(xyzw,
                      cieobs=_CIEOBS,
                      out='cct',
                      wl=None,
                      accuracy=0.1,
                      upper_cct_max=10.0**20,
                      approx_cct_temp=True):
    """
    Convert XYZ tristimulus values to correlated color temperature (CCT) and 
    Duv(distance above (> 0) or below ( < 0) the Planckian locus) by a 
    brute-force search. 

    | The algorithm uses an approximate cct_temp (HA approx., see xyz_to_cct_HA) 
      as starting point or uses the middle of the allowed cct-range 
      (1e2 K - 1e20 K, higher causes overflow) on a log-scale, then constructs 
      a 4-step section of the blackbody (Planckian) locus on which to find the
      minimum distance to the 1960 uv chromaticity of the test source.

    Args:
        :xyzw: 
            | ndarray of tristimulus values
        :cieobs: 
            | luxpy._CIEOBS, optional
            | CMF set used to calculated xyzw.
        :out: 
            | 'cct' (or 1), optional
            | Determines what to return.
            | Other options: 'duv' (or -1), 'cct,duv'(or 2), "[cct,duv]" (or -2)
        :wl: 
            | None, optional
            | Wavelengths used when calculating Planckian radiators.
        :accuracy: 
            | float, optional
            | Stop brute-force search when cct :accuracy: is reached.
        :upper_cct_max: 
            | 10.0**20, optional
            | Limit brute-force search to this cct.
        :approx_cct_temp: 
            | True, optional
            | If True: use xyz_to_cct_HA() to get a first estimate of cct to 
              speed up search.

    Returns:
        :returns: 
            | ndarray with:
            |    cct: out == 'cct' (or 1)
            |    duv: out == 'duv' (or -1)
            |    cct, duv: out == 'cct,duv' (or 2)
            |    [cct,duv]: out == "[cct,duv]" (or -2) 
    
    Notes:
        This program is more accurate, but slower than xyz_to_cct_ohno!
        Note that cct must be between 1e3 K - 1e20 K 
        (very large cct take a long time!!!)
    """

    xyzw = np2d(xyzw)

    if len(xyzw.shape) > 2:
        raise Exception('xyz_to_cct_search(): Input xyzw.shape must be <= 2 !')

    # get 1960 u,v of test source:
    Yuvt = xyz_to_Yuv(np.squeeze(
        xyzw))  # remove possible 1-dim + convert xyzw to CIE 1976 u',v'
    #axis_of_v3t = len(Yuvt.shape)-1 # axis containing color components
    ut = Yuvt[:, 1, None]  #.take([1],axis = axis_of_v3t) # get CIE 1960 u
    vt = (2 / 3) * Yuvt[:, 2,
                        None]  #.take([2],axis = axis_of_v3t) # get CIE 1960 v

    # Initialize arrays:
    ccts = np.ones((xyzw.shape[0], 1)) * np.nan
    duvs = ccts.copy()

    #calculate preliminary solution(s):
    if (approx_cct_temp == True):
        ccts_est = xyz_to_cct_HA(xyzw)
        procent_estimates = np.array([[3000.0, 100000.0, 0.05],
                                      [100000.0, 200000.0, 0.1],
                                      [200000.0, 300000.0, 0.25],
                                      [300000.0, 400000.0, 0.4],
                                      [400000.0, 600000.0, 0.4],
                                      [600000.0, 800000.0, 0.4],
                                      [800000.0, np.inf, 0.25]])
    else:
        upper_cct = np.array(upper_cct_max)
        lower_cct = np.array(10.0**2)
        cct_scale_fun = lambda x: np.log10(x)
        cct_scale_ifun = lambda x: np.power(10.0, x)
        dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2
        ccttemp = np.array([cct_scale_ifun(cct_scale_fun(lower_cct) + dT)])
        ccts_est = np2d(ccttemp * np.ones((xyzw.shape[0], 1)))
        dT_approx_cct_False = dT.copy()

    # Loop through all ccts:
    for i in range(xyzw.shape[0]):

        #initialize CCT search parameters:
        cct = np.nan
        duv = np.nan
        ccttemp = ccts_est[i].copy()

        # Take care of (-1, NaN)'s from xyz_to_cct_HA signifying (CCT < lower, CCT > upper) bounds:
        approx_cct_temp_temp = approx_cct_temp
        if (approx_cct_temp == True):
            cct_scale_fun = lambda x: x
            cct_scale_ifun = lambda x: x
            if (ccttemp != -1) & (
                    np.isnan(ccttemp) == False
            ):  # within validity range of CCT estimator-function
                for ii in range(procent_estimates.shape[0]):
                    if (ccttemp >=
                        (1.0 - 0.05 *
                         (ii == 0)) * procent_estimates[ii, 0]) & (
                             ccttemp < (1.0 + 0.05 *
                                        (ii == 0)) * procent_estimates[ii, 1]):
                        procent_estimate = procent_estimates[ii, 2]
                        break

                dT = np.multiply(
                    ccttemp, procent_estimate
                )  # determines range around CCTtemp (25% around estimate) or 100 K
            elif (ccttemp == -1) & (np.isnan(ccttemp) == False):
                ccttemp = np.array([procent_estimates[0, 0] / 2])
                procent_estimate = 1  # cover 0 K to min_CCT of estimator
                dT = np.multiply(ccttemp, procent_estimate)
            elif (np.isnan(ccttemp) == True):
                upper_cct = np.array(upper_cct_max)
                lower_cct = np.array(10.0**2)
                cct_scale_fun = lambda x: np.log10(x)
                cct_scale_ifun = lambda x: np.power(10.0, x)
                dT = (cct_scale_fun(upper_cct) - cct_scale_fun(lower_cct)) / 2
                ccttemp = np.array(
                    [cct_scale_ifun(cct_scale_fun(lower_cct) + dT)])
                approx_cct_temp = False
        else:
            dT = dT_approx_cct_False

        nsteps = 3
        signduv = 1.0
        ccttemp = ccttemp[0]
        delta_cct = dT
        while ((delta_cct > accuracy)):  # keep converging on CCT

            #generate range of ccts:
            ccts_i = cct_scale_ifun(
                np.linspace(
                    cct_scale_fun(ccttemp) - dT,
                    cct_scale_fun(ccttemp) + dT, nsteps + 1))

            ccts_i[ccts_i < 100.0] = 100.0  # avoid nan's in calculation

            # Generate BB:
            BB = cri_ref(ccts_i, wl3=wl, ref_type=['BB'], cieobs=cieobs)

            # Calculate xyz:
            xyz = spd_to_xyz(BB, cieobs=cieobs)

            # Convert to CIE 1960 u,v:
            Yuv = xyz_to_Yuv(np.squeeze(
                xyz))  # remove possible 1-dim + convert xyz to CIE 1976 u',v'
            #axis_of_v3 = len(Yuv.shape)-1 # axis containing color components
            u = Yuv[:, 1, None]  # get CIE 1960 u
            v = (2.0 / 3.0) * Yuv[:, 2, None]  # get CIE 1960 v

            # Calculate distance between list of uv's and uv of test source:
            dc = ((ut[i] - u)**2 + (vt[i] - v)**2)**0.5
            if np.isnan(dc.min()) == False:
                #eps = _EPS
                q = dc.argmin()

                if np.size(
                        q
                ) > 1:  #to minimize calculation time: only calculate median when necessary
                    cct = np.median(ccts[q])
                    duv = np.median(dc[q])
                    q = np.median(q)
                    q = int(q)  #must be able to serve as index

                else:
                    cct = ccts_i[q]
                    duv = dc[q]

                if (q == 0):
                    ccttemp = cct_scale_ifun(
                        np.array(cct_scale_fun([cct])) + 2 * dT / nsteps)
                    #dT = 2.0*dT/nsteps
                    continue  # look in higher section of planckian locus

                if (q == np.size(ccts_i)):
                    ccttemp = cct_scale_ifun(
                        np.array(cct_scale_fun([cct])) - 2 * dT / nsteps)
                    #dT = 2.0*dT/nsteps
                    continue  # look in lower section of planckian locus

                if (q > 0) & (q < np.size(ccts_i) - 1):
                    dT = 2 * dT / nsteps
                    # get Duv sign:
                    d_p1m1 = ((u[q + 1] - u[q - 1])**2.0 +
                              (v[q + 1] - v[q - 1])**2.0)**0.5

                    x = (dc[q - 1]**2.0 - dc[q + 1]**2.0 +
                         d_p1m1**2.0) / 2.0 * d_p1m1
                    vBB = v[q - 1] + ((v[q + 1] - v[q - 1]) * (x / d_p1m1))
                    signduv = np.sign(vt[i] - vBB)

                #calculate difference with previous intermediate solution:
                delta_cct = abs(cct - ccttemp)

                ccttemp = np.array(cct)  #%set new intermediate CCT
                approx_cct_temp = approx_cct_temp_temp
            else:
                ccttemp = np.nan
                cct = np.nan
                duv = np.nan

        duvs[i] = signduv * abs(duv)
        ccts[i] = cct

    # Regulate output:
    if (out == 'cct') | (out == 1):
        return np2d(ccts)
    elif (out == 'duv') | (out == -1):
        return np2d(duvs)
    elif (out == 'cct,duv') | (out == 2):
        return np2d(ccts), np2d(duvs)
    elif (out == "[cct,duv]") | (out == -2):
        return np.vstack((ccts, duvs)).T
def Ydlep_to_xyz(Ydlep,
                 cieobs=_CIEOBS,
                 xyzw=_COLORTF_DEFAULT_WHITE_POINT,
                 flip_axes=False,
                 **kwargs):
    """
    Convert Y, dominant (complementary) wavelength and excitation purity to XYZ
    tristimulus values.

    Args:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
        :xyzw: 
            | None or narray with tristimulus values of a single (!) native white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.
        :flip_axes:
            | False, optional
            | If True: flip axis 0 and axis 1 in Ydelep to increase speed of loop in function.
            |          (single xyzw with is not flipped!)
    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """

    Ydlep3 = np3d(Ydlep).copy().astype(np.float)

    # flip axis so that longest dim is on first axis  (save time in looping):
    if (Ydlep3.shape[0] < Ydlep3.shape[1]) & (flip_axes == True):
        axes12flipped = True
        Ydlep3 = Ydlep3.transpose((1, 0, 2))
    else:
        axes12flipped = False

    # convert xyzw to Yxyw:
    Yxyw = xyz_to_Yxy(xyzw)
    Yxywo = Yxyw.copy()

    # get spectrum locus Y,x,y and wavelengths:
    SL = _CMF[cieobs]['bar']
    wlsl = SL[0, None].T
    Yxysl = xyz_to_Yxy(SL[1:4].T)[:, None]

    # center on xyzw:
    Yxysl = Yxysl - Yxyw
    Yxyw = Yxyw - Yxyw

    #split:
    Y, dom, pur = asplit(Ydlep3)
    Yw, xw, yw = asplit(Yxyw)
    Ywo, xwo, ywo = asplit(Yxywo)
    Ysl, xsl, ysl = asplit(Yxysl)

    # loop over longest dim:
    x = np.empty(Y.shape)
    y = np.empty(Y.shape)
    for i in range(Ydlep3.shape[1]):

        # find closest wl's to dom:
        #wlslb,wlib = meshblock(wlsl,np.abs(dom[i,:])) #abs because dom<0--> complemtary wl
        wlib, wlslb = np.meshgrid(np.abs(dom[:, i]), wlsl)

        dwl = np.abs(wlslb - wlib)
        q1 = dwl.argmin(axis=0)  # index of closest wl
        dwl[q1] = 10000.0
        q2 = dwl.argmin(axis=0)  # index of second closest wl

        # calculate x,y of dom:
        x_dom_wl = xsl[q1, 0] + (xsl[q2, 0] - xsl[q1, 0]) * (
            np.abs(dom[:, i]) - wlsl[q1, 0]) / (wlsl[q2, 0] - wlsl[q1, 0]
                                                )  # calculate x of dom. wl
        y_dom_wl = ysl[q1, 0] + (ysl[q2, 0] - ysl[q1, 0]) * (
            np.abs(dom[:, i]) - wlsl[q1, 0]) / (wlsl[q2, 0] - wlsl[q1, 0]
                                                )  # calculate y of dom. wl

        # calculate x,y of test:
        d_wl = (x_dom_wl**2.0 +
                y_dom_wl**2.0)**0.5  # distance from white point to dom
        d = pur[:, i] * d_wl
        hdom = math.positive_arctan(x_dom_wl, y_dom_wl, htype='deg')
        x[:, i] = d * np.cos(hdom * np.pi / 180.0)
        y[:, i] = d * np.sin(hdom * np.pi / 180.0)

        # complementary:
        pc = np.where(dom[:, i] < 0.0)
        hdom[pc] = hdom[pc] - np.sign(dom[:, i][pc] -
                                      180.0) * 180.0  # get positive hue angle

        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x_dom_wl, y_dom_wl)).T
        xyw = np.vstack((xw, yw)).T
        xypl1 = np.vstack((xsl[0, None], ysl[0, None])).T
        xypl2 = np.vstack((xsl[-1, None], ysl[-1, None])).T
        da = (xy - xyw)
        db = (xypl2 - xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da, T)
        denom = np.sum(dap * db, axis=1, keepdims=True)
        num = np.sum(dap * dp, axis=1, keepdims=True)
        xy_linecross = (num / denom) * db + xypl1
        d_linecross = np.atleast_2d(
            (xy_linecross[:, 0]**2.0 + xy_linecross[:, 1]**2.0)**0.5).T[:, 0]
        x[:, i][pc] = pur[:, i][pc] * d_linecross[pc] * np.cos(
            hdom[pc] * np.pi / 180)
        y[:, i][pc] = pur[:, i][pc] * d_linecross[pc] * np.sin(
            hdom[pc] * np.pi / 180)
    Yxy = np.dstack((Ydlep3[:, :, 0], x + xwo, y + ywo))
    if axes12flipped == True:
        Yxy = Yxy.transpose((1, 0, 2))
    else:
        Yxy = Yxy.transpose((0, 1, 2))
    return Yxy_to_xyz(Yxy).reshape(Ydlep.shape)
Exemple #29
0
        'Dtype': 'cat02',
        'catmode': '1>2'
    },
    'rg_pars': {
        'nhbins': None,
        'start_hue': 0.0,
        'normalize_gamut': False,
        'normalized_chroma_ref': 100
    },
    'cri_specific_pars': {
        'similarity_ai':
        np.array([[-0.09651, 0.41354, 40.64, 16.55, -0.17],
                  [0.16548, 0.38877, 58.27, 20.37, -0.59],
                  [0.32825, 0.49673, 35.97, 18.05, -6.04],
                  [0.02115, -0.13658, 261.62, 110.99, -44.86],
                  [-0.12686, -0.22593, 99.06, 55.90, -39.86],
                  [0.18488, 0.01172, 58.23, 62.55, -22.86],
                  [-0.03440, 0.23480, 94.71, 32.12, 2.90],
                  [0.04258, 0.05040, 205.54, 53.08, -35.20],
                  [0.15829, 0.13624, 90.21, 70.83, -19.01],
                  [-0.01933, -0.02168, 742.97, 297.66, -227.30]])
    }
}


###############################################################################
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: 
Exemple #30
0
    '1931_2_juddvos1978', '1951_20_scotopic', 'cie_std_dev_obs_f1'
]
_CMF_K_VALUES = [
    683.002, 683.6, 683.002, 683.002, 683.002, 683.002, 1700.06, 0.0
]

#def _dictkv(keys=None,values=None, ordered = True):
#    # Easy input of of keys and values into dict (both should be iterable lists)
#    if ordered is True:
#        return odict(zip(keys,values))
#    else:
#        return dict(zip(keys,values))

_CMF_M_1931_2 = np.array(
    [  # definition of 3x3 matrices to convert from xyz to lms
        [0.38971, 0.68898, -0.07868], [-0.22981, 1.1834, 0.04641],
        [0.0, 0.0, 1.0]
    ])
_CMF_M_2006_2 = np.array([[0.21057582, 0.85509764, -0.039698265],
                          [-0.41707637, 1.1772611, 0.078628251],
                          [0.0, 0.0, 0.51683501]])
_CMF_M_2006_10 = np.array([[0.21701045, 0.83573367, -0.043510597],
                           [-0.42997951, 1.2038895, 0.086210895],
                           [0.0, 0.0, 0.46579234]])

# Note that for the following, no conversion has been defined, so the 1931 HPE matrix is used:
_CMF_M_1964_10 = np.array([[0.38971, 0.68898, -0.07868],
                           [-0.22981, 1.1834, 0.04641], [0.0, 0.0, 1.0]])
_CMF_M_1931_2_JUDD1951 = np.array([[0.38971, 0.68898, -0.07868],
                                   [-0.22981, 1.1834, 0.04641],
                                   [0.0, 0.0, 1.0]])