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
Beispiel #2
0
 def get_subset(self, idx_R=None, idx_S=None):
     """
     Get spectral data related to specific light source and reflectance data
     | (cfr. axis = 1 and axis = 0 in xyz ndarrays).
     
     Args:
         :idx_S: 
             | None, optional
             | Index of light source related spectral data.
             | None: selects all.
         :idx_R:
             | None, optional
             | Index of reflectance sample related spectral data.
             | None selects all.
     Returns:
         :returns:
             | luxpy.CDATA instance with only selected spectral data.
         
     Note: 
         If ndim < 3: selection is based on :idx_R:
     """
     if idx_S is None:
         idx_S = np.arange(self.value.shape[1])
     if idx_R is None:
         idx_R = np.arange(self.value.shape[0])
     if self.value.ndim == 3:
         self.value = self.value[idx_R, idx_S, :]
     else:
         self.value = self.value[idx_R, ...]
     self.shape = self.value.shape
     return self
def generate_vector_field(poly_model, pmodel, \
                          axr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \
                          bxr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), \
                          make_grid = True, limit_grid_radius = 0,color = 'k'):
    """
    Generates a field of vectors using the base color shift model.
    
    | Has the option to plot vector field.
    
    Args:
        :poly_model: 
            | function handle to model
        :pmodel:
            | ndarray with model parameters.
        :axr: 
            | np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), optional
            | Ndarray specifying the a-coordinates at which to apply the model.
        :bxr:
            | np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), optional
            | Ndarray specifying the b-coordinates at which to apply the model.
        :make_grid:
            | True, optional
            | True: generate a 2d-grid from :axr:, :bxr:.
        :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:
        :color:
            | 'k', optional
            | For plotting the vector field.
            | If :color: == 0, no plot will be generated.
    
    Returns:
        :returns: 
            | If :color: == 0: ndarray of axt,bxt,axr,bxr
            | Else: handle to axes used for plotting.
    """
    
    
    # Generate grid from axr, bxr:
    if make_grid == True:
        axr, bxr = generate_grid(ax = axr, bx = bxr, out = 'ax,bx', limit_grid_radius = limit_grid_radius)

    # Apply model at ref. coordinates:
    axt,bxt,Cxt,hxt,axr,bxr,Cxr,hxr = apply_poly_model_at_x(poly_model, pmodel,axr,bxr)
    
    # Plot vectorfield:
    if color is not 0: 
        #plt.plot(axr, bxr,'ro',markersize=2)
        plt.quiver(axr, bxr, axt-axr, bxt-bxr, headlength=1,color = color)
        plt.xlabel("a'")
        plt.ylabel("b'")
        return plt.gca()#plt.show(plot1)
    else:
        return axt,bxt,axr,bxr
Beispiel #4
0
def mutation(Xp, options):
    """
    Performs mutation in the individuals.
    The mutation is one of the operators responsible for random changes in
    the individuals. Each parent x will have a new individual, called trial
    vector u, after the mutation.
    To do that, pick up two random individuals from the population, x2 and
    x3, and creates a difference vector v = x2 - x3. Then, chooses another
    point, called base vector, xb, and creates the trial vector by

       u = xb + F*v = xb + F*(x2 - x3)

    wherein F is an internal parameter, called scale factor.

    Args:
       :Xp: 
           | a n x mu ndarray with mu "parents" and of dimension n
       :options: 
           | the dict with the internal parameters

    Returns:
       :Xo: 
           | a n x mu ndarray with the mu mutated individuals (of dimension n)
    """
    # Creates a mu x mu matrix of 1:n elements on each row
    A = np.arange(options['mu']).repeat(options['mu']).reshape(
        options['mu'], options['mu']).T

    # Now, one removes the diagonal of A, because it contains indexes that repeat
    # the current i-th individual
    A = np.reshape(A[(np.eye(A.shape[0])) == False],
                   (options['mu'], options['mu'] - 1))

    # Now, creates a matrix that permutes the elements of A randomly
    J = np.argsort(np.random.rand(*A.shape), axis=1)
    #    J = getdata('J.txt')-1

    Ilin = J * options['mu'] + np.arange(options['mu'])[:, None]
    A = A.T.flatten()[Ilin].reshape(A.shape)

    # Chooses three random points (for each row)
    xbase = Xp[:, A[:, 0]]  #base vectors
    v = Xp[:, A[:, 1]] - Xp[:, A[:, 2]]  #difference vector

    # Performs the mutation
    Xo = xbase + options['F'] * v

    return Xo
Beispiel #5
0
def vec_to_dict(vec=None, dic={}, vsize=None, keys=None):
    """
    Convert dict to vec and vice versa.
    
    Args:
        :vec: 
            | list or vector array, optional
        :dic: 
            | dict, optional
        :vsize:
            | list or vector array with size of values of dict, optional
        :keys:
            | list or vector array with keys in dict (must be provided).
        
    Returns:
        :returns:
            | x, vsize
            |   x is an array, if vec is None
            |   x is a dict, if vec is not None
    """
    if vec is not None:
        # Put values in vec in dic:
        n = 0  # keeps track of length already read from x
        for i, v in enumerate(keys):
            dic[v] = vec[n + np.arange(vsize[i])]
            n += dic[v].shape[0]
        return dic, vsize
    else:
        # Put values of keys in dic in vec:
        vec = []
        vsize = []
        for i, v in enumerate(keys):
            vec = np.hstack((vec, dic[v]))
            vsize.append(dic[v].shape[0])
        return vec, vsize
Beispiel #6
0
def v_to_cik(v, inverse=False):
    """
    Calculate 2x2 '(covariance matrix)^-1' elements cik 
    
    Args:
        :v: 
            | (Nx5) np.ndarray
            | ellipse parameters [Rmax,Rmin,xc,yc,theta]
    
    Returns:
        :cik: 
            '2x2xN' (covariance matrix)^-1
    
    Notes:
        | cik is not actually a covariance matrix,
        | only for a Gaussian or normal distribution!

    """
    v = np.atleast_2d(v)
    g11 = (1 / v[:, 0] * np.cos(v[:, 4]))**2 + (1 / v[:, 1] *
                                                np.sin(v[:, 4]))**2
    g22 = (1 / v[:, 0] * np.sin(v[:, 4]))**2 + (1 / v[:, 1] *
                                                np.cos(v[:, 4]))**2
    g12 = (1 / v[:, 0]**2 - 1 / v[:, 1]**2) * np.sin(v[:, 4]) * np.cos(v[:, 4])
    cik = np.zeros((g11.shape[0], 2, 2))

    for i in np.arange(g11.shape[0]):
        cik[i, :, :] = np.vstack((np.hstack(
            (g11[i], g12[i])), np.hstack((g12[i], g22[i]))))
        if inverse == True:
            cik[i, :, :] = np.linalg.inv(cik[i, :, :])
    return cik
def apply_poly_model_at_hue_x(poly_model, pmodel, dCHoverC_res, \
                              hx = None, Cxr = 40, sig = _VF_SIG):
    """
    Applies base color shift model at (hue,chroma) coordinates
    
    Args:
        :poly_model: 
            | function handle to model
        :pmodel:
            | ndarray with model parameters.
        :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 luxpy.cri.get_poly_model())
        :hx:
            | None or ndarray, optional
            | None defaults to np.arange(np.pi/10.0,2*np.pi,2*np.pi/10.0)
        :Cxr:
            | 40, optional
        :sig: 
            | _VF_SIG or float, optional
            | Determines smooth transition between hue-bin-boundaries (no hard 
              cutoff at hue bin boundary).
        
    Returns:
        :returns: 
            | ndarrays with dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig
            | Note '_sig' denotes the uncertainty: 
            |     e.g.  dH_x_sig is the uncertainty of dH at input (hue/chroma).
    """
     
    if hx is None:
        dh = 2*np.pi/10.0;
        hx = np.arange(dh/2,2*np.pi,dh) #hue angles at which to apply model, i.e. calculate 'average' measures
        
    # A calculate reference coordinates:
    axr = Cxr*np.cos(hx)
    bxr = Cxr*np.sin(hx)
    
    # B apply model at reference coordinates to obtain test coordinates:
    axt,bxt,Cxt,hxt,axr,bxr,Cxr,hxr = apply_poly_model_at_x(poly_model, pmodel,axr,bxr)
    
    # C Calculate dC/C, dH for test and ref at fixed hues:
    dCoverC_x = (Cxt-Cxr)/(np.hstack((Cxr+Cxt)).max())
    dH_x = (180/np.pi)*(hxt-hxr)
#    dCoverC_x = np.round(dCoverC_x,decimals = 2)
#    dH_x = np.round(dH_x,decimals = 0)

    # D calculate 'average' noise measures using sig-value:
    href = dCHoverC_res[:,0:1]
    dCoverC_res = dCHoverC_res[:,1:2]
    dHoverC_res = dCHoverC_res[:,2:3]
    dHsigi = np.exp((np.dstack((np.abs(hx-href),np.abs((hx-href-2*np.pi)),np.abs(hx-href-2*np.pi))).min(axis=2)**2)/(-2)/sig)
    dH_x_sig = (180/np.pi)*(np.sqrt((dHsigi*(dHoverC_res**2)).sum(axis=0,keepdims=True)/dHsigi.sum(axis=0,keepdims=True)))
    #dH_x_sig_avg = np.sqrt(np.sum(dH_x_sig**2,axis=1)/hx.shape[0])
    dCoverC_x_sig = (np.sqrt((dHsigi*(dCoverC_res**2)).sum(axis=0,keepdims=True)/dHsigi.sum(axis=0,keepdims=True)))
    #dCoverC_x_sig_avg = np.sqrt(np.sum(dCoverC_x_sig**2,axis=1)/hx.shape[0])

    return dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig
