├── .gitignore ├── CMakeLists.txt ├── CTestConfig.cmake ├── README.md ├── Screenshot01.jpg ├── Screenshot02.gif ├── SegmentMesher ├── CMakeLists.txt ├── Resources │ ├── Icons │ │ └── SegmentMesher.png │ └── UI │ │ └── SegmentMesher.ui ├── SegmentMesher.py └── Testing │ ├── CMakeLists.txt │ └── Python │ └── CMakeLists.txt ├── SlicerSegmentMesher.png ├── SlicerSegmentMesher.xcf ├── SuperBuild.cmake └── SuperBuild ├── External_cleaver.cmake └── External_tetgen.cmake /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all dotfiles... 2 | .* 3 | 4 | # Ignore all back-up files... 5 | *~ 6 | *.bak 7 | 8 | # except for .gitignore 9 | !.gitignore 10 | 11 | # Exclude Kdevelop4 files ... 12 | .kdev* 13 | 14 | # Exclude QtCreator files ... 15 | CMakeLists.txt.user* 16 | 17 | # Ignore ctags file 18 | tags 19 | 20 | # Ignore pyc files ... 21 | *.pyc 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | # 3.7 is required for SOURCE_SUBDIR External_cleaver.cmake 3 | 4 | project(SegmentMesher) 5 | 6 | #----------------------------------------------------------------------------- 7 | # Extension meta-information 8 | set(EXTENSION_HOMEPAGE "https://github.com/lassoan/SlicerSegmentMesher") 9 | set(EXTENSION_CATEGORY "Segmentation") 10 | set(EXTENSION_CONTRIBUTORS "Andras Lasso (PerkLab, Queen's University)") 11 | set(EXTENSION_DESCRIPTION "Create volumetric mesh from segmentation using Cleaver2 or TetGen.") 12 | set(EXTENSION_ICONURL "https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/master/SlicerSegmentMesher.png") 13 | set(EXTENSION_SCREENSHOTURLS "https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/master/Screenshot01.jpg") 14 | set(EXTENSION_DEPENDS "NA") # Specified as a space separated string, a list or 'NA' if any 15 | set(EXTENSION_BUILD_SUBDIRECTORY inner-build) 16 | 17 | set(SUPERBUILD_TOPLEVEL_PROJECT inner) 18 | 19 | #----------------------------------------------------------------------------- 20 | # Extension dependencies 21 | find_package(Slicer REQUIRED) 22 | include(${Slicer_USE_FILE}) 23 | mark_as_superbuild(Slicer_DIR) 24 | 25 | find_package(Git REQUIRED) 26 | mark_as_superbuild(GIT_EXECUTABLE) 27 | 28 | #----------------------------------------------------------------------------- 29 | # SuperBuild setup 30 | option(${EXTENSION_NAME}_SUPERBUILD "Build ${EXTENSION_NAME} and the projects it depends on." ON) 31 | mark_as_advanced(${EXTENSION_NAME}_SUPERBUILD) 32 | if(${EXTENSION_NAME}_SUPERBUILD) 33 | include("${CMAKE_CURRENT_SOURCE_DIR}/SuperBuild.cmake") 34 | return() 35 | endif() 36 | 37 | #----------------------------------------------------------------------------- 38 | # Extension modules 39 | add_subdirectory(SegmentMesher) 40 | ## NEXT_MODULE 41 | 42 | #----------------------------------------------------------------------------- 43 | # install directory, install project name, install component, and install subdirectory. 44 | set(CPACK_INSTALL_CMAKE_PROJECTS "${CPACK_INSTALL_CMAKE_PROJECTS};${CMAKE_BINARY_DIR};${EXTENSION_NAME};ALL;/") 45 | set(CPACK_INSTALL_CMAKE_PROJECTS "${CPACK_INSTALL_CMAKE_PROJECTS};${tetgen_DIR};tetgen;RuntimeLibraries;/") 46 | set(CPACK_INSTALL_CMAKE_PROJECTS "${CPACK_INSTALL_CMAKE_PROJECTS};${cleaver_DIR};CLEAVER2;RuntimeCLI;/") 47 | MESSAGE(STATUS "CPACK_INSTALL_CMAKE_PROJECTS = ${CPACK_INSTALL_CMAKE_PROJECTS}") 48 | 49 | include(${Slicer_EXTENSION_CPACK}) 50 | -------------------------------------------------------------------------------- /CTestConfig.cmake: -------------------------------------------------------------------------------- 1 | set(CTEST_PROJECT_NAME "SlicerSegmentMesher") 2 | set(CTEST_NIGHTLY_START_TIME "3:00:00 UTC") 3 | 4 | set(CTEST_DROP_METHOD "http") 5 | set(CTEST_DROP_SITE "slicer.cdash.org") 6 | set(CTEST_DROP_LOCATION "/submit.php?project=Slicer4") 7 | set(CTEST_DROP_SITE_CDASH TRUE) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Segment Mesher extension 2 | 3 | This is a 3D Slicer extension for creating volumetric meshes from segmentation using Cleaver2 or TetGen. 4 | 5 | Cleaver2 mesher is freely usable, without any restrictions. 6 | TetGen mesher is only free for private, research, and educational use (see license for details). 7 | 8 | ![Alt text](Screenshot01.jpg?raw=true "Segment Mesher module user interface") 9 | 10 | ## Installation 11 | 12 | * Download and install a latest stable version of 3D Slicer (https://download.slicer.org). 13 | * Start 3D Slicer application, open the Extension Manager (menu: View / Extension manager) 14 | * Install SegmentMesher extension. 15 | 16 | ## Tutorial 17 | 18 | * Start 3D Slicer 19 | * Load a volume: switch to "Sample Data" module and load MRHead image 20 | * Switch to "Segment Editor" module 21 | * Add a new segment (it will contain the entire head) 22 | * Fill segment by thresholding: click "Threshold" effect set 30 as lower threshold, click "Apply" 23 | * Smooth segment: click "Smoothing" effect, set kernel size to 6mm, click "Apply" 24 | * Add a new segment (it will contain a spherical lesion) 25 | * Paint a sphere in the brain (simulating a lesion): click "Paint" effect, enable "Sphere brush", set "Diameter" to 8%, and click in the yellow slice view 26 | * Switch to "Segment Mesher" module (in Segmentation category) 27 | * Select "Create new Model" for Output model (this will contain the generated volumetric mesh) 28 | * Click Apply button and wait a about a minute 29 | * Inspect results: open "Display" section, enable "Yellow slice clipping", move slider at the top of yellor slice view to move the clipping plane; enable "Keep only whole cells when clipping" to see shape of mesh elements 30 | * Create more accurate mesh: open "Advanced" section, set scale parameter to 0.5, click "Apply", and wait a couple of minutes 31 | 32 | ## Visualize and save results 33 | * Open "Display" section to enable clipping with slices. 34 | * Go to "Segmentations" module to hide current segmentation. 35 | * Switch to "Models" module to adjust visualization parameters. 36 | * To save Output model select in menu: File / Save. 37 | 38 | ![Alt text](Screenshot02.gif?raw=true "Segment meshing result (using Cleaver)") 39 | 40 | ## Mesh generation parameters 41 | 42 | Cleaver parameters are described at https://sciinstitute.github.io/cleaver.pages/manual.html. To make the output mesh elements smaller: decrease value of `--feature_scaling`. To make the output mesh preserve small details (at the cost of more computation time and memory usage): increase `--sampling-rate` (up to 1.0). 43 | 44 | ``` 45 | Input data: 46 | -i [ --input_files ] arg material field paths or segmentation path 47 | This argument is set automatically by SlicerSegmentMesher module. 48 | -B [ --blend_sigma ] arg blending function sigma for input(s) to 49 | remove alias artifacts. 50 | Too low value will not remove staircase artifacts. 51 | Too high value may shrink structures and remove relevant details. 52 | Default: 1.0. 53 | 54 | Output data: 55 | -f [ --output_format ] arg output mesh format (tetgen [default], 56 | scirun, matlab, vtkUSG, vtkPoly, ply 57 | [surface mesh only]) 58 | This argument is set automatically by SlicerSegmentMesher module to vtkUSG. 59 | -n [ --output_name ] arg output mesh name (default 'output') 60 | This argument is set automatically by SlicerSegmentMesher module. 61 | -o [ --output_path ] arg output path prefix 62 | This argument is set automatically by SlicerSegmentMesher module. 63 | 64 | Meshing mode (element size control): 65 | -m [ --element_sizing_method ] arg background mesh mode (adaptive [default], 66 | constant) 67 | 68 | For constant mode: 69 | -a [ --alpha ] arg initial alpha value, default: 0.4 70 | -s [ --alpha_short ] arg alpha short value for constant element 71 | sizing method, default: 0.203 72 | -l [ --alpha_long ] arg alpha long value for constant element 73 | sizing method, default: 0.357 74 | 75 | For adaptive mode: 76 | -F [ --feature_scaling ] arg feature size scaling (higher values make a 77 | coarser mesh), default: 1.0. 78 | Meaningful range is about 0.2 to 5.0. 79 | Lower value makes the output mesh finer, 80 | higher value makes the output mesh coarser and meshing faster. 81 | -L [ --lipschitz ] arg maximum rate of change of element size (1 82 | is uniform), default: 0.2 83 | It specifies how quickly the sizing field may grow away from size-limiting 84 | features (like corners or curved interfaces). 85 | -R [ --sampling_rate ] arg volume sampling rate (lower values make a 86 | coarser mesh), default: 1.0 (full sampling) 87 | Meaningful range is 0.1 to 1.0. 88 | Lower value makes meshing faster, higher value 89 | preserves fine details. 90 | 91 | Advanced: 92 | -b [ --background_mesh ] arg input background mesh 93 | -I [ --indicator_functions ] the input files are indicator functions (boundary is defined as isosurface 94 | where image value = 0) 95 | -z [ --sizing_field ] arg sizing field path (use precomputed sizing field for adaptive mode) 96 | -w [ --write_background_mesh ] write background mesh 97 | --simple use simple interface approximation 98 | -j [ --fix_tet_windup ] ensure positive Jacobians with proper vertex wind-up 99 | (prevents inside-out tetrahedra in the output mesh) 100 | This flag is specified by SlicerSegmentMesher module, no need to specify it as additional option. 101 | -e [ --strip_exterior ] strip exterior tetrahedra (remove temporary elements that are added to make the volume cubic) 102 | This flag is specified by SlicerSegmentMesher module, no need to specify it as additional option. 103 | 104 | Other: 105 | -h [ --help ] display help message 106 | -r [ --record ] arg record operations on tets from input file 107 | -t [ --strict ] warnings become errors 108 | -v [ --verbose ] enable verbose output 109 | This flag is specified by SlicerSegmentMesher module (based on Verbose option). 110 | -V [ --version ] display version information 111 | ``` 112 | 113 | TetGen parameters are described at http://wias-berlin.de/software/tetgen/1.5/doc/manual/manual005.html#sec%3Acmdline 114 | 115 | ## Developers 116 | 117 | ### Split mesh to submeshes 118 | 119 | ```python 120 | meshNode = getNode('Model') 121 | mesh = meshNode.GetMesh() 122 | cellData = mesh.GetCellData() 123 | labelsRange = cellData.GetArray("labels").GetRange() 124 | for labelValue in range(int(labelsRange[0]), int(labelsRange[1]+1)): 125 | threshold = vtk.vtkThreshold() 126 | threshold.SetInputData(mesh) 127 | threshold.SetInputArrayToProcess(0, 0, 0, vtk.vtkDataObject.FIELD_ASSOCIATION_CELLS, "labels") 128 | threshold.ThresholdBetween(labelValue, labelValue) 129 | threshold.Update() 130 | if threshold.GetOutput().GetNumberOfPoints() > 0: 131 | modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode", "{0}_{1}".format(meshNode.GetName(), labelValue)) 132 | modelNode.SetAndObserveMesh(threshold.GetOutput()) 133 | modelNode.CreateDefaultDisplayNodes() 134 | ``` 135 | 136 | ## Acknowledgments 137 | 138 | Cleaver is an Open Source software project that is principally funded through the SCI Institute's NIH/NIGMS CIBC Center. Please use the following acknowledgment and send references to any publications, presentations, or successful funding applications that make use of NIH/NIGMS CIBC software or data sets to SCI: "This project was supported by the National Institute of General Medical Sciences of the National Institutes of Health under grant number P41 GM103545-18." 139 | 140 | 141 | TetGen citation: Si, Hang (2015). "TetGen, a Delaunay-based Tetrahedral Mesh Generator". ACM Transactions on Mathematical Software. 41 (2): 11:1-11:36. doi:10.1145/2629697 142 | -------------------------------------------------------------------------------- /Screenshot01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/37a56a4216048df01af0c6392d90751f47707cea/Screenshot01.jpg -------------------------------------------------------------------------------- /Screenshot02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/37a56a4216048df01af0c6392d90751f47707cea/Screenshot02.gif -------------------------------------------------------------------------------- /SegmentMesher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME SegmentMesher) 3 | 4 | #----------------------------------------------------------------------------- 5 | set(MODULE_PYTHON_SCRIPTS 6 | ${MODULE_NAME}.py 7 | ) 8 | 9 | set(MODULE_PYTHON_RESOURCES 10 | Resources/Icons/${MODULE_NAME}.png 11 | Resources/UI/${MODULE_NAME}.ui 12 | ) 13 | 14 | #----------------------------------------------------------------------------- 15 | slicerMacroBuildScriptedModule( 16 | NAME ${MODULE_NAME} 17 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 18 | RESOURCES ${MODULE_PYTHON_RESOURCES} 19 | WITH_GENERIC_TESTS 20 | ) 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | # Register the unittest subclass in the main script as a ctest. 25 | # Note that the test will also be available at runtime. 26 | slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 27 | 28 | # Additional build-time testing 29 | add_subdirectory(Testing) 30 | endif() 31 | -------------------------------------------------------------------------------- /SegmentMesher/Resources/Icons/SegmentMesher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/37a56a4216048df01af0c6392d90751f47707cea/SegmentMesher/Resources/Icons/SegmentMesher.png -------------------------------------------------------------------------------- /SegmentMesher/Resources/UI/SegmentMesher.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SegmentMesher 4 | 5 | 6 | 7 | 0 8 | 0 9 | 458 10 | 951 11 | 12 | 13 | 14 | 15 | 16 | 17 | Parameter set 18 | 19 | 20 | 21 | 22 | 23 | Parameter set: 24 | 25 | 26 | 27 | 28 | 29 | 30 | true 31 | 32 | 33 | Pick parameter set 34 | 35 | 36 | 37 | vtkMRMLScriptedModuleNode 38 | 39 | 40 | 41 | true 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | SegmentMesher 51 | 52 | 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Inputs 64 | 65 | 66 | 67 | 68 | 69 | Input segmentation: 70 | 71 | 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | Volumetric mesh will be generated from this segmentation node. 80 | 81 | 82 | 83 | vtkMRMLSegmentationNode 84 | 85 | 86 | 87 | false 88 | 89 | 90 | true 91 | 92 | 93 | false 94 | 95 | 96 | false 97 | 98 | 99 | 100 | 101 | 102 | 103 | Segment(s) to mesh: 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | select/unselect all 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Input model: 125 | 126 | 127 | 128 | 129 | 130 | 131 | Volumetric mesh will be generated based on this surface - TetGen only. 132 | 133 | 134 | 135 | vtkMRMLModelNode 136 | 137 | 138 | 139 | false 140 | 141 | 142 | false 143 | 144 | 145 | false 146 | 147 | 148 | 149 | 150 | 151 | 152 | Meshing method: 153 | 154 | 155 | 156 | 157 | 158 | 159 | Meshing algorithm to use 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | Outputs 170 | 171 | 172 | 173 | 174 | 175 | Output model: 176 | 177 | 178 | 179 | 180 | 181 | 182 | true 183 | 184 | 185 | Created volumetric mesh 186 | 187 | 188 | 189 | vtkMRMLModelNode 190 | 191 | 192 | 193 | false 194 | 195 | 196 | false 197 | 198 | 199 | true 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | Advanced 210 | 211 | 212 | true 213 | 214 | 215 | 216 | 217 | 218 | Cleaver parameters 219 | 220 | 221 | 222 | 223 | 224 | Feature scaling (increase for coarser mesh): 225 | 226 | 227 | 228 | 229 | 230 | 231 | To make the output mesh elements smaller: decrease value of `--feature_scaling`. 232 | 233 | 234 | 0.200000000000000 235 | 236 | 237 | 5.000000000000000 238 | 239 | 240 | 0.100000000000000 241 | 242 | 243 | 2.000000000000000 244 | 245 | 246 | 247 | 248 | 249 | 250 | Sampling rate (increase for finer details): 251 | 252 | 253 | 254 | 255 | 256 | 257 | To make the output mesh preserve small details (at the cost of more computation time and memory usage): increase `--sampling-rate` (up to 1.0). 258 | 259 | 260 | 0.100000000000000 261 | 262 | 263 | 1.000000000000000 264 | 265 | 266 | 0.100000000000000 267 | 268 | 269 | 0.200000000000000 270 | 271 | 272 | 273 | 274 | 275 | 276 | Rate of change of element size: 277 | 278 | 279 | 280 | 281 | 282 | 283 | Increase parameter value to allow more variation in element size, 1 is uniform. 284 | 285 | 286 | 1.000000000000000 287 | 288 | 289 | 0.100000000000000 290 | 291 | 292 | 0.200000000000000 293 | 294 | 295 | 296 | 297 | 298 | 299 | Additional command line options: 300 | 301 | 302 | 303 | 304 | 305 | 306 | See description of all parameters in module documentation (Help & Acknowledgment section). 307 | 308 | 309 | 310 | 311 | 312 | 313 | Remove background mesh: 314 | 315 | 316 | 317 | 318 | 319 | 320 | Remove background mesh (filling segmentation reference geometry box). 321 | 322 | 323 | 324 | 325 | 326 | true 327 | 328 | 329 | 330 | 331 | 332 | 333 | Background padding: 334 | 335 | 336 | 337 | 338 | 339 | 340 | Add padding around the segments to ensure some minimum thickness to the background mesh. Increase value if segments have extrusions towards the edge of the padded bounding box. 341 | 342 | 343 | % 344 | 345 | 346 | 200 347 | 348 | 349 | 10 350 | 351 | 352 | 353 | 354 | 355 | 356 | Custom Cleaver executable path: 357 | 358 | 359 | 360 | 361 | 362 | 363 | Set cleaver-cli executable path. 364 | If value is empty then cleaver-cli bundled with this extension will be used. 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | TetGen parameters 375 | 376 | 377 | 378 | 379 | 380 | Use a model as input: 381 | 382 | 383 | 384 | 385 | 386 | 387 | Create mesh from surface instead of segmentation 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | Maximim radius-edge ratio (decrease for more regular mesh): 398 | 399 | 400 | 401 | 402 | 403 | 404 | Decrease maximum radius-edge ratio to generate more regular tetrahedra - will increase processing time. 405 | 406 | 407 | 1 408 | 409 | 410 | 1.000000000000000 411 | 412 | 413 | 20.000000000000000 414 | 415 | 416 | 0.100000000000000 417 | 418 | 419 | 5.000000000000000 420 | 421 | 422 | 423 | 424 | 425 | 426 | Minimum dihedral angle (increase for more regular mesh): 427 | 428 | 429 | 430 | 431 | 432 | 433 | Increase minimum dihedral angle to generate more regular tetrahedra - will increase processing time. 434 | 435 | 436 | 1 437 | 438 | 439 | 180.000000000000000 440 | 441 | 442 | 5.000000000000000 443 | 444 | 445 | 446 | 447 | 448 | 449 | Maximum tetrahedron volume (decrease for finer mesh): 450 | 451 | 452 | 453 | 454 | 455 | 456 | Decrease maximum tetrahedron volume to generate finer tetrahedra - will increase processing time. 457 | 458 | 459 | 1 460 | 461 | 462 | 100000.000000000000000 463 | 464 | 465 | 0.100000000000000 466 | 467 | 468 | 10.000000000000000 469 | 470 | 471 | 472 | 473 | 474 | 475 | TetGen meshing options: 476 | 477 | 478 | 479 | 480 | 481 | 482 | See description of parameters in module documentation (Help & Acknowledgment section). 483 | 484 | 485 | 486 | 487 | 488 | 489 | Custom TetGen executable path: 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 0 498 | 0 499 | 500 | 501 | 502 | <html><head/><body><p>Set tetgen executable path. </p><p>If value is empty then tetgen bundled with this extension will be used.</p></body></html> 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | General parameters 513 | 514 | 515 | 516 | 517 | 518 | Show detailed log: 519 | 520 | 521 | 522 | 523 | 524 | 525 | Show detailed log during model generation. 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | Keep temporary files: 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | Keep temporary files (inputs, computed outputs, logs) after the model generation is completed. 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 0 556 | 0 557 | 558 | 559 | 560 | Open the folder where temporary files are stored. 561 | 562 | 563 | Show temp folder 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | Display 579 | 580 | 581 | true 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | false 594 | 595 | 596 | Run the algorithm. 597 | 598 | 599 | Apply 600 | 601 | 602 | 603 | 604 | 605 | 606 | Qt::TextSelectableByMouse 607 | 608 | 609 | true 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | ctkCheckableComboBox 618 | QComboBox 619 |
ctkCheckableComboBox.h
620 |
621 | 622 | ctkCollapsibleButton 623 | QWidget 624 |
ctkCollapsibleButton.h
625 | 1 626 |
627 | 628 | ctkCollapsibleGroupBox 629 | QGroupBox 630 |
ctkCollapsibleGroupBox.h
631 | 1 632 |
633 | 634 | ctkPathLineEdit 635 | QWidget 636 |
ctkPathLineEdit.h
637 |
638 | 639 | qMRMLClipNodeWidget 640 | QWidget 641 |
qMRMLClipNodeWidget.h
642 |
643 | 644 | qMRMLNodeComboBox 645 | QWidget 646 |
qMRMLNodeComboBox.h
647 |
648 | 649 | qSlicerWidget 650 | QWidget 651 |
qSlicerWidget.h
652 | 1 653 |
654 |
655 | 656 | 657 |
658 | -------------------------------------------------------------------------------- /SegmentMesher/SegmentMesher.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import unittest 4 | import vtk, qt, ctk, slicer 5 | from slicer.ScriptedLoadableModule import * 6 | from slicer.util import VTKObservationMixin 7 | import logging 8 | 9 | # 10 | # SegmentMesher 11 | # 12 | 13 | class SegmentMesher(ScriptedLoadableModule): 14 | """Uses ScriptedLoadableModule base class, available at: 15 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 16 | """ 17 | 18 | def __init__(self, parent): 19 | ScriptedLoadableModule.__init__(self, parent) 20 | self.parent.title = "Segment Mesher" 21 | self.parent.categories = ["Segmentation"] 22 | self.parent.dependencies = [] 23 | self.parent.contributors = ["Andras Lasso (PerkLab - Queen's University)"] 24 | self.parent.helpText = """Create volumetric mesh consisting of tetrahedral elements using Cleaver2 or TetGen meshers. 25 |

