263 lines
12 KiB
C++
263 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "fgc/ICameraSource.h"
|
|
#include "fgc/Logger.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
namespace fgc {
|
|
|
|
// Synthetic camera for development without hardware. On trigger() it generates
|
|
// a small gradient frame (varying with time so successive captures differ) and
|
|
// delivers it to the frame callback - as RGB8 by default, as the raw mosaic
|
|
// (BayerRG8, BayerRG12p) a raw-capture profile would deliver, or as a thermal
|
|
// camera's counts (Mono16: a 15..25 degC gradient with one 300 degC spot that
|
|
// moves with time, profile 0). setProfile() only
|
|
// records the profile name - there is no sensor to configure - which is enough
|
|
// to exercise profile switching end to end with --mock-camera.
|
|
class MockCameraSource : public ICameraSource {
|
|
public:
|
|
explicit MockCameraSource(int count = 1, uint32_t width = 640, uint32_t height = 480,
|
|
PixelLayout layout = PixelLayout::RGB8)
|
|
: count_(count), width_(width), height_(height), layout_(layout) {}
|
|
|
|
void open() override { LOG_INFO << "[mock] camera opened (" << count_ << ")"; }
|
|
void close() override {}
|
|
void start() override { started_ = true; LOG_INFO << "[mock] camera acquisition started"; }
|
|
void stop() override { started_ = false; }
|
|
|
|
bool trigger() override {
|
|
if (!callback_) return false;
|
|
LOG_TRACE_CAT(LogCat::Camera) << "TX trigger cam0 (mock)";
|
|
Frame f = synth();
|
|
LOG_TRACE_CAT(LogCat::Camera)
|
|
<< "RX frame cam0 " << width_ << 'x' << height_ << ' ' << f.data.size() << "B (mock)";
|
|
++frames_;
|
|
callback_(f);
|
|
return true;
|
|
}
|
|
|
|
void setFrameCallback(FrameCallback cb) override { callback_ = std::move(cb); }
|
|
|
|
// A free-running stream whose auto-exposure has settled: every call to
|
|
// framesSince() "delivers" one more frame at the camera's target brightness
|
|
// (50% of full scale), so the keep rule settles after a few scheduler ticks.
|
|
uint64_t frameSeq() override { return stream_seq_; }
|
|
std::vector<FrameStats> framesSince(uint64_t after) override {
|
|
++stream_seq_;
|
|
std::vector<FrameStats> out;
|
|
for (uint64_t s = after + 1; s <= stream_seq_; ++s)
|
|
out.push_back(FrameStats{s, 127.5, 10000.0, 6.0});
|
|
return out;
|
|
}
|
|
bool readFeature(int camera_index, const std::string& name, double& value) override {
|
|
if (camera_index < 0 || camera_index >= count_) return false;
|
|
if (layout_ == PixelLayout::Mono16) { // a thermal camera, like TarisCameraSource
|
|
if (name == "CalibProfile") { value = 0.0; return true; }
|
|
if (name == "KelvinPerCount") { value = 0.01; return true; }
|
|
return false;
|
|
}
|
|
if (name == "IntensityControllerTarget") { value = 50.0; return true; }
|
|
if (name == "IntensityControllerTolerance") { value = 5.0; return true; }
|
|
if (name == "ExposureAutoMin") { value = 82.0; return true; }
|
|
if (name == "ExposureAutoMax") { value = 20000.0; return true; }
|
|
if (name == "GainAutoMin") { value = 0.0; return true; }
|
|
if (name == "GainAutoMax") { value = 24.0; return true; }
|
|
return false;
|
|
}
|
|
int cameraCount() const override { return count_; }
|
|
|
|
bool setProfile(int camera_index, const CameraProfile& profile) override {
|
|
profile_ = profile.name;
|
|
LOG_INFO << "[mock] camera " << camera_index << " profile -> " << profile_;
|
|
return true;
|
|
}
|
|
|
|
// `test camera`. The mock behaves like the real one: minimum exposure and gain
|
|
// drive it to black, maximum gain brightens it, so the self-test passes against
|
|
// it - and a fault can be switched on to prove the test fails when it should.
|
|
enum class Fault { None, NoFrames, IgnoresSettings };
|
|
void setFault(Fault f) { fault_ = f; }
|
|
|
|
bool grabFrame(int camera_index, Frame& out, int timeout_ms) override {
|
|
if (!started_ || camera_index < 0 || camera_index >= count_) return false;
|
|
if (fault_ == Fault::NoFrames) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(timeout_ms, 20)));
|
|
return false;
|
|
}
|
|
out = synth();
|
|
out.cam_id = camera_index;
|
|
++frames_;
|
|
return true;
|
|
}
|
|
bool setFeature(int camera_index, const std::string& name, const std::string& value) override {
|
|
if (camera_index < 0 || camera_index >= count_) return false;
|
|
features_[name] = value;
|
|
return true;
|
|
}
|
|
bool featureRange(int camera_index, const std::string& name, double& min,
|
|
double& max) override {
|
|
if (camera_index < 0 || camera_index >= count_) return false;
|
|
if (layout_ == PixelLayout::Mono16) return false; // a thermal camera: no exposure
|
|
if (name == "ExposureTime") { min = 66.357; max = 9999971.0; return true; }
|
|
if (name == "Gain") { min = 0.0; max = 48.0; return true; }
|
|
if (name == "BlackLevel") { min = 0.0; max = 4095.0; return true; }
|
|
return false;
|
|
}
|
|
bool restoreProfile(int camera_index) override {
|
|
if (camera_index < 0 || camera_index >= count_) return false;
|
|
features_.clear();
|
|
return true;
|
|
}
|
|
bool resetCamera(int camera_index) override {
|
|
if (camera_index < 0 || camera_index >= count_) return false;
|
|
LOG_INFO << "[mock] camera " << camera_index << " reset";
|
|
features_.clear();
|
|
fault_ = Fault::None; // a reset is what clears a stuck camera
|
|
return true;
|
|
}
|
|
|
|
// Synthetic device info so the expanded camera view renders (and is testable)
|
|
// without hardware. Non-hardware fields (temperature, exposure...) stay at 0.
|
|
std::vector<CameraDeviceInfo> deviceInfo() override {
|
|
std::vector<CameraDeviceInfo> out;
|
|
for (int i = 0; i < count_; ++i) {
|
|
CameraDeviceInfo d;
|
|
d.id = "MOCK" + std::to_string(i);
|
|
d.model = "MockCamera";
|
|
d.serial = "SIM-0000";
|
|
d.streaming = started_;
|
|
d.width = width_;
|
|
d.height = height_;
|
|
d.payload_bytes = static_cast<long long>(layoutFrameBytes(layout_, width_, height_));
|
|
d.actual_fps = 1.0;
|
|
d.frames_delivered = (i == 0) ? frames_ : 0; // only cam0 produces frames
|
|
out.push_back(std::move(d));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
// A mid-grey gradient frame. The pattern varies with time so successive
|
|
// captures differ.
|
|
Frame synth() const {
|
|
if (layout_ == PixelLayout::Mono16) return thermal();
|
|
Frame f;
|
|
f.width = width_;
|
|
f.height = height_;
|
|
f.channels = 3;
|
|
f.cam_id = 0;
|
|
f.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch())
|
|
.count();
|
|
f.data.resize(static_cast<size_t>(width_) * height_ * 3);
|
|
|
|
// The scene's mid grey, unless the self-test has forced an extreme.
|
|
double level = 110.0;
|
|
auto num = [&](const char* k, double dflt) {
|
|
auto it = features_.find(k);
|
|
return it == features_.end() ? dflt : std::stod(it->second);
|
|
};
|
|
if (fault_ != Fault::IgnoresSettings && features_.count("ExposureTime")) {
|
|
if (num("ExposureTime", 1e9) < 100.0 && num("Gain", 99.0) <= 0.0)
|
|
level = -100.0; // minimum exposure and gain: black
|
|
else if (num("Gain", 0.0) >= 48.0)
|
|
level = 180.0; // maximum gain: visibly brighter
|
|
}
|
|
|
|
const uint8_t phase = static_cast<uint8_t>(f.timestamp_ms);
|
|
for (uint32_t y = 0; y < height_; ++y) {
|
|
for (uint32_t x = 0; x < width_; ++x) {
|
|
size_t i = (static_cast<size_t>(y) * width_ + x) * 3;
|
|
// Zero-mean texture around `level`. The cast matters:
|
|
// (x+y+phase)%3 is unsigned, so subtracting 1 from the zero case
|
|
// would wrap to UINT_MAX and clip a third of the frame to white.
|
|
const double tex = 20.0 * (static_cast<int>((x + y + phase) % 3) - 1);
|
|
auto clamp8 = [](double v) {
|
|
return static_cast<uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
|
|
};
|
|
f.data[i + 0] = clamp8(level + tex);
|
|
f.data[i + 1] = clamp8(level + tex * 0.8);
|
|
f.data[i + 2] = clamp8(level + tex * 0.6);
|
|
}
|
|
}
|
|
f.layout = PixelLayout::RGB8;
|
|
if (layoutIsBayer(layout_)) f = mosaic(f);
|
|
f.brightness = sampleBrightness(f.data.data(), f.data.size(), f.layout, f.width, f.height);
|
|
return f;
|
|
}
|
|
|
|
// A thermal scene in counts at 0.01 K per count.
|
|
Frame thermal() const {
|
|
Frame f;
|
|
f.width = width_;
|
|
f.height = height_;
|
|
f.channels = 1;
|
|
f.layout = PixelLayout::Mono16;
|
|
f.cam_id = 0;
|
|
f.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch())
|
|
.count();
|
|
std::vector<uint16_t> px(static_cast<size_t>(width_) * height_);
|
|
for (uint32_t y = 0; y < height_; ++y)
|
|
for (uint32_t x = 0; x < width_; ++x) // 288.15 K at the top .. 298.15 K
|
|
px[static_cast<size_t>(y) * width_ + x] =
|
|
static_cast<uint16_t>(28815 + 1000 * y / std::max<uint32_t>(1, height_ - 1));
|
|
const uint32_t hx = static_cast<uint32_t>(f.timestamp_ms / 100) % width_;
|
|
px[static_cast<size_t>(height_ / 2) * width_ + hx] = 57315; // 300 degC
|
|
f.data.resize(px.size() * 2);
|
|
std::memcpy(f.data.data(), px.data(), f.data.size());
|
|
f.brightness = sampleBrightness(f.data.data(), f.data.size(), f.layout, f.width, f.height);
|
|
f.thermal = ThermalFrameInfo{};
|
|
return f;
|
|
}
|
|
|
|
// The RGB frame as the sensor would see it: each pixel keeps only the colour
|
|
// of its filter (BayerRG: R G / G B), 8-bit or packed 12-bit.
|
|
Frame mosaic(const Frame& rgb) const {
|
|
Frame f = rgb;
|
|
f.layout = layout_;
|
|
f.channels = 1;
|
|
const size_t n = static_cast<size_t>(width_) * height_;
|
|
std::vector<uint16_t> m(n);
|
|
for (uint32_t y = 0; y < height_; ++y)
|
|
for (uint32_t x = 0; x < width_; ++x) {
|
|
const int c = (y % 2 == 0) ? (x % 2 == 0 ? 0 : 1) : (x % 2 == 0 ? 1 : 2);
|
|
m[static_cast<size_t>(y) * width_ + x] =
|
|
rgb.data[(static_cast<size_t>(y) * width_ + x) * 3 + c];
|
|
}
|
|
f.data.assign(layoutFrameBytes(layout_, width_, height_), 0);
|
|
if (layout_ == PixelLayout::BayerRG8) {
|
|
for (size_t i = 0; i < n; ++i) f.data[i] = static_cast<uint8_t>(m[i]);
|
|
} else { // BayerRG12p: the 8-bit value << 4, packed two pixels in three bytes
|
|
for (size_t i = 0, o = 0; i < n; i += 2, o += 3) {
|
|
const uint16_t p0 = static_cast<uint16_t>(m[i] << 4);
|
|
const uint16_t p1 = i + 1 < n ? static_cast<uint16_t>(m[i + 1] << 4) : 0;
|
|
f.data[o] = static_cast<uint8_t>(p0 & 0xFF);
|
|
f.data[o + 1] = static_cast<uint8_t>(((p0 >> 8) & 0x0F) | ((p1 & 0x0F) << 4));
|
|
if (o + 2 < f.data.size()) f.data[o + 2] = static_cast<uint8_t>(p1 >> 4);
|
|
}
|
|
}
|
|
return f;
|
|
}
|
|
|
|
int count_;
|
|
uint32_t width_;
|
|
uint32_t height_;
|
|
PixelLayout layout_;
|
|
uint64_t stream_seq_ = 1;
|
|
bool started_ = false;
|
|
long long frames_ = 0;
|
|
std::string profile_;
|
|
FrameCallback callback_;
|
|
Fault fault_ = Fault::None;
|
|
std::map<std::string, std::string> features_; // what setFeature() has set
|
|
};
|
|
|
|
} // namespace fgc
|