LoginSignup
3
0

More than 3 years have passed since last update.

Psychtoolboxでアルファブレンド(色の混ぜ合わせ)

Last updated at Posted at 2020-02-19

Psychtoolbox(PTB)は、Matlabで心理物理学実験を行うためのツールボックスです。公式サイトはこちら。ちなみに私はPsychtoolboxをがんばるの管理人ですが、解説記事を書くのにQiitaのほうが使い勝手がよいので、これからはこちらで情報を共有していこうかと考え中です。
どうぞよろしくお願いいたします。

アルファブレンドをマスターしたい

アルファブレンド(アルファブレンディング)は2つの色を混ぜ合わせるときの方法を指定したもので、基本的な考え方は難しくはありませんが、いろいろな組み合わせがあるため分かりにくいです。私も毎度混乱するので、備忘録として残しておきます。
内容に誤りがあればご指摘ください。なお、PTBでのアルファブレンドはOpenGLのそれとほぼ同じです。

PTBでの色表現

まずはPTBでの色表現から。基本的には次の3通りです。

Pattern1
% スカラーを指定
black = 0;
grey = 0.5;
white = 1;
Pattern2
% RGBチャンネルを指定
red = [1 0 0];
green = [0 1 0];
blue = [0 0 1];
Pattern3
% RGBに加えて、アルファチャンネル(透明度)を指定
red_with_transparency = [1 0 0 0.2]; % alpha = 0.2
green_with_transparency = [0 1 0 0.4]; % alpha = 0.4

PTBでは、Pattern1や2のように透明度(アルファ、A)を明記しない場合は、透明度=1.0となります。この場合、描画先を完全に不透明な色で描画(上書き)します。描画先の色が、描画元の色で入れ替わると思ってもよいです。

従来は0から255の数値を使って色を指定する方法が主流でしたが、最近は0から1に正規化された数値を使うことが推奨されています。PsychDefaultSetup(2);を呼び出すことで、0から1の数値表現が有効になります。

アルファブレンドとは?

模式図を示します。

alphablend.jpg

画面になにかを描画しようとしたとき、そこには描画元の色情報と描画先の色情報があります。
ここでは便宜的に、描画元の色情報を (R1, G1, B1, A1)、描画先の色情報を (R2, G2, B2, A2)とします。RGBは赤緑青を表し、Aは透明度(アルファ)を表します。RGBAはチャンネルとも呼ばれます。

英語のマニュアルを読むときの参考までに、描画元はソース(SOURCE, SRC)、描画先はデスティネーション(DESTINATION, DST, あるいはTARGET)と表されることが多いです。上の模式図でソースアルファと書いていますが、それは描画元のアルファを意味します。

画面に表示される最終的な色は、次の式で計算されます。

最終的な色 = 描画元の色 x a1 + 描画先の色 x a2

つまり、描画元の色を何倍かして、同様に描画先の色も何倍かして、それらを足し合わせるだけです。
ここで分かりにくいのは、a1=A1(描画元のアルファ)、あるいは a2=A2(描画先のアルファ)とは限らないという点です。 a1やa2の組み合わせがいろいろあるので分かりにくいんです。

しかし逆に言えば、アルファブレンドとは、a1とa2をどのように指定するか、ということに過ぎません。a1とa2の組み合わせによって、画面に提示される画像の見た目が劇的に変化します。

よく使われるアルファブレンド

よく使われる、一番人気のブレンディングは次の通りです。

最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)
 # A1は描画元のアルファ

PTBでこれを実現するには、

Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

です。引数の3番目(GL_SRC_ALPHA)が描画元に適用される係数(上の説明でのa1)で、引数の4番目(GL_ONE_MINUS_SRC_ALPHA)が描画先に適用される係数(a2)です。

例えば描画元のアルファが0.3であれば、描画元のRGBのそれぞれに0.3をかけて、描画先のRGBのそれぞれに0.7(=1-0.3)をかけて足し合わせます。

