diff --git a/.gitignore b/.gitignore index f38ebe4d..f9f32578 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,8 @@ __pycache__/ # unwanted results files *.tif -*.tiff napari_cellseg3d/_tests/res/*.csv *.pth -*.db # Distribution / packaging .Python @@ -99,4 +97,4 @@ venv/ /napari_cellseg3d/models/saved_weights/ /docs/res/logo/old_logo/ /reqs/ - +*.db diff --git a/README.md b/README.md index 933b164e..8cd945fc 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,6 @@ A napari plugin for 3D cell segmentation: training, inference, and data review. ## Installation -Note : we recommend using conda to create a new environment for the plugin. - - conda create --name python=3.8 napari-cellseg3d - conda activate napari-cellseg3d - You can install `napari-cellseg3d` via [pip]: pip install napari-cellseg3d diff --git a/docs/conf.py b/docs/conf.py index cd40efa0..313ca06a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,7 +70,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = "en" +language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/res/code/interface.rst b/docs/res/code/interface.rst index a11c7047..674907f9 100644 --- a/docs/res/code/interface.rst +++ b/docs/res/code/interface.rst @@ -1,75 +1,50 @@ interface.py ============= -Classes +Functions ------------- -Button -************************************** -.. autoclass:: napari_cellseg3d.interface::Button - :members: __init__, visibility_condition - -DropdownMenu +open_url ************************************** -.. autoclass:: napari_cellseg3d.interface::DropdownMenu - :members: __init__ +.. autofunction:: napari_cellseg3d.interface::open_url -CheckBox -************************************** -.. autoclass:: napari_cellseg3d.interface::CheckBox - :members: __init__ -AnisotropyWidgets +make_scrollable ************************************** -.. autoclass:: napari_cellseg3d.interface::AnisotropyWidgets - :members: __init__, build, get_anisotropy_resolution_xyz, get_anisotropy_resolution_zyx, anisotropy_zoom_factor,is_enabled,toggle_permanent_visibility - +.. autofunction:: napari_cellseg3d.interface::make_scrollable -FilePathWidget -************************************** -.. autoclass:: napari_cellseg3d.interface::FilePathWidget - :members: __init__, build, get_text_field, get_button, check_ready, set_required, update_field_color, set_description -ScrollArea +make_group ************************************** -.. autoclass:: napari_cellseg3d.interface::ScrollArea - :members: __init__, make_scrollable +.. autofunction:: napari_cellseg3d.interface::make_group -DoubleIncrementCounter +add_to_group ************************************** -.. autoclass:: napari_cellseg3d.interface::DoubleIncrementCounter - :members: __init__, set_precision, make_n +.. autofunction:: napari_cellseg3d.interface::add_to_group -IntIncrementCounter +make_container ************************************** -.. autoclass:: napari_cellseg3d.interface::IntIncrementCounter - :members: __init__, make_n - +.. autofunction:: napari_cellseg3d.interface::make_container -Functions -------------- -open_url +make_button ************************************** -.. autofunction:: napari_cellseg3d.interface::open_url - +.. autofunction:: napari_cellseg3d.interface::make_button -make_group +make_combobox ************************************** -.. autofunction:: napari_cellseg3d.interface::make_group +.. autofunction:: napari_cellseg3d.interface::make_combobox -add_to_group +make_checkbox ************************************** -.. autofunction:: napari_cellseg3d.interface::add_to_group +.. autofunction:: napari_cellseg3d.interface::make_checkbox -make_container -************************************** -.. autofunction:: napari_cellseg3d.interface::make_container combine_blocks ************************************** .. autofunction:: napari_cellseg3d.interface::combine_blocks + add_blank ************************************** .. autofunction:: napari_cellseg3d.interface::add_blank diff --git a/docs/res/guides/convert_module_guide.rst b/docs/res/guides/convert_module_guide.rst index bdf2dbc2..4faf7d6e 100644 --- a/docs/res/guides/convert_module_guide.rst +++ b/docs/res/guides/convert_module_guide.rst @@ -17,12 +17,6 @@ You can : * Remove small objects : You can specify a size threshold in pixels; all objects smaller than this size will be removed in the image. -* Resize anisotropic images : - Specifiy the resolution of your microscope to remove anisotropy from images. - -.. important:: Does not work for instance labels currently. - - .. figure:: ../images/converted_labels.png :scale: 30 % :align: center diff --git a/docs/res/guides/cropping_module_guide.rst b/docs/res/guides/cropping_module_guide.rst index 70713658..5eff2ade 100644 --- a/docs/res/guides/cropping_module_guide.rst +++ b/docs/res/guides/cropping_module_guide.rst @@ -25,12 +25,6 @@ Folders can be stacks of either .png or .tif files, ideally numbered with the in You can then choose the size of the cropped volume, which will be constant throughout the process; make sure it is correct beforehand. Setting a larger size than the size of the image will cause issues. -You can also opt to correct the anisotropy if your image is anisotropic : -simply set the resolution to the one of your microscope. - -.. important:: - This will simply scale the image in the viewer, but saved images will **still be anisotropic.** To resize your image, see :doc:`convert_module_guide` - Once you are ready, you can press **Start** to start the review process. diff --git a/docs/res/guides/review_module_guide.rst b/docs/res/guides/review_module_guide.rst index a26adf55..c3c6c96a 100644 --- a/docs/res/guides/review_module_guide.rst +++ b/docs/res/guides/review_module_guide.rst @@ -19,12 +19,6 @@ Launching the review process .. note:: Only single 3D **.tif** files or 2D stacks of several **.png** or **.tif** in a folder are currently supported. -* Anisotropic data : - This will scale the images to visually remove the anisotropy, so as to make review easier. - -.. important:: - Results will still be saved as anisotropic images. If you wish to resize your images, see the :doc:`convert_module_guide` - * CSV file name : You can then provide a model name, which will be used to name the csv file recording the status of each slice. diff --git a/docs/res/logo/logo_alpha.png b/docs/res/logo/logo_alpha.png deleted file mode 100644 index f35e4711..00000000 Binary files a/docs/res/logo/logo_alpha.png and /dev/null differ diff --git a/napari_cellseg3d/_tests/test_review.py b/napari_cellseg3d/_tests/test_review.py index 97dfaa4b..9ed13c40 100644 --- a/napari_cellseg3d/_tests/test_review.py +++ b/napari_cellseg3d/_tests/test_review.py @@ -5,21 +5,23 @@ def test_launch_review(make_napari_viewer): - view = make_napari_viewer() - widget = rev.Reviewer(view) + view = make_napari_viewer() + widget = rev.Reviewer(view) - # widget.filetype_choice.setCurrentIndex(0) + # widget.filetype_choice.setCurrentIndex(0) - im_path = os.path.dirname(os.path.realpath(__file__)) + "/res/test.tif" + im_path = os.path.dirname(os.path.realpath(__file__)) + "/res/test.tif" - widget.image_path = im_path - widget.label_path = im_path + widget.image_path = im_path + widget.label_path = im_path + + print(widget.image_path) + print(widget.label_path) + print(widget.as_folder) + print(widget.filetype) + widget.run_review() + widget._viewer.close() + + assert widget._viewer is not None - print(widget.image_path) - print(widget.label_path) - print(widget.as_folder) - print(widget.filetype) - widget.run_review() - widget._viewer.close() - assert widget._viewer is not None diff --git a/napari_cellseg3d/interface.py b/napari_cellseg3d/interface.py index 3f35e078..7f39e3bb 100644 --- a/napari_cellseg3d/interface.py +++ b/napari_cellseg3d/interface.py @@ -1,6 +1,5 @@ from typing import Optional from typing import Union -from typing import List from qtpy.QtCore import Qt from qtpy.QtCore import QUrl @@ -14,7 +13,6 @@ from qtpy.QtWidgets import QHBoxLayout from qtpy.QtWidgets import QLabel from qtpy.QtWidgets import QLayout -from qtpy.QtWidgets import QLineEdit from qtpy.QtWidgets import QPushButton from qtpy.QtWidgets import QScrollArea from qtpy.QtWidgets import QSizePolicy @@ -47,515 +45,6 @@ dark_red = "#72071d" # crimson red default_cyan = "#8dd3c7" # turquoise cyan (default matplotlib line color under dark background context) napari_grey = "#262930" # napari background color (grey) -############### - - -def toggle_visibility(checkbox, widget): - """Toggles the visibility of a widget based on the status of a checkbox. - - Args: - checkbox: The QCheckbox that determines whether to show or not - widget: The widget to hide or show - """ - widget.setVisible(checkbox.isChecked()) - - -class Button(QPushButton): - """Class for a button with a title and connected to a function when clicked. Inherits from QPushButton. - - Args: - title (str-like): title of the button. Defaults to None, if None no title is set - func (callable): function to execute when button is clicked. Defaults to None, no binding is made if None - parent (QWidget): parent QWidget to add button to. Defaults to None, no parent is set if None - fixed (bool): if True, will set the size policy of the button to Fixed in h and w. Defaults to True. - - """ - - def __init__( - self, - title: str = None, - func: callable = None, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - super().__init__(parent) - if title is not None: - self.setText(title) - - if func is not None: - self.clicked.connect(func) - - if fixed: - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - def visibility_condition(self, checkbox): - toggle_visibility(checkbox, self) - - -class DropdownMenu(QComboBox): - """Creates a dropdown menu with a title and adds specified entries to it""" - - def __init__( - self, - entries: Optional[list] = None, - parent: Optional[QWidget] = None, - label: Optional[str] = None, - fixed: Optional[bool] = True, - ): - """Args: - entries (array(str)): Entries to add to the dropdown menu. Defaults to None, no entries if None - parent (QWidget): parent QWidget to add dropdown menu to. Defaults to None, no parent is set if None - label (str) : if not None, creates a QLabel with the contents of 'label', and returns the label as well - fixed (bool): if True, will set the size policy of the dropdown menu to Fixed in h and w. Defaults to True. - """ - super().__init__(parent) - self.label = None - if entries is not None: - self.addItems(entries) - if label is not None: - self.label = QLabel(label) - if fixed: - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - -class CheckBox(QCheckBox): - """Shortcut class for creating QCheckBox with a title and a function""" - - def __init__( - self, - title: Optional[str] = None, - func: Optional[callable] = None, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - """ - Args: - title (str-like): title of the checkbox. Defaults to None, if None no title is set - func (callable): function to execute when checkbox is toggled. Defaults to None, no binding is made if None - parent (QWidget): parent QWidget to add checkbox to. Defaults to None, no parent is set if None - fixed (bool): if True, will set the size policy of the checkbox to Fixed in h and w. Defaults to True. - """ - super().__init__(title, parent) - if fixed: - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - if func is not None: - self.toggled.connect(func) - - -class AnisotropyWidgets(QWidget): - """Class that creates widgets for anisotropy handling. Includes : - - A checkbox to hides or shows the controls - - Three spinboxes to enter resolution for each dimension""" - - def __init__( - self, - parent: Optional[QWidget] = None, - default_x: Optional[float] = 1.0, - default_y: Optional[float] = 1.0, - default_z: Optional[float] = 1.0, - always_visible: Optional[bool] = False, - ): - """Creates an instance of AnisotropyWidgets - Args: - - parent: parent QWidget - - default_x: default resolution to use for x axis in microns - - default_y: default resolution to use for y axis in microns - - default_z: default resolution to use for z axis in microns - """ - super().__init__(parent) - - self._layout = QVBoxLayout() - self._layout.setSpacing(0) - self._layout.setContentsMargins(0, 0, 0, 0) - - self.container, self._boxes_layout = make_container(T=7, parent=parent) - self.checkbox = make_checkbox( - "Anisotropic data", self._toggle_display_aniso, parent - ) - - self.box_widgets = DoubleIncrementCounter.make_n( - n=3, min=1.0, max=1000, default=1, step=0.5 - ) - self.box_widgets[0].setValue(default_x) - self.box_widgets[1].setValue(default_y) - self.box_widgets[2].setValue(default_z) - - for w in self.box_widgets: - w.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.box_widgets_lbl = [ - make_label("Resolution in " + axis + " (microns) :", parent=parent) - for axis in "xyz" - ] - - ################## - # tooltips - self.checkbox.setToolTip( - "If you have anisotropic data, you can scale data using your resolution in microns" - ) - [ - w.setToolTip(f"Anisotropic resolution in microns for {dim} axis") - for w, dim in zip(self.box_widgets, "xyz") - ] - ################## - - self.build() - - if always_visible: - self.toggle_permanent_visibility() - - def _toggle_display_aniso(self): - """Shows the choices for correcting anisotropy when viewing results depending on whether :py:attr:`self.checkbox` is checked""" - toggle_visibility(self.checkbox, self.container) - - def build(self): - """Builds the layout of the widget""" - [ - self._boxes_layout.addWidget(widget, alignment=HCENTER_AL) - for widgets in zip(self.box_widgets_lbl, self.box_widgets) - for widget in widgets - ] - # anisotropy - self.container.setLayout(self._boxes_layout) - self.container.setVisible(False) - - add_widgets(self._layout, [self.checkbox, self.container]) - self.setLayout(self._layout) - - def get_anisotropy_resolution_xyz(self, as_factors=True): - """ - Args : - as_factors: if True, returns zoom factors, otherwise returns the input resolution - - Returns : the resolution in microns for each of the three dimensions. ZYX order suitable for napari scale""" - - resolution = [w.value() for w in self.box_widgets] - if as_factors: - return self.anisotropy_zoom_factor(resolution) - - return resolution - - def get_anisotropy_resolution_zyx(self, as_factors=True): - """ - Args : - as_factors: if True, returns zoom factors, otherwise returns the input resolution - - Returns : the resolution in microns for each of the three dimensions. XYZ order suitable for MONAI""" - resolution = [w.value() for w in self.box_widgets] - if as_factors: - resolution = self.anisotropy_zoom_factor(resolution) - - return [resolution[2], resolution[1], resolution[0]] - - @staticmethod - def anisotropy_zoom_factor(aniso_res): - """Computes a zoom factor to correct anisotropy, based on anisotropy resolutions - - Args: - aniso_res: array for anisotropic resolution (float) in microns for each axis - - Returns: an array with the corresponding zoom factors for each axis (all values divided by min) - - """ - - base = min(aniso_res) - zoom_factors = [base / res for res in aniso_res] - return zoom_factors - - def is_enabled(self): - """Returns : whether anisotropy correction has been enabled or not""" - return self.checkbox.isChecked() - - def toggle_permanent_visibility(self): - """Hides the checkbox and always display resolution spinboxes""" - self.checkbox.toggle() - self.checkbox.setVisible(False) - - -class FilePathWidget( - QWidget -): # TODO upgrade logic, include load as folder, highlight if incorrect ? - """Widget to handle the choice of file paths for data throughout the plugin. Provides the following elements : - - An "Open" button to show a file dialog (defined externally) - - A QLineEdit in read only to display the chosen path/file""" - - def __init__( - self, - description: str, - file_function: callable, - parent: Optional[QWidget] = None, - required: Optional[bool] = True, - ): - """Creates a FilePathWidget. - Args: - description (str): Initial text to add to the text box - file_function (callable): Function to handle the file dialog - parent (Optional[QWidget]): parent QWidget - required (Optional[bool]): if True, field will be highlighted in red if empty. Defaults to False. - """ - super().__init__(parent) - self._layout = QHBoxLayout() - self._layout.setSpacing(0) - self._layout.setContentsMargins(0, 0, 0, 0) - - self._initial_desc = description - self.text_field = QLineEdit(description, self) - - self.button = Button("Open", file_function, parent=self, fixed=True) - - self.text_field.setReadOnly(True) - - self.set_required(required) - - def build(self): - """Builds the layout of the widget""" - add_widgets(self._layout, [self.text_field, self.button]) - self.setLayout(self._layout) - - def get_text_field(self): - """Get text field with file path""" - return self.text_field - - def get_button(self): - """Get "Open" button""" - return self.button - - def check_ready(self): - """Check if a path is correctly set""" - if self.text_field.text() in ["", self._initial_desc]: - self.update_field_color("indianred") - self.text_field.setToolTip("Mandatory field !") - return False - else: - self.update_field_color("black") - return True - - def set_required(self, is_required): - """If set to True, will be colored red if incorrectly set""" - if is_required: - self.text_field.textChanged.connect(self.check_ready) - self.check_ready() - - def update_field_color(self, color: str): - """Updates the background of the text field""" - self.text_field.setStyleSheet(f"background-color : {color}") - self.text_field.style().unpolish(self.text_field) - self.text_field.style().polish(self.text_field) - - def set_description(self, text: str): - """Sets the initial description ins the text field""" - self._initial_desc = text - self.text_field.setText(text) - - -class ScrollArea(QScrollArea): - """Creates a QScrollArea and sets it up, then adds the contained_layout to it.""" - - def __init__( - self, - contained_layout: QLayout, - min_wh: Optional[List[int]] = None, - max_wh: Optional[List[int]] = None, - base_wh: Optional[List[int]] = None, - parent: Optional[QWidget] = None, - ): - """ - Args: - contained_layout (QLayout): the layout of widgets to be made scrollable - min_wh (Optional[List[int]]): array of two ints for respectively the minimum width and minimum height of the scrollable area. Defaults to None, lets Qt decide if None - max_wh (Optional[List[int]]): array of two ints for respectively the maximum width and maximum height of the scrollable area. Defaults to None, lets Qt decide if None - base_wh (Optional[List[int]]): array of two ints for respectively the initial width and initial height of the scrollable area. Defaults to None, lets Qt decide if None - parent (Optional[QWidget]): array of two ints for respectively the initial width and initial height of the scrollable area. Defaults to None, lets Qt decide if None - """ - # TODO : optimize the number of created objects ? - super().__init__(parent) - - self._container_widget = ( - QWidget() - ) # required to use QScrollArea.setWidget() - self._container_widget.setSizePolicy( - QSizePolicy.Fixed, QSizePolicy.Maximum - ) - self._container_widget.setLayout(contained_layout) - self._container_widget.adjustSize() - - self.setWidget(self._container_widget) - self.setWidgetResizable(True) - self.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding - ) - - if base_wh is not None: - self.setBaseSize(base_wh[0], base_wh[1]) - if max_wh is not None: - self.setMaximumSize(max_wh[0], max_wh[1]) - if min_wh is not None: - self.setMinimumSize(min_wh[0], min_wh[1]) - - self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) - self.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - - @classmethod - def make_scrollable( - cls, - contained_layout: QLayout, - parent: QWidget, - min_wh: Optional[List[int]] = None, - max_wh: Optional[List[int]] = None, - base_wh: Optional[List[int]] = None, - ): - """Factory method to create a scroll area in a widget - Args: - contained_layout (QLayout): the widget to be made scrollable - parent (QWidget): the parent widget to add the resulting scroll area in - min_wh (Optional[List[int]]): array of two ints for respectively the minimum width and minimum height of the scrollable area. Defaults to None, lets Qt decide if None - max_wh (Optional[List[int]]): array of two ints for respectively the maximum width and maximum height of the scrollable area. Defaults to None, lets Qt decide if None - base_wh (Optional[List[int]]): array of two ints for respectively the initial width and initial height of the scrollable area. Defaults to None, lets Qt decide if None - """ - - scroll = cls(contained_layout, min_wh, max_wh, base_wh) - layout = QVBoxLayout(parent) - # layout.setContentsMargins(0,0,1,1) - layout.setSizeConstraint(QLayout.SetMinAndMaxSize) - layout.addWidget(scroll) - parent.setLayout(layout) - - -def set_spinbox( - box, - min=0, - max=10, - default=0, - step=1, - fixed: Optional[bool] = True, -): - """Args: - class_ : QSpinBox or QDoubleSpinBox - min (Optional[int]): minimum value, defaults to 0 - max (Optional[int]): maximum value, defaults to 10 - default (Optional[int]): default value, defaults to 0 - step (Optional[int]): step value, defaults to 1 - fixed (bool): if True, sets the QSizePolicy of the spinbox to Fixed""" - - box.setMinimum(min) - box.setMaximum(max) - box.setSingleStep(step) - box.setValue(default) - - if fixed: - box.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - -def make_n_spinboxes( - class_, - n: int = 2, - min=0, - max=10, - default=0, - step=1, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, -): - """Creates n increment counters with the specified parameters : - - Args: - class_ : QSpinBox or QDoubleSpinbox - n (int): number of increment counters to create - min (Optional[int]): minimum value, defaults to 0 - max (Optional[int]): maximum value, defaults to 10 - default (Optional[int]): default value, defaults to 0 - step (Optional[int]): step value, defaults to 1 - parent: parent widget, defaults to None - fixed (bool): if True, sets the QSizePolicy of the spinbox to Fixed - """ - if n <= 1: - raise ValueError("Cannot make less than 2 spin boxes") - - boxes = [] - for i in range(n): - box = class_(min, max, default, step, parent, fixed) - boxes.append(box) - return boxes - - -class DoubleIncrementCounter(QDoubleSpinBox): - """Class implementing a number counter with increments (spin box) for floats.""" - - def __init__( - self, - min=0, - max=10, - default=0, - step=1, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - """Args: - min (Optional[int]): minimum value, defaults to 0 - max (Optional[int]): maximum value, defaults to 10 - default (Optional[int]): default value, defaults to 0 - step (Optional[int]): step value, defaults to 1 - parent: parent widget, defaults to None - fixed (bool): if True, sets the QSizePolicy of the spinbox to Fixed""" - - super().__init__(parent) - set_spinbox(self, min, max, default, step, fixed) - - def set_precision(self, decimals): - """Sets the precision of the box to the speicifed number of decimals""" - self.setDecimals(decimals) - - @classmethod - def make_n( - cls, - n: int = 2, - min=0, - max=10, - default=0, - step=1, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - return make_n_spinboxes(cls, n, min, max, default, step, parent, fixed) - - -class IntIncrementCounter(QSpinBox): - """Class implementing a number counter with increments (spin box) for int.""" - - def __init__( - self, - min=0, - max=10, - default=0, - step=1, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - """Args: - min (Optional[int]): minimum value, defaults to 0 - max (Optional[int]): maximum value, defaults to 10 - default (Optional[int]): default value, defaults to 0 - step (Optional[int]): step value, defaults to 1 - parent: parent widget, defaults to None - fixed (bool): if True, sets the QSizePolicy of the spinbox to Fixed""" - - super().__init__(parent) - set_spinbox(self, min, max, default, step, fixed) - - @classmethod - def make_n( - cls, - n: int = 2, - min=0, - max=10, - default=0, - step=1, - parent: Optional[QWidget] = None, - fixed: Optional[bool] = True, - ): - return make_n_spinboxes(cls, n, min, max, default, step, parent, fixed) def add_blank(widget, layout=None): @@ -622,6 +111,98 @@ def make_label(name, parent=None): # TODO update to child class return QLabel(name) +def make_scrollable( + contained_layout, containing_widget, min_wh=None, max_wh=None, base_wh=None +): # TODO convert to child class + """Creates a QScrollArea and sets it up, then adds the contained_widget to it, + and finally adds the scroll area in a layout and sets it to the contaning_widget + + + Args: + contained_layout (QLayout): the widget to be made scrollable + containing_widget (QWidget): the widget to add the resulting scroll area in + min_wh (array(int)): array of two ints for respectively the minimum width and minimum height of the scrollable area. Defaults to None, lets Qt decide if None + max_wh (array(int)): array of two ints for respectively the maximum width and maximum height of the scrollable area. Defaults to None, lets Qt decide if None + base_wh (array(int)): array of two ints for respectively the initial width and initial height of the scrollable area. Defaults to None, lets Qt decide if None + """ + container_widget = QWidget() # required to use QScrollArea.setWidget() + container_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Maximum) + container_widget.setLayout(contained_layout) + container_widget.adjustSize() + # TODO : optimize the number of created objects ? + scroll = QScrollArea() + scroll.setWidget(container_widget) + scroll.setWidgetResizable(True) + scroll.setSizePolicy( + QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding + ) + if base_wh is not None: + scroll.setBaseSize(base_wh[0], base_wh[1]) + if max_wh is not None: + scroll.setMaximumSize(max_wh[0], max_wh[1]) + if min_wh is not None: + scroll.setMinimumSize(min_wh[0], min_wh[1]) + + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + # scroll.adjustSize() + + layout = QVBoxLayout(containing_widget) + # layout.setContentsMargins(0,0,1,1) + layout.setSizeConstraint(QLayout.SetMinAndMaxSize) + layout.addWidget(scroll) + containing_widget.setLayout(layout) + + +def make_n_spinboxes( + n=1, + min=0, + max=10, + default=0, + step=1, + parent=None, + double=False, + fixed=True, +) -> Union[list, QWidget]: # TODO: child class if possible ? + """ + + Args: + n: number of spinboxes, defaults to 1 + min: min value, defaults to 0 + max: max value, defaults to 10 + default: default value, defaults to 0 + step : step value, defaults to 1 + parent: parent widget, defaults to None + double (bool): if True, creates a QDoubleSpinBox rather than a QSpinBox + fixed (bool): if True, sets the QSizePolicy of the spinbox to Fixed + + Returns: + list: A list of n Q(Double)SpinBoxes with specified parameters. If only one box is made, returns the box itself instead + """ + if double: + box_type = QDoubleSpinBox + else: + box_type = QSpinBox + boxes = [] + for i in range(n): + if parent is not None: + widget = box_type(parent) + else: + widget = box_type() + widget.setMinimum(min) + widget.setMaximum(max) + widget.setSingleStep(step) + widget.setValue(default) + + if fixed: + widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + boxes.append(widget) + if len(boxes) == 1: + return boxes[0] + return boxes + + def add_to_group(title, widget, layout, L=7, T=20, R=7, B=11): """Adds a single widget to a layout as a named group with margins specified. @@ -697,7 +278,75 @@ def make_container( return container_widget, container_layout -def make_combobox(): # TODO finish child class conversion +def make_button( # TODO child class + title: str = None, + func: callable = None, + parent: QWidget = None, + fixed: bool = True, +): + """Creates a button with a title and connects it to a function when clicked + + Args: + title (str-like): title of the button. Defaults to None, if None no title is set + func (callable): function to execute when button is clicked. Defaults to None, no binding is made if None + parent (QWidget): parent QWidget to add button to. Defaults to None, no parent is set if None + fixed (bool): if True, will set the size policy of the button to Fixed in h and w. Defaults to True. + + Returns: + QPushButton : created button + """ + if parent is not None: + if title is not None: + btn = QPushButton(title, parent) + else: + btn = QPushButton(parent) + else: + if title is not None: + btn = QPushButton(title, parent) + else: + btn = QPushButton(parent) + + if fixed: + btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + if func is not None: + btn.clicked.connect(func) + + return btn + + +class DropdownMenu(QComboBox): + """Creates a dropdown menu with a title and adds specified entries to it""" + + def __init__( + self, + entries: Optional[list] = None, + parent: Optional[QWidget] = None, + label: Optional[str] = None, + fixed: Optional[bool] = True, + ): + """Args: + entries (array(str)): Entries to add to the dropdown menu. Defaults to None, no entries if None + parent (QWidget): parent QWidget to add dropdown menu to. Defaults to None, no parent is set if None + label (str) : if not None, creates a QLabel with the contents of 'label', and returns the label as well + fixed (bool): if True, will set the size policy of the dropdown menu to Fixed in h and w. Defaults to True. + """ + super().__init__(parent) + self.label = None + if entries is not None: + self.addItems(entries) + if label is not None: + self.label = QLabel(label) + if fixed: + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + +def make_combobox( # TODO child class + entries=None, + parent: QWidget = None, + label: str = None, + fixed: bool = True, +): """Creates a dropdown menu with a title and adds specified entries to it Args: @@ -709,7 +358,13 @@ def make_combobox(): # TODO finish child class conversion Returns: QComboBox : created dropdown menu """ - raise NotImplementedError + if label is not None: + menu = DropdownMenu(entries, parent, label, fixed) + label = menu.label + return menu, label + else: + menu = DropdownMenu(entries, parent, fixed=fixed) + return menu def add_widgets(layout, widgets, alignment=LEFT_AL): @@ -728,6 +383,30 @@ def add_widgets(layout, widgets, alignment=LEFT_AL): layout.addWidget(w, alignment=alignment) +class CheckBox(QCheckBox): + """Shortcut for creating QCheckBox with a title and a function""" + + def __init__( + self, + title: Optional[str] = None, + func: Optional[callable] = None, + parent: Optional[QWidget] = None, + fixed: Optional[bool] = True, + ): + """ + Args: + title (str-like): title of the checkbox. Defaults to None, if None no title is set + func (callable): function to execute when checkbox is toggled. Defaults to None, no binding is made if None + parent (QWidget): parent QWidget to add checkbox to. Defaults to None, no parent is set if None + fixed (bool): if True, will set the size policy of the checkbox to Fixed in h and w. Defaults to True. + """ + super().__init__(title, parent) + if fixed: + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + if func is not None: + self.toggled.connect(func) + + def make_checkbox( # TODO update calls to class title: str = None, func: callable = None, @@ -805,6 +484,115 @@ def combine_blocks( return temp_widget +def toggle_visibility(checkbox, widget): + """Toggles the visibility of a widget based on the status of a checkbox. + + Args: + checkbox: The QCheckbox that determines whether to show or not + widget: The widget to hide or show + """ + widget.setVisible(checkbox.isChecked()) + + +class AnisotropyWidgets(QWidget): + def __init__(self, parent, default_x=1, default_y=1, default_z=1): + super().__init__(parent) + + self._layout = QVBoxLayout() + self._layout.setSpacing(0) + self._layout.setContentsMargins(0, 0, 0, 0) + + self.container, self._boxes_layout = make_container(T=7, parent=parent) + self.checkbox = make_checkbox( + "Anisotropic data", self.toggle_display_aniso, parent + ) + + self.box_widgets = make_n_spinboxes( + n=3, min=1.0, max=1000, default=1, step=0.5, double=True + ) + self.box_widgets[0].setValue(default_x) + self.box_widgets[1].setValue(default_y) + self.box_widgets[2].setValue(default_z) + + for w in self.box_widgets: + w.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.box_widgets_lbl = [ + make_label("Resolution in " + axis + " (microns) :", parent=parent) + for axis in "xyz" + ] + + ################## + # tooltips + self.checkbox.setToolTip( + "If you have anisotropic data, you can scale data using your resolution in microns" + ) + [w.setToolTip("Resolution in microns") for w in self.box_widgets] + ################## + + self.build() + + def toggle_display_aniso(self): + """Shows the choices for correcting anisotropy when viewing results depending on whether :py:attr:`self.checkbox` is checked""" + toggle_visibility(self.checkbox, self.container) + + def build(self): + [ + self._boxes_layout.addWidget(widget, alignment=LEFT_AL) + for widgets in zip(self.box_widgets_lbl, self.box_widgets) + for widget in widgets + ] + # anisotropy + self.container.setLayout(self._boxes_layout) + self.container.setVisible(False) + + add_widgets(self._layout, [self.checkbox, self.container]) + self.setLayout(self._layout) + + def get_anisotropy_resolution_xyz(self, as_factors=True): + """ + Args : + as_factors: if True, returns zoom factors, otherwise returns the input resolution + + Returns : the resolution in microns for each of the three dimensions. ZYX order suitable for napari scale""" + + resolution = [w.value() for w in self.box_widgets] + if as_factors: + return self.anisotropy_zoom_factor(resolution) + + return resolution + + def get_anisotropy_resolution_zyx(self, as_factors=True): + """ + Args : + as_factors: if True, returns zoom factors, otherwise returns the input resolution + + Returns : the resolution in microns for each of the three dimensions. XYZ order suitable for MONAI""" + resolution = [w.value() for w in self.box_widgets] + if as_factors: + resolution = self.anisotropy_zoom_factor(resolution) + + return [resolution[2], resolution[1], resolution[0]] + + def anisotropy_zoom_factor(self, aniso_res): + """Computes a zoom factor to correct anisotropy, based on anisotropy resolutions + + Args: + resolutions: array for resolution (float) in microns for each axis + + Returns: an array with the corresponding zoom factors for each axis (all values divided by min) + + """ + + base = min(aniso_res) + zoom_factors = [base / res for res in aniso_res] + return zoom_factors + + def is_enabled(self): + """Returns : whether anisotropy correction has been enabled or not""" + return self.checkbox.isChecked() + + def open_url(url): """Opens the url given as a string in OS default browser using :py:func:`QDesktopServices.openUrl`. diff --git a/napari_cellseg3d/launch_review.py b/napari_cellseg3d/launch_review.py index 3069e141..ccd665ba 100644 --- a/napari_cellseg3d/launch_review.py +++ b/napari_cellseg3d/launch_review.py @@ -9,6 +9,7 @@ FigureCanvasQTAgg as FigureCanvas, ) from matplotlib.figure import Figure +from monai.transforms import Zoom from qtpy.QtWidgets import QSizePolicy from scipy import ndimage from tifffile import imwrite @@ -72,21 +73,22 @@ def launch_review( images_original = original base_label = base - viewer = napari.Viewer() + view1 = napari.Viewer() + viewer = view1 #TODO fix duplicate name - viewer.scale_bar.visible = True + view1.scale_bar.visible = True - viewer.add_image( + view1.add_image( images_original, name="volume", colormap="inferno", contrast_limits=[200, 1000], scale=zoom_factor, ) # anything bigger than 255 will get mapped to 255... they did it like this because it must have rgb images - viewer.add_labels(base_label, name="labels", seed=0.6, scale=zoom_factor) + view1.add_labels(base_label, name="labels", seed=0.6, scale=zoom_factor) if raw is not None: # raw labels is from the prediction - viewer.add_image( + view1.add_image( ndimage.gaussian_filter(raw, sigma=3), colormap="magenta", name="low_confident", @@ -116,7 +118,7 @@ def launch_review( # def show_so_layer(args): # labeled_c, labeled_sorted, nums = args - # so_layer = viewer.add_image(labeled_c, colormap='cyan', name='small_object', blending='additive') + # so_layer = view1.add_image(labeled_c, colormap='cyan', name='small_object', blending='additive') # # object_slider = QSlider(Qt.Horizontal) # object_slider.setMinimum(0) @@ -131,7 +133,7 @@ def launch_review( # # slider_widget = utils.combine_blocks(lbl, object_slider) # - # viewer.window.add_dock_widget(slider_widget, name='object_size_slider', area='left') + # view1.window.add_dock_widget(slider_widget, name='object_size_slider', area='left') # # def calc_object_callback(t_layer, value, labeled_array, nums): # t_layer.data = label_ct(labeled_array, nums, value) @@ -184,14 +186,14 @@ def quicksave(): # gui = file_widget.show(run=True) # dirpicker.show(run=True) - viewer.window.add_dock_widget(file_widget, name=" ", area="bottom") + view1.window.add_dock_widget(file_widget, name=" ", area="bottom") # @magicgui(call_button="Save") # gui2 = saver.show(run=True) # saver.show(run=True) - # viewer.window.add_dock_widget(gui2, name=" ", area="bottom") + # view1.window.add_dock_widget(gui2, name=" ", area="bottom") - # viewer.window._qt_window.tabifyDockWidget(gui, gui2) #not with FunctionGui ? + # view1.window._qt_window.tabifyDockWidget(gui, gui2) #not with FunctionGui ? # draw canvas @@ -222,7 +224,7 @@ def quicksave(): canvas.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) - viewer.window.add_dock_widget(canvas, name=" ", area="right") + view1.window.add_dock_widget(canvas, name=" ", area="right") @viewer.mouse_drag_callbacks.append def update_canvas_canvas(viewer, event): @@ -230,7 +232,7 @@ def update_canvas_canvas(viewer, event): if "shift" in event.modifiers: try: cursor_position = np.round(viewer.cursor.position).astype(int) - print(f"plot @ {cursor_position}") + print(cursor_position) cropped_volume = crop_volume_around_point( [ @@ -239,14 +241,8 @@ def update_canvas_canvas(viewer, event): cursor_position[2], ], viewer.layers["volume"], - zoom_factor, ) - ########## - ########## - # DEBUG - # viewer.add_image(cropped_volume, name="DEBUG_crop_plot") - xy_axes.imshow( cropped_volume[50], cmap="inferno", vmin=200, vmax=2000 ) @@ -267,9 +263,9 @@ def update_canvas_canvas(viewer, event): print(e) # Qt widget defined in docker.py - dmg = Datamanager(parent=viewer) + dmg = Datamanager(parent=view1) dmg.prepare(r_path, filetype, model_type, checkbox, as_folder) - viewer.window.add_dock_widget(dmg, name=" ", area="left") + view1.window.add_dock_widget(dmg, name=" ", area="left") def update_button(axis_event): @@ -277,13 +273,19 @@ def update_button(axis_event): print(f"slice num is {slice_num}") dmg.update(slice_num) - viewer.dims.events.current_step.connect(update_button) + view1.dims.events.current_step.connect(update_button) + + def crop_volume_around_point(points, layer): - def crop_volume_around_point(points, layer, zoom_factor): if zoom_factor != [1, 1, 1]: - data = np.array(layer.data, dtype=np.int16) - volume = utils.resize(data, zoom_factor) - # image = ndimage.zoom(layer.data, zoom_factor, mode="nearest") # cleaner but much slower... + vol = np.array(layer.data, dtype=np.int16) + volume = Zoom( + zoom_factor, + keep_size=False, + padding_mode="empty", + )(np.expand_dims(vol, axis=0)) + volume = volume[0] + # image = ndimage.zoom(layer.data, zoom_factor, mode="nearest") # cleaner but much slower... else: volume = layer.data @@ -310,7 +312,7 @@ def crop_volume_around_point(points, layer, zoom_factor): if as_folder: crop_temp = volume[crop_slice].persist().compute() else: - crop_temp = volume[crop_slice] + crop_temp = layer.data[crop_slice] cropped_volume = np.zeros((100, 100, 100), np.int16) cropped_volume[ @@ -320,4 +322,4 @@ def crop_volume_around_point(points, layer, zoom_factor): ] = crop_temp return cropped_volume - return viewer, [file_widget, canvas, dmg] + return view1, [file_widget, canvas, dmg] diff --git a/napari_cellseg3d/model_framework.py b/napari_cellseg3d/model_framework.py index 7e1b4faf..7cb0542f 100644 --- a/napari_cellseg3d/model_framework.py +++ b/napari_cellseg3d/model_framework.py @@ -5,6 +5,7 @@ import torch # Qt +from qtpy.QtWidgets import QLineEdit from qtpy.QtWidgets import QProgressBar from qtpy.QtWidgets import QSizePolicy @@ -76,22 +77,21 @@ def __init__(self, viewer: "napari.viewer.Viewer"): # interface # TODO : implement custom model - self.model_filewidget = ui.FilePathWidget( - "Model path", self.load_model_path, self + self.btn_model_path = ui.make_button( + "Open", self.load_model_path, self ) - self.btn_model_path = self.model_filewidget.get_button() - self.lbl_model_path = self.model_filewidget.get_text_field() + self.lbl_model_path = QLineEdit("Model directory", self) + self.lbl_model_path.setReadOnly(True) - self.model_choice = ui.DropdownMenu( + self.model_choice, self.lbl_model_choice = ui.make_combobox( sorted(self.models_dict.keys()), label="Model name" ) - self.lbl_model_choice = self.model_choice.label - self.weights_filewidget = ui.FilePathWidget( - "Weights path", self.load_weights_path, self + self.btn_weights_path = ui.make_button( + "Open", self.load_weights_path, self ) - self.btn_weights_path = self.weights_filewidget.get_button() - self.lbl_weights_path = self.weights_filewidget.get_text_field() + self.lbl_weights_path = QLineEdit("Weights directory", self) + self.lbl_weights_path.setReadOnly(True) self.weights_path_container = ui.combine_blocks( self.btn_weights_path, self.lbl_weights_path, b=0 @@ -121,7 +121,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.log.setVisible(False) """Read-only display for process-related info. Use only for info destined to user.""" - self.btn_save_log = ui.Button( + self.btn_save_log = ui.make_button( "Save log in results folder", func=self.save_log, parent=self.container_report, @@ -187,7 +187,7 @@ def display_status_report(self): # ) # self.progress = QProgressBar(self.container_report) # self.log = QTextEdit(self.container_report) - # self.btn_save_log = ui.Button( + # self.btn_save_log = ui.make_button( # "Save log in results folder", parent=self.container_report # ) # self.btn_save_log.clicked.connect(self.save_log) diff --git a/napari_cellseg3d/plugin_base.py b/napari_cellseg3d/plugin_base.py index 8c5bd8e8..8f37b245 100644 --- a/napari_cellseg3d/plugin_base.py +++ b/napari_cellseg3d/plugin_base.py @@ -2,6 +2,7 @@ import os import napari +from qtpy.QtWidgets import QLineEdit from qtpy.QtWidgets import QSizePolicy from qtpy.QtWidgets import QTabWidget @@ -51,21 +52,17 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): self._default_path = [self.image_path, self.label_path] - self.image_filewidget = ui.FilePathWidget( - "Image path", self.show_dialog_images, self - ) - self.btn_image = self.image_filewidget.get_button() + self.btn_image = ui.make_button("Open", self.show_dialog_images, self) """Button to load image folder""" - self.lbl_image = self.image_filewidget.get_text_field() + self.lbl_image = QLineEdit("Image path", self) + self.lbl_image.setReadOnly(True) - self.label_filewidget = ui.FilePathWidget( - "Label path", self.show_dialog_labels, self - ) - self.lbl_label = self.label_filewidget.get_text_field() - self.btn_label = self.label_filewidget.get_button() + self.lbl_label = QLineEdit("Label path", self) """Button to load label folder""" + self.lbl_label.setReadOnly(True) + self.btn_label = ui.make_button("Open", self.show_dialog_labels, self) - self.filetype_choice = ui.DropdownMenu([".png", ".tif"]) + self.filetype_choice = ui.make_combobox([".png", ".tif"]) self.file_handling_box = ui.make_checkbox( "Load as folder ?", self.show_filetype_choice @@ -76,7 +73,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): QSizePolicy.Fixed, QSizePolicy.Fixed ) - self.btn_close = ui.Button("Close", self.remove_from_viewer, self) + self.btn_close = ui.make_button("Close", self.remove_from_viewer, self) # self.lbl_ft = QLabel("Filetype :", self) # self.lbl_ft2 = QLabel("(Folders of .png or single .tif files)", self) @@ -195,46 +192,45 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): ####################################################### # interface - self.image_filewidget = ui.FilePathWidget( - "Images directory", self.load_image_dataset, self + self.btn_image_files = ui.make_button( + "Open", self.load_image_dataset, self ) - self.btn_image_files = self.image_filewidget.get_button() - self.lbl_image_files = self.image_filewidget.get_text_field() + self.lbl_image_files = QLineEdit("Images directory", self) + self.lbl_image_files.setReadOnly(True) - self.label_filewidget = ui.FilePathWidget( - "Labels directory", self.load_label_dataset, self + self.btn_label_files = ui.make_button( + "Open", self.load_label_dataset, self ) - self.btn_label_files = self.label_filewidget.get_button() - self.lbl_label_files = self.label_filewidget.get_text_field() + self.lbl_label_files = QLineEdit("Labels directory", self) + self.lbl_label_files.setReadOnly(True) - self.filetype_choice = ui.DropdownMenu( + self.filetype_choice, self.lbl_filetype = ui.make_combobox( [".tif", ".tiff"], label="File format" ) - self.lbl_filetype = self.filetype_choice.label """Allows to choose which file will be loaded from folder""" - self.results_filewidget = ui.FilePathWidget( - "Results directory", self.load_results_path, self + self.btn_result_path = ui.make_button( + "Open", self.load_results_path, self ) - self.btn_result_path = self.results_filewidget.get_button() - self.lbl_result_path = self.results_filewidget.get_text_field() + self.lbl_result_path = QLineEdit("Results directory", self) + self.lbl_result_path.setReadOnly(True) ####################################################### def make_close_button(self): - btn = ui.Button("Close", self.remove_from_viewer) + btn = ui.make_button("Close", self.remove_from_viewer) btn.setToolTip( "Close the window and all docked widgets. Make sure to save your work !" ) return btn def make_prev_button(self): - btn = ui.Button( + btn = ui.make_button( "Previous", lambda: self.setCurrentIndex(self.currentIndex() - 1) ) return btn def make_next_button(self): - btn = ui.Button( + btn = ui.make_button( "Next", lambda: self.setCurrentIndex(self.currentIndex() + 1) ) return btn diff --git a/napari_cellseg3d/plugin_convert.py b/napari_cellseg3d/plugin_convert.py index e61f3f4a..7cb6b58d 100644 --- a/napari_cellseg3d/plugin_convert.py +++ b/napari_cellseg3d/plugin_convert.py @@ -1,8 +1,7 @@ import os import napari -import numpy as np -from tifffile import imwrite, imread +from tifffile import imwrite import napari_cellseg3d.interface as ui from napari_cellseg3d import utils @@ -34,39 +33,27 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): ######################## # interface - # label conversion - self.btn_convert_folder_semantic = ui.Button( + self.btn_convert_folder_semantic = ui.make_button( "Convert to semantic labels", func=self.folder_to_semantic ) - self.btn_convert_layer_semantic = ui.Button( + self.btn_convert_layer_semantic = ui.make_button( "Convert to semantic labels", func=self.layer_to_semantic ) - self.btn_convert_folder_instance = ui.Button( + self.btn_convert_folder_instance = ui.make_button( "Convert to instance labels", func=self.folder_to_instance ) - self.btn_convert_layer_instance = ui.Button( + self.btn_convert_layer_instance = ui.make_button( "Convert to instance labels", func=self.layer_to_instance ) - # remove small - self.btn_remove_small_folder = ui.Button( - "Remove small in folder", func=self.folder_remove_small - ) - self.btn_remove_small_layer = ui.Button( - "Remove small in layer", func=self.layer_remove_small - ) - self.small_object_thresh_choice = ui.IntIncrementCounter( - min=1, max=1000, default=15 - ) - # convert anisotropy - self.anisotropy_converter = ui.AnisotropyWidgets( - parent=self, always_visible=True + self.btn_remove_small_folder = ui.make_button( + "Remove in folder", func=self.folder_remove_small ) - self.btn_aniso_folder = ui.Button( - "Correct anisotropy in folder", self.folder_anisotropy, self + self.btn_remove_small_layer = ui.make_button( + "Remove in layer", func=self.layer_remove_small ) - self.btn_aniso_layer = ui.Button( - "Correct anisotropy in layer", self.layer_anisotropy, self + self.small_object_thresh_choice = ui.make_n_spinboxes( + min=1, max=1000, default=15 ) self.lbl_error = ui.make_label("", self) @@ -74,10 +61,6 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): self.btn_image_files.setVisible(False) self.lbl_image_files.setVisible(False) - - # self.results_filewidget.set_required(True) - self.label_filewidget.set_required(False) - # TODO improve not ready check for labels since optional until using folder conversion ############################### # tooltips self.btn_convert_folder_semantic.setToolTip( @@ -102,14 +85,6 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): self.small_object_thresh_choice.setToolTip( "All objects in the image smaller in volume than this number of pixels will be removed" ) - self.btn_aniso_layer.setToolTip( - "Resize the selected layer to be isotropic, based on the chosen resolutions above." - "\nDOES NOT WORK WITH INSTANCE LABELS, CONVERT TO SEMANTIC FIRST" - ) - self.btn_aniso_folder.setToolTip( - "Resize the images in the selected folder to be isotropic, based on the chosen resolutions above." - "\nDOES NOT WORK WITH INSTANCE LABELS, CONVERT TO SEMANTIC FIRST" - ) ############################### self.build() @@ -151,40 +126,31 @@ def build(self): ) ############################### ui.add_blank(layout=layout, widget=self) - ############################### - aniso_group_w, aniso_group_l = ui.make_group( - "Correct anisotropy", l, t, r, b, parent=None - ) - - ui.add_widgets( - aniso_group_l, - [ - self.anisotropy_converter, - ], - ui.LEFT_AL, + ############################################################# + folder_group_w, folder_group_l = ui.make_group( + "Convert folder", l, t, r, b, parent=None ) - aniso_group_w.setLayout(aniso_group_l) - layout.addWidget(aniso_group_w) - - ############################### - ui.add_blank(layout=layout, widget=self) - ############################################################# - small_group_w, small_group_l = ui.make_group( - "Remove small objects", l, t, r, b, parent=None + folder_group_l.addWidget( + ui.combine_blocks( + right_or_below=self.btn_label_files, + left_or_above=self.lbl_label_files, + min_spacing=70, + ) ) ui.add_widgets( - small_group_l, + folder_group_l, [ - self.small_object_thresh_choice, + self.btn_convert_folder_instance, + self.btn_convert_folder_semantic, ], ui.HCENTER_AL, ) - small_group_w.setLayout(small_group_l) - layout.addWidget(small_group_w) - ######################################### + folder_group_w.setLayout(folder_group_l) + layout.addWidget(folder_group_w) + ############################### ui.add_blank(layout=layout, widget=self) ############################################################# layer_group_w, layer_group_l = ui.make_group( @@ -193,12 +159,7 @@ def build(self): ui.add_widgets( layer_group_l, - [ - self.btn_convert_layer_instance, - self.btn_convert_layer_semantic, - self.btn_remove_small_layer, - self.btn_aniso_layer, - ], + [self.btn_convert_layer_instance, self.btn_convert_layer_semantic], ui.HCENTER_AL, ) @@ -206,34 +167,24 @@ def build(self): layout.addWidget(layer_group_w) ############################### ui.add_blank(layout=layout, widget=self) - ############################### - folder_group_w, folder_group_l = ui.make_group( - "Convert folder", l, t, r, b, parent=None - ) - - folder_group_l.addWidget( - ui.combine_blocks( - right_or_below=self.btn_label_files, - left_or_above=self.lbl_label_files, - min_spacing=70, - ) + ############################################################# + small_group_w, small_group_l = ui.make_group( + "Remove small objects", l, t, r, b, parent=None ) ui.add_widgets( - folder_group_l, + small_group_l, [ - self.btn_convert_folder_instance, - self.btn_convert_folder_semantic, + self.small_object_thresh_choice, + self.btn_remove_small_layer, self.btn_remove_small_folder, - self.btn_aniso_folder, ], ui.HCENTER_AL, ) - folder_group_w.setLayout(folder_group_l) - layout.addWidget(folder_group_w) - ############################### - ui.add_blank(layout=layout, widget=self) + small_group_w.setLayout(small_group_l) + layout.addWidget(small_group_w) + ############################################################# ui.add_widgets( layout, @@ -245,23 +196,28 @@ def build(self): ], ) - ui.ScrollArea.make_scrollable( - layout, self, min_wh=[230, 400], base_wh=[230, 450] - ) + ui.make_scrollable(layout, self, min_wh=[230, 400], base_wh=[230, 450]) def folder_to_semantic(self): """Converts folder of labels to semantic labels""" if not self.check_ready_folder(): return - folder_name = f"converted_to_semantic_labels_{utils.get_date_time()}" + results_folder = ( + self.results_path + + f"/converted_to_semantic_labels_{utils.get_date_time()}" + ) + + os.makedirs(results_folder, exist_ok=False) - images = [ - to_semantic(file, is_file_path=True) - for file in self.labels_filepaths - ] + for file in self.labels_filepaths: - self.save_folder(folder_name, images) + image = to_semantic(file, is_file_path=True) + + imwrite( + results_folder + "/" + os.path.basename(file), + image, + ) def layer_to_semantic(self): """Converts selected layer to semantic labels""" @@ -272,11 +228,13 @@ def layer_to_semantic(self): name = self._viewer.layers.selection.active.name semantic_labels = to_semantic(im) - self.save_layer( - f"{name}_semantic_{utils.get_time_filepath()}" - + self.filetype_choice.currentText(), - semantic_labels, - ) + if self.results_path != "": + imwrite( + self.results_path + + f"/{name}_semantic_{utils.get_time_filepath()}" + + self.filetype_choice.currentText(), + semantic_labels, + ) self._viewer.add_labels(semantic_labels, name=f"converted_semantic") @@ -285,15 +243,22 @@ def folder_to_instance(self): if not self.check_ready_folder(): return - images = [ - to_instance(file, is_file_path=True) - for file in self.labels_filepaths - ] - - self.save_folder( - f"converted_to_instance_labels_{utils.get_date_time()}", images + results_folder = ( + self.results_path + + f"/converted_to_instance_labels_{utils.get_date_time()}" ) + os.makedirs(results_folder, exist_ok=False) + + for file in self.labels_filepaths: + + image = to_instance(file, is_file_path=True) + + imwrite( + results_folder + "/" + os.path.basename(file), + image, + ) + def layer_to_instance(self): """Converts the selected layer to instance labels""" if not self.check_ready_layer(): @@ -303,11 +268,13 @@ def layer_to_instance(self): name = self._viewer.layers.selection.active.name instance_labels = to_instance(im) - self.save_layer( - f"{name}_instance_{utils.get_time_filepath()}" - + self.filetype_choice.currentText(), - instance_labels, - ) + if self.results_path != "": + imwrite( + self.results_path + + f"/{name}_instance_{utils.get_time_filepath()}" + + self.filetype_choice.currentText(), + instance_labels, + ) self._viewer.add_labels(instance_labels, name=f"converted_instance") @@ -323,65 +290,39 @@ def layer_remove_small(self): im, self.small_object_thresh_choice.value() ) - self.save_layer( - f"{name}_cleared_{utils.get_time_filepath()}" - + self.filetype_choice.currentText(), - cleared_labels, - ) + if self.results_path != "": + imwrite( + self.results_path + + f"/{name}_cleared_{utils.get_time_filepath()}" + + self.filetype_choice.currentText(), + cleared_labels, + ) - self._viewer.add_image(cleared_labels, name=f"small_cleared") + self._viewer.add_labels(cleared_labels, name=f"small_cleared") def folder_remove_small(self): """Removes small objects in folder of labels""" if not self.check_ready_folder(): return + results_folder = ( + self.results_path + f"/small_cleared_{utils.get_date_time()}" + ) + + os.makedirs(results_folder, exist_ok=False) - images = [ - clear_small_objects( + for file in self.labels_filepaths: + res = clear_small_objects( file, self.small_object_thresh_choice.value(), is_file_path=True, ) - for file in self.labels_filepaths - ] - - self.save_folder(f"small_cleared_{utils.get_date_time()}", images) - def layer_anisotropy(self): - """Corrects anisotropy in the currently selected image""" - if not self.check_ready_layer(): - return - - name = self._viewer.layers.selection.active.name - zoom_factor = self.anisotropy_converter.get_anisotropy_resolution_zyx() - - vol = np.array( - self._viewer.layers.selection.active.data, dtype=np.int16 - ) - isotropic_image = utils.resize(vol, zoom_factor) - - self.save_layer( - f"{name}_isotropic_{utils.get_time_filepath()}" - + self.filetype_choice.currentText(), - isotropic_image, - ) - - self._viewer.add_image(isotropic_image, name=f"isotropic") - - def folder_anisotropy(self): - """Removes anisotropy in folder of images or labels""" - if not self.check_ready_folder(): - return - - zoom_factor = self.anisotropy_converter.get_anisotropy_resolution_zyx() - images = [ - utils.resize(imread(file), zoom_factor) - for file in self.labels_filepaths - ] - - self.save_folder(f"isotropic_{utils.get_date_time()}", images) + imwrite( + results_folder + "/" + os.path.basename(file), + res, + ) - def check_ready_folder(self): # TODO add color change + def check_ready_folder(self): """Check if results and source folders are correctly set""" if self.results_path == "": err = "ERROR : please set results folder" @@ -399,7 +340,7 @@ def check_ready_folder(self): # TODO add color change self.lbl_error.setVisible(True) return False - def check_ready_layer(self): # TODO add color change + def check_ready_layer(self): """Check if results and layer are selected""" if self.results_path == "": err = "ERROR : please set results folder" @@ -415,33 +356,3 @@ def check_ready_layer(self): # TODO add color change return False self.lbl_error.setVisible(False) return True - - def save_layer(self, file_name, image): - - path = os.path.join(self.results_path, file_name) - print(self.results_path) - print(path) - - if self.results_path != "": - imwrite( - path, - image, - ) - - def save_folder(self, folder_name, images): - - results_folder = os.path.join( - self.results_path, - folder_name, - ) - - os.makedirs(results_folder, exist_ok=False) - - for file, image in zip(self.labels_filepaths, images): - - path = os.path.join(results_folder, os.path.basename(file)) - - imwrite( - path, - image, - ) diff --git a/napari_cellseg3d/plugin_crop.py b/napari_cellseg3d/plugin_crop.py index d7ad0172..7becdad0 100644 --- a/napari_cellseg3d/plugin_crop.py +++ b/napari_cellseg3d/plugin_crop.py @@ -40,7 +40,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): super().__init__(viewer, parent) - self.btn_start = ui.Button("Start", self.start, self) + self.btn_start = ui.make_button("Start", self.start, self) self.crop_label_choice = ui.make_checkbox( "Crop labels simultaneously", self.toggle_label_path @@ -48,9 +48,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): self.lbl_label.setVisible(False) self.btn_label.setVisible(False) - self.box_widgets = ui.IntIncrementCounter.make_n( - 3, 1, 1000, DEFAULT_CROP_SIZE - ) + self.box_widgets = ui.make_n_spinboxes(3, 1, 1000, DEFAULT_CROP_SIZE) self.box_lbl = [ ui.make_label("Size in " + axis + " of cropped volume :", self) for axis in "xyz" @@ -139,7 +137,7 @@ def build(self): ], ) - ui.ScrollArea.make_scrollable(layout, self, min_wh=[180, 100]) + ui.make_scrollable(layout, self, min_wh=[180, 100]) def quicksave(self): """Quicksaves the cropped volume in the folder from which they originate, with their original file extension. diff --git a/napari_cellseg3d/plugin_dock.py b/napari_cellseg3d/plugin_dock.py index 4c69a2db..439eca72 100644 --- a/napari_cellseg3d/plugin_dock.py +++ b/napari_cellseg3d/plugin_dock.py @@ -44,7 +44,7 @@ def __init__(self, parent: "napari.viewer.Viewer"): """napari.viewer.Viewer: viewer in which the widget is displayed""" # add some buttons - self.button = ui.Button( + self.button = ui.make_button( "1", self.button_func, parent=self, fixed=False ) self.time_label = ui.make_label("", self) @@ -189,12 +189,6 @@ def create(self, label_dir, model_type, filename=None): return df, csv_path - def update_button(self): - if len(self.df) > 1: - self.button.setText( - self.df.at[self.df.index[self.slice_num], "train"] - ) # puts button values at value of 1st csv item - def update(self, slice_num): """Updates the Datamanager with the index of the current slice, and updates the text with the status contained in the csv (e.g. checked/not checked). @@ -207,12 +201,10 @@ def update(self, slice_num): print(f"New slice review started at {utils.get_time()}") # print(self.df) - - try: - self.update_button() - except IndexError: - self.slice_num -= 1 - self.update_button() + if len(self.df) > 1: + self.button.setText( + self.df.at[self.df.index[self.slice_num], "train"] + ) # puts button values at value of 1st csv item self.time = datetime.now() diff --git a/napari_cellseg3d/plugin_helper.py b/napari_cellseg3d/plugin_helper.py index 8dea6688..85def22d 100644 --- a/napari_cellseg3d/plugin_helper.py +++ b/napari_cellseg3d/plugin_helper.py @@ -30,7 +30,9 @@ def __init__(self, viewer: "napari.viewer.Viewer"): url = str(path) + "/res/logo_alpha.png" image = QPixmap(url) - self.logo_label = ui.Button(func=lambda: ui.open_url(self.repo_url)) + self.logo_label = ui.make_button( + func=lambda: ui.open_url(self.repo_url) + ) self.logo_label.setIcon(QIcon(image)) self.logo_label.setMinimumSize(200, 200) self.logo_label.setIconSize(QSize(200, 200)) @@ -43,17 +45,21 @@ def __init__(self, viewer: "napari.viewer.Viewer"): f"You are using napari-cellseg3d v.{'0.0.1rc2'}\n\n" f"Plugin for cell segmentation developed\n" f"by the Mathis Lab of Adaptive Motor Control\n\n" - f"Code by :\nCyril Achard\nMaxime Vidal\nJessy Lauer\nMackenzie Mathis\n" + f"Code by Cyril Achard and Maxime Vidal and Mackenzie Mathis\n" f"\nReleased under the MIT license", self, ) - self.btn1 = ui.Button("Help...", lambda: ui.open_url(self.help_url)) + self.btn1 = ui.make_button( + "Help...", lambda: ui.open_url(self.help_url) + ) self.btn1.setToolTip("Go to documentation") - self.btn2 = ui.Button("About...", lambda: ui.open_url(self.about_url)) + self.btn2 = ui.make_button( + "About...", lambda: ui.open_url(self.about_url) + ) - self.btnc = ui.Button("Close", self.remove_from_viewer) + self.btnc = ui.make_button("Close", self.remove_from_viewer) self.build() diff --git a/napari_cellseg3d/plugin_metrics.py b/napari_cellseg3d/plugin_metrics.py index 86a3fb98..1a12a0db 100644 --- a/napari_cellseg3d/plugin_metrics.py +++ b/napari_cellseg3d/plugin_metrics.py @@ -40,20 +40,17 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent): ###################################### # interface - - # set new descriptions for Filewidgets - self.image_filewidget.set_description("Ground truth") - self.label_filewidget.set_description("Prediction") - - self.btn_compute_dice = ui.Button("Compute Dice", self.compute_dice) + self.btn_compute_dice = ui.make_button( + "Compute Dice", self.compute_dice + ) self.rotate_choice = ui.make_checkbox("Find best orientation") - self.btn_reset_plot = ui.Button("Clear plots", self.remove_plots) + self.btn_reset_plot = ui.make_button("Clear plots", self.remove_plots) self.lbl_threshold_box = ui.make_label("Score threshold", self) - self.threshold_box = ui.DoubleIncrementCounter( - min=0.1, max=1, default=DEFAULT_THRESHOLD, step=0.1 + self.threshold_box = ui.make_n_spinboxes( + min=0.1, max=1, default=DEFAULT_THRESHOLD, step=0.1, double=True ) self.btn_result_path.setVisible(False) @@ -95,6 +92,10 @@ def build(self): ], ) + self.lbl_image_files.setText("Ground truth") + + self.lbl_label_files.setText("Prediction") + metrics_group_w.setLayout(metrics_group_l) ############################ ui.add_blank(self, self.layout) @@ -124,7 +125,7 @@ def build(self): self.btn_reset_plot.setVisible(False) - ui.ScrollArea.make_scrollable(self.layout, self) + ui.make_scrollable(self.layout, self) def plot_dice(self, dice_coeffs, threshold=DEFAULT_THRESHOLD): """Plots the dice loss for each pair of labels on viewer""" diff --git a/napari_cellseg3d/plugin_model_inference.py b/napari_cellseg3d/plugin_model_inference.py index 33f7ca39..7b9842b6 100644 --- a/napari_cellseg3d/plugin_model_inference.py +++ b/napari_cellseg3d/plugin_model_inference.py @@ -89,7 +89,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): "View results in napari", self.toggle_display_number ) - self.display_number_choice = ui.IntIncrementCounter(min=1, default=5) + self.display_number_choice = ui.make_n_spinboxes(min=1, default=5) self.lbl_display_number = ui.make_label("How many ? (max. 10)", self) self.show_original_checkbox = ui.make_checkbox("Show originals") @@ -97,7 +97,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): ###################### ###################### # TODO : better way to handle SegResNet size reqs ? - self.segres_size = ui.IntIncrementCounter(min=1, max=1024, default=128) + self.segres_size = ui.make_n_spinboxes(min=1, max=1024, default=128) self.model_choice.currentIndexChanged.connect( self.toggle_display_segres_size ) @@ -120,8 +120,8 @@ def __init__(self, viewer: "napari.viewer.Viewer"): "Perform thresholding", self.toggle_display_thresh ) - self.thresholding_count = ui.DoubleIncrementCounter( - max=1, default=0.7, step=0.05 + self.thresholding_count = ui.make_n_spinboxes( + max=1, default=0.7, step=0.05, double=True ) self.thresholding_container, self.thresh_layout = ui.make_container( @@ -130,13 +130,11 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.window_infer_box = ui.make_checkbox("Use window inference") self.window_infer_box.clicked.connect(self.toggle_display_window_size) - sizes_window = ["8", "16", "32", "64", "128", "256", "512"] - self.window_size_choice = ui.DropdownMenu( - sizes_window, label="Window size" - ) - self.lbl_window_size_choice = self.window_size_choice.label - + ( + self.window_size_choice, + self.lbl_window_size_choice, + ) = ui.make_combobox(sizes_window, label="Window size") self.keep_data_on_cpu_box = ui.make_checkbox("Keep data on CPU") self.window_infer_params = ui.combine_blocks( @@ -152,12 +150,12 @@ def __init__(self, viewer: "napari.viewer.Viewer"): "Run instance segmentation", func=self.toggle_display_instance ) - self.instance_method_choice = ui.DropdownMenu( + self.instance_method_choice = ui.make_combobox( ["Connected components", "Watershed"] ) - self.instance_prob_thresh = ui.DoubleIncrementCounter( - max=0.99, default=0.7, step=0.05 + self.instance_prob_thresh = ui.make_n_spinboxes( + n=1, max=0.99, default=0.7, step=0.05, double=True ) self.instance_prob_thresh_lbl = ui.make_label( "Probability threshold :", self @@ -168,8 +166,8 @@ def __init__(self, viewer: "napari.viewer.Viewer"): horizontal=False, ) - self.instance_small_object_thresh = ui.IntIncrementCounter( - max=100, default=10, step=5 + self.instance_small_object_thresh = ui.make_n_spinboxes( + n=1, max=100, default=10, step=5 ) self.instance_small_object_thresh_lbl = ui.make_label( "Small object removal threshold :", self @@ -191,12 +189,14 @@ def __init__(self, viewer: "napari.viewer.Viewer"): ################## ################## - self.btn_start = ui.Button("Start inference", self.start) + self.btn_start = ui.make_button("Start inference", self.start) self.btn_close = self.make_close_button() # hide unused widgets from parent class - self.label_filewidget.setVisible(False) - self.model_filewidget.setVisible(False) + self.btn_label_files.setVisible(False) + self.lbl_label_files.setVisible(False) + self.btn_model_path.setVisible(False) + self.lbl_model_path.setVisible(False) ################## ################## @@ -475,8 +475,8 @@ def build(self): ############ ###### # end of tabs, combine into scrollable - ui.ScrollArea.make_scrollable( - parent=tab, + ui.make_scrollable( + containing_widget=tab, contained_layout=tab_layout, min_wh=[200, 100], ) diff --git a/napari_cellseg3d/plugin_model_training.py b/napari_cellseg3d/plugin_model_training.py index d8090182..99136641 100644 --- a/napari_cellseg3d/plugin_model_training.py +++ b/napari_cellseg3d/plugin_model_training.py @@ -186,22 +186,19 @@ def __init__( self.zip_choice = ui.make_checkbox("Compress results") - self.validation_percent_choice = ui.IntIncrementCounter( - 10, 90, default=80, step=1, parent=self - ) + self.validation_percent_choice = ui.make_combobox(["80%", "90%"]) - self.epoch_choice = ui.IntIncrementCounter( + self.epoch_choice = ui.make_n_spinboxes( min=2, max=1000, default=self.max_epochs ) self.lbl_epoch_choice = ui.make_label("Number of epochs : ", self) - self.loss_choice = ui.DropdownMenu( + self.loss_choice, self.lbl_loss_choice = ui.make_combobox( sorted(self.loss_dict.keys()), label="Loss function" ) - self.lbl_loss_choice = self.loss_choice.label self.loss_choice.setCurrentIndex(loss_index) - self.sample_choice = ui.IntIncrementCounter( + self.sample_choice = ui.make_n_spinboxes( min=2, max=50, default=self.num_samples ) self.lbl_sample_choice = ui.make_label( @@ -210,12 +207,12 @@ def __init__( self.sample_choice.setVisible(False) self.lbl_sample_choice.setVisible(False) - self.batch_choice = ui.IntIncrementCounter( + self.batch_choice = ui.make_n_spinboxes( min=1, max=10, default=self.batch_size ) self.lbl_batch_choice = ui.make_label("Batch size : ", self) - self.val_interval_choice = ui.IntIncrementCounter( + self.val_interval_choice = ui.make_n_spinboxes( default=self.val_interval ) self.lbl_val_interv_choice = ui.make_label( @@ -230,11 +227,10 @@ def __init__( "1e-6", ] - self.learning_rate_choice = ui.DropdownMenu( - learning_rate_vals, label="Learning rate" - ) - self.lbl_learning_rate_choice = self.learning_rate_choice.label - + ( + self.learning_rate_choice, + self.lbl_learning_rate_choice, + ) = ui.make_combobox(learning_rate_vals, label="Learning rate") self.learning_rate_choice.setCurrentIndex(1) self.augment_choice = ui.make_checkbox("Augment data") @@ -244,7 +240,7 @@ def __init__( ] """Close buttons list for each tab""" - self.patch_size_widgets = ui.IntIncrementCounter.make_n( + self.patch_size_widgets = ui.make_n_spinboxes( 3, 10, 1024, DEFAULT_PATCH_SIZE ) @@ -270,7 +266,7 @@ def __init__( self.use_deterministic_choice = ui.make_checkbox( "Deterministic training", func=self.toggle_deterministic_param ) - self.box_seed = ui.IntIncrementCounter(max=10000000, default=23498) + self.box_seed = ui.make_n_spinboxes(max=10000000, default=23498) self.lbl_seed = ui.make_label("Seed", self) self.container_seed = ui.combine_blocks( self.box_seed, self.lbl_seed, horizontal=False @@ -279,7 +275,7 @@ def __init__( self.progress.setVisible(False) """Dock widget containing the progress bar""" - self.btn_start = ui.Button("Start training", self.start) + self.btn_start = ui.make_button("Start training", self.start) self.btn_model_path.setVisible(False) self.lbl_model_path.setVisible(False) @@ -481,7 +477,7 @@ def build(self): ui.add_blank(self, data_tab_layout) ################ ui.add_to_group( - "Validation (%)", + "Validation %", self.validation_percent_choice, data_tab_layout, ) @@ -694,21 +690,21 @@ def build(self): ###### # end of tab layouts - ui.ScrollArea.make_scrollable( + ui.make_scrollable( contained_layout=data_tab_layout, - parent=data_tab, + containing_widget=data_tab, min_wh=[200, 300], ) # , max_wh=[200,1000]) - ui.ScrollArea.make_scrollable( + ui.make_scrollable( contained_layout=augment_tab_l, - parent=augment_tab_w, + containing_widget=augment_tab_w, min_wh=[200, 300], ) - ui.ScrollArea.make_scrollable( + ui.make_scrollable( contained_layout=train_tab_layout, - parent=train_tab, + containing_widget=train_tab, min_wh=[200, 300], ) self.addTab(data_tab, "Data") @@ -793,8 +789,11 @@ def start(self): raise err self.max_epochs = self.epoch_choice.value() - validation_percent = self.validation_percent_choice.value() / 100 + validation_percent_dict = {"80%": 0.8, "90%": 0.9} + validation_percent = validation_percent_dict[ + self.validation_percent_choice.currentText() + ] print(f"val % : {validation_percent}") self.learning_rate = float(self.learning_rate_choice.currentText()) diff --git a/napari_cellseg3d/plugin_review.py b/napari_cellseg3d/plugin_review.py index 19f4f3d9..b919632e 100644 --- a/napari_cellseg3d/plugin_review.py +++ b/napari_cellseg3d/plugin_review.py @@ -46,7 +46,9 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.checkBox = ui.make_checkbox("Create new dataset ?") - self.btn_start = ui.Button("Start reviewing", self.run_review, self) + self.btn_start = ui.make_button( + "Start reviewing", self.run_review, self + ) self.lbl_mod = ui.make_label("Name", self) @@ -132,8 +134,8 @@ def build(self): ui.add_widgets(layout, [self.btn_start, self.btn_close]) - ui.ScrollArea.make_scrollable( - contained_layout=layout, parent=tab, min_wh=[190, 300] + ui.make_scrollable( + contained_layout=layout, containing_widget=tab, min_wh=[190, 300] ) self.addTab(tab, "Review") @@ -164,9 +166,7 @@ def run_review(self): self.filetype = self.filetype_choice.currentText() self.as_folder = self.file_handling_box.isChecked() if self.anisotropy_widgets.is_enabled(): - zoom = self.anisotropy_widgets.get_anisotropy_resolution_zyx( - as_factors=True - ) + zoom = self.anisotropy_widgets.get_anisotropy_resolution_zyx() else: zoom = [1, 1, 1] @@ -206,8 +206,9 @@ def run_review(self): # TODO : might not work, test with predi labels later labels_raw = None + viewer = self._viewer print("New review session\n" + "*" * 20) - previous_viewer = self._viewer + self._viewer.close() self._viewer, self.docked_widgets = launch_review( images, labels, @@ -219,7 +220,8 @@ def run_review(self): self.as_folder, zoom, ) - previous_viewer.close() + + def reset(self): self._viewer.layers.clear() diff --git a/napari_cellseg3d/utils.py b/napari_cellseg3d/utils.py index fd28d13d..9fdf25c5 100644 --- a/napari_cellseg3d/utils.py +++ b/napari_cellseg3d/utils.py @@ -101,17 +101,6 @@ def dice_coeff(y_true, y_pred): return score -def resize(image, zoom_factors): - from monai.transforms import Zoom - - isotropic_image = Zoom( - zoom_factors, - keep_size=False, - padding_mode="empty", - )(np.expand_dims(image, axis=0)) - return isotropic_image[0] - - def align_array_sizes(array_shape, target_shape): index_differences = [] diff --git a/requirements.txt b/requirements.txt index 9885d9f0..7e53b995 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ sphinx-rtd-theme tox twine numpy -napari[all]>=0.4.14 +napari>=0.4.14 QtPy opencv-python>=4.5.5 dask-image>=0.6.0 diff --git a/setup.cfg b/setup.cfg index 3b81114b..b833df1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ package_dir = # the long list after monai is due to monai optional requirements... Not sure how to know in advance which readers it wil use install_requires = numpy - napari[all]>=0.4.14 + napari>=0.4.14 QtPy opencv-python>=4.5.5 dask-image>=0.6.0