@fideus-labs/fidnii 0.2.0 → 0.4.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/fromTiff.d.ts +51 -0
- package/dist/fromTiff.d.ts.map +1 -0
- package/dist/fromTiff.js +64 -0
- package/dist/fromTiff.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -35
- 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 +4 -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/fromTiff.ts +88 -0
- package/src/index.ts +18 -2
- package/src/normalize.ts +119 -0
- package/src/types.ts +95 -1
- package/src/utils/coordinates.ts +26 -24
|
@@ -93,51 +93,67 @@ export function selectResolution(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Get the chunk shape for a volume.
|
|
96
|
+
* Get the chunk shape for a volume as [z, y, x].
|
|
97
|
+
*
|
|
98
|
+
* Looks up `"z"`, `"y"`, `"x"` dimensions by name. If `"z"` is absent
|
|
99
|
+
* (e.g., 2D images with `dims=["y", "x"]` or `["y", "x", "c"]`),
|
|
100
|
+
* the z chunk size defaults to 1. Non-spatial dimensions like `"c"` and
|
|
101
|
+
* `"t"` are ignored.
|
|
97
102
|
*
|
|
98
103
|
* @param ngffImage - The NgffImage to get chunk shape from
|
|
99
104
|
* @returns Chunk shape as [z, y, x]
|
|
105
|
+
* @throws If neither `"y"` nor `"x"` can be found in dims
|
|
100
106
|
*/
|
|
101
107
|
export function getChunkShape(ngffImage: NgffImage): [number, number, number] {
|
|
102
108
|
const chunks = ngffImage.data.chunks
|
|
103
109
|
const dims = ngffImage.dims
|
|
104
110
|
|
|
105
|
-
// Find z, y, x indices in dims
|
|
106
|
-
const zIdx = dims.indexOf("z")
|
|
107
111
|
const yIdx = dims.indexOf("y")
|
|
108
112
|
const xIdx = dims.indexOf("x")
|
|
109
113
|
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
if (yIdx === -1 || xIdx === -1) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Cannot determine chunk shape: dims=[${dims.join(",")}] ` +
|
|
117
|
+
`is missing required "y" and/or "x" axes`,
|
|
118
|
+
)
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
const zIdx = dims.indexOf("z")
|
|
122
|
+
const cz = zIdx !== -1 ? chunks[zIdx] : 1
|
|
123
|
+
|
|
124
|
+
return [cz, chunks[yIdx], chunks[xIdx]]
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
120
|
-
* Get the shape of a volume as [z, y, x].
|
|
128
|
+
* Get the spatial shape of a volume as [z, y, x].
|
|
129
|
+
*
|
|
130
|
+
* Looks up `"z"`, `"y"`, `"x"` dimensions by name. If `"z"` is absent
|
|
131
|
+
* (e.g., 2D images with `dims=["y", "x"]` or `["y", "x", "c"]`),
|
|
132
|
+
* the z size defaults to 1. Non-spatial dimensions like `"c"` and
|
|
133
|
+
* `"t"` are ignored.
|
|
121
134
|
*
|
|
122
135
|
* @param ngffImage - The NgffImage
|
|
123
136
|
* @returns Shape as [z, y, x]
|
|
137
|
+
* @throws If neither `"y"` nor `"x"` can be found in dims
|
|
124
138
|
*/
|
|
125
139
|
export function getVolumeShape(ngffImage: NgffImage): [number, number, number] {
|
|
126
140
|
const shape = ngffImage.data.shape
|
|
127
141
|
const dims = ngffImage.dims
|
|
128
142
|
|
|
129
|
-
// Find z, y, x indices in dims
|
|
130
|
-
const zIdx = dims.indexOf("z")
|
|
131
143
|
const yIdx = dims.indexOf("y")
|
|
132
144
|
const xIdx = dims.indexOf("x")
|
|
133
145
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
if (yIdx === -1 || xIdx === -1) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Cannot determine volume shape: dims=[${dims.join(",")}] ` +
|
|
149
|
+
`is missing required "y" and/or "x" axes`,
|
|
150
|
+
)
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
|
|
153
|
+
const zIdx = dims.indexOf("z")
|
|
154
|
+
const sz = zIdx !== -1 ? shape[zIdx] : 1
|
|
155
|
+
|
|
156
|
+
return [sz, shape[yIdx], shape[xIdx]]
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
/**
|
package/src/fromTiff.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Load TIFF files (OME-TIFF, pyramidal, plain) and produce a
|
|
6
|
+
* {@link Multiscales} object ready for {@link OMEZarrNVImage.create}.
|
|
7
|
+
*
|
|
8
|
+
* Uses {@link https://github.com/fideus-labs/fiff | @fideus-labs/fiff}
|
|
9
|
+
* to present TIFFs as zarrita-compatible stores, then delegates to
|
|
10
|
+
* {@link fromNgffZarr} for OME-Zarr metadata parsing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TiffStoreOptions } from "@fideus-labs/fiff"
|
|
14
|
+
import { TiffStore } from "@fideus-labs/fiff"
|
|
15
|
+
import type { Multiscales } from "@fideus-labs/ngff-zarr"
|
|
16
|
+
import type { FromNgffZarrOptions } from "@fideus-labs/ngff-zarr/browser"
|
|
17
|
+
import { fromNgffZarr } from "@fideus-labs/ngff-zarr/browser"
|
|
18
|
+
import type { Readable } from "zarrita"
|
|
19
|
+
|
|
20
|
+
/** Options for {@link fromTiff}. */
|
|
21
|
+
export interface FromTiffOptions {
|
|
22
|
+
/** Options forwarded to {@link TiffStore} (e.g. IFD offsets, HTTP headers). */
|
|
23
|
+
tiff?: TiffStoreOptions
|
|
24
|
+
/** Options forwarded to {@link fromNgffZarr} (e.g. validate, cache). */
|
|
25
|
+
ngffZarr?: Omit<FromNgffZarrOptions, "version">
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load a TIFF file and return a {@link Multiscales} object ready for
|
|
30
|
+
* {@link OMEZarrNVImage.create}.
|
|
31
|
+
*
|
|
32
|
+
* Supports OME-TIFF, pyramidal TIFF (SubIFDs / legacy / COG), and
|
|
33
|
+
* plain single-image TIFFs. Both local (Blob/ArrayBuffer) and
|
|
34
|
+
* remote (URL with HTTP range requests) sources are supported.
|
|
35
|
+
*
|
|
36
|
+
* @param source - A URL string, Blob/File, ArrayBuffer, or
|
|
37
|
+
* pre-built {@link TiffStore}.
|
|
38
|
+
* @param options - Optional {@link FromTiffOptions}.
|
|
39
|
+
* @returns A {@link Multiscales} object for use with
|
|
40
|
+
* {@link OMEZarrNVImage.create}.
|
|
41
|
+
* @throws If the source cannot be opened as a TIFF or if the
|
|
42
|
+
* synthesized OME-Zarr metadata is invalid.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // From a remote URL
|
|
47
|
+
* const ms = await fromTiff("https://example.com/image.ome.tif")
|
|
48
|
+
* const image = await OMEZarrNVImage.create({
|
|
49
|
+
* multiscales: ms,
|
|
50
|
+
* niivue: nv,
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* // From a File input
|
|
54
|
+
* const file = inputElement.files[0]
|
|
55
|
+
* const ms = await fromTiff(file)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export async function fromTiff(
|
|
59
|
+
source: string | Blob | ArrayBuffer | TiffStore,
|
|
60
|
+
options: FromTiffOptions = {},
|
|
61
|
+
): Promise<Multiscales> {
|
|
62
|
+
let store: TiffStore
|
|
63
|
+
if (source instanceof TiffStore) {
|
|
64
|
+
store = source
|
|
65
|
+
} else if (typeof source === "string") {
|
|
66
|
+
store = await TiffStore.fromUrl(source, options.tiff)
|
|
67
|
+
} else if (source instanceof Blob) {
|
|
68
|
+
store = await TiffStore.fromBlob(source, options.tiff)
|
|
69
|
+
} else if (source instanceof ArrayBuffer) {
|
|
70
|
+
store = await TiffStore.fromArrayBuffer(source, options.tiff)
|
|
71
|
+
} else {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"[fidnii] fromTiff: source must be a URL string, Blob, " +
|
|
74
|
+
"ArrayBuffer, or TiffStore",
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// TiffStore implements zarrita's Readable interface structurally
|
|
79
|
+
// (its get() method accepts string keys including the leading "/"
|
|
80
|
+
// that zarrita uses for AbsolutePath). Since fromNgffZarr (v0.10+)
|
|
81
|
+
// accepts zarr.Readable directly, we only need a type assertion.
|
|
82
|
+
//
|
|
83
|
+
// TiffStore always produces OME-Zarr v0.5 metadata.
|
|
84
|
+
return fromNgffZarr(store as unknown as Readable, {
|
|
85
|
+
version: "0.5",
|
|
86
|
+
...options.ngffZarr,
|
|
87
|
+
})
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,10 +34,15 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
export type { TiffStoreOptions } from "@fideus-labs/fiff"
|
|
38
|
+
export { TiffStore } from "@fideus-labs/fiff"
|
|
37
39
|
// Re-export Methods enum so consumers can check isLabelImage or compare method values
|
|
38
40
|
export { Methods } from "@fideus-labs/ngff-zarr"
|
|
39
41
|
// Worker pool lifecycle (re-exported from ngff-zarr)
|
|
40
|
-
export {
|
|
42
|
+
export {
|
|
43
|
+
terminateOmeroWorkerPool,
|
|
44
|
+
terminateWorkerPool,
|
|
45
|
+
} from "@fideus-labs/ngff-zarr/browser"
|
|
41
46
|
|
|
42
47
|
// Buffer manager
|
|
43
48
|
export { BufferManager } from "./BufferManager.js"
|
|
@@ -69,10 +74,16 @@ export type {
|
|
|
69
74
|
} from "./events.js"
|
|
70
75
|
// Event system (browser-native EventTarget API)
|
|
71
76
|
export { OMEZarrNVImageEvent } from "./events.js"
|
|
77
|
+
export type { FromTiffOptions } from "./fromTiff.js"
|
|
78
|
+
// TIFF support (via @fideus-labs/fiff)
|
|
79
|
+
export { fromTiff } from "./fromTiff.js"
|
|
80
|
+
// RGB normalization utilities
|
|
81
|
+
export type { ChannelWindow } from "./normalize.js"
|
|
82
|
+
export { computeChannelMinMax, normalizeToUint8 } from "./normalize.js"
|
|
72
83
|
// Main class
|
|
73
84
|
export { OMEZarrNVImage } from "./OMEZarrNVImage.js"
|
|
74
85
|
// Region coalescer
|
|
75
|
-
export { RegionCoalescer } from "./RegionCoalescer.js"
|
|
86
|
+
export { buildSelection, RegionCoalescer } from "./RegionCoalescer.js"
|
|
76
87
|
export type { OrthogonalAxis } from "./ResolutionSelector.js"
|
|
77
88
|
// Resolution selector utilities
|
|
78
89
|
export {
|
|
@@ -88,6 +99,7 @@ export {
|
|
|
88
99
|
// Types
|
|
89
100
|
export type {
|
|
90
101
|
AttachedNiivueState,
|
|
102
|
+
ChannelInfo,
|
|
91
103
|
ChunkAlignedRegion,
|
|
92
104
|
ChunkCache,
|
|
93
105
|
ClipPlane,
|
|
@@ -106,9 +118,13 @@ export type {
|
|
|
106
118
|
// Type utilities
|
|
107
119
|
export {
|
|
108
120
|
getBytesPerPixel,
|
|
121
|
+
getChannelInfo,
|
|
109
122
|
getNiftiDataType,
|
|
123
|
+
getRGBNiftiDataType,
|
|
110
124
|
getTypedArrayConstructor,
|
|
125
|
+
isRGBImage,
|
|
111
126
|
NiftiDataType,
|
|
127
|
+
needsRGBNormalization,
|
|
112
128
|
parseZarritaDtype,
|
|
113
129
|
SLICE_TYPE,
|
|
114
130
|
} from "./types.js"
|
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { TypedArray } from "./types.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-channel display window for normalization.
|
|
8
|
+
* Maps source values in `[start, end]` to uint8 `[0, 255]`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ChannelWindow {
|
|
11
|
+
/** Lower bound of the display window (maps to 0) */
|
|
12
|
+
start: number
|
|
13
|
+
/** Upper bound of the display window (maps to 255) */
|
|
14
|
+
end: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize interleaved multi-component data to uint8.
|
|
19
|
+
*
|
|
20
|
+
* Each channel is linearly mapped from its `[start, end]` window to
|
|
21
|
+
* `[0, 255]`, with clamping at both ends. Source data is expected to
|
|
22
|
+
* be interleaved: `[R0, G0, B0, R1, G1, B1, ...]`.
|
|
23
|
+
*
|
|
24
|
+
* @param source - Interleaved multi-component data in any typed array
|
|
25
|
+
* @param components - Number of components per voxel (3 for RGB, 4 for RGBA)
|
|
26
|
+
* @param channelWindows - Per-channel display windows; must have
|
|
27
|
+
* `components` entries. If fewer are provided, remaining channels use
|
|
28
|
+
* the last window.
|
|
29
|
+
* @returns A new `Uint8Array` with normalized uint8 values
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const src = new Uint16Array([0, 32768, 65535, 0, 0, 0])
|
|
34
|
+
* const windows = [
|
|
35
|
+
* { start: 0, end: 65535 },
|
|
36
|
+
* { start: 0, end: 65535 },
|
|
37
|
+
* { start: 0, end: 65535 },
|
|
38
|
+
* ]
|
|
39
|
+
* const result = normalizeToUint8(src, 3, windows)
|
|
40
|
+
* // result ≈ Uint8Array [0, 128, 255, 0, 0, 0]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeToUint8(
|
|
44
|
+
source: TypedArray,
|
|
45
|
+
components: number,
|
|
46
|
+
channelWindows: ChannelWindow[],
|
|
47
|
+
): Uint8Array {
|
|
48
|
+
const len = source.length
|
|
49
|
+
const output = new Uint8Array(len)
|
|
50
|
+
const numVoxels = len / components
|
|
51
|
+
|
|
52
|
+
// Pre-compute per-channel scale factors for performance
|
|
53
|
+
const scales = new Float64Array(components)
|
|
54
|
+
const offsets = new Float64Array(components)
|
|
55
|
+
for (let c = 0; c < components; c++) {
|
|
56
|
+
const win = channelWindows[Math.min(c, channelWindows.length - 1)]
|
|
57
|
+
const range = win.end - win.start
|
|
58
|
+
if (range > 0) {
|
|
59
|
+
scales[c] = 255 / range
|
|
60
|
+
offsets[c] = win.start
|
|
61
|
+
} else {
|
|
62
|
+
// Degenerate window: all values map to 0
|
|
63
|
+
scales[c] = 0
|
|
64
|
+
offsets[c] = 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let v = 0; v < numVoxels; v++) {
|
|
69
|
+
const base = v * components
|
|
70
|
+
for (let c = 0; c < components; c++) {
|
|
71
|
+
const scaled = (source[base + c] - offsets[c]) * scales[c]
|
|
72
|
+
// Clamp to [0, 255] and round
|
|
73
|
+
output[base + c] =
|
|
74
|
+
scaled <= 0 ? 0 : scaled >= 255 ? 255 : (scaled + 0.5) | 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return output
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compute per-channel min/max from interleaved multi-component data.
|
|
83
|
+
*
|
|
84
|
+
* Use this as a fallback when OMERO window metadata is not available.
|
|
85
|
+
* The result can be passed directly to {@link normalizeToUint8}.
|
|
86
|
+
*
|
|
87
|
+
* @param data - Interleaved multi-component data
|
|
88
|
+
* @param components - Number of components per voxel (3 for RGB, 4 for RGBA)
|
|
89
|
+
* @returns Per-channel windows with `start = min` and `end = max`
|
|
90
|
+
*/
|
|
91
|
+
export function computeChannelMinMax(
|
|
92
|
+
data: TypedArray,
|
|
93
|
+
components: number,
|
|
94
|
+
): ChannelWindow[] {
|
|
95
|
+
const windows: ChannelWindow[] = Array.from({ length: components }, () => ({
|
|
96
|
+
start: Infinity,
|
|
97
|
+
end: -Infinity,
|
|
98
|
+
}))
|
|
99
|
+
|
|
100
|
+
const numVoxels = data.length / components
|
|
101
|
+
for (let v = 0; v < numVoxels; v++) {
|
|
102
|
+
const base = v * components
|
|
103
|
+
for (let c = 0; c < components; c++) {
|
|
104
|
+
const val = data[base + c]
|
|
105
|
+
if (val < windows[c].start) windows[c].start = val
|
|
106
|
+
if (val > windows[c].end) windows[c].end = val
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle empty data: set degenerate windows to [0, 0]
|
|
111
|
+
for (let c = 0; c < components; c++) {
|
|
112
|
+
if (windows[c].start === Infinity) {
|
|
113
|
+
windows[c].start = 0
|
|
114
|
+
windows[c].end = 0
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return windows
|
|
119
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import type { Multiscales } from "@fideus-labs/ngff-zarr"
|
|
4
|
+
import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr"
|
|
5
5
|
import type { Niivue, NVImage } from "@niivue/niivue"
|
|
6
6
|
import { SLICE_TYPE } from "@niivue/niivue"
|
|
7
7
|
|
|
@@ -154,6 +154,17 @@ export interface OMEZarrNVImageOptions {
|
|
|
154
154
|
* @see {@link ChunkCache}
|
|
155
155
|
*/
|
|
156
156
|
cache?: ChunkCache
|
|
157
|
+
/**
|
|
158
|
+
* Flip the y-axis in the NIfTI affine for 2D images (default: true).
|
|
159
|
+
*
|
|
160
|
+
* 2D images (e.g., PNGs converted to OME-Zarr) store pixel data with
|
|
161
|
+
* y=0 at the top (increasing downward). NiiVue expects a negative
|
|
162
|
+
* y-scale in the affine to render these right-side up. Set to `false`
|
|
163
|
+
* if your 2D data already has bottom-to-top y ordering.
|
|
164
|
+
*
|
|
165
|
+
* This option has no effect on 3D volumes (images with a `"z"` axis).
|
|
166
|
+
*/
|
|
167
|
+
flipY2D?: boolean
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/**
|
|
@@ -307,14 +318,97 @@ export const NiftiDataType = {
|
|
|
307
318
|
INT32: 8,
|
|
308
319
|
FLOAT32: 16,
|
|
309
320
|
FLOAT64: 64,
|
|
321
|
+
RGB24: 128,
|
|
310
322
|
INT8: 256,
|
|
311
323
|
UINT16: 512,
|
|
312
324
|
UINT32: 768,
|
|
325
|
+
RGBA32: 2304,
|
|
313
326
|
} as const
|
|
314
327
|
|
|
315
328
|
export type NiftiDataTypeCode =
|
|
316
329
|
(typeof NiftiDataType)[keyof typeof NiftiDataType]
|
|
317
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Information about a channel (component) dimension in the image.
|
|
333
|
+
*/
|
|
334
|
+
export interface ChannelInfo {
|
|
335
|
+
/** Index of the `"c"` dimension in `ngffImage.dims` */
|
|
336
|
+
channelAxis: number
|
|
337
|
+
/** Number of components (e.g. 3 for RGB, 4 for RGBA) */
|
|
338
|
+
components: number
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Detect whether an NgffImage has a channel (`"c"`) dimension.
|
|
343
|
+
*
|
|
344
|
+
* @param ngffImage - The NgffImage to inspect
|
|
345
|
+
* @returns Channel information, or `null` if no `"c"` dimension exists
|
|
346
|
+
*/
|
|
347
|
+
export function getChannelInfo(ngffImage: NgffImage): ChannelInfo | null {
|
|
348
|
+
const channelAxis = ngffImage.dims.indexOf("c")
|
|
349
|
+
if (channelAxis === -1) return null
|
|
350
|
+
const components = ngffImage.data.shape[channelAxis]
|
|
351
|
+
return { channelAxis, components }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check whether an NgffImage represents an RGB or RGBA image.
|
|
356
|
+
*
|
|
357
|
+
* An image is considered RGB/RGBA when it has a `"c"` dimension with
|
|
358
|
+
* 3 or 4 components. Any dtype is supported — non-uint8 data is
|
|
359
|
+
* normalized to uint8 at load time (see {@link needsRGBNormalization}).
|
|
360
|
+
*
|
|
361
|
+
* @param ngffImage - The NgffImage to inspect
|
|
362
|
+
* @returns `true` if the image is RGB (3 components) or RGBA (4 components)
|
|
363
|
+
*/
|
|
364
|
+
export function isRGBImage(ngffImage: NgffImage): boolean {
|
|
365
|
+
const info = getChannelInfo(ngffImage)
|
|
366
|
+
if (!info) return false
|
|
367
|
+
return info.components === 3 || info.components === 4
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Check whether a multi-component RGB/RGBA image needs normalization
|
|
372
|
+
* to uint8 before NiiVue can render it.
|
|
373
|
+
*
|
|
374
|
+
* NiiVue only supports `DT_RGB24` (3×uint8) and `DT_RGBA32` (4×uint8)
|
|
375
|
+
* color rendering. Non-uint8 multi-component images must be normalized
|
|
376
|
+
* to uint8 using OMERO window metadata (or computed min/max).
|
|
377
|
+
*
|
|
378
|
+
* @param ngffImage - The NgffImage to inspect
|
|
379
|
+
* @param dtype - The parsed zarr dtype
|
|
380
|
+
* @returns `true` if the image is RGB/RGBA with a non-uint8 dtype
|
|
381
|
+
*/
|
|
382
|
+
export function needsRGBNormalization(
|
|
383
|
+
ngffImage: NgffImage,
|
|
384
|
+
dtype: ZarrDtype,
|
|
385
|
+
): boolean {
|
|
386
|
+
return isRGBImage(ngffImage) && dtype !== "uint8"
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get the NIfTI data type code for a multi-component image.
|
|
391
|
+
*
|
|
392
|
+
* Returns `RGB24` for 3 components and `RGBA32` for 4 components,
|
|
393
|
+
* regardless of the source dtype. Non-uint8 data is normalized to
|
|
394
|
+
* uint8 at load time before being stored in the NVImage buffer.
|
|
395
|
+
*
|
|
396
|
+
* @param channelInfo - Channel information from `getChannelInfo()`
|
|
397
|
+
* @returns The NIfTI data type code (`RGB24` or `RGBA32`)
|
|
398
|
+
* @throws If the component count is not 3 or 4
|
|
399
|
+
*/
|
|
400
|
+
export function getRGBNiftiDataType(
|
|
401
|
+
channelInfo: ChannelInfo,
|
|
402
|
+
): NiftiDataTypeCode {
|
|
403
|
+
if (channelInfo.components === 3) return NiftiDataType.RGB24
|
|
404
|
+
if (channelInfo.components === 4) return NiftiDataType.RGBA32
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Unsupported multi-component image: ` +
|
|
407
|
+
`components=${channelInfo.components}. ` +
|
|
408
|
+
`Only 3 (RGB) or 4 (RGBA) components are supported.`,
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
318
412
|
/**
|
|
319
413
|
* Map zarr dtype to typed array constructor.
|
|
320
414
|
*/
|
package/src/utils/coordinates.ts
CHANGED
|
@@ -135,23 +135,24 @@ export function normalizedToWorld(
|
|
|
135
135
|
const shape = ngffImage.data.shape
|
|
136
136
|
const dims = ngffImage.dims
|
|
137
137
|
|
|
138
|
-
// Find z, y, x indices in dims
|
|
139
|
-
const zIdx = dims.indexOf("z")
|
|
140
138
|
const yIdx = dims.indexOf("y")
|
|
141
139
|
const xIdx = dims.indexOf("x")
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
dimZ = shape[zIdx]
|
|
151
|
-
dimY = shape[yIdx]
|
|
152
|
-
dimX = shape[xIdx]
|
|
141
|
+
if (yIdx === -1 || xIdx === -1) {
|
|
142
|
+
const missingAxes = [xIdx === -1 ? "x" : null, yIdx === -1 ? "y" : null]
|
|
143
|
+
.filter((axis): axis is string => axis !== null)
|
|
144
|
+
.join(", ")
|
|
145
|
+
throw new Error(
|
|
146
|
+
`NgffImage is missing required spatial dimension(s): ${missingAxes}`,
|
|
147
|
+
)
|
|
153
148
|
}
|
|
154
149
|
|
|
150
|
+
// Look up spatial dimensions by name; default z to 1 for 2D images
|
|
151
|
+
const zIdx = dims.indexOf("z")
|
|
152
|
+
const dimZ = zIdx !== -1 ? shape[zIdx] : 1
|
|
153
|
+
const dimY = shape[yIdx]
|
|
154
|
+
const dimX = shape[xIdx]
|
|
155
|
+
|
|
155
156
|
// Convert normalized to pixel
|
|
156
157
|
const pixelCoord: [number, number, number] = [
|
|
157
158
|
normalizedCoord[2] * dimZ, // z
|
|
@@ -176,23 +177,24 @@ export function worldToNormalized(
|
|
|
176
177
|
const shape = ngffImage.data.shape
|
|
177
178
|
const dims = ngffImage.dims
|
|
178
179
|
|
|
179
|
-
// Find z, y, x indices in dims
|
|
180
|
-
const zIdx = dims.indexOf("z")
|
|
181
180
|
const yIdx = dims.indexOf("y")
|
|
182
181
|
const xIdx = dims.indexOf("x")
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
dimZ = shape[zIdx]
|
|
192
|
-
dimY = shape[yIdx]
|
|
193
|
-
dimX = shape[xIdx]
|
|
183
|
+
if (yIdx === -1 || xIdx === -1) {
|
|
184
|
+
const missingAxes = [xIdx === -1 ? "x" : null, yIdx === -1 ? "y" : null]
|
|
185
|
+
.filter((axis): axis is string => axis !== null)
|
|
186
|
+
.join(", ")
|
|
187
|
+
throw new Error(
|
|
188
|
+
`NgffImage is missing required spatial dimension(s): ${missingAxes}`,
|
|
189
|
+
)
|
|
194
190
|
}
|
|
195
191
|
|
|
192
|
+
// Look up spatial dimensions by name; default z to 1 for 2D images
|
|
193
|
+
const zIdx = dims.indexOf("z")
|
|
194
|
+
const dimZ = zIdx !== -1 ? shape[zIdx] : 1
|
|
195
|
+
const dimY = shape[yIdx]
|
|
196
|
+
const dimX = shape[xIdx]
|
|
197
|
+
|
|
196
198
|
// Convert world to pixel
|
|
197
199
|
const pixelCoord = worldToPixel(worldCoord, ngffImage)
|
|
198
200
|
|