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).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tailcyclenet-0.0.2.tar.gz.
File metadata
- Download URL: tailcyclenet-0.0.2.tar.gz
- Upload date:
- Size: 383.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
655b452b85bd316729fd8536dfb38d0a983d1719b4801798717ddd6dc16a2517
|
|
| MD5 |
e540fa620dd4e95fce4e702123776d13
|
|
| BLAKE2b-256 |
1d5be9a5488cec31c6fff3516c6702a95458b30c06980ad73a1224adc872d5d8
|
File details
Details for the file tailcyclenet-0.0.2-py3-none-any.whl.
File metadata
- Download URL: tailcyclenet-0.0.2-py3-none-any.whl
- Upload date:
- Size: 228.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4053036e91c063409e512b96a74428a47680c6f298e87344345cc031c828aa12
|
|
| MD5 |
9a3d9baea69ec465838c806cdfe55dc0
|
|
| BLAKE2b-256 |
eede81e6e275d424cbc4e7bceffbbc68a865bc79b422b4b9c096371902a893b8
|