Skip to content
Draft
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
2 changes: 2 additions & 0 deletions examples/viewer_lib/logic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
IslandsEffectLogic,
PaintEffectLogic,
PaintEraseEffectLogic,
ScissorsEffectLogic,
SegmentEditLogic,
SegmentEditorLogic,
ThresholdEffectLogic,
Expand All @@ -24,6 +25,7 @@
"MedicalViewerLogic",
"PaintEffectLogic",
"PaintEraseEffectLogic",
"ScissorsEffectLogic",
"SegmentEditLogic",
"SegmentEditorLogic",
"SegmentationAppLogic",
Expand Down
2 changes: 2 additions & 0 deletions examples/viewer_lib/logic/segmentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
PaintEffectLogic,
PaintEraseEffectLogic,
)
from .scissors_effect_logic import ScissorsEffectLogic
from .segment_edit_logic import SegmentEditLogic
from .segment_editor_logic import SegmentEditorLogic
from .threshold_effect_logic import ThresholdEffectLogic
Expand All @@ -16,6 +17,7 @@
"IslandsEffectLogic",
"PaintEffectLogic",
"PaintEraseEffectLogic",
"ScissorsEffectLogic",
"SegmentEditLogic",
"SegmentEditorLogic",
"ThresholdEffectLogic",
Expand Down
42 changes: 42 additions & 0 deletions examples/viewer_lib/logic/segmentation/scissors_effect_logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Generic

from trame_server import Server

from trame_slicer.core import SlicerApp
from trame_slicer.segmentation import (
ScissorsSegmentationSliceCut,
SegmentationEffectScissors,
)

from ...ui import ScissorsEffectState, SegmentEditorUI
from .base_segmentation_logic import BaseEffectLogic, U


class ScissorsEffectLogic(BaseEffectLogic[ScissorsEffectState, U], Generic[U]):
def __init__(self, server: Server, slicer_app: SlicerApp):
super().__init__(server, slicer_app, ScissorsEffectState, SegmentationEffectScissors)
self.bind_changes(
{
self.name.operation: self._on_operation_changed,
self.name.cut_mode: self._on_cut_mode_changed,
self.name.symmetric_distance: self._on_symmetric_distance_changed,
}
)

def set_ui(self, ui: SegmentEditorUI):
pass

def _on_operation_changed(self, _operation):
if not self.is_active():
return
self.effect.set_operation(_operation)

def _on_cut_mode_changed(self, _cut_mode: ScissorsSegmentationSliceCut):
if not self.is_active():
return
self.effect.set_cut_mode(_cut_mode)

def _on_symmetric_distance_changed(self, _symmetric_distance: float):
if not self.is_active():
return
self.effect.set_symmetric_distance(_symmetric_distance)
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .base_segmentation_logic import BaseEffectLogic, BaseSegmentationLogic
from .islands_effect_logic import IslandsEffectLogic
from .paint_erase_effect_logic import EraseEffectLogic, PaintEffectLogic
from .scissors_effect_logic import ScissorsEffectLogic
from .segment_edit_logic import SegmentEditLogic
from .threshold_effect_logic import ThresholdEffectLogic

