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
8 changes: 4 additions & 4 deletions PyReconstruct/modules/backend/autoseg/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from PyReconstruct.modules.backend.threading import ThreadPoolProgBar
from PyReconstruct.modules.calc import reducePoints

from .palette import DEFAULT_AUTOSEG_PALETTE, palette_color
from .palette import AUTOSEG_TRACE_PREFIX, DEFAULT_AUTOSEG_PALETTE, palette_color


dt = None
Expand Down Expand Up @@ -761,7 +761,7 @@ def importSection(data_zg, group, snum, series, ids=None):
## Add exteriors as traces
for ext in exteriors:

trace_name = f"autoseg_{id}"
trace_name = f"{AUTOSEG_TRACE_PREFIX}{id}"
trace_color = palette_color(id, palette, color_seed)

trace = Trace(name=trace_name, color=trace_color)
Expand All @@ -771,8 +771,8 @@ def importSection(data_zg, group, snum, series, ids=None):
section.addTrace(trace)

## Add trace to group
series.object_groups.add(f"seg_{dt}", f"autoseg_{id}")
series.object_groups.add(f"seg_{group}", f"autoseg_{id}")
series.object_groups.add(f"seg_{dt}", f"{AUTOSEG_TRACE_PREFIX}{id}")
series.object_groups.add(f"seg_{group}", f"{AUTOSEG_TRACE_PREFIX}{id}")

section.save()

Expand Down
60 changes: 60 additions & 0 deletions PyReconstruct/modules/backend/autoseg/palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,66 @@
]


# Prefix autoseg import gives every trace/object it creates. The object name is
# this prefix followed by the (decimal) segmentation label id, e.g. "autoseg_42".
# Kept here as the single source of truth so both the import (which builds the
# name) and the recolor (which recovers the id from the name) stay in sync.
AUTOSEG_TRACE_PREFIX = "autoseg_"


def label_id_from_name(name: str):
"""Recover the segmentation label id baked into an autoseg object name.

Import names each object ``f"{AUTOSEG_TRACE_PREFIX}{id}"`` (e.g.
"autoseg_42"), so the original label id is the trailing decimal integer.
Returns that int when the name matches exactly, else None (the caller then
falls back to a stable name hash).

Params:
name (str): the object name
Returns:
(int | None): the label id, or None when the name is not an
unmodified autoseg import name
"""
if not isinstance(name, str) or not name.startswith(AUTOSEG_TRACE_PREFIX):
return None
suffix = name[len(AUTOSEG_TRACE_PREFIX):]
# Only a bare run of digits is a genuine label id. This deliberately
# rejects renamed/derived names ("autoseg_42_dendrite", "autoseg_") so they
# take the stable-hash fallback instead of silently colliding on int(42).
if not suffix.isdigit():
return None
return int(suffix)


def palette_color_for_name(name: str, palette=None, seed: int = 0) -> tuple:
"""Return the palette color an object *named* ``name`` should get.

Used to (re)apply the current palette to already-imported objects. When the
name still encodes its autoseg label id (see ``label_id_from_name``) the
result is byte-identical to what import would have assigned for that id --
so re-running with the same palette/seed is a no-op. When the name does not
parse (renamed, or never an autoseg object), the color is derived from a
stable hash of the name string: ``zlib.crc32`` is used deliberately because
it is deterministic across processes and Python runs (unlike the builtin
``hash``, which is salted by PYTHONHASHSEED), so the same name always yields
the same color everywhere.

Params:
name (str): the object name
palette: list of (R, G, B) entries; falls back to the shipped
default when None or empty
seed (int): re-roll seed (same meaning as palette_color)
Returns:
(tuple): an (R, G, B) integer triple from the palette
"""
label_id = label_id_from_name(name)
if label_id is None:
import zlib
label_id = zlib.crc32(str(name).encode("utf-8"))
return palette_color(label_id, palette, seed)


def _mix(value: int, seed: int) -> int:
"""Deterministically scramble an integer (splitmix64/Murmur3 finalizer).

Expand Down
65 changes: 64 additions & 1 deletion PyReconstruct/modules/datatypes/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,70 @@ def removeAllTraceTags(self, obj_names : list, series_states=None, log_event=Tru
self.addLog(name, None, "Remove all trace tags")

self.modified = True


def reapplyAutosegColors(self, obj_names : list, series_states=None, log_event=True):
"""Recolor objects using the CURRENT autoseg palette and seed.

