import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pydicom as dicom
from skimage.morphology import erosion, dilation, binary_closing, binary_opening, binary_erosion, area_opening
from skimage.morphology import disk
from skimage.morphology import square
from skimage.filters import median
from scipy.stats import norm
from skimage import color, io, measure, img_as_ubyte, img_as_float
from skimage.filters import threshold_otsu
from scipy.spatial import distance
from skimage.transform import rotate
from skimage.transform import SimilarityTransform
from skimage.transform import warp
from skimage.transform import matrix_transform
import glob
from sklearn.datasets import load_breast_cancer
from sklearn.decomposition import PCA
import random
from skimage.filters import prewitt
from skimage.filters import prewitt_v
import SimpleITK as sitk
import seaborn as sns
import os
import sklearn.datasets
from LDA import LDA
from skimage.transform import rescale
from math import pi
from scipy import ndimage

def candy_classification_e2025():
    candy_image_in = "data/candy/candy.jpg"
    candy_image = io.imread(candy_image_in)

    debug = False

    # Convert to grayscale
    gray_candy = color.rgb2gray(candy_image)
    if debug:
        plt.imshow(gray_candy)
        plt.title("Gray candy image")
        plt.show()

    if debug:
        # Show histogram
        plt.hist(gray_candy.ravel(), bins=256)
        plt.title("Histogram of gray candy image")
        plt.show()

    # Binarize the image but do not use Otsus threshold
    thresh = threshold_otsu(gray_candy)
    print(f"Answer: Otsu threshold: {thresh:.2f} : {thresh * 255:.0f}")

    thresh = 0.6
    binary_candy = gray_candy < thresh
    if debug:
        plt.imshow(binary_candy)
        plt.title("Binary candy image")
        plt.show()

    num_foreground = np.sum(binary_candy)
    print(f"Answer: Number of foreground pixels: {num_foreground}")

    # Close with small footprint
    footprint = disk(3)
    closing = binary_closing(binary_candy, footprint)
    if debug:
        plt.imshow(closing)
        plt.title("Closed candy image")
        plt.show()

    footprint = disk(6)
    erosion = binary_erosion(closing, footprint)
    if debug:
        plt.imshow(erosion)
        plt.title("Eroded candy image")
        plt.show()

    label_img = measure.label(erosion)
    n_labels = label_img.max()
    print(f"Answer: Number of labels after erosion: {n_labels}")

    # Show label image with a mixed color map
    label_img_color = color.label2rgb(label_img, bg_label=0)
    if debug:
        plt.imshow(label_img_color)
        plt.title("Labeled candy image")
        plt.show()

    # Sample the GRG values in the original image based on the labels
    # store the mean and standard deviation of each label
    region_props = measure.regionprops(label_img, intensity_image=candy_image)
    n_regions = len(region_props)

    mean_vals = []
    std_vals = []
    areas = []
    perimeters = []
    circularities = []
    for i in range(n_regions):
        mean_vals.append(region_props[i].intensity_mean)
        std_vals.append(region_props[i].intensity_std)
        area = region_props[i].area
        perimeter = region_props[i].perimeter
        areas.append(area)
        perimeters.append(perimeter)
        circularity =  (2 * np.sqrt(pi * area)) / perimeter
        circularities.append(circularity)

    # Compute the 75 fractile of the average B intensity values of all regions
    mean_vals = np.array(mean_vals)
    std_vals = np.array(std_vals)

    fractile_75 = np.percentile(mean_vals[:, 2], 75)
    print(f"Answer: 75 fractile of the mean B intensity values: {fractile_75:.2f}")

    # Classify the candies based on the mean B intensity value
    b_thresh = 65

    is_type_A = mean_vals[:, 2] >= b_thresh
    n_type_A = np.sum(is_type_A)

    # True number found by looking at the image
    true_blue_n = 5
    print(f"Estimated Number of blue candies: {n_type_A} out of {n_regions}. True number: {true_blue_n}\n"
          f"Answer: false positives: {n_type_A - true_blue_n}")

    if debug:
        # Visualize the classified candies
        classified_img = np.zeros(label_img.shape + (3,), dtype=np.uint8)
        for i in range(n_regions):
            if is_type_A[i]:
                classified_img[label_img == (i + 1)] = [255, 0, 0]  # Red for type A
            else:
                classified_img[label_img == (i + 1)] = [0, 255, 0]  # Green for type B
        plt.imshow(classified_img)
        plt.title("Classified candy image")
        plt.show()

    # Create a data matrix for PCA with mean and std values of RGB, area, perimeter, circularity
    # n_rows = number of candies
    # n_cols = number of features (mean and std of RGB, area, perimeter, circularity)
    n_rows = n_regions
    n_cols = 2 * 3 + 3  # mean and std of RGB (3 each) + area + perimeter + circularity
    data_matrix = np.zeros((n_rows, n_cols))

    # fill the data matrix
    data_matrix[:, 0] = mean_vals[:, 0]  # mean R
    data_matrix[:, 1] = mean_vals[:, 1]  # mean G
    data_matrix[:, 2] = mean_vals[:, 2]  # mean B
    data_matrix[:, 3] = std_vals[:, 0]   # std R
    data_matrix[:, 4] = std_vals[:, 1]   # std G
    data_matrix[:, 5] = std_vals[:, 2]   # std B
    data_matrix[:, 6] = areas           # area
    data_matrix[:, 7] = perimeters      # perimeter
    data_matrix[:, 8] = circularities    # circularity
    if debug:
        print("Data matrix shape:", data_matrix.shape)

    # Normalize the data matrix by subtracting the mean and dividing by the std
    data_matrix = data_matrix - np.mean(data_matrix, axis=0)
    data_matrix = data_matrix / np.std(data_matrix, axis=0)

    x = data_matrix
    n_feat = x.shape[1]
    n_obs = x.shape[0]
    if debug:
        print(f"Number of features: {n_feat} and number of observations: {n_obs}")
    mn = np.mean(x, axis=0)
    data = x - mn
    std = np.std(data, axis=0)
    data = data / std
    c_x = np.cov(data.T)
    values, vectors = np.linalg.eig(c_x)
    v_norm = values / values.sum() * 100

    # How much of the variation is explained by the first three principal components
    print(f"Answer: Variation explained by first three components: {v_norm[0] + v_norm[1] + v_norm[2]:.2f}%")

    if debug:
        plt.plot(v_norm)
        plt.xlabel('Principal component')
        plt.ylabel('Percent explained variance')
        plt.ylim([0, 100])
        plt.show()

    pc_proj = vectors.T.dot(data.T)
    pc_1 = pc_proj[0, :]
    pc_2 = pc_proj[1, :]

    plt.scatter(pc_1, pc_2)
    plt.xlabel('Principal Component 1')
    plt.ylabel('Principal Component 2')
    plt.show()


    t_1 = 1.5
    t_2 = 2.0
    is_blue_mm = (abs(pc_1) > t_1) & (abs(pc_2) > t_2)
    n_blue = np.sum(is_blue_mm)
    if debug:
        print(f"Number of blue candies {n_blue} out of {n_regions}")
    blue_indices = np.where(is_blue_mm)[0]
    if debug:
        print(f"blue candy indices: {blue_indices}")

    # Visualize the blue candies
    blue_candies_img = np.zeros(label_img.shape + (3,), dtype=np.uint8)
    for idx in blue_indices:
        blue_candies_img[label_img == (idx + 1)] = [255, 255, 0]  # Yellow for blue candies
    plt.imshow(blue_candies_img)
    plt.title("Blue candies candidates")
    plt.show()

    # Find the two samples that are most distant in the PCA space defined by the first two principal components
    max_dist = 0
    idx_1 = -1
    idx_2 = -1
    for i in range(n_regions):
        for j in range(i + 1, n_regions):
            dist_ij = np.sqrt((pc_1[i] - pc_1[j]) ** 2 + (pc_2[i] - pc_2[j]) ** 2)
            if dist_ij > max_dist:
                max_dist = dist_ij
                idx_1 = i
                idx_2 = j
    if debug:
        print(f"Most distant samples are {idx_1} and {idx_2} with distance {max_dist:.2f}")

    # Create a binary image with the two most distant samples highlighted
    if debug:
        distant_samples_img = np.zeros(label_img.shape + (3,), dtype=np.uint8)
        distant_samples_img[label_img == (idx_1 + 1)] = [255, 0, 255]  # Magenta for sample 1
        distant_samples_img[label_img == (idx_2 + 1)] = [0, 255, 255]  # Cyan for sample 2
        plt.imshow(distant_samples_img)
        plt.title("Most distant samples image")
        plt.show()

    # Mask the original image with the two most distant samples and show the result
    # masked_img = np.zeros(candy_image.shape, dtype=np.uint8)
    masked_img = np.full(candy_image.shape, fill_value=255, dtype=np.uint8)
    masked_img[label_img == (idx_1 + 1)] = candy_image[label_img == (idx_1 + 1)]
    masked_img[label_img == (idx_2 + 1)] = candy_image[label_img == (idx_2 + 1)]
    plt.imshow(masked_img)
    plt.title("Masked image with most distant samples")
    plt.show()




