Here's a commit message for the changes to [scripts/launch-ids.py](scripts/launch-ids.py):

```
fix(launch-ids): fix videocrop element error and enhance startup output

- Fix GstAddError caused by duplicate pipeline.add(videocrop) call
  The videocrop element was being added to the pipeline twice: once
  explicitly when crop was enabled, and again in the element linking loop.
  Removed the duplicate add operation.

- Enhance startup configuration display
  Display all configured parameters at startup in a formatted summary:
  * Camera config file path
  * Exposure time (ms)
  * Framerate (Hz)
  * Crop settings (pixels or disabled)
  * Queue buffer size (if configured)
  * Video stream destination (UDP host:port)
  * Control server port (or disabled status)
  * Complete pipeline description

The output is now formatted with separators and aligned labels for
better readability, making it easier to verify settings at startup.
```
This commit is contained in:
yair 2025-11-16 02:20:12 +02:00
parent 083cd86702
commit 193244e9a3
2 changed files with 476 additions and 47 deletions

View File

@ -0,0 +1,222 @@
[Versions]
ueye_api_64.dll=4.93.1730
ueye_usb_64.sys=4.93.1314
ueye_boot_64.sys=4.93.1314
[Sensor]
Sensor=UI308xCP-C
Sensor bit depth=0
Sensor source gain=24
FPN correction mode=0
Black reference mode=0
Sensor digital gain=0
[Image size]
Start X=0
Start Y=0
Start X absolute=0
Start Y absolute=0
Width=1224
Height=1026
Binning=3
Subsampling=0
[Scaler]
Mode=0
Factor=0.000000
[Multi AOI]
Enabled=0
Mode=0
x1=0
x2=0
x3=0
x4=0
y1=0
y2=0
y3=0
y4=0
[Shutter]
Mode=0
Linescan number=0
[Log Mode]
Mode=3
Manual value=0
Manual gain=0
[Timing]
Pixelclock=474
Extended pixelclock range=0
Framerate=86.691659
Exposure=2.100216
Long exposure=0
Dual exposure ratio=0
[Selected Converter]
IS_SET_CM_RGB32=2
IS_SET_CM_RGB24=2
IS_SET_CM_RGB16=2
IS_SET_CM_RGB15=2
IS_SET_CM_Y8=2
IS_SET_CM_RGB8=2
IS_SET_CM_BAYER=8
IS_SET_CM_UYVY=2
IS_SET_CM_UYVY_MONO=2
IS_SET_CM_UYVY_BAYER=2
IS_CM_CBYCRY_PACKED=0
IS_SET_CM_RGBY=8
IS_SET_CM_RGB30=2
IS_SET_CM_Y12=2
IS_SET_CM_BAYER12=8
IS_SET_CM_Y16=2
IS_SET_CM_BAYER16=8
IS_CM_BGR12_UNPACKED=2
IS_CM_BGRA12_UNPACKED=2
IS_CM_JPEG=0
IS_CM_SENSOR_RAW10=8
IS_CM_MONO10=2
IS_CM_BGR10_UNPACKED=2
IS_CM_RGBA8_PACKED=2
IS_CM_RGB8_PACKED=2
IS_CM_RGBY8_PACKED=8
IS_CM_RGB10V2_PACKED=8
IS_CM_RGB12_UNPACKED=2
IS_CM_RGBA12_UNPACKED=2
IS_CM_RGB10_UNPACKED=2
IS_CM_RGB8_PLANAR=2
[Parameters]
Colormode=1
Gamma=1.000000
Hardware Gamma=0
Blacklevel Mode=0
Blacklevel Offset=7
Hotpixel Mode=0
Hotpixel Threshold=0
Sensor Hotpixel=0
Adaptive hotpixel correction enable=0
Adaptive hotpixel correction mode=0
Adaptive hotpixel correction sensitivity=3
GlobalShutter=0
AllowRawWithLut=0
[Gain]
Master=40
Red=5
Green=0
Blue=72
GainBoost=1
[Processing]
EdgeEnhancementFactor=0
RopEffect=0
Whitebalance=0
Whitebalance Red=1.000000
Whitebalance Green=1.000000
Whitebalance Blue=1.000000
Color correction=4
Color_correction_factor=1.000000
Color_correction_satU=100
Color_correction_satV=100
Bayer Conversion=1
JpegCompression=0
NoiseMode=0
ImageEffect=0
LscModel=0
WideDynamicRange=0
[Auto features]
Auto Framerate control=0
Brightness exposure control=0
Brightness gain control=0
Auto Framerate Sensor control=0
Brightness exposure Sensor control=0
Brightness gain Sensor control=0
Brightness exposure Sensor control photometry=0
Brightness gain Sensor control photometry=0
Brightness control once=0
Brightness reference=128
Brightness speed=50
Brightness max gain=100
Brightness max exposure=11.478595
Brightness Aoi Left=0
Brightness Aoi Top=0
Brightness Aoi Width=1224
Brightness Aoi Height=1026
Brightness Hysteresis=2
AutoImageControlMode=2
AutoImageControlPeakWhiteChannel=0
AutoImageControlExposureMinimum=0.000000
AutoImageControlPeakWhiteChannelMode=0
AutoImageControlPeakWhiteGranularity=0
Auto WB control=0
Auto WB type=1
Auto WB RGB color model=0
Auto WB RGB color temperature=5000
Auto WB offsetR=0
Auto WB offsetB=0
Auto WB gainMin=0
Auto WB gainMax=100
Auto WB speed=50
Auto WB Aoi Left=702
Auto WB Aoi Top=192
Auto WB Aoi Width=14
Auto WB Aoi Height=18
Auto WB Once=0
Auto WB Hysteresis=2
Brightness Skip Frames Trigger Mode=4
Brightness Skip Frames Freerun Mode=4
Auto WB Skip Frames Trigger Mode=4
Auto WB Skip Frames Freerun Mode=4
[Trigger and Flash]
Trigger mode=4097
Trigger timeout=200
Trigger delay=0
Trigger debounce mode=0
Trigger debounce delay time=1
Trigger burst size=1
Trigger prescaler frame=64
Trigger prescaler line=1
Trigger input=1
Flash strobe=0
Flash delay=0
Flash duration=0
Flash auto freerun=0
PWM mode=0
PWM frequency=20000000
PWM dutycycle=20000000
GPIO state=3
GPIO direction=0
GPIO1 Config=1
GPIO2 Config=1
[Vertical AOI Merge Mode]
Mode=0
Position=0
Additional Position=0
Height=2
[Level Controlled Trigger Mode]
Mode=0
[Memory]
Camera memory mode=1

