diff --git a/README.md b/README.md index 88a6226..f9a3a5f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,42 @@ A typical first session: Prefer the command line for inspection, conversion, and batch pipelines? See the [command-line guide](docs/cli.md). +## A tour of the GUI + +The full walkthrough, with each step spelled out, is in the +[GUI guide](docs/gui.md). + +**Loading images.** Open a folder (`File → Open folder...`) and every +supported scan and spectrum appears in a thumbnail grid — switch thumbnail +channel, colormap, and row alignment from the sidebar, then double-click a +scan to open the image viewer. + +![Browse mode with a folder of scans loaded](docs/images/gui_browse.png) + +![The image viewer showing a terraced surface](docs/images/gui_viewer.png) + +**Subtracting a background.** `Processing → STM scan-line background...` +(`Ctrl+Alt+B`) fits a per-scan-line background; switch between models +(linear, polynomial, low-pass, piezo-creep variants) with the dropdown and +watch the residual plots to judge the fit before applying. A polynomial +plane fit lives next to it under `Processing → Plane/background +subtraction...` (`Ctrl+Shift+B`). + +![STM scan-line background dialog with a linear fit previewed](docs/images/gui_stm_background.png) + +**Performing an FFT.** `Measurements → FFT viewer...` (`Ctrl+Shift+F`) +shows the spectrum with q-axes in nm⁻¹, intensity controls, and a radial +profile; the tabs fit a reciprocal lattice, correct drift distortion, +suppress mains pickup, and reconstruct a filtered image by inverse FFT. + +![FFT viewer on a moiré superlattice](docs/images/gui_fft.png) + +**Finding features.** `Measurements → Feature finder...` detects maxima or +minima with threshold, spacing, and smoothing controls, then exports the +coordinates to CSV or a synthetic feature image for lattice statistics. + +![Feature finder marking the minima of a moiré superlattice](docs/images/gui_feature_finder.png) + ## Main features ProbeFlow is honest about being a focused toolkit rather than a do-everything @@ -149,6 +185,7 @@ dzdv = numeric_derivative(spec.x_array, z_smooth) ## Documentation +- [GUI guide](docs/gui.md) - [Command-line guide](docs/cli.md) - [Createc `.dat` reader notes](docs/createc_dat_reader.md) - [ROI manual workflow checklist](docs/roi_manual_test_checklist.md) diff --git a/docs/gui.md b/docs/gui.md new file mode 100644 index 0000000..4f86d49 --- /dev/null +++ b/docs/gui.md @@ -0,0 +1,139 @@ +# ProbeFlow GUI + +Launch the graphical interface with: + +```bash +probeflow gui +``` + +This guide walks through the most common workflows: loading images, +subtracting a background, exploring the FFT, and finding features. +The screenshots are generated from the real widgets by +`scripts/generate_gui_screenshots.py` — rerun it after UI changes to +refresh them. + +## Loading images + +Use **File → Open folder...** (or the **Open folder** button in the +sidebar) and pick any folder containing scans. ProbeFlow indexes the +folder and shows a thumbnail for every supported file — Createc `.dat`, +Nanonis `.sxm`, RHK `.sm4`, plus `.VERT` and Nanonis spectroscopy files. + +![Browse mode with a folder of scans loaded](images/gui_browse.png) + +Each card shows the scan size, setpoint, and channel info. The sidebar +controls the thumbnail colormap, channel, row alignment, and size, and the +filter buttons (All / Images / Spectra) narrow the grid. **Double-click a +thumbnail** to open it in the image viewer. + +![The image viewer showing a terraced surface](images/gui_viewer.png) + +The viewer opens on the raw topography with a histogram and contrast +controls in the right-hand sidebar (View tab). The toolbar above the image +switches channel and colormap; **← Prev / Next →** at the bottom steps +through the other scans in the folder. Every tool in the viewer is also +reachable from the **Search** box (or `Ctrl+K`) — type a few letters of +what you want ("background", "profile", "fft") and pick the command. + +Raw microscope files are treated as read-only: everything below operates +on an in-memory copy, and saving always writes a new file. + +## Subtracting a background + +Scans usually come with a tilted plane or scan-line artifacts. Two tools +remove them: + +* **Processing → Plane/background subtraction...** (`Ctrl+Shift+B`) — + polynomial plane fits. +* **Processing → STM scan-line background...** (`Ctrl+Alt+B`) — per-line + background estimation designed for terraced STM topographs. + +![STM scan-line background dialog with a linear fit previewed](images/gui_stm_background.png) + +In the STM background dialog: + +1. Pick the **Fit region** — the whole image, or the active ROI if you + have drawn one around a flat terrace. +2. Pick the **Line statistic** (median is robust to steps and tip + changes) and the **Background model**. Models range from *Linear* + through *2nd/3rd order polynomial*, *Low-pass* and *Line by line* to + the *Piezo creep* family — switch between them with the dropdown and + compare the fits. +3. Click **Preview corrected image**. The right-hand plots show the + per-line statistic with the fitted background and the residual per + scan line, plus residual RMS — switch models until the residuals stop + shrinking. +4. Click **Apply**. The subtraction is recorded in the processing + history (undo with `Ctrl+Z`), and exports carry the full provenance. + +## Performing an FFT + +Open **Measurements → FFT viewer...** (`Ctrl+Shift+F`, or the **FFT** +button in the quick toolbar). The viewer computes the FFT of the current +processed image — subtract the background first, or the spectrum is +dominated by the surface tilt. + +![FFT viewer on a moiré superlattice](images/gui_fft.png) + +The left pane shows the real-space source with its pixel and q-space +resolution; the main pane shows log-magnitude FFT with reciprocal-space +axes. The tabs below cover the common reciprocal-space tasks: + +* **Inspect** — intensity histogram with min/max/brightness/contrast + sliders, and a radial profile of the spectrum. +* **Grid** — fit a reciprocal lattice to the Bragg peaks. +* **Correction** — preview lattice undistortion from the fitted grid. +* **Mains** — detect and suppress mains-frequency pickup streaks. +* **Inverse FFT** — mask regions of the spectrum and reconstruct the + filtered image. + +**Focus FFT** and the zoom buttons home in on the spectral content near +the origin, and the **Export** menu saves the spectrum or filtered image. +For a quick periodicity measurement without the full viewer, use +**Measurements → Find spacing from line profile...** on a line ROI. + +## Working with Feature Finder + +Open **Measurements → Feature finder...** to detect point-like features — +atoms, molecules, defects, moiré sites — on the current image. + +![Feature finder marking the minima of a moiré superlattice](images/gui_feature_finder.png) + +1. Choose the **Detection mode**: *Maxima* for protrusions, *Minima* for + depressions. +2. Choose a **Threshold mode** (*Above*, *Below*, or *Between*) and a + height threshold so that only genuine features qualify. +3. Set the **Detection settings**: *Min distance* enforces one detection + per feature, and *Pre-smooth σ* suppresses pixel noise before the + search. +4. Click **Update preview** — detected features are marked on the image + and counted. + +From the **Export** section you can write the coordinates to CSV, render +a synthetic *feature image* (a disk at every detection, useful for pair +correlation and lattice statistics), or send that feature image straight +to the FFT viewer. + +For segmentation-based workflows — particle size statistics, template +matching, classification, lattice extraction — use the **Feature +Counting** window (button in the Browse sidebar). It requires the +optional `features` extra: + +```bash +pip install "probeflow[features]" +``` + +## Beyond the basics + +* **ROIs** — draw rectangles, ellipses, and lines from the ROI tab; ROIs + restrict background fits, FFTs, and statistics, and are saved as + sidecar files next to the scan. +* **Measurements** — distance and angle measurements, line profiles, ROI + statistics, step heights, pair correlation. +* **Spectroscopy** — `.VERT` and Nanonis spectroscopy files open in a + dedicated spectrum viewer; positions can be overlaid on the topograph. +* **Export** — PNG/PDF/CSV/`.sxm`/`.gwy` export with the full processing + history embedded, so any image can be reproduced from the raw file. + +See [cli.md](cli.md) for the command-line equivalents of these +workflows. diff --git a/docs/images/gui_browse.png b/docs/images/gui_browse.png new file mode 100644 index 0000000..6d844df Binary files /dev/null and b/docs/images/gui_browse.png differ diff --git a/docs/images/gui_feature_finder.png b/docs/images/gui_feature_finder.png new file mode 100644 index 0000000..bfa455b Binary files /dev/null and b/docs/images/gui_feature_finder.png differ diff --git a/docs/images/gui_fft.png b/docs/images/gui_fft.png new file mode 100644 index 0000000..57ace84 Binary files /dev/null and b/docs/images/gui_fft.png differ diff --git a/docs/images/gui_stm_background.png b/docs/images/gui_stm_background.png new file mode 100644 index 0000000..dad00cc Binary files /dev/null and b/docs/images/gui_stm_background.png differ diff --git a/docs/images/gui_viewer.png b/docs/images/gui_viewer.png new file mode 100644 index 0000000..da453c6 Binary files /dev/null and b/docs/images/gui_viewer.png differ diff --git a/scripts/generate_gui_screenshots.py b/scripts/generate_gui_screenshots.py new file mode 100644 index 0000000..2479e14 --- /dev/null +++ b/scripts/generate_gui_screenshots.py @@ -0,0 +1,171 @@ +"""Generate the GUI screenshots used by README.md and docs/gui.md. + +Drives the real widgets offscreen against copies of test_data fixtures and +saves PNGs to docs/images/. Rerun after UI changes to refresh the docs: + + QT_QPA_PLATFORM=offscreen python scripts/generate_gui_screenshots.py + +Fixtures are copied to a temp folder first because viewers write ROI/mask +sidecars next to the scans they open. +""" + +from __future__ import annotations + +import shutil +import sys +import tempfile +import time +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +TESTDATA = REPO / "test_data" +OUT = REPO / "docs" / "images" + +BROWSE_FIXTURES = [ + "createc_scan_terrace_109nm.dat", + "createc_scan_atomic_11nm.dat", + "createc_scan_hires_atomic_9nm.dat", + "createc_scan_island_60nm.dat", + "createc_scan_molecular_30nm_pos.dat", + "createc_scan_step_20nm.dat", + "createc_scan_overview_240nm_pos.dat", + "createc_scan_qplus_10ch_afm.dat", + "sxm_moire_10nm.sxm", +] + +VIEWER_FIXTURE = "createc_scan_terrace_109nm.dat" +FFT_FIXTURE = "sxm_moire_10nm.sxm" +FEATURE_FIXTURE = "sxm_moire_10nm.sxm" + + +def _settle(app, seconds: float = 2.0) -> None: + """Pump the event loop until async loaders/thumbnails have painted.""" + from PySide6.QtCore import QThreadPool + + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + app.processEvents() + time.sleep(0.02) + QThreadPool.globalInstance().waitForDone(10_000) + for _ in range(50): + app.processEvents() + time.sleep(0.01) + + +def _grab(widget, name: str) -> None: + path = OUT / name + widget.grab().save(str(path)) + print(f"wrote {path.relative_to(REPO)}") + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + tmp = Path(tempfile.mkdtemp(prefix="probeflow_shots_")) + scans = tmp / "scans" + scans.mkdir() + for name in BROWSE_FIXTURES: + shutil.copy2(TESTDATA / name, scans / name) + + # Keep the user's real GUI config untouched. + import probeflow.gui.config as gui_config + + gui_config.CONFIG_PATH = tmp / "config.json" + + from PySide6.QtWidgets import QApplication + + app = QApplication.instance() or QApplication(sys.argv) + + from probeflow.core.scan_loader import load_scan + from probeflow.gui.models import SxmFile + from probeflow.gui.styling import THEMES, _build_palette, _build_qss + + theme = THEMES["dark"] + app.setPalette(_build_palette(theme)) + app.setStyleSheet(_build_qss(theme)) + + # ── 1. Main window: Browse grid with a folder loaded ────────────────────── + from probeflow.gui.app import ProbeFlowWindow + + win = ProbeFlowWindow(browse_folder=scans) + win.resize(1480, 860) + win.show() + _settle(app, 4.0) + _grab(win, "gui_browse.png") + win.close() + _settle(app, 0.5) + + # ── 2. Image viewer on a terrace scan ────────────────────────────────────── + from probeflow.gui.dialogs.image_viewer import ImageViewerDialog + + entry = SxmFile(path=scans / VIEWER_FIXTURE, stem=Path(VIEWER_FIXTURE).stem) + viewer = ImageViewerDialog(entry, [entry], "gray", theme) + viewer.resize(1380, 860) + viewer.show() + _settle(app, 3.0) + _grab(viewer, "gui_viewer.png") + + # ── 3. STM background dialog opened from that viewer ────────────────────── + viewer._on_open_stm_background() + dlg = viewer._stm_background_dialog + if dlg is not None: + dlg.resize(1180, 760) + _settle(app, 1.0) + dlg._preview("corrected") + _settle(app, 2.0) + _grab(dlg, "gui_stm_background.png") + dlg.close() + viewer.close() + _settle(app, 0.5) + + # ── 4. FFT viewer on an atomic-resolution scan ───────────────────────────── + from probeflow.gui.dialogs.fft_viewer import FFTViewerDialog + + from probeflow.processing.alignment import align_rows + from probeflow.processing.background import subtract_background + + scan = load_scan(scans / FFT_FIXTURE) + # The FFT viewer is normally opened on the processed image; mirror that + # by levelling the raw plane first so the Bragg peaks are visible. + arr = subtract_background(align_rows(scan.planes[0], "median"), order=1) + fft = FFTViewerDialog(arr, scan.scan_range_m, "gray", theme) + fft.resize(1280, 820) + fft.show() + _settle(app, 2.0) + # Zoom in on the spectral content, as a user inspecting Bragg peaks would. + fft._zoom_by(0.25) + _settle(app, 2.0) + _grab(fft, "gui_fft.png") + fft.close() + _settle(app, 0.5) + + # ── 5. Feature finder with a detection run ───────────────────────────────── + from probeflow.gui.dialogs.feature_finder import FeatureFinderDialog + + fscan = load_scan(scans / FEATURE_FIXTURE) + farr = subtract_background(align_rows(fscan.planes[0], "median"), order=1) + w_m, h_m = fscan.scan_range_m + px_x = w_m / farr.shape[1] + px_y = h_m / farr.shape[0] + finder = FeatureFinderDialog( + farr, pixel_size_x_m=px_x, pixel_size_y_m=px_y, theme=theme) + finder.resize(1100, 700) + finder.show() + _settle(app, 1.0) + # Detect the dark moiré sites: minima, smoothed a little, with a minimum + # spacing so each site is found once — the settings a user would dial in. + finder._minima_btn.click() + finder._below_btn.click() + finder._smooth_spin.setValue(2.0) + finder._dist_spin.setValue(8.0) + finder._run_detection() + _settle(app, 2.0) + _grab(finder, "gui_feature_finder.png") + finder.close() + _settle(app, 0.5) + + shutil.rmtree(tmp, ignore_errors=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())