Skip to content

Commit 2d22068

Browse files
fix(polyseg): Add rotation points feature if series is rotated (#1788)
1 parent 6649bfe commit 2d22068

File tree

4 files changed

+209
-3
lines changed

4 files changed

+209
-3
lines changed

common/reviews/api/tools.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1311,7 +1311,7 @@ export class CircleScissorsTool extends LabelmapBaseTool {
13111311
}
13121312

13131313
// @public (undocumented)
1314-
function clip_2(a: any, b: any, box: any, da?: any, db?: any): 1 | 0;
1314+
function clip_2(a: any, b: any, box: any, da?: any, db?: any): 0 | 1;
13151315

13161316
// @public (undocumented)
13171317
type ClosestControlPoint = ClosestPoint & {

packages/tools/examples/PolySegWasmVolumeLabelmapToSurface/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ async function run() {
218218
// Get Cornerstone imageIds for the source data and fetch metadata into RAM
219219
const imageIds = await createImageIdsAndCacheMetaData({
220220
StudyInstanceUID:
221-
'1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463',
221+
'1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046',
222222
SeriesInstanceUID:
223-
'1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561',
223+
'1.3.12.2.1107.5.2.32.35162.1999123112191238897317963.0.0.0',
224224
wadoRsRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
225225
});
226226

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* Interface representing information about a rotation matrix
3+
* @interface
4+
*/
5+
interface RotationMatrixInformation {
6+
/** Whether the matrix represents a standard basis (identity matrix) */
7+
isStandard: boolean;
8+
/** The rotation matrix as a flat array of 9 numbers [m11, m12, m13, m21, m22, m23, m31, m32, m33] */
9+
rotationMatrix: number[];
10+
}
11+
12+
/**
13+
* Helper function to validate a 3x3 matrix input
14+
* @param matrix - The input matrix as a flat array
15+
* @throws {Error} If matrix is not a valid 3x3 matrix (array of 9 numbers)
16+
*/
17+
function validate3x3Matrix(matrix: number[]): void {
18+
if (!Array.isArray(matrix) || matrix.length !== 9) {
19+
throw new Error('Matrix must be an array of 9 numbers');
20+
}
21+
if (!matrix.every((n) => typeof n === 'number' && !isNaN(n))) {
22+
throw new Error('Matrix must contain only valid numbers');
23+
}
24+
}
25+
26+
/**
27+
* Calculates the inverse of a 3x3 matrix
28+
* @param matrix - The input matrix as a flat array of 9 numbers [m11, m12, m13, m21, m22, m23, m31, m32, m33]
29+
* @returns The inverse matrix as a flat array of 9 numbers
30+
* @throws {Error} If matrix is not invertible or invalid
31+
*/
32+
export function inverse3x3Matrix(matrix: number[]): number[] {
33+
validate3x3Matrix(matrix);
34+
35+
// First, convert the flat array into a 2D matrix for easier handling
36+
const mat = [
37+
[matrix[0], matrix[1], matrix[2]],
38+
[matrix[3], matrix[4], matrix[5]],
39+
[matrix[6], matrix[7], matrix[8]],
40+
];
41+
42+
// Calculate the determinant
43+
const determinant =
44+
mat[0][0] * (mat[1][1] * mat[2][2] - mat[1][2] * mat[2][1]) -
45+
mat[0][1] * (mat[1][0] * mat[2][2] - mat[1][2] * mat[2][0]) +
46+
mat[0][2] * (mat[1][0] * mat[2][1] - mat[1][1] * mat[2][0]);
47+
48+
// Check if matrix is invertible
49+
if (Math.abs(determinant) < 1e-10) {
50+
throw new Error('Matrix is not invertible (determinant is zero)');
51+
}
52+
53+
// Calculate the adjugate matrix
54+
const adjugate = [
55+
// First row
56+
[
57+
mat[1][1] * mat[2][2] - mat[1][2] * mat[2][1],
58+
-(mat[0][1] * mat[2][2] - mat[0][2] * mat[2][1]),
59+
mat[0][1] * mat[1][2] - mat[0][2] * mat[1][1],
60+
],
61+
// Second row
62+
[
63+
-(mat[1][0] * mat[2][2] - mat[1][2] * mat[2][0]),
64+
mat[0][0] * mat[2][2] - mat[0][2] * mat[2][0],
65+
-(mat[0][0] * mat[1][2] - mat[0][2] * mat[1][0]),
66+
],
67+
// Third row
68+
[
69+
mat[1][0] * mat[2][1] - mat[1][1] * mat[2][0],
70+
-(mat[0][0] * mat[2][1] - mat[0][1] * mat[2][0]),
71+
mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0],
72+
],
73+
];
74+
75+
// Calculate inverse by dividing adjugate by determinant
76+
const inverse = [];
77+
for (let i = 0; i < 3; i++) {
78+
for (let j = 0; j < 3; j++) {
79+
inverse.push(adjugate[i][j] / determinant);
80+
}
81+
}
82+
83+
return inverse;
84+
}
85+
86+
/**
87+
* Normalizes a 3D vector
88+
* @param v - Array of 3 numbers representing a vector
89+
* @returns Normalized vector
90+
*/
91+
function normalizeVector(v: number[]): number[] {
92+
const magnitude = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
93+
return v.map((component) => component / magnitude);
94+
}
95+
96+
/**
97+
* Checks if a set of direction vectors forms a standard basis
98+
* @param directions - Array of 9 numbers representing three 3D vectors [x1,x2,x3,y1,y2,y3,z1,z2,z3]
99+
* @returns Object containing whether the basis is standard and the corresponding rotation matrix
100+
* @throws {Error} If directions array is invalid
101+
*/
102+
export function checkStandardBasis(
103+
directions: number[]
104+
): RotationMatrixInformation {
105+
validate3x3Matrix(directions);
106+
107+
// Extract and normalize vectors
108+
const xVector = directions.slice(0, 3);
109+
const yVector = directions.slice(3, 6);
110+
const zVector = directions.slice(6, 9);
111+
112+
const normalizedX = normalizeVector(xVector);
113+
const normalizedY = normalizeVector(yVector);
114+
const normalizedZ = normalizeVector(zVector);
115+
116+
// Standard basis vectors for comparison
117+
const standardBasis = {
118+
x: [1, 0, 0],
119+
y: [0, 1, 0],
120+
z: [0, 0, 1],
121+
};
122+
123+
// Check if vectors match standard basis (allowing for small numerical errors)
124+
const epsilon = 1e-10;
125+
const isStandard =
126+
normalizedX.every(
127+
(val, i) => Math.abs(val - standardBasis.x[i]) < epsilon
128+
) &&
129+
normalizedY.every(
130+
(val, i) => Math.abs(val - standardBasis.y[i]) < epsilon
131+
) &&
132+
normalizedZ.every((val, i) => Math.abs(val - standardBasis.z[i]) < epsilon);
133+
134+
const rotationMatrix = isStandard
135+
? [...standardBasis.x, ...standardBasis.y, ...standardBasis.z]
136+
: inverse3x3Matrix([...normalizedX, ...normalizedY, ...normalizedZ]);
137+
138+
return {
139+
isStandard,
140+
rotationMatrix,
141+
};
142+
}
143+
144+
/**
145+
* Rotates a single point around a given origin using a rotation matrix
146+
* @param point - Array of 3 numbers representing a point [x,y,z]
147+
* @param origin - Array of 3 numbers representing the rotation origin [x,y,z]
148+
* @param rotationMatrix - Array of 9 numbers representing the rotation matrix
149+
* @returns Rotated point as an array of 3 numbers
150+
*/
151+
function rotatePoint(
152+
point: number[],
153+
origin: number[],
154+
rotationMatrix: number[]
155+
): number[] {
156+
const x = point[0] - origin[0];
157+
const y = point[1] - origin[1];
158+
const z = point[2] - origin[2];
159+
return [
160+
rotationMatrix[0] * x +
161+
rotationMatrix[1] * y +
162+
rotationMatrix[2] * z +
163+
origin[0],
164+
rotationMatrix[3] * x +
165+
rotationMatrix[4] * y +
166+
rotationMatrix[5] * z +
167+
origin[1],
168+
rotationMatrix[6] * x +
169+
rotationMatrix[7] * y +
170+
rotationMatrix[8] * z +
171+
origin[2],
172+
];
173+
}
174+
175+
/**
176+
* Rotates an array of points around a given origin using a rotation matrix
177+
* @param rotationMatrix - Array of 9 numbers representing the rotation matrix
178+
* @param origin - Array of 3 numbers representing the rotation origin [x,y,z]
179+
* @param points - Array of points in format [x1,y1,z1,x2,y2,z2,...]
180+
* @returns Array of rotated points in the same format as input
181+
* @throws {Error} If any input array is invalid
182+
*/
183+
export function rotatePoints(
184+
rotationMatrix: number[],
185+
origin: number[],
186+
points: number[]
187+
): number[] {
188+
const rotatedPoints: number[] = [];
189+
for (let i = 0; i < points.length; i += 3) {
190+
const point = points.slice(i, i + 3);
191+
const rotated = rotatePoint(point, origin, rotationMatrix);
192+
rotatedPoints.push(...rotated);
193+
}
194+
195+
return rotatedPoints;
196+
}

packages/tools/src/workers/polySegConverters.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
projectTo2D,
1515
} from '../utilities/math/polyline';
1616
import { isPlaneIntersectingAABB } from '../utilities/planar';
17+
import { checkStandardBasis, rotatePoints } from '../geometricSurfaceUtils';
1718

1819
/**
1920
* Object containing methods for converting between different representations of
@@ -118,6 +119,15 @@ const polySegConverters = {
118119
args.origin,
119120
[args.segmentIndex]
120121
);
122+
const rotationInfo = checkStandardBasis(args.direction);
123+
if (!rotationInfo.isStandard) {
124+
const rotatedPoints = rotatePoints(
125+
rotationInfo.rotationMatrix,
126+
args.origin,
127+
results.points
128+
);
129+
results.points = [...rotatedPoints];
130+
}
121131
return results;
122132
},
123133
/**

0 commit comments

Comments
 (0)