Changelog
September 4, 2026 · View on GitHub
All notable changes to webSMLM (the single webSMLM.html), newest first.
This is the per-release log, including implementation detail worth keeping on
record (specific settings, and notable approaches that were tried and
rejected). The forward-looking roadmap — things worth remembering and
testing later, not history — lives in
docs/REFACTOR_PLAN.md.
DOI column: a GitHub release archives the version on Zenodo with its own
version DOI. Per the project's cadence a release is normally cut for minor bumps
(0.x.0); patch releases (0.x.y) usually ship without a new DOI. The concept
DOI 10.5281/zenodo.21445041 always
resolves to the latest.
| Version | Date | DOI | Summary |
|---|---|---|---|
| 0.12.0 | 2026-09-04 | ✓ | Live streaming (experimental) & precision-informed rendering. Live streaming (window.webSMLM.liveStream) lets an external process push frame chunks in as they're acquired — from a running Micro-Manager/pycromanager acquisition, for instance — each localized and rendered into the reconstruction live, no full stack loaded upfront. Two ways in: an opt-in WebSocket the page connects out to (for hooking into a tab already open, tools/test_livestream_demo.py), or a Playwright-driven bridge (tools/webSMLM-livestream-bridge.mjs) for a fully automated/headless session. The top-level Stop button ends a session either way it was fed, closing the WebSocket first if one's open; drift correction/NeNA/FRC and the locs table are all available while a session is still running, the same as after a normal Localize — committing a new table filter (or the reconstruction panel's crop tool) is refused while streaming, since either would freeze the display on a stale snapshot while the real dataset kept growing underneath it. The periodic reconstruction render is adaptively time-based (the same mechanism a normal Localize run's own live preview uses), not a fixed frame count. New this cycle and not yet as battle-tested as the rest of the app — marked experimental. Render mode (Rendering settings) adds two alternatives to the reconstruction's rendering pipeline: precision (new default) renders each localization as its own Gaussian sized by its real fitted CRLB precision — Picasso's own convention — instead of one uniform blur over the whole buffer; fixed keeps the old uniform-blur behaviour; dither is a much faster stochastic approximation for large/dense datasets (jitters each localization by one seeded draw from its own precision and bins it — the classical Average-Shifted-Histogram/Monte-Carlo-kernel-density argument — measured 10–24× faster than the analytic splat on a real 4.2M-localization dataset), grainier on sparse data so not the default. Reconstruction rendering itself now runs on a dedicated Worker+OffscreenCanvas instead of blocking the main thread, fixing a real regression where a large/dense Run's live preview could get permanently stuck on "Rendering…" (the adaptive preview-interval measurement was reading near-zero once rendering became async, so it kept firing overlapping renders that each discarded the last before finishing). |
| 0.11.13 | 2026-09-01 | — | A per-emitter angle column, a drag-to-reposition depth-colour bar, auto-picked 3D/sSMLM colouring, a true recorded-command CSV path, and a fully rewritable log window. Gauss MLE 3D rotated elliptical now exposes the rotation angle it already fit — a new "angle [deg]" table/CSV column (present whenever any loc has one), wired through the worker-pool message protocol too (widened 14→15 floats at all three sites that must move together), where it had silently never survived a parallel Run before. Depth-colour bar: the z/sSMLM-distance legend used to auto-anchor to the data's own extent, converted through the live pan/zoom transform on every draw — reading fine at fit-zoom, but visibly drifting once zoomed away from that extent (reported). Standard position is now simply the panel's right margin, vertically centred on the current viewport — trivially screen-stable, matching the SPT D-legend's own fixed-margin anchor — and the bar is now click-and-drag repositionable anywhere in the panel, staying exactly put through any further pan/zoom. Auto colouring: "Colour by depth (z)" now auto-checks itself, and the colour map auto-defaults to turbo (3D) / HSV blue-loop (sSMLM-paired), the moment a result actually has that data — not only when a 3D method is picked in the dropdown or Pair is clicked, closing a real gap for a CSV reload of existing 3D/paired data. Loading a movie/CSV no longer auto-opens the Localizations table. Drift x/y path plot now draws into a true square sub-rect (equal nm/px on both axes, like the SR panel's own reconstruction) instead of squeezed to whatever aspect ratio the loaded movie happens to have — sharing one nm value range across both axes wasn't enough on its own, since the raw plot rect's width and height are almost never equal. CSV loads are now genuinely recorded: analyze()'s config.file accepts a .csv directly (parsed via the same code the interactive CSV load uses, skipping Localize/crop/PCFO/calibration but running drift/NeNA/FRC/pairing/tracking/export exactly as it would on a fresh Run's own output) — loading a CSV interactively now logs a real, replayable analyze({file:"x.csv",...}) command, where before it logged nothing at all. Log window: the "Show as CLI"/"Show as JS" toggle (renamed from "Record as …" — it only changes how the same already-logged commands are displayed, it never records anything) now rewrites the entire existing log in place, not just lines written after the click; the version-pill seed banner is now a genuine, commented log entry too, instead of the one line in the log with no // /# marker at all. UI consistency: Gain & offset estimation's separate "Transfer estimates" button is gone — a new "Get estimate" button sits directly under Gain/Camera offset in Localisation settings instead, running the PCFO estimate first if needed, matching spt's own "Get from NeNA" pattern. Smaller fixes: the shared column histogram's Y-axis now uses the same matplotlib-style "×10ⁿ" scientific notation PCFO's own plot already did, for large counts; a genuinely inverted code comment on the worker-batch-size tuning knob (workerBatchTarget) was corrected (higher, not lower, gives finer-grained load balancing). |
| 0.11.12 | 2026-08-31 | — | A crop-tool bug fix, recorded-command coverage for cropping, and a large source-comment readability pass. Bug fix: the raw-panel crop tool deliberately stays "armed" after a crop (so another region can be picked without re-clicking the tool), but a click meant for panning/zooming a histogram — or any other raw-panel plot — could get silently reinterpreted as a crop-corner click, applying an unwanted extra crop of the already-cropped stack. Fixed by checking the panel is actually showing a live frame, not a plot, before treating a click as a crop gesture. Recorded commands: the crop tool now logs its own directly-runnable analyze({cropX0, cropY0, cropX1, cropY1, ...})/CLI-equivalent line (cropping was already headlessly supported, just not visible in the log), and — more importantly — Localize/Estimate/Calibrate's own recorded commands now include the active crop bounds automatically, so replaying a logged command while a crop was in effect no longer silently analyses the wrong region. Comment cleanup: a systematic pass over every module in webSMLM.html gave every previously-uncommented function (~90 total) a one-line intro and compressed dozens of long narrative/duplicated comments down to their actual lesson, while leaving real reference material (citations, documented gotchas, the headless API config reference) untouched — comments only, no behavioural change, verified with a full regression run against the bundled Sample2 L. lactis sample (identical localization count, drift span, and track statistics before and after). Also: docs/REFACTOR_PLAN.md gained two researched roadmap entries (loadable/in-app sCMOS per-pixel calibration maps, comparing the Huang/Picasso and ACCéNT approaches; and an smFRET/ALEX integration sketch mapping the workflow onto existing sSMLM-pairing/3D-calibration/streaming-export building blocks) — planning only, no code changes. |
| 0.11.11 | 2026-08-30 | — | Streaming per-record exports, drift-correction Stop support, an AIM speed/precision knob, an SPT ensemble MSD-vs-lag plot, and a macro-recording log — plus a real drift-correction bug fix. Bug fix: aimDrift2D()/aimDriftZ() segment binning was keyed off the absolute frame number rather than the first frame actually present in the localizations — a Localize run restricted to a sub-range (e.g. skipping the first 5000 frames) produced thousands of empty leading segments, each silently pinning its drift estimate to a full-search-radius corner and corrupting every segment after it. Fixed by binning relative to the first analysed frame instead. Headless/CLI: window.webSMLM.analyze() gains config.onRecord + four new boolean flags (exportTrackData, exportSSmlmCandidates, exportCalibrationPoints, exportPcfoTiles) that stream large per-record datasets (per-track MSD curves, sSMLM candidate pairs, calibration bead points, PCFO tile points) in bounded batches via a live callback rather than the return value — avoiding the cost of a huge JSON blob crossing the DevTools Protocol when CLI-driven; tools/webSMLM-cli.mjs writes each stream to its own .ndjson file. Correct drift (AIM) now supports Stop: pressing it shows the drift curve computed so far but does not apply it to the reconstruction/export, so slow settings (a large dataset, a small segment size) can be judged without waiting out or committing to a run you might discard anyway; AIM's two internal rounds truncate gracefully (round 2 needs every segment's own round-1 result, so a round-1 stop skips it entirely) and the plot marks where estimation actually stopped. A new AIM sample % control (default 100%, no behaviour change) deterministically subsamples each drift segment's own localizations before the search on a large, slow dataset — verified against synthetic ground truth that a moderate percentage only modestly increases estimation error, while a floor (200 locs/segment) prevents thinning an already-small segment into the known empty-segment failure mode above. SPT gains an ensemble MSD-vs-lag plot (computeEnsembleMsd()/fitMsdLine(), scroll/pinch zoomable) generalizing beyond the lag-1-only D estimate for longer tracks — now the default view shown after Track (falling back to the D histogram, then track length, if there's not enough data). Log window: a new "Record as JS/CLI" toggle makes every config-driven action (Localize, Correct drift, 3D calibration, …) log a directly copy-pasteable analyze({...}) call or CLI invocation right before it runs, with ordinary log prose now hard-wrapped so every visible line — not just the first of a long one — carries its own // /# comment marker, so a whole log stretch can be pasted straight into a script. Smaller polish: the Localize timing breakdown and summary lines now report whole seconds instead of raw milliseconds; the raw-panel Contrast readout shortened to B & W: 2399-9280; a Read the Docs link was added next to MIT License/GitHub in the header. |
| 0.11.10 | 2026-08-30 | — | Faster Gaussian MLE fitting, contributed by Hazen Babcock (PR #7). The spherical and axis-aligned-elliptical Gaussian MLE fitters (gaussmle/mle3d, the shared mleNewtonFit() Fisher-scoring driver) gain a separable fast path: since these two models' per-pixel value/Jacobian factor cleanly into independent x- and y-axis terms, the erf/exp terms needed for every pixel in a row (or column) are hoisted out and computed once per axis instead of recomputed for every pixel — the rotated-elliptical fitter (gaussmleEll) is unaffected, since a rotation genuinely couples x and y and can't be separated the same way. Verified independently (not just the PR's own claim): checked the new code line-by-line against the existing per-pixel model implementations for exact algebraic equivalence, confirmed the fast path only engages for the two applicable models (identity-checked function references, correctly excluding the rotated-elliptical closure), and re-ran the exact same single-threaded CLI fit against the bundled Sample2 L. lactis sample on both the pre-PR and post-PR code — 1.89x faster (9648ms → 5110ms) with a byte-identical result.csv (same MD5 checksum, 73,355 localizations). No worker-side changes were needed; the helper functions this reuses were already present in the worker's own code bundle. |
| 0.11.9 | 2026-08-27 | — | Alt (Option on Mac) keyboard hotkeys. Hold Alt — the same physical key macOS labels Option/⌥ — and numbered hints appear over the 10 always-visible top-level action buttons (Load movie/data, Simulate movie, Load/Save settings, Localize, Stop, Save data, Save plot/image, View data/filtering, Quick guide), in on-screen order; tap the matching digit to click that button. Add Shift and the same 10 digits instead open (or, on a second press, close) one of the 10 collapsible sidebar module sections, in on-screen order, scrolling to and focusing its <summary> on open so the very next Tab press lands directly on that section's first input. Alt, not Ctrl, deliberately — Ctrl+1..9 is already bound to browser tab-switching on Windows/Linux, so the page would never see those keydowns there; a modifier-gated scheme also needs no focused-input guard against the app's many free-typable numeric fields, since Alt+1 is a different keystroke from typing "1". Numbering is fixed to on-screen position rather than renumbering around what's currently enabled, so muscle memory (e.g. Alt+5 = Localize) stays valid regardless of app state; a hint simply doesn't render for a button that's disabled or a section not currently on screen. Bug fix caught before release: macOS remaps e.key for the digit row while Option/Alt is held (Option+2 sends "™", not "2"), so an initial e.key-based implementation showed the hint badges correctly but silently never fired on Mac — fixed by matching e.code ("Digit1".."Digit0", the physical key, unaffected by character remapping) instead, verified against a synthetic event reproducing the real macOS behaviour. Hint badges use a fixed blue (#0969da, light theme's own accent) rather than the active theme's own accent colour, so they read identically in dark/light/high-contrast — the same "overlay stays fixed across themes" convention already used for raw-frame overlays and the tracks-colour legend. Known gap: on the mobile/floating sidebar drawer (collapsed by default), Alt+Shift+N still opens the target section, just invisibly until the drawer itself is shown. |
| 0.11.8 | 2026-08-27 | — | Maintenance release: a real mobile TIFF-loading crash fixed, headless per-column histogram export, a filter-autocomplete gap closed, and a documentation cleanup pass. Bug fix: loadTiffFile() picked whole-file-in-memory vs. streamed loading purely by a hardcoded ~1.5 GB file-size ceiling, completely disconnected from the existing Memory budget (GB) setting (which only gated decoded-frame caching further downstream, too late to help). A real, reported case: a 680 MB multi-IFD TIFF crashed on mobile Safari, while the bundled 4.9 GB Leterrier dataset — 30×+ larger on disk — opened fine on the same device, because the smaller file stayed under the ceiling and read its ENTIRE raw buffer plus every frame's IFD metadata into memory up front, while the larger file was always forced onto the always-chunked streaming path regardless of its own size. Fixed by tying the threshold to the Memory budget setting too (Math.min(1.5GB, readBudget())) — unchanged at the 3 GB default, so desktop behaviour (including the original reason the 1.5 GB ceiling was raised) is untouched, but a user on a memory-constrained device can now lower Memory budget (GB) and actually have it apply here; a clear log line now explains when a tight budget — not a genuinely huge file — is what pushed a file onto the streamed path. Since even the DEFAULT 3 GB budget was still enough to let a real-world 680 MB file take the whole-file path (and crash) with no reason for a first-time mobile user to know to lower the setting themselves, Memory budget (GB) now also defaults to 0.5 GB automatically on a narrow/mobile viewport instead of 3 GB (same viewport check the mobile sidebar drawer already used) — desktop is unaffected; not independently confirmed against the actual reported device at time of release, only via Playwright at matching viewport widths. New: window.webSMLM.analyze()'s headless API gains config.exportHistograms: string[] (also --exportHistograms photons,sigma,bg on the CLI/autorun) — renders the shared column histogram for any named per-localization column with no browser window needed, landing in result.plots as hist_<column> PNG+SVG pairs; independent of the existing config.exportPlots, usable with or without it. Show track data's filter box now gets the same autocomplete as the main View data/filtering table (factored into one shared wireFilterAutocomplete() both boxes use). Documentation: a substantial trim/cleanup pass across CLAUDE.md, docs/REFACTOR_PLAN.md, and experimental_data/README.md — condensed narrative that had accumulated across many iterative rounds down to current-state-plus-reusable-gotchas, fixed a widespread word-truncation artifact from a manual editing pass (~55 instances, e.g. unset→unse), merged a duplicated roadmap entry, and closed a couple of now-dangling cross-references. |
| 0.11.7 | 2026-08-25 | — | SPT tracks overlay on the SMLM reconstruction, a "Show track data" per-track table, and a round of reported-bug fixes. New Show tracks toggle (spt) plots a filtered/sampled subset of tracks as thin polylines directly on the reconstruction (drawTracksOverlay()), styled to match the user's own sptPALM-Python (plot_cells_locs_sptPALM.py/plot_single_cell_analysis_sptPALM.py): magenta by default, or coloured by each track's own mean D via the Fire (hot) ramp (Colour tracks by mean D, checked by default, with a colour-scale legend); a filled circle marks each track's own start point, and its track_id sits beside it in white on a semi-transparent backing box for legibility. Tracks are clickable (highlighting magenta/green depending on colour mode) and filtered by Min track length then sampled by Show tracks (%) — deterministically, via a fixed-seed PRNG draw over the full track list, so the same dataset always shows the same track identities and raising either the length threshold or the percentage only ever removes/adds tracks, never reshuffles them (fixing a real, reported bug where the sampling draw was computed over the length-filtered subset instead of the full list, so raising Min track length could make an unrelated track spuriously appear or vanish). Turning the overlay on switches the reconstruction to the Grey colour map, matching the reference pipeline's own plain-background convention. New Show track data button opens a sortable, filterable table of the per-track summary (track_id/n_locs/D_coeff/mean x/y/first/last frame — the same rows Save track data writes to CSV), reusing the main View data/filtering table's own filter grammar; deliberately a separate, minimal v1 implementation, visually matched to the main table (row striping, sticky header, same modal width) after an initial mismatch was reported. Bug fixes: a real, reported line-thickness bug in the tracks overlay — an initial mag*view.zoom formula was unbounded at high zoom and, combined with the canvas's default miter line join, turned sharp track turns into giant spikes when zoomed in to inspect one track; fixed to view.zoom alone (one reconstruction pixel's own on-screen size), clamped and rounded-join regardless. A canvas-state leak left the D colour-bar legend's own border inheriting the tracks overlay's zoom-dependent line width. computeHist()'s x-axis (used by the D and track-length histograms, among others) previously set its right edge exactly at the data max, so a single long-tail outlier bar sat flush against the plot border and was effectively invisible — fixed with a 5% right-edge headroom margin, mirroring the existing Y-axis headroom factor. The mobile/floating sidebar toggle could vanish off-screen after scrolling then resizing (mobile browsers fire a resize event when the address bar collapses/expands mid-scroll, and the toggle's position:fixed offset was computed from a viewport-relative, not scroll-invariant, measurement) — fixed by adding window.scrollY back into the measurement. Resizing the browser window stopped re-fitting the SMLM reconstruction to the panel after the user had ever manually zoomed/panned once (which is almost always, on any real dataset) — a resize now always re-fits, independent of that state. Small button-label consistency renames (Save plot/image, View data/filtering, Localization precision (NeNA & FRC)). |
| 0.11.6 | 2026-08-24 | — | Segmentation-aware single particle tracking (cell-by-cell, headless too), a sidebar/button consolidation pass, and a round of reported-bug fixes. New Segmentation image workflow in spt: Apply segmentation? reveals Load segm. image, loading a separate integer-labelled mask (0=background, 1/2/3/…=cell) through the same loadTiffFile() any movie goes through; computeSegmentedImageData() builds a per-cell {id,cx,cy,areaPx} table (verified numerically exact against an independent numpy computation on the real bundled bf_analysed_JH_procBrightfield_segm.tif, 111 cells), rendered with seeded-random cell colouring (shuffledLabelColors(), ports the idea behind the user's own sptPALM-Python/helper_functions.py) so adjacent cells stay visually distinct. Track now links each qualifying cell's localizations SEPARATELY when segmentation is applied (linkTracksPerCell(), ported from apply_cell_segmentation_sptPALM.py/tracking_sptPALM.py's own use_segmentations branch) — a track can never cross a cell boundary; Min./Max. cell area (px) (default 50/∞) gate which cells qualify, and every localization gains cell_id/cell_area CSV/table columns. The SR-panel segmented-cells overlay went through several rounds of direct user feedback before landing: an Add overlay semi-transparent blend was hard to read, then an opaque-cells-plus-white-points redesign was reported as worse than the familiar reconstruction, before settling on the shipped design — opaque segmented cells with the SAME density reconstruction drawn on top, its background made highly transparent by luminance (floored so sparse/isolated localizations stay visible against a bright cell colour). Two real, reported bugs surfaced and fixed along the way: an exact-dimension-match gate silently drew nothing for the common case of a mask a few px off from the movie's own size (now "warn, don't block", matching the app's own convention); and the overlay's pxnm-rescaling direction was backwards on first ship (correcting Pixel size upward shrank instead of grew the segmentation's on-screen coverage) — caught only by direct user testing against real data, not by re-derivation on paper. Finally wired headlessly: config.segmentationFile/tools/webSMLM-cli.mjs's --segmentation <mask.tif> switch sptTrack to cell-by-cell tracking through a new dedicated #segmentationFileInput conduit, which surfaced a latent bug — linkTracksPerCell() read per-cell area off an interactive-only module-level global instead of taking it as a parameter, silently breaking headless use with no error — fixed by recomputing it from the passed-in labels. Verified end-to-end through the actual CLI subprocess against the real bundled L. lactis movie + segmentation mask. A related coordinate bug was tracked down together with the user: Load data (CSV) used to shift every localization's x/y by a cosmetic centering offset, which silently broke alignment against a segmentation image loaded from a separate source expressing the same original camera coordinate frame — reverted so (0,0) always means the same physical pixel it meant in the original file, confirmed via a zero-difference CSV export→reparse round trip on 569,648 real localizations. Sidebar consolidation: Show D histogram/Show length hist. (spt), Show dist. hist./Show angle hist. (sSMLM), and Show segm. image/Show area hist. (segmentation) each merged into one button with a toggle next to the raw panel's own title (mirroring the pre-existing drift-plot x/y-path toggle); Load movie/Load data merged into one Load movie/data button dispatching on file extension, refusing a selection that mixes a CSV with movie files rather than guessing; README.md cut from 374 to 120 lines, trimmed of content now owned by docs/DOCUMENTATION.md/docs/REFACTOR_PLAN.md. Several small renames/relocations landed and, in a couple of cases, partly reverted on follow-up request — Quick guide (renamed from Help & guide) briefly moved into the header before returning to the sidebar; Show segmentation was renamed Show image after it wrapped to two lines next to Load segm. image; Get from NeNA/Save tracking data were repositioned/renamed for clarity. Bug fixes: the four raw-panel mode-toggle buttons (drift/spt-histogram/sSMLM-histogram/segmentation) could stack instead of replacing each other when switching directly between two without passing through a reclaim point (e.g. Correct drift → Preview pairs) — fixed with a shared hideOtherRawToggleBtns() helper every dispatcher now calls; unchecking 3D localisation? on Gauss MLE 3D (elliptical or rotated) left a stale turbo/hsvBlue colour map in place instead of reverting to Fire, now matching Phasor 2D/Gauss MLE 2D's existing behaviour; the log window's card border used to be capped at the same width as its wrapped text, leaving a visible gap short of the reconstruction panel's own edge — split into an outer box (full width) and an inner text column (now 80ch, a standard terminal width, was 100ch); the Frame scrubber's "/ total frames" text sat visibly lower than its numstep sibling and now reads with a space on each side of the "/"; Pixel size (nm)'s own right-edge alignment (pinned outside any collapsible section for visibility, per a separate earlier fix this cycle) and Min./Max. cell area (px)'s indentation were both fixed for the same underlying CSS direct-child-selector reason; a light-theme-only invisible-text bug on Plot histogram of:; icon buttons (theme switch, sidebar toggle, panel crop/measure tools) unified onto one shared sizing system after a visible size/radius mismatch. |
| 0.11.5 | 2026-08-23 | — | DOCUMENTATION.md established as the single source for both the RTD manual and the in-app "more info…" popups; Help & guide thinned down; a header/raw-panel UI polish pass; numeric-input steppers. docs/DOCUMENTATION.md restructured: §1 "Exploring the user interface" now briefly covers the header/sidebar/log window, with full module behaviour moved into a renamed §2 "Module reference" (now ordered ahead of §3 "Parameters", which follows the same module order) — both section styles now show the on-screen name with the internal module name in parentheses for easier cross-reading. A new §9 "References & further reading" carries every citation the app builds on (Changelog renumbered §9→§10). All 11 in-app .hint "more info…" popups are now generated FROM DOCUMENTATION.md (raw-HTML <!-- HINT:name --> markers, synced via tools/sync_hints.mjs, --check mode for drift detection) instead of being hand-authored separately — the sync pass itself surfaced and fixed real staleness (detect/fit's method list only named 3 of 6, drift never mentioned its x/y-path toggle, sSMLM never mentioned the elliptical fit's real σx/σy, spt claimed no headless exposure though config.sptTrack has existed for a while). The in-app Help & guide modal is now deliberately thin (Guided workflow + Acknowledgements + License only, the old walkthrough/References sections removed) since DOCUMENTATION.md is now the maintained source for the rest. UI polish: the header title/subtitle/theme buttons/sidebar toggle now share one consistent bottom-aligned edge (was an inconsistent baseline/centre mix, plus the toggle button carried an accidental 4px margin bleed from an unrelated generic button rule and a line-height leading mismatch between the title and subtitle text) and the sidebar toggle is now sized to match the theme buttons exactly; the raw-panel Frame counter is now a typeable field (jump straight to a frame number) alongside the scrubber; the raw-panel crop tool now greys out while a plot (drift/NeNA/FRC/PCFO/histogram/line-profile) is shown in that panel, matching the reconstruction panel's own crop tool, and re-enables the moment the scrubber brings back a live frame; the Contrast slider's fill bar no longer visibly drifts off its own thumbs when the window is resized (was a plain 0–100% linear mapping that ignored the thumb's own fixed-pixel inset). Every numeric input across the app now gets an always-visible −/+ stepper (SVG line-drawings, not text glyphs — glyph optical centres vary by font/engine and looked subtly off) — a single global mechanism (addNumberSteppers()) reading each field's own already-wired min/max/step, no per-input work; a native-browser-spinner attempt was tried and reverted first (hover-only, unreachable on touchscreens, inconsistent look across engines/themes). Misc: fixed a spurious "JavaScript appears to be disabled" line appearing in the log on every ordinary load (a <noscript>+.textContent+= interaction — the seed log content is no longer noscript-wrapped); the version-update log line now always gets a blank line before "Ready…"; the pointless-on-mobile "Stack panels" button is now hidden below the mobile breakpoint; the header subtitle now sits on the title's own line to save vertical space. |
| 0.11.4 | 2026-08-22 | — | Rotated-elliptical MLE fit for sSMLM, a "3D localisation?" toggle unifying it with MLE 3D, raw-panel Contrast slider, drift x/y trajectory plot, Read the Docs manual. gaussianMLE/gaussianMLEastig/gaussianMLERotated refactored onto one shared Fisher-scoring Newton driver (mleNewtonFit()) and renamed to gaussianMLEspheric/gaussianMLEelliptic/gaussianMLEellipticangled to match Picasso 0.11.0's own SPHERICAL/ELLIPTIC/ROTATED naming (UI labels: "Gauss MLE 2D spherical"/"Gauss MLE 3D elliptical"/"Gauss MLE 3D rotated elliptical"). The new rotated-elliptical method fits independent σx/σy at either a FIXED angle (the sSMLM pairing step's own calibrated dispersion bearing, for a spectrally-elongated 1st-order PSF) or a FREE per-emitter angle (an astigmatism-axis-alignment diagnostic against real 3D calibration data), point-sampled rather than pixel-integrated — matching Picasso's own _accumulate_rotated formula, checked directly against its source. A new "3D localisation?" checkbox (localize3D, shown for mle3d/gaussmleEll only, default checked) is the single switch selecting free-vs-fixed angle and whether z is computed from a loaded calibration at all; mle3d itself now also runs calibration-free as a plain axis-aligned elliptical 2D fit when unchecked. New optional sigma_x/sigma_y [nm] CSV/table columns expose the per-axis width on every localization from either elliptical method (not just sSMLM-paired ones) — building this surfaced and fixed a real bug where the worker pool's packed result array never carried sx/sy at all, silently losing sSMLM's own sx0th/sy0th/sx1st/sy1st pairing columns on any worker-pool Run. New raw-panel Contrast (Black/White) slider (Picasso-style): a fixed per-stack ADU display range instead of per-frame auto-stretch, with an Auto reset button; now correctly re-estimated after the raw-panel crop tool is used or undone (previously kept showing the pre-crop range). New drift plot x/y trajectory view — toggle next to the existing drift-vs-frame curves to a single path (drift y vs drift x) coloured by frame/time using the current reconstruction colour map, with both axes sharing one physical scale so the path's shape is accurate. New version-check log line: on load, compares the release number against what this browser last saw (localStorage) and logs a "webSMLM updated" line with a changelog link when they differ, ignoring the dev/build suffix so routine local reloads don't spam it. Read the Docs manual (contributed by Aun Al Atiat, PR #6): a Markdown-native Sphinx+MyST build splits docs/DOCUMENTATION.md at each level-2 heading into a multi-page site (https://websmlm.readthedocs.io/en/latest/), with cross-references resolved to MyST labels so they keep working across pages; docs/DOCUMENTATION.md remains the single authored source, generated pages are never committed. Help & guide now links to it at the top; Acknowledgements credits the contribution. |
| 0.11.3 | 2026-08-20 | — | Dark/light/high-contrast theme switcher, SVG plot export, headless plot export, MIT license. New UI colour theme (applyTheme()): three small icon buttons in a new right-aligned .header-actions header area (alongside the Stack panels/Side by side toggle, moved there from the Log panel's title bar) switch between dark (today's only theme), light, and high-contrast, persisted via localStorage with an explicit fail-safe (blocked storage → falls back to dark, matching pre-existing behaviour, no error surfaced). plotColors() now reads the active theme's CSS custom properties live instead of a second hardcoded palette, so every plot automatically tracks it; the export-mode palette (used for saved images/PNGs) stays fixed and independent, as before. Ported into docs/layout_bare.html too. "Save plot / image" (renamed from "Save image", replacing the short-lived separate "Save plot (SVG)" button): a plot (calibration/drift/NeNA/FRC/PCFO/line-profile/histogram) now offers SVG and PNG as file types in one native save dialog — the browser's own "Save as type" dropdown becomes the format picker, decided from the resolved file handle's extension, rather than a second dedicated button. New from-scratch SvgRecordingContext (not a vendored shim) duck-types the narrow Canvas2D surface these 7 plot functions actually use and records real SVG DOM nodes; every plot function runs completely unchanged against it via a _plotTarget redirection. The raw frame/reconstruction stay PNG-only (no vector form at real localization counts). Headless config.exportPlots (--exportPlots on the CLI, exportPlots=1 on ?autorun=) extends this to window.webSMLM.analyze(): renders whichever of drift/NeNA/FRC/PCFO/calibration were actually computed this run as both PNG and SVG into result.plots, with no visible browser window needed at all — verified against a true headless Chromium CLI run producing correct plots for all five types with zero window ever opened. License: switched from CC BY 4.0 to the MIT License, going forward only — versions 0.1.0–0.11.2 (and their Zenodo-archived snapshots) remain available under their original CC BY 4.0 terms. Fixes: a stale lastNena (NeNA precision) could leak from a previously loaded dataset into SPT's "from NeNA" transfer on a completely different dataset if NeNA hadn't been re-run — now reset on every fresh Load movie/Simulate movie/Load data and on crop/uncrop (FRC has no equivalent stash to reset; its result is used once and discarded). |
| 0.11.2 | 2026-08-19 | — | Native Nikon ND2 loading, single particle tracking (SPT), plot panel UI polish, reliability fixes. ND2 (experimental): isNd2File()/loadNd2File()/parseNd2LvField() decode the modern (NIS-Elements 4.0+) chunk-container format directly from real sample files — no official spec exists, so this is a from-scratch reverse-engineered parser, not ported from any GPL reader; wired into loadTiffFile()'s single dispatch point so the interactive file input, calibration loading, and headless analyze() all gained ND2 support with no caller-side changes. A real row-0 pixel-corruption bug (a 24-byte per-frame sub-header misread as pixel data — implausible ADU spikes well above the camera's declared 14-bit ceiling, yet nothing threw, since dimensions/frame-count stayed self-consistent) was caught from a user screenshot and fixed same-cycle, cross-validated byte-for-byte against the independent BSD-3-Clause tlambert03/nd2 reference; real impact, not cosmetic — Localize on one real sample went from 5 localizations before the fix to over 24,000 after, since the corrupted outlier pixel was skewing the whole frame's detection threshold. TIFF and ND2 loaders now also log an embedded pixel size / frame interval / camera info hint when present (from finterval=/XResolution/unit= in ImageJ's own description text, or ND2's per-frame timestamps and camera datasheet chunk) — informational only, never auto-applied. Single particle tracking: new spt sidebar module links per-frame localizations into trajectories (a trackpy-inspired variant — Hungarian-optimal assignment per connected component, gap-bridging memory) and computes a per-track diffusion coefficient (ported from the Hohlbein Lab's own sptPALM-Python pipeline), plotted as a log-binned histogram alongside a track-length histogram with a weighted exponential lifetime fit; editing Frame time/Localization error after Track rescales every D live, without re-linking, since D is exactly linear in 1/frametime once the underlying MSD is fixed. Shipped with headless exposure (config.sptTrack/--sptTrack/sptTrack=1) and a Save spt data per-track summary CSV, distinct from the general per-localization export. Plot panel UI polish: every plot now letterboxes into a fixed 4/3 sub-rectangle centred within its panel (rather than resizing the panel itself, which previously made switching one panel between a frame and a plot visually resize its sibling too); plots render dark on screen but light on export; a new Stack panels/Side by side toggle overrides the previous aspect-ratio auto-heuristic; PCFO's large-number axis gained proper scientific-notation ticks (axisScale()); every plot now draws a consistent axis border and outward tick marks on both axes. Reliability fixes: Stop now actually cancels a running 3D calibration (previously had no effect, a real workflow slip after a user accidentally started calibrating an ordinary movie) and, separately, no longer hangs forever when clicked during a Localize run with both FTM and the worker pool active (a missing shouldStop() check in the FTM barrier-phased loop's own promise-resolution logic — reproduced and fixed via Playwright). NeNA's two curves and SPT's track-length fit now match drift's own green/magenta colour pairing for consistency. (Re-)selecting Phasor 2D or Gaussian MLE 2D now resets the colour map to Fire; SPT's from NeNA button now runs NeNA itself on demand if it hasn't been computed yet, instead of staying disabled. |
| 0.11.1 | 2026-08-17 | — | sSMLM data-model fix (a live drift-correction bug), headless Pair support, ND2 loading groundwork, code restructuring. A user request for Nikon ND2 loading led to checking sSMLM's headless-exposure status, which surfaced a real bug in the shipped 0.11.0 feature: Pair stored the inter-order distance in the z field, and drift correction's "Correct z too (3D)" option keyed off the same check the colour-by-depth toggle used — so a paired result could show that option, and ticking it would silently 1-D-"correct" the spectral distance as if it were spatial drift, corrupting it. Fixed at the root: dist is now a genuinely independent field, pairCore() never touches z at all — which also means a future 3D-fit + sSMLM combination could carry real depth and spectral distance on the same localization without one clobbering the other, instead of just patching a driftZ guard on top of the old aliasing design. renderSuperRes()/zRange() gained a colorField parameter ('z'/'dist') so the one depth-coded render path serves both; the sidebar's colour-scale labels ("Colour by depth (z)"/"z min/max (nm)") now switch live to sSMLM wording ("Colour by distance (sSMLM)"/"sSMLM distance min/max (nm)") instead of always saying "z" regardless of what's actually being coloured. pairCore() itself now throws on bad input (real 3D z, or already-paired dist) rather than only the interactive wrapper checking — verified against the existing paired-CSV fixture that old exports (which still alias z) load and still correctly trip the real-z guard. Headless/CLI/autorun exposure: config.sSmlmPair / --sSmlmPair / sSmlmPair=1 all run pairing right after Localize, before drift/NeNA/FRC — the same two-step pattern already used for estimateGainOffset/cropX0; summary.json/result.sSmlmPair record nPairs/nInput/meanDistance/stdDistance. ND2 loading: researched feasibility (no official spec; the modern NIS-Elements 4.0+ variant is uncompressed/Zip-compressed and tractable via the already-inlined pako, legacy JPEG2000 isn't; tlambert03/nd2 on GitHub is the one BSD-3-Clause reference safe to port from, Bio-Formats/nd2reader/Picasso's own ND2 support are all GPL or wrap that same BSD-3 library) — still blocked on a genuine native-binary sample to validate against (a first .nd2-named sample turned out to be a mislabeled TIFF export). That investigation did land a real, general fix: the multi-file loader used to trust the .tif/.tiff filename extension instead of sniffing actual content, and — more seriously — UTIF.decode() on genuinely non-TIFF bytes was found to silently return an empty-but-truthy object rather than throwing, producing NaN-sized buffers and uncaught canvas exceptions instead of a clean error; both are fixed (isTiffFile() content-sniffing, raw t256/t257 TIFF-tag validation before trusting a decode), and #file now also accepts .nd2. Code restructuring: a new top-of-file MODULE INDEX comment gives each module's current line number (refreshed alongside build-letter bumps going forward); the workers/export modules were physically reordered to match CLAUDE.md's documented module list (previously swapped, a leftover of modules being retrofitted onto pre-existing code); the license header dropped its redundant phasor citation (already covered in README.md/CITATION.cff) and the app's title changed from "browser single-molecule localization POC" to "browser-based single-molecule localization microscopy analysis". |
| 0.11.0 | 2026-08-16 | ✓ | Spectral SMLM (sSMLM) pair-finding, multi-file TIFF combining, render/table memory guards. New Spectral SMLM analysis sidebar module (pairCore()): pairs 0th/1st-order localizations produced by a diffraction grating in the emission path (Martens, Gobes, Archontakis, Brillas, Zijlstra, Albertazzi & Hohlbein, Nano Lett. 22(21), 8618–8625, 2022, ported from HohlbeinLab/sSMLMAnalyzer). Role assignment is directional, not brightness-based — real-data investigation found photon count barely correlates with position (≈50/50 even at confident intensity gaps, likely PSF-overlap/crowding at real emitter densities), so a point qualifies as 0th order only if it has a candidate on the configured signed bearing AND no candidate on the opposite bearing (self-disqualifying, no brightness needed) — verified to recover more pairs than an earlier brightness-gated approach (64.0% vs 59.0%). Preview pairs draws live distance/angle diagnostic histograms (reusing the table module's computeHist()/drawHistogram()) to find a dataset's own window before committing; the distance histogram always scans a wide fixed 0–6000 nm range with the current min/max overlaid as markers so narrowing the fields first can't hide the true peak, and Fit angle & tol. auto-estimates the angle window from a half-max peak walk, doubled as a safety margin (the raw half-max width undershot a hand-tuned window in testing). PSF width (σ) showed a real but imperfect ~65–70% correlation with role and is available as an optional, default-off extra filter (sSmlmRequireNarrower), not required. 2-point pairs only (0th+1st) for now — multi-order chaining and FFT-based auto angle/distance detection are tracked in docs/REFACTOR_PLAN.md. A pair's position is the 0th order's own x/y (undispersed, so already the true position), not the midpoint; the inter-order distance is stored in the paired loc's z, reusing the existing depth-coded render path unchanged (Pair refuses if the current result already has real 3D z); each paired row also carries sigma1st — the 1st order's own σ, exported as a sigma1st [nm] CSV column and shown as a table column, the closest available proxy for the spectrally-smeared 1st order's PSF width since no 2D fit method computes a directional/long-axis σ. New closed-loop hsvBlue colour map (matching the source paper's own figures) auto-selected by Pair; the reconstruction's colour-scale bar now anchors to the actual data's own right edge/vertical centre instead of a fixed canvas corner, so it doesn't float in empty space when many localizations are unpaired and dropped. New loadTiffFilesAuto(): a multi-file selection (Ctrl/Cmd+click) now auto-detects whether the files are one-frame-each (natural-sorted and treated as a sequence, e.g. a per-frame camera dump) or several multi-frame files meant to be concatenated end-to-end into one continuous stack (makeConcatStack(), each component kept in whichever loading strategy — in-memory/sliced/streamed — its own size calls for); backs the interactive file input, calibration loading, and the headless cfg.files/cfg.calibrationFiles config through one shared detection path. New pre-allocation memory guards for the two places that had none: renderSuperRes() (checkRenderSize() — refuses before allocating if the canvas would exceed the hard 16384 px browser limit or the estimated buffer footprint would exceed the Memory budget (GB) setting, leaving the previous reconstruction on screen rather than crashing; its density accumulator also moved Float32Array→Uint16Array, halving that buffer's footprint since a hit count is always a non-negative integer, with an explicit saturate-and-warn guard since Uint16Array wraps silently past 65535) and locTableData() (checkTableSize(), ~200 bytes/row estimated, same memgb-as-per-feature-ceiling reasoning — there's no reliable in-browser free-RAM signal to check against instead, so each feature guards against the same configured budget independently rather than sharing a pool). |
| 0.10.3 | 2026-08-15 | — | Raw-panel crop tool, pixel hover readout, panel/resize UI fixes. New crop tool in the Raw frame panel (rawCropBtn), distinct from the reconstruction panel's existing post-hoc display crop: click two corners and it replaces the loaded stack with just that native-pixel region (makeCroppedStack(), same wrapper pattern as makeFtmStack()), so detection, fitting and FTM all genuinely process fewer pixels rather than filtering an already-finished result — no coordinate-offset bookkeeping needed anywhere downstream, since a smaller stack behaves exactly like a smaller loaded file would. Deselecting the tool restores the original stack (kept as originalStack while a crop is active); re-cropping while still active chains off the true original, so one deselect always fully restores. Also reachable headlessly (config.cropX0/cropY0/cropX1/cropY1 on window.webSMLM.analyze(), --cropX0/… on tools/webSMLM-cli.mjs, ?autorun= query params). New raw-frame pixel hover readout: hovering the raw panel shows a crosshair + x=/y= (native pixel index) and the pixel's value in ADU, plus its photon-converted equivalent once gain/camoffset are calibrated — reuses the same registerPlotHover/drawPlotHover mechanism the FRC/NeNA/drift/calibration/PCFO/histogram plots already share. UI fixes: very wide/short frames (h/w < 0.5) now stack the Raw frame/SMLM reconstruction panels instead of squeezing them side by side to half width on top of already being short; canvas backing-store resizing now uses a ResizeObserver on the shared panel container instead of only a window resize listener, fixing cases (sidebar dock/float toggle, the new stacking switch itself) that changed the panels' on-screen size with no viewport resize behind them to trigger it; the column-histogram hover tooltip was missing its unit on the value ("2682 · 66 count" → "2682 photon · 66 count", reusing the same histData.unit the x-axis label already showed); the FTM chunk log line now also prints the padded context range actually fetched ("frames 129–160 (32-frame chunk, context 119–169)"), not just the non-overlapping core range, after confirming with a bit-identical whole-stack-vs-chunked numerical check that the underlying computation was already correct — just under-documented in the log. |
| 0.10.2 | 2026-08-15 | — | Gain/offset estimation (PCFO), simulation camera model, FTM mobile-OOM fix. New Gain & offset estimation sidebar section (before 3D calibration): the Rieger–Heintzman photon-conversion-factor method (PCFO; Heintzmann, Relich, Nieuwenhuizen, Lidke & Rieger, arXiv:1611.05654) tiles a sample of frames, regresses mean signal against high-spatial-frequency noise variance (robust Tukey-fence outlier clipping on noisevar), and reports gain/offset with a jackknife uncertainty and a diagnostic signal-vs-noise-variance plot — Estimate only computes, a separate Transfer estimates button (disabled until a successful Estimate) applies the result to Localisation settings' Gain/Camera offset, so a stale estimate can't silently overwrite hand-set values. Headlessly reachable via config.estimateGainOffset (window.webSMLM.analyze(), tools/webSMLM-cli.mjs --estimateGainOffset, ?autorun=) through a new DOM-free pcfoCore(), following the same *Core(config, stack, hooks) split as runCore/driftCore/calibrationCore. Simulation settings gain a physically-motivated forward model: emitters now follow a Poisson arrival process over a true areal density (dens, emitters/µm²/frame) with an exponential ON-lifetime (simlifetime), each activating exactly once (no more per-frame-independent re-blinking); a forward camera model (simulation_gain/simulation_offset/simulation_offset_std/simulation_readnoise) applies shot noise → read noise → gain → fixed-pattern offset, decoupled from the fit-side gain/offset so ground truth and the fit's assumptions can be matched or intentionally mismatched. Fixed a real mobile OOM: the barrier-phased FTM worker loop kept its raw-context buffer reachable through the entire following detect/fit dispatch phase (a second, separate wave of allocation chunkmb's sizing never budgeted for) — dropping the reference immediately after use fixes it; chunkmb default moves back 1000→500 MB. runCore() now logs an estimated peak-memory figure when FTM is on (chunk working set + already-cached stack size — two separate budgets that stack, not a shared ceiling), advisory above ~800 MB and gated on memgb≤8 so raising it (now up to 64 GB, was 8, for workstation-scale full-stack caching) doesn't nag every Run once a desktop user has already said they have headroom; this is visibility only — a mobile tab killed for memory pressure gets no JS-visible error at all, so nothing can actually detect or prevent it. UI: Raw frame/SMLM reconstruction/Log panels now outline the actual data frame (canvas border) instead of a card wrapping title+content, matching the Log panel's existing pattern; buttons ~18% shorter (base 34→28 px, paired .btnrow buttons 32→26 px); tightened .main/.card padding so panel titles land on the same line as the sidebar's first button row; renamed "Camera gain (photons/ADU)" → "Gain (photons/ADU)" (Localisation settings and Simulation settings) and "Photons/frame"/"Background (photons/frame)" → "Photons/emitter/frame"/"Background (photons/px)" to disambiguate two different per-frame quantities. Merged from PR #5 (Koen J. A. Martens) — adapted onto 0.10.1's FTM/sidebar architecture, which had landed after the PR branched. |
| 0.10.1 | 2026-08-14 | — | Temporal median filtering (FTM), analysis frame range, sidebar/log UI cleanup. New temporal median filtering: ftmEnabled/ftmWindow subtract each pixel's median over a sliding window of nearby frames, removing slowly-varying background before detection/fitting — floored at camoffset (not 0) and added back, so the fit's own (raw−camoffset)×gain conversion doesn't double-subtract the baseline. Two independent paths: a live raw/FTM-corrected toggle next to the raw panel for scrubbing preview (ftmFrame/ftmFrameParallel, worker-parallel, one frame at a time), and Localize itself running on FTM-corrected frames — processed in memory-bounded chunks (sized from half the chunkmb budget) via makeFtmStack() on the main thread, or a barrier-phased worker loop (alternating a full-pool-parallel FTM-correction phase with the existing full-pool-parallel detect/fit phase, never both job types on the pool at once — each worker has one onmessage slot, not a queue) when a worker pool is available, replacing an initial main-thread-only design that left workers starved (measured 8% utilisation). A genuine chunk-boundary bug — a chunk's naive coreStart±half context padding under-covering the window some of its own frames need once the per-frame edge clamp kicks in near either end of the stack, silently biasing those frames' photon counts — was caught via a worker-vs-serial correctness A/B test and fixed in both paths. The chunk size and progress are now logged (FTM correcting frames X–Y…) so a run no longer goes silently quiet during a chunk's correction phase. Technique from Nieuwenhuizen et al., Nat. Methods 10, 557–562 (2013); ported from the Hohlbein Lab's own FTM2, used in Jabermoradi et al., Phil. Trans. R. Soc. A 380(2220), 20200164 (2022). New fitFirstFrame/fitLastFrame restrict a Localize run to part of the loaded stack (1-based inclusive; the rest is skipped entirely, not just excluded from the result) — live-preview crosshairs (not ROI boxes) are suppressed when scrubbed outside that range. Crop tool fixes: now auto-zooms the SR view to fit the cropped rectangle, and pins the colour scale to the full (uncropped) data range instead of rescaling to whatever's visible; deselecting the tool resets the view. Sidebar/log UI: the 8 "…further info…" inline disclosures are now "more info…" buttons opening a shared popup (same mechanism as the localizations table popup) with real bullet lists instead of dense small-font paragraphs; the stats bar (Frames/Localizations/Loc-per-frame/Compute) is removed — frame counts were already logged on load, and loc/frame is now folded into runCore()'s own "Done:" log line (and the CSV "Load data" line, which didn't report it before); the Raw frame/SMLM reconstruction/Log panels regained a plain gray outline (no fill) after briefly losing it; Save image no longer stays clickable referencing torn-down content after a reload that fails before drawing anything new. chunkmb ("Stream heap chunk") default raised 500→1000 MB. |
| 0.10.0 | 2026-08-11 | ✓ | Scriptable / headless pipeline. Three new layers let webSMLM run outside a click-through UI session, lightest setup first. window.webSMLM.analyze(config) runs the whole load → detect/fit → drift → CSV/log/settings pipeline in one call and returns every result as in-memory data (locs, csvText, logText, settingsText, per-stage timings, a reconstruction PNG rendered via renderSuperRes()'s own detached canvas) instead of touching a DOM control or triggering a download; config is a partial {id: value} object in the same shape PARAMS/a settings JSON already uses, so a new parameter needs no separate headless wiring; analyzeBatch(files, config) loops it over multiple files. URL-param autorun (webSMLM.html?autorun=1&fileUrl=...&pxnm=160&...) runs analyze() the moment the page finishes loading, using the query string as config — works in any double-clicked browser, no driving script needed; &download=1 also saves the results as fixed-named files. tools/webSMLM-cli.mjs (Node + Playwright) is the recommended way to drive it from a terminal: a real, true-headless Chromium — no window ever opens — uploads the input file via page.setInputFiles() (no HTTP server or CORS concern) and calls analyze() straight through page.evaluate(), writing result.csv/settings.json/log.txt/reconstruction.png/summary.json; progress and log lines stream live to the terminal as the run progresses (an in-place, terminal-width-truncated progress bar plus the most recent log line shown as status) rather than only becoming visible once the whole run returns. Two lighter, dependency-free alternatives — tools/browser_sweep.py (stdlib-only Python) and tools/browser-sweep.sh (bash) — drive a real visible browser through a sweep of one parameter's values via the autorun+download mechanism, for a quick multi-value comparison with no npm install. Headless 3D calibration: calibrationCore(config, stack, hooks) — the same DOM-free extraction runCore/driftCore got in this cycle — lets a calibration be built without clicking Calibrate: analyze() gains config.calibrationFile/calibrationFiles (a bead z-stack, builds a fresh calibration before the main run) and config.calibrationOnly (build/return only the calibration); the CLI's --calibration <path> overloads on file extension (.json used as-is, .tif/.tiff triggers a fresh build and writes it back out as <name>_calib.json for reuse), and --calibrationOnly skips localizing entirely. Any of calFirst/calLast/calStep/calRef left unset defaults (whole stack / 10 nm step / auto z-reference) with an explicit logged warning, so a silently-wrong calibration parameter can't slip through unnoticed — validated against the real Leterrier 3D-STORM Z-calibration stack, where building a calibration inline via --calibration beadstack.tif produced the exact same 10,522 localizations and 3.4 nm median CRLB as loading the equivalent pre-built *.calib.json. Convention: every hook in the pipeline (onLog/onProgress in the loaders, runCore, driftCore, frcResolution, calibrationCore) now defaults to the real interactive log()/setProg() when a caller doesn't supply one, so nothing that would show in the interactive Log window goes missing headlessly; onProgress is the sole progress channel (a percentage-as-text experiment through onLog was tried and reverted — it just repeated the same numbers as text with no extra information). UI polish: Correct drift now shows the drift-vs-frame curve automatically instead of needing a separate Show drift click; the raw-panel live preview during Localize now overlays fitted localizations (magenta crosshairs), not just detected ROIs (green boxes); the raw-panel preview's update cadence — previously frame-count-gated and visibly jumpy, especially with many workers — is now time-based (rawPreviewMs, new PARAMS entry, default 200 ms), driven by an independent timer decoupled from bursty worker-message arrival, with a monotonic-frame guard (workers complete out of order, so without one a slower worker's stale batch could visibly rewind the displayed frame) and lower workerBatchTarget/workerBatchMax defaults (8/128 → 24/32), since batch size itself — not any preview-side throttling — turned out to be the dominant bottleneck on how often fresh fit results could even become available. Docs: docs/DOCUMENTATION.md §8 documents the full headless API and CLI; README gets an "Advanced: scripting & headless analysis" section. |
| 0.9.7 | 2026-08-08 | — | Load data (CSV round-trip), sigma_z precision, degenerate-fit rejection, docs. New docs/DOCUMENTATION.md: a detailed reference for every button, module and PARAMS entry (defaults/min/max/step) plus the settings-JSON/calibration-JSON/CSV file formats, complementary to the deliberately-sparse in-app Help & guide. New Load data button (CSV round-trip of Save data): parseCsvLocs() rebuilds a full working result — table, reconstruction, NeNA/FRC/drift/re-export all work on it exactly as after a Run — from only what's actually in the CSV plus the current Pixel size/Magnification controls; stack is left untouched (no raw frame data in a CSV, so re-detection/live preview stay unavailable for loaded data), internal canvas size is derived from the data's own bounding box, and lpx/lpy are reconstructed symmetrically from the CSV's single combined uncertainty column. Action-button rows reflowed: Help & guide now alone in its own row, right-aligned; Load data takes its old spot next to View data + filtering. New sigma_z column (table + CSV) for MLE 3D: gaussianMLEastig now also returns the Fisher-matrix precision of the fitted σx/σy widths, propagated through the calibration curve's local slope into an approximate z-precision (not a true joint CRLB, but the best estimate available); the table's sigma column is renamed sigma_xy for clarity, CSV keeps sigma [nm] unchanged for ThunderSTORM compatibility. MLE fitters (2D and 3D) now reject degenerate fits instead of silently keeping them: photon count was clamped to a floor of 1 during Newton iteration, so a candidate that never found real signal converged onto that floor and was still returned as a "localization" with nonsensical bg/uncertainty — both fitters now track real convergence (not just iteration exhaustion) and reject non-converged, floor-pinned, or non-finite-CRLB results. Fixed: tempClusteringXY alone (no tempClusteringZ) was dropping z entirely (NaN) from merged events, since z-tracking was incorrectly gated on the same flag as the z merge-distance check; z (and its lpz precision) is now tracked/averaged whenever the source data has it, independent of whether a z threshold is set. Default fit radius raised 3→4 px (a radius-3/7×7 window clips astigmatic PSFs that elongate away from focus — the calibration fitter itself already allows widths up to 6 px). Filter box now autocompletes column names as you type (↑/↓/Enter/Tab), sourced live from the current table's own columns plus the tempClusteringXY/tempClusteringZ pseudo-fields. Number inputs now accept , as a decimal separator (typed or pasted), converting to . via execCommand('insertText') since type=number blocks the selection APIs a manual cursor-position insert would need. |
| 0.9.6 | 2026-08-08 | — | Temporal clustering, FRC/NeNA robustness, performance & docs. New temporal clustering: consecutive-frame detections of the same blinking molecule can be merged into higher-precision "events" via tempClusteringXY/tempClusteringZ < N (nm) filter clauses — photon-weighted position, summed photons, inverse-variance-combined uncertainty; the table gains an nmerged column and switches its count to "events", CSV export gains n_merged. getBaseLocs() is now the single place deciding raw-vs-clustered locs, feeding render/export/NeNA/FRC identically to any other filter. FRC made robust against clustered data: previously a degenerate NeNA fit (near-zero/NaN sigma, or a numerically-valid but physically-nonsense one, e.g. 2231 nm) could hang or silently mislead FRC's sampling grid; NeNA's result is now validated and sanity-checked against a new modeUncertaintyNm() fallback tier (histogram-mode of each localization's own precision), which also fixes FRC on any dataset too short for NeNA's consecutive-frame pairs. Performance: fixed a stale stack.px snapshot causing the raw/reconstruction scale bars to mismatch after changing pixel size post-load; TIFF frame decode now uses a bulk TypedArray.set() fast path instead of a per-pixel DataView loop (~45× faster on the common case); the SR-panel live preview's refresh interval is now adaptive instead of fixed, keeping its cost a bounded fraction of wall time instead of growing unboundedly through a long run (measured case: 72%→~12% of total run time). Both newly surfaced as preview/other lines in the run's timing breakdown. UI: the progress bar below the action buttons is now the single, permanently-visible indicator for every long-running operation (drift correction and NeNA/FRC previously had their own bars that only existed while running); crop, ordinary filters, and temporal clustering now all log their changes to the Log window; the table's "…more info…" hint reformatted into bullets with new filter-grammar and histogram explanations. Fixed: cropping after a tempClusteringXY+nmerged filter combination left the reconstruction blank (crop's row-rebuild had missed the clustering-aware path). Docs: copyright/attribution changed to "Hohlbein et al."; new Acknowledgements section in Help & guide crediting Koen J. A. Martens' verified contributions (PR #4 — uniform box-filter detection, Real-time update toggle, wheel-zoom/ctrl+wheel-scrub scroll rework, ROI box sizing); experimental_data/README.md notes that FRC peaks at 40/20 nm on the GATTA-PAINT dataset are harmonics of the 80 nm ruler spacing, not a bug; stale line counts, dead cross-references and a shipped roadmap item cleaned out of CLAUDE.md/docs/REFACTOR_PLAN.md/README.md. |
| 0.9.5 | 2026-08-07 | — | Multi-file loading, crop tool, NeNA-derived FRC. Load movie now accepts a multi-file selection (Ctrl/Cmd+click several single-frame TIFFs) — loadTiffSequence() natural-sorts them by filename and concatenates on demand, so per-frame camera dumps (e.g. GATTAquant's public GATTA-PAINT download) no longer need an ImageJ concatenation step first. New crop tool next to the line-profile tool (SR panel): click two corners to add an x/y-range clause directly into the same _tableFilters array the data table's typed filters use, so a crop affects the reconstruction, CSV export, NeNA and FRC identically to any other filter rather than being a bespoke mechanism — disabled while the panel shows the pre-Run data projection or the calibration bead composite (neither is tied to real per-localization data), and deselecting the tool undoes any crop(s) it added. FRC's sampling grid is now sized from the data's own NeNA-derived localization precision (binsize = σ/2, matching Picasso's frc()/_frc()) instead of the display-magnification-derived size that had no physical meaning; logs the diffusing/mobile-probe caveat (real motion inflates σ, coarsening but not invalidating the estimate) and, when the 2048² grid cap bites, the exact field-of-view size () that would avoid it. FRC/NeNA now auto-recompute when a crop is added or removed while their plot is on screen. MLE's Newton-iteration convergence tolerance is now a PARAMS entry (mleEps, default 0.001 px, matching Picasso's gaussmle default) instead of a hardcoded 1e-4 — measured ~19% faster with a higher convergence rate on real data, since the old tolerance was chasing sub-CRLB-precision digits that don't affect the result. UI relabeling for clarity: Run→Localize, Save locs→Save data, Display table→View data + filtering; References & further reading reformatted into headline + bullet-list references (title-first); camoff→camoffset renamed throughout. Fixed: the floating/mobile sidebar drawer started at the very top of the viewport instead of below the header — its offset is now measured from the real header height (--header-h) rather than guessed; crop and other table filters never actually reached NeNA, FRC or CSV export (lastResult.locs used directly instead of the filtered renderLocs) even though the reconstruction correctly restricted itself — the most visible symptom being FRC's Nyquist number not moving after a crop; the line-profile tool and scale bar reported nonsense distances (raw pixel counts, or distances undercounted by the magnification factor) while viewing the data projection or bead composite, both drawn at native camera-pixel resolution rather than the reconstruction's magnified one. |
| 0.9.4 | 2026-08-04 | — | Parameter registry + gain-aware Poisson MLE fitting. New PARAMS object centralizes every analysis/export parameter (name → {label, min, max, step, default, int}) as the single source of truth for HTML control defaults, Save/Load Settings (now iterates the registry instead of an ad-hoc element-id list), and six previously no-UI pipeline constants (worker-dispatch thresholds, preview timing, etc.) that are now settable via a loaded settings JSON. Load/Save now echoes every parameter's value to the Log window (logParamValues), one line per parameter. Gain-aware fitting: all four fit functions (LS, Poisson-MLE 2D/3D, phasor) now convert each pixel from ADU to true photon units — (raw−camoff)×gain — before fitting, matching Picasso's _to_photons() architecture, instead of scaling photons/bg/bgstd after the fact. Position/width/ratio outputs are provably unaffected (confirmed both analytically and by synthetic validation — LS/phasor outputs are bit-identical regardless of gain/offset); MLE's Poisson likelihood and CRLB (lpx/lpy) are the one place this actually changes a result — fitting raw ADU directly, as before, silently assumed gain=1 and reported ~2× overconfident precision whenever a real camera gain was entered. Export/table histogram no longer double-convert, since photons now arrive already in true units. Module renaming for clarity: io→in/out; new simulation module split out of it; calibration→3D calibration (more 3D methods may join astigmatism later); precision→locprecision. Documentation accuracy pass across README.md (dropped a stale "3D is Phasor-only" limitation), docs/REFACTOR_PLAN.md (trimmed of the "Shipped" table, which duplicated this changelog), CITATION.cff (added Huang et al. 2011), and experimental_data/README.md (removed the not-yet-public Nile Red section; documented camera gain/offset/pixel-size settings for the Leterrier 3D STORM and EPFL SMLM-2016-challenge datasets, including a correction after Christophe Leterrier confirmed the Nikon acquisition software's e⁻/ADU reading is already EM-gain-inclusive, not a value to divide by EM gain again). |
| 0.9.3 | 2026-08-03 | — | Astigmatic calibration robustness + worker-scaling fix. New "Fix bead x,y" option in 3D astigmatism calibration: averages the calibration range into one composite (shown in the SR panel with detected ROI/fit), freezes each bead's x,y from it, then fits only amplitude/σx/σy/background per frame at those fixed positions — the two most failure-prone degrees of freedom no longer wander at large defocus, where the PSF can flatten, ring or split into a second maximum. Scrubbing now overlays those same frozen positions on every frame instead of re-detecting (a calibration in general also now overrides the unrelated live-preview when scrubbing). σ_PSF range raised 2.5→5 px, fit radius 2→10 px. Any loaded movie now shows a quick averaged data projection in the (otherwise empty) SR panel before a Run/Calibration, sampled/capped so it stays fast regardless of stack size. Fixes: the z-axis auto-range could permanently lock to a narrow early sample (rounding to "0–1 nm") on long Runs, because a mid-Run preview render — not just the final, complete one — was allowed to auto-fill the z-range fields; only the final render does now. Worker dispatch (useWorkers) now also triggers on total stack volume, not just per-frame size, so many-small-frame stacks (e.g. the EPFL SMLM-challenge datasets, 64×64 px × ~20,000 frames) parallelize too. Added docs/layout_bare.html — a stripped-down, reusable UI shell (header, buttons, one module, one selector, two display windows, working log panel) backed up from webSMLM's chrome for future single-file projects. experimental_data/README.md documents the new EPFL SMLM-2016-challenge astigmatism benchmark files (bead z-stack + high/low-density ground-truth microtubule stacks) with their verified simulation/camera parameters. |
| 0.9.2 | 2026-07-27 | — | Layout & mobile UI polish. The controls panel is now collapsible (toggle top-left): collapsing gives the two display windows the full width. On desktop, re-opening after a collapse floats the panel as an overlay drawer over the full-width canvases — so toggling never resizes the windows — with a pin button to dock it back into the layout; the drawer is opaque (no screen dimming). On mobile the panel is an overlay drawer so the images sit at the top of the page. Both display windows now take the data aspect ratio once a stack is loaded (equal-sized, no letterboxing / no giant squares) and scale to the available width without overflowing or overlapping. Fixes: pinch-to-zoom restored in the raw frame (zoom = wheel/pinch, scrub = shift+wheel or the slider); long panel-title captions no longer wrap to a second line (ellipsis + hover tooltip); tapping a number field on mobile no longer triggers an iOS page-zoom (16px inputs). The two vendored libraries (pako, UTIF.js) are now clearly bracketed with BEGIN/END banners. |
| 0.9.1 | 2026-07-27 | — | Detection-filter upgrades (PR #4). Third selectable detection filter: uniform box filter (difference of box means + a σ_PSF-sized dilation for local maxima, thresholded on a plain intensity value — Huang et al. 2011), alongside wavelet and DoG. Detection now goes through a single detectSpots() dispatch (main thread + workers); each filter has its own threshold field (their thresholds mean different things). New Real-time update toggle — re-detects/re-fits the scrubbed frame live as you change detection/fit settings, without a full Run. Raw-panel scroll reworked: wheel = zoom, ctrl+wheel = scrub (one frame per notch); the green ROI box now tracks the actual fit radius. |
| 0.9.0 | 2026-07-26 | ✓ | Poisson MLE fitting & an interactive localizations table. New Gaussian MLE 2D — integrated-Gaussian Poisson maximum-likelihood fit (Smith et al. 2010; ported from Picasso gaussmle.py), full Fisher-scoring Newton (~5 iters), now the default method — reporting a proper per-localization CRLB uncertainty (used for the CSV "uncertainty" column and a median-precision log line). New Gaussian MLE 3D — astigmatic σx/σy fit → z from the Gaussian-width calibration, an independent cross-check on Phasor 3D (guarded so it can't run against the wrong calibration model). New Display table — sortable, cumulatively-filterable (intensity > 1000 and uncertainty < 20, Enter to apply, removable chips, Reset) view of the localizations; filtering drives the reconstruction live (with an "active filter" note) and any column can be histogrammed in the raw panel (x-zoom/pan, x-axis from 0). Fit methods renamed for consistency (Phasor 2D/3D, Gaussian LS 2D, Gaussian MLE 2D/3D). Calibration JSON records its source_file; Compute [s] now reports the whole-Run wall clock (compute-only figure kept in the log). |
| 0.8.5 | 2026-07-26 | — | Phasor 3D calibration safeguarding (work in progress). Calibration now carries two explicitly-tagged models — "Phasor magnitude based" (what Phasor 3D uses) and "Gaussian width based" (for future LS/MLE 3D) — saved in the exported JSON (calibration_methods), with an algorithm→model guard so a 3D fit can't run against the wrong calibration. New 3-way calibration graph toggle (σ widths → phasor magnitudes → ratio). Phasor magnitude aligned to the reference FFT and made brightness-independent (` |
| 0.8.4 | 2026-07-26 | — | Sharper canvases & input guards (under-the-hood UI). All canvases (plots + reconstruction + raw frame) now render at device-pixel resolution and re-render on window resize, so axis text, the scale bar and overlays stay crisp at any window size / on retina instead of pixelating. Plot text unified to a standard 12px font; selection boxes right-aligned to 60% width; NeNA x-axis label nudged to the axis. Magnification range extended to 25. Out-of-range number inputs are now clamped to the nearest valid value with a log note. |
| 0.8.3 | 2026-07-25 | — | Sidebar UI enhancements. Collapsible modules (Calibration, Drift, Precision, Memory & streaming, Simulation) are now slim, flat/outlined section bars with an oversized leading triangle — visually distinct from the raised action buttons; their controls are indented so they read as belonging to the section. Whole control tier unified to 12px (labels, inputs, selects, buttons; help/hints stay 11px). Separator line added above Magnification; "Help & guide" no longer bold. |
| 0.8.2 | 2026-07-24 | — | Plot cursor readouts. Hovering inside any axis plot (line profile, NeNA, FRC, drift, calibration) now shows a dashed crosshair and a readout of the cursor position in data units (e.g. FRC gives spatial frequency, its resolution equivalent in nm, and the FRC value). Implemented once via a shared snapshot-restore overlay, so hovering never re-runs the plot render. |
| 0.8.1 | 2026-07-24 | — | NeNA fit switched to the full Endesfelder function (signal Rayleigh + Gaussian short-range + linear long-range). New line-profile / distance tool in the reconstruction — click two points → intensity profile over a 3-px band, plotted with x-zoom/pan and styled axes. Depth colourbar ticks at +max / 0 / min (overlapping label dropped); Phasor 3D auto-selects the Turbo colour map. Clear log / Export log buttons. Plot-label polish (drift/FRC axis titles no longer clipped, FRC "1/7" above its line); Memory/Simulation help collapsed behind "…further info…"; not-yet-implemented FSC checkbox hidden. |
| 0.8.0 | 2026-07-23 | ✓ | Localization precision & resolution (experimental). NeNA — mean per-localization precision from the nearest-neighbour distance distribution (Endesfelder et al. 2014), with a fitted-histogram plot and a fit-covariance error; caveat that it needs a static structure. FRC — image resolution at the 1/7 threshold via Fourier ring correlation of two random halves (Nieuwenhuizen et al. 2013; compact inline radix-2 FFT, no dependency), with the curve plotted and an error from the split spread. Both on the left panel, saved via Save image. New and not yet cross-validated against established tools. |
| 0.7.7 | 2026-07-23 | — | Save image — export either window as PNG (chooser when both have content; reconstruction & raw frame supersampled with crisp overlays; drift/calibration plots snapshotted). Scale bar added to the raw window. Pixel-size changes now live-refresh both scale bars and the nm/px readout. Log timing-breakdown column alignment fix. |
| 0.7.6 | 2026-07-23 | — | Fix: "Colour by depth" toggle now appears (ticked) the moment a 3D fit method is chosen, not only after a run. README refreshed for the wavelet default and updated performance (4.89 GB 3D stack ~12 s / ~350k loc/s; browser ordering note). |
| 0.7.5 | 2026-07-23 | — | Selectable detection filter: à trous B-spline wavelet (ThunderSTORM-style, ~2× faster band-pass) now the default, or DoG band-pass. findMaxima mean+variance fused into one pass. Re-tune k when switching filters (they respond differently). |
| 0.7.4 | 2026-07-23 | — | UI polish: drop decorative button glyphs (self-explaining labels); disclosure "⋯" inherits text colour; Compute stat in seconds; reconstruction caption shows just nm/px; "All client-side, no upload." moved to the subtitle. |
| 0.7.3 | 2026-07-23 | — | Docs only (app unchanged): README gets a "Launch webSMLM" badge and a direct hosted link (…/webSMLM.html); Roadmap trimmed to point at the plan. |
| 0.7.2 | 2026-07-23 | — | Docs only (app unchanged from 0.7.1): experimental_data/README.md links to the public source datasets — GATTAquant GATTA-PAINT 80R RAW and Leterrier's 3D STORM stack (figshare); notes the 4.9 GB 3D stack runs client-side on an iPhone 17. |
| 0.7.1 | 2026-07-23 | — | <noscript> warning when JavaScript is disabled (some browser privacy settings otherwise leave the page blank) — a top banner plus a light-red line in the log. |
| 0.7.0 | 2026-07-22 | ✓ | Drift correction via AIM (adaptive intersection maximization), 2D + 3D; point-based, no FFT (chosen over RCC for exactly that reason — fits the single-file, no-dependency constraint). Segments localizations in time (~100 frames), grid-searches the shift that maximizes coincident localizations against the accumulated reference; a parabolic sub-pixel peak fit replaces Picasso's FFT phase refinement, linear interpolation replaces its spline. Drift-vs-frame plot, reversible correction (raw coordinates kept), clamp-fraction guard. Sawtooth fix: a broad/noisy z intersection peak gave the parabola a near-zero curvature term, flipping the fit to ±0.5-bin offsets on alternating segments; fixed with a linear-preserving [1,2,1] segment smooth (applied once on x/y, twice on the noisier z) that removes the 2-segment oscillation without touching a genuine linear ramp. Clamped-z exclusion: Phasor-3D z is clamped to the calibrated range, so out-of-range localizations pile into the two boundary bins as fixed, non-drifting spikes that would otherwise dominate and oscillate the estimate — excluded via a zClamped flag from the fitter (warns when >20% of z is clamped: the sample's z-range likely exceeds the calibration). |
| 0.6.3 | 2026-07-22 | — | Local/dev only — calibration overlays kept while scrubbing; robust large-stack loading. Folded into 0.7.0. |
| 0.6.2 | 2026-07-22 | — | Type-aware TIFF tag reading (fixes big-endian LONG tags → "could not read image dimensions"). |
| 0.6.1 | 2026-07-22 | ✓ | Large multi-IFD (Micro-Manager MMStack) streaming loader — indexes multi-GB stacks by walking the IFD chain. |
| 0.6.0 | 2026-07-22 | ✓ | UI overhaul & pipeline polish: sidebar reorg, number inputs (not sliders), Save/Load settings, both-panel zoom/pan, sub-pixel crosshair overlays. Fixes: multi-IFD freeze, blank-input render crash, O(n²) live-preview slowdown. |
| 0.5.0 | 2026-07-21 | ✓ | 3D astigmatism (Phasor 3D): bead z-calibration, z per localization from the phasor width ratio, depth-coded render, calibration save/load. |
| 0.4.1 | 2026-07-21 | — | Raw-view refresh fix during parallel runs; GitHub header link. |
| 0.4.0 | 2026-07-20 | ✓ | CSV export (ThunderSTORM-compatible), large-stack streaming, UI tidy-up. |
| 0.3.0 | 2026-07-20 | — | Speed review: band-pass optimization (box-filter cascade replacing the large-scale Gaussian, precomputed mirrored index maps, MessageChannel-based yield instead of throttled setTimeout) + Web Worker pool (~7× / ~30× faster). Rejected: seeding the Gaussian LS fit with a cheap phasor estimate, on the theory that a sub-pixel start needs fewer Gauss-Newton iterations. Doesn't survive contact with dense data — with a second emitter in the ROI, phasor returns a point between them (no better a start than the local maximum, sometimes worse), and more fundamentally, on overlapping emitters the fit isn't slow because the start is poor, it's slow because there's no good single-emitter optimum to find; a better initial guess can't fix model mismatch. Measured per-candidate LS cost: 56 µs on a sparse stack vs 211 µs on a dense one — that 3.8× gap is iterations spent on emitters the single-emitter model can't describe. The real fix for dense data is multi-emitter fitting, not a faster single-emitter fit. |
| 0.2.0 | 2026-07-20 | — | UI / responsive redesign, small-screen (phone/tablet) support. |
| 0.1.0 | 2026-07-19 | ✓ | Initial browser-based SMLM localizer: phasor + 2D-Gaussian fitting, TIFF loader, super-resolution render. |
0.2.0 and 0.3.0 predate the release policy (dev moved fast, no DOIs cut). 0.4.1, 0.6.2 and 0.6.3 were patches without a DOI. 0.6.1 was released despite being a patch.