Example #1
0
def cik_to_v(cik, xyc=None, inverse=False):
    """
    Calculate v-format ellipse descriptor from 2x2 'covariance matrix'^-1 cik 
    
    Args:
        :cik: 
            | 'Nx2x2' (covariance matrix)^-1
        :inverse:
            | If True: input is inverse of cik.
              
            
    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 cik.ndim < 3:
        cik = cik[None, ...]

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

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

    theta = 0.5 * np.arctan2(2 * g12, (g11 - g22)) + (np.pi / 2) * (g12 < 0)
    #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))

    # ensure largest ellipse axis is first (correct angle):
    c = b > a
    a[c], b[c], theta[c] = b[c], a[c], theta[c] + np.pi / 2

    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
Example #2
0
def plot(v,
         origin=None,
         ax=None,
         color='k',
         marker='.',
         linestyle='-',
         **kwargs):
    """
    Plot a vector from origin.
    
    Args:
        :v:
            | vec3 vector.
        :origin:
            | vec3 vector with same size attributes as in :v:.
        :ax: 
            | None, optional
            | axes handle.
            | If None, create new figure with axes ax.
        :color:
            | 'k', optional
            | color specifier.
        :marker:
            | '.', optional
            | marker specifier.
        :linestyle:
            | '-', optional
            | linestyle specifier
        :**kwargs:
            | other keyword specifiers for plot.
          
    Returns:
        :ax:
            | handle to figure axes.          
    """
    if ax is None:
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
    if origin is None:
        origin = vec3(np.zeros(v.x.shape), np.zeros(v.x.shape),
                      np.zeros(v.x.shape))
    ax.plot(np.hstack([origin.x, v.x]),
            np.hstack([origin.y, v.y]),
            np.hstack([origin.z, v.z]),
            color=color,
            marker=marker,
            **kwargs)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    return ax
Example #3
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]
        :inverse:
            | If True: return inverse of cik.
    
    Returns:
        :cik: 
            'Nx2x2' (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 range(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
Example #4
0
def _complete_ldt_lid(LDT, Isym=4):
    """
    Convert LDT LID map with Isym symmetry to a 'full' map with phi: [0,360] and theta: [0,180].
    """
    cangles = LDT['h_angs']
    tangles = LDT['v_angs']
    candela_2d = LDT['candela_2d']
    if Isym == 4:
        # complete cangles:
        a = candela_2d.copy().T
        b = np.hstack((a, a[:, (a.shape[1] - 2)::-1]))
        c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1]))
        candela_2d_0C360 = np.hstack((c, c[:, :1]))
        cangles = np.hstack(
            (cangles, cangles[1:] + 90, cangles[1:] + 180, cangles[1:] + 270))
        # complete  tangles:
        a = candela_2d_0C360.copy()
        b = np.vstack((a, np.zeros(a.shape)[1:, :]))
        tangles = np.hstack((tangles, tangles[1:] + 90))
        candela_2d = b
    elif Isym == -4:
        # complete cangles:
        a = candela_2d.copy().T
        b = np.hstack((a, a[:, (a.shape[1] - 2)::-1]))
        c = np.hstack((b, b[:, (b.shape[1] - 2):0:-1]))
        candela_2d_0C360 = np.hstack((c, c[:, :1]))
        cangles = np.hstack(
            (cangles, -cangles[(cangles.shape[0] - 2)::-1] + 180))
        cangles = np.hstack(
            (cangles, -cangles[(cangles.shape[0] - 2):0:-1] + 360))
        cangles = np.hstack((cangles, cangles[:1]))
        # complete  tangles:
        a = candela_2d_0C360.copy()
        b = np.vstack((a, np.zeros(a.shape)[1:, :]))
        tangles = np.hstack(
            (tangles, -tangles[(tangles.shape[0] - 2)::-1] + 180))
        candela_2d = b
    else:
        raise Exception(
            'complete_ldt_lid(): Other "Isym" than "4", not yet implemented (31/10/2018).'
        )

    LDT['map'] = {'thetas': tangles}
    LDT['map']['phis'] = cangles
    LDT['map']['values'] = candela_2d.T
    return LDT
Example #5
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
Example #6
0
def fit_ellipse(xy):
    """
    Fit an ellipse to supplied data points.

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

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

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

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

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

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

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

        # ensure the angle is betwen 0 and 2*pi
        theta = fmod(theta, 2.0 * np.pi)
    return np.hstack((a, b, xc, yc, theta))
