• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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