Skip to main content

segfix

A GUI tool to fix the instance segmentation of tree point clouds. Load a segmented LiDAR cloud, see each tree in its own colour, and correct mistakes by lassoing points and reassigning, splitting off, or dismissing them — then save back to a corrected version of the input, retaining all fields.

segfix reviewing a segmented plot

Beta note: This software is in beta. Testing is done on Fedora Linux 44 and Windows 11, MacOS should work but has not been tested.

Contributing

Feedback, issues, and PRs all welcome. For issues please use GitHub issues (not a personal message) so the community can benefit.

Install

Requires Python 3.10–3.12.

pip install segfix

Using a dedicated environment:

conda create -n segfix python=3.11
conda activate segfix
pip install segfix

From source

For development, or to get scripts/make_sample.py:

git clone https://github.com/tim-devereux/segfix.git
cd segfix
pip install -e .

Run

segfix

From a source checkout you can generate a practice cloud with built-in segmentation errors first:

python scripts/make_sample.py sample.ply
python scripts/make_sample.py --format las sample.las   # arbor-shaped LAS

Add --spacing for a scan-like cloud, sampled at that pitch across the trunk, crown and ground surfaces — the way to get a file dense enough to exercise the downsample prompt described under Dense clouds:

python scripts/make_sample.py --spacing 0.01 dense.las   # ~4M points, ~140 MB
python scripts/make_sample.py --spacing 0.005 dense.las  # ~16M points, ~540 MB

It reports the spacing it actually achieved, measured with the same estimator segfix runs on load. Add --origin to stand the plot somewhere georeferenced, which is what raises the other load-time question, the global shift:

python scripts/make_sample.py --spacing 0.01 --origin 204300 7223250 12 dense_utm.las

That one asks both questions in a single open — LAS only past 10 km from the origin, since this script's PLY writer stores xyz as float32 and would deliver the cloud already quantised to metres.

segfix will open a startup dialog. Double-click a recent project to reopen it, or click New Project… to import a point cloud file. Importing copies the file into a new project folder (created inside the directory you pick, named after the source file) and opens that copy — edits are always saved to the copy, never the original source file.

Every project opened this way is recorded in ~/.config/segfix/registry.json (a plain JSON file) so it shows up in the "Recent projects" list next time — most-recently-opened at the top and preselected, each row showing how long ago it was last opened.

The per-point tree ID field is auto-detected (treeID, PredInstance, label, …); override with --label-field NAME if needed. Labels that can't be a real tree ID — negatives (other than the noise marker) and values that overflow a signed 32-bit int, both of which some pipelines use as a "no tree" sentinel — are folded into unassigned on load, so they don't show up as spurious trees.

Accepted formats

Binary PLY — RGB-segmented (raycloudtools) or with a label field — and LAS. Both store their points as fixed-size records at a known offset, which is what lets treecatalog.py memory-map a whole plot, read back just the points of one tree, and on save patch only the label bytes that changed.

raycloudtools output

raycloudtools' rayextract trees writes <plot>_segmented.ply — a binary PLY with no label column, each point instead coloured by tree (x y z time nx ny nz red green blue alpha, double xyz). segfix detects the RGB encoding, maps each distinct colour to a tree, and treats pure black (0, 0, 0) as unsegmented. Import the .ply directly; Save patches the colour bytes of points whose tree changed, in place, so the file stays in exactly the format rayextract produced.