このとき、描画先のアルファ(A2)はまったく使われない点に注意をしてください。つまりA2=0.5だろうが、A2=1.0だろうが、BlendFunctionが上のように設定されているかぎり、ブレンディングの結果は変わりません。

では、具体的な数値で見てみましょう。

描画元の色情報 RGBA = [1 1 0 0.5]
描画先の色情報 RGBA = [0.5 0.2 0.4 1]

とします。最終的な色は、

最終的な R = 1 x 0.5 + 0.5 x (1 - 0.5) = 0.75
最終的な G = 1 x 0.5 + 0.2 x (1 - 0.5) = 0.6
最終的な B = 0 x 0.5 + 0.4 x (1 - 0.5) = 0.2
最終的な A = 0.5 x 0.5 + 1 x (1 - 0.5) = 0.75

RGBだけでなく、A(アルファ、透明度)にも計算式が適用される点に注意してください。なお、この最終的な色情報は、描画先の新しい色情報と言い換えることもできます。

実際に確認

チュートリアルで公開されているコードを使って説明します。

次のコードは、画面の左上に白い正方形を提示します。ブレンディングの機能はまだ使っていません。グレーの背景に、白い正方形を提示するだけです。

% 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]);

imageMatrix = ones(80); % 輝度の強度のみ。80 x 80のすべてのピクセルに1が入っているイメージです。
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix); % 描画元となるテクスチャーを作成
dstRect = Screen('Rect', textureIndex); % テクスチャーの四角領域

% 正方形1
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 150));

% ここにコードを加えていく 

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;

描画元と描画先は以下の図のようになります。

white_rect_alpha1.jpg

補足説明です。

描画元について

imageMatrix = ones(80);

上述の Pattern1のように、80x80のすべてのピクセルのRGBAチャンネルに1(白色)が与えられていると思ってください。

textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);

imageMatrixからテクスチャーを作成します。多少語弊がありますが、数値の情報から画像(視覚刺激)を作成する作業です。このテクスチャーはアルファブレンドされない限り白色で、以下の例で何度も(正方形10まで)使い回されます。

描画先について

描画先となる背景色はgrey = 0.5です。RGBA = [0.5 0.5 0.5 1.0]と同じです。透明度アルファは特に指定していないのでデフォルトの1.0です。

DrawTextureを使っていますが、このヘルプは、Matlabのコマンドウィンドウで、

Screen DrawTexture?

とすると表示されます。こちらの説明も役に立つかもしれません。

CenterRectOnPoint(dstRect, 100, 150)

で正方形を描画する位置を指定しています。座標(100, 150) を中心として正方形が描画されます。正方形の幅と高さは80ピクセルです。CenterRectOnPointの詳細は、Matlabのコマンドウィンドウでhelp CenterRectOnPointとすると確認できます。

% ここにコードを加えていく 

という箇所があるかと思いますが、以下で説明することはすべてここに書き加えていってください。

アルファブレンドをしてみよう

% 正方形2
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 150));

この2行を加えてみてください。ブレンドの方法は一番人気の、

最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)

ですね。

BlendFunctionを指定したので、ブレンディングされるかな・・・と思いきや、白い正方形がふたつ並んで表示されると思います。(左側の正方形1はアルファブレンドを指定していない正方形。右側の正方形2はアルファブレンドを指定した正方形)

ただし、ブレンドされていないわけではありません。

正方形2については、描画元の色情報に描画元のアルファ(ソースアルファ)をかける設定(GL_SRC_ALPHA)になっていますが、描画元のアルファは具体的にいくつでしょう? 

答えは1.0です。

さらに描画先の係数は、GL_ONE_MINUS_SRC_ALPHAなので、1から1を引いてゼロ。つまり描画先の色情報にゼロがかけられます。

最終的な色 = 描画元の色 x A1 + 描画先の色 x (1 - A1)

で、A1=1.0ということですから、

最終的な色 = 描画元の色

となりますね。その結果、ブレンドされていないように見えるのです。

globalAlpha

