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.
Files changed (63) hide show
  1. tailcyclenet-0.0.1/LICENSE +28 -0
  2. tailcyclenet-0.0.1/PKG-INFO +175 -0
  3. tailcyclenet-0.0.1/README.md +152 -0
  4. tailcyclenet-0.0.1/pyproject.toml +129 -0
  5. tailcyclenet-0.0.1/setup.cfg +4 -0
  6. tailcyclenet-0.0.1/setup.py +5 -0
  7. tailcyclenet-0.0.1/tailcyclenet/__init__.py +11 -0
  8. tailcyclenet-0.0.1/tailcyclenet/adopt.py +513 -0
  9. tailcyclenet-0.0.1/tailcyclenet/box_prompt.py +69 -0
  10. tailcyclenet-0.0.1/tailcyclenet/checkpoints.py +294 -0
  11. tailcyclenet-0.0.1/tailcyclenet/crop.py +170 -0
  12. tailcyclenet-0.0.1/tailcyclenet/dataset.py +1374 -0
  13. tailcyclenet-0.0.1/tailcyclenet/detector/__init__.py +559 -0
  14. tailcyclenet-0.0.1/tailcyclenet/detector/assign.py +484 -0
  15. tailcyclenet-0.0.1/tailcyclenet/detector/associate.py +196 -0
  16. tailcyclenet-0.0.1/tailcyclenet/detector/config.py +312 -0
  17. tailcyclenet-0.0.1/tailcyclenet/detector/data.py +1513 -0
  18. tailcyclenet-0.0.1/tailcyclenet/detector/evaluate.py +368 -0
  19. tailcyclenet-0.0.1/tailcyclenet/detector/identity.py +110 -0
  20. tailcyclenet-0.0.1/tailcyclenet/detector/pretrained.py +163 -0
  21. tailcyclenet-0.0.1/tailcyclenet/detector/track.py +232 -0
  22. tailcyclenet-0.0.1/tailcyclenet/detector/vit_backbone.py +210 -0
  23. tailcyclenet-0.0.1/tailcyclenet/detector/yolox.py +679 -0
  24. tailcyclenet-0.0.1/tailcyclenet/distributed.py +187 -0
  25. tailcyclenet-0.0.1/tailcyclenet/format.py +1318 -0
  26. tailcyclenet-0.0.1/tailcyclenet/infer/__init__.py +17 -0
  27. tailcyclenet-0.0.1/tailcyclenet/infer/cli.py +341 -0
  28. tailcyclenet-0.0.1/tailcyclenet/infer/driver.py +495 -0
  29. tailcyclenet-0.0.1/tailcyclenet/infer/predictions.py +339 -0
  30. tailcyclenet-0.0.1/tailcyclenet/infer/store.py +102 -0
  31. tailcyclenet-0.0.1/tailcyclenet/infer/window.py +1004 -0
  32. tailcyclenet-0.0.1/tailcyclenet/losses.py +76 -0
  33. tailcyclenet-0.0.1/tailcyclenet/memory.py +349 -0
  34. tailcyclenet-0.0.1/tailcyclenet/metrics.py +277 -0
  35. tailcyclenet-0.0.1/tailcyclenet/model.py +449 -0
  36. tailcyclenet-0.0.1/tailcyclenet/optim.py +268 -0
  37. tailcyclenet-0.0.1/tailcyclenet/query_encoder.py +379 -0
  38. tailcyclenet-0.0.1/tailcyclenet/render.py +286 -0
  39. tailcyclenet-0.0.1/tailcyclenet/unfreeze.py +127 -0
  40. tailcyclenet-0.0.1/tailcyclenet/video.py +161 -0
  41. tailcyclenet-0.0.1/tailcyclenet.egg-info/PKG-INFO +175 -0
  42. tailcyclenet-0.0.1/tailcyclenet.egg-info/SOURCES.txt +61 -0
  43. tailcyclenet-0.0.1/tailcyclenet.egg-info/dependency_links.txt +1 -0
  44. tailcyclenet-0.0.1/tailcyclenet.egg-info/requires.txt +8 -0
  45. tailcyclenet-0.0.1/tailcyclenet.egg-info/top_level.txt +1 -0
  46. tailcyclenet-0.0.1/tests/test_adopt.py +440 -0
  47. tailcyclenet-0.0.1/tests/test_box_prompt.py +144 -0
  48. tailcyclenet-0.0.1/tests/test_combine_roots.py +72 -0
  49. tailcyclenet-0.0.1/tests/test_convert_apt_lbl.py +162 -0
  50. tailcyclenet-0.0.1/tests/test_dataset.py +1531 -0
  51. tailcyclenet-0.0.1/tests/test_detector.py +5500 -0
  52. tailcyclenet-0.0.1/tests/test_distributed.py +512 -0
  53. tailcyclenet-0.0.1/tests/test_format.py +532 -0
  54. tailcyclenet-0.0.1/tests/test_infer.py +1661 -0
  55. tailcyclenet-0.0.1/tests/test_losses.py +148 -0
  56. tailcyclenet-0.0.1/tests/test_memory_budget.py +614 -0
  57. tailcyclenet-0.0.1/tests/test_metrics.py +354 -0
  58. tailcyclenet-0.0.1/tests/test_model.py +917 -0
  59. tailcyclenet-0.0.1/tests/test_optim.py +469 -0
  60. tailcyclenet-0.0.1/tests/test_render.py +373 -0
  61. tailcyclenet-0.0.1/tests/test_render_dataset.py +73 -0
  62. tailcyclenet-0.0.1/tests/test_train.py +256 -0
  63. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"