Skip to content

Commit 5941780

Browse files
PawelLiberadzkinebraszkamsz-rai
authored
Add radar object classification (#298)
* Add initial support for object classification in RadarTrackObjectsNode. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Add API call for setting radar object classes. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Update radar track objects tests to handle object ids. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Add safety static_cast. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Make fixes based on the review. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Prevent underflow in loop condition (#308) * Add handling nan radial speeds in radar nodes (#309) Add handling nan radial speeds in radar postprocess and object tracking nodes. Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> * Radar object classification improvements (#310) * Change array type in fieldData to be compatible with all RGL nodes * Use velocities from raytrace instead of calculating it again (better accuracy) * Do not output predicted objects * Restore the conditions of merging detections into objects * Fix required fields; skip test that fails * Require detections in world frame to predict objects properly * Fix displacementFromSkinning calculation * Fix test * Put all objects to the output * Fix deltaTime calculation * Fix test * Fix time * Fix passing time to new objects * Add comment on skinning fix * Fix coordinate system of width & length * Fix bounding boxes * Fix maxClassificationProbability * Change unit of movement_sensitivity --------- Signed-off-by: Paweł Liberadzki <pawel.liberadzki@robotec.ai> Co-authored-by: Maja Nagarnowicz <72594569+nebraszka@users.noreply.github.com> Co-authored-by: Mateusz Szczygielski <112629916+msz-rai@users.noreply.github.com>
1 parent 4bdc9b6 commit 5941780

File tree

11 files changed

+343
-81
lines changed

11 files changed

+343
-81
lines changed

include/rgl/api/core.h

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ static_assert(std::is_trivial_v<rgl_radar_scope_t>);
151151
static_assert(std::is_standard_layout_v<rgl_radar_scope_t>);
152152
#endif
153153

154+
/**
155+
* Radar object class used in object tracking.
156+
*/
157+
typedef enum : int32_t
158+
{
159+
RGL_RADAR_CLASS_CAR = 0,
160+
RGL_RADAR_CLASS_TRUCK,
161+
RGL_RADAR_CLASS_MOTORCYCLE,
162+
RGL_RADAR_CLASS_BICYCLE,
163+
RGL_RADAR_CLASS_PEDESTRIAN,
164+
RGL_RADAR_CLASS_ANIMAL,
165+
RGL_RADAR_CLASS_HAZARD,
166+
RGL_RADAR_CLASS_UNKNOWN,
167+
RGL_RADAR_CLASS_COUNT
168+
} rgl_radar_object_class_t;
169+
154170
/**
155171
* Represents on-GPU Mesh that can be referenced by Entities on the Scene.
156172
* Each Mesh can be referenced by any number of Entities on different Scenes.
@@ -852,10 +868,12 @@ RGL_API rgl_status_t rgl_node_points_radar_postprocess(rgl_node_t* node, const r
852868
/**
853869
* Creates or modifies RadarTrackObjectsNode.
854870
* The Node takes radar detections point cloud as input (from rgl_node_points_radar_postprocess), groups detections into objects based on given thresholds (node parameters),
855-
* and calculates various characteristics of these objects. This Node is intended to be connected to object list publishing node (from rgl_node_publish_udp_objectlist).
856-
* The output from this Node is in the form of a point cloud, where point XYZ coordinates are tracked objects positions. Additionally, cloud points have tracked object IDs.
857-
* Note that for correct calculation and publishing some of object characteristics (e.g. velocity) user has to call rgl_scene_set_time for current scene.
858-
* Graph input: point cloud, containing RGL_FIELD_DISTANCE_F32, RGL_FIELD_AZIMUTH_F32, RGL_FIELD_ELEVATION_F32 and RGL_FIELD_RADIAL_SPEED_F32
871+
* and calculates various characteristics of these objects. The output from this Node is in the form of a point cloud, where point XYZ coordinates are tracked objects positions.
872+
* Additionally, cloud points have tracked object IDs. Note that for correct calculation and publishing some of object characteristics (e.g. velocity) user has to call
873+
* rgl_scene_set_time for current scene.
874+
* The node expects input in world frame to be able to perform prediction properly.
875+
* Object's orientation, length, and width calculations assume the forward axis is Z and the left axis is -X.
876+
* Graph input: point cloud, containing additionally RGL_FIELD_DISTANCE_F32, RGL_FIELD_AZIMUTH_F32, RGL_FIELD_ELEVATION_F32, RGL_FIELD_RADIAL_SPEED_F32 and ENTITY_ID_I32.
859877
* Graph output: point cloud, contains only XYZ_VEC3_F32 for tracked object positions and ENTITY_ID_I32 for their IDs.
860878
* @param node If (*node) == nullptr, a new Node will be created. Otherwise, (*node) will be modified.
861879
* @param object_distance_threshold The maximum distance between a radar detection and other closest detection classified as the same tracked object (in meters).
@@ -864,13 +882,26 @@ RGL_API rgl_status_t rgl_node_points_radar_postprocess(rgl_node_t* node, const r
864882
* @param object_radial_speed_threshold The maximum radial speed difference between a radar detection and other closest detection classified as the same tracked object (in meters per second).
865883
* @param max_matching_distance The maximum distance between predicted (based on previous frame) and newly detected object position to match objects between frames (in meters).
866884
* @param max_prediction_time_frame The maximum time that object state can be predicted until it will be declared lost unless measured again (in milliseconds).
867-
* @param movement_sensitivity The maximum position change for an object to be qualified as stationary (in meters).
885+
* @param movement_sensitivity The maximum velocity for an object to be qualified as stationary (in meters per seconds).
868886
*/
869887
RGL_API rgl_status_t rgl_node_points_radar_track_objects(rgl_node_t* node, float object_distance_threshold,
870888
float object_azimuth_threshold, float object_elevation_threshold,
871889
float object_radial_speed_threshold, float max_matching_distance,
872890
float max_prediction_time_frame, float movement_sensitivity);
873891

892+
/**
893+
* Modifies RadarTrackObjectsNode to set entity ids to radar object classes mapping.
894+
* This is necessary to call for RadarTrackObjectsNode to classify tracked objects correctly. If not set, objects will be classified as RGL_RADAR_CLASS_UNKNOWN by default.
895+
* Note that entity can only belong to a single class - passing entity id multiple times in entity_ids array will result in the last version to be assigned. There is no
896+
* limit on how many entities can have the same class.
897+
* @param node RadarTrackObjectsNode to modify.
898+
* @param entity_ids Array of RGL entity ids.
899+
* @param object_classes Array of radar object classes.
900+
* @param count Number of elements in entity_ids and object_classes arrays.
901+
*/
902+
RGL_API rgl_status_t rgl_node_points_radar_set_classes(rgl_node_t node, const int32_t* entity_ids,
903+
const rgl_radar_object_class_t* object_classes, int32_t count);
904+
874905
/**
875906
* Creates or modifies FilterGroundPointsNode.
876907
* The Node adds RGL_FIELD_IS_GROUND_I32 which indicates the point is on the ground. Points are not removed.

src/api/apiCore.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,32 @@ void TapeCore::tape_node_points_radar_track_objects(const YAML::Node& yamlNode,
12151215
state.nodes.insert({nodeId, node});
12161216
}
12171217

1218+
RGL_API rgl_status_t rgl_node_points_radar_set_classes(rgl_node_t node, const int32_t* entity_ids,
1219+
const rgl_radar_object_class_t* object_classes, int32_t count)
1220+
{
1221+
auto status = rglSafeCall([&]() {
1222+
RGL_API_LOG("rgl_node_points_radar_set_classes(node={}, entity_ids={}, classes={}, count={})", repr(node),
1223+
repr(entity_ids, count), repr(object_classes, count), count);
1224+
CHECK_ARG(node != nullptr);
1225+
CHECK_ARG(entity_ids != nullptr);
1226+
CHECK_ARG(object_classes != nullptr);
1227+
CHECK_ARG(count >= 0);
1228+
RadarTrackObjectsNode::Ptr trackObjectsNode = Node::validatePtr<RadarTrackObjectsNode>(node);
1229+
trackObjectsNode->setObjectClasses(entity_ids, object_classes, count);
1230+
});
1231+
TAPE_HOOK(node, TAPE_ARRAY(entity_ids, count), TAPE_ARRAY(object_classes, count), count);
1232+
return status;
1233+
}
1234+
1235+
void TapeCore::tape_node_points_radar_set_classes(const YAML::Node& yamlNode, PlaybackState& state)
1236+
{
1237+
auto nodeId = yamlNode[0].as<TapeAPIObjectID>();
1238+
rgl_node_t node = state.nodes.contains(nodeId) ? state.nodes.at(nodeId) : nullptr;
1239+
rgl_node_points_radar_set_classes(node, state.getPtr<const int32_t>(yamlNode[1]),
1240+
state.getPtr<const rgl_radar_object_class_t>(yamlNode[2]), yamlNode[3].as<int32_t>());
1241+
state.nodes.insert({nodeId, node});
1242+
}
1243+
12181244
RGL_API rgl_status_t rgl_node_points_filter_ground(rgl_node_t* node, const rgl_vec3f* sensor_up_vector,
12191245
float ground_angle_threshold)
12201246
{

src/gpu/optixPrograms.cu

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,10 @@ extern "C" __global__ void __closesthit__()
219219
if (wasSkinned) {
220220
Mat3x4f objectToWorld;
221221
optixGetObjectToWorldTransformMatrix(reinterpret_cast<float*>(objectToWorld.rc));
222-
const Vec3f& vA = entityData.vertexDisplacementSincePrevFrame[triangleIndices.x()];
223-
const Vec3f& vB = entityData.vertexDisplacementSincePrevFrame[triangleIndices.y()];
224-
const Vec3f& vC = entityData.vertexDisplacementSincePrevFrame[triangleIndices.z()];
222+
// TODO(msz-rai): To verify if rotation is needed (in some tests it produces more realistic results)
223+
const Vec3f& vA = objectToWorld.rotation() * entityData.vertexDisplacementSincePrevFrame[triangleIndices.x()];
224+
const Vec3f& vB = objectToWorld.rotation() * entityData.vertexDisplacementSincePrevFrame[triangleIndices.y()];
225+
const Vec3f& vC = objectToWorld.rotation() * entityData.vertexDisplacementSincePrevFrame[triangleIndices.z()];
225226
displacementFromSkinning = objectToWorld.scaleVec() * Vec3f((1 - u - v) * vA + u * vB + v * vC);
226227
}
227228

src/graph/Interfaces.hpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ struct IPointsNode : virtual Node
8787
virtual std::size_t getPointCount() const { return getWidth() * getHeight(); }
8888

8989
virtual Mat3x4f getLookAtOriginTransform() const { return Mat3x4f::identity(); }
90-
virtual Vec3f getLinearVelocity() const { return Vec3f{0.0f}; }
91-
virtual Vec3f getAngularVelocity() const { return Vec3f{0.0f}; }
9290

9391
// Data getters
9492
virtual IAnyArray::ConstPtr getFieldData(rgl_field_t field) = 0;
@@ -122,8 +120,6 @@ struct IPointsNodeSingleInput : IPointsNode
122120
virtual size_t getHeight() const override { return input->getHeight(); }
123121

124122
virtual Mat3x4f getLookAtOriginTransform() const override { return input->getLookAtOriginTransform(); }
125-
virtual Vec3f getLinearVelocity() const { return input->getLinearVelocity(); }
126-
virtual Vec3f getAngularVelocity() const { return input->getAngularVelocity(); }
127123

128124
// Data getters
129125
virtual bool hasField(rgl_field_t field) const { return input->hasField(field); }

src/graph/NodesCore.hpp

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <math/Aabb.h>
3535
#include <math/RunningStats.hpp>
3636
#include <gpu/MultiReturn.hpp>
37+
#include <Time.hpp>
3738

3839

3940
struct FormatPointsNode : IPointsNodeSingleInput
@@ -116,8 +117,6 @@ struct RaytraceNode : IPointsNode
116117
size_t getHeight() const override { return 1; } // TODO: implement height in use_rays
117118

118119
Mat3x4f getLookAtOriginTransform() const override { return raysNode->getCumulativeRayTransfrom().inverse(); }
119-
Vec3f getLinearVelocity() const override { return sensorLinearVelocityXYZ; }
120-
Vec3f getAngularVelocity() const override { return sensorAngularVelocityRPY; }
121120

122121
// Data getters
123122
IAnyArray::ConstPtr getFieldData(rgl_field_t field) override
@@ -656,8 +655,11 @@ struct RadarTrackObjectsNode : IPointsNodeSingleInput
656655

657656
struct ObjectBounds
658657
{
658+
Field<ENTITY_ID_I32>::type mostCommonEntityId = RGL_ENTITY_INVALID_ID;
659659
Vec3f position{0};
660660
Aabb3Df aabb{};
661+
Vec3f absVelocity{};
662+
Vec3f relVelocity{};
661663
};
662664

663665
struct ClassificationProbabilities
@@ -684,20 +686,26 @@ struct RadarTrackObjectsNode : IPointsNodeSingleInput
684686

685687
RunningStats<Vec3f> position{};
686688
RunningStats<float> orientation{};
687-
RunningStats<Vec2f> absVelocity{};
688-
RunningStats<Vec2f> relVelocity{};
689-
RunningStats<Vec2f> absAccel{};
690-
RunningStats<Vec2f> relAccel{};
689+
RunningStats<Vec3f> absVelocity{};
690+
RunningStats<Vec3f> relVelocity{};
691+
RunningStats<Vec3f> absAccel{};
692+
RunningStats<Vec3f> relAccel{};
691693
RunningStats<float> orientationRate{};
692694
RunningStats<float> length{};
693695
RunningStats<float> width{};
696+
697+
// Workaround to be able to publish objects in the sensor frame via UDP
698+
// Transform points node cannot transform ObjectState
699+
Vec3f positionSensorFrame{};
694700
};
695701

696702
RadarTrackObjectsNode();
697703

698704
void setParameters(float distanceThreshold, float azimuthThreshold, float elevationThreshold, float radialSpeedThreshold,
699705
float maxMatchingDistance, float maxPredictionTimeFrame, float movementSensitivity);
700706

707+
void setObjectClasses(const int32_t* entityIds, const rgl_radar_object_class_t* objectClasses, int32_t count);
708+
701709
void enqueueExecImpl() override;
702710

703711
bool hasField(rgl_field_t field) const override { return fieldData.contains(field); }
@@ -713,13 +721,16 @@ struct RadarTrackObjectsNode : IPointsNodeSingleInput
713721

714722
private:
715723
Vec3f predictObjectPosition(const ObjectState& objectState, double deltaTimeMs) const;
724+
void parseEntityIdToClassProbability(Field<ENTITY_ID_I32>::type entityId, ClassificationProbabilities& probabilities);
716725
void createObjectState(const ObjectBounds& objectBounds, double currentTimeMs);
717726
void updateObjectState(ObjectState& objectState, const Vec3f& updatedPosition, const Aabb3Df& updatedAabb,
718-
ObjectStatus objectStatus, double currentTimeMs, double deltaTimeMs);
727+
ObjectStatus objectStatus, double currentTimeMs, double deltaTimeMs, const Vec3f& absVelocity,
728+
const Vec3f& relVelocity);
719729
void updateOutputData();
720730

721731
std::list<ObjectState> objectStates;
722-
std::unordered_map<rgl_field_t, IAnyArray::Ptr> fieldData;
732+
std::unordered_map<Field<ENTITY_ID_I32>::type, rgl_radar_object_class_t> entityIdsToClasses;
733+
std::unordered_map<rgl_field_t, IAnyArray::Ptr> fieldData; // All should be DeviceAsyncArray
723734

724735
uint32_t objectIDCounter = 0; // Not static - I assume each ObjectTrackingNode is like a separate radar.
725736
std::queue<uint32_t> objectIDPoll;
@@ -734,14 +745,25 @@ struct RadarTrackObjectsNode : IPointsNodeSingleInput
734745
float maxPredictionTimeFrame =
735746
500.0f; // Maximum time in milliseconds that can pass between two detections of the same object.
736747
// In other words, how long object state can be predicted until it will be declared lost.
737-
float movementSensitivity = 0.01f; // Max position change for an object to be qualified as MovementStatus::Stationary.
748+
float movementSensitivity = 0.01f; // Max velocity for an object to be qualified as MovementStatus::Stationary.
749+
750+
Mat3x4f lookAtSensorFrameTransform { Mat3x4f::identity() };
751+
decltype(Time::zero().asMilliseconds()) currentTime { Time::zero().asMilliseconds() };
738752

739753
HostPinnedArray<Field<XYZ_VEC3_F32>::type>::Ptr xyzHostPtr = HostPinnedArray<Field<XYZ_VEC3_F32>::type>::create();
740754
HostPinnedArray<Field<DISTANCE_F32>::type>::Ptr distanceHostPtr = HostPinnedArray<Field<DISTANCE_F32>::type>::create();
741755
HostPinnedArray<Field<AZIMUTH_F32>::type>::Ptr azimuthHostPtr = HostPinnedArray<Field<AZIMUTH_F32>::type>::create();
742756
HostPinnedArray<Field<ELEVATION_F32>::type>::Ptr elevationHostPtr = HostPinnedArray<Field<ELEVATION_F32>::type>::create();
743757
HostPinnedArray<Field<RADIAL_SPEED_F32>::type>::Ptr radialSpeedHostPtr =
744758
HostPinnedArray<Field<RADIAL_SPEED_F32>::type>::create();
759+
HostPinnedArray<Field<ENTITY_ID_I32>::type>::Ptr entityIdHostPtr = HostPinnedArray<Field<ENTITY_ID_I32>::type>::create();
760+
HostPinnedArray<Field<RELATIVE_VELOCITY_VEC3_F32>::type>::Ptr velocityRelHostPtr =
761+
HostPinnedArray<Field<RELATIVE_VELOCITY_VEC3_F32>::type>::create();
762+
HostPinnedArray<Field<ABSOLUTE_VELOCITY_VEC3_F32>::type>::Ptr velocityAbsHostPtr =
763+
HostPinnedArray<Field<ABSOLUTE_VELOCITY_VEC3_F32>::type>::create();
764+
765+
HostPinnedArray<Field<XYZ_VEC3_F32>::type>::Ptr outXyzHostPtr = HostPinnedArray<Field<XYZ_VEC3_F32>::type>::create();
766+
HostPinnedArray<Field<ENTITY_ID_I32>::type>::Ptr outEntityIdHostPtr = HostPinnedArray<Field<ENTITY_ID_I32>::type>::create();
745767
};
746768

747769
struct FilterGroundPointsNode : IPointsNodeSingleInput

src/graph/RadarPostprocessPointsNode.cpp

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,20 @@ void RadarPostprocessPointsNode::RadarCluster::addPoint(Field<RAY_IDX_U32>::type
278278
minMaxDistance[1] = std::max(minMaxDistance[1], distance);
279279
minMaxAzimuth[0] = std::min(minMaxAzimuth[0], azimuth);
280280
minMaxAzimuth[1] = std::max(minMaxAzimuth[1], azimuth);
281-
minMaxRadialSpeed[0] = std::min(minMaxRadialSpeed[0], radialSpeed);
282-
minMaxRadialSpeed[1] = std::max(minMaxRadialSpeed[1], radialSpeed);
281+
282+
// There are three reasonable cases that should be handled as intended:
283+
// - limit and radialSpeed are nans - limit will be set to nan (from radialSpeed)
284+
// - limit is nan and radialSpeed if a number - limit will be set to a number (from radialSpeed)
285+
// - limit and radialSpeed are numbers - std::min will be called.
286+
// There can technically be a fourth case:
287+
// - limit is a number and radialSpeed is nan - this should technically be handled via order of arguments in std::min
288+
// (assumption that comparison inside will return false); what is more important, such candidate should be eliminated
289+
// with isCandidate method;
290+
// The fourth case should not occur - radial speed is either nan for all entities (first raycast, with delta time = 0) or
291+
// has value for all entities.
292+
minMaxRadialSpeed[0] = std::isnan(minMaxRadialSpeed[0]) ? radialSpeed : std::min(minMaxRadialSpeed[0], radialSpeed);
293+
minMaxRadialSpeed[1] = std::isnan(minMaxRadialSpeed[1]) ? radialSpeed : std::max(minMaxRadialSpeed[1], radialSpeed);
294+
283295
minMaxElevation[0] = std::min(minMaxElevation[0], elevation);
284296
minMaxElevation[1] = std::max(minMaxElevation[1], elevation);
285297
}
@@ -291,12 +303,22 @@ inline bool RadarPostprocessPointsNode::RadarCluster::isCandidate(float distance
291303
const auto isWithinDistanceUpperBound = distance - radarScope.distance_separation_threshold <= minMaxDistance[1];
292304
const auto isWithinAzimuthLowerBound = azimuth + radarScope.azimuth_separation_threshold >= minMaxAzimuth[0];
293305
const auto isWithinAzimuthUpperBound = azimuth - radarScope.azimuth_separation_threshold <= minMaxAzimuth[1];
306+
294307
const auto isWithinRadialSpeedLowerBound = radialSpeed + radarScope.radial_speed_separation_threshold >=
295308
minMaxRadialSpeed[0];
296309
const auto isWithinRadialSpeedUpperBound = radialSpeed - radarScope.radial_speed_separation_threshold <=
297310
minMaxRadialSpeed[1];
311+
312+
// Radial speed may be nan if this is the first raytracing call (delta time equals 0). When cluster has nan radial speed
313+
// limits (technically just do not have radial speed information), the goal below is to ignore radial speed checkout. The
314+
// next goal is to allow adding points to cluster, if these points have non nan radial speed - to eliminate undefined
315+
// radial speed information from cluster. This should also work well, when limits are fine and candidate's radial speed
316+
// is nan - then radial speed checkouts will give false and the candidate will not pass.
317+
// Context for radial speed checkouts below - this can be interpreted as "true if any radial speed limit is nan or
318+
// candidate's radial speed is within limits"
298319
return isWithinDistanceLowerBound && isWithinDistanceUpperBound && isWithinAzimuthLowerBound && isWithinAzimuthUpperBound &&
299-
isWithinRadialSpeedLowerBound && isWithinRadialSpeedUpperBound;
320+
(std::isunordered(minMaxRadialSpeed[0], minMaxRadialSpeed[1]) ||
321+
(isWithinRadialSpeedLowerBound && isWithinRadialSpeedUpperBound));
300322
}
301323

302324
inline bool RadarPostprocessPointsNode::RadarCluster::canMergeWith(const RadarCluster& other,
@@ -330,7 +352,10 @@ inline bool RadarPostprocessPointsNode::RadarCluster::canMergeWith(const RadarCl
330352
areRangesWithinThreshold(minMaxRadialSpeed, other.minMaxRadialSpeed,
331353
radarScope->radial_speed_separation_threshold);
332354

333-
return isDistanceGood && isAzimuthGood && isRadialSpeedGood;
355+
// Radial speed check is ignored if one of limits on it, in one of clusters, is nan.
356+
return isDistanceGood && isAzimuthGood &&
357+
(isRadialSpeedGood || std::isunordered(minMaxRadialSpeed[0], minMaxRadialSpeed[1]) ||
358+
std::isunordered(other.minMaxRadialSpeed[0], other.minMaxRadialSpeed[1]));
334359
}
335360

336361
void RadarPostprocessPointsNode::RadarCluster::takeIndicesFrom(RadarCluster&& other)
@@ -339,8 +364,8 @@ void RadarPostprocessPointsNode::RadarCluster::takeIndicesFrom(RadarCluster&& ot
339364
minMaxDistance[1] = std::max(minMaxDistance[1], other.minMaxDistance[1]);
340365
minMaxAzimuth[0] = std::min(minMaxAzimuth[0], other.minMaxAzimuth[0]);
341366
minMaxAzimuth[1] = std::max(minMaxAzimuth[1], other.minMaxAzimuth[1]);
342-
minMaxRadialSpeed[0] = std::min(minMaxRadialSpeed[0], other.minMaxRadialSpeed[0]);
343-
minMaxRadialSpeed[1] = std::max(minMaxRadialSpeed[1], other.minMaxRadialSpeed[1]);
367+
minMaxRadialSpeed[0] = std::isnan(other.minMaxRadialSpeed[0]) ? minMaxRadialSpeed[0] : std::min(other.minMaxRadialSpeed[0], minMaxRadialSpeed[0]);
368+
minMaxRadialSpeed[1] = std::isnan(other.minMaxRadialSpeed[1]) ? minMaxRadialSpeed[1] : std::min(other.minMaxRadialSpeed[1], minMaxRadialSpeed[1]);
344369
minMaxElevation[0] = std::min(minMaxElevation[0], other.minMaxElevation[0]);
345370
minMaxElevation[1] = std::max(minMaxElevation[1], other.minMaxElevation[1]);
346371

0 commit comments

Comments
 (0)