Raspberry Pi CSI Camera ROS2 Node

June 1, 2026 ยท View on GitHub

Streams hardware or software encoded H.264 frames as ROS2 topics, optionally also allows to stream YUV420/Mono8 or BGR8 uncompressed frames as an Image topic.

Using libcamera to capture frames, and v4l2 with BCM2711 or libav for CPU-bases encoding. DMA heaps are used for fast frame access.

This node allows to calibrate the camera via ROS2 service calls, then streams calibration data as a CameraInfo topic.

This package was designed to work with Phantom Bridge and to provide fast hardware-encoded H.264 video streaming at low CPU cost, but can be used separately to ROSify your Pi camera modules. In order to achive maximum framerate on the Image topics, use YUV420 or Mono8 outputs. The additional BGR8 output costs extra CPU time as the node internally works with YUV420 and needs to scale up the U and V planes. Using the BGR8 output with H.264 is not recommended as it significantly degrades FPS.

The node can handle multiple cameras connected to the same board at the same time via different CSI ports (such as the Compute Module 4 or Pi 5).

Endpoint Badge Endpoint Badge Endpoint Badge Endpoint Badge Endpoint Badge Endpoint Badge

Note

Raspberry Pi 5 no longer has the hardware video encoder the older models had. Encoding to streamable H.264 is done at CPU cost.

Install

Install Docker, Docker Build & Docker Compose

E.g. on Debian/Ubuntu follow these instructions. Then add the current user to the docker group:

sudo usermod -aG docker ${USER}
# log out & back in

Configure

The following is an example config file (~/picam_ros2_params.yaml)

