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