Beispiel #8
0
def crowdingdistance(F):
    """
    Computes the crowding distance of a nondominated front.
    The crowding distance gives a measure of how close the individuals are
    with regard to its neighbors. The higher this value, the greater the
    spacing. This is used to promote better diversity in the population.

    Args:
       F: 
           | an m x mu ndarray with mu individuals and m objectives

    Returns:
       cdist: 
           | a m-length column vector
    """
    m, mu = F.shape  #gets the size of F

    if mu == 2:
        cdist = np.vstack((np.inf, np.inf))
        return cdist

    #[Fs, Is] = sort(F,2); #sorts the objectives by individuals
    Is = F.argsort(axis=1)
    Fs = np.sort(F, axis=1)

    # Creates the numerator
    C = Fs[:, 2:] - Fs[:, :-2]
    C = np.hstack((np.inf * np.ones((m, 1)), C, np.inf * np.ones(
        (m, 1))))  #complements with inf in the extremes

    # Indexing to permute the C matrix in the right ordering
    Aux = np.arange(m).repeat(mu).reshape(m, mu)
    ind = np.ravel_multi_index(
        (Aux.flatten(), Is.flatten()),
        (m, mu
         ))  #converts to lin. indexes # ind = sub2ind([m, mu], Aux(:), Is(:));
    C2 = C.flatten().copy()
    C2[ind] = C2.flatten()
    C = C2.reshape((m, mu))

    # Constructs the denominator
    den = np.repeat((Fs[:, -1] - Fs[:, 0])[:, None], mu, axis=1)

    # Calculates the crowding distance
    cdist = (C / den).sum(axis=0)
    cdist = cdist.flatten()  #assures a column vector
    return cdist
Beispiel #9
0
def cik_to_v(cik, xyc=None, inverse=False):
    """
    Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik 
    
    Args:
        :cik: 
            '2x2xN' (covariance matrix)^-1
            
    Returns:
        :v: 
            | (Nx5) np.ndarray
            | ellipse parameters [Rmax,Rmin,xc,yc,theta]

    Notes:
        | cik is not actually the inverse covariance matrix,
        | only for a Gaussian or normal distribution!

    """
    if inverse == True:
        for i in np.arange(cik.shape[0]):
            cik[i, :, :] = np.linalg.inv(cik[i, :, :])

    g11 = cik[:, 0, 0]
    g22 = cik[:, 1, 1]
    g12 = cik[:, 0, 1]

    theta2 = 1 / 2 * np.arctan2(2 * g12, (g11 - g22))
    theta = theta2 + (np.pi / 2) * (g12 < 0)
    theta2 = theta
    cottheta = np.cos(theta) / np.sin(theta)  #np.cot(theta)
    cottheta[np.isinf(cottheta)] = 0

    a = 1 / np.sqrt((g22 + g12 * cottheta))
    b = 1 / np.sqrt((g11 - g12 * cottheta))

    v = np.vstack((a, b, np.zeros(a.shape), np.zeros(a.shape), theta)).T

    # add center coordinates:
    if xyc is not None:
        v[:, 2:4] = xyc

    return v
Beispiel #10
0
def xtransform(x, params):
    """
    Converts unconstrained variables into their original domains.
    """

    xtrans = np.zeros((params['n']))

    # k allows some variables to be fixed, thus dropped from the optimization.
    k = 0
    for i in np.arange(params['n']):
        if params['BoundClass'][i] == 1:
            # lower bound only
            xtrans[i] = params['LB'][i] + x[k]**2

        elif params['BoundClass'][i] == 2:
            # upper bound only
            xtrans[i] = params['UB'][i] - x[k]**2

        elif params['BoundClass'][i] == 3:
            # lower and upper bounds
            xtrans[i] = (np.sin(x[k]) + 1) / 2
            xtrans[i] = xtrans[i] * (params['UB'][i] -
                                     params['LB'][i]) + params['LB'][i]

            # just in case of any floating point problems
            xtrans[i] = np.hstack(
                (params['LB'][i], np.hstack(
                    (params['UB'][i], xtrans[i])).min())).max()

        elif params['BoundClass'][i] == 4:
            # fixed variable, bounds are equal, set it at either bound
            xtrans[i] = params['LB'][i]

        elif params['BoundClass'][i] == 0:
            # unconstrained variable.
            xtrans[i] = x[k]

        if params['BoundClass'][i] != 4:
            k += 1

    return xtrans
Beispiel #11
0
    def __init__(self, spd = None, wl = None, ax0iswl = True, dtype = 'S', \
                 wl_new = None, interp_method = 'auto', negative_values_allowed = False, \
                 norm_type = None, norm_f = 1,\
                 header = None, sep = ','):
        """
        Initialize instance of SPD.
        
        Args:
            :spd: 
                | None or ndarray or str, optional
                | If None: self.value is initialized with zeros.
                | If str: spd contains filename.
                | If ndarray: ((wavelength, spectra)) or (spectra). 
                |     If latter, :wl: should contain the wavelengths.
            :wl: 
                | None or ndarray, optional
                | Wavelengths.
                | Either specified as a 3-vector ([start, stop, spacing]) 
                | or as full wavelength array.
            :a0iswl:
                | True, optional
                | Signals that first axis of :spd: contains wavelengths.
            :dtype:
                | 'S', optional
                | Type of spectral object (e.g. 'S' for source spectrum, 'R' for
                  reflectance spectra, etc.)
                | See SPD._INTERP_TYPES for more options. 
                | This is used to automatically determine the correct kind of
                  interpolation method according to CIE15-2004.
            :wl_new: 
                | None or ndarray with wavelength range, optional
                | If None: don't interpolate, else perform interpolation.
            :interp_method:
                | - 'auto', optional
                | If 'auto': method is determined based on :dtype:
            :negative-values_allowed:
                | False, optional (for cie_interp())
                | Spectral data can not be negative. Values < 0 are therefore 
                  clipped when set to False.
            :norm_type:
                | None or str, optional
                | - 'lambda': make lambda in norm_f equal to 1
                | - 'area': area-normalization times norm_f
                | - 'max': max-normalization times norm_f
            :norm_f:
                | 1, optional
                | Normalization factor determines size of normalization 
                | for 'max' and 'area' 
                | or which wavelength is normalized to 1 for 'lambda' option.
        """
        if spd is not None:
            if isinstance(spd, str):
                spd = SPD.read_csv_(self, file=spd, header=header, sep=sep)
            if ax0iswl == True:
                self.wl = spd[0]
                self.value = spd[1:]
            else:
                self.wl = wl
                if (self.wl.size == 3):
                    self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2])
                self.value = spd
            if self.value.shape[1] != self.wl.shape[0]:
                raise Exception(
                    'SPD.__init__(): Dimensions of wl and spd do not match.')
        else:
            if (wl is None):
                self.wl = SPD._WL3
            else:
                self.wl = wl
            if (self.wl.size == 3):
                self.wl = np.arange(self.wl[0], self.wl[1] + 1, self.wl[2])
            self.value = np.zeros((1, self.wl.size))

        self.wl = self.wl
        self.dtype = dtype
        self.shape = self.value.shape
        self.N = self.shape[0]

        if wl_new is not None:
            if interp_method == 'auto':
                interp_method = dtype
            self.cie_interp(wl_new,
                            kind=interp_method,
                            negative_values_allowed=negative_values_allowed)
        if norm_type is not None:
            self.normalize(norm_type=norm_type, norm_f=norm_f)
