Add intervalometer filter for automatic exposure control

Implements YASS-inspired automatic exposure control for IDS uEye cameras.
The intervalometer filter analyzes video brightness in real-time and
smoothly ramps camera exposure and gain settings during changing light
conditions - ideal for sunset/sunrise time-lapse photography.

Key features:
- Automatic exposure ramping (0.85-1.24ms configurable range)
- Automatic gain control (0-52 configurable range)
- Real-time brightness analysis (GRAY8, GRAY16, RGB, BGR, BGRA)
- YASS-inspired ramping algorithm (exposure priority, then gain)
- Configurable ramp rates (VSlow/Slow/Medium/Fast/VFast)
- Exposure compensation (±4 stops)
- CSV logging of exposure parameters
- Direct GObject property control (no message bus overhead)

Technical implementation:
- GstBaseTransform filter for in-place processing
- Discovers upstream camera element by name
- Controls camera via g_object_set() for synchronous updates
- Frame-by-frame brightness calculation with format support

Files added:
- gst/intervalometer/gstintervalometer.c: Main implementation (734 lines)
- gst/intervalometer/gstintervalometer.h: Header with structure definitions
- gst/intervalometer/CMakeLists.txt: Build configuration
- gst/intervalometer/README.md: Comprehensive documentation

Files modified:
- gst/CMakeLists.txt: Added intervalometer subdirectory
- build.ps1: Added intervalometer to build and deployment pipeline

Usage example:
  gst-launch-1.0 idsueyesrc name=cam ! \\
    intervalometer enabled=true camera-element=cam \\
      exposure-min=0.85 exposure-max=1.24 \\
      gain-min=0 gain-max=52 ramp-rate=medium ! \\
    autovideosink

Inspired by YASS (Yet Another Sunset Script) for CHDK cameras by
waterwingz, based on work by Fbonomi and soulf2.
This commit is contained in:
yair
2025-11-17 01:05:16 +02:00
parent 5fb24cb294
commit b11cd27d14
6 changed files with 1088 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ endif (OPENCV_FOUND)
add_subdirectory (bayerutils)
add_subdirectory (extractcolor)
add_subdirectory (intervalometer)
if (ENABLE_KLV)
add_subdirectory (klv)

View File

@@ -0,0 +1,28 @@
set (SOURCES
gstintervalometer.c
)
set (HEADERS
gstintervalometer.h)
include_directories (AFTER
${ORC_INCLUDE_DIR})
set (libname gstintervalometer)
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

