Slicer3:Python:ScriptedModulesTipsNTricks

From Slicer Wiki
Revision as of 02:19, 13 April 2011 by Haehn (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
Home < Slicer3:Python:ScriptedModulesTipsNTricks

Back to Slicer3:Python..

Python Scripted Modules - Tips'N'Tricks

This page shows tips and tricks for writing Python Scripted Modules in Slicer3. Most examples are taken from the AtlasCreator module.

Adding your Logo to the Help&Acknowledgement Panel

It is possible to show your custom logo in the Help&Acknowledgement Panel of your Python Scripted Module.

1. Add your logo to the modules directory in .png format. 2. Edit your CMakeLists.txt and add the following section to copy the logo into Slicer's shared module directory. In this example, we include the UPenn logo:

# copy UPenn_logo.png
configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/UPenn_logo.png
  ${CMAKE_BINARY_DIR}/${Slicer3_INSTALL_MODULES_SHARE_DIR}/${PROJECT_NAME}/UPenn_logo.png
  COPYONLY)
  
install(
  FILES ${CMAKE_CURRENT_SOURCE_DIR}/UPenn_logo.png
  DESTINATION ${Slicer3_INSTALL_MODULES_SHARE_DIR}/${PROJECT_NAME}
  )

3. In your ScriptedModuleGUI class, edit the BuildGUI function and add the following code after calling self.BuildHelpAndAboutFrame:

    def BuildGUI(self):
        '''
        Creates the Graphical User Interface (GUI) of the AtlasCreator. Gets called once during loading of the module.
        '''
...
        self._helpAboutFrame = self.BuildHelpAndAboutFrame(self._atlascreatorPage,helpText,aboutText)

        # include the UPenn logo
        logoFrame = self.GetLogoFrame()
        pathToLogo = os.path.normpath(slicer.Application.GetBinDir() + '/../share/Slicer3/Modules/AtlasCreator/UPenn_logo.png')
        logo = slicer.vtkKWIcon()
        
        logoReader = slicer.vtkPNGReader()
        logoReader.SetFileName(pathToLogo)
        logoReader.Update()
        
        logo.SetImage(logoReader.GetOutput())
        
        self._logoLabel = slicer.vtkKWLabel()
        self._logoLabel.SetParent(logoFrame)
        self._logoLabel.Create()
        self._logoLabel.SetImageToIcon(logo)
        slicer.TkCall("pack %s -side top -anchor nw -fill x -padx 2 -pady 2" % self._logoLabel.GetWidgetName())
...

The logo should appear after CMake gets executed the next time.

MRML Events

There are several example on how to observe GUI events in ScriptedModules. It is also possible to observe MRML events from the MRML Scene or from a node other than the vtkMRMLScriptedModuleNode.

The MRML observers can be safely added in the AddGUIObservers method.

Observe MRML Scene events

The following example shows how to observe the MRMLScene::NodeAddedEvent which is fired when a new MRML Node gets added to the scene. Also, we observe the MRMLScene::CloseEvent to update the GUI when the MRMLScene gets closed. Please remember to use the RemoveMRMLNodeObservers method to remove the MRML Scene observers (it is called automatically on teardown of the module).

# The event ids can be found in the MRML C++ header files (f.e. Libs/MRML/vtkMRMLScene.h..)
vtkMRMLScene_NodeAddedEvent = 66000
vtkMRMLScene_CloseEvent = 66003

class AtlasCreatorGUI(ScriptedModuleGUI):

    [...]

    '''=========================================================================================='''
    def AddGUIObservers(self):
        '''
        Add the Observers. This method gets called automatically when the module gets created.
        
        For convenience, we also add the MRML observers here.
        '''
        # listen to MRML scene events
        self._mrmlNodeAddedTag = self.AddMRMLObserverByNumber(slicer.MRMLScene,vtkMRMLScene_NodeAddedEvent)
        self._mrmlSceneCloseTag = self.AddMRMLObserverByNumber(slicer.MRMLScene,vtkMRMLScene_CloseEvent)

    [...]

    '''=========================================================================================='''    
    def RemoveMRMLNodeObservers(self):
        '''
        Remove MRML Node and MRML Scene Observers
        '''
        
        self.RemoveMRMLObserverByNumber(slicer.MRMLScene,vtkMRMLScene_NodeAddedEvent)
        self.RemoveMRMLObserverByNumber(slicer.MRMLScene,vtkMRMLScene_CloseEvent)



    '''=========================================================================================='''    
    def ProcessMRMLEvents(self,callerID,event,callDataID = None):
    ''' gets called, when an observed MRML event was fired '''
    
        # observe MRMLScene events
        if callerID == "MRMLScene" and event == vtkMRMLScene_NodeAddedEvent and callDataID:
            
            callDataAsMRMLNode = slicer.MRMLScene.GetNodeByID(callDataID)
            
            if isinstance(callDataAsMRMLNode, slicer.vtkMRMLScalarVolumeNode):
                print "A new vtkMRMLScalarVolumeNode was added: " + callDataID


        # observe MRMLScene Close events
        elif callerID == "MRMLScene" and event == vtkMRMLScene_CloseEvent:

            print "The MRMLScene was closed."
            self.UpdateGUI()                

    [...]


