""" KALMUS tkinter GUI utility """
import cv2
import kalmus.utils.measure_utils as measure_utils
import os
import sys
import numpy as np
from skimage.color import hsv2rgb, rgb2hsv
from matplotlib.ticker import FuncFormatter
[docs]def compare_two_barcodes(barcode_1, barcode_2):
    """
    Compare the similarity between two barcodes using NRMSE, SSIM, cross correlation, local cross correlation,
    Needleman Wunsch, and Smith Waterman alignment matching
    :param barcode_1: The input barcode 1
    :param barcode_2: The input barcode 2
    :return: The simiarity scores computed using all six metrics
    """
    # Resize two barcodes so they have the same shape
    # Alway resize the small barcode to the shape of the larger barcode
    target_barcode_1 = barcode_1.get_barcode().astype("float64")
    target_barcode_2 = barcode_2.get_barcode().astype("float64")
    if target_barcode_1.size <= target_barcode_2.size:
        target_barcode_1 = cv2.resize(target_barcode_1, dsize=target_barcode_2.shape[:2][::-1],
                                      interpolation=cv2.INTER_LINEAR)
    elif target_barcode_1.size > target_barcode_2.size:
        target_barcode_2 = cv2.resize(target_barcode_2, dsize=target_barcode_1.shape[:2][::-1],
                                      interpolation=cv2.INTER_LINEAR)
    # Get the cross correlation and local cross correlation
    cross_corre = measure_utils.cross_correlation(target_barcode_1.astype("float64"),
                                                  target_barcode_2.astype("float64"))
    loc_cross_corre = measure_utils.local_cross_correlation(target_barcode_1.astype("float64"),
                                                            target_barcode_2.astype("float64"))
    # Get the Normalized root mean squared error and and Structual similarity index measurement
    nrmse = measure_utils.nrmse_similarity(target_barcode_1.astype("float64"),
                                           target_barcode_2.astype("float64"))
    ssim = measure_utils.ssim_similarity(target_barcode_1.astype("float64"),
                                         target_barcode_2.astype("float64"))
    # Get the color/brightness of two barcodes
    if barcode_1.barcode_type == "Color":
        colors_barcode_1 = barcode_1.colors
        colors_barcode_2 = barcode_2.colors
    else:
        colors_barcode_1 = barcode_1.brightness
        colors_barcode_2 = barcode_2.brightness
    # Resample the barcodes if the barcodes have different size/length
    if colors_barcode_1.shape[0] > colors_barcode_2.shape[0]:
        colors_barcode_1 = colors_barcode_1[measure_utils.get_resample_index(len(colors_barcode_1),
                                                                             len(colors_barcode_2))]
    elif colors_barcode_1.shape[0] < colors_barcode_2.shape[0]:
        colors_barcode_2 = colors_barcode_2[measure_utils.get_resample_index(len(colors_barcode_2),
                                                                             len(colors_barcode_1))]
    # Convert the barcode to character string for alignment matching comparison
    # Convert the barcode to correct character based on its barcode type
    if barcode_1.barcode_type == "Color":
        string_1 = measure_utils.generate_hue_strings_from_color_barcode(colors_barcode_1)
        string_2 = measure_utils.generate_hue_strings_from_color_barcode(colors_barcode_2)
    else:
        string_1 = measure_utils.generate_brightness_string_from_brightness_barcode(colors_barcode_1)
        string_2 = measure_utils.generate_brightness_string_from_brightness_barcode(colors_barcode_2)
    # Get the Needleman Wunsch and Smith Waterman alignment matching score
    needleman = measure_utils.compare_needleman_wunsch(string_1, string_2, normalized=True)
    smithw = measure_utils.compare_smith_waterman(string_1, string_2, normalized=True)
    return cross_corre, loc_cross_corre, nrmse, ssim, needleman, smithw 
