NWB Export
The jabs-cli convert-to-nwb command converts a JABS pose estimation HDF5 file to
NWB (Neurodata Without Borders) format using the
ndx-pose extension (plus
ndx-jabs for segmentation contours).
Optional dependency
NWB support is not installed by default. Install the nwb extra before use:
The extra adds pynwb, ndx-pose, ndx-jabs, and ndx-multisubjects as dependencies.
Two output modes are available. Choose the mode based on how the files will be used:
| Mode | When to use |
|---|---|
| Per-identity (default) | DANDI archive upload, tools that expect one subject per file |
Multisubject (--multisubject) |
A single shareable file holding every subject (via the ndx-multisubjects extension) |
Command usage
| Argument / Option | Description |
|---|---|
INPUT_PATH |
JABS pose HDF5 file, any version v2–v8. Format version is inferred automatically from the filename (e.g. _pose_est_v6.h5). |
OUTPUT |
Destination .nwb file. By default (per-identity), used as a naming template; the file itself is not created directly. With --multisubject, the single combined file is written directly to this path. |
--multisubject |
Write a single multi-subject NWB file (using the ndx-multisubjects extension) instead of the default one file per identity. |
--session-description TEXT |
NWB session description string. Defaults to 'JABS PoseEstimation Data'. |
--subjects PATH |
Required. Path to a JSON file with per-animal biological metadata. The conversion fails without the fields DANDI requires - see Subjects JSON format. |
--session-metadata PATH |
Path to a JSON file with NWB session-level metadata (start time, experimenter, etc.). |
--segmentation / --no-segmentation |
Whether to include instance segmentation contours when the pose file has them. Defaults to --segmentation. Pose files before v6, and v6+ files generated without segmentation, carry none either way. |
Examples
# One NWB file per identity (default; recommended for DANDI upload)
jabs-cli convert-to-nwb session_pose_est_v6.h5 session.nwb --subjects subjects.json
# A single multi-subject file holding every identity
jabs-cli convert-to-nwb session_pose_est_v6.h5 session.nwb --subjects subjects.json --multisubject
# Also set session start time and experimenter
jabs-cli convert-to-nwb session_pose_est_v6.h5 session.nwb \
--subjects subjects.json --session-metadata session.json
# Leave segmentation contours out of a pose file that has them
jabs-cli convert-to-nwb session_pose_est_v6.h5 session.nwb \
--subjects subjects.json --no-segmentation
--subjects appears in every example because the conversion fails without it; see
Subjects JSON format.
Subjects JSON format
Pass a JSON file to --subjects to attach per-animal biological metadata to the NWB
output. Keys are identity names: use external IDs from the pose file when present (e.g.
"mouse_a"), or subject_1, subject_2, … (1-based) when the pose file has no external
IDs. subject_1 is identity index 0.
An optional name field renames the identity — see
Naming identities.
--subjects is required
species, sex, and either age or date_of_birth are mandatory on every
identity. The converter validates them before writing anything and fails if any
are missing or malformed, because the DANDI archive rejects files without them.
A key no identity reads is ignored with a warning - either because it matches nothing, or because a higher-precedence key for the same identity shadows it. That usually leaves an identity without metadata and fails the check, so if you see the warning, compare your keys against the identity names it lists.
A blank value ("") counts as not supplied, exactly as the writer treats it.
{
"subject_1": {
"subject_id": "M123",
"sex": "M",
"species": "Mus musculus",
"age": "P70D",
"genotype": "WT",
"strain": "C57BL/6J"
},
"subject_2": {
"subject_id": "M124",
"sex": "F",
"species": "Mus musculus",
"age": "P72D",
"genotype": "Shank3+/-",
"strain": "C57BL/6J"
}
}
| Field | Type | Notes |
|---|---|---|
name |
string | Renames the identity itself: the NWB container, the per-identity output filename, and the bounding box series. Defaults to the key. See Naming identities. |
subject_id |
string | Lab identifier for the animal. Defaults to the identity name when omitted or blank. Must not contain /, which breaks DANDI paths. |
sex |
string | Required. Exactly "M", "F", "O", or "U" - case-sensitive, so "male" and "m" are both rejected. For C. elegans, "XO" or "XX" instead. |
species |
string | Required. Latin binomial, e.g. "Mus musculus" - capitalized genus, lowercase epithet, so "Mus Musculus" and "mouse" are rejected. An NCBI taxonomy IRI such as "http://purl.obolibrary.org/obo/NCBITaxon_10090" is also accepted. |
age |
string | Required (or date_of_birth). ISO 8601 duration, e.g. "P70D" (70 days) or "P2Y". A range is allowed: "P1D/P3D", and either end may be left open ("P90Y/", "/P3D"). A range must be strictly increasing. |
date_of_birth |
string | Alternative to age. ISO 8601 datetime, e.g. "2024-01-15T00:00:00+00:00". |
genotype |
string | Genetic background, e.g. "Shank3B+/-" |
strain |
string | Inbred strain, e.g. "C57BL/6J" |
weight |
string | Body weight as [numeric] [unit] with a space, e.g. "25 g" or "0.025 kg". "25g" is rejected. Units: kg, g, mg, ug, μg, ng, pg. A bare number is accepted only as a JSON float, interpreted as kilograms. |
description |
string | Free-text notes |
cfi |
number | Aging study: cumulative frailty index score (manually scored items, each 0, 0.5 or 1, summed over 28 items). Stored on an ndx-jabs JaxStudySubject. |
cfi_norm |
number | cfi divided by the number of items (28), 0 to 1. |
pll |
number | Proportion of life lived: age at the open-field test divided by age at death, 0 to 1. Leave out for animals not followed to death. |
diet |
string | Diet intervention group: AL (ad libitum), 1D/2D (fasting one day or two consecutive days per week), 20/40 (caloric restriction at 20% or 40% of baseline ad libitum intake). |
housing_id |
string | Identifier of the housing (for example the cage) the animal was kept in. |
test_chamber |
string | Identifier of the test chamber the animal was tested in. |
housing_id and test_chamber are generic JAX study identifiers. cfi, cfi_norm, pll and
diet are specific to the JAX aging study. All are optional. When any aging field is present
the file's NWBFile.subject is written as an ndx-jabs JaxAgingSubject; when only housing_id
or test_chamber is present it is a JaxStudySubject; otherwise it is a plain Subject.
None and blank values are left out, and a non-numeric cfi, cfi_norm or pll is rejected.
In multisubject mode these fields are not added to the SubjectsTable; they are kept only in
the jabs_metadata copy of the subject. The aging study's batch and tester are session-level
fields; see Session metadata JSON format.
In per-identity mode (the default), subject metadata is written to both the standard
NWBFile.subject field and the jabs_metadata scratch field. In multisubject mode it is
written to a SubjectsTable (one row per subject) and to jabs_metadata (see
below).
The validation above is a pre-flight check that mirrors nwbinspector's subject checks,
which DANDI treats as blocking. It is not a substitute for the real thing - run
nwbinspector against the converted files before uploading:
Naming identities
A pose file with no external identities leaves its animals called subject_1,
subject_2, … . That name is not cosmetic — it names the PoseEstimation container, the
per-identity output file ({stem}_{identity_name}.nwb) and the bounding box series.
subject_id does not change any of them; it only labels the Subject.
The name field supplies the external identity such a pose file is missing. A pose file
that already carries external_identities normally keeps them, and needs none of this.
To name an identity, give its entry a name:
{
"subject_1": {
"name": "NV1-B2A",
"subject_id": "M123",
"species": "Mus musculus",
"sex": "M",
"age": "P70D"
}
}
That writes session_NV1-B2A.nwb holding an NV1-B2A container, whose Subject is
M123. Use the same name and subject_id if you want them to agree.
- The key still identifies which identity you mean, so it stays
subject_1(or the pose file's external ID) even though the entry renames it. - Renaming some identities and not others is fine; the rest keep the names they had.
- Names must be unique, and are sanitized for use as container names — any character
that is not alphanumeric,
_or-becomes_, soNV1/B2Ais stored asNV1_B2Aand a warning says so. - Whatever it resolves to is recorded as that identity's
external_idsentry injabs_metadata, which is the field the reader restores identity names from. Identities you leave unnamed keep theirsubject_Nplaceholder, and that placeholder is what lands inexternal_idsfor them. - A pose file that already has external identities can be renamed the same way, keyed by the existing external ID, but normally you would leave those names alone.
Session metadata JSON format
Pass a JSON file to --session-metadata to set NWB session-level fields. This is the
primary way to specify session_start_time, which is not currently stored in JABS pose
files and otherwise defaults to the time the export was run.
{
"session_start_time": "2024-03-15T10:30:00-05:00",
"experimenter": ["Jane Smith", "John Doe"],
"lab": "Kumar Lab",
"institution": "The Jackson Laboratory",
"experiment_description": "Open field test",
"session_id": "session_001",
"keywords": ["open field", "mouse", "behavior"]
}
| Key | Type | Description |
|---|---|---|
session_start_time |
ISO 8601 string | Recording start time. Should include a UTC offset (e.g. -05:00, +00:00, or Z). If no offset is provided, the time is assumed UTC and a warning is emitted. Defaults to export time. |
experimenter |
string or list of strings | Name(s) of the experimenter(s). |
lab |
string | Lab name. |
institution |
string | Institution name. |
experiment_description |
string | Free-text description of the experiment. |
session_id |
string | Lab-specific session identifier. |
keywords |
list of strings | Keywords/tags describing the session. |
batch |
string | Aging study: data-collection batch of the frailty assessment. Stored in an ndx-jabs JaxAgingStudyMetaData. |
tester |
string | Aging study: anonymized ID of the trained expert who assigned the manual frailty-index score. Stored in the same JaxAgingStudyMetaData. |
All fields are optional. Unknown keys are ignored with a warning.
Publishing to the DANDI archive
JABS NWB output is intended for the EMBER archive — the data archive for the NIH BRAIN Initiative's Brain Behavior Quantification and Synchronization (BBQS) program. EMBER is operationally distinct from DANDI but runs on DANDI infrastructure at dandi.emberarchive.org, so DANDI's validation rules apply.
Uploads are validated with nwbinspector,
and any CRITICAL finding blocks the upload. The guidance below is about
clearing that gate.
Requirements at a glance
| Requirement | Why |
|---|---|
| Use per-identity mode (the default) | One Subject per file is what DANDI's subject checks expect. --multisubject is not archive-eligible — see Limitations. |
Pass --subjects with complete metadata |
species, sex, and age or date_of_birth are mandatory. Without them every file fails three CRITICAL checks. |
Pass --session-metadata |
Not mandatory, but it clears the remaining best-practice warnings and records provenance the archive displays. |
Link the videos with jabs-cli link-nwb-video |
Only needed to upload the videos. DANDI accepts a video only when an NWB file points to it. See step 3. |
1. Prepare the metadata files
Write a subjects.json covering every identity in the pose file (see
Subjects JSON format), and a session.json with at least
session_start_time (see
Session metadata JSON format). Pose files do not
record a session start time, so without it the export falls back to the time the
conversion ran, which is not the recording time.
2. Convert
jabs-cli convert-to-nwb session_pose_est_v6.h5 session.nwb \
--subjects subjects.json \
--session-metadata session.json
The converter validates subject metadata before writing anything. If a required field is missing or malformed it reports every problem across every identity at once and writes no files:
Error: Subject metadata required by the DANDI archive is missing or malformed:
subject_1: species is missing; sex is missing; age or date_of_birth is missing
subject_2: sex 'male' must be one of 'M', 'F', 'O', 'U'
A --subjects key that matches no identity is reported as a warning naming the valid
identity names, which is the usual explanation for an identity that looks like it was
given metadata but reports it as missing.
3. Link the videos
To publish the videos the poses were estimated from, the NWB files must point to them: DANDI accepts a video only when an NWB file references it, and the converted pose files do not. Videos uploaded on their own would be disconnected from their sessions.
Each <name>.nwb in the first directory is matched with the <name>.mp4 of the same
name in the second. The two directories can be different or the same directory;
subdirectories are not searched. For each match the command adds an ImageSeries named
video to acquisition whose external_file is the path to the video, relative to the
NWB file. The video is referenced, not copied or changed, and the pose data in the NWB
file is not touched.
- Before linking, the video's frame count and frame rate are checked against the pose
data. A video that disagrees is reported and its NWB file is left unchanged. Use
--no-verifyto skip the check. - A file that already links to its video is skipped, so the command is safe to re-run.
- Use
--dry-runto see what would be linked without modifying any file. - The exit status is 1 if any file could not be linked, for example because its video is missing.
- Requires pynwb 4.0 or newer.
The relative path is resolved against the directory of each NWB file, both by
dandi organize and by nwbinspector, which reports a CRITICAL finding for a link that
does not resolve. Link the files once the NWB and video directories are where they will
stay until the next step, because moving either one afterwards breaks the links.
Then let DANDI gather the files into the dandiset. --update-external-file-paths is what
makes dandi organize follow the links:
dandi organize -d /path/to/dandiset --files-mode copy \
--update-external-file-paths --media-files-mode copy /path/to/nwb_files
This copies each video into the dandiset beside its NWB file and rewrites the link inside
the NWB file to point at that copy. Each NWB file gets its own copy of the video, and
dandi organize adds an _image suffix to the names of NWB files that contain an
ImageSeries.
4. Validate locally before uploading
JABS's pre-flight check mirrors nwbinspector's subject rules, but it is not a
substitute for running the real validator — it checks subject metadata, not the rest
of the file. Run nwbinspector on each output before you upload:
A clean result reports no CRITICAL findings. See
Expected warnings for the ones that are safe to ignore.
5. Upload
Follow the DANDI upload documentation, pointing the DANDI CLI at the EMBER instance rather than the main archive. Upload all per-identity files from a session together — the JABS reader needs the full set of siblings to reassemble the session (see Reading per-identity files).
What JABS checks, and what it does not
| Checked before writing | Left to nwbinspector / the archive |
|---|---|
species present and in Latin binomial or NCBI IRI form |
Everything outside Subject metadata |
sex present and a valid code for the species |
File structure, timestamps, data orientation |
age or date_of_birth present, and well-formed |
Dataset-level best practices |
age ranges strictly increasing |
|
subject_id free of / |
|
weight in [numeric] [unit] form |
Passing the JABS check is necessary but not sufficient — always run
nwbinspector before uploading.
Expected warnings
These appear in a normal, acceptable export and do not block upload:
- Missing session-level metadata, when
--session-metadatais omitted:check_experimenter_exists,check_institution,check_keywords,check_experiment_description, and a missing subjectdescription. All are best-practice suggestions rather than blocking findings.
Limitations for archive submission
--multisubjectoutput cannot be validated. The file depends on thendx-multisubjectsextension, andnwbinspectorfails to read it, so the archive cannot validate it. It also does not populateNWBFile.subject, which DANDI's required subject checks read. Use the default per-identity mode for anything destined for the archive.- Segmentation data is not exported. See Data not exported.
Output modes
Per-identity files (default)
One NWB file is written per animal. The OUTPUT path is used as a naming template;
files are written as {output_stem}_{identity_name}.nwb in the same directory. The
OUTPUT path itself is not created.
session_subject_1.nwb ← identity 0 + all objects
session_subject_2.nwb ← identity 1 + all objects
session_subject_3.nwb ← identity 2 + all objects
This is the most standard output. Each file contains exactly one animal, so
NWBFile.subject is populated with that animal's biological metadata from
--subjects. Any standard NWB tool — including the DANDI archive — can read
the subject field directly without knowing anything about JABS.
Identity names in the filenames come from external_ids in the pose file (sanitized
for filesystem compatibility), or from the name field in --subjects when the pose
file has none — see Naming identities — falling back to
subject_1, subject_2, … when neither supplies one. Static and dynamic objects are written to every per-identity
file identically, since they are session-level data.
Reading per-identity files
The JABS reader re-assembles per-identity files transparently. Point it at any one
sibling file; it detects the per_identity_files flag in jabs_metadata, finds all
siblings, and merges them into a single result with all identities in their original
order.
Multisubject single file (--multisubject)
All identities from the recording session are written into a single, self-contained NWB
file at OUTPUT, using the
ndx-multisubjects extension.
The file is an NdxMultiSubjectsNWBFile (a drop-in NWBFile subclass). Because standard
NWB's NWBFile.subject only holds one subject, multiple subjects are instead described by
a SubjectsTable (one row per animal) stored in acquisition. The pose data itself
is laid out in processing/behavior exactly as in a per-identity file, and the full,
lossless JABS round-trip still rides on the jabs_metadata scratch field.
This mode is intended for sharing a whole session as one artifact. Reading it back with the JABS reader returns all identities directly (no sibling files are involved).
Subject metadata by mode
| Mode | NWBFile.subject | SubjectsTable | jabs_metadata.subjects |
|---|---|---|---|
| Per-identity | Set for this file's identity | — | Set (all identities) |
| Multisubject | Not set | One row per subject | Set (all identities) |
jabs_metadata.subjects always carries the full dict for all identities, in both modes.
This makes each per-identity file self-contained: the JABS reader can recover complete
subject metadata from any sibling without loading the others. In multisubject mode the
SubjectsTable is provided for standard NWB / DANDI consumers; JABS itself recovers
subject metadata from jabs_metadata.
NWB file structure
For the full format specification — including all field definitions, jabs_metadata
keys, and worked examples for static and dynamic objects — see
File Formats — NWB Pose File.
The layout below shows a multisubject file with two animal identities, two static objects
(corners, lixit), and one dynamic object (fecal_boli).
NdxMultiSubjectsNWBFile
├── acquisition/
│ └── SubjectsTable [DynamicTable] multisubject mode only — one row per subject
├── 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)
A per-identity file (the default) uses a plain NWBFile and the same
processing/behavior layout, except:
- The root is a standard NWBFile; there is no SubjectsTable
- NWBFile.subject is populated (when subject metadata is provided)
- Only one animal identity container is present
- 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, or subject_1,
subject_2, … (1-based) when no external IDs are available.
A single Skeleton named subject is shared by all animal identities and stored in
the Skeletons container.
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 that records whether each identity is present in
each frame.
| Mode | Shape stored in file | Shape returned by reader |
|---|---|---|
| Multisubject | (num_frames, num_identities) |
(num_identities, num_frames) |
| Per-identity | (num_frames,) |
(1, num_frames) |
Bounding boxes (optional)
When the pose file contains bounding box data, one TimeSeries per identity is written
with the name jabs_bounding_boxes_{identity_name}.
| Property | Value |
|---|---|
| Name | jabs_bounding_boxes_{identity_name} |
| Shape stored in file | (num_frames, 2, 2) |
| Shape returned by reader | (num_identities, num_frames, 2, 2) |
Format: [[upper_left_x, upper_left_y], [lower_right_x, lower_right_y]] in pixels.
Segmentation contours (optional)
When the pose file contains instance segmentation (v6 and later, and only when the file
was generated with segmentation), one ContourSeries per identity is written to the
behavior processing module, named jabs_segmentation_contours_{identity_name}. It sits
next to that identity's PoseEstimation, so the contours, the keypoints, and the subject
all describe the same animal.
ContourSeries comes from the ndx-jabs
extension rather than ndx-pose, because contours are an intermediate product of JABS's own
shape features, not a representation other pose tools consume. The extension's spec is
embedded in every file that uses it, so readers without ndx-jabs installed can still open
the file.
| Field | Value |
|---|---|
name |
jabs_segmentation_contours_{identity_name} (one per identity) |
data |
shape (num_frames, num_contours, num_vertices, 2) — (x, y) vertices in pixels |
vertex_count |
shape (num_frames, num_contours) — valid vertices in each contour slot |
is_external |
shape (num_frames, num_contours) — True = outer boundary, False = hole |
reference_frame |
Top-left corner of video frame, x increases rightward, y increases downward |
rate |
Frames per second (float) |
unit |
"pixels" |
An animal needs more than one contour on a frame when its outline has a hole (it curls
around a gap) or when an occluder splits it into disjoint parts. Because different
frames need different numbers of contours and vertices, data is padded out to the
largest of each with -1, and vertex_count is what says how much of each slot is
real. Read it rather than scanning for the padding value:
n = contour_series.vertex_count[frame, slot]
if n: # 0 means this slot holds no contour on this frame
vertices = contour_series.data[frame, slot, :n, :]
is_external has no meaning where vertex_count is 0.
The contour dataset is chunked along the frame axis and gzip compressed. It is mostly padding, which compresses well: roughly 40 MB per identity per hour of 30fps video.
Segmentation presence is independent of jabs_identity_mask: an identity can have
contours on a frame where its keypoints were not resolved, because JABS assigns
contours from longterm_seg_id and keypoints from instance_embed_id. Use
vertex_count to decide whether an identity has an outline on a given frame.
Pass --no-segmentation to leave the contours out of a pose file that has them.
jabs_metadata.has_segmentation records whether they were written.
A pose file that has contours but no seg_external_flag dataset is exported without them, and
a warning is logged. is_external is required and cannot say "unknown", so exporting such a
file would mean guessing which contours are holes. JABS-pose writes the two datasets together,
so this should only affect files produced some other way.
Identity names must not collide with names JABS generates in the same container: an identity
cannot be called jabs_identity_mask, and in a --multisubject file one identity cannot be
named jabs_segmentation_contours_<another identity> or jabs_bounding_boxes_<another identity>.
The export fails before writing anything if they do.
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 one
PoseEstimationSeries per keypoint and a dedicated Skeleton. Nodes are named
{object_name}_{i} (zero-indexed).
The constant (x, y) value is written at two timestamps spanning the session —
the first and last frame — giving each series data shape (2, 2). A single-timestamp
series would have shape (1, 2), whose non-time axis is longer than its time axis,
which nwbinspector's check_data_orientation flags regardless of the data being
genuinely static. Repeating the value at both ends leaves it unchanged while keeping
the export clean.
Dynamic objects
Dynamic objects are objects whose position or count may change over time. Unlike animal pose, predictions are made only for a sparse subset of frames. Dynamic objects are available from JABS pose format v7+.
Each dynamic object is a PoseEstimation container with n_predictions irregular
timestamps. One PoseEstimationSeries is written per instance slot × keypoint
combination.
Instance slot occupancy is encoded in the confidence field:
confidence = 1.0— slot is occupied at this predictionconfidence = 0.0— slot is unoccupied; coordinate values are padding and must be ignored
Node naming:
| Condition | Node name pattern | Example |
|---|---|---|
| Single keypoint per instance | {name}_{slot} |
fecal_boli_0 |
| Multiple keypoints per instance | {name}_{slot}_{kp} |
door_0_0, door_0_1 |
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. Standard NWB fields alone are insufficient 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 identity ordering, subject metadata, and object classification. (Keypoint ordering is not stored here; the JABS reader restores it from the canonical keypoint index.)
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. |
cm_per_pixel |
float \| null |
Always | Pixel-to-centimetre scale factor. null if not available. |
external_ids |
list[str] \| null |
Always | External identity names: from the pose file when it has them, otherwise the ones supplied with the name field in --subjects. null if neither. Identities left unnamed take their subject_N placeholder. |
subjects |
dict[str, dict] \| null |
Always | Per-identity subject metadata keyed by identity name, for all identities. null if no subject metadata is available. Fields: subject_id, sex, species, age, date_of_birth, genotype, strain, weight, description. DANDI requires species, sex, and either age or date_of_birth. |
metadata |
dict |
Always | Provenance from the source pose file: source_file, pose_format_version, and optionally source_file_hash. |
has_segmentation |
bool |
Always | true if segmentation contours were written. Absent in older files, which carry no contours. |
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]. |
multisubject |
bool |
Multisubject mode only | true if this is a single multi-subject file written with the ndx-multisubjects extension. |
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. |
split_subject_count |
int |
Per-identity mode only | Total number of subjects in the session across all split files. |
write_set_id |
str |
Per-identity mode only | Random identifier shared by every file written together; stale files at the same stem are ignored on read. Absent in older files. |
Example — multisubject file
{
"format_version": 1,
"multisubject": true,
"identity_names": ["subject_1", "subject_2"],
"num_identities": 2,
"cm_per_pixel": 0.043,
"external_ids": null,
"subjects": {
"subject_1": {
"subject_id": "M123",
"sex": "M",
"species": "Mus musculus",
"age": "P70D",
"genotype": "WT",
"strain": "C57BL/6J",
"weight": null,
"description": null
},
"subject_2": {
"subject_id": "M124",
"sex": "F",
"species": "Mus musculus",
"age": "P72D",
"genotype": "Shank3+/-",
"strain": "C57BL/6J",
"weight": null,
"description": null
}
},
"metadata": {
"source_file": "/data/session_pose_est_v7.h5",
"pose_format_version": 7,
"source_file_hash": "a3f1c8..."
},
"has_segmentation": true,
"static_object_names": ["corners", "lixit"],
"dynamic_object_names": ["fecal_boli"],
"dynamic_object_shapes": {
"fecal_boli": [3, 1]
}
}
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.
Data not exported
Instance segmentation contours (poseest/seg_data) are exported; see
Segmentation contours above. The segmentation
bookkeeping datasets JABS uses to assign those contours to identities are not, because
the export resolves them into per-identity contours. See
File Formats — Data not exported to NWB
for the full list of omitted fields.