Compare commits
7 Commits
ed72f97fe5
...
2faa4508d9
| Author | SHA1 | Date |
|---|---|---|
|
|
2faa4508d9 | |
|
|
44b8a2e84a | |
|
|
df11808d78 | |
|
|
7332c47d6d | |
|
|
c62d8270b3 | |
|
|
fdb3c50b68 | |
|
|
8850337250 |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -231,13 +231,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -91,6 +116,7 @@ BrightnessSample sampleBrightness(const uint8_t* frame, size_t bytes, PixelLayou
|
||||||
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::BayerRG12Packed ? px12Packed(frame, i) / 16.0
|
||||||
: layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0
|
: layout == PixelLayout::Mono16 ? px16(frame, i) / 256.0
|
||||||
: frame[i];
|
: frame[i];
|
||||||
sum += v;
|
sum += v;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue