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..
|