commit
bedc379e97
@ -2,7 +2,7 @@ version: '3.5'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
orthanc:
|
orthanc:
|
||||||
image: jodogne/orthanc-plugins:1.5.7
|
image: jodogne/orthanc-plugins:1.11.0
|
||||||
hostname: orthanc
|
hostname: orthanc
|
||||||
volumes:
|
volumes:
|
||||||
# Config
|
# Config
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
custom: https://giving.massgeneral.org/ohif
|
||||||
33
.github/ISSUE_TEMPLATE/---bug-report.md
vendored
33
.github/ISSUE_TEMPLATE/---bug-report.md
vendored
@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
name: "\U0001F41B Bug report"
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Before Creating an issue**
|
|
||||||
>
|
|
||||||
> - Are you running the latest version?
|
|
||||||
> - Are you reporting to the correct repository?
|
|
||||||
> - Did you search existing issues?
|
|
||||||
|
|
||||||
## Bug Report
|
|
||||||
|
|
||||||
### Describe the Bug
|
|
||||||
|
|
||||||
_A clear and concise description of what the bug is._
|
|
||||||
|
|
||||||
### What steps can we follow to reproduce the bug?
|
|
||||||
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
3. ...
|
|
||||||
|
|
||||||
```js
|
|
||||||
Please use code blocks to show formatted errors or code snippets
|
|
||||||
```
|
|
||||||
|
|
||||||
> :warning: Reports we cannot reproduce are at risk of being marked stale and
|
|
||||||
> closed. The more information you can provide, the more likely we are to look
|
|
||||||
> into and address your issue.
|
|
||||||
25
.github/ISSUE_TEMPLATE/---feature-request.md
vendored
25
.github/ISSUE_TEMPLATE/---feature-request.md
vendored
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
name: "\U0001F680 Feature request"
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: 'Community: Request :hand:, Triage :white_flag:'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
> :hand: Many people requests features. Tell us why yours is important to the
|
|
||||||
> community. How does it add value? Why _this feature_?
|
|
||||||
>
|
|
||||||
> Is your request very specific to your needs? Consider
|
|
||||||
> [contributing it](https://docs.ohif.org/contributing.html) yourself! Or reach
|
|
||||||
> out to a community member that offers
|
|
||||||
> [consulting services](https://docs.ohif.org/help.html#paid--commercial).
|
|
||||||
|
|
||||||
## Request
|
|
||||||
|
|
||||||
**What feature or change would you like to see made?**
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
**Why should we prioritize this feature?**
|
|
||||||
|
|
||||||
...
|
|
||||||
18
.github/ISSUE_TEMPLATE/---support-question.md
vendored
18
.github/ISSUE_TEMPLATE/---support-question.md
vendored
@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
name: "\U0001F917 Support Question"
|
|
||||||
about: "I have a question \U0001F4AC"
|
|
||||||
title: ''
|
|
||||||
labels: 'Community: Question :question:, Triage :white_flag:'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
> :hand: We are a small team with limited resources. Your question is much more
|
|
||||||
> likely to be answered if it is
|
|
||||||
> [a good question](https://stackoverflow.com/help/how-to-ask)
|
|
||||||
|
|
||||||
**Description**
|
|
||||||
|
|
||||||
Questions can often be answered by our documentation. Unable to find an answer
|
|
||||||
in our docs? We'll try to help. In the meantime, if you answer your own
|
|
||||||
question, please respond with the answer here so that others may benefit as
|
|
||||||
well. Better yet, open a PR to expand our docs ^\_^
|
|
||||||
86
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
86
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
name: 'Bug report'
|
||||||
|
description: Create a report to help us improve
|
||||||
|
title: '[Bug] '
|
||||||
|
labels: ['Community: Report :bug:', 'Awaiting Reproduction']
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
👋 Hello, and thank you for contributing to our project! Your support is greatly appreciated.
|
||||||
|
|
||||||
|
🔍 Before proceeding, please make sure to read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md) and familiarize yourself with our [development process](https://v3-docs.ohif.org/development/our-process).
|
||||||
|
|
||||||
|
❓ If you're here to seek general support or ask a question, we encourage you to visit our [community discussion board](https://community.ohif.org/)
|
||||||
|
|
||||||
|
🐞 For bug reports, please complete the following template in as much detail as possible. This will help us reproduce and address the issue efficiently.
|
||||||
|
|
||||||
|
🧪 Finally, ensure that you're using the latest version of the software and check if your issue has already been reported to avoid duplicates.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: bug_description
|
||||||
|
attributes:
|
||||||
|
label: Describe the Bug
|
||||||
|
description: 'A clear and concise description of what the bug is.'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction_steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: 'Please describe the steps to reproduce the issue.'
|
||||||
|
placeholder: "1. First step\n2. Second step\n3. ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: current_behavior
|
||||||
|
attributes:
|
||||||
|
label: The current behavior
|
||||||
|
description:
|
||||||
|
'A clear and concise description of what happens instead of the expected
|
||||||
|
behavior.'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected_behavior
|
||||||
|
attributes:
|
||||||
|
label: The expected behavior
|
||||||
|
description:
|
||||||
|
'A clear and concise description of what you expected to happen.'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: 'OS'
|
||||||
|
description: 'Your operating system.'
|
||||||
|
placeholder: 'e.g., Windows 10, macOS 10.15.4'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: node-version
|
||||||
|
attributes:
|
||||||
|
label: 'Node version'
|
||||||
|
description: 'Your Node.js version.'
|
||||||
|
placeholder: 'e.g., 16.14.0'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: 'Browser'
|
||||||
|
description: 'Your browser.'
|
||||||
|
placeholder: 'e.g., Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: >
|
||||||
|
> :warning: Reports we cannot reproduce are at risk of being marked
|
||||||
|
stale and > closed. The more information you can provide, the more
|
||||||
|
likely we are to look > into and address your issue.
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 🤗 Support Question
|
||||||
|
url: https://community.ohif.org/
|
||||||
|
about: Please use our forum if you have questions or need help.
|
||||||
36
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Create a feature request
|
||||||
|
labels: ['Community: Request :hand:']
|
||||||
|
title: '[Feature Request] '
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
👋 Hello and thank you for your interest in our project!
|
||||||
|
|
||||||
|
🔍 Before you proceed, please read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
🚀 If your request is specific to your needs, consider contributing it yourself! Read our [contributing guides](https://v3-docs.ohif.org/development/contributing) to get started.
|
||||||
|
|
||||||
|
🖊️ Please provide as much detail as possible for your feature request. Mock-up screenshots, workflow or logic flow diagrams are very helpful. Discuss how your requested feature would interact with existing features.
|
||||||
|
|
||||||
|
⏱️ Lastly, tell us why we should prioritize your feature. What impact would it have?
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 'What feature or change would you like to see made?'
|
||||||
|
description:
|
||||||
|
'Please include as much detail as possible including possibly mock up
|
||||||
|
screen shots, workflow or logic flow diagrams etc.'
|
||||||
|
placeholder: '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 'Why should we prioritize this feature?'
|
||||||
|
description:
|
||||||
|
'Discuss if and how the requested feature interacts with existing
|
||||||
|
features.'
|
||||||
|
placeholder: '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
95
.github/pull_request_template.md
vendored
95
.github/pull_request_template.md
vendored
@ -1,14 +1,91 @@
|
|||||||
### PR Checklist
|
<!-- Do Not Delete This! pr_template -->
|
||||||
|
<!-- Please read our Rules of Conduct: https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md -->
|
||||||
- [ ] Brief description of changes
|
<!-- 🕮 Read our guide about our Contributing Guide here https://v3-docs.ohif.org/development/contributing -->
|
||||||
- [ ] Links to any relevant issues
|
<!-- :hand: Thank you for starting this amazing contribution! -->
|
||||||
- [ ] Required status checks are passing
|
|
||||||
- [ ] User cases if changes impact the user's experience
|
|
||||||
- [ ] `@mention` a maintainer to request a review
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Links
|
⚠️⚠️ Please make sure the checklist section below is complete before submitting your PR.
|
||||||
-->
|
To complete the checklist, add an 'x' to each item: [] -> [x]
|
||||||
|
(PRs that do not have all the checkboxes marked will not be approved)
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Provide a clear explanation of the reasoning behind this change, such as:
|
||||||
|
- A link to the issue being addressed, using the format "Fixes #ISSUE_NUMBER"
|
||||||
|
- An image showing the issue or problem being addressed (if not already in the issue)
|
||||||
|
- Error logs or callStacks to help with the understanding of the problem (if not already in the issue)
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Changes & Results
|
||||||
|
|
||||||
|
<!--
|
||||||
|
List all the changes that have been done, such as:
|
||||||
|
- Add new components
|
||||||
|
- Remove old components
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
What are the effects of this change?
|
||||||
|
- Before vs After
|
||||||
|
- Screenshots / GIFs / Videos
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe how we can test your changes.
|
||||||
|
- open a URL
|
||||||
|
- visit a page
|
||||||
|
- click on a button
|
||||||
|
- etc.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
#### PR
|
||||||
|
|
||||||
|
<!--
|
||||||
|
https://semantic-release.gitbook.io/semantic-release/#how-does-it-work
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Please note the letter casing in the provided examples (upper or lower).
|
||||||
|
|
||||||
|
- feat(MeasurementService): add ...
|
||||||
|
- fix(Toolbar): fix ...
|
||||||
|
- docs(Readme): update ...
|
||||||
|
- style(Whitespace): fix ...
|
||||||
|
- refactor(ExtensionManager): ...
|
||||||
|
- test(HangingProtocol): Add test ...
|
||||||
|
- chore(git): update ...
|
||||||
|
- perf(VolumeLoader): ...
|
||||||
|
|
||||||
|
You don't need to have each commit within the Pull Request follow the rule,
|
||||||
|
but the PR title must comply with it, as it will be used as the commit message
|
||||||
|
after the commits are squashed.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [] My Pull Request title is descriptive, accurate and follows the
|
||||||
|
semantic-release format and guidelines.
|
||||||
|
|
||||||
|
#### Code
|
||||||
|
|
||||||
|
- [] My code has been well-documented (function documentation, inline comments,
|
||||||
|
etc.)
|
||||||
|
|
||||||
|
#### Public Documentation Updates
|
||||||
|
|
||||||
|
<!-- https://v3-docs.ohif.org/ -->
|
||||||
|
|
||||||
|
- [] The documentation page has been updated as necessary for any public API
|
||||||
|
additions or removals.
|
||||||
|
|
||||||
|
#### Tested Environment
|
||||||
|
|
||||||
|
- [] OS: <!--[e.g. Windows 10, macOS 10.15.4]-->
|
||||||
|
- [] Node version: <!--[e.g. 16.14.0]-->
|
||||||
|
- [] Browser:
|
||||||
|
<!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]-->
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
|
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
|
||||||
|
|||||||
14
.github/stale.yml
vendored
14
.github/stale.yml
vendored
@ -2,25 +2,19 @@
|
|||||||
# https://github.com/apps/stale
|
# https://github.com/apps/stale
|
||||||
#
|
#
|
||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 21
|
daysUntilStale: 180
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 7
|
daysUntilClose: 60
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- 'Story :raised_hands:'
|
|
||||||
- 'Bug: Verified :bug:'
|
- 'Bug: Verified :bug:'
|
||||||
- 'Task: CI/Tooling :robot:'
|
|
||||||
- 'Task: Docs 📖'
|
|
||||||
- 'Task: Docs :book:'
|
|
||||||
- 'Task: Refactor :hammer_and_wrench:'
|
|
||||||
- 'Task: Tests :microscope:'
|
|
||||||
- 'PR: Awaiting Review 👀'
|
- 'PR: Awaiting Review 👀'
|
||||||
- 'Triage :white_flag:'
|
|
||||||
- 'Extension: Discussion'
|
|
||||||
- 'Announcement 🎉'
|
- 'Announcement 🎉'
|
||||||
- 'IDC:priority'
|
- 'IDC:priority'
|
||||||
- 'IDC:candidate'
|
- 'IDC:candidate'
|
||||||
- 'IDC:collaboration'
|
- 'IDC:collaboration'
|
||||||
|
- 'Community: Request :hand:'
|
||||||
|
- 'Community: Report :bug:'
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: 'Stale :baguette_bread:'
|
staleLabel: 'Stale :baguette_bread:'
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
|||||||
@ -25,13 +25,15 @@ function transpileJavaScript(mode) {
|
|||||||
// These are packages that are not transpiled to our lowest supported
|
// These are packages that are not transpiled to our lowest supported
|
||||||
// JS version (currently ES5). Most of these leverage ES6+ features,
|
// JS version (currently ES5). Most of these leverage ES6+ features,
|
||||||
// that we need to transpile to a different syntax.
|
// that we need to transpile to a different syntax.
|
||||||
exclude,
|
exclude: [/(codecs)/, /(dicomicc)/, exclude],
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
// Find babel.config.js in monorepo root
|
// Find babel.config.js in monorepo root
|
||||||
// https://babeljs.io/docs/en/options#rootmode
|
// https://babeljs.io/docs/en/options#rootmode
|
||||||
rootMode: 'upward',
|
rootMode: 'upward',
|
||||||
envName: mode,
|
envName: mode,
|
||||||
|
cacheCompression: false,
|
||||||
|
cacheDirectory: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,7 +77,11 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
cache: {
|
||||||
|
type: 'filesystem',
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
|
noParse: [/(codec)/, /(dicomicc)/],
|
||||||
rules: [
|
rules: [
|
||||||
transpileJavaScriptRule(mode),
|
transpileJavaScriptRule(mode),
|
||||||
loadWebWorkersRule,
|
loadWebWorkersRule,
|
||||||
@ -89,6 +93,10 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
cssToJavaScript,
|
cssToJavaScript,
|
||||||
|
{
|
||||||
|
test: /\.wasm/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
},
|
||||||
], //.concat(vtkRules),
|
], //.concat(vtkRules),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -103,8 +111,10 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
'@hooks': path.resolve(__dirname, '../platform/viewer/src/hooks'),
|
'@hooks': path.resolve(__dirname, '../platform/viewer/src/hooks'),
|
||||||
'@routes': path.resolve(__dirname, '../platform/viewer/src/routes'),
|
'@routes': path.resolve(__dirname, '../platform/viewer/src/routes'),
|
||||||
'@state': path.resolve(__dirname, '../platform/viewer/src/state'),
|
'@state': path.resolve(__dirname, '../platform/viewer/src/state'),
|
||||||
'cornerstone-wado-image-loader':
|
'dicom-microscopy-viewer':
|
||||||
'cornerstone-wado-image-loader/dist/dynamic-import/cornerstoneWADOImageLoader.min.js',
|
'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
|
||||||
|
'@cornerstonejs/dicom-image-loader':
|
||||||
|
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js',
|
||||||
},
|
},
|
||||||
// Which directories to search when resolving modules
|
// Which directories to search when resolving modules
|
||||||
modules: [
|
modules: [
|
||||||
|
|||||||
149
DATACITATION.md
Normal file
149
DATACITATION.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# OHIF public demo data sets
|
||||||
|
|
||||||
|
The OHIF Viewer's public demo page, available at https://viewer.ohif.org/, uses publicly anonymized demo datasets.
|
||||||
|
These datasets were mostly obtained from the [NIH NCI Imaging Data Commons](https://datacommons.cancer.gov/repository/imaging-data-commons)
|
||||||
|
and [NIH NCI TCIA](https://www.cancerimagingarchive.net/). Before listing the datasets,
|
||||||
|
we would like to extend a special thank you to all groups who have made their datasets publicly available.
|
||||||
|
Without them, we would not have been able to create this demo page.
|
||||||
|
|
||||||
|
Please find below the list of datasets used on the demo page, along with their respective citations.
|
||||||
|
|
||||||
|
|
||||||
|
## Platforms
|
||||||
|
### NIH NCI IDC
|
||||||
|
|
||||||
|
- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188.
|
||||||
|
|
||||||
|
### NIH NCI TCIA
|
||||||
|
|
||||||
|
- Clark, K., Vendt, B., Smith, K., Freymann, J., Kirby, J., Koppel, P., Moore, S., Phillips, S., Maffitt, D., Pringle, M., Tarbox, L., & Prior, F. (2013). The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository. Journal of Digital Imaging, 26(6), 1045–1057. https://doi.org/10.1007/s10278-013-9622-7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Datasets
|
||||||
|
Below you can find the StudyInstanceUID of the studies that are used in the demo page along with their citations.
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.267424821384663813780850856506829388886
|
||||||
|
|
||||||
|
Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm (Vestibular-Schwannoma-SEG)
|
||||||
|
|
||||||
|
- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Bisdas, S., Dimitriadis, A., Grishchuck, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.9YTJ-5Q73
|
||||||
|
|
||||||
|
- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Dimitriadis, A., Grishchuk, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S. R., Bisdas, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of vestibular schwannoma from MRI, an open annotated dataset and baseline algorithm. In Scientific Data (Vol. 8, Issue 1). Springer Science and Business Media LLC. https://doi.org/10.1038/s41597-021-01064-w
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339
|
||||||
|
|
||||||
|
|
||||||
|
ACRIN-NSCLC-FDG-PET (ACRIN 6668)
|
||||||
|
|
||||||
|
- Kinahan, P., Muzi, M., Bialecki, B., Herman, B., & Coombs, L. (2019). Data from the ACRIN 6668 Trial NSCLC-FDG-PET (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/tcia.2019.30ilqfcl
|
||||||
|
|
||||||
|
- Machtay, M., Duan, F., Siegel, B. A., Snyder, B. S., Gorelick, J. J., Reddin, J. S., Munden, R., Johnson, D. W., Wilf, L. H., DeNittis, A., Sherwin, N., Cho, K. H., Kim, S., Videtic, G., Neumann, D. R., Komaki, R., Macapinlac, H., Bradley, J. D., & Alavi, A. (2013). Prediction of Survival by [18F]Fluorodeoxyglucose Positron Emission Tomography in Patients With Locally Advanced Non–Small-Cell Lung Cancer Undergoing Definitive Chemoradiation Therapy: Results of the ACRIN 6668/RTOG 0235 Trial. In Journal of Clinical Oncology (Vol. 31, Issue 30, pp. 3823–3830). American Society of Clinical Oncology (ASCO). https://doi.org/10.1200/jco.2012.47.5947
|
||||||
|
|
||||||
|
|
||||||
|
### 2.25.103659964951665749659160840573802789777
|
||||||
|
|
||||||
|
The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM)
|
||||||
|
|
||||||
|
- Scarpace, L., Mikkelsen, T., Cha, S., Rao, S., Tekchandani, S., Gutman, D., Saltz, J. H., Erickson, B. J., Pedano, N., Flanders, A. E., Barnholtz-Sloan, J., Ostrom, Q., Barboriak, D., & Pierce, L. J. (2016). The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM) (Version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.RNYFUYE9
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458
|
||||||
|
|
||||||
|
Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT)
|
||||||
|
|
||||||
|
|
||||||
|
- Tong T., Li M. (2022) Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT) [Dataset]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/p5k5-tg43
|
||||||
|
|
||||||
|
- Li, M., Gong, J., Bao, Y., Huang, D., Peng, J., & Tong, T. (2022). Special issue “The advance of solid tumor research in China”: Prognosis prediction for stage II colorectal cancer by fusing computed tomography radiomics and deep‐learning features of primary lesions and peripheral lymph nodes. In International Journal of Cancer. Wiley. https://doi.org/10.1002/ijc.34053
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.3023.4024.215308722288168917637555384485
|
||||||
|
|
||||||
|
The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC)
|
||||||
|
|
||||||
|
- Roche, C., Bonaccio, E., & Filippini, J. (2016). The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC) (Version 3) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.CX6YLSUX
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627
|
||||||
|
|
||||||
|
BREAST-DIAGNOSIS
|
||||||
|
|
||||||
|
|
||||||
|
- Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785
|
||||||
|
|
||||||
|
Multimodality annotated HCC cases with and without advanced imaging segmentation (HCC-TACE-Seg)
|
||||||
|
|
||||||
|
|
||||||
|
- Moawad, A. W., Fuentes, D., Morshid, A., Khalaf, A. M., Elmohr, M. M., Abusaif, A., Hazle, J. D., Kaseb, A. O., Hassan, M., Mahvash, A., Szklaruk, J., Qayyom, A., & Elsayes, K. (2021). Multimodality annotated HCC cases with and without advanced imaging segmentation [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.5FNA-0924
|
||||||
|
|
||||||
|
- Morshid, A., Elsayes, K. M., Khalaf, A. M., Elmohr, M. M., Yu, J., Kaseb, A. O., Hassan, M., Mahvash, A., Wang, Z., Hazle, J. D., & Fuentes, D. (2019). A Machine Learning Model to Predict Hepatocellular Carcinoma Response to Transcatheter Arterial Chemoembolization. Radiology: Artificial Intelligence, 1(5), e180021. https://doi.org/10.1148/ryai.2019180021
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.1188.2803.137585363493444318569098508293
|
||||||
|
|
||||||
|
Ultrasound data of a variety of liver masses (B-mode-and-CEUS-Liver)
|
||||||
|
|
||||||
|
- Eisenbrey, J., Lyshchik, A., & Wessner, C. (2021). Ultrasound data of a variety of liver masses [Data set]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/TCIA.2021.v4z7-tc39
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.32722.99.99.62087908186665265759322018723889952421
|
||||||
|
|
||||||
|
NSCLC-Radiomics
|
||||||
|
|
||||||
|
- Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2019). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI
|
||||||
|
|
||||||
|
|
||||||
|
- Aerts, H. J. W. L., Velazquez, E. R., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014, June 3). Decoding tumour phenotype by noninvasive imaging using a quantitative radiomics approach. Nature Communications. Nature Publishing Group. https://doi.org/10.1038/ncomms5006 (link)
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520
|
||||||
|
|
||||||
|
QIN-PROSTATE-Repeatability
|
||||||
|
|
||||||
|
- Fedorov, A; Schwier, M; Clunie, D; Herz, C; Pieper, S; Kikinis, R; Tempany, C; Fennessy, F. (2018). Data From QIN-PROSTATE-Repeatability. The Cancer Imaging Archive. DOI: 10.7937/K9/TCIA.2018.MR1CKGND
|
||||||
|
|
||||||
|
|
||||||
|
- Fedorov A, Vangel MG, Tempany CM, Fennessy FM. Multiparametric Magnetic Resonance Imaging of the Prostate: Repeatability of Volume and Apparent Diffusion Coefficient Quantification. Investigative Radiology. 52, 538–546 (2017). DOI: 10.1097/RLI.0000000000000382
|
||||||
|
|
||||||
|
- Fedorov, A., Schwier, M., Clunie, D., Herz, C., Pieper, S., Kikinis,R., Tempany, C. & Fennessy, F. An annotated test-retest collection of prostate multiparametric MRI. Scientific Data 5, 180281 (2018). DOI:
|
||||||
|
|
||||||
|
### 2.25.141277760791347900862109212450152067508
|
||||||
|
|
||||||
|
The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC)
|
||||||
|
|
||||||
|
- National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2018). The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC) (Version 10) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.OBLAMN27
|
||||||
|
|
||||||
|
- The CPTAC program requests that publications using data from this program include the following statement: “Data used in this publication were generated by the National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC).”
|
||||||
|
|
||||||
|
|
||||||
|
### 2.25.275741864483510678566144889372061815320
|
||||||
|
|
||||||
|
National Lung Screening Trial
|
||||||
|
|
||||||
|
- National Lung Screening Trial Research Team. (2013). Data from the National Lung Screening Trial (NLST) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.HMQ8-J677
|
||||||
|
|
||||||
|
- National Lung Screening Trial Research Team*; Aberle DR, Adams AM, Berg CD, Black WC, Clapp JD, Fagerstrom RM, Gareen IF, Gatsonis C, Marcus PM, Sicks JD (2011). Reduced Lung-Cancer Mortality with Low-Dose Computed Tomographic Screening. New England Journal of Medicine, 365(5), 395–409. https://doi.org/10.1056/nejmoa1102873
|
||||||
|
|
||||||
|
|
||||||
|
### 1.3.6.1.4.1.14519.5.2.1.99.1071.26968527900428638961173806140069
|
||||||
|
|
||||||
|
Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU)
|
||||||
|
|
||||||
|
- Saltz, J., Saltz, M., Prasanna, P., Moffitt, R., Hajagos, J., Bremer, E., Balsamo, J., & Kurc, T. (2021). Stony Brook University COVID-19 Positive Cases [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.BBAG-2923
|
||||||
|
|
||||||
|
|
||||||
|
### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2
|
||||||
|
|
||||||
|
https://data.kitware.com/
|
||||||
@ -1,4 +1,6 @@
|
|||||||
// https://babeljs.io/docs/en/options#babelrcroots
|
// https://babeljs.io/docs/en/options#babelrcroots
|
||||||
|
const { extendDefaultPlugins } = require('svgo');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||||
presets: [
|
presets: [
|
||||||
@ -7,7 +9,19 @@ module.exports = {
|
|||||||
'@babel/preset-typescript',
|
'@babel/preset-typescript',
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
[
|
||||||
'inline-react-svg',
|
'inline-react-svg',
|
||||||
|
{
|
||||||
|
svgo: {
|
||||||
|
plugins: extendDefaultPlugins([
|
||||||
|
{
|
||||||
|
name: 'removeViewBox',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||||
'@babel/plugin-transform-typescript',
|
'@babel/plugin-transform-typescript',
|
||||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||||
|
|||||||
8
extensions/cornerstone-dicom-rt/.webpack/webpack.dev.js
Normal file
8
extensions/cornerstone-dicom-rt/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
};
|
||||||
63
extensions/cornerstone-dicom-rt/.webpack/webpack.prod.js
Normal file
63
extensions/cornerstone-dicom-rt/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const pkg = require('../package.json');
|
||||||
|
|
||||||
|
const outputFile = 'index.umd.js';
|
||||||
|
const rootDir = path.resolve(__dirname, '../');
|
||||||
|
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
|
||||||
|
|
||||||
|
// Todo: add ESM build for the extension in addition to umd build
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: rootDir + '/' + pkg.module,
|
||||||
|
devtool: 'source-map',
|
||||||
|
output: {
|
||||||
|
path: outputFolder,
|
||||||
|
filename: outputFile,
|
||||||
|
library: pkg.name,
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
chunkFilename: '[name].chunk.js',
|
||||||
|
umdNamedDefine: true,
|
||||||
|
globalObject: "typeof self !== 'undefined' ? self : this",
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
react: {
|
||||||
|
root: 'React',
|
||||||
|
commonjs2: 'react',
|
||||||
|
commonjs: 'react',
|
||||||
|
amd: 'react',
|
||||||
|
},
|
||||||
|
'@ohif/core': {
|
||||||
|
commonjs2: '@ohif/core',
|
||||||
|
commonjs: '@ohif/core',
|
||||||
|
amd: '@ohif/core',
|
||||||
|
root: '@ohif/core',
|
||||||
|
},
|
||||||
|
'@ohif/ui': {
|
||||||
|
commonjs2: '@ohif/ui',
|
||||||
|
commonjs: '@ohif/ui',
|
||||||
|
amd: '@ohif/ui',
|
||||||
|
root: '@ohif/ui',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /(node_modules|bower_components)/,
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: [path.resolve('./node_modules'), path.resolve('./src')],
|
||||||
|
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
20
extensions/cornerstone-dicom-rt/LICENSE
Normal file
20
extensions/cornerstone-dicom-rt/LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Open Health Imaging Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
13
extensions/cornerstone-dicom-rt/README.md
Normal file
13
extensions/cornerstone-dicom-rt/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# dicom-rt
|
||||||
|
## Description
|
||||||
|
|
||||||
|
DICOM RT read workflow. This extension will allow you to load a DICOM RTSS image
|
||||||
|
and display it in OHIF.
|
||||||
|
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
OHIF
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
44
extensions/cornerstone-dicom-rt/babel.config.js
Normal file
44
extensions/cornerstone-dicom-rt/babel.config.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
|
||||||
|
env: {
|
||||||
|
test: {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
modules: 'commonjs',
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
],
|
||||||
|
'@babel/preset-react',
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
'@babel/plugin-transform-regenerator',
|
||||||
|
'@babel/plugin-transform-runtime',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
presets: [
|
||||||
|
// WebPack handles ES6 --> Target Syntax
|
||||||
|
['@babel/preset-env', { modules: false }],
|
||||||
|
'@babel/preset-react',
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
],
|
||||||
|
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
presets: [
|
||||||
|
// WebPack handles ES6 --> Target Syntax
|
||||||
|
['@babel/preset-env', { modules: false }],
|
||||||
|
'@babel/preset-react',
|
||||||
|
"@babel/preset-typescript",
|
||||||
|
],
|
||||||
|
plugins: ['react-hot-loader/babel'],
|
||||||
|
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
71
extensions/cornerstone-dicom-rt/package.json
Normal file
71
extensions/cornerstone-dicom-rt/package.json
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "DICOM RT read workflow",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/umd/@ohif/dicom-rt/index.umd.js",
|
||||||
|
"module": "src/index.tsx",
|
||||||
|
"files": [
|
||||||
|
"dist/**",
|
||||||
|
"public/**",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"keywords": [
|
||||||
|
"ohif-extension"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.18.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"dev:dicom-seg": "yarn run dev",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"start": "yarn run dev"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "^3.0.0",
|
||||||
|
"@ohif/extension-default": "^3.0.0",
|
||||||
|
"@ohif/extension-cornerstone": "^3.0.0",
|
||||||
|
"@ohif/i18n": "^1.0.0",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-i18next": "^10.11.0",
|
||||||
|
"react-router": "^6.3.0",
|
||||||
|
"react-router-dom": "^6.3.0",
|
||||||
|
"webpack": "^5.50.0",
|
||||||
|
"webpack-merge": "^5.7.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"react-color": "^2.19.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.21.4",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
|
"@babel/plugin-transform-arrow-functions": "^7.2.0",
|
||||||
|
"@babel/plugin-transform-regenerator": "^7.16.7",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||||
|
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||||
|
"@babel/preset-env": "^7.5.0",
|
||||||
|
"@babel/preset-react": "^7.16.7",
|
||||||
|
"babel-eslint": "^8.0.3",
|
||||||
|
"babel-loader": "^8.0.0-beta.4",
|
||||||
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"copy-webpack-plugin": "^10.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^14.1.0",
|
||||||
|
"eslint": "^5.0.1",
|
||||||
|
"eslint-loader": "^2.0.0",
|
||||||
|
"webpack": "^5.50.0",
|
||||||
|
"webpack-merge": "^5.7.3",
|
||||||
|
"webpack-cli": "^4.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
207
extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.js
Normal file
207
extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import { SOPClassHandlerId } from './id';
|
||||||
|
import loadRTStruct from './loadRTStruct';
|
||||||
|
|
||||||
|
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
|
||||||
|
|
||||||
|
let loadPromises = {};
|
||||||
|
|
||||||
|
function _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
|
) {
|
||||||
|
const instance = instances[0];
|
||||||
|
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPClassUID,
|
||||||
|
wadoRoot,
|
||||||
|
wadoUri,
|
||||||
|
wadoUriRoot,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
const displaySet = {
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
loading: false,
|
||||||
|
isReconstructable: false, // by default for now since it is a volumetric SEG currently
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPClassHandlerId,
|
||||||
|
SOPClassUID,
|
||||||
|
referencedImages: null,
|
||||||
|
referencedSeriesInstanceUID: null,
|
||||||
|
referencedDisplaySetInstanceUID: null,
|
||||||
|
isDerivedDisplaySet: true,
|
||||||
|
isLoaded: false,
|
||||||
|
isHydrated: false,
|
||||||
|
structureSet: null,
|
||||||
|
sopClassUids,
|
||||||
|
instance,
|
||||||
|
wadoRoot,
|
||||||
|
wadoUriRoot,
|
||||||
|
wadoUri,
|
||||||
|
isOverlayDisplaySet: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||||
|
if (
|
||||||
|
instance.ReferencedFrameOfReferenceSequence &&
|
||||||
|
!instance.ReferencedSeriesSequence
|
||||||
|
) {
|
||||||
|
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
instance.ReferencedFrameOfReferenceSequence
|
||||||
|
);
|
||||||
|
referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referencedSeriesSequence) {
|
||||||
|
throw new Error('ReferencedSeriesSequence is missing for the RTSTRUCT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedSeries = referencedSeriesSequence[0];
|
||||||
|
|
||||||
|
displaySet.referencedImages =
|
||||||
|
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||||
|
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||||
|
|
||||||
|
displaySet.getReferenceDisplaySet = () => {
|
||||||
|
const { DisplaySetService } = servicesManager.services;
|
||||||
|
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||||
|
displaySet.referencedSeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||||
|
throw new Error('Referenced DisplaySet is missing for the RT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = referencedDisplaySets[0];
|
||||||
|
|
||||||
|
displaySet.referencedDisplaySetInstanceUID =
|
||||||
|
referencedDisplaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
return referencedDisplaySet;
|
||||||
|
};
|
||||||
|
|
||||||
|
displaySet.load = ({ headers }) =>
|
||||||
|
_load(displaySet, servicesManager, extensionManager, headers);
|
||||||
|
|
||||||
|
return [displaySet];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||||
|
const { SOPInstanceUID } = rtDisplaySet;
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
if (
|
||||||
|
(rtDisplaySet.loading || rtDisplaySet.isLoaded) &&
|
||||||
|
loadPromises[SOPInstanceUID] &&
|
||||||
|
_segmentationExistsInCache(rtDisplaySet, segmentationService)
|
||||||
|
) {
|
||||||
|
return loadPromises[SOPInstanceUID];
|
||||||
|
}
|
||||||
|
|
||||||
|
rtDisplaySet.loading = true;
|
||||||
|
|
||||||
|
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||||
|
// and also return the same promise to any other callers.
|
||||||
|
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||||
|
if (!rtDisplaySet.structureSet) {
|
||||||
|
const structureSet = await loadRTStruct(
|
||||||
|
extensionManager,
|
||||||
|
rtDisplaySet,
|
||||||
|
rtDisplaySet.getReferenceDisplaySet(),
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
|
||||||
|
rtDisplaySet.structureSet = structureSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suppressEvents = true;
|
||||||
|
segmentationService
|
||||||
|
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
|
||||||
|
.then(() => {
|
||||||
|
rtDisplaySet.loading = false;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
rtDisplaySet.loading = false;
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadPromises[SOPInstanceUID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
) {
|
||||||
|
const ReferencedSeriesSequence = [];
|
||||||
|
|
||||||
|
ReferencedFrameOfReferenceSequence.forEach(referencedFrameOfReference => {
|
||||||
|
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
RTReferencedStudySequence.forEach(rtReferencedStudy => {
|
||||||
|
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
RTReferencedSeriesSequence.forEach(rtReferencedSeries => {
|
||||||
|
const ReferencedInstanceSequence = [];
|
||||||
|
const { ContourImageSequence, SeriesInstanceUID } = rtReferencedSeries;
|
||||||
|
|
||||||
|
ContourImageSequence.forEach(contourImage => {
|
||||||
|
ReferencedInstanceSequence.push({
|
||||||
|
ReferencedSOPInstanceUID: contourImage.ReferencedSOPInstanceUID,
|
||||||
|
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const referencedSeries = {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
ReferencedInstanceSequence,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReferencedSeriesSequence.push(referencedSeries);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return ReferencedSeriesSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _segmentationExistsInCache(rtDisplaySet, segmentationService) {
|
||||||
|
// Todo: fix this
|
||||||
|
return false;
|
||||||
|
// This should be abstracted with the CornerstoneCacheService
|
||||||
|
const rtContourId = rtDisplaySet.displaySetInstanceUID;
|
||||||
|
const contour = segmentationService.getContour(rtContourId);
|
||||||
|
|
||||||
|
return contour !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'dicom-rt',
|
||||||
|
sopClassUids,
|
||||||
|
getDisplaySetsFromSeries: instances => {
|
||||||
|
return _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSopClassHandlerModule;
|
||||||
7
extensions/cornerstone-dicom-rt/src/id.js
Normal file
7
extensions/cornerstone-dicom-rt/src/id.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
const id = packageJson.name;
|
||||||
|
const SOPClassHandlerName = 'dicom-rt';
|
||||||
|
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||||
|
|
||||||
|
export { id, SOPClassHandlerId, SOPClassHandlerName };
|
||||||
61
extensions/cornerstone-dicom-rt/src/index.tsx
Normal file
61
extensions/cornerstone-dicom-rt/src/index.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { id } from './id';
|
||||||
|
import React from 'react';
|
||||||
|
import { Types } from '@ohif/core';
|
||||||
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
|
|
||||||
|
const Component = React.lazy(() => {
|
||||||
|
return import(
|
||||||
|
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const OHIFCornerstoneRTViewport = props => {
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Component {...props} />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can remove any of the following modules if you don't need them.
|
||||||
|
*/
|
||||||
|
const extension: Types.Extensions.Extension = {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
* You ID can be anything you want, but it should be unique.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
|
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||||
|
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||||
|
*/
|
||||||
|
getViewportModule({
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
}: Types.Extensions.ExtensionParams) {
|
||||||
|
const ExtendedOHIFCornerstoneRTViewport = props => {
|
||||||
|
return (
|
||||||
|
<OHIFCornerstoneRTViewport
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{ name: 'dicom-rt', component: ExtendedOHIFCornerstoneRTViewport }];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* SopClassHandlerModule should provide a list of sop class handlers that will be
|
||||||
|
* available in OHIF for Modes to consume and use to create displaySets from Series.
|
||||||
|
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
|
||||||
|
* Examples include the default sop class handler provided by the default extension
|
||||||
|
*/
|
||||||
|
getSopClassHandlerModule,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default extension;
|
||||||
318
extensions/cornerstone-dicom-rt/src/loadRTStruct.js
Normal file
318
extensions/cornerstone-dicom-rt/src/loadRTStruct.js
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import dcmjs from 'dcmjs';
|
||||||
|
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
|
||||||
|
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||||
|
|
||||||
|
async function checkAndLoadContourData(instance, datasource) {
|
||||||
|
if (!instance || !instance.ROIContourSequence) {
|
||||||
|
return Promise.reject('Invalid instance object or ROIContourSequence');
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
for (const ROIContour of instance.ROIContourSequence) {
|
||||||
|
if (!ROIContour || !ROIContour.ContourSequence) {
|
||||||
|
return Promise.reject('Invalid ROIContour or ContourSequence');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Contour of ROIContour.ContourSequence) {
|
||||||
|
if (!Contour || !Contour.ContourData) {
|
||||||
|
return Promise.reject('Invalid Contour or ContourData');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contourData = Contour.ContourData;
|
||||||
|
counter++;
|
||||||
|
if (Array.isArray(contourData)) {
|
||||||
|
promises.push(Promise.resolve(contourData));
|
||||||
|
} else if (contourData && contourData.BulkDataURI) {
|
||||||
|
const bulkDataURI = contourData.BulkDataURI;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!datasource ||
|
||||||
|
!datasource.retrieve ||
|
||||||
|
!datasource.retrieve.bulkDataURI
|
||||||
|
) {
|
||||||
|
return Promise.reject(
|
||||||
|
'Invalid datasource object or retrieve function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkDataPromise = datasource.retrieve.bulkDataURI({
|
||||||
|
BulkDataURI: bulkDataURI,
|
||||||
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
|
SOPInstanceUID: instance.SOPInstanceUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(bulkDataPromise);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(`Invalid ContourData: ${contourData}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const flattenedPromises = promises.flat();
|
||||||
|
const resolvedPromises = await Promise.allSettled(flattenedPromises);
|
||||||
|
|
||||||
|
// Modify contourData and replace it in its corresponding ROIContourSequence's Contour's contourData
|
||||||
|
let index = 0;
|
||||||
|
instance.ROIContourSequence.forEach((ROIContour, roiIndex) => {
|
||||||
|
ROIContour.ContourSequence.forEach((Contour, contourIndex) => {
|
||||||
|
const promise = resolvedPromises[index++];
|
||||||
|
|
||||||
|
if (promise.status === 'fulfilled') {
|
||||||
|
const uint8Array = new Uint8Array(promise.value);
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
const dataUint8Array = textDecoder.decode(uint8Array);
|
||||||
|
if (
|
||||||
|
typeof dataUint8Array === 'string' &&
|
||||||
|
dataUint8Array.includes('\\')
|
||||||
|
) {
|
||||||
|
const numSlashes = (dataUint8Array.match(/\\/g) || []).length;
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = dataUint8Array.indexOf('\\', startIndex);
|
||||||
|
let numbersParsed = 0;
|
||||||
|
const ContourData = [];
|
||||||
|
|
||||||
|
while (numbersParsed !== numSlashes + 1) {
|
||||||
|
const str = dataUint8Array.substring(startIndex, endIndex);
|
||||||
|
let value = parseFloat(str);
|
||||||
|
|
||||||
|
ContourData.push(value);
|
||||||
|
startIndex = endIndex + 1;
|
||||||
|
endIndex = dataUint8Array.indexOf('\\', startIndex);
|
||||||
|
endIndex === -1 ? (endIndex = dataUint8Array.length) : endIndex;
|
||||||
|
numbersParsed++;
|
||||||
|
}
|
||||||
|
Contour.ContourData = ContourData;
|
||||||
|
} else {
|
||||||
|
Contour.ContourData = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(promise.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function loadRTStruct(
|
||||||
|
extensionManager,
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
headers
|
||||||
|
) {
|
||||||
|
const utilityModule = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.utilityModule.common'
|
||||||
|
);
|
||||||
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||||
|
const { useBulkDataURI } = dataSource.getConfig?.() || {};
|
||||||
|
|
||||||
|
const { dicomLoaderService } = utilityModule.exports;
|
||||||
|
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
referencedDisplaySet
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set here is loading is asynchronous.
|
||||||
|
// If this function throws its set back to false.
|
||||||
|
rtStructDisplaySet.isLoaded = true;
|
||||||
|
let instance = rtStructDisplaySet.instance;
|
||||||
|
|
||||||
|
if (!useBulkDataURI) {
|
||||||
|
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
null,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
|
||||||
|
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||||
|
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(
|
||||||
|
dicomData.dict
|
||||||
|
);
|
||||||
|
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||||
|
instance = rtStructDataset;
|
||||||
|
} else {
|
||||||
|
await checkAndLoadContourData(instance, dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
StructureSetROISequence,
|
||||||
|
ROIContourSequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
// Define our structure set entry and add it to the rtstruct module state.
|
||||||
|
const structureSet = {
|
||||||
|
StructureSetLabel: instance.StructureSetLabel,
|
||||||
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
|
ROIContours: [],
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||||
|
const ROIContour = ROIContourSequence[i];
|
||||||
|
const { ContourSequence } = ROIContour;
|
||||||
|
|
||||||
|
if (!ContourSequence) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSupported = false;
|
||||||
|
|
||||||
|
const ContourSequenceArray = _toArray(ContourSequence);
|
||||||
|
|
||||||
|
const contourPoints = [];
|
||||||
|
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
ContourData,
|
||||||
|
NumberOfContourPoints,
|
||||||
|
ContourGeometricType,
|
||||||
|
} = ContourSequenceArray[c];
|
||||||
|
|
||||||
|
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||||
|
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let isSupported = false;
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||||
|
points.push({
|
||||||
|
x: ContourData[p],
|
||||||
|
y: ContourData[p + 1],
|
||||||
|
z: ContourData[p + 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ContourGeometricType) {
|
||||||
|
case 'CLOSED_PLANAR':
|
||||||
|
case 'OPEN_PLANAR':
|
||||||
|
case 'POINT':
|
||||||
|
isSupported = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
contourPoints.push({
|
||||||
|
numberOfPoints: NumberOfContourPoints,
|
||||||
|
points,
|
||||||
|
type: ContourGeometricType,
|
||||||
|
isSupported,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour,
|
||||||
|
contourPoints,
|
||||||
|
isSupported
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return structureSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||||
|
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||||
|
imageIdSopInstanceUidPairsEntry =>
|
||||||
|
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
return imageIdSopInstanceUidPairsEntry
|
||||||
|
? imageIdSopInstanceUidPairsEntry.imageId
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
||||||
|
return referencedDisplaySet.images.map(image => {
|
||||||
|
return {
|
||||||
|
imageId: image.imageId,
|
||||||
|
sopInstanceUID: image.SOPInstanceUID,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour,
|
||||||
|
contourPoints,
|
||||||
|
isSupported
|
||||||
|
) {
|
||||||
|
const StructureSetROI = StructureSetROISequence.find(
|
||||||
|
structureSetROI =>
|
||||||
|
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const ROIContourData = {
|
||||||
|
ROINumber: StructureSetROI.ROINumber,
|
||||||
|
ROIName: StructureSetROI.ROIName,
|
||||||
|
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||||
|
ROIDescription: StructureSetROI.ROIDescription,
|
||||||
|
isSupported,
|
||||||
|
contourPoints,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_setROIContourDataColor(ROIContour, ROIContourData);
|
||||||
|
|
||||||
|
if (RTROIObservationsSequence) {
|
||||||
|
// If present, add additional RTROIObservations metadata.
|
||||||
|
_setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
structureSet.ROIContours.push(ROIContourData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||||
|
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
||||||
|
|
||||||
|
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
||||||
|
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
||||||
|
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ROIDisplayColor) {
|
||||||
|
ROIContourData.colorArray = [...ROIDisplayColor];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
const RTROIObservations = RTROIObservationsSequence.find(
|
||||||
|
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (RTROIObservations) {
|
||||||
|
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||||
|
const {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
} = RTROIObservations;
|
||||||
|
|
||||||
|
ROIContourData.RTROIObservations = {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toArray(objOrArray) {
|
||||||
|
return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
|
||||||
|
}
|
||||||
70
extensions/cornerstone-dicom-rt/src/utils/_hydrateRT.ts
Normal file
70
extensions/cornerstone-dicom-rt/src/utils/_hydrateRT.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
async function _hydrateRTDisplaySet({
|
||||||
|
rtDisplaySet,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
segmentationService,
|
||||||
|
hangingProtocolService,
|
||||||
|
viewportGridService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||||
|
|
||||||
|
let segmentationId = null;
|
||||||
|
|
||||||
|
// We need the hydration to notify panels about the new segmentation added
|
||||||
|
const suppressEvents = false;
|
||||||
|
|
||||||
|
segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||||
|
rtDisplaySet,
|
||||||
|
segmentationId,
|
||||||
|
suppressEvents
|
||||||
|
);
|
||||||
|
|
||||||
|
segmentationService.hydrateSegmentation(rtDisplaySet.displaySetInstanceUID);
|
||||||
|
|
||||||
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
|
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||||
|
viewportIndex,
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||||
|
|
||||||
|
// Todo: fix this after we have a better way for stack viewport segmentations
|
||||||
|
|
||||||
|
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||||
|
// is being displayed, if so we need to update the viewport to use volume viewport
|
||||||
|
// (if already is not using it) since Cornerstone3D currently only supports
|
||||||
|
// volume viewport for segmentation
|
||||||
|
viewports.forEach((viewport, index) => {
|
||||||
|
if (index === viewportIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||||
|
viewport.displaySetInstanceUIDs,
|
||||||
|
rtDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldDisplaySeg) {
|
||||||
|
updatedViewports.push({
|
||||||
|
viewportIndex: index,
|
||||||
|
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||||
|
viewportOptions: {
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do the entire update at once
|
||||||
|
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default _hydrateRTDisplaySet;
|
||||||
12
extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts
Normal file
12
extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
function createRTToolGroupAndAddTools(
|
||||||
|
ToolGroupService,
|
||||||
|
customizationService,
|
||||||
|
toolGroupId
|
||||||
|
) {
|
||||||
|
const { tools } =
|
||||||
|
customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||||
|
|
||||||
|
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createRTToolGroupAndAddTools;
|
||||||
70
extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts
Normal file
70
extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import hydrateRTDisplaySet from './_hydrateRT';
|
||||||
|
|
||||||
|
const RESPONSE = {
|
||||||
|
NO_NEVER: -1,
|
||||||
|
CANCEL: 0,
|
||||||
|
HYDRATE_SEG: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
function promptHydrateRT({
|
||||||
|
servicesManager,
|
||||||
|
rtDisplaySet,
|
||||||
|
viewportIndex,
|
||||||
|
toolGroupId = 'default',
|
||||||
|
}) {
|
||||||
|
const { uiViewportDialogService } = servicesManager.services;
|
||||||
|
|
||||||
|
return new Promise(async function(resolve, reject) {
|
||||||
|
const promptResult = await _askHydrate(
|
||||||
|
uiViewportDialogService,
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||||
|
const isHydrated = await hydrateRTDisplaySet({
|
||||||
|
rtDisplaySet,
|
||||||
|
viewportIndex,
|
||||||
|
toolGroupId,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(isHydrated);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const message = 'Do you want to open this Segmentation?';
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
type: 'secondary',
|
||||||
|
text: 'No',
|
||||||
|
value: RESPONSE.CANCEL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
text: 'Yes',
|
||||||
|
value: RESPONSE.HYDRATE_SEG,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const onSubmit = result => {
|
||||||
|
uiViewportDialogService.hide();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
uiViewportDialogService.show({
|
||||||
|
viewportIndex,
|
||||||
|
type: 'info',
|
||||||
|
message,
|
||||||
|
actions,
|
||||||
|
onSubmit,
|
||||||
|
onOutsideClick: () => {
|
||||||
|
uiViewportDialogService.hide();
|
||||||
|
resolve(RESPONSE.CANCEL);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default promptHydrateRT;
|
||||||
@ -0,0 +1,399 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import OHIF, { utils } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
ViewportActionBar,
|
||||||
|
useViewportGrid,
|
||||||
|
LoadingIndicatorTotalPercent,
|
||||||
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
|
||||||
|
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||||
|
import _getStatusComponent from './_getStatusComponent';
|
||||||
|
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||||
|
import _hydrateRTDisplaySet from '../utils/_hydrateRT';
|
||||||
|
|
||||||
|
const { formatDate } = utils;
|
||||||
|
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||||
|
|
||||||
|
function OHIFCornerstoneRTViewport(props) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
displaySets,
|
||||||
|
viewportOptions,
|
||||||
|
viewportIndex,
|
||||||
|
viewportLabel,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
displaySetService,
|
||||||
|
toolGroupService,
|
||||||
|
segmentationService,
|
||||||
|
uiNotificationService,
|
||||||
|
customizationService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||||
|
|
||||||
|
// RT viewport will always have a single display set
|
||||||
|
if (displaySets.length > 1) {
|
||||||
|
throw new Error('RT viewport should only have a single display set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rtDisplaySet = displaySets[0];
|
||||||
|
|
||||||
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
|
// States
|
||||||
|
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||||
|
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||||
|
|
||||||
|
// Hydration means that the RT is opened and segments are loaded into the
|
||||||
|
// segmentation panel, and RT is also rendered on any viewport that is in the
|
||||||
|
// same frameOfReferenceUID as the referencedSeriesUID of the RT. However,
|
||||||
|
// loading basically means RT loading over network and bit unpacking of the
|
||||||
|
// RT data.
|
||||||
|
const [isHydrated, setIsHydrated] = useState(rtDisplaySet.isHydrated);
|
||||||
|
const [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded);
|
||||||
|
const [element, setElement] = useState(null);
|
||||||
|
const [processingProgress, setProcessingProgress] = useState({
|
||||||
|
percentComplete: null,
|
||||||
|
totalSegments: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// refs
|
||||||
|
const referencedDisplaySetRef = useRef(null);
|
||||||
|
|
||||||
|
const { viewports, activeViewportIndex } = viewportGrid;
|
||||||
|
|
||||||
|
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
|
||||||
|
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||||
|
referencedDisplaySet
|
||||||
|
);
|
||||||
|
|
||||||
|
referencedDisplaySetRef.current = {
|
||||||
|
displaySet: referencedDisplaySet,
|
||||||
|
metadata: referencedDisplaySetMetadata,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||||
|
* has enabled the element. Note: we delegate all the image rendering to
|
||||||
|
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||||
|
* the image rendering, element enabling etc.
|
||||||
|
*/
|
||||||
|
const onElementEnabled = evt => {
|
||||||
|
setElement(evt.detail.element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onElementDisabled = () => {
|
||||||
|
setElement(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
displaySet: referencedDisplaySet,
|
||||||
|
} = referencedDisplaySetRef.current;
|
||||||
|
|
||||||
|
// Todo: jump to the center of the first segment
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||||
|
viewportOptions={{
|
||||||
|
viewportType: 'volume',
|
||||||
|
toolGroupId: toolGroupId,
|
||||||
|
orientation: viewportOptions.orientation,
|
||||||
|
viewportId: viewportOptions.viewportId,
|
||||||
|
}}
|
||||||
|
onElementEnabled={onElementEnabled}
|
||||||
|
onElementDisabled={onElementDisabled}
|
||||||
|
></Component>
|
||||||
|
);
|
||||||
|
}, [viewportIndex, rtDisplaySet, toolGroupId]);
|
||||||
|
|
||||||
|
const onSegmentChange = useCallback(
|
||||||
|
direction => {
|
||||||
|
direction = direction === 'left' ? -1 : 1;
|
||||||
|
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||||
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||||
|
|
||||||
|
const { segments } = segmentation;
|
||||||
|
|
||||||
|
const numberOfSegments = Object.keys(segments).length;
|
||||||
|
|
||||||
|
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||||
|
|
||||||
|
// Segment 0 is always background
|
||||||
|
if (newSelectedSegmentIndex >= numberOfSegments - 1) {
|
||||||
|
newSelectedSegmentIndex = 1;
|
||||||
|
} else if (newSelectedSegmentIndex === 0) {
|
||||||
|
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentationService.jumpToSegmentCenter(
|
||||||
|
segmentationId,
|
||||||
|
newSelectedSegmentIndex,
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
setSelectedSegment(newSelectedSegmentIndex);
|
||||||
|
},
|
||||||
|
[selectedSegment]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rtIsLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promptHydrateRT({
|
||||||
|
servicesManager,
|
||||||
|
viewportIndex,
|
||||||
|
rtDisplaySet,
|
||||||
|
}).then(isHydrated => {
|
||||||
|
if (isHydrated) {
|
||||||
|
setIsHydrated(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
|
evt => {
|
||||||
|
if (
|
||||||
|
evt.rtDisplaySet.displaySetInstanceUID ===
|
||||||
|
rtDisplaySet.displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
setRtIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.overlappingSegments) {
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Overlapping Segments',
|
||||||
|
message:
|
||||||
|
'Overlapping segments detected which is not currently supported',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [rtDisplaySet]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
|
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||||
|
({ percentComplete, numSegments }) => {
|
||||||
|
setProcessingProgress({
|
||||||
|
percentComplete,
|
||||||
|
totalSegments: numSegments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [rtDisplaySet]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
Cleanup the SEG viewport when the viewport is destroyed
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||||
|
({ displaySetInstanceUIDs }) => {
|
||||||
|
const activeViewport = viewports[activeViewportIndex];
|
||||||
|
if (
|
||||||
|
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||||
|
) {
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportIndex: activeViewportIndex,
|
||||||
|
displaySetInstanceUIDs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
|
|
||||||
|
if (toolGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolGroup = createRTToolGroupAndAddTools(
|
||||||
|
toolGroupService,
|
||||||
|
customizationService,
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
|
||||||
|
setToolGroupCreated(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// remove the segmentation representations if seg displayset changed
|
||||||
|
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
|
||||||
|
toolGroupService.destroyToolGroup(toolGroupId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsHydrated(rtDisplaySet.isHydrated);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// remove the segmentation representations if seg displayset changed
|
||||||
|
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
referencedDisplaySetRef.current = null;
|
||||||
|
};
|
||||||
|
}, [rtDisplaySet]);
|
||||||
|
|
||||||
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
let childrenWithProps = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!referencedDisplaySetRef.current ||
|
||||||
|
referencedDisplaySet.displaySetInstanceUID !==
|
||||||
|
referencedDisplaySetRef.current.displaySet.displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children && children.length) {
|
||||||
|
childrenWithProps = children.map((child, index) => {
|
||||||
|
return (
|
||||||
|
child &&
|
||||||
|
React.cloneElement(child, {
|
||||||
|
viewportIndex,
|
||||||
|
key: index,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
PatientID,
|
||||||
|
PatientName,
|
||||||
|
PatientSex,
|
||||||
|
PatientAge,
|
||||||
|
SliceThickness,
|
||||||
|
ManufacturerModelName,
|
||||||
|
StudyDate,
|
||||||
|
SeriesDescription,
|
||||||
|
SpacingBetweenSlices,
|
||||||
|
SeriesNumber,
|
||||||
|
} = referencedDisplaySetRef.current.metadata;
|
||||||
|
|
||||||
|
const onStatusClick = async () => {
|
||||||
|
const isHydrated = await _hydrateRTDisplaySet({
|
||||||
|
rtDisplaySet,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsHydrated(isHydrated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewportActionBar
|
||||||
|
onDoubleClick={evt => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
}}
|
||||||
|
onArrowsClick={onSegmentChange}
|
||||||
|
getStatusComponent={() => {
|
||||||
|
return _getStatusComponent({
|
||||||
|
isHydrated,
|
||||||
|
onStatusClick,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
studyData={{
|
||||||
|
label: viewportLabel,
|
||||||
|
useAltStyling: true,
|
||||||
|
studyDate: formatDate(StudyDate),
|
||||||
|
currentSeries: SeriesNumber,
|
||||||
|
seriesDescription: `RT Viewport ${SeriesDescription}`,
|
||||||
|
patientInformation: {
|
||||||
|
patientName: PatientName
|
||||||
|
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||||
|
: '',
|
||||||
|
patientSex: PatientSex || '',
|
||||||
|
patientAge: PatientAge || '',
|
||||||
|
MRN: PatientID || '',
|
||||||
|
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||||
|
spacing:
|
||||||
|
SpacingBetweenSlices !== undefined
|
||||||
|
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||||
|
: '',
|
||||||
|
scanner: ManufacturerModelName || '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
|
{rtIsLoading && (
|
||||||
|
<LoadingIndicatorTotalPercent
|
||||||
|
className="w-full h-full"
|
||||||
|
totalNumbers={processingProgress.totalSegments}
|
||||||
|
percentComplete={processingProgress.percentComplete}
|
||||||
|
loadingText="Loading RTSTRUCT..."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{getCornerstoneViewport()}
|
||||||
|
{childrenWithProps}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OHIFCornerstoneRTViewport.propTypes = {
|
||||||
|
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
|
dataSource: PropTypes.object,
|
||||||
|
children: PropTypes.node,
|
||||||
|
customProps: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIFCornerstoneRTViewport.defaultProps = {
|
||||||
|
customProps: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
|
||||||
|
const image0 = referencedDisplaySet.images[0];
|
||||||
|
const referencedDisplaySetMetadata = {
|
||||||
|
PatientID: image0.PatientID,
|
||||||
|
PatientName: image0.PatientName,
|
||||||
|
PatientSex: image0.PatientSex,
|
||||||
|
PatientAge: image0.PatientAge,
|
||||||
|
SliceThickness: image0.SliceThickness,
|
||||||
|
StudyDate: image0.StudyDate,
|
||||||
|
SeriesDescription: image0.SeriesDescription,
|
||||||
|
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||||
|
SeriesNumber: image0.SeriesNumber,
|
||||||
|
ManufacturerModelName: image0.ManufacturerModelName,
|
||||||
|
SpacingBetweenSlices: image0.SpacingBetweenSlices,
|
||||||
|
};
|
||||||
|
|
||||||
|
return referencedDisplaySetMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OHIFCornerstoneRTViewport;
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Icon, Tooltip } from '@ohif/ui';
|
||||||
|
|
||||||
|
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||||
|
let ToolTipMessage = null;
|
||||||
|
let StatusIcon = null;
|
||||||
|
|
||||||
|
const { t } = useTranslation('Common');
|
||||||
|
const loadStr = t('LOAD');
|
||||||
|
|
||||||
|
switch (isHydrated) {
|
||||||
|
case true:
|
||||||
|
StatusIcon = () => <Icon name="status-alert" />;
|
||||||
|
|
||||||
|
ToolTipMessage = () => (
|
||||||
|
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case false:
|
||||||
|
StatusIcon = () => <Icon name="status-untracked" />;
|
||||||
|
|
||||||
|
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusArea = () => (
|
||||||
|
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||||
|
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||||
|
<StatusIcon />
|
||||||
|
<span className="ml-1">RTSTRUCT</span>
|
||||||
|
</div>
|
||||||
|
{!isHydrated && (
|
||||||
|
<div
|
||||||
|
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||||
|
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||||
|
onMouseUp={onStatusClick}
|
||||||
|
>
|
||||||
|
{loadStr}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ToolTipMessage && (
|
||||||
|
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||||
|
<StatusArea />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!ToolTipMessage && <StatusArea />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 Open Health Imaging Foundation
|
Copyright (c) 2023 Open Health Imaging Foundation
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^12.2.2",
|
||||||
"react-router": "^6.8.1",
|
"react-router": "^6.8.1",
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"webpack": "^5.50.0",
|
"webpack": "^5.50.0",
|
||||||
@ -46,16 +46,16 @@
|
|||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.0",
|
"@babel/core": "^7.21.4",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.2.0",
|
"@babel/plugin-transform-arrow-functions": "^7.2.0",
|
||||||
"@babel/plugin-transform-regenerator": "^7.4.5",
|
"@babel/plugin-transform-regenerator": "^7.16.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.5.0",
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||||
"@babel/preset-env": "^7.5.0",
|
"@babel/preset-env": "^7.5.0",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"babel-eslint": "^8.0.3",
|
"babel-eslint": "^8.0.3",
|
||||||
"babel-loader": "^8.0.0-beta.4",
|
"babel-loader": "^8.0.0-beta.4",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
const segProtocol: Types.HangingProtocol.Protocol = {
|
||||||
|
id: '@ohif/seg',
|
||||||
|
// Don't store this hanging protocol as it applies to the currently active
|
||||||
|
// display set by default
|
||||||
|
// cacheId: null,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'Segmentations',
|
||||||
|
// Just apply this one when specifically listed
|
||||||
|
protocolMatchingRules: [],
|
||||||
|
toolGroupIds: ['default'],
|
||||||
|
// -1 would be used to indicate active only, whereas other values are
|
||||||
|
// the number of required priors referenced - so 0 means active with
|
||||||
|
// 0 or more priors.
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
|
// Default viewport is used to define the viewport when
|
||||||
|
// additional viewports are added using the layout tool
|
||||||
|
defaultViewport: {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportType: 'stack',
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'segDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySetSelectors: {
|
||||||
|
segDisplaySetId: {
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: 'SEG',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
name: 'Segmentations',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: { allowUnmatchedView: true },
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'segDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHangingProtocolModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: segProtocol.id,
|
||||||
|
protocol: segProtocol,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHangingProtocolModule;
|
||||||
|
export { segProtocol };
|
||||||
@ -53,9 +53,11 @@ function _getDisplaySetsFromSeries(
|
|||||||
segments: {},
|
segments: {},
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
instance,
|
instance,
|
||||||
|
instances: [instance],
|
||||||
wadoRoot,
|
wadoRoot,
|
||||||
wadoUriRoot,
|
wadoUriRoot,
|
||||||
wadoUri,
|
wadoUri,
|
||||||
|
isOverlayDisplaySet: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||||
@ -101,9 +103,12 @@ function _getDisplaySetsFromSeries(
|
|||||||
|
|
||||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||||
const { SOPInstanceUID } = segDisplaySet;
|
const { SOPInstanceUID } = segDisplaySet;
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||||
loadPromises[SOPInstanceUID]
|
loadPromises[SOPInstanceUID] &&
|
||||||
|
_segmentationExists(segDisplaySet, segmentationService)
|
||||||
) {
|
) {
|
||||||
return loadPromises[SOPInstanceUID];
|
return loadPromises[SOPInstanceUID];
|
||||||
}
|
}
|
||||||
@ -113,12 +118,6 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
|||||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||||
// and also return the same promise to any other callers.
|
// and also return the same promise to any other callers.
|
||||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||||
const { segmentationService } = servicesManager.services;
|
|
||||||
|
|
||||||
if (_segmentationExistsInCache(segDisplaySet, segmentationService)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!segDisplaySet.segments ||
|
!segDisplaySet.segments ||
|
||||||
Object.keys(segDisplaySet.segments).length === 0
|
Object.keys(segDisplaySet.segments).length === 0
|
||||||
@ -133,11 +132,8 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suppressEvents = true;
|
const suppressEvents = true;
|
||||||
segmentationService.createSegmentationForSEGDisplaySet(
|
segmentationService
|
||||||
segDisplaySet,
|
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
|
||||||
null,
|
|
||||||
suppressEvents
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
segDisplaySet.loading = false;
|
segDisplaySet.loading = false;
|
||||||
resolve();
|
resolve();
|
||||||
@ -175,12 +171,11 @@ async function _loadSegments(extensionManager, segDisplaySet, headers) {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _segmentationExistsInCache(segDisplaySet, segmentationService) {
|
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||||
// This should be abstracted with the CornerstoneCacheService
|
// This should be abstracted with the CornerstoneCacheService
|
||||||
const labelmapVolumeId = segDisplaySet.displaySetInstanceUID;
|
return segmentationService.getSegmentation(
|
||||||
const segVolume = segmentationService.getLabelmapVolume(labelmapVolumeId);
|
segDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
return segVolume !== undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getPixelData(dataset, segments) {
|
function _getPixelData(dataset, segments) {
|
||||||
|
|||||||
@ -3,8 +3,11 @@ import React from 'react';
|
|||||||
|
|
||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule, {
|
||||||
|
protocols,
|
||||||
|
} from './getSopClassHandlerModule';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -30,24 +33,17 @@ const extension = {
|
|||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform any pre-registration tasks here. This is called before the extension
|
|
||||||
* is registered. Usually we run tasks such as: configuring the libraries
|
|
||||||
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
|
|
||||||
* this extension is providing.
|
|
||||||
*/
|
|
||||||
preRegistration: ({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
configuration = {},
|
|
||||||
}) => {},
|
|
||||||
/**
|
/**
|
||||||
* PanelModule should provide a list of panels that will be available in OHIF
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
* for Modes to consume and render. Each panel is defined by a {name,
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||||
*/
|
*/
|
||||||
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
|
getPanelModule: ({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
|
}): Types.Panel[] => {
|
||||||
const wrappedPanelSegmentation = () => {
|
const wrappedPanelSegmentation = () => {
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
@ -68,6 +64,7 @@ const extension = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
getViewportModule({ servicesManager, extensionManager }) {
|
getViewportModule({ servicesManager, extensionManager }) {
|
||||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||||
return (
|
return (
|
||||||
@ -90,6 +87,11 @@ const extension = {
|
|||||||
* Examples include the default sop class handler provided by the default extension
|
* Examples include the default sop class handler provided by the default extension
|
||||||
*/
|
*/
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
|
getHangingProtocolModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default extension;
|
export default extension;
|
||||||
|
|
||||||
|
// Export the protocols separately to allow for extending it at compile time
|
||||||
|
// in other modules
|
||||||
|
export { protocols };
|
||||||
|
|||||||
@ -9,20 +9,13 @@ export default function PanelSegmentation({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
}) {
|
}) {
|
||||||
const {
|
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||||
segmentationService,
|
|
||||||
uiDialogService,
|
|
||||||
viewportGridService,
|
|
||||||
toolGroupService,
|
|
||||||
cornerstoneViewportService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const { t } = useTranslation('PanelSegmentation');
|
const { t } = useTranslation('PanelSegmentation');
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||||
const [
|
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||||
initialSegmentationConfigurations,
|
segmentationService.getConfiguration()
|
||||||
setInitialSegmentationConfigurations,
|
);
|
||||||
] = useState(segmentationService.getConfiguration());
|
|
||||||
|
|
||||||
const [segmentations, setSegmentations] = useState(() =>
|
const [segmentations, setSegmentations] = useState(() =>
|
||||||
segmentationService.getSegmentations()
|
segmentationService.getSegmentations()
|
||||||
@ -62,6 +55,7 @@ export default function PanelSegmentation({
|
|||||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
const segmentations = segmentationService.getSegmentations();
|
const segmentations = segmentationService.getSegmentations();
|
||||||
setSegmentations(segmentations);
|
setSegmentations(segmentations);
|
||||||
|
setSegmentationConfiguration(segmentationService.getConfiguration());
|
||||||
});
|
});
|
||||||
subscriptions.push(unsubscribe);
|
subscriptions.push(unsubscribe);
|
||||||
});
|
});
|
||||||
@ -184,7 +178,7 @@ export default function PanelSegmentation({
|
|||||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSegmentationConfiguration = useCallback(
|
const _setSegmentationConfiguration = useCallback(
|
||||||
(segmentationId, key, value) => {
|
(segmentationId, key, value) => {
|
||||||
segmentationService.setConfiguration({
|
segmentationService.setConfiguration({
|
||||||
segmentationId,
|
segmentationId,
|
||||||
@ -214,54 +208,51 @@ export default function PanelSegmentation({
|
|||||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||||
segmentationConfig={{
|
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||||
initialConfig: initialSegmentationConfigurations,
|
|
||||||
usePercentage: true,
|
|
||||||
}}
|
|
||||||
setRenderOutline={value =>
|
setRenderOutline={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'renderOutline',
|
'renderOutline',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setOutlineOpacityActive={value =>
|
setOutlineOpacityActive={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'outlineOpacity',
|
'outlineOpacity',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setRenderFill={value =>
|
setRenderFill={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'renderFill',
|
'renderFill',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setRenderInactiveSegmentations={value =>
|
setRenderInactiveSegmentations={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'renderInactiveSegmentations',
|
'renderInactiveSegmentations',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setOutlineWidthActive={value =>
|
setOutlineWidthActive={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'outlineWidthActive',
|
'outlineWidthActive',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setFillAlpha={value =>
|
setFillAlpha={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'fillAlpha',
|
'fillAlpha',
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setFillAlphaInactive={value =>
|
setFillAlphaInactive={value =>
|
||||||
setSegmentationConfiguration(
|
_setSegmentationConfiguration(
|
||||||
selectedSegmentationId,
|
selectedSegmentationId,
|
||||||
'fillAlphaInactive',
|
'fillAlphaInactive',
|
||||||
value
|
value
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import React, { useReducer } from 'react';
|
|
||||||
|
|
||||||
// Todo: use defaults in cs3d
|
// Todo: use defaults in cs3d
|
||||||
const initialState = {
|
const initialState = {
|
||||||
renderOutline: true,
|
renderOutline: true,
|
||||||
|
|||||||
@ -1,34 +1,12 @@
|
|||||||
function createSEGToolGroupAndAddTools(
|
function createSEGToolGroupAndAddTools(
|
||||||
toolGroupService,
|
ToolGroupService,
|
||||||
toolGroupId,
|
customizationService,
|
||||||
extensionManager
|
toolGroupId
|
||||||
) {
|
) {
|
||||||
const utilityModule = extensionManager.getModuleEntry(
|
const { tools } =
|
||||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||||
);
|
|
||||||
|
|
||||||
const { toolNames, Enums } = utilityModule.exports;
|
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||||
|
|
||||||
const tools = {
|
|
||||||
active: [
|
|
||||||
{
|
|
||||||
toolName: toolNames.WindowLevel,
|
|
||||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toolName: toolNames.Pan,
|
|
||||||
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toolName: toolNames.Zoom,
|
|
||||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
|
||||||
},
|
|
||||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
|
||||||
],
|
|
||||||
enabled: [{ toolName: toolNames.SegmentationDisplay }],
|
|
||||||
};
|
|
||||||
|
|
||||||
return toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createSEGToolGroupAndAddTools;
|
export default createSEGToolGroupAndAddTools;
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import OHIF, { utils } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
|
LoadingIndicatorTotalPercent,
|
||||||
|
useViewportGrid,
|
||||||
|
ViewportActionBar,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||||
@ -31,6 +33,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
toolGroupService,
|
toolGroupService,
|
||||||
segmentationService,
|
segmentationService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
|
customizationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||||
@ -43,7 +46,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
const segDisplaySet = displaySets[0];
|
const segDisplaySet = displaySets[0];
|
||||||
|
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||||
@ -58,7 +60,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const [processingProgress, setProcessingProgress] = useState({
|
const [processingProgress, setProcessingProgress] = useState({
|
||||||
segmentIndex: 1,
|
percentComplete: null,
|
||||||
totalSegments: null,
|
totalSegments: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,6 +131,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
|
|
||||||
let newSelectedSegmentIndex = selectedSegment + direction;
|
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||||
|
|
||||||
|
// Segment 0 is always background
|
||||||
|
|
||||||
if (newSelectedSegmentIndex > numberOfSegments - 1) {
|
if (newSelectedSegmentIndex > numberOfSegments - 1) {
|
||||||
newSelectedSegmentIndex = 1;
|
newSelectedSegmentIndex = 1;
|
||||||
} else if (newSelectedSegmentIndex === 0) {
|
} else if (newSelectedSegmentIndex === 0) {
|
||||||
@ -163,7 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
|
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||||
evt => {
|
evt => {
|
||||||
if (
|
if (
|
||||||
evt.segDisplaySet.displaySetInstanceUID ===
|
evt.segDisplaySet.displaySetInstanceUID ===
|
||||||
@ -190,10 +194,10 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
const { unsubscribe } = segmentationService.subscribe(
|
||||||
segmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED,
|
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||||
({ segmentIndex, numSegments }) => {
|
({ percentComplete, numSegments }) => {
|
||||||
setProcessingProgress({
|
setProcessingProgress({
|
||||||
segmentIndex,
|
percentComplete,
|
||||||
totalSegments: numSegments,
|
totalSegments: numSegments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -239,8 +243,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
// only, and does NOT interfere with currently displayed segmentations.
|
// only, and does NOT interfere with currently displayed segmentations.
|
||||||
toolGroup = createSEGToolGroupAndAddTools(
|
toolGroup = createSEGToolGroupAndAddTools(
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
toolGroupId,
|
customizationService,
|
||||||
extensionManager
|
toolGroupId
|
||||||
);
|
);
|
||||||
|
|
||||||
setToolGroupCreated(true);
|
setToolGroupCreated(true);
|
||||||
@ -351,42 +355,14 @@ function OHIFCornerstoneSEGViewport(props) {
|
|||||||
|
|
||||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
{segIsLoading && (
|
{segIsLoading && (
|
||||||
<LoadingIndicatorProgress
|
<LoadingIndicatorTotalPercent
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
progress={
|
totalNumbers={processingProgress.totalSegments}
|
||||||
processingProgress.totalSegments !== null
|
percentComplete={processingProgress.percentComplete}
|
||||||
? ((processingProgress.segmentIndex + 1) /
|
loadingText="Loading SEG..."
|
||||||
processingProgress.totalSegments) *
|
|
||||||
100
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
textBlock={
|
|
||||||
!processingProgress.totalSegments ? (
|
|
||||||
<span className="text-white text-sm">Loading SEG ...</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-white text-sm flex items-baseline space-x-2">
|
|
||||||
<div>Loading Segment</div>
|
|
||||||
<div className="w-3">{`${processingProgress.segmentIndex}`}</div>
|
|
||||||
<div>/</div>
|
|
||||||
<div>{`${processingProgress.totalSegments}`}</div>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
<div className="absolute w-full">
|
|
||||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
|
||||||
<Notification
|
|
||||||
id="viewport-notification"
|
|
||||||
message={viewportDialogState.message}
|
|
||||||
type={viewportDialogState.type}
|
|
||||||
actions={viewportDialogState.actions}
|
|
||||||
onSubmit={viewportDialogState.onSubmit}
|
|
||||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -32,11 +32,11 @@
|
|||||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": ">=3.0.0",
|
||||||
"@ohif/extension-cornerstone": "^3.0.0",
|
"@ohif/extension-cornerstone": ">=3.0.0",
|
||||||
"@ohif/extension-measurement-tracking": "^3.0.0",
|
"@ohif/extension-measurement-tracking": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"dcmjs": "^0.29.4",
|
"dcmjs": "^0.29.5",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -46,7 +46,7 @@
|
|||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"@cornerstonejs/adapters": "^0.6.0",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.40.0",
|
"@cornerstonejs/core": "^0.47.1",
|
||||||
"@cornerstonejs/tools": "^0.60.1"
|
"@cornerstonejs/tools": "^0.67.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { metaData, utilities } from '@cornerstonejs/core';
|
import { metaData, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import OHIF from '@ohif/core';
|
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ const _generateReport = (
|
|||||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,7 +103,18 @@ const commandsModule = ({}) => {
|
|||||||
additionalFindingTypes,
|
additionalFindingTypes,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const { StudyInstanceUID } = naturalizedReport;
|
|
||||||
|
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
|
||||||
|
// The content sequence has 5 or more elements, of which
|
||||||
|
// the `[4]` element contains the annotation data, so this is
|
||||||
|
// checking that there is some annotation data present.
|
||||||
|
if (!ContentSequence?.[4].ContentSequence?.length) {
|
||||||
|
console.log(
|
||||||
|
'naturalizedReport missing imaging content',
|
||||||
|
naturalizedReport
|
||||||
|
);
|
||||||
|
throw new Error('Invalid report, no content');
|
||||||
|
}
|
||||||
|
|
||||||
await dataSource.store.dicom(naturalizedReport);
|
await dataSource.store.dicom(naturalizedReport);
|
||||||
|
|
||||||
@ -112,6 +122,11 @@ const commandsModule = ({}) => {
|
|||||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "Mode" route listens for DicomMetadataStore changes
|
||||||
|
// When a new instance is added, it listens and
|
||||||
|
// automatically calls makeDisplaySets
|
||||||
|
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||||
|
|
||||||
return naturalizedReport;
|
return naturalizedReport;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
const srProtocol: Types.HangingProtocol.Protocol = {
|
||||||
|
id: '@ohif/sr',
|
||||||
|
// Don't store this hanging protocol as it applies to the currently active
|
||||||
|
// display set by default
|
||||||
|
// cacheId: null,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'SR Key Images',
|
||||||
|
// Just apply this one when specifically listed
|
||||||
|
protocolMatchingRules: [],
|
||||||
|
toolGroupIds: ['default'],
|
||||||
|
// -1 would be used to indicate active only, whereas other values are
|
||||||
|
// the number of required priors referenced - so 0 means active with
|
||||||
|
// 0 or more priors.
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
|
// Default viewport is used to define the viewport when
|
||||||
|
// additional viewports are added using the layout tool
|
||||||
|
defaultViewport: {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportType: 'stack',
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'srDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: -1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySetSelectors: {
|
||||||
|
srDisplaySetId: {
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: 'SR',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
name: 'SR Key Images',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: { allowUnmatchedView: true },
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'srDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHangingProtocolModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: srProtocol.id,
|
||||||
|
protocol: srProtocol,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHangingProtocolModule;
|
||||||
|
export { srProtocol };
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||||
import { utils, classes } from '@ohif/core';
|
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||||
import addMeasurement from './utils/addMeasurement';
|
import addMeasurement from './utils/addMeasurement';
|
||||||
import isRehydratable from './utils/isRehydratable';
|
import isRehydratable from './utils/isRehydratable';
|
||||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||||
|
|
||||||
|
type InstanceMetadata = Types.InstanceMetadata;
|
||||||
|
|
||||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||||
|
|
||||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||||
|
|
||||||
// TODO ->
|
// TODO ->
|
||||||
// Add SR thumbnail
|
// Add SR thumbnail
|
||||||
// Make viewport
|
// Make viewport
|
||||||
@ -22,6 +25,17 @@ const sopClassUids = [
|
|||||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||||
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||||
|
|
||||||
|
const validateSameStudyUID = (uid: string, instances): void => {
|
||||||
|
instances.forEach(it => {
|
||||||
|
if (it.StudyInstanceUID !== uid) {
|
||||||
|
console.warn('Not all instances have the same UID', uid, it);
|
||||||
|
throw new Error(
|
||||||
|
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const CodeNameCodeSequenceValues = {
|
const CodeNameCodeSequenceValues = {
|
||||||
ImagingMeasurementReport: '126000',
|
ImagingMeasurementReport: '126000',
|
||||||
ImageLibrary: '111028',
|
ImageLibrary: '111028',
|
||||||
@ -50,15 +64,31 @@ const RELATIONSHIP_TYPE = {
|
|||||||
|
|
||||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds instances to the DICOM SR series, rather than creating a new
|
||||||
|
* series, so that as SR's are saved, they append to the series, and the
|
||||||
|
* key image display set gets updated as well, containing just the new series.
|
||||||
|
* @param instances is a list of instances from THIS series that are not
|
||||||
|
* in this DICOM SR Display Set already.
|
||||||
|
*/
|
||||||
|
function addInstances(
|
||||||
|
instances: InstanceMetadata[],
|
||||||
|
displaySetService: DisplaySetService
|
||||||
|
) {
|
||||||
|
this.instances.push(...instances);
|
||||||
|
utils.sortStudyInstances(this.instances);
|
||||||
|
// The last instance is the newest one, so is the one most interesting.
|
||||||
|
// Eventually, the SR viewer should have the ability to choose which SR
|
||||||
|
// gets loaded, and to navigate among them.
|
||||||
|
this.instance = this.instances[this.instances.length - 1];
|
||||||
|
this.isLoaded = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DICOM SR SOP Class Handler
|
* DICOM SR SOP Class Handler
|
||||||
* For all referenced images in the TID 1500/300 sections, add an image to the
|
* For all referenced images in the TID 1500/300 sections, add an image to the
|
||||||
* display (this is TODO - it is not the actual behaviour below unfortunately)
|
* display.
|
||||||
*
|
|
||||||
* This will only display and rehydrate the latest DICOM SR in the given series
|
|
||||||
* It would be possible to add the ability to view older series rehydrations
|
|
||||||
* in the future.
|
|
||||||
*
|
|
||||||
* @param instances is a set of instances all from the same series
|
* @param instances is a set of instances all from the same series
|
||||||
* @param servicesManager is the services that can be used for creating
|
* @param servicesManager is the services that can be used for creating
|
||||||
* @returns The list of display sets created for the given instances object
|
* @returns The list of display sets created for the given instances object
|
||||||
@ -74,6 +104,9 @@ function _getDisplaySetsFromSeries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
utils.sortStudyInstances(instances);
|
utils.sortStudyInstances(instances);
|
||||||
|
// The last instance is the newest one, so is the one most interesting.
|
||||||
|
// Eventually, the SR viewer should have the ability to choose which SR
|
||||||
|
// gets loaded, and to navigate among them.
|
||||||
const instance = instances[instances.length - 1];
|
const instance = instances[instances.length - 1];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -86,15 +119,20 @@ function _getDisplaySetsFromSeries(
|
|||||||
ConceptNameCodeSequence,
|
ConceptNameCodeSequence,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
} = instance;
|
} = instance;
|
||||||
|
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!ConceptNameCodeSequence ||
|
!ConceptNameCodeSequence ||
|
||||||
ConceptNameCodeSequence.CodeValue !==
|
ConceptNameCodeSequence.CodeValue !==
|
||||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||||
) {
|
) {
|
||||||
console.log(
|
servicesManager.services.uiNotificationService.show({
|
||||||
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
|
title: 'DICOM SR',
|
||||||
);
|
message:
|
||||||
|
'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
|
||||||
|
type: 'warning',
|
||||||
|
duration: 6000,
|
||||||
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,14 +149,13 @@ function _getDisplaySetsFromSeries(
|
|||||||
SOPClassHandlerId,
|
SOPClassHandlerId,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
instances,
|
instances,
|
||||||
// Others is a historical value used for instances which is deprecated and will be removed
|
|
||||||
others: instances,
|
|
||||||
referencedImages: null,
|
referencedImages: null,
|
||||||
measurements: null,
|
measurements: null,
|
||||||
isDerivedDisplaySet: true,
|
isDerivedDisplaySet: true,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
instance,
|
instance,
|
||||||
|
addInstances,
|
||||||
};
|
};
|
||||||
|
|
||||||
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
||||||
@ -473,13 +510,13 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
const Finding = mergedContentSequence.find(
|
const finding = mergedContentSequence.find(
|
||||||
item =>
|
item =>
|
||||||
item.ConceptNameCodeSequence.CodeValue ===
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.Finding
|
CodeNameCodeSequenceValues.Finding
|
||||||
);
|
);
|
||||||
|
|
||||||
const FindingSites = mergedContentSequence.filter(
|
const findingSites = mergedContentSequence.filter(
|
||||||
item =>
|
item =>
|
||||||
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||||
CodingSchemeDesignators.SRT &&
|
CodingSchemeDesignators.SRT &&
|
||||||
@ -496,22 +533,22 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Finding &&
|
finding &&
|
||||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
) &&
|
) &&
|
||||||
Finding.ConceptCodeSequence.CodeValue ===
|
finding.ConceptCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
) {
|
) {
|
||||||
measurement.labels.push({
|
measurement.labels.push({
|
||||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||||
value: Finding.ConceptCodeSequence.CodeMeaning,
|
value: finding.ConceptCodeSequence.CodeMeaning,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
||||||
if (FindingSites.length) {
|
if (findingSites.length) {
|
||||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
const cornerstoneFreeTextFindingSite = findingSites.find(
|
||||||
FindingSite =>
|
FindingSite =>
|
||||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
@ -633,24 +670,24 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
|||||||
|
|
||||||
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||||
const { ReferencedSOPSequence } = item;
|
const { ReferencedSOPSequence } = item;
|
||||||
|
if (!ReferencedSOPSequence) return;
|
||||||
if (item.hasOwnProperty('ReferencedSOPClassUID')) {
|
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
|
||||||
const {
|
if (ref.ReferencedSOPClassUID) {
|
||||||
ReferencedSOPClassUID,
|
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
|
||||||
ReferencedSOPInstanceUID,
|
|
||||||
} = ReferencedSOPSequence;
|
|
||||||
|
|
||||||
referencedImages.push({
|
referencedImages.push({
|
||||||
ReferencedSOPClassUID,
|
ReferencedSOPClassUID,
|
||||||
ReferencedSOPInstanceUID,
|
ReferencedSOPInstanceUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return referencedImages;
|
return referencedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getSequenceAsArray(sequence) {
|
function _getSequenceAsArray(sequence) {
|
||||||
|
if (!sequence) return [];
|
||||||
return Array.isArray(sequence) ? sequence : [sequence];
|
return Array.isArray(sequence) ? sequence : [sequence];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
|
import getHangingProtocolModule, {
|
||||||
|
srProtocol,
|
||||||
|
} from './getHangingProtocolModule';
|
||||||
import onModeEnter from './onModeEnter';
|
import onModeEnter from './onModeEnter';
|
||||||
import commandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import init from './init';
|
import preRegistration from './init';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import toolNames from './tools/toolNames';
|
import toolNames from './tools/toolNames';
|
||||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||||
|
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -31,9 +35,7 @@ const dicomSRExtension = {
|
|||||||
id,
|
id,
|
||||||
onModeEnter,
|
onModeEnter,
|
||||||
|
|
||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration,
|
||||||
init({ servicesManager, configuration });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -54,14 +56,9 @@ const dicomSRExtension = {
|
|||||||
|
|
||||||
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
||||||
},
|
},
|
||||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
getCommandsModule,
|
||||||
return commandsModule({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
extensionManager,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
|
// Include dynmically computed values such as toolNames not known till instantiation
|
||||||
getUtilityModule({ servicesManager }) {
|
getUtilityModule({ servicesManager }) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -75,4 +72,6 @@ const dicomSRExtension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default dicomSRExtension;
|
export default dicomSRExtension;
|
||||||
export { hydrateStructuredReport };
|
|
||||||
|
// Put static exports here so they can be type checked
|
||||||
|
export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol };
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
BidirectionalTool,
|
BidirectionalTool,
|
||||||
CobbAngleTool,
|
CobbAngleTool,
|
||||||
EllipticalROITool,
|
EllipticalROITool,
|
||||||
|
CircleROITool,
|
||||||
LengthTool,
|
LengthTool,
|
||||||
PlanarFreehandROITool,
|
PlanarFreehandROITool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
@ -24,6 +25,7 @@ export default function init({
|
|||||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||||
|
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||||
@ -40,6 +42,7 @@ export default function init({
|
|||||||
SRLength: dashedLine,
|
SRLength: dashedLine,
|
||||||
SRBidirectional: dashedLine,
|
SRBidirectional: dashedLine,
|
||||||
SREllipticalROI: dashedLine,
|
SREllipticalROI: dashedLine,
|
||||||
|
SRCircleROI: dashedLine,
|
||||||
SRArrowAnnotate: dashedLine,
|
SRArrowAnnotate: dashedLine,
|
||||||
SRCobbAngle: dashedLine,
|
SRCobbAngle: dashedLine,
|
||||||
SRAngle: dashedLine,
|
SRAngle: dashedLine,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default function onModeEnter({ servicesManager }) {
|
|||||||
const { displaySetService } = servicesManager.services;
|
const { displaySetService } = servicesManager.services;
|
||||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
|
||||||
const srDisplaySets = displaySetCache.filter(
|
const srDisplaySets = [...displaySetCache.values()].filter(
|
||||||
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -307,13 +307,26 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
|
|
||||||
const ellipsePointsWorld = data;
|
const ellipsePointsWorld = data;
|
||||||
|
|
||||||
|
const rotation = viewport.getRotation();
|
||||||
|
|
||||||
canvasCoordinates = ellipsePointsWorld.map(p =>
|
canvasCoordinates = ellipsePointsWorld.map(p =>
|
||||||
viewport.worldToCanvas(p)
|
viewport.worldToCanvas(p)
|
||||||
);
|
);
|
||||||
|
let canvasCorners;
|
||||||
const canvasCorners = <Array<Types.Point2>>(
|
if (rotation == 90 || rotation == 270) {
|
||||||
|
canvasCorners = <Array<Types.Point2>>(
|
||||||
|
utilities.math.ellipse.getCanvasEllipseCorners([
|
||||||
|
canvasCoordinates[2],
|
||||||
|
canvasCoordinates[3],
|
||||||
|
canvasCoordinates[0],
|
||||||
|
canvasCoordinates[1],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
canvasCorners = <Array<Types.Point2>>(
|
||||||
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
|
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const lineUID = `${index}`;
|
const lineUID = `${index}`;
|
||||||
drawing.drawEllipse(
|
drawing.drawEllipse(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ const toolNames = {
|
|||||||
SRLength: 'SRLength',
|
SRLength: 'SRLength',
|
||||||
SRBidirectional: 'SRBidirectional',
|
SRBidirectional: 'SRBidirectional',
|
||||||
SREllipticalROI: 'SREllipticalROI',
|
SREllipticalROI: 'SREllipticalROI',
|
||||||
|
SRCircleROI: 'SRCircleROI',
|
||||||
SRArrowAnnotate: 'SRArrowAnnotate',
|
SRArrowAnnotate: 'SRArrowAnnotate',
|
||||||
SRAngle: 'SRAngle',
|
SRAngle: 'SRAngle',
|
||||||
SRCobbAngle: 'SRCobbAngle',
|
SRCobbAngle: 'SRCobbAngle',
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { DisplaySetService, classes } from '@ohif/core';
|
||||||
|
|
||||||
|
const ImageSet = classes.ImageSet;
|
||||||
|
|
||||||
|
const findInstance = (measurement, displaySetService: DisplaySetService) => {
|
||||||
|
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } =
|
||||||
|
measurement;
|
||||||
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
|
if (!referencedDisplaySet.images) return;
|
||||||
|
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Finds references to display sets inside the measurements
|
||||||
|
* contained within the provided display set.
|
||||||
|
* @return an array of instances referenced.
|
||||||
|
*/
|
||||||
|
const findReferencedInstances = (
|
||||||
|
displaySetService: DisplaySetService,
|
||||||
|
displaySet
|
||||||
|
) => {
|
||||||
|
const instances = [];
|
||||||
|
const instanceById = {};
|
||||||
|
for (const measurement of displaySet.measurements) {
|
||||||
|
const { imageId } = measurement;
|
||||||
|
if (!imageId) continue;
|
||||||
|
if (instanceById[imageId]) continue;
|
||||||
|
|
||||||
|
const instance = findInstance(measurement, displaySetService);
|
||||||
|
if (!instance) {
|
||||||
|
console.log('Measurement', measurement, 'had no instances found');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceById[imageId] = instance;
|
||||||
|
instances.push(instance);
|
||||||
|
}
|
||||||
|
return instances;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new display set containing a single image instance for each
|
||||||
|
* referenced image.
|
||||||
|
*
|
||||||
|
* @param displaySetService
|
||||||
|
* @param displaySet - containing measurements referencing images.
|
||||||
|
* @returns A new (registered/active) display set containing the referenced images
|
||||||
|
*/
|
||||||
|
const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
|
||||||
|
const instances = findReferencedInstances(displaySetService, displaySet);
|
||||||
|
// This will be a member function of the created image set
|
||||||
|
const updateInstances = function() {
|
||||||
|
this.images.splice(
|
||||||
|
0,
|
||||||
|
this.images.length,
|
||||||
|
...findReferencedInstances(displaySetService, displaySet)
|
||||||
|
);
|
||||||
|
this.numImageFrames = this.images.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageSet = new ImageSet(instances);
|
||||||
|
const instance = instances[0];
|
||||||
|
imageSet.setAttributes({
|
||||||
|
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||||
|
SeriesDate: instance.SeriesDate,
|
||||||
|
SeriesTime: instance.SeriesTime,
|
||||||
|
SeriesInstanceUID: imageSet.uid,
|
||||||
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
SeriesNumber: instance.SeriesNumber || 0,
|
||||||
|
SOPClassUID: instance.SOPClassUID,
|
||||||
|
SeriesDescription: `${displaySet.SeriesDescription} KO ${displaySet.instance.SeriesNumber}`,
|
||||||
|
Modality: 'KO',
|
||||||
|
isMultiFrame: false,
|
||||||
|
numImageFrames: instances.length,
|
||||||
|
SOPClassHandlerId: `@ohif/extension-default.sopClassHandlerModule.stack`,
|
||||||
|
isReconstructable: false,
|
||||||
|
// This object is made of multiple instances from other series
|
||||||
|
isCompositeStack: true,
|
||||||
|
madeInClient: true,
|
||||||
|
excludeFromThumbnailBrowser: true,
|
||||||
|
updateInstances,
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySetService.addDisplaySets(imageSet);
|
||||||
|
|
||||||
|
return imageSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createReferencedImageDisplaySet;
|
||||||
@ -2,16 +2,10 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
||||||
|
|
||||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||||
|
|
||||||
import {
|
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||||
Icon,
|
|
||||||
Notification,
|
|
||||||
Tooltip,
|
|
||||||
useViewportDialog,
|
|
||||||
useViewportGrid,
|
|
||||||
ViewportActionBar,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
@ -27,8 +21,8 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
dataSource,
|
dataSource,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
viewportOptions,
|
|
||||||
viewportLabel,
|
viewportLabel,
|
||||||
|
viewportOptions,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
} = props;
|
} = props;
|
||||||
@ -47,7 +41,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
const srDisplaySet = displaySets[0];
|
const srDisplaySet = displaySets[0];
|
||||||
|
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
|
||||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||||
const [measurementCount, setMeasurementCount] = useState(1);
|
const [measurementCount, setMeasurementCount] = useState(1);
|
||||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
|
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
|
||||||
@ -57,7 +50,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
referencedDisplaySetMetadata,
|
referencedDisplaySetMetadata,
|
||||||
setReferencedDisplaySetMetadata,
|
setReferencedDisplaySetMetadata,
|
||||||
] = useState(null);
|
] = useState(null);
|
||||||
const [isHydrated, setIsHydrated] = useState(srDisplaySet.isHydrated);
|
|
||||||
const [element, setElement] = useState(null);
|
const [element, setElement] = useState(null);
|
||||||
const { viewports, activeViewportIndex } = viewportGrid;
|
const { viewports, activeViewportIndex } = viewportGrid;
|
||||||
|
|
||||||
@ -91,10 +83,12 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
SeriesInstanceUIDs[0]
|
SeriesInstanceUIDs[0]
|
||||||
);
|
);
|
||||||
if (displaySets.length) {
|
if (displaySets.length) {
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
viewportGridService.setDisplaySetsForViewports([
|
||||||
|
{
|
||||||
viewportIndex: activeViewportIndex,
|
viewportIndex: activeViewportIndex,
|
||||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -215,9 +209,19 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
// should be passed second since we don't want SR displaySet to
|
// should be passed second since we don't want SR displaySet to
|
||||||
// override the activeImageDisplaySetData
|
// override the activeImageDisplaySetData
|
||||||
displaySets={[activeImageDisplaySetData]}
|
displaySets={[activeImageDisplaySetData]}
|
||||||
|
// It is possible that there is a hanging protocol applying viewportOptions
|
||||||
|
// for the SR, so inherit the viewport options
|
||||||
|
// TODO: Ensure the viewport options are set correctly with respect to
|
||||||
|
// stack etc, in the incoming viewport options.
|
||||||
viewportOptions={{
|
viewportOptions={{
|
||||||
...viewportOptions,
|
...viewportOptions,
|
||||||
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
||||||
|
// viewportType should not be required, as the stack type should be
|
||||||
|
// required already in order to view SR, but sometimes segmentation
|
||||||
|
// views set the viewport type without fixing the allowed display
|
||||||
|
viewportType: 'stack',
|
||||||
|
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||||
|
positionIds: null,
|
||||||
}}
|
}}
|
||||||
onElementEnabled={onElementEnabled}
|
onElementEnabled={onElementEnabled}
|
||||||
initialImageIndex={initialImageIndex}
|
initialImageIndex={initialImageIndex}
|
||||||
@ -289,8 +293,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
if (!srDisplaySet.isLoaded) {
|
if (!srDisplaySet.isLoaded) {
|
||||||
srDisplaySet.load();
|
srDisplaySet.load();
|
||||||
}
|
}
|
||||||
setIsHydrated(srDisplaySet.isHydrated);
|
|
||||||
|
|
||||||
const numMeasurements = srDisplaySet.measurements.length;
|
const numMeasurements = srDisplaySet.measurements.length;
|
||||||
setMeasurementCount(numMeasurements);
|
setMeasurementCount(numMeasurements);
|
||||||
}, [srDisplaySet]);
|
}, [srDisplaySet]);
|
||||||
@ -378,6 +380,7 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
label: viewportLabel,
|
label: viewportLabel,
|
||||||
useAltStyling: true,
|
useAltStyling: true,
|
||||||
studyDate: formatDate(StudyDate),
|
studyDate: formatDate(StudyDate),
|
||||||
|
currentSeries: SeriesNumber,
|
||||||
seriesDescription: SeriesDescription || '',
|
seriesDescription: SeriesDescription || '',
|
||||||
patientInformation: {
|
patientInformation: {
|
||||||
patientName: PatientName
|
patientName: PatientName
|
||||||
@ -398,17 +401,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
|
|
||||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
{getCornerstoneViewport()}
|
{getCornerstoneViewport()}
|
||||||
<div className="absolute w-full">
|
|
||||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
|
||||||
<Notification
|
|
||||||
message={viewportDialogState.message}
|
|
||||||
type={viewportDialogState.type}
|
|
||||||
actions={viewportDialogState.actions}
|
|
||||||
onSubmit={viewportDialogState.onSubmit}
|
|
||||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{childrenWithProps}
|
{childrenWithProps}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -420,8 +412,10 @@ OHIFCornerstoneSRViewport.propTypes = {
|
|||||||
viewportIndex: PropTypes.number.isRequired,
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
dataSource: PropTypes.object,
|
dataSource: PropTypes.object,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
viewportLabel: PropTypes.string,
|
||||||
customProps: PropTypes.object,
|
customProps: PropTypes.object,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
|
viewportLabel: PropTypes.string,
|
||||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,11 @@
|
|||||||
"repository": "OHIF/Viewers",
|
"repository": "OHIF/Viewers",
|
||||||
"main": "dist/index.umd.js",
|
"main": "dist/index.umd.js",
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
|
"types": "src/types/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.tsx",
|
||||||
|
"./types": "./src/types/index.ts"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10",
|
"node": ">=10",
|
||||||
"npm": ">=6",
|
"npm": ">=6",
|
||||||
@ -29,9 +34,13 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-wado-image-loader": "^4.13.0",
|
"@cornerstonejs/dicom-image-loader": "^0.6.6",
|
||||||
"dcmjs": "^0.29.4",
|
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||||
"dicom-parser": "^1.8.9",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
|
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||||
|
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||||
|
"dcmjs": "^0.29.6",
|
||||||
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -44,10 +53,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^0.6.0",
|
"@cornerstonejs/adapters": "^0.6.0",
|
||||||
"@cornerstonejs/core": "^0.40.0",
|
"@cornerstonejs/core": "^0.47.1",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
"@cornerstonejs/streaming-image-volume-loader": "^0.20.4",
|
||||||
"@cornerstonejs/tools": "^0.60.1",
|
"@cornerstonejs/tools": "^0.67.4",
|
||||||
"@kitware/vtk.js": "26.5.6",
|
"@kitware/vtk.js": "27.3.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
|||||||
@ -8,22 +8,35 @@ import {
|
|||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
CONSTANTS,
|
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
import { MeasurementService } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
CinePlayer,
|
||||||
|
useCine,
|
||||||
|
useViewportGrid,
|
||||||
|
Notification,
|
||||||
|
useViewportDialog,
|
||||||
|
} from '@ohif/ui';
|
||||||
|
import {
|
||||||
|
IStackViewport,
|
||||||
|
IVolumeViewport,
|
||||||
|
} from '@cornerstonejs/core/dist/esm/types';
|
||||||
|
|
||||||
import { setEnabledElement } from '../state';
|
import { setEnabledElement } from '../state';
|
||||||
|
|
||||||
import './OHIFCornerstoneViewport.css';
|
import './OHIFCornerstoneViewport.css';
|
||||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||||
import {
|
|
||||||
IStackViewport,
|
|
||||||
IVolumeViewport,
|
|
||||||
} from '@cornerstonejs/core/dist/esm/types';
|
|
||||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
import CornerstoneServices from '../types/CornerstoneServices';
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches the jump to measurement operation, so that if display set is shown,
|
||||||
|
* it can jump to the measurement.
|
||||||
|
*/
|
||||||
|
let cacheJumpToMeasurementEvent;
|
||||||
|
|
||||||
function areEqual(prevProps, nextProps) {
|
function areEqual(prevProps, nextProps) {
|
||||||
if (nextProps.needsRerendering) {
|
if (nextProps.needsRerendering) {
|
||||||
return false;
|
return false;
|
||||||
@ -129,7 +142,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
cornerstoneCacheService,
|
cornerstoneCacheService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
stateSyncService,
|
stateSyncService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services as CornerstoneServices;
|
||||||
|
|
||||||
|
const [viewportDialogState] = useViewportDialog();
|
||||||
|
|
||||||
const cineHandler = () => {
|
const cineHandler = () => {
|
||||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||||
@ -390,8 +405,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
initialImageIndex
|
initialImageIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
storePresentation();
|
// The presentation state will have been stored previously by closing
|
||||||
|
// a viewport. Otherwise, this viewport will be unchanged and the
|
||||||
|
// presentation information will be directly carried over.
|
||||||
const {
|
const {
|
||||||
lutPresentationStore,
|
lutPresentationStore,
|
||||||
positionPresentationStore,
|
positionPresentationStore,
|
||||||
@ -403,6 +419,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
lutPresentation:
|
lutPresentation:
|
||||||
lutPresentationStore[presentationIds?.lutPresentationId],
|
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||||
};
|
};
|
||||||
|
let measurement;
|
||||||
|
if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) {
|
||||||
|
measurement = cacheJumpToMeasurementEvent.measurement;
|
||||||
|
// Delete the position presentation so that viewport navigates direct
|
||||||
|
presentations.positionPresentation = null;
|
||||||
|
cacheJumpToMeasurementEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
cornerstoneViewportService.setViewportData(
|
cornerstoneViewportService.setViewportData(
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
@ -411,6 +434,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
presentations
|
presentations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (measurement) {
|
||||||
|
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadViewportData();
|
loadViewportData();
|
||||||
@ -453,6 +480,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
}, [displaySets, elementRef, viewportIndex]);
|
}, [displaySets, elementRef, viewportIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<React.Fragment>
|
||||||
<div className="viewport-wrapper">
|
<div className="viewport-wrapper">
|
||||||
<ReactResizeDetector
|
<ReactResizeDetector
|
||||||
handleWidth
|
handleWidth
|
||||||
@ -472,7 +500,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
></div>
|
></div>
|
||||||
<CornerstoneOverlays
|
<CornerstoneOverlays
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
toolbarService={toolbarService}
|
toolBarService={toolbarService}
|
||||||
element={elementRef.current}
|
element={elementRef.current}
|
||||||
scrollbarHeight={scrollbarHeight}
|
scrollbarHeight={scrollbarHeight}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
@ -497,6 +525,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute w-full">
|
||||||
|
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||||
|
<Notification
|
||||||
|
id="viewport-notification"
|
||||||
|
message={viewportDialogState.message}
|
||||||
|
type={viewportDialogState.type}
|
||||||
|
actions={viewportDialogState.actions}
|
||||||
|
onSubmit={viewportDialogState.onSubmit}
|
||||||
|
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}, areEqual);
|
}, areEqual);
|
||||||
|
|
||||||
@ -513,12 +554,22 @@ function _subscribeToJumpToMeasurementEvents(
|
|||||||
displaySet => displaySet.displaySetInstanceUID
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
);
|
);
|
||||||
const { unsubscribe } = measurementService.subscribe(
|
const { unsubscribe } = measurementService.subscribe(
|
||||||
measurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||||
({ measurement }) => {
|
props => {
|
||||||
if (!measurement) return;
|
cacheJumpToMeasurementEvent = props;
|
||||||
|
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
|
||||||
// Jump the the measurement if the viewport contains the displaySetUID (fusion)
|
if (!measurement || isConsumed) return;
|
||||||
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
|
||||||
|
// Decide on which viewport should handle this
|
||||||
|
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump(
|
||||||
|
jumpIndex,
|
||||||
|
measurement.displaySetInstanceUID,
|
||||||
|
{ referencedImageId: measurement.referencedImageId }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_jumpToMeasurement(
|
_jumpToMeasurement(
|
||||||
measurement,
|
measurement,
|
||||||
elementRef,
|
elementRef,
|
||||||
@ -529,7 +580,6 @@ function _subscribeToJumpToMeasurementEvents(
|
|||||||
cornerstoneViewportService
|
cornerstoneViewportService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
@ -545,21 +595,20 @@ function _checkForCachedJumpToMeasurementEvents(
|
|||||||
viewportGridService,
|
viewportGridService,
|
||||||
cornerstoneViewportService
|
cornerstoneViewportService
|
||||||
) {
|
) {
|
||||||
|
if (!cacheJumpToMeasurementEvent) return;
|
||||||
|
if (cacheJumpToMeasurementEvent.isConsumed) {
|
||||||
|
cacheJumpToMeasurementEvent = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const displaysUIDs = displaySets.map(
|
const displaysUIDs = displaySets.map(
|
||||||
displaySet => displaySet.displaySetInstanceUID
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
if (!displaysUIDs?.length) return;
|
||||||
|
|
||||||
const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
|
|
||||||
viewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (measurementIdToJumpTo && elementRef) {
|
|
||||||
// Jump to measurement if the measurement exists
|
// Jump to measurement if the measurement exists
|
||||||
const measurement = measurementService.getMeasurement(
|
const { measurement } = cacheJumpToMeasurementEvent;
|
||||||
measurementIdToJumpTo
|
if (measurement && elementRef) {
|
||||||
);
|
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||||
|
|
||||||
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
|
||||||
_jumpToMeasurement(
|
_jumpToMeasurement(
|
||||||
measurement,
|
measurement,
|
||||||
elementRef,
|
elementRef,
|
||||||
@ -587,6 +636,7 @@ function _jumpToMeasurement(
|
|||||||
|
|
||||||
if (!SOPInstanceUID) {
|
if (!SOPInstanceUID) {
|
||||||
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||||
@ -599,15 +649,17 @@ function _jumpToMeasurement(
|
|||||||
|
|
||||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||||
|
|
||||||
const enableElement = getEnabledElement(targetElement);
|
const enabledElement = getEnabledElement(targetElement);
|
||||||
|
|
||||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||||
viewportIndex
|
viewportIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
if (enableElement) {
|
if (enabledElement) {
|
||||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||||
const viewport = enableElement.viewport as IStackViewport | IVolumeViewport;
|
const viewport = enabledElement.viewport as
|
||||||
|
| IStackViewport
|
||||||
|
| IVolumeViewport;
|
||||||
|
|
||||||
let imageIdIndex = 0;
|
let imageIdIndex = 0;
|
||||||
let viewportCameraDirectionMatch = true;
|
let viewportCameraDirectionMatch = true;
|
||||||
@ -627,19 +679,19 @@ function _jumpToMeasurement(
|
|||||||
} else {
|
} else {
|
||||||
// for volume viewport we can't rely on the imageIdIndex since it can be
|
// for volume viewport we can't rely on the imageIdIndex since it can be
|
||||||
// a reconstructed view that doesn't match the original slice numbers etc.
|
// a reconstructed view that doesn't match the original slice numbers etc.
|
||||||
const { viewPlaneNormal } = measurement.metadata;
|
const { viewPlaneNormal: measurementViewPlane } = measurement.metadata;
|
||||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||||
i => i.SOPInstanceUID === SOPInstanceUID
|
i => i.SOPInstanceUID === SOPInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const { orientation } = viewportInfo.getViewportOptions();
|
const { viewPlaneNormal: viewportViewPlane } = viewport.getCamera();
|
||||||
|
|
||||||
|
// should compare abs for both planes since the direction can be flipped
|
||||||
if (
|
if (
|
||||||
orientation &&
|
measurementViewPlane &&
|
||||||
viewPlaneNormal &&
|
|
||||||
!csUtils.isEqual(
|
!csUtils.isEqual(
|
||||||
CONSTANTS.MPR_CAMERA_VALUES[orientation]?.viewPlaneNormal,
|
measurementViewPlane.map(Math.abs),
|
||||||
viewPlaneNormal
|
viewportViewPlane.map(Math.abs)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
viewportCameraDirectionMatch = false;
|
viewportCameraDirectionMatch = false;
|
||||||
@ -656,7 +708,8 @@ function _jumpToMeasurement(
|
|||||||
|
|
||||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||||
// Jump to measurement consumed, remove.
|
// Jump to measurement consumed, remove.
|
||||||
measurementService.removeJumpToMeasurement(viewportIndex);
|
cacheJumpToMeasurementEvent?.consume?.();
|
||||||
|
cacheJumpToMeasurementEvent = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,16 +10,19 @@ import {
|
|||||||
utilities as cstUtils,
|
utilities as cstUtils,
|
||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { ServicesManager } from '@ohif/core';
|
import { Types as OhifTypes } from '@ohif/core';
|
||||||
|
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import callInputDialog from './utils/callInputDialog';
|
import callInputDialog from './utils/callInputDialog';
|
||||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
|
||||||
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||||
|
import { CornerstoneServices } from './types';
|
||||||
|
|
||||||
function commandsModule({ servicesManager, commandsManager }) {
|
function commandsModule({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
||||||
const {
|
const {
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
@ -28,51 +31,14 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
uiDialogService,
|
uiDialogService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
customizationService,
|
|
||||||
measurementService,
|
measurementService,
|
||||||
hangingProtocolService,
|
} = servicesManager.services as CornerstoneServices;
|
||||||
} = (servicesManager as ServicesManager).services;
|
|
||||||
|
|
||||||
const { measurementServiceSource } = this;
|
const { measurementServiceSource } = this;
|
||||||
|
|
||||||
function _getActiveViewportEnabledElement() {
|
function _getActiveViewportEnabledElement() {
|
||||||
return getActiveViewportEnabledElement(viewportGridService);
|
return getActiveViewportEnabledElement(viewportGridService);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getToolGroup(toolGroupId) {
|
|
||||||
let toolGroupIdToUse = toolGroupId;
|
|
||||||
|
|
||||||
if (!toolGroupIdToUse) {
|
|
||||||
// Use the active viewport's tool group if no tool group id is provided
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
|
||||||
|
|
||||||
if (!enabledElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { renderingEngineId, viewportId } = enabledElement;
|
|
||||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
|
||||||
viewportId,
|
|
||||||
renderingEngineId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!toolGroup) {
|
|
||||||
console.warn(
|
|
||||||
'No tool group found for viewportId:',
|
|
||||||
viewportId,
|
|
||||||
'and renderingEngineId:',
|
|
||||||
renderingEngineId
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolGroupIdToUse = toolGroup.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroup(toolGroupIdToUse);
|
|
||||||
return toolGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
/**
|
||||||
* Generates the selector props for the context menu, specific to
|
* Generates the selector props for the context menu, specific to
|
||||||
@ -127,6 +93,36 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
|
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
getNearbyAnnotation({ element, canvasCoordinates }) {
|
||||||
|
const nearbyToolData = actions.getNearbyToolData({
|
||||||
|
nearbyToolData: null,
|
||||||
|
element,
|
||||||
|
canvasCoordinates,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAnnotation = toolName => {
|
||||||
|
const enabledElement = getEnabledElement(element);
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { renderingEngineId, viewportId } = enabledElement;
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
const toolInstance = toolGroup.getToolInstance(toolName);
|
||||||
|
|
||||||
|
return toolInstance?.constructor?.isAnnotation ?? true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return nearbyToolData?.metadata?.toolName &&
|
||||||
|
isAnnotation(nearbyToolData.metadata.toolName)
|
||||||
|
? nearbyToolData
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
|
||||||
// Measurement tool commands:
|
// Measurement tool commands:
|
||||||
|
|
||||||
@ -298,7 +294,7 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
|
|
||||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||||
if (toolName === 'Crosshairs') {
|
if (toolName === 'Crosshairs') {
|
||||||
const activeViewportToolGroup = _getToolGroup(null);
|
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||||
|
|
||||||
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
||||||
uiNotificationService.show({
|
uiNotificationService.show({
|
||||||
@ -317,7 +313,7 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
viewports: [],
|
viewports: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolGroup = _getToolGroup(toolGroupId);
|
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
||||||
|
|
||||||
// if toolGroup has been destroyed, or its viewports have been removed
|
// if toolGroup has been destroyed, or its viewports have been removed
|
||||||
@ -372,6 +368,21 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
},
|
},
|
||||||
showDownloadViewportModal: () => {
|
showDownloadViewportModal: () => {
|
||||||
const { activeViewportIndex } = viewportGridService.getState();
|
const { activeViewportIndex } = viewportGridService.getState();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
activeViewportIndex
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Cannot download a non-cornerstone viewport (image).
|
||||||
|
uiNotificationService.show({
|
||||||
|
title: 'Download Image',
|
||||||
|
message: 'Image cannot be downloaded',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { uiModalService } = servicesManager.services;
|
const { uiModalService } = servicesManager.services;
|
||||||
|
|
||||||
if (uiModalService) {
|
if (uiModalService) {
|
||||||
@ -491,34 +502,23 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
firstImage: () => {
|
|
||||||
|
/** Jumps the active viewport or the specified one to the given slice index */
|
||||||
|
jumpToImage: ({ imageIndex, viewport: gridViewport }): void => {
|
||||||
// Get current active viewport (return if none active)
|
// Get current active viewport (return if none active)
|
||||||
|
let viewport;
|
||||||
|
if (!gridViewport) {
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
if (!enabledElement) {
|
if (!enabledElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { viewport } = enabledElement;
|
viewport = enabledElement.viewport;
|
||||||
|
} else {
|
||||||
// Check viewport is supported
|
viewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||||
if (
|
gridViewport.id
|
||||||
viewport! instanceof StackViewport &&
|
);
|
||||||
viewport! instanceof VolumeViewport
|
|
||||||
) {
|
|
||||||
throw new Error('Unsupported viewport type');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set slice to first slice
|
|
||||||
const options = { imageIndex: 0 };
|
|
||||||
cstUtils.jumpToSlice(viewport.element, options);
|
|
||||||
},
|
|
||||||
lastImage: () => {
|
|
||||||
// Get current active viewport (return if none active)
|
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
|
||||||
if (!enabledElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { viewport } = enabledElement;
|
|
||||||
|
|
||||||
// Get number of slices
|
// Get number of slices
|
||||||
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
|
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
|
||||||
let numberOfSlices = 0;
|
let numberOfSlices = 0;
|
||||||
@ -532,8 +532,14 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
throw new Error('Unsupported viewport type');
|
throw new Error('Unsupported viewport type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jumpIndex =
|
||||||
|
imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
|
||||||
|
if (jumpIndex >= numberOfSlices || jumpIndex < 0) {
|
||||||
|
throw new Error(`Can't jump to ${imageIndex}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Set slice to last slice
|
// Set slice to last slice
|
||||||
const options = { imageIndex: numberOfSlices - 1 };
|
const options = { imageIndex: jumpIndex };
|
||||||
cstUtils.jumpToSlice(viewport.element, options);
|
cstUtils.jumpToSlice(viewport.element, options);
|
||||||
},
|
},
|
||||||
scroll: ({ direction }) => {
|
scroll: ({ direction }) => {
|
||||||
@ -564,9 +570,9 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
return actorEntry.uid.includes(displaySetInstanceUID);
|
return actorEntry.uid.includes(displaySetInstanceUID);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { actor: volumeActor } = actorEntry;
|
const { actor: volumeActor, uid: volumeId } = actorEntry;
|
||||||
|
|
||||||
setColormap(volumeActor, colormap);
|
viewport.setProperties({ colormap, volumeActor }, volumeId);
|
||||||
|
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
viewport.render();
|
viewport.render();
|
||||||
@ -632,148 +638,109 @@ function commandsModule({ servicesManager, commandsManager }) {
|
|||||||
|
|
||||||
getNearbyToolData: {
|
getNearbyToolData: {
|
||||||
commandFn: actions.getNearbyToolData,
|
commandFn: actions.getNearbyToolData,
|
||||||
|
},
|
||||||
|
getNearbyAnnotation: {
|
||||||
|
commandFn: actions.getNearbyAnnotation,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMeasurement: {
|
deleteMeasurement: {
|
||||||
commandFn: actions.deleteMeasurement,
|
commandFn: actions.deleteMeasurement,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
setMeasurementLabel: {
|
setMeasurementLabel: {
|
||||||
commandFn: actions.setMeasurementLabel,
|
commandFn: actions.setMeasurementLabel,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
updateMeasurement: {
|
updateMeasurement: {
|
||||||
commandFn: actions.updateMeasurement,
|
commandFn: actions.updateMeasurement,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setWindowLevel: {
|
setWindowLevel: {
|
||||||
commandFn: actions.setWindowLevel,
|
commandFn: actions.setWindowLevel,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
toolbarServiceRecordInteraction: {
|
toolbarServiceRecordInteraction: {
|
||||||
commandFn: actions.toolbarServiceRecordInteraction,
|
commandFn: actions.toolbarServiceRecordInteraction,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
setToolActive: {
|
setToolActive: {
|
||||||
commandFn: actions.setToolActive,
|
commandFn: actions.setToolActive,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
rotateViewportCW: {
|
rotateViewportCW: {
|
||||||
commandFn: actions.rotateViewport,
|
commandFn: actions.rotateViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: { rotation: 90 },
|
options: { rotation: 90 },
|
||||||
},
|
},
|
||||||
rotateViewportCCW: {
|
rotateViewportCCW: {
|
||||||
commandFn: actions.rotateViewport,
|
commandFn: actions.rotateViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: { rotation: -90 },
|
options: { rotation: -90 },
|
||||||
},
|
},
|
||||||
incrementActiveViewport: {
|
incrementActiveViewport: {
|
||||||
commandFn: actions.incrementActiveViewport,
|
commandFn: actions.incrementActiveViewport,
|
||||||
storeContexts: [],
|
|
||||||
},
|
},
|
||||||
decrementActiveViewport: {
|
decrementActiveViewport: {
|
||||||
commandFn: actions.decrementActiveViewport,
|
commandFn: actions.decrementActiveViewport,
|
||||||
storeContexts: [],
|
|
||||||
},
|
},
|
||||||
flipViewportHorizontal: {
|
flipViewportHorizontal: {
|
||||||
commandFn: actions.flipViewportHorizontal,
|
commandFn: actions.flipViewportHorizontal,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
flipViewportVertical: {
|
flipViewportVertical: {
|
||||||
commandFn: actions.flipViewportVertical,
|
commandFn: actions.flipViewportVertical,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
invertViewport: {
|
invertViewport: {
|
||||||
commandFn: actions.invertViewport,
|
commandFn: actions.invertViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
resetViewport: {
|
resetViewport: {
|
||||||
commandFn: actions.resetViewport,
|
commandFn: actions.resetViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
scaleUpViewport: {
|
scaleUpViewport: {
|
||||||
commandFn: actions.scaleViewport,
|
commandFn: actions.scaleViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: 1 },
|
options: { direction: 1 },
|
||||||
},
|
},
|
||||||
scaleDownViewport: {
|
scaleDownViewport: {
|
||||||
commandFn: actions.scaleViewport,
|
commandFn: actions.scaleViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: -1 },
|
options: { direction: -1 },
|
||||||
},
|
},
|
||||||
fitViewportToWindow: {
|
fitViewportToWindow: {
|
||||||
commandFn: actions.scaleViewport,
|
commandFn: actions.scaleViewport,
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: 0 },
|
options: { direction: 0 },
|
||||||
},
|
},
|
||||||
nextImage: {
|
nextImage: {
|
||||||
commandFn: actions.scroll,
|
commandFn: actions.scroll,
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: 1 },
|
options: { direction: 1 },
|
||||||
},
|
},
|
||||||
previousImage: {
|
previousImage: {
|
||||||
commandFn: actions.scroll,
|
commandFn: actions.scroll,
|
||||||
storeContexts: [],
|
|
||||||
options: { direction: -1 },
|
options: { direction: -1 },
|
||||||
},
|
},
|
||||||
firstImage: {
|
firstImage: {
|
||||||
commandFn: actions.firstImage,
|
commandFn: actions.jumpToImage,
|
||||||
storeContexts: [],
|
options: { imageIndex: 0 },
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
lastImage: {
|
lastImage: {
|
||||||
commandFn: actions.lastImage,
|
commandFn: actions.jumpToImage,
|
||||||
storeContexts: [],
|
options: { imageIndex: -1 },
|
||||||
options: {},
|
},
|
||||||
|
jumpToImage: {
|
||||||
|
commandFn: actions.jumpToImage,
|
||||||
},
|
},
|
||||||
showDownloadViewportModal: {
|
showDownloadViewportModal: {
|
||||||
commandFn: actions.showDownloadViewportModal,
|
commandFn: actions.showDownloadViewportModal,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
toggleCine: {
|
toggleCine: {
|
||||||
commandFn: actions.toggleCine,
|
commandFn: actions.toggleCine,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
arrowTextCallback: {
|
arrowTextCallback: {
|
||||||
commandFn: actions.arrowTextCallback,
|
commandFn: actions.arrowTextCallback,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
setViewportActive: {
|
setViewportActive: {
|
||||||
commandFn: actions.setViewportActive,
|
commandFn: actions.setViewportActive,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
setViewportColormap: {
|
setViewportColormap: {
|
||||||
commandFn: actions.setViewportColormap,
|
commandFn: actions.setViewportColormap,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
toggleStackImageSync: {
|
toggleStackImageSync: {
|
||||||
commandFn: actions.toggleStackImageSync,
|
commandFn: actions.toggleStackImageSync,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
toggleReferenceLines: {
|
toggleReferenceLines: {
|
||||||
commandFn: actions.toggleReferenceLines,
|
commandFn: actions.toggleReferenceLines,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
.dicom-upload-drop-area-border-dash {
|
||||||
|
background-image: repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%);
|
||||||
|
background-position: left top, left bottom, left top, right top;
|
||||||
|
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
||||||
|
background-size: 20px 3px, 20px 3px, 3px 20px, 3px 20px;
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import Dropzone from 'react-dropzone';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import DicomFileUploader from '../../utils/DicomFileUploader';
|
||||||
|
import DicomUploadProgress from './DicomUploadProgress';
|
||||||
|
import { Button } from '@ohif/ui';
|
||||||
|
import './DicomUpload.css';
|
||||||
|
|
||||||
|
type DicomUploadProps = {
|
||||||
|
dataSource;
|
||||||
|
onComplete: () => void;
|
||||||
|
onStarted: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DicomUpload({
|
||||||
|
dataSource,
|
||||||
|
onComplete,
|
||||||
|
onStarted,
|
||||||
|
}: DicomUploadProps): ReactElement {
|
||||||
|
const baseClassNames = 'min-h-[480px] flex flex-col bg-black select-none';
|
||||||
|
const [dicomFileUploaderArr, setDicomFileUploaderArr] = useState([]);
|
||||||
|
|
||||||
|
const onDrop = useCallback(async acceptedFiles => {
|
||||||
|
onStarted();
|
||||||
|
setDicomFileUploaderArr(
|
||||||
|
acceptedFiles.map(file => new DicomFileUploader(file, dataSource))
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getDropZoneComponent = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<Dropzone
|
||||||
|
onDrop={acceptedFiles => {
|
||||||
|
onDrop(acceptedFiles);
|
||||||
|
}}
|
||||||
|
noClick
|
||||||
|
>
|
||||||
|
{({ getRootProps }) => (
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className="m-5 dicom-upload-drop-area-border-dash flex flex-col items-center justify-center h-full"
|
||||||
|
>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Dropzone onDrop={onDrop} noDrag>
|
||||||
|
{({ getRootProps, getInputProps }) => (
|
||||||
|
<div {...getRootProps()}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={false}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{'Add files'}
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
<Dropzone onDrop={onDrop} noDrag>
|
||||||
|
{({ getRootProps, getInputProps }) => (
|
||||||
|
<div {...getRootProps()}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primaryDark"
|
||||||
|
border="primaryActive"
|
||||||
|
disabled={false}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
{'Add folder'}
|
||||||
|
<input
|
||||||
|
{...getInputProps()}
|
||||||
|
webkitdirectory="true"
|
||||||
|
mozdirectory="true"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
</div>
|
||||||
|
<div className="pt-5">or drag images or folders here</div>
|
||||||
|
<div className="pt-3 text-aqua-pale text-lg">
|
||||||
|
(DICOM files supported)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dicomFileUploaderArr.length ? (
|
||||||
|
<div className={classNames('h-[calc(100vh-300px)]', baseClassNames)}>
|
||||||
|
<DicomUploadProgress
|
||||||
|
dicomFileUploaderArr={Array.from(dicomFileUploaderArr)}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={classNames('h-[480px]', baseClassNames)}>
|
||||||
|
{getDropZoneComponent()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DicomUpload.propTypes = {
|
||||||
|
dataSource: PropTypes.object.isRequired,
|
||||||
|
onComplete: PropTypes.func.isRequired,
|
||||||
|
onStarted: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DicomUpload;
|
||||||
@ -0,0 +1,436 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
ReactElement,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Icon, ProgressLoadingBar } from '@ohif/ui';
|
||||||
|
import DicomFileUploader, {
|
||||||
|
EVENTS,
|
||||||
|
UploadStatus,
|
||||||
|
DicomFileUploaderProgressEvent,
|
||||||
|
UploadRejection,
|
||||||
|
} from '../../utils/DicomFileUploader';
|
||||||
|
import DicomUploadProgressItem from './DicomUploadProgressItem';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type DicomUploadProgressProps = {
|
||||||
|
dicomFileUploaderArr: DicomFileUploader[];
|
||||||
|
onComplete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ONE_SECOND = 1000;
|
||||||
|
const ONE_MINUTE = ONE_SECOND * 60;
|
||||||
|
const ONE_HOUR = ONE_MINUTE * 60;
|
||||||
|
|
||||||
|
// The base/initial interval time length used to calculate the
|
||||||
|
// rate of the upload and in turn estimate the
|
||||||
|
// the amount of time remaining for the upload. This is the length
|
||||||
|
// of the very first interval to get a reasonable estimate on screen in
|
||||||
|
// a reasonable amount of time. The length of each interval after the first
|
||||||
|
// is based on the upload rate calculated. Faster rates use this base interval
|
||||||
|
// length. Slower rates below UPLOAD_RATE_THRESHOLD get longer interval times
|
||||||
|
// to obtain more accurate upload rates.
|
||||||
|
const BASE_INTERVAL_TIME = 15000;
|
||||||
|
|
||||||
|
// The upload rate threshold to determine the length of the interval to
|
||||||
|
// calculate the upload rate.
|
||||||
|
const UPLOAD_RATE_THRESHOLD = 75;
|
||||||
|
|
||||||
|
const NO_WRAP_ELLIPSIS_CLASS_NAMES =
|
||||||
|
'text-ellipsis whitespace-nowrap overflow-hidden';
|
||||||
|
|
||||||
|
function DicomUploadProgress({
|
||||||
|
dicomFileUploaderArr,
|
||||||
|
onComplete,
|
||||||
|
}: DicomUploadProgressProps): ReactElement {
|
||||||
|
const [totalUploadSize] = useState(
|
||||||
|
dicomFileUploaderArr.reduce(
|
||||||
|
(acc, fileUploader) => acc + fileUploader.getFileSize(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentUploadSizeRef = useRef<number>(0);
|
||||||
|
|
||||||
|
const uploadRateRef = useRef(0);
|
||||||
|
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState<number>(null);
|
||||||
|
|
||||||
|
const [percentComplete, setPercentComplete] = useState(0);
|
||||||
|
|
||||||
|
const [numFilesCompleted, setNumFilesCompleted] = useState(0);
|
||||||
|
|
||||||
|
const [numFails, setNumFails] = useState(0);
|
||||||
|
|
||||||
|
const [showFailedOnly, setShowFailedOnly] = useState(false);
|
||||||
|
|
||||||
|
const progressBarContainerRef = useRef<HTMLElement>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The effect for measuring and setting the current upload rate. This is
|
||||||
|
* done by measuring the amount of data uploaded in a set interval time.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// The amount of data already uploaded at the start of the interval.
|
||||||
|
let intervalStartUploadSize = 0;
|
||||||
|
|
||||||
|
// The starting time of the interval.
|
||||||
|
let intervalStartTime = Date.now();
|
||||||
|
|
||||||
|
const setUploadRateRef = () => {
|
||||||
|
const uploadSizeFromStartOfInterval =
|
||||||
|
currentUploadSizeRef.current - intervalStartUploadSize;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceStartOfInterval = now - intervalStartTime;
|
||||||
|
|
||||||
|
// Calculate and set the upload rate (ref)
|
||||||
|
uploadRateRef.current =
|
||||||
|
uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
|
||||||
|
|
||||||
|
// Reset the interval starting values.
|
||||||
|
intervalStartUploadSize = currentUploadSizeRef.current;
|
||||||
|
intervalStartTime = now;
|
||||||
|
|
||||||
|
// Only start a new interval if there is more to upload.
|
||||||
|
if (totalUploadSize - currentUploadSizeRef.current > 0) {
|
||||||
|
if (uploadRateRef.current >= UPLOAD_RATE_THRESHOLD) {
|
||||||
|
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME);
|
||||||
|
} else {
|
||||||
|
// The current upload rate is relatively slow, so use a larger
|
||||||
|
// time interval to get a better upload rate estimate.
|
||||||
|
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The very first interval is just the base time interval length.
|
||||||
|
timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The effect for: updating the overall percentage complete; setting the
|
||||||
|
* estimated time remaining; updating the number of files uploaded; and
|
||||||
|
* detecting if any error has occurred.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
let currentTimeRemaining = null;
|
||||||
|
|
||||||
|
// For each uploader, listen for the progress percentage complete and
|
||||||
|
// add promise catch/finally callbacks to detect errors and count number
|
||||||
|
// of uploads complete.
|
||||||
|
const subscriptions = dicomFileUploaderArr.map(fileUploader => {
|
||||||
|
let currentFileUploadSize = 0;
|
||||||
|
|
||||||
|
const updateProgress = (percentComplete: number) => {
|
||||||
|
const previousFileUploadSize = currentFileUploadSize;
|
||||||
|
|
||||||
|
currentFileUploadSize = Math.round(
|
||||||
|
(percentComplete / 100) * fileUploader.getFileSize()
|
||||||
|
);
|
||||||
|
|
||||||
|
currentUploadSizeRef.current = Math.min(
|
||||||
|
totalUploadSize,
|
||||||
|
currentUploadSizeRef.current -
|
||||||
|
previousFileUploadSize +
|
||||||
|
currentFileUploadSize
|
||||||
|
);
|
||||||
|
|
||||||
|
setPercentComplete(
|
||||||
|
(currentUploadSizeRef.current / totalUploadSize) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uploadRateRef.current !== 0) {
|
||||||
|
const uploadSizeRemaining =
|
||||||
|
totalUploadSize - currentUploadSizeRef.current;
|
||||||
|
|
||||||
|
const timeRemaining = Math.round(
|
||||||
|
uploadSizeRemaining / uploadRateRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentTimeRemaining === null) {
|
||||||
|
currentTimeRemaining = timeRemaining;
|
||||||
|
setTimeRemaining(currentTimeRemaining);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not show an increase in the time remaining by two seconds or minutes
|
||||||
|
// so as to prevent jumping the time remaining up and down constantly
|
||||||
|
// due to rounding, inaccuracies in the estimate and slight variations
|
||||||
|
// in upload rates over time.
|
||||||
|
if (timeRemaining < ONE_MINUTE) {
|
||||||
|
const currentSecondsRemaining = Math.ceil(
|
||||||
|
currentTimeRemaining / ONE_SECOND
|
||||||
|
);
|
||||||
|
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||||
|
const delta = secondsRemaining - currentSecondsRemaining;
|
||||||
|
if (delta < 0 || delta > 2) {
|
||||||
|
currentTimeRemaining = timeRemaining;
|
||||||
|
setTimeRemaining(currentTimeRemaining);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRemaining < ONE_HOUR) {
|
||||||
|
const currentMinutesRemaining = Math.ceil(
|
||||||
|
currentTimeRemaining / ONE_MINUTE
|
||||||
|
);
|
||||||
|
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||||
|
const delta = minutesRemaining - currentMinutesRemaining;
|
||||||
|
if (delta < 0 || delta > 2) {
|
||||||
|
currentTimeRemaining = timeRemaining;
|
||||||
|
setTimeRemaining(currentTimeRemaining);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hours remaining...
|
||||||
|
currentTimeRemaining = timeRemaining;
|
||||||
|
setTimeRemaining(currentTimeRemaining);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressCallback = (
|
||||||
|
progressEvent: DicomFileUploaderProgressEvent
|
||||||
|
) => {
|
||||||
|
updateProgress(progressEvent.percentComplete);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the uploader promise to flag any error and count the number of
|
||||||
|
// uploads completed.
|
||||||
|
fileUploader
|
||||||
|
.load()
|
||||||
|
.catch((rejection: UploadRejection) => {
|
||||||
|
if (rejection.status === UploadStatus.Failed) {
|
||||||
|
setNumFails(numFails => numFails + 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// If any error occurred, the percent complete progress stops firing
|
||||||
|
// but this call to updateProgress nicely puts all finished uploads at 100%.
|
||||||
|
updateProgress(100);
|
||||||
|
setNumFilesCompleted(numCompleted => numCompleted + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileUploader.subscribe(EVENTS.PROGRESS, progressCallback);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelAllUploads = useCallback(async () => {
|
||||||
|
for (const dicomFileUploader of dicomFileUploaderArr) {
|
||||||
|
// Important: we need a non-blocking way to cancel every upload,
|
||||||
|
// otherwise the UI will freeze and the user will not be able
|
||||||
|
// to interact with the app and progress will not be updated.
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dicomFileUploader.cancel();
|
||||||
|
resolve();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getFormattedTimeRemaining = useCallback((): string => {
|
||||||
|
if (timeRemaining == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRemaining < ONE_MINUTE) {
|
||||||
|
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||||
|
return `${secondsRemaining} ${
|
||||||
|
secondsRemaining === 1 ? 'second' : 'seconds'
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeRemaining < ONE_HOUR) {
|
||||||
|
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||||
|
return `${minutesRemaining} ${
|
||||||
|
minutesRemaining === 1 ? 'minute' : 'minutes'
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR);
|
||||||
|
return `${hoursRemaining} ${hoursRemaining === 1 ? 'hour' : 'hours'}`;
|
||||||
|
}, [timeRemaining]);
|
||||||
|
|
||||||
|
const getPercentCompleteRounded = useCallback(
|
||||||
|
() => Math.min(100, Math.round(percentComplete)),
|
||||||
|
[percentComplete]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the progress bar should show the infinite animation or not.
|
||||||
|
* Show the infinite animation for progress less than 1% AND if less than
|
||||||
|
* one pixel of the progress bar would be displayed.
|
||||||
|
*/
|
||||||
|
const showInfiniteProgressBar = useCallback((): boolean => {
|
||||||
|
return (
|
||||||
|
getPercentCompleteRounded() < 1 &&
|
||||||
|
(progressBarContainerRef?.current?.offsetWidth ?? 0) *
|
||||||
|
(percentComplete / 100) <
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}, [getPercentCompleteRounded, percentComplete]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the css style for the 'n of m' (files completed) text. The only css attribute
|
||||||
|
* of the style is width such that the 'n of m' is always a fixed width and thus
|
||||||
|
* as each file completes uploading the text on screen does not constantly shift
|
||||||
|
* left and right.
|
||||||
|
*/
|
||||||
|
const getNofMFilesStyle = useCallback(() => {
|
||||||
|
// the number of digits accounts for the digits being on each side of the ' of '
|
||||||
|
const numDigits = 2 * dicomFileUploaderArr.length.toString().length;
|
||||||
|
// the number of digits + 2 spaces and 2 characters for ' of '
|
||||||
|
const numChars = numDigits + 4;
|
||||||
|
return { width: `${numChars}ch` };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="text-lg px-1 pb-4 h-14 flex bg-primary-dark items-center">
|
||||||
|
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||||
|
<>
|
||||||
|
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${
|
||||||
|
dicomFileUploaderArr.length
|
||||||
|
} ${
|
||||||
|
dicomFileUploaderArr.length > 1 ? 'files' : 'file'
|
||||||
|
} completed.`}</span>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={false}
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={onComplete}
|
||||||
|
>
|
||||||
|
{'Close'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
style={getNofMFilesStyle()}
|
||||||
|
className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES, 'text-end')}
|
||||||
|
>
|
||||||
|
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}
|
||||||
|
</span>
|
||||||
|
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||||
|
{' files completed.'}
|
||||||
|
</span>
|
||||||
|
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||||
|
{timeRemaining
|
||||||
|
? `Less than ${getFormattedTimeRemaining()} remaining. `
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
NO_WRAP_ELLIPSIS_CLASS_NAMES,
|
||||||
|
'cursor-pointer text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto'
|
||||||
|
)}
|
||||||
|
onClick={cancelAllUploads}
|
||||||
|
>
|
||||||
|
Cancel All Uploads
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShowFailedOnlyIconComponent = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="ml-auto flex justify-center w-6">
|
||||||
|
{numFails > 0 && (
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon className="cursor-pointer" name="icon-status-alert"></Icon>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPercentCompleteComponent = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-scroll ohif-scrollbar px-2 border-b border-secondary-light">
|
||||||
|
<div className="flex w-full p-2.5 items-center min-h-14">
|
||||||
|
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||||
|
<>
|
||||||
|
<div className="text-xl text-primary-light">
|
||||||
|
{numFails > 0
|
||||||
|
? `Completed with ${numFails} ${
|
||||||
|
numFails > 1 ? 'errors' : 'error'
|
||||||
|
}!`
|
||||||
|
: 'Completed!'}
|
||||||
|
</div>
|
||||||
|
{getShowFailedOnlyIconComponent()}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div ref={progressBarContainerRef} className="flex-grow">
|
||||||
|
<ProgressLoadingBar
|
||||||
|
progress={
|
||||||
|
showInfiniteProgressBar()
|
||||||
|
? undefined
|
||||||
|
: Math.min(100, percentComplete)
|
||||||
|
}
|
||||||
|
></ProgressLoadingBar>
|
||||||
|
</div>
|
||||||
|
<div className="w-24 ml-1 flex items-center">
|
||||||
|
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
|
||||||
|
{getShowFailedOnlyIconComponent()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col grow">
|
||||||
|
{getNumCompletedAndTimeRemainingComponent()}
|
||||||
|
<div className="flex flex-col bg-black text-lg overflow-hidden grow">
|
||||||
|
{getPercentCompleteComponent()}
|
||||||
|
<div className="overflow-y-scroll ohif-scrollbar px-2 grow h-1">
|
||||||
|
{dicomFileUploaderArr
|
||||||
|
.filter(
|
||||||
|
dicomFileUploader =>
|
||||||
|
!showFailedOnly ||
|
||||||
|
dicomFileUploader.getStatus() === UploadStatus.Failed
|
||||||
|
)
|
||||||
|
.map(dicomFileUploader => (
|
||||||
|
<DicomUploadProgressItem
|
||||||
|
key={dicomFileUploader.getFileId()}
|
||||||
|
dicomFileUploader={dicomFileUploader}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DicomUploadProgress.propTypes = {
|
||||||
|
dicomFileUploaderArr: PropTypes.arrayOf(
|
||||||
|
PropTypes.instanceOf(DicomFileUploader)
|
||||||
|
).isRequired,
|
||||||
|
onComplete: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DicomUploadProgress;
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
import React, {
|
||||||
|
ReactElement,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import DicomFileUploader, {
|
||||||
|
DicomFileUploaderProgressEvent,
|
||||||
|
EVENTS,
|
||||||
|
UploadRejection,
|
||||||
|
UploadStatus,
|
||||||
|
} from '../../utils/DicomFileUploader';
|
||||||
|
import { Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
type DicomUploadProgressItemProps = {
|
||||||
|
dicomFileUploader: DicomFileUploader;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
const DicomUploadProgressItem = memo(
|
||||||
|
({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => {
|
||||||
|
const [percentComplete, setPercentComplete] = useState(
|
||||||
|
dicomFileUploader.getPercentComplete()
|
||||||
|
);
|
||||||
|
const [failedReason, setFailedReason] = useState('');
|
||||||
|
const [status, setStatus] = useState(dicomFileUploader.getStatus());
|
||||||
|
|
||||||
|
console.info(`${dicomFileUploader.getFileId()}`);
|
||||||
|
const isComplete = useCallback(() => {
|
||||||
|
return (
|
||||||
|
status === UploadStatus.Failed ||
|
||||||
|
status === UploadStatus.Cancelled ||
|
||||||
|
status === UploadStatus.Success
|
||||||
|
);
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const progressSubscription = dicomFileUploader.subscribe(
|
||||||
|
EVENTS.PROGRESS,
|
||||||
|
(dicomFileUploaderProgressEvent: DicomFileUploaderProgressEvent) => {
|
||||||
|
setPercentComplete(dicomFileUploaderProgressEvent.percentComplete);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
dicomFileUploader
|
||||||
|
.load()
|
||||||
|
.catch((reason: UploadRejection) => {
|
||||||
|
setStatus(reason.status);
|
||||||
|
setFailedReason(reason.message ?? '');
|
||||||
|
})
|
||||||
|
.finally(() => setStatus(dicomFileUploader.getStatus()));
|
||||||
|
|
||||||
|
return () => progressSubscription.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelUpload = useCallback(() => {
|
||||||
|
dicomFileUploader.cancel();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusIcon = (): ReactElement => {
|
||||||
|
switch (dicomFileUploader.getStatus()) {
|
||||||
|
case UploadStatus.Success:
|
||||||
|
return (
|
||||||
|
<Icon name="status-tracked" className="text-primary-light"></Icon>
|
||||||
|
);
|
||||||
|
case UploadStatus.InProgress:
|
||||||
|
return <Icon name="icon-transferring"></Icon>;
|
||||||
|
case UploadStatus.Failed:
|
||||||
|
return <Icon name="icon-alert-small"></Icon>;
|
||||||
|
case UploadStatus.Cancelled:
|
||||||
|
return <Icon name="icon-alert-outline"></Icon>;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full p-2.5 text-lg min-h-14 items-center border-b border-secondary-light overflow-hidden">
|
||||||
|
<div className="flex flex-col gap-1 self-top w-0 grow shrink">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex w-6 justify-center items-center shrink-0">
|
||||||
|
{getStatusIcon()}
|
||||||
|
</div>
|
||||||
|
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||||
|
{dicomFileUploader.getFileName()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{failedReason && <div className="pl-10">{failedReason}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="w-24 flex items-center">
|
||||||
|
{!isComplete() && (
|
||||||
|
<>
|
||||||
|
{dicomFileUploader.getStatus() === UploadStatus.InProgress && (
|
||||||
|
<div className="w-10 text-right">{percentComplete}%</div>
|
||||||
|
)}
|
||||||
|
<div className="flex cursor-pointer ml-auto">
|
||||||
|
<Icon
|
||||||
|
className="w-6 h-6 self-center text-primary-active"
|
||||||
|
name="close"
|
||||||
|
onClick={cancelUpload}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DicomUploadProgressItem.propTypes = {
|
||||||
|
dicomFileUploader: PropTypes.instanceOf(DicomFileUploader).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DicomUploadProgressItem;
|
||||||
45
extensions/cornerstone/src/getCustomizationModule.ts
Normal file
45
extensions/cornerstone/src/getCustomizationModule.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
|
import { toolNames } from './initCornerstoneTools';
|
||||||
|
import DicomUpload from './components/DicomUpload/DicomUpload';
|
||||||
|
|
||||||
|
const tools = {
|
||||||
|
active: [
|
||||||
|
{
|
||||||
|
toolName: toolNames.WindowLevel,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolName: toolNames.Pan,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolName: toolNames.Zoom,
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||||
|
},
|
||||||
|
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
||||||
|
],
|
||||||
|
enabled: [{ toolName: toolNames.SegmentationDisplay }],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCustomizationModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'cornerstoneDicomUploadComponent',
|
||||||
|
value: {
|
||||||
|
id: 'dicomUploadComponent',
|
||||||
|
component: DicomUpload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: 'cornerstone.overlayViewportTools',
|
||||||
|
tools,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCustomizationModule;
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
const mpr: Types.HangingProtocol.Protocol = {
|
const mpr: Types.HangingProtocol.Protocol = {
|
||||||
|
id: 'mpr',
|
||||||
|
name: 'Multi-Planar Reconstruction',
|
||||||
locked: true,
|
locked: true,
|
||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'mpr',
|
createdDate: '2021-02-23',
|
||||||
createdDate: '2021-02-23T19:22:08.894Z',
|
modifiedDate: '2023-04-03',
|
||||||
modifiedDate: '2023-02-17',
|
|
||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
// Unknown number of priors referenced - so just match any study
|
// Unknown number of priors referenced - so just match any study
|
||||||
@ -244,7 +245,7 @@ const mprAnd3DVolumeViewport = {
|
|||||||
{
|
{
|
||||||
id: 'mprDisplaySet',
|
id: 'mprDisplaySet',
|
||||||
options: {
|
options: {
|
||||||
presetName: 'CT-Bone',
|
displayPreset: 'CT-Bone',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -303,7 +304,7 @@ const mprAnd3DVolumeViewport = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'mpr',
|
name: mpr.id,
|
||||||
protocol: mpr,
|
protocol: mpr,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,26 +7,29 @@ import {
|
|||||||
imageRetrievalPoolManager,
|
imageRetrievalPoolManager,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import { Types } from '@ohif/core';
|
import { ServicesManager, Types } from '@ohif/core';
|
||||||
|
|
||||||
import init from './init';
|
import init from './init';
|
||||||
|
import getCustomizationModule from './getCustomizationModule';
|
||||||
import getCommandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import ToolGroupService from './services/ToolGroupService';
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
import SyncGroupService from './services/SyncGroupService';
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
import SegmentationService from './services/SegmentationService';
|
import SegmentationService from './services/SegmentationService';
|
||||||
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
||||||
|
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||||
|
import * as CornerstoneExtensionTypes from './types';
|
||||||
|
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
import { getEnabledElement, reset as enabledElementReset } from './state';
|
import { getEnabledElement, reset as enabledElementReset } from './state';
|
||||||
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
|
||||||
import dicomLoaderService from './utils/dicomLoaderService';
|
import dicomLoaderService from './utils/dicomLoaderService';
|
||||||
|
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||||
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||||
|
|
||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||||
import { PublicViewportOptions } from './services/ViewportService/Viewport';
|
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -72,18 +75,11 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
props: Types.Extensions.ExtensionParams
|
props: Types.Extensions.ExtensionParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { servicesManager } = props;
|
const { servicesManager } = props;
|
||||||
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
||||||
servicesManager.registerService(
|
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
||||||
CornerstoneViewportService(servicesManager)
|
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
||||||
);
|
servicesManager.registerService(SegmentationService.REGISTRATION);
|
||||||
servicesManager.registerService(
|
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||||
ToolGroupService.REGISTRATION(servicesManager)
|
|
||||||
);
|
|
||||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
|
||||||
servicesManager.registerService(SegmentationService(servicesManager));
|
|
||||||
servicesManager.registerService(
|
|
||||||
CornerstoneCacheService.REGISTRATION(servicesManager)
|
|
||||||
);
|
|
||||||
|
|
||||||
return init.call(this, props);
|
return init.call(this, props);
|
||||||
},
|
},
|
||||||
@ -99,7 +95,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
ToolbarService={toolbarService}
|
toolbarService={toolbarService}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
/>
|
/>
|
||||||
@ -114,6 +110,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
getCommandsModule,
|
getCommandsModule,
|
||||||
|
getCustomizationModule,
|
||||||
getUtilityModule({ servicesManager }) {
|
getUtilityModule({ servicesManager }) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -124,7 +121,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
dicomLoaderService,
|
dicomLoaderService,
|
||||||
registerColormap,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -145,5 +141,5 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type { PublicViewportOptions };
|
export type { PublicViewportOptions };
|
||||||
export { measurementMappingUtils };
|
export { measurementMappingUtils, CornerstoneExtensionTypes, toolNames , getActiveViewportEnabledElement};
|
||||||
export default cornerstoneExtension;
|
export default cornerstoneExtension;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF, { Types } from '@ohif/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
@ -36,6 +36,7 @@ window.cornerstoneTools = cornerstoneTools;
|
|||||||
export default async function init({
|
export default async function init({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
configuration,
|
configuration,
|
||||||
appConfig,
|
appConfig,
|
||||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||||
@ -83,6 +84,8 @@ export default async function init({
|
|||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
window.services = servicesManager.services;
|
window.services = servicesManager.services;
|
||||||
|
window.extensionManager = extensionManager;
|
||||||
|
window.commandsManager = commandsManager;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appConfig.showWarningMessageForCrossOrigin &&
|
appConfig.showWarningMessageForCrossOrigin &&
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
LengthTool,
|
LengthTool,
|
||||||
RectangleROITool,
|
RectangleROITool,
|
||||||
EllipticalROITool,
|
EllipticalROITool,
|
||||||
|
CircleROITool,
|
||||||
BidirectionalTool,
|
BidirectionalTool,
|
||||||
ArrowAnnotateTool,
|
ArrowAnnotateTool,
|
||||||
DragProbeTool,
|
DragProbeTool,
|
||||||
@ -29,6 +30,9 @@ import {
|
|||||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||||
|
|
||||||
export default function initCornerstoneTools(configuration = {}) {
|
export default function initCornerstoneTools(configuration = {}) {
|
||||||
|
CrosshairsTool.isAnnotation = false;
|
||||||
|
ReferenceLinesTool.isAnnotation = false;
|
||||||
|
|
||||||
init(configuration);
|
init(configuration);
|
||||||
addTool(PanTool);
|
addTool(PanTool);
|
||||||
addTool(WindowLevelTool);
|
addTool(WindowLevelTool);
|
||||||
@ -41,6 +45,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(LengthTool);
|
addTool(LengthTool);
|
||||||
addTool(RectangleROITool);
|
addTool(RectangleROITool);
|
||||||
addTool(EllipticalROITool);
|
addTool(EllipticalROITool);
|
||||||
|
addTool(CircleROITool);
|
||||||
addTool(BidirectionalTool);
|
addTool(BidirectionalTool);
|
||||||
addTool(ArrowAnnotateTool);
|
addTool(ArrowAnnotateTool);
|
||||||
addTool(DragProbeTool);
|
addTool(DragProbeTool);
|
||||||
@ -83,6 +88,7 @@ const toolNames = {
|
|||||||
Probe: ProbeTool.toolName,
|
Probe: ProbeTool.toolName,
|
||||||
RectangleROI: RectangleROITool.toolName,
|
RectangleROI: RectangleROITool.toolName,
|
||||||
EllipticalROI: EllipticalROITool.toolName,
|
EllipticalROI: EllipticalROITool.toolName,
|
||||||
|
CircleROI: CircleROITool.toolName,
|
||||||
Bidirectional: BidirectionalTool.toolName,
|
Bidirectional: BidirectionalTool.toolName,
|
||||||
Angle: AngleTool.toolName,
|
Angle: AngleTool.toolName,
|
||||||
CobbAngle: CobbAngleTool.toolName,
|
CobbAngle: CobbAngleTool.toolName,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const initMeasurementService = (
|
|||||||
Length,
|
Length,
|
||||||
Bidirectional,
|
Bidirectional,
|
||||||
EllipticalROI,
|
EllipticalROI,
|
||||||
|
CircleROI,
|
||||||
ArrowAnnotate,
|
ArrowAnnotate,
|
||||||
Angle,
|
Angle,
|
||||||
CobbAngle,
|
CobbAngle,
|
||||||
@ -64,6 +65,14 @@ const initMeasurementService = (
|
|||||||
EllipticalROI.toMeasurement
|
EllipticalROI.toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
measurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'CircleROI',
|
||||||
|
CircleROI.matchingCriteria,
|
||||||
|
CircleROI.toAnnotation,
|
||||||
|
CircleROI.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
measurementService.addMapping(
|
measurementService.addMapping(
|
||||||
csTools3DVer1MeasurementSource,
|
csTools3DVer1MeasurementSource,
|
||||||
'ArrowAnnotate',
|
'ArrowAnnotate',
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
import { volumeLoader } from '@cornerstonejs/core';
|
import { volumeLoader } from '@cornerstonejs/core';
|
||||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
import cornerstoneWADOImageLoader, {
|
import dicomImageLoader, {
|
||||||
webWorkerManager,
|
webWorkerManager,
|
||||||
} from 'cornerstone-wado-image-loader';
|
} from '@cornerstonejs/dicom-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import { errorHandler } from '@ohif/core';
|
import { errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ function initWebWorkers(appConfig) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
dicomImageLoader.webWorkerManager.initialize(config);
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,18 +37,18 @@ export default function initWADOImageLoader(
|
|||||||
userAuthenticationService,
|
userAuthenticationService,
|
||||||
appConfig
|
appConfig
|
||||||
) {
|
) {
|
||||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
dicomImageLoader.external.cornerstone = cornerstone;
|
||||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
dicomImageLoader.external.dicomParser = dicomParser;
|
||||||
|
|
||||||
registerVolumeLoader(
|
registerVolumeLoader(
|
||||||
'cornerstoneStreamingImageVolume',
|
'cornerstoneStreamingImageVolume',
|
||||||
cornerstoneStreamingImageVolumeLoader
|
cornerstoneStreamingImageVolumeLoader
|
||||||
);
|
);
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.configure({
|
dicomImageLoader.configure({
|
||||||
decodeConfig: {
|
decodeConfig: {
|
||||||
// !! IMPORTANT !!
|
// !! IMPORTANT !!
|
||||||
// We should set this flag to false, since, by default cornerstone-wado-image-loader
|
// We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader
|
||||||
// will convert everything to integers (to be able to work with cornerstone-2d).
|
// will convert everything to integers (to be able to work with cornerstone-2d).
|
||||||
// Until the default is set to true (which is the case for cornerstone3D),
|
// Until the default is set to true (which is the case for cornerstone3D),
|
||||||
// we should set this flag to false.
|
// we should set this flag to false.
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { ServicesManager } from '@ohif/core';
|
import { ServicesManager, Types } from '@ohif/core';
|
||||||
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
import {
|
||||||
|
cache as cs3DCache,
|
||||||
|
Enums,
|
||||||
|
volumeLoader,
|
||||||
|
utilities as utils,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
import {
|
import {
|
||||||
@ -10,20 +15,21 @@ import {
|
|||||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||||
|
|
||||||
class CornerstoneCacheService {
|
class CornerstoneCacheService {
|
||||||
static REGISTRATION = (serviceManager: ServicesManager) => {
|
static REGISTRATION = {
|
||||||
return {
|
|
||||||
name: 'cornerstoneCacheService',
|
name: 'cornerstoneCacheService',
|
||||||
altName: 'CornerstoneCacheService',
|
altName: 'CornerstoneCacheService',
|
||||||
create: ({ configuration = {} }) => {
|
create: ({
|
||||||
return new CornerstoneCacheService(serviceManager);
|
servicesManager,
|
||||||
|
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
|
||||||
|
return new CornerstoneCacheService(servicesManager);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
stackImageIds: Map<string, string[]> = new Map();
|
stackImageIds: Map<string, string[]> = new Map();
|
||||||
volumeImageIds: Map<string, string[]> = new Map();
|
volumeImageIds: Map<string, string[]> = new Map();
|
||||||
|
readonly servicesManager: ServicesManager;
|
||||||
|
|
||||||
constructor(servicesManager) {
|
constructor(servicesManager: ServicesManager) {
|
||||||
this.servicesManager = servicesManager;
|
this.servicesManager = servicesManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +96,10 @@ class CornerstoneCacheService {
|
|||||||
displaySetService
|
displaySetService
|
||||||
) {
|
) {
|
||||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
return this._getCornerstoneStackImageIds(
|
||||||
|
displaySetService.getDisplaySetByUID(invalidatedDisplaySetInstanceUID),
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: grab the volume and get the id from the viewport itself
|
// Todo: grab the volume and get the id from the viewport itself
|
||||||
@ -100,6 +109,7 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
if (volume) {
|
if (volume) {
|
||||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||||
|
this.volumeImageIds.delete(volumeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
|
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
|
||||||
@ -133,13 +143,18 @@ class CornerstoneCacheService {
|
|||||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
const {
|
||||||
|
displaySetInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
isCompositeStack,
|
||||||
|
} = displaySet;
|
||||||
|
|
||||||
const StackViewportData: StackViewportData = {
|
const StackViewportData: StackViewportData = {
|
||||||
viewportType,
|
viewportType,
|
||||||
data: {
|
data: {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
|
isCompositeStack,
|
||||||
imageIds: stackImageIds,
|
imageIds: stackImageIds,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Maps a DICOM RT Struct ROI Contour to a RTStruct data that can be used
|
||||||
|
* in Segmentation Service
|
||||||
|
*
|
||||||
|
* @param structureSet - A DICOM RT Struct ROI Contour
|
||||||
|
* @param rtDisplaySetUID - A CornerstoneTools DisplaySet UID
|
||||||
|
* @returns An array of object that includes data, id, segmentIndex, color
|
||||||
|
* and geometry Id
|
||||||
|
*/
|
||||||
|
export function mapROIContoursToRTStructData(
|
||||||
|
structureSet: unknown,
|
||||||
|
rtDisplaySetUID: unknown
|
||||||
|
) {
|
||||||
|
return structureSet.ROIContours.map(
|
||||||
|
({ contourPoints, ROINumber, ROIName, colorArray }) => {
|
||||||
|
const data = contourPoints.map(({ points, ...rest }) => {
|
||||||
|
const newPoints = points.map(({ x, y, z }) => {
|
||||||
|
return [x, y, z];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
points: newPoints,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = ROIName || ROINumber;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
id,
|
||||||
|
segmentIndex: ROINumber,
|
||||||
|
color: colorArray,
|
||||||
|
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,8 @@ type Segmentation = {
|
|||||||
colorLUTIndex: number;
|
colorLUTIndex: number;
|
||||||
// if segmentation contains any data (often calculated from labelmap)
|
// if segmentation contains any data (often calculated from labelmap)
|
||||||
cachedStats: Record<string, any>;
|
cachedStats: Record<string, any>;
|
||||||
|
// displaySetInstanceUID
|
||||||
|
displaySetInstanceUID: string;
|
||||||
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
|
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
|
||||||
displayText?: string[];
|
displayText?: string[];
|
||||||
// the id of the segmentation
|
// the id of the segmentation
|
||||||
@ -45,44 +47,22 @@ type Segmentation = {
|
|||||||
segments: Array<Segment>;
|
segments: Array<Segment>;
|
||||||
// the set of segments that are locked
|
// the set of segments that are locked
|
||||||
segmentsLocked: Array<number>;
|
segmentsLocked: Array<number>;
|
||||||
// the segmentation representation type
|
|
||||||
type: csToolsEnums.SegmentationRepresentations;
|
|
||||||
// if labelmap, the id of the volume that the labelmap is associated with
|
|
||||||
volumeId?: string;
|
|
||||||
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
||||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
// but hydrated SEG -> segmentation that is persisted in the store)
|
||||||
hydrated: boolean;
|
hydrated: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
// Schema to generate a segmentation
|
|
||||||
type SegmentationSchema = {
|
|
||||||
// active segment index for the segmentation
|
|
||||||
activeSegmentIndex: number;
|
|
||||||
// statistics that are derived from the segmentation
|
|
||||||
cachedStats: Record<string, any>;
|
|
||||||
// the displayText for the segmentation in the panels
|
|
||||||
displayText?: string[];
|
|
||||||
// segmentation id
|
|
||||||
id: string;
|
|
||||||
// displaySetInstanceUID
|
|
||||||
displaySetInstanceUID: string;
|
|
||||||
// segmentation label
|
|
||||||
label: string;
|
|
||||||
// segment indices that are locked for the segmentation
|
|
||||||
segmentsLocked: Array<number>;
|
|
||||||
// the type of the segmentation (e.g., Labelmap etc.)
|
// the type of the segmentation (e.g., Labelmap etc.)
|
||||||
type: csToolsEnums.SegmentationRepresentations;
|
type: csToolsEnums.SegmentationRepresentations;
|
||||||
// the volume id of the volume that the labelmap is associated with, this only exists for the labelmap representation
|
// the segmentation representation data
|
||||||
volumeId: string;
|
representationData: SegmentationRepresentationData;
|
||||||
// the referenced volumeURI for the segmentation
|
|
||||||
referencedVolumeURI: string;
|
|
||||||
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
|
||||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
|
||||||
hydrated: boolean;
|
|
||||||
// the number of segments in the segmentation
|
|
||||||
segmentCount: number;
|
|
||||||
// the array of segments with their details
|
|
||||||
segments: Array<Segment>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { SegmentationConfig, Segment, Segmentation, SegmentationSchema };
|
type LabelmapSegmentationData = {
|
||||||
|
volumeId: string;
|
||||||
|
referencedVolumeId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SegmentationRepresentationData = {
|
||||||
|
LABELMAP?: LabelmapSegmentationData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SegmentationConfig, Segment, Segmentation };
|
||||||
|
|||||||
@ -1,11 +1,3 @@
|
|||||||
import SegmentationService from './SegmentationService';
|
import SegmentationService from './SegmentationService';
|
||||||
|
|
||||||
export default function ExtendedSegmentationService(servicesManager) {
|
export default SegmentationService;
|
||||||
return {
|
|
||||||
name: 'segmentationService',
|
|
||||||
altName: 'SegmentationService',
|
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return new SegmentationService({ servicesManager });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
Synchronizer,
|
Synchronizer,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import { pubSubServiceInterface } from '@ohif/core';
|
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||||
@ -37,7 +37,17 @@ const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
|||||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||||
|
|
||||||
export default class SyncGroupService {
|
export default class SyncGroupService {
|
||||||
serviceManager: any;
|
static REGISTRATION = {
|
||||||
|
name: 'syncGroupService',
|
||||||
|
altName: 'SyncGroupService',
|
||||||
|
create: ({
|
||||||
|
servicesManager,
|
||||||
|
}: Types.Extensions.ExtensionParams): SyncGroupService => {
|
||||||
|
return new SyncGroupService(servicesManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
servicesManager: ServicesManager;
|
||||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||||
EVENTS: { [key: string]: string };
|
EVENTS: { [key: string]: string };
|
||||||
synchronizerCreators: Record<string, SyncCreator> = {
|
synchronizerCreators: Record<string, SyncCreator> = {
|
||||||
@ -47,8 +57,8 @@ export default class SyncGroupService {
|
|||||||
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
|
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(serviceManager) {
|
constructor(serviceManager: ServicesManager) {
|
||||||
this.serviceManager = serviceManager;
|
this.servicesManager = serviceManager;
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,11 +1,3 @@
|
|||||||
import SyncGroupService from './SyncGroupService';
|
import SyncGroupService from './SyncGroupService';
|
||||||
|
|
||||||
export default function ExtendedSyncGroupService(serviceManager) {
|
export default SyncGroupService;
|
||||||
return {
|
|
||||||
altName: 'SyncGroupService',
|
|
||||||
name: 'syncGroupService',
|
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return new SyncGroupService(serviceManager);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import { pubSubServiceInterface } from '@ohif/core';
|
import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core';
|
||||||
|
import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||||
@ -20,15 +21,15 @@ type Tools = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class ToolGroupService {
|
export default class ToolGroupService {
|
||||||
public static REGISTRATION = serviceManager => {
|
public static REGISTRATION = {
|
||||||
return {
|
|
||||||
name: 'toolGroupService',
|
name: 'toolGroupService',
|
||||||
altName: 'ToolGroupService',
|
altName: 'ToolGroupService',
|
||||||
create: ({ configuration = {} }) => {
|
create: ({
|
||||||
return new ToolGroupService(serviceManager);
|
servicesManager,
|
||||||
|
}: OhifTypes.Extensions.ExtensionParams): ToolGroupService => {
|
||||||
|
return new ToolGroupService(servicesManager);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
serviceManager: any;
|
serviceManager: any;
|
||||||
private toolGroupIds: Set<string> = new Set();
|
private toolGroupIds: Set<string> = new Set();
|
||||||
@ -39,20 +40,60 @@ export default class ToolGroupService {
|
|||||||
EVENTS: { [key: string]: string };
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
constructor(serviceManager) {
|
constructor(serviceManager) {
|
||||||
const { cornerstoneViewportService } = serviceManager.services;
|
const {
|
||||||
|
cornerstoneViewportService,
|
||||||
|
viewportGridService,
|
||||||
|
} = serviceManager.services;
|
||||||
this.cornerstoneViewportService = cornerstoneViewportService;
|
this.cornerstoneViewportService = cornerstoneViewportService;
|
||||||
|
this.viewportGridService = viewportGridService;
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onModeExit() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cornerstone ToolGroup for a given toolGroup UID
|
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
||||||
* @param {string} toolGroupId - The toolGroup uid
|
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
||||||
* @returns {IToolGroup} - The toolGroup
|
* @param toolGroupId - Optional ID of the tool group to retrieve.
|
||||||
|
* @returns The tool group or undefined if it is not found.
|
||||||
*/
|
*/
|
||||||
public getToolGroup(toolGroupId: string): Types.IToolGroup | void {
|
public getToolGroup(toolGroupId?: string): Types.IToolGroup | void {
|
||||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
let toolGroupIdToUse = toolGroupId;
|
||||||
|
|
||||||
|
if (!toolGroupIdToUse) {
|
||||||
|
// Use the active viewport's tool group if no tool group id is provided
|
||||||
|
const enabledElement = getActiveViewportEnabledElement(
|
||||||
|
this.viewportGridService
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { renderingEngineId, viewportId } = enabledElement;
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!toolGroup) {
|
||||||
|
console.warn(
|
||||||
|
'No tool group found for viewportId:',
|
||||||
|
viewportId,
|
||||||
|
'and renderingEngineId:',
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolGroupIdToUse = toolGroup.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupIdToUse);
|
||||||
return toolGroup;
|
return toolGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { PubSubService } from '@ohif/core';
|
import { PubSubService, ServicesManager } from '@ohif/core';
|
||||||
|
import * as OhifTypes from '@ohif/core/types';
|
||||||
import {
|
import {
|
||||||
RenderingEngine,
|
RenderingEngine,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
@ -10,9 +11,13 @@ import {
|
|||||||
cache,
|
cache,
|
||||||
utilities,
|
utilities,
|
||||||
CONSTANTS,
|
CONSTANTS,
|
||||||
|
Enums as csEnums,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
import {
|
||||||
|
utilities as csToolsUtils,
|
||||||
|
Enums as csToolsEnums,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
import { IViewportService } from './IViewportService';
|
import { IViewportService } from './IViewportService';
|
||||||
import { RENDERING_ENGINE_ID } from './constants';
|
import { RENDERING_ENGINE_ID } from './constants';
|
||||||
import ViewportInfo, {
|
import ViewportInfo, {
|
||||||
@ -25,10 +30,6 @@ import {
|
|||||||
VolumeViewportData,
|
VolumeViewportData,
|
||||||
} from '../../types/CornerstoneCacheService';
|
} from '../../types/CornerstoneCacheService';
|
||||||
import { Presentation, Presentations } from '../../types/Presentation';
|
import { Presentation, Presentations } from '../../types/Presentation';
|
||||||
import {
|
|
||||||
setColormap,
|
|
||||||
setLowerUpperColorTransferFunction,
|
|
||||||
} from '../../utils/colormap/transferFunctionHelpers';
|
|
||||||
|
|
||||||
import JumpPresets from '../../utils/JumpPresets';
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
|
|
||||||
@ -43,6 +44,16 @@ const EVENTS = {
|
|||||||
*/
|
*/
|
||||||
class CornerstoneViewportService extends PubSubService
|
class CornerstoneViewportService extends PubSubService
|
||||||
implements IViewportService {
|
implements IViewportService {
|
||||||
|
static REGISTRATION = {
|
||||||
|
name: 'cornerstoneViewportService',
|
||||||
|
altName: 'CornerstoneViewportService',
|
||||||
|
create: ({
|
||||||
|
servicesManager,
|
||||||
|
}: OhifTypes.Extensions.ExtensionParams): CornerstoneViewportService => {
|
||||||
|
return new CornerstoneViewportService(servicesManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
renderingEngine: Types.IRenderingEngine | null;
|
renderingEngine: Types.IRenderingEngine | null;
|
||||||
viewportsInfo: Map<number, ViewportInfo> = new Map();
|
viewportsInfo: Map<number, ViewportInfo> = new Map();
|
||||||
viewportsById: Map<string, ViewportInfo> = new Map();
|
viewportsById: Map<string, ViewportInfo> = new Map();
|
||||||
@ -55,7 +66,7 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
resizeRefreshMode: 'debounce';
|
resizeRefreshMode: 'debounce';
|
||||||
servicesManager = null;
|
servicesManager = null;
|
||||||
|
|
||||||
constructor(servicesManager) {
|
constructor(servicesManager: ServicesManager) {
|
||||||
super(EVENTS);
|
super(EVENTS);
|
||||||
this.renderingEngine = null;
|
this.renderingEngine = null;
|
||||||
this.viewportGridResizeObserver = null;
|
this.viewportGridResizeObserver = null;
|
||||||
@ -71,7 +82,7 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportOptions: PublicViewportOptions,
|
viewportOptions: PublicViewportOptions,
|
||||||
elementRef: HTMLDivElement
|
elementRef: HTMLDivElement
|
||||||
) {
|
): void {
|
||||||
// Use the provided viewportId
|
// Use the provided viewportId
|
||||||
// Not providing a viewportId is frowned upon because it does weird things
|
// Not providing a viewportId is frowned upon because it does weird things
|
||||||
// on moving them around, but it does mostly work.
|
// on moving them around, but it does mostly work.
|
||||||
@ -375,7 +386,7 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
initialImageIndexToUse === null
|
initialImageIndexToUse === null
|
||||||
) {
|
) {
|
||||||
initialImageIndexToUse =
|
initialImageIndexToUse =
|
||||||
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = { ...presentations.lutPresentation?.properties };
|
const properties = { ...presentations.lutPresentation?.properties };
|
||||||
@ -401,7 +412,7 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getInitialImageIndexForStackViewport(
|
private _getInitialImageIndexForViewport(
|
||||||
viewportInfo: ViewportInfo,
|
viewportInfo: ViewportInfo,
|
||||||
imageIds?: string[]
|
imageIds?: string[]
|
||||||
): number {
|
): number {
|
||||||
@ -412,7 +423,29 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { index, preset } = initialImageOptions;
|
const { index, preset } = initialImageOptions;
|
||||||
return this._getInitialImageIndex(imageIds.length, index, preset);
|
const viewportType = viewportInfo.getViewportType();
|
||||||
|
|
||||||
|
let numberOfSlices;
|
||||||
|
if (viewportType === csEnums.ViewportType.STACK) {
|
||||||
|
numberOfSlices = imageIds.length;
|
||||||
|
} else if (viewportType === csEnums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
const viewport = this.getCornerstoneViewport(
|
||||||
|
viewportInfo.getViewportId()
|
||||||
|
);
|
||||||
|
const imageSliceData = csUtils.getImageSliceDataForVolumeViewport(
|
||||||
|
viewport
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!imageSliceData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
({ numberOfSlices } = imageSliceData);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._getInitialImageIndex(numberOfSlices, index, preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getInitialImageIndex(
|
_getInitialImageIndex(
|
||||||
@ -488,16 +521,9 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
const displaySetOptions = displaySetOptionsArray[index];
|
const displaySetOptions = displaySetOptionsArray[index];
|
||||||
const { volumeId } = volume;
|
const { volumeId } = volume;
|
||||||
|
|
||||||
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
|
||||||
|
|
||||||
const callback = ({ volumeActor }) => {
|
|
||||||
voiCallbacks.forEach(callback => callback(volumeActor));
|
|
||||||
};
|
|
||||||
|
|
||||||
volumeInputArray.push({
|
volumeInputArray.push({
|
||||||
imageIds,
|
imageIds,
|
||||||
volumeId,
|
volumeId,
|
||||||
callback,
|
|
||||||
blendMode: displaySetOptions.blendMode,
|
blendMode: displaySetOptions.blendMode,
|
||||||
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
||||||
});
|
});
|
||||||
@ -537,62 +563,118 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
displaySetService,
|
displaySetService,
|
||||||
segmentationService,
|
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
} = this.servicesManager.services;
|
} = this.servicesManager.services;
|
||||||
|
|
||||||
|
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||||
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
|
// Todo: use presentations states
|
||||||
|
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
|
||||||
|
const { volumeId } = volumeInput;
|
||||||
|
const displaySetOption = displaySetOptions[index];
|
||||||
|
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
|
||||||
|
const properties = {};
|
||||||
|
|
||||||
|
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||||
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||||
|
voi.windowWidth,
|
||||||
|
voi.windowCenter
|
||||||
|
);
|
||||||
|
properties.voiRange = { lower, upper };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voiInverted !== undefined) {
|
||||||
|
properties.invert = voiInverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colormap !== undefined) {
|
||||||
|
properties.colormap = colormap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayPreset !== undefined) {
|
||||||
|
properties.preset = displayPreset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { properties, volumeId };
|
||||||
|
});
|
||||||
|
|
||||||
await viewport.setVolumes(volumeInputArray);
|
await viewport.setVolumes(volumeInputArray);
|
||||||
|
volumesProperties.forEach(({ properties, volumeId }) => {
|
||||||
|
viewport.setProperties(properties, volumeId);
|
||||||
|
});
|
||||||
|
|
||||||
this.setPresentations(viewport, presentations);
|
this.setPresentations(viewport, presentations);
|
||||||
|
|
||||||
// load any secondary displaySets
|
// load any secondary displaySets
|
||||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||||
|
|
||||||
const segDisplaySet = displaySetInstanceUIDs
|
// can be SEG or RTSTRUCT for now
|
||||||
|
const overlayDisplaySet = displaySetInstanceUIDs
|
||||||
.map(displaySetService.getDisplaySetByUID)
|
.map(displaySetService.getDisplaySetByUID)
|
||||||
.find(displaySet => displaySet && displaySet.Modality === 'SEG');
|
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
||||||
|
|
||||||
if (segDisplaySet) {
|
if (overlayDisplaySet) {
|
||||||
const { referencedVolumeId } = segDisplaySet;
|
this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport);
|
||||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
} else {
|
||||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||||
|
// and we can look into hydrated segmentations to check if any of them are
|
||||||
|
// associated with the primary displaySet
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
// get segmentations only returns the hydrated segmentations
|
||||||
|
this._addSegmentationRepresentationToToolGroupIfNecessary(
|
||||||
if (referencedVolume) {
|
displaySetInstanceUIDs,
|
||||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
viewport
|
||||||
toolGroup.id,
|
|
||||||
segmentationId
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
|
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||||
|
|
||||||
|
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
|
||||||
|
|
||||||
|
if (imageIndex !== undefined) {
|
||||||
|
csToolsUtils.jumpToSlice(viewport.element, {
|
||||||
|
imageIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
||||||
|
displaySetInstanceUIDs: string[],
|
||||||
|
viewport: any
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
segmentationService,
|
||||||
|
toolGroupService,
|
||||||
|
} = this.servicesManager.services;
|
||||||
|
|
||||||
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
|
|
||||||
|
// this only returns hydrated segmentations
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
|
||||||
|
for (const segmentation of segmentations) {
|
||||||
const toolGroupSegmentationRepresentations =
|
const toolGroupSegmentationRepresentations =
|
||||||
segmentationService.getSegmentationRepresentationsForToolGroup(
|
segmentationService.getSegmentationRepresentationsForToolGroup(
|
||||||
toolGroup.id
|
toolGroup.id
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
// csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
|
||||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
|
||||||
// and we can look into hydrated segmentations to check if any of them are
|
|
||||||
// associated with the primary displaySet
|
|
||||||
// get segmentations only returns the hydrated segmentations
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
|
|
||||||
for (const segmentation of segmentations) {
|
|
||||||
// if there is already a segmentation representation for this segmentation
|
// if there is already a segmentation representation for this segmentation
|
||||||
// for this toolGroup, don't bother at all
|
// for this toolGroup, don't bother at all
|
||||||
if (
|
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
|
||||||
toolGroupSegmentationRepresentations.find(
|
|
||||||
representation => representation.segmentationId === segmentation.id
|
representation => representation.segmentationId === segmentation.id
|
||||||
)
|
);
|
||||||
) {
|
|
||||||
|
if (isSegmentationInToolGroup) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, check if the hydrated segmentations are in the same FOR
|
// otherwise, check if the hydrated segmentations are in the same FOR
|
||||||
// as the primary displaySet, if so add the representation (since it was not there)
|
// as the primary displaySet, if so add the representation (since it was not there)
|
||||||
const { id: segDisplaySetInstanceUID } = segmentation;
|
const { id: segDisplaySetInstanceUID, type } = segmentation;
|
||||||
|
|
||||||
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||||
segDisplaySetInstanceUID
|
segDisplaySetInstanceUID
|
||||||
);
|
);
|
||||||
@ -610,53 +692,44 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDisplaySeg) {
|
if (!shouldDisplaySeg) {
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(
|
return;
|
||||||
viewport.id
|
}
|
||||||
);
|
|
||||||
|
|
||||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||||
toolGroup.id,
|
toolGroup.id,
|
||||||
segmentation.id
|
segmentation.id,
|
||||||
|
false, // already hydrated,
|
||||||
|
segmentation.type
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
private addOverlayRepresentationForDisplaySet(
|
||||||
|
displaySet: any,
|
||||||
|
viewport: any
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
segmentationService,
|
||||||
|
toolGroupService,
|
||||||
|
} = this.servicesManager.services;
|
||||||
|
|
||||||
if (!viewportInfo) {
|
const { referencedVolumeId } = displaySet;
|
||||||
console.warn('Viewport info not defined for', viewport.id);
|
const segmentationId = displaySet.displaySetInstanceUID;
|
||||||
}
|
|
||||||
|
|
||||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
|
||||||
|
|
||||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
const representationType =
|
||||||
|
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
|
||||||
|
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||||
|
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||||
|
|
||||||
if (
|
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||||
initialImageOptions &&
|
toolGroup.id,
|
||||||
(initialImageOptions.preset !== undefined ||
|
segmentationId,
|
||||||
initialImageOptions.index !== undefined)
|
false,
|
||||||
) {
|
representationType
|
||||||
const { index, preset } = initialImageOptions;
|
|
||||||
|
|
||||||
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
|
||||||
viewport
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageIndex = this._getInitialImageIndex(
|
|
||||||
numberOfSlices,
|
|
||||||
index,
|
|
||||||
preset
|
|
||||||
);
|
|
||||||
|
|
||||||
csToolsUtils.jumpToSlice(viewport.element, {
|
|
||||||
imageIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
viewport.render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: keepCamera is an interim solution until we have a better solution for
|
// Todo: keepCamera is an interim solution until we have a better solution for
|
||||||
@ -689,50 +762,6 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getVOICallbacks(volumeId, displaySetOptions) {
|
|
||||||
const {
|
|
||||||
voi,
|
|
||||||
voiInverted: inverted,
|
|
||||||
colormap,
|
|
||||||
presetName,
|
|
||||||
} = displaySetOptions;
|
|
||||||
|
|
||||||
const voiCallbackArray = [];
|
|
||||||
|
|
||||||
// If colormap is set, use it to set the color transfer function
|
|
||||||
if (colormap) {
|
|
||||||
voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (voi instanceof Object && voi.windowWidth && voi.windowCenter) {
|
|
||||||
const { windowWidth, windowCenter } = voi;
|
|
||||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
|
||||||
windowWidth,
|
|
||||||
windowCenter
|
|
||||||
);
|
|
||||||
voiCallbackArray.push(volumeActor =>
|
|
||||||
setLowerUpperColorTransferFunction({
|
|
||||||
volumeActor,
|
|
||||||
lower,
|
|
||||||
upper,
|
|
||||||
inverted,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presetName) {
|
|
||||||
voiCallbackArray.push(volumeActor => {
|
|
||||||
utilities.applyPreset(
|
|
||||||
volumeActor,
|
|
||||||
CONSTANTS.VIEWPORT_PRESETS.find(preset => {
|
|
||||||
return preset.name === presetName;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return voiCallbackArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setDisplaySets(
|
_setDisplaySets(
|
||||||
viewport: StackViewport | VolumeViewport,
|
viewport: StackViewport | VolumeViewport,
|
||||||
viewportData: StackViewportData | VolumeViewportData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
@ -848,21 +877,44 @@ class CornerstoneViewportService extends PubSubService
|
|||||||
return instance.FrameOfReferenceUID;
|
return instance.FrameOfReferenceUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (displaySet.Modality === 'RTSTRUCT') {
|
||||||
|
const { instance } = displaySet;
|
||||||
|
return instance.ReferencedFrameOfReferenceSequence.FrameOfReferenceUID;
|
||||||
|
}
|
||||||
|
|
||||||
const { images } = displaySet;
|
const { images } = displaySet;
|
||||||
if (images && images.length) {
|
if (images && images.length) {
|
||||||
return images[0].FrameOfReferenceUID;
|
return images[0].FrameOfReferenceUID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks through the viewports to see if the specified measurement can be
|
||||||
|
* displayed in one of the viewports.
|
||||||
|
*
|
||||||
|
* @param measurement
|
||||||
|
* The measurement that is desired to view.
|
||||||
|
* @param activeViewportIndex - the index that was active at the time the jump
|
||||||
|
* was initiated.
|
||||||
|
* @return the viewportIndex to display the given measurement
|
||||||
|
*/
|
||||||
|
public getViewportIndexToJump(
|
||||||
|
activeViewportIndex: number,
|
||||||
|
displaySetInstanceUID: string,
|
||||||
|
cameraProps: unknown
|
||||||
|
): number {
|
||||||
|
const viewportInfo = this.viewportsInfo.get(activeViewportIndex);
|
||||||
|
const { referencedImageId } = cameraProps;
|
||||||
|
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
|
||||||
|
return activeViewportIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
[...this.viewportsById.values()].find(viewportInfo =>
|
||||||
|
viewportInfo.contains(displaySetInstanceUID, referencedImageId)
|
||||||
|
)?.viewportIndex ?? -1
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CornerstoneViewportServiceRegistration(serviceManager) {
|
export default CornerstoneViewportService;
|
||||||
return {
|
|
||||||
name: 'cornerstoneViewportService',
|
|
||||||
altName: 'CornerstoneViewportService',
|
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return new CornerstoneViewportService(serviceManager);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { CornerstoneViewportService, CornerstoneViewportServiceRegistration };
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export type PublicDisplaySetOptions = {
|
|||||||
blendMode?: string;
|
blendMode?: string;
|
||||||
slabThickness?: number;
|
slabThickness?: number;
|
||||||
colormap?: string;
|
colormap?: string;
|
||||||
presetName?: string;
|
displayPreset?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DisplaySetOptions = {
|
export type DisplaySetOptions = {
|
||||||
@ -73,7 +73,7 @@ export type DisplaySetOptions = {
|
|||||||
blendMode?: Enums.BlendModes;
|
blendMode?: Enums.BlendModes;
|
||||||
slabThickness?: number;
|
slabThickness?: number;
|
||||||
colormap?: string;
|
colormap?: string;
|
||||||
presetName?: string;
|
displayPreset?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VOI = {
|
type VOI = {
|
||||||
@ -88,6 +88,20 @@ export type DisplaySet = {
|
|||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
const DEFAULT_TOOLGROUP_ID = 'default';
|
const DEFAULT_TOOLGROUP_ID = 'default';
|
||||||
|
|
||||||
|
// Return true if the data contains the given display set UID OR the imageId
|
||||||
|
// if it is a composite object.
|
||||||
|
const dataContains = (
|
||||||
|
data,
|
||||||
|
displaySetUID: string,
|
||||||
|
imageId?: string
|
||||||
|
): boolean => {
|
||||||
|
if (data.displaySetInstanceUID === displaySetUID) return true;
|
||||||
|
if (imageId && data.isCompositeStack && data.imageIds) {
|
||||||
|
return !!data.imageIds.find(dataId => dataId === imageId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
class ViewportInfo {
|
class ViewportInfo {
|
||||||
private viewportId = '';
|
private viewportId = '';
|
||||||
private viewportIndex: number;
|
private viewportIndex: number;
|
||||||
@ -104,6 +118,21 @@ class ViewportInfo {
|
|||||||
this.setPublicDisplaySetOptions([{}]);
|
this.setPublicDisplaySetOptions([{}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the viewport contains the given display set UID,
|
||||||
|
* OR if it is a composite stack and contains the given imageId
|
||||||
|
*/
|
||||||
|
public contains(displaySetUID: string, imageId: string): boolean {
|
||||||
|
if (!this.viewportData?.data) return false;
|
||||||
|
|
||||||
|
if (this.viewportData.data.length) {
|
||||||
|
return !!this.viewportData.data.find(data =>
|
||||||
|
dataContains(data, displaySetUID, imageId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dataContains(this.viewportData.data, displaySetUID, imageId);
|
||||||
|
}
|
||||||
|
|
||||||
public destroy = (): void => {
|
public destroy = (): void => {
|
||||||
this.element = null;
|
this.element = null;
|
||||||
this.viewportData = null;
|
this.viewportData = null;
|
||||||
@ -203,6 +232,10 @@ class ViewportInfo {
|
|||||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!toolGroupId) {
|
||||||
|
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||||
|
}
|
||||||
|
|
||||||
this.setViewportOptions({
|
this.setViewportOptions({
|
||||||
...viewportOptionsEntry,
|
...viewportOptionsEntry,
|
||||||
viewportId: this.viewportId,
|
viewportId: this.viewportId,
|
||||||
@ -228,7 +261,8 @@ class ViewportInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getSyncGroups(): SyncGroup[] {
|
public getSyncGroups(): SyncGroup[] {
|
||||||
return this.viewportOptions.syncGroups || [];
|
this.viewportOptions.syncGroups ||= [];
|
||||||
|
return this.viewportOptions.syncGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
||||||
@ -281,7 +315,7 @@ class ViewportInfo {
|
|||||||
colormap: option.colormap,
|
colormap: option.colormap,
|
||||||
slabThickness: option.slabThickness,
|
slabThickness: option.slabThickness,
|
||||||
blendMode,
|
blendMode,
|
||||||
presetName: option.presetName,
|
displayPreset: option.displayPreset,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import {
|
import { Enums, Types } from '@cornerstonejs/core';
|
||||||
Enums,
|
|
||||||
Types,
|
|
||||||
} from '@cornerstonejs/core';
|
|
||||||
|
|
||||||
|
|
||||||
type StackData = {
|
type StackData = {
|
||||||
StudyInstanceUID: string;
|
StudyInstanceUID: string;
|
||||||
displaySetInstanceUID: string;
|
displaySetInstanceUID: string;
|
||||||
|
// A composite stack is one created from other display sets - kind of like
|
||||||
|
// madeInClient, but specific to indicating that the imageIds can come from
|
||||||
|
// different series or even studies.
|
||||||
|
isCompositeStack?: boolean;
|
||||||
imageIds: string[];
|
imageIds: string[];
|
||||||
frameRate?: number;
|
frameRate?: number;
|
||||||
isClip?: boolean;
|
isClip?: boolean;
|
||||||
@ -20,19 +20,14 @@ type VolumeData = {
|
|||||||
imageIds?: string[];
|
imageIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type StackViewportData = {
|
type StackViewportData = {
|
||||||
viewportType: Enums.ViewportType;
|
viewportType: Enums.ViewportType;
|
||||||
data: StackData;
|
data: StackData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VolumeViewportData = {
|
type VolumeViewportData = {
|
||||||
viewportType: Enums.ViewportType;
|
viewportType: Enums.ViewportType;
|
||||||
data: VolumeData[];
|
data: VolumeData[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type { StackViewportData, VolumeViewportData, StackData, VolumeData };
|
||||||
StackViewportData,
|
|
||||||
VolumeViewportData,
|
|
||||||
StackData,
|
|
||||||
VolumeData,
|
|
||||||
};
|
|
||||||
|
|||||||
16
extensions/cornerstone/src/types/CornerstoneServices.ts
Normal file
16
extensions/cornerstone/src/types/CornerstoneServices.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
|
import SegmentationService from './services/SegmentationService';
|
||||||
|
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
||||||
|
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||||
|
|
||||||
|
interface CornerstoneServices extends Types.Services {
|
||||||
|
cornerstoneViewportService: CornerstoneViewportService;
|
||||||
|
toolGroupService: ToolGroupService;
|
||||||
|
syncGroupService: SyncGroupService;
|
||||||
|
segmentationService: SegmentationService;
|
||||||
|
cornerstoneCacheService: CornerstoneCacheService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CornerstoneServices;
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import * as CornerstoneCacheService from './CornerstoneCacheService'
|
import * as CornerstoneCacheService from './CornerstoneCacheService'
|
||||||
|
import CornerstoneServices from './CornerstoneServices';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CornerstoneCacheService
|
CornerstoneCacheService,
|
||||||
|
CornerstoneServices,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,7 +217,7 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// add the viewport to the toolGroup
|
// add the viewport to the toolGroup
|
||||||
toolGroup.addViewport(downloadViewportId);
|
toolGroup.addViewport(downloadViewportId, renderingEngineId);
|
||||||
|
|
||||||
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
||||||
// make all tools Enabled so that they can not be interacted with
|
// make all tools Enabled so that they can not be interacted with
|
||||||
|
|||||||
220
extensions/cornerstone/src/utils/DicomFileUploader.ts
Normal file
220
extensions/cornerstone/src/utils/DicomFileUploader.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||||
|
|
||||||
|
import { PubSubService } from '@ohif/core';
|
||||||
|
|
||||||
|
export const EVENTS = {
|
||||||
|
PROGRESS: 'event:DicomFileUploader:progress',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DicomFileUploaderEvent {
|
||||||
|
fileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DicomFileUploaderProgressEvent extends DicomFileUploaderEvent {
|
||||||
|
percentComplete: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UploadStatus {
|
||||||
|
NotStarted,
|
||||||
|
InProgress,
|
||||||
|
Success,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelOrFailed = UploadStatus.Cancelled | UploadStatus.Failed;
|
||||||
|
|
||||||
|
export class UploadRejection {
|
||||||
|
message: string;
|
||||||
|
status: CancelOrFailed;
|
||||||
|
|
||||||
|
constructor(status: CancelOrFailed, message: string) {
|
||||||
|
this.message = message;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DicomFileUploader extends PubSubService {
|
||||||
|
private _file;
|
||||||
|
private _fileId;
|
||||||
|
private _dataSource;
|
||||||
|
private _loadPromise;
|
||||||
|
private _abortController = new AbortController();
|
||||||
|
private _status: UploadStatus = UploadStatus.NotStarted;
|
||||||
|
private _percentComplete = 0;
|
||||||
|
|
||||||
|
constructor(file, dataSource) {
|
||||||
|
super(EVENTS);
|
||||||
|
this._file = file;
|
||||||
|
this._fileId = dicomImageLoader.wadouri.fileManager.add(file);
|
||||||
|
this._dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this._fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileName(): string {
|
||||||
|
return this._file.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileSize(): number {
|
||||||
|
return this._file.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this._abortController.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): UploadStatus {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPercentComplete(): number {
|
||||||
|
return this._percentComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<void> {
|
||||||
|
if (this._loadPromise) {
|
||||||
|
// Already started loading, return the load promise.
|
||||||
|
return this._loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loadPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
// The upload listeners: fire progress events and/or settle the promise.
|
||||||
|
const uploadCallbacks = {
|
||||||
|
progress: evt => {
|
||||||
|
if (!evt.lengthComputable) {
|
||||||
|
// Progress computation is not possible.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._status = UploadStatus.InProgress;
|
||||||
|
|
||||||
|
this._percentComplete = Math.round((100 * evt.loaded) / evt.total);
|
||||||
|
this._broadcastEvent(EVENTS.PROGRESS, {
|
||||||
|
fileId: this._fileId,
|
||||||
|
percentComplete: this._percentComplete,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
timeout: () => {
|
||||||
|
this._reject(
|
||||||
|
reject,
|
||||||
|
new UploadRejection(UploadStatus.Failed, 'The request timed out.')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
abort: () => {
|
||||||
|
this._reject(
|
||||||
|
reject,
|
||||||
|
new UploadRejection(UploadStatus.Cancelled, 'Cancelled')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this._reject(
|
||||||
|
reject,
|
||||||
|
new UploadRejection(UploadStatus.Failed, 'The request failed.')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// First try to load the file.
|
||||||
|
dicomImageLoader.wadouri
|
||||||
|
.loadFileRequest(this._fileId)
|
||||||
|
.then(dicomFile => {
|
||||||
|
if (this._abortController.signal.aborted) {
|
||||||
|
this._reject(
|
||||||
|
reject,
|
||||||
|
new UploadRejection(UploadStatus.Cancelled, 'Cancelled')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._checkDicomFile(dicomFile)) {
|
||||||
|
// The file is not DICOM
|
||||||
|
this._reject(
|
||||||
|
reject,
|
||||||
|
new UploadRejection(
|
||||||
|
UploadStatus.Failed,
|
||||||
|
'Not a valid DICOM file.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
this._addRequestCallbacks(request, uploadCallbacks);
|
||||||
|
|
||||||
|
// Do the actual upload by supplying the DICOM file and upload callbacks/listeners.
|
||||||
|
return this._dataSource.store
|
||||||
|
.dicom(dicomFile, request)
|
||||||
|
.then(() => {
|
||||||
|
this._status = UploadStatus.Success;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reason => {
|
||||||
|
this._reject(reject, reason);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reason => {
|
||||||
|
this._reject(reject, reason);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isRejected(): boolean {
|
||||||
|
return (
|
||||||
|
this._status === UploadStatus.Failed ||
|
||||||
|
this._status === UploadStatus.Cancelled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _reject(reject: (reason?: any) => void, reason: any) {
|
||||||
|
if (this._isRejected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason instanceof UploadRejection) {
|
||||||
|
this._status = reason.status;
|
||||||
|
reject(reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._status = UploadStatus.Failed;
|
||||||
|
|
||||||
|
if (reason.message) {
|
||||||
|
reject(new UploadRejection(UploadStatus.Failed, reason.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new UploadRejection(UploadStatus.Failed, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addRequestCallbacks(request: XMLHttpRequest, uploadCallbacks) {
|
||||||
|
const abortCallback = () => request.abort();
|
||||||
|
this._abortController.signal.addEventListener('abort', abortCallback);
|
||||||
|
|
||||||
|
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
|
||||||
|
request.upload.addEventListener(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanUpCallback = () => {
|
||||||
|
this._abortController.signal.removeEventListener('abort', abortCallback);
|
||||||
|
|
||||||
|
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
|
||||||
|
request.upload.removeEventListener(eventName, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.removeEventListener('loadend', cleanUpCallback);
|
||||||
|
};
|
||||||
|
request.addEventListener('loadend', cleanUpCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkDicomFile(arrayBuffer: ArrayBuffer) {
|
||||||
|
if (arrayBuffer.length <= 132) return false;
|
||||||
|
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
|
||||||
|
// bytes from 128 to 132 must be "DICM"
|
||||||
|
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
|
|
||||||
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
|
|
||||||
|
|
||||||
import presets from './presets';
|
|
||||||
|
|
||||||
// get preset from preset name
|
|
||||||
function getPreset(name) {
|
|
||||||
return presets.find(preset => preset.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: we should be able to register a new preset via the utilityModule
|
|
||||||
// of the cornerstone extension
|
|
||||||
export default function applyPreset(actor, presetName) {
|
|
||||||
const preset = getPreset(presetName);
|
|
||||||
// Create color transfer function
|
|
||||||
const colorTransferArray = preset.colorTransfer
|
|
||||||
.split(' ')
|
|
||||||
.splice(1)
|
|
||||||
.map(parseFloat);
|
|
||||||
|
|
||||||
const { shiftRange } = getShiftRange(colorTransferArray);
|
|
||||||
let min = shiftRange[0];
|
|
||||||
const width = shiftRange[1] - shiftRange[0];
|
|
||||||
const cfun = vtkColorTransferFunction.newInstance();
|
|
||||||
const normColorTransferValuePoints = [];
|
|
||||||
for (let i = 0; i < colorTransferArray.length; i += 4) {
|
|
||||||
let value = colorTransferArray[i];
|
|
||||||
const r = colorTransferArray[i + 1];
|
|
||||||
const g = colorTransferArray[i + 2];
|
|
||||||
const b = colorTransferArray[i + 3];
|
|
||||||
|
|
||||||
value = (value - min) / width;
|
|
||||||
normColorTransferValuePoints.push([value, r, g, b]);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun);
|
|
||||||
|
|
||||||
actor.getProperty().setRGBTransferFunction(0, cfun);
|
|
||||||
|
|
||||||
// Create scalar opacity function
|
|
||||||
const scalarOpacityArray = preset.scalarOpacity
|
|
||||||
.split(' ')
|
|
||||||
.splice(1)
|
|
||||||
.map(parseFloat);
|
|
||||||
|
|
||||||
const ofun = vtkPiecewiseFunction.newInstance();
|
|
||||||
const normPoints = [];
|
|
||||||
for (let i = 0; i < scalarOpacityArray.length; i += 2) {
|
|
||||||
let value = scalarOpacityArray[i];
|
|
||||||
const opacity = scalarOpacityArray[i + 1];
|
|
||||||
|
|
||||||
value = (value - min) / width;
|
|
||||||
|
|
||||||
normPoints.push([value, opacity]);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun);
|
|
||||||
|
|
||||||
actor.getProperty().setScalarOpacity(0, ofun);
|
|
||||||
|
|
||||||
const [
|
|
||||||
gradientMinValue,
|
|
||||||
gradientMinOpacity,
|
|
||||||
gradientMaxValue,
|
|
||||||
gradientMaxOpacity,
|
|
||||||
] = preset.gradientOpacity
|
|
||||||
.split(' ')
|
|
||||||
.splice(1)
|
|
||||||
.map(parseFloat);
|
|
||||||
|
|
||||||
actor.getProperty().setUseGradientOpacity(0, true);
|
|
||||||
actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue);
|
|
||||||
actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity);
|
|
||||||
actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue);
|
|
||||||
actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity);
|
|
||||||
|
|
||||||
if (preset.interpolation === '1') {
|
|
||||||
actor.getProperty().setInterpolationTypeToFastLinear();
|
|
||||||
//actor.getProperty().setInterpolationTypeToLinear()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ambient = parseFloat(preset.ambient);
|
|
||||||
//const shade = preset.shade === '1'
|
|
||||||
const diffuse = parseFloat(preset.diffuse);
|
|
||||||
const specular = parseFloat(preset.specular);
|
|
||||||
const specularPower = parseFloat(preset.specularPower);
|
|
||||||
|
|
||||||
//actor.getProperty().setShade(shade)
|
|
||||||
actor.getProperty().setAmbient(ambient);
|
|
||||||
actor.getProperty().setDiffuse(diffuse);
|
|
||||||
actor.getProperty().setSpecular(specular);
|
|
||||||
actor.getProperty().setSpecularPower(specularPower);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShiftRange(colorTransferArray) {
|
|
||||||
// Credit to paraview-glance
|
|
||||||
// https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133
|
|
||||||
|
|
||||||
// shift range is original rgb/opacity range centered around 0
|
|
||||||
let min = Infinity;
|
|
||||||
let max = -Infinity;
|
|
||||||
for (let i = 0; i < colorTransferArray.length; i += 4) {
|
|
||||||
min = Math.min(min, colorTransferArray[i]);
|
|
||||||
max = Math.max(max, colorTransferArray[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = (max - min) / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
shiftRange: [-center, center],
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyPointsToRGBFunction(points, range, cfun) {
|
|
||||||
const width = range[1] - range[0];
|
|
||||||
const rescaled = points.map(([x, r, g, b]) => [
|
|
||||||
x * width + range[0],
|
|
||||||
r,
|
|
||||||
g,
|
|
||||||
b,
|
|
||||||
]);
|
|
||||||
|
|
||||||
cfun.removeAllPoints();
|
|
||||||
rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b));
|
|
||||||
|
|
||||||
return rescaled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyPointsToPiecewiseFunction(points, range, pwf) {
|
|
||||||
const width = range[1] - range[0];
|
|
||||||
const rescaled = points.map(([x, y]) => [x * width + range[0], y]);
|
|
||||||
|
|
||||||
pwf.removeAllPoints();
|
|
||||||
rescaled.forEach(([x, y]) => pwf.addPoint(x, y));
|
|
||||||
|
|
||||||
return rescaled;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
// https://www.slicer.org/w/index.php/Slicer3:2010_GenericAnatomyColors#Lookup_table
|
|
||||||
const colors = [
|
|
||||||
{
|
|
||||||
integerLabel: 0,
|
|
||||||
textLabel: 'background',
|
|
||||||
color: [0, 0, 0, 0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
integerLabel: 1,
|
|
||||||
textLabel: 'tissue',
|
|
||||||
// color: [255, 174, 128, 255],
|
|
||||||
color: [255, 0, 0, 255],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
integerLabel: 2,
|
|
||||||
textLabel: 'bone',
|
|
||||||
// color: [241, 214, 145, 255],
|
|
||||||
color: [0, 255, 0, 255],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
integerLabel: 3,
|
|
||||||
textLabel: 'skin',
|
|
||||||
// color: [177, 122, 101, 255],
|
|
||||||
color: [0, 0, 255, 255],
|
|
||||||
},
|
|
||||||
// ....
|
|
||||||
];
|
|
||||||
|
|
||||||
export default colors;
|
|
||||||
@ -1,435 +0,0 @@
|
|||||||
const presets = [
|
|
||||||
{
|
|
||||||
name: 'CT-AAA',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode1',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '143.556 419.736',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-AAA2',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'16 -3024 0 129.542 0 145.244 0.166667 157.02 0.5 169.918 0.627451 395.575 0.8125 1578.73 0.8125 3071 0.8125',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode2',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'32 -3024 0 0 0 129.542 0.54902 0.25098 0.14902 145.244 0.6 0.627451 0.843137 157.02 0.890196 0.47451 0.6 169.918 0.992157 0.870588 0.392157 395.575 1 0.886275 0.658824 1578.73 1 0.829256 0.957922 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '0 1600',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Bone',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity: '8 -3024 0 -16.4458 0 641.385 0.715686 3071 0.705882',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode3',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'16 -3024 0 0 0 -16.4458 0.729412 0.254902 0.301961 641.385 0.905882 0.815686 0.552941 3071 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-16.4458 641.385',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Bones',
|
|
||||||
gradientOpacity: '4 0 1 985.12 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '8 -1000 0 152.19 0 278.93 0.190476 952 0.2',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode4',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -1000 0.3 0.3 1 -488 0.3 1 0.3 463.28 1 0 0 659.15 1 0.912535 0.0374849 953 1 0.3 0.3',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '152.19 952',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Cardiac',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'12 -3024 0 -77.6875 0 94.9518 0.285714 179.052 0.553571 260.439 0.848214 3071 0.875',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode5',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -3024 0 0 0 -77.6875 0.54902 0.25098 0.14902 94.9518 0.882353 0.603922 0.290196 179.052 1 0.937033 0.954531 260.439 0.615686 0 0 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-77.6875 260.439',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Cardiac2',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'12 -3024 0 42.8964 0 163.488 0.428571 277.642 0.776786 1587 0.754902 3071 0.754902',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode6',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -3024 0 0 0 42.8964 0.54902 0.25098 0.14902 163.488 0.917647 0.639216 0.0588235 277.642 1 0.878431 0.623529 1587 1 1 1 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '42.8964 1587',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Cardiac3',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'14 -3024 0 -86.9767 0 45.3791 0.169643 139.919 0.589286 347.907 0.607143 1224.16 0.607143 3071 0.616071',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode7',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'28 -3024 0 0 0 -86.9767 0 0.25098 1 45.3791 1 0 0 139.919 1 0.894893 0.894893 347.907 1 1 0.25098 1224.16 1 1 1 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-86.9767 1224.16',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Chest-Contrast-Enhanced',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'10 -3024 0 67.0106 0 251.105 0.446429 439.291 0.625 3071 0.616071',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode8',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -3024 0 0 0 67.0106 0.54902 0.25098 0.14902 251.105 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '67.0106 439.291',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Chest-Vessels',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'10 -3024 0 -1278.35 0 22.8277 0.428571 439.291 0.625 3071 0.616071',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode9',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -3024 0 0 0 -1278.35 0.54902 0.25098 0.14902 22.8277 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-1278.35 439.291',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Coronary-Arteries',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'12 -2048 0 136.47 0 159.215 0.258929 318.43 0.571429 478.693 0.776786 3661 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode10',
|
|
||||||
specular: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -2048 0 0 0 136.47 0 0 0 159.215 0.159804 0.159804 0.159804 318.43 0.764706 0.764706 0.764706 478.693 1 1 1 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '136.47 478.693',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Coronary-Arteries-2',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'14 -2048 0 142.677 0 145.016 0.116071 192.174 0.5625 217.24 0.776786 384.347 0.830357 3661 0.830357',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode11',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'28 -2048 0 0 0 142.677 0 0 0 145.016 0.615686 0 0.0156863 192.174 0.909804 0.454902 0 217.24 0.972549 0.807843 0.611765 384.347 0.909804 0.909804 1 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '142.677 384.347',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Coronary-Arteries-3',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'14 -2048 0 128.643 0 129.982 0.0982143 173.636 0.669643 255.884 0.857143 584.878 0.866071 3661 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode12',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'28 -2048 0 0 0 128.643 0 0 0 129.982 0.615686 0 0.0156863 173.636 0.909804 0.454902 0 255.884 0.886275 0.886275 0.886275 584.878 0.968627 0.968627 0.968627 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '128.643 584.878',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Cropped-Volume-Bone',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '10 -2048 0 -451 0 -450 1 1050 1 3661 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode13',
|
|
||||||
specular: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -2048 0 0 0 -451 0 0 0 -450 0.0556356 0.0556356 0.0556356 1050 1 1 1 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-451 1050',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Fat',
|
|
||||||
gradientOpacity: '6 0 1 985.12 1 988 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '14 -1000 0 -100 0 -99 0.15 -60 0.15 -59 0 101.2 0 952 0',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode14',
|
|
||||||
specular: '0',
|
|
||||||
references: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'36 -1000 0.3 0.3 1 -497.5 0.3 1 0.3 -99 0 0 1 -76.946 0 1 0 -65.481 0.835431 0.888889 0.0165387 83.89 1 0 0 463.28 1 0 0 659.15 1 0.912535 0.0374849 2952 1 0.300267 0.299886',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-100 101.2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Liver-Vasculature',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'14 -2048 0 149.113 0 157.884 0.482143 339.96 0.660714 388.526 0.830357 1197.95 0.839286 3661 0.848214',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode15',
|
|
||||||
specular: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'28 -2048 0 0 0 149.113 0 0 0 157.884 0.501961 0.25098 0 339.96 0.695386 0.59603 0.36886 388.526 0.854902 0.85098 0.827451 1197.95 1 1 1 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '149.113 1197.95',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Lung',
|
|
||||||
gradientOpacity: '6 0 1 985.12 1 988 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '12 -1000 0 -600 0 -599 0.15 -400 0.15 -399 0 2952 0',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode16',
|
|
||||||
specular: '0',
|
|
||||||
references: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -1000 0.3 0.3 1 -600 0 0 1 -530 0.134704 0.781726 0.0724558 -460 0.929244 1 0.109473 -400 0.888889 0.254949 0.0240258 2952 1 0.3 0.3',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-600 -399',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-MIP',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity: '8 -3024 0 -637.62 0 700 1 3071 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode17',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer: '16 -3024 0 0 0 -637.62 1 1 1 700 1 1 1 3071 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-637.62 700',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Muscle',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity:
|
|
||||||
'10 -3024 0 -155.407 0 217.641 0.676471 419.736 0.833333 3071 0.803922',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode18',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -3024 0 0 0 -155.407 0.54902 0.25098 0.14902 217.641 0.882353 0.603922 0.290196 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-155.407 419.736',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Pulmonary-Arteries',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'14 -2048 0 -568.625 0 -364.081 0.0714286 -244.813 0.401786 18.2775 0.607143 447.798 0.830357 3592.73 0.839286',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode19',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'28 -2048 0 0 0 -568.625 0 0 0 -364.081 0.396078 0.301961 0.180392 -244.813 0.611765 0.352941 0.0705882 18.2775 0.843137 0.0156863 0.156863 447.798 0.752941 0.752941 0.752941 3592.73 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-568.625 447.798',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Soft-Tissue',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '10 -2048 0 -167.01 0 -160 1 240 1 3661 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode20',
|
|
||||||
specular: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'20 -2048 0 0 0 -167.01 0 0 0 -160 0.0556356 0.0556356 0.0556356 240 1 1 1 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-167.01 240',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CT-Air',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '10',
|
|
||||||
scalarOpacity: '8 -3024 0.705882 -900.0 0.715686 -500.0 0 3071 0',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode21',
|
|
||||||
specular: '0.2',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.1',
|
|
||||||
colorTransfer:
|
|
||||||
'16 -3024 1 1 1 -900.0 0.2 1.0 1.0 -500.0 0.3 0.3 1.0 3071 0 0 0 ',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '-1200.0 -200.0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MR-Angio',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity:
|
|
||||||
'12 -2048 0 151.354 0 158.279 0.4375 190.112 0.580357 200.873 0.732143 3661 0.741071',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode22',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'24 -2048 0 0 0 151.354 0 0 0 158.279 0.74902 0.376471 0 190.112 1 0.866667 0.733333 200.873 0.937255 0.937255 0.937255 3661 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '151.354 200.873',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MR-Default',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '12 0 0 20 0 40 0.15 120 0.3 220 0.375 1024 0.5',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode23',
|
|
||||||
specular: '0',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer:
|
|
||||||
'24 0 0 0 0 20 0.168627 0 0 40 0.403922 0.145098 0.0784314 120 0.780392 0.607843 0.380392 220 0.847059 0.835294 0.788235 1024 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '0 220',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MR-MIP',
|
|
||||||
gradientOpacity: '4 0 1 255 1',
|
|
||||||
specularPower: '1',
|
|
||||||
scalarOpacity: '8 0 0 98.3725 0 416.637 1 2800 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode24',
|
|
||||||
specular: '0',
|
|
||||||
shade: '0',
|
|
||||||
ambient: '0.2',
|
|
||||||
colorTransfer: '16 0 1 1 1 98.3725 1 1 1 416.637 1 1 1 2800 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '1',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '0 416.637',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'MR-T2-Brain',
|
|
||||||
gradientOpacity: '4 0 1 160.25 1',
|
|
||||||
specularPower: '40',
|
|
||||||
scalarOpacity: '10 0 0 36.05 0 218.302 0.171429 412.406 1 641 1',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode25',
|
|
||||||
specular: '0.5',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.3',
|
|
||||||
colorTransfer:
|
|
||||||
'16 0 0 0 0 98.7223 0.956863 0.839216 0.192157 412.406 0 0.592157 0.807843 641 1 1 1',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.6',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '0 412.406',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DTI-FA-Brain',
|
|
||||||
gradientOpacity: '4 0 1 0.9950 1',
|
|
||||||
specularPower: '40',
|
|
||||||
scalarOpacity:
|
|
||||||
'16 0 0 0 0 0.3501 0.0158 0.49379 0.7619 0.6419 1 0.9920 1 0.9950 0 0.9950 0',
|
|
||||||
id: 'vtkMRMLVolumePropertyNode26',
|
|
||||||
specular: '0.5',
|
|
||||||
shade: '1',
|
|
||||||
ambient: '0.3',
|
|
||||||
colorTransfer:
|
|
||||||
'28 0 1 0 0 0 1 0 0 0.24974 0.4941 1 0 0.49949 0 0.9882 1 0.7492 0.51764 0 1 0.9950 1 0 0 0.9950 1 0 0',
|
|
||||||
selectable: 'true',
|
|
||||||
diffuse: '0.9',
|
|
||||||
interpolation: '1',
|
|
||||||
effectiveRange: '0 1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default presets;
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import { cache, utilities } from '@cornerstonejs/core';
|
|
||||||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
|
||||||
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
|
|
||||||
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
|
|
||||||
|
|
||||||
const colormaps = {};
|
|
||||||
|
|
||||||
export function registerColormap(colormap) {
|
|
||||||
colormaps[colormap.Name] = colormap;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColorTransferFunctionFromVolumeMetadata({
|
|
||||||
volumeActor,
|
|
||||||
volumeId,
|
|
||||||
inverted,
|
|
||||||
}) {
|
|
||||||
let lower, upper, windowWidth, windowCenter;
|
|
||||||
|
|
||||||
if (volumeId) {
|
|
||||||
const volume = cache.getVolume(volumeId);
|
|
||||||
const voiLutModule = volume.metadata.voiLut[0];
|
|
||||||
if (voiLutModule) {
|
|
||||||
windowWidth = voiLutModule.windowWidth;
|
|
||||||
windowCenter = voiLutModule.windowCenter;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
windowWidth = 400;
|
|
||||||
windowCenter = 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windowWidth == undefined || windowCenter === undefined) {
|
|
||||||
// Set to something so we can window level it manually.
|
|
||||||
lower = 200;
|
|
||||||
upper = 400;
|
|
||||||
} else {
|
|
||||||
lower = windowCenter - windowWidth / 2.0;
|
|
||||||
upper = windowCenter + windowWidth / 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLowerUpperColorTransferFunction({ volumeActor, lower, upper, inverted });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLowerUpperColorTransferFunction({
|
|
||||||
volumeActor,
|
|
||||||
lower,
|
|
||||||
upper,
|
|
||||||
inverted,
|
|
||||||
}) {
|
|
||||||
volumeActor
|
|
||||||
.getProperty()
|
|
||||||
.getRGBTransferFunction(0)
|
|
||||||
.setMappingRange(lower, upper);
|
|
||||||
|
|
||||||
if (inverted) {
|
|
||||||
utilities.invertRgbTransferFunction(
|
|
||||||
volumeActor.getProperty().getRGBTransferFunction(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColormap(volumeActor, colormap) {
|
|
||||||
const mapper = volumeActor.getMapper();
|
|
||||||
mapper.setSampleDistance(1.0);
|
|
||||||
|
|
||||||
const cfun = vtkColorTransferFunction.newInstance();
|
|
||||||
|
|
||||||
// if we have a custom colormap, use it
|
|
||||||
let preset;
|
|
||||||
if (colormaps[colormap]) {
|
|
||||||
preset = colormaps[colormap];
|
|
||||||
} else {
|
|
||||||
preset = vtkColorMaps.getPresetByName(colormap);
|
|
||||||
}
|
|
||||||
|
|
||||||
cfun.applyColorMap(preset);
|
|
||||||
cfun.setMappingRange(0, 5);
|
|
||||||
|
|
||||||
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
|
|
||||||
|
|
||||||
// Create scalar opacity function
|
|
||||||
const ofun = vtkPiecewiseFunction.newInstance();
|
|
||||||
ofun.addPoint(0, 0.0);
|
|
||||||
ofun.addPoint(0.1, 0.9);
|
|
||||||
ofun.addPoint(5, 1.0);
|
|
||||||
|
|
||||||
volumeActor.getProperty().setScalarOpacity(0, ofun);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
setColormap,
|
|
||||||
setColorTransferFunctionFromVolumeMetadata,
|
|
||||||
setLowerUpperColorTransferFunction,
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { imageLoader } from '@cornerstonejs/core';
|
import { imageLoader } from '@cornerstonejs/core';
|
||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import { DICOMWeb, errorHandler } from '@ohif/core';
|
import { DICOMWeb, errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class DicomLoaderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!someInvalidStrings(imageId)) {
|
if (!someInvalidStrings(imageId)) {
|
||||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
return dicomImageLoader.wadouri.loadFileRequest(imageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const findNearbyToolData = (commandsManager, evt) => {
|
|||||||
}
|
}
|
||||||
const { element, currentPoints } = evt.detail;
|
const { element, currentPoints } = evt.detail;
|
||||||
return commandsManager.runCommand(
|
return commandsManager.runCommand(
|
||||||
'getNearbyToolData',
|
'getNearbyAnnotation',
|
||||||
{
|
{
|
||||||
element,
|
element,
|
||||||
canvasCoordinates: currentPoints?.canvas,
|
canvasCoordinates: currentPoints?.canvas,
|
||||||
|
|||||||
@ -0,0 +1,228 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
import getModalityUnit from './utils/getModalityUnit';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
const CircleROI = {
|
||||||
|
toAnnotation: measurement => {},
|
||||||
|
toMeasurement: (
|
||||||
|
csToolsEventDetail,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
getValueTypeFromToolType
|
||||||
|
) => {
|
||||||
|
const { annotation, viewportId } = csToolsEventDetail;
|
||||||
|
const { metadata, data, annotationUID } = annotation;
|
||||||
|
|
||||||
|
if (!metadata || !data) {
|
||||||
|
console.warn('Length tool: Missing metadata or data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||||
|
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(
|
||||||
|
referencedImageId,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (SOPInstanceUID) {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points } = data.handles;
|
||||||
|
|
||||||
|
const mappedAnnotations = getMappedAnnotations(
|
||||||
|
annotation,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||||
|
const getReport = () =>
|
||||||
|
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uid: annotationUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
points,
|
||||||
|
metadata,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||||
|
toolName: metadata.toolName,
|
||||||
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
|
label: data.label,
|
||||||
|
displayText: displayText,
|
||||||
|
data: data.cachedStats,
|
||||||
|
type: getValueTypeFromToolType(toolName),
|
||||||
|
getReport,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||||
|
const { metadata, data } = annotation;
|
||||||
|
const { cachedStats } = data;
|
||||||
|
const { referencedImageId } = metadata;
|
||||||
|
const targets = Object.keys(cachedStats);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
|
if (!referencedImageId) {
|
||||||
|
// Todo: Non-acquisition plane measurement mapping not supported yet
|
||||||
|
throw new Error(
|
||||||
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = getSOPInstanceAttributes(referencedImageId);
|
||||||
|
|
||||||
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SeriesNumber } = displaySet;
|
||||||
|
const { mean, stdDev, max, area, Modality } = targetStats;
|
||||||
|
const unit = getModalityUnit(Modality);
|
||||||
|
|
||||||
|
annotations.push({
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesNumber,
|
||||||
|
frameNumber,
|
||||||
|
Modality,
|
||||||
|
unit,
|
||||||
|
mean,
|
||||||
|
stdDev,
|
||||||
|
max,
|
||||||
|
area,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is used to convert the measurement data to a format that is
|
||||||
|
suitable for the report generation (e.g. for the csv report). The report
|
||||||
|
returns a list of columns and corresponding values.
|
||||||
|
*/
|
||||||
|
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||||
|
const columns = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
// Add Type
|
||||||
|
columns.push('AnnotationType');
|
||||||
|
values.push('Cornerstone:CircleROI');
|
||||||
|
|
||||||
|
mappedAnnotations.forEach(annotation => {
|
||||||
|
const { mean, stdDev, max, area, unit } = annotation;
|
||||||
|
|
||||||
|
if (!mean || !unit || !max || !area) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(
|
||||||
|
`max (${unit})`,
|
||||||
|
`mean (${unit})`,
|
||||||
|
`std (${unit})`,
|
||||||
|
`area (mm2)`
|
||||||
|
);
|
||||||
|
values.push(max, mean, stdDev, area);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (FrameOfReferenceUID) {
|
||||||
|
columns.push('FrameOfReferenceUID');
|
||||||
|
values.push(FrameOfReferenceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points) {
|
||||||
|
columns.push('points');
|
||||||
|
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||||
|
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||||
|
// so that it can be used in the csv report
|
||||||
|
values.push(points.map(p => p.join(' ')).join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(mappedAnnotations, displaySet) {
|
||||||
|
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText = [];
|
||||||
|
|
||||||
|
// Area is the same for all series
|
||||||
|
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||||
|
|
||||||
|
const instance = displaySet.images.find(
|
||||||
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let InstanceNumber;
|
||||||
|
if (instance) {
|
||||||
|
InstanceNumber = instance.InstanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
|
|
||||||
|
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
|
||||||
|
const roundedArea = utils.roundNumber(area || 0, 2);
|
||||||
|
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
||||||
|
|
||||||
|
// Todo: we need a better UI for displaying all these information
|
||||||
|
mappedAnnotations.forEach(mappedAnnotation => {
|
||||||
|
const { unit, max, SeriesNumber } = mappedAnnotation;
|
||||||
|
|
||||||
|
let maxStr = '';
|
||||||
|
if (max) {
|
||||||
|
const roundedMax = utils.roundNumber(max, 2);
|
||||||
|
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
|
||||||
|
if (!displayText.includes(str)) {
|
||||||
|
displayText.push(str);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CircleROI;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
import { displaySetService, utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
const Length = {
|
const Length = {
|
||||||
toAnnotation: measurement => {},
|
toAnnotation: measurement => {},
|
||||||
@ -32,11 +32,8 @@ const Length = {
|
|||||||
throw new Error('Tool not supported');
|
throw new Error('Tool not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||||
SOPInstanceUID,
|
getSOPInstanceAttributes(
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = getSOPInstanceAttributes(
|
|
||||||
referencedImageId,
|
referencedImageId,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
viewportId
|
viewportId
|
||||||
@ -104,11 +101,8 @@ function getMappedAnnotations(annotation, displaySetService) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||||
SOPInstanceUID,
|
getSOPInstanceAttributes(referencedImageId);
|
||||||
SeriesInstanceUID,
|
|
||||||
frameNumber,
|
|
||||||
} = getSOPInstanceAttributes(referencedImageId);
|
|
||||||
|
|
||||||
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
@ -179,12 +173,8 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const displayText = [];
|
const displayText = [];
|
||||||
|
|
||||||
// Area is the same for all series
|
// Area is the same for all series
|
||||||
const {
|
const { length, SeriesNumber, SOPInstanceUID, frameNumber } =
|
||||||
length,
|
mappedAnnotations[0];
|
||||||
SeriesNumber,
|
|
||||||
SOPInstanceUID,
|
|
||||||
frameNumber,
|
|
||||||
} = mappedAnnotations[0];
|
|
||||||
|
|
||||||
const instance = displaySet.images.find(
|
const instance = displaySet.images.find(
|
||||||
image => image.SOPInstanceUID === SOPInstanceUID
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
@ -198,6 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
|
|
||||||
|
if (length === null || length === undefined) return displayText;
|
||||||
const roundedLength = utils.roundNumber(length, 2);
|
const roundedLength = utils.roundNumber(length, 2);
|
||||||
displayText.push(
|
displayText.push(
|
||||||
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||||
@ -4,7 +4,7 @@ import getModalityUnit from './utils/getModalityUnit';
|
|||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
const RectangleROI = {
|
const RectangleROI = {
|
||||||
toAnnotation: measurement => { },
|
toAnnotation: measurement => {},
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsEventDetail,
|
csToolsEventDetail,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -26,11 +26,8 @@ const RectangleROI = {
|
|||||||
throw new Error('Tool not supported');
|
throw new Error('Tool not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||||
SOPInstanceUID,
|
getSOPInstanceAttributes(
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = getSOPInstanceAttributes(
|
|
||||||
referencedImageId,
|
referencedImageId,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
viewportId
|
viewportId
|
||||||
@ -99,11 +96,8 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||||
SOPInstanceUID,
|
getSOPInstanceAttributes(referencedImageId);
|
||||||
SeriesInstanceUID,
|
|
||||||
frameNumber,
|
|
||||||
} = getSOPInstanceAttributes(referencedImageId);
|
|
||||||
|
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
@ -143,7 +137,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
|||||||
|
|
||||||
// Add Type
|
// Add Type
|
||||||
columns.push('AnnotationType');
|
columns.push('AnnotationType');
|
||||||
values.push('Cornerstone:EllipticalROI');
|
values.push('Cornerstone:RectangleROI');
|
||||||
|
|
||||||
mappedAnnotations.forEach(annotation => {
|
mappedAnnotations.forEach(annotation => {
|
||||||
const { mean, stdDev, max, area, unit } = annotation;
|
const { mean, stdDev, max, area, unit } = annotation;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export default [
|
export default [
|
||||||
'Length',
|
'Length',
|
||||||
'EllipticalROI',
|
'EllipticalROI',
|
||||||
|
'CircleROI',
|
||||||
'Bidirectional',
|
'Bidirectional',
|
||||||
'ArrowAnnotate',
|
'ArrowAnnotate',
|
||||||
'Angle',
|
'Angle',
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { MeasurementService } from '@ohif/core';
|
|||||||
import Length from './Length';
|
import Length from './Length';
|
||||||
import Bidirectional from './Bidirectional';
|
import Bidirectional from './Bidirectional';
|
||||||
import EllipticalROI from './EllipticalROI';
|
import EllipticalROI from './EllipticalROI';
|
||||||
|
import CircleROI from './CircleROI';
|
||||||
import ArrowAnnotate from './ArrowAnnotate';
|
import ArrowAnnotate from './ArrowAnnotate';
|
||||||
import CobbAngle from './CobbAngle';
|
import CobbAngle from './CobbAngle';
|
||||||
import Angle from './Angle';
|
import Angle from './Angle';
|
||||||
@ -25,6 +26,7 @@ const measurementServiceMappingsFactory = (
|
|||||||
const {
|
const {
|
||||||
POLYLINE,
|
POLYLINE,
|
||||||
ELLIPSE,
|
ELLIPSE,
|
||||||
|
CIRCLE,
|
||||||
RECTANGLE,
|
RECTANGLE,
|
||||||
BIDIRECTIONAL,
|
BIDIRECTIONAL,
|
||||||
POINT,
|
POINT,
|
||||||
@ -37,6 +39,7 @@ const measurementServiceMappingsFactory = (
|
|||||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||||
Length: POLYLINE,
|
Length: POLYLINE,
|
||||||
EllipticalROI: ELLIPSE,
|
EllipticalROI: ELLIPSE,
|
||||||
|
CircleROI: CIRCLE,
|
||||||
RectangleROI: RECTANGLE,
|
RectangleROI: RECTANGLE,
|
||||||
PlanarFreehandROI: POLYLINE,
|
PlanarFreehandROI: POLYLINE,
|
||||||
Bidirectional: BIDIRECTIONAL,
|
Bidirectional: BIDIRECTIONAL,
|
||||||
@ -104,6 +107,22 @@ const measurementServiceMappingsFactory = (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
CircleROI: {
|
||||||
|
toAnnotation: CircleROI.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
CircleROI.toMeasurement(
|
||||||
|
csToolsAnnotation,
|
||||||
|
displaySetService,
|
||||||
|
cornerstoneViewportService,
|
||||||
|
_getValueTypeFromToolType
|
||||||
|
),
|
||||||
|
matchingCriteria: [
|
||||||
|
{
|
||||||
|
valueType: MeasurementService.VALUE_TYPES.CIRCLE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
RectangleROI: {
|
RectangleROI: {
|
||||||
toAnnotation: RectangleROI.toAnnotation,
|
toAnnotation: RectangleROI.toAnnotation,
|
||||||
toMeasurement: csToolsAnnotation =>
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
|||||||
@ -3,39 +3,16 @@ import * as cornerstone from '@cornerstonejs/core';
|
|||||||
/**
|
/**
|
||||||
* It checks if the imageId is provided then it uses it to query
|
* It checks if the imageId is provided then it uses it to query
|
||||||
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
|
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
|
||||||
* If the imageId is not provided then it uses the sceneUID to get the viewports
|
* If the imageId is not provided then undefined is returned.
|
||||||
* inside the scene and then it checks each viewport to find the one that has
|
|
||||||
* acquisition plane view, and uses the currentImageId of the viewport to
|
|
||||||
* query the metadata and get UIDs.
|
|
||||||
* @param {string} imageId The image id of the referenced image
|
* @param {string} imageId The image id of the referenced image
|
||||||
* @param {string} sceneUID The scene UID of the measurement tool
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function getSOPInstanceAttributes(
|
export default function getSOPInstanceAttributes(imageId) {
|
||||||
imageId,
|
|
||||||
cornerstoneViewportService,
|
|
||||||
viewportId
|
|
||||||
) {
|
|
||||||
if (imageId) {
|
if (imageId) {
|
||||||
return _getUIDFromImageID(imageId);
|
return _getUIDFromImageID(imageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: implement for volume viewports and use the referencedSeriesInstanceUID
|
// Todo: implement for volume viewports and use the referencedSeriesInstanceUID
|
||||||
|
|
||||||
// if no imageId => measurement is not in the acquisition plane
|
|
||||||
// const metadata = getUIDFromScene(cornerstoneViewportService, viewportId);
|
|
||||||
|
|
||||||
// if (!metadata) {
|
|
||||||
// throw new Error('Not viewport with imageId found');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Since the series and study UID is derived from another viewport in the
|
|
||||||
// // same scene, we cannot include the SOPInstanceUID
|
|
||||||
// return {
|
|
||||||
// SOPInstanceUID: null,
|
|
||||||
// SeriesInstanceUID: metadata.SeriesInstanceUID,
|
|
||||||
// StudyInstanceUID: metadata.StudyInstanceUID,
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getUIDFromImageID(imageId) {
|
function _getUIDFromImageID(imageId) {
|
||||||
@ -48,23 +25,3 @@ function _getUIDFromImageID(imageId) {
|
|||||||
frameNumber: instance.frameNumber || 1,
|
frameNumber: instance.frameNumber || 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// function getUIDFromScene(cornerstoneViewportService) {
|
|
||||||
// const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
|
||||||
// const scene = renderingEngine.getScene(sceneUID);
|
|
||||||
|
|
||||||
// const viewportUIDs = scene.getViewportIds();
|
|
||||||
|
|
||||||
// if (viewportUIDs.length === 0) {
|
|
||||||
// throw new Error('No viewport found in scene');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (let i = 0; i < viewportUIDs.length; i++) {
|
|
||||||
// const vp = renderingEngine.getViewport(viewportUIDs[i]);
|
|
||||||
// const imageId = vp.getCurrentImageId();
|
|
||||||
|
|
||||||
// if (imageId) {
|
|
||||||
// return _getUIDFromImageID(imageId);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -20,3 +20,17 @@ export function easeInOutBell(x: number, baseline: number): number {
|
|||||||
return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
|
return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reversed bell curved function that starts from 1 and goes to baseline and
|
||||||
|
* come back to 1 again. It uses ease in out quadratic for css transition
|
||||||
|
* timing function for each side of the curve.
|
||||||
|
*
|
||||||
|
* @param {number} x - The current time, in the range [0, 1].
|
||||||
|
* @param {number} baseline - The baseline value to start from and return to.
|
||||||
|
* @returns the value of the transition at time x.
|
||||||
|
*/
|
||||||
|
export function reverseEaseInOutBell(x: number, baseline: number): number {
|
||||||
|
const y = easeInOutBell(x, baseline);
|
||||||
|
return -y + 1 + baseline;
|
||||||
|
}
|
||||||
|
|||||||
@ -32,12 +32,12 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/i18n": "^1.0.0",
|
"@ohif/i18n": "^1.0.0",
|
||||||
"dcmjs": "^0.29.4",
|
"dcmjs": "^0.29.5",
|
||||||
"dicomweb-client": "^0.8.4",
|
"dicomweb-client": "^0.10.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^10.11.0",
|
"react-i18next": "^12.2.2",
|
||||||
"webpack": "^5.50.0",
|
"webpack": "^5.50.0",
|
||||||
"webpack-merge": "^5.7.3"
|
"webpack-merge": "^5.7.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import ContextMenuController from './ContextMenuController';
|
import ContextMenuController from './ContextMenuController';
|
||||||
import ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||||
import defaultContextMenu from './defaultContextMenu';
|
import defaultContextMenu from './defaultContextMenu';
|
||||||
import * as CustomizeableContextMenuTypes from './types';
|
import * as CustomizeableContextMenuTypes from './types';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
study.series.forEach(aSeries => {
|
study.series.forEach(aSeries => {
|
||||||
numInstances += aSeries.instances.length;
|
numInstances += aSeries.instances.length;
|
||||||
modalities.add(aSeries.Modality);
|
modalities.add(aSeries.instances[0].Modality);
|
||||||
});
|
});
|
||||||
|
|
||||||
// first instance in the first series
|
// first instance in the first series
|
||||||
@ -87,7 +87,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
date: firstInstance.StudyDate,
|
date: firstInstance.StudyDate,
|
||||||
description: firstInstance.StudyDescription,
|
description: firstInstance.StudyDescription,
|
||||||
mrn: firstInstance.PatientID,
|
mrn: firstInstance.PatientID,
|
||||||
patientName: { Alphabetic: firstInstance.PatientName },
|
patientName: utils.formatPN(firstInstance.PatientName),
|
||||||
studyInstanceUid: firstInstance.StudyInstanceUID,
|
studyInstanceUid: firstInstance.StudyInstanceUID,
|
||||||
time: firstInstance.StudyTime,
|
time: firstInstance.StudyTime,
|
||||||
//
|
//
|
||||||
@ -103,9 +103,20 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
search: studyInstanceUID => {
|
||||||
search: () => {
|
const study = DicomMetadataStore.getStudy(studyInstanceUID);
|
||||||
console.debug(' DICOMLocal QUERY SERIES SEARCH');
|
return study.series.map(aSeries => {
|
||||||
|
const firstInstance = aSeries?.instances[0];
|
||||||
|
return {
|
||||||
|
studyInstanceUid: studyInstanceUID,
|
||||||
|
seriesInstanceUid: firstInstance.SeriesInstanceUID,
|
||||||
|
modality: firstInstance.Modality,
|
||||||
|
seriesNumber: firstInstance.SeriesNumber,
|
||||||
|
seriesDate: firstInstance.SeriesDate,
|
||||||
|
numSeriesInstances: aSeries.instances.length,
|
||||||
|
description: firstInstance.SeriesDescription,
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instances: {
|
instances: {
|
||||||
@ -115,6 +126,18 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
|
directURL: params => {
|
||||||
|
const { instance, tag, defaultType } = params;
|
||||||
|
|
||||||
|
const value = instance[tag];
|
||||||
|
if (value instanceof Array && value[0] instanceof ArrayBuffer) {
|
||||||
|
return URL.createObjectURL(
|
||||||
|
new Blob([value[0]], {
|
||||||
|
type: defaultType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
series: {
|
series: {
|
||||||
metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
|
metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
|
||||||
if (!StudyInstanceUID) {
|
if (!StudyInstanceUID) {
|
||||||
@ -188,6 +211,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
displaySet.images.forEach(instance => {
|
displaySet.images.forEach(instance => {
|
||||||
const NumberOfFrames = instance.NumberOfFrames;
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
|
// in multiframe we start at frame 1
|
||||||
for (let i = 1; i <= NumberOfFrames; i++) {
|
for (let i = 1; i <= NumberOfFrames; i++) {
|
||||||
const imageId = this.getImageIdsForInstance({
|
const imageId = this.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
|
|||||||
@ -22,8 +22,8 @@ import {
|
|||||||
retrieveStudyMetadata,
|
retrieveStudyMetadata,
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
} from './retrieveStudyMetadata.js';
|
} from './retrieveStudyMetadata.js';
|
||||||
import StaticWadoClient from './utils/StaticWadoClient.js';
|
import StaticWadoClient from './utils/StaticWadoClient';
|
||||||
import getDirectURL from '../utils/getDirectURL.js';
|
import getDirectURL from '../utils/getDirectURL';
|
||||||
|
|
||||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
|
||||||
@ -61,6 +61,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
singlepart,
|
singlepart,
|
||||||
} = dicomWebConfig;
|
} = dicomWebConfig;
|
||||||
|
|
||||||
|
const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
|
||||||
|
|
||||||
const qidoConfig = {
|
const qidoConfig = {
|
||||||
url: qidoRoot,
|
url: qidoRoot,
|
||||||
staticWado,
|
staticWado,
|
||||||
@ -90,7 +92,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: ({ params, query }) => {
|
initialize: ({ params, query }) => {
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||||
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
const queryStudyInstanceUIDs = utils.splitComma(
|
||||||
|
query.getAll('StudyInstanceUIDs')
|
||||||
|
);
|
||||||
|
|
||||||
const StudyInstanceUIDs =
|
const StudyInstanceUIDs =
|
||||||
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
|
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
|
||||||
@ -174,7 +178,18 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
||||||
*/
|
*/
|
||||||
directURL: params => {
|
directURL: params => {
|
||||||
return getDirectURL(wadoRoot, params);
|
return getDirectURL({ wadoRoot, singlepart }, params);
|
||||||
|
},
|
||||||
|
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
|
||||||
|
const options = {
|
||||||
|
multipart: false,
|
||||||
|
BulkDataURI,
|
||||||
|
StudyInstanceUID,
|
||||||
|
};
|
||||||
|
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
||||||
|
const ret = (val && val[0]) || undefined;
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
metadata: async ({
|
metadata: async ({
|
||||||
@ -217,12 +232,20 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
store: {
|
store: {
|
||||||
dicom: async dataset => {
|
dicom: async (dataset, request) => {
|
||||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
wadoDicomWebClient.headers = headers;
|
wadoDicomWebClient.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataset instanceof ArrayBuffer) {
|
||||||
|
const options = {
|
||||||
|
datasets: [dataset],
|
||||||
|
request,
|
||||||
|
};
|
||||||
|
|
||||||
|
await wadoDicomWebClient.storeInstances(options);
|
||||||
|
} else {
|
||||||
const meta = {
|
const meta = {
|
||||||
FileMetaInformationVersion:
|
FileMetaInformationVersion:
|
||||||
dataset._meta.FileMetaInformationVersion.Value,
|
dataset._meta.FileMetaInformationVersion.Value,
|
||||||
@ -242,11 +265,14 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
datasets: [part10Buffer],
|
datasets: [part10Buffer],
|
||||||
|
request,
|
||||||
};
|
};
|
||||||
|
|
||||||
await wadoDicomWebClient.storeInstances(options);
|
await wadoDicomWebClient.storeInstances(options);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
_retrieveSeriesMetadataSync: async (
|
_retrieveSeriesMetadataSync: async (
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
filters,
|
filters,
|
||||||
@ -368,7 +394,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
};
|
};
|
||||||
// Todo: this needs to be from wado dicom web client
|
// Todo: this needs to be from wado dicom web client
|
||||||
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
||||||
const ret = (val && val[0]) || undefined;
|
// There are DICOM PDF cases where the first ArrayBuffer in the array is
|
||||||
|
// the bulk data and DICOM video cases where the second ArrayBuffer is
|
||||||
|
// the bulk data. Here we play it safe and do a find.
|
||||||
|
const ret =
|
||||||
|
(val instanceof Array &&
|
||||||
|
val.find(arrayBuffer => arrayBuffer?.byteLength)) ||
|
||||||
|
undefined;
|
||||||
value.Value = ret;
|
value.Value = ret;
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
@ -469,6 +501,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
});
|
});
|
||||||
return imageIds;
|
return imageIds;
|
||||||
},
|
},
|
||||||
|
getConfig() {
|
||||||
|
return dicomWebConfigCopy;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (supportsReject) {
|
if (supportsReject) {
|
||||||
|
|||||||
@ -9,18 +9,19 @@ import { api } from 'dicomweb-client';
|
|||||||
*/
|
*/
|
||||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||||
static studyFilterKeys = {
|
static studyFilterKeys = {
|
||||||
StudyInstanceUID: '0020000D',
|
studyinstanceuid: '0020000D',
|
||||||
PatientName: '00100010',
|
patientname: '00100010',
|
||||||
'00100020': 'mrn',
|
'00100020': 'mrn',
|
||||||
StudyDescription: '00081030',
|
studydescription: '00081030',
|
||||||
StudyDate: '00080020',
|
studydate: '00080020',
|
||||||
ModalitiesInStudy: '00080061',
|
modalitiesinstudy: '00080061',
|
||||||
AccessionNumber: '00080050',
|
accessionnumber: '00080050',
|
||||||
};
|
};
|
||||||
|
|
||||||
static seriesFilterKeys = {
|
static seriesFilterKeys = {
|
||||||
SeriesInstanceUID: '0020000E',
|
seriesinstanceuid: '0020000E',
|
||||||
SeriesNumber: '00200011',
|
seriesnumber: '00200011',
|
||||||
|
modality: '00080060',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(qidoConfig) {
|
constructor(qidoConfig) {
|
||||||
@ -37,15 +38,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
async searchForStudies(options) {
|
async searchForStudies(options) {
|
||||||
if (!this.staticWado) return super.searchForStudies(options);
|
if (!this.staticWado) return super.searchForStudies(options);
|
||||||
|
|
||||||
let searchResult = await super.searchForStudies(options);
|
const searchResult = await super.searchForStudies(options);
|
||||||
const { queryParams } = options;
|
const { queryParams } = options;
|
||||||
|
|
||||||
if (!queryParams) return searchResult;
|
if (!queryParams) return searchResult;
|
||||||
|
|
||||||
|
const lowerParams = this.toLowerParams(queryParams);
|
||||||
const filtered = searchResult.filter(study => {
|
const filtered = searchResult.filter(study => {
|
||||||
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
||||||
if (
|
if (
|
||||||
!this.filterItem(
|
!this.filterItem(
|
||||||
key,
|
key,
|
||||||
queryParams,
|
lowerParams,
|
||||||
study,
|
study,
|
||||||
StaticWadoClient.studyFilterKeys
|
StaticWadoClient.studyFilterKeys
|
||||||
)
|
)
|
||||||
@ -61,16 +65,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
async searchForSeries(options) {
|
async searchForSeries(options) {
|
||||||
if (!this.staticWado) return super.searchForSeries(options);
|
if (!this.staticWado) return super.searchForSeries(options);
|
||||||
|
|
||||||
let searchResult = await super.searchForSeries(options);
|
const searchResult = await super.searchForSeries(options);
|
||||||
const { queryParams } = options;
|
const { queryParams } = options;
|
||||||
if (!queryParams) return searchResult;
|
if (!queryParams) return searchResult;
|
||||||
const filtered = searchResult.filter(study => {
|
const lowerParams = this.toLowerParams(queryParams);
|
||||||
|
|
||||||
|
const filtered = searchResult.filter(series => {
|
||||||
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
||||||
if (
|
if (
|
||||||
!this.filterItem(
|
!this.filterItem(
|
||||||
key,
|
key,
|
||||||
queryParams,
|
lowerParams,
|
||||||
study,
|
series,
|
||||||
StaticWadoClient.seriesFilterKeys
|
StaticWadoClient.seriesFilterKeys
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -136,13 +142,13 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
/**
|
/**
|
||||||
* Filters the return list by the query parameters.
|
* Filters the return list by the query parameters.
|
||||||
*
|
*
|
||||||
* @param {*} key
|
* @param anyCaseKey - a possible search key
|
||||||
* @param {*} queryParams
|
* @param queryParams -
|
||||||
* @param {*} study
|
* @param {*} study
|
||||||
* @param {*} sourceFilterMap
|
* @param {*} sourceFilterMap
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
filterItem(key, queryParams, study, sourceFilterMap) {
|
filterItem(key: string, queryParams, study, sourceFilterMap) {
|
||||||
const altKey = sourceFilterMap[key] || key;
|
const altKey = sourceFilterMap[key] || key;
|
||||||
if (!queryParams) return true;
|
if (!queryParams) return true;
|
||||||
const testValue = queryParams[key] || queryParams[altKey];
|
const testValue = queryParams[key] || queryParams[altKey];
|
||||||
@ -153,6 +159,15 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
return this.compareDateRange(testValue, valueElem.Value[0]);
|
return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||||
}
|
}
|
||||||
const value = valueElem.Value;
|
const value = valueElem.Value;
|
||||||
return this.compareValues(testValue, value) && true;
|
return this.compareValues(testValue, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts the query parameters to lower case query parameters */
|
||||||
|
toLowerParams(queryParams: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const lowerParams = {};
|
||||||
|
Object.entries(queryParams).forEach(([key, value]) => {
|
||||||
|
lowerParams[key.toLowerCase()] = value;
|
||||||
|
});
|
||||||
|
return lowerParams;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
84
extensions/default/src/DicomWebProxyDataSource/index.js
Normal file
84
extensions/default/src/DicomWebProxyDataSource/index.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { IWebApiDataSource } from '@ohif/core';
|
||||||
|
import { createDicomWebApi } from '../DicomWebDataSource/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This datasource is initialized with a url that returns a JSON object with a
|
||||||
|
* dicomWeb datasource configuration array present in a "servers" object.
|
||||||
|
*
|
||||||
|
* Only the first array item is parsed, if there are multiple items in the
|
||||||
|
* dicomWeb configuration array
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function createDicomWebProxyApi(
|
||||||
|
dicomWebProxyConfig,
|
||||||
|
UserAuthenticationService
|
||||||
|
) {
|
||||||
|
const { name } = dicomWebProxyConfig;
|
||||||
|
let dicomWebDelegate = undefined;
|
||||||
|
|
||||||
|
const implementation = {
|
||||||
|
initialize: async ({ params, query }) => {
|
||||||
|
let studyInstanceUIDs = [];
|
||||||
|
|
||||||
|
// there seem to be a couple of variations of the case for this parameter
|
||||||
|
const queryStudyInstanceUIDs =
|
||||||
|
query.get('studyInstanceUIDs') || query.get('studyInstanceUids');
|
||||||
|
if (!queryStudyInstanceUIDs) {
|
||||||
|
throw new Error(`No studyInstanceUids in request for '${name}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = query.get('url');
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error(`No url for '${name}'`);
|
||||||
|
} else {
|
||||||
|
const response = await fetch(url);
|
||||||
|
let data = await response.json();
|
||||||
|
if (!data.servers?.dicomWeb?.[0]) {
|
||||||
|
throw new Error('Invalid configuration returned by url');
|
||||||
|
}
|
||||||
|
|
||||||
|
dicomWebDelegate = createDicomWebApi(
|
||||||
|
data.servers.dicomWeb[0],
|
||||||
|
UserAuthenticationService
|
||||||
|
);
|
||||||
|
studyInstanceUIDs = queryStudyInstanceUIDs.split(';');
|
||||||
|
}
|
||||||
|
return studyInstanceUIDs;
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
studies: {
|
||||||
|
search: params => dicomWebDelegate.query.studies.search(params),
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
search: (...args) => dicomWebDelegate.query.series.search(...args),
|
||||||
|
},
|
||||||
|
instances: {
|
||||||
|
search: (studyInstanceUid, queryParameters) =>
|
||||||
|
dicomWebDelegate.query.instances.search(
|
||||||
|
studyInstanceUid,
|
||||||
|
queryParameters
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
retrieve: {
|
||||||
|
directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args),
|
||||||
|
series: {
|
||||||
|
metadata: (...args) =>
|
||||||
|
dicomWebDelegate.retrieve.series.metadata(...args),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
dicom: (...args) => dicomWebDelegate.store(...args),
|
||||||
|
},
|
||||||
|
deleteStudyMetadataPromise: (...args) =>
|
||||||
|
dicomWebDelegate.deleteStudyMetadataPromise(...args),
|
||||||
|
getImageIdsForDisplaySet: (...args) =>
|
||||||
|
dicomWebDelegate.getImageIdsForDisplaySet(...args),
|
||||||
|
getImageIdsForInstance: (...args) =>
|
||||||
|
dicomWebDelegate.getImageIdsForInstance(...args),
|
||||||
|
};
|
||||||
|
return IWebApiDataSource.create(implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createDicomWebProxyApi };
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user