System designTroubleshooting

ROS 2 image publishing frame rate: find the bottleneck

Diagnose low ROS 2 image publishing frame rate by measuring acquisition, Python conversion, transport and subscriber load before changing hardware.

LumaSpec Vision11 min read Share on LinkedIn

On this page 10 sections
  1. 01Define which frame rate is actually low
  2. 02Build a staged benchmark before optimizing
  3. 03Calculate the payload before blaming the transport
  4. 04Check polling, conversion and callback time
  5. 05Separate middleware behavior from subscriber load
  6. 06Change the architecture only after locating the cost
  7. 07Protect synchronization and image meaning
  8. 08Check hardware suitability without replacing the wrong part
  9. 09Use a repeatable acceptance checklist
  10. 10Questions

A camera can deliver frames quickly while a ROS 2 subscriber receives images slowly. Acquisition, array conversion, message construction, middleware delivery and downstream processing all contribute different costs. A timer frequency or an internal loop counter does not measure the complete path.

For a Python image publisher, the fastest route to a useful diagnosis is a staged benchmark. Measure each boundary, add consumers individually, and compare throughput with image age. You can then change the part that limits performance without sacrificing synchronization or calibration.

Define which frame rate is actually low

Start by naming the measurement point. Record these rates separately for every stream:

  • Acquisition rate: complete frames retrieved from the camera API.
  • Conversion rate: frames converted into the array representation your publisher expects.
  • Publish-call rate: image messages submitted through the publisher API.
  • Receive rate: messages observed by a minimal subscriber.
  • Processing rate: images actually consumed by the inspection or navigation application.

Do not combine several streams into one counter. Publishing two intensity images and one depth image produces three messages per capture set. An aggregate message counter can therefore hide a slow individual stream.

Also measure image age, not just throughput. A subscriber receiving regularly spaced images may still be processing old data from a queue. Preserve acquisition timestamps where available, and document their clock domain before comparing them with host time.

Use a monotonic host clock for elapsed-duration measurements. Use acquisition timestamps or device counters to investigate source cadence and missing frames. Our guide to measuring frame rates from timestamped frames explains why those measurements answer different questions.

Build a staged benchmark before optimizing

Use the same image dimensions, pixel formats, exposure settings and enabled streams throughout each comparison. Allow startup to settle, measure for 30 seconds, and repeat each case three times.

The following sequence separates the likely causes without changing several variables together.

Test What runs What a slowdown suggests
Acquisition only Camera API retrieves and releases frames Camera configuration, device processing, link or acquisition code
Conversion only Acquisition plus array conversion Format conversion, allocation or copying
Publisher without subscribers Conversion plus image-message construction and publishing Publisher-side overhead; delivery costs are not fully exercised
Minimal subscriber One subscriber counts images and records timestamps Serialization, middleware, scheduling or receive overhead
Real consumer The intended processing application Processing cost, synchronization waits or queue policy
Visualization added The previous test plus the image viewer Display conversion, rendering or additional delivery load

The no-subscriber test is useful, but not a complete transport benchmark. Middleware may do less work when no endpoints are matched. Always compare it with a real subscriber.

During each run, record CPU usage by core, process memory, callback durations and available dropped-frame counters. Disable per-frame logging; aggregate statistics once per second instead.

If acquisition alone is slow, follow camera throughput bottleneck checks before tuning ROS 2. If conversion is the first failing stage, use frame validation before Python conversion to separate malformed input from conversion overhead.

Calculate the payload before blaming the transport

Resolution labels alone are not enough. Calculate the bytes carried by every published image, including row padding when present.

For an image message, the payload estimate is:

image bytes = step × height aggregate payload rate = sum(image bytes × stream rate)

Here, step is the row length in bytes. For a tightly packed image, it equals width multiplied by bytes per pixel. Transport headers and serialization overhead are additional.

Consider an illustrative capture set containing two mono8 images and one 16UC1 depth image. Each image is 640 × 400 pixels, with no row padding:

one mono image = 640 × 400 × 1 = 256,000 bytes one depth image = 640 × 400 × 2 = 512,000 bytes capture set = 2 × 256,000 + 512,000 = 1,024,000 bytes at 30 capture sets/s = 30,720,000 bytes/s = 30.72 MB/s at 120 capture sets/s = 122,880,000 bytes/s = 122.88 MB/s

These are decimal megabytes. At the higher rate, image payload alone represents about 983 Mb/s. That leaves little room on a nominal gigabit network link once protocol overhead is included. Local processes do not necessarily send that traffic over a physical network, but they still incur implementation-dependent memory and middleware work.

