fix(SR): Added support for spline and live wire SR items. (#5870)
* 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>
This commit is contained in:
13 files changed
+220
-160
No files matched your search
@@ -1,14 +1,11 @@
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
|
||||
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import OHIF from '@ohif/core';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
|
||||
const { downloadBlob } = utils;
|
||||
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
const { log } = OHIF;
|
||||
|
||||
@@ -75,24 +72,8 @@ const commandsModule = (props: withAppTypes) => {
|
||||
/**
|
||||
*
|
||||
* @param measurementData An array of measurements from the measurements service
|
||||
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
* as opposed to Finding Sites.
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
|
||||
const srDataset = _generateReport(measurementData, additionalFindingTypes, options);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
downloadBlob(reportBlob, { filename: 'dicom-sr.dcm' });
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param dataSource The dataSource that you wish to use to persist the data.
|
||||
* @param dataSource The data source name ('download', 'copyToClipboard', or a named data source).
|
||||
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
* @return The naturalized report
|
||||
@@ -103,23 +84,30 @@ const commandsModule = (props: withAppTypes) => {
|
||||
additionalFindingTypes,
|
||||
options = {},
|
||||
}) => {
|
||||
// Use the @cornerstonejs adapter for converting to/from DICOM
|
||||
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
|
||||
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
|
||||
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
|
||||
const storeFn = commandsManager.runCommand('createStoreFunction', {
|
||||
dataSource,
|
||||
defaultFileName: 'dicom-sr.dcm',
|
||||
});
|
||||
|
||||
if (!storeFn) {
|
||||
log.error('[DICOMSR] No valid store for dataSource:', dataSource);
|
||||
return Promise.reject({});
|
||||
}
|
||||
|
||||
try {
|
||||
const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options);
|
||||
const naturalizedReport = _generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options
|
||||
);
|
||||
|
||||
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
|
||||
const { ContentSequence } = naturalizedReport;
|
||||
// The content sequence has 5 or more elements, of which
|
||||
// the `[4]` element contains the annotation data, so this is
|
||||
// checking that there is some annotation data present.
|
||||
if (!ContentSequence?.[4].ContentSequence?.length) {
|
||||
if (!ContentSequence?.[4]?.ContentSequence?.length) {
|
||||
console.log('naturalizedReport missing imaging content', naturalizedReport);
|
||||
throw new Error('Invalid report, no content');
|
||||
}
|
||||
@@ -128,22 +116,12 @@ const commandsModule = (props: withAppTypes) => {
|
||||
}
|
||||
|
||||
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
|
||||
|
||||
let dicomDict;
|
||||
if (typeof onBeforeDicomStore === 'function') {
|
||||
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
|
||||
|
||||
if (StudyInstanceUID) {
|
||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
}
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
await storeFn(naturalizedReport, { measurementData, dicomDict });
|
||||
|
||||
return naturalizedReport;
|
||||
} catch (error) {
|
||||
@@ -166,7 +144,6 @@ const commandsModule = (props: withAppTypes) => {
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
downloadReport: actions.downloadReport,
|
||||
storeMeasurements: actions.storeMeasurements,
|
||||
hydrateStructuredReport: actions.hydrateStructuredReport,
|
||||
};
|
||||
|
||||
@@ -637,16 +637,35 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
NUMContentItems.forEach(item => {
|
||||
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
|
||||
|
||||
// Handle spatial reference ONLY if ContentSequence exists
|
||||
// Handle spatial reference ONLY if ContentSequence exists.
|
||||
// ContentSequence may be a scalar SCOORD or an array when additional named
|
||||
// SCOORDs (e.g. control points) are nested alongside the primary geometry.
|
||||
// Pick the primary geometry entry: prefer the SCOORD without a
|
||||
// ConceptNameCodeSequence (plain polyline), falling back to the first SCOORD.
|
||||
if (ContentSequence) {
|
||||
const { ValueType } = ContentSequence;
|
||||
const scoordItem = Array.isArray(ContentSequence)
|
||||
? (ContentSequence.find(
|
||||
cs =>
|
||||
(cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D') &&
|
||||
!cs.ConceptNameCodeSequence
|
||||
) ?? ContentSequence.find(cs => cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D'))
|
||||
: ContentSequence;
|
||||
|
||||
if (!scoordItem) {
|
||||
console.warn(
|
||||
'ContentSequence array contains no SCOORD or SCOORD3D entry, skipping annotation.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { ValueType } = scoordItem;
|
||||
|
||||
if (ValueType !== 'SCOORD' && ValueType !== 'SCOORD3D') {
|
||||
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
||||
const coords = _getCoordsFromSCOORDOrSCOORD3D(scoordItem);
|
||||
|
||||
if (coords) {
|
||||
measurement.coords.push(coords);
|
||||
|
||||
Reference in new issue
Block a user