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