diff --git a/CMakeLists.txt b/CMakeLists.txt
index 07b007a..db637b5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,14 @@
cmake_minimum_required(VERSION 3.12.1)
project(NNInteractive)
+
+# Mirror extension metadata here so Slicer's Extension Wizard can open either
+# the repository root or the slicer_plugin directory as the extension source.
+set(EXTENSION_HOMEPAGE "https://github.com/coendevente/SlicerNNInteractive")
+set(EXTENSION_CONTRIBUTORS "Coen de Vente (University of Amsterdam)")
+set(EXTENSION_DESCRIPTION "Deep learning-based framework for interactive segmentation of 3D images. The extension is available under an Apache-2.0 license, but the weights that are being downloaded when running the SlicerNNInteractive server are available under a Creative Commons Attribution Non Commercial Share Alike 4.0 license, as described in the original nnInteractive respository.")
+set(EXTENSION_ICONURL "https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/slicer_plugin/SlicerNNInteractive/Resources/Icons/SlicerNNInteractive.png")
+set(EXTENSION_SCREENSHOTURLS "https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/img/segmentation_result.jpg https://raw.githubusercontent.com/coendevente/SlicerNNInteractive/main/img/plugin_first_sight.png")
+set(EXTENSION_DEPENDS "NA")
+
add_subdirectory(slicer_plugin)
diff --git a/slicer_plugin/CMakeLists.txt b/slicer_plugin/CMakeLists.txt
index ac5e943..e080fc8 100644
--- a/slicer_plugin/CMakeLists.txt
+++ b/slicer_plugin/CMakeLists.txt
@@ -19,6 +19,7 @@ include(${Slicer_USE_FILE})
#-----------------------------------------------------------------------------
# Extension modules
add_subdirectory(SlicerNNInteractive)
+add_subdirectory(nninteractive)
## NEXT_MODULE
if(BUILD_TESTING)
diff --git a/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui b/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui
index 3fc919e..eee6743 100644
--- a/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui
+++ b/slicer_plugin/SlicerNNInteractive/Resources/UI/SlicerNNInteractive.ui
@@ -326,6 +326,33 @@
+ -
+
+
-
+
+
+ Undo history steps:
+
+
+
+ -
+
+
+ Maximum number of plugin prompt states kept for Ctrl+Z / Ctrl+Y.
+
+
+ 1
+
+
+ 100
+
+
+ 2
+
+
+
+
+
diff --git a/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py b/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py
index ed57771..077104c 100644
--- a/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py
+++ b/slicer_plugin/SlicerNNInteractive/SlicerNNInteractive.py
@@ -9,6 +9,7 @@
import numpy as np
from pathlib import Path
+from collections import deque
import slicer
import qt
@@ -178,10 +179,16 @@ def setup(self):
self.all_prompt_buttons = {}
self.setup_prompts()
+ self.max_history_depth = 2
+ self._is_replaying_history = False
+ self._active_request_count = 0
+ self._history_state = None
+
self.init_ui_functionality()
_ = self.get_current_segment_id()
self.previous_states = {}
+ self.reset_prompt_history()
def init_ui_functionality(self):
"""
@@ -194,7 +201,15 @@ def init_ui_functionality(self):
self.ui.Server.text = savedServer
self.server = savedServer.rstrip("/")
+ settings = qt.QSettings()
+ saved_history_depth = settings.value(
+ "SlicerNNInteractive/maxHistoryDepth", self.max_history_depth
+ )
+ self.set_max_history_depth(saved_history_depth, update_widget=False)
+ self.ui.sbHistoryDepth.value = self.max_history_depth
+
self.ui.Server.editingFinished.connect(self.update_server)
+ self.ui.sbHistoryDepth.valueChanged.connect(self.on_history_depth_changed)
self.ui.pbTestServer.clicked.connect(self.test_server_connection)
# Set initial prompt type
@@ -235,6 +250,8 @@ def setup_shortcuts(self):
"r": self.clear_current_segment,
"Shift+L": self.submit_lasso_if_present,
"t": self.toggle_prompt_type, # Add 'T' shortcut to toggle between positive/negative
+ "Ctrl+Z": self.undo_prompt,
+ "Ctrl+Y": self.redo_prompt,
}
self.shortcut_items = {}
@@ -568,6 +585,192 @@ def remove_all_but_last_prompt(self):
for i in range(n):
node.RemoveNthControlPoint(0)
+ def make_history_entry(self, prompt_type, positive_click, payload, segmentation_mask):
+ return {
+ "type": prompt_type,
+ "positive": bool(positive_click),
+ "payload": payload,
+ "mask": np.array(segmentation_mask, dtype=np.uint8, copy=True),
+ }
+
+ def reset_prompt_history(self, baseline_mask=None):
+ if baseline_mask is None:
+ image_data = self.get_image_data()
+ if image_data is not None:
+ baseline_mask = np.zeros(image_data.shape, dtype=np.uint8)
+ else:
+ baseline_mask = np.zeros((0,), dtype=np.uint8)
+ else:
+ baseline_mask = np.array(baseline_mask, dtype=np.uint8, copy=True)
+
+ self._history_state = {
+ "base_mask": baseline_mask,
+ "past": deque(),
+ "future": deque(),
+ }
+
+ def history_can_undo(self):
+ return bool(self._history_state and self._history_state["past"])
+
+ def history_can_redo(self):
+ return bool(self._history_state and self._history_state["future"])
+
+ def record_history_entry(self, prompt_type, positive_click, payload, segmentation_mask):
+ if self._is_replaying_history:
+ return
+
+ if self._history_state is None:
+ self.reset_prompt_history()
+
+ self._history_state["future"].clear()
+ if len(self._history_state["past"]) >= self.max_history_depth:
+ dropped_entry = self._history_state["past"].popleft()
+ self._history_state["base_mask"] = np.array(
+ dropped_entry["mask"], dtype=np.uint8, copy=True
+ )
+
+ entry = self.make_history_entry(
+ prompt_type=prompt_type,
+ positive_click=positive_click,
+ payload=payload,
+ segmentation_mask=segmentation_mask,
+ )
+ self._history_state["past"].append(entry)
+
+ def set_history_base_mask(self, baseline_mask):
+ baseline_mask = np.array(baseline_mask, dtype=np.uint8, copy=True)
+ if self._history_state is None:
+ self.reset_prompt_history(baseline_mask=baseline_mask)
+ return
+
+ self._history_state["base_mask"] = baseline_mask
+ self._history_state["past"].clear()
+ self._history_state["future"].clear()
+
+ def set_max_history_depth(self, max_history_depth, update_widget=True):
+ max_history_depth = max(1, int(max_history_depth))
+ self.max_history_depth = max_history_depth
+
+ if update_widget and hasattr(self, "ui") and hasattr(self.ui, "sbHistoryDepth"):
+ blocker = qt.QSignalBlocker(self.ui.sbHistoryDepth)
+ self.ui.sbHistoryDepth.value = self.max_history_depth
+ del blocker
+
+ if self._history_state is None:
+ return
+
+ while len(self._history_state["past"]) > self.max_history_depth:
+ dropped_entry = self._history_state["past"].popleft()
+ self._history_state["base_mask"] = np.array(
+ dropped_entry["mask"], dtype=np.uint8, copy=True
+ )
+
+ while len(self._history_state["future"]) > self.max_history_depth:
+ self._history_state["future"].pop()
+
+ def on_history_depth_changed(self, value):
+ self.set_max_history_depth(value, update_widget=False)
+ settings = qt.QSettings()
+ settings.setValue("SlicerNNInteractive/maxHistoryDepth", self.max_history_depth)
+
+ def build_history_payload(self, **kwargs):
+ payload = {}
+ for key, value in kwargs.items():
+ if isinstance(value, np.ndarray):
+ payload[key] = np.array(value, copy=True)
+ elif isinstance(value, list):
+ payload[key] = list(value)
+ else:
+ payload[key] = value
+ return payload
+
+ def cancel_pending_lasso(self):
+ lasso_node = self.prompt_types["lasso"]["node"]
+ if lasso_node is None:
+ return False
+
+ if lasso_node.GetNumberOfControlPoints() < 1:
+ return False
+
+ self.on_lasso_cancel_clicked()
+ self.ui.pbInteractionLasso.setChecked(False)
+ return True
+
+ def apply_history_state(self, target_entries):
+ if self._history_state is None:
+ return
+
+ base_mask = np.array(self._history_state["base_mask"], dtype=np.uint8, copy=True)
+ self.show_segmentation(base_mask)
+ upload_result = self.upload_segment_to_server()
+ if upload_result is None:
+ raise RuntimeError("Failed to synchronize base segment during history replay.")
+
+ if not target_entries:
+ return
+
+ self._is_replaying_history = True
+ try:
+ for entry in target_entries:
+ self.replay_history_entry(entry)
+ finally:
+ self._is_replaying_history = False
+
+ self.show_segmentation(target_entries[-1]["mask"])
+
+ def replay_history_entry(self, entry):
+ prompt_type = entry["type"]
+ payload = entry["payload"]
+ positive_click = entry["positive"]
+
+ if prompt_type == "point":
+ self.point_prompt(
+ xyz=payload["xyz"],
+ positive_click=positive_click,
+ sync=False,
+ record_history=False,
+ )
+ elif prompt_type == "bbox":
+ self.bbox_prompt(
+ outer_point_one=payload["outer_point_one"],
+ outer_point_two=payload["outer_point_two"],
+ positive_click=positive_click,
+ sync=False,
+ record_history=False,
+ )
+ elif prompt_type in ["lasso", "scribble"]:
+ self.lasso_or_scribble_prompt(
+ mask=payload["mask"],
+ positive_click=positive_click,
+ tp=prompt_type,
+ sync=False,
+ record_history=False,
+ )
+ elif prompt_type == "clear":
+ # "Clear" is a no-op during replay because show_segmentation is called on masks
+ pass
+ else:
+ raise ValueError(f"Unknown prompt type '{prompt_type}' in history replay.")
+
+ def undo_prompt(self):
+ if self.cancel_pending_lasso():
+ return
+
+ if self._active_request_count > 0 or not self.history_can_undo():
+ return
+
+ entry = self._history_state["past"].pop()
+ self._history_state["future"].appendleft(entry)
+ self.apply_history_state(list(self._history_state["past"]))
+
+ def redo_prompt(self):
+ if self._active_request_count > 0 or not self.history_can_redo():
+ return
+
+ entry = self._history_state["future"].popleft()
+ self._history_state["past"].append(entry)
+ self.apply_history_state(list(self._history_state["past"]))
+
def on_place_button_clicked(self, checked, prompt_name):
self.setup_prompts(skip_if_exists=True)
@@ -643,11 +846,32 @@ def on_point_placed(self, caller, event):
if volume_node:
self.point_prompt(xyz=xyz, positive_click=self.is_positive)
- @ensure_synched
- def point_prompt(self, xyz=None, positive_click=False):
+ def point_prompt(self, xyz=None, positive_click=False, sync=True, record_history=True):
"""
Uploads point prompt to the server.
"""
+ if sync:
+ return self._point_prompt_synched(
+ xyz=xyz,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ return self._point_prompt_impl(
+ xyz=xyz,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ @ensure_synched
+ def _point_prompt_synched(self, xyz=None, positive_click=False, record_history=True):
+ return self._point_prompt_impl(
+ xyz=xyz,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ def _point_prompt_impl(self, xyz=None, positive_click=False, record_history=True):
url = f"{self.server}/add_point_interaction"
seg_response = self.request_to_server(
@@ -662,6 +886,15 @@ def point_prompt(self, xyz=None, positive_click=False):
debug_print(f"{positive_click} point prompt triggered! {xyz}")
self.show_segmentation(unpacked_segmentation)
+ if record_history:
+ self.record_history_entry(
+ prompt_type="point",
+ positive_click=positive_click,
+ payload=self.build_history_payload(xyz=xyz),
+ segmentation_mask=unpacked_segmentation,
+ )
+
+ return unpacked_segmentation
#
# -- Bounding Box
@@ -709,11 +942,35 @@ def _next():
self.prev_caller = caller
- @ensure_synched
- def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False):
+ def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False, sync=True, record_history=True):
"""
Uploads BBox prompt to the server.
"""
+ if sync:
+ return self._bbox_prompt_synched(
+ outer_point_one=outer_point_one,
+ outer_point_two=outer_point_two,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ return self._bbox_prompt_impl(
+ outer_point_one=outer_point_one,
+ outer_point_two=outer_point_two,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ @ensure_synched
+ def _bbox_prompt_synched(self, outer_point_one, outer_point_two, positive_click=False, record_history=True):
+ return self._bbox_prompt_impl(
+ outer_point_one=outer_point_one,
+ outer_point_two=outer_point_two,
+ positive_click=positive_click,
+ record_history=record_history,
+ )
+
+ def _bbox_prompt_impl(self, outer_point_one, outer_point_two, positive_click=False, record_history=True):
url = f"{self.server}/add_bbox_interaction"
seg_response = self.request_to_server(
@@ -729,6 +986,18 @@ def bbox_prompt(self, outer_point_one, outer_point_two, positive_click=False):
seg_response.content, decompress=False
)
self.show_segmentation(unpacked_segmentation)
+ if record_history:
+ self.record_history_entry(
+ prompt_type="bbox",
+ positive_click=positive_click,
+ payload=self.build_history_payload(
+ outer_point_one=outer_point_one,
+ outer_point_two=outer_point_two,
+ ),
+ segmentation_mask=unpacked_segmentation,
+ )
+
+ return unpacked_segmentation
#
# -- Lasso
@@ -832,11 +1101,35 @@ def on_scribble_clicked(self, checked=False):
#
# -- Lasso/scribble
#
- @ensure_synched
- def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso"):
+ def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso", sync=True, record_history=True):
"""
Uploads lasso or scribble prompt to the server.
"""
+ if sync:
+ return self._lasso_or_scribble_prompt_synched(
+ mask=mask,
+ positive_click=positive_click,
+ tp=tp,
+ record_history=record_history,
+ )
+
+ return self._lasso_or_scribble_prompt_impl(
+ mask=mask,
+ positive_click=positive_click,
+ tp=tp,
+ record_history=record_history,
+ )
+
+ @ensure_synched
+ def _lasso_or_scribble_prompt_synched(self, mask, positive_click=False, tp="lasso", record_history=True):
+ return self._lasso_or_scribble_prompt_impl(
+ mask=mask,
+ positive_click=positive_click,
+ tp=tp,
+ record_history=record_history,
+ )
+
+ def _lasso_or_scribble_prompt_impl(self, mask, positive_click=False, tp="lasso", record_history=True):
if np.sum(mask) == 0:
return
@@ -869,6 +1162,14 @@ def lasso_or_scribble_prompt(self, mask, positive_click=False, tp="lasso"):
seg_response.content, decompress=False
)
self.show_segmentation(unpacked_segmentation)
+ if record_history:
+ self.record_history_entry(
+ prompt_type=tp,
+ positive_click=positive_click,
+ payload=self.build_history_payload(mask=np.array(mask, dtype=np.uint8, copy=True)),
+ segmentation_mask=unpacked_segmentation,
+ )
+ return unpacked_segmentation
else:
debug_print(
f"lasso_or_scribble_prompt upload failed with status code: {seg_response.status_code}"
@@ -891,7 +1192,7 @@ def on_scribble_finished(self, caller, event):
else:
return
- mask = slicer.util.arrayFromSegmentBinaryLabelmap(
+ mask = self.get_segment_mask_or_empty(
self.scribble_segment_node, label_name, self.get_volume_node()
)
@@ -952,6 +1253,7 @@ def make_new_segment(self):
# Make sure the right node is selected
self.ui.editor_widget.setSegmentationNode(segmentation_node)
self.segment_editor_node.SetSelectedSegmentID(new_segment_id)
+ self.reset_prompt_history()
return segmentation_node, new_segment_id
@@ -968,9 +1270,15 @@ def clear_current_segment(self):
if selected_segment_id:
debug_print(f"Clearing segment: {selected_segment_id}")
- self.show_segmentation(
- np.zeros(self.get_image_data().shape, dtype=np.uint8)
+ # Record the clear action in history before resetting prompt history or clearing
+ empty_mask = self.get_empty_mask()
+ self.record_history_entry(
+ prompt_type="clear",
+ positive_click=True,
+ payload={},
+ segmentation_mask=empty_mask
)
+ self.show_segmentation(empty_mask)
self.setup_prompts()
self.upload_segment_to_server()
else:
@@ -981,6 +1289,7 @@ def show_segmentation(self, segmentation_mask):
Updates the currently selected segment with the given binary mask array.
"""
t0 = time.time()
+ segmentation_mask = np.array(segmentation_mask, dtype=np.uint8, copy=True)
self.previous_states["segment_data"] = segmentation_mask
segmentationNode, selectedSegmentID = (
@@ -1013,8 +1322,6 @@ def show_segmentation(self, segmentation_mask):
# (see https://github.com/coendevente/SlicerNNInteractive/issues/38)
segmentationNode.GetSegmentation().CollapseBinaryLabelmaps()
- del segmentation_mask
-
debug_print(f"show_segmentation took {time.time() - t0}")
def get_segmentation_node(self):
@@ -1065,6 +1372,39 @@ def get_current_segment_id(self):
"""
return self.ui.editor_widget.mrmlSegmentEditorNode().GetSelectedSegmentID()
+ def get_empty_mask(self):
+ image_data = self.get_image_data()
+ if image_data is None:
+ return np.zeros((0,), dtype=np.uint8)
+
+ return np.zeros(image_data.shape, dtype=np.uint8)
+
+ def get_segment_mask_or_empty(self, segmentation_node, segment_id, volume_node=None):
+ if segmentation_node is None or not segment_id:
+ return self.get_empty_mask()
+
+ segment = segmentation_node.GetSegmentation().GetSegment(segment_id)
+ if segment is None:
+ return self.get_empty_mask()
+
+ if volume_node is None:
+ volume_node = self.get_volume_node()
+
+ try:
+ mask = slicer.util.arrayFromSegmentBinaryLabelmap(
+ segmentation_node, segment_id, volume_node
+ )
+ except Exception as exc:
+ debug_print(
+ f"Falling back to empty mask for segment '{segment_id}': {exc}"
+ )
+ return self.get_empty_mask()
+
+ if mask is None:
+ return self.get_empty_mask()
+
+ return np.array(mask, dtype=np.uint8, copy=True)
+
def get_segment_data(self):
"""
Gets the labelmap array (binary) of the currently selected segment.
@@ -1073,7 +1413,7 @@ def get_segment_data(self):
self.get_selected_segmentation_node_and_segment_id()
)
- mask = slicer.util.arrayFromSegmentBinaryLabelmap(
+ mask = self.get_segment_mask_or_empty(
segmentation_node, selected_segment_id, self.get_volume_node()
)
seg_data_bool = mask.astype(bool)
@@ -1099,6 +1439,9 @@ def selected_segment_changed(self):
debug_print(f"selected_segment_changed: {selected_segment_changed}")
+ if selected_segment_changed and not self._is_replaying_history:
+ self.set_history_base_mask(segment_data.astype(np.uint8))
+
return selected_segment_changed
###############################################################################
@@ -1174,50 +1517,54 @@ def request_to_server(self, *args, **kwargs):
Wraps requests.post in a try/except and shows error in pop up windows if necessary.
"""
- with slicer.util.tryWithErrorDisplay(_("Segmentation failed."), waitCursor=True):
-
- error_message = None
- try:
- response = requests.post(*args, **kwargs)
- debug_print('response:', response)
- except requests.exceptions.MissingSchema as e:
- response = None
- if self.server == "":
- raise RuntimeError("It seems you have not set the server URL yet. You can configure it in the 'Configuration' tab.")
- else:
- raise RuntimeError(f"Server URL '{self.server}' is unreachable. You can edit the URL in the 'Configuration' tab.")
- except requests.exceptions.ConnectionError as e:
- response = None
- raise RuntimeError(f"Failed to connect to server '{self.server}'. Please make sure the server is running and check the server URL in the 'Configuration' tab.")
- except requests.exceptions.InvalidSchema as e:
- append_text_to_error_message = ""
- if not args[0].startswith("http://"):
- append_text_to_error_message = "\n\nHint: Perhaps your Server URL in the 'Configuration' tab should start with 'http://'. For example, if your server runs on localhost and port 1527, 'localhost:1527' would not work as a Server URL, while 'http://localhost:1527' would."
- raise RuntimeError(f'{e}{append_text_to_error_message}')
-
- if response.status_code != 200:
- status_code = response.status_code
- response = None
- raise RuntimeError(f"Something has gone wrong with your request (Status code {status_code}).")
-
- t0 = time.time()
- # Try to parse JSON and check for a specific error.
- content_type = response.headers.get("Content-Type", "")
- if "application/json" in content_type:
- resp_json = response.json()
- if resp_json.get("status") == "error":
- if "No image uploaded" in resp_json.get("message", ""):
- debug_print("No image has been uploaded to the server. Please upload an image first.")
- self.upload_image_to_server()
- self.upload_segment_to_server()
- return self.request_to_server(*args, **kwargs)
+ self._active_request_count += 1
+ try:
+ with slicer.util.tryWithErrorDisplay(_("Segmentation failed."), waitCursor=True):
+
+ error_message = None
+ try:
+ response = requests.post(*args, **kwargs)
+ debug_print('response:', response)
+ except requests.exceptions.MissingSchema as e:
+ response = None
+ if self.server == "":
+ raise RuntimeError("It seems you have not set the server URL yet. You can configure it in the 'Configuration' tab.")
else:
- response = None
- raise RuntimeError(f"Server error: {resp_json.get('message', 'Unknown error')}")
-
- debug_print('1157 took', time.time() - t0)
-
- return response
+ raise RuntimeError(f"Server URL '{self.server}' is unreachable. You can edit the URL in the 'Configuration' tab.")
+ except requests.exceptions.ConnectionError as e:
+ response = None
+ raise RuntimeError(f"Failed to connect to server '{self.server}'. Please make sure the server is running and check the server URL in the 'Configuration' tab.")
+ except requests.exceptions.InvalidSchema as e:
+ append_text_to_error_message = ""
+ if not args[0].startswith("http://"):
+ append_text_to_error_message = "\n\nHint: Perhaps your Server URL in the 'Configuration' tab should start with 'http://'. For example, if your server runs on localhost and port 1527, 'localhost:1527' would not work as a Server URL, while 'http://localhost:1527' would."
+ raise RuntimeError(f'{e}{append_text_to_error_message}')
+
+ if response.status_code != 200:
+ status_code = response.status_code
+ response = None
+ raise RuntimeError(f"Something has gone wrong with your request (Status code {status_code}).")
+
+ t0 = time.time()
+ # Try to parse JSON and check for a specific error.
+ content_type = response.headers.get("Content-Type", "")
+ if "application/json" in content_type:
+ resp_json = response.json()
+ if resp_json.get("status") == "error":
+ if "No image uploaded" in resp_json.get("message", ""):
+ debug_print("No image has been uploaded to the server. Please upload an image first.")
+ self.upload_image_to_server()
+ self.upload_segment_to_server()
+ return self.request_to_server(*args, **kwargs)
+ else:
+ response = None
+ raise RuntimeError(f"Server error: {resp_json.get('message', 'Unknown error')}")
+
+ debug_print('1157 took', time.time() - t0)
+
+ return response
+ finally:
+ self._active_request_count -= 1
def upload_image_to_server(self):
"""
diff --git a/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py b/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py
index fe50756..2b38723 100644
--- a/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py
+++ b/slicer_plugin/SlicerNNInteractive/Testing/Python/SlicerNNInteractiveSegmentationTest.py
@@ -184,6 +184,8 @@ def runTest(self):
mask,
prompt_name
)
+
+ self._verify_bounded_undo_redo(widget)
finally:
self.tearDown()
@@ -271,6 +273,52 @@ def clamp(pt):
)
return labelmap.astype(np.uint8)
+ def _current_segment_mask(self, widget):
+ segmentation_node, segment_id = widget.get_selected_segmentation_node_and_segment_id()
+ labelmap = slicer.util.arrayFromSegmentBinaryLabelmap(
+ segmentation_node, segment_id, widget.get_volume_node()
+ )
+ return labelmap.astype(np.uint8)
+
+ def _verify_bounded_undo_redo(self, widget):
+ widget.clear_current_segment()
+
+ prompt_sequence = [
+ positive([141, 114, 85]),
+ positive([109, 114, 58]),
+ positive([177, 114, 38]),
+ ]
+
+ masks = []
+ for interaction in prompt_sequence:
+ masks.append(
+ self._trigger_point_prompt(widget, interaction["coords"], interaction["positive"])
+ )
+
+ widget.undo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[1]))
+
+ widget.undo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[0]))
+
+ widget.undo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[0]))
+
+ widget.redo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[1]))
+
+ widget.redo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[2]))
+
+ widget.redo_prompt()
+ slicer.app.processEvents()
+ self.assertTrue(np.array_equal(self._current_segment_mask(widget), masks[2]))
+
def _trigger_scribble_prompt(self, widget, interaction):
mask = self._build_scribble_mask(widget, interaction)
self._save_scribble_mask(interaction["mask_name"], mask)
diff --git a/slicer_plugin/nninteractive/CMakeLists.txt b/slicer_plugin/nninteractive/CMakeLists.txt
new file mode 100644
index 0000000..1ded2b2
--- /dev/null
+++ b/slicer_plugin/nninteractive/CMakeLists.txt
@@ -0,0 +1,63 @@
+
+#-----------------------------------------------------------------------------
+set(MODULE_NAME nninteractive)
+
+string(TOUPPER ${MODULE_NAME} MODULE_NAME_UPPER)
+
+#-----------------------------------------------------------------------------
+add_subdirectory(Logic)
+add_subdirectory(Widgets)
+
+#-----------------------------------------------------------------------------
+set(MODULE_EXPORT_DIRECTIVE "Q_SLICER_QTMODULES_${MODULE_NAME_UPPER}_EXPORT")
+
+# Current_{source,binary} and Slicer_{Libs,Base} already included
+set(MODULE_INCLUDE_DIRECTORIES
+ ${CMAKE_CURRENT_SOURCE_DIR}/Logic
+ ${CMAKE_CURRENT_BINARY_DIR}/Logic
+ ${CMAKE_CURRENT_SOURCE_DIR}/Widgets
+ ${CMAKE_CURRENT_BINARY_DIR}/Widgets
+ )
+
+set(MODULE_SRCS
+ qSlicer${MODULE_NAME}Module.cxx
+ qSlicer${MODULE_NAME}Module.h
+ qSlicer${MODULE_NAME}ModuleWidget.cxx
+ qSlicer${MODULE_NAME}ModuleWidget.h
+ )
+
+set(MODULE_MOC_SRCS
+ qSlicer${MODULE_NAME}Module.h
+ qSlicer${MODULE_NAME}ModuleWidget.h
+ )
+
+set(MODULE_UI_SRCS
+ Resources/UI/qSlicer${MODULE_NAME}ModuleWidget.ui
+ )
+
+set(MODULE_TARGET_LIBRARIES
+ vtkSlicer${MODULE_NAME}ModuleLogic
+ qSlicer${MODULE_NAME}ModuleWidgets
+ )
+
+set(MODULE_RESOURCES
+ Resources/qSlicer${MODULE_NAME}Module.qrc
+ )
+
+#-----------------------------------------------------------------------------
+slicerMacroBuildLoadableModule(
+ NAME ${MODULE_NAME}
+ EXPORT_DIRECTIVE ${MODULE_EXPORT_DIRECTIVE}
+ INCLUDE_DIRECTORIES ${MODULE_INCLUDE_DIRECTORIES}
+ SRCS ${MODULE_SRCS}
+ MOC_SRCS ${MODULE_MOC_SRCS}
+ UI_SRCS ${MODULE_UI_SRCS}
+ TARGET_LIBRARIES ${MODULE_TARGET_LIBRARIES}
+ RESOURCES ${MODULE_RESOURCES}
+ WITH_GENERIC_TESTS
+ )
+
+#-----------------------------------------------------------------------------
+if(BUILD_TESTING)
+ add_subdirectory(Testing)
+endif()
diff --git a/slicer_plugin/nninteractive/Logic/CMakeLists.txt b/slicer_plugin/nninteractive/Logic/CMakeLists.txt
new file mode 100644
index 0000000..d6f0cee
--- /dev/null
+++ b/slicer_plugin/nninteractive/Logic/CMakeLists.txt
@@ -0,0 +1,26 @@
+project(vtkSlicer${MODULE_NAME}ModuleLogic)
+
+set(KIT ${PROJECT_NAME})
+
+set(${KIT}_EXPORT_DIRECTIVE "VTK_SLICER_${MODULE_NAME_UPPER}_MODULE_LOGIC_EXPORT")
+
+set(${KIT}_INCLUDE_DIRECTORIES
+ )
+
+set(${KIT}_SRCS
+ vtkSlicer${MODULE_NAME}Logic.cxx
+ vtkSlicer${MODULE_NAME}Logic.h
+ )
+
+set(${KIT}_TARGET_LIBRARIES
+ ${ITK_LIBRARIES}
+ )
+
+#-----------------------------------------------------------------------------
+SlicerMacroBuildModuleLogic(
+ NAME ${KIT}
+ EXPORT_DIRECTIVE ${${KIT}_EXPORT_DIRECTIVE}
+ INCLUDE_DIRECTORIES ${${KIT}_INCLUDE_DIRECTORIES}
+ SRCS ${${KIT}_SRCS}
+ TARGET_LIBRARIES ${${KIT}_TARGET_LIBRARIES}
+ )
diff --git a/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx
new file mode 100644
index 0000000..0dff685
--- /dev/null
+++ b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.cxx
@@ -0,0 +1,73 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+// nninteractive Logic includes
+#include "vtkSlicernninteractiveLogic.h"
+
+// MRML includes
+#include
+
+// VTK includes
+#include
+#include
+#include
+
+// STD includes
+#include
+
+//----------------------------------------------------------------------------
+vtkStandardNewMacro(vtkSlicernninteractiveLogic);
+
+//----------------------------------------------------------------------------
+vtkSlicernninteractiveLogic::vtkSlicernninteractiveLogic() {}
+
+//----------------------------------------------------------------------------
+vtkSlicernninteractiveLogic::~vtkSlicernninteractiveLogic() {}
+
+//----------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::PrintSelf(ostream& os, vtkIndent indent)
+{
+ this->Superclass::PrintSelf(os, indent);
+}
+
+//---------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::SetMRMLSceneInternal(vtkMRMLScene* newScene)
+{
+ vtkNew events;
+ events->InsertNextValue(vtkMRMLScene::NodeAddedEvent);
+ events->InsertNextValue(vtkMRMLScene::NodeRemovedEvent);
+ events->InsertNextValue(vtkMRMLScene::EndBatchProcessEvent);
+ this->SetAndObserveMRMLSceneEventsInternal(newScene, events.GetPointer());
+}
+
+//-----------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::RegisterNodes()
+{
+ assert(this->GetMRMLScene() != 0);
+}
+
+//---------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::UpdateFromMRMLScene()
+{
+ assert(this->GetMRMLScene() != 0);
+}
+
+//---------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::OnMRMLSceneNodeAdded(vtkMRMLNode* vtkNotUsed(node)) {}
+
+//---------------------------------------------------------------------------
+void vtkSlicernninteractiveLogic::OnMRMLSceneNodeRemoved(vtkMRMLNode* vtkNotUsed(node)) {}
diff --git a/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h
new file mode 100644
index 0000000..876a0e8
--- /dev/null
+++ b/slicer_plugin/nninteractive/Logic/vtkSlicernninteractiveLogic.h
@@ -0,0 +1,59 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+// .NAME vtkSlicernninteractiveLogic - slicer logic class for volumes manipulation
+// .SECTION Description
+// This class manages the logic associated with reading, saving,
+// and changing propertied of the volumes
+
+#ifndef __vtkSlicernninteractiveLogic_h
+#define __vtkSlicernninteractiveLogic_h
+
+// Slicer includes
+#include "vtkSlicerModuleLogic.h"
+
+// MRML includes
+
+// STD includes
+#include
+
+#include "vtkSlicernninteractiveModuleLogicExport.h"
+
+class VTK_SLICER_NNINTERACTIVE_MODULE_LOGIC_EXPORT vtkSlicernninteractiveLogic : public vtkSlicerModuleLogic
+{
+public:
+ static vtkSlicernninteractiveLogic* New();
+ vtkTypeMacro(vtkSlicernninteractiveLogic, vtkSlicerModuleLogic);
+ void PrintSelf(ostream& os, vtkIndent indent) override;
+
+protected:
+ vtkSlicernninteractiveLogic();
+ ~vtkSlicernninteractiveLogic() override;
+
+ void SetMRMLSceneInternal(vtkMRMLScene* newScene) override;
+ /// Register MRML Node classes to Scene. Gets called automatically when the MRMLScene is attached to this logic class.
+ void RegisterNodes() override;
+ void UpdateFromMRMLScene() override;
+ void OnMRMLSceneNodeAdded(vtkMRMLNode* node) override;
+ void OnMRMLSceneNodeRemoved(vtkMRMLNode* node) override;
+
+private:
+ vtkSlicernninteractiveLogic(const vtkSlicernninteractiveLogic&); // Not implemented
+ void operator=(const vtkSlicernninteractiveLogic&); // Not implemented
+};
+
+#endif
diff --git a/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png b/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png
new file mode 100644
index 0000000..9c5938e
Binary files /dev/null and b/slicer_plugin/nninteractive/Resources/Icons/nninteractive.png differ
diff --git a/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui
new file mode 100644
index 0000000..d2e6bed
--- /dev/null
+++ b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveFooBarWidget.ui
@@ -0,0 +1,31 @@
+
+
+ qSlicernninteractiveFooBarWidget
+
+
+
+ 0
+ 0
+ 103
+ 27
+
+
+
+ Foo bar
+
+
+
+ 0
+
+ -
+
+
+ Foo Bar
+
+
+
+
+
+
+
+
diff --git a/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui
new file mode 100644
index 0000000..029b1fb
--- /dev/null
+++ b/slicer_plugin/nninteractive/Resources/UI/qSlicernninteractiveModuleWidget.ui
@@ -0,0 +1,66 @@
+
+
+ qSlicernninteractiveModuleWidget
+
+
+
+ 0
+ 0
+ 525
+ 319
+
+
+
+ nninteractive
+
+
+ -
+
+
+ Display
+
+
+
-
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+ qSlicerWidget
+ QWidget
+
+ 1
+
+
+ ctkCollapsibleButton
+ QWidget
+
+ 1
+
+
+ qSlicernninteractiveFooBarWidget
+ QWidget
+ qSlicernninteractiveFooBarWidget.h
+ 1
+
+
+
+
+
diff --git a/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc b/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc
new file mode 100644
index 0000000..2244c04
--- /dev/null
+++ b/slicer_plugin/nninteractive/Resources/qSlicernninteractiveModule.qrc
@@ -0,0 +1,5 @@
+
+
+ Icons/nninteractive.png
+
+
diff --git a/slicer_plugin/nninteractive/Testing/CMakeLists.txt b/slicer_plugin/nninteractive/Testing/CMakeLists.txt
new file mode 100644
index 0000000..35f9732
--- /dev/null
+++ b/slicer_plugin/nninteractive/Testing/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(Cxx)
diff --git a/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt b/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt
new file mode 100644
index 0000000..b7341cd
--- /dev/null
+++ b/slicer_plugin/nninteractive/Testing/Cxx/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(KIT qSlicer${MODULE_NAME}Module)
+
+#-----------------------------------------------------------------------------
+set(KIT_TEST_SRCS
+ #qSlicer${MODULE_NAME}ModuleTest.cxx
+ )
+
+#-----------------------------------------------------------------------------
+slicerMacroConfigureModuleCxxTestDriver(
+ NAME ${KIT}
+ SOURCES ${KIT_TEST_SRCS}
+ WITH_VTK_DEBUG_LEAKS_CHECK
+ WITH_VTK_ERROR_OUTPUT_CHECK
+ )
+
+#-----------------------------------------------------------------------------
+#simple_test(qSlicer${MODULE_NAME}ModuleTest)
diff --git a/slicer_plugin/nninteractive/Widgets/CMakeLists.txt b/slicer_plugin/nninteractive/Widgets/CMakeLists.txt
new file mode 100644
index 0000000..7701b68
--- /dev/null
+++ b/slicer_plugin/nninteractive/Widgets/CMakeLists.txt
@@ -0,0 +1,42 @@
+project(qSlicer${MODULE_NAME}ModuleWidgets)
+
+set(KIT ${PROJECT_NAME})
+
+set(${KIT}_EXPORT_DIRECTIVE "Q_SLICER_MODULE_${MODULE_NAME_UPPER}_WIDGETS_EXPORT")
+
+set(${KIT}_INCLUDE_DIRECTORIES
+ )
+
+set(${KIT}_SRCS
+ qSlicer${MODULE_NAME}FooBarWidget.cxx
+ qSlicer${MODULE_NAME}FooBarWidget.h
+ )
+
+set(${KIT}_MOC_SRCS
+ qSlicer${MODULE_NAME}FooBarWidget.h
+ )
+
+set(${KIT}_UI_SRCS
+ ../Resources/UI/qSlicer${MODULE_NAME}FooBarWidget.ui
+ )
+
+set(${KIT}_RESOURCES
+ ../Resources/qSlicer${MODULE_NAME}Module.qrc
+ )
+
+set(${KIT}_TARGET_LIBRARIES
+ vtkSlicer${MODULE_NAME}ModuleLogic
+ )
+
+#-----------------------------------------------------------------------------
+SlicerMacroBuildModuleWidgets(
+ NAME ${KIT}
+ EXPORT_DIRECTIVE ${${KIT}_EXPORT_DIRECTIVE}
+ INCLUDE_DIRECTORIES ${${KIT}_INCLUDE_DIRECTORIES}
+ SRCS ${${KIT}_SRCS}
+ MOC_SRCS ${${KIT}_MOC_SRCS}
+ UI_SRCS ${${KIT}_UI_SRCS}
+ TARGET_LIBRARIES ${${KIT}_TARGET_LIBRARIES}
+ RESOURCES ${${KIT}_RESOURCES}
+ WRAP_PYTHONQT
+ )
diff --git a/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx
new file mode 100644
index 0000000..4bda2ec
--- /dev/null
+++ b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.cxx
@@ -0,0 +1,63 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Copyright (c) Kitware Inc.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
+ and was partially funded by NIH grant 3P41RR013218-12S1
+
+==============================================================================*/
+
+// FooBar Widgets includes
+#include "qSlicernninteractiveFooBarWidget.h"
+#include "ui_qSlicernninteractiveFooBarWidget.h"
+
+//-----------------------------------------------------------------------------
+class qSlicernninteractiveFooBarWidgetPrivate : public Ui_qSlicernninteractiveFooBarWidget
+{
+ Q_DECLARE_PUBLIC(qSlicernninteractiveFooBarWidget);
+
+protected:
+ qSlicernninteractiveFooBarWidget* const q_ptr;
+
+public:
+ qSlicernninteractiveFooBarWidgetPrivate(qSlicernninteractiveFooBarWidget& object);
+ virtual void setupUi(qSlicernninteractiveFooBarWidget*);
+};
+
+// --------------------------------------------------------------------------
+qSlicernninteractiveFooBarWidgetPrivate::qSlicernninteractiveFooBarWidgetPrivate(qSlicernninteractiveFooBarWidget& object)
+ : q_ptr(&object)
+{
+}
+
+// --------------------------------------------------------------------------
+void qSlicernninteractiveFooBarWidgetPrivate::setupUi(qSlicernninteractiveFooBarWidget* widget)
+{
+ this->Ui_qSlicernninteractiveFooBarWidget::setupUi(widget);
+}
+
+//-----------------------------------------------------------------------------
+// qSlicernninteractiveFooBarWidget methods
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveFooBarWidget::qSlicernninteractiveFooBarWidget(QWidget* parentWidget)
+ : Superclass(parentWidget)
+ , d_ptr(new qSlicernninteractiveFooBarWidgetPrivate(*this))
+{
+ Q_D(qSlicernninteractiveFooBarWidget);
+ d->setupUi(this);
+}
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveFooBarWidget ::~qSlicernninteractiveFooBarWidget() {}
diff --git a/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h
new file mode 100644
index 0000000..ce4c1b4
--- /dev/null
+++ b/slicer_plugin/nninteractive/Widgets/qSlicernninteractiveFooBarWidget.h
@@ -0,0 +1,50 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Copyright (c) Kitware Inc.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
+ and was partially funded by NIH grant 3P41RR013218-12S1
+
+==============================================================================*/
+
+#ifndef __qSlicernninteractiveFooBarWidget_h
+#define __qSlicernninteractiveFooBarWidget_h
+
+// Qt includes
+#include
+
+// FooBar Widgets includes
+#include "qSlicernninteractiveModuleWidgetsExport.h"
+
+class qSlicernninteractiveFooBarWidgetPrivate;
+
+class Q_SLICER_MODULE_NNINTERACTIVE_WIDGETS_EXPORT qSlicernninteractiveFooBarWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ typedef QWidget Superclass;
+ qSlicernninteractiveFooBarWidget(QWidget* parent = 0);
+ ~qSlicernninteractiveFooBarWidget() override;
+
+protected slots:
+
+protected:
+ QScopedPointer d_ptr;
+
+private:
+ Q_DECLARE_PRIVATE(qSlicernninteractiveFooBarWidget);
+ Q_DISABLE_COPY(qSlicernninteractiveFooBarWidget);
+};
+
+#endif
diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx b/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx
new file mode 100644
index 0000000..86f74af
--- /dev/null
+++ b/slicer_plugin/nninteractive/qSlicernninteractiveModule.cxx
@@ -0,0 +1,105 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+// nninteractive Logic includes
+#include
+
+// nninteractive includes
+#include "qSlicernninteractiveModule.h"
+#include "qSlicernninteractiveModuleWidget.h"
+
+//-----------------------------------------------------------------------------
+class qSlicernninteractiveModulePrivate
+{
+public:
+ qSlicernninteractiveModulePrivate();
+};
+
+//-----------------------------------------------------------------------------
+// qSlicernninteractiveModulePrivate methods
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModulePrivate::qSlicernninteractiveModulePrivate() {}
+
+//-----------------------------------------------------------------------------
+// qSlicernninteractiveModule methods
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModule::qSlicernninteractiveModule(QObject* _parent)
+ : Superclass(_parent)
+ , d_ptr(new qSlicernninteractiveModulePrivate)
+{
+}
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModule::~qSlicernninteractiveModule() {}
+
+//-----------------------------------------------------------------------------
+QString qSlicernninteractiveModule::helpText() const
+{
+ return "This is a loadable module that can be bundled in an extension";
+}
+
+//-----------------------------------------------------------------------------
+QString qSlicernninteractiveModule::acknowledgementText() const
+{
+ return "This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN";
+}
+
+//-----------------------------------------------------------------------------
+QStringList qSlicernninteractiveModule::contributors() const
+{
+ QStringList moduleContributors;
+ moduleContributors << QString("John Doe (AnyWare Corp.)");
+ return moduleContributors;
+}
+
+//-----------------------------------------------------------------------------
+QIcon qSlicernninteractiveModule::icon() const
+{
+ return QIcon(":/Icons/nninteractive.png");
+}
+
+//-----------------------------------------------------------------------------
+QStringList qSlicernninteractiveModule::categories() const
+{
+ return QStringList() << "Examples";
+}
+
+//-----------------------------------------------------------------------------
+QStringList qSlicernninteractiveModule::dependencies() const
+{
+ return QStringList();
+}
+
+//-----------------------------------------------------------------------------
+void qSlicernninteractiveModule::setup()
+{
+ this->Superclass::setup();
+}
+
+//-----------------------------------------------------------------------------
+qSlicerAbstractModuleRepresentation* qSlicernninteractiveModule::createWidgetRepresentation()
+{
+ return new qSlicernninteractiveModuleWidget;
+}
+
+//-----------------------------------------------------------------------------
+vtkMRMLAbstractLogic* qSlicernninteractiveModule::createLogic()
+{
+ return vtkSlicernninteractiveLogic::New();
+}
diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModule.h b/slicer_plugin/nninteractive/qSlicernninteractiveModule.h
new file mode 100644
index 0000000..0a172cc
--- /dev/null
+++ b/slicer_plugin/nninteractive/qSlicernninteractiveModule.h
@@ -0,0 +1,68 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+#ifndef __qSlicernninteractiveModule_h
+#define __qSlicernninteractiveModule_h
+
+// Slicer includes
+#include "qSlicerLoadableModule.h"
+
+#include "qSlicernninteractiveModuleExport.h"
+
+class qSlicernninteractiveModulePrivate;
+
+class Q_SLICER_QTMODULES_NNINTERACTIVE_EXPORT qSlicernninteractiveModule : public qSlicerLoadableModule
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.slicer.modules.loadable.qSlicerLoadableModule/1.0");
+ Q_INTERFACES(qSlicerLoadableModule);
+
+public:
+ typedef qSlicerLoadableModule Superclass;
+ explicit qSlicernninteractiveModule(QObject* parent = nullptr);
+ ~qSlicernninteractiveModule() override;
+
+ qSlicerGetTitleMacro(tr("nninteractive"));
+
+ QString helpText() const override;
+ QString acknowledgementText() const override;
+ QStringList contributors() const override;
+
+ QIcon icon() const override;
+
+ QStringList categories() const override;
+ QStringList dependencies() const override;
+
+protected:
+ /// Initialize the module. Register the volumes reader/writer
+ void setup() override;
+
+ /// Create and return the widget representation associated to this module
+ qSlicerAbstractModuleRepresentation* createWidgetRepresentation() override;
+
+ /// Create and return the logic associated to this module
+ vtkMRMLAbstractLogic* createLogic() override;
+
+protected:
+ QScopedPointer d_ptr;
+
+private:
+ Q_DECLARE_PRIVATE(qSlicernninteractiveModule);
+ Q_DISABLE_COPY(qSlicernninteractiveModule);
+};
+
+#endif
diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx
new file mode 100644
index 0000000..2a93d76
--- /dev/null
+++ b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.cxx
@@ -0,0 +1,57 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+// Qt includes
+#include
+
+// Slicer includes
+#include "qSlicernninteractiveModuleWidget.h"
+#include "ui_qSlicernninteractiveModuleWidget.h"
+
+//-----------------------------------------------------------------------------
+class qSlicernninteractiveModuleWidgetPrivate : public Ui_qSlicernninteractiveModuleWidget
+{
+public:
+ qSlicernninteractiveModuleWidgetPrivate();
+};
+
+//-----------------------------------------------------------------------------
+// qSlicernninteractiveModuleWidgetPrivate methods
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModuleWidgetPrivate::qSlicernninteractiveModuleWidgetPrivate() {}
+
+//-----------------------------------------------------------------------------
+// qSlicernninteractiveModuleWidget methods
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModuleWidget::qSlicernninteractiveModuleWidget(QWidget* _parent)
+ : Superclass(_parent)
+ , d_ptr(new qSlicernninteractiveModuleWidgetPrivate)
+{
+}
+
+//-----------------------------------------------------------------------------
+qSlicernninteractiveModuleWidget::~qSlicernninteractiveModuleWidget() {}
+
+//-----------------------------------------------------------------------------
+void qSlicernninteractiveModuleWidget::setup()
+{
+ Q_D(qSlicernninteractiveModuleWidget);
+ d->setupUi(this);
+ this->Superclass::setup();
+}
diff --git a/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h
new file mode 100644
index 0000000..1d0aa50
--- /dev/null
+++ b/slicer_plugin/nninteractive/qSlicernninteractiveModuleWidget.h
@@ -0,0 +1,50 @@
+/*==============================================================================
+
+ Program: 3D Slicer
+
+ Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved.
+
+ See COPYRIGHT.txt
+ or http://www.slicer.org/copyright/copyright.txt for details.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+==============================================================================*/
+
+#ifndef __qSlicernninteractiveModuleWidget_h
+#define __qSlicernninteractiveModuleWidget_h
+
+// Slicer includes
+#include "qSlicerAbstractModuleWidget.h"
+
+#include "qSlicernninteractiveModuleExport.h"
+
+class qSlicernninteractiveModuleWidgetPrivate;
+class vtkMRMLNode;
+
+class Q_SLICER_QTMODULES_NNINTERACTIVE_EXPORT qSlicernninteractiveModuleWidget : public qSlicerAbstractModuleWidget
+{
+ Q_OBJECT
+
+public:
+ typedef qSlicerAbstractModuleWidget Superclass;
+ qSlicernninteractiveModuleWidget(QWidget* parent = 0);
+ virtual ~qSlicernninteractiveModuleWidget();
+
+public slots:
+
+protected:
+ QScopedPointer d_ptr;
+
+ void setup() override;
+
+private:
+ Q_DECLARE_PRIVATE(qSlicernninteractiveModuleWidget);
+ Q_DISABLE_COPY(qSlicernninteractiveModuleWidget);
+};
+
+#endif