@fideus-labs/fidnii 0.2.0 → 0.3.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.
Files changed (40) hide show
  1. package/dist/BufferManager.d.ts +37 -4
  2. package/dist/BufferManager.d.ts.map +1 -1
  3. package/dist/BufferManager.js +70 -22
  4. package/dist/BufferManager.js.map +1 -1
  5. package/dist/OMEZarrNVImage.d.ts +26 -0
  6. package/dist/OMEZarrNVImage.d.ts.map +1 -1
  7. package/dist/OMEZarrNVImage.js +144 -24
  8. package/dist/OMEZarrNVImage.js.map +1 -1
  9. package/dist/RegionCoalescer.d.ts +16 -0
  10. package/dist/RegionCoalescer.d.ts.map +1 -1
  11. package/dist/RegionCoalescer.js +42 -5
  12. package/dist/RegionCoalescer.js.map +1 -1
  13. package/dist/ResolutionSelector.d.ts +14 -2
  14. package/dist/ResolutionSelector.d.ts.map +1 -1
  15. package/dist/ResolutionSelector.js +26 -16
  16. package/dist/ResolutionSelector.js.map +1 -1
  17. package/dist/index.d.ts +6 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/normalize.d.ts +50 -0
  22. package/dist/normalize.d.ts.map +1 -0
  23. package/dist/normalize.js +95 -0
  24. package/dist/normalize.js.map +1 -0
  25. package/dist/types.d.ts +66 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +66 -0
  28. package/dist/types.js.map +1 -1
  29. package/dist/utils/coordinates.d.ts.map +1 -1
  30. package/dist/utils/coordinates.js +20 -26
  31. package/dist/utils/coordinates.js.map +1 -1
  32. package/package.json +3 -4
  33. package/src/BufferManager.ts +83 -22
  34. package/src/OMEZarrNVImage.ts +190 -24
  35. package/src/RegionCoalescer.ts +45 -5
  36. package/src/ResolutionSelector.ts +32 -16
  37. package/src/index.ts +13 -2
  38. package/src/normalize.ts +119 -0
  39. package/src/types.ts +95 -1
  40. package/src/utils/coordinates.ts +26 -24
@@ -5,6 +5,18 @@ import type { TypedArray, ZarrDtype } from "./types.js";
5
5
  * The buffer is resized to match the fetched data dimensions exactly.
6
6
  * Memory is reused when possible to avoid unnecessary allocations.
7
7
  *
8
+ * For multi-component images (RGB/RGBA), `componentsPerVoxel` controls
9
+ * how many scalar elements each spatial voxel occupies. The buffer is
10
+ * sized to hold `spatialPixels * componentsPerVoxel` elements, and the
11
+ * typed array view spans all of them. Spatial dimensions (`[z, y, x]`)
12
+ * track only the spatial extent; the component count is a fixed
13
+ * multiplier on the element count.
14
+ *
15
+ * When the source dtype is not uint8 and `componentsPerVoxel > 1`
16
+ * (non-uint8 RGB/RGBA), the buffer stores **uint8** data because NiiVue
17
+ * only supports `DT_RGB24` / `DT_RGBA32` (uint8-per-channel). The raw
18
+ * data is normalized to uint8 externally before being written here.
19
+ *
8
20
  * Memory reuse strategy:
9
21
  * - Reuse buffer if newSize <= currentCapacity
10
22
  * - Reallocate if newSize > currentCapacity OR newSize < 25% of currentCapacity
