Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add worldToImage and imageToWorld utilities #85

Merged
merged 4 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,9 @@ type ImageSpacingCalibratedEventDetail = {
worldToIndex: mat4;
};

// @public (undocumented)
function imageToWorldCoords(imageCoords: Point2, options: Options_2): Point3 | undefined;

// @public (undocumented)
export class ImageVolume implements IImageVolume {
constructor(props: IVolume);
Expand Down Expand Up @@ -1761,7 +1764,9 @@ declare namespace utilities {
transformWorldToIndex,
prefetchStack,
loadImageToCanvas,
renderToCanvas
renderToCanvas,
worldToImageCoords,
imageToWorldCoords
}
}
export { utilities }
Expand Down Expand Up @@ -2018,6 +2023,9 @@ declare namespace windowLevel {
}
}

// @public (undocumented)
function worldToImageCoords(worldCoords: Point3, options: Options): Point2 | undefined;

// (No @packageDocumentation comment for this package)

```
3 changes: 2 additions & 1 deletion packages/core/src/RenderingEngine/StackViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,8 @@ class StackViewport extends Viewport implements IStackViewport {
const xVoxels = image.columns;
const yVoxels = image.rows;

const zSpacing = image.sliceThickness || EPSILON;
const zSpacing = imagePlaneModule.sliceThickness || EPSILON;
sedghi marked this conversation as resolved.
Show resolved Hide resolved

const zVoxels = 1;

const numComps =
Expand Down
64 changes: 64 additions & 0 deletions packages/core/src/utilities/imageToWorldCoords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { vec3 } from 'gl-matrix';
import { metaData } from '..';
import { Point2, Point3 } from '../types';

type Options = {
imageId?: string;
};

/**
* Given the 2d coordinates on the image space with [0,0] being the top left corner
* of the top left pixel, and options which includes the imageId, it returns the
* 3d coordinates on the world space.
* @param imageCoords - The 2d coordinates on the image
* @param options - options which includes the imageId
* @returns The 3d coordinates on the world.
*
*/
export default function imageToWorldCoords(
sedghi marked this conversation as resolved.
Show resolved Hide resolved
imageCoords: Point2,
options: Options
): Point3 | undefined {
const { imageId } = options;
sedghi marked this conversation as resolved.
Show resolved Hide resolved

if (!imageId) {
throw new Error('imageId is required for the imageToWorldCoords function');
}

const imagePlaneModule = metaData.get('imagePlaneModule', imageId);

if (!imagePlaneModule) {
throw new Error(`No imagePlaneModule found for imageId: ${imageId}`);
}

const {
columnCosines,
columnPixelSpacing,
rowCosines,
rowPixelSpacing,
imagePositionPatient: origin,
} = imagePlaneModule;

// calculate the image coordinates in the world space
const imageCoordsInWorld = vec3.create();

// move from origin in the direction of the row cosines with the amount of
// row pixel spacing times the first element of the image coordinates vector
vec3.scaleAndAdd(
sedghi marked this conversation as resolved.
Show resolved Hide resolved
imageCoordsInWorld,
origin,
rowCosines,
// to accommodate the [0,0] being on the top left corner of the first top left pixel
// but the origin is at the center of the first top left pixel
rowPixelSpacing * (imageCoords[0] - 0.5)
);

vec3.scaleAndAdd(
imageCoordsInWorld,
imageCoordsInWorld,
columnCosines,
columnPixelSpacing * (imageCoords[1] - 0.5)
);

return Array.from(imageCoordsInWorld) as Point3;
}
4 changes: 4 additions & 0 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import transformWorldToIndex from './transformWorldToIndex';
import prefetchStack from './prefetchStack';
import loadImageToCanvas from './loadImageToCanvas';
import renderToCanvas from './renderToCanvas';
import worldToImageCoords from './worldToImageCoords';
import imageToWorldCoords from './imageToWorldCoords';

// name spaces
import * as planar from './planar';
Expand Down Expand Up @@ -52,4 +54,6 @@ export {
prefetchStack,
loadImageToCanvas,
renderToCanvas,
worldToImageCoords,
imageToWorldCoords,
};
133 changes: 133 additions & 0 deletions packages/core/src/utilities/worldToImageCoords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { vec3, vec2, mat4, vec4 } from 'gl-matrix';
import { metaData } from '..';
import { Point2, Point3 } from '../types';

type Options = {
imageId?: string;
};

/**
* Given the 3d coordinates on the world space, and options which includes the imageId
* it returns the image coordinates (IJ) on the image space. The image space is
* defined with [0,0] being on the top left corner of the first top left pixel,
sedghi marked this conversation as resolved.
Show resolved Hide resolved
* the [1,1] being on the bottom right corner of the first top left pixel.
* @param worldCoords - The 3d coordinates on the world.
* @param options - options which includes the imageId
* @returns The 2d coordinates on the image.
*
*/
function worldToImageCoords(
worldCoords: Point3,
sedghi marked this conversation as resolved.
Show resolved Hide resolved
options: Options
): Point2 | undefined {
const { imageId } = options;
sedghi marked this conversation as resolved.
Show resolved Hide resolved

if (!imageId) {
throw new Error('imageId is required for the worldToImageCoords function');
}

const imagePlaneModule = metaData.get('imagePlaneModule', imageId);

if (!imagePlaneModule) {
throw new Error(`No imagePlaneModule found for imageId: ${imageId}`);
}

// For the image coordinates we need to calculate the transformation matrix
// from the world coordinates to the image coordinates.

const {
columnCosines,
columnPixelSpacing,
rowCosines,
rowPixelSpacing,
imagePositionPatient: origin,
rows,
columns,
sliceThickness,
} = imagePlaneModule;

// The origin is the image position patient, but since image coordinates start
// from [0,0] for the top left hand of the first pixel, and the origin is at the
// center of the first pixel, we need to account for this.
const newOrigin = vec3.create();

vec3.scaleAndAdd(newOrigin, origin, columnCosines, -columnPixelSpacing / 2);
vec3.scaleAndAdd(newOrigin, newOrigin, rowCosines, -rowPixelSpacing / 2);

// Translation matrix for the world to image coordinates
const translationMatrix = mat4.create();
sedghi marked this conversation as resolved.
Show resolved Hide resolved
mat4.fromTranslation(
translationMatrix,
vec3.fromValues(newOrigin[0], newOrigin[1], newOrigin[2])
);

// The normal is the cross product of the rowCosines and the columnCosines.
const normal = vec3.create();
vec3.cross(normal, rowCosines, columnCosines);

// The rotation matrix for the world to image coordinates
const rotationMatrix = mat4.fromValues(
rowCosines[0],
rowCosines[1],
rowCosines[2],
0,
columnCosines[0],
columnCosines[1],
columnCosines[2],
0,
normal[0],
normal[1],
normal[2],
0,
0,
0,
0,
1
);

// The scale matrix for the world to image coordinates
const scale = mat4.create();
mat4.fromScaling(
scale,
vec3.fromValues(columnPixelSpacing, rowPixelSpacing, sliceThickness)
sedghi marked this conversation as resolved.
Show resolved Hide resolved
);

// The matrix is the concatenation of the translation, rotation and scale
const matrix = mat4.create();
mat4.multiply(matrix, translationMatrix, rotationMatrix);
mat4.multiply(matrix, matrix, scale);

// The inverse matrix
const inverseMatrix = mat4.create();
mat4.invert(inverseMatrix, matrix);

// The world coordinates are transformed by the inverse matrix
const transformedWorldCoords = vec4.create();
vec4.transformMat4(
transformedWorldCoords,
vec4.fromValues(worldCoords[0], worldCoords[1], worldCoords[2], 1),
inverseMatrix
);

// The transformed world coordinates are divided by the last element of the
// transformed world coordinates to get the image coordinates.
const imageCoords = vec2.fromValues(
transformedWorldCoords[0] / transformedWorldCoords[3],
transformedWorldCoords[1] / transformedWorldCoords[3]
);

if (
imageCoords[0] < 0 ||
imageCoords[0] >= columns ||
imageCoords[1] < 0 ||
imageCoords[1] >= rows
) {
throw new Error(
`The image coordinates are outside of the image, imageCoords: ${imageCoords}`
);
}

return Array.from(imageCoords) as Point2;
sedghi marked this conversation as resolved.
Show resolved Hide resolved
}

export default worldToImageCoords;