"""
Module which contains all customs Exceptions used into ipfml package
"""

class NumpyDimensionComparisonException(Exception):
    """
    Numpy dimensions comparison Exception raised if two numpy arrays provided do not have same dimensions
    """

    def __init__(self):
        Exception.__init__(
            self,
            "Numpy arrays provided for comparisons do not have same dimensions"
        )

class NumpyShapeComparisonException(Exception):
    """
    Numpy shape comparison Exception raised if two numpy arrays provided do not have same shape extactly
    """

            self,
            "Numpy arrays provided for comparisons do not have same shape")

"""
Convolution functions to apply on images
"""

# main imports
import numpy as np

def convolution2D(image, kernel, kernel_size=(5, 5)):
    """Apply 2D convolution on image using specific kernel from `ipfml.filters.kernels`

    Args:
        image: 2D image to apply convolution on
        kernel: specific kernel from `ipfml.filters.kernels` to use
        kernel_size: window size to use (default (5, 5))

    Returns:
        2D numpy array obtained from image using kernel

    Example:

    >>> from ipfml.filters.convolution import convolution2D
    >>> from ipfml.filters.kernels import plane_mean
    >>> import numpy as np
    >>> image = np.arange(81).reshape([9, 9])
    >>> convolved_image = convolution2D(image, plane_mean, (3, 3)) 
    >>> convolved_image.shape
    (7, 7)
    """

    img = np.array(image)

    width, height = img.shape

    kernel_width, kernel_height = kernel_size

    if kernel_width % 2 == 0 or kernel_height % 2 == 0:
        raise ValueError("Invalid kernel size, need to be of odd size")

    padding_height = (kernel_width - 1) / 2
    padding_width = (kernel_width - 1) / 2

    img_diff = []
    for i in range(width):

        if i >= padding_width and i < (width - padding_width):

            row_diff = []

            for j in range(height):

                if j >= padding_height and j < (height - padding_height):

                    # pixel in the center of kernel window size, need to extract window from img
                    window = img[int(i - padding_width):int(i + padding_width +
                                                            1),
                                 int(j - padding_height
                                     ):int(j + padding_height + 1)]

                    diff = kernel(window)
                    row_diff.append(diff)

            img_diff.append(row_diff)

    return np.array(img_diff)

"""
Kernel to apply on images using convolution
"""

import sys

def plane_mean(window):
    """Plane mean kernel to use with convolution process on image

    Args:
        window: the window part to use from image

    Returns:
        Normalized residual error from mean plane

    Example:

    >>> window = np.arange(9).reshape([3, 3])
    >>> result = plane_mean(window)
    >>> (result < 0.0001)
    True
    """

    window = np.array(window)

    width, height = window.shape

    # prepare data
    nb_elem = width * height
    xs = [int(i / height) for i in range(nb_elem)]
    ys = [i % height for i in range(nb_elem)]
    zs = np.array(window).flatten().tolist()

    # get residual (error) from mean plane computed
    tmp_A = []
    tmp_b = []

    for i in range(len(xs)):
        tmp_A.append([xs[i], ys[i], 1])
        tmp_b.append(zs[i])

    b = np.matrix(tmp_b).T
    A = np.matrix(tmp_A)

    fit = (A.T * A).I * A.T * b

    errors = b - A * fit
    residual = np.linalg.norm(errors)

    return residual

# return difference between min and max errors
def plane_max_error(window):
    """Plane max error kernel to use with convolution process on image

    Args:

    Returns:
        Difference between max and min error from mean plane

    Example:

    >>> from ipfml.filters.kernels import plane_max_error
    >>> result = plane_max_error(window)
    True
    """

    tmp_A = []
    tmp_b = []

    # get absolute values from errors
    errors = abs(np.array(errors))

    return (errors.max() - errors.min())

def bilateral_diff(window, func=max):
    """Bilaeral difference kernel to use with convolution process on image
       Apply difference pixel to pixel and keep max on min difference before applying mean

    Args:
        func: max or min function to get difference between pixels

    Returns:
        mean of max or min difference of pixels

    Example:

    >>> from ipfml.filters.kernels import bilateral_diff
    >>> result = bilateral_diff(window)
    >>> result
    3.0
    """

    total_row_diff_list = []
    total_col_diff_list = []

        row_diff_list = []
        col_diff_list = []

        for j in range(height):

            diff_row = 0

            if i == 0:
                diff_row = abs(window[i][j] - window[i + 1][j])

            elif i == width - 1:
                diff_row = abs(window[i][j] - window[i - 1][j])

            else:
                diff1 = abs(window[i][j] - window[i - 1][j])
                diff2 = abs(window[i][j] - window[i + 1][j])
                diff_row = func(diff1, diff2)

            if j == 0:
                diff_col = abs(window[i][j] - window[i][j + 1])

            elif j == height - 1:
                diff_col = abs(window[i][j] - window[i][j - 1])

            else:
                diff1 = abs(window[i][j] - window[i][j - 1])
                diff2 = abs(window[i][j] - window[i][j + 1])
                diff_col = func(diff1, diff2)

            row_diff_list.append(diff_row)
            col_diff_list.append(diff_col)

        total_row_diff_list.append(sum(row_diff_list) / len(row_diff_list))
        total_col_diff_list.append(sum(col_diff_list) / len(col_diff_list))

    row_diff = sum(total_row_diff_list) / len(total_row_diff_list)
    col_diff = sum(total_col_diff_list) / len(total_col_diff_list)

    return func(row_diff, col_diff)

