Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1a30b550 authored by Jeff Brown's avatar Jeff Brown
Browse files

Use spline interpolation for auto-brightness.

Strictly speaking, this is a change in behavior for all products.
Instead of using discrete zones, they will all now use spline
interpolation.  We could make this behavior configurable
but there seems to be little point to it.  The range of brightness
values used will be more or less the same as before, it's just
that what used to be the brightness value for all levels within
a particular zone now becomes the brightness value for the
highest level in that zone and lower values are used for lower
levels within the zone.

Change-Id: I39804ee630ba55f018e1e53c0576b28e7bd27931
parent 270e3381
Loading
Loading
Loading
Loading
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2012 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 android.util;

/**
 * Performs spline interpolation given a set of control points.
 * @hide
 */
public final class Spline {
    private final float[] mX;
    private final float[] mY;
    private final float[] mM;

    private Spline(float[] x, float[] y, float[] m) {
        mX = x;
        mY = y;
        mM = m;
    }

    /**
     * Creates a monotone cubic spline from a given set of control points.
     *
     * The spline is guaranteed to pass through each control point exactly.
     * Moreover, assuming the control points are monotonic (Y is non-decreasing or
     * non-increasing) then the interpolated values will also be monotonic.
     *
     * This function uses the Fritsch-Carlson method for computing the spline parameters.
     * http://en.wikipedia.org/wiki/Monotone_cubic_interpolation
     *
     * @param x The X component of the control points, strictly increasing.
     * @param y The Y component of the control points, monotonic.
     * @return
     *
     * @throws IllegalArgumentException if the X or Y arrays are null, have
     * different lengths or have fewer than 2 values.
     * @throws IllegalArgumentException if the control points are not monotonic.
     */
    public static Spline createMonotoneCubicSpline(float[] x, float[] y) {
        if (x == null || y == null || x.length != y.length || x.length < 2) {
            throw new IllegalArgumentException("There must be at least two control "
                    + "points and the arrays must be of equal length.");
        }

        final int n = x.length;
        float[] d = new float[n - 1]; // could optimize this out
        float[] m = new float[n];

        // Compute slopes of secant lines between successive points.
        for (int i = 0; i < n - 1; i++) {
            float h = x[i + 1] - x[i];
            if (h <= 0f) {
                throw new IllegalArgumentException("The control points must all "
                        + "have strictly increasing X values.");
            }
            d[i] = (y[i + 1] - y[i]) / h;
        }

        // Initialize the tangents as the average of the secants.
        m[0] = d[0];
        for (int i = 1; i < n - 1; i++) {
            m[i] = (d[i - 1] + d[i]) * 0.5f;
        }
        m[n - 1] = d[n - 2];

        // Update the tangents to preserve monotonicity.
        for (int i = 0; i < n - 1; i++) {
            if (d[i] == 0f) { // successive Y values are equal
                m[i] = 0f;
                m[i + 1] = 0f;
            } else {
                float a = m[i] / d[i];
                float b = m[i + 1] / d[i];
                if (a < 0f || b < 0f) {
                    throw new IllegalArgumentException("The control points must have "
                            + "monotonic Y values.");
                }
                float h = FloatMath.hypot(a, b);
                if (h > 9f) {
                    float t = 3f / h;
                    m[i] = t * a * d[i];
                    m[i + 1] = t * b * d[i];
                }
            }
        }
        return new Spline(x, y, m);
    }

