feat(SR): Measurement adapter mappings and save to same series (#3140)

* feat: Add adapter mappings for RectangleROI and Angle and save same

PR comments

* fix: Rehydrate check on non core measurement adapters
This commit is contained in:
Bill Wallace authored and GitHub committed 2023-02-17 16:44:21 -05:00
1 parent 926290f69e
commit 67fc3f733d
42 files changed
+1277 -218

No files matched your search

+4 -3
View File
@@ -36,7 +36,7 @@
"@ohif/extension-cornerstone": "^3.0.0",
"@ohif/extension-measurement-tracking": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -45,7 +45,8 @@
"dependencies": {
"@babel/runtime": "^7.20.13",
"classnames": "^2.3.2",
"@cornerstonejs/core": "0.30.1",
"@cornerstonejs/tools": "0.39.0"
"@cornerstonejs/adapters": "^0.3.1",
"@cornerstonejs/core": "^0.30.1",
"@cornerstonejs/tools": "^0.44.0"
}
}
@@ -30,14 +30,12 @@ const _generateReport = (
const report = MeasurementReport.generateReport(
filteredToolState,
metaData,
utilities.worldToImageCoords
utilities.worldToImageCoords,
options
);
const { dataset } = report;
// Add in top level series options
Object.assign(dataset, options);
// Set the default character set as UTF-8
// https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005
if (typeof dataset.SpecificCharacterSet === 'undefined') {
@@ -89,7 +87,7 @@ const commandsModule = ({}) => {
additionalFindingTypes,
options = {},
}) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// Use the @cornerstonejs adapter for converting to/from DICOM
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements');
@@ -51,13 +51,17 @@ const RELATIONSHIP_TYPE = {
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Basic SOPClassHandler:
* - For all Image types that are stackable, create
* a displaySet with a stack of images
* DICOM SR SOP Class Handler
* For all referenced images in the TID 1500/300 sections, add an image to the
* display (this is TODO - it is not the actual behaviour below unfortunately)
*
* @param {Array} sopClassHandlerModules List of SOP Class Modules
* @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
* This will only display and rehydrate the latest DICOM SR in the given series
* It would be possible to add the ability to view older series rehydrations
* in the future.
*
* @param instances is a set of instances all from the same series
* @param servicesManager is the services that can be used for creating
* @returns The list of display sets created for the given instances object
*/
function _getDisplaySetsFromSeries(
instances,
@@ -69,7 +73,8 @@ function _getDisplaySetsFromSeries(
throw new Error('No instances were provided');
}
const instance = instances[0];
utils.sortStudyInstances(instances);
const instance = instances[instances.length - 1];
const {
StudyInstanceUID,
@@ -85,10 +90,10 @@ function _getDisplaySetsFromSeries(
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport
CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
console.warn(
'Only support Imaging Measurement Report SRs (TID1500) for now'
console.log(
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
);
return [];
}
@@ -105,6 +110,9 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID,
SOPClassHandlerId,
SOPClassUID,
instances,
// Others is a historical value used for instances which is deprecated and will be removed
others: instances,
referencedImages: null,
measurements: null,
isDerivedDisplaySet: true,
@@ -313,13 +321,13 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
MeasurementGroups
);
let measurements = [];
const measurements = [];
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
];
const measurement = _processMeasurement(mergedContentSequence);
@@ -359,7 +367,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
if (
mergedContentSequencesByTrackingUniqueIdentifiers[
trackingUniqueIdentifier
trackingUniqueIdentifier
] === undefined
) {
// Add the full ContentSequence
@@ -474,9 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const FindingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
CodeNameCodeSequenceValues.FindingSite
);
const measurement = {
@@ -493,7 +501,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
Finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
Finding.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
@@ -509,7 +517,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
) &&
FindingSite.ConceptCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.CornerstoneFreeText
CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
@@ -1,29 +0,0 @@
import { addTool, annotation } from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SRLengthTool from './tools/tools/SRLength';
import SRBidirectionalTool from './tools/tools/SRBidirectional';
import SREllipticalROITool from './tools/tools/SREllipticalROI';
import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate';
/**
* @param {object} configuration
*/
export default function init({ configuration = {} }) {
addTool(DICOMSRDisplayTool);
addTool(SRLengthTool);
addTool(SRBidirectionalTool);
addTool(SREllipticalROITool);
addTool(SRArrowAnnotateTool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
[SRLengthTool.toolName]: dashedLine,
[SRBidirectionalTool.toolName]: dashedLine,
[SREllipticalROITool.toolName]: dashedLine,
[SRArrowAnnotateTool.toolName]: dashedLine,
global: {},
});
}
@@ -0,0 +1,49 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
BidirectionalTool,
CobbAngleTool,
EllipticalROITool,
LengthTool,
PlanarFreehandROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
import { Types } from '@ohif/core';
import toolNames from './tools/toolNames';
/**
* @param {object} configuration
*/
export default function init({
configuration = {},
}: Types.Extensions.ExtensionParams): void {
addTool(DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool, {});
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
// TODO - fix the rehydration of Freehand, as it throws an exception
// on a missing polyline. The fix is probably in CS3D
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
// Modify annotation tools to use dashed lines on SR
const dashedLine = {
lineDash: '4,4',
};
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
SRArrowAnnotate: dashedLine,
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
global: {},
});
}
@@ -1,15 +1,15 @@
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
import SRLengthTool from './tools/SRLength';
import SRBidirectional from './tools/SRBidirectional';
import SREllipticalROI from './tools/SREllipticalROI';
import SRArrowAnnotate from './tools/SRArrowAnnotate';
const toolNames = {
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
SRLength: SRLengthTool.toolName,
SRBidirectional: SRBidirectional.toolName,
SREllipticalROI: SREllipticalROI.toolName,
SRArrowAnnotate: SRArrowAnnotate.toolName,
SRLength: 'SRLength',
SRBidirectional: 'SRBidirectional',
SREllipticalROI: 'SREllipticalROI',
SRArrowAnnotate: 'SRArrowAnnotate',
SRAngle: 'SRAngle',
SRCobbAngle: 'SRCobbAngle',
SRRectangleROI: 'SRRectangleROI',
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
};
export default toolNames;
@@ -1,16 +0,0 @@
import { ArrowAnnotateTool } from '@cornerstonejs/tools';
/**
* The reason we are extending ArrowAnnotateTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have ArrowAnnotate
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a ArrowAnnotateTool mapper),
* but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRArrowAnnotateTool extends ArrowAnnotateTool {
static toolName = 'SRArrowAnnotate';
}
export default SRArrowAnnotateTool;
@@ -1,16 +0,0 @@
import { BidirectionalTool } from '@cornerstonejs/tools';
/**
* The reason we are extending BidirectionalTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Bidirectional
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a BidirectionalTool mapper),
* but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRBidirectional extends BidirectionalTool {
static toolName = 'SRBidirectional';
}
export default SRBidirectional;
@@ -1,16 +0,0 @@
import { EllipticalROITool } from '@cornerstonejs/tools';
/**
* The reason we are extending EllipticalROITool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have EllipticalROI
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a EllipticalROITool mapper),
* but with extending and renaming it, there is not mapper for SREllipticalROITool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SREllipticalROI extends EllipticalROITool {
static toolName = 'SREllipticalROI';
}
export default SREllipticalROI;
@@ -1,16 +0,0 @@
import { LengthTool } from '@cornerstonejs/tools';
/**
* The reason we are extending LengthTool is to create a new tool for SR
* viewport which basically has a different name. This is done since Cornerstone
* has shifted from creating tool instances for each annotation, and we have Length
* mappers at the measurementService, so if we didn't do this, we would be mapping
* the SR annotation to the measurementService (since there is a LengthTool mapper),
* but with extending and renaming it, there is not mapper for SRLengthTool; hence
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
*/
class SRLengthTool extends LengthTool {
static toolName = 'SRLength';
}
export default SRLengthTool;
@@ -0,0 +1,12 @@
import { addTool } from '@cornerstonejs/tools';
export default function addToolInstance(
name: string,
toolClass,
configuration?
): void {
class InstanceClass extends toolClass {
static toolName = name;
}
addTool(InstanceClass);
}
@@ -1,6 +1,8 @@
import { adaptersSR } from '@cornerstonejs/adapters';
const cornerstoneAdapters = adaptersSR.Cornerstone3D;
const cornerstoneAdapters =
adaptersSR.Cornerstone3D.MeasurementReport
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
@@ -37,7 +39,7 @@ export default function isRehydratable(displaySet, mappings) {
});
for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier;
const { TrackingIdentifier } = measurements[i] || {};
const hydratable = adapters.some(adapter => {
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
@@ -54,7 +56,13 @@ export default function isRehydratable(displaySet, mappings) {
if (hydratable) {
return true;
}
console.log(
'Measurement is not rehydratable',
TrackingIdentifier,
measurements[i]
);
}
console.log('No measurements found which were rehydratable');
return false;
}
@@ -284,7 +284,7 @@ function OHIFCornerstoneSRViewport(props) {
* isHydratable check, the outcome for the isHydrated state here is always FALSE
* since we don't do the hydration here. Todo: can't we just set it as false? why
* we are changing the state here? isHydrated is always false at this stage, and
* if it is hydrated we don't event use the SR viewport.
* if it is hydrated we don't even use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {
+5 -5
View File
@@ -30,7 +30,7 @@
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"cornerstone-wado-image-loader": "^4.2.1",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -43,10 +43,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "0.3.0",
"@cornerstonejs/core": "0.30.1",
"@cornerstonejs/streaming-image-volume-loader": "^0.11.2",
"@cornerstonejs/tools": "0.39.0",
"@cornerstonejs/adapters": "^0.3.1",
"@cornerstonejs/core": "^0.30.1",
"@cornerstonejs/streaming-image-volume-loader": "^0.8.2",
"@cornerstonejs/tools": "^0.44.0",
"@kitware/vtk.js": "26.4.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
+4 -3
View File
@@ -25,6 +25,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
const Component = React.lazy(() => {
return import(
@@ -62,10 +63,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
/**
* Register the Cornerstone 3D services and set them up for use.
*
*
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
*/
async preRegistration({
servicesManager,
@@ -151,3 +151,4 @@ const cornerstoneExtension: Types.Extensions.Extension = {
};
export default cornerstoneExtension;
export { measurementMappingUtils };
@@ -14,6 +14,8 @@ import {
DragProbeTool,
ProbeTool,
AngleTool,
CobbAngleTool,
PlanarFreehandROITool,
MagnifyTool,
CrosshairsTool,
SegmentationDisplayTool,
@@ -40,6 +42,8 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(ArrowAnnotateTool);
addTool(DragProbeTool);
addTool(AngleTool);
addTool(CobbAngleTool);
addTool(PlanarFreehandROITool);
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
@@ -76,6 +80,8 @@ const toolNames = {
EllipticalROI: EllipticalROITool.toolName,
Bidirectional: BidirectionalTool.toolName,
Angle: AngleTool.toolName,
CobbAngle: CobbAngleTool.toolName,
PlanarFreehandROI: PlanarFreehandROITool.toolName,
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
@@ -1,6 +1,6 @@
import { eventTarget } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore, MeasurementService } from '@ohif/core';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
@@ -23,6 +23,10 @@ const initMeasurementService = (
Bidirectional,
EllipticalROI,
ArrowAnnotate,
Angle,
CobbAngle,
RectangleROI,
PlanarFreehandROI,
} = measurementServiceMappingsFactory(
measurementService,
displaySetService,
@@ -66,6 +70,38 @@ const initMeasurementService = (
ArrowAnnotate.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'CobbAngle',
CobbAngle.matchingCriteria,
CobbAngle.toAnnotation,
CobbAngle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'Angle',
Angle.matchingCriteria,
Angle.toAnnotation,
Angle.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'RectangleROI',
RectangleROI.matchingCriteria,
RectangleROI.toAnnotation,
RectangleROI.toMeasurement
);
measurementService.addMapping(
csTools3DVer1MeasurementSource,
'PlanarFreehandROI',
PlanarFreehandROI.matchingCriteria,
PlanarFreehandROI.toAnnotation,
PlanarFreehandROI.toMeasurement
);
return csTools3DVer1MeasurementSource;
};
@@ -0,0 +1,210 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const Angle = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
displaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { angle } = targetStats;
const unit = '\u00B0';
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
angle,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:Angle');
mappedAnnotations.forEach(annotation => {
const { angle, unit } = annotation;
columns.push(`Angle (${unit})`);
values.push(angle);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const {
angle,
unit,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
);
return displayText;
}
export default Angle;
@@ -0,0 +1,210 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
const CobbAngle = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Cobb Angle tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
displaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations?.[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return;
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { angle } = targetStats;
const unit = '\u00B0';
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
unit,
angle,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:CobbAngle');
mappedAnnotations.forEach(annotation => {
const { angle, unit } = annotation;
columns.push(`Angle (${unit})`);
values.push(angle);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const {
angle,
unit,
SeriesNumber,
SOPInstanceUID,
frameNumber,
} = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (angle === undefined) return displayText;
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`
);
return displayText;
}
export default CobbAngle;
@@ -0,0 +1,148 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
const PlanarFreehandROI = {
toAnnotation: measurement => { },
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('PlanarFreehandROI tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: { ...data, ...data.cachedStats },
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
/**
* It maps an imaging library annotation to a list of simplified annotation properties.
*
* @param {Object} annotationData
* @param {Object} DisplaySetService
* @returns
*/
function getMappedAnnotations(annotationData, DisplaySetService) {
const { metadata, data } = annotationData;
const { label } = data;
const { referencedImageId } = metadata;
const annotations = [];
const {
SOPInstanceUID: _SOPInstanceUID,
SeriesInstanceUID: _SeriesInstanceUID,
} = getSOPInstanceAttributes(referencedImageId) || {};
if (!_SOPInstanceUID || !_SeriesInstanceUID) {
return annotations;
}
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
_SOPInstanceUID,
_SeriesInstanceUID
);
const { SeriesNumber, SeriesInstanceUID } = displaySet;
annotations.push({
SeriesInstanceUID,
SeriesNumber,
label,
data,
});
return annotations;
}
/**
* TBD
* This function is used to convert the measurement data to a format that is suitable for the report generation (e.g. for the csv report).
* The report returns a list of columns and corresponding values.
* @param {*} mappedAnnotations
* @param {*} points
* @param {*} FrameOfReferenceUID
* @returns Object representing the report's content for this tool.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations) {
return '';
}
export default PlanarFreehandROI;
@@ -0,0 +1,228 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import getModalityUnit from './utils/getModalityUnit';
import { utils } from '@ohif/core';
const RectangleROI = {
toAnnotation: measurement => { },
toMeasurement: (
csToolsEventDetail,
DisplaySetService,
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
console.warn('Rectangle ROI tool: Missing metadata or data');
return null;
}
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
}
const {
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
} else {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points } = data.handles;
const mappedAnnotations = getMappedAnnotations(
annotation,
DisplaySetService
);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
};
},
};
function getMappedAnnotations(annotation, DisplaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
if (!targets.length) {
return [];
}
const annotations = [];
Object.keys(cachedStats).forEach(targetId => {
const targetStats = cachedStats[targetId];
if (!referencedImageId) {
// Todo: Non-acquisition plane measurement mapping not supported yet
throw new Error(
'Non-acquisition plane measurement mapping not supported'
);
}
const {
SOPInstanceUID,
SeriesInstanceUID,
frameNumber,
} = getSOPInstanceAttributes(referencedImageId);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID,
frameNumber
);
const { SeriesNumber } = displaySet;
const { mean, stdDev, max, area, Modality } = targetStats;
const unit = getModalityUnit(Modality);
annotations.push({
SeriesInstanceUID,
SOPInstanceUID,
SeriesNumber,
frameNumber,
Modality,
unit,
mean,
stdDev,
max,
area,
});
});
return annotations;
}
/*
This function is used to convert the measurement data to a format that is
suitable for the report generation (e.g. for the csv report). The report
returns a list of columns and corresponding values.
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
const columns = [];
const values = [];
// Add Type
columns.push('AnnotationType');
values.push('Cornerstone:EllipticalROI');
mappedAnnotations.forEach(annotation => {
const { mean, stdDev, max, area, unit } = annotation;
if (!mean || !unit || !max || !area) {
return;
}
columns.push(
`max (${unit})`,
`mean (${unit})`,
`std (${unit})`,
`area (mm2)`
);
values.push(max, mean, stdDev, area);
});
if (FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(FrameOfReferenceUID);
}
if (points) {
columns.push('points');
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
// so that it can be used in the csv report
values.push(points.map(p => p.join(' ')).join(';'));
}
return {
columns,
values,
};
}
function getDisplayText(mappedAnnotations, displaySet) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
const displayText = [];
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.images.find(
image => image.SOPInstanceUID === SOPInstanceUID
);
let InstanceNumber;
if (instance) {
InstanceNumber = instance.InstanceNumber;
}
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
const roundedArea = utils.roundNumber(area || 0, 2);
displayText.push(`${roundedArea} mm<sup>2</sup>`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) {
const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
}
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {
displayText.push(str);
}
});
return displayText;
}
export default RectangleROI;
@@ -1 +1,11 @@
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
export default [
'Length',
'EllipticalROI',
'Bidirectional',
'ArrowAnnotate',
'Angle',
'CobbAngle',
'Probe',
'RectangleROI',
'PlanarFreehandROI',
];
@@ -0,0 +1,3 @@
import * as measurementMappingUtils from './utils';
export { measurementMappingUtils };
@@ -1,19 +1,24 @@
import { MeasurementService } from '@ohif/core';
import Length from './Length';
import Bidirectional from './Bidirectional';
import EllipticalROI from './EllipticalROI';
import ArrowAnnotate from './ArrowAnnotate';
import CobbAngle from './CobbAngle';
import Angle from './Angle';
import PlanarFreehandROI from './PlanarFreehandROI';
import RectangleROI from './RectangleROI';
const measurementServiceMappingsFactory = (
measurementService,
measurementService: MeasurementService,
displaySetService,
cornerstoneViewportService
) => {
/**
* Maps measurement service format object to cornerstone annotation object.
*
* @param {Measurement} measurement The measurement instance
* @param {string} definition The source definition
* @return {Object} Cornerstone annotation data
* @param measurement The measurement instance
* @param definition The source definition
* @return Cornerstone annotation data
*/
const _getValueTypeFromToolType = toolType => {
@@ -23,7 +28,8 @@ const measurementServiceMappingsFactory = (
RECTANGLE,
BIDIRECTIONAL,
POINT,
} = measurementService.VALUE_TYPES;
ANGLE,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attempted, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
@@ -32,14 +38,17 @@ const measurementServiceMappingsFactory = (
Length: POLYLINE,
EllipticalROI: ELLIPSE,
RectangleROI: RECTANGLE,
PlanarFreehandROI: POLYLINE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
CobbAngle: ANGLE,
Angle: ANGLE,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
};
return {
const factories = {
Length: {
toAnnotation: Length.toAnnotation,
toMeasurement: csToolsAnnotation =>
@@ -51,7 +60,7 @@ const measurementServiceMappingsFactory = (
),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
@@ -69,15 +78,16 @@ const measurementServiceMappingsFactory = (
// TODO -> We should eventually do something like shortAxis + longAxis,
// But its still a little unclear how these automatic interpretations will work.
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
{
valueType: measurementService.VALUE_TYPES.POLYLINE,
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
},
EllipticalROI: {
toAnnotation: EllipticalROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
@@ -89,10 +99,43 @@ const measurementServiceMappingsFactory = (
),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.ELLIPSE,
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
},
],
},
RectangleROI: {
toAnnotation: RectangleROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
RectangleROI.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
},
],
},
PlanarFreehandROI: {
toAnnotation: PlanarFreehandROI.toAnnotation,
toMeasurement: csToolsAnnotation =>
PlanarFreehandROI.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
},
],
},
ArrowAnnotate: {
toAnnotation: ArrowAnnotate.toAnnotation,
toMeasurement: csToolsAnnotation =>
@@ -104,12 +147,46 @@ const measurementServiceMappingsFactory = (
),
matchingCriteria: [
{
valueType: measurementService.VALUE_TYPES.POINT,
valueType: MeasurementService.VALUE_TYPES.POINT,
points: 1,
},
],
},
CobbAngle: {
toAnnotation: CobbAngle.toAnnotation,
toMeasurement: csToolsAnnotation =>
CobbAngle.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ANGLE,
},
],
},
Angle: {
toAnnotation: Angle.toAnnotation,
toMeasurement: csToolsAnnotation =>
Angle.toMeasurement(
csToolsAnnotation,
displaySetService,
cornerstoneViewportService,
_getValueTypeFromToolType
),
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.ANGLE,
},
],
},
};
return factories;
};
export default measurementServiceMappingsFactory;
@@ -0,0 +1,17 @@
import getHandlesFromPoints from './getHandlesFromPoints';
import {
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
} from './selection';
import getModalityUnit from './getModalityUnit';
import getSOPInstanceAttributes from './getSOPInstanceAttributes';
export {
getModalityUnit,
getHandlesFromPoints,
getSOPInstanceAttributes,
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
};
@@ -0,0 +1,44 @@
import { annotation as cs3dToolAnnotationUtils } from '@cornerstonejs/tools';
/**
* Check whether an annotation from imaging library is selected or not.
* @param {string} annotationUID uid of imaging library annotation
* @returns boolean
*/
function isAnnotationSelected(annotationUID: string): boolean {
return cs3dToolAnnotationUtils.selection.isAnnotationSelected(annotationUID);
}
/**
* Change an annotation from imaging library's selected property.
* @param annotationUID - uid of imaging library annotation
* @param selected - new value for selected
*/
function setAnnotationSelected(annotationUID: string, selected: boolean): void {
const isCurrentSelected = isAnnotationSelected(annotationUID);
// branch cut, avoid invoking imaging library unnecessarily.
if (isCurrentSelected !== selected) {
cs3dToolAnnotationUtils.selection.setAnnotationSelected(
annotationUID,
selected
);
}
}
function getFirstAnnotationSelected(element) {
const [selectedAnnotationUID] =
cs3dToolAnnotationUtils.selection.getAnnotationsSelected() || [];
if (selectedAnnotationUID) {
return cs3dToolAnnotationUtils.state.getAnnotation(
selectedAnnotationUID,
element
);
}
}
export {
isAnnotationSelected,
setAnnotationSelected,
getFirstAnnotationSelected,
};
+2 -2
View File
@@ -32,8 +32,8 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/i18n": "^1.0.0",
"dcmjs": "0.29.4",
"dicomweb-client": "^0.6.0",
"dcmjs": "^0.29.4",
"dicomweb-client": "^0.8.4",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -1,15 +1,15 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { utils, ServicesManager } from '@ohif/core';
import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui';
import ActionButtons from './ActionButtons';
import debounce from 'lodash.debounce';
import { utils } from '@ohif/core';
import createReportDialogPrompt, {
CREATE_REPORT_DIALOG_RESPONSE,
} from './createReportDialogPrompt';
import createReportAsync from '../Actions/createReportAsync';
import getNextSRSeriesNumber from '../utils/getNextSRSeriesNumber';
import findSRWithSameSeriesDescription from '../utils/findSRWithSameSeriesDescription';
const { downloadCSVReport } = utils;
@@ -17,7 +17,7 @@ export default function PanelMeasurementTable({
servicesManager,
commandsManager,
extensionManager,
}) {
}): React.FunctionComponent {
const [viewportGrid, viewportGridService] = useViewportGrid();
const { activeViewportIndex, viewports } = viewportGrid;
const {
@@ -25,7 +25,7 @@ export default function PanelMeasurementTable({
uiDialogService,
uiNotificationService,
displaySetService,
} = servicesManager.services;
} = (servicesManager as ServicesManager).services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
@@ -72,7 +72,7 @@ export default function PanelMeasurementTable({
measurementService.clearMeasurements();
}
async function createReport() {
async function createReport(): Promise<any> {
// filter measurements that are added to the active study
const activeViewport = viewports[activeViewportIndex];
const measurements = measurementService.getMeasurements();
@@ -109,17 +109,19 @@ export default function PanelMeasurementTable({
? 'Research Derived Series' // default
: promptResult.value; // provided value
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
// Re-use an existing series having the same series description to avoid
// creating too many series instances.
const options = findSRWithSameSeriesDescription(
SeriesDescription,
displaySetService
);
const displaySetInstanceUIDs = await createReportAsync(
return createReportAsync(
servicesManager,
commandsManager,
dataSource,
trackedMeasurements,
{
SeriesDescription,
SeriesNumber,
}
options
);
}
}
@@ -233,16 +235,7 @@ export default function PanelMeasurementTable({
}
PanelMeasurementTable.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
measurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
};
function _getMappedMeasurements(measurementService) {
@@ -0,0 +1,49 @@
import { DisplaySetService, Types } from '@ohif/core';
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
/**
* Find an SR having the same series description.
* This is used by the store service in order to store DICOM SR's having the
* same Series Description into a single series under consecutive instance numbers
* That way, they are all organized as a set and could have tools to view
* "prior" SR instances.
*
* @param SeriesDescription - is the description to look for
* @param displaySetService - the display sets to search for DICOM SR in
* @returns SeriesMetadata from a DICOM SR having the same series description
*/
export default function findSRWithSameSeriesDescription(
SeriesDescription: string,
displaySetService: DisplaySetService
): Types.SeriesMetadata {
const activeDisplaySets = displaySetService.getActiveDisplaySets();
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
const sameSeries = srDisplaySets.find(
ds => ds.SeriesDescription === SeriesDescription
);
if (sameSeries) {
console.log('Storing to same series', sameSeries);
const { instance } = sameSeries;
const {
SeriesInstanceUID,
SeriesDescription,
SeriesDate,
SeriesTime,
SeriesNumber,
Modality,
} = instance;
return {
SeriesInstanceUID,
SeriesDescription,
SeriesDate,
SeriesTime,
SeriesNumber,
Modality,
InstanceNumber: sameSeries.others.length + 1,
};
}
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
return { SeriesDescription, SeriesNumber };
}
+1 -1
View File
@@ -30,7 +30,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+1 -1
View File
@@ -30,7 +30,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+3 -3
View File
@@ -32,10 +32,10 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.3.2",
"@cornerstonejs/core": "0.30.1",
"@cornerstonejs/tools": "0.39.0",
"@cornerstonejs/core": "^0.30.1",
"@cornerstonejs/tools": "^0.44.0",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+1 -1
View File
@@ -30,7 +30,7 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"@ohif/ui": "^2.0.0",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",