Script for manual gamma calibration

Just thought I would add the script I made for the purposes of calibrating gamma manually. It just makes boxes of grey level from min to max in as many steps as specified.

from psychopy.hardware import keyboard
import pyglet
import numpy as np

kb = keyboard.Keyboard()
screens = pyglet.canvas.get_display().get_screens()
screen = len(screens) -1
win = visual.Window(color=(0, 0, 0), screen = screen, fullscr = True)

### Change number of steps ###
nLevels = 11
###

def calibration(nLevels):
    grey_vals = np.linspace(-1, 1, nLevels)
    for i in range(nLevels):
        grey_val = grey_vals[i]
        box = visual.Rect(win, width = 0.5, height = 0.5, fillColor = [grey_val,grey_val,grey_val])
        box.draw()
        win.flip()
        kb.waitKeys()

calibration(nLevels)

no question, just might save someone 5mins.

1 Like

Updating this because I could not get any of the calibrations to actually work. This was due to an error with setGammaRamp which I’ve seen reported many times but couldn’t find a solution that actually worked.

Here is a script which will manually apply the gamma ramp, tested and working for Windows11.

import ctypes
import numpy as np
import win32api
import win32gui

# Load gdi32.dll
gdi32 = ctypes.WinDLL("gdi32")
user32 = ctypes.WinDLL("user32")

monitors = win32api.EnumDisplayMonitors()
if len(monitors) == 1:
    monitor_info = win32api.GetMonitorInfo(monitors[0][0])
    hdc = win32gui.CreateDC("DISPLAY", monitor_info['Device'], None)
elif len(monitors) == 2:
    monitor_info = win32api.GetMonitorInfo(monitors[1][0])
    hdc = win32gui.CreateDC("DISPLAY", monitor_info['Device'], None)
else:
    raise RuntimeError("No monitors detected or more than two monitors detected.")

# Define the LUT structure: an array of 256 RGB triplets (WORD type)
class GAMMARAMP(ctypes.Structure):
    _fields_ = [("red", ctypes.c_ushort * 256),
                ("green", ctypes.c_ushort * 256),
                ("blue", ctypes.c_ushort * 256)]

def set_gamma(value, min_luminance=0, max_luminance=300):
    """Set screen gamma correction with specified gamma value and luminance range (in cd/m²)."""
    if not (0.1 <= value <= 5.0):
        raise ValueError("Gamma value must be between 0.1 and 5.0")
    if min_luminance < 0 or max_luminance <= min_luminance:
        raise ValueError("Invalid luminance range")

    gamma_ramp = GAMMARAMP()

    # Normalize min/max luminance to 0-65535 range
    min_val = int((min_luminance / 300) * 65535)
    max_val = int((max_luminance / 300) * 65535)

    # Generate LUT based on gamma value and luminance limits
    for i in range(256):
        gamma = ((i / 255.0) ** (1.0 / value)) * (max_val - min_val) + min_val
        gamma = min(65535, max(0, int(gamma)))
        gamma_ramp.red[i] = ctypes.c_ushort(gamma)
        gamma_ramp.green[i] = ctypes.c_ushort(gamma)
        gamma_ramp.blue[i] = ctypes.c_ushort(gamma)

    # Apply gamma correction
    success = gdi32.SetDeviceGammaRamp(hdc, ctypes.byref(gamma_ramp))

    if not success:
        raise RuntimeError("Failed to set gamma correction")

# Example Usage
set_gamma(2.2, min_luminance=20, max_luminance=250)

The gamma will reset any time the display settings are updated- e.g. computer is restarted, refresh rate changed etc. etc.