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
26 changes: 21 additions & 5 deletions PyReconstruct/modules/backend/autoseg/palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def palette_color(label_id, palette=None, seed: int = 0) -> tuple:
return tuple(int(c) for c in palette[index])


def next_shuffle_seed(current_seed: int, palette=None, rng=None) -> int:
def next_shuffle_seed(current_seed: int, palette=None, ids=None, rng=None) -> int:
"""Pick a new seed whose color arrangement differs from the current one.

This backs the "Shuffle colors" button: each click must visibly change the
Expand All @@ -174,10 +174,20 @@ def next_shuffle_seed(current_seed: int, palette=None, rng=None) -> int:
option, so determinism and preview==import are preserved exactly as with a
hand-entered seed.

The guarantee is enforced over ``ids``: the label ids the caller can
actually see. With only a few labels on screen, a generic 1..63 fingerprint
can "change" while none of the *visible* labels move color -- a no-op click
for the user. Passing the overlay's present ids ties the guarantee to what
is on screen. When ``ids`` is None (or has no usable entries) it falls back
to the 1..63 range, which reaches every palette entry.

Params:
current_seed (int): the seed currently in effect
palette: list of (R, G, B) entries; falls back to the shipped
default when None or empty
ids: optional iterable of label ids to fingerprint the arrangement
over (the visible/present ids); None uses the default 1..63
range
rng: an optional random.Random (injectable for deterministic tests);
a fresh default source is used when None
Returns:
Expand All @@ -196,10 +206,16 @@ def next_shuffle_seed(current_seed: int, palette=None, rng=None) -> int:
if len(palette) < 2:
return int(current_seed)

# A small id sample fingerprints the arrangement. Reaching every entry over
# ~64 ids is near-certain, so two seeds agreeing across all of them means
# the visible mapping is identical.
sample = range(1, 64)
# Fingerprint the arrangement over the visible ids when supplied (id 0 is
# background, not a segment, so it is dropped); otherwise a 1..63 sample,
# which reaches every entry so two seeds agreeing across all of them means
# the mapping is identical.
if ids is None:
sample = range(1, 64)
else:
sample = [int(i) for i in ids if i]
if not sample:
sample = range(1, 64)

def arrangement(seed):
return tuple(palette_color(i, palette, seed) for i in sample)
Expand Down
19 changes: 19 additions & 0 deletions PyReconstruct/modules/backend/view/zarr_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ def selectID(self, pix_x : int, pix_y : int):
return True
return False

def getPresentIds(self):
"""Return the label ids visible on the current section.

These are the unique non-zero ids in the current section's slice of the
label overlay -- i.e. the labels the user actually sees colored (the
whole crop is colored, not just ``selected_ids``). Used to tie the
shuffle-colors guarantee to what is on screen. Returns an empty list
when this is not a label overlay or the current section falls outside
the overlay's z-range.
"""
if not self.is_labels:
return []
bz = self.zarr.shape[0]
z = round(self.section.n - self.zarr_s)
if not 0 <= z < bz:
return []
present = np.unique(self.zarr[z])
return [int(v) for v in present.tolist() if v != 0]

def deselectAll(self):
"""Deselect all the IDs."""
self.selected_ids = []
Expand Down
29 changes: 23 additions & 6 deletions PyReconstruct/modules/backend/volume/export_volumes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Export 3D objects."""

import importlib.util
from pathlib import Path
from typing import List, Union

Expand All @@ -12,6 +13,18 @@
from PyReconstruct.modules.gui.utils import notify


def collada_available() -> bool:
"""Return True when the optional 'pycollada' package is importable.

Collada (.dae) export goes through trimesh's Collada writer, which imports
'pycollada' lazily. That package is NOT bundled by PyInstaller, so a frozen
("packaged") build never has it available. Callers use this to grey out the
.dae menu item up front instead of offering an export that can only fail.
Uses ``find_spec`` so it merely checks importability without importing.
"""
return importlib.util.find_spec("collada") is not None


