@fideus-labs/fidnii 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { mat4 } from "gl-matrix";
4
+ import { applyOrientationToAffine } from "./orientation.js";
4
5
  /**
5
6
  * Create a 4x4 affine transformation matrix from OME-Zarr scale and translation.
6
7
  *
@@ -60,11 +61,22 @@ export function createAffineFromOMEZarr(scale, translation) {
60
61
  /**
61
62
  * Create an affine matrix from an NgffImage.
62
63
  *
63
- * @param ngffImage - The NgffImage containing scale and translation
64
- * @returns 4x4 affine matrix
64
+ * If the image has RFC-4 anatomical orientation metadata
65
+ * (`axesOrientations`), the affine column vectors and translations
66
+ * are sign-flipped so the matrix encodes direction relative to
67
+ * the NIfTI RAS+ convention. This allows NiiVue's `calculateRAS()`
68
+ * to correctly determine the anatomical layout.
69
+ *
70
+ * When no orientation metadata is present, the matrix is identical
71
+ * to a plain scale + translation affine (backward-compatible).
72
+ *
73
+ * @param ngffImage - The NgffImage containing scale, translation,
74
+ * and optional `axesOrientations`
75
+ * @returns 4x4 affine matrix with orientation signs applied
65
76
  */
66
77
  export function createAffineFromNgffImage(ngffImage) {
67
- return createAffineFromOMEZarr(ngffImage.scale, ngffImage.translation);
78
+ const affine = createAffineFromOMEZarr(ngffImage.scale, ngffImage.translation);
79
+ return applyOrientationToAffine(affine, ngffImage.axesOrientations);
68
80
  }
