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 20 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
228 changes: 217 additions & 11 deletions src/client/dcp_client/gui/napari_window.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING

from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout
import numpy as np

from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QComboBox, QLabel, QGridLayout
from PyQt5.QtCore import Qt
import napari
from napari.qt import thread_worker
import numpy as np

from skimage.filters import sobel
from skimage.morphology import diamond, disk, octagon
from skimage.segmentation import watershed
from skimage.feature import canny, peak_local_max

from scipy import ndimage as ndi

import cv2

from copy import deepcopy

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 @@ -22,33 +50,208 @@ def __init__(self, app: Application):
self.setWindowTitle("napari viewer")

# Load image and get corresponding segmentation filenames
img = self.app.load_image()
self.img = self.app.load_image()
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
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))

self.viewer.add_image(self.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 = set(np.unique(self.layer.data[self.active_mask_index])[1:])
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
# for copying contours
self.instances_updated = set()

# For each instance find the contours and set the color of it to 0 to be invisible
self.find_edges()
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
# self.prev_mask = self.layer.data[0]
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

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 chosen 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
getattr(self.qctrl, target_widget).setEnabled(status)

def switch_to_active_mask(self):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this also True (as per user feedback)?


self.active_mask = True

def switch_to_non_active_mask(self):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

self.instances = set(np.unique(self.layer.data[self.active_mask_index])[1:])

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
self.mask_choice_dropdown.setDisabled(True)
self.active_mask_index = self.mask_choice_dropdown.currentData()
self.instances = set(np.unique(self.layer.data[self.active_mask_index])[1:])
self.prev_mask = self.layer.data[self.active_mask]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

if self.active_mask_index == 1:
self.switch_to_non_active_mask()

def find_edges(self, idx=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain function use

Copy link
Collaborator Author

@KorenMary KorenMary Jan 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit: 3f2edeb

'''
idx - indices of the specific labels from which to get contour
'''
if idx is not None and not isinstance(idx, list):
idx = [idx]

active_mask = self.layer.data[self.active_mask_index]

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

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

if len(instances):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
for i in instances:
if idx is None or i in idx:

mask_instance = (active_mask == i).astype(np.uint8)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why from active_mask? Shouldn't you always try to compute edges from instance mask?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right.


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
# cut the contours
self.layer.data = self.layer.data * np.invert(edges).astype(np.uint8)

def copy_mask_callback(self, layer, event):
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

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))
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

if source_mask[c, event_x, event_y] == 0:
self.new_pixel = True
christinab12 marked this conversation as resolved.
Show resolved Hide resolved
else:
self.new_pixel = False

self.event_coords = (c, event_x, event_y)



elif event.type == "set_data":

active_mask_current = self.active_mask
christinab12 marked this conversation as resolved.
Show resolved Hide resolved

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

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[c] == label

# self.changed = True
# self.instances_updated.add(label)

# Find the color of the label mask at the given point
labels_seg, counts_seg = np.unique(
source_mask[abs(c - 1)][mask_fill],
return_counts=True
)
idx_seg = np.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:

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 +269,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 +282,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 +310,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()
1 change: 1 addition & 0 deletions src/client/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
napari[pyqt5]>=0.4.17
bentoml[grpc]>=1.0.13
opencv-python>=4.8.1
pytest>=7.4.3