fix(cornerstone-dicom-sr): Freehand SR hydration support (#3996)

Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: IbrahimCSAE <ibrahim.mdev@gmail.com>
This commit is contained in:
Igor Octaviano 2024-04-05 06:42:43 -03:00 committed by GitHub
parent 6e5892d202
commit 5645ac1b27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 366 additions and 237 deletions

View File

@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/adapters": "^1.68.1",
"@cornerstonejs/core": "^1.68.1",
"@kitware/vtk.js": "29.7.0",
"react-color": "^2.19.3"
}

View File

@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@cornerstonejs/adapters": "^1.68.1",
"@cornerstonejs/core": "^1.68.1",
"@cornerstonejs/tools": "^1.68.1",
"classnames": "^2.3.2"
}
}

View File

@ -1,6 +1,6 @@
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
import addMeasurement from './utils/addMeasurement';
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
import isRehydratable from './utils/isRehydratable';
import { adaptersSR } from '@cornerstonejs/adapters';
@ -150,13 +150,37 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
return [displaySet];
}
function _load(displaySet, servicesManager, extensionManager) {
async function _load(displaySet, servicesManager, extensionManager) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
async function retrieveBulkData(obj, parentObj = null, key = null) {
for (const prop in obj) {
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
await retrieveBulkData(obj[prop], obj, prop);
} else if (Array.isArray(obj[prop])) {
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
} else if (prop === 'BulkDataURI') {
const value = await dataSource.retrieve.bulkDataURI({
BulkDataURI: obj[prop],
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
});
if (parentObj && key) {
parentObj[key] = new Float32Array(value);
}
}
}
}
if (displaySet.isLoaded !== true) {
await retrieveBulkData(ContentSequence);
}
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
@ -211,7 +235,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
return;
}
if (!newDisplaySet instanceof ImageSet) {
if ((!newDisplaySet) instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
@ -275,7 +299,22 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
const frame =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
/** Update measurement properties */
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frame;
delete measurement.coords;
unloadedMeasurements.splice(j, 1);
}
@ -291,7 +330,7 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
// Standard. But for now, we will support only one ReferenceFrameNumber.
const ReferencedFrameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {

View File

@ -1,6 +1,6 @@
import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule, { srProtocol } from './getHangingProtocolModule';
import { srProtocol } from './getHangingProtocolModule';
import onModeEnter from './onModeEnter';
import getCommandsModule from './commandsModule';
import preRegistration from './init';

View File

@ -1,5 +1,4 @@
import {
addTool,
AngleTool,
annotation,
ArrowAnnotateTool,
@ -9,6 +8,7 @@ import {
CircleROITool,
LengthTool,
PlanarFreehandROITool,
RectangleROITool,
} from '@cornerstonejs/tools';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import addToolInstance from './utils/addToolInstance';
@ -19,24 +19,25 @@ 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.DICOMSRDisplay, DICOMSRDisplayTool);
addToolInstance(toolNames.SRLength, LengthTool);
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
addToolInstance(toolNames.SRCircleROI, CircleROITool);
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
addToolInstance(toolNames.SRAngle, AngleTool);
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
// 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', {
[toolNames.DICOMSRDisplay]: dashedLine,
SRLength: dashedLine,
SRBidirectional: dashedLine,
SREllipticalROI: dashedLine,
@ -45,6 +46,7 @@ export default function init({ configuration = {} }: Types.Extensions.ExtensionP
SRCobbAngle: dashedLine,
SRAngle: dashedLine,
SRPlanarFreehandROI: dashedLine,
SRRectangleROI: dashedLine,
global: {},
});
}

View File

