Read the Manta's 12 bits: BayerRG12Packed, packed the GigE Vision 1.x way
Rietschen's RGB camera switched to 12 bits offers BayerRG12Packed, not BayerRG12p, and the program took it for an unknown format: from 2026-10-26 11:33 every picture was saved as "legacy", undebayered raw bytes. The two are not the same format under two names. Both put two pixels in three bytes, but the GigE Vision 1.x packing carries the first pixel's high bits in the first byte, where GenICam's 12p carries its low ones; read as 12p every other pixel is scrambled. BayerRG12Packed is now a layout of its own, unpacked and sampled for brightness its own way and de-Bayered like 12p. Checked against the hardware on rietschen's NIR camera (Mono12Packed, the same packing): the 12-bit frame unpacked this way differs from an 8-bit frame of the same view by 2.8 on average, unpacked as 12p by 29. The gige recipe prefers BayerRG12Packed, so a re-export keeps the 12 bits. Tests 544/544 (mock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
df11808d78
commit
44b8a2e84a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -231,13 +231,13 @@ 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<uint16_t*>(mosaic.data),
|
||||
unpack12(frame.layout, frame.data.data(), reinterpret_cast<uint16_t*>(mosaic.data),
|
||||
static_cast<size_t>(w) * h);
|
||||
bits = 12;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<uint16_t>((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<uint16_t>((p[0] << 4) | (p[1] & 0x0F))
|
||||
: static_cast<uint16_t>((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<uint16_t>(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<uint16_t>((in[0] << 4) | (in[1] & 0x0F));
|
||||
out[1] = static_cast<uint16_t>((in[2] << 4) | (in[1] >> 4));
|
||||
}
|
||||
if (pixels & 1) out[0] = static_cast<uint16_t>((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;
|
||||
|
|
@ -91,6 +116,7 @@ BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayou
|
|||
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::BayerRG12Packed ? px12Packed(frame, i) / 16.0
|
||||
: layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0
|
||||
: frame[i];
|
||||
sum += v;
|
||||
|
|
|
|||
|
|
@ -22,11 +22,26 @@ std::vector<uint8_t> pack12p(const std::vector<uint16_t>& 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<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::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<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") {
|
||||
|
|
@ -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") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue