fwt_software/tests/test_rawimage.cpp

202 lines
8.2 KiB
C++

#include "fgc/RawImage.h"
#include <doctest/doctest.h>
#include <random>
#include <vector>
using namespace fgc;
namespace {
// Pack 12-bit values the way the camera does (GenICam 12p).
std::vector<uint8_t> pack12p(const std::vector<uint16_t>& v) {
std::vector<uint8_t> out((v.size() * 3 + 1) / 2);
for (size_t i = 0, o = 0; i < v.size(); i += 2, o += 3) {
const uint16_t p0 = v[i];
const uint16_t p1 = i + 1 < v.size() ? v[i + 1] : 0;
out[o] = static_cast<uint8_t>(p0 & 0xFF);
out[o + 1] = static_cast<uint8_t>(((p0 >> 8) & 0x0F) | ((p1 & 0x0F) << 4));
if (o + 2 < out.size()) out[o + 2] = static_cast<uint8_t>(p1 >> 4);
}
return out;
}
// Pack 12-bit values the GigE Vision 1.x way (BayerRG12Packed, the Manta's):
// the first byte carries p0's high bits.
std::vector<uint8_t> pack12Packed(const std::vector<uint16_t>& v) {
std::vector<uint8_t> out((v.size() * 3 + 1) / 2);
for (size_t i = 0, o = 0; i < v.size(); i += 2, o += 3) {
const uint16_t p0 = v[i];
const uint16_t p1 = i + 1 < v.size() ? v[i + 1] : 0;
out[o] = static_cast<uint8_t>(p0 >> 4);
out[o + 1] = static_cast<uint8_t>((p0 & 0x0F) | ((p1 & 0x0F) << 4));
if (o + 2 < out.size()) out[o + 2] = static_cast<uint8_t>(p1 >> 4);
}
return out;
}
} // namespace
TEST_CASE("layout names round-trip, and unknown formats are refused") {
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8,
PixelLayout::BayerRG12p, PixelLayout::BayerRG12Packed,
PixelLayout::Mono16})
CHECK(layoutFromName(layoutName(l)) == l);
CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled
CHECK(layoutFromName("YCbCr422_8_CbYCrY") == PixelLayout::Unknown);
CHECK(std::string(layoutName(PixelLayout::Unknown)) == "Unknown");
}
TEST_CASE("layout properties") {
CHECK(layoutIsBayer(PixelLayout::BayerRG8));
CHECK(layoutIsBayer(PixelLayout::BayerRG12p));
CHECK_FALSE(layoutIsBayer(PixelLayout::RGB8));
CHECK(layoutBits(PixelLayout::BayerRG12p) == 12);
CHECK(layoutBits(PixelLayout::RGB8) == 8);
CHECK(layoutBits(PixelLayout::Unknown) == 0);
// The 1620c's full frame, as measured: 24.3 / 16.2 / 48.6 MB.
CHECK(layoutFrameBytes(PixelLayout::BayerRG12p, 5328, 3040) == 24295680);
CHECK(layoutFrameBytes(PixelLayout::BayerRG8, 5328, 3040) == 16197120);
CHECK(layoutFrameBytes(PixelLayout::RGB8, 5328, 3040) == 48591360);
CHECK(layoutFrameBytes(PixelLayout::BayerRG12p, 3, 1) == 5); // odd pixel count rounds up
// The Manta G-1236C's full frame in BayerRG12Packed: its PayloadSize.
CHECK(layoutIsBayer(PixelLayout::BayerRG12Packed));
CHECK(layoutBits(PixelLayout::BayerRG12Packed) == 12);
CHECK(layoutFrameBytes(PixelLayout::BayerRG12Packed, 4112, 3008) == 18553344);
// The thermal camera: 640 x 480 counts, two bytes each.
CHECK_FALSE(layoutIsBayer(PixelLayout::Mono16));
CHECK(layoutBits(PixelLayout::Mono16) == 16);
CHECK(layoutFrameBytes(PixelLayout::Mono16, 640, 480) == 614400);
}
TEST_CASE("brightness sample: 16-bit is reported on the 0..255 scale") {
std::vector<uint16_t> v(640 * 480, 0x8000);
const auto* b = reinterpret_cast<const uint8_t*>(v.data());
const auto s = sampleBrightness(b, v.size() * 2, PixelLayout::Mono16, 640, 480);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(128.0));
CHECK(s.clipped == doctest::Approx(0.0));
}
TEST_CASE("unpack12p matches GenICam 12p, by hand") {
// p0 = 0xABC, p1 = 0x123: bytes BC, 3A, 12.
const uint8_t in[3] = {0xBC, 0x3A, 0x12};
uint16_t out[2] = {};
unpack12p(in, out, 2);
CHECK(out[0] == 0xABC);
CHECK(out[1] == 0x123);
}
TEST_CASE("unpack12p round-trips every value, odd counts included") {
std::mt19937 rng(12);
std::vector<uint16_t> src(1001);
for (auto& v : src) v = static_cast<uint16_t>(rng() % 4096);
src[0] = 0;
src[1] = 4095;
for (size_t n : {size_t(1000), size_t(1001)}) {
CAPTURE(n);
std::vector<uint16_t> part(src.begin(), src.begin() + n);
const auto packed = pack12p(part);
std::vector<uint16_t> out(n, 0xFFFF);
unpack12p(packed.data(), out.data(), n);
CHECK(out == part);
}
}
TEST_CASE("unpack12Packed matches GigE Vision 12Packed, by hand") {
// p0 = 0xABC, p1 = 0x123: bytes AB, 3C, 12.
const uint8_t in[3] = {0xAB, 0x3C, 0x12};
uint16_t out[2] = {};
unpack12Packed(in, out, 2);
CHECK(out[0] == 0xABC);
CHECK(out[1] == 0x123);
// The same bytes read as 12p are other values: the two must not be confused.
unpack12p(in, out, 2);
CHECK(out[0] != 0xABC);
}
TEST_CASE("unpack12Packed round-trips every value, odd counts included") {
std::mt19937 rng(1236);
std::vector<uint16_t> src(1001);
for (auto& v : src) v = static_cast<uint16_t>(rng() % 4096);
src[0] = 0;
src[1] = 4095;
for (size_t n : {size_t(1000), size_t(1001)}) {
CAPTURE(n);
std::vector<uint16_t> part(src.begin(), src.begin() + n);
const auto packed = pack12Packed(part);
std::vector<uint16_t> out(n, 0xFFFF);
unpack12(PixelLayout::BayerRG12Packed, packed.data(), out.data(), n);
CHECK(out == part);
unpack12(PixelLayout::BayerRG12p, pack12p(part).data(), out.data(), n);
CHECK(out == part);
}
}
TEST_CASE("brightness sample: 8-bit layouts") {
const uint32_t w = 400, h = 300;
SUBCASE("mid grey") {
std::vector<uint8_t> f(w * h, 100);
auto s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG8, w, h);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(100));
CHECK(s.clipped == 0);
}
SUBCASE("half blown out, RGB8 mixes the channels") {
std::vector<uint8_t> f(w * h * 3, 20);
for (size_t i = 0; i < f.size() / 2; ++i) f[i] = 255;
auto s = sampleBrightness(f.data(), f.size(), PixelLayout::RGB8, w, h);
REQUIRE(s.valid());
CHECK(s.clipped == doctest::Approx(0.5).epsilon(0.02));
CHECK(s.mean == doctest::Approx((255 + 20) / 2.0).epsilon(0.02));
}
}
TEST_CASE("brightness sample: 12-bit is reported on the 0..255 scale") {
const uint32_t w = 400, h = 300;
std::vector<uint16_t> v(w * h, 1600); // 1600 / 16 = 100
auto f = pack12p(v);
auto s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12p, w, h);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(100));
CHECK(s.clipped == 0);
std::fill(v.begin(), v.end(), 4095);
f = pack12p(v);
s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12p, w, h);
CHECK(s.mean == doctest::Approx(4095 / 16.0));
CHECK(s.clipped == 1);
// GigE 12Packed, with values that read differently as 12p: 0x640 = 1600.
std::fill(v.begin(), v.end(), 1600);
f = pack12Packed(v);
s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12Packed, w, h);
REQUIRE(s.valid());
CHECK(s.mean == doctest::Approx(100));
}
TEST_CASE("brightness sample refuses what it cannot read") {
std::vector<uint8_t> f(100, 50);
CHECK_FALSE(sampleBrightness(f.data(), f.size(), PixelLayout::Unknown, 10, 10).valid());
CHECK_FALSE(sampleBrightness(f.data(), f.size(), PixelLayout::RGB8, 10, 10).valid()); // too short
CHECK_FALSE(sampleBrightness(nullptr, 0, PixelLayout::Mono8, 10, 10).valid());
CHECK(sampleBrightness(f.data(), f.size(), PixelLayout::Mono8, 10, 10).valid());
}
#include "fgc/mock/MockCameraSource.h"
TEST_CASE("the mock camera delivers raw frames like a raw-capture profile") {
for (PixelLayout l : {PixelLayout::RGB8, PixelLayout::BayerRG8, PixelLayout::BayerRG12p}) {
CAPTURE(layoutName(l));
MockCameraSource cam(1, 64, 48, l);
Frame got;
cam.setFrameCallback([&got](const Frame& f) { got = f; });
REQUIRE(cam.trigger());
CHECK(got.layout == l);
CHECK(got.channels == (l == PixelLayout::RGB8 ? 3 : 1));
CHECK(got.data.size() == layoutFrameBytes(l, 64, 48));
REQUIRE(got.brightness.valid());
// The scene's mid grey (110) whatever the layout and bit depth.
CHECK(got.brightness.mean == doctest::Approx(110).epsilon(0.15));
}
}