Expand All @@ -31,6 +32,7 @@ def __init__(self, server: Server, slicer_app: SlicerApp):
ThresholdEffectLogic,
PaintEffectLogic,
EraseEffectLogic,
ScissorsEffectLogic,
]
self._effect_logic: list[BaseEffectLogic] = [logic(server, slicer_app) for logic in effect_logic]
self._edit_segment_logic = SegmentEditLogic(server, slicer_app)
Expand Down
4 changes: 4 additions & 0 deletions examples/viewer_lib/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
IslandsState,
PaintEffectState,
PaintEffectUI,
ScissorsEffectState,
ScissorsEffectUI,
SegmentDisplayState,
SegmentDisplayUI,
SegmentEditorState,
Expand Down Expand Up @@ -54,6 +56,8 @@
"Preset",
"RangeSlider",
"RangeSliderState",
"ScissorsEffectState",
"ScissorsEffectUI",
"SegmentDisplayState",
"SegmentDisplayUI",
"SegmentEditState",
Expand Down
3 changes: 3 additions & 0 deletions examples/viewer_lib/ui/segmentation/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .islands_effect_ui import IslandsEffectUI, IslandsSegmentationMode, IslandsState
from .paint_effect_ui import PaintEffectState, PaintEffectUI
from .scissors_effect_ui import ScissorsEffectState, ScissorsEffectUI
from .segment_display_ui import SegmentDisplayState, SegmentDisplayUI
from .segment_edit_ui import (
SegmentEditState,
Expand All @@ -21,6 +22,8 @@
"IslandsState",
"PaintEffectState",
"PaintEffectUI",
"ScissorsEffectState",
"ScissorsEffectUI",
"SegmentDisplayState",
"SegmentDisplayUI",
"SegmentEditState",
Expand Down
93 changes: 93 additions & 0 deletions examples/viewer_lib/ui/segmentation/scissors_effect_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from dataclasses import dataclass

from trame.widgets import vuetify3 as vuetify
from trame_server.utils.typed_state import TypedState

from trame_slicer.segmentation.segmentation_effect_scissors_widget import (
ScissorsSegmentationOperation,
ScissorsSegmentationSliceCut,
)

from ..flex_container import FlexContainer


@dataclass
class ScissorsEffectState:
operation: ScissorsSegmentationOperation = ScissorsSegmentationOperation.ERASE_INSIDE
cut_mode: ScissorsSegmentationSliceCut = ScissorsSegmentationSliceCut.UNLIMITED
symmetric_distance: float = 0


class ScissorsEffectUI(FlexContainer):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._typed_state = TypedState(self.state, ScissorsEffectState)

self.operations = {
ScissorsSegmentationOperation.ERASE_INSIDE: "Erase Inside",
ScissorsSegmentationOperation.ERASE_OUTSIDE: "Erase Outside",
ScissorsSegmentationOperation.FILL_INSIDE: "Fill Inside",
ScissorsSegmentationOperation.FILL_OUTSIDE: "Fill Outside",
}

self.cut_modes = {
ScissorsSegmentationSliceCut.UNLIMITED: "Unlimited",
ScissorsSegmentationSliceCut.POSITIVE: "Positive",
ScissorsSegmentationSliceCut.NEGATIVE: "Negative",
ScissorsSegmentationSliceCut.SYMMETRIC: "Symmetric",
}

with self:
with vuetify.VRow():
with (
vuetify.VCol(),
vuetify.VRadioGroup(v_model=self._typed_state.name.operation, label="Operation"),
):
temp = self._typed_state.encode(
[
{
"text": text,
"value": value,
}
for value, text in self.operations.items()
]
)
vuetify.VRadio(
v_for=f"operation in {temp}",
label=("operation.text",),
value=("operation.value",),
)

with (
vuetify.VCol(),
vuetify.VRadioGroup(v_model=self._typed_state.name.cut_mode, label="Cut mode"),
):
temp = self._typed_state.encode(
[
{
"text": text,
"value": value,
}
for value, text in self.cut_modes.items()
]
)
vuetify.VRadio(
v_for=f"operation in {temp}",
label=("operation.text",),
value=("operation.value",),
)
with vuetify.VRow():
vuetify.VNumberInput(
v_model=self._typed_state.name.symmetric_distance,
label="Distance (mm)",
disabled=(
f"{self._typed_state.name.cut_mode} !== {self._typed_state.encode(ScissorsSegmentationSliceCut.SYMMETRIC)}",
),
min=0,
max=9999,
step=(0.0001,),
precision=4,
density="comfortable",
control_variant="stacked",
hide_details=True,
)
2 changes: 2 additions & 0 deletions examples/viewer_lib/ui/segmentation/segment_editor_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ..viewer_layout import ViewerLayoutState
from .islands_effect_ui import IslandsEffectUI
from .paint_effect_ui import PaintEffectUI
from .scissors_effect_ui import ScissorsEffectUI
from .segment_display_ui import SegmentDisplayState, SegmentDisplayUI
from .segment_edit_ui import SegmentEditState, SegmentEditUI
from .segment_list import SegmentList, SegmentListMenu, SegmentListState
Expand Down Expand Up @@ -106,6 +107,7 @@ def _build_ui(self):
self._register_effect_ui(SegmentationEffectErase, PaintEffectUI)
self._register_effect_ui(SegmentationEffectThreshold, ThresholdEffectUI)
self._register_effect_ui(SegmentationEffectIslands, IslandsEffectUI)
self._register_effect_ui(SegmentationEffectScissors, ScissorsEffectUI)
VSpacer(v_else=True)
VDivider()
SegmentDisplayUI(
Expand Down
96 changes: 92 additions & 4 deletions tests/test_segmentation_scissors_effect.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import pytest
from undo_stack import UndoStack

from tests.conftest import a_slice_view, a_threed_view
from tests.view_events import ViewEvents
from trame_slicer.segmentation import SegmentationEffectScissors
from trame_slicer.segmentation import (
ScissorsSegmentationOperation,
ScissorsSegmentationSliceCut,
SegmentationEffectScissors,
)


@pytest.fixture
def undo_stack(a_segmentation_editor):
undo_stack = UndoStack()
a_segmentation_editor.set_undo_stack(undo_stack)
return undo_stack


def apply_scissors_effect(view):
Expand All @@ -15,12 +27,31 @@ def apply_scissors_effect(view):
view_events.mouse_release_event()


def labelmap_sum_is_inferior(ref, labelmap):
return labelmap.sum() < ref


def labelmap_sum_is_superior(ref, labelmap):
return labelmap.sum() > ref


@pytest.mark.parametrize("view", [a_threed_view, a_slice_view])
def test_scissors_effect_can_erase_all_segmentations(
@pytest.mark.parametrize(
("operation", "check"),
[
(ScissorsSegmentationOperation.ERASE_INSIDE, labelmap_sum_is_inferior),
(ScissorsSegmentationOperation.ERASE_OUTSIDE, labelmap_sum_is_inferior),
(ScissorsSegmentationOperation.FILL_INSIDE, labelmap_sum_is_superior),
(ScissorsSegmentationOperation.FILL_OUTSIDE, labelmap_sum_is_superior),
],
)
def test_scissors_effect_can_erase_and_fill(
a_segmentation_editor,
a_segmentation_model,
a_volume_node,
view,
operation,
check,
render_interactive,
request,
):
Expand All @@ -34,13 +65,70 @@ def test_scissors_effect_can_erase_all_segmentations(
)

prev_sum = labelmap.sum()
a_segmentation_editor.set_active_effect_type(SegmentationEffectScissors)
effect: SegmentationEffectScissors = a_segmentation_editor.set_active_effect_type(SegmentationEffectScissors)
effect.set_operation(operation)
apply_scissors_effect(view)

labelmap = a_segmentation_editor.get_segment_labelmap(
a_segmentation_editor.get_segment_ids()[0], as_numpy_array=True
)
assert labelmap.sum() < prev_sum
assert check(prev_sum, labelmap)

if render_interactive:
view.interactor().Start()


def test_scissors_effect_cut_modes(
a_segmentation_editor,
a_segmentation_model,
a_volume_node,
a_slice_view,
undo_stack,
render_interactive,
):
a_segmentation_model.SetDisplayVisibility(False)
segmentation_node = a_segmentation_editor.create_segmentation_node_from_model_node(a_segmentation_model)
a_segmentation_editor.set_active_segmentation(segmentation_node, a_volume_node)

labelmap = a_segmentation_editor.get_segment_labelmap(
a_segmentation_editor.get_segment_ids()[0], as_numpy_array=True
)

prev_sum = labelmap.sum()
effect: SegmentationEffectScissors = a_segmentation_editor.set_active_effect_type(SegmentationEffectScissors)
effect.set_operation(ScissorsSegmentationOperation.ERASE_INSIDE)

parameters = [
(ScissorsSegmentationSliceCut.UNLIMITED, None),
(ScissorsSegmentationSliceCut.POSITIVE, None),
(ScissorsSegmentationSliceCut.NEGATIVE, None),
(ScissorsSegmentationSliceCut.SYMMETRIC, 0),
(ScissorsSegmentationSliceCut.SYMMETRIC, 9999),
]

sums = []

for cut_mode, distance in parameters:
effect.set_cut_mode(cut_mode)
if distance is not None:
effect.set_symmetric_distance(distance)

apply_scissors_effect(a_slice_view)
labelmap = a_segmentation_editor.get_segment_labelmap(
a_segmentation_editor.get_segment_ids()[0], as_numpy_array=True
)
sums.append(labelmap.sum())

assert undo_stack.can_undo()
undo_stack.undo()

unlimited_sum, positive_sum, negative_sum, zero_distance_symmetric_sum, max_distance_symmetric_sum = sums

assert unlimited_sum < prev_sum
assert max_distance_symmetric_sum == unlimited_sum
assert positive_sum > unlimited_sum
assert negative_sum > unlimited_sum
assert zero_distance_symmetric_sum > unlimited_sum

if render_interactive:
a_slice_view.interactor().Start()
6 changes: 6 additions & 0 deletions trame_slicer/segmentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from .brush_source import BrushSource
from .paint_effect_parameters import BrushDiameterMode, BrushShape
from .scissors_effect_parameters import (
ScissorsSegmentationOperation,
ScissorsSegmentationSliceCut,
)
from .segment_modifier import ModificationMode, SegmentModifier
from .segment_properties import SegmentProperties
from .segmentation import Segmentation
Expand Down Expand Up @@ -46,6 +50,8 @@
"BrushSource",
"ModificationMode",
"ScissorsPolygonBrush",
"ScissorsSegmentationOperation",
"ScissorsSegmentationSliceCut",
"SegmentModifier",
"SegmentProperties",
"Segmentation",
Expand Down
Loading