tailcyclenet 0.0.1__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.
- tailcyclenet-0.0.1/LICENSE +28 -0
- tailcyclenet-0.0.1/PKG-INFO +175 -0
- tailcyclenet-0.0.1/README.md +152 -0
- tailcyclenet-0.0.1/pyproject.toml +129 -0
- tailcyclenet-0.0.1/setup.cfg +4 -0
- tailcyclenet-0.0.1/setup.py +5 -0
- tailcyclenet-0.0.1/tailcyclenet/__init__.py +11 -0
- tailcyclenet-0.0.1/tailcyclenet/adopt.py +513 -0
- tailcyclenet-0.0.1/tailcyclenet/box_prompt.py +69 -0
- tailcyclenet-0.0.1/tailcyclenet/checkpoints.py +294 -0
- tailcyclenet-0.0.1/tailcyclenet/crop.py +170 -0
- tailcyclenet-0.0.1/tailcyclenet/dataset.py +1374 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/__init__.py +559 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/assign.py +484 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/associate.py +196 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/config.py +312 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/data.py +1513 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/evaluate.py +368 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/identity.py +110 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/pretrained.py +163 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/track.py +232 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/vit_backbone.py +210 -0
- tailcyclenet-0.0.1/tailcyclenet/detector/yolox.py +679 -0
- tailcyclenet-0.0.1/tailcyclenet/distributed.py +187 -0
- tailcyclenet-0.0.1/tailcyclenet/format.py +1318 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/__init__.py +17 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/cli.py +341 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/driver.py +495 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/predictions.py +339 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/store.py +102 -0
- tailcyclenet-0.0.1/tailcyclenet/infer/window.py +1004 -0
- tailcyclenet-0.0.1/tailcyclenet/losses.py +76 -0
- tailcyclenet-0.0.1/tailcyclenet/memory.py +349 -0
- tailcyclenet-0.0.1/tailcyclenet/metrics.py +277 -0
- tailcyclenet-0.0.1/tailcyclenet/model.py +449 -0
- tailcyclenet-0.0.1/tailcyclenet/optim.py +268 -0
- tailcyclenet-0.0.1/tailcyclenet/query_encoder.py +379 -0
- tailcyclenet-0.0.1/tailcyclenet/render.py +286 -0
- tailcyclenet-0.0.1/tailcyclenet/unfreeze.py +127 -0
- tailcyclenet-0.0.1/tailcyclenet/video.py +161 -0
- tailcyclenet-0.0.1/tailcyclenet.egg-info/PKG-INFO +175 -0
- tailcyclenet-0.0.1/tailcyclenet.egg-info/SOURCES.txt +61 -0
- tailcyclenet-0.0.1/tailcyclenet.egg-info/dependency_links.txt +1 -0
- tailcyclenet-0.0.1/tailcyclenet.egg-info/requires.txt +8 -0
- tailcyclenet-0.0.1/tailcyclenet.egg-info/top_level.txt +1 -0
- tailcyclenet-0.0.1/tests/test_adopt.py +440 -0
- tailcyclenet-0.0.1/tests/test_box_prompt.py +144 -0
- tailcyclenet-0.0.1/tests/test_combine_roots.py +72 -0
- tailcyclenet-0.0.1/tests/test_convert_apt_lbl.py +162 -0
- tailcyclenet-0.0.1/tests/test_dataset.py +1531 -0
- tailcyclenet-0.0.1/tests/test_detector.py +5500 -0
- tailcyclenet-0.0.1/tests/test_distributed.py +512 -0
- tailcyclenet-0.0.1/tests/test_format.py +532 -0
- tailcyclenet-0.0.1/tests/test_infer.py +1661 -0
- tailcyclenet-0.0.1/tests/test_losses.py +148 -0
- tailcyclenet-0.0.1/tests/test_memory_budget.py +614 -0
- tailcyclenet-0.0.1/tests/test_metrics.py +354 -0
- tailcyclenet-0.0.1/tests/test_model.py +917 -0
- tailcyclenet-0.0.1/tests/test_optim.py +469 -0
- tailcyclenet-0.0.1/tests/test_render.py +373 -0
- tailcyclenet-0.0.1/tests/test_render_dataset.py +73 -0
- tailcyclenet-0.0.1/tests/test_train.py +256 -0
- tailcyclenet-0.0.1/tests/test_video.py +130 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, HHMI Janelia
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tailcyclenet
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: finetune a posetail tracker into an animal pose estimator (3D multiview, 3D single-view, 2D single-view)
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
Project-URL: Repository, https://github.com/AI-HHMI/tailcyclenet
|
|
7
|
+
Keywords: animal pose estimation,3d tracking,multi-view,posetail
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: posetail==0.4.1
|
|
15
|
+
Requires-Dist: torch<3,>=2
|
|
16
|
+
Requires-Dist: einops
|
|
17
|
+
Requires-Dist: numpy>=1.26
|
|
18
|
+
Requires-Dist: scipy
|
|
19
|
+
Requires-Dist: pyarrow
|
|
20
|
+
Requires-Dist: av>=12
|
|
21
|
+
Requires-Dist: tqdm
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# tailcyclenet
|
|
25
|
+
|
|
26
|
+
Finetune a [posetail](https://pypi.org/project/posetail/) point tracker into an animal pose
|
|
27
|
+
estimator. Three settings, one model: **3D multiview**, **3D single-view**, **2D single-view**.
|
|
28
|
+
|
|
29
|
+
The pipeline detects animals, crops them, and decodes per-keypoint poses through a single window
|
|
30
|
+
loop. It reads one annotation format (`docs/annotation_format.md`) that serves both hand annotation
|
|
31
|
+
and bulk training across datasets of differing keypoint sets, camera counts, and dimensionality.
|
|
32
|
+
This README is the committed reference: what this is, how to run it, and the invariants a
|
|
33
|
+
contributor must not break.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
Dependencies are managed with [pixi](https://pixi.sh). `posetail==0.3.5` is pinned from PyPI.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pixi install
|
|
43
|
+
pixi run python -c "import posetail, tailcyclenet" # sanity check
|
|
44
|
+
pixi run test # test suite
|
|
45
|
+
pixi run lint # ruff
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- The `LD_LIBRARY_PATH` prepend in `pyproject.toml` is load-bearing — the env ships a newer
|
|
49
|
+
`libstdc++` than some hosts, and without it `import scipy.optimize` dies naming only `CXXABI`.
|
|
50
|
+
- posetail >= 0.3.5 ships every behaviour this repo once monkeypatched (per-frame camera offsets,
|
|
51
|
+
`crop_box_for_points`, `scene_features=`/`input_size=` on the tracker forward); there is no patch
|
|
52
|
+
layer anymore.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Layout
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
tailcyclenet/ library: format, dataset, crop rule, model, inference, metrics, detector
|
|
60
|
+
scripts/ train.py train_detector.py infer.py eval.py convert_*.py
|
|
61
|
+
configs/ base.toml + 2d.toml + 3d.toml + detector.toml (extends one level deep)
|
|
62
|
+
configs/datasets/ per-dataset keypoint and skeleton definitions
|
|
63
|
+
docs/ annotation_format.md — the data format spec (human-owned)
|
|
64
|
+
tests/ invariants (crop rule, converters, geometry)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Training
|
|
70
|
+
|
|
71
|
+
One estimator trains across every dataset root under `[data].path`; a keypoint embedding table is
|
|
72
|
+
what lets roots with different keypoint sets share a model.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 3D (multiview / single-view)
|
|
76
|
+
pixi run python scripts/train.py --config configs/3d.toml --data <root>
|
|
77
|
+
|
|
78
|
+
# 2D (single-view)
|
|
79
|
+
pixi run python scripts/train.py --config configs/2d.toml --data <root>
|
|
80
|
+
|
|
81
|
+
# one node, N gpus: one item per rank, gradients averaged by DDP
|
|
82
|
+
pixi run python scripts/train.py --config configs/3d.toml --data <root> --devices 4
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Facts that are easy to get wrong:
|
|
86
|
+
|
|
87
|
+
- The two configs differ in exactly three keys — `cams_to_sample`, `val_cams_to_sample`,
|
|
88
|
+
`prob_2d_only` — all camera-count questions a one-camera root cannot ask. `n_keypoints` is
|
|
89
|
+
**derived from the data, never configured**.
|
|
90
|
+
- The per-rank batch is structurally **1**; `--devices N` is the only batch dimension this repo has.
|
|
91
|
+
Every iteration count in a config is a **total across ranks** (60,000 is 60,000 samples on any
|
|
92
|
+
gpu count) and the learning rate is scaled by `sqrt(N)`, so a multi-gpu run is two levers off a
|
|
93
|
+
single-gpu one; `provenance.toml` records which it was.
|
|
94
|
+
- `[model].gridresid_offset` has **no default and must be stated** — the two values load the same
|
|
95
|
+
tensors, so a mismatch produces numbers rather than an exception.
|
|
96
|
+
- A run folder writes `keypoint_registry.toml` (the derived keypoint axis) and `provenance.toml`
|
|
97
|
+
(commit + dirty flag). A config is not a provenance record.
|
|
98
|
+
- The video encoder unfreezes mid-run per `[model].video_encoder_requires_grad` (a bool, or an int
|
|
99
|
+
iteration to unfreeze at). A run started before the shipped default (8 blocks at 10,000) is not
|
|
100
|
+
comparable to one after; `false` restores the old arm.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Detector
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pixi run python scripts/train_detector.py --config configs/detector.toml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The recipe lives in the config, not on the CLI — every default is there with its evidence, and an
|
|
111
|
+
unknown key raises rather than silently training at a default. Only `--out`, `--iters` and
|
|
112
|
+
`--device` override.
|
|
113
|
+
|
|
114
|
+
One detector per dataset, and `input_wh` defaults to an aspect-matched size rather than a square:
|
|
115
|
+
a square letterbox on a wide frame wastes most of the canvas and can put the animal below the stride
|
|
116
|
+
the FPN can represent. The regression target is `crop.crop_box_for_points` — the detector
|
|
117
|
+
reproduces *the crop the pose model was trained on*, so `[data].boxes` must equal the pose run's
|
|
118
|
+
`[data].box_source`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Inference and eval
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# one source session (a dataset root works only if it holds a single session in --split)
|
|
126
|
+
pixi run python scripts/infer.py --run runs/<name> --data <session-dir> --split test \
|
|
127
|
+
--detector runs/det-<name> --out pred/
|
|
128
|
+
|
|
129
|
+
# or, straight off raw footage + an anipose calibration
|
|
130
|
+
pixi run python scripts/infer.py --run runs/<name> --out pred/ \
|
|
131
|
+
--videos rec/ --calibration anipose/calibration.toml --cam-regex 'cam([0-9]+)_' \
|
|
132
|
+
--detector runs/det-<name> --max-animals 4
|
|
133
|
+
|
|
134
|
+
pixi run python scripts/eval.py pred/ --data <root> --split test --chunk 500
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **`--out` is a prediction session directory** (`session.toml`, `calibration.toml`, `groups.pq`,
|
|
138
|
+
`points3d.pq`, `keypoints.pq`, `instances.pq`, `windows.pq`), written a block at a time so nothing
|
|
139
|
+
is proportional to clip length. `eval.py` and `render.py` both read it; `render.py` finds its own
|
|
140
|
+
pixels via the session's `[provenance]`.
|
|
141
|
+
- **`--data` and `--videos` are exactly-one-of**, and a run is **one source session** (which may
|
|
142
|
+
hold many groups). For `--videos`, the camera name is the regex **capture group** and the session
|
|
143
|
+
is built in memory — nothing is staged.
|
|
144
|
+
- There is **one** window loop. Box sources: annotations, a detections npz (`--boxes`), or a
|
|
145
|
+
per-dataset detector (`--detector`). Prompt regimes: `none` (query-free), `carry` (previous
|
|
146
|
+
window's own prediction — what deployment does), `self` (two passes), `labels` (an oracle, gated
|
|
147
|
+
off by default).
|
|
148
|
+
|
|
149
|
+
**Defaults are not the recommendation.** The good settings are root-conditional, so sweep them per
|
|
150
|
+
root. Current values: `--anchor carry`, `--overlap 4`, `--refine` derived (on 3D / off 2D),
|
|
151
|
+
`--track on`, `--box-prompt auto`, `--prefetch-windows 1` (bit-exact, performance only),
|
|
152
|
+
`--max-ram` derived from the host. In particular `--anchor` is root-conditional in 2D (a carried
|
|
153
|
+
prior on a crowded root is often the wrong animal's pose) and `--overlap`'s optimum is seam-count
|
|
154
|
+
against seam-size — sweep per root.
|
|
155
|
+
|
|
156
|
+
Four rules that are *not* root-conditional:
|
|
157
|
+
|
|
158
|
+
- **`--vis-thresh` has no meaning in 2D at the shipped default** — the visibility head is only
|
|
159
|
+
trained when `[training.losses].vis_loss_2d_weight` is nonzero (default `0.0`).
|
|
160
|
+
- **`--box-prompt auto` needs a detector or boxes file.** A box-model run without one refuses rather
|
|
161
|
+
than silently falling back to the GT oracle. Pass `--detector`/`--boxes`, or `--box-prompt none`
|
|
162
|
+
to withhold the box.
|
|
163
|
+
- Always run `eval.py` with `--chunk 500` on long clips — the bootstrap resamples groups, so a
|
|
164
|
+
single long clip returns `DEGENERATE`.
|
|
165
|
+
- Use `--min-match-kpts 0.5` for deltas and `0` for absolutes.
|
|
166
|
+
|
|
167
|
+
The largest lever is not a flag: pose accuracy on a ground-truth crop is far better, at full
|
|
168
|
+
coverage, than through the detector, and on a long clip essentially all coverage loss is `no box`.
|
|
169
|
+
Fix the crop path before tuning identity flags.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Reference
|
|
174
|
+
|
|
175
|
+
- `docs/annotation_format.md` — the data format spec (human-owned).
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# tailcyclenet
|
|
2
|
+
|
|
3
|
+
Finetune a [posetail](https://pypi.org/project/posetail/) point tracker into an animal pose
|
|
4
|
+
estimator. Three settings, one model: **3D multiview**, **3D single-view**, **2D single-view**.
|
|
5
|
+
|
|
6
|
+
The pipeline detects animals, crops them, and decodes per-keypoint poses through a single window
|
|
7
|
+
loop. It reads one annotation format (`docs/annotation_format.md`) that serves both hand annotation
|
|
8
|
+
and bulk training across datasets of differing keypoint sets, camera counts, and dimensionality.
|
|
9
|
+
This README is the committed reference: what this is, how to run it, and the invariants a
|
|
10
|
+
contributor must not break.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Dependencies are managed with [pixi](https://pixi.sh). `posetail==0.3.5` is pinned from PyPI.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pixi install
|
|
20
|
+
pixi run python -c "import posetail, tailcyclenet" # sanity check
|
|
21
|
+
pixi run test # test suite
|
|
22
|
+
pixi run lint # ruff
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- The `LD_LIBRARY_PATH` prepend in `pyproject.toml` is load-bearing — the env ships a newer
|
|
26
|
+
`libstdc++` than some hosts, and without it `import scipy.optimize` dies naming only `CXXABI`.
|
|
27
|
+
- posetail >= 0.3.5 ships every behaviour this repo once monkeypatched (per-frame camera offsets,
|
|
28
|
+
`crop_box_for_points`, `scene_features=`/`input_size=` on the tracker forward); there is no patch
|
|
29
|
+
layer anymore.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
tailcyclenet/ library: format, dataset, crop rule, model, inference, metrics, detector
|
|
37
|
+
scripts/ train.py train_detector.py infer.py eval.py convert_*.py
|
|
38
|
+
configs/ base.toml + 2d.toml + 3d.toml + detector.toml (extends one level deep)
|
|
39
|
+
configs/datasets/ per-dataset keypoint and skeleton definitions
|
|
40
|
+
docs/ annotation_format.md — the data format spec (human-owned)
|
|
41
|
+
tests/ invariants (crop rule, converters, geometry)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Training
|
|
47
|
+
|
|
48
|
+
One estimator trains across every dataset root under `[data].path`; a keypoint embedding table is
|
|
49
|
+
what lets roots with different keypoint sets share a model.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 3D (multiview / single-view)
|
|
53
|
+
pixi run python scripts/train.py --config configs/3d.toml --data <root>
|
|
54
|
+
|
|
55
|
+
# 2D (single-view)
|
|
56
|
+
pixi run python scripts/train.py --config configs/2d.toml --data <root>
|
|
57
|
+
|
|
58
|
+
# one node, N gpus: one item per rank, gradients averaged by DDP
|
|
59
|
+
pixi run python scripts/train.py --config configs/3d.toml --data <root> --devices 4
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Facts that are easy to get wrong:
|
|
63
|
+
|
|
64
|
+
- The two configs differ in exactly three keys — `cams_to_sample`, `val_cams_to_sample`,
|
|
65
|
+
`prob_2d_only` — all camera-count questions a one-camera root cannot ask. `n_keypoints` is
|
|
66
|
+
**derived from the data, never configured**.
|
|
67
|
+
- The per-rank batch is structurally **1**; `--devices N` is the only batch dimension this repo has.
|
|
68
|
+
Every iteration count in a config is a **total across ranks** (60,000 is 60,000 samples on any
|
|
69
|
+
gpu count) and the learning rate is scaled by `sqrt(N)`, so a multi-gpu run is two levers off a
|
|
70
|
+
single-gpu one; `provenance.toml` records which it was.
|
|
71
|
+
- `[model].gridresid_offset` has **no default and must be stated** — the two values load the same
|
|
72
|
+
tensors, so a mismatch produces numbers rather than an exception.
|
|
73
|
+
- A run folder writes `keypoint_registry.toml` (the derived keypoint axis) and `provenance.toml`
|
|
74
|
+
(commit + dirty flag). A config is not a provenance record.
|
|
75
|
+
- The video encoder unfreezes mid-run per `[model].video_encoder_requires_grad` (a bool, or an int
|
|
76
|
+
iteration to unfreeze at). A run started before the shipped default (8 blocks at 10,000) is not
|
|
77
|
+
comparable to one after; `false` restores the old arm.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Detector
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pixi run python scripts/train_detector.py --config configs/detector.toml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The recipe lives in the config, not on the CLI — every default is there with its evidence, and an
|
|
88
|
+
unknown key raises rather than silently training at a default. Only `--out`, `--iters` and
|
|
89
|
+
`--device` override.
|
|
90
|
+
|
|
91
|
+
One detector per dataset, and `input_wh` defaults to an aspect-matched size rather than a square:
|
|
92
|
+
a square letterbox on a wide frame wastes most of the canvas and can put the animal below the stride
|
|
93
|
+
the FPN can represent. The regression target is `crop.crop_box_for_points` — the detector
|
|
94
|
+
reproduces *the crop the pose model was trained on*, so `[data].boxes` must equal the pose run's
|
|
95
|
+
`[data].box_source`.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Inference and eval
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# one source session (a dataset root works only if it holds a single session in --split)
|
|
103
|
+
pixi run python scripts/infer.py --run runs/<name> --data <session-dir> --split test \
|
|
104
|
+
--detector runs/det-<name> --out pred/
|
|
105
|
+
|
|
106
|
+
# or, straight off raw footage + an anipose calibration
|
|
107
|
+
pixi run python scripts/infer.py --run runs/<name> --out pred/ \
|
|
108
|
+
--videos rec/ --calibration anipose/calibration.toml --cam-regex 'cam([0-9]+)_' \
|
|
109
|
+
--detector runs/det-<name> --max-animals 4
|
|
110
|
+
|
|
111
|
+
pixi run python scripts/eval.py pred/ --data <root> --split test --chunk 500
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **`--out` is a prediction session directory** (`session.toml`, `calibration.toml`, `groups.pq`,
|
|
115
|
+
`points3d.pq`, `keypoints.pq`, `instances.pq`, `windows.pq`), written a block at a time so nothing
|
|
116
|
+
is proportional to clip length. `eval.py` and `render.py` both read it; `render.py` finds its own
|
|
117
|
+
pixels via the session's `[provenance]`.
|
|
118
|
+
- **`--data` and `--videos` are exactly-one-of**, and a run is **one source session** (which may
|
|
119
|
+
hold many groups). For `--videos`, the camera name is the regex **capture group** and the session
|
|
120
|
+
is built in memory — nothing is staged.
|
|
121
|
+
- There is **one** window loop. Box sources: annotations, a detections npz (`--boxes`), or a
|
|
122
|
+
per-dataset detector (`--detector`). Prompt regimes: `none` (query-free), `carry` (previous
|
|
123
|
+
window's own prediction — what deployment does), `self` (two passes), `labels` (an oracle, gated
|
|
124
|
+
off by default).
|
|
125
|
+
|
|
126
|
+
**Defaults are not the recommendation.** The good settings are root-conditional, so sweep them per
|
|
127
|
+
root. Current values: `--anchor carry`, `--overlap 4`, `--refine` derived (on 3D / off 2D),
|
|
128
|
+
`--track on`, `--box-prompt auto`, `--prefetch-windows 1` (bit-exact, performance only),
|
|
129
|
+
`--max-ram` derived from the host. In particular `--anchor` is root-conditional in 2D (a carried
|
|
130
|
+
prior on a crowded root is often the wrong animal's pose) and `--overlap`'s optimum is seam-count
|
|
131
|
+
against seam-size — sweep per root.
|
|
132
|
+
|
|
133
|
+
Four rules that are *not* root-conditional:
|
|
134
|
+
|
|
135
|
+
- **`--vis-thresh` has no meaning in 2D at the shipped default** — the visibility head is only
|
|
136
|
+
trained when `[training.losses].vis_loss_2d_weight` is nonzero (default `0.0`).
|
|
137
|
+
- **`--box-prompt auto` needs a detector or boxes file.** A box-model run without one refuses rather
|
|
138
|
+
than silently falling back to the GT oracle. Pass `--detector`/`--boxes`, or `--box-prompt none`
|
|
139
|
+
to withhold the box.
|
|
140
|
+
- Always run `eval.py` with `--chunk 500` on long clips — the bootstrap resamples groups, so a
|
|
141
|
+
single long clip returns `DEGENERATE`.
|
|
142
|
+
- Use `--min-match-kpts 0.5` for deltas and `0` for absolutes.
|
|
143
|
+
|
|
144
|
+
The largest lever is not a flag: pose accuracy on a ground-truth crop is far better, at full
|
|
145
|
+
coverage, than through the detector, and on a long clip essentially all coverage loss is `no box`.
|
|
146
|
+
Fix the crop path before tuning identity flags.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Reference
|
|
151
|
+
|
|
152
|
+
- `docs/annotation_format.md` — the data format spec (human-owned).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64,<81", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tailcyclenet"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "finetune a posetail tracker into an animal pose estimator (3D multiview, 3D single-view, 2D single-view)"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
|
+
keywords = ["animal pose estimation", "3d tracking", "multi-view", "posetail"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"posetail==0.4.1",
|
|
20
|
+
"torch>=2,<3",
|
|
21
|
+
"einops",
|
|
22
|
+
"numpy>=1.26",
|
|
23
|
+
"scipy",
|
|
24
|
+
"pyarrow",
|
|
25
|
+
"av>=12",
|
|
26
|
+
"tqdm",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/AI-HHMI/tailcyclenet"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["tailcyclenet", "tailcyclenet.detector", "tailcyclenet.infer"]
|
|
34
|
+
|
|
35
|
+
[tool.pixi.system-requirements]
|
|
36
|
+
libc = { family = "glibc", version = "2.34" }
|
|
37
|
+
|
|
38
|
+
[tool.pixi.workspace]
|
|
39
|
+
channels = ["conda-forge"]
|
|
40
|
+
platforms = ["linux-64"]
|
|
41
|
+
|
|
42
|
+
[tool.pixi.activation.env]
|
|
43
|
+
# The env ships libstdc++.so.6.0.35, the host may ship 6.0.29 with no CXXABI_1.3.15 -- without
|
|
44
|
+
# this the system copy wins the loader search and `import scipy.optimize` dies inside _highspy.
|
|
45
|
+
LD_LIBRARY_PATH = "$PIXI_PROJECT_ROOT/.pixi/envs/default/lib:$LD_LIBRARY_PATH"
|
|
46
|
+
|
|
47
|
+
# The env and repo both live on NFS, so a cold import pays ~2x NFS round-trips per module
|
|
48
|
+
# (torch's ~500 modules dominate startup); PYTHONPYCACHEPREFIX redirects the bytecode cache to a
|
|
49
|
+
# local path, which also survives reboots. Unset it to restore upstream behaviour.
|
|
50
|
+
PYTHONPYCACHEPREFIX = "${HOME}/.cache/tailcyclenet/pycache"
|
|
51
|
+
|
|
52
|
+
# A loader worker's per-item frame-buffer churn grows glibc's arenas unboundedly in a standalone
|
|
53
|
+
# probe and flat under this cap; live, it made no measurable difference either way, but it is the
|
|
54
|
+
# one arm with independent evidence behind it.
|
|
55
|
+
MALLOC_ARENA_MAX = "2"
|
|
56
|
+
|
|
57
|
+
[tool.pixi.pypi-dependencies]
|
|
58
|
+
# posetail is a LIBRARY here, never modified: pinned to the PyPI release (a checkout lives at
|
|
59
|
+
# ../posetail-next for READING only). 0.4.1 lands every workaround this repo carried -- the
|
|
60
|
+
# cube.py patches, crop_box_for_points, input_size=/scene_features=, T=1 guard, occlusion/vis guards --
|
|
61
|
+
# and adds loading packaged TrackerEncoder weights from Hugging Face.
|
|
62
|
+
posetail = "==0.4.1"
|
|
63
|
+
# The pytorch branch, not PyPI's 0.8.0: its Camera is an nn.Module whose projection,
|
|
64
|
+
# triangulation and Jacobians are torch ops, so camera geometry runs on GPU with the model.
|
|
65
|
+
# Declares itself 0.7.2, satisfying posetail's `aniposelib>=0.7.0`.
|
|
66
|
+
aniposelib = { git = "https://github.com/lambdaloop/anipose-lib.git", branch = "pytorch" }
|
|
67
|
+
tailcyclenet = { path = ".", editable = true }
|
|
68
|
+
torch = { version = "<3", index = "https://download.pytorch.org/whl/cu128" }
|
|
69
|
+
torchvision = { version = "*", index = "https://download.pytorch.org/whl/cu128" }
|
|
70
|
+
torchaudio = { version = "*", index = "https://download.pytorch.org/whl/cu128" }
|
|
71
|
+
# A2: the DINOv3 torch.hub repo's own logging module imports this unconditionally
|
|
72
|
+
# (dinov3/logging/__init__.py) -- not a DINOv3 model dependency per se, just a hard import in
|
|
73
|
+
# the hub checkout, so it has to be present before `torch.hub.load('facebookresearch/dinov3', ...)`
|
|
74
|
+
# can even build the module tree.
|
|
75
|
+
termcolor = "*"
|
|
76
|
+
|
|
77
|
+
[tool.pixi.feature.dev.dependencies]
|
|
78
|
+
python-build = "*"
|
|
79
|
+
twine = "*"
|
|
80
|
+
|
|
81
|
+
[tool.pixi.environments]
|
|
82
|
+
default = { solve-group = "default" }
|
|
83
|
+
dev = { features = ["dev"], solve-group = "default" }
|
|
84
|
+
|
|
85
|
+
[tool.pixi.dependencies]
|
|
86
|
+
python = "3.11.*"
|
|
87
|
+
cuda-toolkit = "12.8.*"
|
|
88
|
+
numpy = "==1.26"
|
|
89
|
+
scipy = "<1.17"
|
|
90
|
+
setuptools = "<81"
|
|
91
|
+
pip = "*"
|
|
92
|
+
pyarrow = "*"
|
|
93
|
+
matplotlib = "*"
|
|
94
|
+
pytest = "*"
|
|
95
|
+
# noarch, pure python, needs only pytest + execnet, so it cannot collide with the exact pins
|
|
96
|
+
# below; it parallelises the 29 model builds that dominate the suite.
|
|
97
|
+
pytest-xdist = "*"
|
|
98
|
+
# `--timeout=N` for a suite whose failure mode is a HANG (a loader futex deadlock when anything
|
|
99
|
+
# decodes video before the workers fork), not an assertion. noarch/pure-python like xdist. Not
|
|
100
|
+
# applied by default: the model-building tests are legitimately slow and a global cap would flake.
|
|
101
|
+
pytest-timeout = "*"
|
|
102
|
+
# noarch and pure python, like xdist and timeout above, so it cannot collide with the exact pins
|
|
103
|
+
# the note below is about.
|
|
104
|
+
ruff = "*"
|
|
105
|
+
av = ">=18.0.0,<19"
|
|
106
|
+
# Deliberately NOT listed: toml, opencv, pillow, wandb, schedulefree. posetail pins them
|
|
107
|
+
# EXACTLY and is itself pinned, so the versions are already determined in pixi.lock; re-declaring
|
|
108
|
+
# duplicates a pin that conflicts the moment posetail moves, or asks for `*` and conflicts
|
|
109
|
+
# immediately. If posetail ever drops one, add it here at posetail's own version. Video decoding
|
|
110
|
+
# is provided directly by PyAV above.
|
|
111
|
+
|
|
112
|
+
[tool.pixi.tasks]
|
|
113
|
+
# `-n 8`, default per-test distribution (loadfile pins all 29 model builds to one worker and the
|
|
114
|
+
# wall clock barely moves); each worker re-pays the ~14 s torch import, which is the floor.
|
|
115
|
+
test = "pytest tests/ -q -n 8"
|
|
116
|
+
lint = "ruff check . && python scripts/lint_comments.py"
|
|
117
|
+
|
|
118
|
+
[tool.ruff]
|
|
119
|
+
line-length = 100
|
|
120
|
+
target-version = "py311"
|
|
121
|
+
extend-exclude = ["scratch", "dev"]
|
|
122
|
+
|
|
123
|
+
# Pinned, not ruff's default: 0.16 widened the implied set to autofix rules that rewrite import
|
|
124
|
+
# blocks and dict() calls repo-wide. Widening it is a deliberate act with its own diff.
|
|
125
|
+
[tool.ruff.lint]
|
|
126
|
+
select = ["E4", "E7", "E9", "F"]
|
|
127
|
+
|
|
128
|
+
[tool.pytest.ini_options]
|
|
129
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
# All metadata lives in pyproject.toml [project]. Keep discovery explicit: the repository
|
|
4
|
+
# also contains large data and environment directories that make recursive discovery slow.
|
|
5
|
+
setup(packages=["tailcyclenet", "tailcyclenet.detector", "tailcyclenet.infer"])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""tailcyclenet -- posetail finetuned into an animal pose estimator.
|
|
2
|
+
|
|
3
|
+
No monkeypatching: posetail >= 0.3.5 ships every behaviour this repo once had to patch in.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = _pkg_version("tailcyclenet")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.0.0+unknown"
|