point-affinity-transformer 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.
- point_affinity_transformer-0.1.0/.gitignore +19 -0
- point_affinity_transformer-0.1.0/LICENSE +21 -0
- point_affinity_transformer-0.1.0/PKG-INFO +132 -0
- point_affinity_transformer-0.1.0/README.md +95 -0
- point_affinity_transformer-0.1.0/pyproject.toml +61 -0
- point_affinity_transformer-0.1.0/src/pat/__init__.py +30 -0
- point_affinity_transformer-0.1.0/src/pat/__main__.py +3 -0
- point_affinity_transformer-0.1.0/src/pat/cli.py +85 -0
- point_affinity_transformer-0.1.0/src/pat/cluster.py +97 -0
- point_affinity_transformer-0.1.0/src/pat/inference.py +212 -0
- point_affinity_transformer-0.1.0/src/pat/io.py +49 -0
- point_affinity_transformer-0.1.0/src/pat/metrics.py +41 -0
- point_affinity_transformer-0.1.0/src/pat/model.py +311 -0
- point_affinity_transformer-0.1.0/src/pat/preprocess.py +68 -0
- point_affinity_transformer-0.1.0/src/pat/presets.py +25 -0
- point_affinity_transformer-0.1.0/src/pat/pretrained.py +117 -0
- point_affinity_transformer-0.1.0/src/pat/train/__init__.py +19 -0
- point_affinity_transformer-0.1.0/src/pat/train/augment.py +36 -0
- point_affinity_transformer-0.1.0/src/pat/train/checkpoint.py +28 -0
- point_affinity_transformer-0.1.0/src/pat/train/config.py +117 -0
- point_affinity_transformer-0.1.0/src/pat/train/data.py +255 -0
- point_affinity_transformer-0.1.0/src/pat/train/evaluate.py +78 -0
- point_affinity_transformer-0.1.0/src/pat/train/loop.py +174 -0
- point_affinity_transformer-0.1.0/tests/conftest.py +34 -0
- point_affinity_transformer-0.1.0/tests/test_cluster.py +36 -0
- point_affinity_transformer-0.1.0/tests/test_features.py +112 -0
- point_affinity_transformer-0.1.0/tests/test_metrics.py +21 -0
- point_affinity_transformer-0.1.0/tests/test_model.py +82 -0
- point_affinity_transformer-0.1.0/tests/test_pretrained.py +39 -0
- point_affinity_transformer-0.1.0/tests/test_segment.py +15 -0
- point_affinity_transformer-0.1.0/tests/test_train.py +66 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
wandb/*
|
|
2
|
+
output/*
|
|
3
|
+
outputs/*
|
|
4
|
+
logs/*
|
|
5
|
+
*.pyc
|
|
6
|
+
util/__pycache__/*
|
|
7
|
+
util/__pycache__/datasets.cpython-39.pyc
|
|
8
|
+
*.png
|
|
9
|
+
output.log
|
|
10
|
+
*.log
|
|
11
|
+
misc/*
|
|
12
|
+
data/*
|
|
13
|
+
site/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
ckpt/
|
|
17
|
+
dist/
|
|
18
|
+
.ipynb_checkpoints/
|
|
19
|
+
.cache/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jakob Troidl
|
|
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,132 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: point-affinity-transformer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Point Affinity Transformers for global neuron shape reasoning
|
|
5
|
+
Project-URL: Paper, https://www.biorxiv.org/content/10.1101/2024.11.24.625067v1
|
|
6
|
+
Project-URL: Source, https://github.com/jakobtroidl/neuron-shape-reasoning
|
|
7
|
+
Author: Jakob Troidl
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: connectomics,neuron,point cloud,segmentation,transformer
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: navis>=1.4
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: pyyaml
|
|
19
|
+
Requires-Dist: safetensors>=0.4
|
|
20
|
+
Requires-Dist: scikit-learn>=1.2
|
|
21
|
+
Requires-Dist: torch>=2.1
|
|
22
|
+
Requires-Dist: tqdm
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: einops; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: python-voi==0.0.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Requires-Dist: timm; extra == 'dev'
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: mkdocs-material>=9; extra == 'docs'
|
|
31
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
32
|
+
Provides-Extra: pretrained
|
|
33
|
+
Requires-Dist: gdown>=5; extra == 'pretrained'
|
|
34
|
+
Provides-Extra: wandb
|
|
35
|
+
Requires-Dist: wandb; extra == 'wandb'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
[](https://www.biorxiv.org/content/10.1101/2024.11.24.625067v1)
|
|
39
|
+
[](https://drive.google.com/drive/folders/1vgPSYsqDJyhv1s9aD09GgXqAeGm0Rb1V?usp=sharing)
|
|
40
|
+
[](https://drive.google.com/drive/folders/1vgPSYsqDJyhv1s9aD09GgXqAeGm0Rb1V?usp=sharing)
|
|
41
|
+
|
|
42
|
+
## Point Affinity Transformers
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Installation
|
|
46
|
+
```bash
|
|
47
|
+
pip install point-affinity-transformer
|
|
48
|
+
```
|
|
49
|
+
The package is imported as `pat`. Install a PyTorch build matching your CUDA version first ([pytorch.org](https://pytorch.org/get-started/locally/)).
|
|
50
|
+
|
|
51
|
+
### Getting Started
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import pat
|
|
55
|
+
from pat.io import read_swcs
|
|
56
|
+
|
|
57
|
+
points, gt = read_swcs(["a.swc", "b.swc"]) # (N, 3) coordinates in µm
|
|
58
|
+
points = points / 659.88 # your normalization: the same as for the training data
|
|
59
|
+
model = pat.load_pretrained("my_model", device="cuda") # directory from pat train or pat.save_pretrained
|
|
60
|
+
|
|
61
|
+
seg = pat.segment(model, points, threshold=0.8) # one label per point, -1 = noise
|
|
62
|
+
print(seg.num_neurons, pat.metrics.voi(seg.labels, gt))
|
|
63
|
+
|
|
64
|
+
enc = pat.encode(model, points) # encode once ...
|
|
65
|
+
A = enc.affinity(queries, others) # ... then query affinities between any points
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Point Clouds and Features
|
|
69
|
+
|
|
70
|
+
Point clouds of shape `(B, N, 3)` with `D` extra features are passed to the model as `(B, N, 3 + D)`. Training only needs a neuron label per point.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import torch
|
|
74
|
+
import torch.nn.functional as F
|
|
75
|
+
import pat
|
|
76
|
+
|
|
77
|
+
B, N, D = 8, 1024, 4 # random toy data: replace with your own
|
|
78
|
+
xyz = torch.rand(B, N, 3) * 2 - 1 # coordinates, already normalized to roughly [-1, 1]
|
|
79
|
+
features = torch.randn(B, N, D) # D features per point
|
|
80
|
+
labels = torch.randint(0, 3, (B, N)) # neuron id of every point (training targets)
|
|
81
|
+
pc = torch.cat([xyz, features], dim=-1) # (B, N, 3 + D)
|
|
82
|
+
|
|
83
|
+
model = pat.create_model("ae_d256_m64", depth=4, feature_dim=D)
|
|
84
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
|
|
85
|
+
|
|
86
|
+
# Training: sample random point pairs, target = 1 if both lie on the same neuron
|
|
87
|
+
def gather(x, idx): # x: (B, N, C), idx: (B, Q) -> (B, Q, C)
|
|
88
|
+
return x.gather(1, idx[..., None].expand(-1, -1, x.shape[-1]))
|
|
89
|
+
|
|
90
|
+
model.train()
|
|
91
|
+
for step in range(100):
|
|
92
|
+
i, j = torch.randint(0, N, (2, B, 2048)) # 2048 query pairs per cloud
|
|
93
|
+
target = (labels.gather(1, i) == labels.gather(1, j)).float()
|
|
94
|
+
logits = model(pc, None, gather(pc, i), gather(pc, j), logits=True) # mask=None: no padding
|
|
95
|
+
loss = F.binary_cross_entropy_with_logits(logits, target)
|
|
96
|
+
optimizer.zero_grad()
|
|
97
|
+
loss.backward()
|
|
98
|
+
optimizer.step()
|
|
99
|
+
|
|
100
|
+
# Inference
|
|
101
|
+
model.eval()
|
|
102
|
+
with torch.no_grad():
|
|
103
|
+
latents = model.encode(pc) # (B, num_latents, dim), once per cloud
|
|
104
|
+
affinity = model.decode(latents, pc[:, :5], pc[:, 5:10]) # (B, 5): any query pairs, with features
|
|
105
|
+
|
|
106
|
+
seg = pat.segment(model, pc[0]) # cluster one cloud: labels (N,), -1 = noise
|
|
107
|
+
pat.save_pretrained(model, "my_model") # reload with pat.load_pretrained("my_model")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Coordinates should lie within roughly [-1, 1]. Clouds of different sizes can be padded with `pat.preprocess.pad_batch`, which returns the `mask` to pass instead of
|
|
111
|
+
`None`. A trained 3D model can be extended to `D` features with `pat.expand_features(model, D)` and then fine-tuned
|
|
112
|
+
the same way. To train from SWC files, run `pat train config.yaml` (examples in [`research/configs`](research/configs)),
|
|
113
|
+
optionally with `data.features: [radius]` or, from Python, `pat.train.train(cfg, feature_fn=...)`.
|
|
114
|
+
|
|
115
|
+
See the [quickstart notebook](examples/quickstart.ipynb) for an end-to-end example, and the documentation
|
|
116
|
+
(`pip install -e ".[docs]" && mkdocs serve`) for the API reference.
|
|
117
|
+
|
|
118
|
+
### Citation
|
|
119
|
+
```bibtex
|
|
120
|
+
@techreport{troidlgnsr2024,
|
|
121
|
+
title = {Global Neuron Shape Reasoning with Point Affinity Transformers},
|
|
122
|
+
author = {Troidl, Jakob and Knittel, Johannes and Li, Wanhua and Zhan, Fengnang and Pfister*, Hanspeter and Turaga*, Srinivas},
|
|
123
|
+
journal = {bioRxiv},
|
|
124
|
+
year = {2024},
|
|
125
|
+
publisher = {Cold Spring Harbor Laboratory},
|
|
126
|
+
keywords = {preprint}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Acknowledgements
|
|
131
|
+
We acknowledge NSF grants CRCNS-2309041, NCS-FO-2124179, and NIH grant R01HD104969. We also thank the HHMI Janelia Visiting Scientist Program and the Harvard Data Science Initiative Postdoctoral Fellowship for their support. The code is partially based on [3DShape2VecSet](https://arxiv.org/abs/2301.11445) by Zhang et. al.
|
|
132
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[](https://www.biorxiv.org/content/10.1101/2024.11.24.625067v1)
|
|
2
|
+
[](https://drive.google.com/drive/folders/1vgPSYsqDJyhv1s9aD09GgXqAeGm0Rb1V?usp=sharing)
|
|
3
|
+
[](https://drive.google.com/drive/folders/1vgPSYsqDJyhv1s9aD09GgXqAeGm0Rb1V?usp=sharing)
|
|
4
|
+
|
|
5
|
+
## Point Affinity Transformers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Installation
|
|
9
|
+
```bash
|
|
10
|
+
pip install point-affinity-transformer
|
|
11
|
+
```
|
|
12
|
+
The package is imported as `pat`. Install a PyTorch build matching your CUDA version first ([pytorch.org](https://pytorch.org/get-started/locally/)).
|
|
13
|
+
|
|
14
|
+
### Getting Started
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import pat
|
|
18
|
+
from pat.io import read_swcs
|
|
19
|
+
|
|
20
|
+
points, gt = read_swcs(["a.swc", "b.swc"]) # (N, 3) coordinates in µm
|
|
21
|
+
points = points / 659.88 # your normalization: the same as for the training data
|
|
22
|
+
model = pat.load_pretrained("my_model", device="cuda") # directory from pat train or pat.save_pretrained
|
|
23
|
+
|
|
24
|
+
seg = pat.segment(model, points, threshold=0.8) # one label per point, -1 = noise
|
|
25
|
+
print(seg.num_neurons, pat.metrics.voi(seg.labels, gt))
|
|
26
|
+
|
|
27
|
+
enc = pat.encode(model, points) # encode once ...
|
|
28
|
+
A = enc.affinity(queries, others) # ... then query affinities between any points
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Point Clouds and Features
|
|
32
|
+
|
|
33
|
+
Point clouds of shape `(B, N, 3)` with `D` extra features are passed to the model as `(B, N, 3 + D)`. Training only needs a neuron label per point.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import torch
|
|
37
|
+
import torch.nn.functional as F
|
|
38
|
+
import pat
|
|
39
|
+
|
|
40
|
+
B, N, D = 8, 1024, 4 # random toy data: replace with your own
|
|
41
|
+
xyz = torch.rand(B, N, 3) * 2 - 1 # coordinates, already normalized to roughly [-1, 1]
|
|
42
|
+
features = torch.randn(B, N, D) # D features per point
|
|
43
|
+
labels = torch.randint(0, 3, (B, N)) # neuron id of every point (training targets)
|
|
44
|
+
pc = torch.cat([xyz, features], dim=-1) # (B, N, 3 + D)
|
|
45
|
+
|
|
46
|
+
model = pat.create_model("ae_d256_m64", depth=4, feature_dim=D)
|
|
47
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
|
|
48
|
+
|
|
49
|
+
# Training: sample random point pairs, target = 1 if both lie on the same neuron
|
|
50
|
+
def gather(x, idx): # x: (B, N, C), idx: (B, Q) -> (B, Q, C)
|
|
51
|
+
return x.gather(1, idx[..., None].expand(-1, -1, x.shape[-1]))
|
|
52
|
+
|
|
53
|
+
model.train()
|
|
54
|
+
for step in range(100):
|
|
55
|
+
i, j = torch.randint(0, N, (2, B, 2048)) # 2048 query pairs per cloud
|
|
56
|
+
target = (labels.gather(1, i) == labels.gather(1, j)).float()
|
|
57
|
+
logits = model(pc, None, gather(pc, i), gather(pc, j), logits=True) # mask=None: no padding
|
|
58
|
+
loss = F.binary_cross_entropy_with_logits(logits, target)
|
|
59
|
+
optimizer.zero_grad()
|
|
60
|
+
loss.backward()
|
|
61
|
+
optimizer.step()
|
|
62
|
+
|
|
63
|
+
# Inference
|
|
64
|
+
model.eval()
|
|
65
|
+
with torch.no_grad():
|
|
66
|
+
latents = model.encode(pc) # (B, num_latents, dim), once per cloud
|
|
67
|
+
affinity = model.decode(latents, pc[:, :5], pc[:, 5:10]) # (B, 5): any query pairs, with features
|
|
68
|
+
|
|
69
|
+
seg = pat.segment(model, pc[0]) # cluster one cloud: labels (N,), -1 = noise
|
|
70
|
+
pat.save_pretrained(model, "my_model") # reload with pat.load_pretrained("my_model")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Coordinates should lie within roughly [-1, 1]. Clouds of different sizes can be padded with `pat.preprocess.pad_batch`, which returns the `mask` to pass instead of
|
|
74
|
+
`None`. A trained 3D model can be extended to `D` features with `pat.expand_features(model, D)` and then fine-tuned
|
|
75
|
+
the same way. To train from SWC files, run `pat train config.yaml` (examples in [`research/configs`](research/configs)),
|
|
76
|
+
optionally with `data.features: [radius]` or, from Python, `pat.train.train(cfg, feature_fn=...)`.
|
|
77
|
+
|
|
78
|
+
See the [quickstart notebook](examples/quickstart.ipynb) for an end-to-end example, and the documentation
|
|
79
|
+
(`pip install -e ".[docs]" && mkdocs serve`) for the API reference.
|
|
80
|
+
|
|
81
|
+
### Citation
|
|
82
|
+
```bibtex
|
|
83
|
+
@techreport{troidlgnsr2024,
|
|
84
|
+
title = {Global Neuron Shape Reasoning with Point Affinity Transformers},
|
|
85
|
+
author = {Troidl, Jakob and Knittel, Johannes and Li, Wanhua and Zhan, Fengnang and Pfister*, Hanspeter and Turaga*, Srinivas},
|
|
86
|
+
journal = {bioRxiv},
|
|
87
|
+
year = {2024},
|
|
88
|
+
publisher = {Cold Spring Harbor Laboratory},
|
|
89
|
+
keywords = {preprint}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Acknowledgements
|
|
94
|
+
We acknowledge NSF grants CRCNS-2309041, NCS-FO-2124179, and NIH grant R01HD104969. We also thank the HHMI Janelia Visiting Scientist Program and the Harvard Data Science Initiative Postdoctoral Fellowship for their support. The code is partially based on [3DShape2VecSet](https://arxiv.org/abs/2301.11445) by Zhang et. al.
|
|
95
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "point-affinity-transformer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Point Affinity Transformers for global neuron shape reasoning"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Jakob Troidl" }]
|
|
14
|
+
keywords = ["connectomics", "neuron", "point cloud", "transformer", "segmentation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"torch>=2.1",
|
|
22
|
+
"numpy",
|
|
23
|
+
"scikit-learn>=1.2",
|
|
24
|
+
"safetensors>=0.4",
|
|
25
|
+
"navis>=1.4",
|
|
26
|
+
"pandas",
|
|
27
|
+
"pyyaml",
|
|
28
|
+
"tqdm",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
pretrained = ["gdown>=5"]
|
|
33
|
+
wandb = ["wandb"]
|
|
34
|
+
docs = ["mkdocs-material>=9", "mkdocstrings[python]>=0.24"]
|
|
35
|
+
# timm, einops and python-voi are for comparing against research/legacy;
|
|
36
|
+
# python-voi 0.0.3 ships an empty wheel, so pin 0.0.2.
|
|
37
|
+
dev = ["pytest", "ruff", "timm", "einops", "python-voi==0.0.2"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
pat = "pat.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Paper = "https://www.biorxiv.org/content/10.1101/2024.11.24.625067v1"
|
|
44
|
+
Source = "https://github.com/jakobtroidl/neuron-shape-reasoning"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/pat"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = ["/src/pat", "/tests", "/README.md", "/LICENSE"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 120
|
|
57
|
+
src = ["src"]
|
|
58
|
+
target-version = "py39"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "B"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Point Affinity Transformer: global neuron shape reasoning on point clouds."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from . import cluster, metrics
|
|
6
|
+
from .inference import EncodedCloud, Segmentation, encode, segment
|
|
7
|
+
from .model import PATConfig, PointAffinityTransformer, expand_features
|
|
8
|
+
from .presets import PRESETS, create_model
|
|
9
|
+
from .pretrained import load_pretrained, save_pretrained
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("point-affinity-transformer") # defined in pyproject.toml
|
|
13
|
+
except PackageNotFoundError: # running from a source tree without installing
|
|
14
|
+
__version__ = "unknown"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"PATConfig",
|
|
18
|
+
"PointAffinityTransformer",
|
|
19
|
+
"EncodedCloud",
|
|
20
|
+
"Segmentation",
|
|
21
|
+
"encode",
|
|
22
|
+
"segment",
|
|
23
|
+
"create_model",
|
|
24
|
+
"PRESETS",
|
|
25
|
+
"load_pretrained",
|
|
26
|
+
"save_pretrained",
|
|
27
|
+
"expand_features",
|
|
28
|
+
"cluster",
|
|
29
|
+
"metrics",
|
|
30
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Command line interface: ``pat train | eval | convert``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _train(args):
|
|
9
|
+
from .train import load_config, train
|
|
10
|
+
|
|
11
|
+
train(load_config(args.config, args.set))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _eval(args):
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
from .pretrained import load_pretrained
|
|
20
|
+
from .train import MergedNeuronDataset, SkeletonStore, TensorDirDataset, evaluate, format_results, load_config
|
|
21
|
+
from .train.data import read_root_ids
|
|
22
|
+
|
|
23
|
+
cfg = load_config(args.config, args.set)
|
|
24
|
+
torch.manual_seed(cfg.seed)
|
|
25
|
+
model = load_pretrained(args.model, device=args.device or cfg.device)
|
|
26
|
+
if args.items:
|
|
27
|
+
dataset = TensorDirDataset(args.items)
|
|
28
|
+
else:
|
|
29
|
+
store = SkeletonStore(cfg.data.swc_dir, cfg.data.units, cfg.data.dust_size, cfg.data.cache_dir,
|
|
30
|
+
features=cfg.data.features)
|
|
31
|
+
ids = read_root_ids(cfg.data.eval_neuron_ids or cfg.data.neuron_ids)
|
|
32
|
+
dataset = MergedNeuronDataset(store, ids, cfg.data, train=False)
|
|
33
|
+
thresholds = [float(t) for t in args.thresholds.split(",")]
|
|
34
|
+
results = evaluate(model, dataset, thresholds, min_cluster_size=args.min_cluster_size,
|
|
35
|
+
symmetrize=args.symmetrize, limit=args.limit)
|
|
36
|
+
print(format_results(results))
|
|
37
|
+
if args.out:
|
|
38
|
+
with open(args.out, "w") as f:
|
|
39
|
+
json.dump(results, f, indent=2)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _convert(args):
|
|
43
|
+
import torch
|
|
44
|
+
|
|
45
|
+
from .pretrained import load_legacy_checkpoint, save_pretrained
|
|
46
|
+
|
|
47
|
+
model = load_legacy_checkpoint(args.checkpoint, preset=args.preset)
|
|
48
|
+
print(save_pretrained(model, args.out, dtype=torch.float16 if args.fp16 else None))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv=None):
|
|
52
|
+
parser = argparse.ArgumentParser(prog="pat", description="Point Affinity Transformer")
|
|
53
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
54
|
+
|
|
55
|
+
p = sub.add_parser("train", help="train or fine-tune a model")
|
|
56
|
+
p.add_argument("config", help="YAML training config")
|
|
57
|
+
p.add_argument("--set", nargs="*", default=[], metavar="KEY=VALUE", help="override config entries")
|
|
58
|
+
p.set_defaults(func=_train)
|
|
59
|
+
|
|
60
|
+
p = sub.add_parser("eval", help="evaluate segmentation on merged clouds")
|
|
61
|
+
p.add_argument("config", nargs="?", help="YAML config describing the data (not needed with --items)")
|
|
62
|
+
p.add_argument("--model", required=True, help="model directory (from pat train or save_pretrained)")
|
|
63
|
+
p.add_argument("--items", help="directory of pre-generated items (pc_*.pt, labels_*.pt, mask_*.pt)")
|
|
64
|
+
p.add_argument("--thresholds", default="0.8")
|
|
65
|
+
p.add_argument("--min-cluster-size", type=int, default=30)
|
|
66
|
+
p.add_argument("--symmetrize", action="store_true", help="symmetrize affinities before clustering")
|
|
67
|
+
p.add_argument("--limit", type=int, help="evaluate only the first N items")
|
|
68
|
+
p.add_argument("--device")
|
|
69
|
+
p.add_argument("--out", help="write results as JSON")
|
|
70
|
+
p.add_argument("--set", nargs="*", default=[], metavar="KEY=VALUE")
|
|
71
|
+
p.set_defaults(func=_eval)
|
|
72
|
+
|
|
73
|
+
p = sub.add_parser("convert", help="convert a research .pth checkpoint into a model directory")
|
|
74
|
+
p.add_argument("checkpoint")
|
|
75
|
+
p.add_argument("out")
|
|
76
|
+
p.add_argument("--preset", help="model preset; overrides the one stored in the checkpoint")
|
|
77
|
+
p.add_argument("--fp16", action="store_true", help="store weights in float16")
|
|
78
|
+
p.set_defaults(func=_convert)
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args(argv)
|
|
81
|
+
args.func(args)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Clustering of affinity matrices into neuron labels.
|
|
2
|
+
|
|
3
|
+
The label ``-1`` marks noise: DBSCAN outliers and, after
|
|
4
|
+
[`remove_small_clusters`][pat.cluster.remove_small_clusters], points of clusters
|
|
5
|
+
smaller than ``min_cluster_size``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
from sklearn import cluster as _skcluster
|
|
13
|
+
|
|
14
|
+
NOISE = -1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_numpy(x) -> np.ndarray:
|
|
18
|
+
"""Convert a tensor or array-like to a NumPy array."""
|
|
19
|
+
return x.detach().cpu().numpy() if torch.is_tensor(x) else np.asarray(x)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def num_clusters(labels) -> int:
|
|
23
|
+
"""Number of clusters in ``labels`` numbered ``0..K-1``, ignoring noise (``-1``)."""
|
|
24
|
+
labels = torch.as_tensor(labels)
|
|
25
|
+
return int(labels.max().item()) + 1 if (labels >= 0).any() else 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def agglomerative(distance, threshold: float = 0.8, linkage: str = "average") -> torch.Tensor:
|
|
29
|
+
"""Agglomerative clustering of a precomputed ``(N, N)`` distance matrix.
|
|
30
|
+
|
|
31
|
+
Clusters are merged while their linkage distance is below ``threshold``.
|
|
32
|
+
"""
|
|
33
|
+
d = to_numpy(distance)
|
|
34
|
+
model = _skcluster.AgglomerativeClustering(
|
|
35
|
+
n_clusters=None, metric="precomputed", distance_threshold=threshold, linkage=linkage
|
|
36
|
+
)
|
|
37
|
+
return torch.as_tensor(model.fit_predict(d), dtype=torch.int64)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dbscan(distance, eps: float = 0.2, min_samples: int = 5) -> torch.Tensor:
|
|
41
|
+
"""DBSCAN on a precomputed distance matrix. Noise points get label ``-1``."""
|
|
42
|
+
d = to_numpy(distance)
|
|
43
|
+
labels = _skcluster.DBSCAN(eps=eps, min_samples=min_samples, metric="precomputed").fit_predict(d)
|
|
44
|
+
return torch.as_tensor(labels, dtype=torch.int64)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def remove_small_clusters(labels: torch.Tensor, min_size: int) -> torch.Tensor:
|
|
48
|
+
"""Mark clusters with fewer than ``min_size`` points as noise and relabel.
|
|
49
|
+
|
|
50
|
+
The remaining clusters are numbered ``0..K-1``, largest first. Existing
|
|
51
|
+
``-1`` labels stay noise.
|
|
52
|
+
"""
|
|
53
|
+
labels = torch.as_tensor(labels, dtype=torch.int64)
|
|
54
|
+
out = torch.full_like(labels, NOISE)
|
|
55
|
+
valid = labels >= 0
|
|
56
|
+
if not valid.any():
|
|
57
|
+
return out
|
|
58
|
+
ids, inverse, counts = torch.unique(labels[valid], return_inverse=True, return_counts=True)
|
|
59
|
+
keep = counts >= min_size
|
|
60
|
+
order = torch.argsort(counts.masked_fill(~keep, -1), descending=True, stable=True)
|
|
61
|
+
new_id = torch.full_like(ids, NOISE)
|
|
62
|
+
new_id[order[: int(keep.sum())]] = torch.arange(int(keep.sum()))
|
|
63
|
+
out[valid] = new_id[inverse]
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cluster_affinity(
|
|
68
|
+
affinity,
|
|
69
|
+
threshold: float = 0.8,
|
|
70
|
+
min_cluster_size: int = 30,
|
|
71
|
+
method: str = "agglomerative",
|
|
72
|
+
symmetrize: bool = True,
|
|
73
|
+
) -> torch.Tensor:
|
|
74
|
+
"""Turn an ``(N, N)`` affinity matrix into one label per point.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
affinity: Pairwise same-neuron probabilities.
|
|
78
|
+
threshold: Distance threshold on ``1 - affinity``. For average linkage,
|
|
79
|
+
two groups merge while their mean affinity exceeds ``1 - threshold``.
|
|
80
|
+
For DBSCAN this is ``eps``.
|
|
81
|
+
min_cluster_size: Smaller clusters become noise (``-1``). Use ``0`` to keep all.
|
|
82
|
+
method: ``"agglomerative"`` or ``"dbscan"``.
|
|
83
|
+
symmetrize: Average ``A`` and ``A.T`` first. The model is not exactly
|
|
84
|
+
symmetric in its two inputs; the paper evaluation used ``False``.
|
|
85
|
+
"""
|
|
86
|
+
a = to_numpy(affinity).astype(np.float32)
|
|
87
|
+
if symmetrize:
|
|
88
|
+
a = 0.5 * (a + a.T)
|
|
89
|
+
dist = np.clip(1.0 - a, 0.0, None)
|
|
90
|
+
np.fill_diagonal(dist, 0.0)
|
|
91
|
+
if method == "agglomerative":
|
|
92
|
+
labels = agglomerative(dist, threshold=threshold)
|
|
93
|
+
elif method == "dbscan":
|
|
94
|
+
labels = dbscan(dist, eps=threshold)
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f"Unknown method {method!r}")
|
|
97
|
+
return remove_small_clusters(labels, min_cluster_size) if min_cluster_size > 0 else labels
|