Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Front end dev #49

Merged
merged 68 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
e119a53
Add functionality to copy from the instance mask to the label mask
KorenMary Dec 7, 2023
89aaf2e
Add functionality to delete from instance segmentatio mask
KorenMary Dec 7, 2023
f5fa662
Add dropdown list and confirmation button
KorenMary Dec 7, 2023
8490549
Set instance mask as the default and modify the interface for the opt…
KorenMary Dec 7, 2023
e9de6a8
Lock fill bucket in active mask; lock erase and brush on other mask
KorenMary Dec 7, 2023
d4653df
Fix color change issue in labels, separate colors for intersecting ob…
KorenMary Dec 11, 2023
24e2b57
Add comments
KorenMary Dec 12, 2023
5b9b388
Lock the confirmation Button; Delete redundant lines
KorenMary Dec 12, 2023
3f0634e
Delete print lines
KorenMary Dec 12, 2023
98f4a0c
Add functionality to copy from the instance mask to the label mask
KorenMary Dec 7, 2023
548b915
Add functionality to delete from instance segmentatio mask
KorenMary Dec 7, 2023
f1b8f60
Add dropdown list and confirmation button
KorenMary Dec 7, 2023
385d5f5
Set instance mask as the default and modify the interface for the opt…
KorenMary Dec 7, 2023
4b4bcac
Lock fill bucket in active mask; lock erase and brush on other mask
KorenMary Dec 7, 2023
06e00af
Fix color change issue in labels, separate colors for intersecting ob…
KorenMary Dec 11, 2023
8c680a8
Add comments
KorenMary Dec 12, 2023
aab134b
Lock the confirmation Button; Delete redundant lines
KorenMary Dec 12, 2023
a049960
Delete print lines
KorenMary Dec 12, 2023
a0aac45
Merge branch 'front-end-dev' of https://github.com/HelmholtzAI-Consul…
Dec 13, 2023
366a44d
add opencv
Dec 13, 2023
a5c96d7
remove uneeded imports and changed PyQt5 to pyqt
Dec 13, 2023
a8573f5
remove opencv dependency, switch to skimage
Dec 13, 2023
a03bf3a
Restructure code according to the review
KorenMary Dec 19, 2023
eb913d9
Merge branch 'front-end-dev' of https://github.com/HelmholtzAI-Consul…
KorenMary Dec 19, 2023
9e04613
Restructure code according to the review
KorenMary Dec 19, 2023
eb9daf7
Replaced cv2 function with skimage
KorenMary Dec 20, 2023
1dab8d3
Move the Compute4Mask to utils
KorenMary Dec 20, 2023
2bc24e8
delete imports
KorenMary Jan 6, 2024
04d2434
Allow for copy paste of paths in welcome window
KorenMary Jan 7, 2024
ecd58c3
Prevent using non-unique directory names in the welcome window.
KorenMary Jan 7, 2024
65eab3e
delete widget_list
KorenMary Jan 12, 2024
881bf7d
Merge branch 'main' into front-end-dev
KorenMary Jan 14, 2024
e4af3da
fix empty mask, when no image input present
KorenMary Jan 14, 2024
def63d1
fix if missing one line
KorenMary Jan 14, 2024
b42e90c
add needed code in if clause
KorenMary Jan 14, 2024
c02bd6e
test update to handle case of missing
KorenMary Jan 14, 2024
a1eb731
fix test
KorenMary Jan 14, 2024
1964693
fix test with path
KorenMary Jan 14, 2024
d79b1c2
change to handle missing seg mask
KorenMary Jan 14, 2024
478325a
cover event function clause
KorenMary Jan 14, 2024
04e23ce
cover more cases
KorenMary Jan 14, 2024
be48681
fix to cover event function
KorenMary Jan 14, 2024
5b48b54
Add a test to check the distinct paths in the welcome window.
KorenMary Jan 17, 2024
89f64c6
Add test on_text_changed function
KorenMary Jan 18, 2024
6d671f8
Fix an error in test_on_text_changed
KorenMary Jan 18, 2024
79d3284
test memory issue
KorenMary Jan 18, 2024
41defb5
Refactor the copy mask function
KorenMary Jan 18, 2024
92324b2
Add documentation to the new functions.
KorenMary Jan 19, 2024
f746561
Add tests for napari_ window
KorenMary Jan 19, 2024
9922ce8
Add test for update_source_mask function
KorenMary Jan 21, 2024
f6ac5c4
Add tests for add_to_curated_button function
KorenMary Jan 21, 2024
684b363
change test: remove extra cleanup, change file name
KorenMary Jan 21, 2024
905aaf5
Add cat_test.png
KorenMary Jan 21, 2024
9bdfeb7
Add extra file in eval data path
KorenMary Jan 21, 2024
3f2edeb
Add description of find_edges function
KorenMary Jan 21, 2024
5f587f2
remove unecessary imports and change folder name to in_prog to make …
Jan 23, 2024
68e9dc1
removed unecessary imports
Jan 23, 2024
1b7f007
remove test file
Jan 23, 2024
cf9eca1
remove unecessary import
Jan 23, 2024
feeda95
removed uneeded import
Jan 23, 2024
3205e03
Merge branch 'main' into front-end-dev
Jan 31, 2024
87713a4
major changes to way masks are updated, not yet tested
Feb 1, 2024
7b72c95
documentation, if class mask changes update instance, defautl to pan …
Feb 2, 2024
4c7139c
added add contours funtion to also add border pixels
Feb 5, 2024
e035288
added support for multiple segs open
Feb 8, 2024
11ba5d3
adding contours before saving images so no mismatch between pixels in…
Feb 8, 2024
2078af0
updated tests
Feb 8, 2024
8661eeb
install matplotlib in workflow file for tests to pass
Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/client/dcp_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from dcp_client.utils import utils
from dcp_client.utils import settings

