# pointcloud_to_laserscan: making a depth camera look like a lidar

> pointcloud_to_laserscan is a ROS 2 package with two nodes that convert between sensor_msgs/msg/PointCloud2 and sensor_msgs/msg/LaserScan, so a depth camera or solid state sensor can feed code written against a planar laser. It suits a robot developer who already speaks LaserScan, and it is the wrong tool when the consumer needs the full 3D structure.

**ros-perception/pointcloud_to_laserscan** — Converts a 3D Point Cloud into a 2D laser scan.

- Repository: https://github.com/ros-perception/pointcloud_to_laserscan
- Website: http://wiki.ros.org/pointcloud_to_laserscan
- Stars: 606 · Forks: 381
- Language: C++
- License: BSD-3-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/ros-perception-pointcloud-to-laserscan

## Two nodes for a problem created by sensor choice

pointcloud_to_laserscan is a ROS 2 package that provides components to convert sensor_msgs/msg/PointCloud2 messages to sensor_msgs/msg/LaserScan messages and back. The repository description is shorter, a 3D point cloud into a 2D laser scan, and the readme notes that the package is essentially a port of the original ROS 1 package.

The problem it solves is worth stating plainly, because it is a sensor problem rather than an algorithm problem. Large parts of the robotics stack, navigation in particular, were written against LaserScan, which is a flat polar representation: an array of ranges, one per angle. Newer depth cameras and solid state sensors produce PointCloud2, which is a 3D structure with x, y and z per point. If your navigation expects a LaserScan and your hardware gives you a point cloud, you need something in between, and that something is this package.

There is also the reverse direction, and the reverse direction is the one that surprises people. The second node re-publishes LaserScan messages as PointCloud2 messages, which is what lets you drive a 2D sensor through a pipeline that expects 3D data, or render a laser scan in a visualiser that only understands point clouds.

The package is BSD-3-Clause licensed, the default branch is rolling, and the repository contains a CHANGELOG.rst, a CMakeLists.txt, a package.xml, and the directories include/, launch/, src/ and the usual ROS package layout, with the documentation hosted on the ROS wiki at wiki.ros.org/pointcloud_to_laserscan.

## What the projection actually throws away

Reading the parameter list tells you what the conversion does, and the honest summary is that it slices the cloud into a horizontal band and measures the first surface in each direction.

The PointCloudToLaserScanNode subscribes to cloud_in with sensor_msgs/msg/PointCloud2 and publishes scan with sensor_msgs/msg/LaserScan. The gating parameters define the band. min_height defaults to 2.2e-308 and max_height to 1.8e+308, which are the smallest and largest finite doubles, so by default there is no height restriction at all. The angle parameters bracket the sweep, with angle_min at negative pi and angle_max at pi, which is a full circle, and angle_increment at pi/180, which is one ray per degree and therefore 360 rays in a full sweep.

The range parameters behave the same way. range_min defaults to 0.0 and range_max to 1.8e+308, so nothing is filtered by distance unless you say so.

That default behaviour is the thing to be careful about. A camera looking at a room produces points on the ceiling, the floor, the furniture and the people in it, and a converter that keeps all of them and casts a ray through each of 360 directions will report a range for a direction that a real 2D laser would never have seen, because the ceiling was in the way. Setting a realistic min_height and max_height is what makes the output resemble the sensor you are imitating, and the defaults are permissive rather than useful.

There is one more parameter that changes the output in a way that is easy to miss: use_inf, a boolean that defaults to true, and it controls how a direction with no obstacle is reported. If it is disabled, an infinite range is reported as range_max plus one, and otherwise it is reported as positive infinity. Which of the two a consumer tolerates depends on the consumer, and the parameter exists precisely because the two are not interchangeable.

## Target frames, and the tolerance that goes with them

Both nodes can do the conversion in a specific frame, and both use the same pair of parameters for it.

target_frame is a string with a default of none. If it is provided, the input is transformed into that frame before the conversion happens, and otherwise the output is generated in the same frame as the input. So the parameter is not a convenience, it is a semantic switch: without it the node preserves the input frame, and with it the node takes on the responsibility of getting the geometry right across a transform.

transform_tolerance is a double with a default of 0.01, and it is the time tolerance for transform lookups. It is only used if a target_frame is provided, which follows from the fact that without a target frame there is no lookup to tolerate.

The 0.01 default is a one hundredth of a second, which is short enough to fail. A transform lookup asks the tf tree for the pose of the target frame at the timestamp of the message being converted. If the two are not exactly aligned in time, the lookup either succeeds with the closest available transform or fails outright. In a system where the point cloud is timestamped on sensor receipt and the transform tree is updated at a different rate, a one hundredth of a second tolerance is tight, and the failure mode is a conversion that errors on some frames and works on others.

The queue_size parameter is the third one that appears in both nodes, with a default of the detected number of cores. It is the input queue size, and it is also described with the type double, which is a small oddity in the parameter typing but is how the documentation records it. The default of one message per core is a reasonable starting point for a sensor running faster than the consumer, and it is also where a slow consumer shows up first as latency rather than as dropped frames.

## Back to point cloud, and the shape of the reverse conversion

The reverse node, LaserScanToPointCloudNode, publishes cloud with sensor_msgs/msg/PointCloud2 and subscribes to scan_in with sensor_msgs/msg/LaserScan, and it carries a smaller parameter set: queue_size, target_frame and transform_tolerance.

