From 4ec2dadab5e23c6f95100dc7fda8ca2e611eff51 Mon Sep 17 00:00:00 2001 From: Mariia Date: Sun, 26 Nov 2023 22:38:34 +0100 Subject: [PATCH 1/4] add synchronized instance and label masks --- src/client/dcp_client/config.cfg | 8 +-- src/client/dcp_client/gui/main_window.py | 2 +- src/client/dcp_client/gui/napari_window.py | 59 +++++++++++++++++++++- src/server/dcp_server/models.py | 2 - src/server/dcp_server/utils.py | 1 + 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/client/dcp_client/config.cfg b/src/client/dcp_client/config.cfg index d7eb494..d9d3602 100644 --- a/src/client/dcp_client/config.cfg +++ b/src/client/dcp_client/config.cfg @@ -1,9 +1,9 @@ { "server":{ - "user": "ubuntu", - "host": "jusuf-vm2", - "data-path": "/home/ubuntu/dcp-data", - "ip": "134.94.88.74", + "user": "local", + "host": "local", + "data-path": "None", + "ip": "localhost", "port": 7010 } } \ No newline at end of file diff --git a/src/client/dcp_client/gui/main_window.py b/src/client/dcp_client/gui/main_window.py index 76264d4..b9cd2e9 100644 --- a/src/client/dcp_client/gui/main_window.py +++ b/src/client/dcp_client/gui/main_window.py @@ -15,7 +15,7 @@ class MainWindow(QWidget): '''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. + User can view the images, train the model 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 diff --git a/src/client/dcp_client/gui/napari_window.py b/src/client/dcp_client/gui/napari_window.py index ecef6a8..183dbed 100644 --- a/src/client/dcp_client/gui/napari_window.py +++ b/src/client/dcp_client/gui/napari_window.py @@ -1,14 +1,18 @@ from __future__ import annotations from typing import List, TYPE_CHECKING +import numpy as np + from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout import napari +from napari.qt import thread_worker if TYPE_CHECKING: from dcp_client.app import Application from dcp_client.utils import utils + class NapariWindow(QWidget): '''Napari Window Widget object. Opens the napari image viewer to view and fix the labeles. @@ -26,12 +30,24 @@ def __init__(self, app: Application): self.app.search_segs() # Set the viewer + + # with thread_worker(): self.viewer = napari.Viewer(show=False) + self.viewer.add_image(img, name=utils.get_path_stem(self.app.cur_selected_img)) + # print(self.app.seg_filepaths) 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)) + layer = self.viewer.layers[utils.get_path_stem(self.app.seg_filepaths[0])] + + layer.mouse_drag_callbacks.append(self.copy_mask_callback) + layer.events.set_data.connect(lambda event: self.copy_mask_callback(layer, event)) + + # @layer.mouse_drag_callbacks.appen + main_window = self.viewer.window._qt_window + layout = QVBoxLayout() layout.addWidget(main_window) @@ -48,7 +64,41 @@ def __init__(self, app: Application): layout.addLayout(buttons_layout) self.setLayout(layout) - self.show() + # self.show() + + + def copy_mask_callback(self, layer, event): + + source_mask = layer.data + + if event.type == "mouse_press": + + c, event_x, event_y = event.position + c, event_x, event_y = int(c), int(np.round(event_x)), int(np.round(event_y)) + self.event_coords = (c, event_x, event_y) + + elif event.type == "set_data": + + if self.event_coords is not None: + c, event_x, event_y = self.event_coords + + if c == 0: + + # idx = np.ix_((range(event_x - 1, event_x + 2), range(event_y - 1, event_y + 2))) + labels, counts = np.unique(source_mask[0,event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True) + labels = labels[labels > 0] + + idx = np.argmax(counts[labels > 0]) + + label = labels[idx] + + mask_fill = source_mask[0] == label + source_mask[1][mask_fill] = label + + # self.event_coords = None + else: + pass + def on_add_to_curated_button_clicked(self): ''' @@ -66,11 +116,16 @@ def on_add_to_curated_button_clicked(self): message_text = "Please select the segmenation you wish to save from the layer list" utils.create_warning_box(message_text, message_title="Warning") return + seg = self.viewer.layers[cur_seg_selected].data # Move original image self.app.move_images(self.app.train_data_path) + # print("seg") + # print(seg) + # print(seg.shape, np.unique(seg)) + # Save the (changed) seg self.app.save_image(self.app.train_data_path, cur_seg_selected+'.tiff', seg) @@ -78,6 +133,7 @@ def on_add_to_curated_button_clicked(self): self.app.delete_images(self.app.seg_filepaths) # TODO Create the Archive folder for the rest? Or move them as well? + self.viewer.close() self.close() def on_add_to_inprogress_button_clicked(self): @@ -105,4 +161,5 @@ def on_add_to_inprogress_button_clicked(self): seg = self.viewer.layers[cur_seg_selected].data self.app.save_image(self.app.inprogr_data_path, cur_seg_selected+'.tiff', seg) + self.viewer.close() self.close() \ No newline at end of file diff --git a/src/server/dcp_server/models.py b/src/server/dcp_server/models.py index caf5226..692ae2f 100644 --- a/src/server/dcp_server/models.py +++ b/src/server/dcp_server/models.py @@ -68,8 +68,6 @@ def train(self, imgs, masks): pred_masks = [self.eval(img) for img in masks] print(len(pred_masks)) self.metric = np.mean(aggregated_jaccard_index(masks, pred_masks)) - # pred_masks = [self.eval(img) for img in masks] - # self.loss = self.loss_fn(masks, pred_masks) def masks_to_outlines(self, mask): diff --git a/src/server/dcp_server/utils.py b/src/server/dcp_server/utils.py index 86f5466..607aba3 100644 --- a/src/server/dcp_server/utils.py +++ b/src/server/dcp_server/utils.py @@ -165,6 +165,7 @@ def get_centered_patches(img, if mask_class is not None: # get the class instance for the specific object instance_labels.append(l) + class_l = int(np.unique(mask_class[mask[:,:,0]==l])) #-1 because labels from mask start from 1, we want classes to start from 0 class_labels.append(class_l-1) From 135281bf1eb66469259d0808534270b87bb2024a Mon Sep 17 00:00:00 2001 From: Mariia Date: Mon, 27 Nov 2023 00:17:12 +0100 Subject: [PATCH 2/4] fix the bug, now deletion is copied to the second mask --- src/client/dcp_client/gui/napari_window.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/client/dcp_client/gui/napari_window.py b/src/client/dcp_client/gui/napari_window.py index 183dbed..ac8419d 100644 --- a/src/client/dcp_client/gui/napari_window.py +++ b/src/client/dcp_client/gui/napari_window.py @@ -66,6 +66,7 @@ def __init__(self, app: Application): self.setLayout(layout) # self.show() + def copy_mask_callback(self, layer, event): @@ -84,20 +85,15 @@ def copy_mask_callback(self, layer, event): if c == 0: - # idx = np.ix_((range(event_x - 1, event_x + 2), range(event_y - 1, event_y + 2))) labels, counts = np.unique(source_mask[0,event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True) - labels = labels[labels > 0] - - idx = np.argmax(counts[labels > 0]) - label = labels[idx] + if labels.size > 0: + + idx = np.argmax(counts) + label = labels[idx] - mask_fill = source_mask[0] == label - source_mask[1][mask_fill] = label - - # self.event_coords = None - else: - pass + mask_fill = source_mask[0] == label + source_mask[1][mask_fill] = label def on_add_to_curated_button_clicked(self): From 9a5c33804dba93e3bc55a0438b49bc23ec707701 Mon Sep 17 00:00:00 2001 From: Mariia Date: Tue, 28 Nov 2023 00:54:48 +0100 Subject: [PATCH 3/4] add drop list --- src/client/dcp_client/gui/napari_window.py | 77 +++++++++++++++------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/src/client/dcp_client/gui/napari_window.py b/src/client/dcp_client/gui/napari_window.py index ac8419d..64cb03f 100644 --- a/src/client/dcp_client/gui/napari_window.py +++ b/src/client/dcp_client/gui/napari_window.py @@ -3,7 +3,8 @@ import numpy as np -from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout +from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QComboBox, QLabel, QGridLayout +from PyQt5.QtCore import Qt import napari from napari.qt import thread_worker @@ -40,34 +41,54 @@ def __init__(self, app: Application): self.viewer.add_labels(self.app.load_image(seg_file), name=utils.get_path_stem(seg_file)) layer = self.viewer.layers[utils.get_path_stem(self.app.seg_filepaths[0])] - - layer.mouse_drag_callbacks.append(self.copy_mask_callback) - layer.events.set_data.connect(lambda event: self.copy_mask_callback(layer, event)) + + self.changed = False # @layer.mouse_drag_callbacks.appen main_window = self.viewer.window._qt_window - layout = QVBoxLayout() - layout.addWidget(main_window) + layout = QGridLayout() + layout.addWidget(main_window, 0, 0, 1, 4) + + # User hint + message_label = QLabel('Choose an active mask') + message_label.setAlignment(Qt.AlignRight) + layout.addWidget(message_label, 1, 0) + + # Drop list to choose which is an active mask + self.mask_choice_dropdown = QComboBox() + self.mask_choice_dropdown.addItem('Instance Segmentation Mask', userData=0) + self.mask_choice_dropdown.addItem('Labels Mask', userData=1) + layout.addWidget(self.mask_choice_dropdown, 1, 1) + # mask_choice_dropdown.currentIndexChanged.connect(self.on_mask_choice_changed) + + # when user has chosen the mask, we don't want to change it anymore to avoid errors + lock_button = QPushButton("Confirm Final Choice") + lock_button.clicked.connect(self.set_active_mask) + + layer.mouse_drag_callbacks.append(self.copy_mask_callback) + layer.events.set_data.connect(lambda event: self.copy_mask_callback(layer, event)) - buttons_layout = QHBoxLayout() + layout.addWidget(lock_button, 1, 2) add_to_inprogress_button = QPushButton('Move to \'Curatation in progress\' folder') - buttons_layout.addWidget(add_to_inprogress_button) + layout.addWidget(add_to_inprogress_button, 2, 0, 1, 2) add_to_inprogress_button.clicked.connect(self.on_add_to_inprogress_button_clicked) add_to_curated_button = QPushButton('Move to \'Curated dataset\' folder') - buttons_layout.addWidget(add_to_curated_button) + layout.addWidget(add_to_curated_button, 2, 2, 1, 2) add_to_curated_button.clicked.connect(self.on_add_to_curated_button_clicked) - layout.addLayout(buttons_layout) - self.setLayout(layout) - # self.show() + + def set_active_mask(self): + self.mask_choice_dropdown.setDisabled(True) + self.active_mask_index = self.mask_choice_dropdown.currentData() + def on_mask_choice_changed(self, index): + self.active_mask_index = self.mask_choice_dropdown.itemData(index) - def copy_mask_callback(self, layer, event): source_mask = layer.data @@ -76,24 +97,38 @@ def copy_mask_callback(self, layer, event): c, event_x, event_y = event.position c, event_x, event_y = int(c), int(np.round(event_x)), int(np.round(event_y)) + self.event_coords = (c, event_x, event_y) + + elif event.type == "set_data": - + + if self.event_coords is not None: c, event_x, event_y = self.event_coords + + if c == self.active_mask_index: + + # layer.mode = "PAN_ZOOM" - if c == 0: - - labels, counts = np.unique(source_mask[0,event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True) + labels, counts = np.unique(source_mask[c, event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True) if labels.size > 0: idx = np.argmax(counts) label = labels[idx] - mask_fill = source_mask[0] == label - source_mask[1][mask_fill] = label + mask_fill = source_mask[c] == label + source_mask[abs(c - 1)][mask_fill] = label + + self.changed = True + + else: + + mask_fill = source_mask[abs(c - 1)] == 0 + source_mask[c][mask_fill] = 0 + def on_add_to_curated_button_clicked(self): @@ -118,10 +153,6 @@ def on_add_to_curated_button_clicked(self): # Move original image self.app.move_images(self.app.train_data_path) - # print("seg") - # print(seg) - # print(seg.shape, np.unique(seg)) - # Save the (changed) seg self.app.save_image(self.app.train_data_path, cur_seg_selected+'.tiff', seg) From 2cd526fcc09d940a06d7be8f21a0aa0d677a7fb0 Mon Sep 17 00:00:00 2001 From: Mariia Date: Thu, 30 Nov 2023 22:46:47 +0100 Subject: [PATCH 4/4] Set instance mask as the default and modify the interface for the option with only one mask --- src/client/dcp_client/gui/napari_window.py | 53 +++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/client/dcp_client/gui/napari_window.py b/src/client/dcp_client/gui/napari_window.py index 64cb03f..b729f69 100644 --- a/src/client/dcp_client/gui/napari_window.py +++ b/src/client/dcp_client/gui/napari_window.py @@ -36,7 +36,7 @@ def __init__(self, app: Application): self.viewer = napari.Viewer(show=False) self.viewer.add_image(img, name=utils.get_path_stem(self.app.cur_selected_img)) - # print(self.app.seg_filepaths) + 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)) @@ -44,33 +44,37 @@ def __init__(self, app: Application): self.changed = False - # @layer.mouse_drag_callbacks.appen - main_window = self.viewer.window._qt_window layout = QGridLayout() layout.addWidget(main_window, 0, 0, 1, 4) - # User hint - message_label = QLabel('Choose an active mask') - message_label.setAlignment(Qt.AlignRight) - layout.addWidget(message_label, 1, 0) - - # Drop list to choose which is an active mask - self.mask_choice_dropdown = QComboBox() - self.mask_choice_dropdown.addItem('Instance Segmentation Mask', userData=0) - self.mask_choice_dropdown.addItem('Labels Mask', userData=1) - layout.addWidget(self.mask_choice_dropdown, 1, 1) - # mask_choice_dropdown.currentIndexChanged.connect(self.on_mask_choice_changed) - - # when user has chosen the mask, we don't want to change it anymore to avoid errors - lock_button = QPushButton("Confirm Final Choice") - lock_button.clicked.connect(self.set_active_mask) - - layer.mouse_drag_callbacks.append(self.copy_mask_callback) - layer.events.set_data.connect(lambda event: self.copy_mask_callback(layer, event)) + # set first mask as active by default + self.active_mask_index = 0 + + if layer.data.shape[0] >= 2: + # User hint + message_label = QLabel('Choose an active mask') + message_label.setAlignment(Qt.AlignRight) + layout.addWidget(message_label, 1, 0) + + # Drop list to choose which is an active mask + + self.mask_choice_dropdown = QComboBox() + self.mask_choice_dropdown.addItem('Instance Segmentation Mask', userData=0) + self.mask_choice_dropdown.addItem('Labels Mask', userData=1) + layout.addWidget(self.mask_choice_dropdown, 1, 1) + + + + # when user has chosen the mask, we don't want to change it anymore to avoid errors + lock_button = QPushButton("Confirm Final Choice") + lock_button.clicked.connect(self.set_active_mask) + + layout.addWidget(lock_button, 1, 2) + layer.mouse_drag_callbacks.append(self.copy_mask_callback) + layer.events.set_data.connect(lambda event: self.copy_mask_callback(layer, event)) - layout.addWidget(lock_button, 1, 2) add_to_inprogress_button = QPushButton('Move to \'Curatation in progress\' folder') layout.addWidget(add_to_inprogress_button, 2, 0, 1, 2) @@ -92,7 +96,7 @@ def on_mask_choice_changed(self, index): def copy_mask_callback(self, layer, event): source_mask = layer.data - + if event.type == "mouse_press": c, event_x, event_y = event.position @@ -100,7 +104,6 @@ def copy_mask_callback(self, layer, event): self.event_coords = (c, event_x, event_y) - elif event.type == "set_data": @@ -109,8 +112,6 @@ def copy_mask_callback(self, layer, event): c, event_x, event_y = self.event_coords if c == self.active_mask_index: - - # layer.mode = "PAN_ZOOM" labels, counts = np.unique(source_mask[c, event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True)