import os import shutil import time import uuid from datetime import datetime from decimal import Decimal import gradio as gr import matplotlib.pyplot as plt from settings import DEMO plt.switch_backend("agg") # fix for "RuntimeError: main thread is not in main loop" import numpy as np import pandas as pd from PIL import Image from model import GranaAnalyser ga = GranaAnalyser( "weights/yolo/20240604_yolov8_segm_ABRCR1_all_train4_best.pt", "weights/AS_square_v16.ckpt", "weights/period_measurer_weights-1.298_real_full-fa12970.ckpt", ) def calc_ratio(pixels, nano): """ Calculates ratio of pixels to nanometers and returns as str to populate ratio_input :param pixels: :param nano: :return: """ if not (pixels and nano): pass else: res = pixels / nano return res # https://jakevdp.github.io/PythonDataScienceHandbook/05.13-kernel-density-estimation.html def KDE(dataset, h): # the Kernel function def K(x): return np.exp(-(x ** 2) / 2) / np.sqrt(2 * np.pi) n_samples = dataset.size x_range = dataset # x-value range for plotting KDEs total_sum = 0 # iterate over datapoints for i, xi in enumerate(dataset): total_sum += K((x_range - xi) / h) y_range = total_sum / (h * n_samples) return y_range def prepare_files_for_download( dir_name, grana_data, aggregated_data, detection_visualizations_dict, images_grana_dict, ): """ Save and zip files for download :param dir_name: :param grana_data: DataFrame containing all grana measurements :param aggregated_data: dict containing aggregated measurements :return: """ dir_to_zip = f"{dir_name}/to_zip" # raw data grana_data_csv_path = f"{dir_to_zip}/grana_raw_data.csv" grana_data.to_csv(grana_data_csv_path, index=False) # aggregated measurements aggregated_csv_path = f"{dir_to_zip}/grana_aggregated_data.csv" aggregated_data.to_csv(aggregated_csv_path) # annotated pictures masked_images_dir = f"{dir_to_zip}/annotated_images" os.makedirs(masked_images_dir) for img_name, img in detection_visualizations_dict.items(): filename_split = img_name.split(".") extension = filename_split[-1] filename = ".".join(filename_split[:-1]) filename = f"{filename}_annotated.{extension}" img.save(f"{masked_images_dir}/{filename}") # single_grana images grana_images_dir = f"{dir_to_zip}/single_grana_images" os.makedirs(grana_images_dir) org_images_dict = pd.Series( grana_data["source image"].values, index=grana_data["granum ID"] ).to_dict() for img_name, img in images_grana_dict.items(): org_filename = org_images_dict[img_name] org_filename_split = org_filename.split(".") org_filename_no_ext = ".".join(org_filename_split[:-1]) img_name_ext = f"{org_filename_no_ext}_granum_{str(img_name)}.png" img.save(f"{grana_images_dir}/{img_name_ext}") # zip all files date_str = datetime.today().strftime("%Y-%m-%d") zip_name = f"GRANA_results_{date_str}" zip_path = f"{dir_name}/{zip_name}" shutil.make_archive(zip_path, "zip", dir_to_zip) # delete to_zip dir zip_dir_path = os.path.join(os.getcwd(), dir_to_zip) shutil.rmtree(zip_dir_path) download_file_path = f"{zip_path}.zip" return download_file_path def show_info_on_submit(s): return ( gr.Button(interactive=False), gr.Button(interactive=False), gr.Row(visible=True), gr.Row(visible=False), ) def load_css(): with open("styles.css", "r") as f: css_content = f.read() return css_content primary_hue = gr.themes.Color( c50="#e1f8ee", c100="#b7efd5", c200="#8de6bd", c300="#63dda5", c400="#39d48d", c500="#27b373", c600="#1e8958", c700="#155f3d", c800="#0c3522", c900="#030b07", c950="#000", ) theme = gr.themes.Default( primary_hue=primary_hue, font=[gr.themes.GoogleFont("Ubuntu"), "ui-sans-serif", "system-ui", "sans-serif"], ) def draw_violin_plot(y, ylabel, title): # only generate plot for 3 or more values if y.count() < 3: return None # Colors RED_DARK = "#850e00" DARK_GREEN = "#0c3522" BRIGHT_GREEN = "#8de6bd" # Create jittered version of "x" (which is only 1) x_jittered = [] kde = KDE(y, (y.max() - y.min()) / y.size / 2) kde = kde / kde.max() * 0.2 for y_val in kde: x_jittered.append(1 + np.random.uniform(-y_val, y_val, 1)) fig = plt.figure() ax = fig.add_subplot(1, 1, 1) ax.scatter(x=x_jittered, y=y, s=20, alpha=0.4, c=DARK_GREEN) violins = ax.violinplot( y, widths=0.45, bw_method="silverman", showmeans=False, showmedians=False, showextrema=False, ) # change violin color for pc in violins["bodies"]: pc.set_facecolor(BRIGHT_GREEN) # add a boxplot to ax # but make the whiskers length equal to 1 SD, i.e. in the proportion of the IQ range, but this length should start from the mean but be visible from the box boundary lower = np.mean(y) - 1 * np.std(y) upper = np.mean(y) + 1 * np.std(y) medianprops = dict(linewidth=1, color="black", solid_capstyle="butt") boxplot_stats = [ { "med": np.median(y), "q1": np.percentile(y, 25), "q3": np.percentile(y, 75), "whislo": lower, "whishi": upper, } ] ax.bxp( boxplot_stats, # data for the boxplot showfliers=False, # do not show the outliers beyond the caps. showcaps=True, # show the caps medianprops=medianprops, ) # Add mean value point ax.scatter(1, y.mean(), s=30, color=RED_DARK, zorder=3) ax.set_xticks([]) ax.set_ylabel(ylabel) ax.set_title(title) fig.tight_layout() return fig def transform_aggregated_results_table(results_dict): MEASUREMENT_HEADER = "measurement [unit]" VALUE_HEADER = "value +-SD" def get_value_str(value, std): if np.isnan(value) or np.isnan(std): return "-" value_str = str(Decimal(str(value)).quantize(Decimal("0.01"))) std_str = str(Decimal(str(std)).quantize(Decimal("0.01"))) return f"{value_str} +-{std_str}" def append_to_dict(new_key, old_val_key, old_sd_key): aggregated_dict[MEASUREMENT_HEADER].append(new_key) value_str = get_value_str(results_dict[old_val_key], results_dict[old_sd_key]) aggregated_dict[VALUE_HEADER].append(value_str) aggregated_dict = {MEASUREMENT_HEADER: [], VALUE_HEADER: []} # area append_to_dict("area [nm^2]", "area nm^2", "area nm^2 std") # perimeter append_to_dict("perimeter [nm]", "perimeter nm", "perimeter nm std") # diameter append_to_dict("diameter [nm]", "diameter nm", "diameter nm std") # height append_to_dict("height [nm]", "height nm", "height nm std") # number of layers append_to_dict("number of thylakoids", "Number of layers", "Number of layers std") # SRD append_to_dict("SRD [nm]", "period nm", "period nm std") # GSI append_to_dict("GSI", "GSI", "GSI std") # N grana aggregated_dict[MEASUREMENT_HEADER].append("number of grana") aggregated_dict[VALUE_HEADER].append(str(int(results_dict["N grana"]))) return aggregated_dict def rename_columns_in_results_table(results_table): column_names = { "Granum ID": "granum ID", "File name": "source image", "area nm^2": "area [nm^2]", "perimeter nm": "perimeter [nm]", "diameter nm": "diameter [nm]", "height nm": "height [nm]", "Number of layers": "number of thylakoids", "period nm": "SRD [nm]", "period SD nm": "SRD SD [nm]", } results_table = results_table.rename(columns=column_names) return results_table with gr.Blocks(css=load_css(), theme=theme) as demo: svg = """ """ gr.HTML( f'
Full results are a zip file containing:
" "
Note that GRANA only stores the result files for 1 hour.
", elem_classes="input-row", ) with gr.Row(elem_classes="input-row"): download_file_out = gr.DownloadButton( label="Download results", variant="primary", elem_classes="margin-bottom", ) with gr.Row(): gr.HTML( '