#changed from here
from skimage.feature import canny, peak_local_max
import numpy as np
import cv2
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

class Model(ABC):
@abstractmethod
Expand Down Expand Up @@ -138,4 +142,83 @@ def delete_images(self, image_names):
if os.path.exists(os.path.join(self.cur_selected_path, image_name)):
self.fs_image_storage.delete_image(self.cur_selected_path, image_name)


class Compute4Mask:

@staticmethod
def get_unique_objects(active_mask):
"""
Get unique objects from the active mask.
"""

return set(np.unique(active_mask)[1:])

@staticmethod
def find_edges(instance_mask, idx=None):
'''
Find edges in the instance mask.

Parameters:
- instance_mask (numpy.ndarray): The instance mask array.
- idx (list, optional): Indices of specific labels to get contours.

Returns:
- numpy.ndarray: Array representing edges in the instance segmentation mask.
'''
if idx is not None and not isinstance(idx, list):
idx = [idx]

instances = np.unique(instance_mask)[1:]
edges = np.zeros_like(instance_mask).astype(int)

# to merge the discontinuous contours
kernel = np.ones((5, 5))

if len(instances):
for i in instances:
if idx is None or i in idx:

mask_instance = (instance_mask == i).astype(np.uint8)

edge_mask = 255 * (canny(255 * (mask_instance)) > 0).astype(np.uint8)
edge_mask = cv2.morphologyEx(
edge_mask,
cv2.MORPH_CLOSE,
kernel,
)
edges = edges + edge_mask

# if masks are intersecting then we want to count it only once
edges = edges > 0

return edges

@staticmethod
def get_rounded_pos(event_position):
"""
Get rounded position from the event position.
"""

c, event_x, event_y = event_position
return int(c), int(np.round(event_x)), int(np.round(event_y))

@staticmethod
def argmax (counts):

return np.argmax(counts)

@staticmethod
def get_unique_counts_around_event(source_mask, c, event_x, event_y):
"""
Get unique counts around the specified event position in the source mask.
"""
return np.unique(source_mask[c, event_x - 1: event_x + 2, event_y - 1: event_y + 2], return_counts=True)

@staticmethod
def get_unique_counts_for_mask(source_mask, c, mask_fill):
"""
Get unique counts for the specified mask in the source mask.
"""
return np.unique(source_mask[abs(c - 1)][mask_fill], return_counts=True)


7 changes: 4 additions & 3 deletions src/client/dcp_client/gui/main_window.py
Original file line number Diff line number Diff line change
@@ -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 qtpy.QtWidgets import QWidget, QPushButton, QVBoxLayout, QFileSystemModel, QHBoxLayout, QLabel, QTreeView
from qtpy.QtCore import Qt

from dcp_client.utils import settings
from dcp_client.utils.utils import IconProvider, create_warning_box
Expand All @@ -12,6 +12,7 @@
from dcp_client.app import Application



class MainWindow(QWidget):
'''Main Window Widget object.
Opens the main window of the app where selected images in both directories are listed.
Expand Down Expand Up @@ -147,7 +148,7 @@ def on_launch_napari_button_clicked(self):
create_warning_box(message_text, message_title="Warning")
else:
self.nap_win = NapariWindow(self.app)
self.nap_win.show()
self.nap_win.show()

if __name__ == "__main__":
import sys
Expand Down
191 changes: 182 additions & 9 deletions src/client/dcp_client/gui/napari_window.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING

from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout
from qtpy.QtWidgets import QWidget, QPushButton, QComboBox, QLabel, QGridLayout
from qtpy.QtCore import Qt
import napari
from napari.qt import thread_worker
from dcp_client.app import Compute4Mask


if TYPE_CHECKING:
from dcp_client.app import Application

from dcp_client.utils import utils

widget_list = [
'ellipse_button',
'line_button',
'path_button',
'polygon_button',
'vertex_remove_button',
'vertex_insert_button',
'move_back_button',
'move_front_button',
'label_eraser',
]

class NapariWindow(QWidget):
'''Napari Window Widget object.
Opens the napari image viewer to view and fix the labeles.
Expand All @@ -27,28 +43,182 @@ 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))

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.layer = self.viewer.layers[utils.get_path_stem(self.app.seg_filepaths[0])]
self.qctrl = self.viewer.window.qt_viewer.controls.widgets[self.layer]

