@@ -58,3 +58,111 @@ export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
5858 throw { key : 'NIFTI_HEADER_UNREADABLE' }
5959 }
6060}
61+
62+ function add ( a : number [ ] , b : number [ ] ) : number [ ] {
63+ return a . map ( ( x , i ) => x + b [ i ] )
64+ }
65+
66+ function sub ( a : number [ ] , b : number [ ] ) : number [ ] {
67+ return a . map ( ( x , i ) => x - b [ i ] )
68+ }
69+
70+ function scale ( vec : number [ ] , scalar : number ) : number [ ] {
71+ return vec . map ( ( x ) => x * scalar )
72+ }
73+
74+ function dot ( a : number [ ] , b : number [ ] ) : number {
75+ return a . map ( ( x , i ) => x * b [ i ] ) . reduce ( ( acc , x ) => acc + x , 0 )
76+ }
77+
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+
124+ function argMax ( arr : number [ ] ) : number {
125+ return arr . reduce ( ( acc , x , i ) => ( x > arr [ acc ] ? i : acc ) , 0 )
126+ }
127+
128+ /**
129+ * Identify the nearest principle axes of an image affine.
130+ *
131+ * Affines transform indices in a data array into mm right, anterior and superior of
132+ * an origin in "world coordinates". If moving along an axis in the positive direction
133+ * predominantly moves right, that axis is labeled "R".
134+ *
135+ * @example The identity matrix is in "RAS" orientation:
136+ *
137+ * # Usage
138+ *
139+ * ```ts
140+ * const affine = [[1, 0, 0, 0],
141+ * [0, 1, 0, 0],
142+ * [0, 0, 1, 0],
143+ * [0, 0, 0, 1]]
144+ *
145+ * axisCodes(affine)
146+ * ```
147+ *
148+ * # Result
149+ * ```ts
150+ * ['R', 'A', 'S']
151+ * ```
152+ *
153+ * @returns character codes describing the orientation of an image affine.
154+ */
155+ 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 ) ) )
159+
160+ // Check that indices are 0, 1 and 2 in some order
161+ if ( maxIndices . toSorted ( ) . some ( ( idx , i ) => idx !== i ) ) {
162+ throw { key : 'AMBIGUOUS_AFFINE' }
163+ }
164+
165+ // Positive/negative codes for each world axis
166+ const codes = [ 'RL' , 'AP' , 'SI' ]
167+ return maxIndices . map ( ( idx , i ) => codes [ idx ] [ rotations [ i ] [ idx ] > 0 ? 0 : 1 ] )
168+ }
0 commit comments