@cornerstonejs/tools 1.38.1 → 1.40.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/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tools/VolumeRotateMouseWheelTool.js +2 -2
- package/dist/cjs/tools/VolumeRotateMouseWheelTool.js.map +1 -1
- package/dist/cjs/tools/annotation/LivewireContourTool.d.ts +44 -0
- package/dist/cjs/tools/annotation/LivewireContourTool.js +442 -0
- package/dist/cjs/tools/annotation/LivewireContourTool.js.map +1 -0
- package/dist/cjs/tools/index.d.ts +2 -1
- package/dist/cjs/tools/index.js +3 -1
- package/dist/cjs/tools/index.js.map +1 -1
- package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +10 -0
- package/dist/cjs/utilities/BucketQueue.d.ts +20 -0
- package/dist/cjs/utilities/BucketQueue.js +83 -0
- package/dist/cjs/utilities/BucketQueue.js.map +1 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.d.ts +16 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.js +64 -0
- package/dist/cjs/utilities/livewire/LiveWirePath.js.map +1 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.d.ts +37 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.js +281 -0
- package/dist/cjs/utilities/livewire/LivewireScissors.js.map +1 -0
- package/dist/cjs/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tools/VolumeRotateMouseWheelTool.js +2 -2
- package/dist/esm/tools/VolumeRotateMouseWheelTool.js.map +1 -1
- package/dist/esm/tools/annotation/LivewireContourTool.js +439 -0
- package/dist/esm/tools/annotation/LivewireContourTool.js.map +1 -0
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/utilities/BucketQueue.js +79 -0
- package/dist/esm/utilities/BucketQueue.js.map +1 -0
- package/dist/esm/utilities/livewire/LiveWirePath.js +60 -0
- package/dist/esm/utilities/livewire/LiveWirePath.js.map +1 -0
- package/dist/esm/utilities/livewire/LivewireScissors.js +277 -0
- package/dist/esm/utilities/livewire/LivewireScissors.js.map +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/tools/VolumeRotateMouseWheelTool.d.ts.map +1 -1
- package/dist/types/tools/annotation/LivewireContourTool.d.ts +45 -0
- package/dist/types/tools/annotation/LivewireContourTool.d.ts.map +1 -0
- package/dist/types/tools/index.d.ts +2 -1
- package/dist/types/tools/index.d.ts.map +1 -1
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +10 -0
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
- package/dist/types/utilities/BucketQueue.d.ts +21 -0
- package/dist/types/utilities/BucketQueue.d.ts.map +1 -0
- package/dist/types/utilities/livewire/LiveWirePath.d.ts +17 -0
- package/dist/types/utilities/livewire/LiveWirePath.d.ts.map +1 -0
- package/dist/types/utilities/livewire/LivewireScissors.d.ts +38 -0
- package/dist/types/utilities/livewire/LivewireScissors.d.ts.map +1 -0
- package/dist/types/utilities/math/vec2/liangBarksyClip.d.ts +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/tools/VolumeRotateMouseWheelTool.ts +3 -2
- package/src/tools/annotation/LivewireContourTool.ts +799 -0
- package/src/tools/index.ts +2 -0
- package/src/types/ToolSpecificAnnotationTypes.ts +12 -0
- package/src/utilities/BucketQueue.ts +154 -0
- package/src/utilities/livewire/LiveWirePath.ts +131 -0
- package/src/utilities/livewire/LivewireScissors.ts +582 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { Types } from '@cornerstonejs/core';
|
|
2
|
+
import { BucketQueue } from '../BucketQueue';
|
|
3
|
+
|
|
4
|
+
const MAX_UINT32 = 4294967295;
|
|
5
|
+
const TWO_THIRD_PI = 2 / (3 * Math.PI);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scissors
|
|
9
|
+
*
|
|
10
|
+
* Ref: Eric N. Mortensen, William A. Barrett, Interactive Segmentation with
|
|
11
|
+
* Intelligent Scissors, Graphical Models and Image Processing, Volume 60,
|
|
12
|
+
* Issue 5, September 1998, Pages 349-384, ISSN 1077-3169,
|
|
13
|
+
* DOI: 10.1006/gmip.1998.0480.
|
|
14
|
+
*
|
|
15
|
+
* {@link http://www.sciencedirect.com/science/article/B6WG4-45JB8WN-9/2/6fe59d8089fd1892c2bfb82283065579}
|
|
16
|
+
*
|
|
17
|
+
* Implementation based on
|
|
18
|
+
* {@link http://code.google.com/p/livewire-javascript/}
|
|
19
|
+
*/
|
|
20
|
+
export class LivewireScissors {
|
|
21
|
+
private searchGranularityBits: number;
|
|
22
|
+
private searchGranularity: number;
|
|
23
|
+
|
|
24
|
+
/** Width of the image */
|
|
25
|
+
public readonly width: number;
|
|
26
|
+
|
|
27
|
+
/** Height of the image */
|
|
28
|
+
public readonly height: number;
|
|
29
|
+
|
|
30
|
+
/** Grayscale image */
|
|
31
|
+
private grayscalePixelData: Float32Array;
|
|
32
|
+
|
|
33
|
+
// Laplace zero-crossings (either 0 or 1).
|
|
34
|
+
private laplace: Float32Array;
|
|
35
|
+
|
|
36
|
+
/** Gradient vector magnitude for each pixel */
|
|
37
|
+
private gradMagnitude: Float32Array;
|
|
38
|
+
|
|
39
|
+
/** Gradient of each pixel in the x-direction */
|
|
40
|
+
private gradXNew: Float32Array;
|
|
41
|
+
|
|
42
|
+
/** Gradient of each pixel in the y-direction */
|
|
43
|
+
private gradYNew: Float32Array;
|
|
44
|
+
|
|
45
|
+
/** Dijkstra - start point */
|
|
46
|
+
private startPoint: Types.Point2;
|
|
47
|
+
|
|
48
|
+
/** Dijkstra - store the state of a pixel (visited/unvisited) */
|
|
49
|
+
private visited: boolean[];
|
|
50
|
+
|
|
51
|
+
/** Dijkstra - map a point to its parent along the shortest path to root (start point) */
|
|
52
|
+
private parents: Uint32Array;
|
|
53
|
+
|
|
54
|
+
/** Dijkstra - store the cost to go from the start point to each node */
|
|
55
|
+
private costs: Float32Array;
|
|
56
|
+
|
|
57
|
+
/** Dijkstra - BucketQueue to sort items by priority */
|
|
58
|
+
private priorityQueueNew: BucketQueue<number>;
|
|
59
|
+
|
|
60
|
+
constructor(grayscalePixelData: Float32Array, width: number, height: number) {
|
|
61
|
+
const numPixels = grayscalePixelData.length;
|
|
62
|
+
|
|
63
|
+
this.searchGranularityBits = 8; // Bits of resolution for BucketQueue.
|
|
64
|
+
this.searchGranularity = 1 << this.searchGranularityBits; //bits.
|
|
65
|
+
|
|
66
|
+
this.width = width;
|
|
67
|
+
this.height = height;
|
|
68
|
+
|
|
69
|
+
this.grayscalePixelData = grayscalePixelData;
|
|
70
|
+
this.laplace = null;
|
|
71
|
+
this.gradXNew = null;
|
|
72
|
+
this.gradYNew = null;
|
|
73
|
+
|
|
74
|
+
this.laplace = this._computeLaplace();
|
|
75
|
+
this.gradMagnitude = this._computeGradient();
|
|
76
|
+
this.gradXNew = this._computeGradientX();
|
|
77
|
+
this.gradYNew = this._computeGradientY();
|
|
78
|
+
|
|
79
|
+
this.visited = new Array(numPixels);
|
|
80
|
+
this.parents = new Uint32Array(numPixels);
|
|
81
|
+
this.costs = new Float32Array(numPixels);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public startSearch(startPoint: Types.Point2): void {
|
|
85
|
+
const startPointIndex = this._getPointIndex(startPoint[1], startPoint[0]);
|
|
86
|
+
|
|
87
|
+
this.startPoint = null;
|
|
88
|
+
this.visited.fill(false);
|
|
89
|
+
this.parents.fill(MAX_UINT32);
|
|
90
|
+
this.costs.fill(Infinity);
|
|
91
|
+
this.priorityQueueNew = new BucketQueue<number>({
|
|
92
|
+
numBits: this.searchGranularityBits,
|
|
93
|
+
getPriority: this._getPointCost,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.startPoint = startPoint;
|
|
97
|
+
this.costs[startPointIndex] = 0;
|
|
98
|
+
this.priorityQueueNew.push(startPointIndex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Runs Dijsktra until it finds a path from the start point to the target
|
|
103
|
+
* point. Once it reaches the target point all the state is preserved in order
|
|
104
|
+
* to save processing time the next time the method is called for a new target
|
|
105
|
+
* point. The search is restarted whenever `startSearch` is called.
|
|
106
|
+
* @param targetPoint - Target point
|
|
107
|
+
* @returns An array with all points for the shortest path found that goes
|
|
108
|
+
* from startPoint to targetPoint.
|
|
109
|
+
*/
|
|
110
|
+
public findPathToPoint(targetPoint: Types.Point2): Types.Point2[] {
|
|
111
|
+
if (!this.startPoint) {
|
|
112
|
+
throw new Error('There is no search in progress');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
startPoint,
|
|
117
|
+
_getPointIndex: index,
|
|
118
|
+
_getPointCoordinate: coord,
|
|
119
|
+
} = this;
|
|
120
|
+
const startPointIndex = index(startPoint[1], startPoint[0]);
|
|
121
|
+
const targetPointIndex = index(targetPoint[1], targetPoint[0]);
|
|
122
|
+
const {
|
|
123
|
+
visited: visited,
|
|
124
|
+
parents: parents,
|
|
125
|
+
costs: cost,
|
|
126
|
+
priorityQueueNew: priorityQueue,
|
|
127
|
+
} = this;
|
|
128
|
+
|
|
129
|
+
if (targetPointIndex === startPointIndex) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Stop searching until there are no more items in the queue or it has
|
|
134
|
+
// reached the target point. In case it reaches the target all the remaining
|
|
135
|
+
// items will stay in the queue then once the user moves the mouse to a new
|
|
136
|
+
// location the search can continue from where it left off.
|
|
137
|
+
while (
|
|
138
|
+
!priorityQueue.isEmpty() &&
|
|
139
|
+
parents[targetPointIndex] === MAX_UINT32
|
|
140
|
+
) {
|
|
141
|
+
const pointIndex = priorityQueue.pop();
|
|
142
|
+
|
|
143
|
+
if (visited[pointIndex]) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const point = coord(pointIndex);
|
|
148
|
+
const neighborsPoints = this._getNeighborPoints(point);
|
|
149
|
+
|
|
150
|
+
visited[pointIndex] = true;
|
|
151
|
+
|
|
152
|
+
// Update the cost of all neighbors that have higher costs
|
|
153
|
+
for (let i = 0, len = neighborsPoints.length; i < len; i++) {
|
|
154
|
+
const neighborPoint = neighborsPoints[i];
|
|
155
|
+
const neighbordPointIndex = index(neighborPoint[1], neighborPoint[0]);
|
|
156
|
+
const dist = this._getWeightedDistance(point, neighborPoint);
|
|
157
|
+
const neighborCost = cost[pointIndex] + dist;
|
|
158
|
+
|
|
159
|
+
if (neighborCost < cost[neighbordPointIndex]) {
|
|
160
|
+
if (cost[neighbordPointIndex] !== Infinity) {
|
|
161
|
+
// The item needs to be removed from the priority queue and
|
|
162
|
+
// re-added in order to be moved to the right bucket.
|
|
163
|
+
priorityQueue.remove(neighbordPointIndex);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
cost[neighbordPointIndex] = neighborCost;
|
|
167
|
+
parents[neighbordPointIndex] = pointIndex;
|
|
168
|
+
priorityQueue.push(neighbordPointIndex);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pathPoints = [];
|
|
174
|
+
let pathPointIndex = targetPointIndex;
|
|
175
|
+
|
|
176
|
+
while (pathPointIndex !== MAX_UINT32) {
|
|
177
|
+
pathPoints.push(coord(pathPointIndex));
|
|
178
|
+
pathPointIndex = parents[pathPointIndex];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return pathPoints.reverse();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convert a point coordinate (x,y) into a point index
|
|
186
|
+
* @param index - Point index
|
|
187
|
+
* @returns Point coordinate (x,y)
|
|
188
|
+
*/
|
|
189
|
+
private _getPointIndex = (row: number, col: number) => {
|
|
190
|
+
const { width } = this;
|
|
191
|
+
return row * width + col;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert a point index into a point coordinate (x,y)
|
|
196
|
+
* @param index - Point index
|
|
197
|
+
* @returns Point coordinate (x,y)
|
|
198
|
+
*/
|
|
199
|
+
private _getPointCoordinate = (index: number): Types.Point2 => {
|
|
200
|
+
const x = index % this.width;
|
|
201
|
+
const y = Math.floor(index / this.width);
|
|
202
|
+
|
|
203
|
+
return [x, y];
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculate the delta X between a given point and its neighbor at the right
|
|
208
|
+
* @param x - Point x-coordinate
|
|
209
|
+
* @param y - Point y-coordinate
|
|
210
|
+
* @returns Delta Y between the given point and its neighbor at the right
|
|
211
|
+
*/
|
|
212
|
+
private _getDeltaX(x: number, y: number) {
|
|
213
|
+
const { grayscalePixelData: data, width } = this;
|
|
214
|
+
let index = this._getPointIndex(y, x);
|
|
215
|
+
|
|
216
|
+
// If it is at the end, back up one
|
|
217
|
+
if (x + 1 === width) {
|
|
218
|
+
index--;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return data[index + 1] - data[index];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate the delta Y between a given point and its neighbor at the bottom
|
|
226
|
+
* @param x - Point x-coordinate
|
|
227
|
+
* @param y - Point y-coordinate
|
|
228
|
+
* @returns Delta Y between the given point and its neighbor at the bottom
|
|
229
|
+
*/
|
|
230
|
+
private _getDeltaY(x: number, y: number) {
|
|
231
|
+
const { grayscalePixelData: data, width, height } = this;
|
|
232
|
+
let index = this._getPointIndex(y, x);
|
|
233
|
+
|
|
234
|
+
// If it is at the end, back up one
|
|
235
|
+
if (y + 1 === height) {
|
|
236
|
+
index -= height;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data[index] - data[index + width];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private _getGradientMagnitude(x: number, y: number): number {
|
|
243
|
+
const dx = this._getDeltaX(x, y);
|
|
244
|
+
const dy = this._getDeltaY(x, y);
|
|
245
|
+
|
|
246
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Calculate the Laplacian of Gaussian (LoG) value for a given pixel
|
|
251
|
+
*
|
|
252
|
+
* Kernel Indexes Laplacian of Gaussian Kernel
|
|
253
|
+
* __ __ 02 __ __ 0 0 1 0 0
|
|
254
|
+
* __ 11 12 13 __ 0 1 2 1 0
|
|
255
|
+
* 20 21 22 23 24 1 2 -16 2 1
|
|
256
|
+
* __ 31 32 33 __ 0 1 2 1 0
|
|
257
|
+
* __ __ 42 __ __ 0 0 1 0 0
|
|
258
|
+
*/
|
|
259
|
+
private _getLaplace(x: number, y: number): number {
|
|
260
|
+
const { grayscalePixelData: data, _getPointIndex: index } = this;
|
|
261
|
+
|
|
262
|
+
// Points related to the kernel indexes
|
|
263
|
+
const p02 = data[index(y - 2, x)];
|
|
264
|
+
const p11 = data[index(y - 1, x - 1)];
|
|
265
|
+
const p12 = data[index(y - 1, x)];
|
|
266
|
+
const p13 = data[index(y - 1, x + 1)];
|
|
267
|
+
const p20 = data[index(y, x - 2)];
|
|
268
|
+
const p21 = data[index(y, x - 1)];
|
|
269
|
+
const p22 = data[index(y, x)];
|
|
270
|
+
const p23 = data[index(y, x + 1)];
|
|
271
|
+
const p24 = data[index(y, x + 2)];
|
|
272
|
+
const p31 = data[index(y + 1, x - 1)];
|
|
273
|
+
const p32 = data[index(y + 1, x)];
|
|
274
|
+
const p33 = data[index(y + 1, x + 1)];
|
|
275
|
+
const p42 = data[index(y + 2, x)];
|
|
276
|
+
|
|
277
|
+
// Laplacian of Gaussian
|
|
278
|
+
let lap = p02;
|
|
279
|
+
lap += p11 + 2 * p12 + p13;
|
|
280
|
+
lap += p20 + 2 * p21 - 16 * p22 + 2 * p23 + p24;
|
|
281
|
+
lap += p31 + 2 * p32 + p33;
|
|
282
|
+
lap += p42;
|
|
283
|
+
|
|
284
|
+
return lap;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns a 2D array of gradient magnitude values for grayscale. The values
|
|
289
|
+
* are scaled between 0 and 1, and then flipped, so that it works as a cost
|
|
290
|
+
* function.
|
|
291
|
+
* @returns A gradient object
|
|
292
|
+
*/
|
|
293
|
+
private _computeGradient(): Float32Array {
|
|
294
|
+
const { width, height } = this;
|
|
295
|
+
const gradient = new Float32Array(width * height);
|
|
296
|
+
|
|
297
|
+
let pixelIndex = 0;
|
|
298
|
+
let max = 0;
|
|
299
|
+
let x = 0;
|
|
300
|
+
let y = 0;
|
|
301
|
+
|
|
302
|
+
for (y = 0; y < height - 1; y++) {
|
|
303
|
+
for (x = 0; x < width - 1; x++) {
|
|
304
|
+
gradient[pixelIndex] = this._getGradientMagnitude(x, y);
|
|
305
|
+
max = Math.max(gradient[pixelIndex], max);
|
|
306
|
+
pixelIndex++;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Make the last column the same as the previous one because there is
|
|
310
|
+
// no way to calculate `dx` since x+1 gets out of bounds
|
|
311
|
+
gradient[pixelIndex] = gradient[pixelIndex - 1];
|
|
312
|
+
pixelIndex++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Make the last row the same as the previous one because there is
|
|
316
|
+
// no way to calculate `dy` since y+1 gets out of bounds
|
|
317
|
+
for (let len = gradient.length; pixelIndex < len; pixelIndex++) {
|
|
318
|
+
gradient[pixelIndex] = gradient[pixelIndex - width];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Flip and scale
|
|
322
|
+
for (let i = 0, len = gradient.length; i < len; i++) {
|
|
323
|
+
gradient[i] = 1 - gradient[i] / max;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return gradient;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Returns a 2D array of Laplacian of Gaussian values
|
|
331
|
+
*
|
|
332
|
+
* @param grayscale - The input grayscale
|
|
333
|
+
* @returns A laplace object
|
|
334
|
+
*/
|
|
335
|
+
private _computeLaplace(): Float32Array {
|
|
336
|
+
const { width, height, _getPointIndex: index } = this;
|
|
337
|
+
const laplace = new Float32Array(width * height);
|
|
338
|
+
|
|
339
|
+
// Make the first two rows low cost
|
|
340
|
+
laplace.fill(1, 0, index(2, 0));
|
|
341
|
+
|
|
342
|
+
for (let y = 2; y < height - 2; y++) {
|
|
343
|
+
// Make the first two columns low cost
|
|
344
|
+
laplace[index(y, 0)] = 1;
|
|
345
|
+
laplace[index(y, 1)] = 1;
|
|
346
|
+
|
|
347
|
+
for (let x = 2; x < width - 2; x++) {
|
|
348
|
+
// Threshold needed to get rid of clutter.
|
|
349
|
+
laplace[index(y, x)] = this._getLaplace(x, y) > 0.33 ? 0 : 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Make the last two columns low cost
|
|
353
|
+
laplace[index(y, width - 2)] = 1;
|
|
354
|
+
laplace[index(y, width - 1)] = 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Make the last two rows low cost
|
|
358
|
+
laplace.fill(1, index(height - 2, 0));
|
|
359
|
+
|
|
360
|
+
return laplace;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Returns 2D array of x-gradient values for grayscale
|
|
365
|
+
*
|
|
366
|
+
* @param grayscale - Grayscale pixel data
|
|
367
|
+
* @returns 2D x-gradient array
|
|
368
|
+
*/
|
|
369
|
+
private _computeGradientX(): Float32Array {
|
|
370
|
+
const { width, height } = this;
|
|
371
|
+
const gradX = new Float32Array(width * height);
|
|
372
|
+
let pixelIndex = 0;
|
|
373
|
+
|
|
374
|
+
for (let y = 0; y < height; y++) {
|
|
375
|
+
for (let x = 0; x < width - 1; x++) {
|
|
376
|
+
gradX[pixelIndex++] = this._getDeltaX(x, y);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Make the last column the same as the previous one because there is
|
|
380
|
+
// no way to calculate `dx` since x+1 gets out of bounds
|
|
381
|
+
gradX[pixelIndex] = gradX[pixelIndex - 1];
|
|
382
|
+
pixelIndex++;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return gradX;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Compute the Y gradient.
|
|
390
|
+
*
|
|
391
|
+
* @param grayscale - Grayscale pixel data
|
|
392
|
+
* @returns 2D array of y-gradient values for grayscale
|
|
393
|
+
*/
|
|
394
|
+
private _computeGradientY(): Float32Array {
|
|
395
|
+
const { width, height } = this;
|
|
396
|
+
const gradY = new Float32Array(width * height);
|
|
397
|
+
let pixelIndex = 0;
|
|
398
|
+
|
|
399
|
+
for (let y = 0; y < height - 1; y++) {
|
|
400
|
+
for (let x = 0; x < width; x++) {
|
|
401
|
+
gradY[pixelIndex++] = this._getDeltaY(x, y);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Make the last row the same as the previous one because there is
|
|
406
|
+
// no way to calculate `dy` since y+1 gets out of bounds
|
|
407
|
+
for (let len = gradY.length; pixelIndex < len; pixelIndex++) {
|
|
408
|
+
gradY[pixelIndex] = gradY[pixelIndex - width];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return gradY;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Compute the gradient unit vector.
|
|
416
|
+
* @param px - Point x-coordinate
|
|
417
|
+
* @param py - Point y-coordinate
|
|
418
|
+
* @returns Gradient vector at (px, py), scaled to a magnitude of 1
|
|
419
|
+
*/
|
|
420
|
+
private _getGradientUnitVector(px: number, py: number) {
|
|
421
|
+
const { gradXNew, gradYNew, _getPointIndex: index } = this;
|
|
422
|
+
|
|
423
|
+
const pointGradX = gradXNew[index(py, px)];
|
|
424
|
+
const pointGradY = gradYNew[index(py, px)];
|
|
425
|
+
let gradVecLen = Math.sqrt(
|
|
426
|
+
pointGradX * pointGradX + pointGradY * pointGradY
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// To avoid possible divide-by-0 errors
|
|
430
|
+
gradVecLen = Math.max(gradVecLen, 1e-100);
|
|
431
|
+
|
|
432
|
+
return [pointGradX / gradVecLen, pointGradY / gradVecLen];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Compute the gradiant direction, in radians, between to points
|
|
437
|
+
*
|
|
438
|
+
* @param px - Point `p` x-coordinate of point p.
|
|
439
|
+
* @param py - Point `p` y-coordinate of point p.
|
|
440
|
+
* @param qx - Point `q` x-coordinate of point q.
|
|
441
|
+
* @param qy - Point `q` y-coordinate of point q.
|
|
442
|
+
* @returns Gradient direction
|
|
443
|
+
*/
|
|
444
|
+
private _getGradientDirection(
|
|
445
|
+
px: number,
|
|
446
|
+
py: number,
|
|
447
|
+
qx: number,
|
|
448
|
+
qy: number
|
|
449
|
+
): number {
|
|
450
|
+
const dgpUnitVec = this._getGradientUnitVector(px, py);
|
|
451
|
+
const gdqUnitVec = this._getGradientUnitVector(qx, qy);
|
|
452
|
+
|
|
453
|
+
let dp = dgpUnitVec[1] * (qx - px) - dgpUnitVec[0] * (qy - py);
|
|
454
|
+
let dq = gdqUnitVec[1] * (qx - px) - gdqUnitVec[0] * (qy - py);
|
|
455
|
+
|
|
456
|
+
// Make sure dp is positive, to keep things consistent
|
|
457
|
+
if (dp < 0) {
|
|
458
|
+
dp = -dp;
|
|
459
|
+
dq = -dq;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (px !== qx && py !== qy) {
|
|
463
|
+
// It's going diagonally between pixels
|
|
464
|
+
dp *= Math.SQRT1_2;
|
|
465
|
+
dq *= Math.SQRT1_2;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return TWO_THIRD_PI * (Math.acos(dp) + Math.acos(dq));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Return a weighted distance between two points
|
|
473
|
+
*/
|
|
474
|
+
private _getWeightedDistance(pointA: Types.Point2, pointB: Types.Point2) {
|
|
475
|
+
const { _getPointIndex: index } = this;
|
|
476
|
+
const [aX, aY] = pointA;
|
|
477
|
+
const [bX, bY] = pointB;
|
|
478
|
+
const bIndex = index(bY, bX);
|
|
479
|
+
|
|
480
|
+
// Weighted distance function
|
|
481
|
+
let gradient = this.gradMagnitude[bIndex];
|
|
482
|
+
|
|
483
|
+
if (aX === bX || aY === bY) {
|
|
484
|
+
// The distance is Euclidean-ish; non-diagonal edges should be shorter
|
|
485
|
+
gradient *= Math.SQRT1_2;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const laplace = this.laplace[bIndex];
|
|
489
|
+
const direction = this._getGradientDirection(aX, aY, bX, bY);
|
|
490
|
+
|
|
491
|
+
return 0.43 * gradient + 0.43 * laplace + 0.11 * direction;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get up to 8 neighbors points
|
|
496
|
+
* @param point - Reference point
|
|
497
|
+
* @returns Up to eight neighbor points
|
|
498
|
+
*/
|
|
499
|
+
private _getNeighborPoints(point: Types.Point2): Types.Point2[] {
|
|
500
|
+
const { width, height } = this;
|
|
501
|
+
const list: Types.Point2[] = [];
|
|
502
|
+
|
|
503
|
+
const sx = Math.max(point[0] - 1, 0);
|
|
504
|
+
const sy = Math.max(point[1] - 1, 0);
|
|
505
|
+
const ex = Math.min(point[0] + 1, width - 1);
|
|
506
|
+
const ey = Math.min(point[1] + 1, height - 1);
|
|
507
|
+
|
|
508
|
+
for (let y = sy; y <= ey; y++) {
|
|
509
|
+
for (let x = sx; x <= ex; x++) {
|
|
510
|
+
if (x !== point[0] || y !== point[1]) {
|
|
511
|
+
list.push([x, y]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return list;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private _getPointCost = (pointIndex: number): number => {
|
|
520
|
+
return Math.round(this.searchGranularity * this.costs[pointIndex]);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Create a livewire scissor instance from RAW pixel data
|
|
525
|
+
* @param pixelData - Raw pixel data
|
|
526
|
+
* @param width - Width of the image
|
|
527
|
+
* @param height - Height of the image
|
|
528
|
+
* @param voiRange - VOI Range
|
|
529
|
+
* @returns A LivewireScissors instance
|
|
530
|
+
*/
|
|
531
|
+
public static createInstanceFromRawPixelData(
|
|
532
|
+
pixelData: Float32Array,
|
|
533
|
+
width: number,
|
|
534
|
+
height: number,
|
|
535
|
+
voiRange: Types.VOIRange
|
|
536
|
+
) {
|
|
537
|
+
const numPixels = pixelData.length;
|
|
538
|
+
const grayscalePixelData = new Float32Array(numPixels);
|
|
539
|
+
const { lower: minPixelValue, upper: maxPixelValue } = voiRange;
|
|
540
|
+
const pixelRange = maxPixelValue - minPixelValue;
|
|
541
|
+
|
|
542
|
+
for (let i = 0, len = pixelData.length; i < len; i++) {
|
|
543
|
+
// Grayscale values must be between 0 and 1
|
|
544
|
+
grayscalePixelData[i] = Math.max(
|
|
545
|
+
0,
|
|
546
|
+
Math.min(1, (pixelData[i] - minPixelValue) / pixelRange)
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return new LivewireScissors(grayscalePixelData, width, height);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Create a livewire scissor instance from a RGBA image
|
|
555
|
+
* @param rgbaPixelData - RGBA pixel data
|
|
556
|
+
* @param width - Width of the image
|
|
557
|
+
* @param height - Height of the image
|
|
558
|
+
* @returns A LivewireScissors instance
|
|
559
|
+
*/
|
|
560
|
+
public static createInstanceFromRGBAPixelData(
|
|
561
|
+
rgbaPixelData: Uint8ClampedArray,
|
|
562
|
+
width: number,
|
|
563
|
+
height: number
|
|
564
|
+
): LivewireScissors {
|
|
565
|
+
const numPixels = rgbaPixelData.length / 4;
|
|
566
|
+
const grayscalePixelData = new Float32Array(numPixels);
|
|
567
|
+
|
|
568
|
+
// Multiplier to average an RGB sum and convert it to 0-1 range.
|
|
569
|
+
// 1/x because multiplication is faster than division.
|
|
570
|
+
const avgMultiplier = 1 / (3 * 255);
|
|
571
|
+
|
|
572
|
+
for (let i = 0, offset = 0; i < numPixels; i++, offset += 4) {
|
|
573
|
+
const red = rgbaPixelData[offset];
|
|
574
|
+
const green = rgbaPixelData[offset];
|
|
575
|
+
const blue = rgbaPixelData[offset];
|
|
576
|
+
|
|
577
|
+
grayscalePixelData[i] = (red + green + blue) * avgMultiplier;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return new LivewireScissors(grayscalePixelData, width, height);
|
|
581
|
+
}
|
|
582
|
+
}
|