self.changed = False
self.event_coords = None
self.active_mask_instance = None

main_window = self.viewer.window._qt_window
layout = QVBoxLayout()
layout.addWidget(main_window)

layout = QGridLayout()
layout.addWidget(main_window, 0, 0, 1, 4)
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

# set first mask as active by default
self.active_mask_index = 0

# unique labels
self.instances = Compute4Mask.get_unique_objects(self.layer.data[self.active_mask_index])

# for copying contours
self.instances_updated = set()

# For each instance find the contours and set the color of it to 0 to be invisible
edges = Compute4Mask.find_edges(instance_mask=self.layer.data[0])
self.layer.data = self.layer.data * (~edges).astype(int)

self.switch_to_active_mask()

if self.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.setEnabled(False)
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 chosens the mask, we don't want to change it anymore to avoid errors
lock_button = QPushButton("Confirm Final Choice")
lock_button.setEnabled(False)
lock_button.clicked.connect(self.set_active_mask)

layout.addWidget(lock_button, 1, 2)
self.layer.mouse_drag_callbacks.append(self.copy_mask_callback)
self.layer.events.set_data.connect(lambda event: self.copy_mask_callback(self.layer, event))

buttons_layout = QHBoxLayout()

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 switch_controls(self, target_widget, status: bool):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
"""
Enable or disable a specific widget.

Parameters:
- target_widget (str): The name of the widget to be controlled within the QCtrl object.
- status (bool): If True, the widget will be enabled; if False, it will be disabled.

"""
getattr(self.qctrl, target_widget).setEnabled(status)

def switch_to_active_mask(self):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
"""
Switch the application to the active mask mode by enabling 'paint_button', 'erase_button'
and 'fill_button'.
"""

self.switch_controls("paint_button", True)
self.switch_controls("erase_button", True)
self.switch_controls("fill_button", True)

self.active_mask = True

def switch_to_non_active_mask(self):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
"""
Switch the application to non-active mask mode by enabling 'fill_button' and disabling 'paint_button' and 'erase_button'.
"""

self.instances = Compute4Mask.get_unique_objects(self.layer.data[self.active_mask_index])


self.switch_controls("paint_button", False)
self.switch_controls("erase_button", False)
self.switch_controls("fill_button", True)

self.active_mask = False

def set_active_mask(self):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
"""
Sets the active mask index based on the drop down list, by default
instance segmentation mask is an active mask with index 0.
If the active mask index is 1, it switches to non-active mask mode.
"""
if self.active_mask_index == 1:
self.switch_to_non_active_mask()


def copy_mask_callback(self, layer, event):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
"""
Handles mouse press and set data events to copy masks based on the active mask index.
Parameters:
- layer: The layer object associated with the mask.
- event: The event triggering the callback.
"""

source_mask = layer.data

if event.type == "mouse_press":

c, event_x, event_y = Compute4Mask.get_rounded_pos(event.position)
self.event_coords = (c, event_x, event_y)


elif event.type == "set_data":

if self.viewer.dims.current_step[0] == self.active_mask_index:
self.switch_to_active_mask()
else:
self.switch_to_non_active_mask()

if self.event_coords is not None:

c, event_x, event_y = self.event_coords

if c == self.active_mask_index:
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

# When clicking, the mouse provides a continuous position.
# To identify the color placement, we examine nearby positions within one pixel [idx_x - 1, idx_x + 1] and [idx_y - 1, idx_y + 1].

labels, counts = Compute4Mask.get_unique_counts_around_event(source_mask, c, event_x, event_y)

if labels.size > 0:

# index of the most common color in the area around the click excluding 0
idx = Compute4Mask.argmax(counts)
# the most common color in the area around the click
label = labels[idx]
# get the mask of the instance
mask_fill = source_mask[c] == label

# Find the color of the label mask at the given point
# Determine the most common color in the label mask
labels_seg, counts_seg = Compute4Mask.get_unique_counts_for_mask(source_mask, c, mask_fill)
idx_seg = Compute4Mask.argmax(counts_seg)
label_seg = labels_seg[idx_seg]

# If a new color is used, then it is copied to a label mask
# Otherwise, we copy the existing color from the label mask

if not label in self.instances:
source_mask[abs(c - 1)][mask_fill] = label
else:
source_mask[abs(c - 1)][mask_fill] = label_seg

else:
# the only action to be applied to the instance mask is erasing.
mask_fill = source_mask[abs(c - 1)] == 0
source_mask[c][mask_fill] = 0


def on_add_to_curated_button_clicked(self):
'''
Expand All @@ -66,6 +236,7 @@ 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
Expand All @@ -78,6 +249,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):
Expand Down Expand Up @@ -105,4 +277,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()
4 changes: 2 additions & 2 deletions src/client/dcp_client/gui/welcome_window.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING

from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QFileDialog, QLineEdit
from PyQt5.QtCore import Qt
from qtpy.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QFileDialog, QLineEdit
from qtpy.QtCore import Qt

from dcp_client.gui.main_window import MainWindow
from dcp_client.utils.utils import create_warning_box
Expand Down