pyneuronj 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 (34) hide show
  1. pyneuronj-1.0/LICENSE +21 -0
  2. pyneuronj-1.0/MANIFEST.in +3 -0
  3. pyneuronj-1.0/PKG-INFO +118 -0
  4. pyneuronj-1.0/README.md +86 -0
  5. pyneuronj-1.0/README.zh-CN.md +83 -0
  6. pyneuronj-1.0/examples/basic.py +47 -0
  7. pyneuronj-1.0/examples/neuronj.png +0 -0
  8. pyneuronj-1.0/examples/pyneuronj.png +0 -0
  9. pyneuronj-1.0/pyproject.toml +46 -0
  10. pyneuronj-1.0/setup.cfg +4 -0
  11. pyneuronj-1.0/src/pyneuronj/__init__.py +31 -0
  12. pyneuronj-1.0/src/pyneuronj/_search.py +194 -0
  13. pyneuronj-1.0/src/pyneuronj/_validation.py +67 -0
  14. pyneuronj-1.0/src/pyneuronj/api.py +225 -0
  15. pyneuronj-1.0/src/pyneuronj/data/test.jpg +0 -0
  16. pyneuronj-1.0/src/pyneuronj/detector.py +242 -0
  17. pyneuronj-1.0/src/pyneuronj/io.py +81 -0
  18. pyneuronj-1.0/src/pyneuronj/ndf.py +340 -0
  19. pyneuronj-1.0/src/pyneuronj/path.py +109 -0
  20. pyneuronj-1.0/src/pyneuronj/py.typed +0 -0
  21. pyneuronj-1.0/src/pyneuronj/session.py +110 -0
  22. pyneuronj-1.0/src/pyneuronj/tracer.py +359 -0
  23. pyneuronj-1.0/src/pyneuronj.egg-info/PKG-INFO +118 -0
  24. pyneuronj-1.0/src/pyneuronj.egg-info/SOURCES.txt +32 -0
  25. pyneuronj-1.0/src/pyneuronj.egg-info/dependency_links.txt +1 -0
  26. pyneuronj-1.0/src/pyneuronj.egg-info/requires.txt +16 -0
  27. pyneuronj-1.0/src/pyneuronj.egg-info/top_level.txt +1 -0
  28. pyneuronj-1.0/tests/helpers.py +34 -0
  29. pyneuronj-1.0/tests/test_api.py +144 -0
  30. pyneuronj-1.0/tests/test_detector.py +128 -0
  31. pyneuronj-1.0/tests/test_graph.py +215 -0
  32. pyneuronj-1.0/tests/test_ndf.py +308 -0
  33. pyneuronj-1.0/tests/test_numerics.py +146 -0
  34. pyneuronj-1.0/tests/test_session_io.py +180 -0
