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.
- pyneuronj-1.0/LICENSE +21 -0
- pyneuronj-1.0/MANIFEST.in +3 -0
- pyneuronj-1.0/PKG-INFO +118 -0
- pyneuronj-1.0/README.md +86 -0
- pyneuronj-1.0/README.zh-CN.md +83 -0
- pyneuronj-1.0/examples/basic.py +47 -0
- pyneuronj-1.0/examples/neuronj.png +0 -0
- pyneuronj-1.0/examples/pyneuronj.png +0 -0
- pyneuronj-1.0/pyproject.toml +46 -0
- pyneuronj-1.0/setup.cfg +4 -0
- pyneuronj-1.0/src/pyneuronj/__init__.py +31 -0
- pyneuronj-1.0/src/pyneuronj/_search.py +194 -0
- pyneuronj-1.0/src/pyneuronj/_validation.py +67 -0
- pyneuronj-1.0/src/pyneuronj/api.py +225 -0
- pyneuronj-1.0/src/pyneuronj/data/test.jpg +0 -0
- pyneuronj-1.0/src/pyneuronj/detector.py +242 -0
- pyneuronj-1.0/src/pyneuronj/io.py +81 -0
- pyneuronj-1.0/src/pyneuronj/ndf.py +340 -0
- pyneuronj-1.0/src/pyneuronj/path.py +109 -0
- pyneuronj-1.0/src/pyneuronj/py.typed +0 -0
- pyneuronj-1.0/src/pyneuronj/session.py +110 -0
- pyneuronj-1.0/src/pyneuronj/tracer.py +359 -0
- pyneuronj-1.0/src/pyneuronj.egg-info/PKG-INFO +118 -0
- pyneuronj-1.0/src/pyneuronj.egg-info/SOURCES.txt +32 -0
- pyneuronj-1.0/src/pyneuronj.egg-info/dependency_links.txt +1 -0
- pyneuronj-1.0/src/pyneuronj.egg-info/requires.txt +16 -0
- pyneuronj-1.0/src/pyneuronj.egg-info/top_level.txt +1 -0
- pyneuronj-1.0/tests/helpers.py +34 -0
- pyneuronj-1.0/tests/test_api.py +144 -0
- pyneuronj-1.0/tests/test_detector.py +128 -0
- pyneuronj-1.0/tests/test_graph.py +215 -0
- pyneuronj-1.0/tests/test_ndf.py +308 -0
- pyneuronj-1.0/tests/test_numerics.py +146 -0
- 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.
|
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
|
+
|  |  |
|
|
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% |
|
pyneuronj-1.0/README.md
ADDED
|
@@ -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
|
+
|  |  |
|
|
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
|
+
|  |  |
|
|
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"
|
pyneuronj-1.0/setup.cfg
ADDED
|
@@ -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)
|