def mm_analysis_e2025():
    candy_image_in = "data/candy/mm.jpg"
    candy_image = io.imread(candy_image_in)
    debug = False

    # Convert to grayscale
    gray_candy = color.rgb2gray(candy_image)
    if debug:
        plt.imshow(gray_candy)
        plt.title("Gray candy image")
        plt.show()

    if debug:
        # Show histogram
        plt.hist(gray_candy.ravel(), bins=256)
        plt.title("Histogram of gray candy image")
        plt.show()


    # Binarize the image
    thresh = 0.65
    binary_candy = gray_candy < thresh
    if debug:
        plt.imshow(binary_candy)
        plt.title("Binary candy image")
        plt.show()

    # Close with small footprint
    footprint = disk(3)
    closing = binary_closing(binary_candy, footprint)
    if debug:
        plt.imshow(closing)
        plt.title("Closed candy image")
        plt.show()

    footprint = disk(1)
    erosion = binary_erosion(closing, footprint)
    if debug:
        plt.imshow(erosion)
        plt.title("Eroded candy image")
        plt.show()

    label_img = measure.label(erosion)
    n_labels = label_img.max()
    print(f"Answer: Number of labels after erosion: {n_labels}")

    if debug:
        # Show label image with a mixed color map
        label_img_color = color.label2rgb(label_img, bg_label=0)
        plt.imshow(label_img_color)
        plt.title("Labeled candy image")
        plt.show()

    # Only keep blobs with a circularity larger than 0.7
    # and an area larger than 100 pixels
    region_props = measure.regionprops(label_img)
    n_regions = len(region_props)
    circularities = []
    areas = []
    for i in range(n_regions):
        area = region_props[i].area
        perimeter = region_props[i].perimeter
        if perimeter == 0:
            circularity = 0
        else:
            circularity =  (2 * np.sqrt(pi * area)) / perimeter
        circularities.append(circularity)
        areas.append(area)
    circularities = np.array(circularities)
    areas = np.array(areas)
    area_thresh = 100
    circularity_thresh = 0.7
    is_circular = (circularities > circularity_thresh) & (areas > area_thresh)
    n_circular = np.sum(is_circular)
    print(f"Answer: Number of circular blobs (circularity > {circularity_thresh}): {n_circular} out of {n_regions}")
    circular_label_img = np.zeros(label_img.shape, dtype=np.uint8)

    if debug:
        for i in range(n_regions):
            if is_circular[i]:
                circular_label_img[label_img == (i + 1)] = i + 1
        plt.imshow(color.label2rgb(circular_label_img, bg_label=0))
        plt.title("Circular blobs")
        plt.show()

    # Compute center of mass of each blob and compute the distance to the image center
    # for all blob and compute the average of these distances
    image_center = np.array(candy_image.shape[:2]) / 2
    distances = []
    for i in range(n_regions):
        if is_circular[i]:
            centroid = region_props[i].centroid
            dist = np.sqrt((centroid[0] - image_center[0]) ** 2 + (centroid[1] - image_center[1]) ** 2)
            distances.append(dist)
    distances = np.array(distances)
    avg_distance = np.mean(distances)
    print(f"Answer: Average distance of circular blobs to image center: {avg_distance:.2f} pixels")
    print(f"Answer: Standard deviation of distances: {np.std(distances):.2f} pixels")

    # Crop a square around each circular object and gather all the crops into a list
    crop_size = 100  # pixels
    crops = []
    for i in range(n_regions):
        if is_circular[i]:
            minr, minc, maxr, maxc = region_props[i].bbox
            center_r = (minr + maxr) // 2
            center_c = (minc + maxc) // 2
            half_crop = crop_size // 2
            crop = candy_image[max(0, center_r - half_crop):min(candy_image.shape[0], center_r + half_crop),
                               max(0, center_c - half_crop):min(candy_image.shape[1], center_c + half_crop), :]
            crops.append(crop)
    n_crops = len(crops)
    if debug:
        print(f"Number of crops extracted: {n_crops}")

    if debug:
        # show the first five crops
        for i in range(min(5, n_crops)):
            plt.imshow(crops[i])
            plt.title(f"Crop {i + 1}")
            plt.show()

    # Create an average image of all the crops
    avg_crop = np.zeros((crop_size, crop_size, 3), dtype=np.float32)
    for i in range(n_crops):
        crop = crops[i]
        avg_crop += crop
    avg_crop /= n_crops
    avg_crop = img_as_ubyte(avg_crop / np.max(avg_crop))
    plt.imshow(avg_crop)
    plt.title("Anwers: Average crop image")
    plt.show()

    # Find the two most distant crops based on mutual sum of squared differences
    max_ssd = 0
    idx_1 = -1
    idx_2 = -1
    for i in range(n_crops):
        for j in range(i + 1, n_crops):
            crop_1 = crops[i]
            crop_2 = crops[j]
            # Resize crops to the same size if necessary
            if crop_1.shape != crop_2.shape:
                continue
            ssd = np.sum((crop_1.astype(np.float32) - crop_2.astype(np.float32)) ** 2)
            if ssd > max_ssd:
                max_ssd = ssd
                idx_1 = i
                idx_2 = j
    # show the two most distant crops
    print(f"Most distant crops are {idx_1} and {idx_2} with SSD {max_ssd:.2f}")
    plt.imshow(crops[idx_1])
    plt.title(f"Answer: Most distant crop 1 (index {idx_1})")
    plt.show()
    plt.imshow(crops[idx_2])
    plt.title(f"Answer: Most distant crop 2 (index {idx_2})")
    plt.show()


