diff --git a/gst/linescan/README.md b/gst/linescan/README.md index 568c4cf..4c2ccb2 100644 --- a/gst/linescan/README.md +++ b/gst/linescan/README.md @@ -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) diff --git a/gst/linescan/gstlinescan.c b/gst/linescan/gstlinescan.c index a9713fc..8378057 100644 --- a/gst/linescan/gstlinescan.c +++ b/gst/linescan/gstlinescan.c @@ -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", diff --git a/gst/linescan/gstlinescan.h b/gst/linescan/gstlinescan.h index 5a6e668..a34ea97 100644 --- a/gst/linescan/gstlinescan.h +++ b/gst/linescan/gstlinescan.h @@ -78,6 +78,9 @@ struct _GstLinescan struct _GstLinescanClass { GstBaseTransformClass parent_class; + + /* Signals */ + void (*rollover) (GstLinescan *linescan, GstBuffer *buffer); }; GType gst_linescan_get_type(void); diff --git a/gst/linescan/launch_with_capture.ps1 b/gst/linescan/launch_with_capture.ps1 new file mode 100644 index 0000000..989ec78 --- /dev/null +++ b/gst/linescan/launch_with_capture.ps1 @@ -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. \ No newline at end of file diff --git a/gst/linescan/launch_with_signal.py b/gst/linescan/launch_with_signal.py new file mode 100644 index 0000000..40a0ab4 --- /dev/null +++ b/gst/linescan/launch_with_signal.py @@ -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/// (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/// 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//)') + 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_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()) \ No newline at end of file diff --git a/gst/linescan/rollover_example.c b/gst/linescan/rollover_example.c new file mode 100644 index 0000000..e3d14cf --- /dev/null +++ b/gst/linescan/rollover_example.c @@ -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 +#include +#include + +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; +} \ No newline at end of file diff --git a/gst/linescan/rollover_example.py b/gst/linescan/rollover_example.py new file mode 100644 index 0000000..14973af --- /dev/null +++ b/gst/linescan/rollover_example.py @@ -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()) \ No newline at end of file