Support of RTSTRUCT visualization (#1524)
* Filter segs and fetch correct metadata. * Initiate seg loading logic on series load into viewport. Need to write. * Automatic consumption of SEG functional. * Pull in master. * Fix issues after pulling in master. * Clean up old segmentation handlers. * WIP * WIP * Poppulate viewport with metadata. * Show hide/segmentation in react-vtkjs-viewport. * feat: 🎸 View segmentations in vtkjs viewport * Filter segs and fetch correct metadata. * Initiate seg loading logic on series load into viewport. Need to write. * Automatic consumption of SEG functional. * Pull in master. * Fix issues after pulling in master. * Clean up old segmentation handlers. * WIP * WIP * Poppulate viewport with metadata. * Show hide/segmentation in react-vtkjs-viewport. * feat: 🎸 View segmentations in vtkjs viewport * Rebase on master. * format format format * add basic extension parts * Updated yarn lock * Fetch derived/referenced displaysets in sidepanel * Add buttons, expose seriesDescription, write notes for Danny. * Add growcut tool extention. -> Move to a seperate repo. * Change some text fields. * expose dialog provider * hide draggable area * No pointer events for now; need to revisit * Don't filter seg from the outset for now * fiddling about * fix path to series data * fix logic to determine next available labelmapindex * Ability to swtich between labelmaps * Return a promise so we can make sure our labelmap is loaded before pulling metadata * Better UI to toggle labelmaps * Fix accidental untoggle of active labelmap for ui * Remove brush tool from cornerstone extension * Add cstools in preInit for seg extension * Add buttons for seg magic * Hide the measurements panel for now * Slightly better seg/scissor icons * Add eraser * ability to switch segments * seg color / switcher * Fix vtk labelmap cache. * sidepanel should scroll if there's overflow * Use a fancier range slider * Slightly prettier buttons * Push in progress * A bit more reactive; handle no-meta * Update to vtk.js 11.14.0 and react-vtkjs-viewport 0.6.0 * chore: kickstart preview * Fix conflict issues * Refactor panel * Isolate increment functions * Wip segment table * Hide selector and add scroll to segmentation * Increase width of segment description * Hide not used items * Add new select for segmentations * Align ui * Fix default segmentation * Fix broken import test * Get measurements panel back and add spacing to grouped buttons * Hide brush and selector and refresh when switching segmentations * Add pointer events back and fix broken tests for measurement table * Update segment list when switching humbnails * CR Update: Minor UI adjustments * CR Update: Set active segment * Move components to components folder * Add new settings ui * Integrate configuration * Improve styles * Float numbers * Improve css * Refresh settings menu if side panel changed * Fix settings rendering logic and remove comments * Extract components * Remove not used tools * Remove non used extension * Use style variables and remove comments * Remove non used dep from lock * Remove tools * Remove segmentation from toolbar * Add todo and brushradius from config * Fix packagejson growcut import * Remove webpack config already defined in parent * Update package json description * Change component name to british english to us * Improve prop types * Rename cornerstone label map event * Scope css * Extract and add doc to helper * Update jsdocs for getDerivedDatasets * Add proptypes and cr updates * Update tablistitem proptypes * config is readonly, rollback destructuring * Remove event for side panel * Add jsdocs for util * Extract getimageid to studymetada * Use ohif log instead of console * Remove specific style from side panel * Fix default props for brush radius * Descructure configuration props from settings component to avoid invalid props * Update proptypes to be required (settings) * Update readme * Remove commandsmodule * Get configuration module inside the component * Set selected segment * Rename extension and use modality to filter * Fix for failing tests (#1423) * hello world * Finish rebase for Igor. * Re-fix cornerstoneTools version that got messed up in merge. * Fix reactivity of isDisabled, write implementation for the SEG panel. * Nuke yarn.lock to stack CST to 4.12.0 * Remove debugger statements. * WIP consume RTSTRUCT * Doc API and add endpoints. * Rebase on SEG PR. * Update DICOM Keyword variables. * Fix metadata vars format * Add basic RT panel along with configuration * Wire up configuration with settings component * Mock structure set tables * Improve design of settings * Integrate list of contours * Extract panel section * Filter by seriesuid * Fix slow settings * Add jump to image * Fix expandable items * Improvements and CR update * CR Update: Remove google config * CR Update: Remove erase tool test * CR Update: Remove instance * CR Update: Fix issue with undefined prop * CR Update: Update yarn lock * Hide actions of structure item Co-authored-by: James A. Petts <jamesapetts@gmail.com> Co-authored-by: dannyrb <danny.ri.brown@gmail.com> Co-authored-by: igoroctaviano] <igoroctaviano@users.noreply.github.com> Co-authored-by: Mirna Silva <mirna.silva@radicalimaging.com>
This commit is contained in:
parent
83f9478977
commit
858484f250
@ -7,6 +7,9 @@ import { getEnabledElement } from './state';
|
|||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
const scroll = cornerstoneTools.import('util/scroll');
|
||||||
|
|
||||||
|
const { studyMetadataManager } = OHIF.utils;
|
||||||
|
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager }) => {
|
const commandsModule = ({ servicesManager }) => {
|
||||||
const actions = {
|
const actions = {
|
||||||
rotateViewport: ({ viewports, rotation }) => {
|
rotateViewport: ({ viewports, rotation }) => {
|
||||||
@ -255,9 +258,35 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
cornerstone.setViewport(enabledElement, viewport);
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
jumpToImage: ({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex
|
||||||
|
}) => {
|
||||||
|
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
|
const displaySet = study.findDisplaySet(ds => {
|
||||||
|
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||||
|
displaySet.frameIndex = frameIndex;
|
||||||
|
|
||||||
|
window.store.dispatch(setViewportSpecificData(activeViewportIndex, displaySet));
|
||||||
|
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
jumpToImage: {
|
||||||
|
commandFn: actions.jumpToImage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
getNearbyToolData: {
|
getNearbyToolData: {
|
||||||
commandFn: actions.getNearbyToolData,
|
commandFn: actions.getNearbyToolData,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
|
|||||||
8
extensions/dicom-rt/.webpack/webpack.dev.js
Normal file
8
extensions/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 });
|
||||||
|
};
|
||||||
44
extensions/dicom-rt/.webpack/webpack.prod.js
Normal file
44
extensions/dicom-rt/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.join(__dirname, './..');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
|
||||||
|
return merge(commonConfig, {
|
||||||
|
devtool: 'source-map',
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: ROOT_DIR,
|
||||||
|
library: 'OHIFExtDicomSeg',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
21
extensions/dicom-rt/LICENSE
Normal file
21
extensions/dicom-rt/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 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.
|
||||||
1
extensions/dicom-rt/README.md
Normal file
1
extensions/dicom-rt/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @ohif/extension-dicom-rt
|
||||||
43
extensions/dicom-rt/package.json
Normal file
43
extensions/dicom-rt/package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-rt",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/index.umd.js",
|
||||||
|
"module": "src/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"dev:dicom-html": "yarn run dev",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"prepublishOnly": "yarn run build",
|
||||||
|
"start": "yarn run dev"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "^0.50.0",
|
||||||
|
"cornerstone-core": "^2.2.8",
|
||||||
|
"cornerstone-tools": "^4.0.9",
|
||||||
|
"dcmjs": "^0.8.3",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"react-select": "^3.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
237
extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js
Normal file
237
extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||||
|
import loadRTStruct from './loadRTStruct';
|
||||||
|
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
// TODO: Should probably use dcmjs for this
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||||
|
|
||||||
|
const OHIFDicomRTStructSopClassHandler = {
|
||||||
|
id: 'OHIFDicomRTStructSopClassHandler',
|
||||||
|
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||||
|
sopClassUIDs,
|
||||||
|
getDisplaySetFromSeries: function (
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
dicomWebClient,
|
||||||
|
authorizationHeaders
|
||||||
|
) {
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
|
||||||
|
const metadata = instance.getData().metadata;
|
||||||
|
const {
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = metadata;
|
||||||
|
|
||||||
|
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
|
||||||
|
|
||||||
|
const rtStructDisplaySet = {
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
authorizationHeaders,
|
||||||
|
metadata,
|
||||||
|
isDerived: true,
|
||||||
|
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||||
|
labelmapIndex: null, // Assigned when loaded.
|
||||||
|
isLoaded: false,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!metadata.ReferencedSeriesSequence) {
|
||||||
|
const ReferencedFrameOfReferenceSequence =
|
||||||
|
metadata.ReferencedFrameOfReferenceSequence;
|
||||||
|
|
||||||
|
if (ReferencedFrameOfReferenceSequence) {
|
||||||
|
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
|
||||||
|
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||||
|
return loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
).catch(error => {
|
||||||
|
rtStructDisplaySet.isLoaded = false;
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return rtStructDisplaySet;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
) {
|
||||||
|
const ReferencedSeriesSequence = [];
|
||||||
|
|
||||||
|
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
|
||||||
|
referencedFrameOfReference => {
|
||||||
|
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedStudySequence).forEach(
|
||||||
|
rtReferencedStudy => {
|
||||||
|
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
|
||||||
|
rtReferencedSeries => {
|
||||||
|
const ReferencedInstanceSequence = [];
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
} = rtReferencedSeries;
|
||||||
|
|
||||||
|
_getSequenceAsArray(ContourImageSequence).forEach(
|
||||||
|
contourImage => {
|
||||||
|
ReferencedInstanceSequence.push({
|
||||||
|
ReferencedSOPInstanceUID:
|
||||||
|
contourImage.ReferencedSOPInstanceUID,
|
||||||
|
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSeries = {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
ReferencedInstanceSequence,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReferencedSeriesSequence.push(referencedSeries);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return ReferencedSeriesSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSequenceAsArray(sequence) {
|
||||||
|
return Array.isArray(sequence) ? sequence : [sequence];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getReferencedFrameOfReferenceSequence(instance) {
|
||||||
|
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
|
||||||
|
|
||||||
|
const referencedFrameOfReferenceSequence = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!referencedFrameOfReferenceSequenceRaw ||
|
||||||
|
!referencedFrameOfReferenceSequenceRaw.Value
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequenceRaw.Value.forEach(
|
||||||
|
referencedFrameOfReferenceRaw => {
|
||||||
|
const frameOfReferenceUID = DICOMWeb.getString(
|
||||||
|
referencedFrameOfReferenceRaw['00200052']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameOfReference = { frameOfReferenceUID };
|
||||||
|
const RTReferencedStudySequenceRaw =
|
||||||
|
referencedFrameOfReferenceRaw['30060012'];
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
|
||||||
|
|
||||||
|
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
|
||||||
|
referencedFrameOfReference.rtReferencedStudySequence = [];
|
||||||
|
|
||||||
|
const { rtReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
|
||||||
|
const referencedSopClassUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedStudy = {
|
||||||
|
referencedSopClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
rtReferencedSeriesSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedStudySequence.push(rtReferencedStudy);
|
||||||
|
|
||||||
|
const { rtReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
const rtReferencedSeriesSequenceRaw =
|
||||||
|
rtReferencedStudyRaw['30060014'];
|
||||||
|
|
||||||
|
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
|
||||||
|
const seriesInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedSeriesRaw['0020000E']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedSeries = {
|
||||||
|
seriesInstanceUID,
|
||||||
|
contourImageSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedSeriesSequence.push(rtReferencedSeries);
|
||||||
|
|
||||||
|
const { contourImageSequence } = rtReferencedSeries;
|
||||||
|
|
||||||
|
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
|
||||||
|
|
||||||
|
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
|
||||||
|
const referencedSOPClassUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081160']
|
||||||
|
);
|
||||||
|
const referencedSegmentNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['0062000B']
|
||||||
|
);
|
||||||
|
|
||||||
|
const contourImage = {
|
||||||
|
referencedSOPClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
referencedFrameNumber,
|
||||||
|
referencedSegmentNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
contourImageSequence.push(contourImage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return referencedFrameOfReferenceSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OHIFDicomRTStructSopClassHandler;
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||||
|
return (
|
||||||
|
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||||
|
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<g transform="rotate(0 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(30 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(60 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(90 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(120 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(150 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(180 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(210 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(240 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(270 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(300 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(330 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingIndicator;
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
.dcmrt-panel-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon.expanded {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons .angle-double-down, .icons .angle-double-up {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './PanelSection.css';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const PanelSection = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
visible = false,
|
||||||
|
expanded = false,
|
||||||
|
loading = false,
|
||||||
|
hideVisibleButton = false,
|
||||||
|
onVisibilityChange = () => { },
|
||||||
|
onExpandChange = () => { }
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(visible);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="dcmrt-panel-section"
|
||||||
|
style={{
|
||||||
|
marginBottom: isExpanded ? 0 : 2,
|
||||||
|
height: isExpanded ? '100%' : 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="header">
|
||||||
|
<div>{title}</div>
|
||||||
|
<div className="icons">
|
||||||
|
{!hideVisibleButton && (
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||||
|
name={isVisible ? "eye" : "eye-closed"}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Icon
|
||||||
|
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||||
|
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newExpandValue = !isExpanded;
|
||||||
|
setIsExpanded(newExpandValue);
|
||||||
|
onExpandChange(newExpandValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
expanded: PropTypes.bool,
|
||||||
|
onVisibilityChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.defaultProps = {
|
||||||
|
visible: false,
|
||||||
|
expanded: false,
|
||||||
|
onVisibilityChange: () => { }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PanelSection;
|
||||||
26
extensions/dicom-rt/src/components/RTPanel/RTPanel.css
Normal file
26
extensions/dicom-rt/src/components/RTPanel/RTPanel.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.dcmrt-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
258
extensions/dicom-rt/src/components/RTPanel/RTPanel.js
Normal file
258
extensions/dicom-rt/src/components/RTPanel/RTPanel.js
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import './RTPanel.css';
|
||||||
|
import StructureSetItem from '../StructureSetItem/StructureSetItem';
|
||||||
|
import RTPanelSettings from '../RTSettings/RTSettings';
|
||||||
|
import PanelSection from '../PanelSection/PanelSection';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const { studyMetadataManager } = utils;
|
||||||
|
|
||||||
|
const refreshViewport = () => {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RTPanel component
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.studies
|
||||||
|
* @param {Array} props.viewports - viewportSpecificData
|
||||||
|
* @param {number} props.activeIndex - activeViewportIndex
|
||||||
|
* @param {number} props.isOpen - isOpen
|
||||||
|
* @returns component
|
||||||
|
*/
|
||||||
|
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
||||||
|
const [selectedContour, setSelectedContour] = useState();
|
||||||
|
const DEFAULT_SET_INDEX = 0;
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
referencedDisplaysets: [],
|
||||||
|
sets: [],
|
||||||
|
selectedSet: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, setState] = useState(DEFAULT_STATE);
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const activeViewport = viewports[activeIndex];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateStructureSets = () => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const StructureSets = module.state.StructureSets;
|
||||||
|
|
||||||
|
if (StructureSets && StructureSets.length) {
|
||||||
|
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
activeViewport.SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||||
|
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||||
|
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (viewportSets.length) {
|
||||||
|
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
|
||||||
|
setState({
|
||||||
|
referencedDisplaysets,
|
||||||
|
selectedSet: defaultSet,
|
||||||
|
sets: viewportSets
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(DEFAULT_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateStructureSets();
|
||||||
|
}, [studies, viewports, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowSettings(showSettings && !isOpen);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
|
||||||
|
let interpretedType = '';
|
||||||
|
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||||
|
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameContour = selectedContour && selectedContour === ROINumber;
|
||||||
|
return (
|
||||||
|
<StructureSetItem
|
||||||
|
key={ROINumber}
|
||||||
|
selected={isSameContour}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedContour(isSameContour ? null : ROINumber);
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = toolState.data[0].imageIds;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||||
|
state.selectedSet.SeriesInstanceUID,
|
||||||
|
ROINumber,
|
||||||
|
imageIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||||
|
|
||||||
|
onContourItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={`${ROIName} ${interpretedType}`}
|
||||||
|
index={ROINumber}
|
||||||
|
color={colorArray}
|
||||||
|
visible={visible}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configurationChangeHandler = newConfiguration => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.configuration.lineWidth = newConfiguration.lineWidth;
|
||||||
|
module.configuration.opacity = newConfiguration.opacity;
|
||||||
|
refreshViewport();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSettings) {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
return (
|
||||||
|
<RTPanelSettings
|
||||||
|
configuration={module.configuration}
|
||||||
|
onBack={() => setShowSettings(false)}
|
||||||
|
onChange={configurationChangeHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-panel">
|
||||||
|
<div className="dcmrt-panel-header">
|
||||||
|
<h3>RT Structure Sets</h3>
|
||||||
|
<Icon
|
||||||
|
className="cog-icon"
|
||||||
|
name="cog"
|
||||||
|
width="25px"
|
||||||
|
height="25px"
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{state.referencedDisplaysets.map(displaySet => {
|
||||||
|
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
|
||||||
|
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
return (
|
||||||
|
<PanelSection
|
||||||
|
key={SeriesInstanceUID}
|
||||||
|
title={metadata.StructureSetLabel}
|
||||||
|
loading={!isLoaded || !loadedSet}
|
||||||
|
visible={isLoaded && loadedSet.visible}
|
||||||
|
hideVisibleButton={!isLoaded}
|
||||||
|
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleStructureSet(SeriesInstanceUID);
|
||||||
|
}}
|
||||||
|
onExpandChange={async () => {
|
||||||
|
if (!isLoaded) {
|
||||||
|
await displaySet.load(viewports[activeIndex], studies);
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
setState(state => ({ ...state, selectedSet, sets }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollableArea>
|
||||||
|
<TableList headless>
|
||||||
|
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||||
|
</TableList>
|
||||||
|
</ScrollableArea>
|
||||||
|
</PanelSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.propTypes = {
|
||||||
|
viewports: PropTypes.shape({
|
||||||
|
displaySetInstanceUID: PropTypes.string,
|
||||||
|
frameRate: PropTypes.any,
|
||||||
|
InstanceNumber: PropTypes.number,
|
||||||
|
isMultiFrame: PropTypes.bool,
|
||||||
|
isReconstructable: PropTypes.bool,
|
||||||
|
Modality: PropTypes.string,
|
||||||
|
plugin: PropTypes.string,
|
||||||
|
SeriesDate: PropTypes.string,
|
||||||
|
SeriesDescription: PropTypes.string,
|
||||||
|
SeriesInstanceUID: PropTypes.string,
|
||||||
|
SeriesNumber: PropTypes.any,
|
||||||
|
SeriesTime: PropTypes.string,
|
||||||
|
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
StudyInstanceUID: PropTypes.string,
|
||||||
|
}),
|
||||||
|
activeIndex: PropTypes.number.isRequired,
|
||||||
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.defaultProps = {};
|
||||||
|
|
||||||
|
export default RTPanel;
|
||||||
88
extensions/dicom-rt/src/components/RTSettings/RTSettings.css
Normal file
88
extensions/dicom-rt/src/components/RTSettings/RTSettings.css
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
.dcmrt-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title h3 {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .content {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .header {
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range:not(:last-of-type) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range label {
|
||||||
|
margin: 0;
|
||||||
|
display: unset;
|
||||||
|
min-width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button {
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 4px 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button:hover,
|
||||||
|
.dcmrt-settings .return-button:active,
|
||||||
|
.dcmrt-settings .return-button.active {
|
||||||
|
background-color: var(--active-color);
|
||||||
|
border-color: var(--ui-border-color-active);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
71
extensions/dicom-rt/src/components/RTSettings/RTSettings.js
Normal file
71
extensions/dicom-rt/src/components/RTSettings/RTSettings.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Range } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './RTSettings.css';
|
||||||
|
|
||||||
|
const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||||
|
const toFloat = value => parseFloat(value / 100).toFixed(2);
|
||||||
|
|
||||||
|
const save = (field, value) => {
|
||||||
|
onChange({ ...configuration, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsSection = ({ title, children }) => {
|
||||||
|
return (
|
||||||
|
<div className="settings-section">
|
||||||
|
<div className="header">{title}</div>
|
||||||
|
<div className="content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-settings">
|
||||||
|
<div className="settings-title">
|
||||||
|
<h3>RT Structure Set Settings</h3>
|
||||||
|
<button className="return-button" onClick={onBack}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsSection title="Segment Outline">
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Opacity</label>
|
||||||
|
<Range
|
||||||
|
showPercentage
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={configuration.opacity * 100}
|
||||||
|
onChange={event => save('opacity', toFloat(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Width</label>
|
||||||
|
<Range
|
||||||
|
showValue
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
value={configuration.lineWidth}
|
||||||
|
onChange={event => save('lineWidth', parseInt(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTSettings.propTypes = {
|
||||||
|
configuration: PropTypes.shape({
|
||||||
|
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
onBack: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RTSettings;
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
.dcmrt-structure-set-item .item-color-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-color {
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-info {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 9px;
|
||||||
|
padding-left: 9px;
|
||||||
|
width: 90px;
|
||||||
|
border-left: 1px solid var(--text-secondary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions {
|
||||||
|
margin-left: -1px;
|
||||||
|
background-color: var(--ui-gray-darker);
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
visibility: hidden;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 35px;
|
||||||
|
height: 35px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction:hover, .dcmrt-structure-set-item .item-actions .btnAction:active {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon.--visible {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './StructureSetItem.css';
|
||||||
|
|
||||||
|
const ColoredCircle = ({ color }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="item-color"
|
||||||
|
style={{ backgroundColor: `rgba(${color.join(',')})` }}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ColoredCircle.propTypes = {
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StructureSetItem = ({
|
||||||
|
index,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
itemClass,
|
||||||
|
color,
|
||||||
|
visible = true,
|
||||||
|
onVisibilityChange,
|
||||||
|
selected = false,
|
||||||
|
}) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
return (
|
||||||
|
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||||
|
<TableListItem
|
||||||
|
key={index}
|
||||||
|
itemKey={index}
|
||||||
|
itemIndex={index}
|
||||||
|
itemClass={itemClass}
|
||||||
|
itemMeta={<ColoredCircle color={color} />}
|
||||||
|
itemMetaClass="item-color-section"
|
||||||
|
onItemClick={onClick}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||||
|
{label}
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{false && <div className="item-info">{'...'}</div>}
|
||||||
|
{false && (
|
||||||
|
<div className="item-actions">
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Relabelling...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Relabel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Editing description...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Description
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableListItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.propTypes = {
|
||||||
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
itemClass: PropTypes.string,
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.defaultProps = {
|
||||||
|
itemClass: '',
|
||||||
|
onClick: () => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StructureSetItem;
|
||||||
3
extensions/dicom-rt/src/id.js
Normal file
3
extensions/dicom-rt/src/id.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const id = 'rt';
|
||||||
|
|
||||||
|
export default id;
|
||||||
78
extensions/dicom-rt/src/index.js
Normal file
78
extensions/dicom-rt/src/index.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import init from './init.js';
|
||||||
|
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||||
|
import id from './id.js';
|
||||||
|
import RTPanel from './components/RTPanel/RTPanel';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
init({ servicesManager, configuration });
|
||||||
|
},
|
||||||
|
getPanelModule({ commandsManager }) {
|
||||||
|
const ExtendedRTPanel = props => {
|
||||||
|
const contourItemClickHandler = contourData => {
|
||||||
|
commandsManager.runCommand('jumpToImage', contourData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RTPanel
|
||||||
|
{...props}
|
||||||
|
onContourItemClick={contourItemClickHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
label: 'RTSTRUCT',
|
||||||
|
target: 'rt-panel',
|
||||||
|
isDisabled: studies => {
|
||||||
|
if (!studies) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < studies.length; i++) {
|
||||||
|
const study = studies[i];
|
||||||
|
|
||||||
|
if (study && study.series) {
|
||||||
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
|
const series = study.series[j];
|
||||||
|
console.log(studies);
|
||||||
|
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'rt-panel',
|
||||||
|
component: ExtendedRTPanel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['VIEWER'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getSopClassHandlerModule({ servicesManager }) {
|
||||||
|
return sopClassHandlerModule;
|
||||||
|
},
|
||||||
|
};
|
||||||
25
extensions/dicom-rt/src/init.js
Normal file
25
extensions/dicom-rt/src/init.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import RTStructDisplayTool from './tools/RTStructDisplayTool';
|
||||||
|
import rtStructModule from './tools/modules/rtStructModule';
|
||||||
|
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
TOOL_NAMES: {
|
||||||
|
RT_STRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} configuration
|
||||||
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
|
*/
|
||||||
|
export default function init({ servicesManager, configuration = {} }) {
|
||||||
|
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||||
|
|
||||||
|
TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL = conifg.TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
cornerstoneTools.register('module', 'rtstruct', rtStructModule);
|
||||||
|
cornerstoneTools.addTool(RTStructDisplayTool);
|
||||||
|
}
|
||||||
309
extensions/dicom-rt/src/loadRTStruct.js
Normal file
309
extensions/dicom-rt/src/loadRTStruct.js
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
const { DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
|
export default async function loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
) {
|
||||||
|
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
|
||||||
|
// Set here is loading is asynchronous.
|
||||||
|
// If this function throws its set back to false.
|
||||||
|
rtStructDisplaySet.isLoaded = true;
|
||||||
|
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||||
|
|
||||||
|
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
|
|
||||||
|
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||||
|
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
|
dicomData.dict
|
||||||
|
);
|
||||||
|
|
||||||
|
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||||
|
dicomData.meta
|
||||||
|
);
|
||||||
|
|
||||||
|
// global cornerstone tools state to attach measurements to.
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
StructureSetROISequence,
|
||||||
|
ROIContourSequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
StructureSetLabel,
|
||||||
|
} = rtStructDataset;
|
||||||
|
|
||||||
|
// Define our structure set entry and add it to the rtstruct module state.
|
||||||
|
const structureSet = {
|
||||||
|
StructureSetLabel,
|
||||||
|
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROIContours: [],
|
||||||
|
referencedSeriesSequence:
|
||||||
|
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
rtStructModule.setters.structureSet(structureSet);
|
||||||
|
|
||||||
|
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||||
|
|
||||||
|
const ROIContour = ROIContourSequence[i];
|
||||||
|
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||||
|
|
||||||
|
if (!ContourSequence) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let c = 0; c < ContourSequence.length; c++) {
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
ContourData,
|
||||||
|
NumberOfContourPoints,
|
||||||
|
ContourGeometricType,
|
||||||
|
} = ContourSequence[c];
|
||||||
|
|
||||||
|
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
||||||
|
// TODO: Do we want to visualise types other than closed planar?
|
||||||
|
// We could easily do open planar.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||||
|
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||||
|
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||||
|
toolState,
|
||||||
|
imageId,
|
||||||
|
rtStructDisplayToolName
|
||||||
|
);
|
||||||
|
|
||||||
|
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||||
|
points.push({
|
||||||
|
x: ContourData[p],
|
||||||
|
y: ContourData[p + 1],
|
||||||
|
z: ContourData[p + 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transformPointsToImagePlane(points, imagePlane);
|
||||||
|
|
||||||
|
const measurementData = {
|
||||||
|
handles: {
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROINumber: ReferencedROINumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
imageIdSpecificToolData.push(measurementData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
) {
|
||||||
|
const StructureSetROI = StructureSetROISequence.find(
|
||||||
|
structureSetROI =>
|
||||||
|
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const ROIContourData = {
|
||||||
|
ROINumber: StructureSetROI.ROINumber,
|
||||||
|
ROIName: StructureSetROI.ROIName,
|
||||||
|
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||||
|
ROIDescription: StructureSetROI.ROIDescription,
|
||||||
|
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];
|
||||||
|
} else {
|
||||||
|
//Choose a color from the cornerstoneTools colorLUT
|
||||||
|
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
||||||
|
const { getters } = cornerstoneTools.getModule('segmentation');
|
||||||
|
const color = getters.colorForSegmentIndexColorLUT(
|
||||||
|
0,
|
||||||
|
ROIContourData.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
ROIContourData.colorArray = [...color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _setToolEnabledIfNotEnabled(toolName) {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
const { element } = enabledElement;
|
||||||
|
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
||||||
|
|
||||||
|
if (tool.mode !== 'enabled') {
|
||||||
|
// If not already active or passive, set passive so contours render.
|
||||||
|
cornerstoneTools.setToolEnabled(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
||||||
|
if (toolState.hasOwnProperty(imageId) === false) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
||||||
|
imageIdToolState[toolName] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageIdToolState[toolName].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||||
|
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||||
|
imageIdSopInstanceUidPairsEntry =>
|
||||||
|
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const study = studies.find(
|
||||||
|
study => study.StudyInstanceUID === StudyInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const displaySets = study.displaySets.filter(set => {
|
||||||
|
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (displaySets.length > 1) {
|
||||||
|
console.warn(
|
||||||
|
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||||
|
);
|
||||||
|
// TODO -> We could make check the instance list and see if any match?
|
||||||
|
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||||
|
// ^ Will that even happen?
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySets[0];
|
||||||
|
|
||||||
|
return referencedDisplaySet.images.map(image => {
|
||||||
|
return {
|
||||||
|
imageId: image.getImageId(),
|
||||||
|
sopInstanceUID: image.getSOPInstanceUID(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
93
extensions/dicom-rt/src/tools/RTStructDisplayTool.js
Normal file
93
extensions/dicom-rt/src/tools/RTStructDisplayTool.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||||
|
|
||||||
|
import TOOL_NAMES from '../utils/toolNames';
|
||||||
|
|
||||||
|
// Cornerstone 3rd party dev kit imports
|
||||||
|
const draw = importInternal('drawing/draw');
|
||||||
|
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||||
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
|
||||||
|
* @extends cornerstoneTools.BaseTool
|
||||||
|
*/
|
||||||
|
export default class RTStructDisplayTool extends BaseTool {
|
||||||
|
constructor(props = {}) {
|
||||||
|
const defaultProps = {
|
||||||
|
mixins: ['enabledOrDisabledBinaryTool'],
|
||||||
|
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialProps = Object.assign(defaultProps, props);
|
||||||
|
|
||||||
|
super(initialProps);
|
||||||
|
|
||||||
|
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolData(evt) {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const rtstructModule = this._rtStructModule;
|
||||||
|
|
||||||
|
const toolState = getToolState(evt.currentTarget, this.name);
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lineWidth, opacity } = rtstructModule.configuration;
|
||||||
|
|
||||||
|
// We have tool data for this element - iterate over each one and draw it
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
for (let i = 0; i < toolState.data.length; i++) {
|
||||||
|
const data = toolState.data[i];
|
||||||
|
|
||||||
|
const structureSet = rtstructModule.getters.structureSet(
|
||||||
|
data.structureSetSeriesInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if entire StructureSet is hidden.
|
||||||
|
if (structureSet.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROIContourData = rtstructModule.getters.ROIContour(
|
||||||
|
data.structureSetSeriesInstanceUid,
|
||||||
|
data.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if ROIContour is hidden.
|
||||||
|
if (ROIContourData.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = data.handles.points;
|
||||||
|
|
||||||
|
if (!points.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorArray = ROIContourData.colorArray;
|
||||||
|
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||||
|
colorArray[2]
|
||||||
|
},${opacity})`;
|
||||||
|
|
||||||
|
lineWidth;
|
||||||
|
|
||||||
|
draw(context, context => {
|
||||||
|
drawJoinedLines(
|
||||||
|
context,
|
||||||
|
eventData.element,
|
||||||
|
points[points.length - 1],
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import TOOL_NAMES from '../../../utils/toolNames';
|
||||||
|
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
|
||||||
|
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
|
||||||
|
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
|
||||||
|
* @param {number} ROINumber The ROINumber of the region.
|
||||||
|
* @param {string} imageIdsInStack The stack of imageIds.
|
||||||
|
*
|
||||||
|
* @returns The imageId
|
||||||
|
*/
|
||||||
|
export default function getImageIdOfCenterFrameOfROIContour(
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber,
|
||||||
|
imageIdsInStack
|
||||||
|
) {
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
const imageIdIndicies = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < imageIdsInStack.length; i++) {
|
||||||
|
const imageId = imageIdsInStack[i];
|
||||||
|
|
||||||
|
const imageIdSpecificToolState = toolState[imageId];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!imageIdSpecificToolState ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName] ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName].data
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
|
||||||
|
|
||||||
|
if (
|
||||||
|
_toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
imageIdIndicies.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerImageIdIndex =
|
||||||
|
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
|
||||||
|
|
||||||
|
return imageIdsInStack[centerImageIdIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
return !!toolData.some(
|
||||||
|
toolDataI =>
|
||||||
|
toolDataI.structureSetSeriesInstanceUid ===
|
||||||
|
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
|
||||||
|
* references the given SeriesInstanceUID.
|
||||||
|
* @param {*} StructureSet
|
||||||
|
* @param {*} SeriesInstanceUID
|
||||||
|
*/
|
||||||
|
export default function structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { referencedSeriesSequence } = StructureSet;
|
||||||
|
return referencedSeriesSequence.some(
|
||||||
|
referencedSeries =>
|
||||||
|
referencedSeries.SeriesInstanceUID ===
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
187
extensions/dicom-rt/src/tools/modules/rtStructModule.js
Normal file
187
extensions/dicom-rt/src/tools/modules/rtStructModule.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import getImageIdOfCenterFrameOfROIContour from './lib/getImageIdOfCenterFrameOfROIContour';
|
||||||
|
import structureSetReferencesSeriesInstanceUid from './lib/structureSetReferencesSeriesInstanceUid';
|
||||||
|
|
||||||
|
// We should put this as a helper somewhere as we are using it in mutliple places.
|
||||||
|
function refreshViewport() {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
lineWidth: 3,
|
||||||
|
opacity: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
StructureSets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a structure set to the module.
|
||||||
|
* @param {Object} structureSetData The structure set data.
|
||||||
|
*/
|
||||||
|
function setStructureSet(structureSetData) {
|
||||||
|
state.StructureSets.push(structureSetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the StructureSet with the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*
|
||||||
|
* @returns {Object} The StructureSet.
|
||||||
|
*/
|
||||||
|
function getStructureSet(SeriesInstanceUID) {
|
||||||
|
return state.StructureSets.find(
|
||||||
|
structureSet => structureSet.SeriesInstanceUID === SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ROI Contour with the given ROINumber on the StructureSet defined by the given
|
||||||
|
* SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour to fetch.
|
||||||
|
*/
|
||||||
|
function getROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const structureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (!structureSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return structureSet.ROIContours.find(
|
||||||
|
ROIContour => ROIContour.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setHideStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setShowStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setStructureSetVisible(SeriesInstanceUID, visible = true) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setToggleStructureSet(SeriesInstanceUID) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = !StructureSet.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setHideROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setShowROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setROIContourVisible(SeriesInstanceUID, ROINumber, visible = true) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = !ROIContour.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||||
|
*/
|
||||||
|
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { StructureSets } = state;
|
||||||
|
return StructureSets.filter(StructureSet =>
|
||||||
|
structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getters: {
|
||||||
|
structureSet: getStructureSet,
|
||||||
|
ROIContour: getROIContour,
|
||||||
|
structuresSetsWhichReferenceSeriesInstanceUid: getStructuresSetsWhichReferenceSeriesInstanceUid,
|
||||||
|
imageIdOfCenterFrameOfROIContour: getImageIdOfCenterFrameOfROIContour,
|
||||||
|
},
|
||||||
|
setters: {
|
||||||
|
structureSet: setStructureSet,
|
||||||
|
hideROIContour: setHideROIContour,
|
||||||
|
showROIContour: setShowROIContour,
|
||||||
|
toggleROIContour: setToggleROIContour,
|
||||||
|
hideStructureSet: setHideStructureSet,
|
||||||
|
showStructureSet: setShowStructureSet,
|
||||||
|
toggleStructureSet: setToggleStructureSet,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
configuration,
|
||||||
|
};
|
||||||
5
extensions/dicom-rt/src/utils/toolNames.js
Normal file
5
extensions/dicom-rt/src/utils/toolNames.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
RTSTRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
91
extensions/dicom-rt/src/utils/transformPointsToImagePlane.js
Normal file
91
extensions/dicom-rt/src/utils/transformPointsToImagePlane.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
export default function transformPointsToImagePlane(points, imagePlane) {
|
||||||
|
// See Equation C.7.6.2.1-1 of the DICOM standard
|
||||||
|
|
||||||
|
const {
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
rowPixelSpacing: deltaI,
|
||||||
|
columnPixelSpacing: deltaJ,
|
||||||
|
imagePositionPatient,
|
||||||
|
} = imagePlane;
|
||||||
|
|
||||||
|
const X = [rowCosines[0], rowCosines[1], rowCosines[2]];
|
||||||
|
const Y = [columnCosines[0], columnCosines[1], columnCosines[2]];
|
||||||
|
const S = [
|
||||||
|
imagePositionPatient[0],
|
||||||
|
imagePositionPatient[1],
|
||||||
|
imagePositionPatient[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 9 sets of simulataneous equations to choose from, choose which set to solve
|
||||||
|
// Based on the largest component of each direction cosine.
|
||||||
|
// This avoids NaNs or floating point errors caused by dividing by very small numbers and ensures a safe mapping
|
||||||
|
// when mapping between planes that are close to orthogonal.
|
||||||
|
|
||||||
|
let ix = 0;
|
||||||
|
let iy = 0;
|
||||||
|
let largestDirectionCosineMagnitude = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the element with the largest magnitude in each direction cosine vector.
|
||||||
|
for (let i = 0; i < X.length; i++) {
|
||||||
|
if (Math.abs(X[i]) > largestDirectionCosineMagnitude.x) {
|
||||||
|
ix = i;
|
||||||
|
largestDirectionCosineMagnitude.x = Math.abs(X[i]);
|
||||||
|
}
|
||||||
|
if (Math.abs(Y[i]) > largestDirectionCosineMagnitude.y) {
|
||||||
|
iy = i;
|
||||||
|
largestDirectionCosineMagnitude.y = Math.abs(Y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ci = {
|
||||||
|
// Index of max elements in X and Y
|
||||||
|
ix,
|
||||||
|
iy,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity Check
|
||||||
|
const directionCosineMagnitude = {
|
||||||
|
x: Math.pow(X[0], 2) + Math.pow(X[1], 2) + Math.pow(X[2], 2),
|
||||||
|
y: Math.pow(Y[0], 2) + Math.pow(Y[1], 2) + Math.pow(Y[2], 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (directionCosineMagnitude.x < 0.99 || directionCosineMagnitude.y < 0.99) {
|
||||||
|
throw Error(
|
||||||
|
`Direction cosines do not sum to 1 in quadrature. There is likely a mistake in the DICOM metadata.` +
|
||||||
|
`directionCosineMagnitudes: ${directionCosineMagnitude.x}, ${directionCosineMagnitude.y}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in elements that won't change between points
|
||||||
|
const c = [undefined, Y[ci.ix], X[ci.ix], undefined, X[ci.iy], Y[ci.iy]];
|
||||||
|
|
||||||
|
for (let pointI = 0; pointI < points.length; pointI++) {
|
||||||
|
// Subtract imagePositionPatient from the coordinate
|
||||||
|
const r = [
|
||||||
|
points[pointI].x - S[0],
|
||||||
|
points[pointI].y - S[1],
|
||||||
|
points[pointI].z - S[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set the variable terms in c.
|
||||||
|
c[0] = r[ci.ix];
|
||||||
|
c[3] = r[ci.iy];
|
||||||
|
|
||||||
|
// General case: Solves the two choosen simulataneous equations to go from the patient coordinate system to the imagePlane.
|
||||||
|
const i =
|
||||||
|
(c[0] - (c[1] * c[3]) / c[5]) /
|
||||||
|
(c[2] * deltaI * (1 - (c[1] * c[4]) / (c[2] * c[5])));
|
||||||
|
const j = (c[3] - c[4] * i * deltaI) / (c[5] * deltaJ);
|
||||||
|
|
||||||
|
// NOTE: Add (0.5, 0.5) to the coordinate, as PCS reference frame is with respect to the centre of the first pixel.
|
||||||
|
points[pointI].x = i + 0.5;
|
||||||
|
points[pointI].y = j + 0.5;
|
||||||
|
delete points[pointI].z;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
@ -371,6 +371,7 @@ SegmentationPanel.propTypes = {
|
|||||||
}),
|
}),
|
||||||
activeIndex: PropTypes.number.isRequired,
|
activeIndex: PropTypes.number.isRequired,
|
||||||
studies: PropTypes.array.isRequired,
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
SegmentationPanel.defaultProps = {};
|
SegmentationPanel.defaultProps = {};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file. See
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file. See
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
||||||
|
|
||||||
|
|||||||
@ -144,7 +144,7 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
wadorsuri,
|
wadorsuri,
|
||||||
wadoRoot: server.wadoRoot,
|
wadoRoot: server.wadoRoot,
|
||||||
imageRendering: server.imageRendering,
|
imageRendering: server.imageRendering,
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
thumbnailRendering: server.thumbnailRendering
|
||||||
};
|
};
|
||||||
|
|
||||||
series.instances.push(sopInstance);
|
series.instances.push(sopInstance);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file. See
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,21 @@ export class TableList extends Component {
|
|||||||
defaultItems: PropTypes.object,
|
defaultItems: PropTypes.object,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
headerTitle: PropTypes.string,
|
headerTitle: PropTypes.string,
|
||||||
|
headless: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
headless: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="tableList">
|
<div className="tableList">
|
||||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
{!this.props.headless && (
|
||||||
{this.getHeader()}
|
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||||
</div>
|
{this.getHeader()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="tableListContent">{this.props.children}</div>
|
<div className="tableListContent">{this.props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -85,8 +85,12 @@ import trash from './icons/trash.svg';
|
|||||||
import unlink from './icons/unlink.svg';
|
import unlink from './icons/unlink.svg';
|
||||||
import user from './icons/user.svg';
|
import user from './icons/user.svg';
|
||||||
import youtube from './icons/youtube.svg';
|
import youtube from './icons/youtube.svg';
|
||||||
|
import eye from './icons/eye.svg';
|
||||||
|
import eyeClosed from './icons/eye-closed.svg';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
eye,
|
||||||
|
'eye-closed': eyeClosed,
|
||||||
brush,
|
brush,
|
||||||
scissors,
|
scissors,
|
||||||
user,
|
user,
|
||||||
|
|||||||
1
platform/ui/src/elements/Icon/icons/eye-closed.svg
Normal file
1
platform/ui/src/elements/Icon/icons/eye-closed.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 974 B |
1
platform/ui/src/elements/Icon/icons/eye.svg
Normal file
1
platform/ui/src/elements/Icon/icons/eye.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@ -1,7 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file. See
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@
|
|||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
"@ohif/extension-vtk": "^1.3.9",
|
"@ohif/extension-vtk": "^1.3.9",
|
||||||
"@ohif/i18n": "^0.52.8",
|
"@ohif/i18n": "^0.52.8",
|
||||||
|
"@ohif/extension-dicom-rt": "^0.0.1",
|
||||||
"@ohif/ui": "^1.4.2",
|
"@ohif/ui": "^1.4.2",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||||
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
||||||
|
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
||||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ const appProps = {
|
|||||||
OHIFDicomMicroscopyExtension,
|
OHIFDicomMicroscopyExtension,
|
||||||
OHIFDicomPDFExtension,
|
OHIFDicomPDFExtension,
|
||||||
OHIFDicomSegmentationExtension,
|
OHIFDicomSegmentationExtension,
|
||||||
|
OHIFDicomRtExtension,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user