def change_detection_on_ct_e2025():
    in_dir = "data/ct/"
    all_images = ["1-020.dcm", "1-021.dcm", "1-022.dcm", "1-023.dcm", "1-024.dcm", "1-025.dcm",
                  "1-026.dcm", "1-027.dcm", "1-028.dcm", "1-029.dcm", "1-030.dcm"]

    new_image_in = "1-040.dcm"
    debug = False

    imgs = []
    for image_file in all_images:
        ct = dicom.dcmread(in_dir + image_file)
        img = ct.pixel_array
        imgs.append(img)

    new_image = dicom.dcmread(in_dir + new_image_in).pixel_array
    n_images = len(all_images)

    alpha = 0.85
    background_gray = imgs[0]
    for i in range(1, n_images):
        new_frame_gray = imgs[i]
        background_gray = alpha * background_gray + (1 - alpha) * new_frame_gray
    max_val = np.max(background_gray)

    print(f"Answer: maximum value : {max_val:.2f}")

    if debug:
        plt.imshow(background_gray, cmap="gray")
        plt.title('The Average Image')
        plt.show()

    # Compute the difference image
    dif_img = np.abs(new_image - background_gray)
    if debug:
        plt.imshow(dif_img, cmap="gray")
        plt.title('The Difference Image')
        plt.show()
        max_dif = np.max(dif_img)
        print(f"Maximum difference value : {max_dif:.2f}")

    dif_img_bin = dif_img > 400
    if debug:
        plt.imshow(dif_img_bin, cmap="gray")
        plt.title('The Binary Difference Image')
        plt.show()

    changed_pixels = np.sum(dif_img_bin)
    print(f"Answer: Changed pixels {changed_pixels}")

    back_bin = background_gray > 400
    if debug:
        plt.imshow(back_bin, cmap="gray")
        plt.title('The Binary Average Image')
        plt.show()

    new_img_bin = new_image  > 400
    if debug:
        plt.imshow(new_img_bin, cmap="gray")
        plt.title('The New Image Binary')
        plt.show()

    if debug:
        # Create a color image to show the differences between the two binary images
        diff_color = np.zeros(background_gray.shape + (3,), dtype=np.uint8)
        diff_color[(back_bin == 1) & (new_img_bin == 0)] = [255, 0, 0]  # Red for disappeared
        diff_color[(back_bin == 0) & (new_img_bin == 1)] = [0, 255, 0]  # Green for appeared
        plt.imshow(diff_color)
        plt.title('The Difference Color Image')
        plt.show()

    # Compute the DICE score
    intersection = np.sum(back_bin & new_img_bin)
    dice_score = (2 * intersection) / (np.sum(back_bin) + np.sum(new_img_bin))
    print(f"Answer: DICE score: {dice_score:.2f}")

    # Find the largest overlap region
    label_img = measure.label(back_bin & new_img_bin)
    region_props = measure.regionprops(label_img)
    max_area = 0
    for region in region_props:
        if region.area > max_area:
            max_area = region.area
    print(f"Answer: Largest overlap region area: {max_area}")


