Add UDP control protocol and IDS camera scripts
- Added UDP_CONTROL_PROTOCOL.md documenting the UDP control interface - Added launch-ids.py for IDS camera control - Added test_exposure_control.py for testing exposure settings - Added udp_backup.reg for UDP configuration backup - Added visualize_line_realtime.py for real-time visualization - Updated .gitignore and ROLLINGSUM_GUIDE.md - Removed ini/200fps-2456x4pix-cw.ini configuration file
This commit is contained in:
parent
743bfb8323
commit
fef3f0baad
18
.gitignore
vendored
18
.gitignore
vendored
@ -5,23 +5,6 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
#ignore build folder
|
#ignore build folder
|
||||||
[Bb]uild*/
|
[Bb]uild*/
|
||||||
#Ignore files build by Visual Studio
|
|
||||||
*.obj
|
|
||||||
*.exe
|
|
||||||
*.pdb
|
|
||||||
*.user
|
|
||||||
*.aps
|
|
||||||
*.pch
|
|
||||||
*.vspscc
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*.ncb
|
|
||||||
*.suo
|
|
||||||
*.tlb
|
|
||||||
*.tlh
|
|
||||||
*.bak
|
|
||||||
*.cache
|
|
||||||
*.ilk
|
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
[Bb]in
|
[Bb]in
|
||||||
@ -39,5 +22,6 @@ ipch/
|
|||||||
*.mkv
|
*.mkv
|
||||||
*.raw
|
*.raw
|
||||||
*.dot
|
*.dot
|
||||||
|
*.avi
|
||||||
gst_plugs/
|
gst_plugs/
|
||||||
results/
|
results/
|
||||||
@ -100,7 +100,7 @@ struct _GstRollingSum
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Algorithm (Simplified from cli.py)
|
### Algorithm (Simplified from wissotsky's cli.py)
|
||||||
|
|
||||||
**Per Frame Processing:**
|
**Per Frame Processing:**
|
||||||
|
|
||||||
|
|||||||
@ -1,222 +0,0 @@
|
|||||||
[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=500
|
|
||||||
Start X absolute=0
|
|
||||||
Start Y absolute=500
|
|
||||||
Width=2456
|
|
||||||
Height=4
|
|
||||||
Binning=0
|
|
||||||
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=237
|
|
||||||
Extended pixelclock range=0
|
|
||||||
Framerate=200.151466
|
|
||||||
Exposure=4.903189
|
|
||||||
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=4
|
|
||||||
Hotpixel Mode=2
|
|
||||||
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=0
|
|
||||||
Red=19
|
|
||||||
Green=0
|
|
||||||
Blue=33
|
|
||||||
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=4.903189
|
|
||||||
Brightness Aoi Left=0
|
|
||||||
Brightness Aoi Top=0
|
|
||||||
Brightness Aoi Width=2456
|
|
||||||
Brightness Aoi Height=4
|
|
||||||
Brightness Hysteresis=2
|
|
||||||
AutoImageControlMode=2
|
|
||||||
AutoImageControlPeakWhiteChannel=0
|
|
||||||
AutoImageControlExposureMinimum=0.000000
|
|
||||||
AutoImageControlPeakWhiteChannelMode=0
|
|
||||||
AutoImageControlPeakWhiteGranularity=0
|
|
||||||
Auto WB control=0
|
|
||||||
Auto WB type=2
|
|
||||||
Auto WB RGB color model=1
|
|
||||||
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=0
|
|
||||||
Auto WB Aoi Top=0
|
|
||||||
Auto WB Aoi Width=2456
|
|
||||||
Auto WB Aoi Height=4
|
|
||||||
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=0
|
|
||||||
Trigger timeout=200
|
|
||||||
Trigger delay=0
|
|
||||||
Trigger debounce mode=0
|
|
||||||
Trigger debounce delay time=1
|
|
||||||
Trigger burst size=1
|
|
||||||
Trigger prescaler frame=1
|
|
||||||
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
|
|
||||||
288
scripts/UDP_CONTROL_PROTOCOL.md
Normal file
288
scripts/UDP_CONTROL_PROTOCOL.md
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
# UDP Control Protocol Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the UDP-based control protocol for dynamically controlling the IDS uEye camera exposure during runtime.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ launch-ids.py Process │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ ┌──────────────────────────┐ │
|
||||||
|
│ │ Main Thread │ │ Control Server Thread │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ GStreamer │◄────────┤ UDP Socket (Port 5001) │ │
|
||||||
|
│ │ Pipeline │ Thread- │ Command Parser │ │
|
||||||
|
│ │ - idsueyesrc │ Safe │ Property Setter │ │
|
||||||
|
│ │ - videocrop │ Updates │ Response Handler │ │
|
||||||
|
│ │ - queue │ │ │ │
|
||||||
|
│ │ - udpsink:5000 │ └──────────────────────────┘ │
|
||||||
|
│ └──────────────────┘ ▲ │
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────────────────────────┼───────────────────┘
|
||||||
|
│
|
||||||
|
│ UDP Commands
|
||||||
|
│
|
||||||
|
┌────────┴────────┐
|
||||||
|
│ Control Client │
|
||||||
|
│ (Any UDP tool) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Details
|
||||||
|
|
||||||
|
- **Control Port**: 5001 (UDP)
|
||||||
|
- **Bind Address**: 0.0.0.0 (accepts from any interface)
|
||||||
|
- **Video Port**: 5000 (UDP) - existing video stream, unchanged
|
||||||
|
- **Protocol**: UDP (connectionless, stateless)
|
||||||
|
- **Encoding**: ASCII text
|
||||||
|
- **Delimiter**: Newline (`\n`)
|
||||||
|
|
||||||
|
## Command Format
|
||||||
|
|
||||||
|
### General Structure
|
||||||
|
```
|
||||||
|
COMMAND [PARAMETERS]\n
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands are case-insensitive, but UPPERCASE is recommended for clarity.
|
||||||
|
|
||||||
|
## Supported Commands
|
||||||
|
|
||||||
|
### 1. SET_EXPOSURE
|
||||||
|
|
||||||
|
**Description**: Sets the camera exposure time.
|
||||||
|
|
||||||
|
**Syntax**:
|
||||||
|
```
|
||||||
|
SET_EXPOSURE <value>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `<value>`: Exposure time in seconds (float)
|
||||||
|
- Range: 0.001 to 1.0 seconds (1ms to 1000ms)
|
||||||
|
- Examples: `0.016` (16ms), `0.001` (1ms), `0.100` (100ms)
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```
|
||||||
|
OK <actual_value>
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```
|
||||||
|
ERROR <error_message>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```
|
||||||
|
Client: SET_EXPOSURE 0.016\n
|
||||||
|
Server: OK 0.016\n
|
||||||
|
|
||||||
|
Client: SET_EXPOSURE 2.0\n
|
||||||
|
Server: ERROR Value out of range (0.001-1.0)\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. GET_EXPOSURE
|
||||||
|
|
||||||
|
**Description**: Retrieves the current exposure time.
|
||||||
|
|
||||||
|
**Syntax**:
|
||||||
|
```
|
||||||
|
GET_EXPOSURE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: None
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```
|
||||||
|
OK <current_value>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
Client: GET_EXPOSURE\n
|
||||||
|
Server: OK 0.016\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. SET_FRAMERATE
|
||||||
|
|
||||||
|
**Description**: Sets the camera frame rate.
|
||||||
|
|
||||||
|
**Syntax**:
|
||||||
|
```
|
||||||
|
SET_FRAMERATE <value>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `<value>`: Frame rate in Hz (float)
|
||||||
|
- Range: 1.0 to 500.0 fps
|
||||||
|
- Examples: `22`, `30.5`, `100`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```
|
||||||
|
OK <actual_value>
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```
|
||||||
|
ERROR <error_message>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
Client: SET_FRAMERATE 30\n
|
||||||
|
Server: OK 30.0\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. GET_FRAMERATE
|
||||||
|
|
||||||
|
**Description**: Retrieves the current frame rate.
|
||||||
|
|
||||||
|
**Syntax**:
|
||||||
|
```
|
||||||
|
GET_FRAMERATE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: None
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```
|
||||||
|
OK <current_value>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
Client: GET_FRAMERATE\n
|
||||||
|
Server: OK 22.0\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. STATUS
|
||||||
|
|
||||||
|
**Description**: Get overall pipeline status and current settings.
|
||||||
|
|
||||||
|
**Syntax**:
|
||||||
|
```
|
||||||
|
STATUS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**: None
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```
|
||||||
|
OK exposure=<value> framerate=<value> state=<PLAYING|PAUSED|NULL>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
Client: STATUS\n
|
||||||
|
Server: OK exposure=0.016 framerate=22.0 state=PLAYING\n
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
```
|
||||||
|
ERROR <error_code>: <error_message>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
| Code | Description | Example |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `INVALID_COMMAND` | Unknown command | `ERROR INVALID_COMMAND: Unknown command 'FOO'` |
|
||||||
|
| `INVALID_SYNTAX` | Malformed command | `ERROR INVALID_SYNTAX: Missing parameter` |
|
||||||
|
| `OUT_OF_RANGE` | Value out of valid range | `ERROR OUT_OF_RANGE: Exposure must be 0.001-1.0` |
|
||||||
|
| `PIPELINE_ERROR` | Pipeline not running | `ERROR PIPELINE_ERROR: Pipeline not in PLAYING state` |
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
- The control server runs in a separate daemon thread
|
||||||
|
- GStreamer properties are inherently thread-safe (GObject properties)
|
||||||
|
- The `src.set_property()` method can be safely called from the control thread
|
||||||
|
|
||||||
|
### Non-Blocking Operation
|
||||||
|
- Control server uses non-blocking socket with timeout
|
||||||
|
- Does not interfere with GStreamer pipeline operation
|
||||||
|
- Minimal latency for command processing
|
||||||
|
|
||||||
|
### Response Timing
|
||||||
|
- Responses are sent immediately after processing
|
||||||
|
- Property changes take effect on the next frame capture
|
||||||
|
- No guaranteed synchronization with video stream
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Python Client Example
|
||||||
|
```python
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def send_command(command):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.sendto(command.encode() + b'\n', ('127.0.0.1', 5001))
|
||||||
|
sock.settimeout(1.0)
|
||||||
|
response, _ = sock.recvfrom(1024)
|
||||||
|
sock.close()
|
||||||
|
return response.decode().strip()
|
||||||
|
|
||||||
|
# Set exposure to 10ms
|
||||||
|
print(send_command("SET_EXPOSURE 0.010"))
|
||||||
|
|
||||||
|
# Get current exposure
|
||||||
|
print(send_command("GET_EXPOSURE"))
|
||||||
|
|
||||||
|
# Set framerate to 30fps
|
||||||
|
print(send_command("SET_FRAMERATE 30"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line (netcat/nc)
|
||||||
|
```bash
|
||||||
|
# Set exposure
|
||||||
|
echo "SET_EXPOSURE 0.020" | nc -u 127.0.0.1 5001
|
||||||
|
|
||||||
|
# Get exposure
|
||||||
|
echo "GET_EXPOSURE" | nc -u 127.0.0.1 5001
|
||||||
|
|
||||||
|
# Get status
|
||||||
|
echo "STATUS" | nc -u 127.0.0.1 5001
|
||||||
|
```
|
||||||
|
|
||||||
|
### PowerShell Client
|
||||||
|
```powershell
|
||||||
|
$udpClient = New-Object System.Net.Sockets.UdpClient
|
||||||
|
$endpoint = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("127.0.0.1"), 5001)
|
||||||
|
|
||||||
|
# Send command
|
||||||
|
$bytes = [System.Text.Encoding]::ASCII.GetBytes("SET_EXPOSURE 0.015`n")
|
||||||
|
$udpClient.Send($bytes, $bytes.Length, $endpoint)
|
||||||
|
|
||||||
|
# Receive response
|
||||||
|
$udpClient.Client.ReceiveTimeout = 1000
|
||||||
|
$receiveBytes = $udpClient.Receive([ref]$endpoint)
|
||||||
|
$response = [System.Text.Encoding]::ASCII.GetString($receiveBytes)
|
||||||
|
Write-Host $response
|
||||||
|
|
||||||
|
$udpClient.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
A test client script is provided: `scripts/test_exposure_control.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the camera pipeline
|
||||||
|
uv run scripts/launch-ids.py
|
||||||
|
|
||||||
|
# In another terminal, test exposure control
|
||||||
|
uv run scripts/test_exposure_control.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Possible extensions to the protocol:
|
||||||
|
- Add `SET_GAIN` / `GET_GAIN` commands
|
||||||
|
- Add `SAVE_CONFIG` to save current settings to INI file
|
||||||
|
- Add `RESET` to restore default settings
|
||||||
|
- Support batch commands (multiple commands in one packet)
|
||||||
|
- Add authentication/security for production use
|
||||||
341
scripts/launch-ids.py
Normal file
341
scripts/launch-ids.py
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = "==3.13"
|
||||||
|
# dependencies = []
|
||||||
|
# ///
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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 (0.001-1.0 seconds)
|
||||||
|
# - Dynamic framerate control (1-500 fps)
|
||||||
|
#
|
||||||
|
# Control Commands:
|
||||||
|
# SET_EXPOSURE <value> - Set exposure in seconds (e.g., 0.016)
|
||||||
|
# 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:
|
||||||
|
# echo "SET_EXPOSURE 0.010" | nc -u 127.0.0.1 5001
|
||||||
|
# echo "GET_EXPOSURE" | nc -u 127.0.0.1 5001
|
||||||
|
#
|
||||||
|
# Testing:
|
||||||
|
# Run test client: uv run .\scripts\test_exposure_control.py
|
||||||
|
#
|
||||||
|
# Documentation:
|
||||||
|
# See scripts/UDP_CONTROL_PROTOCOL.md for full protocol details
|
||||||
|
#
|
||||||
|
# Add GStreamer Python packages
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# Check for required environment variable
|
||||||
|
gst_root = os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64")
|
||||||
|
if not gst_root:
|
||||||
|
print("ERROR: GSTREAMER_1_0_ROOT_MSVC_X86_64 environment variable is not set")
|
||||||
|
print("Expected: C:\\bin\\gstreamer\\1.0\\msvc_x86_64\\")
|
||||||
|
print("Please run: . .\\scripts\\setup_gstreamer_env.ps1")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# Remove trailing backslash if present
|
||||||
|
gst_root = gst_root.rstrip("\\")
|
||||||
|
|
||||||
|
gst_site_packages = os.path.join(gst_root, "lib", "site-packages")
|
||||||
|
sys.path.insert(0, gst_site_packages)
|
||||||
|
|
||||||
|
# Add GI typelibs
|
||||||
|
os.environ["GI_TYPELIB_PATH"] = os.path.join(gst_root, "lib", "girepository-1.0")
|
||||||
|
|
||||||
|
# Add GStreamer DLL bin directory
|
||||||
|
os.environ["PATH"] = os.path.join(gst_root, "bin") + ";" + os.environ["PATH"]
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gst", "1.0")
|
||||||
|
from gi.repository import Gst
|
||||||
|
|
||||||
|
class ControlServer:
|
||||||
|
"""UDP server for controlling camera parameters during runtime"""
|
||||||
|
|
||||||
|
def __init__(self, src, pipeline=None, port=5001):
|
||||||
|
self.src = src
|
||||||
|
self.pipeline = pipeline
|
||||||
|
self.port = port
|
||||||
|
self.running = False
|
||||||
|
self.sock = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the control server in a separate thread"""
|
||||||
|
self.running = True
|
||||||
|
self.thread = threading.Thread(target=self.run, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the control server"""
|
||||||
|
self.running = False
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join(timeout=2.0)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Main server loop"""
|
||||||
|
try:
|
||||||
|
# Create UDP socket
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.sock.settimeout(0.5) # Non-blocking with timeout
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock.bind(("0.0.0.0", self.port))
|
||||||
|
except OSError as e:
|
||||||
|
print(f"ERROR: Could not bind control server to port {self.port}: {e}")
|
||||||
|
print("Control server disabled. Video streaming will continue.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Control server listening on UDP port {self.port}")
|
||||||
|
print(" Commands: SET_EXPOSURE <val>, GET_EXPOSURE, SET_FRAMERATE <val>, GET_FRAMERATE, STATUS")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Receive command
|
||||||
|
data, addr = self.sock.recvfrom(1024)
|
||||||
|
command = data.decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
|
if command:
|
||||||
|
# Process command
|
||||||
|
response = self.process_command(command, addr)
|
||||||
|
|
||||||
|
# Send response
|
||||||
|
self.send_response(response, addr)
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
# Normal timeout, continue loop
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
if self.running:
|
||||||
|
print(f"Control server error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_command(self, command, addr):
|
||||||
|
"""Process incoming command and return response"""
|
||||||
|
try:
|
||||||
|
parts = command.strip().upper().split()
|
||||||
|
if not parts:
|
||||||
|
return "ERROR INVALID_SYNTAX: Empty command"
|
||||||
|
|
||||||
|
cmd = parts[0]
|
||||||
|
|
||||||
|
if cmd == "SET_EXPOSURE":
|
||||||
|
return self.handle_set_exposure(parts)
|
||||||
|
elif cmd == "GET_EXPOSURE":
|
||||||
|
return self.handle_get_exposure()
|
||||||
|
elif cmd == "SET_FRAMERATE":
|
||||||
|
return self.handle_set_framerate(parts)
|
||||||
|
elif cmd == "GET_FRAMERATE":
|
||||||
|
return self.handle_get_framerate()
|
||||||
|
elif cmd == "STATUS":
|
||||||
|
return self.handle_status()
|
||||||
|
else:
|
||||||
|
return f"ERROR INVALID_COMMAND: Unknown command '{cmd}'"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR PROCESSING: {str(e)}"
|
||||||
|
|
||||||
|
def handle_set_exposure(self, parts):
|
||||||
|
"""Handle SET_EXPOSURE command"""
|
||||||
|
if len(parts) != 2:
|
||||||
|
return "ERROR INVALID_SYNTAX: Usage: SET_EXPOSURE <value>"
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = float(parts[1])
|
||||||
|
if value < 0.001 or value > 1.0:
|
||||||
|
return "ERROR OUT_OF_RANGE: Exposure must be 0.001-1.0 seconds"
|
||||||
|
|
||||||
|
self.src.set_property("exposure", value)
|
||||||
|
# Verify the value was set
|
||||||
|
actual = self.src.get_property("exposure")
|
||||||
|
return f"OK {actual}"
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return "ERROR INVALID_SYNTAX: Exposure must be a number"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
def handle_get_exposure(self):
|
||||||
|
"""Handle GET_EXPOSURE command"""
|
||||||
|
try:
|
||||||
|
value = self.src.get_property("exposure")
|
||||||
|
return f"OK {value}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
def handle_set_framerate(self, parts):
|
||||||
|
"""Handle SET_FRAMERATE command"""
|
||||||
|
if len(parts) != 2:
|
||||||
|
return "ERROR INVALID_SYNTAX: Usage: SET_FRAMERATE <value>"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
self.src.set_property("framerate", value)
|
||||||
|
actual = self.src.get_property("framerate")
|
||||||
|
return f"OK {actual}"
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return "ERROR INVALID_SYNTAX: Framerate must be a number"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
def handle_get_framerate(self):
|
||||||
|
"""Handle GET_FRAMERATE command"""
|
||||||
|
try:
|
||||||
|
value = self.src.get_property("framerate")
|
||||||
|
return f"OK {value}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
def handle_status(self):
|
||||||
|
"""Handle STATUS command"""
|
||||||
|
try:
|
||||||
|
exposure = self.src.get_property("exposure")
|
||||||
|
framerate = self.src.get_property("framerate")
|
||||||
|
|
||||||
|
# Get pipeline state
|
||||||
|
state = "UNKNOWN"
|
||||||
|
if self.pipeline:
|
||||||
|
_, current_state, _ = self.pipeline.get_state(0)
|
||||||
|
state = current_state.value_nick.upper()
|
||||||
|
|
||||||
|
return f"OK exposure={exposure} framerate={framerate} state={state}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
def send_response(self, response, addr):
|
||||||
|
"""Send response back to client"""
|
||||||
|
try:
|
||||||
|
self.sock.sendto((response + '\n').encode(), addr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to send response: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Exposure in seconds (e.g., 0.016)
|
||||||
|
src.set_property("exposure", 0.016)
|
||||||
|
|
||||||
|
# Frame rate
|
||||||
|
src.set_property("framerate", 22)
|
||||||
|
|
||||||
|
# Video crop to remove bottom 3 pixels
|
||||||
|
videocrop = Gst.ElementFactory.make("videocrop", "crop")
|
||||||
|
videocrop.set_property("bottom", 3)
|
||||||
|
|
||||||
|
# Queue for buffering
|
||||||
|
queue = Gst.ElementFactory.make("queue", "queue")
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Add elements to pipeline
|
||||||
|
pipeline.add(src)
|
||||||
|
pipeline.add(videocrop)
|
||||||
|
pipeline.add(queue)
|
||||||
|
pipeline.add(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")
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Create and start control server
|
||||||
|
control_server = ControlServer(src, pipeline, port=5001)
|
||||||
|
control_server.start()
|
||||||
|
|
||||||
|
# Start the pipeline
|
||||||
|
ret = pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Wait until error or EOS
|
||||||
|
bus = pipeline.get_bus()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Use timeout to allow Ctrl+C to be caught quickly
|
||||||
|
msg = bus.timed_pop_filtered(
|
||||||
|
100 * Gst.MSECOND, # 100ms timeout
|
||||||
|
Gst.MessageType.ERROR | Gst.MessageType.EOS | Gst.MessageType.STATE_CHANGED
|
||||||
|
)
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
t = msg.type
|
||||||
|
if t == Gst.MessageType.ERROR:
|
||||||
|
err, debug = msg.parse_error()
|
||||||
|
print(f"ERROR: {err.message}")
|
||||||
|
print(f"Debug info: {debug}")
|
||||||
|
break
|
||||||
|
elif t == Gst.MessageType.EOS:
|
||||||
|
print("End-Of-Stream reached")
|
||||||
|
break
|
||||||
|
elif t == Gst.MessageType.STATE_CHANGED:
|
||||||
|
if msg.src == pipeline:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted by user")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
print("Stopping control server...")
|
||||||
|
control_server.stop()
|
||||||
|
print("Stopping pipeline...")
|
||||||
|
pipeline.set_state(Gst.State.NULL)
|
||||||
|
print("Pipeline stopped")
|
||||||
159
scripts/test_exposure_control.py
Normal file
159
scripts/test_exposure_control.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.8"
|
||||||
|
# dependencies = []
|
||||||
|
# ///
|
||||||
|
|
||||||
|
"""
|
||||||
|
Test client for UDP exposure control
|
||||||
|
Usage: uv run scripts/test_exposure_control.py
|
||||||
|
|
||||||
|
This script tests the UDP control interface for the IDS uEye camera.
|
||||||
|
Make sure launch-ids.py is running before executing this test.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def send_command(command, host="127.0.0.1", port=5001, timeout=1.0):
|
||||||
|
"""Send a command and return the response"""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send command
|
||||||
|
sock.sendto(command.encode() + b'\n', (host, port))
|
||||||
|
|
||||||
|
# Receive response
|
||||||
|
response, _ = sock.recvfrom(1024)
|
||||||
|
return response.decode().strip()
|
||||||
|
|
||||||
|
except socket.timeout:
|
||||||
|
return "ERROR: Timeout waiting for response (is launch-ids.py running?)"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {e}"
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
def print_test(test_num, description, command, response):
|
||||||
|
"""Print formatted test result"""
|
||||||
|
print(f"\nTest {test_num}: {description}")
|
||||||
|
print(f" Command: {command}")
|
||||||
|
print(f" Response: {response}")
|
||||||
|
|
||||||
|
# Check if response indicates success
|
||||||
|
if response.startswith("OK"):
|
||||||
|
print(" ✓ PASS")
|
||||||
|
elif response.startswith("ERROR"):
|
||||||
|
if "OUT_OF_RANGE" in response or "INVALID" in response:
|
||||||
|
print(" ✓ PASS (Expected error)")
|
||||||
|
else:
|
||||||
|
print(" ✗ FAIL (Unexpected error)")
|
||||||
|
else:
|
||||||
|
print(" ? UNKNOWN")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("UDP Exposure Control Test Client")
|
||||||
|
print("=" * 70)
|
||||||
|
print("Testing UDP control interface on 127.0.0.1:5001")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check if server is reachable
|
||||||
|
print("Checking if control server is reachable...")
|
||||||
|
response = send_command("STATUS", timeout=2.0)
|
||||||
|
if "Timeout" in response:
|
||||||
|
print("✗ FAILED: Control server not responding")
|
||||||
|
print(" Make sure launch-ids.py is running first!")
|
||||||
|
sys.exit(1)
|
||||||
|
print("✓ Control server is reachable\n")
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 1: Get current exposure
|
||||||
|
response = send_command("GET_EXPOSURE")
|
||||||
|
print_test(1, "Get current exposure", "GET_EXPOSURE", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 2: Set exposure to 10ms
|
||||||
|
response = send_command("SET_EXPOSURE 0.110")
|
||||||
|
print_test(2, "Set exposure to 10ms", "SET_EXPOSURE 0.010", response)
|
||||||
|
time.sleep(5.2)
|
||||||
|
|
||||||
|
# Test 3: Verify exposure was set
|
||||||
|
response = send_command("GET_EXPOSURE")
|
||||||
|
print_test(3, "Verify exposure changed", "GET_EXPOSURE", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 4: Set exposure to 20ms
|
||||||
|
response = send_command("SET_EXPOSURE 0.020")
|
||||||
|
print_test(4, "Set exposure to 20ms", "SET_EXPOSURE 0.020", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 5: Get framerate
|
||||||
|
response = send_command("GET_FRAMERATE")
|
||||||
|
print_test(5, "Get current framerate", "GET_FRAMERATE", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 6: Set framerate
|
||||||
|
response = send_command("SET_FRAMERATE 30")
|
||||||
|
print_test(6, "Set framerate to 30 fps", "SET_FRAMERATE 30", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 7: Verify framerate
|
||||||
|
response = send_command("GET_FRAMERATE")
|
||||||
|
print_test(7, "Verify framerate changed", "GET_FRAMERATE", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 8: Get status
|
||||||
|
response = send_command("STATUS")
|
||||||
|
print_test(8, "Get pipeline status", "STATUS", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 9: Invalid command
|
||||||
|
response = send_command("INVALID_CMD")
|
||||||
|
print_test(9, "Send invalid command", "INVALID_CMD", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 10: Out of range exposure (too high)
|
||||||
|
response = send_command("SET_EXPOSURE 5.0")
|
||||||
|
print_test(10, "Out of range exposure (5.0s)", "SET_EXPOSURE 5.0", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 11: Out of range exposure (too low)
|
||||||
|
response = send_command("SET_EXPOSURE 0.0001")
|
||||||
|
print_test(11, "Out of range exposure (0.1ms)", "SET_EXPOSURE 0.0001", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 12: Invalid syntax (missing parameter)
|
||||||
|
response = send_command("SET_EXPOSURE")
|
||||||
|
print_test(12, "Invalid syntax (missing param)", "SET_EXPOSURE", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 13: Invalid syntax (non-numeric)
|
||||||
|
response = send_command("SET_EXPOSURE abc")
|
||||||
|
print_test(13, "Invalid syntax (non-numeric)", "SET_EXPOSURE abc", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 14: Restore original exposure (16ms)
|
||||||
|
response = send_command("SET_EXPOSURE 0.016")
|
||||||
|
print_test(14, "Restore exposure to 16ms", "SET_EXPOSURE 0.016", response)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Test 15: Restore original framerate (22 fps)
|
||||||
|
response = send_command("SET_FRAMERATE 22")
|
||||||
|
print_test(15, "Restore framerate to 22 fps", "SET_FRAMERATE 22", response)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("Test completed!")
|
||||||
|
print()
|
||||||
|
print("Quick reference:")
|
||||||
|
print(" echo 'SET_EXPOSURE 0.010' | nc -u 127.0.0.1 5001")
|
||||||
|
print(" echo 'GET_EXPOSURE' | nc -u 127.0.0.1 5001")
|
||||||
|
print(" echo 'STATUS' | nc -u 127.0.0.1 5001")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
scripts/udp_backup.reg
Normal file
24
scripts/udp_backup.reg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters]
|
||||||
|
; "DefaultReceiveWindow" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters]
|
||||||
|
; "LargeBufferSize" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters]
|
||||||
|
; "MediumBufferSize" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
|
||||||
|
; "TcpWindowSize" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
|
||||||
|
; "MaxConnectionsPerServer" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
|
||||||
|
; "MaxFreeTcbs" was not set
|
||||||
|
|
||||||
|
; [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
|
||||||
|
; "DefaultTTL" was not set
|
||||||
|
|
||||||
|
|
||||||
206
scripts/visualize_line_realtime.py
Normal file
206
scripts/visualize_line_realtime.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.8"
|
||||||
|
# dependencies = [
|
||||||
|
# "numpy>=1.24.0",
|
||||||
|
# "matplotlib>=3.7.0",
|
||||||
|
# ]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
"""
|
||||||
|
Real-time Line Visualization for Camera Data
|
||||||
|
Displays RGB/BGR channel values across the line width in real-time
|
||||||
|
|
||||||
|
Usage: uv run visualize_line_realtime.py [--format BGR|RGB] [--port 5000]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.animation as animation
|
||||||
|
import argparse
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
parser = argparse.ArgumentParser(description='Real-time line channel visualization')
|
||||||
|
parser.add_argument('--format', type=str, default='BGR', choices=['BGR', 'RGB'],
|
||||||
|
help='Input format (default: BGR)')
|
||||||
|
parser.add_argument('--port', type=int, default=5000,
|
||||||
|
help='UDP port (default: 5000)')
|
||||||
|
parser.add_argument('--width', type=int, default=2456,
|
||||||
|
help='Line width in pixels (default: 2456)')
|
||||||
|
parser.add_argument('--fps-limit', type=int, default=30,
|
||||||
|
help='Maximum display fps (default: 30)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Stream parameters
|
||||||
|
LINE_WIDTH = args.width
|
||||||
|
LINE_HEIGHT = 1
|
||||||
|
CHANNELS = 3
|
||||||
|
FRAME_SIZE = LINE_WIDTH * LINE_HEIGHT * CHANNELS
|
||||||
|
|
||||||
|
UDP_IP = "0.0.0.0"
|
||||||
|
UDP_PORT = args.port
|
||||||
|
|
||||||
|
# Create UDP socket with minimal buffer to avoid buffering old packets
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536) # Minimal buffer (64KB)
|
||||||
|
sock.setblocking(False) # Non-blocking for animation
|
||||||
|
sock.bind((UDP_IP, UDP_PORT))
|
||||||
|
|
||||||
|
print(f"Receiving {LINE_WIDTH}x{LINE_HEIGHT} {args.format} on UDP port {UDP_PORT}")
|
||||||
|
print(f"Display update rate: {args.fps_limit} fps max")
|
||||||
|
print("Close the plot window to exit")
|
||||||
|
|
||||||
|
# Initialize plot
|
||||||
|
fig, axes = plt.subplots(2, 1, figsize=(15, 8))
|
||||||
|
fig.suptitle(f'Real-time {args.format} Channel Visualization - Line Sensor',
|
||||||
|
fontsize=14, fontweight='bold')
|
||||||
|
|
||||||
|
# Channel order based on format
|
||||||
|
if args.format == 'BGR':
|
||||||
|
channel_names = ['Blue', 'Green', 'Red']
|
||||||
|
channel_colors = ['b', 'g', 'r']
|
||||||
|
channel_indices = [0, 1, 2] # BGR order
|
||||||
|
else: # RGB
|
||||||
|
channel_names = ['Red', 'Green', 'Blue']
|
||||||
|
channel_colors = ['r', 'g', 'b']
|
||||||
|
channel_indices = [0, 1, 2] # RGB order
|
||||||
|
|
||||||
|
# Initialize line data
|
||||||
|
x_data = np.arange(LINE_WIDTH)
|
||||||
|
y_data = [np.zeros(LINE_WIDTH) for _ in range(CHANNELS)]
|
||||||
|
y_grayscale = np.zeros(LINE_WIDTH) # Combined grayscale
|
||||||
|
|
||||||
|
# Top plot - GRAYSCALE ONLY
|
||||||
|
line_gray, = axes[0].plot(x_data, y_grayscale, 'k-', linewidth=1.0)
|
||||||
|
|
||||||
|
axes[0].set_xlim(0, LINE_WIDTH)
|
||||||
|
axes[0].set_ylim(0, 255)
|
||||||
|
axes[0].set_xlabel('Pixel Position')
|
||||||
|
axes[0].set_ylabel('Grayscale Value')
|
||||||
|
axes[0].set_title('Grayscale (Luminance-weighted)')
|
||||||
|
axes[0].grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Bottom plot - RGB/BGR channels with color
|
||||||
|
lines_separate = []
|
||||||
|
for i in range(CHANNELS):
|
||||||
|
line, = axes[1].plot(x_data, y_data[i], channel_colors[i] + '-',
|
||||||
|
label=channel_names[i], alpha=0.7, linewidth=0.8)
|
||||||
|
lines_separate.append(line)
|
||||||
|
|
||||||
|
axes[1].set_xlim(0, LINE_WIDTH)
|
||||||
|
axes[1].set_ylim(0, 255)
|
||||||
|
axes[1].set_xlabel('Pixel Position')
|
||||||
|
axes[1].set_ylabel('Pixel Value')
|
||||||
|
axes[1].set_title(f'{args.format} Channels: {" | ".join(channel_names)}')
|
||||||
|
axes[1].legend(loc='upper right')
|
||||||
|
axes[1].grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Statistics text
|
||||||
|
stats_text = axes[0].text(0.02, 0.98, '', transform=axes[0].transAxes,
|
||||||
|
verticalalignment='top', fontfamily='monospace',
|
||||||
|
fontsize=9, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
|
||||||
|
|
||||||
|
# Frame counter
|
||||||
|
frame_count = [0]
|
||||||
|
last_update = [0]
|
||||||
|
fps_buffer = deque(maxlen=30)
|
||||||
|
|
||||||
|
# Animation update function
|
||||||
|
def update_plot(frame):
|
||||||
|
"""Update plot with new UDP data"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
if args.fps_limit > 0:
|
||||||
|
min_interval = 1.0 / args.fps_limit
|
||||||
|
if current_time - last_update[0] < min_interval:
|
||||||
|
return [line_gray] + lines_separate + [stats_text]
|
||||||
|
|
||||||
|
# Drain all buffered packets and only use the latest one
|
||||||
|
latest_data = None
|
||||||
|
packets_drained = 0
|
||||||
|
try:
|
||||||
|
# Read all available packets, keep only the last one
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data, addr = sock.recvfrom(65536)
|
||||||
|
if len(data) == FRAME_SIZE:
|
||||||
|
latest_data = data
|
||||||
|
packets_drained += 1
|
||||||
|
except BlockingIOError:
|
||||||
|
# No more packets available
|
||||||
|
break
|
||||||
|
|
||||||
|
# Only process if we got valid data
|
||||||
|
if latest_data is None:
|
||||||
|
return [line_gray] + lines_separate + [stats_text]
|
||||||
|
|
||||||
|
# Parse frame
|
||||||
|
line_data = np.frombuffer(latest_data, dtype=np.uint8).reshape((LINE_HEIGHT, LINE_WIDTH, CHANNELS))
|
||||||
|
|
||||||
|
# Extract channels based on format
|
||||||
|
for i in range(CHANNELS):
|
||||||
|
y_data[i] = line_data[0, :, channel_indices[i]]
|
||||||
|
|
||||||
|
# Calculate grayscale (luminance using standard weights for RGB)
|
||||||
|
# For BGR: weights are [0.114, 0.587, 0.299]
|
||||||
|
# For RGB: weights are [0.299, 0.587, 0.114]
|
||||||
|
if args.format == 'BGR':
|
||||||
|
y_grayscale = (0.114 * y_data[0] + 0.587 * y_data[1] + 0.299 * y_data[2])
|
||||||
|
else: # RGB
|
||||||
|
y_grayscale = (0.299 * y_data[0] + 0.587 * y_data[1] + 0.114 * y_data[2])
|
||||||
|
|
||||||
|
# Update top plot (grayscale only)
|
||||||
|
line_gray.set_ydata(y_grayscale)
|
||||||
|
|
||||||
|
# Update bottom plot (RGB/BGR channels)
|
||||||
|
for i, line in enumerate(lines_separate):
|
||||||
|
line.set_ydata(y_data[i])
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
stats = []
|
||||||
|
for i in range(CHANNELS):
|
||||||
|
ch_data = y_data[i]
|
||||||
|
stats.append(f"{channel_names[i]:5s}: min={ch_data.min():3d} max={ch_data.max():3d} "
|
||||||
|
f"mean={ch_data.mean():6.2f} std={ch_data.std():6.2f}")
|
||||||
|
|
||||||
|
# Add grayscale stats
|
||||||
|
stats.append(f"Gray : min={y_grayscale.min():6.2f} max={y_grayscale.max():6.2f} "
|
||||||
|
f"mean={y_grayscale.mean():6.2f} std={y_grayscale.std():6.2f}")
|
||||||
|
|
||||||
|
# Calculate FPS
|
||||||
|
frame_count[0] += 1
|
||||||
|
if last_update[0] > 0:
|
||||||
|
fps = 1.0 / (current_time - last_update[0])
|
||||||
|
fps_buffer.append(fps)
|
||||||
|
avg_fps = np.mean(fps_buffer)
|
||||||
|
else:
|
||||||
|
avg_fps = 0
|
||||||
|
|
||||||
|
last_update[0] = current_time
|
||||||
|
|
||||||
|
# Update stats text
|
||||||
|
stats_str = f"Frame: {frame_count[0]} FPS: {avg_fps:.1f}\n" + "\n".join(stats)
|
||||||
|
stats_text.set_text(stats_str)
|
||||||
|
|
||||||
|
except BlockingIOError:
|
||||||
|
# No data available
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
return [line_gray] + lines_separate + [stats_text]
|
||||||
|
|
||||||
|
# Set up animation with blit for better performance
|
||||||
|
ani = animation.FuncAnimation(fig, update_plot, interval=10, blit=True, cache_frame_data=False)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
sock.close()
|
||||||
|
print(f"\nReceived {frame_count[0]} frames total")
|
||||||
Loading…
x
Reference in New Issue
Block a user