Compare commits
No commits in common. "2faa4508d9d937353e196c22844be1b085ea7c0f" and "ed72f97fe5ce37188e2b1ae8e8c8ee3b8326e94a" have entirely different histories.
2faa4508d9
...
ed72f97fe5
|
|
@ -714,53 +714,6 @@ feature vocabularies differ, so copying another tower's XML does not work:
|
|||
| `IntensityAutoPrecedence` | `Minimizeexposuretime` / `Minimizenoise` | **`MinimizeBlur` / `MinimizeNoise`** |
|
||||
| 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
|
||||
|
||||
Unbinned RGB8 on the 16 MP sensor is a **48.6 MB frame** - over the ~20 MB
|
||||
|
|
|
|||
|
|
@ -100,9 +100,6 @@ probe_mcu() {
|
|||
# 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
|
||||
# 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() {
|
||||
local tool
|
||||
for tool in /opt/VimbaX*/bin/ListCameras_VmbC /opt/VimbaX*/bin/ListCameras; do
|
||||
|
|
@ -116,7 +113,7 @@ probe_camera_id() {
|
|||
done
|
||||
"$tool" 2>/dev/null | awk '
|
||||
/Camera ID/ { id = $NF }
|
||||
/Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) print id; id = "" }
|
||||
/Transport Layer Path/ { if (id != "" && $NF !~ /Simulator/) { print id; exit } }
|
||||
'
|
||||
return
|
||||
done
|
||||
|
|
@ -137,8 +134,7 @@ probe_camera_lpm_path() {
|
|||
|
||||
probe_report() {
|
||||
local board mcu cam lpm
|
||||
board="$(probe_board)"; mcu="$(probe_mcu)"; lpm="$(probe_camera_lpm_path)"
|
||||
cam="$(probe_camera_id | paste -sd ' ')"
|
||||
board="$(probe_board)"; mcu="$(probe_mcu)"; cam="$(probe_camera_id)"; lpm="$(probe_camera_lpm_path)"
|
||||
echo "board : $board"
|
||||
echo "os : $(. /etc/os-release && echo "$PRETTY_NAME") ($(uname -r))"
|
||||
echo "motor MCU : ${mcu:-NOT FOUND}"
|
||||
|
|
@ -274,7 +270,7 @@ do_vimba() {
|
|||
|
||||
local id; id="$(probe_camera_id)"
|
||||
if [[ -n $id ]]; then
|
||||
say "camera found: ${id//$'\n'/, }"
|
||||
say "camera found: $id"
|
||||
else
|
||||
warn "no camera reported by ListCameras yet"
|
||||
warn "the SDK asks for a reboot before the transport layers are usable"
|
||||
|
|
@ -378,11 +374,6 @@ emit_hardware_ini() {
|
|||
warn "$mcu"
|
||||
mcu="CHOOSE_ONE"
|
||||
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
|
||||
; hardware.ini - what this machine IS. Generated by hardware/$KIT_BOARD/install.sh
|
||||
|
|
|
|||
|
|
@ -17,30 +17,21 @@ 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, BayerRG12Packed };
|
||||
enum class PixelLayout { Unknown, Mono8, RGB8, BayerRG8, BayerRG12p, Mono16 };
|
||||
|
||||
// The GenICam name ("BayerRG12p", "BayerRG12Packed"), or "Unknown".
|
||||
// The GenICam name ("BayerRG12p"), 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 the 12-bit mosaics, 16 for Mono16. 0
|
||||
// for Unknown.
|
||||
// Bits per value as delivered: 8, 12 for BayerRG12p, 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);
|
||||
|
|
@ -49,10 +40,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -120,102 +120,6 @@ 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):
|
||||
try:
|
||||
|
|
@ -227,10 +131,6 @@ def apply_feature(cam, name, value, dry_run):
|
|||
except Exception:
|
||||
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
|
||||
# write fail and leaving whatever was there before. Without this, exporting
|
||||
# 'standard' right after 'night' wrote a standard profile running at the
|
||||
|
|
@ -268,8 +168,6 @@ def main():
|
|||
"fire_gimbal_control", "camera_profiles", "rgb"),
|
||||
help="directory to write <profile>.xml into")
|
||||
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")
|
||||
args = ap.parse_args()
|
||||
|
||||
|
|
@ -293,12 +191,10 @@ def main():
|
|||
cam = cams[0]
|
||||
|
||||
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"profile : {args.profile} ({family} recipe){' (dry run)' if args.dry_run else ''}")
|
||||
print(f"profile : {args.profile}{' (dry run)' if args.dry_run else ''}")
|
||||
refused = []
|
||||
for name, value in recipe:
|
||||
for name, value in RECIPES[args.profile]:
|
||||
state, n, before, after = apply_feature(cam, name, value, args.dry_run)
|
||||
mark = {"set": " ", "would-set": " ?", "missing": " !", "refused": " X",
|
||||
"clamped": " ~", "would-clamp": " ~"}[state]
|
||||
|
|
@ -309,14 +205,13 @@ def main():
|
|||
refused.append(n)
|
||||
|
||||
# What the settings actually cost per frame - the number that decides
|
||||
# whether this profile can be delivered over the link at all.
|
||||
# whether this profile can be delivered over USB at all.
|
||||
try:
|
||||
w = cam.get_feature_by_name("Width").get()
|
||||
h = cam.get_feature_by_name("Height").get()
|
||||
fmt = cam.get_feature_by_name("PixelFormat").get()
|
||||
bpp = {"RGB8": 3, "BGR8": 3, "BayerRG12p": 1.5, "BayerRG12": 2}.get(str(fmt), 1)
|
||||
rate_name = "AcquisitionFrameRateAbs" if family == "gige" else "AcquisitionFrameRate"
|
||||
rate = cam.get_feature_by_name(rate_name).get()
|
||||
rate = cam.get_feature_by_name("AcquisitionFrameRate").get()
|
||||
mb = w * h * bpp / 1e6
|
||||
print(f"\nresult : {w}x{h} {fmt} {mb:.1f} MB/frame "
|
||||
f"{rate:.2f} fps = {mb * rate:.0f} MB/s")
|
||||
|
|
|
|||
|
|
@ -334,35 +334,22 @@ set -euo pipefail
|
|||
|
||||
S_MCU="$(probe_mcu)"
|
||||
S_MCU_N=$(grep -c . <<< "$S_MCU" || true)
|
||||
# Another program on the motor MCU's port - a legacy tower's old program - would
|
||||
# fight this one for it, and the flash would fail half-way. Our own program is
|
||||
# fine: it is stopped before anything needs the port. The old program is started
|
||||
# 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.
|
||||
# Another program on the motor MCU's port - a legacy tower's old program, started
|
||||
# by start_gimbal.service - would fight this one for it, and the flash would
|
||||
# fail half-way. Our own program is fine: it is stopped before anything needs
|
||||
# the port.
|
||||
if (( S_MCU_N == 1 )) && [[ -e $S_MCU ]]; then
|
||||
for pid in $(fuser "$(readlink -f "$S_MCU")" 2>/dev/null); do
|
||||
args="$(ps -o args= -p "$pid" 2>/dev/null)"
|
||||
[[ $args == */fire_gimbal_control* ]] && continue
|
||||
unit="$(ps -o unit= -p "$pid" 2>/dev/null | tr -d ' ')"
|
||||
if [[ $unit == start_gimbal.service ]]; then
|
||||
how="It is started by start_gimbal.service from ~/scripts/startup_gimbal.sh, which
|
||||
also runs the picture transfer: comment out the old program's line there, keep the
|
||||
transfer, then
|
||||
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"
|
||||
Stop it for good before installing. On a legacy tower it is started by
|
||||
start_gimbal.service from ~/scripts/startup_gimbal.sh, which also runs the picture
|
||||
transfer: comment out the old program's line there, keep the transfer, then
|
||||
sudo systemctl restart start_gimbal"
|
||||
done
|
||||
fi
|
||||
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_USBFS="$(cat /sys/module/usbcore/parameters/usbfs_memory_mb 2>/dev/null || echo '?')"
|
||||
S_PIO="$(pio_path || true)"
|
||||
|
|
@ -390,7 +377,7 @@ board : $S_BOARD (kit hardware/$S_BOARD, firmware env $KIT_FW_ENV)
|
|||
software / fw : $S_SW_REV / $S_FW_REV
|
||||
motor MCU : ${S_MCU:-NOT FOUND} [$S_MODE]
|
||||
running firmware : ${S_FW_NOW:-unknown}
|
||||
camera : ${S_CAM_SHOWN:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
|
||||
camera : ${S_CAM:-not found}${S_LPM:+ (LPM $(cat "$S_LPM") at $S_LPM)}
|
||||
usbfs_memory_mb : $S_USBFS
|
||||
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)
|
||||
|
|
@ -617,11 +604,7 @@ if [[ ! -r $HW ]]; then
|
|||
ask serial "No motor MCU found - its /dev/serial/by-id path"
|
||||
KIT_ARGS+=(--serial "$REPLY_VAL")
|
||||
fi
|
||||
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
|
||||
if [[ $CAMS_A != none && -z $S_CAM ]]; then
|
||||
ask camera_id "Camera DEV_ id (none found; the SDK may need a reboot first)"
|
||||
KIT_ARGS+=(--camera-id "$REPLY_VAL")
|
||||
fi
|
||||
|
|
@ -1109,13 +1092,9 @@ if [[ ${DO[profiles]} == change ]]; 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
|
||||
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
|
||||
( cd "$REPO" && GENICAM_GENTL64_PATH=/opt/VimbaX/cti "$HOME/.venvs/vmbpy/bin/python" \
|
||||
scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" "${cam_arg[@]}" ) ||
|
||||
scripts/export_camera_profile.py --profile "$prof" --out "$PROFDIR" ) ||
|
||||
warn "could not export the $prof profile; export it later (docs/tower-installation.md section 8)"
|
||||
done
|
||||
done_step profiles
|
||||
|
|
|
|||
|
|
@ -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 (layoutIsBayer(frame.layout)) {
|
||||
} else if (frame.layout == PixelLayout::BayerRG8 || frame.layout == PixelLayout::BayerRG12p) {
|
||||
cv::Mat mosaic;
|
||||
if (frame.layout == PixelLayout::BayerRG8) {
|
||||
mosaic = cv::Mat(h, w, CV_8UC1, data);
|
||||
} else {
|
||||
mosaic.create(h, w, CV_16UC1);
|
||||
unpack12(frame.layout, frame.data.data(), reinterpret_cast<uint16_t*>(mosaic.data),
|
||||
static_cast<size_t>(w) * h);
|
||||
unpack12p(frame.data.data(), reinterpret_cast<uint16_t*>(mosaic.data),
|
||||
static_cast<size_t>(w) * h);
|
||||
bits = 12;
|
||||
}
|
||||
// The camera's BayerRG is OpenCV's BayerBG (see RawImage.h).
|
||||
|
|
|
|||
|
|
@ -67,8 +67,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +125,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, BayerRG12Packed)";
|
||||
<< "(Mono8, RGB8, BayerRG8, BayerRG12p)";
|
||||
}
|
||||
}
|
||||
f.data.assign(buf, buf + bytes);
|
||||
|
|
@ -190,37 +188,12 @@ private:
|
|||
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
|
||||
// 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).
|
||||
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
|
||||
FeaturePtr f;
|
||||
if (!findFeature(cam, name, f)) return fallback;
|
||||
if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback;
|
||||
double v = fallback;
|
||||
if (f->GetValue(v) == VmbErrorSuccess) return v;
|
||||
VmbInt64_t i = 0;
|
||||
|
|
@ -230,7 +203,7 @@ double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
|
|||
std::string getString(const CameraPtr& cam, const char* name) {
|
||||
FeaturePtr f;
|
||||
std::string v;
|
||||
if (findFeature(cam, name, f) && f->GetValue(v) == VmbErrorSuccess)
|
||||
if (SP_ACCESS(cam)->GetFeatureByName(name, f) == VmbErrorSuccess && f->GetValue(v) == VmbErrorSuccess)
|
||||
return v;
|
||||
return {};
|
||||
}
|
||||
|
|
@ -676,7 +649,8 @@ bool VimbaCameraSource::setFeature(int camera_index, const std::string& name,
|
|||
if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
|
||||
FeaturePtr f;
|
||||
VmbFeatureDataType type = VmbFeatureDataUnknown;
|
||||
if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
|
||||
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess ||
|
||||
f->GetDataType(type) != VmbErrorSuccess)
|
||||
return false;
|
||||
// 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.
|
||||
|
|
@ -767,7 +741,8 @@ bool VimbaCameraSource::readFeature(int camera_index, const std::string& name, d
|
|||
if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
|
||||
FeaturePtr f;
|
||||
VmbFeatureDataType type = VmbFeatureDataUnknown;
|
||||
if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
|
||||
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess ||
|
||||
f->GetDataType(type) != VmbErrorSuccess)
|
||||
return false;
|
||||
if (type == VmbFeatureDataFloat) return f->GetValue(value) == VmbErrorSuccess;
|
||||
if (type == VmbFeatureDataInt) {
|
||||
|
|
@ -785,7 +760,8 @@ bool VimbaCameraSource::featureRange(int camera_index, const std::string& name,
|
|||
if (camera_index < 0 || idx >= impl_->cameras.size()) return false;
|
||||
FeaturePtr f;
|
||||
VmbFeatureDataType type = VmbFeatureDataUnknown;
|
||||
if (!findFeature(impl_->cameras[idx], name, f) || f->GetDataType(type) != VmbErrorSuccess)
|
||||
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(name.c_str(), f) != VmbErrorSuccess ||
|
||||
f->GetDataType(type) != VmbErrorSuccess)
|
||||
return false;
|
||||
if (type == VmbFeatureDataFloat) return f->GetRange(min, max) == VmbErrorSuccess;
|
||||
if (type == VmbFeatureDataInt) {
|
||||
|
|
@ -812,7 +788,7 @@ bool VimbaCameraSource::restoreProfile(int camera_index) {
|
|||
for (auto it = undo.rbegin(); ok && it != undo.rend(); ++it) {
|
||||
FeaturePtr f;
|
||||
VmbFeatureDataType type = VmbFeatureDataUnknown;
|
||||
if (!findFeature(impl_->cameras[idx], it->first, f) ||
|
||||
if (SP_ACCESS(impl_->cameras[idx])->GetFeatureByName(it->first.c_str(), f) != VmbErrorSuccess ||
|
||||
f->GetDataType(type) != VmbErrorSuccess) { ok = false; break; }
|
||||
VmbErrorType rc = VmbErrorWrongType;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3627,12 +3627,6 @@ struct Application::Impl {
|
|||
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();
|
||||
channel->publishStatus(static_cast<int>(Mode::Manual)); // until [Startup] mode says otherwise
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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;
|
||||
}
|
||||
|
|
@ -19,15 +18,13 @@ const char* layoutName(PixelLayout layout) {
|
|||
|
||||
PixelLayout layoutFromName(const std::string& name) {
|
||||
for (PixelLayout l : {PixelLayout::Mono8, PixelLayout::RGB8, PixelLayout::BayerRG8,
|
||||
PixelLayout::BayerRG12p, PixelLayout::BayerRG12Packed,
|
||||
PixelLayout::Mono16})
|
||||
PixelLayout::BayerRG12p, PixelLayout::Mono16})
|
||||
if (name == layoutName(l)) return l;
|
||||
return PixelLayout::Unknown;
|
||||
}
|
||||
|
||||
bool layoutIsBayer(PixelLayout layout) {
|
||||
return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p ||
|
||||
layout == PixelLayout::BayerRG12Packed;
|
||||
return layout == PixelLayout::BayerRG8 || layout == PixelLayout::BayerRG12p;
|
||||
}
|
||||
|
||||
int layoutBits(PixelLayout layout) {
|
||||
|
|
@ -35,8 +32,7 @@ int layoutBits(PixelLayout layout) {
|
|||
case PixelLayout::Mono8:
|
||||
case PixelLayout::RGB8:
|
||||
case PixelLayout::BayerRG8: return 8;
|
||||
case PixelLayout::BayerRG12p:
|
||||
case PixelLayout::BayerRG12Packed: return 12;
|
||||
case PixelLayout::BayerRG12p: return 12;
|
||||
case PixelLayout::Mono16: return 16;
|
||||
case PixelLayout::Unknown: break;
|
||||
}
|
||||
|
|
@ -49,8 +45,7 @@ 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:
|
||||
case PixelLayout::BayerRG12Packed: return (n * 3 + 1) / 2;
|
||||
case PixelLayout::BayerRG12p: return (n * 3 + 1) / 2;
|
||||
case PixelLayout::Mono16: return n * 2;
|
||||
case PixelLayout::Unknown: break;
|
||||
}
|
||||
|
|
@ -71,12 +66,6 @@ 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) {
|
||||
|
|
@ -88,20 +77,6 @@ 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;
|
||||
|
|
@ -115,10 +90,9 @@ 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::BayerRG12Packed ? px12Packed(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::Mono16 ? px16(frame, i) / 256.0
|
||||
: frame[i];
|
||||
sum += v;
|
||||
hi += v >= 250.0;
|
||||
++s.samples;
|
||||
|
|
|
|||
|
|
@ -1097,26 +1097,13 @@ void TestRunner::testHwEnv(TestReport& r) {
|
|||
}
|
||||
|
||||
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;
|
||||
if (!hw_.env) {
|
||||
m.pass = false;
|
||||
m.detail = "declared as '" + hw_.declared->env +
|
||||
"' but no sensor was built -- check [Env] i2c_device; the declaration itself is "
|
||||
"what enables it";
|
||||
} else if (sample) {
|
||||
} else if (auto sample = hw_.env->sample()) {
|
||||
m.detail = "CRC-valid reading from " + hw_.env->name();
|
||||
} else if (on_mcu) {
|
||||
m.pass = false;
|
||||
|
|
|
|||
|
|
@ -22,26 +22,11 @@ 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::BayerRG12Packed,
|
||||
PixelLayout::Mono16})
|
||||
PixelLayout::BayerRG12p, PixelLayout::Mono16})
|
||||
CHECK(layoutFromName(layoutName(l)) == l);
|
||||
CHECK(layoutFromName("BayerRG12") == PixelLayout::Unknown); // unpacked: not handled
|
||||
CHECK(layoutFromName("YCbCr422_8_CbYCrY") == PixelLayout::Unknown);
|
||||
|
|
@ -60,10 +45,6 @@ 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);
|
||||
|
|
@ -104,36 +85,6 @@ 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") {
|
||||
|
|
@ -166,12 +117,6 @@ 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