File Formats
This section documents the format of JABS output files that may be needed for downstream analysis.
Prediction File
An inference file represents the predicted classes for each identity present in one video file.
Location
The prediction files are saved in <JABS project dir>/jabs/predictions/<video_name>.h5 if they were generated by the JABS GUI.
The jabs-classify script saves inference files in <out-dir>/<video_name>_behavior.h5
Contents
The H5 file contains one group, called predictions. This group contains one or more behavior prediction groups.
predictions/
behavior_1/
predicted_class
probabilities
predicted_class_postprocessed #optional
identity_to_track #optional
behavior_2/
...
predicted_class_postprocessed is only present if postprocessing was applied after prediction (e.g. stitching, minimum bout length filtering, etc). identity_to_track is only present for predictions using Pose File Version 3 or earlier.
Attributes
The root file contains the following attributes:
- pose_file: filename of the pose file used during prediction
- pose_hash: blake2b hash of pose file
- version: prediction output version
Each behavior prediction group contains the following attributes:
- classifier_file: filename of the classifier file used to predict
- classifier_hash: blake2b hash of the classifier file
- app_version: JABS application version used to make predictions
- prediction_date: date when predictions were made
predicted_class
- dtype: 8-bit integer
- shape: #identities x #frames
This dataset contains the predicted class. Each element contains one of three values:
- 0: "not behavior"
- 1: "behavior"
- -1: no prediction
predicted_class_postprocessed [optional]
- dtype: 8-bit integer
- shape: #identities x #frames
This dataset contains the predicted class after applying postprocessing. Each element contains one of three values:
- 0: "not behavior"
- 1: "behavior"
- -1: no prediction
probabilities
- dtype: 32-bit floating point
- shape: #identities x #frames
This dataset contains the probability (0.0-1.0) of each prediction. If there is no prediction (the identity doesn't exist at a given frame) then the prediction probability is 0.0.
identity_to_track [optional]
- dtype: 32-bit integer
- shape: #identities x #frames
This dataset is only present when using version 3 of the JABS pose estimation format. It maps each JABS-assigned identity back to the original track ID from the pose file at each frame. -1 indicates the identity does not map to a track for that frame. For Pose File Version 4 and greater, JABS uses the identity assignment contained in the pose file, and identity_to_track is omitted from the prediction file.
Feature File
A feature file represents features calculated by JABS for a single animal in a video file.
Location
Feature files are saved per identity at <JABS project dir>/jabs/features/<video_name>/<identity>/features.h5.
Contents
The H5 file contains feature data described in the feature documentation. Features used in JABS classifiers are located within the features group, further separated by per_frame and window_features_<window_size> groups. Features not used in JABS classifiers are located outside the features group.
All features are a vector of data containing the feature value for each frame in the video.
The root file contains the following attributes:
- distance_scale_factor: scale factor used when converting from pixel space to cm space
- identity: identity value from the original pose value
- num_frames: number of frames in the video
- pose_hash: blake2b hash of pose file
- version: feature version used when generating this feature file
Per-Frame Feature Names
Per frame features are named <feature module> <feature name>. Feature modules cannot contain spaces, while feature names can.
Window Feature Names
Window features are named <feature module> <window operation> <feature name>. Feature modules and window operations cannot contain spaces, while feature names can.
NWB Pose File
JABS can export pose estimation data to NWB (Neurodata Without Borders) format using the ndx-pose 0.2 extension. The resulting files contain all animal keypoints, static and dynamic environmental objects, and supporting metadata needed for a lossless round-trip back into JABS.
See NWB Export for the CLI command and output-mode options.
Output modes
JABS writes NWB in two modes:
| Mode | When to use |
|---|---|
| Combined (default) | Local analysis, sharing with collaborators who can parse JABS-specific fields |
Per-identity (--per-identity) |
DANDI archive upload; tools that expect one subject per file |
In combined mode all identities are written into a single .nwb file.
In per-identity mode one file is written per animal, named
{output_stem}_{identity_name}.nwb. Static and dynamic objects are written to every
per-identity file identically (they are session-level data). The JABS reader
re-assembles per-identity files transparently: point it at any sibling and it merges
all siblings into a single result in the original identity order.
Full file layout
The layout below shows a combined file with two animal identities, two static objects
(corners, lixit), and one dynamic object (fecal_boli).
NWBFile
├── subject/ [Subject] per-identity mode only
├── processing/
│ └── behavior/ [ProcessingModule]
│ ├── Skeletons/ [Skeletons container]
│ │ ├── subject/ Skeleton — animal keypoints + edges
│ │ ├── corners/ Skeleton — static object (4 nodes)
│ │ ├── lixit/ Skeleton — static object (1 or 3 nodes)
│ │ └── fecal_boli/ Skeleton — dynamic object (max_count nodes)
│ │
│ ├── subject_1/ [PoseEstimation] animal identity 0
│ │ ├── nose/ [PoseEstimationSeries] num_frames timestamps
│ │ ├── left_ear/
│ │ └── ...
│ │
│ ├── subject_2/ [PoseEstimation] animal identity 1
│ │ ├── nose/
│ │ └── ...
│ │
│ ├── corners/ [PoseEstimation] static object
│ │ ├── corners_0/ [PoseEstimationSeries] 1 timestamp
│ │ ├── corners_1/
│ │ ├── corners_2/
│ │ └── corners_3/
│ │
│ ├── lixit/ [PoseEstimation] static object
│ │ └── lixit_0/ [PoseEstimationSeries] 1 timestamp
│ │
│ ├── fecal_boli/ [PoseEstimation] dynamic object
│ │ ├── fecal_boli_0/ [PoseEstimationSeries] n_predictions timestamps
│ │ ├── fecal_boli_1/
│ │ └── ...
│ │
│ ├── jabs_identity_mask [TimeSeries] uint8 identity presence mask
│ ├── jabs_bounding_boxes_subject_1 [TimeSeries] optional, one per identity
│ └── jabs_bounding_boxes_subject_2 [TimeSeries] optional, one per identity
│
└── scratch/
└── jabs_metadata/ [ScratchData] JSON string (see below)
In a per-identity file the layout is identical, except NWBFile.subject is populated
(when subject metadata is provided), only one animal identity container is present, and
jabs_identity_mask / jabs_bounding_boxes_<identity> cover that identity only.
Animal pose
Each animal identity is a PoseEstimation container in processing/behavior. The
container name is the sanitized external ID from the pose file (e.g. a cage ID), or
subject_{i} when no external IDs are available. A single Skeleton named subject
is shared by all animal identities and stored in the Skeletons container.
The default mouse skeleton has 12 keypoints:
nose, left_ear, right_ear, base_neck,
left_front_paw, right_front_paw, center_spine,
left_rear_paw, right_rear_paw,
base_tail, mid_tail, tip_tail
PoseEstimationSeries fields (per keypoint)
| Field | Value |
|---|---|
name |
Keypoint name (e.g. "nose", "left_ear") |
data |
shape (num_frames, 2) — (x, y) coordinates in pixels |
rate |
Frames per second (float) |
unit |
"pixels" |
reference_frame |
"Top-left corner of video frame, x increases rightward, y increases downward" |
confidence |
shape (num_frames,) — 0.0 = missing keypoint, > 0.0 = valid |
confidence_definition |
"0.0=invalid/missing keypoint, >0.0=valid keypoint" |
Identity mask
jabs_identity_mask is a TimeSeries (dtype uint8) recording whether each identity
is present in each frame.
| Mode | Shape stored in file | Shape returned by reader |
|---|---|---|
| Combined | (num_frames, num_identities) |
(num_identities, num_frames) |
| Per-identity | (num_frames,) |
(1, num_frames) |
Bounding boxes (optional)
When the source pose file contains bounding box data, one TimeSeries per identity is
written with the name jabs_bounding_boxes_{identity_name}.
| Property | Value |
|---|---|
| Shape stored in file | (num_frames, 2, 2) |
| Shape returned by reader | (num_identities, num_frames, 2, 2) (all stacked) |
| Unit | "pixels" |
Format: [[upper_left_x, upper_left_y], [lower_right_x, lower_right_y]].
Static objects
Static objects are fixed-position spatial landmarks that do not move during a session.
They are read from static_objects/ in JABS pose HDF5 files (pose format v5+).
Common static objects:
| Object | Shape | Description |
|---|---|---|
corners |
(4, 2) |
Four corners of the arena |
lixit |
(1, 2) or (3, 2) |
Water spout — single tip, or tip + left + right |
food_hopper |
(4, 2) |
Four corners of the food hopper opening |
Each static object is a PoseEstimation container with a single timestamp
(t = 0.0 s), one PoseEstimationSeries per keypoint, and a dedicated Skeleton.
Nodes are named {object_name}_{i} (zero-indexed). Confidence is always 1.0 for
static objects and should be ignored by consumers.
Example — corners (4 keypoints)
Skeletons/
corners/
nodes: ["corners_0", "corners_1", "corners_2", "corners_3"]
processing/behavior/
corners/ PoseEstimation
corners_0/ PoseEstimationSeries
data: [[10.0, 20.0]] shape (1, 2)
timestamps: [0.0]
confidence: [1.0]
corners_1/
data: [[300.0, 20.0]]
...
Example — lixit (3-keypoint variant)
Skeletons/
lixit/
nodes: ["lixit_0", "lixit_1", "lixit_2"]
processing/behavior/
lixit/ PoseEstimation
lixit_0/ tip
data: [[62.0, 166.0]]
timestamps: [0.0]
confidence: [1.0]
lixit_1/ left side
data: [[65.0, 160.0]]
...
lixit_2/ right side
data: [[60.0, 172.0]]
...
Dynamic objects
Dynamic objects are objects whose position or count may change over time. Unlike animal pose, predictions are not made every frame — only a sparse subset of frames is sampled. Dynamic objects are introduced in JABS pose format v7.
The source HDF5 pose file stores dynamic objects under dynamic_objects/{name}/:
| Dataset | Shape | Description |
|---|---|---|
points |
(n_predictions, max_count, 2) single-keypoint; (n_predictions, max_count, n_kp, 2) multi-keypoint |
Keypoint coordinates |
counts |
(n_predictions,) |
Number of valid object instances at each prediction |
sample_indices |
(n_predictions,) |
Frame indices at which predictions were made |
The points dataset carries an optional HDF5 attribute axis_order ("xy" or "yx",
default "yx"). JABS normalizes all coordinates to (x, y) on read.
In NWB, each dynamic object is a PoseEstimation container with n_predictions
irregular timestamps (one per sampled frame). One PoseEstimationSeries is written
per instance slot × keypoint combination.
Timestamps
timestamps[p] = sample_indices[p] / fps
# Recover on read:
sample_indices[p] = round(timestamps[p] * fps)
Instance slot validity via confidence
Not all max_count slots are occupied at every prediction. Occupancy is encoded in the
confidence field:
Coordinate values in empty slots are meaningless padding and must not be used.
counts can be recovered on read by summing slots where confidence > 0 at each
prediction.
Node naming
| Condition | Node name pattern | Example |
|---|---|---|
n_keypoints == 1 |
{name}_{slot} |
fecal_boli_0 |
n_keypoints > 1 |
{name}_{slot}_{kp} |
door_0_0, door_0_1, door_1_0 |
Example — fecal_boli (single keypoint per instance, up to 3 instances)
50 predictions were made; up to 3 fecal boli visible at once.
Skeletons/
fecal_boli/
nodes: ["fecal_boli_0", "fecal_boli_1", "fecal_boli_2"]
processing/behavior/
fecal_boli/ PoseEstimation
fecal_boli_0/ slot 0
data: shape (50, 2)
timestamps: [t_0, t_1, ..., t_49]
confidence: [1.0, 1.0, 0.0, ...] # 1.0 where counts > 0
fecal_boli_1/ slot 1
data: shape (50, 2)
timestamps: [t_0, t_1, ..., t_49]
confidence: [1.0, 0.0, 0.0, ...] # 1.0 where counts > 1
fecal_boli_2/ slot 2
data: shape (50, 2)
timestamps: [t_0, t_1, ..., t_49]
confidence: [0.0, 0.0, 0.0, ...] # 1.0 where counts > 2
At prediction p=0, counts[0]=2: slots 0 and 1 are valid, slot 2 is padding.
Example — multi-keypoint dynamic object (door: 2 keypoints, up to 2 instances)
Skeletons/
door/
nodes: ["door_0_0", "door_0_1", "door_1_0", "door_1_1"]
# slot 0 slot 0 slot 1 slot 1
# kp 0 kp 1 kp 0 kp 1
processing/behavior/
door/ PoseEstimation
door_0_0/ slot 0, keypoint 0 — left edge
data: shape (30, 2)
timestamps: [t_0, ..., t_29]
confidence: [1.0, 1.0, ...] # 1.0 where counts > 0
door_0_1/ slot 0, keypoint 1 — right edge
data: shape (30, 2)
timestamps: [t_0, ..., t_29]
confidence: [1.0, 1.0, ...] # same as door_0_0 — slot-level validity
door_1_0/ slot 1, keypoint 0
data: shape (30, 2)
timestamps: [t_0, ..., t_29]
confidence: [0.0, 1.0, ...] # 1.0 where counts > 1
door_1_1/ slot 1, keypoint 1
data: shape (30, 2)
timestamps: [t_0, ..., t_29]
confidence: [0.0, 1.0, ...] # same as door_1_0
jabs_metadata scratch field
Every JABS NWB file contains a ScratchData object named jabs_metadata in the NWB
scratch space. Its data field is a JSON string carrying JABS-specific metadata
needed for a lossless round-trip. It is required because pynwb returns
PoseEstimationSeries in alphabetical order from HDF5, which would otherwise scramble
the keypoint ordering. Tools that do not use the JABS reader can parse this JSON
directly to recover ordered keypoint names, identity ordering, subject metadata, and
object classification.
Keys
| Key | Type | Present | Description |
|---|---|---|---|
format_version |
int |
Always | JABS NWB format version. Currently 1. |
identity_names |
list[str] |
Always | Ordered list of animal identity container names. Defines identity order on read. |
num_identities |
int |
Always | Total number of animal identities in the recording session. |
body_parts |
list[str] |
Always | Ordered list of keypoint names for animal skeletons. |
cm_per_pixel |
float \| null |
Always | Pixel-to-centimetre scale factor. null if not available. |
external_ids |
list[str] \| null |
Always | Original external identity names from the pose file (e.g. cage IDs). null if the pose file had no external IDs. |
subjects |
dict[str, dict] \| null |
Always | Per-identity biological metadata keyed by identity name, for all identities. Fields: subject_id, sex, genotype, strain, age, weight, species, description. null if none provided. |
metadata |
dict |
Always | Provenance from the source pose file: source_file, pose_format_version, and optionally source_file_hash. |
static_object_names |
list[str] |
When static objects present | Names of all static object PoseEstimation containers. |
dynamic_object_names |
list[str] |
When dynamic objects present | Names of all dynamic object PoseEstimation containers. |
dynamic_object_shapes |
dict[str, [int, int]] |
When dynamic objects present | Maps each dynamic object name to [max_count, n_keypoints]. Required to reconstruct the 4-D points array on read. |
per_identity_files |
bool |
Per-identity mode only | true if this file is one of a set of per-identity NWB files. |
source_identity_index |
int |
Per-identity mode only | Zero-based index of the identity in this file. Used to restore original order when merging siblings. |
split_subject_count |
int |
Per-identity mode only | Total number of subjects in the session across all split files. Used to validate all siblings are present before merging. |
Example — combined file
{
"format_version": 1,
"identity_names": ["subject_1", "subject_2"],
"num_identities": 2,
"body_parts": ["nose", "left_ear", "right_ear", "base_neck", "left_front_paw",
"right_front_paw", "center_spine", "left_rear_paw", "right_rear_paw",
"base_tail", "mid_tail", "tip_tail"],
"cm_per_pixel": 0.043,
"external_ids": null,
"subjects": {
"subject_1": {
"subject_id": "M123",
"sex": "M",
"genotype": "WT",
"strain": "C57BL/6J",
"age": "P70D",
"weight": null,
"species": "Mus musculus",
"description": null
},
"subject_2": {
"subject_id": "M124",
"sex": "F",
"genotype": "Shank3+/-",
"strain": "C57BL/6J",
"age": "P72D",
"weight": null,
"species": "Mus musculus",
"description": null
}
},
"metadata": {
"source_file": "/data/session_pose_est_v7.h5",
"pose_format_version": 7,
"source_file_hash": "a3f1c8..."
},
"static_object_names": ["corners", "lixit"],
"dynamic_object_names": ["fecal_boli"],
"dynamic_object_shapes": {
"fecal_boli": [3, 1]
}
}
Example — per-identity file (identity 1 of 3)
{
"format_version": 1,
"identity_names": ["subject_1"],
"num_identities": 3,
"body_parts": ["nose", "left_ear", "right_ear", "base_neck", "left_front_paw",
"right_front_paw", "center_spine", "left_rear_paw", "right_rear_paw",
"base_tail", "mid_tail", "tip_tail"],
"cm_per_pixel": 0.043,
"external_ids": null,
"subjects": {
"subject_1": { "subject_id": "M123", "sex": "M", "genotype": "WT" },
"subject_2": { "subject_id": "M124", "sex": "F", "genotype": "Shank3+/-" },
"subject_3": { "subject_id": "M125", "sex": "M", "genotype": "WT" }
},
"metadata": { "source_file": "...", "pose_format_version": 7 },
"static_object_names": ["corners", "lixit"],
"dynamic_object_names": ["fecal_boli"],
"dynamic_object_shapes": { "fecal_boli": [3, 1] },
"per_identity_files": true,
"source_identity_index": 1,
"split_subject_count": 3
}
Per-identity files store the full subjects dict for all identities, not just the one
identity in that file — making each file self-contained.
Container classification
All PoseEstimation containers in processing/behavior are classified using the three
explicit lists in jabs_metadata:
all PoseEstimation containers in behavior
├── name in identity_names → animal identity
├── name in static_object_names → static object
└── name in dynamic_object_names → dynamic object
Using explicit lists rather than inference rules ensures classification remains correct if new container types are added in future format versions.
Coordinate system
All coordinates in JABS NWB files use the following convention:
| Property | Value |
|---|---|
| Origin | Top-left corner of the video frame |
| x axis | Increases rightward (column direction) |
| y axis | Increases downward (row direction) |
| Units | Pixels |
This applies to animal keypoints, static object points, and dynamic object points. NWB
files always store coordinates in (x, y) order. Dynamic object coordinates may be
stored as (y, x) in the HDF5 pose source (controlled by the axis_order attribute);
JABS flips them to (x, y) before writing NWB.
Data not exported to NWB
The following data present in JABS pose HDF5 files is not included in the NWB output:
| Data | Pose version | HDF5 location | Notes |
|---|---|---|---|
| Instance segmentation masks | v6+ | poseest/seg_data |
Per-frame per-identity binary or instance masks |
| Long-term segmentation IDs | v6+ | poseest/longterm_seg_id |
Identity tracking via segmentation |
| Instance segmentation IDs | v6+ | poseest/instance_seg_id |
Frame-level instance assignments |
| Segmentation external flags | v6+ | poseest/seg_external_flag |
Internal/external identity classification |
There is no standard NWB or ndx-pose representation for instance segmentation masks. If you need this data downstream, read it directly from the source JABS pose HDF5 file.