@@ -123,20 +123,21 @@ void TrajectoryVisualizer::add(
123123 return ;
124124 }
125125
126- // Normalize total costs using Eigen vectorized ops
126+ // Normalize costs for visualization. Outliers (e.g. collision trajectories,
127+ // detected via max-gap clustering) render in dark purple instead of the gradient.
127128 auto addCostLayer = [&](const Eigen::ArrayXf & layer_costs, const std::string & ns) {
128129 float min_val = layer_costs.minCoeff ();
129130 float max_val = layer_costs.maxCoeff ();
130- float range = max_val - min_val;
131- Eigen::ArrayXf normalized;
132- if (range > 0 .0f ) {
133- normalized = (layer_costs - min_val) / range;
134- } else {
135- normalized = Eigen::ArrayXf::Zero (layer_costs.size ());
136- }
131+
132+ float upper_bound = max_val;
133+ bool has_outliers = maxGapSplit (layer_costs, 0 .5f , upper_bound);
134+ float range = (has_outliers ? upper_bound : max_val) - min_val;
137135
138136 for (size_t i = 0 ; i < n_rows; i += trajectory_step_) {
139- addCostColoredTrajectory (i, trajectories, normalized (i), ns, stamp);
137+ float norm = (range > 0 .0f ) ?
138+ (layer_costs (i) - min_val) / range : 0 .0f ;
139+ bool is_outlier = has_outliers && layer_costs (i) > upper_bound;
140+ addCostColoredTrajectory (i, trajectories, norm, is_outlier, ns, stamp);
140141 }
141142 };
142143
@@ -153,6 +154,7 @@ void TrajectoryVisualizer::addCostColoredTrajectory(
153154 size_t trajectory_idx,
154155 const models::Trajectories & trajectories,
155156 float normalized_cost,
157+ bool is_outlier,
156158 const std::string & ns,
157159 const builtin_interfaces::msg::Time & stamp)
158160{
@@ -168,7 +170,9 @@ void TrajectoryVisualizer::addCostColoredTrajectory(
168170 marker.action = Marker::ADD;
169171 marker.pose .orientation .w = 1.0 ;
170172 marker.scale .x = 0.01 ; // line width
171- marker.color = costToColor (normalized_cost);
173+ marker.color = is_outlier ?
174+ utils::createColor (0 .5f , 0 .0f , 0 .5f , 0 .6f ) : // dark purple for outliers
175+ costToColor (normalized_cost);
172176
173177 marker.points .reserve (n_cols / time_step_ + 1 );
174178 for (size_t j = 0 ; j < n_cols; j += time_step_) {
@@ -197,6 +201,37 @@ std_msgs::msg::ColorRGBA TrajectoryVisualizer::costToColor(float normalized)
197201 return utils::createColor (r, g, 0 .0f , 0 .8f );
198202}
199203
204+ bool TrajectoryVisualizer::maxGapSplit (
205+ const Eigen::ArrayXf & values, float gap_ratio_threshold,
206+ float & upper_bound)
207+ {
208+ if (values.size () < 2 ) {
209+ return false ;
210+ }
211+ Eigen::ArrayXf sorted = values;
212+ std::sort (sorted.data (), sorted.data () + sorted.size ());
213+ float total_range = sorted (sorted.size () - 1 ) - sorted (0 );
214+ if (total_range <= 0 .0f ) {
215+ return false ;
216+ }
217+
218+ float largest_gap = 0 .0f ;
219+ Eigen::Index gap_idx = 0 ;
220+ for (Eigen::Index k = 1 ; k < sorted.size (); ++k) {
221+ float gap = sorted (k) - sorted (k - 1 );
222+ if (gap > largest_gap) {
223+ largest_gap = gap;
224+ gap_idx = k;
225+ }
226+ }
227+
228+ if (largest_gap > gap_ratio_threshold * total_range) {
229+ upper_bound = sorted (gap_idx - 1 );
230+ return true ;
231+ }
232+ return false ;
233+ }
234+
200235void TrajectoryVisualizer::reset ()
201236{
202237 marker_id_ = 0 ;
0 commit comments