@bettermedicine/imeasure 0.0.5 → 0.0.6-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 +24 -3
- package/dist/math/cropping.test.d.ts +1 -0
- package/dist/math/cropping.test.js +119 -0
- package/package.json +1 -1
- package/src/math/cropping.test.ts +168 -0
- package/src/math/cropping.ts +39 -6
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
|
@@ -5,18 +5,39 @@ import { calculateZAxisLength, roundPoint3 } from "./utility";
|
|
|
5
5
|
// Tests first.
|
|
6
6
|
export const getInteractiveImeasureCropCuboidDimensions = (measurement, worldToVoxelAffine) => {
|
|
7
7
|
const [voxelStart, voxelEnd] = measurement.map((pt) => roundPoint3(affineTranslateVec3(pt, worldToVoxelAffine)));
|
|
8
|
+
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
8
9
|
const x_size = Math.abs(voxelStart[0] - voxelEnd[0]) +
|
|
9
10
|
INTERACTIVE_IMEASURE_BASE.PADDING * 2;
|
|
10
11
|
const y_size = Math.abs(voxelStart[1] - voxelEnd[1]) +
|
|
11
12
|
INTERACTIVE_IMEASURE_BASE.PADDING * 2;
|
|
13
|
+
// if the z-axis distance between the two points is not null, we need to take
|
|
14
|
+
// z-axis length into account as well.
|
|
15
|
+
const zSizeWithoutPadding = Math.abs(voxelStart[2] - voxelEnd[2]);
|
|
16
|
+
const maxDistanceBetweenPointsAxially = Math.max(x_size, y_size);
|
|
17
|
+
const zLength = calculateZAxisLength(maxDistanceBetweenPointsAxially, voxelToWorldAffine);
|
|
18
|
+
if (zSizeWithoutPadding !== 0) {
|
|
19
|
+
const pixelSpacing = voxelToWorldAffine[0];
|
|
20
|
+
const sliceThickness = voxelToWorldAffine[10];
|
|
21
|
+
// lets start by calculating the proportion between x/y pixel spacing and
|
|
22
|
+
// series slice thickness.
|
|
23
|
+
const proportion = sliceThickness / pixelSpacing;
|
|
24
|
+
// then find the z-axis length based on the largest axis
|
|
25
|
+
// lets take z-axis length and scale it to x/y axis length
|
|
26
|
+
const z_size = Math.max(zSizeWithoutPadding + INTERACTIVE_IMEASURE_BASE.PADDING * 2, INTERACTIVE_IMEASURE_BASE.Z);
|
|
27
|
+
const zSizeScaledWithoutPadding = Math.round(zSizeWithoutPadding * proportion);
|
|
28
|
+
const zSizeScaledPadded = zSizeScaledWithoutPadding + 2 * INTERACTIVE_IMEASURE_BASE.PADDING;
|
|
29
|
+
return [
|
|
30
|
+
Math.max(x_size, zSizeScaledPadded, INTERACTIVE_IMEASURE_BASE.X),
|
|
31
|
+
Math.max(y_size, zSizeScaledPadded, INTERACTIVE_IMEASURE_BASE.Y),
|
|
32
|
+
Math.max(z_size, zLength),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
12
35
|
let xyCubeSize = Math.max(x_size, y_size, INTERACTIVE_IMEASURE_BASE.X);
|
|
13
36
|
// the model always expects an even number of voxels on each axis
|
|
14
37
|
if (xyCubeSize % 2 !== 0) {
|
|
15
38
|
xyCubeSize += 1;
|
|
16
39
|
}
|
|
17
|
-
|
|
18
|
-
const maxDistanceBetweenPoints = Math.max(x_size, y_size);
|
|
19
|
-
const zLength = calculateZAxisLength(maxDistanceBetweenPoints, voxelToWorldAffine);
|
|
40
|
+
console.log("zLength", zLength);
|
|
20
41
|
return [xyCubeSize, xyCubeSize, zLength];
|
|
21
42
|
};
|
|
22
43
|
const getCropCuboidFromCenterAndEdgeLengths = (voxelSpaceCenter, edgeLengths) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { mat4 } from "gl-matrix";
|
|
2
|
+
import { it, describe, expect } from "vitest";
|
|
3
|
+
import { getInteractiveImeasureCropCuboidDimensions } from "./cropping";
|
|
4
|
+
describe("getInteractiveImeasureCropCuboidDimensions", () => {
|
|
5
|
+
const identityMatrix = mat4.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
6
|
+
// a matrix where the pixel spacing is 1mm and slice thickness is 2mm
|
|
7
|
+
// since these are world-to-voxel affine matrices, the 0.5 indicates
|
|
8
|
+
// that for every z-axis millimeter in the world we move 0.5 units in voxel space
|
|
9
|
+
const thickSlicesMatrix = mat4.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, // slice thickness
|
|
10
|
+
0, 0, 0, 0, 1);
|
|
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, 32],
|
|
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, 116],
|
|
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, 116],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
it: "properly scales z-axis when movement happens in all axes",
|
|
51
|
+
measurement: [
|
|
52
|
+
[0, 0, 0],
|
|
53
|
+
[100, 100, 100],
|
|
54
|
+
],
|
|
55
|
+
expectedDimensions: [128, 128, 116],
|
|
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: [216, 216, 216],
|
|
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, 66],
|
|
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
|
+
// 200/2 + 2*padding = 100 + 16 = 116
|
|
82
|
+
expectedDimensions: [216, 216, 116],
|
|
83
|
+
matrix: thickSlicesMatrix,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
it: "properly scales all axes with thick slices",
|
|
87
|
+
measurement: [
|
|
88
|
+
[0, 0, 0],
|
|
89
|
+
[200, 200, 200],
|
|
90
|
+
],
|
|
91
|
+
// 200/2 + 2*padding = 100 + 16 = 116
|
|
92
|
+
expectedDimensions: [216, 216, 116],
|
|
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: [216, 216, 108],
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { mat4 } from "gl-matrix";
|
|
2
|
+
import { it, describe, expect } from "vitest";
|
|
3
|
+
import { Point3D } from "../types/cornerstone";
|
|
4
|
+
import { getInteractiveImeasureCropCuboidDimensions } from "./cropping";
|
|
5
|
+
|
|
6
|
+
type TCropTestCase = {
|
|
7
|
+
it: string;
|
|
8
|
+
measurement: [[number, number, number], [number, number, number]];
|
|
9
|
+
expectedDimensions: [number, number, number];
|
|
10
|
+
matrix?: mat4; // an undefined matrix will default to identity
|
|
11
|
+
only?: true; // only run this test case - useful for debugging
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("getInteractiveImeasureCropCuboidDimensions", () => {
|
|
15
|
+
const identityMatrix = mat4.fromValues(
|
|
16
|
+
1,
|
|
17
|
+
0,
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
1,
|
|
22
|
+
0,
|
|
23
|
+
0,
|
|
24
|
+
0,
|
|
25
|
+
0,
|
|
26
|
+
1,
|
|
27
|
+
0,
|
|
28
|
+
0,
|
|
29
|
+
0,
|
|
30
|
+
0,
|
|
31
|
+
1
|
|
32
|
+
);
|
|
33
|
+
// a matrix where the pixel spacing is 1mm and slice thickness is 2mm
|
|
34
|
+
// since these are world-to-voxel affine matrices, the 0.5 indicates
|
|
35
|
+
// that for every z-axis millimeter in the world we move 0.5 units in voxel space
|
|
36
|
+
const thickSlicesMatrix = mat4.fromValues(
|
|
37
|
+
1,
|
|
38
|
+
0,
|
|
39
|
+
0,
|
|
40
|
+
0,
|
|
41
|
+
0,
|
|
42
|
+
1,
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
0,
|
|
46
|
+
0,
|
|
47
|
+
0.5, // slice thickness
|
|
48
|
+
0,
|
|
49
|
+
0,
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
1
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const testCases: TCropTestCase[] = [
|
|
56
|
+
{
|
|
57
|
+
it: "returns expected minimums in case of a very short base measurement",
|
|
58
|
+
measurement: [
|
|
59
|
+
[100, 100, 256],
|
|
60
|
+
[101, 101, 256],
|
|
61
|
+
],
|
|
62
|
+
expectedDimensions: [128, 128, 32],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
it: "returns expected dimensions for a larger measurement",
|
|
66
|
+
measurement: [
|
|
67
|
+
[0, 100, 256],
|
|
68
|
+
[0, 0, 256],
|
|
69
|
+
],
|
|
70
|
+
// the length of the line in the largest axis (X) is 100, so the z-axis length
|
|
71
|
+
// is 100 + 2*padding = 116 but x/y dimensions are not scaled as they still fit
|
|
72
|
+
// the minimum size of 128
|
|
73
|
+
expectedDimensions: [128, 128, 116],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
it: "returns expected dimensions that extend past minimums in x/y axes",
|
|
77
|
+
measurement: [
|
|
78
|
+
[0, 200, 256],
|
|
79
|
+
[0, 0, 256],
|
|
80
|
+
],
|
|
81
|
+
expectedDimensions: [216, 216, 216],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
it: "properly scales z-axis even if x/y movement is minimal",
|
|
85
|
+
measurement: [
|
|
86
|
+
[0, 0, 0],
|
|
87
|
+
[0, 0, 100],
|
|
88
|
+
],
|
|
89
|
+
// length of the line in the largest axis is 100 so z-axis of crop cuboid
|
|
90
|
+
// should scale accordingly
|
|
91
|
+
expectedDimensions: [128, 128, 116],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
it: "properly scales z-axis when movement happens in all axes",
|
|
95
|
+
measurement: [
|
|
96
|
+
[0, 0, 0],
|
|
97
|
+
[100, 100, 100],
|
|
98
|
+
],
|
|
99
|
+
expectedDimensions: [128, 128, 116],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
it: "properly scales all axes when movement happens in all axes and doesn't fit minimums",
|
|
103
|
+
measurement: [
|
|
104
|
+
[0, 0, 0],
|
|
105
|
+
[200, 200, 200],
|
|
106
|
+
],
|
|
107
|
+
expectedDimensions: [216, 216, 216],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices",
|
|
111
|
+
measurement: [
|
|
112
|
+
[0, 0, 0],
|
|
113
|
+
[0, 0, 100],
|
|
114
|
+
],
|
|
115
|
+
// 100/2 + 2*padding = 50 + 16 = 66
|
|
116
|
+
expectedDimensions: [128, 128, 66],
|
|
117
|
+
matrix: thickSlicesMatrix,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
it: "properly scales z-axis with minimal x/y movement and thick slices when z-axis doesn't fit minimums",
|
|
121
|
+
measurement: [
|
|
122
|
+
[0, 0, 0],
|
|
123
|
+
[0, 0, 200],
|
|
124
|
+
],
|
|
125
|
+
// 200/2 + 2*padding = 100 + 16 = 116
|
|
126
|
+
expectedDimensions: [216, 216, 116],
|
|
127
|
+
matrix: thickSlicesMatrix,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
it: "properly scales all axes with thick slices",
|
|
131
|
+
measurement: [
|
|
132
|
+
[0, 0, 0],
|
|
133
|
+
[200, 200, 200],
|
|
134
|
+
],
|
|
135
|
+
// 200/2 + 2*padding = 100 + 16 = 116
|
|
136
|
+
expectedDimensions: [216, 216, 116],
|
|
137
|
+
matrix: thickSlicesMatrix,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
it: "properly scales all axes if z-axis diff is minimal but not null",
|
|
141
|
+
measurement: [
|
|
142
|
+
[0, 0, 0],
|
|
143
|
+
[200, 200, 100],
|
|
144
|
+
],
|
|
145
|
+
// whereas a 100mm movement in z-axis would result in 66 voxels of cuboid length,
|
|
146
|
+
// we _are_ still trying to keep it cuboid shaped - since the naive z-axis calculation
|
|
147
|
+
// of `max(x_length, y_length) * proportion` yields 108 voxels, we use that as it
|
|
148
|
+
// is bigger and thus more likely to fit a spherical shape.
|
|
149
|
+
expectedDimensions: [216, 216, 108],
|
|
150
|
+
matrix: thickSlicesMatrix,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const hasOnly = testCases.some((test) => test.only);
|
|
154
|
+
testCases.forEach(
|
|
155
|
+
({ it: name, measurement, expectedDimensions, matrix, only }) => {
|
|
156
|
+
if (hasOnly && !only) {
|
|
157
|
+
return; // skip this test if there is an 'only' test case
|
|
158
|
+
}
|
|
159
|
+
it(name, () => {
|
|
160
|
+
const dims = getInteractiveImeasureCropCuboidDimensions(
|
|
161
|
+
measurement,
|
|
162
|
+
matrix || identityMatrix
|
|
163
|
+
);
|
|
164
|
+
expect(dims).toEqual(expectedDimensions);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
});
|
package/src/math/cropping.ts
CHANGED
|
@@ -19,6 +19,7 @@ export const getInteractiveImeasureCropCuboidDimensions = (
|
|
|
19
19
|
const [voxelStart, voxelEnd] = measurement.map((pt) =>
|
|
20
20
|
roundPoint3(affineTranslateVec3(pt, worldToVoxelAffine))
|
|
21
21
|
);
|
|
22
|
+
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
22
23
|
|
|
23
24
|
const x_size =
|
|
24
25
|
Math.abs(voxelStart[0] - voxelEnd[0]) +
|
|
@@ -26,19 +27,51 @@ export const getInteractiveImeasureCropCuboidDimensions = (
|
|
|
26
27
|
const y_size =
|
|
27
28
|
Math.abs(voxelStart[1] - voxelEnd[1]) +
|
|
28
29
|
INTERACTIVE_IMEASURE_BASE.PADDING * 2;
|
|
30
|
+
// if the z-axis distance between the two points is not null, we need to take
|
|
31
|
+
// z-axis length into account as well.
|
|
32
|
+
const zSizeWithoutPadding = Math.abs(voxelStart[2] - voxelEnd[2]);
|
|
33
|
+
|
|
34
|
+
const maxDistanceBetweenPointsAxially = Math.max(x_size, y_size);
|
|
35
|
+
|
|
36
|
+
const zLength = calculateZAxisLength(
|
|
37
|
+
maxDistanceBetweenPointsAxially,
|
|
38
|
+
voxelToWorldAffine
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (zSizeWithoutPadding !== 0) {
|
|
42
|
+
const pixelSpacing = voxelToWorldAffine[0];
|
|
43
|
+
const sliceThickness = voxelToWorldAffine[10];
|
|
44
|
+
// lets start by calculating the proportion between x/y pixel spacing and
|
|
45
|
+
// series slice thickness.
|
|
46
|
+
const proportion = sliceThickness / pixelSpacing;
|
|
47
|
+
// then find the z-axis length based on the largest axis
|
|
48
|
+
|
|
49
|
+
// lets take z-axis length and scale it to x/y axis length
|
|
50
|
+
const z_size = Math.max(
|
|
51
|
+
zSizeWithoutPadding + INTERACTIVE_IMEASURE_BASE.PADDING * 2,
|
|
52
|
+
INTERACTIVE_IMEASURE_BASE.Z
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const zSizeScaledWithoutPadding = Math.round(
|
|
56
|
+
zSizeWithoutPadding * proportion
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const zSizeScaledPadded =
|
|
60
|
+
zSizeScaledWithoutPadding + 2 * INTERACTIVE_IMEASURE_BASE.PADDING;
|
|
61
|
+
return [
|
|
62
|
+
Math.max(x_size, zSizeScaledPadded, INTERACTIVE_IMEASURE_BASE.X),
|
|
63
|
+
Math.max(y_size, zSizeScaledPadded, INTERACTIVE_IMEASURE_BASE.Y),
|
|
64
|
+
Math.max(z_size, zLength),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
29
67
|
|
|
30
68
|
let xyCubeSize = Math.max(x_size, y_size, INTERACTIVE_IMEASURE_BASE.X);
|
|
31
69
|
// the model always expects an even number of voxels on each axis
|
|
32
70
|
if (xyCubeSize % 2 !== 0) {
|
|
33
71
|
xyCubeSize += 1;
|
|
34
72
|
}
|
|
35
|
-
const voxelToWorldAffine = invertAffineMatrix(worldToVoxelAffine);
|
|
36
|
-
const maxDistanceBetweenPoints = Math.max(x_size, y_size);
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
maxDistanceBetweenPoints,
|
|
40
|
-
voxelToWorldAffine
|
|
41
|
-
);
|
|
74
|
+
console.log("zLength", zLength);
|
|
42
75
|
|
|
43
76
|
return [xyCubeSize, xyCubeSize, zLength];
|
|
44
77
|
};
|