@@ -71,68 +71,26 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
7171 }
7272}
7373
74+ /** Vector addition */
7475function add ( a : number [ ] , b : number [ ] ) : number [ ] {
7576 return a . map ( ( x , i ) => x + b [ i ] )
7677}
7778
79+ /** Vector subtraction */
7880function sub ( a : number [ ] , b : number [ ] ) : number [ ] {
7981 return a . map ( ( x , i ) => x - b [ i ] )
8082}
8183
84+ /** Scalar multiplication */
8285function scale ( vec : number [ ] , scalar : number ) : number [ ] {
8386 return vec . map ( ( x ) => x * scalar )
8487}
8588
89+ /** Dot product */
8690function 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-
13694function 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 */
167125export 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