One caveat: noise (X) and unassigned points are both written back as black, so once such a file is reloaded the two are indistinguishable (segfix's .segfix.json sidecar still remembers which were noise for the current project). New trees created during editing (N, splits) get a deterministic colour derived from their id.

arbor output

arbor's pipeline (arbor segment …) writes <plot>_output/<plot>_segmented.laz — a point cloud with a per-point treeID Extra-Bytes column (0 = unassigned). Import that .laz directly: segfix decompresses it to a .las working copy in the project folder (the original .laz is never touched), you fix the treeIDs with the workflow below, and Save patches the .las in place and re-compresses a fresh .laz beside it for arbor to re-read.

One caveat: if a cloud's treeID column is an unsigned type, points you dismiss as noise (X) are written back as 0 (unassigned) — segfix's own .segfix.json sidecar still remembers they were noise, but a reader of the LAS alone cannot tell noise from unassigned. (arbor writes a signed treeID, so this does not apply to its output.)

Dense clouds

On load segfix measures the cloud's typical point spacing. If points are closer than 2 cm it offers to downsample for the session — one point per voxel (2 cm by default, editable in the prompt), which is plenty to see and re-label a tree but a fraction of the points to draw and lasso. Choose Keep Full Resolution and nothing changes.

Downsampling is a working-set choice, not a destructive one. Nothing is written until you save, and on Save the labels you edited are interpolated back onto every original point: each full-resolution point follows the nearest kept point that was the same tree as it, and only where that kept point is one you actually re-labelled — so untouched parts of the cloud keep their original labels byte for byte. The saved file has all of its points and all of its fields, in the format it came in.

Opening and saving a cloud this size take long enough to look like a freeze, so both run behind a progress window naming the phase they are in (reading coordinates, measuring density, downsampling, indexing trees). The work happens on the GUI thread, so the bar advances between phases rather than smoothly through them — a phase that is one numpy call cannot repaint while it runs.

If the cloud is also georeferenced far from the origin, the global-shift question comes first and this one follows. Decline the shift and the downsample offer is skipped entirely: unshifted coordinates that large have already lost sub-metre detail to the float32 cast, so a spacing measured on them would be reading quantisation rather than the cloud.

The one thing to know is that a voxel is the resolution limit while you work: where two trees' points share a voxel, one label represents it, and unassigned points that fall inside a tree's voxels aren't separately selectable until you reload at full resolution. Pick a voxel smaller than the detail you need to separate.

Editing workflow

The menu bar carries the session-level actions: File ▸ Open Project… (Ctrl+O, reopens the startup dialog and switches project without a manual restart) and Save Project (Ctrl+S); Edit ▸ Undo / Redo (Ctrl+Z / Ctrl+Shift+Z); Preferences ▸ Theme ▸ Light / Dark, applied immediately and remembered (via QSettings) for next launch; and Help ▸ About segfix for the version, links, and full MIT licence.

Navigation matches CloudCompare: clouds open Z-up, left-drag rotates, right-drag pans, wheel zooms. Double-click a point while navigating to recentre the orbit on it. A metric scale bar and an X/Y/Z orientation tripod sit in the bottom-left of the view; the point size spinner floats in the top-left.

The right-hand panel holds two tables. All Trees (top) lists every tree in the file — a Done column (✓ when reviewed), tree ID and point count, with a running N/M trees (X %) done line above it — double-click a row to load that tree plus its spatial neighbours into the 3D view. Selected Tree + Neighbours (below) is the review queue for what's currently loaded: a Done checkbox per tree, a 👁 column to hide one from the view, a Fade column to ghost one (still shown, still selectable), and the Prev / Done buttons. Both read the same <cloud>.segfix.json sidecar, written next to the working copy, so a half-finished plot resumes where you left off. Finished rows in either table get a green tint. The Current tree actions (Add selection, send-to-neighbour, Split / Unassign / Noise) float as a fixed column pinned to the right edge of the 3D view, next to the points they act on.

  1. Double-click a tree in All Trees. The camera flies to it and a wireframe box marks it. To declutter a crowded view, use the 👁 (hide) or Fade column in the lower table on specific trees — or Hide others / Fade others in the top-bar View group to do it to every loaded tree except the one under review. Fading keeps a tree visible as faint context and still lets the lasso grab its points; hiding removes it from both.

  2. Inspect it. If it's correct, press Space — the tree is marked done, progress is saved, and the next unfinished tree in the loaded set becomes current. That's the loop.

  3. If it needs fixing, select points with the lasso: press L, drag a freehand loop (Shift adds), Esc to go back to navigating. The tree under review is always the implicit target.

    Key Operation
    Space Mark current tree done, jump to next unfinished
    ← / → Previous / next tree (without marking done)
    L Lasso select
    Ctrl+L Lasso, but only points already in the current tree — grabs a clean patch out of an overlapping crown
    Esc Back to camera / navigation
    A Add selection to the current tree (missing branches, unassigned canopy)
    N Split selection off as a new tree (it joins the queue unreviewed)
    U Unassign selection — or the whole current tree if nothing is selected
    X Mark selection as noise — or the whole current tree if nothing is selected (dismiss a bush/wall in one key)
    Delete / Backspace Same as X (mark noise)
    H Show/hide the unassigned + noise points
    C Cross section on/off
    Shift+L Draw a lasso-section outline
    Shift+C Lasso section on/off
    Ctrl+Z / Ctrl+Shift+Z Undo / Redo (also on the Edit menu)
    Ctrl+S Save Project (also on the File menu)
    Ctrl+O Open another project

    To move stray points to a neighbour instead, lasso them and click one of the → id buttons in the Current tree panel — one per tree within "reach" metres of this one. Clicking that neighbour's table row to make it current and pressing A does the same thing.

    To merge an over-segmented fragment back in, lasso the whole fragment and press A; there is no separate merge key.

  4. For a crowded canopy, two tools in the top bar cut the view down. Both fold into the same visibility as the 👁 column, so hidden points are also unselectable and the lasso can't grab through them:

    • Cross section (C) — a slab along X, Y or Z, set with two sliders.
    • Lasso section (Shift+C) — same idea, but the kept region is an outline you draw (Shift+L, then drag). It's frozen into a point mask as you release, so the camera moves freely afterwards.

    Both reset when a new tree is loaded.

  5. Leftover unassigned points may hide missed trees: they're loaded alongside every tree you open, so lasso one and press N to promote it to a tree of its own (it joins the queue unreviewed).

  6. Save (Ctrl+S) writes to the project copy, never the original import. It patches only the points whose label changed, in place, so the header and every other column are untouched byte for byte.

Layout

File Responsibility
model.py PointCloud data + undo/redo (diff-based)
io.py whole-cloud load/save (binary PLY, LAS/LAZ), label-field and RGB-segmentation detection
operations.py pure, UI-agnostic label edits (reassign/split/unassign/noise)
analysis.py which trees touch which, by sampled point distance (KD-tree)
density.py point-spacing measurement, voxel decimation, and the save-time interpolation back to full resolution
lasso.py 3D screen-space lasso: camera projection + polygon test
cloudview.py the vispy 3D canvas: camera, points, selection halo, tree box
viewer.py label→colour mapping and visibility masks
overlays.py scale bar + orientation axes painted over the canvas
theme.py light/dark palette, remembered in QSettings
widgets.py Qt dock panel wiring selection → operations
icons.py inline SVG icons for the panel buttons and window
treecatalog.py default mode: memory-mapped tree-label grouping, neighbour load + write-back (TreeCatalog = PLY, LasCatalog = LAS, open_catalog picks)
scene_ui.py tree table + scene controller for the default mode
shift_ui.py load-time "large coordinates" (global shift) prompt
density_ui.py load-time "dense cloud" (downsample) prompt
progress_ui.py progress window for the two slow operations, opening and saving
registry.py on-disk list of recently opened files/projects
workspace.py project folders: copy (or decompress .laz→.las) an imported file, never touch the source
startup_ui.py startup dialog: pick a recent entry or start a new project
app.py segfix CLI entry point

Tests

pytest        # core model, operations, and IO round-trip (no GUI needed)

Metadata

Release files for segfix 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for segfix 0.6.0
File Size Uploaded
segfix-0.6.0.tar.gz 116.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for segfix 0.6.0
File Interpreter ABI Platform
segfix-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 217.3 kB

Release files / segfix-0.6.0.tar.gz

Download URL segfix-0.6.0.tar.gz
Size 116.8 kB
Tags Source
SHA-256 checksum
How to use checksums
597ac95b8e7d8678dc72bea7323a7ca33fc072c3cf7d6d0d5390e24abff59793
BLAKE2b-256 checksum
How to use checksums
add8045032e540dd1acd55da0e9deb4fe738f1cea1fd450f772579a81aba0aff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / segfix-0.6.0-py3-none-any.whl

Download URL segfix-0.6.0-py3-none-any.whl
Size 100.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
68d788386a0572061efcfde9f01c49a41f6db5841a3e4ccc8bc9c3c9cae34cc3
BLAKE2b-256 checksum
How to use checksums
b67c1b50c388db862351f2268409a4e4d4179f55759d06f68a8ee4585e2fde7e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release history Release notifications | RSS feed

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.13

2 release files

1.0.12

2 release files

1.0.11

2 release files

1.0.10

2 release files

1.0.9

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page