def parametric_classifier_on_tiger_e2025():
    in_dir = "data/tiger/"
    # Load image and training ROI
    A = io.imread(f'{in_dir}Tiger.png')
    imgT = io.imread(f'{in_dir}ROI_Tiger.png')
    debug = False

    # Get ROI intensities to train the classifier
    C1 = A[np.where(imgT == 90)]  # Stribes
    C2 = A[np.where(imgT == 165)] # No stribes

    # Parametric segmentation: Find optimal threshold between two Gaussians
    v1a = np.std(C1)
    v2a = np.std(C2)
    m1a = np.mean(C1)
    m2a = np.mean(C2)

    # Select one threshold
    th = (v1a**2 * m2a - v2a**2 * m1a - np.sqrt(-v1a**2 * v2a**2 * (2 * m2a * m1a - m2a**2 - 2 * v2a**2 * np.log(v2a/v1a) - m1a**2 + 2*v1a**2 * np.log(v2a/v1a))))/(-v2a**2 + v1a**2)
    print(f"Threshold value: {th:.2f}")

    if debug:
        # Alternative solution approach - plot gaussians and determine intersect (found to be around 76.3)
        plt.plot(norm.pdf(np.arange(C1.min(), C1.max()), m1a, v1a))
        plt.plot(norm.pdf(np.arange(C2.min(), C2.max()), m2a, v2a))
        plt.show()

    # Apply segmentation
    C1_Seg = (A < th)
    plt.imshow(C1_Seg, cmap='gray')
    plt.title("Parametric classifier")
    plt.show()


