% CalibrateMonSpd % % Calling script for monitor calibration. Assumes % you have CMCheckInit/MeasSpd functions that initialize % measurement hardware and return a measured spectral % power distribution respectively. % % NOTE 30-June-2023: I tried to fix some of this mess, to make the functions % more compatible with the way current operating systems, graphics cards and % display devices work. Specifically the massive changes in how hardware gamma % tables work on modern gpu's. I also added support for more Photo Research % colormeters. This seems to work ok with the simulated meterType 0, but I don't % have actual measurement hardware to verify if things work as expected. I guess % it is less broken now, and maybe even working fine, but who knows? % % NOTE (dhb, 8/19/12). This code is a bit dusty, as it is not % being actively maintained. In particular, the PTB display % control has evolved since this was last looked at carefully. % In general, you want to calibrate through the same set of % display calls you'll use in your experiment. Below is some % prose written by Mario Kleiner that describes how the PTB % currently wants you to control and restore your clut. In % an ideal world, this routine would be updated to match. But % more generally, if you use this code you may want to modify % to make sure it is displaying colors the same way you will in % your experiments. The actual work is done in CalibrateMonDrvr % and CalibrateAmbDrvr so that is where you would look. % % We have two functions for this. LoadIdentityClut() for loading an % identity clut and configuring the GPU for identity pixel passthrough, % and RestoreCluts, which restores to the state before % LoadIdentityClut(). I think "sca" calls that, as well as showing the % cursor and other cleanup actions. % % LoadIdentityClut is also automatically called by PsychImaging etc. if % the image processing pipeline is used for % Bits++/Datapixx/Viewpixx,video attenuators etc. % % It is important to always use LoadIdentityClut() instead of self-made % code. Many (most?) graphics cards on most operating systems have % graphics driver bugs and hardware quirks which cause the self-made % identity clut to actually not turn out to be an identity clut in the % hardware. LoadIdentityClut() has heuristics to detect os/gpu combo % and select an appropriately fudged lut to actually get an identity % mapping. In case of new hardware with new quirks we should update % that files detection logic to cope. Additionally there is % SaveIdentityClut() to save a known good lut for automatic use with % LoadIdentityClut, overriding its choice. And there is % BitsPlusIdentityClutTest to test a Bits+ or Datapixx device % thoroughly for problems and aid in fixing them. It is easy to get % fooled into thinking you got it right, even when using a photometer, % because some of the problems are subtle and not detectable without % use of dynamic test patterns like in BitsPlusIdentityClutTest with % Mono++ mode, or special debug functionality as in the % DataPixx/ViewPixx devices. % % I use a DataPixx to test such stuff and PTB has builtin diagnostic to % utilize that hardware to make sure that the video signal is really % untampered. % % LoadIdentityClut also calls special functions in Screen that try to % workaround or fix other hardware interference. E.g., in addition to % cluts messing with pixel passthrough, there is also display dithering % on any digital video output, and some new pre- and post-gamma % corrections in latest generation hardware -- Screen can fix this for % some cards on some operating systems, but only when used through % LoadIdentityClut. % % E.g., almost all AMD cards will cause trouble on OSX when the % PsychtoolboxKernelDriver and LoadIdentityClut etc. is not used, % almost all NVidia cards on OSX will cause trouble unless you use some % special NVidia kernel driver which is somewhere referenced on the % Bits+ pages on our wiki. We have/need similar hacks on Windows, e.g., % PsychGPUControl() for AMD cards. On Linux either PTB's low-level % fixes apply or they are not neccessary. % % So the CalibrateMonSpd etc. should be fixed to use % LoadIdentityClut/RestoreCluts, everything else is just begging for % trouble. % % 7/7/98 dhb Wrote from generic. % dhb dacsize/driver filled in by hand, Screen fails to return it. % 4/7/99 dhb NINEBIT -> NBITS. % dhb Wrote version for Radius 10 bit cards. % 4/23/99 dhb Change wavelength sampling to 380 4 101, PR-650 native. % 9/22/99 dhb, mdr Define boxSize. % 8/11/00 dhb Save mon in rawdata. % 8/18/00 dhb More descriptive information saved. % 8/20/00 dhb Automatic check for RADIUS and number of DAC bits. % 9/10/00 pbe Added option to blank another screen while measuring. % 2/27/02 dhb Various small fixes, including Radeon support. % dhb Change noMeterAvail to whichMeterType. % 11/08/06 cgb, dhb OS/X. % 9/27/08 dhb Default primary bases is 1 now. Use RefitCalLinMod to change later if desired. % 8/19/12 mk Ask user for choice of display output device. % 6/30/23 mk Use new clut mapping to fix this mess on standard gpus. Also allow % choice of different supported colormeters, not just PR-650. % 8/31/23 mk Assign cal.describe.dacsize also if g_usebitspp is already set. global g_usebitspp; % Unified key mapping, no unit color range by default: PsychDefaultSetup(1); % Create calibration structure; cal = []; % Script parameters whichScreen = max(Screen('Screens')); % Type of colormeter to use for measurement of the spectra. See most up to date % list in 'help CMCheckInit', but as of June 2023, there is a range of Photo Research % devices: % 0 = Simulated (Default), 1 = PR650, 4 = PR655, 5 = PR670, 6 = PR705 whichMeterType = 0; cal.describe.leaveRoomTime = 10; cal.describe.nAverage = 2; cal.describe.nMeas = 30; cal.describe.boxSize = 400; cal.nDevices = 3; cal.nPrimaryBases = 1; switch whichMeterType case {0,1} cal.describe.S = [380 4 101]; case 2 cal.describe.S = [380 1 401]; otherwise cal.describe.S = [380 4 101]; % Or should it be [380 5 81]? end cal.manual.use = 0; % Enter screen defaultScreen = whichScreen; whichScreen = input(sprintf('Which screen to calibrate [%g]: ', defaultScreen)); if isempty(whichScreen) whichScreen = defaultScreen; end cal.describe.whichScreen = whichScreen; % If the global flag for using Bits++ is empty, then it hasn't been % initialized and we ask user what to use: if isempty(g_usebitspp) g_usebitspp = input('Which high-res display device? [0=None, 1=CRS Bits++/Bits#/..., 2=VPixx DataPixx/ViewPixx/ProPixx/...]'); end switch(g_usebitspp) case 0 fprintf('Using standard graphics card with 8 bpc framebuffer.\n'); % We want dacsize to be so that 2^dacsize gives the size of the gamma % lut of the display device, iow. a gamma correction table is produced % as output, which can be loaded into the display devices hardware lut. % For a standard display connected to a standard gpu, the relevant gamma % hardware lut is the one of the gpu, with 'reallutsize' slots of size, % so query reallutsize from system and choose dacsize accordingly: [~, ~, reallutsize] = Screen('ReadNormalizedGammaTable', whichScreen); cal.describe.dacsize = log2(reallutsize); case 1 fprintf('Using CRS Bits++/Bits# et al. device in Bits+ CLUT mode.\n'); % For 2^8 = 256 slots builtin hw lut of CRS devices: cal.describe.dacsize = 8; case 2 fprintf('Using VPixx DataPixx/ViewPixx et al. in L48 CLUT mode.\n'); % For 2^8 = 256 slots builtin hw lut of VPixx devices: cal.describe.dacsize = 8; otherwise error('Unsupported display device. Aborted.'); end % Blank screen defaultBlankOtherScreen = 0; blankOtherScreen = input(sprintf('Do you want to blank another screen? (1 for yes, 0 for no) [%g]: ', defaultBlankOtherScreen)); if isempty(blankOtherScreen) blankOtherScreen = defaultBlankOtherScreen; end if blankOtherScreen defaultBlankScreen = 2; whichBlankScreen = input(sprintf('Which screen to blank [%g]: ', defaultBlankScreen)); if isempty(whichBlankScreen) whichBlankScreen = defaultBlankScreen; end cal.describe.whichBlankScreen = whichBlankScreen; end % Prompt for background values. The default is a guess as to what % produces one-half of maximum output for a typical CRT. defBgColor = [190 190 190]'/255; thePrompt = sprintf('Enter RGB values for background (range 0-1) as a row vector [%0.3f %0.3f %0.3f]: ',... defBgColor(1), defBgColor(2), defBgColor(3)); while 1 cal.bgColor = input(thePrompt)'; if isempty(cal.bgColor) cal.bgColor = defBgColor; end [m, n] = size(cal.bgColor); if m ~= 3 || n ~= 1 fprintf('\nMust enter values as a row vector (in brackets). Try again.\n'); elseif (any(defBgColor > 1) || any(defBgColor < 0)) fprintf('\nValues must be in range (0-1) inclusive. Try again.\n'); else break; end end % Get distance from meter to screen. defDistance = .80; theDataPrompt = sprintf('Enter distance from meter to screen (in meters): [%g]: ', defDistance); cal.describe.meterDistance = input(theDataPrompt); if isempty(cal.describe.meterDistance) cal.describe.meterDistance = defDistance; end % Fill in descriptive information computerInfo = Screen('Computer'); hz = Screen('NominalFrameRate', cal.describe.whichScreen); cal.describe.caltype = 'monitor'; if isfield(computerInfo, 'consoleUserName') cal.describe.computer = sprintf('%s''s %s, %s', computerInfo.consoleUserName, computerInfo.machineName, computerInfo.system); else % Better than nothing: cal.describe.computer = OSName; end cal.describe.monitor = input('Enter monitor name: ','s'); cal.describe.driver = sprintf('%s %s','unknown_driver','unknown_driver_version'); cal.describe.hz = hz; cal.describe.who = input('Enter your name: ','s'); cal.describe.date = sprintf('%s %s',date,datestr(now,14)); cal.describe.program = sprintf('CalibrateMonSpd, background set to [%g,%g,%g]',... cal.bgColor(1), cal.bgColor(2), cal.bgColor(3)); cal.describe.comment = input('Describe the calibration: ','s'); % Get name defaultFileName = 'monitor'; thePrompt = sprintf('Enter calibration filename [%s]: ',defaultFileName); newFileName = input(thePrompt,'s'); if isempty(newFileName) newFileName = defaultFileName; end % Fitting parameters cal.describe.gamma.fitType = 'crtPolyLinear'; cal.describe.gamma.contrastThresh = 0.001; cal.describe.gamma.fitBreakThresh = 0.02; % Initialize switch whichMeterType case 0 % Simulated only. case 2 CVIOpen; otherwise CMCheckInit(whichMeterType); end ClockRandSeed; % Calibrate monitor USERPROMPT = 1; cal = CalibrateMonDrvr(cal, USERPROMPT, whichMeterType, blankOtherScreen); % Calibrate ambient USERPROMPT = 0; cal = CalibrateAmbDrvr(cal, USERPROMPT, whichMeterType, blankOtherScreen); % Signal end Beeper; WaitSecs(.75); Beeper; WaitSecs(.75); Beeper; WaitSecs(.75); % Save the structure fprintf(1, '\nSaving to %s.mat\n', newFileName); SaveCalFile(cal, newFileName); % Put up a plot of the essential data figure(1); clf; plot(SToWls(cal.S_device), cal.P_device); xlabel('Wavelength (nm)', 'Fontweight', 'bold'); ylabel('Power', 'Fontweight', 'bold'); title('Phosphor spectra', 'Fontsize', 13, 'Fontname', 'helvetica', 'Fontweight', 'bold'); axis([380, 780, -Inf, Inf]); figure(2); clf; plot(cal.rawdata.rawGammaInput, cal.rawdata.rawGammaTable, '+'); xlabel('Input value', 'Fontweight', 'bold'); ylabel('Normalized output', 'Fontweight', 'bold'); title('Gamma functions', 'Fontsize', 13, 'Fontname', 'helvetica', 'Fontweight', 'bold'); hold on plot(cal.gammaInput, cal.gammaTable); hold off figure(gcf); drawnow; % Close down meter switch whichMeterType case 0 % Simulated needs no close. case 2 CVIClose; otherwise CMClose(whichMeterType); end