#include "fgc/RawImage.h" #include #include #include using namespace fgc; namespace { // Pack 12-bit values the way the camera does (GenICam 12p). std::vector pack12p(const std::vector& v) { std::vector 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(p0 & 0xFF); out[o + 1] = static_cast(((p0 >> 8) & 0x0F) | ((p1 & 0x0F) << 4)); if (o + 2 < out.size()) out[o + 2] = static_cast(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::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 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 v(640 * 480, 0x8000); const auto* b = reinterpret_cast(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 src(1001); for (auto& v : src) v = static_cast(rng() % 4096); src[0] = 0; src[1] = 4095; for (size_t n : {size_t(1000), size_t(1001)}) { CAPTURE(n); std::vector part(src.begin(), src.begin() + n); const auto packed = pack12p(part); std::vector out(n, 0xFFFF); unpack12p(packed.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 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 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 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); } TEST_CASE("brightness sample refuses what it cannot read") { std::vector 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)); } }