Example #7
0
def ndset(F):
    """
    Finds the nondominated set of a set of objective points.

    Args:
      F: 
          | a m x mu ndarray with mu points and m objectives

   Returns:
      :ispar: 
          | a mu-length vector with true in the nondominated points
    """
    mu = F.shape[1]  #number of points

    # The idea is to compare each point with the other ones
    f1 = np.transpose(F[..., None], axes=[0, 2, 1])  #puts in the 3D direction
    f1 = np.repeat(f1, mu, axis=1)
    f2 = np.repeat(F[..., None], mu, axis=2)

    # Now, for the ii-th slice, the ii-th individual is compared with all of the
    # others at once. Then, the usual operations of domination are checked
    # Checks where f1 dominates f2
    aux1 = (f1 <= f2).all(axis=0, keepdims=True)
    aux2 = (f1 < f2).any(axis=0, keepdims=True)

    auxf1 = np.logical_and(aux1, aux2)
    # Checks where f1 is dominated by f2
    aux1 = (f1 >= f2).all(axis=0, keepdims=True)
    aux2 = (f1 > f2).any(axis=0, keepdims=True)
    auxf2 = np.logical_and(aux1, aux2)

    # dom will be a 3D matrix (1 x mu x mu) such that, for the ii-th slice, it
    # will contain +1 if fii dominates the current point, -1 if it is dominated
    # by it, and 0 if they are incomparable
    dom = np.zeros((1, mu, mu), dtype=int)

    dom[auxf1] = 1
    dom[auxf2] = -1

    # Finally, the slices with no -1 are nondominated
    ispar = (dom != -1).all(axis=1)
    ispar = ispar.flatten()
    return ispar
Example #8
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
Example #9
0
def mahalanobis2(x, y=None, mu=None, sigmainv=None):
    """
    Evaluate the squared mahalanobis distance with center mu and shape 
    and orientation determined by sigmainv. 
    
    Args: 
        :x: 
            | scalar or list or ndarray (.ndim = 1 or 2) with x(y)-coordinates 
              at which to evaluate the mahalanobis distance squared.
        :y: 
            | None or scalar or list or ndarray (.ndim = 1) with y-coordinates 
              at which to evaluate the mahalanobis distance squared, optional.
            | If :y: is None, :x: should be a 2d array.
        :mu: 
            | None or ndarray (.ndim = 2) with center coordinates of the 
              mahalanobis ellipse, optional. 
            | None defaults to ndarray([0,0]).
        :sigmainv:
            | None or ndarray with 'inverse covariance matrix', optional 
            | Determines the shape and orientation of the PD.
            | None default to np.eye(2).
    Returns:
         :returns: 
             | ndarray with magnitude of mahalanobis2(x,y)

    """
    if mu is None:
        mu = np.zeros(2)
    if sigmainv is None:
        sigmainv = np.eye(2)

    x = np2d(x)

    if y is not None:
        x = x - mu[0]  # center data on mu
        y = np2d(y) - mu[1]  # center data on mu
    else:
        x = x - mu  # center data on mu
        x, y = asplit(x)
    return (sigmainv[0, 0] * (x**2.0) + sigmainv[1, 1] * (y**2.0) +
            2.0 * sigmainv[0, 1] * (x * y))
Example #10
0
def rgb_to_spec_smits(rgb, intent='rfl', bitdepth=8, wlr=_WL3, rgb2spec=None):
    """
    Convert an array of RGB values to a spectrum using a Smits like conversion as implemented in Mitsuba.
    
    Args:
        :rgb: 
            | ndarray of list of rgb values
        :intent:
            | 'rfl' (or 'spd'), optional
            | type of requested spectrum conversion .
        :bitdepth:
            | 8, optional
            | bit depth of rgb values
        :wlr: 
            | _WL3, optional
            | desired wavelength (nm) range of spectrum.
        :rgb2spec:
            | None, optional
            | Dict with base spectra for white, cyan, magenta, yellow, blue, green and red for each intent.
            | If None: use _BASESPEC_SMITS.
        
    Returns:
        :spec: 
            | ndarray with spectrum or spectra (one for each rgb value, first row are the wavelengths)
    """
    if isinstance(rgb, list):
        rgb = np.atleast_2d(rgb)
    if rgb.max() > 1:
        rgb = rgb / (2**bitdepth - 1)
    if rgb2spec is None:
        rgb2spec = _BASESPEC_SMITS
    if not np.array_equal(rgb2spec['wlr'], getwlr(wlr)):
        rgb2spec = _convert_to_wlr(entries=copy.deepcopy(rgb2spec), wlr=wlr)
    spec = np.zeros((rgb.shape[0], rgb2spec['wlr'].shape[0]))
    for i in range(rgb.shape[0]):
        spec[i, :] = _fromLinearRGB(rgb[i, :],
                                    intent=intent,
                                    rgb2spec=rgb2spec,
                                    wlr=wlr)
    return np.vstack((rgb2spec['wlr'], spec))
Example #11
0
def dtlz_range(fname, M):
    """
    Returns the decision range of a DTLZ function
    The range is simply [0,1] for all variables. What varies is the number 
    of decision variables in each problem. The equation for that is
    n = (M-1) + k
    wherein k = 5 for DTLZ1, 10 for DTLZ2-6, and 20 for DTLZ7.
    
    Args:
        :fname: 
            | a string with the name of the function ('dtlz1', 'dtlz2' etc.)
        :M: 
            | a scalar with the number of objectives
    
       Returns:
          :lim: 
              | a n x 2 matrix wherein the first column is the lower limit 
               (0), and the second column, the upper limit of search (1)
    """
    #Checks if the string has or not the prefix 'dtlz', or if the number later
    #is greater than 7:
    fname = fname.lower()
    if (len(fname) < 5) or (fname[:4] != 'dtlz') or (float(fname[4]) > 7):
        raise Exception(
            'Sorry, the function {:s} is not implemented.'.format(fname))

    # If the name is o.k., defines the value of k
    if fname == 'dtlz1':
        k = 5
    elif fname == 'dtlz7':
        k = 20
    else:  #any other function
        k = 10

    n = (M - 1) + k  #number of decision variables

    lim = np.hstack((np.zeros((n, 1)), np.ones((n, 1))))
    return lim
Example #12
0
def _fromLinearRGB(rgb, intent='rfl', rgb2spec=_BASESPEC_SMITS, wlr=_WL3):

    r, g, b = rgb
    result = np.zeros((rgb2spec['wlr'].shape[0], ))

    if (r <= g) & (r <= b):
        # Compute reflectance spectrum with 'r' as minimum
        result += r * rgb2spec[intent]['white']
        if (g <= b):
            result += (g - r) * rgb2spec[intent]['cyan']
            result += (b - g) * rgb2spec[intent]['blue']
        else:
            result += (b - r) * rgb2spec[intent]['cyan']
            result += (g - b) * rgb2spec[intent]['green']

    elif (g <= r) & (g <= b):
        # Compute reflectance spectrum with 'g' as minimum
        result += g * rgb2spec[intent]['white']
        if (r <= b):
            result += (r - g) * rgb2spec[intent]['magenta']
            result += (b - r) * rgb2spec[intent]['blue']
        else:
            result += (b - g) * rgb2spec[intent]['magenta']
            result += (r - b) * rgb2spec[intent]['red']

    else:
        # Compute reflectance spectrum with 'b' as minimum
        result += b * rgb2spec[intent]['white']
        if (r <= g):
            result += (r - b) * rgb2spec[intent]['yellow']
            result += (g - r) * rgb2spec[intent]['green']
        else:
            result += (g - b) * rgb2spec[intent]['yellow']
            result += (r - g) * rgb2spec[intent]['red']
    result *= rgb2spec[intent]['scalefactor']

    return np.clip(result, 0, None)  # no negative values allowed
Example #13
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
Example #14
0
def Ydlep_to_xyz(Ydlep, cieobs = _CIEOBS, xyzw = _COLORTF_DEFAULT_WHITE_POINT, **kwargs):
    """
    Convert Y, dominant (complementary) wavelength and excitation purity to XYZ
    tristimulus values.

    Args:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
        :xyzw: 
            | None or narray with tristimulus values of white point, optional
            | None defaults to xyz of CIE D65 using the :cieobs: observer.
        :cieobs:
            | luxpy._CIEOBS, optional
            | CMF set to use when calculating spectrum locus coordinates.

    Returns:
        :xyz: 
            | ndarray with tristimulus values
    """


    Ydlep3 = np3d(Ydlep).copy()

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

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

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

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

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

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

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

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

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

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

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


        # calculate intersection of line through white point and test point and purple line:
        xy = np.vstack((x_dom_wl,y_dom_wl)).T
        xyw = np.vstack((xw,yw)).T
        xypl1 = np.vstack((xsl[0,None],ysl[0,None])).T
        xypl2 = np.vstack((xsl[-1,None],ysl[-1,None])).T
        da = (xy-xyw)
        db = (xypl2-xypl1)
        dp = (xyw - xypl1)
        T = np.array([[0.0, -1.0], [1.0, 0.0]])
        dap = np.dot(da,T)
        denom = np.sum(dap * db,axis=1,keepdims=True)
        num = np.sum(dap * dp,axis=1,keepdims=True)
        xy_linecross = (num/denom) *db + xypl1
        d_linecross = np.atleast_2d((xy_linecross[:,0]**2.0 + xy_linecross[:,1]**2.0)**0.5).T[:,0]
        x[:,i][pc] = pur[:,i][pc]*d_linecross[pc]*np.cos(hdom[pc]*np.pi/180)
        y[:,i][pc] = pur[:,i][pc]*d_linecross[pc]*np.sin(hdom[pc]*np.pi/180)
    Yxy = np.dstack((Ydlep3[:,:,0],x + xwo, y + ywo))
    if axes12flipped == True:
        Yxy = Yxy.transpose((1,0,2))
    else:
        Yxy = Yxy.transpose((0,1,2))
    return Yxy_to_xyz(Yxy).reshape(Ydlep.shape)
Example #15
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
Example #16
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)
Example #17
0
def fit_ellipse(xy, center_on_mean_xy=False):
    """
    Fit an ellipse to supplied data points.

    Args:
        :xy: 
            | coordinates of points to fit (Nx2 array)
        :center_on_mean_xy:
            | False, optional
            | Center ellipse on mean of xy 
            | (otherwise it might be offset due to solving 
            | the contrained minization problem: aT*S*a, see ref below.)
            
    Returns:
        :v:
            | vector with ellipse parameters [Rmax,Rmin, xc,yc, theta]
            
    Reference:
        1. Fitzgibbon, A.W., Pilu, M., and Fischer R.B., 
        Direct least squares fitting of ellipsees, 
        Proc. of the 13th Internation Conference on Pattern Recognition, 
        pp 253–257, Vienna, 1996.
    """
    # remove centroid:
    #    center = xy.mean(axis=0)
    #    xy = xy - center

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

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

    # get ellipse center:
    num = b * b - a * c
    if num == 0:
        xc = 0
        yc = 0
    else:
        xc = ((c * d - b * f) / num)
        yc = ((a * f - b * d) / num)

    # get ellipse orientation:
    theta = np.arctan2(np.array(2 * b), np.array((a - c))) / 2
    #    if b == 0:
    #        if a > c:
    #            theta = 0
    #        else:
    #            theta = np.pi/2
    #    else:
    #        if a > c:
    #            theta = np.arctan2(2*b,(a-c))/2
    #        else:
    #            theta =  np.arctan2(2*b,(a-c))/2 + np.pi/2

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

    # assert that a is the major axis (otherwise swap and correct angle)
    if (b > a):
        b, a = a, b
        # ensure the angle is betwen 0 and 2*pi
        theta = fmod(theta, 2.0 * np.pi)

    if center_on_mean_xy == True:
        xc, yc = xy.mean(axis=0)

    return np.hstack((a, b, xc, yc, theta))
Example #18
0
def xyz_to_Ydlep(xyz, cieobs = _CIEOBS, xyzw = _COLORTF_DEFAULT_WHITE_POINT, **kwargs):
    """
    Convert XYZ tristimulus values to Y, dominant (complementary) wavelength
    and excitation purity.

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

    Returns:
        :Ydlep: 
            | ndarray with Y, dominant (complementary) wavelength
              and excitation purity
    """

    xyz3 = np3d(xyz).copy()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if axes12flipped == True:
        Ydlep = Ydlep.transpose((1,0,2))
    else:
        Ydlep = Ydlep.transpose((0,1,2))
    return Ydlep.reshape(xyz.shape)
Example #19
0
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
Example #20
0
Ct_hj = ((jabt_hj[..., 1]**2 + jabt_hj[..., 2]**2))**0.5
Cr_hj = ((jabr_hj[..., 1]**2 + jabr_hj[..., 2]**2))**0.5
Ctn_hj = normalized_chroma_ref * Ct_hj / (
    Cr_hj + 1e-308)  # calculate normalized chroma for samples under test
Ctn_hj[Cr_hj == 0.0] = np.inf
jabtn_hj = jabt_hj.copy()
jabrn_hj = jabr_hj.copy()
jabtn_hj[..., 1], jabtn_hj[...,
                           2] = Ctn_hj * np.cos(ht_hj), Ctn_hj * np.sin(ht_hj)
jabrn_hj[..., 1], jabrn_hj[..., 2] = normalized_chroma_ref * np.cos(
    hr_hj), normalized_chroma_ref * np.sin(hr_hj)

# calculate normalized versions of jabt, jabr:
jabtn = jabt.copy()
jabrn = jabr.copy()
Ctn = np.zeros((jabt.shape[0], jabt.shape[1]))
Crn = Ctn.copy()
for j in range(nhbins):
    Ctn = Ctn + (Ct / Cr_hj[j, ...]) * (hr_idx == j)
    Crn = Crn + (Cr / Cr_hj[j, ...]) * (hr_idx == j)
Ctn *= normalized_chroma_ref
Crn *= normalized_chroma_ref
jabtn[..., 1] = (Ctn * np.cos(ht))
jabtn[..., 2] = (Ctn * np.sin(ht))
jabrn[..., 1] = (Crn * np.cos(hr))
jabrn[..., 2] = (Crn * np.sin(hr))

# closed jabt_hj, jabr_hj for Rg:
jabt_hj_closed = np.vstack((jabt_hj, jabt_hj[:1, ...]))
jabr_hj_closed = np.vstack((jabr_hj, jabr_hj[:1, ...]))
Example #21
0
def rotate(v,
           vecA=None,
           vecB=None,
           rot_axis=None,
           rot_angle=None,
           deg=True,
           norm=False):
    """
    Rotate vector around rotation axis over angle.
    
    Args:
        :v: 
            | vec3 vector.
        :rot_axis:
            | None, optional
            | vec3 vector specifying rotation axis.
        :rot_angle:
            | None, optional
            | float or int rotation angle.
        :deg:
            | True, optional
            | If False, rot_angle is in radians.
        :vecA:, :vecB:
            | None, optional
            | vec3 vectors defining a normal direction (cross(vecA, vecB)) around 
            | which to rotate the vector in :v:. If rot_angle is None: rotation
            | angle is defined by the in-plane angle between vecA and vecB.
        :norm:
            | False, optional
            | Normalize rotated vector.
        
    """

    if (vecA is not None) & (vecB is not None):
        rot_axis = cross(vecA, vecB)  # rotation axis
        if rot_angle is None:
            costheta = dot(vecA, vecB, norm=True)  # rotation angle
            costheta[costheta > 1] = 1
            costheta[costheta < -1] = -1
            rot_angle = np.arccos(costheta)
    elif (rot_angle is not None):
        if deg == True:
            rot_angle = np.deg2rad(rot_angle)
    else:
        raise Exception('vec3.rotate: insufficient not-None input args.')

    # normalize rot_axis
    rot_axis = rot_axis / rot_axis.norm()

    # Create short-hand variables:
    u = rot_axis
    cost = np.cos(rot_angle)
    sint = np.sin(rot_angle)

    # Setup rotation matrix:
    R = np.asarray([[np.zeros(u.x.shape) for j in range(3)] for i in range(3)])
    R[0, 0] = cost + u.x * u.x * (1 - cost)
    R[0, 1] = u.x * u.y * (1 - cost) - u.z * sint
    R[0, 2] = u.x * u.z * (1 - cost) + u.y * sint
    R[1, 0] = u.x * u.y * (1 - cost) + u.z * sint
    R[1, 1] = cost + u.y * u.y * (1 - cost)
    R[1, 2] = u.y * u.z * (1 - cost) - u.x * sint
    R[2, 0] = u.z * u.x * (1 - cost) - u.y * sint
    R[2, 1] = u.z * u.y * (1 - cost) + u.x * sint
    R[2, 2] = cost + u.z * u.z * (1 - cost)

    # calculate dot product of matrix M with vector v:
    v3 = vec3(R[0,0]*v.x + R[0,1]*v.y + R[0,2]*v.z, \
                R[1,0]*v.x + R[1,1]*v.y + R[1,2]*v.z, \
                R[2,0]*v.x + R[2,1]*v.y + R[2,2]*v.z)
    if norm == True:
        v3 = v3 / v3.norm()
    return v3
Example #22
0
def plotellipse(v, cspace_in = 'Yxy', cspace_out = None, nsamples = 100, \
                show = True, axh = None, \
                line_color = 'darkgray', line_style = ':', line_width = 1, line_marker = '', line_markersize = 4,\
                plot_center = False, center_marker = 'o', center_color = 'darkgray', center_markersize = 4,\
                show_grid = True, label_fontname = 'Times New Roman', label_fontsize = 12,\
                out = None):
    """
    Plot ellipse(s) given in v-format [Rmax,Rmin,xc,yc,theta].
    
    Args:
        :v: 
            | (Nx5) ndarray
            | ellipse parameters [Rmax,Rmin,xc,yc,theta]
        :cspace_in:
            | 'Yxy', optional
            | Color space of v.
            | If None: no color space assumed. Axis labels assumed ('x','y').
        :cspace_out:
            | None, optional
            | Color space to plot ellipse(s) in.
            | If None: plot in cspace_in.
        :nsamples:
            | 100 or int, optional
            | Number of points (samples) in ellipse boundary
        :show:
            | True or boolean, optional
            | Plot ellipse(s) (True) or not (False)
        :axh: 
            | None, optional
            | Ax-handle to plot ellipse(s) in.
            | If None: create new figure with axes.
        :line_color:
            | 'darkgray', optional
            | Color to plot ellipse(s) in.
        :line_style:
            | ':', optional
            | Linestyle of ellipse(s).
        :line_width':
            | 1, optional
            | Width of ellipse boundary line.
        :line_marker:
            | 'none', optional
            | Marker for ellipse boundary.
        :line_markersize:
            | 4, optional
            | Size of markers in ellipse boundary.
        :plot_center:
            | False, optional
            | Plot center of ellipse: yes (True) or no (False)
        :center_color:
            | 'darkgray', optional
            | Color to plot ellipse center in.
        :center_marker:
            | 'o', optional
            | Marker for ellipse center.
        :center_markersize:
            | 4, optional
            | Size of marker of ellipse center.
        :show_grid:
            | True, optional
            | Show grid (True) or not (False)
        :label_fontname: 
            | 'Times New Roman', optional
            | Sets font type of axis labels.
        :label_fontsize:
            | 12, optional
            | Sets font size of axis labels.
        :out:
            | None, optional
            | Output of function
            | If None: returns None. Can be used to output axh of newly created
            |      figure axes or to return Yxys an ndarray with coordinates of 
            |       ellipse boundaries in cspace_out (shape = (nsamples,3,N)) 
            
        
    Returns:
        :returns: None, or whatever set by :out:.
    """
    Yxys = np.zeros((nsamples,3,v.shape[0]))
    ellipse_vs = np.zeros((v.shape[0],5))
    for i,vi in enumerate(v):
        
        # Set sample density of ellipse boundary:
        t = np.linspace(0, 2*np.pi, nsamples)
        
        a = vi[0] # major axis
        b = vi[1] # minor axis
        xyc = vi[2:4,None] # center
        theta = vi[-1] # rotation angle
        
        # define rotation matrix:
        R = np.hstack(( np.vstack((np.cos(theta), np.sin(theta))), np.vstack((-np.sin(theta), np.cos(theta)))))
 
        # Calculate ellipses:
        Yxyc = np.vstack((1, xyc)).T
        Yxy = np.vstack((np.ones((1,nsamples)), xyc + np.dot(R, np.vstack((a*np.cos(t), b*np.sin(t))) ))).T
        Yxys[:,:,i] = Yxy
        
        # Convert to requested color space:
        if (cspace_out is not None) & (cspace_in is not None):
            Yxy = colortf(Yxy, cspace_in + '>' + cspace_out)
            Yxyc = colortf(Yxyc, cspace_in + '>' + cspace_out)
            Yxys[:,:,i] = Yxy
            
            # get ellipse parameters in requested color space:
            ellipse_vs[i,:] = math.fit_ellipse(Yxy[:,1:])
            #de = np.sqrt((Yxy[:,1]-Yxyc[:,1])**2 + (Yxy[:,2]-Yxyc[:,2])**2)
            #ellipse_vs[i,:] = np.hstack((de.max(),de.min(),Yxyc[:,1],Yxyc[:,2],np.nan)) # nan because orientation is xy, but request is some other color space. Change later to actual angle when fitellipse() has been implemented

        
        # plot ellipses:
        if show == True:
            if (axh is None) & (i == 0):
                fig = plt.figure()
                axh = fig.add_subplot(111)
            
            if (cspace_in is None):
                xlabel = 'x'
                ylabel = 'y'
            else:
                xlabel = _CSPACE_AXES[cspace_in][1]
                ylabel = _CSPACE_AXES[cspace_in][2]
            
            if (cspace_out is not None):
                xlabel = _CSPACE_AXES[cspace_out][1]
                ylabel = _CSPACE_AXES[cspace_out][2]
            
            if plot_center == True:
                plt.plot(Yxyc[:,1],Yxyc[:,2],color = center_color, linestyle = 'none', marker = center_marker, markersize = center_markersize)

            plt.plot(Yxy[:,1],Yxy[:,2],color = line_color, linestyle = line_style, linewidth = line_width, marker = line_marker, markersize = line_markersize)
            plt.xlabel(xlabel, fontname = label_fontname, fontsize = label_fontsize)
            plt.ylabel(ylabel, fontname = label_fontname, fontsize = label_fontsize)
            if show_grid == True:
                plt.grid()
            #plt.show()     
    Yxys = np.transpose(Yxys,axes=(0,2,1))       
    if out is not None:
        return eval(out)
    else:
        return None
