File: ImageMixingTutorial.m

package info (click to toggle)
psychtoolbox-3 3.0.19.14.dfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 86,796 kB
  • sloc: ansic: 176,245; cpp: 20,103; objc: 5,393; sh: 2,753; python: 1,397; php: 384; makefile: 193; java: 113
file content (480 lines) | stat: -rw-r--r-- 21,512 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
function ImageMixingTutorial(mode, ms, myimgfile)
% ImageMixingTutorial([mode=1][, ms=200][, myimgfile])
%
% ImageMixingTutorial shows how to use a combination of alpha blending,
% offscreen windows and some basic image processing shaders to mix two
% images together, using a "mix weight mask" (aka alpha mask) which itself
% is dynamically updated via Screen() drawing commands like DrawTexture,
% DrawTexture with shaders, FillRect etc. This allows for interesting
% new gaze contingent displays or dynamically changing binocular rivalry
% stimuli.
%
% The basic working principle:
%
% 1. An offscreen window is created which stores the alpha blend mask
%    with per-pixel mixing weights. ("masktex" in the code).
%
% 2. The offscreen window stores the mix weights in its *luminance* channel,
%    (which is the same as the red channel for technical reasons). This way,
%    grayscale "luminance" values (luminance == red == green == blue) directly
%    encode "mixing weights". As we use a normalized 0-1 color range in this
%    demo ("PsychDefaultSetup(2)"), a grayscale value from 0 - 1 (aka from
%    black to white) directly corresponds to a mix weight from 0 - 1. This
%    allows us to use standard Screen() 2D drawing commands as usual to draw
%    a mix weight mask as a grayscale image into the offscreen window without
%    any deeper knowledge or thought about alpha blending. We can use all
%    drawing commands to quickly and dynamically update or redraw the grayscale
%    image in the offscreen window to create a dynamically changing mix weight
%    mask.
%
% 3. A shader is used to convert the grayscale image in the offscreen window
%    into a alpha mask and draw that alpha mask into the framebuffer of the
%    onscreen window, thereby setting the alpha channel of the onscreen window
%    to the desired mix weight mask for mixing the actual stimulus images.
%
% 4. Alpha blending is used to draw the two target stimulus images, mixing
%    them together according to the alpha channel created in step 3 from the
%    grayscale weight mask dynamically created in step 2.
%
% 5. The final mixed stimulus, e.g., a binocular rivalry stimulus, is shown
%    to the subject, rinse wash, repeat with step 2.
%
% This demo shows how to use normalized color ranges from 0 - 1 as a more
% natural representation of such alpha mix weights. It shows how to use the
% 'WeightedColorComponentSum' shader to both morph up to 4 masks together into
% one weight mask, and as an alternate use, how to move the content of the
% red channel of a window (== luminance/grayscale channel in a grayscale image)
% into the alpha channel, allowing to implement step 3 above. It also uses
% alpha blending in combination with a separate offscreen window in a non-usual
% way to allow to logically separate the process of creating/updating a mix weight
% mask from the process of actually applying that mask to a pair of stimulus images.
% This approach is not neccessary for simple gaze-contingent displays or rivalry
% stimuli (cfe. GazeContingentDemo / GazeContingentTutorial / BubbleDemo for simpler
% approaches). It is beneficial for stimuli which require complex mix masks, or
% complex dynamically updated mix masks, as it allows to implement an approach that
% reduces implementation complexity and is more natural or easier on the brain of
% the implementer of the stimulus, with less potential for coding errors or confusion
% about side effects of alpha blending.
%
% The tutorial allows you to switch between different stages of the processing
% involved in this approach and see their effects "live", by use of different
% keys on the keyboard, and to draw a dynamic mask via use of the mousecursor
% as a paint brush. It also shows some automatically running use of procedural
% shaders, texture animation and other Screen drawing primitives.
%
% This tutorial is powerful in its potential use cases, but requires significant
% customization for specific paradigms, and a good and careful reading of the code.
%
% For a much more simple demo and application of the technique, have a look at
% the SimpleImageMixingDemo.m, written and contributed by Natalia Zaretskaya.
% ___________________________________________________________________

% HISTORY
% 11-Nov-2014 mk   Written.

% Use new-style color specifications in normalized range 0.0 - 1.0:
PsychDefaultSetup(2);

