Пример #1
0
def check_dimensions(data, xyzw, caller='cat.apply()'):
    """
    Check if dimensions of data and xyzw match. 
    
    | Does nothing when they do, but raises error if dimensions don't match.
    
    Args:
        :data: 
            | ndarray with color data.
        :xyzw: 
            | ndarray with white point tristimulus values.
        :caller: 
            | str with caller function for error handling, optional
        
    Returns:
        :returns: 
            | ndarray with input color data, 
            | Raises error if dimensions don't match.
    """
    xyzw = np2d(xyzw)
    data = np2d(data)
    if ((xyzw.shape[0] > 1) & (data.shape[0] != xyzw.shape[0]) &
        (data.ndim == 2)):
        raise Exception(
            '{}: Cannot match dim of xyzw with data: xyzw.shape[0]>1 & != data.shape[0]'
            .format(caller))
Пример #2
0
def spd_to_power(data, ptype='ru', cieobs=_CIEOBS):
    """
    Calculate power of spectral data in radiometric, photometric 
    or quantal energy units.
    
    Args:
        :data: 
            | ndarray with spectral data
        :ptype: 
            | 'ru' or str, optional
            | str: - 'ru': in radiometric units 
            |      - 'pu': in photometric units 
            |      - 'pusa': in photometric units with Km corrected 
            |                to standard air (cfr. CIE TN003-2015)
            |      - 'qu': in quantal energy units
        :cieobs: 
            | _CIEOBS or str, optional
            | Type of cmf set to use for photometric units.
    
    Returns:
        returns: 
            | ndarray with normalized spectral data (SI units)
    """
    # get wavelength spacing:
    dl = getwld(data[0])

    if ptype == 'ru':  #normalize to radiometric units
        p = np2d(np.dot(data[1:], dl * np.ones(data.shape[1]))).T

    elif ptype == 'pusa':  # normalize in photometric units with correction of Km to standard air

        # Calculate correction factor for Km in standard air:
        na = _BB['na']  # n for standard air
        c = _BB['c']  # m/s light speed
        lambdad = c / (na * 54 * 1e13) / (1e-9
                                          )  # 555 nm lambda in standard air
        Km_correction_factor = 1 / (
            1 - (1 - 0.9998567) *
            (lambdad - 555))  # correction factor for Km in standard air

        # Get Vlambda and Km (for E):
        Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2)
        Km *= Km_correction_factor
        p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T

    elif ptype == 'pu':  # normalize in photometric units

        # Get Vlambda and Km (for E):
        Vl, Km = vlbar(cieobs=cieobs, wl_new=data[0], out=2)
        p = Km * np2d(np.dot(data[1:], dl * Vl[1])).T

    elif ptype == 'qu':  # normalize to quantual units

        # Get Quantal conversion factor:
        fQ = ((1e-9) / (_BB['h'] * _BB['c']))
        p = np2d(fQ * np.dot(data[1:], dl * data[0])).T

    return p
Пример #3
0
def xyz_to_lms(xyz, cieobs=_CIEOBS, M=None, **kwargs):
    """
    Convert XYZ tristimulus values to LMS cone fundamental responses.

    Args:
        :xyz: 
            | ndarray with tristimulus values
        :cieobs: 
            | _CIEOBS or str, optional
        :M: 
            | None, optional
            | Conversion matrix for xyz to lms.
            |   If None: use the one defined by :cieobs:

    Returns:
        :lms: 
            | ndarray with LMS cone fundamental responses
    """
    xyz = np2d(xyz)

    if M is None:
        M = _CMF[cieobs]['M']

    # convert xyz to lms:
    if len(xyz.shape) == 3:
        lms = np.einsum('ij,klj->kli', M, xyz)
    else:
        lms = np.einsum('ij,lj->li', M, xyz)
    return lms
