128 lines
5.3 KiB
C++
128 lines
5.3 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. Raw frames
|
|
// (BayerRG8, BayerRG12p) are de-Bayered first and saved at their own bit depth,
|
|
// so a 12-bit frame becomes a 12-bit file. Runs background workers 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;
|
|
// De-Bayering for raw frames: edge-aware (sharper, ~2x the time, still
|
|
// ~30 ms for 16 MP) or bilinear.
|
|
bool edge_aware = true;
|
|
};
|
|
|
|
struct Params {
|
|
std::string output_dir;
|
|
std::vector<CameraOutput> cameras; // by camera index, in open order
|
|
std::string tower;
|
|
// How saving shares the CPU (hardware.ini): frames encoded at once, and
|
|
// libjxl threads per frame. Measured: one worker x 4 threads on the IOTA,
|
|
// two x 2 on the 3 Delta (docs/raw12-pipeline-measurements.md).
|
|
int workers = 1;
|
|
int jxl_threads = 4;
|
|
// Frames waiting or being saved beyond which canAccept() says no, so the
|
|
// scan holds instead of piling frames up in memory (24 MB raw each).
|
|
int queue_max = 4;
|
|
};
|
|
|
|
// 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);
|
|
|
|
// Is there room for another frame? False once queue_max frames are waiting
|
|
// or being saved. The scheduler asks before moving to the next waypoint, so
|
|
// a scan that outruns the save rate pauses rather than losing pictures.
|
|
// submit() itself never refuses: a frame already captured is always saved.
|
|
bool canAccept() const;
|
|
// Frames waiting or being saved right now.
|
|
size_t pending() const;
|
|
|
|
// 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);
|
|
// Encode and write one image; false (logged) on failure.
|
|
bool save(const Frame& frame, const CameraOutput& out, const std::string& file);
|
|
// 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_;
|
|
mutable std::mutex mutex_;
|
|
std::condition_variable cv_;
|
|
std::atomic<bool> running_{false};
|
|
std::vector<std::thread> workers_;
|
|
size_t in_flight_ = 0; // frames a worker is saving (under mutex_)
|
|
|
|
mutable std::mutex last_event_mutex_;
|
|
std::optional<CamEvent> last_event_;
|
|
std::atomic<long long> saved_{0}; // images successfully written this session
|
|
};
|
|
|
|
} // namespace fgc
|