@cornerstonejs/tools 3.15.6 → 3.16.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.
Files changed (37) hide show
  1. package/dist/esm/drawingSvg/drawFan.d.ts +4 -0
  2. package/dist/esm/drawingSvg/drawFan.js +62 -0
  3. package/dist/esm/drawingSvg/drawLine.js +2 -1
  4. package/dist/esm/drawingSvg/index.d.ts +2 -1
  5. package/dist/esm/drawingSvg/index.js +2 -1
  6. package/dist/esm/index.d.ts +2 -2
  7. package/dist/esm/index.js +2 -2
  8. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool.d.ts +64 -0
  9. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool.js +752 -0
  10. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/calculateFanShapeCorners.d.ts +8 -0
  11. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/calculateFanShapeCorners.js +143 -0
  12. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/deriveFanGeometry.d.ts +2 -0
  13. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/deriveFanGeometry.js +32 -0
  14. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction.d.ts +7 -0
  15. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction.js +171 -0
  16. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.d.ts +5 -0
  17. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/generateConvexHullFromContour.js +6 -0
  18. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/segmentLargestUSOutlineFromBuffer.d.ts +2 -0
  19. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/segmentLargestUSOutlineFromBuffer.js +123 -0
  20. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/types.d.ts +41 -0
  21. package/dist/esm/tools/annotation/UltrasoundPleuraBLineTool/utils/types.js +0 -0
  22. package/dist/esm/tools/index.d.ts +2 -1
  23. package/dist/esm/tools/index.js +2 -1
  24. package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +10 -0
  25. package/dist/esm/utilities/index.d.ts +2 -1
  26. package/dist/esm/utilities/index.js +2 -1
  27. package/dist/esm/utilities/math/fan/fanUtils.d.ts +10 -0
  28. package/dist/esm/utilities/math/fan/fanUtils.js +99 -0
  29. package/dist/esm/utilities/math/line/intersectLine.d.ts +1 -1
  30. package/dist/esm/utilities/math/line/intersectLine.js +16 -6
  31. package/dist/esm/utilities/math/polyline/convexHull.d.ts +2 -0
  32. package/dist/esm/utilities/math/polyline/convexHull.js +31 -0
  33. package/dist/esm/utilities/math/polyline/index.d.ts +2 -1
  34. package/dist/esm/utilities/math/polyline/index.js +2 -1
  35. package/dist/esm/version.d.ts +1 -1
  36. package/dist/esm/version.js +1 -1
  37. package/package.json +3 -3
