├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── Resources ├── Media │ ├── CarveHoles.png │ ├── CustomRegion.png │ ├── SplitCavities.png │ ├── header.png │ ├── processing.png │ ├── result.gif │ └── threshold.png └── Screenshots │ └── screenshot4.png ├── SegmentEditorWrapSolidify ├── CMakeLists.txt ├── SegmentEditorWrapSolidify.py ├── SegmentEditorWrapSolidifyLib │ ├── SegmentEditorEffect.png │ ├── SegmentEditorEffect.py │ ├── SegmentEditorEffect.ui │ └── __init__.py └── Testing │ ├── CMakeLists.txt │ └── Python │ └── CMakeLists.txt └── SurfaceWrapSolidify.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=python,visualstudiocode 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # celery beat schedule file 99 | celerybeat-schedule 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | ### VisualStudioCode ### 132 | .vscode/* 133 | 134 | ### VisualStudioCode Patch ### 135 | # Ignore all local history of files 136 | .history 137 | 138 | # End of https://www.gitignore.io/api/python,visualstudiocode 139 | 140 | *.s4ext -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(SurfaceWrapSolidify) 4 | 5 | #----------------------------------------------------------------------------- 6 | # Extension meta-information 7 | set(EXTENSION_HOMEPAGE "https://github.com/sebastianandress/Slicer-SurfaceWrapSolidify") 8 | set(EXTENSION_CATEGORY "Segmentation") 9 | set(EXTENSION_CONTRIBUTORS "Sebastian Andress (LMU Munich)") 10 | set(EXTENSION_DESCRIPTION "A Segment Editor Extension that filters the surface of a segment using a combination of shrinkwrapping and raycasting.") 11 | set(EXTENSION_ICONURL "https://github.com/sebastianandress/Slicer-SurfaceWrapSolidify/raw/master/SurfaceWrapSolidify.png") 12 | set(EXTENSION_SCREENSHOTURLS "https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/master/Resources/Screenshots/screenshot4.png") 13 | set(EXTENSION_DEPENDS "NA") 14 | 15 | #----------------------------------------------------------------------------- 16 | # Extension dependencies 17 | find_package(Slicer REQUIRED) 18 | include(${Slicer_USE_FILE}) 19 | 20 | #----------------------------------------------------------------------------- 21 | # Extension modules 22 | add_subdirectory(SegmentEditorWrapSolidify) 23 | ## NEXT_MODULE 24 | 25 | #----------------------------------------------------------------------------- 26 | include(${Slicer_EXTENSION_GENERATE_CONFIG}) 27 | include(${Slicer_EXTENSION_CPACK}) 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2019, Sebastian Andreß**\ 2 | All rights reserved. 3 | 4 | 5 | For more information, please see: 6 | http://www.slicer.org 7 | 8 | The 3D Slicer license below is a BSD style license, with extensions to cover contributions and other issues specific to 3D Slicer. 9 | 10 | 11 | # 3D Slicer Contribution and Software License Agreement ("Agreement"), Version 1.0 (December 20, 2005) 12 | 13 | This Agreement covers contributions to and downloads from the 3D Slicer project ("Slicer") maintained by The Brigham and Women's Hospital, Inc. ("Brigham"). Part A of this Agreement applies to contributions of software and/or data to Slicer (including making revisions of or additions to code and/or data already in Slicer). Part B of this Agreement applies to downloads of software and/or data from Slicer. Part C of this Agreement applies to all transactions with Slicer. If you distribute Software (as defined below) downloaded from Slicer, all of the paragraphs of Part B of this Agreement must be included with and apply to such Software. 14 | 15 | Your contribution of software and/or data to Slicer (including prior to the date of the first publication of this Agreement, each a "Contribution") and/or downloading, copying, modifying, displaying, distributing or use of any software and/or data from Slicer (collectively, the "Software") constitutes acceptance of all of the terms and conditions of this Agreement. If you do not agree to such terms and conditions, you have no right to contribute your Contribution, or to download, copy, modify, display, distribute or use the Software. 16 | 17 | ## PART A. CONTRIBUTION AGREEMENT - License to Brigham with Right to Sublicense ("Contribution Agreement"). 18 | 19 | 1. As used in this Contribution Agreement, "you" means the individual contributing the Contribution to Slicer and the institution or entity which employs or is otherwise affiliated with such individual in connection with such Contribution. 20 | 21 | 2. This Contribution Agreement applies to all Contributions made to Slicer, including without limitation Contributions made prior to the date of first publication of this Agreement. If at any time you make a Contribution to Slicer, you represent that (i) you are legally authorized and entitled to make such Contribution and to grant all licenses granted in this Contribution Agreement with respect to such Contribution; (ii) if your Contribution includes any patient data, all such data is de-identified in accordance with U.S. confidentiality and security laws and requirements, including but not limited to the Health Insurance Portability and Accountability Act (HIPAA) and its regulations, and your disclosure of such data for the purposes contemplated by this Agreement is properly authorized and in compliance with all applicable laws and regulations; and (iii) you have preserved in the Contribution all applicable attributions, copyright notices and licenses for any third party software or data included in the Contribution. 22 | 23 | 3. Except for the licenses granted in this Agreement, you reserve all right, title and interest in your Contribution. 24 | 25 | 4. You hereby grant to Brigham, with the right to sublicense, a perpetual, worldwide, non-exclusive, no charge, royalty-free, irrevocable license to use, reproduce, make derivative works of, display and distribute the Contribution. If your Contribution is protected by patent, you hereby grant to Brigham, with the right to sublicense, a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under your interest in patent rights covering the Contribution, to make, have made, use, sell and otherwise transfer your Contribution, alone or in combination with any other code. 26 | 27 | 5. You acknowledge and agree that Brigham may incorporate your Contribution into Slicer and may make Slicer available to members of the public on an open source basis under terms substantially in accordance with the Software License set forth in Part B of this Agreement. You further acknowledge and agree that Brigham shall have no liability arising in connection with claims resulting from your breach of any of the terms of this Agreement. 28 | 29 | 6. YOU WARRANT THAT TO THE BEST OF YOUR KNOWLEDGE YOUR CONTRIBUTION DOES NOT CONTAIN ANY CODE THAT REQURES OR PRESCRIBES AN "OPEN SOURCE LICENSE" FOR DERIVATIVE WORKS (by way of non-limiting example, the GNU General Public License or other so-called "reciprocal" license that requires any derived work to be licensed under the GNU General Public License or other "open source license"). 30 | 31 | ## PART B. DOWNLOADING AGREEMENT - License from Brigham with Right to Sublicense ("Software License"). 32 | 33 | 1. As used in this Software License, "you" means the individual downloading and/or using, reproducing, modifying, displaying and/or distributing the Software and the institution or entity which employs or is otherwise affiliated with such individual in connection therewith. The Brigham and Women's Hospital, Inc. ("Brigham") hereby grants you, with right to sublicense, with respect to Brigham's rights in the software, and data, if any, which is the subject of this Software License (collectively, the "Software"), a royalty-free, non-exclusive license to use, reproduce, make derivative works of, display and distribute the Software, provided that: 34 | 35 | 1. you accept and adhere to all of the terms and conditions of this Software License; 36 | 37 | 2. in connection with any copy of or sublicense of all or any portion of the Software, all of the terms and conditions in this Software License shall appear in and shall apply to such copy and such sublicense, including without limitation all source and executable forms and on any user documentation, prefaced with the following words: "All or portions of this licensed product (such portions are the "Software") have been obtained under license from The Brigham and Women's Hospital, Inc. and are subject to the following terms and conditions:" 38 | 39 | 3. you preserve and maintain all applicable attributions, copyright notices and licenses included in or applicable to the Software; 40 | 41 | 4. modified versions of the Software must be clearly identified and marked as such, and must not be misrepresented as being the original Software; and 42 | 43 | 5. you consider making, but are under no obligation to make, the source code of any of your modifications to the Software freely available to others on an open source basis. 44 | 45 | 2. **The license granted in this Software License includes without limitation the right to (i) incorporate the Software into proprietary programs (subject to any restrictions applicable to such programs), (ii) add your own copyright statement to your modifications of the Software, and (iii) provide additional or different license terms and conditions in your sublicenses of modifications of the Software; provided that in each case your use, reproduction or distribution of such modifications otherwise complies with the conditions stated in this Software License.** 46 | 47 | 3. This Software License does not grant any rights with respect to third party software, except those rights that Brigham has been authorized by a third party to grant to you, and accordingly you are solely responsible for (i) obtaining any permissions from third parties that you need to use, reproduce, make derivative works of, display and distribute the Software, and (ii) informing your sublicensees, including without limitation your end-users, of their obligations to secure any such required permissions. 48 | 49 | 4. **The Software has been designed for research purposes only and has not been reviewed or approved by the Food and Drug Administration or by any other agency. YOU ACKNOWLEDGE AND AGREE THAT CLINICAL APPLICATIONS ARE NEITHER RECOMMENDED NOR ADVISED. Any commercialization of the Software is at the sole risk of the party or parties engaged in such commercialization. You further agree to use, reproduce, make derivative works of, display and distribute the Software in compliance with all applicable governmental laws, regulations and orders, including without limitation those relating to export and import control.** 50 | 51 | 5. The Software is provided "AS IS" and neither Brigham nor any contributor to the software (each a "Contributor") shall have any obligation to provide maintenance, support, updates, enhancements or modifications thereto. BRIGHAM AND ALL CONTRIBUTORS SPECIFICALLY DISCLAIM ALL EXPRESS AND IMPLIED WARRANTIES OF ANY KIND INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL BRIGHAM OR ANY CONTRIBUTOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY ARISING IN ANY WAY RELATED TO THE SOFTWARE, EVEN IF BRIGHAM OR ANY CONTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. TO THE MAXIMUM EXTENT NOT PROHIBITED BY LAW OR REGULATION, YOU FURTHER ASSUME ALL LIABILITY FOR YOUR USE, REPRODUCTION, MAKING OF DERIVATIVE WORKS, DISPLAY, LICENSE OR DISTRIBUTION OF THE SOFTWARE AND AGREE TO INDEMNIFY AND HOLD HARMLESS BRIGHAM AND ALL CONTRIBUTORS FROM AND AGAINST ANY AND ALL CLAIMS, SUITS, ACTIONS, DEMANDS AND JUDGMENTS ARISING THEREFROM. 52 | 53 | 6. None of the names, logos or trademarks of Brigham or any of Brigham's affiliates or any of the Contributors, or any funding agency, may be used to endorse or promote products produced in whole or in part by operation of the Software or derived from or based on the Software without specific prior written permission from the applicable party. 54 | 55 | 7. Any use, reproduction or distribution of the Software which is not in accordance with this Software License shall automatically revoke all rights granted to you under this Software License and render Paragraphs 1 and 2 of this Software License null and void. 56 | 57 | 8. This Software License does not grant any rights in or to any intellectual property owned by Brigham or any Contributor except those rights expressly granted hereunder. 58 | 59 | ## PART C. MISCELLANEOUS 60 | 61 | This Agreement shall be governed by and construed in accordance with the laws of The Commonwealth of Massachusetts without regard to principles of conflicts of law. This Agreement shall supercede and replace any license terms that you may have agreed to previously with respect to Slicer. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Header](Resources/Media/header.png) 2 | 3 | # Surface Wrap Solidify 4 | 5 | This module for [3D Slicer](https://www.slicer.org) that can fill in internal holes in a segmented image region or retrieve the largest cavity inside a segmentation. 6 | 7 | **Copyright © 2020, Sebastian Andreß**\ 8 | All rights reserved. Please find the license [here](https://github.com/sebastianandress/Slicer-SurfaceWrapSolidify/blob/master/LICENSE.md). 9 | 10 | Please cite the corresponding paper when using this filter for publications: 11 | 12 | @article{3DPrintWrapSolidify, 13 | author = {Weidert, Simon and Andress, Sebastian and Linhart, Christoph and Suero, Eduardo M. and Greiner, Axel and Böcker, Wolfgang and Kammerlander, Christian and Becker, Christopher A.}, 14 | title = {3D printing method for next-day acetabular fracture surgery using a surface filtering pipeline: feasibility and 1-year clinical results}, 15 | journal = {International Journal of Computer Assisted Radiology and Surgery}, 16 | publisher = {Springer}, 17 | date = {2020-01-02}, 18 | } 19 | 20 | ## Introduction 21 | 22 | This segmentation tool was designed for creating fractured bone models for fast 3D printing. Especially in orthopedic trauma surgery, the editing time, as well as the printing time should be as short as possible. Using this effect helps to fulfil both features. Also, by removing inner cancellous structures, it is possible to achieve a fracture reduction on the printed model. 23 | 24 | In our use-case, we used this effect after applying a simple threshold operation and separating the bone with simple brushing and island techniques. See the [workflow example videos](https://1drv.ms/v/s!AqzdGuIdWLfeiNpPJhrVKhDsuxqw7w?e=6DOqgo). The effect was tested on more than 30 acetabular fracture models, it reduced the printing time about 70%. 25 | 26 | While the segmentation tool was originally designed for 3D printing fractured bone models, it has proven to be effective in a wide range of other applications that require removing or segmenting internal holes in a segment. 27 | 28 | ![Screenshot](Resources/Screenshots/screenshot4.png) 29 | 30 | ## How to install 31 | 32 | - [Install 3D Slicer](https://slicer.readthedocs.io/en/latest/user_guide/getting_started.html#installing-3d-slicer) (minimum version: Slicer-4.11) 33 | - [Install **SurfaceWrapSolidify** extension](https://slicer.readthedocs.io/en/latest/user_guide/extensions_manager.html#install-extensions) 34 | 35 | ## How to use 36 | 37 | - Load an image. Example images are available in _Sample data_ module. 38 | - Use _Segment Editor_ module to create an initial segmentation. 39 | - For example use **Threshold** effect, set the level between 300 and the maximal Hounsfield Unit. By using the sphere brush, first erase the femoral head, and subsequently connecting parts in the sacroiliac joint. Using the __Islands__ effect, the exempted hemipelvis was added to a separate segment. 40 | 41 | [![Threshold Video Preview Image](Resources/Media/threshold.png)](https://1drv.ms/v/s!AqzdGuIdWLfeiNpPJhrVKhDsuxqw7w?e=6DOqgo) 42 | 43 | - Go to **Wrap Solidify** effect and adjust options as needed. 44 | - For example, to create fast printable bones, preserving surface cracks: enable _Carve holes_, enable _Create shell_ (to save material in 3D printing and be able to preserve surface cracks), select _model_ for _output_ to preserve maximum details (and to be able to save the output directly as a 3D-printable STL file). 45 | - Click **Apply**. Processing should be completed within about a minute. 46 | - For example, for the inputs described above, the processing time was 1:46 min on a Apple MacBook Pro 2017 (3.1 GHz Intel Core i7 CPU, 16 GB RAM). 47 | - Use menu: _File_ / _Save Data_ (or go to _Data_ module, right-click the output model, and choose _Export to file..._) to save the output model to 3D-printable STL file 48 | 49 | [![Processing Video Preview Image](Resources/Media/processing.png)](https://1drv.ms/v/s!AqzdGuIdWLfeiNpO1rx9ZGbbhk6frQ?e=5NFQMt) 50 | 51 | Example processing result: 52 | 53 | ![Results Image](Resources/Media/result.gif) 54 | 55 | ## Processing parameters 56 | 57 | - **Region** 58 | - **Outer surface:** Fill internal holes in a segment. 59 | - **Carve holes** can be enabled to preserve surface concavities. Concavities that have smaller opening than the specified diameter value are filled in the output segment. Opening refers to the diameter of the narrowest region that separates the cavity from the region outside the segment. 60 | - Example: Internal holes that have no connection to outside the segment (_A_) are always filled. Cavities that have a narrow connection to outside (_B_ and _C_) are filled if the opening is smaller than the specified diameter value. _B_ has smaller opening (about 5mm) then _C_ (about 30mm), therefore _B_ is filled when a smaller diameter value (10mm) is specified. 61 | 62 | ![](Resources\Media\CarveHoles.png) 63 | 64 | - **Largest cavity:** return the largest internal cavity within a segment. 65 | - **Split cavities** can be enabled to ignore small cavities connected to the main one. Cavities that have smaller opening than the specified diameter value are removed from the output segment. Opening refers to the diameter of the narrowest region that separates the cavity from the main one. 66 | - Example: Largest cavity is the connected _A_+_B_+_C_ cavity. Region _B_ is connected to _A_ with a narrowing of about 10mm. Region _C_ is connected to _A_ with a narrowing of about 7mm. 67 | 68 | ![](Resources\Media\SplitCavities.png) 69 | 70 | - **Custom:** Specify a custom initial shape for the surface wrapping. In masking section _Modify other segments_ must be set to _Allow overlap_ to allow the initial segment to overlap with the input segment. 71 | - In the segment list at the top: select the segment that the resulting segmentation will be snapped to. (green in the example below) 72 | - In the segment selector next to the "custom" option, select the initial shape that will be warped. (yellow in the example below) 73 | 74 | ![](Resources\Media\CustomRegion.png) 75 | 76 | - **Create shell**: If enabled then a thin shell is created from the segment. 77 | * _Preserve surface cracks_ makes surface cracks in the input segment preserved in the output. 78 | * _Offset direction_ determines if the original surface should be used as inner or outer surface of the created shell. 79 | * _Output shell thickness_ specifies the distance between the inner and outer wall of the shell. 80 | - **Output**: Selects where to store the created new surface or segment. If the _model_ option is chosen then the surface mesh is not rasterized into a binary labelmap, therefore more details may be preserved. 81 | - **Advanced options** 82 | - **Smoothing factor**: Specifies smoothing between iterations. Higher value makes the output smoother, removing small surface irregularities and sharp edges. 83 | * **Oversampling**: Specifies resolution during internal remeshing. Higher value results in higher accuracy but longer computation time. **Increase this value up to 2-4x if output does not follow the input segmentation accurately enough.** 84 | * **Number of iterations** specifies nunber of internal iterations to converge the initial surface to the final surface. Increase the number of iterations to 10-15 if artifacts appear in the output or output is not accurate enough even though a high oversampling value is used. Increasing the value increases the computation time. 85 | * **Save intermediate results**: Saves all intermediate results during processing. It can be useful for troubleshooting (understanding why the results are not as expected) or understanding what the algorithm does internally. 86 | 87 | ## How it works 88 | 89 | The algorithm was modified compared to the originally published method, to make it more robust, faster, and reduce the number of parameters that users must specify. The algorithm was also extended to be able to get cavities (internal surfaces) in a segmentation. 90 | 91 | The Wrap Solidify Effect internally performs the following operations: 92 | 93 | 1. A surface representation of the selected segment is created (segmented model). 94 | * _Smoothing Factor_ defines smoothing of the input surface representation 95 | 2. Initial surface is generated: 96 | * For outer surface extraction: A larger model is created around the input segmentation. The model is a large enclosing sphere if _carve holes_ is disabled, otherwise a margin growing result. 97 | * For largest cavity extraction: A larger model is created around the input segmentation, it is inverted, shrunk by the value specified in _split cavities_, and the largest segment is preserved. 98 | 3. Shrinkwrapping (iteratively shrinking and uniformly remeshing the sphere model to the segmented model) is used for surface definition. 99 | * _Number of iterations_ is used to define the number of iterations. The algorithm is more robust but it takes longer if many small iterations are performed. 100 | * _Smoothing Factor_ specifies strength of the filter that performs surface smoothing constrained to the original input surface. 101 | * _Oversampling_ is used to define resolution of remesh. Higher value results in higher accuracy but longer computation time. 102 | 4. If _create shell_ is enabled then a thin shell is created from the segment by extruding the surface in normal direction by _Output shell thickness_. If _preserve surface cracks_ option is enabled: all vertices of the surface model that are not touching the segmented model are deleted before extruding. 103 | 5. If output is segmentation: The resulting surface model is converted into a segmentation by rasterizing the closed surface into a binary labelmap. 104 | 105 | ## Acknowledgments 106 | 107 | Thanks a lot to [Andras Lasso](https://github.com/lassoan) for also contributing and improving the module. 108 | 109 | ## Contact information 110 | 111 | For further collaborations, patient studies or any help, do not hesitate to contact [Sebastian Andreß](mailto:sebastian.andress@med.uni-muenchen.de). 112 | -------------------------------------------------------------------------------- /Resources/Media/CarveHoles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/CarveHoles.png -------------------------------------------------------------------------------- /Resources/Media/CustomRegion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/CustomRegion.png -------------------------------------------------------------------------------- /Resources/Media/SplitCavities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/SplitCavities.png -------------------------------------------------------------------------------- /Resources/Media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/header.png -------------------------------------------------------------------------------- /Resources/Media/processing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/processing.png -------------------------------------------------------------------------------- /Resources/Media/result.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/result.gif -------------------------------------------------------------------------------- /Resources/Media/threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Media/threshold.png -------------------------------------------------------------------------------- /Resources/Screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/Resources/Screenshots/screenshot4.png -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------------------------------------------------------- 3 | set(MODULE_NAME SegmentEditorWrapSolidify) 4 | 5 | #----------------------------------------------------------------------------- 6 | set(MODULE_PYTHON_SCRIPTS 7 | ${MODULE_NAME}.py 8 | ${MODULE_NAME}Lib/__init__.py 9 | ${MODULE_NAME}Lib/SegmentEditorEffect.py 10 | ) 11 | 12 | set(MODULE_PYTHON_RESOURCES 13 | ${MODULE_NAME}Lib/SegmentEditorEffect.png 14 | ${MODULE_NAME}Lib/SegmentEditorEffect.ui 15 | ) 16 | 17 | #----------------------------------------------------------------------------- 18 | slicerMacroBuildScriptedModule( 19 | NAME ${MODULE_NAME} 20 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 21 | RESOURCES ${MODULE_PYTHON_RESOURCES} 22 | WITH_GENERIC_TESTS 23 | ) 24 | 25 | #----------------------------------------------------------------------------- 26 | if(BUILD_TESTING) 27 | # Register the unittest subclass in the main script as a ctest. 28 | # Note that the test will also be available at runtime. 29 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 30 | 31 | # Additional build-time testing 32 | #add_subdirectory(Testing) 33 | endif() 34 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/SegmentEditorWrapSolidify.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import vtk, qt, ctk, slicer 4 | from slicer.ScriptedLoadableModule import * 5 | import logging 6 | 7 | class SegmentEditorWrapSolidify(ScriptedLoadableModule): 8 | """Uses ScriptedLoadableModule base class, available at: 9 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 10 | """ 11 | 12 | def __init__(self, parent): 13 | import string 14 | ScriptedLoadableModule.__init__(self, parent) 15 | self.parent.title = "SegmentEditorWrapSolidify" 16 | self.parent.categories = ["Segmentation"] 17 | self.parent.dependencies = [] 18 | self.parent.contributors = ["Sebastian Andress (LMU Munich)"] 19 | self.parent.hidden = True 20 | self.parent.helpText = "This hidden module registers the segment editor effect." 21 | self.parent.helpText += self.getDefaultModuleDocumentationLink() 22 | self.parent.acknowledgementText = """ 23 | This filter was created by Sebastian Andress (LMU University Hospital Munich, Germany, Department of General-, Trauma- and Reconstructive Surgery). 24 | """ 25 | slicer.app.connect("startupCompleted()", self.registerEditorEffect) 26 | 27 | def registerEditorEffect(self): 28 | import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects 29 | instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None) 30 | effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__+'Lib/SegmentEditorEffect.py') 31 | instance.setPythonSource(effectFilename.replace('\\','/')) 32 | instance.self().register() 33 | 34 | # class SegmentEditorWrapSolidifyWidget(ScriptedLoadableModuleWidget): 35 | 36 | # def setup(self): 37 | # ScriptedLoadableModuleWidget.setup(self) 38 | 39 | class SegmentEditorWrapSolidifyTest(ScriptedLoadableModuleTest): 40 | """ 41 | This is the test case for your scripted module. 42 | Uses ScriptedLoadableModuleTest base class, available at: 43 | https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py 44 | """ 45 | 46 | def setUp(self): 47 | """ Do whatever is needed to reset the state - typically a scene clear will be enough. 48 | """ 49 | slicer.mrmlScene.Clear(0) 50 | 51 | def runTest(self): 52 | """Run as few or as many tests as needed here. 53 | """ 54 | self.setUp() 55 | self.test_WrapSolidify1() 56 | 57 | def test_WrapSolidify1(self): 58 | """ 59 | Basic automated test of the segmentation method: 60 | - Create segmentation by placing sphere-shaped seeds 61 | - Run segmentation 62 | - Verify results using segment statistics 63 | The test can be executed from SelfTests module (test name: SegmentEditorWrapSolidify) 64 | """ 65 | 66 | self.delayDisplay("Starting test_WrapSolidify1") 67 | 68 | import vtkSegmentationCorePython as vtkSegmentationCore 69 | import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic 70 | import SampleData 71 | from SegmentStatistics import SegmentStatisticsLogic 72 | 73 | ################################## 74 | self.delayDisplay("Load master volume") 75 | 76 | masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') 77 | 78 | ################################## 79 | self.delayDisplay("Create segmentation containing a two spheres") 80 | 81 | segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') 82 | segmentationNode.CreateDefaultDisplayNodes() 83 | segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) 84 | 85 | features = ["none", "carveCavities", "createShell", "both"] 86 | spheres = [ 87 | [20, 5, 5, 5], 88 | [20, -5,-5,-5]] 89 | appender = vtk.vtkAppendPolyData() 90 | for sphere in spheres: 91 | sphereSource = vtk.vtkSphereSource() 92 | sphereSource.SetRadius(sphere[0]) 93 | sphereSource.SetCenter(sphere[1], sphere[2], sphere[3]) 94 | appender.AddInputConnection(sphereSource.GetOutputPort()) 95 | 96 | for m in features: 97 | segmentName = str(m) 98 | segment = vtkSegmentationCore.vtkSegment() 99 | segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) 100 | appender.Update() 101 | segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput()) 102 | segmentationNode.GetSegmentation().AddSegment(segment) 103 | 104 | ################################## 105 | self.delayDisplay("Create segment editor") 106 | 107 | segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() 108 | segmentEditorWidget.show() 109 | segmentEditorWidget.setMRMLScene(slicer.mrmlScene) 110 | segmentEditorNode = slicer.vtkMRMLSegmentEditorNode() 111 | slicer.mrmlScene.AddNode(segmentEditorNode) 112 | segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) 113 | segmentEditorWidget.setSegmentationNode(segmentationNode) 114 | segmentEditorWidget.setMasterVolumeNode(masterVolumeNode) 115 | 116 | ################################## 117 | self.delayDisplay("Run WrapSolidify Effect") 118 | segmentEditorWidget.setActiveEffectByName("Wrap Solidify") 119 | effect = segmentEditorWidget.activeEffect() 120 | 121 | for t in ["SEGMENTATION", "MODEL"]: 122 | effect.setParameter("outputType", t) 123 | 124 | self.delayDisplay("Creating Output Type %s, activated feature none" %(t)) 125 | segmentEditorWidget.setCurrentSegmentID(segmentationNode.GetSegmentation().GetSegmentIdBySegmentName('none')) 126 | effect.setParameter("carveCavities", False) 127 | effect.setParameter("createShell", False) 128 | effect.self().onApply() 129 | 130 | self.delayDisplay("Creating Output Type %s, activated feature carveCavities" %(t)) 131 | effect.setParameter("carveCavities", True) 132 | effect.setParameter("createShell", False) 133 | segmentEditorWidget.setCurrentSegmentID(segmentationNode.GetSegmentation().GetSegmentIdBySegmentName('carveCavities')) 134 | effect.self().onApply() 135 | 136 | self.delayDisplay("Creating Output Type %s, activated feature createShell" %(t)) 137 | effect.setParameter("carveCavities", False) 138 | effect.setParameter("createShell", True) 139 | segmentEditorWidget.setCurrentSegmentID(segmentationNode.GetSegmentation().GetSegmentIdBySegmentName('createShell')) 140 | effect.self().onApply() 141 | 142 | self.delayDisplay("Creating Output Type %s, activated feature both" %(t)) 143 | effect.setParameter("carveCavities", True) 144 | effect.setParameter("createShell", True) 145 | segmentEditorWidget.setCurrentSegmentID(segmentationNode.GetSegmentation().GetSegmentIdBySegmentName('both')) 146 | effect.self().onApply() 147 | 148 | ################################## 149 | self.delayDisplay("Creating Segments from Models") 150 | for m in features: 151 | model = slicer.util.getNode(m) 152 | segmentName = "MODEL_%s" % m 153 | segment = vtkSegmentationCore.vtkSegment() 154 | segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) 155 | segment.SetColor(model.GetDisplayNode().GetColor()) 156 | segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), model.GetPolyData()) 157 | segmentationNode.GetSegmentation().AddSegment(segment) 158 | 159 | ################################## 160 | self.delayDisplay("Compute statistics") 161 | segStatLogic = SegmentStatisticsLogic() 162 | segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) 163 | segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) 164 | segStatLogic.computeStatistics() 165 | statistics = segStatLogic.getStatistics() 166 | 167 | ################################## 168 | self.delayDisplay("Check a few numerical results") 169 | 170 | # logging.info(round(statistics["none",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 171 | # logging.info(round(statistics["MODEL_none",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 172 | # logging.info(round(statistics["carveCavities",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 173 | # logging.info(round(statistics["MODEL_carveCavities",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 174 | # logging.info(round(statistics["createShell",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 175 | # logging.info(round(statistics["MODEL_createShell",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 176 | # logging.info(round(statistics["both",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 177 | # logging.info(round(statistics["MODEL_both",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3'])) 178 | 179 | self.assertEqual( round(statistics["none",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 46605) 180 | self.assertEqual( round(statistics["MODEL_none",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 46320) 181 | 182 | self.assertEqual( round(statistics["carveCavities",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 46605) 183 | self.assertEqual( round(statistics["MODEL_carveCavities",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 46321) 184 | 185 | self.assertEqual( round(statistics["createShell",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 9257) 186 | self.assertEqual( round(statistics["MODEL_createShell",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 9230) 187 | 188 | self.assertEqual( round(statistics["both",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 9254) 189 | self.assertEqual( round(statistics["MODEL_both",'ScalarVolumeSegmentStatisticsPlugin.volume_mm3']), 9245) 190 | 191 | self.delayDisplay('test_WrapSolidify1 passed') 192 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/SegmentEditorWrapSolidifyLib/SegmentEditorEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/SegmentEditorWrapSolidify/SegmentEditorWrapSolidifyLib/SegmentEditorEffect.png -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/SegmentEditorWrapSolidifyLib/SegmentEditorEffect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import vtk, qt, ctk, slicer 3 | import logging 4 | from SegmentEditorEffects import * 5 | import numpy as np 6 | import math 7 | import vtkSegmentationCorePython 8 | 9 | class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect): 10 | """This effect uses shrinkwrap, raycasting, remesh, and solidifying algorithms to filter the surface from the input segmentation""" 11 | 12 | def __init__(self, scriptedEffect): 13 | scriptedEffect.name = 'Wrap Solidify' 14 | scriptedEffect.perSegment = True # this effect operates on all segments at once (not on a single selected segment) 15 | AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) 16 | 17 | self.logic = WrapSolidifyLogic() 18 | self.logic.logCallback = self.addLog 19 | 20 | def clone(self): 21 | # It should not be necessary to modify this method 22 | import qSlicerSegmentationsEditorEffectsPythonQt as effects 23 | clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) 24 | clonedEffect.setPythonSource(__file__.replace('\\','/')) 25 | return clonedEffect 26 | 27 | def icon(self): 28 | # It should not be necessary to modify this method 29 | iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png') 30 | if os.path.exists(iconPath): 31 | return qt.QIcon(iconPath) 32 | return qt.QIcon() 33 | 34 | def helpText(self): 35 | return """Create a solid segment from the outer surface or an internal surface of a segment. It is using a combination of shrinkwrapping, projection and solidification algorithms.
36 | For further information, license, disclaimers and possible research partnerships visit this github repository. 37 | """ 38 | 39 | def activate(self): 40 | pass 41 | 42 | def deactivate(self): 43 | self.cleanup() 44 | 45 | def cleanup(self): 46 | pass 47 | 48 | def setupOptionsFrame(self): 49 | 50 | # Load widget from .ui file. This .ui file can be edited using Qt Designer 51 | # (Edit / Application Settings / Developer / Qt Designer -> launch). 52 | uiWidget = slicer.util.loadUI(os.path.join(os.path.dirname(__file__), "SegmentEditorEffect.ui")) 53 | self.scriptedEffect.addOptionsWidget(uiWidget) 54 | self.ui = slicer.util.childWidgetVariables(uiWidget) 55 | 56 | # Set scene in MRML widgets. Make sure that in Qt designer 57 | # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's. 58 | # "setMRMLScene(vtkMRMLScene*)" slot. 59 | uiWidget.setMRMLScene(slicer.mrmlScene) 60 | 61 | # Order of buttons in the buttongroup must be the same order as the corresponding 62 | # options in ARG_OPTIONS[]. 63 | 64 | self.ui.regionGroup = qt.QButtonGroup() 65 | self.ui.regionGroup.addButton(self.ui.regionOuterSurfaceRadioButton) 66 | self.ui.regionGroup.addButton(self.ui.regionLargestCavityRadioButton) 67 | self.ui.regionGroup.addButton(self.ui.regionSegmentRadioButton) 68 | 69 | self.ui.shellOffsetDirectionGroup = qt.QButtonGroup() 70 | self.ui.shellOffsetDirectionGroup.addButton(self.ui.shellOffsetInsideRadioButton) 71 | self.ui.shellOffsetDirectionGroup.addButton(self.ui.shellOffsetOutsideRadioButton) 72 | 73 | self.ui.outputTypeGroup = qt.QButtonGroup() 74 | self.ui.outputTypeGroup.addButton(self.ui.outputSegmentRadioButton) 75 | self.ui.outputTypeGroup.addButton(self.ui.outputNewSegmentRadioButton) 76 | self.ui.outputTypeGroup.addButton(self.ui.outputModelRadioButton) 77 | 78 | # Widget to arguments mapping 79 | self.valueEditWidgets = { 80 | ARG_REGION: self.ui.regionGroup, 81 | ARG_REGION_SEGMENT_ID: self.ui.regionSegmentSelector, 82 | ARG_CARVE_HOLES_IN_OUTER_SURFACE: self.ui.carveHolesInOuterSurfaceCheckBox, 83 | ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER: self.ui.carveHolesInOuterSurfaceDiameterSlider, 84 | ARG_SPLIT_CAVITIES: self.ui.splitCavitiesCheckBox, 85 | ARG_SPLIT_CAVITIES_DIAMETER: self.ui.splitCavitiesDiameterSlider, 86 | ARG_CREATE_SHELL: self.ui.createShellCheckBox, 87 | ARG_SHELL_THICKNESS: self.ui.shellThicknessSlider, 88 | ARG_SHELL_OFFSET_DIRECTION: self.ui.shellOffsetDirectionGroup, 89 | ARG_SHELL_PRESERVE_CRACKS: self.ui.shellPreserveCracksCheckBox, 90 | ARG_OUTPUT_TYPE: self.ui.outputTypeGroup, 91 | ARG_OUTPUT_MODEL_NODE: self.ui.outputModelNodeSelector, 92 | ARG_SMOOTHING_FACTOR: self.ui.smoothingFactorSlider, 93 | ARG_REMESH_OVERSAMPLING: self.ui.remeshOversamplingSlider, 94 | ARG_SHRINKWRAP_ITERATIONS: self.ui.iterationsSlider, 95 | ARG_SAVE_INTERMEDIATE_RESULTS: self.ui.saveIntermediateResultsCheckBox 96 | } 97 | 98 | # Add connections 99 | 100 | for argName, widget in self.valueEditWidgets.items(): 101 | widgetClassName = widget.metaObject().getClassName() 102 | if widgetClassName=="qMRMLSliderWidget" or widgetClassName=="ctkSliderWidget": 103 | widget.connect("valueChanged(double)", self.updateMRMLFromGUI) 104 | elif widgetClassName=="QCheckBox": 105 | widget.connect("stateChanged(int)", self.updateMRMLFromGUI) 106 | elif widgetClassName=="qMRMLNodeComboBox": 107 | widget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateMRMLFromGUI) 108 | elif widgetClassName=="QButtonGroup": 109 | widget.connect("buttonClicked(int)", self.updateMRMLFromGUI) 110 | elif widgetClassName=="qMRMLSegmentSelectorWidget": 111 | widget.connect("currentSegmentChanged(QString)", self.updateMRMLFromGUI) 112 | else: 113 | raise Exception("Unexpected widget class: {0}".format(widgetClassName)) 114 | 115 | self.ui.applyButton.connect('clicked()', self.onApply) 116 | 117 | def createCursor(self, widget): 118 | return slicer.util.mainWindow().cursor 119 | 120 | def layoutChanged(self): 121 | pass 122 | 123 | def processInteractionEvents(self, callerInteractor, eventId, viewWidget): 124 | return False # For the sake of example 125 | 126 | def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): 127 | pass # For the sake of example 128 | 129 | def setMRMLDefaults(self): 130 | for (argName, defaultValue) in ARG_DEFAULTS.items(): 131 | self.scriptedEffect.setParameterDefault(argName, defaultValue) 132 | 133 | def updateGUIFromMRML(self): 134 | parameterNode = self.scriptedEffect.parameterSetNode() 135 | if not parameterNode: 136 | return 137 | 138 | # Update values in widgets 139 | for argName, widget in self.valueEditWidgets.items(): 140 | widgetClassName = widget.metaObject().getClassName() 141 | oldBlockSignalsState = widget.blockSignals(True) 142 | if widgetClassName=="qMRMLSliderWidget" or widgetClassName=="ctkSliderWidget": 143 | widget.value = self.scriptedEffect.doubleParameter(argName) 144 | elif widgetClassName=="QCheckBox": 145 | widget.setChecked(self.scriptedEffect.parameter(argName)=='True') 146 | elif widgetClassName=="qMRMLNodeComboBox": 147 | widget.setCurrentNodeID(parameterNode.GetNodeReferenceID(argName)) 148 | elif widgetClassName=="QButtonGroup": 149 | try: 150 | optionIndex = ARG_OPTIONS[argName].index(self.scriptedEffect.parameter(argName)) 151 | except ValueError: 152 | optionIndex = 0 153 | widget.button(-2-optionIndex).setChecked(True) 154 | elif widgetClassName=="qMRMLSegmentSelectorWidget": 155 | segmentationNode = parameterNode.GetSegmentationNode() 156 | segmentId = self.scriptedEffect.parameter(argName) if self.scriptedEffect.parameterDefined(argName) else "" 157 | if widget.currentNode() != segmentationNode: 158 | widget.setCurrentNode(segmentationNode) 159 | widget.setCurrentSegmentID(segmentId) 160 | else: 161 | raise Exception("Unexpected widget class: {0}".format(widgetClassName)) 162 | widget.blockSignals(oldBlockSignalsState) 163 | 164 | # Enable/disable dependent widgets 165 | carveHolesInOuterSurface = (self.scriptedEffect.parameter(ARG_CARVE_HOLES_IN_OUTER_SURFACE) == "True") 166 | splitCavities = (self.scriptedEffect.parameter(ARG_SPLIT_CAVITIES) == "True") 167 | createShell = (self.scriptedEffect.parameter(ARG_CREATE_SHELL) == "True") 168 | region = self.scriptedEffect.parameter(ARG_REGION) 169 | 170 | self.valueEditWidgets[ARG_CARVE_HOLES_IN_OUTER_SURFACE].enabled = (region == REGION_OUTER_SURFACE or region == REGION_LARGEST_CAVITY) 171 | self.valueEditWidgets[ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER].enabled = (carveHolesInOuterSurface 172 | and self.valueEditWidgets[ARG_CARVE_HOLES_IN_OUTER_SURFACE].enabled) 173 | 174 | self.valueEditWidgets[ARG_SPLIT_CAVITIES].enabled = region == REGION_LARGEST_CAVITY 175 | self.valueEditWidgets[ARG_SPLIT_CAVITIES_DIAMETER].enabled = (splitCavities and self.valueEditWidgets[ARG_SPLIT_CAVITIES].enabled) 176 | 177 | self.valueEditWidgets[ARG_REGION_SEGMENT_ID].enabled = (region == REGION_SEGMENT) 178 | 179 | self.valueEditWidgets[ARG_SHELL_THICKNESS].enabled = createShell 180 | for widget in self.valueEditWidgets[ARG_SHELL_OFFSET_DIRECTION].buttons(): 181 | widget.enabled = createShell 182 | self.valueEditWidgets[ARG_SHELL_PRESERVE_CRACKS].enabled = createShell 183 | 184 | def updateMRMLFromGUI(self): 185 | wasModified = self.scriptedEffect.parameterSetNode().StartModify() 186 | for argName, widget in self.valueEditWidgets.items(): 187 | widgetClassName = widget.metaObject().getClassName() 188 | if widgetClassName=="qMRMLSliderWidget" or widgetClassName=="ctkSliderWidget": 189 | self.scriptedEffect.setParameter(argName, widget.value) 190 | elif widgetClassName=="QCheckBox": 191 | self.scriptedEffect.setParameter(argName, "True" if widget.isChecked() else "False") 192 | elif widgetClassName=="qMRMLNodeComboBox": 193 | self.scriptedEffect.parameterSetNode().SetNodeReferenceID(argName, widget.currentNodeID) 194 | elif widgetClassName=="QButtonGroup": 195 | optionName = ARG_OPTIONS[argName][-2-widget.checkedId()] 196 | self.scriptedEffect.setParameter(argName, optionName) 197 | elif widgetClassName=="qMRMLSegmentSelectorWidget": 198 | segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() 199 | if widget.currentNode() != segmentationNode: 200 | widget.setCurrentNode(segmentationNode) 201 | segmentId = widget.currentSegmentID() 202 | self.scriptedEffect.setParameter(argName, segmentId) 203 | else: 204 | raise Exception("Unexpected widget class: {0}".format(widgetClassName)) 205 | self.scriptedEffect.parameterSetNode().EndModify(wasModified) 206 | 207 | def addLog(self, text): 208 | slicer.util.showStatusMessage(text) 209 | slicer.app.processEvents() # force update 210 | 211 | def onApply(self): 212 | 213 | if self.ui.applyButton.text == 'Cancel': 214 | self.logic.requestCancel() 215 | return 216 | 217 | self.scriptedEffect.saveStateForUndo() 218 | 219 | errorMessage = None 220 | self.ui.applyButton.text = 'Cancel' 221 | qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) 222 | try: 223 | # Set inputs 224 | self.logic.segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() 225 | 226 | # Save smoothing factor 227 | segmentationSmoothingFactor = ( 228 | self.logic.segmentationNode.GetSegmentation().GetConversionParameter( 229 | 'Smoothing factor' 230 | ) 231 | ) 232 | 233 | # Continue setting inputs 234 | self.logic.segmentId = currentSegmentId = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() 235 | self.logic.region = self.scriptedEffect.parameter(ARG_REGION) 236 | self.logic.regionSegmentId = self.scriptedEffect.parameter(ARG_REGION_SEGMENT_ID) if self.scriptedEffect.parameterDefined(ARG_REGION_SEGMENT_ID) else "" 237 | self.logic.carveHolesInOuterSurface = (self.scriptedEffect.parameter(ARG_CARVE_HOLES_IN_OUTER_SURFACE) == "True") 238 | self.logic.carveHolesInOuterSurfaceDiameter = self.scriptedEffect.doubleParameter(ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER) 239 | self.logic.splitCavities = (self.scriptedEffect.parameter(ARG_SPLIT_CAVITIES) == "True") 240 | self.logic.splitCavitiesDiameter = self.scriptedEffect.doubleParameter(ARG_SPLIT_CAVITIES_DIAMETER) 241 | self.logic.createShell = (self.scriptedEffect.parameter(ARG_CREATE_SHELL) == "True") 242 | self.logic.shellThickness = self.scriptedEffect.doubleParameter(ARG_SHELL_THICKNESS) 243 | self.logic.shellOffsetDirection = self.scriptedEffect.parameter(ARG_SHELL_OFFSET_DIRECTION) 244 | self.logic.shellPreserveCracks = (self.scriptedEffect.parameter(ARG_SHELL_PRESERVE_CRACKS) == "True") 245 | self.logic.outputType = self.scriptedEffect.parameter(ARG_OUTPUT_TYPE) 246 | self.logic.outputModelNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ARG_OUTPUT_MODEL_NODE) 247 | self.logic.remeshOversampling = self.scriptedEffect.doubleParameter(ARG_REMESH_OVERSAMPLING) 248 | self.logic.smoothingFactor = self.scriptedEffect.doubleParameter(ARG_SMOOTHING_FACTOR) 249 | self.logic.shrinkwrapIterations = self.scriptedEffect.integerParameter(ARG_SHRINKWRAP_ITERATIONS) 250 | self.logic.saveIntermediateResults = (self.scriptedEffect.parameter(ARG_SAVE_INTERMEDIATE_RESULTS) == "True") 251 | # Run the algorithm 252 | self.logic.applyWrapSolidify() 253 | # Save the output model node (a new model node may have been created in the logic) 254 | if self.logic.outputType == OUTPUT_MODEL: 255 | self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ARG_OUTPUT_MODEL_NODE, 256 | self.logic.outputModelNode.GetID() if self.logic.outputModelNode else "") 257 | 258 | # Restore smoothing factor 259 | self.logic.segmentationNode.GetSegmentation().SetConversionParameter( 260 | 'Smoothing factor', 261 | segmentationSmoothingFactor 262 | ) 263 | 264 | self.logic.segmentationNode.GetSegmentation().CreateRepresentation( 265 | 'Closed surface', 266 | True 267 | ) # Last parameter forces conversion even if it exists 268 | 269 | self.logic.segmentationNode.Modified() # Update display 270 | 271 | except Exception as e: 272 | import traceback 273 | traceback.print_exc() 274 | errorMessage = str(e) 275 | slicer.util.showStatusMessage("") 276 | self.ui.applyButton.text = 'Apply' 277 | qt.QApplication.restoreOverrideCursor() 278 | if errorMessage: 279 | slicer.util.errorDisplay("Wrap solidify failed: " + errorMessage) 280 | 281 | 282 | class WrapSolidifyLogic(object): 283 | 284 | def __init__(self): 285 | self.logCallback = None 286 | self.cancelRequested = False 287 | 288 | # Inputs 289 | self.segmentationNode = None 290 | self.segmentId = None 291 | self.region = ARG_DEFAULTS[ARG_REGION] 292 | self.regionSegmentId = None 293 | self.carveHolesInOuterSurface = ARG_DEFAULTS[ARG_CARVE_HOLES_IN_OUTER_SURFACE] 294 | self.carveHolesInOuterSurfaceDiameter = ARG_DEFAULTS[ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER] 295 | self.splitCavities = ARG_DEFAULTS[ARG_SPLIT_CAVITIES] 296 | self.splitCavitiesDiameter = ARG_DEFAULTS[ARG_SPLIT_CAVITIES_DIAMETER] 297 | self.createShell = ARG_DEFAULTS[ARG_CREATE_SHELL] 298 | self.shellThickness = ARG_DEFAULTS[ARG_SHELL_THICKNESS] 299 | self.shellOffsetDirection = ARG_DEFAULTS[ARG_SHELL_OFFSET_DIRECTION] 300 | self.shellPreserveCracks = ARG_DEFAULTS[ARG_SHELL_PRESERVE_CRACKS] 301 | self.outputType = ARG_DEFAULTS[ARG_OUTPUT_TYPE] 302 | self.outputModelNode = None 303 | self.remeshOversampling = ARG_DEFAULTS[ARG_REMESH_OVERSAMPLING] 304 | self.smoothingFactor = ARG_DEFAULTS[ARG_SMOOTHING_FACTOR] 305 | self.shrinkwrapIterations = ARG_DEFAULTS[ARG_SHRINKWRAP_ITERATIONS] 306 | self.saveIntermediateResults = ARG_DEFAULTS[ARG_SAVE_INTERMEDIATE_RESULTS] 307 | 308 | # Temporary variables 309 | self._inputPd = None 310 | self._inputSpacing = None 311 | 312 | def requestCancel(self): 313 | logging.info("User requested cancelling.") 314 | self.cancelRequested = True 315 | 316 | def _log(self, message): 317 | if self.logCallback: 318 | self.logCallback(message) 319 | 320 | def _checkCancelRequested(self): 321 | if self.cancelRequested: 322 | self.checkCancelRequested = False 323 | raise ValueError("Cancel requested") 324 | 325 | def applyWrapSolidify(self): 326 | """Applies the Shrinkwrap-Raycast-Shrinkwrap Filter, a surface filter, to the selected passed segment. 327 | """ 328 | self.cancelRequested = False 329 | 330 | self.intermediateResultCounter = 0 331 | self.previousIntermediateResult = None 332 | 333 | try: 334 | self._log('Get input data...') 335 | self._updateInputPd() 336 | 337 | self._log('Create starting region...') 338 | regionPd = self._getInitialRegionPd() 339 | 340 | shrunkenPd = vtk.vtkPolyData() 341 | shrunkenPd.DeepCopy(self._shrinkWrap(regionPd)) 342 | 343 | if self.region == REGION_LARGEST_CAVITY: 344 | self._log('Extract largest cavity...') 345 | shrunkenPd.DeepCopy(self._extractCavity(shrunkenPd)) 346 | 347 | self._log('Smoothing...') 348 | shrunkenPd.DeepCopy(WrapSolidifyLogic._smoothPolydata(shrunkenPd, self.smoothingFactor)) 349 | self._saveIntermediateResult("Smoothed", shrunkenPd) 350 | 351 | # Create shell 352 | if self.createShell: 353 | if self.shellPreserveCracks: 354 | self._checkCancelRequested() 355 | self._log('Shell - preserving cracks...') 356 | shrunkenPd.DeepCopy(self._shellPreserveCracks(shrunkenPd)) 357 | self._saveIntermediateResult("ShellRemovedCaps", shrunkenPd) 358 | if self.shellThickness > 1e-6: 359 | self._checkCancelRequested() 360 | self._log('Shell - solidifying...') 361 | shrunkenPd.DeepCopy(self._shellSolidify(shrunkenPd, self.shellThickness, self.shellOffsetDirection)) 362 | self._saveIntermediateResult("ShellSolidified", shrunkenPd) 363 | 364 | # Write output to target node 365 | self._log('Save result...') 366 | baseSegmentId = self.regionSegmentId if self.region == REGION_SEGMENT else self.segmentId 367 | if self.outputType == OUTPUT_SEGMENT: 368 | WrapSolidifyLogic._polydataToSegment(shrunkenPd, self.segmentationNode, baseSegmentId) 369 | elif self.outputType == OUTPUT_NEW_SEGMENT: 370 | WrapSolidifyLogic._polydataToNewSegment(shrunkenPd, self.segmentationNode, baseSegmentId) 371 | elif self.outputType == OUTPUT_MODEL: 372 | segment = self.segmentationNode.GetSegmentation().GetSegment(baseSegmentId) 373 | name = segment.GetName() 374 | color = segment.GetColor() 375 | if not self.outputModelNode: 376 | self.outputModelNode = slicer.modules.models.logic().AddModel(shrunkenPd) 377 | self.outputModelNode.SetName(name) 378 | self.outputModelNode.GetDisplayNode().SliceIntersectionVisibilityOn() 379 | else: 380 | self.outputModelNode.SetAndObservePolyData(shrunkenPd) 381 | self.outputModelNode.CreateDefaultDisplayNodes() 382 | self.outputModelNode.GetDisplayNode().SetColor(color) 383 | else: 384 | raise ValueError('Unknown output type: '+self.outputType) 385 | 386 | finally: 387 | self._cleanup() 388 | 389 | 390 | def _cleanup(self): 391 | if self.previousIntermediateResult: 392 | self.previousIntermediateResult.GetDisplayNode().SetVisibility(False) 393 | self._inputPd = None 394 | self._inputSpacing = None 395 | 396 | def _updateInputPd(self): 397 | 398 | segment = self.segmentationNode.GetSegmentation().GetSegment(self.segmentId) 399 | self._inputPd = vtk.vtkPolyData() 400 | 401 | try: 402 | masterRepresentationName = self.segmentationNode.GetSegmentation().GetSourceRepresentationName() 403 | except: 404 | # Legacy (Slicer-5.3 and earlier) 405 | masterRepresentationName = self.segmentationNode.GetSegmentation().GetMasterRepresentationName() 406 | 407 | # Get input polydata and input spacing 408 | if masterRepresentationName == slicer.vtkSegmentationConverter().GetSegmentationBinaryLabelmapRepresentationName(): 409 | # Master representation is binary labelmap 410 | # Reconvert to closed surface using chosen chosen smoothing factor 411 | originalSurfaceSmoothing = float(self.segmentationNode.GetSegmentation().GetConversionParameter( 412 | slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule().GetSmoothingFactorParameterName())) 413 | if abs(originalSurfaceSmoothing-self.smoothingFactor) > 0.001: 414 | self.segmentationNode.GetSegmentation().SetConversionParameter( 415 | slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule().GetSmoothingFactorParameterName(), str(self.smoothingFactor)) 416 | # Force re-conversion 417 | self.segmentationNode.RemoveClosedSurfaceRepresentation() 418 | self.segmentationNode.CreateClosedSurfaceRepresentation() 419 | self.segmentationNode.GetClosedSurfaceRepresentation(self.segmentId, self._inputPd) 420 | if self._inputPd.GetNumberOfPoints() == 0: 421 | raise ValueError("Input segment closed surface representation is empty") 422 | # Get input spacing 423 | inputLabelmap = slicer.vtkOrientedImageData() 424 | self.segmentationNode.GetBinaryLabelmapRepresentation(self.segmentId, inputLabelmap) 425 | extent = inputLabelmap.GetExtent() 426 | if extent[0]>extent[1] or extent[2]>extent[3] or extent[4]>extent[5]: 427 | raise ValueError("Input segment labelmap representation is empty") 428 | self._inputSpacing = math.sqrt(np.sum(np.array(inputLabelmap.GetSpacing())**2)) 429 | else: 430 | # Representation is already closed surface 431 | self.segmentationNode.CreateClosedSurfaceRepresentation() 432 | self.segmentationNode.GetClosedSurfaceRepresentation(self.segmentId, self._inputPd) 433 | # set spacing to have an approxmately 250^3 volume 434 | # this size is not too large for average computing hardware yet 435 | # it is sufficiently detailed for many applications 436 | preferredVolumeSizeInVoxels = 250 * 250 * 250 437 | bounds = np.zeros(6) 438 | self._inputPd.GetBounds(bounds) 439 | volumeSizeInMm3 = (bounds[1] - bounds[0]) * (bounds[3] - bounds[2]) * (bounds[5] - bounds[4]) 440 | self._inputSpacing = pow(volumeSizeInMm3 / preferredVolumeSizeInVoxels, 1 / 3.) 441 | 442 | 443 | def _getInitialRegionPd(self): 444 | """Get initial shape that will be snapped to closest point of the input segment""" 445 | 446 | spacing = self._inputSpacing / self.remeshOversampling 447 | 448 | if self.carveHolesInOuterSurface: 449 | # Grow input polydata to close holes between outer surface and internal cavities. 450 | 451 | # It is less accurate but more robust to dilate labelmap than grow polydata. 452 | # Since accuracy is not important here, we dilate labelmap. 453 | # Convert to labelmap 454 | carveHolesInOuterSurfaceRadius = self.carveHolesInOuterSurfaceDiameter/2.0 455 | # add self.carveHolesInOuterSurfaceDiameter extra to bounds to ensure that the grown input still has an margin around 456 | inputLabelmap = WrapSolidifyLogic._polydataToLabelmap(self._inputPd, spacing, extraMarginToBounds=carveHolesInOuterSurfaceRadius) 457 | 458 | self._saveIntermediateResult("InitialRegionResampled", WrapSolidifyLogic._labelmapToPolydata(inputLabelmap, 1)) 459 | 460 | # Dilate 461 | import vtkITK 462 | margin = vtkITK.vtkITKImageMargin() 463 | margin.SetInputData(inputLabelmap) 464 | margin.CalculateMarginInMMOn() 465 | margin.SetOuterMarginMM(carveHolesInOuterSurfaceRadius) 466 | margin.Update() 467 | # extendedInputLabelmap: 255 near original inputLabelmap voxels, 0 elsewhere 468 | extendedInputLabelmap = margin.GetOutput() 469 | self._saveIntermediateResult("InitialRegionGrown", WrapSolidifyLogic._labelmapToPolydata(extendedInputLabelmap, 255)) 470 | 471 | # Region growing from a corner of the image 472 | seedPoints = vtk.vtkPoints() 473 | seedPoints.InsertNextPoint(inputLabelmap.GetOrigin()[0] + spacing / 2, 474 | inputLabelmap.GetOrigin()[1] + spacing / 2, 475 | inputLabelmap.GetOrigin()[2] + spacing / 2) 476 | seedScalars = vtk.vtkUnsignedCharArray() 477 | seedScalars.InsertNextValue(255) # this will be the label value to the grown region 478 | seedData = vtk.vtkPolyData() 479 | seedData.SetPoints(seedPoints) 480 | seedData.GetPointData().SetScalars(seedScalars) 481 | 482 | regionGrowing = vtk.vtkImageConnectivityFilter() 483 | regionGrowing.SetSeedData(seedData) 484 | regionGrowing.SetScalarRange(-10, 10) 485 | regionGrowing.SetInputData(extendedInputLabelmap) 486 | regionGrowing.Update() 487 | 488 | # outsideObjectImage: 255 outside the object, 0 inside 489 | outsideObjectLabelmap = regionGrowing.GetOutput() 490 | 491 | # Cavity extraction and outer surface extraction starts from the same outer surface shrinkwrap 492 | if self.region == REGION_OUTER_SURFACE or self.region == REGION_LARGEST_CAVITY: 493 | if self.carveHolesInOuterSurface: 494 | # Convert back to polydata 495 | initialRegionPd = vtk.vtkPolyData() 496 | initialRegionPd.DeepCopy(WrapSolidifyLogic._labelmapToPolydata(outsideObjectLabelmap, 255)) 497 | else: 498 | # create sphere that encloses entire segment content 499 | bounds = np.zeros(6) 500 | self._inputPd.GetBounds(bounds) 501 | diameters = np.array([bounds[1]-bounds[0],bounds[3]-bounds[2],bounds[5]-bounds[4]]) 502 | maxRadius = max(diameters)/2.0 503 | sphereSource = vtk.vtkSphereSource() 504 | # to make sure the volume is fully included in the sphere, radius must be sqrt(2) times larger 505 | sphereSource.SetRadius(maxRadius*1.5) 506 | 507 | # Set resolution to be about one magnitude lower than the final resolution 508 | # (by creating an initial surface element for about every 100th final element). 509 | sphereSurfaceArea = 4 * math.pi * maxRadius*maxRadius 510 | voxelSurfaceArea = spacing * spacing 511 | numberOfSurfaceElements = sphereSurfaceArea/voxelSurfaceArea 512 | numberOfIinitialSphereSurfaceElements = numberOfSurfaceElements/100 513 | sphereResolution = math.sqrt(numberOfIinitialSphereSurfaceElements) 514 | # Set resolution to minimum 10 515 | sphereResolution = max(int(sphereResolution), 10) 516 | sphereSource.SetPhiResolution(sphereResolution) 517 | sphereSource.SetThetaResolution(sphereResolution) 518 | sphereSource.SetCenter((bounds[0]+bounds[1])/2.0, (bounds[2]+bounds[3])/2.0, (bounds[4]+bounds[5])/2.0) 519 | sphereSource.Update() 520 | initialRegionPd = sphereSource.GetOutput() 521 | elif self.region == REGION_SEGMENT: 522 | # create initial region from segment (that will be grown) 523 | if not self.regionSegmentId: 524 | raise ValueError("Region segment is not set") 525 | if self.regionSegmentId == self.segmentId: 526 | raise ValueError("Region segment cannot be the same segment as the current segment") 527 | initialRegionPd = vtk.vtkPolyData() 528 | self.segmentationNode.GetClosedSurfaceRepresentation(self.regionSegmentId, initialRegionPd) 529 | if not initialRegionPd or initialRegionPd.GetNumberOfPoints() == 0: 530 | raise ValueError("Region segment is empty") 531 | # initialRegionPd = self._remeshPolydata(initialRegionPd, self._inputSpacing*5.0) # simplify the mesh 532 | else: 533 | raise ValueError("Invalid region: "+self.region) 534 | 535 | cleanPolyData = vtk.vtkCleanPolyData() 536 | cleanPolyData.SetInputData(initialRegionPd) 537 | cleanPolyData.Update() 538 | initialRegionPd = cleanPolyData.GetOutput() 539 | 540 | self._saveIntermediateResult("InitialRegion", initialRegionPd) 541 | return initialRegionPd 542 | 543 | 544 | def _shrinkWrap(self, regionPd): 545 | 546 | shrunkenPd = regionPd 547 | spacing = self._inputSpacing / self.remeshOversampling 548 | 549 | for iterationIndex in range(self.shrinkwrapIterations): 550 | 551 | # shrink 552 | self._checkCancelRequested() 553 | self._log('Shrinking %s/%s...' %(iterationIndex+1, self.shrinkwrapIterations)) 554 | if shrunkenPd.GetNumberOfPoints()<=1 or self._inputPd.GetNumberOfPoints()<=1: 555 | # we must not feed empty polydata into vtkSmoothPolyDataFilter because it would crash the application 556 | raise ValueError("Mesh has become empty during shrink-wrap iterations") 557 | smoothFilter = vtk.vtkSmoothPolyDataFilter() 558 | smoothFilter.SetInputData(0, shrunkenPd) 559 | smoothFilter.SetInputData(1, self._inputPd) # constrain smoothed points to the input surface 560 | smoothFilter.Update() 561 | shrunkenPd = vtk.vtkPolyData() 562 | shrunkenPd.DeepCopy(smoothFilter.GetOutput()) 563 | self._saveIntermediateResult("Shrunken", shrunkenPd) 564 | 565 | # remesh 566 | self._checkCancelRequested() 567 | self._log('Remeshing %s/%s...' %(iterationIndex+1, self.shrinkwrapIterations)) 568 | remeshedPd = WrapSolidifyLogic._remeshPolydata(shrunkenPd, spacing) 569 | shrunkenPd = vtk.vtkPolyData() 570 | shrunkenPd.DeepCopy(remeshedPd) 571 | self._saveIntermediateResult("Remeshed", shrunkenPd) 572 | 573 | return shrunkenPd 574 | 575 | def _extractCavity(self, shrunkenPd): 576 | 577 | spacing = self._inputSpacing / self.remeshOversampling 578 | outsideObjectLabelmap = WrapSolidifyLogic._polydataToLabelmap(shrunkenPd, spacing) # 0=outside, 1=inside 579 | 580 | inputLabelmap = WrapSolidifyLogic._polydataToLabelmap(self._inputPd, referenceImage=outsideObjectLabelmap) 581 | 582 | if self.splitCavities: 583 | # It is less accurate but more robust to dilate labelmap than grow polydata. 584 | # Since accuracy is not important here, we dilate labelmap. 585 | splitCavitiesRadius = self.splitCavitiesDiameter / 2.0 586 | # Dilate 587 | import vtkITK 588 | margin = vtkITK.vtkITKImageMargin() 589 | margin.SetInputData(inputLabelmap) 590 | margin.CalculateMarginInMMOn() 591 | margin.SetOuterMarginMM(splitCavitiesRadius) 592 | margin.Update() 593 | # extendedInputLabelmap: 255 near original inputLabelmap voxels, 0 elsewhere 594 | extendedInputLabelmap = margin.GetOutput() 595 | self._saveIntermediateResult("SplitCavitiesGrown", 596 | WrapSolidifyLogic._labelmapToPolydata(extendedInputLabelmap, 255)) 597 | else: 598 | extendedInputLabelmap = inputLabelmap 599 | 600 | outsideObjectLabelmapInverter = vtk.vtkImageThreshold() 601 | outsideObjectLabelmapInverter.SetInputData(outsideObjectLabelmap) 602 | outsideObjectLabelmapInverter.ThresholdByLower(0) 603 | outsideObjectLabelmapInverter.SetInValue(1) # backgroundValue 604 | outsideObjectLabelmapInverter.SetOutValue(0) # labelValue 605 | outsideObjectLabelmapInverter.SetOutputScalarType(outsideObjectLabelmap.GetScalarType()) 606 | outsideObjectLabelmapInverter.Update() 607 | 608 | addImage = vtk.vtkImageMathematics() 609 | addImage.SetInput1Data(outsideObjectLabelmapInverter.GetOutput()) 610 | addImage.SetInput2Data(extendedInputLabelmap) 611 | addImage.SetOperationToMax() 612 | addImage.Update() 613 | internalHolesLabelmap = addImage.GetOutput() 614 | # internal holes are 0, elsewhere >=1 615 | self._saveIntermediateResult("SplitCavitiesAll", WrapSolidifyLogic._labelmapToPolydata(internalHolesLabelmap, 0)) 616 | 617 | # Find largest internal hole 618 | largestHoleExtract = vtk.vtkImageConnectivityFilter() 619 | largestHoleExtract.SetScalarRange(-0.5, 0.5) 620 | largestHoleExtract.SetInputData(internalHolesLabelmap) 621 | largestHoleExtract.SetExtractionModeToLargestRegion() 622 | largestHoleExtract.Update() 623 | largestHolesLabelmap = largestHoleExtract.GetOutput() 624 | 625 | # Convert back to polydata 626 | initialRegionPd = vtk.vtkPolyData() 627 | initialRegionPd.DeepCopy(WrapSolidifyLogic._labelmapToPolydata(largestHolesLabelmap, 0)) 628 | self._saveIntermediateResult("SplitCavitiesLargest", initialRegionPd) 629 | 630 | if self.splitCavities: 631 | return self._shrinkWrap(initialRegionPd) 632 | else: 633 | return initialRegionPd 634 | 635 | @staticmethod 636 | def _polydataToSegment(polydata, segmentationNode, segmentID): 637 | # Get existing representations 638 | segmentation = segmentationNode.GetSegmentation() 639 | try: 640 | masterRepresentationName = segmentationNode.GetSegmentation().GetSourceRepresentationName() 641 | except: 642 | # Legacy (Slicer-5.3 and earlier) 643 | masterRepresentationName = segmentationNode.GetSegmentation().GetMasterRepresentationName() 644 | representationNames = [] 645 | segmentation.GetContainedRepresentationNames(representationNames) 646 | # Update 647 | slicer.vtkSlicerSegmentationsModuleLogic.ClearSegment(segmentationNode, segmentID) 648 | wasModified = segmentationNode.StartModify() 649 | segment = segmentation.GetSegment(segmentID) 650 | segment.RemoveAllRepresentations() 651 | segment.AddRepresentation(vtkSegmentationCorePython.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), polydata) 652 | segmentation.CreateRepresentation(masterRepresentationName) 653 | for representationName in representationNames: 654 | if representationName: 655 | # already converted 656 | continue 657 | segmentation.CreateRepresentation(representationName) 658 | segmentationNode.EndModify(wasModified) 659 | 660 | @staticmethod 661 | def _polydataToNewSegment(polydata, segmentationNode, segmentID): 662 | segmentation = segmentationNode.GetSegmentation() 663 | baseSegment = segmentation.GetSegment(segmentID) 664 | segment = slicer.vtkSegment() 665 | segment.SetName(baseSegment.GetName() + "_solid") 666 | segment.AddRepresentation(vtkSegmentationCorePython.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), polydata) 667 | segmentation.AddSegment(segment) 668 | 669 | def _saveIntermediateResult(self, name, polydata, color=None): 670 | if not self.saveIntermediateResults: 671 | return 672 | 673 | # Show the last intermediate result only, hide previous 674 | if self.previousIntermediateResult: 675 | self.previousIntermediateResult.GetDisplayNode().SetVisibility(False) 676 | 677 | polyDataCopy = vtk.vtkPolyData() 678 | polyDataCopy.DeepCopy(polydata) 679 | outputModel = slicer.modules.models.logic().AddModel(polyDataCopy) 680 | outputModel.SetName("WrapSolidify-{0}-{1}".format(self.intermediateResultCounter, name)) 681 | self.intermediateResultCounter += 1 682 | outputModel.GetDisplayNode().SliceIntersectionVisibilityOn() 683 | outputModel.GetDisplayNode().SetEdgeVisibility(True) 684 | outputModel.GetDisplayNode().SetBackfaceCulling(False) 685 | if color: 686 | outputModel.GetDisplayNode().SetColor(color) 687 | else: 688 | outputModel.GetDisplayNode().SetColor(1.0,1.0,0) 689 | self.previousIntermediateResult = outputModel 690 | 691 | @staticmethod 692 | def _polydataToLabelmap(polydata, spacing = 1.0, extraMarginToBounds = 0, referenceImage = None): 693 | 694 | binaryLabelmap = vtk.vtkImageData() 695 | 696 | if referenceImage: 697 | origin = referenceImage.GetOrigin() 698 | spacing3 = referenceImage.GetSpacing() 699 | extent = referenceImage.GetExtent() 700 | else: 701 | bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 702 | polydata.GetBounds(bounds) 703 | bounds[0] -= extraMarginToBounds 704 | bounds[2] -= extraMarginToBounds 705 | bounds[4] -= extraMarginToBounds 706 | bounds[1] += extraMarginToBounds 707 | bounds[3] += extraMarginToBounds 708 | bounds[5] += extraMarginToBounds 709 | 710 | spacing3 = np.ones(3) * spacing 711 | dim = [0, 0, 0] 712 | for i in range(3): 713 | # Add 3 to the dimensions to have at least 1 voxel thickness and 1 voxel margin on both sides 714 | dim[i] = int(math.ceil((bounds[i * 2 + 1] - bounds[i * 2]) / spacing3[i])) + 3 715 | 716 | # Subtract one spacing to ensure there is a margin 717 | origin = [ 718 | bounds[0] - spacing3[0], 719 | bounds[2] - spacing3[1], 720 | bounds[4] - spacing3[2]] 721 | 722 | extent = [0, dim[0] - 1, 0, dim[1] - 1, 0, dim[2] - 1] 723 | 724 | binaryLabelmap.SetOrigin(origin) 725 | binaryLabelmap.SetSpacing(spacing3) 726 | binaryLabelmap.SetExtent(extent) 727 | 728 | binaryLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) 729 | binaryLabelmap.GetPointData().GetScalars().Fill(0) 730 | 731 | pol2stenc = vtk.vtkPolyDataToImageStencil() 732 | pol2stenc.SetInputData(polydata) 733 | pol2stenc.SetOutputOrigin(origin) 734 | pol2stenc.SetOutputSpacing(spacing3) 735 | pol2stenc.SetOutputWholeExtent(binaryLabelmap.GetExtent()) 736 | 737 | imgstenc = vtk.vtkImageStencil() 738 | imgstenc.SetInputData(binaryLabelmap) 739 | imgstenc.SetStencilConnection(pol2stenc.GetOutputPort()) 740 | imgstenc.ReverseStencilOn() 741 | imgstenc.SetBackgroundValue(1) 742 | imgstenc.Update() 743 | 744 | return imgstenc.GetOutput() 745 | 746 | @staticmethod 747 | def _labelmapToPolydata(labelmap, value=1): 748 | discreteCubes = vtk.vtkDiscreteMarchingCubes() 749 | discreteCubes.SetInputData(labelmap) 750 | discreteCubes.SetValue(0, value) 751 | 752 | reverse = vtk.vtkReverseSense() 753 | reverse.SetInputConnection(discreteCubes.GetOutputPort()) 754 | reverse.ReverseCellsOn() 755 | reverse.ReverseNormalsOn() 756 | reverse.Update() 757 | 758 | return reverse.GetOutput() 759 | 760 | @staticmethod 761 | def _remeshPolydata(polydata, spacing): 762 | labelmap = WrapSolidifyLogic._polydataToLabelmap(polydata, spacing) 763 | return WrapSolidifyLogic._labelmapToPolydata(labelmap) 764 | 765 | @staticmethod 766 | def _smoothPolydata(polydata, smoothingFactor): 767 | passBand = pow(10.0, -4.0 * smoothingFactor) 768 | smootherSinc = vtk.vtkWindowedSincPolyDataFilter() 769 | smootherSinc.SetInputData(polydata) 770 | smootherSinc.SetNumberOfIterations(20) 771 | smootherSinc.FeatureEdgeSmoothingOff() 772 | smootherSinc.BoundarySmoothingOff() 773 | smootherSinc.NonManifoldSmoothingOn() 774 | smootherSinc.NormalizeCoordinatesOn() 775 | smootherSinc.Update() 776 | return smootherSinc.GetOutput() 777 | 778 | 779 | def _shellPreserveCracks(self, shrunkenPd): 780 | """Remove cells of the mesh that are far from the original surface""" 781 | 782 | # Cells that are far from the input mesh will be removed from this 783 | shrunkenPdWithCracks = vtk.vtkPolyData() 784 | shrunkenPdWithCracks.DeepCopy(shrunkenPd) 785 | shrunkenPdWithCracks.BuildLinks() 786 | 787 | # Measure distance from input mesh 788 | implicitDistance = vtk.vtkImplicitPolyDataDistance() 789 | implicitDistance.SetInput(self._inputPd) 790 | 791 | # Determine cutoff distance 792 | spacing = self._inputSpacing / self.remeshOversampling 793 | maxDistance = 0.9 * spacing # 0.9 because it is a bit more than half diameter of a cube (0.5*sqrt(3)) 794 | 795 | numberOfCells = shrunkenPdWithCracks.GetNumberOfCells() 796 | for c in range(numberOfCells): 797 | cell = shrunkenPdWithCracks.GetCell(c) 798 | points = cell.GetPoints() 799 | for p in range(points.GetNumberOfPoints()): 800 | point = points.GetPoint(p) 801 | distance = implicitDistance.EvaluateFunction(point) # TODO: check if cell locator is faster 802 | if abs(distance) > maxDistance: 803 | shrunkenPdWithCracks.DeleteCell(c) 804 | break 805 | shrunkenPdWithCracks.RemoveDeletedCells() 806 | 807 | return shrunkenPdWithCracks 808 | 809 | @staticmethod 810 | def _shellSolidify(surfacePd, shellThickness, shellOffsetDirection): 811 | """Create a thick shell from a surface by extruding in surface normal direction""" 812 | 813 | # remove double vertices 814 | cleanPolyData = vtk.vtkCleanPolyData() 815 | cleanPolyData.SetInputData(surfacePd) 816 | 817 | # create normals 818 | normals = vtk.vtkPolyDataNormals() 819 | normals.SetComputeCellNormals(1) 820 | normals.SetInputConnection(cleanPolyData.GetOutputPort()) 821 | normals.SplittingOff() 822 | if shellOffsetDirection == SHELL_OFFSET_OUTSIDE: 823 | normals.FlipNormalsOn() 824 | normals.Update() 825 | 826 | surfacePd = vtk.vtkPolyData() 827 | surfacePd.DeepCopy(normals.GetOutput()) 828 | numberOfPoints = surfacePd.GetNumberOfPoints() 829 | 830 | # get boundary edges, used later 831 | featureEdges = vtk.vtkFeatureEdges() 832 | featureEdges.BoundaryEdgesOn() 833 | featureEdges.ColoringOff() 834 | featureEdges.FeatureEdgesOff() 835 | featureEdges.NonManifoldEdgesOff() 836 | featureEdges.ManifoldEdgesOff() 837 | featureEdges.SetInputData(normals.GetOutput()) 838 | featureEdges.Update() 839 | 840 | addingPoints = [] 841 | addingPolys = [] 842 | 843 | allNormalsArray = surfacePd.GetCellData().GetArray('Normals') 844 | 845 | for pointID in range(numberOfPoints): 846 | cellIDs = vtk.vtkIdList() 847 | surfacePd.GetPointCells(pointID, cellIDs) 848 | normalsArray = [] 849 | 850 | # ilterate through all cells/faces which contain point 851 | for i in range(cellIDs.GetNumberOfIds()): 852 | n = allNormalsArray.GetTuple3(cellIDs.GetId(i)) 853 | normalsArray.append(np.array(n)) 854 | 855 | # calculate position of new vert 856 | dir_vec = np.zeros(3) 857 | 858 | for n in normalsArray: 859 | dir_vec = dir_vec + np.array(n) 860 | 861 | dir_vec_norm = dir_vec / np.linalg.norm(dir_vec) 862 | proj_length = np.dot(dir_vec_norm, np.array(normalsArray[0])) 863 | dir_vec_finallenght = dir_vec_norm * proj_length 864 | vertex_neu = np.array(surfacePd.GetPoint(pointID)) + (dir_vec_finallenght * shellThickness) 865 | 866 | # append point 867 | addingPoints.append(vertex_neu) 868 | 869 | for cellID in range(surfacePd.GetNumberOfCells()): 870 | pointIDs = vtk.vtkIdList() 871 | surfacePd.GetCellPoints(cellID, pointIDs) 872 | 873 | newPointIDs = vtk.vtkIdList() 874 | for i in reversed(range(pointIDs.GetNumberOfIds())): 875 | newPointIDs.InsertNextId(int(pointIDs.GetId(i) + numberOfPoints)) 876 | 877 | addingPolys.append(newPointIDs) 878 | 879 | doubleSurfacePoints = vtk.vtkPoints() 880 | doubleSurfacePolys = vtk.vtkCellArray() 881 | 882 | doubleSurfacePoints.DeepCopy(surfacePd.GetPoints()) 883 | doubleSurfacePolys.DeepCopy(surfacePd.GetPolys()) 884 | 885 | for p in addingPoints: 886 | doubleSurfacePoints.InsertNextPoint(p) 887 | for p in addingPolys: 888 | doubleSurfacePolys.InsertNextCell(p) 889 | 890 | doubleSurfacePD = vtk.vtkPolyData() 891 | doubleSurfacePD.SetPoints(doubleSurfacePoints) 892 | doubleSurfacePD.SetPolys(doubleSurfacePolys) 893 | 894 | # add faces to boundary edges 895 | mergePoints = vtk.vtkMergePoints() 896 | mergePoints.InitPointInsertion(doubleSurfacePD.GetPoints(), doubleSurfacePD.GetBounds()) 897 | mergePoints.SetDataSet(doubleSurfacePD) 898 | mergePoints.BuildLocator() 899 | 900 | manifoldPolys = vtk.vtkCellArray() 901 | manifoldPolys.DeepCopy(doubleSurfacePD.GetPolys()) 902 | manifoldPoints = vtk.vtkPoints() 903 | manifoldPoints.DeepCopy(doubleSurfacePD.GetPoints()) 904 | 905 | for e in range(featureEdges.GetOutput().GetNumberOfCells()): 906 | pointIDs = vtk.vtkIdList() 907 | featureEdges.GetOutput().GetCellPoints(e, pointIDs) 908 | if pointIDs.GetNumberOfIds() == 2: # -> Edge 909 | matchingPointIDs = [] 910 | newPointIDs = vtk.vtkIdList() 911 | for p in range(2): 912 | matchingPointIDs.append(mergePoints.IsInsertedPoint(featureEdges.GetOutput().GetPoint(pointIDs.GetId(p)))) 913 | if not (-1) in matchingPointIDs: # edge vertex not found in original pd, should not happen 914 | newPointIDs.InsertNextId(matchingPointIDs[1]) 915 | newPointIDs.InsertNextId(matchingPointIDs[0]) 916 | newPointIDs.InsertNextId(matchingPointIDs[0]+numberOfPoints) 917 | newPointIDs.InsertNextId(matchingPointIDs[1]+numberOfPoints) 918 | manifoldPolys.InsertNextCell(newPointIDs) 919 | 920 | manifoldPD = vtk.vtkPolyData() 921 | manifoldPD.SetPoints(manifoldPoints) 922 | manifoldPD.SetPolys(manifoldPolys) 923 | 924 | triangleFilter = vtk.vtkTriangleFilter() 925 | triangleFilter.SetInputData(manifoldPD) 926 | 927 | # Compute normals to make the result look smooth 928 | normals = vtk.vtkPolyDataNormals() 929 | normals.SetInputConnection(triangleFilter.GetOutputPort()) 930 | normals.Update() 931 | 932 | return normals.GetOutput() 933 | 934 | 935 | ARG_DEFAULTS = {} 936 | ARG_OPTIONS = {} 937 | 938 | ARG_REGION = 'region' 939 | REGION_OUTER_SURFACE = 'outerSurface' 940 | REGION_LARGEST_CAVITY = 'largestCavity' 941 | REGION_SEGMENT = 'segment' 942 | ARG_OPTIONS[ARG_REGION] = [REGION_OUTER_SURFACE, REGION_LARGEST_CAVITY, REGION_SEGMENT] 943 | ARG_DEFAULTS[ARG_REGION] = REGION_OUTER_SURFACE 944 | 945 | ARG_REGION_SEGMENT_ID = 'regionSegmentID' 946 | ARG_DEFAULTS[ARG_REGION_SEGMENT_ID] = '' 947 | 948 | ARG_CARVE_HOLES_IN_OUTER_SURFACE = 'carveHolesInOuterSurface' 949 | ARG_DEFAULTS[ARG_CARVE_HOLES_IN_OUTER_SURFACE] = False 950 | 951 | ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER = 'carveHolesInOuterSurfaceDiameter' 952 | ARG_DEFAULTS[ARG_CARVE_HOLES_IN_OUTER_SURFACE_DIAMETER] = 10.0 953 | 954 | ARG_SPLIT_CAVITIES = 'splitCavities' 955 | ARG_DEFAULTS[ARG_SPLIT_CAVITIES] = False 956 | 957 | ARG_SPLIT_CAVITIES_DIAMETER = 'splitCavitiesDiameter' 958 | ARG_DEFAULTS[ARG_SPLIT_CAVITIES_DIAMETER] = 5 959 | 960 | ARG_CREATE_SHELL = 'createShell' 961 | ARG_DEFAULTS[ARG_CREATE_SHELL] = False 962 | 963 | ARG_SHELL_THICKNESS = 'shellThickness' 964 | ARG_DEFAULTS[ARG_SHELL_THICKNESS] = 1.5 965 | 966 | ARG_SHELL_OFFSET_DIRECTION = 'shellOffsetDirection' 967 | SHELL_OFFSET_INSIDE = 'inside' 968 | SHELL_OFFSET_OUTSIDE = 'outside' 969 | ARG_OPTIONS[ARG_SHELL_OFFSET_DIRECTION] = [SHELL_OFFSET_INSIDE, SHELL_OFFSET_OUTSIDE] 970 | ARG_DEFAULTS[ARG_SHELL_OFFSET_DIRECTION] = SHELL_OFFSET_INSIDE 971 | 972 | ARG_SHELL_PRESERVE_CRACKS = 'preserveCracks' 973 | ARG_DEFAULTS[ARG_SHELL_PRESERVE_CRACKS] = True 974 | 975 | ARG_OUTPUT_TYPE = 'outputType' 976 | OUTPUT_SEGMENT = 'segment' 977 | OUTPUT_NEW_SEGMENT = 'newSegment' 978 | OUTPUT_MODEL = 'model' 979 | ARG_OPTIONS[ARG_OUTPUT_TYPE] = [OUTPUT_SEGMENT, OUTPUT_NEW_SEGMENT, OUTPUT_MODEL] 980 | ARG_DEFAULTS[ARG_OUTPUT_TYPE] = OUTPUT_SEGMENT 981 | 982 | ARG_OUTPUT_MODEL_NODE = 'WrapSolidify.OutputModelNodeID' 983 | 984 | ARG_REMESH_OVERSAMPLING = 'remeshOversampling' 985 | ARG_DEFAULTS[ARG_REMESH_OVERSAMPLING] = 1.5 # 1.5x oversampling by default 986 | 987 | ARG_SMOOTHING_FACTOR = 'smoothingFactor' 988 | ARG_DEFAULTS[ARG_SMOOTHING_FACTOR] = 0.2 989 | 990 | ARG_SHRINKWRAP_ITERATIONS = 'shrinkwrapIterations' 991 | ARG_DEFAULTS[ARG_SHRINKWRAP_ITERATIONS] = 6 992 | 993 | ARG_SAVE_INTERMEDIATE_RESULTS = 'saveIntermediateResults' 994 | ARG_DEFAULTS[ARG_SAVE_INTERMEDIATE_RESULTS] = False 995 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/SegmentEditorWrapSolidifyLib/SegmentEditorEffect.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SegmentEditorWrapSolidify 4 | 5 | 6 | 7 | 0 8 | 0 9 | 311 10 | 264 11 | 12 | 13 | 14 | 15 | 0 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | Region: 30 | 31 | 32 | 33 | 34 | 35 | 36 | QFrame::NoFrame 37 | 38 | 39 | QFrame::Plain 40 | 41 | 42 | 43 | 0 44 | 45 | 46 | 0 47 | 48 | 49 | 0 50 | 51 | 52 | 0 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Create shell: 61 | 62 | 63 | 64 | 65 | 66 | 67 | QFrame::NoFrame 68 | 69 | 70 | QFrame::Plain 71 | 72 | 73 | 74 | 0 75 | 76 | 77 | 0 78 | 79 | 80 | 0 81 | 82 | 83 | 0 84 | 85 | 86 | 0 87 | 88 | 89 | 90 | 91 | Preserve surface cracks: 92 | 93 | 94 | 95 | 96 | 97 | 98 | Offset direction: 99 | 100 | 101 | 102 | 103 | 104 | 105 | outside 106 | 107 | 108 | 109 | 110 | 111 | 112 | Qt::Horizontal 113 | 114 | 115 | 116 | 40 117 | 20 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Hollow out the solidified object and keep only a thin outer shell. 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | inside 136 | 137 | 138 | 139 | 140 | 141 | 142 | Thickness: 143 | 144 | 145 | 146 | 147 | 148 | 149 | Leaves gaps in the created shell where the original segment had surface cracks. Increase remesh oversampling if thin cracks are not preserved. 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Thickness of the output shell. If value is positive then extracted surface is used as outer surface, otherwise it will be used as inner surface. If the value is smaller than the segmentation's resolution then the result may appear fragmented and so a model output or larger shell thickness must be chosen. If surface cracks do not need to be preserved then shell can be created using "Hollow" segment editor effect. 160 | 161 | 162 | 0.100000000000000 163 | 164 | 165 | 0.000000000000000 166 | 167 | 168 | 20.000000000000000 169 | 170 | 171 | length 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | Output: 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 0 190 | 191 | 192 | 0 193 | 194 | 195 | 0 196 | 197 | 198 | 0 199 | 200 | 201 | 0 202 | 203 | 204 | 205 | 206 | 207 | 0 208 | 0 209 | 210 | 211 | 212 | 213 | vtkMRMLModelNode 214 | 215 | 216 | 217 | Model 218 | 219 | 220 | true 221 | 222 | 223 | true 224 | 225 | 226 | Create new Model 227 | 228 | 229 | 230 | 231 | 232 | 233 | New segment 234 | 235 | 236 | 237 | 238 | 239 | 240 | Overwrite segment 241 | 242 | 243 | 244 | 245 | 246 | 247 | Model: 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | Advanced 258 | 259 | 260 | true 261 | 262 | 263 | 6 264 | 265 | 266 | 267 | 268 | 269 | Smoothing Factor used for operation, as the a surface representation of the segmentation will be used for this algorithm. 270 | 271 | 272 | 0.050000000000000 273 | 274 | 275 | 0.100000000000000 276 | 277 | 278 | 1.000000000000000 279 | 280 | 281 | 0.300000000000000 282 | 283 | 284 | 285 | 286 | 287 | 288 | Thickness of the output shell. If the value is very small then it may significantly increase the computation time and may appear fractured if output is segmentation. 289 | 290 | 291 | 0.100000000000000 292 | 293 | 294 | 1.000000000000000 295 | 296 | 297 | 0.100000000000000 298 | 299 | 300 | 5.000000000000000 301 | 302 | 303 | 1.000000000000000 304 | 305 | 306 | x 307 | 308 | 309 | 310 | 311 | 312 | 313 | Save intermediate results: 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 0 322 | 323 | 324 | 0 325 | 326 | 327 | 0 328 | 329 | 330 | 0 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Save intermediate results as models. Useful for diagnostics of the solidification process. 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | Increase this value if output seems not completely converged to input. Increases computation time. 349 | 350 | 351 | 0 352 | 353 | 354 | 1.000000000000000 355 | 356 | 357 | 1.000000000000000 358 | 359 | 360 | 20.000000000000000 361 | 362 | 363 | 364 | 365 | 366 | 367 | Smoothing factor: 368 | 369 | 370 | 371 | 372 | 373 | 374 | Oversampling: 375 | 376 | 377 | 378 | 379 | 380 | 381 | Number of iterations: 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | Apply 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 0 400 | 401 | 402 | 0 403 | 404 | 405 | 0 406 | 407 | 408 | 0 409 | 410 | 411 | 0 412 | 413 | 414 | 415 | 416 | Custom: 417 | 418 | 419 | 420 | 421 | 422 | 423 | Largest cavity 424 | 425 | 426 | 427 | 428 | 429 | 430 | Outer surface 431 | 432 | 433 | 434 | 435 | 436 | 437 | Cavities with smaller diameter than this threshold will not be carved out. 438 | 439 | 440 | 0.100000000000000 441 | 442 | 443 | 0.100000000000000 444 | 445 | 446 | 100.000000000000000 447 | 448 | 449 | length 450 | 451 | 452 | 453 | 454 | 455 | 456 | Propagate surface deeper into concave regions 457 | 458 | 459 | Carve holes: 460 | 461 | 462 | 463 | 464 | 465 | 466 | Propagate surface deeper into concave regions 467 | 468 | 469 | Split cavities: 470 | 471 | 472 | 473 | 474 | 475 | 476 | Cavities with smaller diameter than this threshold will not be carved out. 477 | 478 | 479 | 0.100000000000000 480 | 481 | 482 | 0.100000000000000 483 | 484 | 485 | 100.000000000000000 486 | 487 | 488 | length 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 0 497 | 0 498 | 499 | 500 | 501 | false 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | ctkCollapsibleGroupBox 513 | QGroupBox 514 |
ctkCollapsibleGroupBox.h
515 | 1 516 |
517 | 518 | ctkSliderWidget 519 | QWidget 520 |
ctkSliderWidget.h
521 |
522 | 523 | qMRMLNodeComboBox 524 | QWidget 525 |
qMRMLNodeComboBox.h
526 |
527 | 528 | qMRMLSliderWidget 529 | ctkSliderWidget 530 |
qMRMLSliderWidget.h
531 |
532 | 533 | qMRMLWidget 534 | QWidget 535 |
qMRMLWidget.h
536 | 1 537 |
538 | 539 | qMRMLSegmentSelectorWidget 540 | qMRMLWidget 541 |
qMRMLSegmentSelectorWidget.h
542 |
543 |
544 | 545 | 546 | 547 | SegmentEditorWrapSolidify 548 | mrmlSceneChanged(vtkMRMLScene*) 549 | regionSegmentSelector 550 | setMRMLScene(vtkMRMLScene*) 551 | 552 | 553 | 144 554 | 351 555 | 556 | 557 | 260 558 | 71 559 | 560 | 561 | 562 | 563 | SegmentEditorWrapSolidify 564 | mrmlSceneChanged(vtkMRMLScene*) 565 | outputModelNodeSelector 566 | setMRMLScene(vtkMRMLScene*) 567 | 568 | 569 | 230 570 | 351 571 | 572 | 573 | 183 574 | 163 575 | 576 | 577 | 578 | 579 | SegmentEditorWrapSolidify 580 | mrmlSceneChanged(vtkMRMLScene*) 581 | carveHolesInOuterSurfaceDiameterSlider 582 | setMRMLScene(vtkMRMLScene*) 583 | 584 | 585 | 73 586 | 351 587 | 588 | 589 | 185 590 | 87 591 | 592 | 593 | 594 | 595 | SegmentEditorWrapSolidify 596 | mrmlSceneChanged(vtkMRMLScene*) 597 | shellThicknessSlider 598 | setMRMLScene(vtkMRMLScene*) 599 | 600 | 601 | 202 602 | 351 603 | 604 | 605 | 291 606 | 118 607 | 608 | 609 | 610 | 611 | SegmentEditorWrapSolidify 612 | mrmlSceneChanged(vtkMRMLScene*) 613 | splitCavitiesDiameterSlider 614 | setMRMLScene(vtkMRMLScene*) 615 | 616 | 617 | 110 618 | 249 619 | 620 | 621 | 262 622 | 41 623 | 624 | 625 | 626 | 627 |
628 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/SegmentEditorWrapSolidifyLib/__init__.py: -------------------------------------------------------------------------------- 1 | from SegmentEditorEffects.AbstractScriptedSegmentEditorEffect import * 2 | from SegmentEditorEffects.AbstractScriptedSegmentEditorLabelEffect import * 3 | 4 | from SegmentEditorEffect import * 5 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(Python) 2 | -------------------------------------------------------------------------------- /SegmentEditorWrapSolidify/Testing/Python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) 3 | -------------------------------------------------------------------------------- /SurfaceWrapSolidify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianandress/Slicer-SurfaceWrapSolidify/600681438bca4ab900511d8677b7cb3046767dc2/SurfaceWrapSolidify.png --------------------------------------------------------------------------------