def export3DObjects(series: Series, obj_names : list, output_dir : str, export_type: str, notify_user: bool = True) -> None:
"""Export 3D objects.

Expand All @@ -26,18 +39,22 @@ def export3DObjects(series: Series, obj_names : list, output_dir : str, export_t

## Collada (.dae) export needs the optional 'pycollada' package (trimesh's
## Collada writer imports it lazily and otherwise raises a bare
## ModuleNotFoundError). Surface the requirement here, before any work, so
## the user gets a clear message instead of an unhandled traceback.
## ModuleNotFoundError). The menu normally disables .dae when it is absent
## (see collada_available), but keep this as a backstop -- surface the
## requirement here, before any work, so the user gets a clear message
## instead of an unhandled traceback.
if export_type == "dae":
try:
import collada # noqa: F401 (provided by the 'pycollada' package)
except ImportError:
if notify_user:
notify(
"Collada (.dae) export requires the 'pycollada' package, "
"which is not installed.\n\n"
"Install it (e.g. 'pip install pycollada') and try again, "
"or choose another export format."
"Collada (.dae) export needs the optional 'pycollada' "
"package, which is not available in this installation.\n\n"
"In a pip/uv environment you can install it (e.g. "
"'pip install pycollada'); packaged (frozen) builds do not "
"include Collada support. Otherwise choose another export "
"format."
)
return

Expand Down
1 change: 1 addition & 0 deletions PyReconstruct/modules/datatypes/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def get_username() -> str:
"deccon_act": "[",
"inccon_act": "]",
"blend_act": "Space",
"toggleztraces_act": "", # checkable "Show z-traces"; no default key
"homeview_act": "Home",
"selectall_act": "Ctrl+A",
"deselect_act": "Ctrl+D",
Expand Down
10 changes: 5 additions & 5 deletions PyReconstruct/modules/gui/dialog/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ def getStaticShortcuts(w : QWidget) -> list[QKeySequence]:
("flicker_act", "Switch between current and last viewed section"),
None,
"View",
("focus_act", "Toggle focus mode"),
("hideall_act", "Toggle hide all traces (regardless of trace's hide status)"),
("showall_act", "Toggle show all traces (regardless of trace's hide status)"),
("hideimage_act", "Toggle hide images"),
("focus_act", "Focus mode"),
("hideall_act", "Hide trace layer"),
("showall_act", "Show all traces (ignore hidden)"),
("hideimage_act", "Hide image"),
("decbr_act", "Decrease brightness"),
("incbr_act", "Increase brightness"),
("deccon_act", "Decrease contrast"),
("inccon_act", "Increase contrast"),
("blend_act", "Blend current and last viwed section"),
("blend_act", "Section blend"),
("homeview_act", "Set view to image"),
None,
"Field Interactions",
Expand Down
60 changes: 54 additions & 6 deletions PyReconstruct/modules/gui/main/context_menu_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@
from PyReconstruct.modules.gui.utils import getUserColsMenu, getAlignmentsMenu


def collada_menu_label(available=None):
"""Label for the Collada (.dae) export item, flagged when unavailable.

