CreateSinglePassImageProcessingShaderでシェーダーを使ってみよう


Matlabで心理物理学実験を行うためのツールボックスに、Psychtoolbox(PTB)というのがあります。公式サイトはこちら

つい最近、PTBでシャーダーを使うためのCreateSinglePassImageProcessingShaderという関数があることを知りまして、それについての解説記事です。

とかっこよく宣言したものの、恥ずかしながら私自身、シェーダーがなにかをうまく説明できずにいます。シェーダーがなにかについて知りたいひとはgoogleで検索したり、あるいはQiitaのこちらの記事しっかり学ぶシェーダプログラミングが非常に有益かと思います。

私の理解としては、シェーダーとは、CPUを使った一般的な方法で描画しようとすると負荷が非常に高くなる作業を、グラフィクスカード(ビデオカード、GPU)に任せるというものです。もっと簡単に言えば、シェーダーを使えば超高速に図形を描画できるよ!という話です。

例えば心理物理学実験ではガボールパッチというのがよく使われますが、これを懇切丁寧に描画すると負荷が高くかなりの時間がかかります。この記事では取り上げませんが、シェーダーを使ってガボールパッチを超高速で描画する関数CreateProceduralGaborがPTBには備わっていますので、興味のある方はぜひ確認してみてください。Dr. Peter Scarfeによる解説も大変勉強になります。


ではさっそく、いつものようにDr. Peter Scarfeのコードをベースに始めていきましょう。

% Clear the workspace and the screen
sca;
close all;
clearvars;

% Here we call some default settings for setting up Psychtoolbox
PsychDefaultSetup(2);

% Get the screen numbers. This gives us a number for each of the screens
% attached to our computer.
screens = Screen('Screens');

% To draw we select the maximum of these numbers. So in a situation where we
% have two screens attached to our monitor we will draw to the external
% screen.
screenNumber = max(screens);

% Define black and white (white will be 1 and black 0). This is because
% in general luminace values are defined between 0 and 1 with 255 steps in
% between. All values in Psychtoolbox are defined between 0 and 1
white = WhiteIndex(screenNumber);
black = BlackIndex(screenNumber);

% Do a simply calculation to calculate the luminance value for grey. This
% will be half the luminace values for white
grey = white / 2;

% Open an on screen window using PsychImaging and color it grey.
[windowPtr, windowRect] = PsychImaging('OpenWindow', screenNumber, grey, [10 10 800 800]);

Screen('FillRect', windowPtr, [0 0 1], [350 350 450 450]); % 青い四角形を描画

% ここから

% ここまで

Screen('Flip', windowPtr); % 画面を更新

% Now we have drawn to the screen we wait for a keyboard button press (any
% key) to terminate the demo.
KbStrokeWait;

% Clear the screen.
sca;

このコードはグレーの背景に青い正方形を描画します。こんな感じ。

以下の説明は「ここから」「ここまで」のあいだに記述してください。

myShader = CreateSinglePassImageProcessingShader(windowPtr, 'BackgroundMaskOut', [1 0 0]); % シェーダーを作成
masktex = Screen('OpenOffscreenWindow', screenNumber, [1 0 0], [0 0 200 200]); % 赤い背景でオフスクリーンウィンドウを作成
Screen('FillRect', masktex, [0 1 0], [0 0 100 100]); % オフスクリーンウィンドウに緑の正方形を描画
Screen('DrawTexture', windowPtr, masktex); % オフスクリーンウィンドウをオンスクリーンウィンドウに描画

CreateSinglePassImageProcessingShaderでシェーダーを作成します。この関数では2種類のシェーダーを作成することができます(もうひとつは後述)

2番目の引数にBackgroundMaskOutを指定した場合、特定の色を除去することができます。「特定の色」は3番目の引数で指定します。上の例では[1 0 0]、つまり赤ですね。

Screen('OpenOffscreenWindow')でオフスクリーンウィンドウを作成します。オフスクリーンウィンドウの背景色は赤[1 0 0]で、そのサイズは200x200ピクセルです。

一般的には、PTBでなにかを描画するときはオンスクリーンウィンドウに画像やら幾何学図形を描画して、Flipして画面に反映させます。一方のオフスクリーンウィンドウにも、同様に画像や幾何学図形を描画できますが、そのままではFlipしても画面には反映されません。Flipで反映されるのは、オンスクリーンウィンドウに描画されたものだけです。オフスクリーンウィンドウは予備の画用紙、と考えると分かりやすいかもしれません。

上のコードでは、3行目で緑[0 1 0]の正方形をオフスクリーンウィンドウに描画しています。そして4行目でオフスクリーンウィンドウをオンスクリーンウィンドウに描画しています。オンスクリーンウィンドウにオフスクリーンウィンドウを重ね合わせたと考えると分かりやすいですね。こうすることで初めて、オフスクリーンウィンドウの内容がFlip時に画面に反映されます。

なお重要な点ですが、上のコードではシェーダーを作成はしているのですが、実際には使っていません。

この状態で実行すると下のような画面になると思います。グレー部分はオンスクリーンウィンドウの背景色です。赤い部分はオフスクリーンウィンドウの背景色、そしてオフスクリーンウィンドウに描画した緑の正方形が表示されます。最初に描画した青い正方形が見えなくなっている点に着目してください。

特定の色を除去

勘のよいかたはお気づきかと思いますが、シェーダーを使って赤い色を除去して、背景に存在する青い正方形を見えるようにするというのが目的です。

そのためのシェーダーはすでに作成済みです。
実は上のコードの4行目を次のように変更するだけです。

