From 9f3fab4d5406dc30883f69ef159e70eb335446e1 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Tue, 19 Dec 2023 14:31:38 +0100 Subject: [PATCH 01/33] added worker thread for tasks to display progress bar when running long tasks --- src/client/dcp_client/app.py | 11 ++-- src/client/dcp_client/gui/main_window.py | 70 +++++++++++++++++++----- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/client/dcp_client/app.py b/src/client/dcp_client/app.py index b5e89c2..1189572 100644 --- a/src/client/dcp_client/app.py +++ b/src/client/dcp_client/app.py @@ -72,14 +72,15 @@ def run_train(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and trains the model with all data available in train_data_path """ if not self.ml_model.is_connected: connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) - if not connection_success: return "Connection could not be established. Please check if the server is running and try again." + if not connection_success: return "Warning", "Connection could not be established. Please check if the server is running and try again." # if syncer.host name is None then local machine is used to train + message_title = "Success" if self.syncer.host_name=="local": - return self.ml_model.run_train(self.train_data_path) + message_text = self.ml_model.run_train(self.train_data_path) else: srv_relative_path = self.syncer.sync(src='client', dst='server', path=self.train_data_path) - return self.ml_model.run_train(srv_relative_path) - + message_text = self.ml_model.run_train(srv_relative_path) + return message_text, message_title def run_inference(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and runs inference on all images in eval_data_path """ @@ -107,7 +108,7 @@ def run_inference(self): message_title = "Warning" else: message_text = "Success! Masks generated for all images" - message_title="Success" + message_title = "Success" return message_text, message_title def load_image(self, image_name=None): diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index d4463c6..4794365 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -1,8 +1,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QFileSystemModel, QHBoxLayout, QLabel, QTreeView -from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QFileSystemModel, QHBoxLayout, QLabel, QTreeView, QProgressBar +from PyQt5.QtCore import Qt, QThread, pyqtSignal from dcp_client.utils import settings from dcp_client.utils.utils import IconProvider, create_warning_box @@ -11,16 +11,32 @@ if TYPE_CHECKING: from dcp_client.app import Application +class WorkerThread(QThread): + ''' Worker thread for displaying Pulse ProgressBar during model serving ''' + task_finished = pyqtSignal(tuple) + def __init__(self, app: Application, task: str = None, parent = None,): + super().__init__(parent) + self.app = app + self.task = task + + def run(self): + ''' Once run_inference the tuple of (message_text, message_title) will be returned to on_finished''' + if self.task=='inference': + message_text, message_title = self.app.run_inference() + elif self.task=='train': + message_text, message_title = self.app.run_train() + self.task_finished.emit((message_text, message_title)) class MainWindow(QWidget): - '''Main Window Widget object. + ''' + Main Window Widget object. Opens the main window of the app where selected images in both directories are listed. User can view the images, train the mdoel to get the labels, and visualise the result. :param eval_data_path: Chosen path to images without labeles, selected by the user in the WelcomeWindow :type eval_data_path: string :param train_data_path: Chosen path to images with labeles, selected by the user in the WelcomeWindow :type train_data_path: string - ''' + ''' def __init__(self, app: Application): super().__init__() @@ -115,6 +131,13 @@ def main_window(self): self.main_layout.addLayout(self.curated_layout) + # add progress bar + self.progress_bar = QProgressBar(self) + self.progress_bar.setRange(0,1) + self.main_layout.addWidget(self.progress_bar) + self.worker_thread = WorkerThread(app=self.app) + self.worker_thread.task_finished.connect(self.on_finished) + self.setLayout(self.main_layout) self.show() @@ -131,12 +154,16 @@ def on_item_inprogr_selected(self, item): self.app.cur_selected_path = self.app.inprogr_data_path def on_train_button_clicked(self): - message_text = self.app.run_train() - _ = create_warning_box(message_text) + self.train_button.setEnabled(False) + self.progress_bar.setRange(0,0) + self.worker_thread.task = 'train' + self.worker_thread.start() def on_run_inference_button_clicked(self): - message_text, message_title = self.app.run_inference() - _ = create_warning_box(message_text, message_title) + self.inference_button.setEnabled(False) + self.progress_bar.setRange(0,0) + self.worker_thread.task = 'inference' + self.worker_thread.start() def on_launch_napari_button_clicked(self): ''' @@ -149,18 +176,34 @@ def on_launch_napari_button_clicked(self): self.nap_win = NapariWindow(self.app) self.nap_win.show() + def on_finished(self, result): + ''' + Is called once the worker thread emits the on finished signal + ''' + self.progress_bar.setRange(0,1) # Stop the pulsation + message_text, message_title = result + _ = create_warning_box(message_text, message_title) + self.inference_button.setEnabled(True) + self.train_button.setEnabled(True) + + if __name__ == "__main__": import sys from PyQt5.QtWidgets import QApplication - from app import Application - from bentoml_model import BentomlModel - from fsimagestorage import FilesystemImageStorage - import settings + from dcp_client.app import Application + from dcp_client.utils.bentoml_model import BentomlModel + from dcp_client.utils.fsimagestorage import FilesystemImageStorage + from dcp_client.utils import settings + from dcp_client.utils.sync_src_dst import DataRSync settings.init() image_storage = FilesystemImageStorage() ml_model = BentomlModel() + data_sync = DataRSync(user_name="local", + host_name="local", + server_repo_path=None) app = QApplication(sys.argv) app_ = Application(ml_model=ml_model, + syncer=data_sync, image_storage=image_storage, server_ip='0.0.0.0', server_port=7010, @@ -168,4 +211,5 @@ def on_launch_napari_button_clicked(self): train_data_path='', # set path inprogr_data_path='') # set path window = MainWindow(app=app_) - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) + From 281f15a12623090b5facea5e3f903023bad2275d Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Tue, 19 Dec 2023 14:52:34 +0100 Subject: [PATCH 02/33] fixed progress bar to bottom right --- src/client/dcp_client/gui/main_window.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 4794365..1b3d9c7 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -47,7 +47,8 @@ def __init__(self, app: Application): def main_window(self): self.setWindowTitle(self.title) #self.resize(1000, 1500) - self.main_layout = QHBoxLayout() + main_layout = QVBoxLayout() + dir_layout = QHBoxLayout() self.uncurated_layout = QVBoxLayout() self.inprogress_layout = QVBoxLayout() @@ -77,7 +78,7 @@ def main_window(self): self.inference_button.clicked.connect(self.on_run_inference_button_clicked) # add selected image self.uncurated_layout.addWidget(self.inference_button, alignment=Qt.AlignCenter) - self.main_layout.addLayout(self.uncurated_layout) + dir_layout.addLayout(self.uncurated_layout) # In progress layout self.inprogr_dir_layout = QVBoxLayout() @@ -103,7 +104,7 @@ def main_window(self): self.launch_nap_button.clicked.connect(self.on_launch_napari_button_clicked) # add selected image self.inprogress_layout.addWidget(self.launch_nap_button, alignment=Qt.AlignCenter) - self.main_layout.addLayout(self.inprogress_layout) + dir_layout.addLayout(self.inprogress_layout) # Curated layout self.train_dir_layout = QVBoxLayout() @@ -128,17 +129,21 @@ def main_window(self): self.train_button = QPushButton("Train Model", self) self.train_button.clicked.connect(self.on_train_button_clicked) # add selected image self.curated_layout.addWidget(self.train_button, alignment=Qt.AlignCenter) + dir_layout.addLayout(self.curated_layout) - self.main_layout.addLayout(self.curated_layout) + main_layout.addLayout(dir_layout) # add progress bar + progress_layout = QHBoxLayout() + progress_layout.addStretch(1) self.progress_bar = QProgressBar(self) self.progress_bar.setRange(0,1) - self.main_layout.addWidget(self.progress_bar) + progress_layout.addWidget(self.progress_bar) self.worker_thread = WorkerThread(app=self.app) self.worker_thread.task_finished.connect(self.on_finished) + main_layout.addLayout(progress_layout) - self.setLayout(self.main_layout) + self.setLayout(main_layout) self.show() def on_item_train_selected(self, item): @@ -211,5 +216,4 @@ def on_finished(self, result): train_data_path='', # set path inprogr_data_path='') # set path window = MainWindow(app=app_) - sys.exit(app.exec()) - + sys.exit(app.exec()) \ No newline at end of file From e655be676aec09047f2ef5e259eafd201dd49b1d Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Tue, 19 Dec 2023 16:27:19 +0100 Subject: [PATCH 03/33] function documentation --- src/client/dcp_client/gui/main_window.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 1b3d9c7..3fe13b5 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -45,6 +45,9 @@ def __init__(self, app: Application): self.main_window() def main_window(self): + ''' + Sets up the GUI + ''' self.setWindowTitle(self.title) #self.resize(1000, 1500) main_layout = QVBoxLayout() @@ -147,24 +150,39 @@ def main_window(self): self.show() def on_item_train_selected(self, item): + ''' + Is called once an image is selected in the 'curated dataset' folder + ''' self.app.cur_selected_img = item.data() self.app.cur_selected_path = self.app.train_data_path def on_item_eval_selected(self, item): + ''' + Is called once an image is selected in the 'uncurated dataset' folder + ''' self.app.cur_selected_img = item.data() self.app.cur_selected_path = self.app.eval_data_path def on_item_inprogr_selected(self, item): + ''' + Is called once an image is selected in the 'in progress' folder + ''' self.app.cur_selected_img = item.data() self.app.cur_selected_path = self.app.inprogr_data_path def on_train_button_clicked(self): + ''' + Is called once user clicks the "Train Model" button + ''' self.train_button.setEnabled(False) self.progress_bar.setRange(0,0) self.worker_thread.task = 'train' self.worker_thread.start() def on_run_inference_button_clicked(self): + ''' + Is called once user clicks the "Generate Labels" button + ''' self.inference_button.setEnabled(False) self.progress_bar.setRange(0,0) self.worker_thread.task = 'inference' From d03038e11bb419ed3e02a2565c3829fc81861437 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Tue, 19 Dec 2023 16:28:01 +0100 Subject: [PATCH 04/33] handle error from qicon and add message type in qmessagebox --- src/client/dcp_client/utils/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/client/dcp_client/utils/utils.py b/src/client/dcp_client/utils/utils.py index 1bf1bc7..9e09cab 100644 --- a/src/client/dcp_client/utils/utils.py +++ b/src/client/dcp_client/utils/utils.py @@ -13,8 +13,9 @@ def __init__(self) -> None: self.ICON_SIZE = QSize(512,512) def icon(self, type: 'QFileIconProvider.IconType'): - - fn = type.filePath() + try: + fn = type.filePath() + except AttributeError: return super().icon(type) # TODO handle exception differently? if fn.endswith(settings.accepted_types): a = QPixmap(self.ICON_SIZE) @@ -23,11 +24,17 @@ def icon(self, type: 'QFileIconProvider.IconType'): else: return super().icon(type) -def create_warning_box(message_text, message_title="Warning", add_cancel_btn=False, custom_dialog=None, sim=False): +def create_warning_box(message_text, message_title="Information", add_cancel_btn=False, custom_dialog=None, sim=False): #setup box if custom_dialog is None: msg = QMessageBox() else: msg = custom_dialog - msg.setIcon(QMessageBox.Information) + if message_title=="Warning": + message_type = QMessageBox.Warning + elif message_title=="Error": + message_type = QMessageBox.Critical + else: + message_type = QMessageBox.Information + msg.setIcon(message_type) msg.setText(message_text) msg.setWindowTitle(message_title) # if specified add a cancel button else only an ok From 57d2b76c0683bc6e8b633afd6b2590a7538ffb6f Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Tue, 19 Dec 2023 16:28:27 +0100 Subject: [PATCH 05/33] handle bentoml exceptions --- src/client/dcp_client/app.py | 24 ++++++++++++-------- src/client/dcp_client/utils/bentoml_model.py | 13 +++++++---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/client/dcp_client/app.py b/src/client/dcp_client/app.py index 1189572..bce23d3 100644 --- a/src/client/dcp_client/app.py +++ b/src/client/dcp_client/app.py @@ -74,12 +74,15 @@ def run_train(self): connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) if not connection_success: return "Warning", "Connection could not be established. Please check if the server is running and try again." # if syncer.host name is None then local machine is used to train - message_title = "Success" + message_title = "Information" if self.syncer.host_name=="local": message_text = self.ml_model.run_train(self.train_data_path) else: srv_relative_path = self.syncer.sync(src='client', dst='server', path=self.train_data_path) message_text = self.ml_model.run_train(srv_relative_path) + if message_text is None: + message_text = "An error has occured on the server. Please check your image data and configurations. If the problem persists contact your software provider." + message_title = "Error" return message_text, message_title def run_inference(self): @@ -99,16 +102,19 @@ def run_inference(self): list_of_files_not_suported = self.ml_model.run_inference(srv_relative_path) # sync data so that client gets new masks _ = self.syncer.sync(src='server', dst='client', path=self.eval_data_path) - # check if serving could not be performed for some files and prepare message - list_of_files_not_suported = list(list_of_files_not_suported) - if len(list_of_files_not_suported) > 0: - message_text = "Image types not supported. Only 2D and 3D image shapes currently supported. 3D stacks must be of type grayscale. \ - Currently supported image file formats are: " + ", ".join(settings.accepted_types)+ ". The files that were not supported are: " + ", ".join(list_of_files_not_suported) - message_title = "Warning" + if list_of_files_not_suported is None: + message_text = "An error has occured on the server. Please check your image data and configurations. If the problem persists contact your software provider." + message_title = "Error" else: - message_text = "Success! Masks generated for all images" - message_title = "Success" + list_of_files_not_suported = list(list_of_files_not_suported) + if len(list_of_files_not_suported) > 0: + message_text = "Image types not supported. Only 2D and 3D image shapes currently supported. 3D stacks must be of type grayscale. \ + Currently supported image file formats are: " + ", ".join(settings.accepted_types)+ ". The files that were not supported are: " + ", ".join(list_of_files_not_suported) + message_title = "Warning" + else: + message_text = "Success! Masks generated for all images" + message_title = "Information" return message_text, message_title def load_image(self, image_name=None): diff --git a/src/client/dcp_client/utils/bentoml_model.py b/src/client/dcp_client/utils/bentoml_model.py index 5b38f29..a6e7264 100644 --- a/src/client/dcp_client/utils/bentoml_model.py +++ b/src/client/dcp_client/utils/bentoml_model.py @@ -1,6 +1,7 @@ import asyncio from typing import Optional from bentoml.client import Client as BentoClient +from bentoml.exceptions import BentoMLException from dcp_client.app import Model @@ -24,15 +25,19 @@ def is_connected(self): return bool(self.client) async def _run_train(self, data_path): - response = await self.client.async_train(data_path) - return response + try: + response = await self.client.async_train(data_path) + return response + except BentoMLException: return None def run_train(self, data_path): return asyncio.run(self._run_train(data_path)) async def _run_inference(self, data_path): - response = await self.client.async_segment_image(data_path) - return response + try: + response = await self.client.async_segment_image(data_path) + return response + except BentoMLException: return None def run_inference(self, data_path): list_of_files_not_suported = asyncio.run(self._run_inference(data_path)) From 2c005bece855e3feea48e3b20d5f08ed3999ed7e Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 20 Dec 2023 16:43:53 +0100 Subject: [PATCH 06/33] add try_server_connection function for duplicate actions in train and inference --- src/client/dcp_client/app.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/client/dcp_client/app.py b/src/client/dcp_client/app.py index bce23d3..522b320 100644 --- a/src/client/dcp_client/app.py +++ b/src/client/dcp_client/app.py @@ -67,12 +67,18 @@ def __init__( def upload_data_to_server(self): self.syncer.first_sync(path=self.train_data_path) self.syncer.first_sync(path=self.eval_data_path) + + def try_server_connection(self): + if not self.ml_model.is_connected: + connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) + return connection_success def run_train(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and trains the model with all data available in train_data_path """ - if not self.ml_model.is_connected: - connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) - if not connection_success: return "Warning", "Connection could not be established. Please check if the server is running and try again." + if not self.try_server_connection(): + message_title = "Warning" + message_text = "Connection could not be established. Please check if the server is running and try again." + return message_text, message_title # if syncer.host name is None then local machine is used to train message_title = "Information" if self.syncer.host_name=="local": @@ -87,11 +93,10 @@ def run_train(self): def run_inference(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and runs inference on all images in eval_data_path """ - if not self.ml_model.is_connected: - connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) - if not connection_success: - message_text = "Connection could not be established. Please check if the server is running and try again." - return message_text, "Warning" + if self.try_server_connection() is False: + message_title = "Warning" + message_text = "Connection could not be established. Please check if the server is running and try again." + return message_text, message_title if self.syncer.host_name=="local": # model serving directly from local From 9c3d359e2cfb23280ad10b46890ba231d211791c Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 20 Dec 2023 16:44:12 +0100 Subject: [PATCH 07/33] function documentation --- src/client/dcp_client/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/dcp_client/app.py b/src/client/dcp_client/app.py index 522b320..9815f11 100644 --- a/src/client/dcp_client/app.py +++ b/src/client/dcp_client/app.py @@ -65,10 +65,16 @@ def __init__( self.seg_filepaths = [] def upload_data_to_server(self): + """ + Uploads the train and eval data to the server. + """ self.syncer.first_sync(path=self.train_data_path) self.syncer.first_sync(path=self.eval_data_path) def try_server_connection(self): + """ + Checks if the ml model is connected to server and attempts to connect if not. + """ if not self.ml_model.is_connected: connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) return connection_success From 3a610cca935c3fd361dd29fef93cd8eca0b197ec Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 20 Dec 2023 16:44:42 +0100 Subject: [PATCH 08/33] make buttons class vars to access from tests --- src/client/dcp_client/gui/welcome_window.py | 67 ++++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/client/dcp_client/gui/welcome_window.py b/src/client/dcp_client/gui/welcome_window.py index 5c385a5..04614fd 100644 --- a/src/client/dcp_client/gui/welcome_window.py +++ b/src/client/dcp_client/gui/welcome_window.py @@ -50,18 +50,18 @@ def __init__(self, app: Application): self.path_layout.addWidget(self.inprogr_textbox) self.path_layout.addWidget(self.train_textbox) - file_open_button_val = QPushButton('Browse',self) - file_open_button_val.show() - file_open_button_val.clicked.connect(self.browse_eval_clicked) - file_open_button_prog = QPushButton('Browse',self) - file_open_button_prog.show() - file_open_button_prog.clicked.connect(self.browse_inprogr_clicked) - file_open_button_train = QPushButton('Browse',self) - file_open_button_train.show() - file_open_button_train.clicked.connect(self.browse_train_clicked) - self.button_layout.addWidget(file_open_button_val) - self.button_layout.addWidget(file_open_button_prog) - self.button_layout.addWidget(file_open_button_train) + self.file_open_button_val = QPushButton('Browse',self) + self.file_open_button_val.show() + self.file_open_button_val.clicked.connect(self.browse_eval_clicked) + self.file_open_button_prog = QPushButton('Browse',self) + self.file_open_button_prog.show() + self.file_open_button_prog.clicked.connect(self.browse_inprogr_clicked) + self.file_open_button_train = QPushButton('Browse',self) + self.file_open_button_train.show() + self.file_open_button_train.clicked.connect(self.browse_train_clicked) + self.button_layout.addWidget(self.file_open_button_val) + self.button_layout.addWidget(self.file_open_button_prog) + self.button_layout.addWidget(self.file_open_button_train) input_layout.addLayout(self.text_layout) input_layout.addLayout(self.path_layout) @@ -72,10 +72,11 @@ def __init__(self, app: Application): self.start_button.setFixedSize(120, 30) self.start_button.show() # check if we need to upload data to server + self.done_upload = False # we only do once if self.app.syncer.host_name == "local": self.start_button.clicked.connect(self.start_main) else: - self.start_button.clicked.connect(self.start_upload) + self.start_button.clicked.connect(self.start_upload_and_main) self.main_layout.addWidget(self.start_button, alignment=Qt.AlignCenter) self.setLayout(self.main_layout) @@ -86,13 +87,15 @@ def browse_eval_clicked(self): Activates when the user clicks the button to choose the evaluation directory (QFileDialog) and displays the name of the evaluation directory chosen in the validation textbox line (QLineEdit). ''' - - fd = QFileDialog() - fd.setFileMode(QFileDialog.Directory) - if fd.exec_(): - self.app.eval_data_path = fd.selectedFiles()[0] - self.val_textbox.setText(self.app.eval_data_path) - + self.fd = QFileDialog() + try: + self.fd.setFileMode(QFileDialog.Directory) + if self.fd.exec_(): + self.app.eval_data_path = self.fd.selectedFiles()[0] + self.val_textbox.setText(self.app.eval_data_path) + finally: + self.fd = None + def browse_train_clicked(self): ''' Activates when the user clicks the button to choose the train directory (QFileDialog) and @@ -118,7 +121,6 @@ def browse_inprogr_clicked(self): self.app.inprogr_data_path = fd.selectedFiles()[0] #TODO: case when browse is clicked but nothing is specified - currently it is filled with os.getcwd() self.inprogr_textbox.setText(self.app.inprogr_data_path) - def start_main(self): ''' Starts the main window after the user clicks 'Start' and only if both evaluation and train directories are chosen. @@ -131,13 +133,18 @@ def start_main(self): message_text = "You need to specify a folder both for your uncurated and curated dataset (even if the curated folder is currently empty). Please go back and select folders for both." _ = create_warning_box(message_text, message_title="Warning") - def start_upload(self): - message_text = ("Your current configurations are set to run some operations on the cloud. \n" - "For this we need to upload your data to our server." - "We will now upload your data. Click ok to continue. \n" - "If you do not agree close the application and contact your software provider.") - usr_response = create_warning_box(message_text, message_title="Warning", add_cancel_btn=True) - if usr_response: self.app.upload_data_to_server() - self.hide() - self.mw = MainWindow(self.app) + def start_upload_and_main(self): + ''' + If the configs are set to use remote not local server then the user is asked to confirm the upload of their data + to the server and the upload starts before launching the main window. + ''' + if self.done_upload is False: + message_text = ("Your current configurations are set to run some operations on the cloud. \n" + "For this we need to upload your data to our server." + "We will now upload your data. Click ok to continue. \n" + "If you do not agree close the application and contact your software provider.") + usr_response = create_warning_box(message_text, message_title="Warning", add_cancel_btn=True) + if usr_response: self.app.upload_data_to_server() + self.done_upload = True + self.start_main() \ No newline at end of file From 7df40cf3bba5035d5c2eae58a1df37d8b5f9de87 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 20 Dec 2023 16:45:21 +0100 Subject: [PATCH 09/33] add inference tests --- src/client/test/test_app.py | 69 ++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/src/client/test/test_app.py b/src/client/test/test_app.py index f2a29ca..e742802 100644 --- a/src/client/test/test_app.py +++ b/src/client/test/test_app.py @@ -1,29 +1,39 @@ import os import sys -from skimage import data -from skimage.io import imsave +sys.path.append("../") import pytest +import subprocess +import time -sys.path.append("../") +from skimage import data +from skimage.io import imsave from dcp_client.app import Application from dcp_client.utils.bentoml_model import BentomlModel from dcp_client.utils.fsimagestorage import FilesystemImageStorage from dcp_client.utils.sync_src_dst import DataRSync - @pytest.fixture def app(): img = data.astronaut() img2 = data.cat() - os.mkdir('in_prog') + if not os.path.exists('in_prog'): + os.mkdir('in_prog') + imsave('in_prog/test_img.png', img) + imsave('in_prog/test_img2.png', img2) - imsave('in_prog/test_img.png', img) - imsave('in_prog/test_img2.png', img2) + if not os.path.exists('eval_data_path'): + os.mkdir('eval_data_path') + imsave('eval_data_path/test_img.png', img) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') - app = Application(BentomlModel(), rsyncer, FilesystemImageStorage(), "0.0.0.0", 7010) - + app = Application(BentomlModel(), + rsyncer, + FilesystemImageStorage(), + "0.0.0.0", + 7010, + os.path.join(os.getcwd(), 'eval_data_path')) + app.cur_selected_img = 'test_img.png' app.cur_selected_path = 'in_prog' @@ -43,11 +53,40 @@ def test_load_image(app): os.remove('in_prog/test_img2.png') os.rmdir('in_prog') +def test_run_inference_no_connection(app): + app, _, _ = app + message_text, message_title = app.run_inference() + assert message_text=="Connection could not be established. Please check if the server is running and try again." + assert message_title=="Warning" + +def test_run_inference_run(app): + app, _, _ = app + # start the sevrer in the background locally + command = [ + "bentoml", + "serve", + '--working-dir', + '../server/dcp_server', + "service:svc", + "--reload", + "--port=7010", + ] + process = subprocess.Popen(command) + time.sleep(60) # and wait until it is setup + # then do model serving + message_text, message_title = app.run_inference() + assert message_text== "Success! Masks generated for all images" + assert message_title=="Information" + os.remove('eval_data_path/test_img.png') + os.remove('eval_data_path/test_img_seg.tiff') + os.rmdir('eval_data_path') + process.terminate() + process.wait() + process.kill() + +''' def test_run_train(): - pass - -def test_run_inference(): - pass + def test_save_image(): pass @@ -61,9 +100,7 @@ def test_delete_images(): def test_search_segs(): pass - - - +''' From 0d9e6be5fa7d1b6a87439e25d1e7cfead7e40d95 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 20 Dec 2023 16:45:45 +0100 Subject: [PATCH 10/33] add test for welcome window --- src/client/test/test_welcome_window.py | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/client/test/test_welcome_window.py diff --git a/src/client/test/test_welcome_window.py b/src/client/test/test_welcome_window.py new file mode 100644 index 0000000..7a15b08 --- /dev/null +++ b/src/client/test/test_welcome_window.py @@ -0,0 +1,110 @@ +import pytest +import sys +sys.path.append('../') + +from PyQt5.QtWidgets import QApplication, QFileDialog, QMessageBox +from PyQt5.QtTest import QTest +from PyQt5.QtCore import Qt, QTimer, QCoreApplication + +from dcp_client.gui.welcome_window import WelcomeWindow +from dcp_client.app import Application +from dcp_client.utils.bentoml_model import BentomlModel +from dcp_client.utils.fsimagestorage import FilesystemImageStorage +from dcp_client.utils.sync_src_dst import DataRSync +from dcp_client.utils import utils +from dcp_client.utils import settings + +@pytest.fixture +def setup_global_variable(): + settings.accepted_types = (".jpg", ".jpeg", ".png", ".tiff", ".tif") + yield settings.accepted_types + +@pytest.fixture +def app(qtbot): + rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') + application = Application(BentomlModel(), rsyncer, FilesystemImageStorage(), "0.0.0.0", 7010) + # Create an instance of WelcomeWindow + #q_app = QApplication([]) + widget = WelcomeWindow(application) + qtbot.addWidget(widget) + yield widget + widget.close() + +def test_welcome_window_initialization(app): + assert app.title == "Select Dataset" + assert app.val_textbox.text() == "" + assert app.inprogr_textbox.text() == "" + assert app.train_textbox.text() == "" + +'''' +# TODO wait for github respose +def test_browse_eval_clicked(qtbot, app, monkeypatch): + # Mock the QFileDialog so that it immediately returns a directory + def handle_dialog(*args, **kwargs): + #if app.fd.isVisible(): + QCoreApplication.processEvents() + app.app.eval_data_path = '/path/to/selected/directory' + + #def mock_file_dialog(*args, **kwargs): + # return ['/path/to/selected/directory'] + + #monkeypatch.setattr(QFileDialog, 'getExistingDirectory', mock_file_dialog) + QTimer.singleShot(100, handle_dialog) + + #monkeypatch.setattr(app, 'browse_eval_clicked', mock_file_dialog) + #monkeypatch.setattr(QFileDialog, 'getExistingDirectory', mock_file_dialog) + # Simulate clicking the browse button for evaluation directory + qtbot.mouseClick(app.file_open_button_val, Qt.LeftButton, delay=1) + # Check if the textbox is updated with the selected path + assert app.val_textbox.text() == '/path/to/selected/directory' + +def test_browse_eval_clicked(qtbot, app): + # Simulate clicking the browse button for evaluation directory + qtbot.mouseClick(app.file_open_button_val, Qt.LeftButton) + # Check if the QFileDialog is shown + assert qtbot.waitUntil(lambda: hasattr(app, 'app.eval_data_path'), timeout=1000) + # Check if the textbox is updated with the selected path + assert app.val_textbox.text() == app.app.eval_data_path + + +def test_browse_train_clicked(qtbot, app): + # Simulate clicking the browse button for train directory + qtbot.mouseClick(app.file_open_button_train, Qt.LeftButton) + # Check if the QFileDialog is shown + assert qtbot.waitUntil(lambda: hasattr(app, 'app.train_data_path'), timeout=1000) + # Check if the textbox is updated with the selected path + assert app.train_textbox.text() == app.app.train_data_path + +def test_browse_inprogr_clicked(qtbot, app): + # Simulate clicking the browse button for in-progress directory + qtbot.mouseClick(app.file_open_button_prog, Qt.LeftButton) + # Check if the QFileDialog is shown + assert qtbot.waitUntil(lambda: hasattr(app, 'app.inprogr_data_path'), timeout=1000) + # Check if the textbox is updated with the selected path + assert app.inprogr_textbox.text() == app.app.inprogr_data_path + +''' +def test_start_main(qtbot, app, setup_global_variable): + settings.accepted_types = setup_global_variable + # Set some paths for testing + app.app.eval_data_path = "/path/to/eval" + app.app.train_data_path = "/path/to/train" + + result = None + def execute_warning_box(): + nonlocal result + box = QMessageBox() + result = utils.create_warning_box("Test Message", custom_dialog=box, sim=True) + qtbot.waitUntil(execute_warning_box, timeout=5000) + # Simulate clicking the start button + qtbot.mouseClick(app.start_button, Qt.LeftButton) + # Check if the main window is created + #assert qtbot.waitUntil(lambda: hasattr(app, 'mw'), timeout=1000) + assert hasattr(app, 'mw') + # Check if the WelcomeWindow is hidden + assert app.isHidden() + +''' +def test_start_upload_and_main(qtbot, app, setup_global_variable): + # TODO +''' \ No newline at end of file From ea2fc6cd030a9621f8f151b6a81710a8eabe22aa Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 08:50:19 +0100 Subject: [PATCH 11/33] remove unecessary imports --- src/client/test/test_welcome_window.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/test/test_welcome_window.py b/src/client/test/test_welcome_window.py index 7a15b08..64e7979 100644 --- a/src/client/test/test_welcome_window.py +++ b/src/client/test/test_welcome_window.py @@ -2,9 +2,8 @@ import sys sys.path.append('../') -from PyQt5.QtWidgets import QApplication, QFileDialog, QMessageBox -from PyQt5.QtTest import QTest -from PyQt5.QtCore import Qt, QTimer, QCoreApplication +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import Qt from dcp_client.gui.welcome_window import WelcomeWindow from dcp_client.app import Application From c107205452740cc252e4a4166a6f8ceb92a4f03a Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 08:56:50 +0100 Subject: [PATCH 12/33] adding server install to client workflow --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 423d425..164d004 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,11 @@ jobs: pip install coverage pip install -e ".[testing]" working-directory: src/client + + - name: Install server dependencies (for communication tests) + run: | + pip install -e ".[testing]" + working-directory: src/server - name: Test with pytest run: | From a13f212364c660c09f06884fe579bd1b9f8baef3 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 10:58:35 +0100 Subject: [PATCH 13/33] added tests to main window for qtreeview selections --- src/client/test/test_main_window.py | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/client/test/test_main_window.py diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py new file mode 100644 index 0000000..da8718f --- /dev/null +++ b/src/client/test/test_main_window.py @@ -0,0 +1,110 @@ +import os +import pytest +import sys +sys.path.append('../') + +from skimage import data +from skimage.io import imsave + +from PyQt5.QtCore import Qt +from PyQt5.QtTest import QTest + +from dcp_client.gui.main_window import MainWindow +from dcp_client.app import Application +from dcp_client.utils.bentoml_model import BentomlModel +from dcp_client.utils.fsimagestorage import FilesystemImageStorage +from dcp_client.utils.sync_src_dst import DataRSync +from dcp_client.utils import settings + +@pytest.fixture() +def setup_global_variable(): + settings.accepted_types = (".jpg", ".jpeg", ".png", ".tiff", ".tif") + yield settings.accepted_types + +@pytest.fixture +def app(qtbot, setup_global_variable): + + settings.accepted_types = setup_global_variable + + img1 = data.astronaut() + img2 = data.coffee() + img3 = data.cat() + + if not os.path.exists('train_data_path'): + os.mkdir('train_data_path') + imsave('train_data_path/astronaut.png', img1) + + if not os.path.exists('in_prog'): + os.mkdir('in_prog') + imsave('in_prog/coffee.png', img2) + + if not os.path.exists('eval_data_path'): + os.mkdir('eval_data_path') + imsave('eval_data_path/cat.png', img3) + + rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') + application = Application(BentomlModel(), + rsyncer, + FilesystemImageStorage(), + "0.0.0.0", + 7010, + 'eval_data_path', + 'train_data_path', + 'in_prog') + # Create an instance of MainWindow + widget = MainWindow(application) + qtbot.addWidget(widget) + yield widget + widget.close() + +def test_main_window_setup(qtbot, app, setup_global_variable): + settings.accepted_types = setup_global_variable + assert app.title == "Data Overview" + +def test_item_train_selected(qtbot, app, setup_global_variable): + settings.accepted_types = setup_global_variable + # Select the first item in the tree view + #index = app.list_view_train.model().index(0, 0) + index = app.list_view_train.indexAt(app.list_view_train.viewport().rect().topLeft()) + pos = app.list_view_train.visualRect(index).center() + # Simulate file click + QTest.mouseClick(app.list_view_train.viewport(), + Qt.LeftButton, + pos=pos) + + app.on_item_train_selected(index) + # Assert that the selected item matches the expected item + assert app.list_view_train.selectionModel().currentIndex() == index + assert app.app.cur_selected_img=='astronaut.png' + assert app.app.cur_selected_path=='train_data_path' + +def test_item_inprog_selected(qtbot, app, setup_global_variable): + settings.accepted_types = setup_global_variable + # Select the first item in the tree view + index = app.list_view_inprogr.indexAt(app.list_view_inprogr.viewport().rect().topLeft()) + pos = app.list_view_inprogr.visualRect(index).center() + # Simulate file click + QTest.mouseClick(app.list_view_inprogr.viewport(), + Qt.LeftButton, + pos=pos) + app.on_item_inprogr_selected(index) + # Assert that the selected item matches the expected item + assert app.list_view_inprogr.selectionModel().currentIndex() == index + assert app.app.cur_selected_img == "coffee.png" + assert app.app.cur_selected_path == app.app.inprogr_data_path + +def test_item_eval_selected(qtbot, app, setup_global_variable): + settings.accepted_types = setup_global_variable + # Select the first item in the tree view + index = app.list_view_eval.indexAt(app.list_view_eval.viewport().rect().topLeft()) + pos = app.list_view_eval.visualRect(index).center() + # Simulate file click + QTest.mouseClick(app.list_view_eval.viewport(), + Qt.LeftButton, + pos=pos) + app.on_item_eval_selected(index) + # Assert that the selected item matches the expected item + assert app.list_view_eval.selectionModel().currentIndex() == index + assert app.app.cur_selected_img=='cat.png' + assert app.app.cur_selected_path=='eval_data_path' + \ No newline at end of file From ac04cfc04e52324c1255f4126dc54864e933d1e8 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 13:13:22 +0100 Subject: [PATCH 14/33] typo --- src/client/dcp_client/gui/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 3fe13b5..0081398 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -193,7 +193,7 @@ def on_launch_napari_button_clicked(self): Launches the napari window after the image is selected. ''' if not self.app.cur_selected_img or '_seg.tiff' in self.app.cur_selected_img: - message_text = "Please first select an image you wish to visualise. The selected image must be an original images, not a mask." + message_text = "Please first select an image you wish to visualise. The selected image must be an original image, not a mask." _ = create_warning_box(message_text, message_title="Warning") else: self.nap_win = NapariWindow(self.app) From 4d2ae53b51b464048b29f39726368a465d2c942f Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 15:03:02 +0100 Subject: [PATCH 15/33] moved create warning box to class --- src/client/dcp_client/gui/_my_widget.py | 35 +++++++++++++++++++++++++ src/client/dcp_client/utils/utils.py | 31 ++-------------------- src/client/test/test_mywidget.py | 35 +++++++++++++++++++++++++ src/client/test/test_utils.py | 23 +--------------- 4 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 src/client/dcp_client/gui/_my_widget.py create mode 100644 src/client/test/test_mywidget.py diff --git a/src/client/dcp_client/gui/_my_widget.py b/src/client/dcp_client/gui/_my_widget.py new file mode 100644 index 0000000..38a3744 --- /dev/null +++ b/src/client/dcp_client/gui/_my_widget.py @@ -0,0 +1,35 @@ +from PyQt5.QtWidgets import QWidget, QMessageBox +from PyQt5.QtCore import QTimer + +class MyWidget(QWidget): + + msg = None + sim = False # will be used for testing to simulate user click + + def create_warning_box(self, message_text: str=" ", message_title: str="Information", add_cancel_btn: bool=False, custom_dialog=None) -> None: + #setup box + if custom_dialog is not None: self.msg = custom_dialog + else: self.msg = QMessageBox() + + if message_title=="Warning": + message_type = QMessageBox.Warning + elif message_title=="Error": + message_type = QMessageBox.Critical + else: + message_type = QMessageBox.Information + self.msg.setIcon(message_type) + self.msg.setText(message_text) + self.msg.setWindowTitle(message_title) + # if specified add a cancel button else only an ok + if add_cancel_btn: + self.msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) + # simulate button click if specified - workaround used for testing + if self.sim: QTimer.singleShot(0, self.msg.button(QMessageBox.Cancel).clicked) + else: + self.msg.setStandardButtons(QMessageBox.Ok) + # simulate button click if specified - workaround used for testing + if self.sim: QTimer.singleShot(0, self.msg.button(QMessageBox.Ok).clicked) + # return if user clicks Ok and False otherwise + usr_response = self.msg.exec() + if usr_response == QMessageBox.Ok: return True + else: return False \ No newline at end of file diff --git a/src/client/dcp_client/utils/utils.py b/src/client/dcp_client/utils/utils.py index 9e09cab..0560d8c 100644 --- a/src/client/dcp_client/utils/utils.py +++ b/src/client/dcp_client/utils/utils.py @@ -1,5 +1,5 @@ -from PyQt5.QtWidgets import QFileIconProvider, QMessageBox -from PyQt5.QtCore import QSize, QTimer +from PyQt5.QtWidgets import QFileIconProvider +from PyQt5.QtCore import QSize from PyQt5.QtGui import QPixmap, QIcon from pathlib import Path, PurePath @@ -24,33 +24,6 @@ def icon(self, type: 'QFileIconProvider.IconType'): else: return super().icon(type) -def create_warning_box(message_text, message_title="Information", add_cancel_btn=False, custom_dialog=None, sim=False): - #setup box - if custom_dialog is None: msg = QMessageBox() - else: msg = custom_dialog - if message_title=="Warning": - message_type = QMessageBox.Warning - elif message_title=="Error": - message_type = QMessageBox.Critical - else: - message_type = QMessageBox.Information - msg.setIcon(message_type) - msg.setText(message_text) - msg.setWindowTitle(message_title) - # if specified add a cancel button else only an ok - if add_cancel_btn: - msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) - # simulate button click if specified - workaround used for testing - if sim: QTimer.singleShot(0, msg.button(QMessageBox.Cancel).clicked) - else: - msg.setStandardButtons(QMessageBox.Ok) - # simulate button click if specified - workaround used for testing - if sim: QTimer.singleShot(0, msg.button(QMessageBox.Ok).clicked) - # return if user clicks Ok and False otherwise - usr_response = msg.exec() - if usr_response == QMessageBox.Ok: return True - else: return False - def read_config(name, config_path = 'config.cfg') -> dict: """Reads the configuration file diff --git a/src/client/test/test_mywidget.py b/src/client/test/test_mywidget.py new file mode 100644 index 0000000..95daf98 --- /dev/null +++ b/src/client/test/test_mywidget.py @@ -0,0 +1,35 @@ +import pytest +import sys +sys.path.append('../') + +from PyQt5.QtWidgets import QWidget, QMessageBox + +from dcp_client.gui._my_widget import MyWidget + +@pytest.fixture +def app(qtbot): + #q_app = QApplication([]) + widget = MyWidget() + qtbot.addWidget(widget) + yield widget + widget.close() + +def test_create_warning_box_ok(qtbot, app): + result = None + app.sim = True + def execute_warning_box(): + nonlocal result + box = QMessageBox() + result = app.create_warning_box("Test Message", custom_dialog=box) + qtbot.waitUntil(execute_warning_box, timeout=5000) + assert result is True + +def test_create_warning_box_cancel(qtbot, app): + result = None + app.sim = True + def execute_warning_box(): + nonlocal result + box = QMessageBox() + result = app.create_warning_box("Test Message", add_cancel_btn=True, custom_dialog=box) + qtbot.waitUntil(execute_warning_box, timeout=5000) # Add a timeout for the function to execute + assert result is False diff --git a/src/client/test/test_utils.py b/src/client/test/test_utils.py index 22e3b26..20144c6 100644 --- a/src/client/test/test_utils.py +++ b/src/client/test/test_utils.py @@ -1,8 +1,5 @@ import sys sys.path.append("../") -from qtpy.QtTest import QTest -from qtpy.QtWidgets import QMessageBox -from qtpy.QtCore import Qt, QTimer from dcp_client.utils import utils def test_get_relative_path(): @@ -25,22 +22,4 @@ def test_join_path(): filepath = '/here/we/are/testing/something.txt' path1 = '/here/we/are/testing' path2 = 'something.txt' - assert utils.join_path(path1, path2) == filepath - -def test_create_warning_box_ok(qtbot): - result = None - def execute_warning_box(): - nonlocal result - box = QMessageBox() - result = utils.create_warning_box("Test Message", custom_dialog=box, sim=True) - qtbot.waitUntil(execute_warning_box, timeout=5000) - assert result is True - -def test_create_warning_box_cancel(qtbot): - result = None - def execute_warning_box(): - nonlocal result - box = QMessageBox() - result = utils.create_warning_box("Test Message", add_cancel_btn=True, custom_dialog=box, sim=True) - qtbot.waitUntil(execute_warning_box, timeout=5000) # Add a timeout for the function to execute - assert result is False \ No newline at end of file + assert utils.join_path(path1, path2) == filepath \ No newline at end of file From f6df366bf998b13c0c30ab9c5b77e18c05b08ad3 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 15:03:52 +0100 Subject: [PATCH 16/33] inherit widget from MyWidget --- src/client/dcp_client/gui/main_window.py | 12 +++++++----- src/client/dcp_client/gui/napari_window.py | 17 +++++++++-------- src/client/dcp_client/gui/welcome_window.py | 11 +++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 0081398..de6aa81 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -1,12 +1,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QFileSystemModel, QHBoxLayout, QLabel, QTreeView, QProgressBar +from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QFileSystemModel, QHBoxLayout, QLabel, QTreeView, QProgressBar from PyQt5.QtCore import Qt, QThread, pyqtSignal from dcp_client.utils import settings -from dcp_client.utils.utils import IconProvider, create_warning_box +from dcp_client.utils.utils import IconProvider + from dcp_client.gui.napari_window import NapariWindow +from dcp_client.gui._my_widget import MyWidget if TYPE_CHECKING: from dcp_client.app import Application @@ -27,7 +29,7 @@ def run(self): message_text, message_title = self.app.run_train() self.task_finished.emit((message_text, message_title)) -class MainWindow(QWidget): +class MainWindow(MyWidget): ''' Main Window Widget object. Opens the main window of the app where selected images in both directories are listed. @@ -194,7 +196,7 @@ def on_launch_napari_button_clicked(self): ''' if not self.app.cur_selected_img or '_seg.tiff' in self.app.cur_selected_img: message_text = "Please first select an image you wish to visualise. The selected image must be an original image, not a mask." - _ = create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") else: self.nap_win = NapariWindow(self.app) self.nap_win.show() @@ -205,7 +207,7 @@ def on_finished(self, result): ''' self.progress_bar.setRange(0,1) # Stop the pulsation message_text, message_title = result - _ = create_warning_box(message_text, message_title) + _ = self.create_warning_box(message_text, message_title) self.inference_button.setEnabled(True) self.train_button.setEnabled(True) diff --git a/src/client/dcp_client/gui/napari_window.py b/src/client/dcp_client/gui/napari_window.py index e0451dd..74fbdcc 100644 --- a/src/client/dcp_client/gui/napari_window.py +++ b/src/client/dcp_client/gui/napari_window.py @@ -7,9 +7,10 @@ if TYPE_CHECKING: from dcp_client.app import Application -from dcp_client.utils import utils +from dcp_client.utils.utils import get_path_stem +from dcp_client.gui._my_widget import MyWidget -class NapariWindow(QWidget): +class NapariWindow(MyWidget): '''Napari Window Widget object. Opens the napari image viewer to view and fix the labeles. :param app: @@ -27,9 +28,9 @@ def __init__(self, app: Application): # Set the viewer self.viewer = napari.Viewer(show=False) - self.viewer.add_image(img, name=utils.get_path_stem(self.app.cur_selected_img)) + self.viewer.add_image(img, name=get_path_stem(self.app.cur_selected_img)) for seg_file in self.app.seg_filepaths: - self.viewer.add_labels(self.app.load_image(seg_file), name=utils.get_path_stem(seg_file)) + self.viewer.add_labels(self.app.load_image(seg_file), name=get_path_stem(seg_file)) main_window = self.viewer.window._qt_window layout = QVBoxLayout() @@ -56,7 +57,7 @@ def on_add_to_curated_button_clicked(self): ''' if self.app.cur_selected_path == str(self.app.train_data_path): message_text = "Image is already in the \'Curated data\' folder and should not be changed again" - _ = utils.create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") return # take the name of the currently selected layer (by the user) @@ -64,7 +65,7 @@ def on_add_to_curated_button_clicked(self): # TODO if more than one item is selected this will break! if '_seg' not in cur_seg_selected: message_text = "Please select the segmenation you wish to save from the layer list" - _ = utils.create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") return seg = self.viewer.layers[cur_seg_selected].data @@ -87,7 +88,7 @@ def on_add_to_inprogress_button_clicked(self): # TODO: Do we allow this? What if they moved it by mistake? User can always manually move from their folders?) if self.app.cur_selected_path == str(self.app.train_data_path): message_text = "Images from '\Curated data'\ folder can not be moved back to \'Curatation in progress\' folder." - _ = utils.create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") return # take the name of the currently selected layer (by the user) @@ -95,7 +96,7 @@ def on_add_to_inprogress_button_clicked(self): # TODO if more than one item is selected this will break! if '_seg' not in cur_seg_selected: message_text = "Please select the segmenation you wish to save from the layer list" - _ = utils.create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") return # Move original image diff --git a/src/client/dcp_client/gui/welcome_window.py b/src/client/dcp_client/gui/welcome_window.py index 04614fd..f32d5a7 100644 --- a/src/client/dcp_client/gui/welcome_window.py +++ b/src/client/dcp_client/gui/welcome_window.py @@ -1,17 +1,16 @@ from __future__ import annotations from typing import TYPE_CHECKING -from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QFileDialog, QLineEdit +from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QFileDialog, QLineEdit from PyQt5.QtCore import Qt from dcp_client.gui.main_window import MainWindow -from dcp_client.utils.utils import create_warning_box +from dcp_client.gui._my_widget import MyWidget if TYPE_CHECKING: from dcp_client.app import Application - -class WelcomeWindow(QWidget): +class WelcomeWindow(MyWidget): '''Welcome Window Widget object. The first window of the application providing a dialog that allows users to select directories. Currently supported image file types that can be selected for segmentation are: .jpg, .jpeg, .png, .tiff, .tif. @@ -131,7 +130,7 @@ def start_main(self): self.mw = MainWindow(self.app) else: message_text = "You need to specify a folder both for your uncurated and curated dataset (even if the curated folder is currently empty). Please go back and select folders for both." - _ = create_warning_box(message_text, message_title="Warning") + _ = self.create_warning_box(message_text, message_title="Warning") def start_upload_and_main(self): ''' @@ -143,7 +142,7 @@ def start_upload_and_main(self): "For this we need to upload your data to our server." "We will now upload your data. Click ok to continue. \n" "If you do not agree close the application and contact your software provider.") - usr_response = create_warning_box(message_text, message_title="Warning", add_cancel_btn=True) + usr_response = self.create_warning_box(message_text, message_title="Warning", add_cancel_btn=True) if usr_response: self.app.upload_data_to_server() self.done_upload = True self.start_main() From 36204f92e3a28ff946673a1a21a4a4000a1a64ac Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 15:04:30 +0100 Subject: [PATCH 17/33] split test launch main into two tests --- src/client/test/test_welcome_window.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/client/test/test_welcome_window.py b/src/client/test/test_welcome_window.py index 64e7979..204a1cc 100644 --- a/src/client/test/test_welcome_window.py +++ b/src/client/test/test_welcome_window.py @@ -2,7 +2,6 @@ import sys sys.path.append('../') -from PyQt5.QtWidgets import QMessageBox from PyQt5.QtCore import Qt from dcp_client.gui.welcome_window import WelcomeWindow @@ -10,7 +9,6 @@ from dcp_client.utils.bentoml_model import BentomlModel from dcp_client.utils.fsimagestorage import FilesystemImageStorage from dcp_client.utils.sync_src_dst import DataRSync -from dcp_client.utils import utils from dcp_client.utils import settings @pytest.fixture @@ -83,18 +81,18 @@ def test_browse_inprogr_clicked(qtbot, app): assert app.inprogr_textbox.text() == app.app.inprogr_data_path ''' +def test_start_main_not_selected(qtbot, app): + app.app.train_data_path = None + app.app.eval_data_path = None + app.sim = True + qtbot.mouseClick(app.start_button, Qt.LeftButton) + assert not hasattr(app, 'mw') + def test_start_main(qtbot, app, setup_global_variable): settings.accepted_types = setup_global_variable # Set some paths for testing app.app.eval_data_path = "/path/to/eval" app.app.train_data_path = "/path/to/train" - - result = None - def execute_warning_box(): - nonlocal result - box = QMessageBox() - result = utils.create_warning_box("Test Message", custom_dialog=box, sim=True) - qtbot.waitUntil(execute_warning_box, timeout=5000) # Simulate clicking the start button qtbot.mouseClick(app.start_button, Qt.LeftButton) # Check if the main window is created From a8c1074774420a7b83e2243efb13f366cca8b929 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 15:10:29 +0100 Subject: [PATCH 18/33] add test to main window for train, inf and napari launch --- src/client/test/test_main_window.py | 59 +++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index da8718f..5cba5b6 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -76,7 +76,9 @@ def test_item_train_selected(qtbot, app, setup_global_variable): # Assert that the selected item matches the expected item assert app.list_view_train.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='astronaut.png' - assert app.app.cur_selected_path=='train_data_path' + assert app.app.cur_selected_path==app.app.train_data_path + os.remove('train_data_path/astronaut.png') + os.rmdir('train_data_path') def test_item_inprog_selected(qtbot, app, setup_global_variable): settings.accepted_types = setup_global_variable @@ -92,6 +94,8 @@ def test_item_inprog_selected(qtbot, app, setup_global_variable): assert app.list_view_inprogr.selectionModel().currentIndex() == index assert app.app.cur_selected_img == "coffee.png" assert app.app.cur_selected_path == app.app.inprogr_data_path + os.remove('in_prog/coffee.png') + os.rmdir('in_prog') def test_item_eval_selected(qtbot, app, setup_global_variable): settings.accepted_types = setup_global_variable @@ -106,5 +110,54 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): # Assert that the selected item matches the expected item assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' - assert app.app.cur_selected_path=='eval_data_path' - \ No newline at end of file + assert app.app.cur_selected_path==app.app.eval_data_path + os.remove('eval_data_path/cat.png') + os.rmdir('eval_data_path') + + +def test_train_button_click(qtbot, app): + # Click the "Train Model" button + QTest.mouseClick(app.train_button, Qt.LeftButton) + # Assert that the worker thread is properly configured + assert app.worker_thread.task == 'train' + assert not app.train_button.isEnabled() + # Wait for the worker thread to finish + QTest.qWaitForWindowActive(app) + # The train functionality of the thread is tested with app tests + +def test_inference_button_click(qtbot, app): + # Click the "Generate Labels" button + QTest.mouseClick(app.inference_button, Qt.LeftButton) + # Assert that the worker thread is properly configured + assert app.worker_thread.task == 'inference' + assert not app.inference_button.isEnabled() + # Wwait for the worker thread to finish + QTest.qWaitForWindowActive(app) + # The inference functionality of the thread is tested with app tests + +def test_on_finished(qtbot, app): + assert app.train_button.isEnabled() + assert app.inference_button.isEnabled() + assert not app.worker_thread.isRunning() + +def test_launch_napari_button_click_without_selection(qtbot, app): + # Try clicking the view button without having selected an image + app.sim = True + qtbot.mouseClick(app.launch_nap_button, Qt.LeftButton) + assert not hasattr(app, 'nap_win') + +def test_launch_napari_button_click(qtbot, app): + settings.accepted_types = setup_global_variable + # Simulate selection of an image to view before clivking on view button + index = app.list_view_eval.indexAt(app.list_view_eval.viewport().rect().topLeft()) + pos = app.list_view_eval.visualRect(index).center() + # Simulate file click + QTest.mouseClick(app.list_view_eval.viewport(), + Qt.LeftButton, + pos=pos) + app.on_item_eval_selected(index) + # Now click the view button + qtbot.mouseClick(app.launch_nap_button, Qt.LeftButton) + # Assert that the napari window has launched + assert hasattr(app, 'nap_win') + assert app.nap_win.isVisible() From 01a0474f3ddcf747512f9530ade464cce35fbe62 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 17:06:42 +0100 Subject: [PATCH 19/33] add pytest-qt --- src/client/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/requirements.txt b/src/client/requirements.txt index 56eaff3..798b769 100644 --- a/src/client/requirements.txt +++ b/src/client/requirements.txt @@ -1,3 +1,4 @@ napari[pyqt5]>=0.4.17 bentoml[grpc]==1.0.16 -pytest>=7.4.3 \ No newline at end of file +pytest>=7.4.3 +pytest-qt>=4.2.0 \ No newline at end of file From 4313e7707581d1fb74a85a0cd3dc8f742ceaa3d6 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 17:28:39 +0100 Subject: [PATCH 20/33] remove unnecessary import --- src/client/test/test_mywidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/test/test_mywidget.py b/src/client/test/test_mywidget.py index 95daf98..e75172c 100644 --- a/src/client/test/test_mywidget.py +++ b/src/client/test/test_mywidget.py @@ -2,7 +2,7 @@ import sys sys.path.append('../') -from PyQt5.QtWidgets import QWidget, QMessageBox +from PyQt5.QtWidgets import QMessageBox from dcp_client.gui._my_widget import MyWidget From b1f373321fc099d70f7370454d8483e7d992616f Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Thu, 21 Dec 2023 17:28:56 +0100 Subject: [PATCH 21/33] fixed cleanup on test finishing --- src/client/test/test_app.py | 55 ++++++++++++++++------------- src/client/test/test_main_window.py | 25 +++++++++---- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/client/test/test_app.py b/src/client/test/test_app.py index e742802..cdab8d2 100644 --- a/src/client/test/test_app.py +++ b/src/client/test/test_app.py @@ -15,16 +15,17 @@ @pytest.fixture def app(): - img = data.astronaut() - img2 = data.cat() + img1 = data.astronaut() + img2 = data.coffee() + img3 = data.cat() + if not os.path.exists('in_prog'): os.mkdir('in_prog') - imsave('in_prog/test_img.png', img) - imsave('in_prog/test_img2.png', img2) + imsave('in_prog/coffee.png', img2) if not os.path.exists('eval_data_path'): os.mkdir('eval_data_path') - imsave('eval_data_path/test_img.png', img) + imsave('eval_data_path/cat.png', img3) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') app = Application(BentomlModel(), @@ -33,34 +34,30 @@ def app(): "0.0.0.0", 7010, os.path.join(os.getcwd(), 'eval_data_path')) - - app.cur_selected_img = 'test_img.png' - app.cur_selected_path = 'in_prog' - return app, img, img2 + return app, img1, img2, img3 def test_load_image(app): - app, img, img2 = app # Unpack the app, img, and img2 from the fixture + app, img, img2, _ = app # Unpack the app, img, and img2 from the fixture + + app.cur_selected_img = 'coffee.png' + app.cur_selected_path = 'in_prog' img_test = app.load_image() # if image_name is None assert img.all() == img_test.all() - img_test2 = app.load_image('test_img2.png') # if a filename is given + app.cur_selected_path = 'eval_data_path' + img_test2 = app.load_image('cat.png') # if a filename is given assert img2.all() == img_test2.all() - # delete everything we created - os.remove('in_prog/test_img.png') - os.remove('in_prog/test_img2.png') - os.rmdir('in_prog') - def test_run_inference_no_connection(app): - app, _, _ = app + app, _, _, _ = app message_text, message_title = app.run_inference() assert message_text=="Connection could not be established. Please check if the server is running and try again." assert message_title=="Warning" def test_run_inference_run(app): - app, _, _ = app + app, _, _, _ = app # start the sevrer in the background locally command = [ "bentoml", @@ -75,18 +72,29 @@ def test_run_inference_run(app): time.sleep(60) # and wait until it is setup # then do model serving message_text, message_title = app.run_inference() + # and assert returning message assert message_text== "Success! Masks generated for all images" assert message_title=="Information" - os.remove('eval_data_path/test_img.png') - os.remove('eval_data_path/test_img_seg.tiff') - os.rmdir('eval_data_path') + # finally clean up process process.terminate() process.wait() process.kill() +def test_search_segs(app): + app, _, _, _ = app + app.cur_selected_img = 'cat.png' + app.cur_selected_path = 'eval_data_path' + app.search_segs() + res = app.seg_filepaths + assert len(res)==1 + assert res[0]=='cat_seg.tiff' + # also remove the seg as it is not needed for other scripts + os.remove('eval_data_path/cat_seg.tiff') + + ''' def test_run_train(): - + pass def test_save_image(): pass @@ -97,9 +105,6 @@ def test_move_images(): def test_delete_images(): pass -def test_search_segs(): - pass - ''' diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 5cba5b6..bde4728 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -77,8 +77,6 @@ def test_item_train_selected(qtbot, app, setup_global_variable): assert app.list_view_train.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='astronaut.png' assert app.app.cur_selected_path==app.app.train_data_path - os.remove('train_data_path/astronaut.png') - os.rmdir('train_data_path') def test_item_inprog_selected(qtbot, app, setup_global_variable): settings.accepted_types = setup_global_variable @@ -94,8 +92,6 @@ def test_item_inprog_selected(qtbot, app, setup_global_variable): assert app.list_view_inprogr.selectionModel().currentIndex() == index assert app.app.cur_selected_img == "coffee.png" assert app.app.cur_selected_path == app.app.inprogr_data_path - os.remove('in_prog/coffee.png') - os.rmdir('in_prog') def test_item_eval_selected(qtbot, app, setup_global_variable): settings.accepted_types = setup_global_variable @@ -111,9 +107,6 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' assert app.app.cur_selected_path==app.app.eval_data_path - os.remove('eval_data_path/cat.png') - os.rmdir('eval_data_path') - def test_train_button_click(qtbot, app): # Click the "Train Model" button @@ -161,3 +154,21 @@ def test_launch_napari_button_click(qtbot, app): # Assert that the napari window has launched assert hasattr(app, 'nap_win') assert app.nap_win.isVisible() + + +@pytest.fixture(scope='session', autouse=True) +def cleanup_files(request): + # This code runs after all tests from all files have completed + yield + # Clean up + for fname in os.listdir('train_data_path'): + os.remove(os.path.join('train_data_path', fname)) + os.rmdir('train_data_path') + + for fname in os.listdir('in_prog'): + os.remove(os.path.join('in_prog', fname)) + os.rmdir('in_prog') + + for fname in os.listdir('eval_data_path'): + os.remove(os.path.join('eval_data_path', fname)) + os.rmdir('eval_data_path') \ No newline at end of file From f869d349e2da880c0db113a1af532fae304f0d9a Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 11:58:11 +0100 Subject: [PATCH 22/33] comment out some tests to see what's failing --- src/client/test/test_main_window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index bde4728..5fdab8e 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -107,6 +107,7 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' assert app.app.cur_selected_path==app.app.eval_data_path +''' def test_train_button_click(qtbot, app): # Click the "Train Model" button @@ -171,4 +172,5 @@ def cleanup_files(request): for fname in os.listdir('eval_data_path'): os.remove(os.path.join('eval_data_path', fname)) - os.rmdir('eval_data_path') \ No newline at end of file + os.rmdir('eval_data_path') +''' \ No newline at end of file From 25f72443ea50903f6474c6edebaf7cc42fe6d4cc Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 12:10:32 +0100 Subject: [PATCH 23/33] adding test for on train clicked --- src/client/test/test_main_window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 5fdab8e..59c0209 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -107,7 +107,6 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' assert app.app.cur_selected_path==app.app.eval_data_path -''' def test_train_button_click(qtbot, app): # Click the "Train Model" button @@ -118,7 +117,7 @@ def test_train_button_click(qtbot, app): # Wait for the worker thread to finish QTest.qWaitForWindowActive(app) # The train functionality of the thread is tested with app tests - +'''' def test_inference_button_click(qtbot, app): # Click the "Generate Labels" button QTest.mouseClick(app.inference_button, Qt.LeftButton) From 57975a2d0d728c524e6e98735515731357b3ba6c Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 12:26:06 +0100 Subject: [PATCH 24/33] added launch naparu window tests --- src/client/test/test_main_window.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 59c0209..902864f 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -107,7 +107,7 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' assert app.app.cur_selected_path==app.app.eval_data_path - +''' def test_train_button_click(qtbot, app): # Click the "Train Model" button QTest.mouseClick(app.train_button, Qt.LeftButton) @@ -117,7 +117,7 @@ def test_train_button_click(qtbot, app): # Wait for the worker thread to finish QTest.qWaitForWindowActive(app) # The train functionality of the thread is tested with app tests -'''' + def test_inference_button_click(qtbot, app): # Click the "Generate Labels" button QTest.mouseClick(app.inference_button, Qt.LeftButton) @@ -132,7 +132,7 @@ def test_on_finished(qtbot, app): assert app.train_button.isEnabled() assert app.inference_button.isEnabled() assert not app.worker_thread.isRunning() - +''' def test_launch_napari_button_click_without_selection(qtbot, app): # Try clicking the view button without having selected an image app.sim = True @@ -172,4 +172,3 @@ def cleanup_files(request): for fname in os.listdir('eval_data_path'): os.remove(os.path.join('eval_data_path', fname)) os.rmdir('eval_data_path') -''' \ No newline at end of file From b3e5636652682d37d8c67aa6527de602bb446564 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 13:14:30 +0100 Subject: [PATCH 25/33] attempt to debug actons --- src/client/dcp_client/gui/main_window.py | 20 ++++++++++++++++++-- src/client/test/test_main_window.py | 11 +++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index de6aa81..4a162b4 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -20,14 +20,30 @@ def __init__(self, app: Application, task: str = None, parent = None,): super().__init__(parent) self.app = app self.task = task - + ''' def run(self): - ''' Once run_inference the tuple of (message_text, message_title) will be returned to on_finished''' + #Once run_inference the tuple of (message_text, message_title) will be returned to on_finished if self.task=='inference': message_text, message_title = self.app.run_inference() elif self.task=='train': message_text, message_title = self.app.run_train() self.task_finished.emit((message_text, message_title)) + ''' + + def run(self): + ''' Once run_inference the tuple of (message_text, message_title) will be returned to on_finished''' + try: + if self.task == 'inference': + message_text, message_title = self.app.run_inference() + elif self.task == 'train': + message_text, message_title = self.app.run_train() + else: + message_text, message_title = "Unknown task", "Error" + + self.task_finished.emit((message_text, message_title)) + except Exception as e: + # Log any exceptions that might occur in the thread + print(f"Exception in WorkerThread: {e}") class MainWindow(MyWidget): ''' diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 902864f..bee2870 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -107,15 +107,18 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): assert app.list_view_eval.selectionModel().currentIndex() == index assert app.app.cur_selected_img=='cat.png' assert app.app.cur_selected_path==app.app.eval_data_path -''' + def test_train_button_click(qtbot, app): # Click the "Train Model" button QTest.mouseClick(app.train_button, Qt.LeftButton) # Assert that the worker thread is properly configured + print('AAAAAAAAAA') assert app.worker_thread.task == 'train' + print('BBBBBBBB') assert not app.train_button.isEnabled() + print('CCCCC') # Wait for the worker thread to finish - QTest.qWaitForWindowActive(app) + QTest.qWaitForWindowActive(app, timeout=5000) # The train functionality of the thread is tested with app tests def test_inference_button_click(qtbot, app): @@ -125,14 +128,14 @@ def test_inference_button_click(qtbot, app): assert app.worker_thread.task == 'inference' assert not app.inference_button.isEnabled() # Wwait for the worker thread to finish - QTest.qWaitForWindowActive(app) + QTest.qWaitForWindowActive(app, timeout=5000) # The inference functionality of the thread is tested with app tests def test_on_finished(qtbot, app): assert app.train_button.isEnabled() assert app.inference_button.isEnabled() assert not app.worker_thread.isRunning() -''' + def test_launch_napari_button_click_without_selection(qtbot, app): # Try clicking the view button without having selected an image app.sim = True From 8b9ab3b22edb0edc693500dae148001b3ad45baa Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 14:31:01 +0100 Subject: [PATCH 26/33] remove qWaitForWindowActive --- src/client/test/test_main_window.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index bee2870..c307fdb 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -112,13 +112,10 @@ def test_train_button_click(qtbot, app): # Click the "Train Model" button QTest.mouseClick(app.train_button, Qt.LeftButton) # Assert that the worker thread is properly configured - print('AAAAAAAAAA') assert app.worker_thread.task == 'train' - print('BBBBBBBB') assert not app.train_button.isEnabled() - print('CCCCC') # Wait for the worker thread to finish - QTest.qWaitForWindowActive(app, timeout=5000) + #QTest.qWaitForWindowActive(app, timeout=5000) # The train functionality of the thread is tested with app tests def test_inference_button_click(qtbot, app): @@ -128,7 +125,7 @@ def test_inference_button_click(qtbot, app): assert app.worker_thread.task == 'inference' assert not app.inference_button.isEnabled() # Wwait for the worker thread to finish - QTest.qWaitForWindowActive(app, timeout=5000) + #QTest.qWaitForWindowActive(app, timeout=5000) # The inference functionality of the thread is tested with app tests def test_on_finished(qtbot, app): From bb6109d1f8cd275d747a33e4036ac6334dec4209 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 16:51:56 +0100 Subject: [PATCH 27/33] fixed connection_success referenced before assignment --- src/client/dcp_client/app.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/client/dcp_client/app.py b/src/client/dcp_client/app.py index 9815f11..912bd01 100644 --- a/src/client/dcp_client/app.py +++ b/src/client/dcp_client/app.py @@ -75,13 +75,12 @@ def try_server_connection(self): """ Checks if the ml model is connected to server and attempts to connect if not. """ - if not self.ml_model.is_connected: - connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) + connection_success = self.ml_model.connect(ip=self.server_ip, port=self.server_port) return connection_success def run_train(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and trains the model with all data available in train_data_path """ - if not self.try_server_connection(): + if not self.ml_model.is_connected and not self.try_server_connection(): message_title = "Warning" message_text = "Connection could not be established. Please check if the server is running and try again." return message_text, message_title @@ -99,11 +98,11 @@ def run_train(self): def run_inference(self): """ Checks if the ml model is connected to the server, connects if not (and if possible), and runs inference on all images in eval_data_path """ - if self.try_server_connection() is False: + if not self.ml_model.is_connected and not self.try_server_connection(): message_title = "Warning" message_text = "Connection could not be established. Please check if the server is running and try again." return message_text, message_title - + if self.syncer.host_name=="local": # model serving directly from local list_of_files_not_suported = self.ml_model.run_inference(self.eval_data_path) From 5c3a36ed10f050ed845eeb2201a73c7b340ccd07 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 16:52:25 +0100 Subject: [PATCH 28/33] create a new worker thread each time button is clicked --- src/client/dcp_client/gui/main_window.py | 34 +++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 4a162b4..ef9edd6 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -20,15 +20,6 @@ def __init__(self, app: Application, task: str = None, parent = None,): super().__init__(parent) self.app = app self.task = task - ''' - def run(self): - #Once run_inference the tuple of (message_text, message_title) will be returned to on_finished - if self.task=='inference': - message_text, message_title = self.app.run_inference() - elif self.task=='train': - message_text, message_title = self.app.run_train() - self.task_finished.emit((message_text, message_title)) - ''' def run(self): ''' Once run_inference the tuple of (message_text, message_title) will be returned to on_finished''' @@ -40,10 +31,11 @@ def run(self): else: message_text, message_title = "Unknown task", "Error" - self.task_finished.emit((message_text, message_title)) except Exception as e: # Log any exceptions that might occur in the thread - print(f"Exception in WorkerThread: {e}") + message_text, message_title = f"Exception in WorkerThread: {e}", "Error" + + self.task_finished.emit((message_text, message_title)) class MainWindow(MyWidget): ''' @@ -60,6 +52,7 @@ def __init__(self, app: Application): super().__init__() self.app = app self.title = "Data Overview" + self.worker_thread = None self.main_window() def main_window(self): @@ -160,8 +153,6 @@ def main_window(self): self.progress_bar = QProgressBar(self) self.progress_bar.setRange(0,1) progress_layout.addWidget(self.progress_bar) - self.worker_thread = WorkerThread(app=self.app) - self.worker_thread.task_finished.connect(self.on_finished) main_layout.addLayout(progress_layout) self.setLayout(main_layout) @@ -194,7 +185,10 @@ def on_train_button_clicked(self): ''' self.train_button.setEnabled(False) self.progress_bar.setRange(0,0) - self.worker_thread.task = 'train' + # initialise the worker thread + self.worker_thread = WorkerThread(app=self.app, task='train') + self.worker_thread.task_finished.connect(self.on_finished) + # start the worker thread to train self.worker_thread.start() def on_run_inference_button_clicked(self): @@ -203,7 +197,10 @@ def on_run_inference_button_clicked(self): ''' self.inference_button.setEnabled(False) self.progress_bar.setRange(0,0) - self.worker_thread.task = 'inference' + # initialise the worker thread + self.worker_thread = WorkerThread(app=self.app, task='inference') + self.worker_thread.task_finished.connect(self.on_finished) + # start the worker thread to run inference self.worker_thread.start() def on_launch_napari_button_clicked(self): @@ -223,9 +220,16 @@ def on_finished(self, result): ''' self.progress_bar.setRange(0,1) # Stop the pulsation message_text, message_title = result + print("AAAAAAAAAAAAAAAA", self.worker_thread.isRunning()) _ = self.create_warning_box(message_text, message_title) self.inference_button.setEnabled(True) self.train_button.setEnabled(True) + print("BBBBBBBB", self.train_button.isEnabled(), self.inference_button.isEnabled()) + # Delete the worker thread when it's done + self.worker_thread.quit() + self.worker_thread.wait() + self.worker_thread.deleteLater() + self.worker_thread = None # Set to None to indicate it's no longer in use if __name__ == "__main__": From c3cf6f793d6384347c0892bc143ba223606ddad9 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 16:52:35 +0100 Subject: [PATCH 29/33] adjust tests --- src/client/test/test_main_window.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index c307fdb..52a04e8 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -110,28 +110,37 @@ def test_item_eval_selected(qtbot, app, setup_global_variable): def test_train_button_click(qtbot, app): # Click the "Train Model" button + app.sim = True QTest.mouseClick(app.train_button, Qt.LeftButton) + while app.worker_thread.isRunning(): QTest.qSleep(1000) # Assert that the worker thread is properly configured - assert app.worker_thread.task == 'train' - assert not app.train_button.isEnabled() - # Wait for the worker thread to finish - #QTest.qWaitForWindowActive(app, timeout=5000) + #assert app.worker_thread.task == 'train' + #assert not app.train_button.isEnabled() + # Assert that the worker thread is done and set back to None and the button has been re-enabled + #assert app.train_button.isEnabled() + #assert app.worker_thread is None # The train functionality of the thread is tested with app tests def test_inference_button_click(qtbot, app): # Click the "Generate Labels" button + app.sim = True QTest.mouseClick(app.inference_button, Qt.LeftButton) + # Assert that the worker thread is done and set back to None and the button has been re-enabled + while app.worker_thread.isRunning(): QTest.qSleep(1000) + #app.worker_thread.wait() + #assert app.inference_button.isEnabled() + #assert app.worker_thread is None # Assert that the worker thread is properly configured - assert app.worker_thread.task == 'inference' - assert not app.inference_button.isEnabled() - # Wwait for the worker thread to finish + #assert app.worker_thread.task == 'inference' + #assert not app.inference_button.isEnabled() + # Wait for the worker thread to finish #QTest.qWaitForWindowActive(app, timeout=5000) # The inference functionality of the thread is tested with app tests def test_on_finished(qtbot, app): assert app.train_button.isEnabled() assert app.inference_button.isEnabled() - assert not app.worker_thread.isRunning() + assert app.worker_thread is None def test_launch_napari_button_click_without_selection(qtbot, app): # Try clicking the view button without having selected an image From 8e6a74a386ebf2d61579e9c3d522ddb8b465a680 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 17:03:08 +0100 Subject: [PATCH 30/33] documentation --- src/client/dcp_client/gui/main_window.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index ef9edd6..2dc4cb6 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -218,13 +218,14 @@ def on_finished(self, result): ''' Is called once the worker thread emits the on finished signal ''' - self.progress_bar.setRange(0,1) # Stop the pulsation + # Stop the pulsation + self.progress_bar.setRange(0,1) + # Display message of result message_text, message_title = result - print("AAAAAAAAAAAAAAAA", self.worker_thread.isRunning()) _ = self.create_warning_box(message_text, message_title) + # Re-enable buttons self.inference_button.setEnabled(True) self.train_button.setEnabled(True) - print("BBBBBBBB", self.train_button.isEnabled(), self.inference_button.isEnabled()) # Delete the worker thread when it's done self.worker_thread.quit() self.worker_thread.wait() From 292322bfd08c24d752bbce8099527d518de89808 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 17:28:19 +0100 Subject: [PATCH 31/33] removed unnecessary lines --- src/client/test/test_main_window.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 52a04e8..ca80c6d 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -112,32 +112,21 @@ def test_train_button_click(qtbot, app): # Click the "Train Model" button app.sim = True QTest.mouseClick(app.train_button, Qt.LeftButton) + # Wait until the worker thread is done while app.worker_thread.isRunning(): QTest.qSleep(1000) - # Assert that the worker thread is properly configured - #assert app.worker_thread.task == 'train' - #assert not app.train_button.isEnabled() - # Assert that the worker thread is done and set back to None and the button has been re-enabled - #assert app.train_button.isEnabled() - #assert app.worker_thread is None # The train functionality of the thread is tested with app tests def test_inference_button_click(qtbot, app): # Click the "Generate Labels" button app.sim = True QTest.mouseClick(app.inference_button, Qt.LeftButton) - # Assert that the worker thread is done and set back to None and the button has been re-enabled + # Wait until the worker thread is done while app.worker_thread.isRunning(): QTest.qSleep(1000) - #app.worker_thread.wait() - #assert app.inference_button.isEnabled() - #assert app.worker_thread is None - # Assert that the worker thread is properly configured - #assert app.worker_thread.task == 'inference' - #assert not app.inference_button.isEnabled() - # Wait for the worker thread to finish #QTest.qWaitForWindowActive(app, timeout=5000) # The inference functionality of the thread is tested with app tests def test_on_finished(qtbot, app): + # Assert that the on_finished function re-enabled the buttons and set the worker thread to None assert app.train_button.isEnabled() assert app.inference_button.isEnabled() assert app.worker_thread is None From 932e988aaeb1182a82713325c2ece6093359489d Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 17:31:56 +0100 Subject: [PATCH 32/33] added os.chmod --- src/client/test/test_app.py | 4 ++++ src/client/test/test_fsimagestorage.py | 1 + src/client/test/test_main_window.py | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/client/test/test_app.py b/src/client/test/test_app.py index cdab8d2..a7b3c02 100644 --- a/src/client/test/test_app.py +++ b/src/client/test/test_app.py @@ -21,11 +21,15 @@ def app(): if not os.path.exists('in_prog'): os.mkdir('in_prog') + os.chmod('in_prog', 0o0777) imsave('in_prog/coffee.png', img2) + os.chmod('in_prog/coffee.png', 0o0777) if not os.path.exists('eval_data_path'): os.mkdir('eval_data_path') + os.chmod('eval_data_path', 0o0777) imsave('eval_data_path/cat.png', img3) + os.chmod('eval_data_path/cat.png', 0o0777) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') app = Application(BentomlModel(), diff --git a/src/client/test/test_fsimagestorage.py b/src/client/test/test_fsimagestorage.py index 275e5f0..e042648 100644 --- a/src/client/test/test_fsimagestorage.py +++ b/src/client/test/test_fsimagestorage.py @@ -15,6 +15,7 @@ def sample_image(): img = data.astronaut() fname = 'test_img.png' imsave(fname, img) + os.chmod(fname, 0o0777) return fname def test_load_image(fis, sample_image): diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index ca80c6d..31b7c5f 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -32,15 +32,21 @@ def app(qtbot, setup_global_variable): if not os.path.exists('train_data_path'): os.mkdir('train_data_path') + os.chmod('train_data_path', 0o0777) imsave('train_data_path/astronaut.png', img1) + os.chmod('train_data_path/astronaut.png', 0o0777) if not os.path.exists('in_prog'): os.mkdir('in_prog') + os.chmod('in_prog', 0o0777) imsave('in_prog/coffee.png', img2) + os.chmod('in_prog/coffee.png', 0o0777) if not os.path.exists('eval_data_path'): os.mkdir('eval_data_path') + os.chmod('eval_data_path', 0o0777) imsave('eval_data_path/cat.png', img3) + os.chmod('eval_data_path/cat.png', 0o0777) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') application = Application(BentomlModel(), From abeb4db323ed335bc2e0fcfb8950a0ae0e81b4a4 Mon Sep 17 00:00:00 2001 From: Christina Bukas Date: Wed, 10 Jan 2024 17:54:12 +0100 Subject: [PATCH 33/33] remove chmod --- src/client/test/test_app.py | 4 ---- src/client/test/test_fsimagestorage.py | 1 - src/client/test/test_main_window.py | 6 ------ 3 files changed, 11 deletions(-) diff --git a/src/client/test/test_app.py b/src/client/test/test_app.py index a7b3c02..cdab8d2 100644 --- a/src/client/test/test_app.py +++ b/src/client/test/test_app.py @@ -21,15 +21,11 @@ def app(): if not os.path.exists('in_prog'): os.mkdir('in_prog') - os.chmod('in_prog', 0o0777) imsave('in_prog/coffee.png', img2) - os.chmod('in_prog/coffee.png', 0o0777) if not os.path.exists('eval_data_path'): os.mkdir('eval_data_path') - os.chmod('eval_data_path', 0o0777) imsave('eval_data_path/cat.png', img3) - os.chmod('eval_data_path/cat.png', 0o0777) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') app = Application(BentomlModel(), diff --git a/src/client/test/test_fsimagestorage.py b/src/client/test/test_fsimagestorage.py index e042648..275e5f0 100644 --- a/src/client/test/test_fsimagestorage.py +++ b/src/client/test/test_fsimagestorage.py @@ -15,7 +15,6 @@ def sample_image(): img = data.astronaut() fname = 'test_img.png' imsave(fname, img) - os.chmod(fname, 0o0777) return fname def test_load_image(fis, sample_image): diff --git a/src/client/test/test_main_window.py b/src/client/test/test_main_window.py index 31b7c5f..ca80c6d 100644 --- a/src/client/test/test_main_window.py +++ b/src/client/test/test_main_window.py @@ -32,21 +32,15 @@ def app(qtbot, setup_global_variable): if not os.path.exists('train_data_path'): os.mkdir('train_data_path') - os.chmod('train_data_path', 0o0777) imsave('train_data_path/astronaut.png', img1) - os.chmod('train_data_path/astronaut.png', 0o0777) if not os.path.exists('in_prog'): os.mkdir('in_prog') - os.chmod('in_prog', 0o0777) imsave('in_prog/coffee.png', img2) - os.chmod('in_prog/coffee.png', 0o0777) if not os.path.exists('eval_data_path'): os.mkdir('eval_data_path') - os.chmod('eval_data_path', 0o0777) imsave('eval_data_path/cat.png', img3) - os.chmod('eval_data_path/cat.png', 0o0777) rsyncer = DataRSync(user_name="local", host_name="local", server_repo_path='.') application = Application(BentomlModel(),