Observe MRML Node events

In this example, an existing vtkMRMLScalarVolumeNode is observed for changes in the associated ImageData which results in a vtkMRMLScalarVolumeNode::ImageDataModifiedEvent. It is required to know the ID of the MRML node to add observers. This can be coupled with listening to the MRMLScene events above (save the ID when a new MRMLNode of a certain type was added to the scene and then create observers). Here, we add the observer also in the AddGUIObservers method but in fact this can happen everywhere - even in ProcessMRMLEvents itself.

# The event ids can be found in the MRML C++ header files (f.e. Libs/MRML/vtkMRMLVolumeNode.h)
vtkMRMLVolumeNode_ImageDataModifiedEvent = 18001


class AtlasCreatorGUI(ScriptedModuleGUI):

    [...]

    def AddGUIObservers(self):
        # first, we listen to a MRML node event
        self._scalarVolumeNode = slicer.MRMLScene.GetNodeByID("vtkMRMLScalarVolumeNode1")
        self._imagedataModifiedTag = self.AddMRMLObserverByNumber(self._scalarVolumeNode,vtkMRMLVolumeNode_ImageDataModifiedEvent)

    [...]


    def ProcessMRMLEvents(self,callerID,event,callDataID = None):
    ''' gets called, when an observed MRML event was fired '''
    
        # observe MRMLNode events
        if callerID == "vtkMRMLScalarVolumeNode1" and event == vtkMRMLVolumeNode_ImageDataModifiedEvent:
            print "ImageData of vtkMRMLScalarVolumeNode1 was modified."

    [...]

Use a custom MRML Node for your Scripted Module

By default a Scripted Module uses the vtkMRMLScriptedModuleNode as a storage container. It is also possible to create your own node for this purpose. Since MRMLNodes have to derive from a vtkMRMLNode base class and this is not possible in Python, they should be written in C++.

A full example can be found in Modules/AtlasCreator/Cxx.

1. To include C++ code in your Python Scripted Module, it makes sense to create a sub-directory Cxx/ and create a gui-less module to include and register a custom MRML Node. Instructions for gui-less modules are available here.

2. The MRML Node itself can be written by using the default practices.

3. After creating the MRML Node, the RegisterNodes() method in the logic should be used to register the custom MRML Node within the MRML Scene. This is required for Loading and Saving the scene. This is the only real code the gui-less module has to include.

//----------------------------------------------------------------------------
void vtkAtlasCreatorCxxModuleLogic::RegisterNodes()
{

  vtkMRMLScene* scene = this->GetMRMLScene();

  if (scene)
    {
    vtkMRMLAtlasCreatorNode* atlasCreatorNode = vtkMRMLAtlasCreatorNode::New();
    scene->RegisterNodeClass(atlasCreatorNode);
    atlasCreatorNode->Delete();
    }

}

4. All the pieces can be glued together with CMake which also handles the Python interface through Tcl wrapping (using Subdirs(..) and vtk_wrap_tcl3 commands).

5. In the Scripted Module observe for MRMLScene::NodeAddedEvents as seen above and save pointers to your custom MRML Node IDs. Then, the UpdateMRML() and UpdateGUI() methods should use the pointers to update the node or the GUI.

Using KWWidget Callbacks

Best practice to check if the GUI has changed is to observe the KWWidgets for triggered events. Unfortunately, this is not always possible since not all widgets fire all available events. To still be able to monitor changes in the GUI, KWWidget Callbacks can be used in connection with the Invoke method of the Scripted Module. This also helps to realize wizards in Scripted Modules :).

This example shows how to use callbacks on a vtkKWComboBoxWithLabel to monitor changes in the selection. Certain callbacks attach additional information to the method which gets fired. For example the comboBox callback adds the current selected item as String.

1. Create a method in your ScriptedModule which gets called when the comboBox changes. In connection with a comboBox callback, this method needs an additional argument.

