Difference between revisions of "2013 Project Week Breakout Session:Slicer4Python"
Line 193: | Line 193: | ||
<pre> | <pre> | ||
− | |||
− | |||
volumeNode = slicer.util.getNode(pattern="FA") | volumeNode = slicer.util.getNode(pattern="FA") | ||
logic = VolumeScrollerLogic() | logic = VolumeScrollerLogic() |
Revision as of 23:20, 12 June 2013
Home < 2013 Project Week Breakout Session:Slicer4PythonBack to Summer project week Agenda
Contents
Goals
The material here provides a guided walk through of the resources available for python scripting in Slicer 4. It is based on what is available in the nightly builds as of project week (June 17, 2013, as of slicer revision 22099). One goal is to demonstrate the development of a python scripted slicer module. Another goal is to provide A Guide to Python in Slicer for the Casual Power User which means that this walkthrough of the features of slicer should give you an idea how to make a custom module that helps organize and automate your processing. This can be useful for your own research or you can create helper modules that simplify the work for users who are exploring an algorithm or working with a large set of studies.
Topics
Creating a scripted module from templates with ModuleWizard
This topic is covered in the ModuleWizard documentation. It allows you to create a skeleton extension with any combination of modules you want.
Prerequisites
We'll go through the steps on a Mac, but the steps are essentially the same on all platforms. We'll be using a locally built version of slicer created using these instructions, but the steps can also be performed using a binary download as long as you have a checkout of the slicer source code available.
Note that this tutorial is a companion to the Hello Python Programming tutorial and the Python Scripting documentation.
Make an Extension
For this example we'll make an extension with a single scripted module as a demonstration. We run the following command from the Slicer source directory.
./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate --target ../VolumeTools VolumeTools
We call the extension "VolumeTools" because we will use it to host a demonstration scripted module that scrolls through the volumes available in the current scene.
Make a Scripted Module
Now we want to put a scripted module inside the extension. We can do this with this command (again from the Slicer source directory)
./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate --target ../VolumeTools/VolumeScroller VolumeScroller
Note that we've used the Module template, which is inside the Extension template and directed the result to go inside of our new extension.
Set up the CMakeLists.txt file
Since the template included a stand-in scripted module, we want to delete it and tell CMake to use our newly created VolumeScroller module instead. We also want to get rid of the dummy module. These commands on unix can do this:
rm -rf ../VolumeTools/ScriptedLoadableModuleTemplate perl -pi -e 's/ScriptedLoadableModuleTemplate/VolumeScroller/g' ../VolumeTools/CMakeLists.txt
Note that you'll actually want to edit the CMakeLists.txt file by hand, since it contains the metadata about your extension, like the author, category, documentation URL, etc.
Configuring slicer to use the module
You are now ready to test your module. The easiest way to do this is by specifying the path on the command line, like this:
./Slicer-build/Slicer --additional-module-paths ../VolumeTools/VolumeScroller
The path above assumes you are in the Slicer-superbuild directory of a local build directory, but you can substitute the appropriate path to start slicer.
From there, you should find the VolumeScroller module in the Examples category of the module menu. Congratulations! You are now ready to start programming your scripted module.
Notes for "Real World" usage
For the purposes of this tutorial we don't worry about some things. But if you are planning to complete the process of making this into an extension you should refer to the information about how to build the module with CMake, how to bundle the module, and other extension-related topics on the "How To" column on the right of the slicer developer documentation page. Even though we are making a scripted extension, as of now it is still necessary to use CMake and have a local build tree for at least your initial submission of an extension.
Also we would suggest you start using git for your extension directory from the beginning so that you have a complete log of your development. See github for a free repository hosting site and some we links to git resources.
Basic Development Cycle
Accessing your module via the python console
As a learning experiment, let's try manipulating our widget within the runtime environment. This is a very powerful feature of the python scripted modules in slicer, and as you develop you'll find yourself using this a lot for debugging and for exploring new features.
The first thing to do is to bring up the python console with the View->Python Interactor menu (or the hotkey Control-3/Command-3). We'll spend a lot of time in here during the tutorial.
In the console, you can access the following object:
slicer.modules.VolumeScrollerWidget
which is the instance of our scripted modules widget. Note that this console has tab completion and other nice features. Using this we can access any of the fields of the interface, and even manipulate them. Try some of the following while watching the interface:
b = slicer.modules.VolumeScrollerWidget.applyButton.enabled = True b.enabled = True b.down = True b.down = False b.clicked()
Note how invoking the 'clicked()' method caused the 'Run the algorithm' message? This is because the clicked method is a signal for the button and it is connected to a python callable that is part of our scripted module.
You'll find that on line 132 of our VolumeScroller.py:
self.applyButton.connect('clicked(bool)', self.onApplyButton)
which makes the connection to the onApplyButton method on line 142.
Edit / Reload the code
The Reload & Test Collapsible Box
Notice the two big buttons "Reload" and "Reload and Test" buttons. These are useful during your development. Here's what they do:
- Reload:
- removes the current instance of the modules GUI
- reloads the python source code
- re-creates the GUI in place of the old one
- updates slicer.modules.<moduleName>Widget to point to the new instance
- Reload and Test:
- performs the Reload
- invokes whatever test code you have defined
By default, the test will download a sample dataset and confirm that it was loaded. We'll talk more about the contents of the test below
Reloading
If you click the Reload button nothing much will seem to have happened, just a little flash of the GUI. But this is a very powerful button. If you edit the file VolumeScroller.py you can change the code an reload it. Of course you'd want to use your favorite text editor for this, but as an example, you can run this command from your Slicer directory:
perl -pi -e 's/Parameters/ParametersForMyKillerAlgorithm/g' ../VolumeTools/VolumeScroller/VolumeScroller.py
and when you click the Reload button: Voila! Isn't that just awesome?!? This is just the tip of the iceberg really, since you can completely redefine the GUI incrementally, by adding new widgets, changing the callbacks, updating the layout. And all of it without even needing to exit slicer.
Advanced note: here we pointed slicer to the source directory for the scripted module, so that is the code that is reloaded. If you compiled the extension and pointed slicer to the build tree, then you'd need to edit the .py file from the build tree in order to use the Reload button. You can do this if it's convenient, but don't forget to copy the files back to your source tree or they might be lost.
Refine functionality
At this point it is a "simple matter" of writing the module code to implement your desired functionality.
We won't go through all the steps, but let's start by replacing the existing GUI with something that implements the VolumeScroller interface.
Let's start by replacing lines 81-148 of the template with the following code and then the interface.
# # Volume Scrolling Area # scrollingCollapsibleButton = ctk.ctkCollapsibleButton() scrollingCollapsibleButton.text = "Volume Scrolling" self.layout.addWidget(scrollingCollapsibleButton) # Layout within the scrolling collapsible button scrollingFormLayout = qt.QFormLayout(scrollingCollapsibleButton) # volume selection scroller self.slider = ctk.ctkSliderWidget() self.slider.decimals = 0 self.slider.enabled = False scrollingFormLayout.addRow("Volume", self.slider) # refresh button self.refreshButton = qt.QPushButton("Refresh") scrollingFormLayout.addRow(self.refreshButton) # make connections self.slider.connect('valueChanged(double)', self.onSliderValueChanged) self.refreshButton.connect('clicked()', self.onRefresh) # make an instance of the logic for use by the slots self.logic = VolumeScrollerLogic() # call refresh the slider to set it's initial state self.onRefresh() # Add vertical spacer self.layout.addStretch(1) def onSliderValueChanged(self,value): self.logic.selectVolume(int(value)) def onRefresh(self): volumeCount = self.logic.volumeCount() self.slider.enabled = volumeCount > 0 self.slider.maximum = volumeCount def cleanup(self): pass
You'll see the new interface. You can try to click on the Refresh button, but you'll see an AttributeError in the python console. This is because we have not yet implemented the logic methods. Let's do that now.
For the logic, replace lines 222-239 with the following text:
def volumeCount(self): return len(slicer.util.getNodes('vtkMRML*VolumeNode*')) def selectVolume(self,index): node = slicer.util.getNode('vtkMRML*VolumeNode*',index) selectionNode = slicer.app.applicationLogic().GetSelectionNode() selectionNode.SetReferenceActiveVolumeID( node.GetID() ) slicer.app.applicationLogic().PropagateVolumeSelection(0)
Now we can test this manually by adding a bunch of volumes to the scene. Then click the Refresh button and drag the slider.
Advanced notes:
- Instead of using the Refresh button, we could attach the onRefresh callable to update automatically when the MRML scene invokes a vtkMRMLScene::EndBatchProcessEvent, vtkMRMLScene::NodeAddedEvent or vtkMRMLScene::NodeRemovedEvent event.
- The slicer.util.getNode(s) methods rely on the MRML scene allocating node IDs with the class name as the prefix. This is established behavior that you can rely on so that pattern matching constructs can be used as in the example to select nodes of various types.
What did we just do?
Develop self-tests
Scripted module self-tests are very powerful in a number of contexts:
- During development you can use them to automatically load data and invoke your code. This helps you debug and incrementally develop your code in terms of a working test case.
- The CMakeLists.txt of the scripted module template includes the needed directive so that your self test will be part of the ctest testing of your code. This means that when the code is submitted as a slicer extension, these tests will be run automatically and will be reported on the dashboard. This means that you will be able to confirm correct behavior of your module on different platforms, and your tests will confirm your module is still working as the slicer core is updated.
- The test is registered with the Testing->Self Tests module so that it can be invoked by users. This means that if you get an error report from a user, you can direct them to run the self test to confirm that the basic operations are working and can narrow down to issues related to their data. Or if the self-test fails, then you can investigate why their OS, graphics hardware, or other specific issue is causing the test to fail for them but pass for you.
To implement a self test for this, let's replace lines 306-309 at the bottom of the file with:
volumeNode = slicer.util.getNode(pattern="FA") logic = VolumeScrollerLogic() volumesLogic = slicer.modules.volumes.logic() for sigma in range(5): self.delayDisplay('Making blurred volume with sigma of %d\n' % sigma) outputVolume = volumesLogic.CloneVolume(slicer.mrmlScene, volumeNode, 'blur-%d' % sigma) parameters = { "inputVolume": slicer.util.getNode('FA'), "outputVolume": outputVolume, "sigma": sigma, } blur = slicer.modules.gaussianblurimagefilter slicer.cli.run(blur, None, parameters, wait_for_completion=True) slicer.modules.VolumeScrollerWidget.onRefresh() self.delayDisplay('Selecting original volume') slicer.modules.VolumeScrollerWidget.onSliderValueChanged(0) self.delayDisplay('Selecting final volume') slicer.modules.VolumeScrollerWidget.onSliderValueChanged(5) selectionNode = slicer.app.applicationLogic().GetSelectionNode() if selectionNode.GetActiveVolumeID() != volumeNode.GetID(): raise Exception("Volume ID was not selected!") self.delayDisplay('Test passed!')