"""
Noise filters to apply on images
"""

import random

def _normalise(x):

    if isinstance(x, np.ndarray):
        return np.array(list(map(_normalise, x)))

    if x > 255:
        return 255
    if x < 0:
        return 0

    return x

def _global_noise_filter(image, generator, updator, identical=False):
    """White noise filter to apply on image

    Args:
        image: image used as input (2D or 3D image representation)
        generator: lambda function used to generate random numpy array with specific distribution
        updator: lambda function used to update pixel value
        identical: keep or not identical noise distribution for each canal if RGB Image (default False)

    Returns:
        2D Numpy array with specified noise applied

    Example:

    >>> from ipfml.filters.noise import _global_noise_filter as gf
    >>> image = np.random.uniform(0, 255, 10000).reshape((100, 100))
    >>> generator = lambda h, w: np.random.uniform(-0.5, 0.5, (h, w))
    >>> n = 10
    >>> k = 0.2
    >>> updator = lambda x, noise: x + n * k * noise
    >>> noisy_image = gf(image, generator, updator)
    >>> noisy_image.shape
    (100, 100)
    """

    image_array = np.asarray(image)
    nb_chanel = 1

    if image_array.ndim != 3:
        height, width = image_array.shape
    else:
        height, width, nb_chanel = image_array.shape

    if nb_chanel == 1 or identical:
        noise_filter = generator(height, width)

    # final output numpy array
    output_array = []

    # check number of chanel
    if nb_chanel == 1:

        noisy_image = np.array(list(map(updator, image_array, noise_filter)))

        # normalise values
        return np.array(list(map(_normalise, noisy_image)), 'uint8')

    else:
        # final output numpy array
        output_array = []

        for chanel in range(0, nb_chanel):

            # getting flatten information from image and noise
            image_array_chanel = image_array[:, :, chanel]

            # redefine noise if necessary
            if not identical:
                noise_filter = generator(height, width)

            # compute new pixel value
            # x + n * k * white_noise_filter[i] as example
            noisy_image = np.array(
                list(map(updator, image_array_chanel, noise_filter)))

            # normalise values
            noisy_image = np.array(list(map(_normalise, noisy_image)), 'uint8')

            # in order to concatenate output array
            noisy_image = noisy_image[:, :, np.newaxis]

            # append new chanel
            output_array.append(noisy_image)

        # concatenate RGB image
        output_array = np.concatenate(output_array, axis=2)

        return output_array

