├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build_macos.yml │ ├── build_macos_arm.yml │ ├── build_ubuntu.yml │ ├── build_windows.yml │ └── delete-drafts.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── assets ├── Raidionics.nsi ├── Raidionics_ubuntu │ ├── DEBIAN │ │ └── control │ └── usr │ │ └── share │ │ └── applications │ │ └── Raidionics.desktop ├── hooks │ ├── hook-PySide6.py │ ├── hook-ants.py │ ├── hook-antspyx.py │ ├── hook-dipy.py │ ├── hook-distutils.py │ ├── hook-gdown.py │ ├── hook-gevent.py │ ├── hook-nibabel.py │ ├── hook-pydicom.py │ ├── hook-raidionicsrads.py │ ├── hook-raidionicsseg.py │ ├── hook-rtutils.py │ ├── hook-scikit-learn.py │ ├── hook-scipy.py │ ├── hook-sklearn.py │ ├── hook-statsmodels.py │ └── set_recursion_limit.py ├── images │ ├── Preview_SingleUse.gif │ ├── interface-snapshot.png │ ├── raidionics-logo.icns │ ├── raidionics-logo.ico │ ├── raidionics-logo.png │ └── raidionics_yt_thumbnail.png ├── main.spec ├── main_arm.spec └── requirements.txt ├── build_launcher.py ├── gui ├── Images │ ├── about_raidionics_icon.png │ ├── about_raidionics_icon_pressed.png │ ├── alkmaar-hospital-logo.png │ ├── amsterdam-hospital-logo.png │ ├── arrow_circle_down.png │ ├── arrow_circle_up.png │ ├── arrow_down_icon.png │ ├── arrow_right_icon.png │ ├── brats-challenge-logo.png │ ├── brigham-boston-hospital-logo.png │ ├── browse_icon.png │ ├── cancer-institute-ams-logo.png │ ├── circle_question_icon.png │ ├── classification_icon.png │ ├── close_icon.png │ ├── closed_eye_icon.png │ ├── collapsed_icon.png │ ├── combobox-arrow-icon-10x7.png │ ├── contrast_icon.png │ ├── data_load_icon.png │ ├── data_save_icon.png │ ├── database_icon.png │ ├── delete-cross-icon.png │ ├── dicom_load_icon.png │ ├── download-tray-icon.png │ ├── download_icon.png │ ├── download_icon_black.png │ ├── expand_arrow.png │ ├── file_icon.png │ ├── filled_arrow_right.png │ ├── find_more_icon.png │ ├── find_more_icon_pressed.png │ ├── floppy_disk_icon.png │ ├── folder_icon.png │ ├── folder_simple_icon.png │ ├── github-icon.png │ ├── globe-icon.png │ ├── gothenburg-hospital-icon.png │ ├── groningen-hospital-logo.png │ ├── haaglanden-hospital-logo.png │ ├── help_wavy_question_icon_blue.png │ ├── home-icon.png │ ├── humanitas-milan-hospital-logo.png │ ├── isala-hospital-logo.png │ ├── issues_suggestions_icon.png │ ├── issues_suggestions_pressed_icon.png │ ├── jumpto-icon.png │ ├── large-arrow-down-icon.png │ ├── lariboisiere-hospital-logo.png │ ├── load_file_icon.png │ ├── logo_maker.svg │ ├── logs_icon.png │ ├── minus_icon.png │ ├── more-dots-icon.png │ ├── neurorads-logo.png │ ├── opened_eye_icon.png │ ├── oslo_univeristy_hospital_icon.png │ ├── patient-icon.png │ ├── play_icon.png │ ├── plus_icon.png │ ├── power-icon.png │ ├── preferences-sliders-icon.png │ ├── published_articles_icon.png │ ├── published_articles_icon_pressed.png │ ├── radio_round_toggle_off_icon.png │ ├── radio_round_toggle_on_icon.png │ ├── radio_toggle_off_icon.png │ ├── radio_toggle_on_icon.png │ ├── raidionics-icon.png │ ├── raidionics-logo-square.png │ ├── raidionics-logo.png │ ├── reporting_icon.png │ ├── research_community_icon.png │ ├── research_community_icon_pressed.png │ ├── research_icon.png │ ├── restart_counterclockwise_icon.png │ ├── segmentation_icon.png │ ├── show_around_icon.png │ ├── show_around_icon_pressed.png │ ├── shrink_arrow.png │ ├── statistics_chartbars_icon.png │ ├── stolavs-logo.png │ ├── study_icon.png │ ├── tag-icon.png │ ├── trash-bin_icon.png │ ├── tweesteden-hospital-logo.png │ ├── ucsf-hospital-logo.png │ ├── uncollapsed_icon.png │ ├── upload_icon.png │ ├── utrecht-hospital-logo.png │ └── vienna-hospital-logo.png ├── LogReaderThread.py ├── RaidionicsMainWindow.py ├── SinglePatientComponent │ ├── CentralAreaExecutionWidget.py │ ├── CentralAreaWidget.py │ ├── CentralDisplayArea │ │ ├── CentralDisplayAreaWidget.py │ │ ├── Custom3DView.py │ │ ├── CustomQGraphicsView.py │ │ ├── MultipleTimestamps │ │ │ ├── MTSCentralDisplayAreaWidget.py │ │ │ └── __init__.py │ │ ├── QCollapsibleView.py │ │ └── __init__.py │ ├── LayersInteractorSidePanel │ │ ├── ActionsInteractor │ │ │ ├── ActionsInteractorWidget.py │ │ │ └── __init__.py │ │ ├── AnnotationLayersInteractor │ │ │ ├── AnnotationSingleLayerWidget.py │ │ │ ├── AnnotationsLayersInteractor.py │ │ │ └── __init__.py │ │ ├── AtlasLayersInteractor │ │ │ ├── AtlasSingleLayerCollapsibleGroupBox.py │ │ │ ├── AtlasSingleLayerWidget.py │ │ │ ├── AtlasesLayersInteractor.py │ │ │ └── __init__.py │ │ ├── MRIVolumesInteractor │ │ │ ├── MRISeriesLayerWidget.py │ │ │ ├── MRIVolumesLayerInteractor.py │ │ │ └── __init__.py │ │ ├── SinglePatientLayersWidget.py │ │ ├── TimestampsInteractor │ │ │ ├── TimestampLayerWidget.py │ │ │ ├── TimestampsLayerInteractor.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── PatientResultsSidePanel │ │ ├── PatientResultsSinglePatientSidePanelWidget.py │ │ ├── SinglePatientResultsWidget.py │ │ ├── SurgicalReportingWidget.py │ │ ├── TumorCharacteristicsWidget.py │ │ └── __init__.py │ ├── ProcessProgressWidget.py │ ├── SinglePatientWidget.py │ └── __init__.py ├── StudyBatchComponent │ ├── PatientsListingPanel │ │ ├── PatientListingWidgetItem.py │ │ ├── StudyPatientListingWidget.py │ │ └── __init__.py │ ├── PatientsSummaryPanel │ │ ├── StudyPatientsContentSummaryPanelWidget.py │ │ ├── StudyPatientsReportingSummaryWidget.py │ │ ├── StudyPatientsSegmentationSummaryWidget.py │ │ ├── StudyPatientsSummaryPanelWidget.py │ │ └── __init__.py │ ├── StudiesSidePanel │ │ ├── SingleStudyWidget.py │ │ ├── StudiesSidePanelWidget.py │ │ └── __init__.py │ ├── StudyBatchWidget.py │ └── __init__.py ├── UtilsWidgets │ ├── CustomQDialog │ │ ├── AboutDialog.py │ │ ├── ContrastAdjustmentDialog.py │ │ ├── DisplayDICOMMetadataDialog.py │ │ ├── ImportDICOMDataQDialog.py │ │ ├── ImportDataQDialog.py │ │ ├── ImportFoldersQDialog.py │ │ ├── KeyboardShortcutsDialog.py │ │ ├── LogsViewerDialog.py │ │ ├── ResearchCommunityDialog.py │ │ ├── SavePatientChangesDialog.py │ │ ├── SoftwareSettingsDialog.py │ │ ├── TumorTypeSelectionQDialog.py │ │ ├── VolumeStatisticsDialog.py │ │ └── __init__.py │ ├── CustomQGroupBox │ │ ├── QCollapsibleGroupBox.py │ │ ├── QCollapsibleWidget.py │ │ └── __init__.py │ ├── CustomQTableWidget │ │ ├── ContextMenuQTableWidget.py │ │ ├── ImportDICOMQTableWidget.py │ │ └── __init__.py │ ├── QCircularProgressBar.py │ ├── QCustomIconsPushButton.py │ ├── QDoubleIconsPushButton.py │ ├── QRightIconPushButton.py │ └── __init__.py ├── WelcomeWidget.py └── __init__.py ├── integration_tests ├── __init__.py ├── batch_study_module │ ├── __init__.py │ ├── test_bm_creation_and_data_import.py │ ├── test_bm_creation_and_dicom_import.py │ ├── test_bm_creation_and_edit.py │ └── test_bm_reloading_and_edit.py └── single_patient_module │ ├── __init__.py │ ├── test_sp_creation_and_data_import.py │ ├── test_sp_creation_and_dicom_import.py │ ├── test_sp_creation_and_edit.py │ └── test_sp_reloading_and_edit.py ├── main.py ├── tests ├── __init__.py └── software_launch_test.py └── utils ├── __init__.py ├── backend_logic.py ├── data_structures ├── AnnotationStructure.py ├── AtlasStructure.py ├── InvestigationTimestampStructure.py ├── MRIVolumeStructure.py ├── PatientParametersStructure.py ├── ReportingStructure.py ├── StudyParametersStructure.py ├── UserPreferencesStructure.py └── __init__.py ├── logic ├── PipelineCreationHandler.py ├── PipelineResultsCollector.py └── __init__.py ├── models_download.py ├── patient_dicom.py ├── software_config.py └── utilities.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: dbouget 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Computer settings (please complete the following information):** 14 | - Operating System : [e.g., Windows 10] 15 | - Processor (CPU) type: [e.g., Intel i5] 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 23 | **Error messages** 24 | Please copy the last lines from the session_log.log file, accessible inside the raidionics root folder. The file lives in C:\Users\\.raidionics (on Windows) or /home//.raidionics/ (Linux). 25 | If applicable, also provide any error message that could have been prompted in the console/terminal. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/build_macos.yml: -------------------------------------------------------------------------------- 1 | name: Build macOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | env: 13 | MACOSX_DEPLOYMENT_TARGET: 10.15 14 | 15 | jobs: 16 | build: 17 | name: Build packages 18 | runs-on: macos-13 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python 3.10 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.10" 25 | 26 | - name: Setup OpenGL 27 | run: brew install mesa-glu libxrender libxi libxkbcommon 28 | 29 | - name: Download ANTs 30 | uses: robinraju/release-downloader@main 31 | with: 32 | repository: "raidionics/Raidionics-dependencies" 33 | latest: true 34 | fileName: "ANTsX-v2.4.3_macos.tar.gz" 35 | out-file-path: "downloads" 36 | 37 | - name: Extract ANTs 38 | run: | 39 | cd ${{github.workspace}}/downloads/ 40 | tar -xzf ANTsX-v2.4.3_macos.tar.gz -C ${{github.workspace}}/downloads/ 41 | mkdir ${{github.workspace}}/ANTs 42 | mkdir ${{github.workspace}}/ANTs/install 43 | # mv ${{github.workspace}}/downloads/install ${{github.workspace}}/ANTs/ 44 | 45 | - name: Install dependencies 46 | run: | 47 | python -m pip install --upgrade pip 48 | pip install raidionicsrads 49 | pip install -r assets/requirements.txt 50 | # pip install matplotlib 51 | # pip install --force-reinstall --no-cache-dir pyside6 52 | 53 | - name: Integration tests 54 | timeout-minutes: 10 55 | env: 56 | DISPLAY: ':99.0' 57 | run: | 58 | pip install pytest-qt pytest-cov pytest-timeout 59 | pytest -vvv --cov=gui --cov=utils ${{github.workspace}}/integration_tests --timeout=120 --timeout-method=thread --log-cli-level=DEBUG 60 | 61 | - name: Build software 62 | run: | 63 | pip install pyinstaller 64 | mkdir tmp_dependencies 65 | pyinstaller --log-level INFO --noconfirm --clean assets/main.spec 66 | 67 | - name: Test executable 68 | env: 69 | DISPLAY: ':1' 70 | run: QT_QPA_PLATFORM="offscreen" ./dist/Raidionics/Raidionics & sleep 5; kill -INT %+ 71 | shell: bash 72 | 73 | - name: Test GUI startup 74 | env: 75 | DISPLAY: ':1' 76 | run: | 77 | export QT_QPA_PLATFORM="offscreen" 78 | cd ${{github.workspace}}/tests && python software_launch_test.py 79 | 80 | - name: Make installer 81 | run: | 82 | git clone https://github.com/dbouget/quickpkg.git 83 | quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.1-macOS.pkg 84 | cp -r Raidionics-1.3.1-macOS.pkg dist/Raidionics-1.3.1-macOS-x86_64.pkg 85 | 86 | - name: Upload package 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: Package 90 | path: ${{github.workspace}}/dist/Raidionics-* 91 | if-no-files-found: error 92 | -------------------------------------------------------------------------------- /.github/workflows/build_macos_arm.yml: -------------------------------------------------------------------------------- 1 | name: Build macOS ARM 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | # Allows to run the workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | env: 14 | MACOSX_DEPLOYMENT_TARGET: 11.0 15 | 16 | jobs: 17 | build: 18 | name: Build packages 19 | runs-on: macos-14 20 | defaults: 21 | run: 22 | shell: bash 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 3.10 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.10" 30 | 31 | - name: Setup OpenGL 32 | run: brew install mesa-glu libxi libxkbcommon 33 | 34 | - name: Download ANTs 35 | uses: robinraju/release-downloader@main 36 | with: 37 | repository: "raidionics/Raidionics-dependencies" 38 | latest: true 39 | fileName: "ANTsX-v2.4.3_macos_arm.tar.gz" 40 | out-file-path: "downloads" 41 | 42 | - name: Extract ANTs 43 | run: | 44 | cd ${{github.workspace}}/downloads/ 45 | tar -xzf ANTsX-v2.4.3_macos_arm.tar.gz -C ${{github.workspace}}/downloads/ 46 | mkdir ${{github.workspace}}/ANTs 47 | mv ${{github.workspace}}/downloads/install ${{github.workspace}}/ANTs/ 48 | 49 | - name: Install dependencies 50 | run: | 51 | python -m pip install --upgrade pip 52 | pip install raidionicsrads 53 | pip install -r assets/requirements.txt 54 | # pip install matplotlib 55 | # pip install --force-reinstall --no-cache-dir pyside6 56 | 57 | - name: Integration tests 58 | timeout-minutes: 10 59 | env: 60 | DISPLAY: ':99.0' 61 | run: | 62 | pip install pytest-qt pytest-cov pytest-timeout 63 | pytest -vvv --cov=gui --cov=utils ${{github.workspace}}/integration_tests --timeout=120 --timeout-method=thread --log-cli-level=DEBUG 64 | 65 | - name: Clean build artifacts 66 | run: | 67 | rm -rf build dist __pycache__ tmp_dependencies 68 | 69 | - name: Build software 70 | env: 71 | PYINSTALLER_LOGLEVEL: DEBUG 72 | run: | 73 | pip install pyinstaller 74 | mkdir tmp_dependencies 75 | python -c "import sys; sys.setrecursionlimit(10000); print('Recursion limit during build:', sys.getrecursionlimit())" 76 | pyinstaller --log-level INFO --noconfirm --clean assets/main_arm.spec 77 | 78 | - name: Test executable 79 | env: 80 | DISPLAY: ':1' 81 | run: QT_QPA_PLATFORM="offscreen" ./dist/Raidionics/Raidionics & sleep 5; kill -INT %+ 82 | shell: bash 83 | 84 | - name: Test GUI startup 85 | env: 86 | DISPLAY: ':1' 87 | run: | 88 | export QT_QPA_PLATFORM="offscreen" 89 | cd ${{github.workspace}}/tests && python software_launch_test.py 90 | 91 | - name: Make installer 92 | run: | 93 | git clone https://github.com/dbouget/quickpkg.git 94 | quickpkg/quickpkg dist/Raidionics.app --output Raidionics-1.3.1-macOS.pkg 95 | cp -r Raidionics-1.3.1-macOS.pkg dist/Raidionics-1.3.1-macOS-arm64.pkg 96 | 97 | - name: Upload package 98 | uses: actions/upload-artifact@v4 99 | with: 100 | name: Package 101 | path: ${{github.workspace}}/dist/Raidionics-* 102 | if-no-files-found: error -------------------------------------------------------------------------------- /.github/workflows/build_ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Build Ubuntu 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Build packages 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.9" 22 | 23 | - name: Free Up GitHub Actions Ubuntu Runner Disk Space 24 | uses: dbouget/free-disk-space@main 25 | with: 26 | # This might remove tools that are actually needed, if set to "true" but frees about 6 GB 27 | tool-cache: false 28 | 29 | # All of these default to true, but feel free to set to "false" if necessary for your workflow 30 | android: true 31 | dotnet: true 32 | haskell: true 33 | large-packages: true 34 | swap-storage: true 35 | 36 | - name: Install CL dependencies 37 | run: | 38 | apt update && apt install -y sudo 39 | sudo apt install -y clinfo 40 | 41 | - name: Setup OpenCL 42 | run: | 43 | sudo add-apt-repository ppa:ocl-icd/ppa 44 | sudo apt update 45 | sudo apt-get install -y pocl-opencl-icd 46 | 47 | - name: Setup OpenGL 48 | run: sudo apt install -y '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libegl1 49 | 50 | - name: Debug clinfo 51 | run: clinfo 52 | 53 | - name: Setup X virtual framebuffer 54 | run: sudo apt-get install -y xvfb 55 | 56 | - name: Download ANTs 57 | uses: robinraju/release-downloader@main 58 | with: 59 | repository: "raidionics/Raidionics-dependencies" 60 | latest: true 61 | fileName: "ANTsX-v2.4.3_ubuntu.tar.gz" 62 | out-file-path: "downloads" 63 | 64 | - name: Extract ANTs 65 | run: | 66 | cd ${{github.workspace}}/downloads/ 67 | tar -xzf ANTsX-v2.4.3_ubuntu.tar.gz -C ${{github.workspace}}/downloads/ 68 | mkdir ${{github.workspace}}/ANTs 69 | mkdir ${{github.workspace}}/ANTs/install 70 | # mv ${{github.workspace}}/downloads/install ${{github.workspace}}/ANTs/ 71 | 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install --upgrade pip 75 | pip install raidionicsrads 76 | pip install -r assets/requirements.txt 77 | 78 | - name: Integration tests 79 | timeout-minutes: 10 80 | env: 81 | DISPLAY: ':99.0' 82 | run: | 83 | pip install pytest-qt pytest-cov pytest-timeout 84 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX 85 | pytest -vvv --cov=gui --cov=utils ${{github.workspace}}/integration_tests --cov-report=xml --timeout=120 --log-cli-level=DEBUG 86 | 87 | - name: Build software 88 | run: | 89 | pip install pyinstaller 90 | mkdir tmp_dependencies 91 | pyinstaller --log-level INFO --noconfirm --clean assets/main.spec 92 | 93 | - name: Test executable xvfb 94 | env: 95 | DISPLAY: ':1' 96 | run: xvfb-run ./dist/Raidionics/Raidionics & sleep 5; kill -INT %+ 97 | shell: bash 98 | 99 | - name: Test executable 100 | env: 101 | DISPLAY: ':1' 102 | run: QT_QPA_PLATFORM="offscreen" ./dist/Raidionics/Raidionics & sleep 5; kill -INT %+ 103 | shell: bash 104 | 105 | - name: Test GUI startup 106 | env: 107 | DISPLAY: ':1' 108 | run: | 109 | export QT_QPA_PLATFORM="offscreen" 110 | cd ${{github.workspace}}/tests && python software_launch_test.py 111 | 112 | - name: Make installer 113 | run: | 114 | mkdir -p assets/Raidionics_ubuntu/usr/local/bin 115 | cp -r dist/Raidionics assets/Raidionics_ubuntu/usr/local/bin 116 | dpkg-deb --build --root-owner-group assets/Raidionics_ubuntu 117 | ls -la 118 | cp -r assets/Raidionics_ubuntu.deb dist/Raidionics-1.3.1-ubuntu.deb 119 | 120 | - name: Upload package 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: Package 124 | path: ${{github.workspace}}/dist/Raidionics-* 125 | if-no-files-found: error 126 | 127 | - name: Upload coverage to Codecov 128 | uses: codecov/codecov-action@v4 129 | with: 130 | token: ${{ secrets.CODECOV_TOKEN }} 131 | verbose: true 132 | -------------------------------------------------------------------------------- /.github/workflows/build_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Build packages 15 | runs-on: windows-2022 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.9" 22 | 23 | # Not using the ANTs c++ backend on Windows, have to investigate how to execute the bash scripts. 24 | # But have to download it due to the expected copying of the install folder... 25 | - name: Download ANTs 26 | uses: robinraju/release-downloader@main 27 | with: 28 | repository: "raidionics/Raidionics-dependencies" 29 | latest: true 30 | fileName: "ANTsX-v2.4.3_windows.zip" 31 | out-file-path: "downloads" 32 | 33 | - name: Extract ANTs 34 | run: | 35 | cd ${{github.workspace}}/downloads/ 36 | mkdir ${{github.workspace}}/ANTs 37 | mkdir ${{github.workspace}}/ANTs/install 38 | # tar -xf ANTsX-v2.4.3_windows.zip -C ${{github.workspace}}/ANTs/install/ 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | ls 44 | pip install raidionicsrads 45 | pip install -r assets/requirements.txt 46 | pip install matplotlib 47 | pip install --force-reinstall --no-cache-dir pyside6 48 | 49 | # - name: Integration tests 50 | # env: 51 | # DISPLAY: ':99.0' 52 | # run: | 53 | # pip install pytest-qt pytest-cov 54 | # pytest --cov=gui --cov=utils ${{github.workspace}}/integration_tests 55 | 56 | - name: Build software 57 | run: | 58 | pip install pyinstaller 59 | mkdir tmp_dependencies 60 | pyinstaller --log-level INFO --noconfirm --clean assets/main.spec 61 | 62 | - name: Test executable 63 | run: QT_QPA_PLATFORM="offscreen" ./dist/Raidionics/Raidionics & sleep 5; kill -INT %+ 64 | shell: bash 65 | 66 | - name: Test GUI startup 67 | run: cd ${{github.workspace}}/tests && python software_launch_test.py 68 | 69 | - name: Make installer 70 | run: | 71 | makensis.exe assets/Raidionics.nsi 72 | cp -r assets/Raidionics-1.3.1-win.exe dist/Raidionics-1.3.1-win.exe 73 | 74 | - name: Upload package 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: Package 78 | path: ${{github.workspace}}/dist/Raidionics-* 79 | if-no-files-found: error 80 | -------------------------------------------------------------------------------- /.github/workflows/delete-drafts.yml: -------------------------------------------------------------------------------- 1 | name: Delete Old Draft Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | #- '*' # for every push, run build 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Delete drafts 15 | uses: hugo19941994/delete-draft-releases@v1.0.0 16 | with: 17 | threshold: 5d 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # use glob syntax. 2 | syntax: glob 3 | 4 | .DS_Store 5 | *.elc 6 | *.pyc 7 | *~ 8 | .idea/ 9 | *.xml 10 | os 11 | numpy 12 | *.h5 13 | *.hd5 14 | *.ckpt 15 | *.mhd 16 | *.raw 17 | venv*/ 18 | tmp_dependencies/ 19 | build/ 20 | dist/ 21 | segmentation/resources/ 22 | *.ini 23 | *.sqlite3 24 | *.idea 25 | *.vscode 26 | /home 27 | /Atlases 28 | ANTs/ 29 | *.exe 30 | *.pkg 31 | *.deb 32 | GSI-RADS 33 | GSI-RADS-1.* 34 | quickpkg/ 35 | *.nii.gz 36 | *.csv 37 | *.zip 38 | *.tar.gz 39 | assets/Raidionics_ubuntu/usr/local/bin 40 | .coverage 41 | *.raidionics 42 | integrationtests/ 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "raidionics_rads_lib"] 2 | path = raidionics_rads_lib 3 | url = https://github.com/dbouget/raidionics_rads_lib.git 4 | branch = submodule_seg 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, dbouget 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Raidionics

4 |

Open software for AI-based pre- and postoperative brain tumor segmentation and standardized reporting