Screen('DrawTexture', windowPtr, masktex, [], [], [], [], [], [], myShader);

[]が多過ぎ! という声が聞こえてきそうです。その気持ちはよく分かりますが、そこはがまんしてください(笑) []の意味は引数を明記しないということで、デフォルトの値が使用されます。この例では、4番目から9番目の引数は指定する必要がないため、上のような書き方になります。10番目の引数にmyShaderを指定します。

こんな感じの結果が得られたでしょうか?

上の例はシンプルなのでシェーダーの恩恵が伝わりにくいかもしれませんが、複雑な刺激を呈示したときに、その背景色だけをさーっと消し去ってくれるってすごいことだと思いません? しかもそれをふつうにやろうと思ったら、1ピクセルずつこれは何色か?と確認して、該当する色だったら透明にする、みたいなことをするわけです。めっちゃ時間がかかりそう。それをいとも簡単にやってのけるのがシェーダーのすごいところ!(という話なのだと思います)

WeightedColorComponentSum

CreateSinglePassImageProcessingShaderでは、もうひとつシェーダーを作ることができます。

myShader = CreateSinglePassImageProcessingShader(windowPtr, 'WeightedColorComponentSum');

このシェーダーは、RGBAチャンネルのそれぞれを定数倍して足し合わせ、そうして計算された値をRGBAチャンネルのそれぞれに設定します。分かりにくいですね。具体的に見てみましょう。

RGBA=[0.2 0.4 0.6 0.8]
modulateColor = [0.3 0.5 0.7 0.9]

とします。RGBAは、あるピクセルの赤緑青、そして透明度アルファです。modulateColorはかけ算に使用する係数と思ってください。modulateColorは4つの数字を取り、それぞれRGBAチャンネルに対応します。modulateColorの説明はPsychtoolboxでアルファブレンド(色の混ぜ合わせ)にも書いていますので、よろしければご覧ください。

で、どのような計算をするかというと、

sum = (0.2 x 0.3) + (0.4 x 0.5) + (0.6 x 0.7) + (0.8 x 0.9)

を計算して、このsumを、RGBAのそれぞれのチャンネルに設定します。
つまり、RGBA = [sum sum sum sum] となります。ヘルプに書かれている通りです。

分かったけど、これどんなときに使うの? て思いますよね。

たぶん、いろいろと用途はあると思うのですが、分かりやすいのはカラー画像をグレースケールに変換するときです。RGBのすべてが同じ値になるということは、グレースケールにほかなりません。

サンプルコードで確認してみましょう。例によってDr. Peter Scarfeのコードを使ってください。「ここから」「ここまで」に追記します。

myShader = CreateSinglePassImageProcessingShader(windowPtr, 'WeightedColorComponentSum');
%画像情報をテクスチャに
imdata = imread('testImage.jpg');
imagetex = Screen('MakeTexture', windowPtr, imdata);
Screen('DrawTexture', windowPtr, imagetex);

1行目でシェーダーを作成しています。2行目から4行目までは、PTBで画像を表示するときの一般的なコードです。
testImage.jpgはみなさんのほうで準備してください。カラー画像がよいです。この画像はプログラムと同じフォルダ内に保存します。

まずは上の4行のコードで、画像がカラーで呈示されることを確認してください。

次に、4行目を次のように変更します。

Screen('DrawTexture', windowPtr, imagetex, [], [], [], [], [], [0 0 1 0], myShader);

9番目の引数でmodulateColorを指定します。この例では[0 0 1 0]となっていますね。この意味を考えてみましょう。BをのぞくRGAチャンネルの係数はゼロですから、sumはBチャンネルと等しくなります。そしてsumはRGBAのすべてのチャンネルに適用されるわけですから、元々のBチャンネルがRGAの3つのチャンネルにコピーされると思ってよいですね。その結果、グレースケールの画像が呈示されます。

補足ですが、

modulateColor = [1 0 0 0]
modulateColor = [0 1 0 0]
modulateColor = [1 1 0 0]

などのように設定してもグレースケールになります。ですが有効になっているチャンネルがそれぞれ異なるため、同一の結果にはならないことにご注意ください。

さらに補足すると、

modulateColor = [0 0 0 1]

にすると画像が表示されず、真っ白になります。なぜでしょうか?

modulateColor = [0 0 0 1]ということは、アルファチャンネルの値が、RGBにコピーされることを意味します。ではアルファチャンネルの値はいくつでしょう? 特別に設定していない場合、デフォルトでは1になります。つまりシェーダーを使用した結果、RGBのすべてが1となり、画像の情報は失われ、真っ白になるのです。

さて、上の例では DrawTextureでシェーダーを指定していますが、MakeTextureでも指定可能です。例えば次のコードです。

imdata = imread('testImage.jpg');
imdata(:,:,2) = 0; % Gチャンネルをゼロに
imdata(:,:,3) = 0; % Bチャンネルをゼロに
imdata(:,:,4) = 0; % Aチャンネルをゼロに
imagetex = Screen('MakeTexture', windowPtr, imdata, [], [], [], [], myShader); % シェーダーを指定
Screen('DrawTexture', windowPtr, imagetex); % DrawTextureではシェーダーを指定していない

この場合は、RチャンネルがRGBAすべてのチャンネルにコピーされてグレースケールの画像になります。

この記事ではCreateSinglePassImageProcessingShaderの基本的な動作の説明に終始していて、シェーダーの効果が分かりにくいと思います。シェーダーの効果を体感したいというひとは、PTBに付随しているSimpleImageMixingDemoをぜひご覧ください。きっとびっくりしますよ!