What is missing from that list is as informative as what is present. There are no height parameters, no angle parameters and no range parameters, because the reverse direction does not have to decide anything. A laser scan is already a set of ranges at known angles, so turning it into a point cloud is a mechanical expansion: each range becomes a point at a known distance along a known direction. The lossy decisions were all made on the way down, when the 3D structure was collapsed into a plane, and the reverse node cannot recover them.

That asymmetry is the single most important thing to understand about this package. Round-tripping a point cloud through LaserScan and back does not give you your point cloud. It gives you a sparse set of points on a horizontal slice, expressed in 3D. If you convert a depth camera output to LaserScan for your navigation stack and then convert it back for a recording or a visualisation, the recording is missing everything outside the band and everything occluded by what was inside it.

The frame handling is symmetric, and it is worth noticing that the reverse node also has a target_frame parameter, so you can re-express a laser scan in another frame on the way out, which is occasionally what a pipeline needs when the laser is mounted in a different place from the frame the consumer wants.

Both nodes also share the same queueing behaviour, and both document it the same way: no input is processed if there is not at least one subscriber to the output topic. That is a deliberate flow control, so a node left running with nothing listening does not accumulate a backlog of stale clouds.

## Where pointcloud_to_laserscan is the wrong choice

The first limitation is inherent to the conversion and worth restating: a 2D laser scan is a lossy projection of a 3D cloud, and the amount lost depends entirely on how you configure the height band, the angular limits and the range limits. A team that needs the full structure, for obstacle avoidance in three dimensions, for a drone in a cluttered space, or for anything that reasons about overhangs and ceilings, should use the point cloud directly. Converting it to satisfy an old interface is sometimes right, and sometimes it is a way of losing information that will be needed later.

The second is the default configuration. With no height band, a full 360 degree sweep and no range limits, the output describes a room rather than a laser. Nothing in the defaults is wrong, and everything about them is unsuitable for a real sensor without editing, so a deployment that forgets the parameters will appear to work and produce nonsense.

The third is the frame and time coupling. The target_frame parameter means the node depends on a correct, current transform tree, and the transform_tolerance default of 0.01 is tight. A converter that appears in a launch file without a target_frame will not fail, and a converter with a target_frame will fail intermittently if the timestamps do not line up. Both behaviours are easy to live with once you know them and painful to debug if you do not.

The fourth is that this is a port, and the readme says so. The ROS 1 package it derives from had years of tuning, and the parameters, defaults and edge cases are what that history produced. Porting preserves the interface and the defaults, and it does not guarantee that every corner of the original behaviour has been checked against a current ROS 2 distribution. There is a CHANGELOG.rst in the repository, and the last push was on 2026-10-02, but the repository publishes no tagged releases, so there is no version history to consult the way there would be for a packaged release.

The fifth is scope. This is two nodes and their parameters, in a repository with a CHANGELOG, a CMakeLists.txt, a package.xml, include/, launch/ and src/. It does not include a driver, a sensor abstraction, or anything about which camera to buy, and it assumes the rest of the ROS 2 stack is already in place.

## Conclusion

pointcloud_to_laserscan fits a ROS 2 developer whose navigation or obstacle code already consumes LaserScan and whose sensor happens to produce a point cloud, since the converter handles the projection and the height, angle and range gating that make the result usable. It is the wrong choice if you need a true 3D representation, because a planar scan discards everything above and below the sampled band, and it is the wrong choice if the range limits are left at their defaults. Before adopting it, set min_height, max_height, angle_min, angle_max, range_min and range_max to values that match your sensor, confirm the target_frame you need and the transform tolerance that goes with it, and note that the last push was on 2026-10-02 with no tagged releases in the repository.

## FAQ

### What does pointcloud_to_laserscan do in ROS 2?

It is a ROS 2 package with two components that convert between sensor_msgs/msg/PointCloud2 and sensor_msgs/msg/LaserScan, and it is essentially a port of the original ROS 1 package.

### Which parameters control the point cloud to laser scan conversion?

min_height and max_height set the vertical band sampled from the cloud, angle_min and angle_max bracket the sweep in radians, angle_increment gives the resolution per ray, and range_min and range_max filter the reported distances. use_inf controls whether an infinite range is reported as plus infinity or as range_max plus one.

### What does the target_frame parameter do in pointcloud_to_laserscan?

If it is provided, the input is transformed into that frame before the conversion; otherwise the output is generated in the same frame as the input. The transform_tolerance parameter sets the time tolerance for those transform lookups and is only used when a target_frame is given.

### Can I convert a LaserScan back into a PointCloud2?

Yes, the LaserScanToPointCloudNode re-publishes sensor_msgs/msg/LaserScan messages as sensor_msgs/msg/PointCloud2. It takes only queue_size, target_frame and transform_tolerance, since the height, angle and range decisions were already made in the other direction.

### How do I install pointcloud_to_laserscan?

The repository does not give an install command; it is a standard ROS 2 package with a package.xml and a CMakeLists.txt, built with colcon as part of a ROS 2 workspace. The package documentation is on the ROS wiki at wiki.ros.org/pointcloud_to_laserscan, and the last push was on 2026-10-02 with no tagged releases.

## Sources

- [Issues](https://github.com/ros-perception/pointcloud_to_laserscan/issues)
- [License: BSD-3-Clause](https://github.com/ros-perception/pointcloud_to_laserscan/blob/rolling/LICENSE)
- [Project website](http://wiki.ros.org/pointcloud_to_laserscan)
- [README](https://github.com/ros-perception/pointcloud_to_laserscan/blob/rolling/README.md)
- [ros-perception/pointcloud_to_laserscan on GitHub](https://github.com/ros-perception/pointcloud_to_laserscan)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ros-perception-pointcloud-to-laserscan