pyneuronj-1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PyNeuronJ contributors
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.
@@ -0,0 +1,3 @@
1
+ include README.md README.zh-CN.md LICENSE
2
+ recursive-include examples *.py *.png
3
+ recursive-include tests *.py
pyneuronj-1.0/PKG-INFO ADDED
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyneuronj
3
+ Version: 1.0
4
+ Summary: 2D nerve-fiber tracing with NeuronJ-compatible NDF annotations
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/SummerColdWind/PyNeuronJ
7
+ Project-URL: Issues, https://github.com/SummerColdWind/PyNeuronJ/issues
8
+ Keywords: neuronj,neurite,nerve-fiber,live-wire,microscopy,ndf
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: scipy>=1.10
20
+ Provides-Extra: fast
21
+ Requires-Dist: numba>=0.60; extra == "fast"
22
+ Provides-Extra: examples
23
+ Requires-Dist: Pillow; extra == "examples"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == "dev"
26
+ Requires-Dist: ruff; extra == "dev"
27
+ Requires-Dist: numba>=0.60; extra == "dev"
28
+ Requires-Dist: Pillow; extra == "dev"
29
+ Requires-Dist: build>=1.2; extra == "dev"
30
+ Requires-Dist: twine>=6.1; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # PyNeuronJ
34
+
35
+ [English](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.md) | [简体中文](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.zh-CN.md)
36
+
37
+ ## What is this?
38
+
39
+ A Python implementation of [NeuronJ](https://imagescience.org/meijering/software/neuronj/)
40
+ for tracing nerve fibers in 2D grayscale images. Give it a start and an end point;
41
+ it returns the centerline and its length. NDF files can be read, edited and saved
42
+ for use in NeuronJ.
43
+
44
+ The algorithm follows [Meijering et al. (2004)](https://doi.org/10.1002/cyto.a.20022).
45
+ The default `practical` mode includes numerical corrections.
46
+ Use `mode="paper"` for the paper formulas.
47
+
48
+ ## Installation
49
+
50
+ Python 3.10 or newer:
51
+
52
+ ```bash
53
+ python -m pip install "pyneuronj[fast,examples]"
54
+ ```
55
+
56
+ Requires NumPy and SciPy. The command also installs Numba for acceleration
57
+ and Pillow for the example. For the library alone, use `python -m pip install pyneuronj`.
58
+ To install from source, run `python -m pip install -e ".[fast,examples]"` in the repository.
59
+
60
+ ## Quick start
61
+
62
+ Results on the bundled image, using the same endpoints:
63
+
64
+ | PyNeuronJ | NeuronJ 1.4.3 |
65
+ |:---:|:---:|
66
+ | ![PyNeuronJ result](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/pyneuronj.png) | ![Official NeuronJ result](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/neuronj.png) |
67
+
68
+ Red: centerline. Green: start. Blue: end.
69
+
70
+ ```python
71
+ from importlib.resources import files
72
+
73
+ import numpy as np
74
+ from PIL import Image
75
+ from pyneuronj import PyNeuronJ, load_ndf, save_ndf
76
+
77
+ with files("pyneuronj").joinpath("data/test.jpg").open("rb") as image_file:
78
+ with Image.open(image_file) as source:
79
+ image = np.asarray(source.convert("L"))
80
+
81
+ tracer = PyNeuronJ(image)
82
+ result = tracer.trace(start=(104, 156), end=(149, 316))
83
+
84
+ print(result.points_xy) # Centerline coordinates
85
+ print(result.length_pixels) # Length in pixels
86
+ save_ndf("test.ndf", result)
87
+
88
+ document = load_ndf("test.ndf")
89
+ document.tracings[0].label = "Fiber 1"
90
+ save_ndf("edited.ndf", document)
91
+ ```
92
+
93
+ Coordinates are `(x, y)` pixel centers. For calibrated lengths, use
94
+ `result.length(pixel_size=(sx, sy))`. Replace the image and endpoints to trace
95
+ your own data.
96
+
97
+ NDF supports versions 1.0.0–1.4.3 and saves as 1.4.3. Types, colors, clusters,
98
+ labels and segment boundaries are preserved. To load the saved annotation
99
+ in NeuronJ, open the same image first.
100
+
101
+ The complete example can also be run from the command line:
102
+
103
+ ```bash
104
+ python examples/basic.py
105
+ ```
106
+
107
+ ## Validation
108
+
109
+ Compared with NeuronJ 1.4.3 on 196 images and 9,305 segments, using the same endpoints.
110
+
111
+ | Difference from NeuronJ | `paper` | `practical` |
112
+ |---|---:|---:|
113
+ | Mean centerline distance | 0.026199 px | 0.006588 px |
114
+ | F1 within 2 px | 99.9260% | 99.9823% |
115
+ | Identical displayed vertices | 8,826 / 9,305 | 8,828 / 9,305 |
116
+ | Segments with mean distance > 5 px | 2 | 0 |
117
+ | Segment length MAE | 0.01140 px | 0.00788 px |
118
+ | Complete-tracing length MAPE | 0.01714% | 0.01489% |
@@ -0,0 +1,86 @@
1
+ # PyNeuronJ
2
+
3
+ [English](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.md) | [简体中文](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.zh-CN.md)
4
+
5
+ ## What is this?
6
+
7
+ A Python implementation of [NeuronJ](https://imagescience.org/meijering/software/neuronj/)
8
+ for tracing nerve fibers in 2D grayscale images. Give it a start and an end point;
9
+ it returns the centerline and its length. NDF files can be read, edited and saved
10
+ for use in NeuronJ.
11
+
12
+ The algorithm follows [Meijering et al. (2004)](https://doi.org/10.1002/cyto.a.20022).
13
+ The default `practical` mode includes numerical corrections.
14
+ Use `mode="paper"` for the paper formulas.
15
+
16
+ ## Installation
17
+
18
+ Python 3.10 or newer:
19
+
20
+ ```bash
21
+ python -m pip install "pyneuronj[fast,examples]"
22
+ ```
23
+
24
+ Requires NumPy and SciPy. The command also installs Numba for acceleration
25
+ and Pillow for the example. For the library alone, use `python -m pip install pyneuronj`.
26
+ To install from source, run `python -m pip install -e ".[fast,examples]"` in the repository.
27
+
28
+ ## Quick start
29
+
30
+ Results on the bundled image, using the same endpoints:
31
+
32
+ | PyNeuronJ | NeuronJ 1.4.3 |
33
+ |:---:|:---:|
34
+ | ![PyNeuronJ result](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/pyneuronj.png) | ![Official NeuronJ result](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/neuronj.png) |
35
+
36
+ Red: centerline. Green: start. Blue: end.
37
+
38
+ ```python
39
+ from importlib.resources import files
40
+
41
+ import numpy as np
42
+ from PIL import Image
43
+ from pyneuronj import PyNeuronJ, load_ndf, save_ndf
44
+
45
+ with files("pyneuronj").joinpath("data/test.jpg").open("rb") as image_file:
46
+ with Image.open(image_file) as source:
47
+ image = np.asarray(source.convert("L"))
48
+
49
+ tracer = PyNeuronJ(image)
50
+ result = tracer.trace(start=(104, 156), end=(149, 316))
51
+
52
+ print(result.points_xy) # Centerline coordinates
53
+ print(result.length_pixels) # Length in pixels
54
+ save_ndf("test.ndf", result)
55
+
56
+ document = load_ndf("test.ndf")
57
+ document.tracings[0].label = "Fiber 1"
58
+ save_ndf("edited.ndf", document)
59
+ ```
60
+
61
+ Coordinates are `(x, y)` pixel centers. For calibrated lengths, use
62
+ `result.length(pixel_size=(sx, sy))`. Replace the image and endpoints to trace
63
+ your own data.
64
+
65
+ NDF supports versions 1.0.0–1.4.3 and saves as 1.4.3. Types, colors, clusters,
66
+ labels and segment boundaries are preserved. To load the saved annotation
67
+ in NeuronJ, open the same image first.
68
+
69
+ The complete example can also be run from the command line:
70
+
71
+ ```bash
72
+ python examples/basic.py
73
+ ```
74
+
75
+ ## Validation
76
+
77
+ Compared with NeuronJ 1.4.3 on 196 images and 9,305 segments, using the same endpoints.
78
+
79
+ | Difference from NeuronJ | `paper` | `practical` |
80
+ |---|---:|---:|
81
+ | Mean centerline distance | 0.026199 px | 0.006588 px |
82
+ | F1 within 2 px | 99.9260% | 99.9823% |
83
+ | Identical displayed vertices | 8,826 / 9,305 | 8,828 / 9,305 |
84
+ | Segments with mean distance > 5 px | 2 | 0 |
85
+ | Segment length MAE | 0.01140 px | 0.00788 px |
86
+ | Complete-tracing length MAPE | 0.01714% | 0.01489% |
@@ -0,0 +1,83 @@
1
+ # PyNeuronJ
2
+
3
+ [English](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.md) | [简体中文](https://github.com/SummerColdWind/PyNeuronJ/blob/main/README.zh-CN.md)
4
+
5
+ ## 这是什么
6
+
7
+ [NeuronJ](https://imagescience.org/meijering/software/neuronj/) 的 Python 实现,
8
+ 用于追踪二维灰度图中的神经纤维。给定起点和终点,返回中心线坐标及长度。
9
+ 支持读取、编辑和保存 NeuronJ 的 NDF 标注文件。
10
+
11
+ 算法依据 [Meijering 等人(2004)的论文](https://doi.org/10.1002/cyto.a.20022)。
12
+ 默认的 `practical` 模式包含数值修正;使用 `mode="paper"` 可采用论文公式。
13
+
14
+ ## 安装
15
+
16
+ 需要 Python 3.10 或更高版本:
17
+
18
+ ```bash
19
+ python -m pip install "pyneuronj[fast,examples]"
20
+ ```
21
+
22
+ 依赖 NumPy 和 SciPy。上述命令还会安装用于加速的 Numba 和读取示例图像的 Pillow。
23
+ 只使用库时,执行 `python -m pip install pyneuronj`。
24
+ 从源码安装时,在仓库目录执行 `python -m pip install -e ".[fast,examples]"`。
25
+
26
+ ## 快速开始
27
+
28
+ 同一张示例图像、相同端点的追踪结果:
29
+
30
+ | PyNeuronJ | 原版 NeuronJ 1.4.3 |
31
+ |:---:|:---:|
32
+ | ![PyNeuronJ 结果](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/pyneuronj.png) | ![原版 NeuronJ 结果](https://raw.githubusercontent.com/SummerColdWind/PyNeuronJ/main/examples/neuronj.png) |
33
+
34
+ 红线为中心线,绿点为起点,蓝点为终点。
35
+
36
+ ```python
37
+ from importlib.resources import files
38
+
39
+ import numpy as np
40
+ from PIL import Image
41
+ from pyneuronj import PyNeuronJ, load_ndf, save_ndf
42
+
43
+ with files("pyneuronj").joinpath("data/test.jpg").open("rb") as image_file:
44
+ with Image.open(image_file) as source:
45
+ image = np.asarray(source.convert("L"))
46
+
47
+ tracer = PyNeuronJ(image)
48
+ result = tracer.trace(start=(104, 156), end=(149, 316))
49
+
50
+ print(result.points_xy) # 中心线坐标
51
+ print(result.length_pixels) # 长度,单位为像素
52
+ save_ndf("test.ndf", result)
53
+
54
+ document = load_ndf("test.ndf")
55
+ document.tracings[0].label = "Fiber 1"
56
+ save_ndf("edited.ndf", document)
57
+ ```
58
+
59
+ 坐标顺序为 `(x, y)`,表示像素中心。已知像素尺寸时,用
60
+ `result.length(pixel_size=(sx, sy))` 计算实际长度。
61
+ 处理自己的图像时,替换图像并选择相应端点即可。
62
+
63
+ NDF 支持读取 1.0.0–1.4.3,保存为 1.4.3。类型、颜色、分组、标签和分段信息均会保留。
64
+ 在 NeuronJ 中打开同一张图像后,即可加载保存的标注。
65
+
66
+ 也可以直接运行完整示例:
67
+
68
+ ```bash
69
+ python examples/basic.py
70
+ ```
71
+
72
+ ## 验证
73
+
74
+ 在 196 张图像、9,305 个分段上,与 NeuronJ 1.4.3 使用相同端点进行对照。
75
+
76
+ | 与 NeuronJ 的差异 | `paper` | `practical` |
77
+ |---|---:|---:|
78
+ | 平均中心线距离 | 0.026199 px | 0.006588 px |
79
+ | F1@2px | 99.9260% | 99.9823% |
80
+ | 显示顶点完全相同 | 8,826 / 9,305 | 8,828 / 9,305 |
81
+ | 单段平均距离 >5 px | 2 | 0 |
82
+ | 分段长度 MAE | 0.01140 px | 0.00788 px |
83
+ | 完整追踪长度 MAPE | 0.01714% | 0.01489% |
@@ -0,0 +1,47 @@
1
+ """Trace the bundled real image and save an editable NeuronJ annotation."""
2
+
3
+ import argparse
4
+ from importlib.resources import files
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ from PIL import Image
9
+
10
+ from pyneuronj import PyNeuronJ, save_ndf
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description=__doc__)
15
+ parser.add_argument(
16
+ "--image", type=Path, help="JPEG/TIFF/PNG; defaults to the bundled test.jpg"
17
+ )
18
+ parser.add_argument(
19
+ "--start", type=int, nargs=2, default=(104, 156), metavar=("X", "Y")
20
+ )
21
+ parser.add_argument(
22
+ "--end", type=int, nargs=2, default=(149, 316), metavar=("X", "Y")
23
+ )
24
+ parser.add_argument("--mode", choices=("paper", "practical"), default="practical")
25
+ parser.add_argument("--output", type=Path, default=Path(".local/example/test.ndf"))
26
+ args = parser.parse_args()
27
+
28
+ image_path = args.image or files("pyneuronj").joinpath("data/test.jpg")
29
+ with image_path.open("rb") as image_file, Image.open(image_file) as source:
30
+ image = np.asarray(source.convert("L"))
31
+ tracer = PyNeuronJ(image, mode=args.mode)
32
+ result = tracer.trace(args.start, args.end)
33
+ args.output.parent.mkdir(parents=True, exist_ok=True)
34
+ save_ndf(args.output, result)
35
+ print(f"Image: {image_path} ({image.shape[1]} x {image.shape[0]})")
36
+ print(f"Path: {len(result.points_xy)} points, {result.length_pixels:.2f} px")
37
+ print(f"Saved: {args.output}")
38
+
39
+ # Reuse the same search tree for endpoint updates.
40
+ tracer.set_start(args.start)
41
+ cached = tracer.trace_to(args.end)
42
+ assert np.array_equal(cached.points_xy, result.points_xy)
43
+ print("Fixed-start query matches the direct query")
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
Binary file
Binary file
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyneuronj"
7
+ version = "1.0"
8
+ description = "2D nerve-fiber tracing with NeuronJ-compatible NDF annotations"
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = ["neuronj", "neurite", "nerve-fiber", "live-wire", "microscopy", "ndf"]
14
+ classifiers = [
15
+ "Intended Audience :: Science/Research",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Topic :: Scientific/Engineering :: Image Processing",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = ["numpy>=1.24", "scipy>=1.10"]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/SummerColdWind/PyNeuronJ"
26
+ Issues = "https://github.com/SummerColdWind/PyNeuronJ/issues"
27
+
28
+ [project.optional-dependencies]
29
+ fast = ["numba>=0.60"]
30
+ examples = ["Pillow"]
31
+ dev = ["pytest>=7", "ruff", "numba>=0.60", "Pillow", "build>=1.2", "twine>=6.1"]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.setuptools.package-data]
37
+ pyneuronj = ["py.typed", "data/test.jpg"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ addopts = "-ra"
42
+ pythonpath = ["src"]
43
+
44
+ [tool.ruff]
45
+ line-length = 88
46
+ target-version = "py310"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,31 @@
1
+ """Two-dimensional fiber tracing from an image and endpoint pairs."""
2
+
3
+ from .api import PyNeuronJ
4
+ from .detector import DetectionResult, NeuronDetector
5
+ from .io import load_trace, rasterize_path, save_csv, save_trace
6
+ from .ndf import NdfDocument, NdfTracing, load_ndf, save_ndf
7
+ from .path import TraceResult, compare_polylines, polyline_length, smooth_and_subsample
8
+ from .session import LiveWireSession
9
+ from .tracer import NeuronTracer, NoPathError, SearchTree
10
+
11
+ __all__ = [
12
+ "PyNeuronJ",
13
+ "TraceResult",
14
+ "NoPathError",
15
+ "DetectionResult",
16
+ "NeuronDetector",
17
+ "NeuronTracer",
18
+ "SearchTree",
19
+ "LiveWireSession",
20
+ "polyline_length",
21
+ "smooth_and_subsample",
22
+ "load_trace",
23
+ "save_trace",
24
+ "save_csv",
25
+ "NdfDocument",
26
+ "NdfTracing",
27
+ "load_ndf",
28
+ "save_ndf",
29
+ "rasterize_path",
30
+ "compare_polylines",
31
+ ]
@@ -0,0 +1,194 @@
1
+ """Independent Dijkstra implementations; the same functions run in Python or Numba.
2
+
3
+ Weights have shape (8,N). Invalid edges are +inf. A graph node is y*width+x.
4
+ No Euclidean diagonal multiplier is applied. No positive epsilon is added.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import numpy as np
9
+
10
+ # Fixed row-major neighbor order; this is an implementation choice.
11
+ DX = np.array([-1, 0, 1, -1, 1, -1, 0, 1], dtype=np.int64)
12
+ DY = np.array([-1, -1, -1, 0, 0, 1, 1, 1], dtype=np.int64)
13
+
14
+
15
+ def heap_search(weights, offsets, source, target, max_edge):
16
+ """Indexed binary min-heap; ties are broken by flattened pixel index."""
17
+ n = weights.shape[1]
18
+ distance = np.full(n, np.inf, dtype=np.float64)
19
+ parent = np.full(n, -1, dtype=np.int64)
20
+ settled = np.zeros(n, dtype=np.bool_)
21
+ heap = np.empty(n, dtype=np.int64)
22
+ position = np.full(n, -1, dtype=np.int64)
23
+ distance[source] = 0.0
24
+ parent[source] = source
25
+ heap[0], position[source] = source, 0
26
+ size = 1
27
+ while size:
28
+ u = heap[0]
29
+ position[u] = -1
30
+ size -= 1
31
+ if size:
32
+ last = heap[size]
33
+ heap[0], position[last] = last, 0
34
+ j = 0
35
+ while True:
36
+ left = 2 * j + 1
37
+ if left >= size:
38
+ break
39
+ right = left + 1
40
+ best = left
41
+ if right < size:
42
+ a, b = heap[left], heap[right]
43
+ if distance[b] < distance[a] or (
44
+ distance[b] == distance[a] and b < a
45
+ ):
46
+ best = right
47
+ a, b = heap[j], heap[best]
48
+ if distance[a] < distance[b] or (distance[a] == distance[b] and a < b):
49
+ break
50
+ heap[j], heap[best] = b, a
51
+ position[b], position[a] = j, best
52
+ j = best
53
+ settled[u] = True
54
+ if u == target:
55
+ break
56
+ du = distance[u]
57
+ for k in range(8):
58
+ w = weights[k, u]
59
+ if not np.isfinite(w):
60
+ continue
61
+ v = u + offsets[k]
62
+ if settled[v]:
63
+ continue
64
+ candidate = du + w
65
+ if candidate < distance[v]:
66
+ distance[v], parent[v] = candidate, u
67
+ j = position[v]
68
+ if j < 0:
69
+ j = size
70
+ size += 1
71
+ heap[j], position[v] = v, j
72
+ while j:
73
+ p = (j - 1) // 2
74
+ a, b = heap[p], heap[j]
75
+ if distance[a] < distance[b] or (
76
+ distance[a] == distance[b] and a < b
77
+ ):
78
+ break
79
+ heap[p], heap[j] = b, a
80
+ position[b], position[a] = p, j
81
+ j = p
82
+ # Unsettled tentative distances are NOT valid completed shortest paths.
83
+ for u in range(n):
84
+ if not settled[u]:
85
+ distance[u], parent[u] = np.inf, -1
86
+ return distance, parent, settled
87
+
88
+
89
+ def dial_search(weights, offsets, source, target, max_edge):
90
+ """Circular FIFO buckets for nonnegative INTEGER weights, including zero.
91
+
92
+ Intrusive linked lists support decrease-key without duplicate queue entries.
93
+ Quantization levels and FIFO tie rules are not specified by the 2004 paper.
94
+ """
95
+ n = weights.shape[1]
96
+ ring_size = max_edge + 1
97
+ head = np.full(ring_size, -1, dtype=np.int64)
98
+ tail = np.full(ring_size, -1, dtype=np.int64)
99
+ next_node = np.full(n, -1, dtype=np.int64)
100
+ prev_node = np.full(n, -1, dtype=np.int64)
101
+ bucket_of = np.full(n, -1, dtype=np.int64)
102
+ distance = np.full(n, np.inf, dtype=np.float64)
103
+ parent = np.full(n, -1, dtype=np.int64)
104
+ settled = np.zeros(n, dtype=np.bool_)
105
+ distance[source], parent[source] = 0.0, source
106
+ head[0], tail[0], bucket_of[source] = source, source, 0
107
+ current, active = 0, 1
108
+ while active:
109
+ b = current % ring_size
110
+ u = head[b]
111
+ if u < 0:
112
+ current += 1
113
+ continue
114
+ head[b] = next_node[u]
115
+ if head[b] < 0:
116
+ tail[b] = -1
117
+ else:
118
+ prev_node[head[b]] = -1
119
+ next_node[u], prev_node[u], bucket_of[u] = -1, -1, -1
120
+ active -= 1
121
+ settled[u] = True
122
+ if u == target:
123
+ break
124
+ for k in range(8):
125
+ w = weights[k, u]
126
+ if not np.isfinite(w):
127
+ continue
128
+ v = u + offsets[k]
129
+ if settled[v]:
130
+ continue
131
+ candidate = current + int(w)
132
+ if candidate < distance[v]:
133
+ old_bucket = bucket_of[v]
134
+ if old_bucket >= 0:
135
+ previous, following = prev_node[v], next_node[v]
136
+ if previous < 0:
137
+ head[old_bucket] = following
138
+ else:
139
+ next_node[previous] = following
140
+ if following < 0:
141
+ tail[old_bucket] = previous
142
+ else:
143
+ prev_node[following] = previous
144
+ else:
145
+ active += 1
146
+ distance[v], parent[v] = candidate, u
147
+ new_bucket = candidate % ring_size
148
+ previous = tail[new_bucket]
149
+ prev_node[v], next_node[v] = previous, -1
150
+ if previous < 0:
151
+ head[new_bucket] = v
152
+ else:
153
+ next_node[previous] = v
154
+ tail[new_bucket], bucket_of[v] = v, new_bucket
155
+ for u in range(n):
156
+ if not settled[u]:
157
+ distance[u], parent[u] = np.inf, -1
158
+ return distance, parent, settled
159
+
160
+
161
+ _COMPILED = {}
162
+
163
+
164
+ def run_search(
165
+ weights,
166
+ width: int,
167
+ source: int,
168
+ target: int,
169
+ *,
170
+ engine: str,
171
+ queue: str,
172
+ max_edge: int,
173
+ ):
174
+ function = heap_search if queue == "heap" else dial_search
175
+ resolved = engine
176
+ if engine == "auto":
177
+ import importlib.util
178
+
179
+ resolved = (
180
+ "numba" if importlib.util.find_spec("numba") is not None else "python"
181
+ )
182
+ if resolved == "numba":
183
+ if queue not in _COMPILED:
184
+ try:
185
+ from numba import njit
186
+ except ImportError as exc:
187
+ raise ImportError(
188
+ "Numba unavailable or incompatible. Install the [fast] extra, or set engine='python'."
189
+ ) from exc
190
+ # Keep floating-point arithmetic identical to the Python backend.
191
+ _COMPILED[queue] = njit(cache=True, nogil=True, fastmath=False)(function)
192
+ function = _COMPILED[queue]
193
+ result = function(weights, DY * width + DX, source, target, max_edge)
194
+ return (*result, resolved)