Program Listing for File expression_result.hpp#
↰ Return to documentation for file (src/analysis/expression_result.hpp)
#pragma once
#include <QRectF>
#include <QString>
#include <QStringList>
#include <QVector>
#include <cstdint>
namespace mosaic {
/// One detected face's blendshape scores and classified expression within a
/// single analysed frame. Mirrors run_expression.py's _frame_to_dict()
/// "subjects" entries exactly.
struct ExpressionSubject {
int subjectId = -1;
double confidence = 0.0;
QRectF bbox; ///< bbox_xyxy.
QVector<double> blendshapeScores; ///< Parallel to ExpressionResult::blendshape_names().
QString dominantExpression;
double dominantScore = 0.0;
/// FACS Action Unit intensities (py-feat backend only), each in [0,1],
/// parallel to ExpressionResult::au_names(). Empty for the heuristic/
/// FER+ backends and for any session analyzed before this field existed.
QVector<double> actionUnits;
};
/// One analysed frame. Mirrors run_expression.py's per-frame JSON object.
struct ExpressionFrame {
int frameIndex = 0;
int64_t timestampNs = 0;
int cameraIndex = 0;
QVector<ExpressionSubject> subjects;
};
/// Parses a "<name>.expression.json" file written by
/// analysis/run_expression.py into a queryable in-memory structure, for the
/// Analysis tab's Facial Expression plugin (bbox+label overlay during
/// playback and a per-blendshape score-over-time plot). Mirrors
/// PoseAnalysisResult (src/analysis/pose_analysis_result.hpp) closely — same
/// load()/is_valid()/nearest_frame() shape.
///
/// Usage:
/// @code
/// auto result = ExpressionResult::load(jsonPath);
/// if (result.is_valid()) { ... }
/// @endcode
class ExpressionResult {
public:
ExpressionResult() = default;
/// Parses jsonPath. Returns a default-constructed (is_valid() == false)
/// result if the file is missing or malformed.
static ExpressionResult 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]] const QStringList& blendshape_names() const { return blendshapeNames_; }
[[nodiscard]] const QVector<ExpressionFrame>& frames() const { return frames_; }
/// Action Unit names (py-feat backend only) — empty for the heuristic/
/// FER+ backends and for older files with no "au_names" field at all.
[[nodiscard]] const QStringList& au_names() const { return auNames_; }
[[nodiscard]] bool has_action_units() const { return !auNames_.isEmpty(); }
/// True if at least one frame has at least one detected face. Distinct
/// from is_valid() (which only means "the JSON parsed"): a result can be
/// valid but empty if no face was detected in this camera's footage for
/// the whole session — mirrors PoseAnalysisResult::has_any_detections().
[[nodiscard]] bool has_any_detections() const {
for (const auto& f : frames_) {
if (!f.subjects.isEmpty()) {
return true;
}
}
return false;
}
/// Nearest-frame lookup by frame_index estimate, identical body to
/// PoseAnalysisResult::nearest_frame() — frames() is stored in the
/// ascending frame_index order run_expression.py writes them in, so this
/// is a binary search, not a linear scan. Returns nullptr if there are
/// no frames.
[[nodiscard]] const ExpressionFrame* nearest_frame(int frameIndexEstimate) const;
private:
bool valid_ = false;
QString sourceVideo_;
QString backend_;
QStringList blendshapeNames_;
QStringList auNames_;
QVector<ExpressionFrame> frames_;
};
} // namespace mosaic