69
81
  /**
70
82
  * Convert an affine matrix to a flat array for NIfTI header.
@@ -1 +1 @@
1
- {"version":3,"file":"affine.js","sourceRoot":"","sources":["../../src/utils/affine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"affine.js","sourceRoot":"","sources":["../../src/utils/affine.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,+BAA+B;AAG/B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAA6B,EAC7B,WAAmC;IAEnC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAA;IAE5B,qDAAqD;IACrD,8EAA8E;IAC9E,2EAA2E;IAE3E,8CAA8C;IAC9C,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA;IAElC,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,CAAA;IAE9C,sBAAsB;IACtB,0EAA0E;IAC1E,oEAAoE;IAEpE,4DAA4D;IAC5D,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;IACd,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAEb,6DAA6D;IAC7D,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;IACd,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAEb,4DAA4D;IAC5D,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAEd,wBAAwB;IACxB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;IAEd,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAoB;IAC5D,MAAM,MAAM,GAAG,uBAAuB,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;IAC9E,OAAO,wBAAwB,CAAC,MAAM,EAAE,SAAS,CAAC,gBAAgB,CAAC,CAAA;AACrE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAY;IAK7C,oCAAoC;IACpC,uCAAuC;IACvC,uCAAuC;IACvC,wCAAwC;IACxC,OAAO;QACL,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;KACvD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAY;IAC7C,6DAA6D;IAC7D,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACtE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACtE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEvE,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AACrB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,cAAoB,EACpB,WAAqC,EACrC,WAAqC;IAErC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IAEzC,6BAA6B;IAC7B,yBAAyB;IACzB,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA,CAAC,iBAAiB;IAC7C,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAE3B,yBAAyB;IACzB,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA,CAAC,iBAAiB;IAC7C,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAE3B,yBAAyB;IACzB,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA,CAAC,iBAAiB;IAC7C,MAAM,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;IAE5B,uCAAuC;IACvC,mEAAmE;IACnE,MAAM,iBAAiB,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAA;IAE5D,MAAM,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA,CAAC,WAAW;IAC/D,MAAM,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA,CAAC,WAAW;IAC/D,MAAM,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA,CAAC,WAAW;IAE/D,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAY,EACZ,UAAoC;IAEpC,uDAAuD;IACvD,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,UAAU,CAAA;IACrC,MAAM,OAAO,GAAG;QACd,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACT,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACZ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACZ,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC;QACZ,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACf,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC;QACf,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC;QACf,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;KACnB,CAAA;IAED,IAAI,IAAI,GAAG,QAAQ,EACjB,IAAI,GAAG,QAAQ,EACf,IAAI,GAAG,QAAQ,CAAA;IACjB,IAAI,IAAI,GAAG,CAAC,QAAQ,EAClB,IAAI,GAAG,CAAC,QAAQ,EAChB,IAAI,GAAG,CAAC,QAAQ,CAAA;IAElB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;QAChC,gDAAgD;QAChD,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;QACrE,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;QACrE,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;QAEtE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAC3B,CAAC;IAED,OAAO;QACL,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QACvB,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;KACxB,CAAA;AACH,CAAC"}
@@ -0,0 +1,157 @@
1
+ import type { AnatomicalOrientation } from "@fideus-labs/ngff-zarr";
2
+ import type { mat4 } from "gl-matrix";
3
+ /**
4
+ * Mapping from an array axis orientation to the physical (RAS) row
5
+ * and sign it should occupy in the NIfTI affine.
6
+ *
7
+ * - `physicalRow`: which row of the 4x4 affine the scale/translation
8
+ * should be placed in (0 = R/L, 1 = A/P, 2 = S/I)
9
+ * - `sign`: `1` if the orientation is in the RAS+ direction,
10
+ * `-1` if opposite
11
+ */
12
+ export interface OrientationMapping {
13
+ readonly physicalRow: 0 | 1 | 2;
14
+ readonly sign: 1 | -1;
15
+ }
16
+ /**
17
+ * Sign multipliers for each spatial axis, used to encode anatomical
18
+ * orientation into the NIfTI affine matrix.
19
+ *
20
+ * A value of `1` means the axis increases in the RAS+ direction
21
+ * (Right, Anterior, Superior). A value of `-1` means it increases
22
+ * in the opposite direction (Left, Posterior, Inferior).
23
+ *
24
+ * @deprecated Use {@link getOrientationMapping} for full permutation
25
+ * support. This interface only captures sign, not axis permutations.
26
+ */
27
+ export interface OrientationSigns {
28
+ readonly x: 1 | -1;
29
+ readonly y: 1 | -1;
30
+ readonly z: 1 | -1;
31
+ }
32
+ /**
33
+ * Get the orientation mapping (physical row + RAS sign) for a single
34
+ * axis orientation.
35
+ *
36
+ * For unknown/exotic orientations, returns `undefined`.
37
+ *
38
+ * @param orientation - The anatomical orientation for one axis
39
+ * @returns The physical row and sign, or `undefined` if not a standard
40
+ * L/R, A/P, or S/I orientation
41
+ */
42
+ export declare function getOrientationInfo(orientation: AnatomicalOrientation): OrientationMapping | undefined;
43
+ /**
44
+ * Get orientation mappings for all three spatial axes.
45
+ *
46
+ * Each mapping tells you which physical (RAS) row the array axis
47
+ * maps to and what sign to apply. This supports both sign flips
48
+ * (e.g. LPS) and axis permutations (e.g. when the OME-Zarr y axis
49
+ * encodes S/I instead of A/P).
50
+ *
51
+ * When no orientation metadata is present, returns the identity
52
+ * mapping: x→row 0 sign +1, y→row 1 sign +1, z→row 2 sign +1.
53
+ *
54
+ * @param axesOrientations - Orientation metadata from
55
+ * `NgffImage.axesOrientations`, or `undefined`
56
+ * @returns Mappings for x, y, and z axes
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // LPS data: x→row0 sign-1, y→row1 sign-1, z→row2 sign+1
61
+ * getOrientationMapping(LPS)
62
+ *
63
+ * // Permuted (mri.nii.gz): x→row0 sign-1, y→row2 sign-1, z→row1 sign+1
64
+ * getOrientationMapping(permutedOrientations)
65
+ * ```
66
+ */
67
+ export declare function getOrientationMapping(axesOrientations: Record<string, AnatomicalOrientation> | undefined): {
68
+ x: OrientationMapping;
69
+ y: OrientationMapping;
70
+ z: OrientationMapping;
71
+ };
72
+ /**
73
+ * Compute RAS+ sign multipliers from RFC-4 anatomical orientation metadata.
74
+ *
75
+ * For each spatial axis, determines whether the axis direction is aligned
76
+ * with (+1) or opposite to (-1) the NIfTI RAS+ convention:
77
+ * - x: positive = left-to-right (R), negative = right-to-left (L)
78
+ * - y: positive = posterior-to-anterior (A), negative = anterior-to-posterior (P)
79
+ * - z: positive = inferior-to-superior (S), negative = superior-to-inferior (I)
80
+ *
81
+ * Only the 6 standard L/R, A/P, I/S orientations are handled. Exotic
82
+ * orientations (dorsal/ventral, rostral/caudal, etc.) are treated as
83
+ * unknown and default to +1.
84
+ *
85
+ * **Note**: This function only returns sign information, not axis
86
+ * permutation. For full permutation support, use
87
+ * {@link getOrientationMapping} and {@link applyOrientationToAffine}.
88
+ *
89
+ * @param axesOrientations - Orientation metadata from `NgffImage.axesOrientations`,
90
+ * or `undefined` if no orientation metadata is present
91
+ * @returns Sign multipliers for x, y, and z axes
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * import { LPS, RAS } from "@fideus-labs/ngff-zarr"
96
+ *
97
+ * // LPS data: x and y are anti-RAS+
98
+ * getOrientationSigns(LPS)
99
+ * // => { x: -1, y: -1, z: 1 }
100
+ *
101
+ * // RAS data: all axes are RAS+
102
+ * getOrientationSigns(RAS)
103
+ * // => { x: 1, y: 1, z: 1 }
104
+ *
105
+ * // No orientation: defaults to all positive
106
+ * getOrientationSigns(undefined)
107
+ * // => { x: 1, y: 1, z: 1 }
108
+ * ```
109
+ */
110
+ export declare function getOrientationSigns(axesOrientations: Record<string, AnatomicalOrientation> | undefined): OrientationSigns;
111
+ /**
112
+ * Apply anatomical orientation to an affine matrix in place.
113
+ *
114
+ * Builds a rotation/permutation matrix from the orientation metadata
115
+ * and applies it to the affine's 3x3 rotation/scale submatrix. This
116
+ * supports both simple sign flips (e.g. LPS where axes align with
117
+ * physical axes but directions differ) and full axis permutations
118
+ * (e.g. when OME-Zarr y axis encodes S/I instead of A/P).
119
+ *
120
+ * The input affine is expected to be a diagonal scale+translation
121
+ * matrix in gl-matrix column-major format, as produced by
122
+ * `createAffineFromOMEZarr()`:
123
+ *
124
+ * ```
125
+ * | sx 0 0 tx |
126
+ * | 0 sy 0 ty |
127
+ * | 0 0 sz tz |
128
+ * | 0 0 0 1 |
129
+ * ```
130
+ *
131
+ * **3x3 submatrix**: Each column's scale is placed in the row
132
+ * corresponding to the physical RAS axis the array axis maps to,
133
+ * with the appropriate sign:
134
+ *
135
+ * ```
136
+ * Column j (array axis j):
137
+ * row = physicalRow for axis j
138
+ * affine[j*4 + row] = sign * scale_j
139
+ * ```
140
+ *
141
+ * **Translation column**: Sign-flipped for LPS→RAS conversion but
142
+ * NOT row-permuted. This is because `itkImageToNgffImage` stores
143
+ * the ITK LPS origin values in array-axis-label order without
144
+ * transforming them through the direction matrix. The label
145
+ * assignment (x/y/z) follows the reversed ITK axis indices, so the
146
+ * physical meaning of each translation value matches its original
147
+ * LPS axis, regardless of axis permutation.
148
+ *
149
+ * When `axesOrientations` is `undefined`, the affine is left
150
+ * unchanged (backward-compatible identity mapping).
151
+ *
152
+ * @param affine - 4x4 affine matrix (column-major, modified in place)
153
+ * @param axesOrientations - Orientation metadata from `NgffImage.axesOrientations`
154
+ * @returns The same affine matrix (for chaining)
155
+ */
156
+ export declare function applyOrientationToAffine(affine: mat4, axesOrientations: Record<string, AnatomicalOrientation> | undefined): mat4;
157
+ //# sourceMappingURL=orientation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orientation.d.ts","sourceRoot":"","sources":["../../src/utils/orientation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC/B,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;CACnB;AAuBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,qBAAqB,GACjC,kBAAkB,GAAG,SAAS,CAEhC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,qBAAqB,CACnC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,GAAG,SAAS,GAClE;IAAE,CAAC,EAAE,kBAAkB,CAAC;IAAC,CAAC,EAAE,kBAAkB,CAAC;IAAC,CAAC,EAAE,kBAAkB,CAAA;CAAE,CA2CzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,GAAG,SAAS,GAClE,gBAAgB,CAOlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,IAAI,EACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,GAAG,SAAS,GAClE,IAAI,CAiDN"}
@@ -0,0 +1,225 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Lookup table mapping each RFC-4 anatomical orientation string to
5
+ * its NIfTI RAS+ physical row and sign.
6
+ *
7
+ * RAS+ convention:
8
+ * - Row 0 (X): left-to-right (R+)
9
+ * - Row 1 (Y): posterior-to-anterior (A+)
10
+ * - Row 2 (Z): inferior-to-superior (S+)
11
+ */
12
+ const ORIENTATION_INFO = {
13
+ // L/R pair → physical row 0
14
+ "left-to-right": { physicalRow: 0, sign: 1 },
15
+ "right-to-left": { physicalRow: 0, sign: -1 },
16
+ // A/P pair → physical row 1
17
+ "posterior-to-anterior": { physicalRow: 1, sign: 1 },
18
+ "anterior-to-posterior": { physicalRow: 1, sign: -1 },
19
+ // S/I pair → physical row 2
20
+ "inferior-to-superior": { physicalRow: 2, sign: 1 },
21
+ "superior-to-inferior": { physicalRow: 2, sign: -1 },
22
+ };
23
+ /**
24
+ * Get the orientation mapping (physical row + RAS sign) for a single
25
+ * axis orientation.
26
+ *
27
+ * For unknown/exotic orientations, returns `undefined`.
28
+ *
29
+ * @param orientation - The anatomical orientation for one axis
30
+ * @returns The physical row and sign, or `undefined` if not a standard
31
+ * L/R, A/P, or S/I orientation
32
+ */
33
+ export function getOrientationInfo(orientation) {
34
+ return ORIENTATION_INFO[orientation.value];
35
+ }
36
+ /**
37
+ * Get orientation mappings for all three spatial axes.
38
+ *
39
+ * Each mapping tells you which physical (RAS) row the array axis
40
+ * maps to and what sign to apply. This supports both sign flips
41
+ * (e.g. LPS) and axis permutations (e.g. when the OME-Zarr y axis
42
+ * encodes S/I instead of A/P).
43
+ *
44
+ * When no orientation metadata is present, returns the identity
45
+ * mapping: x→row 0 sign +1, y→row 1 sign +1, z→row 2 sign +1.
46
+ *
47
+ * @param axesOrientations - Orientation metadata from
48
+ * `NgffImage.axesOrientations`, or `undefined`
49
+ * @returns Mappings for x, y, and z axes
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // LPS data: x→row0 sign-1, y→row1 sign-1, z→row2 sign+1
54
+ * getOrientationMapping(LPS)
55
+ *
56
+ * // Permuted (mri.nii.gz): x→row0 sign-1, y→row2 sign-1, z→row1 sign+1
57
+ * getOrientationMapping(permutedOrientations)
58
+ * ```
59
+ */
60
+ export function getOrientationMapping(axesOrientations) {
61
+ const defaultMapping = {
62
+ x: { physicalRow: 0, sign: 1 },
63
+ y: { physicalRow: 1, sign: 1 },
64
+ z: { physicalRow: 2, sign: 1 },
65
+ };
66
+ if (!axesOrientations) {
67
+ return defaultMapping;
68
+ }
69
+ const xOrientation = axesOrientations.x ?? axesOrientations.X;
70
+ const yOrientation = axesOrientations.y ?? axesOrientations.Y;
71
+ const zOrientation = axesOrientations.z ?? axesOrientations.Z;
72
+ const mapping = {
73
+ x: (xOrientation ? getOrientationInfo(xOrientation) : undefined) ??
74
+ defaultMapping.x,
75
+ y: (yOrientation ? getOrientationInfo(yOrientation) : undefined) ??
76
+ defaultMapping.y,
77
+ z: (zOrientation ? getOrientationInfo(zOrientation) : undefined) ??
78
+ defaultMapping.z,
79
+ };
80
+ // Validate that each physicalRow is used exactly once to prevent
81
+ // degenerate affine matrices where columns overwrite each other
82
+ const rowsUsed = new Set([
83
+ mapping.x.physicalRow,
84
+ mapping.y.physicalRow,
85
+ mapping.z.physicalRow,
86
+ ]);
87
+ if (rowsUsed.size !== 3) {
88
+ console.warn("[fidnii] Invalid orientation metadata: multiple axes map to the same physical row. Falling back to identity mapping.");
89
+ return defaultMapping;
90
+ }
91
+ return mapping;
92
+ }
93
+ /**
94
+ * Compute RAS+ sign multipliers from RFC-4 anatomical orientation metadata.
95
+ *
96
+ * For each spatial axis, determines whether the axis direction is aligned
97
+ * with (+1) or opposite to (-1) the NIfTI RAS+ convention:
98
+ * - x: positive = left-to-right (R), negative = right-to-left (L)
99
+ * - y: positive = posterior-to-anterior (A), negative = anterior-to-posterior (P)
100
+ * - z: positive = inferior-to-superior (S), negative = superior-to-inferior (I)
101
+ *
102
+ * Only the 6 standard L/R, A/P, I/S orientations are handled. Exotic
103
+ * orientations (dorsal/ventral, rostral/caudal, etc.) are treated as
104
+ * unknown and default to +1.
105
+ *
106
+ * **Note**: This function only returns sign information, not axis
107
+ * permutation. For full permutation support, use
108
+ * {@link getOrientationMapping} and {@link applyOrientationToAffine}.
109
+ *
110
+ * @param axesOrientations - Orientation metadata from `NgffImage.axesOrientations`,
111
+ * or `undefined` if no orientation metadata is present
112
+ * @returns Sign multipliers for x, y, and z axes
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * import { LPS, RAS } from "@fideus-labs/ngff-zarr"
117
+ *
118
+ * // LPS data: x and y are anti-RAS+
119
+ * getOrientationSigns(LPS)
120
+ * // => { x: -1, y: -1, z: 1 }
121
+ *
122
+ * // RAS data: all axes are RAS+
123
+ * getOrientationSigns(RAS)
124
+ * // => { x: 1, y: 1, z: 1 }
125
+ *
126
+ * // No orientation: defaults to all positive
127
+ * getOrientationSigns(undefined)
128
+ * // => { x: 1, y: 1, z: 1 }
129
+ * ```
130
+ */
131
+ export function getOrientationSigns(axesOrientations) {
132
+ const mapping = getOrientationMapping(axesOrientations);
133
+ return {
134
+ x: mapping.x.sign,
135
+ y: mapping.y.sign,
136
+ z: mapping.z.sign,
137
+ };
138
+ }
139
+ /**
140
+ * Apply anatomical orientation to an affine matrix in place.
141
+ *
142
+ * Builds a rotation/permutation matrix from the orientation metadata
143
+ * and applies it to the affine's 3x3 rotation/scale submatrix. This
144
+ * supports both simple sign flips (e.g. LPS where axes align with
145
+ * physical axes but directions differ) and full axis permutations
146
+ * (e.g. when OME-Zarr y axis encodes S/I instead of A/P).
147
+ *
148
+ * The input affine is expected to be a diagonal scale+translation
149
+ * matrix in gl-matrix column-major format, as produced by
150
+ * `createAffineFromOMEZarr()`:
151
+ *
152
+ * ```
153
+ * | sx 0 0 tx |
154
+ * | 0 sy 0 ty |
155
+ * | 0 0 sz tz |
156
+ * | 0 0 0 1 |
157
+ * ```
158
+ *
159
+ * **3x3 submatrix**: Each column's scale is placed in the row
160
+ * corresponding to the physical RAS axis the array axis maps to,
161
+ * with the appropriate sign:
162
+ *
163
+ * ```
164
+ * Column j (array axis j):
165
+ * row = physicalRow for axis j
166
+ * affine[j*4 + row] = sign * scale_j
167
+ * ```
168
+ *
169
+ * **Translation column**: Sign-flipped for LPS→RAS conversion but
170
+ * NOT row-permuted. This is because `itkImageToNgffImage` stores
171
+ * the ITK LPS origin values in array-axis-label order without
172
+ * transforming them through the direction matrix. The label
173
+ * assignment (x/y/z) follows the reversed ITK axis indices, so the
174
+ * physical meaning of each translation value matches its original
175
+ * LPS axis, regardless of axis permutation.
176
+ *
177
+ * When `axesOrientations` is `undefined`, the affine is left
178
+ * unchanged (backward-compatible identity mapping).
179
+ *
180
+ * @param affine - 4x4 affine matrix (column-major, modified in place)
181
+ * @param axesOrientations - Orientation metadata from `NgffImage.axesOrientations`
182
+ * @returns The same affine matrix (for chaining)
183
+ */
184
+ export function applyOrientationToAffine(affine, axesOrientations) {
185
+ if (!axesOrientations) {
186
+ return affine;
187
+ }
188
+ const mapping = getOrientationMapping(axesOrientations);
189
+ // Extract the current diagonal scale and translation values
190
+ // (the input affine is expected to be diagonal from createAffineFromOMEZarr)
191
+ const sx = affine[0];
192
+ const sy = affine[5];
193
+ const sz = affine[10];
194
+ const tx = affine[12];
195
+ const ty = affine[13];
196
+ const tz = affine[14];
197
+ // Clear the 3x3 submatrix (translation will be overwritten below)
198
+ // Column 0
199
+ affine[0] = 0;
200
+ affine[1] = 0;
201
+ affine[2] = 0;
202
+ // Column 1
203
+ affine[4] = 0;
204
+ affine[5] = 0;
205
+ affine[6] = 0;
206
+ // Column 2
207
+ affine[8] = 0;
208
+ affine[9] = 0;
209
+ affine[10] = 0;
210
+ // Place each axis' scale into the correct physical row.
211
+ // gl-matrix is column-major: index = col * 4 + row
212
+ // Column 0 (array x axis): place sx at physicalRow for x
213
+ affine[0 + mapping.x.physicalRow] = mapping.x.sign * sx;
214
+ // Column 1 (array y axis): place sy at physicalRow for y
215
+ affine[4 + mapping.y.physicalRow] = mapping.y.sign * sy;
216
+ // Column 2 (array z axis): place sz at physicalRow for z
217
+ affine[8 + mapping.z.physicalRow] = mapping.z.sign * sz;
218
+ // Translation: sign-flip for LPS→RAS but keep at original row
219
+ // positions (x→row 0, y→row 1, z→row 2). See JSDoc for why.
220
+ affine[12] = mapping.x.sign * tx;
221
+ affine[13] = mapping.y.sign * ty;
222
+ affine[14] = mapping.z.sign * tz;
223
+ return affine;
224
+ }
225
+ //# sourceMappingURL=orientation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orientation.js","sourceRoot":"","sources":["../../src/utils/orientation.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fideus-labs/fidnii",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Render OME-Zarr images in NiiVue with progressive multi-resolution loading",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,18 +16,18 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@fideus-labs/fiff": "^0.3.2",
20
- "@fideus-labs/ngff-zarr": "^0.11.0",
19
+ "@fideus-labs/fiff": "^0.5.2",
20
+ "@fideus-labs/ngff-zarr": "^0.12.0",
21
21
  "@itk-wasm/downsample": "^1.8.1",