    /**
     * Interpolates the value of Y = f(X) for given X.
     * Clamps X to the domain of the spline.
     *
     * @param x The X value.
     * @return The interpolated Y = f(X) value.
     */
    public float interpolate(float x) {
        // Handle the boundary cases.
        final int n = mX.length;
        if (Float.isNaN(x)) {
            return x;
        }
        if (x <= mX[0]) {
            return mY[0];
        }
        if (x >= mX[n - 1]) {
            return mY[n - 1];
        }

        // Find the index 'i' of the last point with smaller X.
        // We know this will be within the spline due to the boundary tests.
        int i = 0;
        while (x >= mX[i + 1]) {
            i += 1;
            if (x == mX[i]) {
                return mY[i];
            }
        }

        // Perform cubic Hermite spline interpolation.
        float h = mX[i + 1] - mX[i];
        float t = (x - mX[i]) / h;
        return (mY[i] * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t)
                + (mY[i + 1] * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t;
    }

    // For debugging.
    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        final int n = mX.length;
        str.append("[");
        for (int i = 0; i < n; i++) {
            if (i != 0) {
                str.append(", ");
            }
            str.append("(").append(mX[i]);
            str.append(", ").append(mY[i]);
            str.append(": ").append(mM[i]).append(")");
        }
        str.append("]");
        return str.toString();
    }
}
+17 −5
Original line number Diff line number Diff line
@@ -533,13 +533,22 @@
    <integer name="config_longPressOnHomeBehavior">2</integer>

    <!-- Array of light sensor LUX values to define our levels for auto backlight brightness support.
         The N entries of this array define N + 1 zones as follows:
         The N entries of this array define N + 1 control points as follows:

         Zone 0:        0 <= LUX < array[0]
         Zone 1:        array[0] <= LUX < array[1]
         Point 1:        LUX <= 0 (implicit)
         Point 2:        0 < level[1] == LUX < level[2]
         ...
         Zone N:        array[N - 1] <= LUX < array[N]
         Zone N + 1:    array[N] <= LUX < infinity
         Point N:        level[N - 1] == LUX < level[N]
         Point N + 1:    level[N] <= LUX < infinity

         The control points must be strictly increasing.  Each control point
         corresponds to an entry in the brightness backlight values arrays.
         For example, if LUX == level[1] (first element of the levels array)
         then the brightness will be determined by value[1] (first element
         of the brightness values array).

         Spline interpolation is used to determine the auto-brightness
         backlight values for LUX levels between these control points.

         Must be overridden in platform specific overlays -->
    <integer-array name="config_autoBrightnessLevels">
@@ -552,6 +561,7 @@
    <!-- Array of output values for LCD backlight corresponding to the LUX values
         in the config_autoBrightnessLevels array.  This array should have size one greater
         than the size of the config_autoBrightnessLevels array.
         The brightness values must be between 0 and 255 and be non-decreasing.
         This must be overridden in platform specific overlays -->
    <integer-array name="config_autoBrightnessLcdBacklightValues">
    </integer-array>
@@ -559,6 +569,7 @@
    <!-- Array of output values for button backlight corresponding to the LUX values
         in the config_autoBrightnessLevels array.  This array should have size one greater
         than the size of the config_autoBrightnessLevels array.
         The brightness values must be between 0 and 255 and be non-decreasing.
         This must be overridden in platform specific overlays -->
    <integer-array name="config_autoBrightnessButtonBacklightValues">
    </integer-array>
@@ -566,6 +577,7 @@
    <!-- Array of output values for keyboard backlight corresponding to the LUX values
         in the config_autoBrightnessLevels array.  This array should have size one greater
         than the size of the config_autoBrightnessLevels array.
         The brightness values must be between 0 and 255 and be non-decreasing.
         This must be overridden in platform specific overlays -->
    <integer-array name="config_autoBrightnessKeyboardBacklightValues">
    </integer-array>
+46 −36
Original line number Diff line number Diff line
@@ -33,11 +33,11 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Slog;
import android.util.Spline;
import android.util.TimeUtils;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;

