Skip to content

Commit dcdbea0

Browse files
Jo-ByrThibault-Pelletier
authored andcommitted
feat(segmentation): add logical operators effect
Add logical operators segmentation effect logic and example
1 parent a6a6de3 commit dcdbea0

16 files changed

Lines changed: 427 additions & 14 deletions

examples/viewer_lib/logic/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .segmentation import (
66
EraseEffectLogic,
77
IslandsEffectLogic,
8+
LogicalOperatorsEffectLogic,
89
PaintEffectLogic,
910
PaintEraseEffectLogic,
1011
ScissorsEffectLogic,
@@ -22,6 +23,7 @@
2223
"EraseEffectLogic",
2324
"IslandsEffectLogic",
2425
"LoadVolumeLogic",
26+
"LogicalOperatorsEffectLogic",
2527
"MarkupsButtonLogic",
2628
"MedicalViewerLogic",
2729
"PaintEffectLogic",

examples/viewer_lib/logic/segmentation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .base_segmentation_logic import BaseEffectLogic, BaseSegmentationLogic
22
from .brush_effect_logic import BrushEffectLogic
33
from .islands_effect_logic import IslandsEffectLogic
4+
from .logical_operators_effect_logic import LogicalOperatorsEffectLogic
45
from .paint_erase_effect_logic import (
56
EraseEffectLogic,
67
PaintEffectLogic,
@@ -18,6 +19,7 @@
1819
"BrushEffectLogic",
1920
"EraseEffectLogic",
2021
"IslandsEffectLogic",
22+
"LogicalOperatorsEffectLogic",
2123
"PaintEffectLogic",
2224
"PaintEraseEffectLogic",
2325
"ScissorsEffectLogic",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from trame_server import Server
2+
3+
from trame_slicer.core import SlicerApp
4+
from trame_slicer.segmentation import SegmentationEffectLogicalOperators
5+
6+
from ...ui import (
7+
LogicalOperatorsEffectUI,
8+
LogicalOperatorsSegmentationMode,
9+
LogicalOperatorsState,
10+
SegmentEditorUI,
11+
SegmentState,
12+
)
13+
from .base_segmentation_logic import BaseEffectLogic
14+
15+
16+
class LogicalOperatorsEffectLogic(BaseEffectLogic[LogicalOperatorsState, SegmentationEffectLogicalOperators]):
17+
def __init__(self, server: Server, slicer_app: SlicerApp):
18+
super().__init__(server, slicer_app, LogicalOperatorsState, SegmentationEffectLogicalOperators)
19+
self.segmentation_editor.active_segment_id_changed.connect(self._update_available_segments)
20+
21+
def set_ui(self, ui: SegmentEditorUI):
22+
self.set_effect_ui(ui.get_effect_ui(SegmentationEffectLogicalOperators))
23+
24+
def set_effect_ui(self, logical_operators_ui: LogicalOperatorsEffectUI):
25+
logical_operators_ui.apply_clicked.connect(self._on_apply_clicked)
26+
27+
def _update_available_segments(self, *_args, **_kwargs):
28+
self.data.available_segments = [
29+
SegmentState(
30+
name=segment_properties.name,
31+
color=segment_properties.color_hex,
32+
segment_id=segment_id,
33+
is_visible=self.segmentation_editor.get_segment_visibility(segment_id),
34+
)
35+
for segment_id, segment_properties in self.segmentation_editor.get_all_segment_properties().items()
36+
]
37+
if self.data.reference_segment_id not in [segment.segment_id for segment in self.data.available_segments]:
38+
self.data.reference_segment_id = None
39+
40+
def _on_apply_clicked(self):
41+
if not self.is_active():
42+
return
43+
44+
match self.data.logical_operator:
45+
case LogicalOperatorsSegmentationMode.ADD:
46+
self.effect.add(self.data.reference_segment_id)
47+
case LogicalOperatorsSegmentationMode.COPY:
48+
self.effect.copy(self.data.reference_segment_id)
49+
case LogicalOperatorsSegmentationMode.SUBTRACT:
50+
self.effect.subtract(self.data.reference_segment_id)
51+
case LogicalOperatorsSegmentationMode.INTERSECT:
52+
self.effect.intersect(self.data.reference_segment_id)
53+
case LogicalOperatorsSegmentationMode.INVERT:
54+
self.effect.invert()
55+
case LogicalOperatorsSegmentationMode.CLEAR:
56+
self.effect.clear()
57+
case LogicalOperatorsSegmentationMode.FILL:
58+
self.effect.fill()

examples/viewer_lib/logic/segmentation/segment_editor_logic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from .base_segmentation_logic import BaseEffectLogic, BaseSegmentationLogic
1919
from .islands_effect_logic import IslandsEffectLogic
20+
from .logical_operators_effect_logic import LogicalOperatorsEffectLogic
2021
from .paint_erase_effect_logic import EraseEffectLogic, PaintEffectLogic
2122
from .scissors_effect_logic import ScissorsEffectLogic
2223
from .segment_edit_logic import SegmentEditLogic
@@ -30,6 +31,7 @@ def __init__(self, server: Server, slicer_app: SlicerApp):
3031

3132
effect_logic = [
3233
IslandsEffectLogic,
34+
LogicalOperatorsEffectLogic,
3335
ThresholdEffectLogic,
3436
PaintEffectLogic,
3537
EraseEffectLogic,

examples/viewer_lib/ui/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
BrushParametersUI,
1111
IslandsEffectUI,
1212
IslandsState,
13+
LogicalOperatorsEffectUI,
14+
LogicalOperatorsSegmentationMode,
15+
LogicalOperatorsState,
1316
PaintEffectState,
1417
PaintEffectUI,
1518
ScissorsEffectState,
@@ -48,6 +51,9 @@
4851
"LayoutButtonState",
4952
"LoadVolumeState",
5053
"LoadVolumeUI",
54+
"LogicalOperatorsEffectUI",
55+
"LogicalOperatorsSegmentationMode",
56+
"LogicalOperatorsState",
5157
"MarkupsButton",
5258
"MedicalViewerUI",
5359
"MprInteractionButton",

examples/viewer_lib/ui/segmentation/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from .brush_parameters_ui import BrushParametersState, BrushParametersUI
22
from .islands_effect_ui import IslandsEffectUI, IslandsState
3+
from .logical_operators_effect_ui import (
4+
LogicalOperatorsEffectUI,
5+
LogicalOperatorsSegmentationMode,
6+
LogicalOperatorsState,
7+
)
38
from .paint_effect_ui import PaintEffectState, PaintEffectUI
49
from .scissors_effect_ui import ScissorsEffectState, ScissorsEffectUI
510
from .segment_display_ui import SegmentDisplayState, SegmentDisplayUI
@@ -27,6 +32,9 @@
2732
"BrushParametersUI",
2833
"IslandsEffectUI",
2934
"IslandsState",
35+
"LogicalOperatorsEffectUI",
36+
"LogicalOperatorsSegmentationMode",
37+
"LogicalOperatorsState",
3038
"PaintEffectState",
3139
"PaintEffectUI",
3240
"ScissorsEffectState",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from dataclasses import dataclass, field
2+
from enum import Enum, auto
3+
4+
from trame_server.utils.typed_state import TypedState
5+
from trame_vuetify.widgets.vuetify3 import VBtn, VSelect
6+
from undo_stack import Signal
7+
8+
from ..flex_container import FlexContainer
9+
from .segment_state import SegmentState
10+
11+
12+
class LogicalOperatorsSegmentationMode(Enum):
13+
COPY = auto()
14+
ADD = auto()
15+
SUBTRACT = auto()
16+
INTERSECT = auto()
17+
INVERT = auto()
18+
CLEAR = auto()
19+
FILL = auto()
20+
21+
22+
@dataclass
23+
class LogicalOperatorsState:
24+
logical_operator: LogicalOperatorsSegmentationMode = LogicalOperatorsSegmentationMode.COPY
25+
reference_segment_id: str | None = None
26+
available_segments: list[SegmentState] = field(default_factory=list)
27+
28+
29+
class LogicalOperatorsEffectUI(FlexContainer):
30+
apply_clicked = Signal()
31+
32+
def __init__(self, **kwargs):
33+
super().__init__(**kwargs)
34+
self._typed_state = TypedState(self.state, LogicalOperatorsState)
35+
36+
modes_with_reference = [
37+
LogicalOperatorsSegmentationMode.COPY,
38+
LogicalOperatorsSegmentationMode.ADD,
39+
LogicalOperatorsSegmentationMode.SUBTRACT,
40+
LogicalOperatorsSegmentationMode.INTERSECT,
41+
]
42+
43+
with self:
44+
VSelect(
45+
v_model=self._typed_state.name.logical_operator,
46+
label="Logical Operator",
47+
density="comfortable",
48+
hide_details=True,
49+
item_title="title",
50+
item_value="value",
51+
items=(
52+
[
53+
{"title": mode.name.capitalize(), "value": self._typed_state.encode(mode)}
54+
for mode in LogicalOperatorsSegmentationMode
55+
],
56+
),
57+
)
58+
is_mode_with_reference_state_str = f"{[self._typed_state.encode(mode) for mode in modes_with_reference]}.includes({self._typed_state.name.logical_operator})"
59+
VSelect(
60+
v_model=self._typed_state.name.reference_segment_id,
61+
label="Reference segment",
62+
density="comfortable",
63+
hide_details=True,
64+
item_title="title",
65+
item_value="value",
66+
items=(
67+
f"{self._typed_state.name.available_segments}.map((segment) => ({{ 'title': segment.name, 'value': segment.segment_id }}))",
68+
),
69+
v_if=(is_mode_with_reference_state_str,),
70+
)
71+
available_segment_ids = f"{self._typed_state.name.available_segments}.map((segment) => segment.segment_id)"
72+
VBtn(
73+
"Apply",
74+
click=self.apply_clicked,
75+
disabled=(
76+
f"{is_mode_with_reference_state_str} && ({self._typed_state.name.reference_segment_id} === null || !{available_segment_ids}.includes({self._typed_state.name.reference_segment_id}))",
77+
),
78+
)

examples/viewer_lib/ui/segmentation/segment_editor_ui.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
SegmentationEffectDraw,
2121
SegmentationEffectErase,
2222
SegmentationEffectIslands,
23+
SegmentationEffectLogicalOperators,
2324
SegmentationEffectNoTool,
2425
SegmentationEffectPaint,
2526
SegmentationEffectScissors,
@@ -31,6 +32,7 @@
3132
from ..flex_container import FlexContainer
3233
from ..viewer_layout import ViewerLayoutState
3334
from .islands_effect_ui import IslandsEffectUI
35+
from .logical_operators_effect_ui import LogicalOperatorsEffectUI
3436
from .paint_effect_ui import PaintEffectUI
3537
from .scissors_effect_ui import ScissorsEffectUI
3638
from .segment_display_ui import SegmentDisplayState, SegmentDisplayUI
@@ -111,6 +113,7 @@ def _build_ui(self):
111113
with VCardText(classes="align-center", style="flex: 1; min-height: 0; overflow-y: auto;"):
112114
self._register_effect_ui(SegmentationEffectPaint, PaintEffectUI)
113115
self._register_effect_ui(SegmentationEffectErase, PaintEffectUI)
116+
self._register_effect_ui(SegmentationEffectLogicalOperators, LogicalOperatorsEffectUI)
114117
self._register_effect_ui(SegmentationEffectThreshold, ThresholdEffectUI)
115118
self._register_effect_ui(SegmentationEffectIslands, IslandsEffectUI)
116119
self._register_effect_ui(SegmentationEffectScissors, ScissorsEffectUI)
@@ -155,6 +158,11 @@ def build_effect_buttons(self, all: bool = True, **kwargs):
155158
**kwargs,
156159
)
157160
if all:
161+
self._create_effect_button(
162+
"Logical Operators",
163+
"mdi-vector-intersection",
164+
SegmentationEffectLogicalOperators,
165+
)
158166
self._create_effect_button(
159167
"Threshold",
160168
"mdi-auto-fix",

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ def a_segmentation_nifti_file_path(a_data_folder) -> Path:
144144
return a_data_folder.joinpath("segmentation.nii.gz")
145145

146146

147+
@pytest.fixture
148+
def a_segmentation_overlap_file_path(a_data_folder) -> Path:
149+
return a_data_folder.joinpath("segmentation_overlap.nrrd")
150+
151+
147152
@pytest.fixture
148153
def a_model_node(a_slicer_app, a_model_file_path):
149154
return load_model_node(

tests/data

0 commit comments

Comments
 (0)