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.
Files changed (45) hide show
  1. ftw_fbp-0.1.0/LICENSE +21 -0
  2. ftw_fbp-0.1.0/PKG-INFO +143 -0
  3. ftw_fbp-0.1.0/README.md +113 -0
  4. ftw_fbp-0.1.0/pyproject.toml +124 -0
  5. ftw_fbp-0.1.0/setup.cfg +4 -0
  6. ftw_fbp-0.1.0/src/fbp/__init__.py +35 -0
  7. ftw_fbp-0.1.0/src/fbp/core/__init__.py +1 -0
  8. ftw_fbp-0.1.0/src/fbp/core/fast_morph.py +169 -0
  9. ftw_fbp-0.1.0/src/fbp/core/merge.py +119 -0
  10. ftw_fbp-0.1.0/src/fbp/core/postprocess.py +121 -0
  11. ftw_fbp-0.1.0/src/fbp/core/regions.py +150 -0
  12. ftw_fbp-0.1.0/src/fbp/core/surfaces.py +36 -0
  13. ftw_fbp-0.1.0/src/fbp/methods/__init__.py +41 -0
  14. ftw_fbp-0.1.0/src/fbp/methods/base.py +115 -0
  15. ftw_fbp-0.1.0/src/fbp/methods/boundaryvote.py +113 -0
  16. ftw_fbp-0.1.0/src/fbp/methods/components.py +53 -0
  17. ftw_fbp-0.1.0/src/fbp/methods/ftp.py +48 -0
  18. ftw_fbp-0.1.0/src/fbp/methods/gmm.py +49 -0
  19. ftw_fbp-0.1.0/src/fbp/methods/iterh.py +75 -0
  20. ftw_fbp-0.1.0/src/fbp/methods/owt.py +116 -0
  21. ftw_fbp-0.1.0/src/fbp/methods/swst.py +64 -0
  22. ftw_fbp-0.1.0/src/fbp/methods/thresholds.py +39 -0
  23. ftw_fbp-0.1.0/src/fbp/methods/ucm.py +48 -0
  24. ftw_fbp-0.1.0/src/fbp/windowed.py +187 -0
  25. ftw_fbp-0.1.0/src/ftw_fbp.egg-info/PKG-INFO +143 -0
  26. ftw_fbp-0.1.0/src/ftw_fbp.egg-info/SOURCES.txt +43 -0
  27. ftw_fbp-0.1.0/src/ftw_fbp.egg-info/dependency_links.txt +1 -0
  28. ftw_fbp-0.1.0/src/ftw_fbp.egg-info/requires.txt +13 -0
  29. ftw_fbp-0.1.0/src/ftw_fbp.egg-info/top_level.txt +1 -0
  30. ftw_fbp-0.1.0/tests/test_core_fast_morph.py +79 -0
  31. ftw_fbp-0.1.0/tests/test_core_merge.py +64 -0
  32. ftw_fbp-0.1.0/tests/test_core_postprocess.py +101 -0
  33. ftw_fbp-0.1.0/tests/test_core_region_cut.py +110 -0
  34. ftw_fbp-0.1.0/tests/test_core_regions.py +101 -0
  35. ftw_fbp-0.1.0/tests/test_core_surfaces.py +43 -0
  36. ftw_fbp-0.1.0/tests/test_methods_boundaryvote.py +243 -0
  37. ftw_fbp-0.1.0/tests/test_methods_components.py +85 -0
  38. ftw_fbp-0.1.0/tests/test_methods_ftp.py +52 -0
  39. ftw_fbp-0.1.0/tests/test_methods_gmm.py +45 -0
  40. ftw_fbp-0.1.0/tests/test_methods_iterh.py +40 -0
  41. ftw_fbp-0.1.0/tests/test_methods_owt.py +94 -0
  42. ftw_fbp-0.1.0/tests/test_methods_swst.py +51 -0
  43. ftw_fbp-0.1.0/tests/test_methods_thresholds.py +39 -0
  44. ftw_fbp-0.1.0/tests/test_methods_ucm.py +43 -0
  45. 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
+ ```
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()