How to Achieve Consistent CFF (Critical Flicker Fusion) Presentation on a 240Hz Screen?

Hello everyone,

I am currently using MATLAB’s Psychtoolbox to conduct a visual stimulation experiment, aiming to achieve CFF (Critical Flicker Fusion) presentation. The refresh rate of the monitor I am using is 240Hz. However, during the actual operation, I found that the flicker effect is very uneven and appears unstable.

Here is a brief description of my experimental setup:

  • I am generating flicker stimuli using Psychtoolbox in the MATLAB environment.
  • I use the Screen('Flip') function to control the screen flip timing and adjust the flicker frame interval with waitframes .
  • The refresh rate of the monitor is 240Hz, and I want to achieve a flicker frequency of at least 30Hz(requiring the monitor’s refresh rate is 60Hz), to ensure stable high-frequency visual stimulation and achieve CFF.
  • Psychtoolbox version: 3.0.19 - Flavor: Manual Install
  • MATLAB version: 2023b.
    *Window11

However, when testing at 240Hz refresh rate, the flicker stimuli are uneven, with noticeable jumps or discontinuities. I have tried the following methods, but the issue persists:

  1. Adjust waitframes Value: I adjusted waitframes according to different flicker frequencies, but jitter still occurs.
  2. Check Refresh Rate and Timestamps: I controlled the flip timing using ifi and vbl timestamps to reduce errors.
  3. Ensure Priority Level and Drawing Finished: I set the maximum priority level with MaxPriority(window) and called Screen('DrawingFinished', window) to ensure drawing operations complete before flipping.

I suspect this might be related to the synchronization mechanism of high refresh rate monitors or due to insufficient synchronization between MATLAB and the hardware.

I would like to ask if anyone has experience achieving high flicker frequencies (e.g., 60Hz or above) on high refresh rate monitors to achieve CFF? Are there any optimization suggestions for MATLAB and Psychtoolbox in a Windows environment?

Any help or suggestions would be greatly appreciated!

%% Initialize Psychtoolbox
clear
Screen('Preference', 'SkipSyncTests', 1); % Skip sync tests for debugging
PsychDefaultSetup(2);
KbName('UnifyKeyNames'); % Unify key names

% Specify screen number
screenNumber = 1; % Use the first display

% Calculate color values
white = WhiteIndex(screenNumber);
black = BlackIndex(screenNumber);
grey = white / 2;

% Open a window and set it to grey background
[window, windowRect] = PsychImaging('OpenWindow', screenNumber, grey);

% Define screen center position
[xCenter, yCenter] = RectCenter(windowRect);

% Set square size and position
squareSize = 300; % Size of the square
squareRect = CenterRectOnPointd([0, 0, squareSize, squareSize], xCenter, yCenter);

% Define initial colors and frame rate parameters
red = [0.5, 0, 0];
green = [0, 0.5, 0];
waitframes = 5; % Initial wait frames
ifi = Screen('GetFlipInterval', window);

% Initialize control variable
currentFrame = 1; % Used to alternate between red and green squares

% Record initial flip time
vbl = Screen('Flip', window);
[texture1, texture2, GrayImage] = processImage(window, 'house.png', red, green);
scaleFactor = 1.0; % Initial scaling factor

imageRect = CenterRectOnPointd([0, 0, size(GrayImage, 2) * scaleFactor, size(GrayImage, 1) * scaleFactor], xCenter, yCenter);
lastKeyTime = GetSecs; % Initialize last key press time
responseInterval = 0.1; % Keyboard response interval (seconds)
topPriorityLevel = MaxPriority(window);
Priority(topPriorityLevel);

% Enter main loop
while true
    % Check keyboard input
    [keyIsDown, ~, keyCode] = KbCheck;
    currentTime = GetSecs;
    if currentTime - lastKeyTime > responseInterval
        [keyIsDown, secs, keyCode] = KbCheck;

        if keyIsDown
            lastKeyTime = secs; % Update last key press time

            if keyCode(KbName('DownArrow')) % Press down arrow to increase waitframes
                waitframes = waitframes + 1;
            elseif keyCode(KbName('UpArrow')) % Press up arrow to decrease waitframes
                waitframes = max(1, waitframes - 1); % Ensure waitframes is at least 1
            elseif keyCode(KbName('ESCAPE')) % Press ESC to exit loop
                break;
            end
        end
    end

    % Calculate flicker frequency
    flashFrequency = Screen('NominalFrameRate', screenNumber) / (2 * waitframes);

    % Select color to draw based on currentFrame variable
    if currentFrame == 1
        Screen('DrawTexture', window, texture1, [], imageRect);
        currentFrame = 0;
    else
        Screen('DrawTexture', window, texture2, [], imageRect);
        currentFrame = 1;
    end

    % Display current waitframes and flicker frequency on the screen
    textString = sprintf('Waitframes: %d\nFlash Frequency: %.2f Hz', waitframes, flashFrequency);
    Screen('DrawText', window, textString, 50, 50, white);

    % Define fixation point color as blue
    fixationColor = [0, 0, 1]; % RGB color [0, 0, 1] corresponds to blue

    % Define fixation point size
    fixationSize = 15; % Radius of 15 pixels

    % Define fixation point position (center of the screen)
    fixationRect = CenterRectOnPointd([0 0 fixationSize fixationSize], xCenter, yCenter);

    % Draw blue circular fixation point
    Screen('FillOval', window, fixationColor, fixationRect);
    
    Screen('DrawingFinished', window);
    % Flip screen, using waitframes to control flip timing
    vbl = Screen('Flip', window, vbl + (waitframes - 0.5) * ifi);