def fishers_lda_on_tiger_e2025():
    in_dir = "data/tiger/"
    # Load image and training ROI
    A = io.imread(f'{in_dir}Tiger.png')
    imgT = io.imread(f'{in_dir}ROI_Tiger.png')

    # Get ROI intensities to train the classifier
    C1 = A[np.where(imgT == 90)]  # Stribes
    C2 = A[np.where(imgT == 165)] # No stribes

    # Prepare for Fischers LDA
    Input = np.r_[C1.flatten(), C2.flatten()].reshape((-1,1))
    Target = np.r_[np.zeros_like(C1), np.ones_like(C2)]

    # Exercise approach (from exercise 6.6-.8)
    # To find the W use the LDA.py function.
    W = LDA(Input, Target)
    L = np.c_[np.ones((len(A.flatten()), 1)), A.flatten()] @ W.T
    P = np.clip(np.exp(L) / np.sum(np.exp(L),1)[:,np.newaxis], 0, 1)
    pixel_value = A[347, 247]
    print(f'Pixel value at position (347, 247) = {pixel_value}')
    print(f'Probability of C1 in pixel position = {P.reshape(A.shape + (2,))[347, 247,0].round(2)}')


def imshow_ortho(data, origin = None, title=None, filename = None):

    if origin is None:
        origin = np.array(data.shape) // 2

    fig = plt.figure(figsize=(8,4))
    ax = fig.add_subplot(121)
    ax.imshow(data[origin[0], ::-1, ::-1], cmap='gray')
    ax.set_title('(y,z)-plane'), ax.set_axis_off()

    ax = fig.add_subplot(222)
    ax.imshow(data[::-1, origin[1], ::-1], cmap='gray')
    ax.set_title('(x,z)-plane'), ax.set_axis_off()

    ax = fig.add_subplot(224)
    ax.imshow(data[::-1, ::-1, origin[2]], cmap='gray')
    ax.set_title('(x,y)-plane'), ax.set_axis_off()

    if title is not None:
        fig.suptitle(title, fontsize=16)

    if filename is not None:
        plt.savefig(filename)
    plt.show()