@@ -0,0 +1,253 @@
# GStreamer Intervalometer Filter
**Automatic Exposure Control for IDS uEye Cameras**
Inspired by [YASS (Yet Another Sunset Script)](../../yass/README.md) for CHDK cameras.
## Overview
The `intervalometer` element is a GStreamer filter that automatically adjusts camera exposure and gain settings during changing light conditions. It analyzes video brightness in real-time and smoothly ramps camera parameters to maintain optimal exposure - perfect for time-lapse photography during sunset, sunrise, or other variable lighting scenarios.
## Features
- **Automatic Exposure Ramping**: Smoothly adjusts exposure time based on scene brightness
- **Automatic Gain Control**: Increases/decreases sensor gain when exposure limits are reached
- **Configurable Ranges**: Set custom min/max values for exposure (0.85-1.24ms) and gain (0-52)
- **Multiple Ramp Rates**: Choose from VSlow/Slow/Medium/Fast/VFast adjustment speeds
- **Exposure Compensation**: Fine-tune brightness with ±4 stops of compensation
- **CSV Logging**: Optional detailed logging of all exposure parameters
- **Multiple Format Support**: Works with GRAY8, GRAY16, RGB, BGR, and BGRA video
## Requirements
- GStreamer 1.0+
- IDS uEye camera with `idsueyesrc` element
- Camera must support runtime exposure and gain property changes
## Installation
The filter is built as part of the gst-plugins-vision project. Build and install normally:
```bash
mkdir build
cd build
cmake ..
make
make install
```
## Properties
### Control Properties
| Property | Type | Range | Default | Description |
|----------|------|-------|---------|-------------|
| `enabled` | boolean | - | TRUE | Enable/disable auto-exposure |
| `target-brightness` | double | 0-255 | 128.0 | Target average brightness level |
| `compensation` | double | -4.0 to 4.0 | 0.0 | Exposure compensation in stops |
| `camera-element` | string | - | "" | Name of upstream idsueyesrc element |
### Exposure Range
| Property | Type | Range | Default | Description |
|----------|------|-------|---------|-------------|
| `exposure-min` | double | 0.01-1000.0 | 0.85 | Minimum exposure time (ms) |
| `exposure-max` | double | 0.01-1000.0 | 1.24 | Maximum exposure time (ms) |
### Gain Range
| Property | Type | Range | Default | Description |
|----------|------|-------|---------|-------------|
| `gain-min` | int | 0-100 | 0 | Minimum gain value |
| `gain-max` | int | 0-100 | 52 | Maximum gain value |
### Ramping
| Property | Type | Values | Default | Description |
|----------|------|--------|---------|-------------|
| `ramp-rate` | enum | VSlow, Slow, Medium, Fast, VFast | Medium | Speed of parameter changes |
### Logging
| Property | Type | Range | Default | Description |
|----------|------|-------|---------|-------------|
| `log-file` | string | - | "" | Path to CSV log file (empty = disabled) |
## Usage Examples
### Basic Auto-Exposure
```bash
gst-launch-1.0 idsueyesrc name=cam ! \
intervalometer enabled=true camera-element=cam ! \
videoconvert ! autovideosink
```
### Custom Range for Day/Night Transition
Configure for the typical day (0.85ms exposure, gain 52) to night (1.24ms exposure, gain 0) range:
```bash
gst-launch-1.0 idsueyesrc name=cam ! \
intervalometer enabled=true camera-element=cam \
exposure-min=0.85 exposure-max=1.24 \
gain-min=0 gain-max=52 \
ramp-rate=medium ! \
videoconvert ! autovideosink
```
### With Exposure Compensation
Adjust overall brightness with compensation:
```bash
gst-launch-1.0 idsueyesrc name=cam ! \
intervalometer enabled=true camera-element=cam \
compensation=1.0 \
target-brightness=140 ! \
videoconvert ! autovideosink
```
### With CSV Logging
Log all exposure data to a CSV file:
```bash
gst-launch-1.0 idsueyesrc name=cam ! \
intervalometer enabled=true camera-element=cam \
log-file=exposure_log.csv ! \
videoconvert ! autovideosink
```
### Complete Time-Lapse Pipeline
Record a time-lapse with auto-exposure:
```bash
gst-launch-1.0 idsueyesrc name=cam framerate=1 ! \
intervalometer enabled=true camera-element=cam \
exposure-min=0.85 exposure-max=1.24 \
gain-min=0 gain-max=52 \
ramp-rate=slow \
log-file=timelapse_exposure.csv ! \
videoconvert ! x264enc ! mp4mux ! \
filesink location=timelapse.mp4
```
## How It Works
### Exposure Control Algorithm
The filter uses a YASS-inspired algorithm:
1. **Brightness Analysis**: Calculates average brightness of each frame
2. **Error Calculation**: Compares to target brightness (with compensation)
3. **Ramping Priority**:
- When too bright: Decreases exposure first, then gain
- When too dark: Increases exposure first (up to max), then gain
4. **Smooth Ramping**: Changes are gradual based on ramp-rate setting
### Typical Behavior
- **Daytime**: Fast shutter (low exposure), high gain for noise reduction
- **Sunset/Dusk**: Gradually increases exposure time as light fades
- **Night**: Maximum exposure time, minimum gain
### CSV Log Format
When logging is enabled, the filter creates a CSV file with:
```csv
Frame,Time_s,Brightness,Exposure_ms,Gain,Target_Brightness
0,0.000,145.32,0.850,52,128.00
1,0.033,143.21,0.851,52,128.00
2,0.067,142.15,0.853,52,128.00
...
```
## Camera Property Control
The filter finds and controls the upstream `idsueyesrc` element using the `camera-element` property. It sets:
- **exposure**: Exposure time in milliseconds
- **gain**: Master gain (0-100 range)
Ensure your camera source is named and the name matches the `camera-element` property.
## Ramp Rates
| Rate | Multiplier | Best For |
|------|------------|----------|
| VSlow | 0.5x | Very slow light changes, maximum smoothness |
| Slow | 1.0x | Gradual sunset/sunrise over hours |
| Medium | 2.0x | Normal time-lapse scenarios |
| Fast | 4.0x | Faster light changes, clouds passing |
| VFast | 8.0x | Quick adaptation, testing |
## Tips for Best Results
### Time-Lapse Settings
```
exposure-min: 0.85 (or camera-specific minimum)
exposure-max: 1.24 (or 1/framerate to avoid motion blur)
gain-min: 0 (cleanest image)
gain-max: 52 (or camera's limit)
ramp-rate: slow or medium
target-brightness: 128-140
```
### Fast Changing Conditions
```
ramp-rate: fast or vfast
compensation: Adjust to preference (-1.0 for darker, +1.0 for brighter)
```
### Maximum Image Quality
```
gain-max: 20-30 (lower max gain = less noise)
ramp-rate: slow (smoother transitions)
```
## Troubleshooting
**Filter not adjusting exposure:**
- Verify `camera-element` property matches your camera source name
- Check that camera allows runtime exposure/gain changes
- Ensure `enabled=true` is set
**Changes too fast/slow:**
- Adjust `ramp-rate` property
- Check `exposure-min`/`exposure-max` range is appropriate
**Brightness not reaching target:**
- Increase `gain-max` to allow more gain
- Increase `exposure-max` if not motion-limited
- Adjust `target-brightness` or use `compensation`
**Log file not created:**
- Check file path is writable
- Verify `log-file` property is set before starting pipeline
## Comparison to YASS
| Feature | YASS (CHDK) | Intervalometer (GStreamer) |
|---------|-------------|---------------------------|
| Platform | Canon cameras with CHDK | IDS uEye cameras |
| Control | Shutter speed + ISO | Exposure time + Gain |
| Integration | Standalone Lua script | GStreamer pipeline element |
| Real-time | Script-based intervals | Frame-by-frame analysis |
| Logging | CSV to SD card | CSV to filesystem |
## License
This filter is part of gst-plugins-vision and released under the GNU Library General Public License (LGPL).
Inspired by YASS (Yet Another Sunset Script) by waterwingz, based on work by Fbonomi and soulf2, released under GPL.
## See Also
- [YASS Documentation](../../yass/README.md) - Original CHDK script that inspired this filter
- [idsueyesrc](../../sys/idsueye/gstidsueyesrc.c) - IDS uEye camera source element