ブレンドの結果が見えるように正方形3を表示してみましょう。

imageMatrixに適切なアルファチャンネルを指定してもよいのですが、ここでは、DrawTextureのglobalAlphaを使ってみます。すでに設定されているアルファ値を、globalAlphaで更新することができます。globalAlphaはDrawTextureの8番目の引数です。

% 正方形3 globalAlphaを使って、ソースアルファを0.5にしてみる。
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 150), [], [], 0.5);

上のコードを加えると3つ目の正方形が表示され、その色が明るいグレーになっていると思います。これは描画元の白色と描画先(背景色)のgreyがブレンドされた結果です。どのようにブレンドされるかというと、

描画元(白色 = 1.0) x 0.5 + 描画先(グレー = 0.5) x 0.5 = 0.75

です。アルファチャンネルもブレンドされることに注意してください。RGBA = [0.75 0.75 0.75 0.75]です。やっとブレンド結果を見ることができました!

modulateColor

globalAlphaではなく、modulateColorを使う方法もあります。modulateColorはDrawTextureの9番目の引数です。ちなみに、globalAlphaとmodulateColorの両方を指定すると、globalAlphaが無視されます。

modulateColorを指定すると、描画元のRGBAチャンネルの数値をmodulate(変調)、分かりやすく言えば拡大・縮小することができます。例えば、

SRC_RGBA = [0.8 0.6 0.4 1]; % 描画元の色情報
modulateColor = [1 0.5 0.25 0.5];

の場合、

R = 0.8 x 1 = 0.8
G = 0.6 x 0.5 = 0.3
B = 0.4 x 0.25 = 0.1
A = 1 x 0.5 = 0.5

となります。またこれらの数値はあくまで描画元の色情報であり、この色情報に対してさらにアルファブレンドがほどこされます。

また、以下の説明ではScreen('BlendFunction')を記述していませんが、記述していないときは直前のブレンド設定が引き継がれます。いまのところ、

Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

の設定が引き継がれます。

modulateColorの動作確認をしてみましょう。次のコードを追加して正方形4を表示します。

% 正方形4
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 400, 150), [], [], [], [1 1 1 0.5]); 

正方形4の見た目は正方形3と同じになるはずです。上の例では、

modulateColor = [1 1 1 0.5]

となっています。描画元の色情報はRGBA = [1 1 1 1]です。これに対してmodulateColorが適用されます。RGBチャンネルに変化はありません。アルファだけが0.5倍されて、1 x 0.5 = 0.5 になります。

では、次のコードはどんな正方形を描画するでしょうか。

% 正方形5
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 500, 150), [], [], [], [1 0 0 1]);

modulateColor = [1 0 0 1]となっています。RとAについては描画元の情報をそのまま使いますが、GとBについてはゼロになります。

最終的なRとA = 描画元の色 x A1 + 描画先の色 x (1 - A1) = 1 x 1 + 0.5 x 0 = 1
最終的なGとB = 描画先の色 x (1 - A1) = 0

正方形5は真っ赤な正方形になります。


正方形6は正方形5とほぼ同じですが、ソースアルファ(描画元のアルファ)が0.5倍されます。描画元のGとBについてはmodulateColorによってゼロになります。

% 正方形6
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 600, 150), [], [], [], [1 0 0 0.5]);

最終的なR = 描画元(1) x 0.5 + 描画先(0.5) x (1 - 0.5) = 0.75
最終的なG = 描画先(0.5) x (1 - 0.5) = 0.25
最終的なB = 描画先(0.5) x (1 - 0.5) = 0.25
最終的なA = 描画元(0.5) x 0.5 + 描画先(1) x (1 - 0.5) = 0.75

となり、ややくすんだ感じの赤い正方形が表示されます。

GL_ONE と GL_ZERO

ブレンドの方法を変えてみましょう。

% 正方形7
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 300), [], [], [], [1 0 0 0.5]);

まずは

描画元の色情報 RGBA = [1 1 1 1]

であることを思い出してください。

