Skip to content

Commit

Permalink
feat(adapter): add RTSS Adapter and Labelmaps to Contours convertor (#…
Browse files Browse the repository at this point in the history
…734)

* -first commit
-extracted RTSS logic extension from OHIF viewer PR 3092

* -rename RT to RTStruct
-move RTSS formatting out of contour to segmentation function
-refactor generateROIContoursFromSegmentations to generateContourSetFromSegmentation
-add check if no volume is found when generating contousr from segmentation
-rename contourUtils to utilities
-move helper functions out of RTSS.js to utilities folder
-clean unnecessary console logs

* -reduce cs components to be passed

* -port Annotations to RTSS code from OHIF
-adjust utility functions to work with both

* try to make contour for each segment

* -change to process single segmentation
-change roiContour to match segments
-fix for segments thresholding
-use segment labels for contours
-use Segmentation label for RTSS

* optimize performance

* more cleanup

---------
  • Loading branch information
dxlin committed Oct 6, 2023
1 parent 96579a1 commit e3e05bd
Show file tree
Hide file tree
Showing 16 changed files with 1,131 additions and 1 deletion.
343 changes: 343 additions & 0 deletions packages/adapters/src/adapters/Cornerstone3D/RTStruct/RTSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
// Code and structure based on OHIF Viewer:
// extensions\tmtv\src\utils\dicomRTAnnotationExport\RTStructureSet\RTSSReport.js

import {
generateContourSetsFromLabelmap,
AnnotationToPointData
} from "./utilities";
import dcmjs from "dcmjs";
import {
getPatientModule,
getReferencedFrameOfReferenceSequence,
getReferencedSeriesSequence,
getRTROIObservationsSequence,
getRTSeriesModule,
getStructureSetModule
} from "./utilities";

const { DicomMetaDictionary } = dcmjs.data;

