feat(image capture): Enhance SEG and RTSS Support with Dependency Updates and Bug Fixes (#4924)
This commit is contained in:
1 parent
479e280fdf
commit
fc33bd0d59
57 files changed
+1247
-935
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user