Changing screen brightness in accordance with human perception
Since the kernel update to version 3.16 in Arch Linux the function keys for changing screen brightness in my laptop have ceased to work (at least in Openbox). While I was struggling with the problem, I discovered that xbacklight
works well and gives a nice smooth transition effect between brightness levels, so I decided to use it instead. But it did not work exactly in a way I expected it to.
I added proper configuration to Openbox’ rc.xml
:
<keybind key="XF86MonBrightnessUp">
<action name="Execute">
<command>xbacklight -inc 5</command>
</action>
</keybind>
<keybind key="XF86MonBrightnessDown">
<action name="Execute">
<command>xbacklight -dec 5</command>
</action>
</keybind>
This solution changes brightness as expected (from 0 to 100% of /sys/class/backlight/intel_backlight/max_brightness
), but not in a human-friendly way. If you play with this for a while you’ll notice that brightness changes almost unnoticeably when the screen is very bright and more and more rapidly when the screen gets darker. Furthermore, in the end, you get to zero brightness: the backlight is turned off and you don’t see anything anymore. You can of course increase brightness back at that point, but having to do this is quite annoying.
The Weber-Fechner law
As it turns out, human perception isn’t linear. According to the Weber-Fechner law perceived brightness is more or less proportional to the common logarithm (logarithm with base 10). The same principle applies also to the perception of sound.
And here’s how it looks in our case:
In other words, if we increase illuminance of a screen by 5 percentage points the perceived brightness change will be bigger when the screen is dark and smaller when the screen is bright. For example, a change from 5% to 10% of maximum illuminance will be perceived as if the brightness increased by 15 percentage points with respect to the maximum brightness. However, a change from 90% to 95% will be perceived as if the brightness changed by about 1 percentage point.
To be sure that illuminance change is in my case linear, I used a Light Meter app and measured the illuminance using the sensor in my phone (in not-very-scientific conditions, from a small distance):
And yes, it is linear, exactly as expected.
The solution
Let’s say we would like to divide our range of brightness evenly based on perceived brightness, not illuminance. The following Python script does exactly that, and a bit more:
#!/usr/bin/env python3
from math import log10
import sys
import subprocess
def get_backlight():
return float(subprocess.check_output(["xbacklight", "-get"]))
def set_backlight(backlight):
subprocess.call(["xbacklight", "-set", str(backlight)])
def backlight_to_step(backlight, backlight_min, backlight_max, steps):
x_min = log10(backlight_min)
x_max = log10(backlight_max)
return round(log10(backlight) / (x_max - x_min) * steps)
def step_to_backlight(step, backlight_min, backlight_max, steps):
x_min = log10(backlight_min)
x_max = log10(backlight_max)
x = step / steps * (x_max - x_min)
backlight = round(max(min(10 ** x, backlight_max), backlight_min))
return backlight
if __name__ == "__main__":
backlight_min = 2
backlight_max = 100
steps = 20
if len(sys.argv) < 2 or sys.argv[1] not in ["-inc", "-dec"]:
print("usage:\n\t{0} -inc / -dec".format(sys.argv[0]))
sys.exit(0)
current_backlight = get_backlight()
current_step = backlight_to_step(current_backlight, backlight_min, backlight_max, steps)
action = sys.argv[1]
if action == "-inc":
new_step = current_step + 1
elif action == "-dec":
new_step = current_step - 1
new_backlight = step_to_backlight(new_step, backlight_min, backlight_max, steps)
print("Current backlight: {0}\nChanging to: {1}".format(current_backlight, new_backlight))
set_backlight(new_backlight)
The configuration is based on three variables:
backlight_min
- the minimum brightness (in percent); too low value may make it stuck because the increment will be too small forxbacklight
backlight_max
- the maximum brightness (in percent)steps
- the number of steps from minimum to maximum brightness
Having these variables hardcoded does not seem like a very good idea at first, but then how often can I change my mind about e.g. the number of steps?
The script takes one argument: -inc
to increase brightness by one step or -dec
to decrease it. And the algorithm is simple:
- Retrieve the current backlight value by calling
xbacklight -get
. - Use that value to calculate the current step value, rounded to the closest one (brightness might have been changed by some other application in the meantime).
- Increase or decrease the step and calculate new backlight.
- Set the new backlight by calling
xbacklight -set {value}
.
And that’s all. Here’s a gist if you want.
Using it in Openbox
If you’re an Openbox user, just place this script somewhere in your PATH
(~/bin/change-backlight.py
in my case) and add these lines to ~/.config/openbox/rc.xml
:
<keybind key="XF86MonBrightnessUp">
<action name="Execute">
<command>~/bin/change-backlight.py -inc</command>
</action>
</keybind>
<keybind key="XF86MonBrightnessDown">
<action name="Execute">
<command>~/bin/change-backlight.py -dec</command>
</action>
</keybind>
Oh, and don’t forget to reconfigure Openbox: openbox --reconfigure
should do the trick.