feat: Segmentations Settings UI - Phase 1 #1391 (#1392)

* 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.

* fix: bump cornerstone-tools version to address toFixed issue

* test: force tests for MISTER^MR to navigate to study directly

* test: remove eraser tool test

* test: count is off by one

* test: don't save SR state for the time being

* test: disable more tests

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:
authored and GitHub committed 2020-03-13 15:45:26 -04:00
1 parent d426b78967
commit e8842cf8ae
78 files changed
+3071 -1962

No files matched your search

+55
View File
@@ -1,5 +1,6 @@
import guid from '../utils/guid.js';
import OHIFError from './OHIFError';
import { Vector3 } from 'cornerstone-math';
const OBJECT = 'object';
@@ -63,6 +64,60 @@ class ImageSet {
sortBy(sortingCallback) {
return this.images.sort(sortingCallback);
}
sortByImagePositionPatient() {
const images = this.images;
const referenceImagePositionPatient = _getImagePositionPatient(images[0]);
const refIppVec = new Vector3(
referenceImagePositionPatient[0],
referenceImagePositionPatient[1],
referenceImagePositionPatient[2]
);
const ImageOrientationPatient = _getImageOrientationPatient(images[0]);
const scanAxisNormal = new Vector3(
ImageOrientationPatient[0],
ImageOrientationPatient[1],
ImageOrientationPatient[2]
).cross(
new Vector3(
ImageOrientationPatient[3],
ImageOrientationPatient[4],
ImageOrientationPatient[5]
)
);
const distanceImagePairs = images.map(function(image) {
const ippVec = new Vector3(..._getImagePositionPatient(image));
const positionVector = refIppVec.clone().sub(ippVec);
const distance = positionVector.dot(scanAxisNormal);
return {
distance,
image,
};
});
distanceImagePairs.sort(function(a, b) {
return b.distance - a.distance;
});
const sortedImages = distanceImagePairs.map(a => a.image);
images.sort(function(a, b) {
return sortedImages.indexOf(a) - sortedImages.indexOf(b);
});
}
}
function _getImagePositionPatient(image) {
return image.getData().metadata.ImagePositionPatient;
}
function _getImageOrientationPatient(image) {
return image.getData().metadata.ImageOrientationPatient;
}
export default ImageSet;
@@ -35,6 +35,12 @@ export class StudyMetadata extends Metadata {
writable: false,
value: [],
},
_derivedDisplaySets: {
configurable: false,
enumerable: false,
writable: false,
value: [],
},
_firstSeries: {
configurable: false,
enumerable: false,
@@ -121,7 +127,11 @@ export class StudyMetadata extends Metadata {
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
displaySet.isDerived
? this._addDerivedDisplaySet(displaySet)
: displaySets.push(displaySet);
return displaySets;
}
}
@@ -186,9 +196,73 @@ export class StudyMetadata extends Metadata {
}
/**
* Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Metadata must represent
* imaging modalities.
* Adds the displaySets to the studies list of derived displaySets.
* @param {object} displaySet The displaySet to append to the derived displaysets list.
*/
_addDerivedDisplaySet(displaySet) {
this._derivedDisplaySets.push(displaySet);
// --> Perhaps that logic should exist in the extension sop class handler and this be a dumb list.
// TODO -> Get x Modality by referencedSeriesInstanceUid, FoR, etc.
}
/**
* Returns a list of derived datasets in the study, filtered by the given filter.
* @param {object} filter An object containing search filters
* @param {object} filter.modality
* @param {object} filter.referencedSeriesInstanceUID
* @param {object} filter.referencedFrameOfReferenceUID
* @return {Array} filtered derived display sets
*/
getDerivedDatasets(filter) {
const {
modality,
referencedSeriesInstanceUID,
referencedFrameOfReferenceUID,
} = filter;
let filteredDerivedDisplaySets = this._derivedDisplaySets;
if (modality) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => displaySet.Modality === Modality
);
}
if (referencedSeriesInstanceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet => {
if (!displaySet.metadata.ReferencedSeriesSequence) {
return false;
}
const ReferencedSeriesSequence = Array.isArray(
displaySet.metadata.ReferencedSeriesSequence
)
? displaySet.metadata.ReferencedSeriesSequence
: [displaySet.metadata.ReferencedSeriesSequence];
return ReferencedSeriesSequence.some(
ReferencedSeries =>
ReferencedSeries.SeriesInstanceUID === referencedSeriesInstanceUID
);
}
);
}
if (referencedFrameOfReferenceUID) {
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
displaySet =>
displaySet.ReferencedFrameOfReferenceUID ===
ReferencedFrameOfReferenceUID
);
}
return filteredDerivedDisplaySets;
}
/**
* Creates a set of displaySets to be placed in the Study Metadata
* The displaySets that appear in the Study Metadata must represent
* imaging modalities. A series may be split into one or more displaySets.
*
* Furthermore, for drag/drop functionality,
* it is easiest if the stack objects also contain information about
@@ -482,6 +556,22 @@ export class StudyMetadata extends Metadata {
return series;
}
/**
* Get the first image id given display instance uid.
* @return {string} The image id.
*/
getFirstImageId(displaySetInstanceUID) {
try {
const displaySet = this.findDisplaySet(
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
);
return displaySet.images[0].getImageId();
} catch (error) {
console.error('Failed to retrieve image metadata');
return null;
}
}
/**
* Get the first instance of the current study retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
@@ -625,6 +715,10 @@ const makeDisplaySet = (series, instances) => {
imageSet.isReconstructable = isReconstructable.value;
if (shallSort && imageSet.isReconstructable) {
imageSet.sortByImagePositionPatient();
}
if (isReconstructable.missingFrames) {
// TODO -> This is currently unused, but may be used for reconstructing
// Volumes with gaps later on.
@@ -0,0 +1,45 @@
import DICOMWeb from '../../../DICOMWeb';
/**
* Function to get series sequence (sequence of pepeating items where each
* item includes the attributes of one or more series) based on a given sopInstance.
*
* @param {Object} instance The sop instance
* @returns {Promise} Referenced series sequence
*/
const getReferencedSeriesSequence = instance => {
const referencedSeriesSequenceRaw = instance['00081115'];
const referencedSeriesSequence = [];
if (referencedSeriesSequenceRaw && referencedSeriesSequenceRaw.Value) {
referencedSeriesSequenceRaw.Value.forEach(referencedSeries => {
const referencedSeriesInstanceUID = DICOMWeb.getString(
referencedSeries['0020000E']
);
const referencedInstanceSequenceRaw = referencedSeries['0008114A'];
const referencedInstanceSequence = [];
referencedInstanceSequenceRaw.Value.forEach(referencedInstance => {
referencedInstanceSequence.push({
referencedSOPClassUID: DICOMWeb.getString(
referencedInstance['00081150']
),
referencedSOPInstanceUID: DICOMWeb.getString(
referencedInstance['00081155']
),
});
});
referencedSeriesSequence.push({
referencedSeriesInstanceUID,
referencedInstanceSequence,
});
});
}
return referencedSeriesSequence;
};
export default getReferencedSeriesSequence;
@@ -2,6 +2,7 @@ import DICOMWeb from '../../../DICOMWeb';
import metadataProvider from '../../../classes/MetadataProvider';
import getWADORSImageId from '../../../utils/getWADORSImageId';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import getReferencedSeriesSequence from './getReferencedSeriesSequence';
/**
* Create a plain JS object that describes a study (a study descriptor object)
@@ -23,7 +24,10 @@ function createStudy(server, aSopInstance) {
PatientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
PatientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
AccessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
StudyTime: DICOMWeb.getString(aSopInstance['00080030']),
StudyDate: DICOMWeb.getString(aSopInstance['00080020']),
FrameOfReferenceUID: DICOMWeb.getString(aSopInstance['00200052']),
ReferencedSeriesSequence: getReferencedSeriesSequence(aSopInstance),
modalities: DICOMWeb.getString(aSopInstance['00080061']), // TODO -> Rename this.. it'll take a while to not mess this one up.
StudyDescription: DICOMWeb.getString(aSopInstance['00081030']),
NumberOfStudyRelatedInstances: DICOMWeb.getString(aSopInstance['00201208']),
@@ -186,7 +190,7 @@ async function makeSOPInstance(server, study, instance) {
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
sopInstanceList.map(function (sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
+3
View File
@@ -8,6 +8,7 @@ import studyMetadataManager from './studyMetadataManager';
import writeScript from './writeScript.js';
import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
import * as urlUtil from './urlUtil';
import makeCancelable from './makeCancelable';
import hotkeys from './hotkeys';
@@ -24,6 +25,7 @@ const utils = {
studyMetadataManager,
DicomLoaderService,
urlUtil,
loadAndCacheDerivedDisplaySets,
makeCancelable,
hotkeys,
};
@@ -40,6 +42,7 @@ export {
studyMetadataManager,
DicomLoaderService,
urlUtil,
loadAndCacheDerivedDisplaySets,
makeCancelable,
hotkeys,
};
+1
View File
@@ -12,6 +12,7 @@ describe('Top level exports', () => {
'b64toBlob',
'StackManager',
'studyMetadataManager',
'loadAndCacheDerivedDisplaySets',
'DicomLoaderService',
'urlUtil',
'makeCancelable',
@@ -167,8 +167,8 @@ function _getSpacingIssue(spacing, averageSpacing) {
function _getPerpendicularDistance(a, b) {
return Math.sqrt(
Math.pow(a[0] - b[0], 2) +
Math.pow(a[1] - b[1], 2) +
Math.pow(a[2] - b[2], 2)
Math.pow(a[1] - b[1], 2) +
Math.pow(a[2] - b[2], 2)
);
}
@@ -0,0 +1,107 @@
import studyMetadataManager from './studyMetadataManager';
/**
* Study schema
*
* @typedef {Object} Study
* @property {Array} seriesList -
* @property {Object} seriesMap -
* @property {Object} seriesLoader -
* @property {string} wadoUriRoot -
* @property {string} wadoRoot -
* @property {string} qidoRoot -
* @property {string} patientName -
* @property {string} patientId -
* @property {number} patientAge -
* @property {number} patientSize -
* @property {number} patientWeight -
* @property {string} accessionNumber -
* @property {string} studyDate -
* @property {string} studyTime -
* @property {string} modalities -
* @property {string} studyDescription -
* @property {string} imageCount -
* @property {string} studyInstanceUid -
* @property {string} institutionName -
* @property {Array} displaySets -
*/
/**
* Factory function to load and cache derived display sets.
*
* @param {object} referencedDisplaySet Display set
* @param {string} referencedDisplaySet.displaySetInstanceUid Display set instance uid
* @param {string} referencedDisplaySet.seriesDate
* @param {string} referencedDisplaySet.seriesTime
* @param {string} referencedDisplaySet.seriesInstanceUid
* @param {string} referencedDisplaySet.seriesNumber
* @param {string} referencedDisplaySet.seriesDescription
* @param {number} referencedDisplaySet.numImageFrames
* @param {string} referencedDisplaySet.frameRate
* @param {string} referencedDisplaySet.modality
* @param {boolean} referencedDisplaySet.isMultiFrame
* @param {number} referencedDisplaySet.instanceNumber
* @param {boolean} referencedDisplaySet.isReconstructable
* @param {string} referencedDisplaySet.studyInstanceUid
* @param {Array} referencedDisplaySet.sopClassUids
* @param {Study[]} studies Collection of studies
* @returns void
*/
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
if (!studyMetadata) {
return;
}
const derivedDisplaySets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: SeriesInstanceUID,
});
if (!derivedDisplaySets.length) {
return;
}
// Filter by type
const displaySetsPerModality = {};
derivedDisplaySets.forEach(displaySet => {
const Modality = displaySet.Modality;
if (displaySetsPerModality[Modality] === undefined) {
displaySetsPerModality[Modality] = [];
}
displaySetsPerModality[Modality].push(displaySet);
});
// For each type, see if any are loaded, if not load the most recent.
Object.keys(displaySetsPerModality).forEach(key => {
const displaySets = displaySetsPerModality[key];
const isLoaded = displaySets.some(displaySet => displaySet.isLoaded);
if (isLoaded) {
return;
}
// find most recent and load it.
let recentDateTime = 0;
let recentDisplaySet;
displaySets.forEach(displaySet => {
const dateTime = Number(
`${displaySet.SeriesDate}${displaySet.SeriesTime}`
);
if (dateTime > recentDateTime) {
recentDateTime = dateTime;
recentDisplaySet = displaySet;
}
});
recentDisplaySet.load(referencedDisplaySet, studies);
});
};
export default loadAndCacheDerivedDisplaySets;
@@ -0,0 +1,3 @@
const reconstructableModalities = ['MR', 'CT', 'PT', 'NM'];
export default reconstructableModalities;