# Source code for ipfml.processing

    """
    Functions to quickly extract reduced information from image
    """

    from PIL import Image
    import random

    import cv2
    from skimage import transform, color
    from scipy import signal
    from numpy.linalg import svd
    from sklearn.decomposition import FastICA, IncrementalPCA

    import numpy as np
    import ipfml.metrics as metrics
    from ipfml.exceptions import NumpyShapeComparisonException

    import os

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

        Args:
            image: PIL Image or Numpy array

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

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> U, s, V = processing.get_LAB_L_SVD(img)
        >>> U.shape
        (200, 200)
        >>> len(s)
        200
        >>> V.shape
        (200, 200)
        """
        L = metrics.get_LAB_L(image)
        return metrics.get_SVD(L)

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

        Args:
            image: PIL Image or Numpy array

        Returns:
            vector of singular values

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> s = processing.get_LAB_L_SVD_s(img)
        >>> len(s)
        200
        """
        L = metrics.get_LAB_L(image)
        return metrics.get_SVD_s(L)

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

        Args:
            image: PIL Image or Numpy array

        Returns:
            U matrix of SVD compression

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> U = processing.get_LAB_L_SVD_U(img)
        >>> U.shape
        (200, 200)
        """
        L = metrics.get_LAB_L(image)
        return metrics.get_SVD_U(L)

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

        Args:
            image: PIL Image or Numpy array

        Returns:
            V matrix of SVD compression

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> V = processing.get_LAB_L_SVD_V(img)
        >>> V.shape
        (200, 200)
        """

        L = metrics.get_LAB_L(image)
        return metrics.get_SVD_V(L)

    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:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> img_mscn = processing.rgb_to_mscn(img)
        >>> img_mscn.shape
        (200, 200)
        """

        # check if PIL image or not
        img_arr = np.array(image)

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

        return metrics.gray_to_mscn(im)

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

        Args:
            image: 3D RGB image Numpy array or PIL RGB image
            nb_bits: optional parameter which indicates the number of bits to keep (default 4)

        Returns:
            2D Numpy array with low bits information kept

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> low_bits_grey_img = processing.rgb_to_grey_low_bits(img, 5)
        >>> low_bits_grey_img.shape
        (200, 200)
        """

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

        return metrics.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:
            image: 3D RGB image Numpy array or PIL RGB image
            nb_bits: optional parameter which indicates the number of bits to keep (default 4)

        Returns:
            2D Numpy array with low bits information kept

        Example:

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> low_bits_Lab_l_img = processing.rgb_to_LAB_L_low_bits(img, 5)
        >>> low_bits_Lab_l_img.shape
        (200, 200)
        """

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

        return metrics.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:
            image: image to convert using this interval of bits value to keep
            interval: (begin, end) of bits values

        Returns:
            2D Numpy array with reduced values

        >>> from PIL import Image
        >>> from ipfml import processing
        >>> img = Image.open('./images/test_img.png')
        >>> bits_Lab_l_img = processing.rgb_to_LAB_L_bits(img, (2, 6))
        >>> bits_Lab_l_img.shape
        (200, 200)
        """

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

        return metrics.get_bits_img(L_block, interval)

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

        Args:
            image: PIL Image or Numpy array
            block: tuple (width, height) representing the size of each dimension of the block
            pil: block type returned as PIL Image (default True)

        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:

        >>> import numpy as np
        >>> from PIL import Image
        >>> from ipfml import processing
        >>> from ipfml import metrics
        >>> image_values = np.random.randint(255, size=(800, 800, 3))
        >>> blocks = 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 = metrics.get_LAB_L(img_l)
        >>> blocks_L = divide_in_blocks(L, (100, 100))
        >>> len(blocks_L)
        4
        >>> blocks_L[0].width
        100
        '''

        blocks = []
        mode = 'RGB'

        # convert in Numpy array
        image_array = np.array(image)

        # check dimension of input image
        if image_array.ndim != 3:
            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

    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:

        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values_1 = np.random.randint(255, size=(800, 800, 3))
        >>> image_values_2 = np.random.randint(255, size=(800, 800, 3))
        >>> merged_image = processing.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]

        if image_array.ndim != 3:
            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
        for i in range(width):
            for j in range(height):

                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
            pil: block type returned as PIL Image (default True)

        Returns:
            Image with rotation applied

        Example:

        >>> from PIL import Image
        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values = Image.open('./images/test_img.png')
        >>> rotated_image = processing.rotate_image(image_values, 90, pil=False)
        >>> rotated_image.shape
        (200, 200, 3)
        """

        mode = 'RGB'
        image_array = np.asarray(image)

        if image_array.ndim != 3:
            mode = 'L'

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

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

    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:

        >>> from PIL import Image
        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values = Image.open('./images/test_img.png')
        >>> mscn_coefficients = processing.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)

            if image.ndim == 2:
                imdist = image
            if image.ndim == 3:
                imdist = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
            else:
                raise ValueError('Incorrect image shape')

        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 svd_reconstruction(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 PIL import Image
        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values = Image.open('./images/test_img.png')
        >>> reconstructed_image = processing.svd_reconstruction(image_values, (100, 200))
        >>> reconstructed_image.shape
        (200, 200)
        """

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

        U, s, V = 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_reconstruction(image, components):
        """Reconstruct an image from SVD compression using specific number of components to use

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

        Returns:
            Reconstructed image

        Example:

        >>> from PIL import Image
        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values = Image.open('./images/test_img.png')
        >>> reconstructed_image = processing.fast_ica_reconstruction(image_values, 25)
        >>> reconstructed_image.shape
        (200, 200)
        """

        lab_img = metrics.get_LAB_L(image)
        lab_img = np.array(lab_img, 'uint8')

        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_reconstruction(image, components, _batch_size=25):
        """Reconstruct an image from SVD compression using specific number of components to use and batch size

        Args:
            image: PIL Image, Numpy array or path of 3D image
            components: Number of components used for reconstruction
            batch_size: Batch size used for learn (default 25)

        Returns:
            Reconstructed image

        Example:

        >>> from PIL import Image
        >>> import numpy as np
        >>> from ipfml import processing
        >>> image_values = Image.open('./images/test_img.png')
        >>> reconstructed_image = processing.ipca_reconstruction(image_values, 20)
        >>> reconstructed_image.shape
        (200, 200)
        """
        lab_img = metrics.get_LAB_L(image)
        lab_img = np.array(lab_img, 'uint8')

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

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

        return restored_image