Пример #4
0
def lms_to_xyz(lms, cieobs=_CIEOBS, M=None, **kwargs):
    """
    Convert LMS cone fundamental responses to XYZ tristimulus values.

    Args:
        :lms: 
            | ndarray with LMS cone fundamental responses
        :cieobs:
            | _CIEOBS or str, optional
        :M: 
            | None, optional
            | Conversion matrix for xyz to lms.
            |   If None: use the one defined by :cieobs:

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    lms = np2d(lms)

    if M is None:
        M = _CMF[cieobs]['M']

    # convert from lms to xyz:
    if len(lms.shape) == 3:
        xyz = np.einsum('ij,klj->kli', np.linalg.inv(M), lms)
    else:
        xyz = np.einsum('ij,lj->li', np.linalg.inv(M), lms)

    return xyz
Пример #5
0
def parse_x1x2_parameters(x,
                          target_shape,
                          catmode,
                          expand_2d_to_3d=None,
                          default=[1.0, 1.0]):
    """
   Parse input parameters x and make them the target_shape for easy calculation. 
   
   | Input in main function can now be a single value valid for all xyzw or 
     an array with a different value for each xyzw.
   
   Args:
        :x: 
            | list[float, float] or ndarray
        :target_shape: 
            | tuple with shape information
        :catmode: 
            | '1>0>2, optional
            |    -'1>0>2': Two-step CAT 
            |      from illuminant 1 to baseline illuminant 0 to illuminant 2.
            |    -'1>0': One-step CAT 
            |      from illuminant 1 to baseline illuminant 0.
            |    -'0>2': One-step CAT 
            |      from baseline illuminant 0 to illuminant 2. 
        :expand_2d_to_3d: 
            | None, optional 
            | [will be removed in future, serves no purpose]
            | Expand :x: from 2 to 3 dimensions.
        :default:
            | [1.0,1.0], optional
            | Default values for :x:
    
   Returns:
       :returns: 
           | (ndarray, ndarray) for x10 and x20

   """
    if x is None:
        x10 = np.ones(target_shape) * default[0]
        if (catmode == '1>0>2') | (catmode == '1>2'):
            x20 = np.ones(target_shape) * default[1]
        else:
            x20 = np.zeros(target_shape)
            x20.fill(np.nan)
    else:
        x = np2d(x)
        if (catmode == '1>0>2') | (catmode == '1>2'):
            if x.shape[-1] == 2:
                x10 = np.ones(target_shape) * x[..., 0]
                x20 = np.ones(target_shape) * x[..., 1]
            else:
                x10 = np.ones(target_shape) * x
                x20 = x10.copy()
        elif catmode == '1>0':
            x10 = np.ones(target_shape) * x[..., 0]
            x20 = np.zeros(target_shape)
            x20.fill(np.nan)
    return x10, x20
Пример #6
0
def daylightlocus(cct, force_daylight_below4000K = False, cieobs = None, daylight_locus = None):
    """ 
    Calculates daylight chromaticity (xD,yD) from correlated color temperature (cct).
    
    Args:
        :cct: 
            | int or float or list of int/floats or ndarray
        :force_daylight_below4000K: 
            | False or True, optional
            | Daylight locus approximation is not defined below 4000 K, 
            | but by setting this to True, the calculation can be forced to 
            | calculate it anyway.
        :cieobs:
            | CMF set corresponding to xD, yD output.
            | If None: use default CIE15-20xx locus for '1931_2'
            | Else: use the locus specified in :daylight_locus:
        :daylight_locus:
            | None, optional
            | dict with xD(T) and yD(xD) parameters to calculate daylight locus 
            | for specified cieobs.
            | If None: use pre-calculated values.
            | If 'calc': calculate them on the fly.
    
    Returns:
        :(xD, yD): 
            | (ndarray of x-coordinates, ndarray of y-coordinates)
        
    References:
        1. `CIE15:2018, “Colorimetry,” CIE, Vienna, Austria, 2018. <https://doi.org/10.25039/TR.015.2018>`_
    """
    cct = np2d(cct)
    if np.any((cct < 4000.0) & (force_daylight_below4000K == False)):
        raise Exception('spectral.daylightlocus(): Daylight locus approximation not defined below 4000 K')
    
    if (cieobs is None): # use default values for '1931_2' reported in CIE15-20xx
        xD = -4.607*((1e3/cct)**3.0)+2.9678*((1e3/cct)**2.0)+0.09911*(1000.0/cct)+0.244063
        p = cct>=7000.0
        xD[p] = -2.0064*((1.0e3/cct[p])**3.0)+1.9018*((1.0e3/cct[p])**2.0)+0.24748*(1.0e3/cct[p])+0.23704
        yD = -3.0*xD**2.0+2.87*xD-0.275
    else:
        if isinstance(cieobs, str):
            if daylight_locus is None:
                daylight_locus = _DAYLIGHT_LOCI_PARAMETERS[cieobs]
            else:
                if isinstance(daylight_locus,str):
                    if daylight_locus == 'calc':
                        daylight_locus = get_daylightloci_parameters(cieobs = [cieobs])[cieobs]
        else:
            daylight_locus = get_daylightloci_parameters(cieobs = cieobs)['cmf_0']
        pxy, pxT_l7, pxT_L7 = daylight_locus['pxy'], daylight_locus['pxT_l7k'], daylight_locus['pxT_L7k']
        xD = np.polyval(pxT_l7, 1000/cct)
        p = cct>=7000.0
        xD[p] = np.polyval(pxT_L7, 1000/cct[p])
        yD = np.polyval(pxy, xD)        
        
    return xD,yD
Пример #7
0
def Vrb_mb_to_xyz(Vrb,
                  cieobs=_CIEOBS,
                  scaling=[1, 1],
                  M=None,
                  Minverted=False,
                  **kwargs):
    """
    Convert V,r,b (Macleod-Boynton) color coordinates to XYZ tristimulus values.

    | Macleod Boynton: V = R+G, r = R/V, b = B/V
    | Note that R,G,B ~ L,M,S

    Args:
        :Vrb: 
            | ndarray with V,r,b (Macleod-Boynton) color coordinates
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when getting the default M, which is
            | the xyz to lms conversion matrix.
        :scaling:
            | list of scaling factors for r and b dimensions.
        :M: 
            | None, optional
            | Conversion matrix for going from XYZ to RGB (LMS)
            |   If None, :cieobs: determines the M (function does inversion)
        :Minverted:
            | False, optional
            | Bool that determines whether M should be inverted.

    Returns:
        :xyz: 
            | ndarray with tristimulus values

    Reference:
        1. `MacLeod DI, and Boynton RM (1979).
           Chromaticity diagram showing cone excitation by stimuli of equal luminance.
           J. Opt. Soc. Am. 69, 1183–1186.
           <https://www.osapublishing.org/josa/abstract.cfm?uri=josa-69-8-1183>`_
    """
    Vrb = np2d(Vrb)
    RGB = np.empty(Vrb.shape)
    RGB[..., 0] = Vrb[..., 1] * Vrb[..., 0] / scaling[0]
    RGB[..., 2] = Vrb[..., 2] * Vrb[..., 0] / scaling[1]
    RGB[..., 1] = Vrb[..., 0] - RGB[..., 0]
    if M is None:
        M = _CMF[cieobs]['M']
    if Minverted == False:
        M = np.linalg.inv(M)

    if len(RGB.shape) == 3:
        return np.einsum('ij,klj->kli', M, RGB)
    else:
        return np.einsum('ij,lj->li', M, RGB)
Пример #8
0
def xyz_to_xyz(xyz, **kwargs):
    """
    Convert XYZ tristimulus values to XYZ tristimulus values.

    Args:
        :xyz: 
            | ndarray with tristimulus values

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    return np2d(xyz)
Пример #9
0
def xyz_to_wuv(xyz, xyzw=_COLORTF_DEFAULT_WHITE_POINT, **kwargs):
    """
    Convert XYZ tristimulus values CIE 1964 U*V*W* color space.

    Args:
        :xyz: 
            | ndarray with tristimulus values
        :xyzw: 
            | ndarray with tristimulus values of white point, optional
            |  (Defaults to luxpy._COLORTF_DEFAULT_WHITE_POINT)

    Returns:
        :wuv: 
            | ndarray with W*U*V* values
    """
    Yuv = xyz_to_Yuv(np2d(xyz))  # convert to cie 1976 u'v'
    Yuvw = xyz_to_Yuv(np2d(xyzw))
    wuv = np.empty(xyz.shape)
    wuv[..., 0] = 25.0 * (Yuv[..., 0]**(1 / 3)) - 17.0
    wuv[..., 1] = 13.0 * wuv[..., 0] * (Yuv[..., 1] - Yuvw[..., 1])
    wuv[..., 2] = 13.0 * wuv[..., 0] * (Yuv[..., 2] - Yuvw[..., 2]) * (
        2.0 / 3.0)  #*(2/3) to convert to cie 1960 u, v
    return wuv