export default class RTSS {
/**
* Convert handles to RTSS report containing the dcmjs dicom dataset.
*
* Note: current WIP and using segmentation to contour conversion,
* routine that is not fully tested
*
* @param segmentations - Cornerstone tool segmentations data
* @param metadataProvider - Metadata provider
* @param DicomMetadataStore - metadata store instance
* @param cs - cornerstone instance
* @param csTools - cornerstone tool instance
* @returns Report object containing the dataset
*/
static generateRTSSFromSegmentations(
segmentations,
metadataProvider,
DicomMetadataStore,
cornerstoneCache,
cornerstoneToolsEnums,
vtkUtils
) {
// Convert segmentations to ROIContours
const roiContours = [];

const contourSets = generateContourSetsFromLabelmap({
segmentations,
cornerstoneCache,
cornerstoneToolsEnums,
vtkUtils
});

contourSets.forEach((contourSet, segIndex) => {
// Check contour set isn't undefined
if (contourSet) {
const contourSequence = [];
contourSet.sliceContours.forEach(sliceContour => {
/**
* addContour - Adds a new ROI with related contours to ROIContourSequence
*
* @param newContour - cornerstoneTools `ROIContour` object
*
* newContour = {
* name: string,
* description: string,
* contourSequence: array[contour]
* color: array[number],
* metadata: {
* referencedImageId: string,
* FrameOfReferenceUID: string
* }
* }
*
* contour = {
* ContourImageSequence: array[
* { ReferencedSOPClassUID: string, ReferencedSOPInstanceUID: string}
* ]
* ContourGeometricType: string,
* NumberOfContourPoints: number,
* ContourData: array[number]
* }
*/
// Note: change needed if support non-planar contour representation is needed
const sopCommon = metadataProvider.get(
"sopCommonModule",
sliceContour.referencedImageId
);
const ReferencedSOPClassUID = sopCommon.sopClassUID;
const ReferencedSOPInstanceUID = sopCommon.sopInstanceUID;
const ContourImageSequence = [
{ ReferencedSOPClassUID, ReferencedSOPInstanceUID } // NOTE: replace in dcmjs?
];

const sliceContourPolyData = sliceContour.polyData;

sliceContour.contours.forEach((contour, index) => {
const ContourGeometricType = contour.type;
const NumberOfContourPoints =
contour.contourPoints.length;
const ContourData = [];

contour.contourPoints.forEach(point => {
const pointData =
sliceContourPolyData.points[point];
pointData[0] = +pointData[0].toFixed(2);
pointData[1] = +pointData[1].toFixed(2);
pointData[2] = +pointData[2].toFixed(2);
ContourData.push(pointData[0]);
ContourData.push(pointData[1]);
ContourData.push(pointData[2]);
});

contourSequence.push({
ContourImageSequence,
ContourGeometricType,
NumberOfContourPoints,
ContourNumber: index + 1,
ContourData
});
});
});

const segLabel = contourSet.label || `Segment ${segIndex + 1}`;

const ROIContour = {
name: segLabel,
description: segLabel,
contourSequence,
color: contourSet.color,
metadata: contourSet.metadata
};

roiContours.push(ROIContour);
}
});

const rtMetadata = {
name: segmentations.label,
label: segmentations.label
};

let dataset = initializeDataset(
rtMetadata,
roiContours[0].metadata,
metadataProvider
);

roiContours.forEach((contour, index) => {
const roiContour = {
ROIDisplayColor: contour.color || [255, 0, 0],
ContourSequence: contour.contourSequence,
ReferencedROINumber: index + 1
};

dataset.StructureSetROISequence.push(
getStructureSetModule(contour, index, metadataProvider)
);

dataset.ROIContourSequence.push(roiContour);

// ReferencedSeriesSequence
dataset.ReferencedSeriesSequence = getReferencedSeriesSequence(
contour.metadata,
index,
metadataProvider,
DicomMetadataStore
);

// ReferencedFrameOfReferenceSequence
dataset.ReferencedFrameOfReferenceSequence =
getReferencedFrameOfReferenceSequence(
contour.metadata,
metadataProvider,
dataset
);
});

const fileMetaInformationVersionArray = new Uint8Array(2);
fileMetaInformationVersionArray[1] = 1;

const _meta = {
FileMetaInformationVersion: {
Value: [fileMetaInformationVersionArray.buffer],
vr: "OB"
},
TransferSyntaxUID: {
Value: ["1.2.840.10008.1.2.1"],
vr: "UI"
},
ImplementationClassUID: {
Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id
vr: "UI"
},
ImplementationVersionName: {
Value: ["dcmjs"],
vr: "SH"
}
};

dataset._meta = _meta;

return dataset;
}

/**
* Convert handles to RTSSReport report object containing the dcmjs dicom dataset.
*
* Note: The tool data needs to be formatted in a specific way, and currently
* it is limited to the RectangleROIStartEndTool in the Cornerstone.
*
* @param annotations Array of Cornerstone tool annotation data
* @param metadataProvider Metadata provider
* @param options report generation options
* @returns Report object containing the dataset
*/
static generateRTSSFromAnnotations(
annotations,
metadataProvider,
DicomMetadataStore,
options
) {
const rtMetadata = {
name: "RTSS from Annotations",
label: "RTSS from Annotations"
};
let dataset = initializeDataset(
rtMetadata,
annotations[0].metadata,
metadataProvider
);

annotations.forEach((annotation, index) => {
const ContourSequence = AnnotationToPointData.convert(
annotation,
index,
metadataProvider,
options
);

dataset.StructureSetROISequence.push(
getStructureSetModule(annotation, index, metadataProvider)
);

dataset.ROIContourSequence.push(ContourSequence);
dataset.RTROIObservationsSequence.push(
getRTROIObservationsSequence(annotation, index)
);

// ReferencedSeriesSequence
// Todo: handle more than one series
dataset.ReferencedSeriesSequence = getReferencedSeriesSequence(
annotation.metadata,
index,
metadataProvider,
DicomMetadataStore
);

// ReferencedFrameOfReferenceSequence
dataset.ReferencedFrameOfReferenceSequence =
getReferencedFrameOfReferenceSequence(
annotation.metadata,
metadataProvider,
dataset
);
});

const fileMetaInformationVersionArray = new Uint8Array(2);
fileMetaInformationVersionArray[1] = 1;

const _meta = {
FileMetaInformationVersion: {
Value: [fileMetaInformationVersionArray.buffer],
vr: "OB"
},
TransferSyntaxUID: {
Value: ["1.2.840.10008.1.2.1"],
vr: "UI"
},
ImplementationClassUID: {
Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id
vr: "UI"
},
ImplementationVersionName: {
Value: ["dcmjs"],
vr: "SH"
}
};

dataset._meta = _meta;

return dataset;
}

/**
* Generate Cornerstone tool state from dataset
* @param {object} dataset dataset
* @param {object} hooks
* @param {function} hooks.getToolClass Function to map dataset to a tool class
* @returns
*/
//static generateToolState(_dataset, _hooks = {}) {
static generateToolState() {
// Todo
console.warn("RTSS.generateToolState not implemented");
}
}

function initializeDataset(rtMetadata, imgMetadata, metadataProvider) {
const rtSOPInstanceUID = DicomMetaDictionary.uid();

// get the first annotation data
const { referencedImageId: imageId, FrameOfReferenceUID } = imgMetadata;

const { studyInstanceUID } = metadataProvider.get(
"generalSeriesModule",
imageId
);

const patientModule = getPatientModule(imageId, metadataProvider);
const rtSeriesModule = getRTSeriesModule(DicomMetaDictionary);

return {
StructureSetROISequence: [],
ROIContourSequence: [],
RTROIObservationsSequence: [],
ReferencedSeriesSequence: [],
ReferencedFrameOfReferenceSequence: [],
...patientModule,
...rtSeriesModule,
StudyInstanceUID: studyInstanceUID,
SOPClassUID: "1.2.840.10008.5.1.4.1.1.481.3", // RT Structure Set Storage
SOPInstanceUID: rtSOPInstanceUID,
Manufacturer: "dcmjs",
Modality: "RTSTRUCT",
FrameOfReferenceUID,
PositionReferenceIndicator: "",
StructureSetLabel: rtMetadata.label || "",
StructureSetName: rtMetadata.name || "",
ReferringPhysicianName: "",
OperatorsName: "",
StructureSetDate: DicomMetaDictionary.date(),
StructureSetTime: DicomMetaDictionary.time()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { generateContourSetsFromLabelmap } from "./utilities/generateContourSetsFromLabelmap";
import RTSS from "./RTSS";

export { generateContourSetsFromLabelmap, RTSS };

0 comments on commit e3e05bd

Please sign in to comment.