Program Listing for File pose_analysis_result.hpp#
↰ Return to documentation for file (src/analysis/pose_analysis_result.hpp)
#pragma once
#include <QPair>
#include <QPointF>
#include <QRectF>
#include <QString>
#include <QStringList>
#include <QVector>
#include <cstdint>
namespace mosaic {
/// One detected subject's keypoints within a single analysed frame.
/// Mirrors run_pose.py's _result_to_dict() "subjects" entries exactly.
struct PoseSubject {
int subjectId = -1;
double confidence = 0.0;
QVector<QPointF> keypoints; ///< Pixel coordinates, one per keypoint name.
QVector<double> visibilities; ///< 0-1, same order as keypoints.
QRectF bbox; ///< bbox_xyxy.
};
/// Shared visibility threshold for "trust this keypoint" decisions — used
/// both to decide whether to draw a keypoint (SkeletonOverlayW::paintEvent,
/// src/ui/analysis/pose_overlay_player_w.cpp) and whether to include it in
/// derived kinematics (pose_kinematics.cpp). Kept in one place so the two
/// can't silently drift to different thresholds. A missing visibilities
/// entry (index >= visibilities.size()) defaults to "visible" — matches
/// keypoints[] always being written 1:1 with visibilities[] by run_pose.py,
/// so this only matters for a malformed/truncated file, where treating an
/// unknown entry as visible is the same permissive default this codebase
/// already used before this helper existed.
inline bool is_keypoint_visible(const PoseSubject& subject, int keypointIndex) {
return subject.visibilities.value(keypointIndex, 1.0) >= 0.1;
}
/// One analysed frame. Mirrors run_pose.py's per-frame JSON object.
struct PoseFrame {
int frameIndex = 0;
int64_t timestampNs = 0;
int cameraIndex = 0;
QVector<PoseSubject> subjects;
};
/// Parses a .pose.json file written by analysis/run_pose.py (--out-format json)
/// into a queryable in-memory structure, for drawing a live overlay during
/// playback and plotting per-keypoint metrics over time.
///
/// Usage:
/// @code
/// auto result = PoseAnalysisResult::load(jsonPath);
/// if (result.is_valid()) { ... }
/// @endcode
class PoseAnalysisResult {
public:
PoseAnalysisResult() = default;
/// Parses jsonPath. Returns a default-constructed (is_valid() == false)
/// result if the file is missing or malformed.
static PoseAnalysisResult load(const QString& jsonPath);
[[nodiscard]] bool is_valid() const { return valid_; }
/// True if at least one frame has at least one detected subject.
/// Distinct from is_valid() (which only means "the JSON parsed"): a
/// result can be valid but empty if the pose model detected nobody in
/// this camera's footage for the whole session — callers use this to
/// tell that case apart from "hasn't been analyzed yet".
[[nodiscard]] bool has_any_detections() const {
for (const auto& f : frames_) {
if (!f.subjects.isEmpty()) {
return true;
}
}
return false;
}
[[nodiscard]] const QString& source_video() const { return sourceVideo_; }
/// The model id that produced this result (e.g. "yolov8n-pose.pt"), from
/// the JSON's top-level "model" field — empty for files written before
/// this field existed (older, un-namespaced .pose.json files still load
/// fine, they just report no model).
[[nodiscard]] const QString& model() const { return model_; }
[[nodiscard]] const QStringList& keypoint_names() const { return keypointNames_; }
[[nodiscard]] const QVector<QPair<int, int>>& skeleton_edges() const { return skeletonEdges_; }
[[nodiscard]] const QVector<PoseFrame>& frames() const { return frames_; }
/// Nearest-frame lookup by frame_index estimate (e.g. derived from a
/// video playback position). frames() is stored in the ascending
/// frame_index order run_pose.py writes them in, so this is a binary
/// search, not a linear scan. Returns nullptr if there are no frames.
[[nodiscard]] const PoseFrame* nearest_frame(int frameIndexEstimate) const;
private:
bool valid_ = false;
QString sourceVideo_;
QString model_;
QStringList keypointNames_;
QVector<QPair<int, int>> skeletonEdges_;
QVector<PoseFrame> frames_;
};
} // namespace mosaic