blob: 123cd73a3ff396fb95f78a4574011b6e0338e19a [file] [log] [blame]
Dan Gittik8dbd7e92018-12-03 15:35:53 +00001/*
2 * Copyright (C) 2019 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
17package com.android.server.display.whitebalance;
18
19import android.util.Slog;
20
Anthony Han04c78a92019-05-03 13:34:57 -070021import com.android.internal.annotations.VisibleForTesting;
Dan Gittik8dbd7e92018-12-03 15:35:53 +000022import com.android.server.display.utils.RollingBuffer;
23
24import java.io.PrintWriter;
25import java.util.Arrays;
26
27/**
28 * The DisplayWhiteBalanceController uses the AmbientFilter to average ambient changes over time,
29 * filter out the noise, and arrive at an estimate of the actual value.
30 *
31 * When the DisplayWhiteBalanceController detects a change in ambient brightness or color
32 * temperature, it passes it to the AmbientFilter, and when it needs the actual ambient value, it
33 * asks it for an estimate.
34 *
35 * Implementations:
36 * - {@link WeightedMovingAverageAmbientFilter}
37 * A weighted average prioritising recent changes.
38 */
39abstract class AmbientFilter {
40
41 protected static final boolean DEBUG = false; // Enable for verbose logs.
42
43 protected final String mTag;
44 protected boolean mLoggingEnabled;
45
46 // How long ambient value changes are kept and taken into consideration.
47 private final int mHorizon; // Milliseconds
48
49 private final RollingBuffer mBuffer;
50
51 /**
52 * @param tag
53 * The tag used for dumping and logging.
54 * @param horizon
55 * How long ambient value changes are kept and taken into consideration.
56 *
57 * @throws IllegalArgumentException
58 * - horizon is not positive.
59 */
60 AmbientFilter(String tag, int horizon) {
61 validateArguments(horizon);
62 mTag = tag;
63 mLoggingEnabled = false;
64 mHorizon = horizon;
65 mBuffer = new RollingBuffer();
66 }
67
68 /**
69 * Add an ambient value change.
70 *
71 * @param time
72 * The time.
73 * @param value
74 * The ambient value.
75 *
76 * @return Whether the method succeeded or not.
77 */
78 public boolean addValue(long time, float value) {
79 if (value < 0.0f) {
80 return false;
81 }
82 truncateOldValues(time);
83 if (mLoggingEnabled) {
84 Slog.d(mTag, "add value: " + value + " @ " + time);
85 }
86 mBuffer.add(time, value);
87 return true;
88 }
89
90 /**
91 * Get an estimate of the actual ambient color temperature.
92 *
93 * @param time
94 * The time.
95 *
96 * @return An estimate of the actual ambient color temperature.
97 */
98 public float getEstimate(long time) {
99 truncateOldValues(time);
100 final float value = filter(time, mBuffer);
101 if (mLoggingEnabled) {
102 Slog.d(mTag, "get estimate: " + value + " @ " + time);
103 }
104 return value;
105 }
106
107 /**
108 * Clears the filter state.
109 */
110 public void clear() {
111 mBuffer.clear();
112 }
113
114 /**
115 * Enable/disable logging.
116 *
117 * @param loggingEnabled
118 * Whether logging is on/off.
119 *
120 * @return Whether the method succeeded or not.
121 */
122 public boolean setLoggingEnabled(boolean loggingEnabled) {
123 if (mLoggingEnabled == loggingEnabled) {
124 return false;
125 }
126 mLoggingEnabled = loggingEnabled;
127 return true;
128 }
129
130 /**
131 * Dump the state.
132 *
133 * @param writer
134 * The PrintWriter used to dump the state.
135 */
136 public void dump(PrintWriter writer) {
137 writer.println(" " + mTag);
138 writer.println(" mLoggingEnabled=" + mLoggingEnabled);
139 writer.println(" mHorizon=" + mHorizon);
140 writer.println(" mBuffer=" + mBuffer);
141 }
142
143 private void validateArguments(int horizon) {
144 if (horizon <= 0) {
145 throw new IllegalArgumentException("horizon must be positive");
146 }
147 }
148
149 private void truncateOldValues(long time) {
150 final long minTime = time - mHorizon;
151 mBuffer.truncate(minTime);
152 }
153
154 protected abstract float filter(long time, RollingBuffer buffer);
155
156 /**
157 * A weighted average prioritising recent changes.
158 */
Anthony Han04c78a92019-05-03 13:34:57 -0700159 @VisibleForTesting
160 public static class WeightedMovingAverageAmbientFilter extends AmbientFilter {
Dan Gittik8dbd7e92018-12-03 15:35:53 +0000161
162 // How long the latest ambient value change is predicted to last.
163 private static final int PREDICTION_TIME = 100; // Milliseconds
164
165 // Recent changes are prioritised by integrating their duration over y = x + mIntercept
166 // (the higher it is, the less prioritised recent changes are).
167 private final float mIntercept;
168
169 /**
170 * @param tag
171 * The tag used for dumping and logging.
172 * @param horizon
173 * How long ambient value changes are kept and taken into consideration.
174 * @param intercept
175 * Recent changes are prioritised by integrating their duration over y = x + intercept
176 * (the higher it is, the less prioritised recent changes are).
177 *
178 * @throws IllegalArgumentException
179 * - horizon is not positive.
180 * - intercept is NaN or negative.
181 */
182 WeightedMovingAverageAmbientFilter(String tag, int horizon, float intercept) {
183 super(tag, horizon);
184 validateArguments(intercept);
185 mIntercept = intercept;
186 }
187
188 /**
189 * See {@link AmbientFilter#dump base class}.
190 */
191 @Override
192 public void dump(PrintWriter writer) {
193 super.dump(writer);
194 writer.println(" mIntercept=" + mIntercept);
195 }
196
197 // Normalise the times to [t1=0, t2, ..., tN, now + PREDICTION_TIME], so the first change
198 // starts at 0 and the last change is predicted to last a bit, and divide them by 1000 as
199 // milliseconds are high enough to overflow.
200 // The weight of the value from t[i] to t[i+1] is the area under (A.K.A. the integral of)
201 // y = x + mIntercept from t[i] to t[i+1].
202 @Override
203 protected float filter(long time, RollingBuffer buffer) {
204 if (buffer.isEmpty()) {
205 return -1.0f;
206 }
207 float total = 0.0f;
208 float totalWeight = 0.0f;
209 final float[] weights = getWeights(time, buffer);
210 if (DEBUG && mLoggingEnabled) {
211 Slog.v(mTag, "filter: " + buffer + " => " + Arrays.toString(weights));
212 }
213 for (int i = 0; i < weights.length; i++) {
214 final float value = buffer.getValue(i);
215 final float weight = weights[i];
216 total += weight * value;
217 totalWeight += weight;
218 }
219 if (totalWeight == 0.0f) {
220 return buffer.getValue(buffer.size() - 1);
221 }
222 return total / totalWeight;
223 }
224
225 private void validateArguments(float intercept) {
226 if (Float.isNaN(intercept) || intercept < 0.0f) {
227 throw new IllegalArgumentException("intercept must be a non-negative number");
228 }
229 }
230
231 private float[] getWeights(long time, RollingBuffer buffer) {
232 float[] weights = new float[buffer.size()];
233 final long startTime = buffer.getTime(0);
234 float previousTime = 0.0f;
235 for (int i = 1; i < weights.length; i++) {
236 final float currentTime = (buffer.getTime(i) - startTime) / 1000.0f;
237 final float weight = calculateIntegral(previousTime, currentTime);
238 weights[i - 1] = weight;
239 previousTime = currentTime;
240 }
241 final float lastTime = (time + PREDICTION_TIME - startTime) / 1000.0f;
242 final float lastWeight = calculateIntegral(previousTime, lastTime);
243 weights[weights.length - 1] = lastWeight;
244 return weights;
245 }
246
247 private float calculateIntegral(float from, float to) {
248 return antiderivative(to) - antiderivative(from);
249 }
250
251 private float antiderivative(float x) {
252 // f(x) = x + c => F(x) = 1/2 * x^2 + c * x
253 return 0.5f * x * x + mIntercept * x;
254 }
255
256 }
257
258}