Repository navigation
Releases
v0.6.0 — dialog toolbar tools and brush classes
Released 2026-09-29.
Interactive toolbar tools, and brush classes. The first user is dianne-js: its DIANNE tool is a toolbar button next to the host's own actions, and it paints positive and negative strokes with the brush in blue and red.
Changed
TOOLBAR_TOOLSis typedreadonly ToolbarContribution[], the union of the run tools (ToolbarToolContribution, unchanged) and the new dialog tools. Providing tools is unaffected. Code that injects the token and reads run-tool members directly must narrow first, for example withvisibleToolContributions()orisDialogToolContribution(). That is a type-level break, so this is a minor bump under 0.x.visibleToolContributionsskips dialog tools (and anything withoutmodels()), andsortToolContributionsis generic.
Added
-
Dialog tools on
TOOLBAR_TOOLS(kind: 'dialog',ToolbarDialogToolContribution).- Button: placed right after the host's projected toolbar buttons, and shown in the Image view only. It is highlighted while its dialog is open.
- Dialog: clicking opens a floating, non-modal dialog. The tool fills it through
mount(host, ctx, session), with aToolDialogContext: the plot-mode context, withtoolsalways present.mountruns once the dialog has rendered andhostis in the document, so the body can measure itself. - Lifecycle: the same as plot modes, sharing the same controller.
- Closing the dialog, or clicking the button again, tears the body down and calls
deactivate()once. - A re-render restarts the session with the dialog still open.
- Leaving the Image view closes the dialog.
- A failed start closes it with a warning.
- Closing the dialog, or clicking the button again, tears the body down and calls
- Helpers:
dialogToolContributions()validates and sorts them, and help entries are listed with the other tools.
-
Brush classes (
IBrushOptions.label/color). With a label set, the brush paints regions of that one class:- New regions take the label and colour. The colour is marked
colorOverridden, so a preset re-apply keeps it. - A stroke only extends or merges regions with the same label, so painting one class never grows another.
setBrushMode(true, …)sets the class in full.setBrushOptionschanges it only whenlabelorcoloris passed, so the size slider keeps it.- Switching class drops the active region; the next stroke starts a new one.
- With neither set, the brush behaves exactly as before.
- New regions take the label and colour. The colour is marked
-
Toolbar tools for plot modes (
PlotModeContext.tools).armBrush(class),disarm()andactiveTool$go through the same path as the toolbar, so the brush button shows as active and its size control appears. Picking any tool from the toolbar gives the plain tool again.toolsis optional in the type, so a context built against 0.5.0 still type-checks.
npm: @jax-data-science/sci-image-visualizer@0.6.0
Full diff: v0.5.0...v0.6.0
v0.5.0 — contributed plot types
Released 2026-09-24.
The contributed-plot-types release: another package can add a mode to the plot-type selector at runtime. The first one is dianne-js's Digital Pathology - DIANNE (freehand DIANNE labelling over the image view). Backwards compatible: no public type changes shape, and a host that provides no contributions sees no change.
Added
- Contributed plot types (
PLOT_TYPE_CONTRIBUTIONS). A multi-provider token with no factory: nothing registers at import time, and a contribution is a plain object, so the contributing package needs neither decorators nor the Angular compiler.- Base type: a contributed mode rides on a built-in
baseType(in this version onlyPlotType.IMAGE, the OpenSeadragon view). It is plotted exactly as that type would be, with its toolbar, region tools and wheel handling. - Activation: once the viewport is ready,
activate(ctx)receives the publicIVisualizer, aPlotModeViewportand the image stream. - Deactivation: the session is deactivated exactly once, when the user leaves the mode, the image changes or the base view re-renders, or the visualizer is destroyed.
- Gates: descriptors take the
requiresGrayscale/requiresStack/requiresSpatialData/requiresSpatial3dgates, on top of their base type's own. - Side panel (optional): either an Angular component, which injects
PLOT_MODE_CONTEXT/PLOT_MODE_SESSION, or a framework-agnosticmount(host, ctx, session)function. It sits in the right-hand panel area. - Isolation: nothing a contribution throws or rejects escapes. Failing to start (
activate,mount, a panel component throwing as it is first rendered) falls back to the base type with a warning toast. So does failing to clean up (throw or async rejection) while the same mode is being re-activated. An explicit re-selection in the dropdown retries it.
- Base type: a contributed mode rides on a built-in
- A viewport to draw over, on the OpenSeadragon backend.
dataToClient(image px to client px) alongsideclientToData.- A per-redraw
frame$, coalesced to one emission per animation frame and idle while nothing listens, andsettled$(the existinggetViewportChange$). Each starts with the current visible rect, so an overlay draws without waiting for a pan. IVisualizergains an optionalgetPlotModeViewport().
VisualizerComponentfields:plotTypeMenu(the selector's full list: built-ins, then contributed modes) andselectedPlotTypeId(the selector's id, a built-in type or a contributed one).- New exports:
PLOT_TYPE_CONTRIBUTIONS,PLOT_MODE_CONTEXT,PLOT_MODE_SESSION,PlotTypeContribution,ContributedPlotTypeDescriptor,PlotModeContext,PlotModeViewport,PlotModeSession,PlotModePanel,PlotModeRect,PlotTypeOption,PlotTypeIdandisBuiltinPlotType.
Changed
- Unchanged public types:
VisualizerComponent.plotTypeOptionsremainsPlotTypeDescriptor[](the built-in types on offer) andselectedPlotTyperemainsPlotType. The latter is the built-in type on screen: the selection itself, or a contributed mode'sbaseTypewhile one is active. The selector now binds toplotTypeMenu/selectedPlotTypeId. - Internal: the unexported toolbar's plot-type input and output now carry
PlotTypeId, and it takes abasePlotTypeinput. Every rendering and tool decision still takes aPlotType: a contributed id is resolved to its base type first.
Fixed
These three are in the example app and tile server, which are not part of the npm package.
- Tile-server container: runs on Node 22.
lib/zarr3.mjsneedszstdDecompressSyncfromnode:zlib, which does not exist before Node 22.15, so thenode:20-slimimage died on import.engines.nodenow says>=22.15.0. - Pages smoke test: no longer fails the deploy against a tile server that predates the spatial routes. The app already handles that server by offering no spatial entries. A 4xx on that server's
spatial/paths is tolerated and named in the build log; every other 4xx still fails the test. - Demo dataset label: the synthetic demo dataset's gallery label now says the expression is fabricated ("SYNTHETIC · fabricated expression · Visium geometry (demo)"), so it is not taken for measurements next to the real datasets.
npm: @jax-data-science/sci-image-visualizer@0.5.0
Full diff: v0.4.0...v0.5.0
v0.4.0 — the spatial-omics plot modes
Released 2026-09-11.
The spatial-omics release. Two new plot modes:
Spatial omics— one marker per observation over the tissue image, coloured by an annotation column or a gene, with ROI-linked distribution charts. For a dataset whose 3D data is a registered volume, it draws the displayed plane's anatomy with that plane's observations over it, and the toolbar's slice slider scrubs depth.Spatial omics 3D— the same observations as a point cloud under an orbit camera, inside the dataset's reference volume, with screen-space region selection and optional per-cluster density volumes.
Both sit on a new spatial data plane (SPATIAL_DATA_PORT, SpatialDataset) that holds observations resident while column, feature and polygon values arrive one at a time. Volume and Isosurface now read the image stack and nothing else, and IImageMetadata gains mppZ so any stack that knows its slice spacing renders with true physical anisotropy.
Added
-
Dimensionality-reduction charts — UMAP, PCA and t-SNE, in 2D and 3D. The Distribution section gains an Embedding chart kind: the same observations laid out by a reduction rather than by their position in the tissue. A map answers "where", an embedding answers "what is like what", and reading a marker panel needs both.
Whatever the dataset publishes is offered, and each derived embedding records how it was made — the PCA it was computed from, the perplexity, the iteration count, the seed — because an embedding whose parameters are unknown cannot be compared with anything. PCA additionally reports the variance each axis explains.
The scatter is linked to the map both ways: lasso the embedding to select those cells on the tissue, and a class selected anywhere is highlighted here too. A 3-D embedding keeps its camera across a redraw, so recolouring does not throw away an orientation the reader set.
-
Compute a missing t-SNE in the browser, on the GPU. A dropped-in dataset usually carries one UMAP and nothing else. Where the dataset publishes a PCA but no t-SNE, the panel offers to compute one: in a Web Worker so the tab stays responsive, through WebGPU where available and WebAssembly SIMD otherwise, with a progress bar, a cancel button, and the backend named so a long wait has a visible reason.
It runs on the PCA scores, not the expression matrix — 0.51 MB against 185 MB for a whole-transcriptome dataset — so the server reduces and the browser embeds.
Gated by size, with the cost shown before you start. t-SNE is quadratic; 2,688 points measured 95 s on WebGPU, so the estimate is extrapolated from that anchor and the button reads
Compute (~95s). Past 5,000 observations the offer is withdrawn rather than left to be discovered — about 5–6 minutes is the most a reader can reasonably consent to from a button, and 19,416 points would be over an hour. -
A
?beside each chart kind, and a Plotting help dialog. Counts, the heatmap and the three embeddings each answer a different question, and two of them are routinely over-read — a UMAP's distances and a heatmap's unscaled colours — so the caveat is part of the explanation rather than a footnote. -
Detach the whole distribution area into its own window. Charts are read beside the map, not instead of it. The detach state belongs to the area rather than to each chart, so switching from a heatmap to counts to an embedding keeps the window it is already in.
-
Example tile server: a plain
.h5adis now a source, with no conversion command. CSC files are read directly; a CSR file is converted into the wire format on first open and the conversion is reused while it is newer than the file it came from. Dropping a file intoh5ad/is the whole setup. -
Example tile server: PCA and t-SNE for a dataset that publishes neither. PCA is computed on first request (randomized SVD, Halko et al.); t-SNE is available offline for the datasets where the browser's size gate declines.
-
Example tile server: the Visium H&E brain, an image and a published UMAP together — the case that exercises registration and embeddings at once. Its scale is fitted from the 100 µm spot lattice rather than taken from
spot_diameter_fullres, which corresponds to 65 µm on this slide and would put every marker 18% out. -
A gene × class expression heatmap in the Distribution section. Colouring the map by one gene answers "where is this gene"; it cannot answer "which classes express which genes", which is what a marker panel is read for. A new Heatmap chart kind draws the mean expression of each picked gene within each category of a grouping column — the standard mean-expression matrix (scanpy's
matrixplot), with its own gene multi-select for the rows since the other kinds chart whatever single source the map is coloured by.Offered whatever the map is coloured by, seeded with the gene already on screen, and it picks a grouping itself: a heatmap with no columns is not a chart, so "No grouping" is not a usable default here.
Three things it does so the panel is readable rather than merely plotted:
- Z-scored per gene by default (toggleable). Without it one highly-expressed gene saturates the scale and the rest reads as blank — the matrix would show which gene is loudest, not which class expresses what. Z-scored, the scale is diverging and centred on zero, because the sign is then the reading.
- A cell with no measurement is skipped, not counted as zero, and a group with nothing measured is drawn as a gap rather than as a low mean.
- Inside a small selection the columns become the cells themselves. A two-column class matrix is a poor answer for an ROI holding forty cells; "what is in front of me" is the question there. Groups with too few cells for a mean to mean anything are dropped.
Its colour scale is deliberately its own, not the map's
continuousColormap: a z-scored mean per group is a different quantity from a per-cell value, and sharing one scale would invite reading a heatmap cell as a marker colour. -
Hover a marker to read its class; click to select that class. A 34-entry legend cannot be read back from a dot — several classes get similar colours, and matching one to a swatch by eye is the task a tooltip removes. Hovering an observation in either spatial mode now names it, over the column the name came from; a click selects that whole class, exactly as clicking it in the legend does, and clicking it again clears, so a click is always reversible.
It reports whatever the cloud is CURRENTLY coloured by, so it and the legend cannot say different things: a categorical column gives the class, a gene or numeric column gives the value with its unit. With no colour source it stays silent — nothing is being said about the cells, so there is no cluster to name — and a gene cannot be clicked to select, because no set of cells "is" a value.
Hit-testing runs at most once per animation frame against positions cached until the scene or camera moves, so a pointermove does not cost a pass over 3.7M observations. A drag is told from a click by how far the pointer travelled, and a click is ignored while a region tool owns the pointer — placing a polygon vertex is also a click that does not move.
-
The continuous colour scale is pickable, from the library's own colormaps. Gene expression (and any numeric column) followed the image's colormap, which is usually a grey ramp — so the scale was chosen for the tissue and inherited by the measurement, with a Viridis fallback for the grey case. A
Colormappicker now sits under the colour bar in theSpatial omicspanel, offering the library'sCOLORMAP_OPTIONSwith the same gradient swatches the image's picker uses; clearing it goes back to following the image.One setting drives the markers, the 2D gene map, the 3D gene map and the panel's own colour bar, so none of them can disagree about what a colour means.
-
The gene map works in 3D, as one field per imaged section. Ticking
Gene mapinSpatial omics 3Destimates the selected gene's expression over the whole sectioned specimen and raymarches it: exactly the z planes that were imaged carry that slide's own 2D field, and the gaps between sections stay empty. A stack of measured fields at their true depth, rather than a smear.One map section at a timerestricts it to a single sheet — its own control, separate from the cloud's, so all the sheets with one section's cells over them and one sheet inside the whole cloud are both reachable.Volume rendering (interpolate along z)smooths the sheets into a continuous volume. That is a different object and the panel says so: the planes between the sections then carry an ESTIMATE, smoothed from their neighbours' mean, and nothing is drawn beyond the outermost section. It is the move the cluster density volumes make, with the same caveat — a field may be interpolated between sections, individual cells may not. The section restriction is ignored while interpolating, because a volume built from one slide would smear it through the specimen's whole depth and present that as an estimate.Estimated on the reference volume's own lattice, coarsened in-plane but never along z, so there is one plane per section and the field lands on the anatomy with no offset to correct. The in-plane bandwidth is a physical length anchored to the template's voxel, so a sheet and the 2D view of the same section are the same field. Windowing, the log scale and the colormap are shared with the 2D map and the markers, so the three cannot disagree about what a colour means.
-
The 3D scene's three layers can be shown and hidden independently. The reference volume, the observation cloud and the cluster density volumes occupy the same space, so any two of them hide each other — and 374k points drawn as a stack of discs hide the density volumes almost completely, which is the one thing the volum...
v0.3.3 — cellpose-js 0.6.0, napari-js 0.11.1, models under jax-image-tools
Released 2026-08-31.
Backfilled: 0.3.3 was published without an entry.
Changed
-
cellpose-jsbumped to^0.6.0(from^0.5.0) andnapari-jsto^0.11.1(from^0.11.0), in bothdependenciesanddevDependencies. No source change was needed. The bump is covered by the ng-packagr build type-checking against both packages' published.d.ts: the unit tests mapnapari-jsto the WebGPU-free stub insrc/lib/testing, and reachcellpose-jsonly through a type-only import plus the lazyawait import('cellpose-js')inCellposeSegmenterService. -
Hosted model weights now live under the
jax-image-toolsHugging Face organization rather than a personal account: the default cellpose-SAM checkpoint (cpsam_fp16.onnx) and all fourSAM_MODELSentries — micro-sam ViT-T and ViT-B, patho-sam ViT-B and its int8 variant. Hosts that override those URLs throughsetModelUrlor their ownSamModelDefare unaffected.
npm: @jax-data-science/sci-image-visualizer@0.3.3
Full diff: v0.3.2...v0.3.3
v0.3.2 — JSON region bounds rehydrated in the store
Released 2026-08-13.
Fixed
-
Regions handed to the store as JSON now render on every backend (jit-ui#124). A
boundsthat came throughJSON.parse— a host's server response, astructuredClone— has the right fields but not the right prototype, and the renderers disagreed about whether that is acceptable:Region.getShape()(Plotly) and the napari overlay duck-type the bounds, while the OpenSeadragon overlay discriminates withbounds instanceof Rectangle. So a JSON region drew in Heatmap mode and silently vanished in Image mode.instanceofalso gates the store's geometry de-duplication,moveRegion, and the GeoJSON export, so the same regions were undraggable, re-appended on every repeat, and missing from a save — in every mode, unreported because they were at least visible.RegionStorenow normalizes on the way in (setRegions,addRegion,enterStackMode) via the exportedhydrateBounds, so the ~20instanceofcall sites downstream stay valid rather than each having to duck-type. Instances are passed through by reference — overlays and tools mutate the bounds they created during a drag.hydrateBoundsalso fills in what a lean serializer omits (JIT's JavaPolygonSerializerwrites onlynpoints/xpoints/ypoints, while the GeoJSON export readscoordinatesand the overlays readclosed).
npm: @jax-data-science/sci-image-visualizer@0.3.2
Full diff: v0.3.1...v0.3.2
v0.3.1 — cellpose-js 0.5.0
Released 2026-08-12.
Backfilled: 0.3.1 was published without an entry.
Changed
cellpose-jsbumped to^0.5.0(from^0.4.1), in bothdependenciesandpeerDependencies.
Fixed
- The
examples/tile-serverdeployment pulls its image without a service-account key, and its docs/manifests carry placeholders rather than real identities — this repo is public. No effect on the published package;examples/is not part of it.
npm: @jax-data-science/sci-image-visualizer@0.3.1
Full diff: v0.3.0...v0.3.1
v0.3.0 — extensible toolbar, closed model packages removed
Released 2026-08-11.
Removed
-
The YOLO and retinal-layer tools, their model registries, and the
yolo-segdetect-js/jax-ai-jsdependencies. Their checkpoints are not open, so a library that named them could not be published. The built package now has no import edge to either — the remaining mentions are the doc comments explaining this.This includes the ResUNet-a 20x/40x checkpoints, which were briefly enabled here during this cycle and never released: they still run in a browser, just not from this package.
What made the coupling small was that the contracts were already clean. The only thing pulling either package in was the default factory on
INSTANCE_SEGMENTER/SEMANTIC_SEGMENTER, which fell back to an in-library service. Both tokens now have no default: unprovided means the capability is absent.The tools themselves are unchanged and move to
@jax-data-science/sci-image-visualizer-jax-tools, which a JAX host registers.
Added
-
A toolbar tool-contribution mechanism (
TOOLBAR_TOOLS,ToolbarToolContribution), so tools can be added from outside this library rather than hardcoded into the toolbar template.A contribution describes itself — icon, tooltips, checkpoints, parameters, help text — and supplies a
run. The toolbar renders the split button and model menu from that, and the help dialog builds its tool list from the same descriptors, so an open build's help no longer promises buttons that are not there.Parameters are declared (
ToolParamSpec) rather than drawn: one generic dialog renders every tool's fields. A plugin shipping its own dialog component would have to match this library's exact PrimeNG version and styling to look right, and the two dialogs this replaces were entirely numbers plus one checkbox. It is the same vocabulary jit-ui already uses for pipeline step parameters.Returning an empty model list hides a tool, which is the switch for "the weights are not configured in this deployment" — it keeps the tool's help and parameters registered without showing a button that cannot run.
Added
-
Per-model info in all three model pickers. Every item in the SAM, YOLO and retinal-layer dropdowns now carries an info icon whose hover tooltip describes that checkpoint: download size, speed, training domain, measured accuracy, and — for the tiled detectors — why its overlap and merge defaults are what they are.
This replaces the single "About the SAM models" panel (removed below). That panel sat behind an extra click, had to restate every model's name in order to say anything about it, and covered only SAM — YOLO and retinal shipped with no explanation at all. Attaching each description to the row it describes settles all three at once, and puts the text where the choice is actually made.
Copy lives in
toolbar/model-info.ts, keyed by registry model id, and rides on the menu item'stooltipfield (whichp-menu's own rendering ignores — it readstitle). A model with no entry renders no icon, so a host that registers its own checkpoint degrades quietly instead of showing an empty tooltip. The numbers quoted there come from the registries (sizeMb,miou,patchSize, per-model tilingdefaults) and need updating alongside them. -
A dedicated toolbar icon for the retinal-layer tool, replacing the
pi pi-align-leftplaceholder: corner brackets — the framing the YOLO detector already uses, marking the no-prompt tools that sweep the whole view — around an eyeball whose posterior wall carries the segmented layers. -
Purpose-built icons for the two SAM prompt tools,
sam-box-prompt.svgandsam-point-prompt.svg. The box tool wore a generic "crop a photo" glyph (a house behind a dashed marquee) and the point tool a stock cursor-with-sparkles; neither said segmentation, and nothing tied the two tools together.Both show the same subject — an outlined blob holding three cells, which is
cells.svg's vocabulary — so the only difference between the icons is the difference between the tools: a rectangle drawn around the object, versus a cursor clicking on it (with tail and click rays, as inclick.svg). Corner brackets are deliberately not reused here; they mark the no-prompt tools.The point icon's interior is laid out around the cursor rather than the reverse: the cursor sits wholly inside the blob, because where it straddled the outline the outline's black band filled its concave notch and flattened it into a plain triangle; and the three cells are placed off the diagonal the cursor and its rays occupy, because a ray crossing a cell fuses into a lollipop. Both constraints are geometric rather than enforced anywhere, so moving a cell or resizing the cursor needs a look at the result, not just at the numbers.
Each icon ships as one object per component (blob, each cell, cursor, each click ray), labelled for Inkscape's Objects panel, so a piece can be moved or turned without unpicking a merged path. Every object carries an identity
rotate(0, cx, cy)about its own centre, so setting an angle spins it in place rather than swinging it around the canvas origin. Stacking is document order — the cursor paints over the blob — rather than the winding interactions a single path would need. One thing stays merged: a ring (frame, blob) keeps its outer and inner contour in one path, because the inner contour is its hole and separating them turns the ring into a slab.A thin gap separates the cursor and rays from what they overlap. It is a hole cut into those shapes, not white paint — the toolbar recolours icons with
filter: brightness(0) invert(...), which blackens every painted pixel before tinting, so white would tint to the icon colour and disappear. That makes the gap a property of the shapes underneath rather than of the cursor: move or rotate the cursor and the notch stays where it was, and the cut has to be redone against the new positions. Which objects need cutting depends on the layout — currently the blob and two of the three cells.The paths use nonzero winding with inner contours reversed, rather than the
evenoddthe other icons use. That is load-bearing: the arrow overlaps the blob outline, and under evenodd an overlap cancels — an earlier attempt rendered a checkerboard where the two crossed. Under nonzero the overlap fuses while the holes still read (hole = +1−1 = 0; a cell inside it = +1; arrow over the ring = +2). Rings are explicit outer+inner contours, so there is no stroke-to-path step to redo when the geometry changes.Caveat: at
width: 1remon a 1× display these resolve less crisply than the solid-blob draft they replaced — an outline plus three small circles is more detail than 16 device pixels can hold. They are clear from roughly 24px up.picture-segmentation.svgandclick.svgare left in the published asset set — a host may reference them directly — but nothing in this library uses them now.
Changed
-
cells.svg(the cellpose tool) is now framed by a rounded square rather than a circle, reusingsam-box-prompt.svg's frame proportions exactly — same inset, corner radius and weight, scaled into this file's canvas. Cellpose is a prompted tool that runs inside the rectangles you draw, so it belongs with the box-prompt framing rather than looking like a petri dish.It reads better small as well: at 16px a straight axis-aligned edge rasterises crisply where the circle went soft and wobbly. The six cells are untouched, so their own small-size mushiness is unchanged.
-
The help dialog documents the no-prompt tools. Its segmentation section described only SAM and cellpose, so YOLO detection and retinal-layer segmentation were absent from the one place a user goes to find out what a button does. It now splits the tools by shape — prompted ones act on rectangles you draw, no-prompt ones sweep the current view — which is the distinction that explains why two of the buttons want nothing selected first.
It also no longer points at the removed info button, and the general SAM material from that panel (encoders differ, prompting does not; each tool loads its own encoder, so the first click after switching re-encodes once) moved here rather than being dropped.
Removed
- The "About the SAM models" info button and its overlay panel, superseded by the per-model tooltips above.
npm: @jax-data-science/sci-image-visualizer@0.3.0
Full diff: v0.2.19...v0.3.0
v0.2.19 — jax-ai-js ^0.2.0 (dedupe)
Released 2026-08-09.
Changed
-
jax-ai-js^0.1.1 → ^0.2.0. Its stain-normalization moved out tostain-normalization-js; this library only ever used the inference API, so nothing here changes behaviourally.The reason to bump rather than leave it: a consumer on
jax-ai-js0.2.x ended up carrying two copies — 0.2.x hoisted, plus a nested 0.1.x for this library — which a bundler then emitted as two lazy chunks of the same code.
npm: @jax-data-science/sci-image-visualizer@0.2.19
Full diff: v0.2.18...v0.2.19
v0.2.18 — YOLO region holes + abortable model load
Released 2026-08-09.
Fixed
-
YOLO detections keep their interior voids.
YoloDetectToolServicebuilt eachRegionfrompoly.exterioralone, so a mask with a hole through it was committed solid. This does not look like lost geometry, it looks like a detection that over-covers — which is the harder failure to notice.Holes now travel through the same view→image transform as the exterior; a ring left in view-local pixels lands elsewhere on the slide, frequently outside its own exterior. Each polygon already owned its own rings (the tracer emits them that way, and the segmenter service mapped them through), so a mask that splits into several polygons under thresholding needs no disambiguation — the rings were simply being dropped at the last step.
Rings that simplification reduces below three points are dropped rather than emitted as geometry that bounds nothing, and
holesis left unset rather than set to[], so a solid region does not round-trip through export as a donut with no rings.
Added
-
RetinalSegmenterService.getModelaccepts anAbortSignal. The retinal checkpoint is ~590 MB; previously a cancel reached the inference but not the download, so the transfer kept running and the UI sat in "Cancelling" until it finished.segmentSemanticpasses the run's own signal through.Cancellation lands within one graph compile rather than instantly: ORT session creation cannot be interrupted once started, so the signal is rechecked after the download and before the session is built.
A cancel deliberately does not abort a load already in flight. That promise is shared — the toolbar tool and a host's pipeline both use this service — so one caller's cancel must not tear the model out from under another. A second caller joining an in-flight load simply waits.
Changed
jax-ai-js^0.1.0 → ^0.1.1, which is where the signal plumbing lives:fetchModelalready honoured a signal, butfromPretrainednever passed one. No API breaks; the option is additive.
npm: @jax-data-science/sci-image-visualizer@0.2.18
Full diff: v0.2.17...v0.2.18
v0.2.17 — retinal-layer segmentation tool
Released 2026-08-09.
Added
-
Retinal-layer segmentation — a toolbar tool, a parameter dialog, and
ISemanticSegmenter/SEMANTIC_SEGMENTER, backed byjax-ai-js.A third segmentation port rather than a reuse of the other two, because the output is a genuinely different shape: cellpose separates touching instances of one class, YOLO returns overlapping classified objects with a confidence each, and this returns layers that tile the image, never overlap, and where two disconnected patches of a class are one finding rather than two objects. There is no per-object confidence and no NMS because there are no objects.
Only the VNet checkpoint is enabled. Against the ground-truth masks shipped with them, VNet scores mIoU 0.9061 and the three ResUNet-a variants 0.27–0.31. That is not quantization damage — each ONNX export reproduces its own Keras original exactly. They disagree with the masks, folding ground-truth class 1 into their class 2 and class 3 into background, and no relabelling rescues them (the best of all 24 permutations reaches 0.36). Their weights are unpublished pending that question, so their registry entries carry an empty
modelUrland a specific reason. They are listed rather than deleted so a host can show them as unavailable instead of pretending the server's choice does not exist.WebGPU only, with no fallback: ORT-Web's WASM EP dies with
std::bad_allocon these graphs at 512², at every precision.WebGpuRequiredErroris re-thrown unchanged so a host can show a real unsupported-browser message.An empty result distinguishes "found nothing" from "was unsure everywhere" — the second is the signature of the wrong magnification or wrong preprocessing, and reported as a plain empty result a misconfiguration is indistinguishable from an empty field.
npm: @jax-data-science/sci-image-visualizer@0.2.17
Full diff: v0.2.16...v0.2.17