Skip to content

Commit a1e9993

Browse files
authored
feat(pointcloud_preprocessor): improve cloud validation (#11853)
Signed-off-by: Mete Fatih Cırıt <mfc@autoware.org>
1 parent 3a586c2 commit a1e9993

File tree

5 files changed

+211
-11
lines changed

5 files changed

+211
-11
lines changed

perception/autoware_compare_map_segmentation/src/voxel_based_compare_map_filter/node.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ void VoxelBasedCompareMapFilterComponent::checkStatus(
106106
void VoxelBasedCompareMapFilterComponent::input_indices_callback(
107107
const sensor_msgs::msg::PointCloud2::ConstSharedPtr cloud, const PointIndicesConstPtr indices)
108108
{
109-
if (!is_valid(cloud)) {
109+
if (!is_valid(cloud, this->get_logger(), *this->get_clock())) {
110110
RCLCPP_ERROR(this->get_logger(), "[input_indices_callback] Invalid input!");
111111
return;
112112
}

sensing/autoware_pointcloud_preprocessor/include/autoware/pointcloud_preprocessor/filter.hpp

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#define AUTOWARE__POINTCLOUD_PREPROCESSOR__FILTER_HPP_
5454

5555
#include "autoware/pointcloud_preprocessor/transform_info.hpp"
56+
#include "autoware/pointcloud_preprocessor/utility/memory.hpp"
5657

5758
#include <boost/thread/mutex.hpp>
5859

@@ -244,17 +245,141 @@ class Filter : public rclcpp::Node
244245

245246
std::unique_ptr<managed_transform_buffer::ManagedTransformBuffer> managed_tf_buffer_{nullptr};
246247

247-
inline bool is_valid(const PointCloud2ConstPtr & cloud)
248+
/**
249+
* @brief Validate a sensor_msgs::msg::PointCloud2 message for structural consistency and layout.
250+
*
251+
* This function performs a series of lightweight sanity checks to determine whether a
252+
* PointCloud2 message is safe to consume by algorithms expecting an XYZ-compatible,
253+
* organized (dense) point cloud.
254+
*
255+
* Validation errors and warnings are reported using throttled ROS logging to avoid
256+
* excessive log spam when invalid data is repeatedly received.
257+
*
258+
* @details
259+
* The following checks are performed in order:
260+
*
261+
* 1. **Null pointer check**
262+
* - Ensures the input PointCloud2 pointer is valid.
263+
*
264+
* 2. **Point step validation**
265+
* - Verifies that `point_step` is non-zero.
266+
*
267+
* 3. **Dense (organized) cloud requirement**
268+
* - Warns if `is_dense` is false.
269+
* - Currently does *not* fail validation, but will become a hard error
270+
* starting **July 2026**.
271+
*
272+
* 4. **Point field layout compatibility**
273+
* - Ensures the point data layout is compatible with `PointXYZ`
274+
* (fields must begin with `x`, `y`, `z` of type `FLOAT32`).
275+
*
276+
* 5. **Row step consistency**
277+
* - Validates that `row_step == width * point_step`.
278+
* - Currently logs a warning on mismatch.
279+
* - Will become a hard error starting **July 2026**.
280+
*
281+
* 6. **Data buffer size consistency**
282+
* - Ensures `data.size() == height * row_step`.
283+
*
284+
* @param cloud
285+
* Shared pointer to the input PointCloud2 message to validate.
286+
*
287+
* @param logger
288+
* ROS 2 logger used for emitting validation warnings.
289+
*
290+
* @param clock
291+
* ROS 2 clock used for throttled logging.
292+
*
293+
* @return true
294+
* If the point cloud passes all mandatory validation checks.
295+
*
296+
* @return false
297+
* If a critical validation failure is detected (e.g., null pointer,
298+
* incompatible data layout, or inconsistent buffer size).
299+
*
300+
* @note
301+
* - All warnings are throttled with a fixed duration of 5000 ms.
302+
* - Some warnings are marked as *future errors* and are expected to
303+
* become fatal after July 2026.
304+
*
305+
* @warning
306+
* This function assumes consumers expect an XYZ-only point layout.
307+
* Additional fields (e.g., intensity, color) are allowed only if they
308+
* appear *after* the XYZ fields and do not alter the layout compatibility.
309+
*/
310+
static bool is_valid(
311+
const PointCloud2ConstPtr & cloud, const rclcpp::Logger & logger, rclcpp::Clock & clock)
248312
{
249-
if (cloud->width * cloud->height * cloud->point_step != cloud->data.size()) {
250-
RCLCPP_WARN(
251-
this->get_logger(),
252-
"Invalid PointCloud (data = %zu, width = %d, height = %d, step = %d) with stamp %f, "
253-
"and frame %s received!",
254-
cloud->data.size(), cloud->width, cloud->height, cloud->point_step,
255-
rclcpp::Time(cloud->header.stamp).seconds(), cloud->header.frame_id.c_str());
313+
static constexpr rcutils_duration_value_t throttle_duration_ms = 5000;
314+
315+
if (!cloud) {
316+
RCLCPP_WARN_THROTTLE(
317+
logger, clock, throttle_duration_ms, "Invalid PointCloud: Null pointer received.");
318+
return false;
319+
}
320+
321+
// Check: Point Step
322+
if (cloud->point_step == 0) {
323+
RCLCPP_WARN_THROTTLE(
324+
logger, clock, throttle_duration_ms,
325+
"Invalid PointCloud: point_step is 0. "
326+
"Frame: '%s', Stamp: %d.%09u",
327+
cloud->header.frame_id.c_str(), cloud->header.stamp.sec, cloud->header.stamp.nanosec);
328+
return false;
329+
}
330+
331+
// Check: Only accept organized (dense) point clouds
332+
if (!cloud->is_dense) {
333+
RCLCPP_WARN_THROTTLE(
334+
logger, clock, throttle_duration_ms,
335+
"Invalid PointCloud: is_dense is false. "
336+
"The point cloud should be organized (dense) and contain only valid points. "
337+
"This will be an ERROR starting in 2026 July"
338+
"Frame: '%s', Stamp: %d.%09u",
339+
cloud->header.frame_id.c_str(), cloud->header.stamp.sec, cloud->header.stamp.nanosec);
340+
// TODO(mfc): return false; After 2026 July
341+
}
342+
343+
// Check: Point field layout compatibility
344+
if (!utils::is_data_layout_compatible_with_point_xyz(*cloud)) {
345+
RCLCPP_WARN_THROTTLE(
346+
logger, clock, throttle_duration_ms,
347+
"The pointcloud layout is not compatible with PointXYZ. Aborting. "
348+
"Make sure Point fields start with x, y, z as FLOAT32.");
349+
return false;
350+
}
351+
352+
// Check: Row step consistency
353+
size_t expected_row_step =
354+
static_cast<size_t>(cloud->width) * static_cast<size_t>(cloud->point_step);
355+
356+
if (expected_row_step != static_cast<size_t>(cloud->row_step)) {
357+
RCLCPP_WARN_THROTTLE(
358+
logger, clock, throttle_duration_ms,
359+
"Invalid PointCloud: row_step mismatch. "
360+
"Expected: %zu (width %u * point_step %u), Got: %u. "
361+
"Frame: '%s', Stamp: %d.%09u"
362+
" Please fill in the `cloud->row_step` field accordingly."
363+
" This will be an ERROR starting in 2026 July",
364+
expected_row_step, cloud->width, cloud->point_step, cloud->row_step,
365+
cloud->header.frame_id.c_str(), cloud->header.stamp.sec, cloud->header.stamp.nanosec);
366+
// TODO(mfc): return false; After 2026 July
367+
}
368+
369+
// Check: Data buffer size consistency
370+
size_t expected_data_size = static_cast<size_t>(cloud->height) * expected_row_step;
371+
372+
if (expected_data_size != cloud->data.size()) {
373+
RCLCPP_WARN_THROTTLE(
374+
logger, clock, throttle_duration_ms,
375+
"Invalid PointCloud: data size mismatch. "
376+
"Expected: %zu (height %u * row_step %u), Got: %zu. "
377+
"Frame: '%s', Stamp: %d.%09u",
378+
expected_data_size, cloud->height, cloud->row_step, cloud->data.size(),
379+
cloud->header.frame_id.c_str(), cloud->header.stamp.sec, cloud->header.stamp.nanosec);
256380
return false;
257381
}
382+
258383
return true;
259384
}
260385

sensing/autoware_pointcloud_preprocessor/include/autoware/pointcloud_preprocessor/utility/memory.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,40 @@ bool is_data_layout_compatible_with_point_xyzircaedt(
5757
* is to say whether you can memcpy from the PointCloud2 data buffer to a PointXYZIRCAEDT */
5858
bool is_data_layout_compatible_with_point_xyzircaedt(const sensor_msgs::msg::PointCloud2 & input);
5959

60+
/**
61+
* @brief Check whether a PointField sequence is compatible with a PointXYZ memory layout.
62+
*
63+
* This function verifies that the first three fields describe a tightly packed
64+
* PointXYZ prefix:
65+
* - Field 0: "x", FLOAT32, count = 1, offset = offsetof(PointXYZ, x)
66+
* - Field 1: "y", FLOAT32, count = 1, offset = offsetof(PointXYZ, y)
67+
* - Field 2: "z", FLOAT32, count = 1, offset = offsetof(PointXYZ, z)
68+
*
69+
* Additional fields after X, Y, and Z are permitted and are not inspected.
70+
*
71+
* @param fields Vector of PointField descriptors from a PointCloud2 message.
72+
* @return true if the layout begins with a compatible PointXYZ prefix; false otherwise.
73+
*/
74+
bool is_data_layout_compatible_with_point_xyz(
75+
const std::vector<sensor_msgs::msg::PointField> & fields);
76+
77+
/**
78+
* @brief Check whether a PointCloud2 message is compatible with a PointXYZ memory layout.
79+
*
80+
* This is a convenience overload that inspects the PointCloud2::fields array
81+
* and applies the same rules as
82+
* is_data_layout_compatible_with_point_xyz(const std::vector<PointField>&).
83+
*
84+
* The layout is considered compatible if the first three fields correspond to
85+
* a tightly packed PointXYZ (x, y, z as FLOAT32 with correct offsets). Any
86+
* additional fields are allowed.
87+
*
88+
* @param input PointCloud2 message to inspect.
89+
* @return true if the PointCloud2 layout begins with a compatible PointXYZ prefix;
90+
* false otherwise.
91+
*/
92+
bool is_data_layout_compatible_with_point_xyz(const sensor_msgs::msg::PointCloud2 & input);
93+
6094
} // namespace autoware::pointcloud_preprocessor::utils
6195

6296
#endif // AUTOWARE__POINTCLOUD_PREPROCESSOR__UTILITY__MEMORY_HPP_

sensing/autoware_pointcloud_preprocessor/src/filter.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ autoware::pointcloud_preprocessor::Filter::filter_param_callback(
227227
void autoware::pointcloud_preprocessor::Filter::input_indices_callback(
228228
const PointCloud2ConstPtr cloud, const PointIndicesConstPtr indices)
229229
{
230-
if (!is_valid(cloud)) {
230+
if (!is_valid(cloud, this->get_logger(), *this->get_clock())) {
231231
RCLCPP_ERROR(this->get_logger(), "[input_indices_callback] Invalid input!");
232232
return;
233233
}
@@ -389,7 +389,7 @@ void autoware::pointcloud_preprocessor::Filter::faster_input_indices_callback(
389389
return;
390390
}
391391

392-
if (!is_valid(cloud)) {
392+
if (!is_valid(cloud, this->get_logger(), *this->get_clock())) {
393393
RCLCPP_ERROR(this->get_logger(), "[input_indices_callback] Invalid input!");
394394
return;
395395
}

sensing/autoware_pointcloud_preprocessor/src/utility/memory.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,45 @@ bool is_data_layout_compatible_with_point_xyzircaedt(const sensor_msgs::msg::Poi
234234
return is_data_layout_compatible_with_point_xyzircaedt(input.fields);
235235
}
236236

237+
bool is_data_layout_compatible_with_point_xyz(
238+
const std::vector<sensor_msgs::msg::PointField> & fields)
239+
{
240+
if (fields.size() < 3) {
241+
return false;
242+
}
243+
244+
// Expected PointXYZ layout:
245+
// struct { float x; float y; float z; }
246+
constexpr size_t offset_x = 0;
247+
constexpr size_t offset_y = sizeof(float);
248+
constexpr size_t offset_z = sizeof(float) * 2;
249+
250+
bool same_layout = true;
251+
252+
const auto & field_x = fields[0];
253+
same_layout &= field_x.name == "x";
254+
same_layout &= field_x.offset == offset_x;
255+
same_layout &= field_x.datatype == sensor_msgs::msg::PointField::FLOAT32;
256+
same_layout &= field_x.count == 1;
257+
258+
const auto & field_y = fields[1];
259+
same_layout &= field_y.name == "y";
260+
same_layout &= field_y.offset == offset_y;
261+
same_layout &= field_y.datatype == sensor_msgs::msg::PointField::FLOAT32;
262+
same_layout &= field_y.count == 1;
263+
264+
const auto & field_z = fields[2];
265+
same_layout &= field_z.name == "z";
266+
same_layout &= field_z.offset == offset_z;
267+
same_layout &= field_z.datatype == sensor_msgs::msg::PointField::FLOAT32;
268+
same_layout &= field_z.count == 1;
269+
270+
return same_layout;
271+
}
272+
273+
bool is_data_layout_compatible_with_point_xyz(const sensor_msgs::msg::PointCloud2 & input)
274+
{
275+
return is_data_layout_compatible_with_point_xyz(input.fields);
276+
}
277+
237278
} // namespace autoware::pointcloud_preprocessor::utils

0 commit comments

Comments
 (0)