View File

@@ -0,0 +1,693 @@
/* GStreamer
* Copyright (C) 2024 FIXME <fixme@example.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-intervalometer
*
* Automatic exposure control for IDS uEye cameras.
* Inspired by YASS (Yet Another Sunset Script) for CHDK cameras.
*
* Analyzes video brightness and automatically adjusts camera exposure
* and gain settings to maintain optimal exposure during changing light
* conditions (e.g., sunset/sunrise time-lapse).
*
* <refsect2>
* <title>Example launch line</title>
* |[
* gst-launch-1.0 idsueyesrc name=cam ! intervalometer enabled=true camera-element=cam ! videoconvert ! autovideosink
* ]|
* Automatically adjusts exposure and gain on the IDS uEye camera
* </refsect2>
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstintervalometer.h"
#include <math.h>
#include <string.h>
GST_DEBUG_CATEGORY_STATIC (gst_intervalometer_debug);
#define GST_CAT_DEFAULT gst_intervalometer_debug
/* Properties */
enum
{
PROP_0,
PROP_ENABLED,
PROP_TARGET_BRIGHTNESS,
PROP_COMPENSATION,
PROP_EXPOSURE_MIN,
PROP_EXPOSURE_MAX,
PROP_GAIN_MIN,
PROP_GAIN_MAX,
PROP_RAMP_RATE,
PROP_LOG_FILE,
PROP_CAMERA_ELEMENT
};
#define DEFAULT_PROP_ENABLED TRUE
#define DEFAULT_PROP_TARGET_BRIGHTNESS 128.0
#define DEFAULT_PROP_COMPENSATION 0.0
#define DEFAULT_PROP_EXPOSURE_MIN 0.85
#define DEFAULT_PROP_EXPOSURE_MAX 1.24
#define DEFAULT_PROP_GAIN_MIN 0
#define DEFAULT_PROP_GAIN_MAX 52
#define DEFAULT_PROP_RAMP_RATE RAMP_RATE_MEDIUM
#define DEFAULT_PROP_LOG_FILE ""
#define DEFAULT_PROP_CAMERA_ELEMENT ""
/* GStreamer boilerplate */
#define gst_intervalometer_parent_class parent_class
G_DEFINE_TYPE (GstIntervalometer, gst_intervalometer, GST_TYPE_BASE_TRANSFORM);
static GstStaticPadTemplate gst_intervalometer_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE ("{ GRAY8, GRAY16_LE, GRAY16_BE, BGRA, RGB, BGR }"))
);
static GstStaticPadTemplate gst_intervalometer_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE ("{ GRAY8, GRAY16_LE, GRAY16_BE, BGRA, RGB, BGR }"))
);
/* GObject vmethod declarations */
static void gst_intervalometer_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_intervalometer_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static void gst_intervalometer_dispose (GObject * object);
static void gst_intervalometer_finalize (GObject * object);
/* GstBaseTransform vmethod declarations */
static gboolean gst_intervalometer_start (GstBaseTransform * trans);
static gboolean gst_intervalometer_stop (GstBaseTransform * trans);
static gboolean gst_intervalometer_set_caps (GstBaseTransform * trans,
GstCaps * incaps, GstCaps * outcaps);
static GstFlowReturn gst_intervalometer_transform_ip (GstBaseTransform * trans,
GstBuffer * buf);
/* Helper functions */
static void gst_intervalometer_reset (GstIntervalometer * filter);
static gdouble gst_intervalometer_calculate_brightness (GstIntervalometer * filter,
GstBuffer * buf);
static void gst_intervalometer_update_camera_settings (GstIntervalometer * filter,
gdouble brightness);
static void gst_intervalometer_write_log (GstIntervalometer * filter,
gdouble brightness);
static GstElement * gst_intervalometer_find_camera_element (GstIntervalometer * filter);
/* Ramp rate multipliers (based on YASS algorithm) */
static const gdouble ramp_rate_multipliers[] = {
0.5, /* VSLOW */
1.0, /* SLOW */
2.0, /* MEDIUM */
4.0, /* FAST */
8.0 /* VFAST */
};
static void
gst_intervalometer_class_init (GstIntervalometerClass * 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 (gst_intervalometer_debug, "intervalometer", 0,
"Automatic Exposure Controller");
/* Register GObject vmethods */
gobject_class->set_property = gst_intervalometer_set_property;
gobject_class->get_property = gst_intervalometer_get_property;
gobject_class->dispose = gst_intervalometer_dispose;
gobject_class->finalize = gst_intervalometer_finalize;
/* Install GObject properties */
g_object_class_install_property (gobject_class, PROP_ENABLED,
g_param_spec_boolean ("enabled", "Enabled",
"Enable automatic exposure control", DEFAULT_PROP_ENABLED,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_TARGET_BRIGHTNESS,
g_param_spec_double ("target-brightness", "Target Brightness",
"Target average brightness (0-255)", 0.0, 255.0,
DEFAULT_PROP_TARGET_BRIGHTNESS,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_COMPENSATION,
g_param_spec_double ("compensation", "Exposure Compensation",
"Exposure compensation in stops", -4.0, 4.0,
DEFAULT_PROP_COMPENSATION,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_EXPOSURE_MIN,
g_param_spec_double ("exposure-min", "Minimum Exposure",
"Minimum exposure time in milliseconds", 0.01, 1000.0,
DEFAULT_PROP_EXPOSURE_MIN,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_EXPOSURE_MAX,
g_param_spec_double ("exposure-max", "Maximum Exposure",
"Maximum exposure time in milliseconds", 0.01, 1000.0,
DEFAULT_PROP_EXPOSURE_MAX,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_GAIN_MIN,
g_param_spec_int ("gain-min", "Minimum Gain",
"Minimum gain value", 0, 100, DEFAULT_PROP_GAIN_MIN,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_GAIN_MAX,
g_param_spec_int ("gain-max", "Maximum Gain",
"Maximum gain value", 0, 100, DEFAULT_PROP_GAIN_MAX,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_RAMP_RATE,
g_param_spec_enum ("ramp-rate", "Ramp Rate",
"Speed of exposure/gain ramping", GST_TYPE_INTERVALOMETER,
DEFAULT_PROP_RAMP_RATE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_LOG_FILE,
g_param_spec_string ("log-file", "Log File",
"Path to CSV log file (empty to disable logging)",
DEFAULT_PROP_LOG_FILE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_CAMERA_ELEMENT,
g_param_spec_string ("camera-element", "Camera Element Name",
"Name of the upstream camera element to control",
DEFAULT_PROP_CAMERA_ELEMENT,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/* Set element metadata */
gst_element_class_add_pad_template (gstelement_class,
gst_static_pad_template_get (&gst_intervalometer_sink_template));
gst_element_class_add_pad_template (gstelement_class,
gst_static_pad_template_get (&gst_intervalometer_src_template));
gst_element_class_set_static_metadata (gstelement_class,
"Intervalometer Auto-Exposure", "Filter/Effect/Video",
"Automatic exposure control for IDS uEye cameras (inspired by YASS)",
"FIXME <fixme@example.com>");
/* Register GstBaseTransform vmethods */
gstbasetransform_class->start = GST_DEBUG_FUNCPTR (gst_intervalometer_start);
gstbasetransform_class->stop = GST_DEBUG_FUNCPTR (gst_intervalometer_stop);
gstbasetransform_class->set_caps =
GST_DEBUG_FUNCPTR (gst_intervalometer_set_caps);
gstbasetransform_class->transform_ip =
GST_DEBUG_FUNCPTR (gst_intervalometer_transform_ip);
}
static void
gst_intervalometer_init (GstIntervalometer * filter)
{
/* Initialize properties */
filter->enabled = DEFAULT_PROP_ENABLED;
filter->target_brightness = DEFAULT_PROP_TARGET_BRIGHTNESS;
filter->compensation = DEFAULT_PROP_COMPENSATION;
filter->exposure_min = DEFAULT_PROP_EXPOSURE_MIN;
filter->exposure_max = DEFAULT_PROP_EXPOSURE_MAX;
filter->gain_min = DEFAULT_PROP_GAIN_MIN;
filter->gain_max = DEFAULT_PROP_GAIN_MAX;
filter->ramp_rate = DEFAULT_PROP_RAMP_RATE;
filter->log_file = g_strdup (DEFAULT_PROP_LOG_FILE);
filter->camera_element_name = g_strdup (DEFAULT_PROP_CAMERA_ELEMENT);
/* Initialize internal state */
filter->camera_src = NULL;
filter->current_exposure = DEFAULT_PROP_EXPOSURE_MIN;
filter->current_gain = DEFAULT_PROP_GAIN_MIN;
filter->target_exposure = DEFAULT_PROP_EXPOSURE_MIN;
filter->target_gain = DEFAULT_PROP_GAIN_MIN;
filter->frame_count = 0;
filter->start_time = GST_CLOCK_TIME_NONE;
filter->log_fp = NULL;
filter->log_header_written = FALSE;
filter->video_info_valid = FALSE;
filter->ramp_step = 1.0;
/* Set in-place transform */
gst_base_transform_set_in_place (GST_BASE_TRANSFORM (filter), TRUE);
gst_base_transform_set_passthrough (GST_BASE_TRANSFORM (filter), TRUE);
}
static void
gst_intervalometer_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstIntervalometer *filter = GST_INTERVALOMETER (object);
switch (prop_id) {
case PROP_ENABLED:
filter->enabled = g_value_get_boolean (value);
break;
case PROP_TARGET_BRIGHTNESS:
filter->target_brightness = g_value_get_double (value);
break;
case PROP_COMPENSATION:
filter->compensation = g_value_get_double (value);
break;
case PROP_EXPOSURE_MIN:
filter->exposure_min = g_value_get_double (value);
break;
case PROP_EXPOSURE_MAX:
filter->exposure_max = g_value_get_double (value);
break;
case PROP_GAIN_MIN:
filter->gain_min = g_value_get_int (value);
break;
case PROP_GAIN_MAX:
filter->gain_max = g_value_get_int (value);
break;
case PROP_RAMP_RATE:
filter->ramp_rate = g_value_get_enum (value);
break;
case PROP_LOG_FILE:
g_free (filter->log_file);
filter->log_file = g_value_dup_string (value);
break;
case PROP_CAMERA_ELEMENT:
g_free (filter->camera_element_name);
filter->camera_element_name = g_value_dup_string (value);
/* Try to find camera element when name is set */
if (filter->camera_element_name && strlen (filter->camera_element_name) > 0) {
filter->camera_src = gst_intervalometer_find_camera_element (filter);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_intervalometer_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstIntervalometer *filter = GST_INTERVALOMETER (object);
switch (prop_id) {
case PROP_ENABLED:
g_value_set_boolean (value, filter->enabled);
break;
case PROP_TARGET_BRIGHTNESS:
g_value_set_double (value, filter->target_brightness);
break;
case PROP_COMPENSATION:
g_value_set_double (value, filter->compensation);
break;
case PROP_EXPOSURE_MIN:
g_value_set_double (value, filter->exposure_min);
break;
case PROP_EXPOSURE_MAX:
g_value_set_double (value, filter->exposure_max);
break;
case PROP_GAIN_MIN:
g_value_set_int (value, filter->gain_min);
break;
case PROP_GAIN_MAX:
g_value_set_int (value, filter->gain_max);
break;
case PROP_RAMP_RATE:
g_value_set_enum (value, filter->ramp_rate);
break;
case PROP_LOG_FILE:
g_value_set_string (value, filter->log_file);
break;
case PROP_CAMERA_ELEMENT:
g_value_set_string (value, filter->camera_element_name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_intervalometer_dispose (GObject * object)
{
GstIntervalometer *filter = GST_INTERVALOMETER (object);
if (filter->camera_src) {
gst_object_unref (filter->camera_src);
filter->camera_src = NULL;
}
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_intervalometer_finalize (GObject * object)
{
GstIntervalometer *filter = GST_INTERVALOMETER (object);
g_free (filter->log_file);
g_free (filter->camera_element_name);
if (filter->log_fp) {
fclose (filter->log_fp);
filter->log_fp = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static gboolean
gst_intervalometer_start (GstBaseTransform * trans)
{
GstIntervalometer *filter = GST_INTERVALOMETER (trans);
GST_DEBUG_OBJECT (filter, "start");
filter->frame_count = 0;
filter->start_time = gst_clock_get_time (gst_system_clock_obtain ());
/* Open log file if specified */
if (filter->log_file && strlen (filter->log_file) > 0) {
filter->log_fp = fopen (filter->log_file, "w");
if (!filter->log_fp) {
GST_WARNING_OBJECT (filter, "Failed to open log file: %s", filter->log_file);
} else {
filter->log_header_written = FALSE;
}
}
/* Find camera element if name was provided */
if (filter->camera_element_name && strlen (filter->camera_element_name) > 0) {
filter->camera_src = gst_intervalometer_find_camera_element (filter);
if (!filter->camera_src) {
GST_WARNING_OBJECT (filter, "Could not find camera element: %s",
filter->camera_element_name);
}
}
gst_intervalometer_reset (filter);
return TRUE;
}
static gboolean
gst_intervalometer_stop (GstBaseTransform * trans)
{
GstIntervalometer *filter = GST_INTERVALOMETER (trans);
GST_DEBUG_OBJECT (filter, "stop");
if (filter->log_fp) {
fclose (filter->log_fp);
filter->log_fp = NULL;
}
if (filter->camera_src) {
gst_object_unref (filter->camera_src);
filter->camera_src = NULL;
}
return TRUE;
}
static gboolean
gst_intervalometer_set_caps (GstBaseTransform * trans, GstCaps * incaps,
GstCaps * outcaps)
{
GstIntervalometer *filter = GST_INTERVALOMETER (trans);
if (!gst_video_info_from_caps (&filter->video_info, incaps)) {
GST_ERROR_OBJECT (filter, "Failed to parse caps");
return FALSE;
}
filter->video_info_valid = TRUE;
GST_DEBUG_OBJECT (filter, "Set caps: %" GST_PTR_FORMAT, incaps);
return TRUE;
}
static GstFlowReturn
gst_intervalometer_transform_ip (GstBaseTransform * trans, GstBuffer * buf)
{
GstIntervalometer *filter = GST_INTERVALOMETER (trans);
gdouble brightness;
if (!filter->enabled || !filter->video_info_valid) {
return GST_FLOW_OK;
}
/* Calculate brightness from current frame */
brightness = gst_intervalometer_calculate_brightness (filter, buf);
/* Update camera settings based on brightness */
gst_intervalometer_update_camera_settings (filter, brightness);
/* Write to log file if enabled */
if (filter->log_fp) {
gst_intervalometer_write_log (filter, brightness);
}
filter->frame_count++;
return GST_FLOW_OK;
}
static void
gst_intervalometer_reset (GstIntervalometer * filter)
{
filter->current_exposure = filter->exposure_min;
filter->current_gain = filter->gain_min;
filter->target_exposure = filter->exposure_min;
filter->target_gain = filter->gain_min;
filter->frame_count = 0;
}
static gdouble
gst_intervalometer_calculate_brightness (GstIntervalometer * filter,
GstBuffer * buf)
{
GstMapInfo map;
gdouble sum = 0.0;
guint64 pixel_count = 0;
guint i;
if (!gst_buffer_map (buf, &map, GST_MAP_READ)) {
GST_WARNING_OBJECT (filter, "Failed to map buffer");
return filter->target_brightness;
}
/* Calculate average brightness based on format */
switch (GST_VIDEO_INFO_FORMAT (&filter->video_info)) {
case GST_VIDEO_FORMAT_GRAY8:
{
guint8 *data = map.data;
pixel_count = map.size;
for (i = 0; i < pixel_count; i++) {
sum += data[i];
}
break;
}
case GST_VIDEO_FORMAT_GRAY16_LE:
case GST_VIDEO_FORMAT_GRAY16_BE:
{
guint16 *data = (guint16 *) map.data;
pixel_count = map.size / 2;
for (i = 0; i < pixel_count; i++) {
sum += data[i] / 256.0; /* Scale 16-bit to 8-bit range */
}
break;
}
case GST_VIDEO_FORMAT_RGB:
case GST_VIDEO_FORMAT_BGR:
{
/* Calculate luminance: Y = 0.299*R + 0.587*G + 0.114*B */
guint8 *data = map.data;
pixel_count = map.size / 3;
for (i = 0; i < map.size; i += 3) {
sum += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
}
break;
}
case GST_VIDEO_FORMAT_BGRA:
{
guint8 *data = map.data;
pixel_count = map.size / 4;
for (i = 0; i < map.size; i += 4) {
sum += 0.299 * data[i + 2] + 0.587 * data[i + 1] + 0.114 * data[i];
}
break;
}
default:
GST_WARNING_OBJECT (filter, "Unsupported video format");
gst_buffer_unmap (buf, &map);
return filter->target_brightness;
}
gst_buffer_unmap (buf, &map);
if (pixel_count == 0) {
return filter->target_brightness;
}
return sum / pixel_count;
}
static void
gst_intervalometer_update_camera_settings (GstIntervalometer * filter,
gdouble brightness)
{
gdouble error, adjusted_target, exposure_range, gain_range;
gdouble ramp_multiplier;
if (!filter->camera_src) {
return;
}
/* Calculate brightness error with compensation */
error = (filter->target_brightness - brightness) *
pow (2.0, filter->compensation);
/* Adjust target brightness based on error */
adjusted_target = filter->target_brightness + error;
/* Get ramp multiplier */
ramp_multiplier = ramp_rate_multipliers[filter->ramp_rate];
/* Calculate exposure and gain ranges */
exposure_range = filter->exposure_max - filter->exposure_min;
gain_range = filter->gain_max - filter->gain_min;
/* Determine target exposure and gain using YASS-like algorithm */
if (brightness > adjusted_target) {
/* Too bright - decrease exposure first, then gain */
if (filter->current_exposure > filter->exposure_min) {
filter->target_exposure = filter->current_exposure -
(exposure_range * 0.01 * ramp_multiplier);
filter->target_exposure = MAX (filter->target_exposure, filter->exposure_min);
} else if (filter->current_gain > filter->gain_min) {
filter->target_gain = filter->current_gain - (gint) (ramp_multiplier);
filter->target_gain = MAX (filter->target_gain, filter->gain_min);
}
} else {
/* Too dark - increase exposure first up to max, then increase gain */
if (filter->current_exposure < filter->exposure_max) {
filter->target_exposure = filter->current_exposure +
(exposure_range * 0.01 * ramp_multiplier);
filter->target_exposure = MIN (filter->target_exposure, filter->exposure_max);
} else if (filter->current_gain < filter->gain_max) {
filter->target_gain = filter->current_gain + (gint) (ramp_multiplier);
filter->target_gain = MIN (filter->target_gain, filter->gain_max);
}
}
/* Smooth ramping towards target */
if (fabs (filter->current_exposure - filter->target_exposure) > 0.001) {
filter->current_exposure = filter->target_exposure;
g_object_set (filter->camera_src, "exposure", filter->current_exposure, NULL);
GST_DEBUG_OBJECT (filter, "Set exposure to %.3f ms", filter->current_exposure);
}
if (filter->current_gain != filter->target_gain) {
filter->current_gain = filter->target_gain;
g_object_set (filter->camera_src, "gain", filter->current_gain, NULL);
GST_DEBUG_OBJECT (filter, "Set gain to %d", filter->current_gain);
}
}
static void
gst_intervalometer_write_log (GstIntervalometer * filter, gdouble brightness)
{
if (!filter->log_fp) {
return;
}
/* Write CSV header on first frame */
if (!filter->log_header_written) {
fprintf (filter->log_fp,
"Frame,Time_s,Brightness,Exposure_ms,Gain,Target_Brightness\n");
filter->log_header_written = TRUE;
}
/* Calculate elapsed time */
GstClockTime now = gst_clock_get_time (gst_system_clock_obtain ());
gdouble elapsed = (now - filter->start_time) / 1000000000.0;
/* Write data row */
fprintf (filter->log_fp, "%llu,%.3f,%.2f,%.3f,%d,%.2f\n",
(unsigned long long) filter->frame_count,
elapsed,
brightness,
filter->current_exposure,
filter->current_gain,
filter->target_brightness);
fflush (filter->log_fp);
}
static GstElement *
gst_intervalometer_find_camera_element (GstIntervalometer * filter)
{
GstElement *pipeline, *element = NULL;
GstBin *bin;
/* Get pipeline */
pipeline = GST_ELEMENT (gst_element_get_parent (GST_ELEMENT (filter)));
if (!pipeline) {
GST_WARNING_OBJECT (filter, "Not in a pipeline");
return NULL;
}
/* Search for element by name */
if (GST_IS_BIN (pipeline)) {
bin = GST_BIN (pipeline);
element = gst_bin_get_by_name (bin, filter->camera_element_name);
}
gst_object_unref (pipeline);
if (element) {
GST_INFO_OBJECT (filter, "Found camera element: %s",
filter->camera_element_name);
} else {
GST_WARNING_OBJECT (filter, "Could not find camera element: %s",
filter->camera_element_name);
}
return element;
}
static gboolean
plugin_init (GstPlugin * plugin)
{
return gst_element_register (plugin, "intervalometer", GST_RANK_NONE,
GST_TYPE_INTERVALOMETER);
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
GST_VERSION_MINOR,
intervalometer,
"Automatic exposure control for IDS uEye cameras",
plugin_init, GST_PACKAGE_VERSION, GST_PACKAGE_LICENSE, GST_PACKAGE_NAME,
GST_PACKAGE_ORIGIN)

View File

@@ -0,0 +1,104 @@
/* GStreamer
* Copyright (C) 2024 FIXME <fixme@example.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_INTERVALOMETER_H__
#define __GST_INTERVALOMETER_H__
#include <stdio.h>
#include <gst/base/gstbasetransform.h>
#include <gst/video/video.h>
G_BEGIN_DECLS
#define GST_TYPE_INTERVALOMETER \
(gst_intervalometer_get_type())
#define GST_INTERVALOMETER(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_INTERVALOMETER,GstIntervalometer))
#define GST_INTERVALOMETER_CLASS(klass) \
(G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_INTERVALOMETER,GstIntervalometerClass))
#define GST_IS_INTERVALOMETER(obj) \
(G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_INTERVALOMETER))
#define GST_IS_INTERVALOMETER_CLASS(klass) \
(G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_INTERVALOMETER))
typedef struct _GstIntervalometer GstIntervalometer;
typedef struct _GstIntervalometerClass GstIntervalometerClass;
typedef enum {
RAMP_RATE_VSLOW = 0,
RAMP_RATE_SLOW,
RAMP_RATE_MEDIUM,
RAMP_RATE_FAST,
RAMP_RATE_VFAST
} GstIntervalometerRampRate;
/**
* GstIntervalometer:
* @element: the parent element.
*
* Auto-exposure controller for IDS uEye cameras.
* Inspired by YASS (Yet Another Sunset Script) for CHDK cameras.
*/
struct _GstIntervalometer
{
GstBaseTransform element;
/* Properties */
gboolean enabled;
gdouble target_brightness; /* Target average brightness (0-255) */
gdouble compensation; /* Exposure compensation in stops */
gdouble exposure_min; /* Minimum exposure in ms */
gdouble exposure_max; /* Maximum exposure in ms */
gint gain_min; /* Minimum gain (0-100) */
gint gain_max; /* Maximum gain (0-100) */
GstIntervalometerRampRate ramp_rate;
gchar *log_file; /* CSV log file path */
gchar *camera_element_name; /* Name of upstream idsueyesrc element */
/* Internal state */
GstElement *camera_src; /* Reference to upstream camera element */
gdouble current_exposure; /* Current exposure setting */
gint current_gain; /* Current gain setting */
gdouble target_exposure; /* Target exposure for ramping */
gint target_gain; /* Target gain for ramping */
guint64 frame_count; /* Number of frames processed */
GstClockTime start_time; /* Time when processing started */
FILE *log_fp; /* Log file handle */
gboolean log_header_written; /* Whether CSV header has been written */
/* Video info */
GstVideoInfo video_info;
gboolean video_info_valid;
/* Ramping parameters */
gdouble ramp_step; /* Current ramping step size */
};
struct _GstIntervalometerClass
{
GstBaseTransformClass parent_class;
};
GType gst_intervalometer_get_type(void);
G_END_DECLS
#endif /* __GST_INTERVALOMETER_H__ */