...
    '''=========================================================================================='''            
    def UpdateMRMLFromCallback(self,dummyArgument=None):
        '''
        Just passes through to UpdateMRML.
        
        The dummyArgument is a placeholder when this function is called by a ComboBox callback and the current
        value is attached automatically by KWWidgets.
        '''
        self.UpdateMRML()
...

2. Add the callback after the widget was created using the Invoke functionality.

...
        self._toolkitCombo = slicer.vtkKWComboBoxWithLabel()
...

    '''=========================================================================================='''
    def BuildGUI(self):
        '''
        Creates the Graphical User Interface (GUI) of the AtlasCreator. Gets called once during loading of the module.
        '''
...
        self._toolkitCombo.SetParent(self._parametersFrame.GetFrame())
        self._toolkitCombo.Create()
        self._toolkitCombo.GetWidget().ReadOnlyOn()
        self._toolkitCombo.SetLabelText("Toolkit:")
        self._toolkitCombo.SetLabelWidth(20)
        self._toolkitCombo.SetBalloonHelpString("The toolkit to use for Registration.")
        slicer.TkCall("pack %s -side top -anchor nw -fill x -padx 2 -pady 2" % self._toolkitCombo.GetWidgetName())

        self._toolkitCombo.GetWidget().AddValue("'BRAINSFit'")
        self._toolkitCombo.GetWidget().AddValue("'CMTK'")
        self._toolkitCombo.GetWidget().SetValue("'BRAINSFit'")

        # the following callback invokes the UpdateMRMLFromCallback method in this module after each change
        self._toolkitCombo.GetWidget().SetCommand(self.vtkScriptedModuleGUI, "Invoke UpdateMRMLFromCallback")
...

3. In this special case, KWWidgets add the current selected item as String to the callback. The Invoke functionality can only handle strings correctly if they are wrapped in quotes as seen above (e.g. "'BRAINSFit'"..). This is not necessary for integers, for example if the Callback adds coordinates.

Writing Tests in Python

MRML Node tests or simple tests can be easily written in C++ but it is much easier to write tests which inspect observers or need the Slicer application in a scripting language. The following example shows how to test your Python Scripted Module with a combination of a C++ test and several Python tests.

1. It makes sense to create a Testing/ subdirectory.

2. Then, define a testing kit using CMake. This is the standard template for C++ driven tests.


SET(KIT AtlasCreatorLib)
SET(CMAKE_TESTDRIVER_BEFORE_TESTMAIN "DEBUG_LEAKS_ENABLE_EXIT_ERROR();" )
CREATE_TEST_SOURCELIST(Tests ${KIT}CxxTests.cxx
  vtkMRMLAtlasCreatorNodeTest1.cxx
  EXTRA_INCLUDE TestingMacros.h
  )
SET (TestsToRun ${Tests})
REMOVE (TestsToRun ${KIT}CxxTests.cxx)

SET(LIBRARY_NAME ${PROJECT_NAME})

ADD_EXECUTABLE(${KIT}CxxTests ${Tests})
TARGET_LINK_LIBRARIES(${KIT}CxxTests ${lib_name})

SET( ${KIT}_TESTS ${CXX_TEST_PATH}/${KIT}CxxTests)

IF(WIN32)
  SET(${KIT}_TESTS ${CXX_TEST_PATH}/${CMAKE_BUILD_TYPE}/${KIT}CxxTests)
ENDIF(WIN32)

MACRO( SIMPLE_TEST  TESTNAME )
  ADD_TEST( ${TESTNAME} ${LAUNCH_EXE} ${${KIT}_TESTS} ${TESTNAME} )
ENDMACRO( SIMPLE_TEST  )

SIMPLE_TEST( vtkMRMLAtlasCreatorNodeTest1 )

3. The following code can be appended to the Testing/CMakeLists.txt to include tests written in Python. Three different Python tests are appended in this case:


# the following configures the Python tests to be copied into the build directory
configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest1.py
  ${CMAKE_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest1.py
  COPYONLY)

configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest2.py
  ${CMAKE_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest2.py
  COPYONLY)

configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest3.py
  ${CMAKE_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest3.py
  COPYONLY)

install(
  FILES ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest1.py ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest2.py ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLAtlasCreatorNodeLaunchTest3.py
  DESTINATION ${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}
  )

4. The following lines are required to actually add the Python tests to the Testing kit. They will be run by using the Slicer launcher in test-mode with the -p argument for an external Python script.


