@cornerstonejs/tools 2.3.2 → 2.4.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/tools/annotation/CircleROITool.js +1 -1
- package/dist/esm/tools/segmentation/PaintFillTool.js +1 -1
- package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +1 -3
- package/dist/esm/tools/segmentation/strategies/compositions/dynamicThreshold.js +18 -3
- package/dist/esm/tools/segmentation/strategies/compositions/islandRemoval.d.ts +9 -0
- package/dist/esm/tools/segmentation/strategies/compositions/islandRemoval.js +138 -104
- package/dist/esm/tools/segmentation/strategies/compositions/preview.js +3 -1
- package/dist/esm/tools/segmentation/strategies/compositions/regionFill.js +1 -1
- package/dist/esm/tools/segmentation/strategies/compositions/setValue.js +0 -1
- package/dist/esm/tools/segmentation/strategies/utils/normalizeViewportPlane.d.ts +19 -0
- package/dist/esm/tools/segmentation/strategies/utils/normalizeViewportPlane.js +35 -0
- package/dist/esm/types/FloodFillTypes.d.ts +2 -1
- package/dist/esm/utilities/segmentation/VolumetricCalculator.js +3 -1
- package/dist/esm/utilities/segmentation/floodFill.js +9 -11
- package/package.json +3 -3
|
@@ -529,9 +529,9 @@ class CircleROITool extends AnnotationTool {
|
|
|
529
529
|
area,
|
|
530
530
|
mean: stats.mean?.value,
|
|
531
531
|
max: stats.max?.value,
|
|
532
|
+
pointsInShape,
|
|
532
533
|
stdDev: stats.stdDev?.value,
|
|
533
534
|
statsArray: stats.array,
|
|
534
|
-
pointsInShape: pointsInShape,
|
|
535
535
|
isEmptyArea,
|
|
536
536
|
areaUnit,
|
|
537
537
|
radius: worldWidth / 2 / scale,
|
|
@@ -80,7 +80,7 @@ class PaintFillTool extends BaseTool {
|
|
|
80
80
|
return true;
|
|
81
81
|
};
|
|
82
82
|
this.getFramesModified = (fixedDimension, fixedDimensionValue, floodFillResult) => {
|
|
83
|
-
const { boundaries } = floodFillResult;
|
|
83
|
+
const { flooded: boundaries } = floodFillResult;
|
|
84
84
|
if (fixedDimension === 2) {
|
|
85
85
|
return [fixedDimensionValue];
|
|
86
86
|
}
|
|
@@ -90,9 +90,7 @@ export default class BrushStrategy {
|
|
|
90
90
|
const segmentationVoxelManagerToUse = operationData.override?.voxelManager || segmentationVoxelManager;
|
|
91
91
|
const segmentationImageDataToUse = operationData.override?.imageData || segmentationImageData;
|
|
92
92
|
const previewVoxelManager = operationData.preview?.previewVoxelManager ||
|
|
93
|
-
VoxelManager.
|
|
94
|
-
sourceVoxelManager: segmentationVoxelManagerToUse,
|
|
95
|
-
});
|
|
93
|
+
VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
|
|
96
94
|
const previewEnabled = !!operationData.previewColors;
|
|
97
95
|
const previewSegmentIndex = previewEnabled ? 255 : undefined;
|
|
98
96
|
const initializedData = {
|
|
@@ -2,7 +2,7 @@ import { vec3 } from 'gl-matrix';
|
|
|
2
2
|
import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
|
|
3
3
|
export default {
|
|
4
4
|
[StrategyCallbacks.Initialize]: (operationData) => {
|
|
5
|
-
const { operationName, centerIJK, strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, segmentIndex, } = operationData;
|
|
5
|
+
const { operationName, centerIJK, strategySpecificConfiguration, segmentationVoxelManager, imageVoxelManager, segmentIndex, viewport, } = operationData;
|
|
6
6
|
const { THRESHOLD } = strategySpecificConfiguration;
|
|
7
7
|
if (!THRESHOLD?.isDynamic || !centerIJK || !segmentIndex) {
|
|
8
8
|
return;
|
|
@@ -14,6 +14,7 @@ export default {
|
|
|
14
14
|
const boundsIJK = segmentationVoxelManager.getBoundsIJK();
|
|
15
15
|
const { threshold: oldThreshold, dynamicRadius = 0 } = THRESHOLD;
|
|
16
16
|
const useDelta = oldThreshold ? 0 : dynamicRadius;
|
|
17
|
+
const { viewPlaneNormal } = viewport.getCamera();
|
|
17
18
|
const nestedBounds = boundsIJK.map((ijk, idx) => {
|
|
18
19
|
const [min, max] = ijk;
|
|
19
20
|
return [
|
|
@@ -21,8 +22,22 @@ export default {
|
|
|
21
22
|
Math.min(max, centerIJK[idx] + useDelta),
|
|
22
23
|
];
|
|
23
24
|
});
|
|
25
|
+
if (Math.abs(viewPlaneNormal[0]) > 0.8) {
|
|
26
|
+
nestedBounds[0] = [centerIJK[0], centerIJK[0]];
|
|
27
|
+
}
|
|
28
|
+
else if (Math.abs(viewPlaneNormal[1]) > 0.8) {
|
|
29
|
+
nestedBounds[1] = [centerIJK[1], centerIJK[1]];
|
|
30
|
+
}
|
|
31
|
+
else if (Math.abs(viewPlaneNormal[2]) > 0.8) {
|
|
32
|
+
nestedBounds[2] = [centerIJK[2], centerIJK[2]];
|
|
33
|
+
}
|
|
24
34
|
const threshold = oldThreshold || [Infinity, -Infinity];
|
|
25
|
-
const
|
|
35
|
+
const useDeltaSqr = useDelta * useDelta;
|
|
36
|
+
const callback = ({ value, pointIJK }) => {
|
|
37
|
+
const distance = vec3.sqrDist(centerIJK, pointIJK);
|
|
38
|
+
if (distance > useDeltaSqr) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
26
41
|
const gray = Array.isArray(value) ? vec3.len(value) : value;
|
|
27
42
|
threshold[0] = Math.min(gray, threshold[0]);
|
|
28
43
|
threshold[1] = Math.max(gray, threshold[1]);
|
|
@@ -62,6 +77,6 @@ export default {
|
|
|
62
77
|
strategySpecificConfiguration[activeStrategy] = {};
|
|
63
78
|
}
|
|
64
79
|
strategySpecificConfiguration[activeStrategy].dynamicRadiusInCanvas =
|
|
65
|
-
dynamicRadiusInCanvas;
|
|
80
|
+
3 + dynamicRadiusInCanvas;
|
|
66
81
|
},
|
|
67
82
|
};
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { utilities } from '@cornerstonejs/core';
|
|
1
2
|
import type { InitializedOperationData } from '../BrushStrategy';
|
|
3
|
+
export declare enum SegmentationEnum {
|
|
4
|
+
SEGMENT = 1,
|
|
5
|
+
ISLAND = 2,
|
|
6
|
+
INTERIOR = 3,
|
|
7
|
+
EXTERIOR = 4
|
|
8
|
+
}
|
|
2
9
|
declare const _default: {
|
|
3
10
|
onInteractionEnd: (operationData: InitializedOperationData) => void;
|
|
4
11
|
};
|
|
5
12
|
export default _default;
|
|
13
|
+
export declare function createSegmentSet(operationData: InitializedOperationData): utilities.RLEVoxelMap<SegmentationEnum>;
|
|
14
|
+
export declare function covers(rle: any, row: any): boolean;
|
|
@@ -1,123 +1,157 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { utilities } from '@cornerstonejs/core';
|
|
2
2
|
import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents';
|
|
3
3
|
import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
|
|
4
|
+
import normalizeViewportPlane from '../utils/normalizeViewportPlane';
|
|
5
|
+
const { RLEVoxelMap } = utilities;
|
|
6
|
+
const MAX_IMAGE_SIZE = 65535;
|
|
7
|
+
export var SegmentationEnum;
|
|
8
|
+
(function (SegmentationEnum) {
|
|
9
|
+
SegmentationEnum[SegmentationEnum["SEGMENT"] = 1] = "SEGMENT";
|
|
10
|
+
SegmentationEnum[SegmentationEnum["ISLAND"] = 2] = "ISLAND";
|
|
11
|
+
SegmentationEnum[SegmentationEnum["INTERIOR"] = 3] = "INTERIOR";
|
|
12
|
+
SegmentationEnum[SegmentationEnum["EXTERIOR"] = 4] = "EXTERIOR";
|
|
13
|
+
})(SegmentationEnum || (SegmentationEnum = {}));
|
|
4
14
|
export default {
|
|
5
15
|
[StrategyCallbacks.OnInteractionEnd]: (operationData) => {
|
|
6
|
-
const {
|
|
7
|
-
if (!strategySpecificConfiguration.THRESHOLD ||
|
|
16
|
+
const { strategySpecificConfiguration, previewSegmentIndex, segmentIndex } = operationData;
|
|
17
|
+
if (!strategySpecificConfiguration.THRESHOLD ||
|
|
18
|
+
segmentIndex === null ||
|
|
19
|
+
previewSegmentIndex === undefined) {
|
|
8
20
|
return;
|
|
9
21
|
}
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
22
|
+
const segmentSet = createSegmentSet(operationData);
|
|
23
|
+
if (!segmentSet) {
|
|
12
24
|
return;
|
|
13
25
|
}
|
|
14
|
-
|
|
26
|
+
const externalRemoved = removeExternalIslands(operationData, segmentSet);
|
|
27
|
+
if (externalRemoved === undefined) {
|
|
15
28
|
return;
|
|
16
29
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
.map((bound, i) => [
|
|
20
|
-
Math.min(bound[0], ...clickedPoints.map((point) => point[i])),
|
|
21
|
-
Math.max(bound[1], ...clickedPoints.map((point) => point[i])),
|
|
22
|
-
]);
|
|
23
|
-
if (boundsIJK.find((it) => it[0] < 0 || it[1] > 65535)) {
|
|
30
|
+
const arrayOfSlices = removeInternalIslands(operationData, segmentSet);
|
|
31
|
+
if (!arrayOfSlices) {
|
|
24
32
|
return;
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
clickedPoints.forEach((clickedPoint) => {
|
|
58
|
-
if (getter(...clickedPoint) === 1) {
|
|
59
|
-
floodFill(getter, clickedPoint, {
|
|
60
|
-
onFlood,
|
|
61
|
-
diagonals: true,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
let clearedCount = 0;
|
|
66
|
-
let previewCount = 0;
|
|
67
|
-
const callback = ({ index, pointIJK, value: trackValue }) => {
|
|
68
|
-
const value = segmentationVoxelManager.getAtIndex(index);
|
|
69
|
-
if (floodedSet.has(index)) {
|
|
70
|
-
previewCount++;
|
|
71
|
-
const newValue = trackValue === segmentIndex ? segmentIndex : previewSegmentIndex;
|
|
72
|
-
previewVoxelManager.setAtIJKPoint(pointIJK, newValue);
|
|
73
|
-
}
|
|
74
|
-
else if (value === previewSegmentIndex) {
|
|
75
|
-
clearedCount++;
|
|
76
|
-
const newValue = trackValue ?? 0;
|
|
77
|
-
previewVoxelManager.setAtIJKPoint(pointIJK, newValue);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
previewVoxelManager.forEach(callback, {});
|
|
81
|
-
if (floodedCount - previewCount !== 0) {
|
|
82
|
-
console.warn('There were flooded=', floodedCount, 'cleared=', clearedCount, 'preview count=', previewCount, 'not handled', floodedCount - previewCount);
|
|
34
|
+
triggerSegmentationDataModified(operationData.segmentationId, arrayOfSlices, previewSegmentIndex);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
export function createSegmentSet(operationData) {
|
|
38
|
+
const { segmentationVoxelManager, previewSegmentIndex, previewVoxelManager, segmentIndex, viewport, } = operationData;
|
|
39
|
+
const clickedPoints = previewVoxelManager.getPoints();
|
|
40
|
+
if (!clickedPoints?.length) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const boundsIJK = previewVoxelManager
|
|
44
|
+
.getBoundsIJK()
|
|
45
|
+
.map((bound, i) => [
|
|
46
|
+
Math.min(bound[0], ...clickedPoints.map((point) => point[i])),
|
|
47
|
+
Math.max(bound[1], ...clickedPoints.map((point) => point[i])),
|
|
48
|
+
]);
|
|
49
|
+
if (boundsIJK.find((it) => it[0] < 0 || it[1] > MAX_IMAGE_SIZE)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const { toIJK, fromIJK, boundsIJKPrime, error } = normalizeViewportPlane(viewport, boundsIJK);
|
|
53
|
+
if (error) {
|
|
54
|
+
console.warn('Not performing island removal for planes not orthogonal to acquisition plane', error);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const [width, height, depth] = fromIJK(segmentationVoxelManager.dimensions);
|
|
58
|
+
const floodedSet = new RLEVoxelMap(width, height, depth);
|
|
59
|
+
const getter = (i, j, k) => {
|
|
60
|
+
const index = segmentationVoxelManager.toIndex(toIJK([i, j, k]));
|
|
61
|
+
const oldVal = segmentationVoxelManager.getAtIndex(index);
|
|
62
|
+
if (oldVal === previewSegmentIndex || oldVal === segmentIndex) {
|
|
63
|
+
return SegmentationEnum.SEGMENT;
|
|
83
64
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
65
|
+
};
|
|
66
|
+
floodedSet.fillFrom(getter, boundsIJKPrime);
|
|
67
|
+
floodedSet.normalizer = { toIJK, fromIJK, boundsIJKPrime };
|
|
68
|
+
return floodedSet;
|
|
69
|
+
}
|
|
70
|
+
function removeInternalIslands(operationData, floodedSet) {
|
|
71
|
+
const { height, normalizer } = floodedSet;
|
|
72
|
+
const { toIJK } = normalizer;
|
|
73
|
+
const { previewVoxelManager, previewSegmentIndex } = operationData;
|
|
74
|
+
floodedSet.forEachRow((baseIndex, row) => {
|
|
75
|
+
let lastRle;
|
|
76
|
+
for (const rle of [...row]) {
|
|
77
|
+
if (rle.value !== SegmentationEnum.ISLAND) {
|
|
88
78
|
continue;
|
|
89
79
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const onFloodInternal = (i, j, k) => {
|
|
93
|
-
const floodIndex = previewVoxelManager.toIndex([i, j, k]);
|
|
94
|
-
floodedSet.add(floodIndex);
|
|
95
|
-
if ((boundsIJK[0][0] !== boundsIJK[0][1] &&
|
|
96
|
-
(i === boundsIJK[0][0] || i === boundsIJK[0][1])) ||
|
|
97
|
-
(boundsIJK[1][0] !== boundsIJK[1][1] &&
|
|
98
|
-
(j === boundsIJK[1][0] || j === boundsIJK[1][1])) ||
|
|
99
|
-
(boundsIJK[2][0] !== boundsIJK[2][1] &&
|
|
100
|
-
(k === boundsIJK[2][0] || k === boundsIJK[2][1]))) {
|
|
101
|
-
isInternal = false;
|
|
102
|
-
}
|
|
103
|
-
if (isInternal) {
|
|
104
|
-
internalSet.add(floodIndex);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
const pointIJK = previewVoxelManager.toIJK(index);
|
|
108
|
-
if (getter(...pointIJK) !== 0) {
|
|
80
|
+
if (!lastRle) {
|
|
81
|
+
lastRle = rle;
|
|
109
82
|
continue;
|
|
110
83
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
diagonals: false,
|
|
114
|
-
});
|
|
115
|
-
if (isInternal) {
|
|
116
|
-
for (const index of internalSet) {
|
|
117
|
-
previewVoxelManager.setAtIndex(index, previewSegmentIndex);
|
|
118
|
-
}
|
|
84
|
+
for (let iPrime = lastRle.end; iPrime < rle.start; iPrime++) {
|
|
85
|
+
floodedSet.set(baseIndex + iPrime, SegmentationEnum.INTERIOR);
|
|
119
86
|
}
|
|
87
|
+
lastRle = rle;
|
|
120
88
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
89
|
+
});
|
|
90
|
+
floodedSet.forEach((baseIndex, rle) => {
|
|
91
|
+
if (rle.value !== SegmentationEnum.INTERIOR) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const [, jPrime, kPrime] = floodedSet.toIJK(baseIndex);
|
|
95
|
+
const rowPrev = jPrime > 0 ? floodedSet.getRun(jPrime - 1, kPrime) : null;
|
|
96
|
+
const rowNext = jPrime + 1 < height ? floodedSet.getRun(jPrime + 1, kPrime) : null;
|
|
97
|
+
const prevCovers = covers(rle, rowPrev);
|
|
98
|
+
const nextCovers = covers(rle, rowNext);
|
|
99
|
+
if (rle.end - rle.start > 2 && (!prevCovers || !nextCovers)) {
|
|
100
|
+
floodedSet.floodFill(rle.start, jPrime, kPrime, SegmentationEnum.EXTERIOR, { singlePlane: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
floodedSet.forEach((baseIndex, rle) => {
|
|
104
|
+
if (rle.value !== SegmentationEnum.INTERIOR) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
|
|
108
|
+
const clearPoint = toIJK(floodedSet.toIJK(baseIndex + iPrime));
|
|
109
|
+
previewVoxelManager.setAtIJKPoint(clearPoint, previewSegmentIndex);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return previewVoxelManager.getArrayOfModifiedSlices();
|
|
113
|
+
}
|
|
114
|
+
function removeExternalIslands(operationData, floodedSet) {
|
|
115
|
+
const { previewVoxelManager } = operationData;
|
|
116
|
+
const { toIJK, fromIJK } = floodedSet.normalizer;
|
|
117
|
+
const clickedPoints = previewVoxelManager.getPoints();
|
|
118
|
+
let floodedCount = 0;
|
|
119
|
+
clickedPoints.forEach((clickedPoint) => {
|
|
120
|
+
const ijkPrime = fromIJK(clickedPoint);
|
|
121
|
+
const index = floodedSet.toIndex(ijkPrime);
|
|
122
|
+
const [iPrime, jPrime, kPrime] = ijkPrime;
|
|
123
|
+
if (floodedSet.get(index) === SegmentationEnum.SEGMENT) {
|
|
124
|
+
floodedCount += floodedSet.floodFill(iPrime, jPrime, kPrime, SegmentationEnum.ISLAND);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (floodedCount === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const callback = (index, rle) => {
|
|
131
|
+
const [, jPrime, kPrime] = floodedSet.toIJK(index);
|
|
132
|
+
if (rle.value !== SegmentationEnum.ISLAND) {
|
|
133
|
+
for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
|
|
134
|
+
const clearPoint = toIJK([iPrime, jPrime, kPrime]);
|
|
135
|
+
previewVoxelManager.setAtIJKPoint(clearPoint, null);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
floodedSet.forEach(callback, { rowModified: true });
|
|
140
|
+
return floodedCount;
|
|
141
|
+
}
|
|
142
|
+
export function covers(rle, row) {
|
|
143
|
+
if (!row) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
let { start } = rle;
|
|
147
|
+
const { end } = rle;
|
|
148
|
+
for (const rowRle of row) {
|
|
149
|
+
if (start >= rowRle.start && start < rowRle.end) {
|
|
150
|
+
start = rowRle.end;
|
|
151
|
+
if (start >= end) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
@@ -38,7 +38,9 @@ export default {
|
|
|
38
38
|
operationData.segmentationVoxelManager;
|
|
39
39
|
operationData.previewVoxelManager = preview.previewVoxelManager;
|
|
40
40
|
}
|
|
41
|
-
if (segmentIndex ===
|
|
41
|
+
if (segmentIndex === undefined ||
|
|
42
|
+
segmentIndex === null ||
|
|
43
|
+
!previewSegmentIndex) {
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
const configColor = previewColors?.[segmentIndex];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
|
|
2
2
|
export default {
|
|
3
3
|
[StrategyCallbacks.Fill]: (operationData) => {
|
|
4
|
-
const { segmentsLocked, segmentationImageData, segmentationVoxelManager, previewVoxelManager
|
|
4
|
+
const { segmentsLocked, segmentationImageData, segmentationVoxelManager, previewVoxelManager, brushStrategy, centerIJK, } = operationData;
|
|
5
5
|
const isWithinThreshold = brushStrategy.createIsInThreshold?.(operationData);
|
|
6
6
|
const { setValue } = brushStrategy;
|
|
7
7
|
const callback = isWithinThreshold
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
|
|
2
|
-
import { triggerEvent, eventTarget } from '@cornerstonejs/core';
|
|
3
2
|
export default {
|
|
4
3
|
[StrategyCallbacks.INTERNAL_setValue]: (operationData, { value, index }) => {
|
|
5
4
|
const { segmentsLocked, segmentIndex, previewVoxelManager, previewSegmentIndex, segmentationVoxelManager, } = operationData;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
export default function normalizeViewportPlane(viewport: Types.IViewport, boundsIJK: Types.BoundsIJK): {
|
|
3
|
+
toIJK: any;
|
|
4
|
+
boundsIJKPrime: any;
|
|
5
|
+
fromIJK: any;
|
|
6
|
+
error: string;
|
|
7
|
+
} | {
|
|
8
|
+
boundsIJKPrime: any;
|
|
9
|
+
toIJK: (ijkPrime: any) => any;
|
|
10
|
+
fromIJK: (ijk: any) => any;
|
|
11
|
+
type: string;
|
|
12
|
+
error?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
boundsIJKPrime: any;
|
|
15
|
+
toIJK: ([j, k, i]: [any, any, any]) => any[];
|
|
16
|
+
fromIJK: ([i, j, k]: [any, any, any]) => any[];
|
|
17
|
+
type: string;
|
|
18
|
+
error?: undefined;
|
|
19
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseVolumeViewport, utilities } from '@cornerstonejs/core';
|
|
2
|
+
const { isEqual } = utilities;
|
|
3
|
+
const acquisitionMapping = {
|
|
4
|
+
toIJK: (ijkPrime) => ijkPrime,
|
|
5
|
+
fromIJK: (ijk) => ijk,
|
|
6
|
+
type: 'acquistion',
|
|
7
|
+
};
|
|
8
|
+
const jkMapping = {
|
|
9
|
+
toIJK: ([j, k, i]) => [i, j, k],
|
|
10
|
+
fromIJK: ([i, j, k]) => [j, k, i],
|
|
11
|
+
type: 'jk',
|
|
12
|
+
};
|
|
13
|
+
const ikMapping = {
|
|
14
|
+
toIJK: ([i, k, j]) => [i, j, k],
|
|
15
|
+
fromIJK: ([i, j, k]) => [i, k, j],
|
|
16
|
+
type: 'ik',
|
|
17
|
+
};
|
|
18
|
+
export default function normalizeViewportPlane(viewport, boundsIJK) {
|
|
19
|
+
if (!(viewport instanceof BaseVolumeViewport)) {
|
|
20
|
+
return { ...acquisitionMapping, boundsIJKPrime: boundsIJK };
|
|
21
|
+
}
|
|
22
|
+
const { viewPlaneNormal } = viewport.getCamera();
|
|
23
|
+
const mapping = (isEqual(Math.abs(viewPlaneNormal[0]), 1) && jkMapping) ||
|
|
24
|
+
(isEqual(Math.abs(viewPlaneNormal[1]), 1) && ikMapping) ||
|
|
25
|
+
(isEqual(Math.abs(viewPlaneNormal[2]), 1) && acquisitionMapping);
|
|
26
|
+
if (!mapping) {
|
|
27
|
+
return {
|
|
28
|
+
toIJK: null,
|
|
29
|
+
boundsIJKPrime: null,
|
|
30
|
+
fromIJK: null,
|
|
31
|
+
error: `Only mappings orthogonal to acquisition plane are permitted, but requested ${viewPlaneNormal}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { ...mapping, boundsIJKPrime: mapping.fromIJK(boundsIJK) };
|
|
35
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
type FloodFillResult = {
|
|
3
3
|
flooded: Types.Point2[] | Types.Point3[];
|
|
4
|
-
boundaries: Types.Point2[] | Types.Point3[];
|
|
5
4
|
};
|
|
6
5
|
type FloodFillGetter3D = (x: number, y: number, z: number) => unknown;
|
|
7
6
|
type FloodFillGetter2D = (x: number, y: number) => unknown;
|
|
@@ -11,5 +10,7 @@ type FloodFillOptions = {
|
|
|
11
10
|
onBoundary?: (x: number, y: number, z?: number) => void;
|
|
12
11
|
equals?: (a: any, b: any) => boolean;
|
|
13
12
|
diagonals?: boolean;
|
|
13
|
+
bounds?: Map<number, Types.Point2 | Types.Point3>;
|
|
14
|
+
filter?: (point: any) => boolean;
|
|
14
15
|
};
|
|
15
16
|
export type { FloodFillResult, FloodFillGetter, FloodFillOptions };
|
|
@@ -4,7 +4,9 @@ export default class VolumetricCalculator extends BasicStatsCalculator {
|
|
|
4
4
|
const { spacing } = options;
|
|
5
5
|
const stats = BasicStatsCalculator.getStatistics();
|
|
6
6
|
const volumeUnit = spacing ? 'mm\xb3' : 'voxels\xb3';
|
|
7
|
-
const volumeScale = spacing
|
|
7
|
+
const volumeScale = spacing
|
|
8
|
+
? spacing[0] * spacing[1] * spacing[2] * 1000
|
|
9
|
+
: 1;
|
|
8
10
|
stats.volume = {
|
|
9
11
|
value: Array.isArray(stats.count.value)
|
|
10
12
|
? stats.count.value.map((v) => v * volumeScale)
|
|
@@ -2,20 +2,20 @@ function floodFill(getter, seed, options = {}) {
|
|
|
2
2
|
const onFlood = options.onFlood;
|
|
3
3
|
const onBoundary = options.onBoundary;
|
|
4
4
|
const equals = options.equals;
|
|
5
|
+
const filter = options.filter;
|
|
5
6
|
const diagonals = options.diagonals || false;
|
|
6
7
|
const startNode = get(seed);
|
|
7
8
|
const permutations = prunedPermutations();
|
|
8
9
|
const stack = [];
|
|
9
10
|
const flooded = [];
|
|
10
11
|
const visits = new Set();
|
|
11
|
-
const bounds =
|
|
12
|
+
const bounds = options.bounds;
|
|
12
13
|
stack.push({ currentArgs: seed });
|
|
13
14
|
while (stack.length > 0) {
|
|
14
15
|
flood(stack.pop());
|
|
15
16
|
}
|
|
16
17
|
return {
|
|
17
18
|
flooded,
|
|
18
|
-
boundaries: boundaries(),
|
|
19
19
|
};
|
|
20
20
|
function flood(job) {
|
|
21
21
|
const getArgs = job.currentArgs;
|
|
@@ -55,7 +55,7 @@ function floodFill(getter, seed, options = {}) {
|
|
|
55
55
|
function markAsBoundary(prevArgs) {
|
|
56
56
|
const [x, y, z = 0] = prevArgs;
|
|
57
57
|
const iKey = x + 32768 + 65536 * (y + 32768 + 65536 * (z + 32768));
|
|
58
|
-
bounds
|
|
58
|
+
bounds?.set(iKey, prevArgs);
|
|
59
59
|
if (onBoundary) {
|
|
60
60
|
onBoundary(...prevArgs);
|
|
61
61
|
}
|
|
@@ -67,6 +67,12 @@ function floodFill(getter, seed, options = {}) {
|
|
|
67
67
|
for (let j = 0; j < getArgs.length; j += 1) {
|
|
68
68
|
nextArgs[j] += perm[j];
|
|
69
69
|
}
|
|
70
|
+
if (filter?.(nextArgs) === false) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (visited(nextArgs)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
70
76
|
stack.push({
|
|
71
77
|
currentArgs: nextArgs,
|
|
72
78
|
previousArgs: getArgs,
|
|
@@ -96,14 +102,6 @@ function floodFill(getter, seed, options = {}) {
|
|
|
96
102
|
}
|
|
97
103
|
return perms;
|
|
98
104
|
}
|
|
99
|
-
function boundaries() {
|
|
100
|
-
const array = Array.from(bounds.values());
|
|
101
|
-
array.reverse();
|
|
102
|
-
return array;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function defaultEquals(a, b) {
|
|
106
|
-
return a === b;
|
|
107
105
|
}
|
|
108
106
|
function countNonZeroes(array) {
|
|
109
107
|
let count = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Cornerstone3D Tools",
|
|
5
5
|
"types": "./dist/esm/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"canvas": "^2.11.2"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
|
-
"@cornerstonejs/core": "^2.
|
|
107
|
+
"@cornerstonejs/core": "^2.4.0",
|
|
108
108
|
"@kitware/vtk.js": "32.1.1",
|
|
109
109
|
"@types/d3-array": "^3.0.4",
|
|
110
110
|
"@types/d3-interpolate": "^3.0.1",
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"type": "individual",
|
|
124
124
|
"url": "https://ohif.org/donate"
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "70fc2826230875c1c5f3533953fac9a3025833c0"
|
|
127
127
|
}
|