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

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

つい最近、PTBでシャーダーを使うための[CreateSinglePassImageProcessingShader](http://psychtoolbox.org/docs/CreateSinglePassImageProcessingShader)という関数があることを知りまして、それについての解説記事です。

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

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

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

---

ではさっそく、いつものように[Dr. Peter Scarfeのコード](http://peterscarfe.com/totallyminimaldemo.html)をベースに始めていきましょう。

```matlab
% 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;
```

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

![output2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/3e412e2d-0cdb-67d6-f57d-7c75e1b68d2e.jpeg)


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

```matlab
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`でシェーダーを作成します。この関数では２種類のシェーダーを作成することができます（もうひとつは後述）

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

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

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

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

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

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

![output1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/05644381-be41-8c3c-bf6a-aa010ca8545d.jpeg)

# 特定の色を除去

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

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

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

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

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

![output3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495823/257c3ed7-028e-87fb-59e1-d95812f2599f.jpeg)

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

# WeightedColorComponentSum

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

```matlab
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は４つの数字を取り、それぞれRGBAチャンネルに対応します。modulateColorの説明は[Psychtoolboxでアルファブレンド（色の混ぜ合わせ）](https://qiita.com/kurokida/items/105c2878462917c99324#modulatecolor)にも書いていますので、よろしければご覧ください。

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

> 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] となります。[ヘルプ](http://psychtoolbox.org/docs/CreateSinglePassImageProcessingShader)に書かれている通りです。

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

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

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

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

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

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

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

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

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

補足ですが、

> 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でも指定可能です。例えば次のコードです。

```matlab
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`をぜひご覧ください。きっとびっくりしますよ！