diff --git a/README.md b/README.md deleted file mode 100644 index c213a516..00000000 --- a/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# flir_camera_driver - -This repository contains ROS packages for machine vision cameras made by Teledyne/FLIR (formerly known as PointGrey). - -## Packages - -### spinnaker_camera_driver -A camera driver supporting USB3 and GIGE cameras and has been -successfully used for Blackfly, Blackfly S, Chameleon, and Grasshopper -cameras, but should work with any FLIR camera that supports the -Spinnaker SDK. See the -[spinnaker_camera_driver](spinnaker_camera_driver/README.md) for more. -This software is issued under the Apache License Version 2.0 and BSD.\ -Build status: -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary&subject=Humble)](https://build.ros2.org/job/Hbin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary/) -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary&subject=Iron)](https://build.ros2.org/job/Ibin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary/) - -### spinnaker_synchronized_camera_driver -Based on the spinnaker\_camera\_driver package, this driver is specifically designed for cameras hardware triggered by an external signal. Images triggered by the same external pulse will have identical ROS header time stamps. See the [spinnaker_synchronized_camera_driver](spinnaker_synchronized_camera_driver/README.md) for more.\ -Build status: -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary&subject=Humble)](https://build.ros2.org/job/Hbin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary/) -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary&subject=Iron)](https://build.ros2.org/job/Ibin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary/) - -### flir_camera_description -Package with [meshes and urdf](flir_camera_description/README.md) files. -This software is released under a BSD license.\ -Build status: -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary&subject=Humble)](https://build.ros2.org/job/Hbin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary/) -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary&subject=Iron)](https://build.ros2.org/job/Ibin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary/) - -### flir_camera_msgs -Package with with [image exposure and control messages](flir_camera_msgs/README.md). -These are used by the [spinnaker_camera_driver](spinnaker_camera_driver/README.md). -This software is issued under the Apache License Version 2.0.\ -Build status: -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary&subject=Humble)](https://build.ros2.org/job/Hbin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary/) -[![Build Status](https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary&subject=Iron)](https://build.ros2.org/job/Ibin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary/) - diff --git a/README.rst b/README.rst new file mode 120000 index 00000000..176d9c26 --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +doc/index.rst \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 00000000..24f848e2 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,52 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'FLIR camera drivers' +copyright = '2024, Bernd Pfrommer' +author = 'Bernd Pfrommer' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/doc/flir.png b/doc/flir.png new file mode 100644 index 00000000..9bd8af2f Binary files /dev/null and b/doc/flir.png differ diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..0cb47559 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,88 @@ +================================ +ROS Teledyne FLIR camera drivers +================================ + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. image:: doc/flir.png + :target: https://www.flir.com/browse/industrial/machine-vision-cameras/ + +This repository contains ROS2 packages for machine vision cameras made by +Teledyne FLIR (formerly known as PointGrey). Note: this software is *not +supported* by Teleydyne FLIR. + +Packages +======== + +Spinnaker camera driver +----------------------- + +| A camera driver supporting USB3 and GIGE cameras that has been + successfully used for Blackfly, Blackfly S, Chameleon, and Grasshopper + cameras. It should work with any FLIR camera that supports the + Spinnaker SDK. See the + `spinnaker_camera_driver `__ for + more. + +|driver_humble| |driver_iron| |driver_rolling| + +.. |driver_humble| image:: https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary&subject=Humble + :target: https://build.ros2.org/job/Hbin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary/ +.. |driver_iron| image:: https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary&subject=Iron + :target: https://build.ros2.org/job/Ibin_uJ64__spinnaker_camera_driver__ubuntu_jammy_amd64__binary/ +.. |driver_rolling| image:: https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__spinnaker_camera_driver__ubuntu_noble_amd64__binary&subject=Rolling + :target: https://build.ros2.org/job/Rbin_uN64__spinnaker_camera_driver__ubuntu_noble_amd64__binary/ + +Spinnaker synchronized camera driver +------------------------------------ + +| Based on the spinnaker_camera_driver package, this driver is + specifically designed for cameras that are hardware triggered by an external + pulse. Images triggered by the same external pulse will have identical ROS header time stamps. See the + `spinnaker_synchronized_camera_driver `__ + for more. + +|sync_humble| |sync_iron| |sync_rolling| + +.. |sync_humble| image:: https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary&subject=Humble + :target: https://build.ros2.org/job/Hbin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary/ +.. |sync_iron| image:: https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary&subject=Iron + :target: https://build.ros2.org/job/Ibin_uJ64__spinnaker_synchronized_camera_driver__ubuntu_jammy_amd64__binary/ +.. |sync_rolling| image:: https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__spinnaker_synchronized_camera_driver__ubuntu_noble_amd64__binary&subject=Rolling + :target: https://build.ros2.org/job/Rbin_uN64__spinnaker_synchronized_camera_driver__ubuntu_noble_amd64__binary/ + +FLIR camera description +----------------------- + +| Package with `meshes and urdf `__ + files. + +|desc_humble| |desc_iron| |desc_rolling| + +.. |desc_humble| image:: https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary&subject=Humble + :target: https://build.ros2.org/job/Hbin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary/ +.. |desc_iron| image:: https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary&subject=Iron + :target: https://build.ros2.org/job/Ibin_uJ64__flir_camera_description__ubuntu_jammy_amd64__binary/ +.. |desc_rolling| image:: https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__flir_camera_description__ubuntu_noble_amd64__binary&subject=Rolling + :target: https://build.ros2.org/job/Rbin_uN64__flir_camera_description__ubuntu_noble_amd64__binary/ + + +FLIR camera messages +-------------------- + +| Package with with `image exposure and control + messages `__. These are used by the + `spinnaker_camera_driver `__. + + +|msg_humble| |msg_iron| |msg_rolling| + +.. |msg_humble| image:: https://build.ros2.org/buildStatus/icon?job=Hbin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary&subject=Humble + :target: https://build.ros2.org/job/Hbin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary/ +.. |msg_iron| image:: https://build.ros2.org/buildStatus/icon?job=Ibin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary&subject=Iron + :target: https://build.ros2.org/job/Ibin_uJ64__flir_camera_msgs__ubuntu_jammy_amd64__binary/ +.. |msg_rolling| image:: https://build.ros2.org/buildStatus/icon?job=Rbin_uN64__flir_camera_msgs__ubuntu_noble_amd64__binary&subject=Rolling + :target: https://build.ros2.org/job/Rbin_uN64__flir_camera_msgs__ubuntu_noble_amd64__binary/ diff --git a/spinnaker_camera_driver/README.md b/spinnaker_camera_driver/README.md deleted file mode 100644 index 09d23594..00000000 --- a/spinnaker_camera_driver/README.md +++ /dev/null @@ -1,269 +0,0 @@ -# spinnaker_camera_driver: ROS driver for FLIR cameras based on the Spinnaker SDK - -ROS driver for the FLIR cameras using the -[Spinnaker SDK](http://softwareservices.flir.com/Spinnaker/latest/index.htmlspinnaker). - -NOTE: This driver is not written or supported by FLIR. - -## Tested cameras: - -The following cameras have been used with this driver: - -- Blackfly S (USB3, GigE) -- Blackfly (GigE) -- Grashopper (USB3) -- Chameleon (USB3, tested on firmware v1.13.3.00) - -Note: if you get other cameras to work, *please report back*, ideally -submit a pull request with the camera config file you have created. - -## Tested platforms - -Software: - -- ROS2 Galactic under Ubuntu 20.04 LTS -- ROS2 Humble under Ubuntu 22.04 LTS -- Spinnaker 3.1.0.79 (other versions may work as well but this is - what the continuous integration builds are using) - -## How to install - -This driver can be used with or without installing the Spinnaker SDK, -but installing the Spinnaker SDK is recommended because during its -installation the USB kernel configuration is modified as needed and -suitable access permissions are granted (udev rules). -If you choose to *not* use the Spinnaker SDK, you must either run the -[linux setup script](scripts/linux_setup_flir) by running `ros2 run spinnaker_camera_driver linux_setup_flir` -or perform the [required setup steps manually](docs/linux_setup_flir.md). -Without these setup steps, -*the ROS driver will not detect the camera*. -So you must either install the Spinnaker SDK (which also gives you the -useful ``spinview`` tool), or follow the manual setup steps mentioned earlier. - -### Installing from packages -For some architectures and ros distributions you can simply install an apt package: -``` -sudo apt install ros-${ROS_DISTRO}-spinnaker-camera-driver -``` -The package will bring its own set of Spinnaker SDK libraries, so you don't -necessarily have to install the SDK, but it's recommended, see above - -### Building from source - -1) Install the FLIR spinnaker driver. If you skip this part, the -driver will attempt to download the Spinnaker SDK automatically to -obtain the header files and libraries. -2) Prepare the ROS2 driver build: -Make sure you have your ROS2 environment sourced: -``` -source /opt/ros//setup.bash -``` - -Create a workspace (``~/ws``), clone this repo: -``` -mkdir -p ~/ws/src -cd ~/ws/src -git clone --branch humble-devel https://github.com/ros-drivers/flir_camera_driver -cd .. -``` - -To automatically install all packages that the ``flir_camera_driver`` packages -depends upon, run this at the top of your workspace: -``` -rosdep install --from-paths src --ignore-src -``` - -3) Build the driver and source the workspace: -``` -colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -. install/setup.bash -``` - -## Example usage - -### Single node launch -The driver comes with an example launch file (``driver_node.launch.py``) -that you can customize as needed. -``` -# launch with --show-args to print out all available launch arguments -ros2 launch spinnaker_camera_driver driver_node.launch.py camera_type:=blackfly_s serial:="'20435008'" -``` - -### Stereo camera with synchronization - -The launch file ``stereo_synced.launch.py`` provides a working example for launching -drivers for two hardware synchronized Blackfly S cameras. It requires two more packages -to be installed, -[cam_sync_ros2](https://github.com/berndpfrommer/cam_sync_ros2)(for -time stamp syncing) and -[exposure_control_ros2](https://github.com/berndpfrommer/exposure_control_ros2) -(for external exposure control). -The launch file also demonstrates how to use the driver as a composable node. - -## Features - -The ROS driver itself has no notion of the camera type (Blackfly, -Grasshopper etc), nor does it explicitly support any of the many -features that the FLIR cameras have. Rather, all camera features -(called Spinnaker Nodes) are mapped to ROS parameters via a yaml file -that is specific to the camera. On startup the driver reads this -parameter definition file. In the ``config`` directory there are some -parameter definition files for popular cameras (blackfly_s.yaml etc) -that expose some of the more frequently used features like frame rate, -gain, etc. You can add more features by providing your own -parameter definition file. The ROS driver code is just a thin wrapper -around the Spinnaker SDK, and should allow you to access all features available in FLIR's -spinview program. *In addition to the parameters defined in the .yaml -files*, the driver has the following ROS parameters: - -- ``adjust_timestamp``: see below for more documentation -- ``acquisition_timeout``: timeout for expecting frames (in seconds). - If no frame is received for this time, the driver restarts. Default is 3s. -- ``buffer_queue_size``: max number of images to queue internally - before shoving them into the ROS output queue. Decouples the - Spinnaker SDK thread from the ROS publishing thread. Default: 4. -- ``camerainfo_url``: where to find the camera calibration yaml file. -- ``compute_brightness``: if true, compute image brightness and - publish it in meta data message. This is useful for external - exposure control but incurs extra CPU load. Default: false. -- ``connect_while_subscribed``: if true, connect to the SDK and - pull data from the camera *only while subscribers to image or meta - topics are present*. This feature reduces compute load and link - utilization while no ROS subscribers are present, but adds latency - on subscription in that the first image will be published - up to 1s later than without this option. -- ``dump_node_map``: set this to true to get a dump of the node map. This -- ``frame_id``: the ROS frame id to put in the header of the published - image messages. -- ``image_queue_size``: ROS output queue size (number of frames). Default: 4 - feature is helpful when developing a new config file. Default: false. -- ``parameter_file``: location of the .yaml file defining the camera - (blackfly_s.yaml etc) -- ``serial_number``: must have the serial number of the camera. If you - don't know it, put in anything you like and - the driver will croak with an error message, telling you what - cameras serial numbers are available - -## Setting up GigE cameras - -The Spinnaker SDK abstracts away the transport layer so a GigE camera -should work the same way as USB3: you point it to the serial -number and you're set. - -There are a few GigE-specific settings in the Transport Layer Control -group that are important, in particular enabling jumbo frames from the -camera per FLIR's recommendations. The following line in your -camera-specific config file will create a ROS2 parameter -``gev_scps_packet_size``: -``` -gev_scps_packet_size int "TransportLayerControl/GigEVision/GevSCPSPacketSize" -``` -that you can then set in your ROS2 launch file: -``` - "gev_scps_packet_size": 9000 -``` -As far as setting up the camera's IP address: you can set up DHCP on -your network or configure a static persistent IP using spinview -in "Transport Layer Control">"GigE Vision". Check the box for "Current -IP Configuration Persistent IP" first to enable it, then set your -desired addresses under "Persistent IP Address", "Persistent Subnet -Mask" and "Persistent Gateway". NOTE: these look like regular IPs, but -to set them you have to enter the 32-bit integer representation of the -IP address/mask. By hand/calculator: convert the IP octets from -decimal to hex, then combine them and convert to a 32-bit integer, ex: -192.168.0.1 -> 0xC0A80001 -> 3232235521. - -The "Transport Layer Control">"GigE Vision" section of spinview is -also where you'll find that "SCPS Packet Size" setting, which you can -change when not capturing frames, and verify it works in spinview and -without needing to spin up a custom launch file to get started, though -it helps, and you'll probably want one anyway to specify your camera's -serial number. - -For more tips on GigE setup look at FLIR's support pages -[here](https://www.flir.com/support-center/iis/machine-vision/knowledge-base/lost-ethernet-data-packets-on-linux-systems/) -and -[here](https://www.flir.com/support-center/iis/machine-vision/application-note/troubleshooting-image-consistency-errors/). - -### Time stamps - -By default the driver will set the ROS header time stamp to be the -time when the image was delivered by the SDK. Such time stamps are not -very precise and may lag depending on host CPU load. However the -driver has a feature to use the much more accurate sensor-provided -camera time stamps. These are then converted to ROS time stamps by -estimating the offset between ROS and sensor time stamps via a simple -moving average. For the adjustment to work -*the camera must be configured to send time stamps*, and the -``adjust_timestamp`` flag must be set to true, and the relevant field -in the "chunk" must be populated by the camera. For the Blackfly S -the parameters look like this: -``` - 'adjust_timestamp': True, - 'chunk_mode_active': True, - 'chunk_selector_timestamp': 'Timestamp', - 'chunk_enable_timestamp': True, -``` - -When running hardware synchronized cameras in a stereo configuration -two drivers will need to be run, one for each camera. This will mean -however that their published ROS header time stamps are *not* -identical which in turn may prevent down-stream ROS nodes from recognizing the -images as being hardware synchronized. You can use the -[cam_sync_ros2 node](https://github.com/berndpfrommer/cam_sync_ros2) -to force the time stamps to be aligned. In this scenario it is -mandatory to configure the driver to adjust the ROS time stamps as -described above. - -### Automatic exposure - -In most situations it is recommended to enable the built-in auto -exposure of the camera. However, in a -synchronized setting it is sometimes desirable to disable the built-in -auto-exposure and provide it externally. For instance in a stereo setup, -matching left and right image patches can be difficult when each -camera runs its own auto exposure independently. The -[exposure_control_ros2](https://github.com/berndpfrommer/exposure_control_ros2) -package can provide external automatic exposure control. To this end -the driver publishes -[meta data messages](https://github.com/ros-drivers/flir_camera_driver/flir_camera_msgs) and -subscribes to -[camera control -messages](https://github.com/ros-drivers/flir_camera_driver/flir_camera_msgs). See -the launch file directory for examples. - -## How to add new features and develop your own camera configuration file - -[Check out this section](docs/camera_configuration_files.md) for more information on -how to add features. - -## Known issues - -1) If you run multiple drivers in separate nodes that all access USB based -devices, starting a new driver will stop the image acquisition of -currently running drivers. There is an ugly workaround for this -currently implemented: if image delivery stops for more than -``acquisition_timeout`` seconds, the acquisition is restarted. This -operation may not be thread safe so the driver already running could -possibly crash. This issue can be avoided by running all drivers in -the same address space with a composable node (see stereo launch file for -example). - -## How to contribute -Please provide feedback if you cannot get your camera working or if -the code does not compile for you. Feedback is crucial for the -software development process. However, before opening issues on github first -verify that the problem is not present when using spinview. - -Bug fixes and config files for new cameras are greatly -appreciated. Before submitting a pull request, run this to see if your -commit passes some basic lint tests: -``` -colcon test --packages-select spinnaker_camera_driver && colcon test-result --verbose -``` - -## License - -This software is issued under the Apache License Version 2.0. -The file [TargetArch.cmake](cmake/TargetArch.cmake) is released under -a custom license (see file) diff --git a/spinnaker_camera_driver/README.rst b/spinnaker_camera_driver/README.rst new file mode 120000 index 00000000..176d9c26 --- /dev/null +++ b/spinnaker_camera_driver/README.rst @@ -0,0 +1 @@ +doc/index.rst \ No newline at end of file diff --git a/spinnaker_camera_driver/doc/conf.py b/spinnaker_camera_driver/doc/conf.py new file mode 100644 index 00000000..75dcc980 --- /dev/null +++ b/spinnaker_camera_driver/doc/conf.py @@ -0,0 +1,54 @@ +# Copyright 2024 Bernd Pfrommer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +project = 'spinnaker_camera_driver' +# copyright = '2024, Bernd Pfrommer' +author = 'Bernd Pfrommer' + + +# Add any Sphinx extension module names here, as strings. +extensions = [ + 'myst_parser', + 'sphinx.ext.doctest', + 'sphinx_rtd_theme', + 'sphinx.ext.intersphinx', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +source_suffix = '.rst' +# exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +htmlhelp_basename = 'spinnaker_camera_driver_doc' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". + +# html_static_path = ['_static'] diff --git a/spinnaker_camera_driver/doc/index.rst b/spinnaker_camera_driver/doc/index.rst new file mode 100644 index 00000000..6ec95466 --- /dev/null +++ b/spinnaker_camera_driver/doc/index.rst @@ -0,0 +1,397 @@ +============================ +Spinnaker ROS2 Camera Driver +============================ + +.. toctree:: + :maxdepth: 2 + + +This package provides a ROS2 driver for Teledyne/FLIR cameras using the +`Spinnaker +SDK `__. +For hardware-synchronized cameras use the Spinnaker synchronized camera driver by +following the link from the +`flir driver repository `__. + + +NOTE: This driver is not written or supported by FLIR. + +Tested Configurations +===================== +Cameras +------- + +The following cameras have been successfully used with this driver: + +- Blackfly S (USB3, GigE) +- Blackfly (GigE) +- Grashopper (USB3) +- Oryx (reported working) +- Chameleon (USB3, tested on firmware v1.13.3.00) + +Note: if you get other cameras to work, *please report back*, ideally +submit a pull request with the camera config file you have created. + +Platforms +--------- + +- ROS2 Galactic under Ubuntu 20.04 LTS (no longer actively tested) +- ROS2 Humble/Iron/Rolling under Ubuntu 22.04 LTS +- Spinnaker 3.1.0.79 (other versions may work as well but this is what + the continuous integration builds are using)o + +How to install +============== + +This driver can be used with or without installing the Spinnaker SDK, +but installing the Spinnaker SDK is recommended because during its +installation the USB kernel configuration is modified as needed and +suitable access permissions are granted (udev rules). If you choose to +*not* use the Spinnaker SDK, you must either run the `linux setup +script `__ by running +``ros2 run spinnaker_camera_driver linux_setup_flir`` or perform the +required setup steps manually, see `Setting up Linux without Spinnaker SDK`_. +Without these setup steps, *the ROS driver will not detect the camera*. +So you must either install the Spinnaker SDK (which also gives you the useful +``spinview`` tool), or follow the manual setup steps mentioned earlier. + +Installing from packages +------------------------ + +For some architectures and ros distributions you can simply install an +apt package: + +:: + + sudo apt install ros-${ROS_DISTRO}-spinnaker-camera-driver + +The package will bring its own set of Spinnaker SDK libraries, so you +don’t necessarily have to install the SDK, but it’s recommended, see +above. + +Building from source +-------------------- + +1) Install the FLIR spinnaker driver. If you skip this part, the driver + will attempt to download the Spinnaker SDK automatically to obtain + the header files and libraries. +2) Prepare the ROS2 driver build: Make sure you have your ROS2 + environment sourced: + +:: + + source /opt/ros//setup.bash + +Create a workspace (``~/ws``), clone this repo: + +:: + + mkdir -p ~/ws/src + cd ~/ws/src + git clone --branch humble-devel https://github.com/ros-drivers/flir_camera_driver + cd .. + +To automatically install all packages that the ``flir_camera_driver`` +packages depends upon, run this at the top of your workspace: + +:: + + rosdep install --from-paths src --ignore-src + +3) Build the driver and source the workspace: + +:: + + colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + . install/setup.bash + +How to use +========== + +Topics +------ + +Published: + +- ``~/image_raw``: the camera image (image_transport) +- ``~/image_raw/camera_info``: camera calibration +- ``~/meta``: meta data message containing e.g. exposure time. + +Subscribed: + +- ``~/control``: (only when ``enable_external_control`` is set to True) + for external control exposure time and gain. + +Parameters +---------- + +The driver itself has no notion of the camera type (Blackfly, +Grasshopper etc), nor does it explicitly support any features that the FLIR cameras have. +Rather, all camera features (called +Spinnaker Nodes) are mapped to ROS parameters via a yaml file that is +specific to that camera type. On startup the driver reads this parameter +definition file. In the ``config`` directory there are some parameter +definition files for popular cameras (blackfly_s.yaml etc) that expose +some of the more frequently used features like frame rate, gain, etc. +You can add more features by providing your own parameter definition +file, see `How to develop your own camera configuration file`_. +The ROS driver code is just a thin wrapper around the Spinnaker +SDK, and should allow you to access all features available in FLIR’s +spinview program. + +*In addition to the parameters defined in the .yaml +files*, the driver has the following ROS parameters: + +- ``adjust_timestamp``: see `About time stamps`_ below for more documentation. Default: false. +- ``acquisition_timeout``: timeout for expecting frames (in seconds). + If no frame is received for this time, the driver restarts. Default + is 3. +- ``buffer_queue_size``: max number of images to queue internally + before shoving them into the ROS output queue. Decouples the + Spinnaker SDK thread from the ROS publishing thread. Default: 4. +- ``camerainfo_url``: where to find the camera calibration yaml file. +- ``compute_brightness``: if true, compute image brightness and publish + it in meta data message. This is required when running with + the synchronized driver, but incurs extra CPU load. Default: false. +- ``connect_while_subscribed``: if true, connect to the SDK and pull + data from the camera *only while subscribers to image or meta topics + are present*. This feature reduces compute load and link utilization + while no ROS subscribers are present, but adds latency on + subscription: after a subscription the first image will be published up to 1s later + than without this option. +- ``dump_node_map``: set this to true to get a dump of the node map. + Default: false. +- ``enable_external_control``: set this to true to enable external exposure control. + This feature is being deprecated, *do not use*. Default: false. +- ``frame_id``: the ROS frame id to put in the header of the published image messages. +- ``image_queue_size``: ROS output queue size (number of frames). Default: 4 +- ``parameter_file``: location of the .yaml file defining the camera + (blackfly_s.yaml etc) +- ``serial_number``: serial number of the camera. If you + don’t know it, put in anything you like and the driver will croak + with an error message, telling you what cameras serial numbers are + available + +Example usage +------------- +The driver comes with an example launch file (``driver_node.launch.py``) +that you can customize as needed. + +:: + + # launch with --show-args to print out all available launch arguments + ros2 launch spinnaker_camera_driver driver_node.launch.py camera_type:=blackfly_s serial:="'20435008'" + + +About time stamps +================= + +By default the driver will set the ROS header time stamp to be the time +when the image was delivered by the SDK. Such time stamps are not very +precise and may lag depending on host CPU load. However the driver has a +feature to use the much more accurate sensor-provided camera time +stamps. These are then converted to ROS time stamps by estimating the +offset between ROS and sensor time stamps via a simple moving average. +For the adjustment to work *the camera must be configured to send time +stamps*, and the ``adjust_timestamp`` flag must be set to true, and the +relevant field in the “chunk” must be populated by the camera. For the +Blackfly S the parameters look like this: + +:: + + 'adjust_timestamp': True, + 'chunk_mode_active': True, + 'chunk_selector_timestamp': 'Timestamp', + 'chunk_enable_timestamp': True, + + + +Setting up GigE cameras +======================= + +The Spinnaker SDK abstracts away the transport layer so a GigE camera +should work the same way as USB3: you point it to the serial number and +you’re set. + +There are a few GigE-specific settings in the Transport Layer Control +group that are important, in particular enabling jumbo frames from the +camera per FLIR’s recommendations. The following line in your +camera-specific config file will create a ROS2 parameter +``gev_scps_packet_size``: + +:: + + gev_scps_packet_size int "TransportLayerControl/GigEVision/GevSCPSPacketSize" + +that you can then set in your ROS2 launch file: + +:: + + "gev_scps_packet_size": 9000 + +As far as setting up the camera’s IP address: you can set up DHCP on +your network or configure a static persistent IP using spinview in +“Transport Layer Control”>“GigE Vision”. Check the box for “Current IP +Configuration Persistent IP” first to enable it, then set your desired +addresses under “Persistent IP Address”, “Persistent Subnet Mask” and +“Persistent Gateway”. NOTE: these look like regular IPs, but to set them +you have to enter the 32-bit integer representation of the IP +address/mask. By hand/calculator: convert the IP octets from decimal to +hex, then combine them and convert to a 32-bit integer, ex: 192.168.0.1 +-> 0xC0A80001 -> 3232235521. + +The “Transport Layer Control”>“GigE Vision” section of spinview is also +where you’ll find that “SCPS Packet Size” setting, which you can change +when not capturing frames, and verify it works in spinview and without +needing to spin up a custom launch file to get started, though it helps, +and you’ll probably want one anyway to specify your camera’s serial +number. + +For more tips on GigE setup look at FLIR’s support pages +`here `__ +and +`here `__. + + +How to develop your own camera configuration file +================================================= + +The camera configuration file defines the available ROS parameters, and +how they relate to the corresponding `Spinnaker +nodes `__. +The Spinnaker API follows the GenICam standard, where each property +(e.g. exposure mode, gain, …) of the camera is represented by a node. +Many properties are of integer or floating point type, but some are +enumerations (“enum”). Before you modify a configuration file you can +explore the Spinnaker Nodes by using the spinview applications that +comes with the Spinnaker SDK. Once you know what property you want to +expose as a ROS parameter, you add a mapping entry to the yaml +configuration file, e.g.: + +.. code:: + + - name: image_width + type: int + node: ImageFormatControl/Width + +With this entry in place, the ROS driver will now accept an integer +parameter of name ``image_width``, and whenever ``image_width`` changes, +it will apply this change to the Spinnaker Node +``ImageFormatControl/Width``. + +Enumerations (``enum``) parameters are slightly trickier than float and +integers because their values are restricted to a set of strings. Any +other strings will be rejected by the Spinnaker API. Please document the +valid enum strings in the configuration file, e.g.: + +.. code:: + + - name: line1_linemode # valid values: "Input", "Output" + type: enum + node: DigitalIOControl/LineMode + +The hard part is often finding the node name, in the last example +``"DigitalIOControl/LineMode"``. It usually follows by removing spaces +from the ``spinview`` names. If that doesn’t work, launch the driver +with the ``dump_node_map`` parameter set to “True” and look at the +output for inspiration. + +**NOTE: !!!! THE ORDER OF PARAMETER DEFINITION MATTERS !!!!** + +On node startup, the parameters will be declared and initialized in the +order listed in the yaml file. For instance you must list the enum +``exposure_auto`` before the float ``exposure_time`` because on startup, +``exposure_auto`` must first be set to ``Off`` before ``exposure_time`` +can be set, or else the camera refuses to set the exposure time. + +Known issues +============ + +1) If you run multiple drivers in separate nodes that all access USB + based devices, starting a new driver will stop the image acquisition + of currently running drivers. There is an ugly workaround for this + currently implemented: if image delivery stops for more than + ``acquisition_timeout`` seconds, the acquisition is restarted. This + operation may not be thread safe so the driver already running could + possibly crash. This issue can be avoided by running all drivers in + the same address space with a composable node (see stereo launch file + for example). + + +Setting up Linux without Spinnaker SDK +====================================== + +Only use these instructions if you did not install the Spinnaker SDK on +your machine. + +1) Add the “flirimaging” group and make yourself a member of it + +.. code:: + + sudo addgroup flirimaging + sudo usermod -a -G flirimaging ${USER} + +2) Bump the usbfs memory limits + +The following was taken from +`here `__. +Edit the file ``/etc/default/grub`` and change the line default to: + +:: + + GRUB_CMDLINE_LINUX_DEFAULT="quiet splash usbcore.usbfs_memory_mb=1000" + +Then + +:: + + sudo update-grub + +If your system does not have ``/etc/default/grub``, create the file +``/etc/rc.local``, and change its permissions to ‘executable’. Then +write the following text to it: + +:: + + #!/bin/sh -e + sh -c 'echo 1000 > /sys/module/usbcore/parameters/usbfs_memory_mb' + + exit 0 + +3) Setup udev rules + +.. code:: + + echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="1e10", GROUP="flirimaging"' | sudo tee -a /etc/udev/rules.d/40-flir-spinnaker.rules + echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="1724", GROUP="flirimaging"' | sudo tee -a /etc/udev/rules.d/40-flir-spinnaker.rules + sudo service udev restart + sudo udevadm trigger + +4) Logout and log back in (or better, reboot) + +``sudo reboot`` + + +How to contribute +================= + +Please provide feedback if you cannot get your camera working or if the +code does not compile for you. Feedback is crucial for the software +development process. However, before opening issues on github first +verify that the problem is not present when using spinview. + +Bug fixes and config files for new cameras are greatly appreciated. +Before submitting a pull request, run this to see if your commit passes +some basic lint tests: + +:: + + colcon test --packages-select spinnaker_camera_driver && colcon test-result --verbose + + +License +======= + +This software is issued under the Apache License Version 2.0. The file +``cmake/TargetArch.cmake`` is released under a custom license (see file). + diff --git a/spinnaker_camera_driver/docs/camera_configuration_files.md b/spinnaker_camera_driver/docs/camera_configuration_files.md deleted file mode 100644 index 6ca28c1a..00000000 --- a/spinnaker_camera_driver/docs/camera_configuration_files.md +++ /dev/null @@ -1,49 +0,0 @@ -# How to develop your own camera configuration file - -The camera configuration file defines the available ROS parameters, -and how they relate to the corresponding [Spinnaker -nodes](https://www.flir.com/support-center/iis/machine-vision/application-note/spinnaker-nodes/). -The Spinnaker API follows the GenICam standard, where each property -(e.g. exposure mode, gain, ...) of the camera is represented by a -node. Many properties are of integer or floating point type, but some -are enumerations ("enum"). Before you modify a configuration file you -can explore the Spinnaker Nodes by using the spinview applications -that comes with the Spinnaker SDK. Once you know what property you -want to expose as a ROS parameter, you add a mapping entry to the yaml -configuration file, e.g.: -```yaml - - name: image_width - type: int - node: ImageFormatControl/Width -``` - -With this entry in place, the ROS driver will now accept an integer parameter -of name ``image_width``, and whenever -``image_width`` changes, it will apply this change to the Spinnaker -Node ``ImageFormatControl/Width``. - -Enumerations (``enum``) parameters are slightly trickier than float -and integers because their values are restricted to a set of -strings. Any other strings will be rejected by the Spinnaker API. -Please document the valid enum strings in the configuration file, -e.g.: -```yaml - - name: line1_linemode # valid values: "Input", "Output" - type: enum - node: DigitalIOControl/LineMode -``` - -The hard part is often finding the node name, in the last -example ``"DigitalIOControl/LineMode"``. It usually follows by -removing spaces from the ``spinview`` names. If that doesn't work, -launch the driver with the ``dump_node_map`` parameter set to "True" -and look at the output for inspiration. - -**NOTE: !!!! THE ORDER OF PARAMETER DEFINITION MATTERS !!!!** - -On node startup, the parameters will be declared and initialized -in the order listed in the yaml file. For instance you must list -the enum ``exposure_auto`` before the float ``exposure_time`` because on -startup, ``exposure_auto`` must first be set to ``Off`` before -``exposure_time`` can be set, or else the camera refuses to set -the exposure time. diff --git a/spinnaker_camera_driver/docs/linux_setup_flir.md b/spinnaker_camera_driver/docs/linux_setup_flir.md deleted file mode 100644 index 07929e11..00000000 --- a/spinnaker_camera_driver/docs/linux_setup_flir.md +++ /dev/null @@ -1,42 +0,0 @@ -# Manual setup steps - -Only use these instructions if you did not install the Spinnaker SDK on -your machine. - -## Add the "flirimaging" group and make yourself a member of it -```bash -sudo addgroup flirimaging -sudo usermod -a -G flirimaging ${USER} -``` - -## Bump the usbfs memory limits -The following was taken from [here](https://www.flir.com/support-center/iis/machine-vision/application-note/using-linux-with-usb-3.1/). -Edit the file ``/etc/default/grub`` and change the line default to: -``` -GRUB_CMDLINE_LINUX_DEFAULT="quiet splash usbcore.usbfs_memory_mb=1000" -``` -Then -``` -sudo update-grub -``` -If your system does not have ``/etc/default/grub``, create the file ``/etc/rc.local``, and change its permissions to 'executable'. Then write the following text to it: -``` -#!/bin/sh -e -sh -c 'echo 1000 > /sys/module/usbcore/parameters/usbfs_memory_mb' - -exit 0 -``` - -## Setup udev rules -```bash -echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="1e10", GROUP="flirimaging"' | sudo tee -a /etc/udev/rules.d/40-flir-spinnaker.rules -echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="1724", GROUP="flirimaging"' | sudo tee -a /etc/udev/rules.d/40-flir-spinnaker.rules -sudo service udev restart -sudo udevadm trigger -``` - -## Logout and log back in (or better, reboot) - -`` -sudo reboot -`` diff --git a/spinnaker_camera_driver/include/spinnaker_camera_driver/exposure_controller.hpp b/spinnaker_camera_driver/include/spinnaker_camera_driver/exposure_controller.hpp index 6321537f..4aa12702 100644 --- a/spinnaker_camera_driver/include/spinnaker_camera_driver/exposure_controller.hpp +++ b/spinnaker_camera_driver/include/spinnaker_camera_driver/exposure_controller.hpp @@ -30,6 +30,10 @@ class ExposureController virtual ~ExposureController() {} virtual void update(Camera * cam, const std::shared_ptr & img) = 0; virtual void addCamera(const std::shared_ptr & cam) = 0; + virtual double getExposureTime() = 0; + virtual double getGain() = 0; + virtual void link( + const std::unordered_map> & map) = 0; }; } // namespace spinnaker_camera_driver #endif // SPINNAKER_CAMERA_DRIVER__EXPOSURE_CONTROLLER_HPP_ diff --git a/spinnaker_camera_driver/rosdoc2.yaml b/spinnaker_camera_driver/rosdoc2.yaml new file mode 100644 index 00000000..12796451 --- /dev/null +++ b/spinnaker_camera_driver/rosdoc2.yaml @@ -0,0 +1,42 @@ +## This 'attic section' self-documents this file's type and version. +type: 'rosdoc2 config' +version: 1 + +--- + +settings: + ## If this is true, a standard index page is generated in the output directory. + ## It uses the package information from the 'package.xml' to show details + ## about the package, creates a table of contents for the various builders + ## that were run, and may contain links to things like build farm jobs for + ## this package or links to other versions of this package. + ## If this is not specified explicitly, it defaults to 'true'. + generate_package_index: true + + ## Point to python sources relative to package.xml file + # python_source: '' + + ## Don't run doxygen + always_run_doxygen: false + + ## Build python API docs + ## This is most useful if the user would like to generate Python API + ## documentation for a package that is not of the `ament_python` build type. + always_run_sphinx_apidoc: false + + # disable breathe and exhale + enable_breathe: false + enable_exhale: false + + # This setting, if provided, will override the build_type of this package + # for documentation purposes only. If not provided, documentation will be + # generated assuming the build_type in package.xml. + # override_build_type: 'ament_python' + +builders: + - sphinx: { + name: 'spinnaker_camera_driver', + ## This path is relative to output staging. + sphinx_sourcedir: 'doc/', + output_dir: '' + } \ No newline at end of file diff --git a/spinnaker_synchronized_camera_driver/CMakeLists.txt b/spinnaker_synchronized_camera_driver/CMakeLists.txt index 5e6756d1..b63627bc 100644 --- a/spinnaker_synchronized_camera_driver/CMakeLists.txt +++ b/spinnaker_synchronized_camera_driver/CMakeLists.txt @@ -60,7 +60,8 @@ add_library(synchronized_camera_driver SHARED src/time_estimator.cpp src/time_keeper.cpp src/exposure_controller_factory.cpp - src/individual_exposure_controller.cpp) + src/master_exposure_controller.cpp + src/follower_exposure_controller.cpp) ament_target_dependencies(synchronized_camera_driver PUBLIC ${ROS_DEPENDENCIES}) target_link_libraries(synchronized_camera_driver PUBLIC spinnaker_camera_driver::camera_driver PRIVATE yaml-cpp) @@ -111,7 +112,7 @@ if(BUILD_TESTING) find_package(ament_cmake_copyright REQUIRED) find_package(ament_cmake_cppcheck REQUIRED) find_package(ament_cmake_cpplint REQUIRED) - find_package(ament_cmake_black REQUIRED) + find_package(ament_cmake_flake8 REQUIRED) find_package(ament_cmake_lint_cmake REQUIRED) find_package(ament_cmake_pep257 REQUIRED) find_package(ament_cmake_clang_format REQUIRED) @@ -120,7 +121,7 @@ if(BUILD_TESTING) ament_copyright() ament_cppcheck(LANGUAGE c++) ament_cpplint(FILTERS "-build/include,-runtime/indentation_namespace") - ament_black() + ament_flake8() ament_lint_cmake() ament_pep257() ament_clang_format(CONFIG_FILE .clang-format) diff --git a/spinnaker_synchronized_camera_driver/README.md b/spinnaker_synchronized_camera_driver/README.md deleted file mode 100644 index 8e9d7a2c..00000000 --- a/spinnaker_synchronized_camera_driver/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# spinnaker_synchronized_camera_driver: ROS driver for synchronized FLIR cameras based on the Spinnaker SDK - -ROS driver for synchronized FLIR cameras using the -[Spinnaker SDK](http://softwareservices.flir.com/Spinnaker/latest/index.htmlspinnaker). - -NOTE: This driver is not written or supported by FLIR. - -## Tested cameras: - -The following cameras have been used with this driver: - -- Blackfly S (USB3) -- Blackfly (GigE) - -Note: if you get other cameras to work, *please report back*, ideally -submit a pull request with the camera config file you have created. - -## Tested platforms - -Software: - -- ROS2 Humble under Ubuntu 22.04 LTS -- Spinnaker 3.1.0.79 (other versions may work as well but this is - what the continuous integration builds are using) - -## How to install - -It is recommended to first install the Spinnaker SDK from FLIR's web site because it has -tools (SpinView) that are very helpful for finding the correct camera configuration. You will also need to install the [ROS2 spinnaker camera driver](../spinnaker_camera_driver/README.md). It is recommended to first test the single camera drivers before proceeding with the synchronized setup. - -### Installing from packages -For some architectures and ROS2 distributions you can simply install an apt package: -``` -sudo apt install ros-${ROS_DISTRO}-spinnaker-synchronized-camera-driver -``` - -### Building from source - -1) Although not necessary, it is recommended to install the Spinnaker SDK. -2) Prepare the ROS2 driver build: -Make sure you have your ROS2 environment sourced: -``` -source /opt/ros//setup.bash -``` - -Create a workspace (``~/ws``), clone this repo: -``` -mkdir -p ~/ws/src -cd ~/ws/src -git clone --branch humble-devel https://github.com/ros-drivers/flir_camera_driver -cd .. -``` - -To automatically install all packages that the ``flir_camera_driver`` packages -depends upon, run this at the top of your workspace: -``` -rosdep install --from-paths src --ignore-src -``` - -3) Build the driver and source the workspace: -``` -colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -. install/setup.bash -``` - -## Example usage - -### Launch the example stereo node -The driver comes with an example launch file (``synchronized_driver_node.launch.py``) -that must be edited to e.g. adjust for camera type and serial numbers -``` -ros2 launch spinnaker_synchronized_camera_driver synchronized_driver_node.launch.py -``` -Note that the relevant camera parameters must be set here, in particular chunks need to be enabled that have the time stamps, and the cameras synchronization modes need to be set correctly as well. - -## Features - -The synchronized driver has the following parameters: - -- ``cameras``: a list of strings, e.g. ["cam_0", "cam_1"] that gives - the camera names. The driver will instantiate a camera for each name, and - its parameters can then be set in the respective name space, e.g. for a blackfly_s camera you could set the trigger mode via ROS parameter ``cam_0.trigger_mode``. - -The remaining per-camera parameters, *in particular for enabling the chunk mode time stamps* must be set via the launch files in the respective name spaces of each camera. See the launch file for how this is done. - -## Known issues - -See the caveats for the [ROS2 single-camera spinnaker driver](../spinnaker_camera_driver/README.md). - -## How to contribute - -Bug fixes and config files for new cameras are greatly appreciated. Before submitting a pull request, run this to see if your commit passes some basic lint tests: -``` -colcon test --packages-select spinnaker_synchronized_camera_driver && colcon test-result --verbose -``` - -## License - -This software is issued under the Apache License Version 2.0. diff --git a/spinnaker_synchronized_camera_driver/README.rst b/spinnaker_synchronized_camera_driver/README.rst new file mode 120000 index 00000000..176d9c26 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/README.rst @@ -0,0 +1 @@ +doc/index.rst \ No newline at end of file diff --git a/spinnaker_synchronized_camera_driver/doc/conf.py b/spinnaker_synchronized_camera_driver/doc/conf.py new file mode 100644 index 00000000..75dcc980 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/doc/conf.py @@ -0,0 +1,54 @@ +# Copyright 2024 Bernd Pfrommer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +project = 'spinnaker_camera_driver' +# copyright = '2024, Bernd Pfrommer' +author = 'Bernd Pfrommer' + + +# Add any Sphinx extension module names here, as strings. +extensions = [ + 'myst_parser', + 'sphinx.ext.doctest', + 'sphinx_rtd_theme', + 'sphinx.ext.intersphinx', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +source_suffix = '.rst' +# exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +htmlhelp_basename = 'spinnaker_camera_driver_doc' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". + +# html_static_path = ['_static'] diff --git a/spinnaker_synchronized_camera_driver/doc/index.rst b/spinnaker_synchronized_camera_driver/doc/index.rst new file mode 100644 index 00000000..95f29c11 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/doc/index.rst @@ -0,0 +1,156 @@ +========================================= +Spinnaker ROS2 Synchronized Camera Driver +========================================= + +This package provides a driver specifically for hardware synchronized +cameras made by Teledyne/FLIR that work with the `Spinnaker +SDK `__. +The main difference to running several instances of the +`unsynchronized camera driver `__ is +that this driver will assign identical header time stamps to frames that have been +generated by the same synchronization pulse. + +NOTE: This driver is not written or supported by FLIR. + +Tested Configurations +===================== + +Cameras +------- + +The following cameras have been used with this driver: + +- Blackfly S (USB3) +- Blackfly (GigE) + + +Platforms +--------- + +- ROS2 Galactic under Ubuntu 20.04 LTS (no longer actively tested) +- ROS2 Humble/Iron/Rolling under Ubuntu 22.04 LTS +- Spinnaker 3.1.0.79 (other versions may work as well but this is what + the continuous integration builds are using) + +How to install +============== + +The installation is analogous to the one for the unsynchronized +`spinnaker camera driver <../../spinnaker_camera_driver/doc/index.rst>`_. + +How to use +========== + +It is recommended to first get the cameras working with SpinView, then with the single-camera +driver individually before proceeding with the synchronized setup. + +The synchronized camera server works by instantiating a number of individual +camera servers and exposure controllers. Each camera server is fully configurable +with all parameters available as listed in the +`spinnaker camera driver <../../spinnaker_camera_driver/doc/index.rst>`_. Likewise +each exposure controller has its own parameter set. + +There are two types of controllers: master controllers and followers. The master controllers +regulate the brightness of the camera they are controlling, while follower controllers +set the exposure parameters of the camera they are controlling based on the exposure parameters +of the master controller they follow. + +Since there are many parameters involved the setup can be tricky. It is recommended to start from +the ``follower_example.launch.py`` example when using a stereo camera for e.g. VIO, +or the ``master_example.launch.py`` when running cameras with individual exposure control. + +Topics +------ + +Published: + +- ``~//image_raw``: the synchronized camera image +- ``~//camera_info``: the synchronized camera calibration messages +- ``~//meta``: synchronized meta data like exposure time and gain + + +Synchronized server parameters +------------------------------ + +- ``cameras`` (list of strings): names of the cameras. Default: empty list. +- ``exposure_controllers`` (list of strings): names of the exposure controllers. + List must be of same length as the list of camera names. + + +Camera server parameters +------------------------ + +For a list of all parameters, see the `spinnaker camera driver <../../spinnaker_camera_driver/doc/index.rst>`_. +The parameters are exposed under ``.parameter``. + + +Exposure controller parameters +------------------------------ + +- Master exposure controller: + - ``brightness target`` (int): average image brightness to accomplish. Value range is [0..255]. Default: 120. + - ``brightness_tolerance`` (int): how much actual brightness can deviate for ``brightness_target`` before the control parameters + are updated. Default: 5. + - ``exposure_parameter`` (string): Name of the ROS parameter under which exposure time is accessible for the single camera driver. + This must match the ros parameter name associated with the Spinnaker node that controls exposure time, which is + set in the camera ``.yaml`` config file. Default: ``exposure_time``. + - ``gain_parameter`` (string): Name of the ROS parameter controlling camera gain. See camera ``.yaml`` config file. Default: ``gain``. + - ``gain_priority`` (boolean): Gain priority means: If image is too bright, first try reducing the gain, + and only update exposure time if gain is zero. If image is too dark, first try increasing the exposure time, + and only increase gain if maximum exposure time has been reached. Time priority means: if image is too bright, + first try reducing the exposure time, and only when ``min_exposure_time`` has been reached, reduce the gain. If image + is too dark and gain is below ``max_gain``, increase the gain, otherwise increase the exposure time. Default: false (i.e. use Time priority). + - ``max_exposure_time`` (int): Maximum exposure time (in microseconds). Default: 1000. + - ``max_frames_skip`` (int): It sometimes takes a few frames before a change commanded to exposure time or gain will actually + be executed by the camera, and the frame meta data will indicate the change. After commanding a change of exposure + parameters, the exposure controller will wait for at most ``max_frames_skip`` before it gives up and sends a new + command to change exposure parameters (gain or time). Default: 10. + - ``max_gain`` (float): Maximum gain (in db). Default: 10.0 + - ``min_exposure_time`` (int): Minimum exposure time (in microseconds). This is not a hard limit, but has the following + function: if the brightness needs to be reduced and the exposure time would fall below ``min_exposure_time``, then the + gain (if greater than zero) is reduced first. Only if the gain is zero and the image is still too bright will the + exposure time be reduced below ``min_exposure_time``. Default: 1us +- Follower exposure controller: + - ``exposure_parameter`` (string): see Master exposure controller. + - ``gain_parameter`` (string): see Master exposure controller. + - ``master`` (string): the name of the master controller to use + - ``max_frames_skip`` (int): see Master exposure controller. + +Example usage +------------- + +The driver comes with two example launch files that need to be modified for your purposes (update serial numbers etc). +The ``follower_example.launch.py`` can be used as template for stereo cameras, the ``master_example.launch.py`` for situations +where each camera should run their own exposure control. + +:: + + ros2 launch spinnaker_synchronized_camera_driver follower_example.launch.py + +Carefully examine the camera parameters that are set in the launch file, in particular the following ones: + +- ``compute_brightness`` must be true for any camera governed by a master controller. +- ``exposure_auto`` must be off (disable the individual camera controller). +- ``chunk_mode_active`` must be true, and chunk exposure time, gain and frame id must be enabled + + +Known issues +============ + +See the caveats for the `spinnaker camera driver <../../spinnaker_camera_driver/doc/index.rst>`_. + +How to contribute +================= + +Bug fixes and config files for new cameras are greatly appreciated. +Before submitting a pull request, run this to see if your commit passes +some basic lint tests: + +:: + + colcon test --packages-select spinnaker_synchronized_camera_driver && colcon test-result --verbose + +License +======= + +This software is issued under the Apache License Version 2.0. diff --git a/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/follower_exposure_controller.hpp b/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/follower_exposure_controller.hpp new file mode 100644 index 00000000..f05a4903 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/follower_exposure_controller.hpp @@ -0,0 +1,61 @@ +// -*-c++-*-------------------------------------------------------------------- +// Copyright 2024 Bernd Pfrommer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__FOLLOWER_EXPOSURE_CONTROLLER_HPP_ +#define SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__FOLLOWER_EXPOSURE_CONTROLLER_HPP_ + +#include +#include +#include + +namespace spinnaker_synchronized_camera_driver +{ +class FollowerExposureController : public spinnaker_camera_driver::ExposureController +{ +public: + using Camera = spinnaker_camera_driver::Camera; + explicit FollowerExposureController(const std::string & name, rclcpp::Node * n); + void update( + Camera * cam, const std::shared_ptr & img) final; + void addCamera(const std::shared_ptr & cam) final; + double getExposureTime() final { return (currentExposureTime_); }; + double getGain() final { return (currentGain_); } + void link(const std::unordered_map> & map) final; + +private: + rclcpp::Logger get_logger() { return (rclcpp::get_logger(cameraName_)); } + + template + T declare_param(const std::string & n, const T & def) + { + return (node_->declare_parameter(name_ + "." + n, def)); + } + + // ----------------- variables -------------------- + std::string name_; + std::string cameraName_; + rclcpp::Node * node_{0}; + std::string exposureParameterName_; + std::string gainParameterName_; + std::string masterControllerName_; + std::shared_ptr masterController_; + + double currentExposureTime_{0}; + double currentGain_{std::numeric_limits::lowest()}; + int numFramesSkip_{0}; + int maxFramesSkip_{10}; +}; +} // namespace spinnaker_synchronized_camera_driver +#endif // SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__FOLLOWER_EXPOSURE_CONTROLLER_HPP_ diff --git a/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/individual_exposure_controller.hpp b/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/master_exposure_controller.hpp similarity index 72% rename from spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/individual_exposure_controller.hpp rename to spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/master_exposure_controller.hpp index 200a7a3f..dbe0d861 100644 --- a/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/individual_exposure_controller.hpp +++ b/spinnaker_synchronized_camera_driver/include/spinnaker_synchronized_camera_driver/master_exposure_controller.hpp @@ -13,8 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__INDIVIDUAL_EXPOSURE_CONTROLLER_HPP_ -#define SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__INDIVIDUAL_EXPOSURE_CONTROLLER_HPP_ +#ifndef SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__MASTER_EXPOSURE_CONTROLLER_HPP_ +#define SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__MASTER_EXPOSURE_CONTROLLER_HPP_ #include #include @@ -22,14 +22,17 @@ namespace spinnaker_synchronized_camera_driver { -class IndividualExposureController : public spinnaker_camera_driver::ExposureController +class MasterExposureController : public spinnaker_camera_driver::ExposureController { public: - explicit IndividualExposureController(const std::string & name, rclcpp::Node * n); + using Camera = spinnaker_camera_driver::Camera; + explicit MasterExposureController(const std::string & name, rclcpp::Node * n); void update( - spinnaker_camera_driver::Camera * cam, - const std::shared_ptr & img) final; - void addCamera(const std::shared_ptr & cam) final; + Camera * cam, const std::shared_ptr & img) final; + void addCamera(const std::shared_ptr & cam) final; + double getExposureTime() final { return (currentExposureTime_); }; + double getGain() final { return (currentGain_); } + void link(const std::unordered_map> &) final {} private: double calculateGain(double brightRatio) const; @@ -68,4 +71,4 @@ class IndividualExposureController : public spinnaker_camera_driver::ExposureCon bool gainPriority_{false}; }; } // namespace spinnaker_synchronized_camera_driver -#endif // SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__INDIVIDUAL_EXPOSURE_CONTROLLER_HPP_ +#endif // SPINNAKER_SYNCHRONIZED_CAMERA_DRIVER__MASTER_EXPOSURE_CONTROLLER_HPP_ diff --git a/spinnaker_synchronized_camera_driver/launch/follower_example.launch.py b/spinnaker_synchronized_camera_driver/launch/follower_example.launch.py new file mode 100644 index 00000000..688770b1 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/launch/follower_example.launch.py @@ -0,0 +1,151 @@ +# ----------------------------------------------------------------------------- +# Copyright 2024 Bernd Pfrommer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + + +# +# Example file for two Blackfly S cameras that are *externally triggered*, i.e +# you must provide an external hardware synchronization pulse to both cameras! +# +# One of them creates a master controller, the other one a follower. The exposure +# parameters are determined by the master. This is a useful setup for e.g. a +# synchronized stereo camera. +# + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument as LaunchArg +from launch.actions import OpaqueFunction +from launch.substitutions import LaunchConfiguration as LaunchConfig +from launch.substitutions import PathJoinSubstitution as PJoin +from launch_ros.actions import ComposableNodeContainer +from launch_ros.descriptions import ComposableNode +from launch_ros.substitutions import FindPackageShare + +camera_list = { + 'cam0': '20435008', + 'cam1': '20415937', +} + +exposure_controller_parameters = { + 'brightness_target': 120, # from 0..255 + 'brightness_tolerance': 20, # when to update exposure/gain + # watch that max_exposure_time is short enough + # to support the trigger frame rate! + 'max_exposure_time': 15000, # usec + 'min_exposure_time': 5000, # usec + 'max_gain': 29.9, + 'gain_priority': False, +} + +cam_parameters = { + 'debug': False, + 'quiet': True, + 'buffer_queue_size': 1, + 'compute_brightness': True, + 'exposure_auto': 'Off', + 'exposure_time': 10000, # not used under auto exposure + 'trigger_mode': 'On', + 'gain_auto': 'Off', + 'trigger_source': 'Line3', + 'trigger_selector': 'FrameStart', + 'trigger_overlap': 'ReadOut', + 'trigger_activation': 'RisingEdge', + 'balance_white_auto': 'Continuous', + # You must enable chunk mode and chunks: frame_id, exposure_time, and gain + 'chunk_mode_active': True, + 'chunk_selector_frame_id': 'FrameID', + 'chunk_enable_frame_id': True, + 'chunk_selector_exposure_time': 'ExposureTime', + 'chunk_enable_exposure_time': True, + 'chunk_selector_gain': 'Gain', + 'chunk_enable_gain': True, + # The Timestamp is not used at the moment + 'chunk_selector_timestamp': 'Timestamp', + 'chunk_enable_timestamp': True, +} + + +def make_parameters(context): + """Launch synchronized camera driver node.""" + pd = LaunchConfig('camera_parameter_directory') + calib_url = 'file://' + LaunchConfig('calibration_directory').perform(context) + '/' + + exp_ctrl_names = [cam + '.exposure_controller' for cam in camera_list.keys()] + driver_parameters = { + 'cameras': list(camera_list.keys()), + 'exposure_controllers': exp_ctrl_names, + 'ffmpeg_image_transport.encoding': 'hevc_nvenc', # only for ffmpeg image transport + } + # generate identical exposure controller parameters for all cameras + for exp in exp_ctrl_names: + driver_parameters.update( + {exp + '.' + k: v for k, v in exposure_controller_parameters.items()} + ) + # now set cam0 to be master, cam1 to be follower + driver_parameters[exp_ctrl_names[0] + '.type'] = 'master' + driver_parameters[exp_ctrl_names[1] + '.type'] = 'follower' + # tell camera 1 that the master is (camera 0) + driver_parameters[exp_ctrl_names[1] + '.master'] = exp_ctrl_names[0] + + # generate camera parameters + cam_parameters['parameter_file'] = PJoin([pd, 'blackfly_s.yaml']) + for cam, serial in camera_list.items(): + cam_params = {cam + '.' + k: v for k, v in cam_parameters.items()} + cam_params[cam + '.serial_number'] = serial + cam_params[cam + '.camerainfo_url'] = calib_url + serial + '.yaml' + cam_params[cam + '.frame_id'] = cam + driver_parameters.update(cam_params) # insert into main parameter list + # link the camera to its exposure controller. Each camera has its own controller + driver_parameters.update({cam + '.exposure_controller_name': cam + '.exposure_controller'}) + return driver_parameters + + +def launch_setup(context, *args, **kwargs): + container = ComposableNodeContainer( + name='cam_sync_container', + namespace='', + package='rclcpp_components', + executable='component_container', + composable_node_descriptions=[ + ComposableNode( + package='spinnaker_synchronized_camera_driver', + plugin='spinnaker_synchronized_camera_driver::SynchronizedCameraDriver', + name='cam_sync', + parameters=[make_parameters(context)], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + output='screen', + ) # end of container + return [container] + + +def generate_launch_description(): + return LaunchDescription( + [ + LaunchArg( + 'camera_parameter_directory', + default_value=PJoin([FindPackageShare('spinnaker_camera_driver'), 'config']), + description='root directory for camera parameter definitions', + ), + LaunchArg( + 'calibration_directory', + default_value=['camera_calibrations'], + description='root directory for camera calibration files', + ), + OpaqueFunction(function=launch_setup), + ] + ) diff --git a/spinnaker_synchronized_camera_driver/launch/master_example.launch.py b/spinnaker_synchronized_camera_driver/launch/master_example.launch.py new file mode 100644 index 00000000..a2c8cec2 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/launch/master_example.launch.py @@ -0,0 +1,148 @@ +# ----------------------------------------------------------------------------- +# Copyright 2024 Bernd Pfrommer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + + +# +# Example file for two Blackfly S cameras that are *externally triggered*, i.e +# you must provide an external hardware synchronization pulse to both cameras! +# +# Each of them regulates their exposure time individually by instantiating a +# master exposure controller. This setup is useful when cameras are not facing +# the scene, or facing it from very different angles such that different +# lighting conditions must be expected. +# + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument as LaunchArg +from launch.actions import OpaqueFunction +from launch.substitutions import LaunchConfiguration as LaunchConfig +from launch.substitutions import PathJoinSubstitution as PJoin +from launch_ros.actions import ComposableNodeContainer +from launch_ros.descriptions import ComposableNode +from launch_ros.substitutions import FindPackageShare + +camera_list = { + 'cam0': '20435008', + 'cam1': '20415937', +} + +exposure_controller_parameters = { + 'type': 'master', + 'brightness_target': 120, # from 0..255 + 'brightness_tolerance': 20, # when to update exposure/gain + # watch that max_exposure_time is short enough + # to support the trigger frame rate! + 'max_exposure_time': 15000, # usec + 'min_exposure_time': 5000, # usec + 'max_gain': 29.9, + 'gain_priority': False, +} + +cam_parameters = { + 'debug': False, + 'quiet': True, + 'buffer_queue_size': 1, + 'compute_brightness': True, + 'exposure_auto': 'Off', + 'exposure_time': 10000, # not used under auto exposure + 'trigger_mode': 'On', + 'gain_auto': 'Off', + 'trigger_source': 'Line3', + 'trigger_selector': 'FrameStart', + 'trigger_overlap': 'ReadOut', + 'trigger_activation': 'RisingEdge', + 'balance_white_auto': 'Continuous', + # You must enable chunk mode and chunks: frame_id, exposure_time, and gain + 'chunk_mode_active': True, + 'chunk_selector_frame_id': 'FrameID', + 'chunk_enable_frame_id': True, + 'chunk_selector_exposure_time': 'ExposureTime', + 'chunk_enable_exposure_time': True, + 'chunk_selector_gain': 'Gain', + 'chunk_enable_gain': True, + # The Timestamp is not used at the moment + 'chunk_selector_timestamp': 'Timestamp', + 'chunk_enable_timestamp': True, +} + + +def make_parameters(context): + """Launch synchronized camera driver node.""" + pd = LaunchConfig('camera_parameter_directory') + calib_url = 'file://' + LaunchConfig('calibration_directory').perform(context) + '/' + + exp_ctrl_names = [cam + '.exposure_controller' for cam in camera_list.keys()] + driver_parameters = { + 'cameras': list(camera_list.keys()), + 'exposure_controllers': exp_ctrl_names, + 'ffmpeg_image_transport.encoding': 'hevc_nvenc', # only for ffmpeg image transport + } + # generate identical exposure controller parameters for all cameras + for exp in exp_ctrl_names: + driver_parameters.update( + {exp + '.' + k: v for k, v in exposure_controller_parameters.items()} + ) + + # generate camera parameters + cam_parameters['parameter_file'] = PJoin([pd, 'blackfly_s.yaml']) + for cam, serial in camera_list.items(): + cam_params = {cam + '.' + k: v for k, v in cam_parameters.items()} + cam_params[cam + '.serial_number'] = serial + cam_params[cam + '.camerainfo_url'] = calib_url + serial + '.yaml' + cam_params[cam + '.frame_id'] = cam + driver_parameters.update(cam_params) # insert into main parameter list + # link the camera to its exposure controller. Each camera has its own controller + driver_parameters.update({cam + '.exposure_controller_name': cam + '.exposure_controller'}) + return driver_parameters + + +def launch_setup(context, *args, **kwargs): + container = ComposableNodeContainer( + name='cam_sync_container', + namespace='', + package='rclcpp_components', + executable='component_container', + composable_node_descriptions=[ + ComposableNode( + package='spinnaker_synchronized_camera_driver', + plugin='spinnaker_synchronized_camera_driver::SynchronizedCameraDriver', + name='cam_sync', + parameters=[make_parameters(context)], + extra_arguments=[{'use_intra_process_comms': True}], + ), + ], + output='screen', + ) # end of container + return [container] + + +def generate_launch_description(): + return LaunchDescription( + [ + LaunchArg( + 'camera_parameter_directory', + default_value=PJoin([FindPackageShare('spinnaker_camera_driver'), 'config']), + description='root directory for camera parameter definitions', + ), + LaunchArg( + 'calibration_directory', + default_value=['camera_calibrations'], + description='root directory for camera calibration files', + ), + OpaqueFunction(function=launch_setup), + ] + ) diff --git a/spinnaker_synchronized_camera_driver/launch/synchronized_driver_node.launch.py b/spinnaker_synchronized_camera_driver/launch/synchronized_driver_node.launch.py deleted file mode 100644 index 2826b04d..00000000 --- a/spinnaker_synchronized_camera_driver/launch/synchronized_driver_node.launch.py +++ /dev/null @@ -1,95 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright 2022 Bernd Pfrommer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# - -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument as LaunchArg -from launch.actions import OpaqueFunction -from launch.substitutions import LaunchConfiguration as LaunchConfig -from launch.substitutions import PathJoinSubstitution as PJoin -from launch_ros.actions import Node -from launch_ros.substitutions import FindPackageShare - -cam_parameters = { - "parameter_file": "blackfly_s.yaml", - "buffer_queue_size": 1, - "frame_rate_auto": "Off", - "frame_rate_enable": False, - # watch that your exposure time is - # short enough to support the trigger frame rate! - "exposure_auto": "Off", - "exposure_time": 6000, - "gain_auto": "Continuous", - "trigger_mode": "On", - "trigger_source": "Line3", - "trigger_selector": "FrameStart", - "trigger_overlap": "ReadOut", - "trigger_activation": "RisingEdge", - # You must enable chunk mode and enable frame_id - "chunk_mode_active": True, - "chunk_selector_frame_id": "FrameID", - "chunk_enable_frame_id": True, - # The other chunk info should not be required - # "chunk_selector_exposure_time": "ExposureTime", - # "chunk_enable_exposure_time": True, - # "chunk_selector_gain": "Gain", - # "chunk_enable_gain": True, - # "chunk_selector_timestamp": "Timestamp", - # "chunk_enable_timestamp": True, -} - - -def launch_setup(context, *args, **kwargs): - """Launch synchronized camera driver node.""" - pd = LaunchConfig("camera_parameter_directory") - c0, c1 = "cam_0", "cam_1" - c0p, c1p = c0 + ".", c1 + "." - driver_parameters = {"cameras": [c0, c1]} - cam_parameters["parameter_file"] = PJoin([pd, "blackfly_s.yaml"]) - cam_0_parameters = {c0p + k: v for k, v in cam_parameters.items()} - cam_0_parameters[c0p + "serial_number"] = "20435008" - cam_1_parameters = {c1p + k: v for k, v in cam_parameters.items()} - cam_1_parameters[c1p + "serial_number"] = "20415937" - node = Node( - package="spinnaker_synchronized_camera_driver", - executable="synchronized_camera_driver_node", - output="screen", - # prefix=["xterm -e gdb -ex run --args"], - name=[LaunchConfig("driver_name")], - parameters=[driver_parameters, cam_0_parameters, cam_1_parameters], - ) - return [node] - - -def generate_launch_description(): - """Create composable node by calling opaque function.""" - return LaunchDescription( - [ - LaunchArg( - "driver_name", - default_value=["cam_sync"], - description="name of driver node", - ), - LaunchArg( - "camera_parameter_directory", - default_value=PJoin( - [FindPackageShare("spinnaker_camera_driver"), "config"] - ), - description="root directory for camera parameter definitions", - ), - OpaqueFunction(function=launch_setup), - ] - ) diff --git a/spinnaker_synchronized_camera_driver/src/exposure_controller_factory.cpp b/spinnaker_synchronized_camera_driver/src/exposure_controller_factory.cpp index 2619926c..40f13cf3 100644 --- a/spinnaker_synchronized_camera_driver/src/exposure_controller_factory.cpp +++ b/spinnaker_synchronized_camera_driver/src/exposure_controller_factory.cpp @@ -14,8 +14,9 @@ // limitations under the License. #include -#include +#include #include +#include namespace spinnaker_synchronized_camera_driver { @@ -25,8 +26,10 @@ static rclcpp::Logger get_logger() { return (rclcpp::get_logger("cam_sync")); } std::shared_ptr newInstance( const std::string & type, const std::string & name, rclcpp::Node * node) { - if (type == "individual") { - return (std::make_shared(name, node)); + if (type == "master") { + return (std::make_shared(name, node)); + } else if (type == "follower") { + return (std::make_shared(name, node)); } BOMB_OUT("unknown exposure controller type: " << type); return (nullptr); diff --git a/spinnaker_synchronized_camera_driver/src/follower_exposure_controller.cpp b/spinnaker_synchronized_camera_driver/src/follower_exposure_controller.cpp new file mode 100644 index 00000000..88153449 --- /dev/null +++ b/spinnaker_synchronized_camera_driver/src/follower_exposure_controller.cpp @@ -0,0 +1,102 @@ +// -*-c++-*-------------------------------------------------------------------- +// Copyright 2024 Bernd Pfrommer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +namespace spinnaker_synchronized_camera_driver +{ +FollowerExposureController::FollowerExposureController( + const std::string & name, rclcpp::Node * node) +: name_(name), node_(node) +{ + exposureParameterName_ = declare_param("exposure_parameter", "exposure_time"); + gainParameterName_ = declare_param("gain_parameter", "gain"); + maxFramesSkip_ = declare_param("max_frames_skip", 10); // number of frames to wait + masterControllerName_ = declare_param("master", ""); + if (masterControllerName_.empty()) { + BOMB_OUT("master exposure controller must be set for controller " << name_); + } +} +void FollowerExposureController::link( + const std::unordered_map> & map) +{ + const auto it = map.find(masterControllerName_); + if (it == map.end()) { + BOMB_OUT("cannot find master " << masterControllerName_ << " for controller " << name_); + } + masterController_ = it->second; +} + +void FollowerExposureController::update( + spinnaker_camera_driver::Camera * cam, + const std::shared_ptr & img) +{ + // if the exposure parameters are not set yet, set them now + if (currentExposureTime_ == 0) { + currentExposureTime_ = static_cast(img->exposureTime_); + } + if (currentGain_ == std::numeric_limits::lowest()) { + currentGain_ = img->gain_; + } + // check if the exposure and brightness settings reported along with the image + // match what last has been sent to the camera. + if ( + fabs(currentGain_ - img->gain_) <= 0.05 * (currentGain_ + img->gain_) && + fabs(currentExposureTime_ - static_cast(img->exposureTime_)) <= + 0.05 * (currentExposureTime_ + static_cast(img->exposureTime_)) && + numFramesSkip_ < maxFramesSkip_) { + numFramesSkip_ = 0; // no skipping anymore! + } + + if (numFramesSkip_ > 0) { + // Changes in gain or shutter take a few + // frames to arrive at the camera, so we skip those. + numFramesSkip_--; + } else { + const auto masterExposureTime = masterController_->getExposureTime(); + const auto masterGain = masterController_->getGain(); + bool parametersChanged{false}; + if (masterExposureTime != currentExposureTime_) { + const auto expName = cam->getPrefix() + exposureParameterName_; + node_->set_parameter(rclcpp::Parameter(expName, masterExposureTime)); + parametersChanged = true; + } + if (masterGain != currentGain_) { + const auto gainName = cam->getPrefix() + gainParameterName_; + node_->set_parameter(rclcpp::Parameter(gainName, masterGain)); + parametersChanged = true; + } + if (parametersChanged) { + const int b = std::min(std::max(1, static_cast(img->brightness_)), 255); + LOG_INFO( + "bright " << b << " at time/gain: [" << currentExposureTime_ << " " << currentGain_ + << "] new: [" << masterExposureTime << " " << masterGain << "]"); + numFramesSkip_ = maxFramesSkip_; // restart frame skipping + currentExposureTime_ = masterExposureTime; + currentGain_ = masterGain; + } + } +} + +void FollowerExposureController::addCamera( + const std::shared_ptr & cam) +{ + cameraName_ = cam->getName(); +} + +} // namespace spinnaker_synchronized_camera_driver diff --git a/spinnaker_synchronized_camera_driver/src/individual_exposure_controller.cpp b/spinnaker_synchronized_camera_driver/src/master_exposure_controller.cpp similarity index 88% rename from spinnaker_synchronized_camera_driver/src/individual_exposure_controller.cpp rename to spinnaker_synchronized_camera_driver/src/master_exposure_controller.cpp index 4e043a6d..be772cd2 100644 --- a/spinnaker_synchronized_camera_driver/src/individual_exposure_controller.cpp +++ b/spinnaker_synchronized_camera_driver/src/master_exposure_controller.cpp @@ -15,15 +15,14 @@ #include #include -#include #include +#include // #define DEBUG namespace spinnaker_synchronized_camera_driver { -IndividualExposureController::IndividualExposureController( - const std::string & name, rclcpp::Node * node) +MasterExposureController::MasterExposureController(const std::string & name, rclcpp::Node * node) : name_(name), node_(node) { exposureParameterName_ = declare_param("exposure_parameter", "exposure_time"); @@ -35,10 +34,10 @@ IndividualExposureController::IndividualExposureController( minExposureTime_ = std::max(declare_param("min_exposure_time", 10), 1); maxGain_ = declare_param("max_gain", 10); gainPriority_ = declare_param("gain_priority", false); - maxFramesSkip_ = declare_param("min_frames_skip", 10); // number of frames to wait + maxFramesSkip_ = declare_param("max_frames_skip", 10); // number of frames to wait } -double IndividualExposureController::calculateGain(double brightRatio) const +double MasterExposureController::calculateGain(double brightRatio) const { // because gain is in db: // db(G) = 10 * log_10(G) = 10 * ln(G) / ln(10) = 4.34 * ln(G) @@ -50,14 +49,14 @@ double IndividualExposureController::calculateGain(double brightRatio) const return (cappedGain > 0.5 ? cappedGain : 0); } -double IndividualExposureController::calculateExposureTime(double brightRatio) const +double MasterExposureController::calculateExposureTime(double brightRatio) const { const double desiredExposureTime = currentExposureTime_ * brightRatio; const double optTime = std::max(0.0, std::min(desiredExposureTime, maxExposureTime_)); return (optTime); } -bool IndividualExposureController::changeExposure( +bool MasterExposureController::changeExposure( double brightRatio, double minTime, double maxTime, const char * debugMsg) { const double optTime = std::min(std::max(calculateExposureTime(brightRatio), minTime), maxTime); @@ -73,7 +72,7 @@ bool IndividualExposureController::changeExposure( return (false); } -bool IndividualExposureController::changeGain( +bool MasterExposureController::changeGain( double brightRatio, double minGain, double maxGain, const char * debugMsg) { const double optGain = std::min(std::max(calculateGain(brightRatio), minGain), maxGain); @@ -89,7 +88,7 @@ bool IndividualExposureController::changeGain( return (false); } -bool IndividualExposureController::updateExposureWithGainPriority(double brightRatio) +bool MasterExposureController::updateExposureWithGainPriority(double brightRatio) { if (brightRatio < 1) { // image is too bright if (currentGain_ > 0) { @@ -119,7 +118,7 @@ bool IndividualExposureController::updateExposureWithGainPriority(double brightR return (false); } -bool IndividualExposureController::updateExposureWithTimePriority(double brightRatio) +bool MasterExposureController::updateExposureWithTimePriority(double brightRatio) { if (brightRatio < 1) { // image is too bright if (currentExposureTime_ > minExposureTime_) { @@ -163,7 +162,7 @@ bool IndividualExposureController::updateExposureWithTimePriority(double brightR return (false); } -bool IndividualExposureController::updateExposure(double b) +bool MasterExposureController::updateExposure(double b) { const double err_b = (brightnessTarget_ - b); // the current gain is higher than it should be, let's @@ -194,7 +193,7 @@ bool IndividualExposureController::updateExposure(double b) return (false); } -void IndividualExposureController::update( +void MasterExposureController::update( spinnaker_camera_driver::Camera * cam, const std::shared_ptr & img) { @@ -206,11 +205,6 @@ void IndividualExposureController::update( if (currentGain_ == std::numeric_limits::lowest()) { currentGain_ = img->gain_; } -#if 0 - LOG_INFO( - "img: " << img->exposureTime_ << "/" << currentExposureTime_ << " gain: " << img->gain_ << "/" - << currentGain_ << " brightness: " << b); -#endif // check if the reported exposure and brightness settings // match ours. That means the changes have taken effect @@ -244,7 +238,7 @@ void IndividualExposureController::update( } } -void IndividualExposureController::addCamera( +void MasterExposureController::addCamera( const std::shared_ptr & cam) { cameraName_ = cam->getName(); diff --git a/spinnaker_synchronized_camera_driver/src/synchronized_camera_driver.cpp b/spinnaker_synchronized_camera_driver/src/synchronized_camera_driver.cpp index c915e8bc..c088921f 100644 --- a/spinnaker_synchronized_camera_driver/src/synchronized_camera_driver.cpp +++ b/spinnaker_synchronized_camera_driver/src/synchronized_camera_driver.cpp @@ -101,6 +101,10 @@ void SynchronizedCameraDriver::createExposureControllers() BOMB_OUT("no controller type specified for controller " << c); } } + // allow the exposure controllers to link to each other. + for (auto & c : exposureControllers_) { + c.second->link(exposureControllers_); + } } void SynchronizedCameraDriver::createCameras()