@bettermedicine/imeasure 0.0.5 → 0.0.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/README.md +36 -4
- package/dist/math/cropping.js +47 -38
- package/dist/math/cropping.test.d.ts +1 -0
- package/dist/math/cropping.test.js +184 -0
- package/package.json +1 -1
- package/src/math/cropping.test.ts +260 -0
- package/src/math/cropping.ts +71 -59
package/README.md
CHANGED
|
@@ -10,10 +10,15 @@ Better Medicine's iMeasure API.
|
|
|
10
10
|
- [Cropping](#cropping)
|
|
11
11
|
- [Cropping region calulation](#cropping-region-calulation)
|
|
12
12
|
- [Executing the crop](#executing-the-crop)
|
|
13
|
+
- [Working with data from GPU texture buffers](#working-with-data-from-gpu-texture-buffers)
|
|
14
|
+
- [Image data bit depth](#image-data-bit-depth)
|
|
13
15
|
- [Affines](#affines)
|
|
14
16
|
- [World metadata](#world-metadata)
|
|
15
17
|
- [Composing the request payload](#composing-the-request-payload)
|
|
16
18
|
|
|
19
|
+
You can find the primary documentation site at [https://docs.imeasure.bettermedicine.ai](https://docs.imeasure.bettermedicine.ai).
|
|
20
|
+
Module references mentioned in this document can be located under the `@bettermedicine/namespaces` sidebar route.
|
|
21
|
+
|
|
17
22
|
## Installation
|
|
18
23
|
|
|
19
24
|
```bash
|
|
@@ -89,7 +94,15 @@ const {
|
|
|
89
94
|
} = imageCrop;
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
> :bulb: **Note**: In case the measurement we're generating the crop region around is near the edge of the volume or in case the measurement
|
|
98
|
+
> is long enough, the crop region may extend beyond the volume's boundaries. In such cases, the consumer is expected to provide a non-cuboid input
|
|
99
|
+
> volume but still preserve metadata about the originally intended crop region. The utilities provided in the cropping module will handle this
|
|
100
|
+
> case automatically by supplying `cropBoundariesUnclippedVoxel` and `cropCuboidCenterVoxel` corresponding to the original crop region, while the
|
|
101
|
+
> `cropBoundariesVoxel`, `cropBoundariesWorld` and `shape` attributes in `TCropMetadata` will be adjusted to the actual effective cropped region.
|
|
102
|
+
|
|
103
|
+
##### Working with data from GPU texture buffers
|
|
104
|
+
|
|
105
|
+
In modern versions of Cornerstone 3D, unless forced by configuration, the image volume's voxel data only exists in the GPU texture buffer.
|
|
93
106
|
If the above function outputs empty `voxelValues`, we need to take a few additional steps to ensure the data is reachable by our code:
|
|
94
107
|
|
|
95
108
|
```ts
|
|
@@ -183,6 +196,24 @@ function getVTKImageData(cornerstoneVolumeID: VolumeID): vtkImageData {
|
|
|
183
196
|
}
|
|
184
197
|
```
|
|
185
198
|
|
|
199
|
+
##### Image data bit depth
|
|
200
|
+
|
|
201
|
+
DICOM CT HU values typically fit within the range allowance of a 16-bit signed integer. As such, this is what the iMeasure API expects
|
|
202
|
+
as the scalar data type. Unless you're forcing 16-bit textures via OHIF config, the scalar data may be of the `Float32Array` type.
|
|
203
|
+
You can slash the converted `vtkImageData` object's memory footprint by half by using our `formatting.float32ArrayToInt16Array`
|
|
204
|
+
utility inlined into the above `vtkDataArray` instantiation:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { formatting } from '@bettermedicine/imeasure';
|
|
208
|
+
|
|
209
|
+
const da = vtkDataArray.newInstance({
|
|
210
|
+
...
|
|
211
|
+
values: formatting.float32ArrayToInt16Array(
|
|
212
|
+
baseVolume.voxelManager.getCompleteScalarDataArray()
|
|
213
|
+
),
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
186
217
|
#### Affines
|
|
187
218
|
|
|
188
219
|
In order to accurately calculate the crop region, we need some more information about the the shape, orientation, origin and spacing of the series' world coordinate system. The standard way to express this is through an affine transformation matrix - a 4x4 matrix that describes how to transform points from the voxel coordinate system to the world coordinate system.
|
|
@@ -238,8 +269,7 @@ const voxelPoint = math.affines.affineTranslateVec3(worldPoint, worldToVoxelMatr
|
|
|
238
269
|
|
|
239
270
|
### World metadata
|
|
240
271
|
|
|
241
|
-
|
|
242
|
-
please refer to the IO module's documentation for the `IStaticIMeasurePayload` type.
|
|
272
|
+
Please refer to the IO module's documentation for the `IStaticIMeasurePayload` type.
|
|
243
273
|
|
|
244
274
|
### Composing the request payload
|
|
245
275
|
|
|
@@ -251,6 +281,7 @@ const worldMetadata = ...
|
|
|
251
281
|
const cropMetadata = ...
|
|
252
282
|
const clicks = ...
|
|
253
283
|
const id = uuidv4();
|
|
284
|
+
// note: this should be an Int16Array - see the bit depth section above.
|
|
254
285
|
const imageVoxelValues = ... // the flattened array of voxel values from the cropped region
|
|
255
286
|
|
|
256
287
|
const requestPayload = formatting.formatStaticPayload(
|
|
@@ -267,7 +298,8 @@ const requestPayload = formatting.formatStaticPayload(
|
|
|
267
298
|
const resp = await fetch('https://api.bettermedicine.com/imeasure', {
|
|
268
299
|
method: 'POST',
|
|
269
300
|
body: requestPayload,
|
|
270
|
-
// note: setting headers manually here is not recommended as
|
|
301
|
+
// note: setting content-type headers manually here is not recommended as `fetch`
|
|
302
|
+
// should handle it correctly automatically.
|
|
271
303
|
});
|
|
272
304
|
|
|
273
305
|
const responseData = await resp.json();
|
package/dist/math/cropping.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { affineTranslateVec3, invertAffineMatrix } from "./affines";
|
|
2
2
|
import { INTERACTIVE_IMEASURE_BASE } from "../constants";
|
|
3
|
-
import {
|
|
3
|
+
import { roundPoint3 } from "./utility";
|
|
4
|
+
const worldLengthAndAffineToCropCuboidDimensions = (worldLength, worldToVoxelAffine) => {
|
|
5
|
+
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
6
|
+
// we effectively need to make the length of the measurement fit the crop region
|
|
7
|
+
// in all 3 dimensions.
|
|
8
|
+
const pixelSpacing = voxelToWorldAffine[0];
|
|
9
|
+
const sliceThickness = voxelToWorldAffine[10];
|
|
10
|
+
// calculate the length in voxel space
|
|
11
|
+
let xyLength = Math.max(INTERACTIVE_IMEASURE_BASE.X, Math.round(worldLength / pixelSpacing) +
|
|
12
|
+
INTERACTIVE_IMEASURE_BASE.PADDING * 2);
|
|
13
|
+
if (xyLength % 2 !== 0) {
|
|
14
|
+
xyLength += 1; // ensure even number
|
|
15
|
+
}
|
|
16
|
+
let zLength = Math.max(INTERACTIVE_IMEASURE_BASE.Z, Math.round((xyLength * pixelSpacing) / sliceThickness));
|
|
17
|
+
if (zLength % 2 !== 0) {
|
|
18
|
+
zLength += 1; // ensure even number
|
|
19
|
+
}
|
|
20
|
+
return [xyLength, xyLength, zLength];
|
|
21
|
+
};
|
|
4
22
|
// TODO: should expand scale based on z-axis input point diff.
|
|
5
23
|
// Tests first.
|
|
6
24
|
export const getInteractiveImeasureCropCuboidDimensions = (measurement, worldToVoxelAffine) => {
|
|
7
|
-
const [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// the model always expects an even number of voxels on each axis
|
|
14
|
-
if (xyCubeSize % 2 !== 0) {
|
|
15
|
-
xyCubeSize += 1;
|
|
16
|
-
}
|
|
17
|
-
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
18
|
-
const maxDistanceBetweenPoints = Math.max(x_size, y_size);
|
|
19
|
-
const zLength = calculateZAxisLength(maxDistanceBetweenPoints, voxelToWorldAffine);
|
|
20
|
-
return [xyCubeSize, xyCubeSize, zLength];
|
|
25
|
+
const [worldStart, worldEnd] = measurement;
|
|
26
|
+
// calculate the world-space length of the measurement
|
|
27
|
+
const worldLength = Math.sqrt(Math.pow(worldEnd[0] - worldStart[0], 2) +
|
|
28
|
+
Math.pow(worldEnd[1] - worldStart[1], 2) +
|
|
29
|
+
Math.pow(worldEnd[2] - worldStart[2], 2));
|
|
30
|
+
return worldLengthAndAffineToCropCuboidDimensions(worldLength, worldToVoxelAffine);
|
|
21
31
|
};
|
|
22
32
|
const getCropCuboidFromCenterAndEdgeLengths = (voxelSpaceCenter, edgeLengths) => {
|
|
23
33
|
const minCropCoordinates = [
|
|
@@ -47,6 +57,7 @@ const getCropCuboidFromCenterAndEdgeLengths = (voxelSpaceCenter, edgeLengths) =>
|
|
|
47
57
|
export const calculateInteractiveImeasureCropRegion = (worldBasePoints, voxelToWorldAffine) => {
|
|
48
58
|
let center;
|
|
49
59
|
const singlePoint = worldBasePoints.length === 1;
|
|
60
|
+
let worldBaseLength = 0;
|
|
50
61
|
if (singlePoint) {
|
|
51
62
|
center = worldBasePoints[0];
|
|
52
63
|
}
|
|
@@ -56,19 +67,15 @@ export const calculateInteractiveImeasureCropRegion = (worldBasePoints, voxelToW
|
|
|
56
67
|
(worldBasePoints[0][1] + worldBasePoints[1][1]) / 2,
|
|
57
68
|
(worldBasePoints[0][2] + worldBasePoints[1][2]) / 2,
|
|
58
69
|
];
|
|
70
|
+
worldBaseLength = Math.sqrt(Math.pow(worldBasePoints[1][0] - worldBasePoints[0][0], 2) +
|
|
71
|
+
Math.pow(worldBasePoints[1][1] - worldBasePoints[0][1], 2) +
|
|
72
|
+
Math.pow(worldBasePoints[1][2] - worldBasePoints[0][2], 2));
|
|
59
73
|
}
|
|
60
74
|
// we want to convert (and round) this to voxel coordinates
|
|
61
75
|
const worldToVoxelAffine = invertAffineMatrix(voxelToWorldAffine);
|
|
62
76
|
const voxelSpaceCenter = roundPoint3(affineTranslateVec3(center, worldToVoxelAffine));
|
|
63
77
|
// if we only have a single click, use constants for edge lengths.
|
|
64
|
-
|
|
65
|
-
let y_edge = INTERACTIVE_IMEASURE_BASE.Y;
|
|
66
|
-
let z_edge = INTERACTIVE_IMEASURE_BASE.Z;
|
|
67
|
-
if (!singlePoint) {
|
|
68
|
-
// if we have two clicks, calculate the edges based on the points
|
|
69
|
-
const tempProposedCuboidEdges = getInteractiveImeasureCropCuboidDimensions(worldBasePoints, worldToVoxelAffine);
|
|
70
|
-
[x_edge, y_edge, z_edge] = tempProposedCuboidEdges;
|
|
71
|
-
}
|
|
78
|
+
const [x_edge, y_edge, z_edge] = worldLengthAndAffineToCropCuboidDimensions(worldBaseLength, worldToVoxelAffine);
|
|
72
79
|
const { cropCubePoints, croppingPlanes } = getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter, {
|
|
73
80
|
x: x_edge,
|
|
74
81
|
y: y_edge,
|
|
@@ -95,17 +102,17 @@ export const calculateInteractiveImeasureCropRegion = (worldBasePoints, voxelToW
|
|
|
95
102
|
* The function maintains the center and cropping planes, and updates edge lengths if necessary.
|
|
96
103
|
*/
|
|
97
104
|
export const calculateInteractiveImeasureBoundingBox = (firstClicks, extraClicks = [], voxelToWorldAffine) => {
|
|
98
|
-
const worldToVoxelAffine = invertAffineMatrix(voxelToWorldAffine);
|
|
99
105
|
const { center, voxelSpaceCenter, cropCubePoints, croppingPlanes, edgeLengths, } = calculateInteractiveImeasureCropRegion(firstClicks, voxelToWorldAffine);
|
|
100
106
|
let maxDistanceFromCenter = 0;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
extraClicks.forEach((point) => {
|
|
108
|
+
const distanceFromCenter = Math.sqrt(Math.pow(point[0] - center[0], 2) +
|
|
109
|
+
Math.pow(point[1] - center[1], 2) +
|
|
110
|
+
Math.pow(point[2] - center[2], 2));
|
|
111
|
+
maxDistanceFromCenter = Math.max(distanceFromCenter, maxDistanceFromCenter);
|
|
105
112
|
});
|
|
106
|
-
const
|
|
113
|
+
const newEdgeLengths = worldLengthAndAffineToCropCuboidDimensions(maxDistanceFromCenter * 2, voxelToWorldAffine);
|
|
107
114
|
// check if we need to expand the box at all?
|
|
108
|
-
if (
|
|
115
|
+
if (newEdgeLengths[0] <= edgeLengths.x) {
|
|
109
116
|
return {
|
|
110
117
|
center,
|
|
111
118
|
voxelSpaceCenter,
|
|
@@ -114,19 +121,21 @@ export const calculateInteractiveImeasureBoundingBox = (firstClicks, extraClicks
|
|
|
114
121
|
edgeLengths,
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
const { cropCubePoints: newCropCubePoints, croppingPlanes: newCroppingPlanes, } = getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter, newEdgeLengths);
|
|
124
|
+
const { cropCubePoints: newCropCubePoints, croppingPlanes: newCroppingPlanes, } = getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter, {
|
|
125
|
+
x: newEdgeLengths[0],
|
|
126
|
+
y: newEdgeLengths[1],
|
|
127
|
+
z: newEdgeLengths[2],
|
|
128
|
+
});
|
|
124
129
|
return {
|
|
125
130
|
center,
|
|
126
131
|
voxelSpaceCenter,
|
|
127
132
|
cropCubePoints: newCropCubePoints,
|
|
128
133
|
croppingPlanes: newCroppingPlanes,
|
|
129
|
-
edgeLengths:
|
|
134
|
+
edgeLengths: {
|
|
135
|
+
x: newEdgeLengths[0],
|
|
136
|
+
y: newEdgeLengths[1],
|
|
137
|
+
z: newEdgeLengths[2],
|
|
138
|
+
},
|
|
130
139
|
};
|
|
131
140
|
};
|
|
132
141
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { mat4 } from "gl-matrix";
|
|
2
|
+
import { it, describe, expect } from "vitest";
|
|
3
|
+
import { calculateInteractiveImeasureBoundingBox, getInteractiveImeasureCropCuboidDimensions, } from "./cropping";
|
|
4
|
+
const identityMatrix = mat4.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
5
|
+
// a matrix where the pixel spacing is 1mm and slice thickness is 2mm
|
|
6
|
+
// since these are world-to-voxel affine matrices, the 0.5 indicates
|
|
7
|
+
// that for every z-axis millimeter in the world we move 0.5 units in voxel space
|
|
8
|
+
const thickSlicesMatrix = mat4.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, // slice thickness
|
|
9
|
+
0, 0, 0, 0, 1);
|
|
10
|
+
describe("getInteractiveImeasureCropCuboidDimensions", () => {
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
it: "returns expected minimums in case of a very short base measurement",
|
|
14
|
+
measurement: [
|
|
15
|
+
[100, 100, 256],
|
|
16
|
+
[101, 101, 256],
|
|
17
|
+
],
|
|
18
|
+
expectedDimensions: [128, 128, 128],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
it: "returns expected dimensions for a larger measurement",
|
|
22
|
+
measurement: [
|
|
23
|
+
[0, 100, 256],
|
|
24
|
+
[0, 0, 256],
|
|
25
|
+
],
|
|
26
|
+
// the length of the line in the largest axis (X) is 100, so the z-axis length
|
|
27
|
+
// is 100 + 2*padding = 116 but x/y dimensions are not scaled as they still fit
|
|
28
|
+
// the minimum size of 128
|
|
29
|
+
expectedDimensions: [128, 128, 128],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
it: "returns expected dimensions that extend past minimums in x/y axes",
|
|
33
|
+
measurement: [
|
|
34
|
+
[0, 200, 256],
|
|
35
|
+
[0, 0, 256],
|
|
36
|
+
],
|
|
37
|
+
expectedDimensions: [216, 216, 216],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
it: "properly scales z-axis even if x/y movement is minimal",
|
|
41
|
+
measurement: [
|
|
42
|
+
[0, 0, 0],
|
|
43
|
+
[0, 0, 100],
|
|
44
|
+
],
|
|
45
|
+
// length of the line in the largest axis is 100 so z-axis of crop cuboid
|
|
46
|
+
// should scale accordingly
|
|
47
|
+
expectedDimensions: [128, 128, 128],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
it: "properly scales z-axis when movement happens in all axes",
|
|
51
|
+
measurement: [
|
|
52
|
+
[0, 0, 0],
|
|
53
|
+
[50, 50, 50],
|
|
54
|
+
],
|
|
55
|
+
expectedDimensions: [128, 128, 128],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
it: "properly scales all axes when movement happens in all axes and doesn't fit minimums",
|
|
59
|
+
measurement: [
|
|
60
|
+
[0, 0, 0],
|
|
61
|
+
[200, 200, 200],
|
|
62
|
+
],
|
|
63
|
+
expectedDimensions: [362, 362, 362],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices",
|
|
67
|
+
measurement: [
|
|
68
|
+
[0, 0, 0],
|
|
69
|
+
[0, 0, 100],
|
|
70
|
+
],
|
|
71
|
+
// 100/2 + 2*padding = 50 + 16 = 66
|
|
72
|
+
expectedDimensions: [128, 128, 64],
|
|
73
|
+
matrix: thickSlicesMatrix,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices when z-axis doesn't fit minimums",
|
|
77
|
+
measurement: [
|
|
78
|
+
[0, 0, 0],
|
|
79
|
+
[0, 0, 200],
|
|
80
|
+
],
|
|
81
|
+
// the length of the measurement is 200 -> measurement + padding gives us 216 in x/y and
|
|
82
|
+
// half that in z-axis due to slice thickness
|
|
83
|
+
expectedDimensions: [216, 216, 108],
|
|
84
|
+
matrix: thickSlicesMatrix,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
it: "properly scales all axes with thick slices",
|
|
88
|
+
measurement: [
|
|
89
|
+
[0, 0, 0],
|
|
90
|
+
[200, 200, 200],
|
|
91
|
+
],
|
|
92
|
+
expectedDimensions: [362, 362, 182],
|
|
93
|
+
matrix: thickSlicesMatrix,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
it: "properly scales all axes if z-axis diff is minimal but not null",
|
|
97
|
+
measurement: [
|
|
98
|
+
[0, 0, 0],
|
|
99
|
+
[200, 200, 100],
|
|
100
|
+
],
|
|
101
|
+
// whereas a 100mm movement in z-axis would result in 66 voxels of cuboid length,
|
|
102
|
+
// we _are_ still trying to keep it cuboid shaped - since the naive z-axis calculation
|
|
103
|
+
// of `max(x_length, y_length) * proportion` yields 108 voxels, we use that as it
|
|
104
|
+
// is bigger and thus more likely to fit a spherical shape.
|
|
105
|
+
expectedDimensions: [316, 316, 158],
|
|
106
|
+
matrix: thickSlicesMatrix,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const hasOnly = testCases.some((test) => test.only);
|
|
110
|
+
testCases.forEach(({ it: name, measurement, expectedDimensions, matrix, only }) => {
|
|
111
|
+
if (hasOnly && !only) {
|
|
112
|
+
return; // skip this test if there is an 'only' test case
|
|
113
|
+
}
|
|
114
|
+
it(name, () => {
|
|
115
|
+
const dims = getInteractiveImeasureCropCuboidDimensions(measurement, matrix || identityMatrix);
|
|
116
|
+
expect(dims).toEqual(expectedDimensions);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("calculateInteractiveImeasureBoundingBox", () => {
|
|
121
|
+
const testCases = [
|
|
122
|
+
{
|
|
123
|
+
it: "returns a base bounding box for a single click",
|
|
124
|
+
baseClicks: [[100, 100, 100]],
|
|
125
|
+
extraClicks: [],
|
|
126
|
+
expectedCenter: [100, 100, 100],
|
|
127
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
it: "returns a base bounding box for a very short measurement",
|
|
131
|
+
baseClicks: [
|
|
132
|
+
[99, 99, 99],
|
|
133
|
+
[101, 101, 101],
|
|
134
|
+
],
|
|
135
|
+
extraClicks: [],
|
|
136
|
+
expectedCenter: [100, 100, 100],
|
|
137
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
it: "returns base bounding box if extra clicks fit within the initial box",
|
|
141
|
+
baseClicks: [
|
|
142
|
+
[99, 99, 99],
|
|
143
|
+
[101, 101, 101],
|
|
144
|
+
],
|
|
145
|
+
extraClicks: [
|
|
146
|
+
[100, 100, 100],
|
|
147
|
+
[102, 102, 102],
|
|
148
|
+
],
|
|
149
|
+
expectedCenter: [100, 100, 100],
|
|
150
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
it: "returns scaled box if base clicks don't fit minimums but extra clicks fit",
|
|
154
|
+
baseClicks: [
|
|
155
|
+
[50, 50, 50],
|
|
156
|
+
[150, 150, 150],
|
|
157
|
+
],
|
|
158
|
+
extraClicks: [[100, 100, 100]],
|
|
159
|
+
expectedCenter: [100, 100, 100],
|
|
160
|
+
expectedEdgeLengths: { x: 190, y: 190, z: 190 },
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
it: "returns scaled box if base clicks fit minimums but extra clicks don't",
|
|
164
|
+
baseClicks: [
|
|
165
|
+
[99, 99, 99],
|
|
166
|
+
[101, 101, 101],
|
|
167
|
+
],
|
|
168
|
+
extraClicks: [[99, 99, 250]],
|
|
169
|
+
expectedCenter: [100, 100, 100],
|
|
170
|
+
expectedEdgeLengths: { x: 316, y: 316, z: 316 },
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const hasOnly = testCases.some((test) => test.only);
|
|
174
|
+
testCases.forEach(({ it: name, baseClicks, extraClicks, worldToVoxelAffine, expectedCenter, expectedEdgeLengths, only, }) => {
|
|
175
|
+
if (hasOnly && !only) {
|
|
176
|
+
return; // skip this test if there is an 'only' test case
|
|
177
|
+
}
|
|
178
|
+
it(name, () => {
|
|
179
|
+
const result = calculateInteractiveImeasureBoundingBox(baseClicks, extraClicks, worldToVoxelAffine || identityMatrix);
|
|
180
|
+
expect(result.center).toEqual(expectedCenter);
|
|
181
|
+
expect(result.edgeLengths).toEqual(expectedEdgeLengths);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { mat4 } from "gl-matrix";
|
|
2
|
+
import { it, describe, expect, test } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
calculateInteractiveImeasureBoundingBox,
|
|
5
|
+
getInteractiveImeasureCropCuboidDimensions,
|
|
6
|
+
} from "./cropping";
|
|
7
|
+
import { Point3D } from "../types/cornerstone";
|
|
8
|
+
|
|
9
|
+
type TCropTestCase = {
|
|
10
|
+
it: string;
|
|
11
|
+
measurement: [[number, number, number], [number, number, number]];
|
|
12
|
+
expectedDimensions: [number, number, number];
|
|
13
|
+
matrix?: mat4; // an undefined matrix will default to identity
|
|
14
|
+
only?: true; // only run this test case - useful for debugging
|
|
15
|
+
};
|
|
16
|
+
const identityMatrix = mat4.fromValues(
|
|
17
|
+
1,
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
1,
|
|
23
|
+
0,
|
|
24
|
+
0,
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
1,
|
|
28
|
+
0,
|
|
29
|
+
0,
|
|
30
|
+
0,
|
|
31
|
+
0,
|
|
32
|
+
1
|
|
33
|
+
);
|
|
34
|
+
// a matrix where the pixel spacing is 1mm and slice thickness is 2mm
|
|
35
|
+
// since these are world-to-voxel affine matrices, the 0.5 indicates
|
|
36
|
+
// that for every z-axis millimeter in the world we move 0.5 units in voxel space
|
|
37
|
+
const thickSlicesMatrix = mat4.fromValues(
|
|
38
|
+
1,
|
|
39
|
+
0,
|
|
40
|
+
0,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
1,
|
|
44
|
+
0,
|
|
45
|
+
0,
|
|
46
|
+
0,
|
|
47
|
+
0,
|
|
48
|
+
0.5, // slice thickness
|
|
49
|
+
0,
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
1
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
describe("getInteractiveImeasureCropCuboidDimensions", () => {
|
|
57
|
+
const testCases: TCropTestCase[] = [
|
|
58
|
+
{
|
|
59
|
+
it: "returns expected minimums in case of a very short base measurement",
|
|
60
|
+
measurement: [
|
|
61
|
+
[100, 100, 256],
|
|
62
|
+
[101, 101, 256],
|
|
63
|
+
],
|
|
64
|
+
expectedDimensions: [128, 128, 128],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
it: "returns expected dimensions for a larger measurement",
|
|
68
|
+
measurement: [
|
|
69
|
+
[0, 100, 256],
|
|
70
|
+
[0, 0, 256],
|
|
71
|
+
],
|
|
72
|
+
// the length of the line in the largest axis (X) is 100, so the z-axis length
|
|
73
|
+
// is 100 + 2*padding = 116 but x/y dimensions are not scaled as they still fit
|
|
74
|
+
// the minimum size of 128
|
|
75
|
+
expectedDimensions: [128, 128, 128],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
it: "returns expected dimensions that extend past minimums in x/y axes",
|
|
79
|
+
measurement: [
|
|
80
|
+
[0, 200, 256],
|
|
81
|
+
[0, 0, 256],
|
|
82
|
+
],
|
|
83
|
+
expectedDimensions: [216, 216, 216],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
it: "properly scales z-axis even if x/y movement is minimal",
|
|
87
|
+
measurement: [
|
|
88
|
+
[0, 0, 0],
|
|
89
|
+
[0, 0, 100],
|
|
90
|
+
],
|
|
91
|
+
// length of the line in the largest axis is 100 so z-axis of crop cuboid
|
|
92
|
+
// should scale accordingly
|
|
93
|
+
expectedDimensions: [128, 128, 128],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
it: "properly scales z-axis when movement happens in all axes",
|
|
97
|
+
measurement: [
|
|
98
|
+
[0, 0, 0],
|
|
99
|
+
[50, 50, 50],
|
|
100
|
+
],
|
|
101
|
+
expectedDimensions: [128, 128, 128],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
it: "properly scales all axes when movement happens in all axes and doesn't fit minimums",
|
|
105
|
+
measurement: [
|
|
106
|
+
[0, 0, 0],
|
|
107
|
+
[200, 200, 200],
|
|
108
|
+
],
|
|
109
|
+
expectedDimensions: [362, 362, 362],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices",
|
|
113
|
+
measurement: [
|
|
114
|
+
[0, 0, 0],
|
|
115
|
+
[0, 0, 100],
|
|
116
|
+
],
|
|
117
|
+
// 100/2 + 2*padding = 50 + 16 = 66
|
|
118
|
+
expectedDimensions: [128, 128, 64],
|
|
119
|
+
matrix: thickSlicesMatrix,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices when z-axis doesn't fit minimums",
|
|
123
|
+
measurement: [
|
|
124
|
+
[0, 0, 0],
|
|
125
|
+
[0, 0, 200],
|
|
126
|
+
],
|
|
127
|
+
// the length of the measurement is 200 -> measurement + padding gives us 216 in x/y and
|
|
128
|
+
// half that in z-axis due to slice thickness
|
|
129
|
+
expectedDimensions: [216, 216, 108],
|
|
130
|
+
matrix: thickSlicesMatrix,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
it: "properly scales all axes with thick slices",
|
|
134
|
+
measurement: [
|
|
135
|
+
[0, 0, 0],
|
|
136
|
+
[200, 200, 200],
|
|
137
|
+
],
|
|
138
|
+
expectedDimensions: [362, 362, 182],
|
|
139
|
+
matrix: thickSlicesMatrix,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
it: "properly scales all axes if z-axis diff is minimal but not null",
|
|
143
|
+
measurement: [
|
|
144
|
+
[0, 0, 0],
|
|
145
|
+
[200, 200, 100],
|
|
146
|
+
],
|
|
147
|
+
// whereas a 100mm movement in z-axis would result in 66 voxels of cuboid length,
|
|
148
|
+
// we _are_ still trying to keep it cuboid shaped - since the naive z-axis calculation
|
|
149
|
+
// of `max(x_length, y_length) * proportion` yields 108 voxels, we use that as it
|
|
150
|
+
// is bigger and thus more likely to fit a spherical shape.
|
|
151
|
+
expectedDimensions: [316, 316, 158],
|
|
152
|
+
matrix: thickSlicesMatrix,
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
const hasOnly = testCases.some((test) => test.only);
|
|
156
|
+
testCases.forEach(
|
|
157
|
+
({ it: name, measurement, expectedDimensions, matrix, only }) => {
|
|
158
|
+
if (hasOnly && !only) {
|
|
159
|
+
return; // skip this test if there is an 'only' test case
|
|
160
|
+
}
|
|
161
|
+
it(name, () => {
|
|
162
|
+
const dims = getInteractiveImeasureCropCuboidDimensions(
|
|
163
|
+
measurement,
|
|
164
|
+
matrix || identityMatrix
|
|
165
|
+
);
|
|
166
|
+
expect(dims).toEqual(expectedDimensions);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
type TBBTestCase = {
|
|
173
|
+
it: string;
|
|
174
|
+
baseClicks: [Point3D, Point3D] | [Point3D];
|
|
175
|
+
extraClicks: Point3D[];
|
|
176
|
+
worldToVoxelAffine?: mat4;
|
|
177
|
+
expectedCenter: [number, number, number];
|
|
178
|
+
expectedEdgeLengths: { x: number; y: number; z: number };
|
|
179
|
+
only?: true; // only run this test case - useful for debugging
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
describe("calculateInteractiveImeasureBoundingBox", () => {
|
|
183
|
+
const testCases: TBBTestCase[] = [
|
|
184
|
+
{
|
|
185
|
+
it: "returns a base bounding box for a single click",
|
|
186
|
+
baseClicks: [[100, 100, 100]],
|
|
187
|
+
extraClicks: [],
|
|
188
|
+
expectedCenter: [100, 100, 100],
|
|
189
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
it: "returns a base bounding box for a very short measurement",
|
|
193
|
+
baseClicks: [
|
|
194
|
+
[99, 99, 99],
|
|
195
|
+
[101, 101, 101],
|
|
196
|
+
],
|
|
197
|
+
extraClicks: [],
|
|
198
|
+
expectedCenter: [100, 100, 100],
|
|
199
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
it: "returns base bounding box if extra clicks fit within the initial box",
|
|
203
|
+
baseClicks: [
|
|
204
|
+
[99, 99, 99],
|
|
205
|
+
[101, 101, 101],
|
|
206
|
+
],
|
|
207
|
+
extraClicks: [
|
|
208
|
+
[100, 100, 100],
|
|
209
|
+
[102, 102, 102],
|
|
210
|
+
],
|
|
211
|
+
expectedCenter: [100, 100, 100],
|
|
212
|
+
expectedEdgeLengths: { x: 128, y: 128, z: 128 },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
it: "returns scaled box if base clicks don't fit minimums but extra clicks fit",
|
|
216
|
+
baseClicks: [
|
|
217
|
+
[50, 50, 50],
|
|
218
|
+
[150, 150, 150],
|
|
219
|
+
],
|
|
220
|
+
extraClicks: [[100, 100, 100]],
|
|
221
|
+
expectedCenter: [100, 100, 100],
|
|
222
|
+
expectedEdgeLengths: { x: 190, y: 190, z: 190 },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
it: "returns scaled box if base clicks fit minimums but extra clicks don't",
|
|
226
|
+
baseClicks: [
|
|
227
|
+
[99, 99, 99],
|
|
228
|
+
[101, 101, 101],
|
|
229
|
+
],
|
|
230
|
+
extraClicks: [[99, 99, 250]],
|
|
231
|
+
expectedCenter: [100, 100, 100],
|
|
232
|
+
expectedEdgeLengths: { x: 316, y: 316, z: 316 },
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
const hasOnly = testCases.some((test) => test.only);
|
|
236
|
+
testCases.forEach(
|
|
237
|
+
({
|
|
238
|
+
it: name,
|
|
239
|
+
baseClicks,
|
|
240
|
+
extraClicks,
|
|
241
|
+
worldToVoxelAffine,
|
|
242
|
+
expectedCenter,
|
|
243
|
+
expectedEdgeLengths,
|
|
244
|
+
only,
|
|
245
|
+
}) => {
|
|
246
|
+
if (hasOnly && !only) {
|
|
247
|
+
return; // skip this test if there is an 'only' test case
|
|
248
|
+
}
|
|
249
|
+
it(name, () => {
|
|
250
|
+
const result = calculateInteractiveImeasureBoundingBox(
|
|
251
|
+
baseClicks,
|
|
252
|
+
extraClicks,
|
|
253
|
+
worldToVoxelAffine || identityMatrix
|
|
254
|
+
);
|
|
255
|
+
expect(result.center).toEqual(expectedCenter);
|
|
256
|
+
expect(result.edgeLengths).toEqual(expectedEdgeLengths);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
});
|
package/src/math/cropping.ts
CHANGED
|
@@ -10,37 +10,55 @@ import {
|
|
|
10
10
|
TIMeasureCropRegion,
|
|
11
11
|
} from "../types/crop";
|
|
12
12
|
|
|
13
|
+
const worldLengthAndAffineToCropCuboidDimensions = (
|
|
14
|
+
worldLength: number,
|
|
15
|
+
worldToVoxelAffine: mat4
|
|
16
|
+
): [number, number, number] => {
|
|
17
|
+
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
18
|
+
// we effectively need to make the length of the measurement fit the crop region
|
|
19
|
+
// in all 3 dimensions.
|
|
20
|
+
const pixelSpacing = voxelToWorldAffine[0];
|
|
21
|
+
const sliceThickness = voxelToWorldAffine[10];
|
|
22
|
+
// calculate the length in voxel space
|
|
23
|
+
let xyLength = Math.max(
|
|
24
|
+
INTERACTIVE_IMEASURE_BASE.X,
|
|
25
|
+
Math.round(worldLength / pixelSpacing) +
|
|
26
|
+
INTERACTIVE_IMEASURE_BASE.PADDING * 2
|
|
27
|
+
);
|
|
28
|
+
if (xyLength % 2 !== 0) {
|
|
29
|
+
xyLength += 1; // ensure even number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let zLength = Math.max(
|
|
33
|
+
INTERACTIVE_IMEASURE_BASE.Z,
|
|
34
|
+
Math.round((xyLength * pixelSpacing) / sliceThickness)
|
|
35
|
+
);
|
|
36
|
+
if (zLength % 2 !== 0) {
|
|
37
|
+
zLength += 1; // ensure even number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [xyLength, xyLength, zLength];
|
|
41
|
+
};
|
|
42
|
+
|
|
13
43
|
// TODO: should expand scale based on z-axis input point diff.
|
|
14
44
|
// Tests first.
|
|
15
45
|
export const getInteractiveImeasureCropCuboidDimensions = (
|
|
16
46
|
measurement: [Point3D, Point3D],
|
|
17
47
|
worldToVoxelAffine: mat4
|
|
18
48
|
): number[] => {
|
|
19
|
-
const [
|
|
20
|
-
roundPoint3(affineTranslateVec3(pt, worldToVoxelAffine))
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const x_size =
|
|
24
|
-
Math.abs(voxelStart[0] - voxelEnd[0]) +
|
|
25
|
-
INTERACTIVE_IMEASURE_BASE.PADDING * 2;
|
|
26
|
-
const y_size =
|
|
27
|
-
Math.abs(voxelStart[1] - voxelEnd[1]) +
|
|
28
|
-
INTERACTIVE_IMEASURE_BASE.PADDING * 2;
|
|
29
|
-
|
|
30
|
-
let xyCubeSize = Math.max(x_size, y_size, INTERACTIVE_IMEASURE_BASE.X);
|
|
31
|
-
// the model always expects an even number of voxels on each axis
|
|
32
|
-
if (xyCubeSize % 2 !== 0) {
|
|
33
|
-
xyCubeSize += 1;
|
|
34
|
-
}
|
|
35
|
-
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
36
|
-
const maxDistanceBetweenPoints = Math.max(x_size, y_size);
|
|
49
|
+
const [worldStart, worldEnd] = measurement;
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
// calculate the world-space length of the measurement
|
|
52
|
+
const worldLength = Math.sqrt(
|
|
53
|
+
Math.pow(worldEnd[0] - worldStart[0], 2) +
|
|
54
|
+
Math.pow(worldEnd[1] - worldStart[1], 2) +
|
|
55
|
+
Math.pow(worldEnd[2] - worldStart[2], 2)
|
|
41
56
|
);
|
|
42
57
|
|
|
43
|
-
return
|
|
58
|
+
return worldLengthAndAffineToCropCuboidDimensions(
|
|
59
|
+
worldLength,
|
|
60
|
+
worldToVoxelAffine
|
|
61
|
+
);
|
|
44
62
|
};
|
|
45
63
|
|
|
46
64
|
const getCropCuboidFromCenterAndEdgeLengths = (
|
|
@@ -78,7 +96,7 @@ export const calculateInteractiveImeasureCropRegion = (
|
|
|
78
96
|
): TIMeasureCropRegion => {
|
|
79
97
|
let center: Point3D;
|
|
80
98
|
const singlePoint = worldBasePoints.length === 1;
|
|
81
|
-
|
|
99
|
+
let worldBaseLength = 0;
|
|
82
100
|
if (singlePoint) {
|
|
83
101
|
center = worldBasePoints[0];
|
|
84
102
|
} else {
|
|
@@ -87,6 +105,11 @@ export const calculateInteractiveImeasureCropRegion = (
|
|
|
87
105
|
(worldBasePoints[0][1] + worldBasePoints[1][1]) / 2,
|
|
88
106
|
(worldBasePoints[0][2] + worldBasePoints[1][2]) / 2,
|
|
89
107
|
];
|
|
108
|
+
worldBaseLength = Math.sqrt(
|
|
109
|
+
Math.pow(worldBasePoints[1][0] - worldBasePoints[0][0], 2) +
|
|
110
|
+
Math.pow(worldBasePoints[1][1] - worldBasePoints[0][1], 2) +
|
|
111
|
+
Math.pow(worldBasePoints[1][2] - worldBasePoints[0][2], 2)
|
|
112
|
+
);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
// we want to convert (and round) this to voxel coordinates
|
|
@@ -96,18 +119,10 @@ export const calculateInteractiveImeasureCropRegion = (
|
|
|
96
119
|
);
|
|
97
120
|
|
|
98
121
|
// if we only have a single click, use constants for edge lengths.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!singlePoint) {
|
|
104
|
-
// if we have two clicks, calculate the edges based on the points
|
|
105
|
-
const tempProposedCuboidEdges = getInteractiveImeasureCropCuboidDimensions(
|
|
106
|
-
worldBasePoints as [Point3D, Point3D],
|
|
107
|
-
worldToVoxelAffine
|
|
108
|
-
);
|
|
109
|
-
[x_edge, y_edge, z_edge] = tempProposedCuboidEdges;
|
|
110
|
-
}
|
|
122
|
+
const [x_edge, y_edge, z_edge] = worldLengthAndAffineToCropCuboidDimensions(
|
|
123
|
+
worldBaseLength,
|
|
124
|
+
worldToVoxelAffine
|
|
125
|
+
);
|
|
111
126
|
|
|
112
127
|
const { cropCubePoints, croppingPlanes } =
|
|
113
128
|
getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter, {
|
|
@@ -142,8 +157,6 @@ export const calculateInteractiveImeasureBoundingBox = (
|
|
|
142
157
|
extraClicks: Point3D[] = [],
|
|
143
158
|
voxelToWorldAffine: mat4
|
|
144
159
|
): TIMeasureCropRegion => {
|
|
145
|
-
const worldToVoxelAffine = invertAffineMatrix(voxelToWorldAffine);
|
|
146
|
-
|
|
147
160
|
const {
|
|
148
161
|
center,
|
|
149
162
|
voxelSpaceCenter,
|
|
@@ -153,24 +166,22 @@ export const calculateInteractiveImeasureBoundingBox = (
|
|
|
153
166
|
} = calculateInteractiveImeasureCropRegion(firstClicks, voxelToWorldAffine);
|
|
154
167
|
|
|
155
168
|
let maxDistanceFromCenter = 0;
|
|
156
|
-
const extraClicksInVoxelSpace = extraClicks.map((pt) =>
|
|
157
|
-
roundPoint3(affineTranslateVec3(pt, worldToVoxelAffine))
|
|
158
|
-
);
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Math.
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
extraClicks.forEach((point) => {
|
|
171
|
+
const distanceFromCenter = Math.sqrt(
|
|
172
|
+
Math.pow(point[0] - center[0], 2) +
|
|
173
|
+
Math.pow(point[1] - center[1], 2) +
|
|
174
|
+
Math.pow(point[2] - center[2], 2)
|
|
165
175
|
);
|
|
166
|
-
|
|
176
|
+
maxDistanceFromCenter = Math.max(distanceFromCenter, maxDistanceFromCenter);
|
|
167
177
|
});
|
|
168
178
|
|
|
169
|
-
const
|
|
170
|
-
maxDistanceFromCenter
|
|
171
|
-
|
|
179
|
+
const newEdgeLengths = worldLengthAndAffineToCropCuboidDimensions(
|
|
180
|
+
maxDistanceFromCenter * 2,
|
|
181
|
+
voxelToWorldAffine
|
|
182
|
+
);
|
|
172
183
|
// check if we need to expand the box at all?
|
|
173
|
-
if (
|
|
184
|
+
if (newEdgeLengths[0] <= edgeLengths.x) {
|
|
174
185
|
return {
|
|
175
186
|
center,
|
|
176
187
|
voxelSpaceCenter,
|
|
@@ -179,23 +190,24 @@ export const calculateInteractiveImeasureBoundingBox = (
|
|
|
179
190
|
edgeLengths,
|
|
180
191
|
};
|
|
181
192
|
}
|
|
182
|
-
const zLength = calculateZAxisLength(halfEdgeLength * 2, voxelToWorldAffine);
|
|
183
|
-
|
|
184
|
-
const newEdgeLengths = {
|
|
185
|
-
x: halfEdgeLength * 2,
|
|
186
|
-
y: halfEdgeLength * 2,
|
|
187
|
-
z: zLength,
|
|
188
|
-
};
|
|
189
193
|
const {
|
|
190
194
|
cropCubePoints: newCropCubePoints,
|
|
191
195
|
croppingPlanes: newCroppingPlanes,
|
|
192
|
-
} = getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter,
|
|
196
|
+
} = getCropCuboidFromCenterAndEdgeLengths(voxelSpaceCenter, {
|
|
197
|
+
x: newEdgeLengths[0],
|
|
198
|
+
y: newEdgeLengths[1],
|
|
199
|
+
z: newEdgeLengths[2],
|
|
200
|
+
});
|
|
193
201
|
return {
|
|
194
202
|
center,
|
|
195
203
|
voxelSpaceCenter,
|
|
196
204
|
cropCubePoints: newCropCubePoints,
|
|
197
205
|
croppingPlanes: newCroppingPlanes,
|
|
198
|
-
edgeLengths:
|
|
206
|
+
edgeLengths: {
|
|
207
|
+
x: newEdgeLengths[0],
|
|
208
|
+
y: newEdgeLengths[1],
|
|
209
|
+
z: newEdgeLengths[2],
|
|
210
|
+
},
|
|
199
211
|
};
|
|
200
212
|
};
|
|
201
213
|
|