feat: add support to scoord3d (#5016)

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
Pedro KöhlerandBill Wallace authored and GitHub committed 2025-07-31 00:21:26 -04:00
1 parent af40f06524
commit 735405a855
73 files changed
+1032 -993

No files matched your search

@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
@@ -46,8 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@kitware/vtk.js": "32.12.0",
"react-color": "^2.19.3"
}
+4 -4
View File
@@ -38,7 +38,7 @@
"@ohif/extension-cornerstone": "3.11.0-beta.114",
"@ohif/extension-measurement-tracking": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}
@@ -32,12 +32,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio
additionalFindingTypes
);
const report = MeasurementReport.generateReport(
filteredToolState,
metaData,
utilities.worldToImageCoords,
options
);
const report = MeasurementReport.generateReport(filteredToolState, metaData, options);
const { dataset } = report;
@@ -54,7 +49,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio
const commandsModule = (props: withAppTypes) => {
const { servicesManager, extensionManager, commandsManager } = props;
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
const { customizationService } = servicesManager.services;
const actions = {
changeColorMeasurement: ({ uid }) => {
@@ -129,6 +124,9 @@ const commandsModule = (props: withAppTypes) => {
console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content');
}
if (!naturalizedReport.SOPClassUID) {
throw new Error('No sop class uid');
}
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
@@ -63,18 +63,12 @@ function OHIFCornerstoneSRMeasurementViewport(props) {
const updateViewport = useCallback(
newMeasurementSelected => {
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
const { StudyInstanceUID, displaySetInstanceUID } = srDisplaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
// Todo: what happens if there are multiple SOP Classes? Why we are
// not throwing an error?
console.warn('More than one SOPClassUID in the same series is not yet supported.');
}
_getViewportReferencedDisplaySetData(
srDisplaySet,
newMeasurementSelected,
@@ -258,6 +252,9 @@ async function _getViewportReferencedDisplaySetData(
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!referencedDisplaySet?.images) {
return { referencedDisplaySetMetadata: null, referencedDisplaySet: null };
}
const image0 = referencedDisplaySet.images[0];
const referencedDisplaySetMetadata = {
@@ -15,8 +15,12 @@ import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
const { sopClassDictionary } = utils;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { ImageSet, MetadataProvider: metadataProvider } = classes;
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
const { MetadataProvider: metadataProvider } = classes;
const {
TEXT_ANNOTATION_POSITION,
COMMENT_CODE,
CodeScheme: Cornerstone3DCodeScheme,
} = adaptersSR.Cornerstone3D;
type InstanceMetadata = Types.InstanceMetadata;
@@ -216,6 +220,12 @@ async function _load(
});
}
function _measurementBelongsToDisplaySet({ measurement, displaySet }) {
return (
measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID
);
}
/**
* Checks if measurements can be added to a display set.
*
@@ -236,18 +246,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
measurement => measurement.loaded === false
);
if (
unloadedMeasurements.length === 0 ||
!(newDisplaySet instanceof ImageSet) ||
newDisplaySet.unsupported
) {
if (!unloadedMeasurements.length || newDisplaySet.unsupported) {
return;
}
// const { sopClassUids } = newDisplaySet;
// Create a Set for faster lookups
// const sopClassUidSet = new Set(sopClassUids);
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
const imageIdMap = new Map<string, string>();
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
@@ -266,6 +268,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
let measurement = unloadedMeasurements[j];
const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D';
const onBeforeSRAddMeasurement = customizationService.getCustomization(
'onBeforeSRAddMeasurement'
@@ -280,9 +283,15 @@ function _checkIfCanAddMeasurementsToDisplaySet(
}
// if it is 3d SR we can just add the SR annotation
if (is3DSR) {
if (
is3DSR &&
is3DMeasurement &&
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
) {
addSRAnnotation(measurement, null, null);
measurement.loaded = true;
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
unloadedMeasurements.splice(j, 1);
continue;
}
@@ -469,11 +478,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
* @returns {any} - The processed measurement result.
*/
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
return _processTID1410Measurement(mergedContentSequence);
}
@@ -511,12 +516,25 @@ function _processTID1410Measurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
const { ConceptNameCodeSequence: conceptNameItem } = graphicItem;
const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem;
const graphicCode = `${graphicDesignator}:${graphicValue}`;
const pointDataItem = _getCoordsFromSCOORDOrSCOORD3D(graphicItem);
const is3DMeasurement = pointDataItem.ValueType === 'SCOORD3D';
const pointLength = is3DMeasurement ? 3 : 2;
const pointsLength = pointDataItem.GraphicData.length / pointLength;
const measurement = {
loaded: false,
labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
coords: [pointDataItem],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
graphicCode,
is3DMeasurement,
pointsLength,
graphicType: pointDataItem.GraphicType,
};
NUMContentItems.forEach(item => {
@@ -567,6 +585,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
);
const commentSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator &&
item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value
);
const measurement = {
loaded: false,
labels: [],
@@ -575,6 +599,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
};
if (commentSites) {
for (const group of commentSites) {
if (group.TextValue) {
measurement.labels.push({ label: group.TextValue, value: '' });
}
}
}
if (
finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
@@ -726,4 +758,17 @@ function _getSequenceAsArray(sequence) {
return Array.isArray(sequence) ? sequence : [sequence];
}
function isScoordOr3d(group) {
return group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D';
}
function isTextPosition(group) {
const concept = group.ConceptNameCodeSequence[0];
return (
concept &&
concept.CodeValue === TEXT_ANNOTATION_POSITION.value &&
concept.CodingSchemeDesignator === TEXT_ANNOTATION_POSITION.schemeDesignator
);
}
export default getSopClassHandlerModule;
+1 -2
View File
@@ -12,7 +12,6 @@ import {
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@@ -57,7 +56,7 @@ export default function init({
'SRSCOORD3DPoint',
POINT,
SRSCOOR3DProbeMapper.toAnnotation,
SRSCOOR3DProbeMapper.toMeasurement
SRSCOOR3DProbeMapper.toMeasurement.bind(null, { servicesManager })
);
// Modify annotation tools to use dashed lines on SR
@@ -1,4 +1,4 @@
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
import { type Types } from '@cornerstonejs/core';
import {
annotation,
drawing,
@@ -7,13 +7,8 @@ const SRSCOOR3DProbe = {
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
toMeasurement: (
csToolsEventDetail,
displaySetService,
CornerstoneViewportService,
getValueTypeFromToolType,
customizationService
) => {
toMeasurement: ({ servicesManager, getValueTypeFromToolType }, csToolsEventDetail) => {
const { displaySetService } = servicesManager.services;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
@@ -22,14 +17,25 @@ const SRSCOOR3DProbe = {
return null;
}
const { toolName } = metadata;
const { toolName, FrameOfReferenceUID } = metadata;
const { points } = data.handles;
const displaySets = displaySetService
.getActiveDisplaySets()
.filter(ds => ds.FrameOfReferenceUID === FrameOfReferenceUID);
const displaySet = displaySets.filter(ds => ds.isReconstructable)[0] || displaySets[0];
const { StudyInstanceUID: referenceStudyUID, SeriesInstanceUID: referenceSeriesUID } =
displaySets[0] || {};
const displayText = getDisplayText(annotation);
return {
uid: annotationUID,
points,
metadata,
referenceStudyUID,
referenceSeriesUID,
displaySetInstanceUID: displaySet?.displaySetInstanceUID,
toolName: metadata.toolName,
label: data.label,
displayText: displayText,
@@ -56,7 +62,10 @@ function getDisplayText(annotation) {
}
}
return displayText;
return {
primary: displayText,
secondary: [],
};
}
export default SRSCOOR3DProbe;
@@ -1,9 +1,12 @@
import { Types, annotation } from '@cornerstonejs/tools';
import { metaData } from '@cornerstonejs/core';
import { adaptersSR } from '@cornerstonejs/adapters';
import getRenderableData from './getRenderableData';
import toolNames from '../tools/toolNames';
const { MeasurementReport } = adaptersSR.Cornerstone3D;
export default function addSRAnnotation(measurement, imageId, frameNumber) {
let toolName = toolNames.DICOMSRDisplay;
const renderableData = measurement.coords.reduce((acc, coordProps) => {
@@ -25,7 +28,12 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
}
if (valueType === 'SCOORD3D') {
toolName = toolNames.SRSCOORD3DPoint;
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(
measurement.TrackingIdentifier
);
if (!adapter) {
toolName = toolNames.SRSCOORD3DPoint;
}
// get the ReferencedFrameOfReferenceUID from the measurement
frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence;
@@ -1,17 +1,11 @@
import OHIF from '@ohif/core';
import { annotation } from '@cornerstonejs/tools';
const { log } = OHIF;
import { NO_IMAGE_ID } from '@cornerstonejs/adapters';
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
const filteredToolState = {};
function addToFilteredToolState(annotation, toolType) {
if (!annotation.metadata?.referencedImageId) {
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
return;
}
const imageId = annotation.metadata.referencedImageId;
const imageId = annotation.metadata?.referencedImageId ?? NO_IMAGE_ID;
if (!filteredToolState[imageId]) {
filteredToolState[imageId] = {};
@@ -11,7 +11,11 @@ const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
* @returns {string} The extracted label.
*/
export default function getLabelFromDCMJSImportedToolData(toolData) {
const { findingSites = [], finding } = toolData;
const { findingSites = [], finding, annotation } = toolData;
if (annotation.data.label) {
return annotation.data.label;
}
let freeTextLabel = findingSites.find(
fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
@@ -36,6 +36,14 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
ValueType,
imageId,
});
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
// We do not have an explicit draw circle svg helper in Cornerstone3D at
// this time, but we can use the ellipse svg helper to draw a circle, so
// here we reshape the data for that purpose.
@@ -90,6 +98,13 @@ function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) {
imageId,
});
if (!imageId) {
// without the image id it's not possible to perform the calculations below
// these calculations also do not seem to be needed, since everything works
// just fine when we skip them. At least for SCOORD3D annotations.
return pointsWorld;
}
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
@@ -1,15 +1,16 @@
import { utilities, metaData } from '@cornerstonejs/core';
import { utilities, metaData, type Types } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import { vec3 } from 'gl-matrix';
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
import { adaptersSR } from '@cornerstonejs/adapters';
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
import { annotation as CsAnnotation, type Types as ToolTypes } from '@cornerstonejs/tools';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
const { locking } = CsAnnotation;
const { guid } = OHIF.utils;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
const convertCode = (codingValues, code) => {
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
@@ -37,8 +38,10 @@ const convertSites = (codingValues, sites) => {
};
/**
* Hydrates a structured report, for default viewports.
*
* Hydrates a structured report
* Handles 2d and 3d hydration from SCOORD and SCOORD3D points
* For 3D hydration, chooses a volume display set to display with
* FOr 2D hydration, chooses the (first) display set containing the referenced image.
*/
export default function hydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager }: withAppTypes,
@@ -88,7 +91,7 @@ export default function hydrateStructuredReport(
// Mapping of legacy datasets is now directly handled by adapters module
const datasetToUse = instance;
// Use dcmjs to generate toolState.
// Use CS3D adapters to generate toolState.
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
@@ -96,7 +99,6 @@ export default function hydrateStructuredReport(
// that measurements were added to the display set are the same order as
// the measurementGroups in the instance.
sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData
);
@@ -147,6 +149,9 @@ export default function hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (!imageId) {
continue;
}
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
@@ -160,38 +165,55 @@ export default function hydrateStructuredReport(
}
}
/**
* Gets reference data for what frame of reference and the referenced
* image id, or for 3d measurements, the volumeId to apply this annotation to.
*/
function getReferenceData(toolData): ToolTypes.AnnotationMetadata {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
if (!imageId) {
return getReferenceData3D(toolData, servicesManager);
}
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
return {
referencedImageId: imageId,
FrameOfReferenceUID,
};
}
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
const imageId =
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
toolData.uid = guid();
const instance = metaData.get('instance', imageId);
const {
FrameOfReferenceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
const referenceData = getReferenceData(toolData);
const { imageId } = referenceData;
const annotation = {
annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data,
metadata: {
...referenceData,
toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
},
};
utilities.updatePlaneRestriction(annotation.data.handles.points, annotation.metadata);
const source = measurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
@@ -225,7 +247,7 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true);
}
if (!imageIds.includes(imageId)) {
if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
@@ -238,3 +260,92 @@ export default function hydrateStructuredReport(
SeriesInstanceUIDs,
};
}
/**
* For 3d annotations, there are often several display sets which could
* be used to display the annotation. Choose the first annotation with the
* same frame of reference that is reconstructable, or the first display set
* otherwise.
*/
function chooseDisplaySet(displaySets, annotation) {
if (!displaySets?.length) {
console.warn('No display set found for', annotation);
return;
}
if (displaySets.length === 1) {
return displaySets[0];
}
const volumeDs = displaySets.find(ds => ds.isReconstructable);
if (volumeDs) {
return volumeDs;
}
return displaySets[0];
}
/**
* Gets the additional reference data appropriate for a 3d reference.
* This will choose a volume id, frame of reference and a plane restriction.
*/
function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
const { FrameOfReferenceUID } = toolData.annotation.metadata;
const { points } = toolData.annotation.data.handles;
const { displaySetService } = servicesManager.services;
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID
);
if (!displaySetsFOR.length || !points?.length) {
return {
FrameOfReferenceUID,
};
}
const ds = chooseDisplaySet(displaySetsFOR, toolData.annotation);
const cameraView = chooseCameraView(ds, points);
const viewReference = {
...cameraView,
volumeId: ds.displaySetInstanceUID,
FrameOfReferenceUID,
};
utilities.updatePlaneRestriction(points, viewReference);
return viewReference;
}
/**
* Chooses a possible camera view - right now this is fairly basic,
* just setting the unknowns to null.
*/
function chooseCameraView(_ds, points) {
const selectedPoints = choosePoints(points);
const cameraFocalPoint = <Point3>centerOf(selectedPoints);
// These are sufficient to be null for now and can be set on first view
let viewPlaneNormal: Types.Point3 = null;
let viewUp: Types.Point3 = null;
return {
cameraFocalPoint,
viewPlaneNormal,
viewUp,
};
}
function centerOf(points) {
const scale = 1 / points.length;
const center = vec3.create();
for (const point of points) {
vec3.scaleAndAdd(center, center, point, scale);
}
return center;
}
function choosePoints(points) {
if (points.length === 1 || points.length === 2) {
return points;
}
const firstIndex = 0;
const secondIndex = Math.ceil(points.length / 4);
const thirdIndex = Math.ceil(points.length / 2);
// TODO - check if colinear, if so try to find another 3 points.
const newPoints = [points[firstIndex], points[secondIndex], points[thirdIndex]];
return newPoints;
}
@@ -22,13 +22,20 @@ export default function isRehydratable(displaySet, mappings) {
const { measurements } = displaySet;
for (let i = 0; i < measurements.length; i++) {
const { TrackingIdentifier } = measurements[i] || {};
if (!TrackingIdentifier) {
console.warn('No tracking identifier for measurement ', measurements[i]);
const measurement = measurements[i];
if (!measurement) {
continue;
}
const { TrackingIdentifier = '', graphicType, graphicCode, pointsLength } = measurement;
if (!TrackingIdentifier && !graphicType) {
console.warn('No tracking identifier or graphicType for measurement ', measurement);
continue;
}
const adapter = MeasurementReport.getAdapterForTrackingIdentifier(TrackingIdentifier);
const hydratable = adapter && mappingDefinitions.has(adapter.toolType);
const adapters = MeasurementReport.getAdaptersForTypes(graphicCode, graphicType, pointsLength);
const hydratable =
(adapter && mappingDefinitions.has(adapter.toolType)) ||
(adapters && adapters.some(adapter => mappingDefinitions.has(adapter.toolType)));
if (hydratable) {
return true;
@@ -34,7 +34,7 @@
"@ohif/extension-default": "3.11.0-beta.114",
"@ohif/i18n": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"classnames": "^2.3.2"
}
}
+8 -8
View File
@@ -38,10 +38,10 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^3.30.3",
"@cornerstonejs/dicom-image-loader": "^3.32.5",
"@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.21",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.30.3",
"@cornerstonejs/ai": "^3.30.3",
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/labelmap-interpolation": "^3.30.3",
"@cornerstonejs/polymorphic-segmentation": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/adapters": "^3.32.5",
"@cornerstonejs/ai": "^3.32.5",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/labelmap-interpolation": "^3.32.5",
"@cornerstonejs/polymorphic-segmentation": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
@@ -13,7 +13,6 @@ import CinePlayer from '../components/CinePlayer';
import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import ViewportColorbarsContainer from '../components/ViewportColorbar';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
@@ -305,31 +304,6 @@ const OHIFCornerstoneViewport = React.memo(
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
event => handleJumpToMeasurement(event, elementRef, viewportId, cornerstoneViewportService)
);
return () => {
unsubscribe();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
const Notification = customizationService.getCustomization('ui.notificationComponent');
return (
@@ -387,54 +361,6 @@ const OHIFCornerstoneViewport = React.memo(
areEqual
);
// Helper function to handle jumping to measurements
function handleJumpToMeasurement(event, elementRef, viewportId, cornerstoneViewportService) {
const { measurement, isConsumed } = event;
if (!measurement || isConsumed) {
return;
}
const enabledElement = getEnabledElement(elementRef.current);
if (!enabledElement) {
return;
}
const viewport = enabledElement.viewport as csTypes.IStackViewport | csTypes.IVolumeViewport;
const { metadata, displaySetInstanceUID } = measurement;
const viewportDisplaySets = cornerstoneViewportService.getViewportDisplaySets(viewportId);
const showingDisplaySet = viewportDisplaySets.find(
ds => ds.displaySetInstanceUID === displaySetInstanceUID
);
let metadataToUse = metadata;
// if it is not showing the displaySet we need to remove the FOR from the metadata
if (!showingDisplaySet) {
metadataToUse = {
...metadata,
FrameOfReferenceUID: undefined,
};
}
// Todo: make it work with cases where we want to define FOR based measurements too
if (!viewport.isReferenceViewable(metadataToUse, WITH_NAVIGATION)) {
return;
}
try {
viewport.setViewReference(metadata);
viewport.render();
} catch (e) {
console.warn('Unable to apply', metadata, e);
}
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
event?.consume?.();
}
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
@@ -558,16 +484,4 @@ function areEqual(prevProps, nextProps) {
return true;
}
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport;
+79 -13
View File
@@ -146,6 +146,70 @@ function commandsModule({
}
const actions = {
jumpToMeasurementViewport: ({ annotationUID, measurement }) => {
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
const { metadata } = measurement;
const activeViewportId = viewportGridService.getActiveViewportId();
// Finds the best viewport to jump to for showing the annotation view reference
// This may be different from active if there is a viewport already showing the display set.
const viewportId = cornerstoneViewportService.findNavigationCompatibleViewportId(
activeViewportId,
metadata
);
if (viewportId) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
viewport.setViewReference(metadata);
viewport.render();
return;
}
const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement;
if (!referencedDisplaySetInstanceUID) {
console.warn('ViewportGrid::No display set found in', measurement);
return;
}
// Finds the viewport to update to show the given displayset/orientation.
// This will choose a view already containing the measurement display set
// if possible, otherwise will fallback to the active.
const viewportToUpdate = cornerstoneViewportService.findUpdateableViewportConfiguration(
activeViewportId,
measurement
);
if (!viewportToUpdate) {
console.warn('Unable to find a viewport to show this in');
return;
}
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportToUpdate.viewportId,
referencedDisplaySetInstanceUID
);
if (!updatedViewports?.[0]) {
console.warn(
'ViewportGrid::Unable to navigate to viewport containing',
referencedDisplaySetInstanceUID
);
return;
}
updatedViewports[0].viewportOptions = viewportToUpdate.viewportOptions;
// Update stored position presentation
commandsManager.run('updateStoredPositionPresentation', {
viewportId: viewportToUpdate.viewportId,
displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
referencedImageId: measurement.referencedImageId,
options: {
...measurement.metadata,
},
});
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
},
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) {
return;
@@ -328,6 +392,8 @@ function commandsModule({
type,
});
},
/** Stores the changed position presentation */
updateStoredPositionPresentation: ({
viewportId,
displaySetInstanceUIDs,
@@ -351,22 +417,23 @@ function commandsModule({
([key, value]) => {
return (
displaySetInstanceUIDs.every(uid => key.includes(uid)) &&
value.viewportId === viewportId
value?.viewportId === viewportId
);
}
)?.[0];
}
// Create presentation data with referencedImageId and options if provided
const presentationData = referencedImageId
? {
...presentations.positionPresentation,
viewReference: {
referencedImageId,
...options,
},
}
: presentations.positionPresentation;
const presentationData =
referencedImageId || options?.FrameOfReferenceUID
? {
...presentations.positionPresentation,
viewReference: {
referencedImageId,
...options,
},
}
: presentations.positionPresentation;
if (previousReferencedDisplaySetStoreKey) {
setPositionPresentation(previousReferencedDisplaySetStoreKey, presentationData);
@@ -2153,9 +2220,7 @@ function commandsModule({
updateMeasurement: {
commandFn: actions.updateMeasurement,
},
jumpToMeasurement: {
commandFn: actions.jumpToMeasurement,
},
jumpToMeasurement: actions.jumpToMeasurement,
removeMeasurement: {
commandFn: actions.removeMeasurement,
},
@@ -2415,6 +2480,7 @@ function commandsModule({
startRecordingForAnnotationGroup: actions.startRecordingForAnnotationGroup,
endRecordingForAnnotationGroup: actions.endRecordingForAnnotationGroup,
toggleSegmentLabel: actions.toggleSegmentLabel,
jumpToMeasurementViewport: actions.jumpToMeasurementViewport,
initializeSegmentLabelTool: actions.initializeSegmentLabelTool,
};
@@ -24,36 +24,38 @@ export const groupByStudy = (items, grouping, childProps) => {
let firstSelected, firstGroup;
items.forEach(item => {
const studyUID = getItemStudyInstanceUID(item);
if (!groups.has(studyUID)) {
const items = [];
const filter = MeasurementFilters.filterAnd(
MeasurementFilters.filterMeasurementsByStudyUID(studyUID),
grouping.filter
);
const group = {
...grouping,
items,
displayMeasurements: items,
key: studyUID,
isSelected: studyUID === activeStudyUID,
StudyInstanceUID: studyUID,
filter,
measurementFilter: filter,
};
if (group.isSelected && !firstSelected) {
firstSelected = group;
items
.filter(item => item.displaySetInstanceUID)
.forEach(item => {
const studyUID = getItemStudyInstanceUID(item);
if (!groups.has(studyUID)) {
const items = [];
const filter = MeasurementFilters.filterAnd(
MeasurementFilters.filterMeasurementsByStudyUID(studyUID),
grouping.filter
);
const group = {
...grouping,
items,
displayMeasurements: items,
key: studyUID,
isSelected: studyUID === activeStudyUID,
StudyInstanceUID: studyUID,
filter,
measurementFilter: filter,
};
if (group.isSelected && !firstSelected) {
firstSelected = group;
}
firstGroup ||= group;
groups.set(studyUID, group);
}
firstGroup ||= group;
groups.set(studyUID, group);
}
if (!firstSelected && firstGroup) {
firstGroup.isSelected = true;
}
const group = groups.get(studyUID);
group.items.push(item);
});
if (!firstSelected && firstGroup) {
firstGroup.isSelected = true;
}
const group = groups.get(studyUID);
group.items.push(item);
});
return groups;
};
@@ -34,15 +34,20 @@ export default {
if (!viewportId) {
return [];
}
const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetInsaneUIDs) {
if (!displaySetInstanceUIDs) {
return [];
}
const displaySets = displaySetInsaneUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
const displaySets = displaySetInstanceUIDs.map(uid => {
const displaySet = displaySetService.getDisplaySetByUID(uid);
const referencedDisplaySetUID = displaySet?.measurements?.[0]?.displaySetInstanceUID;
if (displaySet.Modality === 'SR' && referencedDisplaySetUID) {
return displaySetService.getDisplaySetByUID(referencedDisplaySetUID);
}
return displaySet;
});
return hangingProtocols
.map(hp => {
@@ -279,7 +279,7 @@ export function useViewportRendering(
}
// Get threshold from colormap if available
if (properties.colormap && properties.colormap.threshold !== undefined) {
if (properties?.colormap && properties.colormap.threshold !== undefined) {
setThresholdState(properties.colormap.threshold);
}
}
+1 -1
View File
@@ -195,7 +195,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
{
name: 'cornerstone',
component: ExtendedOHIFCornerstoneViewport,
isReferenceViewable: props => utils.isReferenceViewable({ ...props, servicesManager }),
isReferenceViewable: utils.isReferenceViewable.bind(null, servicesManager),
},
];
},
+5 -9
View File
@@ -189,17 +189,13 @@ export default async function init({
initCineService(servicesManager);
initStudyPrefetcherService(servicesManager);
[
measurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
measurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
].forEach(event => {
measurementService.subscribe(event, evt => {
const { measurement } = evt;
const { uid: annotationUID } = measurement;
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
});
measurementService.subscribe(measurementService.EVENTS.JUMP_TO_MEASUREMENT, evt => {
const { measurement } = evt;
const { uid: annotationUID } = measurement;
commandsManager.runCommand('jumpToMeasurementViewport', { measurement, annotationUID, evt });
});
// When a custom image load is performed, update the relevant viewports
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
@@ -436,7 +436,7 @@ const connectMeasurementServiceToTools = ({
return;
}
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement;
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
@@ -463,8 +463,12 @@ const connectMeasurementServiceToTools = ({
annotationUID: measurement.uid,
highlighted: false,
isLocked: false,
invalidated: false,
// This is used to force a re-render of the annotation to
// re-calculate cached stats since sometimes in SR we
// get empty cached stats
invalidated: true,
metadata: {
...metadata,
toolName: measurement.toolName,
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
referencedImageId: imageId,
@@ -36,7 +36,6 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -47,6 +46,7 @@ const MIN_STACK_VIEWPORTS_TO_ENQUEUE_RESIZE = 12;
const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6;
export const WITH_NAVIGATION = { withNavigation: true, withOrientation: false };
export const WITH_ORIENTATION = { withNavigation: true, withOrientation: true };
/**
* Handles cornerstone viewport logic including enabling, disabling, and
@@ -551,10 +551,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* was initiated.
* @return the viewportId that the measurement should be displayed in.
*/
public getViewportIdToJump(activeViewportId: string, metadata): string {
public findNavigationCompatibleViewportId(activeViewportId: string, metadata): string {
// First check if the active viewport can just be navigated to show the given item
const activeViewport = this.getCornerstoneViewport(activeViewportId);
if (activeViewport.isReferenceViewable(metadata, { withNavigation: true })) {
if (!activeViewport) {
console.warn('No active viewport found for', activeViewportId);
}
if (activeViewport?.isReferenceViewable(metadata, { withNavigation: true })) {
return activeViewportId;
}
@@ -570,7 +573,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// No viewport is in the right display set/orientation to show this, so see if
// the active viewport could change orientations to show this
if (
activeViewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
activeViewport?.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true })
) {
return activeViewportId;
}
@@ -589,6 +592,86 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return null;
}
/**
* Figures out which viewport to update when the viewport type needs to change.
* This may not be the active viewport if there is already a viewport showing
* the display set, but in the wrong orientation.
*
* The viewport will need to update the viewport type and/or display set to
* display the resulting data.
*
* The first choice will be a viewport already showing the correct display set,
* but showing it as a stack.
*
* Second choice is to see if there is a viewport already showing the right
* orientation for the image, but the wrong display set. This fixes the
* case where the user is in MPR and a viewport other than active should be
* the one to change to display the iamge.
*
* Final choice is to use the provide activeViewportId. This will cover
* changes to/from video and wsi viewports and other cases where no
* viewport is really even close to being able to display the measurement.
*/
public findUpdateableViewportConfiguration(activeViewportId: string, measurement) {
const { metadata, displaySetInstanceUID } = measurement;
const { volumeId, referencedImageId } = metadata;
const { displaySetService, viewportGridService } = this.servicesManager.services;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
let { viewportType } = displaySet;
if (!viewportType) {
if (referencedImageId && !displaySet.isReconstructable) {
viewportType = csEnums.ViewportType.STACK;
} else if (volumeId) {
viewportType = 'volume';
}
}
// Find viewports that could be updated to be volumes to show this view
// That prefers a viewport already showing the right display set.
if (volumeId) {
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
if (viewport?.isReferenceViewable(metadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
}
// Find a viewport in the correct orientation showing a different display set
// which could be used to display the annotation.
const altMetadata = { ...metadata, volumeId: null, referencedImageId: null };
for (const id of this.viewportsById.keys()) {
const viewport = this.getCornerstoneViewport(id);
const viewportDisplaySetUID = viewportGridService.getDisplaySetsUIDsForViewport(id)?.[0];
if (!viewportDisplaySetUID || !viewport) {
continue;
}
if (volumeId) {
altMetadata.volumeId = viewportDisplaySetUID;
}
altMetadata.FrameOfReferenceUID = this._getFrameOfReferenceUID(viewportDisplaySetUID);
if (viewport.isReferenceViewable(altMetadata, { asVolume: true, withNavigation: true })) {
return {
viewportId: id,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
}
// Just display in the active viewport
return {
viewportId: activeViewportId,
displaySetInstanceUID,
viewportOptions: { viewportType },
};
}
/**
* Sets the image data for the given viewport.
*/
@@ -626,7 +709,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// is being used to navigate to the initial view position for measurement
// navigation and other navigation forcing specific views.
let initialImageIndexToUse =
presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex;
presentations?.positionPresentation?.initialImageIndex ?? <number>initialImageIndex;
const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions();
@@ -662,7 +745,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
initialImageIndexToUse = imageIds.indexOf(referencedImageId);
}
if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) {
if (
initialImageIndexToUse === undefined ||
initialImageIndexToUse === null ||
initialImageIndexToUse < 0
) {
initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0;
}
@@ -1085,8 +1172,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt(
Math.pow(dimensions[0] * spacing[0], 2) +
Math.pow(dimensions[1] * spacing[1], 2) +
Math.pow(dimensions[2] * spacing[2], 2)
Math.pow(dimensions[1] * spacing[1], 2) +
Math.pow(dimensions[2] * spacing[2], 2)
);
return slabThickness;
@@ -1214,7 +1301,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
): void {
const viewRef = positionPresentation?.viewReference;
if (viewRef) {
if (viewport.isReferenceViewable(viewRef, WITH_NAVIGATION)) {
// The orientation can be updated here to navigate to the specified
// measurement or previous item, but this will not switch to volume
// or to stack from the other type
if (viewport.isReferenceViewable(viewRef, WITH_ORIENTATION)) {
viewport.setViewReference(viewRef);
} else {
console.warn('Unable to apply reference viewable', viewRef);
@@ -3,20 +3,17 @@ import { Enums } from '@cornerstonejs/core';
import OrientationAxis = Enums.OrientationAxis;
export const isReferenceViewable = ({
viewportId,
reference,
viewportOptions,
servicesManager,
}) => {
export const isReferenceViewable = (servicesManager, viewportId, reference, viewportOptions?) => {
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
if (!viewportOptions) {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
// we can make a customization for this to allow
// we can make a customization for this to allow specific settings
// The annotation can be seen either via navigation or by changing to a volume
const isViewable = viewport.isReferenceViewable(reference, {
withNavigation: true,
asVolume: true,
});
return isViewable;
@@ -83,7 +83,7 @@ const Bidirectional = {
function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { cachedStats = {} } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
@@ -84,7 +84,7 @@ const CircleROI = {
function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { cachedStats={} } = data;
const { referencedImageId } = metadata;
const targets = Object.keys(cachedStats);
@@ -92,6 +92,11 @@ function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
if( !cachedStats ) {
return;
}
const targets = Object.keys(cachedStats);
if (!targets.length) {
@@ -3,7 +3,6 @@ import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { config } from '@cornerstonejs/tools/annotation';
const Length = {
toAnnotation: measurement => {},
@@ -78,7 +77,6 @@ const Length = {
isLocked,
isVisible,
metadata,
// color,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
referencedImageId,
@@ -95,6 +95,11 @@ function getMappedAnnotations(annotation, displaySetService) {
const { metadata, data } = annotation;
const { cachedStats } = data;
const { referencedImageId } = metadata;
if( !cachedStats ) {
return;
}
const targets = Object.keys(cachedStats);
if (!targets.length) {
@@ -211,9 +211,11 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
const maxStr = getStatisticDisplayString(max, unit, 'max');
if (Number.isFinite(max)) {
const maxStr = getStatisticDisplayString(max, unit, 'max');
displayText.primary.push(maxStr);
displayText.primary.push(maxStr);
}
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
@@ -1,5 +1,36 @@
import * as cornerstone from '@cornerstonejs/core';
function getDisplaySet({ metadata, displaySetService }) {
const { volumeId } = metadata;
if( volumeId ) {
const displaySet = displaySetService.getDisplaySetsBy(displaySet =>
volumeId.includes(displaySet.uid)
)[0];
if( displaySet ) {
return displaySet;
}
console.warn("Unable to find volumeId", volumeId);
metadata.volumeId = null;
}
if (!metadata.FrameOfReferenceUID) {
throw new Error(
'No volumeId and no FrameOfReferenceUID provided. Could not find matching displaySet.'
);
}
const displaySet = Array.from(displaySetService.getDisplaySetCache().values()).find(
ds => ds.instance?.FrameOfReferenceUID === metadata.FrameOfReferenceUID
);
if (!displaySet) {
throw new Error('Could not find matching displaySet for the provided FrameOfReferenceUID.');
}
return displaySet;
}
/**
* It checks if the imageId is provided then it uses it to query
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
@@ -13,11 +44,7 @@ export default function getSOPInstanceAttributes(imageId, displaySetService, ann
}
const { metadata } = annotation;
const { volumeId } = metadata;
const displaySet = displaySetService.getDisplaySetsBy(displaySet =>
volumeId.includes(displaySet.uid)
)[0];
const displaySet = getDisplaySet({ metadata, displaySetService });
const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
return {
+1 -1
View File
@@ -36,7 +36,7 @@
"peerDependencies": {
"@ohif/core": "3.11.0-beta.114",
"@ohif/i18n": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicomweb-client": "^0.10.4",
"prop-types": "^15.6.2",
"react": "^18.3.1",
@@ -22,7 +22,7 @@ const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0';
const ImplementationVersionName = 'OHIF-VIEWER-2.0.0';
const ImplementationVersionName = 'OHIF-3.11.0';
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
const metadataProvider = classes.MetadataProvider;
@@ -383,8 +383,6 @@ function PanelStudyBrowser({
// Set the activeTabName and expand the study
const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs);
if (!thumbnailLocation) {
console.warn('jumpToThumbnail: displaySet thumbnail not found.');
return;
}
const { tabName, StudyInstanceUID } = thumbnailLocation;
@@ -5,7 +5,6 @@ const getViewportModule = () => {
{
name: 'chartViewport',
component: LineChartViewport,
isReferenceViewable: () => false,
},
];
};
@@ -6,12 +6,8 @@ import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
import { getSeriesDateTime } from './getCurrentDicomDateTime';
const {
filterAnd,
filterMeasurementsByStudyUID,
filterMeasurementsBySeriesUID,
filterPlanarMeasurement,
} = utils.MeasurementFilters;
const { filterAnd, filterMeasurementsByStudyUID, filterMeasurementsBySeriesUID } =
utils.MeasurementFilters;
async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) {
const { measurementService, displaySetService } = servicesManager.services;
@@ -25,8 +21,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
trackedSeries,
measurementFilter = filterAnd(
filterMeasurementsByStudyUID(StudyInstanceUID),
filterMeasurementsBySeriesUID(trackedSeries),
filterPlanarMeasurement
filterMeasurementsBySeriesUID(trackedSeries)
),
defaultSaveTitle = 'Create Report',
} = ctx;
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+3 -3
View File
@@ -32,14 +32,14 @@
"start": "yarn run dev"
},
"peerDependencies": {
"@cornerstonejs/core": "^3.30.3",
"@cornerstonejs/tools": "^3.30.3",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone-dicom-sr": "3.11.0-beta.114",
"@ohif/extension-default": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"classnames": "^2.3.2",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"lodash.debounce": "^4.0.8",
"prop-types": "^15.6.2",
"react": "^18.3.1",
@@ -21,7 +21,16 @@ const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
const SR_SOPCLASSHANDLERID = '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr';
const SR_SOP_CLASS_HANDLER_ID =
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr';
const COMPREHENSIVE_3D_SR_SOP_CLASS_HANDLER_ID =
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d';
const hasValidSOPClassHandlerId = displaySet => {
return [SR_SOP_CLASS_HANDLER_ID, COMPREHENSIVE_3D_SR_SOP_CLASS_HANDLER_ID].includes(
displaySet.SOPClassHandlerId
);
};
/**
*
@@ -320,18 +329,14 @@ function TrackedMeasurementsContextProvider(
// 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
) {
if (hasValidSOPClassHandlerId(displaySet) && !displaySet.isLoaded && displaySet.load) {
await displaySet.load();
}
// Magic string
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
hasValidSOPClassHandlerId(displaySet) &&
displaySet.isRehydratable === true &&
!displaySet.isHydrated
) {
@@ -29,7 +29,7 @@ function getViewportModule({ servicesManager, commandsManager, extensionManager
{
name: 'cornerstone-tracked',
component: ExtendedOHIFCornerstoneTrackingViewport,
isReferenceViewable: props => utils.isReferenceViewable({ ...props, servicesManager }),
isReferenceViewable: utils.isReferenceViewable.bind(null, servicesManager),
},
];
}
@@ -13,8 +13,7 @@ import {
import { useTrackedMeasurements } from '../getContextModule';
import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
utils.MeasurementFilters;
const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters;
function PanelMeasurementTableTracking(props) {
const [viewportGrid] = useViewportGrid();
@@ -23,9 +22,7 @@ function PanelMeasurementTableTracking(props) {
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const measurementFilter = trackedStudy
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
: filterPlanarMeasurement;
const measurementFilter = trackedStudy ? filterMeasurementsBySeriesUID(trackedSeries) : filterAny;
const onUntrackConfirm = () => {
sendTrackedMeasurementsEvent('UNTRACK_ALL', {});
@@ -35,25 +35,10 @@ function TrackedCornerstoneViewport(
const { SeriesInstanceUID } = displaySet;
const updateIsTracked = useCallback(() => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) {
// A current image id will only exist for volume viewports that can have measurements tracked.
// Typically these are those volume viewports for the series of acquisition.
const currentImageId = viewport?.getCurrentImageId();
if (!currentImageId) {
if (isTracked) {
setIsTracked(false);
}
return;
}
}
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
}, [isTracked, trackedMeasurements, viewportId, SeriesInstanceUID]);
}, [isTracked, SeriesInstanceUID, trackedSeries]);
const onElementEnabled = useCallback(
evt => {
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+1 -1
View File
@@ -32,7 +32,7 @@
"peerDependencies": {
"@ohif/core": "3.11.0-beta.114",
"@ohif/ui": "3.11.0-beta.114",
"dcmjs": "^0.42.0",
"dcmjs": "0.43.1",
"dicom-parser": "^1.8.9",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
+2 -2
View File
@@ -42,8 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^3.16.0",
"@cornerstonejs/tools": "^3.16.0",
"@cornerstonejs/core": "^3.32.5",
"@cornerstonejs/tools": "^3.32.5",
"@ohif/core": "3.11.0-beta.114",
"@ohif/extension-cornerstone": "3.11.0-beta.114",
"@ohif/extension-default": "3.11.0-beta.114",