Exemple #1
0
def _hue_bin_data_to_rg(hue_bin_data):
    jabt_closed = np.vstack(
        (hue_bin_data['jabt_hj'], hue_bin_data['jabt_hj'][:1, ...]))
    jabr_closed = np.vstack(
        (hue_bin_data['jabr_hj'], hue_bin_data['jabr_hj'][:1, ...]))
    notnan_t = np.logical_not(np.isnan(
        jabt_closed[..., 1]))  # avoid NaN's (i.e. empty hue-bins)
    notnan_r = np.logical_not(np.isnan(jabr_closed[..., 1]))
    Rg = np.array([[
        100 * _polyarea(jabt_closed[notnan_t[:, i], i, 1],
                        jabt_closed[notnan_t[:, i], i, 2]) /
        _polyarea(jabr_closed[notnan_r[:, i], i, 1],
                  jabr_closed[notnan_r[:, i], i, 2])
        for i in range(notnan_r.shape[-1])
    ]])
    return Rg
Exemple #2
0
def PX_colorshift_model(Jabt,
                        Jabr,
                        jab_ranges=None,
                        jab_deltas=None,
                        limit_grid_radius=0):
    """
    Pixelates the color space and calculates the color shifts in each pixel.
    
    Args:
        :Jabt: 
            | ndarray with color coordinates under the (single) test SPD.
        :Jabr: 
            | ndarray with color coordinates under the (single) reference SPD.  
        :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)
        :jab_deltas:
            | float or ndarray, optional
            | Specifies the sampling range. 
            | A float uses jab_deltas as the maximum Euclidean distance to select
            | samples around each pixel center. A ndarray of 3 deltas, uses
            | a city block sampling around each pixel center.
        :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: 
            | dict with the following keys:
            |   - 'Jab': dict with with ndarrays for:
            |        Jabt, Jabr, DEi, DEi_ab (only ab-coordinates), DEa (mean) 
            |         and DEa_ab
            |   - 'vshifts': dict with:
            |      * 'vectorshift': ndarray with vector shifts between average
            |                       Jabt and Jabr for each pixel
            |      * 'vectorshift_ab': ndarray with vector shifts averaged 
            |                          over J for each pixel
            |      * 'vectorshift_ab_J0': ndarray with vector shifts averaged 
            |                             over J for each pixel of J=0 plane.
            |      * 'vectorshift_len': length of 'vectorshift'
            |      * 'vectorshift_ab_len': length of 'vectorshift_ab'
            |      * 'vectorshift_ab_J0_len': length of 'vectorshift_ab_J0'
            |      * 'vectorshift_len_DEnormed': length of 'vectorshift' 
            |                                    normalized to 'DEa'
            |      * 'vectorshift_ab_len_DEnormed': length of 'vectorshift_ab' 
            |                                       normalized to 'DEa_ab'
            |      * 'vectorshift_ab_J0_len_DEnormed': length of 'vectorshift_ab_J0' 
            |                                          normalized to 'DEa_ab'
            |   - 'pixeldata': dict with pixel info:
            |      * 'grid' ndarray with coordinates of all pixel centers.
            |      * 'idx': list[int] with pixel index for each non-empty pixel
            |      * 'Jab': ndarray with center coordinates of non-empty pixels
            |      * 'samplenrs': list[list[int]] with sample numbers belong to 
            |                     each non-empty pixel
            |      * 'IDs: summarizing list, 
            |              with column order: 'idxp, jabp, samplenrs'
            |  - 'fielddata' : dict with dicts containing data on the calculated 
            |                  vector-field and circle-fields 
            |      * 'vectorfield': dict with ndarrays for the ab-coordinates 
            |         under the ref. (axr, bxr) and test (axt, bxt) illuminants,
            |         centered at the pixel centers corresponding to the 
                      ab-coordinates of the reference illuminant.
     """

    # get pixelIDs of all samples under ref. conditions:
    gridp, idxp, jabp, pixelsamplenrs, pixelIDs = get_pixel_coordinates(
        Jabr,
        jab_ranges=jab_ranges,
        jab_deltas=jab_deltas,
        limit_grid_radius=limit_grid_radius)

    # get average Jab coordinates for each pixel:
    Npixels = len(idxp)  # number of non-empty pixels
    Jabr_avg = np.zeros((gridp.shape[0], 3))
    Jabr_avg.fill(np.nan)
    Jabt_avg = Jabr_avg.copy()
    for i in range(Npixels):
        Jabr_avg[idxp[i], :] = Jabr[pixelsamplenrs[i], :].mean(axis=0)
        Jabt_avg[idxp[i], :] = Jabt[pixelsamplenrs[i], :].mean(axis=0)
        #jabtemp = Jabr[pixelsamplenrs[i],:]
        #jabtempm = Jabr_avg[idxp[i],:]

    # calculate Jab vector shift:
    vectorshift = Jabt_avg - Jabr_avg

    # calculate ab vector shift:
    uabs = gridp[gridp[:, 0] == 0, 1:3]  #np.unique(gridp[:,1:3],axis=0)
    vectorshift_ab_J0 = np.zeros((uabs.shape[0], 2))
    vectorshift_ab_J0.fill(np.nan)
    vectorshift_ab = np.zeros((vectorshift.shape[0], 2))
    vectorshift_ab.fill(np.nan)
    for i in range(uabs.shape[0]):
        cond = (gridp[:, 1:3] == uabs[i, :]).all(axis=1)
        if cond.any() & np.logical_not(
                np.isnan(vectorshift[cond, 1:3]).all()
        ):  #last condition is to avoid warning of taking nanmean of empty slice when all are NaNs
            vectorshift_ab_J0[i, :] = np.nanmean(vectorshift[cond, 1:3],
                                                 axis=0)
            vectorshift_ab[cond, :] = np.nanmean(vectorshift[cond, 1:3],
                                                 axis=0)

    # Calculate length of shift vectors:
    vectorshift_len = np.sqrt((vectorshift**2).sum(axis=vectorshift.ndim - 1))
    vectorshift_ab_len = np.sqrt(
        (vectorshift_ab**2).sum(axis=vectorshift_ab.ndim - 1))
    vectorshift_ab_J0_len = np.sqrt(
        (vectorshift_ab_J0**2).sum(axis=vectorshift_ab_J0.ndim - 1))

    # Calculate average DE for normalization of vectorshifts
    DEi_Jab_avg = np.sqrt(((Jabt - Jabr)**2).sum(axis=Jabr.ndim - 1))
    DE_Jab_avg = DEi_Jab_avg.mean(axis=0)
    DEi_ab_avg = np.sqrt(
        ((Jabt[..., 1:3] - Jabr[..., 1:3])**2).sum(axis=Jabr[..., 1:3].ndim -
                                                   1))
    DE_ab_avg = DEi_ab_avg.mean(axis=0)

    # calculate vectorfield:
    axr = uabs[:, 0, None]
    bxr = uabs[:, 1, None]
    axt = axr + vectorshift_ab_J0[:, 0, None]
    bxt = bxr + vectorshift_ab_J0[:, 1, None]

    data = {
        'Jab': {
            'Jabr': Jabr_avg,
            'Jabt': Jabt_avg,
            'DEi': DEi_Jab_avg,
            'DEi_ab': DEi_ab_avg,
            'DEa': DE_Jab_avg,
            'DEa_ab': DE_ab_avg
        },
        'vshifts': {
            'vectorshift': vectorshift,
            'vectorshift_ab': vectorshift_ab,
            'vectorshift_ab_J0': vectorshift_ab_J0,
            'vectorshift_len': vectorshift_len,
            'vectorshift_ab_len': vectorshift_ab_len,
            'vectorshift_ab_J0_len': vectorshift_ab_J0_len,
            'vectorshift_len_DEnormed': vectorshift_len / DE_Jab_avg,
            'vectorshift_ab_len_DEnormed': vectorshift_ab_len / DE_ab_avg,
            'vectorshift_ab_J0_len_DEnormed': vectorshift_ab_J0_len / DE_ab_avg
        },
        'pixeldata': {
            'grid': gridp,
            'idx': idxp,
            'Jab': jabp,
            'samplenrs': pixelsamplenrs,
            'IDs': pixelIDs
        },
        'fielddata': {
            'vectorfield': {
                'axr': axr,
                'bxr': bxr,
                'axt': axt,
                'bxt': bxt
            }
        }
    }
    return data