View File

@ -2,31 +2,42 @@
#!/usr/bin/env python3
# /// script
# requires-python = "==3.13"
# dependencies = []
# dependencies = ["argcomplete"]
# ///
#
# IDS uEye Camera Control Script with UDP Exposure Control
#
# This script streams video from an IDS uEye camera via UDP and provides
# a UDP control interface for dynamically adjusting exposure and framerate.
# All parameters are configurable via command-line arguments with sensible defaults.
#
# Setup:
# Run with: . .\scripts\setup_gstreamer_env.ps1 && uv run .\scripts\launch-ids.py
#
# Features:
# - Video streaming on UDP port 5000 (127.0.0.1)
# - Control interface on UDP port 5001 (0.0.0.0)
# - Dynamic exposure control (1.0-1000.0 milliseconds)
# - Dynamic framerate control (1-500 fps)
# Basic Usage:
# uv run .\scripts\launch-ids.py # Use all defaults
# uv run .\scripts\launch-ids.py --help # Show all options
# uv run .\scripts\launch-ids.py -e 16 -f 30 # Set exposure & framerate
# uv run .\scripts\launch-ids.py --port 6000 # Custom streaming port
# uv run .\scripts\launch-ids.py --no-crop --quiet # No cropping, minimal output
#
# Control Commands:
# Features:
# - Configurable video streaming (default: UDP port 5000 to 127.0.0.1)
# - Optional control interface (default: UDP port 5001 on 0.0.0.0)
# - Dynamic exposure control (1.0-1000.0 milliseconds, default: 10ms)
# - Dynamic framerate control (1-20000 fps, default: 750fps)
# - Configurable video cropping (default: crop 3 pixels from bottom)
# - Verbose/quiet output modes
# - Custom camera configuration files
#
# Control Commands (when control server enabled):
# SET_EXPOSURE <value> - Set exposure in milliseconds (e.g., 16)
# GET_EXPOSURE - Get current exposure value
# SET_FRAMERATE <value> - Set framerate in Hz (e.g., 30)
# GET_FRAMERATE - Get current framerate
# STATUS - Get pipeline status and current settings
#
# Example Usage:
# Example Control Usage:
# echo "SET_EXPOSURE 10" | nc -u 127.0.0.1 5001
# echo "GET_EXPOSURE" | nc -u 127.0.0.1 5001
#
@ -37,6 +48,8 @@
# See scripts/UDP_CONTROL_PROTOCOL.md for full protocol details
#
# Add GStreamer Python packages
import argparse
import argcomplete
import os
import sys
import socket
@ -199,8 +212,8 @@ class ControlServer:
try:
value = float(parts[1])
if value < 1.0 or value > 500.0:
return "ERROR OUT_OF_RANGE: Framerate must be 1.0-500.0 Hz"
if value < 1.0 or value > 20200.0:
return "ERROR OUT_OF_RANGE: Framerate must be 1.0-20200.0 Hz"
self.src.set_property("framerate", value)
actual = self.src.get_property("framerate")
@ -243,56 +256,245 @@ class ControlServer:
print(f"Failed to send response: {e}")
def parse_arguments():
"""Parse command line arguments with nice defaults"""
parser = argparse.ArgumentParser(
description='IDS uEye Camera Control Script with UDP Exposure Control',
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
epilog="""
Examples:
%(prog)s # Use all defaults
%(prog)s --exposure 16 --framerate 30 # Basic video settings
%(prog)s --config custom.ini --port 6000 # Custom config and streaming port
%(prog)s --host 192.168.1.100 --no-crop # Stream to remote host without cropping
%(prog)s --control-port 6001 --verbose # Custom control port with verbose output
""",
add_help=True
)
# Camera configuration
camera_group = parser.add_argument_group('Camera Settings')
camera_group.add_argument(
'--config', '--config-file',
type=str,
default='ini/100fps-10exp-2456x4pix-500top-cw-extragain.ini',
metavar='PATH',
help='Camera configuration file path'
)
camera_group.add_argument(
'--exposure', '-e',
type=float,
default=10.0,
metavar='MS',
help='Camera exposure time in milliseconds (1.0-1000.0)'
)
camera_group.add_argument(
'--framerate', '--fps', '-f',
type=float,
default=750.0,
metavar='HZ',
help='Camera framerate in Hz (1.0-20200.0)'
)
# Video processing
video_group = parser.add_argument_group('Video Processing')
video_group.add_argument(
'--crop-bottom',
type=int,
default=3,
metavar='PIXELS',
help='Number of pixels to crop from bottom (0 to disable)'
)
video_group.add_argument(
'--no-crop',
action='store_true',
help='Disable video cropping (equivalent to --crop-bottom 0)'
)
video_group.add_argument(
'--queue-size',
type=int,
default=None,
metavar='BUFFERS',
help='Queue buffer size (default: use GStreamer defaults)'
)
# Network settings
network_group = parser.add_argument_group('Network Settings')
network_group.add_argument(
'--host', '--udp-host',
type=str,
default='127.0.0.1',
metavar='IP',
help='UDP streaming destination host'
)
network_group.add_argument(
'--port', '--udp-port',
type=int,
default=5000,
metavar='PORT',
help='UDP streaming port'
)
network_group.add_argument(
'--control-port',
type=int,
default=5001,
metavar='PORT',
help='UDP control server port'
)
network_group.add_argument(
'--disable-control',
action='store_true',
help='Disable UDP control server'
)
# General options
general_group = parser.add_argument_group('General Options')
general_group.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
general_group.add_argument(
'--quiet', '-q',
action='store_true',
help='Suppress non-error output'
)
# Enable tab completion
argcomplete.autocomplete(parser)
args = parser.parse_args()
# Validation
if args.exposure < 1.0 or args.exposure > 1000.0:
parser.error(f"Exposure must be between 1.0 and 1000.0 ms, got {args.exposure}")
if args.framerate < 1.0 or args.framerate > 20200.0:
parser.error(f"Framerate must be between 1.0 and 20200.0 Hz, got {args.framerate}")
if args.port < 1 or args.port > 65535:
parser.error(f"UDP port must be between 1 and 65535, got {args.port}")
if args.control_port < 1 or args.control_port > 65535:
parser.error(f"Control port must be between 1 and 65535, got {args.control_port}")
if args.crop_bottom < 0:
parser.error(f"Crop bottom must be non-negative, got {args.crop_bottom}")
if args.no_crop:
args.crop_bottom = 0
if args.verbose and args.quiet:
parser.error("Cannot specify both --verbose and --quiet")
# Convert Windows paths to forward slashes for GStreamer compatibility
if args.config:
# Convert backslashes to forward slashes
args.config = args.config.replace('\\', '/')
# Handle Windows drive letters (C:/ -> C:/)
if len(args.config) >= 3 and args.config[1:3] == ':/':
# Already in correct format
pass
elif len(args.config) >= 2 and args.config[1] == ':':
# Convert C: to C:/
args.config = args.config[0] + ':/' + args.config[2:].lstrip('/')
# Check if config file exists (using original path format for file system check)
config_check_path = args.config.replace('/', os.sep) if args.config else None
if config_check_path and not os.path.exists(config_check_path):
if not args.quiet:
print(f"WARNING: Config file '{config_check_path}' does not exist")
print("Camera will use default settings")
return args
# Parse command line arguments
args = parse_arguments()
Gst.init(None)
pipeline = Gst.Pipeline()
src = Gst.ElementFactory.make("idsueyesrc", "src")
src.set_property("config-file", "ini/100fps-10exp-2456x4pix-500top-cw-extragain.ini")
src.set_property("config-file", args.config)
# Exposure in milliseconds (e.g., 10)
src.set_property("exposure", 10)
# Exposure in milliseconds
src.set_property("exposure", args.exposure)
# Frame rate
src.set_property("framerate", 750)
src.set_property("framerate", args.framerate)
# Video crop to remove bottom 3 pixels
videocrop = Gst.ElementFactory.make("videocrop", "crop")
videocrop.set_property("bottom", 3)
# Video crop to remove bottom pixels (if enabled)
elements_to_link = [src]
if args.crop_bottom > 0:
videocrop = Gst.ElementFactory.make("videocrop", "crop")
videocrop.set_property("bottom", args.crop_bottom)
elements_to_link.append(videocrop)
# Queue for buffering
queue = Gst.ElementFactory.make("queue", "queue")
if args.queue_size is not None:
queue.set_property("max-size-buffers", args.queue_size)
# UDP sink to send the raw data
udpsink = Gst.ElementFactory.make("udpsink", "sink")
udpsink.set_property("host", "127.0.0.1")
udpsink.set_property("port", 5000)
udpsink.set_property("host", args.host)
udpsink.set_property("port", args.port)
# Add elements to pipeline
# Add elements to pipeline and build dynamic linking chain
pipeline.add(src)
pipeline.add(videocrop)
pipeline.add(queue)
pipeline.add(udpsink)
elements_to_link.append(queue)
elements_to_link.append(udpsink)
# Link elements: src -> videocrop -> queue -> udpsink
if not src.link(videocrop):
print("ERROR: Failed to link src to videocrop")
exit(1)
if not videocrop.link(queue):
print("ERROR: Failed to link videocrop to queue")
exit(1)
if not queue.link(udpsink):
print("ERROR: Failed to link queue to udpsink")
for element in elements_to_link[1:]: # Skip src which is already added
pipeline.add(element)
# Link elements dynamically based on pipeline configuration
for i in range(len(elements_to_link) - 1):
if not elements_to_link[i].link(elements_to_link[i + 1]):
element_names = [elem.get_name() for elem in elements_to_link]
print(f"ERROR: Failed to link {element_names[i]} to {element_names[i + 1]}")
exit(1)
print("Pipeline created successfully")
print(f"Video stream: UDP port 5000 (host: 127.0.0.1)")
print("Pipeline: idsueyesrc -> videocrop (bottom=3) -> queue -> udpsink")
print()
# Build pipeline description for output
pipeline_description = " -> ".join([elem.get_name() for elem in elements_to_link])
if args.crop_bottom > 0:
pipeline_description = pipeline_description.replace("crop", f"videocrop(bottom={args.crop_bottom})")
if not args.quiet:
print("=" * 60)
print("IDS uEye Camera - Pipeline Configuration")
print("=" * 60)
print(f"Camera config: {args.config}")
print(f"Exposure: {args.exposure} ms")
print(f"Framerate: {args.framerate} Hz")
if args.crop_bottom > 0:
print(f"Crop bottom: {args.crop_bottom} pixels")
else:
print(f"Crop: disabled")
if args.queue_size is not None:
print(f"Queue size: {args.queue_size} buffers")
print()
print(f"Video stream: UDP {args.host}:{args.port}")
if not args.disable_control:
print(f"Control port: UDP 0.0.0.0:{args.control_port}")
else:
print(f"Control: disabled")
print()
print(f"Pipeline: {pipeline_description}")
print("=" * 60)
print()
# Create and start control server
control_server = ControlServer(src, pipeline, port=5001)
control_server.start()
if not args.disable_control:
control_server = ControlServer(src, pipeline, port=args.control_port)
control_server.start()
else:
control_server = None
if args.verbose:
print("Control server disabled")
# Start the pipeline
ret = pipeline.set_state(Gst.State.PLAYING)
@ -300,9 +502,10 @@ if ret == Gst.StateChangeReturn.FAILURE:
print("ERROR: Unable to set the pipeline to the playing state")
exit(1)
print()
print("Pipeline is PLAYING...")
print("Press Ctrl+C to stop")
if not args.quiet:
print()
print("Pipeline is PLAYING...")
print("Press Ctrl+C to stop")
# Wait until error or EOS
bus = pipeline.get_bus()
@ -326,7 +529,7 @@ try:
print("End-Of-Stream reached")
break
elif t == Gst.MessageType.STATE_CHANGED:
if msg.src == pipeline:
if msg.src == pipeline and args.verbose:
old_state, new_state, pending_state = msg.parse_state_changed()
print(f"Pipeline state changed from {old_state.value_nick} to {new_state.value_nick}")
@ -334,8 +537,12 @@ except KeyboardInterrupt:
print("\nInterrupted by user")
# Cleanup
print("Stopping control server...")
control_server.stop()
print("Stopping pipeline...")
if not args.quiet:
print("Stopping control server...")
if control_server:
control_server.stop()
if not args.quiet:
print("Stopping pipeline...")
pipeline.set_state(Gst.State.NULL)
print("Pipeline stopped")
if not args.quiet:
print("Pipeline stopped")