@cornerstonejs/adapters 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -5
- package/src/adapters/Cornerstone/Angle.js +92 -0
- package/src/adapters/Cornerstone/ArrowAnnotate.js +106 -0
- package/src/adapters/Cornerstone/Bidirectional.js +187 -0
- package/src/adapters/Cornerstone/CircleRoi.js +109 -0
- package/src/adapters/Cornerstone/CobbAngle.js +96 -0
- package/src/adapters/Cornerstone/EllipticalRoi.js +149 -0
- package/src/adapters/Cornerstone/FreehandRoi.js +82 -0
- package/src/adapters/Cornerstone/Length.js +80 -0
- package/src/adapters/Cornerstone/MeasurementReport.js +352 -0
- package/src/adapters/Cornerstone/RectangleRoi.js +92 -0
- package/src/adapters/Cornerstone/Segmentation.js +118 -0
- package/src/adapters/Cornerstone/Segmentation_3X.js +632 -0
- package/src/adapters/Cornerstone/Segmentation_4X.js +1543 -0
- package/src/adapters/Cornerstone/cornerstone4Tag.js +1 -0
- package/src/adapters/Cornerstone/index.js +27 -0
- package/src/adapters/Cornerstone3D/ArrowAnnotate.js +155 -0
- package/src/adapters/Cornerstone3D/Bidirectional.js +196 -0
- package/src/adapters/Cornerstone3D/CodingScheme.js +16 -0
- package/src/adapters/Cornerstone3D/EllipticalROI.js +204 -0
- package/src/adapters/Cornerstone3D/Length.js +113 -0
- package/src/adapters/Cornerstone3D/MeasurementReport.js +445 -0
- package/src/adapters/Cornerstone3D/PlanarFreehandROI.js +137 -0
- package/src/adapters/Cornerstone3D/Probe.js +106 -0
- package/src/adapters/Cornerstone3D/cornerstone3DTag.js +1 -0
- package/src/adapters/Cornerstone3D/index.js +24 -0
- package/src/adapters/VTKjs/Segmentation.js +223 -0
- package/src/adapters/VTKjs/index.js +7 -0
- package/src/adapters/helpers.js +19 -0
- package/src/adapters/index.js +11 -0
- package/src/index.js +4 -0
- package/.babelrc +0 -9
- package/.eslintrc.json +0 -18
- package/.prettierrc +0 -5
- package/CHANGELOG.md +0 -106
- package/generate-dictionary.js +0 -145
- package/netlify.toml +0 -20
- package/rollup.config.js +0 -37
- package/test/adapters.test.js +0 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { utilities } from "dcmjs";
|
|
2
|
+
import CORNERSTONE_3D_TAG from "./cornerstone3DTag";
|
|
3
|
+
import MeasurementReport from "./MeasurementReport";
|
|
4
|
+
|
|
5
|
+
const { Length: TID300Length } = utilities.TID300;
|
|
6
|
+
|
|
7
|
+
const LENGTH = "Length";
|
|
8
|
+
const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`;
|
|
9
|
+
|
|
10
|
+
class Length {
|
|
11
|
+
// TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport.
|
|
12
|
+
static getMeasurementData(
|
|
13
|
+
MeasurementGroup,
|
|
14
|
+
sopInstanceUIDToImageIdMap,
|
|
15
|
+
imageToWorldCoords,
|
|
16
|
+
metadata
|
|
17
|
+
) {
|
|
18
|
+
const { defaultState, NUMGroup, SCOORDGroup, ReferencedFrameNumber } =
|
|
19
|
+
MeasurementReport.getSetupMeasurementData(
|
|
20
|
+
MeasurementGroup,
|
|
21
|
+
sopInstanceUIDToImageIdMap,
|
|
22
|
+
metadata,
|
|
23
|
+
Length.toolType
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const referencedImageId =
|
|
27
|
+
defaultState.annotation.metadata.referencedImageId;
|
|
28
|
+
|
|
29
|
+
const { GraphicData } = SCOORDGroup;
|
|
30
|
+
const worldCoords = [];
|
|
31
|
+
for (let i = 0; i < GraphicData.length; i += 2) {
|
|
32
|
+
const point = imageToWorldCoords(referencedImageId, [
|
|
33
|
+
GraphicData[i],
|
|
34
|
+
GraphicData[i + 1]
|
|
35
|
+
]);
|
|
36
|
+
worldCoords.push(point);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const state = defaultState;
|
|
40
|
+
|
|
41
|
+
state.annotation.data = {
|
|
42
|
+
handles: {
|
|
43
|
+
points: [worldCoords[0], worldCoords[1]],
|
|
44
|
+
activeHandleIndex: 0,
|
|
45
|
+
textBox: {
|
|
46
|
+
hasMoved: false
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
cachedStats: {
|
|
50
|
+
[`imageId:${referencedImageId}`]: {
|
|
51
|
+
length: NUMGroup
|
|
52
|
+
? NUMGroup.MeasuredValueSequence.NumericValue
|
|
53
|
+
: 0
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
frameNumber: ReferencedFrameNumber
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static getTID300RepresentationArguments(tool, worldToImageCoords) {
|
|
63
|
+
const { data, finding, findingSites, metadata } = tool;
|
|
64
|
+
const { cachedStats = {}, handles } = data;
|
|
65
|
+
|
|
66
|
+
const { referencedImageId } = metadata;
|
|
67
|
+
|
|
68
|
+
if (!referencedImageId) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Length.getTID300RepresentationArguments: referencedImageId is not defined"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const start = worldToImageCoords(referencedImageId, handles.points[0]);
|
|
75
|
+
const end = worldToImageCoords(referencedImageId, handles.points[1]);
|
|
76
|
+
|
|
77
|
+
const point1 = { x: start[0], y: start[1] };
|
|
78
|
+
const point2 = { x: end[0], y: end[1] };
|
|
79
|
+
|
|
80
|
+
const { length: distance } =
|
|
81
|
+
cachedStats[`imageId:${referencedImageId}`] || {};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
point1,
|
|
85
|
+
point2,
|
|
86
|
+
distance,
|
|
87
|
+
trackingIdentifierTextValue,
|
|
88
|
+
finding,
|
|
89
|
+
findingSites: findingSites || []
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Length.toolType = LENGTH;
|
|
95
|
+
Length.utilityToolType = LENGTH;
|
|
96
|
+
Length.TID300Representation = TID300Length;
|
|
97
|
+
Length.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
|
|
98
|
+
if (!TrackingIdentifier.includes(":")) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":");
|
|
103
|
+
|
|
104
|
+
if (cornerstone3DTag !== CORNERSTONE_3D_TAG) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return toolType === LENGTH;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
MeasurementReport.registerTool(Length);
|
|
112
|
+
|
|
113
|
+
export default Length;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { normalizers, data, utilities, derivations } from "dcmjs";
|
|
2
|
+
|
|
3
|
+
import { toArray, codeMeaningEquals } from "../helpers";
|
|
4
|
+
import Cornerstone3DCodingScheme from "./CodingScheme";
|
|
5
|
+
|
|
6
|
+
const { TID1500, addAccessors } = utilities;
|
|
7
|
+
|
|
8
|
+
const { StructuredReport } = derivations;
|
|
9
|
+
|
|
10
|
+
const { Normalizer } = normalizers;
|
|
11
|
+
|
|
12
|
+
const { TID1500MeasurementReport, TID1501MeasurementGroup } = TID1500;
|
|
13
|
+
|
|
14
|
+
const { DicomMetaDictionary } = data;
|
|
15
|
+
|
|
16
|
+
const FINDING = { CodingSchemeDesignator: "DCM", CodeValue: "121071" };
|
|
17
|
+
const FINDING_SITE = { CodingSchemeDesignator: "SCT", CodeValue: "363698007" };
|
|
18
|
+
const FINDING_SITE_OLD = { CodingSchemeDesignator: "SRT", CodeValue: "G-C0E3" };
|
|
19
|
+
|
|
20
|
+
const codeValueMatch = (group, code, oldCode) => {
|
|
21
|
+
const { ConceptNameCodeSequence } = group;
|
|
22
|
+
if (!ConceptNameCodeSequence) return;
|
|
23
|
+
const { CodingSchemeDesignator, CodeValue } = ConceptNameCodeSequence;
|
|
24
|
+
return (
|
|
25
|
+
(CodingSchemeDesignator == code.CodingSchemeDesignator &&
|
|
26
|
+
CodeValue == code.CodeValue) ||
|
|
27
|
+
(oldCode &&
|
|
28
|
+
CodingSchemeDesignator == oldCode.CodingSchemeDesignator &&
|
|
29
|
+
CodeValue == oldCode.CodeValue)
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function getTID300ContentItem(
|
|
34
|
+
tool,
|
|
35
|
+
toolType,
|
|
36
|
+
ReferencedSOPSequence,
|
|
37
|
+
toolClass,
|
|
38
|
+
worldToImageCoords
|
|
39
|
+
) {
|
|
40
|
+
const args = toolClass.getTID300RepresentationArguments(
|
|
41
|
+
tool,
|
|
42
|
+
worldToImageCoords
|
|
43
|
+
);
|
|
44
|
+
args.ReferencedSOPSequence = ReferencedSOPSequence;
|
|
45
|
+
|
|
46
|
+
const TID300Measurement = new toolClass.TID300Representation(args);
|
|
47
|
+
|
|
48
|
+
return TID300Measurement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getMeasurementGroup(
|
|
52
|
+
toolType,
|
|
53
|
+
toolData,
|
|
54
|
+
ReferencedSOPSequence,
|
|
55
|
+
worldToImageCoords
|
|
56
|
+
) {
|
|
57
|
+
const toolTypeData = toolData[toolType];
|
|
58
|
+
const toolClass =
|
|
59
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[toolType];
|
|
60
|
+
if (
|
|
61
|
+
!toolTypeData ||
|
|
62
|
+
!toolTypeData.data ||
|
|
63
|
+
!toolTypeData.data.length ||
|
|
64
|
+
!toolClass
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Loop through the array of tool instances
|
|
70
|
+
// for this tool
|
|
71
|
+
const Measurements = toolTypeData.data.map(tool => {
|
|
72
|
+
return getTID300ContentItem(
|
|
73
|
+
tool,
|
|
74
|
+
toolType,
|
|
75
|
+
ReferencedSOPSequence,
|
|
76
|
+
toolClass,
|
|
77
|
+
worldToImageCoords
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return new TID1501MeasurementGroup(Measurements);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default class MeasurementReport {
|
|
85
|
+
static getCornerstoneLabelFromDefaultState(defaultState) {
|
|
86
|
+
const { findingSites = [], finding } = defaultState;
|
|
87
|
+
|
|
88
|
+
const cornersoneFreeTextCodingValue =
|
|
89
|
+
Cornerstone3DCodingScheme.codeValues.CORNERSTONEFREETEXT;
|
|
90
|
+
|
|
91
|
+
let freeTextLabel = findingSites.find(
|
|
92
|
+
fs => fs.CodeValue === cornersoneFreeTextCodingValue
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (freeTextLabel) {
|
|
96
|
+
return freeTextLabel.CodeMeaning;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (finding && finding.CodeValue === cornersoneFreeTextCodingValue) {
|
|
100
|
+
return finding.CodeMeaning;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static generateDatasetMeta() {
|
|
105
|
+
// TODO: what is the correct metaheader
|
|
106
|
+
// http://dicom.nema.org/medical/Dicom/current/output/chtml/part10/chapter_7.html
|
|
107
|
+
// TODO: move meta creation to happen in derivations.js
|
|
108
|
+
const fileMetaInformationVersionArray = new Uint8Array(2);
|
|
109
|
+
fileMetaInformationVersionArray[1] = 1;
|
|
110
|
+
|
|
111
|
+
const _meta = {
|
|
112
|
+
FileMetaInformationVersion: {
|
|
113
|
+
Value: [fileMetaInformationVersionArray.buffer],
|
|
114
|
+
vr: "OB"
|
|
115
|
+
},
|
|
116
|
+
//MediaStorageSOPClassUID
|
|
117
|
+
//MediaStorageSOPInstanceUID: sopCommonModule.sopInstanceUID,
|
|
118
|
+
TransferSyntaxUID: {
|
|
119
|
+
Value: ["1.2.840.10008.1.2.1"],
|
|
120
|
+
vr: "UI"
|
|
121
|
+
},
|
|
122
|
+
ImplementationClassUID: {
|
|
123
|
+
Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id
|
|
124
|
+
vr: "UI"
|
|
125
|
+
},
|
|
126
|
+
ImplementationVersionName: {
|
|
127
|
+
Value: ["dcmjs"],
|
|
128
|
+
vr: "SH"
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return _meta;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static generateDerivationSourceDataset(
|
|
136
|
+
StudyInstanceUID,
|
|
137
|
+
SeriesInstanceUID
|
|
138
|
+
) {
|
|
139
|
+
const _vrMap = {
|
|
140
|
+
PixelData: "OW"
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const _meta = MeasurementReport.generateDatasetMeta();
|
|
144
|
+
|
|
145
|
+
const derivationSourceDataset = {
|
|
146
|
+
StudyInstanceUID,
|
|
147
|
+
SeriesInstanceUID,
|
|
148
|
+
_meta: _meta,
|
|
149
|
+
_vrMap: _vrMap
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return derivationSourceDataset;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static getSetupMeasurementData(
|
|
156
|
+
MeasurementGroup,
|
|
157
|
+
sopInstanceUIDToImageIdMap,
|
|
158
|
+
metadata,
|
|
159
|
+
toolType
|
|
160
|
+
) {
|
|
161
|
+
const { ContentSequence } = MeasurementGroup;
|
|
162
|
+
|
|
163
|
+
const contentSequenceArr = toArray(ContentSequence);
|
|
164
|
+
const findingGroup = contentSequenceArr.find(group =>
|
|
165
|
+
codeValueMatch(group, FINDING)
|
|
166
|
+
);
|
|
167
|
+
const findingSiteGroups =
|
|
168
|
+
contentSequenceArr.filter(group =>
|
|
169
|
+
codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD)
|
|
170
|
+
) || [];
|
|
171
|
+
const NUMGroup = contentSequenceArr.find(
|
|
172
|
+
group => group.ValueType === "NUM"
|
|
173
|
+
);
|
|
174
|
+
const SCOORDGroup = toArray(NUMGroup.ContentSequence).find(
|
|
175
|
+
group => group.ValueType === "SCOORD"
|
|
176
|
+
);
|
|
177
|
+
const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence;
|
|
178
|
+
const { ReferencedSOPInstanceUID, ReferencedFrameNumber } =
|
|
179
|
+
ReferencedSOPSequence;
|
|
180
|
+
|
|
181
|
+
const referencedImageId =
|
|
182
|
+
sopInstanceUIDToImageIdMap[ReferencedSOPInstanceUID];
|
|
183
|
+
const imagePlaneModule = metadata.get(
|
|
184
|
+
"imagePlaneModule",
|
|
185
|
+
referencedImageId
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const finding = findingGroup
|
|
189
|
+
? addAccessors(findingGroup.ConceptCodeSequence)
|
|
190
|
+
: undefined;
|
|
191
|
+
const findingSites = findingSiteGroups.map(fsg => {
|
|
192
|
+
return addAccessors(fsg.ConceptCodeSequence);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const defaultState = {
|
|
196
|
+
sopInstanceUid: ReferencedSOPInstanceUID,
|
|
197
|
+
annotation: {
|
|
198
|
+
annotationUID: DicomMetaDictionary.uid(),
|
|
199
|
+
metadata: {
|
|
200
|
+
toolName: toolType,
|
|
201
|
+
referencedImageId,
|
|
202
|
+
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
|
203
|
+
label: ""
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
finding,
|
|
207
|
+
findingSites
|
|
208
|
+
};
|
|
209
|
+
if (defaultState.finding) {
|
|
210
|
+
defaultState.description = defaultState.finding.CodeMeaning;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
defaultState.annotation.metadata.label =
|
|
214
|
+
MeasurementReport.getCornerstoneLabelFromDefaultState(defaultState);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
defaultState,
|
|
218
|
+
NUMGroup,
|
|
219
|
+
SCOORDGroup,
|
|
220
|
+
ReferencedSOPSequence,
|
|
221
|
+
ReferencedSOPInstanceUID,
|
|
222
|
+
ReferencedFrameNumber
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static generateReport(
|
|
227
|
+
toolState,
|
|
228
|
+
metadataProvider,
|
|
229
|
+
worldToImageCoords,
|
|
230
|
+
options
|
|
231
|
+
) {
|
|
232
|
+
// ToolState for array of imageIDs to a Report
|
|
233
|
+
// Assume Cornerstone metadata provider has access to Study / Series / Sop Instance UID
|
|
234
|
+
let allMeasurementGroups = [];
|
|
235
|
+
|
|
236
|
+
/* Patient ID
|
|
237
|
+
Warning - Missing attribute or value that would be needed to build DICOMDIR - Patient ID
|
|
238
|
+
Warning - Missing attribute or value that would be needed to build DICOMDIR - Study Date
|
|
239
|
+
Warning - Missing attribute or value that would be needed to build DICOMDIR - Study Time
|
|
240
|
+
Warning - Missing attribute or value that would be needed to build DICOMDIR - Study ID
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
const sopInstanceUIDsToSeriesInstanceUIDMap = {};
|
|
244
|
+
const derivationSourceDatasets = [];
|
|
245
|
+
|
|
246
|
+
const _meta = MeasurementReport.generateDatasetMeta();
|
|
247
|
+
|
|
248
|
+
// Loop through each image in the toolData
|
|
249
|
+
Object.keys(toolState).forEach(imageId => {
|
|
250
|
+
const sopCommonModule = metadataProvider.get(
|
|
251
|
+
"sopCommonModule",
|
|
252
|
+
imageId
|
|
253
|
+
);
|
|
254
|
+
const generalSeriesModule = metadataProvider.get(
|
|
255
|
+
"generalSeriesModule",
|
|
256
|
+
imageId
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const { sopInstanceUID, sopClassUID } = sopCommonModule;
|
|
260
|
+
const { studyInstanceUID, seriesInstanceUID } = generalSeriesModule;
|
|
261
|
+
|
|
262
|
+
sopInstanceUIDsToSeriesInstanceUIDMap[sopInstanceUID] =
|
|
263
|
+
seriesInstanceUID;
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
!derivationSourceDatasets.find(
|
|
267
|
+
dsd => dsd.SeriesInstanceUID === seriesInstanceUID
|
|
268
|
+
)
|
|
269
|
+
) {
|
|
270
|
+
// Entry not present for series, create one.
|
|
271
|
+
const derivationSourceDataset =
|
|
272
|
+
MeasurementReport.generateDerivationSourceDataset(
|
|
273
|
+
studyInstanceUID,
|
|
274
|
+
seriesInstanceUID
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
derivationSourceDatasets.push(derivationSourceDataset);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const frameNumber = metadataProvider.get("frameNumber", imageId);
|
|
281
|
+
const toolData = toolState[imageId];
|
|
282
|
+
const toolTypes = Object.keys(toolData);
|
|
283
|
+
|
|
284
|
+
const ReferencedSOPSequence = {
|
|
285
|
+
ReferencedSOPClassUID: sopClassUID,
|
|
286
|
+
ReferencedSOPInstanceUID: sopInstanceUID
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const instance = metadataProvider.get("instance", imageId);
|
|
290
|
+
if (
|
|
291
|
+
(instance &&
|
|
292
|
+
instance.NumberOfFrames &&
|
|
293
|
+
instance.NumberOfFrames > 1) ||
|
|
294
|
+
Normalizer.isMultiframeSOPClassUID(sopClassUID)
|
|
295
|
+
) {
|
|
296
|
+
ReferencedSOPSequence.ReferencedFrameNumber = frameNumber;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Loop through each tool type for the image
|
|
300
|
+
const measurementGroups = [];
|
|
301
|
+
|
|
302
|
+
toolTypes.forEach(toolType => {
|
|
303
|
+
const group = getMeasurementGroup(
|
|
304
|
+
toolType,
|
|
305
|
+
toolData,
|
|
306
|
+
ReferencedSOPSequence,
|
|
307
|
+
worldToImageCoords
|
|
308
|
+
);
|
|
309
|
+
if (group) {
|
|
310
|
+
measurementGroups.push(group);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
allMeasurementGroups =
|
|
315
|
+
allMeasurementGroups.concat(measurementGroups);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const tid1500MeasurementReport = new TID1500MeasurementReport(
|
|
319
|
+
{ TID1501MeasurementGroups: allMeasurementGroups },
|
|
320
|
+
options
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const report = new StructuredReport(derivationSourceDatasets);
|
|
324
|
+
|
|
325
|
+
const contentItem = tid1500MeasurementReport.contentItem(
|
|
326
|
+
derivationSourceDatasets,
|
|
327
|
+
{ sopInstanceUIDsToSeriesInstanceUIDMap }
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Merge the derived dataset with the content from the Measurement Report
|
|
331
|
+
report.dataset = Object.assign(report.dataset, contentItem);
|
|
332
|
+
report.dataset._meta = _meta;
|
|
333
|
+
|
|
334
|
+
return report;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Generate Cornerstone tool state from dataset
|
|
339
|
+
* @param {object} dataset dataset
|
|
340
|
+
* @param {object} hooks
|
|
341
|
+
* @param {function} hooks.getToolClass Function to map dataset to a tool class
|
|
342
|
+
* @returns
|
|
343
|
+
*/
|
|
344
|
+
static generateToolState(
|
|
345
|
+
dataset,
|
|
346
|
+
sopInstanceUIDToImageIdMap,
|
|
347
|
+
imageToWorldCoords,
|
|
348
|
+
metadata,
|
|
349
|
+
hooks = {}
|
|
350
|
+
) {
|
|
351
|
+
// For now, bail out if the dataset is not a TID1500 SR with length measurements
|
|
352
|
+
if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"This package can currently only interpret DICOM SR TID 1500"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const REPORT = "Imaging Measurements";
|
|
359
|
+
const GROUP = "Measurement Group";
|
|
360
|
+
const TRACKING_IDENTIFIER = "Tracking Identifier";
|
|
361
|
+
|
|
362
|
+
// Identify the Imaging Measurements
|
|
363
|
+
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(
|
|
364
|
+
codeMeaningEquals(REPORT)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Retrieve the Measurements themselves
|
|
368
|
+
const measurementGroups = toArray(
|
|
369
|
+
imagingMeasurementContent.ContentSequence
|
|
370
|
+
).filter(codeMeaningEquals(GROUP));
|
|
371
|
+
|
|
372
|
+
// For each of the supported measurement types, compute the measurement data
|
|
373
|
+
const measurementData = {};
|
|
374
|
+
|
|
375
|
+
const cornerstoneToolClasses =
|
|
376
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
|
377
|
+
|
|
378
|
+
const registeredToolClasses = [];
|
|
379
|
+
|
|
380
|
+
Object.keys(cornerstoneToolClasses).forEach(key => {
|
|
381
|
+
registeredToolClasses.push(cornerstoneToolClasses[key]);
|
|
382
|
+
measurementData[key] = [];
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
measurementGroups.forEach(measurementGroup => {
|
|
386
|
+
const measurementGroupContentSequence = toArray(
|
|
387
|
+
measurementGroup.ContentSequence
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const TrackingIdentifierGroup =
|
|
391
|
+
measurementGroupContentSequence.find(
|
|
392
|
+
contentItem =>
|
|
393
|
+
contentItem.ConceptNameCodeSequence.CodeMeaning ===
|
|
394
|
+
TRACKING_IDENTIFIER
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const TrackingIdentifierValue = TrackingIdentifierGroup.TextValue;
|
|
398
|
+
|
|
399
|
+
const toolClass = hooks.getToolClass
|
|
400
|
+
? hooks.getToolClass(
|
|
401
|
+
measurementGroup,
|
|
402
|
+
dataset,
|
|
403
|
+
registeredToolClasses
|
|
404
|
+
)
|
|
405
|
+
: registeredToolClasses.find(tc =>
|
|
406
|
+
tc.isValidCornerstoneTrackingIdentifier(
|
|
407
|
+
TrackingIdentifierValue
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (toolClass) {
|
|
412
|
+
const measurement = toolClass.getMeasurementData(
|
|
413
|
+
measurementGroup,
|
|
414
|
+
sopInstanceUIDToImageIdMap,
|
|
415
|
+
imageToWorldCoords,
|
|
416
|
+
metadata
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
console.log(`=== ${toolClass.toolType} ===`);
|
|
420
|
+
console.log(measurement);
|
|
421
|
+
|
|
422
|
+
measurementData[toolClass.toolType].push(measurement);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// NOTE: There is no way of knowing the cornerstone imageIds as that could be anything.
|
|
427
|
+
// That is up to the consumer to derive from the SOPInstanceUIDs.
|
|
428
|
+
return measurementData;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
static registerTool(toolClass) {
|
|
432
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE[
|
|
433
|
+
toolClass.utilityToolType
|
|
434
|
+
] = toolClass;
|
|
435
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[
|
|
436
|
+
toolClass.toolType
|
|
437
|
+
] = toolClass;
|
|
438
|
+
MeasurementReport.MEASUREMENT_BY_TOOLTYPE[toolClass.toolType] =
|
|
439
|
+
toolClass.utilityToolType;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
MeasurementReport.MEASUREMENT_BY_TOOLTYPE = {};
|
|
444
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE = {};
|
|
445
|
+
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE = {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import MeasurementReport from "./MeasurementReport";
|
|
2
|
+
import { utilities } from "dcmjs";
|
|
3
|
+
import CORNERSTONE_3D_TAG from "./cornerstone3DTag";
|
|
4
|
+
import { vec3 } from "gl-matrix";
|
|
5
|
+
|
|
6
|
+
const { Polyline: TID300Polyline } = utilities.TID300;
|
|
7
|
+
|
|
8
|
+
const PLANARFREEHANDROI = "PlanarFreehandROI";
|
|
9
|
+
const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PLANARFREEHANDROI}`;
|
|
10
|
+
const closedContourThreshold = 1e-5;
|
|
11
|
+
|
|
12
|
+
class PlanarFreehandROI {
|
|
13
|
+
static getMeasurementData(
|
|
14
|
+
MeasurementGroup,
|
|
15
|
+
sopInstanceUIDToImageIdMap,
|
|
16
|
+
imageToWorldCoords,
|
|
17
|
+
metadata
|
|
18
|
+
) {
|
|
19
|
+
const { defaultState, SCOORDGroup, ReferencedFrameNumber } =
|
|
20
|
+
MeasurementReport.getSetupMeasurementData(
|
|
21
|
+
MeasurementGroup,
|
|
22
|
+
sopInstanceUIDToImageIdMap,
|
|
23
|
+
metadata,
|
|
24
|
+
PlanarFreehandROI.toolType
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const referencedImageId =
|
|
28
|
+
defaultState.annotation.metadata.referencedImageId;
|
|
29
|
+
const { GraphicData } = SCOORDGroup;
|
|
30
|
+
|
|
31
|
+
const worldCoords = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < GraphicData.length; i += 2) {
|
|
34
|
+
const point = imageToWorldCoords(referencedImageId, [
|
|
35
|
+
GraphicData[i],
|
|
36
|
+
GraphicData[i + 1]
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
worldCoords.push(point);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const distanceBetweenFirstAndLastPoint = vec3.distance(
|
|
43
|
+
worldCoords[worldCoords.length - 1],
|
|
44
|
+
worldCoords[0]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
let isOpenContour = true;
|
|
48
|
+
|
|
49
|
+
// If the contour is closed, this should have been encoded as exactly the same point, so check for a very small difference.
|
|
50
|
+
if (distanceBetweenFirstAndLastPoint < closedContourThreshold) {
|
|
51
|
+
worldCoords.pop(); // Remove the last element which is duplicated.
|
|
52
|
+
|
|
53
|
+
isOpenContour = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let points = [];
|
|
57
|
+
|
|
58
|
+
if (isOpenContour) {
|
|
59
|
+
points.push(worldCoords[0], worldCoords[worldCoords.length - 1]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const state = defaultState;
|
|
63
|
+
|
|
64
|
+
state.annotation.data = {
|
|
65
|
+
polyline: worldCoords,
|
|
66
|
+
isOpenContour,
|
|
67
|
+
handles: {
|
|
68
|
+
points,
|
|
69
|
+
activeHandleIndex: null,
|
|
70
|
+
textBox: {
|
|
71
|
+
hasMoved: false
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
frameNumber: ReferencedFrameNumber
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static getTID300RepresentationArguments(tool, worldToImageCoords) {
|
|
81
|
+
const { data, finding, findingSites, metadata } = tool;
|
|
82
|
+
const { isOpenContour, polyline } = data;
|
|
83
|
+
|
|
84
|
+
const { referencedImageId } = metadata;
|
|
85
|
+
|
|
86
|
+
if (!referencedImageId) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"PlanarFreehandROI.getTID300RepresentationArguments: referencedImageId is not defined"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const points = polyline.map(worldPos =>
|
|
93
|
+
worldToImageCoords(referencedImageId, worldPos)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!isOpenContour) {
|
|
97
|
+
// Need to repeat the first point at the end of to have an explicitly closed contour.
|
|
98
|
+
const firstPoint = points[0];
|
|
99
|
+
|
|
100
|
+
// Explicitly expand to avoid ciruclar references.
|
|
101
|
+
points.push([firstPoint[0], firstPoint[1]]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const area = 0; // TODO -> The tool doesn't have these stats yet.
|
|
105
|
+
const perimeter = 0;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
points,
|
|
109
|
+
area,
|
|
110
|
+
perimeter,
|
|
111
|
+
trackingIdentifierTextValue,
|
|
112
|
+
finding,
|
|
113
|
+
findingSites: findingSites || []
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
PlanarFreehandROI.toolType = PLANARFREEHANDROI;
|
|
119
|
+
PlanarFreehandROI.utilityToolType = PLANARFREEHANDROI;
|
|
120
|
+
PlanarFreehandROI.TID300Representation = TID300Polyline;
|
|
121
|
+
PlanarFreehandROI.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
|
|
122
|
+
if (!TrackingIdentifier.includes(":")) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":");
|
|
127
|
+
|
|
128
|
+
if (cornerstone3DTag !== CORNERSTONE_3D_TAG) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return toolType === PLANARFREEHANDROI;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
MeasurementReport.registerTool(PlanarFreehandROI);
|
|
136
|
+
|
|
137
|
+
export default PlanarFreehandROI;
|