See module documentation for description of meshing parameters. 26 |

Cleaver2 is freely usable, without any restrictions. 27 |

TetGen is only free for private, research, and educational use (see license for details). 28 | """ 29 | #self.parent.helpText += self.getDefaultModuleDocumentationLink() 30 | self.parent.acknowledgementText = """ 31 | This module was originally developed by Andras Lasso (Queen's University, PerkLab) to serve as a convenient frontend for existing commonly used open-source generator software. 32 | 33 |

Cleaver is an Open Source software project that is principally funded through the SCI Institute's NIH/NIGMS CIBC Center. Please use the following acknowledgment and send references to any publications, presentations, or successful funding applications that make use of NIH/NIGMS CIBC software or data sets to SCI: "This project was supported by the National Institute of General Medical Sciences of the National Institutes of Health under grant number P41 GM103545-18." 34 | 35 |

TetGen citation: Si, Hang (2015). "TetGen, a Delaunay-based Tetrahedral Mesh Generator". ACM Transactions on Mathematical Software. 41 (2): 11:1-11:36. doi:10.1145/2629697 36 | """ 37 | 38 | # 39 | # SegmentMesherWidget 40 | # 41 | 42 | class SegmentMesherWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): 43 | """Uses ScriptedLoadableModuleWidget base class, available at: 44 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 45 | """ 46 | 47 | def __init__(self, parent=None): 48 | """ 49 | Called when the user opens the module the first time and the widget is initialized. 50 | """ 51 | ScriptedLoadableModuleWidget.__init__(self, parent) 52 | VTKObservationMixin.__init__(self) # needed for parameter node observation 53 | self.logic = None 54 | self._parameterNode = None 55 | self._updatingGUIFromParameterNode = False 56 | 57 | def setup(self): 58 | ScriptedLoadableModuleWidget.setup(self) 59 | 60 | self.logic = SegmentMesherLogic() 61 | self.logic.logCallback = self.addLog 62 | self.modelGenerationInProgress = False 63 | 64 | uiWidget = slicer.util.loadUI(self.resourcePath('UI/SegmentMesher.ui')) 65 | self.layout.addWidget(uiWidget) 66 | self.ui = slicer.util.childWidgetVariables(uiWidget) 67 | uiWidget.setPalette(slicer.util.mainWindow().style().standardPalette()) 68 | 69 | # Finish UI setup ... 70 | self.ui.parameterNodeSelector.addAttribute( "vtkMRMLScriptedModuleNode", "ModuleName", "SegmentMesher" ) 71 | self.ui.parameterNodeSelector.setMRMLScene( slicer.mrmlScene ) 72 | self.ui.inputSegmentationSelector.setMRMLScene( slicer.mrmlScene ) 73 | self.ui.inputModelSelector.setMRMLScene( slicer.mrmlScene ) 74 | self.ui.outputModelSelector.setMRMLScene( slicer.mrmlScene ) 75 | 76 | self.ui.methodSelectorComboBox.addItem("Cleaver", METHOD_CLEAVER) 77 | self.ui.methodSelectorComboBox.addItem("TetGen", METHOD_TETGEN) 78 | 79 | customCleaverPath = self.logic.getCustomCleaverPath() 80 | self.ui.customCleaverPathSelector.setCurrentPath(customCleaverPath) 81 | self.ui.customCleaverPathSelector.nameFilters = [self.logic.cleaverFilename] 82 | 83 | customTetGenPath = self.logic.getCustomTetGenPath() 84 | self.ui.customTetGenPathSelector.setCurrentPath(customTetGenPath) 85 | self.ui.customTetGenPathSelector.nameFilters = [self.logic.tetGenFilename] 86 | 87 | clipNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLClipModelsNode") 88 | self.ui.clipNodeWidget.setMRMLClipNode(clipNode) 89 | 90 | # These connections ensure that we update parameter node when scene is closed 91 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) 92 | self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) 93 | 94 | # connections 95 | self.ui.selectAllSegmentsButton.connect('clicked(bool)', self.onSelectAllSegmentsButton) 96 | self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) 97 | self.ui.showTemporaryFilesFolderButton.connect('clicked(bool)', self.onShowTemporaryFilesFolder) 98 | self.ui.inputSegmentationSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateMRMLFromGUI) 99 | self.ui.inputModelSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateMRMLFromGUI) 100 | self.ui.outputModelSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateMRMLFromGUI) 101 | self.ui.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI) 102 | # Immediately update deleteTemporaryFiles in the logic to make it possible to decide to 103 | # keep the temporary file while the model generation is running 104 | self.ui.keepTemporaryFilesCheckBox.connect("toggled(bool)", self.onKeepTemporaryFilesToggled) 105 | self.ui.tetgenUseSurface.connect("toggled(bool)", self.updateMRMLFromGUI) 106 | 107 | #Parameter node connections 108 | self.ui.inputSegmentationSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 109 | self.ui.inputModelSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 110 | self.ui.outputModelSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) 111 | self.ui.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateParameterNodeFromGUI) 112 | 113 | 114 | self.ui.showDetailedLogDuringExecutionCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) 115 | self.ui.keepTemporaryFilesCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) 116 | 117 | self.ui.cleaverFeatureScalingParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 118 | self.ui.cleaverSamplingParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 119 | self.ui.cleaverRateParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 120 | self.ui.cleaverAdditionalParametersWidget.connect("textChanged(const QString&)", self.updateParameterNodeFromGUI) 121 | self.ui.cleaverRemoveBackgroundMeshCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) 122 | self.ui.cleaverPaddingPercentSpinBox.connect("valueChanged(int)", self.updateParameterNodeFromGUI) 123 | self.ui.customCleaverPathSelector.connect("currentPathChanged(const QString&)", self.updateParameterNodeFromGUI) 124 | 125 | self.ui.tetgenUseSurface.connect("toggled(bool)", self.updateParameterNodeFromGUI) 126 | self.ui.tetgenRatioParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 127 | self.ui.tetgenAngleParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 128 | self.ui.tetgenVolumeParameterWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) 129 | self.ui.tetGenAdditionalParametersWidget.connect("textChanged(const QString&)", self.updateParameterNodeFromGUI) 130 | self.ui.customTetGenPathSelector.connect("currentPathChanged(const QString&)", self.updateParameterNodeFromGUI) 131 | 132 | # Add vertical spacer 133 | self.layout.addStretch(1) 134 | 135 | # Make sure parameter node is initialized (needed for module reload) 136 | self.initializeParameterNode() 137 | self.ui.parameterNodeSelector.setCurrentNode(self._parameterNode) 138 | self.ui.parameterNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.setParameterNode) 139 | 140 | # Refresh Apply button state 141 | self.updateMRMLFromGUI() 142 | 143 | def enter(self): 144 | """ 145 | Called each time the user opens this module. 146 | """ 147 | # Make sure parameter node exists and observed 148 | self.initializeParameterNode() 149 | self.updateMRMLFromGUI() 150 | 151 | def cleanup(self): 152 | """ 153 | Called when the application closes and the module widget is destroyed. 154 | """ 155 | self.removeObservers() 156 | 157 | def exit(self): 158 | """ 159 | Called each time the user opens a different module. 160 | """ 161 | # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module) 162 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 163 | 164 | def onSceneStartClose(self, caller, event): 165 | """ 166 | Called just before the scene is closed. 167 | """ 168 | # Parameter node will be reset, do not use it anymore 169 | self.setParameterNode(None) 170 | 171 | def onSceneEndClose(self, caller, event): 172 | """ 173 | Called just after the scene is closed. 174 | """ 175 | # If this module is shown while the scene is closed then recreate a new parameter node immediately 176 | if self.parent.isEntered: 177 | self.initializeParameterNode() 178 | 179 | 180 | def initializeParameterNode(self): 181 | """ 182 | Ensure parameter node exists and observed. 183 | """ 184 | # Parameter node stores all user choices in parameter values, node selections, etc. 185 | # so that when the scene is saved and reloaded, these settings are restored. 186 | 187 | self.setParameterNode(self.logic.getParameterNode()) 188 | 189 | # Select default input nodes if nothing is selected yet to save a few clicks for the user 190 | if not self._parameterNode.GetNodeReference("InputSegmentation"): 191 | firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode") 192 | if firstVolumeNode: 193 | self._parameterNode.SetNodeReferenceID("InputSegmentation", firstVolumeNode.GetID()) 194 | 195 | # Select default input nodes if nothing is selected yet to save a few clicks for the user 196 | if not self._parameterNode.GetNodeReference("InputSurface"): 197 | firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLModelNode") 198 | if firstVolumeNode: 199 | self._parameterNode.SetNodeReferenceID("InputSurface", firstVolumeNode.GetID()) 200 | 201 | def setParameterNode(self, inputParameterNode): 202 | """ 203 | Set and observe parameter node. 204 | Observation is needed because when the parameter node is changed then the GUI must be updated immediately. 205 | """ 206 | 207 | if inputParameterNode: 208 | self.logic.setDefaultParameters(inputParameterNode) 209 | 210 | # Unobserve previously selected parameter node and add an observer to the newly selected. 211 | # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module 212 | # those are reflected immediately in the GUI. 213 | if self._parameterNode is not None: 214 | self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 215 | self._parameterNode = inputParameterNode 216 | if self._parameterNode is not None: 217 | self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) 218 | 219 | # Initial GUI update 220 | self.updateGUIFromParameterNode() 221 | 222 | def updateGUIFromParameterNode(self, caller=None, event=None): 223 | """ 224 | This method is called whenever parameter node is changed. 225 | The module GUI is updated to show the current state of the parameter node. 226 | """ 227 | 228 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 229 | return 230 | 231 | # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) 232 | self._updatingGUIFromParameterNode = True 233 | 234 | # Update node selectors and sliders 235 | self.ui.inputSegmentationSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputSegmentation")) 236 | self.ui.inputModelSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputSurface")) 237 | self.ui.outputModelSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputModel")) 238 | self.ui.methodSelectorComboBox.setCurrentText(self._parameterNode.GetParameter("Method")) 239 | 240 | self.ui.showDetailedLogDuringExecutionCheckBox.checked = (self._parameterNode.GetParameter("showDetailedLogDuringExecution") == "true") 241 | self.ui.keepTemporaryFilesCheckBox.checked = (self._parameterNode.GetParameter("keepTemporaryFiles") == "true") 242 | 243 | self.ui.cleaverFeatureScalingParameterWidget.value = float(self._parameterNode.GetParameter("cleaverFeatureScalingParameter")) 244 | self.ui.cleaverSamplingParameterWidget.value = float(self._parameterNode.GetParameter("cleaverSamplingParameter")) 245 | self.ui.cleaverRateParameterWidget.value = float(self._parameterNode.GetParameter("cleaverRateParameter")) 246 | self.ui.cleaverAdditionalParametersWidget.text = self._parameterNode.GetParameter("cleaverAdditionalParameters") 247 | self.ui.cleaverRemoveBackgroundMeshCheckBox.checked = (self._parameterNode.GetParameter("cleaverRemoveBackgroundMesh") == "true") 248 | self.ui.cleaverPaddingPercentSpinBox.value = int(self._parameterNode.GetParameter("cleaverPaddingPercent")) 249 | self.ui.customCleaverPathSelector.setCurrentPath(self._parameterNode.GetParameter("customCleaverPath")) 250 | 251 | self.ui.tetgenUseSurface.checked = (self._parameterNode.GetParameter("tetgenUseSurface") == "true") 252 | self.ui.tetgenRatioParameterWidget.value = float(self._parameterNode.GetParameter("tetgenRatioParameter")) 253 | self.ui.tetgenAngleParameterWidget.value = float(self._parameterNode.GetParameter("tetgenAngleParameter")) 254 | self.ui.tetgenVolumeParameterWidget.value = float(self._parameterNode.GetParameter("tetgenVolumeParameter")) 255 | self.ui.tetGenAdditionalParametersWidget.text = self._parameterNode.GetParameter("tetGenAdditionalParameters") 256 | self.ui.customTetGenPathSelector.setCurrentPath(self._parameterNode.GetParameter("customTetGenPath")) 257 | 258 | 259 | # Update buttons states and tooltips 260 | self.updateMRMLFromGUI() 261 | 262 | # All the GUI updates are done 263 | self._updatingGUIFromParameterNode = False 264 | 265 | def updateParameterNodeFromGUI(self, caller=None, event=None): 266 | """ 267 | This method is called when the user makes any change in the GUI. 268 | The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). 269 | """ 270 | 271 | if self._parameterNode is None or self._updatingGUIFromParameterNode: 272 | return 273 | 274 | wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch 275 | 276 | #Inputs/Outputs 277 | self._parameterNode.SetNodeReferenceID("InputSegmentation", self.ui.inputSegmentationSelector.currentNodeID) 278 | self._parameterNode.SetNodeReferenceID("InputSurface", self.ui.inputModelSelector.currentNodeID) 279 | self._parameterNode.SetNodeReferenceID("OutputModel", self.ui.outputModelSelector.currentNodeID) 280 | self._parameterNode.SetParameter("Method", self.ui.methodSelectorComboBox.currentText) 281 | 282 | #General parameters 283 | self._parameterNode.SetParameter("showDetailedLogDuringExecution", "true" if self.ui.showDetailedLogDuringExecutionCheckBox.checked else "false") 284 | self._parameterNode.SetParameter("keepTemporaryFiles", "true" if self.ui.keepTemporaryFilesCheckBox.checked else "false") 285 | 286 | #Cleaver parameters 287 | self._parameterNode.SetParameter("cleaverFeatureScalingParameter", str(self.ui.cleaverFeatureScalingParameterWidget.value)) 288 | self._parameterNode.SetParameter("cleaverSamplingParameter", str(self.ui.cleaverSamplingParameterWidget.value)) 289 | self._parameterNode.SetParameter("cleaverRateParameter", str(self.ui.cleaverRateParameterWidget.value)) 290 | self._parameterNode.SetParameter("cleaverAdditionalParameters", self.ui.cleaverAdditionalParametersWidget.text) 291 | self._parameterNode.SetParameter("cleaverRemoveBackgroundMesh", "true" if self.ui.cleaverRemoveBackgroundMeshCheckBox.checked else "false") 292 | self._parameterNode.SetParameter("cleaverPaddingPercent", str(self.ui.cleaverPaddingPercentSpinBox.value)) 293 | self._parameterNode.SetParameter("customCleaverPath", self.ui.customCleaverPathSelector.currentPath) 294 | 295 | #TetGen parameters 296 | self._parameterNode.SetParameter("tetgenUseSurface", "true" if self.ui.tetgenUseSurface.checked else "false") 297 | self._parameterNode.SetParameter("tetgenRatioParameter", str(self.ui.tetgenRatioParameterWidget.value)) 298 | self._parameterNode.SetParameter("tetgenAngleParameter", str(self.ui.tetgenAngleParameterWidget.value)) 299 | self._parameterNode.SetParameter("tetgenVolumeParameter", str(self.ui.tetgenVolumeParameterWidget.value)) 300 | self._parameterNode.SetParameter("tetGenAdditionalParameters", self.ui.tetGenAdditionalParametersWidget.text) 301 | self._parameterNode.SetParameter("customTetGenPath", self.ui.customTetGenPathSelector.currentPath) 302 | 303 | self._parameterNode.EndModify(wasModified) 304 | 305 | def updateMRMLFromGUI(self): 306 | 307 | method = self.ui.methodSelectorComboBox.itemData(self.ui.methodSelectorComboBox.currentIndex) 308 | 309 | #Enable correct input selections 310 | inputIsModel = (self.ui.tetgenUseSurface.isChecked() and method == METHOD_TETGEN) 311 | self.ui.inputSegmentationLabel.visible = not inputIsModel 312 | self.ui.inputSegmentationSelector.visible = not inputIsModel 313 | self.ui.segmentSelectorLabel.visible = not inputIsModel 314 | self.ui.segmentSelectorCombBox.visible = not inputIsModel 315 | self.ui.inputModelLabel.visible = inputIsModel 316 | self.ui.inputModelSelector.visible = inputIsModel 317 | segmentationSelected = self.ui.inputSegmentationSelector.currentNode() is not None 318 | self.ui.segmentSelectorCombBox.enabled = segmentationSelected 319 | self.ui.selectAllSegmentsButton.enabled = segmentationSelected 320 | 321 | #populate segments 322 | inputSeg = self.ui.inputSegmentationSelector.currentNode() 323 | oldIndex = self.ui.segmentSelectorCombBox.checkedIndexes() 324 | oldCount = self.ui.segmentSelectorCombBox.count 325 | self.ui.segmentSelectorCombBox.clear() 326 | if inputSeg is not None: 327 | segmentIDs = vtk.vtkStringArray() 328 | inputSeg.GetSegmentation().GetSegmentIDs(segmentIDs) 329 | for index in range(0, segmentIDs.GetNumberOfValues()): 330 | segmentId = segmentIDs.GetValue(index) 331 | self.ui.segmentSelectorCombBox.addItem(inputSeg.GetSegmentation().GetSegment(segmentId).GetName(), segmentId) 332 | 333 | #Restore index - often we will be reloading the data from the same segmentation, so re-select items number of items is the same 334 | if oldCount == self.ui.segmentSelectorCombBox.count: 335 | for index in oldIndex: 336 | self.ui.segmentSelectorCombBox.setCheckState(index, qt.Qt.Checked) 337 | 338 | self.ui.CleaverParametersGroupBox.visible = (method == METHOD_CLEAVER) 339 | self.ui.TetGenParametersGroupBox.visible = (method == METHOD_TETGEN) 340 | 341 | if method == METHOD_TETGEN and self.ui.tetgenUseSurface.isChecked(): 342 | if not self.ui.inputModelSelector.currentNode(): 343 | self.ui.applyButton.text = "Select input surface" 344 | self.ui.applyButton.enabled = False 345 | elif not self.ui.outputModelSelector.currentNode(): 346 | self.ui.applyButton.text = "Select an output model node" 347 | self.ui.applyButton.enabled = False 348 | elif self.ui.inputModelSelector.currentNode() == self.ui.outputModelSelector.currentNode(): 349 | self.ui.applyButton.text = "Choose different Output model" 350 | self.ui.applyButton.enabled = False 351 | else: 352 | self.ui.applyButton.text = "Apply" 353 | self.ui.applyButton.enabled = True 354 | else: 355 | if not self.ui.inputSegmentationSelector.currentNode(): 356 | self.ui.applyButton.text = "Select input segmentation" 357 | self.ui.applyButton.enabled = False 358 | elif not self.ui.outputModelSelector.currentNode(): 359 | self.ui.applyButton.text = "Select an output model node" 360 | self.ui.applyButton.enabled = False 361 | elif self.ui.inputSegmentationSelector.currentNode() == self.ui.outputModelSelector.currentNode(): 362 | self.ui.applyButton.text = "Choose different Output model" 363 | self.ui.applyButton.enabled = False 364 | else: 365 | self.ui.applyButton.text = "Apply" 366 | self.ui.applyButton.enabled = True 367 | 368 | self.updateParameterNodeFromGUI() 369 | 370 | 371 | # def updateGUIFromMRML(self): 372 | # parameterNode = self.parameterNodeSelector.currentNode() 373 | # method = parameterNode.parameter("Method") 374 | # methodIndex = self.methodSelectorComboBox.findData(method) 375 | # wasBlocked = self.methodSelectorComboBox.blockSignals(True) 376 | # self.methodSelectorComboBox.setCurrentIndex(methodIndex) 377 | # self.methodSelectorComboBox.blockSignals(wasBlocked) 378 | 379 | def onShowTemporaryFilesFolder(self): 380 | qt.QDesktopServices().openUrl(qt.QUrl("file:///" + self.logic.getTempDirectoryBase(), qt.QUrl.TolerantMode)); 381 | 382 | def onKeepTemporaryFilesToggled(self, toggle): 383 | self.logic.deleteTemporaryFiles = toggle 384 | 385 | def onApplyButton(self): 386 | if self.modelGenerationInProgress: 387 | self.modelGenerationInProgress = False 388 | self.logic.abortRequested = True 389 | self.ui.applyButton.text = "Cancelling..." 390 | self.ui.applyButton.enabled = False 391 | return 392 | 393 | self.modelGenerationInProgress = True 394 | self.ui.applyButton.text = "Cancel" 395 | self.ui.statusLabel.plainText = '' 396 | slicer.app.setOverrideCursor(qt.Qt.WaitCursor) 397 | try: 398 | self.logic.setCustomCleaverPath(self.ui.customCleaverPathSelector.currentPath) 399 | self.logic.setCustomTetGenPath(self.ui.customTetGenPathSelector.currentPath) 400 | 401 | self.logic.deleteTemporaryFiles = not self.ui.keepTemporaryFilesCheckBox.checked 402 | self.logic.logStandardOutput = self.ui.showDetailedLogDuringExecutionCheckBox.checked 403 | 404 | method = self.ui.methodSelectorComboBox.itemData(self.ui.methodSelectorComboBox.currentIndex) 405 | 406 | #Get list of segments to mesh 407 | segmentIndexes = self.ui.segmentSelectorCombBox.checkedIndexes() 408 | segments = [] 409 | 410 | for index in segmentIndexes: 411 | segments.append(self.ui.segmentSelectorCombBox.itemData(index.row())) 412 | 413 | print(method) 414 | if method == METHOD_CLEAVER: 415 | self.logic.createMeshFromSegmentationCleaver(self.ui.inputSegmentationSelector.currentNode(), 416 | self.ui.outputModelSelector.currentNode(), segments, self.ui.cleaverAdditionalParametersWidget.text, 417 | self.ui.cleaverRemoveBackgroundMeshCheckBox.isChecked(), 418 | self.ui.cleaverPaddingPercentSpinBox.value * 0.01, self.ui.cleaverFeatureScalingParameterWidget.value, self.ui.cleaverSamplingParameterWidget.value, self.ui.cleaverRateParameterWidget.value) 419 | else: 420 | if self.ui.tetgenUseSurface.isChecked(): 421 | if self.ui.inputModelSelector.currentNode().GetUnstructuredGrid() is not None: 422 | self.addLog("Error: Mesh must be a surface, not volumetric") 423 | return 424 | self.logic.createMeshFromPolyDataTetGen(self.ui.inputModelSelector.currentNode().GetPolyData(), 425 | self.ui.outputModelSelector.currentNode(), self.ui.tetGenAdditionalParametersWidget.text, 426 | self.ui.tetgenRatioParameterWidget.value, self.ui.tetgenAngleParameterWidget.value, self.ui.tetgenVolumeParameterWidget.value) 427 | else: 428 | self.logic.createMeshFromSegmentationTetGen(self.ui.inputSegmentationSelector.currentNode(), 429 | self.ui.outputModelSelector.currentNode(), segments, self.ui.tetGenAdditionalParametersWidget.text, 430 | self.ui.tetgenRatioParameterWidget.value, self.ui.tetgenAngleParameterWidget.value, self.ui.tetgenVolumeParameterWidget.value) 431 | 432 | except Exception as e: 433 | print(e) 434 | self.addLog("Error: {0}".format(str(e))) 435 | import traceback 436 | traceback.print_exc() 437 | finally: 438 | slicer.app.restoreOverrideCursor() 439 | self.modelGenerationInProgress = False 440 | self.updateMRMLFromGUI() # restores default Apply button state 441 | 442 | def onSelectAllSegmentsButton(self): 443 | newState = qt.Qt.Unchecked if self.ui.segmentSelectorCombBox.allChecked() else qt.Qt.Checked 444 | model = self.ui.segmentSelectorCombBox.model() 445 | for i in range(self.ui.segmentSelectorCombBox.count): 446 | self.ui.segmentSelectorCombBox.setCheckState(model.index(i, 0), newState) 447 | 448 | def addLog(self, text): 449 | """Append text to log window 450 | """ 451 | self.ui.statusLabel.appendPlainText(text) 452 | slicer.app.processEvents() # force update 453 | 454 | # 455 | # SegmentMesherLogic 456 | # 457 | 458 | class SegmentMesherLogic(ScriptedLoadableModuleLogic): 459 | """This class should implement all the actual 460 | computation done by your module. The interface 461 | should be such that other python code can import 462 | this class and make use of the functionality without 463 | requiring an instance of the Widget. 464 | Uses ScriptedLoadableModuleLogic base class, available at: 465 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 466 | """ 467 | 468 | def __init__(self): 469 | ScriptedLoadableModuleLogic.__init__(self) 470 | self.logCallback = None 471 | self.abortRequested = False 472 | self.deleteTemporaryFiles = True 473 | self.logStandardOutput = False 474 | self.customCleaverPathSettingsKey = 'SegmentMesher/CustomCleaverPath' 475 | self.customTetGenPathSettingsKey = 'SegmentMesher/CustomTetGenPath' 476 | import os 477 | self.scriptPath = os.path.dirname(os.path.abspath(__file__)) 478 | self.cleaverPath = None # this will be determined dynamically 479 | self.tetGenPath = None # this will be determined dynamically 480 | 481 | import platform 482 | executableExt = '.exe' if platform.system() == 'Windows' else '' 483 | self.cleaverFilename = 'cleaver-cli' + executableExt 484 | self.tetGenFilename = 'tetgen' + executableExt 485 | 486 | self.binDirCandidates = [ 487 | # install tree 488 | os.path.join(self.scriptPath, '..'), 489 | os.path.join(self.scriptPath, '../../../bin'), 490 | # build tree 491 | os.path.join(self.scriptPath, '../../../../bin'), 492 | os.path.join(self.scriptPath, '../../../../bin/Release'), 493 | os.path.join(self.scriptPath, '../../../../bin/Debug'), 494 | os.path.join(self.scriptPath, '../../../../bin/RelWithDebInfo'), 495 | os.path.join(self.scriptPath, '../../../../bin/MinSizeRel') ] 496 | 497 | def setDefaultParameters(self, parameterNode): 498 | """ 499 | Initialize parameter node with default settings. 500 | """ 501 | self.setParameterIfNotDefined(parameterNode, "showDetailedLogDuringExecution", "false") 502 | self.setParameterIfNotDefined(parameterNode, "keepTemporaryFiles", "false") 503 | 504 | self.setParameterIfNotDefined(parameterNode, "cleaverFeatureScalingParameter", "2.0") 505 | self.setParameterIfNotDefined(parameterNode, "cleaverSamplingParameter", "0.2") 506 | self.setParameterIfNotDefined(parameterNode, "cleaverRateParameter", "0.2") 507 | self.setParameterIfNotDefined(parameterNode, "cleaverAdditionalParameters", "") 508 | self.setParameterIfNotDefined(parameterNode, "cleaverRemoveBackgroundMesh", "true") 509 | self.setParameterIfNotDefined(parameterNode, "cleaverPaddingPercent", "10") 510 | self.setParameterIfNotDefined(parameterNode, "customCleaverPath", "") 511 | 512 | self.setParameterIfNotDefined(parameterNode, "tetgenUseSurface", "false") 513 | self.setParameterIfNotDefined(parameterNode, "tetgenRatioParameter", "5") 514 | self.setParameterIfNotDefined(parameterNode, "tetgenAngleParameter", "5") 515 | self.setParameterIfNotDefined(parameterNode, "tetgenVolumeParameter", "5") 516 | self.setParameterIfNotDefined(parameterNode, "tetGenAdditionalParameters", "") 517 | self.setParameterIfNotDefined(parameterNode, "customTetGenPath", "") 518 | 519 | 520 | def setParameterIfNotDefined(self, parameterNode, key, value): 521 | if not parameterNode.GetParameter(key): 522 | parameterNode.SetParameter(key, value) 523 | 524 | def addLog(self, text): 525 | logging.info(text) 526 | if self.logCallback: 527 | self.logCallback(text) 528 | 529 | def getCleaverPath(self): 530 | if self.cleaverPath: 531 | return self.cleaverPath 532 | 533 | self.cleaverPath = self.getCustomCleaverPath() 534 | if self.cleaverPath: 535 | return self.cleaverPath 536 | 537 | for binDirCandidate in self.binDirCandidates: 538 | cleaverPath = os.path.abspath(os.path.join(binDirCandidate, self.cleaverFilename)) 539 | logging.debug("Attempt to find executable at: "+cleaverPath) 540 | if os.path.isfile(cleaverPath): 541 | # found 542 | self.cleaverPath = cleaverPath 543 | return self.cleaverPath 544 | 545 | raise ValueError('Cleaver not found') 546 | 547 | def getTetGenPath(self): 548 | if self.tetGenPath: 549 | return self.tetGenPath 550 | 551 | self.tetGenPath = self.getCustomTetGenPath() 552 | if self.tetGenPath: 553 | return self.tetGenPath 554 | 555 | for tetGenBinDirCandidate in self.binDirCandidates: 556 | tetGenPath = os.path.abspath(os.path.join(tetGenBinDirCandidate, self.tetGenFilename)) 557 | logging.debug("Attempt to find executable at: "+tetGenPath) 558 | if os.path.isfile(tetGenPath): 559 | # TetGen found 560 | self.tetGenPath = tetGenPath 561 | return self.tetGenPath 562 | 563 | raise ValueError('TetGen not found') 564 | 565 | def getCustomCleaverPath(self): 566 | settings = qt.QSettings() 567 | if settings.contains(self.customCleaverPathSettingsKey): 568 | return settings.value(self.customCleaverPathSettingsKey) 569 | return '' 570 | 571 | def getCustomTetGenPath(self): 572 | settings = qt.QSettings() 573 | if settings.contains(self.customTetGenPathSettingsKey): 574 | return settings.value(self.customTetGenPathSettingsKey) 575 | return '' 576 | 577 | def setCustomCleaverPath(self, customPath): 578 | # don't save it if already saved 579 | settings = qt.QSettings() 580 | if settings.contains(self.customCleaverPathSettingsKey): 581 | if customPath == settings.value(self.customCleaverPathSettingsKey): 582 | return 583 | settings.setValue(self.customCleaverPathSettingsKey, customPath) 584 | # Update Cleaver bin dir 585 | self.cleaverPath = None 586 | self.getCleaverPath() 587 | 588 | def setCustomTetGenPath(self, customPath): 589 | # don't save it if already saved 590 | settings = qt.QSettings() 591 | if settings.contains(self.customTetGenPathSettingsKey): 592 | if customPath == settings.value(self.customTetGenPathSettingsKey): 593 | return 594 | settings.setValue(self.customTetGenPathSettingsKey, customPath) 595 | # Update TetGen bin dir 596 | self.tetGenPath = None 597 | self.getTetGenPath() 598 | 599 | def startMesher(self, cmdLineArguments, executableFilePath): 600 | self.addLog("Generating volumetric mesh...") 601 | import subprocess 602 | 603 | # Hide console window on Windows 604 | from sys import platform 605 | if platform == "win32": 606 | info = subprocess.STARTUPINFO() 607 | info.dwFlags = 1 608 | info.wShowWindow = 0 609 | else: 610 | info = None 611 | 612 | logging.info("Generate mesh using: "+executableFilePath+": "+repr(cmdLineArguments)) 613 | return subprocess.Popen([executableFilePath] + cmdLineArguments, 614 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, startupinfo=info) 615 | 616 | def logProcessOutput(self, process, processName): 617 | # save process output (if not logged) so that it can be displayed in case of an error 618 | processOutput = '' 619 | import subprocess 620 | for stdout_line in iter(process.stdout.readline, ""): 621 | if self.logStandardOutput: 622 | self.addLog(stdout_line.rstrip()) 623 | else: 624 | processOutput += stdout_line.rstrip() + '\n' 625 | slicer.app.processEvents() # give a chance to click Cancel button 626 | if self.abortRequested: 627 | process.kill() 628 | process.stdout.close() 629 | return_code = process.wait() 630 | if return_code: 631 | if self.abortRequested: 632 | raise ValueError("User requested cancel.") 633 | else: 634 | if processOutput: 635 | self.addLog(processOutput) 636 | raise subprocess.CalledProcessError(return_code, processName) 637 | 638 | def getTempDirectoryBase(self): 639 | tempDir = qt.QDir(slicer.app.temporaryPath) 640 | fileInfo = qt.QFileInfo(qt.QDir(tempDir), "SegmentMesher") 641 | dirPath = fileInfo.absoluteFilePath() 642 | qt.QDir().mkpath(dirPath) 643 | return dirPath 644 | 645 | def createTempDirectory(self): 646 | import qt, slicer 647 | tempDir = qt.QDir(self.getTempDirectoryBase()) 648 | tempDirName = qt.QDateTime().currentDateTime().toString("yyyyMMdd_hhmmss_zzz") 649 | fileInfo = qt.QFileInfo(qt.QDir(tempDir), tempDirName) 650 | dirPath = fileInfo.absoluteFilePath() 651 | qt.QDir().mkpath(dirPath) 652 | return dirPath 653 | 654 | def createMeshFromSegmentationCleaver(self, inputSegmentation, outputMeshNode, segments = [], additionalParameters = None, removeBackgroundMesh = False, 655 | paddingRatio = 0.10, featureScale = 2, samplingRate=0.2, rateOfChange=0.2): 656 | 657 | if additionalParameters is None: 658 | additionalParameters="" 659 | 660 | 661 | self.abortRequested = False 662 | tempDir = self.createTempDirectory() 663 | self.addLog('Mesh generation using Cleaver is started in working directory: '+tempDir) 664 | 665 | inputParamsCleaver = [] 666 | 667 | # Write inputs 668 | qt.QDir().mkpath(tempDir) 669 | 670 | # Create temporary labelmap node. It will be used both for storing reference geometry 671 | # and resulting merged labelmap. 672 | labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode') 673 | parentTransformNode = inputSegmentation.GetParentTransformNode() 674 | labelmapVolumeNode.SetAndObserveTransformNodeID(parentTransformNode.GetID() if parentTransformNode else None) 675 | 676 | # Create binary labelmap representation using default parameters 677 | if not inputSegmentation.GetSegmentation().CreateRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()): 678 | self.addLog('Failed to create binary labelmap representation') 679 | return 680 | 681 | # Set reference geometry in labelmapVolumeNode 682 | referenceGeometry_Segmentation = slicer.vtkOrientedImageData() 683 | inputSegmentation.GetSegmentation().SetImageGeometryFromCommonLabelmapGeometry(referenceGeometry_Segmentation, None, 684 | slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY) 685 | slicer.modules.segmentations.logic().CopyOrientedImageDataToVolumeNode(referenceGeometry_Segmentation, labelmapVolumeNode) 686 | 687 | # Add margin 688 | extent = labelmapVolumeNode.GetImageData().GetExtent() 689 | paddedExtent = [0, -1, 0, -1, 0, -1] 690 | for axisIndex in range(3): 691 | paddingSizeVoxels = int((extent[axisIndex * 2 + 1] - extent[axisIndex * 2]) * paddingRatio) 692 | paddedExtent[axisIndex * 2] = extent[axisIndex * 2] - paddingSizeVoxels 693 | paddedExtent[axisIndex * 2 + 1] = extent[axisIndex * 2 + 1] + paddingSizeVoxels 694 | labelmapVolumeNode.GetImageData().SetExtent(paddedExtent) 695 | labelmapVolumeNode.ShiftImageDataExtentToZeroStart() 696 | 697 | # Get merged labelmap 698 | segmentIdList = vtk.vtkStringArray() 699 | 700 | for segment in segments: 701 | segmentIdList.InsertNextValue(segment) 702 | 703 | if segmentIdList.GetNumberOfValues() == 0: 704 | self.addLog("No input segments are selected, therefore no output is generated.") 705 | return 706 | 707 | slicer.modules.segmentations.logic().ExportSegmentsToLabelmapNode(inputSegmentation, segmentIdList, labelmapVolumeNode, labelmapVolumeNode) 708 | 709 | 710 | inputLabelmapVolumeFilePath = os.path.join(tempDir, "inputLabelmap.nrrd") 711 | slicer.util.saveNode(labelmapVolumeNode, inputLabelmapVolumeFilePath, {"useCompression": False}) 712 | inputParamsCleaver.extend(["--input_files", inputLabelmapVolumeFilePath]) 713 | 714 | # Keep IJK to RAS matrix, we'll need it later 715 | unscaledIjkToRasMatrix = vtk.vtkMatrix4x4() 716 | labelmapVolumeNode.GetIJKToRASDirectionMatrix(unscaledIjkToRasMatrix) # axis directions, without scaling by spacing 717 | ijkToRasMatrix = vtk.vtkMatrix4x4() 718 | labelmapVolumeNode.GetIJKToRASMatrix(ijkToRasMatrix) 719 | origin = ijkToRasMatrix.MultiplyPoint([-0.5, -0.5, -0.5, 1.0]) # Cleaver uses the voxel corner as its origin, therefore we need a half-voxel offset 720 | for i in range(3): 721 | unscaledIjkToRasMatrix.SetElement(i,3, origin[i]) 722 | 723 | # Keep color node, we'll need it later 724 | colorTableNode = labelmapVolumeNode.GetDisplayNode().GetColorNode() 725 | # Background color is transparent by default which is not ideal for 3D display 726 | colorTableNode.SetColor(0,0.6,0.6,0.6,1.0) 727 | 728 | slicer.mrmlScene.RemoveNode(labelmapVolumeNode) 729 | slicer.mrmlScene.RemoveNode(colorTableNode) 730 | 731 | #User set parameters 732 | inputParamsCleaver.extend(["--feature_scaling", "{:.2f}".format(featureScale)]) 733 | inputParamsCleaver.extend(["--sampling_rate", "{:.2f}".format(samplingRate)]) 734 | inputParamsCleaver.extend(["--lipschitz", "{:.2f}".format(rateOfChange)]) 735 | 736 | # Set up output format 737 | 738 | inputParamsCleaver.extend(["--output_path", tempDir+"/"]) 739 | inputParamsCleaver.extend(["--output_format", "vtkUSG"]) # VTK unstructed grid 740 | inputParamsCleaver.append("--fix_tet_windup") # prevent inside-out tets 741 | inputParamsCleaver.append("--strip_exterior") # remove temporary elements that are added to make the volume cubic 742 | 743 | inputParamsCleaver.append("--verbose") 744 | 745 | # Quality 746 | if additionalParameters: 747 | inputParamsCleaver.extend(additionalParameters.split(' ')) 748 | 749 | # Run Cleaver 750 | ep = self.startMesher(inputParamsCleaver, self.getCleaverPath()) 751 | self.logProcessOutput(ep, self.cleaverFilename) 752 | 753 | # Read results 754 | if not self.abortRequested: 755 | outputVolumetricMeshPath = os.path.join(tempDir, "output.vtk") 756 | outputReader = vtk.vtkUnstructuredGridReader() 757 | outputReader.SetFileName(outputVolumetricMeshPath) 758 | outputReader.ReadAllScalarsOn() 759 | outputReader.ReadAllVectorsOn() 760 | outputReader.ReadAllNormalsOn() 761 | outputReader.ReadAllTensorsOn() 762 | outputReader.ReadAllColorScalarsOn() 763 | outputReader.ReadAllTCoordsOn() 764 | outputReader.ReadAllFieldsOn() 765 | outputReader.Update() 766 | 767 | # Cleaver returns the mesh in voxel coordinates, need to transform to RAS space 768 | transformer = vtk.vtkTransformFilter() 769 | transformer.SetInputData(outputReader.GetOutput()) 770 | ijkToRasTransform = vtk.vtkTransform() 771 | ijkToRasTransform.SetMatrix(unscaledIjkToRasMatrix) 772 | transformer.SetTransform(ijkToRasTransform) 773 | 774 | if removeBackgroundMesh: 775 | transformer.Update() 776 | mesh = transformer.GetOutput() 777 | cellData = mesh.GetCellData() 778 | cellData.SetActiveScalars("labels") 779 | backgroundMeshRemover = vtk.vtkThreshold() 780 | backgroundMeshRemover.SetInputData(mesh) 781 | backgroundMeshRemover.SetInputArrayToProcess(0, 0, 0, vtk.vtkDataObject.FIELD_ASSOCIATION_CELLS, vtk.vtkDataSetAttributes.SCALARS) 782 | backgroundMeshRemover.SetLowerThreshold(1) 783 | outputMeshNode.SetUnstructuredGridConnection(backgroundMeshRemover.GetOutputPort()) 784 | else: 785 | outputMeshNode.SetUnstructuredGridConnection(transformer.GetOutputPort()) 786 | 787 | outputMeshDisplayNode = outputMeshNode.GetDisplayNode() 788 | if not outputMeshDisplayNode: 789 | # Initial setup of display node 790 | outputMeshNode.CreateDefaultDisplayNodes() 791 | 792 | outputMeshDisplayNode = outputMeshNode.GetDisplayNode() 793 | outputMeshDisplayNode.SetEdgeVisibility(True) 794 | outputMeshDisplayNode.SetClipping(True) 795 | 796 | colorTableNode = slicer.mrmlScene.AddNode(colorTableNode) 797 | outputMeshDisplayNode.SetAndObserveColorNodeID(colorTableNode.GetID()) 798 | 799 | outputMeshDisplayNode.ScalarVisibilityOn() 800 | outputMeshDisplayNode.SetActiveScalarName('labels') 801 | outputMeshDisplayNode.SetActiveAttributeLocation(vtk.vtkAssignAttribute.CELL_DATA) 802 | outputMeshDisplayNode.SetSliceIntersectionVisibility(True) 803 | outputMeshDisplayNode.SetSliceIntersectionOpacity(0.5) 804 | outputMeshDisplayNode.SetScalarRangeFlag(slicer.vtkMRMLDisplayNode.UseColorNodeScalarRange) 805 | else: 806 | currentColorNode = outputMeshDisplayNode.GetColorNode() 807 | if currentColorNode is not None and currentColorNode.GetType() == currentColorNode.User and currentColorNode.IsA("vtkMRMLColorTableNode"): 808 | # current color table node can be overwritten 809 | currentColorNode.Copy(colorTableNode) 810 | else: 811 | colorTableNode = slicer.mrmlScene.AddNode(colorTableNode) 812 | outputMeshDisplayNode.SetAndObserveColorNodeID(colorTableNode.GetID()) 813 | 814 | # Flip clipping setting twice, this workaround forces update of the display pipeline 815 | # when switching between surface and volumetric mesh 816 | outputMeshDisplayNode.SetClipping(not outputMeshDisplayNode.GetClipping()) 817 | outputMeshDisplayNode.SetClipping(not outputMeshDisplayNode.GetClipping()) 818 | 819 | # Clean up 820 | if self.deleteTemporaryFiles: 821 | import shutil 822 | shutil.rmtree(tempDir) 823 | 824 | self.addLog("Model generation is completed") 825 | 826 | def createMeshFromSegmentationTetGen(self, inputSegmentation, outputMeshNode, segments = [], additionalParameters="", ratio=5, angle=0, volume=10): 827 | 828 | segmentIdList = vtk.vtkStringArray() 829 | for segment in segments: 830 | segmentIdList.InsertNextValue(segment) 831 | 832 | if segmentIdList.GetNumberOfValues() == 0: 833 | logging.info("createMeshFromSegmentationTetGen skipped: there are no selected segments") 834 | return 835 | inputSegmentation.CreateClosedSurfaceRepresentation() 836 | appender = vtk.vtkAppendPolyData() 837 | for i in range(segmentIdList.GetNumberOfValues()): 838 | segmentId = segmentIdList.GetValue(i) 839 | 840 | #Use old function arguments for 4.10 841 | if slicer.app.majorVersion == 4 and slicer.app.minorVersion < 11: 842 | polydata = inputSegmentation.GetClosedSurfaceRepresentation(segmentId) 843 | else: 844 | polydata = vtk.vtkPolyData() 845 | inputSegmentation.GetClosedSurfaceRepresentation(segmentId, polydata) 846 | appender.AddInputData(polydata) 847 | 848 | appender.Update() 849 | self.createMeshFromPolyDataTetGen(appender.GetOutput(), outputMeshNode, additionalParameters, ratio, angle, volume) 850 | 851 | #Clean up representation 852 | inputSegmentation.GetSegmentation().RemoveRepresentation(slicer.vtkSegmentationConverter().GetClosedSurfaceRepresentationName()) 853 | 854 | def createMeshFromPolyDataTetGen(self, inputPolyData, outputMeshNode, additionalParameters="", ratio=5, angle=0, volume=10): 855 | 856 | self.abortRequested = False 857 | tempDir = self.createTempDirectory() 858 | self.addLog('Mesh generation is started in working directory: '+tempDir) 859 | 860 | # Write inputs 861 | qt.QDir().mkpath(tempDir) 862 | 863 | inputSurfaceMeshFilePath = os.path.join(tempDir, "mesh.ply") 864 | inputWriter = vtk.vtkPLYWriter() 865 | inputWriter.SetInputData(inputPolyData) 866 | inputWriter.SetFileName(inputSurfaceMeshFilePath) 867 | inputWriter.SetFileTypeToASCII() 868 | inputWriter.Write() 869 | 870 | #Command line for quality parameters 871 | parameters = 'q'+"{:.2f}".format(ratio)+'/'+"{:.2f}".format(angle)+'a'+"{:.2f}".format(volume) 872 | 873 | inputParamsTetGen = [] 874 | inputParamsTetGen.append("-k"+parameters+additionalParameters) 875 | inputParamsTetGen.append(inputSurfaceMeshFilePath) 876 | 877 | # Run tetgen 878 | ep = self.startMesher(inputParamsTetGen, self.getTetGenPath()) 879 | self.logProcessOutput(ep, self.tetGenFilename) 880 | 881 | # Read results 882 | if not self.abortRequested: 883 | outputVolumetricMeshPath = os.path.join(tempDir, "mesh.1.vtk") 884 | outputReader = vtk.vtkUnstructuredGridReader() 885 | outputReader.SetFileName(outputVolumetricMeshPath) 886 | outputReader.ReadAllScalarsOn() 887 | outputReader.ReadAllVectorsOn() 888 | outputReader.ReadAllNormalsOn() 889 | outputReader.ReadAllTensorsOn() 890 | outputReader.ReadAllColorScalarsOn() 891 | outputReader.ReadAllTCoordsOn() 892 | outputReader.ReadAllFieldsOn() 893 | outputReader.Update() 894 | outputMeshNode.SetUnstructuredGridConnection(outputReader.GetOutputPort()) 895 | 896 | outputMeshDisplayNode = outputMeshNode.GetDisplayNode() 897 | if not outputMeshDisplayNode: 898 | # Initial setup of display node 899 | outputMeshNode.CreateDefaultDisplayNodes() 900 | outputMeshDisplayNode = outputMeshNode.GetDisplayNode() 901 | outputMeshDisplayNode.SetEdgeVisibility(True) 902 | outputMeshDisplayNode.SetClipping(True) 903 | 904 | # Clean up 905 | if self.deleteTemporaryFiles: 906 | import shutil 907 | shutil.rmtree(tempDir) 908 | 909 | self.addLog("Model generation is completed") 910 | 911 | class SegmentMesherTest(ScriptedLoadableModuleTest): 912 | """ 913 | This is the test case for your scripted module. 914 | Uses ScriptedLoadableModuleTest base class, available at: 915 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 916 | """ 917 | 918 | def setUp(self): 919 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. 920 | """ 921 | slicer.mrmlScene.Clear(0) 922 | 923 | def runTest(self): 924 | """Run as few or as many tests as needed here. 925 | """ 926 | self.setUp() 927 | self.test_TetGen1() 928 | 929 | def test_TetGen1(self): 930 | """ Ideally you should have several levels of tests. At the lowest level 931 | tests should exercise the functionality of the logic with different inputs 932 | (both valid and invalid). At higher levels your tests should emulate the 933 | way the user would interact with your code and confirm that it still works 934 | the way you intended. 935 | One of the most important features of the tests is that it should alert other 936 | developers when their changes will have an impact on the behavior of your 937 | module. For example, if a developer removes a feature that you depend on, 938 | your test should break so they know that the feature is needed. 939 | """ 940 | 941 | self.delayDisplay("Starting the test") 942 | 943 | cylinder = vtk.vtkCylinderSource() 944 | cylinder.SetRadius(10) 945 | cylinder.SetHeight(40) 946 | cylinder.Update() 947 | inputModelNode = slicer.modules.models.logic().AddModel(cylinder.GetOutput()) 948 | 949 | outputModelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") 950 | outputModelNode.CreateDefaultDisplayNodes() 951 | 952 | logic = SegmentMesherLogic() 953 | logic.createMeshFromPolyDataTetGen(inputModelNode.GetPolyData(), outputModelNode, '', 100, 0, 100) 954 | 955 | self.assertTrue(outputModelNode.GetMesh().GetNumberOfPoints()>0) 956 | self.assertTrue(outputModelNode.GetMesh().GetNumberOfCells()>0) 957 | 958 | inputModelNode.GetDisplayNode().SetOpacity(0.2) 959 | 960 | outputDisplayNode = outputModelNode.GetDisplayNode() 961 | outputDisplayNode.SetColor(1,0,0) 962 | outputDisplayNode.SetEdgeVisibility(True) 963 | outputDisplayNode.SetClipping(True) 964 | 965 | clipNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLClipModelsNode") 966 | clipNode.SetRedSliceClipState(clipNode.ClipNegativeSpace) 967 | 968 | self.delayDisplay('Test passed!') 969 | 970 | METHOD_CLEAVER = 'CLEAVER' 971 | METHOD_TETGEN = 'TETGEN' 972 | -------------------------------------------------------------------------------- /SegmentMesher/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /SegmentMesher/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /SlicerSegmentMesher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/37a56a4216048df01af0c6392d90751f47707cea/SlicerSegmentMesher.png -------------------------------------------------------------------------------- /SlicerSegmentMesher.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lassoan/SlicerSegmentMesher/37a56a4216048df01af0c6392d90751f47707cea/SlicerSegmentMesher.xcf -------------------------------------------------------------------------------- /SuperBuild.cmake: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | # Enable and setup External project global properties 3 | #----------------------------------------------------------------------------- 4 | 5 | set(ep_common_c_flags "${CMAKE_C_FLAGS_INIT} ${ADDITIONAL_C_FLAGS}") 6 | set(ep_common_cxx_flags "${CMAKE_CXX_FLAGS_INIT} ${ADDITIONAL_CXX_FLAGS}") 7 | 8 | #----------------------------------------------------------------------------- 9 | # Project dependencies 10 | #----------------------------------------------------------------------------- 11 | 12 | include(ExternalProject) 13 | 14 | foreach(dep ${EXTENSION_DEPENDS}) 15 | mark_as_superbuild(${dep}_DIR) 16 | endforeach() 17 | 18 | set(proj ${SUPERBUILD_TOPLEVEL_PROJECT}) 19 | set(${proj}_DEPENDS tetgen cleaver) 20 | 21 | ExternalProject_Include_Dependencies(${proj} 22 | PROJECT_VAR proj 23 | SUPERBUILD_VAR ${EXTENSION_NAME}_SUPERBUILD 24 | ) 25 | 26 | ExternalProject_Add(${proj} 27 | ${${proj}_EP_ARGS} 28 | DOWNLOAD_COMMAND "" 29 | INSTALL_COMMAND "" 30 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR} 31 | BINARY_DIR ${EXTENSION_BUILD_SUBDIRECTORY} 32 | BUILD_ALWAYS 1 33 | CMAKE_CACHE_ARGS 34 | -DSubversion_SVN_EXECUTABLE:STRING=${Subversion_SVN_EXECUTABLE} 35 | -DGIT_EXECUTABLE:STRING=${GIT_EXECUTABLE} 36 | -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} 37 | -DCMAKE_CXX_FLAGS:STRING=${ep_common_cxx_flags} 38 | -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} 39 | -DCMAKE_C_FLAGS:STRING=${ep_common_c_flags} 40 | -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} 41 | -DCMAKE_RUNTIME_OUTPUT_DIRECTORY:PATH=${CMAKE_RUNTIME_OUTPUT_DIRECTORY} 42 | -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} 43 | -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${CMAKE_ARCHIVE_OUTPUT_DIRECTORY} 44 | -DMIDAS_PACKAGE_EMAIL:STRING=${MIDAS_PACKAGE_EMAIL} 45 | -DMIDAS_PACKAGE_API_KEY:STRING=${MIDAS_PACKAGE_API_KEY} 46 | -D${EXTENSION_NAME}_SUPERBUILD:BOOL=OFF 47 | -DEXTENSION_SUPERBUILD_BINARY_DIR:PATH=${${EXTENSION_NAME}_BINARY_DIR} 48 | DEPENDS 49 | ${${proj}_DEPENDS} 50 | ) 51 | -------------------------------------------------------------------------------- /SuperBuild/External_cleaver.cmake: -------------------------------------------------------------------------------- 1 | set(proj cleaver) 2 | 3 | # Set dependency list 4 | 5 | if (NOT ITK_FOUND) 6 | # Cleaver is bundled with Slicer in a custom application. 7 | # In this case ITK dependency must be added. 8 | set(${proj}_DEPENDS ITK) 9 | endif() 10 | 11 | # Include dependent projects if any 12 | ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj) 13 | 14 | if(${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) 15 | message(FATAL_ERROR "Enabling ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj} is not supported !") 16 | endif() 17 | 18 | # Sanity checks 19 | if(DEFINED cleaver_DIR AND NOT EXISTS ${cleaver_DIR}) 20 | message(FATAL_ERROR "cleaver_DIR variable is defined but corresponds to nonexistent directory") 21 | endif() 22 | 23 | if(NOT DEFINED ${proj}_DIR AND NOT ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) 24 | 25 | set(${proj}_INSTALL_DIR ${CMAKE_BINARY_DIR}/${proj}-install) 26 | set(${proj}_DIR ${CMAKE_BINARY_DIR}/${proj}-build) 27 | 28 | ExternalProject_Add(${proj} 29 | # Slicer 30 | ${${proj}_EP_ARGS} 31 | SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj} 32 | SOURCE_SUBDIR src # requires CMake 3.7 or later 33 | BINARY_DIR ${proj}-build 34 | INSTALL_DIR ${${proj}_INSTALL_DIR} 35 | GIT_REPOSITORY "${EP_GIT_PROTOCOL}://github.com/SCIInstitute/Cleaver2.git" 36 | GIT_TAG "e4fa5b091b450d736c479cad4f0349fc08b8fb16" 37 | #--Patch step------------- 38 | #PATCH_COMMAND ${CMAKE_COMMAND} -Delastix_SRC_DIR=${CMAKE_BINARY_DIR}/${proj} 39 | # -P ${CMAKE_CURRENT_LIST_DIR}/${proj}_patch.cmake 40 | #--Configure step------------- 41 | CMAKE_CACHE_ARGS 42 | -DGIT_EXECUTABLE:STRING=${GIT_EXECUTABLE} 43 | -DITK_DIR:STRING=${ITK_DIR} 44 | -DBUILD_CLI:BOOL=ON 45 | -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} 46 | -DCMAKE_CXX_FLAGS:STRING=${ep_common_cxx_flags} 47 | -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} 48 | -DCMAKE_C_FLAGS:STRING=${ep_common_c_flags} 49 | -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD} 50 | -DCMAKE_CXX_STANDARD_REQUIRED:BOOL=${CMAKE_CXX_STANDARD_REQUIRED} 51 | -DCMAKE_CXX_EXTENSIONS:BOOL=${CMAKE_CXX_EXTENSIONS} 52 | -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} 53 | -DBUILD_TESTING:BOOL=OFF 54 | -DCMAKE_MACOSX_RPATH:BOOL=0 55 | -DCMAKE_RUNTIME_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_BIN_DIR} 56 | -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_LIB_DIR} 57 | -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${CMAKE_ARCHIVE_OUTPUT_DIRECTORY} 58 | -DCLEAVER2_RUNTIME_DIR:STRING=${Slicer_INSTALL_THIRDPARTY_LIB_DIR} 59 | -DCLEAVER2_LIBRARY_DIR:STRING=${Slicer_INSTALL_THIRDPARTY_LIB_DIR} 60 | #--Build step----------------- 61 | #--Install step----------------- 62 | # Don't perform installation at the end of the build 63 | INSTALL_COMMAND "" 64 | DEPENDS 65 | ${${proj}_DEPENDS} 66 | ) 67 | 68 | else() 69 | ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDS}) 70 | endif() 71 | 72 | mark_as_superbuild(${proj}_DIR:PATH) 73 | -------------------------------------------------------------------------------- /SuperBuild/External_tetgen.cmake: -------------------------------------------------------------------------------- 1 | set(proj tetgen) 2 | 3 | # Set dependency list 4 | set(${proj}_DEPENDS "") 5 | 6 | # Include dependent projects if any 7 | ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj) 8 | 9 | if(${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) 10 | message(FATAL_ERROR "Enabling ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj} is not supported !") 11 | endif() 12 | 13 | # Sanity checks 14 | if(DEFINED tetgen_DIR AND NOT EXISTS ${tetgen_DIR}) 15 | message(FATAL_ERROR "tetgen_DIR variable is defined but corresponds to nonexistent directory") 16 | endif() 17 | 18 | if(NOT DEFINED ${proj}_DIR AND NOT ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj}) 19 | 20 | set(${proj}_INSTALL_DIR ${CMAKE_BINARY_DIR}/${proj}-install) 21 | set(${proj}_DIR ${CMAKE_BINARY_DIR}/${proj}-build) 22 | 23 | ExternalProject_Add(${proj} 24 | # Slicer 25 | ${${proj}_EP_ARGS} 26 | SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj} 27 | #SOURCE_SUBDIR src # requires CMake 3.7 or later 28 | BINARY_DIR ${proj}-build 29 | INSTALL_DIR ${${proj}_INSTALL_DIR} 30 | GIT_REPOSITORY "${EP_GIT_PROTOCOL}://github.com/lassoan/tetgen.git" 31 | #GIT_TAG "ef057ff89233822b26b04b31c3c043af57d5deff" 32 | #--Patch step------------- 33 | #PATCH_COMMAND ${CMAKE_COMMAND} -Delastix_SRC_DIR=${CMAKE_BINARY_DIR}/${proj} 34 | # -P ${CMAKE_CURRENT_LIST_DIR}/${proj}_patch.cmake 35 | #--Configure step------------- 36 | CMAKE_CACHE_ARGS 37 | -DGIT_EXECUTABLE:STRING=${GIT_EXECUTABLE} 38 | -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} 39 | -DCMAKE_CXX_FLAGS:STRING=${ep_common_cxx_flags} 40 | -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} 41 | -DCMAKE_C_FLAGS:STRING=${ep_common_c_flags} 42 | -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} 43 | -DBUILD_TESTING:BOOL=OFF 44 | -DCMAKE_MACOSX_RPATH:BOOL=0 45 | # location of build outputs in the build tree 46 | -DCMAKE_RUNTIME_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_BIN_DIR} 47 | -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_LIB_DIR} 48 | -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${CMAKE_ARCHIVE_OUTPUT_DIRECTORY} 49 | # location of build outputs in the installation folder 50 | -DTETGEN_RUNTIME_DIR:STRING=${Slicer_INSTALL_THIRDPARTY_LIB_DIR} 51 | #--Build step----------------- 52 | #--Install step----------------- 53 | # Don't perform installation at the end of the build 54 | INSTALL_COMMAND "" 55 | DEPENDS 56 | ${${proj}_DEPENDS} 57 | ) 58 | 59 | else() 60 | ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDS}) 61 | endif() 62 | 63 | mark_as_superbuild(${proj}_DIR:PATH) 64 | --------------------------------------------------------------------------------