Difference between revisions of "Slicer3:Python:ScriptedModulesTipsNTricks"
(9 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
+ | Back to [[Slicer3:Python]].. | ||
+ | |||
= Python Scripted Modules - Tips'N'Tricks = | = Python Scripted Modules - Tips'N'Tricks = | ||
− | This page shows tips and tricks for writing Python Scripted Modules in Slicer3. | + | 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: | ||
+ | |||
+ | <pre> | ||
+ | # 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} | ||
+ | ) | ||
+ | </pre> | ||
+ | |||
+ | 3. In your ScriptedModuleGUI class, edit the BuildGUI function and add the following code '''after calling ''self.BuildHelpAndAboutFrame''''': | ||
+ | |||
+ | <pre> | ||
+ | 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()) | ||
+ | ... | ||
+ | </pre> | ||
− | + | The logo should appear after CMake gets executed the next time. | |
== MRML Events == | == 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 === | === 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). | ||
+ | |||
+ | <pre> | ||
+ | # 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() | ||
+ | |||
+ | [...] | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | |||
=== Observe MRML Node events === | === 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. | ||
+ | |||
+ | <pre> | ||
+ | # 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." | ||
+ | |||
+ | [...] | ||
+ | |||
+ | </pre> | ||
+ | |||
=== Use a custom MRML Node for your Scripted Module === | === 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 [[Slicer3:Loadable_Modules:HOWTO|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. | ||
+ | |||
+ | <pre> | ||
+ | //---------------------------------------------------------------------------- | ||
+ | void vtkAtlasCreatorCxxModuleLogic::RegisterNodes() | ||
+ | { | ||
+ | |||
+ | vtkMRMLScene* scene = this->GetMRMLScene(); | ||
+ | |||
+ | if (scene) | ||
+ | { | ||
+ | vtkMRMLAtlasCreatorNode* atlasCreatorNode = vtkMRMLAtlasCreatorNode::New(); | ||
+ | scene->RegisterNodeClass(atlasCreatorNode); | ||
+ | atlasCreatorNode->Delete(); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | 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 == | == 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. | ||
+ | |||
+ | <pre> | ||
+ | ... | ||
+ | '''==========================================================================================''' | ||
+ | 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() | ||
+ | ... | ||
+ | </pre> | ||
+ | |||
+ | 2. Add the callback after the widget was created using the '''Invoke''' functionality. | ||
+ | |||
+ | <pre> | ||
+ | ... | ||
+ | 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") | ||
+ | ... | ||
+ | </pre> | ||
+ | |||
+ | 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 == | == 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. | ||
+ | |||
+ | <pre> | ||
+ | |||
+ | 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 ) | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <pre> | ||
+ | |||
+ | # 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} | ||
+ | ) | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 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. | ||
+ | |||
+ | <pre> | ||
+ | |||
+ | # 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) | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 5. To define the '''actual Python tests''', the following template is helpful. This shows the base of vtkMRMLAtlasCreatorNodeLaunchTest1.py: | ||
+ | |||
+ | <pre> | ||
+ | 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") | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 6. If everything is glued together properly, a '''make test''' in the ScriptedModule directory runs the C++ and Python tests together. | ||
+ | |||
+ | <pre> | ||
+ | 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 | ||
+ | </pre> | ||
+ | |||
+ | === 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. | ||
+ | |||
+ | <pre> | ||
+ | file(COPY ${SRC} DESTINATION ${DST} PATTERN .svn EXCLUDE) | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 2. Add the following paragraphs to the CMakeLists.txt in your module directory: | ||
+ | |||
+ | <pre> | ||
+ | |||
+ | # 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 "*" | ||
+ | ) | ||
+ | |||
+ | </pre> | ||
+ | |||
+ | 3. The path to the data can be generated in Python (f.e. in your tests) like this: | ||
+ | |||
+ | <pre> | ||
+ | pathToData = os.path.normpath(slicer.Application.GetBinDir() + '/../share/Slicer3/Modules/MODULENAME/TestData/') | ||
+ | </pre> |
Latest revision as of 02:19, 13 April 2011
Home < Slicer3:Python:ScriptedModulesTipsNTricksBack 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/')