/**:
  ros__parameters:
    # node_name: "picam_ros2" # name of the ROS node
    topic_prefix: '/picam_ros2/camera_'
    log_message_every_sec: 5.0
    log_scroll: False

    calibration_frames_needed: 10
    calibration_square_size_m: 0.0175 # set this to your actual calibration square dimension!
    calibration_pattern_size: [ 9, 6 ] # set this to your calibration chessboard size!
    calibration_files: '/calibration' # calibration files saved here

    /camera_0: # 0 is the camera location
      frame_id: 'pi_camera_optical_frame'

      enabled: True
      enable_calibration: True
      width: 1920
      height: 1080
      hflip: False
      vflip: False
      ae_enable: True # auto exposure enabled
      awb_enable: True # auto white-balance 
      contrast: 1.3
      analog_gain: 1.0 # analog gain of the sensor
      # exposure_time_ns: 30000 # manually set fixed exposure time if ae_enable=False
      # awb_mode: 0 # 0=auto, 1=incandescent, 2=tungsten, 3=fluorescent, 4=indoor, 5=daylight, 6=cloudy

      hw_encoder: True # True=using hw-encoder, False=CPU
      bitrate: 3000000
      compression: 30 # 0=no compression, 100=max
      framerate: 30
      # buffer_count: 4 # number of capture buffers

      publish_h264: True
      h264_reliability: RELIABLE # RELIABLE (default), BEST_EFFORT or SYSTEM_DEFAULT
      h264_durability: VOLATILE # TRANSIENT_LOCAL, VOLATILE (default) or SYSTEM_DEFAULT
      h264_history_depth: 10 # default 1

      publish_image: False 
      image_reliability: RELIABLE # RELIABLE (default), BEST_EFFORT or SYSTEM_DEFAULT
      image_durability: TRANSIENT_LOCAL # TRANSIENT_LOCAL, VOLATILE (default) or SYSTEM_DEFAULT
      image_history_depth: 1 # default 1

      publish_info: True
      info_reliability: BEST_EFFORT  # RELIABLE (default), BEST_EFFORT or SYSTEM_DEFAULT
      info_durability: VOLATILE # TRANSIENT_LOCAL, VOLATILE (default) or SYSTEM_DEFAULT
      info_history_depth: 1 # default 1

You can also use our pre-built Docker images, see ghcr.io/phantomcybernetics/picam_ros2 for ROS distributions (only ARM64 is provided as this package is meant to run on Raspberry Pi).

Add service to your compose.yaml

services:
  picam_ros2:
    # select a pre-built image according to your ROS distro
    image: ghcr.io/phantomcybernetics/picam_ros2:main-humble
    # image: ghcr.io/phantomcybernetics/picam_ros2:main-iron
    # image: ghcr.io/phantomcybernetics/picam_ros2:main-jazzy
    # image: ghcr.io/phantomcybernetics/picam_ros2:main-kilted
    # image: ghcr.io/phantomcybernetics/picam_ros2:main-lyrical
    # image: ghcr.io/phantomcybernetics/picam_ros2:main-rolling
    # or use phntm/picam-ros2:$ROS_DISTRO if building from source (see below)

    container_name: picam-ros2
    hostname: picam-ros2.local
    restart: unless-stopped
    privileged: true
    network_mode: host
    ipc: host
    # cpuset: '3' # consider restricting to a single CPU core
    # shm_size: 200m # more room for camera frames
    # environment:
    #  - ROS_DOMAIN_ID=22
    #  - RMW_IMPLEMENTATION=rmw_cyclonedds_cpp # recommended with Phantom Bridge
    volumes:
      - ~/picam_ros2_params.yaml:/ros2_ws/picam_ros2_params.yaml # config goes here
      - ~/picam_ros2_calibration:/calibration # calibration files are stored here
      - /tmp:/tmp
      - /run/udev:/run/udev:ro # cameras need this
    devices:
      - /dev:/dev # cameras need this
    command:
      ros2 launch picam_ros2 picam_launch.py

Launch

docker compose up picam_ros2

Calibration

Camera needs to be calibrated before any CameraInfo messages can be published. In order to calibrate a camera, you'll need a standard OpenCV calibratiion chessboard pattern such as this one (more about these patterns can be found here). Print or display it on a flat screen as large as possible, then make sure your calibration_pattern_size and calibration_square_size_m are set correctly in your YAML. You will need to restart the node/container to load the latest values from the YAML file. Attribute publish_info must be set to True.

Then call the camera_N/calibrate ROS service with true to start the calibration process. There's one such service for each detected camera, N represents the camera's location. Phantom Bridge provides convenient UI to make these service calls, or even map them to keyboard keys or controller buttons.

Then start calling the camera_N/sample_frame service to capture individual calibration frames, the calibration_frames_needed attribute defines how many will be taken. Always make sure the calibration pattern is clearly visible, take samples from various angles and distances without any reflections, cover as much of the camera's field of view as possible.

After the last frame is captured, the node will process all of them (streaming will freeze for a while), then it'll start immediately publishing CameraInfo messages with the updated camera model. To save the calibration, call the camera_N/save_calibration service. The camera distortion coefficients will be saved in a JSON file in the directory configured by the calibration_files attribute and loaded on the next launch of the node.

Tip

If you map the calibration_files directory via volumes from the host filesystem to the Docker container as shown in the compose.yaml examples above, your calibration file will be preserved between Docker container rebuilds.

(Optional) Clone this repo and build the Docker Image from source

cd ~
git clone git@github.com:PhantomCybernetics/picam_ros2.git picam_ros2
cd picam_ros2
ROS_DISTRO=humble; docker build -f Dockerfile -t phntm/picam-ros2:$ROS_DISTRO --build-arg ROS_DISTRO=$ROS_DISTRO .
# then use phntm/picam-ros2:humble in your compose.yaml

Upgrading

You may want to check out and/or follow our Bluesky account for updates and service announcements.

# Stop and remove the current Docker Container
docker stop picam-ros2 && docker rm picam-ros2

# If using pre-built Docker Images, run:
docker image rm ghcr.io/phantomcybernetics/picam_ros2:main-humble
docker compose pull picam_ros2

# If building from source:
docker image rm phntm/picam-ros2:humble
cd ~/picam_ros2
git pull
ROS_DISTRO=humble; docker build -f Dockerfile -t phntm/picam-ros2:$ROS_DISTRO --build-arg ROS_DISTRO=$ROS_DISTRO .

# All done, relaunch
docker compose up picam_ros2

Tested Hardware

BoardEncoderCameraResolutionBitrateFPS
Pi 4BBCM2711imx708_wide_noir1920x1080500000030
CM 4BCM2711imx708_wide1920x1080500000030
CM 4BCM2711imx708_wide + imx708_wide_noir1280x720500000030 + 30
Pi 5CPUimx708_wide1920x1080500000030
Pi 5CPUimx708_wide + imx708_wide_noir1920x1080500000030 + 30