@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.
- package/dist/BufferManager.d.ts +37 -4
- package/dist/BufferManager.d.ts.map +1 -1
- package/dist/BufferManager.js +70 -22
- package/dist/BufferManager.js.map +1 -1
- package/dist/OMEZarrNVImage.d.ts +26 -0
- package/dist/OMEZarrNVImage.d.ts.map +1 -1
- package/dist/OMEZarrNVImage.js +144 -24
- package/dist/OMEZarrNVImage.js.map +1 -1
- package/dist/RegionCoalescer.d.ts +16 -0
- package/dist/RegionCoalescer.d.ts.map +1 -1
- package/dist/RegionCoalescer.js +42 -5
- package/dist/RegionCoalescer.js.map +1 -1
- package/dist/ResolutionSelector.d.ts +14 -2
- package/dist/ResolutionSelector.d.ts.map +1 -1
- package/dist/ResolutionSelector.js +26 -16
- package/dist/ResolutionSelector.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/normalize.d.ts +50 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +95 -0
- package/dist/normalize.js.map +1 -0
- package/dist/types.d.ts +66 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +66 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/coordinates.d.ts.map +1 -1
- package/dist/utils/coordinates.js +20 -26
- package/dist/utils/coordinates.js.map +1 -1
- package/package.json +3 -4
- package/src/BufferManager.ts +83 -22
- package/src/OMEZarrNVImage.ts +190 -24
- package/src/RegionCoalescer.ts +45 -5
- package/src/ResolutionSelector.ts +32 -16
- package/src/index.ts +13 -2
- package/src/normalize.ts +119 -0
- package/src/types.ts +95 -1
- package/src/utils/coordinates.ts +26 -24
package/dist/BufferManager.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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"}
|
package/dist/BufferManager.js
CHANGED
|
@@ -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.
|
|
31
|
-
|
|
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
|
|
51
|
-
if (
|
|
52
|
-
console.warn(`[fidnii] BufferManager: Requested dimensions [${dimensions.join(", ")}] = ${
|
|
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
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
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
|
-
|
|
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
|
|
114
|
+
* The view is sized to match `spatialPixels × componentsPerVoxel`,
|
|
115
|
+
* not the full buffer capacity.
|
|
76
116
|
*/
|
|
77
117
|
getTypedArray() {
|
|
78
|
-
const
|
|
118
|
+
const spatialPixels = this.currentDimensions[0] *
|
|
79
119
|
this.currentDimensions[1] *
|
|
80
120
|
this.currentDimensions[2];
|
|
81
|
-
return new this.TypedArrayCtor(this.buffer, 0,
|
|
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
|
|
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
|
|
126
|
-
if (
|
|
127
|
-
const view = new Uint8Array(this.buffer, 0,
|
|
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
|
|
139
|
-
const
|
|
140
|
-
return
|
|
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":"AAAA,wDAAwD;AACxD,+BAA+B;AAG/B,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAEvE
|
|
1
|
+
{"version":3,"file":"BufferManager.js","sourceRoot":"","sources":["../src/BufferManager.ts"],"names":[],"mappings":"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"}
|
package/dist/OMEZarrNVImage.d.ts
CHANGED
|
@@ -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":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAa,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAM3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAc,MAAM,gBAAgB,CAAA;AAcpD,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"OMEZarrNVImage.d.ts","sourceRoot":"","sources":["../src/OMEZarrNVImage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAa,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAM3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAc,MAAM,gBAAgB,CAAA;AAcpD,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,kCAAkC,EACvC,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAWpB,OAAO,KAAK,EAKV,SAAS,EACT,UAAU,EACV,qBAAqB,EAErB,eAAe,EACf,aAAa,EAEb,YAAY,EAEb,MAAM,YAAY,CAAA;AA0BnB;;;;;;;;;GASG;AACH,qBAAa,cAAe,SAAQ,OAAO;IACzC,oCAAoC;IACpC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IAEjC,sCAAsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAE1B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAE9B,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B,sDAAsD;IACtD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAE7C,oDAAoD;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAE3C,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IAEpD,yCAAyC;IACzC,OAAO,CAAC,WAAW,CAAY;IAE/B,yDAAyD;IACzD,OAAO,CAAC,gBAAgB,CAAQ;IAEhC,gEAAgE;IAChE,OAAO,CAAC,iBAAiB,CAAQ;IAEjC,qCAAqC;IACrC,OAAO,CAAC,SAAS,CAAiB;IAElC,8BAA8B;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IAEjC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IAEjD;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,wCAAwC;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;IAE5C,sFAAsF;IACtF,OAAO,CAAC,oBAAoB,CAAc;IAE1C,4DAA4D;IAC5D,OAAO,CAAC,yBAAyB,CAAC,CAA+B;IAEjE,iDAAiD;IACjD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAE5C,sDAAsD;IACtD,OAAO,CAAC,uBAAuB,CAA6C;IAE5E,0DAA0D;IAC1D,OAAO,CAAC,mBAAmB,CAAiB;IAE5C,4EAA4E;IAC5E,OAAO,CAAC,mBAAmB,CAAY;IAEvC,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAmB;IAEjC,mEAAmE;IACnE,OAAO,CAAC,cAAc,CAAY;IAElC,iFAAiF;IACjF,OAAO,CAAC,sBAAsB,CAAa;IAE3C,uEAAuE;IACvE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoB;IAEjD,6EAA6E;IAC7E,OAAO,CAAC,uBAAuB,CAGhB;IAEf,gFAAgF;IAChF,OAAO,CAAC,uBAAuB,CAA6B;IAM5D,gDAAgD;IAChD,OAAO,CAAC,gBAAgB,CAA8C;IAEtE,mDAAmD;IACnD,OAAO,CAAC,YAAY,CAAiD;IAErE,sDAAsD;IACtD,OAAO,CAAC,mBAAmB,CAGd;IAMb,6DAA6D;IAC7D,OAAO,CAAC,qBAAqB,CAAiB;IAE9C;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAA4B;IAErD;;;OAGG;IACH,OAAO,CAAC,sBAAsB,CACnB;IAEX,mDAAmD;IACnD,OAAO,CAAC,sBAAsB,CAA6C;IAE3E,qEAAqE;IACrE,OAAO,CAAC,qBAAqB,CAAiD;IAM9E,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IAEnC,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IAEnC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAM;IAElD;;OAEG;IACH,OAAO;IAgFP;;;;;;;;;;;;OAYG;WACU,MAAM,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;IA2B5E;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IA8CnC;;;;;;;;;;;;;OAaG;IACG,cAAc,CAClB,WAAW,GAAE,OAAe,EAC5B,OAAO,GAAE,eAA2B,GACnC,OAAO,CAAC,IAAI,CAAC;IAwEhB;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAiBpC;;;;;;;;;;;OAWG;YACW,mBAAmB;IAgHjC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,qBAAqB;IAsE7B;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAgB9B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;;;;;;;;;OAYG;YACW,mBAAmB;IAwCjC;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IA0B1B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IA6BvC;;;;;;OAMG;IACH,OAAO,CAAC,8BAA8B;IA2DtC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAQlC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;;;OAIG;IACH,aAAa,IAAI,UAAU;IAO3B;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAkBpC;;;;;OAKG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAWpC;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;OAEG;IACH,oBAAoB,IAAI,MAAM;IAI9B;;OAEG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;;;;;;;;OASG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;OAEG;IACH,eAAe,IAAI,YAAY;IAW/B;;;;;;;;OAQG;IACH,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAuCxC;;OAEG;IACH,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED;;;OAGG;IACH,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAQxC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiC3B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAwB7B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,iBAAiB;IA2DzB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IAarC;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAkGhC;;OAEG;IACH,OAAO,CAAC,eAAe;IAmCvB;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAQlC;;;;;;;;;;;;;OAaG;IACH,QAAQ,IAAI,KAAK,GAAG,SAAS;IAI7B;;;;;;;OAOG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAmBrC;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAoD9B;;;;OAIG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAkB9B;;;;;;OAMG;IACH,kBAAkB,CAAC,SAAS,EAAE,aAAa,GAAG,eAAe,GAAG,SAAS;IAIzE;;OAEG;IACH,kBAAkB,IAAI,MAAM,EAAE;IAM9B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAQxB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiB5B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAiD5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA4DzB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAmC3B;;;;;;;;;;;;;OAaG;YACW,SAAS;IA2GvB;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAWhC;;OAEG;YACW,gBAAgB;IAgK9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAqEzB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAkB/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,sBAAsB;IA2B9B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,gBAAgB,CAAC,CAAC,SAAS,MAAM,sBAAsB,EACrD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,2BAA2B,CAAC,CAAC,CAAC,EACxC,OAAO,CAAC,EAAE,kCAAkC,GAC3C,IAAI;IAIP;;;;;;OAMG;IACH,mBAAmB,CAAC,CAAC,SAAS,MAAM,sBAAsB,EACxD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,2BAA2B,CAAC,CAAC,CAAC,EACxC,OAAO,CAAC,EAAE,kCAAkC,GAC3C,IAAI;IAQP;;;OAGG;IACH,OAAO,CAAC,UAAU;CAWnB"}
|
package/dist/OMEZarrNVImage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
//
|
|
371
|
-
|
|
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
|
-
//
|
|
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
|
|
451
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1448
|
-
|
|
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
|
-
|
|
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
|
|
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;
|