@@ -59,68 +59,26 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
5959 }
6060}
6161
62+ /** Vector addition */
6263function add ( a : number [ ] , b : number [ ] ) : number [ ] {
6364 return a . map ( ( x , i ) => x + b [ i ] )
6465}
6566
67+ /** Vector subtraction */
6668function sub ( a : number [ ] , b : number [ ] ) : number [ ] {
6769 return a . map ( ( x , i ) => x - b [ i ] )
6870}
6971
72+ /** Scalar multiplication */
7073function scale ( vec : number [ ] , scalar : number ) : number [ ] {
7174 return vec . map ( ( x ) => x * scalar )
7275}
7376
77+ /** Dot product */
7478function 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-
12482function 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 */
155113export 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