From 20c199a47356fb760e1fd4682c4e8da2ee288c13 Mon Sep 17 00:00:00 2001 From: Maple <60655531+maple60@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:57:54 +0900 Subject: [PATCH 1/2] chore: rename internal package to leaf_contour_efd with compatibility wrappers --- build.spec | 2 +- pyproject.toml | 2 +- src/leaf_contour_efd/__init__.py | 1 + src/leaf_contour_efd/__main__.py | 7 + src/leaf_contour_efd/main.py | 190 +++++++++++++++++ .../utils/__init__.py | 0 .../utils/add_ROIs_layer.py | 0 .../widgets/__init__.py | 0 .../widgets/binarize_image.py | 0 .../widgets/calculate_efd.py | 0 .../widgets/clear_viewer.py | 0 .../widgets/crop_rectangle.py | 0 .../widgets/extract_contour.py | 0 .../widgets/make_points_tool_widget.py | 0 .../widgets/rotate_image.py | 0 .../widgets/set_scale.py | 0 src/leaf_shape_tool/__init__.py | 6 + src/leaf_shape_tool/__main__.py | 19 +- src/leaf_shape_tool/main.py | 195 ++---------------- website/installation.qmd | 2 +- website/installation_ja.qmd | 2 +- 21 files changed, 233 insertions(+), 193 deletions(-) create mode 100644 src/leaf_contour_efd/__init__.py create mode 100644 src/leaf_contour_efd/__main__.py create mode 100644 src/leaf_contour_efd/main.py rename src/{leaf_shape_tool => leaf_contour_efd}/utils/__init__.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/utils/add_ROIs_layer.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/__init__.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/binarize_image.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/calculate_efd.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/clear_viewer.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/crop_rectangle.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/extract_contour.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/make_points_tool_widget.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/rotate_image.py (100%) rename src/{leaf_shape_tool => leaf_contour_efd}/widgets/set_scale.py (100%) diff --git a/build.spec b/build.spec index ec69d12..82057ff 100644 --- a/build.spec +++ b/build.spec @@ -104,7 +104,7 @@ runtime_hooks = [ ] a = Analysis( - [os.path.join("src", "leaf_shape_tool", "__main__.py")], + [os.path.join("src", "leaf_contour_efd", "__main__.py")], pathex=[], binaries=binaries, datas=datas, diff --git a/pyproject.toml b/pyproject.toml index 9f276bc..7b68f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ ] [project.scripts] -leaf-contour-efd = "leaf_shape_tool.main:main" +leaf-contour-efd = "leaf_contour_efd.main:main" [tool.setuptools.packages.find] where = ["src"] diff --git a/src/leaf_contour_efd/__init__.py b/src/leaf_contour_efd/__init__.py new file mode 100644 index 0000000..6169a76 --- /dev/null +++ b/src/leaf_contour_efd/__init__.py @@ -0,0 +1 @@ +"""LeafContourEFD package.""" diff --git a/src/leaf_contour_efd/__main__.py b/src/leaf_contour_efd/__main__.py new file mode 100644 index 0000000..ef55652 --- /dev/null +++ b/src/leaf_contour_efd/__main__.py @@ -0,0 +1,7 @@ +"""Command-line entry point for ``python -m leaf_contour_efd``.""" + +from leaf_contour_efd.main import main + + +if __name__ == "__main__": + main() diff --git a/src/leaf_contour_efd/main.py b/src/leaf_contour_efd/main.py new file mode 100644 index 0000000..65abbe3 --- /dev/null +++ b/src/leaf_contour_efd/main.py @@ -0,0 +1,190 @@ +""" +Main entry point for the Napari-based leaf shape analysis workflow. + +This script initializes the Napari viewer and integrates multiple custom widgets +for region-of-interest (ROI) extraction, landmark annotation, image rotation, +binarization, contour extraction, scale calibration, and Elliptic Fourier Descriptor (EFD) analysis. + +The GUI layout and event-driven behavior are designed for interactive leaf +image processing and morphometric analysis. + +Author: Maple +License: BSD-3-Clause +""" + +# For SAM2if getattr(sys, "frozen", False): +import os +import sys + + +def _add_sam2_path(): + if getattr(sys, "frozen", False): + # PyInstaller one-dir/one-file + base = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable)) + internal_sam2 = os.path.join(base, "_internal", "sam2") + if os.path.isdir(internal_sam2): + sys.path.insert(0, internal_sam2) + else: + # Development (run via python) + local_sam2 = os.path.join(os.getcwd(), "sam2") + if os.path.isdir(local_sam2): + sys.path.insert(0, local_sam2) + + +_add_sam2_path() + + +import torch # noqa: E402 + +# TODO: Add GPU support +torch.set_default_device("cpu") # Ensure CPU usage by default + +import napari # noqa: E402 +from functools import partial # noqa: E402 +from qtpy.QtCore import Qt, QTimer # noqa: E402 +from qtpy.QtWidgets import QDockWidget # noqa: E402 + +# --- Custom widget imports --- +from leaf_contour_efd.widgets import crop_rectangle # noqa: E402 +from leaf_contour_efd.widgets.make_points_tool_widget import make_points_tools_widget # noqa: E402 +from leaf_contour_efd.widgets.rotate_image import ( # noqa: E402 + make_points_metadata_widget, + get_active_points_layer, + summarize_points_layer, +) +from leaf_contour_efd.widgets.binarize_image import make_binarize_image_widget # noqa: E402 +from leaf_contour_efd.widgets.extract_contour import make_extract_contour_widget # noqa: E402 +from leaf_contour_efd.widgets.set_scale import make_set_scale_widget # noqa: E402 +from leaf_contour_efd.widgets.calculate_efd import calculate_efd_and_save # noqa: E402 +from leaf_contour_efd.widgets.clear_viewer import make_clear_viewer_widget # noqa: E402 +from leaf_contour_efd.utils.add_ROIs_layer import add_ROIs # noqa: E402 + + +# --------------------------------------------------------------------- +# Initialize Napari viewer +# --------------------------------------------------------------------- + +viewer = napari.Viewer() +# Automatically add ROI layer when a new image layer is added +viewer.layers.events.inserted.connect(partial(add_ROIs, viewer)) + +# --------------------------------------------------------------------- +# Create and register widgets +# --------------------------------------------------------------------- + +roi_widget = crop_rectangle.make_add_roi_widget(viewer) +dock_00 = viewer.window.add_dock_widget( + roi_widget, name="ROI Extractoin Widget", area="right", tabify=False +) + + +# Callback to reset SpinBox to 1 on hard reset (check_keep_base=False) +def _reset_roi_spinbox_to_1(): + # Set the SpinBox value to 1 safely + try: + roi_widget.roi_index.value = 1 # Reset to 1 + except Exception as e: + print("Error resetting ROI SpinBox:", e) + + +# Landmark / point tools widget +dock = make_points_tools_widget(viewer) +viewer.window.add_dock_widget(dock, area="left", name="Point Tools") + +# Image rotation widget (based on landmark metadata) +points_meta_widget = make_points_metadata_widget( + viewer, + get_active_points_layer, + summarize_points_layer, +) +dock_01 = viewer.window.add_dock_widget( + points_meta_widget, name="Rotation Widget", area="right", tabify=False +) + +# Binarization widget (Otsu / SAM2) +binarize_widget = make_binarize_image_widget(viewer) +dock_02 = viewer.window.add_dock_widget( + binarize_widget, name="Binarization Widget", area="right", tabify=False +) + +# Contour extraction widget +contour_widget = make_extract_contour_widget(viewer) +dock_03 = viewer.window.add_dock_widget( + contour_widget, + name="Contour Extraction Widget", + area="right", + tabify=False, +) + +# Clear viewer widget +clear_viewer_widget = make_clear_viewer_widget( + viewer, on_hard_reset=_reset_roi_spinbox_to_1 +) +dock_04 = viewer.window.add_dock_widget( + clear_viewer_widget, name="Clear Viewer", area="right", tabify=False +) + +# Scale calibration widget +set_scale_widget = make_set_scale_widget(viewer) +viewer.window.add_dock_widget(set_scale_widget, area="left", tabify=False) + + +# --------------------------------------------------------------------- +# Arrange dock widgets vertically (right side) +# --------------------------------------------------------------------- +def pack_right_docks_top(): + """ + Dynamically resize and align right-side dock widgets vertically. + Ensures consistent spacing and visibility of all widgets. + """ + main_window = viewer.window._qt_window # QMainWindow instance + qdocks = [ + d + for d in main_window.findChildren(QDockWidget) + if main_window.dockWidgetArea(d) == Qt.RightDockWidgetArea + and not d.isFloating() + ] + if not qdocks: + return + + # Sort by y-position (top to bottom) + qdocks.sort(key=lambda d: d.geometry().top()) + + sizes = [] + for d in qdocks[:-1]: + h = d.widget().sizeHint().height() + d.widget().setMinimumHeight(min(h, 300)) + sizes.append(max(h, 150)) # Ensure visibility + + sizes.append(10_000) # Let the last dock absorb remaining space + + main_window.resizeDocks(qdocks, sizes, Qt.Vertical) + + +# Trigger dock layout adjustment after GUI initialization +QTimer.singleShot(0, pack_right_docks_top) + +# --------------------------------------------------------------------- +# Signal connections +# --------------------------------------------------------------------- +# Trigger EFD calculation automatically after contour extraction +contour_widget.called.connect(calculate_efd_and_save) # EFD calculation + + +# Add ROI shortcut (Shift + S) +@viewer.bind_key("Shift-s") +def _add_roi_shortcut(v): + roi_widget() + + +# --------------------------------------------------------------------- +# Run Napari viewer +# --------------------------------------------------------------------- +def main(): + """Launch the LeafContourEFD GUI.""" + viewer.title = "LeafContourEFD — powered by napari" # Set window title + napari.run() # Run the viewer + + +if __name__ == "__main__": + main() diff --git a/src/leaf_shape_tool/utils/__init__.py b/src/leaf_contour_efd/utils/__init__.py similarity index 100% rename from src/leaf_shape_tool/utils/__init__.py rename to src/leaf_contour_efd/utils/__init__.py diff --git a/src/leaf_shape_tool/utils/add_ROIs_layer.py b/src/leaf_contour_efd/utils/add_ROIs_layer.py similarity index 100% rename from src/leaf_shape_tool/utils/add_ROIs_layer.py rename to src/leaf_contour_efd/utils/add_ROIs_layer.py diff --git a/src/leaf_shape_tool/widgets/__init__.py b/src/leaf_contour_efd/widgets/__init__.py similarity index 100% rename from src/leaf_shape_tool/widgets/__init__.py rename to src/leaf_contour_efd/widgets/__init__.py diff --git a/src/leaf_shape_tool/widgets/binarize_image.py b/src/leaf_contour_efd/widgets/binarize_image.py similarity index 100% rename from src/leaf_shape_tool/widgets/binarize_image.py rename to src/leaf_contour_efd/widgets/binarize_image.py diff --git a/src/leaf_shape_tool/widgets/calculate_efd.py b/src/leaf_contour_efd/widgets/calculate_efd.py similarity index 100% rename from src/leaf_shape_tool/widgets/calculate_efd.py rename to src/leaf_contour_efd/widgets/calculate_efd.py diff --git a/src/leaf_shape_tool/widgets/clear_viewer.py b/src/leaf_contour_efd/widgets/clear_viewer.py similarity index 100% rename from src/leaf_shape_tool/widgets/clear_viewer.py rename to src/leaf_contour_efd/widgets/clear_viewer.py diff --git a/src/leaf_shape_tool/widgets/crop_rectangle.py b/src/leaf_contour_efd/widgets/crop_rectangle.py similarity index 100% rename from src/leaf_shape_tool/widgets/crop_rectangle.py rename to src/leaf_contour_efd/widgets/crop_rectangle.py diff --git a/src/leaf_shape_tool/widgets/extract_contour.py b/src/leaf_contour_efd/widgets/extract_contour.py similarity index 100% rename from src/leaf_shape_tool/widgets/extract_contour.py rename to src/leaf_contour_efd/widgets/extract_contour.py diff --git a/src/leaf_shape_tool/widgets/make_points_tool_widget.py b/src/leaf_contour_efd/widgets/make_points_tool_widget.py similarity index 100% rename from src/leaf_shape_tool/widgets/make_points_tool_widget.py rename to src/leaf_contour_efd/widgets/make_points_tool_widget.py diff --git a/src/leaf_shape_tool/widgets/rotate_image.py b/src/leaf_contour_efd/widgets/rotate_image.py similarity index 100% rename from src/leaf_shape_tool/widgets/rotate_image.py rename to src/leaf_contour_efd/widgets/rotate_image.py diff --git a/src/leaf_shape_tool/widgets/set_scale.py b/src/leaf_contour_efd/widgets/set_scale.py similarity index 100% rename from src/leaf_shape_tool/widgets/set_scale.py rename to src/leaf_contour_efd/widgets/set_scale.py diff --git a/src/leaf_shape_tool/__init__.py b/src/leaf_shape_tool/__init__.py index e69de29..9a62e1c 100644 --- a/src/leaf_shape_tool/__init__.py +++ b/src/leaf_shape_tool/__init__.py @@ -0,0 +1,6 @@ +"""Backward-compatible wrappers for the renamed package. + +This package has been renamed to ``leaf_contour_efd``. +""" + +from leaf_contour_efd import * # noqa: F401,F403 diff --git a/src/leaf_shape_tool/__main__.py b/src/leaf_shape_tool/__main__.py index f8700d4..337a015 100644 --- a/src/leaf_shape_tool/__main__.py +++ b/src/leaf_shape_tool/__main__.py @@ -1,9 +1,16 @@ -# src/leaf_shape_tool/__main__.py +"""Backward-compatible module runner for ``python -m leaf_shape_tool``.""" -""" -Command-line entry point for the LeafContourEFD. -""" +from __future__ import annotations -from leaf_shape_tool.main import main +import warnings -main() +from leaf_contour_efd.main import main + + +if __name__ == "__main__": + warnings.warn( + "`python -m leaf_shape_tool` is deprecated; use `python -m leaf_contour_efd`.", + DeprecationWarning, + stacklevel=1, + ) + main() diff --git a/src/leaf_shape_tool/main.py b/src/leaf_shape_tool/main.py index a12a271..a06ae16 100644 --- a/src/leaf_shape_tool/main.py +++ b/src/leaf_shape_tool/main.py @@ -1,190 +1,19 @@ -""" -Main entry point for the Napari-based leaf shape analysis workflow. - -This script initializes the Napari viewer and integrates multiple custom widgets -for region-of-interest (ROI) extraction, landmark annotation, image rotation, -binarization, contour extraction, scale calibration, and Elliptic Fourier Descriptor (EFD) analysis. - -The GUI layout and event-driven behavior are designed for interactive leaf -image processing and morphometric analysis. +"""Backward-compatible entry point for ``leaf_shape_tool``. -Author: Maple -License: BSD-3-Clause +Deprecated: use ``leaf_contour_efd.main`` instead. """ -# For SAM2if getattr(sys, "frozen", False): -import os -import sys - - -def _add_sam2_path(): - if getattr(sys, "frozen", False): - # PyInstaller one-dir/one-file - base = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable)) - internal_sam2 = os.path.join(base, "_internal", "sam2") - if os.path.isdir(internal_sam2): - sys.path.insert(0, internal_sam2) - else: - # Development (run via python) - local_sam2 = os.path.join(os.getcwd(), "sam2") - if os.path.isdir(local_sam2): - sys.path.insert(0, local_sam2) - - -_add_sam2_path() - - -import torch # noqa: E402 - -# TODO: Add GPU support -torch.set_default_device("cpu") # Ensure CPU usage by default - -import napari # noqa: E402 -from functools import partial # noqa: E402 -from qtpy.QtCore import Qt, QTimer # noqa: E402 -from qtpy.QtWidgets import QDockWidget # noqa: E402 - -# --- Custom widget imports --- -from leaf_shape_tool.widgets import crop_rectangle # noqa: E402 -from leaf_shape_tool.widgets.make_points_tool_widget import make_points_tools_widget # noqa: E402 -from leaf_shape_tool.widgets.rotate_image import ( # noqa: E402 - make_points_metadata_widget, - get_active_points_layer, - summarize_points_layer, -) -from leaf_shape_tool.widgets.binarize_image import make_binarize_image_widget # noqa: E402 -from leaf_shape_tool.widgets.extract_contour import make_extract_contour_widget # noqa: E402 -from leaf_shape_tool.widgets.set_scale import make_set_scale_widget # noqa: E402 -from leaf_shape_tool.widgets.calculate_efd import calculate_efd_and_save # noqa: E402 -from leaf_shape_tool.widgets.clear_viewer import make_clear_viewer_widget # noqa: E402 -from leaf_shape_tool.utils.add_ROIs_layer import add_ROIs # noqa: E402 - - -# --------------------------------------------------------------------- -# Initialize Napari viewer -# --------------------------------------------------------------------- - -viewer = napari.Viewer() -# Automatically add ROI layer when a new image layer is added -viewer.layers.events.inserted.connect(partial(add_ROIs, viewer)) - -# --------------------------------------------------------------------- -# Create and register widgets -# --------------------------------------------------------------------- - -roi_widget = crop_rectangle.make_add_roi_widget(viewer) -dock_00 = viewer.window.add_dock_widget( - roi_widget, name="ROI Extractoin Widget", area="right", tabify=False -) - - -# Callback to reset SpinBox to 1 on hard reset (check_keep_base=False) -def _reset_roi_spinbox_to_1(): - # Set the SpinBox value to 1 safely - try: - roi_widget.roi_index.value = 1 # Reset to 1 - except Exception as e: - print("Error resetting ROI SpinBox:", e) - - -# Landmark / point tools widget -dock = make_points_tools_widget(viewer) -viewer.window.add_dock_widget(dock, area="left", name="Point Tools") - -# Image rotation widget (based on landmark metadata) -points_meta_widget = make_points_metadata_widget( - viewer, - get_active_points_layer, - summarize_points_layer, -) -dock_01 = viewer.window.add_dock_widget( - points_meta_widget, name="Rotation Widget", area="right", tabify=False -) - -# Binarization widget (Otsu / SAM2) -binarize_widget = make_binarize_image_widget(viewer) -dock_02 = viewer.window.add_dock_widget( - binarize_widget, name="Binarization Widget", area="right", tabify=False -) - -# Contour extraction widget -contour_widget = make_extract_contour_widget(viewer) -dock_03 = viewer.window.add_dock_widget( - contour_widget, - name="Contour Extraction Widget", - area="right", - tabify=False, -) - -# Clear viewer widget -clear_viewer_widget = make_clear_viewer_widget( - viewer, on_hard_reset=_reset_roi_spinbox_to_1 -) -dock_04 = viewer.window.add_dock_widget( - clear_viewer_widget, name="Clear Viewer", area="right", tabify=False -) - -# Scale calibration widget -set_scale_widget = make_set_scale_widget(viewer) -viewer.window.add_dock_widget(set_scale_widget, area="left", tabify=False) - - -# --------------------------------------------------------------------- -# Arrange dock widgets vertically (right side) -# --------------------------------------------------------------------- -def pack_right_docks_top(): - """ - Dynamically resize and align right-side dock widgets vertically. - Ensures consistent spacing and visibility of all widgets. - """ - main_window = viewer.window._qt_window # QMainWindow instance - qdocks = [ - d - for d in main_window.findChildren(QDockWidget) - if main_window.dockWidgetArea(d) == Qt.RightDockWidgetArea - and not d.isFloating() - ] - if not qdocks: - return - - # Sort by y-position (top to bottom) - qdocks.sort(key=lambda d: d.geometry().top()) - - sizes = [] - for d in qdocks[:-1]: - h = d.widget().sizeHint().height() - d.widget().setMinimumHeight(min(h, 300)) - sizes.append(max(h, 150)) # Ensure visibility - - sizes.append(10_000) # Let the last dock absorb remaining space - - main_window.resizeDocks(qdocks, sizes, Qt.Vertical) - - -# Trigger dock layout adjustment after GUI initialization -QTimer.singleShot(0, pack_right_docks_top) - -# --------------------------------------------------------------------- -# Signal connections -# --------------------------------------------------------------------- -# Trigger EFD calculation automatically after contour extraction -contour_widget.called.connect(calculate_efd_and_save) # EFD calculation - - -# Add ROI shortcut (Shift + S) -@viewer.bind_key("Shift-s") -def _add_roi_shortcut(v): - roi_widget() +from __future__ import annotations +import warnings -# --------------------------------------------------------------------- -# Run Napari viewer -# --------------------------------------------------------------------- -def main(): - """Launch the LeafContourEFD GUI.""" - viewer.title = "LeafContourEFD — powered by napari" # Set window title - napari.run() # Run the viewer +from leaf_contour_efd.main import main as _main -if __name__ == "__main__": - main() +def main() -> None: + warnings.warn( + "`leaf_shape_tool.main` is deprecated; use `leaf_contour_efd.main`.", + DeprecationWarning, + stacklevel=2, + ) + _main() diff --git a/website/installation.qmd b/website/installation.qmd index 816cefa..71c8e43 100644 --- a/website/installation.qmd +++ b/website/installation.qmd @@ -193,7 +193,7 @@ leaf-contour-efd Or, run it directly from the source directory: ```bash -python -m leaf_shape_tool +python -m leaf_contour_efd ``` ### If uv Is Not Installed diff --git a/website/installation_ja.qmd b/website/installation_ja.qmd index bd74ad6..4b11b4b 100644 --- a/website/installation_ja.qmd +++ b/website/installation_ja.qmd @@ -192,7 +192,7 @@ leaf-contour-efd または、開発ディレクトリから直接起動する場合は以下を実行します。 ```bash -python -m leaf_shape_tool +python -m leaf_contour_efd ``` ### uv がインストールされていない場合 From 9713af03e83cff7aaf37e005b51326ce6e0bfe32 Mon Sep 17 00:00:00 2001 From: Maple <60655531+maple60@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:10:02 +0900 Subject: [PATCH 2/2] fix: execute legacy leaf_shape_tool.main module entrypoint --- src/leaf_shape_tool/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/leaf_shape_tool/main.py b/src/leaf_shape_tool/main.py index a06ae16..d4cdfce 100644 --- a/src/leaf_shape_tool/main.py +++ b/src/leaf_shape_tool/main.py @@ -17,3 +17,7 @@ def main() -> None: stacklevel=2, ) _main() + + +if __name__ == "__main__": + main()