Beispiel #12
0
def minimizebnd(fun, x0, args=(), method = 'nelder-mead', use_bnd = True, \
                bounds = (None,None) , options = None, \
                x0_vsize = None, x0_keys = None, **kwargs):
    """
    Minimization function that allows for bounds on any type of method in 
    SciPy's minimize function by transforming the parameters values 
    | (see Matlab's fminsearchbnd). 
    | Starting values, and lower and upper bounds can also be provided as a dict.
    
    Args:
        :x0: 
            | parameter starting values
            | If x0_keys is None then :x0: is vector else, :x0: is dict and
            | x0_size should be provided with length/size of values for each of 
              the keys in :x0: to convert it to a vector.                
        :use_bnd:
            | True, optional
            | False: omits bounds and defaults to regular minimize function.
        :bounds:
            | (lower, upper), optional
            | Tuple of lists or dicts (x0_keys is None) of lower and upper bounds 
              for each of the parameters values.
        :kwargs: 
            | allows input for other type of arguments (e.g. in OutputFcn)
         
    Note:
        For other input arguments, see ?scipy.minimize()
         
    Returns:
        :res: 
            | dict with minimize() output. 
            | Additionally, function value, fval, of solution is also in :res:,
              as well as a vector or dict (if x0 was dict) 
              with final solutions (res['x'])
        
        
    """
    # Convert dict to vec:
    if isinstance(x0, dict):
        x0 = vec_to_dict(dic=x0, vsize=x0_vsize, keys=x0_keys)

    if use_bnd == False:
        res = minimize(fun, x0, args=args, options=options, **kwargs)
        res['fval'] = fun(res['x'], *args)
        if x0_keys is None:
            res['x_final'] = res['x']
        else:
            res['x_final'] = vec_to_dict(vec=res['x'],
                                         vsize=x0_vsize,
                                         keys=x0_keys)
        return res
    else:

        LB, UB = bounds

        # Convert dict to vec:
        if isinstance(LB, dict):
            LB = vec_to_dict(dic=LB, vsize=x0_vsize, keys=x0_keys)
        if isinstance(LB, dict):
            UB = vec_to_dict(dic=UB, vsize=x0_vsize, keys=x0_keys)

        #size checks
        xsize = x0.shape
        x0 = x0.flatten()
        n = x0.shape[0]

        if LB is None:
            LB = -np.inf * np.ones(n)
        else:
            LB = LB.flatten()

        if UB is None:
            UB = np.inf * np.ones(n)
        else:
            UB = UB.flatten()

        if (n != LB.shape[0]) | (n != UB.shape[0]):
            raise Exception(
                'minimizebnd(): x0 is incompatible in size with either LB or UB.'
            )

        #set default options if necessary
        if options is None:
            options = {}

        # stuff into a struct to pass around
        params = {}
        params['args'] = args
        params['LB'] = LB
        params['UB'] = UB
        params['fun'] = fun
        params['n'] = n
        params['OutputFcn'] = None

        #    % 0 --> unconstrained variable
        #    % 1 --> lower bound only
        #    % 2 --> upper bound only
        #    % 3 --> dual finite bounds
        #    % 4 --> fixed variable
        params['BoundClass'] = np.zeros(n)

        for i in np.arange(n):
            k = np.isfinite(LB[i]) + 2 * np.isfinite(UB[i])
            params['BoundClass'][i] = k
            if (k == 3) & (LB[i] == UB[i]):
                params['BoundClass'][i] = 4

        # transform starting values into their unconstrained
        # surrogates. Check for infeasible starting guesses.
        x0u = x0
        k = 0
        for i in np.arange(n):

            if params['BoundClass'][i] == 1:
                # lower bound only
                if x0[i] <= LB[i]:
                    # infeasible starting value. Use bound.
                    x0u[k] = 0
                else:
                    x0u[k] = np.sqrt(x0[i] - LB[i])

            elif params['BoundClass'][i] == 2:
                # upper bound only
                if x0[i] >= UB[i]:
                    # infeasible starting value. use bound.
                    x0u[k] = 0
                else:
                    x0u[k] = sqrt(UB[i] - x0[i])

            elif params['BoundClass'][i] == 2:
                # lower and upper bounds
                if x0[i] <= LB[i]:
                    # infeasible starting value
                    x0u[k] = -np.pi / 2
                elif x0[i] >= UB[i]:
                    # infeasible starting value
                    x0u[k] = np.pi / 2
                else:
                    x0u[k] = 2 * (x0[i] - LB[i]) / (UB[i] - LB[i]) - 1
                    # shift by 2*pi to avoid problems at zero in fminsearch
                    #otherwise, the initial simplex is vanishingly small
                    x0u[k] = 2 * np.pi + np.asin(
                        np.hstack((-1, np.hstack((1, x0u[k]).min()))).max())

            elif params['BoundClass'][i] == 0:
                # unconstrained variable. x0u(i) is set.
                x0u[k] = x0[i]

            if params['BoundClass'][i] != 4:
                # increment k
                k += 1
            else:
                # fixed variable. drop it before fminsearch sees it.
                # k is not incremented for this variable.
                pass

        # if any of the unknowns were fixed, then we need to shorten x0u now.
        if k <= n:
            x0u = x0u[:k + 1]

        # were all the variables fixed?
        if x0u.shape[0] == 0:
            # All variables were fixed. quit immediately, setting the
            # appropriate parameters, then return.

            # undo the variable transformations into the original space
            x = xtransform(x0u, params)

            # final reshape
            x = x.reshape(xsize)

            # stuff fval with the final value
            fval = params['fun'](x, *params['args'])

            # minimize was not called
            output = {'success': False}

            output['x'] = x
            output['iterations'] = 0
            output['funcount'] = 1
            output['algorithm'] = method
            output[
                'message'] = 'All variables were held fixed by the applied bounds'
            output['status'] = 0

            # return with no call at all to fminsearch
            return output

    # Check for an outputfcn. If there is any, then substitute my
    # own wrapper function.
    # Use a nested function as the OutputFcn wrapper
    def outfun_wrapper(x, **kwargs):
        # we need to transform x first
        xtrans = xtransform(x, params)

        # then call the user supplied OutputFcn
        stop = params['OutputFcn'](xtrans, **kwargs)

        return stop

    if 'OutputFcn' in options:
        if options['OutputFcn'] is not None:
            params['OutputFcn'] = options['OutputFcn']
            options['OutputFcn'] = outfun_wrapper

    # now we can call minimize, but with our own
    # intra-objective function.
    res = minimize(intrafun, x0u, args=params, method=method, options=options)
    #[xu,fval,exitflag,output] = fminsearch(@intrafun,x0u,options,params);

    # get function value:
    fval = intrafun(res['x'], params)

    # undo the variable transformations into the original space
    x = xtransform(res['x'], params)

    # final reshape
    x = x.reshape(xsize)

    res['fval'] = fval
    res['x'] = x  #overwrite x in res to unconstrained format
    if x0_keys is None:
        res['x_final'] = res['x']
    else:
        res['x_final'] = vec_to_dict(vec=res['x'],
                                     vsize=x0_vsize,
                                     keys=x0_keys)

    return res
Beispiel #13
0
def subsample_RFL_set(rfl, rflpath = '', samplefcn = 'rand', S = _CIE_ILLUMINANTS['E'], \
                      jab_ranges = None, jab_deltas = None, cieobs = _VF_CIEOBS, cspace = _VF_CSPACE, \
                      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):
    """
    Sub-samples a spectral reflectance set by pixelization of color space.
    
    Args:
        :rfl: 
            | ndarray or str
            | Array with of str referring to a set of spectral reflectance 
              functions to be subsampled.
            | If str to file: file must contain data as columns, with first 
              column the wavelengths.
        :rflpath:
            | '' or str, optional
            | Path to folder with rfl-set specified in a str :rfl: filename.
        :samplefcn:
            | 'rand' or 'mean', optional
            |   -'rand': selects a random sample from the samples within each pixel
            |   -'mean': returns the mean spectral reflectance in each pixel.
        :S: 
            | _CIE_ILLUMINANTS['E'], optional
            | Illuminant used to calculate the color coordinates of the spectral 
              reflectance samples.
        :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.
        :cspace:
            | _VF_CSPACE or dict, optional
            | Specifies color space. See _VF_CSPACE_EXAMPLE for example structure.
        :cieobs:
            | _VF_CIEOBS or str, optional
            | Specifies CMF set used to calculate color coordinates.
        :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:
            | rflsampled, jabp
            | ndarrays with resp. the subsampled set of spectral reflectance 
              functions and the pixel coordinate centers.
    """
    # Testing effects of sample set, pixel size and gamut size:
    if type(rfl) == str:
        rfl = pd.read_csv(os.path.join(rflpath,rfl),header = None).get_values().T
  
    # Calculate Jab coordinates of samples:
    xyz,xyzw = spd_to_xyz(S, cieobs = cieobs, rfl = rfl.copy(), out = 2)
    cspace_pars = cspace.copy()
    cspace_pars.pop('type')
    cspace_pars['xyzw'] = xyzw
    jab = colortf(xyz,tf = cspace['type'],fwtf = cspace_pars)

    # Generate grid and get samples in each grid:
    gridp,idxp, jabp, pixelsamplenrs, pixelIDs = get_pixel_coordinates(jab, jab_ranges = jab_ranges, jab_deltas = jab_deltas, limit_grid_radius = limit_grid_radius)

    # Get rfls from set using sampling function (mean or rand):
    W = rfl[:1]
    R = rfl[1:]
    rflsampled = np.nan*np.ones((len(idxp),R.shape[1]))
    for i in range(len(idxp)):
        if samplefcn == 'mean':
            rfl_i = np.nanmean(rfl[pixelsamplenrs[i],:],axis = 0)
        else:
            samplenr_i = np.random.randint(len(pixelsamplenrs[i]))
            rfl_i = rfl[pixelsamplenrs[i][samplenr_i],:]
        rflsampled[i,:] = rfl_i        
    rflsampled = np.vstack((W,rflsampled))
    return rflsampled, jabp
Beispiel #14
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
Beispiel #15
0
               "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>`_
