@bettermedicine/imeasure 0.0.1

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 (125) hide show
  1. package/.github/workflows/test-build-imeasure-package.yml +30 -0
  2. package/Makefile +21 -0
  3. package/README.md +31 -0
  4. package/dist/affines/from-cs-volume.d.ts +2 -0
  5. package/dist/affines/from-cs-volume.js +12 -0
  6. package/dist/affines/from-primitives.d.ts +8 -0
  7. package/dist/affines/from-primitives.js +24 -0
  8. package/dist/affines/from-vtk-image-data.d.ts +2 -0
  9. package/dist/affines/from-vtk-image-data.js +13 -0
  10. package/dist/affines/index.d.ts +5 -0
  11. package/dist/affines/index.js +5 -0
  12. package/dist/affines/invert.d.ts +2 -0
  13. package/dist/affines/invert.js +6 -0
  14. package/dist/affines/serialize.d.ts +2 -0
  15. package/dist/affines/serialize.js +12 -0
  16. package/dist/affines/translate.d.ts +3 -0
  17. package/dist/affines/translate.js +10 -0
  18. package/dist/constants.d.ts +10 -0
  19. package/dist/constants.js +10 -0
  20. package/dist/cropping/index.d.ts +24 -0
  21. package/dist/cropping/index.js +52 -0
  22. package/dist/formatting/index.d.ts +3 -0
  23. package/dist/formatting/index.js +3 -0
  24. package/dist/formatting/interactive.d.ts +7 -0
  25. package/dist/formatting/interactive.js +39 -0
  26. package/dist/formatting/static.d.ts +7 -0
  27. package/dist/formatting/static.js +33 -0
  28. package/dist/formatting/utils.d.ts +4 -0
  29. package/dist/formatting/utils.js +20 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +15 -0
  32. package/dist/math/affines/from-cs-volume.d.ts +8 -0
  33. package/dist/math/affines/from-cs-volume.js +18 -0
  34. package/dist/math/affines/from-primitives.d.ts +12 -0
  35. package/dist/math/affines/from-primitives.js +28 -0
  36. package/dist/math/affines/from-vtk-image-data.d.ts +5 -0
  37. package/dist/math/affines/from-vtk-image-data.js +8 -0
  38. package/dist/math/affines/from-vtk-image-data.test.d.ts +1 -0
  39. package/dist/math/affines/from-vtk-image-data.test.js +15 -0
  40. package/dist/math/affines/index.d.ts +6 -0
  41. package/dist/math/affines/index.js +6 -0
  42. package/dist/math/affines/invert.d.ts +2 -0
  43. package/dist/math/affines/invert.js +6 -0
  44. package/dist/math/affines/serialize.d.ts +2 -0
  45. package/dist/math/affines/serialize.js +12 -0
  46. package/dist/math/affines/translate-and-invert.test.d.ts +1 -0
  47. package/dist/math/affines/translate-and-invert.test.js +40 -0
  48. package/dist/math/affines/translate.d.ts +3 -0
  49. package/dist/math/affines/translate.js +10 -0
  50. package/dist/math/cropping.d.ts +23 -0
  51. package/dist/math/cropping.js +142 -0
  52. package/dist/math/index.d.ts +4 -0
  53. package/dist/math/index.js +4 -0
  54. package/dist/math/utility.d.ts +4 -0
  55. package/dist/math/utility.js +19 -0
  56. package/dist/metadata/world.d.ts +10 -0
  57. package/dist/metadata/world.js +25 -0
  58. package/dist/test/testdata.d.ts +2 -0
  59. package/dist/test/testdata.js +2 -0
  60. package/dist/types/arrays.d.ts +1 -0
  61. package/dist/types/arrays.js +1 -0
  62. package/dist/types/cornerstone.d.ts +3 -0
  63. package/dist/types/cornerstone.js +1 -0
  64. package/dist/types/crop.d.ts +16 -0
  65. package/dist/types/crop.js +1 -0
  66. package/dist/types/index.d.ts +3 -0
  67. package/dist/types/index.js +3 -0
  68. package/dist/types/io.d.ts +52 -0
  69. package/dist/types/io.js +1 -0
  70. package/dist/types/metadata.d.ts +19 -0
  71. package/dist/types/metadata.js +1 -0
  72. package/dist/types/ohif.d.ts +10 -0
  73. package/dist/types/ohif.js +1 -0
  74. package/dist/types/series-metadata.d.ts +19 -0
  75. package/dist/types/series-metadata.js +1 -0
  76. package/dist/volumes/crop.d.ts +24 -0
  77. package/dist/volumes/crop.js +52 -0
  78. package/docs/README.md +26 -0
  79. package/docs/docs/api.md +5 -0
  80. package/docs/docusaurus.config.ts +134 -0
  81. package/docs/package-lock.json +17698 -0
  82. package/docs/package.json +54 -0
  83. package/docs/sidebars.ts +33 -0
  84. package/docs/src/components/HomepageFeatures/index.tsx +71 -0
  85. package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  86. package/docs/src/css/custom.css +30 -0
  87. package/docs/src/pages/index.module.css +23 -0
  88. package/docs/src/pages/index.tsx +44 -0
  89. package/docs/src/pages/markdown-page.md +7 -0
  90. package/docs/static/.nojekyll +0 -0
  91. package/docs/static/img/favicon.ico +0 -0
  92. package/docs/static/img/logo.png +0 -0
  93. package/docs/tsconfig.json +8 -0
  94. package/eslint.config.mjs +11 -0
  95. package/package.json +34 -0
  96. package/src/constants.ts +12 -0
  97. package/src/cropping/index.ts +82 -0
  98. package/src/formatting/index.ts +3 -0
  99. package/src/formatting/interactive.ts +60 -0
  100. package/src/formatting/static.ts +49 -0
  101. package/src/formatting/utils.ts +31 -0
  102. package/src/index.ts +17 -0
  103. package/src/math/affines/from-cs-volume.ts +24 -0
  104. package/src/math/affines/from-primitives.ts +43 -0
  105. package/src/math/affines/from-vtk-image-data.test.ts +17 -0
  106. package/src/math/affines/from-vtk-image-data.ts +13 -0
  107. package/src/math/affines/index.ts +6 -0
  108. package/src/math/affines/invert.ts +7 -0
  109. package/src/math/affines/serialize.ts +14 -0
  110. package/src/math/affines/translate-and-invert.test.ts +78 -0
  111. package/src/math/affines/translate.ts +21 -0
  112. package/src/math/cropping.ts +219 -0
  113. package/src/math/index.ts +4 -0
  114. package/src/math/utility.ts +30 -0
  115. package/src/metadata/world.ts +35 -0
  116. package/src/test/testdata.ts +20 -0
  117. package/src/types/arrays.ts +1 -0
  118. package/src/types/cornerstone.ts +4 -0
  119. package/src/types/crop.ts +16 -0
  120. package/src/types/index.ts +3 -0
  121. package/src/types/io.ts +62 -0
  122. package/src/types/metadata.ts +26 -0
  123. package/src/types/ohif.ts +14 -0
  124. package/tsconfig.json +15 -0
  125. package/vitest.config.ts +7 -0