## Question 2A - 3D registration
def volumetric_registration_e2025():
    in_dir = "data/volumes/"
    # Load images
    sitk_volA = sitk.ReadImage(f'{in_dir}volA.nii')
    sitk_volB = sitk.ReadImage(f'{in_dir}volB.nii')
    dataA = sitk.GetArrayFromImage(sitk_volA)
    slice = [59,100,100]

    # Apply transformation
    matrix, Tt = np.loadtxt(f'{in_dir}matrix.txt'), [-2,-10,5]
    transformation = sitk.AffineTransform(matrix.T.flatten(), Tt)
    imageBB = sitk.Resample(sitk_volB, transformation, sitk.sitkLinear)
    dataBB = sitk.GetArrayFromImage(imageBB)

    # Show blended image (as in exercise 7.16)
    overlap_mask = ((dataA != 0).astype('int') + (dataBB != 0).astype('int') == 2).astype('bool')
    blend = dataA + dataBB
    blend[overlap_mask] = (0.2 * dataA[overlap_mask] + 0.8 * dataBB[overlap_mask])
    imshow_ortho(blend, origin = slice)

def dynamic_programming_e2025():
    in_dir = "data/fundus/"
    # Load image
    Im = io.imread(f'{in_dir}Eye_Fundus_1.png')

    # Initialize accumulator image
    Acc = np.zeros_like(Im, dtype='int16')

    # Copy first row
    Acc[0,1:-1] = Im[0,1:-1]
    for ir in range(1,Acc.shape[0]):
        for ic in range(1,Acc.shape[1] - 1): # avoid boundary effects
            Acc[ir,ic] = Im[ir,ic] + np.min([Acc[ir-1, ic-1], Acc[ir-1, ic], Acc[ir-1, ic+1]])

    print(f'Total cost at column 46 = {Acc[-1,46]}')


def gradient_decent_a_e2025():
    in_dir = "data/gradient/"
    img = io.imread(f'{in_dir}MergedObjects.png').astype('uint8')

    # Step 1: Make the inverse distance map
    dist_map = - ndimage.distance_transform_edt(img)

    # Step 2: Make gradient maps
    # Row wise gradient (r-direction)
    g_r = ndimage.prewitt(dist_map, axis = 0)

    # Column wise gradient (c-direction)
    g_c = ndimage.prewitt(dist_map, axis = 1)

    # Gradient-descent algorithm
    p_start = [104, 22]
    step_size = 0.12

    # Gradient at starting position
    p_gradient = np.array([g_r[tuple(p_start)], g_c[tuple(p_start)]])
    newp = np.floor(p_start - step_size * p_gradient)

    print(f"p(1) = {newp.astype('int8')}")


def gradient_decent_b_e2025():
    in_dir = "data/gradient/"
    img = io.imread(f'{in_dir}MergedObjects.png').astype('uint8')
    debug = False

    # Step 1: Make the inverse distance map
    dist_map = - ndimage.distance_transform_edt(img)

    # Step 2: Make gradient maps
    # Row wise gradient (r-direction)
    g_r = ndimage.prewitt(dist_map, axis = 0)

    # Column wise gradient (c-direction)
    g_c = ndimage.prewitt(dist_map, axis = 1)

    # Step 3: Implement a gradient-descent algorithm
    p_start = [243, 167]
    step_size = 0.35

    p = [p_start]
    dist = []
    for _ in range(1,100):
        # Get gradient in position
        p_gradient = np.array([g_r[tuple(p[-1])], g_c[tuple(p[-1])]])
        newp = np.floor(p[-1] - step_size * p_gradient)
        p.append(newp.astype('int'))

        # Euclidian distance between p(n) and p(n-1)
        dist.append(np.sqrt((p[-1][0] - p[-2][0])**2 + (p[-1][1] - p[-2][1])**2))

    if debug:
        plt.plot(dist)
        plt.show()
    print(f"Distance at iteration 40: {dist[39]:.2f}")

if __name__ == '__main__':
    candy_classification_e2025()
    change_detection_on_ct_e2025()
    mm_analysis_e2025()
    parametric_classifier_on_tiger_e2025()
    fishers_lda_on_tiger_e2025()
    volumetric_registration_e2025()
    dynamic_programming_e2025()
    gradient_decent_a_e2025()
    gradient_decent_b_e2025()

