Generate render tool.

This commit is contained in:
James A. Petts 2020-06-05 16:36:11 +01:00
parent 7bd041a02c
commit f4470c9625
13 changed files with 612 additions and 47 deletions

View File

@ -0,0 +1,7 @@
export default {
POINT: 'POINT',
MULTIPOINT: 'MULTIPOINT',
POLYLINE: 'POLYLINE',
CIRCLE: 'CIRCLE',
ELLIPSE: 'ELLIPSE',
};

View File

@ -0,0 +1,5 @@
const TOOL_NAMES = {
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
};
export default TOOL_NAMES;

View File

@ -1,5 +1,8 @@
import id from './id';
import { utils } from '@ohif/core';
import { utils, classes } from '@ohif/core';
import addMeasurement from './utils/addMeasurement.js';
const { ImageSet } = classes;
const sopClassHandlerName = 'dicom-sr';
@ -9,8 +12,6 @@ const sopClassUids = [
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
];
const scoordTypes = ['POINT', 'MULTIPOINT', 'POLYLINE', 'CIRCLE', 'ELLIPSE'];
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
@ -20,6 +21,10 @@ const CodeNameCodeSequenceValues = {
TrackingUniqueIdentifier: '112040',
};
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
};
/**
* Basic SOPClassHandler:
* - For all Image types that are stackable, create
@ -29,16 +34,23 @@ const CodeNameCodeSequenceValues = {
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @returns {Array} The list of display sets created for the given series object
*/
function getDisplaySetsFromSeries(instances) {
function _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
) {
// If the series has no instances, stop here
if (!instances || !instances.length) {
throw new Error('No instances were provided');
}
const { DisplaySetService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const instance = instances[0];
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const { ConceptNameCodeSequence, ContentSequence } = instance;
if (
@ -51,10 +63,6 @@ function getDisplaySetsFromSeries(instances) {
return [];
}
const referencedImages = _getReferencedImagesList(ContentSequence);
const measurements = _getMeasurements(ContentSequence, SOPInstanceUID);
const displaySet = {
plugin: id,
Modality: 'SR',
@ -63,13 +71,141 @@ function getDisplaySetsFromSeries(instances) {
SeriesInstanceUID,
StudyInstanceUID,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
referencedImages,
referencedImages: _getReferencedImagesList(ContentSequence),
measurements: _getMeasurements(ContentSequence),
};
console.log(DisplaySetService);
// Check currently added displaySets and add measurements if the sources exist.
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
activeDisplaySet,
dataSource
);
});
// Subscribe to new displaySets as the source may come in after.
DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
newDisplaySets => {
// If there are still some measurements that have not yet been loaded into cornerstone,
// See if we can load them onto any of the new displaySets.
newDisplaySets.forEach(newDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(
displaySet,
newDisplaySet,
dataSource
);
});
}
);
return [displaySet];
}
function getSopClassHandlerModule() {
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
dataSource
) {
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
if (unloadedMeasurements.length === 0) {
// All already loaded!
return;
}
if (!newDisplaySet instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
const { sopClassUids, images } = newDisplaySet;
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
measurement.coords.some(coord =>
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
)
);
if (unloadedMeasurements.length === 0) {
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
return;
}
const SOPInstanceUIDs = [];
unloadedMeasurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID =
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
}
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
newDisplaySet
);
for (let i = 0; i < images.length; i++) {
if (!unloadedMeasurements.length) {
// All measurements loaded.
break;
}
const image = images[i];
const { SOPInstanceUID } = image;
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
const imageId = imageIdsForDisplaySet[i];
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
addMeasurement(
measurement,
imageId,
newDisplaySet.displaySetInstanceUID
);
unloadedMeasurements.splice(j, 1);
}
}
}
}
}
function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
const { coords } = measurement;
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
return true;
}
}
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(
instances,
servicesManager,
extensionManager
);
};
return [
{
name: sopClassHandlerName,
@ -79,10 +215,7 @@ function getSopClassHandlerModule() {
];
}
function _getMeasurements(
ImagingMeasurementReportContentSequence,
SOPInstanceUID
) {
function _getMeasurements(ImagingMeasurementReportContentSequence) {
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
@ -101,7 +234,7 @@ function _getMeasurements(
MeasurementGroups
);
let allMeasurements = [];
let measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
@ -110,13 +243,15 @@ function _getMeasurements(
trackingUniqueIdentifier
];
const measurements = _processMeasurement(mergedContentSequence);
const measurement = _processMeasurement(mergedContentSequence);
if (measurements) {
allMeasurements = allMeasurements.concat(measurements);
if (measurement) {
measurements.push(measurement);
}
}
);
return measurements;
}
function _getMergedContentSequencesByTrackingUniqueIdentifiers(
@ -172,8 +307,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
}
function _processMeasurement(mergedContentSequence) {
debugger;
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
@ -182,34 +315,134 @@ function _processMeasurement(mergedContentSequence) {
return _processTID1410Measurement(mergedContentSequence);
}
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
}
function _processTID1410Measurement(mergedContentSequence) {
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
// TODO -> Look at RelationshipType => Contains means
const graphicItem = mergedContentSequence.find(
group => group.ValueType === 'SCOORD'
);
if (!graphicItem) {
console.warn(
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
);
return;
}
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
const measurements = [];
const measurement = {
loaded: false,
labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
};
NUMContentItems.forEach(item => {
debugger;
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
const { ContentSequence, MeasuredValueSequence } = item;
// GET -> REPRESENTATION, REFERNECED IMAGE, measurement value
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
);
}
});
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
// TODO -> Look at RelationshipType => Contains means
return [];
return measurement;
}
function _processTID1410Measurement(mergedContentSequence) {
// TODO
console.error('TODO => Process TID1410Measurement');
debugger;
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(
group => group.ValueType === 'NUM'
);
return [ß];
const measurement = {
loaded: false,
labels: [],
coords: [],
};
NUMContentItems.forEach(item => {
const {
ConceptNameCodeSequence,
ContentSequence,
MeasuredValueSequence,
} = item;
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(
`Graphic ${ValueType} not currently supported, skipping annotation.`
);
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
if (coords) {
measurement.coords.push(coords);
}
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
)
);
}
});
return measurement;
}
function _getCoordsFromSCOORDOrSCOORD3D(item) {
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
console.warn(
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
);
return;
}
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = item.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
return coords;
}
function _getLabelFromMeasuredValueSequence(
ConceptNameCodeSequence,
MeasuredValueSequence
) {
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
return { label: CodeMeaning, value: `${NumericValue} ${CodeValue}` }; // E.g. Long Axis: 31.0 mm
}
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {

View File

@ -1,6 +1,7 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import id from './id.js';
import init from './init';
const Component = React.lazy(() => {
return import('./OHIFCornerstoneSRViewport');
@ -23,6 +24,10 @@ export default {
*/
id,
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
/**
*
*

View File

@ -0,0 +1,21 @@
import cornerstoneTools from 'cornerstone-tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import TOOL_NAMES from './constants/toolNames';
const defaultConfig = {
TOOL_NAMES: {
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
},
};
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }) {
const conifg = Object.assign({}, defaultConfig, configuration);
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
cornerstoneTools.addTool(DICOMSRDisplayTool);
}

View File

@ -0,0 +1,123 @@
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
import TOOL_NAMES from '../constants/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
// Cornerstone 3rd party dev kit imports
const draw = importInternal('drawing/draw');
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
const drawCircle = importInternal('drawing/drawCircle');
const drawEllipse = importInternal('drawing/drawEllipse');
const getNewContext = importInternal('drawing/getNewContext');
const BaseTool = importInternal('base/BaseTool');
/**
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
* @extends cornerstoneTools.BaseTool
*/
export default class DICOMSRDisplayTool extends BaseTool {
constructor(props = {}) {
const defaultProps = {
mixins: ['enabledOrDisabledBinaryTool'],
name: TOOL_NAMES.DICOM_SR_DISPLAY_TOOL,
};
const initialProps = Object.assign(defaultProps, props);
super(initialProps);
}
renderToolData(evt) {
const eventData = evt.detail;
const { element } = eventData;
const toolState = getToolState(element, this.name);
if (!toolState) {
return;
}
const options = {
color: toolColors.setToolColor(),
lineWidth: 2,
handleRadius: 6,
};
for (let i = 0; i < toolState.data.length; i++) {
const data = toolState.data[i];
Object.keys(data).forEach(GraphicType => {
const renderableData = data[GraphicType];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
renderPointOrMultipoint(renderableData, eventData, options);
break;
case SCOORD_TYPES.POLYLINE:
renderPolyLine(renderableData, eventData, options);
break;
case SCOORD_TYPES.CIRCLE:
renderCircle(renderableData, eventData, options);
break;
case SCOORD_TYPES.ELLIPSE:
renderEllipse(renderableData, eventData, options);
break;
}
});
}
}
renderPolyLine(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
draw(context, context => {
drawJoinedLines(context, element, points[0], points, options);
});
});
}
renderPointOrMultipoint(renderableData, eventData, options) {
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
draw(context, context => {
drawHandles(context, eventData, points, options);
});
});
}
renderCircle(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(circle => {
const { center, radius } = circle;
drawCircle(context, element, center, radius, options);
});
}
renderEllipse(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(ellipse => {
const { corner1, corner2 } = ellipse;
drawEllipse(
context,
element,
corner1,
corner2,
options,
'pixel',
0 // TODO -> Work our the initial rotation and add it here so we render appropriately rotated ellipses.
);
});
}
}

View File

@ -0,0 +1,135 @@
import cornerstoneTools from 'cornerstone-tools';
import TOOL_NAMES from '../constants/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
import { cornerstone } from '../../../../platform/core/src';
const globalImageIdSpecificToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager;
export default function addMeasurement(
measurement,
imageId,
displaySetInstanceUID
) {
console.log('== ADD MEASUREMENT TO CST ==');
console.log(measurement);
console.log(imageId);
console.log('============================');
// TODO -> GET TRACKING UID AND ADD IT TO TOOL DATA AND ALSO THE MEASUREMENT ENTRY.
// TODO -> Render rotated ellipse .
debugger;
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
const measurementData = {};
measurement.coords.forEach(coord => {
const { GraphicType, GraphicData } = coord;
if (measurementData[GraphicType] === undefined) {
measurementData[GraphicType] = [];
}
const renderableData = _getRenderableData(GraphicType, GraphicData);
measurementData[GraphicType].push(renderableData);
});
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
if (toolState[imageId] === undefined) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState[toolName] === undefined) {
imageIdToolState[toolName] = {
data: [],
};
}
const toolData = imageIdToolState[toolName];
toolData.data.push(measurementData);
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = displaySetInstanceUID;
debugger;
}
function _getRenderableData(GraphicType, GraphicData) {
let renderableData;
switch (GraphicType) {
case SCOORD_TYPES.POINT:
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableData = [];
for (let i = 0; i < GraphicData.length; i += 2) {
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
}
break;
case SCOORD_TYPES.CIRCLE:
const center = { x: GraphicData[0], y: GraphicData[1] };
const onPerimeter = { x: GraphicData[2], y: GraphicData[3] };
const radius = cornerstoneMath.point.distance(center, onPerimeter);
renderableData = {
center,
radius,
};
break;
case SCOORD_TYPES.ELLIPSE:
console.warn('ROTATED ELLIPSE NOT YET SUPPORTED!');
const majorAxis = [
{ x: GraphicData[0], y: GraphicData[1] },
{ x: GraphicData[2], y: GraphicData[3] },
];
const minorAxis = [
{ x: GraphicData[4], y: GraphicData[5] },
{ x: GraphicData[6], y: GraphicData[7] },
];
// Calculate two opposite corners of box defined by two axes.
const minorAxisLength = cornerstoneMath.point.distance(
minorAxis[0],
minorAxis[1]
);
const minorAxisDirection = {
x: (minorAxis[1].x - minorAxis[0].x) / minorAxisLength,
y: (minorAxis[1].y - minorAxis[0].y) / minorAxisLength,
};
const halfMinorAxisLength = minorAxisLength / 2;
// First end point of major axis + half minor axis vector
const corner1 = {
x: majorAxis[0].x + minorAxisDirection.x * halfMinorAxisLength,
y: majorAxis[0].y + minorAxisDirection.y * halfMinorAxisLength,
};
// Second end point of major axis - half of minor axis vector
const corner2 = {
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength,
};
renderableData = {
corner1,
corner2,
};
break;
}
return renderableData;
}

