fwt_software/tools/camera-bench/raw12_bench.cpp

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;
}