Skip to content

Commit 594acdc

Browse files
committed
feat(azimuth_bin_index): implement azimuth bin indexing for improved tracker association
- Introduced a new AzimuthBinIndex class to manage tracker indices across azimuth bins, enhancing the efficiency of the association process. - Updated PolarAssociation to utilize the AzimuthBinIndex for registering and querying tracker indices, streamlining measurement processing. - Removed legacy azimuth bin helper functions to simplify the codebase and improve maintainability.
1 parent df4699c commit 594acdc

4 files changed

Lines changed: 144 additions & 67 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2026 TIER IV, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef AUTOWARE__MULTI_OBJECT_TRACKER__ASSOCIATION__AZIMUTH_BIN_INDEX_HPP_
16+
#define AUTOWARE__MULTI_OBJECT_TRACKER__ASSOCIATION__AZIMUTH_BIN_INDEX_HPP_
17+
18+
#include "autoware/multi_object_tracker/association/scoring/polar_assignment_scoring.hpp"
19+
20+
#include <algorithm>
21+
#include <array>
22+
#include <cmath>
23+
#include <vector>
24+
25+
namespace autoware::multi_object_tracker
26+
{
27+
28+
/// Per-bin tracker record: index + range of the near face.
29+
struct AzimuthBinEntry
30+
{
31+
size_t tracker_idx;
32+
float r_min; // near-face range [m]; float precision is sufficient (< 1 cm error at 300 m)
33+
};
34+
35+
/// 1-D azimuth bin index with soft radial pre-filtering.
36+
37+
class AzimuthBinIndex
38+
{
39+
public:
40+
static constexpr int kNumBins = 24; // 15° per bin, full 360°
41+
static constexpr double kBinWidth = 2.0 * M_PI / kNumBins; // [rad]
42+
43+
void clear()
44+
{
45+
for (auto & bin : bins_) bin.clear();
46+
}
47+
48+
/// Register @p tracker_idx to every bin its @p interval covers.
49+
/// The +1 guard ensures boundary trackers are always findable by find().
50+
void add(
51+
const polar_scoring::AzimuthInterval & interval, size_t tracker_idx, double r_min)
52+
{
53+
const int n = std::min(
54+
static_cast<int>(std::ceil(2.0 * interval.half_span / kBinWidth)) + 1, kNumBins);
55+
const int start = angleToBin(interval.center - interval.half_span);
56+
const AzimuthBinEntry entry{tracker_idx, static_cast<float>(r_min)};
57+
for (int i = 0; i < n; ++i) {
58+
bins_[(start + i) % kNumBins].push_back(entry);
59+
}
60+
}
61+
62+
/// Return unique tracker indices whose bins overlap @p interval and whose
63+
/// stored r_min passes the soft radial pre-filter for @p r_min_query.
64+
std::vector<size_t> find(
65+
const polar_scoring::AzimuthInterval & interval, double r_min_query) const
66+
{
67+
// Exact bin count — no +1, relying on add()'s guard for boundary coverage.
68+
const int n =
69+
std::max(1, static_cast<int>(std::ceil(2.0 * interval.half_span / kBinWidth)));
70+
const int start = angleToBin(interval.center - interval.half_span);
71+
72+
std::vector<size_t> candidates;
73+
for (int i = 0; i < n; ++i) {
74+
for (const auto & entry : bins_[(start + i) % kNumBins]) {
75+
if (radialPrePass(r_min_query, static_cast<double>(entry.r_min))) {
76+
candidates.push_back(entry.tracker_idx);
77+
}
78+
}
79+
}
80+
std::sort(candidates.begin(), candidates.end());
81+
candidates.erase(std::unique(candidates.begin(), candidates.end()), candidates.end());
82+
return candidates;
83+
}
84+
85+
private:
86+
// Sigma formula mirrors radialCompatibility() in polar_assignment_scoring.cpp.
87+
static constexpr double kSigmaBase = 2.0; // [m]
88+
static constexpr double kSigmaRate = 0.03; // [m/m]
89+
// Gate factor: -ln(0.05) ≈ 3.0. Skip when exp(-gap/sigma) < 0.05,
90+
// i.e., gap > 3σ. Always above the 2 m hard gate, so no true match is lost.
91+
static constexpr double kGateFactor = 3.0;
92+
93+
std::array<std::vector<AzimuthBinEntry>, kNumBins> bins_;
94+
95+
/// Returns true when the radial pre-filter passes (candidate should be kept).
96+
/// Equivalent to exp(-r_gap/sigma) >= 0.05, computed without std::exp.
97+
static bool radialPrePass(double r_query, double r_cand)
98+
{
99+
const double r_gap = std::abs(r_query - r_cand);
100+
const double sigma = kSigmaBase + kSigmaRate * std::max(r_query, r_cand);
101+
return r_gap <= kGateFactor * sigma;
102+
}
103+
104+
/// Map any angle [rad] to a bin index in [0, kNumBins).
105+
/// Shifts [-π, π) → [0, 2π) by adding π before bucketing.
106+
static int angleToBin(double angle)
107+
{
108+
double a = std::fmod(angle + M_PI, 2.0 * M_PI);
109+
if (a < 0.0) a += 2.0 * M_PI;
110+
const int bin = static_cast<int>(a / kBinWidth);
111+
return std::min(bin, kNumBins - 1);
112+
}
113+
};
114+
115+
} // namespace autoware::multi_object_tracker
116+
117+
#endif // AUTOWARE__MULTI_OBJECT_TRACKER__ASSOCIATION__AZIMUTH_BIN_INDEX_HPP_

