Skip to content

Commit

Permalink
feat(hesai): software-based precise FoV limit & early publishing (#173)
Browse files Browse the repository at this point in the history
* refactor(hesai): move calibration handling from decoder wrapper to ROS wrapper

* chore(hesai): make clang and pre-commit happy

* chore(hesai_cmd_response): add missing imports

* feat(hesai_decoder): filter points outside of configured FoV

* chore(hesai_ros_wrapper): fix clang-tidy errors

* feat(hesai_common): calculate FoV padding in calibration config

* chore(hesai): fix clang-tidy errors

* feat(hesai): automatic hardware FoV oversizing

* feat(hesai): publish on cloud_max_angle

* fix(hesai_hw_interface): calculate correct FOV angle units

* fix(hesai): cut scans correctly in edge cases

* test(hesai): add scan cutting tests

* fix(hesai_ros_decoder_test): set missing cloud_min_angle, cloud_max_angle in params

* fix(at128): disable unsupported lidar range feature for AT128

* test(hesai): rewrite reference data to reflect edge effects of new scan cutting

The new scan cutting mechanism filters points based on the corrected angle. This changes the points contained/filtered out at the edges of the scan cut.

* fix(at128): do not issue unsupported monitor command

* temp: instrumentation for pointcloud time delay

* ci(pre-commit): autofix

* works now, rebase later

* WIP packet loss

* refactor(hesai): replace `scan_phase` by `sync_angle` and `cut_angle`

* toriaezu working on 360deg lidars

* less hacky working version for 360deg lidars

* wip AT128

* AT128 maybz?

* ci(pre-commit): autofix

* AT128 mostly working, edge cases unsure (need real sensor to test)

* ci(pre-commit): autofix

* AT128 working

* Parameter edge cases working

* correct cut angle for at128

* chore(hesai): regenerate and fix unit tests

* chore: add 'centi' to dictionary

* chore(nebula_tests/hesai): make sure each checked cloud contains at least one point

* chore(nebula_tests/hesai): add fov params to tests, clean up params

* chore(nebula_tests/hesai): add fov cutting unit tests

* chore(hesai): make namespaces c++17-style one-liners

* chore(hesai): mark single-arg constructor explicit

* chore(hesai): favor using over typedef

* chore(hesai): initialize uninitialized vars

* chore(hesai): clean up imports

* chore(hesai): inline function defined in header

* chore(hesai): fix case style for some functions and variables

* chore(hesai: favor range-based loop over index-based one

* chore(hesai): add missing stdexcept include

* fix(hesai): fix logic that decides whether to publish packets

* chore(continental): fix compiler errors

* chore: add missing diagnostic_updater dependency to nebula_tests

* fix(hesai_ros_wrapper): reject sync_angle = 360 as this will crash the sensors

* fix(hesai_ros_wrapper): disallow cutting at start of scan (except for 360deg FoV) to avoid publishing pointclouds later than necessary.

* fix(at128): disallow sync angles outside the [30, 150] deg range

* chore(hesai_hw_interface): remove superfluous "starting with FoV" log messages

* fix(hesai): return error properly when cut angle is set out-of-range

* chore(at128): change `auto` to `int32_t` in angle correction for clarity

* chore(at128): change M_PI to M_PIf for clarity

Co-authored-by: David Wong <[email protected]>

* chore(at128): add note of firmware version where channel output bug occurs

* chore(at128): add explanatory comment on why fov bounds are hard-coded

* chore(hesai): fix 260deg typo in parameter description

* fix(hesai): handle cut at 0/360deg correctly for non-360 and 360deg FoVs

* chore(at128): correct return type and rename `all_channels` function, add doc comments

* chore(hesai_decoder): replace auto with specific types for number types

* chore(hesai): remove unused instrumentation code

* chore: update error message printed when setting HW FoV  fails

* chore(hesai_decoder): add clarifications for decode/output pointcloud handling

* chore(hesai): apply suggestions for re-wording documentation comments

Co-authored-by: David Wong <[email protected]>

* ci(pre-commit): autofix

* chore(hesai_ros_wrapper): add missing nebula_packet include

* chore(hesai_hw_interface): clarify angle units in `checkAndSetLidarRange`

* chore: add ddeg for deci-degrees to cspell dict

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: David Wong <[email protected]>
  • Loading branch information
3 people authored Sep 18, 2024
1 parent 528b732 commit 16e1489
Show file tree
Hide file tree
Showing 56 changed files with 1,293 additions and 580 deletions.
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"block_id",
"Bpearl",
"calib",
"centi",
"ddeg",
"DHAVE",
"Difop",
"extrinsics",
Expand Down
40 changes: 35 additions & 5 deletions nebula_common/include/nebula_common/hesai/hesai_common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
#include "nebula_common/nebula_status.hpp"
#include "nebula_common/util/string_conversions.hpp"

#include <algorithm>
#include <cmath>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
namespace nebula
{
Expand All @@ -36,9 +38,10 @@ struct HesaiSensorConfiguration : public LidarConfigurationBase
{
std::string multicast_ip;
uint16_t gnss_port{};
double scan_phase{};
uint16_t sync_angle{};
double cut_angle{};
double dual_return_distance_threshold{};
std::string calibration_path{};
std::string calibration_path;
uint16_t rotation_speed;
uint16_t cloud_min_angle;
uint16_t cloud_max_angle;
Expand All @@ -58,11 +61,13 @@ inline std::ostream & operator<<(std::ostream & os, HesaiSensorConfiguration con
os << "Multicast: "
<< (arg.multicast_ip.empty() ? "disabled" : "enabled, group " + arg.multicast_ip) << '\n';
os << "GNSS Port: " << arg.gnss_port << '\n';
os << "Scan Phase: " << arg.scan_phase << '\n';
os << "Rotation Speed: " << arg.rotation_speed << '\n';
os << "Sync Angle: " << arg.sync_angle << '\n';
os << "Cut Angle: " << arg.cut_angle << '\n';
os << "FoV Start: " << arg.cloud_min_angle << '\n';
os << "FoV End: " << arg.cloud_max_angle << '\n';
os << "Dual Return Distance Threshold: " << arg.dual_return_distance_threshold << '\n';
os << "Calibration Path: " << arg.calibration_path << '\n';
os << "PTP Profile: " << arg.ptp_profile << '\n';
os << "PTP Domain: " << std::to_string(arg.ptp_domain) << '\n';
os << "PTP Transport Type: " << arg.ptp_transport_type << '\n';
Expand All @@ -76,6 +81,8 @@ struct HesaiCalibrationConfigurationBase : public CalibrationConfigurationBase
virtual nebula::Status LoadFromFile(const std::string & calibration_file) = 0;
virtual nebula::Status SaveToFileFromBytes(
const std::string & calibration_file, const std::vector<uint8_t> & buf) = 0;

[[nodiscard]] virtual std::tuple<float, float> getFovPadding() const = 0;
};

/// @brief struct for Hesai calibration configuration
Expand Down Expand Up @@ -177,6 +184,19 @@ struct HesaiCalibrationConfiguration : public HesaiCalibrationConfigurationBase
ofs.close();
return Status::OK;
}

[[nodiscard]] std::tuple<float, float> getFovPadding() const override
{
float min = INFINITY;
float max = -INFINITY;

for (const auto & item : azimuth_offset_map) {
min = std::min(min, item.second);
max = std::max(max, item.second);
}

return {-max, -min};
}
};

/// @brief struct for Hesai correction configuration (for AT)
Expand Down Expand Up @@ -360,7 +380,7 @@ struct HesaiCorrection : public HesaiCalibrationConfigurationBase
/// @param ch The channel id
/// @param azi The precision azimuth in (0.01 / 256) degree unit
/// @return The azimuth adjustment in 0.01 degree unit
int8_t getAzimuthAdjustV3(uint8_t ch, uint32_t azi) const
[[nodiscard]] int8_t getAzimuthAdjustV3(uint8_t ch, uint32_t azi) const
{
unsigned int i = std::floor(1.f * azi / STEP3);
unsigned int l = azi - i * STEP3;
Expand All @@ -372,13 +392,23 @@ struct HesaiCorrection : public HesaiCalibrationConfigurationBase
/// @param ch The channel id
/// @param azi The precision azimuth in (0.01 / 256) degree unit
/// @return The elevation adjustment in 0.01 degree unit
int8_t getElevationAdjustV3(uint8_t ch, uint32_t azi) const
[[nodiscard]] int8_t getElevationAdjustV3(uint8_t ch, uint32_t azi) const
{
unsigned int i = std::floor(1.f * azi / STEP3);
unsigned int l = azi - i * STEP3;
float k = 1.f * l / STEP3;
return round((1 - k) * elevationOffset[ch * 180 + i] + k * elevationOffset[ch * 180 + i + 1]);
}

[[nodiscard]] std::tuple<float, float> getFovPadding() const override
{
// TODO(mojomex): calculate instead of hard-coding
// The reason this is tricky is that an upper bound over all azimuth/elevation combinations has
// to be found. For other sensors, this is only a function of elevation, so the search space is
// tiny compared to AT128. We should be able to find an upper bound of `getAzimuthAdjustV3` but
// I have not invested the time for now.
return {-5, 5};
}
};

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <cmath>
namespace nebula::drivers
{

/**
* @brief Tests if `angle` is in the region of the circle defined by `start_angle` and `end_angle`.
* Notably, `end_angle` can be smaller than `start_angle`, in which case the region passes over the
* 360/0 deg bound. This function is unit-independent (but all angles have to have the same unit),
* so degrees, radians, and arbitrary scale factors can be used.
*/
template <typename T>
bool angle_is_between(
T start_angle, T end_angle, T angle, bool start_inclusive = true, bool end_inclusive = true)
{
if (!start_inclusive && angle == start_angle) return false;
if (!end_inclusive && angle == end_angle) return false;

return (start_angle <= angle && angle <= end_angle) ||
((end_angle < start_angle) && (angle <= end_angle || start_angle <= angle));
}

/**
* @brief Normalizes an angle to the interval [0; max_angle]. This function is unit-independent.
* `max_angle` is 360 for degrees, 2 * M_PI for radians, and the corresponding scaled value for
* scaled units such as centi-degrees (36000).
*/
template <typename T>
T normalize_angle(T angle, T max_angle)
{
T factor = std::floor((1.0 * angle) / max_angle);
return angle - (factor * max_angle);
}

} // namespace nebula::drivers
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,10 @@ class AngleCorrector
/// @return The corrected angles (azimuth, elevation) in radians and their sin/cos values
virtual CorrectedAngleData getCorrectedAngleData(uint32_t block_azimuth, uint32_t channel_id) = 0;

/// @brief Returns true if the current azimuth lies in a different (new) scan compared to the last
/// azimuth
/// @param current_azimuth The current azimuth value in the sensor's angle resolution
/// @param last_azimuth The last azimuth in the sensor's angle resolution
/// @param sync_azimuth The azimuth set in the sensor configuration, for which the
/// timestamp is aligned to the full second
/// @return true if the current azimuth is in a different scan than the last one, false otherwise
virtual bool hasScanned(
uint32_t current_azimuth, uint32_t last_azimuth, uint32_t sync_azimuth) = 0;
virtual bool passedEmitAngle(uint32_t last_azimuth, uint32_t current_azimuth) = 0;
virtual bool passedTimestampResetAngle(uint32_t last_azimuth, uint32_t current_azimuth) = 0;
virtual bool isInsideFoV(uint32_t last_azimuth, uint32_t current_azimuth) = 0;
virtual bool isInsideOverlap(uint32_t last_azimuth, uint32_t current_azimuth) = 0;
};

} // namespace nebula::drivers
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@
#pragma once

