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
29 changes: 28 additions & 1 deletion src/morphobricks_preprocessing/channel_answers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ def label_ids(answers: DraftAnswers) -> list[str]:
return sorted(k for k in answers.channels if k not in RGB_IDS)


def all_colour(layouts: Sequence[UnitLayout], units: Mapping[UnitKey, UnitAnswer]) -> bool:
"""Return True when the user declared every unit colour (``rgb``): no label is needed then.

Only a saved answer counts (experiment-v1 section 4.4b): the header's pre-tick, a missing
answer or a folder that cannot be read keeps this False, so the labels stay required.
"""
return bool(layouts) and all(_declared_colour(lay, units.get(lay.key)) for lay in layouts)


def _declared_colour(layout: UnitLayout, answer: UnitAnswer | None) -> bool:
"""Return True for a readable unit with a saved ``rgb`` answer that still fits its image."""
return (
layout.usable
and answer is not None
and answer.image_type == "rgb"
and layout.channels == len(RGB_IDS) == len(answer.channels) # a stale answer never counts
)


def _unit(answers: DraftAnswers, layout: UnitLayout) -> UnitAnswer:
saved = answers.units.get(layout.key)
if saved is not None and len(saved.channels) == layout.channels:
Expand Down Expand Up @@ -124,4 +143,12 @@ def synced(answers: DraftAnswers, layouts: Sequence[UnitLayout]) -> DraftAnswers
return replace(answers, channels=channels)


__all__ = ["label_ids", "synced", "untagged_by", "with_labels", "with_tags", "with_type"]
__all__ = [
"all_colour",
"label_ids",
"synced",
"untagged_by",
"with_labels",
"with_tags",
"with_type",
]
30 changes: 26 additions & 4 deletions src/morphobricks_preprocessing/notebook_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from . import notebook_style as ui
from .axis_review import review_axes
from .channel_answers import all_colour
from .channel_labels import UnitLayout
from .channel_units import header_names, slot_count, unit_layouts
from .draft_answers import DraftAnswers
Expand Down Expand Up @@ -70,6 +71,7 @@ def _build_channels_step(self) -> None:
self.channel_types = ImageTypeColumn(store, self._types_changed)
self.channel_grid = TagGrid(store, self.channel_types, self._channels_changed)
self.channel_labels = LabelsCard(store, self._channel_labels_changed)
self.channel_intro = ui.intro(INTRO) # hidden while the stains are not applicable
self.channels_waiting = widgets.HTML()
self.channels_box = widgets.VBox(
[self.channels_waiting, self.channel_labels.widget, self.channel_grid.widget]
Expand All @@ -82,7 +84,7 @@ def channels_step(self, title: str | None = None, purpose: str | None = None) ->
"Name what you stained, then tag every channel of every unit."
if purpose is None
else purpose,
[ui.intro(INTRO), self.channels_box],
[self.channel_intro, self.channels_box],
)

# Opening and closing -----------------------------------------------------------------
Expand All @@ -92,6 +94,7 @@ def _hide_channels(self, why: str = "") -> None:
self._channel_thumbs = None
self.channel_labels.close()
self.channel_grid.close()
ui.set_visible(self.channel_intro, True)
self.channels_waiting.value = why
self._refresh_masks_screen()

Expand Down Expand Up @@ -136,9 +139,25 @@ def _refresh_channels(self) -> None:
self.channel_labels.open(slots, names, layouts)
self._channel_labels_changed()

@property
def _stains_not_applicable(self) -> bool:
"""True when the user declared every folder colour (RGB): R, G, B need no label."""
answers = self.draft_answers
return (
self._channels_key is not None
and answers is not None
and all_colour(self._channel_layouts, answers.units)
)

def _channel_labels_changed(self) -> None:
"""Open the tag grid for the confirmed labels, or close it while they are being changed."""
if self.channel_labels.confirmed and self._channels_key is not None:
"""Open the tag grid for the confirmed labels, or close it while they are being changed.

When every folder is colour the labels are not applicable and the grid opens without them.
"""
na = self._stains_not_applicable
self.channel_labels.set_not_applicable(na)
ui.set_visible(self.channel_intro, not na) # it says to name the stains first
if (self.channel_labels.confirmed or na) and self._channels_key is not None:
self.channel_grid.open(
self._channel_layouts, self.channel_labels.labels, self._channel_thumbs
)
Expand All @@ -148,7 +167,10 @@ def _channel_labels_changed(self) -> None:

def _types_changed(self) -> None:
"""Redraw the grid after an image type (its rows hold the type cells)."""
self.channel_grid.refresh()
if self._stains_not_applicable != self.channel_labels.not_applicable:
self._channel_labels_changed() # every folder became colour, or no longer is
else:
self.channel_grid.refresh()
self._channels_changed()

def _channels_changed(self) -> None:
Expand Down
62 changes: 42 additions & 20 deletions src/morphobricks_preprocessing/notebook_channels_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,28 @@ def __init__(self, store: AnswerStore, on_change: Callable[[], None]) -> None:
self.confirm.observe(self._toggled)
self.status = widgets.HTML()
self.problem = widgets.HTML()
self.not_applicable = False
self.na_note = widgets.HTML()
self.body: list[widgets.Widget] = [
ui.help_text(
"Name what was stained, once per stain. The number of slots is the largest "
"number of channels found in any image; <b>Add label</b> adds more. Header "
"names are offered only when they are valid labels as they are and not "
"generic. "
"Every channel position of every folder needs a label, so an unused slot "
"needs one too: declare it here (for example <i>unused</i>)."
),
self.note,
self.groups_box,
self.rows_box,
self.add_button,
self.skipped,
self.problem,
self.status,
self.confirm.widget,
]
self.widget = ui.card(
"Which labels did you image?",
[
ui.help_text(
"Name what was stained, once per stain. The number of slots is the largest "
"number of channels found in any image; <b>Add label</b> adds more. Header "
"names are offered only when they are valid labels as they are and not "
"generic. "
"Every channel position of every folder needs a label, so an unused slot "
"needs one too: declare it here (for example <i>unused</i>)."
),
self.note,
self.groups_box,
self.rows_box,
self.add_button,
self.skipped,
self.problem,
self.status,
self.confirm.widget,
],
visible=False,
"Which labels did you image?", [*self.body, self.na_note], visible=False
)

@property
Expand All @@ -137,6 +138,27 @@ def confirmed(self) -> bool:
"""True while the list is confirmed (saved and not being changed)."""
return self.confirm.value

def set_not_applicable(self, on: bool) -> None:
"""Say the card is not applicable (every folder is colour, RGB) and hide its questions."""
self.not_applicable = on
kept = ", ".join(self.labels)
self.na_note.value = (
ui.status(
"ok",
"Not applicable: every folder is colour (RGB).",
[
"R, G and B are the fixed names of a colour image's channels, so no label is "
"needed. If some folder is separate channels, change its image type "
"(step 7) and the labels come back."
+ (f" The labels already declared ({kept}) stay saved, unused." if kept else "")
],
)
if on
else ""
)
for widget in self.body:
ui.set_visible(widget, not on)

def display_names(self) -> dict[str, str | None]:
"""Return the shown-as name of each saved label."""
return {k: v.display_name for k, v in self.store.answers().channels.items()}
Expand Down
9 changes: 8 additions & 1 deletion src/morphobricks_preprocessing/notebook_step_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,22 @@ def _image_type_state(self) -> StepState:
return StepState(done, ", ".join(parts), self._changed(6, done))

def _stains_state(self) -> StepState:
"""Return step 8: the confirmed labels, or not applicable when every folder is colour."""
if self._channels_key is None:
return StepState(False, "after the axes")
if self._stains_not_applicable:
return StepState(
True, "not applicable: every folder is colour (RGB)", note="not applicable"
)
done = self.channel_labels.confirmed
text = ", ".join(self.channel_labels.labels) if done else "not confirmed"
return StepState(done, text, self._changed(7, done))

def _tags_state(self) -> StepState:
"""Return step 9: whether every channel of every folder is tagged."""
if self._channels_key is None or not self.channel_labels.confirmed:
if self._channels_key is None or not (
self.channel_labels.confirmed or self._stains_not_applicable
):
return StepState(False, "after the stains")
layouts = self._channel_layouts
left = remaining(layouts, self.draft_answers.units, self.channel_labels.labels)
Expand Down
5 changes: 2 additions & 3 deletions src/morphobricks_preprocessing/notebook_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from . import notebook_style as ui
from .dataset import CONSISTENCY_CODES
from .notebook_axes_quick import _QuickAxes
from .notebook_channels import INTRO as CHANNELS_INTRO
from .notebook_channels_type import ImageTypeRows
from .notebook_check_save import INTRO as CHECK_INTRO
from .notebook_masks_screen import INTRO as MASKS_INTRO
Expand Down Expand Up @@ -107,7 +106,7 @@ def _step_list(self) -> list[Step]:
("Image type", "Separate channels or a colour (RGB) picture, per folder.",
[self.type_rows.widget]),
("Stains", "Name what you stained.",
[ui.intro(CHANNELS_INTRO), self.channel_labels.widget]),
[self.channel_intro, self.channel_labels.widget]),
("Tag channels", "Say which stain each channel is, looking at its picture.",
[self.channel_grid.widget]),
("Masks", "Say what each mask type is.", [ui.intro(MASKS_INTRO), self.masks_box]),
Expand Down Expand Up @@ -210,7 +209,7 @@ def _wait_for_channels(self) -> str:

def _wait_for_tags(self) -> str:
waiting = self._wait_for_channels()
if waiting or self.channel_labels.confirmed:
if waiting or self.channel_labels.confirmed or self._stains_not_applicable:
return waiting
return ui.status("info", "Waiting for the stains.", ["Confirm the stains in step 8 first."])

Expand Down
Loading
Loading