"""
from luxpy import np, _CMF, _CIE_ILLUMINANTS, _MUNSELL, np2d, spd_to_xyz, asplit, ajoin, cie_interp, getwlr, _WL3, np2dT
from luxpy.color.cam.colorappearancemodels import hue_angle, hue_quadrature, naka_rushton

_CAM18SL_WL3 = [390, 830, 1]

_CAM18SL_AXES = {
    'qabW_cam18sl': ["Q (cam18sl)", "aW (cam18sl)", "bW (cam18sl)"]
}

_CAM18SL_UNIQUE_HUE_DATA = {
    'hues': 'red yellow green blue red'.split(),
    'i': np.arange(5.0),
    'hi': [20.14, 90.0, 164.25, 237.53, 380.14],
    'ei': [0.8, 0.7, 1.0, 1.2, 0.8],
    'Hi': [0.0, 100.0, 200.0, 300.0, 400.0]
}

_CAM18SL_NAKA_RUSHTON_PARAMETERS = {
    'n': 0.58,
    'sig': lambda bg: 291.20 + 71.8 * bg**0.78,
    'scaling': 1,
    'noise': 0
}

_CAM18SL_PARAMETERS = {
    'k': [676.7, 794.0, 1461.5],
    'nakarushton': _CAM18SL_NAKA_RUSHTON_PARAMETERS,
Beispiel #16
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.nan*np.ones((gridp.shape[0],3))
    Jabt_avg = Jabr_avg.copy()
    for i in np.arange(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.ones((uabs.shape[0],2))*np.nan
    vectorshift_ab = np.ones((vectorshift.shape[0],2))*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
Beispiel #17
0
def read_spectral_radiance(dvc, wlstart = 360, wlend = 830, wlstep = 1, out = "spd,Errors", Errors = {}, verbosity = _VERBOSITY):
    """
    Read measured spectral radiance (W/m².sr.nm) from device.
    
    Args:
        :dvc:
            | Device handle (of class ctypes).
        :wlstart:
            | 360 or Int, optional
            | Start wavelength in nm. (min = 350 nm)
        :wlend:
            | 830 or Int, optional
            | Start wavelength in nm. (max = 1000 nm)
        :out:
            | "status,Errors", optional
            | Requested return.
        :Errors:
            | Dict with error messages.
        :verbosity:
            | 1, optional
            | 0: no printed error message output.
    
    Returns:
        :spd:
            | ndarray with wavelengths (1st row) and spectral radiance (2nd row; nan's if error).
        :Errors:
            | Dict with error messages.

    """
    out = out.replace(' ','')
    
    # Get wavelength range:
    wls = np.arange(np.int(wlstart), np.int(wlend)+np.int(wlstep), np.int(wlstep), dtype=np.float32)
    
    # Initialize spd filled with nan's:
    spd = np.vstack((wls, np.nan*np.ones(wls.shape)))
    
#    try:
    Errors["SpecRadEx"] = None
    
    # Convert measurement parameters to ctypes:
    dwBeg = DWORD(np.int(wlstart)) # wavelength start in nm
    dwEnd = DWORD(np.int(wlend)) # wavelength end in nm
    
    # create buffer for spectral radiance data:
    fSprad = (FLOAT * wls.shape[0])() 
    
    # get pointer to start of spectral radiance 
    dwError = jtre.JETI_SpecRadEx(dvc, dwBeg, dwEnd, ctypes.byref(fSprad)) 
    Errors["SpecRadEx"] = dwError
    if (dwError != 0):
        if (verbosity == 1):
            print("Could not read spectral radiance data from device. Error code = {}".format(dwError))
    else:
        # Read spectral radiance from buffer:
        Sprad= np.frombuffer(fSprad, np.float32)
            
        # Overwrite 2nd row of spd array with measured spectral radiance values:
        spd[1,:] = Sprad  
#    except:
#        Errors["SpecRadEx"] = "read_spectral_radiance() fails."
#    finally:
    # Generate requested return:
    if out == "spd,Errors":
        return spd, Errors
    elif out == "spd":
        return spd
    elif out == "Errors":
        return Errors
    else:
        raise Exception("Requested output error.")
Beispiel #18
0
def hue_quadrature(h, unique_hue_data=None):
    """
    Get hue quadrature H from hue h.
    
    Args:
        :h: 
            | float or ndarray [(N,) or (N,1)] with hue data in degrees (!).
        :unique_hue data:
            | None or dict, optional
            |   - None: defaults to:
            |         {'hues': 'red yellow green blue red'.split(), 
            |        'i': np.arange(5.0), 
            |        'hi':[20.14, 90.0, 164.25,237.53,380.14],
            |        'ei':[0.8,0.7,1.0,1.2,0.8],
            |        'Hi':[0.0,100.0,200.0,300.0,400.0]}
            |   - dict: user specified unique hue data  
            |           (same structure as above)
    
    Returns:
        :H: 
            | ndarray of Hue quadrature value(s).
    """
    if unique_hue_data is None:
        unique_hue_data = {
            'hues': 'red yellow green blue red'.split(),
            'i': np.arange(5.0),
            'hi': [20.14, 90.0, 164.25, 237.53, 380.14],
            'ei': [0.8, 0.7, 1.0, 1.2, 0.8],
            'Hi': [0.0, 100.0, 200.0, 300.0, 400.0]
        }

    changed_number_to_array = False
    if isinstance(h, float) | isinstance(h, int):
        h = np.atleast_1d(h)
        changed_number_to_array = True

    squeezed = False
    if h.ndim > 1:
        if (h.shape[0] == 1):
            h = np.squeeze(h, axis=0)
            squeezed = True

    hi = unique_hue_data['hi']
    Hi = unique_hue_data['Hi']
    ei = unique_hue_data['ei']
    h[h < hi[0]] += 360.0
    h_tmp = np.atleast_2d(h)
    if h_tmp.shape[0] == 1:
        h_tmp = h_tmp.T
    h_hi = np.repeat(h_tmp, repeats=len(hi), axis=1)
    hi_h = np.repeat(np.atleast_2d(hi), repeats=h.shape[0], axis=0)
    d = (h_hi - hi_h)
    d[d < 0] = 1000.0
    p = d.argmin(axis=1)
    p[p == (len(hi) - 1)] = 0  # make sure last unique hue data is not selected
    H = np.array([
        Hi[pi] + (100.0 * (h[i] - hi[pi]) / ei[pi]) /
        ((h[i] - hi[pi]) / ei[pi] + (hi[pi + 1] - h[i]) / ei[pi + 1])
        for (i, pi) in enumerate(p)
    ])
    if changed_number_to_array:
        H = H[0]
    if squeezed:
        H = np.expand_dims(H, axis=0)
    return H
def VF_colorshift_model(S, cri_type = _VF_CRI_DEFAULT, model_type = _VF_MODEL_TYPE, \
                        cspace = _VF_CSPACE, sampleset = None, pool = False, \
                        pcolorshift = {'href': np.arange(np.pi/10,2*np.pi,2*np.pi/10),'Cref' : _VF_MAXR, 'sig' : _VF_SIG}, \
                        vfcolor = 'k',verbosity = 0):
    """
    Applies full vector field model calculations to spectral data.
    
    Args:
        :S: 
            | nump.ndarray with spectral data.
        :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.
        :modeltype:
            | _VF_MODEL_TYPE or 'M6' or 'M5', optional
            | Specifies degree 5 or degree 6 polynomial model in ab-coordinates.
        :cspace:
            | _VF_CSPACE or dict, optional
            | Specifies color space. See _VF_CSPACE_EXAMPLE for example structure.
        :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: 
            | list[dict] (each list element refers to a different test SPD)
            | with the following keys:
            |   - 'Source': dict with ndarrays of the S, cct and duv of source spd.
            |   - 'metrics': dict with ndarrays for:
            |         * Rf (color fidelity: base + metameric shift)
            |         * Rt (metameric uncertainty index) 
            |         * Rfi (specific color fidelity indices)
            |         * Rti (specific metameric uncertainty indices)
            |         * cri_type (str with cri_type)
            |   - 'Jab': dict with with ndarrays for Jabt, Jabr, DEi
            |   - 'dC/C_dH_x_sig' : 
            |           np.vstack((dCoverC_x,dCoverC_x_sig,dH_x,dH_x_sig)).T
            |           See get_poly_model() for more info.
            |   - 'fielddata': dict with dicts containing data on the calculated 
            |      vector-field and circle-fields: 
            |        * 'vectorfield' : {'axt': vfaxt, 'bxt' : vfbxt, 
            |                           'axr' : vfaxr, 'bxr' : vfbxr},
            |        * 'circlefield' : {'axt': cfaxt, 'bxt' : cfbxt, 
            |                           'axr' : cfaxr, 'bxr' : cfbxr}},
            |   - 'modeldata' : dict with model info:
            |                {'pmodel': pmodel, 
            |                'pcolorshift' : pcolorshift, 
            |                  'dab_model' : dab_model, 
            |                  'dab_res' : dab_res,
            |                  'dab_std' : dab_std,
            |                  'modeltype' : modeltype, 
            |                  'fmodel' : poly_model,
            |                  'Jabtm' : Jabtm, 
            |                  'Jabrm' : Jabrm, 
            |                  'DEim' : DEim},
            |   - 'vshifts' :dict with various vector shifts:
            |        * 'Jabshiftvector_r_to_t' : ndarray with difference vectors
            |                                    between jabt and jabr.
            |        * 'vshift_ab_s' : vshift_ab_s: ab-shift vectors of samples 
            |        * 'vshift_ab_s_vf' : vshift_ab_s_vf: ab-shift vectors of 
            |                             VF model predictions of samples.
            |        * 'vshift_ab_vf' : vshift_ab_vf: ab-shift vectors of VF 
            |                            model predictions of vector field grid.
    """
    
    if type(cri_type) == str:
        cri_type_str = cri_type
    else:
        cri_type_str = None
    
    # Calculate Rf, Rfi and Jabr, Jabt:
    Rf, Rfi, Jabt, Jabr,cct,duv,cri_type  = spd_to_cri(S, cri_type= cri_type,out='Rf,Rfi,jabt,jabr,cct,duv,cri_type', sampleset=sampleset)
    
    # In case of multiple source SPDs, pool:
    if (len(Jabr.shape) == 3) & (Jabr.shape[1]>1) & (pool == True):
        #Nsamples = Jabr.shape[0]
        Jabr = np.transpose(Jabr,(1,0,2)) # set lamps on first dimension
        Jabt = np.transpose(Jabt,(1,0,2))
        Jabr = Jabr.reshape(Jabr.shape[0]*Jabr.shape[1],3) # put all lamp data one after the other
        Jabt = Jabt.reshape(Jabt.shape[0]*Jabt.shape[1],3)
        Jabt = Jabt[:,None,:] # add dim = 1
        Jabr = Jabr[:,None,:]
    

    out = [{} for _ in range(Jabr.shape[1])] #initialize empty list of dicts
    if pool == False:
        N = Jabr.shape[1]
    else:
        N = 1
    for i in range(N):
        
        Jabr_i = Jabr[:,i,:].copy()
        Jabr_i = Jabr_i[:,None,:]
        Jabt_i = Jabt[:,i,:].copy()
        Jabt_i = Jabt_i[:,None,:]

        DEi = np.sqrt((Jabr_i[...,0] - Jabt_i[...,0])**2 + (Jabr_i[...,1] - Jabt_i[...,1])**2 + (Jabr_i[...,2] - Jabt_i[...,2])**2)

        # Determine polynomial model:
        poly_model, pmodel, dab_model, dab_res, dCHoverC_res, dab_std, dCHoverC_std = get_poly_model(Jabt_i, Jabr_i, modeltype = _VF_MODEL_TYPE)
        
        # Apply model at fixed hues:
        href = pcolorshift['href']
        Cref = pcolorshift['Cref']
        sig = pcolorshift['sig']
        dCoverC_x, dCoverC_x_sig, dH_x, dH_x_sig = apply_poly_model_at_hue_x(poly_model, pmodel, dCHoverC_res, hx = href, Cxr = Cref, sig = sig)
        
        # Calculate deshifted a,b values on original samples:
        Jt = Jabt_i[...,0].copy()
        at = Jabt_i[...,1].copy()
        bt = Jabt_i[...,2].copy()
        Jr = Jabr_i[...,0].copy()
        ar = Jabr_i[...,1].copy()
        br = Jabr_i[...,2].copy()
        ar = ar + dab_model[:,0:1] # deshift reference to model prediction
        br = br + dab_model[:,1:2] # deshift reference to model prediction
        
        Jabtm = np.hstack((Jt,at,bt))
        Jabrm = np.hstack((Jr,ar,br))
        
        # calculate color differences between test and deshifted ref:
#        DEim = np.sqrt((Jr - Jt)**2 + (at - ar)**2 + (bt - br)**2)
        DEim = np.sqrt(0*(Jr - Jt)**2 + (at - ar)**2 + (bt - br)**2) # J is not used

        # Apply scaling function to convert DEim to Rti:
        scale_factor = cri_type['scale']['cfactor']
        scale_fcn = cri_type['scale']['fcn']
        avg = cri_type['avg']  
        Rfi_deshifted = scale_fcn(DEim,scale_factor)
        Rf_deshifted = scale_fcn(avg(DEim,axis = 0),scale_factor)
        
        rms = lambda x: np.sqrt(np.sum(x**2,axis=0)/x.shape[0])
        Rf_deshifted_rms = scale_fcn(rms(DEim),scale_factor)
    
        # Generate vector field:
        vfaxt,vfbxt,vfaxr,vfbxr = generate_vector_field(poly_model, pmodel,axr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), bxr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), limit_grid_radius = _VF_MAXR,color = 0)
        vfaxt,vfbxt,vfaxr,vfbxr = generate_vector_field(poly_model, pmodel,axr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), bxr = np.arange(-_VF_MAXR,_VF_MAXR+_VF_DELTAR,_VF_DELTAR), limit_grid_radius = _VF_MAXR,color = 0)

        # Calculate ab-shift vectors of samples and VF model predictions:
        vshift_ab_s = calculate_shiftvectors(Jabt_i, Jabr_i, average = False, vtype = 'ab')[:,0,0:3]
        vshift_ab_s_vf = calculate_shiftvectors(Jabtm,Jabrm, average = False, vtype = 'ab')

        # Calculate ab-shift vectors using vector field model:
        Jabt_vf = np.hstack((np.zeros((vfaxt.shape[0],1)), vfaxt, vfbxt))   
        Jabr_vf = np.hstack((np.zeros((vfaxr.shape[0],1)), vfaxr, vfbxr))   
        vshift_ab_vf = calculate_shiftvectors(Jabt_vf,Jabr_vf, average = False, vtype = 'ab')

        # Generate circle field:
        x,y = plotcircle(radii = np.arange(0,_VF_MAXR+_VF_DELTAR,10), angles = np.arange(0,359,1), out = 'x,y')
        cfaxt,cfbxt,cfaxr,cfbxr = generate_vector_field(poly_model, pmodel,make_grid = False,axr = x[:,None], bxr = y[:,None], limit_grid_radius = _VF_MAXR,color = 0)

        out[i] = {'Source' : {'S' : S, 'cct' : cct[i] , 'duv': duv[i]},
               'metrics' : {'Rf':Rf[:,i], 'Rt': Rf_deshifted, 'Rt_rms' : Rf_deshifted_rms, 'Rfi':Rfi[:,i], 'Rti': Rfi_deshifted, 'cri_type' : cri_type_str},
               'Jab' : {'Jabt' : Jabt_i, 'Jabr' : Jabr_i, 'DEi' : DEi},
               'dC/C_dH_x_sig' : np.vstack((dCoverC_x,dCoverC_x_sig,dH_x,dH_x_sig)).T,
               'fielddata': {'vectorfield' : {'axt': vfaxt, 'bxt' : vfbxt, 'axr' : vfaxr, 'bxr' : vfbxr},
                             'circlefield' : {'axt': cfaxt, 'bxt' : cfbxt, 'axr' : cfaxr, 'bxr' : cfbxr}},
               'modeldata' : {'pmodel': pmodel, 'pcolorshift' : pcolorshift, 
                              'dab_model' : dab_model, 'dab_res' : dab_res,'dab_std' : dab_std,
                              'model_type' : model_type, 'fmodel' : poly_model,
                              'Jabtm' : Jabtm, 'Jabrm' : Jabrm, 'DEim' : DEim},
               'vshifts' : {'Jabshiftvector_r_to_t' : np.hstack((Jt-Jr,at-ar,bt-br)),
                            'vshift_ab_s' : vshift_ab_s,
                            'vshift_ab_s_vf' : vshift_ab_s_vf,
                            'vshift_ab_vf' : vshift_ab_vf}}
     
    return out
Beispiel #20
0
def get_pixel_coordinates(jab, jab_ranges = None, jab_deltas = None, limit_grid_radius = 0):
    """
    Get pixel coordinates corresponding to array of jab color coordinates.
    
    Args:
        :jab: 
            | ndarray of color coordinates
        :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:
            | gridp, idxp, jabp, samplenrs, samplesIDs
            |   - :gridp: ndarray with coordinates of all pixel centers.
            |   - :idxp: list[int] with pixel index for each non-empty pixel
            |   - :jabp: ndarray with center color coordinates of non-empty pixels
            |   - :samplenrs: list[list[int]] with sample numbers belong to each 
            |                 non-empty pixel
            |   - :sampleIDs: summarizing list, 
            |                 with column order: 'idxp, jabp, samplenrs'
    """
    if jab_deltas is None:
        jab_deltas = np.array([_VF_DELTAR,_VF_DELTAR,_VF_DELTAR])
    if jab_ranges is None:
        jab_ranges = np.vstack(([0,100,jab_deltas[0]],[-_VF_MAXR,_VF_MAXR+jab_deltas[1],jab_deltas[1]], [-_VF_MAXR,_VF_MAXR+jab_deltas[2],jab_deltas[2]]))
    
    # Get pixel grid:
    gridp = generate_grid(jab_ranges = jab_ranges, limit_grid_radius = limit_grid_radius) 

    # determine pixel coordinates of each sample in jab:
    samplesIDs = []
    for idx in np.arange(gridp.shape[0]):
        
        # get pixel coordinates:
        jp = gridp[idx,0]
        ap = gridp[idx,1]
        bp = gridp[idx,2]
        Cp = np.sqrt(ap**2+bp**2)
                      
        if type(jab_deltas) == np.ndarray:
            sampleID = np.where(((np.abs(jab[...,0]-jp) <= jab_deltas[0]/2) & (np.abs(jab[...,1]-ap) <= jab_deltas[1]/2) & (np.abs(jab[...,2]-bp) <= jab_deltas[2]/2)))
        else:
            sampleID = np.where((np.sqrt((jab[...,0]-jp)**2 + (jab[...,1]-ap)**2 + (jab[...,2]-bp)**2) <= jab_deltas/2))

        if (sampleID[0].shape[0] > 0):
            samplesIDs.append(np.hstack((idx,np.array([jp,ap,bp]),sampleID[0])))
       
    idxp = [np.int(samplesIDs[i][0]) for i in range(len(samplesIDs))]
    jabp = np.vstack([samplesIDs[i][1:4] for i in range(len(samplesIDs))])
    samplenrs = [np.array(samplesIDs[i][4:],dtype = int).tolist() for i in range(len(samplesIDs))]
    
    return gridp, idxp,jabp,samplenrs, samplesIDs
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
Beispiel #22
0
def plot_hue_bins(hbins = 16, start_hue = 0.0, scalef = 100, \
        plot_axis_labels = False, bin_labels = '#', plot_edge_lines = True, \
        plot_center_lines = False, plot_bin_colors = True, \
        axtype = 'polar', ax = None, force_CVG_layout = False):
    """
    Makes basis plot for Color Vector Graphic (CVG).
    
    Args:
        :hbins:
            | 16 or ndarray with sorted hue bin centers (°), optional
        :start_hue:
            | 0.0, optional
        :scalef:
            | 100, optional
            | Scale factor for graphic.
        :plot_axis_labels:
            | False, optional
            | Turns axis ticks on/off (True/False).
        :bin_labels:
            | None or list[str] or '#', optional
            | Plots labels at the bin center hues.
            |   - None: don't plot.
            |   - list[str]: list with str for each bin. 
            |                (len(:bin_labels:) = :nhbins:)
            |   - '#': plots number.
        :plot_edge_lines:
            | True or False, optional
            | Plot grey bin edge lines with '--'.
        :plot_center_lines:
            | False or True, optional
            | Plot colored lines at 'center' of hue bin.
        :plot_bin_colors:
            | True, optional
            | Colorize hue bins.
        :axtype: 
            | 'polar' or 'cart', optional
            | Make polar or Cartesian plot.
        :ax: 
            | None or 'new' or 'same', optional
            |   - None or 'new' creates new plot
            |   - 'same': continue plot on same axes.
            |   - axes handle: plot on specified axes.
        :force_CVG_layout:
            | False or True, optional
            | True: Force plot of basis of CVG on first encounter.
            
    Returns:
        :returns: 
            | gcf(), gca(), list with rgb colors for hue bins (for use in 
              other plotting fcns)
        
    """

    # Setup hbincenters and hsv_hues:
    if isinstance(hbins, float) | isinstance(hbins, int):
        nhbins = hbins
        dhbins = 360 / (nhbins)  # hue bin width
        hbincenters = np.arange(start_hue + dhbins / 2, 360, dhbins)
        hbincenters = np.sort(hbincenters)

    else:
        hbincenters = hbins
        idx = np.argsort(hbincenters)
        if isinstance(bin_labels, list) | isinstance(bin_labels, np.ndarray):
            bin_labels = bin_labels[idx]
        hbincenters = hbincenters[idx]
        nhbins = hbincenters.shape[0]
    hbincenters = hbincenters * np.pi / 180

    # Setup hbin labels:
    if bin_labels is '#':
        bin_labels = ['#{:1.0f}'.format(i + 1) for i in range(nhbins)]

    # initializing the figure
    cmap = None
    if (ax == None) or (ax == 'new'):
        fig = plt.figure()
        newfig = True
    else:
        newfig = False
    rect = [0.1, 0.1, 0.8,
            0.8]  # setting the axis limits in [left, bottom, width, height]

    if axtype == 'polar':
        # the polar axis:
        if newfig == True:
            ax = fig.add_axes(rect, polar=True, frameon=False)
    else:
        #cartesian axis:
        if newfig == True:
            ax = fig.add_axes(rect)

    if (newfig == True) | (force_CVG_layout == True):

        # Calculate hue-bin boundaries:
        r = np.vstack(
            (np.zeros(hbincenters.shape), scalef * np.ones(hbincenters.shape)))
        theta = np.vstack((np.zeros(hbincenters.shape), hbincenters))
        #t = hbincenters.copy()
        dU = np.roll(hbincenters.copy(), -1)
        dL = np.roll(hbincenters.copy(), 1)
        dtU = dU - hbincenters
        dtL = hbincenters - dL
        dtU[dtU < 0] = dtU[dtU < 0] + 2 * np.pi
        dtL[dtL < 0] = dtL[dtL < 0] + 2 * np.pi
        dL = hbincenters - dtL / 2
        dU = hbincenters + dtU / 2
        dt = (dU - dL)
        dM = dL + dt / 2

        # Setup color for plotting hue bins:
        hsv_hues = hbincenters - 30 * np.pi / 180
        hsv_hues = hsv_hues / hsv_hues.max()

        edges = np.vstack(
            (np.zeros(hbincenters.shape), dL))  # setup hue bin edges array

        if axtype == 'cart':
            if plot_center_lines == True:
                hx = r * np.cos(theta)
                hy = r * np.sin(theta)
            if bin_labels is not None:
                hxv = np.vstack((np.zeros(hbincenters.shape),
                                 1.3 * scalef * np.cos(hbincenters)))
                hyv = np.vstack((np.zeros(hbincenters.shape),
                                 1.3 * scalef * np.sin(hbincenters)))
            if plot_edge_lines == True:
                hxe = np.vstack(
                    (np.zeros(hbincenters.shape), 1.2 * scalef * np.cos(dL)))
                hye = np.vstack(
                    (np.zeros(hbincenters.shape), 1.2 * scalef * np.sin(dL)))

        # Plot hue-bins:
        for i in range(nhbins):

            # Create color from hue angle:
            c = np.abs(np.array(colorsys.hsv_to_rgb(hsv_hues[i], 0.84, 0.9)))
            #c = [abs(c[0]),abs(c[1]),abs(c[2])] # ensure all positive elements
            if i == 0:
                cmap = [c]
            else:
                cmap.append(c)

            if axtype == 'polar':
                if plot_edge_lines == True:
                    ax.plot(edges[:, i],
                            r[:, i] * 1.2,
                            color='grey',
                            marker='None',
                            linestyle=':',
                            linewidth=3,
                            markersize=2)
                if plot_center_lines == True:
                    if np.mod(i, 2) == 1:
                        ax.plot(theta[:, i],
                                r[:, i],
                                color=c,
                                marker=None,
                                linestyle='--',
                                linewidth=2)
                    else:
                        ax.plot(theta[:, i],
                                r[:, i],
                                color=c,
                                marker='o',
                                linestyle='-',
                                linewidth=3,
                                markersize=10)
                if plot_bin_colors == True:
                    bar = ax.bar(dM[i],
                                 r[1, i],
                                 width=dt[i],
                                 color=c,
                                 alpha=0.15)
                if bin_labels is not None:
                    ax.text(hbincenters[i],
                            1.3 * scalef,
                            bin_labels[i],
                            fontsize=12,
                            horizontalalignment='center',
                            verticalalignment='center',
                            color=np.array([1, 1, 1]) * 0.3)
                if plot_axis_labels == False:
                    ax.set_xticklabels([])
                    ax.set_yticklabels([])
            else:
                if plot_edge_lines == True:
                    ax.plot(hxe[:, i],
                            hye[:, i],
                            color='grey',
                            marker='None',
                            linestyle=':',
                            linewidth=3,
                            markersize=2)

                if plot_center_lines == True:
                    if np.mod(i, 2) == 1:
                        ax.plot(hx[:, i],
                                hy[:, i],
                                color=c,
                                marker=None,
                                linestyle='--',
                                linewidth=2)
                    else:
                        ax.plot(hx[:, i],
                                hy[:, i],
                                color=c,
                                marker='o',
                                linestyle='-',
                                linewidth=3,
                                markersize=10)
                if bin_labels is not None:
                    ax.text(hxv[1, i],
                            hyv[1, i],
                            bin_labels[i],
                            fontsize=12,
                            horizontalalignment='center',
                            verticalalignment='center',
                            color=np.array([1, 1, 1]) * 0.3)
                ax.axis(1.1 * np.array(
                    [hxv.min(), hxv.max(),
                     hyv.min(), hyv.max()]))
                if plot_axis_labels == False:
                    ax.set_xticklabels([])
                    ax.set_yticklabels([])
                else:
                    plt.xlabel("a'")
                    plt.ylabel("b'")

        plt.plot(0, 0, color='k', marker='o', linestyle=None)

    return plt.gcf(), plt.gca(), cmap
Beispiel #23
0
def get_spd(dvc = 0, Tint = 0.0, autoTint_max = _TINT_MAX, Nscans = 1, wlstep = 1, 
            wlstart = 360, wlend = 830, 
            twait = _TWAIT_STATUS, out = "spd", close_device = True, 
            laser_on = 0, laser_intensity = 1000, verbosity = _VERBOSITY):
    """
    Measure spectral radiance (W/nm.sr.m²).
    
    Args:
        :dvc:
            | 0 or Int or ctypes.wintypes.LP_c_ulong, optional
            | Number of the spectrometer device to load (0 = 1st) or handle (ctypes) to pre_initialized device.
        :Tint:
            | 0 or Float, optional
            | Integration time in seconds. (if 0: find best integration time, but < autoTint_max).
        :autoTint_max:
            | Limit Tint to this value when Tint = 0.
        :Nscans:
            | 1 or Int, optional
            | Number of scans to average.
        :wlstep: 
            | 1 or Int, optional
            | Wavelength step size in nm.
        :wlstart:
            | 360 or Int, optional
            | Start wavelength in nm. (min = 350 nm)
        :wlend:
            | 830 or Int, optional
            | Start wavelength in nm. (max = 1000 nm)
        :twait:
            | 0.1 or Float, optional
            | Time in seconds to wait before checking status of device. 
            | (If 0: wait :Tint: seconds, unless :Tint: == 0, then wait _TWAIT_STATUS seconds)
        :out:
            | "spd" [",dvc, Errors"], optional
            | Requested return. If "spd" in out.split(","):do spectral measurement, else: initialize dvc handle [and turn laser ON or OFF].
        :close_device:
            | True or False, optional
            | Close device at the end of the measurement.
            | If 'dvc' not in out.split(','): always close!!!
        :laser_on:
            | 0: OFF, >0: ON -> 1: PWM 7Hz, 2: PWM 28 Hz, 3: 255 Hz, optional
            | True (>0): turn laser on to select measurement area; False (0): turn off. 
            | (Can only be ON when "spd" is not in out.split(",") | if Tint is None)
        :laser_intensity: 
            | 1000.0, optional
            | Laser intensity in ‰ (pro-mille).
        :verbosity:
            | 1, optional
            | 0: no printed error message output.
    Returns:
        :returns: 
            | spd [,dvc, Errors] (as specified in :out:)
            | - "spd": ndarray with wavelengths (1st row) and spectral radiance (2nd row).
            | - "dvc": ctypes handle to device (if open) or nan (if closed).
            | - "Errors": dict with error message returned by device during various steps of the spectral measurement process.
    """
    # Initialize dict with errors messages for each of the different measurement steps:
    Errors = {} 
    Errors["get_spd"] = None
    out = out.replace(' ','')
    
    # Get wavelength range:
    wls = np.arange(np.int(wlstart), np.int(wlend)+np.int(wlstep), np.int(wlstep), dtype=np.float32)
    
    # Initialize spd filled with nan's:
    spd = np.vstack((wls, np.nan*np.ones(wls.shape)))

    try:
        # Initialize device :
        dvc, Errors = dvc_open(dvc = dvc, Errors = Errors, out = "dvc,Errors", verbosity = verbosity)    
        
        if (_check_dvc_open(dvc)) & (("spd" in out.split(",")) & (Tint is not None)):
            
            # Turn off laser before starting measurement:
            Errors = set_laser(dvc = dvc, laser_on = False, laser_intensity = laser_intensity, Errors = Errors, verbosity = verbosity)
                    
                            
            # Start measurement:
            Tint, Errors = start_meas(dvc, Tint = Tint, autoTint_max = autoTint_max, Nscans = Nscans, wlstep = wlstep, Errors = Errors, out = "Tint, Errors", verbosity = verbosity)
            
            # wait until measurement is finished (check intermediate status every twait seconds):
            status, Errors = wait_until_meas_is_finished(dvc, Tint = Tint, twait = twait, out = "status,Errors", Errors = Errors, verbosity = verbosity)
            
            if status == False:
                # Read measured spectral radiance from device:
                spd, Errors = read_spectral_radiance(dvc, wlstart = wlstart, wlend = wlend, wlstep = wlstep, out = "spd,Errors", Errors = Errors, verbosity = verbosity)    
            
        elif (("spd" not in out.split(",")) | (Tint is None)): # only dvc handle was requested or to turn laser ON.
            Errors = set_laser(dvc = dvc, laser_on = laser_on, laser_intensity = laser_intensity, Errors = Errors, verbosity = verbosity)
        
        # Close device:
        dvc, Errors = dvc_close(dvc, Errors = Errors, close_device = (close_device) | ('dvc' not in out.split(',')), out = "dvc,Errors", verbosity = verbosity)
    
        
        Errors["get_spd"] = int(np.sum([int(bool(x)) for x in Errors.values() if (x is not None)]) > 0)
        
    except:
        Errors["get_spd"] = "get_spd fails."
        
    finally:
        # Generate requested return:
        if out == "spd":
            return spd
        elif out == "dvc":
            return dvc
        elif out == "Errors":
            return Errors
        elif out == "spd,Errors":
            return spd, Errors
        elif out == "spd,dvc":
            return spd, dvc
        elif out == "spd,Errors,dvc":
            return spd, Errors, dvc
        elif out == "spd,dvc,Errors":
            return spd, dvc, Errors
        else:
            raise Exception("Requested output error.")
Beispiel #24
0
def plot_cri_graphics(data, cri_type = None, hbins = 16, start_hue = 0.0, scalef = 100, \
                      plot_axis_labels = False, bin_labels = None, plot_edge_lines = True, \
                      plot_center_lines = False, plot_bin_colors = True, \
                      axtype = 'polar', ax = None, force_CVG_layout = True,
                      vf_model_type = _VF_MODEL_TYPE, vf_pcolorshift = _VF_PCOLORSHIFT, vf_color = 'k', \
                      vf_bin_labels = _VF_PCOLORSHIFT['labels'], vf_plot_bin_colors = True, \
                      scale_vf_chroma_to_sample_chroma = False,\
                      plot_VF = True, plot_CF = False, plot_SF = False):
    """
    Plot graphical information on color rendition properties.
    
    Args:
        :data: 
            | ndarray with spectral data or dict with pre-computed metrics.
        :cri_type:
            | None, optional
            | If None: defaults to cri_type = 'iesrf'.
            | :hbins:, :start_hue: and :scalef: are ignored if cri_type not None 
            | and values are replaced by those in cri_type['rg_pars']
        :hbins:
            | 16 or ndarray with sorted hue bin centers (°), optional
        :start_hue: 
            | 0.0, optional
        :scalef:
            | 100, optional
            | Scale factor for graphic.
        :plot_axis_labels:
            | False, optional
            | Turns axis ticks on/off (True/False).
        :bin_labels: 
            | None or list[str] or '#', optional
            | Plots labels at the bin center hues.
            |   - None: don't plot.
            |   - list[str]: list with str for each bin. 
            |                (len(:bin_labels:) = :nhbins:)
            |   - '#': plots number.
        :plot_edge_lines:
            | True or False, optional
            | Plot grey bin edge lines with '--'.
        :plot_center_lines:
            | False or True, optional
            | Plot colored lines at 'center' of hue bin.
        :plot_bin_colors: 
            | True, optional
            | Colorize hue bins.
        :axtype: 
            | 'polar' or 'cart', optional
            | Make polar or Cartesian plot.
        :ax: 
            | None or 'new' or 'same', optional
            |   - None or 'new' creates new plot
            |   - 'same': continue plot on same axes.
            |   - axes handle: plot on specified axes.
        :force_CVG_layout: 
            | False or True, optional
            | True: Force plot of basis of CVG.
        :vf_model_type: 
            | _VF_MODEL_TYPE or 'M6' or 'M5', optional
            | Type of polynomial vector field model to use for the calculation of
              base color shift and metameric uncertainty.
        :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.
        :vf_color:
            | 'k', optional
            | For plotting the vector fields.
        :vf_plot_bin_colors: 
            | True, optional
            | Colorize hue bins of VF graph.
        :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.
        :vf_bin_labels:
            | see :bin_labels:
            | Set VF model hue-bin labels.
        :plot_CF:
            | False, optional
            | Plot circle fields.
        :plot_VF:
            | True, optional
            | Plot vector fields.
        :plot_SF:
            | True, optional
            | Plot sample shifts.   
            
    Returns:
        :returns: 
            | (data, 
            | [plt.gcf(),ax_spd, ax_CVG, ax_locC, ax_locH, ax_VF], 
            | cmap )
            | 
            | :data: dict with color rendering data
            | with keys:
            | - '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)
            |    
            | :[...]: list with handles to current figure and 5 axes.
            |
            | :cmap: list with rgb colors for hue bins 
                    (for use in other plotting fcns)
        
    """
    if not isinstance(data,dict):
        data = spd_to_ies_tm30_metrics(data, cri_type = cri_type, hbins = hbins, start_hue = start_hue, scalef = scalef, vf_model_type = vf_model_type, vf_pcolorshift = vf_pcolorshift, scale_vf_chroma_to_sample_chroma = scale_vf_chroma_to_sample_chroma)

    Rcshi, Rf, Rfchhi_vf, Rfhi, Rfhi_vf, Rfhshi_vf, Rfi,  Rg, Rhshi, Rt, Rti, SPD, bjabr, bjabt, cct, cri_type, dataVF, duv = [data[x] for x in sorted(data.keys())]
    hbins = cri_type['rg_pars']['nhbins']
    start_hue = cri_type['rg_pars']['start_hue']
    scalef = cri_type['rg_pars']['normalized_chroma_ref']
        
    #layout = np.array([[3,3,0,0],[1,0,2,2],[0,0,2,1],[2,2,1,1],[0,2,1,1],[1,2,1,1]])
    #layout = np.array([[6,6,0,0],[0,3,3,3],[3,3,3,3],[0,0,3,2],[2,2,2,2],[2,0,2,2],[4,0,2,2]])
    layout = np.array([[6,7,0,0],[0,4,3,3],[3,4,3,3],[0,0,4,2],[2,0,2,2],[4,2,2,2],[4,0,2,2],[2,2,2,2]])
    
    def create_subplot(layout,n, polar = False, frameon = True):
        ax = plt.subplot2grid(layout[0,0:2], layout[n,0:2], colspan = layout[n,2], rowspan = layout[n,3], polar = polar, frameon = frameon)
        return ax
    
    for i in range(cct.shape[0]):
        
        fig = plt.figure(figsize=(10, 6), dpi=144)
    
        # Plot CVG:
        ax_CVG = create_subplot(layout,1, polar = True, frameon = False)
        figCVG, ax, cmap = plot_ColorVectorGraphic(bjabt[...,i,:], bjabr[...,i,:], hbins = hbins, axtype = axtype, ax = ax_CVG, plot_center_lines = plot_center_lines, plot_edge_lines = plot_edge_lines,  plot_bin_colors = plot_bin_colors, scalef = scalef, force_CVG_layout = force_CVG_layout, bin_labels = '#')
        
        # Plot VF:
        ax_VF = create_subplot(layout,2, polar = True, frameon = False)
        if i == 0:
            hbin_cmap = None
    
        ax_VF, hbin_cmap = plot_VF_PX_models([dataVF[i]], dataPX = None, plot_VF = plot_VF, plot_PX = None, axtype = 'polar', ax = ax_VF, \
                           plot_circle_field = plot_CF, plot_sample_shifts = plot_SF, plot_bin_colors = vf_plot_bin_colors, \
                           plot_samples_shifts_at_pixel_center = False, jabp_sampled = None, \
                           plot_VF_colors = [vf_color], plot_PX_colors = ['r'], hbin_cmap = hbin_cmap, force_CVG_layout = True, bin_labels = vf_bin_labels)
    
        # Plot test SPD:
        ax_spd = create_subplot(layout,3)
        ax_spd.plot(SPD[0],SPD[i+1]/SPD[i+1].max(),'r-')
        ax_spd.text(730,0.9,'CCT = {:1.0f} K'.format(cct[i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3)
        ax_spd.text(730,0.8,'Duv = {:1.4f}'.format(duv[i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3)
        ax_spd.text(730,0.7,'IES Rf = {:1.0f}'.format(Rf[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3)
        ax_spd.text(730,0.6,'IES Rg = {:1.0f}'.format(Rg[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3)
        ax_spd.text(730,0.5,'Rt = {:1.0f}'.format(Rt[:,i][0]),fontsize = 9, horizontalalignment='left',verticalalignment='center',rotation = 0, color = np.array([1,1,1])*0.3)
        ax_spd.set_xlabel('Wavelength (nm)', fontsize = 9)
        ax_spd.set_ylabel('Rel. spectral intensity', fontsize = 9)
        ax_spd.set_xlim([360,830])
        
        # Plot local color fidelity, Rfhi:
        ax_Rfi = create_subplot(layout,4)
        for j in range(hbins):
            ax_Rfi.bar(range(hbins)[j],Rfhi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4)
            ax_Rfi.text(range(hbins)[j],Rfhi[j,i]*1.1, '{:1.0f}'.format(Rfhi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',color = np.array([1,1,1])*0.3)
        ax_Rfi.set_ylim([0,120])
        xticks = np.arange(hbins)
        xtickslabels = ['{:1.0f}'.format(ii+1) for ii in range(hbins)]
        ax_Rfi.set_xticks(xticks)
        ax_Rfi.set_xticklabels(xtickslabels, fontsize = 8)
        ax_Rfi.set_ylabel(r'Local color fidelity $R_{f,hi}$')
        ax_Rfi.set_xlabel('Hue bin #')
        
        # Plot local chroma shift, Rcshi:
        ax_locC = create_subplot(layout,5)
        for j in range(hbins):
            ax_locC.bar(range(hbins)[j],Rcshi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4)
            ax_locC.text(range(hbins)[j],-np.sign(Rcshi[j,i])*0.1, '{:1.0f}%'.format(100*Rcshi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',rotation = 90, color = np.array([1,1,1])*0.3)
        ylim = np.array([np.abs(Rcshi.min()),np.abs(Rcshi.min()),0.2]).max()*1.5
        ax_locC.set_ylim([-ylim,ylim])
        ax_locC.set_ylabel(r'Local chroma shift, $R_{cs,hi}$')
        ax_locC.set_xticklabels([])
        ax_locC.set_yticklabels(['{:1.2f}'.format(ii) for ii in ax_locC.set_ylim()], color = 'white')
        
        # Plot local hue shift, Rhshi:
        ax_locH = create_subplot(layout,6)
        for j in range(hbins):
            ax_locH.bar(range(hbins)[j],Rhshi[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4)
            ax_locH.text(range(hbins)[j],-np.sign(Rhshi[j,i])*0.2, '{:1.3f}'.format(Rhshi[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',rotation = 90, color = np.array([1,1,1])*0.3)
        ylim = np.array([np.abs(Rhshi.min()),np.abs(Rhshi.min()),0.2]).max()*1.5
        ax_locH.set_ylim([-ylim,ylim])
        ax_locH.set_ylabel(r'Local hue shift, $R_{hs,hi}$')
        ax_locH.set_xticklabels([])
        ax_locH.set_yticklabels(['{:1.2f}'.format(ii) for ii in ax_locH.set_ylim()], color = 'white')
              
        # Plot local color fidelity of VF, vfRfhi:
        ax_vfRfi = create_subplot(layout,7)
        for j in range(hbins):
            ax_vfRfi.bar(range(hbins)[j],Rfhi_vf[j,i], color = cmap[j], width = 1,edgecolor = 'k', alpha = 0.4)
            ax_vfRfi.text(range(hbins)[j],Rfhi_vf[j,i]*1.1, '{:1.0f}'.format(Rfhi_vf[j,i]) ,fontsize = 9,horizontalalignment='center',verticalalignment='center',color = np.array([1,1,1])*0.3)
        ax_vfRfi.set_ylim([0,120])
        xticks = np.arange(hbins)
        xtickslabels = ['{:1.0f}'.format(ii+1) for ii in range(hbins)]
        ax_vfRfi.set_xticks(xticks)
        ax_vfRfi.set_xticklabels(xtickslabels, fontsize = 8)
        ax_vfRfi.set_ylabel(r'Local VF color fidelity $vfR_{f,hi}$')
        ax_vfRfi.set_xlabel('Hue bin #')
       
        plt.tight_layout()
        
    return  data,  [plt.gcf(),ax_spd, ax_CVG, ax_locC, ax_locH, ax_VF], cmap
Beispiel #25
0
                                    | NK(x) = sign(x) * scaling * ((abs(x)**n) / ((abs(x)**n) + (sig**n))) + noise
                                       
 :cam18sl(): | calculates the output for the CAM18sl model for self-luminous related stimuli. 
             | `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>`_
"""
from luxpy import np, _CMF, _CIE_ILLUMINANTS, _MUNSELL, np2d, spd_to_xyz, asplit, ajoin,cie_interp, getwlr, _WL3, np2dT
from luxpy.color.cam.colorappearancemodels import hue_angle, hue_quadrature, naka_rushton