# add a tcl test 
# ADD_TEST(MeasurementsTest1 ${Slicer3_BINARY_DIR}/Slicer3 --test-mode --script ${Slicer3_SOURCE_DIR}/Modules/Measurements/Testing/Tcl/MeasurementsTest1.tcl)

# add python tests by using the Slicer launcher in test-mode 
ADD_TEST(AtlasCreatorLaunchFixedTest ${Slicer3_BINARY_DIR}/Slicer3 --test-mode -p ${Slicer3_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest1.py)
ADD_TEST(AtlasCreatorLaunchFixedFailProofTest ${Slicer3_BINARY_DIR}/Slicer3 --test-mode -p ${Slicer3_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest2.py)
ADD_TEST(AtlasCreatorLaunchDynamicTest ${Slicer3_BINARY_DIR}/Slicer3 --test-mode -p ${Slicer3_BINARY_DIR}/${Slicer3_INSTALL_MODULES_LIB_DIR}/${PROJECT_NAME}/vtkMRMLAtlasCreatorNodeLaunchTest3.py)

5. To define the actual Python tests, the following template is helpful. This shows the base of vtkMRMLAtlasCreatorNodeLaunchTest1.py:

from Slicer import slicer
import sys
import os
import shutil
import tempfile

#
# AtlasCreator Test 1

# setting some paths
pathToData = os.path.normpath(slicer.Application.GetBinDir() + '/../share/Slicer3/Modules/AtlasCreator/TestData/')
segPath = pathToData + os.sep + 'segmentations' + os.sep 

# it is also possible to create an instance of the Scripted Module
pathToAtlasCreator = os.path.normpath(str(slicer.Application.GetPluginsDir())+'/../Modules/AtlasCreator')
sys.path.append(pathToAtlasCreator)
from AtlasCreatorGUI import *

# gui is the instance of the Scripted Module
gui = AtlasCreatorGUI()

...

# create the node
n = slicer.vtkMRMLAtlasCreatorNode() 

# call some methods
n.InitializeByDefault()

# test on a condition
if CONDITION:
    # all there
    ret = True
else:
    # error
    ret = False

# exit test with the corresponding exit code (0 on Success, 1 on Failure)
if ret:
    slicer.Application.Evaluate("exit 0")
else:
    slicer.Application.Evaluate("exit 1")

6. If everything is glued together properly, a make test in the ScriptedModule directory runs the C++ and Python tests together.

Running tests...
Test project /Users/daniel/SLICER/TRUNK/Slicer3-build/Modules/AtlasCreator
   Start 1: vtkMRMLAtlasCreatorNodeTest1
1/4 Test #1: vtkMRMLAtlasCreatorNodeTest1 ...........   Passed    0.70 sec
   Start 2: AtlasCreatorLaunchFixedTest
2/4 Test #2: AtlasCreatorLaunchFixedTest ............   Passed   29.78 sec
   Start 3: AtlasCreatorLaunchFixedFailProofTest
3/4 Test #3: AtlasCreatorLaunchFixedFailProofTest ...   Passed   37.10 sec
   Start 4: AtlasCreatorLaunchDynamicTest
4/4 Test #4: AtlasCreatorLaunchDynamicTest ..........   Passed   37.09 sec

Distributing Images for Testing

Sometimes it is required to distribute (small) test data with your module. Assuming the data is in a subdirectory TestData/, the following steps can be used to copy it to the shared module directory in the Slicer build tree.

1. Create a file named copydata.cmake in your module directory. This excludes .svn files from the copying.

file(COPY ${SRC} DESTINATION ${DST} PATTERN .svn EXCLUDE)

2. Add the following paragraphs to the CMakeLists.txt in your module directory:


# Include the TestData directory
ADD_CUSTOM_TARGET(copydata ALL
  ${CMAKE_COMMAND}
  -DSRC=${CMAKE_CURRENT_SOURCE_DIR}/TestData
  -DDST=${CMAKE_BINARY_DIR}/${Slicer3_INSTALL_MODULES_SHARE_DIR}/${PROJECT_NAME}/
  -P ${CMAKE_CURRENT_SOURCE_DIR}/copydata.cmake
)

install(
  DIRECTORY TestData
  DESTINATION ${Slicer3_INSTALL_MODULES_SHARE_DIR}/${PROJECT_NAME}/
  FILES_MATCHING PATTERN "*"
)

3. The path to the data can be generated in Python (f.e. in your tests) like this:

pathToData = os.path.normpath(slicer.Application.GetBinDir() + '/../share/Slicer3/Modules/MODULENAME/TestData/')