22
- "lru-cache": "^11.1.0",
23
22
  "@niivue/niivue": "^0.67.0",
24
23
  "gl-matrix": "^3.4.4",
24
+ "lru-cache": "^11.1.0",
25
25
  "nifti-reader-js": "^0.8.0",
26
26
  "zarrita": "^0.6.1",
27
27
  "zod": "^3.25.76"
28
28
  },
29
29
  "devDependencies": {
30
- "@fideus-labs/fizarrita": "^1.3.0",
30
+ "@fideus-labs/fizarrita": "^1.4.0",
31
31
  "@fideus-labs/worker-pool": "^1.0.0",
32
32
  "@playwright/test": "^1.58.1",
33
33
  "@types/node": "^20.19.30",
@@ -68,8 +68,14 @@ import {
68
68
  affineToNiftiSrows,
69
69
  calculateWorldBounds,
70
70
  createAffineFromNgffImage,
71
+ createAffineFromOMEZarr,
71
72
  } from "./utils/affine.js"
72
73
  import { worldToPixel } from "./utils/coordinates.js"
74
+ import {
75
+ applyOrientationToAffine,
76
+ getOrientationMapping,
77
+ getOrientationSigns,
78
+ } from "./utils/orientation.js"
73
79
  import {
74
80
  boundsApproxEqual,
75
81
  computeViewportBounds2D,
@@ -301,8 +307,14 @@ export class OMEZarrNVImage extends NVImage {
301
307
  this._is2D = highResImage.dims.indexOf("z") === -1
302
308
  this._flipY2D = options.flipY2D ?? true
303
309
 
304
- // Calculate volume bounds from highest resolution for most accurate bounds
305
- const highResAffine = createAffineFromNgffImage(highResImage)
310
+ // Calculate volume bounds from highest resolution for most accurate bounds.
311
+ // Use the unadjusted affine (no orientation signs) because volume bounds
312
+ // live in OME-Zarr world space and drive internal clip-plane / viewport math.
313
+ // Orientation is only applied to the NIfTI affine passed to NiiVue.
314
+ const highResAffine = createAffineFromOMEZarr(
315
+ highResImage.scale,
316
+ highResImage.translation,
317
+ )
306
318
  const highResShape = getVolumeShape(highResImage)
307
319
  this._volumeBounds = calculateWorldBounds(highResAffine, highResShape)
308
320
 
@@ -400,13 +412,27 @@ export class OMEZarrNVImage extends NVImage {
400
412
  // Placeholder pixel dimensions
401
413
  hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0]
402
414
 
403
- // Placeholder affine (identity, with y-flip for 2D images)
404
- hdr.affine = [
405
- [1, 0, 0, 0],
406
- [0, this._flipY2D && this._is2D ? -1 : 1, 0, 0],
407
- [0, 0, 1, 0],
415
+ // Placeholder affine (unit scale with orientation permutation/signs)
416
+ // This is replaced by real data when loadResolutionLevel completes,
417
+ // but having a reasonable placeholder avoids rendering glitches during
418
+ // the initial frame.
419
+ const mapping = getOrientationMapping(
420
+ this.multiscales.images[0]?.axesOrientations,
421
+ )
422
+ const placeholderAffine = [
423
+ [0, 0, 0, 0],
424
+ [0, 0, 0, 0],
425
+ [0, 0, 0, 0],
408
426
  [0, 0, 0, 1],
409
427
  ]
428
+ placeholderAffine[mapping.x.physicalRow][0] = mapping.x.sign
429
+ let ySign = mapping.y.sign
430
+ if (this._flipY2D && this._is2D) {
431
+ ySign = (ySign * -1) as 1 | -1
432
+ }
433
+ placeholderAffine[mapping.y.physicalRow][1] = ySign
434
+ placeholderAffine[mapping.z.physicalRow][2] = mapping.z.sign
435
+ hdr.affine = placeholderAffine
410
436
 
411
437
  hdr.sform_code = 1 // Scanner coordinates
412
438
 
@@ -698,25 +724,22 @@ export class OMEZarrNVImage extends NVImage {
698
724
  1,
699
725
  ]
700
726
 
701
- // Build affine with offset for region start
702
- const affine = createAffineFromNgffImage(ngffImage)
703
-
704
- // Adjust translation for region offset
705
- // Buffer pixel [0,0,0] corresponds to source pixel region.chunkAlignedStart
727
+ // Build the unadjusted affine (no orientation signs) and offset it
728
+ // to the loaded region. Buffer bounds use this OME-Zarr-space affine
729
+ // for internal clip-plane / viewport math.
706
730
  const regionStart = region.chunkAlignedStart
731
+ const affine = createAffineFromOMEZarr(
732
+ ngffImage.scale,
733
+ ngffImage.translation,
734
+ )
707
735
  // regionStart is [z, y, x], affine translation is [x, y, z] (indices 12, 13, 14)
708
736
  affine[12] += regionStart[2] * sx // x offset
709
737
  affine[13] += regionStart[1] * sy // y offset
710
738
  affine[14] += regionStart[0] * sz // z offset
711
739
 
712
- // Update current buffer bounds from the un-flipped affine
713
- // (bounds stay in OME-Zarr world space for clip plane math)
740
+ // Update current buffer bounds in OME-Zarr world space
714
741
  this._currentBufferBounds = {
715
- min: [
716
- affine[12], // x offset (world coord of buffer origin)
717
- affine[13], // y offset
718
- affine[14], // z offset
719
- ],
742
+ min: [affine[12], affine[13], affine[14]],
720
743
  max: [
721
744
  affine[12] + fetchedShape[2] * sx,
722
745
  affine[13] + fetchedShape[1] * sy,
@@ -724,11 +747,21 @@ export class OMEZarrNVImage extends NVImage {
724
747
  ],
725
748
  }
726
749
 
727
- // For 2D images, negate y-scale so NiiVue's calculateRAS() flips
728
- // the rows to account for top-to-bottom pixel storage order.
750
+ // Apply orientation signs so the NIfTI affine encodes anatomical
751
+ // direction for NiiVue's calculateRAS()
752
+ applyOrientationToAffine(affine, ngffImage.axesOrientations)
753
+
754
+ // For 2D images, flip y so NiiVue's calculateRAS() accounts for
755
+ // top-to-bottom pixel storage order. We shift the translation so
756
+ // the last row maps to where the first row was, then negate the
757
+ // y column. This composes correctly with any orientation sign.
729
758
  if (this._flipY2D && this._is2D) {
730
- affine[5] = -sy
731
- affine[13] += (fetchedShape[1] - 1) * sy
759
+ // Get the y axis orientation mapping to find where the y scale is stored
760
+ const mapping = getOrientationMapping(ngffImage.axesOrientations)
761
+ // The y scale is at affine[4 + physicalRow] (column 1, appropriate row)
762
+ const yScaleIndex = 4 + mapping.y.physicalRow
763
+ affine[13] += affine[yScaleIndex] * (fetchedShape[1] - 1)
764
+ affine[yScaleIndex] = -affine[yScaleIndex]
732
765
  }
733
766
 
734
767
  // Update affine in header
@@ -2366,17 +2399,28 @@ export class OMEZarrNVImage extends NVImage {
2366
2399
  1,
2367
2400
  ]
2368
2401
 
2369
- // Build affine with offset for region start, then normalize
2402
+ // Build affine with orientation signs, then offset for region start.
2403
+ // Use the original unoriented scale values with orientation signs for
2404
+ // the translation offset calculation. This ensures correctness even when
2405
+ // axes are permuted (where affine diagonal may be zero).
2370
2406
  const affine = createAffineFromNgffImage(ngffImage)
2407
+
2408
+ // Get orientation signs to apply to the offset calculation
2409
+ const signs = getOrientationSigns(ngffImage.axesOrientations)
2410
+
2371
2411
  // Adjust translation for region offset (fetchStart is [z, y, x])
2372
- affine[12] += fetchStart[2] * sx // x offset
2373
- affine[13] += fetchStart[1] * sy // y offset
2374
- affine[14] += fetchStart[0] * sz // z offset
2412
+ affine[12] += fetchStart[2] * signs.x * sx // x offset (orientation-aware)
2413
+ affine[13] += fetchStart[1] * signs.y * sy // y offset (orientation-aware)
2414
+ affine[14] += fetchStart[0] * signs.z * sz // z offset (orientation-aware)
2375
2415
 
2376
- // For 2D images, negate y-scale before normalization
2416
+ // For 2D images, flip y before normalization (composes with orientation)
2377
2417
  if (this._flipY2D && this._is2D) {
2378
- affine[5] = -sy
2379
- affine[13] += (fetchedShape[1] - 1) * sy
2418
+ // Get the y axis orientation mapping to find where the y scale is stored
2419
+ const mapping = getOrientationMapping(ngffImage.axesOrientations)
2420
+ // The y scale is at affine[4 + physicalRow] (column 1, appropriate row)
2421
+ const yScaleIndex = 4 + mapping.y.physicalRow
2422
+ affine[13] += affine[yScaleIndex] * (fetchedShape[1] - 1)
2423
+ affine[yScaleIndex] = -affine[yScaleIndex]
2380
2424
  }
2381
2425
 
2382
2426
  // Apply normalization to the entire affine (scale columns + translation)