Skip to content

Commit

Permalink
feat: Allow set units that will be used for load/save data using Part…
Browse files Browse the repository at this point in the history
…Seg as napari plugin (#1228)

This PR add "Settings Editor" widget to allow edit PartSeg specific
settings.

This PR add option to set to which units, the scale should be set when
loading to the viewer.

This PR also contains refactor that is mentioned here:
#1226 (comment)

## Summary by Sourcery

Add a 'Settings Editor' widget to the PartSeg napari plugin to enable
editing of specific settings, including setting units for data scaling.
Refactor the plugin structure by relocating files to a new 'napari_io'
directory for improved organization.

New Features:
- Introduce a 'Settings Editor' widget to allow users to edit PartSeg
specific settings within the napari plugin.
- Add functionality to set the units for scaling when loading data into
the viewer.

Enhancements:
- Refactor the PartSeg napari plugin structure by moving files from
'napari_plugins' to 'napari_io' for better organization.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new `SettingsEditor` for managing unit settings within
the PartSeg interface.
- Enhanced plugin handling based on the version of the `napari` library.
- **Bug Fixes**
	- Improved clarity of error messages related to plugin loading.
- **Documentation**
- Updated command signatures in the configuration to reflect new module
structure.
- **Chores**
- Restructured import paths across various modules to enhance
organization and maintainability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
Czaki authored Dec 10, 2024
1 parent b68237f commit 917c93a
Show file tree
Hide file tree
Showing 17 changed files with 90 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .github/project_dict.pws
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 12
personal_ws-1.1 en 14
napari
autoupdate
aspell
Expand All @@ -12,3 +12,4 @@ pre
bool
changelog
czi
PartSeg
2 changes: 1 addition & 1 deletion launcher.spec
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ hiddenimports = (
"psygnal._weak_callback",
"imagecodecs._imagecodecs",
"PartSeg.plugins.napari_widgets",
"PartSegCore.napari_plugins",
"PartSegCore.napari_io",
]
+ [x.module_name for x in imageio_known_plugins.values()]
+ [x for x in collect_submodules("skimage") if "tests" not in x]
Expand Down
10 changes: 5 additions & 5 deletions package/PartSeg/common_backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module contains non gui Qt based components
"""

import contextlib
import os.path
from typing import TYPE_CHECKING

Expand All @@ -14,11 +15,10 @@ def napari_get_settings(path=None) -> "NapariSettings":

if path is not None:
path = os.path.join(path, "settings.yaml")

try:
return _napari_get_settings(path)
except Exception: # pylint: disable=broad-except
return _napari_get_settings()
if path is not None:
with contextlib.suppress(Exception):
return _napari_get_settings(path)
return _napari_get_settings()


__all__ = ("napari_get_settings",)
7 changes: 3 additions & 4 deletions package/PartSeg/launcher_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ def _test_imports(): # pragma: no cover
from PartSeg._roi_mask.main_window import MainWindow as MaskMain
from PartSeg.common_backend.base_argparser import _setup_sentry
from PartSeg.common_gui.label_create import LabelChoose
from PartSeg.plugins import napari_widgets
from PartSegCore import napari_plugins
from PartSeg.plugins import napari_io, napari_widgets

if "BorderSmooth" not in dir(napari_widgets):
raise ImportError("napari_widgets not loaded")

if "load_image" not in dir(napari_plugins):
raise ImportError("napari_plugins not loaded")
if "load_image" not in dir(napari_io):
raise ImportError("napari_io not loaded")

with suppress(ImportError):
from napari.qt import get_app
Expand Down
19 changes: 12 additions & 7 deletions package/PartSeg/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ contributions:
commands:
- id: PartSeg.load_roi_project
title: Get PartSeg ROI project Reader
python_name: PartSegCore.napari_plugins.load_roi_project:napari_get_reader
python_name: PartSeg.plugins.napari_io.load_roi_project:napari_get_reader
- id: PartSeg.load_image
title: Get Image Reader
python_name: PartSegCore.napari_plugins.load_image:napari_get_reader
python_name: PartSeg.plugins.napari_io.load_image:napari_get_reader
- id: PartSeg.write_tiff_image
title: Write tiff Image
python_name: PartSegCore.napari_plugins.save_tiff_layer:napari_write_images
python_name: PartSeg.plugins.napari_io.save_tiff_layer:napari_write_images
- id: PartSeg.write_tiff_labels
title: Write tiff Labels
python_name: PartSegCore.napari_plugins.save_tiff_layer:napari_write_labels
python_name: PartSeg.plugins.napari_io.save_tiff_layer:napari_write_labels
- id: PartSeg.load_mask_project
title: Get PartSeg mask project Reader
python_name: PartSegCore.napari_plugins.load_mask_project:napari_get_reader
python_name: PartSeg.plugins.napari_io.load_mask_project:napari_get_reader
- id: PartSeg.save_mask_roi
title: Write Labels as mask project
python_name: PartSegCore.napari_plugins.save_mask_roi:napari_write_labels
python_name: PartSeg.plugins.napari_io.save_mask_roi:napari_write_labels
- id: PartSeg.load_masked_image
title: Get Reader for image with mask
python_name: PartSegCore.napari_plugins.load_masked_image:napari_get_reader
python_name: PartSeg.plugins.napari_io.load_masked_image:napari_get_reader
- id: PartSeg.SimpleMeasurement
title: Create Simple Measurement
python_name: PartSeg.plugins.napari_widgets.simple_measurement_widget:SimpleMeasurement
Expand Down Expand Up @@ -77,6 +77,9 @@ contributions:
- id: PartSeg.Metadata
title: View Layer Metadata
python_name: PartSeg.plugins.napari_widgets:LayerMetadata
- id: PartSeg.Settings
title: PartSeg Settings
python_name: PartSeg.plugins.napari_widgets:SettingsEditor
readers:
- command: PartSeg.load_roi_project
filename_patterns:
Expand Down Expand Up @@ -162,3 +165,5 @@ contributions:
display_name: Watershed
- command: PartSeg.Metadata
display_name: Layer Metadata
- command: PartSeg.Settings
display_name: Settings Editor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from PartSegCore.napari_plugins import (
from PartSeg.plugins.napari_io import (
load_image,
load_mask_project,
load_masked_image,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools

from PartSeg.plugins.napari_io.loader import partseg_loader
from PartSegCore.analysis.load_functions import LoadStackImage
from PartSegCore.napari_plugins.loader import partseg_loader


def napari_get_reader(path: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools

from PartSeg.plugins.napari_io.loader import partseg_loader
from PartSegCore.mask.io_functions import LoadROI
from PartSegCore.napari_plugins.loader import partseg_loader


def napari_get_reader(path: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import functools
import os.path

from PartSeg.plugins.napari_io.loader import partseg_loader
from PartSegCore.mask.io_functions import LoadStackImageWithMask
from PartSegCore.napari_plugins.loader import partseg_loader


def napari_get_reader(path: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import functools
import os.path

from PartSeg.plugins.napari_io.loader import partseg_loader
from PartSegCore.analysis.load_functions import LoadProject
from PartSegCore.napari_plugins.loader import partseg_loader


def napari_get_reader(path: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import numpy as np
from packaging.version import parse as parse_version

from PartSeg.plugins.napari_widgets._settings import get_settings
from PartSegCore import UNIT_SCALE
from PartSegCore.analysis import ProjectTuple
from PartSegCore.io_utils import LoadBase, WrongFileTypeException
from PartSegCore.mask.io_functions import MaskProjectTuple
Expand Down Expand Up @@ -84,7 +86,8 @@ def _image_to_layers(project_info, scale, translate):
def project_to_layers(project_info: typing.Union[ProjectTuple, MaskProjectTuple]):
res_layers = []
if project_info.image is not None and not isinstance(project_info.image, str):
scale = project_info.image.normalized_scaling()
settings = get_settings()
scale = project_info.image.normalized_scaling(UNIT_SCALE[settings.io_units.value])
translate = project_info.image.shift
translate = (0,) * (len(project_info.image.axis_order.replace("C", "")) - len(translate)) + translate
res_layers.extend(_image_to_layers(project_info, scale, translate))
Expand Down
2 changes: 2 additions & 0 deletions package/PartSeg/plugins/napari_widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from PartSeg._launcher.main_window import PartSegGUILauncher
from PartSeg.plugins.napari_widgets._settings import SettingsEditor
from PartSeg.plugins.napari_widgets.algorithm_widgets import (
BorderSmooth,
ConnectedComponents,
Expand Down Expand Up @@ -33,4 +34,5 @@
"ROIAnalysisExtraction",
"ROIMaskExtraction",
"SearchLabel",
"SettingsEditor",
)
29 changes: 28 additions & 1 deletion package/PartSeg/plugins/napari_widgets/_settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os

from magicgui.widgets import Container, create_widget
from napari.layers import Layer

from PartSeg._roi_analysis.partseg_settings import PartSettings
from PartSeg.common_backend import napari_get_settings
from PartSegCore import Units
from PartSegCore.json_hooks import PartSegEncoder

_SETTINGS = None
Expand All @@ -19,8 +21,16 @@ def default(self, o):
class PartSegNapariSettings(PartSettings):
json_encoder_class = PartSegNapariEncoder

@property
def io_units(self) -> Units:
return self.get("io_units", Units.nm)

def get_settings() -> PartSettings:
@io_units.setter
def io_units(self, value: Units):
self.set("io_units", value)


def get_settings() -> PartSegNapariSettings:
global _SETTINGS # noqa: PLW0603 # pylint: disable=global-statement
if _SETTINGS is None:
napari_settings = napari_get_settings()
Expand All @@ -31,3 +41,20 @@ def get_settings() -> PartSettings:
_SETTINGS = PartSegNapariSettings(os.path.join(save_path, "PartSeg_napari_plugins"))
_SETTINGS.load()
return _SETTINGS


class SettingsEditor(Container):
def __init__(self):
super().__init__()
self.settings = get_settings()
self.units_select = create_widget(self.settings.io_units, annotation=Units, label="Units for io")
self.units_select.changed.connect(self.units_selection_changed)
self.settings.connect_("io_units", self.units_changed)
self.append(self.units_select)

def units_selection_changed(self, value):
self.settings.io_units = value
self.settings.dump()

def units_changed(self):
self.units_select.value = self.settings.io_units
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
from napari.layers import Image, Labels, Layer
from packaging.version import parse as parse_version

from PartSegCore.analysis import ProjectTuple
from PartSegCore.mask.io_functions import LoadROIFromTIFF
from PartSegCore.napari_plugins.load_image import napari_get_reader as napari_get_reader_image
from PartSegCore.napari_plugins.load_mask_project import napari_get_reader as napari_get_reader_mask
from PartSegCore.napari_plugins.load_masked_image import napari_get_reader as napari_get_reader_mask_image
from PartSegCore.napari_plugins.load_roi_project import napari_get_reader as napari_get_reader_roi
from PartSegCore.napari_plugins.loader import project_to_layers
from PartSegCore.napari_plugins.save_mask_roi import napari_write_labels
from PartSegCore.napari_plugins.save_tiff_layer import (
from PartSeg.plugins.napari_io.load_image import napari_get_reader as napari_get_reader_image
from PartSeg.plugins.napari_io.load_mask_project import napari_get_reader as napari_get_reader_mask
from PartSeg.plugins.napari_io.load_masked_image import napari_get_reader as napari_get_reader_mask_image
from PartSeg.plugins.napari_io.load_roi_project import napari_get_reader as napari_get_reader_roi
from PartSeg.plugins.napari_io.loader import project_to_layers
from PartSeg.plugins.napari_io.save_mask_roi import napari_write_labels
from PartSeg.plugins.napari_io.save_tiff_layer import (
napari_write_images,
)
from PartSegCore.napari_plugins.save_tiff_layer import (
from PartSeg.plugins.napari_io.save_tiff_layer import (
napari_write_labels as napari_write_labels_tiff,
)
from PartSegCore.analysis import ProjectTuple
from PartSegCore.mask.io_functions import LoadROIFromTIFF
from PartSegImage import GenericImageReader
from PartSegImage import Image as PImage

Expand Down
20 changes: 18 additions & 2 deletions package/tests/test_PartSeg/test_napari_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from packaging.version import parse as parse_version
from qtpy.QtCore import QObject, QTimer, Signal

from PartSeg._roi_analysis.partseg_settings import PartSettings
from PartSeg._roi_analysis.profile_export import ExportDialog, ImportDialog
from PartSeg.common_gui.custom_load_dialog import CustomLoadDialog
from PartSeg.common_gui.custom_save_dialog import CustomSaveDialog
Expand Down Expand Up @@ -46,6 +45,7 @@
from PartSeg.plugins.napari_widgets.measurement_widget import update_properties
from PartSeg.plugins.napari_widgets.roi_extraction_algorithms import ProfilePreviewDialog, QInputDialog
from PartSeg.plugins.napari_widgets.search_label_widget import HIGHLIGHT_LABEL_NAME
from PartSegCore import Units
from PartSegCore.algorithm_describe_base import ROIExtractionProfile
from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection
from PartSegCore.analysis.load_functions import LoadProfileFromJSON
Expand Down Expand Up @@ -85,7 +85,7 @@ def check_direct_mode(layer):
@pytest.fixture(autouse=True)
def _clean_settings(tmp_path):
old_settings = _settings._SETTINGS
_settings._SETTINGS = PartSettings(tmp_path)
_settings._SETTINGS = _settings.PartSegNapariSettings(tmp_path)
yield
_settings._SETTINGS = old_settings

Expand Down Expand Up @@ -639,3 +639,19 @@ def test_add_layer_post_init(self, make_napari_viewer, qtbot):
def test_enum():
assert " " in str(CompareType.lower_threshold)
assert " " in str(FlowType.dark_center)


class TestSettingsWidget:
def test_create(self, qtbot):
w = _settings.SettingsEditor()
qtbot.addWidget(w.native)

def test_change_units(self, qtbot):
s = _settings.get_settings()
s.io_units = Units.µm
w = _settings.SettingsEditor()
qtbot.addWidget(w.native)
w.units_select.value = Units.nm
assert s.io_units == Units.nm
s.io_units = Units.mm
assert w.units_select.value == Units.mm

0 comments on commit 917c93a

Please sign in to comment.