//  [python]
def get_3dmfv_n_est(points, w, mu, sigma, flatten=True, n_original_points=None):
    """
    Compute the fisher vector given the gmm model parameters (w,mu,sigma) and a set of points

    :param points: B X N x 3 tensor of XYZ points
    :param w: B X n_gaussians tensor of gaussian weights
    :param mu: B X n_gaussians X 3 tensor of gaussian cetnters
    :param sigma: B X n_gaussians X 3 tensor of stddev of diagonal covariance
    :return: fv: B X 20*n_gaussians / B X 20 x n_gaussians  tensor of the 3dmfv representation (shape depends on flatten)
    """
    n_original_points = tf.cast(n_original_points, tf.int32)
    n_batches = points.shape[0].value
    n_points = points.shape[1].value
    n_gaussians = mu.shape[0].value
    D = mu.shape[1].value

    #Expand dimension for batch compatibility
    batch_sig = tf.tile(tf.expand_dims(sigma, 0), [n_points, 1, 1])  # n_points X n_gaussians X D
    batch_sig = tf.tile(tf.expand_dims(batch_sig, 0), [n_batches, 1, 1, 1])  # n_batches X n_points X n_gaussians X D
    batch_mu = tf.tile(tf.expand_dims(mu, 0), [n_points, 1, 1]) # n_points X n_gaussians X D
    batch_mu = tf.tile(tf.expand_dims(batch_mu, 0), [n_batches, 1, 1, 1]) # n_batches X n_points X n_gaussians X D
    batch_w = tf.tile(tf.expand_dims(tf.expand_dims(w, 0), 0), [n_batches, n_points, 1])  # n_batches X n_points X n_guassians  - should check what happens when weights change
    batch_points = tf.tile(tf.expand_dims(points, -2), [1, 1, n_gaussians,
                                                        1])  # n_batchesXn_pointsXn_gaussians_D  # Generating the number of points for each gaussian for separate computation

    #Compute derivatives
    w_per_batch_per_d = tf.tile(tf.expand_dims(tf.expand_dims(w, 0), -1), [n_batches, 1, 3*D]) #n_batches X n_gaussians X 128*D (D for min and D for max)

    #Define multivariate noraml distributions
    # mvn = tf.contrib.distributions.MultivariateNormalDiag(loc=batch_mu, scale_diag=batch_sig)
    #Compute probability per point
    # p_per_point = mvn.prob(batch_points)
    p_per_point = (1.0/(tf.pow(2.0*np.pi, D/2.0) * tf.pow(batch_sig[:, :, :, 0], D))) * \
                  tf.exp(-0.5 * tf.reduce_sum(tf.square((batch_points - batch_mu) / batch_sig), axis=3))
    zeros_per_point = tf.zeros_like(p_per_point)

    r = tf.range(n_points)
    r = r[tf.newaxis, :, tf.newaxis]
    r = tf.tile(r, (n_batches, 1, n_gaussians))
    idxs = n_original_points[:, tf.newaxis, tf.newaxis]
    idxs = tf.tile(idxs, (1, n_points, n_gaussians))
    mask = r > idxs

    w_zero_compensate = tf.where(mask, batch_w, zeros_per_point)

    w_p = tf.multiply(p_per_point, batch_w)
    Q = w_p/tf.tile(tf.expand_dims(tf.reduce_sum(w_p, axis=-1), -1), [1, 1, n_gaussians])

    Q = tf.where(mask, zeros_per_point, Q)  # compensate for zero paddings of points
    Q_per_d = tf.tile(tf.expand_dims(Q, -1), [1, 1, 1, D])

    # Compute derivatives and take max and min
    #Method 2: direct derivative formula (convertible to min-max)
    #s0 = tf.reduce_sum(Q, fv_noise)  # n_batches X n_gaussians
    #d_pi = (s0 - n_points * w_per_batch) / (tf.sqrt(w_per_batch) * n_points)
    d_pi_all = tf.expand_dims((Q - batch_w + w_zero_compensate) / (tf.sqrt(batch_w)), -1) # compensate for padding zeros
    d_pi = tf.concat(
        [tf.reduce_max(d_pi_all, axis=1), tf.reduce_sum(d_pi_all, axis=1)], axis=2)

    d_mu_all = Q_per_d * (batch_points - batch_mu) / batch_sig
    d_mu = (1 / (tf.sqrt(w_per_batch_per_d))) * tf.concat(
        [tf.reduce_max(d_mu_all, axis=1), tf.reduce_min(d_mu_all, axis=1), tf.reduce_sum(d_mu_all, axis=1)], axis=2)

    d_sig_all = Q_per_d * ( tf.pow((batch_points - batch_mu) / batch_sig, 2) - 1)
    d_sigma = (1 / (tf.sqrt(2*w_per_batch_per_d))) * tf.concat(
        [tf.reduce_max(d_sig_all, axis=1), tf.reduce_min(d_sig_all, axis=1), tf.reduce_sum(d_sig_all, axis=1)], axis=2)

    if n_original_points is not None:
        effective_n_points = tf.expand_dims(tf.expand_dims(tf.cast(n_original_points, tf.float32), -1), -1)
    else:
        effective_n_points = n_points

    # normalize in the number of points
    d_pi = tf.divide(d_pi, effective_n_points)
    d_mu = tf.divide(d_mu, effective_n_points)
    d_sigma = tf.divide(d_sigma, effective_n_points)

    #Power normaliation
    alpha = 0.5
    d_pi = tf.sign(d_pi) * tf.pow(tf.abs(d_pi), alpha)
    d_mu = tf.sign(d_mu) * tf.pow(tf.abs(d_mu), alpha)
    d_sigma = tf.sign(d_sigma) * tf.pow(tf.abs(d_sigma), alpha)

    # L2 normaliation
    d_pi = tf.nn.l2_normalize(d_pi, axis=1)
    d_mu = tf.nn.l2_normalize(d_mu, axis=1)
    d_sigma = tf.nn.l2_normalize(d_sigma, axis=1)

    if flatten:
        #flatten d_mu and d_sigma
        d_pi = tf.contrib.layers.flatten(tf.transpose(d_pi, perm=[0, 2, 1]))
        d_mu = tf.contrib.layers.flatten(tf.transpose(d_mu,perm=[0,2,1]))
        d_sigma = tf.contrib.layers.flatten(tf.transpose(d_sigma,perm=[0,2,1]))
        fv  = tf.concat([d_pi, d_mu, d_sigma], axis=1)
    else:
        fv = tf.concat([d_pi, d_mu, d_sigma], axis=2)
        fv = tf.transpose(fv, perm=[0, 2, 1])

    return fv