@jax-data-science/sci-image-visualizer 0.2.5 → 0.2.6
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.
|
@@ -6675,6 +6675,13 @@ class VisualizerComponent {
|
|
|
6675
6675
|
// the user doesn't mistake it for the final preview.
|
|
6676
6676
|
this.sharpening = false;
|
|
6677
6677
|
this.running = false;
|
|
6678
|
+
/**
|
|
6679
|
+
* Monotonic render generation. Bumped when a render starts, so a render that
|
|
6680
|
+
* has been superseded by a newer image can be recognised and made inert: its
|
|
6681
|
+
* callbacks return early instead of painting, clearing `running`, releasing
|
|
6682
|
+
* the newer render's overlay, or applying its ROIs.
|
|
6683
|
+
*/
|
|
6684
|
+
this.renderToken = 0;
|
|
6678
6685
|
this.zIndex = 0;
|
|
6679
6686
|
this.maxIndex = 0;
|
|
6680
6687
|
this.contextMenuItems = [];
|
|
@@ -6997,163 +7004,198 @@ class VisualizerComponent {
|
|
|
6997
7004
|
this.loadedFileName = imgInfo.fileName;
|
|
6998
7005
|
this.plotService.setImageMeta(this.imageInfo.imageMeta);
|
|
6999
7006
|
const urls = imgInfo.urls;
|
|
7000
|
-
|
|
7007
|
+
// A newer image ALWAYS preempts an in-flight render. This was
|
|
7008
|
+
// `if (!this.running)`, which DROPPED the new image while the old one
|
|
7009
|
+
// finished: identity above had already flipped to the new file, so the
|
|
7010
|
+
// app reported image B as loaded while image A stayed on screen. Worse,
|
|
7011
|
+
// a cold image holds the slot for its whole server-side cache fill, so
|
|
7012
|
+
// every click for 1-2 minutes was discarded against a blank viewport
|
|
7013
|
+
// and a "Loading image..." overlay that never resolved.
|
|
7014
|
+
if (urls) {
|
|
7015
|
+
const token = ++this.renderToken;
|
|
7016
|
+
const isCurrent = () => this.renderToken === token;
|
|
7017
|
+
if (this.running) {
|
|
7018
|
+
// Stop the previous render's frame streaming (napari volume/surface
|
|
7019
|
+
// preload keeps fetching otherwise) and clear its sharpen flag. Its
|
|
7020
|
+
// callbacks are already inert via the token, so it can neither paint
|
|
7021
|
+
// nor release the overlay this render now owns.
|
|
7022
|
+
this.plotService.cancelLoading?.();
|
|
7023
|
+
this.sharpening = false;
|
|
7024
|
+
}
|
|
7001
7025
|
// image size — measure the plot div directly so the toolbar height is excluded
|
|
7002
7026
|
const plotDiv = document.getElementById(this.plotDivName);
|
|
7003
7027
|
const screenHeight = plotDiv?.offsetHeight || 500;
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7028
|
+
this.plotService.reset();
|
|
7029
|
+
this.stackLoading = imgInfo.isStack && imgInfo.showStack;
|
|
7030
|
+
// set max index of stack — computed before updateZIndex so a
|
|
7031
|
+
// fresh stack (shorter or longer than whatever was previously
|
|
7032
|
+
// loaded) clamps against ITS bounds, not the stale ones.
|
|
7033
|
+
// One URL per slice (0-indexed), so the last reachable index is
|
|
7034
|
+
// length-1 — earlier `length-2` dropped the final slice.
|
|
7035
|
+
this.maxIndex = urls.length > 1 ? urls.length - 1 : 0;
|
|
7036
|
+
// One-shot hint: jump straight to a specific slice (e.g. the
|
|
7037
|
+
// file the user actually clicked within a numbered series).
|
|
7038
|
+
// Consumed immediately so redelivering the same ImageInfo later
|
|
7039
|
+
// (a plot-type switch, reloadAndPlot) doesn't reset the user's
|
|
7040
|
+
// current scrub position back to it.
|
|
7041
|
+
if (imgInfo.initialZIndex !== undefined) {
|
|
7042
|
+
this.zIndex = imgInfo.initialZIndex;
|
|
7043
|
+
imgInfo.initialZIndex = undefined;
|
|
7044
|
+
}
|
|
7045
|
+
// make sure the zindex is within bounds
|
|
7046
|
+
this.updateZIndex(urls);
|
|
7047
|
+
this.running = true;
|
|
7048
|
+
// Multi-tier rendering (small blurry tier first, then sharpen in
|
|
7049
|
+
// place) — sequencing lives in RenderOrchestrator; this component
|
|
7050
|
+
// supplies the phase render and owns the UI flags via callbacks.
|
|
7051
|
+
// 3D plot types render single-pass: the in-place large pass doesn't
|
|
7052
|
+
// rebuild a 3D gl-mesh isosurface, so the sharpen step blanked it.
|
|
7053
|
+
const hasSmallTier = this.isHeatmap &&
|
|
7054
|
+
(imgInfo.smallUrls?.length ?? 0) === urls.length &&
|
|
7055
|
+
(imgInfo.smallUrls?.length ?? 0) > 0;
|
|
7056
|
+
const smallImgInfo = hasSmallTier ? { ...imgInfo, urls: imgInfo.smallUrls } : null;
|
|
7057
|
+
// Enter per-slice stack mode with the given slice→regions map and
|
|
7058
|
+
// seed the "previous shapes" from the now-live slice. enterStackMode
|
|
7059
|
+
// makes zIndex's slice live and resets undo (jit-ui#93).
|
|
7060
|
+
const enterStack = (slices, layout) => {
|
|
7061
|
+
this.plotService.enterStackMode(slices, this.zIndex, layout);
|
|
7062
|
+
const shapes = this.plotService
|
|
7063
|
+
.getRegions()
|
|
7064
|
+
.map((region) => region.getShape(this.plotService.getShowShapeLabel()));
|
|
7065
|
+
this.plotService.setPreviousShapes(shapes);
|
|
7066
|
+
};
|
|
7067
|
+
const applyRoi = () => {
|
|
7068
|
+
// Folder stack: a stack of self-contained per-slice files
|
|
7069
|
+
// (tiled === false; see loadSeriesAsStack). Each slice-file may
|
|
7070
|
+
// carry its own sibling "<stem>.geojson" (roiJsonStrs[z]), but a
|
|
7071
|
+
// fresh folder with none yet leaves roiJsonStrs undefined — key
|
|
7072
|
+
// off `tiled === false`, NOT roiJsonStrs, so an unannotated
|
|
7073
|
+
// folder stack still enters the per-slice-file layout (and saves
|
|
7074
|
+
// back one geojson per slice-file) rather than falling through to
|
|
7075
|
+
// the single-file combined path (jit-ui#93).
|
|
7076
|
+
if (imgInfo.isStack && imgInfo.tiled === false) {
|
|
7077
|
+
const perSlice = imgInfo.roiJsonStrs;
|
|
7078
|
+
const sliceCount = imgInfo.urls?.length ?? perSlice?.length ?? 0;
|
|
7079
|
+
const slices = new Map();
|
|
7080
|
+
for (let z = 0; z < sliceCount; z++) {
|
|
7081
|
+
const json = perSlice?.[z] ?? null;
|
|
7082
|
+
slices.set(z, json ? this.plotService.importRegions(json) : []);
|
|
7083
|
+
}
|
|
7084
|
+
enterStack(slices, 'per-slice-file');
|
|
7085
|
+
return;
|
|
7021
7086
|
}
|
|
7022
|
-
//
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
//
|
|
7026
|
-
//
|
|
7027
|
-
//
|
|
7028
|
-
//
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
(
|
|
7033
|
-
|
|
7034
|
-
// Enter per-slice stack mode with the given slice→regions map and
|
|
7035
|
-
// seed the "previous shapes" from the now-live slice. enterStackMode
|
|
7036
|
-
// makes zIndex's slice live and resets undo (jit-ui#93).
|
|
7037
|
-
const enterStack = (slices, layout) => {
|
|
7038
|
-
this.plotService.enterStackMode(slices, this.zIndex, layout);
|
|
7039
|
-
const shapes = this.plotService
|
|
7040
|
-
.getRegions()
|
|
7041
|
-
.map((region) => region.getShape(this.plotService.getShowShapeLabel()));
|
|
7042
|
-
this.plotService.setPreviousShapes(shapes);
|
|
7043
|
-
};
|
|
7044
|
-
const applyRoi = () => {
|
|
7045
|
-
// Folder stack: a stack of self-contained per-slice files
|
|
7046
|
-
// (tiled === false; see loadSeriesAsStack). Each slice-file may
|
|
7047
|
-
// carry its own sibling "<stem>.geojson" (roiJsonStrs[z]), but a
|
|
7048
|
-
// fresh folder with none yet leaves roiJsonStrs undefined — key
|
|
7049
|
-
// off `tiled === false`, NOT roiJsonStrs, so an unannotated
|
|
7050
|
-
// folder stack still enters the per-slice-file layout (and saves
|
|
7051
|
-
// back one geojson per slice-file) rather than falling through to
|
|
7052
|
-
// the single-file combined path (jit-ui#93).
|
|
7053
|
-
if (imgInfo.isStack && imgInfo.tiled === false) {
|
|
7054
|
-
const perSlice = imgInfo.roiJsonStrs;
|
|
7055
|
-
const sliceCount = imgInfo.urls?.length ?? perSlice?.length ?? 0;
|
|
7087
|
+
// Single-file z-stack: one sibling geojson holding every slice's
|
|
7088
|
+
// regions indexed by QuPath's geometry.plane.z. Enter per-slice
|
|
7089
|
+
// mode (saving back one combined z-indexed geojson) when the
|
|
7090
|
+
// geojson actually carries slice indices, or when there's nothing
|
|
7091
|
+
// yet to author against. A legacy geojson whose regions are all on
|
|
7092
|
+
// the default plane stays global (shown on every slice) so
|
|
7093
|
+
// existing single-plane annotations aren't confined to slice 0.
|
|
7094
|
+
const roiJson = imgInfo.roiJsonStr;
|
|
7095
|
+
if (imgInfo.isStack) {
|
|
7096
|
+
const regions = roiJson ? this.plotService.importRegions(roiJson) : [];
|
|
7097
|
+
const hasSliceInfo = regions.some((r) => (r.z ?? 0) !== 0);
|
|
7098
|
+
if (!roiJson || hasSliceInfo) {
|
|
7056
7099
|
const slices = new Map();
|
|
7057
|
-
for (
|
|
7058
|
-
const
|
|
7059
|
-
|
|
7100
|
+
for (const r of regions) {
|
|
7101
|
+
const z = r.z ?? 0;
|
|
7102
|
+
const bucket = slices.get(z);
|
|
7103
|
+
if (bucket)
|
|
7104
|
+
bucket.push(r);
|
|
7105
|
+
else
|
|
7106
|
+
slices.set(z, [r]);
|
|
7060
7107
|
}
|
|
7061
|
-
enterStack(slices, '
|
|
7108
|
+
enterStack(slices, 'combined');
|
|
7062
7109
|
return;
|
|
7063
7110
|
}
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
//
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
let overlayReleased = false;
|
|
7102
|
-
const releaseOverlay = () => {
|
|
7103
|
-
if (overlayReleased)
|
|
7104
|
-
return;
|
|
7105
|
-
overlayReleased = true;
|
|
7106
|
-
if (imgInfo.isStack && imgInfo.showStack)
|
|
7107
|
-
this.stackLoading = false;
|
|
7108
|
-
this.state.setImageLoading(false);
|
|
7109
|
-
};
|
|
7110
|
-
new RenderOrchestrator({
|
|
7111
|
-
// inPlace=true updates the existing render instead of rebuilding
|
|
7112
|
-
// it, so the canvas doesn't blank during the small→large swap.
|
|
7113
|
-
renderPhase: (phaseInfo, inPlace) => this.plotService.load(phaseInfo, this.zIndex).then((loadedImage) => {
|
|
7111
|
+
}
|
|
7112
|
+
// Single-plane image (or a legacy global z-stack geojson): one
|
|
7113
|
+
// region set for the whole image.
|
|
7114
|
+
if (roiJson) {
|
|
7115
|
+
const regions = this.plotService.importRegions(roiJson);
|
|
7116
|
+
this.plotService.setRegions(regions);
|
|
7117
|
+
const shapes = regions.map((region) => region.getShape(this.plotService.getShowShapeLabel()));
|
|
7118
|
+
this.plotService.setPreviousShapes(shapes);
|
|
7119
|
+
}
|
|
7120
|
+
// Loading an image's saved ROIs is not a user edit — start the
|
|
7121
|
+
// undo history fresh so the first undo can't wipe them (jit-ui#85).
|
|
7122
|
+
this.plotService.resetUndoHistory();
|
|
7123
|
+
};
|
|
7124
|
+
let overlayReleased = false;
|
|
7125
|
+
const releaseOverlay = () => {
|
|
7126
|
+
if (overlayReleased)
|
|
7127
|
+
return;
|
|
7128
|
+
overlayReleased = true;
|
|
7129
|
+
if (imgInfo.isStack && imgInfo.showStack)
|
|
7130
|
+
this.stackLoading = false;
|
|
7131
|
+
this.state.setImageLoading(false);
|
|
7132
|
+
};
|
|
7133
|
+
new RenderOrchestrator({
|
|
7134
|
+
// inPlace=true updates the existing render instead of rebuilding
|
|
7135
|
+
// it, so the canvas doesn't blank during the small→large swap.
|
|
7136
|
+
renderPhase: (phaseInfo, inPlace) => {
|
|
7137
|
+
// Superseded BEFORE this phase started — don't even issue the load.
|
|
7138
|
+
// RenderOrchestrator calls renderPhase once per tier and retries the
|
|
7139
|
+
// sharpen pass on failure, so checking only after the load resolves
|
|
7140
|
+
// would let a preempted render keep fetching slices/tiles for an
|
|
7141
|
+
// image nobody is looking at.
|
|
7142
|
+
if (!isCurrent())
|
|
7143
|
+
return Promise.resolve(null);
|
|
7144
|
+
return this.plotService.load(phaseInfo, this.zIndex).then((loadedImage) => {
|
|
7145
|
+
// Preempted while this phase was loading — drop it on the floor.
|
|
7146
|
+
if (!isCurrent())
|
|
7147
|
+
return null;
|
|
7114
7148
|
// Guard against a newer click reaching us mid-render.
|
|
7115
7149
|
if (phaseInfo.fileName !== loadedImage.filename)
|
|
7116
7150
|
return null;
|
|
7117
7151
|
return this.plotService.plot(this.plotDivName, loadedImage, phaseInfo, screenHeight, this.plotType, inPlace);
|
|
7118
|
-
})
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
//
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7152
|
+
});
|
|
7153
|
+
},
|
|
7154
|
+
smallShown: () => {
|
|
7155
|
+
if (!isCurrent())
|
|
7156
|
+
return; // superseded by a newer image
|
|
7157
|
+
// If the file is still being cached/prepared server-side, keep the full
|
|
7158
|
+
// cache-progress overlay up instead of dropping to the translucent "Sharpening
|
|
7159
|
+
// preview..." spinner over a blank canvas (a large uncached image renders its
|
|
7160
|
+
// small preview before its tiles exist). finished() releases the overlay once
|
|
7161
|
+
// the real render lands.
|
|
7162
|
+
if (this.isCaching)
|
|
7163
|
+
return;
|
|
7164
|
+
// Small tier on screen — drop the full overlay but keep a translucent spinner so
|
|
7165
|
+
// the blurry render isn't mistaken for the final image.
|
|
7166
|
+
releaseOverlay();
|
|
7167
|
+
this.sharpening = true;
|
|
7168
|
+
},
|
|
7169
|
+
sharpenSettled: () => {
|
|
7170
|
+
if (!isCurrent())
|
|
7171
|
+
return; // superseded by a newer image
|
|
7172
|
+
this.sharpening = false;
|
|
7173
|
+
},
|
|
7174
|
+
finished: (_viaSmall, logTag) => {
|
|
7175
|
+
if (!isCurrent())
|
|
7176
|
+
return; // superseded by a newer image
|
|
7177
|
+
// Idempotent — releases now if smallShown deferred it (caching) or was skipped.
|
|
7178
|
+
releaseOverlay();
|
|
7179
|
+
this.running = false;
|
|
7180
|
+
applyRoi();
|
|
7181
|
+
console.log(logTag);
|
|
7182
|
+
},
|
|
7183
|
+
sharpenFailed: (err) => {
|
|
7184
|
+
if (!isCurrent())
|
|
7185
|
+
return; // superseded by a newer image
|
|
7186
|
+
// The small tier stays on screen as the fallback — tell the
|
|
7187
|
+
// user the sharper version isn't coming.
|
|
7188
|
+
const msg = err?.error?.message || err?.message || err?.statusText || String(err);
|
|
7189
|
+
this.messageService.add({
|
|
7190
|
+
key: this.vizAlertToastKey,
|
|
7191
|
+
severity: 'warn',
|
|
7192
|
+
summary: 'Preview not sharpened',
|
|
7193
|
+
detail: `The full-resolution preview did not load (${msg}). The low-resolution preview is still shown. Try clicking the image again.`,
|
|
7194
|
+
});
|
|
7195
|
+
this.running = false;
|
|
7196
|
+
applyRoi();
|
|
7197
|
+
},
|
|
7198
|
+
}).render(imgInfo, smallImgInfo);
|
|
7157
7199
|
}
|
|
7158
7200
|
}
|
|
7159
7201
|
},
|