modulateColor = [1 0 0 0.5]ですから、描画元の色情報が次のようにmodulateされます。

描画元の色情報 RGBA = [1 0 0 0.5]

さらに、描画元の係数がGL_SRC_ALPHA、描画先の係数がGL_ONEになっていることから、次のような結果になります。

最終的なR = 描画元(1.0) x 0.5 + 描画先(0.5) x GL_ONE(1.0) = 1.0
最終的なG = 描画先(0.5) x GL_ONE(1.0) = 0.5
最終的なG = 描画先(0.5) x GL_ONE(1.0) = 0.5
最終的なA = 描画元(0.5) x 0.5 + 描画先(1.0) x GL_ONE(1.0) = 1.0(1.0以上にはならない)

次に、GL_ZEROを使ってみます。

% 正方形8
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 300), [], [], [], [1 0 0 0.5]);

正方形7とほぼ同じですが、描画先の係数がGL_ZEROであるため、描画先の色情報はすべて失われます。その結果、

最終的なR = 描画元(1.0) x ソースアルファ(0.5) = 0.5
最終的なG = 0
最終的なG = 0
最終的なA = 描画元(0.5) x ソースアルファ(0.5) = 0.25

となります。

colorMaskNew

Screen('BlendFunction')の5番目の引数で、colorMaskNewを指定することができます。詳しい説明はこちら

簡単に言えば、RGBAのそれぞれにおいて、ブレンドを有効にしたり無効にしたりできます。例えば、

colorMaskNew = [1 0 0 1]

の場合は、RとAチャンネルについてのみブレンドを実行します。基本的に0または1を指定します。0.5などを指定しても意味がありません。0が指定されたときは、描画先の色情報がそのまま使われることを意味します。

例を見てみましょう。

% 正方形9 Blueチャンネルだけをブレンドする。その他のチャンネルは描画先のものを維持
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO, [0 0 1 0]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 300));

colorMaskNew = [0 0 1 0]なので、B以外のRGAの3つのチャンネルについては描画先の色情報(グレー)がそのまま使われます。

Bについてのみ、次のようなブレンドがなされます。

最終的なB = 描画元 x GL_SRC_ALPHA + 描画先 x GL_ZERO = 1.0

描画元 = 1.0、GL_SRC_ALPHA = 1.0であることに注意してください。

またこれも大切な点ですが、colorMaskNewはいったん設定すると、その後もずっと引き継がれます。

例えば、正方形9の下に以下のコードを加えてみます。

% 正方形10 colorMaskNewが引き継がれることに注意
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 400, 300), [], [], [], [0, 0, 1, 0.7]);

まず、modulateColor = [0, 0, 1, 0.7]によって描画元の色情報が次のようになります。

描画元のRGBA = [0 0 1 0.7]

そして、colorMaskNewの設定が引き継がれているため、Bチャンネルのみにブレンドが適用されます。

最終的なB = 描画元(1.0) x GL_SRC_ALPHA(0.7) + 描画先 x GL_ZERO = 0.7

となります。RGAについては描画先の色情報がそのまま使われます。

アルファチャンネルを設定する

いままでの例では、透明度(アルファ)はglobalAlphaやmodulateColorを使って指定していました。この方法以外にも、単純にアルファチャンネルを設定する方法があります。

imageMatrix = ones(80, 80, 3); % RGBチャンネル
imageMatrix(:,:,4) = 0.5; % A(alpha)チャンネル
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);

このように設定して、次の正方形11は何色になるでしょうか?

% 正方形11
Screen('BlendFunction', windowPtr, GL_ONE, GL_ZERO, [1 1 1 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 100, 450));

これはちょっと引っかけ問題みたいになっていますが、白色の正方形が表示されます。ブレンドされていないように見えますが、

Screen('BlendFunction', windowPtr, GL_ONE, GL_ZERO, [1 1 1 1]);

となっていて、係数にアルファがまったく含まれていないですね。描画元の係数にGL_ONE、描画先の係数にGL_ZEROを指定すると、描画先の色情報が描画元の色情報と単純に入れ替わります。

