Program Listing for File trigger_frame_map.hpp#
↰ Return to documentation for file (src/analysis/trigger_frame_map.hpp)
#pragma once
#include <QString>
#include <QStringList>
#include <QVector>
#include <cstdint>
namespace mosaic {
// One camera's nearest-frame result for a single trigger event.
struct TriggerFrameHit {
int cameraIndex = -1;
int frameId = -1; // -1 = no timestamps_camN.csv data for this camera
double deltaMs = 0.0; // signed: (frame.elapsedNs - trigger.elapsedNs)/1e6
int64_t videoPositionMs = -1; // seek() position in that camera's own mp4 PTS domain; -1 = n/a
};
// One trigger.csv row plus its resolved nearest frame in every camera.
struct TriggerFrameRow {
int rowIndex = 0; // 0-based, order in trigger.csv (excluding header)
int64_t elapsedNs = 0; // raw, shared elapsed_ns() origin — see TriggerRecorder
double elapsedMs = 0.0; // recording-relative, as originally logged; display only
QString wallClock;
QString source;
QString label;
double value = 0.0;
QVector<TriggerFrameHit> frames; // one entry per camera, index == cameraIndex
};
struct TriggerFrameCamera {
int index = 0;
QString videoFile; // relative to session dir, e.g. "video/video_0.mp4"
int framesCaptured = 0;
};
// Resolves every trigger event in a recorded session's trigger.csv to the
// nearest ACTUALLY-CAPTURED frame in every camera — a direct per-camera
// nearest-neighbour lookup against each timestamps_camN.csv, not a resample
// onto SyncManifest's cross-camera uniform master-tick grid (which adds up
// to +-half a tick of quantization on top of native jitter — a different
// problem, "synchronized uniform playback," not "what did camera C actually
// capture nearest instant T"). Pure, fast, deterministic; computed
// synchronously in C++ (not an AnalysisManager subprocess job), mirroring
// how SyncManifest's own generate() is already synchronous C++ despite
// sitting beside the ML-backed analysis plugins.
//
// Requires trigger.csv to have the elapsed_ns column (added alongside this
// class — see TriggerRecorder). Sessions recorded before that fix predate
// the column entirely; generate() returns is_valid() == false for them
// rather than silently reconstructing a value from elapsed_ms against an
// unknown, undiscoverable old TriggerRecorder::startNs offset.
//
// Usage:
// auto m = TriggerFrameMap::generate(sessionPath);
// m.save(sessionPath);
// ...
// auto m2 = TriggerFrameMap::load(sessionPath);
// player->seek(m2.row(i).frames[camIdx].videoPositionMs);
class TriggerFrameMap {
public:
TriggerFrameMap() = default;
// ── Factory ───────────────────────────────────────────────────────────
static TriggerFrameMap generate(const QString& sessionPath);
// Load existing trigger_frame_map.json from sessionPath.
static TriggerFrameMap load(const QString& sessionPath);
// ── Persistence ───────────────────────────────────────────────────────
bool save(const QString& sessionPath) const;
// Flat one-row-per-trigger CSV at an arbitrary path (QFileDialog save-as
// target) — for MNE-Python / external consumption.
bool export_csv(const QString& csvPath) const;
// ── Validity ──────────────────────────────────────────────────────────
[[nodiscard]] bool is_valid() const;
// ── Queries ───────────────────────────────────────────────────────────
[[nodiscard]] int camera_count() const;
[[nodiscard]] int trigger_count() const;
[[nodiscard]] const TriggerFrameCamera& camera_info(int idx) const;
[[nodiscard]] const TriggerFrameRow& row(int idx) const;
[[nodiscard]] const QVector<TriggerFrameRow>& rows() const;
private:
bool valid_ = false;
QString generatedAt_;
QVector<TriggerFrameCamera> cameras_;
QVector<TriggerFrameRow> rows_;
struct FrameTs {
int frameId = 0;
int64_t elapsedNs = 0;
};
static QVector<FrameTs> read_timestamps(const QString& csvPath);
struct RawTrigger {
int64_t elapsedNs = 0;
double elapsedMs = 0.0;
QString wallClock, source, label;
double value = 0.0;
};
// Quote-aware line split — trigger.csv's label field is "..."-quoted
// with doubled internal quotes and may itself contain commas (e.g. a
// free-text stimulus/event marker); a naive split(',') is not safe here.
static QStringList split_csv_line(const QString& line);
// Column-order-independent (looks up each column by header name).
// Sets hasElapsedNsColumn to false (and returns an empty vector) if the
// file is missing, empty, or predates the elapsed_ns column.
static QVector<RawTrigger> read_trigger_csv(const QString& csvPath, bool& hasElapsedNsColumn);
};
} // namespace mosaic