_CAM18SL_WL3 = [390,830,1]

_CAM18SL_AXES = {'qabW_cam18sl' : ["Q (cam18sl)", "aW (cam18sl)", "bW (cam18sl)"]} 

_CAM18SL_UNIQUE_HUE_DATA = {'hues': 'red yellow green blue red'.split(), 'i': np.arange(5.0), 'hi':[20.14, 90.0, 164.25,237.53,380.14],'ei':[0.8,0.7,1.0,1.2,0.8],'Hi':[0.0,100.0,200.0,300.0,400.0]}

_CAM18SL_NAKA_RUSHTON_PARAMETERS = {'n': 0.58, 'sig': lambda bg: 291.20 + 71.8*bg**0.78, 'scaling': 1, 'noise': 0}

_CAM18SL_PARAMETERS = {'k': [676.7, 794.0, 1461.5],
                       'nakarushton': _CAM18SL_NAKA_RUSHTON_PARAMETERS,
                       'unique_hue_data':_CAM18SL_UNIQUE_HUE_DATA, 
                       'cA':0.937 ,'cAlms':[2.0, 1.0, 1/20] ,
                       'ca': 0.63, 'calms':[1.0,-12/11,1/11],
                       'cb': 0.12, 'cblms': [1.0, 1.0,-2.0], 
                       'cM': 3260, 'cHK': [0.0024,1.09], 'cW': [1/11672,2.09], 
                       'cfov': 0.271} 
                       
