Colour representation using PsychoPy and Colour py packages

Hello. This is the short snippet of the code, which I use to check whether colours are presented fideliously . I use EIZO monitor calibrated for sRGB. All the functions that I use for converting between colour spaces are mentioned in code I enclose. Here I use JETI 1511 Spectroval HiRes to obtain XYZ tristimulus values of the presented colour and then convert them into Lab. (Its functionality is tested and I presume there’s no problem in it) The idea here is to pass the colour in the form of CIE Lab coordinates, display it, obtain tristimulus values and (using Colour py again) convert them back to Lab to get how much they differ from the ones we passed in initially.

import ctypes
import numpy as np
from psychopy import visual, core
from colour import Lab_to_XYZ, XYZ_to_RGB, RGB_COLOURSPACES, XYZ_to_Lab
import time

Assuming a 64-bit system

DWORD_PTR = ctypes.c_ulonglong
FLOAT = ctypes.c_float
BOOL = ctypes.c_bool

Load the JETI DLLs

dll_path = r’C:\Users\Stepan\PycharmProjects\sdk\Win64’
jeti_radio_ex = ctypes.CDLL(f’{dll_path}\jeti_radio_ex64.dll’)

def lab_to_rgb(L, a, b):
Lab = np.array([L, a, b]).reshape((1, 3))
XYZ = Lab_to_XYZ(Lab)
colourspace = RGB_COLOURSPACES[‘sRGB’]
RGB = XYZ_to_RGB(
XYZ=XYZ,
colourspace=colourspace,
illuminant=colourspace.whitepoint,
chromatic_adaptation_transform=‘CAT02’,
apply_cctf_encoding=True
)
return RGB

def measurement():
num_devices = ctypes.c_ulong()
device_handle = DWORD_PTR()
X, Y, Z = FLOAT(), FLOAT(), FLOAT()
boStatus = BOOL()

# Initialize the device
if jeti_radio_ex.JETI_GetNumRadioEx(ctypes.byref(num_devices)) != 0 or num_devices.value == 0:
    print("No matching device found or error occurred!")
    return None

if jeti_radio_ex.JETI_OpenRadioEx(0, ctypes.byref(device_handle)) != 0:
    print("Could not open device!")
    return None

# Perform the measurement
if jeti_radio_ex.JETI_MeasureEx(device_handle, 0, 1, 1) != 0:
    print("Could not start measurement!")
    jeti_radio_ex.JETI_CloseRadioEx(device_handle)
    return None

# Wait for measurement to complete
while True:
    time.sleep(0.5)
    result = jeti_radio_ex.JETI_MeasureStatusEx(device_handle, ctypes.byref(boStatus))
    # Either we can't retrieve the measurement status (some error occurs -> we break) or it finished propertly (boStatus = 0 -> we break)
    if result != 0 or not boStatus.value:
        break

# Fetch XYZ values
if jeti_radio_ex.JETI_ChromXYZEx(device_handle, ctypes.byref(X), ctypes.byref(Y), ctypes.byref(Z)) != 0:
    print("Failed to fetch XYZ values.")
    jeti_radio_ex.JETI_CloseRadioEx(device_handle)
    return None

# Close the device
jeti_radio_ex.JETI_CloseRadioEx(device_handle)
return X.value, Y.value, Z.value

def xyz_to_lab(X, Y, Z):
# Normalize XYZ values from 0 to 100 to 0 to 1
X_normalized = X / 100
Y_normalized = Y / 100
Z_normalized = Z / 100

# Convert normalized XYZ to Lab
white_point = RGB_COLOURSPACES[
    'sRGB'].whitepoint  # Ensure this is also scaled appropriately if necessary
Lab = XYZ_to_Lab([X_normalized, Y_normalized, Z_normalized], illuminant=white_point)
return Lab

def main():

#L, a, b = 100, 0, 0  # White
L, a, b = 30, 14, 9 # Example LAB
win = visual.Window(fullscr=True, color=lab_to_rgb(L, a, b), units='pix', monitor='laboratory_monitor', title='Colour representation test')
win.flip()
time.sleep(1)

# Perform measurement and fetch results
xyz_values = measurement()

Lab_measured = xyz_to_lab(xyz_values[0], xyz_values[1], xyz_values[2])


print(f"Initial Lab:{L},{a},{b}")
print(f"Initial Lab converted to XYZ:{Lab_to_XYZ(np.array([L,a,b]))[0]*100},{Lab_to_XYZ(np.array([L,a,b]))[1]*100},{Lab_to_XYZ(np.array([L,a,b]))[2]*100}")

if xyz_values:
    print(f"Measured XYZ values: X={xyz_values[0]}, Y={xyz_values[1]}, Z={xyz_values[2]}")


print(f"Computed Lab values: L={Lab_measured[0]}, a={Lab_measured[1]}, b={Lab_measured[2]}")


# Close the window
win.close()

if name == ‘main’:
main()

I suppose, the problem is in how PsychoPy displays the colour. Is there any mistake in how I initialize the window or my monitor? Do I have to calibrate it somehow using Monitor Center (Now I just have my monitor’s dimensions saved in Monitor Center as well as monitor distance, but nothing more).
If you help me I’d be much obliged. Thank you very much in advance.
P.S. Here I also enclose and example output of thic code.

Initial Lab:30,14,9
Initial Lab converted to XYZ:7.273171195084655,6.235905531182091,4.731715310326789
Measured XYZ values: X=43.44944763183594, Y=42.130001068115234, Z=46.36543655395508
Computed Lab values: L=70.96045683790483, a=10.341901722225488, b=-0.5247431796713276

The problem was hidden in the fact I didn’t use RGB space correctly. My colours were encoded in [0,1] ratio, so I had to use colourSpace=‘rgb1’ instead of colourSpace=‘rgb’. So everyone be patient when choosing a colourSpace.