Copies add another cost. A complete buffer copy requires reading the source and writing the destination. Several conversions and message transfers can therefore create much more memory traffic than the original payload rate suggests.

Check polling, conversion and callback time

A timer controls when a callback becomes eligible to run; it does not guarantee execution at that frequency. Executor scheduling and callback work still determine what happens.

For example, a 20 Hz timer has a 50 ms period. If its callback retrieves at most one frame per stream, that design cannot publish more than 20 frames per second per stream. Increasing the camera rate does not remove that software ceiling.

Now suppose callback execution averages 75 ms. A strictly serial execution path cannot sustain more than approximately:

maximum serial callback rate ≈ 1 / callback duration 1 / 0.075 s ≈ 13.3 callbacks/s

That estimate excludes scheduling gaps and other callbacks. Measure the actual distribution, including slow outliers, rather than relying only on the average.

Instrument queue retrieval, array extraction, conversion, message construction and the publish call separately. A publish call returning does not prove that a subscriber received the message. Some delivery work can happen later.

Check for unnecessary operations such as:

  • Converting an intensity image into color when the consumer accepts mono8.
  • Copying an array to change layout, then copying it again into a message.
  • Resizing images independently in both publisher and subscriber.
  • Waiting sequentially for streams that are not ready together.
  • Formatting large diagnostic strings inside every callback.

Use documented nonblocking queue operations when appropriate. A separate availability check followed by retrieval may become unsafe if another thread can consume the same queue. Keep ownership clear, and do not let slow publishing retain camera buffers indefinitely.

For timer-related uncertainty, compare the results with external-trigger and acquisition timing checks. Hardware cadence and publisher scheduling are separate limits.

Separate middleware behavior from subscriber load

Add one minimal subscriber that records arrivals without displaying, saving or processing images. Compare its count with the publisher's submitted-message count over the same interval.

The ros2 topic hz command is useful as a quick observation tool, but it is another subscriber. Its reported rate depends on its own scheduling, QoS compatibility and ability to receive messages. It is not proof of the camera's acquisition rate or the rate delivered to a different application.

Check the effective QoS settings at both endpoints:

  • Reliability: best effort can tolerate loss, while reliable delivery can introduce retransmission or backpressure depending on the implementation and resource settings.
  • History depth: deeper queues accommodate bursts but can retain stale images.
  • Compatibility: incompatible publisher and subscriber settings can prevent delivery entirely.
  • Consumer requirements: inspection, recording and navigation may need different loss and latency policies.

For example, five buffered images at 30 frames per second represent roughly 167 ms of image data. This is a capacity estimate, not a guaranteed delay. Larger queues do not increase sustained processing capacity.

Test the subscriber locally before testing across machines. Then add the actual consumer, recording and visualization one at a time. Removing the viewer helps only if its rendering, conversions or subscription traffic were significant.

When a rate collapse develops into a freeze, use overload isolation for multiple acquisition sources to investigate queue growth and recovery behavior.

Change the architecture only after locating the cost

A C++ publisher is a reasonable option when measurements identify Python-side message construction, allocation or execution as the limiting stage. It is not a universal fix for slow acquisition, incompatible QoS or a downstream algorithm that cannot keep up.

First move only the measured bottleneck into a small benchmark. Keep dimensions, encodings, QoS and subscriber behavior unchanged. Compare receive rate, CPU usage and image age against the Python implementation.

For compatible C++ nodes, composition and configured intra-process communication can reduce serialization and copying between components. The benefit depends on message ownership, subscriber arrangement and supported communication paths. Simply putting nodes in one process does not prove that every copy disappeared.

Shared-memory transport is also implementation-specific. Shared-memory delivery, loaned messages and application-level zero-copy access are different capabilities. Standard image messages contain variable-length data, so verify support for the exact message type, middleware and configuration rather than assuming a fixed-size requirement applies everywhere.

Adding threads is another measured choice. Some native operations release Python's interpreter lock, while Python-heavy work may remain serialized. Check callback groups, executor behavior and thread safety before increasing concurrency.

Protect synchronization and image meaning

A higher receive rate is not useful if the consumer receives mismatched images or incorrect metadata. Validate correctness again after every scheduling or transport change.

Independent queue reads do not automatically form a synchronized capture set. Match streams using acquisition timestamps or documented sequence identifiers, and define an acceptable timing difference. Do not replace capture timestamps with publish time merely to make images appear newer.

The timestamp-based result matching guide covers the same principle for associating delayed outputs with their source images. For a multi-stream publisher, retain that association through conversion and delivery.