5 | 6 | [![GitHub Downloads](https://img.shields.io/github/downloads/dbouget/Raidionics/total?label=GitHub%20downloads&logo=github)](https://github.com/dbouget/Raidionics/releases) 7 | [![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause) 8 | [![Paper](https://zenodo.org/badge/DOI/10.1038/s41598-023-42048-7.svg)](https://doi.org/10.1038/s41598-023-42048-7) 9 | [![codecov](https://codecov.io/gh/raidionics/Raidionics/branch/master/graph/badge.svg?token=ZSPQVR7RKX)](https://codecov.io/gh/raidionics/Raidionics) 10 | [![GitHub release](https://img.shields.io/github/v/release/raidionics/raidionics?sort=semver)](https://github.com/raidionics/raidionics/releases) 11 | 12 | **Raidionics** was developed by SINTEF Medical Image Analysis. A paper presenting the software and some benchmarks has been published in [Scientific Reports](https://doi.org/10.1038/s41598-023-42048-7). 13 | 14 | 15 |
16 | 17 | ## [Getting started](https://github.com/raidionics/Raidionics#getting-started) 18 | 19 | * Please visit the [wiki](https://github.com/dbouget/Raidionics/wiki) to know more about usage, use-cases, and access tutorials. 20 | * For any issue, please report them [here](https://github.com/dbouget/Raidionics/issues). 21 | * Frequently asked questions (FAQs) can be found [here](https://github.com/dbouget/Raidionics/wiki/Frequently-Asked-Questions-(FAQ)). 22 | 23 | ## [Installation](https://github.com/raidionics/Raidionics#installation) 24 | An installer is provided for the three main Operating Systems: Windows (v10, 64-bit), Ubuntu Linux (>= 18.04), macOS (>= 10.15 Catalina), and macOS ARM (M1/M2 chip). 25 | The software can be downloaded from [here](https://github.com/dbouget/Raidionics/releases) (see **Assets**). 26 | 27 | **NOTE:** For reinstallation, it is recommended to _manually delete_ the `.raidionics/` folder located inside your home directory. 28 | 29 | These steps are only needed to do once: 30 | 1) Download the installer to your Operating System. 31 | 2) Right click the downloaded file, click "open", and follow the instructions to install. 32 | 3) Search for the software "Raidionics" and double click to run. 33 | 34 | ## [Demos and tutorials](https://github.com/raidionics/Raidionics/demos-and-tutorials) 35 | 36 | Very simple demonstrations of the software can be found on [YouTube](https://www.youtube.com/@davidbouget5649). Tutorials can be found in the [wiki](https://github.com/dbouget/Raidionics/wiki). 37 | 38 | [![Watch the video](assets/images/raidionics_yt_thumbnail.png)](https://www.youtube.com/watch?v=cm9Mxg7Fuj8) 39 | 40 | ## [Continuous integration](https://github.com/raidionics/Raidionics/continuous-integration) 41 | 42 | | Operating System | Status | 43 | | - | - | 44 | | **Windows** | ![CI](https://github.com/raidionics/Raidionics/workflows/Build%20Windows/badge.svg?branch=master) | 45 | | **Ubuntu** | ![CI](https://github.com/raidionics/Raidionics/workflows/Build%20Ubuntu/badge.svg?branch=master) | 46 | | **macOS_x86-64** | ![CI](https://github.com/raidionics/Raidionics/workflows/Build%20macOS/badge.svg?branch=master) | 47 | | **macOS_ARM** | ![CI](https://github.com/raidionics/Raidionics/workflows/Build%20macOS%20ARM/badge.svg?branch=master) | 48 | 49 | ## [How to cite](https://github.com/raidionics/Raidionics#how-to-cite) 50 | If you are using Raidionics in your research, please cite the following references. 51 | 52 | The final software including updated performance metrics for preoperative tumors and introducing postoperative tumor segmentation: 53 | ``` 54 | @article{bouget2023raidionics, 55 | author = {Bouget, David and Alsinan, Demah and Gaitan, Valeria and Holden Helland, Ragnhild and Pedersen, André and Solheim, Ole and Reinertsen, Ingerid}, 56 | year = {2023}, 57 | month = {09}, 58 | pages = {}, 59 | title = {Raidionics: an open software for pre-and postoperative central nervous system tumor segmentation and standardized reporting}, 60 | volume = {13}, 61 | journal = {Scientific Reports}, 62 | doi = {10.1038/s41598-023-42048-7}, 63 | } 64 | ``` 65 | 66 | For the preliminary preoperative tumor segmentation validation and software features: 67 | ``` 68 | @article{bouget2022preoptumorseg, 69 | title={Preoperative Brain Tumor Imaging: Models and Software for Segmentation and Standardized Reporting}, 70 | author={Bouget, David and Pedersen, André and Jakola, Asgeir S. and Kavouridis, Vasileios and Emblem, Kyrre E. and Eijgelaar, Roelant S. and Kommers, Ivar and Ardon, Hilko and Barkhof, Frederik and Bello, Lorenzo and Berger, Mitchel S. and Conti Nibali, Marco and Furtner, Julia and Hervey-Jumper, Shawn and Idema, Albert J. S. and Kiesel, Barbara and Kloet, Alfred and Mandonnet, Emmanuel and Müller, Domenique M. J. and Robe, Pierre A. and Rossi, Marco and Sciortino, Tommaso and Van den Brink, Wimar A. and Wagemakers, Michiel and Widhalm, Georg and Witte, Marnix G. and Zwinderman, Aeilko H. and De Witt Hamer, Philip C. and Solheim, Ole and Reinertsen, Ingerid}, 71 | journal={Frontiers in Neurology}, 72 | volume={13}, 73 | year={2022}, 74 | url={https://www.frontiersin.org/articles/10.3389/fneur.2022.932219}, 75 | doi={10.3389/fneur.2022.932219}, 76 | issn={1664-2295} 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /assets/Raidionics.nsi: -------------------------------------------------------------------------------- 1 | !define APP_NAME "Raidionics" 2 | !define COMP_NAME "SINTEF" 3 | !define VERSION "1.3.1" 4 | !define DESCRIPTION "Application" 5 | !define INSTALLER_NAME "Raidionics-1.3.1-win.exe" 6 | !define MAIN_APP_EXE "Raidionics.exe" 7 | !define INSTALL_TYPE "SetShellVarContext current" 8 | !define REG_ROOT "HKLM" 9 | 10 | !define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}" 11 | !define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" 12 | !define REG_START_MENU "Start Menu Folder" 13 | 14 | var SM_Folder 15 | 16 | ###################################################################### 17 | 18 | SetCompressor ZLIB 19 | Name "${APP_NAME}" 20 | Caption "${APP_NAME}" 21 | OutFile "${INSTALLER_NAME}" 22 | BrandingText "${APP_NAME}" 23 | XPStyle on 24 | InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" "" 25 | InstallDir "$PROGRAMFILES\Raidionics" 26 | 27 | !include 'MUI.nsh' 28 | !define MUI_ICON "images\raidionics-logo.ico" 29 | 30 | !define MUI_ABORTWARNING 31 | !define MUI_UNABORTWARNING 32 | 33 | !insertmacro MUI_PAGE_WELCOME 34 | 35 | !insertmacro MUI_PAGE_DIRECTORY 36 | 37 | !ifdef REG_START_MENU 38 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Raidionics" 39 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" 40 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" 41 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" 42 | !insertmacro MUI_PAGE_STARTMENU Application $SM_Folder 43 | !endif 44 | 45 | !insertmacro MUI_PAGE_INSTFILES 46 | !insertmacro MUI_PAGE_FINISH 47 | 48 | !insertmacro MUI_UNPAGE_CONFIRM 49 | !insertmacro MUI_UNPAGE_INSTFILES 50 | !insertmacro MUI_UNPAGE_FINISH 51 | 52 | !insertmacro MUI_LANGUAGE "English" 53 | 54 | # name the installer 55 | OutFile "${INSTALLER_NAME}" 56 | 57 | Section 58 | SectionEnd 59 | 60 | 61 | ####################### UNINSTALL BEFORE UPGRADE ##################### 62 | Section "" SecUninstallPrevious 63 | Call UninstallPrevious 64 | SectionEnd 65 | 66 | Function UninstallPrevious 67 | ; Check for uninstaller. 68 | DetailPrint "Checking for previous Raidionics versions" 69 | ReadRegStr $R0 HKCU "$INSTDIR" "UninstallString" 70 | 71 | ${If} $R0 == "" 72 | ReadRegStr $R0 HKLM "$INSTDIR" "UninstallString" 73 | ${If} $R0 == "" 74 | DetailPrint "No previous installation found" 75 | Goto Done 76 | ${EndIf} 77 | ${EndIf} 78 | 79 | DetailPrint "Removing previous installation." 80 | ; Run the uninstaller silently. 81 | ExecWait '"$R0" /S _?=$INSTDIR' $0 82 | DetailPrint "Uninstaller returned $0" 83 | Done: 84 | FunctionEnd 85 | ###################################################################### 86 | 87 | # default section start; every NSIS script has at least one section. 88 | 89 | ###################################################################### 90 | Section -MainProgram 91 | ${INSTALL_TYPE} 92 | SetOverwrite ifnewer 93 | SetOutPath "$INSTDIR" 94 | SectionEnd 95 | ###################################################################### 96 | 97 | Section -Icons_Reg 98 | SetOutPath "$INSTDIR" 99 | WriteUninstaller "$INSTDIR\uninstall.exe" 100 | 101 | # define the output path for this file 102 | SetOutPath $INSTDIR 103 | 104 | WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" 105 | WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}" 106 | WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe" 107 | WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\images\raidionics-logo.ico" 108 | WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" 109 | WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" 110 | 111 | # Delete .raidionics/ directory if it exists 112 | RMDir /r "$PROFILE\.raidionics\" 113 | 114 | # Create directory 115 | CreateDirectory $INSTDIR 116 | 117 | # PACKAGE ENTIRE CONTENT OF BUNDLE THE NEW BINARY! 118 | File /nonfatal /a /r "..\dist\Raidionics\*" 119 | ExecWait "$INSTDIR\Raidionics-installed.exe" 120 | 121 | !ifdef REG_START_MENU 122 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 123 | CreateDirectory "$SMPROGRAMS\$SM_Folder" 124 | CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" 125 | CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" 126 | CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" 127 | 128 | !insertmacro MUI_STARTMENU_WRITE_END 129 | !endif 130 | 131 | # default section end 132 | SectionEnd 133 | 134 | ###################################################################### 135 | 136 | # Remove location where program is installed as well as addition .raidionics/ directory in home directory 137 | Section Uninstall 138 | ${INSTALL_TYPE} 139 | RMDir /r "$INSTDIR" 140 | RMDir /r "$PROFILE\.raidionics\" 141 | 142 | !ifdef REG_START_MENU 143 | !insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder 144 | Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" 145 | Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" 146 | Delete "$DESKTOP\${APP_NAME}.lnk" 147 | 148 | RMDir "$SMPROGRAMS\$SM_Folder" 149 | !endif 150 | 151 | DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" 152 | DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" 153 | SectionEnd 154 | 155 | ###################################################################### 156 | -------------------------------------------------------------------------------- /assets/Raidionics_ubuntu/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: Raidionics 2 | Version: 1.3.1 3 | Architecture: i386 4 | Maintainer: David Bouget 5 | Description: Raidionics—Reporting and Data System. 6 | Software to automatically segment tumors and their from pre-operative CTs and MRIs, and report them in a standardized manner. 7 | -------------------------------------------------------------------------------- /assets/Raidionics_ubuntu/usr/share/applications/Raidionics.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Raidionics 3 | Comment=Raidionics 4 | Exec=/usr/local/bin/Raidionics/Raidionics 5 | Terminal=false 6 | Type=Application 7 | Icon=/usr/local/bin/Raidionics/assets/images/raidionics-logo.png 8 | Categories=public.app-categorical.medical -------------------------------------------------------------------------------- /assets/hooks/hook-PySide6.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | 3 | hiddenimports = [ 4 | "PySide6.QtCore", 5 | "PySide6.QtGui", 6 | "PySide6.QtWidgets", 7 | "PySide6.QtWebEngineWidgets", 8 | # only include the ones you actually use 9 | ] 10 | -------------------------------------------------------------------------------- /assets/hooks/hook-ants.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("ants") 5 | 6 | datas = collect_data_files("ants") 7 | 8 | #pyinstaller --noconfirm --clean --onefile --paths=/home/andrep/workspace/neuro_rads_prototype/venv/lib/python3.6/site-packages/ants main_custom.spec -------------------------------------------------------------------------------- /assets/hooks/hook-antspyx.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("antspyx") 5 | 6 | datas = collect_data_files("antspyx") -------------------------------------------------------------------------------- /assets/hooks/hook-dipy.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("dipy") 5 | 6 | datas = collect_data_files("dipy") -------------------------------------------------------------------------------- /assets/hooks/hook-distutils.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("distutils") 5 | 6 | datas = collect_data_files("distutils") -------------------------------------------------------------------------------- /assets/hooks/hook-gdown.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files, copy_metadata 3 | 4 | hiddenimports = collect_submodules("gdown") 5 | 6 | datas = copy_metadata("gdown") 7 | datas += collect_data_files("gdown") 8 | -------------------------------------------------------------------------------- /assets/hooks/hook-gevent.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("gevent") 5 | 6 | datas = collect_data_files("gevent") 7 | -------------------------------------------------------------------------------- /assets/hooks/hook-nibabel.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("nibabel") 5 | 6 | datas = collect_data_files("nibabel") -------------------------------------------------------------------------------- /assets/hooks/hook-pydicom.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("pydicom") 5 | 6 | datas = collect_data_files("pydicom") 7 | -------------------------------------------------------------------------------- /assets/hooks/hook-raidionicsrads.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files, copy_metadata 3 | 4 | hiddenimports = collect_submodules("raidionicsrads") 5 | 6 | datas = copy_metadata("raidionicsrads") 7 | datas += collect_data_files("raidionicsrads") 8 | -------------------------------------------------------------------------------- /assets/hooks/hook-raidionicsseg.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files, copy_metadata 3 | 4 | hiddenimports = collect_submodules("raidionicsseg") 5 | 6 | datas = copy_metadata("raidionicsseg") 7 | datas += collect_data_files("raidionicsseg") 8 | -------------------------------------------------------------------------------- /assets/hooks/hook-rtutils.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("rtutils") 5 | 6 | datas = collect_data_files("rtutils") 7 | -------------------------------------------------------------------------------- /assets/hooks/hook-scikit-learn.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("scikit-learn") 5 | 6 | datas = collect_data_files("scikit-learn") -------------------------------------------------------------------------------- /assets/hooks/hook-scipy.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("scipy") 5 | 6 | datas = collect_data_files("scipy") 7 | -------------------------------------------------------------------------------- /assets/hooks/hook-sklearn.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("sklearn") 5 | 6 | datas = collect_data_files("sklearn") -------------------------------------------------------------------------------- /assets/hooks/hook-statsmodels.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | hiddenimports = collect_submodules("statsmodels") 5 | 6 | datas = collect_data_files("statsmodels") -------------------------------------------------------------------------------- /assets/hooks/set_recursion_limit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.setrecursionlimit(5000) 3 | print("Recursion limit hook active") -------------------------------------------------------------------------------- /assets/images/Preview_SingleUse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/Preview_SingleUse.gif -------------------------------------------------------------------------------- /assets/images/interface-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/interface-snapshot.png -------------------------------------------------------------------------------- /assets/images/raidionics-logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/raidionics-logo.icns -------------------------------------------------------------------------------- /assets/images/raidionics-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/raidionics-logo.ico -------------------------------------------------------------------------------- /assets/images/raidionics-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/raidionics-logo.png -------------------------------------------------------------------------------- /assets/images/raidionics_yt_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/assets/images/raidionics_yt_thumbnail.png -------------------------------------------------------------------------------- /assets/main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | import sys 3 | import os 4 | import ants 5 | import shutil 6 | from PyInstaller.utils.hooks import collect_data_files 7 | from numpy import loadtxt 8 | 9 | 10 | # necessary for MacOS 11 | os.environ['LC_CTYPE'] = "en_US.UTF-8" 12 | os.environ['LANG'] = "en_US.UTF-8" 13 | 14 | block_cipher = None 15 | cwd = os.path.abspath(os.getcwd()) 16 | 17 | print("CWD:", cwd) 18 | print("PLATFORM:", sys.platform) 19 | 20 | # fix hidden imports 21 | hidden_imports = ["names", "plotly", "gdown", "ants", "sklearn", "statsmodels", "gevent", "distutils", 22 | "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", "PySide6.QtWebEngineWidgets", "raidionicsrads", 23 | "raidionicsseg", "rtutils"] 24 | 25 | # copy dependencies and images, remove if folder already exists 26 | if os.path.exists(cwd + "/tmp_dependencies/"): 27 | shutil.rmtree(cwd + "/tmp_dependencies/") 28 | shutil.copytree(cwd + "/assets/images/", cwd + "/tmp_dependencies/assets/images/") 29 | shutil.copytree(cwd + "/utils/", cwd + "/tmp_dependencies/utils/") 30 | shutil.copytree(cwd + "/gui/", cwd + "/tmp_dependencies/gui/") 31 | shutil.copytree(cwd + "/ANTs/install/", cwd + "/tmp_dependencies/ANTs/") 32 | 33 | a = Analysis([cwd + '/main.py'], 34 | pathex=[cwd], 35 | binaries=[], 36 | datas=[], 37 | hiddenimports=hidden_imports, 38 | hookspath=[os.path.join(cwd, "assets", "hooks")], 39 | runtime_hooks=[], 40 | excludes=['PySide6.QtQml'], 41 | win_no_prefer_redirects=False, 42 | win_private_assemblies=False, 43 | cipher=block_cipher, 44 | noarchive=False 45 | ) 46 | 47 | pyz = PYZ(a.pure, a.zipped_data, 48 | cipher=block_cipher 49 | ) 50 | 51 | exe = EXE(pyz, 52 | a.scripts, 53 | [], 54 | exclude_binaries=True, 55 | name='Raidionics', 56 | debug=False, 57 | bootloader_ignore_signals=False, 58 | strip=False, 59 | upx=True, 60 | console=True, 61 | icon=cwd + "/tmp_dependencies/assets/images/raidionics-logo.ico" if sys.platform != "darwin" else None 62 | ) 63 | coll = COLLECT(exe, 64 | a.binaries, 65 | Tree(cwd + "/tmp_dependencies/"), 66 | a.zipfiles, 67 | a.datas, 68 | strip=False, 69 | upx=True, 70 | upx_exclude=[], 71 | name='Raidionics' 72 | ) 73 | 74 | # to compile everything into a macOS Bundle (.APP) 75 | if sys.platform == "darwin": 76 | app = BUNDLE(coll, 77 | name='Raidionics.app', 78 | icon=cwd + "/tmp_dependencies/assets/images/raidionics-logo.icns", 79 | bundle_identifier=None, 80 | info_plist={ 81 | 'NSRequiresAquaSystemAppearance': 'true', 82 | 'CFBundleDisplayName': 'Raidionics', 83 | 'CFBundleExecutable': 'Raidionics', 84 | 'CFBundleIdentifier': 'Raidionics', 85 | 'CFBundleInfoDictionaryVersion': '6.0', 86 | 'CFBundleName': 'Raidionics', 87 | 'CFBundleVersion': '1.3.1', 88 | 'CFBundlePackageType': 'APPL', 89 | 'LSBackgroundOnly': 'false', 90 | }, 91 | ) 92 | -------------------------------------------------------------------------------- /assets/main_arm.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5) 3 | import os 4 | import shutil 5 | from PyInstaller.utils.hooks import collect_data_files 6 | from numpy import loadtxt 7 | 8 | # necessary for MacOS 9 | os.environ['LC_CTYPE'] = "en_US.UTF-8" 10 | os.environ['LANG'] = "en_US.UTF-8" 11 | 12 | block_cipher = None 13 | cwd = os.path.abspath(os.getcwd()) 14 | 15 | print("CWD:", cwd) 16 | print("PLATFORM:", sys.platform) 17 | 18 | #def safe_symlink(src, dst): 19 | # try: 20 | # os.symlink(src, dst) 21 | # except FileExistsError: 22 | # os.remove(dst) 23 | # os.symlink(src, dst) 24 | # os.symlink = safe_symlink 25 | 26 | # fix hidden imports 27 | hidden_imports = ["names", "plotly", "gdown", "sklearn", "statsmodels", "gevent", "distutils", "PySide6.QtGui", 28 | "PySide6.QtCore", "PySide6.QtWidgets", "PySide6.QtWebEngineWidgets", "raidionicsrads", "raidionicsseg", "rtutils"] 29 | 30 | # copy dependencies and images, remove if folder already exists 31 | if os.path.exists(cwd + "/tmp_dependencies/"): 32 | shutil.rmtree(cwd + "/tmp_dependencies/") 33 | shutil.copytree(cwd + "/assets/images/", cwd + "/tmp_dependencies/assets/images/", symlinks=False) 34 | shutil.copytree(cwd + "/utils/", cwd + "/tmp_dependencies/utils/", symlinks=False) 35 | shutil.copytree(cwd + "/gui/", cwd + "/tmp_dependencies/gui/", symlinks=False) 36 | shutil.copytree(cwd + "/ANTs/install/", cwd + "/tmp_dependencies/ANTs/", symlinks=False) 37 | 38 | a = Analysis([cwd + '/main.py'], 39 | pathex=[cwd], 40 | binaries=[], 41 | datas = [], 42 | hiddenimports=hidden_imports, 43 | hookspath=[os.path.join(cwd, "assets", "hooks")], 44 | runtime_hooks=[os.path.join(cwd, "assets", "hooks", "set_recursion_limit.py")], 45 | excludes=['PySide6.QtQml', "PySide6.Qt3DAnimation"], 46 | win_no_prefer_redirects=False, 47 | win_private_assemblies=False, 48 | cipher=block_cipher, 49 | noarchive=False 50 | ) 51 | 52 | pyz = PYZ(a.pure, a.zipped_data, 53 | cipher=block_cipher 54 | ) 55 | 56 | exe = EXE(pyz, 57 | a.scripts, 58 | [], 59 | exclude_binaries=True, 60 | name='Raidionics', 61 | debug=False, 62 | bootloader_ignore_signals=False, 63 | strip=False, 64 | upx=True, 65 | console=True, 66 | icon=cwd + "/tmp_dependencies/assets/images/raidionics-logo.ico" if sys.platform != "darwin" else None 67 | ) 68 | coll = COLLECT(exe, 69 | a.binaries, 70 | Tree(cwd + "/tmp_dependencies/"), 71 | a.zipfiles, 72 | a.datas, 73 | strip=False, 74 | upx=True, 75 | upx_exclude=[], 76 | name='Raidionics' 77 | ) 78 | 79 | # to compile everything into a macOS Bundle (.APP) 80 | if sys.platform == "darwin": 81 | app = BUNDLE(coll, 82 | name='Raidionics.app', 83 | icon=cwd + "/tmp_dependencies/assets/images/raidionics-logo.icns", 84 | bundle_identifier=None, 85 | info_plist={ 86 | 'NSRequiresAquaSystemAppearance': 'true', 87 | 'CFBundleDisplayName': 'Raidionics', 88 | 'CFBundleExecutable': 'Raidionics', 89 | 'CFBundleIdentifier': 'Raidionics', 90 | 'CFBundleInfoDictionaryVersion': '6.0', 91 | 'CFBundleName': 'Raidionics', 92 | 'CFBundleVersion': '1.3.1', 93 | 'CFBundlePackageType': 'APPL', 94 | 'LSBackgroundOnly': 'false', 95 | }, 96 | ) 97 | -------------------------------------------------------------------------------- /assets/requirements.txt: -------------------------------------------------------------------------------- 1 | gdown 2 | requests 3 | names 4 | PySide6 5 | plotly 6 | git+https://github.com/dbouget/rt-utils.git@scikit-image -------------------------------------------------------------------------------- /build_launcher.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import PyInstaller.__main__ 3 | 4 | # Set recursion limit early 5 | sys.setrecursionlimit(10000) 6 | print("Recursion limit before build:", sys.getrecursionlimit()) 7 | 8 | PyInstaller.__main__.run([ 9 | '--log-level=INFO', 10 | '--noconfirm', 11 | '--clean', 12 | 'assets/main_arm.spec' 13 | ]) 14 | -------------------------------------------------------------------------------- /gui/Images/about_raidionics_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/about_raidionics_icon.png -------------------------------------------------------------------------------- /gui/Images/about_raidionics_icon_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/about_raidionics_icon_pressed.png -------------------------------------------------------------------------------- /gui/Images/alkmaar-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/alkmaar-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/amsterdam-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/amsterdam-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/arrow_circle_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/arrow_circle_down.png -------------------------------------------------------------------------------- /gui/Images/arrow_circle_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/arrow_circle_up.png -------------------------------------------------------------------------------- /gui/Images/arrow_down_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/arrow_down_icon.png -------------------------------------------------------------------------------- /gui/Images/arrow_right_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/arrow_right_icon.png -------------------------------------------------------------------------------- /gui/Images/brats-challenge-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/brats-challenge-logo.png -------------------------------------------------------------------------------- /gui/Images/brigham-boston-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/brigham-boston-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/browse_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/browse_icon.png -------------------------------------------------------------------------------- /gui/Images/cancer-institute-ams-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/cancer-institute-ams-logo.png -------------------------------------------------------------------------------- /gui/Images/circle_question_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/circle_question_icon.png -------------------------------------------------------------------------------- /gui/Images/classification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/classification_icon.png -------------------------------------------------------------------------------- /gui/Images/close_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/close_icon.png -------------------------------------------------------------------------------- /gui/Images/closed_eye_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/closed_eye_icon.png -------------------------------------------------------------------------------- /gui/Images/collapsed_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/collapsed_icon.png -------------------------------------------------------------------------------- /gui/Images/combobox-arrow-icon-10x7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/combobox-arrow-icon-10x7.png -------------------------------------------------------------------------------- /gui/Images/contrast_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/contrast_icon.png -------------------------------------------------------------------------------- /gui/Images/data_load_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/data_load_icon.png -------------------------------------------------------------------------------- /gui/Images/data_save_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/data_save_icon.png -------------------------------------------------------------------------------- /gui/Images/database_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/database_icon.png -------------------------------------------------------------------------------- /gui/Images/delete-cross-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/delete-cross-icon.png -------------------------------------------------------------------------------- /gui/Images/dicom_load_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/dicom_load_icon.png -------------------------------------------------------------------------------- /gui/Images/download-tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/download-tray-icon.png -------------------------------------------------------------------------------- /gui/Images/download_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/download_icon.png -------------------------------------------------------------------------------- /gui/Images/download_icon_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/download_icon_black.png -------------------------------------------------------------------------------- /gui/Images/expand_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/expand_arrow.png -------------------------------------------------------------------------------- /gui/Images/file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/file_icon.png -------------------------------------------------------------------------------- /gui/Images/filled_arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/filled_arrow_right.png -------------------------------------------------------------------------------- /gui/Images/find_more_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/find_more_icon.png -------------------------------------------------------------------------------- /gui/Images/find_more_icon_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/find_more_icon_pressed.png -------------------------------------------------------------------------------- /gui/Images/floppy_disk_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/floppy_disk_icon.png -------------------------------------------------------------------------------- /gui/Images/folder_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/folder_icon.png -------------------------------------------------------------------------------- /gui/Images/folder_simple_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/folder_simple_icon.png -------------------------------------------------------------------------------- /gui/Images/github-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/github-icon.png -------------------------------------------------------------------------------- /gui/Images/globe-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/globe-icon.png -------------------------------------------------------------------------------- /gui/Images/gothenburg-hospital-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/gothenburg-hospital-icon.png -------------------------------------------------------------------------------- /gui/Images/groningen-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/groningen-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/haaglanden-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/haaglanden-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/help_wavy_question_icon_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/help_wavy_question_icon_blue.png -------------------------------------------------------------------------------- /gui/Images/home-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/home-icon.png -------------------------------------------------------------------------------- /gui/Images/humanitas-milan-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/humanitas-milan-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/isala-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/isala-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/issues_suggestions_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/issues_suggestions_icon.png -------------------------------------------------------------------------------- /gui/Images/issues_suggestions_pressed_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/issues_suggestions_pressed_icon.png -------------------------------------------------------------------------------- /gui/Images/jumpto-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/jumpto-icon.png -------------------------------------------------------------------------------- /gui/Images/large-arrow-down-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/large-arrow-down-icon.png -------------------------------------------------------------------------------- /gui/Images/lariboisiere-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/lariboisiere-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/load_file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/load_file_icon.png -------------------------------------------------------------------------------- /gui/Images/logs_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/logs_icon.png -------------------------------------------------------------------------------- /gui/Images/minus_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/minus_icon.png -------------------------------------------------------------------------------- /gui/Images/more-dots-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/more-dots-icon.png -------------------------------------------------------------------------------- /gui/Images/neurorads-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/neurorads-logo.png -------------------------------------------------------------------------------- /gui/Images/opened_eye_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/opened_eye_icon.png -------------------------------------------------------------------------------- /gui/Images/oslo_univeristy_hospital_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/oslo_univeristy_hospital_icon.png -------------------------------------------------------------------------------- /gui/Images/patient-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/patient-icon.png -------------------------------------------------------------------------------- /gui/Images/play_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/play_icon.png -------------------------------------------------------------------------------- /gui/Images/plus_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/plus_icon.png -------------------------------------------------------------------------------- /gui/Images/power-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/power-icon.png -------------------------------------------------------------------------------- /gui/Images/preferences-sliders-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/preferences-sliders-icon.png -------------------------------------------------------------------------------- /gui/Images/published_articles_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/published_articles_icon.png -------------------------------------------------------------------------------- /gui/Images/published_articles_icon_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/published_articles_icon_pressed.png -------------------------------------------------------------------------------- /gui/Images/radio_round_toggle_off_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/radio_round_toggle_off_icon.png -------------------------------------------------------------------------------- /gui/Images/radio_round_toggle_on_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/radio_round_toggle_on_icon.png -------------------------------------------------------------------------------- /gui/Images/radio_toggle_off_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/radio_toggle_off_icon.png -------------------------------------------------------------------------------- /gui/Images/radio_toggle_on_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/radio_toggle_on_icon.png -------------------------------------------------------------------------------- /gui/Images/raidionics-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/raidionics-icon.png -------------------------------------------------------------------------------- /gui/Images/raidionics-logo-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/raidionics-logo-square.png -------------------------------------------------------------------------------- /gui/Images/raidionics-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/raidionics-logo.png -------------------------------------------------------------------------------- /gui/Images/reporting_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/reporting_icon.png -------------------------------------------------------------------------------- /gui/Images/research_community_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/research_community_icon.png -------------------------------------------------------------------------------- /gui/Images/research_community_icon_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/research_community_icon_pressed.png -------------------------------------------------------------------------------- /gui/Images/research_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/research_icon.png -------------------------------------------------------------------------------- /gui/Images/restart_counterclockwise_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/restart_counterclockwise_icon.png -------------------------------------------------------------------------------- /gui/Images/segmentation_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/segmentation_icon.png -------------------------------------------------------------------------------- /gui/Images/show_around_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/show_around_icon.png -------------------------------------------------------------------------------- /gui/Images/show_around_icon_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/show_around_icon_pressed.png -------------------------------------------------------------------------------- /gui/Images/shrink_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/shrink_arrow.png -------------------------------------------------------------------------------- /gui/Images/statistics_chartbars_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/statistics_chartbars_icon.png -------------------------------------------------------------------------------- /gui/Images/stolavs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/stolavs-logo.png -------------------------------------------------------------------------------- /gui/Images/study_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/study_icon.png -------------------------------------------------------------------------------- /gui/Images/tag-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/tag-icon.png -------------------------------------------------------------------------------- /gui/Images/trash-bin_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/trash-bin_icon.png -------------------------------------------------------------------------------- /gui/Images/tweesteden-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/tweesteden-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/ucsf-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/ucsf-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/uncollapsed_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/uncollapsed_icon.png -------------------------------------------------------------------------------- /gui/Images/upload_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/upload_icon.png -------------------------------------------------------------------------------- /gui/Images/utrecht-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/utrecht-hospital-logo.png -------------------------------------------------------------------------------- /gui/Images/vienna-hospital-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/Images/vienna-hospital-logo.png -------------------------------------------------------------------------------- /gui/LogReaderThread.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from PySide6.QtCore import Signal, QThread 4 | from utils.software_config import SoftwareConfigResources 5 | 6 | 7 | class LogReaderThread(QThread): 8 | message = Signal(str) 9 | 10 | def run(self) -> None: 11 | logfile = open(SoftwareConfigResources.getInstance().get_session_log_filename(), 'r') 12 | newlines = self.follow(logfile) 13 | for l in newlines: 14 | self.write(l) 15 | 16 | def write(self, text): 17 | self.message.emit(text) 18 | 19 | def stop(self): 20 | self.terminate() 21 | 22 | def follow(self, thefile): 23 | # seek the end of the file 24 | thefile.seek(0, os.SEEK_END) 25 | 26 | # start infinite loop 27 | while self.isRunning(): 28 | # read last line of file 29 | line = thefile.readline() # sleep if file hasn't been updated 30 | if not line: 31 | time.sleep(0.1) 32 | continue 33 | 34 | yield line 35 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralAreaWidget.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QStackedWidget 2 | from PySide6.QtGui import QIcon, QPixmap, QColor 3 | from PySide6.QtCore import Qt, QSize, Signal 4 | import logging 5 | from utils.software_config import SoftwareConfigResources 6 | from gui.SinglePatientComponent.CentralDisplayArea.CentralDisplayAreaWidget import CentralDisplayAreaWidget 7 | from gui.SinglePatientComponent.CentralAreaExecutionWidget import CentralAreaExecutionWidget 8 | 9 | 10 | class CentralAreaWidget(QWidget): 11 | """ 12 | 13 | """ 14 | reset_central_viewer = Signal() 15 | mri_volume_imported = Signal(str) # The str is the unique id for the MRI volume, belonging to the active patient 16 | annotation_volume_imported = Signal(str) # The str is the unique id for the annotation volume, belonging to the active patient 17 | atlas_volume_imported = Signal(str) # The str is the unique id for the atlas volume, belonging to the active patient 18 | patient_view_toggled = Signal(str) 19 | volume_view_toggled = Signal(str, bool) 20 | volume_contrast_changed = Signal(str) 21 | annotation_view_toggled = Signal(str, bool) 22 | annotation_opacity_changed = Signal(str, int) 23 | annotation_color_changed = Signal(str, QColor) 24 | annotation_display_state_changed = Signal() 25 | atlas_view_toggled = Signal(str, bool) 26 | atlas_structure_view_toggled = Signal(str, int, bool) 27 | atlas_structure_color_changed = Signal(str, int, QColor) 28 | atlas_structure_opacity_changed = Signal(str, int, int) 29 | standardized_report_imported = Signal(str) 30 | radiological_sequences_imported = Signal() 31 | process_started = Signal() 32 | process_finished = Signal() 33 | pipeline_execution_requested = Signal(str) 34 | 35 | def __init__(self, parent=None): 36 | super(CentralAreaWidget, self).__init__() 37 | self.parent = parent 38 | self.widget_name = "central_area_widget" 39 | self.setBaseSize(QSize((885 / SoftwareConfigResources.getInstance().get_optimal_dimensions().width()) * self.parent.baseSize().width(), 40 | ((935 / SoftwareConfigResources.getInstance().get_optimal_dimensions().height()) * self.parent.baseSize().height()))) 41 | logging.debug("Setting CentralAreaWidget dimensions to {}.".format(self.size())) 42 | 43 | self.__set_interface() 44 | self.__set_layout_dimensions() 45 | self.__set_stylesheets() 46 | self.__set_connections() 47 | 48 | def __set_interface(self): 49 | # self.setBaseSize(self.parent.baseSize()) 50 | self.base_layout = QVBoxLayout(self) 51 | self.base_layout.setContentsMargins(0, 0, 0, 0) 52 | self.base_layout.setSpacing(0) 53 | self.view_stackedwidget = QStackedWidget() 54 | self.display_area_widget = CentralDisplayAreaWidget(self) 55 | self.execution_area_widget = CentralAreaExecutionWidget(self) 56 | self.view_stackedwidget.addWidget(self.display_area_widget) 57 | self.base_layout.addWidget(self.view_stackedwidget) 58 | self.base_layout.addWidget(self.execution_area_widget) 59 | 60 | def __set_stylesheets(self): 61 | pass 62 | 63 | def __set_connections(self): 64 | self.__set_inner_connections() 65 | self.__set_cross_connections() 66 | 67 | def __set_inner_connections(self): 68 | pass 69 | 70 | def __set_cross_connections(self): 71 | # Connections related to data display (from right-hand panel to update the central viewer) 72 | self.reset_central_viewer.connect(self.display_area_widget.reset_viewer) 73 | self.volume_view_toggled.connect(self.display_area_widget.on_volume_layer_toggled) 74 | self.volume_contrast_changed.connect(self.display_area_widget.on_volume_contrast_changed) 75 | self.annotation_view_toggled.connect(self.display_area_widget.on_annotation_layer_toggled) 76 | self.annotation_opacity_changed.connect(self.display_area_widget.on_annotation_opacity_changed) 77 | self.annotation_color_changed.connect(self.display_area_widget.on_annotation_color_changed) 78 | # self.atlas_view_toggled.connect(self.display_area_widget.on_atlas_layer_toggled) 79 | self.atlas_structure_view_toggled.connect(self.display_area_widget.on_atlas_structure_view_toggled) 80 | self.atlas_structure_color_changed.connect(self.display_area_widget.on_atlas_structure_color_changed) 81 | self.atlas_structure_opacity_changed.connect(self.display_area_widget.on_atlas_structure_opacity_changed) 82 | 83 | # Connections related to data loading (from central viewer panel to update the right-handed panel) 84 | self.display_area_widget.mri_volume_imported.connect(self.on_import_mri_volume) 85 | self.display_area_widget.annotation_volume_imported.connect(self.on_import_annotation) 86 | self.display_area_widget.atlas_volume_imported.connect(self.on_import_atlas) 87 | self.display_area_widget.annotation_display_state_changed.connect(self.annotation_display_state_changed) 88 | 89 | # Connections from/to the execution area 90 | self.execution_area_widget.annotation_volume_imported.connect(self.on_import_annotation) 91 | self.execution_area_widget.atlas_volume_imported.connect(self.on_import_atlas) 92 | self.execution_area_widget.standardized_report_imported.connect(self.standardized_report_imported) 93 | self.execution_area_widget.radiological_sequences_imported.connect(self.radiological_sequences_imported) 94 | self.execution_area_widget.process_started.connect(self.process_started) 95 | self.execution_area_widget.process_finished.connect(self.process_finished) 96 | self.volume_view_toggled.connect(self.execution_area_widget.on_volume_layer_toggled) 97 | self.pipeline_execution_requested.connect(self.execution_area_widget.on_pipeline_execution) 98 | 99 | # Connections from the left patient panel 100 | self.patient_view_toggled.connect(self.display_area_widget.on_patient_selected) 101 | 102 | def __set_layout_dimensions(self): 103 | self.view_stackedwidget.setBaseSize(QSize(self.baseSize().width(), self.baseSize().height()-150)) 104 | 105 | def get_widget_name(self): 106 | return self.widget_name 107 | 108 | def on_reload_interface(self): 109 | self.display_area_widget.on_patient_selected() 110 | 111 | def on_patient_selected(self, patient_uid): 112 | self.patient_view_toggled.emit(patient_uid) 113 | 114 | def on_import_mri_volume(self, uid): 115 | self.mri_volume_imported.emit(uid) 116 | 117 | def on_import_annotation(self, uid): 118 | self.annotation_volume_imported.emit(uid) 119 | 120 | def on_import_atlas(self, uid): 121 | self.atlas_volume_imported.emit(uid) 122 | 123 | def on_volume_layer_toggled(self, uid, state): 124 | self.volume_view_toggled.emit(uid, state) 125 | 126 | def on_volume_contrast_changed(self, uid): 127 | self.volume_contrast_changed.emit(uid) 128 | 129 | def on_annotation_layer_toggled(self, uid, state): 130 | self.annotation_view_toggled.emit(uid, state) 131 | 132 | def on_annotation_opacity_changed(self, annotation_uid, opacity): 133 | self.annotation_opacity_changed.emit(annotation_uid, opacity) 134 | 135 | def on_annotation_color_changed(self, annotation_uid, color): 136 | self.annotation_color_changed.emit(annotation_uid, color) 137 | 138 | def on_atlas_layer_toggled(self, uid, state): 139 | self.atlas_view_toggled.emit(uid, state) 140 | 141 | def on_batch_process_started(self) -> None: 142 | self.execution_area_widget.on_process_started() 143 | 144 | def on_batch_process_finished(self) -> None: 145 | self.execution_area_widget.on_process_finished() 146 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralDisplayArea/Custom3DView.py: -------------------------------------------------------------------------------- 1 | # from PySide6.QtDataVisualization import QCustom3DVolume, Q3DScatter 2 | # from PySide6.QtCore import Qt, Signal, QPoint, QSize 3 | # 4 | # import logging 5 | # 6 | # 7 | # class Custom3DView(QCustom3DVolume): 8 | # """ 9 | # @TODO 3D viewer for rendering the annotation, might be only available in PySide6, to check. 10 | # """ 11 | # 12 | # def __init__(self, size=QSize(150, 150), parent=None): 13 | # super(QCustom3DVolume, self).__init__() 14 | # self.parent = parent 15 | # # self.setBaseSize(size) 16 | # # self.setMinimumSize(size) 17 | # # logging.debug("Setting CustomQGraphicsView dimensions to {}.\n".format(self.size())) 18 | # self.graph = Q3DScatter 19 | # self.__set_interface() 20 | # self.__set_stylesheets() 21 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralDisplayArea/MultipleTimestamps/MTSCentralDisplayAreaWidget.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from PySide6.QtWidgets import QWidget, QLabel, QGridLayout, QPushButton 3 | from PySide6.QtCore import QSize, Signal 4 | import numpy as np 5 | import logging 6 | 7 | from gui.SinglePatientComponent.CentralDisplayArea.CustomQGraphicsView import CustomQGraphicsView 8 | from utils.software_config import SoftwareConfigResources 9 | 10 | 11 | class MTSCentralDisplayAreaWidget(QWidget): 12 | """ 13 | 14 | """ 15 | 16 | def __init__(self, parent=None): 17 | super(MTSCentralDisplayAreaWidget, self).__init__() 18 | self.parent = parent 19 | # self.setMinimumWidth((int(885/4) / SoftwareConfigResources.getInstance().get_optimal_dimensions().width()) * self.parent.size().width()) 20 | # self.setMinimumHeight((int(800/4) / SoftwareConfigResources.getInstance().get_optimal_dimensions().height()) * self.parent.size().height()) 21 | self.setBaseSize(QSize((1325 / SoftwareConfigResources.getInstance().get_optimal_dimensions().width()) * self.parent.baseSize().width(), 22 | ((950 / SoftwareConfigResources.getInstance().get_optimal_dimensions().height()) * self.parent.baseSize().height()))) 23 | logging.debug("Setting MTSCentralDisplayAreaWidget dimensions to {}.".format(self.size())) 24 | self.__set_interface() 25 | # self.__set_layout_dimensions() 26 | self.__set_stylesheets() 27 | self.__set_connections() 28 | 29 | def resizeEvent(self, event): 30 | new_size = event.size() 31 | 32 | def __set_interface(self): 33 | pass 34 | 35 | def __set_layout_dimensions(self): 36 | pass 37 | 38 | def __set_stylesheets(self): 39 | pass 40 | 41 | def __set_connections(self): 42 | pass 43 | 44 | def reset_overlay(self): 45 | """ 46 | """ 47 | pass 48 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralDisplayArea/MultipleTimestamps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/SinglePatientComponent/CentralDisplayArea/MultipleTimestamps/__init__.py -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralDisplayArea/QCollapsibleView.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QScrollArea, QPushButton, QSizePolicy,\ 2 | QGridLayout, QSpacerItem, QStackedLayout 3 | from PySide6.QtGui import QIcon, QPixmap, QFont 4 | from PySide6.QtCore import QSize, Signal 5 | import os 6 | 7 | 8 | class Header(QWidget): 9 | def __init__(self, timestamp, content_widget, parent=None): 10 | """ 11 | 12 | """ 13 | super(Header, self).__init__() 14 | self.timestamp = timestamp 15 | self.content = content_widget 16 | self.parent = parent 17 | self.expand_pixmap = QPixmap(os.path.join(os.path.dirname(os.path.realpath(__file__)), 18 | '../../Images/expand_arrow.png')) 19 | self.collapse_pixmap = QPixmap(os.path.join(os.path.dirname(os.path.realpath(__file__)), 20 | '../../Images/shrink_arrow.png')) 21 | 22 | header_layout = QHBoxLayout(self) 23 | self.timestamp_label = QLabel() 24 | self.timestamp_label.setText(self.timestamp) 25 | self.icon = QLabel() 26 | self.icon.setPixmap(self.expand_pixmap) 27 | 28 | header_layout.addWidget(self.timestamp_label) 29 | header_layout.addWidget(self.icon) 30 | self.collapse() 31 | 32 | def mousePressEvent(self, *args): 33 | """Handle mouse events, call the function to toggle groups""" 34 | self.expand() if not self.content.isVisible() else self.collapse() 35 | 36 | def expand(self): 37 | self.content.setVisible(True) 38 | self.icon.setPixmap(self.expand_pixmap) 39 | 40 | def collapse(self): 41 | self.content.setVisible(False) 42 | self.icon.setPixmap(self.collapse_pixmap) 43 | 44 | 45 | class QCollapsibleView(QWidget): 46 | """ 47 | 48 | """ 49 | def __init__(self, name, parent=None): 50 | """ 51 | 52 | """ 53 | super(QCollapsibleView, self).__init__() 54 | self.parent = parent 55 | layout = QVBoxLayout(self) 56 | layout.setContentsMargins(0, 0, 0, 0) 57 | self._content_widget = QWidget() 58 | header = Header(name, self._content_widget, parent) 59 | layout.addWidget(header) 60 | layout.addWidget(self._content_widget) 61 | 62 | # assign header methods to instance attributes so they can be called outside of this class 63 | self.collapse = header.collapse 64 | self.expand = header.expand 65 | self.toggle = header.mousePressEvent 66 | 67 | @property 68 | def contentWidget(self): 69 | """Getter for the content widget 70 | 71 | Returns: Content widget 72 | """ 73 | return self._content_widget 74 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/CentralDisplayArea/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/SinglePatientComponent/CentralDisplayArea/__init__.py -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/ActionsInteractor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/SinglePatientComponent/LayersInteractorSidePanel/ActionsInteractor/__init__.py -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/AnnotationLayersInteractor/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/AtlasesLayersInteractor.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGridLayout, QSpacerItem 2 | from PySide6.QtCore import QSize, Signal 3 | from PySide6.QtGui import QColor 4 | import os 5 | import logging 6 | 7 | from gui.UtilsWidgets.CustomQGroupBox.QCollapsibleWidget import QCollapsibleWidget 8 | from gui.SinglePatientComponent.LayersInteractorSidePanel.AtlasLayersInteractor.AtlasSingleLayerCollapsibleGroupBox import AtlasSingleLayerCollapsibleGroupBox 9 | from gui.SinglePatientComponent.LayersInteractorSidePanel.AtlasLayersInteractor.AtlasSingleLayerWidget import AtlasSingleLayerWidget 10 | 11 | from utils.software_config import SoftwareConfigResources 12 | 13 | 14 | class AtlasesLayersInteractor(QCollapsibleWidget): 15 | """ 16 | 17 | """ 18 | atlas_structure_view_toggled = Signal(str, int, bool) 19 | atlas_opacity_changed = Signal(str, int, int) 20 | atlas_color_changed = Signal(str, int, QColor) 21 | 22 | def __init__(self, parent=None): 23 | super(AtlasesLayersInteractor, self).__init__("Structures") 24 | self.parent = parent 25 | self.volumes_widget = {} 26 | self.__set_interface() 27 | self.__set_connections() 28 | self.__set_layout_dimensions() 29 | self.__set_stylesheets() 30 | 31 | def __set_interface(self): 32 | self.set_icon_filenames(expand_fn=os.path.join(os.path.dirname(os.path.realpath(__file__)), 33 | '../../../Images/arrow_down_icon.png'), 34 | collapse_fn=os.path.join(os.path.dirname(os.path.realpath(__file__)), 35 | '../../../Images/arrow_right_icon.png')) 36 | 37 | def __set_connections(self): 38 | pass 39 | 40 | def __set_layout_dimensions(self): 41 | self.header.set_icon_size(QSize(35, 35)) 42 | self.header.title_label.setFixedHeight(40) 43 | self.header.background_label.setFixedHeight(45) 44 | 45 | def __set_stylesheets(self): 46 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 47 | font_color = software_ss["Color7"] 48 | background_color = software_ss["White"] 49 | pressed_background_color = software_ss["Color6"] 50 | 51 | self.header.background_label.setStyleSheet(""" 52 | QLabel{ 53 | background-color: """ + background_color + """; 54 | border: 2px solid black; 55 | border-radius: 2px; 56 | }""") 57 | 58 | self.header.title_label.setStyleSheet(""" 59 | QLabel{ 60 | background-color: """ + background_color + """; 61 | color: """ + font_color + """; 62 | font:bold; 63 | font-size:14px; 64 | padding-left:40px; 65 | text-align: left; 66 | border: 0px; 67 | }""") 68 | 69 | self.header.icon_label.setStyleSheet(""" 70 | QLabel{ 71 | background-color: """ + background_color + """; 72 | color: """ + font_color + """; 73 | border: 0px; 74 | }""") 75 | 76 | self.content_widget.setStyleSheet(""" 77 | QWidget{ 78 | background-color: """ + background_color + """; 79 | }""") 80 | 81 | def adjustSize(self): 82 | pass 83 | 84 | def reset(self): 85 | """ 86 | 87 | """ 88 | for w in list(self.volumes_widget): 89 | self.content_layout.removeWidget(self.volumes_widget[w]) 90 | self.volumes_widget[w].deleteLater() 91 | self.volumes_widget.pop(w) 92 | self.header.collapse() 93 | 94 | 95 | def get_layer_widget_length(self) -> int: 96 | return len(self.volumes_widget) 97 | 98 | def get_layer_widget_by_index(self, index: int) -> AtlasSingleLayerWidget: 99 | if index >= len(self.volumes_widget): 100 | raise ValueError("[AtlasesLayerInteractor] Trying to retrieve an Atlas layer widget with an out-of-bound index value.") 101 | return self.volumes_widget[list(self.volumes_widget.keys())[index]] 102 | 103 | def get_layer_widget_by_visible_name(self, name: str) -> AtlasSingleLayerWidget: 104 | for w in list(self.volumes_widget.keys()): 105 | if self.volumes_widget[w].display_name_lineedit.text() == name: 106 | return self.volumes_widget[w] 107 | raise ValueError("[AtlasesLayerInteractor] Trying to retrieve a non-existing Atlas layer widget by visible name with: {}.".format(name)) 108 | 109 | def on_volume_view_toggled(self, volume_uid: str, state: bool) -> None: 110 | """ 111 | A change of the displayed MRI volume has been requested by the user, which should lead to an update of all 112 | atlas objects to only show the ones linked to this MRI volume. 113 | 114 | Parameters 115 | ---------- 116 | volume_uid: str 117 | Internal unique identifier for the MRI volume selected by the user. 118 | state: bool 119 | Unused variable, the state should always be True here. 120 | """ 121 | self.reset() 122 | active_patient = SoftwareConfigResources.getInstance().get_active_patient() 123 | 124 | for atlas_id in active_patient.get_all_atlases_for_mri(mri_volume_uid=volume_uid): 125 | if not atlas_id in list(self.volumes_widget.keys()): 126 | self.on_import_volume(atlas_id) 127 | 128 | self.adjustSize() # To force a repaint of the layout with the new elements 129 | 130 | def on_patient_view_toggled(self, patient_uid: str, timestamp_uid: str) -> None: 131 | """ 132 | When a patient has been selected in the left-hand side panel, setting up the display of the first of its 133 | MRI volumes (if multiple) and corresponding atlas volumes. 134 | 135 | Parameters 136 | ---------- 137 | patient_uid: str 138 | Internal unique identifier for the MRI volume selected by the user. 139 | """ 140 | active_patient = SoftwareConfigResources.getInstance().patients_parameters[patient_uid] 141 | volumes_uids = active_patient.get_all_mri_volumes_for_timestamp(timestamp_uid=timestamp_uid) 142 | if len(volumes_uids) > 0: 143 | for atlas_id in active_patient.get_all_atlases_for_mri(mri_volume_uid=volumes_uids[0]): 144 | if not atlas_id in list(self.volumes_widget.keys()): 145 | self.on_import_volume(atlas_id) 146 | self.adjustSize() 147 | 148 | def on_import_volume(self, volume_id): 149 | volume_widget = AtlasSingleLayerWidget(uid=volume_id, parent=self) 150 | self.volumes_widget[volume_id] = volume_widget 151 | self.content_layout.insertWidget(self.content_layout.count(), volume_widget) 152 | # line_label = QLabel() 153 | # line_label.setFixedHeight(3) 154 | # line_label.setStyleSheet("QLabel{background-color: rgb(214, 214, 214);}") 155 | # self.content_layout.insertWidget(self.content_layout.count(), line_label) 156 | 157 | # On-the-fly signals/slots connection for the newly created QWidget 158 | # volume_widget.header_pushbutton.clicked.connect(self.adjustSize) 159 | # volume_widget.right_clicked.connect(self.on_visibility_clicked) 160 | volume_widget.structure_view_toggled.connect(self.on_atlas_structure_view_toggled) 161 | volume_widget.structure_color_value_changed.connect(self.__on_atlas_color_changed) 162 | volume_widget.structure_opacity_value_changed.connect(self.atlas_opacity_changed) 163 | volume_widget.resizeRequested.connect(self.adjustSize) 164 | # Triggers a repaint with adjusted size for the layout 165 | self.adjustSize() 166 | 167 | def __on_atlas_color_changed(self, atlas_uid: str, structure_index: int, color: QColor) -> None: 168 | self.atlas_color_changed.emit(atlas_uid, structure_index, color) 169 | 170 | def on_visibility_clicked(self, uid, state): 171 | self.atlas_view_toggled.emit(uid, state) 172 | 173 | def on_atlas_structure_view_toggled(self, atlas_uid, structure_index, state): 174 | self.atlas_structure_view_toggled.emit(atlas_uid, structure_index, state) 175 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/SinglePatientComponent/LayersInteractorSidePanel/AtlasLayersInteractor/__init__.py -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/MRIVolumesInteractor/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/SinglePatientComponent/LayersInteractorSidePanel/TimestampsInteractor/__init__.py -------------------------------------------------------------------------------- /gui/SinglePatientComponent/LayersInteractorSidePanel/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/PatientResultsSidePanel/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/SinglePatientComponent/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsListingPanel/PatientListingWidgetItem.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QScrollArea, QPushButton, QLabel, QSpacerItem,\ 2 | QGridLayout, QMenu 3 | from PySide6.QtCore import QSize, Qt, Signal, QPoint 4 | from PySide6.QtGui import QIcon, QPixmap, QAction 5 | import os 6 | import logging 7 | from utils.software_config import SoftwareConfigResources 8 | 9 | 10 | class PatientListingWidgetItem(QWidget): 11 | """ 12 | 13 | """ 14 | 15 | patient_selected = Signal(str) 16 | patient_removed = Signal(str) 17 | patient_refresh_triggered = Signal(str) 18 | 19 | def __init__(self, patient_uid: str, parent=None): 20 | super(PatientListingWidgetItem, self).__init__() 21 | self.parent = parent 22 | self.patient_uid = patient_uid 23 | # self.setFixedWidth(self.parent.baseSize().width()) 24 | # self.setBaseSize(QSize(self.width(), 30)) # Defining a base size is necessary as inner widgets depend on it. 25 | self.__set_interface() 26 | self.__set_layout_dimensions() 27 | self.__set_connections() 28 | self.__set_stylesheets() 29 | 30 | def __set_interface(self): 31 | self.layout = QHBoxLayout(self) 32 | self.layout.setSpacing(0) 33 | self.layout.setContentsMargins(0, 0, 10, 0) 34 | 35 | self.patient_uid_label = QLabel(SoftwareConfigResources.getInstance().patients_parameters[self.patient_uid].display_name) 36 | self.patient_uid_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) 37 | self.patient_investigation_pushbutton = QPushButton() 38 | self.patient_investigation_pushbutton.setIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 39 | '../../Images/jumpto-icon.png'))) 40 | self.patient_investigation_pushbutton.setToolTip("To visually inspect the patient's data.") 41 | self.patient_remove_pushbutton = QPushButton() 42 | self.patient_remove_pushbutton.setIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 43 | '../../Images/close_icon.png'))) 44 | self.patient_remove_pushbutton.setToolTip("To remove the patient from the study (but retained on disk).") 45 | # self.patient_remove_pushbutton.setEnabled(False) 46 | self.patient_refresh_pushbutton = QPushButton() 47 | self.patient_refresh_pushbutton.setIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 48 | '../../Images/restart_counterclockwise_icon.png'))) 49 | self.patient_refresh_pushbutton.setToolTip("To refresh the patient summary and statistics.") 50 | self.layout.addWidget(self.patient_remove_pushbutton) 51 | self.layout.addWidget(self.patient_investigation_pushbutton) 52 | self.layout.addWidget(self.patient_uid_label) 53 | self.layout.addWidget(self.patient_refresh_pushbutton) 54 | 55 | def __set_layout_dimensions(self): 56 | self.patient_uid_label.setFixedHeight(30) 57 | self.patient_investigation_pushbutton.setIconSize(QSize(25, 25)) 58 | self.patient_investigation_pushbutton.setFixedSize(QSize(30, 30)) 59 | self.patient_remove_pushbutton.setIconSize(QSize(25, 25)) 60 | self.patient_remove_pushbutton.setFixedSize(QSize(30, 30)) 61 | self.patient_refresh_pushbutton.setIconSize(QSize(25, 25)) 62 | self.patient_refresh_pushbutton.setFixedSize(QSize(30, 30)) 63 | 64 | def __set_connections(self): 65 | self.patient_investigation_pushbutton.clicked.connect(self.__on_patient_investigation_clicked) 66 | self.patient_remove_pushbutton.clicked.connect(self.__on_patient_remove_clicked) 67 | self.patient_refresh_pushbutton.clicked.connect(self.__on_patient_refresh_clicked) 68 | 69 | def __set_stylesheets(self): 70 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 71 | font_color = software_ss["Color7"] 72 | font_style = 'normal' 73 | background_color = software_ss["Color2"] 74 | pressed_background_color = software_ss["Color6"] 75 | 76 | self.setStyleSheet(""" 77 | PatientListingWidgetItem{ 78 | background-color: """ + background_color + """; 79 | border-style: solid; 80 | border-width: 1px; 81 | }""") 82 | 83 | self.patient_uid_label.setStyleSheet(""" 84 | QLabel{ 85 | background-color: """ + background_color + """; 86 | padding-left: 10px; 87 | color: """ + font_color + """; 88 | font-size: 14px; 89 | font-style: bold; 90 | }""") 91 | 92 | self.patient_investigation_pushbutton.setStyleSheet(""" 93 | QPushButton{ 94 | background-color: """ + background_color + """; 95 | color: """ + font_color + """; 96 | font: 12px; 97 | border-style: none; 98 | } 99 | QPushButton::hover{ 100 | border-style: solid; 101 | border-width: 1px; 102 | border-color: rgba(196, 196, 196, 1); 103 | } 104 | QPushButton:pressed{ 105 | border-style:inset; 106 | background-color: """ + pressed_background_color + """; 107 | }""") 108 | 109 | self.patient_remove_pushbutton.setStyleSheet(""" 110 | QPushButton{ 111 | background-color: """ + background_color + """; 112 | color: """ + font_color + """; 113 | font: 12px; 114 | border-style: none; 115 | } 116 | QPushButton::hover{ 117 | border-style: solid; 118 | border-width: 1px; 119 | border-color: rgba(196, 196, 196, 1); 120 | } 121 | QPushButton:pressed{ 122 | border-style:inset; 123 | background-color: """ + pressed_background_color + """; 124 | }""") 125 | 126 | self.patient_refresh_pushbutton.setStyleSheet(""" 127 | QPushButton{ 128 | background-color: """ + background_color + """; 129 | color: """ + font_color + """; 130 | font: 12px; 131 | border-style: none; 132 | } 133 | QPushButton::hover{ 134 | border-style: solid; 135 | border-width: 1px; 136 | border-color: rgba(196, 196, 196, 1); 137 | } 138 | QPushButton:pressed{ 139 | border-style:inset; 140 | background-color: """ + pressed_background_color + """; 141 | }""") 142 | 143 | def __on_patient_investigation_clicked(self): 144 | self.patient_selected.emit(self.patient_uid) 145 | 146 | def __on_patient_remove_clicked(self): 147 | code = SoftwareConfigResources.getInstance().get_active_study().remove_study_patient(self.patient_uid) 148 | if code == 0: # The patient is not in the study list (which should not happen), but somehow the widget exists. 149 | logging.warning("Removing patient {} from study was requested, but patient is not in the study...") 150 | self.patient_removed.emit(self.patient_uid) 151 | 152 | def __on_patient_refresh_clicked(self): 153 | """ 154 | """ 155 | self.patient_refresh_triggered.emit(self.patient_uid) 156 | 157 | def on_process_started(self) -> None: 158 | """ 159 | In order to trigger a GUI freeze where necessary. 160 | """ 161 | self.patient_remove_pushbutton.setEnabled(False) 162 | self.patient_refresh_pushbutton.setEnabled(False) 163 | 164 | def on_process_finished(self): 165 | self.patient_remove_pushbutton.setEnabled(True) 166 | self.patient_refresh_pushbutton.setEnabled(True) 167 | -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsListingPanel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/StudyBatchComponent/PatientsListingPanel/__init__.py -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsContentSummaryPanelWidget.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QScrollArea, QLabel, QSpacerItem,\ 3 | QGridLayout, QTreeWidget, QTreeWidgetItem 4 | from PySide6.QtCore import QSize, Qt, Signal 5 | from gui.StudyBatchComponent.PatientsListingPanel.PatientListingWidgetItem import PatientListingWidgetItem 6 | from utils.software_config import SoftwareConfigResources 7 | 8 | 9 | class StudyPatientsContentSummaryPanelWidget(QWidget): 10 | """ 11 | 12 | """ 13 | patient_selected = Signal(str) 14 | 15 | def __init__(self, parent=None): 16 | super(StudyPatientsContentSummaryPanelWidget, self).__init__() 17 | self.parent = parent 18 | self.__set_interface() 19 | self.__set_layout_dimensions() 20 | self.__set_connections() 21 | self.__set_stylesheets() 22 | 23 | def __set_interface(self): 24 | self.layout = QVBoxLayout(self) 25 | self.layout.setSpacing(0) 26 | self.layout.setContentsMargins(0, 0, 0, 0) 27 | self.patients_list_scrollarea = QScrollArea() 28 | self.patients_list_scrollarea.show() 29 | self.patients_list_scrollarea_layout = QVBoxLayout() 30 | self.patients_list_scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 31 | self.patients_list_scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 32 | self.patients_list_scrollarea.setWidgetResizable(True) 33 | self.patients_list_scrollarea_dummy_widget = QLabel() 34 | self.patients_list_scrollarea_layout.setSpacing(0) 35 | self.patients_list_scrollarea_layout.setContentsMargins(0, 0, 0, 0) 36 | self.patients_list_scrollarea_dummy_widget.setLayout(self.patients_list_scrollarea_layout) 37 | self.patients_list_scrollarea.setWidget(self.patients_list_scrollarea_dummy_widget) 38 | self.layout.addWidget(self.patients_list_scrollarea) 39 | self.__set_interface_listing_header() 40 | self.content_tree_widget = QTreeWidget() 41 | self.content_tree_widget.setColumnCount(2) 42 | self.content_tree_widget.setHeaderLabels(["Content", "Quantity"]) 43 | self.patients_list_scrollarea_layout.insertWidget(self.patients_list_scrollarea_layout.count(), 44 | self.content_tree_widget) 45 | 46 | def __set_interface_listing_header(self): 47 | self.header_layout = QHBoxLayout() 48 | self.header_label = QLabel("Content summary") 49 | self.header_label.setAlignment(Qt.AlignCenter) 50 | self.header_layout.addWidget(self.header_label) 51 | # self.patients_list_scrollarea_layout.insertLayout(self.patients_list_scrollarea_layout.count() - 1, 52 | # self.header_layout) 53 | 54 | def __set_layout_dimensions(self): 55 | self.patients_list_scrollarea.setBaseSize(QSize(self.width(), 300)) 56 | self.header_label.setFixedHeight(30) 57 | 58 | def __set_connections(self): 59 | pass 60 | 61 | def __set_stylesheets(self): 62 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 63 | font_color = software_ss["Color7"] 64 | font_style = 'normal' 65 | background_color = software_ss["Color2"] 66 | pressed_background_color = software_ss["Color6"] 67 | 68 | self.setStyleSheet(""" 69 | StudyPatientListingWidget{ 70 | background-color: """ + background_color + """; 71 | }""") 72 | 73 | self.header_label.setStyleSheet(""" 74 | QLabel{ 75 | font-size: 16px; 76 | font-style: bold; 77 | border: 2px; 78 | border-style: solid; 79 | border-color: """ + background_color + """ """ + background_color + """ black """ + background_color + """; 80 | border-radius: 2px; 81 | }""") 82 | 83 | self.content_tree_widget.setStyleSheet(""" 84 | QTreeWidget{ 85 | color: """ + font_color + """; 86 | font-size: 14px; 87 | text-align: left; 88 | }""") 89 | 90 | self.content_tree_widget.header().setStyleSheet(""" 91 | QHeaderView{ 92 | color: """ + font_color + """; 93 | background-color: """ + background_color + """; 94 | font-size: 15px 95 | }""") 96 | 97 | def adjustSize(self) -> None: 98 | pass 99 | 100 | def on_reset_interface(self) -> None: 101 | self.content_tree_widget.clear() 102 | 103 | def on_patients_import(self) -> None: 104 | self.content_tree_widget.clear() 105 | study_patients_uid = SoftwareConfigResources.getInstance().get_active_study().included_patients_uids 106 | 107 | patient_items = [] 108 | for uid in study_patients_uid: 109 | patient = SoftwareConfigResources.getInstance().get_patient(uid) 110 | ts_uids = patient.get_all_timestamps_uids() 111 | patient_item = QTreeWidgetItem([patient.display_name, str(len(ts_uids))]) 112 | for ts in ts_uids: 113 | volumes_uids = patient.get_all_mri_volumes_for_timestamp(ts) 114 | ts_item = QTreeWidgetItem([ts]) 115 | volumes_item = QTreeWidgetItem(["Volumes", str(len(volumes_uids))]) 116 | for vuid in volumes_uids: 117 | img_item = QTreeWidgetItem([os.path.basename(patient.get_mri_by_uid(vuid).raw_input_filepath)]) 118 | annotations_uids = patient.get_all_annotations_for_mri(vuid) 119 | annotations_item = QTreeWidgetItem(["Annotations", str(len(annotations_uids))]) 120 | for auid in annotations_uids: 121 | anno_item = QTreeWidgetItem([os.path.basename(patient.get_annotation_by_uid(auid).raw_input_filepath), 1]) 122 | annotations_item.addChild(anno_item) 123 | if len(annotations_uids) != 0: 124 | img_item.addChild(annotations_item) 125 | volumes_item.addChild(img_item) 126 | ts_item.addChild(volumes_item) 127 | patient_item.addChild(ts_item) 128 | patient_items.append(patient_item) 129 | 130 | self.content_tree_widget.insertTopLevelItems(0, patient_items) 131 | 132 | def postprocessing_update(self) -> None: 133 | """ 134 | After running a pipeline, the content of each patient might have changed, e.g., with new annotations and the 135 | tree view must be updated. 136 | """ 137 | #@TODO. Lazy approach to redraw from scratch, must be properly done. 138 | self.on_patients_import() 139 | 140 | # Better approach by iterating over the tree widget and populating on-the-fly with the missing elements. 141 | # root = self.content_tree_widget.invisibleRootItem() 142 | # patient_count = root.childCount() 143 | # for p in range(patient_count): 144 | # patient_item = root.child(p) 145 | # # The patients are listed by display name. 146 | # patient = SoftwareConfigResources.getInstance().get_patient_by_display_name(patient_item.text(0)) 147 | # if patient: 148 | # pass 149 | -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsReportingSummaryWidget.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QScrollArea, QLabel, QSpacerItem,\ 4 | QGridLayout, QTableWidget, QTableWidgetItem, QMenu 5 | from PySide6.QtCore import QSize, Qt, Signal 6 | from PySide6.QtGui import QAction 7 | from utils.software_config import SoftwareConfigResources 8 | 9 | 10 | class StudyPatientsReportingSummaryWidget(QWidget): 11 | """ 12 | 13 | """ 14 | patient_selected = Signal(str) 15 | 16 | def __init__(self, parent=None): 17 | super(StudyPatientsReportingSummaryWidget, self).__init__() 18 | self.parent = parent 19 | self.__set_interface() 20 | self.__set_layout_dimensions() 21 | self.__set_connections() 22 | self.__set_stylesheets() 23 | 24 | def __set_interface(self): 25 | self.layout = QVBoxLayout(self) 26 | self.layout.setSpacing(0) 27 | self.layout.setContentsMargins(0, 0, 0, 0) 28 | self.patients_list_scrollarea = QScrollArea() 29 | self.patients_list_scrollarea.show() 30 | self.patients_list_scrollarea_layout = QVBoxLayout() 31 | self.patients_list_scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 32 | self.patients_list_scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 33 | self.patients_list_scrollarea.setWidgetResizable(True) 34 | self.patients_list_scrollarea_dummy_widget = QLabel() 35 | self.patients_list_scrollarea_layout.setSpacing(0) 36 | self.patients_list_scrollarea_layout.setContentsMargins(0, 0, 0, 0) 37 | self.patients_list_scrollarea_dummy_widget.setLayout(self.patients_list_scrollarea_layout) 38 | self.patients_list_scrollarea.setWidget(self.patients_list_scrollarea_dummy_widget) 39 | self.layout.addWidget(self.patients_list_scrollarea) 40 | self.content_table_widget = QTableWidget() 41 | self.content_table_widget.verticalHeader().setVisible(False) 42 | self.content_table_widget.setEditTriggers(QTableWidget.NoEditTriggers) 43 | self.patients_list_scrollarea_layout.insertWidget(self.patients_list_scrollarea_layout.count(), 44 | self.content_table_widget) 45 | 46 | def __set_layout_dimensions(self): 47 | self.patients_list_scrollarea.setBaseSize(QSize(self.width(), 300)) 48 | 49 | def __set_connections(self): 50 | pass 51 | 52 | def __set_stylesheets(self): 53 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 54 | font_color = software_ss["Color7"] 55 | font_style = 'normal' 56 | background_color = software_ss["Color2"] 57 | pressed_background_color = software_ss["Color6"] 58 | 59 | self.content_table_widget.setStyleSheet(""" 60 | QTableWidget{ 61 | color: """ + font_color + """; 62 | font-size: 14px; 63 | text-align: left; 64 | }""") 65 | 66 | self.content_table_widget.horizontalHeader().setStyleSheet(""" 67 | QHeaderView{ 68 | color: """ + font_color + """; 69 | background-color: """ + background_color + """; 70 | font-size: 15px 71 | }""") 72 | 73 | def adjustSize(self) -> None: 74 | pass 75 | 76 | def on_reset_interface(self) -> None: 77 | self.content_table_widget.setRowCount(0) 78 | 79 | def on_patients_import(self) -> None: 80 | """ 81 | @TODO. Should get the list of imported patients to only update those, rather than redo everything 82 | """ 83 | self.content_table_widget.setRowCount(0) 84 | reporting_statistics_table = SoftwareConfigResources.getInstance().get_active_study().reporting_statistics_df 85 | if reporting_statistics_table is None: 86 | return 87 | 88 | self.content_table_widget.setColumnCount(reporting_statistics_table.shape[1]) 89 | self.content_table_widget.setHorizontalHeaderLabels(list(reporting_statistics_table.columns.values)) 90 | 91 | for i in range(reporting_statistics_table.shape[0]): 92 | self.content_table_widget.insertRow(self.content_table_widget.rowCount()) 93 | for j in range(reporting_statistics_table.shape[1]): 94 | self.content_table_widget.setItem(self.content_table_widget.rowCount() - 1, j, 95 | QTableWidgetItem(str(reporting_statistics_table.iloc[i][j]))) 96 | 97 | def postprocessing_update(self) -> None: 98 | #@TODO. Lazy approach to redraw from scratch, must be properly done. 99 | self.on_patients_import() 100 | -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsSummaryPanel/StudyPatientsSummaryPanelWidget.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QScrollArea, QLabel, QSpacerItem,\ 3 | QGridLayout, QStackedWidget, QComboBox 4 | from PySide6.QtCore import QSize, Qt, Signal 5 | from utils.software_config import SoftwareConfigResources 6 | from gui.StudyBatchComponent.PatientsSummaryPanel.StudyPatientsContentSummaryPanelWidget import StudyPatientsContentSummaryPanelWidget 7 | from gui.StudyBatchComponent.PatientsSummaryPanel.StudyPatientsSegmentationSummaryWidget import StudyPatientsSegmentationSummaryWidget 8 | from gui.StudyBatchComponent.PatientsSummaryPanel.StudyPatientsReportingSummaryWidget import StudyPatientsReportingSummaryWidget 9 | 10 | 11 | class StudyPatientsSummaryPanelWidget(QWidget): 12 | """ 13 | 14 | """ 15 | patient_selected = Signal(str) 16 | patients_imported = Signal() 17 | 18 | def __init__(self, parent=None): 19 | super(StudyPatientsSummaryPanelWidget, self).__init__() 20 | self.parent = parent 21 | self.__set_interface() 22 | self.__set_layout_dimensions() 23 | self.__set_connections() 24 | self.__set_stylesheets() 25 | 26 | def __set_interface(self): 27 | self.setAttribute(Qt.WA_StyledBackground, True) # Enables to set e.g. background-color for the QWidget 28 | self.layout = QVBoxLayout(self) 29 | self.layout.setSpacing(0) 30 | self.layout.setContentsMargins(0, 0, 0, 0) 31 | self.patients_list_scrollarea = QScrollArea() 32 | self.patients_list_scrollarea.show() 33 | self.patients_list_scrollarea_layout = QVBoxLayout() 34 | self.patients_list_scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 35 | self.patients_list_scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 36 | self.patients_list_scrollarea.setWidgetResizable(True) 37 | self.patients_list_scrollarea_dummy_widget = QLabel() 38 | self.patients_list_scrollarea_layout.setSpacing(0) 39 | self.patients_list_scrollarea_layout.setContentsMargins(0, 0, 0, 0) 40 | self.patients_list_scrollarea_dummy_widget.setLayout(self.patients_list_scrollarea_layout) 41 | self.patients_list_scrollarea.setWidget(self.patients_list_scrollarea_dummy_widget) 42 | self.layout.addWidget(self.patients_list_scrollarea) 43 | self.main_stackedwidget = QStackedWidget() 44 | self.main_selector_combobox = QComboBox() 45 | self.main_selector_combobox.addItems(["Content summary", "Annotation statistics", "Reporting statistics"]) 46 | self.patients_content_summary_panel = StudyPatientsContentSummaryPanelWidget(self) 47 | self.patients_segmentation_summary_panel = StudyPatientsSegmentationSummaryWidget(self) 48 | self.patients_reporting_summary_panel = StudyPatientsReportingSummaryWidget(self) 49 | self.main_stackedwidget.addWidget(self.patients_content_summary_panel) 50 | self.main_stackedwidget.addWidget(self.patients_segmentation_summary_panel) 51 | self.main_stackedwidget.addWidget(self.patients_reporting_summary_panel) 52 | self.patients_list_scrollarea_layout.addWidget(self.main_selector_combobox) 53 | self.patients_list_scrollarea_layout.addWidget(self.main_stackedwidget) 54 | 55 | def __set_layout_dimensions(self): 56 | self.main_selector_combobox.setFixedHeight(30) 57 | 58 | def __set_connections(self): 59 | self.patients_imported.connect(self.patients_content_summary_panel.on_patients_import) 60 | self.patients_imported.connect(self.patients_segmentation_summary_panel.on_patients_import) 61 | self.patients_imported.connect(self.patients_reporting_summary_panel.on_patients_import) 62 | self.main_selector_combobox.currentIndexChanged.connect(self.__on_selector_index_changed) 63 | 64 | def __set_stylesheets(self): 65 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 66 | font_color = software_ss["Color7"] 67 | background_color = software_ss["Color2"] 68 | pressed_background_color = software_ss["Color6"] 69 | 70 | self.setStyleSheet(""" 71 | QWidget{ 72 | background-color: """ + background_color + """; 73 | }""") 74 | 75 | if os.name == 'nt': 76 | self.main_selector_combobox.setStyleSheet(""" 77 | QComboBox{ 78 | color: """ + font_color + """; 79 | background-color: """ + background_color + """; 80 | font: bold; 81 | font-size: 12px; 82 | text-align: center; 83 | border-style:none; 84 | } 85 | QComboBox::hover{ 86 | border-style: solid; 87 | border-width: 1px; 88 | border-color: rgba(196, 196, 196, 1); 89 | } 90 | QComboBox::drop-down { 91 | subcontrol-origin: padding; 92 | subcontrol-position: top right; 93 | width: 15px; 94 | } 95 | """) 96 | else: 97 | self.main_selector_combobox.setStyleSheet(""" 98 | QComboBox{ 99 | color: """ + font_color + """; 100 | background-color: """ + background_color + """; 101 | font: bold; 102 | font-size: 14px; 103 | text-align: center; 104 | border: 1px solid; 105 | border-color: rgba(196, 196, 196, 1); 106 | } 107 | QComboBox::hover{ 108 | border-style: solid; 109 | border-width: 1px; 110 | border-color: rgba(196, 196, 196, 1); 111 | } 112 | QComboBox::drop-down { 113 | subcontrol-origin: padding; 114 | subcontrol-position: top right; 115 | width: 15px; 116 | border-left-width: 1px; 117 | border-left-color: darkgray; 118 | border-left-style: none; 119 | border-top-right-radius: 3px; /* same radius as the QComboBox */ 120 | border-bottom-right-radius: 3px; 121 | } 122 | QComboBox::down-arrow{ 123 | image: url(""" + os.path.join(os.path.dirname(os.path.realpath(__file__)), 124 | '../../Images/combobox-arrow-icon-10x7.png') + """) 125 | } 126 | """) 127 | 128 | def adjustSize(self) -> None: 129 | pass 130 | 131 | def __on_selector_index_changed(self, index: int) -> None: 132 | self.main_stackedwidget.setCurrentIndex(index) 133 | 134 | def on_study_selected(self, uid: str) -> None: 135 | """ 136 | The selected study is∕should be the active study, so no need to use the uid here? 137 | Otherwise another method should be designed. 138 | """ 139 | self.on_processing_finished() 140 | 141 | def on_processing_finished(self): 142 | self.patients_content_summary_panel.postprocessing_update() 143 | self.patients_segmentation_summary_panel.postprocessing_update() 144 | self.patients_reporting_summary_panel.postprocessing_update() 145 | 146 | def on_patient_refreshed(self, patient_uid: str) -> None: 147 | """ 148 | @TODO. Brute-force approach for now, has to be improved. 149 | """ 150 | self.patients_content_summary_panel.postprocessing_update() 151 | self.patients_segmentation_summary_panel.postprocessing_update() 152 | self.patients_reporting_summary_panel.postprocessing_update() 153 | 154 | def on_patient_removed(self, patient_uid: str) -> None: 155 | self.patients_content_summary_panel.postprocessing_update() 156 | self.patients_segmentation_summary_panel.postprocessing_update() 157 | self.patients_reporting_summary_panel.postprocessing_update() 158 | 159 | def on_reset_interface(self) -> None: 160 | self.patients_content_summary_panel.on_reset_interface() 161 | self.patients_segmentation_summary_panel.on_reset_interface() 162 | self.patients_reporting_summary_panel.on_reset_interface() 163 | self.adjustSize() 164 | self.repaint() -------------------------------------------------------------------------------- /gui/StudyBatchComponent/PatientsSummaryPanel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/StudyBatchComponent/PatientsSummaryPanel/__init__.py -------------------------------------------------------------------------------- /gui/StudyBatchComponent/StudiesSidePanel/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/StudyBatchComponent/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/AboutDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QGridLayout, QDialog, QDialogButtonBox,\ 2 | QComboBox, QPushButton, QScrollArea, QLineEdit, QFileDialog, QMessageBox, QSpinBox, QCheckBox, QStackedWidget 3 | from PySide6.QtCore import Qt, QSize, Signal 4 | from PySide6.QtGui import QIcon, QPixmap 5 | import os 6 | 7 | from utils.software_config import SoftwareConfigResources 8 | 9 | 10 | class AboutDialog(QDialog): 11 | 12 | def __init__(self, parent=None): 13 | super().__init__(parent) 14 | self.setWindowTitle("About Raidionics") 15 | self.__set_interface() 16 | self.__set_layout_dimensions() 17 | self.__set_connections() 18 | self.__set_stylesheets() 19 | self.__textfill() 20 | 21 | def exec(self) -> int: 22 | return super().exec() 23 | 24 | def __set_interface(self): 25 | self.layout = QVBoxLayout(self) 26 | self.layout.setSpacing(5) 27 | self.layout.setContentsMargins(0, 0, 0, 5) 28 | 29 | self.upper_layout = QHBoxLayout() 30 | self.upper_layout.setSpacing(5) 31 | self.upper_layout.setContentsMargins(5, 5, 5, 5) 32 | self.raidionics_logo_label = QLabel() 33 | self.raidionics_logo_label.setPixmap(QPixmap(os.path.join(os.path.dirname(os.path.realpath(__file__)), 34 | '../../Images/raidionics-logo-square.png'))) 35 | self.raidionics_logo_label.setScaledContents(True) 36 | self.about_label = QLabel() 37 | self.about_label.setOpenExternalLinks(True) 38 | # The following counters the possibility to open external links... 39 | # self.about_label.setTextInteractionFlags(Qt.TextSelectableByMouse) 40 | self.upper_layout.addWidget(self.raidionics_logo_label) 41 | self.upper_layout.addWidget(self.about_label) 42 | self.layout.addLayout(self.upper_layout) 43 | 44 | # self.middle_layout = QHBoxLayout() 45 | # self.middle_label = QLabel() 46 | 47 | # Native exit buttons 48 | self.bottom_exit_layout = QHBoxLayout() 49 | self.exit_close_pushbutton = QDialogButtonBox(QDialogButtonBox.Close) 50 | self.bottom_exit_layout.addStretch(1) 51 | self.bottom_exit_layout.addWidget(self.exit_close_pushbutton) 52 | self.layout.addLayout(self.bottom_exit_layout) 53 | 54 | def __set_layout_dimensions(self): 55 | self.setMinimumSize(600, 400) 56 | 57 | def __set_connections(self): 58 | self.exit_close_pushbutton.clicked.connect(self.accept) 59 | 60 | def __set_stylesheets(self): 61 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 62 | font_color = software_ss["Color7"] 63 | background_color = software_ss["Color2"] 64 | 65 | self.setStyleSheet(""" 66 | QDialog{ 67 | background-color: """ + background_color + """; 68 | } 69 | """) 70 | 71 | self.raidionics_logo_label.setStyleSheet(""" 72 | QLabel{ 73 | background-color: """ + background_color + """; 74 | }""") 75 | 76 | self.about_label.setStyleSheet(""" 77 | QLabel{ 78 | color: """ + font_color + """; 79 | background-color: """ + background_color + """; 80 | text-align: top; 81 | }""") 82 | 83 | self.exit_close_pushbutton.setStyleSheet(""" 84 | """) 85 | 86 | def __textfill(self): 87 | text = "

Raidionics

" 88 | text = text + """

Initially developed by the Medical Image Analysis group, Health Research Department, 89 | SINTEF Digital, Trondheim, Norway:\n 90 | * David Bouget (lead developer - maintainer), contact: david.bouget@sintef.no 91 | * André Pedersen (deployment and multi-platform support) 92 | * Demah Alsinan (design) 93 | * Valeria Gaitan (design) 94 | * Javier Pérez de Frutos (logo design) 95 | * Ingerid Reinertsen (project leader) \n\n 96 | For questions about the methodological aspect, please refer to the following published articles: 97 | * Raidionics: an open software for pre-and postoperative central nervous system tumor 98 | segmentation and standardized reporting (article) 99 | * Preoperative brain tumor imaging: models and software for segmentation and standardized 100 | reporting (article) 101 | * Glioblastoma Surgery Imaging-Reporting and Data System: Validation and Performance of 102 | the Automated Segmentation Task (article) 103 | * Glioblastoma Surgery Imaging-Reporting and Data System: Standardized Reporting of 104 | Tumor Volume, Location, and Resectability Based on Automated Segmentations (article) 105 | * Meningioma Segmentation in T1-Weighted MRI Leveraging Global Context and 106 | Attention Mechanisms (article) \n 107 | Current software version: """ + SoftwareConfigResources.getInstance().software_version + """ \n 108 | Website 109 | Github 110 | Feel free to contact us with any feedback or suggestion for improvement. 111 |

112 | """ 113 | self.about_label.setText(text) 114 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/DisplayDICOMMetadataDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem 2 | from PySide6.QtCore import Qt, Signal, QSize 3 | from PySide6.QtGui import QIcon 4 | import os 5 | import datetime 6 | 7 | from utils.software_config import SoftwareConfigResources 8 | from utils.patient_dicom import PatientDICOM, get_tag_readable_name 9 | 10 | 11 | class DisplayMetadataDICOMDialog(QDialog): 12 | def __init__(self, dicom_tags, parent=None): 13 | super().__init__(parent) 14 | self.dicom_tags = dicom_tags 15 | self.setWindowTitle("DICOM Metadata") 16 | self.__set_interface() 17 | self.__set_layout_dimensions() 18 | self.__set_connections() 19 | self.__set_stylesheets() 20 | 21 | def __set_interface(self): 22 | self.layout = QVBoxLayout(self) 23 | self.content_tablewidget = QTableWidget() 24 | self.content_tablewidget.setEditTriggers(QTableWidget.NoEditTriggers) 25 | self.content_tablewidget.setColumnCount(3) 26 | self.content_tablewidget.setHorizontalHeaderLabels(["Tag", "Description", "Value"]) 27 | self.content_tablewidget.verticalHeader().setVisible(False) 28 | self.layout.addWidget(self.content_tablewidget) 29 | 30 | for k in list(self.dicom_tags.keys()): 31 | if self.dicom_tags[k] is not None and self.dicom_tags[k].strip() != "": 32 | self.content_tablewidget.insertRow(self.content_tablewidget.rowCount()) 33 | self.content_tablewidget.setItem(self.content_tablewidget.rowCount() - 1, 0, QTableWidgetItem(k)) 34 | self.content_tablewidget.setItem(self.content_tablewidget.rowCount() - 1, 1, QTableWidgetItem(get_tag_readable_name(k))) 35 | self.content_tablewidget.setItem(self.content_tablewidget.rowCount() - 1, 2, QTableWidgetItem(self.dicom_tags[k])) 36 | for c in range(self.content_tablewidget.columnCount()): 37 | self.content_tablewidget.resizeColumnToContents(c) 38 | 39 | def __set_layout_dimensions(self): 40 | self.setMinimumSize(QSize(800, 600)) 41 | 42 | def __set_connections(self): 43 | pass 44 | 45 | def __set_stylesheets(self): 46 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 47 | font_color = software_ss["Color7"] 48 | background_color = software_ss["Color2"] 49 | pressed_background_color = software_ss["Background_pressed"] 50 | 51 | self.setStyleSheet(""" 52 | QDialog{ 53 | background-color: """ + background_color + """; 54 | color: """ + font_color + """; 55 | }""") 56 | 57 | self.content_tablewidget.setStyleSheet(""" 58 | QTableWidget{ 59 | color: """ + font_color + """; 60 | font-size: 12px; 61 | }""") 62 | 63 | self.content_tablewidget.horizontalHeader().setStyleSheet(""" 64 | QHeaderView{ 65 | color: """ + font_color + """; 66 | font-style: bold; 67 | font-size: 14px; 68 | }""") 69 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/KeyboardShortcutsDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QDialog, QDialogButtonBox, QTreeWidget, QTreeWidgetItem 2 | from PySide6.QtCore import Qt, QSize, Signal 3 | from PySide6.QtGui import QIcon, QPixmap 4 | import os 5 | 6 | from utils.software_config import SoftwareConfigResources 7 | 8 | 9 | class KeyboardShortcutsDialog(QDialog): 10 | 11 | def __init__(self, parent=None): 12 | super().__init__(parent) 13 | self.setWindowTitle("Keyboard shortcuts") 14 | self.__set_interface() 15 | self.__set_layout_dimensions() 16 | self.__set_connections() 17 | self.__set_stylesheets() 18 | self.__fill_table() 19 | 20 | def exec(self) -> int: 21 | return super().exec() 22 | 23 | def __set_interface(self): 24 | self.layout = QVBoxLayout(self) 25 | self.layout.setSpacing(5) 26 | self.layout.setContentsMargins(0, 0, 0, 5) 27 | 28 | self.shortcuts_treewidget = QTreeWidget() 29 | self.layout.addWidget(self.shortcuts_treewidget) 30 | 31 | # Native exit buttons 32 | self.bottom_exit_layout = QHBoxLayout() 33 | self.exit_close_pushbutton = QDialogButtonBox(QDialogButtonBox.Close) 34 | self.bottom_exit_layout.addStretch(1) 35 | self.bottom_exit_layout.addWidget(self.exit_close_pushbutton) 36 | self.layout.addLayout(self.bottom_exit_layout) 37 | 38 | def __set_layout_dimensions(self): 39 | self.setMinimumSize(600, 400) 40 | 41 | def __set_connections(self): 42 | self.exit_close_pushbutton.clicked.connect(self.accept) 43 | 44 | def __set_stylesheets(self): 45 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 46 | font_color = software_ss["Color7"] 47 | background_color = software_ss["Color2"] 48 | pressed_background_color = software_ss["Color6"] 49 | 50 | self.setStyleSheet(""" 51 | QDialog{ 52 | background-color: """ + background_color + """; 53 | color: black; 54 | }""") 55 | 56 | self.shortcuts_treewidget.setStyleSheet(""" 57 | QTreeWidget{ 58 | color: """ + font_color + """; 59 | background-color: """ + background_color + """; 60 | }""") 61 | self.shortcuts_treewidget.header().setStyleSheet(""" 62 | QHeaderView{ 63 | color: """ + font_color + """; 64 | background-color: """ + background_color + """; 65 | font-size: 16px; 66 | }""") 67 | 68 | def __fill_table(self): 69 | self.shortcuts_treewidget.setColumnCount(3) 70 | self.shortcuts_treewidget.setHeaderLabels(["Action", "Shortcut", "Description"]) 71 | 72 | shortcut_item = QTreeWidgetItem(["Exit Raidionics", "Ctrl + Q", "Close the software"]) 73 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 74 | shortcut_item = QTreeWidgetItem(["Open Preferences", "Ctrl + P", "Open the Preferences/Settings panel"]) 75 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 76 | shortcut_item = QTreeWidgetItem(["Open Keyboard shortcuts", "Ctrl + K", "Open the Keyboard shortcuts panel"]) 77 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 78 | shortcut_item = QTreeWidgetItem(["Open Logging", "Ctrl + L", "Open the dashboard with the software execution logs"]) 79 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 80 | shortcut_item = QTreeWidgetItem(["Save current patient/study", "Ctrl + S", "Save on disk the current active patient and active study"]) 81 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 82 | shortcut_item = QTreeWidgetItem(["Toggle tumor display", "S", "Toggle on/off the display of all tumor annotations (must click somewhere on the central display area beforehand)."]) 83 | self.shortcuts_treewidget.insertTopLevelItem(self.shortcuts_treewidget.topLevelItemCount(), shortcut_item) 84 | 85 | for c in range(self.shortcuts_treewidget.columnCount()): 86 | self.shortcuts_treewidget.resizeColumnToContents(c) 87 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/LogsViewerDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QGridLayout, QDialog, QDialogButtonBox,\ 2 | QPushButton, QLineEdit, QFileDialog, QCheckBox, QTextEdit 3 | from PySide6.QtCore import Qt, QSize, Signal, QFile, QUrl 4 | from PySide6.QtGui import QIcon, QMouseEvent, QDesktopServices 5 | import os 6 | 7 | from utils.software_config import SoftwareConfigResources 8 | 9 | 10 | class LogsViewerDialog(QDialog): 11 | 12 | error_detected = Signal(str) 13 | def __init__(self, parent=None): 14 | super().__init__(parent) 15 | self.setWindowTitle("Runtime logs") 16 | self.__set_interface() 17 | self.__set_layout_dimensions() 18 | self.__set_connections() 19 | self.__set_stylesheets() 20 | self.__refresh_logs() 21 | 22 | def exec(self) -> int: 23 | return super().exec() 24 | 25 | def __set_interface(self): 26 | self.layout = QVBoxLayout(self) 27 | self.layout.setSpacing(5) 28 | self.layout.setContentsMargins(0, 0, 0, 5) 29 | 30 | self.__set_upper_interface() 31 | self.central_layout = QHBoxLayout() 32 | self.logs_textedit = QTextEdit() 33 | self.logs_textedit.setReadOnly(True) 34 | self.central_layout.addWidget(self.logs_textedit) 35 | self.layout.addLayout(self.central_layout) 36 | 37 | # Native exit buttons 38 | self.bottom_exit_layout = QHBoxLayout() 39 | self.exit_accept_pushbutton = QDialogButtonBox(QDialogButtonBox.Ok) 40 | self.bottom_exit_layout.addStretch(1) 41 | self.bottom_exit_layout.addWidget(self.exit_accept_pushbutton) 42 | self.layout.addLayout(self.bottom_exit_layout) 43 | 44 | def __set_upper_interface(self): 45 | self.upper_layout = QHBoxLayout() 46 | self.upper_layout.setSpacing(5) 47 | self.upper_layout.setContentsMargins(5, 5, 5, 0) 48 | self.log_filename_label = QLabel("Location") 49 | self.log_filename_lineedit = QLineEdit() 50 | self.log_filename_lineedit.setReadOnly(True) 51 | self.report_error_pushbutton = QPushButton("Report issue") 52 | self.report_error_pushbutton.setIcon(QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 53 | '../../Images/github-icon.png'))) 54 | self.upper_layout.addWidget(self.log_filename_label) 55 | self.upper_layout.addWidget(self.log_filename_lineedit) 56 | self.upper_layout.addStretch(1) 57 | self.upper_layout.addWidget(self.report_error_pushbutton) 58 | self.layout.addLayout(self.upper_layout) 59 | 60 | def __set_layout_dimensions(self): 61 | self.setMinimumSize(800, 600) 62 | self.log_filename_label.setFixedHeight(20) 63 | self.log_filename_lineedit.setFixedHeight(20) 64 | self.log_filename_lineedit.setMinimumWidth(320) 65 | self.report_error_pushbutton.setFixedHeight(20) 66 | 67 | def __set_connections(self): 68 | self.exit_accept_pushbutton.clicked.connect(self.__on_exit_accept_clicked) 69 | self.report_error_pushbutton.clicked.connect(self.__on_report_error_clicked) 70 | 71 | def __set_stylesheets(self): 72 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 73 | font_color = software_ss["Color7"] 74 | background_color = software_ss["Color2"] 75 | pressed_background_color = software_ss["Color6"] 76 | 77 | self.setStyleSheet(""" 78 | QDialog{ 79 | background-color: """ + background_color + """; 80 | } 81 | """) 82 | 83 | self.log_filename_label.setStyleSheet(""" 84 | QLabel{ 85 | color: """ + font_color + """; 86 | background-color: """ + background_color + """; 87 | font: 14px; 88 | }""") 89 | 90 | self.log_filename_lineedit.setStyleSheet(""" 91 | QLineEdit{ 92 | color: """ + font_color + """; 93 | font: 14px; 94 | background-color: """ + background_color + """; 95 | border-style: none; 96 | } 97 | QLineEdit::hover{ 98 | border-style: solid; 99 | border-width: 1px; 100 | border-color: rgba(196, 196, 196, 1); 101 | }""") 102 | 103 | self.report_error_pushbutton.setStyleSheet(""" 104 | QPushButton{ 105 | color: """ + font_color + """; 106 | background-color: """ + background_color + """; 107 | border-style: none; 108 | } 109 | QPushButton::hover{ 110 | border-style: solid; 111 | border-width: 1px; 112 | border-color: rgba(196, 196, 196, 1); 113 | } 114 | QPushButton:pressed{ 115 | border-style:inset; 116 | background-color: """ + pressed_background_color + """; 117 | } 118 | """) 119 | 120 | # self.logs_textedit.setStyleSheet(""" 121 | # QTextEdit{ 122 | # background-color: black; 123 | # }""") 124 | 125 | def __on_exit_accept_clicked(self): 126 | """ 127 | """ 128 | self.accept() 129 | 130 | def __on_report_error_clicked(self): 131 | # opens browser with specified url, directs user to Issues section of GitHub repo 132 | QDesktopServices.openUrl(QUrl("https://github.com/dbouget/Raidionics/issues")) 133 | 134 | def __refresh_logs(self): 135 | self.logs_textedit.clear() 136 | self.log_filename_lineedit.setText(SoftwareConfigResources.getInstance().get_session_log_filename()) 137 | logfile = open(SoftwareConfigResources.getInstance().get_session_log_filename(), 'r') 138 | lines = logfile.readlines() 139 | 140 | for line in lines: 141 | text = "

" + line + "\n

" 142 | if "error" in line.strip().lower(): 143 | text = "

" + line + "\n

" 144 | elif "warning" in line.strip().lower(): 145 | text = "

" + line + "\n

" 146 | elif "runtime" in line.strip().lower(): 147 | text = "

" + line + "\n

" 148 | self.logs_textedit.insertHtml(text) 149 | logfile.close() 150 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/SavePatientChangesDialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDialogButtonBox, QLineEdit 2 | from utils.software_config import SoftwareConfigResources 3 | 4 | 5 | class SavePatientChangesDialog(QDialog): 6 | 7 | def __init__(self, parent=None): 8 | super(SavePatientChangesDialog, self).__init__(parent) 9 | self.setWindowTitle("Save patient changes?") 10 | self.base_layout = QVBoxLayout() 11 | self.warning_label = QLabel('Your changes for the current patient will be lost if you don\'t save them') 12 | self.base_layout.addWidget(self.warning_label) 13 | 14 | self.destination_folder_layout = QHBoxLayout() 15 | self.destination_folder_label = QLabel("Destination: ") 16 | self.destination_folder_lineedit = QLineEdit() 17 | self.destination_folder_layout.addWidget(self.destination_folder_label) 18 | self.destination_folder_layout.addWidget(self.destination_folder_lineedit) 19 | self.base_layout.addLayout(self.destination_folder_layout) 20 | 21 | self.bottom_actions_layout = QHBoxLayout() 22 | self.bottom_actions_layout.addStretch(1) 23 | self.exit_save_pushbutton = QDialogButtonBox() 24 | self.exit_save_pushbutton.addButton("Save", QDialogButtonBox.AcceptRole) 25 | self.bottom_actions_layout.addWidget(self.exit_save_pushbutton) 26 | self.exit_dontsave_pushbutton = QDialogButtonBox() 27 | self.exit_dontsave_pushbutton.addButton("Don\'t save", QDialogButtonBox.AcceptRole) 28 | self.bottom_actions_layout.addWidget(self.exit_dontsave_pushbutton) 29 | self.exit_cancel_pushbutton = QDialogButtonBox() 30 | self.exit_cancel_pushbutton.addButton("Cancel", QDialogButtonBox.RejectRole) 31 | self.bottom_actions_layout.addWidget(self.exit_cancel_pushbutton) 32 | self.base_layout.addLayout(self.bottom_actions_layout) 33 | self.setLayout(self.base_layout) 34 | 35 | self.exit_save_pushbutton.accepted.connect(self.save_changes) 36 | self.exit_dontsave_pushbutton.accepted.connect(self.discard_changes) 37 | self.exit_cancel_pushbutton.rejected.connect(self.reject) 38 | 39 | self.__set_stylesheets() 40 | 41 | def __set_stylesheets(self): 42 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 43 | font_color = software_ss["Color7"] 44 | background_color = software_ss["Color2"] 45 | 46 | self.setStyleSheet(""" 47 | QDialog{ 48 | color: """ + font_color + """; 49 | background-color: """ + background_color + """; 50 | font-size: 12px; 51 | }""") 52 | 53 | def exec(self) -> int: 54 | curr_patient = SoftwareConfigResources.getInstance().get_active_patient() 55 | self.destination_folder_lineedit.blockSignals(True) 56 | self.destination_folder_lineedit.setText(curr_patient.output_folder) 57 | self.destination_folder_lineedit.blockSignals(False) 58 | return super().exec() 59 | 60 | def save_changes(self): 61 | SoftwareConfigResources.getInstance().get_active_patient().save_patient() 62 | self.accept() 63 | 64 | def discard_changes(self): 65 | """ 66 | The changes performed for the patient are not saved on disk, but they are not reverted since there is no 67 | caching for each and every variable within a PatientParameters. 68 | Changes are hence still visible in the software, and are on RAM, until the software is exited. 69 | If the patient is saved manually by the user at a later stage, those discarded changes WILL be stored on disk. 70 | """ 71 | SoftwareConfigResources.getInstance().get_active_patient().set_unsaved_changes_state(False) 72 | self.accept() 73 | 74 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/TumorTypeSelectionQDialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QComboBox, QDialogButtonBox 3 | from PySide6.QtCore import QSize 4 | from utils.software_config import SoftwareConfigResources 5 | 6 | 7 | class TumorTypeSelectionQDialog(QDialog): 8 | 9 | def __init__(self, parent=None): 10 | super(TumorTypeSelectionQDialog, self).__init__(parent) 11 | self.setWindowTitle("Tumor type selection") 12 | self.base_layout = QGridLayout() 13 | self.select_tumor_type_label = QLabel('Tumor type') 14 | self.select_tumor_type_label.setStyleSheet("""QLabel{background-color: rgba(248, 248, 248, 1);}""") 15 | self.base_layout.addWidget(self.select_tumor_type_label, 0, 0) 16 | self.select_tumor_type_combobox = QComboBox() 17 | # self.select_tumor_type_combobox.addItems(["Glioblastoma", "Low-Grade Glioma", "Meningioma", "Metastasis"]) 18 | self.select_tumor_type_combobox.addItems(["Contrast-enhancing", "Non contrast-enhancing"]) 19 | self.tumor_type = "Contrast-enhancing" 20 | 21 | self.base_layout.addWidget(self.select_tumor_type_combobox, 0, 1) 22 | self.exit_accept_pushbutton = QDialogButtonBox(QDialogButtonBox.Ok) 23 | self.base_layout.addWidget(self.exit_accept_pushbutton, 1, 0) 24 | self.exit_cancel_pushbutton = QDialogButtonBox(QDialogButtonBox.Cancel) 25 | self.base_layout.addWidget(self.exit_cancel_pushbutton, 1, 1) 26 | self.setLayout(self.base_layout) 27 | 28 | self.select_tumor_type_combobox.currentTextChanged.connect(self.on_type_selected) 29 | self.exit_accept_pushbutton.accepted.connect(self.accept) 30 | self.exit_cancel_pushbutton.rejected.connect(self.reject) 31 | 32 | self.__set_layout_dimensions() 33 | self.__set_stylesheets() 34 | 35 | def __set_layout_dimensions(self): 36 | self.select_tumor_type_label.setFixedHeight(25) 37 | self.select_tumor_type_combobox.setFixedHeight(25) 38 | self.select_tumor_type_combobox.setFixedWidth(155) 39 | self.setFixedSize(QSize(270, 70)) 40 | 41 | def __set_stylesheets(self): 42 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 43 | font_color = software_ss["Color7"] 44 | background_color = software_ss["Color2"] 45 | 46 | self.setStyleSheet(""" 47 | QDialog{ 48 | background-color: """ + background_color + """; 49 | }""") 50 | 51 | self.select_tumor_type_label.setStyleSheet(""" 52 | QLabel{ 53 | background-color: """ + background_color + """; 54 | color: """ + font_color + """; 55 | font-size: 12px; 56 | }""") 57 | 58 | if os.name == 'nt': 59 | self.select_tumor_type_combobox.setStyleSheet(""" 60 | QComboBox{ 61 | color: """ + font_color + """; 62 | background-color: """ + background_color + """; 63 | font: normal; 64 | font-size: 12px; 65 | border-style:none; 66 | } 67 | QComboBox::hover{ 68 | border-style: solid; 69 | border-width: 1px; 70 | border-color: rgba(196, 196, 196, 1); 71 | } 72 | QComboBox::drop-down { 73 | subcontrol-origin: padding; 74 | subcontrol-position: top right; 75 | width: 15px; 76 | } 77 | """) 78 | else: 79 | self.select_tumor_type_combobox.setStyleSheet(""" 80 | QComboBox{ 81 | color: """ + font_color + """; 82 | background-color: """ + background_color + """; 83 | font: normal; 84 | font-size: 12px; 85 | border: 1px solid; 86 | border-color: rgba(196, 196, 196, 1); 87 | } 88 | QComboBox::hover{ 89 | border-style: solid; 90 | border-width: 1px; 91 | border-color: rgba(196, 196, 196, 1); 92 | } 93 | QComboBox::drop-down { 94 | subcontrol-origin: padding; 95 | subcontrol-position: top right; 96 | width: 15px; 97 | border-left-width: 1px; 98 | border-left-color: darkgray; 99 | border-left-style: none; 100 | border-top-right-radius: 3px; /* same radius as the QComboBox */ 101 | border-bottom-right-radius: 3px; 102 | } 103 | QComboBox::down-arrow{ 104 | image: url(""" + os.path.join(os.path.dirname(os.path.realpath(__file__)), 105 | '../../Images/combobox-arrow-icon-10x7.png') + """) 106 | } 107 | """) 108 | 109 | def on_type_selected(self, text): 110 | self.tumor_type = text 111 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQDialog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/UtilsWidgets/CustomQDialog/__init__.py -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQGroupBox/QCollapsibleGroupBox.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QScrollArea, QPushButton, QSizePolicy, QGridLayout, QSpacerItem 2 | from PySide6.QtGui import QIcon, QPixmap 3 | from PySide6.QtCore import QSize, Signal 4 | 5 | from gui.UtilsWidgets.QCustomIconsPushButton import QCustomIconsPushButton 6 | 7 | 8 | class QCollapsibleGroupBox(QWidget): 9 | """ 10 | @TODO. Poorly designed. Has to be redone to work properly in general. 11 | Might try => https://stackoverflow.com/questions/52615115/how-to-create-collapsible-box-in-pyqt 12 | """ 13 | 14 | clicked_signal = Signal(bool, str) 15 | right_clicked = Signal(str, bool) 16 | 17 | def __init__(self, uid, parent=None, header_style='right', right_header_behaviour='native'): 18 | super(QCollapsibleGroupBox, self).__init__() 19 | self.parent = parent 20 | self.uid = uid # Holding the permanent unique id, while self.title holds the visible name 21 | self.header_style = header_style 22 | self.right_header_behaviour = right_header_behaviour 23 | self.__set_interface() 24 | self.__set_connections() 25 | self.__set_stylesheets() 26 | self.collapsed = False 27 | 28 | def __set_interface(self): 29 | self.layout = QVBoxLayout(self) 30 | # self.layout.setSpacing(0) 31 | self.layout.setContentsMargins(0, 5, 0, 5) 32 | self.header_pushbutton = QCustomIconsPushButton(self.uid, self.parent, icon_style=self.header_style, 33 | right_behaviour=self.right_header_behaviour) 34 | self.header_pushbutton.setCheckable(True) 35 | self.content_label = QLabel() 36 | self.content_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 37 | self.content_label_layout = QVBoxLayout() 38 | self.content_label_layout.setSpacing(0) 39 | self.content_label_layout.setContentsMargins(0, 0, 0, 0) 40 | self.content_label.setLayout(self.content_label_layout) 41 | self.layout.addWidget(self.header_pushbutton) 42 | self.layout.addWidget(self.content_label) 43 | self.content_label.setVisible(False) 44 | self.layout.addStretch(1) 45 | 46 | def __set_connections(self): 47 | self.header_pushbutton.clicked.connect(self.on_header_pushbutton_clicked) 48 | # Propagating signal 49 | if self.right_header_behaviour == 'stand-alone': 50 | self.header_pushbutton.right_clicked.connect(self.right_clicked) 51 | 52 | def __set_stylesheets(self): 53 | self.content_label.setStyleSheet("QLabel{background-color:rgb(128, 255, 128);}") 54 | 55 | # def setStyleSheets(self, header="", content=""): 56 | # self.header_pushbutton.setStyleSheet(header) 57 | # self.content_label.setStyleSheet("QLabel{background-color:rgb(254, 254, 254);}") 58 | 59 | def on_header_pushbutton_clicked(self, state): 60 | self.collapsed = state 61 | self.content_label.setVisible(state) 62 | self.clicked_signal.emit(state, self.uid) 63 | # self.adjustSize() # @TODO. Should not call adjustSize here, but rather when the content_label is filled in 64 | # order to avoid hyper-extension of the layouts. 65 | 66 | def set_header_icons(self, unchecked_icon_path=None, unchecked_icon_size=QSize(), checked_icon_path=None, 67 | checked_icon_size=QSize(), side='right'): 68 | self.header_pushbutton.setIcon(QIcon(QPixmap(unchecked_icon_path)), unchecked_icon_size, side=side, checked=False) 69 | self.header_pushbutton.setIcon(QIcon(QPixmap(checked_icon_path)), checked_icon_size, side=side, checked=True) 70 | 71 | def setFixedSize(self, size): 72 | self.content_label.setFixedSize(size) 73 | 74 | def setBaseSize(self, size): 75 | self.content_label.setBaseSize(size) 76 | 77 | def setMinimumSize(self, size): 78 | self.content_label.setMinimumSize(size) 79 | 80 | def adjustSize(self): 81 | """ 82 | Given that custom content_label can be set whenever the class is used as parent, 83 | the actual content must be parsed to retrieve the optimal height. 84 | Being a collapsible group box, it is assumed that the width will remain constant. 85 | """ 86 | self.content_label.setMinimumSize(self.size()) 87 | items = (self.content_label_layout.itemAt(i) for i in range(self.content_label_layout.count() - 1)) # Last item of sequence being a QSpacerItem 88 | actual_height = 0 89 | for w in items: 90 | if (w.__class__ == QHBoxLayout) or (w.__class__ == QVBoxLayout): 91 | max_height = 0 92 | sub_items = [w.itemAt(i) for i in range(w.count())] 93 | for sw in sub_items: 94 | if sw.__class__ != QSpacerItem: 95 | if sw.wid.sizeHint().height() > max_height: 96 | max_height = sw.wid.sizeHint().height() 97 | actual_height += max_height 98 | elif w.__class__ == QGridLayout: 99 | pass 100 | elif w.__class__ != QSpacerItem: 101 | size = w.wid.sizeHint() 102 | actual_height += size.height() 103 | else: 104 | pass 105 | # N-B: setFixedSize must be used, a simple .resize does not trigger the size update and repainting 106 | self.content_label.setFixedSize(QSize(self.size().width(), actual_height)) 107 | 108 | def clear_content_layout(self): 109 | items = (self.content_label_layout.itemAt(i) for i in reversed(range(self.content_label_layout.count()))) 110 | for i in items: 111 | try: 112 | if i and i.widget(): # Current item is a QWidget that can be directly removed 113 | w = i.widget() 114 | w.setParent(None) 115 | w.deleteLater() 116 | else: # Current item is possibly a layout. @TODO. Should be doing a recursive search in case of inception layouts... 117 | items2 = (i.itemAt(j) for j in reversed(range(i.count()))) 118 | for ii in items2: 119 | if ii and ii.widget(): 120 | w2 = ii.widget() 121 | w2.setParent(None) 122 | w2.deleteLater() 123 | self.content_label_layout.removeItem(i) 124 | except Exception: 125 | pass 126 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQGroupBox/QCollapsibleWidget.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QScrollArea, QPushButton, QSizePolicy,\ 2 | QGridLayout, QSpacerItem, QStackedLayout 3 | from PySide6.QtGui import Qt, QIcon, QPixmap, QFont 4 | from PySide6.QtCore import QSize, Signal 5 | import os 6 | from abc import abstractmethod 7 | 8 | 9 | class Header(QWidget): 10 | """ 11 | Inspired from https://github.com/aronamao/PySide6-Collapsible-Widget 12 | """ 13 | toggled = Signal(bool) # Toggle state in [True, False] 14 | 15 | def __init__(self, name, content_widget, parent=None): 16 | """ 17 | 18 | """ 19 | super(Header, self).__init__() 20 | self.content = content_widget 21 | self.parent = parent 22 | self._title = name 23 | self.expand_pixmap = QPixmap(os.path.join(os.path.dirname(os.path.realpath(__file__)), 24 | '../../Images/expand_arrow.png')) 25 | self.collapse_pixmap = QPixmap(os.path.join(os.path.dirname(os.path.realpath(__file__)), 26 | '../../Images/shrink_arrow.png')) 27 | self.icon_size = QSize(20, 20) 28 | self._layout = QHBoxLayout(self) 29 | self._layout.setSpacing(0) 30 | self._layout.setContentsMargins(0, 0, 0, 0) 31 | self._background_label = QLabel() 32 | 33 | self._background_layout = QHBoxLayout() 34 | 35 | self._icon_label = QLabel() 36 | self._icon_label.setPixmap(self.collapse_pixmap.scaled(self.icon_size, aspectMode=Qt.KeepAspectRatio)) 37 | self._icon_label.setFixedSize(self.icon_size) 38 | self._background_layout.addWidget(self._icon_label) 39 | self._background_layout.setContentsMargins(11, 0, 11, 0) 40 | 41 | self._title_label = QLabel(self._title) 42 | 43 | self._background_layout.addWidget(self._title_label) 44 | self._background_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding)) 45 | self._background_label.setLayout(self._background_layout) 46 | self._layout.addWidget(self._background_label) 47 | self.content.setVisible(False) 48 | 49 | def mousePressEvent(self, *args): 50 | """Handle mouse events, call the function to toggle groups""" 51 | self.expand() if not self.content.isVisible() else self.collapse() 52 | self.toggled.emit(True) if self.content.isVisible() else self.toggled.emit(False) 53 | 54 | @property 55 | def background_label(self): 56 | return self._background_label 57 | 58 | @property 59 | def background_layout(self): 60 | return self._background_layout 61 | 62 | @property 63 | def title_label(self) -> QLabel: 64 | return self._title_label 65 | 66 | @property 67 | def icon_label(self) -> QLabel: 68 | return self._icon_label 69 | 70 | @property 71 | def title(self) -> str: 72 | return self._title 73 | 74 | @title.setter 75 | def title(self, text: str) -> None: 76 | self._title = text 77 | 78 | def expand(self): 79 | self.content.setVisible(True) 80 | self._icon_label.setPixmap(self.expand_pixmap.scaled(self.icon_size, aspectMode=Qt.KeepAspectRatio)) 81 | 82 | def collapse(self): 83 | self.content.setVisible(False) 84 | self._icon_label.setPixmap(self.collapse_pixmap.scaled(self.icon_size, aspectMode=Qt.KeepAspectRatio)) 85 | 86 | def set_icon_filenames(self, expand_fn, collapse_fn): 87 | self.expand_pixmap = QPixmap(expand_fn) 88 | self.collapse_pixmap = QPixmap(collapse_fn) 89 | self._icon_label.setPixmap(self.collapse_pixmap.scaled(self.icon_size, aspectMode=Qt.KeepAspectRatio)) 90 | 91 | def set_icon_size(self, size): 92 | self.icon_size = size 93 | self._icon_label.setPixmap(self.collapse_pixmap.scaled(self.icon_size, aspectMode=Qt.KeepAspectRatio)) 94 | self._icon_label.setFixedSize(size) 95 | 96 | 97 | class QCollapsibleWidget(QWidget): 98 | """ 99 | Class for creating a collapsible group. 100 | """ 101 | 102 | toggled = Signal(bool) # Toggle state in [True, False] 103 | 104 | def __init__(self, name): 105 | """Container Class Constructor to initialize the object 106 | 107 | Args: 108 | name (str): Name for the header 109 | """ 110 | super(QCollapsibleWidget, self).__init__() 111 | self.layout = QVBoxLayout(self) 112 | self.layout.setContentsMargins(0, 0, 0, 0) 113 | self.layout.setSpacing(0) 114 | self._content_widget = QWidget() 115 | self._header = Header(name, self._content_widget) 116 | self.layout.addWidget(self._header) 117 | self._content_layout = QVBoxLayout() 118 | self._content_layout.setContentsMargins(0, 0, 0, 0) 119 | self._content_layout.setSpacing(0) 120 | self._content_widget.setLayout(self._content_layout) 121 | self.layout.addWidget(self._content_widget) 122 | 123 | self.collapse = self._header.collapse 124 | self.expand = self._header.expand 125 | self.toggle = self._header.mousePressEvent 126 | 127 | self._header.toggled.connect(self.on_toggled) 128 | 129 | @property 130 | def content_layout(self): 131 | return self._content_layout 132 | 133 | @property 134 | def content_widget(self): 135 | return self._content_widget 136 | 137 | @property 138 | def header(self): 139 | return self._header 140 | 141 | def on_toggled(self, state): 142 | self.toggled.emit(state) 143 | 144 | def set_icon_filenames(self, expand_fn: str, collapse_fn: str) -> None: 145 | self._header.set_icon_filenames(expand_fn, collapse_fn) 146 | 147 | def clear_content_layout(self): 148 | items = (self.content_layout.itemAt(i) for i in reversed(range(self.content_layout.count()))) 149 | for i in items: 150 | try: 151 | if i and i.widget(): # Current item is a QWidget that can be directly removed 152 | w = i.widget() 153 | w.setParent(None) 154 | w.deleteLater() 155 | else: # Current item is possibly a layout. @TODO. Should be doing a recursive search in case of inception layouts... 156 | items2 = (i.itemAt(j) for j in reversed(range(i.count()))) 157 | for ii in items2: 158 | if ii and ii.widget(): 159 | w2 = ii.widget() 160 | w2.setParent(None) 161 | w2.deleteLater() 162 | self.content_layout.removeItem(i) 163 | except Exception: 164 | pass 165 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQGroupBox/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQTableWidget/ContextMenuQTableWidget.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QScrollArea, QMenu, QTableWidget 2 | from PySide6.QtGui import QIcon, QPixmap, QAction 3 | from PySide6.QtCore import Qt, QSize, Signal, QEvent 4 | 5 | from gui.UtilsWidgets.QCustomIconsPushButton import QCustomIconsPushButton 6 | 7 | 8 | class ContextMenuQTableWidget(QTableWidget): 9 | """ 10 | Generic QTableWidget with a contextual menu appearing under cursor on right click event. 11 | """ 12 | 13 | def __init__(self, parent=None): 14 | super(ContextMenuQTableWidget, self).__init__(parent) 15 | self.current_item = None 16 | self.__set_interface() 17 | self.__set_stylesheets() 18 | self.__set_connections() 19 | 20 | def __set_interface(self): 21 | self.setContextMenuPolicy(Qt.CustomContextMenu) 22 | self.context_menu = QMenu(self) 23 | 24 | def __set_stylesheets(self): 25 | pass 26 | 27 | def __set_connections(self): 28 | pass 29 | 30 | def mousePressEvent(self, event): 31 | """ 32 | """ 33 | if event.button() == Qt.RightButton: 34 | item = self.itemAt(event.pos()) 35 | if item is not None: 36 | # print('Table Item:', item.row(), item.column()) 37 | self.current_item = item 38 | self.context_menu.exec(event.globalPos()) 39 | super(ContextMenuQTableWidget, self).mousePressEvent(event) 40 | 41 | def get_column_values(self, column_index): 42 | result = [] 43 | for r in range(self.rowCount()): 44 | result.append(self.item(r, column_index).text()) 45 | 46 | return result 47 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQTableWidget/ImportDICOMQTableWidget.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QAction 2 | from PySide6.QtCore import Qt, Signal 3 | 4 | from gui.UtilsWidgets.CustomQTableWidget.ContextMenuQTableWidget import ContextMenuQTableWidget 5 | from utils.software_config import SoftwareConfigResources 6 | 7 | 8 | class ImportDICOMQTableWidget(ContextMenuQTableWidget): 9 | """ 10 | 11 | """ 12 | display_metadata_triggered = Signal(int) # Row index of the clicked cell 13 | remove_entry_triggered = Signal(int) # Row index of the clicked cell 14 | 15 | def __init__(self, parent=None): 16 | super(ImportDICOMQTableWidget, self).__init__(parent) 17 | self.__set_interface() 18 | self.__set_stylesheets() 19 | self.__set_connections() 20 | 21 | def __set_interface(self): 22 | self.display_metadata_action = QAction("Display DICOM metadata") 23 | self.context_menu.addAction(self.display_metadata_action) 24 | self.remove_action = QAction("Remove") 25 | self.context_menu.addAction(self.remove_action) 26 | 27 | def __set_stylesheets(self): 28 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 29 | font_color = software_ss["Color7"] 30 | background_color = software_ss["Color2"] 31 | pressed_background_color = software_ss["Background_pressed"] 32 | 33 | self.context_menu.setStyleSheet(""" 34 | QMenu{ 35 | background-color: """ + background_color + """; 36 | color: """ + font_color + """; 37 | font: 11px; 38 | } 39 | QMenu::item:selected{ 40 | background: """ + pressed_background_color + """; 41 | color: white; 42 | } 43 | QMenu::item:pressed{ 44 | background: """ + pressed_background_color + """; 45 | color: white; 46 | border-style: inset; 47 | } 48 | """) 49 | 50 | def __set_connections(self): 51 | self.display_metadata_action.triggered.connect(self.__on_display_metadata_triggered) 52 | self.remove_action.triggered.connect(self.__on_remove_entry_triggered) 53 | 54 | def mousePressEvent(self, event): 55 | """ 56 | """ 57 | if event.button() == Qt.LeftButton: 58 | item = self.itemAt(event.pos()) 59 | if item is not None: 60 | super(ImportDICOMQTableWidget, self).mousePressEvent(event) 61 | else: 62 | super(ImportDICOMQTableWidget, self).mousePressEvent(event) 63 | 64 | def __on_display_metadata_triggered(self): 65 | self.display_metadata_triggered.emit(self.current_item.row()) 66 | 67 | def __on_remove_entry_triggered(self): 68 | self.remove_entry_triggered.emit(self.current_item.row()) 69 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/CustomQTableWidget/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/gui/UtilsWidgets/CustomQTableWidget/__init__.py -------------------------------------------------------------------------------- /gui/UtilsWidgets/QCircularProgressBar.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QHBoxLayout, QPushButton, QWidget 2 | from PySide6.QtCore import QSize, Qt, Signal, QRectF 3 | from PySide6.QtGui import QIcon, QPaintEvent, QPainter, QPainterPath, QPen, QColor, QFont 4 | from utils.software_config import SoftwareConfigResources 5 | 6 | 7 | class QCircularProgressBar(QWidget): 8 | """ 9 | 10 | """ 11 | 12 | def __init__(self, parent): 13 | super(QCircularProgressBar, self).__init__() 14 | self.parent = parent 15 | self.progress_ratio = 0 # Float in [0., 1.] 16 | self.display_header = "Progress: " 17 | self.display_progress = " " 18 | self.setAttribute(Qt.WA_StyledBackground, True) # Enables to set e.g. background-color for the QWidget 19 | self.__set_stylesheets() 20 | 21 | def __set_stylesheets(self): 22 | software_ss = SoftwareConfigResources.getInstance().stylesheet_components 23 | font_color = software_ss["Color7"] 24 | background_color = software_ss["Color2"] 25 | self.setFixedSize(QSize(208, 208)) 26 | self.frame_color = QColor(235, 250, 255) 27 | self.progress_color = QColor(55, 235, 126) # QColor("#30b7e0")) 28 | self.text_color = QColor(67, 88, 90) 29 | 30 | self.setStyleSheet(""" 31 | QWidget{ 32 | background-color: """ + background_color + """; 33 | }""") 34 | 35 | def paintEvent(self, event: QPaintEvent) -> None: 36 | """ 37 | Repainting the widget fully everytime, the painter can only be called/accessed from within here. 38 | 39 | Parameters 40 | ---------- 41 | event: QPaintEvent 42 | Qt internal painting event, not directly used. 43 | """ 44 | pd = self.progress_ratio * 360 45 | rd = 360 - pd 46 | painter = QPainter(self) 47 | painter.fillRect(self.rect(), self.frame_color) 48 | painter.translate(4, 4) 49 | painter.setRenderHint(QPainter.Antialiasing) 50 | path = QPainterPath() 51 | path2 = QPainterPath() 52 | path.moveTo(100, 0) 53 | path.arcTo(QRectF(0, 0, 200, 200), 90, - pd) 54 | 55 | pen = QPen() 56 | pen2 = QPen() 57 | pen.setCapStyle(Qt.FlatCap) 58 | pen.setColor(self.progress_color) 59 | pen.setWidth(8) 60 | painter.strokePath(path, pen) 61 | path2.moveTo(100, 0) 62 | pen2.setWidth(8) 63 | pen2.setColor(Qt.black) 64 | pen2.setCapStyle(Qt.FlatCap) 65 | pen2.setDashPattern([0.5, 1.105]) 66 | path2.arcTo(QRectF(0, 0, 200, 200), 90, rd) 67 | pen2.setDashOffset(2.2) 68 | painter.strokePath(path2, pen2) 69 | text_pen = QPen(self.text_color) 70 | painter.setPen(text_pen) 71 | text_font = QFont("Arial", 18) 72 | text_font.setBold(True) 73 | painter.setFont(text_font) 74 | cast_perc = int(self.progress_ratio * 100.) 75 | painter.drawText(25, 94, self.display_header) 76 | if cast_perc < 10: 77 | painter.drawText(90, 124, self.display_progress) 78 | elif cast_perc < 100: 79 | painter.drawText(80, 124, self.display_progress) 80 | else: 81 | painter.drawText(75, 124, self.display_progress) 82 | painter.end() 83 | 84 | def reset(self): 85 | """ 86 | Repainting the widget with default values at the start of a new process. 87 | """ 88 | self.progress_ratio = 0. 89 | self.display_header = "Progress: " 90 | self.display_progress = "-%" 91 | self.update() 92 | 93 | def advance(self, current_step: int, total_steps: int) -> None: 94 | """ 95 | Advancing the progress of the widget by providing the newly reached step of a process. 96 | 97 | Parameters 98 | ---------- 99 | current_step: int 100 | Value of the step currently performed by the ongoing process. 101 | total_steps: int 102 | Value of the total number of steps to be performed by the ongoing process. 103 | """ 104 | self.progress_ratio = float(current_step/total_steps) 105 | cast_perc = int(self.progress_ratio * 100.) 106 | self.display_header = "Progress: {}/{}".format(current_step, total_steps) 107 | self.display_progress = "{}%".format(cast_perc) 108 | self.update() 109 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/QCustomIconsPushButton.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QHBoxLayout, QPushButton 2 | from PySide6.QtCore import QSize, Qt, Signal 3 | from PySide6.QtGui import QIcon 4 | 5 | 6 | class QCustomIconsPushButton(QPushButton): 7 | """ 8 | 9 | """ 10 | bool_clicked = Signal(bool) 11 | right_clicked = Signal(str, bool) 12 | 13 | def __init__(self, uid, parent=None, icon_style='right', right_behaviour='native'): 14 | super(QCustomIconsPushButton, self).__init__() 15 | self.parent = parent 16 | self.uid = uid 17 | super(QCustomIconsPushButton, self).setText(uid) 18 | self.icon_style = icon_style 19 | self.right_behaviour = right_behaviour 20 | self.left_icon = QIcon() 21 | self.checked_left_icon = QIcon() 22 | self.left_icon_size = QSize() 23 | self.checked_left_icon_size = QSize() 24 | self.right_icon = QIcon() 25 | self.checked_right_icon = QIcon() 26 | self.right_icon_size = QSize() 27 | self.checked_right_icon_size = QSize() 28 | # remove icon 29 | super(QCustomIconsPushButton, self).setIcon(QIcon()) 30 | self.layout = QHBoxLayout(self) 31 | self.layout.setContentsMargins(5, 0, 5, 0) 32 | 33 | if self.icon_style == 'left' or self.icon_style == 'double': 34 | self.label_left_icon = QLabel() 35 | self.label_left_icon.setAttribute(Qt.WA_TranslucentBackground) 36 | self.label_left_icon.setAttribute(Qt.WA_TransparentForMouseEvents) 37 | self.layout.addWidget(self.label_left_icon, alignment=Qt.AlignLeft) 38 | 39 | if self.icon_style == 'right' or self.icon_style == 'double': 40 | if self.right_behaviour != 'native': 41 | self.right_icon_widget = QPushButton() 42 | self.right_icon_widget.setCheckable(True) 43 | else: 44 | self.right_icon_widget = QLabel() 45 | self.right_icon_widget.setAttribute(Qt.WA_TranslucentBackground) 46 | self.right_icon_widget.setAttribute(Qt.WA_TransparentForMouseEvents) 47 | self.layout.addWidget(self.right_icon_widget, alignment=Qt.AlignRight) 48 | 49 | super(QCustomIconsPushButton, self).clicked.connect(self.on_clicked) 50 | if self.right_behaviour == 'stand-alone': 51 | self.right_icon_widget.clicked.connect(self.__on_right_button_clicked) 52 | self.right_icon_widget.toggled.connect(self.__on_right_button_toggled) 53 | 54 | def setIcon(self, icon, size=QSize(), side='right', checked=False): 55 | if side == 'right' and not checked: 56 | self.right_icon = icon 57 | self.right_icon_size = size 58 | if self.right_behaviour == 'native': 59 | self.right_icon_widget.setPixmap(self.right_icon.pixmap(self.right_icon_size)) 60 | else: 61 | self.right_icon_widget.setIcon(self.right_icon) 62 | self.right_icon_widget.setIconSize(self.right_icon_size) 63 | elif side == 'right' and checked: 64 | self.checked_right_icon = icon 65 | self.checked_right_icon_size = size 66 | elif side == 'left' and not checked: 67 | self.left_icon = icon 68 | self.left_icon_size = size 69 | self.label_left_icon.setPixmap(self.left_icon.pixmap(self.left_icon_size)) 70 | elif side == 'left' and checked: 71 | self.checked_left_icon = icon 72 | self.checked_left_icon_size = size 73 | 74 | def setText(self, text): 75 | super(QCustomIconsPushButton, self).setText(text) 76 | 77 | def setStyleSheet(self, styleSheet): 78 | base_stylesheet = "" # "QPushButton{text-align:left;}" 79 | super(QCustomIconsPushButton, self).setStyleSheet(styleSheet + base_stylesheet) 80 | 81 | def on_clicked(self, *args, **kwargs): 82 | if self.icon_style == 'right' or self.icon_style == 'double': 83 | if self.isCheckable() and self.isChecked() and self.checked_right_icon != QIcon(): 84 | if self.right_behaviour == 'native': 85 | self.right_icon_widget.setPixmap(self.checked_right_icon.pixmap(self.checked_right_icon_size)) 86 | # elif self.right_behaviour == 'stand-alone': 87 | # self.right_icon_widget.setIcon(self.checked_right_icon) 88 | # self.right_icon_widget.setIconSize(self.checked_right_icon_size) 89 | elif self.right_icon != QIcon(): 90 | if self.right_behaviour == 'native': 91 | self.right_icon_widget.setPixmap(self.right_icon.pixmap(self.right_icon_size)) 92 | # elif self.right_behaviour == 'stand-alone': 93 | # self.right_icon_widget.setIcon(self.right_icon) 94 | # self.right_icon_widget.setIconSize(self.right_icon_size) 95 | 96 | if self.icon_style == 'left' or self.icon_style == 'double': 97 | if self.isCheckable() and self.isChecked() and self.checked_left_icon != QIcon(): 98 | self.label_left_icon.setPixmap(self.checked_left_icon.pixmap(self.checked_left_icon_size)) 99 | elif self.left_icon != QIcon(): 100 | self.label_left_icon.setPixmap(self.left_icon.pixmap(self.left_icon_size)) 101 | 102 | self.bool_clicked.emit(self.isChecked()) # Has to be a better way to handle this... 103 | 104 | def __on_right_button_clicked(self): 105 | # @TODO. Might be quite stupid, the toggled() signal contains the state as opposed to the clicked() signal... 106 | if self.right_icon_widget.isChecked(): 107 | self.right_icon_widget.setIcon(self.checked_right_icon) 108 | self.right_icon_widget.setIconSize(self.checked_right_icon_size) 109 | else: 110 | self.right_icon_widget.setIcon(self.right_icon) 111 | self.right_icon_widget.setIconSize(self.right_icon_size) 112 | 113 | self.right_clicked.emit(self.uid, self.right_icon_widget.isChecked()) 114 | 115 | def __on_right_button_toggled(self, state): 116 | if state: #self.right_icon_widget.isChecked(): 117 | self.right_icon_widget.setIcon(self.checked_right_icon) 118 | self.right_icon_widget.setIconSize(self.checked_right_icon_size) 119 | else: 120 | self.right_icon_widget.setIcon(self.right_icon) 121 | self.right_icon_widget.setIconSize(self.right_icon_size) 122 | -------------------------------------------------------------------------------- /gui/UtilsWidgets/QDoubleIconsPushButton.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QHBoxLayout, QPushButton 2 | from PySide6.QtCore import QSize, Qt, Signal 3 | from PySide6.QtGui import QIcon 4 | 5 | 6 | class QDoubleIconsPushButton(QPushButton): 7 | """ 8 | 9 | """ 10 | bool_clicked = Signal(bool) 11 | 12 | def __init__(self, text, parent): 13 | super(QDoubleIconsPushButton, self).__init__() 14 | self.parent = parent 15 | super(QDoubleIconsPushButton, self).setText(text) 16 | self.right_icon = QIcon() 17 | self.checked_right_icon = QIcon() 18 | self.right_icon_size = QSize() 19 | self.checked_right_icon_size = QSize() 20 | # remove icon 21 | super(QDoubleIconsPushButton, self).setIcon(QIcon()) 22 | self.label_left_icon = QLabel() 23 | self.label_left_icon.setAttribute(Qt.WA_TranslucentBackground) 24 | self.label_left_icon.setAttribute(Qt.WA_TransparentForMouseEvents) 25 | self.label_right_icon = QLabel() 26 | self.label_right_icon.setAttribute(Qt.WA_TranslucentBackground) 27 | self.label_right_icon.setAttribute(Qt.WA_TransparentForMouseEvents) 28 | lay = QHBoxLayout(self) 29 | lay.setContentsMargins(0, 0, 15, 0) 30 | lay.addWidget(self.label_left_icon, alignment=Qt.AlignLeft) 31 | lay.addWidget(self.label_right_icon, alignment=Qt.AlignRight) 32 | 33 | super(QDoubleIconsPushButton, self).clicked.connect(self.on_clicked) 34 | 35 | def setIcon(self, icon, size=QSize(), side='Right'): 36 | if side == 'Right': 37 | self.right_icon = icon 38 | self.right_icon_size = size 39 | self.label_right_icon.setPixmap(self.right_icon.pixmap(self.right_icon_size)) 40 | else: 41 | self.left_icon = icon 42 | self.left_icon_size = size 43 | self.label_left_icon.setPixmap(self.left_icon.pixmap(self.left_icon_size)) 44 | 45 | def setRightIcon(self, icon, size=QSize()): 46 | self.right_icon = icon 47 | self.right_icon_size = size 48 | self.label_right_icon.setPixmap(self.right_icon.pixmap(self.right_icon_size)) 49 | 50 | def setLeftIcon(self, icon, size=QSize()): 51 | self.left_icon = icon 52 | self.left_icon_size = size 53 | self.label_left_icon.setPixmap(self.left_icon.pixmap(self.left_icon_size)) 54 | 55 | def setCheckedRightIcon(self, icon, size=QSize()): 56 | self.checked_right_icon = icon 57 | self.checked_right_icon_size = size 58 | 59 | def setCheckedLeftIcon(self, icon, size=QSize()): 60 | self.checked_left_icon = icon 61 | self.checked_left_icon_size = size 62 | 63 | def setText(self, text): 64 | super(QDoubleIconsPushButton, self).setText(text) 65 | 66 | def setStyleSheet(self, styleSheet): 67 | base_stylesheet = "" #"QPushButton{text-align:left;}" 68 | super(QDoubleIconsPushButton, self).setStyleSheet(styleSheet + base_stylesheet) 69 | 70 | def on_clicked(self, *args, **kwargs): 71 | if self.isCheckable() and self.isChecked() and self.checked_right_icon != QIcon(): 72 | self.label_right_icon.setPixmap(self.checked_right_icon.pixmap(self.checked_right_icon_size)) 73 | elif self.right_icon != QIcon(): 74 | self.label_right_icon.setPixmap(self.right_icon.pixmap(self.right_icon_size)) 75 | self.bool_clicked.emit(self.isChecked()) # Has to be a better way to handle this... -------------------------------------------------------------------------------- /gui/UtilsWidgets/QRightIconPushButton.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel, QHBoxLayout, QPushButton 2 | from PySide6.QtCore import QSize, Qt, Signal 3 | from PySide6.QtGui import QIcon 4 | 5 | 6 | class QRightIconPushButton(QPushButton): 7 | """ 8 | 9 | """ 10 | bool_clicked = Signal(bool) 11 | 12 | def __init__(self, text, parent): 13 | super(QRightIconPushButton, self).__init__() 14 | self.parent = parent 15 | super(QRightIconPushButton, self).setText(text) 16 | self.icon = QIcon() 17 | self.checked_icon = QIcon() 18 | self.icon_size = QSize() 19 | self.checked_icon_size = QSize() 20 | # remove icon 21 | super(QRightIconPushButton, self).setIcon(QIcon()) 22 | self.label_icon = QLabel() 23 | self.label_icon.setAttribute(Qt.WA_TranslucentBackground) 24 | self.label_icon.setAttribute(Qt.WA_TransparentForMouseEvents) 25 | lay = QHBoxLayout(self) 26 | lay.setContentsMargins(0, 0, 15, 0) 27 | lay.addWidget(self.label_icon, alignment=Qt.AlignRight) 28 | 29 | super(QRightIconPushButton, self).clicked.connect(self.on_clicked) 30 | 31 | def setIcon(self, icon, size=QSize()): 32 | self.icon = icon 33 | self.icon_size = size 34 | self.label_icon.setPixmap(self.icon.pixmap(self.icon_size)) 35 | 36 | def setCheckedIcon(self, icon, size=QSize()): 37 | self.checked_icon = icon 38 | self.checked_icon_size = size 39 | 40 | def setText(self, text): 41 | super(QRightIconPushButton, self).setText(text) 42 | 43 | def setStyleSheet(self, styleSheet): 44 | base_stylesheet = "" #"QPushButton{text-align:left;}" 45 | super(QRightIconPushButton, self).setStyleSheet(styleSheet + base_stylesheet) 46 | 47 | def on_clicked(self, *args, **kwargs): 48 | if self.isCheckable() and self.isChecked() and self.checked_icon != QIcon(): 49 | self.label_icon.setPixmap(self.checked_icon.pixmap(self.checked_icon_size)) 50 | elif self.icon != QIcon(): 51 | self.label_icon.setPixmap(self.icon.pixmap(self.icon_size)) 52 | self.bool_clicked.emit(self.isChecked()) # Has to be a better way to handle this... -------------------------------------------------------------------------------- /gui/UtilsWidgets/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /integration_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/integration_tests/__init__.py -------------------------------------------------------------------------------- /integration_tests/batch_study_module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/integration_tests/batch_study_module/__init__.py -------------------------------------------------------------------------------- /integration_tests/batch_study_module/test_bm_creation_and_data_import.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from time import sleep 4 | import logging 5 | import platform 6 | import traceback 7 | import requests 8 | import zipfile 9 | 10 | import pytest 11 | from PySide6.QtCore import Qt 12 | 13 | from gui.RaidionicsMainWindow import RaidionicsMainWindow 14 | from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog 15 | from utils.software_config import SoftwareConfigResources 16 | from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure 17 | 18 | def_loc = UserPreferencesStructure.getInstance().user_home_location 19 | _ = SoftwareConfigResources.getInstance().get_session_log_filename() 20 | 21 | @pytest.fixture 22 | def test_location(): 23 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 24 | UserPreferencesStructure.getInstance().user_home_location = test_loc 25 | if os.path.exists(test_loc): 26 | shutil.rmtree(test_loc) 27 | os.makedirs(test_loc) 28 | return test_loc 29 | 30 | @pytest.fixture 31 | def test_data_folder(): 32 | test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.1.zip' 33 | test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 34 | test_data_dir = os.path.join(test_dir, 'ApprovedExample') 35 | if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: 36 | return test_data_dir 37 | 38 | archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') 39 | headers = {} 40 | response = requests.get(test_data_url, headers=headers, stream=True) 41 | response.raise_for_status() 42 | if response.status_code == requests.codes.ok: 43 | with open(archive_dl_dest, "wb") as f: 44 | for chunk in response.iter_content(chunk_size=1048576): 45 | f.write(chunk) 46 | with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: 47 | zip_ref.extractall(test_dir) 48 | return test_data_dir 49 | 50 | 51 | @pytest.fixture 52 | def window(): 53 | """ 54 | 55 | """ 56 | window = RaidionicsMainWindow() 57 | window.on_clear_scene() 58 | UserPreferencesStructure.getInstance().disable_modal_warnings = True 59 | return window 60 | 61 | """ 62 | Remaining tests to add: 63 | * Import patient and jump to patient view and assert that the MRIs are correctly displayed (working now) 64 | * Using the Clear main option should also properly reset all related Study Widgets (working now) 65 | * Adding multiple new studies in a row (first with a patient and then without) and checking that the other two 66 | panels are displaying one or no patient accordingly (not working now) 67 | """ 68 | 69 | 70 | def test_empty_study_creation(qtbot, test_location, window): 71 | """ 72 | Creation of a new empty patient. 73 | """ 74 | try: 75 | qtbot.addWidget(window) 76 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 77 | window.batch_study_widget.studies_panel.add_empty_study_action.trigger() 78 | assert len(SoftwareConfigResources.getInstance().study_parameters) == 1 79 | except Exception as e: 80 | if platform.system() == 'Darwin': 81 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 82 | return 83 | 84 | def test_empty_study_add_single_patient_folder(qtbot, test_location, window): 85 | """ 86 | Creation of a new empty study followed by inclusion of a single patient (with regular folder structure). 87 | """ 88 | try: 89 | qtbot.addWidget(window) 90 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 91 | # Creating a new empty study 92 | window.batch_study_widget.studies_panel.add_empty_study_action.trigger() 93 | 94 | # Importing a single folder-based patient 95 | window.batch_study_widget.studies_panel.get_study_widget_by_index(0).import_data_dialog.set_parsing_mode('single') 96 | window.batch_study_widget.studies_panel.get_study_widget_by_index(0).import_data_dialog.set_target_type('regular') 97 | single_patient_filepath = os.path.join(test_location, 'Raw') 98 | window.batch_study_widget.studies_panel.get_study_widget_by_index(0).import_data_dialog.setup_interface_from_selection(directory=single_patient_filepath) 99 | window.batch_study_widget.studies_panel.get_study_widget_by_index(0).import_data_dialog.__on_exit_accept_clicked() 100 | 101 | # Verifying that the patient is correctly listed internally and in the interface 102 | assert SoftwareConfigResources.getInstance().get_active_study().get_total_included_patients() == 1 103 | assert window.batch_study_widget.patient_listing_panel.get_study_patient_widget_length() == 1 104 | #assert window.batch_study_widget.patients_summary_panel.?? 105 | 106 | # Saving the latest modifications to the study on disk by pressing the disk icon 107 | qtbot.mouseClick(window.batch_study_widget.studies_panel.get_study_widget_by_index(0).save_study_pushbutton, Qt.MouseButton.LeftButton) 108 | except Exception as e: 109 | if platform.system() == 'Darwin': 110 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 111 | return 112 | 113 | def test_cleanup(window): 114 | if window.logs_thread.isRunning(): 115 | window.logs_thread.stop() 116 | sleep(2) 117 | UserPreferencesStructure.getInstance().user_home_location = def_loc 118 | UserPreferencesStructure.getInstance().disable_modal_warnings = False 119 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 120 | if os.path.exists(test_loc): 121 | shutil.rmtree(test_loc) 122 | -------------------------------------------------------------------------------- /integration_tests/batch_study_module/test_bm_creation_and_dicom_import.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from time import sleep 4 | import logging 5 | import traceback 6 | import platform 7 | import requests 8 | import zipfile 9 | 10 | import pytest 11 | from PySide6.QtCore import Qt 12 | 13 | from gui.RaidionicsMainWindow import RaidionicsMainWindow 14 | from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog 15 | from utils.software_config import SoftwareConfigResources 16 | from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure 17 | 18 | def_loc = UserPreferencesStructure.getInstance().user_home_location 19 | 20 | @pytest.fixture 21 | def test_location(): 22 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 23 | UserPreferencesStructure.getInstance().user_home_location = test_loc 24 | if os.path.exists(test_loc): 25 | shutil.rmtree(test_loc) 26 | os.makedirs(test_loc) 27 | return test_loc 28 | 29 | @pytest.fixture 30 | def test_data_folder(): 31 | test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.1.zip' 32 | test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 33 | test_data_dir = os.path.join(test_dir, 'ApprovedExample') 34 | if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: 35 | return test_data_dir 36 | 37 | archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') 38 | headers = {} 39 | response = requests.get(test_data_url, headers=headers, stream=True) 40 | response.raise_for_status() 41 | if response.status_code == requests.codes.ok: 42 | with open(archive_dl_dest, "wb") as f: 43 | for chunk in response.iter_content(chunk_size=1048576): 44 | f.write(chunk) 45 | with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: 46 | zip_ref.extractall(test_dir) 47 | return test_data_dir 48 | 49 | @pytest.fixture 50 | def dicom_resources_folder(): 51 | test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-IntegrationTestDicom-v1.3.1.zip' 52 | test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 53 | dicom_resources_dir = os.path.join(test_dir, 'IntegrationTest-dicom') 54 | if os.path.exists(dicom_resources_dir) and len(os.listdir(dicom_resources_dir)) > 0: 55 | return dicom_resources_dir 56 | 57 | archive_dl_dest = os.path.join(test_dir, 'raidionics_dicom_resources.zip') 58 | headers = {} 59 | response = requests.get(test_data_url, headers=headers, stream=True) 60 | response.raise_for_status() 61 | if response.status_code == requests.codes.ok: 62 | with open(archive_dl_dest, "wb") as f: 63 | for chunk in response.iter_content(chunk_size=1048576): 64 | f.write(chunk) 65 | with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: 66 | zip_ref.extractall(test_dir) 67 | return dicom_resources_dir 68 | 69 | @pytest.fixture 70 | def window(): 71 | """ 72 | 73 | """ 74 | window = RaidionicsMainWindow() 75 | window.on_clear_scene() 76 | UserPreferencesStructure.getInstance().disable_modal_warnings = True 77 | return window 78 | 79 | """ 80 | Remaining tests to add: 81 | * Import patient and jump to patient view and assert that the MRIs are correctly displayed (not working now) 82 | * Having two studies and switching from one to the other does not refresh the two other panels... 83 | """ 84 | 85 | def test_dicom_study_reloading(qtbot, test_location, test_data_folder, dicom_resources_folder, window): 86 | """ 87 | Reloading of an existing study based of DICOM files. 88 | """ 89 | try: 90 | qtbot.addWidget(window) 91 | 92 | # Entering the batch study widget view 93 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 94 | 95 | # Importing existing study from Add study > Existing study (*.sraidionics) 96 | raidionics_filename = os.path.join(dicom_resources_folder, 'Raidionics', "studies", "studydicom", "studydicom_study.sraidionics") 97 | window.batch_study_widget.import_data_dialog.reset() 98 | window.batch_study_widget.import_data_dialog.set_parsing_filter("study") 99 | window.batch_study_widget.import_data_dialog.setup_interface_from_files([raidionics_filename]) 100 | window.batch_study_widget.import_data_dialog.__on_exit_accept_clicked() 101 | sleep(10) 102 | assert len(list(SoftwareConfigResources.getInstance().get_active_study().included_patients_uids.keys())) == 2 103 | assert list(SoftwareConfigResources.getInstance().get_active_study().included_patients_uids.keys()) == ['39456', '69190'] 104 | 105 | window.on_clear_scene() 106 | assert SoftwareConfigResources.getInstance().is_study_list_empty() 107 | assert window.batch_study_widget.studies_panel.get_study_widget_length() == 0 108 | except Exception as e: 109 | if platform.system() == 'Darwin': 110 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 111 | return 112 | 113 | def test_cleanup(window): 114 | if window.logs_thread.isRunning(): 115 | window.logs_thread.stop() 116 | sleep(2) 117 | UserPreferencesStructure.getInstance().user_home_location = def_loc 118 | UserPreferencesStructure.getInstance().disable_modal_warnings = False 119 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 120 | if os.path.exists(test_loc): 121 | shutil.rmtree(test_loc) 122 | -------------------------------------------------------------------------------- /integration_tests/batch_study_module/test_bm_creation_and_edit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from time import sleep 4 | import logging 5 | import traceback 6 | import platform 7 | import requests 8 | import zipfile 9 | 10 | import pytest 11 | from PySide6.QtCore import Qt 12 | 13 | from gui.RaidionicsMainWindow import RaidionicsMainWindow 14 | from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog 15 | from utils.software_config import SoftwareConfigResources 16 | from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure 17 | 18 | def_loc = UserPreferencesStructure.getInstance().user_home_location 19 | 20 | @pytest.fixture 21 | def test_location(): 22 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 23 | UserPreferencesStructure.getInstance().user_home_location = test_loc 24 | if os.path.exists(test_loc): 25 | shutil.rmtree(test_loc) 26 | os.makedirs(test_loc) 27 | return test_loc 28 | 29 | @pytest.fixture 30 | def test_data_folder(): 31 | test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.1.zip' 32 | test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 33 | test_data_dir = os.path.join(test_dir, 'ApprovedExample') 34 | if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: 35 | return test_data_dir 36 | 37 | archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') 38 | headers = {} 39 | response = requests.get(test_data_url, headers=headers, stream=True) 40 | response.raise_for_status() 41 | if response.status_code == requests.codes.ok: 42 | with open(archive_dl_dest, "wb") as f: 43 | for chunk in response.iter_content(chunk_size=1048576): 44 | f.write(chunk) 45 | with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: 46 | zip_ref.extractall(test_dir) 47 | return test_data_dir 48 | 49 | 50 | @pytest.fixture 51 | def window(): 52 | """ 53 | 54 | """ 55 | window = RaidionicsMainWindow() 56 | window.on_clear_scene() 57 | UserPreferencesStructure.getInstance().disable_modal_warnings = True 58 | return window 59 | 60 | 61 | def test_empty_study_renaming(qtbot, test_location, window): 62 | """ 63 | Creation of a new empty study followed by renaming. 64 | """ 65 | try: 66 | qtbot.addWidget(window) 67 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 68 | window.batch_study_widget.studies_panel.add_empty_study_action.trigger() 69 | window.batch_study_widget.studies_panel.get_study_widget_by_index(0).study_name_lineedit.setText("Study1") 70 | qtbot.keyClick(window.batch_study_widget.studies_panel.get_study_widget_by_index(0).study_name_lineedit, Qt.Key_Enter) 71 | assert SoftwareConfigResources.getInstance().get_active_study().display_name == "Study1" 72 | 73 | qtbot.mouseClick(window.batch_study_widget.studies_panel.get_study_widget_by_index(0).save_study_pushbutton, Qt.MouseButton.LeftButton) 74 | except Exception as e: 75 | if platform.system() == 'Darwin': 76 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 77 | return 78 | 79 | def test_cleanup(window): 80 | if window.logs_thread.isRunning(): 81 | window.logs_thread.stop() 82 | sleep(2) 83 | UserPreferencesStructure.getInstance().user_home_location = def_loc 84 | UserPreferencesStructure.getInstance().disable_modal_warnings = False 85 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 86 | if os.path.exists(test_loc): 87 | shutil.rmtree(test_loc) 88 | -------------------------------------------------------------------------------- /integration_tests/batch_study_module/test_bm_reloading_and_edit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from time import sleep 4 | import logging 5 | import platform 6 | import traceback 7 | import requests 8 | import zipfile 9 | 10 | import pytest 11 | from PySide6.QtCore import Qt 12 | 13 | from gui.RaidionicsMainWindow import RaidionicsMainWindow 14 | from gui.UtilsWidgets.CustomQDialog.ImportDataQDialog import ImportDataQDialog 15 | from utils.software_config import SoftwareConfigResources 16 | from utils.data_structures.UserPreferencesStructure import UserPreferencesStructure 17 | 18 | def_loc = UserPreferencesStructure.getInstance().user_home_location 19 | 20 | @pytest.fixture 21 | def test_location(): 22 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 23 | UserPreferencesStructure.getInstance().user_home_location = test_loc 24 | if os.path.exists(test_loc): 25 | shutil.rmtree(test_loc) 26 | os.makedirs(test_loc) 27 | return test_loc 28 | 29 | @pytest.fixture 30 | def test_data_folder(): 31 | test_data_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-Raidionics-ApprovedExample-v1.3.1.zip' 32 | test_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 33 | test_data_dir = os.path.join(test_dir, 'ApprovedExample') 34 | if os.path.exists(test_data_dir) and len(os.listdir(test_data_dir)) > 0: 35 | return test_data_dir 36 | 37 | archive_dl_dest = os.path.join(test_dir, 'raidionics_resources.zip') 38 | headers = {} 39 | response = requests.get(test_data_url, headers=headers, stream=True) 40 | response.raise_for_status() 41 | if response.status_code == requests.codes.ok: 42 | with open(archive_dl_dest, "wb") as f: 43 | for chunk in response.iter_content(chunk_size=1048576): 44 | f.write(chunk) 45 | with zipfile.ZipFile(archive_dl_dest, 'r') as zip_ref: 46 | zip_ref.extractall(test_dir) 47 | return test_data_dir 48 | 49 | 50 | @pytest.fixture 51 | def window(): 52 | """ 53 | 54 | """ 55 | window = RaidionicsMainWindow() 56 | window.on_clear_scene() 57 | UserPreferencesStructure.getInstance().disable_modal_warnings = True 58 | return window 59 | 60 | """ 61 | Remaining tests to add: 62 | * Import patient and jump to patient view and assert that the MRIs are correctly displayed (not working now) 63 | * Having two studies and switching from one to the other does not refresh the two other panels... 64 | """ 65 | 66 | 67 | def test_study_reloading(qtbot, test_location, test_data_folder, window): 68 | """ 69 | Reloading of an existing study based off nifti files. 70 | """ 71 | try: 72 | qtbot.addWidget(window) 73 | 74 | # Entering the batch study widget view 75 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 76 | 77 | # Importing existing study from Add study > Existing study (*.sraidionics) 78 | # window.batch_study_widget.results_panel.add_existing_study_actionadd_raidionics_patient_action.trigger() <= Cannot use the actual pushbutton action as it would open the QDialog... 79 | raidionics_filename = os.path.join(test_data_folder, 'Raidionics', "studies", "study1", "study1_study.sraidionics") 80 | window.batch_study_widget.import_data_dialog.reset() 81 | window.batch_study_widget.import_data_dialog.set_parsing_filter("study") 82 | window.batch_study_widget.import_data_dialog.setup_interface_from_files([raidionics_filename]) 83 | window.batch_study_widget.import_data_dialog.__on_exit_accept_clicked() 84 | sleep(10) 85 | assert len(list(SoftwareConfigResources.getInstance().get_active_study().included_patients_uids.keys())) == 2 86 | except Exception as e: 87 | if platform.system() == 'Darwin': 88 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 89 | return 90 | 91 | def test_multiple_study_reloading_and_switching(qtbot, test_location, test_data_folder, window): 92 | """ 93 | Reloading of a multiple existing studies and swapping between them to ensure that all content widgets 94 | are properly updated based on the displayed study. 95 | """ 96 | try: 97 | qtbot.addWidget(window) 98 | 99 | # Entering the batch study widget view 100 | qtbot.mouseClick(window.welcome_widget.left_panel_multiple_patients_pushbutton, Qt.MouseButton.LeftButton) 101 | except Exception as e: 102 | if platform.system() == 'Darwin': 103 | logging.error("Error: {}.\nStack: {}".format(e, traceback.format_exc())) 104 | return 105 | 106 | def test_cleanup(window): 107 | if window.logs_thread.isRunning(): 108 | window.logs_thread.stop() 109 | sleep(2) 110 | UserPreferencesStructure.getInstance().user_home_location = def_loc 111 | UserPreferencesStructure.getInstance().disable_modal_warnings = False 112 | test_loc = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'integrationtests') 113 | if os.path.exists(test_loc): 114 | shutil.rmtree(test_loc) 115 | -------------------------------------------------------------------------------- /integration_tests/single_patient_module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/integration_tests/single_patient_module/__init__.py -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.setrecursionlimit(10000) 3 | # print("/n/nRecursion limit set to:", sys.getrecursionlimit()) 4 | 5 | import getopt 6 | import traceback 7 | import os 8 | import platform 9 | from pathlib import PurePath 10 | import PySide6 11 | import sys 12 | from PySide6.QtWidgets import QApplication 13 | from gui.RaidionicsMainWindow import RaidionicsMainWindow 14 | import logging 15 | 16 | 17 | # macOS relevant 18 | os.environ['LC_CTYPE'] = "en_US.UTF-8" 19 | os.environ['LANG'] = "en_US.UTF-8" 20 | 21 | # macOS Big Sur related: https://stackoverflow.com/questions/64818879/is-there-any-solution-regarding-to-pyqt-library-doesnt-work-in-mac-os-big-sur/64856281 22 | os.environ['QT_MAC_WANTS_LAYER'] = '1' 23 | 24 | # relevant for PySide, Qt stuff. See issue here: https://www.programmersought.com/article/8605863159/ 25 | dirname = os.path.dirname(PySide6.__file__) 26 | plugin_path = os.path.join(dirname, 'plugins', 'platforms') 27 | os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path 28 | 29 | 30 | def main(argv): 31 | gui_usage = 1 32 | try: 33 | opts, args = getopt.getopt(argv, "hg:", ["gui=1"]) 34 | except getopt.GetoptError: 35 | print('main.py') 36 | sys.exit(2) 37 | for opt, arg in opts: 38 | if opt == '-h': 39 | print('main.py') 40 | sys.exit() 41 | # elif opt in ("-g", "--use_gui"): 42 | # gui_usage = int(arg) 43 | try: 44 | from utils.software_config import SoftwareConfigResources 45 | logger = logging.getLogger() 46 | handler = logging.FileHandler(filename=SoftwareConfigResources.getInstance().get_session_log_filename(), 47 | mode='w', encoding='utf-8') 48 | handler.setFormatter(logging.Formatter(fmt="%(asctime)s ; %(name)s ; %(levelname)s ; %(message)s", 49 | datefmt='%d/%m/%Y %H.%M')) 50 | logger.setLevel(logging.DEBUG) 51 | logger.addHandler(handler) 52 | 53 | if gui_usage == 1: 54 | app = QApplication(sys.argv) 55 | window = RaidionicsMainWindow(application=app) 56 | window.show() 57 | app.exec() 58 | 59 | #@TODO. Windows-specific stuff to check. 60 | # # ifdef Q_OS_WIN //this is Windows specific code, not portable 61 | # QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow); 62 | # # endif 63 | # For mac, try: window.raise() 64 | except Exception as e: 65 | print('Process could not proceed. Caught error: {}'.format(e.args[0])) 66 | print('{}'.format(traceback.format_exc())) 67 | logging.critical(traceback.format_exc()) 68 | 69 | 70 | if __name__ == "__main__": 71 | # if platform.system() == 'Windows': 72 | import multiprocessing as mp 73 | mp.freeze_support() 74 | mp.set_start_method('spawn', force=True) 75 | main(sys.argv[1:]) 76 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/tests/__init__.py -------------------------------------------------------------------------------- /tests/software_launch_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import logging 4 | import sys 5 | import subprocess 6 | import traceback 7 | import platform 8 | import signal 9 | import time 10 | 11 | 12 | def software_launch_test(): 13 | """ 14 | The purpose of the unit test is to assert that the software launches, that the GUI is visible, and that no 15 | library linking or DLL issues arised during startup. 16 | """ 17 | logging.basicConfig() 18 | logging.getLogger().setLevel(logging.DEBUG) 19 | logging.info("Running software launch unit test.\n") 20 | 21 | try: 22 | stdout = None 23 | stderr = None 24 | build_executable_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../', 'dist', 'Raidionics') 25 | logging.info("Running executable from: {}.\n".format(build_executable_path)) 26 | if platform.system() == 'Windows': 27 | proc = subprocess.Popen([os.path.join(build_executable_path, 'Raidionics')], stdout=subprocess.PIPE, 28 | stderr=subprocess.PIPE, shell=True, 29 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) 30 | time.sleep(10) 31 | stdout = proc.stdout 32 | stderr = proc.stderr 33 | proc.send_signal(signal.CTRL_BREAK_EVENT) 34 | proc.kill() 35 | else: 36 | os.environ['QT_QPA_PLATFORM'] = "offscreen" 37 | proc = subprocess.Popen([os.path.join(build_executable_path, 'Raidionics')], 38 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid) 39 | time.sleep(10) 40 | stdout = proc.stdout 41 | stderr = proc.stderr 42 | os.killpg(os.getpgid(proc.pid), signal.SIGTERM) 43 | 44 | # Parsing the content of stderr to identify any potential issue or problem! 45 | error_msg = stderr.read().decode("utf-8") 46 | print("Collected stdout: {}\n".format(stdout.read().decode("utf-8"))) 47 | print("Collected stderr: {}\n".format(error_msg)) 48 | if error_msg is not None: 49 | if 'error' in error_msg.lower() or 'failed' in error_msg.lower(): 50 | raise ValueError("Error during software launch unit test.\n") 51 | except Exception as e: 52 | logging.error("Error during software launch unit test with: \n {}.\n".format(traceback.format_exc())) 53 | raise ValueError("Error during software launch unit test with.\n") 54 | 55 | logging.info("Software launch unit test succeeded.\n") 56 | 57 | 58 | software_launch_test() 59 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /utils/data_structures/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | -------------------------------------------------------------------------------- /utils/logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raidionics/Raidionics/ccdbbf574274f42e858d720e152851b40231f29e/utils/logic/__init__.py -------------------------------------------------------------------------------- /utils/utilities.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import shutil 3 | 4 | from copy import deepcopy 5 | from typing import List, Tuple 6 | import time 7 | from aenum import Enum, unique 8 | from typing import Union 9 | import os 10 | import SimpleITK as sitk 11 | import numpy as np 12 | 13 | 14 | def get_type_from_string(enum_type: Enum, string: str) -> Union[str, int]: 15 | if type(string) == str: 16 | for i in range(len(list(enum_type))): 17 | if string == str(list(enum_type)[i]): 18 | return list(enum_type)[i] 19 | return -1 20 | elif type(string) == enum_type: 21 | return string 22 | else: #Unmanaged input type 23 | return -1 24 | 25 | 26 | def get_type_from_name(enum_type: Enum, string: str) -> Union[str, int]: 27 | if type(string) == str: 28 | for i in range(len(list(enum_type))): 29 | if string == list(enum_type)[i].name: 30 | return list(enum_type)[i] 31 | return -1 32 | elif type(string) == enum_type: 33 | return string 34 | else: #Unmanaged input type 35 | return -1 36 | 37 | 38 | def input_file_category_disambiguation(input_filename: str) -> str: 39 | """ 40 | Identifying whether the volume stored on disk under input_filename contains a raw MRI volume or is an integer-like 41 | volume with labels. 42 | The category belongs to [MRI, Annotation]. 43 | 44 | Parameters 45 | ---------- 46 | input_filename: str 47 | Disk location of the volume to disambiguate. 48 | 49 | Returns 50 | ---------- 51 | str 52 | Human-readable category identified for the input. 53 | """ 54 | category = None 55 | try: 56 | reader = sitk.ImageFileReader() 57 | reader.SetFileName(input_filename) 58 | image = reader.Execute() 59 | image_type = image.GetPixelIDTypeAsString() 60 | array = sitk.GetArrayFromImage(image) 61 | except Exception as e: 62 | raise ValueError("Loading the input following input file {} with SimpleITK failed with: \n{}".format(input_filename, e)) 63 | 64 | if len(np.unique(array)) > 255 or np.max(array) > 255 or np.min(array) < -1: 65 | category = "MRI" 66 | # If the input radiological volume has values within [0, 255] only. Empirical solution for now, since less than 67 | # 10 classes are usually handle at any given time. 68 | elif len(np.unique(array)) >= 25: 69 | category = "MRI" 70 | else: 71 | category = "Annotation" 72 | return category 73 | 74 | 75 | def input_file_type_conversion(input_filename: str, output_folder: str) -> str: 76 | # Always converting the input file to nifti (if possible), otherwise will be discarded. 77 | # Saving anyway a correct nifti file inside the raidionics patient folder, for use in the backend. 78 | # @TODO. Do we catch a potential .seg file that would be coming from 3D Slicer for annotations? 79 | pre_file_extension = os.path.basename(input_filename).split('.')[0] 80 | file_extension = '.'.join(os.path.basename(input_filename).split('.')[1:]) 81 | filename = input_filename 82 | if file_extension != 'nii' and file_extension != 'nii.gz': 83 | input_sitk = sitk.ReadImage(input_filename) 84 | nifti_outfilename = os.path.join(output_folder, pre_file_extension + '.nii.gz') 85 | sitk.WriteImage(input_sitk, nifti_outfilename) 86 | filename = nifti_outfilename 87 | else: 88 | filename = os.path.join(output_folder, os.path.basename(input_filename)) 89 | if input_filename != filename and not os.path.exists(filename): 90 | shutil.copyfile(input_filename, filename) 91 | elif input_filename != filename and os.path.exists(filename): 92 | # In case of DICOM conversion, multiple files might have the same filename, hence the check and renaming. 93 | new_filename = os.path.join(output_folder, str(np.random.randint(0, 10000)) + "_" + 94 | os.path.basename(input_filename)) 95 | while os.path.exists(new_filename): 96 | new_filename = os.path.join(output_folder, str(np.random.randint(0, 10000)) + "_" + 97 | os.path.basename(input_filename)) 98 | shutil.copyfile(input_filename, new_filename) 99 | filename = new_filename 100 | 101 | return filename 102 | 103 | 104 | def dicom_write_slice(writer: sitk.ImageFileWriter, series_tag_values: List[Tuple[str, str]], new_img: sitk.Image, 105 | i: int, dest_dir: str) -> None: 106 | """ 107 | Code snippet to save a SimpleITK Image object as DICOM, taken from 108 | https://simpleitk.readthedocs.io/en/v1.2.2/Examples/DicomSeriesFromArray/Documentation.html 109 | 110 | Parameters 111 | ---------- 112 | writer: sitk.ImageFileWriter 113 | SimpleITK object performing the DICOM writing on disk. 114 | series_tag_values: List[Tuple[str, str]] 115 | DICOM metadata to store in the newly created DICOM object 116 | new_img: sitk.Image 117 | Radiological volume to store on disk as DICOM 118 | i: int 119 | Index of the current slice in the 3D radiological volume 120 | dest_dir: str 121 | Destination location on disk where the DICOM volume should be stored 122 | """ 123 | image_slice = new_img[:, :, i] 124 | 125 | # Tags shared by the series. 126 | list(map(lambda tag_value: image_slice.SetMetaData(tag_value[0], tag_value[1]), series_tag_values)) 127 | 128 | # Slice specific tags. 129 | image_slice.SetMetaData("0008|0012", time.strftime("%Y%m%d")) # Instance Creation Date 130 | image_slice.SetMetaData("0008|0013", time.strftime("%H%M%S")) # Instance Creation Time 131 | 132 | # Setting the type to CT preserves the slice location. 133 | image_slice.SetMetaData("0008|0060", "CT") # set the type to CT so the thickness is carried over 134 | 135 | # (0020, 0032) image position patient determines the 3D spacing between slices. 136 | image_slice.SetMetaData("0020|0032", '\\'.join(map(str, new_img.TransformIndexToPhysicalPoint((0, 0, i))))) # Image Position (Patient) 137 | image_slice.SetMetaData("0020,0013", str(i)) # Instance Number 138 | 139 | # Write to the output directory and add the extension dcm, to force writing in DICOM format. 140 | writer.SetFileName(os.path.join(dest_dir, str(i)+'.dcm')) 141 | writer.Execute(image_slice) 142 | 143 | 144 | def sanitize_filename(filename): 145 | if not isinstance(filename, str) or filename.strip() == "": 146 | raise ValueError("Filename must be a non-empty string.") 147 | 148 | try: 149 | filename.encode('utf-8') 150 | except UnicodeEncodeError as e: 151 | raise ValueError(f"Filename contains invalid unicode characters: {e}") 152 | 153 | return filename 154 | 155 | EXCLUDE_DIRS = { 156 | "__MACOSX", 157 | ".DS_Store", 158 | ".Trash", 159 | "$RECYCLE.BIN", 160 | "System Volume Information", 161 | ".git", 162 | ".svn", 163 | ".idea", 164 | ".vscode", 165 | ".ipynb_checkpoints", 166 | ".Spotlight-V100", 167 | ".TemporaryItems", 168 | "lost+found", 169 | } 170 | 171 | def folder_eligibility_check(folder_path: str) -> bool: 172 | """ 173 | Assumed the folder paths were acquired with glob, hence they have a trailing folder separator. 174 | """ 175 | if not os.path.isdir(folder_path): 176 | return False 177 | folder_name = os.path.basename(os.path.dirname(folder_path)) 178 | if folder_name in EXCLUDE_DIRS: 179 | return False 180 | if folder_name != ".raidionics" and folder_name.startswith('.'): 181 | return False 182 | return True --------------------------------------------------------------------------------