ftw-fbp 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.
- ftw_fbp-0.1.0/LICENSE +21 -0
- ftw_fbp-0.1.0/PKG-INFO +143 -0
- ftw_fbp-0.1.0/README.md +113 -0
- ftw_fbp-0.1.0/pyproject.toml +124 -0
- ftw_fbp-0.1.0/setup.cfg +4 -0
- ftw_fbp-0.1.0/src/fbp/__init__.py +35 -0
- ftw_fbp-0.1.0/src/fbp/core/__init__.py +1 -0
- ftw_fbp-0.1.0/src/fbp/core/fast_morph.py +169 -0
- ftw_fbp-0.1.0/src/fbp/core/merge.py +119 -0
- ftw_fbp-0.1.0/src/fbp/core/postprocess.py +121 -0
- ftw_fbp-0.1.0/src/fbp/core/regions.py +150 -0
- ftw_fbp-0.1.0/src/fbp/core/surfaces.py +36 -0
- ftw_fbp-0.1.0/src/fbp/methods/__init__.py +41 -0
- ftw_fbp-0.1.0/src/fbp/methods/base.py +115 -0
- ftw_fbp-0.1.0/src/fbp/methods/boundaryvote.py +113 -0
- ftw_fbp-0.1.0/src/fbp/methods/components.py +53 -0
- ftw_fbp-0.1.0/src/fbp/methods/ftp.py +48 -0
- ftw_fbp-0.1.0/src/fbp/methods/gmm.py +49 -0
- ftw_fbp-0.1.0/src/fbp/methods/iterh.py +75 -0
- ftw_fbp-0.1.0/src/fbp/methods/owt.py +116 -0
- ftw_fbp-0.1.0/src/fbp/methods/swst.py +64 -0
- ftw_fbp-0.1.0/src/fbp/methods/thresholds.py +39 -0
- ftw_fbp-0.1.0/src/fbp/methods/ucm.py +48 -0
- ftw_fbp-0.1.0/src/fbp/windowed.py +187 -0
- ftw_fbp-0.1.0/src/ftw_fbp.egg-info/PKG-INFO +143 -0
- ftw_fbp-0.1.0/src/ftw_fbp.egg-info/SOURCES.txt +43 -0
- ftw_fbp-0.1.0/src/ftw_fbp.egg-info/dependency_links.txt +1 -0
- ftw_fbp-0.1.0/src/ftw_fbp.egg-info/requires.txt +13 -0
- ftw_fbp-0.1.0/src/ftw_fbp.egg-info/top_level.txt +1 -0
- ftw_fbp-0.1.0/tests/test_core_fast_morph.py +79 -0
- ftw_fbp-0.1.0/tests/test_core_merge.py +64 -0
- ftw_fbp-0.1.0/tests/test_core_postprocess.py +101 -0
- ftw_fbp-0.1.0/tests/test_core_region_cut.py +110 -0
- ftw_fbp-0.1.0/tests/test_core_regions.py +101 -0
- ftw_fbp-0.1.0/tests/test_core_surfaces.py +43 -0
- ftw_fbp-0.1.0/tests/test_methods_boundaryvote.py +243 -0
- ftw_fbp-0.1.0/tests/test_methods_components.py +85 -0
- ftw_fbp-0.1.0/tests/test_methods_ftp.py +52 -0
- ftw_fbp-0.1.0/tests/test_methods_gmm.py +45 -0
- ftw_fbp-0.1.0/tests/test_methods_iterh.py +40 -0
- ftw_fbp-0.1.0/tests/test_methods_owt.py +94 -0
- ftw_fbp-0.1.0/tests/test_methods_swst.py +51 -0
- ftw_fbp-0.1.0/tests/test_methods_thresholds.py +39 -0
- ftw_fbp-0.1.0/tests/test_methods_ucm.py +43 -0
- ftw_fbp-0.1.0/tests/test_windowed.py +89 -0
ftw_fbp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fields of The World
|
|
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.
|
ftw_fbp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ftw-fbp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Field boundary prediction post-processing algorithms
|
|
5
|
+
Author-email: Isaac Corley <isaac.corley@taylorgeospatial.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/fieldsoftheworld/fbp
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy>=1.26
|
|
19
|
+
Requires-Dist: scikit-image>=0.22
|
|
20
|
+
Requires-Dist: scipy>=1.11
|
|
21
|
+
Provides-Extra: fast
|
|
22
|
+
Requires-Dist: numba>=0.60; extra == "fast"
|
|
23
|
+
Requires-Dist: SimpleITK>=2.4; extra == "fast"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
28
|
+
Requires-Dist: ty>=0.0.1a32; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# fbp
|
|
32
|
+
|
|
33
|
+
Field boundary prediction post-processing in Python.
|
|
34
|
+
|
|
35
|
+
`fbp` turns background, field, and boundary predictions into field instances. It
|
|
36
|
+
includes BoundaryVote, the method behind the FTW Global Data 2nd Edition, along with
|
|
37
|
+
watershed, connected-component, and published split-and-merge baselines.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install ftw-fbp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The package imports as `fbp`.
|
|
46
|
+
|
|
47
|
+
For development:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git clone https://github.com/fieldsoftheworld/fbp.git
|
|
51
|
+
cd fbp
|
|
52
|
+
uv sync --extra dev
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Use
|
|
56
|
+
|
|
57
|
+
A method takes a `(H, W)` class map and a `(3, H, W)` array of background, field,
|
|
58
|
+
and boundary probabilities, and returns `int32` instance labels with 0 as background.
|
|
59
|
+
Each method is a frozen dataclass, so its settings can be written out and loaded
|
|
60
|
+
again as plain data.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from fbp import BoundaryVote
|
|
64
|
+
|
|
65
|
+
method = BoundaryVote(
|
|
66
|
+
foreground="not_background",
|
|
67
|
+
seed_depth=0.01,
|
|
68
|
+
min_line_support=0.3,
|
|
69
|
+
min_area_m2=900,
|
|
70
|
+
)
|
|
71
|
+
instances = method(segmentation, probabilities, pixel_area_m2=9.0)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This method takes every pixel where field or boundary wins as foreground and floods
|
|
75
|
+
1 - p(boundary) from seeds at least 0.01 deep. It keeps a proposed split when at
|
|
76
|
+
least 30% of the pixels on the dividing line favor boundary, and drops fields under
|
|
77
|
+
900 m². The class docstrings describe every option.
|
|
78
|
+
|
|
79
|
+
For rasters too large for one call, `run_windowed` processes `core`-sized windows
|
|
80
|
+
with `halo` pixels of context on each side. The window whose core holds a field's
|
|
81
|
+
centroid keeps that field, and copies of a field that crosses a window boundary are
|
|
82
|
+
merged into one instance.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from fbp import run_windowed
|
|
86
|
+
|
|
87
|
+
instances = run_windowed(
|
|
88
|
+
method,
|
|
89
|
+
segmentation,
|
|
90
|
+
probabilities,
|
|
91
|
+
pixel_area_m2=9.0,
|
|
92
|
+
core=8192,
|
|
93
|
+
halo=512,
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## FTW Global Data 2nd Edition
|
|
98
|
+
|
|
99
|
+
`fbp.FTW_2E` is the setting used for the 2nd Edition field boundaries, run on 2.5 m
|
|
100
|
+
probabilities:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from fbp import FTW_2E
|
|
104
|
+
|
|
105
|
+
instances = FTW_2E(segmentation, probabilities, pixel_area_m2=6.25)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Besides the BoundaryVote settings above (with `min_line_support=0.5`), it drops
|
|
109
|
+
fields whose mean field probability is under 0.25. It then splits the looser
|
|
110
|
+
foreground of pixels with field probability above 0.10 and adds each resulting field
|
|
111
|
+
that the first pass covers less than half of, which recovers fields whose field
|
|
112
|
+
probability rarely wins the argmax. That second pass is skipped when less than 2% of
|
|
113
|
+
the looser foreground is uncovered. With `fast=True`, the probabilities are rounded
|
|
114
|
+
to the 1/255 grid of the published uint8 rasters, and the watershed runs on integer
|
|
115
|
+
levels. This needs the `fast` extra (SimpleITK and numba):
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
pip install 'ftw-fbp[fast]'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
On the published probability rasters, `FTW_2E` reproduces the published raw outlines.
|
|
122
|
+
It matches all 112 fields pixel for pixel on a 10 km window of tile `15TVG_0_0`
|
|
123
|
+
(Iowa), and 647 of 648 fields (99.7% pixel for pixel) on tile `35MRT_0_0` (Rwanda).
|
|
124
|
+
|
|
125
|
+
## Layout
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
src/fbp/core/ geometry and post-processing building blocks
|
|
129
|
+
src/fbp/methods/ the methods, one module each
|
|
130
|
+
tests/ package tests
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A new method subclasses `fbp.Method` and implements `split`. Keep paper-specific
|
|
134
|
+
evaluation, results, figures, and LaTeX outside this package.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
uv run ruff format --check .
|
|
140
|
+
uv run ruff check .
|
|
141
|
+
uv run ty check
|
|
142
|
+
uv run pytest
|
|
143
|
+
```
|
ftw_fbp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# fbp
|
|
2
|
+
|
|
3
|
+
Field boundary prediction post-processing in Python.
|
|
4
|
+
|
|
5
|
+
`fbp` turns background, field, and boundary predictions into field instances. It
|
|
6
|
+
includes BoundaryVote, the method behind the FTW Global Data 2nd Edition, along with
|
|
7
|
+
watershed, connected-component, and published split-and-merge baselines.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install ftw-fbp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The package imports as `fbp`.
|
|
16
|
+
|
|
17
|
+
For development:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git clone https://github.com/fieldsoftheworld/fbp.git
|
|
21
|
+
cd fbp
|
|
22
|
+
uv sync --extra dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
A method takes a `(H, W)` class map and a `(3, H, W)` array of background, field,
|
|
28
|
+
and boundary probabilities, and returns `int32` instance labels with 0 as background.
|
|
29
|
+
Each method is a frozen dataclass, so its settings can be written out and loaded
|
|
30
|
+
again as plain data.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from fbp import BoundaryVote
|
|
34
|
+
|
|
35
|
+
method = BoundaryVote(
|
|
36
|
+
foreground="not_background",
|
|
37
|
+
seed_depth=0.01,
|
|
38
|
+
min_line_support=0.3,
|
|
39
|
+
min_area_m2=900,
|
|
40
|
+
)
|
|
41
|
+
instances = method(segmentation, probabilities, pixel_area_m2=9.0)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This method takes every pixel where field or boundary wins as foreground and floods
|
|
45
|
+
1 - p(boundary) from seeds at least 0.01 deep. It keeps a proposed split when at
|
|
46
|
+
least 30% of the pixels on the dividing line favor boundary, and drops fields under
|
|
47
|
+
900 m². The class docstrings describe every option.
|
|
48
|
+
|
|
49
|
+
For rasters too large for one call, `run_windowed` processes `core`-sized windows
|
|
50
|
+
with `halo` pixels of context on each side. The window whose core holds a field's
|
|
51
|
+
centroid keeps that field, and copies of a field that crosses a window boundary are
|
|
52
|
+
merged into one instance.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from fbp import run_windowed
|
|
56
|
+
|
|
57
|
+
instances = run_windowed(
|
|
58
|
+
method,
|
|
59
|
+
segmentation,
|
|
60
|
+
probabilities,
|
|
61
|
+
pixel_area_m2=9.0,
|
|
62
|
+
core=8192,
|
|
63
|
+
halo=512,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## FTW Global Data 2nd Edition
|
|
68
|
+
|
|
69
|
+
`fbp.FTW_2E` is the setting used for the 2nd Edition field boundaries, run on 2.5 m
|
|
70
|
+
probabilities:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fbp import FTW_2E
|
|
74
|
+
|
|
75
|
+
instances = FTW_2E(segmentation, probabilities, pixel_area_m2=6.25)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Besides the BoundaryVote settings above (with `min_line_support=0.5`), it drops
|
|
79
|
+
fields whose mean field probability is under 0.25. It then splits the looser
|
|
80
|
+
foreground of pixels with field probability above 0.10 and adds each resulting field
|
|
81
|
+
that the first pass covers less than half of, which recovers fields whose field
|
|
82
|
+
probability rarely wins the argmax. That second pass is skipped when less than 2% of
|
|
83
|
+
the looser foreground is uncovered. With `fast=True`, the probabilities are rounded
|
|
84
|
+
to the 1/255 grid of the published uint8 rasters, and the watershed runs on integer
|
|
85
|
+
levels. This needs the `fast` extra (SimpleITK and numba):
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
pip install 'ftw-fbp[fast]'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
On the published probability rasters, `FTW_2E` reproduces the published raw outlines.
|
|
92
|
+
It matches all 112 fields pixel for pixel on a 10 km window of tile `15TVG_0_0`
|
|
93
|
+
(Iowa), and 647 of 648 fields (99.7% pixel for pixel) on tile `35MRT_0_0` (Rwanda).
|
|
94
|
+
|
|
95
|
+
## Layout
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
src/fbp/core/ geometry and post-processing building blocks
|
|
99
|
+
src/fbp/methods/ the methods, one module each
|
|
100
|
+
tests/ package tests
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A new method subclasses `fbp.Method` and implements `split`. Keep paper-specific
|
|
104
|
+
evaluation, results, figures, and LaTeX outside this package.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
uv run ruff format --check .
|
|
110
|
+
uv run ruff check .
|
|
111
|
+
uv run ty check
|
|
112
|
+
uv run pytest
|
|
113
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ftw-fbp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Field boundary prediction post-processing algorithms"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
authors = [{ name = "Isaac Corley", email = "isaac.corley@taylorgeospatial.org" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=1.26",
|
|
25
|
+
"scikit-image>=0.22",
|
|
26
|
+
"scipy>=1.11",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
# For fast=True: integer h-maxima and a bucket-queue watershed.
|
|
31
|
+
fast = [
|
|
32
|
+
"numba>=0.60",
|
|
33
|
+
"SimpleITK>=2.4",
|
|
34
|
+
]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8",
|
|
37
|
+
"pytest-cov>=5",
|
|
38
|
+
"ruff>=0.15",
|
|
39
|
+
"ty>=0.0.1a32",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Repository = "https://github.com/fieldsoftheworld/fbp"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py312"
|
|
50
|
+
line-length = 88
|
|
51
|
+
src = ["src", "tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = [
|
|
55
|
+
"E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "RET", "PTH",
|
|
56
|
+
"ARG", "RUF", "ANN", "D", "PL", "TID", "PIE", "TRY", "NPY",
|
|
57
|
+
]
|
|
58
|
+
ignore = ["FA100", "FA102", "N806", "PLR2004", "TRY003", "RUF046"]
|
|
59
|
+
extend-safe-fixes = []
|
|
60
|
+
unfixable = []
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.per-file-ignores]
|
|
63
|
+
"tests/**" = ["D100", "D101", "D102", "D103", "D107", "ARG001", "ARG002", "ARG005"]
|
|
64
|
+
"src/fbp/methods/boundaryvote.py" = ["PLR0912", "PLR0913", "PLR0915"]
|
|
65
|
+
"src/fbp/core/merge.py" = ["PLR0913", "PLR0917"]
|
|
66
|
+
"src/fbp/windowed.py" = ["PLR0913"] # run_windowed mirrors run() plus window options
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.isort]
|
|
69
|
+
known-first-party = ["fbp"]
|
|
70
|
+
required-imports = []
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.pydocstyle]
|
|
73
|
+
convention = "google"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.flake8-annotations]
|
|
76
|
+
allow-star-arg-any = true
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
79
|
+
ban-relative-imports = "parents"
|
|
80
|
+
|
|
81
|
+
[tool.ruff.format]
|
|
82
|
+
quote-style = "double"
|
|
83
|
+
indent-style = "space"
|
|
84
|
+
line-ending = "lf"
|
|
85
|
+
skip-magic-trailing-comma = false
|
|
86
|
+
docstring-code-format = true
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
minversion = "8"
|
|
90
|
+
testpaths = ["tests"]
|
|
91
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
92
|
+
filterwarnings = ["default"]
|
|
93
|
+
|
|
94
|
+
[tool.coverage.run]
|
|
95
|
+
source = ["fbp"]
|
|
96
|
+
branch = true
|
|
97
|
+
|
|
98
|
+
[tool.coverage.report]
|
|
99
|
+
show_missing = true
|
|
100
|
+
skip_covered = true
|
|
101
|
+
exclude_also = [
|
|
102
|
+
"if __name__ == .__main__.:",
|
|
103
|
+
"if TYPE_CHECKING:",
|
|
104
|
+
"raise NotImplementedError",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[tool.ty.environment]
|
|
108
|
+
python-version = "3.12"
|
|
109
|
+
root = ["src", "tests"]
|
|
110
|
+
|
|
111
|
+
[tool.ty.src]
|
|
112
|
+
include = ["src", "tests"]
|
|
113
|
+
|
|
114
|
+
[tool.ty.rules]
|
|
115
|
+
possibly-unresolved-reference = "error"
|
|
116
|
+
possibly-missing-attribute = "error"
|
|
117
|
+
possibly-missing-implicit-call = "error"
|
|
118
|
+
division-by-zero = "error"
|
|
119
|
+
redundant-cast = "error"
|
|
120
|
+
unused-ignore-comment = "error"
|
|
121
|
+
unresolved-import = "warn"
|
|
122
|
+
|
|
123
|
+
[tool.ty.terminal]
|
|
124
|
+
output-format = "concise"
|
ftw_fbp-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Field boundary prediction post-processing algorithms."""
|
|
2
|
+
|
|
3
|
+
from fbp.methods import (
|
|
4
|
+
FTW_2E,
|
|
5
|
+
BoundaryVote,
|
|
6
|
+
ClosedComponents,
|
|
7
|
+
Components,
|
|
8
|
+
ExtentBoundaryThreshold,
|
|
9
|
+
FTPWatershed,
|
|
10
|
+
GaussianMixture,
|
|
11
|
+
IterativeWatershed,
|
|
12
|
+
Method,
|
|
13
|
+
OrientedWatershed,
|
|
14
|
+
StochasticWatershed,
|
|
15
|
+
UltrametricContours,
|
|
16
|
+
ftp_pipeline,
|
|
17
|
+
)
|
|
18
|
+
from fbp.windowed import run_windowed
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"FTW_2E",
|
|
22
|
+
"BoundaryVote",
|
|
23
|
+
"ClosedComponents",
|
|
24
|
+
"Components",
|
|
25
|
+
"ExtentBoundaryThreshold",
|
|
26
|
+
"FTPWatershed",
|
|
27
|
+
"GaussianMixture",
|
|
28
|
+
"IterativeWatershed",
|
|
29
|
+
"Method",
|
|
30
|
+
"OrientedWatershed",
|
|
31
|
+
"StochasticWatershed",
|
|
32
|
+
"UltrametricContours",
|
|
33
|
+
"ftp_pipeline",
|
|
34
|
+
"run_windowed",
|
|
35
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared geometry primitives every post-processing method is built from."""
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Integer versions of the two scikit-image calls that dominate BoundaryVote's runtime.
|
|
2
|
+
|
|
3
|
+
On 5,120 x 5,120 production windows, ``h_maxima`` and ``watershed`` take 92% of
|
|
4
|
+
BoundaryVote's time. When the surface is on the 1/255 grid of the uint8 probability
|
|
5
|
+
rasters, both can run on integer levels.
|
|
6
|
+
|
|
7
|
+
``h_maxima_itk`` uses ITK grayscale reconstruction by dilation. It gives the same
|
|
8
|
+
result as scikit-image, checked on 5k and 9k windows, and runs about 10x faster on one
|
|
9
|
+
thread. ``watershed_bucket`` floods with a bucket queue in O(N) under scikit-image's
|
|
10
|
+
rules. Marker pixels of equal cost leave the queue in scan order rather than heap
|
|
11
|
+
order, so the lines move only at exact ties (99.99% of pixels agree).
|
|
12
|
+
|
|
13
|
+
Both reject surfaces that are not on the 1/255 grid. ``quantize_prob`` rounds a float
|
|
14
|
+
softmax onto it, which is what storing it as uint8 does. ``Method(fast=True)`` does
|
|
15
|
+
both.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import SimpleITK as sitk # noqa: N813 (the package's own conventional alias)
|
|
22
|
+
from numba import njit
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
raise ImportError(
|
|
25
|
+
"fast=True needs the fast backend: pip install 'ftw-fbp[fast]'"
|
|
26
|
+
) from e
|
|
27
|
+
from skimage.morphology._util import _offsets_to_raveled_neighbors
|
|
28
|
+
|
|
29
|
+
LEVELS = 255.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def quantize_prob(prob: np.ndarray) -> np.ndarray:
|
|
33
|
+
"""Softmax rounded to 1/255 steps per class, as the uint8 production rasters do."""
|
|
34
|
+
return (np.rint(prob.astype(np.float32) * LEVELS) / LEVELS).astype(np.float32)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _to_levels(x: np.ndarray, name: str) -> np.ndarray:
|
|
38
|
+
u = np.rint(x * LEVELS)
|
|
39
|
+
if np.abs(x * LEVELS - u).max() > 0.25: # fp16 error is ~0.06 levels
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"{name} is not on the 1/255 grid; quantize it with quantize_prob"
|
|
42
|
+
)
|
|
43
|
+
return u.astype(np.uint8)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def h_maxima_itk(image: np.ndarray, h: float, footprint: None = None) -> np.ndarray:
|
|
47
|
+
"""``skimage.morphology.h_maxima`` for a [0, 1] surface on the 1/255 grid."""
|
|
48
|
+
if footprint is not None:
|
|
49
|
+
raise ValueError("only the default (full) footprint is supported")
|
|
50
|
+
img = _to_levels(image, "h_maxima input")
|
|
51
|
+
h_lv = int(np.ceil(h * LEVELS - 1e-9)) # residue >= h <=> levels >= ceil(h*255)
|
|
52
|
+
if h_lv > int(img.max()) - int(img.min()):
|
|
53
|
+
return np.zeros(img.shape, np.uint8)
|
|
54
|
+
marker = np.clip(img.astype(np.int16) - h_lv, 0, 255).astype(np.uint8)
|
|
55
|
+
f = sitk.ReconstructionByDilationImageFilter()
|
|
56
|
+
f.SetFullyConnected(True)
|
|
57
|
+
# GetArrayFromImage (a copy): a view of the temporary result can be freed under us.
|
|
58
|
+
rec = sitk.GetArrayFromImage(
|
|
59
|
+
f.Execute(sitk.GetImageFromArray(marker), sitk.GetImageFromArray(img))
|
|
60
|
+
)
|
|
61
|
+
return ((img.astype(np.int16) - rec) >= h_lv).astype(np.uint8)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@njit(cache=True)
|
|
65
|
+
def _bucket_kernel(img, locs, structure, mask, output): # noqa: ANN001, ANN202, PLR0912, PLR0915
|
|
66
|
+
cap = max(1024, 4 * locs.shape[0])
|
|
67
|
+
ix = np.empty(cap, np.int64)
|
|
68
|
+
sr = np.empty(cap, np.int64)
|
|
69
|
+
nx = np.full(cap, -1, np.int64)
|
|
70
|
+
head = np.full(256, -1, np.int64)
|
|
71
|
+
tail = np.full(256, -1, np.int64)
|
|
72
|
+
n = 0
|
|
73
|
+
for k in range(locs.shape[0]):
|
|
74
|
+
v = img[locs[k]]
|
|
75
|
+
ix[n] = locs[k]
|
|
76
|
+
sr[n] = locs[k]
|
|
77
|
+
nx[n] = -1
|
|
78
|
+
if head[v] < 0:
|
|
79
|
+
head[v] = n
|
|
80
|
+
else:
|
|
81
|
+
nx[tail[v]] = n
|
|
82
|
+
tail[v] = n
|
|
83
|
+
n += 1
|
|
84
|
+
cur = 0
|
|
85
|
+
while cur < 256:
|
|
86
|
+
e = head[cur]
|
|
87
|
+
if e < 0:
|
|
88
|
+
cur += 1
|
|
89
|
+
continue
|
|
90
|
+
head[cur] = nx[e]
|
|
91
|
+
if head[cur] < 0:
|
|
92
|
+
tail[cur] = -1
|
|
93
|
+
ei, es = ix[e], sr[e]
|
|
94
|
+
if output[ei] and ei != es:
|
|
95
|
+
continue
|
|
96
|
+
lab = output[es]
|
|
97
|
+
diff = False
|
|
98
|
+
if not mask[ei]:
|
|
99
|
+
diff = True
|
|
100
|
+
else:
|
|
101
|
+
for q in range(structure.shape[0]):
|
|
102
|
+
nb = structure[q] + ei
|
|
103
|
+
if mask[nb]:
|
|
104
|
+
nl = output[nb]
|
|
105
|
+
if nl != 0 and nl != lab: # noqa: PLR1714 (numba: keep it scalar)
|
|
106
|
+
mask[ei] = False
|
|
107
|
+
diff = True
|
|
108
|
+
break
|
|
109
|
+
if not diff:
|
|
110
|
+
output[ei] = lab
|
|
111
|
+
for q in range(structure.shape[0]):
|
|
112
|
+
nb = structure[q] + ei
|
|
113
|
+
if not mask[nb] or output[nb]:
|
|
114
|
+
continue
|
|
115
|
+
v = img[nb]
|
|
116
|
+
if v < cur: # noqa: PLR1730 skimage: cost >= the popping pixel's cost
|
|
117
|
+
v = cur
|
|
118
|
+
if n == cap:
|
|
119
|
+
cap *= 2
|
|
120
|
+
a = np.empty(cap, np.int64)
|
|
121
|
+
a[:n] = ix[:n]
|
|
122
|
+
ix = a
|
|
123
|
+
a = np.empty(cap, np.int64)
|
|
124
|
+
a[:n] = sr[:n]
|
|
125
|
+
sr = a
|
|
126
|
+
a = np.full(cap, -1, np.int64)
|
|
127
|
+
a[:n] = nx[:n]
|
|
128
|
+
nx = a
|
|
129
|
+
ix[n] = nb
|
|
130
|
+
sr[n] = es
|
|
131
|
+
nx[n] = -1
|
|
132
|
+
if head[v] < 0:
|
|
133
|
+
head[v] = n
|
|
134
|
+
else:
|
|
135
|
+
nx[tail[v]] = n
|
|
136
|
+
tail[v] = n
|
|
137
|
+
n += 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def watershed_bucket(
|
|
141
|
+
image: np.ndarray,
|
|
142
|
+
markers: np.ndarray | None = None,
|
|
143
|
+
mask: np.ndarray | None = None,
|
|
144
|
+
watershed_line: bool = True,
|
|
145
|
+
connectivity: int = 1,
|
|
146
|
+
) -> np.ndarray:
|
|
147
|
+
"""Skimage's ``watershed(image, markers, mask=mask, watershed_line=True)``.
|
|
148
|
+
|
|
149
|
+
For ``image = -surface`` with ``surface`` on the 1/255 grid.
|
|
150
|
+
"""
|
|
151
|
+
if not watershed_line or connectivity != 1:
|
|
152
|
+
raise ValueError("only watershed_line=True, connectivity=1 is implemented")
|
|
153
|
+
if markers is None or mask is None:
|
|
154
|
+
raise ValueError("markers and mask are required")
|
|
155
|
+
lv = _to_levels(image + 1.0, "watershed input") # -(1 - pb) + 1 = pb
|
|
156
|
+
img = np.pad(lv, 1)
|
|
157
|
+
msk = np.pad(mask.astype(bool), 1).ravel()
|
|
158
|
+
out = np.pad(markers.astype(np.int32), 1)
|
|
159
|
+
st = _offsets_to_raveled_neighbors(
|
|
160
|
+
img.shape, np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], bool), center=(1, 1)
|
|
161
|
+
)
|
|
162
|
+
_bucket_kernel(
|
|
163
|
+
img.ravel(),
|
|
164
|
+
np.flatnonzero(out).astype(np.int64),
|
|
165
|
+
st.astype(np.int64),
|
|
166
|
+
msk,
|
|
167
|
+
out.ravel(),
|
|
168
|
+
)
|
|
169
|
+
return out[1:-1, 1:-1].copy()
|