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