Verify these details separately:

  • Image encoding matches the array's actual element type and channel layout.
  • Row stride and byte order are represented correctly.
  • Depth scale and invalid-value conventions are documented for the consumer.
  • Calibration describes the published resolution and image geometry.
  • Rectified images carry calibration information appropriate to rectified output.

The encoding 16UC1 identifies unsigned integer storage with one channel; it does not establish a universal depth unit. A fast pipeline with the wrong scale can produce plausible-looking but incorrect geometry.

Finally, measure the consumer's synchronization waits. A slow auxiliary stream can hold back an otherwise fast image stream without making the publisher itself slow.

Check hardware suitability without replacing the wrong part

A ROS 2 publishing bottleneck does not automatically justify a different camera. First confirm that the current device meets acquisition-only requirements. Use the area scan cameras category and camera finder only when interface, sensor or acquisition requirements genuinely need to change.

The LS-CMR-0053 is a GigE area scan camera. The LS-CMR-0166 uses USB3. These are interface-selection candidates, not substitutes for a synchronized depth system or guarantees of ROS 2 throughput.

Area scan cameras12 MP GigE area scan camera, IMX226, 1/1.7" rolling shutter, 9 fps (LS-CMR-0053)LS-CMR-0053
Sensor format
1/1.7"
Lens mount
C-mount
$560.00View part
Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
$560.00View part

For a dedicated acquisition host, evaluate vision controllers against the measured processing load, camera-port requirements and supported drivers. Additional ports alone do not establish independent bandwidth or sufficient conversion capacity.

Check camera cables when acquisition-only testing shows disconnects, errors or link negotiation problems. The LS-CBL-0199 is a GigE camera cable; the LS-CBL-0219 is a USB3.0 camera cable. Confirm connector compatibility and installation requirements before selection.

Use the accessories finder to review the required connection type. A replacement cable will not resolve excessive message copying, executor delays or a consumer that processes images too slowly.

Use a repeatable acceptance checklist

Before calling the issue resolved, repeat the complete pipeline test with the real workload:

  • Acquisition: each source stream reaches its required cadence with the final exposure and processing settings.
  • Conversion: encoding and pixel values remain correct, including depth scale and invalid values.
  • Publishing: callback duration and publish-call rate are recorded separately from reception.
  • Delivery: the intended subscriber meets the agreed throughput and loss limits.
  • Latency: image age stays bounded rather than rising throughout the run.
  • Synchronization: matched streams remain within the application's timing tolerance.
  • Load: enabling the final consumers does not produce sustained queue growth.
  • Repeatability: startup, shutdown and repeated runs produce comparable results.

Set numerical acceptance limits from the application, not from a convenient display counter. For example, a 30-frame-per-second stream has a 33.3 ms frame period. A serial processing stage averaging longer than that cannot consume every frame indefinitely without parallelism, rate reduction or dropping.

Document the chosen policy: process every frame, or prioritize the newest available frame. Those are different requirements and lead to different queue designs.

If acquisition hardware needs changing, start with the camera configuration step. If the limiting stage remains unclear, send the staged rates, image dimensions, encodings, QoS settings and callback timings through technical support so the next decision is based on measurements.

Questions

Why is my internal frame counter faster than the ROS 2 subscriber?

The counters may observe different stages or count different things. A retrieval loop measures available camera frames, while a subscriber measures delivered messages. Multiple streams can also inflate an aggregate counter. Compare each stream separately over a shared test interval, and inspect timestamps, dropped frames and queue growth.

Will disabling visualization increase the image publishing rate?

Only when visualization contributes enough processing or delivery load to limit the pipeline. A viewer may add image conversion, rendering and another subscription. If message construction or acquisition is already the bottleneck, closing the viewer may have little effect. Compare otherwise identical runs with and without that subscriber.

Should I rewrite a slow Python image publisher in C++?

Consider C++ when profiling identifies Python-side work as the limiting stage. Benchmark an equivalent publisher before migrating the complete application. Keep image formats, QoS and consumers unchanged so the comparison remains meaningful. A language change will not repair an acquisition limit or an overloaded downstream algorithm.

Does shared-memory transport work with standard image messages?

Support depends on the middleware, message representation and configured communication path. Shared-memory transport does not automatically provide zero-copy access to application buffers. Verify standard image-message support and ownership requirements for your exact implementation, then measure whether the change improves delivered rate and image age.

What should I measure besides frames per second?

Useful acceptance criteria combine receive rate, image age, loss and synchronization. A subscriber can report a high rate while processing stale or mismatched images. Record the intended consumer's actual processing behavior and confirm that latency stays bounded throughout a sustained test with all required streams enabled.