end

% Close window
sca;

function [texture1, texture2, GrayImage] = processImage(window, ImagePath, red, green, blurRadius)
    % Load and process image
    Image = imread(ImagePath); % Load image

    % Check the type of image
    if size(Image, 3) == 1
        % Image is grayscale
        GrayImage = im2double(Image);
    elseif size(Image, 3) == 3
        % Image is RGB
        GrayImage = im2double(rgb2gray(Image));
    else
        % If the image is indexed, convert it to RGB
        [X, map] = imread(ImagePath);
        Image = ind2rgb(X, map);
        GrayImage = im2double(rgb2gray(Image));
    end

    % Resize the image to 300x300 pixels
    targetSize = [300, 300];
    GrayImage = imresize(GrayImage, targetSize); % Resize grayscale image

    % Create mask
    lineMask = GrayImage < 0.5; % Assume lines are darker than background, threshold 0.5 can be adjusted
    lineMask = double(lineMask); % Keep range in [0, 1]

    % Apply Gaussian blur to lineMask if blurRadius is provided
    if nargin == 5 && ~isempty(blurRadius) && blurRadius > 0
        % Create Gaussian filter using fspecial
        h = fspecial('gaussian', [5 5], blurRadius);
        lineMask = imfilter(lineMask, h, 'replicate');
    end

    % Expand lineMask to 3 channels to match color matrix dimensions
    lineMask = repmat(lineMask, [1, 1, 3]);

    % Define colors (red lines and green background)
    lineColor1 = reshape(red, [1, 1, 3]); % Red lines
    bgColor1 = reshape(green, [1, 1, 3]); % Green background

    % Generate image with red lines and green background
    colorImage1 = lineMask .* lineColor1 + (1 - lineMask) .* bgColor1;

    % Define colors (green lines and red background)
    lineColor2 = reshape(green, [1, 1, 3]); % Green lines
    bgColor2 = reshape(red, [1, 1, 3]); % Red background

    % Generate image with green lines and red background
    colorImage2 = lineMask .* lineColor2 + (1 - lineMask) .* bgColor2;

    % Create textures
    texture1 = Screen('MakeTexture', window, colorImage1);
    texture2 = Screen('MakeTexture', window, colorImage2);
end

If anyone has any suggestions or experiences, especially regarding how to achieve precise high flicker frequency visual stimulation on high refresh rate monitors to achieve CFF, I would greatly appreciate it!

There are thing that could be improved in your script and approach, but for up to 30 minutes of my time in delving into this more, you’ll have to buy paid support help PsychPaidSupportAndServices.

Three free pieces of advice:

  • If you want to have the best chance of reliable, precisely timed presentation in general, but especially for higher refresh rates like 240 Hz or for multi-display setups, switch to a recommended Linux variant with recommended graphics hardware.
  • If you absolutely must use Microsoft Windows for this (probably the worst choice), do not use dual-display/multi-display setups, where Windows is especially fragile wrt. timing, only a computer with a single display monitor connected and active. No HiDPI displays either.
  • Text drawing is a relatively expensive operation, which can take milliseconds, so avoiding that, or being clever about it, gives more headroom for situations like yours, where you only have 4 msecs available for all processing in a frame.

Even with these tips, MS-Windows may not cut it.

Good luck.

I can see you have been using my demos.

There are a bunch of things you are doing which are not ideal. At a quick glance from a quick scan…

(1) You are needlessly repeating things in the display loop. Remove anything from your loop which is not changing. fixationSize, fixationRect, fixationColor, flashFrequency, there may be more. These all are just repeatedly called on every refresh cycle but do not change, so should not be in the loop.

(2) Your use of DrawingFinished is probably not needed. As stated in the my demos, this is normally used when you have other calculations to perform prior to the flip.

(3) The text drawing does not seem to serve any real purpose and you are again doing repeated processing in the display loop this is not needed. If it is desperately needed make it out of the display loop and only draw it within. Probably better still, draw it to a texture outside of the loop and then add another texture to your “draw texture” call.

(4) FillOval has an additional parameter you can use to specify “perfect up to a size”, this can help with reducing processing. Though I don’t fully understand why you do not just draw a dot instead - it would probably at my guess be faster.

(5) For this use case, I would also suggest maybe experimenting with keyboard queues, rather than your KbCheck.

(6) Don’t use Windows. Your use case is getting to the limit in terms of timing, which seems important for your use case.

Note: the nominal frame rate you are using is not necessarily the actual frame rate (as stated in the demos). You are not using the variable, but just to say that the frame rate on the box is not necessarily the real frame rate.

I assume you are using a flatscreen monitor? If so you need to be aware that these are typically not good for high fidelity timing. For many, many reasons.

There are probably more things, but the above is some quick things to try from a quick scan.

Peter

1 Like