@@ -0,0 +1,8 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import type { FanShapeCorners, RefinementOptions } from './types';
3
+ export declare function pickPoints(hull: Array<Types.Point2>, slack?: number): FanShapeCorners;
4
+ export declare function computeEdgeBuffer(buffer: any, width: any, height: any): Float32Array;
5
+ export declare function refineCornersDirectional(edgeBuf: Float32Array, width: number, height: number, rough: FanShapeCorners, contour: Array<Types.Point2>, opts?: RefinementOptions & {
6
+ slack?: number;
7
+ }): FanShapeCorners;
8
+ export declare function calculateFanShapeCorners(imageBuffer: any, width: any, height: any, hull: Array<Types.Point2>, roughContour: Array<Types.Point2>): FanShapeCorners;
@@ -0,0 +1,143 @@
1
+ export function pickPoints(hull, slack = 7) {
2
+ if (!hull.length) {
3
+ throw new Error('Convex hull is empty');
4
+ }
5
+ const n = hull.length;
6
+ const next = (i) => (i + 1) % n;
7
+ const walk = (from, to) => {
8
+ const idx = [];
9
+ for (let i = from;; i = next(i)) {
10
+ idx.push(i);
11
+ if (i === to) {
12
+ break;
13
+ }
14
+ }
15
+ return idx;
16
+ };
17
+ let i2 = 0, i3 = 0;
18
+ for (let i = 1; i < n; i++) {
19
+ if (hull[i][0] < hull[i2][0]) {
20
+ i2 = i;
21
+ }
22
+ if (hull[i][0] > hull[i3][0]) {
23
+ i3 = i;
24
+ }
25
+ }
26
+ const P2 = hull[i2];
27
+ const P3 = hull[i3];
28
+ const pathA = walk(i2, i3);
29
+ const pathB = walk(i3, i2);
30
+ const globalYmin = Math.min(...hull.map((p) => p[1]));
31
+ const upperPath = pathA.some((i) => hull[i][1] === globalYmin)
32
+ ? pathA
33
+ : pathB;
34
+ const topY = Math.min(...upperPath.map((i) => hull[i][1]));
35
+ let arcPts = upperPath
36
+ .map((i) => hull[i])
37
+ .filter((p) => Math.abs(p[1] - topY) <= slack);
38
+ if (arcPts.length < 2) {
39
+ arcPts = upperPath
40
+ .map((i) => hull[i])
41
+ .sort((a, b) => a[1] - b[1])
42
+ .slice(0, 2);
43
+ }
44
+ const P1 = arcPts.reduce((best, p) => (p[0] < best[0] ? p : best), arcPts[0]);
45
+ const P4 = arcPts.reduce((best, p) => (p[0] > best[0] ? p : best), arcPts[0]);
46
+ return { P1, P2, P3, P4 };
47
+ }
48
+ export function computeEdgeBuffer(buffer, width, height) {
49
+ const total = width * height;
50
+ const channels = buffer.length / total;
51
+ if (![1, 3, 4].includes(channels)) {
52
+ throw new Error('Buffer must be 1,3 or 4 channels per pixel');
53
+ }
54
+ const gray = new Float32Array(total);
55
+ for (let i = 0; i < total; i++) {
56
+ if (channels === 1) {
57
+ gray[i] = buffer[i];
58
+ }
59
+ else {
60
+ const base = i * channels;
61
+ const r = buffer[base];
62
+ const g = buffer[base + 1];
63
+ const b = buffer[base + 2];
64
+ gray[i] = 0.299 * r + 0.587 * g + 0.114 * b;
65
+ }
66
+ }
67
+ const edgeBuf = new Float32Array(total);
68
+ for (let y = 1; y < height - 1; y++) {
69
+ for (let x = 1; x < width - 1; x++) {
70
+ const idx = y * width + x;
71
+ const i00 = idx - width - 1;
72
+ const i01 = idx - width;
73
+ const i02 = idx - width + 1;
74
+ const i10 = idx - 1;
75
+ const i11 = idx;
76
+ const i12 = idx + 1;
77
+ const i20 = idx + width - 1;
78
+ const i21 = idx + width;
79
+ const i22 = idx + width + 1;
80
+ const gx = -gray[i00] +
81
+ gray[i02] +
82
+ -2 * gray[i10] +
83
+ 2 * gray[i12] +
84
+ -gray[i20] +
85
+ gray[i22];
86
+ const gy = gray[i00] +
87
+ 2 * gray[i01] +
88
+ gray[i02] -
89
+ gray[i20] -
90
+ 2 * gray[i21] -
91
+ gray[i22];
92
+ edgeBuf[idx] = Math.hypot(gx, gy);
93
+ }
94
+ }
95
+ return edgeBuf;
96
+ }
97
+ export function refineCornersDirectional(edgeBuf, width, height, rough, contour, opts = {}) {
98
+ const { maxDist = 15, slack = 2 } = opts;
99
+ const directions = {
100
+ P1: { dx: -1, dy: -1 },
101
+ P2: { dx: -1, dy: +1 },
102
+ P3: { dx: +1, dy: +1 },
103
+ P4: { dx: +1, dy: -1 },
104
+ };
105
+ function snapQuadrant(pt, { dx, dy }, threshold = 5) {
106
+ const xmin = dx < 0 ? pt[0] - maxDist : pt[0] - slack;
107
+ const xmax = dx < 0 ? pt[0] + slack : pt[0] + maxDist;
108
+ const ymin = dy < 0 ? pt[1] - maxDist : pt[1] - slack;
109
+ const ymax = dy < 0 ? pt[1] + slack : pt[1] + maxDist;
110
+ let best = pt;
111
+ for (const [cx, cy] of contour) {
112
+ if (cx < xmin || cx > xmax || cy < ymin || cy > ymax) {
113
+ continue;
114
+ }
115
+ const xi = Math.round(cx);
116
+ const yi = Math.round(cy);
117
+ if (xi < 0 || xi >= width || yi < 0 || yi >= height) {
118
+ continue;
119
+ }
120
+ const xAlign = (xi - best[0]) * dx;
121
+ const yAlign = (yi - best[0]) * dy;
122
+ const v = edgeBuf[yi * width + xi];
123
+ if (v > threshold && (xAlign > 0 || yAlign > 0)) {
124
+ best = [cx, cy];
125
+ }
126
+ }
127
+ return best;
128
+ }
129
+ return {
130
+ P1: snapQuadrant(rough.P1, directions.P1),
131
+ P2: snapQuadrant(rough.P2, directions.P2),
132
+ P3: snapQuadrant(rough.P3, directions.P3),
133
+ P4: snapQuadrant(rough.P4, directions.P4),
134
+ };
135
+ }
136
+ export function calculateFanShapeCorners(imageBuffer, width, height, hull, roughContour) {
137
+ const rough = pickPoints(hull);
138
+ const refined = refineCornersDirectional(imageBuffer, width, height, rough, roughContour, {
139
+ maxDist: 20,
140
+ step: 0.5,
141
+ });
142
+ return refined;
143
+ }
@@ -0,0 +1,2 @@
1
+ import type { FanGeometry, FanShapeCorners } from './types';
2
+ export declare function deriveFanGeometry(params: FanShapeCorners): FanGeometry;
@@ -0,0 +1,32 @@
1
+ import { intersectLine } from '../../../../utilities/math/line';
2
+ function angleRad(center, p) {
3
+ return Math.atan2(p[1] - center[1], p[0] - center[0]);
4
+ }
5
+ export function deriveFanGeometry(params) {
6
+ const { P1, P2, P3, P4 } = params;
7
+ const centerResult = intersectLine(P1, P2, P4, P3, true);
8
+ if (!centerResult) {
9
+ throw new Error('Fan edges appear parallel — no apex found');
10
+ }
11
+ const center = centerResult;
12
+ let startAngle = angleRad(center, P1) * (180 / Math.PI);
13
+ let endAngle = angleRad(center, P4) * (180 / Math.PI);
14
+ if (endAngle <= startAngle) {
15
+ const tempAngle = startAngle;
16
+ startAngle = endAngle;
17
+ endAngle = tempAngle;
18
+ }
19
+ const d1 = Math.hypot(P1[0] - center[0], P1[1] - center[1]);
20
+ const d4 = Math.hypot(P4[0] - center[0], P4[1] - center[1]);
21
+ const d2 = Math.hypot(P2[0] - center[0], P2[1] - center[1]);
22
+ const d3 = Math.hypot(P3[0] - center[0], P3[1] - center[1]);
23
+ const innerRadius = Math.min(d1, d4);
24
+ const outerRadius = Math.max(d2, d3);
25
+ return {
26
+ center,
27
+ startAngle,
28
+ endAngle,
29
+ innerRadius,
30
+ outerRadius,
31
+ };
32
+ }
@@ -0,0 +1,7 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ import type { FanShapeContour, ContourExportOptions, PixelDataResult, FanGeometryResult } from './types';
3
+ export declare function exportContourJpeg(pixelData: Types.PixelDataTypedArray, width: number, height: number, contour: FanShapeContour, opts?: ContourExportOptions): string;
4
+ export declare function getPixelData(imageId: string): PixelDataResult | undefined;
5
+ export default function saveBinaryData(url: string, filename: string): void;
6
+ export declare function downloadFanJpeg(imageId: string, contourType?: number): void;
7
+ export declare function calculateFanGeometry(imageId: string): FanGeometryResult | undefined;
@@ -0,0 +1,171 @@
1
+ import { cache } from '@cornerstonejs/core';
2
+ import { segmentLargestUSOutlineFromBuffer } from './segmentLargestUSOutlineFromBuffer';
3
+ import { generateConvexHullFromContour } from './generateConvexHullFromContour';
4
+ import { calculateFanShapeCorners } from './calculateFanShapeCorners';
5
+ import { deriveFanGeometry } from './deriveFanGeometry';
6
+ export function exportContourJpeg(pixelData, width, height, contour, opts = {}) {
7
+ const { strokeStyle = '#f00', lineWidth = 2, quality = 0.92 } = opts;
8
+ const canvas = document.createElement('canvas');
9
+ canvas.width = width;
10
+ canvas.height = height;
11
+ const ctx = canvas.getContext('2d');
12
+ const totalPixels = width * height;
13
+ const channels = pixelData.length / totalPixels;
14
+ const imgData = ctx.createImageData(width, height);
15
+ const out = imgData.data;
16
+ for (let i = 0; i < totalPixels; i++) {
17
+ const baseIn = i * channels;
18
+ const baseOut = i * 4;
19
+ if (channels === 1) {
20
+ const v = pixelData[baseIn];
21
+ out[baseOut] = v;
22
+ out[baseOut + 1] = v;
23
+ out[baseOut + 2] = v;
24
+ out[baseOut + 3] = 255;
25
+ }
26
+ else {
27
+ out[baseOut] = pixelData[baseIn];
28
+ out[baseOut + 1] = pixelData[baseIn + 1];
29
+ out[baseOut + 2] = pixelData[baseIn + 2];
30
+ out[baseOut + 3] = channels === 4 ? pixelData[baseIn + 3] : 255;
31
+ }
32
+ }
33
+ ctx.putImageData(imgData, 0, 0);
34
+ if (contour.length > 0) {
35
+ ctx.strokeStyle = strokeStyle;
36
+ ctx.lineWidth = lineWidth;
37
+ ctx.beginPath();
38
+ ctx.moveTo(contour[0][0] + 0.5, contour[0][1] + 0.5);
39
+ for (let i = 1; i < contour.length; i++) {
40
+ ctx.lineTo(contour[i][0] + 0.5, contour[i][1] + 0.5);
41
+ }
42
+ ctx.closePath();
43
+ ctx.stroke();
44
+ }
45
+ return canvas.toDataURL('image/jpeg', quality);
46
+ }
47
+ export function getPixelData(imageId) {
48
+ const image = cache.getImage(imageId);
49
+ if (!image) {
50
+ return;
51
+ }
52
+ const width = image.width;
53
+ const height = image.height;
54
+ const pixelData = image.getPixelData();
55
+ return {
56
+ pixelData,
57
+ width,
58
+ height,
59
+ };
60
+ }
61
+ export default function saveBinaryData(url, filename) {
62
+ const a = document.createElement('a');
63
+ a.href = url;
64
+ a.download = filename;
65
+ document.body.appendChild(a);
66
+ a.style.display = 'none';
67
+ a.click();
68
+ a.remove();
69
+ }
70
+ function exportFanJpeg(pixelData, width, height, fan, opts = {}) {
71
+ const { center, startAngle: startAngleInDegrees, endAngle: endAngleInDegrees, innerRadius, outerRadius, } = fan;
72
+ const { strokeStyle = '#0ff', lineWidth = 2, quality = 0.92 } = opts;
73
+ const startAngle = (startAngleInDegrees * Math.PI) / 180;
74
+ const endAngle = (endAngleInDegrees * Math.PI) / 180;
75
+ const canvas = document.createElement('canvas');
76
+ canvas.width = width;
77
+ canvas.height = height;
78
+ const ctx = canvas.getContext('2d');
79
+ const total = width * height;
80
+ const channels = pixelData.length / total;
81
+ const imgData = ctx.createImageData(width, height);
82
+ const out = imgData.data;
83
+ for (let i = 0; i < total; i++) {
84
+ const baseOut = i * 4;
85
+ if (channels === 1) {
86
+ const v = pixelData[i];
87
+ out[baseOut] = v;
88
+ out[baseOut + 1] = v;
89
+ out[baseOut + 2] = v;
90
+ out[baseOut + 3] = 255;
91
+ }
92
+ else {
93
+ const baseIn = i * channels;
94
+ out[baseOut] = pixelData[baseIn];
95
+ out[baseOut + 1] = pixelData[baseIn + 1];
96
+ out[baseOut + 2] = pixelData[baseIn + 2];
97
+ out[baseOut + 3] = channels === 4 ? pixelData[baseIn + 3] : 255;
98
+ }
99
+ }
100
+ ctx.putImageData(imgData, 0, 0);
101
+ ctx.beginPath();
102
+ for (let a = startAngle; a <= endAngle; a += 0.01) {
103
+ const x = center[0] + innerRadius * Math.cos(a);
104
+ const y = center[1] + innerRadius * Math.sin(a);
105
+ if (a === startAngle) {
106
+ ctx.moveTo(x, y);
107
+ }
108
+ else {
109
+ ctx.lineTo(x, y);
110
+ }
111
+ }
112
+ for (let a = endAngle; a >= startAngle; a -= 0.01) {
113
+ const x = center[0] + outerRadius * Math.cos(a);
114
+ const y = center[1] + outerRadius * Math.sin(a);
115
+ ctx.lineTo(x, y);
116
+ }
117
+ ctx.closePath();
118
+ ctx.strokeStyle = strokeStyle;
119
+ ctx.lineWidth = lineWidth;
120
+ ctx.stroke();
121
+ return canvas.toDataURL('image/jpeg', quality);
122
+ }
123
+ export function downloadFanJpeg(imageId, contourType = 5) {
124
+ const { contour, simplified, hull, refined, fanGeometry } = calculateFanGeometry(imageId);
125
+ const { pixelData, width, height } = getPixelData(imageId) || {};
126
+ if (!pixelData) {
127
+ return;
128
+ }
129
+ let jpegDataUrl;
130
+ if (contourType === 1) {
131
+ jpegDataUrl = exportContourJpeg(pixelData, width, height, contour);
132
+ }
133
+ else if (contourType === 2) {
134
+ jpegDataUrl = exportContourJpeg(pixelData, width, height, simplified);
135
+ }
136
+ else if (contourType === 3) {
137
+ jpegDataUrl = exportContourJpeg(pixelData, width, height, hull);
138
+ }
139
+ else if (contourType === 4) {
140
+ jpegDataUrl = exportContourJpeg(pixelData, width, height, [
141
+ refined.P1,
142
+ refined.P2,
143
+ refined.P3,
144
+ refined.P4,
145
+ ]);
146
+ }
147
+ else {
148
+ jpegDataUrl = exportFanJpeg(pixelData, width, height, fanGeometry, {
149
+ strokeStyle: '#f00',
150
+ lineWidth: 3,
151
+ quality: 0.95,
152
+ });
153
+ }
154
+ saveBinaryData(jpegDataUrl, 'contour.jpg');
155
+ }
156
+ export function calculateFanGeometry(imageId) {
157
+ const { pixelData, width, height } = getPixelData(imageId) || {};
158
+ if (!pixelData) {
159
+ return;
160
+ }
161
+ const contour = segmentLargestUSOutlineFromBuffer(pixelData, width, height);
162
+ const { simplified, hull } = generateConvexHullFromContour(contour);
163
+ const refined = calculateFanShapeCorners(pixelData, width, height, hull, simplified);
164
+ const fanGeometry = deriveFanGeometry({
165
+ P1: refined.P1,
166
+ P2: refined.P2,
167
+ P3: refined.P3,
168
+ P4: refined.P4,
169
+ });
170
+ return { contour, simplified, hull, refined, fanGeometry };
171
+ }
@@ -0,0 +1,5 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export declare function generateConvexHullFromContour(contour: Array<Types.Point2>): {
3
+ simplified: Types.Point2[];
4
+ hull: Types.Point2[];
5
+ };
@@ -0,0 +1,6 @@
1
+ import { utilities } from '@cornerstonejs/tools';
2
+ export function generateConvexHullFromContour(contour) {
3
+ const simplified = utilities.math.polyline.decimate(contour, 2);
4
+ const hull = utilities.math.polyline.convexHull(simplified);
5
+ return { simplified, hull };
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { FanShapeContour } from './types';
2
+ export declare function segmentLargestUSOutlineFromBuffer(buffer: any, width: any, height: any): FanShapeContour;
@@ -0,0 +1,123 @@
1
+ import { floodFill } from '../../../../utilities/segmentation';
2
+ export function segmentLargestUSOutlineFromBuffer(buffer, width, height) {
3
+ const totalPixels = width * height;
4
+ const channelCount = buffer.length / totalPixels;
5
+ if (![1, 3, 4].includes(channelCount)) {
6
+ throw new Error('Buffer must be 1, 3, or 4 channels per pixel');
7
+ }
8
+ const mask = Array.from({ length: height }, () => new Array(width).fill(false));
9
+ for (let y = 0; y < height; y++) {
10
+ for (let x = 0; x < width; x++) {
11
+ const pixelIndex = y * width + x;
12
+ const base = pixelIndex * channelCount;
13
+ let isForeground = false;
14
+ for (let c = 0; c < Math.min(3, channelCount); c++) {
15
+ if (buffer[base + c] > 0) {
16
+ isForeground = true;
17
+ break;
18
+ }
19
+ }
20
+ mask[y][x] = isForeground;
21
+ }
22
+ }
23
+ const labels = Array.from({ length: height }, () => new Array(width).fill(0));
24
+ let currentLabel = 0;
25
+ const regionSizes = {};
26
+ for (let y = 0; y < height; y++) {
27
+ for (let x = 0; x < width; x++) {
28
+ if (mask[y][x] && labels[y][x] === 0) {
29
+ currentLabel++;
30
+ const getter = (px, py) => {
31
+ if (px < 0 || px >= width || py < 0 || py >= height) {
32
+ return false;
33
+ }
34
+ return mask[py][px] && labels[py][px] === 0;
35
+ };
36
+ let pixelCount = 0;
37
+ const options = {
38
+ onFlood: (px, py) => {
39
+ labels[py][px] = currentLabel;
40
+ pixelCount++;
41
+ },
42
+ diagonals: false,
43
+ };
44
+ floodFill(getter, [x, y], options);
45
+ regionSizes[currentLabel] = pixelCount;
46
+ }
47
+ }
48
+ }
49
+ if (currentLabel === 0) {
50
+ return [];
51
+ }
52
+ const largestLabel = Object.keys(regionSizes).reduce((a, b) => regionSizes[a] > regionSizes[b] ? a : b);
53
+ function isBorder(x, y) {
54
+ if (labels[y][x] !== +largestLabel) {
55
+ return false;
56
+ }
57
+ for (const [dx, dy] of [
58
+ [1, 0],
59
+ [-1, 0],
60
+ [0, 1],
61
+ [0, -1],
62
+ ]) {
63
+ const nx = x + dx, ny = y + dy;
64
+ if (nx < 0 ||
65
+ nx >= width ||
66
+ ny < 0 ||
67
+ ny >= height ||
68
+ labels[ny][nx] !== +largestLabel) {
69
+ return true;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ let start = null;
75
+ outer: for (let y = 0; y < height; y++) {
76
+ for (let x = 0; x < width; x++) {
77
+ if (isBorder(x, y)) {
78
+ start = [x, y];
79
+ break outer;
80
+ }
81
+ }
82
+ }
83
+ if (!start) {
84
+ return [];
85
+ }
86
+ const dirs = [
87
+ [1, 0],
88
+ [1, 1],
89
+ [0, 1],
90
+ [-1, 1],
91
+ [-1, 0],
92
+ [-1, -1],
93
+ [0, -1],
94
+ [1, -1],
95
+ ];
96
+ const contour = [];
97
+ let current = start;
98
+ let prev = [start[0] - 1, start[1]];
99
+ do {
100
+ contour.push([current[0], current[1]]);
101
+ const dx0 = prev[0] - current[0], dy0 = prev[1] - current[1];
102
+ let startDir = dirs.findIndex((d) => d[0] === dx0 && d[1] === dy0);
103
+ if (startDir < 0) {
104
+ startDir = 0;
105
+ }
106
+ let nextPt = null;
107
+ for (let k = 1; k <= 8; k++) {
108
+ const [dx, dy] = dirs[(startDir + k) % 8];
109
+ const nx = current[0] + dx, ny = current[1] + dy;
110
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height && isBorder(nx, ny)) {
111
+ nextPt = [nx, ny];
112
+ const [bdx, bdy] = dirs[(startDir + k - 1 + 8) % 8];
113
+ prev = [current[0] + bdx, current[1] + bdy];
114
+ break;
115
+ }
116
+ }
117
+ if (!nextPt) {
118
+ break;
119
+ }
120
+ current = nextPt;
121
+ } while (current[0] !== start[0] || current[1] !== start[1]);
122
+ return contour;
123
+ }
@@ -0,0 +1,41 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export type FanShapeContour = Types.Point2[];
3
+ export interface FanGeometry {
4
+ center: Types.Point2;
5
+ startAngle: number;
6
+ endAngle: number;
7
+ innerRadius: number;
8
+ outerRadius: number;
9
+ }
10
+ export interface ContourExportOptions {
11
+ strokeStyle?: string;
12
+ lineWidth?: number;
13
+ quality?: number;
14
+ }
15
+ export interface FanExportOptions {
16
+ strokeStyle?: string;
17
+ lineWidth?: number;
18
+ quality?: number;
19
+ }
20
+ export interface FanShapeCorners {
21
+ P1: Types.Point2;
22
+ P2: Types.Point2;
23
+ P3: Types.Point2;
24
+ P4: Types.Point2;
25
+ }
26
+ export interface PixelDataResult {
27
+ pixelData: Types.PixelDataTypedArray;
28
+ width: number;
29
+ height: number;
30
+ }
31
+ export interface RefinementOptions {
32
+ maxDist?: number;
33
+ step?: number;
34
+ }
35
+ export interface FanGeometryResult {
36
+ contour: FanShapeContour;
37
+ simplified: FanShapeContour;
38
+ hull: FanShapeContour;
39
+ refined: FanShapeCorners;
40
+ fanGeometry: FanGeometry;
41
+ }
@@ -37,6 +37,7 @@ import ArrowAnnotateTool from './annotation/ArrowAnnotateTool';
37
37
  import AngleTool from './annotation/AngleTool';
38
38
  import CobbAngleTool from './annotation/CobbAngleTool';
39
39
  import UltrasoundDirectionalTool from './annotation/UltrasoundDirectionalTool';
40
+ import UltrasoundPleuraBLineTool from './annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool';
40
41
  import KeyImageTool from './annotation/KeyImageTool';
41
42
  import AnnotationEraserTool from './AnnotationEraserTool';
42
43
  import RegionSegmentTool from './annotation/RegionSegmentTool';
@@ -56,4 +57,4 @@ import SegmentSelectTool from './segmentation/SegmentSelectTool';
56
57
  import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool';
57
58
  import * as strategies from './segmentation/strategies';
58
59
  import SegmentLabelTool from './segmentation/SegmentLabelTool';
59
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, 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, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, strategies, };
60
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, 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, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, strategies, };
@@ -37,6 +37,7 @@ import ArrowAnnotateTool from './annotation/ArrowAnnotateTool';
37
37
  import AngleTool from './annotation/AngleTool';
38
38
  import CobbAngleTool from './annotation/CobbAngleTool';
39
39
  import UltrasoundDirectionalTool from './annotation/UltrasoundDirectionalTool';
40
+ import UltrasoundPleuraBLineTool from './annotation/UltrasoundPleuraBLineTool/UltrasoundPleuraBLineTool';
40
41
  import KeyImageTool from './annotation/KeyImageTool';
41
42
  import AnnotationEraserTool from './AnnotationEraserTool';
42
43
  import RegionSegmentTool from './annotation/RegionSegmentTool';
@@ -56,4 +57,4 @@ import SegmentSelectTool from './segmentation/SegmentSelectTool';
56
57
  import SegmentBidirectionalTool from './segmentation/SegmentBidirectionalTool';
57
58
  import * as strategies from './segmentation/strategies';
58
59
  import SegmentLabelTool from './segmentation/SegmentLabelTool';
59
- export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, 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, KeyImageTool, AnnotationEraserTool as EraserTool, RectangleScissorsTool, CircleScissorsTool, SphereScissorsTool, RectangleROIThresholdTool, RectangleROIStartEndThresholdTool, CircleROIStartEndThresholdTool, SplineContourSegmentationTool, BrushTool, MagnifyTool, AdvancedMagnifyTool, PaintFillTool, ScaleOverlayTool, OrientationMarkerTool, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, strategies, };
60
+ export { BaseTool, AnnotationTool, AnnotationDisplayTool, PanTool, TrackballRotateTool, 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, SculptorTool, SegmentSelectTool, VolumeRotateTool, RegionSegmentTool, RegionSegmentPlusTool, WholeBodySegmentTool, LabelmapBaseTool, SegmentBidirectionalTool, SegmentLabelTool, strategies, };
@@ -84,6 +84,16 @@ export interface LengthAnnotation extends Annotation {
84
84
  };
