From a98459d9d10ad819d2f64219e17d14704d00d29f Mon Sep 17 00:00:00 2001 From: Francois Budin Date: Wed, 11 May 2016 14:26:50 -0400 Subject: [PATCH 1/4] ENH: Addition of ctk_cli ctk_cli [1] is a command line parsing python package that parses arguments the same way SlicerExecutionModel does. Using ctk_cli allows in Python scripts allows to have the same user interface in Python or in C++. [1] https://github.com/commontk/ctk-cli --- SuperBuild.cmake | 2 +- SuperBuild/External_python-ctk_cli.cmake | 36 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 SuperBuild/External_python-ctk_cli.cmake diff --git a/SuperBuild.cmake b/SuperBuild.cmake index c1c40204dc3..c5da79be547 100644 --- a/SuperBuild.cmake +++ b/SuperBuild.cmake @@ -100,7 +100,7 @@ if(Slicer_USE_SimpleITK) endif() if(Slicer_BUILD_CLI_SUPPORT) - list(APPEND Slicer_DEPENDENCIES SlicerExecutionModel) + list(APPEND Slicer_DEPENDENCIES SlicerExecutionModel python-ctk_cli) endif() if(Slicer_BUILD_EXTENSIONMANAGER_SUPPORT) diff --git a/SuperBuild/External_python-ctk_cli.cmake b/SuperBuild/External_python-ctk_cli.cmake new file mode 100644 index 00000000000..64c37751c2f --- /dev/null +++ b/SuperBuild/External_python-ctk_cli.cmake @@ -0,0 +1,36 @@ +set(proj python-ctk_cli) + +# Set dependency list +set(${proj}_DEPENDENCIES python python-setuptools) + +# Include dependent projects if any +ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj DEPENDS_VAR ${proj}_DEPENDENCIES) + +if(${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) + # XXX - Add a test checking if is available +endif() + +if(NOT DEFINED ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) + set(${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj} ${${CMAKE_PROJECT_NAME}_USE_SYSTEM_python}) +endif() + +set(ctk_cli_REPOSITORY ${git_protocol}://github.com/commontk/ctk-cli.git) +set(ctk_cli_GIT_TAG 4346a22618b299c8eff84c6a179067ca68db43b9) + +if(NOT ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) + + ExternalProject_Add(${proj} + ${${proj}_EP_ARGS} + GIT_REPOSITORY ${ctk_cli_REPOSITORY} + GIT_TAG ${ctk_cli_GIT_TAG} + SOURCE_DIR ${proj} + BUILD_IN_SOURCE 1 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND ${PYTHON_EXECUTABLE} setup.py install + DEPENDS + ${${proj}_DEPENDENCIES} + ) +else() + ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDENCIES}) +endif() From bae311d3d98ebd77ff53029d754775ccc5e88da6 Mon Sep 17 00:00:00 2001 From: Francois Budin Date: Mon, 16 May 2016 16:08:36 -0400 Subject: [PATCH 2/4] BUG: Template argument name was modified in template declaration Template argument name was modified in template declaration but not when using it in the templated function. --- Utilities/Templates/Modules/CLI/TemplateKey.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utilities/Templates/Modules/CLI/TemplateKey.cxx b/Utilities/Templates/Modules/CLI/TemplateKey.cxx index 95b183e9613..1729d2734e4 100644 --- a/Utilities/Templates/Modules/CLI/TemplateKey.cxx +++ b/Utilities/Templates/Modules/CLI/TemplateKey.cxx @@ -15,7 +15,7 @@ namespace { template -int DoIt( int argc, char * argv[], T ) +int DoIt( int argc, char * argv[], TPixel ) { PARSE_ARGS; From 4b216a9f429efd9886d20af275fe9bd9e7052c9e Mon Sep 17 00:00:00 2001 From: Francois Budin Date: Mon, 16 May 2016 16:16:21 -0400 Subject: [PATCH 3/4] ENH: Addition of an example to use ctk_cli ctk_cli allows to parse python scripts command lines the same way they would be parsed with SlicerExecutionModel. This commit adds an example to show how to use this package. --- CMake/SlicerExtensionTemplatesGenerator.cmake | 2 +- .../Modules/PythonCLI/CMakeLists.txt | 24 +++++++++ .../PythonCLIModuleTemplateTest.nhdr.md5 | 1 + .../PythonCLIModuleTemplateTest.raw.md5 | 1 + .../PythonCLI/Data/Input/CTHeadAxial.nhdr.md5 | 1 + .../Data/Input/CTHeadAxial.raw.gz.md5 | 1 + .../PythonCLI/PythonCLIModuleTemplate.py | 36 +++++++++++++ .../PythonCLI/PythonCLIModuleTemplate.xml | 52 +++++++++++++++++++ .../Modules/PythonCLI/Testing/CMakeLists.txt | 1 + .../PythonCLI/Testing/Python/CMakeLists.txt | 17 ++++++ 10 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 Utilities/Templates/Modules/PythonCLI/CMakeLists.txt create mode 100644 Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 create mode 100644 Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 create mode 100644 Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.nhdr.md5 create mode 100644 Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.raw.gz.md5 create mode 100644 Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.py create mode 100644 Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.xml create mode 100644 Utilities/Templates/Modules/PythonCLI/Testing/CMakeLists.txt create mode 100644 Utilities/Templates/Modules/PythonCLI/Testing/Python/CMakeLists.txt diff --git a/CMake/SlicerExtensionTemplatesGenerator.cmake b/CMake/SlicerExtensionTemplatesGenerator.cmake index 89ae8a8fa4e..5d6334db1d4 100644 --- a/CMake/SlicerExtensionTemplatesGenerator.cmake +++ b/CMake/SlicerExtensionTemplatesGenerator.cmake @@ -81,7 +81,7 @@ macro(_append_extension_template_generator_commands module_type) endmacro() # Loop over module type and add template generators -foreach(type IN ITEMS CLI Loadable ScriptedLoadable) +foreach(type IN ITEMS CLI PythonCLI Loadable ScriptedLoadable) _append_extension_template_generator_commands(${type}) endforeach() diff --git a/Utilities/Templates/Modules/PythonCLI/CMakeLists.txt b/Utilities/Templates/Modules/PythonCLI/CMakeLists.txt new file mode 100644 index 00000000000..b6cc49745bd --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/CMakeLists.txt @@ -0,0 +1,24 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME PythonCLIModuleTemplate) + +#----------------------------------------------------------------------------- +set(MODULE_PYTHON_SCRIPTS + ${MODULE_NAME}.py + ) + +set(MODULE_PYTHON_RESOURCES + ${MODULE_NAME}.xml + ) + +#----------------------------------------------------------------------------- +slicerMacroBuildScriptedModule( + NAME ${MODULE_NAME} + SCRIPTS ${MODULE_PYTHON_SCRIPTS} + RESOURCES ${MODULE_PYTHON_RESOURCES} + ) + +#----------------------------------------------------------------------------- +if(BUILD_TESTING) + # Additional build-time testing + add_subdirectory(Testing) +endif() diff --git a/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 b/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 new file mode 100644 index 00000000000..e3f7f4074cd --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 @@ -0,0 +1 @@ +fc6170ceeff3d8217a9dd6a1add2ec8c diff --git a/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 b/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 new file mode 100644 index 00000000000..6e640c3071f --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 @@ -0,0 +1 @@ +0749d4d3f07a217030f9ae33d94c4559 \ No newline at end of file diff --git a/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.nhdr.md5 b/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.nhdr.md5 new file mode 100644 index 00000000000..734a0467eee --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.nhdr.md5 @@ -0,0 +1 @@ +6e5c289c73e14ba7a1b0f8aaf6ed249a \ No newline at end of file diff --git a/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.raw.gz.md5 b/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.raw.gz.md5 new file mode 100644 index 00000000000..f555cb43501 --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Data/Input/CTHeadAxial.raw.gz.md5 @@ -0,0 +1 @@ +3ebd710c9cf9d75750f4569b8caf6d07 \ No newline at end of file diff --git a/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.py b/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.py new file mode 100644 index 00000000000..841dd166246 --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.py @@ -0,0 +1,36 @@ +import itk +from ctk_cli import * +import sys +import logging + +def SmoothingRecursiveGaussianImageFilter(inputVolume, outputVolume, sigma): + reader = itk.ImageFileReader.New(FileName=inputVolume) + filter = itk.SmoothingRecursiveGaussianImageFilter.New(reader) + filter.SetSigma(sigma) + writer = itk.ImageFileWriter.New(filter,FileName=outputVolume) + writer.SetUseCompression(True) + writer.Update() + return 1 + + +def main(): + """Parsing command line arguments and reading input files.""" + logging.basicConfig(level=logging.INFO) + args=CLIArgumentParser().parse_args() + # Run processing + SmoothingRecursiveGaussianImageFilter(args.inputVolume,args.outputVolume,args.sigma) + # Compare output with baseline + reader1 = itk.ImageFileReader.New(FileName=args.outputVolume) + reader2 = itk.ImageFileReader.New(FileName=args.baselineVolume) + compareFilter=itk.ComparisonImageFilter.New(reader1) + compareFilter.SetTestInput(reader1) + compareFilter.SetValidInput(reader2) + diff=compareFilter.GetTotalDifference() + if diff < args.tolerance: + return 0 + return 1 + +if __name__ == "__main__": + ret=main() + if ret: + sys.exit(ret) diff --git a/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.xml b/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.xml new file mode 100644 index 00000000000..604f1421566 --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/PythonCLIModuleTemplate.xml @@ -0,0 +1,52 @@ + + + Examples + PythonCLIModuleTemplate + + 0.0.1 + http://www.example.com/Slicer/Modules/PythonCLIModuleTemplate + Slicer + FirstName LastName (Institution), FirstName LastName (Institution) + This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN + + + + + sigma + sigma + s + + + 1.0 + + + inputVolume + + input + 0 + + + + outputVolume + + output + 1 + + + + baselineVolume + + input + 2 + + + + tolerance + tolerance + t + + + 0.0001 + + + diff --git a/Utilities/Templates/Modules/PythonCLI/Testing/CMakeLists.txt b/Utilities/Templates/Modules/PythonCLI/Testing/CMakeLists.txt new file mode 100644 index 00000000000..655007a0aab --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Python) diff --git a/Utilities/Templates/Modules/PythonCLI/Testing/Python/CMakeLists.txt b/Utilities/Templates/Modules/PythonCLI/Testing/Python/CMakeLists.txt new file mode 100644 index 00000000000..82ea85b1231 --- /dev/null +++ b/Utilities/Templates/Modules/PythonCLI/Testing/Python/CMakeLists.txt @@ -0,0 +1,17 @@ +#----------------------------------------------------------------------------- +set(BASELINE ${CMAKE_CURRENT_SOURCE_DIR}/../../Data/Baseline) +set(INPUT ${CMAKE_CURRENT_SOURCE_DIR}/../../Data/Input) +set(TEMP "${CMAKE_BINARY_DIR}/Testing/Temporary") + +set(CLP ${MODULE_NAME}) + +#----------------------------------------------------------------------------- +set(testname ${CLP}Test) +ExternalData_add_test(${CLP}Data NAME ${testname} COMMAND ${Slicer_LAUNCHER_EXECUTABLE} ${MODULE_NAME}.py + --sigma 2.5 DATA{${INPUT}/CTHeadAxial.nhdr,CTHeadAxial.raw.gz} ${TEMP}/${CLP}Test.nhdr + DATA{${BASELINE}/${CLP}Test.nhdr,${CLP}Test.raw} + ) +set_property(TEST ${testname} PROPERTY LABELS ${CLP}) + +#----------------------------------------------------------------------------- +ExternalData_add_target(${CLP}Data) From 70a7aff60fa995b16109abd7e35953c78e74fb36 Mon Sep 17 00:00:00 2001 From: Francois Budin Date: Mon, 16 May 2016 17:23:57 -0400 Subject: [PATCH 4/4] ENH: Generating new extension based on new module type. Generating a new extension implementing a PythonCLI module by calling the target SlicerGenerateExtensionTemplates. --- Extensions/PythonCLIExtensionTemplate.s4ext | 44 +++++++++++++++ .../CLIModuleTemplate/CLIModuleTemplate.cxx | 2 +- .../PythonCLIExtensionTemplate/CMakeLists.txt | 25 +++++++++ .../PythonCLIExtensionTemplate.png | Bin 0 -> 17838 bytes .../PythonCLIModuleTemplate/CMakeLists.txt | 24 ++++++++ .../PythonCLIModuleTemplateTest.nhdr.md5 | 1 + .../PythonCLIModuleTemplateTest.raw.md5 | 1 + .../Data/Input/CTHeadAxial.nhdr.md5 | 1 + .../Data/Input/CTHeadAxial.raw.gz.md5 | 1 + .../PythonCLIModuleTemplate.py | 36 ++++++++++++ .../PythonCLIModuleTemplate.xml | 52 ++++++++++++++++++ .../Testing/CMakeLists.txt | 1 + .../Testing/Python/CMakeLists.txt | 17 ++++++ 13 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 Extensions/PythonCLIExtensionTemplate.s4ext create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/CMakeLists.txt create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIExtensionTemplate.png create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/CMakeLists.txt create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.nhdr.md5 create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.raw.gz.md5 create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.py create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.xml create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/CMakeLists.txt create mode 100644 Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/Python/CMakeLists.txt diff --git a/Extensions/PythonCLIExtensionTemplate.s4ext b/Extensions/PythonCLIExtensionTemplate.s4ext new file mode 100644 index 00000000000..2ce8e785a1a --- /dev/null +++ b/Extensions/PythonCLIExtensionTemplate.s4ext @@ -0,0 +1,44 @@ +# +# First token of each non-comment line is the keyword and the rest of the line +# (including spaces) is the value. +# - the value can be blank +# + +# This is source code manager (i.e. svn) +scm local +scmurl Testing/PythonCLIExtensionTemplate +scmrevision NA + +# list dependencies +# - These should be names of other modules that have .s4ext files +# - The dependencies will be built first +depends NA + +# Inner build directory (default is ".") +build_subdirectory . + +# homepage +homepage http://slicer.org/slicerWiki/index.php/Documentation/Nightly/Extensions/PythonCLIExtensionTemplate + +# Firstname1 Lastname1 ([SubOrg1, ]Org1), Firstname2 Lastname2 ([SubOrg2, ]Org2) +# For example: Jane Roe (Superware), John Doe (Lab1, Nowhere), Joe Bloggs (Noware) +contributors John Doe (AnyWare Corp.) + +# Match category in the xml description of the module (where it shows up in Modules menu) +category Examples + +# url to icon (png, size 128x128 pixels) +iconurl http://www.example.com/Slicer/Extensions/PythonCLIExtensionTemplate.png + +# Give people an idea what to expect from this code +# - Is it just a test or something you stand behind? +status + +# One line stating what the module does +description This is an example of a simple extension + +# Space separated list of urls +screenshoturls http://www.example.com/Slicer/Extensions/PythonCLIExtensionTemplate/Screenshots/1.png + +# 0 or 1: Define if the extension should be enabled after its installation. +enabled 1 diff --git a/Extensions/Testing/CLIExtensionTemplate/CLIModuleTemplate/CLIModuleTemplate.cxx b/Extensions/Testing/CLIExtensionTemplate/CLIModuleTemplate/CLIModuleTemplate.cxx index 28f2121d40c..f0cc177539d 100644 --- a/Extensions/Testing/CLIExtensionTemplate/CLIModuleTemplate/CLIModuleTemplate.cxx +++ b/Extensions/Testing/CLIExtensionTemplate/CLIModuleTemplate/CLIModuleTemplate.cxx @@ -15,7 +15,7 @@ namespace { template -int DoIt( int argc, char * argv[], T ) +int DoIt( int argc, char * argv[], TPixel ) { PARSE_ARGS; diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/CMakeLists.txt b/Extensions/Testing/PythonCLIExtensionTemplate/CMakeLists.txt new file mode 100644 index 00000000000..a45b67b2db9 --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 2.8.9) + +project(PythonCLIExtensionTemplate) + +#----------------------------------------------------------------------------- +# Extension meta-information +set(EXTENSION_HOMEPAGE "http://slicer.org/slicerWiki/index.php/Documentation/Nightly/Extensions/PythonCLIExtensionTemplate") +set(EXTENSION_CATEGORY "Examples") +set(EXTENSION_CONTRIBUTORS "John Doe (AnyWare Corp.)") +set(EXTENSION_DESCRIPTION "This is an example of a simple extension") +set(EXTENSION_ICONURL "http://www.example.com/Slicer/Extensions/PythonCLIExtensionTemplate.png") +set(EXTENSION_SCREENSHOTURLS "http://www.example.com/Slicer/Extensions/PythonCLIExtensionTemplate/Screenshots/1.png") + +#----------------------------------------------------------------------------- +# Extension dependencies +find_package(Slicer REQUIRED) +include(${Slicer_USE_FILE}) + +#----------------------------------------------------------------------------- +# Extension modules +add_subdirectory(PythonCLIModuleTemplate) +## NEXT_MODULE + +#----------------------------------------------------------------------------- +include(${Slicer_EXTENSION_CPACK}) diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIExtensionTemplate.png b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIExtensionTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..6aae6ab703e9fb715c775e67ab172a1c21db80cd GIT binary patch literal 17838 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 RXdnOp002ovPDHLkV1gHt7s>zt literal 0 HcmV?d00001 diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/CMakeLists.txt b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/CMakeLists.txt new file mode 100644 index 00000000000..b6cc49745bd --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/CMakeLists.txt @@ -0,0 +1,24 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME PythonCLIModuleTemplate) + +#----------------------------------------------------------------------------- +set(MODULE_PYTHON_SCRIPTS + ${MODULE_NAME}.py + ) + +set(MODULE_PYTHON_RESOURCES + ${MODULE_NAME}.xml + ) + +#----------------------------------------------------------------------------- +slicerMacroBuildScriptedModule( + NAME ${MODULE_NAME} + SCRIPTS ${MODULE_PYTHON_SCRIPTS} + RESOURCES ${MODULE_PYTHON_RESOURCES} + ) + +#----------------------------------------------------------------------------- +if(BUILD_TESTING) + # Additional build-time testing + add_subdirectory(Testing) +endif() diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 new file mode 100644 index 00000000000..e3f7f4074cd --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.nhdr.md5 @@ -0,0 +1 @@ +fc6170ceeff3d8217a9dd6a1add2ec8c diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 new file mode 100644 index 00000000000..6e640c3071f --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Baseline/PythonCLIModuleTemplateTest.raw.md5 @@ -0,0 +1 @@ +0749d4d3f07a217030f9ae33d94c4559 \ No newline at end of file diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.nhdr.md5 b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.nhdr.md5 new file mode 100644 index 00000000000..734a0467eee --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.nhdr.md5 @@ -0,0 +1 @@ +6e5c289c73e14ba7a1b0f8aaf6ed249a \ No newline at end of file diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.raw.gz.md5 b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.raw.gz.md5 new file mode 100644 index 00000000000..f555cb43501 --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Data/Input/CTHeadAxial.raw.gz.md5 @@ -0,0 +1 @@ +3ebd710c9cf9d75750f4569b8caf6d07 \ No newline at end of file diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.py b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.py new file mode 100644 index 00000000000..841dd166246 --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.py @@ -0,0 +1,36 @@ +import itk +from ctk_cli import * +import sys +import logging + +def SmoothingRecursiveGaussianImageFilter(inputVolume, outputVolume, sigma): + reader = itk.ImageFileReader.New(FileName=inputVolume) + filter = itk.SmoothingRecursiveGaussianImageFilter.New(reader) + filter.SetSigma(sigma) + writer = itk.ImageFileWriter.New(filter,FileName=outputVolume) + writer.SetUseCompression(True) + writer.Update() + return 1 + + +def main(): + """Parsing command line arguments and reading input files.""" + logging.basicConfig(level=logging.INFO) + args=CLIArgumentParser().parse_args() + # Run processing + SmoothingRecursiveGaussianImageFilter(args.inputVolume,args.outputVolume,args.sigma) + # Compare output with baseline + reader1 = itk.ImageFileReader.New(FileName=args.outputVolume) + reader2 = itk.ImageFileReader.New(FileName=args.baselineVolume) + compareFilter=itk.ComparisonImageFilter.New(reader1) + compareFilter.SetTestInput(reader1) + compareFilter.SetValidInput(reader2) + diff=compareFilter.GetTotalDifference() + if diff < args.tolerance: + return 0 + return 1 + +if __name__ == "__main__": + ret=main() + if ret: + sys.exit(ret) diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.xml b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.xml new file mode 100644 index 00000000000..604f1421566 --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/PythonCLIModuleTemplate.xml @@ -0,0 +1,52 @@ + + + Examples + PythonCLIModuleTemplate + + 0.0.1 + http://www.example.com/Slicer/Modules/PythonCLIModuleTemplate + Slicer + FirstName LastName (Institution), FirstName LastName (Institution) + This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN + + + + + sigma + sigma + s + + + 1.0 + + + inputVolume + + input + 0 + + + + outputVolume + + output + 1 + + + + baselineVolume + + input + 2 + + + + tolerance + tolerance + t + + + 0.0001 + + + diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/CMakeLists.txt b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/CMakeLists.txt new file mode 100644 index 00000000000..655007a0aab --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Python) diff --git a/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/Python/CMakeLists.txt b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/Python/CMakeLists.txt new file mode 100644 index 00000000000..82ea85b1231 --- /dev/null +++ b/Extensions/Testing/PythonCLIExtensionTemplate/PythonCLIModuleTemplate/Testing/Python/CMakeLists.txt @@ -0,0 +1,17 @@ +#----------------------------------------------------------------------------- +set(BASELINE ${CMAKE_CURRENT_SOURCE_DIR}/../../Data/Baseline) +set(INPUT ${CMAKE_CURRENT_SOURCE_DIR}/../../Data/Input) +set(TEMP "${CMAKE_BINARY_DIR}/Testing/Temporary") + +set(CLP ${MODULE_NAME}) + +#----------------------------------------------------------------------------- +set(testname ${CLP}Test) +ExternalData_add_test(${CLP}Data NAME ${testname} COMMAND ${Slicer_LAUNCHER_EXECUTABLE} ${MODULE_NAME}.py + --sigma 2.5 DATA{${INPUT}/CTHeadAxial.nhdr,CTHeadAxial.raw.gz} ${TEMP}/${CLP}Test.nhdr + DATA{${BASELINE}/${CLP}Test.nhdr,${CLP}Test.raw} + ) +set_property(TEST ${testname} PROPERTY LABELS ${CLP}) + +#----------------------------------------------------------------------------- +ExternalData_add_target(${CLP}Data)