@cornerstonejs/adapters 3.0.0-beta.5 → 3.0.0
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/dist/esm/adapters/Cornerstone/Angle.js +61 -74
- package/dist/esm/adapters/Cornerstone/ArrowAnnotate.js +76 -76
- package/dist/esm/adapters/Cornerstone/Bidirectional.js +130 -146
- package/dist/esm/adapters/Cornerstone/CircleRoi.js +96 -95
- package/dist/esm/adapters/Cornerstone/CobbAngle.js +66 -79
- package/dist/esm/adapters/Cornerstone/EllipticalRoi.js +152 -152
- package/dist/esm/adapters/Cornerstone/FreehandRoi.js +68 -69
- package/dist/esm/adapters/Cornerstone/Length.js +54 -63
- package/dist/esm/adapters/Cornerstone/MeasurementReport.js +221 -224
- package/dist/esm/adapters/Cornerstone/ParametricMap.js +88 -110
- package/dist/esm/adapters/Cornerstone/RectangleRoi.js +72 -78
- package/dist/esm/adapters/Cornerstone/Segmentation.js +7 -7
- package/dist/esm/adapters/Cornerstone/Segmentation_3X.js +113 -104
- package/dist/esm/adapters/Cornerstone/Segmentation_4X.js +434 -462
- package/dist/esm/adapters/Cornerstone/index.js +14 -14
- package/dist/esm/adapters/Cornerstone3D/Angle.d.ts +2 -5
- package/dist/esm/adapters/Cornerstone3D/Angle.js +89 -100
- package/dist/esm/adapters/Cornerstone3D/ArrowAnnotate.d.ts +5 -11
- package/dist/esm/adapters/Cornerstone3D/ArrowAnnotate.js +106 -118
- package/dist/esm/adapters/Cornerstone3D/BaseAdapter3D.d.ts +43 -0
- package/dist/esm/adapters/Cornerstone3D/BaseAdapter3D.js +92 -0
- package/dist/esm/adapters/Cornerstone3D/Bidirectional.d.ts +2 -5
- package/dist/esm/adapters/Cornerstone3D/Bidirectional.js +118 -133
- package/dist/esm/adapters/Cornerstone3D/CircleROI.d.ts +2 -7
- package/dist/esm/adapters/Cornerstone3D/CircleROI.js +85 -85
- package/dist/esm/adapters/Cornerstone3D/CobbAngle.d.ts +2 -5
- package/dist/esm/adapters/Cornerstone3D/CobbAngle.js +93 -104
- package/dist/esm/adapters/Cornerstone3D/CodingScheme.js +5 -5
- package/dist/esm/adapters/Cornerstone3D/EllipticalROI.d.ts +2 -7
- package/dist/esm/adapters/Cornerstone3D/EllipticalROI.js +148 -149
- package/dist/esm/adapters/Cornerstone3D/KeyImage.d.ts +24 -0
- package/dist/esm/adapters/Cornerstone3D/KeyImage.js +49 -0
- package/dist/esm/adapters/Cornerstone3D/Length.d.ts +2 -10
- package/dist/esm/adapters/Cornerstone3D/Length.js +83 -93
- package/dist/esm/adapters/Cornerstone3D/MeasurementReport.d.ts +23 -4
- package/dist/esm/adapters/Cornerstone3D/MeasurementReport.js +259 -240
- package/dist/esm/adapters/Cornerstone3D/ParametricMap/generateToolState.js +8 -4
- package/dist/esm/adapters/Cornerstone3D/PlanarFreehandROI.d.ts +3 -5
- package/dist/esm/adapters/Cornerstone3D/PlanarFreehandROI.js +99 -115
- package/dist/esm/adapters/Cornerstone3D/Probe.d.ts +4 -17
- package/dist/esm/adapters/Cornerstone3D/Probe.js +38 -79
- package/dist/esm/adapters/Cornerstone3D/RTStruct/RTSS.js +60 -52
- package/dist/esm/adapters/Cornerstone3D/RTStruct/index.js +3 -1
- package/dist/esm/adapters/Cornerstone3D/RTStruct/utilities/getPatientModule.js +5 -5
- package/dist/esm/adapters/Cornerstone3D/RTStruct/utilities/getReferencedFrameOfReferenceSequence.js +14 -10
- package/dist/esm/adapters/Cornerstone3D/RTStruct/utilities/getReferencedSeriesSequence.js +17 -11
- package/dist/esm/adapters/Cornerstone3D/RTStruct/utilities/getStructureSetModule.js +3 -1
- package/dist/esm/adapters/Cornerstone3D/RectangleROI.d.ts +2 -5
- package/dist/esm/adapters/Cornerstone3D/RectangleROI.js +73 -85
- package/dist/esm/adapters/Cornerstone3D/Segmentation/generateLabelMaps2DFrom3D.js +15 -13
- package/dist/esm/adapters/Cornerstone3D/Segmentation/generateSegmentation.js +15 -9
- package/dist/esm/adapters/Cornerstone3D/Segmentation/generateToolState.js +10 -10
- package/dist/esm/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.js +134 -151
- package/dist/esm/adapters/Cornerstone3D/UltrasoundDirectional.d.ts +2 -5
- package/dist/esm/adapters/Cornerstone3D/UltrasoundDirectional.js +68 -84
- package/dist/esm/adapters/Cornerstone3D/index.d.ts +4 -0
- package/dist/esm/adapters/Cornerstone3D/index.js +21 -17
- package/dist/esm/adapters/VTKjs/Segmentation.js +66 -72
- package/dist/esm/adapters/VTKjs/index.js +2 -2
- package/dist/esm/adapters/helpers/checkIfPerpendicular.js +2 -2
- package/dist/esm/adapters/helpers/checkOrientation.js +8 -8
- package/dist/esm/adapters/helpers/codeMeaningEquals.js +2 -2
- package/dist/esm/adapters/helpers/compareArrays.js +4 -2
- package/dist/esm/adapters/helpers/downloadDICOMData.js +6 -4
- package/dist/esm/adapters/helpers/getDatasetsFromImages.js +20 -18
- package/dist/esm/adapters/helpers/graphicTypeEquals.js +2 -2
- package/dist/esm/adapters/helpers/toArray.js +1 -3
- package/dist/esm/adapters/index.d.ts +2 -0
- package/dist/esm/adapters/index.js +6 -5
- package/dist/esm/node_modules/@babel/runtime/helpers/esm/defineProperty.js +18 -0
- package/dist/esm/node_modules/@babel/runtime/helpers/esm/toPrimitive.js +14 -0
- package/dist/esm/node_modules/@babel/runtime/helpers/esm/toPropertyKey.js +9 -0
- package/dist/esm/node_modules/@babel/runtime/helpers/esm/typeof.js +11 -0
- package/package.json +4 -4
- package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +0 -493
- package/dist/esm/adapters/Cornerstone3D/isValidCornerstoneTrackingIdentifier.js +0 -18
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { utilities, data, normalizers, derivations, log } from 'dcmjs';
|
|
1
|
+
import { utilities, log, data, normalizers, derivations } from 'dcmjs';
|
|
3
2
|
import ndarray from 'ndarray';
|
|
4
3
|
import getDatasetsFromImages from '../helpers/getDatasetsFromImages.js';
|
|
5
4
|
import checkOrientation from '../helpers/checkOrientation.js';
|
|
6
5
|
import compareArrays from '../helpers/compareArrays.js';
|
|
7
6
|
import Events from '../enums/Events.js';
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
rotateDirectionCosinesInPlane
|
|
11
|
-
flipIOP
|
|
12
|
-
flipMatrix2D
|
|
13
|
-
rotateMatrix902D
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
const {
|
|
9
|
+
rotateDirectionCosinesInPlane,
|
|
10
|
+
flipImageOrientationPatient: flipIOP,
|
|
11
|
+
flipMatrix2D,
|
|
12
|
+
rotateMatrix902D
|
|
13
|
+
} = utilities.orientation;
|
|
14
|
+
const {
|
|
15
|
+
BitArray,
|
|
16
|
+
DicomMessage,
|
|
17
|
+
DicomMetaDictionary
|
|
18
|
+
} = data;
|
|
19
|
+
const {
|
|
20
|
+
Normalizer
|
|
21
|
+
} = normalizers;
|
|
22
|
+
const {
|
|
23
|
+
Segmentation: SegmentationDerivation
|
|
24
|
+
} = derivations;
|
|
25
|
+
const {
|
|
26
|
+
encode,
|
|
27
|
+
decode
|
|
28
|
+
} = utilities.compression;
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
*
|
|
@@ -27,7 +34,7 @@ var _utilities$compressio = utilities.compression,
|
|
|
27
34
|
* @property {Object[]} segments - The cornerstoneTools segment metadata that corresponds to the
|
|
28
35
|
* seriesInstanceUid.
|
|
29
36
|
*/
|
|
30
|
-
|
|
37
|
+
const generateSegmentationDefaultOptions = {
|
|
31
38
|
includeSliceSpacing: true,
|
|
32
39
|
rleEncode: false
|
|
33
40
|
};
|
|
@@ -43,9 +50,9 @@ var generateSegmentationDefaultOptions = {
|
|
|
43
50
|
* @returns {Blob}
|
|
44
51
|
*/
|
|
45
52
|
function generateSegmentation(images, inputLabelmaps3D) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
let userOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
54
|
+
const isMultiframe = images[0].imageId.includes("?frame");
|
|
55
|
+
const segmentation = _createSegFromImages(images, isMultiframe, userOptions);
|
|
49
56
|
return fillSegmentation(segmentation, inputLabelmaps3D, userOptions);
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -59,63 +66,63 @@ function generateSegmentation(images, inputLabelmaps3D) {
|
|
|
59
66
|
* @returns {object} The filled segmentation object.
|
|
60
67
|
*/
|
|
61
68
|
function fillSegmentation(segmentation, inputLabelmaps3D) {
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
let userOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
70
|
+
const options = Object.assign({}, generateSegmentationDefaultOptions, userOptions);
|
|
64
71
|
|
|
65
72
|
// Use another variable so we don't redefine labelmaps3D.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const labelmaps3D = Array.isArray(inputLabelmaps3D) ? inputLabelmaps3D : [inputLabelmaps3D];
|
|
74
|
+
let numberOfFrames = 0;
|
|
75
|
+
const referencedFramesPerLabelmap = [];
|
|
76
|
+
for (let labelmapIndex = 0; labelmapIndex < labelmaps3D.length; labelmapIndex++) {
|
|
77
|
+
const labelmap3D = labelmaps3D[labelmapIndex];
|
|
78
|
+
const {
|
|
79
|
+
labelmaps2D,
|
|
80
|
+
metadata
|
|
81
|
+
} = labelmap3D;
|
|
82
|
+
const referencedFramesPerSegment = [];
|
|
83
|
+
for (let i = 1; i < metadata.length; i++) {
|
|
75
84
|
if (metadata[i]) {
|
|
76
85
|
referencedFramesPerSegment[i] = [];
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (labelmaps2D[
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
for (let i = 0; i < labelmaps2D.length; i++) {
|
|
89
|
+
const labelmap2D = labelmaps2D[i];
|
|
90
|
+
if (labelmaps2D[i]) {
|
|
91
|
+
const {
|
|
92
|
+
segmentsOnLabelmap
|
|
93
|
+
} = labelmap2D;
|
|
94
|
+
segmentsOnLabelmap.forEach(segmentIndex => {
|
|
84
95
|
if (segmentIndex !== 0) {
|
|
85
|
-
referencedFramesPerSegment[segmentIndex].push(
|
|
96
|
+
referencedFramesPerSegment[segmentIndex].push(i);
|
|
86
97
|
numberOfFrames++;
|
|
87
98
|
}
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
|
-
};
|
|
91
|
-
for (var _i = 0; _i < labelmaps2D.length; _i++) {
|
|
92
|
-
_loop2(_i);
|
|
93
101
|
}
|
|
94
102
|
referencedFramesPerLabelmap[labelmapIndex] = referencedFramesPerSegment;
|
|
95
|
-
};
|
|
96
|
-
for (var labelmapIndex = 0; labelmapIndex < labelmaps3D.length; labelmapIndex++) {
|
|
97
|
-
_loop();
|
|
98
103
|
}
|
|
99
104
|
segmentation.setNumberOfFrames(numberOfFrames);
|
|
100
|
-
for (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
for (let labelmapIndex = 0; labelmapIndex < labelmaps3D.length; labelmapIndex++) {
|
|
106
|
+
const referencedFramesPerSegment = referencedFramesPerLabelmap[labelmapIndex];
|
|
107
|
+
const labelmap3D = labelmaps3D[labelmapIndex];
|
|
108
|
+
const {
|
|
109
|
+
metadata
|
|
110
|
+
} = labelmap3D;
|
|
111
|
+
for (let segmentIndex = 1; segmentIndex < referencedFramesPerSegment.length; segmentIndex++) {
|
|
112
|
+
const referencedFrameIndicies = referencedFramesPerSegment[segmentIndex];
|
|
106
113
|
if (referencedFrameIndicies) {
|
|
107
114
|
// Frame numbers start from 1.
|
|
108
|
-
|
|
115
|
+
const referencedFrameNumbers = referencedFrameIndicies.map(element => {
|
|
109
116
|
return element + 1;
|
|
110
117
|
});
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
const segmentMetadata = metadata[segmentIndex];
|
|
119
|
+
const labelmaps = _getLabelmapsFromReferencedFrameIndicies(labelmap3D, referencedFrameIndicies);
|
|
113
120
|
segmentation.addSegmentFromLabelmap(segmentMetadata, labelmaps, segmentIndex, referencedFrameNumbers);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
if (options.rleEncode) {
|
|
118
|
-
|
|
125
|
+
const rleEncodedFrames = encode(segmentation.dataset.PixelData, numberOfFrames, segmentation.dataset.Rows, segmentation.dataset.Columns);
|
|
119
126
|
|
|
120
127
|
// Must use fractional now to RLE encode, as the DICOM standard only allows BitStored && BitsAllocated
|
|
121
128
|
// to be 1 for BINARY. This is not ideal and there should be a better format for compression in this manner
|
|
@@ -142,10 +149,12 @@ function fillSegmentation(segmentation, inputLabelmaps3D) {
|
|
|
142
149
|
return segmentation;
|
|
143
150
|
}
|
|
144
151
|
function _getLabelmapsFromReferencedFrameIndicies(labelmap3D, referencedFrameIndicies) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
const {
|
|
153
|
+
labelmaps2D
|
|
154
|
+
} = labelmap3D;
|
|
155
|
+
const labelmaps = [];
|
|
156
|
+
for (let i = 0; i < referencedFrameIndicies.length; i++) {
|
|
157
|
+
const frame = referencedFrameIndicies[i];
|
|
149
158
|
labelmaps.push(labelmaps2D[frame].pixelData);
|
|
150
159
|
}
|
|
151
160
|
return labelmaps;
|
|
@@ -159,7 +168,7 @@ function _getLabelmapsFromReferencedFrameIndicies(labelmap3D, referencedFrameInd
|
|
|
159
168
|
* @returns {Object} The Seg derived dataSet.
|
|
160
169
|
*/
|
|
161
170
|
function _createSegFromImages(images, isMultiframe, options) {
|
|
162
|
-
|
|
171
|
+
const multiframe = getDatasetsFromImages(images, isMultiframe);
|
|
163
172
|
return new SegmentationDerivation([multiframe], options);
|
|
164
173
|
}
|
|
165
174
|
|
|
@@ -178,8 +187,129 @@ function _createSegFromImages(images, isMultiframe, options) {
|
|
|
178
187
|
* @return {[][][]} 3D list containing the track of segments per frame for each labelMap
|
|
179
188
|
* (available only for the overlapping case).
|
|
180
189
|
*/
|
|
181
|
-
function generateToolState(
|
|
182
|
-
|
|
190
|
+
async function generateToolState(referencedImageIds, arrayBuffer, metadataProvider, options) {
|
|
191
|
+
const {
|
|
192
|
+
skipOverlapping = false,
|
|
193
|
+
tolerance = 1e-3,
|
|
194
|
+
TypedArrayConstructor = Uint8Array,
|
|
195
|
+
maxBytesPerChunk = 199000000,
|
|
196
|
+
eventTarget = null,
|
|
197
|
+
triggerEvent = null
|
|
198
|
+
} = options;
|
|
199
|
+
const dicomData = DicomMessage.readFile(arrayBuffer);
|
|
200
|
+
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
|
201
|
+
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
|
202
|
+
const multiframe = Normalizer.normalizeToDataset([dataset]);
|
|
203
|
+
const imagePlaneModule = metadataProvider.get("imagePlaneModule", referencedImageIds[0]);
|
|
204
|
+
const generalSeriesModule = metadataProvider.get("generalSeriesModule", referencedImageIds[0]);
|
|
205
|
+
const SeriesInstanceUID = generalSeriesModule.seriesInstanceUID;
|
|
206
|
+
if (!imagePlaneModule) {
|
|
207
|
+
console.warn("Insufficient metadata, imagePlaneModule missing.");
|
|
208
|
+
}
|
|
209
|
+
const ImageOrientationPatient = Array.isArray(imagePlaneModule.rowCosines) ? [...imagePlaneModule.rowCosines, ...imagePlaneModule.columnCosines] : [imagePlaneModule.rowCosines.x, imagePlaneModule.rowCosines.y, imagePlaneModule.rowCosines.z, imagePlaneModule.columnCosines.x, imagePlaneModule.columnCosines.y, imagePlaneModule.columnCosines.z];
|
|
210
|
+
|
|
211
|
+
// Get IOP from ref series, compute supported orientations:
|
|
212
|
+
const validOrientations = getValidOrientations(ImageOrientationPatient);
|
|
213
|
+
const sliceLength = multiframe.Columns * multiframe.Rows;
|
|
214
|
+
const segMetadata = getSegmentMetadata(multiframe, SeriesInstanceUID);
|
|
215
|
+
const TransferSyntaxUID = multiframe._meta.TransferSyntaxUID.Value[0];
|
|
216
|
+
let pixelData;
|
|
217
|
+
let pixelDataChunks;
|
|
218
|
+
if (TransferSyntaxUID === "1.2.840.10008.1.2.5") {
|
|
219
|
+
const rleEncodedFrames = Array.isArray(multiframe.PixelData) ? multiframe.PixelData : [multiframe.PixelData];
|
|
220
|
+
pixelData = decode(rleEncodedFrames, multiframe.Rows, multiframe.Columns);
|
|
221
|
+
if (multiframe.BitsStored === 1) {
|
|
222
|
+
console.warn("No implementation for rle + bitbacking.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Todo: need to test this with rle data
|
|
227
|
+
pixelDataChunks = [pixelData];
|
|
228
|
+
} else {
|
|
229
|
+
pixelDataChunks = unpackPixelData(multiframe, {
|
|
230
|
+
maxBytesPerChunk
|
|
231
|
+
});
|
|
232
|
+
if (!pixelDataChunks) {
|
|
233
|
+
throw new Error("Fractional segmentations are not yet supported");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const orientation = checkOrientation(multiframe, validOrientations, [imagePlaneModule.rows, imagePlaneModule.columns, referencedImageIds.length], tolerance);
|
|
237
|
+
|
|
238
|
+
// Pre-compute the sop UID to imageId index map so that in the for loop
|
|
239
|
+
// we don't have to call metadataProvider.get() for each imageId over
|
|
240
|
+
// and over again.
|
|
241
|
+
const sopUIDImageIdIndexMap = referencedImageIds.reduce((acc, imageId) => {
|
|
242
|
+
const {
|
|
243
|
+
sopInstanceUID
|
|
244
|
+
} = metadataProvider.get("generalImageModule", imageId);
|
|
245
|
+
acc[sopInstanceUID] = imageId;
|
|
246
|
+
return acc;
|
|
247
|
+
}, {});
|
|
248
|
+
let overlapping = false;
|
|
249
|
+
if (!skipOverlapping) {
|
|
250
|
+
overlapping = checkSEGsOverlapping(pixelDataChunks, multiframe, referencedImageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, sopUIDImageIdIndexMap);
|
|
251
|
+
}
|
|
252
|
+
let insertFunction;
|
|
253
|
+
switch (orientation) {
|
|
254
|
+
case "Planar":
|
|
255
|
+
if (overlapping) {
|
|
256
|
+
insertFunction = insertOverlappingPixelDataPlanar;
|
|
257
|
+
} else {
|
|
258
|
+
insertFunction = insertPixelDataPlanar;
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
case "Perpendicular":
|
|
262
|
+
//insertFunction = insertPixelDataPerpendicular;
|
|
263
|
+
throw new Error("Segmentations orthogonal to the acquisition plane of the source data are not yet supported.");
|
|
264
|
+
case "Oblique":
|
|
265
|
+
throw new Error("Segmentations oblique to the acquisition plane of the source data are not yet supported.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* if SEGs are overlapping:
|
|
269
|
+
1) the labelmapBuffer will contain M volumes which have non-overlapping segments;
|
|
270
|
+
2) segmentsOnFrame will have M * numberOfFrames values to track in which labelMap are the segments;
|
|
271
|
+
3) insertFunction will return the number of LabelMaps
|
|
272
|
+
4) generateToolState return is an array*/
|
|
273
|
+
|
|
274
|
+
const segmentsOnFrameArray = [];
|
|
275
|
+
segmentsOnFrameArray[0] = [];
|
|
276
|
+
const segmentsOnFrame = [];
|
|
277
|
+
const arrayBufferLength = sliceLength * referencedImageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
278
|
+
const labelmapBufferArray = [];
|
|
279
|
+
labelmapBufferArray[0] = new ArrayBuffer(arrayBufferLength);
|
|
280
|
+
|
|
281
|
+
// Pre-compute the indices and metadata so that we don't have to call
|
|
282
|
+
// a function for each imageId in the for loop.
|
|
283
|
+
const imageIdMaps = referencedImageIds.reduce((acc, curr, index) => {
|
|
284
|
+
acc.indices[curr] = index;
|
|
285
|
+
acc.metadata[curr] = metadataProvider.get("instance", curr);
|
|
286
|
+
return acc;
|
|
287
|
+
}, {
|
|
288
|
+
indices: {},
|
|
289
|
+
metadata: {}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// This is the centroid calculation for each segment Index, the data structure
|
|
293
|
+
// is a Map with key = segmentIndex and value = {imageIdIndex: centroid, ...}
|
|
294
|
+
// later on we will use this data structure to calculate the centroid of the
|
|
295
|
+
// segment in the labelmapBuffer
|
|
296
|
+
const segmentsPixelIndices = new Map();
|
|
297
|
+
const overlappingSegments = await insertFunction(segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, pixelDataChunks, multiframe, referencedImageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap, imageIdMaps, eventTarget, triggerEvent);
|
|
298
|
+
|
|
299
|
+
// calculate the centroid of each segment
|
|
300
|
+
const centroidXYZ = new Map();
|
|
301
|
+
segmentsPixelIndices.forEach((imageIdIndexBufferIndex, segmentIndex) => {
|
|
302
|
+
const centroids = calculateCentroid(imageIdIndexBufferIndex, multiframe, metadataProvider, referencedImageIds);
|
|
303
|
+
centroidXYZ.set(segmentIndex, centroids);
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
labelmapBufferArray,
|
|
307
|
+
segMetadata,
|
|
308
|
+
segmentsOnFrame,
|
|
309
|
+
segmentsOnFrameArray,
|
|
310
|
+
centroids: centroidXYZ,
|
|
311
|
+
overlappingSegments
|
|
312
|
+
};
|
|
183
313
|
}
|
|
184
314
|
|
|
185
315
|
// function insertPixelDataPerpendicular(
|
|
@@ -364,154 +494,27 @@ function generateToolState(_x, _x2, _x3, _x4) {
|
|
|
364
494
|
*
|
|
365
495
|
* @returns {String} Returns the imageId
|
|
366
496
|
*/
|
|
367
|
-
function _generateToolState() {
|
|
368
|
-
_generateToolState = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee(referencedImageIds, arrayBuffer, metadataProvider, options) {
|
|
369
|
-
var _options$skipOverlapp, skipOverlapping, _options$tolerance, tolerance, _options$TypedArrayCo, TypedArrayConstructor, _options$maxBytesPerC, maxBytesPerChunk, _options$eventTarget, eventTarget, _options$triggerEvent, triggerEvent, dicomData, dataset, multiframe, imagePlaneModule, generalSeriesModule, SeriesInstanceUID, ImageOrientationPatient, validOrientations, sliceLength, segMetadata, TransferSyntaxUID, pixelData, pixelDataChunks, rleEncodedFrames, orientation, sopUIDImageIdIndexMap, overlapping, insertFunction, segmentsOnFrameArray, segmentsOnFrame, arrayBufferLength, labelmapBufferArray, imageIdMaps, segmentsPixelIndices, overlappingSegments, centroidXYZ;
|
|
370
|
-
return _regeneratorRuntime().wrap(function _callee$(_context) {
|
|
371
|
-
while (1) switch (_context.prev = _context.next) {
|
|
372
|
-
case 0:
|
|
373
|
-
_options$skipOverlapp = options.skipOverlapping, skipOverlapping = _options$skipOverlapp === void 0 ? false : _options$skipOverlapp, _options$tolerance = options.tolerance, tolerance = _options$tolerance === void 0 ? 1e-3 : _options$tolerance, _options$TypedArrayCo = options.TypedArrayConstructor, TypedArrayConstructor = _options$TypedArrayCo === void 0 ? Uint8Array : _options$TypedArrayCo, _options$maxBytesPerC = options.maxBytesPerChunk, maxBytesPerChunk = _options$maxBytesPerC === void 0 ? 199000000 : _options$maxBytesPerC, _options$eventTarget = options.eventTarget, eventTarget = _options$eventTarget === void 0 ? null : _options$eventTarget, _options$triggerEvent = options.triggerEvent, triggerEvent = _options$triggerEvent === void 0 ? null : _options$triggerEvent;
|
|
374
|
-
dicomData = DicomMessage.readFile(arrayBuffer);
|
|
375
|
-
dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
|
376
|
-
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
|
377
|
-
multiframe = Normalizer.normalizeToDataset([dataset]);
|
|
378
|
-
imagePlaneModule = metadataProvider.get("imagePlaneModule", referencedImageIds[0]);
|
|
379
|
-
generalSeriesModule = metadataProvider.get("generalSeriesModule", referencedImageIds[0]);
|
|
380
|
-
SeriesInstanceUID = generalSeriesModule.seriesInstanceUID;
|
|
381
|
-
if (!imagePlaneModule) {
|
|
382
|
-
console.warn("Insufficient metadata, imagePlaneModule missing.");
|
|
383
|
-
}
|
|
384
|
-
ImageOrientationPatient = Array.isArray(imagePlaneModule.rowCosines) ? [].concat(_toConsumableArray(imagePlaneModule.rowCosines), _toConsumableArray(imagePlaneModule.columnCosines)) : [imagePlaneModule.rowCosines.x, imagePlaneModule.rowCosines.y, imagePlaneModule.rowCosines.z, imagePlaneModule.columnCosines.x, imagePlaneModule.columnCosines.y, imagePlaneModule.columnCosines.z]; // Get IOP from ref series, compute supported orientations:
|
|
385
|
-
validOrientations = getValidOrientations(ImageOrientationPatient);
|
|
386
|
-
sliceLength = multiframe.Columns * multiframe.Rows;
|
|
387
|
-
segMetadata = getSegmentMetadata(multiframe, SeriesInstanceUID);
|
|
388
|
-
TransferSyntaxUID = multiframe._meta.TransferSyntaxUID.Value[0];
|
|
389
|
-
if (!(TransferSyntaxUID === "1.2.840.10008.1.2.5")) {
|
|
390
|
-
_context.next = 23;
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
rleEncodedFrames = Array.isArray(multiframe.PixelData) ? multiframe.PixelData : [multiframe.PixelData];
|
|
394
|
-
pixelData = decode(rleEncodedFrames, multiframe.Rows, multiframe.Columns);
|
|
395
|
-
if (!(multiframe.BitsStored === 1)) {
|
|
396
|
-
_context.next = 20;
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
console.warn("No implementation for rle + bitbacking.");
|
|
400
|
-
return _context.abrupt("return");
|
|
401
|
-
case 20:
|
|
402
|
-
// Todo: need to test this with rle data
|
|
403
|
-
pixelDataChunks = [pixelData];
|
|
404
|
-
_context.next = 26;
|
|
405
|
-
break;
|
|
406
|
-
case 23:
|
|
407
|
-
pixelDataChunks = unpackPixelData(multiframe, {
|
|
408
|
-
maxBytesPerChunk: maxBytesPerChunk
|
|
409
|
-
});
|
|
410
|
-
if (pixelDataChunks) {
|
|
411
|
-
_context.next = 26;
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
throw new Error("Fractional segmentations are not yet supported");
|
|
415
|
-
case 26:
|
|
416
|
-
orientation = checkOrientation(multiframe, validOrientations, [imagePlaneModule.rows, imagePlaneModule.columns, referencedImageIds.length], tolerance); // Pre-compute the sop UID to imageId index map so that in the for loop
|
|
417
|
-
// we don't have to call metadataProvider.get() for each imageId over
|
|
418
|
-
// and over again.
|
|
419
|
-
sopUIDImageIdIndexMap = referencedImageIds.reduce(function (acc, imageId) {
|
|
420
|
-
var _metadataProvider$get = metadataProvider.get("generalImageModule", imageId),
|
|
421
|
-
sopInstanceUID = _metadataProvider$get.sopInstanceUID;
|
|
422
|
-
acc[sopInstanceUID] = imageId;
|
|
423
|
-
return acc;
|
|
424
|
-
}, {});
|
|
425
|
-
overlapping = false;
|
|
426
|
-
if (!skipOverlapping) {
|
|
427
|
-
overlapping = checkSEGsOverlapping(pixelDataChunks, multiframe, referencedImageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, sopUIDImageIdIndexMap);
|
|
428
|
-
}
|
|
429
|
-
_context.t0 = orientation;
|
|
430
|
-
_context.next = _context.t0 === "Planar" ? 33 : _context.t0 === "Perpendicular" ? 35 : _context.t0 === "Oblique" ? 36 : 37;
|
|
431
|
-
break;
|
|
432
|
-
case 33:
|
|
433
|
-
if (overlapping) {
|
|
434
|
-
insertFunction = insertOverlappingPixelDataPlanar;
|
|
435
|
-
} else {
|
|
436
|
-
insertFunction = insertPixelDataPlanar;
|
|
437
|
-
}
|
|
438
|
-
return _context.abrupt("break", 37);
|
|
439
|
-
case 35:
|
|
440
|
-
throw new Error("Segmentations orthogonal to the acquisition plane of the source data are not yet supported.");
|
|
441
|
-
case 36:
|
|
442
|
-
throw new Error("Segmentations oblique to the acquisition plane of the source data are not yet supported.");
|
|
443
|
-
case 37:
|
|
444
|
-
/* if SEGs are overlapping:
|
|
445
|
-
1) the labelmapBuffer will contain M volumes which have non-overlapping segments;
|
|
446
|
-
2) segmentsOnFrame will have M * numberOfFrames values to track in which labelMap are the segments;
|
|
447
|
-
3) insertFunction will return the number of LabelMaps
|
|
448
|
-
4) generateToolState return is an array*/
|
|
449
|
-
segmentsOnFrameArray = [];
|
|
450
|
-
segmentsOnFrameArray[0] = [];
|
|
451
|
-
segmentsOnFrame = [];
|
|
452
|
-
arrayBufferLength = sliceLength * referencedImageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
453
|
-
labelmapBufferArray = [];
|
|
454
|
-
labelmapBufferArray[0] = new ArrayBuffer(arrayBufferLength);
|
|
455
|
-
|
|
456
|
-
// Pre-compute the indices and metadata so that we don't have to call
|
|
457
|
-
// a function for each imageId in the for loop.
|
|
458
|
-
imageIdMaps = referencedImageIds.reduce(function (acc, curr, index) {
|
|
459
|
-
acc.indices[curr] = index;
|
|
460
|
-
acc.metadata[curr] = metadataProvider.get("instance", curr);
|
|
461
|
-
return acc;
|
|
462
|
-
}, {
|
|
463
|
-
indices: {},
|
|
464
|
-
metadata: {}
|
|
465
|
-
}); // This is the centroid calculation for each segment Index, the data structure
|
|
466
|
-
// is a Map with key = segmentIndex and value = {imageIdIndex: centroid, ...}
|
|
467
|
-
// later on we will use this data structure to calculate the centroid of the
|
|
468
|
-
// segment in the labelmapBuffer
|
|
469
|
-
segmentsPixelIndices = new Map();
|
|
470
|
-
_context.next = 47;
|
|
471
|
-
return insertFunction(segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, pixelDataChunks, multiframe, referencedImageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap, imageIdMaps, eventTarget, triggerEvent);
|
|
472
|
-
case 47:
|
|
473
|
-
overlappingSegments = _context.sent;
|
|
474
|
-
// calculate the centroid of each segment
|
|
475
|
-
centroidXYZ = new Map();
|
|
476
|
-
segmentsPixelIndices.forEach(function (imageIdIndexBufferIndex, segmentIndex) {
|
|
477
|
-
var centroids = calculateCentroid(imageIdIndexBufferIndex, multiframe, metadataProvider, referencedImageIds);
|
|
478
|
-
centroidXYZ.set(segmentIndex, centroids);
|
|
479
|
-
});
|
|
480
|
-
return _context.abrupt("return", {
|
|
481
|
-
labelmapBufferArray: labelmapBufferArray,
|
|
482
|
-
segMetadata: segMetadata,
|
|
483
|
-
segmentsOnFrame: segmentsOnFrame,
|
|
484
|
-
segmentsOnFrameArray: segmentsOnFrameArray,
|
|
485
|
-
centroids: centroidXYZ,
|
|
486
|
-
overlappingSegments: overlappingSegments
|
|
487
|
-
});
|
|
488
|
-
case 51:
|
|
489
|
-
case "end":
|
|
490
|
-
return _context.stop();
|
|
491
|
-
}
|
|
492
|
-
}, _callee);
|
|
493
|
-
}));
|
|
494
|
-
return _generateToolState.apply(this, arguments);
|
|
495
|
-
}
|
|
496
497
|
function findReferenceSourceImageId(multiframe, frameSegment, imageIds, metadataProvider, tolerance, sopUIDImageIdIndexMap) {
|
|
497
|
-
|
|
498
|
+
let imageId = undefined;
|
|
498
499
|
if (!multiframe) {
|
|
499
500
|
return imageId;
|
|
500
501
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
502
|
+
const {
|
|
503
|
+
FrameOfReferenceUID,
|
|
504
|
+
PerFrameFunctionalGroupsSequence,
|
|
505
|
+
SourceImageSequence,
|
|
506
|
+
ReferencedSeriesSequence
|
|
507
|
+
} = multiframe;
|
|
505
508
|
if (!PerFrameFunctionalGroupsSequence || PerFrameFunctionalGroupsSequence.length === 0) {
|
|
506
509
|
return imageId;
|
|
507
510
|
}
|
|
508
|
-
|
|
511
|
+
const PerFrameFunctionalGroup = PerFrameFunctionalGroupsSequence[frameSegment];
|
|
509
512
|
if (!PerFrameFunctionalGroup) {
|
|
510
513
|
return imageId;
|
|
511
514
|
}
|
|
512
|
-
|
|
515
|
+
let frameSourceImageSequence = undefined;
|
|
513
516
|
if (PerFrameFunctionalGroup.DerivationImageSequence) {
|
|
514
|
-
|
|
517
|
+
let DerivationImageSequence = PerFrameFunctionalGroup.DerivationImageSequence;
|
|
515
518
|
if (Array.isArray(DerivationImageSequence)) {
|
|
516
519
|
if (DerivationImageSequence.length !== 0) {
|
|
517
520
|
DerivationImageSequence = DerivationImageSequence[0];
|
|
@@ -537,8 +540,8 @@ function findReferenceSourceImageId(multiframe, frameSegment, imageIds, metadata
|
|
|
537
540
|
imageId = getImageIdOfSourceImageBySourceImageSequence(frameSourceImageSequence, sopUIDImageIdIndexMap);
|
|
538
541
|
}
|
|
539
542
|
if (imageId === undefined && ReferencedSeriesSequence) {
|
|
540
|
-
|
|
541
|
-
|
|
543
|
+
const referencedSeriesSequence = Array.isArray(ReferencedSeriesSequence) ? ReferencedSeriesSequence[0] : ReferencedSeriesSequence;
|
|
544
|
+
const ReferencedSeriesInstanceUID = referencedSeriesSequence.SeriesInstanceUID;
|
|
542
545
|
imageId = getImageIdOfSourceImagebyGeometry(ReferencedSeriesInstanceUID, FrameOfReferenceUID, PerFrameFunctionalGroup, imageIds, metadataProvider, tolerance);
|
|
543
546
|
}
|
|
544
547
|
return imageId;
|
|
@@ -550,18 +553,20 @@ function findReferenceSourceImageId(multiframe, frameSegment, imageIds, metadata
|
|
|
550
553
|
*/
|
|
551
554
|
|
|
552
555
|
function checkSEGsOverlapping(pixelData, multiframe, imageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, sopUIDImageIdIndexMap) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
556
|
+
const {
|
|
557
|
+
SharedFunctionalGroupsSequence,
|
|
558
|
+
PerFrameFunctionalGroupsSequence,
|
|
559
|
+
SegmentSequence,
|
|
560
|
+
Rows,
|
|
561
|
+
Columns
|
|
562
|
+
} = multiframe;
|
|
563
|
+
let numberOfSegs = SegmentSequence.length;
|
|
559
564
|
if (numberOfSegs < 2) {
|
|
560
565
|
return false;
|
|
561
566
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
567
|
+
const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
|
|
568
|
+
const sliceLength = Columns * Rows;
|
|
569
|
+
const groupsLen = PerFrameFunctionalGroupsSequence.length;
|
|
565
570
|
|
|
566
571
|
/** sort groupsLen to have all the segments for each frame in an array
|
|
567
572
|
* frame 2 : 1, 2
|
|
@@ -569,91 +574,76 @@ function checkSEGsOverlapping(pixelData, multiframe, imageIds, validOrientations
|
|
|
569
574
|
* frame 5 : 4
|
|
570
575
|
*/
|
|
571
576
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
segmentArray.push(frameSegment);
|
|
591
|
-
frameSegmentsMapping.set(imageIdIndex, segmentArray);
|
|
592
|
-
}
|
|
593
|
-
} else {
|
|
594
|
-
frameSegmentsMapping.set(imageIdIndex, [frameSegment]);
|
|
577
|
+
let frameSegmentsMapping = new Map();
|
|
578
|
+
for (let frameSegment = 0; frameSegment < groupsLen; ++frameSegment) {
|
|
579
|
+
const segmentIndex = getSegmentIndex(multiframe, frameSegment);
|
|
580
|
+
if (segmentIndex === undefined) {
|
|
581
|
+
console.warn("Could not retrieve the segment index for frame segment " + frameSegment + ", skipping this frame.");
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const imageId = findReferenceSourceImageId(multiframe, frameSegment, imageIds, metadataProvider, tolerance, sopUIDImageIdIndexMap);
|
|
585
|
+
if (!imageId) {
|
|
586
|
+
console.warn("Image not present in stack, can't import frame : " + frameSegment + ".");
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const imageIdIndex = imageIds.findIndex(element => element === imageId);
|
|
590
|
+
if (frameSegmentsMapping.has(imageIdIndex)) {
|
|
591
|
+
let segmentArray = frameSegmentsMapping.get(imageIdIndex);
|
|
592
|
+
if (!segmentArray.includes(frameSegment)) {
|
|
593
|
+
segmentArray.push(frameSegment);
|
|
594
|
+
frameSegmentsMapping.set(imageIdIndex, segmentArray);
|
|
595
595
|
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
_ret = _loop3();
|
|
600
|
-
if (_ret === 0) continue;
|
|
596
|
+
} else {
|
|
597
|
+
frameSegmentsMapping.set(imageIdIndex, [frameSegment]);
|
|
598
|
+
}
|
|
601
599
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
for (var j = 0, len = data.length; j < len; ++j) {
|
|
622
|
-
if (data[j] !== 0) {
|
|
623
|
-
temp2DArray[j]++;
|
|
624
|
-
if (temp2DArray[j] > 1) {
|
|
625
|
-
return true;
|
|
626
|
-
}
|
|
600
|
+
for (let [, role] of frameSegmentsMapping.entries()) {
|
|
601
|
+
let temp2DArray = new TypedArrayConstructor(sliceLength).fill(0);
|
|
602
|
+
for (let i = 0; i < role.length; ++i) {
|
|
603
|
+
const frameSegment = role[i];
|
|
604
|
+
const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[frameSegment];
|
|
605
|
+
const ImageOrientationPatientI = sharedImageOrientationPatient || PerFrameFunctionalGroups.PlaneOrientationSequence.ImageOrientationPatient;
|
|
606
|
+
const view = readFromUnpackedChunks(pixelData, frameSegment * sliceLength, sliceLength);
|
|
607
|
+
const pixelDataI2D = ndarray(view, [Rows, Columns]);
|
|
608
|
+
const alignedPixelDataI = alignPixelDataWithSourceData(pixelDataI2D, ImageOrientationPatientI, validOrientations, tolerance);
|
|
609
|
+
if (!alignedPixelDataI) {
|
|
610
|
+
console.warn("Individual SEG frames are out of plane with respect to the first SEG frame, this is not yet supported, skipping this frame.");
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const data = alignedPixelDataI.data;
|
|
614
|
+
for (let j = 0, len = data.length; j < len; ++j) {
|
|
615
|
+
if (data[j] !== 0) {
|
|
616
|
+
temp2DArray[j]++;
|
|
617
|
+
if (temp2DArray[j] > 1) {
|
|
618
|
+
return true;
|
|
627
619
|
}
|
|
628
620
|
}
|
|
629
621
|
}
|
|
630
622
|
}
|
|
631
|
-
} catch (err) {
|
|
632
|
-
_iterator.e(err);
|
|
633
|
-
} finally {
|
|
634
|
-
_iterator.f();
|
|
635
623
|
}
|
|
636
624
|
return false;
|
|
637
625
|
}
|
|
638
626
|
function insertOverlappingPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, pixelData, multiframe, imageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
627
|
+
const {
|
|
628
|
+
SharedFunctionalGroupsSequence,
|
|
629
|
+
PerFrameFunctionalGroupsSequence,
|
|
630
|
+
Rows,
|
|
631
|
+
Columns
|
|
632
|
+
} = multiframe;
|
|
633
|
+
const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
|
|
634
|
+
const sliceLength = Columns * Rows;
|
|
635
|
+
const arrayBufferLength = sliceLength * imageIds.length * TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
646
636
|
// indicate the number of labelMaps
|
|
647
|
-
|
|
637
|
+
let M = 1;
|
|
648
638
|
|
|
649
639
|
// indicate the current labelMap array index;
|
|
650
|
-
|
|
640
|
+
let m = 0;
|
|
651
641
|
|
|
652
642
|
// temp array for checking overlaps
|
|
653
|
-
|
|
643
|
+
let tempBuffer = labelmapBufferArray[m].slice(0);
|
|
654
644
|
|
|
655
645
|
// temp list for checking overlaps
|
|
656
|
-
|
|
646
|
+
let tempSegmentsOnFrame = structuredClone(segmentsOnFrameArray[m]);
|
|
657
647
|
|
|
658
648
|
/** split overlapping SEGs algorithm for each segment:
|
|
659
649
|
* A) copy the labelmapBuffer in the array with index 0
|
|
@@ -662,81 +652,71 @@ function insertOverlappingPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray,
|
|
|
662
652
|
* D) if overlap, repeat increasing the index m up to M (if out of memory, add new buffer in the array and M++);
|
|
663
653
|
*/
|
|
664
654
|
|
|
665
|
-
|
|
666
|
-
for (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (m >= M) {
|
|
710
|
-
labelmapBufferArray[m] = new ArrayBuffer(arrayBufferLength);
|
|
711
|
-
segmentsOnFrameArray[m] = [];
|
|
712
|
-
M++;
|
|
713
|
-
}
|
|
714
|
-
tempBuffer = labelmapBufferArray[m].slice(0);
|
|
715
|
-
tempSegmentsOnFrame = structuredClone(segmentsOnFrameArray[m]);
|
|
716
|
-
_i2 = 0;
|
|
717
|
-
break;
|
|
718
|
-
} else {
|
|
719
|
-
labelmap2DView[j] = segmentIndex;
|
|
720
|
-
segmentOnFrame = true;
|
|
655
|
+
let numberOfSegs = multiframe.SegmentSequence.length;
|
|
656
|
+
for (let segmentIndexToProcess = 1; segmentIndexToProcess <= numberOfSegs; ++segmentIndexToProcess) {
|
|
657
|
+
for (let i = 0, groupsLen = PerFrameFunctionalGroupsSequence.length; i < groupsLen; ++i) {
|
|
658
|
+
const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[i];
|
|
659
|
+
const segmentIndex = getSegmentIndex(multiframe, i);
|
|
660
|
+
if (segmentIndex === undefined) {
|
|
661
|
+
throw new Error("Could not retrieve the segment index. Aborting segmentation loading.");
|
|
662
|
+
}
|
|
663
|
+
if (segmentIndex !== segmentIndexToProcess) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const ImageOrientationPatientI = sharedImageOrientationPatient || PerFrameFunctionalGroups.PlaneOrientationSequence.ImageOrientationPatient;
|
|
667
|
+
|
|
668
|
+
// Since we moved to the chunks approach, we need to read the data
|
|
669
|
+
// and handle scenarios where the portion of data is in one chunk
|
|
670
|
+
// and the other portion is in another chunk
|
|
671
|
+
const view = readFromUnpackedChunks(pixelData, i * sliceLength, sliceLength);
|
|
672
|
+
const pixelDataI2D = ndarray(view, [Rows, Columns]);
|
|
673
|
+
const alignedPixelDataI = alignPixelDataWithSourceData(pixelDataI2D, ImageOrientationPatientI, validOrientations, tolerance);
|
|
674
|
+
if (!alignedPixelDataI) {
|
|
675
|
+
throw new Error("Individual SEG frames are out of plane with respect to the first SEG frame. " + "This is not yet supported. Aborting segmentation loading.");
|
|
676
|
+
}
|
|
677
|
+
const imageId = findReferenceSourceImageId(multiframe, i, imageIds, metadataProvider, tolerance, sopUIDImageIdIndexMap);
|
|
678
|
+
if (!imageId) {
|
|
679
|
+
console.warn("Image not present in stack, can't import frame : " + i + ".");
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const sourceImageMetadata = metadataProvider.get("instance", imageId);
|
|
683
|
+
if (Rows !== sourceImageMetadata.Rows || Columns !== sourceImageMetadata.Columns) {
|
|
684
|
+
throw new Error("Individual SEG frames have different geometry dimensions (Rows and Columns) " + "respect to the source image reference frame. This is not yet supported. " + "Aborting segmentation loading. ");
|
|
685
|
+
}
|
|
686
|
+
const imageIdIndex = imageIds.findIndex(element => element === imageId);
|
|
687
|
+
const byteOffset = sliceLength * imageIdIndex * TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
688
|
+
const labelmap2DView = new TypedArrayConstructor(tempBuffer, byteOffset, sliceLength);
|
|
689
|
+
const data = alignedPixelDataI.data;
|
|
690
|
+
let segmentOnFrame = false;
|
|
691
|
+
for (let j = 0, len = alignedPixelDataI.data.length; j < len; ++j) {
|
|
692
|
+
if (data[j]) {
|
|
693
|
+
if (labelmap2DView[j] !== 0) {
|
|
694
|
+
m++;
|
|
695
|
+
if (m >= M) {
|
|
696
|
+
labelmapBufferArray[m] = new ArrayBuffer(arrayBufferLength);
|
|
697
|
+
segmentsOnFrameArray[m] = [];
|
|
698
|
+
M++;
|
|
721
699
|
}
|
|
700
|
+
tempBuffer = labelmapBufferArray[m].slice(0);
|
|
701
|
+
tempSegmentsOnFrame = structuredClone(segmentsOnFrameArray[m]);
|
|
702
|
+
i = 0;
|
|
703
|
+
break;
|
|
704
|
+
} else {
|
|
705
|
+
labelmap2DView[j] = segmentIndex;
|
|
706
|
+
segmentOnFrame = true;
|
|
722
707
|
}
|
|
723
708
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
segmentsOnFrame[imageIdIndex].push(segmentIndex);
|
|
709
|
+
}
|
|
710
|
+
if (segmentOnFrame) {
|
|
711
|
+
if (!tempSegmentsOnFrame[imageIdIndex]) {
|
|
712
|
+
tempSegmentsOnFrame[imageIdIndex] = [];
|
|
713
|
+
}
|
|
714
|
+
tempSegmentsOnFrame[imageIdIndex].push(segmentIndex);
|
|
715
|
+
if (!segmentsOnFrame[imageIdIndex]) {
|
|
716
|
+
segmentsOnFrame[imageIdIndex] = [];
|
|
733
717
|
}
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
_ret2;
|
|
737
|
-
for (var i = 0, groupsLen = PerFrameFunctionalGroupsSequence.length; i < groupsLen; ++i) {
|
|
738
|
-
_ret2 = _loop4(i);
|
|
739
|
-
if (_ret2 === 0) continue;
|
|
718
|
+
segmentsOnFrame[imageIdIndex].push(segmentIndex);
|
|
719
|
+
}
|
|
740
720
|
}
|
|
741
721
|
labelmapBufferArray[m] = tempBuffer.slice(0);
|
|
742
722
|
segmentsOnFrameArray[m] = structuredClone(tempSegmentsOnFrame);
|
|
@@ -747,64 +727,68 @@ function insertOverlappingPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray,
|
|
|
747
727
|
tempSegmentsOnFrame = structuredClone(segmentsOnFrameArray[m]);
|
|
748
728
|
}
|
|
749
729
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
730
|
+
const getSegmentIndex = (multiframe, frame) => {
|
|
731
|
+
const {
|
|
732
|
+
PerFrameFunctionalGroupsSequence,
|
|
733
|
+
SharedFunctionalGroupsSequence
|
|
734
|
+
} = multiframe;
|
|
735
|
+
const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[frame];
|
|
754
736
|
return PerFrameFunctionalGroups && PerFrameFunctionalGroups.SegmentIdentificationSequence ? PerFrameFunctionalGroups.SegmentIdentificationSequence.ReferencedSegmentNumber : SharedFunctionalGroupsSequence.SegmentIdentificationSequence ? SharedFunctionalGroupsSequence.SegmentIdentificationSequence.ReferencedSegmentNumber : undefined;
|
|
755
737
|
};
|
|
756
738
|
function insertPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray, labelmapBufferArray, pixelData, multiframe, imageIds, validOrientations, metadataProvider, tolerance, TypedArrayConstructor, segmentsPixelIndices, sopUIDImageIdIndexMap, imageIdMaps, eventTarget, triggerEvent) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
739
|
+
const {
|
|
740
|
+
SharedFunctionalGroupsSequence,
|
|
741
|
+
PerFrameFunctionalGroupsSequence,
|
|
742
|
+
Rows,
|
|
743
|
+
Columns
|
|
744
|
+
} = multiframe;
|
|
745
|
+
const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
|
|
746
|
+
const sliceLength = Columns * Rows;
|
|
747
|
+
let i = 0;
|
|
748
|
+
const groupsLen = PerFrameFunctionalGroupsSequence.length;
|
|
749
|
+
const chunkSize = Math.ceil(groupsLen / 10); // 10% of total length
|
|
750
|
+
|
|
751
|
+
const shouldTriggerEvent = triggerEvent && eventTarget;
|
|
752
|
+
let overlapping = false;
|
|
769
753
|
// Below, we chunk the processing of the frames to avoid blocking the main thread
|
|
770
754
|
// if the segmentation is large. We also use a promise to allow the caller to
|
|
771
755
|
// wait for the processing to finish.
|
|
772
|
-
return new Promise(
|
|
756
|
+
return new Promise(resolve => {
|
|
773
757
|
function processInChunks() {
|
|
774
758
|
// process one chunk
|
|
775
|
-
for (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
759
|
+
for (let end = Math.min(i + chunkSize, groupsLen); i < end; ++i) {
|
|
760
|
+
const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[i];
|
|
761
|
+
const ImageOrientationPatientI = sharedImageOrientationPatient || PerFrameFunctionalGroups.PlaneOrientationSequence.ImageOrientationPatient;
|
|
762
|
+
const view = readFromUnpackedChunks(pixelData, i * sliceLength, sliceLength);
|
|
763
|
+
const pixelDataI2D = ndarray(view, [Rows, Columns]);
|
|
764
|
+
const alignedPixelDataI = alignPixelDataWithSourceData(pixelDataI2D, ImageOrientationPatientI, validOrientations, tolerance);
|
|
781
765
|
if (!alignedPixelDataI) {
|
|
782
766
|
throw new Error("Individual SEG frames are out of plane with respect to the first SEG frame. " + "This is not yet supported. Aborting segmentation loading.");
|
|
783
767
|
}
|
|
784
|
-
|
|
768
|
+
const segmentIndex = getSegmentIndex(multiframe, i);
|
|
785
769
|
if (segmentIndex === undefined) {
|
|
786
770
|
throw new Error("Could not retrieve the segment index. Aborting segmentation loading.");
|
|
787
771
|
}
|
|
788
772
|
if (!segmentsPixelIndices.has(segmentIndex)) {
|
|
789
773
|
segmentsPixelIndices.set(segmentIndex, {});
|
|
790
774
|
}
|
|
791
|
-
|
|
775
|
+
const imageId = findReferenceSourceImageId(multiframe, i, imageIds, metadataProvider, tolerance, sopUIDImageIdIndexMap);
|
|
792
776
|
if (!imageId) {
|
|
793
777
|
console.warn("Image not present in stack, can't import frame : " + i + ".");
|
|
794
778
|
continue;
|
|
795
779
|
}
|
|
796
|
-
|
|
780
|
+
const sourceImageMetadata = imageIdMaps.metadata[imageId];
|
|
797
781
|
if (Rows !== sourceImageMetadata.Rows || Columns !== sourceImageMetadata.Columns) {
|
|
798
782
|
throw new Error("Individual SEG frames have different geometry dimensions (Rows and Columns) " + "respect to the source image reference frame. This is not yet supported. " + "Aborting segmentation loading. ");
|
|
799
783
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
for (
|
|
784
|
+
const imageIdIndex = imageIdMaps.indices[imageId];
|
|
785
|
+
const byteOffset = sliceLength * imageIdIndex * TypedArrayConstructor.BYTES_PER_ELEMENT;
|
|
786
|
+
const labelmap2DView = new TypedArrayConstructor(labelmapBufferArray[0], byteOffset, sliceLength);
|
|
787
|
+
const data = alignedPixelDataI.data;
|
|
788
|
+
const indexCache = [];
|
|
789
|
+
for (let j = 0, len = alignedPixelDataI.data.length; j < len; ++j) {
|
|
806
790
|
if (data[j]) {
|
|
807
|
-
for (
|
|
791
|
+
for (let x = j; x < len; ++x) {
|
|
808
792
|
if (data[x]) {
|
|
809
793
|
if (!overlapping && labelmap2DView[x] !== 0) {
|
|
810
794
|
overlapping = true;
|
|
@@ -820,16 +804,16 @@ function insertPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray, labelmapBu
|
|
|
820
804
|
break;
|
|
821
805
|
}
|
|
822
806
|
}
|
|
823
|
-
|
|
807
|
+
const segmentIndexObject = segmentsPixelIndices.get(segmentIndex);
|
|
824
808
|
segmentIndexObject[imageIdIndex] = indexCache;
|
|
825
809
|
segmentsPixelIndices.set(segmentIndex, segmentIndexObject);
|
|
826
810
|
}
|
|
827
811
|
|
|
828
812
|
// trigger an event after each chunk
|
|
829
813
|
if (shouldTriggerEvent) {
|
|
830
|
-
|
|
814
|
+
const percentComplete = Math.round(i / groupsLen * 100);
|
|
831
815
|
triggerEvent(eventTarget, Events.SEGMENTATION_LOAD_PROGRESS, {
|
|
832
|
-
percentComplete
|
|
816
|
+
percentComplete
|
|
833
817
|
});
|
|
834
818
|
}
|
|
835
819
|
|
|
@@ -853,8 +837,8 @@ function insertPixelDataPlanar(segmentsOnFrame, segmentsOnFrameArray, labelmapBu
|
|
|
853
837
|
* @return {Uint8Array} The unpacked pixelData.
|
|
854
838
|
*/
|
|
855
839
|
function unpackPixelData(multiframe, options) {
|
|
856
|
-
|
|
857
|
-
|
|
840
|
+
const segType = multiframe.SegmentationType;
|
|
841
|
+
let data;
|
|
858
842
|
if (Array.isArray(multiframe.PixelData)) {
|
|
859
843
|
data = multiframe.PixelData[0];
|
|
860
844
|
} else {
|
|
@@ -869,11 +853,9 @@ function unpackPixelData(multiframe, options) {
|
|
|
869
853
|
// MAX 2GB is the limit right now to allocate a buffer
|
|
870
854
|
return getUnpackedChunks(data, options.maxBytesPerChunk);
|
|
871
855
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
return element !== 0 && element !== max;
|
|
876
|
-
}) === undefined;
|
|
856
|
+
const pixelData = new Uint8Array(data);
|
|
857
|
+
const max = multiframe.MaximumFractionalValue;
|
|
858
|
+
const onlyMaxAndZero = pixelData.find(element => element !== 0 && element !== max) === undefined;
|
|
877
859
|
if (!onlyMaxAndZero) {
|
|
878
860
|
// This is a fractional segmentation, which is not currently supported.
|
|
879
861
|
return;
|
|
@@ -907,8 +889,10 @@ function getUnpackedChunks(data, maxBytesPerChunk) {
|
|
|
907
889
|
* @return {String} The corresponding imageId.
|
|
908
890
|
*/
|
|
909
891
|
function getImageIdOfSourceImageBySourceImageSequence(SourceImageSequence, sopUIDImageIdIndexMap) {
|
|
910
|
-
|
|
911
|
-
|
|
892
|
+
const {
|
|
893
|
+
ReferencedSOPInstanceUID,
|
|
894
|
+
ReferencedFrameNumber
|
|
895
|
+
} = SourceImageSequence;
|
|
912
896
|
return ReferencedFrameNumber ? getImageIdOfReferencedFrame(ReferencedSOPInstanceUID, ReferencedFrameNumber, sopUIDImageIdIndexMap) : sopUIDImageIdIndexMap[ReferencedSOPInstanceUID];
|
|
913
897
|
}
|
|
914
898
|
|
|
@@ -928,8 +912,8 @@ function getImageIdOfSourceImagebyGeometry(ReferencedSeriesInstanceUID, FrameOfR
|
|
|
928
912
|
if (ReferencedSeriesInstanceUID === undefined || PerFrameFunctionalGroup.PlanePositionSequence === undefined || PerFrameFunctionalGroup.PlanePositionSequence[0] === undefined || PerFrameFunctionalGroup.PlanePositionSequence[0].ImagePositionPatient === undefined) {
|
|
929
913
|
return undefined;
|
|
930
914
|
}
|
|
931
|
-
for (
|
|
932
|
-
|
|
915
|
+
for (let imageIdsIndexc = 0; imageIdsIndexc < imageIds.length; ++imageIdsIndexc) {
|
|
916
|
+
const sourceImageMetadata = metadataProvider.get("instance", imageIds[imageIdsIndexc]);
|
|
933
917
|
if (sourceImageMetadata === undefined || sourceImageMetadata.ImagePositionPatient === undefined || sourceImageMetadata.FrameOfReferenceUID !== FrameOfReferenceUID || sourceImageMetadata.SeriesInstanceUID !== ReferencedSeriesInstanceUID) {
|
|
934
918
|
continue;
|
|
935
919
|
}
|
|
@@ -950,11 +934,11 @@ function getImageIdOfSourceImagebyGeometry(ReferencedSeriesInstanceUID, FrameOfR
|
|
|
950
934
|
* @return {String} The imageId that corresponds to the sopInstanceUid.
|
|
951
935
|
*/
|
|
952
936
|
function getImageIdOfReferencedFrame(sopInstanceUid, frameNumber, sopUIDImageIdIndexMap) {
|
|
953
|
-
|
|
937
|
+
const imageId = sopUIDImageIdIndexMap[sopInstanceUid];
|
|
954
938
|
if (!imageId) {
|
|
955
939
|
return;
|
|
956
940
|
}
|
|
957
|
-
|
|
941
|
+
const imageIdFrameNumber = Number(imageId.split("frame=")[1]);
|
|
958
942
|
return imageIdFrameNumber === frameNumber - 1 ? imageId : undefined;
|
|
959
943
|
}
|
|
960
944
|
|
|
@@ -965,7 +949,7 @@ function getImageIdOfReferencedFrame(sopInstanceUid, frameNumber, sopUIDImageIdI
|
|
|
965
949
|
* @return {Number[8][6]} An array of valid orientations.
|
|
966
950
|
*/
|
|
967
951
|
function getValidOrientations(iop) {
|
|
968
|
-
|
|
952
|
+
const orientations = [];
|
|
969
953
|
|
|
970
954
|
// [0, 1, 2]: 0, 0hf, 0vf
|
|
971
955
|
// [3, 4, 5]: 90, 90hf, 90vf
|
|
@@ -974,7 +958,7 @@ function getValidOrientations(iop) {
|
|
|
974
958
|
orientations[0] = iop;
|
|
975
959
|
orientations[1] = flipIOP.h(iop);
|
|
976
960
|
orientations[2] = flipIOP.v(iop);
|
|
977
|
-
|
|
961
|
+
const iop90 = rotateDirectionCosinesInPlane(iop, Math.PI / 2);
|
|
978
962
|
orientations[3] = iop90;
|
|
979
963
|
orientations[4] = flipIOP.h(iop90);
|
|
980
964
|
orientations[5] = flipIOP.v(iop90);
|
|
@@ -1032,17 +1016,17 @@ function alignPixelDataWithSourceData(pixelData2D, iop, orientations, tolerance)
|
|
|
1032
1016
|
}
|
|
1033
1017
|
}
|
|
1034
1018
|
function getSegmentMetadata(multiframe, seriesInstanceUid) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1019
|
+
const segmentSequence = multiframe.SegmentSequence;
|
|
1020
|
+
let data = [];
|
|
1037
1021
|
if (Array.isArray(segmentSequence)) {
|
|
1038
|
-
data = [undefined]
|
|
1022
|
+
data = [undefined, ...segmentSequence];
|
|
1039
1023
|
} else {
|
|
1040
1024
|
// Only one segment, will be stored as an object.
|
|
1041
1025
|
data = [undefined, segmentSequence];
|
|
1042
1026
|
}
|
|
1043
1027
|
return {
|
|
1044
|
-
seriesInstanceUid
|
|
1045
|
-
data
|
|
1028
|
+
seriesInstanceUid,
|
|
1029
|
+
data
|
|
1046
1030
|
};
|
|
1047
1031
|
}
|
|
1048
1032
|
|
|
@@ -1056,18 +1040,18 @@ function getSegmentMetadata(multiframe, seriesInstanceUid) {
|
|
|
1056
1040
|
* @returns {Uint8Array} A new Uint8Array containing the requested bytes.
|
|
1057
1041
|
*/
|
|
1058
1042
|
function readFromUnpackedChunks(chunks, offset, length) {
|
|
1059
|
-
|
|
1043
|
+
const mapping = getUnpackedOffsetAndLength(chunks, offset, length);
|
|
1060
1044
|
|
|
1061
1045
|
// If all the data is in one chunk, we can just slice that chunk
|
|
1062
1046
|
if (mapping.start.chunkIndex === mapping.end.chunkIndex) {
|
|
1063
1047
|
return new Uint8Array(chunks[mapping.start.chunkIndex].buffer, mapping.start.offset, length);
|
|
1064
1048
|
} else {
|
|
1065
1049
|
// If the data spans multiple chunks, we need to create a new Uint8Array and copy the data from each chunk
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
for (
|
|
1069
|
-
|
|
1070
|
-
|
|
1050
|
+
let result = new Uint8Array(length);
|
|
1051
|
+
let resultOffset = 0;
|
|
1052
|
+
for (let i = mapping.start.chunkIndex; i <= mapping.end.chunkIndex; i++) {
|
|
1053
|
+
let start = i === mapping.start.chunkIndex ? mapping.start.offset : 0;
|
|
1054
|
+
let end = i === mapping.end.chunkIndex ? mapping.end.offset : chunks[i].length;
|
|
1071
1055
|
result.set(new Uint8Array(chunks[i].buffer, start, end - start), resultOffset);
|
|
1072
1056
|
resultOffset += end - start;
|
|
1073
1057
|
}
|
|
@@ -1075,9 +1059,7 @@ function readFromUnpackedChunks(chunks, offset, length) {
|
|
|
1075
1059
|
}
|
|
1076
1060
|
}
|
|
1077
1061
|
function getUnpackedOffsetAndLength(chunks, offset, length) {
|
|
1078
|
-
var totalBytes = chunks.reduce(
|
|
1079
|
-
return total + chunk.length;
|
|
1080
|
-
}, 0);
|
|
1062
|
+
var totalBytes = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
1081
1063
|
if (offset < 0 || offset + length > totalBytes) {
|
|
1082
1064
|
throw new Error("Offset and length out of bounds");
|
|
1083
1065
|
}
|
|
@@ -1105,61 +1087,51 @@ function getUnpackedOffsetAndLength(chunks, offset, length) {
|
|
|
1105
1087
|
};
|
|
1106
1088
|
}
|
|
1107
1089
|
function calculateCentroid(imageIdIndexBufferIndex, multiframe, metadataProvider, imageIds) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
for (
|
|
1116
|
-
|
|
1117
|
-
imageIdIndex = _Object$entries$_i[0],
|
|
1118
|
-
bufferIndices = _Object$entries$_i[1];
|
|
1119
|
-
var z = Number(imageIdIndex);
|
|
1090
|
+
let xAcc = 0;
|
|
1091
|
+
let yAcc = 0;
|
|
1092
|
+
let zAcc = 0;
|
|
1093
|
+
let worldXAcc = 0;
|
|
1094
|
+
let worldYAcc = 0;
|
|
1095
|
+
let worldZAcc = 0;
|
|
1096
|
+
let count = 0;
|
|
1097
|
+
for (const [imageIdIndex, bufferIndices] of Object.entries(imageIdIndexBufferIndex)) {
|
|
1098
|
+
const z = Number(imageIdIndex);
|
|
1120
1099
|
if (!bufferIndices || bufferIndices.length === 0) {
|
|
1121
1100
|
continue;
|
|
1122
1101
|
}
|
|
1123
1102
|
|
|
1124
1103
|
// Get metadata for this slice
|
|
1125
|
-
|
|
1126
|
-
|
|
1104
|
+
const imageId = imageIds[z];
|
|
1105
|
+
const imagePlaneModule = metadataProvider.get("imagePlaneModule", imageId);
|
|
1127
1106
|
if (!imagePlaneModule) {
|
|
1128
1107
|
console.debug("Missing imagePlaneModule metadata for centroid calculation");
|
|
1129
1108
|
continue;
|
|
1130
1109
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
worldZAcc += worldZ;
|
|
1157
|
-
count++;
|
|
1158
|
-
}
|
|
1159
|
-
} catch (err) {
|
|
1160
|
-
_iterator2.e(err);
|
|
1161
|
-
} finally {
|
|
1162
|
-
_iterator2.f();
|
|
1110
|
+
const {
|
|
1111
|
+
imagePositionPatient,
|
|
1112
|
+
rowCosines,
|
|
1113
|
+
columnCosines,
|
|
1114
|
+
rowPixelSpacing,
|
|
1115
|
+
columnPixelSpacing
|
|
1116
|
+
} = imagePlaneModule;
|
|
1117
|
+
for (const bufferIndex of bufferIndices) {
|
|
1118
|
+
const y = Math.floor(bufferIndex / multiframe.Rows);
|
|
1119
|
+
const x = bufferIndex % multiframe.Rows;
|
|
1120
|
+
|
|
1121
|
+
// Image coordinates
|
|
1122
|
+
xAcc += x;
|
|
1123
|
+
yAcc += y;
|
|
1124
|
+
zAcc += z;
|
|
1125
|
+
|
|
1126
|
+
// Calculate world coordinates
|
|
1127
|
+
// P(world) = P(image) * IOP * spacing + IPP
|
|
1128
|
+
const worldX = imagePositionPatient[0] + x * rowCosines[0] * columnPixelSpacing + y * columnCosines[0] * rowPixelSpacing;
|
|
1129
|
+
const worldY = imagePositionPatient[1] + x * rowCosines[1] * columnPixelSpacing + y * columnCosines[1] * rowPixelSpacing;
|
|
1130
|
+
const worldZ = imagePositionPatient[2] + x * rowCosines[2] * columnPixelSpacing + y * columnCosines[2] * rowPixelSpacing;
|
|
1131
|
+
worldXAcc += worldX;
|
|
1132
|
+
worldYAcc += worldY;
|
|
1133
|
+
worldZAcc += worldZ;
|
|
1134
|
+
count++;
|
|
1163
1135
|
}
|
|
1164
1136
|
}
|
|
1165
1137
|
return {
|
|
@@ -1173,13 +1145,13 @@ function calculateCentroid(imageIdIndexBufferIndex, multiframe, metadataProvider
|
|
|
1173
1145
|
y: worldYAcc / count,
|
|
1174
1146
|
z: worldZAcc / count
|
|
1175
1147
|
},
|
|
1176
|
-
count
|
|
1148
|
+
count
|
|
1177
1149
|
};
|
|
1178
1150
|
}
|
|
1179
|
-
|
|
1180
|
-
generateSegmentation
|
|
1181
|
-
generateToolState
|
|
1182
|
-
fillSegmentation
|
|
1151
|
+
const Segmentation = {
|
|
1152
|
+
generateSegmentation,
|
|
1153
|
+
generateToolState,
|
|
1154
|
+
fillSegmentation
|
|
1183
1155
|
};
|
|
1184
1156
|
|
|
1185
1157
|
export { _createSegFromImages, _getLabelmapsFromReferencedFrameIndicies, alignPixelDataWithSourceData, calculateCentroid, checkSEGsOverlapping, Segmentation as default, fillSegmentation, findReferenceSourceImageId, generateSegmentation, generateToolState, getImageIdOfReferencedFrame, getImageIdOfSourceImageBySourceImageSequence, getImageIdOfSourceImagebyGeometry, getSegmentIndex, getSegmentMetadata, getUnpackedChunks, getUnpackedOffsetAndLength, getValidOrientations, insertOverlappingPixelDataPlanar, insertPixelDataPlanar, readFromUnpackedChunks, unpackPixelData };
|