using System; using MyParking.Shared; namespace MultiWheelC.StateEstimation { /// /// 根据连续有效的Detour世界位姿和真实时间差估算车辆二维速度。 /// public sealed class VelocityEstimator2D { public const double DefaultLinearFilterTimeConstantSeconds = 0.15; public const double DefaultAngularFilterTimeConstantSeconds = 0.20; private readonly FirstOrderLowPassFilter _worldVelocityXFilter; private readonly FirstOrderLowPassFilter _worldVelocityYFilter; private readonly FirstOrderLowPassFilter _angularVelocityFilter; private bool _hasPreviousSample; private Pose2D _previousPoseInWorld; private double _previousTimestampSeconds; /// /// 创建使用默认0.15s线速度和0.20s角速度时间常数的估计器。 /// public VelocityEstimator2D() : this( DefaultLinearFilterTimeConstantSeconds, DefaultAngularFilterTimeConstantSeconds) { } /// /// 创建使用指定线速度和角速度滤波时间常数的估计器。 /// public VelocityEstimator2D( double linearFilterTimeConstantSeconds, double angularFilterTimeConstantSeconds) { _worldVelocityXFilter = new FirstOrderLowPassFilter( linearFilterTimeConstantSeconds); _worldVelocityYFilter = new FirstOrderLowPassFilter( linearFilterTimeConstantSeconds); _angularVelocityFilter = new FirstOrderLowPassFilter( angularFilterTimeConstantSeconds); } /// /// 获取是否已经保存了可用于下一次差分的位姿基准。 /// public bool HasPreviousSample => _hasPreviousSample; /// /// 使用一个新的有效定位样本更新并返回车辆状态。 /// public VehicleState Update( Pose2D poseInWorld, double sampleTimestampSeconds) { EnsureFinitePose( poseInWorld, nameof(poseInWorld)); EnsureFiniteNonNegative( sampleTimestampSeconds, nameof(sampleTimestampSeconds)); var normalizedPoseInWorld = new Pose2D( poseInWorld.XMeters, poseInWorld.YMeters, AngleMath.NormalizeRadians( poseInWorld.YawRadians)); if (!_hasPreviousSample) { return Reset( normalizedPoseInWorld, sampleTimestampSeconds); } var deltaTimeSeconds = sampleTimestampSeconds - _previousTimestampSeconds; if (deltaTimeSeconds <= 0.0) { throw new ArgumentOutOfRangeException( nameof(sampleTimestampSeconds), "新定位样本的单调时间戳必须严格大于上一帧。"); } var rawVelocityXInWorld = (normalizedPoseInWorld.XMeters - _previousPoseInWorld.XMeters) / deltaTimeSeconds; var rawVelocityYInWorld = (normalizedPoseInWorld.YMeters - _previousPoseInWorld.YMeters) / deltaTimeSeconds; var rawAngularVelocity = AngleMath.ShortestDifferenceRadians( normalizedPoseInWorld.YawRadians, _previousPoseInWorld.YawRadians) / deltaTimeSeconds; var filteredTwistInWorld = new Twist2D( _worldVelocityXFilter.Update( rawVelocityXInWorld, deltaTimeSeconds), _worldVelocityYFilter.Update( rawVelocityYInWorld, deltaTimeSeconds), _angularVelocityFilter.Update( rawAngularVelocity, deltaTimeSeconds)); _previousPoseInWorld = normalizedPoseInWorld; _previousTimestampSeconds = sampleTimestampSeconds; return new VehicleState( sampleTimestampSeconds, normalizedPoseInWorld, filteredTwistInWorld, true); } /// /// 更新位姿差分基准但保留当前滤波速度,避免定位跳变形成虚假速度尖峰。 /// public VehicleState RebasePreservingVelocity( Pose2D poseInWorld, double sampleTimestampSeconds) { EnsureFinitePose( poseInWorld, nameof(poseInWorld)); EnsureFiniteNonNegative( sampleTimestampSeconds, nameof(sampleTimestampSeconds)); var normalizedPoseInWorld = new Pose2D( poseInWorld.XMeters, poseInWorld.YMeters, AngleMath.NormalizeRadians( poseInWorld.YawRadians)); _previousPoseInWorld = normalizedPoseInWorld; _previousTimestampSeconds = sampleTimestampSeconds; _hasPreviousSample = true; var hasValidVelocityEstimate = _worldVelocityXFilter.IsInitialized && _worldVelocityYFilter.IsInitialized && _angularVelocityFilter.IsInitialized; var retainedTwistInWorld = hasValidVelocityEstimate ? new Twist2D( _worldVelocityXFilter.Value, _worldVelocityYFilter.Value, _angularVelocityFilter.Value) : Twist2D.Zero; return new VehicleState( sampleTimestampSeconds, normalizedPoseInWorld, retainedTwistInWorld, hasValidVelocityEstimate); } /// /// 使用当前定位重新建立差分基准,并返回速度无效的零速状态。 /// public VehicleState Reset( Pose2D poseInWorld, double sampleTimestampSeconds) { EnsureFinitePose( poseInWorld, nameof(poseInWorld)); EnsureFiniteNonNegative( sampleTimestampSeconds, nameof(sampleTimestampSeconds)); _previousPoseInWorld = new Pose2D( poseInWorld.XMeters, poseInWorld.YMeters, AngleMath.NormalizeRadians( poseInWorld.YawRadians)); _previousTimestampSeconds = sampleTimestampSeconds; _hasPreviousSample = true; _worldVelocityXFilter.Reset(); _worldVelocityYFilter.Reset(); _angularVelocityFilter.Reset(); return new VehicleState( sampleTimestampSeconds, _previousPoseInWorld, Twist2D.Zero, false); } /// /// 清除差分基准和全部滤波历史,使下一帧重新初始化估计器。 /// public void Reset() { _hasPreviousSample = false; _previousPoseInWorld = Pose2D.Identity; _previousTimestampSeconds = 0.0; _worldVelocityXFilter.Reset(); _worldVelocityYFilter.Reset(); _angularVelocityFilter.Reset(); } /// /// 检查位姿是否由有限数值组成。 /// private static void EnsureFinitePose( Pose2D pose, string parameterName) { if (!IsFinite(pose.XMeters) || !IsFinite(pose.YMeters) || !IsFinite(pose.YawRadians)) { throw new ArgumentOutOfRangeException( parameterName, "速度估计使用的车辆位姿必须由有限数值组成。"); } } /// /// 检查数值是否为非负有限值。 /// private static void EnsureFiniteNonNegative( double value, string parameterName) { if (!IsFinite(value) || value < 0.0) { throw new ArgumentOutOfRangeException( parameterName, "速度估计使用的采样时刻必须是非负有限值。"); } } /// /// 判断数值是否可用于速度估计。 /// private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } } }