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 2023-02-17 16:44:21 -05:00 committed by GitHub
parent 926290f69e
commit 67fc3f733d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1277 additions and 218 deletions

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"
}
}

View File

@ -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');

View File

@ -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) {

View File

@ -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: {},
});
}

View File

@ -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: {},
});
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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) {

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",

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 };

View File

@ -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,

View File

@ -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;
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1 +1,11 @@
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
export default [
'Length',
'EllipticalROI',
'Bidirectional',
'ArrowAnnotate',
'Angle',
'CobbAngle',
'Probe',
'RectangleROI',
'PlanarFreehandROI',
];

View File

@ -0,0 +1,3 @@
import * as measurementMappingUtils from './utils';
export { measurementMappingUtils };

View File

@ -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;

View File

@ -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,
};

View File

@ -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,
};

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",

View File

@ -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) {

View File

@ -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 };
}

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",

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",

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",

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",

View File

@ -35,6 +35,8 @@ function initDefaultToolGroup(
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.Magnify },
{ toolName: toolNames.SegmentationDisplay },
],
@ -173,6 +175,8 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.PlanarFreehandROI },
{ toolName: toolNames.SegmentationDisplay },
],
disabled: [

View File

@ -493,6 +493,38 @@ const toolbarButtons = [
],
'Angle'
),
// Next two tools can be added once icons are added
// _createToolButton(
// 'Cobb Angle',
// 'tool-cobb-angle',
// 'Cobb Angle',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'CobbAngle',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Cobb Angle'
// ),
// _createToolButton(
// 'Planar Freehand ROI',
// 'tool-freehand',
// 'PlanarFreehandROI',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'PlanarFreehandROI',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Planar Freehand ROI'
// ),
_createToolButton(
'Magnify',
'tool-magnify',

View File

@ -34,6 +34,7 @@ function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) {
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.Magnify },
],
enabled: [{ toolName: toolNames.SegmentationDisplay }],
@ -149,6 +150,7 @@ function initMPRToolGroup(toolNames, Enums, toolGroupService, commandsManager) {
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.StackScroll },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.SegmentationDisplay },
],
disabled: [{ toolName: toolNames.Crosshairs }],

View File

@ -37,8 +37,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"dcmjs": "0.29.4",
"dicomweb-client": "^0.6.0",
"dcmjs": "^0.29.4",
"dicomweb-client": "^0.8.4",
"isomorphic-base64": "^1.0.2",
"lodash.merge": "^4.6.1",
"lodash.clonedeep": "^4.5.0",

View File

