A browser-based, no-code interface for segmenting grain/splat boundaries in SEM (or optical) micrographs, replicating the Original | Otsu | U-Net | Ground truth figure layout from the reference paper — on any number of images at once.
pip install -r requirements.txt
streamlit run app.py
# or simply:
bash launch.shThen open http://localhost:8501 in your browser.
Drag-and-drop one or more TIF/PNG/JPG micrographs. The SEM info bar (scale bar, kV, WD, timestamp, Zeiss logo …) is detected and stripped automatically from every image before any analysis runs.
| Mode | What happens |
|---|---|
| Classical only | Otsu (global) + Otsu (adaptive) + Watershed. No GPU, no training, runs in seconds. |
| Bootstrap — per-image | For each image: auto-generate watershed pseudo-labels, train a small U-Net from scratch on those pseudo-labels, run inference. Use when images differ significantly from each other. |
| Bootstrap — shared model | Train the U-Net on the first image's pseudo-labels, then run inference on all subsequent images with that shared model. Faster when images are from the same material/magnification. |
| Load checkpoint | Upload a .pt checkpoint (from a previous training run saved by train.py) and run inference only. |
Preprocessing
CLAHE contrast enhancement— local histogram equalisation, improves faint boundariesBilateral denoising— edge-preserving noise reduction
Watershed
Smooth σ(4–30): controls grain scale. Too low → over-segmented (too many tiny fragments). Too high → under-segmented (grains merge). Default 14 worked well on the provided sample.Prominence threshold(5–40): only keep local minima that are this deep. Higher → fewer, larger grains detected.Boundary thickness(1–7 px): how wide the boundary target is for U-Net training. Wider = more positive pixels = easier class balance.
U-Net (visible when a non-classical mode is selected)
- Epoch count, patch size, batch size, learning rate, base channels — all configurable
- Inference stride: smaller = more overlap = smoother stitching at cost of speed
Boundary line cleanup (yellow-line output)
Test-time augmentation— averages the prediction over 4 flips/rotations; small, free accuracy bumpSkeletonize to 1px line— thins the raw prediction down to a clean centerline instead of a thick blob, matching the look of a hand-drawn ground-truth overlayRemove speckle blobs smaller than (px)— kills isolated noise before thinningFinal line thickness (px)— re-thickens the skeleton to the width you wantAccuracy tolerance band (px)— how many pixels off a prediction can be from the reference line and still count as a match, used for the boundary F1/precision/recall numbers
Click
For each image: a 5-panel comparison figure (Original | Otsu global | Otsu adaptive | Watershed | U-Net), boundary overlays coloured on the original image (U-Net drawn as a thin yellow line, matching the reference figure's ground-truth panel), grain area histogram, U-Net loss curve (if trained), and accuracy metrics.
Cross-image comparison bar charts (grain count, mean area), a pooled grain area distribution histogram across all images, and a summary table downloadable as CSV.
One-click download of a zip containing all masks, figures, and the CSV for every processed image.
There is no setting in this app that guarantees 95% accuracy — that number is a property of your training data and your model, not of the code. What the app now gives you:
- A visual style fix: the U-Net output is post-processed (speckle removal → morphological closing → skeletonization) into a thin yellow line that looks like the hand-drawn ground-truth panel, instead of a thick, noisy blue blob.
- A measurement tool, not a promise:
postprocessing.compute_metrics()reports IoU, Dice, and a pixel-tolerant boundary precision/recall/F1 (the standard way to score edge/boundary predictions, since a line one pixel off from hand-drawn ground truth is still a correct detection). Use these numbers to actually track progress toward whatever accuracy target you need. - A caveat that matters: in Bootstrap modes, and in the app's Results tab by default, "accuracy" is measured against the watershed pseudo-label, not hand-verified ground truth. Watershed pseudo-labels are themselves approximate, so a high score there tells you the U-Net agrees with a classical heuristic — it does not certify real-world accuracy. To get a trustworthy number:
- Hand-correct a batch of watershed masks (see workflow below).
- Train on the corrected masks with
train.py --images ... --masks .... - Run
predict.py --checkpoint ... --gt <hand-labelled mask>— only then does the printed IoU/F1 reflect real accuracy.
- Test-time augmentation (
--ttainpredict.py, or the toggle in the app) gives a small, free accuracy improvement by averaging predictions over 4 orientations — it does not replace having good training data.
In short: the pipeline is built to let you measure and improve toward a target accuracy honestly; treat any single quoted percentage — including ones this app prints from pseudo-labels — with appropriate skepticism until it's checked against real ground truth.
The bootstrap modes use watershed pseudo-labels as training targets for the U-Net. These are a strong classical approximation but are not hand-verified. The resulting U-Net will be qualitatively better than plain Otsu at cleaning up spurious noise but will have the same topological limitations as the watershed method itself.
For production-quality segmentation matching the paper's IoU numbers, you need hand-annotated masks:
- Run the app in Classical only mode to get watershed pseudo-labels
- Download the watershed masks (Download tab → individual image)
- Correct them manually in ImageJ / CVAT / Labelme (they're already close — you're editing, not drawing from scratch)
- Save corrected masks as PNG (white = boundary, black = background), matching the image filename
- Re-train with
train.py --images <folder> --masks <corrected_folder> --epochs 150 --base_ch 32 - Load the resulting
.ptcheckpoint in the app's Load checkpoint mode
sem_app/
├── app.py ← Streamlit app (this is the main file)
├── launch.sh ← One-command startup
├── requirements.txt
│
├── preprocessing.py ← Load + info-bar crop + CLAHE + denoise
├── otsu_baseline.py ← Otsu global and adaptive thresholding
├── pseudo_labels.py ← Watershed pseudo-label generator
├── postprocessing.py ← Thin yellow-line cleanup + accuracy metrics (IoU/Dice/boundary F1)
├── unet_model.py ← U-Net architecture (PyTorch)
├── dataset.py ← Patch extraction, augmentation, sliding-window + TTA inference
└── train.py ← Training loop, loss functions, CLI trainer
Everything runs on CPU — no GPU required. On a modern laptop:
- Classical only: ~2–5s per image
- Bootstrap training (40 epochs, patch 128, base_ch 16): ~45–90s per image on CPU, ~5–10s on GPU
- Inference only (after training): ~5–15s per image on CPU
For batch jobs of 10+ images with U-Net training, a CUDA GPU (even a modest RTX 3060) cuts the per-image time by ~10–20×.