From 44945dab091a43f2bada39f78c6e56a939d333a2 Mon Sep 17 00:00:00 2001 From: Saleem Edah-Tally Date: Mon, 21 Oct 2024 17:38:41 +0200 Subject: [PATCH] Add a parameter set selector and append the result in a table. StenosisMeasurement3D - create an MRML parameter node class - do not create a default parameter node in the selector - enforce selection of a parameter set - use the parameter node in logic - append the results in an MRML table - improve existing code. --- StenosisMeasurement3D/CMakeLists.txt | 4 + StenosisMeasurement3D/Logic/CMakeLists.txt | 1 + .../vtkSlicerStenosisMeasurement3DLogic.cxx | 317 ++++++++++++++--- .../vtkSlicerStenosisMeasurement3DLogic.h | 20 +- StenosisMeasurement3D/MRML/CMakeLists.txt | 27 ++ ...MRMLStenosisMeasurement3DParameterNode.cxx | 203 +++++++++++ ...tkMRMLStenosisMeasurement3DParameterNode.h | 88 +++++ ...SlicerStenosisMeasurement3DModuleWidget.ui | 314 +++++++++++------ StenosisMeasurement3D/Widgets/CMakeLists.txt | 2 + .../qSlicerStenosisMeasurement3DModule.cxx | 2 + ...licerStenosisMeasurement3DModuleWidget.cxx | 326 +++++++++++------- ...qSlicerStenosisMeasurement3DModuleWidget.h | 30 +- 12 files changed, 1055 insertions(+), 279 deletions(-) create mode 100644 StenosisMeasurement3D/MRML/CMakeLists.txt create mode 100644 StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.cxx create mode 100644 StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.h diff --git a/StenosisMeasurement3D/CMakeLists.txt b/StenosisMeasurement3D/CMakeLists.txt index 06efbb0..dc5196d 100644 --- a/StenosisMeasurement3D/CMakeLists.txt +++ b/StenosisMeasurement3D/CMakeLists.txt @@ -7,6 +7,7 @@ string(TOUPPER ${MODULE_NAME} MODULE_NAME_UPPER) #----------------------------------------------------------------------------- add_subdirectory(Logic) +add_subdirectory(MRML) add_subdirectory(Widgets) #----------------------------------------------------------------------------- @@ -18,6 +19,8 @@ set(MODULE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/Logic ${CMAKE_CURRENT_SOURCE_DIR}/Widgets ${CMAKE_CURRENT_BINARY_DIR}/Widgets + ${CMAKE_CURRENT_SOURCE_DIR}/MRML + ${CMAKE_CURRENT_BINARY_DIR}/MRML ${ExtraMarkups_ModuleMRML_INCLUDE_DIRS} ) @@ -38,6 +41,7 @@ set(MODULE_UI_SRCS ) set(MODULE_TARGET_LIBRARIES + vtkSlicer${MODULE_NAME}ModuleMRML vtkSlicer${MODULE_NAME}ModuleLogic qSlicer${MODULE_NAME}ModuleWidgets vtkSlicerMarkupsModuleMRML diff --git a/StenosisMeasurement3D/Logic/CMakeLists.txt b/StenosisMeasurement3D/Logic/CMakeLists.txt index 42ece26..aaa4828 100644 --- a/StenosisMeasurement3D/Logic/CMakeLists.txt +++ b/StenosisMeasurement3D/Logic/CMakeLists.txt @@ -6,6 +6,7 @@ set(${KIT}_EXPORT_DIRECTIVE "VTK_SLICER_${MODULE_NAME_UPPER}_MODULE_LOGIC_EXPORT set(${KIT}_INCLUDE_DIRECTORIES ${ExtraMarkups_ModuleMRML_INCLUDE_DIRS} + ${vtkSlicer${MODULE_NAME}ModuleMRML_INCLUDE_DIRS} ) set(${KIT}_SRCS diff --git a/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.cxx b/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.cxx index a406493..00fd017 100644 --- a/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.cxx +++ b/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.cxx @@ -30,9 +30,18 @@ #include #include #include +#include +#include +#include +#include -// STD includes -#include +static const char* COLUMN_LABEL_STUDY = "Study"; +static const char* COLUMN_LABEL_WALL = "Wall"; +static const char* COLUMN_LABEL_LUMEN = "Lumen"; +static const char* COLUMN_LABEL_LESION = "Lesion"; +static const char* COLUMN_LABEL_STENOSIS = "Stenosis"; +static const char* COLUMN_LABEL_LENGTH = "Length"; +static const char* COLUMN_LABEL_NOTES = "Notes"; //---------------------------------------------------------------------------- vtkStandardNewMacro(vtkSlicerStenosisMeasurement3DLogic); @@ -67,6 +76,10 @@ void vtkSlicerStenosisMeasurement3DLogic::SetMRMLSceneInternal(vtkMRMLScene * ne void vtkSlicerStenosisMeasurement3DLogic::RegisterNodes() { assert(this->GetMRMLScene() != 0); + if (this->GetMRMLScene()) + { + this->GetMRMLScene()->RegisterNodeClass(vtkSmartPointer::New()); + } } //--------------------------------------------------------------------------- @@ -76,58 +89,84 @@ void vtkSlicerStenosisMeasurement3DLogic::UpdateFromMRMLScene() } //--------------------------------------------------------------------------- -void vtkSlicerStenosisMeasurement3DLogic -::OnMRMLSceneNodeAdded(vtkMRMLNode* vtkNotUsed(node)) +void vtkSlicerStenosisMeasurement3DLogic::OnMRMLSceneNodeAdded(vtkMRMLNode* node) { } //--------------------------------------------------------------------------- -void vtkSlicerStenosisMeasurement3DLogic -::OnMRMLSceneNodeRemoved(vtkMRMLNode* vtkNotUsed(node)) +void vtkSlicerStenosisMeasurement3DLogic::OnMRMLSceneNodeRemoved(vtkMRMLNode* vtkNotUsed(node)) { } //--------------------------------------------------------------------------- -double vtkSlicerStenosisMeasurement3DLogic::Process(vtkMRMLMarkupsShapeNode * wall, - vtkMRMLSegmentationNode * lumen, std::string segmentID, - vtkMRMLMarkupsFiducialNode * boundary, - vtkPolyData * wallOpenOut, vtkPolyData * lumenOpenOut, - vtkPolyData * wallClosedOut, vtkPolyData * lumenClosedOut) +bool vtkSlicerStenosisMeasurement3DLogic::Process(vtkVariantArray * results) { - // N.B: we don't call ::UpdateBoundaryControlPointPosition here. - if (wall == nullptr || lumen == nullptr || segmentID.empty() || boundary == nullptr - || wall->GetNumberOfControlPoints() < 4 || boundary->GetNumberOfControlPoints() < 2 - || wall->GetShapeName() != vtkMRMLMarkupsShapeNode::Tube + if (!results) + { + vtkErrorMacro("Please provide a vtkVariantArray to hold the results."); + return false; + } + vtkMRMLStenosisMeasurement3DParameterNode * parameterNode = this->ParameterNode; + if (!parameterNode) + { + vtkErrorMacro("Parameter node is NULL."); + return false; + } + vtkMRMLMarkupsShapeNode * wallShapeNode = parameterNode->GetInputShapeNode(); + vtkMRMLSegmentationNode * lumenSegmentationNode = parameterNode->GetInputSegmentationNode(); + std::string segmentID = parameterNode->GetInputSegmentID(); + vtkMRMLMarkupsFiducialNode * boundaryFiducialNode = parameterNode->GetInputFiducialNode(); + vtkPolyData * outputWallOpenPolyData = parameterNode->GetOutputWallOpenPolyData(); + vtkPolyData * outputLumenOpenPolyData = parameterNode->GetOutputLumenOpenPolyData(); + vtkPolyData * outputWallClosedPolyData = parameterNode->GetOutputWallClosedPolyData(); + vtkPolyData * outputLumenClosedPolyData = parameterNode->GetOutputLumenClosedPolyData(); + + // Note: we don't call ::UpdateBoundaryControlPointPosition here. + if (wallShapeNode == nullptr || boundaryFiducialNode == nullptr + || lumenSegmentationNode == nullptr || segmentID.empty() + || wallShapeNode->GetNumberOfControlPoints() < 4 || boundaryFiducialNode->GetNumberOfControlPoints() < 2 + || wallShapeNode->GetShapeName() != vtkMRMLMarkupsShapeNode::Tube ) { vtkErrorMacro("Invalid input."); - return -1.0; + return false; } - + + if (outputWallOpenPolyData == nullptr || outputLumenOpenPolyData == nullptr + || outputWallClosedPolyData == nullptr || outputLumenClosedPolyData == nullptr) + { + vtkErrorMacro("Invalid output: 4 polydata objects must be provided for open/closed lumen/wall output."); + return false; + } + + // Put a ficucial point on the nearest point of the wall spline. + this->UpdateBoundaryControlPointPosition(0, boundaryFiducialNode, wallShapeNode); + this->UpdateBoundaryControlPointPosition(1, boundaryFiducialNode, wallShapeNode); + // Get wall polydata from shape markups node. - vtkPolyData * wallOpenSurface = wall->GetShapeWorld(); - vtkPolyData * wallClosedSurface = wall->GetCappedTubeWorld(); + vtkPolyData * wallOpenSurface = wallShapeNode->GetShapeWorld(); + vtkPolyData * wallClosedSurface = wallShapeNode->GetCappedTubeWorld(); // Generate lumen polydata from lumen segment. vtkNew lumenSurface; - if (!lumen->GetClosedSurfaceRepresentation(segmentID, lumenSurface)) + if (!lumenSegmentationNode->GetClosedSurfaceRepresentation(segmentID, lumenSurface)) { - if (!lumen->CreateClosedSurfaceRepresentation()) + if (!lumenSegmentationNode->CreateClosedSurfaceRepresentation()) { vtkErrorMacro("Cannot create closed surface from segmentation."); - return -1.0; + return false; } - lumen->GetClosedSurfaceRepresentation(segmentID, lumenSurface); + lumenSegmentationNode->GetClosedSurfaceRepresentation(segmentID, lumenSurface); } // The first 2 fiducial points are used to cut through the lumen and wall polydata at arbitrary positions. double p1[3] = { 0.0 }; double p2[3] = { 0.0 }; - boundary->GetNthControlPointPositionWorld(0, p1); - boundary->GetNthControlPointPositionWorld(1, p2); + boundaryFiducialNode->GetNthControlPointPositionWorld(0, p1); + boundaryFiducialNode->GetNthControlPointPositionWorld(1, p2); // Get spline polydata from shape markups node. vtkSmartPointer spline = vtkSmartPointer::New(); - wall->GetTrimmedSplineWorld(spline); + wallShapeNode->GetTrimmedSplineWorld(spline); vtkPoints * splinePoints = spline->GetPoints(); // Get boundaries where polydatas will be cut. const vtkIdType p1IdType = spline->FindPoint(p1); @@ -158,25 +197,59 @@ double vtkSlicerStenosisMeasurement3DLogic::Process(vtkMRMLMarkupsShapeNode * wa // Open surface: Clip wall and lumen at p1. Clip the result at p2. vtkNew wallIntermediate; - this->Clip(wallOpenSurface, wallIntermediate, p1, startDirection, false); - this->Clip(wallIntermediate, wallOpenOut, p2, endDirection, false); + if (!this->Clip(wallOpenSurface, wallIntermediate, p1, startDirection, false)) + { + return false; + } + if (!this->Clip(wallIntermediate, outputWallOpenPolyData, p2, endDirection, false)) + { + return false; + } vtkNew lumenIntermediate; - this->Clip(lumenSurface, lumenIntermediate, p1, startDirection, false); - this->Clip(lumenIntermediate, lumenOpenOut, p2, endDirection, false); + if (!this->Clip(lumenSurface, lumenIntermediate, p1, startDirection, false)) + { + return false; + } + + if (!this->Clip(lumenIntermediate, outputLumenOpenPolyData, p2, endDirection, false)) + { + return false; + } // Closed surface - this->ClipClosed(wallClosedSurface, wallClosedOut, p1, startDirection, p2, endDirection); - this->ClipClosed(lumenSurface, lumenClosedOut, p1, startDirection, p2, endDirection); - - return this->CalculateClippedSplineLength(boundary, wall); + if (!this->ClipClosed(wallClosedSurface, outputWallClosedPolyData, p1, startDirection, p2, endDirection)) + { + return false; + } + if (!this->ClipClosed(lumenSurface, outputLumenClosedPolyData, p1, startDirection, p2, endDirection)) + { + return false; + } + + if (!this->ComputeResults(results)) + { + vtkErrorMacro("Failed to compute the results."); + return false; + } + vtkMRMLTableNode * outputTableNode = this->ParameterNode->GetOutputTableNode(); + if (results && outputTableNode) + { + if (this->DefineOutputTable()) + { + outputTableNode->GetTable()->InsertNextRow(results); + outputTableNode->Modified(); + } + } + + return true; } //--------------------------------------------------------------------------- bool vtkSlicerStenosisMeasurement3DLogic::Clip(vtkPolyData * input, vtkPolyData * output, double * origin, double * normal, bool clipped) { - if (input == nullptr || origin == NULL || normal == NULL) + if (input == nullptr || origin == nullptr || normal == nullptr || output == nullptr) { vtkErrorMacro("Can't clip, invalid parameters."); return false; @@ -191,6 +264,7 @@ bool vtkSlicerStenosisMeasurement3DLogic::Clip(vtkPolyData * input, vtkPolyData clipper->GenerateClippedOutputOn(); clipper->Update(); + output->Initialize(); if (clipped) { output->DeepCopy(clipper->GetClippedOutput()); @@ -208,7 +282,8 @@ bool vtkSlicerStenosisMeasurement3DLogic::UpdateBoundaryControlPointPosition { // Put a ficucial point on the nearest point of the wall spline. if (pointIndex < 0 || fiducialNode == nullptr || shapeNode == nullptr - || pointIndex > (fiducialNode->GetNumberOfControlPoints() - 1) ) + || pointIndex > (fiducialNode->GetNumberOfControlPoints() - 1) + || (shapeNode && shapeNode->GetShapeName() != vtkMRMLMarkupsShapeNode::Tube)) { vtkErrorMacro("Can't update control point position, invalid parameters."); return false; @@ -232,11 +307,11 @@ bool vtkSlicerStenosisMeasurement3DLogic::UpdateBoundaryControlPointPosition bool vtkSlicerStenosisMeasurement3DLogic::ClipClosed(vtkPolyData * input, vtkPolyData * output, double * startOrigin, double * startNormal, double * endOrigin, double * endNormal) { - if (input == nullptr || startOrigin == NULL || startNormal == NULL - || endOrigin == NULL || endNormal == NULL + if (input == nullptr || startOrigin == nullptr || startNormal == nullptr + || endOrigin == nullptr || endNormal == nullptr || output == nullptr ) { - vtkErrorMacro("Can't clip, invalid parameters."); + vtkErrorMacro("Can't clip closed surface, invalid parameters."); return false; } vtkNew startPlane; @@ -258,6 +333,7 @@ bool vtkSlicerStenosisMeasurement3DLogic::ClipClosed(vtkPolyData * input, vtkPol vtkNew triangleFilter; triangleFilter->SetInputData(clipper->GetOutput()); triangleFilter->Update(); + output->Initialize(); output->DeepCopy(triangleFilter->GetOutput()); return true; @@ -306,3 +382,166 @@ double vtkSlicerStenosisMeasurement3DLogic::CalculateClippedSplineLength(vtkMRML } return length; } + +//----------------------------------------------------------------------------- +void vtkSlicerStenosisMeasurement3DLogic::ProcessMRMLNodesEvents(vtkObject* caller, unsigned long event, void* callData) +{ + if (!this->ParameterNode) + { + return; + } + vtkMRMLMarkupsFiducialNode * inputFiducialNode = vtkMRMLMarkupsFiducialNode::SafeDownCast(this->ParameterNode->GetInputFiducialNode()); + vtkMRMLMarkupsShapeNode * inputShapeNode = vtkMRMLMarkupsShapeNode::SafeDownCast(this->ParameterNode->GetInputShapeNode()); + if (inputFiducialNode && inputShapeNode) + { + vtkMRMLMarkupsDisplayNode * fiducialDisplayNode = inputFiducialNode->GetMarkupsDisplayNode(); + if (!fiducialDisplayNode) + { + return; + } + this->UpdateBoundaryControlPointPosition(0, inputFiducialNode, inputShapeNode); + this->UpdateBoundaryControlPointPosition(1, inputFiducialNode, inputShapeNode); + } +} + +//----------------------------------------------------------------------------- +void vtkSlicerStenosisMeasurement3DLogic::SetParameterNode(vtkMRMLStenosisMeasurement3DParameterNode* parameterNode) +{ + if (this->ParameterNode) + { + vtkUnObserveMRMLNodeMacro(this->ParameterNode->GetInputFiducialNode()); + vtkUnObserveMRMLNodeMacro(this->ParameterNode->GetInputShapeNode()); + } + this->ParameterNode = parameterNode; + if (!this->ParameterNode) + { + return; + } + // Needed for python scripting. + if (!this->ParameterNode->GetScene()) + { + this->ParameterNode->SetScene(this->GetMRMLScene()); + } + // Put p1 and p2 ficucial points on the tube spline at nearest point when the fiducial or tube nodes are updated. + vtkNew events; + events->InsertNextValue(vtkMRMLMarkupsFiducialNode::PointEndInteractionEvent); + vtkObserveMRMLNodeEventsMacro(this->ParameterNode->GetInputFiducialNode(), events.GetPointer()); + vtkObserveMRMLNodeEventsMacro(this->ParameterNode->GetInputShapeNode(), events.GetPointer()); + // Move p1 and p2 now. + vtkMRMLMarkupsFiducialNode * inputFiducialNode = this->ParameterNode->GetInputFiducialNode(); + vtkMRMLMarkupsShapeNode * inputShapeNode = this->ParameterNode->GetInputShapeNode(); + if (inputFiducialNode && inputShapeNode && inputShapeNode->GetShapeName() == vtkMRMLMarkupsShapeNode::Tube) + { + this->UpdateBoundaryControlPointPosition(0, inputFiducialNode, inputShapeNode); + this->UpdateBoundaryControlPointPosition(1, inputFiducialNode, inputShapeNode); + } +} + +//----------------------------------------------------------------------------- +bool vtkSlicerStenosisMeasurement3DLogic::DefineOutputTable() +{ + /* + * Define an input table structure to store the results in append mode only. + */ + if (!this->ParameterNode or (this->ParameterNode && !this->ParameterNode->GetOutputTableNode())) + { + return false; + } + + vtkMRMLTableNode * outputTableNode = this->ParameterNode->GetOutputTableNode(); + + if (outputTableNode->GetNumberOfColumns() == 0) + { + vtkNew studyColumn; + vtkNew wallColumn; + vtkNew lumenColumn; + vtkNew lesionColumn; + vtkNew stenosisColumn; + vtkNew lengthColumn; + vtkNew notesColumn; + + studyColumn->SetName(COLUMN_LABEL_STUDY); + wallColumn->SetName(COLUMN_LABEL_WALL); + lumenColumn->SetName(COLUMN_LABEL_LUMEN); + lesionColumn->SetName(COLUMN_LABEL_LESION); + stenosisColumn->SetName(COLUMN_LABEL_STENOSIS); + lengthColumn->SetName(COLUMN_LABEL_LENGTH); + notesColumn->SetName(COLUMN_LABEL_NOTES); + + outputTableNode->AddColumn(studyColumn); + outputTableNode->AddColumn(wallColumn); + outputTableNode->AddColumn(lumenColumn); + outputTableNode->AddColumn(lesionColumn); + outputTableNode->AddColumn(stenosisColumn); + outputTableNode->AddColumn(lengthColumn); + outputTableNode->AddColumn(notesColumn); + + outputTableNode->SetColumnTitle(COLUMN_LABEL_STUDY, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Study")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_WALL, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Wall")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_LUMEN, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Lumen")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_LESION, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Lesion")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_STENOSIS, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Stenosis")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_LENGTH, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Length")); + outputTableNode->SetColumnTitle(COLUMN_LABEL_NOTES, vtkMRMLTr("vtkSlicerStenosisMeasurement3DLogic", "Notes")); + + outputTableNode->SetUseColumnTitleAsColumnHeader(true); + outputTableNode->Modified(); + } + return true; +} + +//----------------------------------------------------------------------------- +bool vtkSlicerStenosisMeasurement3DLogic::ComputeResults(vtkVariantArray * results) +{ + if (!this->ParameterNode || !results) + { + return false; + } + results->Initialize(); + vtkPolyData * wallClosedPolyData = this->ParameterNode->GetOutputWallClosedPolyData(); + if (!wallClosedPolyData) + { + vtkErrorMacro("Unexpected empty wall closed surface."); + return false; + } + vtkPolyData * lumenClosedPolyData = this->ParameterNode->GetOutputLumenClosedPolyData(); + if (!lumenClosedPolyData) + { + vtkErrorMacro("Unexpected empty lumen closed surface."); + return false; + } + vtkNew wallMassProperties; + wallMassProperties->SetInputData(wallClosedPolyData); + wallMassProperties->Update(); + vtkNew lumenMassProperties; + lumenMassProperties->SetInputData(lumenClosedPolyData); + lumenMassProperties->Update(); + // Get the volumes. + const double wallVolume = wallMassProperties->GetVolume(); + const double lumenVolume = lumenMassProperties->GetVolume(); + const double lesionVolume = wallVolume - lumenVolume; + // Calculate stenosis degree. + double degree = -1.0; + if (wallVolume) + { + degree = (lesionVolume / wallVolume); + } + // Get the spline length between boundary points. + double length = -1.0; + vtkMRMLMarkupsShapeNode * inputShapeNode = this->ParameterNode->GetInputShapeNode(); + vtkMRMLMarkupsFiducialNode * inputFiducialNode = this->ParameterNode->GetInputFiducialNode(); + if (inputShapeNode && inputFiducialNode) + { + length = this->CalculateClippedSplineLength(inputFiducialNode, inputShapeNode); + } + // Return the result in a variant array. + results->InsertNextValue(this->ParameterNode->GetName()); + results->InsertNextValue(wallVolume); + results->InsertNextValue(lumenVolume); + results->InsertNextValue(lesionVolume); + results->InsertNextValue(degree); + results->InsertNextValue(length); + results->InsertNextValue(""); // Notes. + + return true; +} diff --git a/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.h b/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.h index 966cb62..3df5ac8 100644 --- a/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.h +++ b/StenosisMeasurement3D/Logic/vtkSlicerStenosisMeasurement3DLogic.h @@ -36,6 +36,8 @@ #include #include #include +#include "vtkMRMLStenosisMeasurement3DParameterNode.h" +#include /// \ingroup Slicer_QtModules_ExtensionTemplate class VTK_SLICER_STENOSISMEASUREMENT3D_MODULE_LOGIC_EXPORT vtkSlicerStenosisMeasurement3DLogic : @@ -49,12 +51,16 @@ class VTK_SLICER_STENOSISMEASUREMENT3D_MODULE_LOGIC_EXPORT vtkSlicerStenosisMeas bool UpdateBoundaryControlPointPosition(int pointIndex, vtkMRMLMarkupsFiducialNode * fiducialNode, vtkMRMLMarkupsShapeNode * shapeNode); - double Process(vtkMRMLMarkupsShapeNode * wall, - vtkMRMLSegmentationNode * lumen, std::string segmentID, - vtkMRMLMarkupsFiducialNode * boundary, - vtkPolyData * wallOpenOut, vtkPolyData * lumenOpenOut, - vtkPolyData * wallClosedOut, vtkPolyData * lumenClosedOut); + bool Process(vtkVariantArray * results); + + void ProcessMRMLNodesEvents(vtkObject *caller, + unsigned long event, + void *callData ) override; + + vtkMRMLStenosisMeasurement3DParameterNode * GetParameterNode() {return ParameterNode;} + void SetParameterNode(vtkMRMLStenosisMeasurement3DParameterNode * parameterNode); + protected: vtkSlicerStenosisMeasurement3DLogic(); ~vtkSlicerStenosisMeasurement3DLogic() override; @@ -74,6 +80,10 @@ class VTK_SLICER_STENOSISMEASUREMENT3D_MODULE_LOGIC_EXPORT vtkSlicerStenosisMeas double * startOrigin, double * startNormal, double * endOrigin, double * endNormal); double CalculateClippedSplineLength(vtkMRMLMarkupsFiducialNode * fiducialNode, vtkMRMLMarkupsShapeNode * shapeNode); + bool DefineOutputTable(); + bool ComputeResults(vtkVariantArray * results); + + vtkWeakPointer ParameterNode; private: vtkSlicerStenosisMeasurement3DLogic(const vtkSlicerStenosisMeasurement3DLogic&); // Not implemented diff --git a/StenosisMeasurement3D/MRML/CMakeLists.txt b/StenosisMeasurement3D/MRML/CMakeLists.txt new file mode 100644 index 0000000..62b2c9b --- /dev/null +++ b/StenosisMeasurement3D/MRML/CMakeLists.txt @@ -0,0 +1,27 @@ +project(vtkSlicer${MODULE_NAME}ModuleMRML) + +set(KIT ${PROJECT_NAME}) + +set(${KIT}_EXPORT_DIRECTIVE "VTK_SLICER_${MODULE_NAME_UPPER}_MODULE_MRML_EXPORT") + +set(${KIT}_INCLUDE_DIRECTORIES + ${ExtraMarkups_ModuleMRML_INCLUDE_DIRS} + ) + +set(${KIT}_SRCS + vtkMRML${MODULE_NAME}ParameterNode.cxx + vtkMRML${MODULE_NAME}ParameterNode.h + ) + +set(${KIT}_TARGET_LIBRARIES + ${MRML_LIBRARIES} + ) + +#----------------------------------------------------------------------------- +SlicerMacroBuildModuleMRML( + NAME ${KIT} + EXPORT_DIRECTIVE ${${KIT}_EXPORT_DIRECTIVE} + INCLUDE_DIRECTORIES ${${KIT}_INCLUDE_DIRECTORIES} + SRCS ${${KIT}_SRCS} + TARGET_LIBRARIES ${${KIT}_TARGET_LIBRARIES} + ) diff --git a/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.cxx b/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.cxx new file mode 100644 index 0000000..259da5f --- /dev/null +++ b/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.cxx @@ -0,0 +1,203 @@ +#include "vtkMRMLStenosisMeasurement3DParameterNode.h" + +// VTK includes +#include +#include + +// MRML includes +#include +#include +#include +#include +#include + +static const char* InputShapeNodeReferenceRole = "inputShape"; +static const char* InputFiducialNodeReferenceRole = "inputFiducial"; +static const char* InputSegmentationNodeReferenceRole = "inputSegmentation"; +static const char* OutputWallModelNodeReferenceRole = "outputWallModel"; +static const char* OutputLumenModelNodeReferenceRole = "outputLumenModel"; +static const char* OutputTableNodeReferenceRole = "outputTable"; + +//---------------------------------------------------------------------------- +vtkMRMLNodeNewMacro(vtkMRMLStenosisMeasurement3DParameterNode); + +//---------------------------------------------------------------------------- +vtkMRMLStenosisMeasurement3DParameterNode::vtkMRMLStenosisMeasurement3DParameterNode() +{ + this->HideFromEditors = 1; + this->AddToSceneOn(); + + this->AddNodeReferenceRole(InputShapeNodeReferenceRole); + this->AddNodeReferenceRole(InputFiducialNodeReferenceRole); + this->AddNodeReferenceRole(InputSegmentationNodeReferenceRole); + this->AddNodeReferenceRole(OutputWallModelNodeReferenceRole); + this->AddNodeReferenceRole(OutputLumenModelNodeReferenceRole); + this->AddNodeReferenceRole(OutputTableNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLStenosisMeasurement3DParameterNode::~vtkMRMLStenosisMeasurement3DParameterNode() = default; + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetScene(vtkMRMLScene* scene) +{ + Superclass::SetScene(scene); + if (scene && !this->GetName()) + { + const std::string name = scene->GenerateUniqueName(this->GetNodeTagName()); + this->SetName(name.c_str()); + } +} + + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::PrintSelf(ostream& os, vtkIndent indent) +{ + Superclass::PrintSelf(os,indent); + vtkMRMLPrintBeginMacro(os, indent); + vtkMRMLPrintStdStringMacro(InputSegmentID); + vtkMRMLPrintEndMacro(); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::ReadXMLAttributes(const char** atts) +{ + // Read all MRML node attributes from two arrays of names and values + int disabledModify = this->StartModify(); + + Superclass::ReadXMLAttributes(atts); + + vtkMRMLReadXMLBeginMacro(atts); + vtkMRMLReadXMLStringMacro(segmentID, InputSegmentID); + vtkMRMLReadXMLEndMacro(); + + this->EndModify(disabledModify); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::WriteXML(ostream& of, int nIndent) +{ + Superclass::WriteXML(of, nIndent); + vtkMRMLWriteXMLBeginMacro(of); + vtkMRMLWriteXMLStringMacro(segmentID, InputSegmentID); + vtkMRMLWriteXMLEndMacro(); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::CopyContent(vtkMRMLNode* anode, bool deepCopy/*=true*/) +{ + MRMLNodeModifyBlocker blocker(this); + Superclass::CopyContent(anode, deepCopy); + + vtkMRMLCopyBeginMacro(anode); + vtkMRMLCopyStringMacro(InputSegmentID); + vtkMRMLCopyEndMacro(); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetInputShapeNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(InputShapeNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetInputShapeNodeID() +{ + return this->GetNodeReferenceID(InputShapeNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLMarkupsShapeNode* vtkMRMLStenosisMeasurement3DParameterNode::GetInputShapeNode() +{ + return vtkMRMLMarkupsShapeNode::SafeDownCast(this->GetNodeReference(InputShapeNodeReferenceRole)); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetInputFiducialNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(InputFiducialNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetInputFiducialNodeID() +{ + return this->GetNodeReferenceID(InputFiducialNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLMarkupsFiducialNode* vtkMRMLStenosisMeasurement3DParameterNode::GetInputFiducialNode() +{ + return vtkMRMLMarkupsFiducialNode::SafeDownCast(this->GetNodeReference(InputFiducialNodeReferenceRole)); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetInputSegmentationNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(InputSegmentationNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetInputSegmentationNodeID() +{ + return this->GetNodeReferenceID(InputSegmentationNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLSegmentationNode* vtkMRMLStenosisMeasurement3DParameterNode::GetInputSegmentationNode() +{ + return vtkMRMLSegmentationNode::SafeDownCast(this->GetNodeReference(InputSegmentationNodeReferenceRole)); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetOutputWallModelNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(OutputWallModelNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetOutputWallModelNodeID() +{ + return this->GetNodeReferenceID(OutputWallModelNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLModelNode* vtkMRMLStenosisMeasurement3DParameterNode::GetOutputWallModelNode() +{ + return vtkMRMLModelNode::SafeDownCast(this->GetNodeReference(OutputWallModelNodeReferenceRole)); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetOutputLumenModelNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(OutputLumenModelNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetOutputLumenModelNodeID() +{ + return this->GetNodeReferenceID(OutputLumenModelNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLModelNode* vtkMRMLStenosisMeasurement3DParameterNode::GetOutputLumenModelNode() +{ + return vtkMRMLModelNode::SafeDownCast(this->GetNodeReference(OutputLumenModelNodeReferenceRole)); +} + +//---------------------------------------------------------------------------- +void vtkMRMLStenosisMeasurement3DParameterNode::SetOutputTableNodeID(const char *nodeID) +{ + this->SetNodeReferenceID(OutputTableNodeReferenceRole, nodeID); +} + +//---------------------------------------------------------------------------- +const char * vtkMRMLStenosisMeasurement3DParameterNode::GetOutputTableNodeID() +{ + return this->GetNodeReferenceID(OutputTableNodeReferenceRole); +} + +//---------------------------------------------------------------------------- +vtkMRMLTableNode* vtkMRMLStenosisMeasurement3DParameterNode::GetOutputTableNode() +{ + return vtkMRMLTableNode::SafeDownCast(this->GetNodeReference(OutputTableNodeReferenceRole)); +} diff --git a/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.h b/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.h new file mode 100644 index 0000000..e2a27bc --- /dev/null +++ b/StenosisMeasurement3D/MRML/vtkMRMLStenosisMeasurement3DParameterNode.h @@ -0,0 +1,88 @@ +#ifndef __vtkmrmlstenosismeasurement3dparameternode_h_ +#define __vtkmrmlstenosismeasurement3dparameternode_h_ + +#include "vtkMRML.h" +#include "vtkMRMLScene.h" +#include "vtkMRMLNode.h" +#include "vtkSlicerStenosisMeasurement3DModuleMRMLExport.h" +#include + +class vtkMRMLMarkupsShapeNode; +class vtkMRMLMarkupsFiducialNode; +class vtkMRMLSegmentationNode; +class vtkMRMLModelNode; +class vtkMRMLTableNode; + +class VTK_SLICER_STENOSISMEASUREMENT3D_MODULE_MRML_EXPORT vtkMRMLStenosisMeasurement3DParameterNode :public vtkMRMLNode +{ +public: + static vtkMRMLStenosisMeasurement3DParameterNode *New(); + vtkTypeMacro(vtkMRMLStenosisMeasurement3DParameterNode, vtkMRMLNode); + void PrintSelf(ostream& os, vtkIndent indent) override; + + vtkMRMLNode* CreateNodeInstance() override; + void SetScene(vtkMRMLScene * scene) override; + + /// Set node attributes from XML attributes + void ReadXMLAttributes( const char** atts) override; + + /// Write this node's information to a MRML file in XML format. + void WriteXML(ostream& of, int indent) override; + + vtkMRMLCopyContentMacro(vtkMRMLStenosisMeasurement3DParameterNode); + const char* GetNodeTagName() override {return "StenosisMeasurement3DParameterSet";} + + void SetInputShapeNodeID(const char *nodeID); + const char *GetInputShapeNodeID(); + vtkMRMLMarkupsShapeNode* GetInputShapeNode(); + + void SetInputFiducialNodeID(const char *nodeID); + const char *GetInputFiducialNodeID(); + vtkMRMLMarkupsFiducialNode* GetInputFiducialNode(); + + void SetInputSegmentationNodeID(const char *nodeID); + const char *GetInputSegmentationNodeID(); + vtkMRMLSegmentationNode* GetInputSegmentationNode(); + + vtkSetStdStringFromCharMacro(InputSegmentID); + vtkGetCharFromStdStringMacro(InputSegmentID); + + void SetOutputWallModelNodeID(const char *nodeID); + const char *GetOutputWallModelNodeID(); + vtkMRMLModelNode* GetOutputWallModelNode(); + + void SetOutputLumenModelNodeID(const char *nodeID); + const char *GetOutputLumenModelNodeID(); + vtkMRMLModelNode* GetOutputLumenModelNode(); + + void SetOutputTableNodeID(const char *nodeID); + const char *GetOutputTableNodeID(); + vtkMRMLTableNode* GetOutputTableNode(); + + vtkSetObjectMacro(OutputWallOpenPolyData, vtkPolyData); + vtkGetObjectMacro(OutputWallOpenPolyData, vtkPolyData) + + vtkSetObjectMacro(OutputLumenOpenPolyData, vtkPolyData); + vtkGetObjectMacro(OutputLumenOpenPolyData, vtkPolyData); + + vtkSetObjectMacro(OutputWallClosedPolyData, vtkPolyData); + vtkGetObjectMacro(OutputWallClosedPolyData, vtkPolyData); + + vtkSetObjectMacro(OutputLumenClosedPolyData, vtkPolyData); + vtkGetObjectMacro(OutputLumenClosedPolyData, vtkPolyData); + +protected: + vtkMRMLStenosisMeasurement3DParameterNode(); + ~vtkMRMLStenosisMeasurement3DParameterNode() override; + + vtkMRMLStenosisMeasurement3DParameterNode(const vtkMRMLStenosisMeasurement3DParameterNode&); + void operator=(const vtkMRMLStenosisMeasurement3DParameterNode&); + + std::string InputSegmentID; + vtkSmartPointer OutputWallOpenPolyData; + vtkSmartPointer OutputLumenOpenPolyData; + vtkSmartPointer OutputWallClosedPolyData; + vtkSmartPointer OutputLumenClosedPolyData; +}; + +#endif // __vtkmrmlstenosismeasurement3dparameternode_h_ diff --git a/StenosisMeasurement3D/Resources/UI/qSlicerStenosisMeasurement3DModuleWidget.ui b/StenosisMeasurement3D/Resources/UI/qSlicerStenosisMeasurement3DModuleWidget.ui index c0814fa..dec9163 100644 --- a/StenosisMeasurement3D/Resources/UI/qSlicerStenosisMeasurement3DModuleWidget.ui +++ b/StenosisMeasurement3D/Resources/UI/qSlicerStenosisMeasurement3DModuleWidget.ui @@ -15,105 +15,41 @@ - + - + - Wall surface: + Parameter set: - - - Select an input shape (tube) node, drawn to represent the vascular wall. - - - - vtkMRMLMarkupsShapeNode - - - - Tube - - - true - - - false - - - true - - - true - - + + false - - - - - Select an input markups fiducial node. - -The first and second points are the boundaries between which the analysis will be performed. They should not be at the very ends of the wall surface for accurate results. + A parameter set groups parameters that define a named study distinctly. - vtkMRMLMarkupsFiducialNode + vtkMRMLStenosisMeasurement3DParameterNode - - P - - + true - - true - - - true + + - - true + + StenosisMeasurement3D true - - true - - - - - - - Boundary node: - - - - - - - Lumen surface: - - - - - - - Select a segment representing the vascular lumen. - -This should ideally exceed the wall surface a little, and must not be bifurcated for accurate results. - - - true - - - true + + true @@ -123,16 +59,129 @@ This should ideally exceed the wall surface a little, and must not be bifurcated - + + + Inputs + + + + + + Wall surface: + + + + + + + Select an input shape (tube) node, drawn to represent the vascular wall. + + + + vtkMRMLMarkupsShapeNode + + + + Tube + + + true + + + false + + + true + + + true + + + false + + + + + + + Boundary node: + + + + + + + Select an input markups fiducial node. + +The first and second points are the boundaries between which the analysis will be performed. They should not be at the very ends of the wall surface for accurate results. + + + + vtkMRMLMarkupsFiducialNode + + + + P + + + true + + + true + + + true + + + true + + + true + + + true + + + + + + + Lumen surface: + + + + + + + Select a segment representing the vascular lumen. + +This should ideally exceed the wall surface a little, and must not be bifurcated for accurate results. + + + true + + + true + + + true + + + + + + + + - Result + Output - + @@ -206,16 +255,16 @@ This should ideally exceed the wall surface a little, and must not be bifurcated - + - Model + Nodes true - + @@ -278,6 +327,43 @@ This should ideally exceed the wall surface a little, and must not be bifurcated + + + + Table: + + + + + + + Specify a table to store the result in append mode. + + + + vtkMRMLTableNode + + + + + + + Result table + + + true + + + true + + + true + + + + + + @@ -305,7 +391,9 @@ This should ideally exceed the wall surface a little, and must not be bifurcated true - Run the algorithm. + Run the algorithm. + +The result can be optionally appended to the specified output table. Apply @@ -350,7 +438,39 @@ This should ideally exceed the wall surface a little, and must not be bifurcated qSlicerStenosisMeasurement3DModuleWidget mrmlSceneChanged(vtkMRMLScene*) - inputShapeSelector + wallModelSelector + setMRMLScene(vtkMRMLScene*) + + + 309 + 342 + + + 342 + 457 + + + + + qSlicerStenosisMeasurement3DModuleWidget + mrmlSceneChanged(vtkMRMLScene*) + lumenModelSelector + setMRMLScene(vtkMRMLScene*) + + + 309 + 342 + + + 342 + 501 + + + + + qSlicerStenosisMeasurement3DModuleWidget + mrmlSceneChanged(vtkMRMLScene*) + inputSegmentSelector setMRMLScene(vtkMRMLScene*) @@ -358,8 +478,8 @@ This should ideally exceed the wall surface a little, and must not be bifurcated 159 - 303 - 25 + 324 + 130 @@ -382,7 +502,7 @@ This should ideally exceed the wall surface a little, and must not be bifurcated qSlicerStenosisMeasurement3DModuleWidget mrmlSceneChanged(vtkMRMLScene*) - inputSegmentSelector + inputShapeSelector setMRMLScene(vtkMRMLScene*) @@ -390,15 +510,15 @@ This should ideally exceed the wall surface a little, and must not be bifurcated 159 - 324 - 130 + 303 + 25 qSlicerStenosisMeasurement3DModuleWidget mrmlSceneChanged(vtkMRMLScene*) - wallModelSelector + parameterSetSelector setMRMLScene(vtkMRMLScene*) @@ -406,15 +526,15 @@ This should ideally exceed the wall surface a little, and must not be bifurcated 342 - 342 - 457 + 353 + 22 qSlicerStenosisMeasurement3DModuleWidget mrmlSceneChanged(vtkMRMLScene*) - lumenModelSelector + outputTableSelector setMRMLScene(vtkMRMLScene*) @@ -422,8 +542,8 @@ This should ideally exceed the wall surface a little, and must not be bifurcated 342 - 342 - 501 + 333 + 463 diff --git a/StenosisMeasurement3D/Widgets/CMakeLists.txt b/StenosisMeasurement3D/Widgets/CMakeLists.txt index 99d6d62..5b8630f 100644 --- a/StenosisMeasurement3D/Widgets/CMakeLists.txt +++ b/StenosisMeasurement3D/Widgets/CMakeLists.txt @@ -25,7 +25,9 @@ set(${KIT}_RESOURCES ) set(${KIT}_TARGET_LIBRARIES + ${MRML_LIBRARIES} vtkSlicer${MODULE_NAME}ModuleLogic + vtkSlicer${MODULE_NAME}ModuleMRML ) #----------------------------------------------------------------------------- diff --git a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModule.cxx b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModule.cxx index 1a0512f..92fab28 100644 --- a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModule.cxx +++ b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModule.cxx @@ -18,6 +18,8 @@ // StenosisMeasurement3D Logic includes #include +#include + // StenosisMeasurement3D includes #include "qSlicerStenosisMeasurement3DModule.h" #include "qSlicerStenosisMeasurement3DModuleWidget.h" diff --git a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.cxx b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.cxx index bd3a2b6..c5a10c7 100644 --- a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.cxx +++ b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.cxx @@ -21,6 +21,7 @@ // Slicer includes #include "qSlicerStenosisMeasurement3DModuleWidget.h" #include "ui_qSlicerStenosisMeasurement3DModuleWidget.h" +#include "vtkMRMLStenosisMeasurement3DParameterNode.h" #include #include #include @@ -31,10 +32,11 @@ #include #include #include -#include +#include #include #include #include +#include #include //----------------------------------------------------------------------------- @@ -43,6 +45,10 @@ class qSlicerStenosisMeasurement3DModuleWidgetPrivate: public Ui_qSlicerStenosis { public: qSlicerStenosisMeasurement3DModuleWidgetPrivate(); + void EnableWorkspace(); + + // It is observed to update the logic only, not to update the widgets. + vtkWeakPointer ParameterNode; }; //----------------------------------------------------------------------------- @@ -53,6 +59,21 @@ qSlicerStenosisMeasurement3DModuleWidgetPrivate::qSlicerStenosisMeasurement3DMod { } +void qSlicerStenosisMeasurement3DModuleWidgetPrivate::EnableWorkspace() +{ + /* + * We enforce explicit creation of a parameter set. + * In real work, we have to name a study immediately, the name conveying + * context, meaning and purpose, thereby preventing straying. + * A default parameter set if any will not eliminate an interaction step + * to define a study name. + */ + bool enabled = this->ParameterNode != nullptr; + this->inputsCollapsibleButton->setEnabled(enabled); + this->outputCollapsibleButton->setEnabled(enabled); + this->applyButton->setEnabled(enabled); +} + //----------------------------------------------------------------------------- // qSlicerStenosisMeasurement3DModuleWidget methods @@ -75,9 +96,9 @@ void qSlicerStenosisMeasurement3DModuleWidget::setup() Q_D(qSlicerStenosisMeasurement3DModuleWidget); d->setupUi(this); this->Superclass::setup(); + d->EnableWorkspace(); - d->resultCollapsibleButton->setCollapsed(true); - d->modelCollapsibleButton->setCollapsed(true); + d->outputCollapsibleButton->setCollapsed(true); QObject::connect(d->applyButton, SIGNAL(clicked()), this, SLOT(onApply())); @@ -87,16 +108,23 @@ void qSlicerStenosisMeasurement3DModuleWidget::setup() this, SLOT(onFiducialNodeChanged(vtkMRMLNode*))); QObject::connect(d->inputFiducialSelector, SIGNAL(nodeAddedByUser(vtkMRMLNode*)), this, SLOT(onFiducialNodeChanged(vtkMRMLNode*))); - - // Put p1 and p2 ficucial points on the tube spline at nearest point when they are moved. - this->fiducialObservation = vtkSmartPointer::New(); - this->fiducialObservation->SetClientData( reinterpret_cast(this) ); - this->fiducialObservation->SetCallback(qSlicerStenosisMeasurement3DModuleWidget::onFiducialPointEndInteraction); - - // Put p1 and p2 ficucial points on the tube spline at nearest point when the tube is updated. - this->tubeObservation = vtkSmartPointer::New(); - this->tubeObservation->SetClientData( reinterpret_cast(this) ); - this->tubeObservation->SetCallback(qSlicerStenosisMeasurement3DModuleWidget::onTubePointEndInteraction); + QObject::connect(d->inputSegmentSelector, SIGNAL(currentNodeChanged(vtkMRMLNode*)), + this, SLOT(onSegmentationNodeChanged(vtkMRMLNode*))); + QObject::connect(d->inputSegmentSelector, SIGNAL(currentSegmentChanged(QString)), + this, SLOT(onSegmentIDChanged(QString))); + QObject::connect(d->wallModelSelector, SIGNAL(currentNodeChanged(vtkMRMLNode*)), + this, SLOT(onWallModelNodeChanged(vtkMRMLNode*))); + QObject::connect(d->lumenModelSelector, SIGNAL(currentNodeChanged(vtkMRMLNode*)), + this, SLOT(onLumenModelNodeChanged(vtkMRMLNode*))); + QObject::connect(d->outputTableSelector, SIGNAL(currentNodeChanged(vtkMRMLNode*)), + this, SLOT(onTableNodeChanged(vtkMRMLNode*))); + + QObject::connect(d->parameterSetSelector, SIGNAL(currentNodeChanged(vtkMRMLNode*)), + this, SLOT(setParameterNode(vtkMRMLNode*))); + + // We won't check the structure of the table and assume it has been created in the module. + const QString attributeName = QString(MODULE_TITLE) + QString(".Role"); + d->outputTableSelector->addAttribute("vtkMRMLTableNode", attributeName, "OutputTable"); } //----------------------------------------------------------------------------- @@ -143,54 +171,41 @@ void qSlicerStenosisMeasurement3DModuleWidget::onApply() vtkSmartPointer lumenOpen = vtkSmartPointer::New(); vtkSmartPointer wallClosed = vtkSmartPointer::New(); vtkSmartPointer lumenClosed = vtkSmartPointer::New(); + d->ParameterNode->SetOutputWallOpenPolyData(wallOpen); + d->ParameterNode->SetOutputLumenOpenPolyData(lumenOpen); + d->ParameterNode->SetOutputWallClosedPolyData(wallClosed); + d->ParameterNode->SetOutputLumenClosedPolyData(lumenClosed); // Do the job. - double length = this->logic->Process(shapeNodeReal, segmentationNodeReal, currentSegmentID, - fiducialNodeReal, wallOpen, lumenOpen, wallClosed, lumenClosed); - if (length < 0.0) + // ParameterNode is set in logic by onParameterNodeModified(). + vtkNew results; + if (!this->logic->Process(results)) { this->showStatusMessage(qSlicerStenosisMeasurement3DModuleWidget::tr("Processing failed."), 5000); return; } - // Finally show result. - this->showResult(wallClosed, lumenClosed, length); + // Finally show result. An optional table is updated at the same time. + this->showResult(results); // Optionally create models. this->createModels(wallOpen, lumenOpen); } //----------------------------------------------------------------------------- -void qSlicerStenosisMeasurement3DModuleWidget::showResult(vtkPolyData * wall, vtkPolyData * lumen, - double length) +void qSlicerStenosisMeasurement3DModuleWidget::showResult(vtkVariantArray * results) { Q_D(qSlicerStenosisMeasurement3DModuleWidget); - - vtkNew wallMassProperties; - wallMassProperties->SetInputData(wall); - wallMassProperties->Update(); - vtkNew lumenMassProperties; - lumenMassProperties->SetInputData(lumen); - lumenMassProperties->Update(); - - if (wall == nullptr) - { - d->wallResultLabel->clear(); - d->lesionResultLabel->clear(); - d->stenosisResultLabel->clear(); - } - if (lumen == nullptr) - { - d->lumenResultLabel->clear(); - d->lesionResultLabel->clear(); - d->stenosisResultLabel->clear(); - } - if (wall == nullptr && lumen == nullptr) + if (!results) { + this->showStatusMessage(qSlicerStenosisMeasurement3DModuleWidget::tr("Unexpected NULL result array."), 5000); return; } - const double wallVolume = wallMassProperties->GetVolume(); - const double lumenVolume = lumenMassProperties->GetVolume(); - const double lesionVolume = wallVolume - lumenVolume; - - // Use the facilities of MRML measurement classes. + // Get the volumes. + const double wallVolume = results->GetValue(1).ToDouble(); + const double lumenVolume = results->GetValue(2).ToDouble(); + const double lesionVolume =results->GetValue(3).ToDouble(); + const double degree =results->GetValue(4).ToDouble(); + const double length =results->GetValue(5).ToDouble(); + + // Use the facilities of MRML measurement classes to format the volumes. auto show = [&] (const double& volume, QLabel * widget) { vtkNew volumeMeasurement; @@ -204,15 +219,15 @@ void qSlicerStenosisMeasurement3DModuleWidget::showResult(vtkPolyData * wall, vt widget->setToolTip(tip.c_str()); }; - d->resultCollapsibleButton->setCollapsed(false); + d->outputCollapsibleButton->setCollapsed(false); show(wallVolume, d->wallResultLabel); show(lumenVolume, d->lumenResultLabel); show(lesionVolume, d->lesionResultLabel); + // Show the stenosis degree. std::string stenosisDegree = "#ERR"; if (wallVolume > 0) { - const double degree = (lesionVolume / wallVolume); vtkNew measurement; measurement->SetValue(degree); measurement->SetDisplayCoefficient(100); @@ -225,7 +240,8 @@ void qSlicerStenosisMeasurement3DModuleWidget::showResult(vtkPolyData * wall, vt d->stenosisResultLabel->setToolTip(tip.c_str()); } d->stenosisResultLabel->setText(stenosisDegree.c_str()); - + + // Show the length of the spline between boundary points. std::string lengthMeasured = "#ERR"; if (length > 0) { @@ -297,114 +313,176 @@ bool qSlicerStenosisMeasurement3DModuleWidget::showStatusMessage(const QString& } //----------------------------------------------------------------------------- -void qSlicerStenosisMeasurement3DModuleWidget::onFiducialPointEndInteraction(vtkObject *caller, - unsigned long event, void *clientData, void *callData) +void qSlicerStenosisMeasurement3DModuleWidget::onFiducialNodeChanged(vtkMRMLNode * node) { - qSlicerStenosisMeasurement3DModuleWidget * client = reinterpret_cast(clientData); - if (!client || !client->currentShapeNode) - { - return; - } - vtkMRMLMarkupsShapeNode * shapeNode = vtkMRMLMarkupsShapeNode::SafeDownCast(client->currentShapeNode); - // React only if shape is a tube. - if (!shapeNode || shapeNode->GetShapeName() != vtkMRMLMarkupsShapeNode::Tube) - { - return; - } - vtkMRMLMarkupsFiducialNode * fiducialNode = vtkMRMLMarkupsFiducialNode::SafeDownCast(client->currentFiducialNode); - if (!fiducialNode) - { - return; - } - - vtkMRMLMarkupsDisplayNode * fiducialDisplayNode = fiducialNode->GetMarkupsDisplayNode(); - const int activeControlPoint = fiducialDisplayNode->GetActiveControlPoint(); - if (activeControlPoint > 1) + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - return; + d->ParameterNode->SetInputFiducialNodeID(node ? node->GetID() : nullptr); } - // Move the control point to closest point on spline. - client->logic->UpdateBoundaryControlPointPosition(activeControlPoint, fiducialNode, shapeNode); } //----------------------------------------------------------------------------- -void qSlicerStenosisMeasurement3DModuleWidget::onTubePointEndInteraction(vtkObject *caller, - unsigned long event, void *clientData, void *callData) +void qSlicerStenosisMeasurement3DModuleWidget::onShapeNodeChanged(vtkMRMLNode * node) { - qSlicerStenosisMeasurement3DModuleWidget * client = reinterpret_cast(clientData); - if (!client || !client->currentShapeNode) + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - return; + d->ParameterNode->SetInputShapeNodeID(node ? node->GetID() : nullptr); } - vtkMRMLMarkupsShapeNode * shapeNode = vtkMRMLMarkupsShapeNode::SafeDownCast(client->currentShapeNode); - if (!shapeNode || shapeNode->GetShapeName() != vtkMRMLMarkupsShapeNode::Tube) +} + +//----------------------------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::onSegmentationNodeChanged(vtkMRMLNode * node) +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - return; + d->ParameterNode->SetInputSegmentationNodeID(node ? node->GetID() : nullptr); } - vtkMRMLMarkupsFiducialNode * fiducialNode = vtkMRMLMarkupsFiducialNode::SafeDownCast(client->currentFiducialNode); - if (!fiducialNode) + /* + * The segmentation selector is special. + * If we don't clear it explicitly, the last segment is selected + * in many scenarios, and Apply fails nevertheless. + * Despite this clearing, the right segment ID in the parameter node + * is selected. + */ + QSignalBlocker blocker(d->inputSegmentSelector); + d->inputSegmentSelector->setCurrentSegmentID(""); +} + +//----------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::onSegmentIDChanged(QString segmentID) +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - return; + d->ParameterNode->SetInputSegmentID(segmentID.toStdString().c_str()); } - - // Move control points to closest point on spline. - client->logic->UpdateBoundaryControlPointPosition(0, fiducialNode, shapeNode); - client->logic->UpdateBoundaryControlPointPosition(1, fiducialNode, shapeNode); } //----------------------------------------------------------------------------- -void qSlicerStenosisMeasurement3DModuleWidget::onFiducialNodeChanged(vtkMRMLNode * node) +void qSlicerStenosisMeasurement3DModuleWidget::onWallModelNodeChanged(vtkMRMLNode * node) { - if (this->currentFiducialNode == node) + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - return; + d->ParameterNode->SetOutputWallModelNodeID(node ? node->GetID() : nullptr); } - if (this->currentFiducialNode) +} + +//----------------------------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::onLumenModelNodeChanged(vtkMRMLNode * node) +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - // Disconnect the currently observed node. - this->currentFiducialNode->RemoveObserver(this->fiducialObservation); + d->ParameterNode->SetOutputLumenModelNodeID(node ? node->GetID() : nullptr); } - this->currentFiducialNode = node; - if (this->currentFiducialNode) +} + +//----------------------------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::onTableNodeChanged(vtkMRMLNode * node) +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (d->ParameterNode) { - // Connect the current node. - this->currentFiducialNode->AddObserver(vtkMRMLMarkupsNode::PointEndInteractionEvent, this->fiducialObservation); + d->ParameterNode->SetOutputTableNodeID(node ? node->GetID() : nullptr); } - // Move control points to closest point on spline. - if (this->currentShapeNode && this->currentFiducialNode) +} + +//----------------------------------------------------------- +bool qSlicerStenosisMeasurement3DModuleWidget::setEditedNode(vtkMRMLNode* node, + QString role /* = QString()*/, + QString context /* = QString()*/) +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + Q_UNUSED(role); + Q_UNUSED(context); + + if (vtkMRMLStenosisMeasurement3DParameterNode::SafeDownCast(node)) { - vtkMRMLMarkupsFiducialNode * fiducialNode = vtkMRMLMarkupsFiducialNode::SafeDownCast(this->currentFiducialNode); - vtkMRMLMarkupsShapeNode * shapeNode = vtkMRMLMarkupsShapeNode::SafeDownCast(this->currentShapeNode); - this->logic->UpdateBoundaryControlPointPosition(0, fiducialNode, shapeNode); - this->logic->UpdateBoundaryControlPointPosition(1, fiducialNode, shapeNode); + d->parameterSetSelector->setCurrentNode(node); + return true; } + return false; } -//----------------------------------------------------------------------------- -void qSlicerStenosisMeasurement3DModuleWidget::onShapeNodeChanged(vtkMRMLNode * node) +//------------------------------------------------------------------------------ +void qSlicerStenosisMeasurement3DModuleWidget::setParameterNode(vtkMRMLNode* node) { - if (this->currentShapeNode == node) + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + vtkMRMLStenosisMeasurement3DParameterNode* parameterNode = vtkMRMLStenosisMeasurement3DParameterNode::SafeDownCast(node); + qvtkReconnect(d->ParameterNode, parameterNode, vtkCommand::ModifiedEvent, this, SLOT(onParameterNodeModified())); + d->ParameterNode = parameterNode; + d->EnableWorkspace(); + this->updateWidgetFromMRML(); + this->onParameterNodeModified(); +} + +//---------------------------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::updateWidgetFromMRML() +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (!d->ParameterNode) { + // Reset all UI widgets. + d->inputShapeSelector->setCurrentNode(nullptr); + d->inputFiducialSelector->setCurrentNode(nullptr); + d->inputSegmentSelector->setCurrentNode(nullptr); + d->inputSegmentSelector->setCurrentSegmentID(nullptr); + d->wallModelSelector->setCurrentNode(nullptr); + d->lumenModelSelector->setCurrentNode(nullptr); + d->outputTableSelector->setCurrentNode(nullptr); + d->wallResultLabel->clear(); + d->lumenResultLabel->clear(); + d->lesionResultLabel->clear(); + d->stenosisResultLabel->clear(); + d->lengthResultLabel->clear(); return; } - if (this->currentShapeNode) - { - // Disconnect the currently observed node. - this->currentShapeNode->RemoveObserver(this->tubeObservation); - } - this->currentShapeNode = node; - if (currentShapeNode) + + /* + * The parameter node will get in turn updated. + * Since we do not update widgets when it changes because it's private, + * we will not end in infinite recursion. + * + */ + d->inputShapeSelector->setCurrentNode(d->ParameterNode->GetInputShapeNode()); + d->inputFiducialSelector->setCurrentNode(d->ParameterNode->GetInputFiducialNode()); + /* + * The segmentation selector is special. The segmentID must be explicitly + * cleared if the segmentation is null. + */ { - // Connect the current node. - this->currentShapeNode->AddObserver(vtkMRMLMarkupsNode::PointEndInteractionEvent, this->tubeObservation); + QSignalBlocker blocker(d->inputSegmentSelector); + d->inputSegmentSelector->setCurrentNode(d->ParameterNode->GetInputSegmentationNode()); + d->inputSegmentSelector->setCurrentSegmentID(d->ParameterNode->GetInputSegmentationNode() + ? QString(d->ParameterNode->GetInputSegmentID()) + : nullptr); } - // Move control points to closest point on spline. - if (this->currentShapeNode && this->currentFiducialNode) + + d->wallModelSelector->setCurrentNode(d->ParameterNode->GetOutputWallModelNode()); + d->lumenModelSelector->setCurrentNode(d->ParameterNode->GetOutputLumenModelNode()); + d->outputTableSelector->setCurrentNode(d->ParameterNode->GetOutputTableNode()); + + /* + * Despite the MRML table, recompute is unavoidable since it stores + * results from one or many parameter sets. + */ + d->wallResultLabel->clear(); + d->lumenResultLabel->clear(); + d->lesionResultLabel->clear(); + d->stenosisResultLabel->clear(); + d->lengthResultLabel->clear(); +} + +//---------------------------------------------------------------------------- +void qSlicerStenosisMeasurement3DModuleWidget::onParameterNodeModified() +{ + Q_D(qSlicerStenosisMeasurement3DModuleWidget); + if (this->logic) { - vtkMRMLMarkupsFiducialNode * fiducialNode = vtkMRMLMarkupsFiducialNode::SafeDownCast(this->currentFiducialNode); - vtkMRMLMarkupsShapeNode * shapeNode = vtkMRMLMarkupsShapeNode::SafeDownCast(this->currentShapeNode); - this->logic->UpdateBoundaryControlPointPosition(0, fiducialNode, shapeNode); - this->logic->UpdateBoundaryControlPointPosition(1, fiducialNode, shapeNode); + this->logic->SetParameterNode(d->ParameterNode); } } - diff --git a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.h b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.h index 3466868..ad6eff6 100644 --- a/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.h +++ b/StenosisMeasurement3D/qSlicerStenosisMeasurement3DModuleWidget.h @@ -29,9 +29,9 @@ #include #include #include +#include class qSlicerStenosisMeasurement3DModuleWidgetPrivate; -class vtkMRMLNode; /// \ingroup Slicer_QtModules_ExtensionTemplate class Q_SLICER_QTMODULES_STENOSISMEASUREMENT3D_EXPORT qSlicerStenosisMeasurement3DModuleWidget : @@ -45,31 +45,33 @@ class Q_SLICER_QTMODULES_STENOSISMEASUREMENT3D_EXPORT qSlicerStenosisMeasurement qSlicerStenosisMeasurement3DModuleWidget(QWidget *parent=0); virtual ~qSlicerStenosisMeasurement3DModuleWidget(); + bool setEditedNode(vtkMRMLNode* node, QString role = QString(), QString context = QString()) override; + public slots: void onApply(); + void setParameterNode(vtkMRMLNode* node); + +protected slots: void onShapeNodeChanged(vtkMRMLNode * node); void onFiducialNodeChanged(vtkMRMLNode * node); + void onSegmentationNodeChanged(vtkMRMLNode * node); + void onSegmentIDChanged(QString segmentID); + void onWallModelNodeChanged(vtkMRMLNode * node); + void onLumenModelNodeChanged(vtkMRMLNode * node); + void onTableNodeChanged(vtkMRMLNode * node); + void onParameterNodeModified(); // Update the parameter node in the logic only. protected: QScopedPointer d_ptr; void setup() override; bool showStatusMessage(const QString& message, int duration = 0); - void showResult(vtkPolyData * wall, vtkPolyData * lumen, double lenght); + void showResult(vtkVariantArray * results); void createModels(vtkPolyData * wall, vtkPolyData * lumen); - - vtkSmartPointer fiducialObservation; - static void onFiducialPointEndInteraction(vtkObject *caller, - unsigned long event, void *clientData, void *callData); - - vtkSmartPointer tubeObservation; - static void onTubePointEndInteraction(vtkObject *caller, - unsigned long event, void *clientData, void *callData); - - vtkWeakPointer currentShapeNode; - vtkWeakPointer currentFiducialNode; + void updateWidgetFromMRML(); + vtkSmartPointer logic; - + private: Q_DECLARE_PRIVATE(qSlicerStenosisMeasurement3DModuleWidget); Q_DISABLE_COPY(qSlicerStenosisMeasurement3DModuleWidget);