気を取り直して、次のようにしてみましょう。

% 正方形12
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 200, 450));

globalAlphaもmodulateColorも指定していませんが、ブレンドされた正方形が表示されたと思います。
正方形の色は、RGBAのいずれも次のようになります。

RGBA = 描画元(1.0)x0.5 + 描画先(0.5)x0.5 = 0.75

GL_DST_ALPHA

ここまでの例では描画先(デスティネーション, DST)のアルファをまったく使っていませんでした。
最後は描画先のアルファを使ってみましょう。正方形13と14のふたつの正方形を描画しますが、14は13の右下に描画され、重複する部分が存在します。

% 正方形13
imageMatrix = ones(80, 80, 3); % RGBチャンネル
imageMatrix(:,:,4) = 0.4; % A(alpha)チャンネル
textureIndex=Screen('MakeTexture', windowPtr, imageMatrix);
Screen('BlendFunction', windowPtr, GL_SRC_ALPHA, GL_ZERO, [1 0 0 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 300, 450));
% 正方形14
Screen('BlendFunction', windowPtr, GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, [1 1 1 1]);
Screen('DrawTexture', windowPtr, textureIndex, [], CenterRectOnPoint(dstRect, 320, 470));

やや複雑な例ですが、順番に考えてみましょう。

正方形13の描画元のRGBA = [1 1 1 0.4]
正方形13の描画先のRGBA = [0.5 0.5 0.5 1]

正方形13については、colorMaskNew = [1 0 0 1]としているため、RとAチャンネルのみがブレンドされます。描画元の係数はGL_SRC_ALPHAで、描画先の係数はGL_ZEROです。

グレーの背景とブレンドされた正方形13・・・①
R = 描画元(1) x 0.4 = 0.4
G(ブレンドされない) = 0.5
B(ブレンドされない) = 0.5
A = 描画元(0.4) x 0.4 = 0.16

続いて正方形14についてですが、14の一部は13の上に描画されます。
まずは重複しない部分について考えてみましょう。

正方形14の描画元のRGBA = [1 1 1 0.4]
重複しない描画先のRGBA = [0.5 0.5 0.5 1]

正方形14については、colorMaskNew = [1 1 1 1]としているため、すべてのチャンネルがブレンドされます。描画元の係数はGL_DST_ALPHAで、描画先の係数はGL_ONE_MINUS_DST_ALPHAです。いずれも初めての係数ですね。でも考え方は同じです。描画先のアルファが1であることに注意して計算します。

重複していない部分について
正方形14のRGB = 描画元(1) x 1 + 0.5 x (1 - 1) = 1
正方形14のA = 描画元(0.4) x 1 + 0.5 x (1 - 1) = 0.4

つまり、重複していない部分は白色になります。

そして重複部分については、描画先が背景とブレンドされた後の正方形13であることに留意します。

正方形14の描画元のRGBA = [1 1 1 0.4]
重複している描画先のRGBA = [0.4 0.5 0.5 0.16]・・・上の①

GL_DST_ALPHA = 0.16ですから、

最終的なR = 描画元(1) x 0.16 + 0.4 x (1 - 0.16) = 0.496
最終的なG = 描画元(1) x 0.16 + 0.5 x (1 - 0.16) = 0.58
最終的なB = 描画元(1) x 0.16 + 0.5 x (1 - 0.16) = 0.58
最終的なA = 描画元(0.4) x 0.16 + 0.16 x (1 - 0.16) = 0.1984

まとめ

  • 描画元の色情報と描画先の色情報を確認
  • 描画元にも、描画先にも、色情報のRGBと透明度アルファAが存在
  • 色情報にかけられる係数はなにか(描画元のアルファ? 描画先のアルファ? あるいは1からアルファを引いたもの?)
  • 描画元の色情報と描画先の色情報のそれぞれに係数をかけて、足し合わせたものが最終的な色
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0