ball¶
[ball]: the treadmill ball under a tethered animal, in the world frame.
The ball's outline in every view that sees it places the ball, its center on each frame (filtered and smoothed) and its radius (ball.outline); the 3D pose gives the first guess and orients the animal on it (ball.contact); spintrack, an optional extra (ball._spintrack), tracks its rotation in one view's video, which deeperfly brings into its own world frame (ball.track). Neither package reads the other's files (ADR-0144).
CenterTrack
dataclass
¶
The ball's center and the views' offsets over a stretch, filtered forward.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
Float[ndarray, 'T S']
|
The filtered center and offsets, |
cov |
Float[ndarray, 'T S S']
|
Their covariances, |
seen |
ndarray
|
Whether a frame's outline was measured, rather than only predicted. |
wander |
Float[ndarray, S]
|
The random walks' variances per frame, |
Source code in src/deeperfly/ball/outline.py
smoothed ¶
The centers (T, 3) and their covariances from every frame (RTS).
Source code in src/deeperfly/ball/outline.py
Contact
dataclass
¶
The ball, and the animal's axes where it stands on it, in world coordinates.
Attributes:
| Name | Type | Description |
|---|---|---|
axes |
Float[ndarray, '3 3']
|
World to animal, |
center |
Float[ndarray, 3]
|
The ball's center, |
radius |
float
|
The ball's radius, in world units. |
on_surface |
float
|
The share of the tips that rest on the ball. |
height |
float
|
The stance centroid above the surface, in ball radii. |
level |
bool
|
Whether the world's up stands in for the ball's normal, the animal not believably standing on the ball. |
Source code in src/deeperfly/ball/contact.py
line ¶
One log line: where the animal stands on the ball.
Source code in src/deeperfly/ball/contact.py
MissingSpintrack ¶
OutlineFilter ¶
The ball's center, frame by frame, with each view's offset: random walks.
Each frame predicts the last state, its covariance grown by the walks, and fits the outlines with that prediction as the prior; a frame no view shows the outline in keeps the prediction. Live tracking steps it as frames come.
Source code in src/deeperfly/ball/outline.py
__init__ ¶
Start at center with covariance cov, each offset at 0 within OFFSET_PX.
The radius is held.
Source code in src/deeperfly/ball/outline.py
step ¶
Take one frame's gray_frames; whether its outline was measured.
A frame the narrow search finds no outline in is searched again wide: a ball that jerks moves farther in a frame than the narrow search reaches.
Source code in src/deeperfly/ball/outline.py
OutlineFit
dataclass
¶
A sphere fitted to the outlines.
Attributes:
| Name | Type | Description |
|---|---|---|
center |
Float[ndarray, 3]
|
Its center, world coordinates. |
radius |
float
|
Its radius, world units. |
offsets |
Float[ndarray, V]
|
Each view's edges' offset from the outline, in pixels, in the views' order. |
cov |
Float[ndarray, 'S S']
|
The covariance of the center and the offsets, |
edges |
dict[str, int]
|
How many edges each view gave, by view name. |
residual_px |
float
|
The median edge offset from the fitted outline, in pixels. |
Source code in src/deeperfly/ball/outline.py
Stance
dataclass
¶
The 3D points the ball is guessed from and the animal oriented by.
Attributes:
| Name | Type | Description |
|---|---|---|
tips |
Float[ndarray, 'P 3']
|
The finite points that rest on the ball in stance, |
body |
Float[ndarray, 3]
|
The centroid of the points the animal stands on, NaN if unknown. |
body_axis |
Float[ndarray, 3]
|
Hind to front, NaN if unknown. |
Source code in src/deeperfly/ball/contact.py
of
classmethod
¶
of(
tips: Float[ndarray, "T P 3"],
stand: Float[ndarray, "T Q 3"],
front: Float[ndarray, "T F 3"],
hind: Float[ndarray, "T H 3"],
) -> Stance
The stance over some frames of world points, NaN where unknown.
Each point's median over the frames, then the centroids.
Source code in src/deeperfly/ball/contact.py
ball_ray ¶
The ray through the ball's image center, and the sine of its angular radius.
The ray is (origin, unit direction) in the world; the angular radius is the mean
angle between it and the rays of four points of the circle's rim.
Source code in src/deeperfly/ball/contact.py
detect_circle ¶
The ball's image circle (cx, cy, r) in deeperfly's pixels, from start on.
It is detected in the first frames of start:stop.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
spintrack's |
Source code in src/deeperfly/ball/track.py
fit_outline ¶
fit_outline(
views: Mapping[str, View],
images: dict[str, ndarray],
center: ndarray,
radius: float,
*,
prior: tuple[ndarray, ndarray] | None = None,
fit_radius: bool = False,
widths: Sequence[float] = WIDTHS,
) -> OutlineFit
The sphere whose outline matches the ball's edges in images, by view name.
Starts from center and radius, which must put the predicted outline within
the first of widths pixels of the edges. prior is the (mean, covariance) of
the center and the views' offsets (in views' order) before this frame, which
holds what the outlines barely pin; without one, the offsets are within
OFFSET_PX of zero and the views must see the ball from more than one line.
fit_radius frees the radius. images are gray_frames.
Raises:
| Type | Description |
|---|---|
ValueError
|
If no view shows enough outline, or the views look along one line and there is no prior, or the radius is asked of them. |
Source code in src/deeperfly/ball/outline.py
gray_frame ¶
A decoded (H, W, C) frame as the blurred float luma the edges are found on.
Source code in src/deeperfly/ball/outline.py
grazing_circle ¶
(n, 3) world points of the circle where view's rays graze a sphere.
What view images as the sphere's outline, from a view outside the sphere.
Source code in src/deeperfly/ball/outline.py
guess_ball ¶
guess_ball(
view: View,
circle: tuple[float, float, float],
stance: Stance,
) -> tuple[np.ndarray, float] | None
A first (center, radius) of the ball, from one view and the leg tips.
circle is the ball's image (cx, cy, r) in view's pixels. None with too few
tips, or no centroid to start the search from.
Source code in src/deeperfly/ball/contact.py
require_spintrack
cached
¶
The spintrack module, or MissingSpintrack saying how to install it.
Source code in src/deeperfly/ball/_spintrack.py
rim_points ¶
(n, 2) pixels of a sphere's outline in view, evenly around it.
Source code in src/deeperfly/ball/outline.py
stand_on ¶
The animal's axes on the ball, and why the world's up stands in, if it does.
Source code in src/deeperfly/ball/contact.py
track_ball ¶
track_ball(
view: View,
video: Video,
start: int,
stop: int,
axes: ndarray,
rim: ndarray,
*,
tracking: Mapping[str, Any],
two_pass: bool = False,
) -> tuple[dict[str, np.ndarray], list[str], Any]
Track the ball's rotation over frames start:stop of video, which view sees.
rim is points of the ball's outline at start, (n, 2) in deeperfly's pixels;
axes (world to animal) orients spintrack's animal frame. Returns rotation,
step and tracks over the stretch (NaN at a frame spintrack dropped), the names
of tracks' columns, and spintrack's quality summary.
Source code in src/deeperfly/ball/track.py
track_outline ¶
track_outline(
views: dict[str, View],
frames: Iterable[dict[str, ndarray]],
center: ndarray,
cov: ndarray,
radius: float,
) -> CenterTrack
The ball's center on each of frames (by view name, gray_frames), filtered.
Starts at center with covariance cov, the first frame with the widest search;
the radius is held. CenterTrack.smoothed brings in the later frames.
Source code in src/deeperfly/ball/outline.py
The ball's center and radius from its outline in several views, filtered over time.
A sphere's outline in a view is the projection of the circle where the camera's rays graze it, so each view's outline pins the sphere across that view's rays, and two or more views pin it in depth too: the outline does what one view's apparent size and the leg tips did before, and the radius comes out with the center. Each view's outline is measured as the bright-to-dark edge along the normals of the predicted one, where the ball is not hidden (a leg crossing the rim leaves no edge, or a weak one), and the center (and the radius) is solved by robust Gauss-Newton on the edges' offsets, in pixels, through deeperfly's camera model, distortion included.
A view's edges all sit off the true outline by an offset of their own, its calibration's error and the edge detector's (octacam's views sat -1.5 to +2.3 px off), which is solved for with the center. One view's apparent size then tells distance only as well as its offset is known: alone, the hind camera's size had put the ball's depth 3.5 units off, two thirds of its radius.
Over a stretch of frames the center and the views' offsets are random walks, filtered (Kalman) and smoothed (Rauch-Tung-Striebel). Each frame's fit takes the prediction as its prior, so a direction the outlines pin follows them and one they barely pin keeps the prior. The offsets drift slowly, so they are learned where views on different lines see the ball and carried where they do not: as when a ball drops out of octacam's side views and only its front and hind cameras, which face each other along one line, still see it. Depth along that line is then carried across from the frames on either side, and the covariance says how well each frame's center is known.
CenterTrack
dataclass
¶
The ball's center and the views' offsets over a stretch, filtered forward.
Attributes:
| Name | Type | Description |
|---|---|---|
state |
Float[ndarray, 'T S']
|
The filtered center and offsets, |
cov |
Float[ndarray, 'T S S']
|
Their covariances, |
seen |
ndarray
|
Whether a frame's outline was measured, rather than only predicted. |
wander |
Float[ndarray, S]
|
The random walks' variances per frame, |
Source code in src/deeperfly/ball/outline.py
smoothed ¶
The centers (T, 3) and their covariances from every frame (RTS).
Source code in src/deeperfly/ball/outline.py
OutlineFilter ¶
The ball's center, frame by frame, with each view's offset: random walks.
Each frame predicts the last state, its covariance grown by the walks, and fits the outlines with that prediction as the prior; a frame no view shows the outline in keeps the prediction. Live tracking steps it as frames come.
Source code in src/deeperfly/ball/outline.py
__init__ ¶
Start at center with covariance cov, each offset at 0 within OFFSET_PX.
The radius is held.
Source code in src/deeperfly/ball/outline.py
step ¶
Take one frame's gray_frames; whether its outline was measured.
A frame the narrow search finds no outline in is searched again wide: a ball that jerks moves farther in a frame than the narrow search reaches.
Source code in src/deeperfly/ball/outline.py
OutlineFit
dataclass
¶
A sphere fitted to the outlines.
Attributes:
| Name | Type | Description |
|---|---|---|
center |
Float[ndarray, 3]
|
Its center, world coordinates. |
radius |
float
|
Its radius, world units. |
offsets |
Float[ndarray, V]
|
Each view's edges' offset from the outline, in pixels, in the views' order. |
cov |
Float[ndarray, 'S S']
|
The covariance of the center and the offsets, |
edges |
dict[str, int]
|
How many edges each view gave, by view name. |
residual_px |
float
|
The median edge offset from the fitted outline, in pixels. |
Source code in src/deeperfly/ball/outline.py
fit_outline ¶
fit_outline(
views: Mapping[str, View],
images: dict[str, ndarray],
center: ndarray,
radius: float,
*,
prior: tuple[ndarray, ndarray] | None = None,
fit_radius: bool = False,
widths: Sequence[float] = WIDTHS,
) -> OutlineFit
The sphere whose outline matches the ball's edges in images, by view name.
Starts from center and radius, which must put the predicted outline within
the first of widths pixels of the edges. prior is the (mean, covariance) of
the center and the views' offsets (in views' order) before this frame, which
holds what the outlines barely pin; without one, the offsets are within
OFFSET_PX of zero and the views must see the ball from more than one line.
fit_radius frees the radius. images are gray_frames.
Raises:
| Type | Description |
|---|---|
ValueError
|
If no view shows enough outline, or the views look along one line and there is no prior, or the radius is asked of them. |
Source code in src/deeperfly/ball/outline.py
gray_frame ¶
A decoded (H, W, C) frame as the blurred float luma the edges are found on.
Source code in src/deeperfly/ball/outline.py
grazing_circle ¶
(n, 3) world points of the circle where view's rays graze a sphere.
What view images as the sphere's outline, from a view outside the sphere.
Source code in src/deeperfly/ball/outline.py
rim_points ¶
(n, 2) pixels of a sphere's outline in view, evenly around it.
Source code in src/deeperfly/ball/outline.py
track_outline ¶
track_outline(
views: dict[str, View],
frames: Iterable[dict[str, ndarray]],
center: ndarray,
cov: ndarray,
radius: float,
) -> CenterTrack
The ball's center on each of frames (by view name, gray_frames), filtered.
Starts at center with covariance cov, the first frame with the widest search;
the radius is held. CenterTrack.smoothed brings in the later frames.
Source code in src/deeperfly/ball/outline.py
Where the animal stands on the ball, and a first guess of the ball, from the 3D pose.
The ball itself is placed by its outline in every view that sees it (ball.outline); the pose starts it and orients the animal on it. A tethered animal's leg tips rest on the ball in stance, so the first guess is the sphere, centered on one view's ray through the ball's image center, that rests the most tips on its surface, its radius following from that distance and the circle's angular size. The tips cover only a small cap of the ball and one view's apparent size tells distance poorly, so the guess is a start, not the ball (it was 2.5% of a radius off along the view's ray on octacam).
The animal's axes on a ball: up is the ball's normal through the centroid of the points it stands on (the thorax-coxa points), forward the body axis (hind to front) on the tangent plane. When it is not believably standing on the ball, too few tips on the surface or the body not just above it, the world's up stands in for the normal and only the heading comes from the body. On the lab's octacam 73-84% of the tips rest on the ball in the opening frames, scattered about 2% of its radius about its surface.
Contact
dataclass
¶
The ball, and the animal's axes where it stands on it, in world coordinates.
Attributes:
| Name | Type | Description |
|---|---|---|
axes |
Float[ndarray, '3 3']
|
World to animal, |
center |
Float[ndarray, 3]
|
The ball's center, |
radius |
float
|
The ball's radius, in world units. |
on_surface |
float
|
The share of the tips that rest on the ball. |
height |
float
|
The stance centroid above the surface, in ball radii. |
level |
bool
|
Whether the world's up stands in for the ball's normal, the animal not believably standing on the ball. |
Source code in src/deeperfly/ball/contact.py
line ¶
One log line: where the animal stands on the ball.
Source code in src/deeperfly/ball/contact.py
Stance
dataclass
¶
The 3D points the ball is guessed from and the animal oriented by.
Attributes:
| Name | Type | Description |
|---|---|---|
tips |
Float[ndarray, 'P 3']
|
The finite points that rest on the ball in stance, |
body |
Float[ndarray, 3]
|
The centroid of the points the animal stands on, NaN if unknown. |
body_axis |
Float[ndarray, 3]
|
Hind to front, NaN if unknown. |
Source code in src/deeperfly/ball/contact.py
of
classmethod
¶
of(
tips: Float[ndarray, "T P 3"],
stand: Float[ndarray, "T Q 3"],
front: Float[ndarray, "T F 3"],
hind: Float[ndarray, "T H 3"],
) -> Stance
The stance over some frames of world points, NaN where unknown.
Each point's median over the frames, then the centroids.
Source code in src/deeperfly/ball/contact.py
ball_ray ¶
The ray through the ball's image center, and the sine of its angular radius.
The ray is (origin, unit direction) in the world; the angular radius is the mean
angle between it and the rays of four points of the circle's rim.
Source code in src/deeperfly/ball/contact.py
guess_ball ¶
guess_ball(
view: View,
circle: tuple[float, float, float],
stance: Stance,
) -> tuple[np.ndarray, float] | None
A first (center, radius) of the ball, from one view and the leg tips.
circle is the ball's image (cx, cy, r) in view's pixels. None with too few
tips, or no centroid to start the search from.
Source code in src/deeperfly/ball/contact.py
stand_on ¶
The animal's axes on the ball, and why the world's up stands in, if it does.
Source code in src/deeperfly/ball/contact.py
spintrack on one view's frames, and the rotation it measures in the world frame.
spintrack's camera frame is OpenCV's, as deeperfly's is, so a rotation vector it
measures reaches the world as R.T @ v, with R the view's world-to-camera rotation.
That holds whatever animal frame spintrack was handed: the animal frame only orients
its forward, side and turn, kept verbatim in tracks. spintrack's pixel centers sit at
half-integers and deeperfly's at integers (ADR-0055), so its pixels are moved by -0.5.
spintrack models a pinhole with its principal point at the frame's center and no distortion; it is handed the view's vertical field of view and nothing else of the lens. Its rotations are measured on the ball's own image, so the difference moves the camera's axes by the principal point's offset in angle, a fraction of a degree on a well-centered lens.
detect_circle ¶
The ball's image circle (cx, cy, r) in deeperfly's pixels, from start on.
It is detected in the first frames of start:stop.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
spintrack's |
Source code in src/deeperfly/ball/track.py
track_ball ¶
track_ball(
view: View,
video: Video,
start: int,
stop: int,
axes: ndarray,
rim: ndarray,
*,
tracking: Mapping[str, Any],
two_pass: bool = False,
) -> tuple[dict[str, np.ndarray], list[str], Any]
Track the ball's rotation over frames start:stop of video, which view sees.
rim is points of the ball's outline at start, (n, 2) in deeperfly's pixels;
axes (world to animal) orients spintrack's animal frame. Returns rotation,
step and tracks over the stretch (NaN at a frame spintrack dropped), the names
of tracks' columns, and spintrack's quality summary.
Source code in src/deeperfly/ball/track.py
Access to the optional spintrack tracker, behind one explicit gate.
spintrack runs SAM 3 through torch and transformers and builds a Rust extension, so the
deeperfly[ball] extra holds it and a plain install stays without it (ADR-0144).
Everything that tracks calls require_spintrack inside a function body, never at
module scope; the contact fit imports nothing from here, so it runs without the extra.
MissingSpintrack ¶
require_spintrack
cached
¶
The spintrack module, or MissingSpintrack saying how to install it.