fwt_software/include/fgc/ImagePipeline.h

114 lines
4.5 KiB
C++

#pragma once
#include "fgc/ICameraSource.h"
#include "fgc/IControlChannel.h"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <thread>
#include <vector>
namespace fgc {
// Gimbal orientation in degrees, stamped onto each CamEvent.
struct Orientation {
float yaw_deg = 0.f;
float pitch_deg = 0.f;
};
// Consumes captured frames and turns them into stored artifacts: rotate by
// Camera.rotation_deg, encode to JPEG XL, write to
// <output_dir>/<label>/<timestamp>.jxl, and publish a CamEvent. Runs a
// background worker so encoding never blocks acquisition. Split out of the old
// VimbaHandler::SaveImage so any camera source (real or mock) can feed it.
class ImagePipeline {
public:
// Per-camera output settings. Label, rotation and encoding are all properties
// of the individual camera now, so they travel together and are indexed by
// Frame::cam_id rather than assumed uniform across the rig.
struct CameraOutput {
std::string label; // output subdir + CamEvent "cam"
int rotation_deg = 0; // clockwise; 0/90/180/270
double jxl_distance = 0.8; // 0 = lossless
int jxl_effort = 4;
};
struct Params {
std::string output_dir;
std::vector<CameraOutput> cameras; // by camera index, in open order
std::string tower;
bool demo = false; // copy demo_image instead of encoding
std::string demo_image = "test_smoke.jxl";
bool display = false; // show an OpenCV preview window
};
// orientation: returns the current gimbal yaw+pitch (degrees) for CamEvent.
// Called from submit(), i.e. on the camera's delivery thread at the moment the
// frame arrives - see submit() for why it must not be sampled any later.
ImagePipeline(IControlChannel& channel, std::function<Orientation()> orientation, Params params);
~ImagePipeline();
void start();
void stop();
// Enqueue a frame for processing (thread-safe; copies the frame).
//
// The gimbal orientation is sampled HERE, not in the worker, and travels with
// the frame. Sampling it after encoding would attribute to the image whatever
// direction the gimbal had reached by the time the encode finished - which is
// only harmless while captures happen at standstill and nothing overlaps. The
// moment encoding runs while the gimbal is already slewing to the next
// waypoint, or the queue backs up, the published heading and pitch belong to a
// different pose than the pixels.
void submit(const Frame& frame);
// Runtime-adjustable encoder settings (console "set camera ..." commands).
// These apply to every camera: they exist for quick experiments, and asking
// which camera on every keystroke would make that worse, not better.
void setDistance(double d) { for (auto& c : params_.cameras) c.jxl_distance = d; }
void setEffort(int e) { for (auto& c : params_.cameras) c.jxl_effort = e; }
void setDisplay(bool on) { params_.display = on; }
// Last CamEvent published (camera label, heading/pitch, timestamp), for the
// UI's camera panel. Thread-safe; nullopt until the first capture is saved.
std::optional<CamEvent> lastEvent() const;
// Number of images successfully written this session (for the camera view).
long long imagesSaved() const { return saved_.load(); }
private:
// A frame and the pose it was taken at, captured together at submit() time so
// encoding latency and queue depth cannot separate them.
struct QueuedFrame {
Frame frame;
Orientation orientation;
};
void run();
void process(const QueuedFrame& queued);
// Output settings for a camera index, or sane defaults if the index is out of
// range (a source delivering more cameras than were configured).
const CameraOutput& outputFor(int cam_id) const;
IControlChannel& channel_;
std::function<Orientation()> orientation_;
Params params_;
std::queue<QueuedFrame> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::atomic<bool> running_{false};
std::thread worker_;
mutable std::mutex last_event_mutex_;
std::optional<CamEvent> last_event_;
std::atomic<long long> saved_{0}; // images successfully written this session
};
} // namespace fgc