Data Collection Plugin Tutorial: JSON Summary Writer¶
A worked example building a JsonSummary DataCollection plugin that writes the run's tracker metrics to a structured JSON file. Use it alongside the Writing a Plugin guide.
For tutorials on other plugin types, see: Phenomena Plugin Tutorial | Gaze Plugin Tutorial | Object Detection Plugin Tutorial
Wiring status in v1.0.0 — read this first
The DataCollectionPlugin base declares three hooks, but only one is
wired in v1.0.0:
generate_charts(output_dir, **kwargs)is invoked — duringfinalize_runwhen--chartsis enabled (mindsight/outputs/chart_output.py:188-199). This tutorial builds against it.on_frame()andon_run_complete()have zero call sites. They are documented for completeness as the intended per-frame / post-run contract, but the pipeline never calls them in 1.0.0.data_collection_registryis not wired into argparse.build_parseronly loops the gaze, object-detection, and phenomena registries (mindsight/cli_flags.py:243-252), so a DataCollection plugin'sadd_arguments()flags never register. A plugin can still activate by keyingfrom_argsoff an existing flag (e.g.--charts) or unconditionally — see CLI activation.
Net effect: a DataCollection plugin can contribute post-run chart/output
files via generate_charts, but it cannot see per-frame state and cannot own
its own CLI flag until the registry is wired in a future release.
Overview¶
DataCollectionPlugin handles custom post-run output. In v1.0.0 the working
surface is generate_charts(), which receives the run's summary data and returns
the paths it wrote. This tutorial builds JsonSummary — a plugin that dumps
every tracker's scalar metrics to a JSON file next to the charts.
Source: Plugins/DataCollection/JsonSummary/json_summary.py
File structure¶
Plugins/DataCollection/JsonSummary/
├── __init__.py # empty
└── json_summary.py # PLUGIN_CLASS = JsonSummaryPlugin
Class definition¶
import json
import os
from Plugins import DataCollectionPlugin
class JsonSummaryPlugin(DataCollectionPlugin):
name = "json_summary"
DataCollection plugins are stateless in 1.0.0 — there is no per-frame hook to
accumulate into, so there is nothing to initialise. Everything happens in
generate_charts.
The generate_charts() method (the working hook)¶
generate_charts is called once, after the run, when --charts is enabled. It
receives the output directory as its first positional argument and the run
summary as keyword arguments.
def generate_charts(self, output_dir, **kwargs):
total_frames = kwargs.get('total_frames', 0)
fps = kwargs.get('fps', 0.0)
all_trackers = kwargs.get('all_trackers', [])
pid_map = kwargs.get('pid_map')
metrics = []
for tracker in all_trackers:
if hasattr(tracker, 'summary_metrics'):
metrics.extend(
tracker.summary_metrics(total_frames, fps, pid_map=pid_map))
path = os.path.join(output_dir, "run_summary.json")
with open(path, 'w') as fh:
json.dump({
'total_frames': total_frames,
'fps': fps,
'metrics': metrics,
}, fh, indent=2)
return [path]
What generate_charts() receives¶
The exact call is in chart_output.generate_run_charts:
plugin.generate_charts(str(chart_dir),
total_frames=total_frames,
fps=fps,
all_trackers=all_trackers,
pid_map=pid_map)
| Keyword | Type | Description |
|---|---|---|
output_dir (positional) |
str |
The resolved chart directory. |
total_frames |
int |
Total frames processed. |
fps |
float |
Source frame rate (for seconds conversions). |
all_trackers |
list |
Every active phenomena tracker instance. |
pid_map |
dict or None |
Face track ID to display-label mapping. |
Reach the phenomena metrics through each tracker's summary_metrics() hook (see
Plugin Base Classes) — that is the same
tidy data the built-in {stem}_summary.csv is built from. Exceptions raised here
are caught and downgraded to a RuntimeWarning, so a broken chart plugin never
crashes the run.
Return value¶
Return a list of file paths you created (for logging). Return [] if you
wrote nothing.
CLI activation¶
Because add_arguments() is not wired for this registry (see the warning above),
the plugin cannot register its own flag in 1.0.0. Activate it by keying
from_args off an existing flag — --charts is the natural choice, since that is
the flag that gates generate_charts being called at all:
@classmethod
def from_args(cls, args):
# No dedicated flag yet (registry not wired into argparse). Activate
# whenever charts are being generated.
return cls() if getattr(args, "charts", None) else None
When from_args returns None, the plugin is not built. build_data_plugins
(mindsight/factory.py:76-88) instantiates whichever plugins activate and seeds
ctx['data_plugins'], which finalize_run consumes.
You may still define add_arguments for forward compatibility — it is harmless,
it simply has no effect until the registry is wired in a later release.
Running it¶
--charts both enables chart generation and (via the from_args above) activates
JsonSummary. The plugin writes charts/run_summary.json alongside the built-in
time-series charts.
Example output¶
{
"total_frames": 3600,
"fps": 30.0,
"metrics": [
{"phenomenon": "joint_attention", "participant": "all", "partner": "",
"object": "", "metric": "frames_active", "value": 143},
{"phenomenon": "gaze_following", "participant": "P0", "partner": "P1",
"object": "", "metric": "event_count", "value": 6}
]
}
The spec-only hooks (on_frame / on_run_complete)¶
The base class also declares a per-frame and a post-run hook. They are the intended contract for stateful, per-frame collection, but the pipeline does not call them in v1.0.0 — do not build a plugin that depends on them yet.
def on_frame(self, **kwargs) -> None:
"""SPEC ONLY — not called in 1.0.0.
Intended per-frame hook. Common kwargs: frame_no, persons_gaze,
face_bboxes, hit_events, face_track_ids, hits, objects, confirmed_objs."""
def on_run_complete(self, **kwargs) -> None:
"""SPEC ONLY — not called in 1.0.0.
Intended post-run hook. Common kwargs: total_frames, joint_frames,
confirmed_frames, total_hits, look_counts, source, all_trackers."""
If you need per-frame gaze events today, write a PhenomenaPlugin instead —
its update(**kwargs) hook is called every frame and its summary_tables() hook
writes a real per-event stream CSV. See the
Phenomena Plugin Tutorial.
Complete code¶
import json
import os
from Plugins import DataCollectionPlugin
class JsonSummaryPlugin(DataCollectionPlugin):
"""Dump every tracker's scalar metrics to a JSON file (post-run)."""
name = "json_summary"
def generate_charts(self, output_dir, **kwargs):
total_frames = kwargs.get('total_frames', 0)
fps = kwargs.get('fps', 0.0)
all_trackers = kwargs.get('all_trackers', [])
pid_map = kwargs.get('pid_map')
metrics = []
for tracker in all_trackers:
if hasattr(tracker, 'summary_metrics'):
metrics.extend(
tracker.summary_metrics(total_frames, fps, pid_map=pid_map))
path = os.path.join(output_dir, "run_summary.json")
with open(path, 'w') as fh:
json.dump({
'total_frames': total_frames,
'fps': fps,
'metrics': metrics,
}, fh, indent=2)
return [path]
@classmethod
def from_args(cls, args):
# No dedicated flag yet (registry not wired into argparse).
return cls() if getattr(args, "charts", None) else None
PLUGIN_CLASS = JsonSummaryPlugin