#_CAM18SL_SURROUND_PARAMETERS = {'surrounds': ['dark'], 'dark' : {'c': None, 'Nc':None,'F':None,'FLL':None}}

__all__ = ['cam18sl','_CAM18SL_AXES','_CAM18SL_UNIQUE_HUE_DATA', '_CAM18SL_PARAMETERS','_CAM18SL_NAKA_RUSHTON_PARAMETERS', '_CAM18SL_SURROUND_PARAMETERS']
Beispiel #26
0
    h = np.atleast_1d((np.arctan2(y, x) * r2d))
    h[np.where(h < 0)] = h[np.where(h < 0)] + h360
    return h


jabt, jabr = out2
# jabt2,jabr2=out2
# ii=0
# ht = positive_arctan(jabt[:,ii,1],jabt[:,ii,2], htype='rad')
# ht2 = positive_arctan(jabt2[...,1], jabt2[...,2], htype = 'rad')
# ht = np.arctan2(jabt[:,ii,2],jabt[:,ii,1])
# ht2 = np.arctan2(jabt2[...,2], jabt2[...,1])

#-----------------------------------------------------------
dh = 360 / nhbins
hue_bin_edges = np.arange(start_hue, 360 + 1, dh) * np.pi / 180

# get hues of jabt, jabr:
ht = cam.hue_angle(jabt[..., 1], jabt[..., 2], htype='rad')
hr = cam.hue_angle(jabr[..., 1], jabr[..., 2], htype='rad')

# Get chroma of jabt, jabr:
Ct = ((jabt[..., 1]**2 + jabt[..., 2]**2))**0.5
Cr = ((jabr[..., 1]**2 + jabr[..., 2]**2))**0.5

# Calculate DEi between jabt, jabr:
DEi = ((jabt - jabr)**2).sum(axis=-1, keepdims=True)**0.5

# calculate hue-bin averages for jabt, jabr:
jabt_hj = np.ones((nhbins, ht.shape[1], 3)) * np.nan
jabr_hj = np.ones((nhbins, hr.shape[1], 3)) * np.nan