From 50285fbe712e5c3b65aa47a8110463c693c4237b Mon Sep 17 00:00:00 2001 From: fima <fima@dtu.dk> Date: Mon, 27 Nov 2023 15:35:25 +0100 Subject: [PATCH] Gui launcher system --- qim3d/gui/annotation_tool.py | 329 ++++++++++++++++++++-------------- qim3d/gui/data_explorer.py | 11 +- qim3d/gui/iso3d.py | 10 +- qim3d/gui/local_thickness.py | 11 ++ qim3d/tests/utils/test_doi.py | 10 +- qim3d/utils/internal_tools.py | 54 ++++-- 6 files changed, 260 insertions(+), 165 deletions(-) diff --git a/qim3d/gui/annotation_tool.py b/qim3d/gui/annotation_tool.py index ae44f498..c4b121ee 100644 --- a/qim3d/gui/annotation_tool.py +++ b/qim3d/gui/annotation_tool.py @@ -2,18 +2,20 @@ import tifffile import os import numpy as np import gradio as gr -from qim3d.io import load # load or DataLoader? +from qim3d.io import load # load or DataLoader? +from qim3d.utils import internal_tools + class Interface: def __init__(self): self.verbose = False self.title = "Annotation tool" - #self.plot_height = 768 + # self.plot_height = 768 self.height = 1024 - #self.width = 960 + # self.width = 960 self.max_masks = 3 self.mask_opacity = 0.5 - self.cmy_hex = ['#00ffff','#ff00ff','#ffff00'] # Colors for max_masks>3? + self.cmy_hex = ["#00ffff", "#ff00ff", "#ffff00"] # Colors for max_masks>3? # CSS path current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -32,13 +34,12 @@ class Interface: self.interface.launch( quiet=quiet, height=self.height, - #width=self.width, + # width=self.width, show_tips=False, - **kwargs + **kwargs, ) return - def get_result(self): # Get the temporary files from gradio @@ -62,239 +63,301 @@ class Interface: return mask - - def create_interface(self, img=None): + def create_interface(self, img=None): with gr.Blocks(css=self.css_path) as gradio_interface: masks_state = gr.State(value={}) - counts = gr.Number(value=1,visible=False) - + counts = gr.Number(value=1, visible=False) + with gr.Row(): - with gr.Column(scale=1,min_width=320): + with gr.Column(scale=1, min_width=320): upload_img_btn = gr.UploadButton( - label='Upload image', - file_types=['image'], - interactive=True if img is None else False - ) + label="Upload image", + file_types=["image"], + interactive=True if img is None else False, + ) clear_img_btn = gr.Button( - value='Clear image', - interactive=False if img is None else True - ) - + value="Clear image", interactive=False if img is None else True + ) + with gr.Row(): - with gr.Column(scale=2,min_width=32): + with gr.Column(scale=2, min_width=32): selected_mask = gr.Radio( - choices = ["Mask 1"], - value = "Mask 1", - label="Choose which mask to draw", - scale=1 + choices=["Mask 1"], + value="Mask 1", + label="Choose which mask to draw", + scale=1, ) - with gr.Column(scale=1,min_width=64): + with gr.Column(scale=1, min_width=64): add_mask_btn = gr.Button( - value='Add mask', + value="Add mask", scale=2, ) with gr.Row(): prep_dl_btn = gr.Button( - value='Prepare mask for download', - visible=False if img is None else True - ) + value="Prepare mask for download", + visible=False if img is None else True, + ) with gr.Row(): save_output = gr.File( show_label=True, label="Output file", visible=False, ) - + with gr.Column(scale=4): with gr.Row(): input_img = gr.Image( label="Input", - tool='sketch', + tool="sketch", value=img, height=600, width=600, - brush_color='#00ffff', + brush_color="#00ffff", mask_opacity=self.mask_opacity, - interactive=False if img is None else True - ) - + interactive=False if img is None else True, + ) + output_masks = [] for mask_idx in range(self.max_masks): - with gr.Row(): # make a new row for every mask - output_mask=gr.Image( + with gr.Row(): # make a new row for every mask + output_mask = gr.Image( label=f"Mask {mask_idx+1}", - visible=True if mask_idx==0 else False, - image_mode='L', + visible=True if mask_idx == 0 else False, + image_mode="L", height=600, width=600, - interactive=False if img is None else True, # If statement added bc of bug after Gradio 3.44.x - show_download_button=False - ) + interactive=False + if img is None + else True, # If statement added bc of bug after Gradio 3.44.x + show_download_button=False, + ) output_masks.append(output_mask) - + # Operations - operations = Operations(max_masks=self.max_masks,cmy_hex=self.cmy_hex) - + operations = Operations(max_masks=self.max_masks, cmy_hex=self.cmy_hex) + # Update component configuration when image is uploaded - upload_img_btn.upload(fn=operations.upload_img_update, - inputs=upload_img_btn, - outputs=[input_img,clear_img_btn,upload_img_btn,prep_dl_btn] + output_masks - ) - + upload_img_btn.upload( + fn=operations.upload_img_update, + inputs=upload_img_btn, + outputs=[input_img, clear_img_btn, upload_img_btn, prep_dl_btn] + + output_masks, + ) + # Add mask below when 'add mask' button is clicked add_mask_btn.click( fn=operations.increment_mask, inputs=counts, - outputs=[counts, selected_mask] + output_masks - ) - + outputs=[counts, selected_mask] + output_masks, + ) + # Draw mask when input image is edited input_img.edit( fn=operations.update_masks, - inputs=[input_img,selected_mask,masks_state,upload_img_btn], - outputs=output_masks - ) - + inputs=[input_img, selected_mask, masks_state, upload_img_btn], + outputs=output_masks, + ) + # Update brush color according to radio setting selected_mask.change( fn=operations.update_brush_color, - inputs=selected_mask,outputs=input_img - ) - + inputs=selected_mask, + outputs=input_img, + ) + # Make file download visible prep_dl_btn.click( fn=operations.save_mask, inputs=output_masks, - outputs=[save_output,save_output] - ) - + outputs=[save_output, save_output], + ) + # Update 'Add mask' button interactivit according to the current count counts.change( fn=operations.set_add_mask_btn_interactivity, inputs=counts, - outputs=add_mask_btn - ) - + outputs=add_mask_btn, + ) + # Reset component configuration when image is cleared clear_img_btn.click( fn=operations.clear_img_update, inputs=None, - outputs=[selected_mask,prep_dl_btn,save_output,counts,input_img,upload_img_btn,clear_img_btn] + output_masks - ) - + outputs=[ + selected_mask, + prep_dl_btn, + save_output, + counts, + input_img, + upload_img_btn, + clear_img_btn, + ] + + output_masks, + ) + return gradio_interface + class Operations: def __init__(self, max_masks, cmy_hex): self.max_masks = max_masks self.cmy_hex = cmy_hex - - def update_masks(self,input_img,selected_mask,masks_state,file): + def update_masks(self, input_img, selected_mask, masks_state, file): # Binarize mask (it is not per default due to anti-aliasing) - input_mask = input_img['mask'] - input_mask[input_mask>0]=255 + input_mask = input_img["mask"] + input_mask[input_mask > 0] = 255 try: file_name = file.name except AttributeError: - file_name = 'nb_img' + file_name = "nb_img" # Add new file to state dictionary when this function sees it first time if file_name not in masks_state.keys(): - masks_state[file_name]=[[] for _ in range(self.max_masks)] + masks_state[file_name] = [[] for _ in range(self.max_masks)] # Get index of currently selected and non-selected masks - sel_mask_idx = int(selected_mask[-1])-1 - nonsel_mask_idxs = [mask_idx for mask_idx in list(range(self.max_masks)) if mask_idx != sel_mask_idx] + sel_mask_idx = int(selected_mask[-1]) - 1 + nonsel_mask_idxs = [ + mask_idx + for mask_idx in list(range(self.max_masks)) + if mask_idx != sel_mask_idx + ] # Add background to state first time function is invoked in current session - if len(masks_state[file_name][0])==0: + if len(masks_state[file_name][0]) == 0: for i in range(len(masks_state[file_name])): masks_state[file_name][i].append(input_mask) - + # Check for discrepancy between what is drawn and what is shown as output masks masks_state_combined = 0 for i in range(len(masks_state[file_name])): - masks_state_combined+=masks_state[file_name][i][-1] - discrepancy = masks_state_combined!=input_mask - if np.any(discrepancy): # Correct discrepancy in output masks + masks_state_combined += masks_state[file_name][i][-1] + discrepancy = masks_state_combined != input_mask + if np.any(discrepancy): # Correct discrepancy in output masks for i in range(self.max_masks): - masks_state[file_name][i][-1][discrepancy]=0 - + masks_state[file_name][i][-1][discrepancy] = 0 + # Add most recent change in input to currently selected mask mask2append = input_mask for mask_idx in nonsel_mask_idxs: - mask2append -= masks_state[file_name][mask_idx][-1] + mask2append -= masks_state[file_name][mask_idx][-1] masks_state[file_name][sel_mask_idx].append(mask2append) - + return [masks_state[file_name][i][-1] for i in range(self.max_masks)] - def save_mask(self,*masks): + def save_mask(self, *masks): # Go from multi-channel to single-channel mask - stacked_masks = np.stack(masks,axis=-1) + stacked_masks = np.stack(masks, axis=-1) final_mask = np.zeros_like(masks[0]) - final_mask[np.where(stacked_masks==255)[:2]]=np.where(stacked_masks==255)[-1]+1 + final_mask[np.where(stacked_masks == 255)[:2]] = ( + np.where(stacked_masks == 255)[-1] + 1 + ) # Save output image in a temp space (and to current directory which is a bug) filename = "mask.tif" - tifffile.imwrite(filename,final_mask) + tifffile.imwrite(filename, final_mask) save_output_update = gr.File(visible=True) - + return save_output_update, filename - def increment_mask(self,counts): + def increment_mask(self, counts): # increment count by 1 - counts+=1 - counts=int(counts) + counts += 1 + counts = int(counts) counts_update = gr.Number(value=counts) - selected_mask_update = gr.Radio(value = f"Mask {counts}", choices = [f"Mask {i+1}" for i in range(counts)]) - output_masks_update = [gr.Image(visible=True)]*counts + [gr.Image(visible=False)]*(self.max_masks-counts) + selected_mask_update = gr.Radio( + value=f"Mask {counts}", choices=[f"Mask {i+1}" for i in range(counts)] + ) + output_masks_update = [gr.Image(visible=True)] * counts + [ + gr.Image(visible=False) + ] * (self.max_masks - counts) return [counts_update, selected_mask_update] + output_masks_update - def update_brush_color(self,selected_mask): - sel_mask_idx = int(selected_mask[-1])-1 - if sel_mask_idx<len(self.cmy_hex): + def update_brush_color(self, selected_mask): + sel_mask_idx = int(selected_mask[-1]) - 1 + if sel_mask_idx < len(self.cmy_hex): input_img_update = gr.Image(brush_color=self.cmy_hex[sel_mask_idx]) else: - input_img_update = gr.Image(brush_color='#000000') # Return black brush - + input_img_update = gr.Image(brush_color="#000000") # Return black brush + return input_img_update - def set_add_mask_btn_interactivity(self,counts): - add_mask_btn_update = gr.Button(interactive=True) if counts<self.max_masks else gr.Button(interactive=False) + def set_add_mask_btn_interactivity(self, counts): + add_mask_btn_update = ( + gr.Button(interactive=True) + if counts < self.max_masks + else gr.Button(interactive=False) + ) return add_mask_btn_update def clear_img_update(self): - selected_mask_update = gr.Radio(choices = ["Mask 1"], value = "Mask 1") # Reset radio component to only show 'Mask 1' - prep_dl_btn_update = gr.Button(visible=False) # Make 'Prepare mask for download' button invisible - save_output_update = gr.File(visible=False) # Make File save box invisible - counts_update = gr.Number(value=1) # Reset invisible counter to 1 - input_img_update = gr.Image(value=None,interactive=False) # Set input image component to non-interactive (so a new image cannot be uploaded directly in the component) - upload_img_btn_update = gr.Button(interactive=True) # Make 'Upload image' button interactive - clear_img_btn_update = gr.Button(interactive=False) # Make 'Clear image' button non-interactive - output_masks_update = [gr.Image(value=None,visible=True if i==0 else False,interactive=False) for i in range(self.max_masks)] # Remove drawn masks and set as invisible except mask 1. 'interactive=False' added bc of bug after Gradio 3.44.x - - return [selected_mask_update, - prep_dl_btn_update, - save_output_update, - counts_update, - input_img_update, - upload_img_btn_update, - clear_img_btn_update] + output_masks_update - - def upload_img_update(self,file): - input_img_update = gr.Image(value=load(file.name),interactive=True) # Upload image from button to Image components - clear_img_btn_update = gr.Button(interactive=True) # Make 'Clear image' button interactive - upload_img_btn_update = gr.Button(interactive=False) # Make 'Upload image' button non-interactive - prep_dl_btn_update = gr.Button(visible=True) # Make 'Prepare mask for download' button visible - output_masks_update = [gr.Image(interactive=True)]*self.max_masks # This line is added bc of bug in Gradio after 3.44.x - - return [input_img_update, - clear_img_btn_update, - upload_img_btn_update, - prep_dl_btn_update] + output_masks_update - \ No newline at end of file + selected_mask_update = gr.Radio( + choices=["Mask 1"], value="Mask 1" + ) # Reset radio component to only show 'Mask 1' + prep_dl_btn_update = gr.Button( + visible=False + ) # Make 'Prepare mask for download' button invisible + save_output_update = gr.File(visible=False) # Make File save box invisible + counts_update = gr.Number(value=1) # Reset invisible counter to 1 + input_img_update = gr.Image( + value=None, interactive=False + ) # Set input image component to non-interactive (so a new image cannot be uploaded directly in the component) + upload_img_btn_update = gr.Button( + interactive=True + ) # Make 'Upload image' button interactive + clear_img_btn_update = gr.Button( + interactive=False + ) # Make 'Clear image' button non-interactive + output_masks_update = [ + gr.Image(value=None, visible=True if i == 0 else False, interactive=False) + for i in range(self.max_masks) + ] # Remove drawn masks and set as invisible except mask 1. 'interactive=False' added bc of bug after Gradio 3.44.x + + return [ + selected_mask_update, + prep_dl_btn_update, + save_output_update, + counts_update, + input_img_update, + upload_img_btn_update, + clear_img_btn_update, + ] + output_masks_update + + def upload_img_update(self, file): + input_img_update = gr.Image( + value=load(file.name), interactive=True + ) # Upload image from button to Image components + clear_img_btn_update = gr.Button( + interactive=True + ) # Make 'Clear image' button interactive + upload_img_btn_update = gr.Button( + interactive=False + ) # Make 'Upload image' button non-interactive + prep_dl_btn_update = gr.Button( + visible=True + ) # Make 'Prepare mask for download' button visible + output_masks_update = [ + gr.Image(interactive=True) + ] * self.max_masks # This line is added bc of bug in Gradio after 3.44.x + + return [ + input_img_update, + clear_img_btn_update, + upload_img_btn_update, + prep_dl_btn_update, + ] + output_masks_update + + +if __name__ == "__main__": + # Get port using the QIM API + port_dict = internal_tools.get_port_dict() + internal_tools.gradio_header(Interface().title, port_dict["port"]) + + # Creates interface + app = Interface().create_interface() + app.launch(server_name="0.0.0.0", server_port=int(port_dict["port"])) diff --git a/qim3d/gui/data_explorer.py b/qim3d/gui/data_explorer.py index 7ac43a16..76ad85ef 100644 --- a/qim3d/gui/data_explorer.py +++ b/qim3d/gui/data_explorer.py @@ -252,7 +252,6 @@ class Session: ).strftime("%Y-%m-%d %H:%M") self.file_size = os.path.getsize(self.data_path) - def create_summary_dict(self): # Create dictionary if self.error_message: @@ -477,6 +476,10 @@ class Pipeline: if __name__ == "__main__": - app = Interface() - app.show_header = True - app.launch(server_name="0.0.0.0", show_error=True) + # Get port using the QIM API + port_dict = internal_tools.get_port_dict() + internal_tools.gradio_header(Interface().title, port_dict["port"]) + + # Creates interface + app = Interface().create_interface() + app.launch(server_name="0.0.0.0", server_port=int(port_dict["port"])) diff --git a/qim3d/gui/iso3d.py b/qim3d/gui/iso3d.py index 64e760d5..60526920 100644 --- a/qim3d/gui/iso3d.py +++ b/qim3d/gui/iso3d.py @@ -398,6 +398,10 @@ class Interface: if __name__ == "__main__": - app = Interface() - app.show_header = True - app.launch(server_name="0.0.0.0", show_error=True, default_port=True) + # Get port using the QIM API + port_dict = internal_tools.get_port_dict() + internal_tools.gradio_header(Interface().title, port_dict["port"]) + + # Creates interface + app = Interface().create_interface() + app.launch(server_name="0.0.0.0", server_port=int(port_dict["port"])) diff --git a/qim3d/gui/local_thickness.py b/qim3d/gui/local_thickness.py index faf5cc62..fa4dda33 100644 --- a/qim3d/gui/local_thickness.py +++ b/qim3d/gui/local_thickness.py @@ -12,6 +12,7 @@ import plotly.graph_objects as go import localthickness as lt import matplotlib + # matplotlib.use("Agg") import matplotlib.pyplot as plt @@ -420,3 +421,13 @@ class Pipeline: tifffile.imwrite(filename, session.vol_thickness) return filename + + +if __name__ == "__main__": + # Get port using the QIM API + port_dict = internal_tools.get_port_dict() + internal_tools.gradio_header(Interface().title, port_dict["port"]) + + # Creates interface + app = Interface().create_interface() + app.launch(server_name="0.0.0.0", server_port=int(port_dict["port"])) diff --git a/qim3d/tests/utils/test_doi.py b/qim3d/tests/utils/test_doi.py index a4181088..0db54357 100644 --- a/qim3d/tests/utils/test_doi.py +++ b/qim3d/tests/utils/test_doi.py @@ -6,16 +6,10 @@ doi = "https://doi.org/10.1007/s10851-021-01041-3" def test_get_bibtex(): bibtext = qim3d.utils.doi.get_bibtex(doi) - assert ( - bibtext - == "@article{Stephensen_2021,\n\tdoi = {10.1007/s10851-021-01041-3},\n\turl = {https://doi.org/10.1007%2Fs10851-021-01041-3},\n\tyear = 2021,\n\tmonth = {jun},\n\tpublisher = {Springer Science and Business Media {LLC}},\n\tvolume = {63},\n\tnumber = {8},\n\tpages = {1069--1083},\n\tauthor = {Hans J. T. Stephensen and Anne Marie Svane and Carlos B. Villanueva and Steven A. Goldman and Jon Sporring},\n\ttitle = {Measuring Shape Relations Using r-Parallel Sets},\n\tjournal = {Journal of Mathematical Imaging and Vision}\n}" - ) + assert "Measuring Shape Relations Using r-Parallel Sets" in bibtext def test_get_reference(): reference = qim3d.utils.doi.get_reference(doi) - assert ( - reference - == "Stephensen, H. J. T., Svane, A. M., Villanueva, C. B., Goldman, S. A., & Sporring, J. (2021). Measuring Shape Relations Using r-Parallel Sets. Journal of Mathematical Imaging and Vision, 63(8), 1069รข\x80\x931083. https://doi.org/10.1007/s10851-021-01041-3\n" - ) + assert "Stephensen" in reference diff --git a/qim3d/utils/internal_tools.py b/qim3d/utils/internal_tools.py index bc06e5f9..eb18c11b 100644 --- a/qim3d/utils/internal_tools.py +++ b/qim3d/utils/internal_tools.py @@ -9,7 +9,8 @@ import numpy as np import socket import os import shutil - +import requests +import getpass from PIL import Image from pathlib import Path from qim3d.io.logger import log @@ -29,7 +30,7 @@ def mock_plot(): """ # TODO: Check if using Agg backend conflicts with other pipelines - + matplotlib.use("Agg") fig = plt.figure(figsize=(5, 4)) @@ -173,6 +174,7 @@ def sizeof(num, suffix="B"): num /= 1024.0 return f"{num:.1f} Y{suffix}" + def is_server_running(ip, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -182,9 +184,10 @@ def is_server_running(ip, port): except: return False -def temp_data(folder,remove = False,n = 3,img_shape = (32,32)): + +def temp_data(folder, remove=False, n=3, img_shape=(32, 32)): """Creates a temporary folder to test deep learning tools. - + Creates two folders, 'train' and 'test', who each also have two subfolders 'images' and 'labels'. n random images are then added to all four subfolders. If the 'remove' variable is True, the folders and their content are removed. @@ -198,8 +201,8 @@ def temp_data(folder,remove = False,n = 3,img_shape = (32,32)): Example: >>> tempdata('temporary_folder',n = 10, img_shape = (16,16)) """ - folder_trte = ['train','test'] - sub_folders = ['images','labels'] + folder_trte = ["train", "test"] + sub_folders = ["images", "labels"] # Creating train/test folder path_train = Path(folder) / folder_trte[0] @@ -212,7 +215,7 @@ def temp_data(folder,remove = False,n = 3,img_shape = (32,32)): path_test_lab = path_test / sub_folders[1] # Random image - img = np.random.randint(2,size = img_shape,dtype = np.uint8) + img = np.random.randint(2, size=img_shape, dtype=np.uint8) img = Image.fromarray(img) if not os.path.exists(path_train): @@ -221,10 +224,10 @@ def temp_data(folder,remove = False,n = 3,img_shape = (32,32)): os.makedirs(path_train_lab) os.makedirs(path_test_lab) for i in range(n): - img.save(path_train_im / f'img_train{i}.png') - img.save(path_train_lab / f'img_train{i}.png') - img.save(path_test_im / f'img_test{i}.png') - img.save(path_test_lab / f'img_test{i}.png') + img.save(path_train_im / f"img_train{i}.png") + img.save(path_train_lab / f"img_train{i}.png") + img.save(path_test_im / f"img_test{i}.png") + img.save(path_test_lab / f"img_test{i}.png") if remove: for filename in os.listdir(folder): @@ -235,13 +238,30 @@ def temp_data(folder,remove = False,n = 3,img_shape = (32,32)): elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: - log.warning('Failed to delete %s. Reason: %s' % (file_path, e)) - + log.warning("Failed to delete %s. Reason: %s" % (file_path, e)) + os.rmdir(folder) - + + def stringify_path(path): - """Converts an os.PathLike object to a string - """ - if isinstance(path,os.PathLike): + """Converts an os.PathLike object to a string""" + if isinstance(path, os.PathLike): path = path.__fspath__() return path + + +def get_port_dict(): + # Gets user and port + username = getpass.getuser() + url = f"https://platform.qim.dk/qim-api/get-port/{username}" + + response = requests.get(url, timeout=10) + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Parse the JSON response into a Python dictionary + port_dict = response.json() + else: + # Print an error message if the request was not successful + raise (f"Error: {response.status_code}") + + return port_dict -- GitLab