% Setup default mode to color vs. gray.
if nargin < 1
    mode = 1;
end

% Setup default aperture size to 2*200 x 2*200 pixels.
if nargin < 2
    ms=200;
end

% Basepath to our own demo images:
basepath = [ PsychtoolboxRoot 'PsychDemos' filesep ];

% Use default demo images, if no special image was provided.
if nargin < 3
    myimgfile= [basepath 'konijntjes1024x768.jpg'];
end

myblurimgfile= [basepath 'konijntjes1024x768blur.jpg'];
mygrayimgfile= [basepath 'konijntjes1024x768gray.jpg'];

try
    % Set background color to black aka zero intensity:
    backgroundcolor = 0.0;

    % Get the list of screens and choose the one with the highest screen number.
    screenNumber=max(Screen('Screens'));

    % Open a double buffered fullscreen window. Use PsychImaging(), so the
    % normalized 0.0 - 1.0 color format is used for drawing, instead of the
    % old 0 - 255 range:
    [w, wRect] = PsychImaging('OpenWindow', screenNumber, backgroundcolor);

    % Open an offscreen window the same size as the onscreen window. We use
    % this to define the alpha/mixing weight channel used to later mix
    % two images together:
    masktex = Screen('OpenOffscreenWindow', w, [0 0 0 0]);
    DrawFormattedText(masktex, 'Draw something into the mask with the mouse!', 'center', 40, [1 1 1 1]);
    Screen('TextSize', masktex, 256);

    % Load image file:
    fprintf('Using image ''%s''\n', myimgfile);
    imdata=imread(myimgfile);
    imdatablur=imread(myblurimgfile);
    imdatagray=imread(mygrayimgfile);

    % Crop image if it is larger then screen size. There's no image scaling
    % in maketexture:
    [iy, ix, id]=size(imdata);
    [wW, wH]=WindowSize(w);

    if ix>wW || iy>wH
        disp('Image size exceeds screen size');
        disp('Image will be cropped');
    end

    if ix>wW
        cl=round((ix-wW)/2);
        cr=(ix-wW)-cl;
    else
        cl=0;
        cr=0;
    end
    if iy>wH
        ct=round((iy-wH)/2);
        cb=(iy-wH)-ct;
    else
        ct=0;
        cb=0;
    end

    % imdataXXX is the cropped version of the images.
    imdata=imdata(1+ct:iy-cb, 1+cl:ix-cr,:);
    imdatablur=imdatablur(1+ct:iy-cb, 1+cl:ix-cr,:);
    imdatagray=imdatagray(1+ct:iy-cb, 1+cl:ix-cr,:);

    % Compute image for foveated region and periphery:
    switch (mode)
        case 1
            % Mode 1:
            % Fovea contains original image data:
            secondimdata = imdata;
            % Periphery contains grayscale-version:
            firstimdata = imdatagray;
        case 2
            % Fovea contains original image data:
            secondimdata = imdata;
            % Periphery contains blurred-version:
            firstimdata = imdatablur;
        case 3
            % Fovea contains color-inverted image data:
            secondimdata(:,:,:) = 255 - imdata(:,:,:);
            % Periphery contains original data:
            firstimdata = imdata;
        case 4
            % Test-case: One shouldn't see any foveated region on the
            % screen - this is a basic correctness test for blending.
            secondimdata = imdata;
            firstimdata = imdata;
        case 5
            secondimdata = imdata;
            firstimdata = imread([basepath 'PsychExampleExperiments/OldNewRecognition/stims/stim3.jpg']);
        otherwise
            % Unknown mode! We force abortion:
            fprintf('Invalid mode provided!');
            abortthisbeast
    end

    % Build texture for first image:
    firstImage=Screen('MakeTexture', w, firstimdata);

    % Build texture for second image:
    secondImage=Screen('MakeTexture', w, secondimdata);

    % We create a two layers Luminance + Alpha matrix for use as "gaussian" transparency
    % (or mixing weights) brush: Layer 1 (Luminance) is filled with luminance
    % value 1.0 aka white - the ones() function does this nicely for us, by
    % first filling both layers with 1.0:
    [x,y] = meshgrid(-ms:ms, -ms:ms);
    maskblob = ones(2*ms+1, 2*ms+1, 2);

    % Layer 2 (Transparency aka Alpha) is now filled/overwritten with a gaussian
    % transparency/mixing mask.
    xsd = ms / 2.2;
    ysd = ms / 2.2;
    maskblob(:,:,2) = exp(-((x / xsd).^2) - ((y / ysd).^2));

    % Copy alpha to luminance, just for visualization of the "mask brush" later on:
    maskblob(:,:,1) = maskblob(:,:,2);

    % Build a single transparency mask texture:
    gaussbrush = Screen('MakeTexture', w, maskblob);

    % Do initial flip to show blank screen:
    Screen('Flip', w);

    % The mouse-cursor position will define drawing-position. Set cursor
    % initially to center of screen, but do hide it from view:
    [a,b] = RectCenter(wRect);
    SetMouse(a,b,screenNumber);

    % Wait until all keys on keyboard are released:
    KbReleaseWait;

    % Show first image:
    Screen('DrawTexture', w, firstImage);
    Screen('TextSize', w, 24);
    DrawFormattedText(w, 'Step1: Create first texture:\nPress a key to continue\n', 0, 40, 1, 50);
    Screen('Flip', w);

    % Wait for mouseclick:
    KbStrokeWait;

    % Show second image:
    Screen('DrawTexture', w, secondImage);
    Screen('TextSize', w, 24);
    DrawFormattedText(w, 'Step2: Create second texture:\nPress a key to continue\n', 0, 40, 1, 50);
    Screen('Flip', w);
    KbStrokeWait;

    coverage = 0.25;
    mode = 0;
    brushtype = 0;
    startAngle = 0;
    imgRect = CenterRect(Screen('Rect', firstImage), wRect);
    cRect = OffsetRect([0 0 300 300], imgRect(RectLeft), imgRect(RectTop));

    % Build a procedural sine grating texture for a grating with a support of
    % 300 x 300 pixels and a RGBA color offset of 0.5:
    gratingtex = CreateProceduralSineGrating(w, 300, 300, [0.5 0.5 0.5 0.5]);
    gRect = OffsetRect([0 0 300 300], imgRect(RectLeft), imgRect(RectTop) + 300);
    phase = 0;

    % Create a shader that allows to combine the up to four input channels
    % of a texture into a weighted linear combination, using 'DrawTexture's
    % modulateColor parameter to specify the weights. This is used for
    % morphing between up to four alpha-masks, stored in the morphedAlphaTexture.
    minimorphshader = CreateSinglePassImageProcessingShader(w, 'WeightedColorComponentSum');
    
    % Create a texture with the alpha masks we want to morph between.
    %
    % We only create a one channel "luminance" texture which contains a gauss blob.
    % In the following use of this texture we'd like to "morph" between an all-zero layer,
    % an all-one layer and the "maskblob" gaussian shape layer. However, here we can optimize
    % a bit: We don't need an all-zero layer, because we get that implicitely, as any zero
    % value multiplied by any weight will always result in zero ( 0 * x == 0 for any x).
    % A "single layer" luminance texture will store our gaussian blob shape. Now any
    % "single layer luminance texture" automatically gets its alpha channel initialized to a
    % layer of all ones, ie., it implicitely carries around an alpha channel (the 4th channel)
    % which is filled with ones. In practice morphTex will have layers 1-3 (red, green, blue)
    % filled with the luminance values from morphTargets, and layer 4 filled
    % with 1's, so we can morph between the maskblob shape and a "constant one shape" simply by
    % drawing with the minimorphshader attached and modulateColor set to [w, 0, 0, 1-w] with w
    % moving between 0.0 and 1.0. We can morph between an "constant zero shape" and the morphTargets
    % shape by modulateColor set to [w, 0, 0, 0] with w moving between 0.0 and 1.0.
    % For this to work we need to use the minimorphshader during texture drawing. For convenience
    % we already attach the minimorphshader to the texture here, for later use:
    ysd = ms / 1.5;
    xsd = ysd;
    morphTargets = 100 * exp( -((x / xsd).^2) - ((y / ysd).^2) );
    morphTex = Screen('MakeTexture', w, morphTargets, [], [], 2, [], minimorphshader);
    mRect = []; %OffsetRect(Screen('Rect', morphTex), imgRect(RectLeft), imgRect(RectTop) + 600);

    blobtex = CreateProceduralGaussBlob(w, 300, 300, [], 1);

    ESCAPE = KbName('ESCAPE');
    SPACE = KbName('space');
    LeftArrow = KbName('LeftArrow');

    while 1
        % Query current mouse cursor position:
        [mx, my, buttons]=GetMouse;

        % Query keyboard:
        [pressed secs keycode] = KbCheck;
        if pressed
            KbReleaseWait;

            % ESC exits demo.
            if keycode(ESCAPE)
                break;
            end

            % Left Cursor key switches "drawing tool" for mask:
            if keycode(LeftArrow)
                brushtype = mod(brushtype + 1, 3);
            end

            % Space key switches display mode: mask, intermediate steps, final stim:
            if keycode(SPACE)
                mode = mod(mode + 1, 4);
            end
        end

        % --------------- Update / Draw into alpha "mixing" mask texture: -------------------

        % Compute position and size of destinationrect for gaussian brush
        % texture:
        dRect = CenterRectOnPoint(Screen('Rect', gaussbrush), mx, my);

        % Any buttons pressed for drawing/erasing?
        if any(buttons)
            % Yes! Draw into alpha mask image:

            % Which mouse button, if any?
            if any(buttons(2:end))
                % 2nd, 3rd, ... button: Erase by overpainting with zero alpha:
                cfactor = 0;
                Screen('Blendfunction', masktex, GL_ONE, GL_ZERO);
            else
                % First or none. Draw and accumulate with positive alpha:
                cfactor = 1;
                Screen('Blendfunction', masktex, GL_ONE, GL_ONE);
            end

            % Which drawing tool?
            switch brushtype
                case 0,
                  % Gaussian blob texture:
                  Screen('DrawTexture', masktex, gaussbrush, [], dRect, [], [], [], cfactor * [coverage coverage coverage coverage]);
                case 1,
                  % Oval:
                  Screen('FillOval', masktex, cfactor * [coverage coverage coverage coverage], CenterRectOnPoint([0 0 40 40], mx, my));
                case 2,
                  % Text can only be drawn, not erased, so do nothing in "erase mode":
                  if cfactor > 0
                      % Paint:
                      Screen('DrawText', masktex, 'Hello!', mx, my, [1 1 1 1]);
                  end
            end
        end

        % Some useless animations, just to show we can...
        Screen('Blendfunction', masktex, GL_ONE, GL_ZERO);

        Screen('FillRect', masktex, [0 0 0 0], cRect);
        startAngle = mod(startAngle + 2, 360);
        Screen('FillArc', masktex, [1 1 1 1], cRect, startAngle, 20);
        
        % Another useless animation - a procedural sine grating:
        amplitude = 0.5;
        freq = 5/360;
        angle = 0;
        Screen('DrawTexture', masktex, gratingtex, [], gRect, angle, [], [], [], [], [], [phase, freq, amplitude, 0]);
        
        % And another one - a mask morphing from all-zero to a gauss blob to all-one and back:
        if 0
            % From 0 -> Blob -> 1 -> Blob -> 0
            morphValue = sin(((phase / 10) - 90) / 360 * 2 * pi) + 1;
            if morphValue < 1
                weights = [morphValue, 0, 0, 0];
            else
                eweight = morphValue - 1;
                weights = [1 - eweight, 0, 0, eweight];
            end
            Screen('DrawTexture', masktex, morphTex, [], mRect, [], [], [], weights);
        else
            % From 0 -> Superblob -> 0
            morphValue = (sin(((phase / 10) - 90) / 360 * 2 * pi) + 1) / 2;
            weights = [morphValue, 0, 0, 0];
