@jax-data-science/sci-image-visualizer 0.1.0 → 0.2.1
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/esm2022/lib/contracts/image.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type.mjs +2 -2
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +15 -4
- package/esm2022/lib/implementations/osd/histogram-sampler.mjs +46 -1
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +194 -10
- package/esm2022/lib/visualizer.component.mjs +7 -2
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +259 -14
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/lib/contracts/image.contract.d.ts +6 -0
- package/lib/implementations/osd/histogram-sampler.d.ts +17 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +25 -0
- package/package.json +1 -1
|
@@ -166,7 +166,7 @@ const PLOT_TYPE_DESCRIPTORS = {
|
|
|
166
166
|
// ── napari-js WebGPU (jit-ui#102). The 3D types take a runtime decimate factor. ──
|
|
167
167
|
[PlotType.NAPARI_IMAGE]: { type: PlotType.NAPARI_IMAGE, label: 'Image (napari · WebGPU)', icon: 'pi pi-image', dimensions: '2d', source: 'image' },
|
|
168
168
|
[PlotType.NAPARI_SCATTER]: { type: PlotType.NAPARI_SCATTER, label: 'Scatter 2D (napari · WebGPU)', icon: 'pi pi-chart-scatter', dimensions: '2d', source: 'regions' },
|
|
169
|
-
[PlotType.NAPARI_SURFACE]: { type: PlotType.NAPARI_SURFACE, label: 'Surface (napari · WebGPU)', productionLabel: 'Surface', icon: 'assets/plotting/surface.svg', dimensions: '3d', source: 'image',
|
|
169
|
+
[PlotType.NAPARI_SURFACE]: { type: PlotType.NAPARI_SURFACE, label: 'Surface (napari · WebGPU)', productionLabel: 'Surface', icon: 'assets/plotting/surface.svg', dimensions: '3d', source: 'image' }, // a height-field is z=intensity of one plane — works for any grayscale/RGB image (RGB→luminance), no stack needed
|
|
170
170
|
[PlotType.NAPARI_SCATTER3D]: { type: PlotType.NAPARI_SCATTER3D, label: 'Scatter 3D (napari · WebGPU)', icon: 'assets/plotting/3d-coordinates.svg', dimensions: '3d', source: 'image', requiresStack: true, requiresGrayscale: true },
|
|
171
171
|
[PlotType.NAPARI_VOLUME]: { type: PlotType.NAPARI_VOLUME, label: 'Volume (napari · WebGPU)', productionLabel: 'Volume', icon: 'assets/plotting/cube-3d.svg', dimensions: '3d', source: 'image', requiresStack: true, requiresGrayscale: true },
|
|
172
172
|
[PlotType.NAPARI_ISOSURFACE]: { type: PlotType.NAPARI_ISOSURFACE, label: 'Isosurface (napari · WebGPU)', productionLabel: 'Isosurface', icon: 'assets/plotting/isosurface.svg', dimensions: '3d', source: 'image', requiresStack: true, requiresGrayscale: true },
|
|
@@ -6720,6 +6720,11 @@ class VisualizerComponent {
|
|
|
6720
6720
|
const caps = this.plotService.capabilities;
|
|
6721
6721
|
const isStack = !!this.imageInfo?.isStack;
|
|
6722
6722
|
const isGrayscale = !!this.imageInfo?.isGrayscale;
|
|
6723
|
+
// A multichannel image's bands are each single-band (scalar), so the scalar
|
|
6724
|
+
// plot modes (Heatmap / Surface / Volume / Isosurface) apply per channel even
|
|
6725
|
+
// though the RGB composite isn't grayscale — don't gate them out.
|
|
6726
|
+
const m0 = this.imageInfo?.imageMeta?.[0];
|
|
6727
|
+
const isMultichannel = (m0?.channelCount ?? 1) > 1 && (m0?.rgbChannels ?? 1) < 3;
|
|
6723
6728
|
this.plotTypeOptions = this.plotService.getPlotTypeDescriptors()
|
|
6724
6729
|
.filter((d) => {
|
|
6725
6730
|
// Outside test mode, only the curated set (those with a productionLabel)
|
|
@@ -6730,7 +6735,7 @@ class VisualizerComponent {
|
|
|
6730
6735
|
return false;
|
|
6731
6736
|
if (d.requiresStack && !isStack)
|
|
6732
6737
|
return false;
|
|
6733
|
-
if (d.requiresGrayscale && !isGrayscale)
|
|
6738
|
+
if (d.requiresGrayscale && !isGrayscale && !isMultichannel)
|
|
6734
6739
|
return false;
|
|
6735
6740
|
return true;
|
|
6736
6741
|
})
|
|
@@ -15137,6 +15142,51 @@ class HistogramSampler {
|
|
|
15137
15142
|
console.warn('[viz:window] per-image window compute failed — using identity 0..255', err);
|
|
15138
15143
|
}
|
|
15139
15144
|
}
|
|
15145
|
+
/**
|
|
15146
|
+
* Client-side histogram for the SIMPLE (`tiled:false`) path: bin an
|
|
15147
|
+
* already-decoded RGBA frame's OWN pixels — no tile server, no fetch. Same
|
|
15148
|
+
* output shape as {@link computeImageWindow}: grayscale → [intensity];
|
|
15149
|
+
* RGB → [R, G, B]. This is what lets the Channels & Histogram pane work for
|
|
15150
|
+
* serverless images (the processing-pipeline preview, DICOM/TIFF stacks),
|
|
15151
|
+
* which never hit the tile / `/histogram` endpoints.
|
|
15152
|
+
*/
|
|
15153
|
+
computeSimpleHistogram(z, data, gray) {
|
|
15154
|
+
const cR = new Array(256).fill(0);
|
|
15155
|
+
const cG = gray ? null : new Array(256).fill(0);
|
|
15156
|
+
const cB = gray ? null : new Array(256).fill(0);
|
|
15157
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
15158
|
+
if (data[i + 3] === 0)
|
|
15159
|
+
continue; // skip transparent padding
|
|
15160
|
+
if (gray) {
|
|
15161
|
+
cR[maxRgb(data[i], data[i + 1], data[i + 2])]++;
|
|
15162
|
+
}
|
|
15163
|
+
else {
|
|
15164
|
+
cR[data[i]]++;
|
|
15165
|
+
cG[data[i + 1]]++;
|
|
15166
|
+
cB[data[i + 2]]++;
|
|
15167
|
+
}
|
|
15168
|
+
}
|
|
15169
|
+
this.sliceHistograms.set(z, gray ? [histogram256(cR)] : [histogram256(cR), histogram256(cG), histogram256(cB)]);
|
|
15170
|
+
this.host.onChannelHistogramsSampled();
|
|
15171
|
+
}
|
|
15172
|
+
/**
|
|
15173
|
+
* Per-channel histograms for a SIMPLE (`tiled:false`) MULTICHANNEL image: bin
|
|
15174
|
+
* each already-decoded channel plane's own pixels (no tile server). One
|
|
15175
|
+
* histogram per channel, so the pane's per-channel selector works serverlessly.
|
|
15176
|
+
*/
|
|
15177
|
+
computeSimpleMultichannelHistograms(z, planes) {
|
|
15178
|
+
this.sliceHistograms.set(z, planes.map((p) => {
|
|
15179
|
+
const c = new Array(256).fill(0);
|
|
15180
|
+
const d = p.data;
|
|
15181
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
15182
|
+
if (d[i + 3] === 0)
|
|
15183
|
+
continue;
|
|
15184
|
+
c[d[i]]++; // single-band plane: R=G=B, bin R
|
|
15185
|
+
}
|
|
15186
|
+
return histogram256(c);
|
|
15187
|
+
}));
|
|
15188
|
+
this.host.onChannelHistogramsSampled();
|
|
15189
|
+
}
|
|
15140
15190
|
/**
|
|
15141
15191
|
* Native-bit-depth histogram for >8-bit images, from the server `/histogram`
|
|
15142
15192
|
* endpoint (the 8-bit canvas tiles can't carry 16-bit values). Cached per
|
|
@@ -15531,6 +15581,14 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15531
15581
|
/** preview blob URL → full-res upscaled blob URL (see toFullResUrl), so
|
|
15532
15582
|
* re-visiting a simple-mode slice doesn't re-upscale. Revoked on file change. */
|
|
15533
15583
|
this.simpleFullResUrls = new Map();
|
|
15584
|
+
/** SERVERLESS multichannel (tiled:false + channelCount>1): the stack's
|
|
15585
|
+
* per-slice per-channel plane URLs, the current slice's decoded planes, and
|
|
15586
|
+
* the last composite blob URL. Composited client-side via display.channelRgbLut
|
|
15587
|
+
* (no tile server); kept OFF the tiled isMultiChannel path. */
|
|
15588
|
+
this.simpleMultichannel = false;
|
|
15589
|
+
this.simpleChannelUrls = [];
|
|
15590
|
+
this.simpleChannelPlanes = [];
|
|
15591
|
+
this.simpleCompositeUrl = null;
|
|
15534
15592
|
/** Skip the full-res upscale above this longest-side dimension — a folder
|
|
15535
15593
|
* stack is per-file previews (well under this); this only guards against an
|
|
15536
15594
|
* accidental enormous canvas allocation. */
|
|
@@ -15662,6 +15720,12 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15662
15720
|
this.colorLut = buildColormapLut(cm?.data?.value, !!rev);
|
|
15663
15721
|
this.channelStates = channels;
|
|
15664
15722
|
this.invertBg = !!invert;
|
|
15723
|
+
// Serverless multichannel: re-composite the current slice from its cached
|
|
15724
|
+
// planes with the new channel states + re-open (no tile-invalidate path).
|
|
15725
|
+
if (this.simpleMultichannel) {
|
|
15726
|
+
void this.recompositeAndOpen();
|
|
15727
|
+
return;
|
|
15728
|
+
}
|
|
15665
15729
|
// Multichannel: each channel is its own TiledImage — visibility is the
|
|
15666
15730
|
// image's opacity (window/gamma/colour are applied by recolorChannelTile
|
|
15667
15731
|
// on the invalidate below). Re-applying the current slice's reveal picks up
|
|
@@ -15771,17 +15835,39 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15771
15835
|
const filename = imageInfo?.fileName;
|
|
15772
15836
|
this.authHeaders = {};
|
|
15773
15837
|
const [width, height] = imageInfo.trueImageSize ?? [0, 0];
|
|
15774
|
-
const
|
|
15838
|
+
const meta = imageInfo.imageMeta?.[0];
|
|
15839
|
+
// SERVERLESS MULTICHANNEL: composite the slice's per-channel planes
|
|
15840
|
+
// client-side (no tile server), driven by the channel pane. Kept OFF the
|
|
15841
|
+
// tiled isMultiChannel path (see compositeSimpleMultichannel).
|
|
15842
|
+
const chUrls = imageInfo.channelUrls;
|
|
15843
|
+
this.simpleMultichannel = !!chUrls?.length && (meta?.channelCount ?? 1) > 1;
|
|
15775
15844
|
let url;
|
|
15776
|
-
if (
|
|
15845
|
+
if (this.simpleMultichannel) {
|
|
15846
|
+
this.simpleChannelUrls = chUrls;
|
|
15777
15847
|
try {
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
//
|
|
15781
|
-
|
|
15848
|
+
await this.loadSimpleChannelPlanes(chUrls[z] ?? chUrls[0]);
|
|
15849
|
+
url = await this.compositeSimpleMultichannel();
|
|
15850
|
+
// NB: histograms are binned in plot(), AFTER destroyViewer clears the
|
|
15851
|
+
// sampler — computing them here would be wiped by that clear.
|
|
15782
15852
|
}
|
|
15783
15853
|
catch (err) {
|
|
15784
|
-
console.warn('[OSD] simple
|
|
15854
|
+
console.warn('[OSD] simple multichannel composite failed', err);
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15857
|
+
else {
|
|
15858
|
+
this.simpleChannelUrls = [];
|
|
15859
|
+
this.simpleChannelPlanes = [];
|
|
15860
|
+
const rawUrl = this.simpleStack.urlFor(imageInfo, z);
|
|
15861
|
+
if (rawUrl) {
|
|
15862
|
+
try {
|
|
15863
|
+
const previewUrl = await this.simpleStack.fetchAsBlobUrl(rawUrl);
|
|
15864
|
+
// Upscale the (downscaled) preview to full resolution so OSD's world
|
|
15865
|
+
// matches the full-res ROI coordinate space — see toFullResUrl.
|
|
15866
|
+
url = await this.toFullResUrl(previewUrl, width, height);
|
|
15867
|
+
}
|
|
15868
|
+
catch (err) {
|
|
15869
|
+
console.warn('[OSD] simple-mode slice fetch failed', err);
|
|
15870
|
+
}
|
|
15785
15871
|
}
|
|
15786
15872
|
}
|
|
15787
15873
|
// No loadable URL (empty urls[] or the fetch failed): signal "couldn't
|
|
@@ -15790,10 +15876,9 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15790
15876
|
// the router can fall back, instead of mounting an <img> with an undefined
|
|
15791
15877
|
// src that throws at runtime.
|
|
15792
15878
|
if (!url) {
|
|
15793
|
-
console.warn('[OSD] simple-mode slice has no loadable URL; skipping OSD render',
|
|
15879
|
+
console.warn('[OSD] simple-mode slice has no loadable URL; skipping OSD render', filename);
|
|
15794
15880
|
return { descriptor: null, infoB64: '', z, filename };
|
|
15795
15881
|
}
|
|
15796
|
-
const meta = imageInfo.imageMeta?.[0];
|
|
15797
15882
|
const descriptor = {
|
|
15798
15883
|
width,
|
|
15799
15884
|
height,
|
|
@@ -15862,6 +15947,123 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15862
15947
|
this.simpleFullResUrls.set(previewUrl, fullResUrl);
|
|
15863
15948
|
return fullResUrl;
|
|
15864
15949
|
}
|
|
15950
|
+
/** Sample a SIMPLE-mode slice's own decoded pixels into the histogram — the
|
|
15951
|
+
* serverless analog of the tiled samplers (no tile server). Fire-and-forget;
|
|
15952
|
+
* a decode failure just leaves the pane empty rather than throwing. */
|
|
15953
|
+
async sampleSimpleHistogram(url, z) {
|
|
15954
|
+
if (!url)
|
|
15955
|
+
return;
|
|
15956
|
+
try {
|
|
15957
|
+
const img = await this.loadImageEl(url);
|
|
15958
|
+
const w = img.naturalWidth, h = img.naturalHeight;
|
|
15959
|
+
if (!w || !h)
|
|
15960
|
+
return;
|
|
15961
|
+
const c = document.createElement('canvas');
|
|
15962
|
+
c.width = w;
|
|
15963
|
+
c.height = h;
|
|
15964
|
+
const ctx = c.getContext('2d', { willReadFrequently: true });
|
|
15965
|
+
if (!ctx)
|
|
15966
|
+
return;
|
|
15967
|
+
ctx.drawImage(img, 0, 0);
|
|
15968
|
+
const data = ctx.getImageData(0, 0, w, h).data;
|
|
15969
|
+
this.sampler.computeSimpleHistogram(z, data, this.isGrayscaleImage);
|
|
15970
|
+
}
|
|
15971
|
+
catch (err) {
|
|
15972
|
+
console.warn('[OSD] simple-mode histogram sample failed', err);
|
|
15973
|
+
}
|
|
15974
|
+
}
|
|
15975
|
+
/** Fetch + decode a slice's per-channel planes (single-band grayscale) into
|
|
15976
|
+
* pixel buffers, for the SERVERLESS multichannel compositor. Auth-safe: goes
|
|
15977
|
+
* through SimpleSliceAccessService (blob:/data: as-is; http via HttpClient). */
|
|
15978
|
+
async loadSimpleChannelPlanes(urls) {
|
|
15979
|
+
const planes = [];
|
|
15980
|
+
for (const u of urls ?? []) {
|
|
15981
|
+
try {
|
|
15982
|
+
const previewUrl = await this.simpleStack.fetchAsBlobUrl(u);
|
|
15983
|
+
const img = await this.loadImageEl(previewUrl);
|
|
15984
|
+
const w = img.naturalWidth, h = img.naturalHeight;
|
|
15985
|
+
if (!w || !h) {
|
|
15986
|
+
planes.push({ data: new Uint8ClampedArray(0), width: 0, height: 0 });
|
|
15987
|
+
continue;
|
|
15988
|
+
}
|
|
15989
|
+
const c = document.createElement('canvas');
|
|
15990
|
+
c.width = w;
|
|
15991
|
+
c.height = h;
|
|
15992
|
+
const ctx = c.getContext('2d', { willReadFrequently: true });
|
|
15993
|
+
if (!ctx) {
|
|
15994
|
+
planes.push({ data: new Uint8ClampedArray(0), width: 0, height: 0 });
|
|
15995
|
+
continue;
|
|
15996
|
+
}
|
|
15997
|
+
ctx.drawImage(img, 0, 0);
|
|
15998
|
+
planes.push({ data: ctx.getImageData(0, 0, w, h).data, width: w, height: h });
|
|
15999
|
+
}
|
|
16000
|
+
catch (err) {
|
|
16001
|
+
console.warn('[OSD] channel plane decode failed', err);
|
|
16002
|
+
planes.push({ data: new Uint8ClampedArray(0), width: 0, height: 0 });
|
|
16003
|
+
}
|
|
16004
|
+
}
|
|
16005
|
+
this.simpleChannelPlanes = planes;
|
|
16006
|
+
}
|
|
16007
|
+
/** Composite the cached per-channel planes into ONE RGBA image using the
|
|
16008
|
+
* current channel states (colour/window/gamma/visibility) — the client-side,
|
|
16009
|
+
* serverless analog of the tiled per-channel 'lighter' compositor
|
|
16010
|
+
* (same display.channelRgbLut math as recolorChannelTile, applied per-plane).
|
|
16011
|
+
* Returns a blob: URL of the composite PNG, revoking the previous one. */
|
|
16012
|
+
async compositeSimpleMultichannel() {
|
|
16013
|
+
const planes = this.simpleChannelPlanes;
|
|
16014
|
+
if (!planes.length)
|
|
16015
|
+
return undefined;
|
|
16016
|
+
const w = planes[0].width, h = planes[0].height;
|
|
16017
|
+
if (!w || !h)
|
|
16018
|
+
return undefined;
|
|
16019
|
+
const states = this.store.currentChannelStates();
|
|
16020
|
+
const out = new Uint8ClampedArray(w * h * 4);
|
|
16021
|
+
for (let c = 0; c < planes.length; c++) {
|
|
16022
|
+
const st = states[c];
|
|
16023
|
+
if (st && st.visible === false)
|
|
16024
|
+
continue; // hidden channel contributes nothing
|
|
16025
|
+
const plane = planes[c];
|
|
16026
|
+
if (plane.width !== w || plane.height !== h || !plane.data.length)
|
|
16027
|
+
continue;
|
|
16028
|
+
const { r, g, b } = this.display.channelRgbLut(st);
|
|
16029
|
+
const pd = plane.data;
|
|
16030
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
16031
|
+
if (pd[i + 3] === 0)
|
|
16032
|
+
continue;
|
|
16033
|
+
const lum = pd[i]; // single-band plane (R=G=B)
|
|
16034
|
+
out[i] += r[lum]; // Uint8ClampedArray clamps → additive ('lighter')
|
|
16035
|
+
out[i + 1] += g[lum];
|
|
16036
|
+
out[i + 2] += b[lum];
|
|
16037
|
+
}
|
|
16038
|
+
}
|
|
16039
|
+
for (let i = 3; i < out.length; i += 4)
|
|
16040
|
+
out[i] = 255; // opaque
|
|
16041
|
+
const canvas = document.createElement('canvas');
|
|
16042
|
+
canvas.width = w;
|
|
16043
|
+
canvas.height = h;
|
|
16044
|
+
const ctx = canvas.getContext('2d');
|
|
16045
|
+
if (!ctx)
|
|
16046
|
+
return undefined;
|
|
16047
|
+
ctx.putImageData(new ImageData(out, w, h), 0, 0);
|
|
16048
|
+
const blob = await new Promise((res) => canvas.toBlob(res, 'image/png'));
|
|
16049
|
+
if (!blob)
|
|
16050
|
+
return undefined;
|
|
16051
|
+
if (this.simpleCompositeUrl)
|
|
16052
|
+
URL.revokeObjectURL(this.simpleCompositeUrl);
|
|
16053
|
+
this.simpleCompositeUrl = URL.createObjectURL(blob);
|
|
16054
|
+
return this.simpleCompositeUrl;
|
|
16055
|
+
}
|
|
16056
|
+
/** Re-composite the current slice from its cached planes (new channel states)
|
|
16057
|
+
* and re-open — the serverless analog of the tiled invalidate-on-channel-change. */
|
|
16058
|
+
async recompositeAndOpen() {
|
|
16059
|
+
const url = await this.compositeSimpleMultichannel();
|
|
16060
|
+
if (!url || !this.viewer)
|
|
16061
|
+
return;
|
|
16062
|
+
try {
|
|
16063
|
+
this.viewer.open({ type: 'image', url });
|
|
16064
|
+
}
|
|
16065
|
+
catch { /* viewer gone */ }
|
|
16066
|
+
}
|
|
15865
16067
|
/** Load a URL into an HTMLImageElement (resolves once decoded). Uses
|
|
15866
16068
|
* document.createElement rather than `new Image()` because this file's
|
|
15867
16069
|
* `Image` import is image-js's decoder, not the DOM element. */
|
|
@@ -15877,6 +16079,10 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15877
16079
|
for (const u of this.simpleFullResUrls.values())
|
|
15878
16080
|
URL.revokeObjectURL(u);
|
|
15879
16081
|
this.simpleFullResUrls.clear();
|
|
16082
|
+
if (this.simpleCompositeUrl) {
|
|
16083
|
+
URL.revokeObjectURL(this.simpleCompositeUrl);
|
|
16084
|
+
this.simpleCompositeUrl = null;
|
|
16085
|
+
}
|
|
15880
16086
|
}
|
|
15881
16087
|
/** Mount the viewer with a custom tile source pointing at `GET /tile`. */
|
|
15882
16088
|
plot(plotDiv, imageLoaded, imageInfo, _screenHeight, _plotType, inPlace) {
|
|
@@ -15932,6 +16138,15 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
15932
16138
|
this.isMultiChannel = false;
|
|
15933
16139
|
this.cache.clearChannelGroups();
|
|
15934
16140
|
this.destroyViewer();
|
|
16141
|
+
// Serverless histogram — AFTER destroyViewer (its last act is sampler.clear()).
|
|
16142
|
+
// Multichannel bins each cached channel plane; single-band / RGB bins the
|
|
16143
|
+
// decoded frame's own pixels.
|
|
16144
|
+
if (this.simpleMultichannel) {
|
|
16145
|
+
this.sampler.computeSimpleMultichannelHistograms(loaded.z, this.simpleChannelPlanes);
|
|
16146
|
+
}
|
|
16147
|
+
else {
|
|
16148
|
+
void this.sampleSimpleHistogram(loaded.url, loaded.z);
|
|
16149
|
+
}
|
|
15935
16150
|
}
|
|
15936
16151
|
else {
|
|
15937
16152
|
// Multichannel fluorescence (indexed/LUT-bearing stacks) composite client-side
|
|
@@ -16438,6 +16653,24 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
16438
16653
|
if (!this.viewer)
|
|
16439
16654
|
return;
|
|
16440
16655
|
if (this.simpleMode) {
|
|
16656
|
+
if (this.simpleMultichannel) {
|
|
16657
|
+
const urls = this.simpleChannelUrls[z] ?? this.simpleChannelUrls[0];
|
|
16658
|
+
if (!urls)
|
|
16659
|
+
return;
|
|
16660
|
+
this.currentZ = z;
|
|
16661
|
+
this.viewportPixels = null;
|
|
16662
|
+
void (async () => {
|
|
16663
|
+
await this.loadSimpleChannelPlanes(urls);
|
|
16664
|
+
if (this.currentZ !== z || !this.viewer)
|
|
16665
|
+
return;
|
|
16666
|
+
this.sampler.computeSimpleMultichannelHistograms(z, this.simpleChannelPlanes);
|
|
16667
|
+
const url = await this.compositeSimpleMultichannel();
|
|
16668
|
+
if (this.currentZ !== z || !this.viewer || !url)
|
|
16669
|
+
return;
|
|
16670
|
+
this.viewer.open({ type: 'image', url });
|
|
16671
|
+
})();
|
|
16672
|
+
return;
|
|
16673
|
+
}
|
|
16441
16674
|
const rawUrl = this.simpleUrls[z] ?? this.simpleUrls[0];
|
|
16442
16675
|
if (!rawUrl)
|
|
16443
16676
|
return;
|
|
@@ -16456,6 +16689,7 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
16456
16689
|
console.warn('[OSD] slice re-open failed', e?.message ?? e);
|
|
16457
16690
|
});
|
|
16458
16691
|
this.viewer.open({ type: 'image', url });
|
|
16692
|
+
void this.sampleSimpleHistogram(url, z); // re-bin the scrubbed slice
|
|
16459
16693
|
})
|
|
16460
16694
|
.catch((err) => console.warn('[OSD] slice fetch failed', err));
|
|
16461
16695
|
return;
|
|
@@ -18648,7 +18882,13 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
18648
18882
|
// beyond that one file's own extent).
|
|
18649
18883
|
const info = this.loaded?.imageInfo;
|
|
18650
18884
|
if (this.simpleStack.isSimple(info)) {
|
|
18651
|
-
|
|
18885
|
+
// Serverless multichannel volume: fetch the requested channel's OWN plane
|
|
18886
|
+
// (channelUrls[z][channel]) so each band stays distinct — the client-side
|
|
18887
|
+
// analog of the server's per-channel /tile?channel=c. Else the z-anchor URL.
|
|
18888
|
+
const chUrls = info.channelUrls;
|
|
18889
|
+
const url = channel != null && chUrls?.[z]?.[channel] != null
|
|
18890
|
+
? chUrls[z][channel]
|
|
18891
|
+
: this.simpleStack.urlFor(info, z);
|
|
18652
18892
|
if (!url)
|
|
18653
18893
|
throw new Error(`[napari-js] no URL for slice ${z}`);
|
|
18654
18894
|
return this.simpleStack.fetchAsBitmap(url);
|
|
@@ -19258,8 +19498,13 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
19258
19498
|
async mountVolume(viewer, info, plotType) {
|
|
19259
19499
|
const token = this.loadToken; // bail before rendering if a Cancel / new plot bumps this
|
|
19260
19500
|
const desc = await this.ensureDescriptor();
|
|
19261
|
-
|
|
19262
|
-
|
|
19501
|
+
// Serverless multichannel (tiled:false + channelUrls): no tile descriptor, so
|
|
19502
|
+
// derive the channel count from imageMeta and assemble each band from its own
|
|
19503
|
+
// channelUrls[z][c] plane (fetchSlice does the per-channel routing).
|
|
19504
|
+
const simpleMc = this.simpleStack.isSimple(info) && !!info?.channelUrls?.length
|
|
19505
|
+
&& (info?.imageMeta?.[0]?.channelCount ?? 1) > 1;
|
|
19506
|
+
const channelCount = simpleMc ? info.imageMeta[0].channelCount : (desc?.channels ?? 1);
|
|
19507
|
+
const multichannel = simpleMc || (!!desc?.multichannel && channelCount > 1);
|
|
19263
19508
|
const res = volumeResolutionFor(this.resolutionScale);
|
|
19264
19509
|
const rendering = isNapariIsosurface(plotType) ? 'iso' : 'mip';
|
|
19265
19510
|
const states = this.store.currentChannelStates();
|