@cornerstonejs/tools 3.5.2 → 3.6.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/config.d.ts +7 -0
- package/dist/esm/cursors/SVGCursorDescriptor.js +2 -2
- package/dist/esm/enums/WorkerTypes.d.ts +2 -1
- package/dist/esm/enums/WorkerTypes.js +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/tools/annotation/BidirectionalTool.d.ts +3 -0
- package/dist/esm/tools/annotation/BidirectionalTool.js +39 -1
- package/dist/esm/tools/annotation/LabelTool.d.ts +1 -1
- package/dist/esm/tools/annotation/LabelTool.js +10 -2
- package/dist/esm/tools/index.d.ts +2 -1
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/segmentation/SegmentBidirectionalTool.d.ts +27 -0
- package/dist/esm/tools/segmentation/SegmentBidirectionalTool.js +253 -0
- package/dist/esm/tools/segmentation/strategies/utils/getStrategyData.js +5 -10
- package/dist/esm/types/CalculatorTypes.d.ts +15 -3
- package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +34 -0
- package/dist/esm/utilities/contours/generateContourSetsFromLabelmap.js +1 -0
- package/dist/esm/utilities/math/basic/BasicStatsCalculator.js +68 -5
- package/dist/esm/utilities/registerComputeWorker.js +4 -1
- package/dist/esm/utilities/segmentation/VolumetricCalculator.js +1 -0
- package/dist/esm/utilities/segmentation/contourAndFindLargestBidirectional.js +7 -5
- package/dist/esm/utilities/segmentation/findLargestBidirectional.d.ts +5 -0
- package/dist/esm/utilities/segmentation/findLargestBidirectional.js +1 -1
- package/dist/esm/utilities/segmentation/getOrCreateSegmentationVolume.js +5 -1
- package/dist/esm/utilities/segmentation/getReferenceVolumeForSegmentationVolume.d.ts +1 -0
- package/dist/esm/utilities/segmentation/getReferenceVolumeForSegmentationVolume.js +20 -0
- package/dist/esm/utilities/segmentation/getSegmentLargestBidirectional.d.ts +5 -0
- package/dist/esm/utilities/segmentation/getSegmentLargestBidirectional.js +54 -0
- package/dist/esm/utilities/segmentation/getStatistics.js +42 -107
- package/dist/esm/utilities/segmentation/index.d.ts +3 -1
- package/dist/esm/utilities/segmentation/index.js +3 -1
- package/dist/esm/utilities/segmentation/isLineInSegment.d.ts +13 -1
- package/dist/esm/utilities/segmentation/isLineInSegment.js +20 -12
- package/dist/esm/utilities/segmentation/segmentContourAction.js +1 -0
- package/dist/esm/utilities/segmentation/utilsForWorker.d.ts +38 -0
- package/dist/esm/utilities/segmentation/utilsForWorker.js +125 -0
- package/dist/esm/workers/computeWorker.js +284 -40
- package/package.json +3 -3
|
@@ -13,6 +13,9 @@ function createBasicStatsState(storePointData) {
|
|
|
13
13
|
minLPS: null,
|
|
14
14
|
runMean: [0],
|
|
15
15
|
m2: [0],
|
|
16
|
+
m3: [0],
|
|
17
|
+
m4: [0],
|
|
18
|
+
allValues: [[]],
|
|
16
19
|
pointsInShape: storePointData ? PointsManager.create3(1024) : null,
|
|
17
20
|
};
|
|
18
21
|
}
|
|
@@ -25,6 +28,9 @@ function basicStatsCallback(state, newValue, pointLPS = null, pointIJK = null) {
|
|
|
25
28
|
state.sum.push(state.sum[0], state.sum[0]);
|
|
26
29
|
state.runMean.push(0, 0);
|
|
27
30
|
state.m2.push(state.m2[0], state.m2[0]);
|
|
31
|
+
state.m3.push(state.m3[0], state.m3[0]);
|
|
32
|
+
state.m4.push(state.m4[0], state.m4[0]);
|
|
33
|
+
state.allValues.push([], []);
|
|
28
34
|
}
|
|
29
35
|
if (state?.pointsInShape && pointLPS) {
|
|
30
36
|
state.pointsInShape.push(pointLPS);
|
|
@@ -33,11 +39,19 @@ function basicStatsCallback(state, newValue, pointLPS = null, pointIJK = null) {
|
|
|
33
39
|
state.count += 1;
|
|
34
40
|
state.max.forEach((it, idx) => {
|
|
35
41
|
const value = newArray[idx];
|
|
42
|
+
state.allValues[idx].push(value);
|
|
43
|
+
const n = state.count;
|
|
36
44
|
const delta = value - state.runMean[idx];
|
|
45
|
+
const delta_n = delta / n;
|
|
46
|
+
const term1 = delta * delta_n * (n - 1);
|
|
37
47
|
state.sum[idx] += value;
|
|
38
|
-
state.runMean[idx] +=
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
state.runMean[idx] += delta_n;
|
|
49
|
+
state.m4[idx] +=
|
|
50
|
+
term1 * delta_n * delta_n * (n * n - 3 * n + 3) +
|
|
51
|
+
6 * delta_n * delta_n * state.m2[idx] -
|
|
52
|
+
4 * delta_n * state.m3[idx];
|
|
53
|
+
state.m3[idx] += term1 * delta_n * (n - 2) - 3 * delta_n * state.m2[idx];
|
|
54
|
+
state.m2[idx] += term1;
|
|
41
55
|
if (value < state.min[idx]) {
|
|
42
56
|
state.min[idx] = value;
|
|
43
57
|
if (idx === 0) {
|
|
@@ -54,9 +68,37 @@ function basicStatsCallback(state, newValue, pointLPS = null, pointIJK = null) {
|
|
|
54
68
|
}
|
|
55
69
|
});
|
|
56
70
|
}
|
|
71
|
+
function calculateMedian(values) {
|
|
72
|
+
if (values.length === 0) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
76
|
+
const mid = Math.floor(sorted.length / 2);
|
|
77
|
+
if (sorted.length % 2 === 0) {
|
|
78
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return sorted[mid];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
57
84
|
function basicGetStatistics(state, unit) {
|
|
58
85
|
const mean = state.sum.map((sum) => sum / state.count);
|
|
59
86
|
const stdDev = state.m2.map((squaredDiffSum) => Math.sqrt(squaredDiffSum / state.count));
|
|
87
|
+
const skewness = state.m3.map((m3, idx) => {
|
|
88
|
+
const variance = state.m2[idx] / state.count;
|
|
89
|
+
if (variance === 0) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
return m3 / (state.count * Math.pow(variance, 1.5));
|
|
93
|
+
});
|
|
94
|
+
const kurtosis = state.m4.map((m4, idx) => {
|
|
95
|
+
const variance = state.m2[idx] / state.count;
|
|
96
|
+
if (variance === 0) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
return m4 / (state.count * variance * variance) - 3;
|
|
100
|
+
});
|
|
101
|
+
const median = state.allValues.map((values) => calculateMedian(values));
|
|
60
102
|
const named = {
|
|
61
103
|
max: {
|
|
62
104
|
name: 'max',
|
|
@@ -88,14 +130,32 @@ function basicGetStatistics(state, unit) {
|
|
|
88
130
|
},
|
|
89
131
|
count: {
|
|
90
132
|
name: 'count',
|
|
91
|
-
label: '
|
|
133
|
+
label: 'Voxel Count',
|
|
92
134
|
value: state.count,
|
|
93
135
|
unit: null,
|
|
94
136
|
},
|
|
137
|
+
median: {
|
|
138
|
+
name: 'median',
|
|
139
|
+
label: 'Median',
|
|
140
|
+
value: median.length === 1 ? median[0] : median,
|
|
141
|
+
unit,
|
|
142
|
+
},
|
|
143
|
+
skewness: {
|
|
144
|
+
name: 'skewness',
|
|
145
|
+
label: 'Skewness',
|
|
146
|
+
value: skewness.length === 1 ? skewness[0] : skewness,
|
|
147
|
+
unit: null,
|
|
148
|
+
},
|
|
149
|
+
kurtosis: {
|
|
150
|
+
name: 'kurtosis',
|
|
151
|
+
label: 'Kurtosis',
|
|
152
|
+
value: kurtosis.length === 1 ? kurtosis[0] : kurtosis,
|
|
153
|
+
unit: null,
|
|
154
|
+
},
|
|
95
155
|
pointsInShape: state.pointsInShape,
|
|
96
156
|
array: [],
|
|
97
157
|
};
|
|
98
|
-
named.array.push(named.max, named.mean, named.stdDev, named.
|
|
158
|
+
named.array.push(named.max, named.mean, named.stdDev, named.median, named.skewness, named.kurtosis, named.count);
|
|
99
159
|
const store = state.pointsInShape !== null;
|
|
100
160
|
const freshState = createBasicStatsState(store);
|
|
101
161
|
state.max = freshState.max;
|
|
@@ -108,6 +168,9 @@ function basicGetStatistics(state, unit) {
|
|
|
108
168
|
state.minLPS = freshState.minLPS;
|
|
109
169
|
state.runMean = freshState.runMean;
|
|
110
170
|
state.m2 = freshState.m2;
|
|
171
|
+
state.m3 = freshState.m3;
|
|
172
|
+
state.m4 = freshState.m4;
|
|
173
|
+
state.allValues = freshState.allValues;
|
|
111
174
|
state.pointsInShape = freshState.pointsInShape;
|
|
112
175
|
return named;
|
|
113
176
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getWebWorkerManager } from '@cornerstonejs/core';
|
|
2
|
+
import { getConfig } from '../config';
|
|
2
3
|
let registered = false;
|
|
3
4
|
export function registerComputeWorker() {
|
|
4
5
|
if (registered) {
|
|
@@ -12,9 +13,11 @@ export function registerComputeWorker() {
|
|
|
12
13
|
});
|
|
13
14
|
};
|
|
14
15
|
const workerManager = getWebWorkerManager();
|
|
16
|
+
const config = getConfig();
|
|
17
|
+
const computeWorkerConfig = config.computeWorker;
|
|
15
18
|
const options = {
|
|
16
19
|
maxWorkerInstances: 1,
|
|
17
|
-
autoTerminateOnIdle: {
|
|
20
|
+
autoTerminateOnIdle: computeWorkerConfig?.autoTerminateOnIdle ?? {
|
|
18
21
|
enabled: true,
|
|
19
22
|
idleTimeThreshold: 2000,
|
|
20
23
|
},
|
|
@@ -42,6 +42,7 @@ function volumetricGetStatistics(state, stats, options) {
|
|
|
42
42
|
: stats.count.value * volumeScale,
|
|
43
43
|
unit: volumeUnit,
|
|
44
44
|
name: 'volume',
|
|
45
|
+
label: 'Volume',
|
|
45
46
|
};
|
|
46
47
|
stats.maxIJKs = state.maxIJKs.filter((entry) => entry.pointIJK !== undefined);
|
|
47
48
|
stats.array.push(stats.volume);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { generateContourSetsFromLabelmap } from '../contours';
|
|
2
|
-
import SegmentationRepresentations from '../../enums/SegmentationRepresentations';
|
|
3
2
|
import findLargestBidirectional from './findLargestBidirectional';
|
|
4
|
-
|
|
3
|
+
import getOrCreateSegmentationVolume from './getOrCreateSegmentationVolume';
|
|
5
4
|
export default function contourAndFindLargestBidirectional(segmentation) {
|
|
6
5
|
const contours = generateContourSetsFromLabelmap({
|
|
7
6
|
segmentations: segmentation,
|
|
@@ -9,15 +8,18 @@ export default function contourAndFindLargestBidirectional(segmentation) {
|
|
|
9
8
|
if (!contours?.length || !contours[0].sliceContours.length) {
|
|
10
9
|
return;
|
|
11
10
|
}
|
|
12
|
-
const {
|
|
11
|
+
const { segments = [
|
|
13
12
|
null,
|
|
14
13
|
{ label: 'Unspecified', color: null, containedSegmentIndices: null },
|
|
15
14
|
], } = segmentation;
|
|
16
|
-
const
|
|
15
|
+
const vol = getOrCreateSegmentationVolume(segmentation.segmentationId);
|
|
16
|
+
if (!vol) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
17
19
|
const segmentIndex = segments.findIndex((it) => !!it);
|
|
18
20
|
if (segmentIndex === -1) {
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
23
|
segments[segmentIndex].segmentIndex = segmentIndex;
|
|
22
|
-
return findLargestBidirectional(contours[0],
|
|
24
|
+
return findLargestBidirectional(contours[0], vol.volumeId, segments[segmentIndex]);
|
|
23
25
|
}
|
|
@@ -1 +1,6 @@
|
|
|
1
|
+
import type { BidirectionalData } from './createBidirectionalToolData';
|
|
1
2
|
export default function findLargestBidirectional(contours: any, segVolumeId: string, segment: any): any;
|
|
3
|
+
export declare function createBidirectionalForSlice(sliceContour: any, isInSegment: any, currentMax?: {
|
|
4
|
+
maxMajor: number;
|
|
5
|
+
maxMinor: number;
|
|
6
|
+
}): BidirectionalData;
|
|
@@ -18,7 +18,7 @@ export default function findLargestBidirectional(contours, segVolumeId, segment)
|
|
|
18
18
|
}
|
|
19
19
|
return maxBidirectional;
|
|
20
20
|
}
|
|
21
|
-
function createBidirectionalForSlice(sliceContour, isInSegment, currentMax = { maxMajor: 0, maxMinor: 0 }) {
|
|
21
|
+
export function createBidirectionalForSlice(sliceContour, isInSegment, currentMax = { maxMajor: 0, maxMinor: 0 }) {
|
|
22
22
|
const { points } = sliceContour.polyData;
|
|
23
23
|
const { maxMinor: currentMaxMinor, maxMajor: currentMaxMajor } = currentMax;
|
|
24
24
|
let maxMajor = currentMaxMajor * currentMaxMajor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cache, volumeLoader } from '@cornerstonejs/core';
|
|
1
|
+
import { cache, volumeLoader, utilities } from '@cornerstonejs/core';
|
|
2
2
|
import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
|
|
3
3
|
function getOrCreateSegmentationVolume(segmentationId) {
|
|
4
4
|
const { representationData } = getSegmentation(segmentationId);
|
|
@@ -15,6 +15,10 @@ function getOrCreateSegmentationVolume(segmentationId) {
|
|
|
15
15
|
if (!labelmapImageIds || labelmapImageIds.length === 1) {
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
+
const isValidVolume = utilities.isValidVolume(labelmapImageIds);
|
|
19
|
+
if (!isValidVolume) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
segVolume = volumeLoader.createAndCacheVolumeFromImagesSync(volumeId, labelmapImageIds);
|
|
19
23
|
return segVolume;
|
|
20
24
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getReferenceVolumeForSegmentationVolume(segmentationVolumeId: string): any;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cache } from '@cornerstonejs/core';
|
|
2
|
+
export function getReferenceVolumeForSegmentationVolume(segmentationVolumeId) {
|
|
3
|
+
const segmentationVolume = cache.getVolume(segmentationVolumeId);
|
|
4
|
+
if (!segmentationVolume) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const referencedVolumeId = segmentationVolume.referencedVolumeId;
|
|
8
|
+
let imageVolume;
|
|
9
|
+
if (referencedVolumeId) {
|
|
10
|
+
imageVolume = cache.getVolume(referencedVolumeId);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const imageIds = segmentationVolume.imageIds;
|
|
14
|
+
const image = cache.getImage(imageIds[0]);
|
|
15
|
+
const referencedImageId = image.referencedImageId;
|
|
16
|
+
const volumeInfo = cache.getVolumeContainingImageId(referencedImageId);
|
|
17
|
+
imageVolume = volumeInfo?.volume;
|
|
18
|
+
}
|
|
19
|
+
return imageVolume;
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getWebWorkerManager } from '@cornerstonejs/core';
|
|
2
|
+
import { WorkerTypes } from '../../enums';
|
|
3
|
+
import { registerComputeWorker } from '../registerComputeWorker';
|
|
4
|
+
import { triggerWorkerProgress, getSegmentationDataForWorker, prepareVolumeStrategyDataForWorker, prepareStackDataForWorker, } from './utilsForWorker';
|
|
5
|
+
export async function getSegmentLargestBidirectional({ segmentationId, segmentIndices, mode = 'individual', }) {
|
|
6
|
+
registerComputeWorker();
|
|
7
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_LARGEST_BIDIRECTIONAL, 0);
|
|
8
|
+
const segData = getSegmentationDataForWorker(segmentationId, segmentIndices);
|
|
9
|
+
if (!segData) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const { operationData, segImageIds, reconstructableVolume, indices } = segData;
|
|
13
|
+
const bidirectionalData = reconstructableVolume
|
|
14
|
+
? await calculateVolumeBidirectional({
|
|
15
|
+
operationData,
|
|
16
|
+
indices,
|
|
17
|
+
mode,
|
|
18
|
+
})
|
|
19
|
+
: await calculateStackBidirectional({
|
|
20
|
+
segImageIds,
|
|
21
|
+
indices,
|
|
22
|
+
mode,
|
|
23
|
+
});
|
|
24
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_LARGEST_BIDIRECTIONAL, 100);
|
|
25
|
+
return bidirectionalData;
|
|
26
|
+
}
|
|
27
|
+
async function calculateVolumeBidirectional({ operationData, indices, mode }) {
|
|
28
|
+
const strategyData = prepareVolumeStrategyDataForWorker(operationData);
|
|
29
|
+
const { segmentationVoxelManager, segmentationImageData } = strategyData;
|
|
30
|
+
const segmentationScalarData = segmentationVoxelManager.getCompleteScalarDataArray();
|
|
31
|
+
const segmentationInfo = {
|
|
32
|
+
scalarData: segmentationScalarData,
|
|
33
|
+
dimensions: segmentationImageData.getDimensions(),
|
|
34
|
+
spacing: segmentationImageData.getSpacing(),
|
|
35
|
+
origin: segmentationImageData.getOrigin(),
|
|
36
|
+
direction: segmentationImageData.getDirection(),
|
|
37
|
+
};
|
|
38
|
+
const bidirectionalData = await getWebWorkerManager().executeTask('compute', 'getSegmentLargestBidirectionalInternal', {
|
|
39
|
+
segmentationInfo,
|
|
40
|
+
indices,
|
|
41
|
+
mode,
|
|
42
|
+
});
|
|
43
|
+
return bidirectionalData;
|
|
44
|
+
}
|
|
45
|
+
async function calculateStackBidirectional({ segImageIds, indices, mode }) {
|
|
46
|
+
const { segmentationInfo } = prepareStackDataForWorker(segImageIds);
|
|
47
|
+
const bidirectionalData = await getWebWorkerManager().executeTask('compute', 'getSegmentLargestBidirectionalInternal', {
|
|
48
|
+
segmentationInfo,
|
|
49
|
+
indices,
|
|
50
|
+
mode,
|
|
51
|
+
isStack: true,
|
|
52
|
+
});
|
|
53
|
+
return bidirectionalData;
|
|
54
|
+
}
|
|
@@ -1,53 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import VolumetricCalculator from './VolumetricCalculator';
|
|
4
|
-
import { getStrategyData } from '../../tools/segmentation/strategies/utils/getStrategyData';
|
|
1
|
+
import { utilities, getWebWorkerManager } from '@cornerstonejs/core';
|
|
2
|
+
import { triggerWorkerProgress, getSegmentationDataForWorker, prepareVolumeStrategyDataForWorker, prepareStackDataForWorker, getImageReferenceInfo, } from './utilsForWorker';
|
|
5
3
|
import { getPixelValueUnitsImageId } from '../getPixelValueUnits';
|
|
6
|
-
import
|
|
7
|
-
import ensureImageVolume from '../../tools/segmentation/strategies/compositions/ensureImageVolume';
|
|
8
|
-
import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
|
|
9
|
-
import { registerComputeWorker } from '../registerComputeWorker';
|
|
4
|
+
import VolumetricCalculator from './VolumetricCalculator';
|
|
10
5
|
import { WorkerTypes } from '../../enums';
|
|
6
|
+
import { registerComputeWorker } from '../registerComputeWorker';
|
|
11
7
|
const radiusForVol1 = Math.pow((3 * 1000) / (4 * Math.PI), 1 / 3);
|
|
12
|
-
const workerManager = getWebWorkerManager();
|
|
13
|
-
const triggerWorkerProgress = (eventTarget, progress) => {
|
|
14
|
-
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
|
|
15
|
-
progress,
|
|
16
|
-
type: WorkerTypes.COMPUTE_STATISTICS,
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
8
|
async function getStatistics({ segmentationId, segmentIndices, mode = 'collective', }) {
|
|
20
9
|
registerComputeWorker();
|
|
21
|
-
triggerWorkerProgress(
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const { Labelmap } = representationData;
|
|
25
|
-
if (!Labelmap) {
|
|
26
|
-
console.debug('No labelmap found for segmentation', segmentationId);
|
|
10
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 0);
|
|
11
|
+
const segData = getSegmentationDataForWorker(segmentationId, segmentIndices);
|
|
12
|
+
if (!segData) {
|
|
27
13
|
return;
|
|
28
14
|
}
|
|
29
|
-
const segVolumeId =
|
|
30
|
-
const segImageIds = Labelmap.imageIds;
|
|
31
|
-
const operationData = {
|
|
32
|
-
segmentationId,
|
|
33
|
-
volumeId: segVolumeId,
|
|
34
|
-
imageIds: segImageIds,
|
|
35
|
-
};
|
|
36
|
-
let reconstructableVolume = false;
|
|
37
|
-
if (segImageIds) {
|
|
38
|
-
const refImageIds = segImageIds.map((imageId) => {
|
|
39
|
-
const image = cache.getImage(imageId);
|
|
40
|
-
return image.referencedImageId;
|
|
41
|
-
});
|
|
42
|
-
reconstructableVolume = utilities.isValidVolume(refImageIds);
|
|
43
|
-
}
|
|
44
|
-
let indices = segmentIndices;
|
|
45
|
-
if (!indices) {
|
|
46
|
-
indices = [getActiveSegmentIndex(segmentationId)];
|
|
47
|
-
}
|
|
48
|
-
else if (!Array.isArray(indices)) {
|
|
49
|
-
indices = [indices, 255];
|
|
50
|
-
}
|
|
15
|
+
const { operationData, segVolumeId, segImageIds, reconstructableVolume, indices, } = segData;
|
|
51
16
|
const { refImageId, modalityUnitOptions } = getImageReferenceInfo(segVolumeId, segImageIds);
|
|
52
17
|
const unit = getPixelValueUnitsImageId(refImageId, modalityUnitOptions);
|
|
53
18
|
const stats = reconstructableVolume
|
|
@@ -66,13 +31,7 @@ async function getStatistics({ segmentationId, segmentIndices, mode = 'collectiv
|
|
|
66
31
|
return stats;
|
|
67
32
|
}
|
|
68
33
|
async function calculateVolumeStatistics({ operationData, indices, unit, mode, }) {
|
|
69
|
-
const strategyData =
|
|
70
|
-
operationData,
|
|
71
|
-
strategy: {
|
|
72
|
-
ensureSegmentationVolumeFor3DManipulation: ensureSegmentationVolume.ensureSegmentationVolumeFor3DManipulation,
|
|
73
|
-
ensureImageVolumeFor3DManipulation: ensureImageVolume.ensureImageVolumeFor3DManipulation,
|
|
74
|
-
},
|
|
75
|
-
});
|
|
34
|
+
const strategyData = prepareVolumeStrategyDataForWorker(operationData);
|
|
76
35
|
const { segmentationVoxelManager, imageVoxelManager, segmentationImageData, imageData, } = strategyData;
|
|
77
36
|
const spacing = segmentationImageData.getSpacing();
|
|
78
37
|
const { boundsIJK: boundsOrig } = segmentationVoxelManager;
|
|
@@ -80,26 +39,27 @@ async function calculateVolumeStatistics({ operationData, indices, unit, mode, }
|
|
|
80
39
|
return VolumetricCalculator.getStatistics({ spacing });
|
|
81
40
|
}
|
|
82
41
|
const segmentationScalarData = segmentationVoxelManager.getCompleteScalarDataArray();
|
|
83
|
-
const imageScalarData = imageVoxelManager.getCompleteScalarDataArray();
|
|
84
42
|
const segmentationInfo = {
|
|
85
43
|
scalarData: segmentationScalarData,
|
|
86
44
|
dimensions: segmentationImageData.getDimensions(),
|
|
87
45
|
spacing: segmentationImageData.getSpacing(),
|
|
88
46
|
origin: segmentationImageData.getOrigin(),
|
|
47
|
+
direction: segmentationImageData.getDirection(),
|
|
89
48
|
};
|
|
90
49
|
const imageInfo = {
|
|
91
|
-
scalarData:
|
|
50
|
+
scalarData: imageVoxelManager.getCompleteScalarDataArray(),
|
|
92
51
|
dimensions: imageData.getDimensions(),
|
|
93
52
|
spacing: imageData.getSpacing(),
|
|
94
53
|
origin: imageData.getOrigin(),
|
|
54
|
+
direction: imageData.getDirection(),
|
|
95
55
|
};
|
|
96
|
-
const stats = await
|
|
56
|
+
const stats = await getWebWorkerManager().executeTask('compute', 'calculateSegmentsStatisticsVolume', {
|
|
97
57
|
segmentationInfo,
|
|
98
58
|
imageInfo,
|
|
99
59
|
indices,
|
|
100
60
|
mode,
|
|
101
61
|
});
|
|
102
|
-
triggerWorkerProgress(
|
|
62
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 100);
|
|
103
63
|
if (mode === 'collective') {
|
|
104
64
|
return processSegmentationStatistics({
|
|
105
65
|
stats,
|
|
@@ -123,6 +83,18 @@ async function calculateVolumeStatistics({ operationData, indices, unit, mode, }
|
|
|
123
83
|
return finalStats;
|
|
124
84
|
}
|
|
125
85
|
}
|
|
86
|
+
const updateStatsArray = (stats, newStat) => {
|
|
87
|
+
if (!stats.array) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const existingIndex = stats.array.findIndex((stat) => stat.name === newStat.name);
|
|
91
|
+
if (existingIndex !== -1) {
|
|
92
|
+
stats.array[existingIndex] = newStat;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
stats.array.push(newStat);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
126
98
|
const processSegmentationStatistics = ({ stats, unit, spacing, segmentationImageData, imageVoxelManager, }) => {
|
|
127
99
|
stats.mean.unit = unit;
|
|
128
100
|
stats.max.unit = unit;
|
|
@@ -144,42 +116,32 @@ const processSegmentationStatistics = ({ stats, unit, spacing, segmentationImage
|
|
|
144
116
|
value: mean.value,
|
|
145
117
|
unit,
|
|
146
118
|
};
|
|
119
|
+
updateStatsArray(stats, stats.peakValue);
|
|
147
120
|
}
|
|
148
121
|
}
|
|
122
|
+
if (stats.volume && stats.mean) {
|
|
123
|
+
const mtv = stats.volume.value;
|
|
124
|
+
const suvMean = stats.mean.value;
|
|
125
|
+
stats.lesionGlycolysis = {
|
|
126
|
+
name: 'lesionGlycolysis',
|
|
127
|
+
label: 'Lesion Glycolysis',
|
|
128
|
+
value: mtv * suvMean,
|
|
129
|
+
unit: `${stats.volume.unit}·${unit}`,
|
|
130
|
+
};
|
|
131
|
+
updateStatsArray(stats, stats.lesionGlycolysis);
|
|
132
|
+
}
|
|
149
133
|
return stats;
|
|
150
134
|
};
|
|
151
135
|
async function calculateStackStatistics({ segImageIds, indices, unit, mode }) {
|
|
152
|
-
triggerWorkerProgress(
|
|
153
|
-
const segmentationInfo =
|
|
154
|
-
const
|
|
155
|
-
for (const segImageId of segImageIds) {
|
|
156
|
-
const segImage = cache.getImage(segImageId);
|
|
157
|
-
const segPixelData = segImage.getPixelData();
|
|
158
|
-
const segVoxelManager = segImage.voxelManager;
|
|
159
|
-
const segSpacing = [segImage.rowPixelSpacing, segImage.columnPixelSpacing];
|
|
160
|
-
const refImageId = segImage.referencedImageId;
|
|
161
|
-
const refImage = cache.getImage(refImageId);
|
|
162
|
-
const refPixelData = refImage.getPixelData();
|
|
163
|
-
const refVoxelManager = refImage.voxelManager;
|
|
164
|
-
const refSpacing = [refImage.rowPixelSpacing, refImage.columnPixelSpacing];
|
|
165
|
-
segmentationInfo.push({
|
|
166
|
-
scalarData: segPixelData,
|
|
167
|
-
dimensions: segVoxelManager.dimensions,
|
|
168
|
-
spacing: segSpacing,
|
|
169
|
-
});
|
|
170
|
-
imageInfo.push({
|
|
171
|
-
scalarData: refPixelData,
|
|
172
|
-
dimensions: refVoxelManager.dimensions,
|
|
173
|
-
spacing: refSpacing,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
const stats = await workerManager.executeTask('compute', 'calculateSegmentsStatisticsStack', {
|
|
136
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 0);
|
|
137
|
+
const { segmentationInfo, imageInfo } = prepareStackDataForWorker(segImageIds);
|
|
138
|
+
const stats = await getWebWorkerManager().executeTask('compute', 'calculateSegmentsStatisticsStack', {
|
|
177
139
|
segmentationInfo,
|
|
178
140
|
imageInfo,
|
|
179
141
|
indices,
|
|
180
142
|
mode,
|
|
181
143
|
});
|
|
182
|
-
triggerWorkerProgress(
|
|
144
|
+
triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 100);
|
|
183
145
|
const spacing = segmentationInfo[0].spacing;
|
|
184
146
|
const segmentationImageData = segmentationInfo[0];
|
|
185
147
|
const imageVoxelManager = imageInfo[0].voxelManager;
|
|
@@ -237,31 +199,4 @@ function getSphereStats(testMax, radiusIJK, segData, imageVoxels, spacing) {
|
|
|
237
199
|
});
|
|
238
200
|
return VolumetricCalculator.getStatistics({ spacing });
|
|
239
201
|
}
|
|
240
|
-
function getImageReferenceInfo(segVolumeId, segImageIds) {
|
|
241
|
-
let refImageId;
|
|
242
|
-
let modalityUnitOptions;
|
|
243
|
-
if (segVolumeId) {
|
|
244
|
-
const segmentationVolume = cache.getVolume(segVolumeId);
|
|
245
|
-
const referencedVolumeId = segmentationVolume.referencedVolumeId;
|
|
246
|
-
const volume = cache.getVolume(referencedVolumeId);
|
|
247
|
-
if (volume?.imageIds?.length > 0) {
|
|
248
|
-
refImageId = volume.imageIds[0];
|
|
249
|
-
}
|
|
250
|
-
modalityUnitOptions = {
|
|
251
|
-
isPreScaled: Object.keys(volume.scaling || {}).length > 0,
|
|
252
|
-
isSuvScaled: Boolean(volume.scaling?.PT),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
else if (segImageIds?.length) {
|
|
256
|
-
const segImage = cache.getImage(segImageIds[0]);
|
|
257
|
-
refImageId = segImage.referencedImageId;
|
|
258
|
-
const refImage = cache.getImage(refImageId);
|
|
259
|
-
const scalingModule = metaData.get('scalingModule', refImageId);
|
|
260
|
-
modalityUnitOptions = {
|
|
261
|
-
isPreScaled: Boolean(refImage.preScale?.scaled),
|
|
262
|
-
isSuvScaled: typeof scalingModule?.preScale?.scaled === 'number',
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
return { refImageId, modalityUnitOptions };
|
|
266
|
-
}
|
|
267
202
|
export default getStatistics;
|
|
@@ -26,4 +26,6 @@ import getStatistics from './getStatistics';
|
|
|
26
26
|
import * as validateLabelmap from './validateLabelmap';
|
|
27
27
|
import { computeStackLabelmapFromVolume } from '../../stateManagement/segmentation/helpers/computeStackLabelmapFromVolume';
|
|
28
28
|
import { computeVolumeLabelmapFromStack } from '../../stateManagement/segmentation/helpers/computeVolumeLabelmapFromStack';
|
|
29
|
-
|
|
29
|
+
import { getReferenceVolumeForSegmentationVolume } from './getReferenceVolumeForSegmentationVolume';
|
|
30
|
+
import { getSegmentLargestBidirectional } from './getSegmentLargestBidirectional';
|
|
31
|
+
export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, };
|
|
@@ -26,4 +26,6 @@ import getStatistics from './getStatistics';
|
|
|
26
26
|
import * as validateLabelmap from './validateLabelmap';
|
|
27
27
|
import { computeStackLabelmapFromVolume } from '../../stateManagement/segmentation/helpers/computeStackLabelmapFromVolume';
|
|
28
28
|
import { computeVolumeLabelmapFromStack } from '../../stateManagement/segmentation/helpers/computeVolumeLabelmapFromStack';
|
|
29
|
-
|
|
29
|
+
import { getReferenceVolumeForSegmentationVolume } from './getReferenceVolumeForSegmentationVolume';
|
|
30
|
+
import { getSegmentLargestBidirectional } from './getSegmentLargestBidirectional';
|
|
31
|
+
export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, };
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
import { vec3 } from 'gl-matrix';
|
|
3
|
+
import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
|
3
4
|
export default function isLineInSegment(point1: Types.Point3, point2: Types.Point3, isInSegment: any): boolean;
|
|
5
|
+
declare function createIsInSegmentMetadata({ dimensions, imageData, voxelManager, segmentIndex, containedSegmentIndices, }: {
|
|
6
|
+
dimensions: number[];
|
|
7
|
+
imageData: vtkImageData;
|
|
8
|
+
voxelManager: Types.IVoxelManager<number>;
|
|
9
|
+
segmentIndex: number;
|
|
10
|
+
containedSegmentIndices?: Set<number>;
|
|
11
|
+
}): {
|
|
12
|
+
testCenter: (point1: any, point2: any) => boolean;
|
|
13
|
+
toIJK: (point: any) => vec3;
|
|
14
|
+
testIJK: (ijk: any) => boolean;
|
|
15
|
+
};
|
|
4
16
|
declare function createIsInSegment(segVolumeId: string, segmentIndex: number, containedSegmentIndices?: Set<number>): {
|
|
5
17
|
testCenter: (point1: any, point2: any) => boolean;
|
|
6
18
|
toIJK: (point: any) => vec3;
|
|
7
19
|
testIJK: (ijk: any) => boolean;
|
|
8
20
|
};
|
|
9
|
-
export { createIsInSegment, isLineInSegment };
|
|
21
|
+
export { createIsInSegment, createIsInSegmentMetadata, isLineInSegment };
|
|
@@ -19,25 +19,19 @@ export default function isLineInSegment(point1, point2, isInSegment) {
|
|
|
19
19
|
}
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
console.warn(`No volume found for ${segVolumeId}`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const voxelManager = vol.voxelManager;
|
|
29
|
-
const width = vol.dimensions[0];
|
|
30
|
-
const pixelsPerSlice = width * vol.dimensions[1];
|
|
22
|
+
function createIsInSegmentMetadata({ dimensions, imageData, voxelManager, segmentIndex, containedSegmentIndices, }) {
|
|
23
|
+
const width = dimensions[0];
|
|
24
|
+
const pixelsPerSlice = width * dimensions[1];
|
|
31
25
|
return {
|
|
32
26
|
testCenter: (point1, point2) => {
|
|
33
27
|
const point = vec3.add(vec3.create(), point1, point2).map((it) => it / 2);
|
|
34
|
-
const ijk =
|
|
28
|
+
const ijk = imageData.worldToIndex(point).map(Math.round);
|
|
35
29
|
const [i, j, k] = ijk;
|
|
36
30
|
const index = i + j * width + k * pixelsPerSlice;
|
|
37
31
|
const value = voxelManager.getAtIndex(index);
|
|
38
32
|
return value === segmentIndex || containedSegmentIndices?.has(value);
|
|
39
33
|
},
|
|
40
|
-
toIJK: (point) =>
|
|
34
|
+
toIJK: (point) => imageData.worldToIndex(point),
|
|
41
35
|
testIJK: (ijk) => {
|
|
42
36
|
const [i, j, k] = ijk;
|
|
43
37
|
const index = Math.round(i) + Math.round(j) * width + Math.round(k) * pixelsPerSlice;
|
|
@@ -46,4 +40,18 @@ function createIsInSegment(segVolumeId, segmentIndex, containedSegmentIndices) {
|
|
|
46
40
|
},
|
|
47
41
|
};
|
|
48
42
|
}
|
|
49
|
-
|
|
43
|
+
function createIsInSegment(segVolumeId, segmentIndex, containedSegmentIndices) {
|
|
44
|
+
const vol = cache.getVolume(segVolumeId);
|
|
45
|
+
if (!vol) {
|
|
46
|
+
console.warn(`No volume found for ${segVolumeId}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
return createIsInSegmentMetadata({
|
|
50
|
+
dimensions: vol.dimensions,
|
|
51
|
+
imageData: vol.imageData,
|
|
52
|
+
voxelManager: vol.voxelManager,
|
|
53
|
+
segmentIndex,
|
|
54
|
+
containedSegmentIndices,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export { createIsInSegment, createIsInSegmentMetadata, isLineInSegment };
|
|
@@ -6,6 +6,7 @@ import BidirectionalTool from '../../tools/annotation/BidirectionalTool';
|
|
|
6
6
|
import { getSegmentations } from '../../stateManagement/segmentation/getSegmentations';
|
|
7
7
|
import { getActiveSegmentIndex } from '../../stateManagement/segmentation/getActiveSegmentIndex';
|
|
8
8
|
export default function segmentContourAction(element, configuration) {
|
|
9
|
+
console.warn('Deprecation Alert: There is a new getSegmentLargestBidirectional function that handles volume, stack and individual segment cases properly. This function is deprecated and will be removed in a future version.');
|
|
9
10
|
const { data: configurationData } = configuration;
|
|
10
11
|
const enabledElement = getEnabledElement(element);
|
|
11
12
|
const segment = (configurationData.getSegment || defaultGetSegment)(enabledElement, configurationData);
|