├── .gitattributes
├── .github
├── pull_request_template.md
└── workflows
│ ├── README.md
│ ├── autoyapf.yml
│ ├── conda_build_and_publish.yml
│ └── docker_build_test_publish.yml
├── .gitignore
├── .style.yapf
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Documentation
├── documentation.md
├── pics
│ ├── ROI.png
│ ├── line.png
│ ├── sliceXPick.png
│ ├── viewer.pptx
│ ├── windowLevel.png
│ └── zoom.png
├── readme-images
│ ├── StandaloneViewerEgg.PNG
│ ├── WebCILViewer2D.PNG
│ └── WebCILViewer3D.PNG
├── singlewidgetviewer
│ └── singlewidgetviewer.md
└── triwidgetviewer
│ ├── images
│ ├── anchor.png
│ ├── broken_link.png
│ ├── link.png
│ ├── show.png
│ └── tree_icon.png
│ └── triWidgetViewer.md
├── LICENSE
├── README.md
├── Wrappers
├── Python
│ ├── ccpi
│ │ ├── __init__.py
│ │ ├── viewer
│ │ │ ├── CILViewer.py
│ │ │ ├── CILViewer2D.py
│ │ │ ├── CILViewerBase.py
│ │ │ ├── QCILRenderWindowInteractor.py
│ │ │ ├── QCILViewerWidget.py
│ │ │ ├── __init__.py
│ │ │ ├── cli
│ │ │ │ └── resample.py
│ │ │ ├── icons
│ │ │ │ ├── anchor.png
│ │ │ │ ├── broken_link.png
│ │ │ │ ├── link.png
│ │ │ │ ├── plus.png
│ │ │ │ ├── show.png
│ │ │ │ ├── sync.png
│ │ │ │ └── tree_icon.png
│ │ │ ├── iviewer.py
│ │ │ ├── standalone_viewer.py
│ │ │ ├── ui
│ │ │ │ ├── __init__.py
│ │ │ │ ├── dialogs.py
│ │ │ │ ├── main_windows.py
│ │ │ │ └── qt_widgets.py
│ │ │ ├── undirected_graph.py
│ │ │ ├── utils
│ │ │ │ ├── CameraData.py
│ │ │ │ ├── __init__.py
│ │ │ │ ├── colormaps.py
│ │ │ │ ├── conversion.py
│ │ │ │ ├── error_handling.py
│ │ │ │ ├── example_data.py
│ │ │ │ ├── hdf5_io.py
│ │ │ │ ├── io.py
│ │ │ │ └── visualisation_pipeline.py
│ │ │ ├── viewerLinker.py
│ │ │ └── widgets
│ │ │ │ ├── __init__.py
│ │ │ │ ├── box_widgets.py
│ │ │ │ └── slider.py
│ │ └── web_viewer
│ │ │ ├── README.md
│ │ │ ├── __init__.py
│ │ │ ├── dev-environment.yml
│ │ │ ├── test
│ │ │ ├── __init__.py
│ │ │ ├── test_trameviewer.py
│ │ │ ├── test_trameviewer2d.py
│ │ │ ├── test_trameviewer3d.py
│ │ │ └── test_web_app.py
│ │ │ ├── trame_viewer.py
│ │ │ ├── trame_viewer2D.py
│ │ │ ├── trame_viewer3D.py
│ │ │ └── web_app.py
│ ├── conda-recipe
│ │ ├── bld.bat
│ │ ├── build.sh
│ │ ├── environment.yml
│ │ ├── meta.yaml
│ │ └── ui_env.yml
│ ├── examples
│ │ ├── 2Dimage_reading.py
│ │ ├── error_observer.py
│ │ ├── image_reader_and_writer.py
│ │ ├── read_and_resample.py
│ │ ├── rectilinearwipe.py
│ │ ├── ui_examples
│ │ │ ├── BoxWidgetAroundSlice.py
│ │ │ ├── FourDockableLinkedViewerWidgets.py
│ │ │ ├── SingleViewerCentralWidget.py
│ │ │ ├── SingleViewerCentralWidget2.py
│ │ │ ├── TwoLinkedViewersCentralWidget.py
│ │ │ └── opacity_in_viewer.py
│ │ ├── viewer2D-without-qt.py
│ │ └── viewer3D-without-qt.py
│ ├── setup.py
│ └── test
│ │ ├── __init__.py
│ │ ├── test_CILViewer3D.py
│ │ ├── test_CILViewerBase.py
│ │ ├── test_camera_data.py
│ │ ├── test_cli_resample.py
│ │ ├── test_conversion.py
│ │ ├── test_cropped_readers.py
│ │ ├── test_example_data.py
│ │ ├── test_hdf5.py
│ │ ├── test_io.py
│ │ ├── test_resample_readers.py
│ │ ├── test_ui_dialogs.py
│ │ ├── test_version.py
│ │ ├── test_viewer_main_windows.py
│ │ └── test_vtk_image_resampler.py
└── javascript
│ └── test_vtk.html
└── docker
└── web-app
├── Dockerfile
├── README.md
├── clone-and-install.sh
├── create-data-folder.sh
├── entrypoint.sh
├── environment.yml
└── install-mambaforge-and-env.sh
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Prevent windows from converting line endings:
2 | *.sh text eol=lf
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Describe your changes
2 |
3 |
4 | ## Describe any testing you have performed
5 | *Consider adding example code to [examples](https://github.com/vais-ral/CILViewer/tree/pr-template/Wrappers/Python/examples)*
6 |
7 |
8 | ## Link relevant issues
9 |
10 |
11 | ## Checklist when you are ready to request a review
12 |
13 | - [ ] I have performed a self-review of my code
14 | - [ ] I have added docstrings in line with the guidance in the [CIL developer guide](https://tomographicimaging.github.io/CIL/nightly/developer_guide.html)
15 | - [ ] I have implemented unit tests that cover any new or modified functionality
16 | - [ ] CHANGELOG.md has been updated with any functionality change
17 | - [ ] Request review from all relevant developers
18 | - [ ] Change pull request label to 'waiting for review'
19 |
20 | ## Contribution Notes
21 | - [ ] The content of this Pull Request (the Contribution) is intentionally submitted for inclusion in CILViewer (the Work) under the terms and conditions of the [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html)
22 | - [ ] I confirm that the contribution does not violate any intellectual property rights of third parties
23 |
24 | Qt contributions should follow Qt naming conventions i.e. camelCase method names.
25 |
26 | VTK contributions should follow VTK naming conventions i.e. PascalCase method names.
27 |
--------------------------------------------------------------------------------
/.github/workflows/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Actions
2 |
3 | ## Building the Conda Package: [conda_build_and_publish](https://github.com/vais-ral/CILViewer/blob/master/.github/workflows/conda_build_and_publish.yml)
4 | This github action builds and tests the conda package, by using the [conda-package-publish-action](https://github.com/paskino/conda-package-publish-action)
5 |
6 | When making an [annotated](https://git-scm.com/book/en/v2/Git-Basics-Tagging) tag, the code is built, tested and published to the [ccpi conda channel for ccpi-viewer](https://anaconda.org/ccpi/ccpi-viewer/files) as noarch package, as pure python. Previously we provided packages for linux, windows and macOS versions.
7 |
8 | When pushing to master, or opening or modifying a pull request to master, a single variant is built and tested, but not published. This variant is `python=3.7` and `numpy=1.18`. Notice that we removed the variants from the recipe as we do not have a strong dependency on numpy or python.
9 |
10 | ## Auto-yapf: [autoyapf](https://github.com/vais-ral/CILViewer/blob/master/.github/workflows/autoyapf.yml)
11 | This action runs yapf auto-formatting on the whole repository, it runs on both push to master, and push to pull requests. Once it has ran yapf, it will commit to the branch it has checked out for the action, so on master, and the pull request branch when running on a pull request.
12 |
13 | It works by using mritunjaysharma394's autoyapf github action, to perform the formatting then manually committing to the branch that is checked out, but only after checking if there are any changes to files in the git tree.
14 |
--------------------------------------------------------------------------------
/.github/workflows/autoyapf.yml:
--------------------------------------------------------------------------------
1 | name: Yapf-ify this PR
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - 'main'
7 | - 'master'
8 | push:
9 | branches:
10 | - 'main'
11 | - 'master'
12 | - 'releases/**'
13 |
14 | jobs:
15 | autoyapf:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - if: github.event_name != 'pull_request'
19 | uses: actions/checkout@v3
20 | - if: github.event_name == 'pull_request'
21 | uses: actions/checkout@v3
22 | with:
23 | repository: ${{ github.event.pull_request.head.repo.full_name }}
24 | ref: ${{ github.event.pull_request.head.ref }}
25 |
26 | - name: autoyapf
27 | id: autoyapf
28 | uses: mritunjaysharma394/autoyapf@v2
29 | with:
30 | args: --parallel --recursive --in-place .
31 |
32 | - name: Check for modified files
33 | id: git-check
34 | run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi)
35 |
36 | - name: Push changes
37 | if: steps.git-check.outputs.modified == 'true'
38 | continue-on-error: true
39 | run: |
40 | git config --global user.name 'github-actions'
41 | git config --global user.email 'github-actions@github.com'
42 | git commit -am "Automated autoyapf fixes"
43 | git push
--------------------------------------------------------------------------------
/.github/workflows/conda_build_and_publish.yml:
--------------------------------------------------------------------------------
1 | name: publish_conda
2 |
3 | on:
4 | release:
5 | types: [published]
6 | push:
7 | branches: [ master ]
8 | tags:
9 | - '**'
10 | pull_request:
11 | branches: [ master ]
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-22.04
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0 # All history for use later with the meta.yaml file for conda-recipes
20 | - name: publish-to-conda
21 | uses: TomographicImaging/conda-package-publish-action@v2
22 | with:
23 | subDir: 'Wrappers/Python/conda-recipe'
24 | channels: '-c conda-forge -c ccpi'
25 | AnacondaToken: ${{ secrets.ANACONDA_TOKEN }}
26 | publish: ${{ github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') }}
27 | test_all: ${{(github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || (github.ref == 'refs/heads/master')}}
28 | additional_apt_packages: 'libegl1-mesa libegl1-mesa-dev'
29 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/docker_build_test_publish.yml:
--------------------------------------------------------------------------------
1 | name: Docker build test publish
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-22.04
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - name: Install docker and pre-reqs
14 | shell: bash -l {0}
15 | run: |
16 | sudo apt-get update -y
17 | sudo apt-get upgrade -y
18 | sudo apt-get install ca-certificates curl gnupg lsb-release -y
19 | sudo mkdir -p /etc/apt/keyrings
20 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
21 | echo \
22 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
23 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
24 | sudo apt-get update
25 | sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
26 | sudo docker run hello-world
27 |
28 | - name: Build docker container
29 | shell: bash -l {0}
30 | run: |
31 | docker build -t cil-viewer docker/web-app
32 |
33 | - name: Run docker container with tests
34 | shell: bash -l {0}
35 | run: |
36 | docker run --rm --entrypoint /bin/bash -v /home/runner/work/CILViewer/CILViewer:/root/source_code cil-viewer -c "source ./mambaforge/etc/profile.d/conda.sh && conda activate cilviewer_webapp && conda install cil-data pytest pyside2 eqt>=1.0.0 -c ccpi && python -m pytest /root/source_code/Wrappers/Python -k 'not test_version and not test_cli_resample and not test_CILViewerBase and not test_CILViewer3D and not test_viewer_main_windows and not test_ui_dialogs'"
37 | # TODO: publish to come later
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ccpi_viewer.egg-info
2 | *.pyc
3 | __pycache__
4 | *.log
5 |
6 | Wrappers/Python/ccpi/viewer/version.py
7 | Wrappers/Python/ccpi/web_viewer/data/*
8 |
9 | # Hide PyCharm or other Jetbrains IDE files
10 | .idea
11 |
12 | Wrappers/Python/build/*
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.style.yapf:
--------------------------------------------------------------------------------
1 | [style]
2 | based_on_style = pep8
3 | column_limit = 120
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Developer Contribution Guide
2 | Contribute to the repository by opening a pull request.
3 |
4 | ## Local
5 | Clone the source code.
6 | ```sh
7 | git clone git@github.com:TomographicImaging/CILViewer.git
8 | ```
9 | Navigate the folder.
10 | ```sh
11 | cd CILViewer
12 | ```
13 |
14 | Create a new environment.
15 | ```sh
16 | conda env create –f Wrappers/Python/conda-recipe/environment.yml
17 | ```
18 | Activate the environment.
19 | ```sh
20 | conda activate cilviewer
21 | ```
22 | Install the package.
23 | ```sh
24 | pip install ./Wrappers/Python --no-dependencies
25 | ```
26 |
27 | ### Run tests
28 | Before merging a pull request, all tests must pass.
29 | Install the required packages:
30 | ```sh
31 | conda install eqt pillow pyside2 pytest -c ccpi cil-data=22.0.0
32 | ```
33 | Tests can be run locally from the repository folder
34 | ```sh
35 | python -m pytest Wrappers/Python/test
36 | ```
37 |
38 | ## Continuous integration
39 |
40 | ### Changelog
41 | Located in [CHANGELOG.md](./CHANGELOG.md).
42 |
43 | ##### Changelog style
44 | The changelog file needs to be updated manually every time a pull request (PR) is submitted.
45 | - Itemise the message with "-".
46 | - Be concise by explaining the overall changes in only a few words.
47 | - Mention the relevant PR.
48 |
49 | ###### Example:
50 | - Add CONTRIBUTING.md #403
51 |
--------------------------------------------------------------------------------
/Documentation/documentation.md:
--------------------------------------------------------------------------------
1 | # CILViewer Documentation
2 |
3 | ## Using the 2D and 3D Viewers keyboard interactors
4 |
5 | ### **2D Viewer keybindings**
6 | The interactive viewer CILViewer2D provides:
7 | - Keyboard Interactions:
8 | - 'h' display the help
9 | - 'x' slices on the YZ plane
10 | - 'y' slices on the XZ plane
11 | - 'z' slices on the XY
12 | - 'a' auto window/level to accomodate all values
13 | - 's' save render to PNG (current_render.png)
14 | - 'l' plots horizontal and vertical profiles of the displayed image at the pointer location
15 | - 'i' toggles interpolation
16 | - slice up/down: mouse scroll (10 x pressing SHIFT)
17 | - Window/Level: ALT + Right Mouse Button + drag
18 | - Pan: CTRL + Right Mouse Button + drag
19 | - Zoom: SHIFT + Right Mouse Button + drag (up: zoom in, down: zoom out)
20 | - Pick: Left Mouse Click
21 | - ROI (square):
22 | - Create ROI: CTRL + Left Mouse Button
23 | - Resize ROI: Left Mouse Button on outline + drag
24 | - Translate ROI: Middle Mouse Button within ROI
25 | - Delete ROI: ALT + Left Mouse Button
26 |
27 | ### Demonstration on head dataset[1]
28 |
29 | | 2D viewer | Zoom | Slice X + Pick |
30 | |----- |--- |--- |
31 | ||||
32 |
33 | | ROI | Line profiles |
34 | |--- |--- |
35 | |||
36 |
37 | ### **3D Viewer keybindings**
38 | The interactive 3D viewer CILViewer provides:
39 | - Keyboard Interactions:
40 | - 'h' display the help
41 | - 'x' slices on the YZ plane
42 | - 'y' slices on the XZ plane
43 | - 'z' slices on the XY
44 | - 'r' save render to current_render.png
45 | - 's' toggle visibility of slice
46 | - 'v' toggle visibility of volume render
47 | - 'c' activates volume render clipping plane widget, for slicing through a volume.
48 | - 'a' whole image Auto Window/Level on the slice.
49 | - 'i' interpolation of the slice.
50 | - Slice: Mouse Scroll
51 | - Zoom: Right Mouse + Move Up/Down
52 | - Pan: Middle Mouse Button + Move or Shift + Left Mouse + Move
53 | - Adjust Camera: Left Mouse + Move
54 | - Rotate: Ctrl + Left Mouse + Move
55 |
56 | ## References
57 |
58 | [1] The head dataset is avaiable in [CIL-Data as 'head.mha'](https://github.com/TomographicImaging/CIL-Data) along with its license.
--------------------------------------------------------------------------------
/Documentation/pics/ROI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/ROI.png
--------------------------------------------------------------------------------
/Documentation/pics/line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/line.png
--------------------------------------------------------------------------------
/Documentation/pics/sliceXPick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/sliceXPick.png
--------------------------------------------------------------------------------
/Documentation/pics/viewer.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/viewer.pptx
--------------------------------------------------------------------------------
/Documentation/pics/windowLevel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/windowLevel.png
--------------------------------------------------------------------------------
/Documentation/pics/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/pics/zoom.png
--------------------------------------------------------------------------------
/Documentation/readme-images/StandaloneViewerEgg.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/readme-images/StandaloneViewerEgg.PNG
--------------------------------------------------------------------------------
/Documentation/readme-images/WebCILViewer2D.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/readme-images/WebCILViewer2D.PNG
--------------------------------------------------------------------------------
/Documentation/readme-images/WebCILViewer3D.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/readme-images/WebCILViewer3D.PNG
--------------------------------------------------------------------------------
/Documentation/singlewidgetviewer/singlewidgetviewer.md:
--------------------------------------------------------------------------------
1 | # 2D Slice Viewer
2 |
3 | The 2D slice viewer was designed to quickly visualise and interact with large 3D datasets. There are a number of interactions
4 | which will allow the user to slice through the image on 3 axes, plot a histogram and profile horizontal and vertical cross sections of the image.
5 |
6 | ## Opening a file
7 | Click the folder icon in the toolbar and select the file to load. This application can load single files or multiple files in tiff format.
8 |
9 | ## Saving the current image in the 2D slicer
10 | Click the save icon in the toolbar and pick a destination.
11 |
12 | ## Interactions
13 | Mouse and keyboard interaction for the 2D Slicer.
14 |
15 | ### 2D Slicer
16 | #### Keyboard Interactions
17 | * x - Slices on the YZ plane
18 | * y - Slices on the XZ plane
19 | * z - Slices on the XY plane
20 | * a - Auto window/level to accomodate all values
21 | * l - Plots horizontal and vertical profiles of the displayed image at the pointer location
22 |
23 | #### Mouse Interactions
24 | * Slice Up/Down - Scroll (x10 speed pressing SHIFT)
25 | * Window/Level - ALT + Right Mouse Button + Drag
26 | * Pan - CTRL + Right Mouse Button + Drag
27 | * Zoom - SHIFT + Right Mouse Buttin + Drag (up: zoom in, down: zoom out)
28 | * Pick - Left Mouse Click
29 | * Region of interest
30 | * Create ROI - CTRL + Left Mouse Button
31 | * Resize ROI - Click + Drag nodes
32 | * Translate ROI - Middle Mouse Button within ROI + Drag
33 | * Delete ROI - ALT + Left Mouse Button
34 |
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/images/anchor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/triwidgetviewer/images/anchor.png
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/images/broken_link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/triwidgetviewer/images/broken_link.png
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/images/link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/triwidgetviewer/images/link.png
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/images/show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/triwidgetviewer/images/show.png
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/images/tree_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Documentation/triwidgetviewer/images/tree_icon.png
--------------------------------------------------------------------------------
/Documentation/triwidgetviewer/triWidgetViewer.md:
--------------------------------------------------------------------------------
1 | # Tri Widget Viewer
2 |
3 | The tri widget viewer was designed to display 3 different views of the data simultaneously.
4 | The main element of the viewer is a 2D slicer. Two sub-windows display a 3D slicer and graph.
5 | Generating the graph will also generate surfaces which are rendered in the 3D slicer.
6 | Interaction with the 2D and 3D slicers can be linked or unlinked depending on user preference.
7 |
8 | ## Opening a file
9 | Click the folder icon in the toolbar and select the file to load. This application can load single files or multiple files in tiff format.
10 |
11 | ## Saving the current image in the 2D slicer
12 | Click the save icon in the toolbar and pick a destination.
13 |
14 | ## Interactions
15 | Mouse and keyboard interaction differ for the different viewers. The 2D Slicer has the most interactions available.
16 |
17 | ### 2D Slicer
18 | #### Keyboard Interactions
19 | * x - Slices on the YZ plane
20 | * y - Slices on the XZ plane
21 | * z - Slices on the XY plane
22 | * a - Auto window/level to accomodate all values
23 | * l - Plots horizontal and vertical profiles of the displayed image at the pointer location
24 |
25 | #### Mouse Interactions
26 | * Slice Up/Down - Scroll (x10 speed pressing SHIFT)
27 | * Window/Level - ALT + Right Mouse Button + Drag
28 | * Pan - CTRL + Right Mouse Button + Drag
29 | * Zoom - SHIFT + Right Mouse Buttin + Drag (up: zoom in, down: zoom out)
30 | * Pick - Left Mouse Click
31 | * Region of interest
32 | * Create ROI - CTRL + Left Mouse Button
33 | * Resize ROI - Click + Drag nodes
34 | * Translate ROI - Middle Mouse Button within ROI + Drag
35 | * Delete ROI - ALT + Left Mouse Button
36 |
37 | ### 3D Slicer
38 | #### Keyboard Interactions
39 | * x - Slices on the YZ plane
40 | * y - Slices on the XZ plane
41 | * z - Slices on the XY plane
42 |
43 | #### Mouse Interactions
44 | * Adjust Camera - Left Mouse Button + Drag
45 | * Pan - SHIFT + Left Mouse Button + Drag
46 | * Rotate - CTRL + Left Mouse Button + Drag
47 | * Zoom - Right Mouse Button + Drag (up: zoom in, down: zoom out)
48 |
49 |
50 | ### Graph
51 | #### Mouse Interaction
52 | * Zoom - Scroll
53 | * Pan - ALT + Left Mouse Button + Drag
54 | * Select Region - Left Mouse Button + Drag
55 | * Pick - Left Mouse Button
56 |
57 |
58 | ## Generating the graph
59 | The graph can be generated in the graph tools panel. This is opened using the graph icon. ![alt text][graph icon]
60 |
61 | Click "Generate Graph" to generate the graph and 3D surfaces. One the graph has been generated, modify the parameters and click "Update" to push the changes to the viewers.
62 |
63 | ### Parameters
64 |
65 | * Iso Value - As a percentage of the pixel value range, what iso value to calculate the tree and surfaces from.
66 | * Global Iso - If unchecked, will use local iso-value
67 | * Colour Surfaces - Colour individual elements of the rendered object
68 | * Log Tree Size -
69 | * Collapse Priority -
70 |
71 | ## Linking the viewers
72 |
73 | Interactions on the 2D and 3D viewers can be linked.
74 | The status of the link is shown by the icon on the button in the top left of the 2D and 3D viewers. Toggling the state is achieved by clicking this button.
75 | When the icon is a closed link ![alt text][linked icon] interactions will be passed from one viewer to the other.
76 | When the icon is an open link ![alt text][unlinked icon] interactions will not be passed between the viewers.
77 |
78 | If slice actions are performed while the viewers are unlinked, they will become out of sync. If this is undesireable, scroll until both viewers reach the extent
79 | of the current slicing direction and then the slice will be syncronised again.
80 |
81 | If window/level actions are performed while the viewer are unlinked, they will become out of sync. If this is undesireable, link and perform a window/level change
82 | action. The levels will synchronise again.
83 |
84 | #### Interactions available from both viewers
85 | These actions will be triggered by performing the action in either the 2D or 3D viewer.
86 |
87 | * Slice - Change the slice
88 | * x - Slice in the YZ plane
89 | * y - Slice in te XZ plane
90 | * z - slice in the XY plane
91 |
92 | #### Interactions only available in 2D viewer
93 | These actions will only be triggered by performing the action in the 2D viewer.
94 |
95 | * Window/Level - Adjust the window/level for the image
96 |
97 | ## Docking/Showing the Graph and 3D slicer
98 |
99 | The 3D slicer and graph widgets can be removed from the main window and resized in order to get a better view of certain elements.
100 | If you have closed either of the windows, they can be brought back by clicking the show icon ![alt text][show icon]
101 |
102 | [graph icon]: images/tree_icon.png "This icon opens the graph tool panel"
103 | [linked icon]: images/link.png "This icon shows the viewers are linked"
104 | [unlinked icon]: images/broken_link.png "This icon shows the viewers are un-linked"
105 | [anchor icon]: images/anchor.png "This icon re-docks floating widgets and shows closed widgets"
106 | [show icon]: ./images.show.png "This icon brings closed windows back"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | | Master | Development | Anaconda binaries |
2 | |--------|-------------|-------------------|
3 | | [](https://anvil.softeng-support.ac.uk/jenkins/job/CILsingle/job/CCPi-Viewer/) | [](https://anvil.softeng-support.ac.uk/jenkins/job/CILsingle/job/CCPi-Viewer-dev/) |  [ ](https://anaconda.org/ccpi/ccpi-viewer) |
4 |
5 | # CILViewer
6 | A simple interactive viewer based on VTK classes and written in Python.
7 | - The classes in [`viewer`](Wrappers/Python/ccpi/viewer/) define generic viewers that can be embedded in [Qt](https://www.qt.io/) or other user interfaces. [`CILviewer2D`](Wrappers/Python/ccpi/viewer/CILViewer2D.py) is a 2D viewer and [`CILviewer`](Wrappers/Python/ccpi/viewer/CILViewer.py) is a 3D viewer.
8 | - The classes in [`web viewer`](Wrappers/Python/ccpi/web_viewer/) define a viewer embedded in [trame](https://kitware.github.io/trame/).
9 |
10 | Examples of QApplications are the [`iviewer`](Wrappers/Python/ccpi/viewer/iviewer.py) and the [`standalone viewer`](Wrappers/Python/ccpi/viewer/standalone_viewer.py). An example of use in an external software is [iDVC](https://github.com/TomographicImaging/iDVC).
11 |
12 | ## Installation instructions
13 | To install via `conda`, create a minimal environment using:
14 |
15 | ```bash
16 | conda create --name cilviewer ccpi-viewer=24.0.1 -c ccpi -c conda-forge
17 | ```
18 | ### UI requirements
19 | To use the extra [UI utilities](Wrappers/Python/ccpi/viewer/ui) the environment needs to be updated to include the extra requirements `eqt` and `pyside2`. The [UI examples](Wrappers/Python/examples/ui_examples) require `cil-data` as well. The environment can be updated to include these dependencies as follows:
20 | ```sh
21 | conda env update --name cilviewer --file Wrappers/Python/conda-recipe/ui_env.yml
22 | ```
23 |
24 | ## Run the standalone viewer QApplication
25 |
26 | - Activate your environment using: ``conda activate cilviewer``.
27 | - Launch by typing: `cilviewer`
28 | - Load a dataset using the File menu. Currently supported data formats:
29 | - HDF5, including Nexus
30 | - Raw
31 | - Tiff
32 | - Numpy
33 | - Metaimage (mha and mhd)
34 |
35 |
36 |
37 | Data shown is [1].
38 |
39 | ## Install and run the trame web viewer
40 | Follow the [instructions](https://github.com/vais-ral/CILViewer/tree/master/Wrappers/Python/ccpi/web_viewer) to install and run the web viewer.
41 |
42 |
43 |
44 | Data shown is [2].
45 |
46 | ## Documentation
47 | More information on how to use the viewer can be found in [Documentation.md](./Documentation/documentation.md).
48 |
49 | ## Developer Contribution Guide
50 | We welcome contributions. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidance.
51 |
52 | ## Notice
53 | The CIL Viewer code took initial inspiration from a previous project of Edoardo Pasca and Lukas Batteau [PyVE](https://sourceforge.net/p/pyve/code/ci/master/tree/PyVE/), the license of which we report here:
54 |
55 | ```
56 | Copyright (c) 2012, Edoardo Pasca and Lukas Batteau
57 | All rights reserved.
58 |
59 | Redistribution and use in source and binary forms, with or without modification,
60 | are permitted provided that the following conditions are met:
61 |
62 | * Redistributions of source code must retain the above copyright notice, this list
63 | of conditions and the following disclaimer.
64 |
65 | * Redistributions in binary form must reproduce the above copyright notice, this
66 | list of conditions and the following disclaimer in the documentation and/or other
67 | materials provided with the distribution.
68 |
69 | * Neither the name of Edoardo Pasca or Lukas Batteau nor the names of any
70 | contributors may be used to endorse or promote products derived from this
71 | software without specific prior written permission.
72 |
73 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
74 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
75 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
76 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
77 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
78 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
79 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
80 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
81 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
82 | ```
83 |
84 | ## References
85 | [1] The chocolate egg dataset shown in above examples is dataset `egg2`:
86 |
87 | Jakob Sauer Jørgensen, Martin Skovgaard Andersen, & Carsten Gundlach. (2021). HDTomo TXRM micro-CT datasets [Data set]. Zenodo. https://doi.org/10.5281/zenodo.4822516
88 |
89 | [2] The head dataset is avaiable in [CIL-Data as 'head.mha'](https://github.com/TomographicImaging/CIL-Data) along with its license.
90 |
91 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/__init__.py
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/QCILRenderWindowInteractor.py:
--------------------------------------------------------------------------------
1 | import vtk
2 | from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
3 | from PySide2.QtCore import Qt, QEvent
4 |
5 |
6 | class QCILRenderWindowInteractor(QVTKRenderWindowInteractor):
7 | '''
8 | A QVTKRenderWindowInteractor for Python and Qt. Uses a vtkGenericRenderWindowInteractor to handle the interactions.
9 | Use GetRenderWindow() to get the vtkRenderWindow. Create with the keyword stereo=1 in order to generate
10 | a stereo-capable window. Extends the QVTKRenderWindowInteractor to accept also ALT modifier.
11 |
12 | More info: https://docs.vtk.org/en/latest/api/python/vtkmodules/vtkmodules.qt.QVTKRenderWindowInteractor.html
13 | '''
14 |
15 | def __init__(self, parent=None, **kw):
16 | '''Constructor'''
17 | super(QCILRenderWindowInteractor, self).__init__(parent, **kw)
18 | self.__saveModifiers = self._QVTKRenderWindowInteractor__saveModifiers
19 | self.__saveX = self._QVTKRenderWindowInteractor__saveX
20 | self.__saveY = self._QVTKRenderWindowInteractor__saveY
21 | self.__saveButtons = self._QVTKRenderWindowInteractor__saveButtons
22 | self.__wheelDelta = self._QVTKRenderWindowInteractor__wheelDelta
23 | #print ("__saveModifiers should be defined", self.__saveModifiers)
24 |
25 | def _GetCtrlShiftAlt(self, ev):
26 | '''Get CTRL SHIFT ALT key modifiers'''
27 | ctrl = shift = alt = False
28 |
29 | if hasattr(ev, 'modifiers'):
30 |
31 | if ev.modifiers() & Qt.ShiftModifier:
32 | shift = True
33 | if ev.modifiers() & Qt.ControlModifier:
34 | ctrl = True
35 | if ev.modifiers() & Qt.AltModifier:
36 | alt = True
37 | else:
38 | if self.__saveModifiers & Qt.ShiftModifier:
39 | shift = True
40 | if self.__saveModifiers & Qt.ControlModifier:
41 | ctrl = True
42 | if self.__saveModifiers & Qt.AltModifier:
43 | alt = True
44 |
45 | return ctrl, shift, alt
46 |
47 | def enterEvent(self, ev):
48 | '''Overload of enterEvent from base class to use _GetCtrlShiftAlt'''
49 | ctrl, shift, alt = self._GetCtrlShiftAlt(ev)
50 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None)
51 | self._Iren.EnterEvent()
52 |
53 | def leaveEvent(self, ev):
54 | '''Overload of leaveEvent from base class to use _GetCtrlShiftAlt'''
55 | ctrl, shift, alt = self._GetCtrlShiftAlt(ev)
56 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, ctrl, shift, chr(0), 0, None)
57 | self._Iren.LeaveEvent()
58 |
59 | def mousePressEvent(self, ev):
60 | '''Overload of mousePressEvent from base class to use _GetCtrlShiftAlt'''
61 | ctrl, shift, alt = self._GetCtrlShiftAlt(ev)
62 | repeat = 0
63 | if ev.type() == QEvent.MouseButtonDblClick:
64 | repeat = 1
65 | self._Iren.SetEventInformationFlipY(ev.x(), ev.y(), ctrl, shift, chr(0), repeat, None)
66 | self._Iren.SetAltKey(alt)
67 | self._Iren.SetShiftKey(shift)
68 | self._Iren.SetControlKey(ctrl)
69 |
70 | self._ActiveButton = ev.button()
71 |
72 | if self._ActiveButton == Qt.LeftButton:
73 | self._Iren.LeftButtonPressEvent()
74 | elif self._ActiveButton == Qt.RightButton:
75 | self._Iren.RightButtonPressEvent()
76 | elif self._ActiveButton == Qt.MidButton:
77 | self._Iren.MiddleButtonPressEvent()
78 |
79 | def mouseMoveEvent(self, ev):
80 | '''Overload of mouseMoveEvent from base class to use _GetCtrlShiftAlt'''
81 | self.__saveModifiers = ev.modifiers()
82 | self.__saveButtons = ev.buttons()
83 | self.__saveX = ev.x()
84 | self.__saveY = ev.y()
85 |
86 | #ctrl, shift = self._GetCtrlShift(ev)
87 | ctrl, shift, alt = self._GetCtrlShiftAlt(ev)
88 | self._Iren.SetEventInformationFlipY(ev.x(), ev.y(), ctrl, shift, chr(0), 0, None)
89 | self._Iren.SetAltKey(alt)
90 | self._Iren.MouseMoveEvent()
91 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/QCILViewerWidget.py:
--------------------------------------------------------------------------------
1 | import vtk
2 | import sys
3 | import vtk
4 | from PySide2 import QtCore, QtWidgets
5 | from ccpi.viewer.QCILRenderWindowInteractor import QCILRenderWindowInteractor
6 | from ccpi.viewer import viewer2D, viewer3D
7 |
8 |
9 | class QCILViewerWidget(QtWidgets.QFrame):
10 | '''A QFrame to embed in Qt application containing a VTK Render Window
11 |
12 | All the interaction is passed from Qt to VTK.
13 |
14 | :param viewer: The viewer you want to embed in Qt: CILViewer2D or CILViewer
15 | :param interactorStyle: The interactor style for the Viewer.
16 | '''
17 |
18 | def __init__(self,
19 | parent,
20 | viewer,
21 | shape=(600, 600),
22 | debug=False,
23 | renderer=None,
24 | interactorStyle=None,
25 | enableSliderWidget=True):
26 | '''Creator. Creates an instance of a QFrame and of a CILViewer
27 |
28 | The viewer is placed in the QFrame inside a QVBoxLayout.
29 | The viewer is accessible as member 'viewer'
30 | '''
31 |
32 | super(QCILViewerWidget, self).__init__(parent=parent)
33 | # currently the size of the frame is set by stretching to the whole
34 | # area in the main window. A resize of the MainWindow triggers a resize of
35 | # the QFrame to occupy the whole area available.
36 |
37 | dimx, dimy = shape
38 | # self.resize(dimx, dimy)
39 |
40 | self.vtkWidget = QCILRenderWindowInteractor(self)
41 |
42 | if renderer is None:
43 | self.ren = vtk.vtkRenderer()
44 | else:
45 | self.ren = renderer
46 |
47 | self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
48 | # https://discourse.vtk.org/t/qvtkwidget-render-window-is-outside-main-qt-app-window/1539/8?u=edoardo_pasca
49 | self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
50 |
51 | if viewer is viewer2D:
52 | self.viewer = viewer(dimx=dimx,
53 | dimy=dimy,
54 | ren=self.ren,
55 | renWin=self.vtkWidget.GetRenderWindow(),
56 | iren=self.iren,
57 | debug=debug,
58 | enableSliderWidget=enableSliderWidget)
59 | al = self.viewer.axisLabelsText
60 | self.viewer.setAxisLabels([al[0], al[1], ''], False)
61 | elif viewer is viewer3D:
62 | self.viewer = viewer(dimx=dimx,
63 | dimy=dimy,
64 | ren=self.ren,
65 | renWin=self.vtkWidget.GetRenderWindow(),
66 | iren=self.iren,
67 | debug=debug)
68 | else:
69 | raise KeyError("Viewer class not provided. Submit an uninstantiated viewer class object"
70 | "using 'viewer' keyword")
71 |
72 | if interactorStyle is not None:
73 | self.viewer.style = interactorStyle(self.viewer)
74 | self.viewer.iren.SetInteractorStyle(self.viewer.style)
75 |
76 | self.vl = QtWidgets.QVBoxLayout()
77 | self.vl.addWidget(self.vtkWidget)
78 | self.setLayout(self.vl)
79 | self.adjustSize()
80 |
81 |
82 | class QCILDockableWidget(QtWidgets.QDockWidget):
83 | '''Inserts a vtk viewer in a dock widget.'''
84 |
85 | def __init__(self, parent=None, viewer=viewer2D, shape=(600, 600), interactorStyle=None, title=""):
86 | '''Creates an instance of a `QCILDockableWidget` and inserts it in a `QDockWidget`.
87 | Sets the title of the dock widget.'''
88 | super(QCILDockableWidget, self).__init__(parent)
89 |
90 | self.frame = QCILViewerWidget(parent, viewer, shape=shape, interactorStyle=interactorStyle)
91 | self.viewer = self.frame.viewer
92 |
93 | self.setWindowTitle(title)
94 |
95 | self.setWidget(self.frame)
96 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/__init__.py:
--------------------------------------------------------------------------------
1 | SLICE_ORIENTATION_XY = 2 # Z
2 | SLICE_ORIENTATION_XZ = 1 # Y
3 | SLICE_ORIENTATION_YZ = 0 # X
4 |
5 | CONTROL_KEY = 8
6 | SHIFT_KEY = 4
7 | ALT_KEY = -128
8 |
9 | SLICE_ACTOR = 'slice_actor'
10 | OVERLAY_ACTOR = 'overlay_actor'
11 | HISTOGRAM_ACTOR = 'histogram_actor'
12 | HELP_ACTOR = 'help_actor'
13 | CURSOR_ACTOR = 'cursor_actor'
14 | CROSSHAIR_ACTOR = 'crosshair_actor'
15 | LINEPLOT_ACTOR = 'lineplot_actor'
16 | WIPE_ACTOR = 'wipe_actor'
17 |
18 | from .CILViewer import CILViewer as viewer3D
19 | from .CILViewer2D import CILViewer2D as viewer2D
20 | from .CILViewer import CILInteractorStyle as istyle3D
21 | from .CILViewer2D import CILInteractorStyle as istyle2D
22 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/cli/resample.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser
2 |
3 | import yaml
4 | import schema
5 | from schema import SchemaError, Schema, Optional
6 |
7 | from ccpi.viewer.utils.io import ImageReader, ImageWriter
8 | '''
9 | This command line tool takes a dataset file and a yaml file as input.
10 | It resamples or crops the dataset as it reads it in, and then writes
11 | out the resulting dataset to a file.
12 |
13 | Supported file types for reading:
14 | hdf5, nxs, mha, raw, numpy
15 |
16 | Supported file types for writing:
17 | hdf5, nxs, mha
18 |
19 | '''
20 |
21 | EXAMPLE_YAML_FILE = '''
22 | input:
23 | file_name: '24737_fd_normalised.nxs'
24 | shape: (1024,1024,1024)
25 | is_fortran: False
26 | is_big_endian: True
27 | typecode: 'float32'
28 | dataset_name: '/entry1/tomo_entry/data/data' # valid for HDF5 and Zarr
29 | resample:
30 | target_size: 1
31 | resample_z: True
32 | output:
33 | file_name: 'this_fname.nxs'
34 | format: 'hdf5' # npy, METAImage, NIFTI (or Zarr to come)
35 | '''
36 | '''
37 |
38 | THIS IS WHAT THE OUTPUT STRUCTURE OF AN EXAMPLE OUTPUT HDF5 FILE WOULD LOOK LIKE:
39 | Entry 1 contains attributes of the original dataset
40 | Entry 2 contains the resampled dataset and attributes
41 |
42 | - entry1 :
43 | - tomo_entry :
44 | - data :
45 | - data :
46 | - bit_depth : 64
47 | - file_name : test_3D_data.npy
48 | - header_length : 128
49 | - is_big_endian : False
50 | - origin : [0. 0. 0.]
51 | - shape : [500 100 600]
52 | - spacing : [1 1 1]
53 | - resampled: False
54 | - cropped: False
55 | - entry2 :
56 | - tomo_entry :
57 | - data :
58 | - data :
59 | - cropped : False
60 | - origin : [2.23861279 2.23861279 0. ]
61 | - resample_z : True
62 | - resampled : True
63 | - spacing : [5.47722558 5.47722558 1. ]
64 | - original_dataset: /entry1/tomo_entry/data/data
65 | '''
66 |
67 | # This validates the input yaml file:
68 | schema = Schema({
69 | 'input': {
70 | 'file_name': str,
71 | Optional('shape'): tuple, # only for raw
72 | Optional('is_fortran'): bool, # only for raw
73 | Optional('is_big_endian'): bool, # only for raw
74 | Optional('typecode'): str, # only for raw
75 | Optional('dataset_name'): str
76 | }, # only for hdf5 # need to set default
77 | 'resample': {
78 | 'target_size': float,
79 | 'resample_z': bool
80 | },
81 | 'output': {
82 | 'file_name': str,
83 | 'format': str
84 | }
85 | })
86 |
87 |
88 | def parse_arguments():
89 | parser = ArgumentParser(prog='resampler',
90 | description='Resamples a dataset file & writes out to a file.' +
91 | ' Either specify a yaml file or set at minimum: -i, -o and -target_size')
92 |
93 | parser.add_argument(
94 | '-f',
95 | help='Input yaml file. May be used in place of all other arguments. If set, all other arguments are ignored.',
96 | type=str)
97 | parser.add_argument('--example', help='Prints an example input yaml file.', action='store_true')
98 |
99 | parser.add_argument('-i', help='Input dataset filename. Required if -f is not set.')
100 | parser.add_argument('--dataset_name',
101 | help='Dataset name, only required if input file is HDF5/Nexus format.',
102 | type=str,
103 | default='/entry1/tomo_entry/data/data')
104 | parser.add_argument('--shape',
105 | help='Shape of input dataset file, only required if input file is raw format.',
106 | type=lambda s: [int(item) for item in s.strip('[').strip(']').split(',')])
107 | parser.add_argument('--is_fortran',
108 | help='Whether input dataset file is fortran order, only required if input file is raw format.')
109 | parser.add_argument('--is_big_endian',
110 | help='Whether input dataset file is big endian, only required if input file is raw format.')
111 | parser.add_argument('--typecode',
112 | help='Typecode of input dataset, only required if input file is raw format.',
113 | type=str)
114 |
115 | parser.add_argument('-target_size',
116 | help='Target size to downsample dataset to, in MB. Required if -f is not set.',
117 | type=float)
118 | parser.add_argument('--resample_z',
119 | help='Whether to resample along the z axis of the dataset. Optional.',
120 | default=True)
121 |
122 | parser.add_argument('-o', help='Output filename. Required if -f is not set.')
123 | parser.add_argument('--out_format',
124 | help='File format to write downsampled data to',
125 | choices=['hdf5', 'nxs', 'mha'],
126 | type=str,
127 | default='nxs')
128 |
129 | args = parser.parse_args()
130 |
131 | return args
132 |
133 |
134 | def get_params_from_args(args):
135 |
136 | if args.f is not None:
137 |
138 | with open(args.f) as f:
139 | params_raw = yaml.safe_load(f)
140 | params = {}
141 | for key, dict in params_raw.items():
142 | # each of the values in data_raw is a dict
143 | params[key] = {}
144 | for sub_key, value in dict.items():
145 | try:
146 | params[key][sub_key] = eval(value)
147 | except:
148 | params[key][sub_key] = value
149 | try:
150 | schema.validate(params)
151 | except SchemaError as e:
152 | print(e)
153 |
154 | else:
155 | for a in [args.i, args.target_size, args.o]:
156 | if a is None:
157 | raise Exception("If yaml file is not set: -i, --target_size, and -o must be set")
158 |
159 | params = {
160 | 'input': {
161 | 'file_name': args.i
162 | },
163 | 'resample': {
164 | 'target_size': args.target_size
165 | },
166 | 'output': {
167 | 'file_name': args.o
168 | }
169 | }
170 |
171 | args_input = ['shape', 'typecode', 'dataset_name']
172 | bool_args_input = ['is_fortran', 'is_big_endian']
173 |
174 | for a in args_input:
175 | if eval(f"args.{a}") is not None:
176 | params['input'][a] = eval(f"args.{a}")
177 |
178 | for a in bool_args_input:
179 | if eval(f"args.{a}") is not None:
180 | params['input'][a] = eval(eval(f"args.{a}"))
181 |
182 | if args.resample_z is not None:
183 | params['resample']['resample_z'] = eval(args.resample_z)
184 |
185 | if args.out_format is not None:
186 | params['output']['format'] = args.out_format
187 |
188 | return params
189 |
190 |
191 | def main():
192 | args = parse_arguments()
193 | # Do nothing else if we are just printing an example yaml file:
194 | if args.example:
195 | print(EXAMPLE_YAML_FILE)
196 | return
197 | params = get_params_from_args(args)
198 |
199 | raw_attrs = None
200 | dataset_name = None
201 | if 'input' in params.keys():
202 | raw_attrs = {}
203 | for key, value in params['input'].items():
204 | if key == 'dataset_name':
205 | dataset_name = value
206 | elif key == 'file_name':
207 | pass
208 | else:
209 | raw_attrs[key] = value
210 |
211 | target_size = params['resample']['target_size']
212 | if target_size is not None:
213 | target_size *= 1e6
214 |
215 | reader = ImageReader(file_name=params['input']['file_name'],
216 | resample=True,
217 | target_size=target_size,
218 | resample_z=params['resample']['resample_z'],
219 | raw_image_attrs=raw_attrs,
220 | hdf5_dataset_name=dataset_name)
221 | downsampled_image = reader.Read()
222 | original_image_attrs = reader.GetOriginalImageAttrs()
223 | loaded_image_attrs = reader.GetLoadedImageAttrs()
224 |
225 | writer = ImageWriter()
226 | writer.SetFileName(params['output']['file_name'])
227 | writer.SetFileFormat(params['output']['format'])
228 | writer.SetOriginalDataset(None, original_image_attrs)
229 | writer.AddChildDataset(downsampled_image, loaded_image_attrs)
230 | writer.Write()
231 |
232 |
233 | if __name__ == '__main__':
234 | main()
235 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/anchor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/anchor.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/broken_link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/broken_link.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/link.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/plus.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/show.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/show.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/sync.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/icons/tree_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/icons/tree_icon.png
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/iviewer.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import vtk
3 | from PySide2 import QtCore, QtWidgets
4 | from ccpi.viewer import viewer2D, viewer3D
5 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget
6 | import ccpi.viewer.viewerLinker as vlink
7 | from ccpi.viewer.utils.conversion import Converter
8 | import numpy as np
9 | from ccpi.viewer.utils import example_data
10 |
11 |
12 | class SingleViewerCenterWidget(QtWidgets.QMainWindow):
13 |
14 | def __init__(self, parent=None, viewer=viewer2D):
15 | QtWidgets.QMainWindow.__init__(self, parent)
16 |
17 | self.frame = QCILViewerWidget(parent, viewer=viewer, shape=(600, 600))
18 |
19 | if viewer == viewer3D:
20 | self.frame.viewer.setVolumeRenderOpacityMethod('scalar')
21 |
22 | self.setCentralWidget(self.frame)
23 |
24 | self.show()
25 |
26 | def set_input(self, data):
27 | self.frame.viewer.setInputData(data)
28 |
29 |
30 | class TwoLinkedViewersCenterWidget(QtWidgets.QMainWindow):
31 |
32 | def __init__(self, parent=None, viewer1='2D', viewer2='2D'):
33 | QtWidgets.QMainWindow.__init__(self, parent)
34 | #self.resize(800,600)
35 | styles = []
36 | viewers = []
37 |
38 | for viewer in [viewer1, viewer2]:
39 | if viewer == '2D':
40 | styles.append(vlink.Linked2DInteractorStyle)
41 | elif viewer == '3D':
42 | styles.append(vlink.Linked3DInteractorStyle)
43 | viewers.append(eval('viewer' + viewer))
44 | self.frame1 = QCILViewerWidget(parent, viewer=viewers[0], shape=(600, 600), interactorStyle=styles[0])
45 | self.frame2 = QCILViewerWidget(parent, viewer=viewers[1], shape=(600, 600), interactorStyle=styles[1])
46 |
47 | # Initially link viewers
48 | self.linkedViewersSetup()
49 | self.linker.enable()
50 |
51 | layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight)
52 | layout.addWidget(self.frame1)
53 | layout.addWidget(self.frame2)
54 |
55 | cw = QtWidgets.QWidget()
56 | cw.setLayout(layout)
57 | self.setCentralWidget(cw)
58 | self.central_widget = cw
59 |
60 | self.show()
61 |
62 | def linkedViewersSetup(self):
63 | v1 = self.frame1.viewer
64 | v2 = self.frame2.viewer
65 | self.linker = vlink.ViewerLinker(v1, v2)
66 | self.linker.setLinkPan(True)
67 | self.linker.setLinkZoom(True)
68 | self.linker.setLinkWindowLevel(True)
69 | self.linker.setLinkSlice(True)
70 |
71 | def set_input(self, data1, data2):
72 | self.frame1.viewer.setInputData(data1)
73 | self.frame2.viewer.setInputData(data2)
74 |
75 |
76 | class iviewer(object):
77 | '''
78 | a Qt interactive viewer that can be used as plotter2D with one single dataset
79 | Parameters
80 | ----------
81 | data: vtkImageData
82 | image to be displayed
83 | moredata: vtkImageData, optional
84 | extra image to be displayed
85 | viewer1: string - '2D' or '3D'
86 | the type of viewer to display the first image on
87 | viewer2: string - '2D' or '3D', optional
88 | the type of viewer to display the second image on (if present)
89 |
90 | '''
91 |
92 | def __init__(self, data, *moredata, **kwargs):
93 | '''Creator'''
94 | app = QtWidgets.QApplication(sys.argv)
95 | self.app = app
96 |
97 | self.setUp(data, *moredata, **kwargs)
98 | self.show()
99 |
100 | def setUp(self, data, *moredata, **kwargs):
101 | if len(moredata) == 0:
102 | # can change the behaviour by setting which viewer you want
103 | # between viewer2D and viewer3D
104 | viewer_type = kwargs.get('viewer1', '2D')
105 | if viewer_type == '2D':
106 | viewer = viewer2D
107 | elif viewer_type == '3D':
108 | viewer = viewer3D
109 | window = SingleViewerCenterWidget(viewer=viewer)
110 | window.set_input(self.convert_to_vtkImage(data))
111 | else:
112 | viewer1 = kwargs.get('viewer1', '2D')
113 | viewer2 = kwargs.get('viewer2', '2D')
114 | window = TwoLinkedViewersCenterWidget(viewer1=viewer1, viewer2=viewer2)
115 | window.set_input(self.convert_to_vtkImage(data), self.convert_to_vtkImage(moredata[0]))
116 | viewer_type = None
117 | self.viewer1_type = viewer1
118 | self.viewer2_type = viewer2
119 |
120 | self.window = window
121 | self.viewer_type = viewer_type
122 | self.has_run = None
123 |
124 | def show(self):
125 | if self.has_run is None:
126 | self.has_run = self.app.exec_()
127 | else:
128 | print('No instance can be run interactively again. Delete and re-instantiate.')
129 |
130 | def __del__(self):
131 | '''destructor'''
132 | self.app.exit()
133 |
134 | def convert_to_vtkImage(self, data):
135 | '''convert the data to vtkImageData for the viewer'''
136 | if isinstance(data, vtk.vtkImageData):
137 | vtkImage = data
138 |
139 | elif isinstance(data, np.ndarray):
140 | vtkImage = Converter.numpy2vtkImage(data)
141 |
142 | elif hasattr(data, 'as_array'):
143 | # this makes it likely it is a CIL/SIRF DataContainer
144 | # currently this will only deal with the actual data
145 | # but it will parse the metadata in future
146 | return self.convert_to_vtkImage(data.as_array())
147 |
148 | return vtkImage
149 |
150 |
151 | if __name__ == "__main__":
152 |
153 | err = vtk.vtkFileOutputWindow()
154 | err.SetFileName("viewer.log")
155 | vtk.vtkOutputWindow.SetInstance(err)
156 |
157 | data = example_data.HEAD.get()
158 | iviewer(data, data, viewer1='2D', viewer2='3D')
159 |
160 | # To use your own metaimage file, uncomment:
161 | # reader = vtk.vtkMetaImageReader()
162 | # reader.SetFileName('head.mha')
163 | # reader.Update()
164 | #iviewer(reader.GetOutput(), reader.GetOutput(), viewer1='2D', viewer2='3D')
165 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/standalone_viewer.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from functools import partial
3 |
4 | import vtk
5 | from ccpi.viewer import viewer2D, viewer3D
6 | from ccpi.viewer.ui.main_windows import TwoViewersMainWindow
7 | from PySide2 import QtWidgets
8 | from PySide2.QtWidgets import QAction, QCheckBox
9 |
10 |
11 | class StandaloneViewerMainWindow(TwoViewersMainWindow):
12 | '''
13 | A main window for displaying two viewers side by side, with a menu bar
14 | for selecting the images to be displayed.
15 |
16 | The first image selected will be displayed on both viewers. The second
17 | is an image overlay which is displayed on the 2D viewer/s only.
18 |
19 | A dock widget is created which contains widgets for displaying the
20 | file name of the image shown on the viewer, and the level of downsampling
21 | of the image displayed on the viewer.
22 | '''
23 |
24 | def __init__(self,
25 | title="StandaloneViewer",
26 | app_name="Standalone Viewer",
27 | settings_name=None,
28 | organisation_name=None,
29 | viewer1_type='2D',
30 | viewer2_type='3D'):
31 | super(StandaloneViewerMainWindow, self).__init__(title, app_name, settings_name, organisation_name,
32 | viewer1_type, viewer2_type)
33 |
34 | self.addToMenu()
35 | self.addToViewerCoordsDockWidget()
36 |
37 | self.image_overlay = vtk.vtkImageData()
38 |
39 | def addToMenu(self):
40 | '''
41 | Adds actions to the menu bar for selecting the images to be displayed
42 | '''
43 | file_menu = self.menus['File']
44 |
45 | # insert image selection as first action in file menu:
46 |
47 | image2_action = QAction("Select Image Overlay", self)
48 | image2_action.triggered.connect(lambda: self.setViewersInputFromDialog(self.viewers, input_num=2))
49 | file_menu.insertAction(file_menu.actions()[0], image2_action)
50 |
51 | image1_action = QAction("Select Image", self)
52 | image1_action.triggered.connect(lambda: self.setViewersInputFromDialog(self.viewers))
53 | file_menu.insertAction(file_menu.actions()[0], image1_action)
54 |
55 | def addToViewerCoordsDockWidget(self):
56 | '''
57 | Adds widgets to the viewer coords dock widget for displaying the
58 | image overlay, and for showing/hiding the 2D and 3D viewers.
59 | '''
60 | checkbox = QCheckBox("Show Image Overlay")
61 | checkbox.setChecked(True)
62 | self.viewer_coords_dock.widget().addSpanningWidget(checkbox, 'image_overlay')
63 | checkbox.stateChanged.connect(partial(self.showHideImageOverlay))
64 |
65 | checkbox2 = QCheckBox("Show 2D Viewer")
66 | checkbox2.setChecked(True)
67 | checkbox2.stateChanged.connect(partial(self.showHideViewer, 0))
68 |
69 | checkbox3 = QCheckBox("Show 3D Viewer")
70 | checkbox3.setChecked(True)
71 | self.viewer_coords_dock.widget().addWidget(checkbox3, checkbox2, 'show_viewer')
72 | checkbox3.stateChanged.connect(partial(self.showHideViewer, 1))
73 |
74 | def showHideImageOverlay(self, show=True):
75 | '''
76 | Shows or hides the image overlay/s on the viewers
77 | '''
78 | for viewer in self.viewer_coords_dock.viewers:
79 | if isinstance(viewer, viewer2D):
80 | if show:
81 | if hasattr(self, 'image_overlay'):
82 | viewer.setInputData2(self.image_overlay)
83 | else:
84 | self.image_overlay = viewer.image2
85 | viewer.setInputData2(vtk.vtkImageData())
86 |
87 |
88 | class standalone_viewer(object):
89 | '''
90 | Launches a StandaloneViewerMainWindow instance.
91 |
92 | Parameters:
93 | ------------
94 | title: str
95 | title of the window
96 | viewer1_type: '2D' or '3D'
97 | viewer2_type: '2D', '3D' or None
98 | if None, only one viewer is displayed
99 | '''
100 |
101 | def __init__(self, title="", viewer1_type='2D', viewer2_type='3D', *args, **kwargs):
102 | '''Creator
103 |
104 | Parameters:
105 | ------------
106 | title: str
107 | title of the window
108 | viewer1_type: '2D' or '3D'
109 | viewer2_type: '2D', '3D' or None
110 | if None, only one viewer is displayed
111 | '''
112 | app = QtWidgets.QApplication(sys.argv)
113 | self.app = app
114 |
115 | self.set_up(title, viewer1_type, viewer2_type, *args, **kwargs)
116 |
117 | def set_up(self, title, viewer1_type, viewer2_type=None, *args, **kwargs):
118 | '''
119 | Sets up the standalone viewer.
120 |
121 | Parameters:
122 | ------------
123 | title: str
124 | title of the window
125 | viewer1_type: '2D' or '3D'
126 | viewer2_type: '2D', '3D' or None
127 | if None, only one viewer is displayed
128 | '''
129 |
130 | window = StandaloneViewerMainWindow(title, viewer1_type, viewer2_type)
131 |
132 | self.window = window
133 | self.has_run = None
134 |
135 | window.show()
136 |
137 | def show(self):
138 | '''
139 | Shows the window
140 | '''
141 | if self.has_run is None:
142 | self.has_run = self.app.exec_()
143 | else:
144 | print('No instance can be run interactively again. Delete and re-instantiate.')
145 |
146 | def __del__(self):
147 | '''destructor'''
148 | self.app.exit()
149 |
150 |
151 | def main():
152 | # Run a standalone viewer with a 2D and a 3D viewer:
153 | err = vtk.vtkFileOutputWindow()
154 | err.SetFileName("viewer.log")
155 | vtk.vtkOutputWindow.SetInstance(err)
156 | standalone_viewer_instance = standalone_viewer("Standalone Viewer", viewer1_type='2D', viewer2_type='3D')
157 | standalone_viewer_instance.show()
158 | return 0
159 |
160 |
161 | if __name__ == '__main__':
162 | main()
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/ccpi/viewer/ui/__init__.py
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/ui/qt_widgets.py:
--------------------------------------------------------------------------------
1 | from eqt.ui.UIFormWidget import FormDockWidget
2 | from PySide2.QtWidgets import QComboBox, QLabel
3 | from ccpi.viewer.CILViewer import CILViewer
4 | from ccpi.viewer.CILViewer2D import CILViewer2D
5 |
6 |
7 | class ViewerCoordsDockWidget(FormDockWidget):
8 | ''' This is the dockwidget which
9 | shows the original and downsampled image
10 | size and the user can select whether coordinates
11 | are displayed in system of original or downsampled image'''
12 |
13 | def __init__(self, parent, title="Viewer Information", viewers=None):
14 | '''
15 | Parameters
16 | ----------
17 | parent : QWidget
18 | The parent widget.
19 | viewers : list of CILViewer2D and/or CILViewer instances, default = None
20 | The viewers which this dock widget will display information for.
21 | '''
22 | super(ViewerCoordsDockWidget, self).__init__(parent, title)
23 |
24 | self.setViewers(viewers)
25 |
26 | form = self.widget()
27 |
28 | viewer_coords_widgets = {}
29 |
30 | viewer_coords_widgets['image'] = QLabel()
31 | viewer_coords_widgets['image'].setText("Display image: ")
32 |
33 | viewer_coords_widgets['image_combobox'] = QComboBox()
34 | viewer_coords_widgets['image_combobox'].setEnabled(False)
35 | form.addWidget(viewer_coords_widgets['image_combobox'], viewer_coords_widgets['image'], 'image')
36 |
37 | viewer_coords_widgets['coords_info'] = QLabel()
38 | viewer_coords_widgets['coords_info'].setText(
39 | "The viewer displays a downsampled image for visualisation purposes: ")
40 | viewer_coords_widgets['coords_info'].setVisible(False)
41 |
42 | form.addSpanningWidget(viewer_coords_widgets['coords_info'], 'coords_info')
43 |
44 | form.addWidget(QLabel(""), QLabel("Loaded Image Size: "), 'loaded_image_dims')
45 |
46 | viewer_coords_widgets['displayed_image_dims_label'] = QLabel()
47 | viewer_coords_widgets['displayed_image_dims_label'].setText("Displayed Image Size: ")
48 | viewer_coords_widgets['displayed_image_dims_label'].setVisible(False)
49 |
50 | viewer_coords_widgets['displayed_image_dims_field'] = QLabel()
51 | viewer_coords_widgets['displayed_image_dims_field'].setText("")
52 | viewer_coords_widgets['displayed_image_dims_field'].setVisible(False)
53 |
54 | form.addWidget(viewer_coords_widgets['displayed_image_dims_field'],
55 | viewer_coords_widgets['displayed_image_dims_label'], 'displayed_image_dims')
56 |
57 | viewer_coords_widgets['coords'] = QLabel()
58 | viewer_coords_widgets['coords'].setText("Display viewer coordinates in: ")
59 |
60 | viewer_coords_widgets['coords_combo'] = QComboBox()
61 | viewer_coords_widgets['coords_combo'].addItems(["Loaded Image", "Downsampled Image"])
62 | viewer_coords_widgets['coords_combo'].setEnabled(False)
63 | form.addWidget(viewer_coords_widgets['coords_combo'], viewer_coords_widgets['coords'], 'coords_combo')
64 |
65 | viewer_coords_widgets['coords_warning'] = QLabel()
66 | viewer_coords_widgets['coords_warning'].setText("Warning: These coordinates are approximate.")
67 | viewer_coords_widgets['coords_warning'].setVisible(False)
68 |
69 | form.addSpanningWidget(viewer_coords_widgets['coords_warning'], 'coords_warning')
70 |
71 | @property
72 | def viewers(self):
73 | ''' Get the viewers which this dock widget will display information for.
74 |
75 | Returns
76 | -------
77 | list of CILViewer2D and/or CILViewer3D instances
78 | The viewers which this dock widget will display information for.'''
79 | return self._viewers
80 |
81 | @viewers.setter
82 | def viewers(self, viewers):
83 | '''
84 | Set the viewers which this dock widget will display information for.
85 |
86 | Parameters
87 | ----------
88 | viewers : list of CILViewer2D and/or CILViewer instances
89 | The viewers which this dock widget will display information for.
90 | '''
91 | if viewers is not None:
92 | if not isinstance(viewers, list):
93 | viewers = [viewers]
94 | for viewer in viewers:
95 | if not isinstance(viewer, (CILViewer2D, CILViewer)):
96 | raise TypeError("viewers must be a list of CILViewer2D and/or CILViewer instances")
97 | self._viewers = viewers
98 |
99 | def setViewers(self, viewers):
100 | ''' Set the viewers which this dock widget will display information for.
101 |
102 | Parameters
103 | ----------
104 | viewers : list of CILViewer2D and/or CILViewer instances
105 | The viewers which this dock widget will display information for.
106 | '''
107 | self.viewers = viewers
108 |
109 | def getViewers(self):
110 | ''' Get the viewers which this dock widget will display information for.
111 |
112 | Returns
113 | -------
114 | list of CILViewer2D and/or CILViewer instances
115 | The viewers which this dock widget will display information for.'''
116 | return self.viewers
117 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/undirected_graph.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on Sat May 12 13:35:53 2018
4 |
5 | @author: ofn77899
6 | """
7 |
8 | import vtk
9 | from vtk import vtkGraphLayoutView
10 |
11 |
12 | def generate_data():
13 | g = vtk.vtkMutableDirectedGraph()
14 |
15 | # add vertices
16 | v = []
17 | for i in range(6):
18 | v.append(g.AddVertex())
19 |
20 | g.AddEdge(v[0], v[1])
21 | g.AddEdge(v[1], v[2])
22 | g.AddEdge(v[1], v[3])
23 | g.AddEdge(v[0], v[4])
24 | g.AddEdge(v[0], v[5])
25 |
26 | weights = vtk.vtkDoubleArray()
27 | weights.SetNumberOfComponents(1)
28 | weights.SetName("Weights")
29 |
30 | X = vtk.vtkDoubleArray()
31 | X.SetNumberOfComponents(1)
32 | X.SetName("X")
33 |
34 | Y = vtk.vtkDoubleArray()
35 | Y.SetNumberOfComponents(1)
36 | Y.SetName("Y")
37 |
38 | X.InsertNextValue(0)
39 | X.InsertNextValue(0)
40 | X.InsertNextValue(1)
41 | X.InsertNextValue(-1)
42 | X.InsertNextValue(0.5)
43 | X.InsertNextValue(-0.5)
44 |
45 | Y.InsertNextValue(0)
46 | Y.InsertNextValue(1)
47 | Y.InsertNextValue(1.5)
48 | Y.InsertNextValue(2)
49 | Y.InsertNextValue(-.5)
50 | Y.InsertNextValue(-.8)
51 |
52 | weights.InsertNextValue(1.)
53 | weights.InsertNextValue(2.)
54 | weights.InsertNextValue(3.)
55 | weights.InsertNextValue(4.)
56 | weights.InsertNextValue(5.)
57 |
58 | g.GetEdgeData().AddArray(weights)
59 | g.GetVertexData().AddArray(X)
60 | g.GetVertexData().AddArray(Y)
61 |
62 | return g
63 |
64 |
65 | class GraphInteractorStyle(vtk.vtkInteractorStyleRubberBand2D):
66 |
67 | def __init__(self, callback):
68 | vtk.vtkInteractorStyleRubberBand2D.__init__(self)
69 | self._viewer = callback
70 |
71 | self.AddObserver("MouseMoveEvent", self.OnMouseMoveEvent, 1.0)
72 |
73 | def GetEventPosition(self):
74 | return self.GetInteractor().GetEventPosition()
75 |
76 | def GetRenderer(self):
77 | return self._viewer.GetRenderer()
78 |
79 | def Render(self):
80 | self._viewer.Render()
81 |
82 | def display2world(self, viewerposition):
83 | vc = vtk.vtkCoordinate()
84 | vc.SetCoordinateSystemToViewport()
85 | vc.SetValue(viewerposition + (0.0, ))
86 |
87 | return vc.GetComputedWorldValue(self.GetRenderer())
88 |
89 | def OnMouseMoveEvent(self, interactor, event):
90 | position = interactor.GetInteractor().GetEventPosition()
91 | world_position = self.display2world(position)
92 | level = world_position[1] * 100
93 |
94 | # Don't display values outside the graph scope
95 | if level < 0:
96 | level = 0
97 | if level > 100:
98 | level = 100
99 |
100 | # Create the label
101 | point_label = "Level: {:.1f} %".format(level)
102 |
103 | # Set the label and push change
104 | self.updateCornerAnnotation('pointAnnotation', point_label)
105 | self.Render()
106 |
107 |
108 | class UndirectedGraph(vtkGraphLayoutView):
109 |
110 | annotations = {"featureAnnotation": 0, "pointAnnotation": 2}
111 |
112 | def __init__(self, renWin=None, iren=None):
113 | super().__init__()
114 |
115 | if renWin:
116 | self.renWin = renWin
117 | else:
118 | self.renWin = vtk.vtkRenderWindow()
119 | if iren:
120 | self.iren = iren
121 | else:
122 | self.iren = vtk.vtkRenderWindowInteractor()
123 |
124 | self.SetRenderWindow(self.renWin)
125 | self.SetInteractor(self.iren)
126 |
127 | # Add observer to display level.
128 | self.iren.AddObserver("MouseMoveEvent", self.OnMouseMoveEvent, 1.)
129 |
130 | # Create corner annotations
131 | self.featureAnnotation = self.createCornerAnnotation()
132 | self.pointAnnotation = self.createCornerAnnotation()
133 |
134 | def update(self, input_data):
135 |
136 | # Create layout strategy
137 | layoutStrategy = vtk.vtkAssignCoordinatesLayoutStrategy()
138 | layoutStrategy.SetYCoordArrayName('Y')
139 | layoutStrategy.SetXCoordArrayName('X')
140 |
141 | self.AddRepresentationFromInput(input_data)
142 | self.SetVertexLabelArrayName("VertexID")
143 | self.SetVertexLabelVisibility(True)
144 |
145 | self.SetLayoutStrategy(layoutStrategy)
146 |
147 | annotation_link = vtk.vtkAnnotationLink()
148 | annotation_link.AddObserver("AnnotationChangedEvent", self.select_callback)
149 | self.GetRepresentation(0).SetAnnotationLink(annotation_link)
150 | self.GetRepresentation(0).SetScalingArrayName('VertexID')
151 | self.GetRepresentation(0).ScalingOn()
152 |
153 | self.ResetCamera()
154 | self.Render()
155 |
156 | def createCornerAnnotation(self):
157 | cornerAnnotation = vtk.vtkCornerAnnotation()
158 | cornerAnnotation.SetMaximumFontSize(12)
159 | cornerAnnotation.PickableOff()
160 | cornerAnnotation.VisibilityOff()
161 | cornerAnnotation.GetTextProperty().ShadowOn()
162 | self.GetRenderer().AddActor(cornerAnnotation)
163 |
164 | return cornerAnnotation
165 |
166 | def updateCornerAnnotation(self, target_annotation, label):
167 |
168 | annotation = getattr(self, target_annotation)
169 |
170 | # Make sure the annotation is visible
171 | if not annotation.GetVisibility():
172 | annotation.VisibilityOn()
173 |
174 | # Get the correct corner
175 | corner_index = self.annotations[target_annotation]
176 | annotation.SetText(corner_index, label)
177 |
178 | def run(self, input_data):
179 | self.update(input_data)
180 | self.GetInteractor().Start()
181 |
182 | ######### INTERACTOR CALBACKS #########
183 | def select_callback(self, interactor, event):
184 | sel = interactor.GetCurrentSelection()
185 |
186 | for nn in range(sel.GetNumberOfNodes()):
187 | sel_ids = sel.GetNode(nn).GetSelectionList()
188 | if sel_ids.GetNumberOfTuples() > 0:
189 | for ii in range(sel_ids.GetNumberOfTuples()):
190 | print(int(sel_ids.GetTuple1(ii)))
191 |
192 | def display2world(self, viewerposition):
193 | vc = vtk.vtkCoordinate()
194 | vc.SetCoordinateSystemToViewport()
195 | vc.SetValue(viewerposition + (0.0, ))
196 |
197 | return vc.GetComputedWorldValue(self.GetRenderer())
198 |
199 | def OnMouseMoveEvent(self, interactor, event):
200 |
201 | # Allow default behaviour
202 | interactor.GetInteractorStyle().OnMouseMove()
203 |
204 | position = interactor.GetEventPosition()
205 | world_position = self.display2world(position)
206 | level = world_position[1] * 100
207 |
208 | # Don't display values outside the graph scope
209 | if level < 0:
210 | level = 0
211 | if level > 100:
212 | level = 100
213 |
214 | # Create the label
215 | point_label = "Level: {:.1f} %".format(level)
216 |
217 | # Set the label and push change
218 | self.updateCornerAnnotation('pointAnnotation', point_label)
219 | self.Render()
220 |
221 |
222 | if __name__ == "__main__":
223 | UndirectedGraph().run(generate_data())
224 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/utils/CameraData.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones, Laura Murgatroyd
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | from dataclasses import dataclass
20 |
21 | import vtk
22 |
23 |
24 | @dataclass(init=False)
25 | class CameraData:
26 | '''
27 | A dataclass to store the camera position, focal point and view up
28 | '''
29 | position: list
30 | focalPoint: list
31 | viewUp: list
32 |
33 | def __init__(self, camera: vtk.vtkCamera):
34 | self.position = camera.GetPosition()
35 | self.focalPoint = camera.GetFocalPoint()
36 | self.viewUp = camera.GetViewUp()
37 |
38 | @staticmethod
39 | def CopyDataToCamera(camera_data, vtkcamera):
40 | '''
41 | Copy the camera_data to a vtkcamera
42 |
43 | Parameters
44 | ----------
45 | camera_data : CameraData
46 | The camera data to copy
47 | vtkcamera : vtkCamera
48 | The vtk camera to copy to.
49 | '''
50 | vtkcamera.SetPosition(*camera_data.position)
51 | vtkcamera.SetFocalPoint(*camera_data.focalPoint)
52 | vtkcamera.SetViewUp(*camera_data.viewUp)
53 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .conversion import *
2 | from .colormaps import *
3 |
4 | from .visualisation_pipeline import cilClipPolyDataBetweenPlanes, cilPlaneClipper, cilMaskPolyData
5 |
6 | from .CameraData import CameraData
7 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/utils/error_handling.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | # See examples/error_observer.py for an example of using the ErrorObserver
4 | # and EndObserver.
5 |
6 |
7 | class ErrorObserver:
8 |
9 | def __init__(self, callback_fn=print):
10 | '''
11 | Parameters:
12 | -----------
13 | callback_fn : function, default print
14 | is called when an error occurs. It acts on the error message (str)
15 | so must be a function that takes a string as a parameter.
16 | '''
17 | self.__error_occurred = False
18 | self.__get_error_message = None
19 | self.CallDataType = 'string0'
20 | self.callback_fn = callback_fn
21 |
22 | def __call__(self, obj, event, message):
23 | self.__error_occurred = True
24 | self.__get_error_message = message
25 | self.callback_fn(self.__get_error_message)
26 |
27 | def error_occurred(self):
28 | occ = self.__error_occurred
29 | self.__error_occurred = False
30 | return occ
31 |
32 | def get_error_message(self):
33 | return self.__get_error_message
34 |
35 |
36 | class EndObserver:
37 | ''' Occurs when the observed Algorithm finishes
38 | '''
39 |
40 | def __init__(self, error_observer, callback_fn):
41 | '''
42 | Parameters
43 | ----------
44 | error_observer: ErrorObserver
45 | the error observer attached to the object the EndObserver is attached to
46 | callback_fn: function
47 | function to run if an error hasn't been identified by the error_observer'''
48 | self.callback_fn = callback_fn
49 | self.error_observer = error_observer
50 | self.CallDataType = 'string0'
51 |
52 | def __call__(self, obj, event):
53 | if not self.error_observer.error_occurred():
54 | self.callback_fn()
55 |
56 |
57 | # Format warning messages:
58 | def customise_warnings():
59 | # prints warning message in bold yellow, file name and line number
60 | # without customising, warning message is printed twice, and is not
61 | # as clear
62 | def custom_warning_format(message, category, filename, lineno, line=None):
63 | return "{1}:{2}: \033[1;33;40m {1}: {0} \033[0m \n ".format(message, category.__name__, filename, lineno)
64 |
65 | warnings.formatwarning = custom_warning_format
66 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/utils/example_data.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import vtk
4 | import sys
5 |
6 | # this is the location where the cil-data module saves the datasets
7 | data_dir = os.path.abspath(os.path.join(sys.prefix, 'share', 'cil'))
8 |
9 |
10 | class DATA(object):
11 |
12 | @classmethod
13 | def dfile(cls):
14 | return None
15 |
16 | @classmethod
17 | def get(cls, **kwargs):
18 | ddir = kwargs.get('data_dir', data_dir)
19 | loader = TestData(data_dir=ddir)
20 | return loader.load(cls.dfile(), **kwargs)
21 |
22 |
23 | class HEAD(DATA):
24 |
25 | @classmethod
26 | def dfile(cls):
27 | return TestData.HEAD
28 |
29 |
30 | class TestData(object):
31 | '''Class to return test data
32 |
33 | provides 1 dataset:
34 | HEAD = 'head.mha'
35 | '''
36 | HEAD = 'head.mha'
37 |
38 | def __init__(self, **kwargs):
39 | self.data_dir = kwargs.get('data_dir', data_dir)
40 |
41 | def load(self, which, **kwargs):
42 | '''
43 | Return a test data of the requested image
44 |
45 | Parameters
46 | ----------
47 | which: str
48 | Image selector: HEAD
49 |
50 | Returns
51 | -------
52 | vtkImageData
53 | The loaded dataset
54 | '''
55 | if which not in [TestData.HEAD]:
56 | raise ValueError('Unknown TestData {}.'.format(which))
57 |
58 | data_file = os.path.join(self.data_dir, which)
59 |
60 | reader = vtk.vtkMetaImageReader()
61 | reader.SetFileName(data_file)
62 | reader.Update()
63 | return reader.GetOutput()
64 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/utils/hdf5_io.py:
--------------------------------------------------------------------------------
1 | import vtk
2 | from vtk.util.vtkAlgorithm import VTKPythonAlgorithmBase
3 | import h5py
4 | import numpy as np
5 | from vtk.numpy_interface import dataset_adapter as dsa
6 | from vtk.util import numpy_support
7 |
8 | # Methods for reading and writing HDF5 files:
9 |
10 |
11 | def write_image_data_to_hdf5(filename, data, dataset_name, attributes={}):
12 | '''
13 | Writes vtkImageData to a dataset in a HDF5 file
14 |
15 | Args:
16 | filename: string - name of HDF5 file to write to.
17 | data: vtkImageData - image data to write.
18 | DatasetName: string - DatasetName for HDF5 dataset.
19 | attributes: dict - attributes to assign to HDF5 dataset.
20 | '''
21 |
22 | with h5py.File(filename, "a") as f:
23 | # The function imgdata.GetPointData().GetScalars() returns a pointer to a
24 | # vtkArray where the data is stored as X-Y-Z.
25 | array = numpy_support.vtk_to_numpy(data.GetPointData().GetScalars())
26 |
27 | # Note that we flip the dimensions here because
28 | # VTK's order is Fortran whereas h5py writes in
29 | # C order. We don't want to do deep copies so we write
30 | # with dimensions flipped and pretend the array is
31 | # C order.
32 | array = array.reshape(data.GetDimensions()[::-1])
33 | try:
34 | dset = f.create_dataset(dataset_name, data=array)
35 | except RuntimeError:
36 | print("Unable to save image data to {0}."
37 | "Dataset with name {1} already exists in this file.".format(filename, dataset_name))
38 | return
39 | for key, value in attributes.items():
40 | dset.attrs[key] = value
41 |
42 |
43 | class HDF5Reader(VTKPythonAlgorithmBase):
44 | '''
45 | vtkAlgorithm for reading vtkImageData from a HDF5 file
46 | Adapted from:
47 | https://blog.kitware.com/developing-hdf5-readers-using-vtkpythonalgorithm/
48 | To use this, you must set a FileName (the file you are reading), and also a
49 | DatasetName (the name of where in the hdf5 file the data will be saved)
50 | '''
51 |
52 | def __init__(self):
53 | VTKPythonAlgorithmBase.__init__(self, nInputPorts=0, nOutputPorts=1, outputType='vtkImageData')
54 |
55 | self._FileName = None
56 | self._DatasetName = None
57 | self._4DSliceIndex = 0
58 | self._4DIndex = 0
59 |
60 | def RequestData(self, request, inInfo, outInfo):
61 | self._update_output_data(outInfo)
62 | return 1
63 |
64 | def _update_output_data(self, outInfo):
65 | if self._DatasetName is None:
66 | raise Exception("DataSetName must be set.")
67 | if self._FileName is None:
68 | raise Exception("FileName must be set.")
69 | with h5py.File(self._FileName, 'r') as f:
70 | info = outInfo.GetInformationObject(0)
71 | shape = np.shape(f[self._DatasetName])
72 | # print("keys:", list(f.keys()))
73 | # print(shape)
74 | ue = info.Get(vtk.vtkStreamingDemandDrivenPipeline.UPDATE_EXTENT())
75 | # Note that we flip the update extents because VTK is Fortran order
76 | # whereas h5py reads in C order. When writing we pretend that the
77 | # data was C order so we have to flip the extents/dimensions.
78 | if len(shape) == 3:
79 | data = f[self._DatasetName][ue[4]:ue[5] + 1, ue[2]:ue[3] + 1, ue[0]:ue[1] + 1]
80 | elif len(shape) == 4:
81 | if self._4DIndex == 0:
82 | data = f[self._DatasetName][self._4DSliceIndex, ue[4]:ue[5] + 1, ue[2]:ue[3] + 1, ue[0]:ue[1] + 1]
83 | elif self._4DIndex == 1:
84 | data = f[self._DatasetName][ue[4]:ue[5] + 1, self._4DSliceIndex, ue[2]:ue[3] + 1, ue[0]:ue[1] + 1]
85 | elif self._4DIndex == 2:
86 | data = f[self._DatasetName][ue[4]:ue[5] + 1, ue[2]:ue[3] + 1, self._4DSliceIndex, ue[0]:ue[1] + 1]
87 | elif self._4DIndex == 3:
88 | data = f[self._DatasetName][ue[4]:ue[5] + 1, ue[2]:ue[3] + 1, ue[0]:ue[1] + 1, self._4DSliceIndex]
89 | else:
90 | raise Exception("Currently only 3D and 4D datasets are supported.")
91 | # print("attributes: ", f.attrs.items())
92 | output = dsa.WrapDataObject(vtk.vtkImageData.GetData(outInfo))
93 | output.SetExtent(ue)
94 | output.PointData.append(data.ravel(), self._DatasetName)
95 | output.PointData.SetActiveScalars(self._DatasetName)
96 | return output
97 |
98 | def SetFileName(self, fname):
99 | if fname != self._FileName:
100 | self.Modified()
101 | if self._DatasetName is not None:
102 | with h5py.File(fname, 'r') as f:
103 | if not (self._DatasetName in f):
104 | raise Exception("No dataset named {} exists in {}.".format(self._DatasetName, fname))
105 | self._FileName = fname
106 |
107 | def GetFileName(self):
108 | return self._FileName
109 |
110 | def SetDatasetName(self, lname):
111 | if lname != self._DatasetName:
112 | self.Modified()
113 | if self._FileName is not None:
114 | with h5py.File(self._FileName, 'r') as f:
115 | if not (lname in f):
116 | raise Exception("No dataset named {} exists in {}.".format(lname, self._FileName))
117 | self._DatasetName = lname
118 |
119 | def GetDatasetName(self):
120 | return self._DatasetName
121 |
122 | def Set4DIndex(self, index):
123 | '''Sets which index is the 4th dimension that we will only read 1 slice of.'''
124 | if index not in range(0, 4):
125 | raise Exception("4D Index must be between 0 and 3.")
126 | if index != self._4DIndex:
127 | self.Modified()
128 | self._4DIndex = index
129 |
130 | def Set4DSliceIndex(self, index):
131 | '''Sets which index to read along the 4th dimension.'''
132 | if index != self._4DSliceIndex:
133 | self.Modified()
134 | self._4DSliceIndex = index
135 |
136 | def GetDimensions(self):
137 | if self._FileName is None:
138 | raise Exception("FileName must be set.")
139 | with h5py.File(self._FileName, 'r') as f:
140 | # Note that we flip the shape because VTK is Fortran order
141 | # whereas h5py reads in C order. When writing we pretend that the
142 | # data was C order so we have to flip the extents/dimensions.
143 | if self._DatasetName is None:
144 | raise Exception("DataSetName must be set.")
145 | return f[self._DatasetName].shape[::-1]
146 |
147 | def GetDataSetAttributes(self):
148 | if self._FileName is None:
149 | raise Exception("FileName must be set.")
150 | with h5py.File(self._FileName, 'r') as f:
151 | if self._DatasetName is None:
152 | raise Exception("DataSetName must be set.")
153 | return dict(f[self._DatasetName].attrs)
154 |
155 | def GetOrigin(self):
156 | # There is not a standard way to set the origin in a HDF5
157 | # file so we do not have a way to read it. Therefore we
158 | # assume it is at 0,0,0
159 | return (0, 0, 0)
160 |
161 | def GetDataType(self):
162 | if self._FileName is None:
163 | raise Exception("FileName must be set.")
164 | with h5py.File(self._FileName, 'r') as f:
165 | data_type = f.get(self._DatasetName).dtype
166 | return data_type
167 |
168 | def RequestInformation(self, request, inInfo, outInfo):
169 | dims = self.GetDimensions()
170 | info = outInfo.GetInformationObject(0)
171 | info.Set(vtk.vtkStreamingDemandDrivenPipeline.WHOLE_EXTENT(), (0, dims[0] - 1, 0, dims[1] - 1, 0, dims[2] - 1),
172 | 6)
173 | return 1
174 |
175 |
176 | class HDF5SubsetReader(VTKPythonAlgorithmBase):
177 | '''Modifies a HDF5Reader to return a different extent from an HDF5 file
178 |
179 |
180 | Examples:
181 | ---------
182 |
183 | reader = HDF5Reader()
184 | reader.SetFileName('file.h5')
185 | reader.SetDatasetName("ImageData")
186 |
187 | cropped_reader = HDF5SubsetReader()
188 | cropped_reader.SetInputConnection(reader.GetOutputPort())
189 | cropped_reader.SetUpdateExtent((0, 2, 3, 5, 1, 2))
190 | cropped_reader.Update()
191 | '''
192 |
193 | def __init__(self):
194 | VTKPythonAlgorithmBase.__init__(self,
195 | nInputPorts=1,
196 | inputType='vtkImageData',
197 | nOutputPorts=1,
198 | outputType='vtkImageData')
199 | self.__UpdateExtent = None
200 |
201 | def RequestInformation(self, request, inInfo, outInfo):
202 | info = outInfo.GetInformationObject(0)
203 | info.Set(vtk.vtkStreamingDemandDrivenPipeline.WHOLE_EXTENT(), self.__UpdateExtent, 6)
204 | return 1
205 |
206 | def RequestUpdateExtent(self, request, inInfo, outInfo):
207 | if self.__UpdateExtent is not None:
208 | info = inInfo[0].GetInformationObject(0)
209 |
210 | whole_extent = info.Get(vtk.vtkStreamingDemandDrivenPipeline.WHOLE_EXTENT())
211 | set_extent = list(info.Get(vtk.vtkStreamingDemandDrivenPipeline.UPDATE_EXTENT()))
212 |
213 | for i, value in enumerate(set_extent):
214 | if value == -1:
215 | set_extent[i] = whole_extent[i]
216 | else:
217 | if i % 2 == 0:
218 | if value < whole_extent[i]:
219 | raise ValueError("Requested extent {}\
220 | is outside of original image extent {} as {}<{}".format(
221 | set_extent, whole_extent, value, whole_extent[i]))
222 | else:
223 | if value > whole_extent[i]:
224 | raise ValueError("Requested extent {}\
225 | is outside of original image extent {} as {}>{}".format(
226 | set_extent, whole_extent, value, whole_extent[i]))
227 |
228 | self.SetUpdateExtent(set_extent)
229 |
230 | info.Set(vtk.vtkStreamingDemandDrivenPipeline.UPDATE_EXTENT(), self.__UpdateExtent, 6)
231 | return 1
232 |
233 | def RequestData(self, request, inInfo, outInfo):
234 | inp = vtk.vtkImageData.GetData(inInfo[0])
235 | opt = vtk.vtkImageData.GetData(outInfo)
236 | opt.ShallowCopy(inp)
237 | return 1
238 |
239 | def SetUpdateExtent(self, ue):
240 | if ue != self.__UpdateExtent:
241 | self.Modified()
242 | self.__UpdateExtent = ue
243 |
244 | def GetUpdateExtent(self):
245 | return self.__UpdateExtent
246 |
247 | def GetOutput(self):
248 | return self.GetOutputDataObject(0)
249 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .box_widgets import cilviewerBoxWidget, cilviewerLineWidget
2 | from .slider import SliderCallback, SliceSliderRepresentation
3 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/viewer/widgets/slider.py:
--------------------------------------------------------------------------------
1 | import vtk
2 | import logging
3 |
4 | logger = logging.getLogger(__name__)
5 | # Define a new event type
6 | SLIDER_EVENT = vtk.vtkCommand.UserEvent + 1
7 |
8 |
9 | class SliceSliderRepresentation(vtk.vtkSliderRepresentation2D):
10 | """A slider representation for the slice selector slider on a 2D CILViewer
11 |
12 | Parameters
13 | -----------
14 | orientation: str, optional
15 | The orientation of the slider. Can be 'horizontal' or 'vertical'
16 | offset: float, optional
17 | The offset of the slider from the edge of the window. Default is 0.12
18 |
19 | """
20 |
21 | def __init__(self, orientation='horizontal', offset=0.12):
22 | self.tube_width = 0.004
23 | self.slider_length = 0.015
24 | self.slider_width = 0.015
25 | self.end_cap_length = 0.008
26 | self.end_cap_width = 0.02
27 | self.title_height = 0.02
28 | self.label_height = 0.02
29 | self.bar_color = 'Gray'
30 | cil_pink = [[el / 0xff for el in [0xe5, 0x06, 0x95]], [el / 0xff for el in [0xc9, 0x2c, 0x99]],
31 | [el / 0xff for el in [0x99, 0x3d, 0xbb]], [el / 0xff for el in [0x51, 0x0c, 0x76]]]
32 |
33 | self.orientation = 'horizontal'
34 | self.offset = 0.12
35 |
36 | self.p1 = [self.offset, self.end_cap_width * 1.1]
37 | self.p2 = [1 - self.offset, self.end_cap_width * 1.1]
38 |
39 | self.title = None
40 |
41 | if orientation == 'vertical':
42 | self.offset = offset
43 | self.p1 = [self.end_cap_width * 1.1, self.offset]
44 | self.p2 = [self.end_cap_width * 1.1, 1 - self.offset]
45 |
46 | self.SetTitleText(self.title)
47 |
48 | self.GetPoint1Coordinate().SetCoordinateSystemToNormalizedDisplay()
49 | self.GetPoint1Coordinate().SetValue(self.p1[0], self.p1[1])
50 | self.GetPoint2Coordinate().SetCoordinateSystemToNormalizedDisplay()
51 | self.GetPoint2Coordinate().SetValue(self.p2[0], self.p2[1])
52 |
53 | self.SetTubeWidth(self.tube_width)
54 | self.SetSliderLength(self.slider_length)
55 | # slider_width = self.tube_width
56 | # slider.SetSliderWidth(slider_width)
57 | self.SetEndCapLength(self.end_cap_length)
58 | self.SetEndCapWidth(self.end_cap_width)
59 | self.SetTitleHeight(self.title_height)
60 | self.SetLabelHeight(self.label_height)
61 |
62 | # Set the colors of the slider components.
63 | # Change the color of the bar.
64 | self.GetTubeProperty().SetColor(vtk.vtkNamedColors().GetColor3d(self.bar_color))
65 | # Change the color of the ends of the bar.
66 | self.GetCapProperty().SetColor(cil_pink[2])
67 | # Change the color of the knob that slides.
68 | self.GetSliderProperty().SetColor(cil_pink[1])
69 | # Change the color of the knob when the mouse is held on it.
70 | self.GetSelectedProperty().SetColor(cil_pink[0])
71 |
72 |
73 | class SliderCallback:
74 | '''
75 | Class to propagate the effects of interaction between the slider widget and the viewer
76 | the slider is embedded into, and viceversa.
77 |
78 | Parameters
79 | -----------
80 | viewer : CILViewer2D the slider is embedded into
81 | slider_widget : the vtkSliderWidget that is embedded in the viewer
82 | '''
83 |
84 | def __init__(self, viewer, slider_widget):
85 | self.viewer = viewer
86 | self.slider_widget = slider_widget
87 |
88 | def __call__(self, caller, ev):
89 | '''Update the slice displayed by the viewer when the slider is moved
90 |
91 | Parameters
92 | -----------
93 | caller : the slider widget
94 | ev : the event that triggered the update
95 | '''
96 | slider_widget = caller
97 | value = slider_widget.GetRepresentation().GetValue()
98 | self.viewer.displaySlice(int(value))
99 | self.update_label(value)
100 | self.viewer.getInteractor().InvokeEvent(SLIDER_EVENT)
101 |
102 | def update_label(self, value):
103 | '''Update the text label on the slider. This is called by update_from_viewer
104 |
105 | Parameters
106 | -----------
107 | value : the value to be displayed on text label the slider
108 | '''
109 | rep = self.slider_widget.GetRepresentation()
110 | maxval = rep.GetMaximumValue()
111 | txt = "{}/{}".format(int(value), int(maxval))
112 | rep.SetLabelFormat(txt)
113 |
114 | def update_from_viewer(self, caller, ev):
115 | '''Update the slider widget from the viewer. This is called when the viewer changes the slice
116 |
117 | Parameters
118 | -----------
119 | caller : the interactor style
120 | ev : the event that triggered the update
121 | '''
122 | # The caller is the interactor style
123 | logger.info(f"Updating for event {ev}")
124 | value = caller.GetActiveSlice()
125 | self.slider_widget.GetRepresentation().SetValue(value)
126 | self.update_label(value)
127 | caller.GetRenderWindow().Render()
128 |
129 | def update_orientation(self, caller, ev):
130 | '''Update the slider widget when the orientation is changed
131 |
132 | Parameters
133 | -----------
134 | caller : the interactor style
135 | ev : the event that triggered the update
136 | '''
137 | logger.info(f"Updating orientation {ev}")
138 | value = caller.GetActiveSlice()
139 | dims = caller._viewer.img3D.GetDimensions()
140 | maxslice = dims[caller.GetSliceOrientation()] - 1
141 | sr = self.slider_widget.GetRepresentation()
142 | sr.SetMinimumValue(0)
143 | sr.SetMaximumValue(maxslice)
144 | self.update_from_viewer(caller, ev)
145 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/README.md:
--------------------------------------------------------------------------------
1 | To setup your environment for using the web application we recommend using conda as it can simplify things in comparison to other implementations of python environment handling.
2 |
3 | Follow these steps:
4 | - Install a variant of conda. For this example we will use mambaforge (includes mamba, a faster conda implementation, and has conda-forge added as a default channel). It can be downloaded here: https://github.com/conda-forge/miniforge/releases
5 | - Create the conda environment:
6 | - `mamba env create -f dev-environment.yml`
7 | - Activate the environment
8 | - `mamba activate cilviewer_webapp`
9 | - Install the app in the environment from the `CILViewer` directory
10 | - `pip install ./Wrappers/Python`
11 | - Start the web application
12 | - `web_cilviewer path/to/folder/of/data/to/use`
13 | where `path/to/folder/of/data/to/use` is the folder storing the data i.e. no filename included
14 | - Pass the 2D arg to the script if you want to use the 2D viewer i.e. `--2D` or `-d args`. This needs to be added before the path
15 | - `web_cilviewer --2D path/to/folder/of/data/to/use`
16 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/dev-environment.yml:
--------------------------------------------------------------------------------
1 | # run: conda env create --file environment.yml
2 | name: cilviewer_webapp
3 | channels:
4 | - conda-forge
5 | - ccpi
6 | dependencies:
7 | - python==3.9
8 | - matplotlib # Optional for more colormaps
9 | - h5py
10 | - pip
11 | - schema
12 | - pyyaml
13 | - pip:
14 | # Have to install Trame via pip due to unavailability on conda
15 | - trame <3, >=2.1.1 # Unpinned worked with version 2.1.1, should work with higher versions.
16 | - vtk==9.1
17 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/test/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/test/test_web_app.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | import unittest
19 | from unittest import mock
20 |
21 | from ccpi.web_viewer.web_app import arg_parser, reset_viewer2d, data_finder, set_viewer2d, main, change_orientation, change_opacity_mapping
22 |
23 |
24 | class WebAppTest(unittest.TestCase):
25 |
26 | def setUp(self):
27 | reset_viewer2d()
28 |
29 | @mock.patch("ccpi.web_viewer.web_app.print")
30 | @mock.patch("ccpi.web_viewer.web_app.sys")
31 | def test_arg_parser_handles_h(self, sys, print_output):
32 | help_string = "web_app.py [optional args: -h, -d] \n" \
33 | "Args:\n" \
34 | "-h: Show this help and exit the program\n" \
35 | "-d, --2D: Use the 2D viewer instead of the 3D viewer, the default is to just use the 3D viewer."
36 | sys.argv = ["python_file.py", "-h"]
37 | arg_parser()
38 |
39 | print_output.assert_called_once_with(help_string)
40 |
41 | @mock.patch("ccpi.web_viewer.web_app.print")
42 | @mock.patch("ccpi.web_viewer.web_app.sys")
43 | def test_arg_parser_handles_2d(self, sys, print_output):
44 | from ccpi.web_viewer.web_app import VIEWER_2D
45 | self.assertEqual(VIEWER_2D, False)
46 | sys.argv = ["python_file.py", "--2D"]
47 | arg_parser()
48 |
49 | from ccpi.web_viewer.web_app import VIEWER_2D
50 | self.assertEqual(VIEWER_2D, True)
51 | print_output.assert_not_called()
52 |
53 | @mock.patch("ccpi.web_viewer.web_app.print")
54 | @mock.patch("ccpi.web_viewer.web_app.sys")
55 | def test_arg_parser_handles_d(self, sys, print_output):
56 | from ccpi.web_viewer.web_app import VIEWER_2D
57 | self.assertEqual(VIEWER_2D, False)
58 | sys.argv = ["python_file.py", "-d"]
59 | arg_parser()
60 |
61 | from ccpi.web_viewer.web_app import VIEWER_2D
62 | self.assertEqual(VIEWER_2D, True)
63 | print_output.assert_not_called()
64 |
65 | @mock.patch("ccpi.web_viewer.web_app.print")
66 | @mock.patch("ccpi.web_viewer.web_app.sys")
67 | def test_arg_parser_does_not_do_anything_with_unused_args(self, sys, print_output):
68 | sys.argv = ["python_file.py", "file/path/to/path"]
69 | arg_parser()
70 |
71 | print_output.assert_called_once_with(
72 | "This arg: file/path/to/path is not a valid file or directory. Assuming it is for trame.")
73 |
74 | @mock.patch("ccpi.web_viewer.web_app.os")
75 | @mock.patch("ccpi.web_viewer.web_app.print")
76 | @mock.patch("ccpi.web_viewer.web_app.sys")
77 | def test_data_finder_handles_directory_that_was_passed(self, sys, print_output, os):
78 | sys.argv = ["python_file.py", "dir/path/to/path"]
79 | os.listdir.return_value = ["path/to/file1.txt", "path/to/file2.txt"]
80 | os.path.isfile.return_value = False
81 | os.path.isdir.return_value = True
82 | os.path.join.return_value = "path/to/file1.txt"
83 |
84 | return_value = data_finder()
85 |
86 | os.listdir.assert_called_once_with("dir/path/to/path")
87 | print_output.assert_not_called()
88 | self.assertEqual(return_value, ["path/to/file1.txt", "path/to/file1.txt"])
89 |
90 | @mock.patch("ccpi.web_viewer.web_app.os")
91 | @mock.patch("ccpi.web_viewer.web_app.print")
92 | @mock.patch("ccpi.web_viewer.web_app.sys")
93 | def test_data_finder_handles_file_that_was_passed(self, sys, print_output, os):
94 | sys.argv = ["python_file.py", "/path/to/file.txt"]
95 | os.path.isfile.return_value = True
96 | os.path.isdir.return_value = True
97 |
98 | return_value = data_finder()
99 |
100 | print_output.assert_not_called()
101 | self.assertEqual(return_value, ["/path/to/file.txt"])
102 |
103 | @mock.patch("ccpi.web_viewer.web_app.os")
104 | @mock.patch("ccpi.web_viewer.web_app.print")
105 | @mock.patch("ccpi.web_viewer.web_app.sys")
106 | def test_data_finder_handles_multiple_passed_args(self, sys, print_output, os):
107 | sys.argv = ["python_file.py", "path/to/file1.txt", "path/to/file2.txt"]
108 | os.listdir.return_value = ["path/to/file1.txt", "path/to/file2.txt"]
109 | os.path.isfile.return_value = True
110 | os.path.isdir.return_value = False
111 | os.path.join.return_value = "path/to/file1.txt"
112 |
113 | return_value = data_finder()
114 |
115 | print_output.assert_not_called()
116 | self.assertEqual(return_value, ["path/to/file1.txt", "path/to/file2.txt"])
117 |
118 | @mock.patch("ccpi.web_viewer.web_app.arg_parser")
119 | @mock.patch("ccpi.web_viewer.web_app.TrameViewer2D")
120 | @mock.patch("ccpi.web_viewer.web_app.TrameViewer3D")
121 | def test_main_creates_trame_viewer_2d_when_VIEWER_2D_is_true(self, viewer3d, viewer2d, arg_parser):
122 | set_viewer2d(True)
123 | data_files = mock.MagicMock()
124 | arg_parser.return_value = data_files
125 |
126 | main()
127 |
128 | viewer3d.assert_not_called()
129 | viewer2d.assert_called_once_with(data_files)
130 | viewer2d.return_value.start.assert_called_once()
131 |
132 | @mock.patch("ccpi.web_viewer.web_app.arg_parser")
133 | @mock.patch("ccpi.web_viewer.web_app.TrameViewer2D")
134 | @mock.patch("ccpi.web_viewer.web_app.TrameViewer3D")
135 | def test_main_creates_trame_viewer_3d_when_VIEWER_2D_is_false(self, viewer3d, viewer2d, arg_parser):
136 | set_viewer2d(False)
137 | data_files = mock.MagicMock()
138 | arg_parser.return_value = data_files
139 |
140 | main()
141 |
142 | viewer3d.assert_called_once_with(data_files)
143 | viewer3d.return_value.start.assert_called_once()
144 | viewer2d.assert_not_called()
145 |
146 | @mock.patch("ccpi.web_viewer.web_app.TRAME_VIEWER")
147 | def test_change_orientation_orientation_not_kwargs_calls_nothing(self, trame_viewer):
148 | change_orientation()
149 |
150 | trame_viewer.switch_to_orientation.assert_not_called()
151 |
152 | @mock.patch("ccpi.web_viewer.web_app.TRAME_VIEWER")
153 | def test_change_orientation_is_not_an_int_gets_cast_to_int_before_passed(self, trame_viewer):
154 | change_orientation(orientation="0")
155 |
156 | trame_viewer.switch_to_orientation.assert_called_once_with(0)
157 |
158 | @mock.patch("ccpi.web_viewer.web_app.TRAME_VIEWER")
159 | def test_change_opacity_mapping_not_kwargs_calls_nothing(self, trame_viewer):
160 | change_opacity_mapping()
161 |
162 | trame_viewer.set_opacity_mapping.assert_not_called()
163 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/trame_viewer2D.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | from trame.app import get_server
19 | from trame.widgets import vuetify
20 |
21 | from ccpi.viewer.CILViewer2D import CILViewer2D, SLICE_ORIENTATION_XY
22 | from ccpi.web_viewer.trame_viewer import TrameViewer
23 |
24 | server = get_server()
25 | state, ctrl = server.state, server.controller
26 |
27 |
28 | class TrameViewer2D(TrameViewer):
29 |
30 | def __init__(self, list_of_files: list = None):
31 | self.first_load = True
32 | super().__init__(list_of_files=list_of_files, viewer=CILViewer2D)
33 |
34 | self.model_choice = None
35 | self.background_choice = None
36 | self.slice_slider = None
37 | self.orientation_radio_buttons = None
38 | self.auto_window_level_button = None
39 | self.toggle_window_details_button = None
40 | self.slice_window_range_slider = None
41 | self.slice_window_slider = None
42 | self.slice_level_slider = None
43 | self.tracing_switch = None
44 | self.line_profile_switch = None
45 | self.interpolation_of_slice_switch = None
46 | self.remove_roi_button = None
47 | self.reset_defaults_button = None
48 | self.slice_interaction_col = None
49 | self.slice_interaction_row = None
50 | self.slice_interaction_section = None
51 |
52 | self.create_drawer_ui_elements()
53 |
54 | self.construct_drawer_layout()
55 |
56 | self.layout.content.children = [
57 | vuetify.VContainer(fluid=True, classes="pa-0 fill-height", children=[self.html_view])
58 | ]
59 | self.reset_defaults()
60 | self.layout.flush_content()
61 |
62 | def create_drawer_ui_elements(self):
63 | self.model_choice = self.create_model_selector()
64 | self.background_choice = self.create_background_selector()
65 | self.slice_slider = self.create_slice_slider()
66 | self.orientation_radio_buttons = self.create_orientation_radio_buttons()
67 | self.toggle_window_details_button = self.create_toggle_window_details_button()
68 | self.auto_window_level_button = self.create_auto_window_level_button()
69 | self.slice_window_range_slider = self.construct_slice_window_range_slider()
70 | self.slice_window_slider = self.construct_slice_window_slider()
71 | self.slice_level_slider = self.construct_slice_level_slider()
72 | self.tracing_switch = self.create_tracing_switch()
73 | self.interpolation_of_slice_switch = self.create_interpolation_of_slice_switch()
74 | self.remove_roi_button = self.create_remove_roi_button()
75 | self.reset_defaults_button = self.create_reset_defaults_button()
76 |
77 | def construct_drawer_layout(self):
78 | # The difference is that we use range slider instead of detailed sliders
79 | self.slice_interaction_col = vuetify.VCol([
80 | self.slice_slider, self.orientation_radio_buttons, self.auto_window_level_button,
81 | self.toggle_window_details_button, self.slice_window_range_slider, self.slice_window_slider,
82 | self.slice_level_slider, self.tracing_switch, self.interpolation_of_slice_switch
83 | ])
84 | self.slice_interaction_row = vuetify.VRow(self.slice_interaction_col)
85 | self.slice_interaction_section = vuetify.VContainer(self.slice_interaction_row)
86 | self.layout.drawer.children = [
87 | "Choose model to load", self.model_choice,
88 | vuetify.VDivider(), "Choose background color", self.background_choice,
89 | vuetify.VDivider(), self.slice_interaction_section,
90 | vuetify.VDivider(),
91 | "Use Ctrl + Click on the slice, to show the ROI of the current slice, Click and drag to resize and reposition.\n"
92 | "Move the ROI by using the middle mouse button.",
93 | vuetify.VDivider(), self.remove_roi_button,
94 | vuetify.VDivider(), self.reset_defaults_button
95 | ]
96 |
97 | def load_file(self, file_name, _="scalar"):
98 | super().load_file(file_name, windowing_method="scalar")
99 | if not self.first_load:
100 | self.update_slice_slider_data()
101 | self.update_slice_windowing_defaults()
102 | self.create_drawer_ui_elements()
103 | self.construct_drawer_layout()
104 | self.reset_defaults()
105 | else:
106 | self.first_load = False
107 |
108 | def update_slice_windowing_defaults(self):
109 | self.update_slice_data()
110 |
111 | if hasattr(self, "slice_window_range_slider") and self.slice_window_range_slider:
112 | self.slice_window_range_slider = self.construct_slice_window_range_slider()
113 | self.slice_level_slider = self.construct_slice_level_slider()
114 | self.slice_window_slider = self.construct_slice_window_slider()
115 | self.construct_drawer_layout()
116 | self.layout.flush_content()
117 |
118 | def create_remove_roi_button(self):
119 | return vuetify.VBtn("Remove ROI", hide_details=True, dense=True, solo=True, click=self.remove_roi)
120 |
121 | def create_tracing_switch(self):
122 | return vuetify.VSwitch(label="Toggle Tracing",
123 | v_model=("toggle_tracing", False),
124 | hide_details=True,
125 | dense=True,
126 | solo=True)
127 |
128 | def create_interpolation_of_slice_switch(self):
129 | return vuetify.VSwitch(
130 | label="Toggle Interpolation",
131 | v_model=("toggle_interpolation", False),
132 | hide_details=True,
133 | dense=True,
134 | solo=True,
135 | )
136 |
137 | def create_reset_defaults_button(self):
138 | return vuetify.VBtn("Reset Defaults", hide_details=True, dense=True, solo=True, click=self.reset_defaults)
139 |
140 | def change_tracing(self, tracing: bool):
141 | if tracing:
142 | self.cil_viewer.imageTracer.On()
143 | else:
144 | self.cil_viewer.imageTracer.Off()
145 |
146 | def change_interpolation(self, interpolation: bool):
147 | if not interpolation:
148 | self.cil_viewer.imageSlice.GetProperty().SetInterpolationTypeToNearest()
149 | else:
150 | self.cil_viewer.imageSlice.GetProperty().SetInterpolationTypeToLinear()
151 | self.cil_viewer.updatePipeline()
152 |
153 | def change_window_level_detail_sliders(self, show_detailed: bool):
154 | super().change_window_level_detail_sliders(show_detailed)
155 |
156 | # Reconstruct the detailed sliders
157 | self.slice_window_range_slider = self.construct_slice_window_range_slider()
158 | self.slice_window_slider = self.construct_slice_window_slider()
159 | self.slice_level_slider = self.construct_slice_level_slider()
160 |
161 | # Reconstruct the drawer and push it
162 | self.construct_drawer_layout()
163 | self.layout.flush_content()
164 |
165 | def remove_roi(self):
166 | self.cil_viewer.style.RemoveROIWidget()
167 |
168 | def reset_defaults(self):
169 | state["background_color"] = "cil_viewer_blue"
170 | state["slice"] = self.default_slice
171 | state["orientation"] = f"{SLICE_ORIENTATION_XY}"
172 |
173 | # resets to window-level based on 5th, 95th percentiles over volume:
174 | min, max = self.cil_viewer.getImageMapRange((5., 95.), "scalar")
175 | window, level = self.cil_viewer.getSliceWindowLevelFromRange(min, max)
176 | if not self.window_level_sliders_are_percentages:
177 | state["slice_window_range"] = (min, max)
178 | state["slice_window"] = window
179 | state["slice_level"] = level
180 | else:
181 | state["slice_window_percentiles"] = (5., 95.)
182 | state["slice_window_as_percentage"] = self.convert_value_to_percentage(window)
183 | state["slice_level_as_percentage"] = self.convert_value_to_percentage(level)
184 |
185 | state["toggle_tracing"] = False
186 | state["toggle_interpolation"] = False
187 | self.cil_viewer.updatePipeline()
188 | self.html_view.update()
189 | self.layout.flush_content()
190 |
191 | def change_slice_number(self, slice_number):
192 | self.cil_viewer.setActiveSlice(slice_number)
193 | self.cil_viewer.updatePipeline()
194 | self.html_view.update()
195 |
--------------------------------------------------------------------------------
/Wrappers/Python/ccpi/web_viewer/web_app.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | import getopt
19 | import os
20 | import sys
21 |
22 | from trame.app import get_server
23 |
24 | from ccpi.web_viewer.trame_viewer2D import TrameViewer2D
25 | from ccpi.web_viewer.trame_viewer3D import TrameViewer3D
26 |
27 | server = get_server()
28 | state, ctrl = server.state, server.controller
29 |
30 | TRAME_VIEWER = None
31 | VIEWER_2D = False
32 |
33 |
34 | def reset_viewer2d():
35 | set_viewer2d(False)
36 |
37 |
38 | def set_viewer2d(new_value):
39 | global VIEWER_2D
40 | VIEWER_2D = new_value
41 |
42 |
43 | def arg_parser():
44 | """
45 | Parse the passed arguments to the current
46 | :return:
47 | """
48 | help_string = "web_app.py [optional args: -h, -d] \n" \
49 | "Args:\n" \
50 | "-h: Show this help and exit the program\n" \
51 | "-d, --2D: Use the 2D viewer instead of the 3D viewer, the default is to just use the 3D viewer."
52 | try:
53 | opts, args = getopt.getopt(sys.argv[1:], "hd", ["2D"])
54 | except getopt.GetoptError:
55 | print(help_string)
56 | sys.exit(2)
57 | for opt, arg in opts:
58 | if opt == '-h':
59 | print(help_string)
60 | elif opt in ("-d", "--2D"):
61 | global VIEWER_2D
62 | VIEWER_2D = True
63 | return data_finder()
64 |
65 |
66 | def data_finder():
67 | """
68 | Finds all files that are needed to be passed to the TrameViewer that in a list that is digestible, using the passed args.
69 | :return: list, of full file paths of data in the given directory parameter
70 | """
71 | data_files = []
72 | for index, arg in enumerate(sys.argv):
73 | if index == 0 or arg[0] == '-':
74 | # this is the python script in index 0 and not a passed arg
75 | continue
76 | if os.path.isfile(arg):
77 | data_files.append(arg)
78 | elif os.path.isdir(arg):
79 | files_in_dir = os.listdir(arg)
80 | for file in files_in_dir:
81 | data_files.append(os.path.join(arg, file))
82 | else:
83 | print(f"This arg: {arg} is not a valid file or directory. Assuming it is for trame.")
84 | return data_files
85 |
86 |
87 | def main() -> int:
88 | """
89 | Create the main class and run the TrameViewer
90 | :return: int, exit code for the program
91 | """
92 | data_files = arg_parser()
93 | global TRAME_VIEWER
94 | if not VIEWER_2D:
95 | TRAME_VIEWER = TrameViewer3D(data_files)
96 | TRAME_VIEWER.start()
97 | else:
98 | TRAME_VIEWER = TrameViewer2D(data_files)
99 | TRAME_VIEWER.start()
100 | return 0
101 |
102 |
103 | @state.change("slice")
104 | def update_slice(**kwargs):
105 | TRAME_VIEWER.change_slice_number(kwargs["slice"])
106 |
107 |
108 | @state.change("orientation")
109 | def change_orientation(**kwargs):
110 | if "orientation" in kwargs:
111 | orientation = kwargs["orientation"]
112 | if orientation is not int:
113 | orientation = int(orientation)
114 | TRAME_VIEWER.switch_to_orientation(orientation)
115 |
116 |
117 | @state.change("opacity")
118 | def change_opacity_mapping(**kwargs):
119 | if "opacity" in kwargs:
120 | TRAME_VIEWER.set_opacity_mapping(kwargs["opacity"])
121 |
122 |
123 | @state.change("file_name")
124 | def change_model(**kwargs):
125 | TRAME_VIEWER.load_file(kwargs['file_name'], kwargs.get('opacity', "scalar"))
126 |
127 |
128 | @state.change("color_map")
129 | def change_color_map(**kwargs):
130 | TRAME_VIEWER.change_color_map(kwargs['color_map'])
131 |
132 |
133 | @state.change("windowing")
134 | def change_windowing(**kwargs):
135 | TRAME_VIEWER.change_windowing(kwargs["windowing"][0], kwargs["windowing"][1], windowing_method=kwargs["opacity"])
136 |
137 |
138 | @state.change("coloring")
139 | def change_coloring(**kwargs):
140 | TRAME_VIEWER.change_coloring(kwargs["coloring"][0], kwargs["coloring"][1])
141 |
142 |
143 | @state.change("slice_window")
144 | def change_slice_window(**kwargs):
145 | TRAME_VIEWER.change_slice_window(kwargs["slice_window"])
146 |
147 |
148 | @state.change("slice_window_as_percentage")
149 | def change_slice_window_as_percentage(**kwargs):
150 | TRAME_VIEWER.change_slice_window_as_percentage(kwargs["slice_window_as_percentage"])
151 |
152 |
153 | @state.change("slice_level")
154 | def change_slice_level(**kwargs):
155 | TRAME_VIEWER.change_slice_level(kwargs["slice_level"])
156 |
157 |
158 | @state.change("slice_level_as_percentage")
159 | def change_slice_level_as_percentage(**kwargs):
160 | TRAME_VIEWER.change_slice_level_as_percentage(kwargs["slice_level_as_percentage"])
161 |
162 |
163 | @state.change("slice_window_range")
164 | def change_slice_window_level_range(**kwargs):
165 | min_window = kwargs["slice_window_range"][0]
166 | max_window = kwargs["slice_window_range"][1]
167 | TRAME_VIEWER.change_slice_window_level_range(min_window, max_window)
168 |
169 |
170 | @state.change("slice_window_percentiles")
171 | def change_slice_window_level_percentiles(**kwargs):
172 | min_window = kwargs["slice_window_percentiles"][0]
173 | max_window = kwargs["slice_window_percentiles"][1]
174 | TRAME_VIEWER.change_slice_window_level_percentiles(min_window, max_window)
175 |
176 |
177 | @state.change("slice_detailed_sliders")
178 | def change_slice_detailed_sliders(**kwargs):
179 | TRAME_VIEWER.change_window_level_detail_sliders(kwargs["slice_detailed_sliders"])
180 |
181 |
182 | @state.change("slice_visibility")
183 | def change_slice_visibility(**kwargs):
184 | TRAME_VIEWER.change_slice_visibility(kwargs["slice_visibility"])
185 |
186 |
187 | @state.change("volume_visibility")
188 | def change_volume_visibility(**kwargs):
189 | TRAME_VIEWER.change_volume_visibility(kwargs["volume_visibility"])
190 |
191 |
192 | @state.change("background_color")
193 | def change_background_color(**kwargs):
194 | TRAME_VIEWER.change_background_color(kwargs["background_color"])
195 |
196 |
197 | @state.change("toggle_clipping")
198 | def change_clipping(**kwargs):
199 | TRAME_VIEWER.change_clipping(kwargs["toggle_clipping"])
200 |
201 |
202 | @state.change("toggle_tracing")
203 | def change_tracing(**kwargs):
204 | TRAME_VIEWER.change_tracing(kwargs["toggle_tracing"])
205 |
206 |
207 | @state.change("toggle_profiling")
208 | def change_profiling(**kwargs):
209 | TRAME_VIEWER.change_profiling(kwargs["toggle_profiling"])
210 |
211 |
212 | @state.change("toggle_interpolation")
213 | def change_interpolation(**kwargs):
214 | TRAME_VIEWER.change_interpolation(kwargs["toggle_interpolation"])
215 |
216 |
217 | @state.change("show_slice_histogram")
218 | def show_slice_histogram(**kwargs):
219 | TRAME_VIEWER.show_slice_histogram(kwargs["show_slice_histogram"])
220 |
221 |
222 | if __name__ == "__main__":
223 | sys.exit(main())
224 |
--------------------------------------------------------------------------------
/Wrappers/Python/conda-recipe/bld.bat:
--------------------------------------------------------------------------------
1 |
2 | cd %RECIPE_DIR%/..
3 |
4 | pip install .
5 | if errorlevel 1 exit 1
6 |
--------------------------------------------------------------------------------
/Wrappers/Python/conda-recipe/build.sh:
--------------------------------------------------------------------------------
1 | cd $RECIPE_DIR/..
2 |
3 | pip install .
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Wrappers/Python/conda-recipe/environment.yml:
--------------------------------------------------------------------------------
1 | name: cilviewer
2 | channels:
3 | - ccpi
4 | - conda-forge
5 | dependencies:
6 | - python
7 | - numpy
8 | - vtk
9 | - importlib_metadata # [py<38]
10 | - h5py
11 | - pillow
12 | - pyyaml
13 | - schema
14 |
--------------------------------------------------------------------------------
/Wrappers/Python/conda-recipe/meta.yaml:
--------------------------------------------------------------------------------
1 | package:
2 | name: ccpi-viewer
3 | version: {{ environ.get('GIT_DESCRIBE_TAG','v')[1:] }}
4 |
5 | source:
6 | path: ../../../
7 |
8 | build:
9 | skip: True # [py==38 and np==115]
10 | preserve_egg_dir: False
11 | number: {{ GIT_DESCRIBE_NUMBER }}
12 | noarch: python
13 | entry_points:
14 | - resample = ccpi.viewer.cli.resample:main
15 | - web_cilviewer = ccpi.web_viewer.web_app:main
16 | - cilviewer = ccpi.viewer.standalone_viewer:main
17 |
18 | test:
19 | requires:
20 | - cil-data=22.0.0
21 | - pillow
22 | - pytest
23 | - pyside2
24 | - eqt>=1.0.0
25 | source_files:
26 | - ./Wrappers/Python/test
27 |
28 | commands:
29 | - python -m pytest Wrappers/Python/test # [not win]
30 |
31 | requirements:
32 | build:
33 | - python >=3.7
34 | - vtk
35 | - setuptools
36 |
37 | run:
38 | - python >=3.7
39 | - numpy
40 | - vtk
41 | - importlib_metadata # [py<38]
42 | - h5py
43 | - pyyaml
44 | - schema
45 |
46 | about:
47 | home: http://www.ccpi.ac.uk
48 | license: Apache v.2.0 license
49 | summary: 'CCPi Core Imaging Library (Viewer)'
50 |
--------------------------------------------------------------------------------
/Wrappers/Python/conda-recipe/ui_env.yml:
--------------------------------------------------------------------------------
1 | name: cilviewer
2 | channels:
3 | - ccpi
4 | - conda-forge
5 | dependencies:
6 | - pyside2
7 | - eqt>=1.0.0
8 | - cil-data=22.0.0
9 |
10 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/2Dimage_reading.py:
--------------------------------------------------------------------------------
1 | from ccpi.viewer.CILViewer2D import CILViewer2D
2 | import vtk
3 |
4 | # This example imports a 2D tiff image and tests the 2D viewer
5 | DATASET_TO_READ = [path] # insert path here
6 | if DATASET_TO_READ is not None:
7 | reader = vtk.vtkTIFFReader()
8 | reader.SetFileName(DATASET_TO_READ)
9 | reader.Update()
10 |
11 | v = CILViewer2D()
12 | print(reader.GetOutput())
13 | v.setInputData(reader.GetOutput())
14 | v.startRenderLoop()
15 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/error_observer.py:
--------------------------------------------------------------------------------
1 | import vtk
2 | from ccpi.viewer.iviewer import iviewer
3 | from ccpi.viewer.utils.conversion import cilHDF5ResampleReader
4 | from ccpi.viewer.utils.error_handling import EndObserver, ErrorObserver
5 | '''
6 | An example which shows attaching an error observer and end observer
7 | to reading a HDF5 file
8 | '''
9 |
10 | if __name__ == "__main__":
11 | hdf5_name = r'C:\Users\lhe97136\Work\Data\24737_fd_normalised.nxs'
12 | readerhdf5 = cilHDF5ResampleReader()
13 | ''' Create the error observer and attach it to the hdf5 reader. If an error occurs,
14 | it will run the callback_fn, which is given the error message as an input.
15 | So in this case it will print the error message.'''
16 | error_obs = ErrorObserver(callback_fn=print)
17 | readerhdf5.AddObserver(vtk.vtkCommand.ErrorEvent, error_obs)
18 | ''' Create the end observer and attach it to the hdf5 reader.
19 | When the function it calls finishes, it checks the given error observer to see if an error occurred.
20 | If not, it then runs the callback_fn. In this case, it displays the hdf5 image in the iviewer.'''
21 | end_obs = EndObserver(
22 | error_observer=error_obs,
23 | callback_fn=lambda: iviewer(readerhdf5.GetOutputDataObject(0), readerhdf5.GetOutputDataObject(0)))
24 | readerhdf5.AddObserver(vtk.vtkCommand.EndEvent, end_obs)
25 | readerhdf5.SetFileName(hdf5_name)
26 | ''' With this commented, we get an exception due to DatasetName not being set.
27 | Uncommenting this runs the callback_fn in the EndEvent: '''
28 | #readerhdf5.SetDatasetName("entry1/tomo_entry/data/data")
29 | readerhdf5.Update()
30 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/image_reader_and_writer.py:
--------------------------------------------------------------------------------
1 | ''''
2 | This example:
3 | 1. Writes an example dataset to a file: example_dataset.npy
4 | 2. Reads and resamples the dataset.
5 | 3. Writes out this resampled dataset and details about the original dataset to resampled_dataset.hdf5
6 | 4. prints the structure of this hdf5 file we have written out
7 | 5. Reads the resampled_dataset.hdf5
8 | 6. Writes out the resampled dataset from step 2 to a metaimage file: resampled_dataset_2.mha using the ImageWriter
9 | 7. Reads the resampled_dataset_2.mha
10 | 8. Displays the resulting datasets on the viewer.
11 |
12 | Note: to test if it works with a real dataset, set DATASET_TO_READ to a filepath containing
13 | a dataset, this will skip step 1 and read in your dataset instead.
14 |
15 | '''
16 |
17 | from ccpi.viewer.utils.io import ImageReader
18 | from ccpi.viewer.utils.io import cilviewerHDF5Reader, cilviewerHDF5Writer, ImageWriter
19 | import h5py
20 | from ccpi.viewer.iviewer import iviewer
21 | import numpy as np
22 | import vtk
23 |
24 | DATASET_TO_READ = None
25 | TARGET_SIZE = (100)**3
26 | FILE_TO_WRITE = 'resampled_dataset.hdf5'
27 | LOG_FILE = 'image_reader_and_writer.log'
28 | SECOND_FILE_TO_WRITE = 'resampled_dataset_2.mha'
29 |
30 |
31 | # --- UTILS ---------------------------------
32 | def descend_hdf5_obj(obj, sep='\t'):
33 | """
34 | Iterates through the groups in a HDF5 file (obj)
35 | and prints the groups and datasets names and
36 | datasets attributes
37 | """
38 | if type(obj) in [h5py._hl.group.Group, h5py._hl.files.File]:
39 | for key in obj.keys():
40 | print(sep, '-', key, ':', obj[key])
41 | descend_hdf5_obj(obj[key], sep=sep + '\t')
42 | elif type(obj) == h5py._hl.dataset.Dataset:
43 | for key in obj.attrs.keys():
44 | print(sep + '\t', '-', key, ':', obj.attrs[key])
45 |
46 |
47 | def print_hdf5_metadata(path, group='/'):
48 | """
49 | print HDF5 file metadata
50 | path: (str)
51 | path of hdf5 file
52 | group: (str), default: '/'
53 | a specific group to print the metadata for,
54 | defaults to the root group
55 | """
56 | with h5py.File(path, 'r') as f:
57 | descend_hdf5_obj(f[group])
58 |
59 |
60 | # ----- STEP 1 ------------------------------
61 |
62 | if DATASET_TO_READ is None:
63 | input_3D_array = np.random.random(size=(500, 100, 600))
64 | # write to NUMPY: ----------
65 | DATASET_TO_READ = 'test_3D_data.npy'
66 | np.save(DATASET_TO_READ, input_3D_array)
67 |
68 | # ----- STEP 2 ------------------------------
69 | reader = ImageReader(file_name=DATASET_TO_READ, target_size=TARGET_SIZE, resample_z=True, log_file=LOG_FILE)
70 | resampled_image = reader.Read()
71 |
72 | resampled_image_attrs = reader.GetLoadedImageAttrs()
73 | original_image_attrs = reader.GetOriginalImageAttrs()
74 |
75 | # ----- STEP 3 ------------------------------
76 | writer = cilviewerHDF5Writer()
77 | writer.SetFileName(FILE_TO_WRITE)
78 | # format='hdf5'
79 | writer.SetOriginalDataset(None, original_image_attrs)
80 | writer.AddChildDataset(resampled_image, resampled_image_attrs)
81 | writer.Write()
82 |
83 | # ---- STEP 4 --------------------------------
84 | print_hdf5_metadata(FILE_TO_WRITE)
85 |
86 | # ---- STEP 5 --------------------------------
87 | reader = cilviewerHDF5Reader()
88 | reader.SetFileName(FILE_TO_WRITE)
89 | reader.Update()
90 | read_resampled_image = reader.GetOutputDataObject(0)
91 |
92 | print(read_resampled_image.GetOrigin())
93 | print(read_resampled_image.GetSpacing())
94 |
95 | # ---- STEP 6 --------------------------------
96 | writer = ImageWriter()
97 | writer.SetFileName(SECOND_FILE_TO_WRITE)
98 | writer.SetFileFormat('mha')
99 | writer.AddChildDataset(resampled_image)
100 | writer.Write()
101 |
102 | # ---- STEP 7 --------------------------------
103 | reader = vtk.vtkMetaImageReader()
104 | reader.SetFileName(SECOND_FILE_TO_WRITE)
105 | reader.Update()
106 | read_resampled_image2 = reader.GetOutputDataObject(0)
107 |
108 | print(read_resampled_image2.GetOrigin())
109 | print(read_resampled_image2.GetSpacing())
110 |
111 | # ---- STEP 8 --------------------------------
112 | iviewer(read_resampled_image, read_resampled_image2)
113 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/read_and_resample.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import vtk
3 | import os
4 | from ccpi.viewer import viewer2D
5 | from ccpi.viewer.utils.conversion import Converter, parseNpyHeader, cilNumpyMETAImageWriter
6 | from tqdm import tqdm
7 | from vtk.util.vtkAlgorithm import VTKPythonAlgorithmBase
8 | import functools
9 | import tempfile
10 | from ccpi.viewer.utils.conversion import cilNumpyResampleReader
11 |
12 | if __name__ == "__main__":
13 | fname = os.path.abspath(r"E:\Documents\Dataset\CCPi\DVC\f000_crop\frame_000_f.npy")
14 |
15 | def progress(x, y):
16 | print("{:.0f}%".format(100 * x.GetProgress()))
17 |
18 | reader = cilNumpyResampleReader()
19 | reader.SetFileName(fname)
20 | reader.SetTargetSize(512**3)
21 | reader.AddObserver(vtk.vtkCommand.ProgressEvent, progress)
22 | reader.Update()
23 | resampled_image = reader.GetOutput()
24 | print("Spacing ", resampled_image.GetSpacing())
25 |
26 | v = viewer2D()
27 | v.setInputData(resampled_image)
28 |
29 | v.sliceActor.SetInterpolate(True)
30 |
31 | print("interpolated?", v.sliceActor.GetInterpolate())
32 | v.startRenderLoop()
33 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/rectilinearwipe.py:
--------------------------------------------------------------------------------
1 | # Example of RectilinearWipe in CILViewer
2 | # freely based on https://kitware.github.io/vtk-examples/site/Cxx/Widgets/RectilinearWipeWidget/
3 | # Author Edoardo Pasca 2021
4 |
5 | import vtk
6 | import numpy
7 | import os
8 | from ccpi.viewer.utils.conversion import Converter
9 | from vtk.util import numpy_support, vtkImageImportFromArray
10 |
11 | # Utility functions to transform numpy arrays to vtkImageData and viceversa
12 |
13 |
14 | def numpy2vtkImporter(nparray, spacing=(1., 1., 1.), origin=(0, 0, 0), transpose=[2, 1, 0]):
15 | '''Creates a vtkImageImportFromArray object and returns it.
16 |
17 | It handles the different axis order from numpy to VTK'''
18 | importer = vtkImageImportFromArray.vtkImageImportFromArray()
19 | importer.SetArray(numpy.transpose(nparray, transpose).copy())
20 | importer.SetDataSpacing(spacing)
21 | importer.SetDataOrigin(origin)
22 | return importer
23 |
24 |
25 | ren = vtk.vtkRenderer()
26 | renWin = vtk.vtkRenderWindow()
27 | renWin.AddRenderer(ren)
28 | interactor = vtk.vtkRenderWindowInteractor()
29 | interactor.SetRenderWindow(renWin)
30 |
31 | # here we load the whole dataset. It may be possible to read only part of it?
32 | data1 = numpy.load(os.path.abspath("C:/Users/ofn77899/Data/dvc/frame_000_f.npy"))
33 | data2 = numpy.load(os.path.abspath("C:/Users/ofn77899/Data/dvc/frame_010_f.npy"))
34 | img1 = Converter.numpy2vtkImage(data1, deep=0)
35 | img2 = Converter.numpy2vtkImage(data2, deep=1)
36 |
37 | reader1 = vtk.vtkExtractVOI()
38 | reader2 = vtk.vtkExtractVOI()
39 |
40 | reader1.SetInputData(img1)
41 | reader2.SetInputData(img2)
42 |
43 | extent1 = list(img1.GetExtent())
44 | extent2 = list(img2.GetExtent())
45 | #extent is slice number N
46 | N1 = 512
47 | extent1[4] = N1
48 | extent1[5] = N1
49 | extent2[4] = N1
50 | extent2[5] = N1
51 |
52 | reader1.SetVOI(*extent1)
53 | reader2.SetVOI(*extent2)
54 | # img1 = numpy.expand_dims(data[512], axis=0)
55 |
56 | # reader1 = numpy2vtkImporter(img1, transpose=[0,1,2])
57 | # # img1 = Converter.numpy2vtkImage(tmp, deep=1)
58 |
59 | # img2 = numpy.expand_dims(data[612], axis=0)
60 | # reader2 = numpy2vtkImporter(img2, transpose=[0,1,2])
61 | # img2 = Converter.numpy2vtkImage(tmp, deep=1)
62 |
63 | wipe = vtk.vtkImageRectilinearWipe()
64 | wipe.SetInputConnection(0, reader1.GetOutputPort())
65 | wipe.SetInputConnection(1, reader2.GetOutputPort())
66 | wipe.SetPosition(256, 256)
67 | wipe.SetWipe(0)
68 |
69 | wipeActor = vtk.vtkImageActor()
70 | wipeActor.GetMapper().SetInputConnection(wipe.GetOutputPort())
71 |
72 | wipeWidget = vtk.vtkRectilinearWipeWidget()
73 | wipeWidget.SetInteractor(interactor)
74 |
75 | wipeWidgetRep = wipeWidget.GetRepresentation()
76 | wipeWidgetRep.SetImageActor(wipeActor)
77 | wipeWidgetRep.SetRectilinearWipe(wipe)
78 | wipeWidgetRep.GetProperty().SetLineWidth(2.0)
79 | wipeWidgetRep.GetProperty().SetOpacity(0.75)
80 |
81 | ren.AddActor(wipeActor)
82 | renWin.SetSize(512, 512)
83 |
84 | renWin.Render()
85 | wipeWidget.On()
86 | interactor.Start()
87 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/BoxWidgetAroundSlice.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from PySide2 import QtWidgets
3 | from ccpi.viewer import viewer2D, viewer3D
4 | from SingleViewerCentralWidget import SingleViewerCenterWidget
5 | from ccpi.viewer.widgets import cilviewerBoxWidget, cilviewerLineWidget
6 |
7 | app = QtWidgets.QApplication(sys.argv)
8 | # can change the behaviour by setting which viewer you want
9 | # between viewer2D and viewer3D
10 | window = SingleViewerCenterWidget(viewer=viewer2D)
11 | line_widget = cilviewerLineWidget.CreateAtCoordOnXYPlane(window.frame.viewer, 'x', 5)
12 | line_widget.On()
13 | box_widget = cilviewerBoxWidget.CreateAroundSliceOnXYPlane(window.frame.viewer,
14 | 'y',
15 | 10,
16 | width=1,
17 | outline_color=(0, 0, 1))
18 | box_widget.On()
19 | window.frame.viewer.updatePipeline()
20 |
21 | sys.exit(app.exec_())
22 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/FourDockableLinkedViewerWidgets.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import vtk
3 | import os
4 | from PySide2 import QtCore, QtWidgets
5 | from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
6 | from ccpi.viewer.QCILRenderWindowInteractor import QCILRenderWindowInteractor
7 | from ccpi.viewer import viewer2D, viewer3D
8 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget, QCILDockableWidget
9 | # Import linking class to join 2D and 3D viewers
10 | import ccpi.viewer.viewerLinker as vlink
11 | from ccpi.viewer.utils import example_data
12 |
13 |
14 | class FourLinkedViewersDockableWidget(QtWidgets.QMainWindow):
15 |
16 | def __init__(self, parent=None, reader=None):
17 | QtWidgets.QMainWindow.__init__(self, parent)
18 | #self.resize(800,600)
19 |
20 | # create the dockable widgets with the viewer inside
21 | self.v00 = QCILDockableWidget(parent,
22 | viewer=viewer2D,
23 | shape=(600, 600),
24 | title="X",
25 | interactorStyle=vlink.Linked2DInteractorStyle)
26 | self.v01 = QCILDockableWidget(viewer=viewer2D,
27 | shape=(600, 600),
28 | title="Y",
29 | interactorStyle=vlink.Linked2DInteractorStyle)
30 | self.v10 = QCILDockableWidget(viewer=viewer2D,
31 | shape=(600, 600),
32 | title="Z",
33 | interactorStyle=vlink.Linked2DInteractorStyle)
34 | self.v11 = QCILDockableWidget(viewer=viewer3D,
35 | shape=(600, 600),
36 | title="3D",
37 | interactorStyle=vlink.Linked3DInteractorStyle)
38 |
39 | # Create the viewer linkers
40 | viewerLinkers = self.linkedViewersSetup(self.v00, self.v01, self.v10, self.v11)
41 |
42 | for linker in viewerLinkers:
43 | linker.enable()
44 |
45 | self.viewerLinkers = viewerLinkers
46 |
47 | head = example_data.HEAD.get()
48 |
49 | for el in [self.v00, self.v01, self.v10, self.v11]:
50 | el.viewer.setInputData(head)
51 | # set slice orientation
52 | self.v00.viewer.setSliceOrientation('x')
53 | self.v01.viewer.setSliceOrientation('y')
54 | self.v10.viewer.setSliceOrientation('z')
55 | # disable reslicing by the user
56 | self.v00.viewer.style.reslicing_enabled = False
57 | self.v01.viewer.style.reslicing_enabled = False
58 | self.v10.viewer.style.reslicing_enabled = False
59 |
60 | # add to the GUI
61 |
62 | self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.v00, QtCore.Qt.Vertical)
63 | self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.v01, QtCore.Qt.Vertical)
64 |
65 | self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.v10, QtCore.Qt.Vertical)
66 | self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.v11, QtCore.Qt.Vertical)
67 |
68 | self.show()
69 |
70 | def linkedViewersSetup(self, *args):
71 | linked = [viewer for viewer in args]
72 | # link all combination of viewers
73 | #pairs = [(linked[0],linked[i]) for i in range(1, len(linked))]
74 | pairs = []
75 | for i in range(len(linked)):
76 | for j in range(len(linked)):
77 | if not i == j:
78 | pairs.append((linked[i], linked[j]))
79 |
80 | linkers = []
81 | for pair in pairs:
82 | v2d = pair[0].viewer
83 | v3d = pair[1].viewer
84 | link2D3D = vlink.ViewerLinker(v2d, v3d)
85 | link2D3D.setLinkPan(False)
86 | link2D3D.setLinkZoom(False)
87 | link2D3D.setLinkWindowLevel(True)
88 | link2D3D.setLinkSlice(False)
89 | linkers.append(link2D3D)
90 | return linkers
91 |
92 |
93 | if __name__ == "__main__":
94 | err = vtk.vtkFileOutputWindow()
95 | err.SetFileName("viewer.log")
96 | vtk.vtkOutputWindow.SetInstance(err)
97 |
98 | app = QtWidgets.QApplication(sys.argv)
99 |
100 | reader = vtk.vtkNIFTIImageReader()
101 | data_dir = os.path.abspath(
102 | 'C:/Users/ofn77899/Documents/Projects/PETMR/Publications/2020RS_MCIR/cluster_test/recons')
103 |
104 | reader.SetFileName(
105 | os.path.join(
106 | data_dir,
107 | 'ungated_Reg-FGP_TV-alpha5.0_nGates1_nSubsets1_pdhg_wPrecond_gamma1.0_wAC_wNorm_wRands-riters100_noMotion_iters_39.nii'
108 | ))
109 | window = FourLinkedViewersDockableWidget(reader=None)
110 |
111 | sys.exit(app.exec_())
112 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/SingleViewerCentralWidget.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import vtk
3 | from PySide2 import QtCore, QtWidgets
4 | from ccpi.viewer import viewer2D, viewer3D
5 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget
6 | from ccpi.viewer.utils import example_data
7 |
8 |
9 | class SingleViewerCenterWidget(QtWidgets.QMainWindow):
10 |
11 | def __init__(self, parent=None, viewer=viewer2D):
12 | QtWidgets.QMainWindow.__init__(self, parent)
13 |
14 | self.frame = QCILViewerWidget(parent, viewer=viewer, shape=(600, 600))
15 |
16 | head = example_data.HEAD.get()
17 |
18 | self.frame.viewer.setInputData(head)
19 |
20 | self.setCentralWidget(self.frame)
21 |
22 | self.show()
23 |
24 |
25 | if __name__ == "__main__":
26 |
27 | app = QtWidgets.QApplication(sys.argv)
28 | # can change the behaviour by setting which viewer you want
29 | # between viewer2D and viewer3D
30 | window = SingleViewerCenterWidget(viewer=viewer3D)
31 |
32 | sys.exit(app.exec_())
33 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/SingleViewerCentralWidget2.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import vtk
3 | from PySide2 import QtCore, QtWidgets
4 | from ccpi.viewer import viewer2D, viewer3D
5 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget
6 | import os
7 | from ccpi.viewer.utils import example_data
8 |
9 |
10 | class SingleViewerCenterWidget(QtWidgets.QMainWindow):
11 |
12 | def __init__(self, parent=None, viewer=viewer2D):
13 | QtWidgets.QMainWindow.__init__(self, parent)
14 | self.setGeometry(450, 250, 1000, 1000)
15 | self.frame = QCILViewerWidget(parent, viewer=viewer, shape=(2000, 2000))
16 |
17 | head = example_data.HEAD.get()
18 |
19 | self.frame.viewer.setInputData(head)
20 | self.frame.viewer.setInputData2(head)
21 |
22 | self.setCentralWidget(self.frame)
23 |
24 | self.show()
25 |
26 |
27 | if __name__ == "__main__":
28 |
29 | app = QtWidgets.QApplication(sys.argv)
30 | # can change the behaviour by setting which viewer you want
31 | # between viewer2D and viewer3D
32 | window = SingleViewerCenterWidget(viewer=viewer2D)
33 |
34 | sys.exit(app.exec_())
35 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/TwoLinkedViewersCentralWidget.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import vtk
3 | from PySide2 import QtCore, QtWidgets
4 | from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
5 | from ccpi.viewer.QCILRenderWindowInteractor import QCILRenderWindowInteractor
6 | from ccpi.viewer import viewer2D, viewer3D
7 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget
8 | # Import linking class to join 2D and 3D viewers
9 | import ccpi.viewer.viewerLinker as vlink
10 | from ccpi.viewer.utils import example_data
11 |
12 |
13 | class TwoLinkedViewersCenterWidget(QtWidgets.QMainWindow):
14 |
15 | def __init__(self, parent=None):
16 | QtWidgets.QMainWindow.__init__(self, parent)
17 | #self.resize(800,600)
18 |
19 | self.frame1 = QCILViewerWidget(parent,
20 | viewer=viewer2D,
21 | shape=(600, 600),
22 | interactorStyle=vlink.Linked2DInteractorStyle)
23 | self.frame2 = QCILViewerWidget(parent,
24 | viewer=viewer3D,
25 | shape=(600, 600),
26 | interactorStyle=vlink.Linked3DInteractorStyle)
27 |
28 | head = example_data.HEAD.get()
29 |
30 | self.frame1.viewer.setInputData(head)
31 | self.frame2.viewer.setInputData(head)
32 |
33 | # Initially link viewers
34 | self.linkedViewersSetup()
35 | self.link2D3D.enable()
36 |
37 | layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight)
38 | layout.addWidget(self.frame1)
39 | layout.addWidget(self.frame2)
40 |
41 | cw = QtWidgets.QWidget()
42 | cw.setLayout(layout)
43 | self.setCentralWidget(cw)
44 | self.central_widget = cw
45 | self.show()
46 |
47 | def linkedViewersSetup(self):
48 | v2d = self.frame1.viewer
49 | v3d = self.frame2.viewer
50 | self.link2D3D = vlink.ViewerLinker(v2d, v3d)
51 | self.link2D3D.setLinkPan(False)
52 | self.link2D3D.setLinkZoom(False)
53 | self.link2D3D.setLinkWindowLevel(True)
54 | self.link2D3D.setLinkSlice(True)
55 |
56 |
57 | if __name__ == "__main__":
58 |
59 | app = QtWidgets.QApplication(sys.argv)
60 |
61 | #window = MainWindow3()
62 | window = TwoLinkedViewersCenterWidget()
63 |
64 | sys.exit(app.exec_())
65 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/ui_examples/opacity_in_viewer.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import vtk
4 | from ccpi.viewer import viewer3D
5 | from ccpi.viewer.iviewer import SingleViewerCenterWidget
6 | from ccpi.viewer.QCILViewerWidget import QCILViewerWidget
7 | from eqt.ui.UIFormWidget import FormDockWidget
8 | from PySide2 import QtWidgets
9 | from PySide2.QtCore import Qt
10 | from PySide2.QtWidgets import QComboBox
11 | from ccpi.viewer.utils import example_data
12 |
13 | try:
14 | import qdarkstyle
15 | from qdarkstyle.dark.palette import DarkPalette
16 | set_style = True
17 | except:
18 | set_style = False
19 |
20 |
21 | class OpacityViewerWidget(SingleViewerCenterWidget):
22 |
23 | def __init__(self, parent=None, viewer=viewer3D):
24 | SingleViewerCenterWidget.__init__(self, parent)
25 |
26 | self.frame = QCILViewerWidget(parent, viewer=viewer, shape=(600, 600))
27 |
28 | self.setCentralWidget(self.frame)
29 |
30 | self.create_settings_dockwidget()
31 |
32 | if set_style:
33 | self.set_app_style()
34 |
35 | self.show()
36 |
37 | def set_input(self, data):
38 | self.frame.viewer.setInputData(data)
39 |
40 | def create_settings_dockwidget(self):
41 | form_dock_widget = FormDockWidget(title='')
42 | drop_down = QComboBox()
43 | drop_down.addItems(['gradient', 'scalar'])
44 | drop_down.currentTextChanged.connect(
45 | lambda: self.frame.viewer.setVolumeRenderOpacityMethod(drop_down.currentText()))
46 | form_dock_widget.addWidget(drop_down, "Select Opacity Method:", 'select_opacity')
47 | self.addDockWidget(Qt.TopDockWidgetArea, form_dock_widget)
48 |
49 | def set_app_style(self):
50 | '''Sets app stylesheet '''
51 | style = qdarkstyle.load_stylesheet(palette=DarkPalette)
52 | self.setStyleSheet(style)
53 |
54 |
55 | class viewer_window(object):
56 | '''
57 | a Qt interactive viewer with one single dataset
58 | Parameters
59 | ----------
60 | data: vtkImageData
61 | image to be displayed
62 | '''
63 |
64 | def __init__(self, data):
65 | '''Creator'''
66 | app = QtWidgets.QApplication(sys.argv)
67 | self.app = app
68 |
69 | self.setUp(data)
70 | self.show()
71 |
72 | def setUp(self, data):
73 | window = OpacityViewerWidget()
74 | window.set_input(data)
75 | self.window = window
76 | self.has_run = None
77 |
78 | def show(self):
79 | if self.has_run is None:
80 | self.has_run = self.app.exec_()
81 | else:
82 | print('No instance can be run interactively again. Delete and re-instantiate.')
83 |
84 |
85 | if __name__ == "__main__":
86 |
87 | err = vtk.vtkFileOutputWindow()
88 | err.SetFileName("viewer.log")
89 | vtk.vtkOutputWindow.SetInstance(err)
90 |
91 | # To use your own dataset:
92 | # reader = vtk.vtkMetaImageReader()
93 | # reader.SetFileName(r'head.mha')
94 | # reader.Update()
95 | # viewer_window(reader.GetOutput())
96 |
97 | data = example_data.HEAD.get()
98 | viewer_window(data)
99 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/viewer2D-without-qt.py:
--------------------------------------------------------------------------------
1 | from ccpi.viewer import viewer2D
2 | from ccpi.viewer.utils import example_data
3 |
4 | v = viewer2D()
5 | # Load head data
6 | data = example_data.HEAD.get()
7 | v.setInputData(data)
8 | v.startRenderLoop()
9 |
--------------------------------------------------------------------------------
/Wrappers/Python/examples/viewer3D-without-qt.py:
--------------------------------------------------------------------------------
1 | from ccpi.viewer import viewer3D
2 | from ccpi.viewer.utils import example_data
3 |
4 | v = viewer3D()
5 | # Load head data
6 | data = example_data.HEAD.get()
7 | v.setInputData(data)
8 | v.startRenderLoop()
--------------------------------------------------------------------------------
/Wrappers/Python/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright 2017 Edoardo Pasca
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | from setuptools import setup
17 | import os
18 | import subprocess
19 |
20 |
21 | def version2pep440(version):
22 | """normalises the version from git describe to pep440
23 |
24 | https://www.python.org/dev/peps/pep-0440/#id29
25 | """
26 | if version[0] == "v":
27 | version = version[1:]
28 |
29 | if u"-" in version:
30 | v = version.split("-")
31 | v_pep440 = "{}.dev{}".format(v[0], v[1])
32 | else:
33 | v_pep440 = version
34 |
35 | return v_pep440
36 |
37 |
38 | git_version_string = subprocess.check_output("git describe", shell=True).decode("utf-8").rstrip()[1:]
39 |
40 | if os.environ.get("CONDA_BUILD", 0) == "1":
41 | cwd = os.path.join(os.environ.get("RECIPE_DIR"), "..")
42 | # requirements are processed by conda
43 | requires = []
44 | else:
45 | requires = ["numpy", "vtk"]
46 | cwd = os.getcwd()
47 |
48 | version = version2pep440(git_version_string)
49 |
50 | # update the version string
51 | fname = os.path.join(cwd, "ccpi", "viewer", "version.py")
52 |
53 | if os.path.exists(fname):
54 | os.remove(fname)
55 | with open(fname, "w") as f:
56 | f.write("version = '{}'".format(version))
57 |
58 | setup(
59 | name="ccpi-viewer",
60 | version=version,
61 | packages=[
62 | "ccpi", "ccpi.viewer", "ccpi.viewer.utils", "ccpi.web_viewer", "ccpi.viewer.widgets", "ccpi.viewer.cli",
63 | "ccpi.viewer.ui"
64 | ],
65 | install_requires=requires,
66 | zip_safe=False,
67 | # metadata for upload to PyPI
68 | author="Edoardo Pasca",
69 | author_email="edoardo.pasca@stfc.ac.uk",
70 | description="CCPi CILViewer",
71 | license="Apache v2.0",
72 | keywords="3D data viewer",
73 | url="http://www.ccpi.ac.uk", # project home page, if any
74 | entry_points={
75 | "console_scripts": [
76 | "resample = ccpi.viewer.cli.resample:main", "web_cilviewer = ccpi.web_viewer.web_app:main",
77 | "cilviewer = ccpi.viewer.standalone_viewer:main"
78 | ]
79 | },
80 | )
81 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TomographicImaging/CILViewer/65c59499e25fc3a6131bae78201e83c097e0ce2b/Wrappers/Python/test/__init__.py
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_CILViewer3D.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 STFC, United Kingdom Research and Innovation
2 | #
3 | # Author 2023 Laura Murgatroyd
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 | import os
18 | import unittest
19 | from unittest import mock
20 |
21 | from ccpi.viewer.CILViewer import CILViewer
22 |
23 | # skip the tests on GitHub actions
24 | if os.environ.get('CONDA_BUILD', '0') == '1':
25 | skip_test = True
26 | else:
27 | skip_test = False
28 |
29 | print("skip_test is set to ", skip_test)
30 |
31 |
32 | @unittest.skipIf(skip_test, "Skipping tests on GitHub Actions")
33 | class CILViewer3DTest(unittest.TestCase):
34 |
35 | def setUp(self):
36 | self.cil_viewer = CILViewer()
37 |
38 | def test_getGradientOpacityPercentiles_returns_correct_percentiles_when_image_values_start_at_zero(self):
39 | self.cil_viewer.gradient_opacity_limits = [40, 50]
40 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[0, 50])
41 | expected_percentages = (80.0, 100.0)
42 | actual_percentages = self.cil_viewer.getGradientOpacityPercentiles()
43 | self.assertEqual(expected_percentages, actual_percentages)
44 |
45 | def test_getGradientOpacityPercentiles_returns_correct_percentiles_when_image_values_start_at_non_zero(self):
46 | self.cil_viewer.gradient_opacity_limits = [40, 50]
47 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[10, 50])
48 | expected_percentages = (75.0, 100.0)
49 | actual_percentages = self.cil_viewer.getGradientOpacityPercentiles()
50 | self.assertEqual(expected_percentages, actual_percentages)
51 |
52 | def test_getScalarOpacityPercentiles_returns_correct_percentiles_when_image_values_start_at_zero(self):
53 | self.cil_viewer.scalar_opacity_limits = [40, 50]
54 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[0, 50])
55 | expected_percentages = (80.0, 100.0)
56 | actual_percentages = self.cil_viewer.getScalarOpacityPercentiles()
57 | self.assertEqual(expected_percentages, actual_percentages)
58 |
59 | def test_getScalarOpacityPercentiles_returns_correct_percentiles_when_image_values_start_at_non_zero(self):
60 | self.cil_viewer.scalar_opacity_limits = [40, 50]
61 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[10, 50])
62 | expected_percentages = (75.0, 100.0)
63 | actual_percentages = self.cil_viewer.getScalarOpacityPercentiles()
64 | self.assertEqual(expected_percentages, actual_percentages)
65 |
66 | def test_getVolumeColorPercentiles_returns_correct_percentiles_when_image_values_start_at_zero(self):
67 | self.cil_viewer.volume_colormap_limits = [40, 50]
68 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[0, 50])
69 | expected_percentages = (80.0, 100.0)
70 | actual_percentages = self.cil_viewer.getVolumeColorPercentiles()
71 | self.assertEqual(expected_percentages, actual_percentages)
72 |
73 | def test_getVolumeColorPercentiles_returns_correct_percentiles_when_image_values_start_at_non_zero(self):
74 | self.cil_viewer.volume_colormap_limits = [40, 50]
75 | self.cil_viewer.getImageMapWholeRange = mock.MagicMock(return_value=[10, 50])
76 | expected_percentages = (75.0, 100.0)
77 | actual_percentages = self.cil_viewer.getVolumeColorPercentiles()
78 | self.assertEqual(expected_percentages, actual_percentages)
79 |
80 |
81 | if __name__ == '__main__':
82 | unittest.main()
83 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_CILViewerBase.py:
--------------------------------------------------------------------------------
1 | # Copyright 2023 STFC, United Kingdom Research and Innovation
2 | #
3 | # Author 2023 Laura Murgatroyd
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 | import unittest
18 | from unittest import mock
19 | import os
20 |
21 | from ccpi.viewer.CILViewer import CILViewerBase
22 |
23 | # skip the tests on GitHub actions
24 | if os.environ.get('CONDA_BUILD', '0') == '1':
25 | skip_test = True
26 | else:
27 | skip_test = False
28 |
29 | print("skip_test is set to ", skip_test)
30 |
31 |
32 | @unittest.skipIf(skip_test, "Skipping tests on GitHub Actions")
33 | class CILViewerBaseTest(unittest.TestCase):
34 |
35 | def setUp(self):
36 | '''Creates an instance of the CIL viewer base class.'''
37 | self.CILViewerBase_instance = CILViewerBase()
38 |
39 | def test_setAxisLabels(self):
40 | '''Edits the labels in the axes widget and checks they have been set correctly.
41 | The test is performed with overwrite flag set to default, True, or False.'''
42 | labels = ['a', 'b', 'c']
43 | self.CILViewerBase_instance.setAxisLabels(labels)
44 | new_labels = self.CILViewerBase_instance.getCurrentAxisLabelsText()
45 | self.assertEqual(new_labels, labels)
46 | self.assertEqual(self.CILViewerBase_instance.axisLabelsText, labels)
47 |
48 | labels_2 = ['c', 'd', 'e']
49 | self.CILViewerBase_instance.setAxisLabels(labels_2, False)
50 | new_labels = self.CILViewerBase_instance.getCurrentAxisLabelsText()
51 | self.assertEqual(new_labels, labels_2)
52 | self.assertEqual(self.CILViewerBase_instance.axisLabelsText, labels)
53 |
54 |
55 | @unittest.skipIf(skip_test, "Skipping tests on GitHub Actions")
56 | class CILViewer3DTest(unittest.TestCase):
57 |
58 | def setUp(self):
59 | self.cil_viewer = CILViewerBase()
60 |
61 | def test_getSliceColorPercentiles_returns_correct_percentiles_when_slice_values_start_at_zero(self):
62 | self.cil_viewer.getSliceMapRange = mock.MagicMock(return_value=[40, 50])
63 | self.cil_viewer.getSliceMapWholeRange = mock.MagicMock(return_value=[0, 50])
64 | expected_percentages = (80.0, 100.0)
65 | actual_percentages = self.cil_viewer.getSliceColorPercentiles()
66 | self.assertEqual(expected_percentages, actual_percentages)
67 |
68 | def test_getSliceColorPercentiles_returns_correct_percentiles_when_slice_values_start_at_non_zero(self):
69 | self.cil_viewer.getSliceMapRange = mock.MagicMock(return_value=[40, 50])
70 | self.cil_viewer.getSliceMapWholeRange = mock.MagicMock(return_value=[10, 50])
71 | expected_percentages = (75.0, 100.0)
72 | actual_percentages = self.cil_viewer.getSliceColorPercentiles()
73 | self.assertEqual(expected_percentages, actual_percentages)
74 |
75 | def test_multiple_widgets_cant_be_saved_with_same_name(self):
76 | widget1 = mock.MagicMock()
77 | widget2 = mock.MagicMock()
78 | widget_name = "widget1"
79 | self.cil_viewer.addWidgetReference(widget1, widget_name)
80 | with self.assertRaises(ValueError):
81 | self.cil_viewer.addWidgetReference(widget2, widget_name)
82 |
83 |
84 | if __name__ == '__main__':
85 | unittest.main()
86 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_camera_data.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 STFC, United Kingdom Research and Innovation
3 | #
4 | # Author 2022 Samuel Jones
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | import unittest
19 |
20 | import vtk
21 |
22 | from ccpi.viewer.utils import CameraData
23 |
24 |
25 | class CameraDataTest(unittest.TestCase):
26 |
27 | def setUp(self):
28 | self.camera = vtk.vtkCamera()
29 | self.cam_pos = (1., 1., 1.)
30 | self.focal_pos = (2., 2., 2.)
31 | self.view_up = (0.57735026, 0.57735026, 0.57735026)
32 | self.camera.SetPosition(*self.cam_pos)
33 | self.camera.SetFocalPoint(*self.focal_pos)
34 | self.camera.SetViewUp(*self.view_up)
35 |
36 | self.data = CameraData(self.camera)
37 |
38 | def test_init_sets_all_values(self):
39 | self.assertEqual(self.data.position, self.cam_pos)
40 | self.assertEqual(self.data.focalPoint, self.focal_pos)
41 | self.assertAlmostEqual(self.data.viewUp[0], self.view_up[0])
42 | self.assertAlmostEqual(self.data.viewUp[1], self.view_up[1])
43 | self.assertAlmostEqual(self.data.viewUp[2], self.view_up[2])
44 |
45 | def test_copy_data_to_camera_does_so(self):
46 | camera_to_copy_to = vtk.vtkCamera()
47 |
48 | self.assertNotEqual(self.cam_pos, camera_to_copy_to.GetPosition())
49 | self.assertNotEqual(self.focal_pos, camera_to_copy_to.GetFocalPoint())
50 | self.assertNotEqual(self.view_up, camera_to_copy_to.GetViewUp())
51 |
52 | CameraData.CopyDataToCamera(self.data, camera_to_copy_to)
53 |
54 | self.assertEqual(self.cam_pos, camera_to_copy_to.GetPosition())
55 | self.assertEqual(self.focal_pos, camera_to_copy_to.GetFocalPoint())
56 | self.assertAlmostEqual(self.view_up[0], camera_to_copy_to.GetViewUp()[0])
57 | self.assertAlmostEqual(self.view_up[1], camera_to_copy_to.GetViewUp()[1])
58 | self.assertAlmostEqual(self.view_up[2], camera_to_copy_to.GetViewUp()[2])
59 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_cli_resample.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | import numpy as np
5 | import os
6 | import unittest
7 |
8 | import h5py
9 | import numpy as np
10 | import vtk
11 | from ccpi.viewer.utils.conversion import Converter
12 | from ccpi.viewer.utils.io import cilviewerHDF5Reader
13 | import yaml
14 |
15 | from os import system
16 |
17 |
18 | def calculate_target_downsample_shape(max_size, total_size, shape, acq=False):
19 | if not acq:
20 | xy_axes_magnification = np.power(max_size / total_size, 1 / 3)
21 | slice_per_chunk = int(1 / xy_axes_magnification)
22 | else:
23 | slice_per_chunk = 1
24 | xy_axes_magnification = np.power(max_size / total_size, 1 / 2)
25 | num_chunks = 1 + len([i for i in range(slice_per_chunk, shape[2], slice_per_chunk)])
26 |
27 | target_image_shape = (int(xy_axes_magnification * shape[0]), int(xy_axes_magnification * shape[1]), num_chunks)
28 | return target_image_shape
29 |
30 |
31 | class TestCLIResample(unittest.TestCase):
32 |
33 | def setUp(self):
34 | # Generate random 3D array:
35 | bits = 16
36 | self.bytes_per_element = int(bits / 8)
37 | self.input_3D_array = np.random.randint(10, size=(5, 10, 6), dtype=eval(f"np.uint{bits}"))
38 |
39 | # write to HDF5: -----------
40 | self.hdf5_filename_3D = 'test_3D_data.h5'
41 | self.hdf5_yaml_filename = 'test_hdf5.yaml'
42 | with h5py.File(self.hdf5_filename_3D, 'w') as f:
43 | f.create_dataset('/entry1/tomo_entry/data/data', data=self.input_3D_array)
44 |
45 | self.hdf5_dict = {
46 | 'input': {
47 | 'file_name': self.hdf5_filename_3D,
48 | 'dataset_name': '/entry1/tomo_entry/data/data'
49 | },
50 | 'resample': {
51 | 'target_size': 100e-6,
52 | 'resample_z': False
53 | },
54 | 'output': {
55 | 'file_name': 'test_hdf5_out.hdf5',
56 | 'format': 'hdf5'
57 | }
58 | }
59 | with open(self.hdf5_yaml_filename, 'w') as file:
60 | yaml.dump(self.hdf5_dict, file)
61 |
62 | # write to raw: -------------
63 | bytes_3D_array = bytes(self.input_3D_array)
64 | self.raw_filename_3D = 'test_3D_data.raw'
65 | self.raw_yaml_filename = 'test_raw.yaml'
66 | with open(self.raw_filename_3D, 'wb') as f:
67 | f.write(bytes_3D_array)
68 | self.raw_dict = {
69 | 'input': {
70 | 'file_name': self.raw_filename_3D,
71 | 'shape': str(np.shape(self.input_3D_array)),
72 | 'is_fortran': False,
73 | 'is_big_endian': False,
74 | 'typecode': str(self.input_3D_array.dtype)
75 | },
76 | 'resample': {
77 | 'target_size': 100e-6,
78 | 'resample_z': False
79 | },
80 | 'output': {
81 | 'file_name': 'test_raw_out.hdf5',
82 | 'format': 'hdf5'
83 | }
84 | }
85 | with open(self.raw_yaml_filename, 'w') as file:
86 | yaml.dump(self.raw_dict, file)
87 |
88 | def _test_resampling_acq_data(self, reader, target_size):
89 | og_shape = np.shape(self.input_3D_array)
90 | og_shape = (og_shape[2], og_shape[1], og_shape[0])
91 | og_size = og_shape[0] * og_shape[1] * og_shape[2] * self.bytes_per_element
92 | reader.Update()
93 | image = reader.GetOutputDataObject(0)
94 | extent = image.GetExtent()
95 |
96 | shape = calculate_target_downsample_shape(target_size, og_size, og_shape, acq=True)
97 | expected_size = shape[0] * shape[1] * shape[2]
98 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
99 | resulting_size = resulting_shape[0] * \
100 | resulting_shape[1]*resulting_shape[2]
101 | # angle (z direction) is first index in numpy array, and in cil
102 | # but it is the last in vtk.
103 | resulting_z_shape = extent[5] + 1
104 | og_z_shape = np.shape(self.input_3D_array)[0]
105 |
106 | self.assertEqual(resulting_size, expected_size)
107 | self.assertEqual(resulting_z_shape, og_z_shape)
108 |
109 | def test_resample_with_yaml(self):
110 | dicts = [self.hdf5_dict, self.raw_dict]
111 | filenames = [self.hdf5_yaml_filename, self.raw_yaml_filename]
112 | subtest_labels = ['HDF5', 'raw']
113 | for i, dict in enumerate(dicts):
114 | with self.subTest(format=subtest_labels[i]):
115 | yaml_file = filenames[i]
116 |
117 | # Tests image with correct target size is generated by resample reader:
118 | system('resample -f {}'.format(yaml_file))
119 |
120 | reader = cilviewerHDF5Reader()
121 | reader.SetFileName(dict['output']['file_name'])
122 | target_size = int(dict['resample']['target_size'] * 1e6)
123 | self._test_resampling_acq_data(reader, target_size)
124 |
125 | def test_resample_command_line_hdf5(self):
126 | dict = self.hdf5_dict
127 |
128 | # Tests image with correct target size is generated by resample reader:
129 | input_file = dict['input']['file_name']
130 | target_size = dict['resample']['target_size']
131 | resample_z = dict['resample']['resample_z']
132 | out = dict['output']['file_name']
133 | out_format = dict['output']['format']
134 |
135 | # specific to hdf5:
136 | dset_name = dict['input']['dataset_name']
137 |
138 | command = f'resample -i {input_file} --dataset_name {dset_name} -o {out} -target_size {target_size} --resample_z {resample_z} --out_format {out_format}'
139 |
140 | if system(command) != 0:
141 | raise Exception("Error running test_resample_command_line_hdf5")
142 |
143 | reader = cilviewerHDF5Reader()
144 | reader.SetFileName(dict['output']['file_name'])
145 |
146 | target_size = int(target_size * 1e6)
147 | self._test_resampling_acq_data(reader, target_size)
148 |
149 | def test_resample_command_line_raw(self):
150 | dict = self.raw_dict
151 |
152 | # Tests image with correct target size is generated by resample reader:
153 | input_file = dict['input']['file_name']
154 | target_size = dict['resample']['target_size']
155 | resample_z = dict['resample']['resample_z']
156 | out = dict['output']['file_name']
157 | out_format = dict['output']['format']
158 |
159 | # specific to raw:
160 | shape = list(eval(dict['input']['shape']))
161 | shape = f"{shape[0]},{shape[1]},{shape[2]}"
162 | is_fortran = dict['input']['is_fortran']
163 | # is_fortran = False
164 | is_big_endian = dict['input']['is_big_endian']
165 | typecode = dict['input']['typecode']
166 |
167 | command = f'resample -i {input_file} --shape {shape} --is_fortran {is_fortran} --is_big_endian {is_big_endian} --typecode {typecode} -o {out} -target_size {target_size} --resample_z {resample_z} --out_format {out_format}'
168 |
169 | if system(command) != 0:
170 | raise Exception("Error running test_resample_command_line_raw")
171 |
172 | reader = cilviewerHDF5Reader()
173 | reader.SetFileName(dict['output']['file_name'])
174 |
175 | target_size = int(target_size * 1e6)
176 | self._test_resampling_acq_data(reader, target_size)
177 |
178 | def tearDown(self):
179 | files = [self.hdf5_filename_3D, self.hdf5_yaml_filename, self.raw_filename_3D, self.raw_yaml_filename]
180 | for f in files:
181 | os.remove(f)
182 |
183 |
184 | if __name__ == '__main__':
185 | unittest.main()
186 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_conversion.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | import numpy as np
5 | import vtk
6 | from ccpi.viewer.utils.conversion import (Converter, cilRawResampleReader, cilMetaImageResampleReader,
7 | cilNumpyResampleReader, cilNumpyMETAImageWriter)
8 |
9 | import numpy as np
10 | '''
11 | This will test parts of the utils/conversion.py file other than
12 | the Resample and Cropped readers. (See test_cropped_readers.py
13 | and test_resample_readers.py for tests of these)
14 |
15 | '''
16 |
17 |
18 | class TestConversion(unittest.TestCase):
19 |
20 | def setUp(self):
21 | # Generate random 3D array and write to HDF5:
22 | np.random.seed(1)
23 | self.input_3D_array = np.random.randint(10, size=(5, 10, 6), dtype=np.uint8)
24 | bytes_3D_array = bytes(self.input_3D_array)
25 | self.raw_filename_3D = 'test_3D_data.raw'
26 | with open(self.raw_filename_3D, 'wb') as f:
27 | f.write(bytes_3D_array)
28 |
29 | def test_WriteMETAImageHeader(self):
30 | '''writes a mhd file to go with a raw
31 | datafile, using cilNumpyMETAImageWriter.WriteMETAImageHeader and then tests if this can
32 | be read successfully with vtk.vtkMetaImageReader
33 | by comparing to array read with cilRawResampleReader
34 | directly from RawResampleReader and the original contents'''
35 |
36 | # read raw file's info:
37 | data_filename = self.raw_filename_3D
38 | header_filename = 'raw_header.mhd'
39 | typecode = 'uint8'
40 | big_endian = False
41 | header_length = 0
42 | shape = np.shape(self.input_3D_array)
43 | shape_to_write = shape[::-1] # because it is not a fortran order array we have to swap
44 | cilNumpyMETAImageWriter.WriteMETAImageHeader(data_filename,
45 | header_filename,
46 | typecode,
47 | big_endian,
48 | header_length,
49 | shape_to_write,
50 | spacing=(1., 1., 1.),
51 | origin=(0., 0., 0.))
52 |
53 | reader = vtk.vtkMetaImageReader()
54 | reader.SetFileName(header_filename)
55 | reader.Update()
56 | read_mhd_raw = Converter.vtk2numpy(reader.GetOutput())
57 |
58 | reader = cilRawResampleReader()
59 | target_size = int(1e12)
60 | reader.SetTargetSize(target_size)
61 | reader.SetBigEndian(False)
62 | reader.SetIsFortran(False)
63 | reader.SetFileName(self.raw_filename_3D)
64 | raw_type_code = str(self.input_3D_array.dtype)
65 | reader.SetTypeCodeName(raw_type_code)
66 | reader.SetStoredArrayShape(shape)
67 | reader.Update()
68 |
69 | image = reader.GetOutput()
70 | raw_array = Converter.vtk2numpy(image)
71 |
72 | np.testing.assert_array_equal(read_mhd_raw, raw_array)
73 | np.testing.assert_array_equal(read_mhd_raw, self.input_3D_array)
74 |
75 | def tearDown(self):
76 | files = [self.raw_filename_3D]
77 | for f in files:
78 | os.remove(f)
79 |
80 |
81 | if __name__ == '__main__':
82 | unittest.main()
83 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_cropped_readers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | import numpy as np
5 | import vtk
6 | from ccpi.viewer.utils.conversion import (Converter, cilRawCroppedReader, cilMetaImageCroppedReader,
7 | cilNumpyCroppedReader, cilTIFFCroppedReader)
8 |
9 |
10 | class TestCroppedReaders(unittest.TestCase):
11 |
12 | def setUp(self):
13 | # Generate random 3D array and write to HDF5:
14 | np.random.seed(1)
15 | shape = (5, 4, 6) # was 10 times larger
16 | bits = 8
17 | self.input_3D_array = np.random.randint(10, size=shape, dtype=eval(f"np.uint{bits}"))
18 | self.input_3D_array = np.reshape(np.arange(self.input_3D_array.size),
19 | newshape=shape).astype(dtype=eval(f"np.uint{bits}"))
20 | self.raw_type_code = str(self.input_3D_array.dtype)
21 | bytes_3D_array = bytes(self.input_3D_array)
22 | self.raw_filename_3D = 'test_3D_data.raw'
23 | with open(self.raw_filename_3D, 'wb') as f:
24 | f.write(bytes_3D_array)
25 |
26 | self.numpy_filename_3D = 'test_3D_data.npy'
27 | np.save(self.numpy_filename_3D, self.input_3D_array)
28 |
29 | self.meta_filename_3D = 'test_3D_data.mha'
30 | vtk_image = Converter.numpy2vtkImage(self.input_3D_array)
31 | writer = vtk.vtkMetaImageWriter()
32 | writer.SetFileName(self.meta_filename_3D)
33 | writer.SetInputData(vtk_image)
34 | writer.SetCompression(False)
35 | writer.Write()
36 | # Write TIFFs
37 | fnames = []
38 | arr = self.input_3D_array
39 | from PIL import Image
40 | for i in range(arr.shape[0]):
41 | fname = 'tiff_test_file_{:03d}.tiff'.format(i)
42 | fnames.append(os.path.abspath(fname))
43 | # Using vtk the Y axis gets reversed
44 | # vtk_image = Converter.numpy2vtkImage(np.expand_dims(arr[i,:,:], axis=0))
45 | # twriter.SetFileName(fnames[-1])
46 | # twriter.SetInputData(vtk_image)
47 | # twriter.Write()
48 | im = Image.fromarray(arr[i])
49 | im.save(fnames[-1])
50 |
51 | self.tiff_fnames = fnames
52 |
53 | def check_extent(self, reader, target_z_extent):
54 | reader.Update()
55 | image = reader.GetOutput()
56 | extent = list(image.GetExtent())
57 | og_shape = np.shape(self.input_3D_array)
58 | og_extent = [0, og_shape[2] - 1, 0, og_shape[1] - 1, 0, og_shape[0] - 1]
59 | expected_extent = og_extent
60 | expected_extent[4] = target_z_extent[0]
61 | expected_extent[5] = target_z_extent[1]
62 | self.assertEqual(extent, expected_extent)
63 |
64 | def check_values(self, target_z_extent, read_cropped_image, expected_array=None):
65 | if expected_array is None:
66 | expected_array = self.input_3D_array
67 | read_cropped_array = Converter.vtk2numpy(read_cropped_image)
68 | cropped_array = expected_array[target_z_extent[0]:target_z_extent[1] + 1, :, :]
69 | np.testing.assert_array_equal(cropped_array, read_cropped_array)
70 |
71 | def test_raw_cropped_reader(self):
72 | target_z_extent = [1, 3]
73 | reader = cilRawCroppedReader()
74 | og_shape = np.shape(self.input_3D_array)
75 | reader.SetFileName(self.raw_filename_3D)
76 | reader.SetTargetZExtent(tuple(target_z_extent))
77 | reader.SetBigEndian(False)
78 | reader.SetIsFortran(False)
79 | raw_type_code = str(self.input_3D_array.dtype)
80 | reader.SetTypeCodeName(raw_type_code)
81 | reader.SetStoredArrayShape(og_shape)
82 | self.check_extent(reader, target_z_extent)
83 | self.check_values(target_z_extent, reader.GetOutput())
84 | # Check raw type code was set correctly:
85 | self.assertEqual(raw_type_code, reader.GetTypeCodeName())
86 |
87 | def test_meta_and_numpy_cropped_readers(self):
88 | readers = [cilNumpyCroppedReader(), cilMetaImageCroppedReader()]
89 | filenames = [self.numpy_filename_3D, self.meta_filename_3D]
90 | subtest_labels = ['cilNumpyCroppedReader', 'cilMetaImageCroppedReader']
91 | for i, reader in enumerate(readers):
92 | with self.subTest(reader=subtest_labels[i]):
93 | filename = filenames[i]
94 | target_z_extent = [1, 3]
95 | reader.SetFileName(filename)
96 | reader.SetTargetZExtent(tuple(target_z_extent))
97 | self.check_extent(reader, target_z_extent)
98 | self.check_values(target_z_extent, reader.GetOutput())
99 |
100 | def _setup_tiff_cropped_reader(self, target_z_extent):
101 | reader = cilTIFFCroppedReader()
102 | reader.SetFileName(self.tiff_fnames)
103 | reader.SetTargetZExtent(target_z_extent)
104 | return reader
105 |
106 | def test_tiff_cropped_reader(self):
107 | target_z_extent = [1, 3]
108 | reader = self._setup_tiff_cropped_reader(tuple(target_z_extent))
109 | self.check_extent(reader, target_z_extent)
110 | # Check raw type code was set correctly:
111 | self.assertEqual(self.raw_type_code, reader.GetTypeCodeName())
112 | self.check_values(target_z_extent, reader.GetOutput())
113 |
114 | def test_tiff_cropped_reader_when_orientation_set(self):
115 | target_z_extent = [1, 3]
116 | reader = self._setup_tiff_cropped_reader(tuple(target_z_extent))
117 | reader.SetOrientationType(4) # this flips the y axis
118 | expected_array = np.flip(np.copy(self.input_3D_array), axis=1)
119 | self.check_extent(reader, target_z_extent)
120 | # Check raw type code was set correctly:
121 | self.assertEqual(self.raw_type_code, reader.GetTypeCodeName())
122 | self.check_values(target_z_extent, reader.GetOutput(), expected_array)
123 |
124 | def tearDown(self):
125 | files = [self.raw_filename_3D, self.numpy_filename_3D, self.meta_filename_3D] + self.tiff_fnames
126 | for f in files:
127 | os.remove(f)
128 |
129 |
130 | if __name__ == '__main__':
131 | unittest.main()
132 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_example_data.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from ccpi.viewer.utils import example_data
3 |
4 |
5 | class TestExampleData(unittest.TestCase):
6 |
7 | def test_head_data(self):
8 | data = example_data.HEAD.get()
9 | expected_dimensions = [64, 64, 93]
10 | read_dimensions = data.GetDimensions()
11 |
12 | for i in range(0, len(expected_dimensions)):
13 | self.assertEqual(expected_dimensions[i], read_dimensions[i])
14 |
15 |
16 | if __name__ == '__main__':
17 | unittest.main()
18 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_hdf5.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | import h5py
5 | import numpy as np
6 | import vtk
7 | from ccpi.viewer.utils.conversion import Converter, calculate_target_downsample_shape, cilHDF5CroppedReader, cilHDF5ResampleReader
8 | from ccpi.viewer.utils.hdf5_io import (HDF5Reader, HDF5SubsetReader, write_image_data_to_hdf5)
9 |
10 |
11 | class TestHDF5IO(unittest.TestCase):
12 |
13 | def setUp(self):
14 | # Generate random 3D array and write to HDF5:
15 | np.random.seed(1)
16 | self.input_3D_array = np.random.random(size=(5, 10, 6))
17 | self.hdf5_filename_3D = 'test_3D_data.h5'
18 | with h5py.File(self.hdf5_filename_3D, 'w') as f:
19 | f.create_dataset('ImageData', data=self.input_3D_array)
20 |
21 | # Generate random 4D array and write to HDF5:
22 | self.input_4D_array = np.random.random(size=(10, 7, 10, 6))
23 | self.hdf5_filename_4D = 'test_4D_data.h5'
24 | with h5py.File(self.hdf5_filename_4D, 'w') as f:
25 | f.create_dataset('ImageData', data=self.input_4D_array)
26 |
27 | def test_read_hdf5(self):
28 | # Write a numpy array to a HDF5 and then test using
29 | # HDF5Reader to read it back:
30 | reader = HDF5Reader()
31 | reader.SetFileName(self.hdf5_filename_3D)
32 | reader.SetDatasetName("ImageData")
33 | reader.Update()
34 | array_image_data = reader.GetOutputDataObject(0)
35 | read_array = Converter.vtk2numpy(array_image_data)
36 | np.testing.assert_array_equal(self.input_3D_array, read_array)
37 |
38 | def test_read_hdf5_channel(self):
39 | # Test reading a specific channel in a 4D dataset
40 | channel_index = 5
41 | reader = HDF5Reader()
42 | reader.SetFileName(self.hdf5_filename_4D)
43 | reader.SetDatasetName("ImageData")
44 | reader.Set4DSliceIndex(channel_index)
45 | reader.Set4DIndex(0)
46 | reader.Update()
47 | array_image_data = reader.GetOutputDataObject(0)
48 | read_array = Converter.vtk2numpy(array_image_data)
49 | np.testing.assert_array_equal(self.input_4D_array[channel_index], read_array)
50 |
51 | def test_hdf5_subset_reader(self):
52 | # With the subset reader: -----------------------------
53 | # Test cropping the extent of a dataset
54 | cropped_array = self.input_3D_array[1:3, 3:6, 0:3]
55 | reader = HDF5Reader()
56 | reader.SetFileName(self.hdf5_filename_3D)
57 | reader.SetDatasetName("ImageData")
58 | cropped_reader = HDF5SubsetReader()
59 | cropped_reader.SetInputConnection(reader.GetOutputPort())
60 | # NOTE: the extent is in vtk so is in fortran order, whereas
61 | # above we had the np array in C-order so x and y cropping swapped
62 | cropped_reader.SetUpdateExtent((0, 2, 3, 5, 1, 2))
63 | cropped_reader.Update()
64 | array_image_data = cropped_reader.GetOutputDataObject(0)
65 | read_cropped_array = Converter.vtk2numpy(array_image_data)
66 | np.testing.assert_array_equal(cropped_array, read_cropped_array)
67 |
68 | def test_read_cropped_hdf5_reader(self):
69 | # # With the Cropped reader: -----------------------------
70 | cropped_array = self.input_3D_array[1:3, 3:6, 0:3]
71 | reader = cilHDF5CroppedReader()
72 | reader.SetFileName(self.hdf5_filename_3D)
73 | reader.SetDatasetName("ImageData")
74 | reader.SetTargetExtent((0, 2, 3, 5, 1, 2))
75 | reader.Update()
76 | array_image_data = reader.GetOutput()
77 | read_cropped_array = Converter.vtk2numpy(array_image_data)
78 | np.testing.assert_array_equal(cropped_array, read_cropped_array)
79 |
80 | def test_read_cropped_hdf5_channel(self):
81 | # Test cropping the extent of a dataset
82 | channel_index = 5
83 | cropped_array = self.input_4D_array[1:2, channel_index, 3:6, 0:3]
84 | hdf5_filename = 'test_numpy_data.h5'
85 | with h5py.File(hdf5_filename, 'w') as f:
86 | f.create_dataset('ImageData', data=self.input_4D_array)
87 | reader = HDF5Reader()
88 | reader.SetFileName(self.hdf5_filename_4D)
89 | reader.SetDatasetName("ImageData")
90 | reader.Set4DSliceIndex(channel_index)
91 | reader.Set4DIndex(1)
92 | cropped_reader = HDF5SubsetReader()
93 | cropped_reader.SetInputConnection(reader.GetOutputPort())
94 | # NOTE: the extent is in vtk so is in fortran order, whereas
95 | # above we had the np array in C-order so x and y cropping swapped
96 | cropped_reader.SetUpdateExtent((0, 2, 3, 5, 1, 1))
97 | cropped_reader.Update()
98 | array_image_data = cropped_reader.GetOutputDataObject(0)
99 | read_cropped_array = Converter.vtk2numpy(array_image_data)
100 | np.testing.assert_array_equal(cropped_array, read_cropped_array)
101 |
102 | def test_write_hdf5(self):
103 | # Provides an example image with extent (-10, 10, -10, 10, -10, 10):
104 | image_source = vtk.vtkRTAnalyticSource()
105 | image_source.Update()
106 | image_data = image_source.GetOutput()
107 | numpy_data = Converter.vtk2numpy(image_data)
108 |
109 | self.hdf5_filename_RT = "test_image_data.hdf5"
110 |
111 | write_image_data_to_hdf5(self.hdf5_filename_RT, image_data, dataset_name='RTData')
112 |
113 | # Test reading hdf5:
114 | reader = HDF5Reader()
115 | reader.SetFileName(self.hdf5_filename_RT)
116 | reader.SetDatasetName('RTData')
117 | reader.Update()
118 |
119 | read_image_data = reader.GetOutputDataObject(0)
120 | read_numpy_data = Converter.vtk2numpy(read_image_data)
121 | np.testing.assert_array_equal(numpy_data, read_numpy_data)
122 | # currently fails because we don't save extent to hdf5:
123 | # self.assertEqual(image_data.GetExtent(), read_image_data.GetExtent())
124 | os.remove(self.hdf5_filename_RT)
125 |
126 | def test_hdf5_resample_reader(self):
127 | # Tests image with correct target size is generated by resample reader:
128 | # Not a great test, but at least checks the resample reader runs
129 | # without crashing
130 | # TODO: improve this test
131 | readerhdf5 = cilHDF5ResampleReader()
132 | readerhdf5.SetFileName(self.hdf5_filename_3D)
133 | readerhdf5.SetDatasetName("ImageData")
134 | target_size = 100
135 | readerhdf5.SetTargetSize(target_size)
136 | readerhdf5.Update()
137 | image = readerhdf5.GetOutput()
138 | extent = image.GetExtent()
139 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
140 | og_shape = np.shape(self.input_3D_array)
141 | og_shape = (og_shape[2], og_shape[1], og_shape[0])
142 | og_size = og_shape[0] * og_shape[1] * og_shape[2] * readerhdf5.GetBytesPerElement()
143 | expected_shape = calculate_target_downsample_shape(target_size, og_size, og_shape)
144 | self.assertEqual(resulting_shape, expected_shape)
145 |
146 | # Now test if we get the full image extent if our
147 | # target size is larger than the size of the image:
148 | target_size = og_size * 2
149 | readerhdf5.SetTargetSize(target_size)
150 | readerhdf5.Update()
151 | image = readerhdf5.GetOutput()
152 | extent = image.GetExtent()
153 | expected_shape = og_shape
154 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
155 | self.assertEqual(resulting_shape, expected_shape)
156 | resulting_array = Converter.vtk2numpy(image)
157 | np.testing.assert_array_equal(self.input_3D_array, resulting_array)
158 |
159 | # Now test if we get the correct z extent if we set that we
160 | # have acquisition data
161 | readerhdf5 = cilHDF5ResampleReader()
162 | readerhdf5.SetDatasetName("ImageData")
163 | readerhdf5.SetFileName(self.hdf5_filename_3D)
164 | target_size = 100
165 | readerhdf5.SetTargetSize(target_size)
166 | readerhdf5.SetIsAcquisitionData(True)
167 | readerhdf5.Update()
168 | image = readerhdf5.GetOutput()
169 | extent = image.GetExtent()
170 | shape_not_acquisition = calculate_target_downsample_shape(target_size, og_size, og_shape, acq=True)
171 | expected_size = shape_not_acquisition[0] * \
172 | shape_not_acquisition[1]*shape_not_acquisition[2]
173 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
174 | resulting_size = resulting_shape[0] * \
175 | resulting_shape[1]*resulting_shape[2]
176 | # angle (z direction) is first index in numpy array, and in cil
177 | # but it is the last in vtk.
178 | resulting_z_shape = extent[5] + 1
179 | og_z_shape = np.shape(self.input_3D_array)[0]
180 | self.assertEqual(resulting_size, expected_size)
181 | self.assertEqual(resulting_z_shape, og_z_shape)
182 |
183 | def tearDown(self):
184 | files = [self.hdf5_filename_3D, self.hdf5_filename_4D]
185 | for f in files:
186 | os.remove(f)
187 |
188 |
189 | if __name__ == '__main__':
190 | unittest.main()
191 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_resample_readers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 | import warnings
4 |
5 | import numpy as np
6 | import vtk
7 | from ccpi.viewer.utils.conversion import Converter, calculate_target_downsample_shape, \
8 | cilRawResampleReader, cilTIFFResampleReader, cilNumpyMETAImageWriter, cilMetaImageResampleReader,\
9 | cilNumpyResampleReader
10 |
11 |
12 | class TestResampleReaders(unittest.TestCase):
13 |
14 | def setUp(self):
15 | # Generate random 3D array and write to HDF5:
16 | np.random.seed(1)
17 | bits = 16
18 | self.bytes_per_element = int(bits / 8)
19 | shape = (5, 10, 6)
20 | self.size_to_resample_to = 100
21 | self.size_greater_than_input_size = 10000
22 | self.input_3D_array = np.random.randint(10, size=shape, dtype=eval(f"np.uint{bits}"))
23 | self.input_3D_array = np.reshape(np.arange(self.input_3D_array.size),
24 | newshape=shape).astype(dtype=eval(f"np.uint{bits}"))
25 | bytes_3D_array = bytes(self.input_3D_array)
26 | self.raw_filename_3D = 'test_3D_data.raw'
27 | with open(self.raw_filename_3D, 'wb') as f:
28 | f.write(bytes_3D_array)
29 |
30 | # write header to go with raw file
31 | self.mhd_filename_3D = 'raw_header.mhd'
32 | typecode = f'uint{bits}'
33 | big_endian = False
34 | header_length = 0
35 | shape = np.shape(self.input_3D_array)
36 | shape_to_write = shape[::-1] # because it is not a fortran order array we have to swap
37 | cilNumpyMETAImageWriter.WriteMETAImageHeader(self.raw_filename_3D,
38 | self.mhd_filename_3D,
39 | typecode,
40 | big_endian,
41 | header_length,
42 | shape_to_write,
43 | spacing=(1., 1., 1.),
44 | origin=(0., 0., 0.))
45 |
46 | self.numpy_filename_3D = 'test_3D_data.npy'
47 | np.save(self.numpy_filename_3D, self.input_3D_array)
48 |
49 | self.meta_filename_3D = 'test_3D_data.mha'
50 | vtk_image = Converter.numpy2vtkImage(self.input_3D_array)
51 | writer = vtk.vtkMetaImageWriter()
52 | writer.SetFileName(self.meta_filename_3D)
53 | writer.SetInputData(vtk_image)
54 | writer.SetCompression(False)
55 | writer.Write()
56 |
57 | # Create TIFF Files
58 | fnames = []
59 | arr = self.input_3D_array
60 | from PIL import Image
61 | for i in range(arr.shape[0]):
62 | fname = 'tiff_test_file_{:03d}.tiff'.format(i)
63 | fnames.append(os.path.abspath(fname))
64 | # Using vtk the Y axis gets reversed
65 | # vtk_image = Converter.numpy2vtkImage(np.expand_dims(arr[i,:,:], axis=0))
66 | # twriter.SetFileName(fnames[-1])
67 | # twriter.SetInputData(vtk_image)
68 | # twriter.Write()
69 | im = Image.fromarray(arr[i])
70 | im.save(fnames[-1])
71 |
72 | self.tiff_fnames = fnames
73 |
74 | def resample_reader_test1(self, reader, target_size, expected_array=None):
75 | # Tests image with correct target size is generated by resample reader:
76 | # Not a great test, but at least checks the resample reader runs
77 | # without crashing
78 | # TODO: improve this test
79 |
80 | if expected_array is None:
81 | expected_array = self.input_3D_array
82 |
83 | reader.SetTargetSize(target_size)
84 | reader.Modified()
85 | reader.Update()
86 | og_shape = np.shape(expected_array)
87 | raw_type_code = str(expected_array.dtype)
88 |
89 | if target_size < self.input_3D_array.size * self.bytes_per_element:
90 | # Check raw type code was set correctly:
91 | self.assertEqual(raw_type_code, reader.GetTypeCodeName())
92 | image = reader.GetOutput()
93 | extent = image.GetExtent()
94 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
95 | og_shape = (og_shape[2], og_shape[1], og_shape[0])
96 | og_size = og_shape[0] * og_shape[1] * og_shape[2] * self.bytes_per_element
97 | expected_shape = calculate_target_downsample_shape(target_size, og_size, og_shape)
98 | self.assertEqual(resulting_shape, expected_shape)
99 | # Now test if we get the correct z extent if we set that we
100 | # have acquisition data
101 | reader.SetIsAcquisitionData(True)
102 | # why do we need this?
103 | reader.Modified()
104 | reader.Update()
105 | image = reader.GetOutput()
106 | extent2 = image.GetExtent()
107 | extent = extent2
108 | shape_acquisition = calculate_target_downsample_shape(target_size, og_size, og_shape, acq=True)
109 | expected_size = shape_acquisition[0] * \
110 | shape_acquisition[1]*shape_acquisition[2]
111 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
112 | resulting_size = resulting_shape[0] * \
113 | resulting_shape[1]*resulting_shape[2]
114 | # angle (z direction) is first index in numpy array, and in cil
115 | # but it is the last in vtk.
116 | resulting_z_shape = extent[5] + 1
117 | og_z_shape = og_shape[2]
118 | self.assertEqual(resulting_size, expected_size)
119 | self.assertEqual(resulting_z_shape, og_z_shape)
120 | else:
121 | reader.Modified()
122 | reader.Update()
123 | image = reader.GetOutput()
124 | resulting_shape = image.GetDimensions()
125 | expected_shape = np.shape(expected_array)
126 | self.assertEqual(resulting_shape, expected_shape[::-1])
127 | resulting_array = Converter.vtk2numpy(image)
128 | np.testing.assert_array_equal(np.asfortranarray(expected_array), resulting_array)
129 |
130 | def test_raw_resample_reader(self):
131 | og_shape = np.shape(self.input_3D_array)
132 | reader = cilRawResampleReader()
133 | reader.SetFileName(self.raw_filename_3D)
134 | reader.SetBigEndian(False)
135 | reader.SetIsFortran(False)
136 | raw_type_code = str(self.input_3D_array.dtype)
137 | reader.SetTypeCodeName(raw_type_code)
138 | reader.SetStoredArrayShape(og_shape)
139 | self.resample_reader_test1(reader, self.size_to_resample_to)
140 |
141 | def test_raw_resample_reader_when_resampling_not_needed(self):
142 | og_shape = np.shape(self.input_3D_array)
143 | reader = cilRawResampleReader()
144 | reader.SetFileName(self.raw_filename_3D)
145 | reader.SetBigEndian(False)
146 | reader.SetIsFortran(False)
147 | raw_type_code = str(self.input_3D_array.dtype)
148 | reader.SetTypeCodeName(raw_type_code)
149 | reader.SetStoredArrayShape(og_shape)
150 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
151 |
152 | def _setup_tiff_resample_reader(self):
153 | reader = cilTIFFResampleReader()
154 | reader.SetFileName(self.tiff_fnames)
155 | return reader
156 |
157 | def test_tiff_resample_reader(self):
158 | reader = self._setup_tiff_resample_reader()
159 | self.resample_reader_test1(reader, self.size_to_resample_to)
160 |
161 | def test_tiff_resample_reader_with_orientation_type_set(self):
162 | reader = self._setup_tiff_resample_reader()
163 | reader.SetOrientationType(4) # this flips the y axis
164 | expected_array = np.flip(np.copy(self.input_3D_array), axis=1)
165 | self.resample_reader_test1(reader, self.size_to_resample_to, expected_array)
166 |
167 | def test_tiff_resample_reader_when_resampling_not_needed(self):
168 | reader = self._setup_tiff_resample_reader()
169 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
170 |
171 | def test_tiff_resample_reader_with_orientation_type_set_when_resampling_not_needed(self):
172 | reader = self._setup_tiff_resample_reader()
173 | reader.SetOrientationType(4) # this flips the y axis
174 | expected_array = np.flip(np.copy(self.input_3D_array), axis=1)
175 | self.resample_reader_test1(reader, self.size_greater_than_input_size, expected_array)
176 |
177 | def test_meta_resample_reader_mha(self):
178 | reader = cilMetaImageResampleReader()
179 | reader.SetFileName(self.meta_filename_3D)
180 | self.resample_reader_test1(reader, self.size_to_resample_to)
181 |
182 | def test_meta_resample_reader_mha_when_resampling_not_needed(self):
183 | reader = cilMetaImageResampleReader()
184 | reader.SetFileName(self.meta_filename_3D)
185 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
186 |
187 | def test_meta_resample_reader_mhd(self):
188 | reader = cilMetaImageResampleReader()
189 | reader.SetFileName(self.mhd_filename_3D)
190 | self.resample_reader_test1(reader, self.size_to_resample_to)
191 |
192 | def test_meta_resample_reader_mhd_when_resampling_not_needed(self):
193 | reader = cilMetaImageResampleReader()
194 | reader.SetFileName(self.mhd_filename_3D)
195 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
196 |
197 | def test_npy_resample_reader(self):
198 | reader = cilNumpyResampleReader()
199 | reader.SetFileName(self.numpy_filename_3D)
200 | self.resample_reader_test1(reader, self.size_to_resample_to)
201 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
202 |
203 | def test_npy_resample_reader_when_resampling_not_needed(self):
204 | reader = cilNumpyResampleReader()
205 | reader.SetFileName(self.numpy_filename_3D)
206 | self.resample_reader_test1(reader, self.size_to_resample_to)
207 | self.resample_reader_test1(reader, self.size_greater_than_input_size)
208 |
209 | def tearDown(self):
210 | files = [self.raw_filename_3D, self.numpy_filename_3D, self.meta_filename_3D
211 | ] + self.tiff_fnames + [self.mhd_filename_3D]
212 | for f in files:
213 | # print (f'removing {f}')
214 | os.remove(f)
215 | if os.path.exists(f):
216 | warnings.warn(f'file {f} not deleted!')
217 |
218 |
219 | if __name__ == '__main__':
220 | unittest.main()
221 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_version.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 |
4 | class TestModuleBase(unittest.TestCase):
5 |
6 | def test_version(self):
7 | try:
8 | from ccpi.viewer import version as dversion
9 | a = dversion.version
10 | print("version", a)
11 | self.assertTrue(isinstance(a, str))
12 | except ImportError as ie:
13 | self.assertFalse(True, str(ie))
14 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_viewer_main_windows.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import unittest
4 | from unittest import mock
5 |
6 | import vtk
7 | from ccpi.viewer.CILViewer import CILViewer
8 | from ccpi.viewer.CILViewer2D import CILViewer2D
9 | from ccpi.viewer.ui.main_windows import ViewerMainWindow
10 | from PySide2.QtWidgets import QApplication
11 |
12 | # skip the tests on GitHub actions
13 | if os.environ.get('CONDA_BUILD', '0') == '1':
14 | skip_as_conda_build = True
15 | else:
16 | skip_as_conda_build = False
17 |
18 | print("skip_as_conda_build is set to ", skip_as_conda_build)
19 |
20 |
21 | @unittest.skipIf(skip_as_conda_build, "On conda builds do not do any test with interfaces")
22 | class TestViewerMainWindow(unittest.TestCase):
23 | ''' Methods which have their full functionality tested:
24 | - onAppSettingsDialogAccepted
25 | - setDefaultDownsampledSize
26 | - getDefaulDownsampledSize
27 | - getTargetImageSize
28 | - updateViewerCoords
29 | '''
30 |
31 | def setUp(self):
32 | pass
33 |
34 | def tearDown(self) -> None:
35 | pass
36 |
37 | def test_all(self):
38 | # https://stackoverflow.com/questions/5387299/python-unittest-testcase-execution-order
39 | # https://stackoverflow.com/questions/11145583/unit-and-functional-testing-a-pyside-based-application
40 | self._test_init()
41 | self._test_init_calls_super_init()
42 | self._test_createAppSettingsDialog_calls_setAppSettingsDialogWidgets()
43 | self._test_acceptViewerSettings_when_gpu_checked()
44 | self._test_acceptViewerSettings_when_gpu_unchecked()
45 | self._test_setDefaultDownsampledSize()
46 | self._test_getDefaultDownsampledSize()
47 | self._test_getTargetImageSize_when_vis_size_is_None()
48 | self._test_getTargetImageSize_when_vis_size_is_not_None()
49 | self._test_updateViewerCoords_with_display_unsampled_coords_selected()
50 | self._test_updateViewerCoords_with_display_downsampled_coords_selected()
51 | self._test_updateViewerCoords_with_3D_viewer()
52 | self._test_updateViewerCoords_with_no_img3D()
53 |
54 | def _test_init(self):
55 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
56 | assert vmw is not None
57 |
58 | def _test_init_calls_super_init(self):
59 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
60 | # If the super init is called, then the following attributes should be set:
61 | assert vmw.app_name == "testing app name"
62 | assert vmw.threadpool is not None
63 | assert vmw.progress_windows == {}
64 |
65 | def _test_createAppSettingsDialog_calls_setAppSettingsDialogWidgets(self):
66 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
67 | vmw.setAppSettingsDialogWidgets = mock.MagicMock()
68 | vmw.setViewerSettingsDialogWidgets = mock.MagicMock()
69 | vmw.createAppSettingsDialog()
70 | vmw.setAppSettingsDialogWidgets.assert_called_once()
71 | vmw.setViewerSettingsDialogWidgets.assert_called_once()
72 |
73 | def _test_acceptViewerSettings_when_gpu_checked(self):
74 |
75 | vmw = self._setup_acceptViewerSettings_tests()
76 |
77 | vmw.acceptViewerSettings()
78 |
79 | vmw.settings.assert_has_calls(
80 | [mock.call.setValue('use_gpu_volume_mapper', True),
81 | mock.call.setValue('vis_size', 1.0)], any_order=True)
82 |
83 | assert isinstance(vmw.viewers[0].volume_mapper, vtk.vtkSmartVolumeMapper)
84 |
85 | def _test_acceptViewerSettings_when_gpu_unchecked(self):
86 |
87 | vmw, settings_dialog = self._setup_acceptViewerSettings_tests()
88 |
89 | vmw.acceptViewerSettings(settings_dialog)
90 |
91 | vmw.settings.assert_has_calls(
92 | [mock.call.setValue('use_gpu_volume_mapper', False),
93 | mock.call.setValue('vis_size', 1.0)], any_order=True)
94 |
95 | assert isinstance(vmw.viewers[0].volume_mapper, vtk.vtkFixedPointVolumeRayCastMapper)
96 |
97 | def _setup_acceptViewerSettings_tests(self):
98 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
99 | vmw.settings = mock.MagicMock()
100 | vmw.settings.setValue = mock.MagicMock()
101 | vis_size_field = mock.MagicMock()
102 | vis_size_field.value.return_value = 1.0
103 | gpu_checkbox_field = mock.MagicMock()
104 | gpu_checkbox_field.isChecked.return_value = True
105 | dark_checkbox_field = mock.MagicMock()
106 | dark_checkbox_field.isChecked.return_value = False
107 | settings_dialog = mock.MagicMock()
108 | settings_dialog.widgets = {
109 | 'vis_size_field': vis_size_field,
110 | 'gpu_checkbox_field': gpu_checkbox_field,
111 | 'dark_checkbox_field': dark_checkbox_field
112 | }
113 | viewer3D = CILViewer()
114 | viewer3D.volume_mapper = None
115 | vmw._viewers = [viewer3D]
116 | vmw._vs_dialog = settings_dialog
117 | return vmw
118 |
119 | def _test_acceptViewerSettings_when_gpu_unchecked(self):
120 | vmw = self._setup_acceptViewerSettings_tests()
121 | vmw._vs_dialog.widgets['gpu_checkbox_field'].isChecked.return_value = False
122 |
123 | vmw.acceptViewerSettings()
124 | vmw.settings.assert_has_calls(
125 | [mock.call.setValue('use_gpu_volume_mapper', False),
126 | mock.call.setValue('vis_size', 1.0)], any_order=True)
127 |
128 | assert isinstance(vmw.viewers[0].volume_mapper, vtk.vtkFixedPointVolumeRayCastMapper)
129 |
130 | def _test_setDefaultDownsampledSize(self):
131 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
132 | vmw.setDefaultDownsampledSize(5)
133 | assert vmw.default_downsampled_size == 5
134 |
135 | def _test_getDefaultDownsampledSize(self):
136 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
137 | # Test what the default value is:
138 | assert vmw.getDefaultDownsampledSize() == 512**3
139 | vmw.default_downsampled_size = 5
140 | assert vmw.getDefaultDownsampledSize() == 5
141 |
142 | def _test_getTargetImageSize_when_vis_size_is_None(self):
143 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
144 | vmw.settings.setValue("vis_size", None)
145 | vmw.getDefaultDownsampledSize = mock.MagicMock()
146 | vmw.getDefaultDownsampledSize.return_value = 512**3
147 | returned_target_size = vmw.getTargetImageSize()
148 | vmw.getDefaultDownsampledSize.assert_called_once()
149 | assert (returned_target_size == 512**3)
150 |
151 | def _test_getTargetImageSize_when_vis_size_is_not_None(self):
152 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
153 | vmw.settings.setValue("vis_size", 5)
154 | vmw.getDefaultDownsampledSize = mock.MagicMock()
155 | returned_target_size = vmw.getTargetImageSize()
156 | vmw.getDefaultDownsampledSize.assert_not_called()
157 | assert (returned_target_size == 5 * (1024**3))
158 |
159 | def _test_updateViewerCoords_with_display_unsampled_coords_selected(self):
160 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
161 | viewer2D = CILViewer2D()
162 | viewer2D.visualisation_downsampling = [2, 2, 2]
163 | viewer2D.img3D = vtk.vtkImageData()
164 | viewer2D.setDisplayUnsampledCoordinates = mock.MagicMock()
165 | viewer2D.updatePipeline = mock.MagicMock()
166 | vmw.viewer_coords_dock.viewers = [viewer2D]
167 | viewer_coords_widgets = vmw.viewer_coords_dock.getWidgets()
168 | viewer_coords_widgets['coords_combo_field'].setCurrentIndex(0)
169 | viewer_coords_widgets['coords_warning_field'].setVisible = mock.MagicMock()
170 | vmw.updateViewerCoords()
171 |
172 | # Expect display unsampled coords to be called with True
173 | # and the warning field to be visible:
174 | viewer2D.setDisplayUnsampledCoordinates.assert_called_once_with(True)
175 | viewer_coords_widgets['coords_warning_field'].setVisible.assert_called_once_with(True)
176 | viewer2D.updatePipeline.assert_called_once()
177 |
178 | viewer2D.updatePipeline = mock.MagicMock() # reset call count
179 | viewer2D.visualisation_downsampling = [1, 1, 1]
180 | vmw.updateViewerCoords()
181 | # Expect the warning field to be hidden:
182 | viewer_coords_widgets['coords_warning_field'].setVisible.assert_called_with(False)
183 | viewer2D.updatePipeline.assert_called_once()
184 |
185 | def _test_updateViewerCoords_with_display_downsampled_coords_selected(self):
186 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
187 | viewer2D = CILViewer2D()
188 | viewer2D.visualisation_downsampling = [2, 2, 2]
189 | viewer2D.img3D = vtk.vtkImageData()
190 | viewer2D.setDisplayUnsampledCoordinates = mock.MagicMock()
191 | viewer2D.updatePipeline = mock.MagicMock()
192 | vmw.viewer_coords_dock.viewers = [viewer2D]
193 | viewer_coords_widgets = vmw.viewer_coords_dock.getWidgets()
194 | viewer_coords_widgets['coords_combo_field'].setCurrentIndex(1)
195 | viewer_coords_widgets['coords_warning_field'].setVisible = mock.MagicMock()
196 | vmw.updateViewerCoords()
197 |
198 | # Expect display unsampled coords to be called with False
199 | # and the warning field to be hidden:
200 | viewer2D.setDisplayUnsampledCoordinates.assert_called_with(False)
201 | viewer_coords_widgets['coords_warning_field'].setVisible.assert_called_once_with(False)
202 | viewer2D.updatePipeline.assert_called()
203 |
204 | def _test_updateViewerCoords_with_3D_viewer(self):
205 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
206 | viewer3D = CILViewer()
207 | viewer3D.visualisation_downsampling = [2, 2, 2]
208 | viewer3D.updatePipeline = mock.MagicMock()
209 | viewer3D.setDisplayUnsampledCoordinates = mock.MagicMock()
210 | vmw.viewer_coords_dock.viewers = [viewer3D]
211 | viewer_coords_widgets = vmw.viewer_coords_dock.getWidgets()
212 | viewer_coords_widgets['coords_combo_field'].setCurrentIndex(1)
213 | viewer_coords_widgets['coords_warning_field'].setVisible = mock.MagicMock()
214 | vmw.updateViewerCoords()
215 |
216 | # Expect we don't call anything:
217 | viewer3D.setDisplayUnsampledCoordinates.assert_not_called()
218 | viewer_coords_widgets['coords_warning_field'].setVisible.assert_not_called()
219 | viewer3D.updatePipeline.assert_not_called()
220 |
221 | def _test_updateViewerCoords_with_no_img3D(self):
222 | vmw = ViewerMainWindow(title="Testing Title", app_name="testing app name")
223 | viewer2D = CILViewer2D()
224 | viewer2D.visualisation_downsampling = [2, 2, 2]
225 | viewer2D.img3D = None
226 | viewer2D.setDisplayUnsampledCoordinates = mock.MagicMock()
227 | viewer2D.updatePipeline = mock.MagicMock()
228 | vmw.viewer_coords_dock.viewers = [viewer2D]
229 | viewer_coords_widgets = vmw.viewer_coords_dock.getWidgets()
230 | viewer_coords_widgets['coords_combo_field'].setCurrentIndex(1)
231 | viewer_coords_widgets['coords_warning_field'].setVisible = mock.MagicMock()
232 | vmw.updateViewerCoords()
233 |
234 | # Expect we don't call anything:
235 | viewer2D.setDisplayUnsampledCoordinates.assert_not_called()
236 | viewer_coords_widgets['coords_warning_field'].setVisible.assert_not_called()
237 | viewer2D.updatePipeline.assert_not_called()
238 |
239 |
240 | if __name__ == '__main__':
241 | unittest.main()
242 |
--------------------------------------------------------------------------------
/Wrappers/Python/test/test_vtk_image_resampler.py:
--------------------------------------------------------------------------------
1 | from ccpi.viewer.utils.conversion import Converter
2 | from ccpi.viewer.utils.conversion import vtkImageResampler
3 | import unittest
4 | import numpy as np
5 |
6 |
7 | def calculate_target_downsample_shape(max_size, total_size, shape, acq=False):
8 | if not acq:
9 | xy_axes_magnification = np.power(max_size / total_size, 1 / 3)
10 | slice_per_chunk = int(1 / xy_axes_magnification)
11 | else:
12 | slice_per_chunk = 1
13 | xy_axes_magnification = np.power(max_size / total_size, 1 / 2)
14 | num_chunks = 1 + len([i for i in range(slice_per_chunk, shape[2], slice_per_chunk)])
15 |
16 | target_image_shape = (int(xy_axes_magnification * shape[0]), int(xy_axes_magnification * shape[1]), num_chunks)
17 | return target_image_shape
18 |
19 |
20 | class TestVTKImageResampler(unittest.TestCase):
21 |
22 | def setUp(self):
23 | # Generate random 3D array and convert to VTK Image Data:
24 | np.random.seed(1)
25 | bits = 16
26 | self.bytes_per_element = int(bits / 8)
27 | self.input_3D_array = np.random.randint(10, size=(50, 10, 60), dtype=eval(f"np.uint{bits}"))
28 | self.input_vtk_image = Converter.numpy2vtkImage(self.input_3D_array)
29 |
30 | def test_vtk_resample_reader(self):
31 | # Tests image with correct target size is generated by resample reader:
32 | # Not a great test, but at least checks the resample reader runs
33 | # without crashing
34 | # TODO: improve this test
35 | reader = vtkImageResampler()
36 | reader.SetInputDataObject(self.input_vtk_image)
37 | target_size = 100
38 | reader.SetTargetSize(target_size)
39 | reader.Update()
40 |
41 | image = reader.GetOutput()
42 | extent = image.GetExtent()
43 | og_shape = np.shape(self.input_3D_array)
44 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
45 | og_shape = (og_shape[2], og_shape[1], og_shape[0])
46 | og_size = og_shape[0] * og_shape[1] * og_shape[2] * self.bytes_per_element
47 | expected_shape = calculate_target_downsample_shape(target_size, og_size, og_shape)
48 | self.assertEqual(resulting_shape, expected_shape)
49 |
50 | # # Now test if we get the full image extent if our
51 | # # target size is larger than the size of the image:
52 | target_size = og_size * 2
53 | reader.SetTargetSize(target_size)
54 | reader.Update()
55 | image = reader.GetOutput()
56 | extent = image.GetExtent()
57 | expected_shape = og_shape
58 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
59 | self.assertEqual(resulting_shape, expected_shape)
60 | resulting_array = Converter.vtk2numpy(image)
61 | np.testing.assert_array_equal(self.input_3D_array, resulting_array)
62 |
63 | # # Now test if we get the correct z extent if we set that we
64 | # # have acquisition data
65 | target_size = 100
66 | reader.SetTargetSize(target_size)
67 | reader.SetIsAcquisitionData(True)
68 | reader.Update()
69 | image = reader.GetOutput()
70 | extent = image.GetExtent()
71 | shape_not_acquisition = calculate_target_downsample_shape(target_size, og_size, og_shape, acq=True)
72 | expected_size = shape_not_acquisition[0] * \
73 | shape_not_acquisition[1]*shape_not_acquisition[2]
74 | resulting_shape = (extent[1] + 1, (extent[3] + 1), (extent[5] + 1))
75 | resulting_size = resulting_shape[0] * \
76 | resulting_shape[1]*resulting_shape[2]
77 | # angle (z direction) is first index in numpy array, and in cil
78 | # but it is the last in vtk.
79 | resulting_z_shape = extent[5] + 1
80 | og_z_shape = np.shape(self.input_3D_array)[0]
81 | self.assertEqual(resulting_size, expected_size)
82 | self.assertEqual(resulting_z_shape, og_z_shape)
83 |
84 |
85 | if __name__ == '__main__':
86 | unittest.main()
87 |
--------------------------------------------------------------------------------
/Wrappers/javascript/test_vtk.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
22 |
23 |
--------------------------------------------------------------------------------
/docker/web-app/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BASE_IMAGE=ubuntu:20.04
2 | FROM ${BASE_IMAGE} as base
3 |
4 | LABEL \
5 | author.name="Edoardo Pasca, Laura Murgatroyd , Samuel Jones" \
6 | author.email=edoardo.pasca@stfc.ac.uk \
7 | maintainer.email=edoardo.pasca@stfc.ac.uk \
8 | maintainer.url=https://www.ccpi.ac.uk/ \
9 | source.url=https://github.com/vais-ral/CILViewer \
10 | licence="Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0)" \
11 | description="CILViewer Ubuntu"
12 |
13 | ENV LANG en_GB.UTF-8
14 | ENV LANGUAGE en_GB:en
15 |
16 | ARG USER="abc"
17 | ARG GROUP="users"
18 | ARG HOME="/home/${USER}"
19 | WORKDIR ${HOME}
20 |
21 | USER root
22 | RUN apt-get update -y && \
23 | apt-get upgrade -y && \
24 | apt-get install curl git -y && \
25 | # Add VTK dependencies and xvfb for running displays in a container
26 | apt-get install libgl1 libxrender1 libgl1-mesa-dev xvfb -y
27 |
28 | USER ${NB_USER}:${NB_GROUP}
29 |
30 | COPY environment.yml .
31 | COPY install-mambaforge-and-env.sh .
32 | RUN bash install-mambaforge-and-env.sh
33 | RUN rm install-mambaforge-and-env.sh
34 | RUN rm environment.yml
35 |
36 | COPY clone-and-install.sh .
37 | RUN bash clone-and-install.sh
38 | RUN rm clone-and-install.sh
39 |
40 | COPY create-data-folder.sh .
41 | RUN bash create-data-folder.sh
42 | RUN rm create-data-folder.sh
43 |
44 | COPY entrypoint.sh /usr/local/bin/
45 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
46 | # set default parameter for entrypoint
47 | #https://docs.docker.com/engine/reference/builder/#cmd
48 | CMD ["/home/abc/data"]
49 |
50 |
--------------------------------------------------------------------------------
/docker/web-app/README.md:
--------------------------------------------------------------------------------
1 | To build the docker image run this command, with the working directory being the directory of this README:
2 | `docker build .`
3 |
4 | To find the image id, look at the image list using:
5 | `docker image list`
6 |
7 | Using the most recently created docker image as the image ID:
8 | `docker run -p 8080:8080 `
9 |
10 | The container should start with a web server running an individual client's CILViewer instance on 0.0.0.0:8080, and the listed docker IP address in the attached console.
11 |
12 | To bind a directory with data on the host one could launch
13 |
14 | `docker run -p 8080:8080 --mount type=bind,source="$(pwd)"/volume,target=/home/abc/bind /home/abc/bind`
15 |
--------------------------------------------------------------------------------
/docker/web-app/clone-and-install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | git clone https://github.com/vais-ral/CILViewer
4 |
5 | . /home/abc/mambaforge/etc/profile.d/conda.sh
6 | conda activate cilviewer_webapp
7 | cd CILViewer/Wrappers/Python
8 | pip install .
9 |
10 | cd ../../..
11 | rm -rf CILViewer
--------------------------------------------------------------------------------
/docker/web-app/create-data-folder.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | mkdir /home/abc/data
4 | curl -L -O https://github.com/TomographicImaging/CIL-Data/raw/5affe9b1c3bd20b28aee7756aa968d7c2a9eeff4/head.mha
5 | mv head.mha /home/abc/data
--------------------------------------------------------------------------------
/docker/web-app/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | . /home/abc/mambaforge/etc/profile.d/conda.sh
4 | conda activate cilviewer_webapp
5 |
6 | xvfb-run -a web_cilviewer $1 --host 0.0.0.0 --server
7 |
--------------------------------------------------------------------------------
/docker/web-app/environment.yml:
--------------------------------------------------------------------------------
1 | name: cilviewer_webapp
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python==3.9
6 | - matplotlib # Optional for more colormaps
7 | - h5py
8 | - pip
9 | - schema
10 | - pyyaml
11 | - pip:
12 | # Have to install Trame via pip due to unavailability on conda
13 | - trame <3, >=2.1.1 # Unpinned worked with version 2.1.1, should work with higher versions.
14 | - vtk==9.1
15 |
--------------------------------------------------------------------------------
/docker/web-app/install-mambaforge-and-env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # https://stackoverflow.com/a/246128
4 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
5 | INSTALL_DIR=$HOME/mambaforge
6 |
7 | cd $HOME
8 | curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
9 | bash $HOME/Miniforge3-$(uname)-$(uname -m).sh -b -p $INSTALL_DIR
10 | rm $HOME/Miniforge3-Linux-x86_64.sh
11 | . $INSTALL_DIR/etc/profile.d/conda.sh
12 | mamba env create -f $SCRIPT_DIR/environment.yml
13 | conda activate cilviewer_webapp
14 |
--------------------------------------------------------------------------------