Program Listing for File gaze2d_result.hpp#

Return to documentation for file (src/analysis/gaze2d_result.hpp)

#pragma once
#include <QRect>
#include <QString>
#include <QVector>
#include <cstdint>
#include <optional>

namespace mosaic {

/// One processed video frame's face detection + calibration-free 2D gaze
/// heuristic. Mirrors run_gaze2d.py's "frames" JSON array exactly.
/// faceDetected == false means faceBoxPx/gazeDx/gazeDy are meaningless
/// (default-constructed, not real values) — the single source of truth
/// callers must check, matching every sibling result class's own
/// "one boolean gates several parallel fields" convention.
struct Gaze2dFrame {
    int frameIndex      = 0;
    int64_t timestampMs = 0;
    bool faceDetected   = false;
    QRect faceBoxPx;
    double gazeDx = 0.0;
    double gazeDy = 0.0;
};

/// Parses a "<video_stem>.gaze2d.json" file written by
/// analysis/run_gaze2d.py into a queryable in-memory structure, for the
/// Analysis tab's 2D Gaze (calibration-free) plugin.
///
/// Usage:
/// @code
///   auto result = Gaze2dResult::load(jsonPath);
///   if (result.is_valid()) { ... }
/// @endcode
class Gaze2dResult {
   public:
    Gaze2dResult() = default;

    /// Parses jsonPath. Returns a default-constructed (is_valid() == false)
    /// result if the file is missing or malformed.
    static Gaze2dResult load(const QString& jsonPath);

    [[nodiscard]] bool is_valid() const { return valid_; }
    [[nodiscard]] const QString& source_video() const { return sourceVideo_; }
    [[nodiscard]] double min_confidence() const { return minConfidence_; }
    [[nodiscard]] const QVector<Gaze2dFrame>& frames() const { return frames_; }

    /// Precomputed by run_gaze2d.py from the raw per-frame gaze values —
    /// not recomputed here, avoiding duplicating aggregate math in two
    /// languages. std::nullopt if no frame in the file had a detected face.
    [[nodiscard]] double pct_frames_with_face() const { return pctFramesWithFace_; }
    [[nodiscard]] std::optional<double> mean_gaze_dx() const { return meanGazeDx_; }
    [[nodiscard]] std::optional<double> mean_gaze_dy() const { return meanGazeDy_; }
    [[nodiscard]] std::optional<double> pct_on_target() const { return pctOnTarget_; }

    /// Binary search by timestamp (frames() is written in chronological
    /// order by run_gaze2d.py, and defensively re-sorted here) with a
    /// before/after numerically-closer tie-break — mirrors
    /// GazeFusionResult::nearest_frame()'s exact convention. Returns
    /// nullptr only if frames() is empty.
    [[nodiscard]] const Gaze2dFrame* nearest_frame(int64_t timestampMsEstimate) const;

   private:
    bool valid_ = false;
    QString sourceVideo_;
    double minConfidence_ = 0.0;
    QVector<Gaze2dFrame> frames_;

    double pctFramesWithFace_ = 0.0;
    std::optional<double> meanGazeDx_;
    std::optional<double> meanGazeDy_;
    std::optional<double> pctOnTarget_;
};

} // namespace mosaic