* fix(SR): Added support for spline and live wire SR items. * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Add a script to checkout a worktree for test builds * fix: Allow download for testing sr validator * Remove script that wasn't intended to be included * Bump CS3D version. * PR comments - simplify code and use single codepath for download * Allow both download and save buttons for SEG and RTSTRUCT --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import dcmjs from 'dcmjs';
|
|
import { classes, Types, utils } from '@ohif/core';
|
|
import { cache, metaData } from '@cornerstonejs/core';
|
|
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
|
|
import { adaptersRT, adaptersSEG } from '@cornerstonejs/adapters';
|
|
import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default';
|
|
|
|
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
|
|
|
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
|
const { viewports, activeViewportId } = viewportGridService.getState();
|
|
const targetViewportId = viewportId || activeViewportId;
|
|
|
|
const viewport = viewports.get(targetViewportId);
|
|
|
|
return viewport;
|
|
};
|
|
|
|
const {
|
|
Cornerstone3D: {
|
|
Segmentation: { generateSegmentation },
|
|
},
|
|
} = adaptersSEG;
|
|
|
|
const {
|
|
Cornerstone3D: {
|
|
RTSS: { generateRTSSFromRepresentation },
|
|
},
|
|
} = adaptersRT;
|
|
|
|
|
|
const commandsModule = ({
|
|
servicesManager,
|
|
extensionManager,
|
|
commandsManager,
|
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
|
const { segmentationService, displaySetService, viewportGridService } =
|
|
servicesManager.services as AppTypes.Services;
|
|
|
|
const actions = {
|
|
/**
|
|
* Loads segmentations for a specified viewport.
|
|
* The function prepares the viewport for rendering, then loads the segmentation details.
|
|
* Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume.
|
|
*
|
|
* @param {Object} params - Parameters for the function.
|
|
* @param params.segmentations - Array of segmentations to be loaded.
|
|
* @param params.viewportId - the target viewport ID.
|
|
*
|
|
*/
|
|
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
|
// Todo: handle adding more than one segmentation
|
|
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
|
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
|
|
|
const segmentation = segmentations[0];
|
|
const segmentationId = segmentation.segmentationId;
|
|
const label = segmentation.config.label;
|
|
const segments = segmentation.config.segments;
|
|
|
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
|
|
|
await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
|
segmentationId,
|
|
segments,
|
|
label,
|
|
});
|
|
|
|
segmentationService.addOrUpdateSegmentation(segmentation);
|
|
|
|
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
|
|
segmentationId,
|
|
});
|
|
|
|
return segmentationId;
|
|
},
|
|
/**
|
|
* Generates a segmentation from a given segmentation ID.
|
|
* This function retrieves the associated segmentation and
|
|
* its referenced volume, extracts label maps from the
|
|
* segmentation volume, and produces segmentation data
|
|
* alongside associated metadata.
|
|
*
|
|
* @param {Object} params - Parameters for the function.
|
|
* @param params.segmentationId - ID of the segmentation to be generated.
|
|
* @param params.options - Optional configuration for the generation process.
|
|
*
|
|
* @returns Returns the generated segmentation data.
|
|
*/
|
|
generateSegmentation: ({ segmentationId, options = {} }) => {
|
|
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
|
const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId;
|
|
|
|
const { imageIds } = segmentation.representationData.Labelmap;
|
|
|
|
const segImages = imageIds.map(imageId => cache.getImage(imageId));
|
|
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
|
|
|
|
const labelmaps2D = [];
|
|
|
|
let z = 0;
|
|
|
|
for (const segImage of segImages) {
|
|
const segmentsOnLabelmap = new Set();
|
|
const pixelData = segImage.getPixelData();
|
|
const { rows, columns } = segImage;
|
|
|
|
// Use a single pass through the pixel data
|
|
for (let i = 0; i < pixelData.length; i++) {
|
|
const segment = pixelData[i];
|
|
if (segment !== 0) {
|
|
segmentsOnLabelmap.add(segment);
|
|
}
|
|
}
|
|
|
|
labelmaps2D[z++] = {
|
|
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
|
|
pixelData,
|
|
rows,
|
|
columns,
|
|
};
|
|
}
|
|
|
|
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
|
|
|
|
const labelmap3D = {
|
|
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
|
|
metadata: [],
|
|
labelmaps2D,
|
|
};
|
|
|
|
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
|
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
|
|
|
|
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
|
|
// segmentation service already has a color for each segment
|
|
if (!segment) {
|
|
return;
|
|
}
|
|
|
|
const { label } = segment;
|
|
|
|
const firstRepresentation = representations[0];
|
|
const color = segmentationService.getSegmentColor(
|
|
firstRepresentation.viewportId,
|
|
segmentationId,
|
|
segment.segmentIndex
|
|
);
|
|
|
|
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
|
color.slice(0, 3).map(value => value / 255)
|
|
).map(value => Math.round(value));
|
|
|
|
const segmentMetadata = {
|
|
SegmentNumber: segmentIndex.toString(),
|
|
SegmentLabel: label,
|
|
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
|
|
SegmentAlgorithmName: segment?.algorithmName || 'OHIF Brush',
|
|
RecommendedDisplayCIELabValue,
|
|
SegmentedPropertyCategoryCodeSequence: {
|
|
CodeValue: 'T-D0050',
|
|
CodingSchemeDesignator: 'SRT',
|
|
CodeMeaning: 'Tissue',
|
|
},
|
|
SegmentedPropertyTypeCodeSequence: {
|
|
CodeValue: 'T-D0050',
|
|
CodingSchemeDesignator: 'SRT',
|
|
CodeMeaning: 'Tissue',
|
|
},
|
|
};
|
|
labelmap3D.metadata[segmentIndex] = segmentMetadata;
|
|
});
|
|
|
|
const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, {
|
|
predecessorImageId,
|
|
...options,
|
|
});
|
|
|
|
return generatedSegmentation;
|
|
},
|
|
/**
|
|
* Downloads a segmentation based on the provided segmentation ID.
|
|
* This function retrieves the associated segmentation and
|
|
* uses it to generate the corresponding DICOM dataset, which
|
|
* is then downloaded with an appropriate filename.
|
|
*
|
|
* @param {Object} params - Parameters for the function.
|
|
* @param params.segmentationId - ID of the segmentation to be downloaded.
|
|
*
|
|
*/
|
|
downloadSegmentation: ({ segmentationId }) => {
|
|
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
|
const generatedSegmentation = actions.generateSegmentation({
|
|
segmentationId,
|
|
});
|
|
const storeFn = commandsManager.runCommand('createStoreFunction', {
|
|
dataSource: 'download',
|
|
defaultFileName: `${segmentationInOHIF.label}.dcm`,
|
|
});
|
|
storeFn(generatedSegmentation.dataset);
|
|
},
|
|
/**
|
|
* Stores a segmentation based on the provided segmentationId into a specified data source.
|
|
* The SeriesDescription is derived from user input or defaults to the segmentation label,
|
|
* and in its absence, defaults to 'Research Derived Series'.
|
|
*
|
|
* @param {Object} params - Parameters for the function.
|
|
* @param params.segmentationId - ID of the segmentation to be stored.
|
|
* @param params.dataSource - Data source where the generated segmentation will be stored.
|
|
*
|
|
* @returns {Object|void} Returns the naturalized report if successfully stored,
|
|
* otherwise throws an error.
|
|
*/
|
|
storeSegmentation: async ({ segmentationId, dataSource, modality = 'SEG' }) => {
|
|
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
|
|
if (!segmentation) {
|
|
throw new Error('No segmentation found');
|
|
}
|
|
|
|
const { label, predecessorImageId } = segmentation;
|
|
|
|
const {
|
|
value: reportName,
|
|
dataSourceName,
|
|
series,
|
|
priorSeriesNumber,
|
|
action,
|
|
} = await createReportDialogPrompt({
|
|
servicesManager,
|
|
extensionManager,
|
|
predecessorImageId,
|
|
title: 'Store Segmentation',
|
|
modality,
|
|
enableDownload: true,
|
|
});
|
|
|
|
if (action !== PROMPT_RESPONSES.CREATE_REPORT) {
|
|
return;
|
|
}
|
|
|
|
const defaultFileName =
|
|
modality === 'RTSTRUCT'
|
|
? `rtss-${segmentationId}.dcm`
|
|
: `${label || 'segmentation'}.dcm`;
|
|
|
|
const storeFn = commandsManager.runCommand('createStoreFunction', {
|
|
dataSource: dataSourceName,
|
|
defaultFileName,
|
|
});
|
|
|
|
if (!storeFn) {
|
|
throw new Error(`No valid store for dataSource: ${dataSourceName}`);
|
|
}
|
|
|
|
try {
|
|
const args = {
|
|
segmentationId,
|
|
options: {
|
|
SeriesDescription: series ? undefined : reportName || label || 'Contour Series',
|
|
SeriesNumber: series ? undefined : 1 + priorSeriesNumber,
|
|
predecessorImageId: series,
|
|
},
|
|
};
|
|
const generatedDataAsync =
|
|
(modality === 'SEG' && actions.generateSegmentation(args)) ||
|
|
(modality === 'RTSTRUCT' && actions.generateContour(args));
|
|
const generatedData = await generatedDataAsync;
|
|
|
|
if (!generatedData?.dataset) {
|
|
throw new Error('Error during segmentation generation');
|
|
}
|
|
|
|
const { dataset: naturalizedReport } = generatedData;
|
|
|
|
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out
|
|
if (naturalizedReport.StudyID === 'No Study ID') {
|
|
naturalizedReport.StudyID = '';
|
|
}
|
|
|
|
await storeFn(naturalizedReport, {});
|
|
|
|
return naturalizedReport;
|
|
} catch (error) {
|
|
console.debug('Error storing segmentation:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
generateContour: async args => {
|
|
const { segmentationId, options } = args;
|
|
const segmentations = segmentationService.getSegmentation(segmentationId);
|
|
|
|
// inject colors to the segmentIndex
|
|
const firstRepresentation =
|
|
segmentationService.getRepresentationsForSegmentation(segmentationId)[0];
|
|
Object.entries(segmentations.segments).forEach(([segmentIndex, segment]) => {
|
|
segment.color = segmentationService.getSegmentColor(
|
|
firstRepresentation.viewportId,
|
|
segmentationId,
|
|
Number(segmentIndex)
|
|
);
|
|
});
|
|
const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId;
|
|
const dataset = await generateRTSSFromRepresentation(segmentations, {
|
|
predecessorImageId,
|
|
...options,
|
|
});
|
|
return { dataset };
|
|
},
|
|
|
|
/**
|
|
* Downloads an RTSS instance from a segmentation or contour
|
|
* representation.
|
|
*/
|
|
downloadRTSS: async args => {
|
|
const { dataset } = await actions.generateContour(args);
|
|
const { InstanceNumber: instanceNumber = 1, SeriesInstanceUID: seriesUID } = dataset;
|
|
const storeFn = commandsManager.runCommand('createStoreFunction', {
|
|
dataSource: 'download',
|
|
defaultFileName: `rtss-${seriesUID}-${instanceNumber}.dcm`,
|
|
});
|
|
await storeFn(dataset);
|
|
},
|
|
|
|
toggleActiveSegmentationUtility: ({ itemId: buttonId }) => {
|
|
const { uiState, setUIState } = useUIStateStore.getState();
|
|
const isButtonActive = uiState['activeSegmentationUtility'] === buttonId;
|
|
console.log('toggleActiveSegmentationUtility', isButtonActive, buttonId);
|
|
// if the button is active, clear the active segmentation utility
|
|
if (isButtonActive) {
|
|
setUIState('activeSegmentationUtility', null);
|
|
} else {
|
|
setUIState('activeSegmentationUtility', buttonId);
|
|
}
|
|
},
|
|
};
|
|
|
|
const definitions = {
|
|
loadSegmentationsForViewport: actions.loadSegmentationsForViewport,
|
|
generateSegmentation: actions.generateSegmentation,
|
|
downloadSegmentation: actions.downloadSegmentation,
|
|
storeSegmentation: actions.storeSegmentation,
|
|
downloadRTSS: actions.downloadRTSS,
|
|
toggleActiveSegmentationUtility: actions.toggleActiveSegmentationUtility,
|
|
};
|
|
|
|
return {
|
|
actions,
|
|
definitions,
|
|
defaultContext: 'SEGMENTATION',
|
|
};
|
|
};
|
|
|
|
export default commandsModule;
|