@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
}
_getTextBoxLinesFromLabels(labels) {
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 3);
const labelLength = Math.min(labels.length, 5);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
}
return lines;
@ -77,6 +77,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
const { style: annotationStyle } = annotation.config;
for (let i = 0; i < filteredAnnotations.length; i++) {
const annotation = filteredAnnotations[i];
@ -87,6 +88,10 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
styleSpecifier.annotationUID = annotationUID;
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
this.getToolName()
];
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
const color =
@ -98,6 +103,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
color,
lineDash,
lineWidth,
...groupStyle,
};
Object.keys(renderableData).forEach(GraphicType => {
@ -160,6 +166,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
const drawingOptions = {
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
};
let allCanvasCoordinates = [];
renderableData.map((data, index) => {
@ -307,6 +314,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
{
color: options.color,
width: options.lineWidth,
lineDash: options.lineDash,
}
);
});

View File

@ -1,15 +1,13 @@
import { vec3 } from 'gl-matrix';
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
import toolNames from '../tools/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
const EPSILON = 1e-4;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
// TODO -> Render rotated ellipse .
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
const toolName = toolNames.DICOMSRDisplay;
const measurementData = {
@ -27,26 +25,25 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
}
measurementData.renderableData[GraphicType].push(
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
_getRenderableData(GraphicType, GraphicData, imageId)
);
});
// Use the metadata provider to grab its imagePlaneModule metadata
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const annotationManager = annotation.state.getAnnotationManager();
// Create Cornerstone3D Annotation from measurement
const frameNumber =
(measurement.coords[0].ReferencedSOPSequence &&
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
1;
/**
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
* This is used before the annotation is hydrated. If hydrated the measurement will be added
* to the measurement service and will be available for the other viewports.
*/
const SRAnnotation: Types.Annotation = {
annotationUID: measurement.TrackingUniqueIdentifier,
highlighted: false,
isLocked: false,
invalidated: false,
metadata: {
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
toolName: toolName,
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
referencedImageId: imageId,
},
data: {
@ -58,28 +55,14 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
renderableData: measurementData.renderableData,
},
frameNumber: frameNumber,
frameNumber,
},
};
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation(SRAnnotation);
measurement.loaded = true;
measurement.imageId = imageId;
measurement.displaySetInstanceUID = displaySetInstanceUID;
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
// It'd be super weird if it didn't anyway as a SCOORD.
measurement.ReferencedSOPInstanceUID =
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
measurement.frameNumber = frameNumber;
delete measurement.coords;
}
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
function _getRenderableData(GraphicType, GraphicData, imageId) {
let renderableData: csTypes.Point3[];
switch (GraphicType) {

View File

@ -125,6 +125,10 @@ function OHIFCornerstoneSRViewport(props) {
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
// return;
// }
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@ -253,12 +257,16 @@ function OHIFCornerstoneSRViewport(props) {
* if it is hydrated we don't even use the SR viewport.
*/
useEffect(() => {
if (!srDisplaySet.isLoaded) {
srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
}, [srDisplaySet]);
const loadSR = async () => {
if (!srDisplaySet.isLoaded) {
await srDisplaySet.load();
}
const numMeasurements = srDisplaySet.measurements.length;
setMeasurementCount(numMeasurements);
updateViewport(measurementSelected);
};
loadSR();
}, [dataSource, srDisplaySet]);
/**
* Hook to update the tracking identifiers when the selected measurement changes or
@ -275,19 +283,11 @@ function OHIFCornerstoneSRViewport(props) {
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
/**
* Data fetching for the SR displaySet, which updates the measurements and
* also gets the referenced image displaySet that SR is based on.
*/
useEffect(() => {
updateViewport(measurementSelected);
}, [dataSource, srDisplaySet]);
useEffect(() => {
viewportActionCornersService.setComponents([
{

View File

@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.67.0",
"@cornerstonejs/dicom-image-loader": "^1.68.1",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",
@ -55,10 +55,10 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.67.0",
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@cornerstonejs/adapters": "^1.68.1",
"@cornerstonejs/core": "^1.68.1",
"@cornerstonejs/streaming-image-volume-loader": "^1.68.1",
"@cornerstonejs/tools": "^1.68.1",
"@kitware/vtk.js": "29.7.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",

View File

@ -357,6 +357,10 @@ const connectMeasurementServiceToTools = (
imageId = dataSource.getImageIdsForInstance({ instance });
}
/**
* This annotation is used by the cornerstone viewport.
* This is not the read-only annotation rendered by the SR viewport.
*/
const annotationManager = annotation.state.getAnnotationManager();
annotationManager.addAnnotation({
annotationUID: measurement.uid,
@ -369,11 +373,16 @@ const connectMeasurementServiceToTools = (
referencedImageId: imageId,
},
data: {
/**
* Don't remove this destructuring of data here.
* This is used to pass annotation specific data forward e.g. contour
*/
...(data.annotation.data || {}),
text: data.annotation.data.text,
handles: { ...data.annotation.data.handles },
cachedStats: { ...data.annotation.data.cachedStats },
label: data.annotation.data.label,
frameNumber: frameNumber,
frameNumber,
},
});
}

View File

@ -1,14 +1,22 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { getDisplayUnit } from './utils';
import { utils } from '@ohif/core';
/**
* Represents a mapping utility for Planar Freehand ROI measurements.
*/
const PlanarFreehandROI = {
toAnnotation: measurement => {},
/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
* @param {Object} csToolsEventDetail Cornerstone event data
* @param {DisplaySetService} DisplaySetService Service for managing display sets
* @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports
* @param {Function} getValueTypeFromToolType Function to get value type from tool type
* @returns {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
@ -16,7 +24,7 @@ const PlanarFreehandROI = {
CornerstoneViewportService,
getValueTypeFromToolType
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
if (!metadata || !data) {
@ -26,19 +34,14 @@ const PlanarFreehandROI = {
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
const validToolType = SUPPORTED_TOOLS.includes(toolName);
if (!validToolType) {
throw new Error('Tool not supported');
throw new Error(`Tool ${toolName} not supported`);
}
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(
referencedImageId,
CornerstoneViewportService,
viewportId
);
const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } =
getSOPInstanceAttributes(referencedImageId);
let displaySet;
if (SOPInstanceUID) {
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
@ -48,92 +51,151 @@ const PlanarFreehandROI = {
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
}
const { points, textBox } = data.handles;
const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService);
const displayText = getDisplayText(mappedAnnotations);
const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID);
return {
uid: annotationUID,
SOPInstanceUID,
FrameOfReferenceUID,
points,
textBox,
points: data.contour.polyline,
textBox: data.handles.textBox,
metadata,
frameNumber,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.label,
displayText: displayText,
data: { ...data, ...data.cachedStats },
displayText: getDisplayText(annotation, displaySet),
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),
getReport,
getReport: () => getColumnValueReport(annotation),
};
},
};
/**
* 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).
* This function is used to convert the measurement data to a
* format that is suitable for 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.
*
* @param {object} annotation
* @returns {object} Report's content from this tool
*/
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
function getColumnValueReport(annotation) {
const columns = [];
const values = [];
return {
columns,
values,
};
/** Add type */
columns.push('AnnotationType');
values.push('Cornerstone:PlanarFreehandROI');
/** Add cachedStats */
const { metadata, data } = annotation;
const { mean, stdDev, max, area, unit, areaUnit, perimeter } =
data.cachedStats[`imageId:${metadata.referencedImageId}`];
columns.push(`Maximum`, `Mean`, `Std Dev`, 'Pixel Unit', `Area`, 'Unit', 'Perimeter');
values.push(max, mean, stdDev, unit, area, areaUnit, perimeter);
/** Add FOR */
if (metadata.FrameOfReferenceUID) {
columns.push('FrameOfReferenceUID');
values.push(metadata.FrameOfReferenceUID);
}
/** Add points */
if (data.contour.polyline) {
/**
* 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
*/
columns.push('points');
values.push(data.contour.polyline.map(p => p.join(' ')).join(';'));
}
return { columns, values };
}
function getDisplayText(mappedAnnotations) {
return '';
/**
* Retrieves the display text for an annotation in a display set.
*
* @param {Object} annotation - The annotation object.
* @param {Object} displaySet - The display set object.
* @returns {string[]} - An array of display text.
*/
function getDisplayText(annotation, displaySet) {
const { metadata, data } = annotation;
if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) {
return [];
}
const { mean, stdDev, max, area, modalityUnit, areaUnit, perimeter } =
data.cachedStats[`imageId:${metadata.referencedImageId}`];
const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId);
const displayText = [];
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}` : '';
const { SeriesNumber } = displaySet;
if (SeriesNumber) {
displayText.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
}
if (area) {
/**
* Add Area
* Area sometimes becomes undefined if `preventHandleOutsideImage` is off
*/
const roundedArea = utils.roundNumber(area || 0, 2);
displayText.push(`Area: ${roundedArea} ${getDisplayUnit(areaUnit)}`);
}
if (mean) {
if (Array.isArray(mean)) {
const meanValues = mean.map(value => utils.roundNumber(value));
displayText.push(`Mean: ${meanValues.join(', ')} ${modalityUnit}`);
} else {
displayText.push(`Mean: ${utils.roundNumber(mean)} ${modalityUnit}`);
}
}
if (max) {
if (Array.isArray(max)) {
const maxValues = max.map(value => utils.roundNumber(value, 2));
displayText.push(`Max: ${maxValues.join(', ')} ${modalityUnit}`);
} else {
displayText.push(`Max: ${utils.roundNumber(max, 2)} ${modalityUnit}`);
}
}
if (stdDev) {
if (Array.isArray(stdDev)) {
const stdDevValues = stdDev.map(value => utils.roundNumber(value));
displayText.push(`Std Dev: ${stdDevValues.join(', ')} ${modalityUnit}`);
} else {
displayText.push(`Std Dev: ${utils.roundNumber(stdDev)} ${modalityUnit}`);
}
}
if (perimeter) {
if (Array.isArray(perimeter)) {
const perimeterValues = perimeter.map(value => utils.roundNumber(value));
displayText.push(`Perimeter: ${perimeterValues.join(', ')} ${modalityUnit}`);
} else {
displayText.push(`Perimeter: ${utils.roundNumber(perimeter)} ${modalityUnit}`);
}
}
return displayText;
}
export default PlanarFreehandROI;

View File

@ -32,8 +32,8 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^1.67.0",
"@cornerstonejs/tools": "^1.67.0",
"@cornerstonejs/core": "^1.68.1",
"@cornerstonejs/tools": "^1.68.1",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.71",
"@ohif/ui": "3.8.0-beta.71",

View File

@ -183,60 +183,69 @@ function TrackedMeasurementsContextProvider(
// ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle
useEffect(() => {
if (viewports.size > 0) {
const activeViewport = viewports.get(activeViewportId);
const triggerPromptHydrateFlow = async () => {
if (viewports.size > 0) {
const activeViewport = viewports.get(activeViewportId);
if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) {
return;
if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) {
return;
}
// Todo: Getting the first displaySetInstanceUID is wrong, but we don't have
// tracking fusion viewports yet. This should change when we do.
const { displaySetService } = servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
if (!displaySet) {
return;
}
// If this is an SR produced by our SR SOPClassHandler,
// and it hasn't been loaded yet, do that now so we
// can check if it can be rehydrated or not.
//
// Note: This happens:
// - If the viewport is not currently an OHIFCornerstoneSRViewport
// - If the displaySet has never been hung
//
// Otherwise, the displaySet will be loaded by the useEffect handler
// listening to displaySet changes inside OHIFCornerstoneSRViewport.
// The issue here is that this handler in TrackedMeasurementsContext
// ends up occurring before the Viewport is created, so the displaySet
// is not loaded yet, and isRehydratable is undefined unless we call load().
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
!displaySet.isLoaded &&
displaySet.load
) {
await displaySet.load();
}
// Magic string
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
displaySet.isRehydratable === true
) {
console.log('sending event...', trackedMeasurements);
sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
SeriesInstanceUID: displaySet.SeriesInstanceUID,
viewportId: activeViewportId,
});
}
}
// Todo: Getting the first displaySetInstanceUID is wrong, but we don't have
// tracking fusion viewports yet. This should change when we do.
const { displaySetService } = servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
if (!displaySet) {
return;
}
// If this is an SR produced by our SR SOPClassHandler,
// and it hasn't been loaded yet, do that now so we
// can check if it can be rehydrated or not.
//
// Note: This happens:
// - If the viewport is not currently an OHIFCornerstoneSRViewport
// - If the displaySet has never been hung
//
// Otherwise, the displaySet will be loaded by the useEffect handler
// listening to displaySet changes inside OHIFCornerstoneSRViewport.
// The issue here is that this handler in TrackedMeasurementsContext
// ends up occurring before the Viewport is created, so the displaySet
// is not loaded yet, and isRehydratable is undefined unless we call load().
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
!displaySet.isLoaded &&
displaySet.load
) {
displaySet.load();
}
// Magic string
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
displaySet.isRehydratable === true
) {
console.log('sending event...', trackedMeasurements);
sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', {
displaySetInstanceUID: displaySet.displaySetInstanceUID,
SeriesInstanceUID: displaySet.SeriesInstanceUID,
viewportId: activeViewportId,
});
}
}
}, [activeViewportId, sendTrackedMeasurementsEvent, servicesManager.services, viewports]);
};
triggerPromptHydrateFlow();
}, [
trackedMeasurements,
activeViewportId,
sendTrackedMeasurementsEvent,
servicesManager.services,
viewports,
]);
return (
<TrackedMeasurementsContext.Provider

View File

@ -19,7 +19,7 @@ function PanelStudyBrowserTracking({
requestDisplaySetCreationForStudy,
dataSource,
}) {
const { displaySetService, uiDialogService, hangingProtocolService, uiNotificationService } =
const { displaySetService, uiDialogService, hangingProtocolService, uiNotificationService, measurementService } =
servicesManager.services;
const navigate = useNavigate();
@ -340,6 +340,12 @@ function PanelStudyBrowserTracking({
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
});
const measurements = measurementService.getMeasurements();
measurements.forEach(m => {
if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) {
measurementService.remove(m.uid);
}
});
}}
onClickThumbnail={() => {}}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}

View File

@ -129,6 +129,8 @@ function initSRToolGroup(extensionManager, toolGroupService) {
{ toolName: SRToolNames.SRBidirectional },
{ toolName: SRToolNames.SREllipticalROI },
{ toolName: SRToolNames.SRCircleROI },
{ toolName: SRToolNames.SRPlanarFreehandROI },
{ toolName: SRToolNames.SRRectangleROI },
],
enabled: [
{

View File

@ -155,14 +155,6 @@ const moreTools = [
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'RectangleROI',
icon: 'tool-rectangle',
label: 'Rectangle',
tooltip: 'Rectangle',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'CalibrationLine',
icon: 'tool-calibration',

View File

@ -67,6 +67,22 @@ const toolbarButtons: Button[] = [
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'PlanarFreehandROI',
icon: 'tool-freehand-polygon',
label: 'Freehand',
tooltip: 'Freehand ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'RectangleROI',
icon: 'tool-rectangle',
label: 'Rectangle',
tooltip: 'Rectangle ROI',
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
}),
createButton({
id: 'CircleROI',
icon: 'tool-circle',

View File

@ -53,7 +53,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^1.67.0",
"@cornerstonejs/dicom-image-loader": "^1.68.1",
"@emotion/serialize": "^1.1.3",
"@ohif/core": "3.8.0-beta.71",
"@ohif/extension-cornerstone": "3.8.0-beta.71",

View File

@ -37,7 +37,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.67.0",
"@cornerstonejs/dicom-image-loader": "^1.68.1",
"@ohif/ui": "3.8.0-beta.71",
"cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21"

View File

@ -53,6 +53,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'area', // TODO: Add concept names instead (descriptor)
'mean',
'stdDev',
'perimeter',
'length',
'shortestDiameter',
'longestDiameter',

View File

@ -1582,13 +1582,13 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^1.67.0":
version "1.67.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.67.0.tgz#84c881a2b1c7bc3cec2ae55df869d82740cc6049"
integrity sha512-EnIpdMD7kx42eJYGHsqjahWTZj/ZTkP+5XCWXpLp0BY0p+O53oSV9aYZXvyjd/kWQPr+30L+010zPuE5oQzIxA==
"@cornerstonejs/adapters@^1.68.1":
version "1.68.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.68.1.tgz#9f4fa768f85759bb49e8f252c9a4e5b4acd09b1f"
integrity sha512-p4pKMX70/COLf1dSIDUonEBHhSPYklH2OkS9yOmdjOVYzvWxyaQWuczd1V+2cWYanD5WGqRzarl4KpJAK9W+zQ==
dependencies:
"@babel/runtime-corejs2" "^7.17.8"
"@cornerstonejs/tools" "^1.67.0"
"@cornerstonejs/tools" "^1.68.1"
buffer "^6.0.3"
dcmjs "^0.29.8"
gl-matrix "^3.4.3"
@ -1635,10 +1635,10 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.5.tgz#8690b61a86fa53ef38a70eee9d665a79229517c0"
integrity sha512-MZCUy8VG0VG5Nl1l58+g+kH3LujAzLYTfJqkwpWI2gjSrGXnP6lgwyy4GmPRZWVoS40/B1LDNALK905cNWm+sg==
"@cornerstonejs/core@^1.67.0":
version "1.67.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.67.0.tgz#711a915b70b5273d8838b59c8c06c553079b0ee7"
integrity sha512-t8VI5acGGu0wN51UzHyEXOjlBjoAJjIVcbBOQejC0yOWfGoy+I9y/h5de41pZyhfbXYNNXA1dHZDDtLi9sauBg==
"@cornerstonejs/core@^1.68.1":
version "1.68.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.68.1.tgz#f5d366924b791b117d77e2fe83cee82dced0f3fa"
integrity sha512-F+ptRQ4kYPLS1buDvpQhy3ckdJa4K4B7uz3Ujq3BaEw1AZPB01S6OFw6HtbYs8i+kIi8ZF1fdWvDheO0bpefBQ==
dependencies:
"@kitware/vtk.js" "29.7.0"
comlink "^4.4.1"
@ -1646,34 +1646,34 @@
gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0"
"@cornerstonejs/dicom-image-loader@^1.67.0":
version "1.67.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.67.0.tgz#9e70858100821682105f88a927aa4dff96e75cda"
integrity sha512-ZhziYYKcci1X11iWA4qOzEVxtVUbDUKqx9ustsAD9vv9P7WfDOI4EHflUhW+X1hIAG5vl7xv6LAG0aRkVnwrrw==
"@cornerstonejs/dicom-image-loader@^1.68.1":
version "1.68.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.68.1.tgz#fd2cda6de4060ade8092a0fc887f32d4ce979684"
integrity sha512-xjmp2Izy4KMPUTZVutdnx0TXhFrLmlJP3ZZ54ypjJyKGdv4EIGyDvGmCHkD7G37dYROTbZJVBNtjukCrfvJ1Zg==
dependencies:
"@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
"@cornerstonejs/codec-openjpeg" "^1.2.2"
"@cornerstonejs/codec-openjph" "^2.4.5"
"@cornerstonejs/core" "^1.67.0"
"@cornerstonejs/core" "^1.68.1"
dicom-parser "^1.8.9"
pako "^2.0.4"
uuid "^9.0.0"
"@cornerstonejs/streaming-image-volume-loader@^1.67.0":
version "1.67.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.67.0.tgz#5e22c828e5a5c9932e728314edbfa95abf81982e"
integrity sha512-W5+CKSxkLI4BbcdXYVb/M+yeN6Z+G8AWOQISR8qX9NGnK3GR/Id7+9OH+HP6FmIZ4YBXew9/DOqRgEtTtyDZGQ==
"@cornerstonejs/streaming-image-volume-loader@^1.68.1":
version "1.68.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.68.1.tgz#93ab87a0b77717200c7ea35ddb832651f02882de"
integrity sha512-4n0oNuf+SNxVfZ6gr5CwSgKpu5f3S3+uFxzRcvUH5DMXwVGxwU1XF8CTFfvEnw9xBn3x6Dg4ckR1a682CyuwXA==
dependencies:
"@cornerstonejs/core" "^1.67.0"
"@cornerstonejs/core" "^1.68.1"
comlink "^4.4.1"
"@cornerstonejs/tools@^1.67.0":
version "1.67.0"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.67.0.tgz#47c6459859cc5122f780a7ec67c2b9d41b067b38"
integrity sha512-CPFWRF4IePxHV8KoXE6PBY0Q3jw8riJ7ZaJajAzXLpgvTSvIYu0m7tNuFG8iKVppq6KTgSywO4r9VbfcvZXOyw==
"@cornerstonejs/tools@^1.68.1":
version "1.68.1"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.68.1.tgz#17e177be628bd633391566d8110b2799be7d5068"
integrity sha512-fbXIqcgJKa2RUmwLvxbbISSvwkfwZBoKCjtI/j0AMowpV/gpm86Br6/MomNmPQvdfuF5HfQRdBHjZS71Vid1pQ==
dependencies:
"@cornerstonejs/core" "^1.67.0"
"@cornerstonejs/core" "^1.68.1"
"@icr/polyseg-wasm" "0.4.0"
"@types/offscreencanvas" "2019.7.3"
comlink "^4.4.1"