@ -11,7 +11,7 @@ export const polygonRoi = {
id: 'PolygonRoi',
name: 'Polygon',
toolGroup: 'allTools',
cornerstoneToolType: 'FreehandRoiTool',
cornerstoneToolType: 'PlanarFreehandROITool',
options: {
measurementTable: {
displayFunction,

View File

@ -71,6 +71,7 @@ const EVENTS = {
};
const VALUE_TYPES = {
ANGLE: 'value_type::polyline',
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
@ -103,18 +104,15 @@ class MeasurementService extends PubSubService {
},
};
public static VALUE_TYPES = VALUE_TYPES;
public readonly VALUE_TYPES = VALUE_TYPES;
constructor() {
super(EVENTS);
this.sources = {};
this.mappings = {};
this.measurements = {};
this._jumpToMeasurementCache = {};
Object.defineProperty(this, 'VALUE_TYPES', {
value: VALUE_TYPES,
writable: false,
enumerable: true,
configurable: false,
});
}
/**
@ -529,6 +527,7 @@ class MeasurementService extends PubSubService {
measurement = toMeasurementSchema(sourceAnnotationDetail);
measurement.source = source;
} catch (error) {
console.log('Failed to map', error);
throw new Error(
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
);

View File

@ -66,8 +66,7 @@
"core-js": "^3.16.1",
"cornerstone-math": "^0.1.9",
"cornerstone-wado-image-loader": "^4.2.1",
"dayjs": "^1.11.6",
"dcmjs": "0.29.4",
"dcmjs": "^0.29.4",
"detect-gpu": "^4.0.16",
"dicom-parser": "^1.8.9",
"dotenv-webpack": "^1.7.0",

@ -1 +1 @@
Subproject commit 4d59660c2883ed749a680e5fb6d4624ab54c9422
Subproject commit 1bca96aa2aeae7d90b3c8db73845c85dd27b34d5

View File

@ -1407,10 +1407,10 @@
resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@cornerstonejs/adapters/-/adapters-0.3.0.tgz#c0a2a904c92ebfae77520eba4fc64c07ebd06db4"
integrity sha512-o3GIrccNM0q/QF0//ueVIXeRltUmtTBkTR47qPgIY9gdi+3sYEFHEW6RTXe3OOtDDXOWgM88Jp3mWWUK4ZeXxg==
"@cornerstonejs/adapters@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-0.3.1.tgz#50cfaf3de3c5a55f22f7fae1a9f6584a1c343ffa"
integrity sha512-+tqMLVYeSZjzwtzbAPqRMq5iI47nrtXz4f3GQ1jKUSmZqQyvgWRzAizcfnl6OEIrgjeg/2MHdSGOEWu12H8M+w==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
dcmjs "^0.29.4"
@ -1443,7 +1443,15 @@
resolved "https://registry.npmjs.org/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81"
integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng==
"@cornerstonejs/core@0.30.1", "@cornerstonejs/core@^0.30.1":
"@cornerstonejs/core@^0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.25.1.tgz#7e5c70858018d99417b61965ac606754542f77ad"
integrity sha512-/Ve9qwGyRRK1uUCrefh9cVG3OIaaS9LQoTUk9YMUbsoWxd6JitXh31XVHjlvNtBYGMRVdz+uGPS2zSxq1vi4aA==
dependencies:
detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0"
"@cornerstonejs/core@^0.30.1":
version "0.30.1"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.30.1.tgz#6cb7088260641e7e55de2edb11979b1a0c00c794"
integrity sha512-A7t6WtPut17uLGpv3cUfk1gSzEvWiFcCg1K3H0WGySdZsf4tIinb5xA+9oD2eaBPbvTXmIef4HWFIAsN3kAEgg==
@ -1451,28 +1459,28 @@
detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0"
"@cornerstonejs/core@^0.28.0":
version "0.28.0"
resolved "https://registry.npmjs.org/@cornerstonejs/core/-/core-0.28.0.tgz#bc5114dfd332fcc5caf4ab049bcb08f6457e5f97"
integrity sha512-DSAdH2+8h/rRCTR6WbbGkY+vqnAVVZH47+GgGQQHscEwDD/K3ZO62JMl4LgcgkDzI0LIIZAk6CgAtok31yG/3Q==
"@cornerstonejs/core@^0.31.0":
version "0.31.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.31.0.tgz#087731cc9c1745f02e8936a55956c4ca5d52f3d9"
integrity sha512-qJmR2JpPPuSJsIDW7seH+bJrhE/e5QNwuk2T8EzlWtHR3QcB2lSq/o7cX6kGOJDwn4KrE1p/9QZAJ4GaFlBoyQ==
dependencies:
detect-gpu "^4.0.45"
lodash.clonedeep "4.5.0"
"@cornerstonejs/streaming-image-volume-loader@^0.11.2":
version "0.11.8"
resolved "https://registry.npmjs.org/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.11.8.tgz#79dc9bf7f00a3277d9880f919c324cdeda2c28d6"
integrity sha512-fp+mmCUMWQQFzx9AzzmOJO1ntBStHQmxK3Zbi/sMs9TYcue+Zw14oA+qcuJ/7UZayb+c99unfWjhafjBiHWNeg==
"@cornerstonejs/streaming-image-volume-loader@^0.8.2":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.8.3.tgz#9370f1a609c5d83b3be59af2a0f44c57f68efdb0"
integrity sha512-p4kQisqQM/hD5yr/TZKSlq2Ux6eOJteuJrJRNaub46bGklU5M2J0TPShEkwpMDPIJeir9hNeQyg7q8fy5IoH+A==
dependencies:
"@cornerstonejs/core" "^0.30.1"
cornerstone-wado-image-loader "^4.8.0"
"@cornerstonejs/core" "^0.25.1"
cornerstone-wado-image-loader "^4.7.0"
"@cornerstonejs/tools@0.39.0":
version "0.39.0"
resolved "https://registry.npmjs.org/@cornerstonejs/tools/-/tools-0.39.0.tgz#7d9bac4c55bc19145c2051f7c7150b34570849e4"
integrity sha512-xDR51lOEuNIDaTJltZvxDK+BIdIbznXUsWt+OpYiiygCQYt1Lt2eHufO2wVFVPrMgvm0dryF+WQbRVwtWvx4yA==
"@cornerstonejs/tools@^0.44.0":
version "0.44.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.44.0.tgz#7bec3dbcd25fce648cab49021c67ca2e3a80b1f1"
integrity sha512-/LfxjXwR4o1qzsByk4pf3vgRK+4BJGkth1G+dqYLj06iUsDYX724kR6AodEFZniwXUiSCZO7JDYOyns6fk7+GQ==
dependencies:
"@cornerstonejs/core" "^0.28.0"
"@cornerstonejs/core" "^0.31.0"
lodash.clonedeep "4.5.0"
lodash.get "^4.4.2"
@ -8544,7 +8552,7 @@ cornerstone-math@^0.1.9:
resolved "https://registry.npmjs.org/cornerstone-math/-/cornerstone-math-0.1.10.tgz#a3f99db64d73c5adee61ae0d570128eca1682d07"
integrity sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ==
cornerstone-wado-image-loader@^4.2.1, cornerstone-wado-image-loader@^4.8.0:
cornerstone-wado-image-loader@^4.2.1, cornerstone-wado-image-loader@^4.7.0:
version "4.9.1"
resolved "https://registry.npmjs.org/cornerstone-wado-image-loader/-/cornerstone-wado-image-loader-4.9.1.tgz#442eeca78ff0bcf9f9cbde04e8c429f35b31c55a"
integrity sha512-l0HRxGAupfufnHjT9uFpwAtafvuGMKKB9SCL8dHUwCK+7jwNeVigy8s6+Oki2nycaJUzy2TRQaauO78mIf3grg==
@ -9259,12 +9267,12 @@ dateformat@^3.0.0:
resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dayjs@^1.10.4, dayjs@^1.11.6:
dayjs@^1.10.4:
version "1.11.7"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
dcmjs@0.29.4, dcmjs@^0.29.4:
dcmjs@^0.29.4:
version "0.29.4"
resolved "https://registry.npmjs.org/dcmjs/-/dcmjs-0.29.4.tgz#3fb13945611979f756bc91b3f199fd87b4eabdb4"
integrity sha512-PzD6C4oB7v/AVTIPbkS6D6hjgVcSW7+T+DsxFpkD1xcEgU4qjGM16WGEHH3ZAPDBF0Xgm2d9FXUu/lXZIyfmow==
@ -9649,10 +9657,10 @@ dicom-parser@^1.8.9:
resolved "https://registry.npmjs.org/dicom-parser/-/dicom-parser-1.8.20.tgz#e5ef817d80d2fbc093ffd8c2a73a1cb5b47f50bd"
integrity sha512-R8NXEcaqXu7Qe5exY662aXiJCbiE6fEF8+QpSkVcXTTecFzfIaW8KJ2tSwYvt0UMHP0AUBW13VxQSXfKv/0FUA==
dicomweb-client@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/dicomweb-client/-/dicomweb-client-0.6.0.tgz#5e35ada52fe0155af1cc1f0e84f9c7f76477f92d"
integrity sha512-VAkBg4W6odIo2XsFxqjN/rptd7bQ8oHpRuKH5d46E9BUIPzRschazE8Dx1xg7/l3N3f1M70jB7yJ339arAXiDQ==
dicomweb-client@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.8.4.tgz#3da814cedb9415facb50bc5f43af8d961a991c74"
integrity sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw==
didyoumean@^1.2.2:
version "1.2.2"