Пример #10
0
def xyz_to_srgb(xyz, gamma=2.4, **kwargs):
    """
    Calculates IEC:61966 sRGB values from xyz.

    Args:
        :xyz: 
            | ndarray with relative tristimulus values.
        :gamma: 
            | 2.4, optional
            | compression in sRGB

    Returns:
        :rgb: 
            | ndarray with R,G,B values (uint8).
    """

    xyz = np2d(xyz)

    # define 3x3 matrix
    M = np.array([[3.2404542, -1.5371385, -0.4985314],
                  [-0.9692660, 1.8760108, 0.0415560],
                  [0.0556434, -0.2040259, 1.0572252]])

    if len(xyz.shape) == 3:
        srgb = np.einsum('ij,klj->kli', M, xyz / 100)
    else:
        srgb = np.einsum('ij,lj->li', M, xyz / 100)

    # perform clipping:
    srgb[np.where(srgb > 1)] = 1
    srgb[np.where(srgb < 0)] = 0

    # test for the dark colours in the non-linear part of the function:
    dark = np.where(srgb <= 0.0031308)

    # apply gamma function:
    g = 1 / gamma

    # and scale to range 0-255:
    rgb = srgb.copy()
    rgb = (1.055 * rgb**g - 0.055) * 255

    # non-linear bit for dark colours
    rgb[dark] = (srgb[dark].copy() * 12.92) * 255

    # clip to range:
    rgb[rgb > 255] = 255
    rgb[rgb < 0] = 0

    return rgb
Пример #11
0
def xyz_to_Vrb_mb(xyz, cieobs=_CIEOBS, scaling=[1, 1], M=None, **kwargs):
    """
    Convert XYZ tristimulus values to V,r,b (Macleod-Boynton) color coordinates.

    | Macleod Boynton: V = R+G, r = R/V, b = B/V
    | Note that R,G,B ~ L,M,S

    Args:
        :xyz: 
            | ndarray with tristimulus values
        :cieobs: 
            | luxpy._CIEOBS, optional
            | CMF set to use when getting the default M, which is
              the xyz to lms conversion matrix.
        :scaling:
            | list of scaling factors for r and b dimensions.
        :M: 
            | None, optional
            | Conversion matrix for going from XYZ to RGB (LMS)
            |   If None, :cieobs: determines the M (function does inversion)

    Returns:
        :Vrb: 
            | ndarray with V,r,b (Macleod-Boynton) color coordinates

    Reference:
        1. `MacLeod DI, and Boynton RM (1979).
           Chromaticity diagram showing cone excitation by stimuli of equal luminance.
           J. Opt. Soc. Am. 69, 1183–1186.
           <https://www.osapublishing.org/josa/abstract.cfm?uri=josa-69-8-1183>`_
    """
    xyz = np2d(xyz)

    if M is None:
        M = _CMF[cieobs]['M']

    if len(xyz.shape) == 3:
        RGB = np.einsum('ij,klj->kli', M, xyz)
    else:
        RGB = np.einsum('ij,lj->li', M, xyz)
    Vrb = np.empty(xyz.shape)
    Vrb[..., 0] = RGB[..., 0] + RGB[..., 1]
    Vrb[..., 1] = RGB[..., 0] / Vrb[..., 0] * scaling[0]
    Vrb[..., 2] = RGB[..., 2] / Vrb[..., 0] * scaling[1]
    return Vrb
Пример #12
0
def Yxy_to_xyz(Yxy, **kwargs):
    """
    Convert CIE Yxy chromaticity values to XYZ tristimulus values.

    Args:
        :Yxy: 
            | ndarray with Yxy chromaticity values
            |  (Y value refers to luminance or luminance factor)

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    Yxy = np2d(Yxy)
    xyz = np.empty(Yxy.shape)
    xyz[..., 1] = Yxy[..., 0]
    xyz[..., 0] = Yxy[..., 0] * Yxy[..., 1] / Yxy[..., 2]
    xyz[..., 2] = Yxy[..., 0] * (1.0 - Yxy[..., 1] - Yxy[..., 2]) / Yxy[..., 2]
    return xyz
Пример #13
0
def xyz_to_lab(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs):
    """
    Convert XYZ tristimulus values to CIE 1976 L*a*b* (CIELAB) coordinates.

    Args:
        :xyz: 
            | ndarray with tristimulus values
        :xyzw:
            | None or ndarray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating xyzw.

    Returns:
        :lab: 
            | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates
    """
    xyz = np2d(xyz)

    if xyzw is None:
        xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs)

    # get and normalize (X,Y,Z) to white point:
    XYZr = xyz / xyzw

    # Apply cube-root compression:
    fXYZr = XYZr**(1.0 / 3.0)

    # Check for T/Tn <= 0.008856: (Note (24/116)**3 = 0.008856)
    pqr = XYZr <= (24 / 116)**3

    # calculate f(T) for T/Tn <= 0.008856: (Note:(1/3)*((116/24)**2) = 841/108 = 7.787)
    fXYZr[pqr] = ((841 / 108) * XYZr[pqr] + 16.0 / 116.0)

    # calculate L*, a*, b*:
    Lab = np.empty(xyz.shape)
    Lab[..., 0] = 116.0 * (fXYZr[..., 1]) - 16.0
    Lab[pqr[..., 1], 0] = 841 / 108 * 116 * XYZr[pqr[..., 1], 1]
    Lab[..., 1] = 500.0 * (fXYZr[..., 0] - fXYZr[..., 1])
    Lab[..., 2] = 200.0 * (fXYZr[..., 1] - fXYZr[..., 2])
    return Lab
Пример #14
0
def geomean(data, axis = 0, keepdims = False):
    """
    Calculate geometric mean along axis.
    
    Args:
        :data:
            | list of values or ndarray
        :axis:
            | 0, optional
            | Axis along which to calculate geomean.
        :keepdims:
            | False or True, optional
            | Keep original dimensions of array.
    
    Returns:
        :returns:
            | ndarray with geomean values. 
    """
    data = np2d(data)
    return np.power(data.prod(axis=axis, keepdims = keepdims),1/data.shape[axis])
Пример #15
0
def xyz_to_Yxy(xyz, **kwargs):
    """
    Convert XYZ tristimulus values CIE Yxy chromaticity values.

    Args:
        :xyz: 
            | ndarray with tristimulus values

    Returns:
        :Yxy: 
            | ndarray with Yxy chromaticity values
            |  (Y value refers to luminance or luminance factor)
    """
    xyz = np2d(xyz)
    Yxy = np.empty(xyz.shape)
    sumxyz = xyz[..., 0] + xyz[..., 1] + xyz[..., 2]
    Yxy[..., 0] = xyz[..., 1]
    Yxy[..., 1] = xyz[..., 0] / sumxyz
    Yxy[..., 2] = xyz[..., 1] / sumxyz
    return Yxy
Пример #16
0
def rms(data,axis = 0, keepdims = False):
    """
    Calculate root-mean-square along axis.
    
    Args:
        :data: 
            | list of values or ndarray
        :axis:
            | 0, optional
            | Axis along which to calculate rms.
        :keepdims:
            | False or True, optional
            | Keep original dimensions of array.
    
    Returns:
        :returns:
            | ndarray with rms values.
    """
    data = np2d(data)
    return np.sqrt(np.power(data,2).mean(axis=axis, keepdims = keepdims))
Пример #17
0
def Yuv_to_xyz(Yuv, **kwargs):
    """
    Convert CIE 1976 Yu'v' chromaticity values to XYZ tristimulus values.

    Args:
        :Yuv: 
            | ndarray with CIE 1976 Yu'v' chromaticity values
            |  (Y value refers to luminance or luminance factor)

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    Yuv = np2d(Yuv)
    xyz = np.empty(Yuv.shape)
    xyz[..., 1] = Yuv[..., 0]
    xyz[..., 0] = Yuv[..., 0] * (9.0 * Yuv[..., 1]) / (4.0 * Yuv[..., 2])
    xyz[..., 2] = Yuv[..., 0] * (12.0 - 3.0 * Yuv[..., 1] -
                                 20.0 * Yuv[..., 2]) / (4.0 * Yuv[..., 2])
    return xyz
