feat: adapt linescan scripts to use uv and proper rollover-based file saving
- Updated launch_with_signal.py with PEP 723 metadata for uv compatibility - Added day/night mode presets with command-line arguments - Implemented proper rollover-only file saving via Python/PIL callbacks - Removed multifilesink from pipeline (was saving every frame incorrectly) - Added funny auto-generated output directory names (e.g., fuzzy-photon) - Updated rollover_example.py to follow same pattern with uv support - Updated rollover_example.c to demonstrate signal detection without file saving - Updated launch_with_capture.ps1 to remove incorrect multifilesink usage - All scripts now save files ONLY on rollover events, not every frame - Pipeline simplified: camera -> linescan -> display (files saved in callback)
This commit is contained in:
@@ -20,6 +20,29 @@ The `linescan` plugin simulates a line scan camera by extracting a single row or
|
||||
| `line-index` | int | -1 | Index of row/column to extract (-1 for middle of image) |
|
||||
| `output-size` | int | 800 | Number of lines to accumulate (width for horizontal mode, height for vertical mode) |
|
||||
|
||||
## Signals
|
||||
|
||||
### `rollover`
|
||||
|
||||
```c
|
||||
void user_function (GstElement *linescan,
|
||||
GstBuffer *buffer,
|
||||
gpointer user_data)
|
||||
```
|
||||
|
||||
Emitted when the buffer position wraps around to 0 after accumulating `output-size` lines. The `buffer` parameter contains the completed line scan image at the moment of rollover.
|
||||
|
||||
**Parameters:**
|
||||
- `linescan`: The linescan element emitting the signal
|
||||
- `buffer`: The completed line scan buffer (read-only, contains full image)
|
||||
- `user_data`: User data set when the signal handler was connected
|
||||
|
||||
**Use cases:**
|
||||
- Save completed line scan images to disk automatically
|
||||
- Process completed frames (e.g., run image analysis)
|
||||
- Trigger external actions when a scan cycle completes
|
||||
- Capture periodic snapshots during continuous operation
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Horizontal Line Scan (Extract Row 100)
|
||||
@@ -52,6 +75,74 @@ gst-launch-1.0 idsueyesrc config-file=config.ini framerate=200 ! \
|
||||
pngenc ! filesink location=linescan.png
|
||||
```
|
||||
|
||||
### Capturing Frames on Rollover (Using Signal)
|
||||
|
||||
The `rollover` signal fires when the buffer wraps around, allowing you to capture completed frames:
|
||||
|
||||
**Python example** (`rollover_example.py`):
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import Gst
|
||||
|
||||
frame_counter = 0
|
||||
|
||||
def on_rollover(linescan, buffer, filesink):
|
||||
global frame_counter
|
||||
filename = f"linescan_{frame_counter:04d}.raw"
|
||||
print(f"Rollover! Saving to: {filename}")
|
||||
filesink.set_property('location', filename)
|
||||
frame_counter += 1
|
||||
|
||||
Gst.init(None)
|
||||
pipeline = Gst.parse_launch(
|
||||
"videotestsrc pattern=ball ! "
|
||||
"linescan direction=horizontal line-index=100 output-size=400 ! "
|
||||
"tee name=t "
|
||||
"t. ! queue ! videoconvert ! autovideosink "
|
||||
"t. ! queue ! filesink name=fsink location=init.raw"
|
||||
)
|
||||
|
||||
linescan = pipeline.get_by_name("linescan")
|
||||
filesink = pipeline.get_by_name("fsink")
|
||||
linescan.connect("rollover", on_rollover, filesink)
|
||||
|
||||
pipeline.set_state(Gst.State.PLAYING)
|
||||
# ... run main loop ...
|
||||
```
|
||||
|
||||
**Alternative: Using multifilesink with tee** (no signal needed):
|
||||
```bash
|
||||
# Automatically saves each output frame with incrementing counter
|
||||
gst-launch-1.0 videotestsrc pattern=ball ! \
|
||||
linescan direction=horizontal line-index=100 output-size=400 ! \
|
||||
tee name=t \
|
||||
t. ! queue ! videoconvert ! autovideosink \
|
||||
t. ! queue ! multifilesink location="frame_%04d.raw" max-files=100
|
||||
```
|
||||
|
||||
**Real-world example with IDS uEye camera:**
|
||||
```powershell
|
||||
# PowerShell script (see launch_with_capture.ps1)
|
||||
$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 ! `
|
||||
tee name=t `
|
||||
t. ! queue ! videoconvert ! autovideosink `
|
||||
t. ! queue ! multifilesink location="linescan_%04d.raw" max-files=100
|
||||
```
|
||||
|
||||
See examples:
|
||||
- [`rollover_example.py`](rollover_example.py) - Basic rollover signal demo
|
||||
- [`launch_with_signal.py`](launch_with_signal.py) - Full IDS camera pipeline with signal
|
||||
- [`launch_with_capture.ps1`](launch_with_capture.ps1) - PowerShell script with multifilesink
|
||||
|
||||
## How It Works
|
||||
|
||||
### Horizontal Mode (default)
|
||||
|
||||
@@ -46,6 +46,15 @@
|
||||
GST_DEBUG_CATEGORY_STATIC (gst_linescan_debug);
|
||||
#define GST_CAT_DEFAULT gst_linescan_debug
|
||||
|
||||
/* Signals */
|
||||
enum
|
||||
{
|
||||
SIGNAL_ROLLOVER,
|
||||
LAST_SIGNAL
|
||||
};
|
||||
|
||||
static guint gst_linescan_signals[LAST_SIGNAL] = { 0 };
|
||||
|
||||
/* Properties */
|
||||
enum
|
||||
{
|
||||
@@ -156,6 +165,22 @@ gst_linescan_class_init (GstLinescanClass * klass)
|
||||
1, G_MAXINT, DEFAULT_PROP_OUTPUT_SIZE,
|
||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
/* Install signals */
|
||||
/**
|
||||
* GstLinescan::rollover:
|
||||
* @linescan: the linescan instance
|
||||
* @buffer: the completed buffer at rollover
|
||||
*
|
||||
* Emitted when the buffer position wraps around to 0 after accumulating
|
||||
* output_size lines. The buffer contains a complete line scan image.
|
||||
* Applications can connect to this signal to save the image to disk.
|
||||
*/
|
||||
gst_linescan_signals[SIGNAL_ROLLOVER] =
|
||||
g_signal_new ("rollover", G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GstLinescanClass, rollover),
|
||||
NULL, NULL, g_cclosure_marshal_VOID__BOXED,
|
||||
G_TYPE_NONE, 1, GST_TYPE_BUFFER);
|
||||
|
||||
/* Set element metadata */
|
||||
gst_element_class_add_pad_template (gstelement_class,
|
||||
gst_static_pad_template_get (&gst_linescan_sink_template));
|
||||
@@ -580,14 +605,17 @@ gst_linescan_transform (GstBaseTransform * trans, GstBuffer * inbuf,
|
||||
/* Increment buffer position */
|
||||
filter->buffer_position++;
|
||||
|
||||
/* Wrap around when we reach the output size */
|
||||
/* Check for rollover and emit signal */
|
||||
gboolean rollover_occurred = FALSE;
|
||||
if (filter->direction == GST_LINESCAN_DIRECTION_HORIZONTAL) {
|
||||
if (filter->buffer_position >= out_height) {
|
||||
filter->buffer_position = 0;
|
||||
rollover_occurred = TRUE;
|
||||
}
|
||||
} else {
|
||||
if (filter->buffer_position >= out_width) {
|
||||
filter->buffer_position = 0;
|
||||
rollover_occurred = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +625,13 @@ gst_linescan_transform (GstBaseTransform * trans, GstBuffer * inbuf,
|
||||
gst_buffer_unmap (inbuf, &map_in);
|
||||
gst_buffer_unmap (outbuf, &map_out);
|
||||
|
||||
/* Emit rollover signal if buffer wrapped around */
|
||||
if (rollover_occurred) {
|
||||
GST_DEBUG_OBJECT (filter, "Rollover occurred at frame %lu",
|
||||
(unsigned long) filter->frame_count);
|
||||
g_signal_emit (filter, gst_linescan_signals[SIGNAL_ROLLOVER], 0, outbuf);
|
||||
}
|
||||
|
||||
filter->frame_count++;
|
||||
|
||||
GST_LOG_OBJECT (filter, "Processed frame %lu, buffer position: %d",
|
||||
|
||||
@@ -78,6 +78,9 @@ struct _GstLinescan
|
||||
struct _GstLinescanClass
|
||||
{
|
||||
GstBaseTransformClass parent_class;
|
||||
|
||||
/* Signals */
|
||||
void (*rollover) (GstLinescan *linescan, GstBuffer *buffer);
|
||||
};
|
||||
|
||||
GType gst_linescan_get_type(void);
|
||||
|
||||
43
gst/linescan/launch_with_capture.ps1
Normal file
43
gst/linescan/launch_with_capture.ps1
Normal file
@@ -0,0 +1,43 @@
|
||||
# Linescan Pipeline Example - PowerShell GStreamer Launch
|
||||
#
|
||||
# This script demonstrates a GStreamer linescan pipeline using gst-launch-1.0.
|
||||
# NOTE: This saves EVERY frame using multifilesink, not just on rollover.
|
||||
#
|
||||
# For proper rollover-only saving, use the Python scripts instead:
|
||||
# - launch_with_signal.py (full-featured with camera)
|
||||
# - rollover_example.py (simple demo)
|
||||
#
|
||||
# This script is kept as a reference for the pipeline structure.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Build the plugins: .\build.ps1
|
||||
# 2. Run from workspace root: c:\dev\gst-plugins-vision
|
||||
#
|
||||
# Usage:
|
||||
# .\gst\linescan\launch_with_capture.ps1
|
||||
#
|
||||
# Pipeline:
|
||||
# idsueyesrc -> intervalometer -> videocrop -> queue -> linescan -> videoconvert -> autovideosink
|
||||
#
|
||||
# WARNING: This example saves every output frame, not just on rollover!
|
||||
# Use Python scripts for rollover-based capture.
|
||||
|
||||
$env:GST_DEBUG="linescan:5"
|
||||
|
||||
# Simple pipeline - display only (no file saving)
|
||||
# For file saving on rollover, use launch_with_signal.py instead
|
||||
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=linescan_timelapse.csv ! `
|
||||
videocrop bottom=3 ! `
|
||||
queue ! `
|
||||
linescan direction=vertical output-size=1900 ! `
|
||||
videoconvert ! autovideosink
|
||||
|
||||
# Note: multifilesink has been removed because it saves EVERY frame.
|
||||
# To save only on rollover events, use the Python scripts which connect
|
||||
# to the rollover signal and save via PIL/numpy.
|
||||
512
gst/linescan/launch_with_signal.py
Normal file
512
gst/linescan/launch_with_signal.py
Normal file
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["argcomplete", "numpy", "pillow"]
|
||||
# ///
|
||||
#
|
||||
# Linescan Pipeline with Rollover Signal Capture
|
||||
#
|
||||
# This script creates a GStreamer pipeline that captures linescan images from an IDS uEye camera
|
||||
# and saves frames only when the linescan buffer wraps around (rollover events).
|
||||
# Features dual output: live preview display and automatic file saving on rollover.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Build the GStreamer plugins first: .\build.ps1
|
||||
# 2. Ensure GST_PLUGIN_PATH includes the build directory with custom plugins
|
||||
# 3. The custom linescan plugin must be built and available to GStreamer
|
||||
#
|
||||
# Basic Usage:
|
||||
# uv run .\gst\linescan\launch_with_signal.py # Run with defaults (day mode)
|
||||
# uv run .\gst\linescan\launch_with_signal.py --mode night # Night mode (higher exposure & gain)
|
||||
# uv run .\gst\linescan\launch_with_signal.py --debug # Enable debug output
|
||||
# uv run .\gst\linescan\launch_with_signal.py --format png # Save as PNG
|
||||
# uv run .\gst\linescan\launch_with_signal.py --output-dir custom # Custom output directory
|
||||
# uv run .\gst\linescan\launch_with_signal.py --gst-debug # Enable GStreamer debug (level 3)
|
||||
# uv run .\gst\linescan\launch_with_signal.py --help # Show all options
|
||||
# python gst/linescan/launch_with_signal.py # Alternative without uv
|
||||
#
|
||||
# Features:
|
||||
# - Automatic capture on linescan buffer rollover (precise frame control)
|
||||
# - Live preview display window (autovideosink)
|
||||
# - File output with automatic sequential numbering
|
||||
# - IDS uEye camera source with intervalometer integration
|
||||
# - Configurable camera settings (exposure, framerate, gain)
|
||||
# - Vertical linescan direction with 1900px output size
|
||||
# - 3-pixel bottom crop for sensor cleanup
|
||||
# - Dual output via tee element (display + file save)
|
||||
#
|
||||
# Pipeline Architecture:
|
||||
# idsueyesrc -> intervalometer -> videocrop -> queue -> linescan -> videoconvert -> autovideosink
|
||||
#
|
||||
# Note: Files are saved via Python in the rollover signal callback, not through the pipeline
|
||||
#
|
||||
# Configuration:
|
||||
# - Camera config: ini/roi-night.ini (adjust CONFIG_FILE variable if needed)
|
||||
# - Camera modes:
|
||||
# * Day mode (default): exposure=0.4ms, gain=0, framerate=200fps
|
||||
# * Night mode: exposure=5.25ms, gain=42, framerate=200fps
|
||||
# - Device ID: 2 (set in source.set_property)
|
||||
# - Linescan output size: 1900 pixels (set in linescan.set_property)
|
||||
# - Output directory: results/<date>/<funny-name>/ (auto-created with funny names)
|
||||
# - Output format: JPEG (quality 95) or PNG (lossless)
|
||||
#
|
||||
# Output Files:
|
||||
# - Format: linescan_rollover_####.jpeg (or .png with --format png)
|
||||
# - Location: results/<date>/<funny-name>/ by default (e.g., "fuzzy-photon")
|
||||
# or custom path with --output-dir
|
||||
# - Triggered by: Buffer rollover events
|
||||
# - Quality: JPEG=95, PNG=lossless
|
||||
# - Sequential numbering: 0000, 0001, 0002, etc.
|
||||
# - Example: results/20251118/bouncy-pixel/linescan_rollover_0000.jpeg
|
||||
#
|
||||
# Stopping:
|
||||
# Press Ctrl+C to stop the pipeline gracefully
|
||||
#
|
||||
# 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'"
|
||||
# - Cause: Custom plugins not found in GST_PLUGIN_PATH
|
||||
# - Solution:
|
||||
# 1. Verify plugins are built in the output 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
|
||||
#
|
||||
# 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:
|
||||
# - Files are saved via Python/PIL in the rollover callback (only when buffer wraps)
|
||||
# - The display shows the live linescan accumulation
|
||||
# - No buffer drops related to encoding since file saving is outside the pipeline
|
||||
# - Rollover occurs after accumulating output-size (1900) lines from camera frames
|
||||
#
|
||||
# Notes:
|
||||
# - Requires GStreamer with custom vision plugins installed (build with .\build.ps1)
|
||||
# - Must run from workspace root (c:/dev/gst-plugins-vision) or adjust CONFIG_FILE path
|
||||
# - GST_PLUGIN_PATH must include the directory containing built custom plugins
|
||||
# - Enable GStreamer debug logging by modifying Gst.debug_set_threshold_for_name() call
|
||||
# - Intervalometer provides automatic gain ramping for timelapse scenarios
|
||||
# - The rollover signal is defined in gst/linescan/gstlinescan.c:178-182
|
||||
# - To check if plugin is loaded: GST_DEBUG=2 gst-inspect-1.0 linescan
|
||||
# - Uses multifilesink for automatic sequential file numbering
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 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, GLib
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import random
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
frame_counter = 0
|
||||
output_dir = None # Will be set in main()
|
||||
|
||||
# Funny two-word name lists
|
||||
ADJECTIVES = [
|
||||
"happy", "sleepy", "grumpy", "bouncy", "fuzzy", "dizzy", "sparkly", "wobbly",
|
||||
"quirky", "snappy", "zippy", "jolly", "fluffy", "bumpy", "zesty", "peppy"
|
||||
]
|
||||
NOUNS = [
|
||||
"pixel", "photon", "widget", "gadget", "scanner", "buffer", "tensor", "matrix",
|
||||
"vector", "sensor", "filter", "shutter", "aperture", "capture", "snapshot", "frame"
|
||||
]
|
||||
|
||||
# Config file path - adjust if running from different directory
|
||||
# From workspace root: "ini/roi-night.ini"
|
||||
# From gst/linescan: "../../ini/roi-night.ini"
|
||||
CONFIG_FILE = "ini/roi-night.ini"
|
||||
|
||||
def on_rollover(linescan, buffer):
|
||||
"""Called when linescan buffer wraps around - save the buffer to file"""
|
||||
global frame_counter
|
||||
|
||||
# Create output directory if needed
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
# Get format from args if available, otherwise default to jpeg
|
||||
import __main__
|
||||
file_format = getattr(__main__, 'output_format', 'jpeg')
|
||||
|
||||
# Generate filename
|
||||
filename = os.path.join(output_dir, f"linescan_rollover_{frame_counter:04d}.{file_format}")
|
||||
|
||||
# Map the buffer to get image data
|
||||
success, mapinfo = buffer.map(Gst.MapFlags.READ)
|
||||
if not success:
|
||||
print(f"[ROLLOVER ERROR] Failed to map buffer for frame {frame_counter}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Get caps to determine image dimensions
|
||||
caps = buffer.get_caps() if hasattr(buffer, 'get_caps') else linescan.get_static_pad("src").get_current_caps()
|
||||
structure = caps.get_structure(0)
|
||||
width = structure.get_value('width')
|
||||
height = structure.get_value('height')
|
||||
format_str = structure.get_value('format')
|
||||
|
||||
# Convert buffer data to numpy array
|
||||
data = np.frombuffer(mapinfo.data, dtype=np.uint8)
|
||||
|
||||
# Determine number of channels based on format
|
||||
if format_str in ['RGB', 'BGR']:
|
||||
channels = 3
|
||||
data = data.reshape((height, width, channels))
|
||||
if format_str == 'BGR':
|
||||
# Convert BGR to RGB for PIL
|
||||
data = data[:, :, ::-1]
|
||||
elif format_str in ['GRAY8']:
|
||||
channels = 1
|
||||
data = data.reshape((height, width))
|
||||
else:
|
||||
print(f"[ROL LOVER WARNING] Unsupported format {format_str}, saving as-is")
|
||||
data = data.reshape((height, width, -1))
|
||||
|
||||
# Create PIL Image and save
|
||||
if channels == 1:
|
||||
img = Image.fromarray(data, mode='L')
|
||||
else:
|
||||
img = Image.fromarray(data, mode='RGB')
|
||||
|
||||
if file_format == 'png':
|
||||
img.save(filename, 'PNG')
|
||||
else:
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
|
||||
print(f"[ROLLOVER] Frame {frame_counter} saved to: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ROLLOVER ERROR] Failed to save frame {frame_counter}: {e}")
|
||||
finally:
|
||||
buffer.unmap(mapinfo)
|
||||
frame_counter += 1
|
||||
|
||||
def on_message(bus, message):
|
||||
"""Handle pipeline messages"""
|
||||
t = message.type
|
||||
if t == Gst.MessageType.EOS:
|
||||
print("\nEnd-of-stream")
|
||||
loop.quit()
|
||||
elif t == Gst.MessageType.ERROR:
|
||||
err, debug = message.parse_error()
|
||||
print(f"\nError: {err}")
|
||||
print(f"Debug: {debug}")
|
||||
loop.quit()
|
||||
elif t == Gst.MessageType.WARNING:
|
||||
warn, debug = message.parse_warning()
|
||||
print(f"\nWarning: {warn}")
|
||||
|
||||
def main():
|
||||
global loop, output_format
|
||||
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Linescan pipeline with rollover signal capture',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
%(prog)s # Run with defaults (day mode)
|
||||
%(prog)s --mode night # Night mode (higher exposure & gain)
|
||||
%(prog)s --debug # Enable debug output
|
||||
%(prog)s --format png # Save as PNG instead of JPEG
|
||||
%(prog)s --output-dir my_captures # Custom output directory
|
||||
%(prog)s --gst-debug # Enable GStreamer debug
|
||||
%(prog)s --gst-debug-level 4 # Set custom GStreamer debug level
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument('--debug', '-d', action='store_true',
|
||||
help='Enable debug output from this script')
|
||||
parser.add_argument('--gst-debug', action='store_true',
|
||||
help='Enable GStreamer debug output (level 3)')
|
||||
parser.add_argument('--gst-debug-level', type=int, metavar='LEVEL',
|
||||
help='Set GStreamer debug level (0-9, default: 3 if --gst-debug)')
|
||||
parser.add_argument('--format', '-f', choices=['jpeg', 'png'], default='jpeg',
|
||||
help='Output image format (default: jpeg)')
|
||||
parser.add_argument('--output-dir', '-o', metavar='DIR',
|
||||
help='Output directory (default: results/<date>/<funny-name>)')
|
||||
parser.add_argument('--mode', '-m', choices=['day', 'night'], default='day',
|
||||
help='Camera mode: day (0.4ms exposure, 0 gain) or night (5.25ms exposure, 42 gain) (default: day)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Make format globally available for rollover callback
|
||||
output_format = args.format
|
||||
|
||||
# Set up output directory
|
||||
global output_dir
|
||||
if args.output_dir:
|
||||
output_dir = args.output_dir
|
||||
else:
|
||||
# Create default: results/<date>/<funny-name>
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
results_base = os.path.join("results", date_str)
|
||||
|
||||
# Generate funny name: adjective-noun
|
||||
funny_name = f"{random.choice(ADJECTIVES)}-{random.choice(NOUNS)}"
|
||||
output_dir = os.path.join(results_base, funny_name)
|
||||
|
||||
# If name exists, add number suffix
|
||||
if os.path.exists(output_dir):
|
||||
seq = 1
|
||||
while True:
|
||||
output_dir = os.path.join(results_base, f"{funny_name}-{seq}")
|
||||
if not os.path.exists(output_dir):
|
||||
break
|
||||
seq += 1
|
||||
|
||||
if args.debug:
|
||||
print(f"Using auto-generated output directory: {output_dir}")
|
||||
|
||||
# Set GStreamer debug level
|
||||
if args.gst_debug_level is not None:
|
||||
debug_level = args.gst_debug_level
|
||||
elif args.gst_debug:
|
||||
debug_level = 3
|
||||
else:
|
||||
debug_level = None
|
||||
|
||||
if debug_level is not None:
|
||||
os.environ['GST_DEBUG'] = str(debug_level)
|
||||
if args.debug:
|
||||
print(f"Setting GST_DEBUG={debug_level}")
|
||||
|
||||
Gst.init(None)
|
||||
|
||||
if args.debug:
|
||||
print("Initializing GStreamer pipeline...")
|
||||
print(f"GStreamer version: {Gst.version_string()}")
|
||||
|
||||
# Create output directory early
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
if args.debug:
|
||||
print(f"Created output directory: {output_dir}")
|
||||
|
||||
# Create pipeline with tee to split output
|
||||
pipeline = Gst.Pipeline.new("linescan-capture")
|
||||
|
||||
# Source chain
|
||||
source = Gst.ElementFactory.make("idsueyesrc", "source")
|
||||
intervalometer = Gst.ElementFactory.make("intervalometer", "intervalometer")
|
||||
videocrop = Gst.ElementFactory.make("videocrop", "crop")
|
||||
queue1 = Gst.ElementFactory.make("queue", "queue1")
|
||||
linescan = Gst.ElementFactory.make("linescan", "linescan")
|
||||
|
||||
# Display output
|
||||
videoconvert = Gst.ElementFactory.make("videoconvert", "convert")
|
||||
videosink = Gst.ElementFactory.make("autovideosink", "videosink")
|
||||
|
||||
# Check which elements failed to create
|
||||
elements = {
|
||||
'pipeline': pipeline,
|
||||
'idsueyesrc': source,
|
||||
'intervalometer': intervalometer,
|
||||
'videocrop': videocrop,
|
||||
'queue1': queue1,
|
||||
'linescan': linescan,
|
||||
'videoconvert': videoconvert,
|
||||
'videosink': videosink
|
||||
}
|
||||
|
||||
failed_elements = [name for name, elem in elements.items() if elem is None]
|
||||
|
||||
if failed_elements:
|
||||
print("ERROR: Failed to create the following elements:")
|
||||
for name in failed_elements:
|
||||
print(f" - {name}")
|
||||
print("\nTroubleshooting:")
|
||||
if 'linescan' in failed_elements or 'idsueyesrc' in failed_elements:
|
||||
print(" Custom plugins not found. Make sure:")
|
||||
print(" 1. Run .\build.ps1 to build the plugins")
|
||||
print(" 2. Set GST_PLUGIN_PATH to include the build directory")
|
||||
print(" 3. Verify with: gst-inspect-1.0 linescan")
|
||||
if 'intervalometer' in failed_elements:
|
||||
print(" Intervalometer plugin not found (custom plugin)")
|
||||
return -1
|
||||
|
||||
if args.debug:
|
||||
print("All elements created successfully")
|
||||
|
||||
# Configure source
|
||||
if args.debug:
|
||||
print(f"Configuring camera with config file: {CONFIG_FILE}")
|
||||
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
print(f"WARNING: Config file not found: {CONFIG_FILE}")
|
||||
print("Continuing without config file...")
|
||||
else:
|
||||
source.set_property("config-file", CONFIG_FILE)
|
||||
|
||||
# Set camera parameters based on mode
|
||||
if args.mode == 'day':
|
||||
exposure = 0.4
|
||||
gain = 0
|
||||
else: # night
|
||||
exposure = 5.25
|
||||
gain = 42
|
||||
|
||||
framerate = 200 # Same for both modes
|
||||
device_id = 2
|
||||
|
||||
source.set_property("exposure", exposure)
|
||||
source.set_property("framerate", framerate)
|
||||
source.set_property("gain", gain)
|
||||
source.set_property("device-id", device_id)
|
||||
|
||||
if args.debug:
|
||||
print(f"Camera mode: {args.mode}")
|
||||
print(f"Camera settings: exposure={exposure}ms, framerate={framerate}fps, gain={gain}, device-id={device_id}")
|
||||
|
||||
# Configure intervalometer
|
||||
intervalometer.set_property("enabled", True)
|
||||
intervalometer.set_property("camera-element", source)
|
||||
intervalometer.set_property("ramp-rate", "vslow")
|
||||
intervalometer.set_property("update-interval", 1000)
|
||||
intervalometer.set_property("gain-max", 52)
|
||||
intervalometer.set_property("log-file", "timelapse.csv")
|
||||
|
||||
# Configure videocrop
|
||||
videocrop.set_property("bottom", 3)
|
||||
|
||||
# Configure linescan
|
||||
linescan.set_property("direction", 1) # vertical
|
||||
linescan.set_property("output-size", 1900)
|
||||
|
||||
if args.debug:
|
||||
print(f"Linescan: direction=vertical, output-size=1900")
|
||||
print(f"Files will be saved via rollover callback (not through pipeline)")
|
||||
|
||||
# Connect rollover signal - files are saved in the callback
|
||||
linescan.connect("rollover", on_rollover)
|
||||
|
||||
# Add all elements to pipeline
|
||||
pipeline.add(source)
|
||||
pipeline.add(intervalometer)
|
||||
pipeline.add(videocrop)
|
||||
pipeline.add(queue1)
|
||||
pipeline.add(linescan)
|
||||
pipeline.add(videoconvert)
|
||||
pipeline.add(videosink)
|
||||
|
||||
# Link elements - simple chain now
|
||||
if not source.link(intervalometer):
|
||||
print("ERROR: Could not link source to intervalometer")
|
||||
return -1
|
||||
if not intervalometer.link(videocrop):
|
||||
print("ERROR: Could not link intervalometer to videocrop")
|
||||
return -1
|
||||
if not videocrop.link(queue1):
|
||||
print("ERROR: Could not link videocrop to queue1")
|
||||
return -1
|
||||
if not queue1.link(linescan):
|
||||
print("ERROR: Could not link queue1 to linescan")
|
||||
return -1
|
||||
if not linescan.link(videoconvert):
|
||||
print("ERROR: Could not link linescan to videoconvert")
|
||||
return -1
|
||||
if not videoconvert.link(videosink):
|
||||
print("ERROR: Could not link videoconvert to videosink")
|
||||
return -1
|
||||
|
||||
# Enable debug for linescan if requested
|
||||
if args.debug or args.gst_debug:
|
||||
Gst.debug_set_threshold_for_name("linescan", Gst.DebugLevel.LOG)
|
||||
if args.debug:
|
||||
print("Enabled LOG level debugging for linescan element")
|
||||
|
||||
# Set up message handling before starting
|
||||
bus = pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message", on_message)
|
||||
|
||||
if args.debug:
|
||||
print("\nStarting pipeline...")
|
||||
print(f"Pipeline state: {pipeline.get_state(0)[1].value_nick}")
|
||||
|
||||
# Start pipeline
|
||||
print(f"Frames will be saved to: {output_dir}/")
|
||||
print("Press Ctrl+C to stop\n")
|
||||
|
||||
ret = pipeline.set_state(Gst.State.PLAYING)
|
||||
if ret == Gst.StateChangeReturn.FAILURE:
|
||||
print("\nERROR: Unable to set pipeline to playing state")
|
||||
print("\nPossible causes:")
|
||||
print(" 1. Camera not connected or unavailable (device-id=2)")
|
||||
print(" 2. Config file issue: check if ini/roi-night.ini exists and is valid")
|
||||
print(" 3. Element compatibility issues")
|
||||
print("\nTo debug further:")
|
||||
print(" - Run with --debug flag for more information")
|
||||
print(" - Run with --gst-debug to see GStreamer messages")
|
||||
print(" - Check camera with: gst-launch-1.0 idsueyesrc ! fakesink")
|
||||
print(" - Verify config file exists and camera is connected")
|
||||
|
||||
# Try to get more error info from bus
|
||||
bus = pipeline.get_bus()
|
||||
msg = bus.timed_pop_filtered(Gst.SECOND, Gst.MessageType.ERROR | Gst.MessageType.WARNING)
|
||||
if msg:
|
||||
if msg.type == Gst.MessageType.ERROR:
|
||||
err, debug = msg.parse_error()
|
||||
print(f"\nGStreamer Error: {err}")
|
||||
if debug:
|
||||
print(f"Debug info: {debug}")
|
||||
|
||||
return -1
|
||||
|
||||
if args.debug:
|
||||
# Wait for state change to complete
|
||||
state_ret, state, pending = pipeline.get_state(Gst.CLOCK_TIME_NONE)
|
||||
print(f"Pipeline state change: {state_ret.value_nick}")
|
||||
print(f"Current state: {state.value_nick}")
|
||||
if pending != Gst.State.VOID_PENDING:
|
||||
print(f"Pending state: {pending.value_nick}")
|
||||
|
||||
# Run main loop
|
||||
loop = GLib.MainLoop()
|
||||
try:
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nStopping pipeline...")
|
||||
|
||||
# Cleanup
|
||||
pipeline.set_state(Gst.State.NULL)
|
||||
print(f"\nCaptured {frame_counter} rollover frames")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
149
gst/linescan/rollover_example.c
Normal file
149
gst/linescan/rollover_example.c
Normal file
@@ -0,0 +1,149 @@
|
||||
/* Simple Rollover Example - Linescan Buffer Capture Demo (C)
|
||||
*
|
||||
* This is a minimal C example showing rollover signal detection.
|
||||
*
|
||||
* NOTE: This example demonstrates signal connection but doesn't save files.
|
||||
* For actual file saving, use the Python examples (rollover_example.py or
|
||||
* launch_with_signal.py) which use PIL/numpy to properly save images.
|
||||
*
|
||||
* In C, you would need to:
|
||||
* 1. Map the buffer (gst_buffer_map)
|
||||
* 2. Parse the caps to get width/height/format
|
||||
* 3. Use a library like libpng or libjpeg to encode and save
|
||||
*
|
||||
* Compile with:
|
||||
* gcc rollover_example.c -o rollover_example `pkg-config --cflags --libs gstreamer-1.0 gstreamer-video-1.0`
|
||||
*
|
||||
* Usage:
|
||||
* ./rollover_example
|
||||
*
|
||||
* Pipeline:
|
||||
* videotestsrc -> linescan -> videoconvert -> autovideosink
|
||||
* ↓
|
||||
* (rollover signal)
|
||||
* ↓
|
||||
* C callback prints message
|
||||
*/
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static gint frame_counter = 0;
|
||||
|
||||
/* Callback function when rollover signal is emitted */
|
||||
static void
|
||||
on_rollover (GstElement *linescan, GstBuffer *buffer, gpointer user_data)
|
||||
{
|
||||
GstCaps *caps;
|
||||
GstStructure *structure;
|
||||
gint width, height;
|
||||
const gchar *format_str;
|
||||
|
||||
frame_counter++;
|
||||
|
||||
/* Get buffer information */
|
||||
GstPad *srcpad = gst_element_get_static_pad (linescan, "src");
|
||||
caps = gst_pad_get_current_caps (srcpad);
|
||||
structure = gst_caps_get_structure (caps, 0);
|
||||
|
||||
gst_structure_get_int (structure, "width", &width);
|
||||
gst_structure_get_int (structure, "height", &height);
|
||||
format_str = gst_structure_get_string (structure, "format");
|
||||
|
||||
g_print ("[ROLLOVER] Frame %d - %dx%d %s (size: %lu bytes)\n",
|
||||
frame_counter,
|
||||
width, height,
|
||||
format_str,
|
||||
(unsigned long) gst_buffer_get_size (buffer));
|
||||
|
||||
/* NOTE: To actually save the buffer to a file, you would:
|
||||
* 1. Map the buffer: gst_buffer_map(buffer, &map_info, GST_MAP_READ)
|
||||
* 2. Encode using libpng/libjpeg based on the data in map_info.data
|
||||
* 3. Write to file with the frame_counter in the filename
|
||||
* 4. Unmap: gst_buffer_unmap(buffer, &map_info)
|
||||
*
|
||||
* For a complete example with file saving, see the Python version.
|
||||
*/
|
||||
|
||||
gst_caps_unref (caps);
|
||||
gst_object_unref (srcpad);
|
||||
}
|
||||
|
||||
int
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
GstElement *pipeline, *source, *linescan;
|
||||
GstElement *videoconvert, *videosink;
|
||||
GstBus *bus;
|
||||
GstMessage *msg;
|
||||
|
||||
/* Initialize GStreamer */
|
||||
gst_init (&argc, &argv);
|
||||
|
||||
/* Create elements - simplified pipeline without file branch */
|
||||
pipeline = gst_pipeline_new ("rollover-pipeline");
|
||||
source = gst_element_factory_make ("videotestsrc", "source");
|
||||
linescan = gst_element_factory_make ("linescan", "linescan");
|
||||
videoconvert = gst_element_factory_make ("videoconvert", "convert");
|
||||
videosink = gst_element_factory_make ("autovideosink", "videosink");
|
||||
|
||||
if (!pipeline || !source || !linescan || !videoconvert || !videosink) {
|
||||
g_printerr ("Not all elements could be created.\n");
|
||||
g_printerr ("Make sure linescan plugin is built and in GST_PLUGIN_PATH.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Configure elements */
|
||||
g_object_set (source, "pattern", 18, NULL); /* ball pattern */
|
||||
g_object_set (linescan,
|
||||
"direction", 0, /* horizontal */
|
||||
"line-index", 100,
|
||||
"output-size", 400,
|
||||
NULL);
|
||||
|
||||
/* Connect to rollover signal */
|
||||
g_signal_connect (linescan, "rollover", G_CALLBACK (on_rollover), NULL);
|
||||
|
||||
/* Build the pipeline - simple chain */
|
||||
gst_bin_add_many (GST_BIN (pipeline), source, linescan,
|
||||
videoconvert, videosink, NULL);
|
||||
|
||||
if (!gst_element_link_many (source, linescan, videoconvert, videosink, NULL)) {
|
||||
g_printerr ("Elements could not be linked.\n");
|
||||
gst_object_unref (pipeline);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Start playing */
|
||||
g_print ("Pipeline running. Rollover events will be printed.\n");
|
||||
g_print ("Press Ctrl+C to stop.\n\n");
|
||||
gst_element_set_state (pipeline, GST_STATE_PLAYING);
|
||||
|
||||
/* Wait until error or EOS */
|
||||
bus = gst_element_get_bus (pipeline);
|
||||
msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
|
||||
GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
|
||||
|
||||
/* Handle any errors */
|
||||
if (msg != NULL) {
|
||||
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
|
||||
GError *err;
|
||||
gchar *debug_info;
|
||||
gst_message_parse_error (msg, &err, &debug_info);
|
||||
g_printerr ("\nError: %s\n", err->message);
|
||||
g_error_free (err);
|
||||
g_free (debug_info);
|
||||
}
|
||||
gst_message_unref (msg);
|
||||
}
|
||||
|
||||
/* Free resources */
|
||||
gst_object_unref (bus);
|
||||
gst_element_set_state (pipeline, GST_STATE_NULL);
|
||||
gst_object_unref (pipeline);
|
||||
|
||||
g_print ("\nDetected %d rollover events\n", frame_counter);
|
||||
|
||||
return 0;
|
||||
}
|
||||
198
gst/linescan/rollover_example.py
Normal file
198
gst/linescan/rollover_example.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["numpy", "pillow"]
|
||||
# ///
|
||||
#
|
||||
# Simple Rollover Example - Linescan Buffer Capture Demo
|
||||
#
|
||||
# This is a minimal example showing how to capture linescan images using
|
||||
# the rollover signal with a videotestsrc. Files are saved via Python/PIL
|
||||
# in the rollover callback, demonstrating the correct approach.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Build the GStreamer plugins: .\build.ps1
|
||||
# 2. Ensure GST_PLUGIN_PATH includes the build directory
|
||||
#
|
||||
# Usage:
|
||||
# uv run .\gst\linescan\rollover_example.py # Run with defaults
|
||||
# python gst/linescan/rollover_example.py # Alternative without uv
|
||||
#
|
||||
# Features:
|
||||
# - Uses videotestsrc (ball pattern) for testing
|
||||
# - Saves images on rollover signal (not every frame)
|
||||
# - Live preview window showing linescan accumulation
|
||||
# - JPEG output to current directory
|
||||
#
|
||||
# Pipeline:
|
||||
# videotestsrc -> linescan -> videoconvert -> autovideosink
|
||||
# ↓
|
||||
# (rollover signal)
|
||||
# ↓
|
||||
# Python callback saves file
|
||||
#
|
||||
# See launch_with_signal.py for a full-featured version with camera support.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 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\\")
|
||||
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, GLib
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
frame_counter = 0
|
||||
|
||||
def on_rollover(linescan, buffer):
|
||||
"""Callback when rollover signal is emitted - save buffer to JPEG"""
|
||||
global frame_counter
|
||||
|
||||
filename = f"linescan_frame_{frame_counter:04d}.jpeg"
|
||||
|
||||
# Map the buffer to get image data
|
||||
success, mapinfo = buffer.map(Gst.MapFlags.READ)
|
||||
if not success:
|
||||
print(f"[ERROR] Failed to map buffer for frame {frame_counter}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Get caps to determine image dimensions
|
||||
caps = linescan.get_static_pad("src").get_current_caps()
|
||||
structure = caps.get_structure(0)
|
||||
width = structure.get_value('width')
|
||||
height = structure.get_value('height')
|
||||
format_str = structure.get_value('format')
|
||||
|
||||
# Convert buffer data to numpy array
|
||||
data = np.frombuffer(mapinfo.data, dtype=np.uint8)
|
||||
|
||||
# Handle different formats
|
||||
if format_str in ['RGB', 'BGR']:
|
||||
channels = 3
|
||||
data = data.reshape((height, width, channels))
|
||||
if format_str == 'BGR':
|
||||
data = data[:, :, ::-1] # Convert BGR to RGB
|
||||
elif format_str == 'GRAY8':
|
||||
channels = 1
|
||||
data = data.reshape((height, width))
|
||||
else:
|
||||
data = data.reshape((height, width, -1))
|
||||
|
||||
# Create PIL Image and save
|
||||
if channels == 1:
|
||||
img = Image.fromarray(data, mode='L')
|
||||
else:
|
||||
img = Image.fromarray(data, mode='RGB')
|
||||
|
||||
img.save(filename, 'JPEG', quality=95)
|
||||
print(f"[ROLLOVER] Frame {frame_counter} saved to: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to save frame {frame_counter}: {e}")
|
||||
finally:
|
||||
buffer.unmap(mapinfo)
|
||||
frame_counter += 1
|
||||
|
||||
def main():
|
||||
Gst.init(None)
|
||||
|
||||
# Create pipeline - simplified without file branch
|
||||
# videotestsrc -> linescan -> videoconvert -> autovideosink
|
||||
# ↓
|
||||
# (rollover callback saves files)
|
||||
|
||||
pipeline = Gst.Pipeline.new("rollover-pipeline")
|
||||
|
||||
# Create elements
|
||||
source = Gst.ElementFactory.make("videotestsrc", "source")
|
||||
linescan = Gst.ElementFactory.make("linescan", "linescan")
|
||||
convert = Gst.ElementFactory.make("videoconvert", "convert")
|
||||
videosink = Gst.ElementFactory.make("autovideosink", "videosink")
|
||||
|
||||
if not all([pipeline, source, linescan, convert, videosink]):
|
||||
print("ERROR: Not all elements could be created")
|
||||
print("Make sure linescan plugin is built and in GST_PLUGIN_PATH")
|
||||
return -1
|
||||
|
||||
# Configure elements
|
||||
source.set_property("pattern", 18) # ball pattern
|
||||
linescan.set_property("direction", 0) # horizontal
|
||||
linescan.set_property("line-index", 100)
|
||||
linescan.set_property("output-size", 400)
|
||||
|
||||
# Connect to rollover signal - files saved in callback
|
||||
linescan.connect("rollover", on_rollover)
|
||||
|
||||
# Add elements to pipeline
|
||||
pipeline.add(source)
|
||||
pipeline.add(linescan)
|
||||
pipeline.add(convert)
|
||||
pipeline.add(videosink)
|
||||
|
||||
# Link elements - simple chain
|
||||
if not source.link(linescan):
|
||||
print("ERROR: Could not link source to linescan")
|
||||
return -1
|
||||
if not linescan.link(convert):
|
||||
print("ERROR: Could not link linescan to convert")
|
||||
return -1
|
||||
if not convert.link(videosink):
|
||||
print("ERROR: Could not link convert to videosink")
|
||||
return -1
|
||||
|
||||
# Start playing
|
||||
pipeline.set_state(Gst.State.PLAYING)
|
||||
|
||||
# Create main loop
|
||||
loop = GLib.MainLoop()
|
||||
|
||||
# Handle bus messages
|
||||
bus = pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
|
||||
def on_message(bus, message):
|
||||
t = message.type
|
||||
if t == Gst.MessageType.EOS:
|
||||
print("\nEnd-of-stream")
|
||||
loop.quit()
|
||||
elif t == Gst.MessageType.ERROR:
|
||||
err, debug = message.parse_error()
|
||||
print(f"\nError: {err}, {debug}")
|
||||
loop.quit()
|
||||
|
||||
bus.connect("message", on_message)
|
||||
|
||||
print("Pipeline running. Files saved on rollover to current directory.")
|
||||
print("Press Ctrl+C to stop.\n")
|
||||
try:
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping...")
|
||||
|
||||
# Cleanup
|
||||
pipeline.set_state(Gst.State.NULL)
|
||||
print(f"Captured {frame_counter} rollover frames")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user