Merge support for the older GigE cameras (Manta) on rietschen

Rietschen runs three Allied Vision Manta G-1236 on one GbE port, whose
firmware predates the names the program and the Alvium use. A feature
missing by its SFNC name is looked up by the older one (ExposureTimeAbs,
AcquisitionFrameRateAbs, ExposureAutoTarget, ExposureAutoAdjustTol), and
export_camera_profile.py picks a gige recipe for such a camera: frame
trigger off, BayerRG12Packed or BayerRG8 or Mono8, 60 MB/s, the frame rate
at the camera's maximum, auto-exposure on the frame mean at 30 % +- 5 %.
BayerRG12Packed, the GigE Vision 1.x 12-bit packing, is read as a layout of
its own. The camera probe lists every camera and install-tower asks which
is the colour one.

Two fixes reach every tower: the keep rule now reads the camera's own
target, tolerance and limits after the camera is opened (before, it judged
every frame against 50 +- 5 % until the first profile switch), and test
hardware env waits one read period for a DHT11's first reading.

Running on rietschen since 2026-10-20, in 12 bits since 2026-10-26: 52
waypoints at 3 s, 2.9 min a sweep. Tests 544/544 (mock).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Peter Bochmann 2026-10-26 11:59:47 +01:00
commit 2faa4508d9
Signed by: peter.bochmann
GPG Key ID: 94E8C523489B8AC1
11 changed files with 362 additions and 43 deletions

View File

@ -714,6 +714,53 @@ feature vocabularies differ, so copying another tower's XML does not work:
| `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** | | `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** |
| Sensor | 12 MP | 16 MP, 5328x3040 | | Sensor | 12 MP | 16 MP, 5328x3040 |
### Older GigE cameras (Manta) `[partial]`
Rietschen has three Allied Vision Manta G-1236 on one GbE port (`enp1s0`,
MTU 8000): RGB (G-1236C), NIR and ACR (both G-1236B). Their firmware
(00.01.54) predates the names the program and the Alvium use:
| Program / Alvium | Manta |
|---|---|
| `ExposureTime` | `ExposureTimeAbs` |
| `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` | `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 (`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
own target and tolerance, read once the camera is open. With the RGB camera's
`FitRange`, which fits the histogram wherever its mean lands, no waypoint
settled. 30 rather than the Alvium's 50: at 50 the sky over the horizon burnt
out at noon, where smoke would rise, and at 35 it was still too bright; the old
program ran these cameras at 23-25.
Give `--camera` the id: with several cameras the first one the SDK lists is
not the colour one, and the order changes from run to run. `install.sh
--probe` lists them all, `install.sh --hardware-ini` writes `CHOOSE_ONE`
without `--camera-id`, and `install-tower` asks (`camera_id`).
What is still missing: a Manta has no `DeviceReset`, so the program cannot
reset a stuck camera (it says so and goes on); and without chunk data the keep
rule cannot tell a dark scene at the exposure limit, so at night it keeps the
newest frame after `frame_timeout_ms`.
`[verified]` on rietschen's NIR camera, 2026-10-20, with the motor mocked and
the old program running beside it: `test camera` PASS (5 frames 4112x3008
Mono8 in 1.7 s, dark 100 % <= 20, bright mean 255), `snapshot` writes a
3008x4112 grey JXL (`rotation_deg = 270`, as the old program rotated), and a
scan keeps a picture every 5 s without a timeout. The colour camera is not yet
tested: the old program has it open.
### Binning is not optional, and the frame-rate ceiling is not what it looks like ### Binning is not optional, and the frame-rate ceiling is not what it looks like
Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB

View File

@ -100,6 +100,9 @@ probe_mcu() {
# therefore taken only from a block whose transport layer is the USB or GigE # therefore taken only from a block whose transport layer is the USB or GigE
# TL. A tower configured with DEV_Cam1 would "find its camera" and capture # TL. A tower configured with DEV_Cam1 would "find its camera" and capture
# synthetic images forever. # synthetic images forever.
#
# One id per line, like probe_mcu: rietschen has three GigE cameras on one port,
# and the first one listed is not the colour one, so the caller has to choose.
probe_camera_id() { probe_camera_id() {
local tool local tool
for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do
@ -113,7 +116,7 @@ probe_camera_id() {
done done
"$tool" 2>/dev/null | awk ' "$tool" 2>/dev/null | awk '
/Camera ID/ { id = $NF } /Camera ID/ { id = $NF }
/Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) { print id; exit } } /Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) print id; id = "" }
' '
return return
done done
@ -134,7 +137,8 @@ probe_camera_lpm_path() {
probe_report() { probe_report() {
local board mcu cam lpm local board mcu cam lpm
board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)"; lpm="$(probe_camera_lpm_path)" board="$(probe_board)"; mcu="$(probe_mcu)"; lpm="$(probe_camera_lpm_path)"
cam="$(probe_camera_id | paste -sd ' ')"
echo "board : $board" echo "board : $board"
echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))" echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))"
echo "motor MCU : ${mcu:-NOT FOUND}" echo "motor MCU : ${mcu:-NOT FOUND}"
@ -270,7 +274,7 @@ do_vimba() {
local id; id="$(probe_camera_id)" local id; id="$(probe_camera_id)"
if [[ -n $id ]]; then if [[ -n $id ]]; then
say "camera found: $id" say "camera found: ${id//$'\n'/, }"
else else
warn "no camera reported by ListCameras yet" warn "no camera reported by ListCameras yet"
warn "the SDK asks for a reboot before the transport layers are usable" warn "the SDK asks for a reboot before the transport layers are usable"
@ -374,6 +378,11 @@ emit_hardware_ini() {
warn "$mcu" warn "$mcu"
mcu="CHOOSE_ONE" mcu="CHOOSE_ONE"
fi fi
if [[ $(wc -l <<< "$cam") -gt 1 ]]; then
warn "several cameras found; pick one with --camera-id:"
warn "$cam"
cam="CHOOSE_ONE"
fi
cat <<EOF cat <<EOF
; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh ; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh

View File

@ -17,21 +17,30 @@ namespace fgc {
// BayerRG12p the same mosaic at 12 bits, packed: two pixels in three bytes // BayerRG12p the same mosaic at 12 bits, packed: two pixels in three bytes
// (GenICam "12p": p0 = b0 | (b1 & 0x0F) << 8, // (GenICam "12p": p0 = b0 | (b1 & 0x0F) << 8,
// p1 = b1 >> 4 | b2 << 4) // 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 // Mono16 two bytes per pixel, grey, host byte order - the thermal
// camera's counts (TarisCameraSource.h), all 16 bits used // camera's counts (TarisCameraSource.h), all 16 bits used
// //
// OpenCV names Bayer patterns by the second row, so the camera's BayerRG is // 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). // 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); const char* layoutName(PixelLayout layout);
// From a GenICam name; Unknown for anything the program does not handle. // From a GenICam name; Unknown for anything the program does not handle.
PixelLayout layoutFromName(const std::string& name); PixelLayout layoutFromName(const std::string& name);
// True for the raw mosaic layouts, which need de-Bayering before saving as colour. // True for the raw mosaic layouts, which need de-Bayering before saving as colour.
bool layoutIsBayer(PixelLayout layout); 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); int layoutBits(PixelLayout layout);
// Bytes one frame of width x height takes in this layout (0 for Unknown). // 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); 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 // (0..4095). `in` holds ceil(pixels * 3 / 2) bytes; an odd last pixel uses the
// low half of its pair, as GenICam specifies. // low half of its pair, as GenICam specifies.
void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels); 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 // 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 // pixel, so all colour sites of a mosaic are visited. Cheap enough to run on

View File

@ -120,6 +120,102 @@ RECIPES = {
], ],
} }
# The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) name
# things differently - ExposureTimeAbs, AcquisitionFrameRateAbs, ExposureAutoTarget
# - and have no IntensityController, no DeviceLinkThroughputLimit and no 12-bit
# packed Bayer the program reads. Measured on rietschen's NIR camera, 2026-10-20.
# The link is shared: the camera free-runs as fast as StreamBytesPerSecond lets
# it (41 MB/s gave 3.2 fps at 12.4 MB a frame), and the cameras of one tower sit
# on one GbE port (~124 MB/s), so each gets half of it - room for RGB and NIR.
# PixelFormat takes the first the camera offers: raw Bayer on the colour camera,
# de-Bayered on the tower and a third of RGB8 on the wire; Mono8 on a mono one.
# The packet size is left alone: it has to fit the port's MTU, which is the
# tower's network setup, not the camera's.
_GIGE_COMMON = [
("TriggerSelector", "FrameStart"),
("TriggerMode", "Off"),
("AcquisitionMode", "Continuous"),
("BinningVertical", 1),
("BinningHorizontal", 1),
("OffsetX", 0),
("OffsetY", 0),
("Width", 100000),
("Height", 100000),
# 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
# 5.2 s. A short fixed exposure first, so the rate is not capped by the
# exposure the camera happens to hold, as in the Alvium recipe.
("ExposureAuto", "Off"),
("ExposureTimeAbs", 1000.0),
("AcquisitionFrameRateAbs", 1000.0), # clamped to the camera's maximum
("ChunkModeActive", False),
# The keep rule waits for the frame mean to reach the camera's target, +- its
# tolerance, which it reads from the camera. The camera has to aim at the
# mean: the RGB camera came with FitRange (which fits the histogram, wherever
# the mean lands) and no waypoint settled (2026-10-21). 30 % rather than the
# Alvium's 50: at 50 the sky over the horizon burnt out at noon, where smoke
# would rise, and at 35 it was still too bright; the old program ran these
# cameras at 23-25.
("ExposureAutoAlg", "Mean"),
("ExposureAutoAdjustTol", 5),
("ExposureAutoTarget", 30),
("ExposureAuto", "Continuous"),
("GainAuto", "Continuous"),
]
GIGE_RECIPES = {
"standard": _GIGE_COMMON + [
("ExposureAutoMax", 20000),
# Rietschen's RGB camera sat at 200-600 us at noon: a floor of 200 left it
# no room to darken a bright view, and without chunk data the host cannot
# tell a camera at its limit from one still settling. The Manta goes to 16.
("ExposureAutoMin", 20),
("GainAutoMax", 24.0),
],
"night": _GIGE_COMMON + [
("ExposureAutoMax", 500000),
("ExposureAutoMin", 1000),
("GainAutoMax", 48.0), # clamped to the camera's 40
],
}
def has_feature(cam, name):
try:
cam.get_feature_by_name(name)
return True
except Exception:
return False
def recipe_family(cam):
"""'gige' for a camera that only knows the older names, else 'alvium'."""
if not has_feature(cam, "ExposureTime") and has_feature(cam, "ExposureTimeAbs"):
return "gige"
return "alvium"
def resolve_value(feat, value):
"""The value to write to this feature; None if a choice list has no match.
A tuple is a list of choices for an enum: the first one the camera offers.
An integer feature takes no float, even a whole one (ExposureAutoMin is Int
on the Manta and Float on the Alvium)."""
if isinstance(value, tuple):
try:
offered = {str(e) for e in feat.get_available_entries()}
except Exception:
offered = set()
return next((v for v in value if v in offered), None)
if isinstance(feat, vmbpy.IntFeature) and isinstance(value, float):
return int(round(value))
return value
def apply_feature(cam, name, value, dry_run): def apply_feature(cam, name, value, dry_run):
try: try:
@ -131,6 +227,10 @@ def apply_feature(cam, name, value, dry_run):
except Exception: except Exception:
before = None before = None
value = resolve_value(feat, value)
if value is None:
return ("refused", name, before, "none of the choices is offered")
# Clamp a numeric value the camera cannot reach, rather than letting the # Clamp a numeric value the camera cannot reach, rather than letting the
# write fail and leaving whatever was there before. Without this, exporting # write fail and leaving whatever was there before. Without this, exporting
# 'standard' right after 'night' wrote a standard profile running at the # 'standard' right after 'night' wrote a standard profile running at the
@ -168,6 +268,8 @@ def main():
"fire_gimbal_control", "camera_profiles", "rgb"), "fire_gimbal_control", "camera_profiles", "rgb"),
help="directory to write <profile>.xml into") help="directory to write <profile>.xml into")
ap.add_argument("--camera", default=None, help="camera id (default: the first real one)") ap.add_argument("--camera", default=None, help="camera id (default: the first real one)")
ap.add_argument("--family", default="auto", choices=("auto", "alvium", "gige"),
help="which recipe set (default: from the features the camera has)")
ap.add_argument("--dry-run", action="store_true") ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args() args = ap.parse_args()
@ -191,10 +293,12 @@ def main():
cam = cams[0] cam = cams[0]
with cam: with cam:
family = recipe_family(cam) if args.family == "auto" else args.family
recipe = (GIGE_RECIPES if family == "gige" else RECIPES)[args.profile]
print(f"camera : {cam.get_id()} {cam.get_model()}") print(f"camera : {cam.get_id()} {cam.get_model()}")
print(f"profile : {args.profile}{' (dry run)' if args.dry_run else ''}") print(f"profile : {args.profile} ({family} recipe){' (dry run)' if args.dry_run else ''}")
refused = [] refused = []
for name, value in RECIPES[args.profile]: for name, value in recipe:
state, n, before, after = apply_feature(cam, name, value, args.dry_run) state, n, before, after = apply_feature(cam, name, value, args.dry_run)
mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X", mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X",
"clamped": " ~", "would-clamp": " ~"}[state] "clamped": " ~", "would-clamp": " ~"}[state]
@ -205,13 +309,14 @@ def main():
refused.append(n) refused.append(n)
# What the settings actually cost per frame - the number that decides # What the settings actually cost per frame - the number that decides
# whether this profile can be delivered over USB at all. # whether this profile can be delivered over the link at all.
try: try:
w = cam.get_feature_by_name("Width").get() w = cam.get_feature_by_name("Width").get()
h = cam.get_feature_by_name("Height").get() h = cam.get_feature_by_name("Height").get()
fmt = cam.get_feature_by_name("PixelFormat").get() fmt = cam.get_feature_by_name("PixelFormat").get()
bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1) bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1)
rate = cam.get_feature_by_name("AcquisitionFrameRate").get() rate_name = "AcquisitionFrameRateAbs" if family == "gige" else "AcquisitionFrameRate"
rate = cam.get_feature_by_name(rate_name).get()
mb = w * h * bpp / 1e6 mb = w * h * bpp / 1e6
print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame " print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame "
f"{rate:.2f} fps = {mb * rate:.0f} MB/s") f"{rate:.2f} fps = {mb * rate:.0f} MB/s")

View File

@ -334,22 +334,35 @@ set -euo pipefail
S_MCU="$(probe_mcu)" S_MCU="$(probe_mcu)"
S_MCU_N=$(grep -c . <<< "$S_MCU" || true) S_MCU_N=$(grep -c . <<< "$S_MCU" || true)
# Another program on the motor MCU's port - a legacy tower's old program, started # Another program on the motor MCU's port - a legacy tower's old program - would
# by start_gimbal.service - would fight this one for it, and the flash would # fight this one for it, and the flash would fail half-way. Our own program is
# fail half-way. Our own program is fine: it is stopped before anything needs # fine: it is stopped before anything needs the port. The old program is started
# the port. # differently from tower to tower: start_gimbal.service from
# ~/scripts/startup_gimbal.sh, which also runs the picture transfer, on
# staeffelsberg; gimbal_control.service on its own on rietschen.
if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then
for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do
args="$(ps -o args= -p "$pid" 2>/dev/null)" args="$(ps -o args= -p "$pid" 2>/dev/null)"
[[ $args == */fire_gimbal_control* ]] && continue [[ $args == */fire_gimbal_control* ]] && continue
die "the motor MCU's port is held by another program: pid $pid ($args). unit="$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')"
Stop it for good before installing. On a legacy tower it is started by if [[ $unit == start_gimbal.service ]]; then
start_gimbal.service from ~/scripts/startup_gimbal.sh, which also runs the picture how="It is started by start_gimbal.service from ~/scripts/startup_gimbal.sh, which
transfer: comment out the old program's line there, keep the transfer, then also runs the picture transfer: comment out the old program's line there, keep the
transfer, then
sudo systemctl restart start_gimbal" sudo systemctl restart start_gimbal"
elif [[ $unit == *.service ]]; then
how="It is started by $unit:
sudo systemctl disable --now $unit"
else
how="Find what starts it (systemctl status $pid) and stop that."
fi
die "the motor MCU's port is held by another program: pid $pid ($args).
Stop it for good before installing. $how"
done done
fi fi
S_CAM="$(probe_camera_id)" S_CAM="$(probe_camera_id)"
S_CAM_N=$(grep -c . <<< "$S_CAM" || true)
S_CAM_SHOWN="$(paste -sd ' ' <<< "$S_CAM")"
S_LPM="$(probe_camera_lpm_path)" S_LPM="$(probe_camera_lpm_path)"
S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')" S_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
S_PIO="$(pio_path || true)" S_PIO="$(pio_path || true)"
@ -377,7 +390,7 @@ board : $S_BOARD (kit hardware/$S_BOARD, firmware env $KIT_FW_ENV)
software / fw : $S_SW_REV / $S_FW_REV software / fw : $S_SW_REV / $S_FW_REV
motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE] motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE]
running firmware : ${S_FW_NOW:-unknown} running firmware : ${S_FW_NOW:-unknown}
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)} camera : ${S_CAM_SHOWN:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
usbfs_memory_mb : $S_USBFS usbfs_memory_mb : $S_USBFS
hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent) hardware.ini : $([[ -r $HW ]] && echo "present, $(hw_todos | grep -c . || true) TODO(s)" || echo absent)
config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent) config.ini : $([[ -r $CFG ]] && echo "$CFG, tower_name $(ini_get "$CFG" General tower_name 2>/dev/null || echo '?'), [Startup] home $(ini_get "$CFG" Startup home 2>/dev/null || echo unset), mode $(ini_get "$CFG" Startup mode 2>/dev/null || echo unset)" || echo absent)
@ -604,7 +617,11 @@ if [[ ! -r $HW ]]; then
ask serial "No motor MCU found - its /dev/serial/by-id path" ask serial "No motor MCU found - its /dev/serial/by-id path"
KIT_ARGS+=(--serial "$REPLY_VAL") KIT_ARGS+=(--serial "$REPLY_VAL")
fi fi
if [[ $CAMS_A != none && -z $S_CAM ]]; then if [[ $CAMS_A != none ]] && (( S_CAM_N > 1 )); then
echo "Several cameras:"; nl -w2 -s') ' <<< "$S_CAM"
ask camera_id "Which is the colour camera (its DEV_ id)"
KIT_ARGS+=(--camera-id "$REPLY_VAL")
elif [[ $CAMS_A != none && -z $S_CAM ]]; then
ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)" ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)"
KIT_ARGS+=(--camera-id "$REPLY_VAL") KIT_ARGS+=(--camera-id "$REPLY_VAL")
fi fi
@ -1092,9 +1109,13 @@ if [[ ${DO[profiles]} == change ]]; then
"$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then "$HOME/.venvs/vmbpy/bin/pip" install -q /opt/VimbaX/api/python/vmbpy-*.whl; }; }; then
PROFDIR="$(prof_dir)" # the config may have moved since the plan was made PROFDIR="$(prof_dir)" # the config may have moved since the plan was made
mkdir -p "$PROFDIR" mkdir -p "$PROFDIR"
# The camera hardware.ini names, not the first one the SDK lists: on a
# tower with several (rietschen: three GigE) that is another camera.
cam_id="$(ini_get "$HW" Camera.rgb id 2>/dev/null || true)"
cam_arg=(); [[ $cam_id == DEV_* ]] && cam_arg=(--camera "$cam_id")
for prof in "${PROF_MISSING[@]}"; do for prof in "${PROF_MISSING[@]}"; do
( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \ ( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \
scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" ) || scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" "${cam_arg[@]}" ) ||
warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)" warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)"
done done
done_step profiles done_step profiles

