import os
import numpy as np
from fury import actor
from fury.shaders import (
    attribute_to_actor,
    compose_shader,
    import_fury_shader,
    shader_to_actor,
)
[docs]
def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity):
    """Visualize one or many Tensor Ellipsoids with different features.
    Parameters
    ----------
    centers : ndarray(N, 3)
        Tensor ellipsoid positions.
    axes : ndarray (3, 3) or (N, 3, 3)
        Axes of the tensor ellipsoid.
    lengths : ndarray (3, ) or (N, 3)
        Axes lengths.
    colors : ndarray (N,3) or tuple (3,)
        R, G and B should be in the range [0, 1].
    scales : float or ndarray (N, )
        Tensor ellipsoid size.
    opacity : float
        Takes values from 0 (fully transparent) to 1 (opaque).
    Returns
    -------
    box_actor: Actor
    """
    box_actor = actor.box(centers, colors=colors, scales=scales)
    box_actor.GetMapper().SetVBOShiftScaleMethod(False)
    box_actor.GetProperty().SetOpacity(opacity)
    # Number of vertices that make up the box
    n_verts = 8
    big_centers = np.repeat(centers, n_verts, axis=0)
    attribute_to_actor(box_actor, big_centers, "center")
    big_scales = np.repeat(scales, n_verts, axis=0)
    attribute_to_actor(box_actor, big_scales, "scale")
    big_values = np.repeat(np.array(lengths, dtype=float), n_verts, axis=0)
    attribute_to_actor(box_actor, big_values, "evals")
    evec1 = np.array([item[0] for item in axes])
    evec2 = np.array([item[1] for item in axes])
    evec3 = np.array([item[2] for item in axes])
    big_vectors_1 = np.repeat(evec1, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_1, "evec1")
    big_vectors_2 = np.repeat(evec2, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_2, "evec2")
    big_vectors_3 = np.repeat(evec3, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_3, "evec3")
    # Start of shader implementation
    vs_dec = """
        in vec3 center;
        in float scale;
        in vec3 evals;
        in vec3 evec1;
        in vec3 evec2;
        in vec3 evec3;
        out vec4 vertexMCVSOutput;
        out vec3 centerMCVSOutput;
        out float scaleVSOutput;
        out vec3 evalsVSOutput;
        out mat3 tensorMatrix;
        """
    # Variables assignment
    v_assign = """
        vertexMCVSOutput = vertexMC;
        centerMCVSOutput = center;
        scaleVSOutput = scale;
        """
    # Normalization
    n_evals = "evalsVSOutput = evals/(max(evals.x, max(evals.y, evals.z)));"
    # Values constraint to avoid incorrect visualizations
    evals = "evalsVSOutput = clamp(evalsVSOutput,0.05,1);"
    # Scaling matrix
    sc_matrix = """
        mat3 S = mat3(1/evalsVSOutput.x, 0.0, 0.0,
                      0.0, 1/evalsVSOutput.y, 0.0,
                      0.0, 0.0, 1/evalsVSOutput.z);
        """
    # Rotation matrix
    rot_matrix = "mat3 R = mat3(evec1, evec2, evec3);"
    # Tensor matrix
    t_matrix = "tensorMatrix = inverse(R) * S * R;"
    vs_impl = compose_shader(
        [v_assign, n_evals, evals, sc_matrix, rot_matrix, t_matrix]
    )
    # Adding shader implementation to actor
    shader_to_actor(box_actor, "vertex", decl_code=vs_dec, impl_code=vs_impl)
    fs_vars_dec = """
        in vec4 vertexMCVSOutput;
        in vec3 centerMCVSOutput;
        in float scaleVSOutput;
        in vec3 evalsVSOutput;
        in mat3 tensorMatrix;
        uniform mat4 MCVCMatrix;
        """
    # Importing the sphere SDF
    sd_sphere = import_fury_shader(os.path.join("sdf", "sd_sphere.frag"))
    # SDF definition
    sdf_map = """
        float map(in vec3 position)
        {
            /*
            As the scaling is not a rigid body transformation, we multiply
            by a factor to compensate for distortion and not overestimate
            the distance.
            */
            float scFactor = min(evalsVSOutput.x, min(evalsVSOutput.y,
                                     evalsVSOutput.z));
            /*
            The approximation of distance is calculated by stretching the
            space such that the ellipsoid becomes a sphere (multiplying by
            the transformation matrix) and then computing the distance to
            a sphere in that space (using the sphere SDF).
            */
            return sdSphere(tensorMatrix * (position - centerMCVSOutput),
                scaleVSOutput*0.48) * scFactor;
        }
        """
    # Importing central differences function for computing surface normals
    central_diffs_normal = import_fury_shader(os.path.join("sdf", "central_diffs.frag"))
    # Importing raymarching function
    cast_ray = import_fury_shader(os.path.join("ray_marching", "cast_ray.frag"))
    # Importing the function that generates the ray components
    ray_generation = import_fury_shader(os.path.join("ray_marching", "gen_ray.frag"))
    # Importing Blinn-Phong model for lighting
    blinn_phong_model = import_fury_shader(
        os.path.join("lighting", "blinn_phong_model.frag")
    )
    # Full fragment shader declaration
    fs_dec = compose_shader(
        [
            fs_vars_dec,
            sd_sphere,
            sdf_map,
            central_diffs_normal,
            cast_ray,
            ray_generation,
            blinn_phong_model,
        ]
    )
    shader_to_actor(box_actor, "fragment", decl_code=fs_dec)
    ray_components = """
        vec3 ro; vec3 rd; float t;
        gen_ray(ro, rd, t);
        """
    # Fragment shader output definition
    # If surface is detected, color is assigned, otherwise, nothing is painted
    frag_output_def = """
        if(t < 20)
        {
            vec3 pos = ro + t * rd;
            vec3 normal = centralDiffsNormals(pos, .0001);
            // Light Direction
            vec3 ld = normalize(ro - pos);
            // Light Attenuation
            float la = dot(ld, normal);
            vec3 color = blinnPhongIllumModel(la, lightColor0,
                diffuseColor, specularPower, specularColor, ambientColor);
            fragOutput0 = vec4(color, opacity);
        }
        else
        {
            discard;
        }
        """
    # Full fragment shader implementation
    sdf_frag_impl = compose_shader([ray_components, frag_output_def])
    shader_to_actor(box_actor, "fragment", impl_code=sdf_frag_impl, block="light")
    return box_actor 
[docs]
def double_cone(centers, axes, angles, colors, scales, opacity):
    """Visualize one or many Double Cones with different features.
    Parameters
    ----------
    centers : ndarray(N, 3)
        Cone positions.
    axes : ndarray (3, 3) or (N, 3, 3)
        Axes of the cone.
    angles : float or ndarray (N, )
        Angles of the cone.
    colors : ndarray (N, 3) or tuple (3,)
        R, G and B should be in the range [0, 1].
    scales : float or ndarray (N, )
        Cone size.
    opacity : float
        Takes values from 0 (fully transparent) to 1 (opaque).
    Returns
    -------
    box_actor: Actor
    """
    box_actor = actor.box(centers, colors=colors, scales=scales)
    box_actor.GetMapper().SetVBOShiftScaleMethod(False)
    box_actor.GetProperty().SetOpacity(opacity)
    # Number of vertices that make up the box
    n_verts = 8
    big_centers = np.repeat(centers, n_verts, axis=0)
    attribute_to_actor(box_actor, big_centers, "center")
    big_scales = np.repeat(scales, n_verts, axis=0)
    attribute_to_actor(box_actor, big_scales, "scale")
    evec1 = np.array([item[0] for item in axes])
    evec2 = np.array([item[1] for item in axes])
    evec3 = np.array([item[2] for item in axes])
    big_vectors_1 = np.repeat(evec1, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_1, "evec1")
    big_vectors_2 = np.repeat(evec2, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_2, "evec2")
    big_vectors_3 = np.repeat(evec3, n_verts, axis=0)
    attribute_to_actor(box_actor, big_vectors_3, "evec3")
    big_angles = np.repeat(np.array(angles, dtype=float), n_verts, axis=0)
    attribute_to_actor(box_actor, big_angles, "angle")
    # Start of shader implementation
    vs_dec = """
        in vec3 center;
        in float scale;
        in vec3 evec1;
        in vec3 evec2;
        in vec3 evec3;
        in float angle;
        out vec4 vertexMCVSOutput;
        out vec3 centerMCVSOutput;
        out float scaleVSOutput;
        out mat3 rotationMatrix;
        out float angleVSOutput;
        """
    # Variables assignment
    v_assign = """
        vertexMCVSOutput = vertexMC;
        centerMCVSOutput = center;
        scaleVSOutput = scale;
        angleVSOutput = angle;
        """
    # Rotation matrix
    rot_matrix = """
        mat3 R = mat3(normalize(evec1), normalize(evec2), normalize(evec3));
        float a = radians(90);
        mat3 rot = mat3(cos(a),-sin(a), 0,
                        sin(a), cos(a), 0,
                            0 ,      0, 1);
        rotationMatrix = transpose(R) * rot;
        """
    vs_impl = compose_shader([v_assign, rot_matrix])
    shader_to_actor(box_actor, "vertex", decl_code=vs_dec, impl_code=vs_impl)
    fs_vars_dec = """
        in vec4 vertexMCVSOutput;
        in vec3 centerMCVSOutput;
        in float scaleVSOutput;
        in mat3 rotationMatrix;
        in float angleVSOutput;
        uniform mat4 MCVCMatrix;
        """
    # Importing the cone SDF
    sd_cone = import_fury_shader(os.path.join("sdf", "sd_cone.frag"))
    # Importing the union operation SDF
    sd_union = import_fury_shader(os.path.join("sdf", "sd_union.frag"))
    # SDF definition
    sdf_map = """
        float map(in vec3 position)
        {
            vec3 p = (position - centerMCVSOutput)/scaleVSOutput
                *rotationMatrix;
            float angle = clamp(angleVSOutput, 0, 6.283);
            vec2 a = vec2(sin(angle), cos(angle));
            float h = .5 * a.y;
            return opUnion(sdCone(p,a,h), sdCone(-p,a,h)) * scaleVSOutput;
        }
        """
    # Importing central differences function for computing surface normals
    central_diffs_normal = import_fury_shader(os.path.join("sdf", "central_diffs.frag"))
    # Importing raymarching function
    cast_ray = import_fury_shader(os.path.join("ray_marching", "cast_ray.frag"))
    # Importing the function that generates the ray components
    ray_generation = import_fury_shader(os.path.join("ray_marching", "gen_ray.frag"))
    # Importing Blinn-Phong model for lighting
    blinn_phong_model = import_fury_shader(
        os.path.join("lighting", "blinn_phong_model.frag")
    )
    # Full fragment shader declaration
    fs_dec = compose_shader(
        [
            fs_vars_dec,
            sd_cone,
            sd_union,
            sdf_map,
            central_diffs_normal,
            cast_ray,
            ray_generation,
            blinn_phong_model,
        ]
    )
    shader_to_actor(box_actor, "fragment", decl_code=fs_dec)
    ray_components = """
        vec3 ro; vec3 rd; float t;
        gen_ray(ro, rd, t);
        """
    # Fragment shader output definition
    # If surface is detected, color is assigned, otherwise, nothing is painted
    frag_output_def = """
        if(t < 20)
        {
            vec3 pos = ro + t * rd;
            vec3 normal = centralDiffsNormals(pos, .0001);
            // Light Direction
            vec3 ld = normalize(ro - pos);
            // Light Attenuation
            float la = dot(ld, normal);
            vec3 color = blinnPhongIllumModel(la, lightColor0,
                diffuseColor, specularPower, specularColor, ambientColor);
            fragOutput0 = vec4(color, opacity);
        }
        else
        {
            discard;
        }
        """
    # Full fragment shader implementation
    sdf_frag_impl = compose_shader([ray_components, frag_output_def])
    shader_to_actor(box_actor, "fragment", impl_code=sdf_frag_impl, block="light")
    return box_actor 
[docs]
def main_dir_uncertainty(evals, evecs, signal, sigma, b_matrix):
    """Calculate the angle of the cone of uncertainty that represents the
    perturbation of the main eigenvector of the diffusion tensor matrix.
    Parameters
    ----------
    evals : ndarray (3, ) or (N, 3)
        Eigenvalues.
    evecs : ndarray (3, 3) or (N, 3, 3)
        Eigenvectors.
    signal : 3D or 4D ndarray
        Predicted signal.
    sigma : ndarray
        Standard deviation of the noise.
    b_matrix : array (N, 7)
        Design matrix for DTI.
    Returns
    -------
    angles : array
    Notes
    -----
    The uncertainty calculation is based on first-order matrix perturbation
    analysis described in [1]_. The idea is to estimate the variance of the
    main eigenvector which corresponds to the main direction of diffusion,
    directly from estimated D and its estimated covariance matrix
    :math:`\\Delta D` (see [2]_, equation 4). The angle :math:`\\Theta`
    between the perturbed principal eigenvector of D,
    :math:`\\epsilon_1 + \\Delta\\epsilon_1`, and the estimated eigenvector
    :math:`\\epsilon_1`, measures the angular deviation of the main fiber
    direction and can be approximated by:
    .. math::
        \\Theta = tan^{-1}(\\|\\Delta \\ epsilon_1\\|)
    Giving way to a graphical construct for displaying both the main
    eigenvector of D and its associated uncertainty, with the so-called
    "uncertainty cone".
    References
    ----------
    .. [1] Basser, P. J. (1997). Quantifying errors in fiber direction and
        diffusion tensor field maps resulting from MR noise. In 5th Scientific
        Meeting of the ISMRM (Vol. 1740).
    .. [2] Chang, L. C., Koay, C. G., Pierpaoli, C., & Basser, P. J. (2007).
        Variance of estimated DTI-derived parameters via first-order perturbation
        methods. Magnetic Resonance in Medicine: An Official Journal of the
        International Society for Magnetic Resonance in Medicine, 57(1), 141-149.
    """
    angles = np.ones(evecs.shape[0])
    for i in range(evecs.shape[0]):
        sigma_e = np.diag(signal[i] / sigma**2)
        k = np.dot(np.transpose(b_matrix), sigma_e)
        sigma_ = np.dot(k, b_matrix)
        dd = np.diag(sigma_)
        delta_DD = np.array(
            [[dd[0], dd[3], dd[4]], [dd[3], dd[1], dd[5]], [dd[4], dd[5], dd[2]]]
        )
        # perturbation matrix of tensor D
        try:
            delta_D = np.linalg.inv(delta_DD)
        except np.linalg.LinAlgError:
            delta_D = delta_DD
        D_ = evecs
        eigen_vals = evals[i]
        e1, e2, e3 = np.array(D_[i, :, 0]), np.array(D_[i, :, 1]), np.array(D_[i, :, 2])
        lambda1, lambda2, lambda3 = eigen_vals[0], eigen_vals[1], eigen_vals[2]
        if lambda1 > lambda2 and lambda1 > lambda3:
            # The perturbation of the eigenvector associated with the largest
            # eigenvalue is given by
            a = np.dot(
                np.outer(np.dot(e1, delta_D), np.transpose(e2)) / (lambda1 - lambda2),
                e2,
            )
            b = np.dot(
                np.outer(np.dot(e1, delta_D), np.transpose(e3)) / (lambda1 - lambda3),
                e3,
            )
            delta_e1 = a + b
            # The angle \theta between the perturbed principal eigenvector of D
            theta = np.arctan(np.linalg.norm(delta_e1))
            angles[i] = theta
        else:
            # If the condition is not satisfied it means that there is not a
            # predominant diffusion direction, hence the uncertainty will be
            # higher and a default value close to pi/2 is assigned
            theta = 1.39626
    return angles