View File

@ -17,9 +17,12 @@ export default class ExtensionManager {
this.modules[moduleType] = [];
});
this.dataSourceMap = {};
this.defaultDataSourceName = appConfig.defaultDataSourceName;
this.activeDataSource = undefined;
}
console.log('modules map');
console.log(this.modulesMap);
setActiveDataSource(dataSourceName) {
this.activeDataSource = dataSourceName;
}
/**
@ -137,6 +140,10 @@ export default class ExtensionManager {
};
getDataSources = dataSourceName => {
if (dataSourceName === undefined) {
dataSourceName = this.activeDataSource;
}
// Note: this currently uses the data source name, which feels weird...
return this.dataSourceMap[dataSourceName];
};

View File

@ -23,4 +23,5 @@ window.config = {
},
},
],
defaultDataSourceName: 'dicomweb',
};

View File

@ -13,6 +13,8 @@ import {
// redux as reduxOHIF,
} from '@ohif/core';
// TODO -> this feels bad.
/**
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
* @param {object[]} defaultExtensions - array of extension objects

View File

@ -27,12 +27,15 @@ export default function ModeRoute({
: [StudyInstanceUIDs];
const { extensions, sopClassHandlers } = mode;
// TODO:
// - Check query/params for specific dataSource
// - If provided, query for that dataSource instance
// - If not provided, select default datasource
// - Update `extensionManager` to have a method to retrieve the default source
const dataSources = extensionManager.getDataSources(dataSourceName);
if (dataSourceName === undefined) {
dataSourceName = extensionManager.defaultDataSourceName;
}
extensionManager.setActiveDataSource(dataSourceName);
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const route = mode.routes[0];

View File

@ -68,6 +68,30 @@ export default function buildModeRoutes(
exact: true,
});
});
const defaultDataSourceName = extensionManager.defaultDataSourceName;
// Add default DataSource route.
const path = `/${mode.id}`;
// TODO move up.
const component = ({ location }) => (
<ViewModelProvider>
<ModeRoute
location={location}
mode={mode}
dataSourceName={defaultDataSourceName}
extensionManager={extensionManager}
servicesManager={servicesManager}
/>
</ViewModelProvider>
);
routes.push({
path,
component,
exact: true,
});
});
return routes;

View File

@ -29,8 +29,7 @@ const createRoutes = (
const routes =
buildModeRoutes(modes, dataSources, extensionManager, servicesManager) ||
[];
// TODO: Shouldn't need to guard input routes with an empty array?
const allRoutes = [...(routes || []), ...bakedInRoutes];
const allRoutes = [...routes, ...bakedInRoutes];
console.log(
'Creating Routes: ',