#include "nebula_common/hesai/hesai_common.hpp"
#include "nebula_decoders/nebula_decoders_common/angles.hpp"
#include "nebula_decoders/nebula_decoders_hesai/decoders/angle_corrector.hpp"

#include <nebula_common/nebula_common.hpp>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <memory>
#include <optional>
#include <ostream>
#include <utility>

namespace nebula::drivers
{
Expand All @@ -27,38 +35,96 @@ template <size_t ChannelN, size_t AngleUnit>
class AngleCorrectorCalibrationBased : public AngleCorrector<HesaiCalibrationConfiguration>
{
private:
static constexpr size_t MAX_AZIMUTH_LEN = 360 * AngleUnit;
static constexpr size_t MAX_AZIMUTH = 360 * AngleUnit;

std::array<float, ChannelN> elevation_angle_rad_{};
std::array<float, ChannelN> azimuth_offset_rad_{};
std::array<float, MAX_AZIMUTH_LEN> block_azimuth_rad_{};
std::array<float, MAX_AZIMUTH> block_azimuth_rad_{};

std::array<float, ChannelN> elevation_cos_{};
std::array<float, ChannelN> elevation_sin_{};
std::array<std::array<float, ChannelN>, MAX_AZIMUTH_LEN> azimuth_cos_{};
std::array<std::array<float, ChannelN>, MAX_AZIMUTH_LEN> azimuth_sin_{};
std::array<std::array<float, ChannelN>, MAX_AZIMUTH> azimuth_cos_{};
std::array<std::array<float, ChannelN>, MAX_AZIMUTH> azimuth_sin_{};

public:
uint32_t emit_angle_raw_;
uint32_t timestamp_reset_angle_raw_;
uint32_t fov_start_raw_;
uint32_t fov_end_raw_;

bool is_360_;

explicit AngleCorrectorCalibrationBased(
const std::shared_ptr<const HesaiCalibrationConfiguration> & sensor_calibration)
const std::shared_ptr<const HesaiCalibrationConfiguration> & sensor_calibration,
double fov_start_azimuth_deg, double fov_end_azimuth_deg, double scan_cut_azimuth_deg)
{
if (sensor_calibration == nullptr) {
throw std::runtime_error(
"Cannot instantiate AngleCorrectorCalibrationBased without calibration data");
}

// ////////////////////////////////////////
// Elevation lookup tables
// ////////////////////////////////////////

int32_t correction_min = INT32_MAX;
int32_t correction_max = INT32_MIN;

auto round_away_from_zero = [](float value) {
return (value < 0) ? std::floor(value) : std::ceil(value);
};

for (size_t channel_id = 0; channel_id < ChannelN; ++channel_id) {
float elevation_angle_deg = sensor_calibration->elev_angle_map.at(channel_id);
float azimuth_offset_deg = sensor_calibration->azimuth_offset_map.at(channel_id);

int32_t azimuth_offset_raw = round_away_from_zero(azimuth_offset_deg * AngleUnit);
correction_min = std::min(correction_min, azimuth_offset_raw);
correction_max = std::max(correction_max, azimuth_offset_raw);

elevation_angle_rad_[channel_id] = deg2rad(elevation_angle_deg);
azimuth_offset_rad_[channel_id] = deg2rad(azimuth_offset_deg);

elevation_cos_[channel_id] = cosf(elevation_angle_rad_[channel_id]);
elevation_sin_[channel_id] = sinf(elevation_angle_rad_[channel_id]);
}

for (size_t block_azimuth = 0; block_azimuth < MAX_AZIMUTH_LEN; block_azimuth++) {
// ////////////////////////////////////////
// Raw azimuth threshold angles
// ////////////////////////////////////////

int32_t emit_angle_raw = std::ceil(scan_cut_azimuth_deg * AngleUnit);
emit_angle_raw -= correction_min;
emit_angle_raw_ = normalize_angle<int32_t>(emit_angle_raw, MAX_AZIMUTH);

int32_t fov_start_raw = std::floor(fov_start_azimuth_deg * AngleUnit);
fov_start_raw -= correction_max;
fov_start_raw_ = normalize_angle<int32_t>(fov_start_raw, MAX_AZIMUTH);

int32_t fov_end_raw = std::ceil(fov_end_azimuth_deg * AngleUnit);
fov_end_raw -= correction_min;
fov_end_raw_ = normalize_angle<int32_t>(fov_end_raw, MAX_AZIMUTH);

// Reset timestamp on FoV start if FoV < 360 deg and scan is cut at FoV end.
// Otherwise, reset timestamp on publish
is_360_ =
normalize_angle(fov_start_azimuth_deg, 360.) == normalize_angle(fov_end_azimuth_deg, 360.);
bool reset_timestamp_on_publish = is_360_ || (normalize_angle(fov_end_azimuth_deg, 360.) !=
normalize_angle(scan_cut_azimuth_deg, 360.));

if (reset_timestamp_on_publish) {
int32_t timestamp_reset_angle_raw = std::floor(scan_cut_azimuth_deg * AngleUnit);
timestamp_reset_angle_raw -= correction_max;
timestamp_reset_angle_raw_ = normalize_angle<int32_t>(timestamp_reset_angle_raw, MAX_AZIMUTH);
} else {
timestamp_reset_angle_raw_ = fov_start_raw_;
}

// ////////////////////////////////////////
// Azimuth lookup tables
// ////////////////////////////////////////

for (size_t block_azimuth = 0; block_azimuth < MAX_AZIMUTH; block_azimuth++) {
block_azimuth_rad_[block_azimuth] = deg2rad(block_azimuth / static_cast<double>(AngleUnit));

for (size_t channel_id = 0; channel_id < ChannelN; ++channel_id) {
Expand All @@ -74,6 +140,8 @@ class AngleCorrectorCalibrationBased : public AngleCorrector<HesaiCalibrationCon
CorrectedAngleData getCorrectedAngleData(uint32_t block_azimuth, uint32_t channel_id) override
{
float azimuth_rad = block_azimuth_rad_[block_azimuth] + azimuth_offset_rad_[channel_id];
azimuth_rad = normalize_angle(azimuth_rad, M_PIf * 2);

float elevation_rad = elevation_angle_rad_[channel_id];

return {
Expand All @@ -85,15 +153,27 @@ class AngleCorrectorCalibrationBased : public AngleCorrector<HesaiCalibrationCon
elevation_cos_[channel_id]};
}

bool hasScanned(uint32_t current_azimuth, uint32_t last_azimuth, uint32_t sync_azimuth) override
bool passedEmitAngle(uint32_t last_azimuth, uint32_t current_azimuth) override
{
// Cut the scan when the azimuth passes over the sync_azimuth
uint32_t current_diff_from_sync =
(MAX_AZIMUTH_LEN + current_azimuth - sync_azimuth) % MAX_AZIMUTH_LEN;
uint32_t last_diff_from_sync =
(MAX_AZIMUTH_LEN + last_azimuth - sync_azimuth) % MAX_AZIMUTH_LEN;
return angle_is_between(last_azimuth, current_azimuth, emit_angle_raw_, false);
}

bool passedTimestampResetAngle(uint32_t last_azimuth, uint32_t current_azimuth) override
{
return angle_is_between(last_azimuth, current_azimuth, timestamp_reset_angle_raw_, false);
}

return current_diff_from_sync < last_diff_from_sync;
bool isInsideFoV(uint32_t last_azimuth, uint32_t current_azimuth) override
{
if (is_360_) return true;
return angle_is_between(fov_start_raw_, fov_end_raw_, current_azimuth) ||
angle_is_between(timestamp_reset_angle_raw_, emit_angle_raw_, last_azimuth);
}

bool isInsideOverlap(uint32_t last_azimuth, uint32_t current_azimuth) override
{
return angle_is_between(timestamp_reset_angle_raw_, emit_angle_raw_, current_azimuth) ||
angle_is_between(timestamp_reset_angle_raw_, emit_angle_raw_, last_azimuth);
}
};

Expand Down
Loading

0 comments on commit 16e1489

Please sign in to comment.