From fff437c13c4a7d1a8fadac3a86292d4d5dd7ca35 Mon Sep 17 00:00:00 2001 From: Peter Bochmann Date: Mon, 26 Oct 2026 14:08:35 +0100 Subject: [PATCH] Let a tower choose its camera's auto-exposure target: --target The Alvium recipe sets no IntensityControllerTarget, so a camera kept its default of 50 %, which burns out the sky over a dark forest; staeffelsberg's colour camera runs at 20 (set by hand in the Viewer), rietschen's Manta at 30. export_camera_profile.py --target now sets it for either family (IntensityControllerTarget, or ExposureAutoTarget on a Manta). Without the option nothing changes. For podrosche, whose migration plans 20 %. Co-Authored-By: Claude Opus 5.5 --- docs/tower-installation.md | 5 +++++ scripts/export_camera_profile.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/tower-installation.md b/docs/tower-installation.md index 4693387..6fcaa57 100644 --- a/docs/tower-installation.md +++ b/docs/tower-installation.md @@ -696,6 +696,11 @@ property the Viewer workflow exists to provide: GenICam validity is stateful, so a hand-written XML can be individually plausible and collectively wrong. Here the camera is the judge, and anything it refused is printed rather than assumed. +`--target ` sets the auto-exposure target, a tower's own choice: the +Alvium recipe sets none, so the camera keeps its default of 50, which burns out +the sky over a dark forest. Staeffelsberg runs at 20, rietschen at 30. The keep +rule reads the target back from the camera, so nothing else has to change. + Read the summary line it prints - it is the number that decides whether the profile can be delivered at all: diff --git a/scripts/export_camera_profile.py b/scripts/export_camera_profile.py index 0fc5ea6..8697976 100755 --- a/scripts/export_camera_profile.py +++ b/scripts/export_camera_profile.py @@ -193,6 +193,20 @@ def has_feature(cam, name): return False +def with_target(recipe, family, target): + """The recipe with its auto-exposure target set to `target` percent. + + A tower's own choice, not a recipe's: staeffelsberg's colour camera runs at + 20 %, rietschen's at 30, where the Alvium's default of 50 burnt out the sky. + The keep rule reads the target back from the camera, so nothing else needs + to know. An existing target entry is replaced; otherwise it goes last.""" + if target is None: + return recipe + name = "ExposureAutoTarget" if family == "gige" else "IntensityControllerTarget" + out = [(n, v) for n, v in recipe if n != name] + return out + [(name, float(target))] + + 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"): @@ -270,6 +284,9 @@ def main(): 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("--target", type=float, default=None, + help="auto-exposure target in percent (default: the recipe's, or the " + "camera's own where the recipe sets none)") ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() @@ -294,7 +311,8 @@ def main(): with cam: family = recipe_family(cam) if args.family == "auto" else args.family - recipe = (GIGE_RECIPES if family == "gige" else RECIPES)[args.profile] + recipe = with_target((GIGE_RECIPES if family == "gige" else RECIPES)[args.profile], + family, args.target) print(f"camera : {cam.get_id()} {cam.get_model()}") print(f"profile : {args.profile} ({family} recipe){' (dry run)' if args.dry_run else ''}") refused = []