def white_noise(image,
                n,
                identical=False,
                distribution_interval=(-0.5, 0.5),
                k=0.2):

    Args:
        n: used to set importance of noise [1, 999]
        distribution_interval: set the distribution interval of normal law distribution (default (-0.5, 0.5))
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.2)

    Returns:
        2D Numpy array with white noise applied

    Example:

    >>> from ipfml.filters.noise import white_noise
    >>> noisy_image = white_noise(image, 10)
    (100, 100)
    """

    a, b = distribution_interval
    generator = lambda h, w: np.random.uniform(a, b, (h, w))

    updator = lambda x, noise: x + n * k * noise

    return _global_noise_filter(image, generator, updator, identical)

def gaussian_noise(image,
                   n,
                   identical=False,
                   distribution_interval=(0, 1),
                   k=0.1):
    """Gaussian noise filter to apply on image

    Args:
        distribution_interval: set the distribution interval of normal law distribution (default (0, 1))
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.1)

    Returns:
        2D Numpy array with gaussian noise applied

    Example:

    >>> from ipfml.filters.noise import gaussian_noise
    >>> noisy_image = gaussian_noise(image, 10)
    (100, 100)
    """

    generator = lambda h, w: np.random.normal(a, b, (h, w))

def laplace_noise(image,
                  n,
                  identical=False,
                  distribution_interval=(0, 1),
                  k=0.1):
    """Laplace noise filter to apply on image

    Args:

    Returns:
        2D Numpay array with Laplace noise applied

    Example:

    >>> from ipfml.filters.noise import laplace_noise
    >>> noisy_image = laplace_noise(image, 10)
    (100, 100)
    """

    generator = lambda h, w: np.random.laplace(a, b, (h, w))

def cauchy_noise(image,
                 n,
                 identical=False,
                 distribution_interval=(0, 1),
                 k=0.0002):
    """Cauchy noise filter to apply on image

    Args:
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.0002)

    Returns:
        2D Numpy array with Cauchy noise applied

    Example:

    >>> from ipfml.filters.noise import cauchy_noise
    >>> noisy_image = cauchy_noise(image, 10)
    (100, 100)
    """

    generator = lambda h, w: np.random.standard_cauchy((h, w))

def log_normal_noise(image,
                     n,
                     identical=False,
                     distribution_interval=(0, 1),
                     k=0.05):
    """Log-normal noise filter to apply on image

    Args:
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.05)

    Returns:
        2D Numpy array with Log-normal noise applied

    Example:

    >>> from ipfml.filters.noise import log_normal_noise
    >>> noisy_image = log_normal_noise(image, 10)
    (100, 100)
    """

    generator = lambda h, w: np.random.lognormal(a, b, (h, w))

def mut_white_noise(image,
                    n,
                    identical=False,
                    distribution_interval=(0, 1),
                    k=0.002):
    """Multiplied White noise filter to apply on image

    Args:
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.002)

    Returns:
        2D Numpy array with multiplied white noise applied

    Example:

    >>> from ipfml.filters.noise import mut_white_noise
    >>> noisy_image = mut_white_noise(image, 10)
    (100, 100)
    """

    min_value = 1 - (k / 2)
    max_value = 1 + (k / 2)

    generator = lambda h, w: min_value + (np.random.uniform(a, b, (h, w)) * (max_value - min_value))

    updator = lambda x, noise: x * pow(noise, n)

def salt_pepper_noise(image, n, identical=False, p=0.1, k=0.5):
    """Pepper salt noise filter to apply on image

    Args:
        p: probability to increase pixel value otherwise decrease it
        k: variable that specifies the amount of noise to be taken into account in the output image (default 0.5)

    Returns:
        2D Numpy array with salt and pepper noise applied

    Example:

    >>> from ipfml.filters.noise import salt_pepper_noise
    >>> noisy_image = salt_pepper_noise(image, 10)
    (100, 100)
    """

    def _generator(h, w):

        x = w * h
        nb_elem = int(p * x)

        elements = np.full(x, 0)
        elements[0:nb_elem] = 1
        np.random.shuffle(elements)

        return elements.reshape(h, w)

    image_array = np.array(image)
    if image_array.ndim == 3:
        height, width, _ = image_array.shape
    else:

    # need same random variable for each pixel value if identical
    if identical:
        gen = np.random.uniform(0, 1, height * width)
        for i in range(2):
            gen = np.insert(gen, 0, gen)
        gen = iter(gen)

    # here noise variable is boolean to update or not pixel value
    def _updator(x, noise):

        # apply specific changes to each value of 1D array
        if isinstance(x, np.ndarray):
            return np.array(list(map(_updator, x, noise)))

        # probabilty to increase or decrease pixel value
        if identical:
            rand = next(gen)
        else:
            rand = random.uniform(0, 1)

        if noise:
            if rand > 0.5:
                return x + n * k
            else:
                return x - n * k
        else:
            return x

    return _global_noise_filter(image, _generator, _updator, identical)

"""
Full-reference Image Quality Assessment (FR-IQA) methods
"""

# computations imports
from skimage.measure import compare_ssim, compare_psnr

# ipfml imports
from ipfml.exceptions import NumpyShapeComparisonException

def _prepare_arrays(img_true, img_test, p_dtype='float32'):
    """
    Prepare image data

    Raises:
        NumpyShapeComparisonException: if shape of images are not the same
    """

    img_true = np.asarray(img_true, dtype=p_dtype)
    img_test = np.asarray(img_test, dtype=p_dtype)

    if img_true.shape != img_test.shape:
        raise NumpyShapeComparisonException

    return img_true, img_test

def mse(img_true, img_test):
    """Returns Mean-Squared Error score between two Numpy arrays

    Args:
        img_true: Image, numpy array of any dimension
        img_test: Image, numpy array of any dimension

    Returns:
        Computed MSE score

    Raises:

    Example:
        >>> from ipfml.iqa import fr
        >>> import numpy as np
        >>> arr1 = np.arange(10)
        >>> arr2 = np.arange(5, 15)
        >>> mse_score = fr.mse(arr1, arr2)
        >>> mse_score
        25.0
    """

    img_true, img_test = _prepare_arrays(img_true, img_test)

    return np.mean(np.square(img_true - img_test), dtype=np.float64)

def rmse(img_true, img_test):
    """Returns Root Mean-Squared Error score between two Numpy arrays

    Args:

    Returns:
        Computed RMSE score

    Raises:

    Example:
        >>> rmse_score = fr.rmse(arr1, arr2)
        >>> rmse_score
        5.0
    """

    return np.sqrt(mse(img_true, img_test))

def mae(img_true, img_test):
    """Returns Mean Absolute Error between two Numpy arrays

    Args:

    Returns:
        Computed MAE score

    Raises:

    Example:
        >>> mae_score = fr.mae(arr1, arr2)
        >>> mae_score
        5.0
    """

    return np.mean(np.absolute(img_true - img_test), dtype=np.float64)

def psnr(img_true, img_test):
    """Returns the computed Peak Signal to Noise Ratio (PSNR) between two images

    Args:

    Returns:
        Computed PSNR score

    Example:
        >>> arr1 = np.arange(100).reshape(10, 10)
        >>> arr2 = np.arange(5, 105).reshape(10, 10)
        >>> psnr_score = fr.psnr(arr1, arr2)
        >>> int(psnr_score)
        34
    """

    img_true, img_test = _prepare_arrays(img_true, img_test, 'uint8')

    return compare_psnr(img_true, img_test)

def ssim(img_true, img_test):
    """Returns the computed Structural Similarity (SSIM) between two images

    Args:

    Returns:
        Computed SSIM score

    Example:
        >>> ssim_score = fr.ssim(arr1, arr2)
        >>> int(ssim_score)
        0
    """

    if img_true.ndim == 3:
        return compare_ssim(img_true, img_test, multichannel=True)
    else:
        return compare_ssim(img_true, img_test)

def ms_ssim(img_true, img_test):
    """
    Implemented later..
    """
    pass

def vif(img_true, img_test):
    """
    """
    pass

"""
Contains all metrics used for model results comparisons
"""

def coefficient_of_determination(y, predicted):
    """Returns the coefficient of determination between `y` and `y_predicted` values

    Args:
        y: array of reference values
        predicted: array of predicted values

    Returns:
        coefficient of determination

    Example:

    >>> from ipfml import metrics
    >>> y = np.arange(10)
    >>> predicted = np.arange(10) * 1.05
    >>> coeff = metrics.coefficient_of_determination(y, predicted)
    >>> int(coeff)
    1
    """

    y = np.asarray(y)
    predicted = np.asarray(predicted)

    y_mean = y.mean()

    numerator_sum = 0
    denominator_sum = 0

    for id_val, val in enumerate(y):
        numerator_sum += (predicted[id_val] - y_mean) * (
            predicted[id_val] - y_mean)
        denominator_sum += (val - y_mean) * (val - y_mean)

    return numerator_sum / denominator_sum

"""
Functions for image compression and extraction
"""

# image processing imports
from numpy.linalg import svd

def get_SVD(image):
    """Transforms Image using SVD compression

    Args:
        image: image to convert into SVD compression

    Return:
        U, s, V obtained from SVD compression

    Usage:

    >>> from PIL import Image
    >>> from ipfml.processing import compression
    >>> img = Image.open('./images/test_img.png')
    >>> U, s, V = compression.get_SVD(img)
    >>> U.shape
    (200, 200, 3)
    >>> len(s)
    200
    >>> V.shape
    (200, 3, 3)
    """
    return svd(image, full_matrices=False)

def get_SVD_s(image):
    """Transforms Image into SVD and returns only 's' part

    Args:
        image: image to convert

    Returns:
        vector of singular values obtained from SVD compression

    Usage:

    >>> s = compression.get_SVD_s(img)
    >>> len(s)
    200
    """
    U, s, V = svd(image, full_matrices=False)
    return s

def get_SVD_U(image):
    """Transforms Image into SVD and returns only 'U' part

    Args:

    Returns:
        U matrix from SVD compression

    Usage:

    >>> U = compression.get_SVD_U(img)
    """

    return U

def get_SVD_V(image):
    """Transforms Image into SVD and returns only 'V' part

    Args:

    Returns:
        V matrix obtained from SVD compression

    Usage :

    >>> V = compression.get_SVD_V(img)
    (200, 3, 3)
    """

    return V

"""
All movements that can be applied on image such as rotations, fusions, flips
"""

from PIL import Image
from skimage import transform as sk_transform

def fusion_images(images, pil=True):
    '''Fusion array of images into single image

    Args:
        images: array of images (PIL Image or Numpy array)
        pil: block type returned as PIL Image (default True)

    Returns:
        merged image from array of images

    Raises:
        ValueError: if `images` is not an array or is empty
        NumpyShapeComparisonException: if `images` array contains images with different shapes

    Example:

    >>> from ipfml.processing import movement
    >>> image_values_1 = np.random.randint(255, size=(800, 800, 3))
    >>> image_values_2 = np.random.randint(255, size=(800, 800, 3))
    >>> merged_image = movement.fusion_images([image_values_1, image_values_2], pil=False)
    >>> merged_image.shape
    (800, 800, 3)
    '''

    mode = 'RGB'
    dim = 1

    if len(images) == 0:
        raise ValueError('Empty array of images provided...')

    # convert image in numpy array (perhaps not necessary)
    images = [np.asarray(img) for img in images]
    image_array = images[0]

        mode = 'L'
        width, height = image_array.shape
    else:
        width, height, dim = image_array.shape

    # raise exception if all images do not have same shape
    if not np.array([image_array.shape == a.shape for a in images]).all():
        raise NumpyShapeComparisonException()

    if dim == 1:
        image_mean = np.empty([width, height])
    else:
        image_mean = np.empty([width, height, dim])

    nb_images = len(images)

    # construction of mean image from rotation

            if dim == 1:
                grey_value = 0

                # for each image we merge pixel values
                for img in images:
                    grey_value += img[i][j]

                image_mean[i][j] = grey_value / nb_images

            else:
                for k in range(dim):
                    canal_value = 0

                    # for each image we merge pixel values
                    for img in images:
                        canal_value += img[i][j][k]

                    image_mean[i][j][k] = canal_value / nb_images

    image_mean = np.array(image_mean, 'uint8')

    if pil:
        return Image.fromarray(image_mean, mode)
    else:
        return image_mean

def rotate_image(image, angle=90, pil=True):
    """Rotate image using specific angle

    Args:
        image: PIL Image or Numpy array
        angle: Angle value of the rotation

    Returns:
        Image with rotation applied

    Example:

    >>> image_values = Image.open('./images/test_img.png')
    >>> rotated_image = movement.rotate_image(image_values, 90, pil=False)
    >>> rotated_image.shape
    """

        mode = 'L'

    rotated_image = np.array(
        sk_transform.rotate(image_array, angle) * 255, 'uint8')

    if pil:
        return Image.fromarray(rotated_image, mode)
    else:
        return rotated_image

"""
Functions for reconstruction process of image using reduction/compression methods
"""

from numpy.linalg import svd as np_svd
from sklearn.decomposition import FastICA, IncrementalPCA

from ipfml.processing import transform

def svd(image, interval):
    """Reconstruct an image from SVD compression using specific interval of Singular Values

    Args:
        image: PIL Image, Numpy array or path of 3D image
        interval: Interval used for reconstruction

    Returns:
        Reconstructed image

    Example:

    >>> from ipfml.processing import reconstruction
    >>> reconstructed_image = reconstruction.svd(image_values, (100, 200))
    >>> reconstructed_image.shape
    (200, 200)
    """

    begin, end = interval
    lab_img = transform.get_LAB_L(image)
    lab_img = np.array(lab_img, 'uint8')

    U, s, V = np_svd(lab_img, full_matrices=True)

    # reconstruction using specific interval
    smat = np.zeros((end - begin, end - begin), dtype=complex)
    smat[:, :] = np.diag(s[begin:end])
    output_img = np.dot(U[:, begin:end], np.dot(smat, V[begin:end, :]))

    return output_img

def fast_ica(image, components):
    """Reconstruct an image from Fast ICA compression using specific number of components to use

    Args:
        components: Number of components used for reconstruction

    Returns:

    Example:

    >>> reconstructed_image = reconstruction.fast_ica(image_values, 25)
    (200, 200)
    """

    ica = FastICA(n_components=50)
    # run ICA on image
    ica.fit(lab_img)
    # reconstruct image with independent components
    image_ica = ica.fit_transform(lab_img)
    restored_image = ica.inverse_transform(image_ica)

    return restored_image

def ipca(image, components, _batch_size=25):
    """Reconstruct an image from IPCA compression using specific number of components to use and batch size

    Args:
        batch_size: Batch size used for learn (default 25)

    Returns:

    Example:

    >>> reconstructed_image = reconstruction.ipca(image_values, 20)
    (200, 200)
    """

    transformer = IncrementalPCA(
        n_components=components, batch_size=_batch_size)

    transformed_image = transformer.fit_transform(lab_img)
    restored_image = transformer.inverse_transform(transformed_image)

"""
All segmentation methods applied on images
"""

def divide_in_blocks(image, block_size, pil=True):
    '''Divide image into equal size blocks

    Args:
        block: tuple (width, height) representing the size of each dimension of the block

    Returns:
        list containing all 2D Numpy blocks (in RGB or not)

    Raises:
        ValueError: If `image_width` or `image_height` are not compatible to produce correct block sizes

    Example:

    >>> from ipfml.processing import transform, segmentation
    >>> image_values = np.random.randint(255, size=(800, 800, 3))
    >>> blocks = segmentation.divide_in_blocks(image_values, (20, 20))
    >>> len(blocks)
    1600
    >>> blocks[0].width
    20
    >>> blocks[0].height
    20
    >>> img_l = Image.open('./images/test_img.png')
    >>> L = transform.get_LAB_L(img_l)
    >>> blocks_L = segmentation.divide_in_blocks(L, (100, 100))
    >>> len(blocks_L)
    4
    >>> blocks_L[0].width
    100
    '''

    blocks = []

    # convert in Numpy array

    # check dimension of input image
        mode = 'L'
        image_width, image_height = image_array.shape
    else:
        image_width, image_height, _ = image_array.shape

    # check size compatibility
    width, height = block_size

    if (image_width % width != 0):
        raise ValueError("Width size issue, block size not compatible")

    if (image_height % height != 0):
        raise ValueError("Height size issue, block size not compatible")

    nb_block_width = image_width / width
    nb_block_height = image_height / height

    for i in range(int(nb_block_width)):

        begin_x = i * width

        for j in range(int(nb_block_height)):

            begin_y = j * height

            # getting sub block information
            current_block = image_array[begin_x:(begin_x + width), begin_y:(
                begin_y + height)]

            if pil:
                blocks.append(
                    Image.fromarray(current_block.astype('uint8'), mode))
            else:
                blocks.append(current_block)

    return blocks

"""
Functions which can be used to extract information from image or reduce it
"""

import os

from scipy import misc
from sklearn import preprocessing
from skimage import io, color
import cv2

from ipfml.processing import compression

def get_LAB(image):
    """Transforms RGB  Image into Lab

    Args:

    Returns:
        Lab information

    Usage:

    >>> from ipfml.processing import transform
    >>> Lab = transform.get_LAB(img)
    >>> Lab.shape
    """

    return color.rgb2lab(image)

def get_LAB_L(image):
    """Transforms RGB Image into Lab and returns L

    Args:

    Returns:
        The L chanel from Lab information

    >>> L = transform.get_LAB_L(img)
    >>> L.shape
    (200, 200)
    """

    lab = get_LAB(image)
    return lab[:, :, 0]

def get_LAB_a(image):
    """Transforms RGB Image into LAB and returns a

    Args:

    Returns:
        The a chanel from Lab information

    Usage:

    >>> a = transform.get_LAB_a(img)
    >>> a.shape
    (200, 200)
    """

    return lab[:, :, 1]

def get_LAB_b(image):
    """Transforms RGB Image into LAB and returns b

    Args:

    Returns:
        The b chanel from Lab information

    Usage :

    >>> b = transform.get_LAB_b(img)
    >>> b.shape
    (200, 200)
    """

    return lab[:, :, 2]

def get_XYZ(image):
    """Transforms RGB Image into XYZ

    Args:

    Returns:
        XYZ information obtained from transformation

    Usage:

    >>> transform.get_XYZ(img).shape
    """

    return color.rgb2xyz(image)

def get_XYZ_X(image):
    """Transforms RGB Image into XYZ and returns X

    Args:

    Returns:
        The X chanel from XYZ information

    Usage:

    >>> x = transform.get_XYZ_X(img)
    >>> x.shape
    (200, 200)
    """

    xyz = color.rgb2xyz(image)
    return xyz[:, :, 0]

def get_XYZ_Y(image):
    """Transforms RGB Image into XYZ and returns Y

    Args:

    Returns:
        The Y chanel from XYZ information

    Usage:

    >>> y = transform.get_XYZ_Y(img)
    >>> y.shape
    (200, 200)
    """

    return xyz[:, :, 1]

def get_XYZ_Z(image):
    """Transforms RGB Image into XYZ and returns Z

    Args:

    Returns:
        The Z chanel from XYZ information

    Raises:
        ValueError: If `nb_bits` has unexpected value. `nb_bits` needs to be in interval [1, 8].

    Usage:

    >>> z = transform.get_XYZ_Z(img)
    >>> z.shape
    (200, 200)
    """

    return xyz[:, :, 2]

def get_low_bits_img(image, nb_bits=4):
    """Returns Image or Numpy array with data information reduced using only low bits

    Args:
        nb_bits: optional parameter which indicates the number of bits to keep

    Returns:
        Numpy array with reduced values

    Usage:

    >>> low_bits_img = transform.get_low_bits_img(img, 5)
    >>> low_bits_img.shape
    """

    if nb_bits <= 0:
        raise ValueError(
            "unexpected value of number of bits to keep. @nb_bits needs to be positive and greater than 0."
        )

    if nb_bits > 8:
            "Unexpected value of number of bits to keep. @nb_bits needs to be in interval [1, 8]."
        )

    img_arr = np.array(image)

    bits_values = sum([pow(2, i - 1) for i in range(1, nb_bits + 1)])

    return img_arr & bits_values

def get_bits_img(image, interval):
    """Returns only bits specified into the interval

    Args:
        image: image to convert using this interval of bits value to keep
        interval: (begin, end) of bits values

    Returns:

    Raises:
        ValueError: If min value from interval is not >= 1.
        ValueError: If max value from interval is not <= 8.
        ValueError: If min value from interval >= max value.

    Usage:

    >>> bits_img = transform.get_bits_img(img, (2, 5))
    >>> bits_img.shape
    """

    if begin < 1:
            "Unexpected value of interval. Interval min value needs to be >= 1."
        )

    if end > 8:
            "Unexpected value of interval. Interval min value needs to be <= 8."
        )

    if begin >= end:
        raise ValueError("Unexpected interval values order.")

    bits_values = sum([pow(2, i - 1) for i in range(begin, end + 1)])

def gray_to_mscn(image):
    """Convert Grayscale Image into Mean Subtracted Contrast Normalized (MSCN)

    Args:
        image: grayscale image

    Returns:
        MSCN matrix obtained from transformation

    Usage:

    >>> img = transform.get_LAB_L(img)
    >>> img_mscn = transform.gray_to_mscn(img)
    >>> img_mscn.shape
    (200, 200)
    """

    s = 7 / 6
    blurred = cv2.GaussianBlur(image, (7, 7),
                               s)  # apply gaussian blur to the image
    blurred_sq = blurred * blurred
    sigma = cv2.GaussianBlur(image * image, (7, 7), s)
    sigma = abs(sigma - blurred_sq)**0.5
    sigma = sigma + 1.0 / 255  # avoid DivideByZero Exception
    mscn = (image - blurred) / sigma  # MSCN(i, j) image

    return mscn

def rgb_to_mscn(image):
    """Convert RGB Image into Mean Subtracted Contrast Normalized (MSCN)

    Args:
        image: 3D RGB image Numpy array or PIL RGB image

    Returns:
        2D Numpy array with MSCN information

    Example:

    >>> img_mscn = transform.rgb_to_mscn(img)
    (200, 200)
    """

    # check if PIL image or not

    # convert rgb image to gray
    im = np.array(color.rgb2gray(img_arr) * 255, 'uint8')

    return gray_to_mscn(im)

def get_mscn_coefficients(image):
    """Compute the Mean Substracted Constrast Normalized coefficients of an image

    Args:
        image: PIL Image, Numpy array or path of image

    Returns:
        MSCN coefficients

    Raises:
        FileNotFoundError: If `image` is set as str path and image was not found
        ValueError: If `image` numpy shape are not correct

    Example:

    >>> mscn_coefficients = transform.get_mscn_coefficients(image_values)
    >>> mscn_coefficients.shape
    (200, 200)
    """

    if isinstance(image, str):
        if os.path.exists(image):
            # open image directly as grey level image
            imdist = cv2.imread(image, 0)
        else:
            raise FileNotFoundError('Image not found in your system')

    elif isinstance(image, np.ndarray):
        # convert if necessary to grey level numpy array
        if image.ndim == 2:
            imdist = image
        if image.ndim == 3:
            imdist = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        else:
            raise ValueError('Incorrect image shape')
    else:
        # if PIL Image
        image = np.asarray(image)

        else:

    imdist = imdist.astype(np.float64)
    imdist = imdist / 255.0

    # calculating MSCN coefficients
    mu = cv2.GaussianBlur(
        imdist, (7, 7), 7 / 6, borderType=cv2.BORDER_CONSTANT)
    mu_sq = mu * mu
    sigma = cv2.GaussianBlur(
        imdist * imdist, (7, 7), 7 / 6, borderType=cv2.BORDER_CONSTANT)
    sigma = np.sqrt(abs((sigma - mu_sq)))
    structdis = (imdist - mu) / (sigma + 1)
    return structdis

def get_LAB_L_SVD(image):
    """Returns Singular values from LAB L Image information

    Args:

    Returns:
        U, s, V information obtained from SVD compression using Lab

    Example:

    >>> U, s, V = transform.get_LAB_L_SVD(img)
    (200, 200)
    >>> len(s)
    200
    (200, 200)
    """
    L = get_LAB_L(image)
    return compression.get_SVD(L)

def get_LAB_L_SVD_s(image):
    """Returns s (Singular values) SVD from L of LAB Image information

    Args:

    Returns:
        vector of singular values

    Example:

    >>> s = transform.get_LAB_L_SVD_s(img)
    >>> len(s)
    200
    """
    return compression.get_SVD_s(L)

def get_LAB_L_SVD_U(image):
    """Returns U SVD from L of LAB Image information

    Args:

    Returns:
        U matrix of SVD compression

    Example:

    >>> U = transform.get_LAB_L_SVD_U(img)
    (200, 200)
    """
    return compression.get_SVD_U(L)

def get_LAB_L_SVD_V(image):
    """Returns V SVD from L of LAB Image information

    Args:

    Returns:
        V matrix of SVD compression

    Example:

    >>> V = transform.get_LAB_L_SVD_V(img)
    (200, 200)
    """

    return compression.get_SVD_V(L)

def rgb_to_grey_low_bits(image, nb_bits=4):
    """Convert RGB Image into grey image using only 4 low bits values

    Args:
        nb_bits: optional parameter which indicates the number of bits to keep (default 4)

    Returns:
        2D Numpy array with low bits information kept

    Example:

    >>> low_bits_grey_img = transform.rgb_to_grey_low_bits(img, 5)
    >>> low_bits_grey_img.shape
    (200, 200)
    """

    grey_block = np.array(color.rgb2gray(img_arr) * 255, 'uint8')

    return get_low_bits_img(grey_block, nb_bits)

def rgb_to_LAB_L_low_bits(image, nb_bits=4):
    """Convert RGB Image into Lab L channel image using only 4 low bits values

    Args:

    Returns:

    Example:

    >>> low_bits_Lab_l_img = transform.rgb_to_LAB_L_low_bits(img, 5)
    >>> low_bits_Lab_l_img.shape
    (200, 200)
    """

    L_block = np.asarray(get_LAB_L(image), 'uint8')

    return get_low_bits_img(L_block, nb_bits)

def rgb_to_LAB_L_bits(image, interval):
    """Returns only bits from LAB L canal specified into the interval

    Args:

    Returns:
        2D Numpy array with reduced values

    >>> bits_Lab_l_img = transform.rgb_to_LAB_L_bits(img, (2, 6))
    >>> bits_Lab_l_img.shape
    (200, 200)
    """

    return get_bits_img(L_block, interval)

"""
Utils functions of ipfml package (normalization, integral...)
"""

import math
import sys

# computation imports
from scipy.integrate import simps

def normalize_arr(arr):
    """Normalize data of 1D array shape

    Args:
        arr: array data of 1D shape

    Returns:
        Normalized 1D array

    Example:

    >>> from ipfml import utils
    >>> arr = np.arange(5)
    >>> arr_normalized = utils.normalize_arr(arr)
    >>> arr_normalized[1]
    0.1
    """

    output_arr = []
    sum_value = sum(arr)

    for v in arr:
        # add of epsilon value in order to avoid Zero Division
        output_arr.append(v / sum_value)

    return output_arr

def normalize_arr_with_range(arr, min, max):
    '''Normalize data of 1D array shape

    Args:

    Returns:
        Normalized 1D Numpy array

    Example:

    >>> arr = np.arange(11)
    >>> arr_normalized = utils.normalize_arr_with_range(arr, 0, 20)
    0.05
    '''

        output_arr.append((v - min) / (max - min + sys.float_info.epsilon))

def normalize_2D_arr(arr):
    """Return array normalize from its min and max values

    Args:
        arr: 2D Numpy array

    Returns:
        Normalized 2D Numpy array

    Example:

    >>> img_normalized = utils.normalize_2D_arr(img_mscn)
    >>> img_normalized.shape
    (200, 200)
    """

    # getting min and max value from 2D array
    max_value = arr.max(axis=1).max()
    min_value = arr.min(axis=1).min()

    # normalize each row
    _, height = arr.shape

    for row_index in range(0, height):
        values = arr[row_index, :]
        output_array.append(
            normalize_arr_with_range(values, min_value, max_value))

    return np.asarray(output_array)

def integral_area_trapz(y_values, dx):
    """Returns area under curves from provided data points using Trapezium rule

    Args:
        y_values: y values of curve
        dx: number of unit for x axis

    Returns:
        Area under curves obtained from these points

    Example:

    >>> y_values = np.array([5, 20, 4, 18, 19, 18, 7, 4])
    >>> area = utils.integral_area_trapz(y_values, dx=5)
    >>> area
    452.5
    """

    return np.trapz(y_values, dx=dx)

def integral_area_simps(y_values, dx):
    """Returns area under curves from provided data points using Simpsons rule

    Args:

    Returns:

    Example:

    >>> area = utils.integral_area_simps(y_values, dx=5)
    >>> area
    460.0
    """

    return simps(y_values, dx=dx)

def get_indices_of_highest_values(arr, n):
    """Returns indices of n highest values from list or 1D numpy array

    Args:
        arr: List of numpy array
        n: number of highest elements wanted

    Returns:
        `n` indices of highest values

    Example:

    >>> arr = np.arange(10)
    >>> indices = utils.get_indices_of_highest_values(arr, 2)
    >>> indices
    array([9, 8])
    """
    return np.array(arr).argsort()[-n:][::-1]

def get_indices_of_lowest_values(arr, n):

    Args:

    Returns:

    Example:

    >>> indices = utils.get_indices_of_lowest_values(arr, 2)
    array([0, 1])
    """
    return np.array(arr).argsort()[::-1][-n:][::-1]