Exemple #3
0
def xyz_to_rfl(xyz, CSF = None, rfl = None, out = 'rfl_est', \
                 refspd = None, D = None, cieobs = _CIEOBS, \
                 cspace = 'xyz', cspace_tf = {},\
                 interp_type = 'nd', k_neighbours = 4, verbosity = 0):
    """
    Approximate spectral reflectance of xyz values based on nd-dimensional linear interpolation 
    or k nearest neighbour interpolation of samples from a standard reflectance set.
    
    Args:
        :xyz: 
            | ndarray with xyz values of target points.
        :CSF:
            | None, optional
            | RGB camera response functions.
            | If None: input :xyz: contains raw rgb (float) values. Override :cspace:
            | argument and perform estimation directly in raw rgb space!!!
        :rfl: 
            | ndarray, optional
            | Reflectance set for color coordinate to rfl mapping.
        :out: 
            | 'rfl_est' or str, optional
        :refspd: 
            | None, optional
            | Refer ence spectrum for color coordinate to rfl mapping.
            | None defaults to D65.
        :cieobs:
            | _CIEOBS, optional
            | CMF set used for calculation of xyz from spectral data.
        :cspace:
            | 'xyz',  optional
            | Color space for color coordinate to rfl mapping.
            | Tip: Use linear space (e.g. 'xyz', 'Yuv',...) for (interp_type == 'nd'),
            |      and perceptually uniform space (e.g. 'ipt') for (interp_type == 'nearest')
        :cspace_tf:
            | {}, optional
            | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform.
        :interp_type:
            | 'nd', optional
            | Options:
            | - 'nd': perform n-dimensional linear interpolation using Delaunay triangulation.
            | - 'nearest': perform nearest neighbour interpolation. 
        :k_neighbours:
            | 4 or int, optional
            | Number of nearest neighbours for reflectance spectrum interpolation.
            | Neighbours are found using scipy.spatial.cKDTree
        :verbosity:
            | 0, optional
            | If > 0: make a plot of the color coordinates of original and 
            | rendered image pixels.

    Returns:
        :returns: 
            | :rfl_est:
            | ndarrays with estimated reflectance spectra.
    """

    # get rfl set:
    if rfl is None:  # use IESTM30['4880'] set
        rfl = _CRI_RFL['ies-tm30']['4880']['5nm']

    wlr = rfl[0]

    # get Ref spd:
    if refspd is None:
        refspd = _CIE_ILLUMINANTS['D65'].copy()
    refspd = cie_interp(
        refspd, wlr,
        kind='linear')  # force spd to same wavelength range as rfl

    # Calculate rgb values of standard rfl set under refspd:
    if CSF is None:
        # Calculate lab coordinates:
        xyz_rr, xyz_wr = spd_to_xyz(refspd,
                                    relative=True,
                                    rfl=rfl,
                                    cieobs=cieobs,
                                    out=2)
        cspace_tf_copy = cspace_tf.copy()
        cspace_tf_copy[
            'xyzw'] = xyz_wr  # put correct white point in param. dict
        lab_rr = colortf(xyz_rr,
                         tf=cspace,
                         fwtf=cspace_tf_copy,
                         bwtf=cspace_tf_copy)[:, 0, :]
    else:
        # Calculate rgb coordinates from camera sensitivity functions
        rgb_rr = rfl_to_rgb(rfl, spd=refspd, CSF=CSF, wl=None)
        lab_rr = rgb_rr
        xyz = xyz
        lab_rr = np.round(lab_rr, _ROUNDING)  # speed up search

    # Convert xyz to lab-type values under refspd:
    if CSF is None:
        lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy)
    else:
        lab = xyz  # xyz contained rgb values !!!
        rgb = xyz
        lab = np.round(lab, _ROUNDING)  # speed up search

    if interp_type == 'nearest':
        # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric
        # color coordinates for each value in lab_ur (i.e. smallest DE):
        # Construct cKDTree:
        tree = sp.spatial.cKDTree(lab_rr, copy_data=True)

        # Interpolate rfls using k nearest neightbours and inverse distance weigthing:
        d, inds = tree.query(lab, k=k_neighbours)
        if k_neighbours > 1:
            d += _EPS
            w = (1.0 / d**2)[:, :, None]  # inverse distance weigthing
            rfl_est = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(w, axis=1)
        else:
            rfl_est = rfl[inds + 1, :].copy()
    elif interp_type == 'nd':

        rfl_est = math.ndinterp1_scipy(lab_rr, rfl[1:], lab)

        _isnan = np.isnan(rfl_est[:, 0])

        if (
                _isnan.any()
        ):  #do nearest neigbour method for those that fail using Delaunay (i.e. ndinterp1_scipy)

            # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric
            # color coordinates for each value in lab_ur (i.e. smallest DE):
            # Construct cKDTree:
            tree = sp.spatial.cKDTree(lab_rr, copy_data=True)

            # Interpolate rfls using k nearest neightbours and inverse distance weigthing:
            d, inds = tree.query(lab[_isnan, ...], k=k_neighbours)

            if k_neighbours > 1:
                d += _EPS
                w = (1.0 / d**2)[:, :, None]  # inverse distance weigthing
                rfl_est_isnan = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(
                    w, axis=1)
            else:
                rfl_est_isnan = rfl[inds + 1, :].copy()
            rfl_est[_isnan, :] = rfl_est_isnan

    else:
        raise Exception('xyz_to_rfl(): unsupported interp_type!')

    rfl_est[
        rfl_est <
        0] = 0  #can occur for points outside convexhull of standard rfl set.

    rfl_est = np.vstack((rfl[0], rfl_est))

    if ((verbosity > 0) | ('xyz_est' in out.split(',')) |
        ('lab_est' in out.split(',')) | ('DEi_ab' in out.split(',')) |
        ('DEa_ab' in out.split(','))) & (CSF is None):
        xyz_est, _ = spd_to_xyz(refspd,
                                rfl=rfl_est,
                                relative=True,
                                cieobs=cieobs,
                                out=2)
        cspace_tf_copy = cspace_tf.copy()
        cspace_tf_copy[
            'xyzw'] = xyz_wr  # put correct white point in param. dict
        lab_est = colortf(xyz_est, tf=cspace, fwtf=cspace_tf_copy)[:, 0, :]
        DEi_ab = np.sqrt(((lab_est[:, 1:3] - lab[:, 1:3])**2).sum(axis=1))
        DEa_ab = DEi_ab.mean()
    elif ((verbosity > 0) | ('xyz_est' in out.split(',')) |
          ('rgb_est' in out.split(',')) | ('DEi_rgb' in out.split(',')) |
          ('DEa_rgb' in out.split(','))) & (CSF is not None):
        rgb_est = rfl_to_rgb(rfl_est[1:], spd=refspd, CSF=CSF, wl=wlr)
        xyz_est = rgb_est
        DEi_rgb = np.sqrt(((rgb_est - rgb)**2).sum(axis=1))
        DEa_rgb = DEi_rgb.mean()

    if verbosity > 0:
        if CSF is None:
            ax = plot_color_data(lab[...,1], lab[...,2], z = lab[...,0], \
                            show = False, cieobs = cieobs, cspace = cspace, \
                            formatstr = 'ro', label = 'Original')
            plot_color_data(lab_est[...,1], lab_est[...,2], z = lab_est[...,0], \
                            show = True, axh = ax, cieobs = cieobs, cspace = cspace, \
                            formatstr = 'bd', label = 'Rendered')
        else:
            n = 100  #min(rfl.shape[0]-1,rfl_est.shape[0]-1)
            s = np.random.permutation(rfl.shape[0] -
                                      1)[:min(n, rfl.shape[0] - 1)]
            st = np.random.permutation(rfl_est.shape[0] -
                                       1)[:min(n, rfl_est.shape[0] - 1)]
            fig = plt.figure()
            ax = np.zeros((3, ), dtype=np.object)
            ax[0] = fig.add_subplot(131)
            ax[1] = fig.add_subplot(132)
            ax[2] = fig.add_subplot(133, projection='3d')
            ax[0].plot(rfl[0], rfl[1:][s].T, linestyle='-')
            ax[0].set_title('Original RFL set (random selection of all)')
            ax[0].set_ylim([0, 1])
            ax[1].plot(rfl_est[0], rfl_est[1:][st].T, linestyle='--')
            ax[0].set_title('Estimated RFL set (random selection of targets)')
            ax[1].set_ylim([0, 1])
            ax[2].plot(rgb[st, 0],
                       rgb[st, 1],
                       rgb[st, 2],
                       'ro',
                       label='Original')
            ax[2].plot(rgb_est[st, 0],
                       rgb_est[st, 1],
                       rgb_est[st, 2],
                       'bd',
                       label='Rendered')
            ax[2].legend()
    if out == 'rfl_est':
        return rfl_est
    elif out == 'rfl_est,xyz_est':
        return rfl_est, xyz_est
    else:
        return eval(out)
