Skip to content

Commit 09fb921

Browse files
committed
rf: Remove unnecessary scaling, just orthogonalize
1 parent 7147381 commit 09fb921

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
@@ -71,68 +71,26 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
7171
}
7272
}
7373

74+
/** Vector addition */
7475
function add(a: number[], b: number[]): number[] {
7576
return a.map((x, i) => x + b[i])
7677
}
7778

79+
/** Vector subtraction */
7880
function sub(a: number[], b: number[]): number[] {
7981
return a.map((x, i) => x - b[i])
8082
}
8183

84+
/** Scalar multiplication */
8285
function scale(vec: number[], scalar: number): number[] {
8386
return vec.map((x) => x * scalar)
8487
}
8588

89+
/** Dot product */
8690
function dot(a: number[], b: number[]): number {
8791
return a.map((x, i) => x * b[i]).reduce((acc, x) => acc + x, 0)
8892
}
8993

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

172146
// Check that indices are 0, 1 and 2 in some order
173147
if (maxIndices.toSorted().some((idx, i) => idx !== i)) {
@@ -176,5 +150,5 @@ export function axisCodes(affine: number[][]): string[] {
176150

177151
// Positive/negative codes for each world axis
178152
const codes = ['RL', 'AP', 'SI']
179-
return maxIndices.map((idx, i) => codes[idx][rotations[i][idx] > 0 ? 0 : 1])
153+
return maxIndices.map((idx, i) => codes[idx][basis[i][idx] > 0 ? 0 : 1])
180154
}

0 commit comments

Comments
 (0)