diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 1d236b1..4693387 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -726,13 +726,15 @@ MTU 8000): RGB (G-1236C), NIR and ACR (both G-1236B). Their firmware | `AcquisitionFrameRate` (+ `…Enable`) | `AcquisitionFrameRateAbs` - which a free-running Manta runs at, up to `AcquisitionFrameRateLimit` | | `IntensityControllerTarget` / `…Tolerance` | `ExposureAutoTarget` / `ExposureAutoAdjustTol` | | `DeviceLinkThroughputLimit` | `StreamBytesPerSecond` (max 124 MB/s, shared by every camera on the port) | -| `BayerRG12p`, chunk data, `DeviceReset` | none | +| `BayerRG12p` | `BayerRG12Packed`: the GigE Vision 1.x packing, p0's high bits first - its own unpacking, see `RawImage.h` | +| chunk data, `DeviceReset` | none | The program looks a missing feature up under its older name, so `test camera` and the telemetry work. `export_camera_profile.py` picks its `gige` recipe by itself when a camera has `ExposureTimeAbs` and no `ExposureTime`: frame trigger off (the legacy cameras booted with `TriggerMode = On`, `Line1`), `BayerRG8` on -the colour camera and `Mono8` on a mono one, 60 MB/s each, the frame rate at +the colour camera (`BayerRG12Packed`, 12 bits, when it has it) and `Mono8` on a +mono one, 60 MB/s each, the frame rate at the camera's maximum (rietschen's RGB camera came set to 0.955 fps), and auto-exposure on the frame mean (`ExposureAutoAlg = Mean`) at 30 % +- 5 %, down to 20 us by day. The keep rule waits for the frame mean to reach the camera's diff --git a/include/fgc/RawImage.h b/include/fgc/RawImage.h index 90a2888..8adaf7c 100644 --- a/include/fgc/RawImage.h +++ b/include/fgc/RawImage.h @@ -17,21 +17,30 @@ namespace fgc { // BayerRG12p the same mosaic at 12 bits, packed: two pixels in three bytes // (GenICam "12p": p0 = b0 | (b1 & 0x0F) << 8, // p1 = b1 >> 4 | b2 << 4) +// BayerRG12Packed +// the same, packed the GigE Vision 1.x way that older GigE +// cameras use (the Manta G-1236C on rietschen): the first byte +// holds p0's HIGH bits, not its low ones +// p0 = b0 << 4 | (b1 & 0x0F), +// p1 = b2 << 4 | b1 >> 4 +// Same size as BayerRG12p; read as 12p it would scramble every +// other pixel. // Mono16 two bytes per pixel, grey, host byte order - the thermal // camera's counts (TarisCameraSource.h), all 16 bits used // // OpenCV names Bayer patterns by the second row, so the camera's BayerRG is // OpenCV's COLOR_BayerBG2* (measured 2026-09-30 against the camera's own RGB8). -enum class PixelLayout { Unknown, Mono8, RGB8, BayerRG8, BayerRG12p, Mono16 }; +enum class PixelLayout { Unknown, Mono8, RGB8, BayerRG8, BayerRG12p, Mono16, BayerRG12Packed }; -// The GenICam name ("BayerRG12p"), or "Unknown". +// The GenICam name ("BayerRG12p", "BayerRG12Packed"), or "Unknown". const char* layoutName(PixelLayout layout); // From a GenICam name; Unknown for anything the program does not handle. PixelLayout layoutFromName(const std::string& name); // True for the raw mosaic layouts, which need de-Bayering before saving as colour. bool layoutIsBayer(PixelLayout layout); -// Bits per value as delivered: 8, 12 for BayerRG12p, 16 for Mono16. 0 for Unknown. +// Bits per value as delivered: 8, 12 for the 12-bit mosaics, 16 for Mono16. 0 +// for Unknown. int layoutBits(PixelLayout layout); // Bytes one frame of width x height takes in this layout (0 for Unknown). size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height); @@ -40,6 +49,10 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height); // (0..4095). `in` holds ceil(pixels * 3 / 2) bytes; an odd last pixel uses the // low half of its pair, as GenICam specifies. void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels); +// The same for BayerRG12Packed (GigE Vision 1.x packing, see PixelLayout). +void unpack12Packed(const uint8_t* in, uint16_t* out, size_t pixels); +// Either of the two, by layout: the 12-bit mosaic unpacked to 16-bit words. +void unpack12(PixelLayout layout, const uint8_t* in, uint16_t* out, size_t pixels); // Brightness of a frame from a sparse sample of its pixels: every `stride`-th // pixel, so all colour sites of a mosaic are visited. Cheap enough to run on diff --git a/scripts/export_camera_profile.py b/scripts/export_camera_profile.py index 5438d89..0fc5ea6 100755 --- a/scripts/export_camera_profile.py +++ b/scripts/export_camera_profile.py @@ -141,7 +141,10 @@ _GIGE_COMMON = [ ("OffsetY", 0), ("Width", 100000), ("Height", 100000), - ("PixelFormat", ("BayerRG8", "Mono8")), + # 12 bits where the camera has them in the GigE packing the program reads + # (rietschen's RGB camera, chosen there 2026-10-26: 18.6 MB, 3.1 fps at + # 60 MB/s); 8 bits otherwise. The mono cameras' Mono12Packed is not read. + ("PixelFormat", ("BayerRG12Packed", "BayerRG8", "Mono8")), ("StreamBytesPerSecond", 60000000), # Free-running, a Manta still runs at AcquisitionFrameRateAbs, not at its # limit: rietschen's RGB camera came with 0.955 and delivered 5 frames in diff --git a/src/camera/ImagePipeline.cpp b/src/camera/ImagePipeline.cpp index 2cf7abc..e3d854a 100644 --- a/src/camera/ImagePipeline.cpp +++ b/src/camera/ImagePipeline.cpp @@ -231,14 +231,14 @@ bool ImagePipeline::save(const Frame& frame, const CameraOutput& out, const std: img = cv::Mat(h, w, CV_16UC1, data); bits = 16; distance = 0.f; - } else if (frame.layout == PixelLayout::BayerRG8 || frame.layout == PixelLayout::BayerRG12p) { + } else if (layoutIsBayer(frame.layout)) { cv::Mat mosaic; if (frame.layout == PixelLayout::BayerRG8) { mosaic = cv::Mat(h, w, CV_8UC1, data); } else { mosaic.create(h, w, CV_16UC1); - unpack12p(frame.data.data(), reinterpret_cast(mosaic.data), - static_cast(w) * h); + unpack12(frame.layout, frame.data.data(), reinterpret_cast(mosaic.data), + static_cast(w) * h); bits = 12; } // The camera's BayerRG is OpenCV's BayerBG (see RawImage.h). diff --git a/src/camera/VimbaCameraSource.cpp b/src/camera/VimbaCameraSource.cpp index 068de83..542040f 100644 --- a/src/camera/VimbaCameraSource.cpp +++ b/src/camera/VimbaCameraSource.cpp @@ -67,6 +67,8 @@ PixelLayout layoutOf(VmbPixelFormatType f) { case VmbPixelFormatRgb8: return PixelLayout::RGB8; case VmbPixelFormatBayerRG8: return PixelLayout::BayerRG8; case VmbPixelFormatBayerRG12p: return PixelLayout::BayerRG12p; + // The older GigE cameras' 12 bits (Manta): packed differently, see RawImage.h. + case VmbPixelFormatBayerRG12Packed: return PixelLayout::BayerRG12Packed; default: return PixelLayout::Unknown; } } @@ -125,7 +127,7 @@ public: warned_layout_ = true; LOG_WARN << "camera cam" << cam_id_ << ": pixel format 0x" << std::hex << pf << std::dec << " is not one the program handles " - << "(Mono8, RGB8, BayerRG8, BayerRG12p)"; + << "(Mono8, RGB8, BayerRG8, BayerRG12p, BayerRG12Packed)"; } } f.data.assign(buf, buf + bytes); diff --git a/src/core/RawImage.cpp b/src/core/RawImage.cpp index 62c24c2..238a29b 100644 --- a/src/core/RawImage.cpp +++ b/src/core/RawImage.cpp @@ -10,6 +10,7 @@ const char* layoutName(PixelLayout layout) { case PixelLayout::RGB8: return "RGB8"; case PixelLayout::BayerRG8: return "BayerRG8"; case PixelLayout::BayerRG12p: return "BayerRG12p"; + case PixelLayout::BayerRG12Packed: return "BayerRG12Packed"; case PixelLayout::Mono16: return "Mono16"; case PixelLayout::Unknown: break; } @@ -18,13 +19,15 @@ const char* layoutName(PixelLayout layout) { PixelLayout layoutFromName(const std::string& name) { for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8, - PixelLayout::BayerRG12p, PixelLayout::Mono16}) + PixelLayout::BayerRG12p, PixelLayout::BayerRG12Packed, + PixelLayout::Mono16}) if (name == layoutName(l)) return l; return PixelLayout::Unknown; } bool layoutIsBayer(PixelLayout layout) { - return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p; + return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p || + layout == PixelLayout::BayerRG12Packed; } int layoutBits(PixelLayout layout) { @@ -32,7 +35,8 @@ int layoutBits(PixelLayout layout) { case PixelLayout::Mono8: case PixelLayout::RGB8: case PixelLayout::BayerRG8: return 8; - case PixelLayout::BayerRG12p: return 12; + case PixelLayout::BayerRG12p: + case PixelLayout::BayerRG12Packed: return 12; case PixelLayout::Mono16: return 16; case PixelLayout::Unknown: break; } @@ -45,7 +49,8 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height) { case PixelLayout::Mono8: case PixelLayout::BayerRG8: return n; case PixelLayout::RGB8: return n * 3; - case PixelLayout::BayerRG12p: return (n * 3 + 1) / 2; + case PixelLayout::BayerRG12p: + case PixelLayout::BayerRG12Packed: return (n * 3 + 1) / 2; case PixelLayout::Mono16: return n * 2; case PixelLayout::Unknown: break; } @@ -66,6 +71,12 @@ inline uint16_t px12p(const uint8_t* in, size_t i) { : static_cast((p[1] >> 4) | (p[2] << 4)); } +inline uint16_t px12Packed(const uint8_t* in, size_t i) { + const uint8_t* p = in + (i >> 1) * 3; + return (i & 1) == 0 ? static_cast((p[0] << 4) | (p[1] & 0x0F)) + : static_cast((p[2] << 4) | (p[1] >> 4)); +} + } // namespace void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) { @@ -77,6 +88,20 @@ void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) { if (pixels & 1) out[0] = static_cast(in[0] | ((in[1] & 0x0F) << 8)); } +void unpack12Packed(const uint8_t* in, uint16_t* out, size_t pixels) { + const size_t pairs = pixels / 2; + for (size_t k = 0; k < pairs; ++k, in += 3, out += 2) { + out[0] = static_cast((in[0] << 4) | (in[1] & 0x0F)); + out[1] = static_cast((in[2] << 4) | (in[1] >> 4)); + } + if (pixels & 1) out[0] = static_cast((in[0] << 4) | (in[1] & 0x0F)); +} + +void unpack12(PixelLayout layout, const uint8_t* in, uint16_t* out, size_t pixels) { + if (layout == PixelLayout::BayerRG12Packed) unpack12Packed(in, out, pixels); + else unpack12p(in, out, pixels); +} + BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayout layout, uint32_t width, uint32_t height, size_t stride) { BrightnessSample s; @@ -90,9 +115,10 @@ BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayou double sum = 0; size_t hi = 0; for (size_t i = 0; i < values; i += stride) { - double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0 - : layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0 - : frame[i]; + double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0 + : layout == PixelLayout::BayerRG12Packed ? px12Packed(frame, i) / 16.0 + : layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0 + : frame[i]; sum += v; hi += v >= 250.0; ++s.samples; diff --git a/tests/test_rawimage.cpp b/tests/test_rawimage.cpp index 327dd4b..54ae248 100644 --- a/tests/test_rawimage.cpp +++ b/tests/test_rawimage.cpp @@ -22,11 +22,26 @@ std::vector pack12p(const std::vector& v) { 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 pack12Packed(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 >> 4); + out[o + 1] = static_cast((p0 & 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}) + 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); @@ -45,6 +60,10 @@ TEST_CASE("layout properties") { 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); @@ -85,6 +104,36 @@ TEST_CASE("unpack12p round-trips every value, odd counts included") { } } +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 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 = pack12Packed(part); + std::vector 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") { @@ -117,6 +166,12 @@ TEST_CASE("brightness sample: 12-bit is reported on the 0..255 scale") { 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") {