Exemple #4
0
def plot_chromaticity_diagram_colors(diagram_samples = 256, diagram_opacity = 1.0, diagram_lightness = 0.25,\
                                      cieobs = _CIEOBS, cspace = 'Yxy', cspace_pars = {},\
                                      show = True, axh = None,\
                                      show_grid = False, label_fontname = 'Times New Roman', label_fontsize = 12,\
                                      **kwargs):
    """
    Plot the chromaticity diagram colors.
    
    Args:
        :diagram_samples:
            | 256, optional
            | Sampling resolution of color space.
        :diagram_opacity:
            | 1.0, optional
            | Sets opacity of chromaticity diagram
        :diagram_lightness:
            | 0.25, optional
            | Sets lightness of chromaticity diagram
        :axh: 
            | None or axes handle, optional
            | Determines axes to plot data in.
            | None: make new figure.
        :show:
            | True or False, optional
            | Invoke matplotlib.pyplot.show() right after plotting
        :cieobs:
            | luxpy._CIEOBS or str, optional
            | Determines CMF set to calculate spectrum locus or other.
        :cspace:
            | luxpy._CSPACE or str, optional
            | Determines color space / chromaticity diagram to plot data in.
            | Note that data is expected to be in specified :cspace:
        :cspace_pars:
            | {} or dict, optional
            | Dict with parameters required by color space specified in :cspace: 
            | (for use with luxpy.colortf())
        :show_grid:
            | False, optional
            | Show grid (True) or not (False)
        :label_fontname: 
            | 'Times New Roman', optional
            | Sets font type of axis labels.
        :label_fontsize:
            | 12, optional
            | Sets font size of axis labels.
        :kwargs: 
            | additional keyword arguments for use with matplotlib.pyplot.
        
    Returns:
        
    """

    if isinstance(cieobs, str):
        SL = _CMF[cieobs]['bar'][1:4].T
    else:
        SL = cieobs[1:4].T
    SL = 100.0 * SL / (SL[:, 1, None] + _EPS)
    SL = SL[SL.sum(axis=1) >
            0, :]  # avoid div by zero in xyz-to-Yxy conversion
    SL = colortf(SL, tf=cspace, tfa0=cspace_pars)
    plambdamax = SL[:, 1].argmax()
    SL = np.vstack(
        (SL[:(plambdamax + 1), :], SL[0])
    )  # add lowest wavelength data and go to max of gamut in x (there is a reversal for some cmf set wavelengths >~700 nm!)
    Y, x, y = asplit(SL)
    SL = np.vstack((x, y)).T

    # create grid for conversion to srgb
    offset = _EPS
    min_x = min(offset, x.min())
    max_x = max(1, x.max())
    min_y = min(offset, y.min())
    max_y = max(1, y.max())
    ii, jj = np.meshgrid(
        np.linspace(min_x - offset, max_x + offset, int(diagram_samples)),
        np.linspace(max_y + offset, min_y - offset, int(diagram_samples)))
    ij = np.dstack((ii, jj))
    ij[ij == 0] = offset

    ij2D = ij.reshape((diagram_samples**2, 2))
    ij2D = np.hstack((diagram_lightness * 100 * np.ones(
        (ij2D.shape[0], 1)), ij2D))
    xyz = colortf(ij2D, tf=cspace + '>xyz', tfa0=cspace_pars)

    xyz[xyz < 0] = 0
    xyz[np.isinf(xyz.sum(axis=1)), :] = np.nan
    xyz[np.isnan(xyz.sum(axis=1)), :] = offset

    srgb = xyz_to_srgb(xyz)
    srgb = srgb / srgb.max()
    srgb = srgb.reshape((diagram_samples, diagram_samples, 3))

    if show == True:
        if axh is None:
            fig = plt.figure()
            axh = fig.add_subplot(111)
        polygon = Polygon(SL, facecolor='none', edgecolor='none')
        axh.add_patch(polygon)
        image = axh.imshow(srgb,
                           interpolation='bilinear',
                           extent=(min_x, max_x, min_y - 0.05, max_y),
                           clip_path=None,
                           alpha=diagram_opacity)
        image.set_clip_path(polygon)
        axh.plot(x, y, color='darkgray')
        if (cspace == 'Yxy') & (isinstance(cieobs, str)):
            axh.set_xlim([0, 1])
            axh.set_ylim([0, 1])
        elif (cspace == 'Yuv') & (isinstance(cieobs, str)):
            axh.set_xlim([0, 0.6])
            axh.set_ylim([0, 0.6])
        if (cspace is not None):
            xlabel = _CSPACE_AXES[cspace][1]
            ylabel = _CSPACE_AXES[cspace][2]
            if (label_fontname is not None) & (label_fontsize is not None):
                axh.set_xlabel(xlabel,
                               fontname=label_fontname,
                               fontsize=label_fontsize)
                axh.set_ylabel(ylabel,
                               fontname=label_fontname,
                               fontsize=label_fontsize)

        if show_grid == True:
            axh.grid(True)
        #plt.show()

        return axh
    else:
        return None
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):
    """
    Generate Individual Observer CMFs (cone fundamentals) 
    based on CIE2006 cone fundamentals and published literature 
    on observer variability in color matching and in physiological parameters.
    
    Args:
        :age: 
            | 32 or float or int, optional
            | Observer age
        :fieldsize:
            | 10, optional
            | Field size of stimulus in degrees (between 2° and 10°).
        :wl: 
            | None, optional
            | Interpolation/extraplation of :LMS: output to specified wavelengths.
            | None: output original _WL = np.array([390,780,5])
        :var_od_lens:
            | 0, optional
            | Std Dev. in peak optical density [%] of lens.
        :var_od_macula:
            | 0, optional
            | Std Dev. in peak optical density [%] of macula.
        :var_od_L:
            | 0, optional
            | Std Dev. in peak optical density [%] of L-cone.
        :var_od_M:
            | 0, optional
            | Std Dev. in peak optical density [%] of M-cone.
        :var_od_S:
            | 0, optional
            | Std Dev. in peak optical density [%] of S-cone.
        :var_shft_L:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of L-cone. 
        :var_shft_L:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of M-cone.  
        :var_shft_S:
            | 0, optional
            | Std Dev. in peak wavelength shift [nm] of S-cone. 
        :out: 
            | 'LMS' or , optional
            | Determines output.
        :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' : ndarray with individual observer area-normalized 
            |           cone fundamentals. Wavelength have been added.
                
            | [- 'trans_lens': ndarray with lens transmission 
            |      (no wavelengths added, no interpolation)
            |  - 'trans_macula': ndarray with macula transmission 
            |      (no wavelengths added, no interpolation)
            |  - 'sens_photopig' : ndarray with photopigment sens. 
            |      (no wavelengths added, no interpolation)]
            
    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>`_
    """
    fs = fieldsize
    rmd = _INDVCMF_DATA['rmd'].copy()
    LMSa = _INDVCMF_DATA['LMSa'].copy()
    docul = _INDVCMF_DATA['docul'].copy()

    # field size corrected macular density:
    pkOd_Macula = 0.485 * np.exp(-fs / 6.132) * (
        1 + var_od_macula / 100)  # varied peak optical density of macula
    corrected_rmd = rmd * pkOd_Macula

    # age corrected lens/ocular media density:
    if (age <= 60):
        correct_lomd = docul[:1] * (1 + 0.02 * (age - 32)) + docul[1:2]
    else:
        correct_lomd = docul[:1] * (1.56 + 0.0667 * (age - 60)) + docul[1:2]
    correct_lomd = correct_lomd * (1 + var_od_lens / 100
                                   )  # varied overall optical density of lens

    # Peak Wavelength Shift:
    wl_shifted = np.empty(LMSa.shape)
    wl_shifted[0] = _WL + var_shft_L
    wl_shifted[1] = _WL + var_shft_M
    wl_shifted[2] = _WL + var_shft_S

    LMSa_shft = np.empty(LMSa.shape)
    kind = 'cubic'
    LMSa_shft[0] = sp.interpolate.interp1d(wl_shifted[0],
                                           LMSa[0],
                                           kind=kind,
                                           bounds_error=False,
                                           fill_value="extrapolate")(_WL)
    LMSa_shft[1] = sp.interpolate.interp1d(wl_shifted[1],
                                           LMSa[1],
                                           kind=kind,
                                           bounds_error=False,
                                           fill_value="extrapolate")(_WL)
    LMSa_shft[2] = sp.interpolate.interp1d(wl_shifted[2],
                                           LMSa[2],
                                           kind=kind,
                                           bounds_error=False,
                                           fill_value="extrapolate")(_WL)
    #    LMSa[2,np.where(_WL >= _WL_CRIT)] = 0 #np.nan # Not defined above 620nm
    #    LMSa_shft[2,np.where(_WL >= _WL_CRIT)] = 0

    ssw = np.hstack(
        (0, np.sign(np.diff(LMSa_shft[2, :]))
         ))  #detect poor interpolation (sign switch due to instability)
    LMSa_shft[2, np.where((ssw >= 0) & (_WL > 560))] = np.nan

    # corrected LMS (no age correction):
    pkOd_L = (0.38 + 0.54 * np.exp(-fs / 1.333)) * (
        1 + var_od_L / 100)  # varied peak optical density of L-cone
    pkOd_M = (0.38 + 0.54 * np.exp(-fs / 1.333)) * (
        1 + var_od_M / 100)  # varied peak optical density of M-cone
    pkOd_S = (0.30 + 0.45 * np.exp(-fs / 1.333)) * (
        1 + var_od_S / 100)  # varied peak optical density of S-cone

    alpha_lms = 0. * LMSa_shft
    alpha_lms[0] = 1 - 10**(-pkOd_L * (10**LMSa_shft[0]))
    alpha_lms[1] = 1 - 10**(-pkOd_M * (10**LMSa_shft[1]))
    alpha_lms[2] = 1 - 10**(-pkOd_S * (10**LMSa_shft[2]))

    # this fix is required because the above math fails for alpha_lms[2,:]==0
    alpha_lms[2, np.where(_WL >= _WL_CRIT)] = 0

    # Corrected to Corneal Incidence:
    lms_barq = alpha_lms * (10**(-corrected_rmd - correct_lomd)) * np.ones(
        alpha_lms.shape)

    # Corrected to Energy Terms:
    lms_bar = lms_barq * _WL

    # Set NaN values to zero:
    lms_bar[np.isnan(lms_bar)] = 0

    # normalized:
    LMS = 100 * lms_bar / np.nansum(lms_bar, axis=1, keepdims=True)

    # Output extra:
    trans_lens = 10**(-correct_lomd)
    trans_macula = 10**(-corrected_rmd)
    sens_photopig = alpha_lms * _WL

    # Add wavelengths:
    LMS = np.vstack((_WL, LMS))

    if ('xyz' in out.lower().split(',')):
        LMS = lmsb_to_xyzb(LMS,
                           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')

    # Interpolate/extrapolate:
    if wl is None:
        interpolation = None
    else:
        interpolation = 'cubic'
    LMS = spd(LMS, wl=wl, interpolation=interpolation, norm_type='area')

    if (out == 'LMS'):
        return LMS
    elif (out == 'LMS,trans_lens,trans_macula,sens_photopig'):
        return LMS, trans_lens, trans_macula, sens_photopig
    elif (out == 'LMS,trans_lens,trans_macula,sens_photopig,LMSa'):
        return LMS, trans_lens, trans_macula, sens_photopig, LMSa
    else:
        return eval(out)
Exemple #6
0
def cie_interp(data,
               wl_new,
               kind=None,
               negative_values_allowed=False,
               extrap_values=None):
    """
    Interpolate / extrapolate spectral data following standard CIE15-2018.
    
    | The kind of interpolation depends on the spectrum type defined in :kind:. 
    | Extrapolation is always done by replicate the closest known values.
    
    Args:
        :data: 
            | ndarray with spectral data 
            | (.shape = (number of spectra + 1, number of original wavelengths))
        :wl_new: 
            | ndarray with new wavelengths
        :kind: 
            | None, optional
            |   - If :kind: is None, return original data.
            |   - If :kind: is a spectrum type (see _INTERP_TYPES), the correct 
            |     interpolation type if automatically chosen.
            |   - Or :kind: can be any interpolation type supported by 
            |     scipy.interpolate.interp1d (math.interp1d if nan's are present!!)
        :negative_values_allowed: 
            | False, optional
            | If False: negative values are clipped to zero.
        :extrap_values:
            | None, optional
            | If None: use CIE recommended 'closest value' approach when extrapolating.
            | If float or list or ndarray, use those values to fill extrapolated value(s).
            | If 'ext': use normal extrapolated values by scipy.interpolate.interp1d
    
    Returns:
        :returns: 
            | ndarray of interpolated spectral data.
            | (.shape = (number of spectra + 1, number of wavelength in wl_new))
    """
    if (kind is not None):
        # Wavelength definition:
        wl_new = getwlr(wl_new)

        if (not np.array_equal(data[0], wl_new)) | np.isnan(data).any():

            extrap_values = np.atleast_1d(extrap_values)

            # Set interpolation type based on data type:
            if kind in _INTERP_TYPES['linear']:
                kind = 'linear'
            elif kind in _INTERP_TYPES['cubic']:
                kind = 'cubic'

            # define wl, S, wl_new:
            wl = np.array(data[0])
            S = data[1:]
            wl_new = np.array(wl_new)

            # Interpolate each spectrum in S:
            N = S.shape[0]
            nan_indices = np.isnan(S)

            # Interpolate all (if not all rows have nan):
            rows_with_nans = np.where(nan_indices.sum(axis=1))[0]
            if not (rows_with_nans.size == N):
                #allrows_nans = False
                if extrap_values[0] is None:
                    fill_value = (0, 0)
                elif (((type(extrap_values[0]) == np.str_) |
                       (type(extrap_values[0]) == str))
                      and (extrap_values[0][:3] == 'ext')):
                    fill_value = 'extrapolate'
                else:
                    fill_value = (extrap_values[0], extrap_values[-1])
                Si = sp.interpolate.interp1d(wl,
                                             S,
                                             kind=kind,
                                             bounds_error=False,
                                             fill_value=fill_value)(wl_new)

                #extrapolate by replicating closest known (in source data!) value (conform CIE15-2004 recommendation)
                if extrap_values[0] is None:
                    Si[:, wl_new < wl[0]] = S[:, :1]
                    Si[:, wl_new > wl[-1]] = S[:, -1:]

            else:
                #allrows_nans = True
                Si = np.zeros([N, wl_new.shape[0]])
                Si.fill(np.nan)

            # Re-interpolate those which have none:
            if nan_indices.any():
                #looping required as some values are NaN's
                for i in rows_with_nans:

                    nonan_indices = np.logical_not(nan_indices[i])
                    wl_nonan = wl[nonan_indices]
                    S_i_nonan = S[i][nonan_indices]
                    Si_nonan = math.interp1(wl_nonan,
                                            S_i_nonan,
                                            wl_new,
                                            kind=kind,
                                            ext='extrapolate')
                    #                    Si_nonan = sp.interpolate.interp1d(wl_nonan, S_i_nonan, kind = kind, bounds_error = False, fill_value = 'extrapolate')(wl_new)

                    #extrapolate by replicating closest known (in source data!) value (conform CIE15-2004 recommendation)
                    if extrap_values[0] is None:
                        Si_nonan[wl_new < wl_nonan[0]] = S_i_nonan[0]
                        Si_nonan[wl_new > wl_nonan[-1]] = S_i_nonan[-1]
                    elif (((type(extrap_values[0]) == np.str_) |
                           (type(extrap_values[0]) == str))
                          and (extrap_values[0][:3] == 'ext')):
                        pass
                    else:
                        Si_nonan[wl_new < wl_nonan[0]] = extrap_values[0]
                        Si_nonan[wl_new > wl_nonan[-1]] = extrap_values[-1]

                    Si[i] = Si_nonan

            # No negative values allowed for spectra:
            if negative_values_allowed == False:
                if np.any(Si): Si[Si < 0.0] = 0.0

            # Add wavelengths to data array:
            return np.vstack((wl_new, Si))

    return data
def xyz_to_rfl(xyz, rfl = None, out = 'rfl_est', \
                 refspd = None, D = None, cieobs = _CIEOBS, \
                 cspace = 'xyz', cspace_tf = {},\
                 interp_type = 'nd', k_neighbours = 4, verbosity = 0):
    """
    Approximate spectral reflectance of xyz based on nd-dimensional linear interpolation 
    or k nearest neighbour interpolation of samples from a standard reflectance set.
    
    Args:
        :xyz: 
            | ndarray with tristimulus values of target points.
        :rfl: 
            | ndarray, optional
            | Reflectance set for color coordinate to rfl mapping.
        :out: 
            | 'rfl_est' or str, optional
        :refspd: 
            | None, optional
            | Refer ence spectrum for color coordinate to rfl mapping.
            | None defaults to D65.
        :cieobs:
            | _CIEOBS, optional
            | CMF set used for calculation of xyz from spectral data.
        :cspace:
            | 'xyz',  optional
            | Color space for color coordinate to rfl mapping.
            | Tip: Use linear space (e.g. 'xyz', 'Yuv',...) for (interp_type == 'nd'),
            |      and perceptually uniform space (e.g. 'ipt') for (interp_type == 'nearest')
        :cspace_tf:
            | {}, optional
            | Dict with parameters for xyz_to_cspace and cspace_to_xyz transform.
        :interp_type:
            | 'nd', optional
            | Options:
            | - 'nd': perform n-dimensional linear interpolation using Delaunay triangulation.
            | - 'nearest': perform nearest neighbour interpolation. 
        :k_neighbours:
            | 4 or int, optional
            | Number of nearest neighbours for reflectance spectrum interpolation.
            | Neighbours are found using scipy.spatial.cKDTree
        :verbosity:
            | 0, optional
            | If > 0: make a plot of the color coordinates of original and 
            | rendered image pixels.

    Returns:
        :returns: 
            | :rfl_est:
            | ndarrays with estimated reflectance spectra.
    """

    # get rfl set:
    if rfl is None:  # use IESTM30['4880'] set
        rfl = _CRI_RFL['ies-tm30']['4880']['5nm']

    # get Ref spd:
    if refspd is None:
        refspd = _CIE_ILLUMINANTS['D65'].copy()

    # Calculate lab-type coordinates of standard rfl set under refspd:
    xyz_rr, xyz_wr = spd_to_xyz(refspd,
                                relative=True,
                                rfl=rfl,
                                cieobs=cieobs,
                                out=2)
    cspace_tf_copy = cspace_tf.copy()
    cspace_tf_copy['xyzw'] = xyz_wr  # put correct white point in param. dict
    lab_rr = colortf(xyz_rr,
                     tf=cspace,
                     fwtf=cspace_tf_copy,
                     bwtf=cspace_tf_copy)[:, 0, :]

    # Convert xyz to lab-type values under refspd:
    lab = colortf(xyz, tf=cspace, fwtf=cspace_tf_copy, bwtf=cspace_tf_copy)

    if interp_type == 'nearest':
        # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric
        # color coordinates for each value in lab_ur (i.e. smallest DE):
        # Construct cKDTree:
        tree = sp.spatial.cKDTree(lab_rr, copy_data=True)

        # Interpolate rfls using k nearest neightbours and inverse distance weigthing:
        d, inds = tree.query(lab, k=k_neighbours)
        if k_neighbours > 1:
            d += _EPS
            w = (1.0 / d**2)[:, :, None]  # inverse distance weigthing
            rfl_est = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(w, axis=1)
        else:
            rfl_est = rfl[inds + 1, :].copy()
    elif interp_type == 'nd':
        rfl_est = math.ndinterp1_scipy(lab_rr, rfl[1:], lab)

        _isnan = np.isnan(rfl_est[:, 0])

        if (
                _isnan.any()
        ):  #do nearest neigbour method for those that fail using Delaunay (i.e. ndinterp1_scipy)

            # Find rfl (cfr. lab_rr) from rfl set that results in 'near' metameric
            # color coordinates for each value in lab_ur (i.e. smallest DE):
            # Construct cKDTree:
            tree = sp.spatial.cKDTree(lab_rr, copy_data=True)

            # Interpolate rfls using k nearest neightbours and inverse distance weigthing:
            d, inds = tree.query(lab[_isnan, ...], k=k_neighbours)
            if k_neighbours > 1:
                d += _EPS
                w = (1.0 / d**2)[:, :, None]  # inverse distance weigthing
                rfl_est_isnan = np.sum(w * rfl[inds + 1, :], axis=1) / np.sum(
                    w, axis=1)
            else:
                rfl_est_isnan = rfl[inds + 1, :].copy()
            rfl_est[_isnan, :] = rfl_est_isnan
    else:
        raise Exception('xyz_to_rfl(): unsupported interp_type!')

    rfl_est[
        rfl_est <
        0] = 0  #can occur for points outside convexhull of standard rfl set.

    rfl_est = np.vstack((rfl[0], rfl_est))

    if (verbosity > 0) | ('xyz_est' in out.split(',')) | (
            'lab_est' in out.split(',')) | ('DEi_ab' in out.split(',')) | (
                'DEa_ab' in out.split(',')):
        xyz_est, _ = spd_to_xyz(refspd,
                                rfl=rfl_est,
                                relative=True,
                                cieobs=cieobs,
                                out=2)
        cspace_tf_copy = cspace_tf.copy()
        cspace_tf_copy[
            'xyzw'] = xyz_wr  # put correct white point in param. dict
        lab_est = colortf(xyz_est, tf=cspace, fwtf=cspace_tf_copy)[:, 0, :]
        DEi_ab = np.sqrt(((lab_est[:, 1:3] - lab[:, 1:3])**2).sum(axis=1))
        DEa_ab = DEi_ab.mean()

    if verbosity > 0:
        ax = plot_color_data(lab[...,1], lab[...,2], z = lab[...,0], \
                        show = False, cieobs = cieobs, cspace = cspace, \
                        formatstr = 'ro', label = 'Original')
        plot_color_data(lab_est[...,1], lab_est[...,2], z = lab_est[...,0], \
                        show = True, axh = ax, cieobs = cieobs, cspace = cspace, \
                        formatstr = 'bd', label = 'Rendered')

    if out == 'rfl_est':
        return rfl_est
    elif out == 'rfl_est,xyz_est':
        return rfl_est, xyz_est
    else:
        return eval(out)