@jax-data-science/sci-image-visualizer 0.2.0 → 0.2.2
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/channel-histogram/channel-histogram.component.mjs +2 -1
- package/esm2022/lib/contracts/channel-histogram-api.contract.mjs +1 -1
- package/esm2022/lib/contracts/plot-type.mjs +2 -2
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +39 -8
- package/esm2022/lib/routing-visualizer.service.mjs +2 -1
- package/esm2022/lib/store/visualizer-store.service.mjs +12 -1
- package/esm2022/lib/visualizer.component.mjs +7 -2
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +58 -9
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/lib/contracts/channel-histogram-api.contract.d.ts +2 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +2 -0
- package/lib/routing-visualizer.service.d.ts +1 -0
- package/lib/store/visualizer-store.service.d.ts +7 -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 },
|
|
@@ -3223,6 +3223,9 @@ class VisualizerStore {
|
|
|
3223
3223
|
// metadata on load (1 "Intensity" channel for grayscale; R/G/B for RGB). The
|
|
3224
3224
|
// Channels & Histogram pane edits these and both backends recolor live.
|
|
3225
3225
|
this.channelStates$ = new BehaviorSubject([]);
|
|
3226
|
+
/** Index of the channel selected in the Channels pane — the ONE band a
|
|
3227
|
+
* single-scalar 3D view (the napari Surface) follows for a multichannel image. */
|
|
3228
|
+
this.selectedChannel$ = new BehaviorSubject(0);
|
|
3226
3229
|
// The derived defaults (full window, gamma 1, default tint) for the current
|
|
3227
3230
|
// image, kept so "Reset" can restore a channel's window/gamma/colour.
|
|
3228
3231
|
this.defaultChannelStates = [];
|
|
@@ -3372,6 +3375,7 @@ class VisualizerStore {
|
|
|
3372
3375
|
this.defaultChannelStates = next.map((c) => ({ ...c }));
|
|
3373
3376
|
if (next.length !== this.channelStates$.value.length) {
|
|
3374
3377
|
this.channelStates$.next(next);
|
|
3378
|
+
this.selectedChannel$.next(0); // new channel structure → reset the selected band
|
|
3375
3379
|
}
|
|
3376
3380
|
}
|
|
3377
3381
|
/** Channels for an image:
|
|
@@ -3410,6 +3414,13 @@ class VisualizerStore {
|
|
|
3410
3414
|
currentChannelStates() {
|
|
3411
3415
|
return this.channelStates$.value;
|
|
3412
3416
|
}
|
|
3417
|
+
/** The channel selected in the pane (drives a single-scalar 3D Surface). */
|
|
3418
|
+
getSelectedChannel() { return this.selectedChannel$.asObservable(); }
|
|
3419
|
+
currentSelectedChannel() { return this.selectedChannel$.value; }
|
|
3420
|
+
setSelectedChannel(index) {
|
|
3421
|
+
if (this.selectedChannel$.value !== index)
|
|
3422
|
+
this.selectedChannel$.next(index);
|
|
3423
|
+
}
|
|
3413
3424
|
/** Patch one channel by index (immutable update). */
|
|
3414
3425
|
setChannelState(index, partial) {
|
|
3415
3426
|
const next = this.channelStates$.value.map((c) => c.index === index ? { ...c, ...partial, index: c.index } : c);
|
|
@@ -6251,6 +6262,7 @@ class ChannelHistogramComponent {
|
|
|
6251
6262
|
}
|
|
6252
6263
|
selectChannel(ch) {
|
|
6253
6264
|
this.selected = ch;
|
|
6265
|
+
this.api.setSelectedChannel(ch.index); // a single-scalar 3D Surface follows the selected band
|
|
6254
6266
|
this.histRetries = 0;
|
|
6255
6267
|
this.loadHistogram();
|
|
6256
6268
|
}
|
|
@@ -6720,6 +6732,11 @@ class VisualizerComponent {
|
|
|
6720
6732
|
const caps = this.plotService.capabilities;
|
|
6721
6733
|
const isStack = !!this.imageInfo?.isStack;
|
|
6722
6734
|
const isGrayscale = !!this.imageInfo?.isGrayscale;
|
|
6735
|
+
// A multichannel image's bands are each single-band (scalar), so the scalar
|
|
6736
|
+
// plot modes (Heatmap / Surface / Volume / Isosurface) apply per channel even
|
|
6737
|
+
// though the RGB composite isn't grayscale — don't gate them out.
|
|
6738
|
+
const m0 = this.imageInfo?.imageMeta?.[0];
|
|
6739
|
+
const isMultichannel = (m0?.channelCount ?? 1) > 1 && (m0?.rgbChannels ?? 1) < 3;
|
|
6723
6740
|
this.plotTypeOptions = this.plotService.getPlotTypeDescriptors()
|
|
6724
6741
|
.filter((d) => {
|
|
6725
6742
|
// Outside test mode, only the curated set (those with a productionLabel)
|
|
@@ -6730,7 +6747,7 @@ class VisualizerComponent {
|
|
|
6730
6747
|
return false;
|
|
6731
6748
|
if (d.requiresStack && !isStack)
|
|
6732
6749
|
return false;
|
|
6733
|
-
if (d.requiresGrayscale && !isGrayscale)
|
|
6750
|
+
if (d.requiresGrayscale && !isGrayscale && !isMultichannel)
|
|
6734
6751
|
return false;
|
|
6735
6752
|
return true;
|
|
6736
6753
|
})
|
|
@@ -18705,6 +18722,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
18705
18722
|
this.surfaceLayer = null;
|
|
18706
18723
|
/** Which band the surface samples (a channel index for multichannel, else the composite). */
|
|
18707
18724
|
this.surfaceChannel = undefined;
|
|
18725
|
+
/** True when the mounted Surface follows one band of a multichannel image. */
|
|
18726
|
+
this.surfaceMultichannel = false;
|
|
18708
18727
|
/** Pre-loaded per-slice luminance planes (already decimated to the surface grid), keyed by z,
|
|
18709
18728
|
* so the stack slider rebuilds the surface instantly. Filled by {@link preloadSurfacePlanes}. */
|
|
18710
18729
|
this.surfacePlanes = new Map();
|
|
@@ -18877,7 +18896,13 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
18877
18896
|
// beyond that one file's own extent).
|
|
18878
18897
|
const info = this.loaded?.imageInfo;
|
|
18879
18898
|
if (this.simpleStack.isSimple(info)) {
|
|
18880
|
-
|
|
18899
|
+
// Serverless multichannel volume: fetch the requested channel's OWN plane
|
|
18900
|
+
// (channelUrls[z][channel]) so each band stays distinct — the client-side
|
|
18901
|
+
// analog of the server's per-channel /tile?channel=c. Else the z-anchor URL.
|
|
18902
|
+
const chUrls = info.channelUrls;
|
|
18903
|
+
const url = channel != null && chUrls?.[z]?.[channel] != null
|
|
18904
|
+
? chUrls[z][channel]
|
|
18905
|
+
: this.simpleStack.urlFor(info, z);
|
|
18881
18906
|
if (!url)
|
|
18882
18907
|
throw new Error(`[napari-js] no URL for slice ${z}`);
|
|
18883
18908
|
return this.simpleStack.fetchAsBitmap(url);
|
|
@@ -19487,8 +19512,13 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
19487
19512
|
async mountVolume(viewer, info, plotType) {
|
|
19488
19513
|
const token = this.loadToken; // bail before rendering if a Cancel / new plot bumps this
|
|
19489
19514
|
const desc = await this.ensureDescriptor();
|
|
19490
|
-
|
|
19491
|
-
|
|
19515
|
+
// Serverless multichannel (tiled:false + channelUrls): no tile descriptor, so
|
|
19516
|
+
// derive the channel count from imageMeta and assemble each band from its own
|
|
19517
|
+
// channelUrls[z][c] plane (fetchSlice does the per-channel routing).
|
|
19518
|
+
const simpleMc = this.simpleStack.isSimple(info) && !!info?.channelUrls?.length
|
|
19519
|
+
&& (info?.imageMeta?.[0]?.channelCount ?? 1) > 1;
|
|
19520
|
+
const channelCount = simpleMc ? info.imageMeta[0].channelCount : (desc?.channels ?? 1);
|
|
19521
|
+
const multichannel = simpleMc || (!!desc?.multichannel && channelCount > 1);
|
|
19492
19522
|
const res = volumeResolutionFor(this.resolutionScale);
|
|
19493
19523
|
const rendering = isNapariIsosurface(plotType) ? 'iso' : 'mip';
|
|
19494
19524
|
const states = this.store.currentChannelStates();
|
|
@@ -19878,9 +19908,15 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
19878
19908
|
*/
|
|
19879
19909
|
async mountSurface(viewer) {
|
|
19880
19910
|
const desc = await this.ensureDescriptor();
|
|
19881
|
-
const
|
|
19882
|
-
|
|
19883
|
-
|
|
19911
|
+
const info = this.loaded?.imageInfo;
|
|
19912
|
+
// Serverless multichannel (tiled:false + channelUrls) has no descriptor — detect
|
|
19913
|
+
// it from imageMeta so the Surface still follows one band.
|
|
19914
|
+
const simpleMc = this.simpleStack.isSimple(info) && !!info?.channelUrls?.length
|
|
19915
|
+
&& (info?.imageMeta?.[0]?.channelCount ?? 1) > 1;
|
|
19916
|
+
const multichannel = simpleMc || (!!desc?.multichannel && (desc?.channels ?? 1) > 1);
|
|
19917
|
+
this.surfaceMultichannel = multichannel;
|
|
19918
|
+
// A height field is single-scalar → follow the pane-SELECTED channel.
|
|
19919
|
+
this.surfaceChannel = multichannel ? this.store.currentSelectedChannel() : undefined;
|
|
19884
19920
|
this.surfaceMaxGrid = surfaceResolutionFor(this.resolutionScale).maxGrid;
|
|
19885
19921
|
this.imageMode = 'grayscale';
|
|
19886
19922
|
this.volumeMultichannel = false;
|
|
@@ -20105,13 +20141,25 @@ class NapariVisualizerService extends BaseStoreVisualizer {
|
|
|
20105
20141
|
this.store.getReverseScale(),
|
|
20106
20142
|
this.store.getInvert(),
|
|
20107
20143
|
this.store.getChannelStates(),
|
|
20108
|
-
|
|
20144
|
+
this.store.getSelectedChannel(),
|
|
20145
|
+
]).subscribe(([colormap, reverse, invert, channels, selected]) => {
|
|
20109
20146
|
this.currentColormap = colormap ?? null;
|
|
20110
20147
|
this.currentReverse = reverse;
|
|
20111
20148
|
this.invertEnabled = invert;
|
|
20112
20149
|
const layer = this.surfaceLayer;
|
|
20113
20150
|
if (!layer || !this.viewer)
|
|
20114
20151
|
return;
|
|
20152
|
+
// Selected channel changed (multichannel): re-fetch THAT band's planes and
|
|
20153
|
+
// rebuild the height-field for the current slice.
|
|
20154
|
+
if (this.surfaceMultichannel && selected !== this.surfaceChannel) {
|
|
20155
|
+
this.surfaceChannel = selected;
|
|
20156
|
+
void (async () => {
|
|
20157
|
+
await this.preloadSurfacePlanes(this.viewer);
|
|
20158
|
+
if (this.viewer)
|
|
20159
|
+
await this.buildSurface(this.viewer, this.loaded?.z ?? 0);
|
|
20160
|
+
})().catch((err) => console.error('[napari-js] surface channel switch failed:', err));
|
|
20161
|
+
return;
|
|
20162
|
+
}
|
|
20115
20163
|
const st = this.surfaceState(channels);
|
|
20116
20164
|
const win = [st?.min ?? 0, st?.max ?? 255];
|
|
20117
20165
|
const windowChanged = !this.surfaceWindow || win[0] !== this.surfaceWindow[0] || win[1] !== this.surfaceWindow[1];
|
|
@@ -21266,6 +21314,7 @@ class RoutingVisualizerService {
|
|
|
21266
21314
|
/** Export the underlying data (16-bit multi-band TIFF) via the active backend. */
|
|
21267
21315
|
exportData() { this.renderer().exportData(); }
|
|
21268
21316
|
getChannels$() { return this.store.getChannelStates(); }
|
|
21317
|
+
setSelectedChannel(index) { this.store.setSelectedChannel(index); }
|
|
21269
21318
|
setChannelState(index, partial) {
|
|
21270
21319
|
this.store.setChannelState(index, partial);
|
|
21271
21320
|
}
|