@@ -98,9 +98,9 @@ final class DisplayPowerController {
    // average of light samples.  Different constants are used
    // to calculate the average light level when adapting to brighter or
    // dimmer environments.
    // This parameter only controls the averaging of light samples.
    private static final long BRIGHTENING_LIGHT_TIME_CONSTANT = 1500;
    private static final long DIMMING_LIGHT_TIME_CONSTANT = 3000;
    // This parameter only controls the filtering of light samples.
    private static final long BRIGHTENING_LIGHT_TIME_CONSTANT = 500;
    private static final long DIMMING_LIGHT_TIME_CONSTANT = 2000;

    // Stability requirements in milliseconds for accepting a new brightness
    // level.  This is used for debouncing the light sensor.  Different constants
@@ -144,8 +144,7 @@ final class DisplayPowerController {

    // Auto-brightness.
    private boolean mUseSoftwareAutoBrightnessConfig;
    private int[] mAutoBrightnessLevelsConfig;
    private int[] mAutoBrightnessLcdBacklightValuesConfig;
    private Spline mScreenAutoBrightnessSpline;

    // Amount of time to delay auto-brightness after screen on while waiting for
    // the light sensor to warm-up in milliseconds.
@@ -289,17 +288,18 @@ final class DisplayPowerController {
        mUseSoftwareAutoBrightnessConfig = resources.getBoolean(
                com.android.internal.R.bool.config_automatic_brightness_available);
        if (mUseSoftwareAutoBrightnessConfig) {
            mAutoBrightnessLevelsConfig = resources.getIntArray(
            int[] lux = resources.getIntArray(
                    com.android.internal.R.array.config_autoBrightnessLevels);
            mAutoBrightnessLcdBacklightValuesConfig = resources.getIntArray(
            int[] screenBrightness = resources.getIntArray(
                    com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
            if (mAutoBrightnessLcdBacklightValuesConfig.length
                    != mAutoBrightnessLevelsConfig.length + 1) {

            mScreenAutoBrightnessSpline = createAutoBrightnessSpline(lux, screenBrightness);
            if (mScreenAutoBrightnessSpline == null) {
                Slog.e(TAG, "Error in config.xml.  config_autoBrightnessLcdBacklightValues "
                        + "(size " + mAutoBrightnessLcdBacklightValuesConfig.length + ") "
                        + "should have exactly one more entry than "
                        + "config_autoBrightnessLevels (size "
                        + mAutoBrightnessLevelsConfig.length + ").  "
                        + "(size " + screenBrightness.length + ") "
                        + "must be monotic and have exactly one more entry than "
                        + "config_autoBrightnessLevels (size " + lux.length + ") "
                        + "which must be strictly increasing.  "
                        + "Auto-brightness will be disabled.");
                mUseSoftwareAutoBrightnessConfig = false;
            }
@@ -322,6 +322,31 @@ final class DisplayPowerController {
        }
    }

    private static Spline createAutoBrightnessSpline(int[] lux, int[] brightness) {
        try {
            final int n = brightness.length;
            float[] x = new float[n];
            float[] y = new float[n];
            y[0] = brightness[0];
            for (int i = 1; i < n; i++) {
                x[i] = lux[i - 1];
                y[i] = brightness[i];
            }

            Spline spline = Spline.createMonotoneCubicSpline(x, y);
            if (false) {
                Slog.d(TAG, "Auto-brightness spline: " + spline);
                for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
                    Slog.d(TAG, String.format("  %7.1f: %7.1f", v, spline.interpolate(v)));
                }
            }
            return spline;
        } catch (IllegalArgumentException ex) {
            Slog.e(TAG, "Could not create auto-brightness spline.", ex);
            return null;
        }
    }

    /**
     * Returns true if the proximity sensor screen-off function is available.
     */
@@ -768,13 +793,13 @@ final class DisplayPowerController {
            return;
        }

        final int newScreenAutoBrightness = mapLuxToBrightness(mLightMeasurement,
                mAutoBrightnessLevelsConfig,
                mAutoBrightnessLcdBacklightValuesConfig);
        final int newScreenAutoBrightness = interpolateBrightness(
                mScreenAutoBrightnessSpline, mLightMeasurement);
        if (mScreenAutoBrightness != newScreenAutoBrightness) {
            if (DEBUG) {
                Slog.d(TAG, "updateAutoBrightness: mScreenAutoBrightness="
                        + mScreenAutoBrightness);
                        + mScreenAutoBrightness + "newScreenAutoBrightness="
                        + newScreenAutoBrightness);
            }

            mScreenAutoBrightness = newScreenAutoBrightness;
@@ -784,20 +809,8 @@ final class DisplayPowerController {
        }
    }

    /**
     * Maps a light sensor measurement in lux to a brightness value given
     * a table of lux breakpoint values and a table of brightnesses that
     * is one element larger.
     */
    private static int mapLuxToBrightness(float lux,
            int[] fromLux, int[] toBrightness) {
        // TODO implement interpolation and possibly range expansion
        int level = 0;
        final int count = fromLux.length;
        while (level < count && lux >= fromLux[level]) {
            level += 1;
        }
        return toBrightness[level];
    private static int interpolateBrightness(Spline spline, float lux) {
        return Math.min(255, Math.max(0, (int)Math.round(spline.interpolate(lux))));
    }

    private void sendOnStateChanged() {
@@ -839,10 +852,7 @@ final class DisplayPowerController {
        pw.println("  mScreenBrightnessDimConfig=" + mScreenBrightnessDimConfig);
        pw.println("  mUseSoftwareAutoBrightnessConfig="
                + mUseSoftwareAutoBrightnessConfig);
        pw.println("  mAutoBrightnessLevelsConfig="
                + Arrays.toString(mAutoBrightnessLevelsConfig));
        pw.println("  mAutoBrightnessLcdBacklightValuesConfig="
                + Arrays.toString(mAutoBrightnessLcdBacklightValuesConfig));
        pw.println("  mScreenAutoBrightnessSpline=" + mScreenAutoBrightnessSpline);
        pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);

        if (Looper.myLooper() == mHandler.getLooper()) {