perception/autoware_multi_object_tracker/include/autoware/multi_object_tracker/association/polar_association.hpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define AUTOWARE__MULTI_OBJECT_TRACKER__ASSOCIATION__POLAR_ASSOCIATION_HPP_
1717

1818
#include "autoware/multi_object_tracker/association/association_base.hpp"
19+
#include "autoware/multi_object_tracker/association/azimuth_bin_index.hpp"
1920
#include "autoware/multi_object_tracker/association/scoring/polar_assignment_scoring.hpp"
2021
#include "autoware/multi_object_tracker/association/solver/gnn_solver.hpp"
2122
#include "autoware/multi_object_tracker/configurations.hpp"
@@ -26,7 +27,6 @@
2627

2728
#include <geometry_msgs/msg/pose.hpp>
2829

29-
#include <array>
3030
#include <list>
3131
#include <memory>
3232
#include <optional>
@@ -45,8 +45,6 @@ namespace autoware::multi_object_tracker
4545
class PolarAssociation : public AssociationBase
4646
{
4747
public:
48-
// Number of azimuth bins (15° each, full circle)
49-
static constexpr int kNumAzimuthBins = 24;
5048

5149
explicit PolarAssociation(const AssociatorConfig & config);
5250
~PolarAssociation() override = default;
@@ -77,8 +75,7 @@ class PolarAssociation : public AssociationBase
7775
std::shared_ptr<autoware_utils_debug::TimeKeeper> time_keeper_;
7876
std::optional<geometry_msgs::msg::Pose> ego_pose_;
7977

80-
// Azimuth bin index: kNumAzimuthBins bins × 15° each, each bin holds tracker indices
81-
std::array<std::vector<size_t>, kNumAzimuthBins> azimuth_bins_;
78+
AzimuthBinIndex azimuth_bin_index_;
8279

8380
PolarPreparationData prepareAssociationData(
8481
const types::DynamicObjectList & measurements,

perception/autoware_multi_object_tracker/lib/association/polar_association.cpp

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
2525

26-
#include <algorithm>
2726
#include <cmath>
2827
#include <iterator>
2928
#include <list>
@@ -43,56 +42,6 @@ constexpr double INVALID_SCORE = 0.0;
4342
// surface of an object, not from its interior or far side.
4443
constexpr double NEAR_FACE_GAP_THRESHOLD = 2.0;
4544

46-
// Azimuth bin helpers
47-
// 24 bins × 15° (π/12 rad) each, covering the full [0, 2π) circle.
48-
constexpr double kAzimuthBinWidth =
49-
2.0 * M_PI / static_cast<double>(PolarAssociation::kNumAzimuthBins);
50-
51-
/// Map any angle to a bin index in [0, kNumAzimuthBins).
52-
int azimuthToBin(double angle)
53-
{
54-
// Shift from [-π, π) to [0, 2π) then divide by bin width.
55-
double a = std::fmod(angle + M_PI, 2.0 * M_PI);
56-
if (a < 0.0) a += 2.0 * M_PI;
57-
const int bin = static_cast<int>(a / kAzimuthBinWidth);
58-
return std::min(bin, PolarAssociation::kNumAzimuthBins - 1);
59-
}
60-
61-
/// Register tracker_idx to every bin that the azimuth interval covers.
62-
void registerToAzimuthBins(
63-
std::array<std::vector<size_t>, PolarAssociation::kNumAzimuthBins> & bins,
64-
const polar_scoring::AzimuthInterval & azimuth, const size_t tracker_idx)
65-
{
66-
// Number of bins the interval spans (always at least 1, capped at all bins).
67-
const int n = std::min(
68-
static_cast<int>(std::ceil(2.0 * azimuth.half_span / kAzimuthBinWidth)) + 1,
69-
PolarAssociation::kNumAzimuthBins);
70-
const int start_bin = azimuthToBin(azimuth.center - azimuth.half_span);
71-
for (int i = 0; i < n; ++i) {
72-
bins[(start_bin + i) % PolarAssociation::kNumAzimuthBins].push_back(tracker_idx);
73-
}
74-
}
75-
76-
/// Collect unique tracker indices from all bins covered by the azimuth interval.
77-
std::vector<size_t> queryAzimuthBins(
78-
const std::array<std::vector<size_t>, PolarAssociation::kNumAzimuthBins> & bins,
79-
const polar_scoring::AzimuthInterval & azimuth)
80-
{
81-
const int n = std::min(
82-
static_cast<int>(std::ceil(2.0 * azimuth.half_span / kAzimuthBinWidth)) + 1,
83-
PolarAssociation::kNumAzimuthBins);
84-
const int start_bin = azimuthToBin(azimuth.center - azimuth.half_span);
85-
86-
std::vector<size_t> candidates;
87-
for (int i = 0; i < n; ++i) {
88-
const auto & bin_vec = bins[(start_bin + i) % PolarAssociation::kNumAzimuthBins];
89-
candidates.insert(candidates.end(), bin_vec.begin(), bin_vec.end());
90-
}
91-
std::sort(candidates.begin(), candidates.end());
92-
candidates.erase(std::unique(candidates.begin(), candidates.end()), candidates.end());
93-
return candidates;
94-
}
95-
9645
} // namespace
9746

9847
using autoware_utils_debug::ScopedTimeTrack;
@@ -146,7 +95,7 @@ PolarAssociation::PolarPreparationData PolarAssociation::prepareAssociationData(
14695
// Extract tracked objects and build azimuth bin index
14796
{
14897
size_t tracker_idx = 0;
149-
for (auto & bin : azimuth_bins_) bin.clear();
98+
azimuth_bin_index_.clear();
15099

151100
for (const auto & tracker : trackers) {
152101
types::DynamicObject tracked_object;
@@ -155,8 +104,8 @@ PolarAssociation::PolarPreparationData PolarAssociation::prepareAssociationData(
155104
// Compute polar footprint and register to all azimuth bins it covers
156105
prep_data.tracker_footprints.emplace_back(
157106
polar_scoring::computePolarFootprint(tracked_object, ego_x, ego_y, ego_yaw));
158-
registerToAzimuthBins(
159-
azimuth_bins_, prep_data.tracker_footprints.back().azimuth, tracker_idx);
107+
const auto & fp = prep_data.tracker_footprints.back();
108+
azimuth_bin_index_.add(fp.azimuth, tracker_idx, fp.r_min);
160109

161110
prep_data.tracked_objects.emplace_back(std::move(tracked_object));
162111
prep_data.tracker_labels.emplace_back(tracker->getHighestProbLabel());
@@ -184,7 +133,7 @@ void PolarAssociation::processMeasurement(
184133
// Compute measurement polar footprint and query azimuth bins for candidate trackers
185134
const auto meas_fp =
186135
polar_scoring::computePolarFootprint(measurement_object, ego_x, ego_y, ego_yaw);
187-
const auto candidate_indices = queryAzimuthBins(azimuth_bins_, meas_fp.azimuth);
136+
const auto candidate_indices = azimuth_bin_index_.find(meas_fp.azimuth, meas_fp.r_min);
188137

189138
for (const size_t tracker_idx : candidate_indices) {
190139
const auto tracker_type = prep_data.tracker_types[tracker_idx];

perception/autoware_multi_object_tracker/lib/association/scoring/polar_assignment_scoring.cpp

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ PolarFootprint computePolarFootprint(
5353
return {{azimuth, 0.0}, range, range, z_min, z_max};
5454
}
5555

56-
// Compute centroid-based azimuth as the reference center
56+
// Centroid direction used as the reference from which corner offsets are measured.
57+
// For partial observations (I-shape, L-shape) the centroid may not be the angular
58+
// midpoint of the visible polygon, so we recompute the true angular midpoint below.
5759
const double cx = object.pose.position.x - ego_x;
5860
const double cy = object.pose.position.y - ego_y;
5961
const double center_azimuth = normalizeAngle(std::atan2(cy, cx) - ego_yaw);
6062

6163
double r_min = std::numeric_limits<double>::max();
6264
double r_max = 0.0;
63-
double max_offset = 0.0;
65+
// Signed angular offsets from center_azimuth tracked independently.
66+
double az_lo = std::numeric_limits<double>::max();
67+
double az_hi = std::numeric_limits<double>::lowest();
6468

6569
for (const auto & pt : points) {
6670
const double dx = pt.x() - ego_x;
@@ -69,17 +73,27 @@ PolarFootprint computePolarFootprint(
6973
r_min = std::min(r_min, range);
7074
r_max = std::max(r_max, range);
7175

72-
// Skip corners too close to ego for stable azimuth computation
73-
if (range < MIN_RANGE) continue;
76+
// Skip only truly degenerate corners (corner at ego position).
77+
if (range < 1e-6) continue;
7478

7579
const double azimuth = normalizeAngle(std::atan2(dy, dx) - ego_yaw);
76-
const double offset = std::abs(normalizeAngle(azimuth - center_azimuth));
77-
max_offset = std::max(max_offset, offset);
80+
const double offset = normalizeAngle(azimuth - center_azimuth); // signed
81+
az_lo = std::min(az_lo, offset);
82+
az_hi = std::max(az_hi, offset);
7883
}
7984

8085
r_min = std::max(r_min, MIN_RANGE);
8186

82-
return {{center_azimuth, max_offset}, r_min, r_max, z_min, z_max};
87+
if (az_lo > az_hi) {
88+
// All corners were at the ego position — degenerate polygon.
89+
return {{center_azimuth, 0.0}, r_min, r_max, z_min, z_max};
90+
}
91+
92+
// Angular midpoint of the visible span, corrected from the centroid's azimuth.
93+
const double corrected_center = normalizeAngle(center_azimuth + (az_lo + az_hi) / 2.0);
94+
const double half_span = (az_hi - az_lo) / 2.0;
95+
96+
return {{corrected_center, half_span}, r_min, r_max, z_min, z_max};
8397
}
8498

8599
double azimuthIoU(const AzimuthInterval & a, const AzimuthInterval & b)

0 commit comments

Comments
 (0)