Collada export needs the optional 'pycollada' package, which frozen
(PyInstaller) builds never bundle. When it is unavailable the label gains a
"(not installed)" suffix so the (disabled) item explains itself. Pass
``available`` explicitly to keep the string logic pure/testable; when None
the live availability is queried (import kept local so this menu module
doesn't pull in the heavy export backend at import time).

Params:
available (bool | None): whether pycollada is importable; None to
query it live
Returns:
(str): the menu label
"""
if available is None:
from PyReconstruct.modules.backend.volume.export_volumes import (
collada_available,
)
available = collada_available()
return "Collada (.dae)" if available else "Collada (.dae) (not installed)"


def disable_unavailable_export_formats(widget):
"""Grey out 3D-export formats whose optional dependency is missing.

Currently only Collada (.dae), which needs 'pycollada' (absent from frozen
builds). Disabling the menu item up front means a packaged user is never
offered an export that can only fail; the runtime guard in
``export3DObjects`` remains a backstop. Call once, right after the object
menu is populated onto ``widget`` (both the field context menu and the
object-list table build it). No-op when the action isn't on this widget or
Collada IS available.
"""
from PyReconstruct.modules.backend.volume.export_volumes import (
collada_available,
)
act = getattr(widget, "export3D_dae_act", None)
if act is not None and not collada_available():
act.setEnabled(False)


def edit_selected_label(active):
"""Resolve the Q8 top-level edit action's (label, enabled) for a selection.

Expand Down Expand Up @@ -146,7 +189,11 @@ def get_context_menu_list_obj(self):
("editobjradius_act", "Edit radius...", "", self.editRadius),
("editobjshape_act", "Edit shape...", "", self.editShape),
None,
("smoothtraces_act", "Smooth traces", "", self.smoothObject),
# Distinct attr_name from the field Trace submenu's
# "smoothtraces_act": both menus are populated onto the same
# widget, so a shared name meant one silently shadowed the
# other (same class of bug as the old export3D_act).
("smoothobj_act", "Smooth traces", "", self.smoothObject),
("splitobj_act", "Split into separate objects", "", self.splitObject),
None,
("removealltags_act", "Remove all tags", "", self.removeAllTags),
Expand Down Expand Up @@ -177,15 +224,16 @@ def get_context_menu_list_obj(self):
[
# unique attr_names per format (previously all "export3D_act",
# so four of five silently shadowed the last on the widget).
# Collada requires the optional 'pycollada' package; the
# export handler surfaces that requirement gracefully
# (export_volumes.export3DObjects), so the dependency note
# is no longer crammed into the label.
# Collada needs the optional 'pycollada' package (never
# bundled in frozen builds): its label is flagged when
# unavailable and the item is disabled after build (see
# disable_unavailable_export_formats); export3DObjects
# keeps a runtime guard as a backstop.
("export3D_obj_act", "Wavefront (.obj)", "", lambda : self.exportAs3D("obj")),
("export3D_off_act", "Object File Format (.off)", "", lambda : self.exportAs3D("off")),
("export3D_ply_act", "Stanford PLY (.ply)", "", lambda : self.exportAs3D("ply")),
("export3D_stl_act", "STL (.stl)", "", lambda : self.exportAs3D("stl")),
("export3D_dae_act", "Collada (.dae)", "", lambda : self.exportAs3D("dae")),
("export3D_dae_act", collada_menu_label(), "", lambda : self.exportAs3D("dae")),
]

},
Expand Down
4 changes: 2 additions & 2 deletions PyReconstruct/modules/gui/main/field_widget_2_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ def editTraceShape(self, traces : list):
def getZtraceMenu(self):
"""Get the context menu list for interacting with ztraces."""
context_menu_list = [
("editztracce_act", "Edit Z-trace attributes...", "", self.editZtraceAttributes),
("editztracce_act", "Edit z-trace attributes...", "", self.editZtraceAttributes),
("smoothztrace_act", "Smooth", "", self.smoothZtrace),
None,
{
Expand All @@ -1207,7 +1207,7 @@ def getZtraceMenu(self):
},
("setztracealignment_act", "Edit alignment...", "", self.editZtraceAlignment),
None,
("deleteztrace_act", "Delete Z-traces", "", self.deleteZtrace)
("deleteztrace_act", "Delete z-traces", "", self.deleteZtrace)
]
return context_menu_list

Expand Down
6 changes: 5 additions & 1 deletion PyReconstruct/modules/gui/main/main_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,8 @@

from .menubar import return_menubar

from .context_menu_list import get_field_menu_list, edit_selected_label
from .context_menu_list import (
get_field_menu_list,
edit_selected_label,
disable_unavailable_export_formats,
)
34 changes: 33 additions & 1 deletion PyReconstruct/modules/gui/main/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,26 @@ def createMenuBar(self):
## Populate menu bar with menus and options
populateMenuBar(self, self.menubar, menu)

## Seed menubar checkables that have no aboutToShow resync hook from the
## live option, so they are correct on first show. checkActions keeps
## them synced thereafter (see its View-toggle block). NOTE: the menubar
## has no per-open resync, so an external change to show_ztraces (e.g.
## the all-options dialog) is only reflected on the next checkActions
## pass (field interactions) or menubar rebuild -- an accepted
## limitation for a rarely-out-of-band toggle.
self.toggleztraces_act.setChecked(bool(self.series.getOption("show_ztraces")))

def createContextMenus(self):
"""Create right-click menus used in the field."""
## Create user columns options
field_menu_list = get_field_menu_list(self)
self.field_menu = QMenu(self)
populateMenu(self, self.field_menu, field_menu_list)

## Grey out export formats whose optional dependency is missing
## (e.g. Collada/.dae without 'pycollada', as in frozen builds).
disable_unavailable_export_formats(self)

