Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - implement support for DICOM-RTSS #305

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/DicomMetaDictionary.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ DicomMetaDictionary.sopClassNamesByUID = {
"1.2.840.10008.5.1.4.1.1.88.33": "ComprehensiveSR",
"1.2.840.10008.5.1.4.1.1.128": "PETImage",
"1.2.840.10008.5.1.4.1.1.130": "EnhancedPETImage",
"1.2.840.10008.5.1.4.1.1.128.1": "LegacyConvertedEnhancedPETImage"
"1.2.840.10008.5.1.4.1.1.128.1": "LegacyConvertedEnhancedPETImage",
"1.2.840.10008.5.1.4.1.1.481.3": "RTStructureSetStorage"
};

DicomMetaDictionary.dictionary = dictionary;
Expand Down
102 changes: 102 additions & 0 deletions src/adapters/Cornerstone/RTSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import log from "../../log.js";
import { datasetToBlob } from "../../datasetToBlob.js";
import { DicomMessage } from "../../DicomMessage.js";
import { DicomMetaDictionary } from "../../DicomMetaDictionary.js";
import { Normalizer } from "../../normalizers.js";
import { RTSS as RTSSDerivation } from "../../derivations/index.js";

const RTSS = {
generateMockRTSS
};

export default RTSS;

/**
*
* @typedef {Object} BrushData
* @property {Object} toolState - The cornerstoneTools global toolState.
* @property {Object[]} rtss - The cornerstoneTools segment metadata that corresponds to the
* seriesInstanceUid.
*/

const generateRTSSDefaultOptions = {
includeSliceSpacing: true,
rleEncode: true
};

/**
* generateMockRTSS - Generates cornerstoneTools brush data, given a stack of
* imageIds, images and the cornerstoneTools brushData.
*
* @param {object[]} images An array of cornerstone images that contain the source
* data under `image.data.byteArray.buffer`.
* @param {Object|Object[]} inputLabelmaps3D The cornerstone `Labelmap3D` object, or an array of objects.
* @param {Object} userOptions Options to pass to the segmentation derivation and `fillSegmentation`.
* @returns {Blob}
*/
function generateMockRTSS(images, inputLabelmaps3D, userOptions = {}) {
const isMultiFrame = images[0].imageId.includes("?frame");
const rtss = _createRTSSFromImages(images, isMultiFrame, userOptions);

console.log(images);

return fillRTSS(rtss, inputLabelmaps3D, userOptions);
}

/**
* fillRTSS - Fills a derived rtss dataset with array of cornerstoneTools `ROIContour` data.
*
* @param {object[]} rtss An empty rtss derived dataset.
* @param {Object[]} inputROIContours The array of cornerstone `ROIContour` objects to add.
* @param {Object} userOptions Options object to override default options.
* @returns {Blob} description
*/
function fillRTSS(rtss, inputROIContours, userOptions = {}) {
const options = Object.assign({}, generateRTSSDefaultOptions, userOptions);

inputROIContours.forEach(roiContour => {
rtss.addContour(roiContour);
});

const rtssBlob = datasetToBlob(rtss.dataset);
return rtssBlob;
}

/**
* _createRTSSFromImages - description
*
* @param {Object[]} images An array of the cornerstone image objects.
* @param {Boolean} isMultiframe Whether the images are multiframe.
* @returns {Object} The RTSS derived dataSet.
*/
function _createRTSSFromImages(images, isMultiframe, options) {
const datasets = [];

if (isMultiframe) {
const image = images[0];
const arrayBuffer = image.data.byteArray.buffer;

const dicomData = DicomMessage.readFile(arrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);

dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);

datasets.push(dataset);
} else {
for (let i = 0; i < images.length; i++) {
const image = images[i];
const arrayBuffer = image.data.byteArray.buffer;
const dicomData = DicomMessage.readFile(arrayBuffer);
const dataset = DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);

dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
datasets.push(dataset);
}
}

const multiframe = Normalizer.normalizeToDataset(datasets);

