Updated sample code for calling CLI via C++

Hello,
below is an updated example that shows how to call a CLI module from C++. I used the code from the link, but it seems to be quite old. So, i you want an update, please take my code (with any modification).

https://www.slicer.org/wiki/Documentation/Nightly/Developers/SlicerExecutionModel/Programmatic_Invocation

void MyWidget::generate3dModel()
{
    Q_D(MyWidget);
    vtkMRMLScene* scene = qSlicerCoreApplication::application()->mrmlScene();

    // get module and its widget
    qSlicerCLIModule* cliModule = static_cast<qSlicerCLIModule*>(qSlicerCoreApplication::application()->moduleManager()->module("GrayscaleModelMaker"));
    qSlicerCLIModuleWidget *cliWidget = static_cast<qSlicerCLIModuleWidget*>(cliModule->widgetRepresentation());
    vtkSlicerCLIModuleLogic* moduleLogic = vtkSlicerCLIModuleLogic::SafeDownCast(cliModule->logic());

    // get/create CLI module node
    vtkMRMLCommandLineModuleNode* cliNode = moduleLogic->CreateNodeInScene();

    // get/create output node
    QString outputNodeID(this->volumeNode()->GetID());
    outputNodeID += "Model";
    vtkMRMLModelNode* outputNode = vtkMRMLModelNode::SafeDownCast(scene->GetFirstNodeByName(outputNodeID.toStdString().c_str()));
    if (!outputNode)
    {
        outputNode = vtkMRMLModelNode::SafeDownCast(scene->CreateNodeByClass("vtkMRMLModelNode"));
        outputNode->SetName(outputNodeID.toStdString().c_str());
        
        // Add node to the scene
        scene->AddNode(outputNode);
        outputNode->Delete();
    }

    // extract threshold from volume display node
    vtkMRMLScalarVolumeDisplayNode* VolumeDisplayNode = vtkMRMLScalarVolumeDisplayNode::SafeDownCast(this->volumeNode()->GetDisplayNode());
    double threshold = VolumeDisplayNode->GetLowerThreshold();
    
    // set parameters
    cliNode->SetParameterAsNode("InputVolume", this->volumeNode());
    cliNode->SetParameterAsNode("OutputGeometry", outputNode);
    cliNode->SetParameterAsFloat("Threshold", threshold);
    cliNode->SetParameterAsString("Name", outputNodeID.toStdString().c_str());
    cliNode->SetParameterAsInt("Smooth", 15); // default
    cliNode->SetParameterAsFloat("Decimate", 0.25f); // default
    cliNode->SetParameterAsBool("SplitNormals", true); // default
    cliNode->SetParameterAsBool("PointNormals", true); // default

    // connect cli widget with cli module node
    cliWidget->setCurrentCommandLineModuleNode(cliNode);

    // Connect node modified event to the update the progress bar and to proceed to the next step
    this->qvtkConnect(cliNode, vtkCommand::ModifiedEvent, this, SLOT(on3dModelGenerationProgress(vtkObject*)));

    // start via logic
    moduleLogic->Apply(cliNode);
}

void MyWidget::on3dModelGenerationProgress(vtkObject* cliNode)
{
    Q_D(MyWidget);

    vtkMRMLCommandLineModuleNode * node = vtkMRMLCommandLineModuleNode::SafeDownCast(cliNode);
    if (node)
    {
        // Update Progress
        ModuleProcessInformation* info = node->GetModuleDescription().GetProcessInformation();
        switch (node->GetStatus())
        {
        case vtkMRMLCommandLineModuleNode::Cancelled:
            d->cliProgressBar->setMaximum(0);
            break;
        case vtkMRMLCommandLineModuleNode::Scheduled:
            d->cliProgressBar->setMaximum(0);
            break;
        case vtkMRMLCommandLineModuleNode::Running:
            d->cliProgressBar->setMaximum(info->Progress != 0.0 ? 100 : 0);
            d->cliProgressBar->setValue(info->Progress * 100.);
            break;
        case vtkMRMLCommandLineModuleNode::Completed:
        case vtkMRMLCommandLineModuleNode::CompletedWithErrors:
            d->cliProgressBar->reset();
            this->qvtkDisconnect(cliNode, vtkCommand::ModifiedEvent, this, SLOT(on3dModelGenerationProgress(vtkObject*)));
            break;
        default:
        case vtkMRMLCommandLineModuleNode::Idle:
            d->cliProgressBar->reset();
            break;
        }
    }
}

I have one question to this: where would be the right place to remove the vtkMRMLCommandLineModuleNode again after execution? I tried to remove it in the case vtkMRMLCommandLineModuleNode::Completed: case, but the program crashed…

1 Like

The VTK object that invoked an event should not be deleted in the callback of that event. You can set a single-shot QTimer with 0 timeout to clean up the temporary nodes after the callback is completed.

I updated the code slightly (removed my own callback and used qSlicerCLIProgressBar):

