feat(image capture): Enhance SEG and RTSS Support with Dependency Updates and Bug Fixes (#4924)

This commit is contained in:
Alireza authored and GitHub committed 2025-04-04 12:11:29 -04:00
1 parent 479e280fdf
commit fc33bd0d59
57 files changed
+1247 -935

No files matched your search

+7 -7
View File
@@ -38,7 +38,7 @@
"@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.8.4",
"@cornerstonejs/dicom-image-loader": "^3.8.5",
"@ohif/core": "3.10.0-beta.148",
"@ohif/ui": "3.10.0-beta.148",
"dcmjs": "*",
@@ -51,12 +51,12 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^3.8.4",
"@cornerstonejs/ai": "^3.8.4",
"@cornerstonejs/core": "^3.8.4",
"@cornerstonejs/labelmap-interpolation": "^3.8.4",
"@cornerstonejs/polymorphic-segmentation": "^3.8.4",
"@cornerstonejs/tools": "^3.8.4",
"@cornerstonejs/adapters": "^3.8.5",
"@cornerstonejs/ai": "^3.8.5",
"@cornerstonejs/core": "^3.8.5",
"@cornerstonejs/labelmap-interpolation": "^3.8.5",
"@cornerstonejs/polymorphic-segmentation": "^3.8.5",
"@cornerstonejs/tools": "^3.8.5",
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "32.12.0",
"html2canvas": "^1.4.1",
+24 -3
View File
@@ -35,6 +35,7 @@ import { toolNames } from './initCornerstoneTools';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats';
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
@@ -495,7 +496,6 @@ function commandsModule({
downloadCSVSegmentationReport: ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const cachedStats = segmentation.cachedStats;
const { representationData } = segmentation;
const { Labelmap } = representationData;
@@ -1021,6 +1021,10 @@ function commandsModule({
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup.hasTool(toolName)) {
return;
}
const prevConfig = toolGroup?.getToolConfiguration(toolName);
toolGroup?.setToolConfiguration(
toolName,
@@ -1539,7 +1543,8 @@ function commandsModule({
DefaultHistoryMemo.redo();
},
toggleSegmentPreviewEdit: ({ toggle }) => {
const labelmapTools = getLabelmapTools({ toolGroupService });
let labelmapTools = getLabelmapTools({ toolGroupService });
labelmapTools = labelmapTools.filter(tool => !tool.toolName.includes('Eraser'));
labelmapTools.forEach(tool => {
tool.configuration = {
...tool.configuration,
@@ -1562,7 +1567,8 @@ function commandsModule({
});
},
toggleUseCenterSegmentIndex: ({ toggle }) => {
const labelmapTools = getLabelmapTools({ toolGroupService });
let labelmapTools = getLabelmapTools({ toolGroupService });
labelmapTools = labelmapTools.filter(tool => !tool.toolName.includes('Eraser'));
labelmapTools.forEach(tool => {
tool.configuration = {
...tool.configuration,
@@ -1707,6 +1713,20 @@ function commandsModule({
const activeSegmentation = segmentationService.getActiveSegmentation(activeViewportId);
segmentationService.addSegment(activeSegmentation.segmentationId);
},
loadSegmentationDisplaySetsForViewport: ({ viewportId, displaySetInstanceUIDs }) => {
const updatedViewports = getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySetInstanceUIDs,
});
updatedViewports.forEach(viewport => {
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
});
});
},
};
const definitions = {
@@ -1985,6 +2005,7 @@ function commandsModule({
increaseBrushSize: actions.increaseBrushSize,
decreaseBrushSize: actions.decreaseBrushSize,
addNewSegment: actions.addNewSegment,
loadSegmentationDisplaySetsForViewport: actions.loadSegmentationDisplaySetsForViewport,
};
return {
@@ -303,7 +303,7 @@ function DicomUploadProgress({
<span
className={classNames(
NO_WRAP_ELLIPSIS_CLASS_NAMES,
'text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
'text-primary hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
)}
onClick={cancelAllUploads}
>
@@ -89,7 +89,7 @@ const DicomUploadProgressItem = memo(
)}
<div className="ml-auto flex cursor-pointer">
<Icons.Close
className="text-primary-active self-center"
className="text-primary self-center"
onClick={cancelUpload}
/>
</div>
+11 -2
View File
@@ -82,6 +82,7 @@ const stackRetrieveOptions = {
},
};
const unsubscriptions = [];
/**
*
*/
@@ -142,6 +143,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
segmentationService.removeAllSegmentations();
unsubscriptions.forEach(unsubscribe => unsubscribe());
},
/**
@@ -149,7 +152,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
*
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
*/
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
preRegistration: async function (props: Types.Extensions.ExtensionParams): Promise<void> {
const { servicesManager, serviceProvidersManager } = props;
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
servicesManager.registerService(ToolGroupService.REGISTRATION);
@@ -167,7 +170,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
return init.call(this, props);
const initResult = await init.call(this, props);
unsubscriptions.push(...initResult.unsubscriptions);
return {
...initResult,
};
},
getToolbarModule,
getHangingProtocolModule,
+14 -6
View File
@@ -193,13 +193,14 @@ export default async function init({
});
// Setup segmentation event handlers
setupSegmentationDataModifiedHandler({
segmentationService,
customizationService,
commandsManager,
});
const { unsubscribe: unsubscribeSegmentationDataModifiedHandler } =
setupSegmentationDataModifiedHandler({
segmentationService,
customizationService,
commandsManager,
});
setupSegmentationModifiedHandler({
const { unsubscribe: unsubscribeSegmentationModifiedHandler } = setupSegmentationModifiedHandler({
segmentationService,
});
@@ -299,6 +300,13 @@ export default async function init({
// Call this function when initializing
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
const unsubscriptions = [
unsubscribeSegmentationDataModifiedHandler,
unsubscribeSegmentationModifiedHandler,
];
return { unsubscriptions };
}
function initializeWebWorkerProgressHandler(uiNotificationService) {
@@ -2,12 +2,10 @@ import React from 'react';
import { SegmentationTable } from '@ohif/ui-next';
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
import { metaData } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core/src';
export default function PanelSegmentation({
servicesManager,
commandsManager,
children,
}: withAppTypes) {
export default function PanelSegmentation({ children }: withAppTypes) {
const { commandsManager, servicesManager } = useSystem();
const { customizationService, displaySetService } = servicesManager.services;
const { segmentationsWithRepresentations, disabled } =
@@ -427,6 +427,8 @@ class SegmentationService extends PubSubService {
...metaData.get('instance', image.referencedImageId),
}));
segDisplaySet.imageIds = derivedImageIds;
// We should parse the segmentation as separate slices to support overlapping segments.
// This parsing should occur in the CornerstoneJS library adapters.
// For now, we use the volume returned from the library and chop it here.
@@ -513,7 +515,7 @@ class SegmentationService extends PubSubService {
type: LABELMAP,
data: {
imageIds: derivedImageIds,
referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet),
// referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet),
referencedImageIds: imageIds as string[],
},
},
@@ -566,9 +568,10 @@ class SegmentationService extends PubSubService {
const referencedImageIds = referencedDisplaySet.imageIds;
// find the first image id that contains a referenced SOP instance UID
const firstSegmentedSliceImageId = referencedImageIds.find(imageId =>
referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId))
);
const firstSegmentedSliceImageId =
referencedImageIds?.find(imageId =>
referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId))
) || null;
rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
// Map ROI contours to RT Struct Data
@@ -742,6 +742,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire
// a request through the data source?
// (This call may or may not create sub-requests for series metadata)
const { displaySetService } = this.servicesManager.services;
const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
const { hangingProtocolService } = this.servicesManager.services;
@@ -750,7 +751,13 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetInstanceUIDs = [];
for (const [index, data] of viewportData.data.entries()) {
const { volume, imageIds, displaySetInstanceUID } = data;
const { imageIds, displaySetInstanceUID } = data;
let volume = data.volume;
if (!volume) {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
volume = csToolsUtils.getOrCreateImageVolume(displaySet.images.map(image => image.imageId));
}
displaySetInstanceUIDs.push(displaySetInstanceUID);
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import html2canvas from 'html2canvas';
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
import { useSystem } from '@ohif/core/src';
@@ -103,6 +103,9 @@ const CornerstoneViewportDownloadForm = ({
return;
}
const segmentationRepresentations =
segmentation.state.getViewportSegmentationRepresentations(activeViewportId);
const { viewport } = activeViewportEnabledElement;
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
@@ -113,20 +116,48 @@ const CornerstoneViewportDownloadForm = ({
await downloadViewport.setStack([imageId]);
downloadViewport.setProperties(properties);
return {
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
};
} else if (downloadViewport instanceof BaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]);
return {
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
};
}
if (segmentationRepresentations.length > 0) {
segmentationRepresentations.forEach(segRepresentation => {
const { segmentationId, colorLUTIndex, type } = segRepresentation;
if (type === Enums.SegmentationRepresentations.Labelmap) {
segmentation.addLabelmapRepresentationToViewportMap({
[downloadViewport.id]: [
{
segmentationId,
type: Enums.SegmentationRepresentations.Labelmap,
config: {
colorLUTOrIndex: colorLUTIndex,
},
},
],
});
}
if (type === Enums.SegmentationRepresentations.Contour) {
segmentation.addContourRepresentationToViewportMap({
[downloadViewport.id]: [
{
segmentationId,
type: Enums.SegmentationRepresentations.Contour,
config: {
colorLUTOrIndex: colorLUTIndex,
},
},
],
});
}
});
}
return {
width: Math.min(width || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
height: Math.min(height || DEFAULT_SIZE, MAX_TEXTURE_SIZE),
};
} catch (error) {
console.error('Error loading image:', error);
}
@@ -167,6 +198,10 @@ const CornerstoneViewportDownloadForm = ({
setTimeout(() => {
handleLoadImage(viewportDimensions.width, viewportDimensions.height);
handleToggleAnnotations(showAnnotations);
// we need a resize here to make suer annotations world to canvas
// are properly calculated
renderingEngine.resize();
renderingEngine.render();
}, 100);
}
}, [viewportDimensions, showAnnotations]);
@@ -0,0 +1,31 @@
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySetInstanceUIDs,
}: withAppTypes) {
const { hangingProtocolService, viewportGridService } = servicesManager.services;
const { isHangingProtocolLayout } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
displaySetInstanceUIDs[0],
isHangingProtocolLayout
);
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
export { getUpdatedViewportsForSegmentation };
@@ -9,10 +9,15 @@ export function setupSegmentationDataModifiedHandler({
customizationService,
commandsManager,
}) {
segmentationService.subscribeDebounced(
const { unsubscribe } = segmentationService.subscribeDebounced(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const readableText = customizationService.getCustomization('panelSegmentation.readableText');
// Check for segments with bidirectional measurements and update them
@@ -46,17 +51,23 @@ export function setupSegmentationDataModifiedHandler({
},
1000
);
return { unsubscribe };
}
/**
* Sets up the handler for segmentation modification events
*/
export function setupSegmentationModifiedHandler({ segmentationService }) {
segmentationService.subscribe(
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const annotationState = cornerstoneTools.annotation.state.getAllAnnotations();
const bidirectionalAnnotations = annotationState.filter(
annotation =>
@@ -90,4 +101,6 @@ export function setupSegmentationModifiedHandler({ segmentationService }) {
});
}
);
return { unsubscribe };
}