@@ -70,3 +70,111 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
7070 throw { key : 'NIFTI_HEADER_UNREADABLE' }
7171 }
7272}
73+
74+ function add ( a : number [ ] , b : number [ ] ) : number [ ] {
75+ return a . map ( ( x , i ) => x + b [ i ] )
76+ }
77+
78+ function sub ( a : number [ ] , b : number [ ] ) : number [ ] {
79+ return a . map ( ( x , i ) => x - b [ i ] )
80+ }
81+
82+ function scale ( vec : number [ ] , scalar : number ) : number [ ] {
83+ return vec . map ( ( x ) => x * scalar )
84+ }
85+
86+ function dot ( a : number [ ] , b : number [ ] ) : number {
87+ return a . map ( ( x , i ) => x * b [ i ] ) . reduce ( ( acc , x ) => acc + x , 0 )
88+ }
89+
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+
136+ function argMax ( arr : number [ ] ) : number {
137+ return arr . reduce ( ( acc , x , i ) => ( x > arr [ acc ] ? i : acc ) , 0 )
138+ }
139+
140+ /**
141+ * Identify the nearest principle axes of an image affine.
142+ *
143+ * Affines transform indices in a data array into mm right, anterior and superior of
144+ * an origin in "world coordinates". If moving along an axis in the positive direction
145+ * predominantly moves right, that axis is labeled "R".
146+ *
147+ * @example The identity matrix is in "RAS" orientation:
148+ *
149+ * # Usage
150+ *
151+ * ```ts
152+ * const affine = [[1, 0, 0, 0],
153+ * [0, 1, 0, 0],
154+ * [0, 0, 1, 0],
155+ * [0, 0, 0, 1]]
156+ *
157+ * axisCodes(affine)
158+ * ```
159+ *
160+ * # Result
161+ * ```ts
162+ * ['R', 'A', 'S']
163+ * ```
164+ *
165+ * @returns character codes describing the orientation of an image affine.
166+ */
167+ 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 ) ) )
171+
172+ // Check that indices are 0, 1 and 2 in some order
173+ if ( maxIndices . toSorted ( ) . some ( ( idx , i ) => idx !== i ) ) {
174+ throw { key : 'AMBIGUOUS_AFFINE' }
175+ }
176+
177+ // Positive/negative codes for each world axis
178+ const codes = [ 'RL' , 'AP' , 'SI' ]
179+ return maxIndices . map ( ( idx , i ) => codes [ idx ] [ rotations [ i ] [ idx ] > 0 ? 0 : 1 ] )
180+ }
0 commit comments