Skip to content

Commit 642cedb

Browse files
committed
rf: Remove unnecessary scaling, just orthogonalize
1 parent 7975907 commit 642cedb

File tree

1 file changed

+24
-50
lines changed

1 file changed

+24
-50
lines changed

src/files/nifti.ts

Lines changed: 24 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,68 +59,26 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
5959
}
6060
}
6161

62+
/** Vector addition */
6263
function add(a: number[], b: number[]): number[] {
6364
return a.map((x, i) => x + b[i])
6465
}
6566

67+
/** Vector subtraction */
6668
function sub(a: number[], b: number[]): number[] {
6769
return a.map((x, i) => x - b[i])
6870
}
6971

72+
/** Scalar multiplication */
7073
function scale(vec: number[], scalar: number): number[] {
7174
return vec.map((x) => x * scalar)
7275
}
7376

77+
/** Dot product */
7478
function dot(a: number[], b: number[]): number {
7579
return a.map((x, i) => x * b[i]).reduce((acc, x) => acc + x, 0)
7680
}
7781

78-
function extractRotation(affine: number[][]): number[][] {
79-
// This function is an extract of the Python function transforms3d.affines.decompose44
80-
// (https://github.com/matthew-brett/transforms3d/blob/6a43a98/transforms3d/affines.py#L10-L153)
81-
//
82-
// To explain the conventions of the s{xyz}* parameters:
83-
//
84-
// The upper left 3x3 of the affine is a matrix we will call RZS which can be decomposed
85-
//
86-
// RZS = R * Z * S
87-
//
88-
// where R is a 3x3 rotation matrix, Z is a diagonal matrix of scalings
89-
//
90-
// Z = diag([sx, xy, sz])
91-
//
92-
// and S is a shear matrix with the form
93-
//
94-
// S = [[1, sxy, sxz],
95-
// [0, 1, syz],
96-
// [0, 0, 1]]
97-
//
98-
// Note that this function does not return scales, shears or translations, and
99-
// does not guarantee a right-handed rotation matrix, as that is not necessary for our use.
100-
101-
// Operate on columns, which are the cosines that project input coordinates onto output axes
102-
const [cosX, cosY, cosZ] = [0, 1, 2].map((j) => [0, 1, 2].map((i) => affine[i][j]))
103-
104-
const sx = Math.sqrt(dot(cosX, cosX))
105-
const normX = cosX.map((x) => x / sx) // Unit vector
106-
107-
// Orthogonalize cosY with respect to normX
108-
const sx_sxy = dot(normX, cosY)
109-
const orthY = sub(cosY, scale(normX, sx_sxy))
110-
const sy = Math.sqrt(dot(orthY, orthY))
111-
const normY = orthY.map((y) => y / sy)
112-
113-
// Orthogonalize cosZ with respect to normX and normY
114-
const sx_sxz = dot(normX, cosZ)
115-
const sy_syz = dot(normY, cosZ)
116-
const orthZ = sub(cosZ, add(scale(normX, sx_sxz), scale(normY, sy_syz)))
117-
const sz = Math.sqrt(dot(orthZ, orthZ))
118-
const normZ = orthZ.map((z) => z / sz)
119-
120-
// Transposed normalized cosines
121-
return [normX, normY, normZ]
122-
}
123-
12482
function argMax(arr: number[]): number {
12583
return arr.reduce((acc, x, i) => (x > arr[acc] ? i : acc), 0)
12684
}
@@ -153,9 +111,25 @@ function argMax(arr: number[]): number {
153111
* @returns character codes describing the orientation of an image affine.
154112
*/
155113
export function axisCodes(affine: number[][]): string[] {
156-
// Note that rotation is transposed
157-
const rotations = extractRotation(affine)
158-
const maxIndices = rotations.map((row) => argMax(row.map(Math.abs)))
114+
// This function is an extract of the Python function transforms3d.affines.decompose44
115+
// (https://github.com/matthew-brett/transforms3d/blob/6a43a98/transforms3d/affines.py#L10-L153)
116+
//
117+
// As an optimization, this only orthogonalizes the basis,
118+
// and does not normalize to unit vectors.
119+
120+
// Operate on columns, which are the cosines that project input coordinates onto output axes
121+
const [cosX, cosY, cosZ] = [0, 1, 2].map((j) => [0, 1, 2].map((i) => affine[i][j]))
122+
123+
// Orthogonalize cosY with respect to cosX
124+
const orthY = sub(cosY, scale(cosX, dot(cosX, cosY)))
125+
126+
// Orthogonalize cosZ with respect to cosX and orthY
127+
const orthZ = sub(
128+
cosZ, add(scale(cosX, dot(cosX, cosZ)), scale(orthY, dot(orthY, cosZ)))
129+
)
130+
131+
const basis = [cosX, orthY, orthZ]
132+
const maxIndices = basis.map((row) => argMax(row.map(Math.abs)))
159133

160134
// Check that indices are 0, 1 and 2 in some order
161135
if (maxIndices.toSorted().some((idx, i) => idx !== i)) {
@@ -164,5 +138,5 @@ export function axisCodes(affine: number[][]): string[] {
164138

165139
// Positive/negative codes for each world axis
166140
const codes = ['RL', 'AP', 'SI']
167-
return maxIndices.map((idx, i) => codes[idx][rotations[i][idx] > 0 ? 0 : 1])
141+
return maxIndices.map((idx, i) => codes[idx][basis[i][idx] > 0 ? 0 : 1])
168142
}

0 commit comments

Comments
 (0)