Compare commits

..

11 Commits

Author SHA1 Message Date
yair
b9d664ad23 scripts: make append_signals.py memory-efficient using ffmpeg streaming
Rewrite to use ffmpeg's tile and loop filters instead of Pillow stitching.
This eliminates the need to load entire panorama into memory.

Changes:
- Remove Pillow dependency
- Use ffmpeg's tile filter to create horizontal panorama
- Use loop filter to repeat the tiled frame
- Apply scrolling crop filter for animation
- No intermediate stitched image file created
- Memory usage stays constant regardless of image count

Benefits for large datasets (e.g., 1379 frames):
- Old approach: ~2.6GB in RAM (2.6M x 1005 pixels)
- New approach: Constant low memory (streams through ffmpeg)

Tested with 5 frames: creates 10s video, 304 frames, 2.61MB
2025-11-22 17:12:07 +02:00
yair
bfc8756a2a scripts: rewrite append_signals.py for linescan scrolling video
Complete rewrite to properly handle linescan image sequences:
- Stitches linescan images horizontally into wide panorama
- Creates scrolling video that pans left-to-right
- Configurable scroll speed based on capture rate (750 lines/sec)
- Output saved one folder up from image source
- Uses Pillow for image stitching, ffmpeg for video creation

Features:
- --scroll-speed: multiplier for playback speed (1.0 = real-time)
- --lines-per-second: linescan capture rate (default: 750)
- --max-frames: limit frames for testing
- --fps: output video framerate (default: 30)
- --width: viewport width (default: 1920)
- Automatic cleanup of temporary stitched image

Example usage:
  # Real-time playback
  uv run scripts\append_signals.py results\20251122\bumpy-filter

  # 2x speed
  uv run scripts\append_signals.py results\20251122\bumpy-filter --scroll-speed 2.0

  # Test with 10 frames
  uv run scripts\append_signals.py results\20251122\bumpy-filter --max-frames 10
2025-11-22 16:59:41 +02:00
yair
11b279cb08 scripts: rewrite append_signals.py for linescan scrolling video
Complete rewrite to properly handle linescan image sequences:
- Stitches linescan images horizontally into wide panorama
- Creates scrolling video that pans left-to-right
- Configurable scroll speed based on capture rate (750 lines/sec)
- Output saved one folder up from image source
- Uses Pillow for image stitching, ffmpeg for video creation

Features:
- --scroll-speed: multiplier for playback speed (1.0 = real-time)
- --lines-per-second: linescan capture rate (default: 750)
- --max-frames: limit frames for testing
- --fps: output video framerate (default: 30)
- --width: viewport width (default: 1920)
- Automatic cleanup of temporary stitched image

Example usage:
  # Real-time playback
  uv run scripts\append_signals.py results\20251122\bumpy-filter

  # 2x speed
  uv run scripts\append_signals.py results\20251122\bumpy-filter --scroll-speed 2.0

  # Test with 10 frames
  uv run scripts\append_signals.py results\20251122\bumpy-filter --max-frames 10
2025-11-22 16:59:26 +02:00
yair
411310e4f1 scripts: add append_signals.py for frame sequence to video conversion
Add Python script to concatenate frame sequences into videos using ffmpeg.

Features:
- Handles non-sequential frames with gaps in numbering
- Automatic output naming: {folder_name}_{unix_timestamp}.mp4
- Configurable FPS, width, quality (CRF), and frame limit
- Uses uv inline dependencies for easy execution
- Supports .jpeg, .jpg, and .png files

Example usage:
  uv run scripts\append_signals.py results\20251122\bumpy-filter
  uv run scripts\append_signals.py results\20251122\bumpy-filter --max-frames 100 --fps 60
2025-11-22 16:50:09 +02:00
yair
11350301b9 intervalometer: reduce verbosity of deadband messages
Change deadband zone message from DEBUG to LOG level to prevent
spam while still showing gain/exposure adjustment messages.

This allows using --gst-debug to see important ramping information
without flooding the output with 'Within deadband zone' messages
every frame.
2025-11-22 16:42:27 +02:00
yair
ddb30e00e1 deprcat rollingsum filter 2025-11-22 12:08:49 +02:00
yair
89f904cd71 align with linescan example defaults 2025-11-22 09:55:33 +02:00
yair
a81587e8c6 slow and debug works 2025-11-22 09:52:45 +02:00
yair
d2c1030354 wteak settings 2025-11-21 17:46:53 +02:00
yair
45fa4c07b4 Add brightness-deadband property to intervalometer to prevent oscillation
- Add new brightness-deadband property (default 10.0, range 0.0-50.0)
- Implements deadband/tolerance zone around target brightness
- When brightness is within ±deadband, no adjustments are made
- Prevents oscillation at fast update rates (10-100ms)
- Allows fast corrections when brightness significantly deviates
- Enables fast ramp rates without flicker/oscillation
- Updated README with detailed deadband documentation and usage examples
- Solves exposure fluctuation issue at high frame rates with fast updates
2025-11-21 13:46:19 +02:00
yair
93d8da45e6 rem prints 2025-11-21 13:40:29 +02:00
13 changed files with 527 additions and 1313 deletions

View File

