Skip to content

Commit

Permalink
Adding additional (debug and scripting) parameters to the calibration…
Browse files Browse the repository at this point in the history
… script
  • Loading branch information
dekuenstle authored Jan 10, 2024
2 parents 39f3979 + b0208c4 commit 29deef9
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 51 deletions.
207 changes: 160 additions & 47 deletions psychopy_pixx/calibration/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import numpy as np
import click
import matplotlib.pyplot as plt
import csv
import os
from datetime import datetime
import pandas as pd

from psychopy_pixx.calibration.photometer import findPhotometer
from psychopy_pixx.devices import ViewPixx
Expand All @@ -15,8 +19,12 @@ def measure_luminances(
allGuns=True,
autoMode='auto',
random=False,
inverted=False,
stimSize=4,
n_measures=50):
n_measures=50,
timeestimation_output=False,
all_measurements=False,
savefiles='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5'):
"""Automatically measures a series of gun values and measures
the luminance with a photometer.
Expand Down Expand Up @@ -60,6 +68,8 @@ def measure_luminances(
sf=[window.clientSize[0] / 512.0, window.clientSize[1] / 512.0])
testPatch = visual.PatchStim(window, tex='sqr', size=stimSize,
color=initRGB, units='norm')

date_time = datetime.now().strftime("%Y-%m-%d_%H-%M") # save date and time for file distinction

if autoMode != 'semi':
message.setText('Q to quit at any time')
Expand All @@ -72,9 +82,14 @@ def measure_luminances(
if photometer.type == 'S470' and n_measures is not None:
photometer.n_repeat = n_measures

if random:
if random and inverted:
print(f'ERROR: you can not set both random={random} and inverted={inverted}!')
return 1
if random and not inverted:
shuffled_index = np.random.permutation(len(levels))
toTest = levels[shuffled_index]
elif not random and inverted:
toTest = levels[::-1]
else:
toTest = levels

Expand All @@ -84,9 +99,32 @@ def measure_luminances(
guns = [0]
# this will hold the measured luminance values
lumsList = np.zeros((4, len(toTest)))

# for (approx) ending time calculation timestamps and counter
counter = 0
start = time.time()

# create list for logging all measurments
if all_measurements:
allLums_data = []

# prepare logging
if all_measurements and savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':

data_file = f"{savefiles}/allMeasurments{date_time}.csv"
writer = csv.writer(open(data_file, 'w'))
# create header
header = ['levels']
for i in range(1, n_measures + 1):
header.append(f'measurement_{i:03}')
writer.writerow(header)

# for each gun, for each value run test
for gun in guns:
for valN, DACval in enumerate(toTest):
# counter for timeestimation
counter = valN + 1

lum = (DACval * 2) - 1 # from range 0:1 into -1:1
# only do luminanc=-1 once
if lum == -1 and gun > 0:
Expand All @@ -109,8 +147,25 @@ def measure_luminances(

# take measurement
if autoMode == 'auto':
actualLum = photometer.getLum()
print(f"\t{valN + 1}/{len(toTest)} At DAC value {DACval:.2f}\t: {actualLum:.2f}cd/m^2")
if all_measurements:
actualLum, lums = photometer.getLum(return_all=True)
allLums_data.append([DACval] + lums)
else:
actualLum = photometer.getLum()
print(f"\t{valN+1:4d}/{len(toTest)} At DAC value {DACval:5.3f}\t: {actualLum:6.2f}cd/m^2")
if timeestimation_output and counter%10 == 0:
current = time.time()-start
duration = current/counter
estimated_end = time.time() + (duration * (len(toTest)-counter))
time_format_current = time.strftime('%H:%M:%S', time.gmtime(current))
time_format_duration = time.strftime('%H:%M:%S', time.gmtime(duration))
time_format_end = time.strftime('%d.%m.%y %H:%M', time.localtime(estimated_end))
print('')
print(f' Time-Estimation:')
print(f' We needed {time_format_current} unitl now ({counter} levels).')
print(f' This results into {time_format_duration} per level.')
print(f' Estimated ending time: {time_format_end}')
print('')
lumsList[gun, valN] = actualLum
# check for quit request
for thisKey in event.getKeys():
Expand All @@ -119,7 +174,7 @@ def measure_luminances(
return np.array([])

elif autoMode == 'semi':
print(f"\t{valN + 1}/{len(toTest)} At DAC value {DACval:.2f}")
print(f"\t{valN+1:4d}/{len(toTest)} At DAC value {DACval:5.3f}")

done = False
while not done:
Expand All @@ -130,6 +185,9 @@ def measure_luminances(
elif thisKey in (' ', 'space'):
done = True

if all_measurements and savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':
writer.writerow(allLums_data[-1])

if random: # revert shuffling
lumsList = lumsList[:, shuffled_index.argsort()]
return lumsList
Expand All @@ -142,37 +200,63 @@ def measure_luminances(
@click.option('-p', '--photometer', required=True, help='photometer name supported by psychopy')
@click.option('--port', help='Port of the photometer', default=None)
@click.option('--random', help='Measure in randomized order.', is_flag=True)
@click.option('--inverted', help='Measure in inverted order.', is_flag=True)
@click.option('--levelspost', help='Number of measurements after linearization', default=100)
@click.option('--restests', help='Number of test points for luminance resolution', default=5)
@click.option('--plot', help='Show plots.', is_flag=True)
@click.option('--linearize/--no-linearize', help='Do (not) linearize the luminance. Default is linearizing.', default=True)
@click.option('--plot', is_flag=False, flag_value='.', help='Create, show and save plots.', default='no_plots_8e26a619-e688-4dcf-b010-7bd5fca459d8')
@click.option('--gamma', help='Gamma with which the monitor is to be corrected. (default: 1.0 (linearization))', type=float, default=1.0)
@click.option('--measures', help='Number of measurements to average per color level (only S470 photometer).', type=int, default=250)
def calibration_routine_cli(levels, monitor, screen, photometer, port, random, levelspost, restests, plot, linearize, measures):
@click.option('--savefiles', is_flag=False, flag_value='.', help='save measurements as files into the given directory', default='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5')
@click.option('--all_measurements', help='with this option, all measurments from the photometer are saved', is_flag=True)
@click.option('--script', help='prevents routine from opening plots and user prompts to be able to use the tool more automated', is_flag=True)
@click.option('--timeestimation_output', help='prints a time estimation of how much longer the calibration will take', is_flag=True)
@click.option('--no_scanning', help='with this option you swith from "scanning backlight" to "normal backlight"', is_flag=True)
@click.option('--bg_intensity', help='intensity of the backlight', default=255)
@click.option('--lut', help='look up table (lut) the script should use for correction/calibration', is_flag=False, flag_value='.', default='no_lut_f99fc889-c6e3-4588-ad44-4f8a9554f7b5')
def calibration_routine_cli(levels, monitor, screen, photometer, port, random, inverted, levelspost, restests, plot, measures, gamma=1.0,
savefiles='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5', all_measurements=False, script=False, timeestimation_output=False, no_scanning=False, bg_intensity=255, lut='no_lut_f99fc889-c6e3-4588-ad44-4f8a9554f7b5'):

from psychopy import monitors, visual # lazy import

# check if paths exist
# Note: I have to use the string with the uuid as control, so if the option is not set, I dont log/plot
if savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':
if not os.path.isdir(savefiles):
print(f'ERROR: "{savefiles}" is no valide directory')
return 1
if plot!='no_plots_8e26a619-e688-4dcf-b010-7bd5fca459d8':
if not os.path.isdir(savefiles):
print(f'ERROR: "{plot}" is no valide directory')
return 1

print(f"Setup monitor {monitor}, search for photometer {photometer} ...")
monitor = monitors.Monitor(monitor)
photometer = findPhotometer(device=photometer, ports=port)
if photometer is None:
raise ValueError('Photometer not found. You might specify (another) port or name.')

# monitor setup
monitor_size = monitor.getSizePix()
if monitor_size is None:
raise ValueError("No monitor size defined. Please setup monitor in psychopy's monitor center.")
window = visual.Window(
fullscr=0, size=monitor_size, gamma=1, units='norm', useFBO=True,
monitor=monitor, allowGUI=True, winType='pyglet', screen=screen)
vpixx = ViewPixx(window)
vpixx.scanning_backlight = not no_scanning
vpixx.backlight = bg_intensity
print(f'backlight intensity is: {vpixx.backlight}')

monitor_state = {
'Width': monitor.getWidth(),
'Distance': monitor.getDistance(),
'SizePix': monitor_size,
**vpixx.register}
register_str = "\n".join(f"\t{key}: {val}" for key, val in monitor_state.items())
click.confirm(f'This is your monitor state. Ok?\n{register_str}\n' , abort=True)
if not script:
monitor_state = {
'Width': monitor.getWidth(),
'Distance': monitor.getDistance(),
'SizePix': monitor_size,
**vpixx.register}
register_str = "\n".join(f"\t{key}: {val}" for key, val in monitor_state.items())
click.confirm(f'This is your monitor state. Ok?\n{register_str}\n' , abort=True)

measure_kwargs = dict(window=window, photometer=photometer, random=random,
measure_kwargs = dict(window=window, photometer=photometer, random=random, inverted=inverted,
allGuns=False, n_measures=measures)
print(f"Measure a few black and white screens ...")
blackwhiteLums = measure_luminances(np.array([1, 1, 1 , 0, 0, 0]), **measure_kwargs)[0]
Expand All @@ -181,11 +265,34 @@ def calibration_routine_cli(levels, monitor, screen, photometer, port, random, l
or blackwhiteLums[3:].max() - blackwhiteLums[3:].min() > 0.1):
raise ValueError(f"Black / white measurements are quite differerent."
f"Probably something is wrong. {blackwhiteLums}")
click.confirm(f'Your monitor shows {minLum:.2f} cd/m^2 to {maxLum:.2f} cd/m^2. Ok?', abort=True)
if savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':
data = np.vstack((100*np.array([1, 1, 1 , 0, 0, 0]), blackwhiteLums)).T
date_time = datetime.now().strftime("%Y-%m-%d_%H-%M") # save date and time for file distinction
data_file = f"{savefiles}/blackwhiteLums_{date_time}.csv"
np.savetxt(data_file, data, fmt="%.2f", delimiter=",", header='levels,luminance_gun1,luminance_gun2,luminance_gun3,luminance_gun4') # luminances in cd/m2"
if not script:
click.confirm(f'Your monitor shows {minLum:.2f} cd/m^2 to {maxLum:.2f} cd/m^2. Ok?', abort=True)

# measurements
print(f"Measure luminance series ...")
levelsPre = np.linspace(0, 1, levels, endpoint=True)
lumsPre = measure_luminances(levelsPre, **measure_kwargs)
measure_kwargs_realMeasurment = dict(window=window, photometer=photometer, random=random, inverted=inverted,
allGuns=False, n_measures=measures, timeestimation_output=timeestimation_output, all_measurements=all_measurements, savefiles=savefiles)
lumsPre = measure_luminances(levelsPre, **measure_kwargs_realMeasurment)
if savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':
data = np.vstack((100*levelsPre, lumsPre)).T # percent for better accuracy, all 4 post guns
date_time = datetime.now().strftime("%Y-%m-%d_%H-%M") # save date and time for file distinction
data_file = f"{savefiles}/luminancePre_{date_time}.csv"
np.savetxt(data_file, data, fmt="%.2f", delimiter=",", header='levels,luminance_gun1,luminance_gun2,luminance_gun3,luminance_gun4') # luminances in cd/m2"

#try to set pretrained lut
if lut != 'no_lut_f99fc889-c6e3-4588-ad44-4f8a9554f7b5':
lut = pd.read_csv(lut)
lut = lut.sort_values(by='levels')
print(lut)
levelsPre = lut['levels'].values
print(levelsPre)
lumsPre = np.array([lut['prediciton'].values, lut['prediciton'].values, lut['prediciton'].values, lut['prediciton'].values])

print("Create new monitor calibration.")
monitor.newCalib(width=monitor.getWidth(), distance=monitor.getDistance())
Expand All @@ -205,14 +312,13 @@ def calibration_routine_cli(levels, monitor, screen, photometer, port, random, l
'random_measures': random,
}

if linearize:
print("Linearize luminance ...")
vpixx.linearize_luminance()
print(f'Correct luminance (gamma={gamma}) ...')
vpixx.correct_luminance(gamma)

if levelspost > 0:
print(f"Measure luminances again for validation ...")
levelsPost = np.linspace(0, 1, levelspost, endpoint=True)
lumsPost = measure_luminances(levelsPost, **measure_kwargs)
lumsPost = measure_luminances(levelsPost, **measure_kwargs_realMeasurment)
monitor.setLumsPost(lumsPost)
monitor.setLevelsPost(levelsPost)

Expand All @@ -221,7 +327,7 @@ def calibration_routine_cli(levels, monitor, screen, photometer, port, random, l
reslevels = np.linspace(0, 1, restests, endpoint=False)
resoffset = np.r_[np.inf, np.arange(14, 6 - 1, -1).astype(float)]
reslevels = reslevels.reshape(-1, 1) + 2**-resoffset.reshape(1, -1)
reslums = measure_luminances(reslevels.ravel(), **measure_kwargs)
reslums = measure_luminances(reslevels.ravel(), **measure_kwargs_realMeasurment)
reslums = reslums.reshape(4, reslevels.shape[0], reslevels.shape[1]).transpose(1, 0, 2)
monitor.currentCalib['lumsRes'] = reslums
monitor.currentCalib['levelsRes'] = reslevels
Expand All @@ -230,37 +336,44 @@ def calibration_routine_cli(levels, monitor, screen, photometer, port, random, l
print("Save new monitor calibration ...")
monitor.save()
window.close()
if savefiles!='no_savefile_f99fc889-c6e3-4588-ad44-4f8a9554f7b5' and levelspost > 0:
data = np.vstack((100*levelsPost, lumsPost)).T # percent for better accuracy, all 4 post guns
date_time = datetime.now().strftime("%Y-%m-%d_%H-%M") # save date and time for file distinction
data_file = f"{savefiles}/luminancePost_{date_time}.csv"
np.savetxt(data_file, data, fmt="%.2f", delimiter=",", header='levels,luminance_gun1,luminance_gun2,luminance_gun3,luminance_gun4') # luminances in cd/m2"

print("Plot measurements ...")
plt.plot(levelsPre, lumsPre[0], label='pre')
if levelspost > 0:
plt.plot([0, 1], [lumsPre[0, 0], lumsPre[0, -1]], '--', linewidth=3, label='expected')
plt.plot(levelsPost, lumsPost[0], label='post')
plt.xlabel("Grey Level")
plt.ylabel("Luminance [cd/m^2]")
plt.legend(loc='best')
plt.title(f'Luminance before and after calibration ({photometer.type}, {monitor.currentCalibName})')
plot_file = f"{monitor.currentCalibName}_luminance.pdf"
print(f"Save plot {plot_file}...")
plt.savefig(plot_file)
if plot:
plt.show()
if plot != 'no_plots_8e26a619-e688-4dcf-b010-7bd5fca459d8':
print("Plot measurements ...")
plt.plot(levelsPre, lumsPre[0], label='pre')
if levelspost > 0:
plt.plot([0, 1], [lumsPre[0, 0], lumsPre[0, -1]], '--', linewidth=3, label='expected')
plt.plot(levelsPost, lumsPost[0], label='post')
plt.xlabel("Grey Level")
plt.ylabel("Luminance [cd/m^2]")
plt.legend(loc='best')
plt.title(f'Luminance before and after calibration ({photometer.type}, {monitor.currentCalibName})')
plot_file = f"{plot}/{monitor.currentCalibName}_luminance.pdf"
print(f"Save plot {plot_file}...")
plt.savefig(plot_file)
if not script:
plt.show()

if restests > 0:
for lums, levels in zip(monitor.currentCalib['lumsRes'], monitor.currentCalib['levelsRes']):
lums = lums[0]
plt.plot(-resoffset[1:], lums[1:] - lums[0],
label=f'{levels[0]:.4f}')
plt.xlabel("Log2(grey level difference)")
plt.ylabel("Luminance difference")
plt.yscale('log', base=2)
plt.title(f'Luminance resolution ({photometer.type}, {monitor.currentCalibName})')
plt.legend(loc='best', title='Grey level')
plot_file = f"{monitor.currentCalibName}_resolution.pdf"
print(f"Save plot {plot_file}...")
plt.savefig(plot_file)
if plot:
plt.show()
if plot != 'no_plots_8e26a619-e688-4dcf-b010-7bd5fca459d8':
plt.xlabel("Log2(grey level difference)")
plt.ylabel("Luminance difference")
plt.yscale('log', base=2)
plt.title(f'Luminance resolution ({photometer.type}, {monitor.currentCalibName})')
plt.legend(loc='best', title='Grey level')
plot_file = f"{plot}/{monitor.currentCalibName}_resolution.pdf"
print(f"Save plot {plot_file}...")
plt.savefig(plot_file)
if not script:
plt.show()


print("Done.")
8 changes: 4 additions & 4 deletions psychopy_pixx/devices/viewpixx.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(self, win, gamma=None):
self._setup_shader()


def linearize_luminance(self, assert_register=True):
def correct_luminance(self, gamma=1.0, assert_register=True):
if assert_register:
try:
calib_reg = self.window.monitor.currentCalib['viewpixx']['register']
Expand All @@ -55,7 +55,7 @@ def linearize_luminance(self, assert_register=True):
for k, v in calib_reg.items():
assert register[k] == v, f"Expects {k}={v}, got {k}={register[k]}"

self.shader_clut = interp_clut(self.window.monitor)
self.shader_clut = interp_clut(self.window.monitor, gamma)

@property
def shader_clut(self) -> np.ndarray:
Expand Down Expand Up @@ -218,7 +218,7 @@ def use_calibration_register(self):
self.register = self.window.monitor.currentCalib['viewpixx']['register']


def interp_clut(monitor):
def interp_clut(monitor, gamma):
lums = monitor.getLumsPre()
levels = monitor.getLevelsPre()

Expand Down Expand Up @@ -247,7 +247,7 @@ def interp_clut(monitor):
desired_lums = np.linspace(0, 1, 2**16, endpoint=True)
desired_levels = np.empty((nguns, len(desired_lums)))
for gun in range(4):
desired_levels[gun, :] = np.interp(x=desired_lums, xp=lums[gun], fp=levels)
desired_levels[gun, :] = (np.interp(x=desired_lums, xp=lums[gun], fp=levels))**gamma # = (invertedfunction)**gamma (if gamma == 1.0 it is just a linearisation)
return desired_levels


Expand Down

0 comments on commit 29deef9

Please sign in to comment.