Пример #18
0
def xyz_to_Yuv(xyz, **kwargs):
    """
    Convert XYZ tristimulus values CIE 1976 Yu'v' chromaticity values.

    Args:
        :xyz: 
            | ndarray with tristimulus values

    Returns:
        :Yuv: 
            | ndarray with CIE 1976 Yu'v' chromaticity values
            |  (Y value refers to luminance or luminance factor)
    """
    xyz = np2d(xyz)
    Yuv = np.empty(xyz.shape)
    denom = xyz[..., 0] + 15.0 * xyz[..., 1] + 3.0 * xyz[..., 2]
    Yuv[..., 0] = xyz[..., 1]
    Yuv[..., 1] = 4.0 * xyz[..., 0] / denom
    Yuv[..., 2] = 9.0 * xyz[..., 1] / denom
    return Yuv
Пример #19
0
def lab_to_xyz(lab, xyzw=None, cieobs=_CIEOBS, **kwargs):
    """
    Convert CIE 1976 L*a*b* (CIELAB) color coordinates to XYZ tristimulus values.

    Args:
        :lab: 
            | ndarray with CIE 1976 L*a*b* (CIELAB) color coordinates
        :xyzw:
            | None or ndarray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating xyzw.

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    lab = np2d(lab)

    if xyzw is None:
        xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs)

    # make xyzw same shape as data:
    xyzw = xyzw * np.ones(lab.shape)

    # get L*, a*, b* and Xw, Yw, Zw:
    fXYZ = np.empty(lab.shape)
    fXYZ[..., 1] = (lab[..., 0] + 16.0) / 116.0
    fXYZ[..., 0] = lab[..., 1] / 500.0 + fXYZ[..., 1]
    fXYZ[..., 2] = fXYZ[..., 1] - lab[..., 2] / 200.0

    # apply 3rd power:
    xyz = (fXYZ**3.0) * xyzw

    # Now calculate T where T/Tn is below the knee point:
    pqr = fXYZ <= (24 / 116)  #(24/116)**3**(1/3)
    xyz[pqr] = np.squeeze(xyzw[pqr] * ((fXYZ[pqr] - 16.0 / 116.0) /
                                       (841 / 108)))

    return xyz
Пример #20
0
def srgb_to_xyz(rgb, gamma=2.4, **kwargs):
    """
    Calculates xyz from IEC:61966 sRGB values.

    Args:
        :rgb: 
            | ndarray with srgb values (uint8).
        :gamma: 
            | 2.4, optional
            | compression in sRGB
            
    Returns:
        :xyz: 
            | ndarray with relative tristimulus values.

    """
    rgb = np2d(rgb)

    # define 3x3 matrix
    M = np.array([[0.4124564, 0.3575761, 0.1804375],
                  [0.2126729, 0.7151522, 0.0721750],
                  [0.0193339, 0.1191920, 0.9503041]])

    # scale device coordinates:
    sRGB = rgb / 255

    # test for non-linear part of conversion
    nonlin = np.where((rgb / 255) < 0.0031308)  #0.03928)

    # apply gamma function to convert to sRGB
    srgb = sRGB.copy()
    srgb = ((srgb + 0.055) / 1.055)**gamma

    srgb[nonlin] = sRGB[nonlin] / 12.92

    if len(srgb.shape) == 3:
        xyz = np.einsum('ij,klj->kli', M, srgb) * 100
    else:
        xyz = np.einsum('ij,lj->li', M, srgb) * 100
    return xyz
Пример #21
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)
Пример #22
0
def wuv_to_xyz(wuv, xyzw=_COLORTF_DEFAULT_WHITE_POINT, **kwargs):
    """
    Convert CIE 1964 U*V*W* color space coordinates to XYZ tristimulus values.

    Args:
        :wuv: 
            | ndarray with W*U*V* values
        :xyzw: 
            | ndarray with tristimulus values of white point, optional
            |  (Defaults to luxpy._COLORTF_DEFAULT_WHITE_POINT)

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    wuv = np2d(wuv)
    Yuvw = xyz_to_Yuv(xyzw)  # convert to cie 1976 u'v'
    Yuv = np.empty(wuv.shape)
    Yuv[..., 0] = ((wuv[..., 0] + 17.0) / 25.0)**3.0
    Yuv[..., 1] = Yuvw[..., 1] + wuv[..., 1] / (13.0 * wuv[..., 0])
    Yuv[..., 2] = Yuvw[..., 2] + wuv[..., 2] / (13.0 * wuv[..., 0]) * (
        3.0 / 2.0)  # convert to cie 1960 u, v
    return Yuv_to_xyz(Yuv)
