Skip to content

Latest commit

 

History

History
157 lines (122 loc) · 6.75 KB

PROJECT_FILE_FORMAT.md

File metadata and controls

157 lines (122 loc) · 6.75 KB

The .bmp.json project file

A portable, reloadable snapshot of one trial's analysis - everything except the video itself. Save one from the app after tracking ("Save project file"), open it later on any computer, and the app restores the trial without re-tracking. examples/ has a real one for each of the three sample clips.

Implemented in app/js/project-file.js.

What it is for

This file is for the tool: complete, lossless, resumable. The CSV/XLSX export is for a human and their stats software: summarised and lossy on purpose. Neither replaces the other, and they are deliberately not the same file.

The video is not in it. Videos are enormous and the user already has the one this came from, so the file carries a fingerprint of it instead (see below) and asks for it back when reopened.

Written as

JSON, UTF-8, 2-space indent, never minified. A researcher may open this in a plain text editor on a machine with nothing better installed, so field names are real words (frame, cx, cy, valid) rather than compact shorthand, and the indentation is there so the structure can be skimmed by eye.

Top-level shape

{
  "format": "barnes-maze-pipeline-session",
  "formatVersion": 2,
  "toolVersion": "0.1.0",
  "savedAt": "2026-09-05T18:44:23.333Z",
  "video":      { ... },   // fingerprint of the video this came from
  "roi":        { ... },   // platform, holes, target, scale
  "metadata":   { ... },   // trial/cohort/group/animal, free text
  "thresholds": { ... },   // every parameter, whether or not it was used
  "results":    { ... },   // trajectory + per-hole signals
  "overrides":  { ... },   // the human corrections
  "flags":      { ... }    // what was flagged for review
}

video - the fingerprint

fileName, fileSize, lastModified, totalFrames, timescale, trackDuration, width, height, codec, duplicateTimestampFrames (a count).

totalFrames, timescale and trackDuration together are the exact rational frame rate, not a rounded decimal - test51 is 15000/1001 fps, not 15. All timing on reload is recomputed from these, never from an assumed fps.

On reload the app re-checks the file you hand it against this section (checkVideoFingerprint). It is strict about anything that would silently corrupt the analysis - a different frame count, a different duration or frame rate, different dimensions, a different file size all refuse the load, and the refusal names both values ("expected 9000 frames, this video has 8991") rather than just saying it doesn't match. A changed filename warns and continues, because a rename doesn't change the video. lastModified is deliberately never checked: copying a file changes it.

roi

platform ({x, y, r}), holes (20 of the same), targetHoleIndex, holeStart (whether 0- or 1-based hole numbering was on screen when this was saved - recorded rather than assumed, so a reload and an export always agree), diameterCm (the real-world scale, what makes path length and speed reportable in cm), and edited (whether a human moved any of it).

metadata

number, cohort, group, animalId, priorTrials - free text exactly as entered, never parsed by anything. Animal IDs keep their leading zeros here and all the way through the XLSX export.

thresholds

All ten, in one flat object: maskThreshold, minArea, maxArea, minCenterDensity, minRun, gapTolerance, speedRatio, reengageFraction, serialBar, randomBar.

The full set is always written, even when a stage was never run in that session. A missing threshold is ambiguous when you come back to the file later; an unused one is not.

results

  • trajectory - one entry per frame of the whole video: {frame, cx, cy, area, valid}. A frame the tracker could not resolve is written as valid: false with cx/cy/area null. It is never interpolated and never dropped (hard rule #1), so the array index and the video's frame number always agree.
  • trialStartFrame, trackedCount, failedCount, totalFrames.
  • holeSignals.centerDensity and holeSignals.rimDensity - one array per hole, one value per frame. This is the primary nose-poke evidence: pixel change inside each hole measured against the median background, computed independently of where the tracked blob was.

overrides - the part that can't be recovered

eventOverrides is keyed "holeIndex:startFrame:endFrame", valued with what the human said it actually was ("poke", "entry", "thigmotaxis", ...). classifierOverride is the human-confirmed search strategy verdict, if one was set.

This is the most valuable content in the file - the only part that cannot be recomputed from anything else in it. Everything else here is either measured from the video or typed in once; these are judgment calls somebody made while watching the footage.

flags

cellFlags (which fields were flagged as needing review) and videoFlag (a whole-video flag with its reasons and note, or null).

What is deliberately not stored

Events, merged visits, measures, and the search-strategy verdict are all absent, and that is on purpose. Each is a cheap, pure recomputation from the trajectory + thresholds + overrides that are here, and the app already recomputes them fresh rather than trusting a cached value. Freezing them into this file would create a second source of truth that could silently drift out of sync with the code that produced it.

The practical consequence: reopening an old project file gives you the current build's analysis of the original tracking data, not a replay of an old build's conclusions.

Versioning

formatVersion is currently 2. Loading is refused in both directions:

  • Newer than this build - "saved by a newer version of the tool (format v3, this build only understands up to v2)."
  • Older than this build - also refused, with a re-track instruction. This sounds harsher than necessary, but v1 files are exactly the ones affected by the serialization bug the version bump exists to flag, and no migration path was written for them.

Version 1 → 2 was bumped the day a real bug was fixed: centerDensity and rimDensity are Float32Arrays in memory, and JSON.stringify serializes a typed array as {"0":0.1,"1":0.2,...} - an object with no .length. Every reload silently found zero events, because the qualifying-run loop's i < n never ran. They are converted to real arrays on write now, and the version number says so.

Independently of the version check, the loader also validates the shape of the hole-signal payload and refuses anything that isn't real arrays. The version bump stops that specific bug recurring; the shape check stops any future regression shaped like it, version bump or not. Both would rather refuse a file than analyse empty data.