Q_D(CxScalarVolumeDisplayWidget);
    vtkMRMLScene* scene = qSlicerCoreApplication::application()->mrmlScene();

    // get module and its widget
    qSlicerCLIModule* cliModule = static_cast<qSlicerCLIModule*>(qSlicerCoreApplication::application()->moduleManager()->module("GrayscaleModelMaker"));
    qSlicerCLIModuleWidget *cliWidget = static_cast<qSlicerCLIModuleWidget*>(cliModule->widgetRepresentation());
    vtkSlicerCLIModuleLogic* moduleLogic = vtkSlicerCLIModuleLogic::SafeDownCast(cliModule->logic());

    if (!d->cliNode)
    {
        // create CLI module node
        d->cliNode = moduleLogic->CreateNodeInScene();
        d->cliNode->Register(d);

        // get/create output node
        QString outputNodeID(this->volumeNode()->GetID());
        outputNodeID += "Model";
        vtkMRMLModelNode* outputNode = vtkMRMLModelNode::SafeDownCast(scene->GetFirstNodeByName(outputNodeID.toStdString().c_str()));
        if (!outputNode)
        {
            outputNode = vtkMRMLModelNode::SafeDownCast(scene->CreateNodeByClass("vtkMRMLModelNode"));
            outputNode->SetName(outputNodeID.toStdString().c_str());
        
            // Add node to the scene
            scene->AddNode(outputNode);
            outputNode->Delete();
        }

        // extract threshold from volume display node
        vtkMRMLScalarVolumeDisplayNode* VolumeDisplayNode = vtkMRMLScalarVolumeDisplayNode::SafeDownCast(this->volumeNode()->GetDisplayNode());
        double threshold = VolumeDisplayNode->GetLowerThreshold();
    
        // set parameters
        d->cliNode->SetParameterAsNode("InputVolume", this->volumeNode());
        d->cliNode->SetParameterAsNode("OutputGeometry", outputNode);
        d->cliNode->SetParameterAsFloat("Threshold", threshold);
        d->cliNode->SetParameterAsString("Name", outputNodeID.toStdString().c_str());
        d->cliNode->SetParameterAsInt("Smooth", 15); // default
        d->cliNode->SetParameterAsFloat("Decimate", 0.25f); // default
        d->cliNode->SetParameterAsBool("SplitNormals", true); // default
        d->cliNode->SetParameterAsBool("PointNormals", true); // default

        // connect cli widget with cli module node
        cliWidget->setCurrentCommandLineModuleNode(d->cliNode);

        // connect to qSlicerCLIProgressBar
        progressBar->setCommandLineModuleNode(d->cliNode);

        // start via logic
        moduleLogic->Apply(d->cliNode);
    }

Thank you for sharing this.

It is an important design principle that all processing must be implemented in a module’s logic class, and not in the user interface. This decoupling of GUI from logic allows performing batch processing (without instantiating GUI) or using a module from another module. There are a few more similar principles - see development tutorial here.

Therefore, a few suggestions:

  • Do not retreive or use cliWidget. A module’s widget should never be used from another module.
  • Node ID gets assigned automatically when a node is added to the scene and must not be get or set manually. To create an output model node, add it to the scene, and retrieve the object point, use vtkMRMLNode* outputModelNode = scene->AddNewNodeByClass("vtkMRMLModelNode"); (this single line replaces 5-10 lines in your code)
  • All data processing should happen in your module logic, not in the GUI (widget) class, so move Generate3dModel method to your module logic and call that method from the GUI. See for example how it is done in Crop Volume module. If you need progress bar or execution in the background then you can return the created CLI module node to the caller.

Trivial: There is no “Name” parameter of Grayscale Model Maker module, so you can remove setting of that parameter.

Thanks for elaborating on this.

  • I removed the cliWidget (it was a remainder from the initial sample code, which used a vtkCommandLineModuleGUI)
  • I replaced the code for creating the output node
  • I moved the code to the logic object
  • The Grayscale Model Maker lists a parameter called Name, therefore I tried to use it:

A minimalistic example could look like this:

vtkMRMLScene* scene = qSlicerCoreApplication::application()->mrmlScene();

// get module and its logic
qSlicerCLIModule* cliModule = static_cast<qSlicerCLIModule*>(qSlicerCoreApplication::application()->moduleManager()->module("GrayscaleModelMaker"));
vtkSlicerCLIModuleLogic* moduleLogic = vtkSlicerCLIModuleLogic::SafeDownCast(cliModule->logic());

// create CLI module node
vtkMRMLCommandLineModuleNode* cliNode = moduleLogic->CreateNodeInScene();

// get/create output node
vtkMRMLModelNode* outputNode = vtkMRMLModelNode::SafeDownCast(scene->AddNewNodeByClass("vtkMRMLModelNode", "modelName"));

// extract threshold from volume display node
vtkMRMLScalarVolumeDisplayNode* VolumeDisplayNode = vtkMRMLScalarVolumeDisplayNode::SafeDownCast(volumeNode->GetDisplayNode());
double threshold = VolumeDisplayNode->GetLowerThreshold();

// set parameters
cliNode->SetParameterAsNode("InputVolume", volumeNode);
cliNode->SetParameterAsNode("OutputGeometry", outputNode);
cliNode->SetParameterAsFloat("Threshold", threshold);

// start via logic
moduleLogic->Apply(cliNode);

Thank you, I’ve updated example in the Slicer wiki (with some changes that show how processing should be implemented in the module logic).

1 Like