Repository navigation
Add ROS1 bag tools and canonical layout: bag_to_laz, laz_to_bag - #582
Draft
michalpelka wants to merge 2 commits into
Draft
michalpelka wants to merge 2 commits into
michalpelka wants to merge 2 commits into
Conversation
ROS1 bags join the MCAP <-> session pattern, without a ROS installation: bag_to_laz is mcap_to_laz with a ROS1 reader, laz_to_bag is laz_to_mcap with a ROS1 writer (same sources, one define each). The session keeps the lidar ring, and the canonical layout is one more --lidar-type. - rosbags/Ros1Bag: ROS1 bag 2.0 reader (none/bz2/lz4 chunks) and writer (uncompressed chunks + index), PointCloud2/Imu codecs, and Ros1BagFileReader/Ros1BagFileWriter with the McapFileReader/Writer APIs (topic auto-detection, PointCloud2 and livox_ros_driver CustomMsg). - cdr_serializer: decodePc2Points() split out of decodePc2(), shared by the MCAP and ROS1 readers. - McapWriter: layouts exposed as pointCloudFields()/pointCloudPointStep()/ packPointCloud() so both formats write identical records; new PointCloudLayout::Canonical (x y z intensity, ring u16, time f32, t u32). Its float32 `time` is what FAST-LIO/LIO-SAM Velodyne point types match. - MandeyeSessionWriter: optional `ring` as a uint16 LAS extra-bytes attribute (user_data stays the lidar id, point_source_ID stays free); load_point_cloud() can return it through an optional rings output. - mcap_to_laz/bag_to_laz: --imu-rotation (IMU into the lidar frame) and --imu-acc-unit g|ms2; sessions are written with the ring. - laz_to_mcap/laz_to_bag: --lidar-type canonical, the ring from the session, points sorted by time then ring, --imu-acc-unit g|ms2. Defaults keep the existing behaviour (IMU copied as g). - Tests: ROS1 codecs and bag round-trips, ROS1 file reader/writer (canonical layout, Livox CustomMsg), session ring attribute, canonical MCAP layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGZUAwWykveANwv2o9sxWD
Every positional argument before the session dir is an input, read in the order given into one session -- for recordings split into parts (Oxford Spires ships some sequences as several bags). The chunk grid is absolute, so a chunk that straddles two inputs stays one chunk, and IMU samples are collected from every input. All inputs must resolve to the same lidar and IMU topics. Checked on Oxford Spires keble-college-02: the canonical bag split in two mid-chunk imports byte-identical (16 LAZ + 16 IMU csv) to the unsplit bag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGZUAwWykveANwv2o9sxWD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ROS1 bags without ROS:
bag_to_laz/laz_to_bag, ring in sessions, canonical layoutROS1 bags join the existing MCAP ↔ session tools, and no ROS installation is needed:
mcap_to_laz(existing)bag_to_laz(new)mcap_to_laz, with a ROS1 reader (-DHDMAPPING_IMPORT_ROS1_BAG)laz_to_mcap(existing)--lidar-type canonical; IMU unit optionlaz_to_bag(new)laz_to_mcap, with a ROS1 writer (-DHDMAPPING_EXPORT_ROS1_BAG)The session stays the hub, so any of the four tools can be chained with any other. The "canonical" bag for the benchmark's spinning-lidar branch is one more
--lidar-type.Changes
rosbags/Ros1Bag:PointCloud2andImucodecs.Ros1BagFileReaderandRos1BagFileWriter, with the same API asMcapFileReaderandMcapFileWriter. That means the same options, topic auto-detection,McapPoint/McapImuSamplestructs and layouts.livox_ros_driver/CustomMsg, with ring = line, like the MCAP reader.cdr_serializer.hpp:decodePc2Points()is split out ofdecodePc2(). The MCAP and ROS1 readers share it, so presets, time-field rules and warnings are identical.decodePc2()'s behaviour is unchanged.McapWriter:pointCloudFields(),pointCloudPointStep()andpackPointCloud(), so MCAP and ROS1 write byte-identical records. Existing layouts are unchanged; the tests confirm it.PointCloudLayout::Canonical:x y z intensity(float32),ring(uint16),time(float32 s) andt(uint32 ns), relative to the first point.time: FAST-LIO and LIO-SAM Velodyne point types declarefloat time, and PCL matches fields by name and type. They read the existingVelodynelayout's float64timeas 0.MandeyeSessionWriter(dir, with_rgb, with_ring)storesMcapPoint::ringas a uint16 LAS extra-bytes attribute namedring.user_datastays the lidar id andpoint_source_IDstays free, so standard LAS semantics are kept. PDAL and CloudCompare show it as aringfield.load_point_cloud()takes an optionalstd::vector<uint16_t>* rings, filled for exactly the points it keeps; it holds 0s when the file has no attribute. Existing callers are unaffected.mcap_to_laz,bag_to_laz):bag_to_laz part0.bag part1.bag … session/reads them in the order given into one session, for recordings split into parts. Chunks stay on the absolute 20 s grid, so a chunk straddling two files stays one chunk. All inputs must resolve to the same lidar and IMU topics.--imu-rotation qx qy qz qwrotates the IMU into the lidar frame, the frame HDMapping assumes.--imu-acc-unit g|ms2gives the bag's unit. The defaultgcopies values as before;ms2divides by 9.80665.laz_to_mcap,laz_to_bag):--lidar-type canonical, with the ring taken from the session.--imu-acc-unit g|ms2gives the output unit. The defaultgkeeps the existing behaviour;ms2writes m/s² assensor_msgs/Imudefines it.Usage: a ROS1 Hesai bag to the canonical bag
Verification
hdmapping_rosbags_testspasses: 38 test cases, 29,896 assertions, 0 failures. The new tests cover:Ros1BagFileWriter→Ros1BagFileReadercanonical round trip, with topics auto-detected and no empty serial-number topic;ringattribute (VLR type and name, values,user_dataandpoint_source_IDuntouched);2024-03-12-keble-college-02(Hesai QT64 and Alphasense IMU; 5.1 GB ROS1 bag with bz2 chunks):bag_to_laz: 16 chunks, all 182,696,017 points from 3007 messages, 119,865 IMU samples. It took 533 s and 33 MB of memory. The IMU csv matches an independent Python conversion (rotation and g) to float precision.laz_to_bagandlaz_to_mcap(canonical): 3001 messages at 10 Hz, all points and IMU samples. Each took about 60 s and 1.6 GB of memory.rosbag info(ROS Noetic) shows standard md5sums and a valid index.rosbags,mcap-ros2-support):timeis sorted and starts at 0;tequalstime × 10⁹;lidarframe;Known limitations
IMU lever arm is not applied.
--imu-rotationturns the IMU into the lidar frame, but the accelerometer still measures at the IMU's own origin. At the lidar origin, r away, the specific force isf_imu + α × r + ω × (ω × r). For Oxford Spires r is 8.6 cm: about 0.09 m/s² at 1 rad/s, and about 0.4 m/s² during a 5 rad/s² turn. Fixing it needs a lever-arm option and angular acceleration taken from the gyro over a short window, because a raw difference amplifies noise. Gyro values are unaffected.No progress output while reading a bag.
bag_to_lazandmcap_to_lazlog only when a pass ends. On a 5.1 GB bz2 bag that's two silent passes of about 4.5 min each, IMU first and then lidar, and it looks stuck. Both formats know their message counts up front: MCAP from its statistics record, and ROS1 from the chunk-info records of its index, which the reader currently skips. That's enough for a percentage and an ETA. The same import could also be faster: one pass instead of two, and chunks decompressed in parallel.IMU units are a flag, not a rule.
sensor_msgs/Imuis m/s² by definition and a session is in g, so each direction has exactly one correct conversion.--imu-acc-unitdefaults tog(copy as is) on both import and export, as the tools did before: a standard ROS bag imported with defaults is off by 9.81, andlaz_to_mcap/laz_to_bagwrite g intosensor_msgs/Imu. The canonical pipeline passesms2on both sides. A fix would always convert, and auto-detect old files in g from |a| at rest.Sweep boundaries: the session doesn't keep the sensor's own sweep boundaries, so
--msg_hzcuts on an absolute 1/hz grid. For a 10 Hz spinning lidar, each message still covers about 360°. The first message is short if the recording starts partway through a bin.Import speed:
bag_to_lazreads the bag twice (IMU pass, then lidar pass), asmcap_to_lazdoes. With bz2 chunks, decompression dominates.MCAP on Humble: no Humble image used by the benchmark has
rosbag2_storage_mcap, soros2 bag playthere needs that plugin, or a conversion to sqlite3.lz4 chunks follow the LZ4 frame format that
roslz4writes. They're tested only indirectly; the real-data run uses bz2. On Windows the lz4 path is compiled out, like themcaptarget's LZ4 support.Formatting:
clang-formatwasn't available, so formatting follows the neighbouring files by hand.🤖 Generated with Claude Code