fwt_software/tools/camera-bench/bench.cpp

211 lines
8.6 KiB
C++

// bench.cpp - tower CPU cost per full-resolution frame, using the production
// JpegXlEncoder.cpp unchanged. Frames come from camera_bench.py `save`.
//
// bench all <frames_dir> <out_dir> every measurement, one JSON line each
// bench load <frames_dir> <out_dir> <secs> encode colour d0.8 e4 in a loop
// (to stream alongside)
#include "fgc/JpegXlEncoder.h"
#include "fgc/Logger.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/imgproc.hpp>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using Clock = std::chrono::steady_clock;
// Frame size, from the .json camera_bench.py writes beside each frame.
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 ms(Clock::time_point a, Clock::time_point b) {
return std::chrono::duration<double, std::milli>(b - a).count();
}
// Median of `reps` runs of fn (fn returns nothing; timed around the call).
static double timeit(int reps, const std::function<void()>& fn) {
std::vector<double> t;
for (int i = 0; i < reps; ++i) {
auto a = Clock::now();
fn();
t.push_back(ms(a, Clock::now()));
}
std::sort(t.begin(), t.end());
return t[t.size() / 2];
}
static void line(const std::string& what, double t, long long bytes = -1) {
std::printf("{\"test\": \"%s\", \"ms\": %.1f", what.c_str(), t);
if (bytes >= 0) std::printf(", \"bytes\": %lld", bytes);
std::printf("}\n");
std::fflush(stdout);
}
// GenICam 12p: two pixels in three bytes, little-endian.
static std::vector<uint16_t> unpack12p(const std::vector<uint8_t>& in) {
std::vector<uint16_t> out(static_cast<size_t>(W) * H);
for (size_t i = 0, o = 0; o + 1 < out.size(); 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));
}
return out;
}
// Bench-only: lossless single-channel 16-bit JXL carrying 12 significant bits.
// The production encoder is 8-bit only.
static bool encodeGray(const std::string& path, const void* img, int bits, 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 = 1;
bi.uses_original_profile = JXL_TRUE;
if (JxlEncoderSetBasicInfo(enc.get(), &bi) != JXL_ENC_SUCCESS) return false;
JxlColorEncoding ce = {};
JxlColorEncodingSetToSRGB(&ce, JXL_TRUE);
JxlEncoderSetColorEncoding(enc.get(), &ce);
auto* fs = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_EFFORT, effort);
JxlEncoderSetFrameDistance(fs, 0);
JxlEncoderSetFrameLossless(fs, JXL_TRUE);
// 12-bit values in 16-bit words must be read at the header's bit depth, or
// libjxl rescales them from 0..65535 and the result is not lossless.
JxlBitDepth depth = {JXL_BIT_DEPTH_FROM_CODESTREAM, 0, 0};
JxlEncoderSetFrameBitDepth(fs, &depth);
JxlPixelFormat pf = {1, bits > 8 ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};
if (JxlEncoderAddImageFrame(fs, &pf, img, static_cast<size_t>(W) * H * (bits > 8 ? 2 : 1)) != JXL_ENC_SUCCESS)
return false;
JxlEncoderCloseInput(enc.get());
std::vector<uint8_t> buf(1 << 20);
uint8_t* next = buf.data();
size_t avail = buf.size();
JxlEncoderStatus r;
while ((r = JxlEncoderProcessOutput(enc.get(), &next, &avail)) == JXL_ENC_NEED_MORE_OUTPUT) {
size_t off = next - buf.data();
buf.resize(buf.size() * 2);
next = buf.data() + off;
avail = buf.size() - off;
}
if (r != JXL_ENC_SUCCESS) return false;
std::ofstream(path, std::ios::binary).write(reinterpret_cast<char*>(buf.data()),
next - buf.data());
return true;
}
static long long fsize(const std::string& p) { return static_cast<long long>(fs::file_size(p)); }
int main(int argc, char** argv) {
if (argc < 4) { std::fprintf(stderr, "usage: bench all|load <frames> <out> [secs]\n"); return 2; }
const std::string mode = argv[1], dir = argv[2], out = argv[3];
fs::create_directories(out);
{
std::ifstream j(dir + "/BayerRG8_0.json");
std::string t{std::istreambuf_iterator<char>(j), {}};
W = jsonInt(t, "width");
H = jsonInt(t, "height");
}
auto rgb = load(dir + "/RGB8_0.bin");
auto bayer = load(dir + "/BayerRG8_0.bin");
auto b12 = load(dir + "/BayerRG12p_0.bin");
if (rgb.size() != size_t(W) * H * 3 || bayer.size() != size_t(W) * H || b12.empty()) {
std::fprintf(stderr, "frames missing or wrong size\n");
return 1;
}
if (mode == "raw8true") {
for (int e : {1, 2, 3}) {
std::string p = out + "/raw8true_e" + std::to_string(e) + ".jxl";
double t = timeit(3, [&] { encodeGray(p, bayer.data(), 8, e, 3); });
line("raw8_truelossless_e" + std::to_string(e) + "_t3", t, fsize(p));
}
return 0;
}
if (mode == "load") {
const double secs = argc > 4 ? std::atof(argv[4]) : 60;
auto t0 = Clock::now();
int n = 0;
while (ms(t0, Clock::now()) < secs * 1000) {
fgc::JpegXlEncoder::encodeToFile(out + "/load.jxl", rgb.data(), W, H, 3, 0.8f, 4);
++n;
}
line("load_colour_d0.8_e4_count" + std::to_string(n), ms(t0, Clock::now()) / n);
return 0;
}
// 1. The copy FrameReceived makes of every delivered frame.
std::vector<uint8_t> dst;
line("copy_rgb8_48.6MB", timeit(5, [&] { dst.assign(rgb.begin(), rgb.end()); }));
line("copy_bayer8_16.2MB", timeit(5, [&] { dst.assign(bayer.begin(), bayer.end()); }));
// 2. De-Bayer. The camera's BayerRG8 is OpenCV's BayerBG (measured: matches
// the camera's own RGB8 channel means).
cv::Mat b(H, W, CV_8UC1, bayer.data()), c;
line("debayer_bilinear", timeit(5, [&] { cv::cvtColor(b, c, cv::COLOR_BayerBG2RGB); }));
line("debayer_ea", timeit(3, [&] { cv::cvtColor(b, c, cv::COLOR_BayerBG2RGB_EA); }));
line("debayer_vng", timeit(3, [&] { cv::cvtColor(b, c, cv::COLOR_BayerBG2RGB_VNG); }));
cv::Mat cam_rgb(H, W, CV_8UC3, rgb.data()), rot;
line("rotate_180_rgb8", timeit(5, [&] { cv::rotate(cam_rgb, rot, cv::ROTATE_180); }));
cv::cvtColor(b, c, cv::COLOR_BayerBG2RGB);
// 3. Colour JXL, lossy as production: camera RGB8 and tower-de-Bayered.
for (int threads : {3, 4})
for (int e : {1, 2, 3, 4, 5}) {
std::string p = out + "/rgb_d08_e" + std::to_string(e) + ".jxl";
double t = timeit(3, [&] {
fgc::JpegXlEncoder::encodeToFile(p, rgb.data(), W, H, 3, 0.8f, e, threads);
});
line("colour_camRGB8_d0.8_e" + std::to_string(e) + "_t" + std::to_string(threads), t,
fsize(p));
}
for (int e : {1, 4}) {
std::string p = out + "/deb_d08_e" + std::to_string(e) + ".jxl";
double t = timeit(3, [&] {
fgc::JpegXlEncoder::encodeToFile(p, c.data, W, H, 3, 0.8f, e, 3);
});
line("colour_towerDebayer_d0.8_e" + std::to_string(e) + "_t3", t, fsize(p));
}
// 4. Raw mosaic, lossless, single channel.
for (int e : {1, 2, 3, 4}) {
std::string p = out + "/raw8_e" + std::to_string(e) + ".jxl";
double t = timeit(3, [&] {
fgc::JpegXlEncoder::encodeToFile(p, bayer.data(), W, H, 1, 0.0f, e, 3);
});
line("raw8_lossless_e" + std::to_string(e) + "_t3", t, fsize(p));
}
std::vector<uint16_t> u;
line("unpack_12p", timeit(3, [&] { u = unpack12p(b12); }));
for (int e : {1, 2, 3}) {
std::string p = out + "/raw12_e" + std::to_string(e) + ".jxl";
double t = timeit(3, [&] { encodeGray(p, u.data(), 12, e, 3); });
line("raw12_lossless_e" + std::to_string(e) + "_t3", t, fsize(p));
}
return 0;
}