Program Listing for File rppg_result.hpp#

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

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

namespace mosaic {

/// One HR-analysis window's estimate. Mirrors run_rppg.py's "windows" JSON
/// array exactly. bpm/smoothedBpm/snrDb are NaN when this window had no
/// reliable estimate ("no_face"/insufficient-data — never fabricated, see
/// run_rppg.py's MIN_VALID_FRACTION gate).
struct RppgWindow {
    int64_t startMs           = 0;
    int64_t endMs             = 0;
    double bpm                = std::numeric_limits<double>::quiet_NaN();
    double smoothedBpm        = std::numeric_limits<double>::quiet_NaN();
    double snrDb              = std::numeric_limits<double>::quiet_NaN();
    double validFrameFraction = 0.0;
};

/// One processed video frame's face-ROI detection, for the debug overlay
/// (PoseOverlayPlayerW::set_rppg_result()) — a separate, finer granularity
/// from RppgWindow's HR-analysis windows. faceDetected == false means
/// roiBboxPx is meaningless (default-constructed, not a real box).
struct RppgFrame {
    int frameIndex      = 0;
    int64_t timestampMs = 0;
    bool faceDetected   = false;
    QRect roiBboxPx;
};

/// Parses a "<video_stem>.<backend>.rppg.json" file written by
/// analysis/run_rppg.py into a queryable in-memory structure, for the
/// Analysis tab's Remote Heart Rate (rPPG) plugin.
///
/// @warning EXPERIMENTAL — this is a research-grade heart-rate estimate
/// only, not a medical device and not clinically validated (see the
/// plugin's own persistent UI disclaimer banner).
///
/// Usage:
/// @code
///   auto result = RppgResult::load(jsonPath);
///   if (result.is_valid()) { ... }
/// @endcode
class RppgResult {
   public:
    RppgResult() = default;

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

    [[nodiscard]] bool is_valid() const { return valid_; }
    [[nodiscard]] const QString& source_video() const { return sourceVideo_; }
    [[nodiscard]] const QString& backend() const { return backend_; }
    [[nodiscard]] double window_sec() const { return windowSec_; }
    [[nodiscard]] double hop_sec() const { return hopSec_; }
    [[nodiscard]] const QVector<RppgWindow>& windows() const { return windows_; }
    [[nodiscard]] const QVector<RppgFrame>& frames() const { return frames_; }

    /// Precomputed by run_rppg.py from the raw (non-smoothed) per-window
    /// bpm values — not recomputed here, avoiding duplicating percentile/
    /// mean math in two languages. std::nullopt if no window in the file
    /// had any reliable estimate.
    [[nodiscard]] std::optional<double> mean_bpm() const { return meanBpm_; }
    [[nodiscard]] std::optional<double> median_bpm() const { return medianBpm_; }
    [[nodiscard]] std::optional<double> min_bpm() const { return minBpm_; }
    [[nodiscard]] std::optional<double> max_bpm() const { return maxBpm_; }
    [[nodiscard]] double pct_windows_good() const { return pctWindowsGood_; }

    /// Binary search by start time (windows()/frames() are both written in
    /// chronological order by run_rppg.py) with a before/after
    /// numerically-closer tie-break — mirrors GazeFusionResult::
    /// nearest_frame()'s exact convention. Returns nullptr only if
    /// windows()/frames() is empty.
    [[nodiscard]] const RppgWindow* nearest_window(int64_t timestampMsEstimate) const;
    [[nodiscard]] const RppgFrame* nearest_frame(int64_t timestampMsEstimate) const;

   private:
    bool valid_ = false;
    QString sourceVideo_;
    QString backend_;
    double windowSec_ = 0.0;
    double hopSec_    = 0.0;
    QVector<RppgWindow> windows_;
    QVector<RppgFrame> frames_;

    std::optional<double> meanBpm_;
    std::optional<double> medianBpm_;
    std::optional<double> minBpm_;
    std::optional<double> maxBpm_;
    double pctWindowsGood_ = 0.0;
};

} // namespace mosaic