├── .gitignore ├── CMakeLists.txt ├── License.txt ├── Logo ├── SliceTracker.ai └── SliceTracker_original.ai ├── README.md ├── Screenshots ├── Slicetracker.gif └── needle_tracking.png ├── SliceTracker.png ├── SliceTracker ├── CMakeLists.txt ├── Resources │ ├── Colors │ │ └── mpReviewColors.csv │ ├── Icons │ │ ├── SliceTracker.png │ │ ├── icon-needle.png │ │ ├── icon-newImageData.png │ │ ├── icon-quickSegmentation.png │ │ ├── icon-revealCursor.png │ │ ├── icon-template.png │ │ ├── icon-track.png │ │ └── icon-zframe.png │ ├── default.cfg │ ├── version.json │ ├── version.json.in │ └── zframe │ │ ├── ProstateTemplate.csv │ │ ├── zframe-config.csv │ │ └── zframe-model.vtk ├── SliceTracker.py ├── SliceTrackerRegistration.py ├── SliceTrackerUtils │ ├── __init__.py │ ├── algorithms │ │ ├── __init__.py │ │ ├── automaticProstateSegmentation.py │ │ └── zFrameRegistration.py │ ├── configuration.py │ ├── constants.py │ ├── helpers.py │ ├── preopHandler.py │ ├── session.py │ ├── sessionData.py │ ├── steps │ │ ├── __init__.py │ │ ├── base.py │ │ ├── evaluation.py │ │ ├── overview.py │ │ ├── plugins │ │ │ ├── __init__.py │ │ │ ├── case.py │ │ │ ├── charts.py │ │ │ ├── needle.py │ │ │ ├── results.py │ │ │ ├── segmentation │ │ │ │ ├── __init__.py │ │ │ │ ├── automatic.py │ │ │ │ ├── base.py │ │ │ │ └── manual.py │ │ │ ├── segmentationValidator.py │ │ │ ├── targeting.py │ │ │ ├── targets.py │ │ │ └── training.py │ │ ├── segmentation.py │ │ └── zFrameRegistration.py │ └── watch.py ├── SurfaceCutToLabel.py └── __init__.py ├── Testing ├── CMakeLists.txt └── SliceTrackerTests.py ├── doc ├── output_example.json └── output_schema.json ├── puml ├── RegistrationResults.puml ├── SliceTracker.puml ├── SliceTrackerPlugins.puml ├── SliceTrackerSession.puml ├── SliceTrackerSteps.puml ├── SlicerDevelopmentToolbox.puml ├── components.puml └── intraop_reception.puml ├── startSlicelet.sh └── startSlicelet_Ubuntu.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /RegistrationModule/RegistrationModule.pyc 2 | /RegistrationModule/qRegistrationModule.qrc 3 | /RegistrationModule/Log 4 | RegistrationModule/Resources/Testing/intraopDir/* 5 | RegistrationModule/Resources/Testing/preopDir/* 6 | RegistrationModule/Resources/Testing/testData_1/* 7 | RegistrationModule/Resources/Testing/testData_2/* 8 | RegistrationModule/Resources/Testing/testData_3/* 9 | .gitignore.save 10 | *.pyc 11 | .idea/* -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.9) 2 | 3 | #----------------------------------------------------------------------------- 4 | # Extension meta-information 5 | set(EXTENSION_NAME SliceTracker) 6 | set(EXTENSION_HOMEPAGE "https://github.com/SlicerProstate/SliceTracker") 7 | set(EXTENSION_CATEGORY "IGT") 8 | set(EXTENSION_CONTRIBUTORS "Christian Herz (SPL), Peter Behringer (SPL), Kyle MacNeil (Med-i Lab, Queen's; SPL), Andrey Fedorov (SPL)") 9 | set(EXTENSION_DESCRIPTION "This extension provides modules to support in-bore MRI-guided prostate biopsy.") 10 | set(EXTENSION_ICONURL "https://www.slicer.org/w/images/b/b1/SliceTracker_Logo_1.1_128x128.png") 11 | set(EXTENSION_SCREENSHOTURLS "http://wiki.slicer.org/slicerWiki/images/7/78/Slicetracker.gif") 12 | set(EXTENSION_STATUS "beta") 13 | set(EXTENSION_DEPENDS "SlicerDevelopmentToolbox mpReview SegmentEditorExtraEffects DeepInfer ZFrameRegistration") 14 | 15 | #----------------------------------------------------------------------------- 16 | find_package(Slicer REQUIRED) 17 | include(${Slicer_USE_FILE}) 18 | 19 | add_subdirectory(SliceTracker) 20 | 21 | 22 | #----------------------------------------------------------------------------- 23 | if(BUILD_TESTING) 24 | 25 | # Register the unittest subclass in the main script as a ctest. 26 | # Note that the test will also be available at runtime. 27 | # slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) 28 | 29 | # Additional build-time testing 30 | # add_subdirectory(Testing) 31 | endif() 32 | 33 | #----------------------------------------------------------------------------- 34 | include(${Slicer_EXTENSION_CPACK}) 35 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | 2 | For more information, please see: 3 | 4 | http://www.slicer.org 5 | 6 | The 3D Slicer license below is a BSD style license, with extensions 7 | to cover contributions and other issues specific to 3D Slicer. 8 | 9 | 10 | 3D Slicer Contribution and Software License Agreement ("Agreement") 11 | Version 1.0 (December 20, 2005) 12 | 13 | This Agreement covers contributions to and downloads from the 3D 14 | Slicer project ("Slicer") maintained by The Brigham and Women's 15 | Hospital, Inc. ("Brigham"). Part A of this Agreement applies to 16 | contributions of software and/or data to Slicer (including making 17 | revisions of or additions to code and/or data already in Slicer). Part 18 | B of this Agreement applies to downloads of software and/or data from 19 | Slicer. Part C of this Agreement applies to all transactions with 20 | Slicer. If you distribute Software (as defined below) downloaded from 21 | Slicer, all of the paragraphs of Part B of this Agreement must be 22 | included with and apply to such Software. 23 | 24 | Your contribution of software and/or data to Slicer (including prior 25 | to the date of the first publication of this Agreement, each a 26 | "Contribution") and/or downloading, copying, modifying, displaying, 27 | distributing or use of any software and/or data from Slicer 28 | (collectively, the "Software") constitutes acceptance of all of the 29 | terms and conditions of this Agreement. If you do not agree to such 30 | terms and conditions, you have no right to contribute your 31 | Contribution, or to download, copy, modify, display, distribute or use 32 | the Software. 33 | 34 | PART A. CONTRIBUTION AGREEMENT - License to Brigham with Right to 35 | Sublicense ("Contribution Agreement"). 36 | 37 | 1. As used in this Contribution Agreement, "you" means the individual 38 | contributing the Contribution to Slicer and the institution or 39 | entity which employs or is otherwise affiliated with such 40 | individual in connection with such Contribution. 41 | 42 | 2. This Contribution Agreement applies to all Contributions made to 43 | Slicer, including without limitation Contributions made prior to 44 | the date of first publication of this Agreement. If at any time you 45 | make a Contribution to Slicer, you represent that (i) you are 46 | legally authorized and entitled to make such Contribution and to 47 | grant all licenses granted in this Contribution Agreement with 48 | respect to such Contribution; (ii) if your Contribution includes 49 | any patient data, all such data is de-identified in accordance with 50 | U.S. confidentiality and security laws and requirements, including 51 | but not limited to the Health Insurance Portability and 52 | Accountability Act (HIPAA) and its regulations, and your disclosure 53 | of such data for the purposes contemplated by this Agreement is 54 | properly authorized and in compliance with all applicable laws and 55 | regulations; and (iii) you have preserved in the Contribution all 56 | applicable attributions, copyright notices and licenses for any 57 | third party software or data included in the Contribution. 58 | 59 | 3. Except for the licenses granted in this Agreement, you reserve all 60 | right, title and interest in your Contribution. 61 | 62 | 4. You hereby grant to Brigham, with the right to sublicense, a 63 | perpetual, worldwide, non-exclusive, no charge, royalty-free, 64 | irrevocable license to use, reproduce, make derivative works of, 65 | display and distribute the Contribution. If your Contribution is 66 | protected by patent, you hereby grant to Brigham, with the right to 67 | sublicense, a perpetual, worldwide, non-exclusive, no-charge, 68 | royalty-free, irrevocable license under your interest in patent 69 | rights covering the Contribution, to make, have made, use, sell and 70 | otherwise transfer your Contribution, alone or in combination with 71 | any other code. 72 | 73 | 5. You acknowledge and agree that Brigham may incorporate your 74 | Contribution into Slicer and may make Slicer available to members 75 | of the public on an open source basis under terms substantially in 76 | accordance with the Software License set forth in Part B of this 77 | Agreement. You further acknowledge and agree that Brigham shall 78 | have no liability arising in connection with claims resulting from 79 | your breach of any of the terms of this Agreement. 80 | 81 | 6. YOU WARRANT THAT TO THE BEST OF YOUR KNOWLEDGE YOUR CONTRIBUTION 82 | DOES NOT CONTAIN ANY CODE THAT REQURES OR PRESCRIBES AN "OPEN 83 | SOURCE LICENSE" FOR DERIVATIVE WORKS (by way of non-limiting 84 | example, the GNU General Public License or other so-called 85 | "reciprocal" license that requires any derived work to be licensed 86 | under the GNU General Public License or other "open source 87 | license"). 88 | 89 | PART B. DOWNLOADING AGREEMENT - License from Brigham with Right to 90 | Sublicense ("Software License"). 91 | 92 | 1. As used in this Software License, "you" means the individual 93 | downloading and/or using, reproducing, modifying, displaying and/or 94 | distributing the Software and the institution or entity which 95 | employs or is otherwise affiliated with such individual in 96 | connection therewith. The Brigham and Women?s Hospital, 97 | Inc. ("Brigham") hereby grants you, with right to sublicense, with 98 | respect to Brigham's rights in the software, and data, if any, 99 | which is the subject of this Software License (collectively, the 100 | "Software"), a royalty-free, non-exclusive license to use, 101 | reproduce, make derivative works of, display and distribute the 102 | Software, provided that: 103 | 104 | (a) you accept and adhere to all of the terms and conditions of this 105 | Software License; 106 | 107 | (b) in connection with any copy of or sublicense of all or any portion 108 | of the Software, all of the terms and conditions in this Software 109 | License shall appear in and shall apply to such copy and such 110 | sublicense, including without limitation all source and executable 111 | forms and on any user documentation, prefaced with the following 112 | words: "All or portions of this licensed product (such portions are 113 | the "Software") have been obtained under license from The Brigham and 114 | Women's Hospital, Inc. and are subject to the following terms and 115 | conditions:" 116 | 117 | (c) you preserve and maintain all applicable attributions, copyright 118 | notices and licenses included in or applicable to the Software; 119 | 120 | (d) modified versions of the Software must be clearly identified and 121 | marked as such, and must not be misrepresented as being the original 122 | Software; and 123 | 124 | (e) you consider making, but are under no obligation to make, the 125 | source code of any of your modifications to the Software freely 126 | available to others on an open source basis. 127 | 128 | 2. The license granted in this Software License includes without 129 | limitation the right to (i) incorporate the Software into 130 | proprietary programs (subject to any restrictions applicable to 131 | such programs), (ii) add your own copyright statement to your 132 | modifications of the Software, and (iii) provide additional or 133 | different license terms and conditions in your sublicenses of 134 | modifications of the Software; provided that in each case your use, 135 | reproduction or distribution of such modifications otherwise 136 | complies with the conditions stated in this Software License. 137 | 138 | 3. This Software License does not grant any rights with respect to 139 | third party software, except those rights that Brigham has been 140 | authorized by a third party to grant to you, and accordingly you 141 | are solely responsible for (i) obtaining any permissions from third 142 | parties that you need to use, reproduce, make derivative works of, 143 | display and distribute the Software, and (ii) informing your 144 | sublicensees, including without limitation your end-users, of their 145 | obligations to secure any such required permissions. 146 | 147 | 4. The Software has been designed for research purposes only and has 148 | not been reviewed or approved by the Food and Drug Administration 149 | or by any other agency. YOU ACKNOWLEDGE AND AGREE THAT CLINICAL 150 | APPLICATIONS ARE NEITHER RECOMMENDED NOR ADVISED. Any 151 | commercialization of the Software is at the sole risk of the party 152 | or parties engaged in such commercialization. You further agree to 153 | use, reproduce, make derivative works of, display and distribute 154 | the Software in compliance with all applicable governmental laws, 155 | regulations and orders, including without limitation those relating 156 | to export and import control. 157 | 158 | 5. The Software is provided "AS IS" and neither Brigham nor any 159 | contributor to the software (each a "Contributor") shall have any 160 | obligation to provide maintenance, support, updates, enhancements 161 | or modifications thereto. BRIGHAM AND ALL CONTRIBUTORS SPECIFICALLY 162 | DISCLAIM ALL EXPRESS AND IMPLIED WARRANTIES OF ANY KIND INCLUDING, 163 | BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR 164 | A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL 165 | BRIGHAM OR ANY CONTRIBUTOR BE LIABLE TO ANY PARTY FOR DIRECT, 166 | INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES 167 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY ARISING IN ANY WAY 168 | RELATED TO THE SOFTWARE, EVEN IF BRIGHAM OR ANY CONTRIBUTOR HAS 169 | BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. TO THE MAXIMUM 170 | EXTENT NOT PROHIBITED BY LAW OR REGULATION, YOU FURTHER ASSUME ALL 171 | LIABILITY FOR YOUR USE, REPRODUCTION, MAKING OF DERIVATIVE WORKS, 172 | DISPLAY, LICENSE OR DISTRIBUTION OF THE SOFTWARE AND AGREE TO 173 | INDEMNIFY AND HOLD HARMLESS BRIGHAM AND ALL CONTRIBUTORS FROM AND 174 | AGAINST ANY AND ALL CLAIMS, SUITS, ACTIONS, DEMANDS AND JUDGMENTS 175 | ARISING THEREFROM. 176 | 177 | 6. None of the names, logos or trademarks of Brigham or any of 178 | Brigham's affiliates or any of the Contributors, or any funding 179 | agency, may be used to endorse or promote products produced in 180 | whole or in part by operation of the Software or derived from or 181 | based on the Software without specific prior written permission 182 | from the applicable party. 183 | 184 | 7. Any use, reproduction or distribution of the Software which is not 185 | in accordance with this Software License shall automatically revoke 186 | all rights granted to you under this Software License and render 187 | Paragraphs 1 and 2 of this Software License null and void. 188 | 189 | 8. This Software License does not grant any rights in or to any 190 | intellectual property owned by Brigham or any Contributor except 191 | those rights expressly granted hereunder. 192 | 193 | PART C. MISCELLANEOUS 194 | 195 | This Agreement shall be governed by and construed in accordance with 196 | the laws of The Commonwealth of Massachusetts without regard to 197 | principles of conflicts of law. This Agreement shall supercede and 198 | replace any license terms that you may have agreed to previously with 199 | respect to Slicer. 200 | -------------------------------------------------------------------------------- /Logo/SliceTracker.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/Logo/SliceTracker.ai -------------------------------------------------------------------------------- /Logo/SliceTracker_original.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/Logo/SliceTracker_original.ai -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Alt text](SliceTracker.png) 2 | 3 | ### Overview 4 | 5 | SliceTracker is a [3D Slicer](http://slicer.org) extension designed to support the workflow of the in-bore MRI-guided targeted prostate biopsy (MRgBx) (see references below for [clinical context](http://ncigt.org/prostate-biopsy)). SliceTracker was developed and tested to support transperineal MRgBx procedure in the [Advanced Multimodality Image Guided Operating (AMIGO)](http://www.brighamandwomens.org/research/amigo/default.aspx) at the Brigham and Women's Hospital, Boston. Its applicability to other types of procedures has not been evaluated. 6 | 7 | Capabilities of SliceTracker include: 8 | * automatic processing of the patient DICOM images 9 | * planning of the procedure (biopsy target localization) 10 | * calibration of the intra-procedural image frame of reference with the transperineal biopsy template 11 | * automated segmentation of the prostate gland 12 | * deformable intensity-based registration to support 13 | * re-identification of the pre-procedurally defined targets in the intra-procedural images 14 | * automated, continuous tracking of the biopsy targets during the course of the procedure 15 | * structured collection of the relevant data during the course of the procedure (images, results of segmentation and registration, target tracking) 16 | * visualization of the intra-procedural images and support of specialized hanging protocols to facilitate needle tracking, biopsy template and calibration device visualization, assessment of the image registration results, etc. 17 | 18 | For more details, please read [SliceTracker user guide](https://slicerprostate.gitbooks.io/slicetracker). 19 | 20 | ![](Screenshots/Slicetracker.gif) 21 | 22 | ### Disclaimer 23 | 24 | SlicerTracker, same as 3D Slicer, is a research software. **SliceTracker is NOT an FDA-approved medical device**. It is not intended for clinical use. The user assumes full responsibility to comply with the appropriate regulations. 25 | 26 | ### Support 27 | 28 | Please feel free to contact us for questions, feedback, suggestions, bugs, or you can create issues in the issue tracker: https://github.com/SlicerProstate/SliceTracker/issues 29 | 30 | * [Andrey Fedorov](https://github.com/fedorov) fedorov@bwh.harvard.edu 31 | 32 | * Christian Herz cherz@bwh.harvard.edu 33 | 34 | * Peter Behringer peterbehringer@gmx.de 35 | 36 | ### Acknowledgments 37 | 38 | This is the main publication describing SliceTracker: 39 | 40 | Herz, C., MacNeil, K., Behringer, P. A., Tokuda, J., Mehrtash, A., Mousavi, P., Kikinis, R., Fennessy, F. M., Tempany, C. M., Tuncali, K. & Fedorov, A. Open Source Platform for Transperineal In-bore MRI-guided Targeted Prostate Biopsy. IEEE Transactions on Biomedical Engineering 1–1 (2019). DOI: [10.1109/TBME.2019.2918731](https://ieeexplore.ieee.org/document/8721095) PMID: [31135342](https://www.ncbi.nlm.nih.gov/pubmed/31135342) 41 | 42 | Development of SliceTracker is supported in part by the following NIH grants: 43 | 44 | * P41 EB015898 National Center for Image Guided Therapy (NCIGT), http://ncigt.org 45 | * U24 CA180918 Quantitative Image Informatics for Cancer Research (QIICR), http://qiicr.org 46 | * R01 CA111288 Enabling Technologies for MRI-guided prostate interventions 47 | 48 | Several components of SliceTracker were adopted from other open source projects 49 | as follows: 50 | * Needle guidance core functionality was adopted from 51 | https://github.com/ProstateBRP/NeedleGuideTemplate, courtesy Junichi 52 | Tokuda @tokjun 53 | * [Z-frame 54 | calibration](https://github.com/SlicerProstate/SliceTracker/commits/master/ZFrameCalibration) 55 | was adopted from the [ProstateNav module of 3D Slicer version 56 | 3](https://www.slicer.org/slicerWiki/index.php/Modules:ProstateNav-Documentation-3.6) (see source 57 | code [here](https://github.com/SlicerProstate/ProstateNav), no revision 58 | history); although we 59 | do not have the precise record of contribution to that functionality in the 60 | ProstateNav module, we believe main contributors were Junichi Tokuda and 61 | Simon Di Maio (while at BWH, now at [Intuitive 62 | Surgical](http://www.intuitivesurgical.com/)) 63 | 64 | ### References 65 | 66 | The following publications cover different aspects of work that led to the development of SliceTracker. 67 | 68 | 1. Behringer P., Herz C., Penzkofer T., Tuncali K., Tempany C., Fedorov A. 2015. Open-­source Platform for Prostate Motion Tracking during in­-bore Targeted MRI­-guided Biopsy. In: MICCAI Workshop on Clinical Image-based Procedures: Translational Research in Medical Imaging. DOI: [10.1007/978-3-319-31808-0_15](http://doi.org/10.1007/978-3-319-31808-0_15) (also see the [accompanying web site](http://slicerprostate.github.io/ProstateMotionStudy/)): **software development, prostate motion tracking**. 69 | 2. Fedorov A., Beichel R., Kalpathy-Cramer J., Finet J., Fillion-Robin J-CC., Pujol S., Bauer C., Jennings D., Fennessy F., Sonka M., Buatti J., Aylward S., Miller J V., Pieper S., Kikinis R. 2012. 3D Slicer as an image computing platform for the Quantitative Imaging Network. Magnetic resonance imaging 30:1323–1341. DOI: [10.1016/j.mri.2012.05.001](http://doi.org/10.1016/j.mri.2012.05.001): **3D Slicer platform**. 70 | 3. Fedorov A., Tuncali K., Fennessy FM., Tokuda J., Hata N., Wells WM., Kikinis R., Tempany CM. 2012. Image registration for targeted MRI-guided transperineal prostate biopsy. Journal of magnetic resonance imaging: JMRI 36:987–992. DOI: [10.1002/jmri.23688](http://doi.org/10.1002/jmri.23688): **deformable registration approach**. 71 | 4. Penzkofer T., Tuncali K., Fedorov A., Song S-E., Tokuda J., Fennessy FM., Vangel MG., Kibel AS., Mulkern RV., Wells WM., Hata N., Tempany CMC. 2015. Transperineal in-bore 3-T MR imaging-guided prostate biopsy: a prospective clinical observational study. Radiology 274:170–180. DOI: [10.1148/radiol.14140221](http://doi.org/10.1148/radiol.14140221): **clinical results**. 72 | 5. Tokuda J., Tuncali K., Iordachita I., Song S-EE., Fedorov A., Oguro S., Lasso A., Fennessy FM., Tempany CM., Hata N. 2012. In-bore setup and software for 3T MRI-guided transperineal prostate biopsy. Physics in medicine and biology 57:5823–5840. DOI: [10.1088/0031-9155/57/18/5823](http://doi.org/10.1088/0031-9155/57/18/5823): **procedure technical setup**. 73 | 74 | -------------------------------------------------------------------------------- /Screenshots/Slicetracker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/Screenshots/Slicetracker.gif -------------------------------------------------------------------------------- /Screenshots/needle_tracking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/Screenshots/needle_tracking.png -------------------------------------------------------------------------------- /SliceTracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker.png -------------------------------------------------------------------------------- /SliceTracker/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #----------------------------------------------------------------------------- 2 | set(MODULE_NAME SliceTracker) 3 | 4 | file(GLOB_RECURSE MODULE_PYTHON_SCRIPTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} 5 | *.py 6 | ) 7 | 8 | file(GLOB_RECURSE MODULE_PYTHON_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} 9 | ${PROJECT_SOURCE_DIR}/${MODULE_NAME}/Resources/* 10 | ) 11 | 12 | #----------------------------------------------------------------------------- 13 | slicerMacroBuildScriptedModule( 14 | NAME ${MODULE_NAME} 15 | SCRIPTS ${MODULE_PYTHON_SCRIPTS} 16 | RESOURCES ${MODULE_PYTHON_RESOURCES} 17 | ) 18 | 19 | slicerMacroBuildScriptedModule( 20 | NAME SliceTrackerRegistration 21 | SCRIPTS SliceTrackerRegistration.py 22 | RESOURCES "" 23 | ) 24 | 25 | slicerMacroBuildScriptedModule( 26 | NAME SurfaceCutToLabel 27 | SCRIPTS SurfaceCutToLabel.py 28 | RESOURCES ${MODULE_PYTHON_RESOURCES} 29 | ) 30 | 31 | # Get the current working branch 32 | execute_process( 33 | COMMAND git rev-parse --abbrev-ref HEAD 34 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 35 | OUTPUT_VARIABLE GIT_BRANCH 36 | OUTPUT_STRIP_TRAILING_WHITESPACE 37 | ) 38 | 39 | # Get the latest abbreviated commit hash of the working branch 40 | execute_process( 41 | COMMAND git log -1 --format=%h 42 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 43 | OUTPUT_VARIABLE GIT_COMMIT_HASH 44 | OUTPUT_STRIP_TRAILING_WHITESPACE 45 | ) 46 | 47 | execute_process(COMMAND ${GIT_EXECUTABLE} config --get remote.origin.url 48 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 49 | OUTPUT_VARIABLE GIT_WC_URL 50 | OUTPUT_STRIP_TRAILING_WHITESPACE 51 | ) 52 | 53 | configure_file( 54 | ${CMAKE_CURRENT_SOURCE_DIR}/Resources/version.json.in 55 | ${CMAKE_CURRENT_SOURCE_DIR}/Resources/version.json 56 | ) 57 | -------------------------------------------------------------------------------- /SliceTracker/Resources/Colors/mpReviewColors.csv: -------------------------------------------------------------------------------- 1 | Number,Label,R,G,B,A 2 | 0,background,0,0,0,0 3 | 1,WholeGland,51,204,0,1 4 | 2,PeripheralZone,153,90,50,1 5 | 3,TumorROI_PZ_1,255,255,0,1 6 | 4,TumorROI_PZ_2,255,255,160,1 7 | 5,TumorROI_CGTZ_1,250,130,0,1 8 | 6,TumorROI_CGTZ_2,250,180,53,1 9 | 7,BPHROI_1,255,102,102,1 10 | 8,NormalROI_PZ_1,51,204,0,1 11 | 9,NormalROI_CGTZ_1,153,255,102,1 12 | 10,LeftArteryROI,255,0,0,1 13 | 11,RightArteryROI,190,0,0,1 14 | 12,ObturatorMuscleROI,153,102,255,1 15 | 13,CGTZ,99,184,255,1 16 | 14,LeftAntBase,10,10,255,1 17 | 15,LeftAntMid,152,245,255,1 18 | 16,LeftAntApex,0,134,139,1 19 | 17,LeftPostBase,255,153,18,1 20 | 18,LeftPostMid,233,150,122,1 21 | 19,LeftPostApex,255,106,106,1 22 | 20,RightAntBase,255,52,179,1 23 | 21,RightAntMid,205,105,201,1 24 | 22,RightAntApex,137,104,205,1 25 | 23,RightPostBase,154,50,205,1 26 | 24,RightPostMid,240,240,130,1 27 | 25,RightPostApex,255,187,128,1 28 | 26,TumorROI_PZ_3,178,172,35,1 29 | 27,TumorROI_PZ_4,220,200,40,1 30 | 28,TumorROI_CGTZ_3,200,100,0,1 31 | 29,TumorROI_CGTZ_4,200,150,53,1 32 | 30,BPHROI_2,255,102,102,1 33 | 31,NormalROI_PZ_2,0,150,0,1 34 | 32,TumorROI_CGTZ_2,0,100,0,1 35 | 36 | -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/SliceTracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/SliceTracker.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-needle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-needle.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-newImageData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-newImageData.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-quickSegmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-quickSegmentation.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-revealCursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-revealCursor.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-template.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-track.png -------------------------------------------------------------------------------- /SliceTracker/Resources/Icons/icon-zframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/Resources/Icons/icon-zframe.png -------------------------------------------------------------------------------- /SliceTracker/Resources/default.cfg: -------------------------------------------------------------------------------- 1 | [ZFrame Registration] 2 | class: OpenSourceZFrameRegistration 3 | 4 | [Series Descriptions] 5 | PLANNING_IMAGE_PATTERN: ^(.*?)((a|A)(x|X))+(.*?)((t|T)(2))+ 6 | COVER_PROSTATE_PATTERN: COVER PROSTATE 7 | COVER_TEMPLATE_PATTERN: COVER TEMPLATE 8 | NEEDLE_IMAGE_PATTERN: GUIDANCE 9 | VIBE_IMAGE_PATTERN: VIBE 10 | 11 | [Color File] 12 | FileName: mpReviewColors.csv 13 | SegmentedColorName: WholeGland 14 | 15 | [Evaluation] 16 | # possible layouts: LAYOUT_FOUR_UP_QUANTITATIVE, LAYOUT_SIDE_BY_SIDE 17 | Default_Layout: LAYOUT_FOUR_UP_QUANTITATIVE 18 | 19 | [Modes] 20 | Demo_Mode: False 21 | 22 | [Segmentation] 23 | Use_Deep_Learning: True 24 | 25 | [DICOM] 26 | Incoming_Port: 11112 27 | 28 | [General] 29 | CASE_NUMBER_OF_DIGITS: 3 -------------------------------------------------------------------------------- /SliceTracker/Resources/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "GIT_WC_URL": "https://github.com/che85/SliceTracker.git", 3 | "GIT_COMMIT_HASH": "44ec1ee" 4 | } 5 | -------------------------------------------------------------------------------- /SliceTracker/Resources/version.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "GIT_WC_URL": "@GIT_WC_URL@", 3 | "GIT_COMMIT_HASH": "@GIT_COMMIT_HASH@" 4 | } -------------------------------------------------------------------------------- /SliceTracker/Resources/zframe/ProstateTemplate.csv: -------------------------------------------------------------------------------- 1 | "Prostate Biopsy Template" 2 | "A",-7,35.0,25.0,30,35.0,25.0,50,170 3 | "A",-6,30.0,25.0,30,30.0,25.0,50,170 4 | "A",-5,25.0,25.0,30,25.0,25.0,50,170 5 | "A",-4,20.0,25.0,30,20.0,25.0,50,170 6 | "A",-3,15.0,25.0,30,15.0,25.0,50,170 7 | "A",-2,10.0,25.0,30,10.0,25.0,50,170 8 | "A",-1,5.0,25.0,30,5.0,25.0,50,170 9 | "A",0,0.0,25.0,30,0.0,25.0,50,170 10 | "A",1,-5.0,25.0,30,-5.0,25.0,50,170 11 | "A",2,-10.0,25.0,30,-10.0,25.0,50,170 12 | "A",3,-15.0,25.0,30,-15.0,25.0,50,170 13 | "A",4,-20.0,25.0,30,-20.0,25.0,50,170 14 | "A",5,-25.0,25.0,30,-25.0,25.0,50,170 15 | "A",6,-30.0,25.0,30,-30.0,25.0,50,170 16 | "A",7,-35.0,25.0,30,-35.0,25.0,50,170 17 | "B",-7,35.0,20.0,30,35.0,20.0,50,170 18 | "B",-6,30.0,20.0,30,30.0,20.0,50,170 19 | "B",-5,25.0,20.0,30,25.0,20.0,50,170 20 | "B",-4,20.0,20.0,30,20.0,20.0,50,170 21 | "B",-3,15.0,20.0,30,15.0,20.0,50,170 22 | "B",-2,10.0,20.0,30,10.0,20.0,50,170 23 | "B",-1,5.0,20.0,30,5.0,20.0,50,170 24 | "B",0,0.0,20.0,30,0.0,20.0,50,170 25 | "B",1,-5.0,20.0,30,-5.0,20.0,50,170 26 | "B",2,-10.0,20.0,30,-10.0,20.0,50,170 27 | "B",3,-15.0,20.0,30,-15.0,20.0,50,170 28 | "B",4,-20.0,20.0,30,-20.0,20.0,50,170 29 | "B",5,-25.0,20.0,30,-25.0,20.0,50,170 30 | "B",6,-30.0,20.0,30,-30.0,20.0,50,170 31 | "B",7,-35.0,20.0,30,-35.0,20.0,50,170 32 | "C",-7,35.0,15.0,30,35.0,15.0,50,170 33 | "C",-6,30.0,15.0,30,30.0,15.0,50,170 34 | "C",-5,25.0,15.0,30,25.0,15.0,50,170 35 | "C",-4,20.0,15.0,30,20.0,15.0,50,170 36 | "C",-3,15.0,15.0,30,15.0,15.0,50,170 37 | "C",-2,10.0,15.0,30,10.0,15.0,50,170 38 | "C",-1,5.0,15.0,30,5.0,15.0,50,170 39 | "C",0,0.0,15.0,30,0.0,15.0,50,170 40 | "C",1,-5.0,15.0,30,-5.0,15.0,50,170 41 | "C",2,-10.0,15.0,30,-10.0,15.0,50,170 42 | "C",3,-15.0,15.0,30,-15.0,15.0,50,170 43 | "C",4,-20.0,15.0,30,-20.0,15.0,50,170 44 | "C",5,-25.0,15.0,30,-25.0,15.0,50,170 45 | "C",6,-30.0,15.0,30,-30.0,15.0,50,170 46 | "C",7,-35.0,15.0,30,-35.0,15.0,50,170 47 | "D",-7,35.0,10.0,30,35.0,10.0,50,170 48 | "D",-6,30.0,10.0,30,30.0,10.0,50,170 49 | "D",-5,25.0,10.0,30,25.0,10.0,50,170 50 | "D",-4,20.0,10.0,30,20.0,10.0,50,170 51 | "D",-3,15.0,10.0,30,15.0,10.0,50,170 52 | "D",-2,10.0,10.0,30,10.0,10.0,50,170 53 | "D",-1,5.0,10.0,30,5.0,10.0,50,170 54 | "D",0,0.0,10.0,30,0.0,10.0,50,170 55 | "D",1,-5.0,10.0,30,-5.0,10.0,50,170 56 | "D",2,-10.0,10.0,30,-10.0,10.0,50,170 57 | "D",3,-15.0,10.0,30,-15.0,10.0,50,170 58 | "D",4,-20.0,10.0,30,-20.0,10.0,50,170 59 | "D",5,-25.0,10.0,30,-25.0,10.0,50,170 60 | "D",6,-30.0,10.0,30,-30.0,10.0,50,170 61 | "D",7,-35.0,10.0,30,-35.0,10.0,50,170 62 | "E",-7,35.0,5.0,30,35.0,5.0,50,170 63 | "E",-6,30.0,5.0,30,30.0,5.0,50,170 64 | "E",-5,25.0,5.0,30,25.0,5.0,50,170 65 | "E",-4,20.0,5.0,30,20.0,5.0,50,170 66 | "E",-3,15.0,5.0,30,15.0,5.0,50,170 67 | "E",-2,10.0,5.0,30,10.0,5.0,50,170 68 | "E",-1,5.0,5.0,30,5.0,5.0,50,170 69 | "E",0,0.0,5.0,30,0.0,5.0,50,170 70 | "E",1,-5.0,5.0,30,-5.0,5.0,50,170 71 | "E",2,-10.0,5.0,30,-10.0,5.0,50,170 72 | "E",3,-15.0,5.0,30,-15.0,5.0,50,170 73 | "E",4,-20.0,5.0,30,-20.0,5.0,50,170 74 | "E",5,-25.0,5.0,30,-25.0,5.0,50,170 75 | "E",6,-30.0,5.0,30,-30.0,5.0,50,170 76 | "E",7,-35.0,5.0,30,-35.0,5.0,50,170 77 | "F",-7,35.0,0.0,30,35.0,0.0,50,170 78 | "F",-6,30.0,0.0,30,30.0,0.0,50,170 79 | "F",-5,25.0,0.0,30,25.0,0.0,50,170 80 | "F",-4,20.0,0.0,30,20.0,0.0,50,170 81 | "F",-3,15.0,0.0,30,15.0,0.0,50,170 82 | "F",-2,10.0,0.0,30,10.0,0.0,50,170 83 | "F",-1,5.0,0.0,30,5.0,0.0,50,170 84 | "F",0,0.0,0.0,30,0.0,0.0,50,170 85 | "F",1,-5.0,0.0,30,-5.0,0.0,50,170 86 | "F",2,-10.0,0.0,30,-10.0,0.0,50,170 87 | "F",3,-15.0,0.0,30,-15.0,0.0,50,170 88 | "F",4,-20.0,0.0,30,-20.0,0.0,50,170 89 | "F",5,-25.0,0.0,30,-25.0,0.0,50,170 90 | "F",6,-30.0,0.0,30,-30.0,0.0,50,170 91 | "F",7,-35.0,0.0,30,-35.0,0.0,50,170 92 | "G",-7,35.0,-5.0,30,35.0,-5.0,50,170 93 | "G",-6,30.0,-5.0,30,30.0,-5.0,50,170 94 | "G",-5,25.0,-5.0,30,25.0,-5.0,50,170 95 | "G",-4,20.0,-5.0,30,20.0,-5.0,50,170 96 | "G",-3,15.0,-5.0,30,15.0,-5.0,50,170 97 | "G",-2,10.0,-5.0,30,10.0,-5.0,50,170 98 | "G",-1,5.0,-5.0,30,5.0,-5.0,50,170 99 | "G",0,0.0,-5.0,30,0.0,-5.0,50,170 100 | "G",1,-5.0,-5.0,30,-5.0,-5.0,50,170 101 | "G",2,-10.0,-5.0,30,-10.0,-5.0,50,170 102 | "G",3,-15.0,-5.0,30,-15.0,-5.0,50,170 103 | "G",4,-20.0,-5.0,30,-20.0,-5.0,50,170 104 | "G",5,-25.0,-5.0,30,-25.0,-5.0,50,170 105 | "G",6,-30.0,-5.0,30,-30.0,-5.0,50,170 106 | "G",7,-35.0,-5.0,30,-35.0,-5.0,50,170 107 | "H",-7,35.0,-10.0,30,35.0,-10.0,50,170 108 | "H",-6,30.0,-10.0,30,30.0,-10.0,50,170 109 | "H",-5,25.0,-10.0,30,25.0,-10.0,50,170 110 | "H",-4,20.0,-10.0,30,20.0,-10.0,50,170 111 | "H",-3,15.0,-10.0,30,15.0,-10.0,50,170 112 | "H",-2,10.0,-10.0,30,10.0,-10.0,50,170 113 | "H",-1,5.0,-10.0,30,5.0,-10.0,50,170 114 | "H",0,0.0,-10.0,30,0.0,-10.0,50,170 115 | "H",1,-5.0,-10.0,30,-5.0,-10.0,50,170 116 | "H",2,-10.0,-10.0,30,-10.0,-10.0,50,170 117 | "H",3,-15.0,-10.0,30,-15.0,-10.0,50,170 118 | "H",4,-20.0,-10.0,30,-20.0,-10.0,50,170 119 | "H",5,-25.0,-10.0,30,-25.0,-10.0,50,170 120 | "H",6,-30.0,-10.0,30,-30.0,-10.0,50,170 121 | "H",7,-35.0,-10.0,30,-35.0,-10.0,50,170 122 | "I",-7,35.0,-15.0,30,35.0,-15.0,50,170 123 | "I",-6,30.0,-15.0,30,30.0,-15.0,50,170 124 | "I",-5,25.0,-15.0,30,25.0,-15.0,50,170 125 | "I",-4,20.0,-15.0,30,20.0,-15.0,50,170 126 | "I",-3,15.0,-15.0,30,15.0,-15.0,50,170 127 | "I",-2,10.0,-15.0,30,10.0,-15.0,50,170 128 | "I",-1,5.0,-15.0,30,5.0,-15.0,50,170 129 | "I",0,0.0,-15.0,30,0.0,-15.0,50,170 130 | "I",1,-5.0,-15.0,30,-5.0,-15.0,50,170 131 | "I",2,-10.0,-15.0,30,-10.0,-15.0,50,170 132 | "I",3,-15.0,-15.0,30,-15.0,-15.0,50,170 133 | "I",4,-20.0,-15.0,30,-20.0,-15.0,50,170 134 | "I",5,-25.0,-15.0,30,-25.0,-15.0,50,170 135 | "I",6,-30.0,-15.0,30,-30.0,-15.0,50,170 136 | "I",7,-35.0,-15.0,30,-35.0,-15.0,50,170 137 | "J",-7,35.0,-20.0,30,35.0,-20.0,50,170 138 | "J",-6,30.0,-20.0,30,30.0,-20.0,50,170 139 | "J",-5,25.0,-20.0,30,25.0,-20.0,50,170 140 | "J",-4,20.0,-20.0,30,20.0,-20.0,50,170 141 | "J",-3,15.0,-20.0,30,15.0,-20.0,50,170 142 | "J",-2,10.0,-20.0,30,10.0,-20.0,50,170 143 | "J",-1,5.0,-20.0,30,5.0,-20.0,50,170 144 | "J",0,0.0,-20.0,30,0.0,-20.0,50,170 145 | "J",1,-5.0,-20.0,30,-5.0,-20.0,50,170 146 | "J",2,-10.0,-20.0,30,-10.0,-20.0,50,170 147 | "J",3,-15.0,-20.0,30,-15.0,-20.0,50,170 148 | "J",4,-20.0,-20.0,30,-20.0,-20.0,50,170 149 | "J",5,-25.0,-20.0,30,-25.0,-20.0,50,170 150 | "J",6,-30.0,-20.0,30,-30.0,-20.0,50,170 151 | "J",7,-35.0,-20.0,30,-35.0,-20.0,50,170 152 | "K",-7,35.0,-25.0,30,35.0,-25.0,50,170 153 | "K",-6,30.0,-25.0,30,30.0,-25.0,50,170 154 | "K",-5,25.0,-25.0,30,25.0,-25.0,50,170 155 | "K",-4,20.0,-25.0,30,20.0,-25.0,50,170 156 | "K",-3,15.0,-25.0,30,15.0,-25.0,50,170 157 | "K",-2,10.0,-25.0,30,10.0,-25.0,50,170 158 | "K",-1,5.0,-25.0,30,5.0,-25.0,50,170 159 | "K",0,0.0,-25.0,30,0.0,-25.0,50,170 160 | "K",1,-5.0,-25.0,30,-5.0,-25.0,50,170 161 | "K",2,-10.0,-25.0,30,-10.0,-25.0,50,170 162 | "K",3,-15.0,-25.0,30,-15.0,-25.0,50,170 163 | "K",4,-20.0,-25.0,30,-20.0,-25.0,50,170 164 | "K",5,-25.0,-25.0,30,-25.0,-25.0,50,170 165 | "K",6,-30.0,-25.0,30,-30.0,-25.0,50,170 166 | "K",7,-35.0,-25.0,30,-35.0,-25.0,50,170 167 | "L",-7,35.0,-30.0,30,35.0,-30.0,50,170 168 | "L",-6,30.0,-30.0,30,30.0,-30.0,50,170 169 | "L",-5,25.0,-30.0,30,25.0,-30.0,50,170 170 | "L",-4,20.0,-30.0,30,20.0,-30.0,50,170 171 | "L",-3,15.0,-30.0,30,15.0,-30.0,50,170 172 | "L",-2,10.0,-30.0,30,10.0,-30.0,50,170 173 | "L",-1,5.0,-30.0,30,5.0,-30.0,50,170 174 | "L",0,0.0,-30.0,30,0.0,-30.0,50,170 175 | "L",1,-5.0,-30.0,30,-5.0,-30.0,50,170 176 | "L",2,-10.0,-30.0,30,-10.0,-30.0,50,170 177 | "L",3,-15.0,-30.0,30,-15.0,-30.0,50,170 178 | "L",4,-20.0,-30.0,30,-20.0,-30.0,50,170 179 | "L",5,-25.0,-30.0,30,-25.0,-30.0,50,170 180 | "L",6,-30.0,-30.0,30,-30.0,-30.0,50,170 181 | "L",7,-35.0,-30.0,30,-35.0,-30.0,50,170 182 | "M",-7,35.0,-35.0,30,35.0,-35.0,50,170 183 | "M",-6,30.0,-35.0,30,30.0,-35.0,50,170 184 | "M",-5,25.0,-35.0,30,25.0,-35.0,50,170 185 | "M",-4,20.0,-35.0,30,20.0,-35.0,50,170 186 | "M",-3,15.0,-35.0,30,15.0,-35.0,50,170 187 | "M",-2,10.0,-35.0,30,10.0,-35.0,50,170 188 | "M",-1,5.0,-35.0,30,5.0,-35.0,50,170 189 | "M",0,0.0,-35.0,30,0.0,-35.0,50,170 190 | "M",1,-5.0,-35.0,30,-5.0,-35.0,50,170 191 | "M",2,-10.0,-35.0,30,-10.0,-35.0,50,170 192 | "M",3,-15.0,-35.0,30,-15.0,-35.0,50,170 193 | "M",4,-20.0,-35.0,30,-20.0,-35.0,50,170 194 | "M",5,-25.0,-35.0,30,-25.0,-35.0,50,170 195 | "M",6,-30.0,-35.0,30,-30.0,-35.0,50,170 196 | "M",7,-35.0,-35.0,30,-35.0,-35.0,50,170 197 | "N",-7,35.0,-40.0,30,35.0,-40.0,50,170 198 | "N",-6,30.0,-40.0,30,30.0,-40.0,50,170 199 | "N",-5,25.0,-40.0,30,25.0,-40.0,50,170 200 | "N",-4,20.0,-40.0,30,20.0,-40.0,50,170 201 | "N",-3,15.0,-40.0,30,15.0,-40.0,50,170 202 | "N",-2,10.0,-40.0,30,10.0,-40.0,50,170 203 | "N",-1,5.0,-40.0,30,5.0,-40.0,50,170 204 | "N",0,0.0,-40.0,30,0.0,-40.0,50,170 205 | "N",1,-5.0,-40.0,30,-5.0,-40.0,50,170 206 | "N",2,-10.0,-40.0,30,-10.0,-40.0,50,170 207 | "N",3,-15.0,-40.0,30,-15.0,-40.0,50,170 208 | "N",4,-20.0,-40.0,30,-20.0,-40.0,50,170 209 | "N",5,-25.0,-40.0,30,-25.0,-40.0,50,170 210 | "N",6,-30.0,-40.0,30,-30.0,-40.0,50,170 211 | "N",7,-35.0,-40.0,30,-35.0,-40.0,50,170 212 | -------------------------------------------------------------------------------- /SliceTracker/Resources/zframe/zframe-config.csv: -------------------------------------------------------------------------------- 1 | 30.0,30.0,0.0,0.0,0.0,0.0 2 | 30.0,0.0,0.0,0.0,-0.70710678118655,0.70710678118655 3 | 30.0,-30.0,0.0,0.0,0.0,1.0 4 | 0.0,-30.0,0.0,-0.70710678118655,0.0,0.70710678118655 5 | -30.0,-30.0,0.0,0.0,0.0,1.0 6 | -30.0,0.0,0.0,0.0,0.70710678118655,0.70710678118655 7 | -30.0,30.0,0.0,0.0,0.0,1.0 8 | 9 | -------------------------------------------------------------------------------- /SliceTracker/Resources/zframe/zframe-model.vtk: -------------------------------------------------------------------------------- 1 | # vtk DataFile Version 3.0 2 | vtk output 3 | ASCII 4 | DATASET POLYDATA 5 | POINTS 168 float 6 | 31.5 30 30 31.5 30 -30 30.75 31.299 30 7 | 30.75 31.299 -30 29.25 31.299 30 29.25 31.299 -30 8 | 28.5 30 30 28.5 30 -30 29.25 28.701 30 9 | 29.25 28.701 -30 30.75 28.701 30 30.75 28.701 -30 10 | 31.5 30 30 30.75 31.299 30 29.25 31.299 30 11 | 28.5 30 30 29.25 28.701 30 30.75 28.701 30 12 | 30.75 28.701 -30 29.25 28.701 -30 28.5 30 -30 13 | 29.25 31.299 -30 30.75 31.299 -30 31.5 30 -30 14 | 31.5 -30 30 31.5 30 -30 30.75 -29.0814 30.9186 15 | 30.75 30.9186 -29.0814 29.25 -29.0814 30.9186 29.25 30.9186 -29.0814 16 | 28.5 -30 30 28.5 30 -30 29.25 -30.9186 29.0814 17 | 29.25 29.0814 -30.9186 30.75 -30.9186 29.0814 30.75 29.0814 -30.9186 18 | 31.5 -30 30 30.75 -29.0814 30.9186 29.25 -29.0814 30.9186 19 | 28.5 -30 30 29.25 -30.9186 29.0814 30.75 -30.9186 29.0814 20 | 30.75 29.0814 -30.9186 29.25 29.0814 -30.9186 28.5 30 -30 21 | 29.25 30.9186 -29.0814 30.75 30.9186 -29.0814 31.5 30 -30 22 | 31.5 -30 30 31.5 -30 -30 30.75 -28.701 30 23 | 30.75 -28.701 -30 29.25 -28.701 30 29.25 -28.701 -30 24 | 28.5 -30 30 28.5 -30 -30 29.25 -31.299 30 25 | 29.25 -31.299 -30 30.75 -31.299 30 30.75 -31.299 -30 26 | 31.5 -30 30 30.75 -28.701 30 29.25 -28.701 30 27 | 28.5 -30 30 29.25 -31.299 30 30.75 -31.299 30 28 | 30.75 -31.299 -30 29.25 -31.299 -30 28.5 -30 -30 29 | 29.25 -28.701 -30 30.75 -28.701 -30 31.5 -30 -30 30 | 31.0607 30 28.9393 -28.9393 30 -31.0607 30.5303 31.299 29.4697 31 | -29.4697 31.299 -30.5303 29.4697 31.299 30.5303 -30.5303 31.299 -29.4697 32 | 28.9393 30 31.0607 -31.0607 30 -28.9393 29.4697 28.701 30.5303 33 | -30.5303 28.701 -29.4697 30.5303 28.701 29.4697 -29.4697 28.701 -30.5303 34 | 31.0607 30 28.9393 30.5303 31.299 29.4697 29.4697 31.299 30.5303 35 | 28.9393 30 31.0607 29.4697 28.701 30.5303 30.5303 28.701 29.4697 36 | -29.4697 28.701 -30.5303 -30.5303 28.701 -29.4697 -31.0607 30 -28.9393 37 | -30.5303 31.299 -29.4697 -29.4697 31.299 -30.5303 -28.9393 30 -31.0607 38 | -28.5 -30 30 -28.5 -30 -30 -29.25 -28.701 30 39 | -29.25 -28.701 -30 -30.75 -28.701 30 -30.75 -28.701 -30 40 | -31.5 -30 30 -31.5 -30 -30 -30.75 -31.299 30 41 | -30.75 -31.299 -30 -29.25 -31.299 30 -29.25 -31.299 -30 42 | -28.5 -30 30 -29.25 -28.701 30 -30.75 -28.701 30 43 | -31.5 -30 30 -30.75 -31.299 30 -29.25 -31.299 30 44 | -29.25 -31.299 -30 -30.75 -31.299 -30 -31.5 -30 -30 45 | -30.75 -28.701 -30 -29.25 -28.701 -30 -28.5 -30 -30 46 | -28.5 30 30 -28.5 -30 -30 -29.25 30.9186 29.0814 47 | -29.25 -29.0814 -30.9186 -30.75 30.9186 29.0814 -30.75 -29.0814 -30.9186 48 | -31.5 30 30 -31.5 -30 -30 -30.75 29.0814 30.9186 49 | -30.75 -30.9186 -29.0814 -29.25 29.0814 30.9186 -29.25 -30.9186 -29.0814 50 | -28.5 30 30 -29.25 30.9186 29.0814 -30.75 30.9186 29.0814 51 | -31.5 30 30 -30.75 29.0814 30.9186 -29.25 29.0814 30.9186 52 | -29.25 -30.9186 -29.0814 -30.75 -30.9186 -29.0814 -31.5 -30 -30 53 | -30.75 -29.0814 -30.9186 -29.25 -29.0814 -30.9186 -28.5 -30 -30 54 | -28.5 30 30 -28.5 30 -30 -29.25 31.299 30 55 | -29.25 31.299 -30 -30.75 31.299 30 -30.75 31.299 -30 56 | -31.5 30 30 -31.5 30 -30 -30.75 28.701 30 57 | -30.75 28.701 -30 -29.25 28.701 30 -29.25 28.701 -30 58 | -28.5 30 30 -29.25 31.299 30 -30.75 31.299 30 59 | -31.5 30 30 -30.75 28.701 30 -29.25 28.701 30 60 | -29.25 28.701 -30 -30.75 28.701 -30 -31.5 30 -30 61 | -30.75 31.299 -30 -29.25 31.299 -30 -28.5 30 -30 62 | 63 | POLYGONS 56 308 64 | 4 0 1 3 2 65 | 4 2 3 5 4 66 | 4 4 5 7 6 67 | 4 6 7 9 8 68 | 4 8 9 11 10 69 | 4 10 11 1 0 70 | 6 12 13 14 15 16 17 71 | 6 18 19 20 21 22 23 72 | 4 24 25 27 26 73 | 4 26 27 29 28 74 | 4 28 29 31 30 75 | 4 30 31 33 32 76 | 4 32 33 35 34 77 | 4 34 35 25 24 78 | 6 36 37 38 39 40 41 79 | 6 42 43 44 45 46 47 80 | 4 48 49 51 50 81 | 4 50 51 53 52 82 | 4 52 53 55 54 83 | 4 54 55 57 56 84 | 4 56 57 59 58 85 | 4 58 59 49 48 86 | 6 60 61 62 63 64 65 87 | 6 66 67 68 69 70 71 88 | 4 72 73 75 74 89 | 4 74 75 77 76 90 | 4 76 77 79 78 91 | 4 78 79 81 80 92 | 4 80 81 83 82 93 | 4 82 83 73 72 94 | 6 84 85 86 87 88 89 95 | 6 90 91 92 93 94 95 96 | 4 96 97 99 98 97 | 4 98 99 101 100 98 | 4 100 101 103 102 99 | 4 102 103 105 104 100 | 4 104 105 107 106 101 | 4 106 107 97 96 102 | 6 108 109 110 111 112 113 103 | 6 114 115 116 117 118 119 104 | 4 120 121 123 122 105 | 4 122 123 125 124 106 | 4 124 125 127 126 107 | 4 126 127 129 128 108 | 4 128 129 131 130 109 | 4 130 131 121 120 110 | 6 132 133 134 135 136 137 111 | 6 138 139 140 141 142 143 112 | 4 144 145 147 146 113 | 4 146 147 149 148 114 | 4 148 149 151 150 115 | 4 150 151 153 152 116 | 4 152 153 155 154 117 | 4 154 155 145 144 118 | 6 156 157 158 159 160 161 119 | 6 162 163 164 165 166 167 120 | 121 | POINT_DATA 168 122 | NORMALS Normals float 123 | 1 0 0 1 0 0 0.5 0.866025 -1.92296e-16 124 | 0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 125 | -1 -4.10207e-10 9.10842e-26 -1 -4.10207e-10 9.10842e-26 -0.5 -0.866025 1.92296e-16 126 | -0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 127 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 128 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 129 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 130 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 131 | 1 0 0 1 0 0 0.5 0.612372 0.612372 132 | 0.5 0.612372 0.612372 -0.5 0.612372 0.612372 -0.5 0.612372 0.612372 133 | -1 -2.9006e-10 -2.9006e-10 -1 -2.9006e-10 -2.9006e-10 -0.5 -0.612372 -0.612372 134 | -0.5 -0.612372 -0.612372 0.5 -0.612372 -0.612372 0.5 -0.612372 -0.612372 135 | 0 -0.707107 0.707107 0 -0.707107 0.707107 0 -0.707107 0.707107 136 | 0 -0.707107 0.707107 0 -0.707107 0.707107 0 -0.707107 0.707107 137 | 0 0.707107 -0.707107 0 0.707107 -0.707107 0 0.707107 -0.707107 138 | 0 0.707107 -0.707107 0 0.707107 -0.707107 0 0.707107 -0.707107 139 | 1 0 0 1 0 0 0.5 0.866025 -1.92296e-16 140 | 0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 141 | -1 -4.10207e-10 9.10842e-26 -1 -4.10207e-10 9.10842e-26 -0.5 -0.866025 1.92296e-16 142 | -0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 143 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 144 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 145 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 146 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 147 | 0.707107 -1.57009e-16 -0.707107 0.707107 -1.57009e-16 -0.707107 0.353553 0.866025 -0.353553 148 | 0.353553 0.866025 -0.353553 -0.353553 0.866025 0.353553 -0.353553 0.866025 0.353553 149 | -0.707107 -4.10207e-10 0.707107 -0.707107 -4.10207e-10 0.707107 -0.353553 -0.866025 0.353553 150 | -0.353553 -0.866025 0.353553 0.353553 -0.866025 -0.353553 0.353553 -0.866025 -0.353553 151 | 0.707107 1.57009e-16 0.707107 0.707107 1.57009e-16 0.707107 0.707107 1.57009e-16 0.707107 152 | 0.707107 1.57009e-16 0.707107 0.707107 1.57009e-16 0.707107 0.707107 1.57009e-16 0.707107 153 | -0.707107 -1.57009e-16 -0.707107 -0.707107 -1.57009e-16 -0.707107 -0.707107 -1.57009e-16 -0.707107 154 | -0.707107 -1.57009e-16 -0.707107 -0.707107 -1.57009e-16 -0.707107 -0.707107 -1.57009e-16 -0.707107 155 | 1 0 0 1 0 0 0.5 0.866025 -1.92296e-16 156 | 0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 157 | -1 -4.10207e-10 9.10842e-26 -1 -4.10207e-10 9.10842e-26 -0.5 -0.866025 1.92296e-16 158 | -0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 159 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 160 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 161 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 162 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 163 | 1 0 0 1 0 0 0.5 0.612372 -0.612372 164 | 0.5 0.612372 -0.612372 -0.5 0.612372 -0.612372 -0.5 0.612372 -0.612372 165 | -1 -2.9006e-10 2.9006e-10 -1 -2.9006e-10 2.9006e-10 -0.5 -0.612372 0.612372 166 | -0.5 -0.612372 0.612372 0.5 -0.612372 0.612372 0.5 -0.612372 0.612372 167 | 0 0.707107 0.707107 0 0.707107 0.707107 0 0.707107 0.707107 168 | 0 0.707107 0.707107 0 0.707107 0.707107 0 0.707107 0.707107 169 | 0 -0.707107 -0.707107 0 -0.707107 -0.707107 0 -0.707107 -0.707107 170 | 0 -0.707107 -0.707107 0 -0.707107 -0.707107 0 -0.707107 -0.707107 171 | 1 0 0 1 0 0 0.5 0.866025 -1.92296e-16 172 | 0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 -0.5 0.866025 -1.92296e-16 173 | -1 -4.10207e-10 9.10842e-26 -1 -4.10207e-10 9.10842e-26 -0.5 -0.866025 1.92296e-16 174 | -0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 0.5 -0.866025 1.92296e-16 175 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 176 | 0 2.22045e-16 1 0 2.22045e-16 1 0 2.22045e-16 1 177 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 178 | 0 -2.22045e-16 -1 0 -2.22045e-16 -1 0 -2.22045e-16 -1 179 | 180 | TEXTURE_COORDINATES TCoords 2 float 181 | 1 0 1 1 0.666667 0 0.666667 1 0.333333 182 | 0 0.333333 1 0 0 0 1 0.333333 0 183 | 0.333333 1 0.666667 0 0.666667 1 1.5 -0 0.75 184 | -1.29904 -0.75 -1.29904 -1.5 6.1531e-10 -0.75 1.29904 0.75 1.29904 185 | 0.75 1.29904 -0.75 1.29904 -1.5 6.1531e-10 -0.75 -1.29904 0.75 186 | -1.29904 1.5 -0 1 0 1 1 0.666667 0 187 | 0.666667 1 0.333333 0 0.333333 1 0 0 0 188 | 1 0.333333 0 0.333333 1 0.666667 0 0.666667 1 189 | 1.5 -0 0.75 -1.29904 -0.75 -1.29904 -1.5 6.1531e-10 -0.75 190 | 1.29904 0.75 1.29904 0.75 1.29904 -0.75 1.29904 -1.5 6.1531e-10 191 | -0.75 -1.29904 0.75 -1.29904 1.5 -0 1 0 1 192 | 1 0.666667 0 0.666667 1 0.333333 0 0.333333 1 193 | 0 0 0 1 0.333333 0 0.333333 1 0.666667 194 | 0 0.666667 1 1.5 -0 0.75 -1.29904 -0.75 -1.29904 195 | -1.5 6.1531e-10 -0.75 1.29904 0.75 1.29904 0.75 1.29904 -0.75 196 | 1.29904 -1.5 6.1531e-10 -0.75 -1.29904 0.75 -1.29904 1.5 -0 197 | 1 0 1 1 0.666667 0 0.666667 1 0.333333 198 | 0 0.333333 1 0 0 0 1 0.333333 0 199 | 0.333333 1 0.666667 0 0.666667 1 1.5 -0 0.75 200 | -1.29904 -0.75 -1.29904 -1.5 6.1531e-10 -0.75 1.29904 0.75 1.29904 201 | 0.75 1.29904 -0.75 1.29904 -1.5 6.1531e-10 -0.75 -1.29904 0.75 202 | -1.29904 1.5 -0 1 0 1 1 0.666667 0 203 | 0.666667 1 0.333333 0 0.333333 1 0 0 0 204 | 1 0.333333 0 0.333333 1 0.666667 0 0.666667 1 205 | 1.5 -0 0.75 -1.29904 -0.75 -1.29904 -1.5 6.1531e-10 -0.75 206 | 1.29904 0.75 1.29904 0.75 1.29904 -0.75 1.29904 -1.5 6.1531e-10 207 | -0.75 -1.29904 0.75 -1.29904 1.5 -0 1 0 1 208 | 1 0.666667 0 0.666667 1 0.333333 0 0.333333 1 209 | 0 0 0 1 0.333333 0 0.333333 1 0.666667 210 | 0 0.666667 1 1.5 -0 0.75 -1.29904 -0.75 -1.29904 211 | -1.5 6.1531e-10 -0.75 1.29904 0.75 1.29904 0.75 1.29904 -0.75 212 | 1.29904 -1.5 6.1531e-10 -0.75 -1.29904 0.75 -1.29904 1.5 -0 213 | 1 0 1 1 0.666667 0 0.666667 1 0.333333 214 | 0 0.333333 1 0 0 0 1 0.333333 0 215 | 0.333333 1 0.666667 0 0.666667 1 1.5 -0 0.75 216 | -1.29904 -0.75 -1.29904 -1.5 6.1531e-10 -0.75 1.29904 0.75 1.29904 217 | 0.75 1.29904 -0.75 1.29904 -1.5 6.1531e-10 -0.75 -1.29904 0.75 218 | -1.29904 1.5 -0 219 | -------------------------------------------------------------------------------- /SliceTracker/SliceTracker.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import logging 4 | from slicer.ScriptedLoadableModule import * 5 | 6 | from SliceTrackerUtils.configuration import SliceTrackerConfiguration 7 | from SliceTrackerUtils.constants import SliceTrackerConstants 8 | from SliceTrackerUtils.session import SliceTrackerSession 9 | 10 | from SliceTrackerUtils.steps.base import SliceTrackerStep 11 | from SliceTrackerUtils.steps.overview import SliceTrackerOverviewStep 12 | from SliceTrackerUtils.steps.zFrameRegistration import SliceTrackerZFrameRegistrationStep 13 | from SliceTrackerUtils.steps.evaluation import SliceTrackerEvaluationStep 14 | from SliceTrackerUtils.steps.segmentation import SliceTrackerSegmentationStep 15 | 16 | from SlicerDevelopmentToolboxUtils.buttons import * 17 | from SlicerDevelopmentToolboxUtils.events import SlicerDevelopmentToolboxEvents 18 | from SlicerDevelopmentToolboxUtils.constants import DICOMTAGS 19 | from SlicerDevelopmentToolboxUtils.decorators import logmethod 20 | from SlicerDevelopmentToolboxUtils.helpers import WatchBoxAttribute 21 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin, ModuleLogicMixin 22 | from SlicerDevelopmentToolboxUtils.widgets import CustomStatusProgressbar, DICOMBasedInformationWatchBox 23 | 24 | 25 | class SliceTracker(ScriptedLoadableModule): 26 | 27 | def __init__(self, parent): 28 | ScriptedLoadableModule.__init__(self, parent) 29 | self.parent.title = "SliceTracker" 30 | self.parent.categories = ["Radiology"] 31 | self.parent.dependencies = ["SlicerDevelopmentToolbox", "mpReview", "mpReviewPreprocessor", 32 | "SegmentEditorSurfaceCut", "DeepInfer", "ZFrameRegistration"] 33 | self.parent.contributors = ["Christian Herz (SPL)", "Peter Behringer (SPL)", 34 | "Kyle MacNeil (Med-i Lab, Queen's; SPL)", "Andriy Fedorov (SPL)"] 35 | 36 | self.parent.helpText = """ SliceTracker facilitates support of MRI-guided targeted prostate biopsy. 37 | See the documentation for 38 | details.""" 39 | self.parent.acknowledgementText = """Surgical Planning Laboratory, Brigham and Women's Hospital, Harvard 40 | Medical School, Boston, USA This work was supported in part by the National 41 | Institutes of Health through grants U24 CA180918, 42 | R01 CA111288 and P41 EB015898.""" 43 | 44 | 45 | class SliceTrackerWidget(ModuleWidgetMixin, SliceTrackerConstants, ScriptedLoadableModuleWidget): 46 | 47 | def __init__(self, parent=None): 48 | ScriptedLoadableModuleWidget.__init__(self, parent) 49 | self.modulePath = os.path.dirname(slicer.util.modulePath(self.moduleName)) 50 | SliceTrackerConfiguration(self.moduleName, os.path.join(self.modulePath, 'Resources', "default.cfg")) 51 | self.logic = SliceTrackerLogic() 52 | 53 | self.session = SliceTrackerSession() 54 | self.session.steps = [] 55 | self.session.removeEventObservers() 56 | self.session.addEventObserver(self.session.CloseCaseEvent, lambda caller, event: self.cleanup()) 57 | self.session.addEventObserver(SlicerDevelopmentToolboxEvents.NewFileIndexedEvent, self.onNewFileIndexed) 58 | self.demoMode = str(self.getSetting("Demo_Mode", moduleName=self.moduleName)).lower() == 'true' 59 | 60 | def enter(self): 61 | if not slicer.dicomDatabase: 62 | slicer.util.errorDisplay("Slicer DICOMDatabase was not found. In order to be able to use SliceTracker, you will " 63 | "need to set a proper location for the Slicer DICOMDatabase.") 64 | self.layout.parent().enabled = slicer.dicomDatabase is not None 65 | 66 | def exit(self): 67 | pass 68 | 69 | def onReload(self): 70 | ScriptedLoadableModuleWidget.onReload(self) 71 | 72 | @logmethod(logging.DEBUG) 73 | def cleanup(self): 74 | ScriptedLoadableModuleWidget.cleanup(self) 75 | self.patientWatchBox.sourceFile = None 76 | self.preopWatchBox.sourceFile = None 77 | self.intraopWatchBox.sourceFile = None 78 | 79 | def setup(self): 80 | ScriptedLoadableModuleWidget.setup(self) 81 | 82 | for step in [SliceTrackerOverviewStep, SliceTrackerZFrameRegistrationStep, 83 | SliceTrackerSegmentationStep, SliceTrackerEvaluationStep]: 84 | self.session.registerStep(step()) 85 | 86 | self.customStatusProgressBar = CustomStatusProgressbar() 87 | self.setupPatientWatchBox() 88 | self.setupViewSettingGroupBox() 89 | self.setupTabBarNavigation() 90 | self.setupConnections() 91 | self.setupSessionObservers() 92 | self.layout.addStretch(1) 93 | 94 | def setupPatientWatchBox(self): 95 | WatchBoxAttribute.TRUNCATE_LENGTH = 20 96 | patientWatchBoxInformation = [WatchBoxAttribute('PatientName', "Patient's Name: ", DICOMTAGS.PATIENT_NAME, 97 | masked=self.demoMode), 98 | WatchBoxAttribute('PatientID', 'Patient ID: ', DICOMTAGS.PATIENT_ID, 99 | masked=self.demoMode), 100 | WatchBoxAttribute('DOB', "Patient's Birth Date: ", DICOMTAGS.PATIENT_BIRTH_DATE, 101 | masked=self.demoMode)] 102 | self.patientWatchBox = DICOMBasedInformationWatchBox(patientWatchBoxInformation, title="Patient Information", 103 | columns=2) 104 | self.layout.addWidget(self.patientWatchBox) 105 | 106 | preopWatchBoxInformation = [WatchBoxAttribute('StudyDate', 'Study Date: ', DICOMTAGS.STUDY_DATE)] 107 | self.preopWatchBox = DICOMBasedInformationWatchBox(preopWatchBoxInformation, title="Preop Information", columns=2) 108 | self.layout.addWidget(self.preopWatchBox) 109 | 110 | intraopWatchBoxInformation = [WatchBoxAttribute('CurrentSeries', 'Current Series: ', [DICOMTAGS.SERIES_NUMBER, 111 | DICOMTAGS.SERIES_DESCRIPTION]), 112 | WatchBoxAttribute('StudyDate', 'Study Date: ', DICOMTAGS.STUDY_DATE)] 113 | self.intraopWatchBox = DICOMBasedInformationWatchBox(intraopWatchBoxInformation, title="Intraop Information", 114 | columns=2) 115 | self.registrationDetailsButton = self.createButton("", icon=Icons.settings, styleSheet="border:none;", 116 | maximumWidth=16) 117 | self.layout.addWidget(self.intraopWatchBox) 118 | 119 | def setupViewSettingGroupBox(self): 120 | iconSize = qt.QSize(24, 24) 121 | self.infoButton = self.createButton("", icon=Icons.info, iconSize=iconSize, checkable=True, 122 | toolTip="Display Patient/Study Information", checked=True) 123 | self.redOnlyLayoutButton = RedSliceLayoutButton() 124 | self.sideBySideLayoutButton = SideBySideLayoutButton() 125 | self.fourUpLayoutButton = FourUpLayoutButton() 126 | self.fourUpPlotLayoutButton = FourUpPlotViewLayoutButton() 127 | self.layoutButtons = [self.redOnlyLayoutButton, self.sideBySideLayoutButton, self.fourUpLayoutButton, self.fourUpPlotLayoutButton] 128 | self.crosshairButton = CrosshairButton() 129 | self.settingsButton = ModuleSettingsButton(self.moduleName) 130 | self.dicomConnectionTestButton = DICOMConnectionTestButton() 131 | self.dicomConnectionTestButton.setToolTip("Test DICOM connection") 132 | self.showAnnotationsButton = self.createButton("", icon=Icons.text_info, iconSize=iconSize, checkable=True, 133 | toolTip="Display annotations", checked=True) 134 | 135 | viewSettingButtons = [self.redOnlyLayoutButton, self.sideBySideLayoutButton, self.fourUpPlotLayoutButton, 136 | self.fourUpLayoutButton, self.infoButton, self.crosshairButton, 137 | self.settingsButton, self.dicomConnectionTestButton] 138 | 139 | for step in self.session.steps: 140 | viewSettingButtons += step.viewSettingButtons 141 | 142 | self.layout.addWidget(self.createHLayout(viewSettingButtons)) 143 | 144 | self.resetViewSettingButtons() 145 | 146 | def resetViewSettingButtons(self): 147 | for step in self.session.steps: 148 | step.resetViewSettingButtons() 149 | self.crosshairButton.checked = False 150 | 151 | def setupTabBarNavigation(self): 152 | self.tabWidget = SliceTrackerTabWidget() 153 | self.tabWidget.addEventObserver(self.tabWidget.AvailableLayoutsChangedEvent, self.onAvailableLayoutsChanged) 154 | self.layout.addWidget(self.tabWidget) 155 | self.tabWidget.hideTabs() 156 | 157 | def setupConnections(self): 158 | self.infoButton.connect('toggled(bool)', self.onShowInformationToggled) 159 | self.showAnnotationsButton.connect('toggled(bool)', self.onShowAnnotationsToggled) 160 | 161 | def setupSessionObservers(self): 162 | self.session.addEventObserver(self.session.PreprocessingSuccessfulEvent, self.onSuccessfulPreProcessing) 163 | self.session.addEventObserver(self.session.CurrentSeriesChangedEvent, self.onCurrentSeriesChanged) 164 | self.session.addEventObserver(self.session.CloseCaseEvent, self.onCaseClosed) 165 | 166 | def removeSessionObservers(self): 167 | self.session.removeEventObserver(self.session.PreprocessingSuccessfulEvent, self.onSuccessfulPreProcessing) 168 | self.session.removeEventObserver(self.session.CurrentSeriesChangedEvent, self.onCurrentSeriesChanged) 169 | self.session.removeEventObserver(self.session.CloseCaseEvent, self.onCaseClosed) 170 | 171 | def onSuccessfulPreProcessing(self, caller, event): 172 | dicomFileName = self.logic.getFileList(self.session.preopDICOMDirectory)[0] 173 | filename = os.path.join(self.session.preopDICOMDirectory, dicomFileName) 174 | self.patientWatchBox.sourceFile = filename 175 | self.preopWatchBox.sourceFile = filename 176 | 177 | def onShowAnnotationsToggled(self, checked): 178 | allSliceAnnotations = self.sliceAnnotations[:] 179 | 180 | def onShowInformationToggled(self, checked): 181 | self.patientWatchBox.visible = checked 182 | self.preopWatchBox.visible = checked 183 | self.intraopWatchBox.visible = checked 184 | 185 | @vtk.calldata_type(vtk.VTK_STRING) 186 | def onNewFileIndexed(self, caller, event, callData): 187 | text, size, currentIndex = ast.literal_eval(callData) 188 | if not self.customStatusProgressBar.visible: 189 | self.customStatusProgressBar.show() 190 | self.customStatusProgressBar.maximum = size 191 | self.customStatusProgressBar.updateStatus(text, currentIndex) 192 | 193 | @vtk.calldata_type(vtk.VTK_STRING) 194 | def onCurrentSeriesChanged(self, caller, event, callData=None): 195 | receivedFile = self.session.loadableList[callData][0] if callData else None 196 | if not self.session.data.usePreopData and self.patientWatchBox.sourceFile is None: 197 | self.patientWatchBox.sourceFile = receivedFile 198 | self.preopWatchBox.setInformation("StudyDate", "NA") 199 | self.intraopWatchBox.sourceFile = receivedFile 200 | 201 | @vtk.calldata_type(vtk.VTK_STRING) 202 | def onAvailableLayoutsChanged(self, caller, event, callData): 203 | layouts = ast.literal_eval(callData) 204 | for layoutButton in self.layoutButtons: 205 | layoutButton.enabled = layoutButton.LAYOUT in layouts 206 | 207 | @vtk.calldata_type(vtk.VTK_STRING) 208 | def onCaseClosed(self, caller, event, callData): 209 | self.customStatusProgressBar.reset() 210 | 211 | 212 | class SliceTrackerLogic(ModuleLogicMixin): 213 | 214 | def __init__(self): 215 | pass 216 | 217 | 218 | class SliceTrackerTabWidget(qt.QTabWidget, ModuleWidgetMixin): 219 | 220 | AvailableLayoutsChangedEvent = SliceTrackerStep.AvailableLayoutsChangedEvent 221 | 222 | def __init__(self): 223 | super(SliceTrackerTabWidget, self).__init__() 224 | self.session = SliceTrackerSession() 225 | self._createTabs() 226 | self.currentChanged.connect(self.onCurrentTabChanged) 227 | self.onCurrentTabChanged(0) 228 | 229 | def hideTabs(self): 230 | self.tabBar().hide() 231 | 232 | def _createTabs(self): 233 | # TODO: cleanup on reload? 234 | for step in self.session.steps: 235 | logging.debug("Adding tab for %s step" % step.NAME) 236 | self.addTab(step, step.NAME) 237 | step.addEventObserver(step.ActivatedEvent, self.onStepActivated) 238 | step.addEventObserver(self.AvailableLayoutsChangedEvent, self.onStepAvailableLayoutChanged) 239 | 240 | @vtk.calldata_type(vtk.VTK_STRING) 241 | def onStepAvailableLayoutChanged(self, caller, event, callData): 242 | self.invokeEvent(self.AvailableLayoutsChangedEvent, callData) 243 | 244 | def onStepActivated(self, caller, event): 245 | name = caller.GetAttribute("Name") 246 | index = next((i for i, step in enumerate(self.session.steps) if step.NAME == name), None) 247 | if index is not None: 248 | self.setCurrentIndex(index) 249 | 250 | @logmethod(logging.DEBUG) 251 | def onCurrentTabChanged(self, index): 252 | for idx, step in enumerate(self.session.steps): 253 | if index != idx: 254 | if step.active: 255 | self.session.previousStep = step 256 | step.active = False 257 | self.session.steps[index].active = True 258 | self.updateSizes(index) 259 | 260 | def updateSizes(self, index): 261 | for i in range(self.count): 262 | if i != index: 263 | self.widget(i).setSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Ignored) 264 | 265 | self.widget(index).setSizePolicy(qt.QSizePolicy.Preferred, qt.QSizePolicy.Preferred) 266 | self.widget(index).resize(self.widget(index).minimumSizeHint) 267 | self.resize(self.minimumSizeHint) 268 | self.adjustSize() 269 | 270 | class SliceTrackerSlicelet(qt.QWidget, ModuleWidgetMixin): 271 | 272 | class MainWindow(qt.QWidget): 273 | 274 | def __init__(self, parent=None): 275 | qt.QWidget.__init__(self) 276 | self.objectName = "qSlicerAppMainWindow" 277 | self.setLayout(qt.QVBoxLayout()) 278 | self.mainFrame = qt.QFrame() 279 | self.mainFrame.setLayout(qt.QHBoxLayout()) 280 | 281 | self._statusBar = qt.QStatusBar() 282 | self._statusBar.setMaximumHeight(35) 283 | 284 | self.layout().addWidget(self.mainFrame) 285 | self.layout().addWidget(self._statusBar) 286 | 287 | def statusBar(self): 288 | self._statusBar = getattr(self, "_statusBar", None) 289 | if not self._statusBar: 290 | self._statusBar = qt.QStatusBar() 291 | return self._statusBar 292 | 293 | def __init__(self): 294 | qt.QWidget.__init__(self) 295 | 296 | logging.debug(slicer.dicomDatabase) 297 | 298 | self.mainWidget = SliceTrackerSlicelet.MainWindow() 299 | 300 | self.setupLayoutWidget() 301 | 302 | self.moduleFrame = qt.QWidget() 303 | self.moduleFrame.setLayout(qt.QVBoxLayout()) 304 | self.widget = SliceTrackerWidget(self.moduleFrame) 305 | self.widget.setup() 306 | 307 | # TODO: resize self.widget.parent to minimum possible width 308 | 309 | self.scrollArea = qt.QScrollArea() 310 | self.scrollArea.setWidget(self.widget.parent) 311 | self.scrollArea.setWidgetResizable(True) 312 | self.scrollArea.setMinimumWidth(self.widget.parent.minimumSizeHint.width()) 313 | 314 | self.splitter = qt.QSplitter() 315 | self.splitter.setOrientation(qt.Qt.Horizontal) 316 | self.splitter.addWidget(self.scrollArea) 317 | self.splitter.addWidget(self.layoutWidget) 318 | self.splitter.splitterMoved.connect(self.onSplitterMoved) 319 | 320 | self.splitter.setStretchFactor(0,0) 321 | self.splitter.setStretchFactor(1,1) 322 | self.splitter.handle(1).installEventFilter(self) 323 | 324 | self.mainWidget.mainFrame.layout().addWidget(self.splitter) 325 | self.mainWidget.show() 326 | 327 | def setupLayoutWidget(self): 328 | self.layoutWidget = qt.QWidget() 329 | self.layoutWidget.setLayout(qt.QHBoxLayout()) 330 | layoutWidget = slicer.qMRMLLayoutWidget() 331 | layoutManager = slicer.qSlicerLayoutManager() 332 | layoutManager.setMRMLScene(slicer.mrmlScene) 333 | layoutManager.setScriptedDisplayableManagerDirectory(slicer.app.slicerHome + "/bin/Python/mrmlDisplayableManager") 334 | layoutWidget.setLayoutManager(layoutManager) 335 | slicer.app.setLayoutManager(layoutManager) 336 | layoutWidget.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) 337 | self.layoutWidget.layout().addWidget(layoutWidget) 338 | 339 | def eventFilter(self, obj, event): 340 | if event.type() == qt.QEvent.MouseButtonDblClick: 341 | self.onSplitterClick() 342 | 343 | def onSplitterMoved(self, pos, index): 344 | vScroll = self.scrollArea.verticalScrollBar() 345 | logging.debug(self.moduleFrame.width, self.widget.parent.width, self.scrollArea.width, vScroll.width) 346 | vScrollbarWidth = 4 if not vScroll.isVisible() else vScroll.width + 4 # TODO: find out, what is 4px wide 347 | if self.scrollArea.minimumWidth != self.widget.parent.minimumSizeHint.width() + vScrollbarWidth: 348 | self.scrollArea.setMinimumWidth(self.widget.parent.minimumSizeHint.width() + vScrollbarWidth) 349 | 350 | def onSplitterClick(self): 351 | if self.splitter.sizes()[0] > 0: 352 | self.splitter.setSizes([0, self.splitter.sizes()[1]]) 353 | else: 354 | minimumWidth = self.widget.parent.minimumSizeHint.width() 355 | self.splitter.setSizes([minimumWidth, self.splitter.sizes()[1]-minimumWidth]) 356 | 357 | 358 | if __name__ == "SliceTrackerSlicelet": 359 | import sys 360 | logging.debug(sys.argv) 361 | slicelet = SliceTrackerSlicelet() 362 | -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Christian' -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/algorithms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/SliceTrackerUtils/algorithms/__init__.py -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/algorithms/automaticProstateSegmentation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | import vtk 5 | from collections import OrderedDict 6 | 7 | import slicer 8 | import DeepInfer 9 | 10 | from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin 11 | 12 | 13 | class AutomaticSegmentationLogic(ModuleLogicMixin): 14 | 15 | DeepLearningStartedEvent = vtk.vtkCommand.UserEvent + 438 16 | DeepLearningFinishedEvent = vtk.vtkCommand.UserEvent + 439 17 | DeepLearningFailedEvent = vtk.vtkCommand.UserEvent + 441 18 | # DeepLearningStatusChangedEvent = vtk.vtkCommand.UserEvent + 440 19 | 20 | def __init__(self): 21 | self.inputVolume = None 22 | self.colorNode = None 23 | 24 | def cleanup(self): 25 | self.inputVolume = None 26 | 27 | def run(self, inputVolume, domain, colorNode=None): 28 | 29 | self.colorNode = colorNode 30 | if not inputVolume: 31 | raise ValueError("No input volume found for initializing prostate segmentation deep learning.") 32 | 33 | self.inputVolume = inputVolume 34 | self.invokeEvent(self.DeepLearningStartedEvent) 35 | outputLabel = self._runDocker(domain) 36 | 37 | if outputLabel: 38 | self.invokeEvent(self.DeepLearningFinishedEvent, outputLabel) 39 | else: 40 | self.invokeEvent(self.DeepLearningFailedEvent) 41 | 42 | return outputLabel 43 | 44 | def _runDocker(self, domain): 45 | logic = DeepInfer.DeepInferLogic() 46 | parameters = DeepInfer.ModelParameters() 47 | with open(os.path.join(DeepInfer.JSON_LOCAL_DIR, "ProstateSegmenter.json"), "r") as fp: 48 | j = json.load(fp, object_pairs_hook=OrderedDict) 49 | 50 | iodict = parameters.create_iodict(j) 51 | dockerName, modelName, dataPath = parameters.create_model_info(j) 52 | logging.debug(iodict) 53 | 54 | inputs = { 55 | 'InputVolume' : self.inputVolume 56 | } 57 | 58 | outputLabel = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode") 59 | outputLabel.SetName(self.inputVolume.GetName()+"-label") 60 | outputs = { 61 | 'OutputLabel': outputLabel 62 | } 63 | 64 | params = dict() 65 | params['Domain'] = domain 66 | params['OutputSmoothing'] = 0 67 | params['ProcessingType'] = 'Fast' 68 | params['InferenceType'] = 'Single' 69 | params['verbose'] = 1 70 | 71 | logic.executeDocker(dockerName, modelName, dataPath, iodict, inputs, params) 72 | 73 | if logic.abort: 74 | return None 75 | 76 | logic.updateOutput(iodict, outputs) 77 | 78 | if self.colorNode: 79 | displayNode = outputLabel.GetDisplayNode() 80 | displayNode.SetAndObserveColorNodeID(self.colorNode.GetID()) 81 | 82 | return outputLabel -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/algorithms/zFrameRegistration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import slicer 5 | from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin 6 | 7 | 8 | class ZFrameRegistrationBase(ModuleLogicMixin): 9 | 10 | ZFRAME_TRANSFORM_NAME = "ZFrameTransform" 11 | 12 | def __init__(self, inputVolume): 13 | self.inputVolume = inputVolume 14 | self.outputTransform = None 15 | self.outputVolume = None 16 | 17 | def getOutputTransformation(self): 18 | return self.outputTransform 19 | 20 | def getOutputVolume(self): 21 | return self.outputVolume 22 | 23 | def runRegistration(self): 24 | raise NotImplementedError 25 | 26 | 27 | class LineMarkerRegistration(ZFrameRegistrationBase): 28 | 29 | def __init__(self, inputVolume): 30 | super(LineMarkerRegistration, self).__init__(inputVolume) 31 | self.markerConfigPath = os.path.join(os.path.dirname(sys.modules[self.__module__].__file__), '..', 'Resources', 32 | 'zframe', 'zframe-config.csv') 33 | 34 | def runRegistration(self): 35 | volumesLogic = slicer.modules.volumes.logic() 36 | self.outputVolume = volumesLogic.CreateAndAddLabelVolume(slicer.mrmlScene, self.inputVolume, 37 | self.inputVolume.GetName() + '-label') 38 | seriesNumber = self.inputVolume.GetName().split(":")[0] 39 | self.outputTransform = self.createLinearTransformNode(seriesNumber + "-" + self.ZFRAME_TRANSFORM_NAME) 40 | 41 | params = {'inputVolume': self.inputVolume, 'markerConfigFile': self.markerConfigPath, 42 | 'outputVolume': self.outputVolume, 'markerTransform': self.outputTransform} 43 | slicer.cli.run(slicer.modules.linemarkerregistration, None, params, wait_for_completion=True) 44 | 45 | 46 | class OpenSourceZFrameRegistration(ZFrameRegistrationBase): 47 | 48 | def __init__(self, inputVolume): 49 | super(OpenSourceZFrameRegistration, self).__init__(inputVolume) 50 | 51 | def runRegistration(self, start, end): 52 | assert start != -1 and end != -1 53 | seriesNumber = self.inputVolume.GetName().split(":")[0] 54 | self.outputTransform = self.createLinearTransformNode(seriesNumber + "-" + self.ZFRAME_TRANSFORM_NAME) 55 | 56 | params = {'inputVolume': self.inputVolume, 'startSlice': start, 'endSlice': end, 57 | 'outputTransform': self.outputTransform} 58 | slicer.cli.run(slicer.modules.zframeregistration, None, params, wait_for_completion=True) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/configuration.py: -------------------------------------------------------------------------------- 1 | import ConfigParser 2 | import inspect, os 3 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin 4 | from constants import SliceTrackerConstants as constants 5 | 6 | 7 | class SliceTrackerConfiguration(ModuleWidgetMixin): 8 | 9 | def __init__(self, moduleName, configFile): 10 | self.moduleName = moduleName 11 | self.configFile = configFile 12 | self.loadConfiguration() 13 | 14 | def loadConfiguration(self): 15 | 16 | config = ConfigParser.RawConfigParser() 17 | config.read(self.configFile) 18 | 19 | if not self.getSetting("ZFrame_Registration_Class_Name"): 20 | self.setSetting("ZFrame_Registration_Class_Name", config.get('ZFrame Registration', 'class')) 21 | 22 | if not self.getSetting("PLANNING_IMAGE_PATTERN"): 23 | self.setSetting("PLANNING_IMAGE_PATTERN", config.get('Series Descriptions', 'PLANNING_IMAGE_PATTERN')) 24 | if not self.getSetting("COVER_PROSTATE_PATTERN"): 25 | self.setSetting("COVER_PROSTATE_PATTERN", config.get('Series Descriptions', 'COVER_PROSTATE_PATTERN')) 26 | if not self.getSetting("COVER_TEMPLATE_PATTERN"): 27 | self.setSetting("COVER_TEMPLATE_PATTERN", config.get('Series Descriptions', 'COVER_TEMPLATE_PATTERN')) 28 | if not self.getSetting("NEEDLE_IMAGE_PATTERN"): 29 | self.setSetting("NEEDLE_IMAGE_PATTERN", config.get('Series Descriptions', 'NEEDLE_IMAGE_PATTERN')) 30 | if not self.getSetting("VIBE_IMAGE_PATTERN"): 31 | self.setSetting("VIBE_IMAGE_PATTERN", config.get('Series Descriptions', 'VIBE_IMAGE_PATTERN')) 32 | 33 | seriesTypes = [constants.COVER_TEMPLATE, constants.COVER_PROSTATE, constants.GUIDANCE_IMAGE, 34 | constants.VIBE_IMAGE, constants.OTHER_IMAGE] 35 | 36 | self.setSetting("SERIES_TYPES", seriesTypes) 37 | 38 | if not self.getSetting("Color_File_Name") or not os.path.exists(self.getSetting("Color_File_Name")): 39 | colorFilename = config.get('Color File', 'FileName') 40 | self.setSetting("Color_File_Name", os.path.join(os.path.dirname(inspect.getfile(self.__class__)), 41 | '../Resources/Colors', colorFilename)) 42 | 43 | if not self.getSetting("Segmentation_Color_Name"): 44 | segmentedColorName = config.get('Color File', 'SegmentedColorName') 45 | self.setSetting("Segmentation_Color_Name", segmentedColorName) 46 | 47 | if not self.getSetting("DEFAULT_EVALUATION_LAYOUT"): 48 | self.setSetting("DEFAULT_EVALUATION_LAYOUT", config.get('Evaluation', 'Default_Layout')) 49 | 50 | if not self.getSetting("Demo_Mode"): 51 | self.setSetting("Demo_Mode", config.get('Modes', 'Demo_Mode')) 52 | 53 | if not self.getSetting("Use_Deep_Learning"): 54 | self.setSetting("Use_Deep_Learning", config.get('Segmentation', 'Use_Deep_Learning')) 55 | 56 | if not self.getSetting("Incoming_DICOM_Port"): 57 | self.setSetting("Incoming_DICOM_Port", config.get('DICOM', 'Incoming_Port')) 58 | 59 | if not self.getSetting("CASE_NUMBER_OF_DIGITS"): 60 | self.setSetting("CASE_NUMBER_OF_DIGITS", config.get('General', 'CASE_NUMBER_OF_DIGITS')) 61 | 62 | self.replaceOldValues() 63 | 64 | def replaceOldValues(self): 65 | for setting in ['PLANNING_IMAGE', 'COVER_TEMPLATE', 'COVER_PROSTATE', 'NEEDLE_IMAGE', 'VIBE_IMAGE']: 66 | if self.getSetting(setting): 67 | self.setSetting(setting+"_PATTERN", self.getSetting(setting)) 68 | self.removeSetting(setting) 69 | 70 | for other in ['OTHER_IMAGE', 'Rating_Enabled', 'Maximum_Rating_Score']: 71 | if self.getSetting(other): 72 | self.removeSetting(other) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/constants.py: -------------------------------------------------------------------------------- 1 | import slicer 2 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin as helper 3 | 4 | class SliceTrackerConstants(object): 5 | 6 | MODULE_NAME = "SliceTracker" 7 | 8 | PREOP_SAMPLE_DATA_URL = 'https://github.com/SlicerProstate/SliceTracker/releases/download/test-data/Preop-deid.zip' 9 | INTRAOP_SAMPLE_DATA_URL = 'https://github.com/SlicerProstate/SliceTracker/releases/download/test-data/Intraop-deid.zip' 10 | 11 | JSON_FILENAME = "results.json" 12 | 13 | MISSING_PREOP_ANNOTATION_TEXT = "No preop data available" 14 | LEFT_VIEWER_SLICE_ANNOTATION_TEXT = 'BIOPSY PLAN' 15 | RIGHT_VIEWER_SLICE_ANNOTATION_TEXT = 'TRACKED TARGETS' 16 | RIGHT_VIEWER_SLICE_TRANSFORMED_ANNOTATION_TEXT = 'OLD' 17 | RIGHT_VIEWER_SLICE_NEEDLE_IMAGE_ANNOTATION_TEXT = 'NEW' 18 | APPROVED_RESULT_TEXT_ANNOTATION = "approved" 19 | REJECTED_RESULT_TEXT_ANNOTATION = "rejected" 20 | SKIPPED_RESULT_TEXT_ANNOTATION = "skipped" 21 | 22 | LAYOUT_RED_SLICE_ONLY = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView 23 | LAYOUT_FOUR_UP = slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView 24 | LAYOUT_FOUR_UP_QUANTITATIVE = slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpPlotView 25 | LAYOUT_SIDE_BY_SIDE = slicer.vtkMRMLLayoutNode.SlicerLayoutSideBySideView 26 | ALLOWED_LAYOUTS = [LAYOUT_SIDE_BY_SIDE, LAYOUT_FOUR_UP, LAYOUT_RED_SLICE_ONLY, LAYOUT_FOUR_UP_QUANTITATIVE] 27 | 28 | PLANNING_IMAGE = "PLANNING IMAGE" 29 | COVER_PROSTATE = "COVER PROSTATE" 30 | COVER_TEMPLATE = "COVER TEMPLATE" 31 | GUIDANCE_IMAGE = "GUIDANCE" 32 | VIBE_IMAGE = "VIBE" 33 | OTHER_IMAGE = "OTHER" 34 | 35 | TRACKABLE_IMAGE_TYPES = [COVER_PROSTATE, COVER_TEMPLATE, GUIDANCE_IMAGE] 36 | 37 | ZFrame_INSTRUCTION_STEPS = {1: "Scroll and click into ZFrame center to set ROI center", 38 | 2: "Click outside of upper right ZFrame corner to set ROI border"} 39 | 40 | IntraopSeriesSelectorToolTip = """ 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 55 | 56 | 57 | 60 | 63 | 64 | 65 | 68 | 71 | 72 | 73 | 76 | 79 | 80 | 81 |
50 | 51 | 53 | tracked(registration result available) 54 |
58 | 59 | 61 | untracked(no registration result available) 62 |
66 | 67 | 69 | skipped(no registration result available) 70 |
74 | 75 | 77 | rejected(non satisfactory/approved registration result available) 78 |
82 | 83 | 84 | """ % (helper.createAndGetRawColoredPixelMap("green"), helper.createAndGetRawColoredPixelMap("yellow"), 85 | helper.createAndGetRawColoredPixelMap("red"), helper.createAndGetRawColoredPixelMap("grey")) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import datetime 4 | import qt 5 | import vtk 6 | import re 7 | import slicer 8 | 9 | from constants import SliceTrackerConstants as constants 10 | 11 | from SlicerDevelopmentToolboxUtils.decorators import logmethod 12 | from SlicerDevelopmentToolboxUtils.widgets import ExtendedQMessageBox 13 | from SlicerDevelopmentToolboxUtils.module.logic import LogicBase 14 | from SlicerDevelopmentToolboxUtils.module.base import ModuleBase 15 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin 16 | from SlicerDevelopmentToolboxUtils.metaclasses import Singleton 17 | from SlicerDevelopmentToolboxUtils.icons import Icons 18 | 19 | 20 | class NewCaseSelectionNameWidget(qt.QMessageBox, ModuleWidgetMixin): 21 | 22 | PREFIX = "Case" 23 | SUFFIX = "-" + datetime.date.today().strftime("%Y%m%d") 24 | SUFFIX_PATTERN = "-[0-9]{8}" 25 | 26 | def __init__(self, destination, parent=None): 27 | super(NewCaseSelectionNameWidget, self).__init__(parent) 28 | self.CASE_NUMBER_OF_DIGITS = int(self.getSetting("CASE_NUMBER_OF_DIGITS", moduleName="SliceTracker")) 29 | self.PATTERN = self.PREFIX +"[0-9]{" + str(self.CASE_NUMBER_OF_DIGITS - 1) + "}[0-9]{1}" + self.SUFFIX_PATTERN 30 | 31 | if not os.path.exists(destination): 32 | raise OSError("Directory %s doesn't exist" % destination) 33 | self.destinationRoot = destination 34 | self.newCaseDirectory = None 35 | self.minimum = self.getNextCaseNumber() 36 | self.setupUI() 37 | self.setupConnections() 38 | self.onCaseNumberChanged(self.minimum) 39 | 40 | def getNextCaseNumber(self): 41 | caseNumber = 0 42 | for dirName in [dirName for dirName in os.listdir(self.destinationRoot) 43 | if os.path.isdir(os.path.join(self.destinationRoot, dirName)) and re.match(self.PATTERN, dirName)]: 44 | number = int(re.split(self.SUFFIX_PATTERN, dirName)[0].split(self.PREFIX)[1]) 45 | caseNumber = caseNumber if caseNumber > number else number 46 | return caseNumber+1 47 | 48 | def setupUI(self): 49 | self.setWindowTitle("Case Number Selection") 50 | self.spinbox = qt.QSpinBox() 51 | self.spinbox.setRange(self.minimum, int("9" * self.CASE_NUMBER_OF_DIGITS)) 52 | 53 | self.hideInvisibleUnneededComponents() 54 | 55 | self.textLabel = qt.QLabel("Please select a case number for the new case.") 56 | self.textLabel.setStyleSheet("font-weight: bold;") 57 | self.previewLabel = qt.QLabel("New case directory:") 58 | self.preview = qt.QLabel() 59 | self.notice = qt.QLabel() 60 | self.notice.setStyleSheet("color:red;") 61 | 62 | self.okButton = self.addButton(self.Ok) 63 | self.okButton.enabled = False 64 | self.cancelButton = self.addButton(self.Cancel) 65 | self.setDefaultButton(self.okButton) 66 | 67 | self.groupBox = qt.QGroupBox() 68 | self.groupBox.setLayout(qt.QGridLayout()) 69 | self.groupBox.layout().addWidget(self.textLabel, 0, 0, 1, 2) 70 | self.groupBox.layout().addWidget(qt.QLabel("Proposed Case Number"), 1, 0) 71 | self.groupBox.layout().addWidget(self.spinbox, 1, 1) 72 | self.groupBox.layout().addWidget(self.previewLabel, 2, 0, 1, 2) 73 | self.groupBox.layout().addWidget(self.preview, 3, 0, 1, 2) 74 | self.groupBox.layout().addWidget(self.notice, 4, 0, 1, 2) 75 | 76 | self.groupBox.layout().addWidget(self.okButton, 5, 0) 77 | self.groupBox.layout().addWidget(self.cancelButton, 5, 1) 78 | 79 | self.layout().addWidget(self.groupBox, 1, 1) 80 | 81 | def hideInvisibleUnneededComponents(self): 82 | for oName in ["qt_msgbox_label", "qt_msgboxex_icon_label"]: 83 | try: 84 | slicer.util.findChild(self, oName).hide() 85 | except RuntimeError: 86 | pass 87 | 88 | def setupConnections(self): 89 | self.spinbox.valueChanged.connect(self.onCaseNumberChanged) 90 | 91 | def onCaseNumberChanged(self, caseNumber): 92 | formatString = '%0' + str(self.CASE_NUMBER_OF_DIGITS) + 'd' 93 | caseNumber = formatString % caseNumber 94 | directory = self.PREFIX+caseNumber+self.SUFFIX 95 | self.newCaseDirectory = os.path.join(self.destinationRoot, directory) 96 | self.preview.setText( self.newCaseDirectory) 97 | self.okButton.enabled = not os.path.exists(self.newCaseDirectory) 98 | self.notice.text = "" if not os.path.exists(self.newCaseDirectory) else "Note: Directory already exists." 99 | 100 | 101 | class SeriesTypeManager(LogicBase): 102 | 103 | SeriesTypeManuallyAssignedEvent = vtk.vtkCommand.UserEvent + 2334 104 | 105 | MODULE_NAME = constants.MODULE_NAME 106 | 107 | __metaclass__ = Singleton 108 | 109 | assignedSeries = {} 110 | 111 | def __init__(self): 112 | LogicBase.__init__(self) 113 | self.seriesTypes = self.getSetting("SERIES_TYPES") 114 | 115 | def clear(self): 116 | self.assignedSeries = {} 117 | 118 | def getSeriesType(self, series): 119 | try: 120 | return self.assignedSeries[series] 121 | except KeyError: 122 | self.assignedSeries[series] = self.computeSeriesType(series) 123 | return self.assignedSeries[series] 124 | 125 | def computeSeriesType(self, series): 126 | if self.getSetting("COVER_PROSTATE_PATTERN") in series: 127 | seriesType = constants.COVER_PROSTATE 128 | elif self.getSetting("COVER_TEMPLATE_PATTERN") in series: 129 | seriesType = constants.COVER_TEMPLATE 130 | elif self.getSetting("NEEDLE_IMAGE_PATTERN") in series: 131 | seriesType = constants.GUIDANCE_IMAGE 132 | elif self.getSetting("VIBE_IMAGE_PATTERN") in series: 133 | seriesType = constants.VIBE_IMAGE 134 | else: 135 | seriesType = constants.OTHER_IMAGE 136 | return seriesType 137 | 138 | def autoAssign(self, series): 139 | self.assignedSeries[series] = self.getSeriesType(series) 140 | 141 | def assign(self, series, seriesType=None): 142 | if series in self.assignedSeries.keys() and self.assignedSeries[series] == seriesType: 143 | return 144 | if seriesType: 145 | assert seriesType in self.seriesTypes 146 | self.assignedSeries[series] = seriesType 147 | self.invokeEvent(self.SeriesTypeManuallyAssignedEvent) 148 | else: 149 | self.autoAssign(series) 150 | 151 | def isCoverProstate(self, series): 152 | return self._hasSeriesType(series, constants.COVER_PROSTATE) 153 | 154 | def isCoverTemplate(self, series): 155 | return self._hasSeriesType(series, constants.COVER_TEMPLATE) 156 | 157 | def isGuidance(self, series): 158 | return self._hasSeriesType(series, constants.GUIDANCE_IMAGE) 159 | 160 | def isVibe(self, series): 161 | return self._hasSeriesType(series, constants.VIBE_IMAGE) 162 | 163 | def isOther(self, series): 164 | return self._hasSeriesType(series, constants.OTHER_IMAGE) or not (self.isCoverProstate(series) or 165 | self.isCoverTemplate(series) or 166 | self.isGuidance(series) or 167 | self.isVibe(series)) 168 | 169 | def _hasSeriesType(self, series, seriesType): 170 | return self.getSeriesType(series) == seriesType 171 | 172 | 173 | class IncomingDataMessageBox(ExtendedQMessageBox): 174 | 175 | def __init__(self, parent=None): 176 | super(IncomingDataMessageBox, self).__init__(parent) 177 | self.setWindowTitle("Incoming image data") 178 | self.setText("New data has been received. What do you want do?") 179 | self.setIcon(qt.QMessageBox.Question) 180 | trackButton = self.addButton(qt.QPushButton('Track targets'), qt.QMessageBox.AcceptRole) 181 | self.addButton(qt.QPushButton('Postpone'), qt.QMessageBox.NoRole) 182 | self.setDefaultButton(trackButton) 183 | 184 | 185 | class SeriesTypeToolButton(qt.QToolButton, ModuleBase, ModuleWidgetMixin): 186 | 187 | MODULE_NAME = constants.MODULE_NAME 188 | 189 | class SeriesTypeListWidget(qt.QListWidget, ModuleWidgetMixin): 190 | 191 | @property 192 | def series(self): 193 | return self._series 194 | 195 | @series.setter 196 | def series(self, value): 197 | self._series = value 198 | self._preselectSeriesType() 199 | 200 | def __init__(self, series=None): 201 | qt.QListWidget.__init__(self) 202 | self.seriesTypeManager = SeriesTypeManager() 203 | self._series = series 204 | self.setup() 205 | self.setupConnections() 206 | 207 | def setup(self): 208 | self.clear() 209 | for index, seriesType in enumerate(self.seriesTypeManager.seriesTypes): 210 | self.addItem(seriesType) 211 | self.setFixedSize(self.sizeHintForColumn(0) + 2 * self.frameWidth, 212 | self.sizeHintForRow(0) * self.count + 2 * self.frameWidth) 213 | self._preselectSeriesType() 214 | 215 | def _preselectSeriesType(self): 216 | if not self._series: 217 | self.setCurrentItem(None) 218 | seriesType = self.seriesTypeManager.getSeriesType(self._series) 219 | for index in range(self.count): 220 | if seriesType == self.item(index).text(): 221 | self.setCurrentItem(self.item(index)) 222 | return 223 | 224 | def setupConnections(self): 225 | self.itemSelectionChanged.connect(self.onSelectionChanged) 226 | 227 | def onSelectionChanged(self): 228 | currentSeriesType = self.seriesTypeManager.getSeriesType(self._series) 229 | selectedSeriesType = self.selectedItems()[0].text() 230 | if selectedSeriesType != currentSeriesType: 231 | if slicer.util.confirmYesNoDisplay("You are about to change the series type for series:\n\n" 232 | "Name: {0} \n\n" 233 | "Current series type: {1} \n" 234 | "New series type: {2} \n\n" 235 | "This could have significant impact on the workflow.\n\n" 236 | "Do you want to proceed?".format(self._series, currentSeriesType, 237 | selectedSeriesType)): 238 | self.seriesTypeManager.assign(self._series, self.selectedItems()[0].text()) 239 | else: 240 | self._preselectSeriesType() 241 | 242 | def __init__(self): 243 | qt.QToolButton.__init__(self) 244 | ModuleBase.__init__(self) 245 | self.setPopupMode(self.InstantPopup) 246 | self.setMenu(qt.QMenu(self)) 247 | self.action = qt.QWidgetAction(self) 248 | self.listWidget = None 249 | self.setIcon(Icons.edit) 250 | self.enabled = False 251 | 252 | @logmethod(logging.DEBUG) 253 | def setSeries(self, series): 254 | if not self.listWidget: 255 | self.listWidget = self.SeriesTypeListWidget(series) 256 | self.action.setDefaultWidget(self.listWidget) 257 | self.menu().addAction(self.action) 258 | seriesTypeManager = SeriesTypeManager() 259 | seriesTypeManager.addEventObserver(seriesTypeManager.SeriesTypeManuallyAssignedEvent, 260 | lambda caller, event: self.menu().close()) 261 | else: 262 | self.listWidget.series = series 263 | self.updateTooltipAndIcon(series) 264 | 265 | def updateTooltipAndIcon(self, series): 266 | seriesTypeManager = SeriesTypeManager() 267 | currentType = seriesTypeManager.getSeriesType(series) 268 | autoType = seriesTypeManager.computeSeriesType(series) 269 | if currentType != autoType: 270 | icon = Icons.info 271 | tooltip = "Series type assignment changed!\n\n" \ 272 | "Auto-calculated series type: {}\n\n" \ 273 | "Assigned series type: {}".format(autoType, currentType) 274 | else: 275 | icon = Icons.edit 276 | tooltip = self.listWidget.currentItem().text() if self.listWidget.currentItem() else "" 277 | self.setIcon(icon) 278 | self.setToolTip(tooltip) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/preopHandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import slicer 3 | import qt 4 | import vtk 5 | import xml 6 | import getpass 7 | import datetime 8 | import logging 9 | import re 10 | 11 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin, ModuleLogicMixin 12 | from SlicerDevelopmentToolboxUtils.widgets import CustomStatusProgressbar, SliceWidgetConfirmYesNoDialog 13 | from SlicerDevelopmentToolboxUtils.constants import FileExtension 14 | from SlicerDevelopmentToolboxUtils.decorators import onReturnProcessEvents 15 | from SlicerDevelopmentToolboxUtils.exceptions import PreProcessedDataError, NoEligibleSeriesFoundError 16 | 17 | from .algorithms.automaticProstateSegmentation import AutomaticSegmentationLogic 18 | from .steps.plugins.segmentationValidator import SliceTrackerSegmentationValidatorPlugin 19 | from .constants import SliceTrackerConstants 20 | from .sessionData import SegmentationData, PreopData 21 | 22 | 23 | class PreopDataHandler(ModuleWidgetMixin, ModuleLogicMixin): 24 | 25 | MODULE_NAME = SliceTrackerConstants.MODULE_NAME 26 | 27 | PreprocessingStartedEvent = vtk.vtkCommand.UserEvent + 434 28 | PreprocessingFinishedEvent = vtk.vtkCommand.UserEvent + 435 29 | PreprocessedDataErrorEvent = vtk.vtkCommand.UserEvent + 436 30 | 31 | @staticmethod 32 | def getFirstMpReviewPreprocessedStudy(directory): 33 | # TODO add check here and selected the one which has targets in it 34 | # TODO: if several studies are available provide a drop down or anything similar for choosing 35 | directoryNames = filter(os.path.isdir, [os.path.join(directory, f) for f in os.listdir(directory)]) 36 | return None if not len(directoryNames) else directoryNames[0] 37 | 38 | @staticmethod 39 | def wasDirectoryPreprocessed(directory): 40 | # TODO: this check needs to be more specific with expected sub directories 41 | firstDir = PreopDataHandler.getFirstMpReviewPreprocessedStudy(directory) 42 | if not firstDir: 43 | return False 44 | from mpReview import mpReviewLogic 45 | return mpReviewLogic.wasmpReviewPreprocessed(directory) 46 | 47 | @property 48 | def outputDirectory(self): 49 | self._outputDirectory = getattr(self, "_outputDirectory", None) 50 | return self._outputDirectory 51 | 52 | @outputDirectory.setter 53 | def outputDirectory(self, directory): 54 | if directory and not os.path.exists(directory): 55 | self.createDirectory(directory) 56 | self._outputDirectory = directory 57 | 58 | @property 59 | def segmentationData(self): 60 | try: 61 | return self.data.preopData.segmentation 62 | except AttributeError: 63 | return None 64 | 65 | @segmentationData.setter 66 | def segmentationData(self, value): 67 | assert self.preopData 68 | self.preopData.segmentation = value 69 | 70 | @property 71 | def preopData(self): 72 | return self.data.preopData 73 | 74 | @preopData.setter 75 | def preopData(self, value): 76 | self.data.preopData = value 77 | 78 | def __init__(self, inputDirectory, outputDirectory, data): 79 | self._inputDirectory = inputDirectory 80 | self.outputDirectory = outputDirectory 81 | self.data = data 82 | 83 | def handle(self): 84 | if self._runPreProcessing(): 85 | self.runModule() 86 | else: 87 | slicer.util.infoDisplay("No DICOM data could be processed. Please select another directory.", 88 | windowTitle="SliceTracker") 89 | 90 | def _runPreProcessing(self): 91 | from mpReviewPreprocessor import mpReviewPreprocessorLogic 92 | mpReviewPreprocessorLogic = mpReviewPreprocessorLogic() 93 | progress = self.createProgressDialog() 94 | progress.canceled.connect(lambda: mpReviewPreprocessorLogic.cancelProcess()) 95 | 96 | @onReturnProcessEvents 97 | def updateProgressBar(**kwargs): 98 | for key, value in kwargs.iteritems(): 99 | if hasattr(progress, key): 100 | setattr(progress, key, value) 101 | 102 | success = mpReviewPreprocessorLogic.importAndProcessData(self._inputDirectory, outputDir=self.outputDirectory, 103 | copyDICOM=False, 104 | progressCallback=updateProgressBar) 105 | progress.canceled.disconnect(lambda: mpReviewPreprocessorLogic.cancelProcess()) 106 | progress.close() 107 | return success 108 | 109 | def runModule(self, invokeEvent=True): 110 | if invokeEvent: 111 | self.invokeEvent(self.PreprocessingStartedEvent) 112 | 113 | def onModuleReturn(): 114 | slicer.modules.mpReviewWidget.saveButton.clicked.disconnect(onModuleReturn) 115 | self.layoutManager.selectModule(self.MODULE_NAME) 116 | slicer.mrmlScene.Clear(0) 117 | self.loadPreProcessedData() 118 | 119 | self.setSetting('InputLocation', None, moduleName="mpReview") 120 | self.layoutManager.selectModule("mpReview") 121 | mpReview = slicer.modules.mpReviewWidget 122 | self.setSetting('InputLocation', self.outputDirectory, moduleName="mpReview") 123 | mpReview.onReload() 124 | slicer.modules.mpReviewWidget.saveButton.clicked.connect(onModuleReturn) 125 | self.layoutManager.selectModule(mpReview.moduleName) 126 | 127 | def loadPreProcessedData(self): 128 | try: 129 | self.loadMpReviewProcessedData() 130 | except PreProcessedDataError as exc: 131 | self.onPreopLoadingFailed(str(exc)) 132 | except NoEligibleSeriesFoundError as exc: 133 | slicer.util.errorDisplay(str(exc), windowTitle="SliceTracker") 134 | self.invokeEvent(self.PreprocessedDataErrorEvent) 135 | 136 | def loadMpReviewProcessedData(self): 137 | studyDir = self.getFirstMpReviewPreprocessedStudy(self.outputDirectory) 138 | resourcesDir = os.path.join(studyDir, 'RESOURCES') 139 | if not self.isMpReviewStudyDirectoryValid(resourcesDir): 140 | raise PreProcessedDataError 141 | 142 | self.preopImagePath, self.preopSegmentationPath = self.findPreopImageAndSegmentationPaths(resourcesDir) 143 | 144 | self.data.initialTargetsPath = os.path.join(studyDir, 'Targets') 145 | 146 | loadedPreopVolume = self.loadPreopVolume() 147 | loadedPreopTargets = self.loadPreopTargets() 148 | loadedPreopT2Label = self.loadT2Label() if os.path.exists(self.preopSegmentationPath) else False 149 | success = all(r is True for r in [loadedPreopT2Label, loadedPreopVolume, loadedPreopTargets]) 150 | 151 | if success: 152 | if not self.data.usePreopData: 153 | self._createPreopData(algorithm="Manual") 154 | self.segmentationData.note = "mpReview preprocessed" 155 | self.segmentationData._label = self.data.initialLabel 156 | self.invokeEvent(self.PreprocessingFinishedEvent) 157 | else: 158 | useDeepLearning = str(self.getSetting("Use_Deep_Learning", moduleName=self.MODULE_NAME)).lower() == "true" 159 | if loadedPreopTargets and loadedPreopVolume and useDeepLearning: 160 | if slicer.util.confirmYesNoDisplay("No WholeGland segmentation found in preop data. Automatic segmentation is " 161 | "available. Would you like to proceed with the automatic segmentation?", 162 | windowTitle="SliceTracker"): 163 | self._createPreopData(algorithm="Automatic") 164 | self.runAutomaticSegmentation() 165 | return 166 | raise PreProcessedDataError 167 | 168 | def onPreopLoadingFailed(self, detailed=None, offerRevisit=True): 169 | detailed = detailed if detailed else "Make sure that the correct mpReview directory structure is used. " \ 170 | "\n\nSliceTracker expects a T2 volume, WholeGland segmentation and target(s)." 171 | message = "Loading preop data failed. Do you want to " \ 172 | "open/revisit pre-processing for the current case?" 173 | if slicer.util.confirmYesNoDisplay(message, windowTitle="SliceTracker", detailedText=detailed): 174 | self.runModule() 175 | else: 176 | self.invokeEvent(self.PreprocessedDataErrorEvent) 177 | 178 | def _createPreopData(self, algorithm, segmentationType="Prostate"): 179 | self.preopData = PreopData() 180 | self.segmentationData = SegmentationData(segmentationType=segmentationType, algorithm=algorithm) 181 | 182 | def runAutomaticSegmentation(self): 183 | logic = AutomaticSegmentationLogic() 184 | logic.addEventObserver(logic.DeepLearningFinishedEvent, self.onSegmentationFinished) 185 | logic.addEventObserver(logic.DeepLearningFailedEvent, 186 | lambda c,e: self.onPreopLoadingFailed("Automatic segmentation failed.")) 187 | customStatusProgressBar = CustomStatusProgressbar() 188 | customStatusProgressBar.text = "Running DeepInfer for automatic prostate segmentation" 189 | 190 | from mpReview import mpReviewLogic 191 | mpReviewColorNode, _ = mpReviewLogic.loadColorTable(self.getSetting("Color_File_Name", moduleName=self.MODULE_NAME)) 192 | 193 | domain = 'BWH_WITHOUT_ERC' 194 | prompt = SliceWidgetConfirmYesNoDialog(self.data.initialVolume, 195 | "Was an endorectal coil used during preop acquisition?").exec_() 196 | 197 | self.preopData.usedERC = False 198 | if prompt == qt.QDialogButtonBox.Yes: 199 | self.preopData.usedERC = True 200 | domain = 'BWH_WITH_ERC' 201 | elif prompt == qt.QDialogButtonBox.Cancel: 202 | raise PreProcessedDataError 203 | 204 | self.segmentationData.startTime = self.getTime() 205 | logic.run(self.data.initialVolume, domain, mpReviewColorNode) 206 | 207 | @vtk.calldata_type(vtk.VTK_OBJECT) 208 | def onSegmentationValidated(self, caller, event, labelNode): 209 | if "_modified" in labelNode.GetName(): 210 | self.segmentationData.userModified["endTime"] = self.getTime() 211 | self.segmentationData._modifiedLabel = labelNode 212 | else: 213 | self.segmentationData.userModified = None 214 | if not self.preopSegmentationPath: 215 | self.createDirectory(self.preopSegmentationPath) 216 | segmentedColorName = self.getSetting("Segmentation_Color_Name", moduleName=self.MODULE_NAME) 217 | timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") 218 | filename = getpass.getuser() + '-' + segmentedColorName + '-' + timestamp 219 | self.saveNodeData(labelNode, self.preopSegmentationPath, extension=FileExtension.NRRD, name=filename) 220 | self.loadPreProcessedData() 221 | customStatusProgressBar = CustomStatusProgressbar() 222 | customStatusProgressBar.text = "Done automatic prostate segmentation" 223 | 224 | @vtk.calldata_type(vtk.VTK_OBJECT) 225 | def onSegmentationFinished(self, caller, event, labelNode): 226 | self.segmentationData.endTime = self.getTime() 227 | self.segmentationData._label = labelNode 228 | segmentationValidator = SliceTrackerSegmentationValidatorPlugin(self.data.initialVolume, labelNode) 229 | segmentationValidator.addEventObserver(segmentationValidator.StartedEvent, lambda c, e: self.invokeEvent(e)) 230 | segmentationValidator.addEventObserver(segmentationValidator.ModifiedEvent, self.onSegmentationModificationStarted) 231 | segmentationValidator.addEventObserver(segmentationValidator.FinishedEvent, self.onSegmentationValidated) 232 | segmentationValidator.addEventObserver(segmentationValidator.CanceledEvent, 233 | lambda c, e: self.onPreopLoadingFailed("Segmentation validation canceled.")) 234 | segmentationValidator.run() 235 | 236 | def onSegmentationModificationStarted(self, caller, event): 237 | self.segmentationData.setModified(startTime=self.getTime()) 238 | 239 | def isMpReviewStudyDirectoryValid(self, resourcesDir): 240 | if not os.path.exists(resourcesDir): 241 | logging.debug("The selected directory `{}` does not fit the mpReview directory structure. Make sure that you select " 242 | "the study root directory which includes directories RESOURCES".format(resourcesDir)) 243 | return False 244 | return True 245 | 246 | def findPreopImageAndSegmentationPaths(self, resourcesDir): 247 | from mpReview import mpReviewLogic 248 | seriesMap, _ = mpReviewLogic.loadMpReviewProcessedData(resourcesDir) 249 | 250 | regex = self.getSetting("PLANNING_IMAGE_PATTERN", moduleName=self.MODULE_NAME) 251 | 252 | for series in seriesMap: 253 | seriesName = str(seriesMap[series]['LongName']) 254 | logging.debug('series Number ' + series + ' ' + seriesName) 255 | 256 | xmlFile = os.path.join(seriesMap[series]['NRRDLocation']).replace(".nrrd", ".xml") 257 | segmentationsPath = os.path.join(os.path.dirname(os.path.dirname(xmlFile)), 'Segmentations') 258 | 259 | dom = xml.dom.minidom.parse(xmlFile) 260 | seriesDescription = self.findElement(dom, "SeriesDescription") 261 | if re.match(regex, seriesDescription) or seriesDescription == regex: 262 | return seriesMap[series]['NRRDLocation'], segmentationsPath 263 | 264 | raise NoEligibleSeriesFoundError("No eligible series found for preop AX T2 segmentation. MpReview might not have " 265 | "processed the right series or series names are different from BWH internally used ones") 266 | 267 | def loadT2Label(self): 268 | if self.data.initialLabel and self.data.initialLabel.GetScene() is not None: 269 | return True 270 | mostRecentFilename = self.getMostRecentWholeGlandSegmentation(self.preopSegmentationPath) 271 | success = False 272 | if mostRecentFilename: 273 | filename = os.path.join(self.preopSegmentationPath, mostRecentFilename) 274 | success, self.data.initialLabel = slicer.util.loadLabelVolume(filename, returnNode=True) 275 | if success: 276 | self.data.initialLabel.SetName('t2-label') 277 | return success 278 | 279 | def loadPreopVolume(self): 280 | if self.data.initialVolume and self.data.initialVolume.GetScene() is not None: 281 | return True 282 | success, self.data.initialVolume = slicer.util.loadVolume(self.preopImagePath, returnNode=True) 283 | if success: 284 | self.data.initialVolume.SetName('VOLUME-PREOP') 285 | return success 286 | 287 | def loadPreopTargets(self): 288 | if self.data.initialTargets and self.data.initialTargets.GetScene() is not None: 289 | return True 290 | if not os.path.exists(self.data.initialTargetsPath): 291 | return False 292 | mostRecentTargets = self.getMostRecentTargetsFile(self.data.initialTargetsPath) 293 | success = False 294 | if mostRecentTargets: 295 | filename = os.path.join(self.data.initialTargetsPath, mostRecentTargets) 296 | success, self.data.initialTargets = slicer.util.loadMarkupsFiducialList(filename, returnNode=True) 297 | if success: 298 | self.data.initialTargets.SetName('initialTargets') 299 | self.data.initialTargets.SetLocked(True) 300 | return success 301 | 302 | def getMostRecentWholeGlandSegmentation(self, path): 303 | return self.getMostRecentFile(path, FileExtension.NRRD, filter="WholeGland") 304 | 305 | def getMostRecentTargetsFile(self, path): 306 | return self.getMostRecentFile(path, FileExtension.FCSV) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/SliceTrackerUtils/steps/__init__.py -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/base.py: -------------------------------------------------------------------------------- 1 | import vtk 2 | from SlicerDevelopmentToolboxUtils.decorators import beforeRunProcessEvents, onModuleSelected 3 | from SlicerDevelopmentToolboxUtils.module.base import WidgetBase 4 | from SlicerDevelopmentToolboxUtils.module.logic import SessionBasedLogicBase 5 | 6 | from ..constants import SliceTrackerConstants as constants 7 | from ..session import SliceTrackerSession 8 | 9 | 10 | class SliceTrackerWidgetBase(WidgetBase): 11 | 12 | MODULE_NAME = constants.MODULE_NAME 13 | 14 | SessionClass = SliceTrackerSession 15 | 16 | AvailableLayoutsChangedEvent = vtk.vtkCommand.UserEvent + 4233 17 | 18 | def __init__(self): 19 | super(SliceTrackerWidgetBase, self).__init__() 20 | 21 | def setup(self): 22 | self.setupSliceWidgets() 23 | self.setupAdditionalViewSettingButtons() 24 | 25 | def setupSliceWidgets(self): 26 | self.createSliceWidgetClassMembers("Red") 27 | self.createSliceWidgetClassMembers("Yellow") 28 | self.createSliceWidgetClassMembers("Green") 29 | 30 | def setupAdditionalViewSettingButtons(self): 31 | pass 32 | 33 | def addSessionObservers(self): 34 | super(SliceTrackerWidgetBase, self).addSessionObservers() 35 | self.session.addEventObserver(self.session.NewImageSeriesReceivedEvent, self.onNewImageSeriesReceived) 36 | self.session.addEventObserver(self.session.CurrentSeriesChangedEvent, self.onCurrentSeriesChanged) 37 | self.session.addEventObserver(self.session.LoadingMetadataSuccessfulEvent, self.onLoadingMetadataSuccessful) 38 | self.session.addEventObserver(self.session.PreprocessingSuccessfulEvent, self.onPreprocessingSuccessful) 39 | 40 | def removeSessionEventObservers(self): 41 | super(SliceTrackerWidgetBase, self).removeSessionEventObservers() 42 | self.session.removeEventObserver(self.session.NewImageSeriesReceivedEvent, self.onNewImageSeriesReceived) 43 | self.session.removeEventObserver(self.session.CurrentSeriesChangedEvent, self.onCurrentSeriesChanged) 44 | self.session.removeEventObserver(self.session.LoadingMetadataSuccessfulEvent, self.onLoadingMetadataSuccessful) 45 | self.session.removeEventObserver(self.session.PreprocessingSuccessfulEvent, self.onPreprocessingSuccessful) 46 | 47 | def onActivation(self): 48 | self.layoutManager.layoutChanged.connect(self.onLayoutChanged) 49 | self.session.addEventObserver(self.session.CurrentResultChangedEvent, self.onCurrentResultChanged) 50 | super(SliceTrackerWidgetBase, self).onActivation() 51 | 52 | def onDeactivation(self): 53 | self.layoutManager.layoutChanged.disconnect(self.onLayoutChanged) 54 | self.session.removeEventObserver(self.session.CurrentResultChangedEvent, self.onCurrentResultChanged) 55 | super(SliceTrackerWidgetBase, self).onDeactivation() 56 | 57 | def onCurrentResultChanged(self, caller, event): 58 | pass 59 | 60 | @onModuleSelected(constants.MODULE_NAME) 61 | def onLayoutChanged(self, layout=None): 62 | pass 63 | 64 | def setAvailableLayouts(self, layouts): 65 | if not all([l in constants.ALLOWED_LAYOUTS for l in layouts]): 66 | raise ValueError("Not all of the delivered layouts are allowed to be used in SliceTracker") 67 | self.invokeEvent(self.AvailableLayoutsChangedEvent, str(layouts)) 68 | 69 | def addPlugin(self, plugin): 70 | super(SliceTrackerWidgetBase, self).addPlugin(plugin) 71 | plugin.addEventObserver(self.AvailableLayoutsChangedEvent, self.onPluginAvailableLayoutChanged) 72 | 73 | @vtk.calldata_type(vtk.VTK_STRING) 74 | def onPluginAvailableLayoutChanged(self, caller, event, callData): 75 | self.invokeEvent(self.AvailableLayoutsChangedEvent, callData) 76 | 77 | def resetViewSettingButtons(self): 78 | pass 79 | 80 | @vtk.calldata_type(vtk.VTK_STRING) 81 | def onNewImageSeriesReceived(self, caller, event, callData): 82 | pass 83 | 84 | @vtk.calldata_type(vtk.VTK_STRING) 85 | def onCurrentSeriesChanged(self, caller, event, callData=None): 86 | pass 87 | 88 | def onLoadingMetadataSuccessful(self, caller, event): 89 | pass 90 | 91 | def onPreprocessingSuccessful(self, caller, event): 92 | pass 93 | 94 | def setupFourUpView(self, volume, clearLabels=True): 95 | self.layoutManager.setLayout(constants.LAYOUT_FOUR_UP) 96 | self.setBackgroundToVolumeID(volume, clearLabels) 97 | 98 | def setBackgroundToVolumeID(self, volume, clearLabels=True, showLabelOutline=True): 99 | super(SliceTrackerWidgetBase, self).setBackgroundToVolumeID(volume, clearLabels, showLabelOutline) 100 | self.setDefaultOrientation() 101 | 102 | def setDefaultOrientation(self): 103 | self.redSliceNode.SetOrientationToAxial() 104 | self.yellowSliceNode.SetOrientationToSagittal() 105 | self.greenSliceNode.SetOrientationToCoronal() 106 | self.updateFOV() # TODO: shall not be called here 107 | 108 | def setAxialOrientation(self): 109 | for sliceNode in self._sliceNodes: 110 | sliceNode.SetOrientationToAxial() 111 | self.updateFOV() # TODO: shall not be called here 112 | 113 | def updateFOV(self): 114 | # if self.getSetting("COVER_TEMPLATE_PATTERN") in self.intraopSeriesSelector.currentText: 115 | # self.setDefaultFOV(self.redSliceLogic, 1.0) 116 | # self.setDefaultFOV(self.yellowSliceLogic, 1.0) 117 | # self.setDefaultFOV(self.greenSliceLogic, 1.0) 118 | # el 119 | if self.layoutManager.layout == constants.LAYOUT_RED_SLICE_ONLY: 120 | self.setDefaultFOV(self.redSliceLogic) 121 | elif self.layoutManager.layout == constants.LAYOUT_SIDE_BY_SIDE: 122 | self.setDefaultFOV(self.redSliceLogic) 123 | self.setDefaultFOV(self.yellowSliceLogic) 124 | elif self.layoutManager.layout in [constants.LAYOUT_FOUR_UP_QUANTITATIVE, constants.LAYOUT_FOUR_UP]: 125 | self.setDefaultFOV(self.redSliceLogic) 126 | self.yellowSliceLogic.FitSliceToAll() 127 | self.greenSliceLogic.FitSliceToAll() 128 | 129 | @beforeRunProcessEvents 130 | def setDefaultFOV(self, sliceLogic, factor=0.5): 131 | sliceLogic.FitSliceToAll() 132 | FOV = sliceLogic.GetSliceNode().GetFieldOfView() 133 | self.setFOV(sliceLogic, [FOV[0] * factor, FOV[1] * factor, FOV[2]]) 134 | 135 | def setupRedSlicePreview(self, selectedSeries): 136 | self.layoutManager.setLayout(constants.LAYOUT_RED_SLICE_ONLY) 137 | self.hideAllFiducialNodes() 138 | try: 139 | result = self.session.data.getResultsBySeries(selectedSeries)[0] 140 | volume = result.volumes.fixed 141 | except IndexError: 142 | volume = self.session.getOrCreateVolumeForSeries(selectedSeries) 143 | self.setBackgroundToVolumeID(volume) 144 | 145 | 146 | class SliceTrackerStep(SliceTrackerWidgetBase): 147 | 148 | def __init__(self): 149 | self.viewSettingButtons = [] 150 | self.parameterNode.SetAttribute("Name", self.NAME) 151 | super(SliceTrackerStep, self).__init__() 152 | 153 | 154 | class SliceTrackerLogicBase(SessionBasedLogicBase): 155 | 156 | MODULE_NAME = constants.MODULE_NAME 157 | SessionClass = SliceTrackerSession 158 | 159 | def __init__(self): 160 | super(SliceTrackerLogicBase, self).__init__() 161 | 162 | 163 | class SliceTrackerPlugin(SliceTrackerWidgetBase): 164 | 165 | def __init__(self): 166 | super(SliceTrackerPlugin, self).__init__() 167 | 168 | def clearData(self): 169 | pass -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/evaluation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import qt 3 | import vtk 4 | import slicer 5 | from base import SliceTrackerLogicBase, SliceTrackerStep 6 | from ..constants import SliceTrackerConstants as constants 7 | from plugins.results import SliceTrackerRegistrationResultsPlugin 8 | from plugins.targets import SliceTrackerTargetTablePlugin 9 | from plugins.charts import SliceTrackerDisplacementChartPlugin 10 | 11 | from SlicerDevelopmentToolboxUtils.icons import Icons 12 | 13 | 14 | class SliceTrackerEvaluationStepLogic(SliceTrackerLogicBase): 15 | 16 | def __init__(self): 17 | super(SliceTrackerEvaluationStepLogic, self).__init__() 18 | 19 | 20 | class SliceTrackerEvaluationStep(SliceTrackerStep): 21 | 22 | NAME = "Evaluation" 23 | LogicClass = SliceTrackerEvaluationStepLogic 24 | LayoutClass = qt.QVBoxLayout 25 | 26 | def __init__(self): 27 | self.modulePath = os.path.dirname(slicer.util.modulePath(self.MODULE_NAME)).replace(".py", "") 28 | super(SliceTrackerEvaluationStep, self).__init__() 29 | self.consentGivenBy = None 30 | 31 | def setup(self): 32 | super(SliceTrackerEvaluationStep, self).setup() 33 | self.setupRegistrationValidationButtons() 34 | 35 | self.regResultsPlugin = SliceTrackerRegistrationResultsPlugin() 36 | self.addPlugin(self.regResultsPlugin) 37 | self.regResultsPlugin.addEventObserver(self.regResultsPlugin.RegistrationTypeSelectedEvent, 38 | self.onRegistrationTypeSelected) 39 | self.regResultsPlugin.addEventObserver(self.regResultsPlugin.NoRegistrationResultsAvailable, 40 | self.onNoRegistrationResultsAvailable) 41 | self.regResultsPlugin.addEventObserver(self.regResultsPlugin.RegistrationResultsAvailable, 42 | self.onRegistrationResultsAvailable) 43 | 44 | self.targetTablePlugin = SliceTrackerTargetTablePlugin(movingEnabled=True) 45 | self.addPlugin(self.targetTablePlugin) 46 | 47 | self.displacementChartPlugin = SliceTrackerDisplacementChartPlugin() 48 | self.addPlugin(self.displacementChartPlugin) 49 | 50 | self.displacementChartPlugin.addEventObserver(self.displacementChartPlugin.ShowEvent, self.onShowDisplacementChart) 51 | self.displacementChartPlugin.addEventObserver(self.displacementChartPlugin.HideEvent, self.onHideDisplacementChart) 52 | 53 | self.layout().addWidget(self.regResultsPlugin) 54 | self.layout().addWidget(self.targetTablePlugin) 55 | self.layout().addWidget(self.registrationEvaluationButtonsGroupBox) 56 | self.layout().addWidget(self.displacementChartPlugin.collapsibleButton) 57 | self.layout().addStretch(1) 58 | 59 | def setupRegistrationValidationButtons(self): 60 | iconSize = qt.QSize(36, 36) 61 | self.approveRegistrationResultButton = self.createButton("", icon=Icons.thumbs_up, iconSize=iconSize, 62 | toolTip="Approve") 63 | self.retryRegistrationButton = self.createButton("", icon=Icons.retry, iconSize=iconSize, toolTip="Retry") 64 | self.rejectRegistrationResultButton = self.createButton("", icon=Icons.thumbs_down, iconSize=iconSize, 65 | toolTip="Reject") 66 | self.registrationEvaluationButtonsGroupBox = self.createHLayout([self.retryRegistrationButton, 67 | self.approveRegistrationResultButton, 68 | self.rejectRegistrationResultButton]) 69 | 70 | def setupConnections(self): 71 | self.retryRegistrationButton.clicked.connect(self.onRetryRegistrationButtonClicked) 72 | self.approveRegistrationResultButton.clicked.connect(self.onApproveRegistrationResultButtonClicked) 73 | self.rejectRegistrationResultButton.clicked.connect(self.onRejectRegistrationResultButtonClicked) 74 | # self.registrationDetailsButton.clicked.connect(self.onShowRegistrationDetails) 75 | 76 | def addSessionObservers(self): 77 | super(SliceTrackerEvaluationStep, self).addSessionObservers() 78 | self.session.addEventObserver(self.session.InitiateEvaluationEvent, self.onInitiateEvaluation) 79 | 80 | def removeSessionEventObservers(self): 81 | super(SliceTrackerEvaluationStep, self).removeSessionEventObservers() 82 | self.session.removeEventObserver(self.session.InitiateEvaluationEvent, self.onInitiateEvaluation) 83 | 84 | def onShowDisplacementChart(self, caller, event): 85 | if self.layoutManager.layout != constants.LAYOUT_FOUR_UP_QUANTITATIVE: 86 | self.displacementChartPlugin.collapsibleButton.show() 87 | 88 | def onHideDisplacementChart(self, caller, event): 89 | self.displacementChartPlugin.collapsibleButton.hide() 90 | 91 | def onRetryRegistrationButtonClicked(self): 92 | self.session.retryRegistration() 93 | 94 | def onApproveRegistrationResultButtonClicked(self): 95 | self.consentGivenBy = self.session._getConsent() 96 | if self.consentGivenBy: 97 | results = self.session.data.getResultsBySeriesNumber(self.currentResult.seriesNumber) 98 | for result in [r for r in results if r is not self.currentResult]: 99 | result.reject(self.consentGivenBy) 100 | 101 | if self.session.seriesTypeManager.isCoverProstate(self.session.currentResult.name) and \ 102 | self.session.data.getMostRecentApprovedCoverProstateRegistration() is not None: 103 | self.session.data.getMostRecentApprovedCoverProstateRegistration().skip() 104 | 105 | self.currentResult.approve(registrationType=self.regResultsPlugin.registrationButtonGroup.checkedButton().name, 106 | consentedBy=self.consentGivenBy) 107 | 108 | def onRejectRegistrationResultButtonClicked(self): 109 | self.consentGivenBy = self.session._getConsent() 110 | if self.consentGivenBy: 111 | results = self.session.data.getResultsBySeriesNumber(self.currentResult.seriesNumber) 112 | for result in [r for r in results if r is not self.currentResult]: 113 | result.reject(consentedBy=self.consentGivenBy) 114 | self.currentResult.reject(consentedBy=self.consentGivenBy) 115 | 116 | def onInitiateEvaluation(self, caller, event): 117 | self.active = True 118 | 119 | def onActivation(self): 120 | super(SliceTrackerEvaluationStep, self).onActivation() 121 | self.consentGivenBy = None 122 | if not self.currentResult: 123 | return 124 | # self.redOnlyLayoutButton.enabled = False 125 | # self.sideBySideLayoutButton.enabled = True 126 | self.rejectRegistrationResultButton.enabled = not self.session.seriesTypeManager.isCoverProstate(self.currentResult.name) 127 | self.currentResult.save(self.session.outputDirectory) 128 | self.currentResult.printSummary() 129 | 130 | def onDeactivation(self): 131 | super(SliceTrackerEvaluationStep, self).onDeactivation() 132 | self.hideAllLabels() 133 | self.hideAllFiducialNodes() 134 | 135 | @vtk.calldata_type(vtk.VTK_STRING) 136 | def onRegistrationTypeSelected(self, caller, event, callData): 137 | self.targetTablePlugin.currentTargets = getattr(self.currentResult.targets, callData) 138 | 139 | def onNoRegistrationResultsAvailable(self, caller, event): 140 | self.targetTablePlugin.currentTargets = None 141 | self.approveRegistrationResultButton.enabled = False 142 | 143 | def onRegistrationResultsAvailable(self, caller, event): 144 | self.approveRegistrationResultButton.enabled = True 145 | self.targetTablePlugin.enabled = True -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/SliceTrackerUtils/steps/plugins/__init__.py -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/case.py: -------------------------------------------------------------------------------- 1 | import slicer 2 | import os 3 | import ctk 4 | import vtk 5 | import qt 6 | 7 | from ...helpers import NewCaseSelectionNameWidget 8 | from ..base import SliceTrackerPlugin, SliceTrackerLogicBase 9 | 10 | from SlicerDevelopmentToolboxUtils.helpers import WatchBoxAttribute 11 | from SlicerDevelopmentToolboxUtils.widgets import BasicInformationWatchBox 12 | from SlicerDevelopmentToolboxUtils.icons import Icons 13 | from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin 14 | 15 | 16 | class SliceTrackerCaseManagerLogic(SliceTrackerLogicBase): 17 | 18 | def __init__(self): 19 | super(SliceTrackerCaseManagerLogic, self).__init__() 20 | 21 | 22 | class SliceTrackerCaseManagerPlugin(SliceTrackerPlugin): 23 | 24 | LogicClass = SliceTrackerCaseManagerLogic 25 | NAME = "CaseManager" 26 | 27 | @property 28 | def caseRootDir(self): 29 | return self.casesRootDirectoryButton.directory 30 | 31 | @caseRootDir.setter 32 | def caseRootDir(self, path): 33 | try: 34 | exists = os.path.exists(path) 35 | except TypeError: 36 | exists = False 37 | self.setSetting('CasesRootLocation', path if exists else None) 38 | self.casesRootDirectoryButton.text = ModuleLogicMixin.truncatePath(path) if exists else "Choose output directory" 39 | self.casesRootDirectoryButton.toolTip = path 40 | self.openCaseButton.enabled = exists 41 | self.createNewCaseButton.enabled = exists 42 | 43 | def __init__(self): 44 | super(SliceTrackerCaseManagerPlugin, self).__init__() 45 | self.caseRootDir = self.getSetting('CasesRootLocation', self.MODULE_NAME) 46 | slicer.app.connect('aboutToQuit()', self.onSlicerQuits) 47 | 48 | def onSlicerQuits(self): 49 | if self.session.isRunning(): 50 | self.onCloseCaseButtonClicked() 51 | 52 | def clearData(self): 53 | self.update() 54 | 55 | def setup(self): 56 | super(SliceTrackerCaseManagerPlugin, self).setup() 57 | iconSize = qt.QSize(36, 36) 58 | self.createNewCaseButton = self.createButton("", icon=Icons.new, iconSize=iconSize, toolTip="Start a new case") 59 | self.openCaseButton = self.createButton("", icon=Icons.open, iconSize=iconSize, toolTip="Open case") 60 | self.closeCaseButton = self.createButton("", icon=Icons.exit, iconSize=iconSize, 61 | toolTip="Close case with resume support", enabled=False) 62 | self.setupCaseWatchBox() 63 | self.casesRootDirectoryButton = self.createDirectoryButton(text="Choose cases root location", 64 | caption="Choose cases root location", 65 | directory=self.getSetting('CasesRootLocation', 66 | self.MODULE_NAME)) 67 | self.caseDirectoryInformationArea = ctk.ctkCollapsibleButton() 68 | self.caseDirectoryInformationArea.collapsed = True 69 | self.caseDirectoryInformationArea.text = "Directory Settings" 70 | self.directoryConfigurationLayout = qt.QGridLayout(self.caseDirectoryInformationArea) 71 | self.directoryConfigurationLayout.addWidget(qt.QLabel("Cases Root Directory"), 1, 0, 1, 1) 72 | self.directoryConfigurationLayout.addWidget(self.casesRootDirectoryButton, 1, 1, 1, 1) 73 | self.directoryConfigurationLayout.addWidget(self.caseWatchBox, 2, 0, 1, qt.QSizePolicy.ExpandFlag) 74 | 75 | self.caseGroupBox = qt.QGroupBox("Case") 76 | self.caseGroupBoxLayout = qt.QFormLayout(self.caseGroupBox) 77 | self.caseGroupBoxLayout.addWidget(self.createHLayout([self.createNewCaseButton, self.openCaseButton, 78 | self.closeCaseButton])) 79 | self.caseGroupBoxLayout.addWidget(self.caseDirectoryInformationArea) 80 | self.layout().addWidget(self.caseGroupBox) 81 | 82 | def setupCaseWatchBox(self): 83 | watchBoxInformation = [WatchBoxAttribute('CurrentCaseDirectory', 'Directory'), 84 | WatchBoxAttribute('CurrentPreopDICOMDirectory', 'Preop DICOM Directory: '), 85 | WatchBoxAttribute('CurrentIntraopDICOMDirectory', 'Intraop DICOM Directory: '), 86 | WatchBoxAttribute('mpReviewDirectory', 'mpReview Directory: ')] 87 | self.caseWatchBox = BasicInformationWatchBox(watchBoxInformation, title="Current Case") 88 | 89 | def setupConnections(self): 90 | self.createNewCaseButton.clicked.connect(self.onCreateNewCaseButtonClicked) 91 | self.openCaseButton.clicked.connect(self.onOpenCaseButtonClicked) 92 | self.closeCaseButton.clicked.connect(self.onCloseCaseButtonClicked) 93 | self.casesRootDirectoryButton.directoryChanged.connect(lambda: setattr(self, "caseRootDir", 94 | self.casesRootDirectoryButton.directory)) 95 | 96 | def onCreateNewCaseButtonClicked(self): 97 | if not self.checkAndWarnUserIfCaseInProgress(): 98 | return 99 | self.caseDialog = NewCaseSelectionNameWidget(self.caseRootDir) 100 | selectedButton = self.caseDialog.exec_() 101 | if selectedButton == qt.QMessageBox.Ok: 102 | self.session.createNewCase(self.caseDialog.newCaseDirectory) 103 | 104 | def onOpenCaseButtonClicked(self): 105 | if not self.checkAndWarnUserIfCaseInProgress(): 106 | return 107 | self.session.directory = qt.QFileDialog.getExistingDirectory(self.parent().window(), "Select Case Directory", 108 | self.caseRootDir) 109 | 110 | def onCloseCaseButtonClicked(self): 111 | if not self.session.data.completed: 112 | dialog = qt.QMessageBox(qt.QMessageBox.Question, "SliceTracker", "Do you want to mark this case as completed?", qt.QMessageBox.Yes | qt.QMessageBox.No | qt.QMessageBox.Cancel, slicer.util.mainWindow(), qt.Qt.WindowStaysOnTopHint).exec_() 113 | if dialog == qt.QMessageBox.Yes: 114 | self.session.complete() 115 | elif dialog == qt.QMessageBox.Cancel: 116 | return 117 | if self.session.isRunning(): 118 | self.session.close(save=False) 119 | 120 | def onNewCaseStarted(self, caller, event): 121 | self.update() 122 | 123 | def onCaseOpened(self, caller, event): 124 | self.update() 125 | 126 | def update(self): 127 | self.updateCaseButtons() 128 | self.updateCaseWatchBox() 129 | 130 | @vtk.calldata_type(vtk.VTK_STRING) 131 | def onCaseClosed(self, caller, event, callData): 132 | self.clearData() 133 | 134 | def onLoadingMetadataSuccessful(self, caller, event): 135 | self.updateCaseButtons() 136 | 137 | def updateCaseWatchBox(self): 138 | if not self.session.isRunning(): 139 | self.caseWatchBox.reset() 140 | return 141 | self.caseWatchBox.setInformation("CurrentCaseDirectory", os.path.relpath(self.session.directory, self.caseRootDir), 142 | toolTip=self.session.directory) 143 | self.caseWatchBox.setInformation("CurrentPreopDICOMDirectory", os.path.relpath(self.session.preopDICOMDirectory, 144 | self.caseRootDir), 145 | toolTip=self.session.preopDICOMDirectory) 146 | self.caseWatchBox.setInformation("CurrentIntraopDICOMDirectory", os.path.relpath(self.session.intraopDICOMDirectory, 147 | self.caseRootDir), 148 | toolTip=self.session.intraopDICOMDirectory) 149 | self.caseWatchBox.setInformation("mpReviewDirectory", os.path.relpath(self.session.preprocessedDirectory, 150 | self.caseRootDir), 151 | toolTip=self.session.preprocessedDirectory) 152 | 153 | def updateCaseButtons(self): 154 | self.closeCaseButton.enabled = self.session.directory is not None 155 | 156 | def checkAndWarnUserIfCaseInProgress(self): 157 | if self.session.isRunning(): 158 | if not slicer.util.confirmYesNoDisplay("Current case will be closed. Do you want to proceed?"): 159 | return False 160 | self.session.close() 161 | return True -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/charts.py: -------------------------------------------------------------------------------- 1 | import vtk 2 | import qt 3 | import ctk 4 | import ast 5 | import slicer 6 | import logging 7 | 8 | from ..base import SliceTrackerPlugin, SliceTrackerLogicBase, SliceTrackerStep 9 | from ...session import SliceTrackerSession 10 | from ...sessionData import RegistrationResult 11 | from ...constants import SliceTrackerConstants as constants 12 | 13 | from SlicerDevelopmentToolboxUtils.decorators import onModuleSelected 14 | 15 | 16 | class SliceTrackerDisplacementChartLogic(SliceTrackerLogicBase): 17 | 18 | def __init__(self): 19 | super(SliceTrackerDisplacementChartLogic, self).__init__() 20 | self.session = SliceTrackerSession() 21 | 22 | def calculateTargetDisplacement(self, prevTargets, currTargets, targetIndex): 23 | prevPos = [0.0, 0.0, 0.0] 24 | currPos = [0.0, 0.0, 0.0] 25 | currTargets.GetNthFiducialPosition(targetIndex, currPos) 26 | prevTargets.GetNthFiducialPosition(targetIndex, prevPos) 27 | displacement = [currPos[0] - prevPos[0], currPos[1] - prevPos[1], currPos[2] - prevPos[2]] 28 | return displacement 29 | 30 | def isTargetDisplacementChartDisplayable(self, selectedSeries): 31 | if not selectedSeries or not (self.session.seriesTypeManager.isCoverProstate(selectedSeries) or 32 | self.session.seriesTypeManager.isGuidance(selectedSeries)) or \ 33 | self.session.data.registrationResultWasRejected(selectedSeries): 34 | return False 35 | selectedSeriesNumber = RegistrationResult.getSeriesNumberFromString(selectedSeries) 36 | approvedResults = sorted([r for r in self.session.data.registrationResults.values() if r.approved], 37 | key=lambda result: result.seriesNumber) 38 | nonSelectedApprovedResults = filter(lambda x: x.seriesNumber != selectedSeriesNumber, approvedResults) 39 | if len(nonSelectedApprovedResults) == 0 or self.session.currentResult is None: 40 | return False 41 | return True 42 | 43 | 44 | class SliceTrackerDisplacementChartPlugin(SliceTrackerPlugin): 45 | 46 | ShowEvent = vtk.vtkCommand.UserEvent + 561 47 | HideEvent = vtk.vtkCommand.UserEvent + 562 48 | 49 | NAME = "DisplacementChart" 50 | LogicClass = SliceTrackerDisplacementChartLogic 51 | 52 | PLOT_COLOR_LR = [213/255.0, 94/255.0, 0] 53 | PLOT_COLOR_PA = [0, 114/255.0, 178/255.0] 54 | PLOT_COLOR_IS = [204/255.0, 121/255.0, 167/255.0] 55 | PLOT_COLOR_3D = [0, 0, 0] 56 | PLOT_NAMES = ["L/R Displacement", "P/A Displacement", "I/S Displacement", "3-D Distance"] 57 | 58 | @property 59 | def plotWidgetViewNode(self): 60 | if not self._plotWidget: 61 | return None 62 | return self._plotWidget.mrmlPlotViewNode() 63 | 64 | def __init__(self): 65 | super(SliceTrackerDisplacementChartPlugin, self).__init__() 66 | 67 | @onModuleSelected(SliceTrackerStep.MODULE_NAME) 68 | def onMrmlSceneCleared(self, caller=None, event=None, callData=None): 69 | self.resetAndInitializeData() 70 | 71 | def resetChart(self): 72 | for arr in [self.arrX, self.arrXD, self.arrYD, self.arrZD, self.arrD]: 73 | arr.Initialize() 74 | 75 | def setup(self): 76 | super(SliceTrackerDisplacementChartPlugin, self).setup() 77 | 78 | self.collapsibleButton = ctk.ctkCollapsibleButton() 79 | self.collapsibleButton.text = "Plotting" 80 | self.collapsibleButton.collapsed = 0 81 | self.collapsibleButton.setLayout(qt.QGridLayout()) 82 | 83 | self.resetAndInitializeData() 84 | 85 | self.showLegendCheckBox = qt.QCheckBox('Show legend') 86 | self.showLegendCheckBox.setChecked(1) 87 | 88 | self.collapsibleButton.layout().addWidget(self.showLegendCheckBox, 0, 0) 89 | self.layout().addWidget(self.collapsibleButton) 90 | 91 | def resetAndInitializeData(self): 92 | self._plotView = None 93 | self._plotWidget = None 94 | self._plotViewNode = None 95 | self._plotSeriesNodes = [] 96 | 97 | self._initializeChartTable() 98 | self._initializePlotChartNode() 99 | self._initializePlotView() 100 | self._initializePlotWidgets() 101 | 102 | def _initializeChartTable(self): 103 | self._chartTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", "Target Displacement") 104 | 105 | def _initializePlotChartNode(self): 106 | self._plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode") 107 | self._plotChartNode.SetXAxisTitle('Series Number') 108 | self._plotChartNode.SetYAxisTitle('Displacement') 109 | 110 | def createFloatArray(name): 111 | floatArray = vtk.vtkFloatArray() 112 | floatArray.SetName(name) 113 | self._chartTable.AddColumn(floatArray) 114 | return floatArray 115 | 116 | self.arrX = createFloatArray('X Axis') 117 | self.arrXD = createFloatArray('L/R Displ') 118 | self.arrYD = createFloatArray('P/A Displ') 119 | self.arrZD = createFloatArray('I/S Displ') 120 | self.arrD = createFloatArray('3-D Dis') 121 | 122 | def _initializePlotView(self): 123 | if self._plotView: 124 | self.collapsibleButton.layout().removeWidget(self._plotView) 125 | self._plotView = slicer.qMRMLPlotView() 126 | self._plotView.setMinimumSize(400, 200) 127 | self._plotView.setMRMLScene(slicer.mrmlScene) 128 | self._plotView.show() 129 | self.collapsibleButton.layout().addWidget(self._plotView, 1, 0) 130 | 131 | def _initializePlotWidgets(self): 132 | if self.layoutManager.layout == constants.LAYOUT_FOUR_UP_QUANTITATIVE: 133 | self._plotWidget = self.layoutManager.plotWidget(0) \ 134 | if self.layoutManager.plotWidget(0).isVisible() else self.layoutManager.plotWidget(1) 135 | self._updatePlotViewNodes() 136 | 137 | def _updatePlotViewNodes(self): 138 | self._initializePlotViewNode() 139 | if self.plotWidgetViewNode: 140 | self.plotWidgetViewNode.SetPlotChartNodeID(self._plotChartNode.GetID()) 141 | 142 | def _initializePlotViewNode(self): 143 | if self._plotViewNode is None: 144 | self._plotViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode") 145 | self._plotView.setMRMLPlotViewNode(self._plotViewNode) 146 | self._plotViewNode.SetPlotChartNodeID(self._plotChartNode.GetID()) 147 | 148 | def _initializeChart(self, coverProstateSeriesNumber): 149 | self.arrX.InsertNextValue(coverProstateSeriesNumber) 150 | for arr in [self.arrXD, self.arrYD, self.arrZD, self.arrD]: 151 | arr.InsertNextValue(0) 152 | 153 | def setupConnections(self): 154 | self.showLegendCheckBox.connect('stateChanged(int)', self.onShowLegendChanged) 155 | 156 | def onActivation(self): 157 | super(SliceTrackerDisplacementChartPlugin, self).onActivation() 158 | defaultLayout = getattr(constants, self.getSetting("DEFAULT_EVALUATION_LAYOUT"), constants.LAYOUT_SIDE_BY_SIDE) 159 | if defaultLayout != self.layoutManager.layout: 160 | self.layoutManager.setLayout(defaultLayout) 161 | else: 162 | self.onLayoutChanged(defaultLayout) 163 | 164 | def onDeactivation(self): 165 | super(SliceTrackerDisplacementChartPlugin, self).onDeactivation() 166 | if self.plotWidgetViewNode: 167 | self.plotWidgetViewNode.SetPlotChartNodeID(None) 168 | 169 | def onShowLegendChanged(self, checked): 170 | self._plotChartNode.SetLegendVisibility(True if checked == 2 else False) 171 | 172 | def addSessionObservers(self): 173 | super(SliceTrackerDisplacementChartPlugin, self).addSessionObservers() 174 | self.session.addEventObserver(self.session.TargetSelectionEvent, self.onTargetSelectionChanged) 175 | 176 | def removeSessionEventObservers(self): 177 | super(SliceTrackerDisplacementChartPlugin, self).removeSessionEventObservers() 178 | self.session.removeEventObserver(self.session.TargetSelectionEvent, self.onTargetSelectionChanged) 179 | 180 | @onModuleSelected(SliceTrackerPlugin.MODULE_NAME) 181 | def onLayoutChanged(self, layout=None): 182 | self.collapsibleButton.visible = layout != constants.LAYOUT_FOUR_UP_QUANTITATIVE 183 | if not self.collapsibleButton.visible: 184 | self._initializePlotWidgets() 185 | 186 | @vtk.calldata_type(vtk.VTK_STRING) 187 | def onTargetSelectionChanged(self, caller, event, callData): 188 | info = ast.literal_eval(callData) 189 | targetsAvailable = info['nodeId'] and info['index'] != -1 190 | if targetsAvailable: 191 | self.targetIndex = info['index'] 192 | self.currResultTargets = slicer.mrmlScene.GetNodeByID(info['nodeId']) 193 | logging.debug(info) 194 | self.updateTargetDisplacementChart(targetsAvailable) 195 | 196 | def updateTargetDisplacementChart(self, targetsAvailable): 197 | if self.logic.isTargetDisplacementChartDisplayable(self.session.currentSeries) and targetsAvailable: 198 | self.resetChart() 199 | results = sorted([r for r in self.session.data.registrationResults.values() if r.approved], 200 | key=lambda result: result.seriesNumber) 201 | if not self.session.currentResult.wasEvaluated(): 202 | results.append(self.session.currentResult) 203 | for currIndex, currResult in enumerate(results[1:], 1): 204 | prevTargets = results[currIndex - 1].targets.approved 205 | if not self.session.currentResult.wasEvaluated() and currIndex == len(results[1:]): 206 | currTargets = self.currResultTargets 207 | else: 208 | currTargets = currResult.targets.approved 209 | displacement = self.logic.calculateTargetDisplacement(prevTargets, currTargets, self.targetIndex) 210 | self.invokeEvent(self.ShowEvent) 211 | self.addPlotPoints([displacement], currResult.seriesNumber) 212 | self.invokeEvent(self.ShowEvent) 213 | else: 214 | self.invokeEvent(self.HideEvent) 215 | 216 | def addPlotPoints(self, displacement, seriesNumber): 217 | numCurrentRows = self._chartTable.GetNumberOfRows() 218 | self._plotViewNode.SetPlotChartNodeID(self._plotChartNode.GetID()) 219 | 220 | for i in range(len(displacement)): 221 | if numCurrentRows == 0: 222 | self._initializeChart(self.session.data.getMostRecentApprovedCoverProstateRegistration().seriesNumber) 223 | self.arrX.InsertNextValue(seriesNumber) 224 | for component, arr in enumerate([self.arrXD, self.arrYD, self.arrZD]): 225 | arr.InsertNextValue(displacement[i][component]) 226 | distance = (displacement[i][0] ** 2 + displacement[i][1] ** 2 + displacement[i][2] ** 2) ** 0.5 227 | self.arrD.InsertNextValue(distance) 228 | if self._plotChartNode.GetNumberOfPlotSeriesNodes() == 0: 229 | for index, plot in enumerate([self.PLOT_COLOR_LR, self.PLOT_COLOR_PA, self.PLOT_COLOR_IS, self.PLOT_COLOR_3D], start=1): 230 | self.createPlot(plot, index) 231 | else: 232 | self._plotChartNode.RemoveAllPlotSeriesNodeIDs() 233 | for plotSeriesNode in self._plotSeriesNodes: 234 | self._plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode.GetID()) 235 | 236 | def createPlot(self, color, plotNumber): 237 | plotSeriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.PLOT_NAMES[plotNumber - 1]) 238 | plotSeriesNode.SetAndObserveTableNodeID(self._chartTable.GetID()) 239 | plotSeriesNode.SetXColumnName(self._chartTable.GetColumnName(0)) 240 | plotSeriesNode.SetYColumnName(self._chartTable.GetColumnName(plotNumber)) 241 | plotSeriesNode.SetColor(color) 242 | plotSeriesNode.SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatter) 243 | plotSeriesNode.SetMarkerStyle(4) 244 | plotSeriesNode.SetMarkerSize(3 * plotSeriesNode.GetLineWidth()) 245 | 246 | self._plotSeriesNodes.append(plotSeriesNode) 247 | self._plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode.GetID()) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/needle.py: -------------------------------------------------------------------------------- 1 | # predefined python file for needle segmentation -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/segmentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/SliceTrackerUtils/steps/plugins/segmentation/__init__.py -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/segmentation/automatic.py: -------------------------------------------------------------------------------- 1 | import vtk 2 | 3 | from base import SliceTrackerSegmentationPluginBase 4 | from ....algorithms.automaticProstateSegmentation import AutomaticSegmentationLogic 5 | 6 | class SliceTrackerAutomaticSegmentationPlugin(SliceTrackerSegmentationPluginBase): 7 | 8 | NAME = "AutomaticSegmentation" 9 | ALGORITHM_TYPE ="Automatic" 10 | LogicClass = AutomaticSegmentationLogic 11 | 12 | def __init__(self): 13 | super(SliceTrackerAutomaticSegmentationPlugin, self).__init__() 14 | self.logic.addEventObserver(self.logic.DeepLearningStartedEvent, self._onSegmentationStarted) 15 | self.logic.addEventObserver(self.logic.DeepLearningFinishedEvent, self._onSegmentationFinished) 16 | # self.logic.addEventObserver(self.logic.DeepLearningStatusChangedEvent, self.onStatusChanged) 17 | self.logic.addEventObserver(self.logic.DeepLearningFailedEvent, self._onSegmentationFailed) 18 | 19 | def cleanup(self): 20 | super(SliceTrackerAutomaticSegmentationPlugin, self).cleanup() 21 | self.logic.cleanup() 22 | 23 | def setup(self): 24 | super(SliceTrackerAutomaticSegmentationPlugin, self).setup() 25 | 26 | def onActivation(self): 27 | if str(self.getSetting("Use_Deep_Learning")).lower() == 'true': 28 | self.startSegmentation() 29 | 30 | def startSegmentation(self): 31 | self.logic.run(self.session.fixedVolume, domain='BWH_WITHOUT_ERC', colorNode=self.session.mpReviewColorNode) 32 | 33 | @vtk.calldata_type(vtk.VTK_OBJECT) 34 | def _onSegmentationFinished(self, caller, event, labelNode): 35 | # self.onStatusChanged(None, None, str({'text': "Labelmap prediction created", 'value': 100})) 36 | super(SliceTrackerAutomaticSegmentationPlugin, self)._onSegmentationFinished(caller, event, labelNode) 37 | 38 | # @vtk.calldata_type(vtk.VTK_STRING) 39 | # def onStatusChanged(self, caller, event, callData): 40 | # from SlicerDevelopmentToolboxUtils.widgets import CustomStatusProgressbar 41 | # statusBar = CustomStatusProgressbar() 42 | # if not statusBar.visible: 43 | # statusBar.show() 44 | # import ast 45 | # status = ast.literal_eval(str(callData)) 46 | # self.updateProgressBar(progress=statusBar, text=status["text"].replace("\n", ""), value=status["value"], 47 | # maximum = 100) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/segmentation/base.py: -------------------------------------------------------------------------------- 1 | import vtk 2 | from ...base import SliceTrackerPlugin 3 | 4 | 5 | class SliceTrackerSegmentationPluginBase(SliceTrackerPlugin): 6 | 7 | SegmentationStartedEvent = vtk.vtkCommand.UserEvent + 435 8 | SegmentationFinishedEvent = vtk.vtkCommand.UserEvent + 436 9 | SegmentationFailedEvent = vtk.vtkCommand.UserEvent + 437 10 | 11 | def __init__(self): 12 | super(SliceTrackerSegmentationPluginBase, self).__init__() 13 | self.reset() 14 | 15 | def reset(self): 16 | self.startTime = None 17 | self.endTime = None 18 | 19 | def startSegmentation(self): 20 | raise NotImplementedError 21 | 22 | def _onSegmentationStarted(self, caller, event): 23 | self.reset() 24 | self.startTime = self.getTime() 25 | self.invokeEvent(self.SegmentationStartedEvent) 26 | 27 | @vtk.calldata_type(vtk.VTK_OBJECT) 28 | def _onSegmentationFinished(self, caller, event, labelNode): 29 | self.endTime = self.getTime() 30 | self.invokeEvent(self.SegmentationFinishedEvent, labelNode) 31 | 32 | def _onSegmentationFailed(self, caller, event): 33 | self.reset() 34 | self.invokeEvent(self.SegmentationFailedEvent) 35 | -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/segmentation/manual.py: -------------------------------------------------------------------------------- 1 | import qt 2 | import vtk 3 | import slicer 4 | 5 | from ...base import SliceTrackerPlugin, SliceTrackerLogicBase 6 | from ....constants import SliceTrackerConstants as constants 7 | from base import SliceTrackerSegmentationPluginBase 8 | from SurfaceCutToLabel import SurfaceCutToLabelWidget 9 | 10 | from SlicerDevelopmentToolboxUtils.decorators import onModuleSelected 11 | 12 | 13 | class SliceTrackerManualSegmentationLogic(SliceTrackerLogicBase): 14 | 15 | def __init__(self): 16 | super(SliceTrackerManualSegmentationLogic, self).__init__() 17 | 18 | 19 | class SliceTrackerManualSegmentationPlugin(SliceTrackerSegmentationPluginBase): 20 | 21 | NAME = "ManualSegmentation" 22 | ALGORITHM_TYPE ="Manual" 23 | LogicClass = SliceTrackerManualSegmentationLogic 24 | 25 | SegmentationCanceledEvent = SliceTrackerSegmentationPluginBase.SegmentationFailedEvent 26 | 27 | @property 28 | def segmentModelNode(self): 29 | return self.surfaceCutToLabelWidget.logic.segmentModelNode 30 | 31 | @property 32 | def inputMarkupNode(self): 33 | return self.surfaceCutToLabelWidget.logic.inputMarkupNode 34 | 35 | def __init__(self): 36 | super(SliceTrackerManualSegmentationPlugin, self).__init__() 37 | 38 | def setup(self): 39 | super(SliceTrackerManualSegmentationPlugin, self).setup() 40 | 41 | self.surfaceCutGroupBox = qt.QWidget() 42 | self.surfaceCutGroupBox.setLayout(qt.QVBoxLayout()) 43 | self.surfaceCutToLabelWidget = SurfaceCutToLabelWidget(self.surfaceCutGroupBox) 44 | self.surfaceCutToLabelWidget.setup() 45 | self.surfaceCutToLabelWidget.selectorsGroupBoxVisible = False 46 | self.surfaceCutToLabelWidget.colorGroupBoxVisible = False 47 | 48 | if str(self.getSetting('DeveloperMode', 'Developer')).lower() == 'true': 49 | self.surfaceCutToLabelWidget.reloadCollapsibleButton.hide() 50 | 51 | self.segmentationGroupBox = qt.QGroupBox("SurfaceCut Segmentation") 52 | self.segmentationGroupBoxLayout = qt.QGridLayout() 53 | self.segmentationGroupBox.setLayout(self.segmentationGroupBoxLayout) 54 | self.segmentationGroupBoxLayout.addWidget(self.surfaceCutGroupBox, 0, 0) 55 | self.layout().addWidget(self.segmentationGroupBox) 56 | 57 | @onModuleSelected(SliceTrackerPlugin.MODULE_NAME) 58 | def onLayoutChanged(self, layout=None): 59 | self._refreshSegmentModelViewNodes() 60 | 61 | def _refreshSegmentModelViewNodes(self): 62 | sliceNodes = [self.yellowSliceNode] if self.layoutManager.layout == constants.LAYOUT_SIDE_BY_SIDE else \ 63 | [self.redSliceNode, self.yellowSliceNode, self.greenSliceNode] 64 | nodes = [self.surfaceCutToLabelWidget.logic.segmentModelNode, self.surfaceCutToLabelWidget.logic.inputMarkupNode] 65 | for node in [n for n in nodes if n]: 66 | self.refreshViewNodeIDs(node, sliceNodes) 67 | 68 | def onActivation(self): 69 | super(SliceTrackerManualSegmentationPlugin, self).onActivation() 70 | self.surfaceCutToLabelWidget.logic.colorNode = self.session.mpReviewColorNode 71 | self.surfaceCutToLabelWidget.colorSpin.setValue(self.session.segmentedLabelValue) 72 | self.surfaceCutToLabelWidget.imageVolumeSelector.setCurrentNode(self.session.fixedVolume) 73 | self._addSurfaceCutEventObservers() 74 | if str(self.getSetting("Use_Deep_Learning")).lower() == 'false': 75 | self.surfaceCutToLabelWidget.quickSegmentationButton.checked = True 76 | 77 | def onDeactivation(self): 78 | super(SliceTrackerManualSegmentationPlugin, self).onDeactivation() 79 | self._removeSurfaceCutEventObservers() 80 | 81 | def _addSurfaceCutEventObservers(self): 82 | self.surfaceCutToLabelWidget.addEventObserver(self.surfaceCutToLabelWidget.SegmentationStartedEvent, 83 | self._onSegmentationStarted) 84 | self.surfaceCutToLabelWidget.addEventObserver(self.surfaceCutToLabelWidget.SegmentationCanceledEvent, 85 | self._onSegmentationFailed) 86 | self.surfaceCutToLabelWidget.addEventObserver(self.surfaceCutToLabelWidget.SegmentationFinishedEvent, 87 | self._onSegmentationFinished) 88 | 89 | def _removeSurfaceCutEventObservers(self): 90 | self.surfaceCutToLabelWidget.removeEventObserver(self.surfaceCutToLabelWidget.SegmentationStartedEvent, 91 | self._onSegmentationStarted) 92 | self.surfaceCutToLabelWidget.removeEventObserver(self.surfaceCutToLabelWidget.SegmentationCanceledEvent, 93 | self._onSegmentationFailed) 94 | self.surfaceCutToLabelWidget.removeEventObserver(self.surfaceCutToLabelWidget.SegmentationFinishedEvent, 95 | self._onSegmentationFinished) 96 | 97 | def _onSegmentationStarted(self, caller, event): 98 | if str(self.getSetting("Use_Deep_Learning")).lower() == 'true': 99 | if not self._preCheckExistingSegmentation(): 100 | return 101 | else: 102 | labelVolume = self.surfaceCutToLabelWidget.labelVolume 103 | if labelVolume and not labelVolume.GetName().endswith("_modified"): 104 | clonedLabelNode = self.logic.volumesLogic.CloneVolume(slicer.mrmlScene, labelVolume, 105 | labelVolume.GetName()+"_modified") 106 | self.surfaceCutToLabelWidget.labelVolume = clonedLabelNode 107 | self.setupFourUpView(self.session.fixedVolume) 108 | self.setDefaultOrientation() 109 | super(SliceTrackerManualSegmentationPlugin, self)._onSegmentationStarted(caller, event) 110 | 111 | def _preCheckExistingSegmentation(self): 112 | if slicer.util.confirmYesNoDisplay("The automatic segmentation will be overwritten. Do you want to proceed?", 113 | windowTitle="SliceTracker"): 114 | return True 115 | self.surfaceCutToLabelWidget.deactivateQuickSegmentationMode(cancelled=True) 116 | return False 117 | 118 | @vtk.calldata_type(vtk.VTK_OBJECT) 119 | def _onSegmentationFinished(self, caller, event, labelNode): 120 | displayNode = labelNode.GetDisplayNode() 121 | displayNode.SetAndObserveColorNodeID(self.session.mpReviewColorNode.GetID()) 122 | self.surfaceCutToLabelWidget.colorSpin.setValue(self.session.segmentedLabelValue) 123 | super(SliceTrackerManualSegmentationPlugin, self)._onSegmentationFinished(caller, event, labelNode) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/segmentationValidator.py: -------------------------------------------------------------------------------- 1 | import ctk 2 | import qt 3 | import slicer 4 | import logging 5 | 6 | from SlicerDevelopmentToolboxUtils.events import SlicerDevelopmentToolboxEvents 7 | from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin, ModuleLogicMixin 8 | from SlicerDevelopmentToolboxUtils.icons import Icons 9 | from SlicerDevelopmentToolboxUtils.decorators import logmethod 10 | 11 | 12 | class SliceTrackerSegmentationValidatorPlugin(qt.QDialog, ModuleWidgetMixin): 13 | 14 | StartedEvent = SlicerDevelopmentToolboxEvents.StartedEvent 15 | ModifiedEvent = SlicerDevelopmentToolboxEvents.StatusChangedEvent 16 | FinishedEvent = SlicerDevelopmentToolboxEvents.FinishedEvent 17 | CanceledEvent = SlicerDevelopmentToolboxEvents.CanceledEvent 18 | 19 | def __init__(self, inputVolume, labelNode): 20 | qt.QDialog.__init__(self) 21 | self.className = self.__class__.__name__ 22 | self.setWindowTitle("Validate Segmentation") 23 | self.setWindowFlags(qt.Qt.WindowStaysOnTopHint) 24 | self.volumeNode = inputVolume 25 | self.labelNode = labelNode 26 | self._initializeMembers() 27 | self.setup() 28 | 29 | def _initializeMembers(self): 30 | self.segmentationModified = False 31 | self.observedSegmentation = None 32 | 33 | def setup(self): 34 | self.setLayout(qt.QGridLayout()) 35 | self.setupSliceWidget() 36 | 37 | iconSize = qt.QSize(36, 36) 38 | self.modifySegmentButton = self.createButton("Modify Segmentation", icon=slicer.modules.segmenteditor.icon, 39 | iconSize=iconSize) 40 | self.confirmSegmentButton = self.createButton("Confirm Segmentation", icon=Icons.apply, iconSize=iconSize) 41 | self.cancelButton = self.createButton("Cancel", icon=Icons.cancel, iconSize=iconSize) 42 | 43 | self.setupSegmentEditor() 44 | self.layout().addWidget(self.segmentEditorWidget, 0, 0, 2, 1) 45 | self.layout().addWidget(self.sliceWidget, 0, 1) 46 | self.layout().addWidget(self.createHLayout([self.modifySegmentButton,self.confirmSegmentButton,self.cancelButton]), 47 | 1, 1) 48 | self.setupConnections() 49 | 50 | def setupConnections(self): 51 | self.modifySegmentButton.clicked.connect(self.onModifySegmentButtonClicked) 52 | self.confirmSegmentButton.clicked.connect(self.onConfirmSegmentButtonClicked) 53 | self.cancelButton.clicked.connect(lambda pushed: self.close()) 54 | 55 | def setupSliceWidget(self): 56 | self.sliceNode = slicer.mrmlScene.CreateNodeByClass('vtkMRMLSliceNode') 57 | self.sliceNode.SetName("Black") 58 | self.sliceNode.SetLayoutName("Black") 59 | self.sliceNode.SetLayoutLabel("BL") 60 | self.sliceNode.SetOrientationToAxial() 61 | slicer.mrmlScene.AddNode(self.sliceNode) 62 | self.sliceWidget = self.layoutManager.viewWidget(self.sliceNode) 63 | 64 | self.sliceLogic = slicer.app.applicationLogic().GetSliceLogic(self.sliceNode) 65 | self.sliceNode.SetMappedInLayout(1) 66 | 67 | def setupSegmentEditor(self): 68 | self.segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() 69 | self.segmentEditorWidget.setMRMLScene(slicer.mrmlScene) 70 | self.segmentEditorWidget.visible = False 71 | self.segmentEditorWidget.setSegmentationNodeSelectorVisible(False) 72 | self.segmentEditorWidget.setMasterVolumeNodeSelectorVisible(False) 73 | self.segmentEditorWidget.setSwitchToSegmentationsButtonVisible(False) 74 | self.segmentEditorWidget.findChild(qt.QPushButton, "AddSegmentButton").hide() 75 | self.segmentEditorWidget.findChild(qt.QPushButton, "RemoveSegmentButton").hide() 76 | self.segmentEditorWidget.findChild(ctk.ctkMenuButton, "Show3DButton").hide() 77 | self.segmentEditorWidget.findChild(ctk.ctkExpandableWidget, "SegmentsTableResizableFrame").hide() 78 | self.segmentEditorWidget.setSizePolicy(qt.QSizePolicy.Maximum, qt.QSizePolicy.Expanding) 79 | 80 | def _initializeSegmentationNode(self): 81 | self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") 82 | self.segmentationNode.CreateDefaultDisplayNodes() 83 | self.segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(self.volumeNode) 84 | 85 | def _initializeSegmentEditorNode(self): 86 | self.segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentEditorNode") 87 | self.segmentEditorWidget.setMRMLSegmentEditorNode(self.segmentEditorNode) 88 | 89 | @logmethod(logging.DEBUG) 90 | def run(self): 91 | self.invokeEvent(self.StartedEvent) 92 | self.segmentationModified = False 93 | self._initializeSegmentationNode() 94 | self._initializeSegmentEditorNode() 95 | 96 | self.sliceNode.SetMappedInLayout(1) 97 | self.sliceLogic.GetSliceCompositeNode().SetBackgroundVolumeID(self.volumeNode.GetID()) 98 | self.sliceLogic.GetSliceCompositeNode().SetLabelVolumeID(self.labelNode.GetID()) 99 | self.sliceLogic.FitSliceToAll() 100 | self.sliceNode.RotateToVolumePlane(self.volumeNode) 101 | self.sliceNode.SetUseLabelOutline(True) 102 | self.resize(int(slicer.util.mainWindow().width/3*2), int(slicer.util.mainWindow().height/3*2)) 103 | result = self.exec_() 104 | if result == qt.QDialog.Rejected: 105 | logging.debug("{}: Dialog got rejected.".format(self.className)) 106 | self.invokeEvent(self.CanceledEvent) 107 | elif result == qt.QDialog.Accepted: 108 | logging.debug("{}: Dialog got approved.".format(self.className)) 109 | self.invokeEvent(self.FinishedEvent, self.labelNode) 110 | self.cleanup() 111 | 112 | def onModifySegmentButtonClicked(self): 113 | self.invokeEvent(self.ModifiedEvent) 114 | slicer.modules.segmentations.logic().ImportLabelmapToSegmentationNode(self.labelNode, self.segmentationNode) 115 | segmentID = self.segmentationNode.GetSegmentation().GetNthSegment(0).GetName() 116 | segmentationDisplayNode = self.segmentationNode.GetDisplayNode() 117 | segmentationDisplayNode.SetSegmentVisibility2DFill(segmentID, False) 118 | self.segmentEditorWidget.setSegmentationNode(self.segmentationNode) 119 | self.segmentEditorWidget.setMasterVolumeNode(self.volumeNode) 120 | self.addSegmentationObserver(self.segmentationNode) 121 | self.segmentEditorWidget.show() 122 | self.modifySegmentButton.hide() 123 | 124 | def onConfirmSegmentButtonClicked(self): 125 | if self.segmentationModified is True: 126 | volumesLogic = slicer.modules.volumes.logic() 127 | clonedLabelNode = volumesLogic.CloneVolume(slicer.mrmlScene, self.labelNode, 128 | self.labelNode.GetName() + "_modified") 129 | self.labelNode = clonedLabelNode 130 | slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode(self.segmentationNode, self.labelNode) 131 | ModuleLogicMixin.runBRAINSResample(inputVolume=self.labelNode, referenceVolume=self.volumeNode, 132 | outputVolume=self.labelNode) 133 | self.accept() 134 | 135 | @logmethod(logging.DEBUG) 136 | def addSegmentationObserver(self, segmentation): 137 | import vtkSegmentationCorePython as vtkSegmentationCore 138 | self.observedSegmentation = segmentation 139 | self.segmentObserver = self.observedSegmentation.AddObserver( 140 | vtkSegmentationCore.vtkSegmentation.RepresentationModified, 141 | self.onSegmentModified) 142 | 143 | @logmethod(logging.DEBUG) 144 | def removeSegmentationObserver(self): 145 | if self.observedSegmentation: 146 | self.observedSegmentation.RemoveObserver(self.segmentObserver) 147 | self.segmentObserver = None 148 | 149 | def onSegmentModified(self, caller, event): 150 | self.segmentationModified = True 151 | 152 | @logmethod(logging.DEBUG) 153 | def cleanup(self): 154 | self.removeSegmentationObserver() 155 | if self.observedSegmentation: 156 | slicer.mrmlScene.RemoveNode(self.segmentationNode) 157 | slicer.mrmlScene.RemoveNode(self.segmentEditorNode) 158 | self.observedSegmentation = None 159 | self.segmentEditorWidget.hide() 160 | self.modifySegmentButton.show() -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/targeting.py: -------------------------------------------------------------------------------- 1 | import qt 2 | from ...constants import SliceTrackerConstants as constants 3 | from ..base import SliceTrackerPlugin, SliceTrackerLogicBase 4 | from targets import SliceTrackerTargetTablePlugin 5 | 6 | from SlicerDevelopmentToolboxUtils.helpers import SliceAnnotation 7 | from SlicerDevelopmentToolboxUtils.widgets import TargetCreationWidget 8 | 9 | 10 | class SliceTrackerTargetingLogic(SliceTrackerLogicBase): 11 | 12 | def __init__(self): 13 | super(SliceTrackerTargetingLogic, self).__init__() 14 | 15 | 16 | class SliceTrackerTargetingPlugin(SliceTrackerPlugin): 17 | 18 | LogicClass = SliceTrackerTargetingLogic 19 | 20 | NAME = "Targeting" 21 | TargetingStartedEvent = TargetCreationWidget.StartedEvent 22 | TargetingFinishedEvent = TargetCreationWidget.FinishedEvent 23 | 24 | @property 25 | def title(self): 26 | return self.targetingGroupBox.title 27 | 28 | @title.setter 29 | def title(self, value): 30 | self.targetingGroupBox.setTitle(value) 31 | 32 | def __init__(self, **kwargs): 33 | super(SliceTrackerTargetingPlugin, self).__init__() 34 | self._processKwargs(**kwargs) 35 | 36 | def setup(self): 37 | super(SliceTrackerTargetingPlugin, self).setup() 38 | 39 | self._setupTargetCreationWidget() 40 | 41 | self.preopTargetTableGroupBox, self.preopTargetTablePlugin = \ 42 | self._createTargetTableGroupBox("Pre-operative Targets") 43 | self.intraopTargetTableGroupBox, self.intraopTargetTablePlugin = \ 44 | self._createTargetTableGroupBox("Intra-operative Targets", 45 | additionalComponents=[self.targetCreationWidget, 46 | self.targetCreationWidget.buttons]) 47 | 48 | self.targetingGroupBox = qt.QGroupBox("Target Placement") 49 | self.targetingGroupBox.setLayout(qt.QFormLayout()) 50 | self.targetingGroupBox.layout().addRow(self.preopTargetTableGroupBox) 51 | self.targetingGroupBox.layout().addRow(self.intraopTargetTableGroupBox) 52 | self.layout().addWidget(self.targetingGroupBox, 1, 0, 2, 2) 53 | 54 | def _createTargetTableGroupBox(self, title, additionalComponents=None): 55 | additionalComponents = additionalComponents if additionalComponents else [] 56 | groupbox = qt.QGroupBox(title) 57 | groupbox.setAlignment(qt.Qt.AlignCenter) 58 | groupbox.setLayout(qt.QFormLayout()) 59 | groupbox.setAlignment(qt.Qt.AlignCenter) 60 | targetTablePlugin = SliceTrackerTargetTablePlugin() 61 | self.addPlugin(targetTablePlugin) 62 | groupbox.layout().addRow(targetTablePlugin) 63 | for c in additionalComponents: 64 | groupbox.layout().addRow(c) 65 | return groupbox, targetTablePlugin 66 | 67 | def _setupTargetCreationWidget(self): 68 | self.targetCreationWidget = TargetCreationWidget(DEFAULT_FIDUCIAL_LIST_NAME="IntraopTargets", 69 | ICON_SIZE=qt.QSize(36, 36)) 70 | self.targetCreationWidget.addEventObserver(self.TargetingStartedEvent, self._onTargetingStarted) 71 | self.targetCreationWidget.addEventObserver(self.TargetingFinishedEvent, self._onTargetingFinished) 72 | 73 | def preopAvailableAndTargetsDefined(self): 74 | return self.session.data.usePreopData and self.session.movingTargets and not self.session.retryMode 75 | 76 | def onActivation(self): 77 | super(SliceTrackerTargetingPlugin, self).onActivation() 78 | self._setFiducialWidgetVisible(True) 79 | 80 | if self.preopAvailableAndTargetsDefined(): 81 | self._setFiducialWidgetVisible(False) 82 | self.targetCreationWidget.currentNode = None 83 | self.preopTargetTablePlugin.currentTargets = self.session.movingTargets 84 | else: 85 | approvedCoverProstate = self.session.data.getMostRecentApprovedCoverProstateRegistration() 86 | if (approvedCoverProstate is not None 87 | and self.session.seriesTypeManager.isCoverProstate(self.session.currentSeries)): 88 | clone = self.logic.cloneFiducials(approvedCoverProstate.targets.approved, "IntraopTargets") 89 | self.targetCreationWidget.currentNode = clone 90 | self.intraopTargetTablePlugin.currentTargets = self.session.movingTargets 91 | self.session.movingTargets = clone 92 | self.setFiducialNodeVisibility(clone, True) 93 | self.session.applyDefaultTargetDisplayNode(clone) 94 | self.preopTargetTableGroupBox.visible = False 95 | 96 | self.targetingGroupBox.visible = not self.session.retryMode 97 | 98 | def onDeactivation(self): 99 | super(SliceTrackerTargetingPlugin, self).onDeactivation() 100 | self.targetCreationWidget.reset() 101 | self._removeSliceAnnotations() 102 | 103 | def startTargeting(self): 104 | self.targetCreationWidget.startPlacing() 105 | 106 | def _setFiducialWidgetVisible(self, visible): 107 | self.targetCreationWidget.visible = visible 108 | self.preopTargetTableGroupBox.visible = not visible and self.preopAvailableAndTargetsDefined() 109 | self.intraopTargetTablePlugin.visible = not visible 110 | 111 | def _onTargetingStarted(self, caller, event): 112 | self._addSliceAnnotations() 113 | self.targetCreationWidget.show() 114 | self.intraopTargetTablePlugin.visible = False 115 | self.setupFourUpView(self.session.currentSeriesVolume, clearLabels=False) 116 | self.invokeEvent(self.TargetingStartedEvent) 117 | 118 | def _addSliceAnnotations(self): 119 | self._removeSliceAnnotations() 120 | widgets = [self.yellowWidget] if self.layoutManager.layout == constants.LAYOUT_SIDE_BY_SIDE else \ 121 | [self.redWidget, self.yellowWidget, self.greenWidget] 122 | for widget in widgets: 123 | self.sliceAnnotations.append(SliceAnnotation(widget, "Targeting Mode", opacity=0.5, 124 | verticalAlign="top", horizontalAlign="center")) 125 | 126 | def _removeSliceAnnotations(self): 127 | self.sliceAnnotations = getattr(self, "sliceAnnotations", []) 128 | for annotation in self.sliceAnnotations: 129 | annotation.remove() 130 | self.sliceAnnotations = [] 131 | 132 | def _onTargetingFinished(self, caller, event): 133 | self._removeSliceAnnotations() 134 | if self.targetCreationWidget.hasTargetListAtLeastOneTarget(): 135 | if not self.preopAvailableAndTargetsDefined(): 136 | self.session.movingTargets = self.targetCreationWidget.currentNode 137 | self.session.setupPreopLoadedTargets() 138 | else: 139 | self.session.temporaryIntraopTargets = self.targetCreationWidget.currentNode 140 | self._setFiducialWidgetVisible(False) 141 | self.intraopTargetTablePlugin.currentTargets = self.targetCreationWidget.currentNode 142 | else: 143 | if not self.preopAvailableAndTargetsDefined(): 144 | self.session.movingTargets = None 145 | 146 | self.invokeEvent(self.TargetingFinishedEvent) -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/plugins/training.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ast 3 | import shutil 4 | import qt 5 | import vtk 6 | import ctk 7 | import slicer 8 | 9 | from ...constants import SliceTrackerConstants 10 | from ..base import SliceTrackerPlugin 11 | 12 | from SlicerDevelopmentToolboxUtils.helpers import SampleDataDownloader 13 | from SlicerDevelopmentToolboxUtils.decorators import * 14 | 15 | 16 | class SliceTrackerTrainingPlugin(SliceTrackerPlugin): 17 | 18 | NAME = "Training" 19 | 20 | def __init__(self): 21 | super(SliceTrackerTrainingPlugin, self).__init__() 22 | self.sampleDownloader = SampleDataDownloader(True) 23 | 24 | def setup(self): 25 | super(SliceTrackerTrainingPlugin, self).setup() 26 | self.collapsibleTrainingArea = ctk.ctkCollapsibleButton() 27 | self.collapsibleTrainingArea.collapsed = True 28 | self.collapsibleTrainingArea.text = "Training Incoming Data Simulation" 29 | 30 | self.simulatePreopPhaseButton = self.createButton("Simulate preop reception", enabled=False) 31 | self.simulateIntraopPhaseButton = self.createButton("Simulate intraop reception", enabled=False) 32 | 33 | self.trainingsAreaLayout = qt.QGridLayout(self.collapsibleTrainingArea) 34 | self.trainingsAreaLayout.addWidget(self.createHLayout([self.simulatePreopPhaseButton, 35 | self.simulateIntraopPhaseButton])) 36 | self.layout().addWidget(self.collapsibleTrainingArea) 37 | 38 | def setupConnections(self): 39 | self.simulatePreopPhaseButton.clicked.connect(self.startPreopPhaseSimulation) 40 | self.simulateIntraopPhaseButton.clicked.connect(self.startIntraopPhaseSimulation) 41 | 42 | def addSessionObservers(self): 43 | super(SliceTrackerTrainingPlugin, self).addSessionObservers() 44 | self.session.addEventObserver(self.session.IncomingDataSkippedEvent, self.onIncomingDataSkipped) 45 | 46 | def removeSessionEventObservers(self): 47 | super(SliceTrackerTrainingPlugin, self).removeSessionEventObservers() 48 | self.session.removeEventObserver(self.session.IncomingDataSkippedEvent, self.onIncomingDataSkipped) 49 | 50 | def startPreopPhaseSimulation(self): 51 | self.session.trainingMode = True 52 | if self.session.preopDICOMReceiver.dicomReceiver.isRunning(): 53 | self.session.preopDICOMReceiver.dicomReceiver.stopStoreSCP() 54 | self.simulatePreopPhaseButton.enabled = False 55 | preopZipFile = self.initiateSampleDataDownload(SliceTrackerConstants.PREOP_SAMPLE_DATA_URL) 56 | if not self.sampleDownloader.wasCanceled() and preopZipFile: 57 | self.unzipFileAndCopyToDirectory(preopZipFile, self.session.preopDICOMDirectory) 58 | 59 | def startIntraopPhaseSimulation(self): 60 | self.simulateIntraopPhaseButton.enabled = False 61 | intraopZipFile = self.initiateSampleDataDownload(SliceTrackerConstants.INTRAOP_SAMPLE_DATA_URL) 62 | if not self.sampleDownloader.wasCanceled() and intraopZipFile: 63 | self.unzipFileAndCopyToDirectory(intraopZipFile, self.session.intraopDICOMDirectory) 64 | 65 | def initiateSampleDataDownload(self, url): 66 | filename = os.path.basename(url) 67 | self.sampleDownloader.resetAndInitialize() 68 | self.sampleDownloader.addEventObserver(self.sampleDownloader.StatusChangedEvent, self.onDownloadProgressUpdated) 69 | # self.customStatusProgressBar.show() 70 | downloadedFile = self.sampleDownloader.downloadFileIntoCache(url, filename) 71 | # self.customStatusProgressBar.hide() 72 | return None if self.sampleDownloader.wasCanceled() else downloadedFile 73 | 74 | @onReturnProcessEvents 75 | @vtk.calldata_type(vtk.VTK_STRING) 76 | def onDownloadProgressUpdated(self, caller, event, callData): 77 | message, percent = ast.literal_eval(callData) 78 | logging.info("%s, %s" %(message, percent)) 79 | # self.customStatusProgressBar.updateStatus(message, percent) 80 | 81 | def unzipFileAndCopyToDirectory(self, filepath, copyToDirectory): 82 | import zipfile 83 | try: 84 | zip_ref = zipfile.ZipFile(filepath, 'r') 85 | destination = filepath.replace(os.path.basename(filepath), "") 86 | logging.debug("extracting to %s " % destination) 87 | zip_ref.extractall(destination) 88 | zip_ref.close() 89 | self.copyDirectory(filepath.replace(".zip", ""), copyToDirectory) 90 | except zipfile.BadZipfile as exc: 91 | if self.session.preopDICOMReceiver: 92 | self.session.preopDICOMReceiver.hide() 93 | slicer.util.errorDisplay("An error appeared while extracting %s. If the file is corrupt, please delete it and try " 94 | "again." % filepath, detailedText=str(exc.message)) 95 | self.clearData() 96 | 97 | def copyDirectory(self, source, destination, recursive=True): 98 | print source 99 | assert os.path.isdir(source) 100 | for listObject in os.listdir(source): 101 | current = os.path.join(source, listObject) 102 | if os.path.isdir(current) and recursive: 103 | self.copyDirectory(current, destination, recursive) 104 | else: 105 | shutil.copy(current, destination) 106 | 107 | @logmethod(logging.DEBUG) 108 | def onNewCaseStarted(self, caller, event): 109 | self.simulatePreopPhaseButton.enabled = True 110 | 111 | def onIncomingDataSkipped(self, caller, event): 112 | self.simulatePreopPhaseButton.enabled = False 113 | self.simulateIntraopPhaseButton.enabled = True 114 | 115 | @vtk.calldata_type(vtk.VTK_STRING) 116 | def onCaseClosed(self, caller, event, callData): 117 | self.simulatePreopPhaseButton.enabled = False 118 | self.simulateIntraopPhaseButton.enabled = False 119 | 120 | def onPreprocessingSuccessful(self, caller, event): 121 | self.simulateIntraopPhaseButton.enabled = self.session.trainingMode -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/steps/segmentation.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import qt 3 | import slicer 4 | import vtk 5 | 6 | from base import SliceTrackerLogicBase, SliceTrackerStep 7 | from ..constants import SliceTrackerConstants as constants 8 | from ..sessionData import SegmentationData 9 | 10 | from plugins.targeting import SliceTrackerTargetingPlugin 11 | from plugins.segmentation.manual import SliceTrackerManualSegmentationPlugin 12 | from plugins.segmentation.automatic import SliceTrackerAutomaticSegmentationPlugin 13 | 14 | from SlicerDevelopmentToolboxUtils.helpers import SliceAnnotation 15 | from SlicerDevelopmentToolboxUtils.decorators import onModuleSelected 16 | from SlicerDevelopmentToolboxUtils.icons import Icons 17 | 18 | 19 | class SliceTrackerSegmentationStepLogic(SliceTrackerLogicBase): 20 | 21 | def __init__(self): 22 | super(SliceTrackerSegmentationStepLogic, self).__init__() 23 | 24 | def inputsAreSet(self): 25 | if self.session.data.usePreopData: 26 | return self.session.movingVolume and self.session.fixedVolume and self.session.movingLabel and \ 27 | self.session.fixedLabel and self.session.movingTargets 28 | else: 29 | return self.session.fixedVolume and self.session.fixedLabel and self.session.movingTargets 30 | 31 | def loadInitialData(self): 32 | self.session.movingLabel = self.session.data.initialLabel 33 | self.session.movingVolume = self.session.data.initialVolume 34 | self.session.movingTargets = self.session.data.initialTargets 35 | 36 | 37 | class SliceTrackerSegmentationStep(SliceTrackerStep): 38 | 39 | NAME = "Segmentation" 40 | LogicClass = SliceTrackerSegmentationStepLogic 41 | LayoutClass = qt.QVBoxLayout 42 | 43 | def __init__(self): 44 | super(SliceTrackerSegmentationStep, self).__init__() 45 | self.session.retryMode = False 46 | self.segmentationData = None 47 | 48 | def setup(self): 49 | super(SliceTrackerSegmentationStep, self).setup() 50 | self._setupTargetingPlugin() 51 | self._setupManualSegmentationPlugin() 52 | self._setupAutomaticSegmentationPlugin() 53 | self._setupNavigationButtons() 54 | self.layout().addWidget(self.manualSegmentationPlugin) 55 | self.layout().addWidget(self.createHLayout([self.backButton, self.finishStepButton])) 56 | self.layout().addWidget(self.targetingPlugin) 57 | self.layout().addStretch(1) 58 | 59 | def _setupTargetingPlugin(self): 60 | self.targetingPlugin = SliceTrackerTargetingPlugin() 61 | self.targetingPlugin.addEventObserver(self.targetingPlugin.TargetingStartedEvent, self._onTargetingStarted) 62 | self.targetingPlugin.addEventObserver(self.targetingPlugin.TargetingFinishedEvent, self._onTargetingFinished) 63 | self.addPlugin(self.targetingPlugin) 64 | 65 | def _setupManualSegmentationPlugin(self): 66 | self.manualSegmentationPlugin = SliceTrackerManualSegmentationPlugin() 67 | self.manualSegmentationPlugin.addEventObserver(self.manualSegmentationPlugin.SegmentationStartedEvent, 68 | self._onSegmentationStarted) 69 | self.manualSegmentationPlugin.addEventObserver(self.manualSegmentationPlugin.SegmentationCanceledEvent, 70 | self._onSegmentationCanceled) 71 | self.manualSegmentationPlugin.addEventObserver(self.manualSegmentationPlugin.SegmentationFinishedEvent, 72 | self._onManualSegmentationFinished) 73 | self.manualSegmentationPlugin.surfaceCutToLabelWidget.segmentEditorButtonVisible = False 74 | self.addPlugin(self.manualSegmentationPlugin) 75 | 76 | def _setupAutomaticSegmentationPlugin(self): 77 | self.automaticSegmentationPlugin = SliceTrackerAutomaticSegmentationPlugin() 78 | self.automaticSegmentationPlugin.addEventObserver(self.automaticSegmentationPlugin.SegmentationStartedEvent, 79 | self._onAutomaticSegmentationStarted) 80 | self.automaticSegmentationPlugin.addEventObserver(self.automaticSegmentationPlugin.SegmentationFailedEvent, 81 | self._onSegmentationFailed) 82 | self.automaticSegmentationPlugin.addEventObserver(self.automaticSegmentationPlugin.SegmentationFinishedEvent, 83 | self._onAutomaticSegmentationFinished) 84 | self.addPlugin(self.automaticSegmentationPlugin) 85 | 86 | def _setupNavigationButtons(self): 87 | iconSize = qt.QSize(36, 36) 88 | self.backButton = self.createButton("", icon=Icons.back, iconSize=iconSize, 89 | toolTip="Return to last step") 90 | self.finishStepButton = self.createButton("", icon=Icons.start, iconSize=iconSize, 91 | toolTip="Run Registration/Finish Step") 92 | 93 | def setupConnections(self): 94 | super(SliceTrackerSegmentationStep, self).setupConnections() 95 | self.backButton.clicked.connect(self._onBackButtonClicked) 96 | self.finishStepButton.clicked.connect(self._onFinishStepButtonClicked) 97 | 98 | def addSessionObservers(self): 99 | super(SliceTrackerSegmentationStep, self).addSessionObservers() 100 | self.session.addEventObserver(self.session.InitiateSegmentationEvent, self.onInitiateSegmentation) 101 | 102 | def removeSessionEventObservers(self): 103 | super(SliceTrackerSegmentationStep, self).removeSessionEventObservers() 104 | self.session.removeEventObserver(self.session.InitiateSegmentationEvent, self.onInitiateSegmentation) 105 | 106 | def onActivation(self): 107 | self.segmentationData = None 108 | self.finishStepButton.enabled = False 109 | self.session.fixedVolume = self.session.currentSeriesVolume 110 | if not self.session.fixedVolume: 111 | return 112 | self._updateAvailableLayouts() 113 | super(SliceTrackerSegmentationStep, self).onActivation() 114 | 115 | def onDeactivation(self): 116 | super(SliceTrackerSegmentationStep, self).onDeactivation() 117 | self._removeMissingPreopDataAnnotation() 118 | 119 | @onModuleSelected(SliceTrackerStep.MODULE_NAME) 120 | def onLayoutChanged(self, layout=None): 121 | if self.layoutManager.layout == constants.LAYOUT_SIDE_BY_SIDE: 122 | self._setupSideBySideSegmentationView() 123 | elif self.layoutManager.layout in [constants.LAYOUT_FOUR_UP, constants.LAYOUT_RED_SLICE_ONLY]: 124 | self._removeMissingPreopDataAnnotation() 125 | self.setBackgroundToVolumeID(self.session.currentSeriesVolume, clearLabels=False) 126 | 127 | @vtk.calldata_type(vtk.VTK_STRING) 128 | def onInitiateSegmentation(self, caller, event, callData): 129 | self._initiateSegmentation(ast.literal_eval(callData)) 130 | 131 | def _initiateSegmentation(self, retryMode=False): 132 | self.session.retryMode = retryMode 133 | self.finishStepButton.setEnabled(True if self.logic.inputsAreSet() else False) 134 | if self.session.seriesTypeManager.isCoverProstate(self.session.currentSeries): 135 | self.initializeCoverProstate() 136 | else: 137 | self._loadLatestCoverProstateResultData() 138 | self.active = True 139 | 140 | def initializeCoverProstate(self): 141 | if self.session.data.usePreopData: 142 | if self.session.retryMode: 143 | if not self._loadLatestCoverProstateResultData(): 144 | self.logic.loadInitialData() 145 | else: 146 | self.logic.loadInitialData() 147 | else: 148 | self.session.movingVolume = self.session.currentSeriesVolume 149 | 150 | def _loadLatestCoverProstateResultData(self): 151 | coverProstate = self.session.data.getMostRecentApprovedCoverProstateRegistration() 152 | if coverProstate: 153 | self.session.movingVolume = coverProstate.volumes.fixed 154 | self.session.movingLabel = coverProstate.labels.fixed 155 | self.session.movingTargets = coverProstate.targets.approved 156 | return True 157 | return False 158 | 159 | def _updateAvailableLayouts(self): 160 | layouts = [constants.LAYOUT_RED_SLICE_ONLY, constants.LAYOUT_FOUR_UP] 161 | if self.session.data.usePreopData or self.session.retryMode: 162 | layouts.append(constants.LAYOUT_SIDE_BY_SIDE) 163 | self.setAvailableLayouts(layouts) 164 | 165 | def _setupSideBySideSegmentationView(self): 166 | # TODO: red slice view should not be possible to set target 167 | coverProstate = self.session.data.getMostRecentApprovedCoverProstateRegistration() 168 | redVolume = coverProstate.volumes.fixed if coverProstate and self.session.retryMode else self.session.data.initialVolume 169 | redLabel = coverProstate.labels.fixed if coverProstate and self.session.retryMode else self.session.data.initialLabel 170 | 171 | if redVolume and redLabel: 172 | self.redCompositeNode.SetBackgroundVolumeID(redVolume.GetID()) 173 | self.redCompositeNode.SetLabelVolumeID(redLabel.GetID()) 174 | self.redCompositeNode.SetLabelOpacity(1) 175 | else: 176 | self.redCompositeNode.SetBackgroundVolumeID(None) 177 | self.redCompositeNode.SetLabelVolumeID(None) 178 | self._addMissingPreopDataAnnotation(self.redWidget) 179 | self.yellowCompositeNode.SetBackgroundVolumeID(self.session.currentSeriesVolume.GetID()) 180 | self.setAxialOrientation() 181 | 182 | if redVolume and redLabel: 183 | self.redSliceNode.SetUseLabelOutline(True) 184 | self.redSliceNode.RotateToVolumePlane(redVolume) 185 | 186 | def _addMissingPreopDataAnnotation(self, widget): 187 | self._removeMissingPreopDataAnnotation() 188 | self.noPreopSegmentationAnnotation = SliceAnnotation(widget, constants.MISSING_PREOP_ANNOTATION_TEXT, 189 | opacity=0.7, color=(1, 0, 0)) 190 | 191 | def _removeMissingPreopDataAnnotation(self): 192 | self.noPreopSegmentationAnnotation = getattr(self, "noPreopSegmentationAnnotation", None) 193 | if self.noPreopSegmentationAnnotation: 194 | self.noPreopSegmentationAnnotation.remove() 195 | self.noPreopSegmentationAnnotation = None 196 | 197 | def _onBackButtonClicked(self): 198 | if self.session.retryMode: 199 | self.session.retryMode = False 200 | if self.session.previousStep: 201 | self.session.previousStep.active = True 202 | 203 | def _onFinishStepButtonClicked(self): 204 | self.session.data.segmentModelNode = self.manualSegmentationPlugin.segmentModelNode 205 | self.session.data.inputMarkupNode = self.manualSegmentationPlugin.inputMarkupNode 206 | if not self.session.data.usePreopData and not self.session.retryMode: 207 | self._createCoverProstateRegistrationResultManually() 208 | else: 209 | self.session.onInvokeRegistration(initial=True, retryMode=self.session.retryMode, 210 | segmentationData=self.segmentationData) 211 | 212 | def _createCoverProstateRegistrationResultManually(self): 213 | fixedVolume = self.session.currentSeriesVolume 214 | result = self.session.generateNameAndCreateRegistrationResult(fixedVolume) 215 | approvedRegistrationType = "bSpline" 216 | result.targets.original = self.session.movingTargets 217 | targetName = str(result.seriesNumber) + '-TARGETS-' + approvedRegistrationType + result.suffix 218 | clone = self.logic.cloneFiducials(self.session.movingTargets, targetName) 219 | self.session.applyDefaultTargetDisplayNode(clone) 220 | result.setTargets(approvedRegistrationType, clone) 221 | result.volumes.fixed = fixedVolume 222 | result.labels.fixed = self.session.fixedLabel 223 | result.receivedTime = self.session.seriesTimeStamps[result.name.replace(result.suffix, "")] 224 | result.segmentationData = self.segmentationData 225 | 226 | if self.session.seriesTypeManager.isCoverProstate(self.session.currentResult.name) and \ 227 | self.session.data.getMostRecentApprovedCoverProstateRegistration() is not None: 228 | self.session.data.getMostRecentApprovedCoverProstateRegistration().skip() 229 | 230 | result.approve(approvedRegistrationType, consentedBy="Clinician") 231 | 232 | def _onAutomaticSegmentationStarted(self, caller, event): 233 | self.manualSegmentationPlugin.enabled = False 234 | self._onSegmentationStarted(caller, event) 235 | 236 | def _onSegmentationStarted(self, caller, event): 237 | # self.setAvailableLayouts([constants.LAYOUT_RED_SLICE_ONLY, constants.LAYOUT_SIDE_BY_SIDE, constants.LAYOUT_FOUR_UP]) 238 | self.targetingPlugin.enabled = False 239 | self.backButton.enabled = False 240 | self.finishStepButton.enabled = False 241 | 242 | def _onSegmentationCanceled(self, caller, event): 243 | # self.setAvailableLayouts([constants.LAYOUT_FOUR_UP]) 244 | self.layoutManager.setLayout(constants.LAYOUT_FOUR_UP) 245 | self.backButton.enabled = True 246 | self.targetingPlugin.enabled = True 247 | if self.logic.inputsAreSet(): 248 | self._displaySegmentationComparison() 249 | self.finishStepButton.setEnabled(1 if self.logic.inputsAreSet() else 0) # TODO: need to revise that 250 | 251 | def _onSegmentationFailed(self, caller, event): 252 | import logging 253 | logging.debug("Segmentation failed") 254 | self.setAvailableLayouts([constants.LAYOUT_FOUR_UP]) 255 | self.layoutManager.setLayout(constants.LAYOUT_FOUR_UP) 256 | self.backButton.enabled = True 257 | self.finishStepButton.setEnabled(False) 258 | 259 | @vtk.calldata_type(vtk.VTK_STRING) 260 | def onNewImageSeriesReceived(self, caller, event, callData): 261 | if not self.active: 262 | return 263 | newImageSeries = ast.literal_eval(callData) 264 | for series in reversed(newImageSeries): 265 | if self.session.seriesTypeManager.isCoverProstate(series): 266 | if series != self.session.currentSeries: 267 | if slicer.util.confirmYesNoDisplay("Another %s was received. Do you want to use this one?" 268 | % self.getSetting("COVER_PROSTATE_PATTERN")): 269 | self.session.currentSeries = series 270 | self.active = False 271 | self._initiateSegmentation() 272 | 273 | @vtk.calldata_type(vtk.VTK_OBJECT) 274 | def _onAutomaticSegmentationFinished(self, caller, event, labelNode): 275 | self.manualSegmentationPlugin.enabled = True 276 | surfaceCutToLabelWidget = self.manualSegmentationPlugin.surfaceCutToLabelWidget 277 | 278 | # segmentationsLogic = slicer.modules.segmentations.logic() 279 | # 280 | # segmentationNode = surfaceCutToLabelWidget.segmentationNode 281 | # map(lambda x: segmentationNode.RemoveSegment(x), surfaceCutToLabelWidget.getSegmentIDs()) 282 | # segmentationsLogic.ImportLabelmapToSegmentationNode(labelNode, segmentationNode) 283 | # surfaceCutToLabelWidget.configureSegmentVisibility() 284 | # surfaceCutToLabelWidget.segmentEditorWidgetButton.enabled = True 285 | 286 | surfaceCutToLabelWidget.imageVolume = self.session.currentSeriesVolume 287 | surfaceCutToLabelWidget.labelVolume = labelNode 288 | 289 | self.createSegmentationDataOrSetModified(self.automaticSegmentationPlugin, labelNode) 290 | self._onSegmentationFinished(caller, event, labelNode) 291 | 292 | def createSegmentationDataOrSetModified(self, plugin, labelNode): 293 | if not self.segmentationData or self.segmentationData.algorithm=="Manual": 294 | self.segmentationData = SegmentationData(segmentationType="Prostate",algorithm=plugin.ALGORITHM_TYPE, 295 | startTime=plugin.startTime, endTime=plugin.endTime, label=labelNode) 296 | else: 297 | self.segmentationData.setModified(startTime=plugin.startTime, endTime=plugin.endTime, label=labelNode) 298 | 299 | @vtk.calldata_type(vtk.VTK_OBJECT) 300 | def _onManualSegmentationFinished(self, caller, event, labelNode): 301 | self.createSegmentationDataOrSetModified(self.manualSegmentationPlugin, labelNode) 302 | self._onSegmentationFinished(caller, event, labelNode) 303 | 304 | @vtk.calldata_type(vtk.VTK_OBJECT) 305 | def _onSegmentationFinished(self, caller, event, labelNode): 306 | _, suffix = self.session.getRegistrationResultNameAndGeneratedSuffix(self.session.currentSeries) 307 | if labelNode.GetName().find(suffix) == -1 and not labelNode.GetName().endswith("_modified"): 308 | labelNode.SetName(labelNode.GetName() + suffix) 309 | self.session.fixedLabel = labelNode 310 | self.finishStepButton.setEnabled(1 if self.logic.inputsAreSet() else 0) 311 | self.backButton.enabled = True 312 | self._displaySegmentationComparison() 313 | 314 | def _displaySegmentationComparison(self): 315 | self.setAvailableLayouts([constants.LAYOUT_SIDE_BY_SIDE]) 316 | if self.session.data.usePreopData or self.session.retryMode: 317 | self.setAxialOrientation() 318 | self._removeMissingPreopDataAnnotation() 319 | self.targetingPlugin.enabled = True 320 | if self.session.data.usePreopData or self.session.retryMode: 321 | self.layoutManager.setLayout(constants.LAYOUT_SIDE_BY_SIDE) 322 | self._setBackgroundAndLabel("red", self.session.movingVolume, self.session.movingLabel) 323 | self._setBackgroundAndLabel("yellow", self.session.fixedVolume, self.session.fixedLabel) 324 | self._centerLabelsOnVisibleSliceWidgets() 325 | elif not self.session.movingTargets: 326 | self.targetingPlugin.startTargeting() 327 | else: 328 | for compositeNode, sliceNode in zip(self._compositeNodes, self._sliceNodes): 329 | compositeNode.SetLabelVolumeID(self.session.fixedLabel.GetID()) 330 | compositeNode.SetLabelOpacity(1) 331 | sliceNode.SetUseLabelOutline(True) 332 | 333 | def _setBackgroundAndLabel(self, viewName, volume, label): 334 | compositeNode = getattr(self, viewName+"CompositeNode") 335 | compositeNode.SetReferenceBackgroundVolumeID(volume.GetID()) 336 | compositeNode.SetLabelVolumeID(label.GetID()) 337 | compositeNode.SetLabelOpacity(1) 338 | sliceNode = getattr(self, viewName+"SliceNode") 339 | sliceNode.SetOrientationToAxial() 340 | sliceNode.RotateToVolumePlane(volume) 341 | sliceNode.SetUseLabelOutline(True) 342 | 343 | def _centerLabelsOnVisibleSliceWidgets(self): 344 | for widget in self.getAllVisibleWidgets(): 345 | compositeNode = widget.mrmlSliceCompositeNode() 346 | sliceNode = widget.sliceLogic().GetSliceNode() 347 | labelID = compositeNode.GetLabelVolumeID() 348 | if labelID: 349 | label = slicer.mrmlScene.GetNodeByID(labelID) 350 | centroid = self.logic.getCentroidForLabel(label, self.session.segmentedLabelValue) 351 | if centroid: 352 | sliceNode.JumpSliceByCentering(centroid[0], centroid[1], centroid[2]) 353 | 354 | def _onTargetingStarted(self, caller, event): 355 | self.manualSegmentationPlugin.enabled = False 356 | self.backButton.enabled = False 357 | 358 | def _onTargetingFinished(self, caller, event): 359 | self.finishStepButton.setEnabled(1 if self.logic.inputsAreSet() else 0) 360 | self.manualSegmentationPlugin.enabled = True 361 | self.backButton.enabled = True -------------------------------------------------------------------------------- /SliceTracker/SliceTrackerUtils/watch.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Christian' 2 | 3 | import sys, getopt, os, dicom 4 | import time 5 | 6 | class NotDirectoryError(Exception): 7 | pass 8 | 9 | class DICOMDirectoryObserver(object): 10 | 11 | def __init__(self, directory, host, port): 12 | if not os.path.isdir(directory): 13 | raise NotDirectoryError("The directory is actually no directory") 14 | self.directory = directory 15 | self.host = host 16 | self.port = port 17 | self.files = set() 18 | 19 | def listdirRecursive(self, rootDir): 20 | files = [] 21 | for root, subFolders, dirFiles in os.walk(rootDir): 22 | for f in dirFiles: 23 | try: 24 | fileName = os.path.join(root,f) 25 | dicom.read_file(fileName) 26 | files.append(fileName) 27 | except: 28 | pass 29 | return files 30 | 31 | def watch(self, secondsToWait=1): 32 | while True: 33 | currentFiles = self.listdirRecursive(self.directory) 34 | if len(self.files) < len(currentFiles): 35 | print "Number of files changed" 36 | for newFile in self.getNewFiles(currentFiles): 37 | self.storeSCU(newFile) 38 | time.sleep(secondsToWait) 39 | 40 | def getNewFiles(self, files): 41 | newFiles = [] 42 | for currentFile in files: 43 | if currentFile not in self.files: 44 | newFiles.append(currentFile) 45 | return newFiles 46 | 47 | def storeSCU(self, fileName): 48 | cmd = ('storescu ' + self.host + ' ' + self.port + ' ' + fileName) 49 | print cmd 50 | os.system(cmd) 51 | self.files.add(fileName) 52 | 53 | def main(argv): 54 | watchDirectory = '' 55 | host = 'localhost' 56 | port = '11112' 57 | interval = 1 58 | try: 59 | opts, args = getopt.getopt(argv,"i:d:h:p:?",["help","directory=","host=","port=","interval="]) 60 | except getopt.GetoptError: 61 | print 'watch.py -d -h -p -i ' 62 | sys.exit(2) 63 | for opt, arg in opts: 64 | if opt in ("-?", "--help"): 65 | print 'watch.py -d -h -p ' 66 | sys.exit() 67 | elif opt in ("-d", "--directory"): 68 | watchDirectory = arg 69 | elif opt in ("-h", "--host"): 70 | host = arg 71 | elif opt in ("-p", "--port"): 72 | port = arg 73 | elif opt in ("-i", "--interval"): 74 | interval = int(arg) 75 | if watchDirectory and host and port: 76 | print 'Directory to watch is: ', watchDirectory 77 | print 'Host to send DICOM files to is: ', host 78 | print 'Port to send DICOM files to is: ', port 79 | 80 | watcher = DICOMDirectoryObserver(directory=watchDirectory, host=host, port=port) 81 | print "Will watch!" 82 | watcher.watch(interval) 83 | 84 | if __name__ == "__main__": 85 | main(sys.argv[1:]) 86 | 87 | 88 | #client use: $ sudo storescp -v -p 104 89 | #python watch.py -d "/Users/Christian/Documents/TEST1" -h localhost -p 104 -i 1 90 | -------------------------------------------------------------------------------- /SliceTracker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlicerProstate/SliceTracker/c5c341d2c3a204275194d7f3c2720b96177fe117/SliceTracker/__init__.py -------------------------------------------------------------------------------- /Testing/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | macro(getExtensions subdirs directory) 2 | file(GLOB children RELATIVE ${directory} ${directory}/*) 3 | set(dirList "") 4 | foreach(child ${children}) 5 | if(IS_DIRECTORY ${directory}/${child}) 6 | list(APPEND dirList ${child}) 7 | endif() 8 | endforeach() 9 | set(${subdirs} ${dirList}) 10 | endmacro() 11 | 12 | if(NOT "$ENV{SLICER_EXTENSIONS_DIR}" STREQUAL "") 13 | getExtensions(possibleExtensions $ENV{SLICER_EXTENSIONS_DIR}) 14 | 15 | set(ADDITIONAL_MODULE_PATHS "") 16 | 17 | foreach(subdir ${possibleExtensions}) 18 | #message("${EXTENSION_DEPENDS}") 19 | string(FIND "${EXTENSION_DEPENDS}" "${subdir}" pos) 20 | if(${pos} GREATER -1) 21 | message("$ENV{SLICER_EXTENSIONS_DIR}/${subdir}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}") 22 | list(APPEND ADDITIONAL_MODULE_PATHS "$ENV{SLICER_EXTENSIONS_DIR}/${subdir}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}") 23 | endif() 24 | #ADD_SUBDIRECTORY(${subdir}) 25 | endforeach() 26 | 27 | slicer_add_python_unittest( 28 | SCRIPT SliceTrackerTests.py 29 | SLICER_ARGS 30 | --additional-module-paths ${ADDITIONAL_MODULE_PATHS} 31 | ) 32 | else() 33 | slicer_add_python_unittest(SCRIPT SliceTrackerTests.py) 34 | endif() -------------------------------------------------------------------------------- /Testing/SliceTrackerTests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os, inspect, slicer 3 | from SliceTrackerUtils.session import SliceTrackerSession 4 | from SliceTrackerUtils.sessionData import SessionData 5 | 6 | __all__ = ['SliceTrackerSessionTests', 'RegistrationResultsTest'] 7 | 8 | tempDir = os.path.join(slicer.app.temporaryPath, "SliceTrackerResults") 9 | 10 | # class SliceTrackerSessionTests(unittest.TestCase): 11 | # 12 | # @classmethod 13 | # def setUpClass(cls): 14 | # cls.session = SliceTrackerSession() 15 | # 16 | # def runTest(self): 17 | # self.test_SliceTrackerSessionEvents() 18 | # self.test_SliceTrackerSessionSingleton() 19 | # 20 | # def test_SliceTrackerSessionEvents(self): 21 | # self.directoryChangedEventCalled = False 22 | # self.session.addEventObserver(self.session.DirectoryChangedEvent, 23 | # lambda event,caller:setattr(self, "directoryChangedEventCalled", True)) 24 | # 25 | # self.assertFalse(self.directoryChangedEventCalled) 26 | # self.session.directory = tempDir 27 | # self.assertTrue(self.directoryChangedEventCalled) 28 | # 29 | # def test_SliceTrackerSessionSingleton(self): 30 | # session = SliceTrackerSession() 31 | # self.assertTrue(self.session is session) 32 | # self.assertTrue(session.directory == self.session.directory) 33 | 34 | 35 | class RegistrationResultsTest(unittest.TestCase): 36 | 37 | @classmethod 38 | def setUpClass(cls): 39 | cls.registrationResults = SessionData() 40 | 41 | def runTest(self): 42 | self.test_Reading_json() 43 | self.test_Writing_json() 44 | 45 | def test_Reading_json(self): 46 | directory = os.path.join(os.path.dirname(inspect.getfile(self.__class__)), "..", "doc") 47 | inputFileName = os.path.join(directory, "output_example.json") 48 | self.registrationResults.load(inputFileName) 49 | 50 | def test_Writing_json(self): 51 | self.registrationResults.resumed = True 52 | self.registrationResults.completed = True 53 | self.registrationResults.save(tempDir) -------------------------------------------------------------------------------- /doc/output_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "usedPreopData": true, 3 | "biasCorrected": true, 4 | "preop": { 5 | "segmentation": { 6 | "algorithm": "automatic", 7 | "startTime": "2017-03-07T16:39:20.38Z", 8 | "endTime": "2017-03-07T16:39:20.38Z", 9 | "original": "t2-label.nrrd", 10 | "userModified": { 11 | "startTime": "2017-03-07T16:39:20.38Z", 12 | "endTime": "2017-03-07T16:39:20.38Z", 13 | "fileName": "t2-label_modified.nrrd" 14 | } 15 | } 16 | }, 17 | "initialVolume": "VOLUME-PREOP-N4.nrrd", 18 | "initialTargets": "PreopTargets.fcsv", 19 | "zFrameRegistration": { 20 | "name": "2: AX TSE T2 COVER TEMPLATE", 21 | "startTime": "2017-03-07T16:39:20.38Z", 22 | "endTime": "2017-03-07T16:39:20.38Z", 23 | "seriesType": "COVER TEMPLATE", 24 | "volume": "2-CoverTemplate.nrrd", 25 | "transform": "2-ZFrameTransform.h5" 26 | }, 27 | "procedureEvents": { 28 | "caseStarted": "2017-03-07T15:23:20.38Z", 29 | "caseClosed": [ 30 | { 31 | "time": "2017-03-07T16:33:20.38Z", 32 | "logfile": "Slicer_25707_20170308_144138.log" 33 | } 34 | ] 35 | }, 36 | "results": [ 37 | { 38 | "name": "3: AXIAL T2 COVER PROSTATE 3mm 0gap at iso", 39 | "seriesType": "COVER PROSTATE", 40 | "receivedTime": "2017-03-07T15:38:20.38Z", 41 | "status": { 42 | "state": "approved", 43 | "time": "2017-03-07T16:39:20.38Z" 44 | }, 45 | "registrationType": "bSpline", 46 | "labels": { 47 | "fixed": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label.nrrd", 48 | "moving": "t2-label.nrrd" 49 | }, 50 | "volumes": { 51 | "fixed": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso.nrrd", 52 | "moving": "VOLUME-PREOP-N4_1.nrrd", 53 | "bSpline": "3-VOLUME-bSpline.nrrd", 54 | "affine": "3-VOLUME-affine.nrrd", 55 | "rigid": "3-VOLUME-rigid.nrrd" 56 | }, 57 | "transforms": { 58 | "bSpline": "3-TRANSFORM-bSpline.h5", 59 | "affine": "3-TRANSFORM-affine.h5", 60 | "rigid": "3-TRANSFORM-rigid.h5" 61 | }, 62 | "targets": { 63 | "original": "targets-PREOP.fcsv", 64 | "bSpline": "3-TARGETS-bSpline.fcsv", 65 | "affine": "3-TARGETS-affine.fcsv", 66 | "rigid": "3-TARGETS-rigid.fcsv", 67 | "approved": { 68 | "userModified": [ 69 | false 70 | ], 71 | "fileName": "3-TARGETS-approved.fcsv" 72 | } 73 | }, 74 | "segmentation": { 75 | "algorithm": "automatic", 76 | "startTime": "2017-03-07T16:39:20.38Z", 77 | "endTime": "2017-03-07T16:39:20.38Z", 78 | "original": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label.nrrd", 79 | "userModified": { 80 | "startTime": "2017-03-07T16:39:20.38Z", 81 | "endTime": "2017-03-07T16:39:20.38Z", 82 | "fileName": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label_modified.nrrd" 83 | } 84 | }, 85 | "registration": { 86 | "startTime": "2017-03-07T16:39:20.38Z", 87 | "endTime": "2017-03-07T16:39:20.38Z" 88 | }, 89 | "score": 5 90 | }, 91 | { 92 | "name": "5: AX TSE T2 GUIDANCE FOR NEEDLE at FIX _H0", 93 | "seriesType": "COVER TEMPLATE", 94 | "receivedTime": "2017-03-07T15:43:20.38Z", 95 | "status": { 96 | "state": "approved", 97 | "time": "2017-03-07T16:44:20.38Z" 98 | }, 99 | "registration": { 100 | "startTime": "2017-03-07T16:39:20.38Z", 101 | "endTime": "2017-03-07T16:39:20.38Z" 102 | }, 103 | "registrationType": "bSpline", 104 | "labels": { 105 | "fixed": "5-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0-label.nrrd", 106 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label.nrrd" 107 | }, 108 | "transforms": { 109 | "bSpline": "5-TRANSFORM-bSpline.h5", 110 | "affine": "5-TRANSFORM-affine.h5", 111 | "rigid": "5-TRANSFORM-rigid.h5" 112 | }, 113 | "targets": { 114 | "original": "3-TARGETS-approved.fcsv", 115 | "bSpline": "5-TARGETS-bSpline.fcsv", 116 | "affine": "5-TARGETS-affine.fcsv", 117 | "rigid": "5-TARGETS-rigid.fcsv", 118 | "approved": { 119 | "userModified": [ 120 | true 121 | ], 122 | "fileName": "5-TARGETS-approved.fcsv" 123 | } 124 | }, 125 | "volumes": { 126 | "bSpline": "5-VOLUME-bSpline.nrrd", 127 | "affine": "5-VOLUME-affine.nrrd", 128 | "rigid": "5-VOLUME-rigid.nrrd", 129 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso.nrrd", 130 | "fixed": "5-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0.nrrd" 131 | } 132 | }, 133 | { 134 | "name": "6: AX TSE T2 GUIDANCE FOR NEEDLE at FIX _H0", 135 | "receivedTime": "2017-03-07T15:48:20.38Z", 136 | "status": { 137 | "state": "rejected", 138 | "time": "2017-03-07T16:49:20.38Z" 139 | }, 140 | "registration": { 141 | "startTime": "2017-03-07T16:39:20.38Z", 142 | "endTime": "2017-03-07T16:39:20.38Z" 143 | }, 144 | "labels": { 145 | "fixed": "6-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0-label.nrrd", 146 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label.nrrd" 147 | }, 148 | "transforms": { 149 | "bSpline": "6-TRANSFORM-bSpline.h5", 150 | "affine": "6-TRANSFORM-affine.h5", 151 | "rigid": "6-TRANSFORM-rigid.h5" 152 | }, 153 | "targets": { 154 | "bSpline": "6-TARGETS-bSpline.fcsv", 155 | "affine": "6-TARGETS-affine.fcsv", 156 | "rigid": "6-TARGETS-rigid.fcsv", 157 | "original": "3-TARGETS-approved.fcsv" 158 | }, 159 | "volumes": { 160 | "bSpline": "6-VOLUME-bSpline.nrrd", 161 | "affine": "6-VOLUME-affine.nrrd", 162 | "rigid": "6-VOLUME-rigid.nrrd", 163 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso.nrrd", 164 | "fixed": "6-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0.nrrd" 165 | } 166 | }, 167 | { 168 | "name": "9: AX TSE T2 GUIDANCE FOR NEEDLE at FIX _H0", 169 | "receivedTime": "2017-03-07T15:51:20.38Z", 170 | "status": { 171 | "state": "skipped", 172 | "time": "2017-03-07T16:51:43.38Z" 173 | }, 174 | "volumes": { 175 | "fixed": "9-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0.nrrd" 176 | } 177 | }, 178 | { 179 | "name": "10: AX TSE T2 GUIDANCE FOR NEEDLE at FIX _H0_Retry_1", 180 | "receivedTime": "2017-03-07T15:53:20.38Z", 181 | "status": { 182 | "state": "approved", 183 | "time": "2017-03-07T16:54:43.38Z" 184 | }, 185 | "suffix": "_Retry_1", 186 | "registration": { 187 | "startTime": "2017-03-07T16:39:20.38Z", 188 | "endTime": "2017-03-07T16:39:20.38Z" 189 | }, 190 | "registrationType": "bSpline", 191 | "labels": { 192 | "fixed": "10-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0-label_Retry_1.nrrd", 193 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso-label.nrrd" 194 | }, 195 | "transforms": { 196 | "bSpline": "10-TRANSFORM-bSpline_Retry_1.h5", 197 | "affine": "10-TRANSFORM-affine_Retry_1.h5", 198 | "rigid": "10-TRANSFORM-rigid_Retry_1.h5" 199 | }, 200 | "targets": { 201 | "original": "3-TARGETS-approved.fcsv", 202 | "bSpline": "10-TARGETS-bSpline_Retry_1.fcsv", 203 | "affine": "10-TARGETS-affine_Retry_1.fcsv", 204 | "rigid": "10-TARGETS-rigid_Retry_1.fcsv", 205 | "approved": { 206 | "userModified": [ 207 | false 208 | ], 209 | "fileName": "10-TARGETS-approved.fcsv" 210 | } 211 | }, 212 | "segmentation": { 213 | "algorithm": "manual", 214 | "startTime": "2017-03-07T16:39:20.38Z", 215 | "endTime": "2017-03-07T16:39:20.38Z", 216 | "original": "10-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0-label_Retry_1.nrrd" 217 | }, 218 | "score": 5, 219 | "volumes": { 220 | "fixed": "10-AX-TSE-T2-GUIDANCE-FOR-NEEDLE-at-FIX-_H0.nrrd", 221 | "moving": "3-AXIAL-T2-COVER-PROSTATE-3mm-0gap-at-iso.nrrd", 222 | "bSpline": "10-VOLUME-bSpline_Retry_1.nrrd", 223 | "affine": "10-VOLUME-affine_Retry_1.nrrd", 224 | "rigid": "10-VOLUME-rigid_Retry_1.nrrd" 225 | } 226 | } 227 | ] 228 | } -------------------------------------------------------------------------------- /doc/output_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" :"https://raw.githubusercontent.com/slicerprostate/slicetracker/master/doc/output_schema.json#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "additionalProperties": false, 5 | "properties": { 6 | "procedureEvents": { 7 | "type": "object", 8 | "additionalProperties": false, 9 | "properties": { 10 | "caseStarted": { "$ref": "#/definitions/TIMESTAMP" }, 11 | "caseClosed": { "$ref": "#/definitions/TIMESTAMP_LOGFILE_LIST" }, 12 | "caseResumed": { "$ref": "#/definitions/TIMESTAMP_LIST" }, 13 | "caseCompleted": { "$ref": "#/definitions/TIMESTAMP_LOGFILE" } 14 | }, "require": ["caseStarted", "caseClosed"] 15 | }, 16 | "usedPreopData": { "type": "boolean" }, 17 | "biasCorrected": { "type": "boolean" }, 18 | "preop": { 19 | "segmentation": { "$ref": "#/definitions/SEGMENTATION" } 20 | }, 21 | "initialVolume": { "type": "string" }, 22 | "initialTargets": { "type": "string" }, 23 | "zFrameRegistration": { 24 | "type": "object", 25 | "additionalProperties": false, 26 | "properties": { 27 | "name": { "type": "string" }, 28 | "startTime": { "$ref": "#/definitions/TIMESTAMP" }, 29 | "endTime": { "$ref": "#/definitions/TIMESTAMP" }, 30 | "seriesType": {"type": "string" }, 31 | "volume": { "$ref": "#/definitions/NRRD_FILE" }, 32 | "transform": { "$ref": "#/definitions/TRANSFORM_FILE" } 33 | }, "require": ["name", "startTime", "endTime", "volume", "transform"] 34 | }, 35 | "results": { "$ref": "#/definitions/RESULTS" } 36 | }, 37 | "required": ["procedureEvents", "usedPreopData", "initialVolume", "initialTargets", "zFrameRegistration", "results"], 38 | "definitions": { 39 | "RESULTS": { 40 | "type": "array", 41 | "items": { "$ref": "#/definitions/RESULT" } 42 | }, 43 | "RESULT": { 44 | "type": "object", 45 | "additionalProperties": false, 46 | "properties": { 47 | "name": { "type": "string" }, 48 | "seriesType": {"type": "string" }, 49 | "receivedTime": { "$ref": "#/definitions/TIMESTAMP" }, 50 | "status": { 51 | "type": "object", 52 | "additionalProperties": false, 53 | "properties": { 54 | "state": {"type": "string", 55 | "enum": [ "approved", "skipped", "rejected"] 56 | }, 57 | "time": { "$ref": "#/definitions/TIMESTAMP" } 58 | }, "required": ["state", "time"] 59 | }, 60 | "segmentation": { "$ref": "#/definitions/SEGMENTATION" }, 61 | "registration": { "$ref": "#/definitions/REGISTRATION" }, 62 | "registrationType": { "$ref": "#/definitions/REGISTRATION_TYPE" }, 63 | "score": { "type": "number" }, 64 | "suffix": { 65 | "type": "string", 66 | "pattern": "^_Retry_([0-9])+$" 67 | }, 68 | "labels": { "$ref": "#/definitions/VOLUME_TYPES" }, 69 | "transforms": { "$ref": "#/definitions/REGISTRATION_TYPES" }, 70 | "volumes": { 71 | "allOf": [ 72 | { "$ref": "#/definitions/REGISTRATION_TYPES" }, 73 | { "$ref": "#/definitions/VOLUME_TYPES" } 74 | ] 75 | }, 76 | "targets": { "$ref": "#/definitions/TARGETS" } 77 | }, 78 | "required": ["name", "status", "receivedTime"] 79 | }, 80 | "REGISTRATION_TYPE": { 81 | "type": "string", 82 | "enum": ["rigid", "affine", "bSpline" 83 | ] 84 | }, 85 | "REGISTRATION_TYPES": { 86 | "type": "object", 87 | "properties": { 88 | "rigid": { "type": "string" }, 89 | "affine": { "type": "string" }, 90 | "bSpline": { "type": "string" } 91 | } 92 | }, 93 | "VOLUME_TYPES": { 94 | "type": "object", 95 | "properties": { 96 | "fixed": { "type": "string" }, 97 | "moving": { "type": "string" } 98 | } 99 | }, 100 | "TARGETS" : { 101 | "allOf": [ 102 | { "$ref": "#/definitions/REGISTRATION_TYPES" }, 103 | { 104 | "properties": { 105 | "original": { "type": "string" }, 106 | "approved": { 107 | "type": "object", 108 | "additionalProperties": false, 109 | "properties": { 110 | "userModified": { 111 | "type": "array", 112 | "items": { 113 | "type": "boolean" 114 | } 115 | }, 116 | "fileName": { "$ref": "#/definitions/FIDUCIALS_FILE" } 117 | }, "required": ["userModified"] 118 | } 119 | }, "required": ["original", "rigid", "affine", "bSpline"] 120 | } 121 | ], 122 | "required": ["original"] 123 | }, 124 | "TRANSFORM_FILE": { 125 | "type": "string", 126 | "pattern": "^([0-9])+-([a-z,A-Z])+.h5$" 127 | }, 128 | "FIDUCIALS_FILE": { 129 | "type": "string", 130 | "pattern": "^([0-9])+-([a-z,A-Z])+-approved.fcsv$" 131 | }, 132 | "NRRD_FILE": { 133 | "type": "string", 134 | "pattern": ".nrrd$" 135 | }, 136 | "TIMESTAMP": { 137 | "type": "string", 138 | "format": "date-time" 139 | }, 140 | "TIMESTAMP_LIST": { 141 | "type": "array", 142 | "items": { "$ref": "#/definitions/TIMESTAMP" } 143 | }, 144 | "TIMESTAMP_LOGFILE_LIST": { 145 | "type": "array", 146 | "items": { "$ref": "#/definitions/TIMESTAMP_LOGFILE" } 147 | }, 148 | "TIMESTAMP_LOGFILE": { 149 | "type": "object", 150 | "additionalProperties": false, 151 | "properties": { 152 | "time": { "$ref": "#/definitions/TIMESTAMP" }, 153 | "logfile": { "type": "string" } 154 | }, "required": ["time", "logfile"] 155 | }, 156 | "SEGMENTATION": { 157 | "type": "object", 158 | "additionalProperties": false, 159 | "properties": { 160 | "algorithm": { 161 | "type": "string", 162 | "enum": [ "manual", "automatic"] 163 | }, 164 | "startTime": { "$ref": "#/definitions/TIMESTAMP" }, 165 | "endTime": { "$ref": "#/definitions/TIMESTAMP" }, 166 | "original": { "$ref": "#/definitions/NRRD_FILE" }, 167 | "userModified": { 168 | "type": "object", 169 | "additionalProperties": false, 170 | "properties": { 171 | "startTime": { "$ref": "#/definitions/TIMESTAMP" }, 172 | "endTime": { "$ref": "#/definitions/TIMESTAMP" }, 173 | "fileName": { "$ref": "#/definitions/NRRD_FILE" } 174 | } 175 | } 176 | }, "required": ["algorithm", "startTime", "endTime", "original"] 177 | }, 178 | "REGISTRATION": { 179 | "type": "object", 180 | "additionalProperties": false, 181 | "properties": { 182 | "startTime": { "$ref": "#/definitions/TIMESTAMP" }, 183 | "endTime": { "$ref": "#/definitions/TIMESTAMP" } 184 | }, "required": ["startTime", "endTime"] 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /puml/RegistrationResults.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "RegistrationResults" #DDDDDD { 4 | 5 | class SessionData { 6 | .. events .. 7 | + NewResultCreatedEvent 8 | .. properties .. 9 | + resumed 10 | + completed 11 | __ 12 | + startTimeStamp: localtime 13 | + resumeTimeStamps: list(localtime) 14 | + closedTimeStamps: list(localtime) 15 | * completedTimeStamp 16 | + usePreopData: boolean 17 | + biasCorrectionDone: boolean 18 | 19 | + segmentgModelNode 20 | + inputMarkupNode 21 | 22 | + initialVolume 23 | + initialTargets 24 | 25 | # _savedRegistrationResults: list 26 | # _registrationResults: OrderedDict 27 | __ 28 | + resetAndInitializeData() 29 | + load(directory, filename) 30 | + save(outputDir) 31 | 32 | # _loadResultFileData(directory, filename, loadFunction, setFunction) 33 | # _loadOrGetFileData(directory, filename, loadFunction) 34 | 35 | } 36 | 37 | ModuleLogicMixin <|-- SessionData 38 | 39 | class RegistrationStatus { 40 | + UNDEFINED_STATUS = "undefined" 41 | + SKIPPED_STATUS = "skipped" 42 | + APPROVED_STATUS = "approved" 43 | + REJECTED_STATUS = "rejected" 44 | .. properties .. 45 | + approved 46 | + skipped 47 | + rejected 48 | -- 49 | + status 50 | -- 51 | + hasStatus(expected): bool 52 | + approve() 53 | + skip() 54 | + reject() 55 | } 56 | 57 | ModuleLogicMixin <|-- RegistrationStatus 58 | 59 | 60 | class RegistrationResult { 61 | + {static} getSeriesNumberFromString 62 | # _targets 63 | # _volumes 64 | # _labels 65 | # _transforms 66 | } 67 | 68 | RegistrationStatus <|-- RegistrationResult 69 | 70 | class AbstractRegistrationData { 71 | + FILE_EXTENSION 72 | -- 73 | + {abstract} initializeMembers() 74 | + {abstract} asList(): list 75 | + {abstract} asDict(): dict 76 | -- 77 | + getFileName(node): string 78 | + getFileNameByAttributeName(name): string 79 | + getAllFileNames(): string[] 80 | + save(directory) 81 | } 82 | 83 | ModuleLogicMixin <|-- AbstractRegistrationData 84 | AbstractRegistrationData <|-- RegistrationTypeData 85 | 86 | class RegistrationTypeData { 87 | + rigid 88 | + affine 89 | + bSpline 90 | } 91 | 92 | class Labels { 93 | + FILE_EXTENSION: FileExtension.NRRD 94 | + fixed 95 | + moving 96 | } 97 | 98 | class Volumes { 99 | + FILE_EXTENSION: FileExtension.NRRD 100 | + fixed 101 | + moving 102 | } 103 | 104 | class Transforms { 105 | + FILE_EXTENSION: FileExtension.H5 106 | } 107 | 108 | class Targets { 109 | + FILE_EXTENSION: FileExtension.FCSV 110 | + original 111 | + approved 112 | + approve(registrationType) 113 | } 114 | 115 | AbstractRegistrationData <|-- Labels 116 | RegistrationTypeData <|-- Volumes 117 | RegistrationTypeData <|-- Transforms 118 | RegistrationTypeData <|-- Targets 119 | 120 | Volumes <.. RegistrationResult::_volumes 121 | Targets <.. RegistrationResult::_targets 122 | Labels <.. RegistrationResult::_labels 123 | Transforms <.. RegistrationResult::_transforms 124 | 125 | SessionData::_registrationResults ..> RegistrationResult 126 | } 127 | 128 | @enduml -------------------------------------------------------------------------------- /puml/SliceTracker.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | !include SlicerDevelopmentToolbox.puml 4 | !include RegistrationResults.puml 5 | !include SliceTrackerSession.puml 6 | !include SliceTrackerSteps.puml 7 | 8 | 9 | ModuleWidgetMixin <|-- SliceTrackerWidget 10 | SliceTrackerConstants <|-- SliceTrackerWidget 11 | 12 | 13 | class SliceTrackerWidget { 14 | .. properties .. 15 | + session 16 | + stepManager 17 | -- 18 | 19 | + enter( 20 | + setup() 21 | + cleanup() 22 | 23 | } 24 | 25 | SliceTrackerSession <.. SliceTrackerWidget::session: uses 26 | SliceTrackerStepManager <.. SliceTrackerWidget::stepManager 27 | 28 | @enduml 29 | -------------------------------------------------------------------------------- /puml/SliceTrackerPlugins.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | abstract class SliceTrackerPlugin { 4 | + {abstract} LogicClass 5 | } 6 | 7 | class SliceTrackerLogicBase 8 | SliceTrackerLogicBase <.. SliceTrackerPlugin::LogicClass: uses instance of 9 | 10 | 11 | package "SliceTracker Plugins" #DDDDDD { 12 | 13 | package "Training Management" #FDADDF { 14 | 15 | class SliceTrackerTrainingPlugin { 16 | } 17 | 18 | SliceTrackerPlugin <|-- SliceTrackerTrainingPlugin 19 | } 20 | 21 | package "Registration Results Management" #FDADDF { 22 | 23 | class SliceTrackerRegistrationResultsPlugin { 24 | + LogicClass 25 | + onLayoutChanged() 26 | } 27 | 28 | class SliceTrackerRegistrationResultsLogic 29 | 30 | SliceTrackerPlugin <|-- SliceTrackerRegistrationResultsPlugin 31 | SliceTrackerRegistrationResultsPlugin::LogicClass ..> SliceTrackerRegistrationResultsLogic: uses 32 | 33 | } 34 | 35 | package "Target Management" #FDDDDD { 36 | 37 | class SliceTrackerTargetTablePlugin { 38 | + LogicClass 39 | + targetTableModel 40 | } 41 | 42 | class SliceTrackerTargetTableLogic { 43 | } 44 | 45 | class ZFrameGuidanceComputation { 46 | } 47 | 48 | class CustomTargetTableModel { 49 | # _guidanceComputations 50 | } 51 | 52 | SliceTrackerPlugin <|-- SliceTrackerTargetTablePlugin 53 | SliceTrackerTargetTablePlugin::LogicClass ..> SliceTrackerTargetTableLogic: uses 54 | SliceTrackerTargetTablePlugin::targetTableModel ..> CustomTargetTableModel: uses 55 | ZFrameGuidanceComputation <.. CustomTargetTableModel::_guidanceComputations 56 | } 57 | 58 | package "Targeting" #FDDDFF { 59 | 60 | class SliceTrackerTargetingPlugin { 61 | + LogicClass 62 | } 63 | 64 | class SliceTrackerTargetTargetingLogic { 65 | } 66 | 67 | SliceTrackerPlugin <|-- SliceTrackerTargetingPlugin 68 | SliceTrackerTargetingPlugin::LogicClass ..> SliceTrackerTargetTargetingLogic: uses 69 | 70 | } 71 | 72 | package "Case Management" #DDF0DD { 73 | 74 | class SliceTrackerCaseManagerPlugin { 75 | + LogicClass 76 | } 77 | 78 | class SliceTrackerCaseManagerLogic { 79 | } 80 | 81 | SliceTrackerPlugin <|-- SliceTrackerCaseManagerPlugin 82 | SliceTrackerCaseManagerPlugin::LogicClass ..> SliceTrackerCaseManagerLogic: uses 83 | } 84 | 85 | package "Future Plugins" #DDFFDD { 86 | class SliceTrackerNeedleSegmentationPlugin { 87 | + LogicClass 88 | } 89 | 90 | SliceTrackerPlugin <|-- SliceTrackerNeedleSegmentationPlugin 91 | SliceTrackerNeedleSegmentationPlugin::LogicClass ..> SliceTrackerNeedleSegmentationLogic: uses 92 | 93 | class SliceTrackerTargetDisplacementChartPlugin { 94 | + LogicClass 95 | } 96 | 97 | SliceTrackerPlugin <|-- SliceTrackerTargetDisplacementChartPlugin 98 | SliceTrackerTargetDisplacementChartPlugin::LogicClass ..> SliceTrackerTargetDisplacementChartLogic: uses 99 | } 100 | 101 | package "Segmentation Management" #5555FF { 102 | 103 | class SliceTrackerSegmentationPluginBase { 104 | .. event .. 105 | + SegmentationStartedEvent 106 | + SegmentationFinishedEvent 107 | } 108 | 109 | SliceTrackerPlugin <|-- SliceTrackerSegmentationPluginBase 110 | 111 | class SliceTrackerAutomaticSegmentationPlugin { 112 | + LogicClass 113 | } 114 | class AutomaticSegmentationLogic 115 | 116 | SliceTrackerSegmentationPluginBase <|--SliceTrackerAutomaticSegmentationPlugin 117 | AutomaticSegmentationLogic <.. SliceTrackerAutomaticSegmentationPlugin::LogicClass: uses 118 | 119 | 120 | class SliceTrackerManualSegmentationPlugin { 121 | .. event .. 122 | + SegmentationCancelledEvent 123 | __ 124 | + volumeClipToLabelWidget 125 | } 126 | 127 | SliceTrackerSegmentationPluginBase <|--SliceTrackerManualSegmentationPlugin 128 | SurfaceCutToLabelWidget <.. SliceTrackerManualSegmentationPlugin::volumeClipToLabelWidget: uses 129 | 130 | class SurfaceCutToLabelWidget { 131 | .. event .. 132 | + SegmentationFinishedEvent 133 | + SegmentationStartedEvent 134 | + SegmentationCanceledEvent 135 | -- 136 | + logic 137 | } 138 | 139 | class SurfaceCutToLabelLogic { 140 | } 141 | 142 | SurfaceCutToLabelWidget::logic ..> SurfaceCutToLabelLogic: uses 143 | 144 | 145 | } 146 | 147 | } 148 | 149 | SliceTrackerWidgetBase <|-- SliceTrackerPlugin 150 | 151 | 152 | @enduml -------------------------------------------------------------------------------- /puml/SliceTrackerSession.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "Session" #DDDDDD { 4 | 5 | abstract class SessionBase { 6 | .. events .. 7 | DirectoryChangedEvent: vtk.vtkCommand.UserEvent 8 | .. properties .. 9 | + directory 10 | -- 11 | + {abstract} load() 12 | + {abstract} save() 13 | } 14 | 15 | 16 | Singleton <-- SliceTrackerSession 17 | SessionBase <-- SliceTrackerSession 18 | 19 | class SliceTrackerSession { 20 | .. events .. 21 | -- 22 | .. properties .. 23 | + preopDICOMDirectory 24 | + intraopDICOMDirectory 25 | + preprocessedDirectory 26 | + outputDirectory 27 | + currentSeries 28 | -- 29 | + steps 30 | + trainingMode: boolean 31 | -- 32 | + intraopLabel 33 | + regResults: RegistrationResults 34 | __ 35 | + createNewCase(destination) 36 | + openCase(directory) 37 | + closeCase() 38 | + completeCase() 39 | + registerStep(step) 40 | + startIntraopDICOMReceiver() 41 | + stopIntraopDICOMReceiver() 42 | + importDICOMSeries(newFileList) 43 | + makeSeriesNumberDescription(dcmFile): string 44 | + createLoadableFileListForSeries(series): list 45 | } 46 | } 47 | 48 | SliceTrackerStep <.. SliceTrackerSession::steps: has [0..n] 49 | RegistrationResults <.. SliceTrackerSession::regResults : use 50 | 51 | @enduml -------------------------------------------------------------------------------- /puml/SliceTrackerSteps.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | !include SliceTrackerPlugins.puml 4 | 5 | package "SliceTrackerSteps" #DDDDDD { 6 | 7 | class SliceTrackerTabWidget { 8 | + session 9 | + __init__(session) 10 | # _createTabs() 11 | + onCurrentTabChanged(step) 12 | } 13 | 14 | abstract class StepBase { 15 | + MODULE_NAME = "SliceTracker" 16 | __ 17 | + session 18 | + modulePath: string 19 | + resourcesPath 20 | __ 21 | + getModulePath(): string 22 | + getSetting(self, setting, moduleName=None, default=None) 23 | + setSetting(self, setting, value, moduleName=None) 24 | } 25 | 26 | abstract class SliceTrackerWidgetBase { 27 | + {abstract} NAME: string 28 | + {abstract} LogicClass 29 | + {abstract} setup() 30 | + {abstract} cleanup() 31 | + {abstract} setupConnections() 32 | + {abstract} onLayoutChanged() 33 | + setupSessionObservers() 34 | + setupSliceWidgets() 35 | + setupAdditionalViewSettingButtons() 36 | + resetViewSettingButtons() 37 | + onNewCaseStarted() 38 | + onCaseClosed() 39 | + onIncomingDataSkipped() 40 | + onNewImageDataReceived() 41 | + onCoverTemplateReceived() 42 | + onZFrameRegistrationSuccessful() 43 | } 44 | 45 | StepBase <|-- SliceTrackerWidgetBase 46 | 47 | abstract class SliceTrackerStep { 48 | # _plugins 49 | .. events .. 50 | ActivatedEvent: vtk.vtkCommand.UserEvent 51 | DeactivatedEvent: vtk.vtkCommand.UserEvent 52 | .. properties .. 53 | + active: bool 54 | __ 55 | + onActivation() 56 | + onDeactivation() 57 | + removeSessionEventObservers() 58 | } 59 | 60 | SliceTrackerWidgetBase <|-- SliceTrackerStep 61 | SliceTrackerPlugin <.. SliceTrackerStep::_plugins: has [0..n] 62 | 63 | 64 | abstract class SliceTrackerLogicBase { 65 | .. abstract methods .. 66 | + {abstract} cleanup() 67 | __ 68 | + getOrCreateVolumeForSeries(series) 69 | } 70 | 71 | StepBase <|-- SliceTrackerLogicBase 72 | 73 | class SliceTrackerOverviewStep { 74 | + onLayoutChanged() 75 | + trainingPlugin 76 | + regResultPlugin 77 | + targetTablePlugin 78 | } 79 | 80 | SliceTrackerTrainingPlugin <.. SliceTrackerOverviewStep::trainingPlugin 81 | SliceTrackerTargetTablePlugin <.. SliceTrackerOverviewStep::targetTablePlugin 82 | 83 | class SliceTrackerSegmentationStep { 84 | + onLayoutChanged() 85 | } 86 | 87 | class SliceTrackerZFrameRegistrationStep { 88 | + onLayoutChanged() 89 | } 90 | 91 | class SliceTrackerEvaluationStep { 92 | + regResultPlugin 93 | __ 94 | + onLayoutChanged() 95 | } 96 | 97 | SliceTrackerRegistrationResultsPlugin <.. SliceTrackerEvaluationStep::regResultPlugin 98 | SliceTrackerRegistrationResultsPlugin <.. SliceTrackerOverviewStep::regResultPlugin 99 | 100 | 101 | 'class OverViewStepLogic 102 | 'class SegmentationStepLogic 103 | 'class ZFrameRegistrationStepLogic 104 | 'class EvaluationStepLogic 105 | ' 106 | SliceTrackerLogicBase <|-- OverViewStepLogic 107 | SliceTrackerLogicBase <|-- SegmentationStepLogic 108 | SliceTrackerLogicBase <|-- ZFrameRegistrationStepLogic 109 | SliceTrackerLogicBase <|-- EvaluationStepLogic 110 | 111 | SliceTrackerStep <|-- SliceTrackerOverviewStep 112 | SliceTrackerStep <|-- SliceTrackerSegmentationStep 113 | SliceTrackerStep <|-- SliceTrackerZFrameRegistrationStep 114 | SliceTrackerStep <|-- SliceTrackerEvaluationStep 115 | 116 | OverViewStepLogic <.. SliceTrackerOverviewStep: uses 117 | SegmentationStepLogic <.. SliceTrackerSegmentationStep: uses 118 | ZFrameRegistrationStepLogic <.. SliceTrackerZFrameRegistrationStep: uses 119 | EvaluationStepLogic <.. SliceTrackerEvaluationStep: uses 120 | 121 | SliceTrackerLogicBase <.. SliceTrackerWidgetBase::LogicClass : uses 122 | } 123 | 124 | QTabWidget <|-- SliceTrackerTabWidget 125 | QWidget <|-- SliceTrackerWidgetBase 126 | ModuleWidgetMixin <|-- SliceTrackerWidgetBase 127 | ModuleLogicMixin <|-- SliceTrackerLogicBase 128 | 129 | SliceTrackerSession <.. StepBase::session : uses 130 | 'SliceTrackerSession "0" --> "*" SliceTrackerStep 131 | 132 | @enduml -------------------------------------------------------------------------------- /puml/SlicerDevelopmentToolbox.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "SlicerDevelopmentToolbox" #DDDDDD { 4 | class ModuleWidgetMixin { 5 | .. properties .. 6 | + layoutManager 7 | -- 8 | + {static} truncatePath(path) 9 | + createSliceWidgetClassMembers(name) 10 | + getAllVisibleWidgets() 11 | + getOrCreateCustomProgressBar() 12 | + setFOV(sliceLogic, FOV) 13 | + removeNodeFromMRMLScene(node) 14 | + refreshViewNodeIDs(node, sliceNodes) 15 | + removeViewNodeIDs(node, sliceNodes) 16 | + jumpSliceNodeToTarget(sliceNode, targetNode, index) 17 | + resetToRegularViewMode() 18 | + confirmOrSaveDialog(message) 19 | ... 20 | } 21 | class ModuleLogicMixin 22 | 23 | } 24 | 25 | @enduml -------------------------------------------------------------------------------- /puml/components.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "SliceTracker" { 4 | [OverView] 5 | [ZFrameCalibration] 6 | [Segmentation] 7 | [Evaluation] 8 | 9 | [Session] 10 | 11 | database "RegistrationResults" { 12 | 13 | } 14 | } 15 | 16 | @enduml -------------------------------------------------------------------------------- /puml/intraop_reception.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | participant "Session" as A 3 | participant "SmartDICOMReceiver" as B 4 | participant "ParameterNodeObservationMixin" as C 5 | participant "ZFrameRegistrationStep" as D 6 | participant "OverviewStep" as E 7 | participant "Operator" as O 8 | 9 | A --> B: start storecp 10 | B --> B: receiving DICOM 11 | B --> A: DICOM reception finished 12 | A --> A: import DICOM into database 13 | 14 | alt Depending on series that has been received 15 | 16 | else COVER_TEMPLATE and not zFrameRegistratationSuccessful 17 | A --> C: invoke event CoverTemplateReceivedEvent 18 | C --> D: onCoverTemplateReceived 19 | D -> D: activate 20 | O --> D: initialize template 21 | O --> D: run calibration 22 | O --> D: approve calibration result 23 | D --> A: zFrameRegistratationSuccessful = True 24 | A --> C: invoke event ZFrameCalibrationSuccessfulEvent 25 | E -> E: activate 26 | E -> E: select eligible series 27 | else COVER_PROSTATE and zFrameRegistratationSuccessful 28 | A --> C: invoke event CoverProstateReceivedEvent 29 | A --> E: 30 | else NEEDLE_IMAGE 31 | A --> C: invoke event NeedleImageReceivedEvent 32 | else VIBE_IMAGE 33 | A --> C: invoke event VibeImageReceivedEvent 34 | else OTHERS 35 | end 36 | 37 | @enduml -------------------------------------------------------------------------------- /startSlicelet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SLICER="/Applications/Slicer.app/Contents/MacOS/Slicer" 3 | 4 | $SLICER --python-code "from SliceTracker import SliceTrackerSlicelet; slicelet=SliceTrackerSlicelet();" --no-splash --no-main-window -------------------------------------------------------------------------------- /startSlicelet_Ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SLICER="/home/parallels/sources/cpp/Slicer/Build/Slicer-build/Slicer" 3 | 4 | $SLICER --python-code "from SliceTracker import SliceTrackerSlicelet; slicelet=SliceTrackerSlicelet();" --no-splash --no-main-window --------------------------------------------------------------------------------