85
85
  };
86
86
  }
87
+ export interface UltrasoundPleuraBLineAnnotation extends Annotation {
88
+ data: {
89
+ handles: {
90
+ points: [Types.Point3, Types.Point3];
91
+ activeHandleIndex: number | null;
92
+ };
93
+ annotationType: 'pleura' | 'bLine';
94
+ label: string;
95
+ };
96
+ }
87
97
  export interface AdvancedMagnifyAnnotation extends Annotation {
88
98
  data: {
89
99
  zoomFactor: number;
@@ -40,4 +40,5 @@ import * as geometricSurfaceUtils from './geometricSurfaceUtils';
40
40
  import setAnnotationLabel from './setAnnotationLabel';
41
41
  import { moveAnnotationToViewPlane } from './moveAnnotationToViewPlane';
42
42
  import getOrCreateImageVolume from './segmentation/getOrCreateImageVolume';
43
- export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, moveAnnotationToViewPlane, getOrCreateImageVolume, };
43
+ import * as usFanExtraction from '../tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction';
44
+ export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, usFanExtraction, setAnnotationLabel, moveAnnotationToViewPlane, getOrCreateImageVolume, };
@@ -40,4 +40,5 @@ import * as geometricSurfaceUtils from './geometricSurfaceUtils';
40
40
  import setAnnotationLabel from './setAnnotationLabel';
