@cornerstonejs/tools 5.4.11 → 5.4.13

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.
Files changed (46) hide show
  1. package/dist/esm/index.d.ts +2 -2
  2. package/dist/esm/index.js +2 -2
  3. package/dist/esm/stateManagement/segmentation/helpers/isSegmentationOverlayCompatible.d.ts +3 -0
  4. package/dist/esm/stateManagement/segmentation/helpers/isSegmentationOverlayCompatible.js +87 -0
  5. package/dist/esm/stateManagement/segmentation/internalAddSegmentationRepresentation.js +5 -0
  6. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.d.ts +12 -2
  7. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.js +21 -1
  8. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.d.ts +5 -0
  9. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.js +47 -1
  10. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.d.ts +5 -1
  11. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.js +49 -4
  12. package/dist/esm/tools/annotation/ClickSegmentTool.d.ts +51 -0
  13. package/dist/esm/tools/annotation/ClickSegmentTool.js +748 -0
  14. package/dist/esm/tools/annotation/regionSegmentHoverCursors.d.ts +4 -0
  15. package/dist/esm/tools/annotation/regionSegmentHoverCursors.js +4 -0
  16. package/dist/esm/tools/displayTools/Labelmap/syncStackLabelmapActors.js +5 -0
  17. package/dist/esm/tools/index.d.ts +2 -1
  18. package/dist/esm/tools/index.js +2 -1
  19. package/dist/esm/tools/segmentation/BrushTool.js +7 -3
  20. package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +21 -0
  21. package/dist/esm/tools/segmentation/strategies/utils/crossLayerErase.js +20 -0
  22. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.d.ts +12 -0
  23. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.js +128 -0
  24. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.d.ts +2 -0
  25. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.js +36 -0
  26. package/dist/esm/utilities/segmentation/createLabelmapMemo.d.ts +7 -0
  27. package/dist/esm/utilities/segmentation/createLabelmapMemo.js +41 -8
  28. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.d.ts +20 -0
  29. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.js +118 -0
  30. package/dist/esm/utilities/segmentation/floodFillSliceLazy.d.ts +33 -0
  31. package/dist/esm/utilities/segmentation/floodFillSliceLazy.js +167 -0
  32. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.d.ts +51 -0
  33. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.js +0 -0
  34. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.d.ts +10 -0
  35. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.js +21 -0
  36. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.d.ts +57 -0
  37. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.js +559 -0
  38. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.d.ts +10 -0
  39. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.js +44 -0
  40. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.d.ts +61 -0
  41. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.js +524 -0
  42. package/dist/esm/utilities/segmentation/islandRemoval.d.ts +3 -2
  43. package/dist/esm/utilities/segmentation/islandRemoval.js +3 -0
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/package.json +5 -5
@@ -0,0 +1,4 @@
1
+ export declare const CIRCLE_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3C/svg%3E\") 12 12, crosshair";
2
+ export declare const PLUS_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3Cpath%20d='M12%208v8M8%2012h8'%20stroke='%2300dc82'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, copy";
3
+ export declare const BLOCKED_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23ff5a5a'%20stroke-width='2'/%3E%3Cpath%20d='M5.7%205.7L18.3%2018.3'%20stroke='%23ff5a5a'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, not-allowed";
4
+ export declare const PENDING_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23a0a6ad'%20stroke-width='2'%20stroke-dasharray='4%203'/%3E%3C/svg%3E\") 12 12, progress";
@@ -0,0 +1,4 @@
1
+ export const CIRCLE_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3C/svg%3E\") 12 12, crosshair";
2
+ export const PLUS_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%2300dc82'%20stroke-width='2'/%3E%3Cpath%20d='M12%208v8M8%2012h8'%20stroke='%2300dc82'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, copy";
3
+ export const BLOCKED_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23ff5a5a'%20stroke-width='2'/%3E%3Cpath%20d='M5.7%205.7L18.3%2018.3'%20stroke='%23ff5a5a'%20stroke-width='2'%20stroke-linecap='round'/%3E%3C/svg%3E\") 12 12, not-allowed";
4
+ export const PENDING_CURSOR = "url(\"data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%3E%3Ccircle%20cx='12'%20cy='12'%20r='9'%20fill='none'%20stroke='%23a0a6ad'%20stroke-width='2'%20stroke-dasharray='4%203'/%3E%3C/svg%3E\") 12 12, progress";
@@ -3,6 +3,8 @@ import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData.js';
3
3
  import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction.js';