[docs]def get_comparison_result_text(barcode_1, barcode_2):
    """
    Get the text comparison result using the compare_two_barcodes function
    :param barcode_1: The compared barcode 1
    :param barcode_2: The compared barcode 2
    :return: The text comparison result
    """
    # Return error text if two barcodes are not in the same type
    if barcode_1.barcode_type != barcode_2.barcode_type:
        result_text = "ERROR:\nComparison between different types of\n" \
                      
"barcode type {:s} and {:s}\nare not allowed.".format(barcode_1.barcode_type,
                                                                            barcode_2.barcode_type)
        return result_text
    # Get the similarity score from the compare_two_barcodes function
    cross_corre, loc_cross_corre, nrmse, ssim, needleman, smith = compare_two_barcodes(barcode_1, barcode_2)
    # Get the text result
    result_text = "Comparison metrics:\n" \
                  
"NRMSE Similarity:                  {:10.4f}\n" \
                  
"SSIM Similarity:                     {:10.4f}\n" \
                  
"Cross Correlation:                 {:10.4f}\n" \
                  
"Local Cross Correlation:         {:10.4f}\n" \
                  
"Needleman Wunsch Similarity:{:10.4f}\n" \
                  
"Smith Waterman Similarity:     {:10.4f}".format(nrmse, ssim, cross_corre, loc_cross_corre,
                                                                   needleman, smith)
    return result_text 
[docs]def get_time(barcode, x_pos, y_pos):
    """
    Get the time (hr, min, sec) at a point of barcode
    :param barcode: The barcode object
    :param x_pos: The x position of the query
    :param y_pos: The y position of the query
    :return: time frame, hr, min, sec at the given point (x_pos, y_pos)
    """
    # Compute the frame label
    frame = (barcode.skip_over + barcode.sampled_frame_rate *
             ((x_pos * barcode.get_barcode().shape[0]) + y_pos)) * barcode.scale_factor
    frame = int(frame)
    # If frame rate is not given, use 30 as default
    if barcode.fps is None:
        barcode.fps = 30
    # Compute the time label
    time_tot_sec = frame / barcode.fps
    time_sec = int(time_tot_sec % 60)
    time_min = int((time_tot_sec / 60) % 60)
    time_hr = int(time_tot_sec / 3600)
    return frame, time_hr, time_min, time_sec 
[docs]def update_graph(barcode_1, barcode_2, axes, bin_step=5):
    """
    Update the plotted graph (in place)
    :param barcode_1: The barcode 1
    :param barcode_2: The barcode 2
    :param axes: The axes of the plotted figure
    :param bin_step: The step of histogram bin
    """
    # Plot the barcode with the correct color map based on their barcode types
    if barcode_1.barcode_type == "Brightness":
        axes[0][0].imshow(barcode_1.get_barcode().astype("uint8"), cmap='gray', vmin=0, vmax=255)
    else:
        axes[0][0].imshow(barcode_1.get_barcode().astype("uint8"))
    if barcode_2.barcode_type == "Brightness":
        axes[1][0].imshow(barcode_2.get_barcode().astype("uint8"), cmap='gray', vmin=0, vmax=255)
    else:
        axes[1][0].imshow(barcode_2.get_barcode().astype("uint8"))
    # Rescale the axis range
    # And update the plot
    for axis in axes.ravel():
        axis.relim()
        axis.autoscale_view()
    # Update the axis ticks of the plotted axes
    update_axes_ticks(barcode_1, barcode_2, axes)
    # Update the title of the plotted axes
    update_axes_title(axes, barcode_1, barcode_2)
    # Update the histogram of the barcode 1
    update_hist(barcode_1, ax=axes[0][1], bin_step=bin_step)
    # Update the histogram of the barcode 2
    update_hist(barcode_2, ax=axes[1][1], bin_step=bin_step) 
[docs]def update_axes_ticks(barcode1, barcode2, axes):
    """
    Update the axes ticks
    If two barcodes have the same temporal dimensions, the ticks will show the temporal position of a pixel in the
    barcode image. Otherwise, the axes ticks will show the spatial position of a pixel.
    :param barcode1: The barcode 1
    :param barcode2: The barcode 2
    :param axes: The axes of the plotted figure
    """
    frames_per_column_1 = barcode1.sampled_frame_rate * barcode1.get_barcode().shape[0] \
                          