def get_entropy(arr):
    """Returns the computed entropy from arr

    Args:
        arr: numpy array

    Returns:
        entropy score computed

    Example:

    >>> entropy = utils.get_entropy(arr)
    >>> int(entropy)
    0
    """

    arr = np.array(arr)
    eigen_values = []
    sum_eigen_values = (arr * arr).sum()

    for val in arr:
        eigen_values.append(val * val)

    v = []

    for val in eigen_values:
        v.append(val / sum_eigen_values)

    entropy = 0

    for val in v:
        if val > 0:
            entropy += val * math.log(val)

    entropy *= -1

    entropy /= math.log(len(v))

    return entropy

def get_entropy_without_i(arr, i):
    """Returns the computed entropy from arr without contribution of i

    Args:
        i: column index

    Returns:

    Example:

    >>> entropy = utils.get_entropy_without_i(arr, 3)
    0
    """

    arr = np.array([v for index, v in enumerate(arr) if index != i])

    return get_entropy(arr)

def get_entropy_contribution_of_i(arr, i):
    """Returns the entropy contribution i column

    Args:

    Returns:
        entropy contribution score computed

    Example:

    >>> entropy = utils.get_entropy_contribution_of_i(arr, 3)
    0
    """

    return get_entropy(arr) - get_entropy_without_i(arr, i)