4
4
  import { ActorRenderMode, cache, utilities, } from '@cornerstonejs/core';
5
5
  import { triggerSegmentationRender } from '../../../stateManagement/segmentation/SegmentationRenderingEngine.js';
6
+ import { isSegmentationOverlayCompatible } from '../../../stateManagement/segmentation/helpers/isSegmentationOverlayCompatible.js';
7
+ import { SegmentationRepresentations } from '../../../enums/index.js';
6
8
  import { updateLabelmapSegmentationImageReferences } from '../../../stateManagement/segmentation/updateLabelmapSegmentationImageReferences.js';
7
9
  import { getCurrentLabelmapImageIdsForViewport } from '../../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport.js';
8
10
  import { getLabelmapActorEntries } from '../../../stateManagement/segmentation/helpers/getSegmentationActor.js';
@@ -18,6 +20,9 @@ export function syncStackLabelmapActors(viewport, segmentationId) {
18
20
  if (!currentImageId) {
19
21
  return;
20
22
  }
23
+ if (!isSegmentationOverlayCompatible(viewport, segmentationId, SegmentationRepresentations.Labelmap)) {
24
+ return;
25
+ }
21
26
  updateLabelmapSegmentationImageReferences(viewport.id, segmentationId);
22
27
  const derivedImageIds = getCurrentLabelmapImageIdsForViewport(viewport.id, segmentationId) ?? [];
23
28
  const derivedImageIdSet = new Set(derivedImageIds);
@@ -44,6 +44,7 @@ import KeyImageTool from './annotation/KeyImageTool.js';
44
44
  import AnnotationEraserTool from './AnnotationEraserTool.js';
45
45
  import RegionSegmentTool from './annotation/RegionSegmentTool.js';
46
46
  import RegionSegmentPlusTool from './annotation/RegionSegmentPlusTool.js';
47
+ import ClickSegmentTool from './annotation/ClickSegmentTool.js';
47
48
  import WholeBodySegmentTool from './annotation/WholeBodySegmentTool.js';
48
49
  import LabelmapBaseTool from './segmentation/LabelmapBaseTool.js';
49
50
  import RectangleScissorsTool from './segmentation/RectangleScissorsTool.js';
@@ -61,4 +62,4 @@ import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool.js
61
62
  import * as strategies from './segmentation/strategies/index.js';
62
63
  import SegmentLabelTool from './segmentation/SegmentLabelTool.js';
63
64
  import LabelMapEditWithContourTool from './segmentation/LabelmapEditWithContour.js';
64
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
65
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, ClickSegmentTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
@@ -44,6 +44,7 @@ import KeyImageTool from './annotation/KeyImageTool.js';
44
44
  import AnnotationEraserTool from './AnnotationEraserTool.js';
45
45
  import RegionSegmentTool from './annotation/RegionSegmentTool.js';
46
46
  import RegionSegmentPlusTool from './annotation/RegionSegmentPlusTool.js';
47
+ import ClickSegmentTool from './annotation/ClickSegmentTool.js';
47
48
  import WholeBodySegmentTool from './annotation/WholeBodySegmentTool.js';
48
49
  import LabelmapBaseTool from './segmentation/LabelmapBaseTool.js';
49
50
  import RectangleScissorsTool from './segmentation/RectangleScissorsTool.js';
@@ -61,4 +62,4 @@ import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool.js
61
62
  import * as strategies from './segmentation/strategies/index.js';
62
63
  import SegmentLabelTool from './segmentation/SegmentLabelTool.js';
63
64
  import LabelMapEditWithContourTool from './segmentation/LabelmapEditWithContour.js';
64
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
65
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, VolumeCroppingTool, VolumeCroppingControlTool, DragProbeTool, WindowLevelTool, WindowLevelRegionTool, StackScrollTool, PlanarRotateTool, ZoomTool, MIPJumpToClickTool, ReferenceCursors, CrosshairsTool, ReferenceLinesTool, OverlayGridTool, SegmentationIntersectionTool, BidirectionalTool, LabelTool, LengthTool, HeightTool, ProbeTool, RectangleROITool, EllipticalROITool, CircleROITool, ETDRSGridTool, SplineROITool, PlanarFreehandROITool, PlanarFreehandContourSegmentationTool, LivewireContourTool, LivewireContourSegmentationTool, ArrowAnnotateTool, AngleTool, CobbAngleTool, UltrasoundDirectionalTool, UltrasoundPleuraBLineTool, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, OrientationControllerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, ClickSegmentTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, LabelMapEditWithContourTool, strategies, };
@@ -111,9 +111,6 @@ class BrushTool extends LabelmapBaseTool {
111
111
  return false;
112
112
  }
