@cornerstonejs/tools 5.4.12 → 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.
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.d.ts +12 -2
- package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.js +21 -1
- package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.d.ts +5 -0
- package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.js +47 -1
- package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.d.ts +5 -1
- package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.js +49 -4
- package/dist/esm/tools/annotation/ClickSegmentTool.d.ts +51 -0
- package/dist/esm/tools/annotation/ClickSegmentTool.js +748 -0
- package/dist/esm/tools/annotation/regionSegmentHoverCursors.d.ts +4 -0
- package/dist/esm/tools/annotation/regionSegmentHoverCursors.js +4 -0
- package/dist/esm/tools/index.d.ts +2 -1
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/segmentation/BrushTool.js +1 -0
- package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +21 -0
- package/dist/esm/tools/segmentation/strategies/utils/crossLayerErase.js +20 -0
- package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.d.ts +12 -0
- package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.js +128 -0
- package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.d.ts +2 -0
- package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.js +36 -0
- package/dist/esm/utilities/segmentation/createLabelmapMemo.d.ts +7 -0
- package/dist/esm/utilities/segmentation/createLabelmapMemo.js +41 -8
- package/dist/esm/utilities/segmentation/floodFillIslandRemoval.d.ts +20 -0
- package/dist/esm/utilities/segmentation/floodFillIslandRemoval.js +118 -0
- package/dist/esm/utilities/segmentation/floodFillSliceLazy.d.ts +33 -0
- package/dist/esm/utilities/segmentation/floodFillSliceLazy.js +167 -0
- package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.d.ts +51 -0
- package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.js +0 -0
- package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.d.ts +10 -0
- package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.js +21 -0
- package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.d.ts +57 -0
- package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.js +559 -0
- package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.d.ts +10 -0
- package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.js +44 -0
- package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.d.ts +61 -0
- package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.js +524 -0
- package/dist/esm/utilities/segmentation/islandRemoval.d.ts +3 -2
- package/dist/esm/utilities/segmentation/islandRemoval.js +3 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +4 -4
|
@@ -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";
|
|
@@ -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, };
|
package/dist/esm/tools/index.js
CHANGED
|
@@ -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, };
|
|
@@ -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,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
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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>;
|