%            Screen('DrawTexture', masktex, morphTex, [], mRect, [], [], [], weights);
        end

        if 1
            % Use shader to draw blob with controllable amplitude and standard deviation:
            % Parameter vector [amplitude, stddev, aspect, 0]:
            morphValue = (sin(((phase / 10) - 90) / 360 * 2 * pi) + 1) / 2;
            Screen('DrawTexture', masktex, blobtex, [], [], [], [], [], [], [], kPsychDontDoRotation, [morphValue * 10, 100, 1.0, 0]);
            %Screen('DrawTexture', masktex, blobtex, [], [], [], [], [], [], [], kPsychDontDoRotation, [1, 200 * morphValue, 1.0, 0]);
        end
        
        phase = phase + 4;

        % --------------- Update actual stimulus, using the alpha "mixing" mask texture: -------------------

        % Step 1: Draw the alpha-mask into the backbuffer.
        if mode > 0
            % Actual use of masktex to define transition/mix:

            % First clear framebuffer to backgroundcolor, not using
            % alpha blending (== GL_ONE, GL_ZERO). Enable all channels
            % for writing [1 1 1 1], so everything gets cleared to good
            % starting values:
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [1 1 1 1]);
            Screen('FillRect', w, backgroundcolor);

            % Then keep alpha blending disabled and draw the mask
            % texture, but *only* into the alpha channel. Don't touch
            % the RGB color channels but use the channel mask via
            % [R G B A] = [0 0 0 1] to only enable the alpha-channel
            % for drawing into it. Use of modulateColor = [1 0 0 0] and
            % the minimorphshader causes the red channel to be copied into
            % the alpha channel. As red == luminance this means the grayscale
            % luminance value of masktext directly defines the final mask weights.
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [0 0 0 1]);
            Screen('DrawTexture', w, masktex, [], [], [], [], [], [1 0 0 0], minimorphshader);
        else
            % Visualize the alpha/mask masktex itself to explain
            % the concept - alpha values of 1 will show as white,
            % values of zero as black, intermediates as gray levels:
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [1 1 1 1]);
            Screen('DrawTexture', w, masktex, [], [], [], [], [], [1 0 0 0], minimorphshader);
        end

        % Step 2: Draw first image. It is only/increasingly drawn where
        % the alpha-value in the backbuffer is 1.0 or close, leaving
        % the foveated area (low or zero alpha values) alone:
        % This is done by weighting each color value of each pixel
        % with the corresponding alpha-value in the backbuffer
        % (GL_DST_ALPHA). Disable alpha channel writes via [1 1 1 0], so
        % alpha mask stays untouched and only RGB color channels are
        % affected:
        if mode == 1 || mode == 3
            Screen('BlendFunction', w, GL_DST_ALPHA, GL_ZERO, [1 1 1 0]);
            Screen('DrawTexture', w, firstImage);
        end

        % Step 3: Draw second image, but only/increasingly where the
        % alpha-value in the backbuffer is zero or low: This is
        % done by weighting each color value with one minus the
        % corresponding alpha-value in the backbuffer
        % (GL_ONE_MINUS_DST_ALPHA).
        if mode == 2 || mode == 3
            Screen('BlendFunction', w, GL_ONE_MINUS_DST_ALPHA, GL_ONE, [1 1 1 0]);
            Screen('DrawTexture', w, secondImage);
        end

        % Draw some text with explanation of the different steps:
        switch(mode)
            case 0,
                txt = 'Step3: Draw into alpha mask texture around mouse position:\nThis shows the alpha mask texture used for mixing of the images (white = 1.0 alpha weight, black = 0.0 alpha weight)';
            case 1,
                txt = 'Step4: Draw first texture, but weight each incoming source color pixel by the alpha value stored in the framebuffers alpha channel';
            case 2,
                txt = 'Step5: Draw second texture, but weight each incoming source color pixel by 1 minus the alpha value stored in the framebuffers alpha channel';
            case 3,
                txt = 'Perform alpha weighted compositing (all previous steps together):\n1. Draw alpha weight mask according to mouse position,\n2. Overdraw with alpha-weighted first texture,\n3. Overdraw with 1-alpha weighted second texture.';
        end
        txt = [txt '\nPress the SPACE key to continue to next step.\nPress Cursor left key to switch drawing tool.\nPress ESCAPE to exit demo.\n'];
        txt = [txt 'Press left mouse button to draw mask, other mouse button to erase mask'];
        DrawFormattedText(w, txt, 0, 40, [1 0 0], 60);

        % Show final result on screen. The 'Flip' also clears the drawing
        % surface back to black background color and a zero alpha value:
        Screen('Flip', w);
    end

    % Display full image a last time, just for fun...
    Screen('BlendFunction', w, GL_ONE, GL_ZERO);
    Screen('DrawTexture', w, secondImage);
    Screen('Flip', w);

    sca;
    fprintf('End of ImageMixingTutorial. Bye!\n\n');
    return;
catch
    %this "catch" section executes in case of an error in the "try" section
    %above.  Importantly, it closes the onscreen window if its open.
    sca;
    ShowCursor;
    Priority(0);
    psychrethrow(psychlasterror);
end %try..catch..