feat: add support to scoord3d (#5016)
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
1 parent
af40f06524
commit
735405a855
73 files changed
+1032
-993
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user