41
41
  import { moveAnnotationToViewPlane } from './moveAnnotationToViewPlane';
42
42
  import getOrCreateImageVolume from './segmentation/getOrCreateImageVolume';
43
- export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, moveAnnotationToViewPlane, getOrCreateImageVolume, };
43
+ import * as usFanExtraction from '../tools/annotation/UltrasoundPleuraBLineTool/utils/fanExtraction';
44
+ export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, usFanExtraction, setAnnotationLabel, moveAnnotationToViewPlane, getOrCreateImageVolume, };
@@ -0,0 +1,10 @@
1
+ import type { Types } from '@cornerstonejs/core';
2
+ export type Interval = Types.Point2;
3
+ export type FanPair = [Types.Point2, Types.Point2];
4
+ export type FanPairs = FanPair[];
5
+ export declare function angleFromCenter(center: Types.Point2, point: Types.Point2): number;
6
+ export declare function intervalFromPoints(center: Types.Point2, pair: FanPair): Types.Point2;
7
+ export declare function mergeIntervals(intervals: Interval[]): Interval[];
8
+ export declare function subtractIntervals(blocked: Interval[], target: Interval): Interval[];
9
+ export declare function clipInterval(inner: Types.Point2, outerMerged: Interval[]): Interval[];
10
+ export declare function calculateInnerFanPercentage(center: Types.Point2, outerFanPairs: FanPairs, innerFanPairs: FanPairs): number;