775 lines
32 KiB
C++
775 lines
32 KiB
C++
// raw12_bench.cpp - the 12-bit raw capture pipeline on the tower: unpack the
|
|
// camera's BayerRG12p, de-Bayer, encode JPEG XL, write; against today's RGB8
|
|
// path. Every stage is timed on its own, quality is measured against each
|
|
// frame's own uncompressed version, and example files are written for the
|
|
// report. Frames come from camera_bench.py `save` (BayerRG12p_0, BayerRG8_0,
|
|
// RGB8_0 .bin/.json) or make_synthetic.py.
|
|
//
|
|
// raw12_bench check <frames> <out>
|
|
// the bench 8-bit encoder matches production's output; 12-bit lossless
|
|
// decodes exactly; de-Bayer colour order matches the camera's RGB8.
|
|
// raw12_bench matrix <frames> <out>
|
|
// stage times and size for every kind x effort 1-4 x threads 1-4; a
|
|
// distance sweep with quality. One JSON line each.
|
|
// raw12_bench pipeline <frames> <out> <kind> <workers> <threads> <secs> <period_ms> <effort> <distance>
|
|
// saves back to back (period 0) or one per period_ms, as a scan keeps one
|
|
// frame per waypoint: throughput, capture->saved latency, backlog, peak
|
|
// memory, CPU temperature/clock/load/power once a second.
|
|
// raw12_bench examples <frames> <out>
|
|
// one .jxl per compression type plus its decoded PNG, for the report.
|
|
// raw12_bench idle <frames> <out> <secs>
|
|
//
|
|
// Kinds:
|
|
// rgb8 camera RGB8 -> 8-bit colour JXL (today's path)
|
|
// c8_bil c8_ea BayerRG8 -> de-Bayer (bilinear / edge-aware) -> 8-bit colour
|
|
// c12_bil c12_ea BayerRG12p -> unpack -> 16-bit de-Bayer -> 12-bit colour
|
|
// raw12 BayerRG12p -> unpack -> 12-bit mosaic, one channel
|
|
// raw12_planes as raw12, rearranged into four colour planes (R, G1 / G2, B)
|
|
// tiled in one image, so each region is a smooth single colour
|
|
// raw8 BayerRG8 mosaic, one channel
|
|
// distance 0 = lossless (with uses_original_profile set, unlike production's
|
|
// encoder - known-issues #20).
|
|
#include "fgc/JpegXlEncoder.h"
|
|
|
|
#include <jxl/decode.h>
|
|
#include <jxl/decode_cxx.h>
|
|
#include <jxl/encode.h>
|
|
#include <jxl/encode_cxx.h>
|
|
#include <jxl/thread_parallel_runner.h>
|
|
#include <jxl/thread_parallel_runner_cxx.h>
|
|
|
|
#include <opencv2/core.hpp>
|
|
#include <opencv2/imgcodecs.hpp>
|
|
#include <opencv2/imgproc.hpp>
|
|
|
|
#include <sys/resource.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <condition_variable>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
static int W = 0, H = 0;
|
|
|
|
static int jsonInt(const std::string& text, const std::string& key) {
|
|
auto at = text.find("\"" + key + "\"");
|
|
return at == std::string::npos ? 0 : std::atoi(text.c_str() + text.find(':', at) + 1);
|
|
}
|
|
static std::vector<uint8_t> load(const std::string& p) {
|
|
std::ifstream f(p, std::ios::binary);
|
|
return {std::istreambuf_iterator<char>(f), {}};
|
|
}
|
|
static double msSince(Clock::time_point a) {
|
|
return std::chrono::duration<double, std::milli>(Clock::now() - a).count();
|
|
}
|
|
|
|
// ---- conversion ----------------------------------------------------------------
|
|
|
|
// GenICam 12p: two pixels in three bytes, little-endian.
|
|
static void unpack12p(const uint8_t* in, uint16_t* out, size_t n) {
|
|
for (size_t i = 0, o = 0; o + 1 < n; i += 3, o += 2) {
|
|
out[o] = static_cast<uint16_t>(in[i] | ((in[i + 1] & 0x0F) << 8));
|
|
out[o + 1] = static_cast<uint16_t>((in[i + 1] >> 4) | (in[i + 2] << 4));
|
|
}
|
|
}
|
|
|
|
// The camera's BayerRG is OpenCV's BayerBG (measured 2026-09-30: matches the
|
|
// camera's own RGB8 channel order).
|
|
static int demosaicCode(bool ea) { return ea ? cv::COLOR_BayerBG2RGB_EA : cv::COLOR_BayerBG2RGB; }
|
|
|
|
// Mosaic -> four quarter-size colour planes tiled in one image:
|
|
// [ R | G1 ]
|
|
// [ G2 | B ]
|
|
static void toPlanes(const uint16_t* m, uint16_t* p) {
|
|
const int hw = W / 2, hh = H / 2;
|
|
for (int y = 0; y < hh; ++y)
|
|
for (int x = 0; x < hw; ++x) {
|
|
const size_t s = static_cast<size_t>(2 * y) * W + 2 * x;
|
|
p[static_cast<size_t>(y) * W + x] = m[s];
|
|
p[static_cast<size_t>(y) * W + hw + x] = m[s + 1];
|
|
p[static_cast<size_t>(y + hh) * W + x] = m[s + W];
|
|
p[static_cast<size_t>(y + hh) * W + hw + x] = m[s + W + 1];
|
|
}
|
|
}
|
|
|
|
// ---- JPEG XL --------------------------------------------------------------------
|
|
|
|
// Encoder with production's settings for 8-bit lossy (uses_original_profile off,
|
|
// decoding speed 0), plus a correct lossless mode and 12-bit input in 16-bit
|
|
// words (which needs the bit depth taken from the header - see known-issues
|
|
// #20 and docs/camera-capture-measurements.md).
|
|
static bool encodeMem(std::vector<uint8_t>& out, const void* img, int channels, int bits,
|
|
float distance, int effort, int threads) {
|
|
auto enc = JxlEncoderMake(nullptr);
|
|
auto runner = JxlThreadParallelRunnerMake(nullptr, threads);
|
|
JxlEncoderSetParallelRunner(enc.get(), JxlThreadParallelRunner, runner.get());
|
|
JxlBasicInfo bi;
|
|
JxlEncoderInitBasicInfo(&bi);
|
|
bi.xsize = W; bi.ysize = H; bi.bits_per_sample = bits; bi.num_color_channels = channels;
|
|
bi.uses_original_profile = distance == 0.0f ? JXL_TRUE : JXL_FALSE;
|
|
if (JxlEncoderSetBasicInfo(enc.get(), &bi) != JXL_ENC_SUCCESS) return false;
|
|
JxlColorEncoding ce = {};
|
|
JxlColorEncodingSetToSRGB(&ce, channels < 3 ? JXL_TRUE : JXL_FALSE);
|
|
if (JxlEncoderSetColorEncoding(enc.get(), &ce) != JXL_ENC_SUCCESS) return false;
|
|
auto* fs = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
|
|
JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_EFFORT, effort);
|
|
JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_DECODING_SPEED, 0);
|
|
if (distance == 0.0f) {
|
|
JxlEncoderSetFrameDistance(fs, 0);
|
|
JxlEncoderSetFrameLossless(fs, JXL_TRUE);
|
|
} else {
|
|
JxlEncoderSetFrameLossless(fs, JXL_FALSE);
|
|
JxlEncoderSetFrameDistance(fs, distance);
|
|
}
|
|
const bool wide = bits > 8;
|
|
if (wide) {
|
|
JxlBitDepth depth = {JXL_BIT_DEPTH_FROM_CODESTREAM, 0, 0};
|
|
JxlEncoderSetFrameBitDepth(fs, &depth);
|
|
}
|
|
JxlPixelFormat pf = {static_cast<uint32_t>(channels), wide ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8,
|
|
JXL_NATIVE_ENDIAN, 0};
|
|
const size_t bytes = static_cast<size_t>(W) * H * channels * (wide ? 2 : 1);
|
|
if (JxlEncoderAddImageFrame(fs, &pf, img, bytes) != JXL_ENC_SUCCESS) return false;
|
|
JxlEncoderCloseInput(enc.get());
|
|
out.resize(1 << 22);
|
|
uint8_t* next = out.data();
|
|
size_t avail = out.size();
|
|
JxlEncoderStatus r;
|
|
while ((r = JxlEncoderProcessOutput(enc.get(), &next, &avail)) == JXL_ENC_NEED_MORE_OUTPUT) {
|
|
size_t off = next - out.data();
|
|
out.resize(out.size() * 2);
|
|
next = out.data() + off;
|
|
avail = out.size() - off;
|
|
}
|
|
if (r != JXL_ENC_SUCCESS) return false;
|
|
out.resize(next - out.data());
|
|
return true;
|
|
}
|
|
|
|
static bool writeFile(const std::string& path, const std::vector<uint8_t>& data) {
|
|
FILE* f = std::fopen(path.c_str(), "wb");
|
|
if (!f) return false;
|
|
bool ok = std::fwrite(data.data(), 1, data.size(), f) == data.size();
|
|
ok = std::fflush(f) == 0 && ok;
|
|
ok = std::fclose(f) == 0 && ok;
|
|
return ok;
|
|
}
|
|
|
|
// Decode to `channels` x 16-bit at the file's own bit depth (12-bit files give
|
|
// 0..4095, 8-bit files 0..255).
|
|
static bool decode16(const std::vector<uint8_t>& data, int channels, std::vector<uint16_t>& px,
|
|
int* bits = nullptr) {
|
|
auto dec = JxlDecoderMake(nullptr);
|
|
JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE);
|
|
JxlDecoderSetInput(dec.get(), data.data(), data.size());
|
|
JxlDecoderCloseInput(dec.get());
|
|
JxlPixelFormat pf = {static_cast<uint32_t>(channels), JXL_TYPE_UINT16, JXL_NATIVE_ENDIAN, 0};
|
|
for (;;) {
|
|
auto s = JxlDecoderProcessInput(dec.get());
|
|
if (s == JXL_DEC_BASIC_INFO) {
|
|
JxlBasicInfo bi;
|
|
JxlDecoderGetBasicInfo(dec.get(), &bi);
|
|
if (bits) *bits = static_cast<int>(bi.bits_per_sample);
|
|
} else if (s == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
|
|
size_t n;
|
|
JxlDecoderImageOutBufferSize(dec.get(), &pf, &n);
|
|
px.resize(n / 2);
|
|
JxlDecoderSetImageOutBuffer(dec.get(), &pf, px.data(), n);
|
|
JxlBitDepth bd = {JXL_BIT_DEPTH_FROM_CODESTREAM, 0, 0};
|
|
JxlDecoderSetImageOutBitDepth(dec.get(), &bd);
|
|
} else if (s == JXL_DEC_FULL_IMAGE || s == JXL_DEC_SUCCESS) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- frames and jobs -------------------------------------------------------------
|
|
|
|
struct Frames {
|
|
std::vector<uint8_t> rgb, bayer8, b12p;
|
|
};
|
|
|
|
struct Kind {
|
|
int out_channels; // channels in the JXL
|
|
int bits; // bits in the JXL
|
|
bool lossy_ok; // a lossy distance makes sense for this kind
|
|
};
|
|
|
|
static Kind kindInfo(const std::string& k) {
|
|
if (k == "rgb8" || k == "c8_bil" || k == "c8_ea") return {3, 8, true};
|
|
if (k == "c12_bil" || k == "c12_ea") return {3, 12, true};
|
|
if (k == "raw12" || k == "raw12_planes") return {1, 12, k == "raw12_planes"};
|
|
if (k == "raw8") return {1, 8, false};
|
|
return {0, 0, false};
|
|
}
|
|
|
|
struct Stages {
|
|
double unpack = 0, demosaic = 0, encode = 0, write = 0;
|
|
double total() const { return unpack + demosaic + encode + write; }
|
|
};
|
|
|
|
// The uncompressed image a kind encodes (what quality is measured against).
|
|
struct Prepared {
|
|
std::vector<uint16_t> u16; // 12-bit data
|
|
cv::Mat mat; // colour or mosaic as the encoder sees it
|
|
const void* ptr = nullptr;
|
|
};
|
|
|
|
static bool prepare(const Frames& f, const std::string& k, Prepared& p, Stages& st) {
|
|
auto t = Clock::now();
|
|
const size_t n = static_cast<size_t>(W) * H;
|
|
if (k == "rgb8") {
|
|
p.ptr = f.rgb.data();
|
|
return true;
|
|
}
|
|
if (k == "raw8") {
|
|
p.ptr = f.bayer8.data();
|
|
return true;
|
|
}
|
|
if (k == "c8_bil" || k == "c8_ea") {
|
|
cv::Mat b(H, W, CV_8UC1, const_cast<uint8_t*>(f.bayer8.data()));
|
|
cv::cvtColor(b, p.mat, demosaicCode(k == "c8_ea"));
|
|
st.demosaic = msSince(t);
|
|
p.ptr = p.mat.data;
|
|
return true;
|
|
}
|
|
p.u16.resize(n);
|
|
unpack12p(f.b12p.data(), p.u16.data(), n);
|
|
st.unpack = msSince(t);
|
|
t = Clock::now();
|
|
if (k == "raw12") {
|
|
p.ptr = p.u16.data();
|
|
return true;
|
|
}
|
|
if (k == "raw12_planes") {
|
|
p.mat.create(H, W, CV_16UC1);
|
|
toPlanes(p.u16.data(), reinterpret_cast<uint16_t*>(p.mat.data));
|
|
st.demosaic = msSince(t); // the rearrangement stands in for de-Bayering
|
|
p.ptr = p.mat.data;
|
|
return true;
|
|
}
|
|
if (k == "c12_bil" || k == "c12_ea") {
|
|
cv::Mat b(H, W, CV_16UC1, p.u16.data());
|
|
cv::cvtColor(b, p.mat, demosaicCode(k == "c12_ea"));
|
|
st.demosaic = msSince(t);
|
|
p.ptr = p.mat.data;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool runJob(const Frames& f, const std::string& k, const std::string& path, int effort,
|
|
float distance, int threads, Stages& st, std::vector<uint8_t>* keep = nullptr,
|
|
Prepared* keep_prep = nullptr) {
|
|
Prepared p;
|
|
if (!prepare(f, k, p, st)) return false;
|
|
const Kind info = kindInfo(k);
|
|
std::vector<uint8_t> data;
|
|
auto t = Clock::now();
|
|
if (!encodeMem(data, p.ptr, info.out_channels, info.bits, info.lossy_ok ? distance : 0.0f,
|
|
effort, threads))
|
|
return false;
|
|
st.encode = msSince(t);
|
|
t = Clock::now();
|
|
if (!writeFile(path, data)) return false;
|
|
st.write = msSince(t);
|
|
if (keep) *keep = std::move(data);
|
|
if (keep_prep) *keep_prep = std::move(p);
|
|
return true;
|
|
}
|
|
|
|
// ---- quality ----------------------------------------------------------------------
|
|
|
|
struct Quality {
|
|
double psnr = -1; // dB, against the kind's own uncompressed image, peak = full scale
|
|
double detail = -1; // Laplacian energy decoded / reference (1 = all fine detail kept)
|
|
long long differing = -1;
|
|
};
|
|
|
|
static Quality quality(const std::vector<uint8_t>& jxl, const Prepared& p, const std::string& k,
|
|
const Frames& f) {
|
|
const Kind info = kindInfo(k);
|
|
std::vector<uint16_t> dec;
|
|
Quality q;
|
|
if (!decode16(jxl, info.out_channels, dec)) return q;
|
|
const size_t n = static_cast<size_t>(W) * H * info.out_channels;
|
|
// Reference as 16-bit values at the same scale.
|
|
std::vector<uint16_t> ref(n);
|
|
const void* src = p.ptr;
|
|
if (info.bits == 8) {
|
|
const uint8_t* s = static_cast<const uint8_t*>(src);
|
|
for (size_t i = 0; i < n; ++i) ref[i] = s[i];
|
|
} else {
|
|
std::memcpy(ref.data(), src, n * 2);
|
|
}
|
|
(void)f;
|
|
double se = 0;
|
|
long long diff = 0;
|
|
for (size_t i = 0; i < n && i < dec.size(); ++i) {
|
|
const double d = double(dec[i]) - double(ref[i]);
|
|
se += d * d;
|
|
diff += d != 0;
|
|
}
|
|
const double peak = info.bits == 8 ? 255.0 : 4095.0;
|
|
const double mse = se / n;
|
|
q.psnr = mse == 0 ? 99.0 : 10 * std::log10(peak * peak / mse);
|
|
q.differing = diff;
|
|
// Fine detail: variance of the Laplacian of one channel (green for colour).
|
|
auto lapvar = [&](const std::vector<uint16_t>& v) {
|
|
cv::Mat ch(H, W, CV_32F);
|
|
for (int y = 0; y < H; ++y)
|
|
for (int x = 0; x < W; ++x) {
|
|
const size_t i = (static_cast<size_t>(y) * W + x) * info.out_channels +
|
|
(info.out_channels == 3 ? 1 : 0);
|
|
ch.at<float>(y, x) = v[i];
|
|
}
|
|
cv::Mat lap;
|
|
cv::Laplacian(ch, lap, CV_32F);
|
|
cv::Scalar m, sd;
|
|
cv::meanStdDev(lap, m, sd);
|
|
return sd[0] * sd[0];
|
|
};
|
|
const double vr = lapvar(ref);
|
|
q.detail = vr > 0 ? lapvar(dec) / vr : -1;
|
|
return q;
|
|
}
|
|
|
|
// ---- samples: temperature, clock, CPU load, package power ------------------
|
|
|
|
static double readNum(const std::string& p, double fallback = -1) {
|
|
std::ifstream f(p);
|
|
double v;
|
|
return (f >> v) ? v : fallback;
|
|
}
|
|
static double cpuTempC() {
|
|
double best = -1;
|
|
for (auto& z : fs::directory_iterator("/sys/class/thermal")) {
|
|
if (z.path().filename().string().rfind("thermal_zone", 0) != 0) continue;
|
|
std::ifstream t(z.path() / "type");
|
|
std::string type;
|
|
t >> type;
|
|
double v = readNum((z.path() / "temp").string());
|
|
if (type == "x86_pkg_temp") return v / 1000;
|
|
best = std::max(best, v / 1000);
|
|
}
|
|
return best;
|
|
}
|
|
static double avgMhz() {
|
|
double sum = 0;
|
|
int n = 0;
|
|
for (int i = 0; i < 64; ++i) {
|
|
double v = readNum("/sys/devices/system/cpu/cpu" + std::to_string(i) +
|
|
"/cpufreq/scaling_cur_freq");
|
|
if (v < 0) break;
|
|
sum += v / 1000;
|
|
++n;
|
|
}
|
|
return n ? sum / n : -1;
|
|
}
|
|
struct CpuTimes { unsigned long long busy = 0, total = 0; };
|
|
static CpuTimes cpuTimes() {
|
|
std::ifstream f("/proc/stat");
|
|
std::string cpu;
|
|
unsigned long long v[8] = {};
|
|
f >> cpu >> v[0] >> v[1] >> v[2] >> v[3] >> v[4] >> v[5] >> v[6] >> v[7];
|
|
CpuTimes t;
|
|
for (auto x : v) t.total += x;
|
|
t.busy = t.total - v[3] - v[4];
|
|
return t;
|
|
}
|
|
// The package energy counter is root-only; FGC_RAPL names a copy a root loop
|
|
// refreshes (run_raw12_tests.sh).
|
|
static const std::string kRapl = std::getenv("FGC_RAPL")
|
|
? std::getenv("FGC_RAPL")
|
|
: "/sys/class/powercap/intel-rapl:0/energy_uj";
|
|
static double rssMb() {
|
|
std::ifstream f("/proc/self/status");
|
|
std::string line;
|
|
while (std::getline(f, line))
|
|
if (line.rfind("VmRSS:", 0) == 0) return std::atof(line.c_str() + 6) / 1024;
|
|
return -1;
|
|
}
|
|
|
|
struct Sampler {
|
|
std::atomic<bool> stop{false};
|
|
std::thread th;
|
|
std::function<void(std::string&)> extra;
|
|
std::vector<double> temps, mhz, watts, load, rss;
|
|
|
|
void start(double secs_total) {
|
|
th = std::thread([this, secs_total] {
|
|
auto t0 = Clock::now();
|
|
CpuTimes prev = cpuTimes();
|
|
double e0 = readNum(kRapl);
|
|
auto tp = Clock::now();
|
|
while (!stop && msSince(t0) < secs_total * 1000 + 500) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
CpuTimes now = cpuTimes();
|
|
double pc = now.total > prev.total
|
|
? 100.0 * (now.busy - prev.busy) / (now.total - prev.total)
|
|
: 0;
|
|
prev = now;
|
|
double e1 = readNum(kRapl);
|
|
double w = (e0 >= 0 && e1 >= e0) ? (e1 - e0) / 1e6 / (msSince(tp) / 1000) : -1;
|
|
e0 = e1;
|
|
tp = Clock::now();
|
|
double tc = cpuTempC(), mh = avgMhz(), rs = rssMb();
|
|
temps.push_back(tc); mhz.push_back(mh); load.push_back(pc); rss.push_back(rs);
|
|
if (w >= 0) watts.push_back(w);
|
|
std::string x;
|
|
if (extra) extra(x);
|
|
std::printf("{\"t\": %.0f, \"temp_c\": %.1f, \"mhz\": %.0f, \"cpu_pct\": %.1f, "
|
|
"\"watts\": %.2f, \"rss_mb\": %.0f%s}\n",
|
|
msSince(t0) / 1000, tc, mh, pc, w, rs, x.c_str());
|
|
std::fflush(stdout);
|
|
}
|
|
});
|
|
}
|
|
void finish() { stop = true; if (th.joinable()) th.join(); }
|
|
};
|
|
|
|
static double mean(const std::vector<double>& v) {
|
|
double s = 0;
|
|
for (double x : v) s += x;
|
|
return v.empty() ? -1 : s / v.size();
|
|
}
|
|
static double maxOf(const std::vector<double>& v) {
|
|
return v.empty() ? -1 : *std::max_element(v.begin(), v.end());
|
|
}
|
|
static double minOf(const std::vector<double>& v) {
|
|
return v.empty() ? -1 : *std::min_element(v.begin(), v.end());
|
|
}
|
|
static double pct(std::vector<double> v, double p) {
|
|
if (v.empty()) return -1;
|
|
std::sort(v.begin(), v.end());
|
|
return v[std::min(v.size() - 1, static_cast<size_t>(p * (v.size() - 1) + 0.5))];
|
|
}
|
|
static double cpuSecondsSelf() {
|
|
rusage r{};
|
|
getrusage(RUSAGE_SELF, &r);
|
|
return r.ru_utime.tv_sec + r.ru_utime.tv_usec / 1e6 + r.ru_stime.tv_sec +
|
|
r.ru_stime.tv_usec / 1e6;
|
|
}
|
|
|
|
// ---- modes -------------------------------------------------------------------------
|
|
|
|
static void emitJob(const std::string& k, int e, float d, int t, const Stages& s, long long bytes,
|
|
const Quality* q = nullptr) {
|
|
std::printf("{\"kind\": \"%s\", \"effort\": %d, \"distance\": %.2f, \"threads\": %d, "
|
|
"\"unpack_ms\": %.1f, \"demosaic_ms\": %.1f, \"encode_ms\": %.1f, "
|
|
"\"write_ms\": %.1f, \"total_ms\": %.1f, \"bytes\": %lld",
|
|
k.c_str(), e, kindInfo(k).lossy_ok ? d : 0.0f, t, s.unpack, s.demosaic, s.encode,
|
|
s.write, s.total(), bytes);
|
|
if (q)
|
|
std::printf(", \"psnr_db\": %.2f, \"detail\": %.4f, \"differing\": %lld", q->psnr,
|
|
q->detail, q->differing);
|
|
std::printf("}\n");
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
// Median of `reps` runs, stage by stage (each stage's own median).
|
|
static Stages timed(const Frames& f, const std::string& k, int e, float d, int t, int reps,
|
|
const std::string& path, long long& bytes) {
|
|
std::vector<double> u, m, en, w;
|
|
for (int i = 0; i < reps; ++i) {
|
|
Stages s;
|
|
runJob(f, k, path, e, d, t, s);
|
|
u.push_back(s.unpack); m.push_back(s.demosaic); en.push_back(s.encode); w.push_back(s.write);
|
|
}
|
|
std::error_code ec;
|
|
bytes = static_cast<long long>(fs::file_size(path, ec));
|
|
return {pct(u, 0.5), pct(m, 0.5), pct(en, 0.5), pct(w, 0.5)};
|
|
}
|
|
|
|
static int check(const Frames& f, const std::string& out) {
|
|
// 1. The bench 8-bit encoder reproduces production's file.
|
|
const std::string a = out + "/check_prod.jxl", b = out + "/check_bench.jxl";
|
|
fgc::JpegXlEncoder::encodeToFile(a, f.rgb.data(), W, H, 3, 1.0f, 4, 4);
|
|
Stages s;
|
|
runJob(f, "rgb8", b, 4, 1.0f, 4, s);
|
|
const bool same = load(a) == load(b);
|
|
// 2. 12-bit lossless round trips (colour and mosaic).
|
|
long long diffs[2] = {-1, -1};
|
|
const char* kinds[2] = {"c12_ea", "raw12"};
|
|
for (int i = 0; i < 2; ++i) {
|
|
std::vector<uint8_t> data;
|
|
Prepared p;
|
|
Stages st;
|
|
runJob(f, kinds[i], out + "/check_ll.jxl", 1, 0.0f, 4, st, &data, &p);
|
|
diffs[i] = quality(data, p, kinds[i], f).differing;
|
|
}
|
|
// 3. Colour order: the tower's de-Bayer against the camera's RGB8 (different
|
|
// captures of the same scene, so means only).
|
|
Prepared p;
|
|
Stages st;
|
|
prepare(f, "c8_ea", p, st);
|
|
cv::Scalar mt = cv::mean(p.mat), mc = cv::mean(cv::Mat(H, W, CV_8UC3,
|
|
const_cast<uint8_t*>(f.rgb.data())));
|
|
std::printf("{\"check\": \"bench_matches_production\", \"ok\": %s}\n"
|
|
"{\"check\": \"lossless_12bit_differing\", \"c12_ea\": %lld, \"raw12\": %lld}\n"
|
|
"{\"check\": \"colour_order\", \"tower_rgb\": [%.1f, %.1f, %.1f], "
|
|
"\"camera_rgb\": [%.1f, %.1f, %.1f]}\n",
|
|
same ? "true" : "false", diffs[0], diffs[1], mt[0], mt[1], mt[2], mc[0], mc[1],
|
|
mc[2]);
|
|
return 0;
|
|
}
|
|
|
|
static void matrix(const Frames& f, const std::string& out) {
|
|
long long bytes = 0;
|
|
// Effort x threads at distance 1.0 (lossless for the mosaic kinds).
|
|
for (const std::string k : {"rgb8", "c8_bil", "c8_ea", "c12_bil", "c12_ea"})
|
|
for (int t : {1, 2, 3, 4})
|
|
for (int e : {1, 2, 3, 4}) {
|
|
auto s = timed(f, k, e, 1.0f, t, 3, out + "/m.jxl", bytes);
|
|
emitJob(k, e, 1.0f, t, s, bytes);
|
|
}
|
|
for (const std::string k : {"raw12", "raw12_planes", "raw8"})
|
|
for (int t : {1, 2, 3, 4})
|
|
for (int e : {1, 2, 3}) {
|
|
auto s = timed(f, k, e, 0.0f, t, 3, out + "/m.jxl", bytes);
|
|
emitJob(k, e, 0.0f, t, s, bytes);
|
|
}
|
|
// Distance sweep with quality, effort 3 and 4, 4 threads.
|
|
for (const std::string k : {"c12_ea", "c12_bil", "rgb8", "c8_ea", "raw12_planes"})
|
|
for (int e : {3, 4})
|
|
for (float d : {0.0f, 0.3f, 0.5f, 0.8f, 1.0f, 1.5f, 2.0f}) {
|
|
if (d == 0.0f && k != "c12_ea" && k != "raw12_planes") continue;
|
|
const std::string path = out + "/q.jxl";
|
|
auto s = timed(f, k, e, d, 4, 2, path, bytes);
|
|
std::vector<uint8_t> data;
|
|
Prepared p;
|
|
Stages st;
|
|
runJob(f, k, path, e, d, 4, st, &data, &p);
|
|
Quality q = quality(data, p, k, f);
|
|
emitJob(k, e, d, 4, s, bytes, &q);
|
|
}
|
|
}
|
|
|
|
static void examples(const Frames& f, const std::string& out) {
|
|
struct Ex { std::string name, kind; float d; int e; };
|
|
const std::vector<Ex> list = {
|
|
{"rgb8_d0.8_e4_today", "rgb8", 0.8f, 4},
|
|
{"c8_ea_d1.0", "c8_ea", 1.0f, 3},
|
|
{"c12_ea_d0.3", "c12_ea", 0.3f, 3},
|
|
{"c12_ea_d0.5", "c12_ea", 0.5f, 3},
|
|
{"c12_ea_d0.8", "c12_ea", 0.8f, 3},
|
|
{"c12_ea_d1.0", "c12_ea", 1.0f, 3},
|
|
{"c12_ea_d1.5", "c12_ea", 1.5f, 3},
|
|
{"c12_ea_d2.0", "c12_ea", 2.0f, 3},
|
|
{"c12_ea_lossless", "c12_ea", 0.0f, 1},
|
|
{"c12_bil_d1.0", "c12_bil", 1.0f, 3},
|
|
{"raw12_lossless", "raw12", 0.0f, 1},
|
|
{"raw12_planes_lossless", "raw12_planes", 0.0f, 1},
|
|
{"raw12_planes_d0.5", "raw12_planes", 0.5f, 3},
|
|
};
|
|
fs::create_directories(out + "/jxl");
|
|
fs::create_directories(out + "/png");
|
|
for (const auto& ex : list) {
|
|
const std::string path = out + "/jxl/" + ex.name + ".jxl";
|
|
std::vector<uint8_t> data;
|
|
Prepared p;
|
|
Stages s;
|
|
runJob(f, ex.kind, path, ex.e, ex.d, 4, s, &data, &p);
|
|
const Kind info = kindInfo(ex.kind);
|
|
std::vector<uint16_t> dec;
|
|
decode16(data, info.out_channels, dec);
|
|
// Decoded image as a 16-bit PNG (values scaled to fill 16 bits). Mosaic
|
|
// kinds are de-Bayered first so every example is a viewable colour image.
|
|
cv::Mat img;
|
|
const double scale = info.bits == 8 ? 257.0 : 16.0;
|
|
if (info.out_channels == 3) {
|
|
cv::Mat m(H, W, CV_16UC3, dec.data());
|
|
m.convertTo(img, CV_16UC3, scale);
|
|
cv::cvtColor(img, img, cv::COLOR_RGB2BGR);
|
|
} else {
|
|
cv::Mat mosaic(H, W, CV_16UC1);
|
|
uint16_t* mo = reinterpret_cast<uint16_t*>(mosaic.data);
|
|
if (ex.kind == "raw12_planes") { // undo the tiling
|
|
const int hw = W / 2, hh = H / 2;
|
|
for (int y = 0; y < hh; ++y)
|
|
for (int x = 0; x < hw; ++x) {
|
|
const size_t d0 = static_cast<size_t>(2 * y) * W + 2 * x;
|
|
mo[d0] = dec[static_cast<size_t>(y) * W + x];
|
|
mo[d0 + 1] = dec[static_cast<size_t>(y) * W + hw + x];
|
|
mo[d0 + W] = dec[static_cast<size_t>(y + hh) * W + x];
|
|
mo[d0 + W + 1] = dec[static_cast<size_t>(y + hh) * W + hw + x];
|
|
}
|
|
} else {
|
|
std::memcpy(mo, dec.data(), dec.size() * 2);
|
|
}
|
|
cv::Mat colour;
|
|
cv::cvtColor(mosaic, colour, cv::COLOR_BayerBG2BGR_EA);
|
|
colour.convertTo(img, CV_16UC3, scale);
|
|
}
|
|
cv::imwrite(out + "/png/" + ex.name + ".png", img);
|
|
std::printf("{\"example\": \"%s\", \"kind\": \"%s\", \"distance\": %.2f, \"effort\": %d, "
|
|
"\"bytes\": %zu, \"total_ms\": %.1f}\n",
|
|
ex.name.c_str(), ex.kind.c_str(), ex.d, ex.e, data.size(), s.total());
|
|
std::fflush(stdout);
|
|
}
|
|
// The uncompressed references, for the figures: 12-bit EA colour and the
|
|
// 8-bit view of the same frame (12-bit >> 4), so 8 vs 12 bit is one moment.
|
|
Prepared p;
|
|
Stages s;
|
|
prepare(f, "c12_ea", p, s);
|
|
cv::Mat ref16, ref8;
|
|
p.mat.convertTo(ref16, CV_16UC3, 16.0);
|
|
cv::cvtColor(ref16, ref16, cv::COLOR_RGB2BGR);
|
|
cv::imwrite(out + "/png/reference_12bit.png", ref16);
|
|
p.mat.convertTo(ref8, CV_8UC3, 1.0 / 16.0);
|
|
cv::cvtColor(ref8, ref8, cv::COLOR_RGB2BGR);
|
|
cv::imwrite(out + "/png/reference_8bit_same_frame.png", ref8);
|
|
}
|
|
|
|
static int pipeline(const Frames& f, const std::string& out, const std::string& kind, int workers,
|
|
int threads, double secs, int period_ms, int effort, float distance) {
|
|
std::mutex mu;
|
|
std::condition_variable cv;
|
|
std::deque<Clock::time_point> queue;
|
|
std::atomic<long long> done{0};
|
|
std::atomic<bool> quit{false};
|
|
std::vector<double> lat, enc;
|
|
size_t max_q = 0;
|
|
const bool paced = period_ms > 0;
|
|
|
|
Sampler s;
|
|
s.extra = [&](std::string& x) {
|
|
size_t q;
|
|
{ std::lock_guard<std::mutex> l(mu); q = queue.size(); }
|
|
x = ", \"done\": " + std::to_string(done.load()) + ", \"queue\": " + std::to_string(q);
|
|
};
|
|
const double cpu0 = cpuSecondsSelf();
|
|
auto t0 = Clock::now();
|
|
s.start(secs);
|
|
|
|
std::vector<std::thread> ws;
|
|
for (int w = 0; w < workers; ++w)
|
|
ws.emplace_back([&, w] {
|
|
const std::string path = out + "/p_" + kind + "_" + std::to_string(w) + ".jxl";
|
|
for (;;) {
|
|
Clock::time_point enq;
|
|
if (paced) {
|
|
std::unique_lock<std::mutex> l(mu);
|
|
cv.wait(l, [&] { return !queue.empty() || quit; });
|
|
if (quit) return;
|
|
enq = queue.front();
|
|
queue.pop_front();
|
|
} else {
|
|
if (msSince(t0) >= secs * 1000) return;
|
|
enq = Clock::now();
|
|
}
|
|
Stages st;
|
|
runJob(f, kind, path, effort, distance, threads, st);
|
|
++done;
|
|
std::lock_guard<std::mutex> l(mu);
|
|
lat.push_back(msSince(enq));
|
|
enc.push_back(st.total());
|
|
}
|
|
});
|
|
|
|
if (paced) {
|
|
auto next = Clock::now();
|
|
while (msSince(t0) < secs * 1000) {
|
|
{
|
|
std::lock_guard<std::mutex> l(mu);
|
|
queue.push_back(Clock::now());
|
|
max_q = std::max(max_q, queue.size());
|
|
}
|
|
cv.notify_one();
|
|
next += std::chrono::milliseconds(period_ms);
|
|
std::this_thread::sleep_until(next);
|
|
}
|
|
std::lock_guard<std::mutex> l(mu);
|
|
quit = true;
|
|
}
|
|
cv.notify_all();
|
|
size_t backlog;
|
|
{ std::lock_guard<std::mutex> l(mu); backlog = queue.size(); }
|
|
for (auto& w : ws) w.join();
|
|
const double wall = msSince(t0) / 1000;
|
|
s.finish();
|
|
const double cpu = cpuSecondsSelf() - cpu0;
|
|
rusage r{};
|
|
getrusage(RUSAGE_SELF, &r);
|
|
|
|
std::lock_guard<std::mutex> l(mu);
|
|
std::printf("{\"summary\": \"pipeline\", \"kind\": \"%s\", \"workers\": %d, \"threads\": %d, "
|
|
"\"effort\": %d, \"distance\": %.2f, \"period_ms\": %d, \"secs\": %.1f, "
|
|
"\"frames\": %lld, \"frames_per_s\": %.3f, \"job_ms_p50\": %.0f, "
|
|
"\"latency_ms_p50\": %.0f, \"latency_ms_p95\": %.0f, \"latency_ms_max\": %.0f, "
|
|
"\"max_queue\": %zu, \"backlog_at_end\": %zu, \"cpu_s_per_frame\": %.3f, "
|
|
"\"peak_rss_mb\": %.0f, \"temp_c_mean\": %.1f, \"temp_c_max\": %.1f, "
|
|
"\"mhz_mean\": %.0f, \"mhz_min\": %.0f, \"cpu_pct_mean\": %.1f, "
|
|
"\"watts_mean\": %.2f}\n",
|
|
kind.c_str(), workers, threads, effort, distance, period_ms, wall, done.load(),
|
|
done.load() / wall, pct(enc, 0.5), pct(lat, 0.5), pct(lat, 0.95), maxOf(lat),
|
|
max_q, backlog, done ? cpu / done.load() : -1, r.ru_maxrss / 1024.0,
|
|
mean(s.temps), maxOf(s.temps), mean(s.mhz), minOf(s.mhz), mean(s.load),
|
|
mean(s.watts));
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 4) {
|
|
std::fprintf(stderr, "usage: raw12_bench check|matrix|pipeline|examples|idle <frames> <out> ...\n");
|
|
return 2;
|
|
}
|
|
const std::string mode = argv[1], dir = argv[2], out = argv[3];
|
|
fs::create_directories(out);
|
|
|
|
if (mode == "idle") {
|
|
Sampler s;
|
|
const double secs = argc > 4 ? std::atof(argv[4]) : 60;
|
|
s.start(secs);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(secs * 1000)));
|
|
s.finish();
|
|
std::printf("{\"summary\": \"idle\", \"secs\": %.0f, \"temp_c_mean\": %.1f, "
|
|
"\"temp_c_max\": %.1f, \"mhz_mean\": %.0f, \"cpu_pct_mean\": %.1f, "
|
|
"\"watts_mean\": %.2f}\n",
|
|
secs, mean(s.temps), maxOf(s.temps), mean(s.mhz), mean(s.load), mean(s.watts));
|
|
return 0;
|
|
}
|
|
|
|
{
|
|
std::ifstream j(dir + "/BayerRG12p_0.json");
|
|
std::string t{std::istreambuf_iterator<char>(j), {}};
|
|
W = jsonInt(t, "width");
|
|
H = jsonInt(t, "height");
|
|
}
|
|
Frames f;
|
|
f.rgb = load(dir + "/RGB8_0.bin");
|
|
f.bayer8 = load(dir + "/BayerRG8_0.bin");
|
|
f.b12p = load(dir + "/BayerRG12p_0.bin");
|
|
const size_t n = static_cast<size_t>(W) * H;
|
|
if (!W || f.rgb.size() != n * 3 || f.bayer8.size() != n || f.b12p.size() != n * 3 / 2) {
|
|
std::fprintf(stderr, "frames missing or wrong size\n");
|
|
return 1;
|
|
}
|
|
if (mode == "check") return check(f, out);
|
|
if (mode == "matrix") { matrix(f, out); return 0; }
|
|
if (mode == "examples") { examples(f, out); return 0; }
|
|
if (mode == "pipeline" && argc >= 11)
|
|
return pipeline(f, out, argv[4], std::atoi(argv[5]), std::atoi(argv[6]),
|
|
std::atof(argv[7]), std::atoi(argv[8]), std::atoi(argv[9]),
|
|
static_cast<float>(std::atof(argv[10])));
|
|
std::fprintf(stderr, "bad arguments\n");
|
|
return 2;
|
|
}
|