segfix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
segfix-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tim Devereux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
segfix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: segfix
3
+ Version: 0.1.0
4
+ Summary: GUI tool to fix instance segmentation of tree point clouds
5
+ Author-email: Tim Devereux <devereux.tj@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tim-devereux/segfix
8
+ Project-URL: Issues, https://github.com/tim-devereux/segfix/issues
9
+ Keywords: lidar,point-cloud,segmentation,forestry,napari
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: X11 Applications :: Qt
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Visualization
17
+ Requires-Python: <3.13,>=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: napari[all]>=0.9
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: plyfile>=1.0
23
+ Requires-Dist: scipy>=1.10
24
+ Requires-Dist: laspy[lazrs]>=2.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # segfix
30
+
31
+ A GUI tool to **fix the instance segmentation of tree point clouds**. Load a
32
+ segmented LiDAR cloud, see each tree in its own colour, and correct mistakes by
33
+ lassoing points and reassigning, splitting off, or dismissing them — then save
34
+ back to a corrected version of the input, retaining all fields.
35
+
36
+ Built on [napari](https://napari.org) for fast 3D rendering and point selection.
37
+
38
+ <img width="1920" height="1013" alt="image" src="https://github.com/user-attachments/assets/dc454e6e-93ad-45f9-aa20-a61d225c0a94" />
39
+
40
+ ## Contributing
41
+
42
+ Feedback, issues, and PRs all welcome. For issues please use [GitHub issues](https://github.com/tim-devereux/segfix/issues) (not a personal message) so the community can benefit.
43
+ ## Install
44
+
45
+ Requires Python 3.10–3.12 (napari/Qt do not yet ship wheels for 3.13+).
46
+
47
+ ```bash
48
+ git clone https://github.com/tim-devereux/segfix.git
49
+ cd segfix
50
+ conda create -n segfix python=3.11
51
+ conda activate segfix
52
+ pip install -e .
53
+ ```
54
+
55
+ ## Run
56
+
57
+ ```bash
58
+ segfix
59
+ # or generate a practice cloud with built-in segmentation errors first:
60
+ python scripts/make_sample.py sample.ply
61
+ python scripts/make_sample.py --format las sample.las # arbor-shaped LAS
62
+ ```
63
+
64
+ `segfix` will open a startup dialog. Double-click
65
+ a recent project to reopen it, or click **New Project…** to import a point cloud
66
+ file. Importing copies the file into a new project folder (created inside the
67
+ directory you pick, named after the source file) and opens that copy — edits are
68
+ always saved to the copy, never the original source file.
69
+
70
+ Every project opened this way is recorded in `~/.config/segfix/registry.json`
71
+ (a plain JSON file) so it shows up in the "Recent projects"
72
+ list next time — most-recently-opened at the top and preselected, each row
73
+ showing how long ago it was last opened.
74
+
75
+ The per-point tree ID field is auto-detected (`treeID`, `PredInstance`,
76
+ `label`, …); override with `--label-field NAME` if needed.
77
+
78
+ ### Accepted formats
79
+
80
+ **Binary PLY** — RGB-segmented (raycloudtools) or with a label field — and
81
+ **LAS**. Both store their points as fixed-size records at a known offset, which
82
+ is what lets `treecatalog.py` memory-map a whole plot, read back just the points
83
+ of one tree, and on save patch only the label bytes that changed.
84
+
85
+ ### raycloudtools output
86
+
87
+ [raycloudtools](https://github.com/csiro-robotics/raycloudtools)' `rayextract
88
+ trees` writes `<plot>_segmented.ply` — a binary PLY with no label column, each
89
+ point instead **coloured by tree** (`x y z time nx ny nz red green blue alpha`,
90
+ double xyz). segfix detects the RGB encoding, maps each distinct colour to a
91
+ tree, and treats pure black `(0, 0, 0)` as unsegmented. Import the `.ply`
92
+ directly; **Save** patches the colour bytes of points whose tree changed, in
93
+ place, so the file stays in exactly the format `rayextract` produced.
94
+
95
+ One caveat: noise (`X`) and unassigned points are **both** written back as black,
96
+ so once such a file is reloaded the two are indistinguishable (segfix's
97
+ `.segfix.json` sidecar still remembers which were noise for the current
98
+ project). New trees created during editing (`N`, splits) get a deterministic
99
+ colour derived from their id.
100
+
101
+ ### arbor output
102
+
103
+ [arbor](https://github.com/r-lidar/arbor)'s pipeline (`arbor segment …`) writes
104
+ `<plot>_output/<plot>_segmented.laz` — a point cloud with a per-point `treeID`
105
+ Extra-Bytes column (`0` = unassigned). Import that `.laz` directly: segfix
106
+ decompresses it to a `.las` working copy in the project folder (the original
107
+ `.laz` is never touched), you fix the `treeID`s with the workflow below, and
108
+ **Save** patches the `.las` in place *and* re-compresses a fresh `.laz` beside
109
+ it for arbor to re-read.
110
+
111
+ One caveat: if a cloud's `treeID` column is an *unsigned* type, points you
112
+ dismiss as noise (`X`) are written back as `0` (unassigned) — segfix's own
113
+ `.segfix.json` sidecar still remembers they were noise, but a reader of the LAS
114
+ alone cannot tell noise from unassigned. (arbor writes a signed `treeID`, so
115
+ this does not apply to its output.)
116
+
117
+ ## Editing workflow
118
+
119
+ Navigation matches CloudCompare: clouds open **Z-up**, **left-drag rotates,
120
+ right-drag pans, wheel zooms**.
121
+
122
+ Two tables stack in the right-hand panel. **All Trees** (top) lists every tree
123
+ in the file, with a `✓` once it has been reviewed — **double-click a row** to
124
+ load that tree plus its spatial neighbours into the 3D view. **Selected Tree +
125
+ Neighbours** (below) is the review queue for what's currently loaded: a Done
126
+ checkbox per tree, a 👁 column to hide one from the view, a **Fade** column to
127
+ ghost one (still shown, still selectable), and the Prev / Done buttons. Both
128
+ read the same `<cloud>.segfix.json` sidecar, written next to the working copy,
129
+ so a half-finished plot resumes where you left off.
130
+
131
+ 1. Double-click a tree in **All Trees**. The camera flies to it and a
132
+ wireframe box marks it. To declutter a crowded view, use the 👁 (hide) or
133
+ **Fade** column in the lower table on specific trees — or **Hide others** /
134
+ **Fade others** in the top-bar **View** group to do it to every loaded tree
135
+ except the one under review. Fading keeps a tree visible as faint context
136
+ and still lets the lasso grab its points; hiding removes it from both.
137
+ 2. Inspect it. If it's correct, press **Space** — the tree is marked done,
138
+ progress is saved, and the next unfinished tree in the loaded set becomes
139
+ current. That's the loop.
140
+ 3. If it needs fixing, **select** points with the lasso: press **L**, drag a
141
+ freehand loop (Shift adds), **Esc** to go back to navigating. The tree
142
+ under review is always the implicit target.
143
+
144
+ | Key | Operation |
145
+ |-----|-----------|
146
+ | `Space` | Mark current tree done, jump to next unfinished |
147
+ | `←` / `→` | Previous / next tree (without marking done) |
148
+ | `L` | Lasso select |
149
+ | `Ctrl+L` | Lasso, but only points already in the current tree — grabs a clean patch out of an overlapping crown |
150
+ | `Esc` | Back to camera / navigation |
151
+ | `A` | Add selection to the current tree (missing branches, unassigned canopy) |
152
+ | `N` | Split selection off as a new tree (it joins the queue unreviewed) |
153
+ | `U` | Unassign selection — or the whole current tree if nothing is selected |
154
+ | `X` | Mark selection as noise — or the whole current tree if nothing is selected (dismiss a bush/wall in one key) |
155
+ | `Delete` / `Backspace` | Same as `X` (mark noise) |
156
+ | `H` | Show/hide the unassigned + noise points |
157
+ | `C` | Cross section on/off |
158
+ | `Shift+L` | Draw a lasso-section outline |
159
+ | `Shift+C` | Lasso section on/off |
160
+ | `Ctrl+Z` / `Ctrl+Shift+Z` | Undo / Redo |
161
+ | `Ctrl+S` | Save Project |
162
+
163
+ To move stray points *to a neighbour* instead, lasso them and click one of
164
+ the **→ id** buttons in the Current tree panel — one per tree within
165
+ "reach" metres of this one. Clicking that neighbour's table row to make it
166
+ current and pressing `A` does the same thing.
167
+
168
+ To **merge** an over-segmented fragment back in, lasso the whole fragment
169
+ and press `A`; there is no separate merge key.
170
+ 4. For a crowded canopy, two tools in the top bar cut the view down. Both fold
171
+ into the same visibility as the 👁 column, so hidden points are also
172
+ unselectable and the lasso can't grab through them:
173
+ - **Cross section (`C`)** — a slab along X, Y or Z, set with two sliders.
174
+ - **Lasso section (`Shift+C`)** — same idea, but the kept region is an
175
+ outline you draw (`Shift+L`, then drag). It's frozen into a point mask
176
+ as you release, so the camera moves freely afterwards.
177
+
178
+ Both reset when a new tree is loaded.
179
+ 5. Leftover unassigned points may hide missed trees: they're loaded alongside
180
+ every tree you open, so lasso one and press `N` to promote it to a tree of
181
+ its own (it joins the queue unreviewed).
182
+ 6. **Save** (`Ctrl+S`) writes to the project copy, never the original import.
183
+ It patches only the points whose label changed, in place, so the header and
184
+ every other column are untouched byte for byte.
185
+
186
+ ## Layout
187
+
188
+ | File | Responsibility |
189
+ |------|----------------|
190
+ | `model.py` | `PointCloud` data + undo/redo (diff-based) |
191
+ | `io.py` | whole-cloud load/save (binary PLY, LAS/LAZ), label-field and RGB-segmentation detection |
192
+ | `operations.py` | pure, UI-agnostic label edits (reassign/split/unassign/noise) |
193
+ | `analysis.py` | which trees touch which, by sampled point distance (KD-tree) |
194
+ | `lasso.py` | 3D screen-space lasso: camera projection + polygon test |
195
+ | `viewer.py` | napari layer + label→colour mapping |
196
+ | `widgets.py` | Qt dock panel wiring selection → operations |
197
+ | `icons.py` | inline SVG icons for the panel buttons and window |
198
+ | `treecatalog.py` | default mode: memory-mapped tree-label grouping, neighbour load + write-back (`TreeCatalog` = PLY, `LasCatalog` = LAS, `open_catalog` picks) |
199
+ | `scene_ui.py` | tree table + scene controller for the default mode |
200
+ | `registry.py` | on-disk list of recently opened files/projects |
201
+ | `workspace.py` | project folders: copy (or decompress `.laz`→`.las`) an imported file, never touch the source |
202
+ | `startup_ui.py` | startup dialog: pick a recent entry or start a new project |
203
+ | `app.py` | `segfix` CLI entry point |
204
+
205
+ ## Tests
206
+
207
+ ```bash
208
+ pytest # core model, operations, and IO round-trip (no GUI needed)
209
+ ```
segfix-0.1.0/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # segfix
2
+
3
+ A GUI tool to **fix the instance segmentation of tree point clouds**. Load a
4
+ segmented LiDAR cloud, see each tree in its own colour, and correct mistakes by
5
+ lassoing points and reassigning, splitting off, or dismissing them — then save
6
+ back to a corrected version of the input, retaining all fields.
7
+
8
+ Built on [napari](https://napari.org) for fast 3D rendering and point selection.
9
+
10
+ <img width="1920" height="1013" alt="image" src="https://github.com/user-attachments/assets/dc454e6e-93ad-45f9-aa20-a61d225c0a94" />
11
+
12
+ ## Contributing
13
+
14
+ Feedback, issues, and PRs all welcome. For issues please use [GitHub issues](https://github.com/tim-devereux/segfix/issues) (not a personal message) so the community can benefit.
15
+ ## Install
16
+
17
+ Requires Python 3.10–3.12 (napari/Qt do not yet ship wheels for 3.13+).
18
+
19
+ ```bash
20
+ git clone https://github.com/tim-devereux/segfix.git
21
+ cd segfix
22
+ conda create -n segfix python=3.11
23
+ conda activate segfix
24
+ pip install -e .
25
+ ```
26
+
27
+ ## Run
28
+
29
+ ```bash
30
+ segfix
31
+ # or generate a practice cloud with built-in segmentation errors first:
32
+ python scripts/make_sample.py sample.ply
33
+ python scripts/make_sample.py --format las sample.las # arbor-shaped LAS
34
+ ```
35
+
36
+ `segfix` will open a startup dialog. Double-click
37
+ a recent project to reopen it, or click **New Project…** to import a point cloud
38
+ file. Importing copies the file into a new project folder (created inside the
39
+ directory you pick, named after the source file) and opens that copy — edits are
40
+ always saved to the copy, never the original source file.
41
+
42
+ Every project opened this way is recorded in `~/.config/segfix/registry.json`
43
+ (a plain JSON file) so it shows up in the "Recent projects"
44
+ list next time — most-recently-opened at the top and preselected, each row
45
+ showing how long ago it was last opened.
46
+
47
+ The per-point tree ID field is auto-detected (`treeID`, `PredInstance`,
48
+ `label`, …); override with `--label-field NAME` if needed.
49
+
50
+ ### Accepted formats
51
+
52
+ **Binary PLY** — RGB-segmented (raycloudtools) or with a label field — and
53
+ **LAS**. Both store their points as fixed-size records at a known offset, which
54
+ is what lets `treecatalog.py` memory-map a whole plot, read back just the points
55
+ of one tree, and on save patch only the label bytes that changed.
56
+
57
+ ### raycloudtools output
58
+
59
+ [raycloudtools](https://github.com/csiro-robotics/raycloudtools)' `rayextract
60
+ trees` writes `<plot>_segmented.ply` — a binary PLY with no label column, each
61
+ point instead **coloured by tree** (`x y z time nx ny nz red green blue alpha`,
62
+ double xyz). segfix detects the RGB encoding, maps each distinct colour to a
63
+ tree, and treats pure black `(0, 0, 0)` as unsegmented. Import the `.ply`
64
+ directly; **Save** patches the colour bytes of points whose tree changed, in
65
+ place, so the file stays in exactly the format `rayextract` produced.
66
+
67
+ One caveat: noise (`X`) and unassigned points are **both** written back as black,
68
+ so once such a file is reloaded the two are indistinguishable (segfix's
69
+ `.segfix.json` sidecar still remembers which were noise for the current
70
+ project). New trees created during editing (`N`, splits) get a deterministic
71
+ colour derived from their id.
72
+
73
+ ### arbor output
74
+
75
+ [arbor](https://github.com/r-lidar/arbor)'s pipeline (`arbor segment …`) writes
76
+ `<plot>_output/<plot>_segmented.laz` — a point cloud with a per-point `treeID`
77
+ Extra-Bytes column (`0` = unassigned). Import that `.laz` directly: segfix
78
+ decompresses it to a `.las` working copy in the project folder (the original
79
+ `.laz` is never touched), you fix the `treeID`s with the workflow below, and
80
+ **Save** patches the `.las` in place *and* re-compresses a fresh `.laz` beside
81
+ it for arbor to re-read.
82
+
83
+ One caveat: if a cloud's `treeID` column is an *unsigned* type, points you
84
+ dismiss as noise (`X`) are written back as `0` (unassigned) — segfix's own
85
+ `.segfix.json` sidecar still remembers they were noise, but a reader of the LAS
86
+ alone cannot tell noise from unassigned. (arbor writes a signed `treeID`, so
87
+ this does not apply to its output.)
88
+
89
+ ## Editing workflow
90
+
91
+ Navigation matches CloudCompare: clouds open **Z-up**, **left-drag rotates,
92
+ right-drag pans, wheel zooms**.
93
+
94
+ Two tables stack in the right-hand panel. **All Trees** (top) lists every tree
95
+ in the file, with a `✓` once it has been reviewed — **double-click a row** to
96
+ load that tree plus its spatial neighbours into the 3D view. **Selected Tree +
97
+ Neighbours** (below) is the review queue for what's currently loaded: a Done
98
+ checkbox per tree, a 👁 column to hide one from the view, a **Fade** column to
99
+ ghost one (still shown, still selectable), and the Prev / Done buttons. Both
100
+ read the same `<cloud>.segfix.json` sidecar, written next to the working copy,
101
+ so a half-finished plot resumes where you left off.
102
+
103
+ 1. Double-click a tree in **All Trees**. The camera flies to it and a
104
+ wireframe box marks it. To declutter a crowded view, use the 👁 (hide) or
105
+ **Fade** column in the lower table on specific trees — or **Hide others** /
106
+ **Fade others** in the top-bar **View** group to do it to every loaded tree
107
+ except the one under review. Fading keeps a tree visible as faint context
108
+ and still lets the lasso grab its points; hiding removes it from both.
109
+ 2. Inspect it. If it's correct, press **Space** — the tree is marked done,
110
+ progress is saved, and the next unfinished tree in the loaded set becomes
111
+ current. That's the loop.
112
+ 3. If it needs fixing, **select** points with the lasso: press **L**, drag a
113
+ freehand loop (Shift adds), **Esc** to go back to navigating. The tree
114
+ under review is always the implicit target.
115
+
116
+ | Key | Operation |
117
+ |-----|-----------|
118
+ | `Space` | Mark current tree done, jump to next unfinished |
119
+ | `←` / `→` | Previous / next tree (without marking done) |
120
+ | `L` | Lasso select |
121
+ | `Ctrl+L` | Lasso, but only points already in the current tree — grabs a clean patch out of an overlapping crown |
122
+ | `Esc` | Back to camera / navigation |
123
+ | `A` | Add selection to the current tree (missing branches, unassigned canopy) |
124
+ | `N` | Split selection off as a new tree (it joins the queue unreviewed) |
125
+ | `U` | Unassign selection — or the whole current tree if nothing is selected |
126
+ | `X` | Mark selection as noise — or the whole current tree if nothing is selected (dismiss a bush/wall in one key) |
127
+ | `Delete` / `Backspace` | Same as `X` (mark noise) |
128
+ | `H` | Show/hide the unassigned + noise points |
129
+ | `C` | Cross section on/off |
130
+ | `Shift+L` | Draw a lasso-section outline |
131
+ | `Shift+C` | Lasso section on/off |
132
+ | `Ctrl+Z` / `Ctrl+Shift+Z` | Undo / Redo |
133
+ | `Ctrl+S` | Save Project |
134
+
135
+ To move stray points *to a neighbour* instead, lasso them and click one of
136
+ the **→ id** buttons in the Current tree panel — one per tree within
137
+ "reach" metres of this one. Clicking that neighbour's table row to make it
138
+ current and pressing `A` does the same thing.
139
+
140
+ To **merge** an over-segmented fragment back in, lasso the whole fragment
141
+ and press `A`; there is no separate merge key.
142
+ 4. For a crowded canopy, two tools in the top bar cut the view down. Both fold
143
+ into the same visibility as the 👁 column, so hidden points are also
144
+ unselectable and the lasso can't grab through them:
145
+ - **Cross section (`C`)** — a slab along X, Y or Z, set with two sliders.
146
+ - **Lasso section (`Shift+C`)** — same idea, but the kept region is an
147
+ outline you draw (`Shift+L`, then drag). It's frozen into a point mask
148
+ as you release, so the camera moves freely afterwards.
149
+
150
+ Both reset when a new tree is loaded.
151
+ 5. Leftover unassigned points may hide missed trees: they're loaded alongside
152
+ every tree you open, so lasso one and press `N` to promote it to a tree of
153
+ its own (it joins the queue unreviewed).
154
+ 6. **Save** (`Ctrl+S`) writes to the project copy, never the original import.
155
+ It patches only the points whose label changed, in place, so the header and
156
+ every other column are untouched byte for byte.
157
+
158
+ ## Layout
159
+
160
+ | File | Responsibility |
161
+ |------|----------------|
162
+ | `model.py` | `PointCloud` data + undo/redo (diff-based) |
163
+ | `io.py` | whole-cloud load/save (binary PLY, LAS/LAZ), label-field and RGB-segmentation detection |
164
+ | `operations.py` | pure, UI-agnostic label edits (reassign/split/unassign/noise) |
165
+ | `analysis.py` | which trees touch which, by sampled point distance (KD-tree) |
166
+ | `lasso.py` | 3D screen-space lasso: camera projection + polygon test |
167
+ | `viewer.py` | napari layer + label→colour mapping |
168
+ | `widgets.py` | Qt dock panel wiring selection → operations |
169
+ | `icons.py` | inline SVG icons for the panel buttons and window |
170
+ | `treecatalog.py` | default mode: memory-mapped tree-label grouping, neighbour load + write-back (`TreeCatalog` = PLY, `LasCatalog` = LAS, `open_catalog` picks) |
171
+ | `scene_ui.py` | tree table + scene controller for the default mode |
172
+ | `registry.py` | on-disk list of recently opened files/projects |
173
+ | `workspace.py` | project folders: copy (or decompress `.laz`→`.las`) an imported file, never touch the source |
174
+ | `startup_ui.py` | startup dialog: pick a recent entry or start a new project |
175
+ | `app.py` | `segfix` CLI entry point |
176
+
177
+ ## Tests
178
+
179
+ ```bash
180
+ pytest # core model, operations, and IO round-trip (no GUI needed)
181
+ ```
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "segfix"
7
+ version = "0.1.0"
8
+ description = "GUI tool to fix instance segmentation of tree point clouds"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10,<3.13"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Tim Devereux", email = "devereux.tj@gmail.com"}]
14
+ keywords = ["lidar", "point-cloud", "segmentation", "forestry", "napari"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: X11 Applications :: Qt",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering :: Visualization",
23
+ ]
24
+ dependencies = [
25
+ # >=0.9: viewer._patch_napari_shown_in_full_3d reads _PointSliceRequest.shown,
26
+ # a field 0.8 does not have (and whose response kwarg 0.8 spells differently),
27
+ # so the app crashes on the first layer add under older napari.
28
+ "napari[all]>=0.9",
29
+ "numpy>=1.24",
30
+ "plyfile>=1.0",
31
+ "scipy>=1.10",
32
+ # arbor's pipeline emits segmented .laz; laspy reads the header + Extra-Bytes
33
+ # layout and does the .laz<->.las conversion. Per-point read/write still goes
34
+ # through numpy.memmap on the uncompressed .las (see treecatalog.LasCatalog).
35
+ # lazrs is the LAZ backend (laszip works too if that's what's installed).
36
+ "laspy[lazrs]>=2.5",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/tim-devereux/segfix"
41
+ Issues = "https://github.com/tim-devereux/segfix/issues"
42
+
43
+ [project.optional-dependencies]
44
+ dev = ["pytest>=7.0"]
45
+
46
+ [project.scripts]
47
+ segfix = "segfix.app:main"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
segfix-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """segfix — fix instance segmentation of tree point clouds."""
2
+
3
+ from .model import NOISE, UNASSIGNED, PointCloud
4
+
5
+ __all__ = ["PointCloud", "UNASSIGNED", "NOISE"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,68 @@
1
+ """Segmentation-quality analysis: spatial neighbour queries.
2
+
3
+ UI-agnostic, like :mod:`~segfix.operations`. Point-set distances are
4
+ approximated by sampling each tree down to a few thousand points and querying
5
+ a KD-tree — exact enough for "does this touch?" questions at forest scale,
6
+ and fast enough to run per keypress.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+ from .model import NOISE, UNASSIGNED, PointCloud
14
+
15
+ _SAMPLE_CAP = 3000
16
+
17
+
18
+ def _sample(rng, idx: np.ndarray, cap: int = _SAMPLE_CAP) -> np.ndarray:
19
+ if idx.size <= cap:
20
+ return idx
21
+ return rng.choice(idx, cap, replace=False)
22
+
23
+
24
+ def neighbours_by_points(
25
+ cloud: PointCloud, tid: int, reach: float, rng=None
26
+ ) -> set[int]:
27
+ """IDs of trees whose points come within ``reach`` metres of tree
28
+ ``tid``'s points.
29
+
30
+ Bounding-box tests massively over-count neighbours in a closed canopy
31
+ (a tall tree's box spans its whole crown), so boxes are only used as a
32
+ prefilter; candidates are confirmed by sampled point-to-point distance.
33
+ """
34
+ from scipy.spatial import cKDTree
35
+
36
+ rng = rng or np.random.default_rng(0)
37
+ labels, coords = cloud.labels, cloud.coords
38
+ mine = np.flatnonzero(labels == tid)
39
+ if mine.size == 0:
40
+ return set()
41
+ lo = coords[mine].min(axis=0) - reach
42
+ hi = coords[mine].max(axis=0) + reach
43
+ # Per-axis chained test avoids two full (N, 3) boolean temporaries.
44
+ cx, cy, cz = coords[:, 0], coords[:, 1], coords[:, 2]
45
+ in_box = (
46
+ (cx >= lo[0]) & (cx <= hi[0])
47
+ & (cy >= lo[1]) & (cy <= hi[1])
48
+ & (cz >= lo[2]) & (cz <= hi[2])
49
+ )
50
+ in_box[mine] = False
51
+ # Everything below works off the in-box points only, so the per-candidate
52
+ # loop no longer rescans all N labels once per candidate.
53
+ box_idx = np.flatnonzero(in_box)
54
+ box_labels = labels[box_idx]
55
+ cand = np.unique(box_labels)
56
+ cand = cand[(cand != UNASSIGNED) & (cand != NOISE) & (cand != tid)]
57
+ if not cand.size:
58
+ return set()
59
+ kd = cKDTree(coords[_sample(rng, mine)])
60
+ out: set[int] = set()
61
+ for t in cand:
62
+ theirs = box_idx[box_labels == t]
63
+ d, _ = kd.query(
64
+ coords[_sample(rng, theirs)], k=1, distance_upper_bound=reach
65
+ )
66
+ if np.isfinite(d).any():
67
+ out.add(int(t))
68
+ return out