├── .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 | 
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 | 
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 | [](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 | [](https://1drv.ms/v/s!AqzdGuIdWLfeiNpO1rx9ZGbbhk6frQ?e=5NFQMt)
50 |
51 | Example processing result:
52 |
53 | 
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 | 
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 | 
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 | 
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 |
515 | 1
516 |
517 |
518 | ctkSliderWidget
519 | QWidget
520 |
521 |
522 |
523 | qMRMLNodeComboBox
524 | QWidget
525 |
526 |
527 |
528 | qMRMLSliderWidget
529 | ctkSliderWidget
530 |
531 |
532 |
533 | qMRMLWidget
534 | QWidget
535 |
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
--------------------------------------------------------------------------------