Example #23
0
def cct_to_xyz(ccts,
               duv=None,
               cieobs=_CIEOBS,
               wl=None,
               mode='lut',
               out=None,
               accuracy=0.1,
               force_out_of_lut=True,
               upper_cct_max=10.0 * 20,
               approx_cct_temp=True):
    """
    Convert correlated color temperature (CCT) and Duv (distance above (>0) or 
    below (<0) the Planckian locus) to XYZ tristimulus values.
    
    | Finds xyzw_estimated by minimization of:
    |    
    |    F = numpy.sqrt(((100.0*(cct_min - cct)/(cct))**2.0) 
    |         + (((duv_min - duv)/(duv))**2.0))
    |    
    | with cct,duv the input values and cct_min, duv_min calculated using 
    | luxpy.xyz_to_cct(xyzw_estimated,...).
    
    Args:
        :ccts: 
            | ndarray of cct values
        :duv: 
            | None or ndarray of duv values, optional
            | Note that duv can be supplied together with cct values in :ccts: 
              as ndarray with shape (N,2)
        :cieobs: 
            | luxpy._CIEOBS, optional
            | CMF set used to calculated xyzw.
        :mode: 
            | 'lut' or 'search', optional
            | Determines what method to use.
        :out: 
            | None (or 1), optional
            | If not None or 1: output a ndarray that contains estimated 
              xyz and minimization results: 
            | (cct_min, duv_min, F_min (objective fcn value))
        :wl: 
            | None, optional
            | Wavelengths used when calculating Planckian radiators.
        :accuracy: 
            | float, optional
            | Stop brute-force search when cct :accuracy: is reached.
        :upper_cct_max: 
            | 10.0**20, optional
            | Limit brute-force search to this cct.
        :approx_cct_temp: 
            | True, optional
            | If True: use xyz_to_cct_HA() to get a first estimate of cct to 
              speed up search.
        :force_out_of_lut: 
            | True, optional
            | If True and cct is out of range of the LUT, then switch to 
              brute-force search method, else return numpy.nan values.
        
    Returns:
        :returns: 
            | ndarray with estimated XYZ tristimulus values
    
    Note:
        If duv is not supplied (:ccts:.shape is (N,1) and :duv: is None), 
        source is assumed to be on the Planckian locus.
	 """
    # make ccts a min. 2d np.array:
    if isinstance(ccts, list):
        ccts = np2dT(np.array(ccts))
    else:
        ccts = np2d(ccts)

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

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

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

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

    if duv is not None:

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

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

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

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

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

            else:
                xyz_est[i] = xyz0

    if (out is None) | (out == 1):
        return xyz_est
    else:
        # Also output results of minimization:
        return np.concatenate((xyz_est, results), axis=1)
