@cornerstonejs/tools 3.0.5 → 3.1.1
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/enums/ChangeTypes.d.ts +2 -1
- package/dist/esm/enums/ChangeTypes.js +1 -0
- package/dist/esm/tools/SculptorTool.js +2 -2
- package/dist/esm/tools/annotation/AngleTool.js +5 -2
- package/dist/esm/tools/annotation/ArrowAnnotateTool.js +6 -4
- package/dist/esm/tools/annotation/BidirectionalTool.js +6 -2
- package/dist/esm/tools/annotation/CircleROITool.js +6 -2
- package/dist/esm/tools/annotation/CobbAngleTool.js +5 -2
- package/dist/esm/tools/annotation/EllipticalROITool.js +6 -2
- package/dist/esm/tools/annotation/LengthTool.js +5 -2
- package/dist/esm/tools/annotation/PlanarFreehandROITool.js +2 -2
- package/dist/esm/tools/annotation/RectangleROITool.js +5 -2
- package/dist/esm/tools/annotation/RegionSegmentPlusTool.js +8 -3
- package/dist/esm/tools/annotation/RegionSegmentTool.js +1 -0
- package/dist/esm/tools/annotation/WholeBodySegmentTool.js +1 -0
- package/dist/esm/tools/annotation/planarFreehandROITool/drawLoop.js +6 -2
- package/dist/esm/tools/base/AnnotationTool.d.ts +2 -15
- package/dist/esm/tools/base/GrowCutBaseTool.d.ts +2 -0
- package/dist/esm/tools/base/GrowCutBaseTool.js +77 -5
- package/dist/esm/types/AnnotationTypes.d.ts +18 -16
- package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +1 -0
- package/dist/esm/utilities/index.d.ts +2 -1
- package/dist/esm/utilities/index.js +2 -1
- package/dist/esm/utilities/segmentation/growCut/growCutShader.d.ts +1 -1
- package/dist/esm/utilities/segmentation/growCut/growCutShader.js +43 -2
- package/dist/esm/utilities/segmentation/growCut/runGrowCut.js +52 -5
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.d.ts +7 -1
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.js +62 -130
- package/dist/esm/utilities/setAnnotationLabel.d.ts +2 -0
- package/dist/esm/utilities/setAnnotationLabel.js +6 -0
- package/package.json +3 -3
|
@@ -6,6 +6,7 @@ declare enum ChangeTypes {
|
|
|
6
6
|
Completed = "Completed",
|
|
7
7
|
InterpolationUpdated = "InterpolationUpdated",
|
|
8
8
|
History = "History",
|
|
9
|
-
MetadataReferenceModified = "MetadataReferenceModified"
|
|
9
|
+
MetadataReferenceModified = "MetadataReferenceModified",
|
|
10
|
+
LabelChange = "LabelChange"
|
|
10
11
|
}
|
|
11
12
|
export default ChangeTypes;
|
|
@@ -8,5 +8,6 @@ var ChangeTypes;
|
|
|
8
8
|
ChangeTypes["InterpolationUpdated"] = "InterpolationUpdated";
|
|
9
9
|
ChangeTypes["History"] = "History";
|
|
10
10
|
ChangeTypes["MetadataReferenceModified"] = "MetadataReferenceModified";
|
|
11
|
+
ChangeTypes["LabelChange"] = "LabelChange";
|
|
11
12
|
})(ChangeTypes || (ChangeTypes = {}));
|
|
12
13
|
export default ChangeTypes;
|
|
@@ -2,7 +2,7 @@ import { getEnabledElement } from '@cornerstonejs/core';
|
|
|
2
2
|
import { BaseTool } from './base';
|
|
3
3
|
import { getAnnotations } from '../stateManagement';
|
|
4
4
|
import { point } from '../utilities/math';
|
|
5
|
-
import { Events, ToolModes, AnnotationStyleStates } from '../enums';
|
|
5
|
+
import { Events, ToolModes, AnnotationStyleStates, ChangeTypes, } from '../enums';
|
|
6
6
|
import { triggerAnnotationRenderForViewportIds } from '../utilities/triggerAnnotationRenderForViewportIds';
|
|
7
7
|
import { hideElementCursor, resetElementCursor, } from '../cursors/elementCursor';
|
|
8
8
|
import { getStyleProperty } from '../stateManagement/annotation/config/helpers';
|
|
@@ -70,7 +70,7 @@ class SculptorTool extends BaseTool {
|
|
|
70
70
|
if (toolInstance.configuration.calculateStats) {
|
|
71
71
|
activeAnnotation.invalidated = true;
|
|
72
72
|
}
|
|
73
|
-
triggerAnnotationModified(activeAnnotation, element);
|
|
73
|
+
triggerAnnotationModified(activeAnnotation, element, ChangeTypes.HandlesUpdated);
|
|
74
74
|
};
|
|
75
75
|
this.dragCallback = (evt) => {
|
|
76
76
|
const eventData = evt.detail;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Events } from '../../enums';
|
|
1
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
2
2
|
import { getEnabledElement, utilities as csUtils, getEnabledElementByViewportId, } from '@cornerstonejs/core';
|
|
3
3
|
import { AnnotationTool } from '../base';
|
|
4
4
|
import throttle from '../../utilities/throttle';
|
|
@@ -210,6 +210,9 @@ class AngleTool extends AnnotationTool {
|
|
|
210
210
|
const enabledElement = getEnabledElement(element);
|
|
211
211
|
const { renderingEngine } = enabledElement;
|
|
212
212
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
213
|
+
if (annotation.invalidated) {
|
|
214
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
215
|
+
}
|
|
213
216
|
};
|
|
214
217
|
this.cancel = (element) => {
|
|
215
218
|
if (this.isDrawing) {
|
|
@@ -465,7 +468,7 @@ class AngleTool extends AnnotationTool {
|
|
|
465
468
|
};
|
|
466
469
|
}
|
|
467
470
|
annotation.invalidated = false;
|
|
468
|
-
triggerAnnotationModified(annotation, element);
|
|
471
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
469
472
|
return cachedStats;
|
|
470
473
|
}
|
|
471
474
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Events } from '../../enums';
|
|
1
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
2
2
|
import { getEnabledElement, utilities as csUtils, getEnabledElementByViewportId, } from '@cornerstonejs/core';
|
|
3
3
|
import { AnnotationTool } from '../base';
|
|
4
4
|
import { addAnnotation, getAnnotations, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
|
|
@@ -11,6 +11,7 @@ import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnota
|
|
|
11
11
|
import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
|
|
12
12
|
import { resetElementCursor, hideElementCursor, } from '../../cursors/elementCursor';
|
|
13
13
|
import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
|
|
14
|
+
import { setAnnotationLabel } from '../../utilities';
|
|
14
15
|
class ArrowAnnotateTool extends AnnotationTool {
|
|
15
16
|
static { this.toolName = 'ArrowAnnotate'; }
|
|
16
17
|
constructor(toolProps = {}, defaultToolProps = {
|
|
@@ -127,7 +128,7 @@ class ArrowAnnotateTool extends AnnotationTool {
|
|
|
127
128
|
this._endCallback = (evt) => {
|
|
128
129
|
const eventDetail = evt.detail;
|
|
129
130
|
const { element } = eventDetail;
|
|
130
|
-
const { annotation, viewportIdsToRender, newAnnotation, hasMoved } = this.editData;
|
|
131
|
+
const { annotation, viewportIdsToRender, newAnnotation, hasMoved, movingTextBox, } = this.editData;
|
|
131
132
|
const { data } = annotation;
|
|
132
133
|
if (newAnnotation && !hasMoved) {
|
|
133
134
|
return;
|
|
@@ -152,11 +153,12 @@ class ArrowAnnotateTool extends AnnotationTool {
|
|
|
152
153
|
annotation.data.text = text;
|
|
153
154
|
triggerAnnotationCompleted(annotation);
|
|
154
155
|
this.createMemo(element, annotation, { newAnnotation: !!this.memo });
|
|
156
|
+
setAnnotationLabel(annotation, element, text);
|
|
155
157
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
156
158
|
});
|
|
157
159
|
}
|
|
158
|
-
else {
|
|
159
|
-
triggerAnnotationModified(annotation, element);
|
|
160
|
+
else if (!movingTextBox) {
|
|
161
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
160
162
|
}
|
|
161
163
|
this.doneEditMemo();
|
|
162
164
|
this.editData = null;
|
|
@@ -9,7 +9,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
|
|
|
9
9
|
import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
|
|
10
10
|
import { drawLine as drawLineSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
|
|
11
11
|
import { state } from '../../store/state';
|
|
12
|
-
import { Events } from '../../enums';
|
|
12
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
13
13
|
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
|
|
14
14
|
import * as lineSegment from '../../utilities/math/line';
|
|
15
15
|
import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
|
|
@@ -219,6 +219,7 @@ class BidirectionalTool extends AnnotationTool {
|
|
|
219
219
|
data.handles.points[3] = viewport.canvasToWorld([endX, endY]);
|
|
220
220
|
annotation.invalidated = true;
|
|
221
221
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
222
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
222
223
|
this.editData.hasMoved = true;
|
|
223
224
|
};
|
|
224
225
|
this._dragModifyCallback = (evt) => {
|
|
@@ -254,6 +255,9 @@ class BidirectionalTool extends AnnotationTool {
|
|
|
254
255
|
annotation.invalidated = true;
|
|
255
256
|
}
|
|
256
257
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
258
|
+
if (annotation.invalidated) {
|
|
259
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
260
|
+
}
|
|
257
261
|
};
|
|
258
262
|
this._dragModifyHandle = (evt) => {
|
|
259
263
|
const eventDetail = evt.detail;
|
|
@@ -629,7 +633,7 @@ class BidirectionalTool extends AnnotationTool {
|
|
|
629
633
|
};
|
|
630
634
|
}
|
|
631
635
|
annotation.invalidated = false;
|
|
632
|
-
triggerAnnotationModified(annotation, element);
|
|
636
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
633
637
|
return cachedStats;
|
|
634
638
|
};
|
|
635
639
|
this._isInsideVolume = (index1, index2, index3, index4, dimensions) => {
|
|
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
|
|
|
8
8
|
import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
|
|
9
9
|
import { drawCircle as drawCircleSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
|
|
10
10
|
import { state } from '../../store/state';
|
|
11
|
-
import { Events } from '../../enums';
|
|
11
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
12
12
|
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
|
|
13
13
|
import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
|
|
14
14
|
import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
|
|
@@ -196,6 +196,7 @@ class CircleROITool extends AnnotationTool {
|
|
|
196
196
|
annotation.invalidated = true;
|
|
197
197
|
this.editData.hasMoved = true;
|
|
198
198
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
199
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
199
200
|
};
|
|
200
201
|
this._dragModifyCallback = (evt) => {
|
|
201
202
|
this.isDrawing = true;
|
|
@@ -232,6 +233,9 @@ class CircleROITool extends AnnotationTool {
|
|
|
232
233
|
const enabledElement = getEnabledElement(element);
|
|
233
234
|
const { renderingEngine } = enabledElement;
|
|
234
235
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
236
|
+
if (annotation.invalidated) {
|
|
237
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
238
|
+
}
|
|
235
239
|
};
|
|
236
240
|
this._dragHandle = (evt) => {
|
|
237
241
|
const eventDetail = evt.detail;
|
|
@@ -552,7 +556,7 @@ class CircleROITool extends AnnotationTool {
|
|
|
552
556
|
}
|
|
553
557
|
}
|
|
554
558
|
annotation.invalidated = false;
|
|
555
|
-
triggerAnnotationModified(annotation, element);
|
|
559
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
556
560
|
return cachedStats;
|
|
557
561
|
};
|
|
558
562
|
this._isInsideVolume = (index1, index2, dimensions) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { vec3 } from 'gl-matrix';
|
|
2
|
-
import { Events } from '../../enums';
|
|
2
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
3
3
|
import { getEnabledElement } from '@cornerstonejs/core';
|
|
4
4
|
import { AnnotationTool } from '../base';
|
|
5
5
|
import throttle from '../../utilities/throttle';
|
|
@@ -237,6 +237,9 @@ class CobbAngleTool extends AnnotationTool {
|
|
|
237
237
|
const enabledElement = getEnabledElement(element);
|
|
238
238
|
const { renderingEngine } = enabledElement;
|
|
239
239
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
240
|
+
if (annotation.invalidated) {
|
|
241
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
242
|
+
}
|
|
240
243
|
};
|
|
241
244
|
this.cancel = (element) => {
|
|
242
245
|
if (!this.isDrawing) {
|
|
@@ -672,7 +675,7 @@ class CobbAngleTool extends AnnotationTool {
|
|
|
672
675
|
};
|
|
673
676
|
}
|
|
674
677
|
annotation.invalidated = false;
|
|
675
|
-
triggerAnnotationModified(annotation, element);
|
|
678
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
676
679
|
return cachedStats;
|
|
677
680
|
}
|
|
678
681
|
}
|
|
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
|
|
|
8
8
|
import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
|
|
9
9
|
import { drawCircle as drawCircleSvg, drawEllipseByCoordinates as drawEllipseSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
|
|
10
10
|
import { state } from '../../store/state';
|
|
11
|
-
import { Events } from '../../enums';
|
|
11
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
12
12
|
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
|
|
13
13
|
import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
|
|
14
14
|
import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
|
|
@@ -240,6 +240,7 @@ class EllipticalROITool extends AnnotationTool {
|
|
|
240
240
|
annotation.invalidated = true;
|
|
241
241
|
this.editData.hasMoved = true;
|
|
242
242
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
243
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
243
244
|
};
|
|
244
245
|
this._dragModifyCallback = (evt) => {
|
|
245
246
|
this.isDrawing = true;
|
|
@@ -276,6 +277,9 @@ class EllipticalROITool extends AnnotationTool {
|
|
|
276
277
|
const enabledElement = getEnabledElement(element);
|
|
277
278
|
const { renderingEngine } = enabledElement;
|
|
278
279
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
280
|
+
if (annotation.invalidated) {
|
|
281
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
282
|
+
}
|
|
279
283
|
};
|
|
280
284
|
this._dragHandle = (evt) => {
|
|
281
285
|
const eventDetail = evt.detail;
|
|
@@ -618,7 +622,7 @@ class EllipticalROITool extends AnnotationTool {
|
|
|
618
622
|
};
|
|
619
623
|
}
|
|
620
624
|
annotation.invalidated = false;
|
|
621
|
-
triggerAnnotationModified(annotation, element);
|
|
625
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
622
626
|
return cachedStats;
|
|
623
627
|
};
|
|
624
628
|
this._isInsideVolume = (index1, index2, dimensions) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Events } from '../../enums';
|
|
1
|
+
import { Events, ChangeTypes } from '../../enums';
|
|
2
2
|
import { getEnabledElement, utilities as csUtils, utilities, getEnabledElementByViewportId, } from '@cornerstonejs/core';
|
|
3
3
|
import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits';
|
|
4
4
|
import { AnnotationTool } from '../base';
|
|
@@ -187,6 +187,9 @@ class LengthTool extends AnnotationTool {
|
|
|
187
187
|
}
|
|
188
188
|
this.editData.hasMoved = true;
|
|
189
189
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
190
|
+
if (annotation.invalidated) {
|
|
191
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
192
|
+
}
|
|
190
193
|
};
|
|
191
194
|
this.cancel = (element) => {
|
|
192
195
|
if (this.isDrawing) {
|
|
@@ -448,7 +451,7 @@ class LengthTool extends AnnotationTool {
|
|
|
448
451
|
};
|
|
449
452
|
}
|
|
450
453
|
annotation.invalidated = false;
|
|
451
|
-
triggerAnnotationModified(annotation, element);
|
|
454
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
452
455
|
return cachedStats;
|
|
453
456
|
}
|
|
454
457
|
_isInsideVolume(index1, index2, dimensions) {
|
|
@@ -365,8 +365,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
|
|
|
365
365
|
}
|
|
366
366
|
if (!this.commonData?.movingTextBox) {
|
|
367
367
|
const { data } = annotation;
|
|
368
|
-
if (!data.cachedStats[targetId]
|
|
369
|
-
data.cachedStats[targetId].areaUnit == null) {
|
|
368
|
+
if (!data.cachedStats[targetId]?.unit) {
|
|
370
369
|
data.cachedStats[targetId] = {
|
|
371
370
|
Modality: null,
|
|
372
371
|
area: null,
|
|
@@ -374,6 +373,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
|
|
|
374
373
|
mean: null,
|
|
375
374
|
stdDev: null,
|
|
376
375
|
areaUnit: null,
|
|
376
|
+
unit: null,
|
|
377
377
|
};
|
|
378
378
|
this._calculateCachedStats(annotation, viewport, renderingEngine, enabledElement);
|
|
379
379
|
}
|
|
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
|
|
|
8
8
|
import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
|
|
9
9
|
import { drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, drawRectByCoordinates as drawRectSvg, } from '../../drawingSvg';
|
|
10
10
|
import { state } from '../../store/state';
|
|
11
|
-
import { Events } from '../../enums';
|
|
11
|
+
import { ChangeTypes, Events } from '../../enums';
|
|
12
12
|
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
|
|
13
13
|
import * as rectangle from '../../utilities/math/rectangle';
|
|
14
14
|
import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
|
|
@@ -242,6 +242,9 @@ class RectangleROITool extends AnnotationTool {
|
|
|
242
242
|
this.editData.hasMoved = true;
|
|
243
243
|
const enabledElement = getEnabledElement(element);
|
|
244
244
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
245
|
+
if (annotation.invalidated) {
|
|
246
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
247
|
+
}
|
|
245
248
|
};
|
|
246
249
|
this.cancel = (element) => {
|
|
247
250
|
if (this.isDrawing) {
|
|
@@ -504,7 +507,7 @@ class RectangleROITool extends AnnotationTool {
|
|
|
504
507
|
}
|
|
505
508
|
}
|
|
506
509
|
annotation.invalidated = false;
|
|
507
|
-
triggerAnnotationModified(annotation, element);
|
|
510
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
|
|
508
511
|
return cachedStats;
|
|
509
512
|
};
|
|
510
513
|
this._isInsideVolume = (index1, index2, dimensions) => {
|
|
@@ -6,11 +6,12 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
6
6
|
constructor(toolProps = {}, defaultToolProps = {
|
|
7
7
|
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
8
8
|
configuration: {
|
|
9
|
+
isPartialVolume: false,
|
|
9
10
|
positiveSeedVariance: 0.4,
|
|
10
11
|
negativeSeedVariance: 0.9,
|
|
11
12
|
subVolumePaddingPercentage: 0.1,
|
|
12
13
|
islandRemoval: {
|
|
13
|
-
enabled:
|
|
14
|
+
enabled: false,
|
|
14
15
|
},
|
|
15
16
|
},
|
|
16
17
|
}) {
|
|
@@ -41,7 +42,7 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
async getGrowCutLabelmap(growCutData) {
|
|
44
|
-
const { segmentation: { referencedVolumeId }, renderingEngineId, viewportId, worldPoint, options, } = growCutData;
|
|
45
|
+
const { segmentation: { referencedVolumeId, labelmapVolumeId }, renderingEngineId, viewportId, worldPoint, options, } = growCutData;
|
|
45
46
|
const renderingEngine = getRenderingEngine(renderingEngineId);
|
|
46
47
|
const viewport = renderingEngine.getViewport(viewportId);
|
|
47
48
|
const { subVolumePaddingPercentage } = this.configuration;
|
|
@@ -49,7 +50,11 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
49
50
|
...options,
|
|
50
51
|
subVolumePaddingPercentage,
|
|
51
52
|
};
|
|
52
|
-
return growCut.runOneClickGrowCut(
|
|
53
|
+
return growCut.runOneClickGrowCut({
|
|
54
|
+
referencedVolumeId,
|
|
55
|
+
worldPosition: worldPoint,
|
|
56
|
+
options: mergedOptions,
|
|
57
|
+
});
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
export default RegionSegmentPlusTool;
|
|
@@ -11,6 +11,7 @@ class RegionSegmentTool extends GrowCutBaseTool {
|
|
|
11
11
|
constructor(toolProps = {}, defaultToolProps = {
|
|
12
12
|
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
13
13
|
configuration: {
|
|
14
|
+
isPartialVolume: true,
|
|
14
15
|
positiveSeedVariance: 0.5,
|
|
15
16
|
negativeSeedVariance: 0.9,
|
|
16
17
|
},
|
|
@@ -14,6 +14,7 @@ class WholeBodySegmentTool extends GrowCutBaseTool {
|
|
|
14
14
|
constructor(toolProps = {}, defaultToolProps = {
|
|
15
15
|
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
16
16
|
configuration: {
|
|
17
|
+
isPartialVolume: true,
|
|
17
18
|
positivePixelRange: POSITIVE_PIXEL_RANGE,
|
|
18
19
|
negativePixelRange: NEGATIVE_PIXEL_RANGE,
|
|
19
20
|
islandRemoval: {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getEnabledElement, utilities } from '@cornerstonejs/core';
|
|
2
2
|
import { resetElementCursor, hideElementCursor, } from '../../../cursors/elementCursor';
|
|
3
|
-
import { Events } from '../../../enums';
|
|
3
|
+
import { ChangeTypes, Events } from '../../../enums';
|
|
4
4
|
import { state } from '../../../store/state';
|
|
5
5
|
import { vec3 } from 'gl-matrix';
|
|
6
6
|
import { shouldSmooth, getInterpolatedPoints, } from '../../../utilities/planarFreehandROITool/smoothPoints';
|
|
7
7
|
import getMouseModifierKey from '../../../eventDispatchers/shared/getMouseModifier';
|
|
8
8
|
import triggerAnnotationRenderForViewportIds from '../../../utilities/triggerAnnotationRenderForViewportIds';
|
|
9
|
-
import { triggerContourAnnotationCompleted } from '../../../stateManagement/annotation/helpers/state';
|
|
9
|
+
import { triggerAnnotationModified, triggerContourAnnotationCompleted, } from '../../../stateManagement/annotation/helpers/state';
|
|
10
10
|
import findOpenUShapedContourVectorToPeak from './findOpenUShapedContourVectorToPeak';
|
|
11
11
|
import { polyline } from '../../../utilities/math';
|
|
12
12
|
import { removeAnnotation } from '../../../stateManagement/annotation/annotationState';
|
|
@@ -97,8 +97,12 @@ function mouseDragDrawCallback(evt) {
|
|
|
97
97
|
const numPointsAdded = addCanvasPointsToArray(element, canvasPoints, canvasPos, this.commonData);
|
|
98
98
|
this.drawData.polylineIndex = polylineIndex + numPointsAdded;
|
|
99
99
|
}
|
|
100
|
+
annotation.invalidated = true;
|
|
100
101
|
}
|
|
101
102
|
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
103
|
+
if (annotation.invalidated) {
|
|
104
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
|
|
105
|
+
}
|
|
102
106
|
}
|
|
103
107
|
function mouseUpDrawCallback(evt) {
|
|
104
108
|
const { allowOpenContours } = this.configuration;
|
|
@@ -41,22 +41,9 @@ declare abstract class AnnotationTool extends AnnotationDisplayTool {
|
|
|
41
41
|
annotationUID: string;
|
|
42
42
|
data: {
|
|
43
43
|
[key: string]: unknown;
|
|
44
|
-
handles?:
|
|
45
|
-
points?: Types.Point3[];
|
|
46
|
-
activeHandleIndex?: number | null;
|
|
47
|
-
textBox?: {
|
|
48
|
-
hasMoved?: boolean;
|
|
49
|
-
worldPosition?: Types.Point3;
|
|
50
|
-
worldBoundingBox?: {
|
|
51
|
-
topLeft: Types.Point3;
|
|
52
|
-
topRight: Types.Point3;
|
|
53
|
-
bottomLeft: Types.Point3;
|
|
54
|
-
bottomRight: Types.Point3;
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
[key: string]: unknown;
|
|
58
|
-
};
|
|
44
|
+
handles?: import("../../types/AnnotationTypes").Handles;
|
|
59
45
|
cachedStats?: Record<string, unknown>;
|
|
46
|
+
label?: string;
|
|
60
47
|
};
|
|
61
48
|
deleting: boolean;
|
|
62
49
|
};
|
|
@@ -33,6 +33,7 @@ declare class GrowCutBaseTool extends BaseTool {
|
|
|
33
33
|
refresh(): void;
|
|
34
34
|
protected getGrowCutLabelmap(_growCutData: GrowCutToolData): Promise<Types.IImageVolume>;
|
|
35
35
|
protected runGrowCut(): Promise<void>;
|
|
36
|
+
protected applyPartialGrowCutLabelmap(segmentationId: string, segmentIndex: number, targetLabelmap: Types.IImageVolume, sourceLabelmap: Types.IImageVolume): void;
|
|
36
37
|
protected applyGrowCutLabelmap(segmentationId: string, segmentIndex: number, targetLabelmap: Types.IImageVolume, sourceLabelmap: Types.IImageVolume): void;
|
|
37
38
|
private _runLastCommand;
|
|
38
39
|
protected getLabelmapSegmentationData(viewport: Types.IViewport): Promise<{
|
|
@@ -41,6 +42,7 @@ declare class GrowCutBaseTool extends BaseTool {
|
|
|
41
42
|
labelmapVolumeId: string;
|
|
42
43
|
referencedVolumeId: string;
|
|
43
44
|
}>;
|
|
45
|
+
private _createFakeVolume;
|
|
44
46
|
protected _isOrthogonalView(viewport: Types.IViewport, referencedVolumeId: string): boolean;
|
|
45
47
|
protected getRemoveIslandData(_growCutData: GrowCutToolData): RemoveIslandData;
|
|
46
48
|
private _removeIslands;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { getEnabledElement, utilities as csUtils, cache, getRenderingEngine,
|
|
1
|
+
import { getEnabledElement, utilities as csUtils, cache, getRenderingEngine, volumeLoader, imageLoader, ImageVolume, } from '@cornerstonejs/core';
|
|
2
2
|
import { BaseTool } from '../base';
|
|
3
3
|
import { SegmentationRepresentations } from '../../enums';
|
|
4
4
|
import { segmentIndex as segmentIndexController, state as segmentationState, activeSegmentation, } from '../../stateManagement/segmentation';
|
|
5
5
|
import { triggerSegmentationDataModified } from '../../stateManagement/segmentation/triggerSegmentationEvents';
|
|
6
6
|
import { getSVGStyleForSegment } from '../../utilities/segmentation/getSVGStyleForSegment';
|
|
7
7
|
import IslandRemoval from '../../utilities/segmentation/islandRemoval';
|
|
8
|
+
import { getOrCreateSegmentationVolume } from '../../utilities/segmentation';
|
|
9
|
+
import { getCurrentLabelmapImageIdForViewport } from '../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport';
|
|
8
10
|
const { transformWorldToIndex, transformIndexToWorld } = csUtils;
|
|
9
11
|
class GrowCutBaseTool extends BaseTool {
|
|
10
12
|
static { this.lastGrowCutCommand = null; }
|
|
@@ -90,7 +92,11 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
90
92
|
},
|
|
91
93
|
});
|
|
92
94
|
const growcutLabelmap = await this.getGrowCutLabelmap(updatedGrowCutData);
|
|
93
|
-
|
|
95
|
+
const { isPartialVolume } = config;
|
|
96
|
+
const fn = isPartialVolume
|
|
97
|
+
? this.applyPartialGrowCutLabelmap
|
|
98
|
+
: this.applyGrowCutLabelmap;
|
|
99
|
+
fn(segmentationId, segmentIndex, labelmap, growcutLabelmap);
|
|
94
100
|
this._removeIslands(growCutData);
|
|
95
101
|
};
|
|
96
102
|
await growCutCommand();
|
|
@@ -99,7 +105,7 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
99
105
|
}
|
|
100
106
|
this.growCutData = null;
|
|
101
107
|
}
|
|
102
|
-
|
|
108
|
+
applyPartialGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
|
|
103
109
|
const srcLabelmapData = sourceLabelmap.voxelManager.getCompleteScalarDataArray();
|
|
104
110
|
const tgtVoxelManager = targetLabelmap.voxelManager;
|
|
105
111
|
const [srcColumns, srcRows, srcNumSlices] = sourceLabelmap.dimensions;
|
|
@@ -124,6 +130,16 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
124
130
|
}
|
|
125
131
|
triggerSegmentationDataModified(segmentationId);
|
|
126
132
|
}
|
|
133
|
+
applyGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
|
|
134
|
+
const tgtVoxelManager = targetLabelmap.voxelManager;
|
|
135
|
+
const srcVoxelManager = sourceLabelmap.voxelManager;
|
|
136
|
+
srcVoxelManager.forEach(({ value, index }) => {
|
|
137
|
+
if (value === segmentIndex) {
|
|
138
|
+
tgtVoxelManager.setAtIndex(index, value);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
triggerSegmentationDataModified(segmentationId);
|
|
142
|
+
}
|
|
127
143
|
_runLastCommand({ shrinkExpandAmount = 0 } = {}) {
|
|
128
144
|
const cmd = GrowCutBaseTool.lastGrowCutCommand;
|
|
129
145
|
if (cmd) {
|
|
@@ -139,9 +155,39 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
139
155
|
const segmentIndex = segmentIndexController.getActiveSegmentIndex(segmentationId);
|
|
140
156
|
const { representationData } = segmentationState.getSegmentation(segmentationId);
|
|
141
157
|
const labelmapData = representationData[SegmentationRepresentations.Labelmap];
|
|
142
|
-
|
|
158
|
+
let { volumeId: labelmapVolumeId, referencedVolumeId } = labelmapData;
|
|
143
159
|
if (!labelmapVolumeId) {
|
|
144
|
-
|
|
160
|
+
const referencedImageIds = viewport.getImageIds();
|
|
161
|
+
if (!csUtils.isValidVolume(referencedImageIds)) {
|
|
162
|
+
const currentImageId = viewport.getCurrentImageId();
|
|
163
|
+
const currentImage = cache.getImage(currentImageId);
|
|
164
|
+
const fakeImage = imageLoader.createAndCacheDerivedImage(currentImageId);
|
|
165
|
+
const fakeVolume = this._createFakeVolume([
|
|
166
|
+
currentImage.imageId,
|
|
167
|
+
fakeImage.imageId,
|
|
168
|
+
]);
|
|
169
|
+
referencedVolumeId = fakeVolume.volumeId;
|
|
170
|
+
const currentLabelmapImageId = getCurrentLabelmapImageIdForViewport(viewport.id, segmentationId);
|
|
171
|
+
const fakeDerivedImage = imageLoader.createAndCacheDerivedImage(currentLabelmapImageId);
|
|
172
|
+
const fakeLabelmapVolume = this._createFakeVolume([
|
|
173
|
+
currentLabelmapImageId,
|
|
174
|
+
fakeDerivedImage.imageId,
|
|
175
|
+
]);
|
|
176
|
+
labelmapVolumeId = fakeLabelmapVolume.volumeId;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const segVolume = getOrCreateSegmentationVolume(segmentationId);
|
|
180
|
+
labelmapVolumeId = segVolume.volumeId;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!referencedVolumeId) {
|
|
184
|
+
const { imageIds: segImageIds } = labelmapData;
|
|
185
|
+
const referencedImageIds = segImageIds.map((imageId) => cache.getImage(imageId).referencedImageId);
|
|
186
|
+
const volumeId = cache.generateVolumeId(referencedImageIds);
|
|
187
|
+
const imageVolume = cache.getVolume(volumeId);
|
|
188
|
+
referencedVolumeId = imageVolume
|
|
189
|
+
? imageVolume.volumeId
|
|
190
|
+
: (await volumeLoader.createAndCacheVolumeFromImagesSync(volumeId, referencedImageIds)).volumeId;
|
|
145
191
|
}
|
|
146
192
|
return {
|
|
147
193
|
segmentationId,
|
|
@@ -150,6 +196,32 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
150
196
|
referencedVolumeId,
|
|
151
197
|
};
|
|
152
198
|
}
|
|
199
|
+
_createFakeVolume(imageIds) {
|
|
200
|
+
const volumeId = cache.generateVolumeId(imageIds);
|
|
201
|
+
const cachedVolume = cache.getVolume(volumeId);
|
|
202
|
+
if (cachedVolume) {
|
|
203
|
+
return cachedVolume;
|
|
204
|
+
}
|
|
205
|
+
const volumeProps = csUtils.generateVolumePropsFromImageIds(imageIds, volumeId);
|
|
206
|
+
const spacing = volumeProps.spacing;
|
|
207
|
+
if (spacing[2] === 0) {
|
|
208
|
+
spacing[2] = 1;
|
|
209
|
+
}
|
|
210
|
+
const derivedVolume = new ImageVolume({
|
|
211
|
+
volumeId,
|
|
212
|
+
dataType: volumeProps.dataType,
|
|
213
|
+
metadata: structuredClone(volumeProps.metadata),
|
|
214
|
+
dimensions: volumeProps.dimensions,
|
|
215
|
+
spacing: volumeProps.spacing,
|
|
216
|
+
origin: volumeProps.origin,
|
|
217
|
+
direction: volumeProps.direction,
|
|
218
|
+
referencedVolumeId: volumeProps.referencedVolumeId,
|
|
219
|
+
imageIds: volumeProps.imageIds,
|
|
220
|
+
referencedImageIds: volumeProps.referencedImageIds,
|
|
221
|
+
});
|
|
222
|
+
cache.putVolumeSync(volumeId, derivedVolume);
|
|
223
|
+
return derivedVolume;
|
|
224
|
+
}
|
|
153
225
|
_isOrthogonalView(viewport, referencedVolumeId) {
|
|
154
226
|
const volume = cache.getVolume(referencedVolumeId);
|
|
155
227
|
const volumeImageData = volume.imageData;
|
|
@@ -16,23 +16,10 @@ type Annotation = {
|
|
|
16
16
|
viewUp?: Types.Point3;
|
|
17
17
|
};
|
|
18
18
|
data: {
|
|
19
|
-
handles?:
|
|
20
|
-
points?: Types.Point3[];
|
|
21
|
-
activeHandleIndex?: number | null;
|
|
22
|
-
textBox?: {
|
|
23
|
-
hasMoved?: boolean;
|
|
24
|
-
worldPosition?: Types.Point3;
|
|
25
|
-
worldBoundingBox?: {
|
|
26
|
-
topLeft: Types.Point3;
|
|
27
|
-
topRight: Types.Point3;
|
|
28
|
-
bottomLeft: Types.Point3;
|
|
29
|
-
bottomRight: Types.Point3;
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
[key: string]: unknown;
|
|
33
|
-
};
|
|
19
|
+
handles?: Handles;
|
|
34
20
|
[key: string]: unknown;
|
|
35
21
|
cachedStats?: Record<string, unknown>;
|
|
22
|
+
label?: string;
|
|
36
23
|
};
|
|
37
24
|
};
|
|
38
25
|
type Annotations = Array<Annotation>;
|
|
@@ -42,4 +29,19 @@ type GroupSpecificAnnotations = {
|
|
|
42
29
|
type AnnotationState = {
|
|
43
30
|
[key: string]: GroupSpecificAnnotations;
|
|
44
31
|
};
|
|
45
|
-
|
|
32
|
+
type Handles = {
|
|
33
|
+
points?: Types.Point3[];
|
|
34
|
+
activeHandleIndex?: number | null;
|
|
35
|
+
textBox?: {
|
|
36
|
+
hasMoved?: boolean;
|
|
37
|
+
worldPosition?: Types.Point3;
|
|
38
|
+
worldBoundingBox?: {
|
|
39
|
+
topLeft: Types.Point3;
|
|
40
|
+
topRight: Types.Point3;
|
|
41
|
+
bottomLeft: Types.Point3;
|
|
42
|
+
bottomRight: Types.Point3;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
export type { Annotation, Annotations, GroupSpecificAnnotations, AnnotationState, Handles, };
|
|
@@ -37,4 +37,5 @@ import normalizeViewportPlane from './normalizeViewportPlane';
|
|
|
37
37
|
import IslandRemoval from './segmentation/islandRemoval';
|
|
38
38
|
import { getPixelValueUnits, getPixelValueUnitsImageId } from './getPixelValueUnits';
|
|
39
39
|
import * as geometricSurfaceUtils from './geometricSurfaceUtils';
|
|
40
|
-
|
|
40
|
+
import setAnnotationLabel from './setAnnotationLabel';
|
|
41
|
+
export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, };
|
|
@@ -37,4 +37,5 @@ import normalizeViewportPlane from './normalizeViewportPlane';
|
|
|
37
37
|
import IslandRemoval from './segmentation/islandRemoval';
|
|
38
38
|
import { getPixelValueUnits, getPixelValueUnitsImageId, } from './getPixelValueUnits';
|
|
39
39
|
import * as geometricSurfaceUtils from './geometricSurfaceUtils';
|
|
40
|
-
|
|
40
|
+
import setAnnotationLabel from './setAnnotationLabel';
|
|
41
|
+
export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const shader = "\nconst MAX_STRENGTH = 65535f;\n\n// Workgroup
|
|
1
|
+
declare const shader = "\nconst MAX_STRENGTH = 65535f;\n\n// Workgroup size - X*Y*Z must be multiple of 32 for better performance\noverride workGroupSizeX = 1u;\noverride workGroupSizeY = 1u;\noverride workGroupSizeZ = 1u;\n\n// Compare the current voxel to neighbors using a 9x9x9 window\noverride windowSize = 9i;\n\nstruct Params {\n size: vec3u,\n iteration: u32,\n}\n\n// New structure to track bounds of modified voxels\nstruct Bounds {\n minX: atomic<i32>,\n minY: atomic<i32>,\n minZ: atomic<i32>,\n maxX: atomic<i32>,\n maxY: atomic<i32>,\n maxZ: atomic<i32>,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage> volumePixelData: array<f32>;\n@group(0) @binding(2) var<storage, read_write> labelmap: array<u32>;\n@group(0) @binding(3) var<storage, read_write> strengthData: array<f32>;\n@group(0) @binding(4) var<storage> prevLabelmap: array<u32>;\n@group(0) @binding(5) var<storage> prevStrengthData: array<f32>;\n@group(0) @binding(6) var<storage, read_write> updatedVoxelsCounter: array<atomic<u32>>;\n@group(0) @binding(7) var<storage, read_write> modifiedBounds: Bounds;\n\nfn getPixelIndex(ijkPos: vec3u) -> u32 {\n let numPixelsPerSlice = params.size.x * params.size.y;\n return ijkPos.x + ijkPos.y * params.size.x + ijkPos.z * numPixelsPerSlice;\n}\n\nfn updateBounds(position: vec3i) {\n // Atomically update min bounds (use min operation)\n let oldMinX = atomicMin(&modifiedBounds.minX, position.x);\n let oldMinY = atomicMin(&modifiedBounds.minY, position.y);\n let oldMinZ = atomicMin(&modifiedBounds.minZ, position.z);\n\n // Atomically update max bounds (use max operation)\n let oldMaxX = atomicMax(&modifiedBounds.maxX, position.x);\n let oldMaxY = atomicMax(&modifiedBounds.maxY, position.y);\n let oldMaxZ = atomicMax(&modifiedBounds.maxZ, position.z);\n}\n\n@compute @workgroup_size(workGroupSizeX, workGroupSizeY, workGroupSizeZ)\nfn main(\n @builtin(global_invocation_id) globalId: vec3u,\n) {\n // Make sure it will not get out of bounds for volume with sizes that\n // are not multiple of workGroupSize\n if (\n globalId.x >= params.size.x ||\n globalId.y >= params.size.y ||\n globalId.z >= params.size.z\n ) {\n return;\n }\n\n // Initialize bounds for the first iteration\n if (params.iteration == 0 && globalId.x == 0 && globalId.y == 0 && globalId.z == 0) {\n // Initialize to opposite extremes to ensure any update will improve the bounds\n atomicStore(&modifiedBounds.minX, i32(params.size.x));\n atomicStore(&modifiedBounds.minY, i32(params.size.y));\n atomicStore(&modifiedBounds.minZ, i32(params.size.z));\n atomicStore(&modifiedBounds.maxX, -1);\n atomicStore(&modifiedBounds.maxY, -1);\n atomicStore(&modifiedBounds.maxZ, -1);\n }\n\n let currentCoord = vec3i(globalId);\n let currentPixelIndex = getPixelIndex(globalId);\n\n let numPixels = arrayLength(&volumePixelData);\n let currentPixelValue = volumePixelData[currentPixelIndex];\n\n if (params.iteration == 0) {\n // All non-zero initial labels are given maximum strength\n strengthData[currentPixelIndex] = select(MAX_STRENGTH, 0., labelmap[currentPixelIndex] == 0);\n\n // Update bounds for non-zero initial labels\n if (labelmap[currentPixelIndex] != 0) {\n updateBounds(currentCoord);\n }\n return;\n }\n\n // It should at least copy the values from previous state\n var newLabel = prevLabelmap[currentPixelIndex];\n var newStrength = prevStrengthData[currentPixelIndex];\n\n let window = i32(ceil(f32(windowSize - 1) * .5));\n let minWindow = -1i * window;\n let maxWindow = 1i * window;\n\n for (var k = minWindow; k <= maxWindow; k++) {\n for (var j = minWindow; j <= maxWindow; j++) {\n for (var i = minWindow; i <= maxWindow; i++) {\n // Skip current voxel\n if (i == 0 && j == 0 && k == 0) {\n continue;\n }\n\n let neighborCoord = currentCoord + vec3i(i, j, k);\n\n // Boundary conditions. Do not grow outside of the volume\n if (\n neighborCoord.x < 0i || neighborCoord.x >= i32(params.size.x) ||\n neighborCoord.y < 0i || neighborCoord.y >= i32(params.size.y) ||\n neighborCoord.z < 0i || neighborCoord.z >= i32(params.size.z)\n ) {\n continue;\n }\n\n let neighborIndex = getPixelIndex(vec3u(neighborCoord));\n let neighborPixelValue = volumePixelData[neighborIndex];\n let prevNeighborStrength = prevStrengthData[neighborIndex];\n let strengthCost = abs(neighborPixelValue - currentPixelValue);\n let takeoverStrength = prevNeighborStrength - strengthCost;\n\n if (takeoverStrength > newStrength) {\n newLabel = prevLabelmap[neighborIndex];\n newStrength = takeoverStrength;\n }\n }\n }\n }\n\n if (labelmap[currentPixelIndex] != newLabel) {\n atomicAdd(&updatedVoxelsCounter[params.iteration], 1u);\n\n // Update bounds for modified voxels\n updateBounds(currentCoord);\n }\n\n labelmap[currentPixelIndex] = newLabel;\n strengthData[currentPixelIndex] = newStrength;\n}\n";
|
|
2
2
|
export default shader;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const shader = `
|
|
2
2
|
const MAX_STRENGTH = 65535f;
|
|
3
3
|
|
|
4
|
-
// Workgroup
|
|
5
|
-
// otherwise warps are sub allocated and some threads will not process anything
|
|
4
|
+
// Workgroup size - X*Y*Z must be multiple of 32 for better performance
|
|
6
5
|
override workGroupSizeX = 1u;
|
|
7
6
|
override workGroupSizeY = 1u;
|
|
8
7
|
override workGroupSizeZ = 1u;
|
|
@@ -15,6 +14,16 @@ struct Params {
|
|
|
15
14
|
iteration: u32,
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
// New structure to track bounds of modified voxels
|
|
18
|
+
struct Bounds {
|
|
19
|
+
minX: atomic<i32>,
|
|
20
|
+
minY: atomic<i32>,
|
|
21
|
+
minZ: atomic<i32>,
|
|
22
|
+
maxX: atomic<i32>,
|
|
23
|
+
maxY: atomic<i32>,
|
|
24
|
+
maxZ: atomic<i32>,
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
19
28
|
@group(0) @binding(1) var<storage> volumePixelData: array<f32>;
|
|
20
29
|
@group(0) @binding(2) var<storage, read_write> labelmap: array<u32>;
|
|
@@ -22,12 +31,25 @@ struct Params {
|
|
|
22
31
|
@group(0) @binding(4) var<storage> prevLabelmap: array<u32>;
|
|
23
32
|
@group(0) @binding(5) var<storage> prevStrengthData: array<f32>;
|
|
24
33
|
@group(0) @binding(6) var<storage, read_write> updatedVoxelsCounter: array<atomic<u32>>;
|
|
34
|
+
@group(0) @binding(7) var<storage, read_write> modifiedBounds: Bounds;
|
|
25
35
|
|
|
26
36
|
fn getPixelIndex(ijkPos: vec3u) -> u32 {
|
|
27
37
|
let numPixelsPerSlice = params.size.x * params.size.y;
|
|
28
38
|
return ijkPos.x + ijkPos.y * params.size.x + ijkPos.z * numPixelsPerSlice;
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
fn updateBounds(position: vec3i) {
|
|
42
|
+
// Atomically update min bounds (use min operation)
|
|
43
|
+
let oldMinX = atomicMin(&modifiedBounds.minX, position.x);
|
|
44
|
+
let oldMinY = atomicMin(&modifiedBounds.minY, position.y);
|
|
45
|
+
let oldMinZ = atomicMin(&modifiedBounds.minZ, position.z);
|
|
46
|
+
|
|
47
|
+
// Atomically update max bounds (use max operation)
|
|
48
|
+
let oldMaxX = atomicMax(&modifiedBounds.maxX, position.x);
|
|
49
|
+
let oldMaxY = atomicMax(&modifiedBounds.maxY, position.y);
|
|
50
|
+
let oldMaxZ = atomicMax(&modifiedBounds.maxZ, position.z);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
@compute @workgroup_size(workGroupSizeX, workGroupSizeY, workGroupSizeZ)
|
|
32
54
|
fn main(
|
|
33
55
|
@builtin(global_invocation_id) globalId: vec3u,
|
|
@@ -42,6 +64,17 @@ fn main(
|
|
|
42
64
|
return;
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
// Initialize bounds for the first iteration
|
|
68
|
+
if (params.iteration == 0 && globalId.x == 0 && globalId.y == 0 && globalId.z == 0) {
|
|
69
|
+
// Initialize to opposite extremes to ensure any update will improve the bounds
|
|
70
|
+
atomicStore(&modifiedBounds.minX, i32(params.size.x));
|
|
71
|
+
atomicStore(&modifiedBounds.minY, i32(params.size.y));
|
|
72
|
+
atomicStore(&modifiedBounds.minZ, i32(params.size.z));
|
|
73
|
+
atomicStore(&modifiedBounds.maxX, -1);
|
|
74
|
+
atomicStore(&modifiedBounds.maxY, -1);
|
|
75
|
+
atomicStore(&modifiedBounds.maxZ, -1);
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
let currentCoord = vec3i(globalId);
|
|
46
79
|
let currentPixelIndex = getPixelIndex(globalId);
|
|
47
80
|
|
|
@@ -51,6 +84,11 @@ fn main(
|
|
|
51
84
|
if (params.iteration == 0) {
|
|
52
85
|
// All non-zero initial labels are given maximum strength
|
|
53
86
|
strengthData[currentPixelIndex] = select(MAX_STRENGTH, 0., labelmap[currentPixelIndex] == 0);
|
|
87
|
+
|
|
88
|
+
// Update bounds for non-zero initial labels
|
|
89
|
+
if (labelmap[currentPixelIndex] != 0) {
|
|
90
|
+
updateBounds(currentCoord);
|
|
91
|
+
}
|
|
54
92
|
return;
|
|
55
93
|
}
|
|
56
94
|
|
|
@@ -97,6 +135,9 @@ fn main(
|
|
|
97
135
|
|
|
98
136
|
if (labelmap[currentPixelIndex] != newLabel) {
|
|
99
137
|
atomicAdd(&updatedVoxelsCounter[params.iteration], 1u);
|
|
138
|
+
|
|
139
|
+
// Update bounds for modified voxels
|
|
140
|
+
updateBounds(currentCoord);
|
|
100
141
|
}
|
|
101
142
|
|
|
102
143
|
labelmap[currentPixelIndex] = newLabel;
|
|
@@ -23,7 +23,8 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
23
23
|
labelmap.dimensions[2] !== numSlices) {
|
|
24
24
|
throw new Error('Volume and labelmap must have the same size');
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
let numIterations = Math.floor(Math.sqrt(rows ** 2 + columns ** 2 + numSlices ** 2) / 2);
|
|
27
|
+
numIterations = Math.min(numIterations, 500);
|
|
27
28
|
const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
|
|
28
29
|
let volumePixelData = volume.voxelManager.getCompleteScalarDataArray();
|
|
29
30
|
if (!(volumePixelData instanceof Float32Array)) {
|
|
@@ -37,6 +38,7 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
37
38
|
const device = await adapter.requestDevice({ requiredLimits });
|
|
38
39
|
const BUFFER_SIZE = volumePixelData.byteLength;
|
|
39
40
|
const UPDATED_VOXELS_COUNTER_BUFFER_SIZE = numIterations * Uint32Array.BYTES_PER_ELEMENT;
|
|
41
|
+
const BOUNDS_BUFFER_SIZE = 6 * Int32Array.BYTES_PER_ELEMENT;
|
|
40
42
|
const shaderModule = device.createShaderModule({
|
|
41
43
|
code: shaderCode,
|
|
42
44
|
});
|
|
@@ -78,6 +80,21 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
78
80
|
GPUBufferUsage.COPY_SRC |
|
|
79
81
|
GPUBufferUsage.COPY_DST,
|
|
80
82
|
});
|
|
83
|
+
const gpuBoundsBuffer = device.createBuffer({
|
|
84
|
+
size: BOUNDS_BUFFER_SIZE,
|
|
85
|
+
usage: GPUBufferUsage.STORAGE |
|
|
86
|
+
GPUBufferUsage.COPY_SRC |
|
|
87
|
+
GPUBufferUsage.COPY_DST,
|
|
88
|
+
});
|
|
89
|
+
const initialBounds = new Int32Array([
|
|
90
|
+
columns,
|
|
91
|
+
rows,
|
|
92
|
+
numSlices,
|
|
93
|
+
-1,
|
|
94
|
+
-1,
|
|
95
|
+
-1,
|
|
96
|
+
]);
|
|
97
|
+
device.queue.writeBuffer(gpuBoundsBuffer, 0, initialBounds);
|
|
81
98
|
const bindGroupLayout = device.createBindGroupLayout({
|
|
82
99
|
entries: [
|
|
83
100
|
{
|
|
@@ -129,6 +146,13 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
129
146
|
type: 'storage',
|
|
130
147
|
},
|
|
131
148
|
},
|
|
149
|
+
{
|
|
150
|
+
binding: 7,
|
|
151
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
152
|
+
buffer: {
|
|
153
|
+
type: 'storage',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
132
156
|
],
|
|
133
157
|
});
|
|
134
158
|
const bindGroups = [0, 1].map((i) => {
|
|
@@ -181,6 +205,12 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
181
205
|
buffer: gpuCounterBuffer,
|
|
182
206
|
},
|
|
183
207
|
},
|
|
208
|
+
{
|
|
209
|
+
binding: 7,
|
|
210
|
+
resource: {
|
|
211
|
+
buffer: gpuBoundsBuffer,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
184
214
|
],
|
|
185
215
|
});
|
|
186
216
|
});
|
|
@@ -208,10 +238,6 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
208
238
|
size: UPDATED_VOXELS_COUNTER_BUFFER_SIZE,
|
|
209
239
|
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
210
240
|
});
|
|
211
|
-
const labelmapStagingBufferTemp = device.createBuffer({
|
|
212
|
-
size: BUFFER_SIZE,
|
|
213
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
214
|
-
});
|
|
215
241
|
const limitProcessingTime = maxProcessingTime
|
|
216
242
|
? performance.now() + maxProcessingTime
|
|
217
243
|
: 0;
|
|
@@ -257,13 +283,34 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
|
|
|
257
283
|
size: BUFFER_SIZE,
|
|
258
284
|
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
259
285
|
});
|
|
286
|
+
const boundsStagingBuffer = device.createBuffer({
|
|
287
|
+
size: BOUNDS_BUFFER_SIZE,
|
|
288
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
289
|
+
});
|
|
260
290
|
commandEncoder.copyBufferToBuffer(gpuLabelmapBuffers[outputLabelmapBufferIndex], 0, labelmapStagingBuffer, 0, BUFFER_SIZE);
|
|
291
|
+
commandEncoder.copyBufferToBuffer(gpuBoundsBuffer, 0, boundsStagingBuffer, 0, BOUNDS_BUFFER_SIZE);
|
|
261
292
|
device.queue.submit([commandEncoder.finish()]);
|
|
262
293
|
await labelmapStagingBuffer.mapAsync(GPUMapMode.READ, 0, BUFFER_SIZE);
|
|
263
294
|
const labelmapResultBuffer = labelmapStagingBuffer.getMappedRange(0, BUFFER_SIZE);
|
|
264
295
|
const labelmapResult = new Uint32Array(labelmapResultBuffer);
|
|
265
296
|
labelmapData.set(labelmapResult);
|
|
266
297
|
labelmapStagingBuffer.unmap();
|
|
298
|
+
await boundsStagingBuffer.mapAsync(GPUMapMode.READ, 0, BOUNDS_BUFFER_SIZE);
|
|
299
|
+
const boundsResultBuffer = boundsStagingBuffer.getMappedRange(0, BOUNDS_BUFFER_SIZE);
|
|
300
|
+
const boundsResult = new Int32Array(boundsResultBuffer.slice(0));
|
|
301
|
+
boundsStagingBuffer.unmap();
|
|
302
|
+
const minX = boundsResult[0];
|
|
303
|
+
const minY = boundsResult[1];
|
|
304
|
+
const minZ = boundsResult[2];
|
|
305
|
+
const maxX = boundsResult[3];
|
|
306
|
+
const maxY = boundsResult[4];
|
|
307
|
+
const maxZ = boundsResult[5];
|
|
267
308
|
labelmap.voxelManager.setCompleteScalarDataArray(labelmapData);
|
|
309
|
+
labelmap.voxelManager.clearBounds();
|
|
310
|
+
labelmap.voxelManager.setBounds([
|
|
311
|
+
[minX, maxX],
|
|
312
|
+
[minY, maxY],
|
|
313
|
+
[minZ, maxZ],
|
|
314
|
+
]);
|
|
268
315
|
}
|
|
269
316
|
export { runGrowCut as default, runGrowCut as run };
|
|
@@ -3,7 +3,13 @@ import type { GrowCutOptions } from './runGrowCut';
|
|
|
3
3
|
type GrowCutOneClickOptions = GrowCutOptions & {
|
|
4
4
|
subVolumePaddingPercentage?: number | [number, number, number];
|
|
5
5
|
subVolumeMinPadding?: number | [number, number, number];
|
|
6
|
+
negativeSeedMargin?: number;
|
|
7
|
+
negativeSeedsCount?: number;
|
|
6
8
|
};
|
|
7
|
-
declare function runOneClickGrowCut(referencedVolumeId
|
|
9
|
+
declare function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }: {
|
|
10
|
+
referencedVolumeId: string;
|
|
11
|
+
worldPosition: Types.Point3;
|
|
12
|
+
options?: GrowCutOneClickOptions;
|
|
13
|
+
}): Promise<Types.IImageVolume>;
|
|
8
14
|
export { runOneClickGrowCut as default, runOneClickGrowCut };
|
|
9
15
|
export type { GrowCutOneClickOptions };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { vec3 } from 'gl-matrix';
|
|
2
1
|
import { utilities as csUtils, cache, volumeLoader } from '@cornerstonejs/core';
|
|
3
2
|
import { run } from './runGrowCut';
|
|
4
3
|
const { transformWorldToIndex, transformIndexToWorld } = csUtils;
|
|
@@ -6,63 +5,25 @@ const POSITIVE_SEED_VALUE = 254;
|
|
|
6
5
|
const NEGATIVE_SEED_VALUE = 255;
|
|
7
6
|
const POSITIVE_SEED_VARIANCE = 0.1;
|
|
8
7
|
const NEGATIVE_SEED_VARIANCE = 0.8;
|
|
9
|
-
|
|
10
|
-
const SUBVOLUME_MIN_PADDING = 5;
|
|
11
|
-
function _createSubVolume(referencedVolume, positiveRegionData, options) {
|
|
12
|
-
const { dimensions } = referencedVolume;
|
|
13
|
-
const positiveRegionSize = vec3.sub(vec3.create(), positiveRegionData.boundingBox.bottomRight, positiveRegionData.boundingBox.topLeft);
|
|
14
|
-
let subVolumePaddingPercentage = options?.subVolumePaddingPercentage ?? SUBVOLUME_PADDING_PERCENTAGE;
|
|
15
|
-
let subVolumeMinPadding = options?.subVolumeMinPadding ?? SUBVOLUME_MIN_PADDING;
|
|
16
|
-
if (typeof subVolumePaddingPercentage === 'number') {
|
|
17
|
-
subVolumePaddingPercentage = [
|
|
18
|
-
subVolumePaddingPercentage,
|
|
19
|
-
subVolumePaddingPercentage,
|
|
20
|
-
subVolumePaddingPercentage,
|
|
21
|
-
];
|
|
22
|
-
}
|
|
23
|
-
if (typeof subVolumeMinPadding === 'number') {
|
|
24
|
-
subVolumeMinPadding = [
|
|
25
|
-
subVolumeMinPadding,
|
|
26
|
-
subVolumeMinPadding,
|
|
27
|
-
subVolumeMinPadding,
|
|
28
|
-
];
|
|
29
|
-
}
|
|
30
|
-
const padding = vec3.mul(vec3.create(), positiveRegionSize, subVolumePaddingPercentage);
|
|
31
|
-
vec3.round(padding, padding);
|
|
32
|
-
vec3.max(padding, padding, subVolumeMinPadding);
|
|
33
|
-
const subVolumeSize = vec3.scaleAndAdd(vec3.create(), positiveRegionSize, padding, 2);
|
|
34
|
-
const ijkTopLeft = vec3.sub(vec3.create(), positiveRegionData.boundingBox.topLeft, padding);
|
|
35
|
-
const ijkBottomRight = vec3.add(vec3.create(), ijkTopLeft, subVolumeSize);
|
|
36
|
-
vec3.max(ijkTopLeft, ijkTopLeft, [0, 0, 0]);
|
|
37
|
-
vec3.min(ijkTopLeft, ijkTopLeft, dimensions);
|
|
38
|
-
vec3.max(ijkBottomRight, ijkBottomRight, [0, 0, 0]);
|
|
39
|
-
vec3.min(ijkBottomRight, ijkBottomRight, dimensions);
|
|
40
|
-
const subVolumeBoundsIJK = {
|
|
41
|
-
minX: ijkTopLeft[0],
|
|
42
|
-
maxX: ijkBottomRight[0],
|
|
43
|
-
minY: ijkTopLeft[1],
|
|
44
|
-
maxY: ijkBottomRight[1],
|
|
45
|
-
minZ: ijkTopLeft[2],
|
|
46
|
-
maxZ: ijkBottomRight[2],
|
|
47
|
-
};
|
|
48
|
-
return csUtils.createSubVolume(referencedVolume.volumeId, subVolumeBoundsIJK, {
|
|
49
|
-
targetBuffer: {
|
|
50
|
-
type: 'Float32Array',
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
function _getPositiveRegionData(referencedVolume, worldPosition, options) {
|
|
8
|
+
function _generateSeeds(referencedVolume, labelmap, worldPosition, options) {
|
|
55
9
|
const [width, height, numSlices] = referencedVolume.dimensions;
|
|
56
10
|
const subVolPixelData = referencedVolume.voxelManager.getCompleteScalarDataArray();
|
|
57
11
|
const numPixelsPerSlice = width * height;
|
|
58
12
|
const ijkStartPosition = transformWorldToIndex(referencedVolume.imageData, worldPosition);
|
|
59
|
-
const
|
|
13
|
+
const startIndex = ijkStartPosition[2] * numPixelsPerSlice +
|
|
60
14
|
ijkStartPosition[1] * width +
|
|
61
|
-
ijkStartPosition[0]
|
|
15
|
+
ijkStartPosition[0];
|
|
16
|
+
const referencePixelValue = subVolPixelData[startIndex];
|
|
62
17
|
const positiveSeedVariance = options.positiveSeedVariance ?? POSITIVE_SEED_VARIANCE;
|
|
63
18
|
const positiveSeedVarianceValue = Math.abs(referencePixelValue * positiveSeedVariance);
|
|
64
19
|
const minPositivePixelValue = referencePixelValue - positiveSeedVarianceValue;
|
|
65
20
|
const maxPositivePixelValue = referencePixelValue + positiveSeedVarianceValue;
|
|
21
|
+
const negativeSeedVariance = options.negativeSeedVariance ?? NEGATIVE_SEED_VARIANCE;
|
|
22
|
+
const negativeSeedVarianceValue = Math.abs(referencePixelValue * negativeSeedVariance);
|
|
23
|
+
const minNegativePixelValue = referencePixelValue - negativeSeedVarianceValue;
|
|
24
|
+
const maxNegativePixelValue = referencePixelValue + negativeSeedVarianceValue;
|
|
25
|
+
const positiveSeedValue = options.positiveSeedValue ?? POSITIVE_SEED_VALUE;
|
|
26
|
+
const negativeSeedValue = options.negativeSeedValue ?? NEGATIVE_SEED_VALUE;
|
|
66
27
|
const neighborsCoordDelta = [
|
|
67
28
|
[-1, 0, 0],
|
|
68
29
|
[1, 0, 0],
|
|
@@ -71,32 +32,23 @@ function _getPositiveRegionData(referencedVolume, worldPosition, options) {
|
|
|
71
32
|
[0, 0, -1],
|
|
72
33
|
[0, 0, 1],
|
|
73
34
|
];
|
|
74
|
-
let minX = Infinity;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let maxX = -Infinity;
|
|
78
|
-
let maxY = -Infinity;
|
|
79
|
-
let maxZ = -Infinity;
|
|
80
|
-
const startVoxelIndex = ijkStartPosition[2] * numPixelsPerSlice +
|
|
81
|
-
ijkStartPosition[1] * width +
|
|
82
|
-
ijkStartPosition[0];
|
|
83
|
-
const voxelIndexesSet = new Set([startVoxelIndex]);
|
|
84
|
-
const worldVoxelSet = new Set([worldPosition]);
|
|
35
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
36
|
+
const voxelIndexesSet = new Set([startIndex]);
|
|
37
|
+
labelmap.voxelManager.setAtIndex(startIndex, positiveSeedValue);
|
|
85
38
|
const queue = [ijkStartPosition];
|
|
86
39
|
while (queue.length) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const nz = z + neighborCoordDelta[2];
|
|
40
|
+
const [x, y, z] = queue.shift();
|
|
41
|
+
minX = x < minX ? x : minX;
|
|
42
|
+
minY = y < minY ? y : minY;
|
|
43
|
+
minZ = z < minZ ? z : minZ;
|
|
44
|
+
maxX = x > maxX ? x : maxX;
|
|
45
|
+
maxY = y > maxY ? y : maxY;
|
|
46
|
+
maxZ = z > maxZ ? z : maxZ;
|
|
47
|
+
for (let i = 0; i < neighborsCoordDelta.length; i++) {
|
|
48
|
+
const [dx, dy, dz] = neighborsCoordDelta[i];
|
|
49
|
+
const nx = x + dx;
|
|
50
|
+
const ny = y + dy;
|
|
51
|
+
const nz = z + dz;
|
|
100
52
|
if (nx < 0 ||
|
|
101
53
|
nx >= width ||
|
|
102
54
|
ny < 0 ||
|
|
@@ -106,71 +58,51 @@ function _getPositiveRegionData(referencedVolume, worldPosition, options) {
|
|
|
106
58
|
continue;
|
|
107
59
|
}
|
|
108
60
|
const neighborVoxelIndex = nz * numPixelsPerSlice + ny * width + nx;
|
|
109
|
-
|
|
110
|
-
if (voxelIndexesSet.has(neighborVoxelIndex) ||
|
|
111
|
-
neighborPixelValue < minPositivePixelValue ||
|
|
112
|
-
neighborPixelValue > maxPositivePixelValue) {
|
|
61
|
+
if (voxelIndexesSet.has(neighborVoxelIndex)) {
|
|
113
62
|
continue;
|
|
114
63
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
64
|
+
const neighborPixelValue = subVolPixelData[neighborVoxelIndex];
|
|
65
|
+
if (neighborPixelValue >= minPositivePixelValue &&
|
|
66
|
+
neighborPixelValue <= maxPositivePixelValue) {
|
|
67
|
+
labelmap.voxelManager.setAtIndex(neighborVoxelIndex, positiveSeedValue);
|
|
68
|
+
voxelIndexesSet.add(neighborVoxelIndex);
|
|
69
|
+
queue.push([nx, ny, nz]);
|
|
70
|
+
}
|
|
120
71
|
}
|
|
121
72
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const ijkPosition = transformWorldToIndex(subVolume.imageData, worldPosition);
|
|
148
|
-
const referencePixelValue = subVolPixelData[ijkPosition[2] * width * height + ijkPosition[1] * width + ijkPosition[0]];
|
|
149
|
-
const negativeSeedVariance = options.negativeSeedVariance ?? NEGATIVE_SEED_VARIANCE;
|
|
150
|
-
const negativeSeedValue = options.negativeSeedValue ?? NEGATIVE_SEED_VALUE;
|
|
151
|
-
const negativeSeedVarianceValue = Math.abs(referencePixelValue * negativeSeedVariance);
|
|
152
|
-
const minNegativePixelValue = referencePixelValue - negativeSeedVarianceValue;
|
|
153
|
-
const maxNegativePixelValue = referencePixelValue + negativeSeedVarianceValue;
|
|
154
|
-
for (let i = 0, len = subVolPixelData.length; i < len; i++) {
|
|
155
|
-
const pixelValue = subVolPixelData[i];
|
|
156
|
-
if (!labelmapData[i] &&
|
|
157
|
-
(pixelValue < minNegativePixelValue || pixelValue > maxNegativePixelValue)) {
|
|
158
|
-
labelmap.voxelManager.setAtIndex(i, negativeSeedValue);
|
|
73
|
+
const margin = options.negativeSeedMargin ?? 30;
|
|
74
|
+
const minXwMargin = Math.max(0, minX - margin);
|
|
75
|
+
const minYwMargin = Math.max(0, minY - margin);
|
|
76
|
+
const minZwMargin = Math.max(0, minZ - margin);
|
|
77
|
+
const maxXwMargin = Math.min(width - 1, maxX + margin);
|
|
78
|
+
const maxYwMargin = Math.min(height - 1, maxY + margin);
|
|
79
|
+
const maxZwMargin = Math.min(numSlices - 1, maxZ + margin);
|
|
80
|
+
const negativeSeedsCount = options.negativeSeedsCount ?? 70;
|
|
81
|
+
const negativeIndexesSet = new Set();
|
|
82
|
+
let attempts = 0;
|
|
83
|
+
while (negativeIndexesSet.size < negativeSeedsCount &&
|
|
84
|
+
attempts < negativeSeedsCount * 50) {
|
|
85
|
+
attempts++;
|
|
86
|
+
const rx = Math.floor(Math.random() * (maxXwMargin - minXwMargin + 1) + minXwMargin);
|
|
87
|
+
const ry = Math.floor(Math.random() * (maxYwMargin - minYwMargin + 1) + minYwMargin);
|
|
88
|
+
const rz = Math.floor(Math.random() * (maxZwMargin - minZwMargin + 1) + minZwMargin);
|
|
89
|
+
const randomIndex = rz * numPixelsPerSlice + ry * width + rx;
|
|
90
|
+
if (voxelIndexesSet.has(randomIndex)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const randomVal = subVolPixelData[randomIndex];
|
|
94
|
+
if (randomVal >= minNegativePixelValue &&
|
|
95
|
+
randomVal <= maxNegativePixelValue) {
|
|
96
|
+
labelmap.voxelManager.setAtIndex(randomIndex, negativeSeedValue);
|
|
97
|
+
negativeIndexesSet.add(randomIndex);
|
|
159
98
|
}
|
|
160
99
|
}
|
|
161
100
|
}
|
|
162
|
-
async function
|
|
163
|
-
const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(subVolume.volumeId);
|
|
164
|
-
_setPositiveSeedValues(labelmap, positiveRegionData, options);
|
|
165
|
-
_setNegativeSeedValues(subVolume, labelmap, worldPosition, options);
|
|
166
|
-
return labelmap;
|
|
167
|
-
}
|
|
168
|
-
async function runOneClickGrowCut(referencedVolumeId, worldPosition, viewport, options) {
|
|
101
|
+
async function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }) {
|
|
169
102
|
const referencedVolume = cache.getVolume(referencedVolumeId);
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
await run(subVolume.volumeId, labelmap.volumeId);
|
|
103
|
+
const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(referencedVolumeId);
|
|
104
|
+
_generateSeeds(referencedVolume, labelmap, worldPosition, options);
|
|
105
|
+
await run(referencedVolumeId, labelmap.volumeId);
|
|
174
106
|
return labelmap;
|
|
175
107
|
}
|
|
176
108
|
export { runOneClickGrowCut as default, runOneClickGrowCut };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { triggerAnnotationModified } from '../stateManagement/annotation/helpers/state';
|
|
2
|
+
import { ChangeTypes } from '../enums';
|
|
3
|
+
export default function setAnnotationLabel(annotation, element, updatedLabel) {
|
|
4
|
+
annotation.data.label = updatedLabel;
|
|
5
|
+
triggerAnnotationModified(annotation, element, ChangeTypes.LabelChange);
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Cornerstone3D Tools",
|
|
5
5
|
"types": "./dist/esm/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"canvas": "^2.11.2"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
|
-
"@cornerstonejs/core": "^3.
|
|
106
|
+
"@cornerstonejs/core": "^3.1.1",
|
|
107
107
|
"@kitware/vtk.js": "32.9.0",
|
|
108
108
|
"@types/d3-array": "^3.0.4",
|
|
109
109
|
"@types/d3-interpolate": "^3.0.1",
|
|
@@ -122,5 +122,5 @@
|
|
|
122
122
|
"type": "individual",
|
|
123
123
|
"url": "https://ohif.org/donate"
|
|
124
124
|
},
|
|
125
|
-
"gitHead": "
|
|
125
|
+
"gitHead": "91a215bf75b76f6cbd017a811b5dfcff60f7f060"
|
|
126
126
|
}
|