Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
139 changes: 139 additions & 0 deletions docs/gui.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added docs/images/gui_browse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/gui_feature_finder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/gui_fft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/gui_stm_background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/gui_viewer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 171 additions & 0 deletions scripts/generate_gui_screenshots.py
Original file line number Diff line number Diff line change
@@ -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())
Loading