├── launch
├── debug.conf
├── viz.launch
├── example.launch
├── driver.launch
└── decoder.launch
├── .clang-format
├── .gitignore
├── package.xml
├── CMakeLists.txt
├── .cmake-format.yaml
├── .clang-tidy
├── LICENSE
├── cmake
└── CompilerWarnings.cmake
├── README.md
└── src
├── viz.cpp
├── lidar.h
├── lidar.cpp
├── driver.cpp
└── decoder.cpp
/launch/debug.conf:
--------------------------------------------------------------------------------
1 | log4j.logger.ros.ouster_decoder=DEBUG
2 |
--------------------------------------------------------------------------------
/.clang-format:
--------------------------------------------------------------------------------
1 | BasedOnStyle: Google
2 | BinPackArguments: false
3 | BinPackParameters: false
4 | PointerAlignment: Left
5 | DerivePointerAlignment: false
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Prerequisites
2 | *.d
3 |
4 | # Compiled Object files
5 | *.slo
6 | *.lo
7 | *.o
8 | *.obj
9 |
10 | # Precompiled Headers
11 | *.gch
12 | *.pch
13 |
14 | # Compiled Dynamic libraries
15 | *.so
16 | *.dylib
17 | *.dll
18 |
19 | # Fortran module files
20 | *.mod
21 | *.smod
22 |
23 | # Compiled Static libraries
24 | *.lai
25 | *.la
26 | *.a
27 | *.lib
28 |
29 | # Executables
30 | *.exe
31 | *.out
32 | *.app
33 |
34 | build/
35 | .vscode/
36 | ouster_decoder.*
37 |
--------------------------------------------------------------------------------
/launch/viz.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/launch/example.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ouster_decoder
4 | 0.1.0
5 | Ouster decoder
6 | Chao Qu
7 | BSD
8 | catkin
9 |
10 | cv_bridge
11 | image_transport
12 | pcl_ros
13 | sensor_msgs
14 |
15 | ouster_ros
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.15)
2 | project(ouster_decoder)
3 |
4 | set(CMAKE_CXX_STANDARD 17)
5 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
6 | include(CompilerWarnings)
7 |
8 | find_package(catkin REQUIRED COMPONENTS cv_bridge image_transport pcl_ros
9 | sensor_msgs ouster_ros)
10 | catkin_package()
11 |
12 | add_executable(ouster_decoder src/lidar.cpp src/decoder.cpp)
13 | target_include_directories(ouster_decoder PRIVATE src ${catkin_INCLUDE_DIRS})
14 | target_link_libraries(ouster_decoder PRIVATE ${catkin_LIBRARIES})
15 | enable_warnings(ouster_decoder)
16 |
17 | add_executable(ouster_driver src/driver.cpp)
18 | target_include_directories(ouster_driver PRIVATE ${catkin_INCLUDE_DIRS})
19 | target_link_libraries(ouster_driver PRIVATE ${catkin_LIBRARIES})
20 |
--------------------------------------------------------------------------------
/.cmake-format.yaml:
--------------------------------------------------------------------------------
1 | additional_commands:
2 | catkin_package:
3 | kwargs:
4 | INCLUDE_DIRS: "*"
5 | LIBRARIES: "*"
6 | CATKIN_DEPENDS: "*"
7 | DEPENDS: "*"
8 | CFG_EXTRAS: "*"
9 | cc_library:
10 | flags:
11 | - INTERFACE
12 | kwargs:
13 | HDRS: "*"
14 | SRCS: "*"
15 | COPTS: "*"
16 | DEFINES: "*"
17 | LINKOPTS: "*"
18 | DEPS: "*"
19 | INCDIRS: "*"
20 | cc_binary:
21 | kwargs:
22 | SRCS: "*"
23 | COPTS: "*"
24 | DEFINES: "*"
25 | LINKOPTS: "*"
26 | DEPS: "*"
27 | cc_test:
28 | kwargs:
29 | SRCS: "*"
30 | COPTS: "*"
31 | DEFINES: "*"
32 | LINKOPTS: "*"
33 | DEPS: "*"
34 | cc_bench:
35 | kwargs:
36 | SRCS: "*"
37 | COPTS: "*"
38 | DEFINES: "*"
39 | LINKOPTS: "*"
40 | DEPS: "*"
41 | dangle_parens: false
42 | line_ending: unix
43 | line_width: 80
44 | tab_size: 2
45 |
--------------------------------------------------------------------------------
/.clang-tidy:
--------------------------------------------------------------------------------
1 | ---
2 | # NOTE there must be no spaces before the '-', so put the comma last.
3 | InheritParentConfig: true
4 | Checks: '
5 | bugprone-*,
6 | -bugprone-forward-declaration-namespace,
7 | -bugprone-macro-parentheses,
8 | -bugprone-lambda-function-name,
9 | -bugprone-reserved-identifier,
10 | cppcoreguidelines-*,
11 | -cppcoreguidelines-avoid-magic-numbers,
12 | -cppcoreguidelines-interfaces-global-init,
13 | -cppcoreguidelines-macro-usage,
14 | -cppcoreguidelines-owning-memory,
15 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
16 | -cppcoreguidelines-pro-bounds-constant-array-index,
17 | -cppcoreguidelines-pro-bounds-pointer-arithmetic,
18 | -cppcoreguidelines-pro-type-cstyle-cast,
19 | -cppcoreguidelines-pro-type-reinterpret-cast,
20 | -cppcoreguidelines-pro-type-static-cast-downcast,
21 | -cppcoreguidelines-pro-type-union-access,
22 | -cppcoreguidelines-pro-type-vararg,
23 | -cppcoreguidelines-special-member-functions,
24 | -facebook-hte-RelativeInclude,
25 | hicpp-exception-baseclass,
26 | hicpp-avoid-goto,
27 | modernize-*,
28 | -modernize-concat-nested-namespaces,
29 | -modernize-return-braced-init-list,
30 | -modernize-use-auto,
31 | -modernize-use-default-member-init,
32 | -modernize-use-using,
33 | -modernize-use-trailing-return-type,
34 | performance-*,
35 | -performance-noexcept-move-constructor,
36 | -performance-unnecessary-value-param,
37 | '
38 | HeaderFilterRegex: 'torch/csrc/.*'
39 | AnalyzeTemporaryDtors: false
40 | CheckOptions:
41 | ...
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2018, ouster-lidar
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/launch/driver.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/cmake/CompilerWarnings.cmake:
--------------------------------------------------------------------------------
1 | # from here:
2 | #
3 | # https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md
4 |
5 | function(enable_warnings target)
6 |
7 | set(CLANG_WARNINGS
8 | -Wall
9 | -Wextra # reasonable and standard
10 | # -Wshadow # warn the user if a variable declaration shadows one from a
11 | # parent context
12 | -Wnon-virtual-dtor # warn the user if a class with virtual functions has a
13 | # non-virtual destructor. This helps catch hard to
14 | # track down memory errors
15 | # -Wold-style-cast # warn for c-style casts
16 | -Wcast-align # warn for potential performance problem casts
17 | -Wunused # warn on anything being unused
18 | -Woverloaded-virtual # warn if you overload (not override) a virtual
19 | # function
20 | # -Wpedantic # warn if non-standard C++ is used
21 | # -Wconversion # warn on type conversions that may lose data
22 | # -Wsign-conversion # warn on sign conversions
23 | -Wnull-dereference # warn if a null dereference is detected
24 | # -Wdouble-promotion # warn if float is implicit promoted to double
25 | -Wformat=2 # warn on security issues around functions that format output
26 | # (ie printf)
27 | )
28 |
29 | set(GCC_WARNINGS
30 | ${CLANG_WARNINGS}
31 | -Wmisleading-indentation # warn if indentation implies blocks where blocks
32 | # do not exist
33 | -Wduplicated-cond # warn if if / else chain has duplicated conditions
34 | # -Wduplicated-branches # warn if if / else branches have duplicated code
35 | -Wlogical-op # warn about logical operations being used where bitwise were
36 | # probably wanted
37 | # -Wuseless-cast # warn if you perform a cast to the same type
38 | )
39 |
40 | if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
41 | set(PROJECT_WARNINGS ${CLANG_WARNINGS})
42 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
43 | set(PROJECT_WARNINGS ${GCC_WARNINGS})
44 | else()
45 | message(
46 | AUTHOR_WARNING
47 | "No compiler warnings set for '${CMAKE_CXX_COMPILER_ID}' compiler.")
48 | endif()
49 |
50 | target_compile_options(${target} INTERFACE ${PROJECT_WARNINGS})
51 |
52 | endfunction()
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ouster_decoder
2 |
3 | This decoder is intended to extend the ouster_ros package from https://github.com/ouster-lidar/ouster_example
4 |
5 | It has very low latency (<0.2ms) compared to ouster_example (>3ms), tested on Intel i7 11th gen cpu.
6 |
7 | The decoder only supports LEGACY and single return profile. Currently there's no plan for dual return profile.
8 |
9 | ## Important
10 |
11 | The timestamp of both the cloud and image message is the time of the last column, not the first column.
12 | This is different from the official driver, which uses timestamp of the first column.
13 |
14 | ## Usage
15 |
16 | Run the ouster driver
17 | ```
18 | roslaunch ouster_decoder driver.launch replay:=true/false
19 | ```
20 |
21 | Then run the decoder
22 | ```
23 | roslaunch ouster_decoder decoder.launch
24 | ```
25 |
26 | The driver node is the same as the one from `ouster_example` except that it publishes a string message to topic `/os_node/metadata` that you should also record. When in replay mode, there's no need to specify a metadata file. The metadata file will still be saved in case one forgot to record the metadata message.
27 |
28 | ## Decoder
29 |
30 | The decoder allocates storge for a range image and a point cloud on startup.
31 | When receiving a packet, it fills the range image and the point cloud instead of doing all computations at the end of a sweep. This drastically redueces latency when publishing the cloud/image.
32 |
33 | Each pixel in the range image has the following fields:
34 | ```
35 | struct Data {
36 | float x;
37 | float y;
38 | float z;
39 | uint16_t r16u; // range
40 | uint16_t s16u; // signal
41 | };
42 | ```
43 |
44 | The corresponding point cloud has point type XYZI.
45 |
46 | During each callback, we process the incoming packet and immediately decode and convert to 3d points. When we reach the end of a sweep, the data is directly published without any extra computations.
47 |
48 | Therefore, the publish latency of our decoder is typically less than 0.2ms, while the ouster `os_cloud_node` takes more than 3ms to publish a point cloud.
49 |
50 | ## Data Drops
51 |
52 | Our decoder also checks for missing data. This is very useful for debugging purposes. The decoder tracks the frame and column id of each packet and reports any jumps (both forward and backward) if detected.
53 | The decoder then makes sure that missing data is zeroed out in the message.
54 |
55 | Therefore, one can safely stop and replay a recorded rosbag without restarting the driver and decoder.
56 |
--------------------------------------------------------------------------------
/src/viz.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include
10 | #include
11 |
12 | namespace ouster_decoder {
13 |
14 | namespace it = image_transport;
15 | namespace sm = sensor_msgs;
16 |
17 | cv::Mat ApplyCmap(const cv::Mat& input, int cmap, uint8_t bad_color) {
18 | cv::Mat disp;
19 | input.convertTo(disp, CV_8UC1);
20 | cv::applyColorMap(disp, disp, cmap);
21 | return disp;
22 | }
23 |
24 | class Viz {
25 | public:
26 | explicit Viz(const ros::NodeHandle& pnh);
27 |
28 | void CameraCb(const sm::ImageConstPtr& image_ptr,
29 | const sm::CameraInfoConstPtr& cinfo_ptr);
30 |
31 | private:
32 | ros::NodeHandle pnh_;
33 | it::ImageTransport it_;
34 | it::CameraSubscriber sub_camera_;
35 | std::string cmap_{"gray"};
36 | };
37 |
38 | Viz::Viz(const ros::NodeHandle& pnh) : pnh_{pnh}, it_{pnh} {
39 | sub_camera_ = it_.subscribeCamera("image", 1, &Viz::CameraCb, this);
40 | ROS_INFO_STREAM("Subscribing to: " << sub_camera_.getTopic());
41 | }
42 |
43 | void Viz::CameraCb(const sm::ImageConstPtr& image_ptr,
44 | const sm::CameraInfoConstPtr& cinfo_ptr) {
45 | const auto mat = cv_bridge::toCvShare(image_ptr)->image;
46 | const auto h = mat.rows;
47 | const auto w = mat.cols;
48 |
49 | // Extract range and intensity
50 | const cv::Mat mat_map(mat.rows, mat.cols, CV_16UC(8), mat.data);
51 | cv::Mat range_raw, signal_raw;
52 | cv::extractChannel(mat_map, range_raw, 6);
53 | cv::extractChannel(mat_map, signal_raw, 7);
54 |
55 | auto range_color = ApplyCmap(range_raw / 100, cv::COLORMAP_PINK, 0);
56 | auto signal_color = ApplyCmap(signal_raw / 4, cv::COLORMAP_PINK, 0);
57 |
58 | // set invalid range (0) to black
59 | range_color.setTo(0, range_raw == 0);
60 | signal_color.setTo(0, range_raw == 0);
61 |
62 | cv::Mat signal_half0;
63 | cv::Mat signal_half1;
64 | signal_half0.create(h / 2, w, CV_8UC3);
65 | signal_half1.create(h / 2, w, CV_8UC3);
66 |
67 | for (int r = 0; r < h; ++r) {
68 | const int rr = r / 2;
69 | if (r % 2 == 0) {
70 | signal_color.row(r).copyTo(signal_half0.row(rr));
71 | } else {
72 | signal_color.row(r).copyTo(signal_half1.row(rr));
73 | }
74 | }
75 |
76 | // save raw data to ouster
77 | const int id = image_ptr->header.seq;
78 | const std::vector png_flags = {cv::IMWRITE_PNG_COMPRESSION, 0};
79 | cv::imwrite(fmt::format("/tmp/ouster/range_{:04d}.png", id), range_raw, png_flags);
80 | cv::imwrite(fmt::format("/tmp/ouster/signal_{:04d}.png", id), signal_raw, png_flags);
81 | ROS_INFO_STREAM("Writing message with id: " << id);
82 |
83 | constexpr auto win_flags =
84 | cv::WINDOW_FREERATIO | cv::WINDOW_GUI_EXPANDED | cv::WINDOW_NORMAL;
85 |
86 | cv::namedWindow("range", win_flags);
87 | cv::imshow("range", range_color);
88 |
89 | cv::namedWindow("signal", win_flags);
90 | cv::imshow("signal", signal_color);
91 |
92 | cv::namedWindow("signal_half0", win_flags);
93 | cv::imshow("signal_half0", signal_half0);
94 |
95 | cv::namedWindow("signal_half1", win_flags);
96 | cv::imshow("signal_half1", signal_half1);
97 |
98 | cv::waitKey(1);
99 | }
100 |
101 | } // namespace ouster_decoder
102 |
103 | int main(int argc, char** argv) {
104 | ros::init(argc, argv, "os_viz");
105 |
106 | ouster_decoder::Viz node(ros::NodeHandle("~"));
107 | ros::spin();
108 |
109 | return 0;
110 | }
111 |
--------------------------------------------------------------------------------
/launch/decoder.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/lidar.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include "ouster_ros/os_ros.h"
8 |
9 | namespace ouster_decoder {
10 |
11 | inline constexpr double Deg2Rad(double deg) { return deg * M_PI / 180.0; }
12 |
13 | /// @brief image data in scan
14 | struct ImageData {
15 | float x{};
16 | float y{};
17 | float z{};
18 | uint16_t r16u{};
19 | uint16_t s16u{};
20 |
21 | void set_range(float range, double scale) noexcept {
22 | r16u = static_cast(
23 | std::min(range * scale,
24 | static_cast(std::numeric_limits::max())));
25 | }
26 |
27 | void set_bad() noexcept {
28 | x = y = z = std::numeric_limits::quiet_NaN();
29 | r16u = s16u = 0;
30 | }
31 | };
32 |
33 | static_assert(sizeof(ImageData) == sizeof(float) * 4,
34 | "Size of ImageData must be 4 floats (16 bytes)");
35 |
36 | /// @brief Stores SensorInfo from ouster with some other useful data
37 | struct LidarModel {
38 | LidarModel() = default;
39 | explicit LidarModel(const std::string& metadata);
40 |
41 | /// @brief whether this model is ready
42 | bool Initialized() const { return !altitudes.empty(); }
43 |
44 | int rows{}; // number of beams
45 | int cols{}; // cols of a full scan
46 | int freq{}; // frequency
47 | double dt_col{}; // delta time between two columns [s]
48 | double dt_packet{}; // delta time between two packets [s]
49 | double d_azimuth{}; // delta angle between two columns [rad]
50 | double beam_offset{}; // distance between beam to origin
51 | std::vector altitudes; // altitude angles, high to low [rad]
52 | std::vector azimuths; // azimuths offset angles [rad]
53 | ouster_ros::sensor::sensor_info info; // sensor info
54 | ouster_ros::sensor::packet_format const* pf{nullptr}; // packet format
55 |
56 | const auto& pixel_shifts() const noexcept {
57 | return info.format.pixel_shift_by_row;
58 | }
59 |
60 | /// @brief Convert lidar range data to xyz
61 | /// @details see software manual 3.1.2 Lidar Range to XYZ
62 | ///
63 | /// y r
64 | /// ^ / -> rotate clockwise
65 | /// | /
66 | /// | /
67 | /// |/ theta
68 | /// o ---------> x (connector)
69 | ///
70 | Eigen::Vector3f ToPoint(float range, float theta_enc, int row) const;
71 |
72 | /// @brief Return a unique id for a measurement
73 | int Uid(int fid, int mid) const noexcept { return fid * cols + mid; }
74 |
75 | /// @brief Update camera info with this model
76 | void UpdateCameraInfo(sensor_msgs::CameraInfo& cinfo) const;
77 | };
78 |
79 | /// @brief Stores data for a (sub)scan
80 | struct LidarScan {
81 | int icol{0}; // column index
82 | int iscan{0}; // subscan index
83 | int prev_uid{-1}; // previous uid
84 | double min_range{0.25}; // minimum range
85 | double max_range{256.0}; // maximum range
86 | double range_scale{}; // raw range is uint_16, divide by scale to meter
87 | bool destagger{false}; // whether to destagger scan
88 |
89 | // cv::Mat image;
90 | sensor_msgs::ImagePtr image_ptr; // each pixel is ImageData
91 | sensor_msgs::PointCloud2 cloud; // each point is (x,y,z,intensity)
92 | std::vector times; // all time stamps [nanosecond]
93 |
94 | float* CloudPtr(int r, int c) {
95 | const auto i = r * cols() + c;
96 | return reinterpret_cast(cloud.data.data() + i * cloud.point_step);
97 | }
98 |
99 | ImageData* ImagePtr(int r, int c) {
100 | const auto i = r * cols() + c;
101 | return reinterpret_cast(image_ptr->data.data() +
102 | i * sizeof(ImageData));
103 | }
104 |
105 | int rows() const noexcept { return static_cast(cloud.height); }
106 | int cols() const noexcept { return static_cast(cloud.width); }
107 |
108 | /// @brief whether this scan is full
109 | bool IsFull() const noexcept { return icol >= cols(); }
110 |
111 | /// @brief Starting column of this scan
112 | int StartingCol() const noexcept { return iscan * cols(); }
113 |
114 | /// @brief Detect if there is a jump in the lidar data
115 | /// @return 0 - no jump, >0 - jump forward in time, <0 - jump backward in time
116 | int DetectJump(int uid) noexcept;
117 |
118 | /// @brief Allocate storage for the scan
119 | void Allocate(int rows, int cols);
120 |
121 | /// @brief Hard reset internal counters and prev_uid
122 | void HardReset() noexcept;
123 |
124 | /// @brief Try to reset the internal counters if it is full
125 | void SoftReset(int full_col) noexcept;
126 |
127 | /// @brief Invalidate an entire column
128 | void InvalidateColumn(double dt_col) noexcept;
129 |
130 | /// @brief Decode column
131 | void DecodeColumn(const uint8_t* const col_buf, const LidarModel& model);
132 |
133 | /// @brief Update camera info roi data with this scan
134 | void UpdateCinfo(sensor_msgs::CameraInfo& cinfo) const noexcept;
135 | };
136 |
137 | std::vector MakePointFieldsXYZI() noexcept;
138 |
139 | } // namespace ouster_decoder
140 |
--------------------------------------------------------------------------------
/src/lidar.cpp:
--------------------------------------------------------------------------------
1 | #include "lidar.h"
2 |
3 | #include
4 |
5 | namespace ouster_decoder {
6 |
7 | namespace os = ouster_ros::sensor;
8 |
9 | constexpr float kMmToM = 0.001;
10 | constexpr double kTau = 2 * M_PI;
11 | constexpr float kNaNF = std::numeric_limits::quiet_NaN();
12 |
13 | namespace {
14 |
15 | /// @brief Convert a vector of double from deg to rad
16 | std::vector TransformDeg2Rad(const std::vector& degs) {
17 | std::vector rads;
18 | rads.reserve(degs.size());
19 | for (const auto& deg : degs) {
20 | rads.push_back(Deg2Rad(deg));
21 | }
22 | return rads;
23 | }
24 |
25 | } // namespace
26 |
27 | LidarModel::LidarModel(const std::string& metadata) {
28 | info = os::parse_metadata(metadata);
29 | pf = &os::get_format(info);
30 |
31 | rows = info.beam_altitude_angles.size();
32 | cols = os::n_cols_of_lidar_mode(info.mode);
33 | freq = os::frequency_of_lidar_mode(info.mode);
34 |
35 | dt_col = 1.0 / freq / cols;
36 | d_azimuth = kTau / cols;
37 | dt_packet = dt_col * pf->columns_per_packet;
38 | beam_offset = info.lidar_origin_to_beam_origin_mm * kMmToM;
39 | altitudes = TransformDeg2Rad(info.beam_altitude_angles);
40 | azimuths = TransformDeg2Rad(info.beam_azimuth_angles);
41 | }
42 |
43 | Eigen::Vector3f LidarModel::ToPoint(float range,
44 | float theta_enc,
45 | int row) const {
46 | const float n = beam_offset;
47 | const float d = range - n;
48 | const float phi = altitudes[row];
49 | const float cos_phi = std::cos(phi);
50 | const float theta = theta_enc - azimuths[row];
51 |
52 | return {d * std::cos(theta) * cos_phi + n * std::cos(theta_enc),
53 | d * std::sin(theta) * cos_phi + n * std::sin(theta_enc),
54 | d * std::sin(phi)};
55 | }
56 |
57 | void LidarModel::UpdateCameraInfo(sensor_msgs::CameraInfo& cinfo) const {
58 | cinfo.height = rows;
59 | cinfo.width = cols;
60 | cinfo.distortion_model = info.prod_line;
61 |
62 | // cinfo.D.reserve(altitudes.size() + azimuths.size());
63 | // cinfo.D.insert(cinfo.D.end(), altitudes.begin(), altitudes.end());
64 | // cinfo.D.insert(cinfo.D.end(), azimuths.begin(), azimuths.end());
65 | cinfo.D.reserve(pixel_shifts().size());
66 | cinfo.D.insert(cinfo.D.end(), pixel_shifts().begin(), pixel_shifts().end());
67 |
68 | cinfo.K[0] = dt_col; // time between each column
69 | // cinfo.K[1] = d_azimuth; // radian between each column
70 | // cinfo.K[2] = beam_offset; // distance from center to beam
71 | }
72 |
73 | void LidarScan::Allocate(int rows, int cols) {
74 | // Don't do any work if rows and cols are the same
75 | // image.create(rows, cols, CV_32FC4);
76 | if (!image_ptr) image_ptr = boost::make_shared();
77 |
78 | image_ptr->height = rows;
79 | image_ptr->width = cols;
80 | image_ptr->encoding = "32FC4";
81 | image_ptr->step = cols * sizeof(ImageData);
82 | image_ptr->data.resize(rows * cols * sizeof(ImageData));
83 |
84 | cloud.width = cols;
85 | cloud.height = rows;
86 | cloud.point_step = 16; // xyzi
87 | cloud.row_step = cloud.point_step * cloud.width;
88 | cloud.fields = MakePointFieldsXYZI();
89 | cloud.is_dense = 1u;
90 | cloud.data.resize(rows * cols * cloud.point_step);
91 |
92 | times.clear();
93 | times.resize(cols, 0);
94 | }
95 |
96 | int LidarScan::DetectJump(int uid) noexcept {
97 | int jump = 0;
98 |
99 | if (prev_uid >= 0) {
100 | // Ideally the increment should be 1 hence the jump should be 0
101 | jump = uid - prev_uid - 1;
102 | }
103 |
104 | prev_uid = uid;
105 | return jump;
106 | }
107 |
108 | void LidarScan::HardReset() noexcept {
109 | icol = 0;
110 | iscan = 0;
111 | prev_uid = -1;
112 | }
113 |
114 | void LidarScan::SoftReset(int full_col) noexcept {
115 | // Reset col (usually to 0 but in the rare case that data jumps forward
116 | // it will be non-zero)
117 | icol = icol % cols();
118 |
119 | // Reset scan if we have a full sweep
120 | if (iscan * cols() >= full_col) {
121 | iscan = 0;
122 | }
123 | }
124 |
125 | void LidarScan::InvalidateColumn(double dt_col) noexcept {
126 | for (int irow = 0; irow < static_cast(cloud.height); ++irow) {
127 | auto* ptr = CloudPtr(irow, icol);
128 | ptr[0] = ptr[1] = ptr[2] = kNaNF;
129 | }
130 |
131 | for (int irow = 0; irow < rows(); ++irow) {
132 | auto* ptr = ImagePtr(irow, icol);
133 | ptr->set_bad();
134 | }
135 |
136 | // It is possible that the jump spans two subscans, this will cause the
137 | // first timestamp to be wrong when we publish the data, therefore we need
138 | // to extrapolate timestamp here
139 | times.at(icol) = (icol == 0 ? times.back() : times.at(icol - 1)) +
140 | static_cast(dt_col * 1e9);
141 |
142 | // Move on to next column
143 | ++icol;
144 | }
145 |
146 | void LidarScan::DecodeColumn(const uint8_t* const col_buf,
147 | const LidarModel& model) {
148 | const auto& pf = *model.pf;
149 | const uint64_t t_ns = pf.col_timestamp(col_buf);
150 | const uint16_t mid = pf.col_measurement_id(col_buf);
151 | const uint32_t status = pf.col_status(col_buf) & 0x0001;
152 | // Legacy mode
153 | // const bool col_valid = (status == 0xffffffff);
154 | // const bool col_valid = status;
155 |
156 | // Compute azimuth angle theta0, this should always be valid
157 | // const auto theta_enc = kTau - mid * model_.d_azimuth;
158 | // const float theta_enc = kTau * (1.0f - encoder / 90112.0f);
159 | const float theta_enc = kTau - mid * model.d_azimuth;
160 | times.at(icol) = t_ns;
161 |
162 | for (int ipx = 0; ipx < pf.pixels_per_column; ++ipx) {
163 | // Data to fill
164 | Eigen::Vector3f xyz;
165 | xyz.setConstant(kNaNF);
166 | float r{};
167 | uint16_t s16u{};
168 |
169 | if (status) {
170 | const uint8_t* const px_buf = pf.nth_px(ipx, col_buf);
171 | const auto raw_range = pf.px_range(px_buf);
172 | const float range = raw_range * kMmToM; // used to compute xyz
173 |
174 | if (min_range <= range && range <= max_range) {
175 | xyz = model.ToPoint(range, theta_enc, ipx);
176 | r = xyz.norm(); // we compute range ourselves
177 | s16u = pf.px_signal(px_buf);
178 | }
179 | // s16u += pf.px_ambient(px_buf);
180 | }
181 |
182 | // Now we set cloud and image data
183 | // There is no destagger for cloud, so we update point no matter what
184 | auto* cptr = CloudPtr(ipx, icol);
185 | cptr[0] = xyz.x();
186 | cptr[1] = xyz.y();
187 | cptr[2] = xyz.z();
188 | cptr[3] = static_cast(s16u);
189 |
190 | // However image can be destaggered, and pixel can go out of bound
191 | // add pixel shift to get where the pixel should be
192 | const auto col_shift = model.pixel_shifts()[ipx];
193 | const auto im_col = destagger ? icol + col_shift : icol;
194 |
195 | if (0 <= im_col && im_col < cols()) {
196 | auto* iptr = ImagePtr(ipx, im_col);
197 | iptr->x = xyz.x();
198 | iptr->y = xyz.y();
199 | iptr->z = xyz.z();
200 | iptr->set_range(r, range_scale);
201 | iptr->s16u = s16u;
202 | } else {
203 | auto* iptr = ImagePtr(ipx, im_col % cols());
204 | iptr->set_bad();
205 | }
206 | }
207 |
208 | // Move on to next column
209 | ++icol;
210 | }
211 |
212 | void LidarScan::UpdateCinfo(sensor_msgs::CameraInfo& cinfo) const noexcept {
213 | cinfo.R[0] = range_scale;
214 | cinfo.binning_x = iscan; // index of subscan within a full scan
215 |
216 | // Update camera info roi with curr_scan
217 | auto& roi = cinfo.roi;
218 | roi.x_offset = StartingCol();
219 | roi.y_offset = 0; // this is always 0
220 | roi.width = cols();
221 | roi.height = rows();
222 | roi.do_rectify = destagger;
223 | }
224 |
225 | std::vector MakePointFieldsXYZI() noexcept {
226 | using sensor_msgs::PointField;
227 | std::vector fields;
228 | fields.reserve(4);
229 |
230 | PointField field;
231 | field.name = "x";
232 | field.offset = 0;
233 | field.datatype = PointField::FLOAT32;
234 | field.count = 1;
235 | fields.push_back(field);
236 |
237 | field.name = "y";
238 | field.offset = 4;
239 | field.datatype = PointField::FLOAT32;
240 | field.count = 1;
241 | fields.push_back(field);
242 |
243 | field.name = "z";
244 | field.offset = 8;
245 | field.datatype = PointField::FLOAT32;
246 | field.count = 1;
247 | fields.push_back(field);
248 |
249 | field.name = "intensity";
250 | field.offset = 12;
251 | field.datatype = PointField::FLOAT32;
252 | field.count = 1;
253 | fields.push_back(field);
254 |
255 | return fields;
256 | }
257 |
258 | } // namespace ouster_decoder
259 |
--------------------------------------------------------------------------------
/src/driver.cpp:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018, Ouster, Inc.
3 | * All rights reserved.
4 | *
5 | * @file
6 | * @brief Example node to publish raw sensor output on ROS topics
7 | *
8 | * ROS Parameters
9 | * sensor_hostname: hostname or IP in dotted decimal form of the sensor
10 | * udp_dest: hostname or IP where the sensor will send data packets
11 | * lidar_port: port to which the sensor should send lidar data
12 | * imu_port: port to which the sensor should send imu data
13 | */
14 |
15 | // This is a modified version of ouster_ros/os_node.cpp
16 | // It is intended to have the same behavior as os_node.
17 | // The only difference is that we also advertise the metadata message.
18 |
19 | #include
20 | #include
21 |
22 | #include
23 | #include
24 | #include
25 |
26 | #include "ouster_ros/GetMetadata.h"
27 | #include "ouster_ros/PacketMsg.h"
28 | #include "ouster_ros/os_ros.h"
29 |
30 | using PacketMsg = ouster_ros::PacketMsg;
31 | using OsGetMetadata = ouster_ros::GetMetadata;
32 | namespace sensor = ouster::sensor;
33 |
34 | // fill in values that could not be parsed from metadata
35 | void populate_metadata_defaults(sensor::sensor_info& info,
36 | sensor::lidar_mode specified_lidar_mode) {
37 | if (!info.name.size()) info.name = "UNKNOWN";
38 |
39 | if (!info.sn.size()) info.sn = "UNKNOWN";
40 |
41 | ouster::util::version v = ouster::util::version_of_string(info.fw_rev);
42 | if (v == ouster::util::invalid_version)
43 | ROS_WARN("Unknown sensor firmware version; output may not be reliable");
44 | else if (v < sensor::min_version)
45 | ROS_WARN("Firmware < %s not supported; output may not be reliable",
46 | to_string(sensor::min_version).c_str());
47 |
48 | if (!info.mode) {
49 | ROS_WARN("Lidar mode not found in metadata; output may not be reliable");
50 | info.mode = specified_lidar_mode;
51 | }
52 |
53 | if (!info.prod_line.size()) info.prod_line = "UNKNOWN";
54 |
55 | if (info.beam_azimuth_angles.empty() || info.beam_altitude_angles.empty()) {
56 | ROS_WARN("Beam angles not found in metadata; using design values");
57 | info.beam_azimuth_angles = sensor::gen1_azimuth_angles;
58 | info.beam_altitude_angles = sensor::gen1_altitude_angles;
59 | }
60 | }
61 |
62 | // try to write metadata file
63 | bool write_metadata(const std::string& meta_file, const std::string& metadata) {
64 | std::ofstream ofs;
65 | ofs.open(meta_file);
66 | ofs << metadata << std::endl;
67 | ofs.close();
68 | if (ofs) {
69 | ROS_INFO("Wrote metadata to %s", meta_file.c_str());
70 | } else {
71 | ROS_WARN(
72 | "Failed to write metadata to %s; check that the path is valid. If "
73 | "you provided a relative path, please note that the working "
74 | "directory of all ROS nodes is set by default to $ROS_HOME",
75 | meta_file.c_str());
76 | return false;
77 | }
78 | return true;
79 | }
80 |
81 | int connection_loop(ros::NodeHandle& nh,
82 | sensor::client& cli,
83 | const sensor::sensor_info& info) {
84 | auto lidar_packet_pub = nh.advertise("lidar_packets", 1280);
85 | auto imu_packet_pub = nh.advertise("imu_packets", 100);
86 |
87 | auto pf = sensor::get_format(info);
88 |
89 | PacketMsg lidar_packet, imu_packet;
90 | lidar_packet.buf.resize(pf.lidar_packet_size + 1);
91 | imu_packet.buf.resize(pf.imu_packet_size + 1);
92 |
93 | while (ros::ok()) {
94 | auto state = sensor::poll_client(cli);
95 | if (state == sensor::EXIT) {
96 | ROS_INFO("poll_client: caught signal, exiting");
97 | return EXIT_SUCCESS;
98 | }
99 | if (state & sensor::CLIENT_ERROR) {
100 | ROS_ERROR("poll_client: returned error");
101 | return EXIT_FAILURE;
102 | }
103 | if (state & sensor::LIDAR_DATA) {
104 | if (sensor::read_lidar_packet(cli, lidar_packet.buf.data(), pf))
105 | lidar_packet_pub.publish(lidar_packet);
106 | }
107 | if (state & sensor::IMU_DATA) {
108 | if (sensor::read_imu_packet(cli, imu_packet.buf.data(), pf))
109 | imu_packet_pub.publish(imu_packet);
110 | }
111 | ros::spinOnce();
112 | }
113 | return EXIT_SUCCESS;
114 | }
115 |
116 | void advertise_service(ros::NodeHandle& nh,
117 | ros::ServiceServer& srv,
118 | const sensor::sensor_info& info) {
119 | auto metadata = sensor::to_string(info);
120 | ROS_INFO("Using lidar_mode: %s", sensor::to_string(info.mode).c_str());
121 | ROS_INFO("%s sn: %s firmware rev: %s",
122 | info.prod_line.c_str(),
123 | info.sn.c_str(),
124 | info.fw_rev.c_str());
125 | if (srv) {
126 | // shutdown first and readvertise
127 | ROS_INFO("Shutting down %s service and re-advertise",
128 | srv.getService().c_str());
129 | srv.shutdown();
130 | }
131 | srv = nh.advertiseService(
132 | "get_metadata",
133 | [metadata](OsGetMetadata::Request&, OsGetMetadata::Response& res) {
134 | if (metadata.empty()) return false;
135 | res.metadata = metadata;
136 | return true;
137 | });
138 | ROS_INFO("Advertise service to %s", srv.getService().c_str());
139 | }
140 |
141 | int main(int argc, char** argv) {
142 | ros::init(argc, argv, "os_node");
143 | ros::NodeHandle nh("~");
144 | // Extra stuff
145 | ros::Publisher pub_meta;
146 | ros::Subscriber meta_sub;
147 | ros::ServiceServer srv;
148 |
149 | // empty indicates "not set" since roslaunch xml can't optionally set params
150 | auto hostname = nh.param("sensor_hostname", std::string{});
151 | auto udp_dest = nh.param("udp_dest", std::string{});
152 | auto lidar_port = nh.param("lidar_port", 0);
153 | auto imu_port = nh.param("imu_port", 0);
154 | auto replay = nh.param("replay", false);
155 | auto lidar_mode_arg = nh.param("lidar_mode", std::string{});
156 | auto timestamp_mode_arg = nh.param("timestamp_mode", std::string{});
157 |
158 | std::string udp_profile_lidar_arg;
159 | nh.param("udp_profile_lidar", udp_profile_lidar_arg, "");
160 |
161 | // optional udp_profile_lidar;
162 | // if (udp_profile_lidar_arg.size()) {
163 | // if (replay)
164 | // ROS_WARN("UDP Profile Lidar set in replay mode. Will be ignored.");
165 |
166 | // // set lidar profile from param
167 | // udp_profile_lidar =
168 | // sensor::udp_profile_lidar_of_string(udp_profile_lidar_arg);
169 | // if (!udp_profile_lidar) {
170 | // ROS_ERROR("Invalid udp profile lidar: %s",
171 | // udp_profile_lidar_arg.c_str()); return EXIT_FAILURE;
172 | // }
173 | // }
174 |
175 | // set lidar mode from param
176 | sensor::lidar_mode lidar_mode = sensor::MODE_UNSPEC;
177 | if (lidar_mode_arg.size()) {
178 | if (replay) ROS_WARN("Lidar mode set in replay mode. Will be ignored");
179 |
180 | lidar_mode = sensor::lidar_mode_of_string(lidar_mode_arg);
181 | if (!lidar_mode) {
182 | ROS_ERROR("Invalid lidar mode %s", lidar_mode_arg.c_str());
183 | return EXIT_FAILURE;
184 | }
185 | }
186 |
187 | // set timestamp mode from param
188 | sensor::timestamp_mode timestamp_mode = sensor::TIME_FROM_UNSPEC;
189 | if (timestamp_mode_arg.size()) {
190 | if (replay) ROS_WARN("Timestamp mode set in replay mode. Will be ignored");
191 |
192 | timestamp_mode = sensor::timestamp_mode_of_string(timestamp_mode_arg);
193 | if (!timestamp_mode) {
194 | ROS_ERROR("Invalid timestamp mode %s", timestamp_mode_arg.c_str());
195 | return EXIT_FAILURE;
196 | }
197 | }
198 |
199 | // fall back to metadata file name based on hostname, if available
200 | auto meta_file = nh.param("metadata", std::string{});
201 | // if (!meta_file.size() && hostname.size()) meta_file = hostname + ".json";
202 |
203 | if (!replay && (!hostname.size() || !udp_dest.size())) {
204 | ROS_ERROR("Must specify both hostname and udp destination");
205 | return EXIT_FAILURE;
206 | }
207 |
208 | // ROS_INFO("Client version: %s", ouster::CLIENT_VERSION_FULL);
209 |
210 | if (replay) {
211 | ROS_INFO("Running in replay mode");
212 |
213 | auto meta_cb = [&nh, &srv](const std_msgs::String& str_msg) {
214 | auto info = sensor::parse_metadata(str_msg.data);
215 | advertise_service(nh, srv, info);
216 | };
217 |
218 | // populate info for config service
219 | try {
220 | if (meta_file.empty()) {
221 | meta_sub = nh.subscribe(
222 | "metadata", 1, meta_cb);
223 | ROS_INFO("metadata is empty, subscribing to %s",
224 | meta_sub.getTopic().c_str());
225 |
226 | } else {
227 | ROS_INFO("metadata file is given, using %s", meta_file.c_str());
228 | auto info = sensor::metadata_from_json(meta_file);
229 | advertise_service(nh, srv, info);
230 | }
231 |
232 | // just serve config service
233 | ros::spin();
234 | return EXIT_SUCCESS;
235 | } catch (const std::runtime_error& e) {
236 | ROS_ERROR("Error when running in replay mode: %s", e.what());
237 | }
238 | } else {
239 | ROS_INFO("Waiting for sensor %s to initialize ...", hostname.c_str());
240 | ROS_INFO("Sending data to %s", udp_dest.c_str());
241 |
242 | // use no-config version of init_client to allow for random ports
243 | // auto cli = sensor::init_client(hostname, lidar_port, imu_port);
244 | auto cli = sensor::init_client(
245 | hostname, udp_dest, lidar_mode, timestamp_mode, lidar_port, imu_port);
246 |
247 | if (!cli) {
248 | ROS_ERROR("Failed to initialize sensor at: %s", hostname.c_str());
249 | return EXIT_FAILURE;
250 | }
251 | ROS_INFO("Sensor initialized successfully");
252 |
253 | // write metadata file to cwd (usually ~/.ros)
254 | auto metadata = sensor::get_metadata(*cli);
255 | if (meta_file.empty()) {
256 | meta_file = hostname + ".json"; // hostname must be nonempty
257 | ROS_INFO("meta_file not given, use: %s", meta_file.c_str());
258 | }
259 |
260 | // write metadata file. If metadata_path is relative, will use cwd
261 | // (usually ~/.ros)
262 | if (!write_metadata(meta_file, metadata)) {
263 | ROS_ERROR("Exiting because of failure to write metadata path to %s",
264 | meta_file.c_str());
265 | return EXIT_FAILURE;
266 | }
267 |
268 | // populate sensor info
269 | auto info = sensor::parse_metadata(metadata);
270 | populate_metadata_defaults(info, sensor::MODE_UNSPEC);
271 | metadata = to_string(info); // regenerate metadata
272 |
273 | // publish metadata
274 | pub_meta = nh.advertise("metadata", 1, true);
275 | std_msgs::String meta_msg;
276 | meta_msg.data = metadata;
277 | pub_meta.publish(meta_msg);
278 | ROS_INFO("Publish metadata to %s", pub_meta.getTopic().c_str());
279 |
280 | srv = nh.advertiseService(
281 | "get_metadata",
282 | [metadata](OsGetMetadata::Request&, OsGetMetadata::Response& res) {
283 | if (metadata.empty()) return false;
284 | res.metadata = metadata;
285 | return true;
286 | });
287 |
288 | ROS_INFO("Using lidar_mode: %s", sensor::to_string(info.mode).c_str());
289 | ROS_INFO("%s sn: %s firmware rev: %s",
290 | info.prod_line.c_str(),
291 | info.sn.c_str(),
292 | info.fw_rev.c_str());
293 |
294 | // publish packet messages from the sensor
295 | return connection_loop(nh, *cli, info);
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src/decoder.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | #include "lidar.h"
7 | #include "ouster_ros/GetMetadata.h"
8 | #include "ouster_ros/PacketMsg.h"
9 |
10 | namespace ouster_decoder {
11 |
12 | namespace os = ouster_ros::sensor;
13 | namespace sm = sensor_msgs;
14 |
15 | constexpr double kDefaultGravity = 9.807; // [m/s^2] earth gravity
16 |
17 | /// @brief Decoder node
18 | class Decoder {
19 | public:
20 | explicit Decoder(const ros::NodeHandle& pnh);
21 |
22 | // No copy no move
23 | Decoder(const Decoder&) = delete;
24 | Decoder& operator=(const Decoder&) = delete;
25 | Decoder(Decoder&&) = delete;
26 | Decoder& operator=(Decoder&&) = delete;
27 |
28 | /// Callbacks
29 | void LidarPacketCb(const ouster_ros::PacketMsg& lidar_msg);
30 | void ImuPacketCb(const ouster_ros::PacketMsg& imu_msg);
31 |
32 | private:
33 | /// Initialize ros related stuff (frame, publisher, subscriber)
34 | void InitRos();
35 | /// Initialize all parameters
36 | void InitParams();
37 | /// Initialize ouster related stuff
38 | void InitOuster();
39 | void InitModel(const std::string& metadata);
40 | void InitScan(const LidarModel& model);
41 | void SendTransform(const LidarModel& model);
42 |
43 | /// Whether we are still waiting for alignment to mid 0
44 | [[nodiscard]] bool NeedAlign(int mid);
45 |
46 | /// Publish messages
47 | void PublishAndReset();
48 |
49 | /// Record processing time of lidar callback, print warning if it exceeds time
50 | /// between two packets
51 | void Timing(const ros::Time& start) const;
52 |
53 | // ros
54 | ros::NodeHandle pnh_;
55 | image_transport::ImageTransport it_;
56 | ros::Subscriber lidar_sub_, imu_sub_, meta_sub_;
57 | ros::Publisher cloud_pub_, imu_pub_;
58 | ros::Publisher range_pub_, signal_pub_;
59 | image_transport::CameraPublisher camera_pub_;
60 | tf2_ros::StaticTransformBroadcaster static_tf_;
61 | std::string sensor_frame_, lidar_frame_, imu_frame_;
62 |
63 | // data
64 | LidarScan scan_;
65 | LidarModel model_;
66 | sm::CameraInfoPtr cinfo_msg_;
67 |
68 | // params
69 | double gravity_{}; // gravity
70 | bool replay_{false}; // replay mode will reinitialize on jump
71 | bool need_align_{true}; // whether to align scan
72 | double acc_noise_var_{}; // discrete time acc noise variance
73 | double gyr_noise_var_{}; // discrete time gyr noise variance
74 | double vis_signal_scale_{1.0}; // scale signal visualization
75 | };
76 |
77 | Decoder::Decoder(const ros::NodeHandle& pnh) : pnh_(pnh), it_(pnh) {
78 | InitParams();
79 | InitRos();
80 | InitOuster();
81 | }
82 |
83 | void Decoder::InitRos() {
84 | // Subscribers, queue size is 1 second
85 | lidar_sub_ =
86 | pnh_.subscribe("lidar_packets", 640, &Decoder::LidarPacketCb, this);
87 | imu_sub_ = pnh_.subscribe("imu_packets", 100, &Decoder::ImuPacketCb, this);
88 | ROS_INFO_STREAM("Subscribing lidar packets: " << lidar_sub_.getTopic());
89 | ROS_INFO_STREAM("Subscribing imu packets: " << imu_sub_.getTopic());
90 |
91 | // Publishers
92 | camera_pub_ = it_.advertiseCamera("image", 10);
93 | cloud_pub_ = pnh_.advertise("cloud", 10);
94 | imu_pub_ = pnh_.advertise("imu", 100);
95 | range_pub_ = pnh_.advertise("range", 5);
96 | signal_pub_ = pnh_.advertise("signal", 5);
97 |
98 | // Frames
99 | sensor_frame_ = pnh_.param("sensor_frame", "os_sensor");
100 | lidar_frame_ = pnh_.param("lidar_frame", "os_lidar");
101 | imu_frame_ = pnh_.param("imu_frame", "os_imu");
102 | ROS_INFO_STREAM("Sensor frame: " << sensor_frame_);
103 | ROS_INFO_STREAM("Lidar frame: " << lidar_frame_);
104 | ROS_INFO_STREAM("Imu frame: " << imu_frame_);
105 | }
106 |
107 | void Decoder::InitParams() {
108 | replay_ = pnh_.param("replay", false);
109 | ROS_INFO("Replay: %s", replay_ ? "true" : "false");
110 | gravity_ = pnh_.param("gravity", kDefaultGravity);
111 | ROS_INFO("Gravity: %f", gravity_);
112 | scan_.destagger = pnh_.param("destagger", false);
113 | ROS_INFO("Destagger: %s", scan_.destagger ? "true" : "false");
114 | scan_.min_range = pnh_.param("min_range", 0.5);
115 | scan_.max_range = pnh_.param("max_range", 127.0);
116 | scan_.range_scale = pnh_.param("range_scale", 512.0);
117 | ROS_INFO("Range: [%f, %f], scale: %f",
118 | scan_.min_range,
119 | scan_.max_range,
120 | scan_.range_scale);
121 | if (scan_.max_range * scan_.range_scale >
122 | static_cast(std::numeric_limits::max())) {
123 | throw std::domain_error("max range exceeds representation");
124 | }
125 |
126 | acc_noise_var_ = pnh_.param("acc_noise_std", 0.0023);
127 | gyr_noise_var_ = pnh_.param("gyr_noise_std", 0.00026);
128 | // https://github.com/ethz-asl/kalibr/wiki/IMU-Noise-Model
129 | acc_noise_var_ = std::pow(acc_noise_var_, 2) * 100.0;
130 | gyr_noise_var_ = std::pow(gyr_noise_var_, 2) * 100.0;
131 | ROS_INFO("Discrete time acc noise var: %f, gyr nosie var: %f",
132 | acc_noise_var_,
133 | gyr_noise_var_);
134 | vis_signal_scale_ = pnh_.param("vis_signal_scale", 4.0);
135 | ROS_INFO("Signal scale: %f", vis_signal_scale_);
136 | }
137 |
138 | void Decoder::InitOuster() {
139 | ROS_INFO_STREAM("=== Initializing Ouster Decoder ===");
140 | // wait for service
141 | auto client = pnh_.serviceClient("get_metadata");
142 |
143 | // NOTE: it is possible that in replay mode, the service was shutdown and
144 | // re-advertised. If the client call is too soon, then we risk getting the old
145 | // metadata. It is also possible that the call would fail because the driver
146 | // side hasn't finished re-advertising. Therefore in replay mode, we add a
147 | // small delay before calling the service.
148 | if (replay_) {
149 | ros::Duration(0.1).sleep();
150 | }
151 |
152 | client.waitForExistence();
153 |
154 | ouster_ros::GetMetadata srv{};
155 | // Initialize everything if service call is successful
156 | if (client.call(srv)) {
157 | InitModel(srv.response.metadata);
158 | InitScan(model_);
159 | SendTransform(model_);
160 | } else {
161 | ROS_ERROR_STREAM(client.getService() << " call failed, abort.");
162 | ros::shutdown();
163 | }
164 | }
165 |
166 | void Decoder::InitModel(const std::string& metadata) {
167 | // parse metadata into lidar model
168 | model_ = LidarModel{metadata};
169 | ROS_INFO("Lidar mode %s: %d x %d @ %d hz, delta_azimuth %f",
170 | os::to_string(model_.info.mode).c_str(),
171 | model_.rows,
172 | model_.cols,
173 | model_.freq,
174 | model_.d_azimuth);
175 | ROS_INFO("Columns per packet: %d, Pixels per column: %d",
176 | model_.pf->columns_per_packet,
177 | model_.pf->pixels_per_column);
178 |
179 | // Generate partial camera info message
180 | cinfo_msg_ = boost::make_shared();
181 | model_.UpdateCameraInfo(*cinfo_msg_);
182 | }
183 |
184 | void Decoder::InitScan(const LidarModel& model) {
185 | int num_subscans = pnh_.param("divide", 1);
186 | // Make sure cols is divisible by num_subscans
187 | if (num_subscans < 1 || model.cols % num_subscans != 0) {
188 | throw std::domain_error(
189 | "num subscans is not divisible by cols: " + std::to_string(model.cols) +
190 | " / " + std::to_string(num_subscans));
191 | }
192 |
193 | // Each block has 16 cols, make sure we dont divide into anything smaller
194 | num_subscans = std::min(num_subscans, model.cols / 16);
195 |
196 | const int subscan_cols = model.cols / num_subscans;
197 | ROS_INFO("Subscan %d x %d, total %d", model.rows, subscan_cols, num_subscans);
198 | scan_.Allocate(model.rows, subscan_cols);
199 | }
200 |
201 | void Decoder::SendTransform(const LidarModel& model) {
202 | static_tf_.sendTransform(ouster_ros::transform_to_tf_msg(
203 | model.info.imu_to_sensor_transform, sensor_frame_, imu_frame_));
204 | static_tf_.sendTransform(ouster_ros::transform_to_tf_msg(
205 | model.info.lidar_to_sensor_transform, sensor_frame_, lidar_frame_));
206 | }
207 |
208 | void Decoder::PublishAndReset() {
209 | std_msgs::Header header;
210 | header.frame_id = lidar_frame_;
211 | header.stamp.fromNSec(scan_.times.back()); // use time of the last column
212 |
213 | // Publish image and camera_info
214 | // cinfo stores information about the full sweep, while roi stores information
215 | // about the subscan
216 | if (camera_pub_.getNumSubscribers() > 0) {
217 | // const auto image_msg =
218 | // cv_bridge::CvImage(header, "32FC4", scan_.image).toImageMsg();
219 | cinfo_msg_->header = header;
220 | // Update camera info roi with scan
221 | scan_.UpdateCinfo(*cinfo_msg_);
222 | scan_.image_ptr->header = header;
223 | camera_pub_.publish(scan_.image_ptr, cinfo_msg_);
224 | }
225 |
226 | // Publish range image on demand
227 | if (range_pub_.getNumSubscribers() > 0 ||
228 | signal_pub_.getNumSubscribers() > 0) {
229 | // cast image as 8 channel short so that we can extract the last 2 as range
230 | // and signal
231 | const auto image = cv_bridge::toCvShare(scan_.image_ptr)->image;
232 | const cv::Mat image16u(scan_.rows(), scan_.cols(), CV_16UC(8), image.data);
233 |
234 | if (range_pub_.getNumSubscribers() > 0) {
235 | cv::Mat range;
236 | cv::extractChannel(image16u, range, 6);
237 | range_pub_.publish(
238 | cv_bridge::CvImage(header, "16UC1", range).toImageMsg());
239 | }
240 |
241 | if (signal_pub_.getNumSubscribers() > 0) {
242 | cv::Mat signal;
243 | cv::extractChannel(image16u, signal, 7);
244 | // multiply by 32 for visualization purposes
245 | signal_pub_.publish(
246 | cv_bridge::CvImage(header, "16UC1", signal * vis_signal_scale_)
247 | .toImageMsg());
248 | }
249 | }
250 |
251 | // Publish cloud
252 | if (cloud_pub_.getNumSubscribers() > 0) {
253 | scan_.cloud.header = header;
254 | cloud_pub_.publish(scan_.cloud);
255 | }
256 |
257 | // Increment subscan counter
258 | ++scan_.iscan;
259 | // Reset cached data after publish
260 | scan_.SoftReset(model_.cols);
261 | }
262 |
263 | void Decoder::Timing(const ros::Time& t_start) const {
264 | const auto t_end = ros::Time::now();
265 | const auto t_proc = (t_end - t_start).toSec();
266 | const auto ratio = t_proc / model_.dt_packet;
267 | const auto t_proc_ms = t_proc * 1e3;
268 | const auto t_block_ms = model_.dt_packet * 1e3;
269 |
270 | ROS_DEBUG_THROTTLE(
271 | 1, "time [ms] %.4f / %.4f (%.1f%%)", t_proc_ms, t_block_ms, ratio * 100);
272 | }
273 |
274 | bool Decoder::NeedAlign(int mid) {
275 | if (need_align_ && mid == 0) {
276 | need_align_ = false;
277 | ROS_WARN("Align start of scan to mid %d, icol in scan %d", mid, scan_.icol);
278 | }
279 | return need_align_;
280 | }
281 |
282 | void Decoder::LidarPacketCb(const ouster_ros::PacketMsg& lidar_msg) {
283 | const auto t0 = ros::Time::now();
284 | const auto* packet_buf = lidar_msg.buf.data();
285 | const auto& pf = *model_.pf;
286 | const int fid = pf.frame_id(packet_buf);
287 |
288 | for (int col = 0; col < pf.columns_per_packet; ++col) {
289 | // Get column buffer
290 | const uint8_t* const col_buf = pf.nth_col(col, packet_buf);
291 | // const int fid = pf.col_frame_id(col_buf);
292 | const int mid = pf.col_measurement_id(col_buf);
293 |
294 | // If we set need_align to true then this will wait for mid = 0 to
295 | // start a scan
296 | if (NeedAlign(mid)) {
297 | continue;
298 | }
299 |
300 | // The invariant here is that scan_.icol will always point at the current
301 | // column to be filled at the beginning of the loop
302 |
303 | // Sometimes the lidar packet will jump forward by a large chunk, we handle
304 | // this case here
305 | const auto uid = model_.Uid(fid, mid);
306 | const auto jump = scan_.DetectJump(uid);
307 | if (jump == 0) {
308 | // Data arrived as expected, decode and forward
309 | scan_.DecodeColumn(col_buf, model_);
310 | if (scan_.IsFull()) {
311 | PublishAndReset();
312 | Timing(t0);
313 | }
314 | } else if (0 < jump && jump <= model_.cols) {
315 | // A jump smaller than a full sweep
316 | ROS_WARN("Packet jumped to f%d:m%d by %d columns (%.2f sweeps).",
317 | fid,
318 | mid,
319 | jump,
320 | static_cast(jump) / model_.cols);
321 | // Detect a jump, we need to forward scan icol by the same amount as jump
322 | // We could directly increment icol and publish if necessary, but
323 | // this will require us to zero the whole cloud at publish time which is
324 | // very time-consuming. Therefore, we choose to advance icol one by one
325 | // and zero out each column in the point cloud
326 | for (int i = 0; i < jump; ++i) {
327 | // zero cloud column at current col and then increment
328 | scan_.InvalidateColumn(model_.dt_col);
329 | // It is possible that this jump will span two scans, so if that is
330 | // the case, we need to publish the previous scan before moving forward
331 | if (scan_.IsFull()) {
332 | ROS_WARN("Jumped into a new scan, need to publish the previous one");
333 | PublishAndReset();
334 | Timing(t0);
335 | }
336 | }
337 | } else {
338 | // Handle backward jump or large forward jump
339 | ROS_ERROR("Packet jumped to f%d:m%d by %d columns (%.2f sweeps).",
340 | fid,
341 | mid,
342 | jump,
343 | static_cast(jump) / model_.cols);
344 | if (replay_) {
345 | ROS_WARN("Large jump detected in replay mode, re-initialize...");
346 | need_align_ = true;
347 | scan_.HardReset();
348 | // Also need to reinitialize everything since it is possible that it is
349 | // a different dataset
350 | InitOuster();
351 | } else {
352 | ROS_FATAL("Large jump detected in normal mode, shutting down...");
353 | ros::shutdown();
354 | }
355 | return;
356 | }
357 | }
358 | }
359 |
360 | void Decoder::ImuPacketCb(const ouster_ros::PacketMsg& imu_msg) {
361 | const auto* buf = imu_msg.buf.data();
362 | const auto& pf = *model_.pf;
363 |
364 | sm::Imu m;
365 | m.header.stamp.fromNSec(pf.imu_gyro_ts(buf));
366 | m.header.frame_id = imu_frame_;
367 |
368 | // Invalidate orientation data since we don't have it
369 | // http://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/Imu.html
370 | auto& q = m.orientation;
371 | q.x = q.y = q.z = q.w = 0;
372 | m.orientation_covariance[0] = -1;
373 |
374 | auto& a = m.linear_acceleration;
375 | a.x = pf.imu_la_x(buf) * gravity_;
376 | a.y = pf.imu_la_y(buf) * gravity_;
377 | a.z = pf.imu_la_z(buf) * gravity_;
378 |
379 | auto& w = m.angular_velocity;
380 | w.x = Deg2Rad(pf.imu_av_x(buf));
381 | w.y = Deg2Rad(pf.imu_av_y(buf));
382 | w.z = Deg2Rad(pf.imu_av_z(buf));
383 |
384 | for (int i = 0; i < 9; i += 4) {
385 | m.linear_acceleration_covariance[i] = acc_noise_var_;
386 | m.angular_velocity_covariance[i] = gyr_noise_var_;
387 | }
388 |
389 | imu_pub_.publish(m);
390 | }
391 |
392 | } // namespace ouster_decoder
393 |
394 | int main(int argc, char** argv) {
395 | ros::init(argc, argv, "os_decoder");
396 |
397 | ouster_decoder::Decoder node(ros::NodeHandle("~"));
398 | ros::spin();
399 |
400 | return 0;
401 | }
402 |
--------------------------------------------------------------------------------