* barcode1.scale_factor
    frames_per_column_2 = barcode2.sampled_frame_rate * barcode2.get_barcode().shape[0] \
                          
* barcode2.scale_factor
    seconds_per_column1 = frames_per_column_1 / barcode1.fps
    seconds_per_column2 = frames_per_column_2 / barcode2.fps
    axes[0][0].set_ylabel("{:.2f}s per column".format(seconds_per_column1))
    axes[1][0].set_ylabel("{:.2f}s per column".format(seconds_per_column2))
    if np.round(seconds_per_column1, decimals=0) == np.round(seconds_per_column2, decimals=0):
        def get_seconds_str_yticks(tick_value, pos):
            seconds = np.round(tick_value * seconds_per_column2, decimals=0).astype("int")
            return "{:02d}:{:02d}".format(seconds // 60, seconds % 60)
        axes[1][0].xaxis.set_major_formatter(FuncFormatter(get_seconds_str_yticks))
        axes[1][0].set_xlabel("Elapsed Time (mins:secs)")
        def get_seconds_str_xticks(tick_value, pos):
            seconds = tick_value * barcode1.sampled_frame_rate * barcode1.scale_factor / barcode1.fps
            return "{:.1f}s".format(seconds)
        axes[0][0].yaxis.set_major_formatter(FuncFormatter(get_seconds_str_xticks))
        axes[1][0].yaxis.set_major_formatter(FuncFormatter(get_seconds_str_xticks)) 
[docs]def update_axes_title(axes, barcode_1, barcode_2):
    """
    Update the title of the plotted axes (in place)
    :param axes: The plotted axes of the figure
    :param barcode_1: The barcode 1
    :param barcode_2: The barcode 2
    """
    title_1 = "Barcode 1"
    title_2 = "Barcode 2"
    keys = ["Film Title", "Produced Year", "Genre"]
    # Update the meta data into the title of the plotted figure
    if barcode_1.meta_data is not None:
        for key in keys:
            if key in barcode_1.meta_data.keys():
                title_1 += "    {:s}: {:s}".format(key, barcode_1.meta_data[key])
    # Update the Frame sampling type and color/brightness metric into the title
    title_1 += "    Frame Type:{:s}    {:s} Metric:{:s}".format(barcode_1.frame_type, barcode_1.barcode_type,
                                                                barcode_1.color_metric)
    # Update the meta data into the title of the plotted figure
    if barcode_2.meta_data is not None:
        for key in keys:
            if key in barcode_2.meta_data.keys():
                title_2 += "    {:s}: {:s}".format(key, barcode_2.meta_data[key])
    # Update the Frame sampling type and color/brightness metric into the title
    title_2 += "    Frame Type:{:s}    {:s} Metric:{:s}".format(barcode_2.frame_type, barcode_2.barcode_type,
                                                                barcode_2.color_metric)
    # Set up the title
    axes[0][0].set_title(title_1, fontsize=8)
    axes[1][0].set_title(title_2, fontsize=8)
    # Change the histogram's label and xticks accordingly
    if barcode_1.barcode_type == "Color":
        axes[0][1].set_xticks(np.arange(0, 361, 30))
        axes[0][1].set_xlabel("Color Hue (0 - 360)")
        axes[0][1].set_ylabel("Number of frames")
    elif barcode_1.barcode_type == "Brightness":
        axes[0][1].set_xticks(np.arange(0, 256, 16))
        axes[0][1].set_xlabel("Brightness (0 - 255)")
        axes[0][1].set_ylabel("Number of frames")
    if barcode_2.barcode_type == "Color":
        axes[1][1].set_xticks(np.arange(0, 361, 30))
        axes[1][1].set_xlabel("Color Hue (0 - 360)")
        axes[1][1].set_ylabel("Number of frames")
    elif barcode_2.barcode_type == "Brightness":
        axes[1][1].set_xticks(np.arange(0, 256, 32))
        axes[1][1].set_xlabel("Brightness (0 - 255)")
        axes[1][1].set_ylabel("Number of frames") 
[docs]def update_hist(barcode, ax, bin_step=5):
    """
    Update the histogram of the plotted figure (in place)
    :param barcode: The barcode of the corresponding histogram
    :param ax: The axis that contain the histogram
    :param bin_step: The step of the bin in the plotted histogram
    """
    bin_step = bin_step
    # Plot the histogram based on the barcode's type
    if barcode.barcode_type == "Color":
        # If the barcode type is color
        # Then plot the barcode's hue value
        normalized_barcode = barcode.get_barcode().astype("float") / 255
        hsv_colors = rgb2hsv(normalized_barcode.reshape(-1, 1, 3))
        hue = hsv_colors[..., 0] * 360
        N, bins, patches = ax.hist(hue[:, 0], bins=(np.arange(0, 361, bin_step)))
        # Paint each bin with its corresponding color in hue
        paint_hue_hist(bin_step, patches)
    else:
        # If the barcode type is brightness
        N, bins, patches = ax.hist(barcode.brightness[:, 0], bins=(np.arange(0, 256, bin_step)))
        # Then paint each bin with its brightness intensity
        paint_gray_hist(bin_step, patches, opacity=0.9) 
[docs]def paint_hue_hist(bin_step, patches):
    """
    Paint each bin of the hue histogram with its corresponding color in hue (in place)
    :param bin_step: The step of the bin in the histogram
    :param patches: The patches of the histogram that will be painted later
    """
    # Paint each bin of the histogram
    for i in range(len(patches)):
        # Get the hue first
        hue = np.array([i * bin_step / 360.0, 1.0, 1.0]).astype("float64")
        hue = np.expand_dims(np.expand_dims(hue, 0), 0)
        # Then get the corresponding color
        rgb = hsv2rgb(hue).tolist()
        rgb_tuple = (rgb[0][0][0], rgb[0][0][1], rgb[0][0][2])
        # Paint each patch of the histogram
        patches[i].set_facecolor(rgb_tuple) 
[docs]def paint_gray_hist(bin_step, patches, opacity=0.8):
    """
    Paint each bin of the brightness histogram with its corresponding brightness intensity (in place)
    :param bin_step: The step of the bin in the histogram
    :param patches: The patches of the histogram
    :param opacity: Opacity of the brightness intensity to avoid totally white in the high intensity bin \
                    To make the histogram bin in high intensity area more observable
    """
    for i in range(len(patches)):
        # Get the RGB gray color for each bin
        bri_rgb_tuple = (bin_step * i * opacity / 255, bin_step * i * opacity / 255, bin_step * i * opacity / 255)
        patches[i].set_facecolor(bri_rgb_tuple) 
[docs]def resource_path(relative_path):
    """
    Internal utility function
    Use to convert the input relative path to the absolute path
    This is used for the Pyinstaller wrapper
    :param relative_path: The relative path to the file
    :return: The absolute path to the file
    """
    try:
        # If the MEIPASS base path exist
        base_path = sys._MEIPASS
    except Exception:
        # Otherwise use the absolute path
        if relative_path.endswith(".ico"):
            # If the path points to .ico image
            if os.name != "nt":
                # If the running os is linux
                relative_path = relative_path[:-3]
                # Replace the .ico image with image in .xbm format
                relative_path += "xbm"
        # Join the path
        base_path = os.path.abspath(os.path.dirname(__file__))
        base_path = os.path.join(base_path, '../data')
        if relative_path.endswith(".xbm"):
            # If the image is .xbm image
            if os.name != "nt":
                # and the running os is linux
                # Add @ identifier at the front of path
                base_path = "@" + base_path
    # Return the path
    return os.path.join(base_path, relative_path)