Skip to content

Commit f2c457c

Browse files
committed
feat(segmentation): add draw segmentation effect
1 parent 7c77aad commit f2c457c

9 files changed

Lines changed: 437 additions & 279 deletions

examples/viewer_lib/ui/segmentation/segment_editor_ui.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from trame_slicer.segmentation import (
1919
SegmentationEffect,
20+
SegmentationEffectDraw,
2021
SegmentationEffectErase,
2122
SegmentationEffectIslands,
2223
SegmentationEffectNoTool,
@@ -138,6 +139,12 @@ def build_effect_buttons(self, all: bool = True, **kwargs):
138139
SegmentationEffectScissors,
139140
**kwargs,
140141
)
142+
self._create_effect_button(
143+
"Draw",
144+
"mdi-pencil",
145+
SegmentationEffectDraw,
146+
**kwargs,
147+
)
141148
if all:
142149
self._create_effect_button(
143150
"Threshold",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
from tests.conftest import a_slice_view, a_threed_view
4+
from tests.view_events import MouseButton, ViewEvents
5+
from trame_slicer.segmentation import SegmentationEffectDraw
6+
7+
8+
def apply_draw_effect(view):
9+
view_events = ViewEvents(view)
10+
center_x, center_y = view_events.view_center()
11+
view_events.mouse_move_to(center_x, center_y)
12+
view_events.mouse_press_event()
13+
view_events.mouse_release_event()
14+
view_events.mouse_move_to(0, center_y)
15+
view_events.mouse_press_event()
16+
view_events.mouse_release_event()
17+
view_events.mouse_move_to(0, 0)
18+
view_events.mouse_press_event()
19+
view_events.mouse_release_event()
20+
view_events.mouse_press_event(MouseButton.Right)
21+
22+
23+
@pytest.mark.parametrize("view", [a_slice_view, a_threed_view])
24+
def test_draw_effect_adds_segmentation_to_selected_segment(
25+
a_slicer_app,
26+
a_segmentation_editor,
27+
a_volume_node,
28+
view,
29+
request,
30+
render_interactive,
31+
):
32+
view = request.getfixturevalue(view.__name__)
33+
a_slicer_app.display_manager.show_volume(a_volume_node, vr_preset="MR-Default")
34+
35+
segmentation_node = a_segmentation_editor.create_empty_segmentation_node()
36+
a_segmentation_editor.set_active_segmentation(segmentation_node, a_volume_node)
37+
segment_id = a_segmentation_editor.add_empty_segment()
38+
a_segmentation_editor.set_active_segment_id(segment_id)
39+
a_segmentation_editor.set_active_effect_type(SegmentationEffectDraw)
40+
apply_draw_effect(view)
41+
array = a_segmentation_editor.get_segment_labelmap(segment_id, as_numpy_array=True)
42+
assert array.sum() > 0
43+
44+
if render_interactive:
45+
a_segmentation_editor.show_3d(True)
46+
view.interactor().Start()

trame_slicer/core/segmentation_editor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Segmentation,
3232
SegmentationDisplay,
3333
SegmentationEffect,
34+
SegmentationEffectDraw,
3435
SegmentationEffectErase,
3536
SegmentationEffectIslands,
3637
SegmentationEffectNoTool,
@@ -54,6 +55,7 @@ class SegmentationEditor(SignalContainer):
5455
"""
5556

5657
builtin_effects: ClassVar[list[type[SegmentationEffect]]] = [
58+
SegmentationEffectDraw,
5759
SegmentationEffectErase,
5860
SegmentationEffectIslands,
5961
SegmentationEffectNoTool,

trame_slicer/segmentation/__init__.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
from .brush_source import BrushSource
44
from .paint_effect_parameters import BrushDiameterMode, BrushShape
5+
from .polygon_brush import PolygonBrush
56
from .segment_modifier import ModificationMode, SegmentModifier
67
from .segment_properties import SegmentProperties
78
from .segmentation import Segmentation
89
from .segmentation_display import SegmentationDisplay, SegmentationOpacityEnum
910
from .segmentation_effect import SegmentationEffect
11+
from .segmentation_effect_draw import SegmentationEffectDraw
12+
from .segmentation_effect_draw_widget import SegmentationDrawPipeline
1013
from .segmentation_effect_islands import SegmentationEffectIslands
1114
from .segmentation_effect_no_tool import SegmentationEffectNoTool
1215
from .segmentation_effect_paint_erase import (
@@ -16,11 +19,7 @@
1619
)
1720
from .segmentation_effect_pipeline import SegmentationEffectPipeline
1821
from .segmentation_effect_scissors import SegmentationEffectScissors
19-
from .segmentation_effect_scissors_widget import (
20-
ScissorsPolygonBrush,
21-
SegmentationScissorsPipeline,
22-
SegmentationScissorsWidget,
23-
)
22+
from .segmentation_effect_scissors_widget import SegmentationScissorsPipeline
2423
from .segmentation_effect_threshold import (
2524
AutoThresholdMethod,
2625
AutoThresholdMode,
@@ -37,6 +36,10 @@
3736
SegmentationPaintWidget2D,
3837
SegmentationPaintWidget3D,
3938
)
39+
from .segmentation_polygon_widget import (
40+
SegmentationPolygonPipeline,
41+
SegmentationPolygonWidget,
42+
)
4043

4144
__all__ = [
4245
"AutoThresholdMethod",
@@ -45,12 +48,14 @@
4548
"BrushShape",
4649
"BrushSource",
4750
"ModificationMode",
48-
"ScissorsPolygonBrush",
51+
"PolygonBrush",
4952
"SegmentModifier",
5053
"SegmentProperties",
5154
"Segmentation",
5255
"SegmentationDisplay",
56+
"SegmentationDrawPipeline",
5357
"SegmentationEffect",
58+
"SegmentationEffectDraw",
5459
"SegmentationEffectErase",
5560
"SegmentationEffectIslands",
5661
"SegmentationEffectNoTool",
@@ -65,8 +70,9 @@
6570
"SegmentationPaintWidget",
6671
"SegmentationPaintWidget2D",
6772
"SegmentationPaintWidget3D",
73+
"SegmentationPolygonPipeline",
74+
"SegmentationPolygonWidget",
6875
"SegmentationScissorsPipeline",
69-
"SegmentationScissorsWidget",
7076
"SegmentationThresholdPipeline2D",
7177
"ThresholdParameters",
7278
]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from vtkmodules.vtkCommonCore import vtkPoints
2+
from vtkmodules.vtkCommonDataModel import vtkCellArray, vtkPolyData
3+
from vtkmodules.vtkRenderingCore import (
4+
vtkActor2D,
5+
vtkPolyDataMapper2D,
6+
vtkProp,
7+
vtkProperty2D,
8+
)
9+
10+
11+
class PolygonBrush:
12+
"""Display the draw polygon as 2D lines"""
13+
14+
def __init__(self):
15+
super().__init__()
16+
self._points = vtkPoints()
17+
self._lines = vtkCellArray()
18+
self._vertices = vtkCellArray()
19+
self._poly = vtkPolyData()
20+
self._poly.SetLines(self._lines)
21+
self._poly.SetVerts(self._vertices)
22+
self._poly.SetPoints(self._points)
23+
24+
self._brush_mapper = vtkPolyDataMapper2D()
25+
self._brush_mapper.SetInputData(self._poly)
26+
self._brush_actor = vtkActor2D()
27+
self._brush_actor.SetMapper(self._brush_mapper)
28+
self._brush_actor.VisibilityOff()
29+
props = self._brush_actor.GetProperty()
30+
props.SetColor(1.0, 1.0, 0.0)
31+
props.SetPointSize(4.0)
32+
props.SetLineWidth(2.0)
33+
34+
def set_visibility(self, visible: bool):
35+
self._brush_actor.SetVisibility(int(visible))
36+
37+
def move_last_point(self, x: int, y: int) -> None:
38+
count = self._points.GetNumberOfPoints()
39+
if count == 0:
40+
self.add_point(x, y)
41+
else:
42+
self._points.SetPoint(count - 1, [float(x), float(y), 1.0])
43+
self._points.Modified()
44+
45+
def add_point(self, x: int, y: int) -> None:
46+
self._points.InsertNextPoint([float(x), float(y), 1.0])
47+
count = self._points.GetNumberOfPoints()
48+
if count > 1:
49+
self._lines.InsertNextCell(2, [count - 1, count - 2])
50+
self._vertices.InsertNextCell(1, [count - 1])
51+
self._points.Modified()
52+
53+
def reset(self) -> None:
54+
self._points.SetNumberOfPoints(0)
55+
self._lines.Reset()
56+
self._vertices.Reset()
57+
self._poly.Modified()
58+
59+
@property
60+
def points(self) -> vtkPoints:
61+
return self._points
62+
63+
def get_prop(self) -> vtkProp:
64+
"""
65+
Return brush prop.
66+
Can be used to add or remove the brush from the renderer, configure rendering properties (visibility, color, ...)
67+
"""
68+
return self._brush_actor
69+
70+
def get_property(self) -> vtkProperty2D:
71+
return self._brush_actor.GetProperty()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import annotations
2+
3+
from slicer import vtkMRMLAbstractViewNode, vtkMRMLNode
4+
5+
from .segmentation_effect import SegmentationEffect
6+
from .segmentation_effect_draw_widget import SegmentationDrawPipeline
7+
from .segmentation_effect_pipeline import SegmentationEffectPipeline
8+
9+
10+
class SegmentationEffectDraw(SegmentationEffect):
11+
def __init__(self) -> None:
12+
super().__init__()
13+
14+
def _create_pipeline(
15+
self, _view_node: vtkMRMLAbstractViewNode, _parameter: vtkMRMLNode
16+
) -> SegmentationEffectPipeline | None:
17+
return SegmentationDrawPipeline()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from slicer import vtkMRMLInteractionEventData
2+
3+
from .segmentation_polygon_widget import SegmentationPolygonPipeline
4+
5+
6+
class SegmentationDrawPipeline(SegmentationPolygonPipeline):
7+
def _LeftButtonPressed(self, event_data: vtkMRMLInteractionEventData) -> bool:
8+
x, y = event_data.GetDisplayPosition()
9+
if self.widget.is_painting():
10+
self.widget.add_point(x, y)
11+
else:
12+
self.widget.start_painting(x, y)
13+
self.RequestRender()
14+
return True
15+
16+
def _LeftButtonReleased(self, _event_data: vtkMRMLInteractionEventData) -> bool:
17+
self.widget.pause_painting()
18+
return True
19+
20+
def _MouseMoved(self, event_data: vtkMRMLInteractionEventData) -> bool:
21+
if self.widget.is_painting():
22+
x, y = event_data.GetDisplayPosition()
23+
self.widget.move_last_point(x, y)
24+
if self.widget.is_painting():
25+
self.widget.add_point(x, y)
26+
self.RequestRender()
27+
28+
# Always let other interactor and displayable managers do whatever they want
29+
return False
30+
31+
def _RightButtonPressed(self, _event_data: vtkMRMLInteractionEventData) -> bool:
32+
self.widget.stop_painting()
33+
self.RequestRender()
34+
return True
35+
36+
def _RightButtonReleased(self, _event_data: vtkMRMLInteractionEventData) -> bool:
37+
return True

0 commit comments

Comments
 (0)