113
113
  this._editData = this.createEditData(element);
114
- this._activateDraw(element);
115
- hideElementCursor(element);
116
- evt.preventDefault();
117
114
  this._previewData.isDrag = false;
118
115
  this._previewData.timerStart = Date.now();
119
116
  const canvasPoint = vec2.clone(currentPoints.canvas);
@@ -126,6 +123,10 @@ class BrushTool extends LabelmapBaseTool {
126
123
  world: vec3.clone(worldPoint),
127
124
  };
128
125
  this._hoverData = this.createHoverData(element, canvasPoint);
126
+ if (!this._hoverData) {
127
+ this._editData = null;
128
+ return false;
129
+ }
129
130
  this._calculateCursor(element, canvasPoint);
130
131
  this._resetLazyEditState();
131
132
  if (this._isLazyLabelmapEditingEnabled(this._hoverData.viewport)) {
@@ -137,6 +138,9 @@ class BrushTool extends LabelmapBaseTool {
137
138
  if (!operationData) {
138
139
  return false;
139
140
  }
141
+ this._activateDraw(element);
142
+ hideElementCursor(element);
143
+ evt.preventDefault();
140
144
  this.applyActiveStrategyCallback(enabledElement, operationData, StrategyCallbacks.OnInteractionStart);
141
145
  return true;
142
146
  };
@@ -2,6 +2,7 @@ import { triggerSegmentationDataModified } from '../../../stateManagement/segmen
2
2
  import compositions from './compositions/index.js';
3
3
  import { getStrategyData } from './utils/getStrategyData.js';
4
4
  import { StrategyCallbacks } from '../../../enums/index.js';
5
+ import { memoAsStep } from '../../../utilities/segmentation/createLabelmapMemo.js';
5
6
  import { eraseCrossLayerOverwrites, prepareOverlapOperationData, } from './utils/labelmapOverlap.js';
6
7
  import { shouldUseLazyLabelmapEditing } from '../utils/shouldUseLazyLabelmapEditing.js';
7
8
  class BrushStrategy {
@@ -23,7 +24,27 @@ class BrushStrategy {
23
24
  }
24
25
  if (initializedData.memo?.segmentationVoxelManager !==
25
26
  initializedData.segmentationVoxelManager) {
27
+ const previousMemo = initializedData.memo;
26
28
  initializedData.memo = initializedData.createMemo(initializedData.segmentationId, initializedData.segmentationVoxelManager);
29
+ if (previousMemo &&
30
+ previousMemo !== initializedData.memo &&
31
+ previousMemo.commitMemo?.()) {
32
+ (initializedData.memo.priorSteps ||= []).push(memoAsStep(previousMemo));
33
+ }
34
+ }
35
+ const moveStep = initializedData.labelmapEditTransaction?.moveStep;
36
+ if (moveStep && initializedData.memo) {
37
+ const { segmentationId } = initializedData;
38
+ (initializedData.memo.priorSteps ||= []).push({
39
+ undo: () => {
40
+ moveStep.undo();
41
+ triggerSegmentationDataModified(segmentationId);
42
+ },
43
+ redo: () => {
44
+ moveStep.redo();
45
+ triggerSegmentationDataModified(segmentationId);
46
+ },
47
+ });
27
48
  }
28
49
  if (initializedData.segmentationVoxelManager !==
29
50
  originalSegmentationVoxelManager ||
@@ -1,4 +1,5 @@
1
1
  import { getSegmentation } from '../../../../stateManagement/segmentation/getSegmentation.js';
2
+ import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents.js';
2
3
  import { collectCrossLayerEraseBindings, eraseLabelmapEditTransactionOverwrites, } from '../../../../stateManagement/segmentation/helpers/labelmapSegmentationState.js';
3
4
  function collectCrossLayerEraseBindingsForOperation(operationData) {
4
5
  const { segmentationId, labelmapId, overwriteSegmentIndices } = operationData;
@@ -12,12 +13,31 @@ function eraseCrossLayerOverwrites(operationData) {
12
13
  if (!segmentation) {
13
14
  return [];
14
15
  }
16
+ const { memo } = operationData;
15
17
  return eraseLabelmapEditTransactionOverwrites(segmentation, operationData.labelmapEditTransaction, {
16
18
  viewport: operationData.viewport,
17
19
  referenceImageData: operationData.segmentationImageData,
18
20
  isInObject: operationData.isInObject,
19
21
  isInObjectBoundsIJK: operationData.isInObjectBoundsIJK,
20
22
  imageId: operationData.imageId,
23
+ crossLayerEraseCallback: memo
24
+ ? ({ voxelManager, labelValue, indices }) => {
25
+ (memo.postSteps ||= []).push({
26
+ undo: () => {
27
+ for (const index of indices) {
28
+ voxelManager.setAtIndex(index, labelValue);
29
+ }
30
+ triggerSegmentationDataModified(operationData.segmentationId);
31
+ },
32
+ redo: () => {
33
+ for (const index of indices) {
34
+ voxelManager.setAtIndex(index, 0);
35
+ }
36
+ triggerSegmentationDataModified(operationData.segmentationId);
37
+ },
38
+ });
39
+ }
40
+ : undefined,
21
41
  });
22
42
  }
23
43
  export { collectCrossLayerEraseBindingsForOperation as collectCrossLayerEraseBindings, eraseCrossLayerOverwrites, };
@@ -0,0 +1,12 @@
1
+ import { type Types } from '@cornerstonejs/core';
2
+ export declare function commitSliceMasksToLabelmapVolume({ labelmapVolume, sliceMasks, width: w, height: h, paintIndex, historyVoxelManager, }: {
3
+ labelmapVolume: Types.IImageVolume;
4
+ sliceMasks: Map<number, Uint8Array>;
5
+ width: number;
6
+ height: number;
7
+ paintIndex: number;
8
+ historyVoxelManager?: Types.IVoxelManager<number>;
9
+ }): {
10
+ floodedPoints: Types.Point3[];
11
+ voxelCount: number;
12
+ };
@@ -0,0 +1,128 @@
1
+ import { cache, utilities } from '@cornerstonejs/core';
2
+ import { FLOOD_SLICE_FLAG_VISITED } from './floodFillSliceLazy.js';
3
+ const { VoxelManager } = utilities;
4
+ export function commitSliceMasksToLabelmapVolume({ labelmapVolume, sliceMasks, width: w, height: h, paintIndex, historyVoxelManager, }) {
5
+ const floodedPoints = [];
6
+ let voxelCount = 0;
7
+ const vm = labelmapVolume.voxelManager;
8
+ const historyWriter = historyVoxelManager;
9
+ const [labelmapWidth, labelmapHeight, depth] = labelmapVolume.dimensions;
10
+ if (labelmapWidth !== w || labelmapHeight !== h) {
11
+ throw new Error(`commitSliceMasksToLabelmapVolume: labelmap in-plane dimensions ` +
12
+ `${labelmapWidth}x${labelmapHeight} do not match mask dimensions ${w}x${h}`);
13
+ }
14
+ const frameSize = w * h;
15
+ const expectedLen = frameSize * depth;
16
+ const zs = Array.from(sliceMasks.keys()).sort((a, b) => a - b);
17
+ for (let zi = 0; zi < zs.length; zi++) {
18
+ const z = zs[zi];
19
+ if (z < 0 || z >= depth) {
20
+ continue;
21
+ }
22
+ const flags = sliceMasks.get(z);
23
+ if (!flags || flags.length !== frameSize) {
24
+ continue;
25
+ }
26
+ let sliceTouched = false;
27
+ let minX = Infinity;
28
+ let minY = Infinity;
29
+ let maxX = -Infinity;
30
+ let maxY = -Infinity;
31
+ const appendRunFlooded = (y, x0, x1) => {
32
+ for (let xi = x0; xi < x1; xi++) {
33
+ floodedPoints.push([xi, y, z]);
34
+ }
35
+ voxelCount += x1 - x0;
36
+ sliceTouched = true;
37
+ minX = Math.min(minX, x0);
38
+ maxX = Math.max(maxX, x1 - 1);
39
+ minY = Math.min(minY, y);
40
+ maxY = Math.max(maxY, y);
41
+ };
42
+ const denseScalar = !historyWriter &&
43
+ vm.scalarData &&
44
+ vm.scalarData.length >= expectedLen &&
45
+ typeof vm.scalarData.fill === 'function';
46
+ if (denseScalar) {
47
+ const base = z * frameSize;
48
+ const data = vm.scalarData;
49
+ for (let y = 0; y < h; y++) {
50
+ const row = y * w;
51
+ for (let x = 0; x < w;) {
52
+ const li = row + x;
53
+ if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
54
+ x++;
55
+ continue;
56
+ }
57
+ const x0 = x;
58
+ while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
59
+ x++;
60
+ }
61
+ data.fill(paintIndex, base + row + x0, base + row + x);
62
+ appendRunFlooded(y, x0, x);
63
+ }
64
+ }
65
+ }
66
+ else {
67
+ const imageIds = labelmapVolume.imageIds;
68
+ const image = imageIds?.length && z < imageIds.length
69
+ ? cache.getImage(imageIds[z])
70
+ : null;
71
+ const svm = image?.voxelManager;
72
+ if (!historyWriter &&
73
+ svm?.scalarData &&
74
+ svm.scalarData.length >= frameSize &&
75
+ typeof svm.scalarData.fill === 'function') {
76
+ const data = svm.scalarData;
77
+ for (let y = 0; y < h; y++) {
78
+ const row = y * w;
79
+ for (let x = 0; x < w;) {
80
+ const li = row + x;
81
+ if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
82
+ x++;
83
+ continue;
84
+ }
85
+ const x0 = x;
86
+ while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
87
+ x++;
88
+ }
89
+ data.fill(paintIndex, row + x0, row + x);
90
+ appendRunFlooded(y, x0, x);
91
+ }
92
+ }
93
+ svm.modifiedSlices.add(z);
94
+ }
95
+ else {
96
+ const writer = historyWriter ?? vm;
97
+ for (let y = 0; y < h; y++) {
98
+ const row = y * w;
99
+ for (let x = 0; x < w; x++) {
100
+ if (!(flags[row + x] & FLOOD_SLICE_FLAG_VISITED)) {
101
+ continue;
102
+ }
103
+ const index = z * frameSize + row + x;
104
+ writer.setAtIndex(index, paintIndex);
105
+ floodedPoints.push([x, y, z]);
106
+ voxelCount++;
107
+ sliceTouched = true;
108
+ minX = Math.min(minX, x);
109
+ maxX = Math.max(maxX, x);
110
+ minY = Math.min(minY, y);
111
+ maxY = Math.max(maxY, y);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ if (sliceTouched) {
117
+ vm.modifiedSlices.add(z);
118
+ if (Number.isFinite(minX) &&
119
+ Number.isFinite(minY) &&
120
+ Number.isFinite(maxX) &&
121
+ Number.isFinite(maxY)) {
122
+ VoxelManager.addBounds(vm.boundsIJK, [minX, minY, z]);
123
+ VoxelManager.addBounds(vm.boundsIJK, [maxX, maxY, z]);
124
+ }
125
+ }
126
+ }
127
+ return { floodedPoints, voxelCount };
128
+ }
@@ -0,0 +1,2 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export declare function createEnsureSliceLoadedForVolume(volume: Types.IImageVolume): (z: number) => Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { imageLoader } from '@cornerstonejs/core';
2
+ export function createEnsureSliceLoadedForVolume(volume) {
3
+ const numSlices = volume.dimensions[2];
4
+ const imageIds = volume.imageIds;
5
+ if (!imageIds?.length) {
6
+ return async () => undefined;
7
+ }
8
+ const loadedSlices = new Set();
9
+ const inFlight = new Map();
10
+ return async function ensureSliceLoaded(z) {
11
+ if (!Number.isFinite(z) || z < 0 || z >= numSlices) {
12
+ return;
13
+ }
14
+ if (loadedSlices.has(z)) {
15
+ return;
16
+ }
17
+ const existing = inFlight.get(z);
18
+ if (existing) {
19
+ return existing;
20
+ }
21
+ const imageId = imageIds[z];
22
+ if (!imageId) {
23
+ return;
24
+ }
25
+ const promise = imageLoader
26
+ .loadImage(imageId)
27
+ .then(() => {
28
+ loadedSlices.add(z);
29
+ })
30
+ .finally(() => {
31
+ inFlight.delete(z);
32
+ });
33
+ inFlight.set(z, promise);
34
+ return promise;
35
+ };
36
+ }
@@ -1,5 +1,9 @@
1
1
  import { utilities } from '@cornerstonejs/core';
2
2
  import type { Types } from '@cornerstonejs/core';
3
+ export type LabelmapRestoreStep = {
4
+ undo: () => void;
5
+ redo: () => void;
6
+ };
3
7
  export type LabelmapMemo = Types.Memo & {
4
8
  segmentationVoxelManager: Types.IVoxelManager<number>;
5
9
  voxelManager: Types.IVoxelManager<number>;
@@ -7,6 +11,8 @@ export type LabelmapMemo = Types.Memo & {
7
11
  undoVoxelManager?: Types.IVoxelManager<number>;
8
12
  memo?: LabelmapMemo;
9
13
  id: string;
14
+ priorSteps?: LabelmapRestoreStep[];
15
+ postSteps?: LabelmapRestoreStep[];
10
16
  };
11
17
  export declare function createLabelmapMemo<T>(segmentationId: string, segmentationVoxelManager: Types.IVoxelManager<T>): {
12
18
  segmentationId: string;
@@ -17,6 +23,7 @@ export declare function createLabelmapMemo<T>(segmentationId: string, segmentati
17
23
  id: string;
18
24
  operationType: string;
19
25
  };
26
+ export declare function memoAsStep(memo: LabelmapMemo): LabelmapRestoreStep;
20
27
  export declare function restoreMemo(isUndo?: boolean): void;
21
28
  export declare function createRleMemo<T>(segmentationId: string, segmentationVoxelManager: Types.IVoxelManager<T>): {
22
29
  segmentationId: string;
@@ -4,14 +4,46 @@ const { VoxelManager, RLEVoxelMap } = utilities;
4
4
  export function createLabelmapMemo(segmentationId, segmentationVoxelManager) {
5
5
  return createRleMemo(segmentationId, segmentationVoxelManager);
6
6
  }
7
+ export function memoAsStep(memo) {
8
+ return {
9
+ undo: () => memo.restoreMemo(true),
10
+ redo: () => memo.restoreMemo(false),
11
+ };
12
+ }
7
13
  export function restoreMemo(isUndo) {
8
- const { segmentationVoxelManager, undoVoxelManager, redoVoxelManager } = this;
9
- const useVoxelManager = isUndo === false ? redoVoxelManager : undoVoxelManager;
10
- useVoxelManager.forEach(({ value, pointIJK }) => {
11
- segmentationVoxelManager.setAtIJKPoint(pointIJK, value);
12
- });
13
- const slices = useVoxelManager.getArrayOfModifiedSlices();
14
- triggerSegmentationDataModified(this.segmentationId, slices);
14
+ const undo = isUndo !== false;
15
+ const priorSteps = this.priorSteps ?? [];
16
+ const postSteps = this.postSteps ?? [];
17
+ const restoreVoxelChanges = () => {
18
+ const { segmentationVoxelManager, undoVoxelManager, redoVoxelManager } = this;
19
+ const useVoxelManager = undo ? undoVoxelManager : redoVoxelManager;
20
+ if (!useVoxelManager) {
21
+ return;
22
+ }
23
+ useVoxelManager.forEach(({ value, pointIJK }) => {
24
+ segmentationVoxelManager.setAtIJKPoint(pointIJK, value);
25
+ });
26
+ const slices = useVoxelManager.getArrayOfModifiedSlices();
27
+ triggerSegmentationDataModified(this.segmentationId, slices);
28
+ };
29
+ if (undo) {
30
+ for (let i = postSteps.length - 1; i >= 0; i--) {
31
+ postSteps[i].undo();
32
+ }
33
+ restoreVoxelChanges();
34
+ for (let i = priorSteps.length - 1; i >= 0; i--) {
35
+ priorSteps[i].undo();
36
+ }
37
+ }
38
+ else {
39
+ for (const step of priorSteps) {
40
+ step.redo();
41
+ }
42
+ restoreVoxelChanges();
43
+ for (const step of postSteps) {
44
+ step.redo();
45
+ }
46
+ }
15
47
  }
16
48
  export function createRleMemo(segmentationId, segmentationVoxelManager) {
17
49
  const voxelManager = VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
@@ -30,8 +62,9 @@ function commitMemo() {
30
62
  if (this.redoVoxelManager) {
31
63
  return true;
32
64
  }
65
+ const hasSteps = !!(this.priorSteps?.length || this.postSteps?.length);
33
66
  if (!this.voxelManager.modifiedSlices.size) {
34
- return false;
67
+ return hasSteps;
35
68
  }
36
69
  const { segmentationVoxelManager } = this;
37
70
  const undoVoxelManager = VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager);
@@ -0,0 +1,20 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import IslandRemoval, { SegmentationEnum } from './islandRemoval.js';
3
+ export { SegmentationEnum };
4
+ export default class FloodFillIslandRemoval extends IslandRemoval {
5
+ private verboseLogging;
6
+ private usingPreviewLayer;
7
+ private internalFilledPoints;
8
+ constructor(options?: {
9
+ maxInternalRemove?: number;
10
+ fillInternalEdge?: boolean;
11
+ verboseLogging?: boolean;
12
+ });
13
+ initialize(viewport: any, segmentationVoxels: any, options: any): boolean;
14
+ private static countRleValueVolume;
15
+ floodFillSegmentIsland(): number;
16
+ removeExternalIslands(): number;
17
+ removeInternalIslands(): number[];
18
+ protected onInternalPointFilled(point: Types.Point3): void;
19
+ getInternalFilledPoints(): Types.Point3[];
20
+ }
@@ -0,0 +1,118 @@
1
+ import { utilities } from '@cornerstonejs/core';
2
+ import IslandRemoval, { SegmentationEnum } from './islandRemoval.js';
3
+ const { logger } = utilities;
4
+ const { growCutLog: islandRemovalLog } = logger;
5
+ export { SegmentationEnum };
6
+ export default class FloodFillIslandRemoval extends IslandRemoval {
7
+ constructor(options) {
8
+ super(options);
9
+ this.verboseLogging = false;
10
+ this.usingPreviewLayer = false;
11
+ this.internalFilledPoints = [];
12
+ this.verboseLogging = options?.verboseLogging ?? this.verboseLogging;
13
+ }
14
+ initialize(viewport, segmentationVoxels, options) {
15
+ this.usingPreviewLayer = !!segmentationVoxels.sourceVoxelManager;
16
+ const initialized = super.initialize(viewport, segmentationVoxels, options);
17
+ if (initialized && this.verboseLogging) {
18
+ const { segmentSet } = this;
19
+ islandRemovalLog.info('islandRemoval: initialize', {
20
+ segmentIndex: this.segmentIndex,
21
+ previewSegmentIndex: this.previewSegmentIndex,
22
+ segmentSetDimensions: {
23
+ width: segmentSet.width,
24
+ height: segmentSet.height,
25
+ depth: segmentSet.depth,
26
+ },
27
+ boundsIJKPrime: segmentSet.normalizer.boundsIJKPrime,
28
+ clickedPoints: this.selectedPoints,
29
+ segmentVoxelsInPlaneGrid: FloodFillIslandRemoval.countRleValueVolume(segmentSet, SegmentationEnum.SEGMENT),
30
+ });
31
+ }
32
+ return initialized;
33
+ }
34
+ static countRleValueVolume(segmentSet, value) {
35
+ let n = 0;
36
+ segmentSet.forEach((_baseIndex, rle) => {
37
+ if (rle.value === value) {
38
+ n += rle.end - rle.start;
39
+ }
40
+ });
41
+ return n;
42
+ }
43
+ floodFillSegmentIsland() {
44
+ if (this.verboseLogging) {
45
+ const { selectedPoints, segmentSet } = this;
46
+ const { fromIJK } = segmentSet.normalizer;
47
+ for (const clickedPoint of selectedPoints) {
48
+ const ijkPrime = fromIJK(clickedPoint);
49
+ const atClick = segmentSet.get(segmentSet.toIndex(ijkPrime));
50
+ if (atClick !== SegmentationEnum.SEGMENT) {
51
+ islandRemovalLog.info('islandRemoval: floodFillSegmentIsland click skipped (not SEGMENT)', {
52
+ clickedPointVolumeIJK: clickedPoint,
53
+ ijkPrime,
54
+ segmentSetAtIndex: atClick,
55
+ });
56
+ }
57
+ }
58
+ }
59
+ const floodedCount = super.floodFillSegmentIsland();
60
+ if (this.verboseLogging) {
61
+ islandRemovalLog.info('islandRemoval: floodFillSegmentIsland done', {
62
+ totalIslandVoxels: floodedCount,
63
+ islandVoxelsAfterFlood: FloodFillIslandRemoval.countRleValueVolume(this.segmentSet, SegmentationEnum.ISLAND),
64
+ });
65
+ }
66
+ return floodedCount;
67
+ }
68
+ removeExternalIslands() {
69
+ const { previewVoxelManager, segmentSet } = this;
70
+ const { toIJK } = segmentSet.normalizer;
71
+ const sourceVoxelManager = previewVoxelManager.sourceVoxelManager ?? previewVoxelManager;
72
+ if (!this.usingPreviewLayer) {
73
+ islandRemovalLog.warn('islandRemoval: removeExternalIslands has no preview layer; ' +
74
+ 'external island cleanup of accepted voxels is skipped. ' +
75
+ 'Run island removal through a preview layer to clear external islands.');
76
+ return 0;
77
+ }
78
+ let clearedVoxels = 0;
79
+ const callback = (index, rle) => {
80
+ const [, jPrime, kPrime] = segmentSet.toIJK(index);
81
+ if (rle.value !== SegmentationEnum.ISLAND) {
82
+ for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
83
+ const clearPoint = toIJK([iPrime, jPrime, kPrime]);
84
+ const sourceVal = sourceVoxelManager.getAtIJKPoint(clearPoint);
85
+ if (sourceVal === this.previewSegmentIndex) {
86
+ previewVoxelManager.setAtIJKPoint(clearPoint, null);
87
+ clearedVoxels += 1;
88
+ }
89
+ }
90
+ }
91
+ };
92
+ segmentSet.forEach(callback, { rowModified: true });
93
+ if (this.verboseLogging) {
94
+ islandRemovalLog.info('islandRemoval: removeExternalIslands', {
95
+ clearedVoxels,
96
+ });
97
+ }
98
+ return clearedVoxels;
99
+ }
100
+ removeInternalIslands() {
101
+ this.internalFilledPoints = [];
102
+ const modifiedSlices = super.removeInternalIslands();
103
+ if (this.verboseLogging) {
104
+ islandRemovalLog.info('islandRemoval: removeInternalIslands', {
105
+ modifiedSliceCount: modifiedSlices?.length,
106
+ internalFilledPoints: this.internalFilledPoints.length,
107
+ maxInternalRemove: this.maxInternalRemove,
108
+ });
109
+ }
110
+ return modifiedSlices;
111
+ }
112
+ onInternalPointFilled(point) {
113
+ this.internalFilledPoints.push(point);
114
+ }
115
+ getInternalFilledPoints() {
116
+ return this.internalFilledPoints;
117
+ }
118
+ }
@@ -0,0 +1,33 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export declare const FLOOD_SLICE_FLAG_VISITED = 1;
3
+ export type FloodFillSliceLazyOptions = {
4
+ width: number;
5
+ height: number;
6
+ depth: number;
7
+ equals: (node: unknown, startNode: unknown) => boolean;
8
+ ensureSliceLoaded?: (z: number) => Promise<void>;
9
+ yieldEvery?: number;
10
+ planar?: boolean;
11
+ maxDeltaK?: number;
12
+ maxDeltaIJ?: number;
13
+ isCancelled?: () => boolean;
14
+ maxVoxels?: number;
15
+ shouldContinue?: (stats: {
16
+ voxelCount: number;
17
+ bbox: {
18
+ min: Types.Point3;
19
+ max: Types.Point3;
20
+ };
21
+ }) => boolean;
22
+ validateEvery?: number;
23
+ };
24
+ export type FloodFillSliceLazyResult = {
25
+ sliceMasks: Map<number, Uint8Array>;
26
+ voxelCount: number;
27
+ truncated: boolean;
28
+ bbox: {
29
+ min: Types.Point3;
30
+ max: Types.Point3;
31
+ } | null;
32
+ };
33
+ export declare function floodFill3dSliceLazy(getter: (x: number, y: number, z: number) => number | undefined, seed: Types.Point3, options: FloodFillSliceLazyOptions): Promise<FloodFillSliceLazyResult>;