## Organize actions
self.trace_actions = [
self.tracemenu,
Expand Down Expand Up @@ -296,6 +309,17 @@ def checkActions(self, context_menu=False, clicked_trace=None, clicked_label=Non
self.hideimage_act.setChecked(self.field.hide_image)
self.blend_act.setChecked(self.field.blend_sections)

## Focus mode needs a selection to turn ON (it focuses the selected
## object); disabling the checkbox when nothing is selected avoids the
## "Please select at least one trace" error from a menu click. Stays
## enabled while already in focus mode so it can always be turned off.
self.focus_act.setEnabled(bool(self.field.focus_mode) or bool(selected_traces))

## Menubar "Show z-traces" checkbox: same live-state resync as the
## field View toggles above (setChecked emits `toggled`, not
## `triggered`, so it never re-fires the handler).
self.toggleztraces_act.setChecked(bool(self.series.getOption("show_ztraces")))

## Group visibility
for group, viz in self.series.groups_visibility.items():
try:
Expand Down Expand Up @@ -2468,8 +2492,16 @@ def shuffleAutosegColors(self):

current = self.series.getOption("autoseg_color_seed") or 0
palette = self.series.getOption("autoseg_color_palette") or None
# Enforce the "always reshuffles" guarantee over the labels actually
# visible on this section, not a fixed 1..63 range: with only a few
# labels on screen a new seed could recolor ids the user can't see and
# leave the visible ones unchanged (a no-op click). Fall back to the
# default range when the overlay can't supply present ids.
zarr_layer = self.field.zarr_layer
present_ids = zarr_layer.getPresentIds() if zarr_layer else None
self.series.setOption(
"autoseg_color_seed", next_shuffle_seed(current, palette)
"autoseg_color_seed",
next_shuffle_seed(current, palette, ids=present_ids)
)
self.field.generateView()

Expand Down
6 changes: 5 additions & 1 deletion PyReconstruct/modules/gui/main/menubar.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,11 @@ def return_view_menu(self):
("viewmag_act", "View magnification...", "", self.field.setViewMagnification),
("findview_act", "Set zoom when finding contours...", "", self.setFindZoom),
None,
("toggleztraces_act", "Toggle show Z-traces", "", self.toggleZtraces),
# Checkable, mirroring the live show_ztraces option (see
# MainWindow.checkActions / createMenuBar for the resync). The
# (series, "checkbox") form keeps it configurable-shortcut-capable
# like the field View toggles; its option default is "" (no key).
("toggleztraces_act", "Show z-traces", (self.series, "checkbox"), self.toggleZtraces),
None,
{
"attr_name": "palettemenu",
Expand Down
7 changes: 7 additions & 0 deletions PyReconstruct/modules/gui/table/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@ def getCall(col_name, opt_name):
self.context_menu = QMenu(self)
populateMenu(self, self.context_menu, context_menu_list)

# Grey out export formats whose optional dependency is missing
# (e.g. Collada/.dae without 'pycollada', as in frozen builds).
from PyReconstruct.modules.gui.main.context_menu_list import (
disable_unavailable_export_formats,
)
disable_unavailable_export_formats(self)

def updateTitle(self):
"""Update the title of the table."""
is_regex = tuple(self.re_filters) != (".*",)
Expand Down
2 changes: 1 addition & 1 deletion PyReconstruct/modules/gui/table/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def createMenus(self):
populateMenuBar(self, self.menubar, menubar_list)

# create the right-click menu -- prepend the list-only table ops
# (invert selection + copy row text) whose handlers belong to THIS
# (invert selection + copy trace values) whose handlers belong to THIS
# table, then the shared trace menu built by the field.
context_menu_list = [
("inverttraceselection_act", "Invert selection", "", self.invertSelection),
Expand Down
2 changes: 1 addition & 1 deletion PyReconstruct/modules/gui/table/ztrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def createMenus(self):
populateMenuBar(self, self.menubar, menubar_list)

# create the right-click menu -- prepend the list-only table ops
# (invert selection + copy row text) whose handlers belong to THIS
# (invert selection + copy z-trace values) whose handlers belong to THIS
# table, then the shared z-trace menu (also used in the field).
context_menu_list = [
("invertztraceselection_act", "Invert selection", "", self.invertSelection),
Expand Down
Loading
Loading