@@ -35,7 +35,7 @@ udpsink host=10.81.2.183 port=5000
now moving to automatic exposure (see [gstintervalometer](gst\intervalometer\gstintervalometer.c)) now moving to automatic exposure (see [gstintervalometer](gst\intervalometer\gstintervalometer.c))
```pwsh ```pwsh
uv run .\scripts\launch-ids.py ` uv run .\scripts\launch-ids.py `
--config .\ini\2456x4pix-500top-cw.ini ` --config .\ini\2456x4pix-500top-cw-extragain.ini `
--device-id 1 ` --device-id 1 `
--framerate 750 ` --framerate 750 `
--gain 52 ` --gain 52 `
@@ -49,11 +49,27 @@ uv run .\scripts\launch-ids.py `
intervalometer enabled=true camera-element=cam ` intervalometer enabled=true camera-element=cam `
ramp-rate=vslow ` ramp-rate=vslow `
update-interval=1000 ` update-interval=1000 `
gain-ma52 ` gain=52 `
log-file=timelapse.csv ! ` log-file=timelapse.csv ! `
videocrop bottom=3 ! queue ! udpsink host=10.81.2.183 port=5000 videocrop bottom=3 ! queue ! udpsink host=10.81.2.183 port=5000
``` ```
```pwsh
$env:GST_DEBUG="linescan:5";
gst-launch-1.0 idsueyesrc config-file=ini/roi-night.ini `
exposure=5.25 framerate=200 gain=42 name=cam device-id=2 ! `
intervalometer enabled=true camera-element=cam `
ramp-rate=vslow `
update-interval=1000 `
gain-max=52 `
log-file=timelapse.csv ! `
videocrop bottom=3 ! `
queue ! `
linescan direction=vertical output-size=1900 ! `
videoconvert ! autovideosink
```
#### Receive and Display #### Receive and Display
```pwsh ```pwsh
uv run .\scripts\recv_raw_rolling.py --display-fps 60 uv run .\scripts\recv_raw_rolling.py --display-fps 60

View File

@@ -1,613 +0,0 @@
# GStreamer Rolling Sum Plugin - Complete Documentation
## Table of Contents
- [Overview](#overview)
- [How It Works](#how-it-works)
- [Architecture & Design](#architecture--design)
- [Plugin Properties](#plugin-properties)
- [Basic Usage](#basic-usage)
- [Debugging](#debugging)
- [CSV Analysis](#csv-analysis)
- [Recommended Thresholds](#recommended-thresholds)
- [Troubleshooting](#troubleshooting)
- [Performance](#performance)
- [Integration Examples](#integration-examples)
- [Developer Guide](#developer-guide)
- [References](#references)
## Overview
The `rollingsum` plugin analyzes video frames in real-time by tracking the mean pixel intensity of a specific column across frames. It maintains a rolling window of these values and can drop frames that deviate significantly from the rolling mean, useful for detecting and filtering unstable or anomalous frames.
**Element Name:** `rollingsum` - Transform element that analyzes pixel values and selectively drops frames
**Purpose:** Monitor a vertical column of pixels in video frames, calculate the rolling mean over a time window, and drop frames when the current frame's column mean deviates significantly from the rolling mean baseline.
## How It Works
1. **Column Analysis**: Extracts mean pixel intensity from a specified vertical column
2. **Rolling Window**: Maintains a circular buffer of recent column means
3. **Deviation Detection**: Calculates how much each frame deviates from the rolling mean
4. **Frame Filtering**: Optionally drops frames exceeding the deviation threshold
5. **CSV Logging**: Records all frame statistics for analysis
### Data Flow
```mermaid
graph TD
A[Video Frame] --> B[Extract Column]
B --> C[Calculate Column Mean]
C --> D[Store in Ring Buffer]
D --> E[Update Rolling Mean]
E --> F{Deviation > Threshold?}
F -->|Yes| G[DROP Frame]
F -->|No| H[PASS Frame]
C --> E
style G fill:#ff6b6b
style H fill:#51cf66
```
### Ring Buffer Operation
```mermaid
graph LR
subgraph Ring Buffer
A[0] --> B[1]
B --> C[2]
C --> D[...]
D --> E[N-1]
E ---|wrap| A
end
F[New Frame Mean] --> G[ring_index]
G --> A
H[Rolling Mean] --> I[Sum all values]
I --> J[Divide by count]
style G fill:#ffd43b
```
## Architecture & Design
### Base Class
- Inherits from `GstBaseTransform` (similar to [`select`](gst/select/gstselect.c))
- In-place transform (analysis only, no frame modification)
- Returns `GST_BASE_TRANSFORM_FLOW_DROPPED` to drop frames
- Returns `GST_FLOW_OK` to pass frames
### Element Structure
```c
struct _GstRollingSum
{
GstBaseTransform element;
/* Properties */
gint window_size; // Number of frames in rolling window (default: 1000)
gint column_index; // Which column to analyze (default: 1, second column)
gint stride; // Row sampling stride (default: 1, every row)
gdouble threshold; // Deviation threshold for dropping (default: 0.5)
gchar *csv_file; // CSV output file path (default: NULL)
/* State */
gdouble *ring_buffer; // Circular buffer of column means
gint ring_index; // Current position in ring buffer
gint frame_count; // Total frames processed
gdouble rolling_mean; // Current rolling mean
FILE *csv_fp; // CSV file pointer
};
```
### Algorithm (Simplified from wissotsky's cli.py)
**Per Frame Processing:**
1. **Extract column data:**
- Select column at column_index
- Sample every stride rows
- Calculate mean of sampled pixels: frame_mean
2. **Update ring buffer:**
- Store frame_mean in ring_buffer[ring_index]
- Increment ring_index (wrap around)
3. **Calculate rolling mean:**
- Sum values in ring buffer (up to window_size or frame_count)
- Divide by actual window size
4. **Calculate deviation:**
- deviation = abs(frame_mean - rolling_mean)
- normalized_deviation = deviation / 255.0 (for 8-bit video)
5. **Decision:**
- If normalized_deviation > threshold: DROP frame
- Else: PASS frame
**Key Simplifications from cli.py:**
- **No EMA tracking**: Use simple rolling mean instead of exponential moving average
- **No variance tracking**: Use fixed threshold instead of dynamic variance-based detection
- **No recording logic**: Just drop/pass, no buffering for output segments
- **No patience mechanism**: Immediate decision per frame
### Video Format Support
**Initial Implementation:**
- **Primary target**: Grayscale (GRAY8, GRAY16)
- **Secondary**: Bayer formats (common in machine vision)
**Caps Filter:**
```c
static GstStaticPadTemplate sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (
"video/x-raw, format=(string){GRAY8,GRAY16_LE,GRAY16_BE}; "
"video/x-bayer, format=(string){bggr,grbg,gbrg,rggb}"
)
);
```
## Plugin Properties
| Property | Type | Default | Range | Description |
|----------|------|---------|-------|-------------|
| `window-size` | int | 1000 | 1-100000 | Number of frames in rolling window |
| `column-index` | int | 1 | 0-width | Which vertical column to analyze (0-based) |
| `stride` | int | 1 | 1-height | Row sampling stride (1 = every row) |
| `threshold` | double | 0.5 | 0.0-1.0 | Normalized deviation threshold for dropping frames |
| `csv-file` | string | NULL | - | Path to CSV file for logging (NULL = no logging) |
### Understanding Normalized Deviation
- **Range**: 0.0 to 1.0
- **Calculation**: `absolute_deviation / 255.0` (for 8-bit video)
- **Meaning**: Fraction of the full pixel range
- `0.001` = deviation of ~0.255 pixel values
- `0.01` = deviation of ~2.55 pixel values
- `0.1` = deviation of ~25.5 pixel values
## Basic Usage
### Simple Pipeline
```powershell
gst-launch-1.0 idsueyesrc config-file=config.ini ! `
videoconvert ! `
video/x-raw,format=GRAY8 ! `
rollingsum window-size=1000 column-index=1 threshold=0.0002 ! `
autovideosink
```
### With CSV Logging
```powershell
gst-launch-1.0 idsueyesrc config-file=config.ini exposure=0.5 ! `
videoconvert ! `
video/x-raw,format=GRAY8 ! `
rollingsum window-size=1000 column-index=1 threshold=0.0002 csv-file=output.csv ! `
fakesink
```
### Custom Configuration
```powershell
gst-launch-1.0 idsueyesrc config-file=config.ini ! `
rollingsum window-size=5000 column-index=320 stride=2 threshold=0.3 ! `
queue ! `
autovideosink
```
### With Format Conversion
```powershell
gst-launch-1.0 idsueyesrc ! `
videoconvert ! `
video/x-raw,format=GRAY8 ! `
rollingsum ! `
autovideosink
```
## Debugging
### Enable Debug Output
Use the `GST_DEBUG` environment variable to see detailed plugin operation:
#### Windows PowerShell
```powershell
$env:GST_DEBUG="rollingsum:5"; gst-launch-1.0 [pipeline...]
```
#### Windows CMD
```cmd
set GST_DEBUG=rollingsum:5 && gst-launch-1.0 [pipeline...]
```
#### Linux/Mac
```bash
GST_DEBUG=rollingsum:5 gst-launch-1.0 [pipeline...]
```
### Debug Levels
| Level | Output |
|-------|--------|
| `rollingsum:1` | Errors only |
| `rollingsum:2` | Warnings |
| `rollingsum:3` | Info messages (file open/close) |
| `rollingsum:4` | Debug (caps negotiation) |
| `rollingsum:5` | Log (all frame processing) |
### Example Debug Output
```
0:00:04.029432200 DEBUG rollingsum gstrollingsum.c:436: Extracted column mean: 10.07
0:00:04.032257100 DEBUG rollingsum gstrollingsum.c:466: Frame 1: mean=10.07, rolling_mean=10.07, deviation=0.00 (normalized=0.0000)
```
**Key Fields:**
- `Frame N`: Frame number
- `mean`: Current frame's column mean
- `rolling_mean`: Average of last N frames (window-size)
- `deviation`: Absolute difference
- `normalized`: Deviation as fraction of 255
### Common Debug Scenarios
#### 1. Verify Plugin Loaded
```powershell
gst-inspect-1.0 rollingsum
```
Should show plugin details. If not found, check `GST_PLUGIN_PATH`.
#### 2. Check CSV File Creation
Look for this in debug output:
```
INFO rollingsum: Opened CSV file: output.csv
```
#### 3. Monitor Frame Drops
Look for:
```
DEBUG rollingsum: Dropping frame 42: deviation 0.0005 > threshold 0.0002
```
#### 4. Verify Caps Negotiation
```
DEBUG rollingsum: set_caps
DEBUG rollingsum: Video format: GRAY8, 1224x1026
```
## CSV Analysis
### CSV Format
The output CSV contains:
```csv
frame,column_mean,rolling_mean,deviation,normalized_deviation,dropped
1,10.071150,10.071150,0.000000,0.000000,0
2,10.059454,10.065302,0.005848,0.000023,0
...
```
### Analyze Results
Use the included analysis script:
```powershell
uv run scripts/analyze_sma.py output.csv
```
**Output includes:**
- Statistical summary (min/max/mean/std)
- Threshold recommendations based on percentiles
- Standard deviation-based suggestions
- Visualization plots saved to `results/debug/`
- Archived CSV with timestamp in `results/debug/`
**Output files are automatically organized:**
- `results/debug/output_YYYYMMDD_HHMMSS.csv` - Archived CSV
- `results/debug/output_analysis_YYYYMMDD_HHMMSS.png` - Analysis plots
The `results/` directory is gitignored to keep your repository clean.
### Interpreting Results
The analysis provides threshold recommendations:
| Percentile | Description | Use Case |
|------------|-------------|----------|
| 99th | Drops top 1% | Very conservative, catch only extreme outliers |
| 95th | Drops top 5% | Conservative, good for quality control |
| 90th | Drops top 10% | Balanced, moderate filtering |
| 75th | Drops top 25% | Aggressive, maximum quality |
## Recommended Thresholds
Based on analysis of stable camera footage:
### For General Use
```powershell
# Conservative (1-2% frame drop)
threshold=0.0003
# Moderate (5-10% frame drop)
threshold=0.0002
# Aggressive (20-25% frame drop)
threshold=0.0001
```
### For Specific Scenarios
**High-speed acquisition** (minimal processing):
```powershell
window-size=100 threshold=0.0005
```
**Quality-focused** (stable scenes):
```powershell
window-size=1000 threshold=0.0001
```
**Real-time monitoring** (fast response):
```powershell
window-size=50 threshold=0.0002
```
## Troubleshooting
### No frames being dropped (threshold too high)
**Symptom**: `dropped` column always 0 in CSV
**Solution**:
1. Run with CSV logging
2. Analyze with `uv run scripts/analyze_sma.py output.csv`
3. Use recommended threshold from 90th-99th percentile
### Too many frames dropped (threshold too low)
**Symptom**: Most frames have `dropped=1`, choppy video
**Solution**:
1. Increase threshold (try doubling current value)
2. Check if column_index is appropriate
3. Verify video is stable (not shaking/moving)
### CSV file not created
**Check**:
1. File path is writable
2. Look for "Opened CSV file" in debug output (`GST_DEBUG=rollingsum:3`)
3. Verify csv-file property is set correctly
### Column index out of range
**Symptom**:
```
WARNING rollingsum: Column index 1000 >= width 1224, using column 0
```
**Solution**: Set `column-index` to value < video width
### Inconsistent results
**Possible causes**:
1. Window size too small (< 50 frames)
2. Sampling moving/dynamic content
3. Column contains edge/artifact data
**Solutions**:
- Increase `window-size` to 500-1000
- Choose different `column-index` (avoid edges)
- Use `stride=2` or higher for faster processing
## Performance
### Performance Tips
1. **Larger window = more stable** but slower to adapt to scene changes
2. **Stride > 1** reduces computation but less accurate column mean
3. **CSV logging** has minimal performance impact
4. **Debug level 5** can produce massive logs, use only when needed
### Memory Usage
- Ring buffer: `window_size * sizeof(double)` = ~8KB for default 1000 frames
- Minimal per-frame allocation
### CPU Usage
- Column extraction: O(height/stride)
- Rolling mean update: O(1) using incremental sum
- Very lightweight compared to full-frame processing
### Optimization Opportunities
1. **Incremental mean**: Track sum instead of recalculating
2. **SIMD**: Vectorize column summation
3. **Skip calculation**: Only recalc every N frames if baseline is stable
## Integration Examples
### Python Script Control
```python
import subprocess
# Run pipeline with CSV logging
subprocess.run([
'gst-launch-1.0',
'idsueyesrc', 'config-file=config.ini',
'!', 'videoconvert',
'!', 'video/x-raw,format=GRAY8',
'!', 'rollingsum',
'window-size=1000',
'column-index=1',
'threshold=0.0002',
'csv-file=output.csv',
'!', 'fakesink'
])
# Analyze results
subprocess.run(['uv', 'run', 'scripts/analyze_sma.py', 'output.csv'])
```
### Adaptive Threshold
Use analysis results to set optimal threshold for next run:
```python
import pandas as pd
# Analyze previous run
df = pd.read_csv('output.csv')
recommended_threshold = df['normalized_deviation'].quantile(0.95)
print(f"Recommended threshold: {recommended_threshold:.6f}")
```
## Developer Guide
### Implementation Files
**Directory Structure:**
```
gst/rollingsum/
├── CMakeLists.txt
├── gstrollingsum.c
└── gstrollingsum.h
```
**gstrollingsum.h:**
- Element type definitions
- Structure declarations
- Property enums
- Function prototypes
**gstrollingsum.c:**
- GObject methods (init, dispose, get/set properties)
- GstBaseTransform methods (transform_ip)
- Helper functions (extract_column_mean, update_rolling_mean)
- Plugin registration
**CMakeLists.txt:**
- Build configuration (copy from [`gst/select/CMakeLists.txt`](gst/select/CMakeLists.txt))
- Link GStreamer base and video libraries
### Adding New Features
Key files:
- [`gst/rollingsum/gstrollingsum.c`](gst/rollingsum/gstrollingsum.c) - Main implementation
- [`gst/rollingsum/gstrollingsum.h`](gst/rollingsum/gstrollingsum.h) - Header/structures
- [`gst/rollingsum/CMakeLists.txt`](gst/rollingsum/CMakeLists.txt) - Build config
### Rebuild After Changes
```powershell
.\build.ps1 # Windows
```
```bash
./build.sh # Linux
```
### Testing
```powershell
# Quick test
gst-inspect-1.0 rollingsum
# Full pipeline test with debug
$env:GST_DEBUG="rollingsum:5"
gst-launch-1.0 videotestsrc ! rollingsum ! fakesink
```
### Testing Strategy
**Unit Tests:**
- Ring buffer wrapping
- Mean calculation accuracy
- Threshold comparison logic
**Integration Tests:**
- Pipeline with videotestsrc
- Pipeline with idsueyesrc
- Frame drop verification
- Property changes during playback
**Test Cases:**
1. Static video (all frames similar) → all pass
2. Single bright frame → that frame drops
3. Gradual change → frames pass
4. Periodic pattern → pattern frames drop
### Integration with Existing Project
**Build System:**
Update [`gst/CMakeLists.txt`](gst/CMakeLists.txt):
```cmake
add_subdirectory (rollingsum)
```
**Documentation:**
Update [`README.md`](README.md):
- Add rollingsum to "Other elements" section
- Add pipeline example
### Future Enhancements
**Phase 2 (If Needed):**
- Add EMA baseline tracking (like cli.py)
- Add variance-based thresholds
- Support multiple columns or regions
- Add metadata output (tag frames with deviation values)
- RGB format support (analyze specific channel)
**Phase 3 (Advanced):**
- Full cli.py recording logic
- Buffer and output segments
- Integration with probe detection systems
### Implementation Checklist
- [x] Create gst/rollingsum directory
- [x] Implement gstrollingsum.h
- [x] Implement gstrollingsum.c
- [x] Create CMakeLists.txt
- [x] Update gst/CMakeLists.txt
- [x] Build and test basic functionality
- [x] Test with idsueyesrc
- [x] Update README.md
- [x] Create feature branch
- [x] Commit and document
## References
- Original algorithm: `cli.py` lines 64-79 (column extraction and mean comparison)
- Template element: [`gst/select/gstselect.c`](gst/select/gstselect.c)
- GStreamer base transform: [GstBaseTransform documentation](https://gstreamer.freedesktop.org/documentation/base/gstbasetransform.html)
- [scripts/analyze_sma.py](scripts/analyze_sma.py) - Analysis tool
- GStreamer documentation: https://gstreamer.freedesktop.org/documentation/
## Support
For issues or questions:
1. Enable debug output (`$env:GST_DEBUG="rollingsum:5"` in PowerShell)
2. Generate CSV log and analyze
3. Check this guide's troubleshooting section
4. Review debug output for errors/warnings

View File

@@ -12,6 +12,5 @@ if (ENABLE_KLV)
endif () endif ()
add_subdirectory (misb) add_subdirectory (misb)
add_subdirectory (rollingsum)
add_subdirectory (select) add_subdirectory (select)
add_subdirectory (videoadjust) add_subdirectory (videoadjust)

View File

@@ -73,6 +73,7 @@ make install
| Property | Type | Range | Default | Description | | Property | Type | Range | Default | Description |
|----------|------|-------|---------|-------------| |----------|------|-------|---------|-------------|
| `brightness-smoothing` | double | 0.0-1.0 | 0.1 | Temporal smoothing factor (EMA alpha) | | `brightness-smoothing` | double | 0.0-1.0 | 0.1 | Temporal smoothing factor (EMA alpha) |
| `brightness-deadband` | double | 0.0-50.0 | 10.0 | Deadband zone to prevent oscillation (0=disabled) |
### Logging ### Logging
@@ -138,6 +139,7 @@ gst-launch-1.0 \
ramp-rate=vslow \ ramp-rate=vslow \
update-interval=1000 \ update-interval=1000 \
brightness-smoothing=0.1 \ brightness-smoothing=0.1 \
brightness-deadband=10.0 \
log-file=timelapse.csv ! \ log-file=timelapse.csv ! \
videocrop bottom=3 ! queue ! videoconvert ! autovideosink videocrop bottom=3 ! queue ! videoconvert ! autovideosink
``` ```
@@ -146,6 +148,7 @@ gst-launch-1.0 \
- `ramp-rate=vslow`: 5% exposure steps per update (smooth transitions) - `ramp-rate=vslow`: 5% exposure steps per update (smooth transitions)
- `update-interval=1000`: Updates every 1 second (not too aggressive) - `update-interval=1000`: Updates every 1 second (not too aggressive)
- `brightness-smoothing=0.1`: Filters out moving objects (cars, people, birds) - `brightness-smoothing=0.1`: Filters out moving objects (cars, people, birds)
- `brightness-deadband=10.0`: Prevents oscillation by creating a stable zone
### Complete Time-Lapse Recording ### Complete Time-Lapse Recording
@@ -166,14 +169,15 @@ gst-launch-1.0 idsueyesrc name=cam framerate=1 ! \
### Exposure Control Algorithm ### Exposure Control Algorithm
The filter uses a YASS-inspired algorithm: The filter uses a YASS-inspired algorithm with deadband control:
1. **Brightness Analysis**: Calculates average brightness of each frame 1. **Brightness Analysis**: Calculates average brightness of each frame
2. **Error Calculation**: Compares to target brightness (with compensation) 2. **Deadband Check**: If brightness is within deadband zone, skip adjustments (prevents oscillation)
3. **Ramping Priority**: 3. **Error Calculation**: Compares to target brightness (with compensation)
4. **Ramping Priority**:
- When too bright: Decreases exposure first, then gain - When too bright: Decreases exposure first, then gain
- When too dark: Increases exposure first (up to max), then gain - When too dark: Increases exposure first (up to max), then gain
4. **Smooth Ramping**: Changes are gradual based on ramp-rate setting 5. **Smooth Ramping**: Changes are gradual based on ramp-rate setting
### Typical Behavior ### Typical Behavior
@@ -246,6 +250,29 @@ smoothed_brightness = (alpha × current_brightness) + ((1 - alpha) × previous_s
**Effect:** With `brightness-smoothing=0.1`, the algorithm effectively averages brightness over ~10 frames, filtering out cars, people, and birds while still tracking slow lighting trends. **Effect:** With `brightness-smoothing=0.1`, the algorithm effectively averages brightness over ~10 frames, filtering out cars, people, and birds while still tracking slow lighting trends.
### Brightness Deadband
The `brightness-deadband` property creates a tolerance zone around the target brightness where no adjustments are made. This prevents oscillation caused by continuous micro-adjustments.
**How it works:**
- When brightness is within ±deadband of target, no changes are made
- When brightness exceeds the deadband, normal adjustments resume
- Allows fast corrections when needed, prevents hunting when stable
| Value | Behavior | Best For |
|-------|----------|----------|
| 0.0 | No deadband (disabled) | Maximum responsiveness (may oscillate) |
| 5.0 | Narrow deadband | Slow update rates (>500ms) |
| **10.0** | **Standard deadband (default)** | **Fast update rates (10-100ms), prevents oscillation** |
| 20.0 | Wide deadband | Very stable, less responsive |
**Example:**
- With `target-brightness=128` and `brightness-deadband=10.0`
- No adjustments when brightness is between 118-138
- Adjustments resume when brightness < 118 or > 138
**Important:** Higher deadband = more stability but less precision. Lower deadband = more precision but potential oscillation at fast update rates.
## Tips for Best Results ## Tips for Best Results
### Dawn/Dusk Time-Lapse (Recommended Configuration) ### Dawn/Dusk Time-Lapse (Recommended Configuration)
@@ -254,6 +281,7 @@ smoothed_brightness = (alpha × current_brightness) + ((1 - alpha) × previous_s
ramp-rate: vslow (5% steps - very gradual) ramp-rate: vslow (5% steps - very gradual)
update-interval: 1000 (1 second between updates) update-interval: 1000 (1 second between updates)
brightness-smoothing: 0.1 (filter moving objects) brightness-smoothing: 0.1 (filter moving objects)
brightness-deadband: 10.0 (prevent oscillation)
exposure-min: 0.85 (or camera minimum) exposure-min: 0.85 (or camera minimum)
exposure-max: 1.24 (or 1/framerate) exposure-max: 1.24 (or 1/framerate)
gain-min: 0 (cleanest image) gain-min: 0 (cleanest image)
@@ -267,15 +295,18 @@ target-brightness: 128
- Brightness smoothing filters transient changes (cars, people) - Brightness smoothing filters transient changes (cars, people)
- Results in smooth, flicker-free time-lapse - Results in smooth, flicker-free time-lapse
### Fast Changing Conditions ### Fast Changing Conditions (with Fast Update Rates)
``` ```
ramp-rate: fast or vfast ramp-rate: fast or vfast
update-interval: 100-500 update-interval: 10-100 (very fast updates)
brightness-smoothing: 0.3-1.0 (more responsive) brightness-smoothing: 0.3-1.0 (more responsive)
brightness-deadband: 10.0-15.0 (ESSENTIAL to prevent oscillation)
compensation: Adjust to preference (-1.0 for darker, +1.0 for brighter) compensation: Adjust to preference (-1.0 for darker, +1.0 for brighter)
``` ```
**Critical:** When using fast update rates (10-100ms), `brightness-deadband` is ESSENTIAL to prevent oscillation. Without it, the algorithm will continuously overshoot and create flickering.
### Maximum Image Quality ### Maximum Image Quality
``` ```
@@ -284,13 +315,15 @@ ramp-rate: slow or vslow (smoother transitions)
update-interval: 1000-2000 update-interval: 1000-2000
``` ```
### Avoiding Flickering ### Avoiding Flickering and Oscillation
If you experience flickering or oscillation: If you experience flickering or oscillation:
1. **Increase update-interval**: Start with 1000ms 1. **Enable deadband (MOST IMPORTANT)**: Set `brightness-deadband=10.0` or higher
2. **Decrease ramp-rate**: Use `vslow` or `slow` 2. **Increase update-interval**: Start with 1000ms for slow changes, or keep at 10-100ms with deadband for fast response
3. **Enable brightness-smoothing**: Set to 0.1 or lower 3. **Decrease ramp-rate**: Use `vslow` or `slow`
4. **Check your settings**: At 50fps, 100ms updates = every 5 frames (too fast!) 4. **Enable brightness-smoothing**: Set to 0.1 or lower
**The New Solution:** With the `brightness-deadband` parameter, you can now use fast update rates (10ms) with fast ramp rates without oscillation! The deadband creates a stable zone that prevents continuous micro-adjustments.
## Troubleshooting ## Troubleshooting
@@ -300,10 +333,11 @@ If you experience flickering or oscillation:
- Ensure `enabled=true` is set - Ensure `enabled=true` is set
**Flickering or oscillating exposure:** **Flickering or oscillating exposure:**
- **Primary cause:** Update interval too fast for your frame rate - **Primary cause:** No deadband zone at fast update rates
- **Solution:** Increase `update-interval` to 1000ms - **Solution:** Set `brightness-deadband=10.0` (or higher)
- **Alternative:** Increase `update-interval` to 1000ms
- **Also try:** Set `ramp-rate=vslow` and `brightness-smoothing=0.1` - **Also try:** Set `ramp-rate=vslow` and `brightness-smoothing=0.1`
- **At 50fps:** Never use update intervals < 500ms - **New capability:** With deadband enabled, you CAN use fast update intervals (10-100ms) for rapid response without oscillation!
**Changes too fast/slow:** **Changes too fast/slow:**
- Adjust `ramp-rate` property - Adjust `ramp-rate` property
@@ -359,6 +393,18 @@ The original implementation used GObject property specs to query exposure limits
Added Exponential Moving Average (EMA) filtering to handle transient brightness changes from moving objects (cars, people, birds). This prevents exposure oscillation while maintaining responsiveness to actual lighting changes. Added Exponential Moving Average (EMA) filtering to handle transient brightness changes from moving objects (cars, people, birds). This prevents exposure oscillation while maintaining responsiveness to actual lighting changes.
### Brightness Deadband (Anti-Oscillation)
Added deadband control to prevent continuous micro-adjustments that cause oscillation. When brightness is within the deadband zone (default ±10 units), no adjustments are made. This allows:
- Fast update rates (10-100ms) without oscillation
- Rapid response when changes exceed deadband
- Stable operation at any ramp rate
**Implementation in [`gstintervalometer.c:698-707`](gst/intervalometer/gstintervalometer.c:698-707):**
- Checks absolute error against deadband before making adjustments
- Skips exposure/gain changes when within tolerance
- Allows full-speed corrections when brightness significantly deviates
## License ## License
This filter is part of gst-plugins-vision and released under the GNU Library General Public License (LGPL). This filter is part of gst-plugins-vision and released under the GNU Library General Public License (LGPL).

View File

@@ -62,7 +62,8 @@ enum
PROP_LOG_FILE, PROP_LOG_FILE,
PROP_CAMERA_ELEMENT, PROP_CAMERA_ELEMENT,
PROP_UPDATE_INTERVAL, PROP_UPDATE_INTERVAL,
PROP_BRIGHTNESS_SMOOTHING PROP_BRIGHTNESS_SMOOTHING,
PROP_BRIGHTNESS_DEADBAND
}; };
#define DEFAULT_PROP_ENABLED TRUE #define DEFAULT_PROP_ENABLED TRUE
@@ -77,6 +78,7 @@ enum
#define DEFAULT_PROP_CAMERA_ELEMENT "" #define DEFAULT_PROP_CAMERA_ELEMENT ""
#define DEFAULT_PROP_UPDATE_INTERVAL 100 /* Update every 100ms (10 Hz) */ #define DEFAULT_PROP_UPDATE_INTERVAL 100 /* Update every 100ms (10 Hz) */
#define DEFAULT_PROP_BRIGHTNESS_SMOOTHING 0.1 /* 10% new, 90% history - heavy smoothing for time-lapse */ #define DEFAULT_PROP_BRIGHTNESS_SMOOTHING 0.1 /* 10% new, 90% history - heavy smoothing for time-lapse */
#define DEFAULT_PROP_BRIGHTNESS_DEADBAND 10.0 /* ±10 brightness units deadband zone */
/* GStreamer boilerplate */ /* GStreamer boilerplate */
#define gst_intervalometer_parent_class parent_class #define gst_intervalometer_parent_class parent_class
@@ -242,6 +244,14 @@ gst_intervalometer_class_init (GstIntervalometerClass * klass)
0.0, 1.0, DEFAULT_PROP_BRIGHTNESS_SMOOTHING, 0.0, 1.0, DEFAULT_PROP_BRIGHTNESS_SMOOTHING,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_BRIGHTNESS_DEADBAND,
g_param_spec_double ("brightness-deadband", "Brightness Deadband",
"Deadband zone around target brightness where no adjustments are made (0=disabled). "
"Creates a stable zone to prevent oscillation. "
"Recommended: 10.0 for fast update rates, 5.0 for slower rates, 0.0 to disable",
0.0, 50.0, DEFAULT_PROP_BRIGHTNESS_DEADBAND,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/* Set element metadata */ /* Set element metadata */
gst_element_class_add_pad_template (gstelement_class, gst_element_class_add_pad_template (gstelement_class,
gst_static_pad_template_get (&gst_intervalometer_sink_template)); gst_static_pad_template_get (&gst_intervalometer_sink_template));
@@ -278,6 +288,7 @@ gst_intervalometer_init (GstIntervalometer * filter)
filter->camera_element_name = g_strdup (DEFAULT_PROP_CAMERA_ELEMENT); filter->camera_element_name = g_strdup (DEFAULT_PROP_CAMERA_ELEMENT);
filter->update_interval = DEFAULT_PROP_UPDATE_INTERVAL; filter->update_interval = DEFAULT_PROP_UPDATE_INTERVAL;
filter->brightness_smoothing = DEFAULT_PROP_BRIGHTNESS_SMOOTHING; filter->brightness_smoothing = DEFAULT_PROP_BRIGHTNESS_SMOOTHING;
filter->brightness_deadband = DEFAULT_PROP_BRIGHTNESS_DEADBAND;
/* Initialize internal state */ /* Initialize internal state */
filter->camera_src = NULL; filter->camera_src = NULL;
@@ -350,6 +361,9 @@ gst_intervalometer_set_property (GObject * object, guint prop_id,
case PROP_BRIGHTNESS_SMOOTHING: case PROP_BRIGHTNESS_SMOOTHING:
filter->brightness_smoothing = g_value_get_double (value); filter->brightness_smoothing = g_value_get_double (value);
break; break;
case PROP_BRIGHTNESS_DEADBAND:
filter->brightness_deadband = g_value_get_double (value);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break; break;
@@ -399,6 +413,9 @@ gst_intervalometer_get_property (GObject * object, guint prop_id,
case PROP_BRIGHTNESS_SMOOTHING: case PROP_BRIGHTNESS_SMOOTHING:
g_value_set_double (value, filter->brightness_smoothing); g_value_set_double (value, filter->brightness_smoothing);
break; break;
case PROP_BRIGHTNESS_DEADBAND:
g_value_set_double (value, filter->brightness_deadband);
break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break; break;
@@ -690,6 +707,7 @@ gst_intervalometer_update_camera_settings (GstIntervalometer * filter,
{ {
gdouble error, adjusted_target, exposure_range, gain_range; gdouble error, adjusted_target, exposure_range, gain_range;
gdouble ramp_multiplier; gdouble ramp_multiplier;
gdouble abs_error;
if (!filter->camera_src) { if (!filter->camera_src) {
return; return;
@@ -699,6 +717,14 @@ gst_intervalometer_update_camera_settings (GstIntervalometer * filter,
error = (filter->target_brightness - brightness) * error = (filter->target_brightness - brightness) *
pow (2.0, filter->compensation); pow (2.0, filter->compensation);
/* Check deadband zone - if enabled and brightness is within tolerance, skip adjustments */
abs_error = fabs(filter->target_brightness - brightness);
if (filter->brightness_deadband > 0.0 && abs_error < filter->brightness_deadband) {
GST_LOG_OBJECT (filter, "Within deadband zone (error=%.2f < %.2f), skipping adjustment",
abs_error, filter->brightness_deadband);
return;
}
/* Adjust target brightness based on error */ /* Adjust target brightness based on error */
adjusted_target = filter->target_brightness + error; adjusted_target = filter->target_brightness + error;

View File

@@ -75,6 +75,7 @@ struct _GstIntervalometer
gchar *camera_element_name; /* Name of upstream idsueyesrc element */ gchar *camera_element_name; /* Name of upstream idsueyesrc element */
guint update_interval; /* Update interval in milliseconds */ guint update_interval; /* Update interval in milliseconds */
gdouble brightness_smoothing; /* Brightness smoothing factor (0-1, 0=no smoothing) */ gdouble brightness_smoothing; /* Brightness smoothing factor (0-1, 0=no smoothing) */
gdouble brightness_deadband; /* Deadband zone to prevent oscillation (0=disabled) */
/* Internal state */ /* Internal state */
GstElement *camera_src; /* Reference to upstream camera element */ GstElement *camera_src; /* Reference to upstream camera element */

View File

@@ -64,13 +64,6 @@
# #
# Troubleshooting: # Troubleshooting:
# #
# Error: "unknown signal name: rollover"
# - Cause: Linescan plugin not found or not properly registered with GStreamer
# - Solution:
# 1. Build plugins: .\build.ps1
# 2. Ensure GST_PLUGIN_PATH environment variable includes build output directory
# 3. Check plugin was built: look for linescan.dll in build directory
# 4. Test plugin availability: gst-inspect-1.0 linescan
# #
# Error: "No such element 'linescan'" or "No such element 'idsueyesrc'" # Error: "No such element 'linescan'" or "No such element 'idsueyesrc'"
# - Cause: Custom plugins not found in GST_PLUGIN_PATH # - Cause: Custom plugins not found in GST_PLUGIN_PATH
@@ -79,10 +72,6 @@
# 2. Set GST_PLUGIN_PATH to include the build directory # 2. Set GST_PLUGIN_PATH to include the build directory
# 3. Run: gst-inspect-1.0 --print-plugin-auto-install-info to check plugins # 3. Run: gst-inspect-1.0 --print-plugin-auto-install-info to check plugins
# #
# Error: "Not all elements could be created"
# - Cause: Missing GStreamer plugins or dependencies
# - Solution: Check that all required plugins are built and GStreamer is properly installed
#
# Performance Notes: # Performance Notes:
# - Files are saved via Python/PIL in the rollover callback (only when buffer wraps) # - Files are saved via Python/PIL in the rollover callback (only when buffer wraps)
# - The display shows the live linescan accumulation # - The display shows the live linescan accumulation
@@ -169,7 +158,7 @@ def on_rollover(linescan, buffer):
# Map the buffer to get image data # Map the buffer to get image data
success, mapinfo = buffer.map(Gst.MapFlags.READ) success, mapinfo = buffer.map(Gst.MapFlags.READ)
if not success: if not success:
# print(f"[ROLLOVER ERROR] Failed to map buffer for frame {frame_counter}") print(f"[ROLLOVER ERROR] Failed to map buffer for frame {frame_counter}")
return return
try: try:
@@ -208,7 +197,7 @@ def on_rollover(linescan, buffer):
else: else:
img.save(filename, 'JPEG', quality=95) img.save(filename, 'JPEG', quality=95)
print(f"[ROLLOVER] Frame {frame_counter} saved to: {filename}") # print(f"[ROLLOVER] Frame {frame_counter} saved to: {filename}")
except Exception as e: except Exception as e:
print(f"[ROLLOVER ERROR] Failed to save frame {frame_counter}: {e}") print(f"[ROLLOVER ERROR] Failed to save frame {frame_counter}: {e}")
@@ -261,7 +250,7 @@ Examples:
parser.add_argument('--output-dir', '-o', metavar='DIR', parser.add_argument('--output-dir', '-o', metavar='DIR',
help='Output directory (default: results/<date>/<funny-name>)') help='Output directory (default: results/<date>/<funny-name>)')
parser.add_argument('--mode', '-m', choices=['day', 'night'], default='day', parser.add_argument('--mode', '-m', choices=['day', 'night'], default='day',
help='Camera mode: day (0.4ms exposure, 0 gain) or night (5.25ms exposure, 65 gain) (default: day)') help='Camera mode: day (0.2ms exposure, 0 gain) or night (5.25ms exposure, 65 gain) (default: day)')
args = parser.parse_args() args = parser.parse_args()
@@ -395,10 +384,12 @@ Examples:
# Configure intervalometer # Configure intervalometer
intervalometer.set_property("enabled", True) intervalometer.set_property("enabled", True)
intervalometer.set_property("camera-element", source) intervalometer.set_property("camera-element", "source")
intervalometer.set_property("ramp-rate", "vslow") intervalometer.set_property("ramp-rate", "slow")
intervalometer.set_property("update-interval", 1000) intervalometer.set_property("update-interval", 500)
intervalometer.set_property("gain-max", 65) intervalometer.set_property("brightness-deadband", 10.0)
intervalometer.set_property("gain-max", 82)
intervalometer.set_property("target-brightness", 80.0)
intervalometer.set_property("log-file", "timelapse.csv") intervalometer.set_property("log-file", "timelapse.csv")
# Configure videocrop # Configure videocrop

View File

@@ -1,28 +0,0 @@
set (SOURCES
gstrollingsum.c
)
set (HEADERS
gstrollingsum.h)
include_directories (AFTER
${ORC_INCLUDE_DIR})
set (libname gstrollingsum)
add_library (${libname} MODULE
${SOURCES}
${HEADERS})
target_link_libraries (${libname}
${ORC_LIBRARIES}
${GLIB2_LIBRARIES}
${GOBJECT_LIBRARIES}
${GSTREAMER_LIBRARY}
${GSTREAMER_BASE_LIBRARY}
${GSTREAMER_VIDEO_LIBRARY})
if (WIN32)
install (FILES $<TARGET_PDB_FILE:${libname}> DESTINATION ${PDB_INSTALL_DIR} COMPONENT pdb OPTIONAL)
endif ()
install(TARGETS ${libname} LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR})

View File

@@ -1,535 +0,0 @@
/* GStreamer
* Copyright (C) 2024 <your-name@your-email.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
/**
* SECTION:element-rollingsum
*
* Drops frames based on rolling mean analysis of a single column.
* Inspired by cli.py detection algorithm, simplified for real-time streaming.
*
* <refsect2>
* <title>Example launch line</title>
* |[
* gst-launch-1.0 idsueyesrc config-file=config.ini ! rollingsum window-size=1000 column-index=1 threshold=0.5 ! autovideosink
* ]|
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstrollingsum.h"
#include <string.h>
#include <math.h>
#include <stdio.h>
enum
{
PROP_0,
PROP_WINDOW_SIZE,
PROP_COLUMN_INDEX,
PROP_STRIDE,
PROP_THRESHOLD,
PROP_CSV_FILENAME,
PROP_LAST
};
#define DEFAULT_PROP_WINDOW_SIZE 1000
#define DEFAULT_PROP_COLUMN_INDEX 1
#define DEFAULT_PROP_STRIDE 1
#define DEFAULT_PROP_THRESHOLD 0.5
#define DEFAULT_PROP_CSV_FILENAME NULL
/* Supported video formats */
#define SUPPORTED_CAPS \
GST_VIDEO_CAPS_MAKE("{ GRAY8, GRAY16_LE, GRAY16_BE }") ";" \
"video/x-bayer, format=(string){bggr,grbg,gbrg,rggb}"
static GstStaticPadTemplate gst_rolling_sum_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (SUPPORTED_CAPS)
);
static GstStaticPadTemplate gst_rolling_sum_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (SUPPORTED_CAPS)
);
/* GObject vmethod declarations */
static void gst_rolling_sum_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_rolling_sum_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static void gst_rolling_sum_dispose (GObject * object);
/* GstBaseTransform vmethod declarations */
static gboolean gst_rolling_sum_set_caps (GstBaseTransform * trans,
GstCaps * incaps, GstCaps * outcaps);
static GstFlowReturn gst_rolling_sum_transform_ip (GstBaseTransform * trans,
GstBuffer * buf);
/* GstRollingSum method declarations */
static void gst_rolling_sum_reset (GstRollingSum * filter);
static gdouble gst_rolling_sum_extract_column_mean (GstRollingSum * filter,
GstBuffer * buf);
/* setup debug */
GST_DEBUG_CATEGORY_STATIC (rolling_sum_debug);
#define GST_CAT_DEFAULT rolling_sum_debug
G_DEFINE_TYPE (GstRollingSum, gst_rolling_sum, GST_TYPE_BASE_TRANSFORM);
/************************************************************************/
/* GObject vmethod implementations */
/************************************************************************/
static void
gst_rolling_sum_dispose (GObject * object)
{
GstRollingSum *filter = GST_ROLLING_SUM (object);
GST_DEBUG ("dispose");
/* Close CSV file if open */
if (filter->csv_file) {
fclose (filter->csv_file);
filter->csv_file = NULL;
}
/* Free CSV filename */
if (filter->csv_filename) {
g_free (filter->csv_filename);
filter->csv_filename = NULL;
}
gst_rolling_sum_reset (filter);
/* chain up to the parent class */
G_OBJECT_CLASS (gst_rolling_sum_parent_class)->dispose (object);
}
static void
gst_rolling_sum_class_init (GstRollingSumClass * klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
GstBaseTransformClass *gstbasetransform_class =
GST_BASE_TRANSFORM_CLASS (klass);
GST_DEBUG_CATEGORY_INIT (rolling_sum_debug, "rollingsum", 0,
"Rolling Sum Filter");
GST_DEBUG ("class init");
/* Register GObject vmethods */
gobject_class->dispose = GST_DEBUG_FUNCPTR (gst_rolling_sum_dispose);
gobject_class->set_property = GST_DEBUG_FUNCPTR (gst_rolling_sum_set_property);
gobject_class->get_property = GST_DEBUG_FUNCPTR (gst_rolling_sum_get_property);
/* Install GObject properties */
g_object_class_install_property (gobject_class, PROP_WINDOW_SIZE,
g_param_spec_int ("window-size", "Window Size",
"Number of frames in rolling window", 1, 100000,
DEFAULT_PROP_WINDOW_SIZE,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE |
GST_PARAM_MUTABLE_PLAYING));
g_object_class_install_property (gobject_class, PROP_COLUMN_INDEX,
g_param_spec_int ("column-index", "Column Index",
"Which vertical column to analyze (0-based)", 0, G_MAXINT,
DEFAULT_PROP_COLUMN_INDEX,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE |
GST_PARAM_MUTABLE_PLAYING));
g_object_class_install_property (gobject_class, PROP_STRIDE,
g_param_spec_int ("stride", "Row Stride",
"Row sampling stride (1 = every row)", 1, G_MAXINT,
DEFAULT_PROP_STRIDE,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE |
GST_PARAM_MUTABLE_PLAYING));
g_object_class_install_property (gobject_class, PROP_THRESHOLD,
g_param_spec_double ("threshold", "Threshold",
"Normalized deviation threshold for dropping frames", 0.0, 1.0,
DEFAULT_PROP_THRESHOLD,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE |
GST_PARAM_MUTABLE_PLAYING));
g_object_class_install_property (gobject_class, PROP_CSV_FILENAME,
g_param_spec_string ("csv-file", "CSV File",
"Path to CSV file for logging frame data (NULL = no logging)",
DEFAULT_PROP_CSV_FILENAME,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE |
GST_PARAM_MUTABLE_READY));
gst_element_class_add_pad_template (gstelement_class,
gst_static_pad_template_get (&gst_rolling_sum_sink_template));
gst_element_class_add_pad_template (gstelement_class,
gst_static_pad_template_get (&gst_rolling_sum_src_template));
gst_element_class_set_static_metadata (gstelement_class,
"Rolling sum filter", "Filter/Effect/Video",
"Drops frames based on rolling mean analysis of a single column",
"Your Name <your-email@example.com>");
/* Register GstBaseTransform vmethods */
gstbasetransform_class->set_caps =
GST_DEBUG_FUNCPTR (gst_rolling_sum_set_caps);
gstbasetransform_class->transform_ip =
GST_DEBUG_FUNCPTR (gst_rolling_sum_transform_ip);
}
static void
gst_rolling_sum_init (GstRollingSum * filter)
{
GST_DEBUG_OBJECT (filter, "init class instance");
filter->window_size = DEFAULT_PROP_WINDOW_SIZE;
filter->column_index = DEFAULT_PROP_COLUMN_INDEX;
filter->stride = DEFAULT_PROP_STRIDE;
filter->threshold = DEFAULT_PROP_THRESHOLD;
filter->csv_filename = NULL;
filter->ring_buffer = NULL;
filter->ring_index = 0;
filter->frame_count = 0;
filter->rolling_mean = 0.0;
filter->rolling_sum = 0.0;
filter->info_set = FALSE;
filter->csv_file = NULL;
gst_base_transform_set_in_place (GST_BASE_TRANSFORM (filter), TRUE);
gst_rolling_sum_reset (filter);
}
static void
gst_rolling_sum_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstRollingSum *filter = GST_ROLLING_SUM (object);
GST_DEBUG_OBJECT (filter, "setting property %s", pspec->name);
switch (prop_id) {
case PROP_WINDOW_SIZE:
{
gint new_size = g_value_get_int (value);
if (new_size != filter->window_size) {
filter->window_size = new_size;
/* Reallocate ring buffer */
gst_rolling_sum_reset (filter);
}
break;
}
case PROP_COLUMN_INDEX:
filter->column_index = g_value_get_int (value);
break;
case PROP_STRIDE:
filter->stride = g_value_get_int (value);
break;
case PROP_THRESHOLD:
filter->threshold = g_value_get_double (value);
break;
case PROP_CSV_FILENAME:
{
const gchar *filename = g_value_get_string (value);
/* Close old file if open */
if (filter->csv_file) {
fclose (filter->csv_file);
filter->csv_file = NULL;
}
/* Free old filename */
if (filter->csv_filename) {
g_free (filter->csv_filename);
filter->csv_filename = NULL;
}
/* Set new filename and open file */
if (filename && filename[0] != '\0') {
filter->csv_filename = g_strdup (filename);
filter->csv_file = fopen (filter->csv_filename, "w");
if (filter->csv_file) {
/* Write CSV header */
fprintf (filter->csv_file, "frame,column_mean,rolling_mean,deviation,normalized_deviation,dropped\n");
fflush (filter->csv_file);
GST_INFO_OBJECT (filter, "Opened CSV file: %s", filter->csv_filename);
} else {
GST_ERROR_OBJECT (filter, "Failed to open CSV file: %s", filter->csv_filename);
g_free (filter->csv_filename);
filter->csv_filename = NULL;
}
}
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_rolling_sum_get_property (GObject * object, guint prop_id, GValue * value,
GParamSpec * pspec)
{
GstRollingSum *filter = GST_ROLLING_SUM (object);
GST_DEBUG_OBJECT (filter, "getting property %s", pspec->name);
switch (prop_id) {
case PROP_WINDOW_SIZE:
g_value_set_int (value, filter->window_size);
break;
case PROP_COLUMN_INDEX:
g_value_set_int (value, filter->column_index);
break;
case PROP_STRIDE:
g_value_set_int (value, filter->stride);
break;
case PROP_THRESHOLD:
g_value_set_double (value, filter->threshold);
break;
case PROP_CSV_FILENAME:
g_value_set_string (value, filter->csv_filename);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static gboolean
gst_rolling_sum_set_caps (GstBaseTransform * trans, GstCaps * incaps,
GstCaps * outcaps)
{
GstRollingSum *filter = GST_ROLLING_SUM (trans);
GST_DEBUG_OBJECT (filter, "set_caps");
if (!gst_video_info_from_caps (&filter->video_info, incaps)) {
GST_ERROR_OBJECT (filter, "Failed to parse caps");
return FALSE;
}
filter->info_set = TRUE;
GST_DEBUG_OBJECT (filter, "Video format: %s, %dx%d",
gst_video_format_to_string (GST_VIDEO_INFO_FORMAT (&filter->video_info)),
GST_VIDEO_INFO_WIDTH (&filter->video_info),
GST_VIDEO_INFO_HEIGHT (&filter->video_info));
return TRUE;
}
static gdouble
gst_rolling_sum_extract_column_mean (GstRollingSum * filter, GstBuffer * buf)
{
GstMapInfo map;
gdouble sum = 0.0;
gint count = 0;
gint width, height, stride_bytes;
gint row, col_offset;
guint8 *data;
GstVideoFormat format;
if (!filter->info_set) {
GST_WARNING_OBJECT (filter, "Video info not set yet");
return 0.0;
}
if (!gst_buffer_map (buf, &map, GST_MAP_READ)) {
GST_ERROR_OBJECT (filter, "Failed to map buffer");
return 0.0;
}
data = map.data;
width = GST_VIDEO_INFO_WIDTH (&filter->video_info);
height = GST_VIDEO_INFO_HEIGHT (&filter->video_info);
stride_bytes = GST_VIDEO_INFO_PLANE_STRIDE (&filter->video_info, 0);
format = GST_VIDEO_INFO_FORMAT (&filter->video_info);
/* Check column index is valid */
if (filter->column_index >= width) {
GST_WARNING_OBJECT (filter, "Column index %d >= width %d, using column 0",
filter->column_index, width);
filter->column_index = 0;
}
/* Calculate column offset based on format */
if (format == GST_VIDEO_FORMAT_GRAY8) {
col_offset = filter->column_index;
/* Sum column values with stride */
for (row = 0; row < height; row += filter->stride) {
sum += data[row * stride_bytes + col_offset];
count++;
}
} else if (format == GST_VIDEO_FORMAT_GRAY16_LE ||
format == GST_VIDEO_FORMAT_GRAY16_BE) {
col_offset = filter->column_index * 2;
/* Sum column values with stride */
for (row = 0; row < height; row += filter->stride) {
guint16 pixel_value;
guint8 *pixel_ptr = &data[row * stride_bytes + col_offset];
if (format == GST_VIDEO_FORMAT_GRAY16_LE) {
pixel_value = pixel_ptr[0] | (pixel_ptr[1] << 8);
} else {
pixel_value = (pixel_ptr[0] << 8) | pixel_ptr[1];
}
sum += pixel_value;
count++;
}
} else {
/* For Bayer formats, treat as GRAY8 */
col_offset = filter->column_index;
for (row = 0; row < height; row += filter->stride) {
sum += data[row * stride_bytes + col_offset];
count++;
}
}
gst_buffer_unmap (buf, &map);
return count > 0 ? sum / count : 0.0;
}
static GstFlowReturn
gst_rolling_sum_transform_ip (GstBaseTransform * trans, GstBuffer * buf)
{
GstRollingSum *filter = GST_ROLLING_SUM (trans);
gdouble frame_mean, deviation, old_value;
gint effective_window_size;
GST_DEBUG_OBJECT (filter, "transform_ip called, frame_count=%d", filter->frame_count);
/* Extract column mean from current frame */
frame_mean = gst_rolling_sum_extract_column_mean (filter, buf);
GST_DEBUG_OBJECT (filter, "Extracted column mean: %.2f", frame_mean);
/* Store in ring buffer */
old_value = filter->ring_buffer[filter->ring_index];
filter->ring_buffer[filter->ring_index] = frame_mean;
/* Update rolling sum efficiently */
if (filter->frame_count < filter->window_size) {
/* Still filling the buffer */
filter->rolling_sum += frame_mean;
filter->frame_count++;
effective_window_size = filter->frame_count;
} else {
/* Buffer is full, replace old value */
filter->rolling_sum = filter->rolling_sum - old_value + frame_mean;
effective_window_size = filter->window_size;
}
/* Update rolling mean */
filter->rolling_mean = filter->rolling_sum / effective_window_size;
/* Calculate deviation */
deviation = fabs(frame_mean - filter->rolling_mean);
/* Normalize deviation (assuming 8-bit equivalent range) */
gdouble normalized_deviation = deviation / 255.0;
GST_DEBUG_OBJECT (filter,
"Frame %d: mean=%.2f, rolling_mean=%.2f, deviation=%.2f (normalized=%.4f)",
filter->frame_count, frame_mean, filter->rolling_mean, deviation,
normalized_deviation);
/* Advance ring buffer index */
filter->ring_index = (filter->ring_index + 1) % filter->window_size;
/* Decision: drop or pass frame */
gboolean dropped = FALSE;
if (normalized_deviation > filter->threshold) {
GST_DEBUG_OBJECT (filter,
"Dropping frame %d: deviation %.4f > threshold %.4f",
filter->frame_count, normalized_deviation, filter->threshold);
dropped = TRUE;
}
/* Write to CSV if file is open */
if (filter->csv_file) {
fprintf (filter->csv_file, "%d,%.6f,%.6f,%.6f,%.6f,%d\n",
filter->frame_count, frame_mean, filter->rolling_mean,
deviation, normalized_deviation, dropped ? 1 : 0);
fflush (filter->csv_file);
}
if (dropped) {
return GST_BASE_TRANSFORM_FLOW_DROPPED;
}
return GST_FLOW_OK;
}
static void
gst_rolling_sum_reset (GstRollingSum * filter)
{
GST_DEBUG_OBJECT (filter, "reset");
/* Free old ring buffer if exists */
if (filter->ring_buffer) {
g_free (filter->ring_buffer);
}
/* Allocate new ring buffer */
filter->ring_buffer = g_new0 (gdouble, filter->window_size);
filter->ring_index = 0;
filter->frame_count = 0;
filter->rolling_mean = 0.0;
filter->rolling_sum = 0.0;
}
static gboolean
plugin_init (GstPlugin * plugin)
{
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "rollingsum", 0, "rollingsum");
GST_DEBUG ("plugin_init");
GST_CAT_INFO (GST_CAT_DEFAULT, "registering rollingsum element");
if (!gst_element_register (plugin, "rollingsum", GST_RANK_NONE,
GST_TYPE_ROLLING_SUM)) {
return FALSE;
}
return TRUE;
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
GST_VERSION_MINOR,
rollingsum,
"Filter that drops frames based on rolling mean analysis",
plugin_init, GST_PACKAGE_VERSION, GST_PACKAGE_LICENSE, GST_PACKAGE_NAME,
GST_PACKAGE_ORIGIN);

View File

@@ -1,82 +0,0 @@
/* GStreamer
* Copyright (C) 2024 <your-name@your-email.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifndef __GST_ROLLING_SUM_H__
#define __GST_ROLLING_SUM_H__
#include <gst/base/gstbasetransform.h>
#include <gst/video/video.h>
#include <stdio.h>
G_BEGIN_DECLS
#define GST_TYPE_ROLLING_SUM \
(gst_rolling_sum_get_type())
#define GST_ROLLING_SUM(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_ROLLING_SUM,GstRollingSum))
#define GST_ROLLING_SUM_CLASS(klass) \
(G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_ROLLING_SUM,GstRollingSumClass))
#define GST_IS_ROLLING_SUM(obj) \
(G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_ROLLING_SUM))
#define GST_IS_ROLLING_SUM_CLASS(klass) \
(G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_ROLLING_SUM))
typedef struct _GstRollingSum GstRollingSum;
typedef struct _GstRollingSumClass GstRollingSumClass;
/**
* GstRollingSum:
* @element: the parent element.
*
* The opaque GstRollingSum data structure.
*/
struct _GstRollingSum
{
GstBaseTransform element;
/* Properties */
gint window_size; /* Number of frames in rolling window */
gint column_index; /* Which column to analyze (0-based) */
gint stride; /* Row sampling stride */
gdouble threshold; /* Deviation threshold for dropping frames */
gchar *csv_filename; /* CSV output filename (NULL = no CSV) */
/* State */
gdouble *ring_buffer; /* Circular buffer of column means */
gint ring_index; /* Current position in ring buffer */
gint frame_count; /* Total frames processed */
gdouble rolling_mean; /* Current rolling mean */
gdouble rolling_sum; /* Current rolling sum for efficient mean update */
FILE *csv_file; /* CSV file handle */
/* Video format info */
GstVideoInfo video_info;
gboolean info_set;
};
struct _GstRollingSumClass
{
GstBaseTransformClass parent_class;
};
GType gst_rolling_sum_get_type(void);
G_END_DECLS
#endif /* __GST_ROLLING_SUM_H__ */

View File

@@ -1,18 +1,49 @@
# Pipeline with tee to display and save frames on rollover # Linescan Pipeline Launcher
# Simple wrapper around launch-ids.py with sensible defaults
# Run from workspace root: c:\dev\gst-plugins-vision # Run from workspace root: c:\dev\gst-plugins-vision
# Or adjust the config-file path below
param(
[Parameter(Position=0)]
[ValidateSet('day', 'night')]
[string]$Mode = 'day',
[switch]$Help
)
if ($Help) {
Write-Host @"
Linescan Pipeline Launcher
Usage: .\launch_linescan.ps1 [day|night] [-Help]
Modes:
day - Day mode: exposure=0.2ms, gain=0, framerate=200fps (default)
night - Night mode: exposure=5.25ms, gain=65, framerate=200fps
All settings are configured in launch-ids.py with these defaults:
- Config: ini/roi-night.ini
- Device ID: 1
- Intervalometer: enabled
- Ramp rate: fast
- Update interval: 500ms
- Brightness deadband: 10.0
- Gain max: 82
- Target brightness: 80
- Log file: timelapse.csv
Examples:
.\launch_linescan.ps1 # Run in day mode (default)
.\launch_linescan.ps1 day # Run in day mode (explicit)
.\launch_linescan.ps1 night # Run in night mode
.\launch_linescan.ps1 -Help # Show this help
"@
exit 0
}
$env:GST_DEBUG="linescan:5" $env:GST_DEBUG="linescan:5"
# Note: Run this from the workspace root (c:\dev\gst-plugins-vision) Write-Host "Launching linescan pipeline in $Mode mode..."
# If running from gst/linescan/, use: config-file=../../ini/roi-night.ini Write-Host ""
uv run .\scripts\launch-ids.py `
--config .\ini\2456x4pix-500top-cw-extragain.ini ` # All configuration is handled by launch-ids.py defaults
--device-id 1 ` uv run .\scripts\launch-ids.py --mode $Mode
--framerate 750 `
--gain 0 `
--gain-boost `
--exposure 0.04 `
--host 10.81.2.183 `
--port 5000 `
--control-port 5001

285
scripts/append_signals.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
Create scrolling panorama videos from linescan image sequences.
This script uses ffmpeg's streaming filters to efficiently process linescan images
without loading the entire stitched panorama into memory. Images are horizontally
concatenated and scrolled left-to-right on-the-fly.
"""
import argparse
import subprocess
import sys
import time
from pathlib import Path
from typing import List
def find_frames(directory: Path, pattern: str = "*.jpeg") -> List[Path]:
"""Find all frame files matching the pattern, sorted by name."""
frames = sorted(directory.glob(pattern))
if not frames:
frames = sorted(directory.glob("*.jpg"))
if not frames:
frames = sorted(directory.glob("*.png"))
return frames
def create_scrolling_video_efficient(
frames: List[Path],
output_file: Path,
output_width: int = 1920,
scroll_speed: float = 1.0,
fps: int = 30,
crf: int = 18,
lines_per_second: float = 750.0
) -> None:
"""
Create scrolling video from image sequence using ffmpeg streaming.
Memory-efficient - doesn't create intermediate stitched image.
Args:
frames: List of image file paths
output_file: Output video file
output_width: Width of output video viewport
scroll_speed: Scroll speed multiplier (1.0 = real-time @ 750 lines/sec)
fps: Output video framerate
crf: Quality (lower = better)
lines_per_second: Linescan capture rate (pixels per second)
"""
num_frames = len(frames)
# Get dimensions from first frame
probe_cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=s=x:p=0",
str(frames[0])
]
try:
result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
img_width, img_height = map(int, result.stdout.strip().split('x'))
except Exception as e:
print(f"Error probing image dimensions: {e}")
sys.exit(1)
# Calculate total width and video parameters
total_width = img_width * num_frames
pixels_per_second = lines_per_second * scroll_speed
pixels_per_frame = pixels_per_second / fps
scrollable_width = total_width - output_width
duration = scrollable_width / pixels_per_second
total_frames = int(duration * fps)
print(f"\nVideo parameters:")
print(f" Input: {num_frames} images of {img_width}x{img_height}")
print(f" Total panorama width: {total_width} pixels")
print(f" Output viewport: {output_width}x{img_height}")
print(f" Scroll speed: {scroll_speed}x real-time ({pixels_per_second:.1f} pixels/sec)")
print(f" Pixels per frame: {pixels_per_frame:.2f}")
print(f" Video duration: {duration:.2f} seconds")
print(f" Total frames: {total_frames}")
print(f" Output FPS: {fps}")
# Create concat file for ffmpeg
concat_file = frames[0].parent / f"concat_temp_{int(time.time())}.txt"
try:
with open(concat_file, 'w') as f:
for frame in frames:
f.write(f"file '{frame.absolute()}'\n")
# Build scrolling crop expression: x position moves based on time
crop_expr = f"crop={output_width}:{img_height}:min({pixels_per_second}*t\\,{scrollable_width}):0"
# Filter chain: tile creates horizontal panorama, loop repeats it, crop scrolls
filter_complex = f"tile=layout={num_frames}x1[pano];[pano]loop=loop=-1:size=1:start=0,{crop_expr}"
cmd = [
"ffmpeg",
"-y",
"-f", "concat",
"-safe", "0",
"-i", str(concat_file),
"-filter_complex", filter_complex,
"-t", str(duration),
"-r", str(fps),
"-c:v", "libx264",
"-crf", str(crf),
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
str(output_file)
]
print(f"\nRunning ffmpeg...")
print(f"Command: {' '.join(cmd)}\n")
try:
subprocess.run(cmd, check=True)
print(f"\n✓ Scrolling video created successfully: {output_file}")
# Show file size
size_mb = output_file.stat().st_size / (1024 * 1024)
print(f" File size: {size_mb:.2f} MB")
print(f" Duration: {duration:.2f} seconds")
except subprocess.CalledProcessError as e:
print(f"Error: ffmpeg command failed with return code {e.returncode}")
sys.exit(1)
finally:
# Clean up concat file
if concat_file.exists():
concat_file.unlink()
def main():
parser = argparse.ArgumentParser(
description="Create scrolling panorama videos from linescan images (memory-efficient)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Real-time playback (1.0x speed, 750 lines/second)
python append_signals.py results/20251122/bumpy-filter
# Slow motion (0.5x speed)
python append_signals.py results/20251122/bumpy-filter --scroll-speed 0.5
# Fast playback (2x speed)
python append_signals.py results/20251122/bumpy-filter --scroll-speed 2.0
# Process only first 100 frames
python append_signals.py results/20251122/bumpy-filter --max-frames 100
# Higher FPS for smoother scrolling
python append_signals.py results/20251122/bumpy-filter --fps 60
# Custom output width (4K)
python append_signals.py results/20251122/bumpy-filter --width 3840
Note: This version uses ffmpeg's tile filter for memory-efficient processing.
No intermediate stitched image is created - everything is streamed.
"""
)
parser.add_argument(
"input_dir",
type=Path,
help="Directory containing the linescan frame sequence"
)
parser.add_argument(
"-o", "--output",
type=Path,
help="Output video file (default: {folder_name}_scroll_{timestamp}.mp4)"
)
parser.add_argument(
"--scroll-speed",
type=float,
default=1.0,
help="Scroll speed multiplier (1.0 = real-time at 750 lines/sec, default: 1.0)"
)
parser.add_argument(
"--lines-per-second",
type=float,
default=750.0,
help="Linescan capture rate in lines/pixels per second (default: 750)"
)
parser.add_argument(
"--fps",
type=int,
default=30,
help="Output video framerate (default: 30)"
)
parser.add_argument(
"--width",
type=int,
default=1920,
help="Output video viewport width in pixels (default: 1920)"
)
parser.add_argument(
"--crf",
type=int,
default=18,
help="Constant Rate Factor for quality, lower=better (default: 18, range: 0-51)"
)
parser.add_argument(
"--pattern",
type=str,
default="*.jpeg",
help="File pattern to match frames (default: *.jpeg)"
)
parser.add_argument(
"--max-frames",
type=int,
default=None,
help="Maximum number of frames to process (default: all frames)"
)
args = parser.parse_args()
# Validate input directory
if not args.input_dir.exists():
print(f"Error: Input directory does not exist: {args.input_dir}")
sys.exit(1)
if not args.input_dir.is_dir():
print(f"Error: Input path is not a directory: {args.input_dir}")
sys.exit(1)
# Find frames
frames = find_frames(args.input_dir, args.pattern)
if not frames:
print(f"Error: No frames found in {args.input_dir} matching pattern {args.pattern}")
sys.exit(1)
total_frames = len(frames)
if args.max_frames is not None and args.max_frames > 0:
frames = frames[:args.max_frames]
print(f"Found {total_frames} frames, processing first {len(frames)} frames")
else:
print(f"Found {len(frames)} frames in {args.input_dir}")
# Set output file (one folder up from input directory)
if args.output is None:
unix_time = int(time.time())
folder_name = args.input_dir.name
output_filename = f"{folder_name}_scroll_{unix_time}.mp4"
output_file = args.input_dir.parent / output_filename
else:
output_file = args.output
# Validate CRF range
if not 0 <= args.crf <= 51:
print("Error: CRF must be between 0 and 51")
sys.exit(1)
# Create scrolling video using efficient streaming method
create_scrolling_video_efficient(
frames=frames,
output_file=output_file,
output_width=args.width,
scroll_speed=args.scroll_speed,
fps=args.fps,
crf=args.crf,
lines_per_second=args.lines_per_second
)
if __name__ == "__main__":
main()

View File

@@ -17,6 +17,9 @@
# Basic Usage: # Basic Usage:
# uv run .\scripts\launch-ids.py # Use all defaults # 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 --help # Show all options
# uv run .\scripts\launch-ids.py --mode day # Day mode preset (0.2ms, 0 gain)
# uv run .\scripts\launch-ids.py --mode night # Night mode preset (5.25ms, 65 gain)
# uv run .\scripts\launch-ids.py --intervalometer # Enable auto-exposure control
# uv run .\scripts\launch-ids.py exposure 16 # Set exposure to 16ms (simplified) # uv run .\scripts\launch-ids.py exposure 16 # Set exposure to 16ms (simplified)
# uv run .\scripts\launch-ids.py framerate 30 # Set framerate to 30fps (simplified) # uv run .\scripts\launch-ids.py framerate 30 # Set framerate to 30fps (simplified)
# uv run .\scripts\launch-ids.py gain 50 # Set gain to 50 (simplified) # uv run .\scripts\launch-ids.py gain 50 # Set gain to 50 (simplified)
@@ -26,7 +29,7 @@
# uv run .\scripts\launch-ids.py --display # Enable 1/4 sized preview window # uv run .\scripts\launch-ids.py --display # Enable 1/4 sized preview window
# #
# Features: # Features:
# - Configurable video streaming (default: UDP port 5000 to 127.0.0.1) # - Configurable video streaming (default: UDP port 5000 to 10.81.2.183)
# - Optional control interface (default: UDP port 5001 on 0.0.0.0) # - Optional control interface (default: UDP port 5001 on 0.0.0.0)
# - Dynamic exposure control (0.015-30000 milliseconds, default: 10ms) # - Dynamic exposure control (0.015-30000 milliseconds, default: 10ms)
# - Auto-exposure mode support (--auto-exposure flag) # - Auto-exposure mode support (--auto-exposure flag)
@@ -510,6 +513,9 @@ def parse_arguments():
epilog=""" epilog="""
Examples: Examples:
%(prog)s # Use all defaults %(prog)s # Use all defaults
%(prog)s --mode day # Day mode (0.2ms, 0 gain)
%(prog)s --mode night # Night mode (5.25ms, 65 gain)
%(prog)s --mode day --intervalometer # Day mode with auto-exposure
%(prog)s exposure 16 # Set exposure to 16ms %(prog)s exposure 16 # Set exposure to 16ms
%(prog)s framerate 30 # Set framerate to 30fps %(prog)s framerate 30 # Set framerate to 30fps
%(prog)s gain 50 # Set gain to 50 %(prog)s gain 50 # Set gain to 50
@@ -532,12 +538,34 @@ Examples:
nargs='?', nargs='?',
help='Value to set for the property (simplified syntax)') help='Value to set for the property (simplified syntax)')
# Mode selection
mode_group = parser.add_argument_group('Mode Selection')
mode_group.add_argument(
'--mode', '-m',
choices=['day', 'night'],
default='day',
help='Camera mode preset: day (0.2ms exposure, 0 gain, 200fps) or night (5.25ms exposure, 65 gain, 200fps)'
)
mode_group.add_argument(
'--intervalometer',
dest='intervalometer',
action='store_true',
default=True,
help='Enable intervalometer for automatic exposure control (enabled by default)'
)
mode_group.add_argument(
'--no-intervalometer',
dest='intervalometer',
action='store_false',
help='Disable intervalometer'
)
# Camera configuration # Camera configuration
camera_group = parser.add_argument_group('Camera Settings') camera_group = parser.add_argument_group('Camera Settings')
camera_group.add_argument( camera_group.add_argument(
'--config', '--config-file', '--config', '--config-file',
type=str, type=str,
default='ini/100fps-10exp-2456x4pix-500top-cw-extragain.ini', default='ini/2456x4pix-500top-cw-extragain.ini',
metavar='PATH', metavar='PATH',
help='Camera configuration file path' help='Camera configuration file path'
) )
@@ -565,7 +593,7 @@ Examples:
camera_group.add_argument( camera_group.add_argument(
'--device-id', '--device-id',
type=int, type=int,
default=0, default=1,
metavar='ID', metavar='ID',
help='Device ID (system enumeration, 0 is first, 0-254)' help='Device ID (system enumeration, 0 is first, 0-254)'
) )
@@ -624,7 +652,7 @@ Examples:
network_group.add_argument( network_group.add_argument(
'--host', '--udp-host', '--host', '--udp-host',
type=str, type=str,
default='127.0.0.1', default='10.81.2.183',
metavar='IP', metavar='IP',
help='UDP streaming destination host' help='UDP streaming destination host'
) )
@@ -704,6 +732,22 @@ Examples:
elif args.property and not args.value: elif args.property and not args.value:
parser.error(f"Property '{args.property}' requires a value") parser.error(f"Property '{args.property}' requires a value")
# Handle mode presets (day is default)
if args.mode == 'day':
if args.exposure is None:
args.exposure = 0.2
if args.gain is None:
args.gain = 0
if args.framerate is None:
args.framerate = 200
elif args.mode == 'night':
if args.exposure is None:
args.exposure = 5.25
if args.gain is None:
args.gain = 65
if args.framerate is None:
args.framerate = 200
# Validation - only validate if provided # Validation - only validate if provided
if args.exposure is not None and (args.exposure < 0.015 or args.exposure > 30000): if args.exposure is not None and (args.exposure < 0.015 or args.exposure > 30000):
parser.error(f"Exposure must be between 0.015 and 30000 ms, got {args.exposure}") parser.error(f"Exposure must be between 0.015 and 30000 ms, got {args.exposure}")
@@ -795,8 +839,30 @@ if args.auto_gain:
if args.gain_boost: if args.gain_boost:
src.set_property("gain-boost", True) src.set_property("gain-boost", True)
# Video crop to remove bottom pixels (if enabled) # Build pipeline elements list
elements_to_link = [src] elements_to_link = [src]
# Add intervalometer if enabled
if args.intervalometer:
intervalometer = Gst.ElementFactory.make("intervalometer", "intervalometer")
if not intervalometer:
print("ERROR: Could not create intervalometer element")
print("Make sure the intervalometer plugin is built and in GST_PLUGIN_PATH")
exit(1)
# Configure intervalometer
intervalometer.set_property("enabled", True)
intervalometer.set_property("camera-element", "src")
intervalometer.set_property("ramp-rate", "slow")
intervalometer.set_property("update-interval", 500)
intervalometer.set_property("brightness-deadband", 10.0)
intervalometer.set_property("gain-max", 82)
intervalometer.set_property("target-brightness", 80.0)
intervalometer.set_property("log-file", "timelapse.csv")
elements_to_link.append(intervalometer)
# Video crop to remove bottom pixels (if enabled)
if args.crop_bottom > 0: if args.crop_bottom > 0:
videocrop = Gst.ElementFactory.make("videocrop", "crop") videocrop = Gst.ElementFactory.make("videocrop", "crop")
videocrop.set_property("bottom", args.crop_bottom) videocrop.set_property("bottom", args.crop_bottom)
@@ -933,6 +999,8 @@ if not args.quiet:
print("=" * 60) print("=" * 60)
print("IDS uEye Camera - Pipeline Configuration") print("IDS uEye Camera - Pipeline Configuration")
print("=" * 60) print("=" * 60)
if args.mode:
print(f"Mode: {args.mode}")
print(f"Camera config: {args.config}") print(f"Camera config: {args.config}")
print(f"Camera ID: {args.camera_id}") print(f"Camera ID: {args.camera_id}")
print(f"Device ID: {args.device_id}") print(f"Device ID: {args.device_id}")
@@ -951,6 +1019,15 @@ if not args.quiet:
print(f"Auto-exposure: {'enabled' if args.auto_exposure else '(from INI file)'}") print(f"Auto-exposure: {'enabled' if args.auto_exposure else '(from INI file)'}")
print(f"Auto-gain: {'enabled' if args.auto_gain else '(from INI file)'}") print(f"Auto-gain: {'enabled' if args.auto_gain else '(from INI file)'}")
print(f"Gain boost: {'enabled' if args.gain_boost else '(from INI file)'}") print(f"Gain boost: {'enabled' if args.gain_boost else '(from INI file)'}")
if args.intervalometer:
print()
print("Intervalometer: enabled")
print(" Ramp rate: slow")
print(" Update int: 500 ms")
print(" Deadband: 10.0")
print(" Gain max: 82")
print(" Target bright: 80")
print(" Log file: timelapse.csv")
if args.crop_bottom > 0: if args.crop_bottom > 0:
print(f"Crop bottom: {args.crop_bottom} pixels") print(f"Crop bottom: {args.crop_bottom} pixels")
else: else: