@cornerstonejs/tools 2.6.5 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/stateManagement/segmentation/polySeg/Labelmap/labelmapComputationStrategies.js +2 -1
- package/dist/esm/tools/annotation/LengthTool.js +6 -3
- package/dist/esm/tools/annotation/RegionSegmentPlusTool.d.ts +15 -0
- package/dist/esm/tools/annotation/RegionSegmentPlusTool.js +40 -0
- package/dist/esm/tools/annotation/RegionSegmentTool.d.ts +21 -0
- package/dist/esm/tools/annotation/RegionSegmentTool.js +103 -0
- package/dist/esm/tools/annotation/WholeBodySegmentTool.d.ts +28 -0
- package/dist/esm/tools/annotation/WholeBodySegmentTool.js +188 -0
- package/dist/esm/tools/base/ContourSegmentationBaseTool.js +5 -46
- package/dist/esm/tools/base/GrowCutBaseTool.d.ts +49 -0
- package/dist/esm/tools/base/GrowCutBaseTool.js +106 -0
- package/dist/esm/tools/index.d.ts +4 -1
- package/dist/esm/tools/index.js +4 -1
- package/dist/esm/tools/segmentation/strategies/fillSphere.js +2 -2
- package/dist/esm/utilities/getSphereBoundsInfo.d.ts +5 -2
- package/dist/esm/utilities/getSphereBoundsInfo.js +36 -13
- package/dist/esm/utilities/segmentation/createLabelmapVolumeForViewport.js +1 -1
- package/dist/esm/utilities/segmentation/getSVGStyleForSegment.d.ts +17 -0
- package/dist/esm/utilities/segmentation/getSVGStyleForSegment.js +68 -0
- package/dist/esm/utilities/segmentation/growCut/growCutShader.d.ts +2 -0
- package/dist/esm/utilities/segmentation/growCut/growCutShader.js +106 -0
- package/dist/esm/utilities/segmentation/growCut/index.d.ts +7 -0
- package/dist/esm/utilities/segmentation/growCut/index.js +4 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCut.d.ts +16 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCut.js +269 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCutForBoundingBox.d.ts +17 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCutForBoundingBox.js +111 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCutForSphere.d.ts +9 -0
- package/dist/esm/utilities/segmentation/growCut/runGrowCutForSphere.js +164 -0
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.d.ts +13 -0
- package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.js +176 -0
- package/dist/esm/utilities/segmentation/index.d.ts +2 -1
- package/dist/esm/utilities/segmentation/index.js +2 -1
- package/package.json +3 -3
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { cache } from '@cornerstonejs/core';
|
|
2
|
+
import shaderCode from './growCutShader';
|
|
3
|
+
const GB = 1024 * 1024 * 1024;
|
|
4
|
+
const WEBGPU_MEMORY_LIMIT = 2 * GB;
|
|
5
|
+
const DEFAULT_GROWCUT_OPTIONS = {
|
|
6
|
+
windowSize: 3,
|
|
7
|
+
maxProcessingTime: 30000,
|
|
8
|
+
inspection: {
|
|
9
|
+
numCyclesInterval: 5,
|
|
10
|
+
numCyclesBelowThreshold: 3,
|
|
11
|
+
threshold: 1e-4,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT_GROWCUT_OPTIONS) {
|
|
15
|
+
const workGroupSize = [8, 8, 4];
|
|
16
|
+
const { windowSize, maxProcessingTime } = Object.assign({}, DEFAULT_GROWCUT_OPTIONS, options);
|
|
17
|
+
const inspection = Object.assign({}, DEFAULT_GROWCUT_OPTIONS.inspection, options.inspection);
|
|
18
|
+
const volume = cache.getVolume(referenceVolumeId);
|
|
19
|
+
const labelmap = cache.getVolume(labelmapVolumeId);
|
|
20
|
+
const [columns, rows, numSlices] = volume.dimensions;
|
|
21
|
+
if (labelmap.dimensions[0] !== columns ||
|
|
22
|
+
labelmap.dimensions[1] !== rows ||
|
|
23
|
+
labelmap.dimensions[2] !== numSlices) {
|
|
24
|
+
throw new Error('Volume and labelmap must have the same size');
|
|
25
|
+
}
|
|
26
|
+
const numIterations = Math.floor(Math.sqrt(rows ** 2 + columns ** 2 + numSlices ** 2) / 2);
|
|
27
|
+
const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
|
|
28
|
+
let volumePixelData = volume.voxelManager.getCompleteScalarDataArray();
|
|
29
|
+
if (!(volumePixelData instanceof Float32Array)) {
|
|
30
|
+
volumePixelData = new Float32Array(volumePixelData);
|
|
31
|
+
}
|
|
32
|
+
const requiredLimits = {
|
|
33
|
+
maxStorageBufferBindingSize: WEBGPU_MEMORY_LIMIT,
|
|
34
|
+
maxBufferSize: WEBGPU_MEMORY_LIMIT,
|
|
35
|
+
};
|
|
36
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
37
|
+
const device = await adapter.requestDevice({ requiredLimits });
|
|
38
|
+
const BUFFER_SIZE = volumePixelData.byteLength;
|
|
39
|
+
const UPDATED_VOXELS_COUNTER_BUFFER_SIZE = numIterations * Uint32Array.BYTES_PER_ELEMENT;
|
|
40
|
+
const shaderModule = device.createShaderModule({
|
|
41
|
+
code: shaderCode,
|
|
42
|
+
});
|
|
43
|
+
const numIterationIndex = 3;
|
|
44
|
+
const paramsArrayValues = new Uint32Array([
|
|
45
|
+
columns,
|
|
46
|
+
rows,
|
|
47
|
+
numSlices,
|
|
48
|
+
0,
|
|
49
|
+
]);
|
|
50
|
+
const gpuParamsBuffer = device.createBuffer({
|
|
51
|
+
size: paramsArrayValues.byteLength,
|
|
52
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
53
|
+
});
|
|
54
|
+
const gpuVolumePixelDataBuffer = device.createBuffer({
|
|
55
|
+
size: BUFFER_SIZE,
|
|
56
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
57
|
+
});
|
|
58
|
+
device.queue.writeBuffer(gpuVolumePixelDataBuffer, 0, volumePixelData);
|
|
59
|
+
const gpuLabelmapBuffers = [0, 1].map(() => device.createBuffer({
|
|
60
|
+
size: BUFFER_SIZE,
|
|
61
|
+
usage: GPUBufferUsage.STORAGE |
|
|
62
|
+
GPUBufferUsage.COPY_SRC |
|
|
63
|
+
GPUBufferUsage.COPY_DST,
|
|
64
|
+
}));
|
|
65
|
+
device.queue.writeBuffer(gpuLabelmapBuffers[0], 0, new Uint32Array(labelmapData));
|
|
66
|
+
const gpuStrengthBuffers = [0, 1].map(() => {
|
|
67
|
+
const strengthBuffer = device.createBuffer({
|
|
68
|
+
size: BUFFER_SIZE,
|
|
69
|
+
usage: GPUBufferUsage.STORAGE |
|
|
70
|
+
GPUBufferUsage.COPY_SRC |
|
|
71
|
+
GPUBufferUsage.COPY_DST,
|
|
72
|
+
});
|
|
73
|
+
return strengthBuffer;
|
|
74
|
+
});
|
|
75
|
+
const gpuCounterBuffer = device.createBuffer({
|
|
76
|
+
size: UPDATED_VOXELS_COUNTER_BUFFER_SIZE,
|
|
77
|
+
usage: GPUBufferUsage.STORAGE |
|
|
78
|
+
GPUBufferUsage.COPY_SRC |
|
|
79
|
+
GPUBufferUsage.COPY_DST,
|
|
80
|
+
});
|
|
81
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
82
|
+
entries: [
|
|
83
|
+
{
|
|
84
|
+
binding: 0,
|
|
85
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
86
|
+
buffer: {
|
|
87
|
+
type: 'uniform',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
binding: 1,
|
|
92
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
93
|
+
buffer: {
|
|
94
|
+
type: 'read-only-storage',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
binding: 2,
|
|
99
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
100
|
+
buffer: {
|
|
101
|
+
type: 'storage',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
binding: 3,
|
|
106
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
107
|
+
buffer: {
|
|
108
|
+
type: 'storage',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
binding: 4,
|
|
113
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
114
|
+
buffer: {
|
|
115
|
+
type: 'read-only-storage',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
binding: 5,
|
|
120
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
121
|
+
buffer: {
|
|
122
|
+
type: 'read-only-storage',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
binding: 6,
|
|
127
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
128
|
+
buffer: {
|
|
129
|
+
type: 'storage',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
const bindGroups = [0, 1].map((i) => {
|
|
135
|
+
const outputLabelmapBuffer = gpuLabelmapBuffers[i];
|
|
136
|
+
const outputStrengthBuffer = gpuStrengthBuffers[i];
|
|
137
|
+
const previouLabelmapBuffer = gpuLabelmapBuffers[(i + 1) % 2];
|
|
138
|
+
const previousStrengthBuffer = gpuStrengthBuffers[(i + 1) % 2];
|
|
139
|
+
return device.createBindGroup({
|
|
140
|
+
layout: bindGroupLayout,
|
|
141
|
+
entries: [
|
|
142
|
+
{
|
|
143
|
+
binding: 0,
|
|
144
|
+
resource: {
|
|
145
|
+
buffer: gpuParamsBuffer,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
binding: 1,
|
|
150
|
+
resource: {
|
|
151
|
+
buffer: gpuVolumePixelDataBuffer,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
binding: 2,
|
|
156
|
+
resource: {
|
|
157
|
+
buffer: outputLabelmapBuffer,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
binding: 3,
|
|
162
|
+
resource: {
|
|
163
|
+
buffer: outputStrengthBuffer,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
binding: 4,
|
|
168
|
+
resource: {
|
|
169
|
+
buffer: previouLabelmapBuffer,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
binding: 5,
|
|
174
|
+
resource: {
|
|
175
|
+
buffer: previousStrengthBuffer,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
binding: 6,
|
|
180
|
+
resource: {
|
|
181
|
+
buffer: gpuCounterBuffer,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
const pipeline = device.createComputePipeline({
|
|
188
|
+
layout: device.createPipelineLayout({
|
|
189
|
+
bindGroupLayouts: [bindGroupLayout],
|
|
190
|
+
}),
|
|
191
|
+
compute: {
|
|
192
|
+
module: shaderModule,
|
|
193
|
+
entryPoint: 'main',
|
|
194
|
+
constants: {
|
|
195
|
+
workGroupSizeX: workGroupSize[0],
|
|
196
|
+
workGroupSizeY: workGroupSize[1],
|
|
197
|
+
workGroupSizeZ: workGroupSize[2],
|
|
198
|
+
windowSize,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const numWorkGroups = [
|
|
203
|
+
Math.ceil(columns / workGroupSize[0]),
|
|
204
|
+
Math.ceil(rows / workGroupSize[1]),
|
|
205
|
+
Math.ceil(numSlices / workGroupSize[2]),
|
|
206
|
+
];
|
|
207
|
+
const gpuUpdatedVoxelsCounterStagingBuffer = device.createBuffer({
|
|
208
|
+
size: UPDATED_VOXELS_COUNTER_BUFFER_SIZE,
|
|
209
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
210
|
+
});
|
|
211
|
+
const labelmapStagingBufferTemp = device.createBuffer({
|
|
212
|
+
size: BUFFER_SIZE,
|
|
213
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
214
|
+
});
|
|
215
|
+
const limitProcessingTime = maxProcessingTime
|
|
216
|
+
? performance.now() + maxProcessingTime
|
|
217
|
+
: 0;
|
|
218
|
+
let currentInspectionNumCyclesInterval = inspection.numCyclesInterval;
|
|
219
|
+
let belowThresholdCounter = 0;
|
|
220
|
+
for (let i = 0; i < numIterations; i++) {
|
|
221
|
+
paramsArrayValues[numIterationIndex] = i;
|
|
222
|
+
device.queue.writeBuffer(gpuParamsBuffer, 0, paramsArrayValues);
|
|
223
|
+
const commandEncoder = device.createCommandEncoder();
|
|
224
|
+
const passEncoder = commandEncoder.beginComputePass();
|
|
225
|
+
passEncoder.setPipeline(pipeline);
|
|
226
|
+
passEncoder.setBindGroup(0, bindGroups[i % 2]);
|
|
227
|
+
passEncoder.dispatchWorkgroups(numWorkGroups[0], numWorkGroups[1], numWorkGroups[2]);
|
|
228
|
+
passEncoder.end();
|
|
229
|
+
commandEncoder.copyBufferToBuffer(gpuCounterBuffer, i * Uint32Array.BYTES_PER_ELEMENT, gpuUpdatedVoxelsCounterStagingBuffer, i * Uint32Array.BYTES_PER_ELEMENT, Uint32Array.BYTES_PER_ELEMENT);
|
|
230
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
231
|
+
const inspect = i > 0 && !(i % currentInspectionNumCyclesInterval);
|
|
232
|
+
if (inspect) {
|
|
233
|
+
await gpuUpdatedVoxelsCounterStagingBuffer.mapAsync(GPUMapMode.READ, 0, UPDATED_VOXELS_COUNTER_BUFFER_SIZE);
|
|
234
|
+
const updatedVoxelsCounterResultBuffer = gpuUpdatedVoxelsCounterStagingBuffer.getMappedRange(0, UPDATED_VOXELS_COUNTER_BUFFER_SIZE);
|
|
235
|
+
const updatedVoxelsCounterBufferData = new Uint32Array(updatedVoxelsCounterResultBuffer.slice(0));
|
|
236
|
+
const updatedVoxelsRatio = updatedVoxelsCounterBufferData[i] / volumePixelData.length;
|
|
237
|
+
gpuUpdatedVoxelsCounterStagingBuffer.unmap();
|
|
238
|
+
if (i >= 1 && updatedVoxelsRatio < inspection.threshold) {
|
|
239
|
+
currentInspectionNumCyclesInterval = 1;
|
|
240
|
+
belowThresholdCounter++;
|
|
241
|
+
if (belowThresholdCounter === inspection.numCyclesBelowThreshold) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
currentInspectionNumCyclesInterval = inspection.numCyclesInterval;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (limitProcessingTime && performance.now() > limitProcessingTime) {
|
|
250
|
+
console.warn(`Exceeded processing time limit (${maxProcessingTime})ms`);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const commandEncoder = device.createCommandEncoder();
|
|
255
|
+
const outputLabelmapBufferIndex = (numIterations + 1) % 2;
|
|
256
|
+
const labelmapStagingBuffer = device.createBuffer({
|
|
257
|
+
size: BUFFER_SIZE,
|
|
258
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
259
|
+
});
|
|
260
|
+
commandEncoder.copyBufferToBuffer(gpuLabelmapBuffers[outputLabelmapBufferIndex], 0, labelmapStagingBuffer, 0, BUFFER_SIZE);
|
|
261
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
262
|
+
await labelmapStagingBuffer.mapAsync(GPUMapMode.READ, 0, BUFFER_SIZE);
|
|
263
|
+
const labelmapResultBuffer = labelmapStagingBuffer.getMappedRange(0, BUFFER_SIZE);
|
|
264
|
+
const labelmapResult = new Uint32Array(labelmapResultBuffer);
|
|
265
|
+
labelmapData.set(labelmapResult);
|
|
266
|
+
labelmapStagingBuffer.unmap();
|
|
267
|
+
labelmap.voxelManager.setCompleteScalarDataArray(labelmapData);
|
|
268
|
+
}
|
|
269
|
+
export { runGrowCut as default, runGrowCut as run };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import type { GrowCutOptions } from './runGrowCut';
|
|
3
|
+
type BoundingBoxInfo = {
|
|
4
|
+
boundingBox: {
|
|
5
|
+
ijkTopLeft: Types.Point3;
|
|
6
|
+
ijkBottomRight: Types.Point3;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
type GrowCutBoundingBoxOptions = GrowCutOptions & {
|
|
10
|
+
positiveSeedValue?: number;
|
|
11
|
+
negativeSeedValue?: number;
|
|
12
|
+
negativePixelRange: [number, number];
|
|
13
|
+
positivePixelRange: [number, number];
|
|
14
|
+
};
|
|
15
|
+
declare function runGrowCutForBoundingBox(referencedVolumeId: string, boundingBoxInfo: BoundingBoxInfo, options?: GrowCutBoundingBoxOptions): Promise<Types.IImageVolume>;
|
|
16
|
+
export { runGrowCutForBoundingBox as default, runGrowCutForBoundingBox };
|
|
17
|
+
export type { BoundingBoxInfo, GrowCutBoundingBoxOptions };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { volumeLoader, utilities as csUtils } from '@cornerstonejs/core';
|
|
2
|
+
import { run } from './runGrowCut';
|
|
3
|
+
const POSITIVE_SEED_VALUE = 254;
|
|
4
|
+
const NEGATIVE_SEED_VALUE = 255;
|
|
5
|
+
const NEGATIVE_PIXEL_RANGE = [-Infinity, -995];
|
|
6
|
+
const POSITIVE_PIXEL_RANGE = [0, 1900];
|
|
7
|
+
function _setNegativeSeedValues(subVolume, labelmap, options) {
|
|
8
|
+
const { negativeSeedValue = NEGATIVE_SEED_VALUE, negativePixelRange = NEGATIVE_PIXEL_RANGE, } = options;
|
|
9
|
+
const subVolPixelData = subVolume.voxelManager.getCompleteScalarDataArray();
|
|
10
|
+
const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
|
|
11
|
+
const [width, height, numSlices] = labelmap.dimensions;
|
|
12
|
+
const middleSliceIndex = Math.floor(numSlices / 2);
|
|
13
|
+
const visited = new Array(width * height).fill(false);
|
|
14
|
+
const sliceOffset = middleSliceIndex * width * height;
|
|
15
|
+
const bfs = (startX, startY) => {
|
|
16
|
+
const queue = [[startX, startY]];
|
|
17
|
+
while (queue.length) {
|
|
18
|
+
const [x, y] = queue.shift();
|
|
19
|
+
const slicePixelIndex = y * width + x;
|
|
20
|
+
if (x < 0 ||
|
|
21
|
+
x >= width ||
|
|
22
|
+
y < 0 ||
|
|
23
|
+
y >= height ||
|
|
24
|
+
visited[slicePixelIndex]) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
visited[slicePixelIndex] = true;
|
|
28
|
+
const volumeVoxelIndex = sliceOffset + slicePixelIndex;
|
|
29
|
+
const volumeVoxelValue = subVolPixelData[volumeVoxelIndex];
|
|
30
|
+
if (volumeVoxelValue < negativePixelRange[0] ||
|
|
31
|
+
volumeVoxelValue > negativePixelRange[1]) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
labelmap.voxelManager.setAtIndex(volumeVoxelIndex, negativeSeedValue);
|
|
35
|
+
queue.push([x - 1, y]);
|
|
36
|
+
queue.push([x + 1, y]);
|
|
37
|
+
queue.push([x, y - 1]);
|
|
38
|
+
queue.push([x, y + 1]);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const scanLine = (startX, limitX, incX, y) => {
|
|
42
|
+
for (let x = startX; x !== limitX; x += incX) {
|
|
43
|
+
const slicePixelIndex = y * width + x;
|
|
44
|
+
const volumeVoxelIndex = sliceOffset + slicePixelIndex;
|
|
45
|
+
const volumeVoxelValue = subVolPixelData[volumeVoxelIndex];
|
|
46
|
+
if (volumeVoxelValue < negativePixelRange[0] ||
|
|
47
|
+
volumeVoxelValue > negativePixelRange[1]) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (!visited[slicePixelIndex]) {
|
|
51
|
+
bfs(x, y);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (let y = 0; y < height; y++) {
|
|
56
|
+
scanLine(0, width - 1, 1, y);
|
|
57
|
+
scanLine(width - 1, 0, -1, y);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function _setPositiveSeedValues(subVolume, labelmap, options) {
|
|
61
|
+
const { positiveSeedValue = POSITIVE_SEED_VALUE, positivePixelRange = POSITIVE_PIXEL_RANGE, } = options;
|
|
62
|
+
const subVolPixelData = subVolume.voxelManager.getCompleteScalarDataArray();
|
|
63
|
+
const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
|
|
64
|
+
const [width, height, numSlices] = labelmap.dimensions;
|
|
65
|
+
const middleSliceIndex = Math.floor(numSlices / 2);
|
|
66
|
+
const startSliceIndex = Math.max(middleSliceIndex - 3, 0);
|
|
67
|
+
const stopSliceIndex = Math.max(startSliceIndex + 5, numSlices);
|
|
68
|
+
const pixelsPerSlice = width * height;
|
|
69
|
+
for (let z = startSliceIndex; z < stopSliceIndex; z++) {
|
|
70
|
+
const zOffset = z * pixelsPerSlice;
|
|
71
|
+
for (let y = 0; y < height; y++) {
|
|
72
|
+
const yOffset = y * width;
|
|
73
|
+
for (let x = 0; x < width; x++) {
|
|
74
|
+
const index = zOffset + yOffset + x;
|
|
75
|
+
const pixelValue = subVolPixelData[index];
|
|
76
|
+
const isPositiveValue = pixelValue >= positivePixelRange[0] &&
|
|
77
|
+
pixelValue <= positivePixelRange[1];
|
|
78
|
+
if (isPositiveValue) {
|
|
79
|
+
labelmap.voxelManager.setAtIndex(index, positiveSeedValue);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function _createAndCacheSegmentationSubVolumeForBoundingBox(subVolume, options) {
|
|
86
|
+
const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(subVolume.volumeId);
|
|
87
|
+
_setPositiveSeedValues(subVolume, labelmap, options);
|
|
88
|
+
_setNegativeSeedValues(subVolume, labelmap, options);
|
|
89
|
+
return labelmap;
|
|
90
|
+
}
|
|
91
|
+
async function runGrowCutForBoundingBox(referencedVolumeId, boundingBoxInfo, options) {
|
|
92
|
+
const { boundingBox } = boundingBoxInfo;
|
|
93
|
+
const { ijkTopLeft, ijkBottomRight } = boundingBox;
|
|
94
|
+
const subVolumeBoundsIJK = {
|
|
95
|
+
minX: ijkTopLeft[0],
|
|
96
|
+
maxX: ijkBottomRight[0],
|
|
97
|
+
minY: ijkTopLeft[1],
|
|
98
|
+
maxY: ijkBottomRight[1],
|
|
99
|
+
minZ: ijkTopLeft[2],
|
|
100
|
+
maxZ: ijkBottomRight[2],
|
|
101
|
+
};
|
|
102
|
+
const subVolume = csUtils.createSubVolume(referencedVolumeId, subVolumeBoundsIJK, {
|
|
103
|
+
targetBuffer: {
|
|
104
|
+
type: 'Float32Array',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const labelmap = await _createAndCacheSegmentationSubVolumeForBoundingBox(subVolume, options);
|
|
108
|
+
await run(subVolume.volumeId, labelmap.volumeId);
|
|
109
|
+
return labelmap;
|
|
110
|
+
}
|
|
111
|
+
export { runGrowCutForBoundingBox as default, runGrowCutForBoundingBox };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import { type GrowCutOptions } from './runGrowCut';
|
|
3
|
+
type SphereInfo = {
|
|
4
|
+
center: Types.Point3;
|
|
5
|
+
radius: number;
|
|
6
|
+
};
|
|
7
|
+
declare function runGrowCutForSphere(referencedVolumeId: string, sphereInfo: SphereInfo, viewport: Types.IViewport, options?: GrowCutOptions): Promise<Types.IImageVolume>;
|
|
8
|
+
export { runGrowCutForSphere as default, runGrowCutForSphere };
|
|
9
|
+
export type { SphereInfo, GrowCutOptions as GrowCutSphereOptions };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { quat, vec3 } from 'gl-matrix';
|
|
2
|
+
import { utilities as csUtils, cache, volumeLoader } from '@cornerstonejs/core';
|
|
3
|
+
import { run } from './runGrowCut';
|
|
4
|
+
import { getSphereBoundsInfo } from '../../getSphereBoundsInfo';
|
|
5
|
+
const { transformWorldToIndex } = csUtils;
|
|
6
|
+
const POSITIVE_SEED_VALUE = 254;
|
|
7
|
+
const NEGATIVE_SEED_VALUE = 255;
|
|
8
|
+
const POSITIVE_SEED_VARIANCE = 0.1;
|
|
9
|
+
const NEGATIVE_SEED_VARIANCE = 0.8;
|
|
10
|
+
function _getGrowCutSphereBoundsInfo(referencedVolume, sphereBoundsInfo) {
|
|
11
|
+
const { topLeftWorld, bottomRightWorld } = sphereBoundsInfo;
|
|
12
|
+
const topLeftIJK = transformWorldToIndex(referencedVolume.imageData, topLeftWorld);
|
|
13
|
+
const bottomRightIJK = transformWorldToIndex(referencedVolume.imageData, bottomRightWorld);
|
|
14
|
+
return {
|
|
15
|
+
...sphereBoundsInfo,
|
|
16
|
+
topLeftIJK,
|
|
17
|
+
bottomRightIJK,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function _getSphereBoundsInfo(referencedVolume, sphereInfo) {
|
|
21
|
+
const direction = referencedVolume.imageData.getDirection();
|
|
22
|
+
const vecColumn = vec3.fromValues(direction[3], direction[4], direction[5]);
|
|
23
|
+
const { center: sphereCenterPoint, radius: sphereRadius } = sphereInfo;
|
|
24
|
+
const refVolImageData = referencedVolume.imageData;
|
|
25
|
+
const topCirclePoint = vec3.scaleAndAdd(vec3.create(), sphereCenterPoint, vecColumn, -sphereRadius);
|
|
26
|
+
const bottomCirclePoint = vec3.scaleAndAdd(vec3.create(), sphereCenterPoint, vecColumn, sphereRadius);
|
|
27
|
+
const sphereBoundsInfo = getSphereBoundsInfo([bottomCirclePoint, topCirclePoint], refVolImageData);
|
|
28
|
+
return _getGrowCutSphereBoundsInfo(referencedVolume, sphereBoundsInfo);
|
|
29
|
+
}
|
|
30
|
+
function _createSubVolumeFromSphere(referencedVolume, sphereInfo, viewport) {
|
|
31
|
+
const refVolImageData = referencedVolume.imageData;
|
|
32
|
+
const camera = viewport.getCamera();
|
|
33
|
+
const { ijkVecRowDir, ijkVecColDir } = csUtils.getVolumeDirectionVectors(refVolImageData, camera);
|
|
34
|
+
const obliqueView = [ijkVecRowDir, ijkVecColDir].some((vec) => !csUtils.isEqual(Math.abs(vec[0]), 1) &&
|
|
35
|
+
!csUtils.isEqual(Math.abs(vec[1]), 1) &&
|
|
36
|
+
!csUtils.isEqual(Math.abs(vec[2]), 1));
|
|
37
|
+
if (obliqueView) {
|
|
38
|
+
console.warn('Oblique view is not supported!');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { boundsIJK: sphereBoundsIJK } = _getSphereBoundsInfo(referencedVolume, sphereInfo);
|
|
42
|
+
const subVolumeBoundsIJK = {
|
|
43
|
+
minX: sphereBoundsIJK[0][0],
|
|
44
|
+
maxX: sphereBoundsIJK[0][1] + 1,
|
|
45
|
+
minY: sphereBoundsIJK[1][0],
|
|
46
|
+
maxY: sphereBoundsIJK[1][1] + 1,
|
|
47
|
+
minZ: sphereBoundsIJK[2][0],
|
|
48
|
+
maxZ: sphereBoundsIJK[2][1] + 1,
|
|
49
|
+
};
|
|
50
|
+
return csUtils.createSubVolume(referencedVolume.volumeId, subVolumeBoundsIJK, {
|
|
51
|
+
targetBuffer: {
|
|
52
|
+
type: 'Float32Array',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function _setPositiveSeedValues(referencedVolume, labelmap, sphereInfo, options) {
|
|
57
|
+
const refVolumePixelData = referencedVolume.voxelManager.getCompleteScalarDataArray();
|
|
58
|
+
const worldStartPos = sphereInfo.center;
|
|
59
|
+
const [width, height, numSlices] = referencedVolume.dimensions;
|
|
60
|
+
const numPixelsPerSlice = width * height;
|
|
61
|
+
const ijkStartPosition = transformWorldToIndex(referencedVolume.imageData, worldStartPos);
|
|
62
|
+
const referencePixelValue = refVolumePixelData[ijkStartPosition[2] * numPixelsPerSlice +
|
|
63
|
+
ijkStartPosition[1] * width +
|
|
64
|
+
ijkStartPosition[0]];
|
|
65
|
+
const positiveSeedValue = options.positiveSeedValue ?? POSITIVE_SEED_VALUE;
|
|
66
|
+
const positiveSeedVariance = options.positiveSeedVariance ?? POSITIVE_SEED_VARIANCE;
|
|
67
|
+
const positiveSeedVarianceValue = Math.abs(referencePixelValue * positiveSeedVariance);
|
|
68
|
+
const minPositivePixelValue = referencePixelValue - positiveSeedVarianceValue;
|
|
69
|
+
const maxPositivePixelValue = referencePixelValue + positiveSeedVarianceValue;
|
|
70
|
+
const neighborsCoordDelta = [
|
|
71
|
+
[-1, 0, 0],
|
|
72
|
+
[1, 0, 0],
|
|
73
|
+
[0, -1, 0],
|
|
74
|
+
[0, 1, 0],
|
|
75
|
+
[0, 0, -1],
|
|
76
|
+
[0, 0, 1],
|
|
77
|
+
];
|
|
78
|
+
const startVoxelIndex = ijkStartPosition[2] * numPixelsPerSlice +
|
|
79
|
+
ijkStartPosition[1] * width +
|
|
80
|
+
ijkStartPosition[0];
|
|
81
|
+
labelmap.voxelManager.setAtIndex(startVoxelIndex, positiveSeedValue);
|
|
82
|
+
const queue = [ijkStartPosition];
|
|
83
|
+
while (queue.length) {
|
|
84
|
+
const ijkVoxel = queue.shift();
|
|
85
|
+
const [x, y, z] = ijkVoxel;
|
|
86
|
+
for (let i = 0, len = neighborsCoordDelta.length; i < len; i++) {
|
|
87
|
+
const neighborCoordDelta = neighborsCoordDelta[i];
|
|
88
|
+
const nx = x + neighborCoordDelta[0];
|
|
89
|
+
const ny = y + neighborCoordDelta[1];
|
|
90
|
+
const nz = z + neighborCoordDelta[2];
|
|
91
|
+
if (nx < 0 ||
|
|
92
|
+
nx >= width ||
|
|
93
|
+
ny < 0 ||
|
|
94
|
+
ny >= height ||
|
|
95
|
+
nz < 0 ||
|
|
96
|
+
nz >= numSlices) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const neighborVoxelIndex = nz * numPixelsPerSlice + ny * width + nx;
|
|
100
|
+
const neighborPixelValue = refVolumePixelData[neighborVoxelIndex];
|
|
101
|
+
const neighborLabelmapValue = labelmap.voxelManager.getAtIndex(neighborVoxelIndex);
|
|
102
|
+
if (neighborLabelmapValue === positiveSeedValue ||
|
|
103
|
+
neighborPixelValue < minPositivePixelValue ||
|
|
104
|
+
neighborPixelValue > maxPositivePixelValue) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
labelmap.voxelManager.setAtIndex(neighborVoxelIndex, positiveSeedValue);
|
|
108
|
+
queue.push([nx, ny, nz]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function _setNegativeSeedValues(subVolume, labelmap, sphereInfo, viewport, options) {
|
|
113
|
+
const subVolPixelData = subVolume.voxelManager.getCompleteScalarDataArray();
|
|
114
|
+
const [columns, rows, numSlices] = labelmap.dimensions;
|
|
115
|
+
const numPixelsPerSlice = columns * rows;
|
|
116
|
+
const { worldVecRowDir, worldVecSliceDir } = csUtils.getVolumeDirectionVectors(labelmap.imageData, viewport.getCamera());
|
|
117
|
+
const ijkSphereCenter = transformWorldToIndex(subVolume.imageData, sphereInfo.center);
|
|
118
|
+
const referencePixelValue = subVolPixelData[ijkSphereCenter[2] * columns * rows +
|
|
119
|
+
ijkSphereCenter[1] * columns +
|
|
120
|
+
ijkSphereCenter[0]];
|
|
121
|
+
const negativeSeedVariance = options.negativeSeedVariance ?? NEGATIVE_SEED_VARIANCE;
|
|
122
|
+
const negativeSeedValue = options?.negativeSeedValue ?? NEGATIVE_SEED_VALUE;
|
|
123
|
+
const negativeSeedVarianceValue = Math.abs(referencePixelValue * negativeSeedVariance);
|
|
124
|
+
const minNegativePixelValue = referencePixelValue - negativeSeedVarianceValue;
|
|
125
|
+
const maxNegativePixelValue = referencePixelValue + negativeSeedVarianceValue;
|
|
126
|
+
const numCirclePoints = 360;
|
|
127
|
+
const rotationAngle = (2 * Math.PI) / numCirclePoints;
|
|
128
|
+
const worldQuat = quat.setAxisAngle(quat.create(), worldVecSliceDir, rotationAngle);
|
|
129
|
+
const vecRotation = vec3.clone(worldVecRowDir);
|
|
130
|
+
for (let i = 0; i < numCirclePoints; i++) {
|
|
131
|
+
const worldCircleBorderPoint = vec3.scaleAndAdd(vec3.create(), sphereInfo.center, vecRotation, sphereInfo.radius);
|
|
132
|
+
const ijkCircleBorderPoint = transformWorldToIndex(labelmap.imageData, worldCircleBorderPoint);
|
|
133
|
+
const [x, y, z] = ijkCircleBorderPoint;
|
|
134
|
+
vec3.transformQuat(vecRotation, vecRotation, worldQuat);
|
|
135
|
+
if (x < 0 ||
|
|
136
|
+
x >= columns ||
|
|
137
|
+
y < 0 ||
|
|
138
|
+
y >= rows ||
|
|
139
|
+
z < 0 ||
|
|
140
|
+
z >= numSlices) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const offset = x + y * columns + z * numPixelsPerSlice;
|
|
144
|
+
const pixelValue = subVolPixelData[offset];
|
|
145
|
+
if (pixelValue < minNegativePixelValue ||
|
|
146
|
+
pixelValue > maxNegativePixelValue) {
|
|
147
|
+
labelmap.voxelManager.setAtIndex(offset, negativeSeedValue);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function _createAndCacheSegmentationSubVolumeForSphere(subVolume, sphereInfo, viewport, options) {
|
|
152
|
+
const labelmap = await volumeLoader.createAndCacheDerivedLabelmapVolume(subVolume.volumeId);
|
|
153
|
+
_setPositiveSeedValues(subVolume, labelmap, sphereInfo, options);
|
|
154
|
+
_setNegativeSeedValues(subVolume, labelmap, sphereInfo, viewport, options);
|
|
155
|
+
return labelmap;
|
|
156
|
+
}
|
|
157
|
+
async function runGrowCutForSphere(referencedVolumeId, sphereInfo, viewport, options) {
|
|
158
|
+
const referencedVolume = cache.getVolume(referencedVolumeId);
|
|
159
|
+
const subVolume = _createSubVolumeFromSphere(referencedVolume, sphereInfo, viewport);
|
|
160
|
+
const labelmap = await _createAndCacheSegmentationSubVolumeForSphere(subVolume, sphereInfo, viewport, options);
|
|
161
|
+
await run(subVolume.volumeId, labelmap.volumeId);
|
|
162
|
+
return labelmap;
|
|
163
|
+
}
|
|
164
|
+
export { runGrowCutForSphere as default, runGrowCutForSphere };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import type { GrowCutOptions } from './runGrowCut';
|
|
3
|
+
type GrowCutOneClickOptions = GrowCutOptions & {
|
|
4
|
+
positiveSeedValue?: number;
|
|
5
|
+
negativeSeedValue?: number;
|
|
6
|
+
positiveSeedVariance?: number;
|
|
7
|
+
negativeSeedVariance?: number;
|
|
8
|
+
subVolumePaddingPercentage?: number | [number, number, number];
|
|
9
|
+
subVolumeMinPadding?: number | [number, number, number];
|
|
10
|
+
};
|
|
11
|
+
declare function runOneClickGrowCut(referencedVolumeId: string, worldPosition: Types.Point3, viewport: Types.IViewport, options?: GrowCutOneClickOptions): Promise<Types.IImageVolume>;
|
|
12
|
+
export { runOneClickGrowCut as default, runOneClickGrowCut };
|
|
13
|
+
export type { GrowCutOneClickOptions };
|