@@ -0,0 +1,30 @@
1
+ name: Test and Build imeasure package
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+ jobs:
11
+ test-and-build:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+ - name: Set up Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: "18"
22
+ - name: Install dependencies
23
+ run: npm install
24
+ - name: Lint
25
+ run: npm run lint
26
+ - name: Run tests
27
+ run: npm test
28
+ - name: Build package
29
+ run: npm run build
30
+ # TODO: Add steps to publish the package on main branch?
package/Makefile ADDED
@@ -0,0 +1,21 @@
1
+ .PHONY: test build lint dev-docs build-docs build-publish-docs
2
+
3
+ bootstrap:
4
+ npm install
5
+ cd ./docs && npm install
6
+
7
+ test:
8
+ npm test
9
+ build:
10
+ npm run build
11
+ lint:
12
+ npm run lint
13
+
14
+ dev-docs:
15
+ cd ./docs && npm run dev
16
+
17
+ build-docs:
18
+ cd ./docs && npm run build
19
+
20
+ build-publish-docs:
21
+ @make build-docs
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Imeasure SDK
2
+
3
+ This is a collection of TypeScript types and utility functions for working with
4
+ Better Medicine's iMeasure API.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @bettermedicine/imeasure
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ### API request & response types
15
+
16
+ TODO: a brief summary of how to import the I/O types
17
+
18
+ ### Cropping
19
+
20
+ TODO: A brief explanation of how cropping works and which utilities we provide for it.
21
+
22
+ Note: mention how cornerstone3 volumes mostly live in the GPU buffer and how these need to be
23
+ converted to CPU buffers for cropping operations.
24
+
25
+ #### Bounding box calculations
26
+
27
+ TODO: A brief explanation on how these work and why they are necessary
28
+
29
+ ### World metadata
30
+
31
+ TODO: A brief explanation of the world metadata we require for the request and how to derive it
@@ -0,0 +1,2 @@
1
+ import { ImageVolume } from "../types/cornerstone";
2
+ export declare const getAffineFromCornerstoneVolume: (volume: ImageVolume) => import("gl-matrix").mat4;
@@ -0,0 +1,12 @@
1
+ import { getAffineMatrixFromPrimitives, } from "./from-primitives";
2
+ export const getAffineFromCornerstoneVolume = (volume) => {
3
+ const spacing = volume.spacing;
4
+ const origin = volume.origin;
5
+ const direction = volume.direction;
6
+ const bounds = volume.voxelManager?.getDefaultBounds();
7
+ const extent = bounds.flatMap((b) => [
8
+ b[0],
9
+ b[1],
10
+ ]);
11
+ return getAffineMatrixFromPrimitives({ spacing, origin, direction, extent });
12
+ };
@@ -0,0 +1,8 @@
1
+ import { mat3, mat4, vec3 } from "gl-matrix";
2
+ export type TAffinePrimitivesInput = {
3
+ spacing: vec3;
4
+ origin: vec3;
5
+ direction: mat3;
6
+ extent: [number, number, number, number, number, number];
7
+ };
8
+ export declare const getAffineMatrixFromPrimitives: (input: TAffinePrimitivesInput) => mat4;
@@ -0,0 +1,24 @@
1
+ import { mat4 } from "gl-matrix";
2
+ export const getAffineMatrixFromPrimitives = (input) => {
3
+ const { spacing, origin, direction, extent } = input;
4
+ const adjustedOrigin = [
5
+ origin[0] + extent[0] * spacing[0],
6
+ origin[1] + extent[2] * spacing[1],
7
+ origin[2] + extent[4] * spacing[2],
8
+ ];
9
+ const mat = mat4.create();
10
+ mat[0] = spacing[0] * direction[0]; // X-axis
11
+ mat[1] = spacing[0] * direction[3];
12
+ mat[2] = spacing[0] * direction[6];
13
+ mat[4] = spacing[1] * direction[1]; // Y-axis
14
+ mat[5] = spacing[1] * direction[4];
15
+ mat[6] = spacing[1] * direction[7];
16
+ mat[8] = spacing[2] * direction[2]; // Z-axis
17
+ mat[9] = spacing[2] * direction[5];
18
+ mat[10] = spacing[2] * direction[8];
19
+ // Set the translation (origin)
20
+ mat[12] = adjustedOrigin[0];
21
+ mat[13] = adjustedOrigin[1];
22
+ mat[14] = adjustedOrigin[2];
23
+ return mat;
24
+ };
@@ -0,0 +1,2 @@
1
+ import vtkImageData from "@kitware/vtk.js/Common/DataModel/ImageData";
2
+ export declare const getAffineMatrix: (imageData: vtkImageData) => import("gl-matrix").mat4;
@@ -0,0 +1,13 @@
1
+ import { getAffineMatrixFromPrimitives } from "./from-primitives";
2
+ // Given a vtkImageData object, this function returns the affine matrix that allows us to convert
3
+ // voxel-space coordinates to world-space coordinates & vice versa.
4
+ // Respects origin, extent, spacing, and direction of the vtkImageData object.
5
+ export const getAffineMatrix = (imageData) => {
6
+ const spacing = imageData.getSpacing(); // [sx, sy, sz]
7
+ const origin = imageData.getOrigin(); // [ox, oy, oz]
8
+ const direction = imageData.getDirection(); // 3x3 direction matrix
9
+ const extent = imageData.getExtent(); // [xmin, xmax, ymin, ymax, zmin, zmax]
10
+ // the affine matrix should work in "isolation" so that when applied to voxel-space point
11
+ // coordinates, we get the world-space coordinates.
12
+ return getAffineMatrixFromPrimitives({ spacing, origin, direction, extent });
13
+ };
@@ -0,0 +1,5 @@
1
+ export * from "./from-primitives";
2
+ export * from "./from-cs-volume";
3
+ export * from "./from-vtk-image-data";
4
+ export * from "./invert";
5
+ export * from "./translate";
@@ -0,0 +1,5 @@
1
+ export * from "./from-primitives";
2
+ export * from "./from-cs-volume";
3
+ export * from "./from-vtk-image-data";
4
+ export * from "./invert";
5
+ export * from "./translate";
@@ -0,0 +1,2 @@
1
+ import { mat4 } from "gl-matrix";
2
+ export declare const invertAffineMatrix: (matrix: mat4) => mat4;
@@ -0,0 +1,6 @@
1
+ import { mat4 } from "gl-matrix";
2
+ export const invertAffineMatrix = (matrix) => {
3
+ const inv = mat4.create();
4
+ mat4.invert(inv, matrix);
5
+ return inv;
6
+ };
@@ -0,0 +1,2 @@
1
+ import { mat4, vec4 } from "gl-matrix";
2
+ export declare const serializeAffineMatrix: (mat: mat4) => [vec4, vec4, vec4, vec4];
@@ -0,0 +1,12 @@
1
+ import { vec4 } from "gl-matrix";
2
+ // A serialized plain mat4 looks like a float32array of 16 elements.
3
+ // For it to be readable (esp. in a format that Numpy likes), we chunk it
4
+ // into 4 vec4s.
5
+ export const serializeAffineMatrix = (mat) => {
6
+ return [
7
+ vec4.fromValues(mat[0], mat[4], mat[8], mat[12]),
8
+ vec4.fromValues(mat[1], mat[5], mat[9], mat[13]),
9
+ vec4.fromValues(mat[2], mat[6], mat[10], mat[14]),
10
+ vec4.fromValues(mat[3], mat[7], mat[11], mat[15]),
11
+ ];
12
+ };
@@ -0,0 +1,3 @@
1
+ import { mat4, vec3 } from "gl-matrix";
2
+ import { Point3D } from "../types/cornerstone";
3
+ export declare const affineTranslateVec3: (voxelCoord: vec3 | Point3D, affine: mat4) => vec3;
@@ -0,0 +1,10 @@
1
+ import { vec4 } from "gl-matrix";
2
+ // We use affines to convert between voxel-space and world-space coordinates.
3
+ // Takes a coordinate and an affine matrix, returns the translated coordinate.
4
+ // This is useful for converting between voxel-space and world-space coordinates.
5
+ export const affineTranslateVec3 = (voxelCoord, affine) => {
6
+ const homogeneousVoxel = vec4.fromValues(voxelCoord[0], voxelCoord[1], voxelCoord[2], 1);
7
+ const worldCoord = vec4.create(); // Output container for the translated coordinate
8
+ vec4.transformMat4(worldCoord, homogeneousVoxel, affine);
9
+ return [worldCoord[0], worldCoord[1], worldCoord[2]];
10
+ };
@@ -0,0 +1,10 @@
1
+ export declare const INTERACTIVE_IMEASURE_BASE_X_VOXELS = 128;
2
+ export declare const INTERACTIVE_IMEASURE_BASE_Y_VOXELS = 128;
3
+ export declare const INTERACTIVE_IMEASURE_BASE_Z_VOXELS = 32;
4
+ export declare const INTERACTIVE_IMEASURE_BASE_PADDING_VOXELS = 8;
5
+ export declare const INTERACTIVE_IMEASURE_BASE: {
6
+ X: number;
7
+ Y: number;
8
+ Z: number;
9
+ PADDING: number;
10
+ };
@@ -0,0 +1,10 @@
1
+ export const INTERACTIVE_IMEASURE_BASE_X_VOXELS = 128;
2
+ export const INTERACTIVE_IMEASURE_BASE_Y_VOXELS = 128;
3
+ export const INTERACTIVE_IMEASURE_BASE_Z_VOXELS = 32;
4
+ export const INTERACTIVE_IMEASURE_BASE_PADDING_VOXELS = 8;
5
+ export const INTERACTIVE_IMEASURE_BASE = {
6
+ X: INTERACTIVE_IMEASURE_BASE_X_VOXELS,
7
+ Y: INTERACTIVE_IMEASURE_BASE_Y_VOXELS,
8
+ Z: INTERACTIVE_IMEASURE_BASE_Z_VOXELS,
9
+ PADDING: INTERACTIVE_IMEASURE_BASE_PADDING_VOXELS,
10
+ };
@@ -0,0 +1,24 @@
1
+ import vtkImageData from "@kitware/vtk.js/Common/DataModel/ImageData";
2
+ import { TCropMetadata } from "../types/metadata";
3
+ import { TCroppingPlanes, TIMeasureCropRegion } from "../types/crop";
4
+ export type TInteractiveImeasureCropResult = {
5
+ voxelValues: Uint8Array | Int16Array | Float32Array;
6
+ volume: vtkImageData;
7
+ cropMeta: TCropMetadata;
8
+ };
9
+ /**
10
+ * Crops a VTK volume using specified crop boundaries and returns the cropped volume data along with metadata.
11
+ *
12
+ * This function takes a `vtkImageData` volume and a crop region definition, applies cropping planes,
13
+ * and computes the effective cropped boundaries in both voxel and world coordinates. The result includes
14
+ * the cropped volume, its scalar voxel values, and metadata about the crop operation.
15
+ */
16
+ export declare const cropFromVTKVolumeByCropBoundaries: (vtkVolume: vtkImageData, cropRegion: TIMeasureCropRegion) => TInteractiveImeasureCropResult;
17
+ /**
18
+ * Crops a VTK volume using specified cropping planes.
19
+ *
20
+ * This function applies cropping planes to a `vtkImageData` volume and returns the cropped volume.
21
+ * The cropping planes define the boundaries of the crop operation in voxel coordinates, ordered as:
22
+ * [minX, maxX, minY, maxY, minZ, maxZ].
23
+ */
24
+ export declare const cropVolumeByCroppingPlanes: (vtkVolume: vtkImageData, croppingPlanes: TCroppingPlanes) => vtkImageData;
@@ -0,0 +1,52 @@
1
+ import cropFilter from "@kitware/vtk.js/Filters/General/ImageCropFilter";
2
+ import { affineTranslateVec3, getAffineMatrixFromVtkImageData, } from "../math/affines";
3
+ import { vec3ToPoint3D } from "../formatting";
4
+ /**
5
+ * Crops a VTK volume using specified crop boundaries and returns the cropped volume data along with metadata.
6
+ *
7
+ * This function takes a `vtkImageData` volume and a crop region definition, applies cropping planes,
8
+ * and computes the effective cropped boundaries in both voxel and world coordinates. The result includes
9
+ * the cropped volume, its scalar voxel values, and metadata about the crop operation.
10
+ */
11
+ export const cropFromVTKVolumeByCropBoundaries = (vtkVolume, cropRegion) => {
12
+ const voxelToWorldAffine = getAffineMatrixFromVtkImageData(vtkVolume);
13
+ const croppedVolume = cropVolumeByCroppingPlanes(vtkVolume, cropRegion.croppingPlanes);
14
+ // Our resulting crop may differ from the crop cube we passed in in case the
15
+ // crop cube exceeded the bounds of the original volume. To get its exact, effective
16
+ // coordinates, we get its extent...
17
+ const [minX, maxX, minY, maxY, minZ, maxZ] = croppedVolume.getExtent();
18
+ const actualCroppedBounds = [
19
+ [minX, minY, minZ],
20
+ [maxX, maxY, maxZ],
21
+ ];
22
+ // ...and translate it to world coordinates
23
+ const cropBoundariesWorld = actualCroppedBounds.map((pt) => vec3ToPoint3D(affineTranslateVec3(pt, voxelToWorldAffine)));
24
+ return {
25
+ voxelValues: croppedVolume
26
+ .getPointData()
27
+ .getScalars()
28
+ .getData(),
29
+ volume: croppedVolume,
30
+ cropMeta: {
31
+ cropCuboidCenterVoxel: cropRegion.voxelSpaceCenter,
32
+ cropBoundariesUnclippedVoxel: cropRegion.cropCubePoints,
33
+ cropBoundariesVoxel: actualCroppedBounds,
34
+ cropBoundariesWorld: cropBoundariesWorld,
35
+ shape: croppedVolume.getDimensions(),
36
+ },
37
+ };
38
+ };
39
+ /**
40
+ * Crops a VTK volume using specified cropping planes.
41
+ *
42
+ * This function applies cropping planes to a `vtkImageData` volume and returns the cropped volume.
43
+ * The cropping planes define the boundaries of the crop operation in voxel coordinates, ordered as:
44
+ * [minX, maxX, minY, maxY, minZ, maxZ].
45
+ */
46
+ export const cropVolumeByCroppingPlanes = (vtkVolume, croppingPlanes) => {
47
+ const cropper = cropFilter.newInstance();
48
+ cropper.setInputData(vtkVolume);
49
+ cropper.setCroppingPlanes(croppingPlanes);
50
+ cropper.update();
51
+ return cropper.getOutputData();
52
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./static";
2
+ export * from "./interactive";
3
+ export * from "./utils";
@@ -0,0 +1,3 @@
1
+ export * from "./static";
2
+ export * from "./interactive";
3
+ export * from "./utils";
@@ -0,0 +1,7 @@
1
+ import { TInteractiveIMeasureModeClicks } from "../types/io";
2
+ import { TWorldMetadata, TCropMetadata } from "../types/metadata";
3
+ export type TInteractiveImeasureInput = {
4
+ id: string;
5
+ clicks: TInteractiveIMeasureModeClicks[];
6
+ } & TWorldMetadata & TCropMetadata;
7
+ export declare const formatInteractivePayload: (input: TInteractiveImeasureInput, image: Int16Array, mask?: Uint8Array) => import("undici-types").FormData;
@@ -0,0 +1,39 @@
1
+ import { vec3ToPoint3D } from "./utils";
2
+ export const formatInteractivePayload = (input, image, mask) => {
3
+ const fd = new FormData();
4
+ const metadata = {
5
+ id: input.id,
6
+ clicks: input.clicks,
7
+ world_origin: vec3ToPoint3D(input.worldOrigin),
8
+ world_shape: vec3ToPoint3D(input.worldShape),
9
+ world_spacing: vec3ToPoint3D(input.worldSpacing),
10
+ rescale_intercept: input.rescaleIntercept,
11
+ rescale_slope: input.rescaleSlope,
12
+ image_orientation_patient: input.imageOrientationPatient,
13
+ crop_boundaries_world: input.cropBoundariesWorld,
14
+ crop_boundaries_voxel: input.cropBoundariesVoxel,
15
+ crop_boundaries_unclipped_voxel: input.cropBoundariesUnclippedVoxel,
16
+ crop_cuboid_center_voxel: input.cropCuboidCenterVoxel,
17
+ shape: input.shape,
18
+ world_bounding_image_positions: [
19
+ vec3ToPoint3D(input.worldBoundingImagePositions[0]),
20
+ vec3ToPoint3D(input.worldBoundingImagePositions[1]),
21
+ ],
22
+ image_count: input.imageCount,
23
+ };
24
+ const metadataBlob = new Blob([JSON.stringify(metadata)], {
25
+ type: "application/json",
26
+ });
27
+ const imageBlob = new Blob([image.buffer], {
28
+ type: "application/octet-stream",
29
+ });
30
+ fd.append("metadata", metadataBlob, "metadata.json");
31
+ fd.append("image", imageBlob, "image.bin");
32
+ if (mask) {
33
+ const maskBlob = new Blob([mask.buffer], {
34
+ type: "application/octet-stream",
35
+ });
36
+ fd.append("mask", maskBlob, "mask.bin");
37
+ }
38
+ return fd;
39
+ };
@@ -0,0 +1,7 @@
1
+ import { Point3D } from "../types/cornerstone";
2
+ import { TWorldMetadata, TCropMetadata } from "../types/metadata";
3
+ export type TStaticImeasureInput = {
4
+ id: string;
5
+ clicks: [Point3D, Point3D];
6
+ } & TWorldMetadata & TCropMetadata;
7
+ export declare const formatStaticPayload: (input: TStaticImeasureInput, image: Int16Array) => import("undici-types").FormData;
@@ -0,0 +1,33 @@
1
+ import { vec3ToPoint3D } from "./utils";
2
+ export const formatStaticPayload = (input, image) => {
3
+ const fd = new FormData();
4
+ const metadata = {
5
+ id: input.id,
6
+ clicks: input.clicks,
7
+ world_origin: vec3ToPoint3D(input.worldOrigin),
8
+ world_shape: vec3ToPoint3D(input.worldShape),
9
+ world_spacing: vec3ToPoint3D(input.worldSpacing),
10
+ rescale_intercept: input.rescaleIntercept,
11
+ rescale_slope: input.rescaleSlope,
12
+ image_orientation_patient: input.imageOrientationPatient,
13
+ crop_boundaries_world: input.cropBoundariesWorld,
14
+ crop_boundaries_voxel: input.cropBoundariesVoxel,
15
+ crop_boundaries_unclipped_voxel: input.cropBoundariesUnclippedVoxel,
16
+ crop_cuboid_center_voxel: input.cropCuboidCenterVoxel,
17
+ shape: input.shape,
18
+ world_bounding_image_positions: [
19
+ vec3ToPoint3D(input.worldBoundingImagePositions[0]),
20
+ vec3ToPoint3D(input.worldBoundingImagePositions[1]),
21
+ ],
22
+ image_count: input.imageCount,
23
+ };
24
+ const metadataBlob = new Blob([JSON.stringify(metadata)], {
25
+ type: "application/json",
26
+ });
27
+ const imageBlob = new Blob([image.buffer], {
28
+ type: "application/octet-stream",
29
+ });
30
+ fd.append("metadata", metadataBlob, "metadata.json");
31
+ fd.append("image", imageBlob, "image.bin");
32
+ return fd;
33
+ };
@@ -0,0 +1,4 @@
1
+ import { vec3 } from "gl-matrix";
2
+ import { Point3D } from "../types/cornerstone";
3
+ export declare const float32arrayToInt16: (float32Array: Float32Array) => Int16Array;
4
+ export declare const vec3ToPoint3D: (vec: vec3) => Point3D;
@@ -0,0 +1,20 @@
1
+ // Since DICOM CT HU values always fall well within the int16 range
2
+ // of (-32768, 32767), we can safely convert the output array to int16-s.
3
+ // This reduces the size of our payload by half compared to float32.
4
+ export const float32arrayToInt16 = (float32Array) => {
5
+ // if we're already dealing with an Int16Array, just return it
6
+ if (float32Array instanceof Int16Array) {
7
+ return float32Array;
8
+ }
9
+ const int16Buffer = new ArrayBuffer(float32Array.length * Int16Array.BYTES_PER_ELEMENT);
10
+ const int16Array = new Int16Array(int16Buffer);
11
+ const float32View = new DataView(float32Array.buffer);
12
+ for (let i = 0; i < float32Array.length; i++) {
13
+ const floatValue = float32View.getFloat32(i * Float32Array.BYTES_PER_ELEMENT, true);
14
+ int16Array[i] = floatValue;
15
+ }
16
+ return int16Array;
17
+ };
18
+ export const vec3ToPoint3D = (vec) => {
19
+ return [vec[0], vec[1], vec[2]];
20
+ };
@@ -0,0 +1,6 @@
1
+ export type * as Types from "./types";
2
+ export * as constants from "./constants";
3
+ export * as formatting from "./formatting";
4
+ export * as math from "./math";
5
+ export * as metadata from "./metadata/world";
6
+ export * as cropping from "./cropping";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export * as constants from "./constants";
2
+ export * as formatting from "./formatting";
3
+ export * as math from "./math";
4
+ export * as metadata from "./metadata/world";
5
+ export * as cropping from "./cropping";
6
+ // TODO:
7
+ // expose utils, volumes, metadata, math modules
8
+ // write/port tests, especially for math module
9
+ // document math flow - how do we go from a measurement to a crop region
10
+ // write docs with docusaurus (and typedoc?)
11
+ // include API docs
12
+ // add readme
13
+ // add publish script
14
+ // make this repo public
15
+ // set up hosting for docs
@@ -0,0 +1,8 @@
1
+ import { ImageVolume } from "../../types/cornerstone";
2
+ /**
3
+ * Computes a voxel-to-world affine transformation matrix for a given Cornerstone ImageVolume.
4
+ *
5
+ * This function extracts the spacing, origin, direction, and voxel bounds from the provided
6
+ * volume and constructs the affine matrix using these primitives.
7
+ */
8
+ export declare const getAffineFromCornerstoneVolume: (volume: ImageVolume) => import("gl-matrix").mat4;
@@ -0,0 +1,18 @@
1
+ import { getAffineMatrixFromPrimitives, } from "./from-primitives";
2
+ /**
3
+ * Computes a voxel-to-world affine transformation matrix for a given Cornerstone ImageVolume.
4
+ *
5
+ * This function extracts the spacing, origin, direction, and voxel bounds from the provided
6
+ * volume and constructs the affine matrix using these primitives.
7
+ */
8
+ export const getAffineFromCornerstoneVolume = (volume) => {
9
+ const spacing = volume.spacing;
10
+ const origin = volume.origin;
11
+ const direction = volume.direction;
12
+ const bounds = volume.voxelManager?.getDefaultBounds();
13
+ const extent = bounds.flatMap((b) => [
14
+ b[0],
15
+ b[1],
16
+ ]);
17
+ return getAffineMatrixFromPrimitives({ spacing, origin, direction, extent });
18
+ };
@@ -0,0 +1,12 @@
1
+ import { mat3, mat4, vec3 } from "gl-matrix";
2
+ export type TAffinePrimitivesInput = {
3
+ spacing: vec3;
4
+ origin: vec3;
5
+ direction: mat3;
6
+ extent: [number, number, number, number, number, number];
7
+ };
8
+ /**
9
+ * Computes a voxel-to-world affine transformation matrix based on
10
+ * the provided set of world metadata.
11
+ */
12
+ export declare const getAffineMatrixFromPrimitives: (input: TAffinePrimitivesInput) => mat4;
@@ -0,0 +1,28 @@
1
+ import { mat4 } from "gl-matrix";
2
+ /**
3
+ * Computes a voxel-to-world affine transformation matrix based on
4
+ * the provided set of world metadata.
5
+ */
6
+ export const getAffineMatrixFromPrimitives = (input) => {
7
+ const { spacing, origin, direction, extent } = input;
8
+ const adjustedOrigin = [
9
+ origin[0] + extent[0] * spacing[0],
10
+ origin[1] + extent[2] * spacing[1],
11
+ origin[2] + extent[4] * spacing[2],
12
+ ];
13
+ const mat = mat4.create();
14
+ mat[0] = spacing[0] * direction[0]; // X-axis
15
+ mat[1] = spacing[0] * direction[3];
16
+ mat[2] = spacing[0] * direction[6];
17
+ mat[4] = spacing[1] * direction[1]; // Y-axis
18
+ mat[5] = spacing[1] * direction[4];
19
+ mat[6] = spacing[1] * direction[7];
20
+ mat[8] = spacing[2] * direction[2]; // Z-axis
21
+ mat[9] = spacing[2] * direction[5];
22
+ mat[10] = spacing[2] * direction[8];
23
+ // Set the translation (origin)
24
+ mat[12] = adjustedOrigin[0];
25
+ mat[13] = adjustedOrigin[1];
26
+ mat[14] = adjustedOrigin[2];
27
+ return mat;
28
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Computes a voxel-to-world affine transformation matrix from a vtkImageData object.
3
+ */
4
+ import vtkImageData from "@kitware/vtk.js/Common/DataModel/ImageData";
5
+ export declare const getAffineMatrixFromVtkImageData: (imageData: vtkImageData) => import("gl-matrix").mat4;
@@ -0,0 +1,8 @@
1
+ import { getAffineMatrixFromPrimitives } from "./from-primitives";
2
+ export const getAffineMatrixFromVtkImageData = (imageData) => {
3
+ const spacing = imageData.getSpacing(); // [sx, sy, sz]
4
+ const origin = imageData.getOrigin(); // [ox, oy, oz]
5
+ const direction = imageData.getDirection(); // 3x3 direction matrix
6
+ const extent = imageData.getExtent(); // [xmin, xmax, ymin, ymax, zmin, zmax]
7
+ return getAffineMatrixFromPrimitives({ spacing, origin, direction, extent });
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import vtkImageData from "@kitware/vtk.js/Common/DataModel/ImageData";
2
+ import { it, describe, expect } from "vitest";
3
+ import { getAffineMatrixFromVtkImageData } from "./from-vtk-image-data";
4
+ import { sampleFFSMatrix } from "../../test/testdata";
5
+ describe("getAffineMatrixFromVtkImageData", () => {
6
+ it("returns the expected affine matrix for a sample vtkImageData object", () => {
7
+ const imageData = vtkImageData.newInstance({
8
+ spacing: [0.880859375, 0.880859375, 1],
9
+ origin: [-224.0595703125, -397.5695703125, -511.4],
10
+ extent: [0, 512, 0, 512, 0, 524],
11
+ });
12
+ const matrix = getAffineMatrixFromVtkImageData(imageData);
13
+ expect(matrix).toEqual(sampleFFSMatrix);
14
+ });
15
+ });
@@ -0,0 +1,6 @@
1
+ export * from "./from-primitives";
2
+ export * from "./from-cs-volume";
3
+ export * from "./from-vtk-image-data";
4
+ export * from "./invert";
5
+ export * from "./translate";
6
+ export * from "./serialize";
@@ -0,0 +1,6 @@
1
+ export * from "./from-primitives";
2
+ export * from "./from-cs-volume";
3
+ export * from "./from-vtk-image-data";
4
+ export * from "./invert";
5
+ export * from "./translate";
6
+ export * from "./serialize";
@@ -0,0 +1,2 @@
1
+ import { mat4 } from "gl-matrix";
2
+ export declare const invertAffineMatrix: (matrix: mat4) => mat4;
@@ -0,0 +1,6 @@
1
+ import { mat4 } from "gl-matrix";
2
+ export const invertAffineMatrix = (matrix) => {
3
+ const inv = mat4.create();
4
+ mat4.invert(inv, matrix);
5
+ return inv;
6
+ };
@@ -0,0 +1,2 @@
1
+ import { mat4, vec4 } from "gl-matrix";
2
+ export declare const serializeAffineMatrix: (mat: mat4) => [vec4, vec4, vec4, vec4];
@@ -0,0 +1,12 @@
1
+ import { vec4 } from "gl-matrix";
2
+ // A serialized plain mat4 looks like a float32array of 16 elements.
3
+ // For it to be readable (esp. in a format that Numpy likes), we chunk it
4
+ // into 4 vec4s.
5
+ export const serializeAffineMatrix = (mat) => {
6
+ return [
7
+ vec4.fromValues(mat[0], mat[4], mat[8], mat[12]),
8
+ vec4.fromValues(mat[1], mat[5], mat[9], mat[13]),
9
+ vec4.fromValues(mat[2], mat[6], mat[10], mat[14]),
10
+ vec4.fromValues(mat[3], mat[7], mat[11], mat[15]),
11
+ ];
12
+ };
@@ -0,0 +1 @@
1
+ export {};