@@ -16,13 +28,27 @@ export declare class BufferManager {
16
28
  private readonly TypedArrayCtor;
17
29
  private readonly bytesPerPixel;
18
30
  private readonly dtype;
31
+ /**
32
+ * Number of scalar components per spatial voxel.
33
+ * 1 for scalar images, 3 for RGB, 4 for RGBA.
34
+ */
35
+ readonly componentsPerVoxel: number;
36
+ /**
37
+ * Whether this buffer stores normalized uint8 data for a non-uint8
38
+ * RGB/RGBA source. When `true`, {@link getTypedArray} returns a
39
+ * `Uint8Array` and `bytesPerPixel` is 1, regardless of the source
40
+ * dtype.
41
+ */
42
+ readonly isNormalizedRGB: boolean;
19
43
  /**
20
44
  * Create a new BufferManager.
21
45
  *
22
46
  * @param maxPixels - Maximum number of pixels allowed (budget)
23
47
  * @param dtype - Data type for the buffer
48
+ * @param componentsPerVoxel - Number of components per spatial voxel
49
+ * (default: 1; pass 3 for RGB, 4 for RGBA)
24
50
  */
25
- constructor(maxPixels: number, dtype: ZarrDtype);
51
+ constructor(maxPixels: number, dtype: ZarrDtype, componentsPerVoxel?: number);
26
52
  /**
27
53
  * Resize the buffer to fit the given dimensions.
28
54
  *
@@ -44,7 +70,8 @@ export declare class BufferManager {
44
70
  /**
45
71
  * Get a typed array view over the current buffer region.
46
72
  *
47
- * The view is sized to match currentDimensions, not the full buffer capacity.
73
+ * The view is sized to match `spatialPixels × componentsPerVoxel`,
74
+ * not the full buffer capacity.
48
75
  */
49
76
  getTypedArray(): TypedArray;
50
77
  /**
@@ -52,11 +79,17 @@ export declare class BufferManager {
52
79
  */
53
80
  getDimensions(): [number, number, number];
54
81
  /**
55
- * Get the total number of pixels in the current buffer region.
82
+ * Get the total number of spatial pixels in the current buffer region.
83
+ * This does NOT include the component multiplier.
56
84
  */
57
85
  getPixelCount(): number;
58
86
  /**
59
- * Get the buffer capacity in pixels.
87
+ * Get the total number of scalar elements in the current buffer region.
88
+ * For multi-component images, this is `spatialPixels × componentsPerVoxel`.
89
+ */
90
+ getElementCount(): number;
91
+ /**
92
+ * Get the buffer capacity in scalar elements.
60
93
  */
61
94
  getCapacity(): number;
62
95
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"BufferManager.d.ts","sourceRoot":"","sources":["../src/BufferManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,SAAS,EAAE,MAAM,YAAY,CAAA;AAG9E;;;;;;;;;GASG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IAEjC;;;;;OAKG;gBACS,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS;IAW/C;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,UAAU;IA6BxD;;OAEG;IACH,SAAS,IAAI,WAAW;IAIxB;;;;OAIG;IACH,aAAa,IAAI,UAAU;IAQ3B;;OAEG;IACH,aAAa,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAIzC;;OAEG;IACH,aAAa,IAAI,MAAM;IAQvB;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,QAAQ,IAAI,SAAS;IAIrB;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;;;;OAKG;IACH,cAAc,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;CAK9D"}
1
+ {"version":3,"file":"BufferManager.d.ts","sourceRoot":"","sources":["../src/BufferManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,SAAS,EAAE,MAAM,YAAY,CAAA;AAG9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IAEjC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IAEnC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IAEjC;;;;;;;OAOG;gBAED,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,EAChB,kBAAkB,GAAE,MAAU;IAuBhC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,UAAU;IAgCxD;;OAEG;IACH,SAAS,IAAI,WAAW;IAIxB;;;;;OAKG;IACH,aAAa,IAAI,UAAU;IAY3B;;OAEG;IACH,aAAa,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAIzC;;;OAGG;IACH,aAAa,IAAI,MAAM;IAQvB;;;OAGG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,QAAQ,IAAI,SAAS;IAIrB;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,KAAK,IAAI,IAAI;IAYb;;;;;OAKG;IACH,cAAc,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;CAM9D"}
@@ -7,6 +7,18 @@ import { getBytesPerPixel, getTypedArrayConstructor } from "./types.js";
7
7
  * The buffer is resized to match the fetched data dimensions exactly.
8
8
  * Memory is reused when possible to avoid unnecessary allocations.
9
9
  *
10
+ * For multi-component images (RGB/RGBA), `componentsPerVoxel` controls
11
+ * how many scalar elements each spatial voxel occupies. The buffer is
12
+ * sized to hold `spatialPixels * componentsPerVoxel` elements, and the
13
+ * typed array view spans all of them. Spatial dimensions (`[z, y, x]`)
14
+ * track only the spatial extent; the component count is a fixed
15
+ * multiplier on the element count.
16
+ *
17
+ * When the source dtype is not uint8 and `componentsPerVoxel > 1`
18
+ * (non-uint8 RGB/RGBA), the buffer stores **uint8** data because NiiVue
19
+ * only supports `DT_RGB24` / `DT_RGBA32` (uint8-per-channel). The raw
20
+ * data is normalized to uint8 externally before being written here.
21
+ *
10
22
  * Memory reuse strategy:
11
23
  * - Reuse buffer if newSize <= currentCapacity
12
24
  * - Reallocate if newSize > currentCapacity OR newSize < 25% of currentCapacity
@@ -18,17 +30,41 @@ export class BufferManager {
18
30
  TypedArrayCtor;
19
31
  bytesPerPixel;
20
32
  dtype;
33
+ /**
34
+ * Number of scalar components per spatial voxel.
35
+ * 1 for scalar images, 3 for RGB, 4 for RGBA.
36
+ */
37
+ componentsPerVoxel;
38
+ /**
39
+ * Whether this buffer stores normalized uint8 data for a non-uint8
40
+ * RGB/RGBA source. When `true`, {@link getTypedArray} returns a
41
+ * `Uint8Array` and `bytesPerPixel` is 1, regardless of the source
42
+ * dtype.
43
+ */
44
+ isNormalizedRGB;
21
45
  /**
22
46
  * Create a new BufferManager.
23
47
  *
24
48
  * @param maxPixels - Maximum number of pixels allowed (budget)
25
49
  * @param dtype - Data type for the buffer
50
+ * @param componentsPerVoxel - Number of components per spatial voxel
51
+ * (default: 1; pass 3 for RGB, 4 for RGBA)
26
52
  */
27
- constructor(maxPixels, dtype) {
53
+ constructor(maxPixels, dtype, componentsPerVoxel = 1) {
28
54
  this.maxPixels = maxPixels;
29
55
  this.dtype = dtype;
30
- this.TypedArrayCtor = getTypedArrayConstructor(dtype);
31
- this.bytesPerPixel = getBytesPerPixel(dtype);
56
+ this.componentsPerVoxel = componentsPerVoxel;
57
+ // Non-uint8 RGB/RGBA: the output buffer stores normalized uint8 data
58
+ // because NiiVue only supports uint8-per-channel color rendering.
59
+ this.isNormalizedRGB = componentsPerVoxel > 1 && dtype !== "uint8";
60
+ if (this.isNormalizedRGB) {
61
+ this.TypedArrayCtor = Uint8Array;
62
+ this.bytesPerPixel = 1;
63
+ }
64
+ else {
65
+ this.TypedArrayCtor = getTypedArrayConstructor(dtype);
66
+ this.bytesPerPixel = getBytesPerPixel(dtype);
67
+ }
32
68
  // Initialize with empty buffer - will be allocated on first resize
33
69
  this.currentDimensions = [0, 0, 0];
34
70
  this.buffer = new ArrayBuffer(0);
@@ -47,17 +83,20 @@ export class BufferManager {
47
83
  * @returns TypedArray view over the (possibly new) buffer
48
84
  */
49
85
  resize(dimensions) {
50
- const requiredPixels = dimensions[0] * dimensions[1] * dimensions[2];
51
- if (requiredPixels > this.maxPixels) {
52
- console.warn(`[fidnii] BufferManager: Requested dimensions [${dimensions.join(", ")}] = ${requiredPixels} pixels exceeds maxPixels (${this.maxPixels}). ` +
86
+ const spatialPixels = dimensions[0] * dimensions[1] * dimensions[2];
87
+ if (spatialPixels > this.maxPixels) {
88
+ console.warn(`[fidnii] BufferManager: Requested dimensions [${dimensions.join(", ")}] = ${spatialPixels} pixels exceeds maxPixels (${this.maxPixels}). ` +
53
89
  `Proceeding anyway (likely at lowest resolution).`);
54
90
  }
55
- const currentCapacityPixels = this.buffer.byteLength / this.bytesPerPixel;
56
- const utilizationRatio = currentCapacityPixels > 0 ? requiredPixels / currentCapacityPixels : 0;
57
- const needsReallocation = requiredPixels > currentCapacityPixels || utilizationRatio < 0.25;
91
+ // Total elements = spatial pixels × components per voxel
92
+ const requiredElements = spatialPixels * this.componentsPerVoxel;
93
+ const currentCapacityElements = this.buffer.byteLength / this.bytesPerPixel;
94
+ const utilizationRatio = currentCapacityElements > 0
95
+ ? requiredElements / currentCapacityElements
96
+ : 0;
97
+ const needsReallocation = requiredElements > currentCapacityElements || utilizationRatio < 0.25;
58
98
  if (needsReallocation) {
59
- // Allocate new buffer
60
- const newByteLength = requiredPixels * this.bytesPerPixel;
99
+ const newByteLength = requiredElements * this.bytesPerPixel;
61
100
  this.buffer = new ArrayBuffer(newByteLength);
62
101
  }
63
102
  this.currentDimensions = [...dimensions];
@@ -72,13 +111,14 @@ export class BufferManager {
72
111
  /**
73
112
  * Get a typed array view over the current buffer region.
74
113
  *
75
- * The view is sized to match currentDimensions, not the full buffer capacity.
114
+ * The view is sized to match `spatialPixels × componentsPerVoxel`,
115
+ * not the full buffer capacity.
76
116
  */
77
117
  getTypedArray() {
78
- const pixelCount = this.currentDimensions[0] *
118
+ const spatialPixels = this.currentDimensions[0] *
79
119
  this.currentDimensions[1] *
80
120
  this.currentDimensions[2];
81
- return new this.TypedArrayCtor(this.buffer, 0, pixelCount);
121
+ return new this.TypedArrayCtor(this.buffer, 0, spatialPixels * this.componentsPerVoxel);
82
122
  }
83
123
  /**
84
124
  * Get the current buffer dimensions [z, y, x].
@@ -87,7 +127,8 @@ export class BufferManager {
87
127
  return [...this.currentDimensions];
88
128
  }
89
129
  /**
90
- * Get the total number of pixels in the current buffer region.
130
+ * Get the total number of spatial pixels in the current buffer region.
131
+ * This does NOT include the component multiplier.
91
132
  */
92
133
  getPixelCount() {
93
134
  return (this.currentDimensions[0] *
@@ -95,7 +136,14 @@ export class BufferManager {
95
136
  this.currentDimensions[2]);
96
137
  }
97
138
  /**
98
- * Get the buffer capacity in pixels.
139
+ * Get the total number of scalar elements in the current buffer region.
140
+ * For multi-component images, this is `spatialPixels × componentsPerVoxel`.
141
+ */
142
+ getElementCount() {
143
+ return this.getPixelCount() * this.componentsPerVoxel;
144
+ }
145
+ /**
146
+ * Get the buffer capacity in scalar elements.
99
147
  */
100
148
  getCapacity() {
101
149
  return this.buffer.byteLength / this.bytesPerPixel;
@@ -122,9 +170,9 @@ export class BufferManager {
122
170
  * Clear the current buffer region to zeros.
123
171
  */
124
172
  clear() {
125
- const pixelCount = this.getPixelCount();
126
- if (pixelCount > 0) {
127
- const view = new Uint8Array(this.buffer, 0, pixelCount * this.bytesPerPixel);
173
+ const elementCount = this.getElementCount();
174
+ if (elementCount > 0) {
175
+ const view = new Uint8Array(this.buffer, 0, elementCount * this.bytesPerPixel);
128
176
  view.fill(0);
129
177
  }
130
178
  }
@@ -135,9 +183,9 @@ export class BufferManager {
135
183
  * @returns True if current buffer can fit the dimensions
136
184
  */
137
185
  canAccommodate(dimensions) {
138
- const requiredPixels = dimensions[0] * dimensions[1] * dimensions[2];
139
- const currentCapacityPixels = this.buffer.byteLength / this.bytesPerPixel;
140
- return requiredPixels <= currentCapacityPixels;
186
+ const requiredElements = dimensions[0] * dimensions[1] * dimensions[2] * this.componentsPerVoxel;
187
+ const currentCapacityElements = this.buffer.byteLength / this.bytesPerPixel;
188
+ return requiredElements <= currentCapacityElements;
141
189
  }
142
190
  }
143
191
  //# sourceMappingURL=BufferManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"BufferManager.js","sourceRoot":"","sources":["../src/BufferManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BufferManager.js","sourceRoot":"","sources":["../src/BufferManager.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,21 @@ export declare class OMEZarrNVImage extends NVImage {
44
44
  private isLoading;
45
45
  /** Data type of the volume */
46
46
  private readonly dtype;
47
+ /**
48
+ * Channel dimension info, or `null` for scalar (single-component) images.
49
+ * When non-null, the image has a `"c"` dimension and is treated as
50
+ * multi-component (RGB/RGBA) data.
51
+ */
52
+ private readonly _channelInfo;
53
+ /**
54
+ * Whether the image is 2D (no `"z"` dimension).
55
+ */
56
+ private readonly _is2D;
57
+ /**
58
+ * Whether to negate the y-scale in the NIfTI affine for 2D images
59
+ * so that NiiVue renders them right-side up.
60
+ */
61
+ private readonly _flipY2D;
47
62
  /** Full volume bounds in world space */
48
63
  private readonly _volumeBounds;
49
64
  /** Current buffer bounds in world space (may differ from full volume when clipped) */
@@ -210,6 +225,17 @@ export declare class OMEZarrNVImage extends NVImage {
210
225
  * @param levelIndex - The resolution level index
211
226
  */
212
227
  private ensureOmeroMetadata;
228
+ /**
229
+ * Get per-channel normalization windows for non-uint8 RGB/RGBA.
230
+ *
231
+ * Uses OMERO `window.start`/`window.end` (or `window.min`/`window.max`)
232
+ * when available. Falls back to computing min/max from the raw data.
233
+ *
234
+ * @param data - Raw multi-component data from the zarr fetch
235
+ * @param components - Number of components per voxel (3 or 4)
236
+ * @returns Per-channel windows for normalization to uint8
237
+ */
238
+ private _getChannelWindows;
213
239
  /**
214
240
  * Handle clip plane change from NiiVue.
215
241
  * This is called when the user interacts with clip planes in NiiVue.
@@ -1 +1 @@
1
- {"version":3,"file":"OMEZarrNVImage.d.ts","sourceRoot":"","sources":["../src/OMEZarrNVImage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OMEZarrNVImage.d.ts","sourceRoot":"","sources":["../src/OMEZarrNVImage.ts"],"names":[],"mappings":"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"}
@@ -8,9 +8,10 @@ import { NIFTI1 } from "nifti-reader-js";
8
8
  import { BufferManager } from "./BufferManager.js";
9
9
  import { alignToChunks, clipPlanesToNiivue, clipPlanesToPixelRegion, createDefaultClipPlanes, MAX_CLIP_PLANES, normalizeVector, validateClipPlanes, } from "./ClipPlanes.js";
10
10
  import { OMEZarrNVImageEvent, } from "./events.js";
11
+ import { computeChannelMinMax, normalizeToUint8 } from "./normalize.js";
11
12
  import { RegionCoalescer } from "./RegionCoalescer.js";
12
13
  import { getChunkShape, getVolumeShape, select2DResolution, selectResolution, } from "./ResolutionSelector.js";
13
- import { getBytesPerPixel, getNiftiDataType, parseZarritaDtype, } from "./types.js";
14
+ import { getBytesPerPixel, getChannelInfo, getNiftiDataType, getRGBNiftiDataType, isRGBImage, NiftiDataType, needsRGBNormalization, parseZarritaDtype, } from "./types.js";
14
15
  import { affineToNiftiSrows, calculateWorldBounds, createAffineFromNgffImage, } from "./utils/affine.js";
15
16
  import { worldToPixel } from "./utils/coordinates.js";
16
17
  import { boundsApproxEqual, computeViewportBounds2D, computeViewportBounds3D, } from "./ViewportBounds.js";
@@ -57,6 +58,21 @@ export class OMEZarrNVImage extends NVImage {
57
58
  isLoading = false;
58
59
  /** Data type of the volume */
59
60
  dtype;
61
+ /**
62
+ * Channel dimension info, or `null` for scalar (single-component) images.
63
+ * When non-null, the image has a `"c"` dimension and is treated as
64
+ * multi-component (RGB/RGBA) data.
65
+ */
66
+ _channelInfo;
67
+ /**
68
+ * Whether the image is 2D (no `"z"` dimension).
69
+ */
70
+ _is2D;
71
+ /**
72
+ * Whether to negate the y-scale in the NIfTI affine for 2D images
73
+ * so that NiiVue renders them right-side up.
74
+ */
75
+ _flipY2D;
60
76
  /** Full volume bounds in world space */
61
77
  _volumeBounds;
62
78
  /** Current buffer bounds in world space (may differ from full volume when clipped) */
@@ -152,6 +168,18 @@ export class OMEZarrNVImage extends NVImage {
152
168
  // Get data type from highest resolution image
153
169
  const highResImage = this.multiscales.images[0];
154
170
  this.dtype = parseZarritaDtype(highResImage.data.dtype);
171
+ // Detect channel (component) dimension for multi-component images
172
+ this._channelInfo = getChannelInfo(highResImage);
173
+ // Validate multi-component images: only RGB (3) / RGBA (4) are supported
174
+ if (this._channelInfo && !isRGBImage(highResImage)) {
175
+ throw new Error(`Unsupported multi-component image: found ${this._channelInfo.components} ` +
176
+ `components with dtype '${this.dtype}'. Only RGB (3 components) ` +
177
+ `and RGBA (4 components) images are supported. For other ` +
178
+ `multi-component images, select a single component before loading.`);
179
+ }
180
+ // Detect 2D images (no z axis) and store y-flip preference
181
+ this._is2D = highResImage.dims.indexOf("z") === -1;
182
+ this._flipY2D = options.flipY2D ?? true;
155
183
  // Calculate volume bounds from highest resolution for most accurate bounds
156
184
  const highResAffine = createAffineFromNgffImage(highResImage);
157
185
  const highResShape = getVolumeShape(highResImage);
@@ -162,8 +190,11 @@ export class OMEZarrNVImage extends NVImage {
162
190
  const selection = selectResolution(this.multiscales, this.maxPixels, this._clipPlanes, this._volumeBounds);
163
191
  this.targetLevelIndex = selection.levelIndex;
164
192
  this.currentLevelIndex = this.multiscales.images.length - 1;
165
- // Create buffer manager (dynamic sizing, no pre-allocation)
166
- this.bufferManager = new BufferManager(this.maxPixels, this.dtype);
193
+ // Create buffer manager (dynamic sizing, no pre-allocation).
194
+ // For multi-component images, each spatial voxel has multiple
195
+ // scalar elements (e.g. 3 for RGB, 4 for RGBA).
196
+ const componentsPerVoxel = this._channelInfo?.components ?? 1;
197
+ this.bufferManager = new BufferManager(this.maxPixels, this.dtype, componentsPerVoxel);
167
198
  // Initialize NVImage properties with placeholder values
168
199
  // Actual values will be set when data is first loaded
169
200
  this.initializeNVImageProperties();
@@ -213,15 +244,23 @@ export class OMEZarrNVImage extends NVImage {
213
244
  this.hdr = hdr;
214
245
  // Placeholder dimensions (will be updated when data loads)
215
246
  hdr.dims = [3, 1, 1, 1, 1, 1, 1, 1];
216
- // Set data type
217
- hdr.datatypeCode = getNiftiDataType(this.dtype);
218
- hdr.numBitsPerVoxel = getBytesPerPixel(this.dtype) * 8;
247
+ // Set data type — use RGB24/RGBA32 for multi-component images
248
+ // (any dtype; non-uint8 data is normalized to uint8 at load time)
249
+ if (this._channelInfo && isRGBImage(this.multiscales.images[0])) {
250
+ const rgbCode = getRGBNiftiDataType(this._channelInfo);
251
+ hdr.datatypeCode = rgbCode;
252
+ hdr.numBitsPerVoxel = rgbCode === NiftiDataType.RGB24 ? 24 : 32;
253
+ }
254
+ else {
255
+ hdr.datatypeCode = getNiftiDataType(this.dtype);
256
+ hdr.numBitsPerVoxel = getBytesPerPixel(this.dtype) * 8;
257
+ }
219
258
  // Placeholder pixel dimensions
220
259
  hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0];
221
- // Placeholder affine (identity)
260
+ // Placeholder affine (identity, with y-flip for 2D images)
222
261
  hdr.affine = [
223
262
  [1, 0, 0, 0],
224
- [0, 1, 0, 0],
263
+ [0, this._flipY2D && this._is2D ? -1 : 1, 0, 0],
225
264
  [0, 0, 1, 0],
226
265
  [0, 0, 0, 1],
227
266
  ];
@@ -367,8 +406,22 @@ export class OMEZarrNVImage extends NVImage {
367
406
  const result = await this.coalescer.fetchRegion(ngffImage, levelIndex, fetchRegion, requesterId);
368
407
  // Resize buffer to match fetched data exactly (no upsampling!)
369
408
  const targetData = this.bufferManager.resize(fetchedShape);
370
- // Direct copy of fetched data
371
- targetData.set(result.data);
409
+ // For non-uint8 RGB/RGBA, we need OMERO metadata *before* copying
410
+ // so we can normalize the raw data to uint8 using channel windows.
411
+ const normalize = needsRGBNormalization(ngffImage, this.dtype);
412
+ if (normalize && !this.isLabelImage) {
413
+ await this.ensureOmeroMetadata(ngffImage, levelIndex);
414
+ }
415
+ if (normalize && this._channelInfo) {
416
+ // Non-uint8 RGB/RGBA: normalize raw data to uint8 using OMERO windows
417
+ const windows = this._getChannelWindows(result.data, this._channelInfo.components);
418
+ const normalized = normalizeToUint8(result.data, this._channelInfo.components, windows);
419
+ targetData.set(normalized);
420
+ }
421
+ else {
422
+ // uint8 RGB or scalar: direct copy
423
+ targetData.set(result.data);
424
+ }
372
425
  // Update this.img to point to the (possibly new) buffer
373
426
  this.img = this.bufferManager.getTypedArray();
374
427
  // Update NVImage header with correct dimensions and transforms
@@ -377,8 +430,9 @@ export class OMEZarrNVImage extends NVImage {
377
430
  // Label images: apply a discrete colormap instead of OMERO windowing
378
431
  this._applyLabelColormap(this, result.data);
379
432
  }
380
- else {
381
- // Compute or apply OMERO metadata for cal_min/cal_max
433
+ else if (!normalize) {
434
+ // Scalar / uint8 RGB: compute or apply OMERO for cal_min/cal_max.
435
+ // (Normalized RGB already consumed the OMERO window above.)
382
436
  await this.ensureOmeroMetadata(ngffImage, levelIndex);
383
437
  }
384
438
  // Reset global_min so NiiVue's refreshLayers() re-runs calMinMax() on real data.
@@ -447,11 +501,8 @@ export class OMEZarrNVImage extends NVImage {
447
501
  affine[12] += regionStart[2] * sx; // x offset
448
502
  affine[13] += regionStart[1] * sy; // y offset
449
503
  affine[14] += regionStart[0] * sz; // z offset
450
- // Update affine in header
451
- const srows = affineToNiftiSrows(affine);
452
- this.hdr.affine = [srows.srow_x, srows.srow_y, srows.srow_z, [0, 0, 0, 1]];
453
- // Update current buffer bounds
454
- // Buffer starts at region.chunkAlignedStart and has extent fetchedShape
504
+ // Update current buffer bounds from the un-flipped affine
505
+ // (bounds stay in OME-Zarr world space for clip plane math)
455
506
  this._currentBufferBounds = {
456
507
  min: [
457
508
  affine[12], // x offset (world coord of buffer origin)
@@ -464,6 +515,15 @@ export class OMEZarrNVImage extends NVImage {
464
515
  affine[14] + fetchedShape[0] * sz,
465
516
  ],
466
517
  };
518
+ // For 2D images, negate y-scale so NiiVue's calculateRAS() flips
519
+ // the rows to account for top-to-bottom pixel storage order.
520
+ if (this._flipY2D && this._is2D) {
521
+ affine[5] = -sy;
522
+ affine[13] += (fetchedShape[1] - 1) * sy;
523
+ }
524
+ // Update affine in header
525
+ const srows = affineToNiftiSrows(affine);
526
+ this.hdr.affine = [srows.srow_x, srows.srow_y, srows.srow_z, [0, 0, 0, 1]];
467
527
  // Recalculate RAS orientation
468
528
  this.calculateRAS();
469
529
  }
@@ -581,13 +641,49 @@ export class OMEZarrNVImage extends NVImage {
581
641
  const needsCompute = isPreviewLevel ||
582
642
  (isTargetLevel && this._omeroComputedForLevel !== this.targetLevelIndex);
583
643
  if (needsCompute) {
584
- const computedOmero = await computeOmeroFromNgffImage(ngffImage);
644
+ // Pass the chunk cache so decoded chunks from OMERO statistics
645
+ // computation are reused by subsequent zarrGet() calls.
646
+ const omeroOpts = this._chunkCache
647
+ ? { cache: this._chunkCache }
648
+ : undefined;
649
+ const computedOmero = await computeOmeroFromNgffImage(ngffImage, omeroOpts);
585
650
  this._omero = computedOmero;
586
651
  this._omeroComputedForLevel = levelIndex;
587
652
  this.applyOmeroToHeader();
588
653
  }
589
654
  }
590
655
  }
656
+ /**
657
+ * Get per-channel normalization windows for non-uint8 RGB/RGBA.
658
+ *
659
+ * Uses OMERO `window.start`/`window.end` (or `window.min`/`window.max`)
660
+ * when available. Falls back to computing min/max from the raw data.
661
+ *
662
+ * @param data - Raw multi-component data from the zarr fetch
663
+ * @param components - Number of components per voxel (3 or 4)
664
+ * @returns Per-channel windows for normalization to uint8
665
+ */
666
+ _getChannelWindows(data, components) {
667
+ if (this._omero?.channels?.length) {
668
+ const windows = [];
669
+ for (let c = 0; c < components; c++) {
670
+ const channel = this._omero.channels[Math.min(c, this._omero.channels.length - 1)];
671
+ const win = channel?.window;
672
+ if (win) {
673
+ windows.push({
674
+ start: win.start ?? win.min ?? 0,
675
+ end: win.end ?? win.max ?? 1,
676
+ });
677
+ }
678
+ else {
679
+ windows.push({ start: 0, end: 1 });
680
+ }
681
+ }
682
+ return windows;
683
+ }
684
+ // No OMERO metadata: fall back to per-channel min/max from data
685
+ return computeChannelMinMax(data, components);
686
+ }
591
687
  /**
592
688
  * Handle clip plane change from NiiVue.
593
689
  * This is called when the user interacts with clip planes in NiiVue.
@@ -1438,14 +1534,22 @@ export class OMEZarrNVImage extends NVImage {
1438
1534
  * Create a new slab buffer state for a slice type.
1439
1535
  */
1440
1536
  _createSlabBuffer(sliceType) {
1441
- const bufferManager = new BufferManager(this.maxPixels, this.dtype);
1537
+ const componentsPerVoxel = this._channelInfo?.components ?? 1;
1538
+ const bufferManager = new BufferManager(this.maxPixels, this.dtype, componentsPerVoxel);
1442
1539
  const nvImage = new NVImage();
1443
1540
  // Initialize with placeholder NIfTI header (same as main image setup)
1444
1541
  const hdr = new NIFTI1();
1445
1542
  nvImage.hdr = hdr;
1446
1543
  hdr.dims = [3, 1, 1, 1, 1, 1, 1, 1];
1447
- hdr.datatypeCode = getNiftiDataType(this.dtype);
1448
- hdr.numBitsPerVoxel = getBytesPerPixel(this.dtype) * 8;
1544
+ if (this._channelInfo && isRGBImage(this.multiscales.images[0])) {
1545
+ const rgbCode = getRGBNiftiDataType(this._channelInfo);
1546
+ hdr.datatypeCode = rgbCode;
1547
+ hdr.numBitsPerVoxel = rgbCode === NiftiDataType.RGB24 ? 24 : 32;
1548
+ }
1549
+ else {
1550
+ hdr.datatypeCode = getNiftiDataType(this.dtype);
1551
+ hdr.numBitsPerVoxel = getBytesPerPixel(this.dtype) * 8;
1552
+ }
1449
1553
  hdr.pixDims = [1, 1, 1, 1, 0, 0, 0, 0];
1450
1554
  hdr.affine = [
1451
1555
  [1, 0, 0, 0],
@@ -1657,7 +1761,17 @@ export class OMEZarrNVImage extends NVImage {
1657
1761
  const result = await this.coalescer.fetchRegion(ngffImage, levelIndex, fetchRegion, `slab-${SLICE_TYPE[sliceType]}-${levelIndex}`);
1658
1762
  // Resize buffer and copy data
1659
1763
  const targetData = slabState.bufferManager.resize(fetchedShape);
1660
- targetData.set(result.data);
1764
+ const normalize = needsRGBNormalization(ngffImage, this.dtype);
1765
+ if (normalize && this._channelInfo) {
1766
+ // Non-uint8 RGB/RGBA: normalize raw data to uint8 using OMERO windows
1767
+ const windows = this._getChannelWindows(result.data, this._channelInfo.components);
1768
+ const normalized = normalizeToUint8(result.data, this._channelInfo.components, windows);
1769
+ targetData.set(normalized);
1770
+ }
1771
+ else {
1772
+ // uint8 RGB or scalar: direct copy
1773
+ targetData.set(result.data);
1774
+ }
1661
1775
  slabState.nvImage.img =
1662
1776
  slabState.bufferManager.getTypedArray();
1663
1777
  // Update slab position tracking
@@ -1669,8 +1783,9 @@ export class OMEZarrNVImage extends NVImage {
1669
1783
  // Label images: apply discrete colormap to the slab NVImage
1670
1784
  this._applyLabelColormap(slabState.nvImage, result.data);
1671
1785
  }
1672
- else if (this._omero) {
1673
- // Apply OMERO metadata if available
1786
+ else if (this._omero && !normalize) {
1787
+ // Apply OMERO metadata for scalar / uint8 RGB.
1788
+ // Normalized RGB already consumed the OMERO window during normalization.
1674
1789
  this._applyOmeroToSlabHeader(slabState.nvImage);
1675
1790
  }
1676
1791
  // Reset global_min so NiiVue recalculates intensity ranges
@@ -1752,6 +1867,11 @@ export class OMEZarrNVImage extends NVImage {
1752
1867
  affine[12] += fetchStart[2] * sx; // x offset
1753
1868
  affine[13] += fetchStart[1] * sy; // y offset
1754
1869
  affine[14] += fetchStart[0] * sz; // z offset
1870
+ // For 2D images, negate y-scale before normalization
1871
+ if (this._flipY2D && this._is2D) {
1872
+ affine[5] = -sy;
1873
+ affine[13] += (fetchedShape[1] - 1) * sy;
1874
+ }
1755
1875
  // Apply normalization to the entire affine (scale columns + translation)
1756
1876
  for (let i = 0; i < 15; i++) {
1757
1877
  affine[i] *= normalizationScale;