Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/leaf_contour_efd/utils/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Path utilities for output/export directories."""

from __future__ import annotations

from pathlib import Path
import sys

from appdirs import user_data_dir

_APP_DIR_NAME = "leaf_contour_efd"


def _user_writable_base_dir() -> Path:
"""Return a user-writable directory for application outputs."""
return Path(user_data_dir(_APP_DIR_NAME)).resolve()


def _is_writable_dir(path: Path) -> bool:
"""Return whether ``path`` can be used as a writable directory."""
try:
path.mkdir(parents=True, exist_ok=True)
probe = path / ".write_test"
with probe.open("w", encoding="utf-8"):
pass
probe.unlink(missing_ok=True)
return True
except OSError:
return False


def get_output_base_dir() -> Path:
"""Return the base directory used for writing the ``output`` folder.

Rules
-----
- Frozen on all platforms: user-writable application data directory.
- Non-frozen: repository/application root when writable, otherwise user data.
"""
fallback = _user_writable_base_dir()

if getattr(sys, "frozen", False):
return fallback

# Development mode: src/leaf_contour_efd/utils/paths.py -> repo root
repo_root = Path(__file__).resolve().parents[3]
if _is_writable_dir(repo_root):
return repo_root
return fallback


def get_output_dir(*parts: str) -> Path:
"""Return an output subdirectory and ensure it exists.

If directory creation fails in the primary base location, this function
transparently falls back to a user-writable directory.
"""
out_dir = get_output_base_dir() / "output"
if parts:
out_dir = out_dir.joinpath(*parts)

try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError:
out_dir = _user_writable_base_dir() / "output"
if parts:
out_dir = out_dir.joinpath(*parts)
out_dir.mkdir(parents=True, exist_ok=True)

return out_dir
3 changes: 2 additions & 1 deletion src/leaf_contour_efd/widgets/binarize_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from napari.layers import Image
from napari.utils.colormaps import DirectLabelColormap
from pathlib import Path
from leaf_contour_efd.utils.paths import get_output_dir


def make_binarize_image_widget(viewer: "napari.Viewer"):
Expand Down Expand Up @@ -223,7 +224,7 @@ def binarize_image(
method: str = "Otsu",
threshold: int = 128,
save_check: bool = True,
save_dir: Path = Path("./output/binarized_images"),
save_dir: Path = get_output_dir("binarized_images"),
):
"""
Run the selected binarization method
Expand Down
7 changes: 3 additions & 4 deletions src/leaf_contour_efd/widgets/calculate_efd.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pandas as pd
from pathlib import Path
import copy
from leaf_contour_efd.utils.paths import get_output_dir


def close_contour(df_contour: pd.DataFrame) -> np.ndarray:
Expand Down Expand Up @@ -391,10 +392,8 @@ def calculate_efd_and_save(payload) -> None:
# fallback to plain string
leaf_id = str(leaf_id)

output_dir_ef = Path("output/coefficients_efd")
output_dir_ef_normalized = Path("output/coefficients_efd_normalized")
output_dir_ef.mkdir(parents=True, exist_ok=True)
output_dir_ef_normalized.mkdir(parents=True, exist_ok=True)
output_dir_ef = get_output_dir("coefficients_efd")
output_dir_ef_normalized = get_output_dir("coefficients_efd_normalized")
ef_file = output_dir_ef / f"{id}_{leaf_id}.csv"
ef_normalized_file = output_dir_ef_normalized / f"{id}_{leaf_id}.csv"
df_ef.to_csv(ef_file, index=False)
Expand Down
3 changes: 2 additions & 1 deletion src/leaf_contour_efd/widgets/clear_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from typing import Sequence

from qtpy.QtCore import QTimer
from leaf_contour_efd.utils.paths import get_output_dir


def _clear_except_async(viewer, keep_names: set[str], on_done=None):
Expand Down Expand Up @@ -151,7 +152,7 @@ def _get_base_image_id() -> str:

# ---- create FileEdit with dynamic default path ----
default_image_id = _get_base_image_id()
default_dir = Path("output/rois")
default_dir = get_output_dir("rois")
# default_dir.mkdir(parents=True, exist_ok=True)
default_path = default_dir / f"{default_image_id}.png"

Expand Down
3 changes: 2 additions & 1 deletion src/leaf_contour_efd/widgets/crop_rectangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from pathlib import Path
import napari
from qtpy.QtCore import QTimer
from leaf_contour_efd.utils.paths import get_output_dir

COLOR_CYCLE = ["#FF4B00", "#005AFF"] # base(orange), tip(blue)
LABELS = ("base", "tip")
Expand Down Expand Up @@ -243,7 +244,7 @@ def _apply():
def add_roi(
roi_index: int = 1,
save_cropped: bool = True,
save_dir: str = "output/cropped_images",
save_dir: Path = get_output_dir("cropped_images"),
):
"""Add a cropped ROI image and its landmark layer."""
ROIs_layer = viewer.layers["ROIs"]
Expand Down
7 changes: 4 additions & 3 deletions src/leaf_contour_efd/widgets/extract_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from datetime import datetime
from napari.utils.colormaps import DirectLabelColormap
from qtpy.QtCore import QTimer
from leaf_contour_efd.utils.paths import get_output_dir


def make_extract_contour_widget(viewer: "napari.Viewer"):
Expand Down Expand Up @@ -76,9 +77,9 @@ def make_extract_contour_widget(viewer: "napari.Viewer"):
call_button="Extract Contour",
)
def extract_contour(
folder_csv=Path("./output/contour"),
folder_final_mask=Path("./output/binarized_image_final"),
folder_blob_mask=Path("./output/contour_image"),
folder_csv=Path(get_output_dir("contour")),
folder_final_mask=Path(get_output_dir("binarized_image_final")),
folder_blob_mask=Path(get_output_dir("contour_image")),
save_final_mask=True,
save_blob_mask=True,
):
Expand Down
3 changes: 2 additions & 1 deletion src/leaf_contour_efd/widgets/rotate_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pathlib import Path
from numbers import Integral
import copy
from leaf_contour_efd.utils.paths import get_output_dir


# -------------------------------------------------------------------------
Expand Down Expand Up @@ -289,7 +290,7 @@ def make_points_metadata_widget(
def points_metadata_widget(
viewer: "napari.Viewer",
save_rotated: bool = True,
save_dir: str = "output/rotated_images",
save_dir: Path = get_output_dir("rotated_images"),
) -> dict[str, Any]:
"""Rotate the image corresponding to the current Points layer."""
try:
Expand Down
Loading