Lets a user reapply today's palette (colorblind-safe default or a custom
one) to objects imported before the palette existed -- their old colors
were baked in at import time and never update on their own.

Each object's color is resolved from its name: an unmodified autoseg
name ("autoseg_<id>") recovers its label id and gets exactly the color a
fresh import would assign; any other name falls back to a stable hash of
the name (see ``palette_color_for_name``). The color is written through
the same per-section ``editTraceAttributes`` path every other bulk
attribute edit uses, so it is one undoable operation and the field/lists
refresh correctly.

Params:
obj_names (list): the names of objects to recolor
series_states (dict): optional dict for GUI undo states
log_event (bool): True if the event should be logged
"""
from PyReconstruct.modules.backend.autoseg.palette import (
DEFAULT_AUTOSEG_PALETTE,
palette_color_for_name,
)

## Resolve the palette + seed once (empty override -> curated default),
## then the per-object color once -- these do not vary by section.
palette = self.getOption("autoseg_color_palette") or DEFAULT_AUTOSEG_PALETTE
color_seed = self.getOption("autoseg_color_seed") or 0
color_map = {
name: palette_color_for_name(name, palette, color_seed)
for name in obj_names
}

## Touch only the sections the selected objects appear on.
for snum, section in self.enumerateSections(
message="Reapplying autoseg colors...",
series_states=series_states,
section_numbers=self.getObjectSections(obj_names)
):
modified = False
for obj_name in obj_names:
if obj_name in section.contours:
traces = section.contours[obj_name].getTraces()
if traces:
section.editTraceAttributes(
traces,
name=None,
color=color_map[obj_name],
tags=None,
mode=None,
log_event=False
)
modified = True
if modified:
section.save()

if log_event:
for name in obj_names:
self.addLog(name, None, "Reapply autoseg colors")

self.modified = True

def hideObjects(self, obj_names : list, hide=True, series_states=None, log_event=True):
"""Hide all traces of a set of objects throughout the series.

Expand Down
5 changes: 5 additions & 0 deletions PyReconstruct/modules/gui/main/context_menu_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ def get_context_menu_list_obj(self):
None,
("setobjalignment_act", "Edit alignment...", "", self.editAlignment),
None,
# Reapply the current autoseg palette (colorblind-safe default
# or a custom one) to objects imported before the palette
# existed, whose old colors were baked in at import time.
("reapplyautosegcolors_act", "Reapply autoseg colors...", "", self.reapplyAutosegColors),
None,
# Lock/Unlock lives here as its single home (it is a stored
# object attribute); do NOT re-add it to another submenu.
("lockobj_act", "Lock", "", self.lockObjects),
Expand Down
29 changes: 28 additions & 1 deletion PyReconstruct/modules/gui/main/field_widget_3_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
TextWidget,
)
from PyReconstruct.modules.gui.utils import (
notify
notify,
notifyConfirm,
)
from PyReconstruct.modules.gui.table import (
HistoryTableWidget,
Expand Down Expand Up @@ -166,6 +167,32 @@ def editAttributes(self, obj_names : list):

return True

@object_function(update_objects=True, reload_field=True)
def reapplyAutosegColors(self, obj_names : list):
"""Recolor selected objects with the current autoseg palette + seed.

Confirms first because it discards the objects' existing colors. Locked
objects are blocked by the object_function wrapper (update_objects=True),
exactly as any other bulk attribute edit; a single series undo restores
every prior color.
"""
n = len(obj_names)
s = "s" if n != 1 else ""
confirmed = notifyConfirm(
f"Recolor {n} selected object{s} using the current autoseg palette "
"and seed?\n\n"
"This replaces the objects' existing colors. You can undo it.",
yn=True,
)
if not confirmed:
return False

self.series.reapplyAutosegColors(
obj_names,
series_states=self.series_states,
)
return True

@object_function(update_objects=True, reload_field=True)
def smoothObject(self, obj_names: list):
"""Smooth object traces."""
Expand Down
Loading
Loading