View File

@ -231,14 +231,14 @@ bool ImagePipeline::save(const Frame& frame, const CameraOutput& out, const std:
img = cv::Mat(h, w, CV_16UC1, data); img = cv::Mat(h, w, CV_16UC1, data);
bits = 16; bits = 16;
distance = 0.f; distance = 0.f;
} else if (frame.layout == PixelLayout::BayerRG8 || frame.layout == PixelLayout::BayerRG12p) { } else if (layoutIsBayer(frame.layout)) {
cv::Mat mosaic; cv::Mat mosaic;
if (frame.layout == PixelLayout::BayerRG8) { if (frame.layout == PixelLayout::BayerRG8) {
mosaic = cv::Mat(h, w, CV_8UC1, data); mosaic = cv::Mat(h, w, CV_8UC1, data);
} else { } else {
mosaic.create(h, w, CV_16UC1); 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); static_cast<size_t>(w) * h);
bits = 12; bits = 12;
} }
// The camera's BayerRG is OpenCV's BayerBG (see RawImage.h). // The camera's BayerRG is OpenCV's BayerBG (see RawImage.h).

View File

@ -67,6 +67,8 @@ PixelLayout layoutOf(VmbPixelFormatType f) {
case VmbPixelFormatRgb8: return PixelLayout::RGB8; case VmbPixelFormatRgb8: return PixelLayout::RGB8;
case VmbPixelFormatBayerRG8: return PixelLayout::BayerRG8; case VmbPixelFormatBayerRG8: return PixelLayout::BayerRG8;
case VmbPixelFormatBayerRG12p: return PixelLayout::BayerRG12p; 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; default: return PixelLayout::Unknown;
} }
} }
@ -125,7 +127,7 @@ public:
warned_layout_ = true; warned_layout_ = true;
LOG_WARN << "camera cam" << cam_id_ << ": pixel format 0x" << std::hex LOG_WARN << "camera cam" << cam_id_ << ": pixel format 0x" << std::hex
<< pf << std::dec << " is not one the program handles " << pf << std::dec << " is not one the program handles "
<< "(Mono8, RGB8, BayerRG8, BayerRG12p)"; << "(Mono8, RGB8, BayerRG8, BayerRG12p, BayerRG12Packed)";
} }
} }
f.data.assign(buf, buf + bytes); f.data.assign(buf, buf + bytes);
@ -188,12 +190,37 @@ private:
bool has_ = false; bool has_ = false;
}; };
// The older GigE cameras (Manta G-1236 on rietschen, firmware 00.01.54) predate
// the SFNC names the program uses and the Alvium answers to. Where a camera has
// no feature by the program's name, it is looked up by the older one. The
// IntensityController pair is the Alvium's auto-exposure target and tolerance;
// the Manta's ExposureAuto* pair means the same, in percent too.
const char* legacyFeatureName(const std::string& name) {
static const std::pair<const char*, const char*> kAliases[] = {
{"ExposureTime", "ExposureTimeAbs"},
{"AcquisitionFrameRate", "AcquisitionFrameRateAbs"},
{"IntensityControllerTarget", "ExposureAutoTarget"},
{"IntensityControllerTolerance", "ExposureAutoAdjustTol"},
};
for (const auto& [sfnc, legacy] : kAliases)
if (name == sfnc) return legacy;
return nullptr;
}
// A feature by the program's name, or by its older name on a camera that has only
// that one.
bool findFeature(const CameraPtr& cam, const std::string& name, FeaturePtr& f) {
if (SP_ACCESS(cam)->GetFeatureByName(name.c_str(), f) == VmbErrorSuccess) return true;
const char* legacy = legacyFeatureName(name);
return legacy && SP_ACCESS(cam)->GetFeatureByName(legacy, f) == VmbErrorSuccess;
}
// Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for // Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for
// telemetry). Numeric takes an integer feature too: some are Int on one model and // telemetry). Numeric takes an integer feature too: some are Int on one model and
// Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c). // Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c).
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) { double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
FeaturePtr f; FeaturePtr f;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback; if (!findFeature(cam, name, f)) return fallback;
double v = fallback; double v = fallback;
if (f->GetValue(v) == VmbErrorSuccess) return v; if (f->GetValue(v) == VmbErrorSuccess) return v;
VmbInt64_t i = 0; VmbInt64_t i = 0;
@ -203,7 +230,7 @@ double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
std::string getString(const CameraPtr& cam, const char* name) { std::string getString(const CameraPtr& cam, const char* name) {
FeaturePtr f; FeaturePtr f;
std::string v; std::string v;
if (SP_ACCESS(cam)->GetFeatureByName(name, f) == VmbErrorSuccess && f->GetValue(v) == VmbErrorSuccess) if (findFeature(cam, name, f) && f->GetValue(v) == VmbErrorSuccess)
return v; return v;
return {}; return {};
} }
@ -649,8 +676,7 @@ bool VimbaCameraSource::setFeature(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
// The value before the first change, so restoreProfile() can put it back // The value before the first change, so restoreProfile() can put it back
// live. Only the first: a second change must not record the test's own value. // live. Only the first: a second change must not record the test's own value.
@ -741,8 +767,7 @@ bool VimbaCameraSource::readFeature(int camera_index, const std::string& name, d
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -760,8 +785,7 @@ bool VimbaCameraSource::featureRange(int camera_index, const std::string& name,
if (camera_index < 0 || idx >= impl_->cameras.size()) return false; if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
f->GetDataType(type) != VmbErrorSuccess)
return false; return false;
if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess; if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess;
if (type == VmbFeatureDataInt) { if (type == VmbFeatureDataInt) {
@ -788,7 +812,7 @@ bool VimbaCameraSource::restoreProfile(int camera_index) {
for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) { for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) {
FeaturePtr f; FeaturePtr f;
VmbFeatureDataType type = VmbFeatureDataUnknown; VmbFeatureDataType type = VmbFeatureDataUnknown;
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(it->first.c_str(), f) != VmbErrorSuccess || if (!findFeature(impl_->cameras[idx], it->first, f) ||
f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; } f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; }
VmbErrorType rc = VmbErrorWrongType; VmbErrorType rc = VmbErrorWrongType;
try { try {

View File

@ -3627,6 +3627,12 @@ struct Application::Impl {
return 3; // distinct from the config (1) and lock (2) exits return 3; // distinct from the config (1) and lock (2) exits
} }
// Again, now the camera is open: the keep rule reads the camera's own
// auto-exposure target, tolerance and limits, and before open() it got
// none of them and judged every frame against its 50 +- 5 % default -
// until the first profile switch. Rietschen's cameras aim at 35 %.
configureSettle();
pipeline->start(); pipeline->start();
channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise

View File

@ -10,6 +10,7 @@ const char* layoutName(PixelLayout layout) {
case PixelLayout::RGB8: return "RGB8"; case PixelLayout::RGB8: return "RGB8";
case PixelLayout::BayerRG8: return "BayerRG8"; case PixelLayout::BayerRG8: return "BayerRG8";
case PixelLayout::BayerRG12p: return "BayerRG12p"; case PixelLayout::BayerRG12p: return "BayerRG12p";
case PixelLayout::BayerRG12Packed: return "BayerRG12Packed";
case PixelLayout::Mono16: return "Mono16"; case PixelLayout::Mono16: return "Mono16";
case PixelLayout::Unknown: break; case PixelLayout::Unknown: break;
} }
@ -18,13 +19,15 @@ const char* layoutName(PixelLayout layout) {
PixelLayout layoutFromName(const std::string& name) { PixelLayout layoutFromName(const std::string& name) {
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8, 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; if (name == layoutName(l)) return l;
return PixelLayout::Unknown; return PixelLayout::Unknown;
} }
bool layoutIsBayer(PixelLayout layout) { 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) { int layoutBits(PixelLayout layout) {
@ -32,7 +35,8 @@ int layoutBits(PixelLayout layout) {
case PixelLayout::Mono8: case PixelLayout::Mono8:
case PixelLayout::RGB8: case PixelLayout::RGB8:
case PixelLayout::BayerRG8: return 8; 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::Mono16: return 16;
case PixelLayout::Unknown: break; case PixelLayout::Unknown: break;
} }
@ -45,7 +49,8 @@ size_t layoutFrameBytes(PixelLayout layout, uint32_t width, uint32_t height) {
case PixelLayout::Mono8: case PixelLayout::Mono8:
case PixelLayout::BayerRG8: return n; case PixelLayout::BayerRG8: return n;
case PixelLayout::RGB8: return n * 3; 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::Mono16: return n * 2;
case PixelLayout::Unknown: break; 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)); : 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 } // namespace
void unpack12p(const uint8_t* in, uint16_t* out, size_t pixels) { 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)); 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, BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayout layout,
uint32_t width, uint32_t height, size_t stride) { uint32_t width, uint32_t height, size_t stride) {
BrightnessSample s; BrightnessSample s;
@ -90,9 +115,10 @@ BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayou
double sum = 0; double sum = 0;
size_t hi = 0; size_t hi = 0;
for (size_t i = 0; i < values; i += stride) { for (size_t i = 0; i < values; i += stride) {
double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0 double v = layout == PixelLayout::BayerRG12p ? px12p(frame, i) / 16.0
: layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0 : layout == PixelLayout::BayerRG12Packed ? px12Packed(frame, i) / 16.0
: frame[i]; : layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0
: frame[i];
sum += v; sum += v;
hi += v >= 250.0; hi += v >= 250.0;
++s.samples; ++s.samples;

View File

@ -1097,13 +1097,26 @@ void TestRunner::testHwEnv(TestReport& r) {
} }
setProgress(s.id, 2, 2, "reading sensor"); setProgress(s.id, 2, 2, "reading sensor");
std::optional<EnvSample> sample = hw_.env ? hw_.env->sample() : std::nullopt;
// The firmware reads its DHT11 every 10 s, the first time 2 s after it
// starts, so a run straight after the host connected may have no reading
// yet: install-tower --verify failed on rietschen 1.4 s into its run, 36 s
// after the same test had passed. Wait one read period before calling it
// missing.
if (hw_.env && !sample && on_mcu) {
const auto deadline = clock_t_::now() + 12s;
while (!sample && clock_t_::now() < deadline && !cancelled()) {
std::this_thread::sleep_for(200ms);
sample = hw_.env->sample();
}
}
CheckResult m; CheckResult m;
if (!hw_.env) { if (!hw_.env) {
m.pass = false; m.pass = false;
m.detail = "declared as '" + hw_.declared->env + m.detail = "declared as '" + hw_.declared->env +
"' but no sensor was built -- check [Env] i2c_device; the declaration itself is " "' but no sensor was built -- check [Env] i2c_device; the declaration itself is "
"what enables it"; "what enables it";
} else if (auto sample = hw_.env->sample()) { } else if (sample) {
m.detail = "CRC-valid reading from " + hw_.env->name(); m.detail = "CRC-valid reading from " + hw_.env->name();
} else if (on_mcu) { } else if (on_mcu) {
m.pass = false; m.pass = false;

View File

@ -22,11 +22,26 @@ std::vector<uint8_t> pack12p(const std::vector<uint16_t>& v) {
return out; 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 } // namespace
TEST_CASE("layout names round-trip, and unknown formats are refused") { TEST_CASE("layout names round-trip, and unknown formats are refused") {
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8, 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(layoutName(l)) == l);
CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled
CHECK(layoutFromName("YCbCr422_8_CbYCrY") == PixelLayout::Unknown); 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::BayerRG8, 5328, 3040) == 16197120);
CHECK(layoutFrameBytes(PixelLayout::RGB8, 5328, 3040) == 48591360); CHECK(layoutFrameBytes(PixelLayout::RGB8, 5328, 3040) == 48591360);
CHECK(layoutFrameBytes(PixelLayout::BayerRG12p, 3, 1) == 5); // odd pixel count rounds up 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. // The thermal camera: 640 x 480 counts, two bytes each.
CHECK_FALSE(layoutIsBayer(PixelLayout::Mono16)); CHECK_FALSE(layoutIsBayer(PixelLayout::Mono16));
CHECK(layoutBits(PixelLayout::Mono16) == 16); 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") { TEST_CASE("brightness sample: 8-bit layouts") {
const uint32_t w = 400, h = 300; const uint32_t w = 400, h = 300;
SUBCASE("mid grey") { 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); s = sampleBrightness(f.data(), f.size(), PixelLayout::BayerRG12p, w, h);
CHECK(s.mean == doctest::Approx(4095 / 16.0)); CHECK(s.mean == doctest::Approx(4095 / 16.0));
CHECK(s.clipped == 1); 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") { TEST_CASE("brightness sample refuses what it cannot read") {