1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.uwb.correction.primers; 17 18 import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI; 19 import static com.android.server.uwb.correction.math.MathHelper.F_PI; 20 import static com.android.server.uwb.correction.math.MathHelper.MS_PER_SEC; 21 import static com.android.server.uwb.correction.math.MathHelper.normalizeRadians; 22 23 import static java.lang.Math.abs; 24 import static java.lang.Math.exp; 25 import static java.lang.Math.max; 26 import static java.lang.Math.min; 27 import static java.lang.Math.signum; 28 import static java.lang.Math.toDegrees; 29 30 import android.os.Build; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import com.android.server.uwb.correction.math.MathHelper; 37 import com.android.server.uwb.correction.math.Pose; 38 import com.android.server.uwb.correction.math.SphericalVector; 39 import com.android.server.uwb.correction.math.SphericalVector.Annotated; 40 import com.android.server.uwb.correction.pose.IPoseSource; 41 42 import java.util.ArrayDeque; 43 import java.util.Queue; 44 45 /** 46 * Tracks the correlation between azimuth and pose yaw to determine if the controlee has gone 47 * to the other side of the device. This mirrors the azimuth value if: 48 * - The prediction crosses the ±90° threshold. 49 * - The covariance between pose yaw and azimuth exceeds a certain threshold. 50 */ 51 public class BackAzimuthPrimer implements IPrimer { 52 static final String TAG = "BackAzimuthPrimer"; 53 /** The decay rate of the low pass filter. (0-1, decay per sample) */ 54 private static final double FOM_DECAY = 0.1; 55 /** How quickly the FOM falls when using mirrored predictions. */ 56 private static final int MINIMUM_DETERMINATIONS = 4; 57 58 private static final boolean sDebug; 59 // Keep sample time measurements reasonable to prevent div/0 or overflows. 60 private static final long MIN_TIME_MS = 5; // Shortest allowed time between ranging samples 61 private static final long MAX_TIME_MS = 5000; // Longest allowed time between ranging samples 62 63 static { 64 sDebug = (Build.TYPE != null && Build.TYPE.equals("userdebug")) 65 || System.getProperty("DEBUG") != null; 66 } 67 68 private final boolean mMaskRawAzimuthWhenBackfacing; 69 private final float mDiscrepancyCoefficient; 70 private final int mWindowSize; 71 private final float mStdDev; 72 private final float mNormalThresholdRadPerSec; 73 private final float mMirrorThresholdRadPerSec; 74 private final Queue<Float> mScoreHistory; 75 private final Queue<Float> mDiscrepancyHistory = new ArrayDeque<>(); 76 private boolean mMirrored = false; 77 private float mLastAzimuthPrediction; 78 // This initial value causes the first sample to have the least effect. 79 private long mLastSampleTimeMs = Long.MIN_VALUE; 80 private Pose mLastPose; 81 private SphericalVector mLastInput; 82 private int mDeterminationCount = 0; 83 private double mFomFilterValue = MINIMUM_FOM; 84 private double mLastGoodReferenceTimeMs; 85 86 /** 87 * Creates a new instance of the BackAzimuthPrimer class. 88 * 89 * @param normalThresholdRadPerSec How many radians per second of correlated rotation are 90 * necessary to force a non-mirrored azimuth. 91 * @param mirrorThresholdRadPerSec How many radians per second of correlated rotation are 92 * necessary to force a mirrored azimuth. 93 * @param windowSize The size of the moving window filter for determining 94 * correlation. 95 * @param maskRawAzimuthWhenBackfacing If true, readings from the back will be replaced with 96 * predictions. If false, azimuth readings will be mirrored 97 * front-to-back. 98 * @param stdDev Controls the width of the curve used to judge if the 99 * readings are acting like 100 * unmirrored or mirrored readings. 101 * @param discrepancyCoefficient The coefficient of how much the typical forward 102 * prediction 103 * error (rads per sample) should count against the forward 104 * score (rads per second). For 105 * example, a value of 0.5 will cause the score to be 5 106 * degrees lower if the typical front 107 * prediction error is 10. 108 */ BackAzimuthPrimer(float normalThresholdRadPerSec, float mirrorThresholdRadPerSec, int windowSize, boolean maskRawAzimuthWhenBackfacing, float stdDev, float discrepancyCoefficient )109 public BackAzimuthPrimer(float normalThresholdRadPerSec, 110 float mirrorThresholdRadPerSec, 111 int windowSize, 112 boolean maskRawAzimuthWhenBackfacing, 113 float stdDev, 114 float discrepancyCoefficient 115 ) { 116 mNormalThresholdRadPerSec = normalThresholdRadPerSec; 117 mMirrorThresholdRadPerSec = mirrorThresholdRadPerSec; 118 mMaskRawAzimuthWhenBackfacing = maskRawAzimuthWhenBackfacing; 119 mDiscrepancyCoefficient = discrepancyCoefficient; 120 mScoreHistory = new ArrayDeque<>(); 121 mWindowSize = windowSize; 122 mStdDev = stdDev; 123 } 124 125 /** 126 * Uses pose information to disambiguate the input azimuth. 127 * 128 * @param input The original UWB reading. 129 * @param prediction The previous filtered UWB result adjusted by the pose change since then. 130 * @param poseSource A pose source that may indicate phone orientation. 131 * @param timeMs When the input occurred, in ms since boot. 132 * @return A replacement value for the UWB vector that has been corrected for the situation. 133 */ 134 @SuppressWarnings("ConstantConditions") /* Unboxing longs in mCaptureTimes */ 135 @Override prime( @onNull SphericalVector.Annotated input, @Nullable SphericalVector prediction, @Nullable IPoseSource poseSource, long timeMs)136 public SphericalVector.Annotated prime( 137 @NonNull SphericalVector.Annotated input, 138 @Nullable SphericalVector prediction, 139 @Nullable IPoseSource poseSource, 140 long timeMs) { 141 if (!input.hasAzimuth || poseSource == null || prediction == null) { 142 // Can't perform correction if there is no azimuth data, no pose information, 143 // or no prediction. 144 return input; 145 } 146 147 long timeDeltaMs = min(MAX_TIME_MS, max(MIN_TIME_MS, timeMs - mLastSampleTimeMs)); 148 float timeScale = (float) MathHelper.MS_PER_SEC / timeDeltaMs; 149 mLastSampleTimeMs = timeMs; 150 151 // Mirror if the pose simply rotated the azimuth past the 90-degree mark. 152 if (abs(prediction.azimuth) > F_HALF_PI 153 && abs(mLastAzimuthPrediction) <= F_HALF_PI) { 154 // Prediction went from forward to backward-facing. 155 mMirrored = true; 156 flipScoreHistory(); 157 } else if (abs(prediction.azimuth) <= F_HALF_PI 158 && abs(mLastAzimuthPrediction) > F_HALF_PI) { 159 // Prediction went from backward to forward-facing. 160 mMirrored = false; 161 flipScoreHistory(); 162 } 163 mLastAzimuthPrediction = prediction.azimuth; 164 // Because the prediction is influenced by mirroring itself and can have a significant 165 // delay due to the filter, we will not be using the prediction to guess front/back. 166 167 // Get coordinates for normal and mirrored azimuth versions of the input. 168 // input.vector may be >90deg due to previous primers, so front/back is forced here. 169 SphericalVector normalInput = forceAzimuth(input, false); 170 SphericalVector mirrorInput = forceAzimuth(input, true); 171 172 Pose newPose = poseSource.getPose(); 173 if (mLastPose == null || newPose == null || mLastPose == newPose || mLastInput == null) { 174 // Can't do anything without pose deltas and input history. 175 mLastPose = newPose; 176 mLastInput = normalInput; 177 return input; 178 } 179 180 // Pose transform for theorizing how the previous reading might have changed. 181 // Note that we're using a full pose transform instead of just azimuth changes, as 182 // the phone may have rolled or performed other movements that aren't just azimuth. 183 Pose deltaPose = Pose.compose(newPose.inverted(), mLastPose); 184 185 // Theorize, based on the old location, what the new location should be for mirrored and 186 // unmirrored inputs. 187 SphericalVector normalTheory = transformSpherical(mLastInput, deltaPose); 188 SphericalVector mirrorTheory = transformSpherical(mirrorAzimuth(mLastInput), deltaPose); 189 190 // Compute how many radians of pose change have affected the azimuth. More movement means 191 // more certainty can be applied to the score. 192 float azimuthDeltaFromPoseRad = 193 normalizeRadians(abs(normalTheory.azimuth - mLastInput.azimuth)); 194 195 // Judge how well the front and back predictions did. 196 float normalDifference = abs(normalizeRadians(normalTheory.azimuth - normalInput.azimuth)); 197 float mirrorDifference = abs(normalizeRadians(mirrorTheory.azimuth - mirrorInput.azimuth)); 198 // Note that one of these predictions will be perfect if the input itself is a prediction, 199 // which FovPrimer might do. Carrying this detail in SphericalVector.Sparse may provide an 200 // opportunity to ignore scoring when the input is predicted. 201 float normalAccuracy = bell(normalDifference, mStdDev); 202 float mirrorAccuracy = bell(mirrorDifference, mStdDev); 203 204 // Score by the user's pose-induced azimuth change. 205 float scoreRadPerSec = (normalAccuracy - mirrorAccuracy) // score per sample 206 * azimuthDeltaFromPoseRad // convert to score radians per sample 207 * timeScale; // convert to score radians per second 208 209 // Bias the score toward the back based on UWB noise. 210 float scoreRadPerSecBiased = biasScore(scoreRadPerSec, normalDifference, mirrorDifference); 211 212 mLastInput = normalInput; 213 mLastPose = newPose; 214 215 mScoreHistory.offer(scoreRadPerSecBiased); 216 217 double typScore = 0; 218 if (mScoreHistory.size() > mWindowSize) { 219 mScoreHistory.poll(); 220 // Get the median score. 221 typScore = mScoreHistory.stream().mapToDouble(Float::doubleValue).sorted() 222 .skip(mWindowSize / 2).limit(1 + (mWindowSize % 2)).average().getAsDouble(); 223 224 // Finally, the mirroring decision. 225 if (typScore > mNormalThresholdRadPerSec) { 226 mMirrored = false; 227 if (mDeterminationCount < MINIMUM_DETERMINATIONS) { 228 mDeterminationCount++; 229 } 230 } else if (typScore < -mMirrorThresholdRadPerSec) { 231 mMirrored = true; 232 if (mDeterminationCount < MINIMUM_DETERMINATIONS) { 233 mDeterminationCount++; 234 } 235 } 236 } 237 238 if (sDebug) { 239 Log.d(TAG, 240 String.format( 241 "time %4d, pose % 6.1f, nd % 6.1f (%3d%%), md % 6.1f (%3d%%), " 242 + "rawSco % 5.1f, sco % 5.1f, aggSco % 5.1f, %s", 243 timeDeltaMs, 244 toDegrees(azimuthDeltaFromPoseRad), 245 toDegrees(normalDifference), (int) (normalAccuracy * 100), 246 toDegrees(mirrorDifference), (int) (mirrorAccuracy * 100), 247 toDegrees(scoreRadPerSec), 248 toDegrees(scoreRadPerSecBiased), 249 toDegrees(typScore), 250 mMirrored ? "mirr" : "norm" 251 )); 252 } 253 254 SphericalVector result = input; 255 256 if (mMirrored && mMaskRawAzimuthWhenBackfacing) { 257 // Replace angles with prediction. The mMaskRawAzimuthWhenBackfacing setting will be set 258 // when through-device readings are poor or not predictably mirrored. 259 result = SphericalVector.fromRadians( 260 prediction.azimuth, 261 prediction.elevation, 262 input.distance); 263 } 264 265 result = forceAzimuth(result, mMirrored); 266 267 Annotated annotatedResult = new Annotated( 268 result, 269 true, 270 input.hasElevation, 271 input.hasDistance).copyFomFrom(input); 272 273 updateFom(annotatedResult, normalAccuracy, mirrorAccuracy); 274 275 return annotatedResult; 276 } 277 278 /** 279 * Changes the azimuthFom of the annotated result to reflect 3 conditions: 280 * 1. How long the filter has been using predicted values 281 * 2. How well-correlated yaw and azimuth are (normal or mirror Accuracy) 282 * 3. Whether or not a number of initial good determinations have been made. 283 * @param annotatedResult The reading whose azimuthFom will be updated. 284 * @param normalAccuracy The computed accuracy of correlation for the normal case. 285 * @param mirrorAccuracy The computed accuracy of correlation for the mirrored case. 286 */ updateFom(Annotated annotatedResult, float normalAccuracy, float mirrorAccuracy)287 private void updateFom(Annotated annotatedResult, float normalAccuracy, float mirrorAccuracy) { 288 double newFom; 289 if (mMirrored) { 290 newFom = mirrorAccuracy; 291 if (mMaskRawAzimuthWhenBackfacing) { 292 // We've gone totally to predictions. Tweak the FOM based on how fresh our data is. 293 double elapsedMs = mLastSampleTimeMs - mLastGoodReferenceTimeMs; 294 double fom = max(1 - elapsedMs / MS_PER_SEC * FALLOFF_FOM_PER_SEC, MINIMUM_FOM); 295 annotatedResult.azimuthFom *= fom; 296 } 297 } else { 298 newFom = normalAccuracy; 299 300 // This brings the FOM back up for subsequent estimations 301 mLastGoodReferenceTimeMs = mLastSampleTimeMs; 302 } 303 mFomFilterValue = mFomFilterValue * (1 - FOM_DECAY) + (newFom * FOM_DECAY); 304 annotatedResult.azimuthFom *= mFomFilterValue; 305 306 if (mDeterminationCount < MINIMUM_DETERMINATIONS) { 307 // If we haven't actually seen good evidence of front or back, our certainty is up to 308 // 50% less, depending on how many determinations have been made. 309 annotatedResult.azimuthFom *= 310 0.5 + ((double) mDeterminationCount) / MINIMUM_DETERMINATIONS / 2; 311 } 312 } 313 314 /** Flips the score history, for when azimuth goes from front to behind or vice-versa. */ flipScoreHistory()315 private void flipScoreHistory() { 316 for (int x = 0; x < mScoreHistory.size(); x++) { 317 Float sh = mScoreHistory.poll(); 318 mScoreHistory.offer(sh == null ? null : -sh); 319 } 320 } 321 322 /** 323 * Biases a score toward the back based on the accuracy of front-facing predictions. 324 * 325 * @param scoreRadPerSec The score to bias. 326 * @param normalDifference The difference between the front-based prediction and front-based 327 * reading. 328 * @param mirrorDifference The difference between the back-based prediction and back-based 329 * reading. 330 * @return A new, adjusted version of the input score. 331 */ biasScore(float scoreRadPerSec, float normalDifference, float mirrorDifference)332 private float biasScore(float scoreRadPerSec, float normalDifference, float mirrorDifference) { 333 // Discrepancy measures how much the forward-facing values are off. They will be WAY off 334 // if the UWB signal is noisy, and slightly off if the signal is in the back. Rear azimuths 335 // are usually both. 336 mDiscrepancyHistory.offer(normalDifference); 337 338 if (mDiscrepancyHistory.size() > mWindowSize) { 339 mDiscrepancyHistory.poll(); 340 float avgDiscrepancyRad = 341 (float) mDiscrepancyHistory.stream().mapToDouble(Float::doubleValue).average() 342 .getAsDouble(); 343 344 // Average discrepancy is multiplied by the configurable coefficient to bias the 345 // score toward the back. 346 return scoreRadPerSec - avgDiscrepancyRad * mDiscrepancyCoefficient; 347 } 348 return scoreRadPerSec; 349 } 350 351 /** 352 * Applies a pose delta (a transform) to a spherical coordinate. 353 * 354 * @param input The spherical vector to transform. 355 * @param deltaPose The pose object representing how to transform the input. 356 * @return A new SphericalVector representing the input transformed by the delta pose. 357 */ transformSpherical(SphericalVector input, Pose deltaPose)358 private SphericalVector transformSpherical(SphericalVector input, Pose deltaPose) { 359 return SphericalVector.fromCartesian(deltaPose.transformPoint(input.toCartesian())); 360 } 361 362 /** 363 * Mirrors the azimuth front-to-back or back-to-front. 364 * 365 * @param vector The SphericalVector to mirror. 366 * @return A mirrored version of the SphericalVector. 367 */ 368 @NonNull mirrorAzimuth(SphericalVector vector)369 private SphericalVector mirrorAzimuth(SphericalVector vector) { 370 return SphericalVector.fromRadians( 371 signum(vector.azimuth) * (F_PI - abs(vector.azimuth)), 372 vector.elevation, 373 vector.distance); 374 } 375 376 /** 377 * Forces the azimuth to be front or back, mirroring it as necessary. 378 * 379 * @param vector The SphericalVector to force to a direction. 380 * @param back If true, forces the SphericalVector's azimuth to be back, otherwise forward. 381 * @return A version of the SphericalVector that is facing the specified direction. 382 */ 383 @NonNull forceAzimuth(SphericalVector vector, boolean back)384 private SphericalVector forceAzimuth(SphericalVector vector, boolean back) { 385 if (back == abs(vector.azimuth) < F_HALF_PI) { 386 return mirrorAzimuth(vector); 387 } 388 return vector; 389 } 390 391 /** 392 * Plots x on a bell curve with a magnitude of 1. This is a gaussian curve(φ) multiplied by 393 * 1/φ(0) so that bell(0, n) = 1. 394 * 395 * @param x The x value of the normal curve. 396 * @param stdDev The standard deviation to use for the initial normal curve. 397 * @return A value along a gaussian curve scaled by 1/φ(0). 398 */ bell(float x, float stdDev)399 private float bell(float x, float stdDev) { 400 float variance = stdDev * stdDev; 401 return (float) exp(-(x * x / (2 * variance))); 402 } 403 } 404