Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ cache invalidation logic (version mismatch, pose hash, distance scale).
- `__init__.py` files: unused imports (F401) are allowed
- **Naming**: `PascalCase` classes, `snake_case` functions/methods/files,
`UPPER_SNAKE_CASE` constants, `_` prefix for private members
- Prefer American English spelling (e.g., "behavior" not "behaviour").

## Coding Standards

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ For detailed information about JABS architecture, development setup, and impleme

- **General questions**: Open an issue on GitHub
- **Security issues**: Email jabs@jax.org (do not open public issues)
- **Development questions**: See [DEVELOPMENT.md](docs/DEVELOPMENT.md) or contact jabs@jax.org
- **Development questions**: See [DEVELOPMENT.md](docs/development/DEVELOPMENT.md) or contact jabs@jax.org

## Code of Conduct

Expand Down
598 changes: 598 additions & 0 deletions docs/development/jabs-nwb-format.md

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion packages/jabs-core/src/jabs/core/abstract/pose_est.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
from shapely.geometry import MultiPoint

from jabs.core.types import DynamicObjectData
from jabs.core.utils import hash_file

MINIMUM_CONFIDENCE = 0.3
Expand Down Expand Up @@ -132,7 +133,8 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30
self._hash = hash_file(file_path)
self._fps = fps

self._static_objects = {}
self._static_objects: dict = {}
self._dynamic_objects: dict[str, DynamicObjectData] = {}

# check cache version, if it doesn't match, clear the cache file for this pose file
if self._cache_dir is not None and not self.check_cache_version():
Expand Down Expand Up @@ -270,6 +272,28 @@ def static_objects(self):
"""get static objects from the pose file"""
return self._static_objects

@property
def dynamic_objects(self) -> dict[str, DynamicObjectData]:
"""Get dynamic objects from the pose file.

Returns:
Mapping of object name to jabs.core.types.DynamicObjectData.
Empty for pose versions that do not support dynamic objects (v2-v6).
"""
return self._dynamic_objects

def get_dynamic_object(self, name: str) -> DynamicObjectData | None:
"""Get a named dynamic object from the pose file.

Args:
name: Name of the dynamic object (e.g. "fecal_boli").

Returns:
DynamicObjectData for the requested object, or None if not present.
Always None for pose versions that do not support dynamic objects (v2-v6).
"""
return self._dynamic_objects.get(name)

def get_identity_convex_hulls(self, identity):
"""get a list of length #frames containing convex hulls for the given identity.

Expand Down Expand Up @@ -419,3 +443,11 @@ def _cache_file_path(self) -> Path | None:
return None
filename = self._path.name.replace(".h5", "_cache.h5")
return self._cache_dir / filename

def get_bounding_boxes(self, identity: int) -> np.ndarray | None:
"""Get bounding box array for an identity index.

Default implementation returns None, indicating no bounding box data.
Bounding boxes are not available in pose file versions <8, this is included for interface consistency.
"""
return None
3 changes: 2 additions & 1 deletion packages/jabs-core/src/jabs/core/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .inference import AggregationSpec, InferenceSampling
from .keypoints import FrameKeypoints, FrameKeypointsData, KeypointAnnotation
from .model import ModelInfo
from .pose import PoseData
from .pose import DynamicObjectData, PoseData
from .prediction import BehaviorPrediction, ClassifierMetadata
from .results import InferenceRunMetadata, KeypointInferenceResult
from .video import VideoInfo
Expand All @@ -12,6 +12,7 @@
"AggregationSpec",
"BehaviorPrediction",
"ClassifierMetadata",
"DynamicObjectData",
"FrameKeypoints",
"FrameKeypointsData",
"InferenceRunMetadata",
Expand Down
34 changes: 34 additions & 0 deletions packages/jabs-core/src/jabs/core/types/pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@
from typing import Any

import numpy as np
import numpy.typing as npt


@dataclass(frozen=True)
class DynamicObjectData:
"""Container for a single dynamic object read from a pose file.

Dynamic objects are objects that may change position or count over time, but
are not predicted every frame. Only frames listed in sample_indices have
valid predictions. Coordinates are always stored in (x, y) order.

points is always 4-D regardless of how many keypoints each object
instance has. Single-keypoint objects (e.g. fecal boli) have
n_keypoints=1 after normalization on read.

Attributes:
points: Detected keypoint coordinates in (x, y) order, shape
(n_predictions, max_count, n_keypoints, 2).
counts: Number of valid detected objects for each prediction, shape
(n_predictions,).
sample_indices: Frame indices at which each prediction was made, shape
(n_predictions,).
"""

points: npt.NDArray[np.float64]
counts: npt.NDArray[np.int64]
sample_indices: npt.NDArray[np.int64]


@dataclass(frozen=True)
Expand All @@ -20,8 +47,13 @@ class PoseData:
Format is [[upper_left_x, upper_left_y], [lower_right_x, lower_right_y]].
segmentation_data: Optional segmentation masks or data.
static_objects: Dictionary of static objects (e.g., 'lixit') and their positions.
dynamic_objects: Dictionary of dynamic objects (e.g., 'fecal_boli') and their data.
external_ids: Optional list of external identifiers for each identity.
Maps an identity index to an external ID string.
subjects: Optional per-animal biological metadata, keyed by identity name
(matching the values in external_ids). Each value is a free-form
dict; standard keys are subject_id, sex, genotype,
strain, age, weight, species, description.
metadata: Dictionary for any additional provenance or experimental metadata.
"""

Expand All @@ -35,7 +67,9 @@ class PoseData:
bounding_boxes: np.ndarray | None = None
segmentation_data: np.ndarray | None = None
static_objects: dict[str, np.ndarray] = field(default_factory=dict)
dynamic_objects: dict[str, DynamicObjectData] = field(default_factory=dict)
Comment thread
gbeane marked this conversation as resolved.
external_ids: list[str] | None = None
subjects: dict[str, dict] | None = None
metadata: dict[str, Any] = field(default_factory=dict)

def __post_init__(self):
Expand Down
1 change: 0 additions & 1 deletion packages/jabs-io/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ authors = [

dependencies = [
"jabs-core",
"ndx-pose>=0.2.2",
"numpy>=2.0.0,<3.0.0",
]

Expand Down
2 changes: 1 addition & 1 deletion packages/jabs-io/src/jabs/io/internal/pose/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Pose estimation NWB adapters."""
"""Pose estimation adapters (NWB requires the [nwb] extra)."""

from jabs.io.internal.pose.nwb import PoseNWBAdapter

Expand Down
Loading
Loading