tailcyclenet
Finetune a posetail point tracker into an animal pose estimator. Three settings, one model: 3D multiview, 3D single-view, 2D single-view.
The pipeline detects animals, crops them, and decodes per-keypoint poses through a single window
loop. It reads one annotation format (docs/annotation_format.md) that serves both hand annotation
and bulk training across datasets of differing keypoint sets, camera counts, and dimensionality.
This README is the committed reference: what this is, how to run it, and the invariants a
contributor must not break.
Setup
Dependencies are managed with pixi. posetail==0.3.5 is pinned from PyPI.
pixi install
pixi run python -c "import posetail, tailcyclenet" # sanity check
pixi run test # test suite
pixi run lint # ruff
- The
LD_LIBRARY_PATHprepend inpyproject.tomlis load-bearing — the env ships a newerlibstdc++than some hosts, and without itimport scipy.optimizedies naming onlyCXXABI. - posetail >= 0.3.5 ships every behaviour this repo once monkeypatched (per-frame camera offsets,
crop_box_for_points,scene_features=/input_size=on the tracker forward); there is no patch layer anymore.
Layout
tailcyclenet/ library: format, dataset, crop rule, model, inference, metrics, detector
scripts/ train.py train_detector.py infer.py eval.py convert_*.py
configs/ base.toml + 2d.toml + 3d.toml + detector.toml (extends one level deep)
configs/datasets/ per-dataset keypoint and skeleton definitions
docs/ annotation_format.md — the data format spec (human-owned)
tests/ invariants (crop rule, converters, geometry)
Training
One estimator trains across every dataset root under [data].path; a keypoint embedding table is
what lets roots with different keypoint sets share a model.
# 3D (multiview / single-view)
pixi run python scripts/train.py --config configs/3d.toml --data <root>
# 2D (single-view)
pixi run python scripts/train.py --config configs/2d.toml --data <root>
# one node, N gpus: one item per rank, gradients averaged by DDP
pixi run python scripts/train.py --config configs/3d.toml --data <root> --devices 4
Facts that are easy to get wrong:
- The two configs differ in exactly three keys —
cams_to_sample,val_cams_to_sample,prob_2d_only— all camera-count questions a one-camera root cannot ask.n_keypointsis derived from the data, never configured. - The per-rank batch is structurally 1;
--devices Nis the only batch dimension this repo has. Every iteration count in a config is a total across ranks (60,000 is 60,000 samples on any gpu count) and the learning rate is scaled bysqrt(N), so a multi-gpu run is two levers off a single-gpu one;provenance.tomlrecords which it was. [model].gridresid_offsethas no default and must be stated — the two values load the same tensors, so a mismatch produces numbers rather than an exception.- A run folder writes
keypoint_registry.toml(the derived keypoint axis) andprovenance.toml(commit + dirty flag). A config is not a provenance record. - The video encoder unfreezes mid-run per
[model].video_encoder_requires_grad(a bool, or an int iteration to unfreeze at). A run started before the shipped default (8 blocks at 10,000) is not comparable to one after;falserestores the old arm.
Detector
pixi run python scripts/train_detector.py --config configs/detector.toml
The recipe lives in the config, not on the CLI — every default is there with its evidence, and an
unknown key raises rather than silently training at a default. Only --out, --iters and
--device override.
One detector per dataset, and input_wh defaults to an aspect-matched size rather than a square:
a square letterbox on a wide frame wastes most of the canvas and can put the animal below the stride
the FPN can represent. The regression target is crop.crop_box_for_points — the detector
reproduces the crop the pose model was trained on, so [data].boxes must equal the pose run's
[data].box_source.
Inference and eval
# one source session (a dataset root works only if it holds a single session in --split)
pixi run python scripts/infer.py --run runs/<name> --data <session-dir> --split test \
--detector runs/det-<name> --out pred/
# or, straight off raw footage + an anipose calibration
pixi run python scripts/infer.py --run runs/<name> --out pred/ \
--videos rec/ --calibration anipose/calibration.toml --cam-regex 'cam([0-9]+)_' \
--detector runs/det-<name> --max-animals 4
pixi run python scripts/eval.py pred/ --data <root> --split test --chunk 500
--outis a prediction session directory (session.toml,calibration.toml,groups.pq,points3d.pq,keypoints.pq,instances.pq,windows.pq), written a block at a time so nothing is proportional to clip length.eval.pyandrender.pyboth read it;render.pyfinds its own pixels via the session's[provenance].--dataand--videosare exactly-one-of, and a run is one source session (which may hold many groups). For--videos, the camera name is the regex capture group and the session is built in memory — nothing is staged.- There is one window loop. Box sources: annotations, a detections npz (
--boxes), or a per-dataset detector (--detector). Prompt regimes:none(query-free),carry(previous window's own prediction — what deployment does),self(two passes),labels(an oracle, gated off by default).
Defaults are not the recommendation. The good settings are root-conditional, so sweep them per
root. Current values: --anchor carry, --overlap 4, --refine derived (on 3D / off 2D),
--track on, --box-prompt auto, --prefetch-windows 1 (bit-exact, performance only),
--max-ram derived from the host. In particular --anchor is root-conditional in 2D (a carried
prior on a crowded root is often the wrong animal's pose) and --overlap's optimum is seam-count
against seam-size — sweep per root.
Four rules that are not root-conditional:
--vis-threshhas no meaning in 2D at the shipped default — the visibility head is only trained when[training.losses].vis_loss_2d_weightis nonzero (default0.0).--box-prompt autoneeds a detector or boxes file. A box-model run without one refuses rather than silently falling back to the GT oracle. Pass--detector/--boxes, or--box-prompt noneto withhold the box.- Always run
eval.pywith--chunk 500on long clips — the bootstrap resamples groups, so a single long clip returnsDEGENERATE. - Use
--min-match-kpts 0.5for deltas and0for absolutes.
The largest lever is not a flag: pose accuracy on a ground-truth crop is far better, at full
coverage, than through the detector, and on a long clip essentially all coverage loss is no box.
Fix the crop path before tuning identity flags.
Reference
docs/annotation_format.md— the data format spec (human-owned).
Metadata
Release files for tailcyclenet 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tailcyclenet-0.1.0.tar.gz | 391.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tailcyclenet-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 624.2 kB
Release files / tailcyclenet-0.1.0.tar.gz
| Download URL | tailcyclenet-0.1.0.tar.gz |
|---|---|
| Size | 391.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d34cd9c27984583d484fbbed1246e99f9edc5f624e7b31b61acd6d1c92b22ee
|
|
BLAKE2b-256 checksum How to use checksums |
c51af79823967980fde6486e72f5377c1582f48088b63cb8d56339083484f2fe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|
Release files / tailcyclenet-0.1.0-py3-none-any.whl
| Download URL | tailcyclenet-0.1.0-py3-none-any.whl |
|---|---|
| Size | 232.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d72fcaa452612ec01481a17138926cc9276ee787729a46f0cc7322f59732e331
|
|
BLAKE2b-256 checksum How to use checksums |
e317d4d5e30e477d5597804a64cb02392273f38bd03b420adb835da5916f1304
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|