Пример #23
0
def xyz_to_luv(xyz, xyzw=None, cieobs=_CIEOBS, **kwargs):
    """
    Convert XYZ tristimulus values to CIE 1976 L*u*v* (CIELUV) coordinates.

    Args:
        :xyz: 
            | ndarray with tristimulus values
        :xyzw:
            | None or ndarray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating xyzw.

    Returns:
        :luv: 
            | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates
    """
    xyz = np2d(xyz)

    if xyzw is None:
        xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs)

    # Calculate u',v' of test and white:
    Yuv = xyz_to_Yuv(xyz)
    Yuvw = xyz_to_Yuv(todim(xyzw,
                            xyz.shape))  # todim: make xyzw same shape as xyz

    #uv1976 to CIELUV
    luv = np.empty(xyz.shape)
    YdivYw = Yuv[..., 0] / Yuvw[..., 0]
    luv[..., 0] = 116.0 * YdivYw**(1.0 / 3.0) - 16.0
    p = np.where(YdivYw <= (6.0 / 29.0)**3.0)
    luv[..., 0][p] = ((29.0 / 3.0)**3.0) * YdivYw[p]
    luv[..., 1] = 13.0 * luv[..., 0] * (Yuv[..., 1] - Yuvw[..., 1])
    luv[..., 2] = 13.0 * luv[..., 0] * (Yuv[..., 2] - Yuvw[..., 2])
    return luv
Пример #24
0
def luv_to_xyz(luv, xyzw=None, cieobs=_CIEOBS, **kwargs):
    """
    Convert CIE 1976 L*u*v* (CIELUVB) coordinates to XYZ tristimulus values.

    Args:
        :luv: 
            | ndarray with CIE 1976 L*u*v* (CIELUV) color coordinates
        :xyzw:
            | None or ndarray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs: 
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating xyzw.

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """
    luv = np2d(luv)

    if xyzw is None:
        xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs)

    # Make xyzw same shape as luv and convert to Yuv:
    Yuvw = todim(xyz_to_Yuv(xyzw), luv.shape, equal_shape=True)

    # calculate u'v' from u*,v*:
    Yuv = np.empty(luv.shape)
    Yuv[..., 1:3] = (luv[..., 1:3] / (13 * luv[..., :1])) + Yuvw[..., 1:3]
    Yuv[Yuv[..., 0] == 0, 1:3] = 0

    Yuv[..., 0] = Yuvw[..., 0] * (((luv[..., 0] + 16.0) / 116.0)**3.0)
    p = np.where((Yuv[..., 0] / Yuvw[..., 0]) < ((6.0 / 29.0)**3.0))
    Yuv[..., 0][p] = Yuvw[..., 0][p] * (luv[..., 0][p] / ((29.0 / 3.0)**3.0))

    return Yuv_to_xyz(Yuv)
Пример #25
0
def jabz_to_xyz(jabz, ztype='jabz', **kwargs):
    """
    Convert Jz,az,bz color coordinates to XYZ tristimulus values.

    Args:
        :jabz: 
            | ndarray with Jz,az,bz color coordinates
        :ztype:
            | 'jabz', optional
            | String with requested return:
            | Options: 'jabz', 'iabz'
    Returns:
        :xyz: 
            | ndarray with tristimulus values

    Note:
     | 1. :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation!
     |
     | 2a. Jz represents the 'lightness' relative to a D65 white with luminance = 10000 cd/m² 
     |      (note that Jz that not exactly equal 1 for this high value, but rather for 102900 cd/m2)
     | 2b.  az, bz represent respectively a red-green and a yellow-blue opponent axis 
     |      (but note that a D65 shows a small offset from (0,0))

    Reference:
        1. `Safdar, M., Cui, G., Kim,Y. J., and Luo, M. R. (2017).
        Perceptually uniform color space for image signals including high dynamic range and wide gamut.
        Opt. Express, vol. 25, no. 13, pp. 15131–15151, June, 2017.
        <http://www.opticsexpress.org/abstract.cfm?URI=oe-25-13-15131>`_
    """
    jabz = np2d(jabz)

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

    # Convert Iabz to lmsp:
    M = np.linalg.inv(
        np.array([[0.5, 0.5, 0], [3.524000, -4.066708, 0.542708],
                  [0.199076, 1.096799, -1.295875]]))

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

    # Convert lmsp to lms:

    lms = 10000 * (((3424 / 2**12) - lmsp**(1 / (1.7 * 2523 / 2**5))) /
                   (((2392 / 2**7) * lmsp**(1 / (1.7 * 2523 / 2**5))) -
                    (2413 / 2**7)))**(1 / (2610 / (2**14)))

    # Convert lms to xyz:
    # Setup X',Y',Z' from X,Y,Z transform as matrix:
    b = 1.15
    g = 0.66
    M_to_xyzp = np.array([[b, 0, 1 - b], [1 - g, g, 0], [0, 0, 1]])

    # Define X',Y',Z' to L,M,S conversion matrix:
    M_to_lms = np.array([[0.41478972, 0.579999, 0.0146480],
                         [-0.2015100, 1.120649, 0.0531008],
                         [-0.0166008, 0.264800, 0.6684799]])

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

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

    return xyz
