I’m working on a project based on the OHIF Viewer. My goal is to load measurement data from a mock JSON file and have it automatically drawn on the corresponding image slice when the viewer loads.
When implementing measurement in the ViewerLayout component I use a useEffect hook that is bound to the activeViewportId and renderingEngineId. Inside this hook I do the following:
Read my mock measurement data.
Enrich it with the current study/series/instance UIDs.
Attempt to add it to the measurementService and annotationManager.
The Problem:
The measurement data successfully appears in the “Measurements” side panel, and measurementService.getMeasurements() shows the data correctly. However, the annotation (the actual PlanarFreehandROI drawing) is not rendered on the viewport’s SVG layer. There are no errors in the console.
It seems the data is tracked by the measurementService, but the rendering part is not being triggered correctly.
What is the correct, idiomatic way in OHIF/Cornerstone.js to programmatically add a measurement from data and ensure it gets rendered?
I’ve tried using measurementService.annotationToMeasurement and annotationManager.addAnnotation manually. Is this correct, or should measurementService.addMeasurement handle the rendering automatically through its mappings?
My current implementation uses a useEffect hook. Is there a more reliable event-based approach (e.g., listening for Enums.Events.IMAGE_RENDERED) to ensure the viewport is fully ready before I attempt to add the annotation?
Here are the relevant code snippets and my mock data structure. Any guidance would be greatly appreciated!
const getViewportInfo = useCallback(() => {
return cornerstoneViewportService.getViewportInfo(activeViewportId);
}, [cornerstoneViewportService, activeViewportId]);
const getCurrentImageId = useCallback(() => {
if (!activeViewportId) return null;
const viewportObj = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
return viewportObj?.getCurrentImageId();
}, [cornerstoneViewportService, activeViewportId]);
if (!dataSource) {
console.error('ViewerLayout: Invalid DataSource');
} else {
servicesManager.services.dataSource = dataSource;
servicesManager.services.getImageIdsForDisplaySet = ds =>
dataSource.getImageIdsForDisplaySet(ds);
}
const customToMeasurementSchema = useCallback(
raw => {
console.log('customToMeasurementSchema', raw);
if (!dataSource?.getImageIdsForDisplaySet || !displaySetService.getDisplaySetByUID) {
return null;
}
const ds = displaySetService.getDisplaySetByUID(raw.displaySetInstanceUID);
if (!ds) {
console.warn(`DisplaySet with UID ${raw.displaySetInstanceUID} not found.`);
return null;
}
const imageIds = dataSource.getImageIdsForDisplaySet(ds) || [];
const imageId = imageIds[raw.frameNumber - 1] || imageIds[0];
const measurementSchema = {
uid: raw.uid,
SOPInstanceUID: raw.SOPInstanceUID,
FrameOfReferenceUID: ds.FrameOfReferenceUID || raw.metadata?.FrameOfReferenceUID,
referenceStudyUID: raw.referenceStudyUID || raw.studyInstanceUID,
referenceSeriesUID: raw.seriesInstanceUID || ds.SeriesInstanceUID,
frameNumber: raw.frameNumber || 1,
displaySetInstanceUID: raw.displaySetInstanceUID,
label: raw.label || '',
displayText: raw.displayText || [],
type: raw.type,
points: raw.points || [],
source: raw.source,
toolName: raw.toolName,
referencedImageId: imageId,
metadata: {
...raw.metadata,
FrameOfReferenceUID: ds.FrameOfReferenceUID || raw.metadata?.FrameOfReferenceUID,
referencedImageId: imageId,
},
data: { [imageId]: raw.data },
selected: raw.selected || false,
textBox: raw.textBox,
};
console.log('customToMeasurementSchema Output:', measurementSchema);
return measurementSchema;
},
[dataSource, displaySetService]
);
const toAnnotationSchema = useCallback((measurement, annotationType) => {
const annotationObj = {
annotationType,
uid: measurement.uid,
imageId: measurement.referencedImageId,
frameNumber: measurement.frameNumber || 1,
visible: true,
handles: {
points: measurement.points.map(([x, y, z]) => ({
x,
y,
world: [x, y, z],
})),
textBox: {
x: measurement.textBox?.worldPosition[0],
y: measurement.textBox?.worldPosition[1],
},
},
textBox: {
x: measurement.textBox?.worldPosition[0],
y: measurement.textBox?.worldPosition[1],
},
style: {
color: 'rgb(0,255,0)',
lineWidth: 3.5,
fill: 'rgba(0,255,0,0.1)',
},
metadata: {
...(measurement.metadata || {}),
FrameOfReferenceUID:
measurement.metadata?.FrameOfReferenceUID || measurement.FrameOfReferenceUID || '',
},
data: measurement.data,
};
console.log('toAnnotationSchema Output:', annotationObj);
return annotationObj;
}, []);
const injectMeasurements = useCallback(
(currentImageId, displayUID, instance, viewportElement) => {
console.log('Instance object in injectMeasurements:', instance);
const renderingEngine = getRenderingEngine(renderingEngineId);
if (!renderingEngine) {
console.error('Rendering engine not found in injectMeasurements');
return;
}
const viewport = renderingEngine.getViewport(activeViewportId);
if (!viewport) {
console.error('Viewport not found');
return;
}
const { clientWidth, clientHeight } = viewport.element;
const topLeftWorld = viewport.canvasToWorld([0, 0]);
const bottomRightWorld = viewport.canvasToWorld([clientWidth, clientHeight]);
const center = [
(topLeftWorld[0] + bottomRightWorld[0]) / 2,
(topLeftWorld[1] + bottomRightWorld[1]) / 2,
(topLeftWorld[2] + bottomRightWorld[2]) / 2,
];
const worldWidth = Math.abs(bottomRightWorld[0] - topLeftWorld[0]);
const worldHeight = Math.abs(bottomRightWorld[1] - topLeftWorld[1]);
const dynamicPoints = [
[center[0] - worldWidth * 0.1, center[1] - worldHeight * 0.1, center[2]],
[center[0] + worldWidth * 0.1, center[1] - worldHeight * 0.1, center[2]],
[center[0] + worldWidth * 0.1, center[1] + worldHeight * 0.1, center[2]],
[center[0] - worldWidth * 0.1, center[1] + worldHeight * 0.1, center[2]],
];
console.log('Calculated dynamic points:', dynamicPoints);
const commonProps = {
displaySetInstanceUID: displayUID,
StudyInstanceUID: instance.StudyInstanceUID,
referenceStudyUID: instance.StudyInstanceUID,
referenceSeriesUID: instance.SeriesInstanceUID,
SOPInstanceUID: instance.SOPInstanceUID,
metadata: {
...measurementsMock[0],
FrameOfReferenceUID: instance.FrameOfReferenceUID,
referencedImageId: currentImageId,
data: measurementsMock[0].data,
textBox: measurementsMock[0].textBox,
},
FrameOfReferenceUID: instance.FrameOfReferenceUID,
};
console.log('Common Props before raw assignment (with actual UIDs):', commonProps);
const source = measurementService.getSource('Cornerstone3DTools', '0.1');
const annotationManager = annotation.state.getAnnotationManager();
if (!annotationManager || typeof annotationManager.addAnnotation !== 'function') {
console.error(
'Annotation Manager is not available or addAnnotation method is missing! Cannot inject measurements.'
);
return;
}
const rawTemplate = measurementsMock[0];
measurementsMock.forEach(rawTemplate => {
const raw = {
...rawTemplate,
...commonProps,
points: dynamicPoints,
metadata: {
...(rawTemplate.metadata || {}),
...(commonProps.metadata || {}),
},
referencedImageId: currentImageId,
SOPInstanceUID: commonProps.SOPInstanceUID,
FrameOfReferenceUID: commonProps.FrameOfReferenceUID,
referenceSeriesUID: commonProps.referenceSeriesUID,
referenceStudyUID: commonProps.referenceStudyUID,
};
console.log('Raw object for customToMeasurementSchema (after dynamic merge):', raw);
measurementService.annotationToMeasurement(
source,
measurementService.VALUE_TYPES.POLYLINE,
raw,
false
);
let existingAnnotation = annotationManager.getAnnotation(raw.uid);
if (!existingAnnotation) {
console.log('Injecting measurement (no existing annotation found with this UID)');
const annotationObj = toAnnotationSchema(raw, raw.toolName || 'PlanarFreehandROI');
console.log('Eklenecek Annotation FoR:', annotationObj.metadata.FrameOfReferenceUID);
annotationObj.imageId = currentImageId;
console.log(
'Final annotationObj before addAnnotation (full object):',
JSON.stringify(annotationObj, null, 2)
);
const engine = getRenderingEngine(renderingEngineId);
if (!engine) {
console.error(`[ViewerLayout] Geçersiz renderingEngineId: ${renderingEngineId}`);
return;
}
const groupKey = annotationObj.metadata.FrameOfReferenceUID;
annotationManager.addAnnotation(annotationObj, groupKey);
engine.render();
const added = annotationManager.getAnnotation(raw.uid);
if (!added) {
console.error(`Annotation UID ${raw.uid} eklenemedi.`);
}
console.log(
'Annotation retrieved from manager AFTER addAnnotation:',
annotationManager.getAnnotation(raw.uid)
);
} else {
console.log(
`Annotation with UID ${raw.uid} already exists in manager. Skipping injection.`
);
}
});
},
[measurementService, toAnnotationSchema, renderingEngineId, activeViewportId]
);
useEffect(() => {
const source = measurementService.getSource('Cornerstone3DTools', '0.1');
measurementService.addMapping(
source,
measurementService.VALUE_TYPES.POLYLINE,
{ points: measurementsMock[0].points.length },
toAnnotationSchema,
customToMeasurementSchema
);
}, [measurementService, toAnnotationSchema, customToMeasurementSchema]);
useEffect(() => {
const timeoutId = setTimeout(() => {
const viewportInfo = getViewportInfo();
if (viewportInfo?.renderingEngineId) {
setRenderingEngineId(viewportInfo.renderingEngineId);
console.log('[ViewerLayout] renderingEngineId set:', viewportInfo.renderingEngineId);
}
}, 5000);
return () => clearTimeout(timeoutId);
}, [getViewportInfo]);
useEffect(() => {
if (!renderingEngineId || !activeViewportId) return;
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
if (!toolGroup) {
console.error('ToolGroup not available, skipping tool configuration.');
return;
}
toolGroup.setToolActive(PlanarFreehandROITool.toolName, {
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
});
console.log('[ViewerLayout] Tool configured for:', toolGroup);
}, [renderingEngineId, activeViewportId]);
useEffect(() => {
if (!activeViewportId || !viewports.has(activeViewportId) || !renderingEngineId) {
return;
}
const renderingEngine = getRenderingEngine(renderingEngineId);
if (!renderingEngine) {
console.error('ViewerLayout: Invalid Rendering Engine');
return;
}
const viewport = renderingEngine.getViewport(activeViewportId);
if (!viewport) {
console.error('ViewerLayout: Invalid Viewport');
return;
}
const currentImageId = getCurrentImageId();
console.log('Active Viewport currentImageId:', currentImageId);
if (!currentImageId) {
console.error('ViewerLayout: No current image ID');
return;
}
if (showLoadingIndicator) {
setShowLoadingIndicator(false);
}
const dsUIDs = viewports.get(activeViewportId).displaySetInstanceUIDs || [];
if (!dsUIDs.length) return;
const displayUID = dsUIDs[0];
const ds = displaySetService.getDisplaySetByUID(displayUID);
if (!ds?.instances?.length) return;
const instance = ds.instances[0];
const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId);
if (toolGroup) {
const viewportIdsInToolGroup = toolGroup.getViewportIds();
console.log('Viewport IDs in ToolGroup:', viewportIdsInToolGroup);
let isViewportAlreadyAdded = false;
if (Array.isArray(viewportIdsInToolGroup)) {
isViewportAlreadyAdded = viewportIdsInToolGroup.includes(activeViewportId);
}
if (!isViewportAlreadyAdded) {
toolGroup.addViewport(activeViewportId, renderingEngineId);
console.log(
`[ViewerLayout] Added viewport ${activeViewportId} (engine: ${renderingEngineId}) to toolGroup ${toolGroup.id}`
);
}
} else {
console.error(`No toolGroup found for viewport ${activeViewportId}. Cannot add viewport.`);
return;
}
injectMeasurements(currentImageId, displayUID, instance, viewport.element);
viewport.render();
console.log('[ViewerLayout] Measurements and annotations injected successfully');
}, [
activeViewportId,
viewports,
renderingEngineId,
displaySetService,
getCurrentImageId,
injectMeasurements,
showLoadingIndicator,
]);
Mock data is added to the measurement service, the annotation service is also triggered and data appears to have been added, but the relevant changes are not displayed in the svg layer, as a result, no drawing is visible on the image.