commit
bedc379e97
@ -2,7 +2,7 @@ version: '3.5'
|
||||
|
||||
services:
|
||||
orthanc:
|
||||
image: jodogne/orthanc-plugins:1.5.7
|
||||
image: jodogne/orthanc-plugins:1.11.0
|
||||
hostname: orthanc
|
||||
volumes:
|
||||
# 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
|
||||
|
||||
- [ ] Brief description of changes
|
||||
- [ ] Links to any relevant issues
|
||||
- [ ] Required status checks are passing
|
||||
- [ ] User cases if changes impact the user's experience
|
||||
- [ ] `@mention` a maintainer to request a review
|
||||
<!-- Do Not Delete This! pr_template -->
|
||||
<!-- Please read our Rules of Conduct: https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md -->
|
||||
<!-- 🕮 Read our guide about our Contributing Guide here https://v3-docs.ohif.org/development/contributing -->
|
||||
<!-- :hand: Thank you for starting this amazing contribution! -->
|
||||
|
||||
<!--
|
||||
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 -->
|
||||
[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
|
||||
#
|
||||
# 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
|
||||
daysUntilClose: 7
|
||||
daysUntilClose: 60
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- 'Story :raised_hands:'
|
||||
- 'Bug: Verified :bug:'
|
||||
- 'Task: CI/Tooling :robot:'
|
||||
- 'Task: Docs 📖'
|
||||
- 'Task: Docs :book:'
|
||||
- 'Task: Refactor :hammer_and_wrench:'
|
||||
- 'Task: Tests :microscope:'
|
||||
- 'PR: Awaiting Review 👀'
|
||||
- 'Triage :white_flag:'
|
||||
- 'Extension: Discussion'
|
||||
- 'Announcement 🎉'
|
||||
- 'IDC:priority'
|
||||
- 'IDC:candidate'
|
||||
- 'IDC:collaboration'
|
||||
- 'Community: Request :hand:'
|
||||
- 'Community: Report :bug:'
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
# 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
|
||||
// JS version (currently ES5). Most of these leverage ES6+ features,
|
||||
// that we need to transpile to a different syntax.
|
||||
exclude,
|
||||
exclude: [/(codecs)/, /(dicomicc)/, exclude],
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
// Find babel.config.js in monorepo root
|
||||
// https://babeljs.io/docs/en/options#rootmode
|
||||
rootMode: 'upward',
|
||||
envName: mode,
|
||||
cacheCompression: false,
|
||||
cacheDirectory: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,7 +77,11 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
},
|
||||
module: {
|
||||
noParse: [/(codec)/, /(dicomicc)/],
|
||||
rules: [
|
||||
transpileJavaScriptRule(mode),
|
||||
loadWebWorkersRule,
|
||||
@ -89,6 +93,10 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
},
|
||||
},
|
||||
cssToJavaScript,
|
||||
{
|
||||
test: /\.wasm/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
], //.concat(vtkRules),
|
||||
},
|
||||
resolve: {
|
||||
@ -103,8 +111,10 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
'@hooks': path.resolve(__dirname, '../platform/viewer/src/hooks'),
|
||||
'@routes': path.resolve(__dirname, '../platform/viewer/src/routes'),
|
||||
'@state': path.resolve(__dirname, '../platform/viewer/src/state'),
|
||||
'cornerstone-wado-image-loader':
|
||||
'cornerstone-wado-image-loader/dist/dynamic-import/cornerstoneWADOImageLoader.min.js',
|
||||
'dicom-microscopy-viewer':
|
||||
'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
|
||||
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
|
||||
const { extendDefaultPlugins } = require('svgo');
|
||||
|
||||
module.exports = {
|
||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||
presets: [
|
||||
@ -7,7 +9,19 @@ module.exports = {
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
'inline-react-svg',
|
||||
[
|
||||
'inline-react-svg',
|
||||
{
|
||||
svgo: {
|
||||
plugins: extendDefaultPlugins([
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false,
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
'@babel/plugin-transform-typescript',
|
||||
['@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
|
||||
|
||||
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
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^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-dom": "^6.8.1",
|
||||
"webpack": "^5.50.0",
|
||||
@ -46,16 +46,16 @@
|
||||
"react-color": "^2.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.0",
|
||||
"@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.4.5",
|
||||
"@babel/plugin-transform-runtime": "^7.5.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.0.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",
|
||||
|
||||
@ -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: {},
|
||||
sopClassUids,
|
||||
instance,
|
||||
instances: [instance],
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
@ -101,9 +103,12 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = segDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
if (
|
||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||
loadPromises[SOPInstanceUID]
|
||||
loadPromises[SOPInstanceUID] &&
|
||||
_segmentationExists(segDisplaySet, segmentationService)
|
||||
) {
|
||||
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
|
||||
// and also return the same promise to any other callers.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
if (_segmentationExistsInCache(segDisplaySet, segmentationService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!segDisplaySet.segments ||
|
||||
Object.keys(segDisplaySet.segments).length === 0
|
||||
@ -133,11 +132,8 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
null,
|
||||
suppressEvents
|
||||
)
|
||||
segmentationService
|
||||
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
|
||||
.then(() => {
|
||||
segDisplaySet.loading = false;
|
||||
resolve();
|
||||
@ -175,12 +171,11 @@ async function _loadSegments(extensionManager, segDisplaySet, headers) {
|
||||
return segments;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(segDisplaySet, segmentationService) {
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
const labelmapVolumeId = segDisplaySet.displaySetInstanceUID;
|
||||
const segVolume = segmentationService.getLabelmapVolume(labelmapVolumeId);
|
||||
|
||||
return segVolume !== undefined;
|
||||
return segmentationService.getSegmentation(
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
function _getPixelData(dataset, segments) {
|
||||
|
||||
@ -3,8 +3,11 @@ import React from 'react';
|
||||
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getSopClassHandlerModule, {
|
||||
protocols,
|
||||
} from './getSopClassHandlerModule';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -31,23 +34,16 @@ const extension = {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
|
||||
getPanelModule: ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}): Types.Panel[] => {
|
||||
const wrappedPanelSegmentation = () => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
@ -68,6 +64,7 @@ const extension = {
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
@ -90,6 +87,11 @@ const extension = {
|
||||
* Examples include the default sop class handler provided by the default extension
|
||||
*/
|
||||
getSopClassHandlerModule,
|
||||
getHangingProtocolModule,
|
||||
};
|
||||
|
||||
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,
|
||||
commandsManager,
|
||||
}) {
|
||||
const {
|
||||
segmentationService,
|
||||
uiDialogService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [
|
||||
initialSegmentationConfigurations,
|
||||
setInitialSegmentationConfigurations,
|
||||
] = useState(segmentationService.getConfiguration());
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() =>
|
||||
segmentationService.getSegmentations()
|
||||
@ -62,6 +55,7 @@ export default function PanelSegmentation({
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
setSegmentationConfiguration(segmentationService.getConfiguration());
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
@ -184,7 +178,7 @@ export default function PanelSegmentation({
|
||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
};
|
||||
|
||||
const setSegmentationConfiguration = useCallback(
|
||||
const _setSegmentationConfiguration = useCallback(
|
||||
(segmentationId, key, value) => {
|
||||
segmentationService.setConfiguration({
|
||||
segmentationId,
|
||||
@ -214,54 +208,51 @@ export default function PanelSegmentation({
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
segmentationConfig={{
|
||||
initialConfig: initialSegmentationConfigurations,
|
||||
usePercentage: true,
|
||||
}}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderOutline',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineOpacity',
|
||||
value
|
||||
)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderFill',
|
||||
value
|
||||
)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineWidthActive',
|
||||
value
|
||||
)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlpha',
|
||||
value
|
||||
)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
setSegmentationConfiguration(
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlphaInactive',
|
||||
value
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import React, { useReducer } from 'react';
|
||||
|
||||
// Todo: use defaults in cs3d
|
||||
const initialState = {
|
||||
renderOutline: true,
|
||||
|
||||
@ -1,34 +1,12 @@
|
||||
function createSEGToolGroupAndAddTools(
|
||||
toolGroupService,
|
||||
toolGroupId,
|
||||
extensionManager
|
||||
ToolGroupService,
|
||||
customizationService,
|
||||
toolGroupId
|
||||
) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
const { tools } =
|
||||
customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
|
||||
const { toolNames, Enums } = utilityModule.exports;
|
||||
|
||||
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, {});
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||
}
|
||||
|
||||
export default createSEGToolGroupAndAddTools;
|
||||
|
||||
@ -3,7 +3,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
|
||||
LoadingIndicatorTotalPercent,
|
||||
useViewportGrid,
|
||||
ViewportActionBar,
|
||||
} from '@ohif/ui';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
@ -31,6 +33,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||
@ -43,7 +46,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
const segDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||
|
||||
// States
|
||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||
@ -58,7 +60,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
||||
const [element, setElement] = useState(null);
|
||||
const [processingProgress, setProcessingProgress] = useState({
|
||||
segmentIndex: 1,
|
||||
percentComplete: null,
|
||||
totalSegments: null,
|
||||
});
|
||||
|
||||
@ -129,6 +131,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||
|
||||
// Segment 0 is always background
|
||||
|
||||
if (newSelectedSegmentIndex > numberOfSegments - 1) {
|
||||
newSelectedSegmentIndex = 1;
|
||||
} else if (newSelectedSegmentIndex === 0) {
|
||||
@ -163,7 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (
|
||||
evt.segDisplaySet.displaySetInstanceUID ===
|
||||
@ -190,10 +194,10 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED,
|
||||
({ segmentIndex, numSegments }) => {
|
||||
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||
({ percentComplete, numSegments }) => {
|
||||
setProcessingProgress({
|
||||
segmentIndex,
|
||||
percentComplete,
|
||||
totalSegments: numSegments,
|
||||
});
|
||||
}
|
||||
@ -239,8 +243,8 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
// only, and does NOT interfere with currently displayed segmentations.
|
||||
toolGroup = createSEGToolGroupAndAddTools(
|
||||
toolGroupService,
|
||||
toolGroupId,
|
||||
extensionManager
|
||||
customizationService,
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
@ -351,42 +355,14 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
{segIsLoading && (
|
||||
<LoadingIndicatorProgress
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="w-full h-full"
|
||||
progress={
|
||||
processingProgress.totalSegments !== null
|
||||
? ((processingProgress.segmentIndex + 1) /
|
||||
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>
|
||||
)
|
||||
}
|
||||
totalNumbers={processingProgress.totalSegments}
|
||||
percentComplete={processingProgress.percentComplete}
|
||||
loadingText="Loading SEG..."
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -32,11 +32,11 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/core": ">=3.0.0",
|
||||
"@ohif/extension-cornerstone": ">=3.0.0",
|
||||
"@ohif/extension-measurement-tracking": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@ -46,7 +46,7 @@
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"classnames": "^2.3.2",
|
||||
"@cornerstonejs/adapters": "^0.6.0",
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/tools": "^0.60.1"
|
||||
"@cornerstonejs/core": "^0.47.1",
|
||||
"@cornerstonejs/tools": "^0.67.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { metaData, utilities } from '@cornerstonejs/core';
|
||||
|
||||
import OHIF from '@ohif/core';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
@ -41,7 +41,6 @@ const _generateReport = (
|
||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||
}
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
@ -104,7 +103,18 @@ const commandsModule = ({}) => {
|
||||
additionalFindingTypes,
|
||||
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);
|
||||
|
||||
@ -112,6 +122,11 @@ const commandsModule = ({}) => {
|
||||
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;
|
||||
} catch (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 { utils, classes } from '@ohif/core';
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
type InstanceMetadata = Types.InstanceMetadata;
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
||||
|
||||
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||
|
||||
// TODO ->
|
||||
// Add SR thumbnail
|
||||
// Make viewport
|
||||
@ -22,6 +25,17 @@ const sopClassUids = [
|
||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||
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 = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
@ -50,15 +64,31 @@ const RELATIONSHIP_TYPE = {
|
||||
|
||||
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
|
||||
* 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)
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* display.
|
||||
* @param instances is a set of instances all from the same series
|
||||
* @param servicesManager is the services that can be used for creating
|
||||
* @returns The list of display sets created for the given instances object
|
||||
@ -74,6 +104,9 @@ function _getDisplaySetsFromSeries(
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -86,15 +119,20 @@ function _getDisplaySetsFromSeries(
|
||||
ConceptNameCodeSequence,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
||||
|
||||
if (
|
||||
!ConceptNameCodeSequence ||
|
||||
ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
) {
|
||||
console.log(
|
||||
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
|
||||
);
|
||||
servicesManager.services.uiNotificationService.show({
|
||||
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 [];
|
||||
}
|
||||
|
||||
@ -111,14 +149,13 @@ function _getDisplaySetsFromSeries(
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
instances,
|
||||
// Others is a historical value used for instances which is deprecated and will be removed
|
||||
others: instances,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
sopClassUids,
|
||||
instance,
|
||||
addInstances,
|
||||
};
|
||||
|
||||
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
||||
@ -327,7 +364,7 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
trackingUniqueIdentifier => {
|
||||
const mergedContentSequence =
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
trackingUniqueIdentifier
|
||||
];
|
||||
|
||||
const measurement = _processMeasurement(mergedContentSequence);
|
||||
@ -367,7 +404,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
|
||||
if (
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
trackingUniqueIdentifier
|
||||
] === undefined
|
||||
) {
|
||||
// Add the full ContentSequence
|
||||
@ -473,18 +510,18 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
const Finding = mergedContentSequence.find(
|
||||
const finding = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.Finding
|
||||
);
|
||||
|
||||
const FindingSites = mergedContentSequence.filter(
|
||||
const findingSites = mergedContentSequence.filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.SRT &&
|
||||
CodingSchemeDesignators.SRT &&
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.FindingSite
|
||||
CodeNameCodeSequenceValues.FindingSite
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
@ -496,28 +533,28 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
};
|
||||
|
||||
if (
|
||||
Finding &&
|
||||
finding &&
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||
finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
Finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
) {
|
||||
measurement.labels.push({
|
||||
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.
|
||||
if (FindingSites.length) {
|
||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
||||
if (findingSites.length) {
|
||||
const cornerstoneFreeTextFindingSite = findingSites.find(
|
||||
FindingSite =>
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
@ -633,17 +670,16 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
|
||||
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||
const { ReferencedSOPSequence } = item;
|
||||
if (!ReferencedSOPSequence) return;
|
||||
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
|
||||
if (ref.ReferencedSOPClassUID) {
|
||||
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
|
||||
|
||||
if (item.hasOwnProperty('ReferencedSOPClassUID')) {
|
||||
const {
|
||||
ReferencedSOPClassUID,
|
||||
ReferencedSOPInstanceUID,
|
||||
} = ReferencedSOPSequence;
|
||||
|
||||
referencedImages.push({
|
||||
ReferencedSOPClassUID,
|
||||
ReferencedSOPInstanceUID,
|
||||
});
|
||||
referencedImages.push({
|
||||
ReferencedSOPClassUID,
|
||||
ReferencedSOPInstanceUID,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -651,6 +687,7 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
}
|
||||
|
||||
function _getSequenceAsArray(sequence) {
|
||||
if (!sequence) return [];
|
||||
return Array.isArray(sequence) ? sequence : [sequence];
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule, {
|
||||
srProtocol,
|
||||
} from './getHangingProtocolModule';
|
||||
import onModeEnter from './onModeEnter';
|
||||
import commandsModule from './commandsModule';
|
||||
import init from './init';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import preRegistration from './init';
|
||||
import { id } from './id.js';
|
||||
import toolNames from './tools/toolNames';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -31,9 +35,7 @@ const dicomSRExtension = {
|
||||
id,
|
||||
onModeEnter,
|
||||
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
preRegistration,
|
||||
|
||||
/**
|
||||
*
|
||||
@ -54,14 +56,9 @@ const dicomSRExtension = {
|
||||
|
||||
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
||||
},
|
||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
getCommandsModule,
|
||||
getSopClassHandlerModule,
|
||||
// Include dynmically computed values such as toolNames not known till instantiation
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
@ -75,4 +72,6 @@ const 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,
|
||||
CobbAngleTool,
|
||||
EllipticalROITool,
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
} from '@cornerstonejs/tools';
|
||||
@ -24,6 +25,7 @@ export default function init({
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||
@ -40,6 +42,7 @@ export default function init({
|
||||
SRLength: dashedLine,
|
||||
SRBidirectional: dashedLine,
|
||||
SREllipticalROI: dashedLine,
|
||||
SRCircleROI: dashedLine,
|
||||
SRArrowAnnotate: dashedLine,
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
|
||||
@ -4,7 +4,7 @@ export default function onModeEnter({ servicesManager }) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
|
||||
const srDisplaySets = displaySetCache.filter(
|
||||
const srDisplaySets = [...displaySetCache.values()].filter(
|
||||
ds => ds.SOPClassHandlerId === SOPClassHandlerId
|
||||
);
|
||||
|
||||
|
||||
@ -307,13 +307,26 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
|
||||
const ellipsePointsWorld = data;
|
||||
|
||||
const rotation = viewport.getRotation();
|
||||
|
||||
canvasCoordinates = ellipsePointsWorld.map(p =>
|
||||
viewport.worldToCanvas(p)
|
||||
);
|
||||
|
||||
const canvasCorners = <Array<Types.Point2>>(
|
||||
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
|
||||
);
|
||||
let canvasCorners;
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
const lineUID = `${index}`;
|
||||
drawing.drawEllipse(
|
||||
|
||||
@ -5,6 +5,7 @@ const toolNames = {
|
||||
SRLength: 'SRLength',
|
||||
SRBidirectional: 'SRBidirectional',
|
||||
SREllipticalROI: 'SREllipticalROI',
|
||||
SRCircleROI: 'SRCircleROI',
|
||||
SRArrowAnnotate: 'SRArrowAnnotate',
|
||||
SRAngle: 'SRAngle',
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
||||
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
Notification,
|
||||
Tooltip,
|
||||
useViewportDialog,
|
||||
useViewportGrid,
|
||||
ViewportActionBar,
|
||||
} from '@ohif/ui';
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@ -27,8 +21,8 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportIndex,
|
||||
viewportOptions,
|
||||
viewportLabel,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
@ -47,7 +41,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
const srDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||
const [measurementCount, setMeasurementCount] = useState(1);
|
||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
|
||||
@ -57,7 +50,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
referencedDisplaySetMetadata,
|
||||
setReferencedDisplaySetMetadata,
|
||||
] = useState(null);
|
||||
const [isHydrated, setIsHydrated] = useState(srDisplaySet.isHydrated);
|
||||
const [element, setElement] = useState(null);
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
|
||||
@ -91,10 +83,12 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
SeriesInstanceUIDs[0]
|
||||
);
|
||||
if (displaySets.length) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
||||
});
|
||||
viewportGridService.setDisplaySetsForViewports([
|
||||
{
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -215,9 +209,19 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// should be passed second since we don't want SR displaySet to
|
||||
// override the 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,
|
||||
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}
|
||||
initialImageIndex={initialImageIndex}
|
||||
@ -289,8 +293,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
srDisplaySet.load();
|
||||
}
|
||||
setIsHydrated(srDisplaySet.isHydrated);
|
||||
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
}, [srDisplaySet]);
|
||||
@ -378,6 +380,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
@ -398,17 +401,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
{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}
|
||||
</div>
|
||||
</>
|
||||
@ -420,8 +412,10 @@ OHIFCornerstoneSRViewport.propTypes = {
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
customProps: PropTypes.object,
|
||||
viewportOptions: PropTypes.object,
|
||||
viewportLabel: PropTypes.string,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.tsx",
|
||||
"types": "src/types/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./types": "./src/types/index.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
@ -29,9 +34,13 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"cornerstone-wado-image-loader": "^4.13.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"@cornerstonejs/dicom-image-loader": "^0.6.6",
|
||||
"@cornerstonejs/codec-charls": "^1.2.3",
|
||||
"@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",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
@ -44,10 +53,10 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^0.6.0",
|
||||
"@cornerstonejs/core": "^0.40.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.16.0",
|
||||
"@cornerstonejs/tools": "^0.60.1",
|
||||
"@kitware/vtk.js": "26.5.6",
|
||||
"@cornerstonejs/core": "^0.47.1",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.20.4",
|
||||
"@cornerstonejs/tools": "^0.67.4",
|
||||
"@kitware/vtk.js": "27.3.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
@ -8,22 +8,35 @@ import {
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
utilities as csUtils,
|
||||
CONSTANTS,
|
||||
} 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 './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
import {
|
||||
IStackViewport,
|
||||
IVolumeViewport,
|
||||
} from '@cornerstonejs/core/dist/esm/types';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
||||
import CornerstoneServices from '../types/CornerstoneServices';
|
||||
|
||||
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) {
|
||||
if (nextProps.needsRerendering) {
|
||||
return false;
|
||||
@ -129,7 +142,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
cornerstoneCacheService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services;
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
@ -390,8 +405,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
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 {
|
||||
lutPresentationStore,
|
||||
positionPresentationStore,
|
||||
@ -403,6 +419,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
lutPresentation:
|
||||
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(
|
||||
viewportIndex,
|
||||
@ -411,6 +434,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
displaySetOptions,
|
||||
presentations
|
||||
);
|
||||
|
||||
if (measurement) {
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
}
|
||||
};
|
||||
|
||||
loadViewportData();
|
||||
@ -453,50 +480,64 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}, [displaySets, elementRef, viewportIndex]);
|
||||
|
||||
return (
|
||||
<div className="viewport-wrapper">
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
skipOnMount={true} // Todo: make these configurable
|
||||
refreshMode={'debounce'}
|
||||
refreshRate={200} // transition amount in side panel
|
||||
onResize={onResize}
|
||||
targetRef={elementRef.current}
|
||||
/>
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
ref={elementRef}
|
||||
></div>
|
||||
<CornerstoneOverlays
|
||||
viewportIndex={viewportIndex}
|
||||
toolbarService={toolbarService}
|
||||
element={elementRef.current}
|
||||
scrollbarHeight={scrollbarHeight}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{isCineEnabled && (
|
||||
<CinePlayer
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-3"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
skipOnMount={true} // Todo: make these configurable
|
||||
refreshMode={'debounce'}
|
||||
refreshRate={200} // transition amount in side panel
|
||||
onResize={onResize}
|
||||
targetRef={elementRef.current}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
ref={elementRef}
|
||||
></div>
|
||||
<CornerstoneOverlays
|
||||
viewportIndex={viewportIndex}
|
||||
toolBarService={toolbarService}
|
||||
element={elementRef.current}
|
||||
scrollbarHeight={scrollbarHeight}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{isCineEnabled && (
|
||||
<CinePlayer
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-3"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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);
|
||||
|
||||
@ -513,22 +554,31 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
);
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
measurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
||||
({ measurement }) => {
|
||||
if (!measurement) return;
|
||||
|
||||
// Jump the the measurement if the viewport contains the displaySetUID (fusion)
|
||||
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||
props => {
|
||||
cacheJumpToMeasurementEvent = props;
|
||||
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
|
||||
if (!measurement || isConsumed) return;
|
||||
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(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@ -545,21 +595,20 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
if (!cacheJumpToMeasurementEvent) return;
|
||||
if (cacheJumpToMeasurementEvent.isConsumed) {
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
return;
|
||||
}
|
||||
const displaysUIDs = displaySets.map(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
);
|
||||
if (!displaysUIDs?.length) return;
|
||||
|
||||
const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (measurementIdToJumpTo && elementRef) {
|
||||
// Jump to measurement if the measurement exists
|
||||
const measurement = measurementService.getMeasurement(
|
||||
measurementIdToJumpTo
|
||||
);
|
||||
|
||||
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
||||
// Jump to measurement if the measurement exists
|
||||
const { measurement } = cacheJumpToMeasurementEvent;
|
||||
if (measurement && elementRef) {
|
||||
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
@ -587,6 +636,7 @@ function _jumpToMeasurement(
|
||||
|
||||
if (!SOPInstanceUID) {
|
||||
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
@ -599,15 +649,17 @@ function _jumpToMeasurement(
|
||||
|
||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
|
||||
const enableElement = getEnabledElement(targetElement);
|
||||
const enabledElement = getEnabledElement(targetElement);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (enableElement) {
|
||||
if (enabledElement) {
|
||||
// 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 viewportCameraDirectionMatch = true;
|
||||
@ -627,19 +679,19 @@ function _jumpToMeasurement(
|
||||
} else {
|
||||
// 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.
|
||||
const { viewPlaneNormal } = measurement.metadata;
|
||||
const { viewPlaneNormal: measurementViewPlane } = measurement.metadata;
|
||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
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 (
|
||||
orientation &&
|
||||
viewPlaneNormal &&
|
||||
measurementViewPlane &&
|
||||
!csUtils.isEqual(
|
||||
CONSTANTS.MPR_CAMERA_VALUES[orientation]?.viewPlaneNormal,
|
||||
viewPlaneNormal
|
||||
measurementViewPlane.map(Math.abs),
|
||||
viewportViewPlane.map(Math.abs)
|
||||
)
|
||||
) {
|
||||
viewportCameraDirectionMatch = false;
|
||||
@ -656,7 +708,8 @@ function _jumpToMeasurement(
|
||||
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
// Jump to measurement consumed, remove.
|
||||
measurementService.removeJumpToMeasurement(viewportIndex);
|
||||
cacheJumpToMeasurementEvent?.consume?.();
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,16 +10,19 @@ import {
|
||||
utilities as cstUtils,
|
||||
ReferenceLinesTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { Types as OhifTypes } from '@ohif/core';
|
||||
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { CornerstoneServices } from './types';
|
||||
|
||||
function commandsModule({ servicesManager, commandsManager }) {
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
||||
const {
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
@ -28,51 +31,14 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
uiDialogService,
|
||||
cornerstoneViewportService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
const { measurementServiceSource } = this;
|
||||
|
||||
function _getActiveViewportEnabledElement() {
|
||||
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 = {
|
||||
/**
|
||||
* Generates the selector props for the context menu, specific to
|
||||
@ -127,6 +93,36 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
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:
|
||||
|
||||
@ -298,7 +294,7 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = _getToolGroup(null);
|
||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||
|
||||
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
||||
uiNotificationService.show({
|
||||
@ -317,7 +313,7 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
viewports: [],
|
||||
};
|
||||
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
||||
|
||||
// if toolGroup has been destroyed, or its viewports have been removed
|
||||
@ -372,6 +368,21 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
},
|
||||
showDownloadViewportModal: () => {
|
||||
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;
|
||||
|
||||
if (uiModalService) {
|
||||
@ -491,33 +502,22 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
}
|
||||
}
|
||||
},
|
||||
firstImage: () => {
|
||||
// Get current active viewport (return if none active)
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
if (!enabledElement) {
|
||||
return;
|
||||
}
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
// Check viewport is supported
|
||||
if (
|
||||
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: () => {
|
||||
/** 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)
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
if (!enabledElement) {
|
||||
return;
|
||||
let viewport;
|
||||
if (!gridViewport) {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
if (!enabledElement) {
|
||||
return;
|
||||
}
|
||||
viewport = enabledElement.viewport;
|
||||
} else {
|
||||
viewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||
gridViewport.id
|
||||
);
|
||||
}
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
// Get number of slices
|
||||
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
|
||||
@ -532,8 +532,14 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
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
|
||||
const options = { imageIndex: numberOfSlices - 1 };
|
||||
const options = { imageIndex: jumpIndex };
|
||||
cstUtils.jumpToSlice(viewport.element, options);
|
||||
},
|
||||
scroll: ({ direction }) => {
|
||||
@ -564,9 +570,9 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
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) {
|
||||
viewport.render();
|
||||
@ -632,148 +638,109 @@ function commandsModule({ servicesManager, commandsManager }) {
|
||||
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
},
|
||||
getNearbyAnnotation: {
|
||||
commandFn: actions.getNearbyAnnotation,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
deleteMeasurement: {
|
||||
commandFn: actions.deleteMeasurement,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setMeasurementLabel: {
|
||||
commandFn: actions.setMeasurementLabel,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
updateMeasurement: {
|
||||
commandFn: actions.updateMeasurement,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toolbarServiceRecordInteraction: {
|
||||
commandFn: actions.toolbarServiceRecordInteraction,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: [],
|
||||
options: { rotation: 90 },
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: [],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
incrementActiveViewport: {
|
||||
commandFn: actions.incrementActiveViewport,
|
||||
storeContexts: [],
|
||||
},
|
||||
decrementActiveViewport: {
|
||||
commandFn: actions.decrementActiveViewport,
|
||||
storeContexts: [],
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
flipViewportVertical: {
|
||||
commandFn: actions.flipViewportVertical,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
scaleUpViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
scaleDownViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
fitViewportToWindow: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: [],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.scroll,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.scroll,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
firstImage: {
|
||||
commandFn: actions.firstImage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
commandFn: actions.jumpToImage,
|
||||
options: { imageIndex: 0 },
|
||||
},
|
||||
lastImage: {
|
||||
commandFn: actions.lastImage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
commandFn: actions.jumpToImage,
|
||||
options: { imageIndex: -1 },
|
||||
},
|
||||
jumpToImage: {
|
||||
commandFn: actions.jumpToImage,
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleCine: {
|
||||
commandFn: actions.toggleCine,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
arrowTextCallback: {
|
||||
commandFn: actions.arrowTextCallback,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setViewportActive: {
|
||||
commandFn: actions.setViewportActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setViewportColormap: {
|
||||
commandFn: actions.setViewportColormap,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleStackImageSync: {
|
||||
commandFn: actions.toggleStackImageSync,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
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';
|
||||
|
||||
const mpr: Types.HangingProtocol.Protocol = {
|
||||
id: 'mpr',
|
||||
name: 'Multi-Planar Reconstruction',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'mpr',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2023-02-17',
|
||||
createdDate: '2021-02-23',
|
||||
modifiedDate: '2023-04-03',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
// Unknown number of priors referenced - so just match any study
|
||||
@ -244,7 +245,7 @@ const mprAnd3DVolumeViewport = {
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
options: {
|
||||
presetName: 'CT-Bone',
|
||||
displayPreset: 'CT-Bone',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -303,7 +304,7 @@ const mprAnd3DVolumeViewport = {
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: 'mpr',
|
||||
name: mpr.id,
|
||||
protocol: mpr,
|
||||
},
|
||||
{
|
||||
|
||||
@ -7,26 +7,29 @@ import {
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { Types } from '@ohif/core';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
|
||||
import init from './init';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
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';
|
||||
import * as CornerstoneExtensionTypes from './types';
|
||||
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import { getEnabledElement, reset as enabledElementReset } from './state';
|
||||
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||
import dicomLoaderService from './utils/dicomLoaderService';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
import { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -72,18 +75,11 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
props: Types.Extensions.ExtensionParams
|
||||
): Promise<void> {
|
||||
const { servicesManager } = props;
|
||||
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
||||
servicesManager.registerService(
|
||||
CornerstoneViewportService(servicesManager)
|
||||
);
|
||||
servicesManager.registerService(
|
||||
ToolGroupService.REGISTRATION(servicesManager)
|
||||
);
|
||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||
servicesManager.registerService(SegmentationService(servicesManager));
|
||||
servicesManager.registerService(
|
||||
CornerstoneCacheService.REGISTRATION(servicesManager)
|
||||
);
|
||||
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
||||
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
||||
servicesManager.registerService(SyncGroupService.REGISTRATION);
|
||||
servicesManager.registerService(SegmentationService.REGISTRATION);
|
||||
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||
|
||||
return init.call(this, props);
|
||||
},
|
||||
@ -99,7 +95,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
ToolbarService={toolbarService}
|
||||
toolbarService={toolbarService}
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
@ -114,6 +110,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
];
|
||||
},
|
||||
getCommandsModule,
|
||||
getCustomizationModule,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
@ -124,7 +121,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
getEnabledElement,
|
||||
dicomLoaderService,
|
||||
registerColormap,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -145,5 +141,5 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
};
|
||||
|
||||
export type { PublicViewportOptions };
|
||||
export { measurementMappingUtils };
|
||||
export { measurementMappingUtils, CornerstoneExtensionTypes, toolNames , getActiveViewportEnabledElement};
|
||||
export default cornerstoneExtension;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import OHIF, { Types } from '@ohif/core';
|
||||
import React from 'react';
|
||||
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
@ -36,6 +36,7 @@ window.cornerstoneTools = cornerstoneTools;
|
||||
export default async function init({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
appConfig,
|
||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
@ -83,6 +84,8 @@ export default async function init({
|
||||
} = servicesManager.services;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
window.commandsManager = commandsManager;
|
||||
|
||||
if (
|
||||
appConfig.showWarningMessageForCrossOrigin &&
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
LengthTool,
|
||||
RectangleROITool,
|
||||
EllipticalROITool,
|
||||
CircleROITool,
|
||||
BidirectionalTool,
|
||||
ArrowAnnotateTool,
|
||||
DragProbeTool,
|
||||
@ -29,6 +30,9 @@ import {
|
||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||
|
||||
export default function initCornerstoneTools(configuration = {}) {
|
||||
CrosshairsTool.isAnnotation = false;
|
||||
ReferenceLinesTool.isAnnotation = false;
|
||||
|
||||
init(configuration);
|
||||
addTool(PanTool);
|
||||
addTool(WindowLevelTool);
|
||||
@ -41,6 +45,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(LengthTool);
|
||||
addTool(RectangleROITool);
|
||||
addTool(EllipticalROITool);
|
||||
addTool(CircleROITool);
|
||||
addTool(BidirectionalTool);
|
||||
addTool(ArrowAnnotateTool);
|
||||
addTool(DragProbeTool);
|
||||
@ -83,6 +88,7 @@ const toolNames = {
|
||||
Probe: ProbeTool.toolName,
|
||||
RectangleROI: RectangleROITool.toolName,
|
||||
EllipticalROI: EllipticalROITool.toolName,
|
||||
CircleROI: CircleROITool.toolName,
|
||||
Bidirectional: BidirectionalTool.toolName,
|
||||
Angle: AngleTool.toolName,
|
||||
CobbAngle: CobbAngleTool.toolName,
|
||||
|
||||
@ -24,6 +24,7 @@ const initMeasurementService = (
|
||||
Length,
|
||||
Bidirectional,
|
||||
EllipticalROI,
|
||||
CircleROI,
|
||||
ArrowAnnotate,
|
||||
Angle,
|
||||
CobbAngle,
|
||||
@ -64,6 +65,14 @@ const initMeasurementService = (
|
||||
EllipticalROI.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'CircleROI',
|
||||
CircleROI.matchingCriteria,
|
||||
CircleROI.toAnnotation,
|
||||
CircleROI.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'ArrowAnnotate',
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import { volumeLoader } from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import cornerstoneWADOImageLoader, {
|
||||
import dicomImageLoader, {
|
||||
webWorkerManager,
|
||||
} from 'cornerstone-wado-image-loader';
|
||||
} from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { errorHandler } from '@ohif/core';
|
||||
|
||||
@ -28,7 +28,7 @@ function initWebWorkers(appConfig) {
|
||||
};
|
||||
|
||||
if (!initialized) {
|
||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||
dicomImageLoader.webWorkerManager.initialize(config);
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
@ -37,18 +37,18 @@ export default function initWADOImageLoader(
|
||||
userAuthenticationService,
|
||||
appConfig
|
||||
) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||
dicomImageLoader.external.cornerstone = cornerstone;
|
||||
dicomImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
registerVolumeLoader(
|
||||
'cornerstoneStreamingImageVolume',
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
|
||||
cornerstoneWADOImageLoader.configure({
|
||||
dicomImageLoader.configure({
|
||||
decodeConfig: {
|
||||
// !! 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).
|
||||
// Until the default is set to true (which is the case for cornerstone3D),
|
||||
// we should set this flag to false.
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import {
|
||||
cache as cs3DCache,
|
||||
Enums,
|
||||
volumeLoader,
|
||||
utilities as utils,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import {
|
||||
@ -10,20 +15,21 @@ import {
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
|
||||
class CornerstoneCacheService {
|
||||
static REGISTRATION = (serviceManager: ServicesManager) => {
|
||||
return {
|
||||
name: 'cornerstoneCacheService',
|
||||
altName: 'CornerstoneCacheService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new CornerstoneCacheService(serviceManager);
|
||||
},
|
||||
};
|
||||
static REGISTRATION = {
|
||||
name: 'cornerstoneCacheService',
|
||||
altName: 'CornerstoneCacheService',
|
||||
create: ({
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
|
||||
return new CornerstoneCacheService(servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
stackImageIds: Map<string, string[]> = new Map();
|
||||
volumeImageIds: Map<string, string[]> = new Map();
|
||||
readonly servicesManager: ServicesManager;
|
||||
|
||||
constructor(servicesManager) {
|
||||
constructor(servicesManager: ServicesManager) {
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
@ -90,7 +96,10 @@ class CornerstoneCacheService {
|
||||
displaySetService
|
||||
) {
|
||||
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
|
||||
@ -100,6 +109,7 @@ class CornerstoneCacheService {
|
||||
|
||||
if (volume) {
|
||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||
this.volumeImageIds.delete(volumeId);
|
||||
}
|
||||
|
||||
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
|
||||
@ -133,13 +143,18 @@ class CornerstoneCacheService {
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
StudyInstanceUID,
|
||||
isCompositeStack,
|
||||
} = displaySet;
|
||||
|
||||
const StackViewportData: StackViewportData = {
|
||||
viewportType,
|
||||
data: {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
isCompositeStack,
|
||||
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;
|
||||
// if segmentation contains any data (often calculated from labelmap)
|
||||
cachedStats: Record<string, any>;
|
||||
// displaySetInstanceUID
|
||||
displaySetInstanceUID: string;
|
||||
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
|
||||
displayText?: string[];
|
||||
// the id of the segmentation
|
||||
@ -45,44 +47,22 @@ type Segmentation = {
|
||||
segments: Array<Segment>;
|
||||
// the set of segments that are locked
|
||||
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
|
||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
||||
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.)
|
||||
type: csToolsEnums.SegmentationRepresentations;
|
||||
// the volume id of the volume that the labelmap is associated with, this only exists for the labelmap representation
|
||||
volumeId: string;
|
||||
// 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>;
|
||||
// the segmentation representation data
|
||||
representationData: SegmentationRepresentationData;
|
||||
};
|
||||
|
||||
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';
|
||||
|
||||
export default function ExtendedSegmentationService(servicesManager) {
|
||||
return {
|
||||
name: 'segmentationService',
|
||||
altName: 'SegmentationService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new SegmentationService({ servicesManager });
|
||||
},
|
||||
};
|
||||
}
|
||||
export default SegmentationService;
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
Synchronizer,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
import { pubSubServiceInterface, Types, ServicesManager } from '@ohif/core';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||
@ -37,7 +37,17 @@ const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||
|
||||
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 } = {};
|
||||
EVENTS: { [key: string]: string };
|
||||
synchronizerCreators: Record<string, SyncCreator> = {
|
||||
@ -47,8 +57,8 @@ export default class SyncGroupService {
|
||||
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
|
||||
};
|
||||
|
||||
constructor(serviceManager) {
|
||||
this.serviceManager = serviceManager;
|
||||
constructor(serviceManager: ServicesManager) {
|
||||
this.servicesManager = serviceManager;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
//
|
||||
|
||||
@ -1,11 +1,3 @@
|
||||
import SyncGroupService from './SyncGroupService';
|
||||
|
||||
export default function ExtendedSyncGroupService(serviceManager) {
|
||||
return {
|
||||
altName: 'SyncGroupService',
|
||||
name: 'syncGroupService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new SyncGroupService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
export default SyncGroupService;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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 = {
|
||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||
@ -20,14 +21,14 @@ type Tools = {
|
||||
};
|
||||
|
||||
export default class ToolGroupService {
|
||||
public static REGISTRATION = serviceManager => {
|
||||
return {
|
||||
name: 'toolGroupService',
|
||||
altName: 'ToolGroupService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new ToolGroupService(serviceManager);
|
||||
},
|
||||
};
|
||||
public static REGISTRATION = {
|
||||
name: 'toolGroupService',
|
||||
altName: 'ToolGroupService',
|
||||
create: ({
|
||||
servicesManager,
|
||||
}: OhifTypes.Extensions.ExtensionParams): ToolGroupService => {
|
||||
return new ToolGroupService(servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
serviceManager: any;
|
||||
@ -39,20 +40,60 @@ export default class ToolGroupService {
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor(serviceManager) {
|
||||
const { cornerstoneViewportService } = serviceManager.services;
|
||||
const {
|
||||
cornerstoneViewportService,
|
||||
viewportGridService,
|
||||
} = serviceManager.services;
|
||||
this.cornerstoneViewportService = cornerstoneViewportService;
|
||||
this.viewportGridService = viewportGridService;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
onModeExit() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cornerstone ToolGroup for a given toolGroup UID
|
||||
* @param {string} toolGroupId - The toolGroup uid
|
||||
* @returns {IToolGroup} - The toolGroup
|
||||
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
||||
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
||||
* @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 {
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
public getToolGroup(toolGroupId?: string): Types.IToolGroup | void {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { PubSubService } from '@ohif/core';
|
||||
import { PubSubService, ServicesManager } from '@ohif/core';
|
||||
import * as OhifTypes from '@ohif/core/types';
|
||||
import {
|
||||
RenderingEngine,
|
||||
StackViewport,
|
||||
@ -10,9 +11,13 @@ import {
|
||||
cache,
|
||||
utilities,
|
||||
CONSTANTS,
|
||||
Enums as csEnums,
|
||||
} 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 { RENDERING_ENGINE_ID } from './constants';
|
||||
import ViewportInfo, {
|
||||
@ -25,10 +30,6 @@ import {
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
import { Presentation, Presentations } from '../../types/Presentation';
|
||||
import {
|
||||
setColormap,
|
||||
setLowerUpperColorTransferFunction,
|
||||
} from '../../utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
|
||||
@ -43,6 +44,16 @@ const EVENTS = {
|
||||
*/
|
||||
class CornerstoneViewportService extends PubSubService
|
||||
implements IViewportService {
|
||||
static REGISTRATION = {
|
||||
name: 'cornerstoneViewportService',
|
||||
altName: 'CornerstoneViewportService',
|
||||
create: ({
|
||||
servicesManager,
|
||||
}: OhifTypes.Extensions.ExtensionParams): CornerstoneViewportService => {
|
||||
return new CornerstoneViewportService(servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
renderingEngine: Types.IRenderingEngine | null;
|
||||
viewportsInfo: Map<number, ViewportInfo> = new Map();
|
||||
viewportsById: Map<string, ViewportInfo> = new Map();
|
||||
@ -55,7 +66,7 @@ class CornerstoneViewportService extends PubSubService
|
||||
resizeRefreshMode: 'debounce';
|
||||
servicesManager = null;
|
||||
|
||||
constructor(servicesManager) {
|
||||
constructor(servicesManager: ServicesManager) {
|
||||
super(EVENTS);
|
||||
this.renderingEngine = null;
|
||||
this.viewportGridResizeObserver = null;
|
||||
@ -71,7 +82,7 @@ class CornerstoneViewportService extends PubSubService
|
||||
viewportIndex: number,
|
||||
viewportOptions: PublicViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
) {
|
||||
): void {
|
||||
// Use the provided viewportId
|
||||
// Not providing a viewportId is frowned upon because it does weird things
|
||||
// on moving them around, but it does mostly work.
|
||||
@ -375,7 +386,7 @@ class CornerstoneViewportService extends PubSubService
|
||||
initialImageIndexToUse === null
|
||||
) {
|
||||
initialImageIndexToUse =
|
||||
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
||||
this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
|
||||
}
|
||||
|
||||
const properties = { ...presentations.lutPresentation?.properties };
|
||||
@ -401,7 +412,7 @@ class CornerstoneViewportService extends PubSubService
|
||||
});
|
||||
}
|
||||
|
||||
private _getInitialImageIndexForStackViewport(
|
||||
private _getInitialImageIndexForViewport(
|
||||
viewportInfo: ViewportInfo,
|
||||
imageIds?: string[]
|
||||
): number {
|
||||
@ -412,7 +423,29 @@ class CornerstoneViewportService extends PubSubService
|
||||
}
|
||||
|
||||
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(
|
||||
@ -488,16 +521,9 @@ class CornerstoneViewportService extends PubSubService
|
||||
const displaySetOptions = displaySetOptionsArray[index];
|
||||
const { volumeId } = volume;
|
||||
|
||||
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
||||
|
||||
const callback = ({ volumeActor }) => {
|
||||
voiCallbacks.forEach(callback => callback(volumeActor));
|
||||
};
|
||||
|
||||
volumeInputArray.push({
|
||||
imageIds,
|
||||
volumeId,
|
||||
callback,
|
||||
blendMode: displaySetOptions.blendMode,
|
||||
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
||||
});
|
||||
@ -537,120 +563,77 @@ class CornerstoneViewportService extends PubSubService
|
||||
) {
|
||||
const {
|
||||
displaySetService,
|
||||
segmentationService,
|
||||
toolGroupService,
|
||||
} = 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);
|
||||
volumesProperties.forEach(({ properties, volumeId }) => {
|
||||
viewport.setProperties(properties, volumeId);
|
||||
});
|
||||
|
||||
this.setPresentations(viewport, presentations);
|
||||
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
|
||||
const segDisplaySet = displaySetInstanceUIDs
|
||||
// can be SEG or RTSTRUCT for now
|
||||
const overlayDisplaySet = displaySetInstanceUIDs
|
||||
.map(displaySetService.getDisplaySetByUID)
|
||||
.find(displaySet => displaySet && displaySet.Modality === 'SEG');
|
||||
.find(displaySet => displaySet?.isOverlayDisplaySet);
|
||||
|
||||
if (segDisplaySet) {
|
||||
const { referencedVolumeId } = segDisplaySet;
|
||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
|
||||
if (referencedVolume) {
|
||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentationId
|
||||
);
|
||||
}
|
||||
if (overlayDisplaySet) {
|
||||
this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport);
|
||||
} else {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
const toolGroupSegmentationRepresentations =
|
||||
segmentationService.getSegmentationRepresentationsForToolGroup(
|
||||
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
|
||||
// for this toolGroup, don't bother at all
|
||||
if (
|
||||
toolGroupSegmentationRepresentations.find(
|
||||
representation => representation.segmentationId === segmentation.id
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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)
|
||||
const { id: segDisplaySetInstanceUID } = segmentation;
|
||||
|
||||
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
segDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
let shouldDisplaySeg = false;
|
||||
|
||||
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
|
||||
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
|
||||
shouldDisplaySeg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(
|
||||
viewport.id
|
||||
);
|
||||
|
||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentation.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
|
||||
if (!viewportInfo) {
|
||||
console.warn('Viewport info not defined for', viewport.id);
|
||||
this._addSegmentationRepresentationToToolGroupIfNecessary(
|
||||
displaySetInstanceUIDs,
|
||||
viewport
|
||||
);
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||
|
||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||
|
||||
if (
|
||||
initialImageOptions &&
|
||||
(initialImageOptions.preset !== undefined ||
|
||||
initialImageOptions.index !== undefined)
|
||||
) {
|
||||
const { index, preset } = initialImageOptions;
|
||||
|
||||
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
||||
viewport
|
||||
);
|
||||
|
||||
const imageIndex = this._getInitialImageIndex(
|
||||
numberOfSlices,
|
||||
index,
|
||||
preset
|
||||
);
|
||||
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
|
||||
|
||||
if (imageIndex !== undefined) {
|
||||
csToolsUtils.jumpToSlice(viewport.element, {
|
||||
imageIndex,
|
||||
});
|
||||
@ -659,6 +642,96 @@ class CornerstoneViewportService extends PubSubService
|
||||
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 =
|
||||
segmentationService.getSegmentationRepresentationsForToolGroup(
|
||||
toolGroup.id
|
||||
) || [];
|
||||
|
||||
// if there is already a segmentation representation for this segmentation
|
||||
// for this toolGroup, don't bother at all
|
||||
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
|
||||
representation => representation.segmentationId === segmentation.id
|
||||
);
|
||||
|
||||
if (isSegmentationInToolGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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)
|
||||
const { id: segDisplaySetInstanceUID, type } = segmentation;
|
||||
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
segDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
let shouldDisplaySeg = false;
|
||||
|
||||
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
|
||||
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
|
||||
shouldDisplaySeg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldDisplaySeg) {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentation.id,
|
||||
false, // already hydrated,
|
||||
segmentation.type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private addOverlayRepresentationForDisplaySet(
|
||||
displaySet: any,
|
||||
viewport: any
|
||||
) {
|
||||
const {
|
||||
segmentationService,
|
||||
toolGroupService,
|
||||
} = this.servicesManager.services;
|
||||
|
||||
const { referencedVolumeId } = displaySet;
|
||||
const segmentationId = displaySet.displaySetInstanceUID;
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
|
||||
|
||||
const representationType =
|
||||
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
|
||||
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||
|
||||
segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentationId,
|
||||
false,
|
||||
representationType
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: keepCamera is an interim solution until we have a better solution for
|
||||
// keeping the camera position when the viewport data is changed
|
||||
public updateViewport(
|
||||
@ -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(
|
||||
viewport: StackViewport | VolumeViewport,
|
||||
viewportData: StackViewportData | VolumeViewportData,
|
||||
@ -848,21 +877,44 @@ class CornerstoneViewportService extends PubSubService
|
||||
return instance.FrameOfReferenceUID;
|
||||
}
|
||||
|
||||
if (displaySet.Modality === 'RTSTRUCT') {
|
||||
const { instance } = displaySet;
|
||||
return instance.ReferencedFrameOfReferenceSequence.FrameOfReferenceUID;
|
||||
}
|
||||
|
||||
const { images } = displaySet;
|
||||
if (images && images.length) {
|
||||
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) {
|
||||
return {
|
||||
name: 'cornerstoneViewportService',
|
||||
altName: 'CornerstoneViewportService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new CornerstoneViewportService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { CornerstoneViewportService, CornerstoneViewportServiceRegistration };
|
||||
export default CornerstoneViewportService;
|
||||
|
||||
@ -63,7 +63,7 @@ export type PublicDisplaySetOptions = {
|
||||
blendMode?: string;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
presetName?: string;
|
||||
displayPreset?: string;
|
||||
};
|
||||
|
||||
export type DisplaySetOptions = {
|
||||
@ -73,7 +73,7 @@ export type DisplaySetOptions = {
|
||||
blendMode?: Enums.BlendModes;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
presetName?: string;
|
||||
displayPreset?: string;
|
||||
};
|
||||
|
||||
type VOI = {
|
||||
@ -88,6 +88,20 @@ export type DisplaySet = {
|
||||
const STACK = 'stack';
|
||||
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 {
|
||||
private viewportId = '';
|
||||
private viewportIndex: number;
|
||||
@ -104,6 +118,21 @@ class ViewportInfo {
|
||||
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 => {
|
||||
this.element = null;
|
||||
this.viewportData = null;
|
||||
@ -203,6 +232,10 @@ class ViewportInfo {
|
||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||
}
|
||||
|
||||
if (!toolGroupId) {
|
||||
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||
}
|
||||
|
||||
this.setViewportOptions({
|
||||
...viewportOptionsEntry,
|
||||
viewportId: this.viewportId,
|
||||
@ -228,7 +261,8 @@ class ViewportInfo {
|
||||
}
|
||||
|
||||
public getSyncGroups(): SyncGroup[] {
|
||||
return this.viewportOptions.syncGroups || [];
|
||||
this.viewportOptions.syncGroups ||= [];
|
||||
return this.viewportOptions.syncGroups;
|
||||
}
|
||||
|
||||
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
||||
@ -281,7 +315,7 @@ class ViewportInfo {
|
||||
colormap: option.colormap,
|
||||
slabThickness: option.slabThickness,
|
||||
blendMode,
|
||||
presetName: option.presetName,
|
||||
displayPreset: option.displayPreset,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import {
|
||||
Enums,
|
||||
Types,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import { Enums, Types } from '@cornerstonejs/core';
|
||||
|
||||
type StackData = {
|
||||
StudyInstanceUID: 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[];
|
||||
frameRate?: number;
|
||||
isClip?: boolean;
|
||||
@ -20,19 +20,14 @@ type VolumeData = {
|
||||
imageIds?: string[];
|
||||
};
|
||||
|
||||
type StackViewportData = {
|
||||
type StackViewportData = {
|
||||
viewportType: Enums.ViewportType;
|
||||
data: StackData;
|
||||
};
|
||||
|
||||
type VolumeViewportData = {
|
||||
type VolumeViewportData = {
|
||||
viewportType: Enums.ViewportType;
|
||||
data: VolumeData[];
|
||||
};
|
||||
|
||||
export type {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
StackData,
|
||||
VolumeData,
|
||||
};
|
||||
export type { 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 CornerstoneServices from './CornerstoneServices';
|
||||
|
||||
export type {
|
||||
CornerstoneCacheService
|
||||
CornerstoneCacheService,
|
||||
CornerstoneServices,
|
||||
}
|
||||
|
||||
@ -217,7 +217,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
);
|
||||
|
||||
// add the viewport to the toolGroup
|
||||
toolGroup.addViewport(downloadViewportId);
|
||||
toolGroup.addViewport(downloadViewportId, renderingEngineId);
|
||||
|
||||
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
||||
// 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 cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||
import { api } from 'dicomweb-client';
|
||||
import { DICOMWeb, errorHandler } from '@ohif/core';
|
||||
|
||||
@ -114,7 +114,7 @@ class DicomLoaderService {
|
||||
}
|
||||
|
||||
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;
|
||||
return commandsManager.runCommand(
|
||||
'getNearbyToolData',
|
||||
'getNearbyAnnotation',
|
||||
{
|
||||
element,
|
||||
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 getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { displaySetService, utils } from '@ohif/core';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const Length = {
|
||||
toAnnotation: measurement => {},
|
||||
@ -32,15 +32,12 @@ const Length = {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
cornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||
getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
cornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
@ -104,11 +101,8 @@ function getMappedAnnotations(annotation, displaySetService) {
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
@ -179,12 +173,8 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
const displayText = [];
|
||||
|
||||
// Area is the same for all series
|
||||
const {
|
||||
length,
|
||||
SeriesNumber,
|
||||
SOPInstanceUID,
|
||||
frameNumber,
|
||||
} = mappedAnnotations[0];
|
||||
const { length, SeriesNumber, SOPInstanceUID, frameNumber } =
|
||||
mappedAnnotations[0];
|
||||
|
||||
const instance = displaySet.images.find(
|
||||
image => image.SOPInstanceUID === SOPInstanceUID
|
||||
@ -198,6 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
||||
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||
|
||||
if (length === null || length === undefined) return displayText;
|
||||
const roundedLength = utils.roundNumber(length, 2);
|
||||
displayText.push(
|
||||
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||
@ -4,7 +4,7 @@ import getModalityUnit from './utils/getModalityUnit';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const RectangleROI = {
|
||||
toAnnotation: measurement => { },
|
||||
toAnnotation: measurement => {},
|
||||
toMeasurement: (
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
@ -26,15 +26,12 @@ const RectangleROI = {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } =
|
||||
getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
@ -99,11 +96,8 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
frameNumber,
|
||||
} = getSOPInstanceAttributes(referencedImageId);
|
||||
const { SOPInstanceUID, SeriesInstanceUID, frameNumber } =
|
||||
getSOPInstanceAttributes(referencedImageId);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
@ -143,7 +137,7 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||
|
||||
// Add Type
|
||||
columns.push('AnnotationType');
|
||||
values.push('Cornerstone:EllipticalROI');
|
||||
values.push('Cornerstone:RectangleROI');
|
||||
|
||||
mappedAnnotations.forEach(annotation => {
|
||||
const { mean, stdDev, max, area, unit } = annotation;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export default [
|
||||
'Length',
|
||||
'EllipticalROI',
|
||||
'CircleROI',
|
||||
'Bidirectional',
|
||||
'ArrowAnnotate',
|
||||
'Angle',
|
||||
|
||||
@ -2,6 +2,7 @@ import { MeasurementService } from '@ohif/core';
|
||||
import Length from './Length';
|
||||
import Bidirectional from './Bidirectional';
|
||||
import EllipticalROI from './EllipticalROI';
|
||||
import CircleROI from './CircleROI';
|
||||
import ArrowAnnotate from './ArrowAnnotate';
|
||||
import CobbAngle from './CobbAngle';
|
||||
import Angle from './Angle';
|
||||
@ -25,6 +26,7 @@ const measurementServiceMappingsFactory = (
|
||||
const {
|
||||
POLYLINE,
|
||||
ELLIPSE,
|
||||
CIRCLE,
|
||||
RECTANGLE,
|
||||
BIDIRECTIONAL,
|
||||
POINT,
|
||||
@ -37,6 +39,7 @@ const measurementServiceMappingsFactory = (
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalROI: ELLIPSE,
|
||||
CircleROI: CIRCLE,
|
||||
RectangleROI: RECTANGLE,
|
||||
PlanarFreehandROI: POLYLINE,
|
||||
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: {
|
||||
toAnnotation: RectangleROI.toAnnotation,
|
||||
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
|
||||
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
|
||||
* If the imageId is not provided then it uses the sceneUID to get the viewports
|
||||
* 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.
|
||||
* If the imageId is not provided then undefined is returned.
|
||||
* @param {string} imageId The image id of the referenced image
|
||||
* @param {string} sceneUID The scene UID of the measurement tool
|
||||
* @returns
|
||||
*/
|
||||
export default function getSOPInstanceAttributes(
|
||||
imageId,
|
||||
cornerstoneViewportService,
|
||||
viewportId
|
||||
) {
|
||||
export default function getSOPInstanceAttributes(imageId) {
|
||||
if (imageId) {
|
||||
return _getUIDFromImageID(imageId);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -48,23 +25,3 @@ function _getUIDFromImageID(imageId) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"dcmjs": "^0.29.4",
|
||||
"dicomweb-client": "^0.8.4",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicomweb-client": "^0.10.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^10.11.0",
|
||||
"react-i18next": "^12.2.2",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import ContextMenuController from './ContextMenuController';
|
||||
import ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import defaultContextMenu from './defaultContextMenu';
|
||||
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 dcmjs from 'dcmjs';
|
||||
|
||||
@ -75,7 +75,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
study.series.forEach(aSeries => {
|
||||
numInstances += aSeries.instances.length;
|
||||
modalities.add(aSeries.Modality);
|
||||
modalities.add(aSeries.instances[0].Modality);
|
||||
});
|
||||
|
||||
// first instance in the first series
|
||||
@ -87,7 +87,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
date: firstInstance.StudyDate,
|
||||
description: firstInstance.StudyDescription,
|
||||
mrn: firstInstance.PatientID,
|
||||
patientName: { Alphabetic: firstInstance.PatientName },
|
||||
patientName: utils.formatPN(firstInstance.PatientName),
|
||||
studyInstanceUid: firstInstance.StudyInstanceUID,
|
||||
time: firstInstance.StudyTime,
|
||||
//
|
||||
@ -103,9 +103,20 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
},
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: () => {
|
||||
console.debug(' DICOMLocal QUERY SERIES SEARCH');
|
||||
search: studyInstanceUID => {
|
||||
const study = DicomMetadataStore.getStudy(studyInstanceUID);
|
||||
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: {
|
||||
@ -115,6 +126,18 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
},
|
||||
},
|
||||
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: {
|
||||
metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
|
||||
if (!StudyInstanceUID) {
|
||||
@ -188,6 +211,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
displaySet.images.forEach(instance => {
|
||||
const NumberOfFrames = instance.NumberOfFrames;
|
||||
if (NumberOfFrames > 1) {
|
||||
// in multiframe we start at frame 1
|
||||
for (let i = 1; i <= NumberOfFrames; i++) {
|
||||
const imageId = this.getImageIdsForInstance({
|
||||
instance,
|
||||
|
||||
@ -22,8 +22,8 @@ import {
|
||||
retrieveStudyMetadata,
|
||||
deleteStudyMetadataPromise,
|
||||
} from './retrieveStudyMetadata.js';
|
||||
import StaticWadoClient from './utils/StaticWadoClient.js';
|
||||
import getDirectURL from '../utils/getDirectURL.js';
|
||||
import StaticWadoClient from './utils/StaticWadoClient';
|
||||
import getDirectURL from '../utils/getDirectURL';
|
||||
|
||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||
|
||||
@ -61,6 +61,8 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
singlepart,
|
||||
} = dicomWebConfig;
|
||||
|
||||
const dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig));
|
||||
|
||||
const qidoConfig = {
|
||||
url: qidoRoot,
|
||||
staticWado,
|
||||
@ -90,7 +92,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
const implementation = {
|
||||
initialize: ({ params, query }) => {
|
||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
||||
const queryStudyInstanceUIDs = utils.splitComma(
|
||||
query.getAll('StudyInstanceUIDs')
|
||||
);
|
||||
|
||||
const StudyInstanceUIDs =
|
||||
(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
|
||||
*/
|
||||
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: {
|
||||
metadata: async ({
|
||||
@ -217,36 +232,47 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
},
|
||||
|
||||
store: {
|
||||
dicom: async dataset => {
|
||||
dicom: async (dataset, request) => {
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
wadoDicomWebClient.headers = headers;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
FileMetaInformationVersion:
|
||||
dataset._meta.FileMetaInformationVersion.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||
ImplementationClassUID,
|
||||
ImplementationVersionName,
|
||||
};
|
||||
if (dataset instanceof ArrayBuffer) {
|
||||
const options = {
|
||||
datasets: [dataset],
|
||||
request,
|
||||
};
|
||||
|
||||
const denaturalized = denaturalizeDataset(meta);
|
||||
const dicomDict = new DicomDict(denaturalized);
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
} else {
|
||||
const meta = {
|
||||
FileMetaInformationVersion:
|
||||
dataset._meta.FileMetaInformationVersion.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||
ImplementationClassUID,
|
||||
ImplementationVersionName,
|
||||
};
|
||||
|
||||
dicomDict.dict = denaturalizeDataset(dataset);
|
||||
const denaturalized = denaturalizeDataset(meta);
|
||||
const dicomDict = new DicomDict(denaturalized);
|
||||
|
||||
const part10Buffer = dicomDict.write();
|
||||
dicomDict.dict = denaturalizeDataset(dataset);
|
||||
|
||||
const options = {
|
||||
datasets: [part10Buffer],
|
||||
};
|
||||
const part10Buffer = dicomDict.write();
|
||||
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
const options = {
|
||||
datasets: [part10Buffer],
|
||||
request,
|
||||
};
|
||||
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataSync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
@ -368,7 +394,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
};
|
||||
// Todo: this needs to be from wado dicom web client
|
||||
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;
|
||||
return ret;
|
||||
});
|
||||
@ -469,6 +501,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
||||
});
|
||||
return imageIds;
|
||||
},
|
||||
getConfig() {
|
||||
return dicomWebConfigCopy;
|
||||
},
|
||||
};
|
||||
|
||||
if (supportsReject) {
|
||||
|
||||
@ -9,18 +9,19 @@ import { api } from 'dicomweb-client';
|
||||
*/
|
||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
static studyFilterKeys = {
|
||||
StudyInstanceUID: '0020000D',
|
||||
PatientName: '00100010',
|
||||
studyinstanceuid: '0020000D',
|
||||
patientname: '00100010',
|
||||
'00100020': 'mrn',
|
||||
StudyDescription: '00081030',
|
||||
StudyDate: '00080020',
|
||||
ModalitiesInStudy: '00080061',
|
||||
AccessionNumber: '00080050',
|
||||
studydescription: '00081030',
|
||||
studydate: '00080020',
|
||||
modalitiesinstudy: '00080061',
|
||||
accessionnumber: '00080050',
|
||||
};
|
||||
|
||||
static seriesFilterKeys = {
|
||||
SeriesInstanceUID: '0020000E',
|
||||
SeriesNumber: '00200011',
|
||||
seriesinstanceuid: '0020000E',
|
||||
seriesnumber: '00200011',
|
||||
modality: '00080060',
|
||||
};
|
||||
|
||||
constructor(qidoConfig) {
|
||||
@ -37,15 +38,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
async searchForStudies(options) {
|
||||
if (!this.staticWado) return super.searchForStudies(options);
|
||||
|
||||
let searchResult = await super.searchForStudies(options);
|
||||
const searchResult = await super.searchForStudies(options);
|
||||
const { queryParams } = options;
|
||||
|
||||
if (!queryParams) return searchResult;
|
||||
|
||||
const lowerParams = this.toLowerParams(queryParams);
|
||||
const filtered = searchResult.filter(study => {
|
||||
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
||||
if (
|
||||
!this.filterItem(
|
||||
key,
|
||||
queryParams,
|
||||
lowerParams,
|
||||
study,
|
||||
StaticWadoClient.studyFilterKeys
|
||||
)
|
||||
@ -61,16 +65,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
async searchForSeries(options) {
|
||||
if (!this.staticWado) return super.searchForSeries(options);
|
||||
|
||||
let searchResult = await super.searchForSeries(options);
|
||||
const searchResult = await super.searchForSeries(options);
|
||||
const { queryParams } = options;
|
||||
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)) {
|
||||
if (
|
||||
!this.filterItem(
|
||||
key,
|
||||
queryParams,
|
||||
study,
|
||||
lowerParams,
|
||||
series,
|
||||
StaticWadoClient.seriesFilterKeys
|
||||
)
|
||||
) {
|
||||
@ -136,13 +142,13 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
/**
|
||||
* Filters the return list by the query parameters.
|
||||
*
|
||||
* @param {*} key
|
||||
* @param {*} queryParams
|
||||
* @param anyCaseKey - a possible search key
|
||||
* @param queryParams -
|
||||
* @param {*} study
|
||||
* @param {*} sourceFilterMap
|
||||
* @returns
|
||||
*/
|
||||
filterItem(key, queryParams, study, sourceFilterMap) {
|
||||
filterItem(key: string, queryParams, study, sourceFilterMap) {
|
||||
const altKey = sourceFilterMap[key] || key;
|
||||
if (!queryParams) return true;
|
||||
const testValue = queryParams[key] || queryParams[altKey];
|
||||
@ -153,6 +159,15 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||
}
|
||||
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