return new RTSSDerivation([multiframe], options);
}
2 changes: 2 additions & 0 deletions src/adapters/Cornerstone/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import EllipticalRoi from "./EllipticalRoi.js";
import CircleRoi from "./CircleRoi.js";
import ArrowAnnotate from "./ArrowAnnotate.js";
import Segmentation from "./Segmentation.js";
import RTSS from "./RTSS.js";
import CobbAngle from "./CobbAngle";
import Angle from "./Angle";
import RectangleRoi from "./RectangleRoi";
Expand All @@ -19,6 +20,7 @@ const Cornerstone = {
ArrowAnnotate,
MeasurementReport,
Segmentation,
RTSS,
CobbAngle,
Angle,
RectangleRoi
Expand Down
128 changes: 128 additions & 0 deletions src/derivations/RTSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import DerivedDataset from "./DerivedDataset";
import { DicomMetaDictionary } from "../DicomMetaDictionary";

export default class RTSS extends DerivedDataset {
constructor(datasets, options = {}) {
super(datasets, options);
}

// this assumes a normalized multiframe input and will create
// a multiframe derived image
derive() {
super.derive();

this.assignToDataset({
SOPClassUID:
DicomMetaDictionary.sopClassUIDsByName.RTStructureSetStorage,
Modality: "RTSTRUCT",
ValueType: "CONTAINER"
});

this.assignFromReference([]);

this.dataset.ReferencedFrameOfReferenceSequence = [];
this.dataset.StructureSetROISequence = [];
this.dataset.ROIContourSequence = [];
this.dataset.RTROIObservationsSequence = [];
//console.log(this.referencedDataset);

// NOTE: was previously under addContour
const dataset = this.dataset;

// Populate ReferencedFrameOfReferenceSequence
// Referenced DICOM data
const ReferencedFrameOfReferenceSequence =
dataset.ReferencedFrameOfReferenceSequence;

//const FrameOfReferenceUID =
const FrameOfReferenceUID = this.referencedDataset.FrameOfReferenceUID;

// DICOM set that is referenced
const ContourImageSequence = [];
this.referencedDataset.ReferencedSeriesSequence.ReferencedInstanceSequence.forEach(
instance => {
ContourImageSequence.push(instance);
}
);

const RTReferencedSeriesSequence = [];
const RTReferencedSeries = {
SeriesInstaceUID: this.referencedDataset.ReferencedSeriesSequence
.SeriesInstanceUID,
ContourImageSequence
};
RTReferencedSeriesSequence.push(RTReferencedSeries);

const RTReferencedStudySequence = [];

const RTReferencedStudy = {
ReferencedSOPClassUID: "1.2.840.10008.3.1.2.3.1", // Detached Study Management SOP Class
ReferencedSOPInstanceUID: this.referencedDataset.StudyInstanceUID,
RTReferencedSeriesSequence
};

RTReferencedStudySequence.push(RTReferencedStudy);

const ReferencedFrameOfReference = {
FrameOfReferenceUID,
RTReferencedStudySequence
};
ReferencedFrameOfReferenceSequence.push(ReferencedFrameOfReference);
}

/**
* addContour - Adds a new ROI with related contours to ROIContourSequence
*
* @param {Object} newContour cornerstoneTools `ROIContour` object
*
* newContour = {
* name: string,
* description: string,
* contourSequence: array[contour]
* }
*
* contour = {
* ContourImageSequence: array[
* { ReferencedSOPClassUID: string, ReferencedSOPInstanceUID: string}
* ]
* ContourGeometricType: string,
* NumberOfContourPoints: number,
* ContourData: array[number]
* }
*/
addContour(newContour) {
// Start
const dataset = this.dataset;

// ROI set information
const ReferencedFrameOfReferenceUID = this.referencedDataset
.FrameOfReferenceUID;
const StructureSetROISequence = dataset.StructureSetROISequence;

const ROINumber = StructureSetROISequence.length + 1;

const StructureSetROI = {
ROINumber,
ReferencedFrameOfReferenceUID,
ROIName: newContour.name,
ROIDescription: newContour.description,
ROIGenerationAlgorithm: "MANUAL"
};

StructureSetROISequence.push(StructureSetROI);

// Contour Data
const ROIContourSequence = dataset.ROIContourSequence;

const roiContour = {
ROIDisplayColor: [255, 0, 0], // implement in tool?
ContourSequence: newContour.contourSequence,
ReferencedROINumber: ROINumber
};

ROIContourSequence.push(roiContour);

// ROI Observation data
//const RTROIObservationsSequence = dataset.RTROIObservationsSequence;
}
}
4 changes: 3 additions & 1 deletion src/derivations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import DerivedImage from "./DerivedImage";
import Segmentation from "./Segmentation";
import ParametricMap from "./ParametricMap";
import StructuredReport from "./StructuredReport";
import RTSS from "./RTSS";

export {
DerivedDataset,
DerivedPixels,
DerivedImage,
Segmentation,
ParametricMap,
StructuredReport
StructuredReport,
RTSS
};
77 changes: 77 additions & 0 deletions src/rtss/coding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
class Code {
constructor(options) {
this[_value] = options.value;
this[_meaning] = options.meaning;
this[_schemeDesignator] = options.schemeDesignator;
this[_schemeVersion] = options.schemeVersion || null;
}

get value() {
return this[_value];
}

get meaning() {
return this[_meaning];
}

get schemeDesignator() {
return this[_schemeDesignator];
}

get schemeVersion() {
return this[_schemeVersion];
}
}

class CodedConcept {
constructor(options) {
if (options.value === undefined) {
throw new Error("Option 'value' is required for CodedConcept.");
}
if (options.meaning === undefined) {
throw new Error("Option 'meaning' is required for CodedConcept.");
}
if (options.schemeDesignator === undefined) {
throw new Error(
"Option 'schemeDesignator' is required for CodedConcept."
);
}
this.CodeValue = options.value;
this.CodeMeaning = options.meaning;
this.CodingSchemeDesignator = options.schemeDesignator;
if ("schemeVersion" in options) {
this.CodingSchemeVersion = options.schemeVersion;
}
}

equals(other) {
if (
other.value === this.value &&
other.schemeDesignator === this.schemeDesignator
) {
if (other.schemeVersion && this.schemeVersion) {
return other.schemeVersion === this.schemeVersion;
}
return true;
}
return false;
}

get value() {
return this.CodeValue;
}

get meaning() {
return this.CodeMeaning;
}

get schemeDesignator() {
return this.CodingSchemeDesignator;
}

get schemeVersion() {
return this.CodingSchemeVersion;
}
}

export { Code, CodedConcept };
Loading