Program Listing for File gaze_fusion_result.hpp#
↰ Return to documentation for file (src/analysis/gaze_fusion_result.hpp)
#pragma once
#include <QRectF>
#include <QString>
#include <QStringList>
#include <QVector>
#include <array>
#include <cstdint>
namespace mosaic {
/// Plain 3-vector (room-space mm) — std::array rather than QVector3D
/// deliberately, matching CalibrationData's own std::array<double,N>
/// convention for geometric data (src/core/settings.hpp) and keeping this
/// class free of the QtGui dependency QVector3D would pull in.
using Vec3 = std::array<double, 3>;
/// One contributing camera's raw gaze data within a single fused frame.
/// Mirrors run_gaze_fusion.py's per-frame "per_camera" entries exactly.
struct GazeFusionCamera {
int cameraIndex = -1;
QRectF faceBoxPx; ///< face_box_px, that camera's video pixel space.
double gazeDx = 0.0, gazeDy = 0.0;
Vec3 originRoom = {0, 0, 0};
Vec3 directionRoom = {0, 0, 0};
double confidence = 0.0;
};
/// Static per-camera room position (extrinsic_rt's translation column),
/// written once per file — used by the room-view widget's camera icons.
struct GazeFusionRoomCamera {
int index = -1;
Vec3 positionRoom = {0, 0, 0};
};
/// One fused master-tick. Mirrors run_gaze_fusion.py's per-frame JSON object.
struct GazeFusionFrame {
int64_t tick = 0;
int64_t timestampNs = 0;
int numCameras = 0;
bool isTriangulated = false;
Vec3 fusedOriginRoom = {0, 0, 0};
Vec3 fusedDirectionRoom = {0, 0, 0};
double residualRmsMm = -1.0; ///< -1 = not applicable (numCameras < 2).
bool hasTarget = false;
Vec3 targetPointRoom = {0, 0, 0};
QVector<GazeFusionCamera> perCamera;
};
/// Parses a session-root "gaze_fusion.json" file written by
/// analysis/run_gaze_fusion.py into a queryable in-memory structure, for the
/// Analysis tab's Multi-Camera Gaze Fusion plugin (per-camera bbox+arrow
/// overlay during playback, plus a 3D room-view widget). Mirrors
/// ExpressionResult (src/analysis/expression_result.hpp) closely — same
/// load()/is_valid() shape — with one deliberate divergence: frames are
/// keyed on the shared master tick/timestamp (not any single video's
/// frame_index, since fusion is inherently cross-camera), so nearest-frame
/// lookup is by timestamp, not frame index.
///
/// Usage:
/// @code
/// auto result = GazeFusionResult::load(jsonPath);
/// if (result.is_valid()) { ... }
/// @endcode
class GazeFusionResult {
public:
GazeFusionResult() = default;
/// Parses jsonPath. Returns a default-constructed (is_valid() == false)
/// result if the file is missing or malformed.
static GazeFusionResult load(const QString& jsonPath);
[[nodiscard]] bool is_valid() const { return valid_; }
[[nodiscard]] const QStringList& source_videos() const { return sourceVideos_; }
[[nodiscard]] const QVector<GazeFusionRoomCamera>& cameras() const { return cameras_; }
[[nodiscard]] bool plane_defined() const { return planeDefined_; }
[[nodiscard]] Vec3 plane_point() const { return planePoint_; }
[[nodiscard]] Vec3 plane_normal() const { return planeNormal_; }
[[nodiscard]] double master_fps() const { return masterFps_; }
[[nodiscard]] const QVector<GazeFusionFrame>& frames() const { return frames_; }
/// Nearest-frame lookup by timestamp (ns) estimate — binary search,
/// since frames() is stored in the ascending timestampNs order
/// run_gaze_fusion.py writes ascending master ticks in. Returns
/// nullptr if there are no frames.
[[nodiscard]] const GazeFusionFrame* nearest_frame(int64_t timestampNsEstimate) const;
private:
bool valid_ = false;
QStringList sourceVideos_;
QVector<GazeFusionRoomCamera> cameras_;
bool planeDefined_ = false;
Vec3 planePoint_ = {0, 0, 0};
Vec3 planeNormal_ = {0, 0, 1};
double masterFps_ = 25.0;
QVector<GazeFusionFrame> frames_;
};
} // namespace mosaic