Пример #26
0
def mahalanobis2(x, y = None, z = None, mu = None, sigmainv = None):
    """
    Evaluate the squared mahalanobis distance
    
    Args: 
        :x: 
            | scalar or list or ndarray (.ndim = 1 or 2) with x(y)-coordinates 
              at which to evaluate the mahalanobis distance squared.
        :y: 
            | None or scalar or list or ndarray (.ndim = 1) with y-coordinates 
              at which to evaluate the mahalanobis distance squared, optional.
            | If :y: is None, :x: should be a 2d array.
        :z: 
            | None or scalar or list or ndarray (.ndim = 1) with z-coordinates 
              at which to evaluate the mahalanobis distance squared, optional.
            | If :z: is None & :y: is None, then :x: should be a 2d array.
        :mu: 
            | None or ndarray (.ndim = 1) with center coordinates of the 
              mahalanobis ellipse, optional. 
            | None defaults to zeros(2) or zeros(3).
        :sigmainv:
            | None or ndarray with 'inverse covariance matrix', optional 
            | Determines the shape and orientation of the PD.
            | None default to np.eye(2) or eye(3).
    Returns:
         :returns: 
             | ndarray with magnitude of mahalanobis2(x,y[,z])

    """
    if (y is None) & (z is None):
        p = x.shape[-1]
    elif (z is None):
        p = x.shape[-1] if (y is None) else 2
    elif (z is not None):
        p = 3 if (y is not None) else 2
    
    if mu is None:
        mu = np.zeros(p)
    if sigmainv is None:
        sigmainv = np.eye(p)
    
    x = np2d(x)
    mu = np2d(mu)

    if (y is None) & (z is None):
        x = x - mu
        if p == 2:
            x, y = asplit(x)
        elif p==3:
            x, y, z = asplit(x)
    elif (z is None):
        if y is None:
            x = x - mu
            x, y = asplit(x)
        else:
            x = x - mu[...,0] # center data on mu 
            y = np2d(y) - mu[...,1] # center data on mu 
    elif (z is not None):
        if (y is not None):
            x = x - mu[0] # center data on mu 
            y = np2d(y) - mu[...,1] # center data on mu 
            z = np2d(z) - mu[...,2] # center data on mu 
        else:
            x = x - mu[...,0] # center data on mu 
            y = np2d(z) - mu[...,1] # center data on mu 
            
    if p == 2:
        return (sigmainv[0,0] * (x**2.0) + sigmainv[1,1] * (y**2.0) + 2.0*sigmainv[0,1]*(x*y))
    else:
        return (sigmainv[0,0] * (x**2.0) + sigmainv[1,1] * (y**2.0) + 2.0*sigmainv[0,1]*(x*y) + 
                sigmainv[2,2] * (z**2.0) + 2.0*sigmainv[0,2]*(x*z) +  2.0*sigmainv[1,2]*(y*z))
Пример #27
0
def symmM_to_posdefM(A = None, atol = 1.0e-9, rtol = 1.0e-9, method = 'make', forcesymm = True):
    """
    Convert a symmetric matrix to a positive definite one. 
    
    Args:
        :A: 
            | ndarray
        :atol:
            | float, optional
            | The absolute tolerance parameter (see Notes of numpy.allclose())
        :rtol:
            | float, optional
            | The relative tolerance parameter (see Notes of numpy.allclose())
        :method: 
            | 'make' or 'nearest', optional (see notes for more info)
        :forcesymm: 
            | True or False, optional
            | If A is not symmetric, force symmetry using: 
            |    A = numpy.triu(A) + numpy.triu(A).T - numpy.diag(numpy.diag(A))
    
    Returns:
        :returns:
            | ndarray with positive-definite matrix.
        
    Notes on supported methods:
        1. `'make': A Python/Numpy port of Muhammad Asim Mubeen's matlab function 
        Spd_Mat.m 
        <https://nl.mathworks.com/matlabcentral/fileexchange/45873-positive-definite-matrix>`_
        2. `'nearest': A Python/Numpy port of John D'Errico's `nearestSPD` 
        MATLAB code. 
        <https://stackoverflow.com/questions/43238173/python-convert-matrix-to-positive-semi-definite>`_
    """
    if A is not None:
        A = np2d(A)
        
        
        # Make sure matrix A is symmetric up to a certain tolerance:
        sn = check_symmetric(A, atol = atol, rtol = rtol) 
        if ((A.shape[0] != A.shape[1]) | (sn != True)):
            if (forcesymm == True)  &  (A.shape[0] == A.shape[1]):
                A = np.triu(A) + np.triu(A).T - np.diag(np.diag(A))
            else:
                raise Exception('symmM_to_posdefM(): matrix A not symmetric.')
        
        
        if check_posdef(A, atol = atol, rtol = rtol) == True:
            return A
        else:

            if method == 'make':

                # A Python/Numpy port of Muhammad Asim Mubeen's matlab function Spd_Mat.m
                #
                # See: https://nl.mathworks.com/matlabcentral/fileexchange/45873-positive-definite-matrix
                Val, Vec = np.linalg.eig(A) 
                Val = np.real(Val)
                Vec = np.real(Vec)
                Val[np.where(Val==0)] = _EPS #making zero eigenvalues non-zero
                p = np.where(Val<0)
                Val[p] = -Val[p] #making negative eigenvalues positive
                return   np.dot(Vec,np.dot(np.diag(Val) , Vec.T))
 
            
            elif method == 'nearest':
                
                 # A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code [1], which
                 # credits [2].
                 #
                 # [1] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd
                 #
                 # [2] N.J. Higham, "Computing a nearest symmetric positive semidefinite
                 # matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6
                 #
                 # See: https://stackoverflow.com/questions/43238173/python-convert-matrix-to-positive-semi-definite
                
                B = (A + A.T) / 2.0
                _, s, V = np.linalg.svd(B)

                H = np.dot(V.T, np.dot(np.diag(s), V))

                A2 = (B + H) / 2.0

                A3 = (A2 + A2.T) / 2.0

                if check_posdef(A3, atol = atol, rtol = rtol) == True:
                    return A3

                spacing = np.spacing(np.linalg.norm(A))
                I = np.eye(A.shape[0])
                k = 1
                while not check_posdef(A3, atol = atol, rtol = rtol):
                    mineig = np.min(np.real(np.linalg.eigvals(A3)))
                    A3 += I * (-mineig * k**2.0+ spacing)
                    k += 1

                return A3
