Is the known bug that prevents gamma calibration with second monitor & windows still persistent?

Hello!

So I’ve been trying to set up stimuli using psychopy on a laptop plugged into a CRT (as the secondary stimulus monitor). I’ve noticed that gamma calibration does not work and I saw that this is a known issue with windows and secondary displays (documented in the troubleshooting section)

I was wondering if this is still an issue or if Microsoft has fixed this bug (or if there’s a way around it in psychopy somehow).

Thanks!

Hi There,

Please could you tell us a bit more about how it is not working? e.g. are you getting any error messages?

Thanks!
Becca

Hi Becca,

I didn’t get an error, but when I tried using monitor.setGamma, changing the value had no impact on the luminance on the screen.

Hey Becca, sorry for the weird double reply. I accidentally hit delete on my post and couldn’t figure out a way to “undelete” it.

Long story short: I’ve managed to work around this issue, but it’s a somewhat janky workaround that involves manually setting and resetting a manually created gamma ramp using the ctypes and win32api modules.

Basic steps are:

  • Manually generate a gamma correction LUT using either of the two equations shown by PsychoPy here.
  • Use ctypes.windll.user32.EnumDisplayMonitors to pull the handles for all of your connected displays
  • Pass those handles to win32api.getmonitorinfo to get the official “display names” of your connected monitors
  • Pass those display names to ctypes.windll.gdi32.CreateDCA(*Device Name*, Null, Null, 0) to create a unique “device context” for each display.
  • Use that device context to call ctypes.windll.gdi32.SetDeviceGammaRamp(*createdDCA, *created LUT)
  • When you’re done, call ctypes.windll.gdi32.SetDeviceGammaRamp again right before core.quit() to reset the gamma values back to 1,1,1;

I can drop a detailed code snippet if this seems like it’s worth trying. I’m in the process of trying to fix this more seamlessly, but for now the manual workaround has allowed us to do successfully linearize three different monitors that we’re using for a single experiment.

Hope this helps!!

hi there! a detailed code snippet would be awesome! i’m also trying to calibrate a two-display setup and can’t seem to find a way around the gamma ramp error. thank you!

Hey csagas, here is how I get around this issue on an experiment we put together that required linearizing two different monitors. It’s a bit of a pain, because you have to manually generate the gamma correction LUT, but it beats not being able to linearize multiple monitors.

First, you’ll want to import the gamma correction LUT for each monitor, as well as a “reset” ramp that puts everything back to a gamma of 1,1,1 after you’re done with your experiment:

# Import linearization LUT for each monitor and reset ramp

# Left monitor ramp
leftMonitorLUT = np.array(np.genfromtxt("C:\\LOCATION OF CSV FILE CONTAINING YOUR LUT", delimiter=','))
leftMonitorLUT = leftMonitorLUT[:,1:4]
leftMonitorLUT = np.transpose(leftMonitorLUT)
leftMonitorLUT = leftMonitorLUT/255
leftMonitorLUT = np.ascontiguousarray(leftMonitorLUT)
leftMonitorRamp = (np.around(255.0*leftMonitorLUT)).astype(np.uint16)
leftMonitorRamp.byteswap(True) 

# Right monitor ramp
rightMonitorLUT = np.array(np.genfromtxt("C:\\LOCATION OF CSV FILE CONTAINING YOUR LUT", delimiter=','))
rightMonitorLUT = rightMonitorLUT[:,1:4]
rightMonitorLUT = np.transpose(rightMonitorLUT)
rightMonitorLUT = rightMonitorLUT/255
rightMonitorLUT = np.ascontiguousarray(rightMonitorLUT)
rightMonitorRamp = (np.around(255.0*rightMonitorLUT)).astype(np.uint16)
rightMonitorRamp.byteswap(True) 

# Reset ramp
resetLUT = np.array(np.genfromtxt("C:\\LOCATION OF RESET LUT CSV FILE", delimiter=','))
resetLUT = np.transpose(resetLUT)
resetLUT = resetLUT/255
resetLUT = np.ascontiguousarray(resetLUT)
resetRamp = (np.around(255.0*resetLUT)).astype(np.uint16)
resetRamp.byteswap(True)

Once you have your ramps, you have to generate a device context (DC) for each monitor. This number tells Windows what piece of hardware it is being told to manipulate. This is where PsychoPy’s bug lives… in the source code they still use the getDC function, which doesn’t seem to work properly with multiple monitors anymore (no idea why…).

# Get monitor handles and generate DC for each monitor
devices =[]
for idx, (hMon, hDC, (left, top, right, bottom)) in enumerate(win32api.EnumDisplayMonitors(None, None)):
    devices.append(hMon.handle)

monitorInfo =[]

for m in devices:
    b = win32api.GetMonitorInfo(m)
    monitorInfo.append(b)

screen1 = monitorInfo[0]['Device']
screen2 = monitorInfo[1]['Device']

screen1 = screen1.encode('utf-8')
screen2 = screen2.encode('utf-8')

screen1DC = windll.gdi32.CreateDCA(c_char_p(screen1), c_int(0), c_int(0), c_int(0))
screen2DC = windll.gdi32.CreateDCA(c_char_p(screen2), c_int(0), c_int(0), c_int(0))

Now that we have a Device Context for each of our monitors, we can call SetDeviceGammaRamp to set the gamma ramp for each monitor individually. The only reason this code is nested inside a for loop is because SetDeviceGammaRamp used to fail on the first try once in a while. This hasn’t ever happened to me, but it’s considered best practice to avoid errors:

for n in range(2):
    a = windll.gdi32.SetDeviceGammaRamp(screen1DC, leftMonitorRamp.ctypes)
    b = windll.gdi32.SetDeviceGammaRamp(screen2DC, rightMonitorRamp.ctypes)
    if a and b:
        break
    if not a and not b:
        print("failed to set gamma ramp")

And when you’re done with the experiment (i.e. at every potential break point) you have to call SetDeviceGammaRamp again, this time applying the “reset” ramp so that your monitors go back to normal, non-linearized output. It’s also best practice to delete the DCs that you created.

for n in range(2):
                a = windll.gdi32.SetDeviceGammaRamp(screen1DC, resetRamp.ctypes)
                b = windll.gdi32.SetDeviceGammaRamp(screen2DC, resetRamp.ctypes)
                if a and b and c:
                    break
                if not a or not b or not c:
                    print("failed to reset gamma ramp")
            windll.gdi32.DeleteDC(screen1DC)
            windll.gdi32.DeleteDC(screen2DC)

It’s definitely not the cleanest approach, but it works. Note that you could just use the equations in psychopy to generate your gamma ramp right there in the code, I just created mine in a separate script and exported them as CSV files, hence the imports at the beginning. I’m by no means an expert, so my apologies to more seasoned programmers who had to cringe through the messy code above. If anyone can/wants to clean it up, please do! And if you have any questions, let me know.

Hope this is helpful!

Best,
Dragos