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