Пример #28
0
def spd_to_mcri(SPD, D = 0.9, E = None, Yb = 20.0, out = 'Rm', wl = None):
    """
    Calculates the MCRI or Memory Color Rendition Index, Rm
    
    Args: 
        :SPD: 
            | ndarray with spectral data (can be multiple SPDs, 
            |  first axis are the wavelengths)
        :D: 
            | 0.9, optional
            | Degree of adaptation.
        :E: 
            | None, optional
            | Illuminance in lux 
            |  (used to calculate La = (Yb/100)*(E/pi) to then calculate D 
            |  following the 'cat02' model). 
            | If None: the degree is determined by :D:
            |  If (:E: is not None) & (:Yb: is None):  :E: is assumed to contain 
            |  the adapting field luminance La (cd/m²).
        :Yb: 
            | 20.0, optional
            | Luminance factor of background. (used when calculating La from E)
            | If None, E contains La (cd/m²).
        :out: 
            | 'Rm' or str, optional
            | Specifies requested output (e.g. 'Rm,Rmi,cct,duv') 
        :wl: 
            | None, optional
            | Wavelengths (or [start, end, spacing]) to interpolate the SPDs to. 
            | None: default to no interpolation   
    
    Returns:
        :returns: 
            | float or ndarray with MCRI Rm for :out: 'Rm'
            | Other output is also possible by changing the :out: str value.        
          
    References:
        1. `K.A.G. Smet, W.R. Ryckaert, M.R. Pointer, G. Deconinck, P. Hanselaer,(2012)
        “A memory colour quality metric for white light sources,” 
        Energy Build., vol. 49, no. C, pp. 216–225.
        <http://www.sciencedirect.com/science/article/pii/S0378778812000837>`_
    """
    SPD = np2d(SPD)
    
    if wl is not None: 
        SPD = spd(data = SPD, interpolation = _S_INTERP_TYPE, kind = 'np', wl = wl)
    
    
    # unpack metric default values:
    avg, catf, cieobs, cri_specific_pars, cspace, ref_type, rg_pars, sampleset, scale = [_MCRI_DEFAULTS[x] for x in sorted(_MCRI_DEFAULTS.keys())] 
    similarity_ai = cri_specific_pars['similarity_ai']
    Mxyz2lms = cspace['Mxyz2lms'] 
    scale_fcn = scale['fcn']
    scale_factor = scale['cfactor']
    sampleset = eval(sampleset)
    
    # A. calculate xyz:
    xyzti, xyztw = spd_to_xyz(SPD, cieobs = cieobs['xyz'],  rfl = sampleset, out = 2)
    if 'cct' in out.split(','):
        cct, duv = xyz_to_cct(xyztw, cieobs = cieobs['cct'], out = 'cct,duv',mode = 'lut')
        
    # B. perform chromatic adaptation to adopted whitepoint of ipt color space, i.e. D65:
    if catf is not None:
        Dtype_cat, F, Yb_cat, catmode_cat, cattype_cat, mcat_cat, xyzw_cat = [catf[x] for x in sorted(catf.keys())]
        
        # calculate degree of adaptationn D:
        if E is not None:
            if Yb is not None:
                La = (Yb/100.0)*(E/np.pi)
            else:
                La = E
            D = cat.get_degree_of_adaptation(Dtype = Dtype_cat, F = F, La = La)
        else:
            Dtype_cat = None # direct input of D

        if (E is None) and (D is None):
            D = 1.0 # set degree of adaptation to 1 !
        if D > 1.0: D = 1.0
        if D < 0.6: D = 0.6 # put a limit on the lowest D

        # apply cat:
        xyzti = cat.apply(xyzti, cattype = cattype_cat, catmode = catmode_cat, xyzw1 = xyztw,xyzw0 = None, xyzw2 = xyzw_cat, D = D, mcat = [mcat_cat], Dtype = Dtype_cat)
        xyztw = cat.apply(xyztw, cattype = cattype_cat, catmode = catmode_cat, xyzw1 = xyztw,xyzw0 = None, xyzw2 = xyzw_cat, D = D, mcat = [mcat_cat], Dtype = Dtype_cat)
     
    # C. convert xyz to ipt and split:
    ipt = xyz_to_ipt(xyzti, cieobs = cieobs['xyz'], M = Mxyz2lms) #input matrix as published in Smet et al. 2012, Energy and Buildings
    I,P,T = asplit(ipt)  

    # D. calculate specific (hue dependent) similarity indicators, Si:
    if len(xyzti.shape) == 3:
        ai = np.expand_dims(similarity_ai, axis = 1)
    else: 
        ai = similarity_ai
    a1,a2,a3,a4,a5 = asplit(ai)
    mahalanobis_d2 = (a3*np.power((P - a1),2.0) + a4*np.power((T - a2),2.0) + 2.0*a5*(P-a1)*(T-a2))
    if (len(mahalanobis_d2.shape)==3) & (mahalanobis_d2.shape[-1]==1):
        mahalanobis_d2 = mahalanobis_d2[:,:,0].T
    Si = np.exp(-0.5*mahalanobis_d2)

    # E. calculate general similarity indicator, Sa:
    Sa = avg(Si, axis = 0,keepdims = True)

    # F. rescale similarity indicators (Si, Sa) with a 0-1 scale to memory color rendition indices (Rmi, Rm) with a 0 - 100 scale:
    Rmi = scale_fcn(np.log(Si),scale_factor = scale_factor)
    Rm = np2d(scale_fcn(np.log(Sa),scale_factor = scale_factor))

    # G. calculate Rg (polyarea of test / polyarea of memory colours):
    if 'Rg' in out.split(','):
        I = I[...,None] #broadcast_shape(I, target_shape = None,expand_2d_to_3d = 0)
        a1 = a1[:,None]*np.ones(I.shape)#broadcast_shape(a1, target_shape = None,expand_2d_to_3d = 0)
        a2 = a2[:,None]*np.ones(I.shape) #broadcast_shape(a2, target_shape = None,expand_2d_to_3d = 0)
        a12 = np.concatenate((a1,a2),axis=2) #broadcast_shape(np.hstack((a1,a2)), target_shape = ipt.shape,expand_2d_to_3d = 0)
        ipt_mc = np.concatenate((I,a12),axis=2)
        nhbins, normalize_gamut, normalized_chroma_ref, start_hue  = [rg_pars[x] for x in sorted(rg_pars.keys())]
    
        hue_bin_data = _get_hue_bin_data(ipt, ipt_mc, 
                                         start_hue = start_hue, nhbins = nhbins,
                                         normalized_chroma_ref = normalized_chroma_ref)
        Rg = _hue_bin_data_to_rg(hue_bin_data)

    if (out != 'Rm'):
        return  eval(out)
    else:
        return Rm
Пример #29
0
def _massage_input_and_init_output(data,
                                   dataw,
                                   inputtype='xyz',
                                   direction='forward',
                                   n_out=3):
    """
    Redimension input data to ensure most they have the appropriate sizes for easy and efficient looping.
    |
    | 1. Convert data and dataw to atleast_2d ndarrays
    | 2. Make axis 1 of dataw have 'same' dimensions as data
    | 3. Make dataw have same lights source axis size as data
    | 4. Flip light source axis to axis=0 for efficient looping
    | 5. Initialize output array camout to 'same' shape as data but with camout.shape[-1] == n_out
    
    Args:
        :data: 
            | ndarray with input tristimulus values 
            | or spectral data 
            | or input color appearance correlates
            | Can be of shape: (N [, xM], x 3), whereby: 
            | N refers to samples and M refers to light sources.
            | Note that for spectral input shape is (N x (M+1) x wl) 
        :dataw: 
            | None or ndarray, optional
            | Input tristimulus values or spectral data of white point.
            | None defaults to the use of CIE illuminant C.
        :inputtype:
            | 'xyz' or 'spd', optional
            | Specifies the type of input: 
            |     tristimulus values or spectral data for the forward mode.
        :direction:
            | 'forward' or 'inverse', optional
            |   -'forward': xyz -> cam
            |   -'inverse': cam -> xyz 
        :n_out:
            | 3, optional
            | output size of last dimension of camout 
            | (e.g. n_out=3 for j,a,b output or n_out = 5 for J,M,h,a,b output)
            
    Returns:
        :data:
            | ndarray with reshaped data
        :dataw:
            | ndarray with reshaped dataw
        :camout:
            | NaN filled ndarray for output of CAMv (camout.shape[-1] == Nout) 
        :originalshape:
            | original shape of data
            
    Notes:
        For an example on the use, see code _simple_cam() (type: _simple_cam??)
    """
    # Convert data and dataw to atleast_2d ndarrays:
    data = np2d(data).copy(
    )  # stimulus data (can be upto NxMx3 for xyz, or [N x (M+1) x wl] for spd))
    dataw = np2d(dataw).copy(
    )  # white point (can be upto Nx3 for xyz, or [(N+1) x wl] for spd)
    originalshape = data.shape  # to restore output to same shape

    # Make axis 1 of dataw have 'same' dimensions as data:
    if (data.ndim == 2):
        data = np.expand_dims(data, axis=1)  # add light source axis 1

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

    dataw = np.expand_dims(
        dataw, axis=1)  # add extra axis to move light source to axis 0

    # Make dataw have same lights source dimension size as data:
    if inputtype == 'xyz':
        if dataw.shape[0] == 1:
            dataw = np.repeat(dataw, data.shape[0], axis=0)
        if (data.shape[0] == 1) & (dataw.shape[0] > 1):
            data = np.repeat(data, dataw.shape[0], axis=0)
    else:
        dataw = np.array([
            np.vstack((dataw[:1, 0, :], dataw[i + 1:i + 2, 0, :]))
            for i in range(dataw.shape[0] - 1)
        ])
        if (data.shape[0] == 1) & (dataw.shape[0] > 1):
            data = np.repeat(data, dataw.shape[0], axis=0)

    # Initialize output array:
    if n_out is not None:
        dshape = list((data).shape)
        dshape[-1] = n_out  # requested number of correlates: e.g. j,a,b
        if (inputtype != 'xyz') & (direction == 'forward'):
            dshape[-2] = dshape[
                -2] - 1  # wavelength row doesn't count & only with forward can the input data be spectral
        camout = np.zeros(dshape)
        camout.fill(np.nan)
    else:
        camout = None
    return data, dataw, camout, originalshape
Пример #30
0
def ipt_to_xyz(ipt, cieobs=_CIEOBS, xyzw=None, M=None, **kwargs):
    """
    Convert XYZ tristimulus values to IPT color coordinates.

    | I: Lightness axis, P, red-green axis, T: yellow-blue axis.

    Args:
        :ipt: 
            | ndarray with IPT color coordinates
        :xyzw:
            | None or ndarray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating xyzw for rescaling Mxyz2lms
            | (only when not None).
        :M: | None, optional
            | None defaults to xyz to lms conversion matrix determined by:cieobs:

    Returns:
        :xyz: 
            | ndarray with tristimulus values

    Note:
        :xyz: is assumed to be under D65 viewing conditions! If necessary perform chromatic adaptation !

    Reference:
        1. `Ebner F, and Fairchild MD (1998).
           Development and testing of a color space (IPT) with improved hue uniformity.
           In IS&T 6th Color Imaging Conference, (Scottsdale, Arizona, USA), pp. 8–13.
           <http://www.ingentaconnect.com/content/ist/cic/1998/00001998/00000001/art00003?crawler=true>`_
    """
    ipt = np2d(ipt)

    # get M to convert xyz to lms and apply normalization to matrix or input your own:
    if M is None:
        M = _IPT_M['xyz2lms'][cieobs].copy(
        )  # matrix conversions from xyz to lms
        if xyzw is None:
            xyzw = spd_to_xyz(_CIE_ILLUMINANTS['D65'], cieobs=cieobs,
                              out=1) / 100.0
        else:
            xyzw = xyzw / 100.0
        M = math.normalize_3x3_matrix(M, xyzw)

    # convert from ipt to lms':
    if len(ipt.shape) == 3:
        lmsp = np.einsum('ij,klj->kli', np.linalg.inv(_IPT_M['lms2ipt']), ipt)
    else:
        lmsp = np.einsum('ij,lj->li', np.linalg.inv(_IPT_M['lms2ipt']), ipt)

    # reverse response compression: lms' to lms
    lms = lmsp**(1.0 / 0.43)
    p = np.where(lmsp < 0.0)
    lms[p] = -np.abs(lmsp[p])**(1.0 / 0.43)

    # convert from lms to xyz:
    if np.ndim(M) == 2:
        if len(ipt.shape) == 3:
            xyz = np.einsum('ij,klj->kli', np.linalg.inv(M), lms)
        else:
            xyz = np.einsum('ij,lj->li', np.linalg.inv(M), lms)
    else:
        if len(
                ipt.shape
        ) == 3:  # second dim of lms must match dim of 1st of M and 1st dim of xyzw
            xyz = np.concatenate([
                np.einsum('ij,klj->kli', np.linalg.inv(M[i]),
                          lms[:, i:i + 1, :]) for i in range(M.shape[0])
            ],
                                 axis=1)
        else:  # first dim of lms must match dim of 1st of M and 1st dim of xyzw
            xyz = np.concatenate([
                np.einsum('ij,lj->li', np.linalg.inv(M[i]), lms[i:i + 1, :])
                for i in range(M.shape[0])
            ],
                                 axis=0)

    #xyz = np.dot(np.linalg.inv(M),lms.T).T
    xyz = xyz * 100.0
    xyz[np.where(xyz < 0.0)] = 0.0

    return xyz