@cornerstonejs/tools 3.8.0 → 3.8.2
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/store/addTool.js +5 -4
- package/dist/esm/tools/annotation/RegionSegmentPlusTool.d.ts +4 -0
- package/dist/esm/tools/annotation/RegionSegmentPlusTool.js +67 -6
- package/dist/esm/tools/base/GrowCutBaseTool.d.ts +6 -0
- package/dist/esm/tools/base/GrowCutBaseTool.js +20 -21
- package/dist/esm/utilities/segmentation/growCut/constants.d.ts +8 -0
- package/dist/esm/utilities/segmentation/growCut/constants.js +8 -0
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.d.ts +16 -5
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.js +160 -67
- package/package.json +3 -3
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { state } from './state';
|
|
2
2
|
export function addTool(ToolClass) {
|
|
3
3
|
const toolName = ToolClass.toolName;
|
|
4
|
-
const toolAlreadyAdded = state.tools[toolName] !== undefined;
|
|
5
4
|
if (!toolName) {
|
|
6
5
|
throw new Error(`No Tool Found for the ToolClass ${ToolClass.name}`);
|
|
7
6
|
}
|
|
8
|
-
state.tools[toolName]
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
if (!state.tools[toolName]) {
|
|
8
|
+
state.tools[toolName] = {
|
|
9
|
+
toolClass: ToolClass,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
11
12
|
}
|
|
12
13
|
export function hasTool(ToolClass) {
|
|
13
14
|
const toolName = ToolClass.toolName;
|
|
@@ -8,7 +8,11 @@ type RegionSegmentPlusToolData = GrowCutToolData & {
|
|
|
8
8
|
declare class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
9
9
|
static toolName: string;
|
|
10
10
|
protected growCutData: RegionSegmentPlusToolData | null;
|
|
11
|
+
private mouseTimer;
|
|
12
|
+
private allowedToProceed;
|
|
11
13
|
constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
|
|
14
|
+
mouseMoveCallback(evt: EventTypes.MouseMoveEventType): void;
|
|
15
|
+
onMouseStable(evt: EventTypes.MouseMoveEventType, worldPoint: Types.Point3, element: HTMLDivElement): Promise<void>;
|
|
12
16
|
preMouseDownCallback(evt: EventTypes.MouseDownActivateEventType): Promise<boolean>;
|
|
13
17
|
protected getRemoveIslandData(growCutData: RegionSegmentPlusToolData): RemoveIslandData;
|
|
14
18
|
protected getGrowCutLabelmap(growCutData: any): Promise<Types.IImageVolume>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cache, utilities as csUtils, getEnabledElement, } from '@cornerstonejs/core';
|
|
2
2
|
import { growCut } from '../../utilities/segmentation';
|
|
3
3
|
import GrowCutBaseTool from '../base/GrowCutBaseTool';
|
|
4
|
+
import { calculateGrowCutSeeds } from '../../utilities/segmentation/growCut/runOneClickGrowCut';
|
|
4
5
|
class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
5
6
|
static { this.toolName = 'RegionSegmentPlus'; }
|
|
6
7
|
constructor(toolProps = {}, defaultToolProps = {
|
|
@@ -16,10 +17,68 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
16
17
|
},
|
|
17
18
|
}) {
|
|
18
19
|
super(toolProps, defaultToolProps);
|
|
20
|
+
this.mouseTimer = null;
|
|
21
|
+
this.allowedToProceed = false;
|
|
22
|
+
}
|
|
23
|
+
mouseMoveCallback(evt) {
|
|
24
|
+
const eventData = evt.detail;
|
|
25
|
+
const { currentPoints, element } = eventData;
|
|
26
|
+
const { world: worldPoint } = currentPoints;
|
|
27
|
+
element.style.cursor = 'default';
|
|
28
|
+
if (this.mouseTimer !== null) {
|
|
29
|
+
window.clearTimeout(this.mouseTimer);
|
|
30
|
+
this.mouseTimer = null;
|
|
31
|
+
}
|
|
32
|
+
this.mouseTimer = window.setTimeout(() => {
|
|
33
|
+
this.onMouseStable(evt, worldPoint, element);
|
|
34
|
+
}, this.configuration.mouseStabilityDelay || 500);
|
|
35
|
+
}
|
|
36
|
+
async onMouseStable(evt, worldPoint, element) {
|
|
37
|
+
await super.preMouseDownCallback(evt);
|
|
38
|
+
const refVolume = cache.getVolume(this.growCutData.segmentation.referencedVolumeId);
|
|
39
|
+
const seeds = calculateGrowCutSeeds(refVolume, worldPoint, {});
|
|
40
|
+
const { positiveSeedIndices, negativeSeedIndices } = seeds;
|
|
41
|
+
let cursor;
|
|
42
|
+
if (positiveSeedIndices.size / negativeSeedIndices.size > 20 ||
|
|
43
|
+
negativeSeedIndices.size < 30) {
|
|
44
|
+
cursor = 'not-allowed';
|
|
45
|
+
this.allowedToProceed = false;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
cursor = 'copy';
|
|
49
|
+
this.allowedToProceed = true;
|
|
50
|
+
}
|
|
51
|
+
const enabledElement = getEnabledElement(element);
|
|
52
|
+
if (element) {
|
|
53
|
+
element.style.cursor = cursor;
|
|
54
|
+
requestAnimationFrame(() => {
|
|
55
|
+
if (element.style.cursor !== cursor) {
|
|
56
|
+
element.style.cursor = cursor;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (this.allowedToProceed) {
|
|
61
|
+
this.seeds = seeds;
|
|
62
|
+
}
|
|
63
|
+
if (enabledElement && enabledElement.viewport) {
|
|
64
|
+
enabledElement.viewport.render();
|
|
65
|
+
}
|
|
19
66
|
}
|
|
20
67
|
async preMouseDownCallback(evt) {
|
|
68
|
+
if (!this.allowedToProceed) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
21
71
|
const eventData = evt.detail;
|
|
22
|
-
const { currentPoints } = eventData;
|
|
72
|
+
const { currentPoints, element } = eventData;
|
|
73
|
+
const enabledElement = getEnabledElement(element);
|
|
74
|
+
if (enabledElement) {
|
|
75
|
+
element.style.cursor = 'wait';
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
if (element.style.cursor !== 'wait') {
|
|
78
|
+
element.style.cursor = 'wait';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
23
82
|
const { world: worldPoint } = currentPoints;
|
|
24
83
|
await super.preMouseDownCallback(evt);
|
|
25
84
|
this.growCutData = csUtils.deepMerge(this.growCutData, {
|
|
@@ -32,7 +91,10 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
32
91
|
this.growCutData.islandRemoval = {
|
|
33
92
|
worldIslandPoints: [worldPoint],
|
|
34
93
|
};
|
|
35
|
-
this.runGrowCut();
|
|
94
|
+
await this.runGrowCut();
|
|
95
|
+
if (element) {
|
|
96
|
+
element.style.cursor = 'default';
|
|
97
|
+
}
|
|
36
98
|
return true;
|
|
37
99
|
}
|
|
38
100
|
getRemoveIslandData(growCutData) {
|
|
@@ -42,13 +104,12 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
|
|
|
42
104
|
};
|
|
43
105
|
}
|
|
44
106
|
async getGrowCutLabelmap(growCutData) {
|
|
45
|
-
const { segmentation: { referencedVolumeId
|
|
46
|
-
const renderingEngine = getRenderingEngine(renderingEngineId);
|
|
47
|
-
const viewport = renderingEngine.getViewport(viewportId);
|
|
107
|
+
const { segmentation: { referencedVolumeId }, worldPoint, options, } = growCutData;
|
|
48
108
|
const { subVolumePaddingPercentage } = this.configuration;
|
|
49
109
|
const mergedOptions = {
|
|
50
110
|
...options,
|
|
51
111
|
subVolumePaddingPercentage,
|
|
112
|
+
seeds: this.seeds,
|
|
52
113
|
};
|
|
53
114
|
return growCut.runOneClickGrowCut({
|
|
54
115
|
referencedVolumeId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Types } from '@cornerstonejs/core';
|
|
2
2
|
import { BaseTool } from '../base';
|
|
3
3
|
import type { EventTypes, PublicToolProps, ToolProps } from '../../types';
|
|
4
|
+
import type { GrowCutOneClickOptions } from '../../utilities/segmentation/growCut/runOneClickGrowCut';
|
|
4
5
|
type GrowCutToolData = {
|
|
5
6
|
metadata: Types.ViewReference & {
|
|
6
7
|
viewUp?: Types.Point3;
|
|
@@ -16,6 +17,7 @@ type GrowCutToolData = {
|
|
|
16
17
|
};
|
|
17
18
|
viewportId: string;
|
|
18
19
|
renderingEngineId: string;
|
|
20
|
+
options?: Partial<GrowCutOneClickOptions>;
|
|
19
21
|
};
|
|
20
22
|
type RemoveIslandData = {
|
|
21
23
|
worldIslandPoints?: Types.Point3[];
|
|
@@ -26,6 +28,10 @@ declare class GrowCutBaseTool extends BaseTool {
|
|
|
26
28
|
static toolName: any;
|
|
27
29
|
protected growCutData: GrowCutToolData | null;
|
|
28
30
|
private static lastGrowCutCommand;
|
|
31
|
+
protected seeds: {
|
|
32
|
+
positiveSeedIndices: Set<number>;
|
|
33
|
+
negativeSeedIndices: Set<number>;
|
|
34
|
+
} | null;
|
|
29
35
|
constructor(toolProps: PublicToolProps, defaultToolProps: ToolProps);
|
|
30
36
|
preMouseDownCallback(evt: EventTypes.MouseDownActivateEventType): Promise<boolean>;
|
|
31
37
|
shrink(): void;
|
|
@@ -3,6 +3,7 @@ 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
|
+
import { DEFAULT_POSITIVE_STD_DEV_MULTIPLIER, DEFAULT_NEGATIVE_SEED_MARGIN, } from '../../utilities/segmentation/growCut/constants';
|
|
6
7
|
import { getSVGStyleForSegment } from '../../utilities/segmentation/getSVGStyleForSegment';
|
|
7
8
|
import IslandRemoval from '../../utilities/segmentation/islandRemoval';
|
|
8
9
|
import { getOrCreateSegmentationVolume } from '../../utilities/segmentation';
|
|
@@ -13,9 +14,8 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
13
14
|
constructor(toolProps, defaultToolProps) {
|
|
14
15
|
const baseToolProps = csUtils.deepMerge({
|
|
15
16
|
configuration: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
shrinkExpandIncrement: 0.05,
|
|
17
|
+
positiveStdDevMultiplier: DEFAULT_POSITIVE_STD_DEV_MULTIPLIER,
|
|
18
|
+
shrinkExpandIncrement: 0.1,
|
|
19
19
|
islandRemoval: {
|
|
20
20
|
enabled: false,
|
|
21
21
|
},
|
|
@@ -70,39 +70,38 @@ class GrowCutBaseTool extends BaseTool {
|
|
|
70
70
|
async runGrowCut() {
|
|
71
71
|
const { growCutData, configuration: config } = this;
|
|
72
72
|
const { segmentation: { segmentationId, segmentIndex, labelmapVolumeId }, } = growCutData;
|
|
73
|
-
const hasSeedVarianceData = config.positiveSeedVariance !== undefined &&
|
|
74
|
-
config.negativeSeedVariance !== undefined;
|
|
75
73
|
const labelmap = cache.getVolume(labelmapVolumeId);
|
|
76
|
-
let
|
|
74
|
+
let shrinkExpandAccumulator = 0;
|
|
77
75
|
const growCutCommand = async ({ shrinkExpandAmount = 0 } = {}) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
let newNegativeSeedVariance = undefined;
|
|
81
|
-
shrinkExpandValue += shrinkExpandAmount;
|
|
82
|
-
if (hasSeedVarianceData) {
|
|
83
|
-
newPositiveSeedVariance = positiveSeedVariance + shrinkExpandValue;
|
|
84
|
-
newNegativeSeedVariance = negativeSeedVariance + shrinkExpandValue;
|
|
76
|
+
if (shrinkExpandAmount !== 0) {
|
|
77
|
+
this.seeds = null;
|
|
85
78
|
}
|
|
86
|
-
|
|
79
|
+
shrinkExpandAccumulator += shrinkExpandAmount;
|
|
80
|
+
const newPositiveStdDevMultiplier = Math.max(0.1, config.positiveStdDevMultiplier + shrinkExpandAccumulator);
|
|
81
|
+
const negativeSeedMargin = shrinkExpandAmount < 0
|
|
82
|
+
? Math.max(1, DEFAULT_NEGATIVE_SEED_MARGIN -
|
|
83
|
+
Math.abs(shrinkExpandAccumulator) * 3)
|
|
84
|
+
: DEFAULT_NEGATIVE_SEED_MARGIN + shrinkExpandAccumulator * 3;
|
|
85
|
+
const updatedGrowCutData = {
|
|
86
|
+
...growCutData,
|
|
87
87
|
options: {
|
|
88
|
+
...(growCutData.options || {}),
|
|
88
89
|
positiveSeedValue: segmentIndex,
|
|
89
90
|
negativeSeedValue: 255,
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
positiveStdDevMultiplier: newPositiveStdDevMultiplier,
|
|
92
|
+
negativeSeedMargin,
|
|
92
93
|
},
|
|
93
|
-
}
|
|
94
|
+
};
|
|
94
95
|
const growcutLabelmap = await this.getGrowCutLabelmap(updatedGrowCutData);
|
|
95
96
|
const { isPartialVolume } = config;
|
|
96
97
|
const fn = isPartialVolume
|
|
97
98
|
? this.applyPartialGrowCutLabelmap
|
|
98
99
|
: this.applyGrowCutLabelmap;
|
|
99
100
|
fn(segmentationId, segmentIndex, labelmap, growcutLabelmap);
|
|
100
|
-
this._removeIslands(
|
|
101
|
+
this._removeIslands(updatedGrowCutData);
|
|
101
102
|
};
|
|
102
103
|
await growCutCommand();
|
|
103
|
-
|
|
104
|
-
GrowCutBaseTool.lastGrowCutCommand = growCutCommand;
|
|
105
|
-
}
|
|
104
|
+
GrowCutBaseTool.lastGrowCutCommand = growCutCommand;
|
|
106
105
|
this.growCutData = null;
|
|
107
106
|
}
|
|
108
107
|
applyPartialGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const POSITIVE_SEED_LABEL = 254;
|
|
2
|
+
export declare const NEGATIVE_SEED_LABEL = 255;
|
|
3
|
+
export declare const DEFAULT_NEIGHBORHOOD_RADIUS = 1;
|
|
4
|
+
export declare const DEFAULT_POSITIVE_STD_DEV_MULTIPLIER = 1.8;
|
|
5
|
+
export declare const DEFAULT_NEGATIVE_STD_DEV_MULTIPLIER = 3.2;
|
|
6
|
+
export declare const DEFAULT_NEGATIVE_SEED_MARGIN = 30;
|
|
7
|
+
export declare const DEFAULT_NEGATIVE_SEEDS_COUNT = 70;
|
|
8
|
+
export declare const MAX_NEGATIVE_SEED_ATTEMPTS_MULTIPLIER = 50;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const POSITIVE_SEED_LABEL = 254;
|
|
2
|
+
export const NEGATIVE_SEED_LABEL = 255;
|
|
3
|
+
export const DEFAULT_NEIGHBORHOOD_RADIUS = 1;
|
|
4
|
+
export const DEFAULT_POSITIVE_STD_DEV_MULTIPLIER = 1.8;
|
|
5
|
+
export const DEFAULT_NEGATIVE_STD_DEV_MULTIPLIER = 3.2;
|
|
6
|
+
export const DEFAULT_NEGATIVE_SEED_MARGIN = 30;
|
|
7
|
+
export const DEFAULT_NEGATIVE_SEEDS_COUNT = 70;
|
|
8
|
+
export const MAX_NEGATIVE_SEED_ATTEMPTS_MULTIPLIER = 50;
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import type { Types } from '@cornerstonejs/core';
|
|
2
2
|
import type { GrowCutOptions } from './runGrowCut';
|
|
3
3
|
type GrowCutOneClickOptions = GrowCutOptions & {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
initialNeighborhoodRadius?: number;
|
|
5
|
+
positiveStdDevMultiplier?: number;
|
|
6
|
+
negativeStdDevMultiplier?: number;
|
|
6
7
|
negativeSeedMargin?: number;
|
|
7
|
-
|
|
8
|
+
negativeSeedsTargetPatches?: number;
|
|
9
|
+
positiveSeedValue?: number;
|
|
10
|
+
negativeSeedValue?: number;
|
|
11
|
+
seeds?: {
|
|
12
|
+
positiveSeedIndices: Set<number>;
|
|
13
|
+
negativeSeedIndices: Set<number>;
|
|
14
|
+
};
|
|
8
15
|
};
|
|
16
|
+
declare function calculateGrowCutSeeds(referencedVolume: Types.IImageVolume, worldPosition: Types.Point3, options?: GrowCutOneClickOptions): {
|
|
17
|
+
positiveSeedIndices: Set<number>;
|
|
18
|
+
negativeSeedIndices: Set<number>;
|
|
19
|
+
} | null;
|
|
9
20
|
declare function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }: {
|
|
10
21
|
referencedVolumeId: string;
|
|
11
22
|
worldPosition: Types.Point3;
|
|
12
23
|
options?: GrowCutOneClickOptions;
|
|
13
|
-
}): Promise<Types.IImageVolume>;
|
|
14
|
-
export { runOneClickGrowCut as default, runOneClickGrowCut };
|
|
24
|
+
}): Promise<Types.IImageVolume | null>;
|
|
25
|
+
export { runOneClickGrowCut as default, runOneClickGrowCut, calculateGrowCutSeeds, };
|
|
15
26
|
export type { GrowCutOneClickOptions };
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import { utilities as csUtils, cache, volumeLoader } from '@cornerstonejs/core';
|
|
2
2
|
import { run } from './runGrowCut';
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
3
|
+
import { POSITIVE_SEED_LABEL, NEGATIVE_SEED_LABEL, DEFAULT_NEIGHBORHOOD_RADIUS, DEFAULT_POSITIVE_STD_DEV_MULTIPLIER, DEFAULT_NEGATIVE_STD_DEV_MULTIPLIER, DEFAULT_NEGATIVE_SEED_MARGIN, DEFAULT_NEGATIVE_SEEDS_COUNT, MAX_NEGATIVE_SEED_ATTEMPTS_MULTIPLIER, } from './constants';
|
|
4
|
+
const { transformWorldToIndex } = csUtils;
|
|
5
|
+
const MAX_POSITIVE_SEEDS = 100000;
|
|
6
|
+
function calculateGrowCutSeeds(referencedVolume, worldPosition, options) {
|
|
7
|
+
const { dimensions, imageData: refImageData } = referencedVolume;
|
|
8
|
+
const [width, height, numSlices] = dimensions;
|
|
9
|
+
const referenceVolumeVoxelManager = referencedVolume.voxelManager;
|
|
10
|
+
const scalarData = referenceVolumeVoxelManager.getCompleteScalarDataArray();
|
|
11
11
|
const numPixelsPerSlice = width * height;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
const neighborhoodRadius = options?.initialNeighborhoodRadius ?? DEFAULT_NEIGHBORHOOD_RADIUS;
|
|
13
|
+
const positiveK = options?.positiveStdDevMultiplier ?? DEFAULT_POSITIVE_STD_DEV_MULTIPLIER;
|
|
14
|
+
const negativeK = options?.negativeStdDevMultiplier ?? DEFAULT_NEGATIVE_STD_DEV_MULTIPLIER;
|
|
15
|
+
const negativeSeedMargin = options?.negativeSeedMargin ?? DEFAULT_NEGATIVE_SEED_MARGIN;
|
|
16
|
+
const negativeSeedsTargetPatches = options?.negativeSeedsTargetPatches ?? DEFAULT_NEGATIVE_SEEDS_COUNT;
|
|
17
|
+
const ijkStart = transformWorldToIndex(refImageData, worldPosition).map(Math.round);
|
|
18
|
+
const startIndex = referenceVolumeVoxelManager.toIndex(ijkStart);
|
|
19
|
+
if (ijkStart[0] < 0 ||
|
|
20
|
+
ijkStart[0] >= width ||
|
|
21
|
+
ijkStart[1] < 0 ||
|
|
22
|
+
ijkStart[1] >= height ||
|
|
23
|
+
ijkStart[2] < 0 ||
|
|
24
|
+
ijkStart[2] >= numSlices) {
|
|
25
|
+
console.warn('Click position is outside volume bounds.');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const initialStats = csUtils.calculateNeighborhoodStats(scalarData, dimensions, ijkStart, neighborhoodRadius);
|
|
29
|
+
if (initialStats.count === 0) {
|
|
30
|
+
initialStats.mean = scalarData[startIndex];
|
|
31
|
+
initialStats.stdDev = 0;
|
|
32
|
+
}
|
|
33
|
+
const positiveIntensityMin = initialStats.mean - positiveK * initialStats.stdDev;
|
|
34
|
+
const positiveIntensityMax = initialStats.mean + positiveK * initialStats.stdDev;
|
|
27
35
|
const neighborsCoordDelta = [
|
|
28
36
|
[-1, 0, 0],
|
|
29
37
|
[1, 0, 0],
|
|
@@ -32,18 +40,33 @@ function _generateSeeds(referencedVolume, labelmap, worldPosition, options) {
|
|
|
32
40
|
[0, 0, -1],
|
|
33
41
|
[0, 0, 1],
|
|
34
42
|
];
|
|
35
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const queue = [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
44
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
45
|
+
const positiveSeedIndices = new Set();
|
|
46
|
+
const queue = [];
|
|
47
|
+
const startValue = scalarData[startIndex];
|
|
48
|
+
if (startValue >= positiveIntensityMin &&
|
|
49
|
+
startValue <= positiveIntensityMax) {
|
|
50
|
+
positiveSeedIndices.add(startIndex);
|
|
51
|
+
queue.push(ijkStart);
|
|
52
|
+
minX = maxX = ijkStart[0];
|
|
53
|
+
minY = maxY = ijkStart[1];
|
|
54
|
+
minZ = maxZ = ijkStart[2];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.warn('Clicked voxel intensity is outside the calculated positive range. No positive seeds generated.');
|
|
58
|
+
return { positiveSeedIndices: new Set(), negativeSeedIndices: new Set() };
|
|
59
|
+
}
|
|
60
|
+
let currentQueueIndex = 0;
|
|
61
|
+
while (currentQueueIndex < queue.length &&
|
|
62
|
+
positiveSeedIndices.size < MAX_POSITIVE_SEEDS) {
|
|
63
|
+
const [x, y, z] = queue[currentQueueIndex++];
|
|
64
|
+
minX = Math.min(x, minX);
|
|
65
|
+
minY = Math.min(y, minY);
|
|
66
|
+
minZ = Math.min(z, minZ);
|
|
67
|
+
maxX = Math.max(x, maxX);
|
|
68
|
+
maxY = Math.max(y, maxY);
|
|
69
|
+
maxZ = Math.max(z, maxZ);
|
|
47
70
|
for (let i = 0; i < neighborsCoordDelta.length; i++) {
|
|
48
71
|
const [dx, dy, dz] = neighborsCoordDelta[i];
|
|
49
72
|
const nx = x + dx;
|
|
@@ -57,52 +80,122 @@ function _generateSeeds(referencedVolume, labelmap, worldPosition, options) {
|
|
|
57
80
|
nz >= numSlices) {
|
|
58
81
|
continue;
|
|
59
82
|
}
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
83
|
+
const neighborIndex = nz * numPixelsPerSlice + ny * width + nx;
|
|
84
|
+
if (positiveSeedIndices.has(neighborIndex)) {
|
|
62
85
|
continue;
|
|
63
86
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
const neighborValue = scalarData[neighborIndex];
|
|
88
|
+
if (neighborValue >= positiveIntensityMin &&
|
|
89
|
+
neighborValue <= positiveIntensityMax) {
|
|
90
|
+
positiveSeedIndices.add(neighborIndex);
|
|
91
|
+
if (positiveSeedIndices.size < MAX_POSITIVE_SEEDS) {
|
|
92
|
+
queue.push([nx, ny, nz]);
|
|
93
|
+
}
|
|
70
94
|
}
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
if (positiveSeedIndices.size >= MAX_POSITIVE_SEEDS) {
|
|
98
|
+
console.debug(`Reached maximum number of positive seeds (${MAX_POSITIVE_SEEDS}). Stopping BFS.`);
|
|
99
|
+
}
|
|
100
|
+
if (positiveSeedIndices.size === 0) {
|
|
101
|
+
console.warn('No positive seeds found after BFS.');
|
|
102
|
+
return { positiveSeedIndices: new Set(), negativeSeedIndices: new Set() };
|
|
103
|
+
}
|
|
104
|
+
let positiveSum = 0;
|
|
105
|
+
let positiveSumSq = 0;
|
|
106
|
+
positiveSeedIndices.forEach((index) => {
|
|
107
|
+
const value = scalarData[index];
|
|
108
|
+
positiveSum += value;
|
|
109
|
+
positiveSumSq += value * value;
|
|
110
|
+
});
|
|
111
|
+
const positiveCount = positiveSeedIndices.size;
|
|
112
|
+
const positiveMean = positiveSum / positiveCount;
|
|
113
|
+
const positiveVariance = positiveSumSq / positiveCount - positiveMean * positiveMean;
|
|
114
|
+
const positiveStdDev = Math.sqrt(Math.max(0, positiveVariance));
|
|
115
|
+
const negativeDiffThreshold = negativeK * positiveStdDev;
|
|
116
|
+
const minXm = Math.max(0, minX - negativeSeedMargin);
|
|
117
|
+
const minYm = Math.max(0, minY - negativeSeedMargin);
|
|
118
|
+
const minZm = Math.max(0, minZ - negativeSeedMargin);
|
|
119
|
+
const maxXm = Math.min(width - 1, maxX + negativeSeedMargin);
|
|
120
|
+
const maxYm = Math.min(height - 1, maxY + negativeSeedMargin);
|
|
121
|
+
const maxZm = Math.min(numSlices - 1, maxZ + negativeSeedMargin);
|
|
122
|
+
const negativeSeedIndices = new Set();
|
|
82
123
|
let attempts = 0;
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
let patchesAdded = 0;
|
|
125
|
+
const maxAttempts = negativeSeedsTargetPatches * MAX_NEGATIVE_SEED_ATTEMPTS_MULTIPLIER;
|
|
126
|
+
while (patchesAdded < negativeSeedsTargetPatches && attempts < maxAttempts) {
|
|
85
127
|
attempts++;
|
|
86
|
-
const rx = Math.floor(Math.random() * (
|
|
87
|
-
const ry = Math.floor(Math.random() * (
|
|
88
|
-
const rz = Math.floor(Math.random() * (
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
128
|
+
const rx = Math.floor(Math.random() * (maxXm - minXm + 1) + minXm);
|
|
129
|
+
const ry = Math.floor(Math.random() * (maxYm - minYm + 1) + minYm);
|
|
130
|
+
const rz = Math.floor(Math.random() * (maxZm - minZm + 1) + minZm);
|
|
131
|
+
const centerIndex = rz * numPixelsPerSlice + ry * width + rx;
|
|
132
|
+
if (positiveSeedIndices.has(centerIndex) ||
|
|
133
|
+
negativeSeedIndices.has(centerIndex)) {
|
|
91
134
|
continue;
|
|
92
135
|
}
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
const centerValue = scalarData[centerIndex];
|
|
137
|
+
if (Math.abs(centerValue - positiveMean) > negativeDiffThreshold) {
|
|
138
|
+
let patchContributed = false;
|
|
139
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
140
|
+
const ny = ry + dy;
|
|
141
|
+
if (ny < 0 || ny >= height) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
145
|
+
const nx = rx + dx;
|
|
146
|
+
if (nx < 0 || nx >= width) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const neighborIndex = rz * numPixelsPerSlice + ny * width + nx;
|
|
150
|
+
if (positiveSeedIndices.has(neighborIndex) ||
|
|
151
|
+
negativeSeedIndices.has(neighborIndex)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
negativeSeedIndices.add(neighborIndex);
|
|
155
|
+
patchContributed = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (patchContributed) {
|
|
159
|
+
patchesAdded++;
|
|
160
|
+
}
|
|
98
161
|
}
|
|
99
162
|
}
|
|
163
|
+
if (negativeSeedIndices.size === 0) {
|
|
164
|
+
console.warn('Could not find any negative seeds. GrowCut might fail or produce poor results.');
|
|
165
|
+
}
|
|
166
|
+
console.debug('positiveSeedIndices', positiveSeedIndices.size);
|
|
167
|
+
console.debug('negativeSeedIndices', negativeSeedIndices.size);
|
|
168
|
+
return { positiveSeedIndices, negativeSeedIndices };
|
|
100
169
|
}
|
|
101
170
|
async function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }) {
|
|
102
171
|
const referencedVolume = cache.getVolume(referencedVolumeId);
|
|
103
172
|
const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(referencedVolumeId);
|
|
104
|
-
|
|
105
|
-
|
|
173
|
+
labelmap.voxelManager.forEach(({ index, value }) => {
|
|
174
|
+
if (value !== 0) {
|
|
175
|
+
labelmap.voxelManager.setAtIndex(index, 0);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const seeds = options.seeds ??
|
|
179
|
+
calculateGrowCutSeeds(referencedVolume, worldPosition, options);
|
|
180
|
+
const positiveSeedLabel = options?.positiveSeedValue ?? POSITIVE_SEED_LABEL;
|
|
181
|
+
const negativeSeedLabel = options?.negativeSeedValue ?? NEGATIVE_SEED_LABEL;
|
|
182
|
+
if (!seeds) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const { positiveSeedIndices, negativeSeedIndices } = seeds;
|
|
186
|
+
if (positiveSeedIndices.size < 10 ||
|
|
187
|
+
positiveSeedIndices.size > MAX_POSITIVE_SEEDS ||
|
|
188
|
+
negativeSeedIndices.size < 10) {
|
|
189
|
+
console.warn('Not enough seeds found. GrowCut might fail or produce poor results.');
|
|
190
|
+
return labelmap;
|
|
191
|
+
}
|
|
192
|
+
positiveSeedIndices.forEach((index) => {
|
|
193
|
+
labelmap.voxelManager.setAtIndex(index, positiveSeedLabel);
|
|
194
|
+
});
|
|
195
|
+
negativeSeedIndices.forEach((index) => {
|
|
196
|
+
labelmap.voxelManager.setAtIndex(index, negativeSeedLabel);
|
|
197
|
+
});
|
|
198
|
+
await run(referencedVolumeId, labelmap.volumeId, options);
|
|
106
199
|
return labelmap;
|
|
107
200
|
}
|
|
108
|
-
export { runOneClickGrowCut as default, runOneClickGrowCut };
|
|
201
|
+
export { runOneClickGrowCut as default, runOneClickGrowCut, calculateGrowCutSeeds, };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cornerstonejs/tools",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.2",
|
|
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": "^3.1.0"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
|
-
"@cornerstonejs/core": "^3.8.
|
|
106
|
+
"@cornerstonejs/core": "^3.8.2",
|
|
107
107
|
"@kitware/vtk.js": "32.12.1",
|
|
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": "d14852bdc9402b62ca685c559663438c19079e07"
|
|
126
126
|
}
|