Example #24
0
def spd_to_cqs(SPD, version='v9.0', out='Qa', wl=None):
    """
    Calculates CQS Qa (Qai) or Qf (Qfi) or Qp (Qpi) for versions v9.0 or v7.5.
    
    Args:
        :SPD: 
            | ndarray with spectral data (can be multiple SPDs, 
              first axis are the wavelengths)
        :version: 
            | 'v9.0' or 'v7.5', optional
        :out: 
            | 'Qa' or str, optional
            | Specifies requested output (e.g. 'Qa,Qai,Qf,cct,duv') 
        :wl: 
            | None, optional
            | Wavelengths (or [start, end, spacing]) to interpolate the SPDs to. 
            | None: default to no interpolation   
    
    Returns:
        :returns:
            | float or ndarray with CQS Qa for :out: 'Qa'
            | Other output is also possible by changing the :out: str value. 
    
    References:
        1. `W. Davis and Y. Ohno, 
        “Color quality scale,” (2010), 
        Opt. Eng., vol. 49, no. 3, pp. 33602–33616.
        <http://spie.org/Publications/Journal/10.1117/1.3360335>`_
    
    """
    outlist = out.split()
    if isinstance(version, str):
        cri_type = 'cqs-' + version
    elif isinstance(version, dict):
        cri_type = version

    # calculate DEI, labti, labri and get cspace_pars and rg_pars:
    DEi, labti, labri, cct, duv, cri_type = spd_to_DEi(
        SPD, cri_type=cri_type, out='DEi,jabt,jabr,cct,duv,cri_type', wl=wl)

    # further unpack cri_type:
    scale_fcn = cri_type['scale']['fcn']
    scale_factor = cri_type['scale']['cfactor']
    avg = cri_type['avg']
    cri_specific_pars = cri_type['cri_specific_pars']
    rg_pars = cri_type['rg_pars']

    # get maxC: to limit chroma-enhancement:
    maxC = cri_specific_pars['maxC']

    # make 3d:
    test_original_shape = labti.shape
    if len(test_original_shape) < 3:
        labti = labti[:, None]
        labri = labri[:, None]
        DEi = DEi[:, None]
        cct = cct[:, None]

    # calculate Rg for each spd:
    Qf = np.zeros((1, labti.shape[1]))
    Qfi = np.zeros((labti.shape[0], labti.shape[1]))

    if version == 'v7.5':
        GA = (9.2672 * (1.0e-11)) * cct**3.0 - (
            8.3959 * (1.0e-7)) * cct**2.0 + 0.00255 * cct - 1.612
    elif version == 'v9.0':
        GA = np.ones(cct.shape)
    else:
        raise Exception('.cri.spd_to_cqs(): Unrecognized CQS version.')

    if ('Qf' in outlist) | ('Qfi' in outlist):

        # loop of light source spds
        for ii in range(labti.shape[1]):
            Qfi[:, ii] = GA[ii] * scale_fcn(DEi[:, ii], [scale_factor[0]])
            Qf[:, ii] = GA[ii] * scale_fcn(avg(DEi[:, ii, None], axis=0),
                                           [scale_factor[0]])

    if ('Qa' in outlist) | ('Qai' in outlist) | ('Qp' in outlist) | (
            'Qpi' in outlist):

        Qa = Qf.copy()
        Qai = Qfi.copy()
        Qp = Qf.copy()
        Qpi = Qfi.copy()

        # loop of light source spds
        for ii in range(labti.shape[1]):

            # calculate deltaC:
            deltaC = np.sqrt(
                np.power(labti[:, ii, 1:3], 2).sum(
                    axis=1, keepdims=True)) - np.sqrt(
                        np.power(labri[:, ii, 1:3], 2).sum(axis=1,
                                                           keepdims=True))
            # limit chroma increase:
            DEi_Climited = DEi[:, ii, None].copy()
            deltaC_Climited = deltaC.copy()
            if maxC is None:
                maxC = 10000.0
            limitC = np.where(deltaC >= maxC)[0]
            deltaC_Climited[limitC] = maxC
            p_deltaC_pos = np.where(deltaC > 0.0)[0]
            DEi_Climited[p_deltaC_pos] = np.sqrt(
                DEi_Climited[p_deltaC_pos]**2.0 - deltaC_Climited[p_deltaC_pos]
                **2.0)  # increase in chroma is not penalized!

            if ('Qa' in outlist) | ('Qai' in outlist):
                Qai[:, ii,
                    None] = GA[ii] * scale_fcn(DEi_Climited, [scale_factor[1]])
                Qa[:, ii] = GA[ii] * scale_fcn(avg(DEi_Climited, axis=0),
                                               [scale_factor[1]])

            if ('Qp' in outlist) | ('Qpi' in outlist):
                deltaC_pos = deltaC_Climited * (deltaC_Climited >= 0.0)
                deltaCmu = np.mean(deltaC_Climited * (deltaC_Climited >= 0.0))
                Qpi[:, ii, None] = GA[ii] * scale_fcn(
                    (DEi_Climited - deltaC_pos),
                    [scale_factor[2]
                     ])  # or ?? np.sqrt(DEi_Climited**2 - deltaC_pos**2) ??
                Qp[:, ii] = GA[ii] * scale_fcn(
                    (avg(DEi_Climited, axis=0) - deltaCmu), [scale_factor[2]])

    if ('Qg' in outlist):
        Qg = Qf.copy()
        for ii in range(labti.shape[1]):
            Qg[:, ii] = 100.0 * math.polyarea(
                labti[:, ii, 1], labti[:, ii, 2]) / math.polyarea(
                    labri[:, ii, 1], labri[:, ii, 2]
                )  # calculate Rg =  gamut area ratio of test and ref

    if out == 'Qa':
        return Qa
    else:
        return eval(out)