例題20-2:PsychToolboxからPsychoPyへ

A: チュートリアルワークショップフォロー編、その2はチュートリアルで扱った「PsychToolboxのサンプルスクリプトをPsychoPyで書き直す」の内容を紹介します。Sがいないと埒があかないのでSを捜索して連れてきました。

S: あー、えーと。よろしくお願いします。あの、いつものB君はどこへ?

A: B君は なぜか いなくなってしまったので今回は二人で。さて、ではお題のPsychToolboxのサンプルスクリプトを見ていただきましょうか。目標のスクリプトはPsychToolboxのPsychDemosフォルダに含まれるGratingDemo.m。ちょっと長いですが全体を示します。非常にシンプルなデモで、ただ単にGratingを表示するだけです。

function GratingDemo
% GratingDemo
%
% Displays a stationary grating.  See also DriftDemo, DriftDemo2, DriftDemo3 and DriftWaitDemo.

% ---------- Program History ----------

% 07/01/1999 dgp Added arbitrary orientation.
% 12/10/2001 awi Added font conditional.
% 02/21/2002 dgp Mentioned DriftDemo.
% 04/03/2002 awi Merged OS9 and Win versions, which had fallen out of sync.
% 04/13/2002 dgp Used Arial, eliminating need for conditional.
% 07/15/2003 dgp Added comments explaining f and lambda.
% 08/16/2006 rhh Added user-friendly parameters, such as tiltInDegrees,
%                pixelsPerPeriod, periodsCoveredByOneStandardDeviation and widthOfGrid.
% 08/18/2006 rhh Expanded comments and created comment sections.
% 10/04/2006 dhb Minimize warnings.
% 10/11/2006 dhb Use maximum available screen.
% 10/14/2006 dhb Save and restore altered prefs, more extensive comments for them
% 07/12/2006 prf Changed method of rotating the grating

% ---------- Parameter Setup ----------
% Initializes the program's parameters.

% Prevents MATLAB from reprinting the source code when the program runs.
echo off

% *** To rotate the grating, set tiltInDegrees to a new value.
tiltInDegrees = 7; % The tilt of the grating in degrees.
tiltInRadians = tiltInDegrees * pi / 180; % The tilt of the grating in radians.

% *** To lengthen the period of the grating, increase pixelsPerPeriod.
pixelsPerPeriod = 33; % How many pixels will each period/cycle occupy?
spatialFrequency = 1 / pixelsPerPeriod; % How many periods/cycles are there in a pixel?
radiansPerPixel = spatialFrequency * (2 * pi); % = (periods per pixel) * (2 pi radians per period)

% *** To enlarge the gaussian mask, increase periodsCoveredByOneStandardDeviation.
% The parameter "periodsCoveredByOneStandardDeviation" is approximately
% equal to
% the number of periods/cycles covered by one standard deviation of the radius of
% the gaussian mask.
periodsCoveredByOneStandardDeviation = 1.5;
% The parameter "gaussianSpaceConstant" is approximately equal to the
% number of pixels covered by one standard deviation of the radius of
% the gaussian mask.
gaussianSpaceConstant = periodsCoveredByOneStandardDeviation  * pixelsPerPeriod;

% *** If the grating is clipped on the sides, increase widthOfGrid.
widthOfGrid = 400;
halfWidthOfGrid = widthOfGrid / 2;
widthArray = (-halfWidthOfGrid) : halfWidthOfGrid;  % widthArray is used in creating the meshgrid.


% For an explanation of the try-catch block, see the section "Error Handling"
% at the end of this document.
try

    % ---------- Window Setup ----------
    % Opens a window.

    % Screen is able to do a lot of configuration and performance checks on
    % open, and will print out a fair amount of detailed information when
    % it does.  These commands supress that checking behavior and just let
    % the demo go straight into action.  See ScreenTest for an example of
    % how to do detailed checking.
    oldVisualDebugLevel = Screen('Preference', 'VisualDebugLevel', 3);
    oldSupressAllWarnings = Screen('Preference', 'SuppressAllWarnings', 1);

    % Find out how many screens and use largest screen number.
    whichScreen = max(Screen('Screens'));

    % Hides the mouse cursor
    HideCursor;

    % Opens a graphics window on the main monitor (screen 0).  If you have
    % multiple monitors connected to your computer, then you can specify
    % a different monitor by supplying a different number in the second
    % argument to OpenWindow, e.g. Screen('OpenWindow', 2).
    window = Screen('OpenWindow', whichScreen);


    % ---------- Color Setup ----------
    % Gets color values.

    % Retrieves color codes for black and white and gray.
    black = BlackIndex(window);  % Retrieves the CLUT color code for black.
    white = WhiteIndex(window);  % Retrieves the CLUT color code for white.
    gray = (black + white) / 2;  % Computes the CLUT color code for gray.
    if round(gray)==white
        gray=black;
    end

    % Taking the absolute value of the difference between white and gray will
    % help keep the grating consistent regardless of whether the CLUT color
    % code for white is less or greater than the CLUT color code for black.
    absoluteDifferenceBetweenWhiteAndGray = abs(white - gray);


    % ---------- Image Setup ----------
    % Stores the image in a two dimensional matrix.

    % Creates a two-dimensional square grid.  For each element i = i(x0, y0) of
    % the grid, x = x(x0, y0) corresponds to the x-coordinate of element "i"
    % and y = y(x0, y0) corresponds to the y-coordinate of element "i"
    [x y] = meshgrid(widthArray, widthArray);

    % Replaced original method of changing the orientation of the grating
    % (gradient = y - tan(tiltInRadians) .* x) with sine and cosine (adapted from DriftDemo).
    % Use of tangent was breakable because it is undefined for theta near pi/2 and the period
    % of the grating changed with change in theta.

    a=cos(tiltInRadians)*radiansPerPixel;
    b=sin(tiltInRadians)*radiansPerPixel;

    % Converts meshgrid into a sinusoidal grating, where elements
    % along a line with angle theta have the same value and where the
    % period of the sinusoid is equal to "pixelsPerPeriod" pixels.
    % Note that each entry of gratingMatrix varies between minus one and
    % one; -1 <= gratingMatrix(x0, y0)  <= 1
    gratingMatrix = sin(a*x+b*y);


    % Creates a circular Gaussian mask centered at the origin, where the number
    % of pixels covered by one standard deviation of the radius is
    % approximately equal to "gaussianSpaceConstant."
    % For more information on circular and elliptical Gaussian distributions, please see
    % http://mathworld.wolfram.com/GaussianFunction.html
    % Note that since each entry of circularGaussianMaskMatrix is "e"
    % raised to a negative exponent, each entry of
    % circularGaussianMaskMatrix is one over "e" raised to a positive
    % exponent, which is always between zero and one;
    % 0 < circularGaussianMaskMatrix(x0, y0) <= 1
    circularGaussianMaskMatrix = exp(-((x .^ 2) + (y .^ 2)) / (gaussianSpaceConstant ^ 2));

    % Since each entry of gratingMatrix varies between minus one and one and each entry of
    % circularGaussianMaskMatrix vary between zero and one, each entry of
    % imageMatrix varies between minus one and one.
    % -1 <= imageMatrix(x0, y0) <= 1
    imageMatrix = gratingMatrix .* circularGaussianMaskMatrix;

    % Since each entry of imageMatrix is a fraction between minus one and
    % one, multiplying imageMatrix by absoluteDifferenceBetweenWhiteAndGray
    % and adding the gray CLUT color code baseline
    % converts each entry of imageMatrix into a shade of gray:
    % if an entry of "m" is minus one, then the corresponding pixel is black;
    % if an entry of "m" is zero, then the corresponding pixel is gray;
    % if an entry of "m" is one, then the corresponding pixel is white.
    grayscaleImageMatrix = gray + absoluteDifferenceBetweenWhiteAndGray * imageMatrix;


    % ---------- Image Display ----------
    % Displays the image in the window.

    % Colors the entire window gray.
    Screen('FillRect', window, gray);

    % Writes the image to the window.
    Screen('PutImage', window, grayscaleImageMatrix);

    % Writes text to the window.
    currentTextRow = 0;
    Screen('DrawText', window, sprintf('black = %d, white = %d', black, white), 0, currentTextRow, black);
    currentTextRow = currentTextRow + 20;
    Screen('DrawText', window, 'Press any key to exit.', 0, currentTextRow, black);

    % Updates the screen to reflect our changes to the window.
    Screen('Flip', window);

    % Waits for the user to press a key.
    KbWait;

    % ---------- Window Cleanup ----------

    % Closes all windows.
    Screen('CloseAll');

    % Restores the mouse cursor.
    ShowCursor;

    % Restore preferences
    Screen('Preference', 'VisualDebugLevel', oldVisualDebugLevel);
    Screen('Preference', 'SuppressAllWarnings', oldSupressAllWarnings);
catch

    % ---------- Error Handling ----------
    % If there is an error in our code, we will end up here.

    % The try-catch block ensures that Screen will restore the display and return us
    % to the MATLAB prompt even if there is an error in our code.  Without this try-catch
    % block, Screen could still have control of the display when MATLAB throws an error, in
    % which case the user will not see the MATLAB prompt.
    Screen('CloseAll');

    % Restores the mouse cursor.
    ShowCursor;

    % Restore preferences
    Screen('Preference', 'VisualDebugLevel', oldVisualDebugLevel);
    Screen('Preference', 'SuppressAllWarnings', oldSupressAllWarnings);

    % We throw the error again so the user sees the error description.
    psychrethrow(psychlasterror);

end

A: ちょっと単純すぎると思うんだけど、なんでこのサンプルを?

S: 単純すぎるとは私も思ったんやけどね、チュートリアルワークショップの時間内で収めるためにはこのくらいが限界やった。

A: なるほど。

S: あと、マルチスクリーンは考慮しない、psychopy.visual.GratingStimを使えば一発で書けるんやけど敢えてPsychToolboxと同じ方法で書くという条件もつけた。チュートリアルの時は言わんかったんやけどMatlabのtry-cacthにも触れんかった。

A: ははっ、GratingStim使ったら元も子もないからな。んじゃ、順番に見ていきますか。まずはウィンドウのセットアップから。定数の定義はそこだけ先にやってもわけがわからないので適時触れます。

S: まずPsychToolboxのソースの一部を抜き出したものを示し、続いてその部分で行われている処理をPsychoPyで書き直したコードを示します。ポイントはPsychoPyのコードのコメントに書いてありますが、PsychoPyのコードを示した後でAと私で少し補足します。

ウィンドウのセットアップ

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % ---------- Window Setup ----------
    % Opens a window.

    % Screen is able to do a lot of configuration and performance checks on
    % open, and will print out a fair amount of detailed information when
    % it does.  These commands supress that checking behavior and just let
    % the demo go straight into action.  See ScreenTest for an example of
    % how to do detailed checking.
    oldVisualDebugLevel = Screen('Preference', 'VisualDebugLevel', 3);
    oldSupressAllWarnings = Screen('Preference', 'SuppressAllWarnings', 1);

    % Find out how many screens and use largest screen number.
    whichScreen = max(Screen('Screens'));

    % Hides the mouse cursor
    HideCursor;

    % Opens a graphics window on the main monitor (screen 0).  If you have
    % multiple monitors connected to your computer, then you can specify
    % a different monitor by supplying a different number in the second
    % argument to OpenWindow, e.g. Screen('OpenWindow', 2).
    window = Screen('OpenWindow', whichScreen);
########## PsychoPy ##########
#psychopy(というかpython)では効率化のために
#必要最小限のモジュールしか読み込まないので
#明示的に使用するモジュールを読み込む必要がある。
#今回のサンプルでは以下のモジュールを使用する。
import psychopy.visual
import psychopy.core
import Image
import numpy

#プログラム内で使用する定数の宣言はnumpyをimportした後の方が容易なので
#この位置で行うのがよい。

#psychopyではpsychopy.visual.Windowでウィンドウを作成する
#・今回のサンプルではマルチスクリーンは考慮しない。
#・fullscrはフルスクリーンを指定している。
#・unitsは使用する単位の指定。このサンプルではPsychToolboxに合わせて
#  pixを使用するがdegなども使える。
win = psychopy.visual.Window(fullscr=True, units='pix')

#マウスカーソルを隠す。標準設定で表示されないので不要だが、
#PsychToolboxとの対応をとるということで一応書いておいた。
win.setMouseVisible(False)

A: まあだいたい重要なポイントはコメントに書いてあるわけだけど、何か補足ある?

S: そやな、 PsychToolboxはScreen()という関数、というかプロシージャがすべての操作の起点になるんやけど、PsychoPyではオブジェクトが操作の起点になる 。これはMatlabとPythonの言語仕様の違いによるもので、おそらくここがPsychToolboxとPythonの最大の違い。PsychToolboxからPsychoPy引っ越すにはオブジェクトという考え方に慣れなあかん。

A: って言ってもとりあえずは「こう書く」と覚えておけば大丈夫だと思うけどねー。それよりもゼロオリジンとか整数の除算の方が引っかかると思う。

S: まーね。でもそれは後で触れるから。簡単な実験プログラムを書くくらいなら大した差ではないかも知れんけど、自分で関数を定義したりある程度の規模のアプリケーションを開発したりしようとするとこの差が一番しんどい。

A: ほとんどの人はそこまで必要ないでしょ。ほんじゃ、次。

灰色を表す値を求め、白との差を計算する

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % ---------- Color Setup ----------
    % Gets color values.

    % Retrieves color codes for black and white and gray.
    black = BlackIndex(window);  % Retrieves the CLUT color code for black.
    white = WhiteIndex(window);  % Retrieves the CLUT color code for white.
    gray = (black + white) / 2;  % Computes the CLUT color code for gray.
    if round(gray)==white
        gray=black;
    end

    % Taking the absolute value of the difference between white and gray will
    % help keep the grating consistent regardless of whether the CLUT color
    % code for white is less or greater than the CLUT color code for black.
    absoluteDifferenceBetweenWhiteAndGray = abs(white - gray);
########## PsychoPy ##########
#psychopyでは常に白が1.0、灰色が0.0、黒が-1.0
#ただし今回はPILを経由させるため最終的には0(黒)-255(白)に変換される
#なお、if文はpythonでは
#if round(gray)==white:
#    gray=black
#のようにifの行の最後にコロンを付け、if文の範囲は字下げで示す
#Matlabのif文のendに相当するものは不要

black = -1.0
white = 1.0
gray=0.0
absoluteDifferenceBetweenWhiteAndGray = abs(white-gray)

A: 色の設定。さて、ここで補足は?

S: 私はPsychToolboxについては黒が0、白が255になる環境でしか使ったことがないやけど、この処理を見る限りそうならない環境があって、いろんな環境に対応させるためにはBlackIndex()、WhiteIndex()で値を得ないといけないみたいやね。PsychoPyは少なくとも私が知る限り必ず黒が-1.0、白が1.0になる。それだけかな。

A: そ。んじゃ、次へ。

グレーティングを準備する

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % ---------- Image Setup ----------
    % Stores the image in a two dimensional matrix.

    % Creates a two-dimensional square grid.  For each element i = i(x0, y0) of
    % the grid, x = x(x0, y0) corresponds to the x-coordinate of element "i"
    % and y = y(x0, y0) corresponds to the y-coordinate of element "i"
    [x y] = meshgrid(widthArray, widthArray);

    % Replaced original method of changing the orientation of the grating
    % (gradient = y - tan(tiltInRadians) .* x) with sine and cosine (adapted from DriftDemo).
    % Use of tangent was breakable because it is undefined for theta near pi/2 and the period
    % of the grating changed with change in theta.

    a=cos(tiltInRadians)*radiansPerPixel;
    b=sin(tiltInRadians)*radiansPerPixel;

    % Converts meshgrid into a sinusoidal grating, where elements
    % along a line with angle theta have the same value and where the
    % period of the sinusoid is equal to "pixelsPerPeriod" pixels.
    % Note that each entry of gratingMatrix varies between minus one and
    % one; -1 <= gratingMatrix(x0, y0)  <= 1
    gratingMatrix = sin(a*x+b*y);


    % Creates a circular Gaussian mask centered at the origin, where the number
    % of pixels covered by one standard deviation of the radius is
    % approximately equal to "gaussianSpaceConstant."
    % For more information on circular and elliptical Gaussian distributions, please see
    % http://mathworld.wolfram.com/GaussianFunction.html
    % Note that since each entry of circularGaussianMaskMatrix is "e"
    % raised to a negative exponent, each entry of
    % circularGaussianMaskMatrix is one over "e" raised to a positive
    % exponent, which is always between zero and one;
    % 0 < circularGaussianMaskMatrix(x0, y0) <= 1
    circularGaussianMaskMatrix = exp(-((x .^ 2) + (y .^ 2)) / (gaussianSpaceConstant ^ 2));

    % Since each entry of gratingMatrix varies between minus one and one and each entry of
    % circularGaussianMaskMatrix vary between zero and one, each entry of
    % imageMatrix varies between minus one and one.
    % -1 <= imageMatrix(x0, y0) <= 1
    imageMatrix = gratingMatrix .* circularGaussianMaskMatrix;

    % Since each entry of imageMatrix is a fraction between minus one and
    % one, multiplying imageMatrix by absoluteDifferenceBetweenWhiteAndGray
    % and adding the gray CLUT color code baseline
    % converts each entry of imageMatrix into a shade of gray:
    % if an entry of "m" is minus one, then the corresponding pixel is black;
    % if an entry of "m" is zero, then the corresponding pixel is gray;
    % if an entry of "m" is one, then the corresponding pixel is white.
    grayscaleImageMatrix = gray + absoluteDifferenceBetweenWhiteAndGray * imageMatrix;
########## PsychoPy ##########
#numpyの機能を使えばほぼMatlabと同様に書ける。
#from numpy import * とすればいちいちsinやcosの前に"numpy."は不要
#meshgridの引数widthArrayだけはmatlabのように書けないのでnumpy.arange()を使用
widthArray = numpy.arange(-halfWidthOfGrid,halfWidthOfGrid)

x,y = numpy.meshgrid(widthArray, widthArray)

a = numpy.cos(tiltInRadians)*radiansPerPixel;
b = numpy.sin(tiltInRadians)*radiansPerPixel;

gratingMatrix = numpy.sin(a*x+b*y);

#matlabのx.^2は numpyではx**2と書く。
circularGaussianMaskMatrix =
        numpy.exp(-((x**2)+(y**2))/(gaussianSpaceConstant**2))

#matlabのx.*yは numpyではx*yと書く。
imageMatrix = gratingMatrix * circularGaussianMaskMatrix

#これは書き換えなくてもそのままで大丈夫
grayscaleImageMatrix
         = gray + absoluteDifferenceBetweenWhiteAndGray * imageMatrix

A: PsychToolboxのソースの分量が多く見えますが、ほとんどコメントなのでコードはほんの数行です。コメントを省略してもよかったんですが敢えてそのままで。ここで補足は?

S: この辺りの行列演算の書き換えがどうなるかを示したかったのもGratingDemo.mをお題に選んだ理由。で、ごらんのとおり演算子の違いなどがあるんやけど、だいたい同じような感じで書き換えることができる。これはnumpy、scipyの機能に依存する部分が大きい。コメントにもあるようにfrom numpy import *とすればnumpy.meshgrid()ではなくmeshgrid()と書けるので、さらに違和感は少なくなる。もっとも、matlabに出来るだけ近づけたいのであればnumpyではなくてfrom pylab import *の方がええと思うけど。

A: この講座ではどのモジュールからimportされているのかはっきりさせるためにfrom ほげほげ import *の形は使わないことにしてるけどね( 例題1-4 参照)。演算子の違いはいろいろあるけど、一番でかいのは[ ]かな?

S: そやね。個人的にはMatlabの文法はあまり好きになれんのやけど、その一つが( )。Matlabでは( )が関数の呼び出しにもベクトルの要素の取得にも使われるから、他人が書いたプログラムを読んでいて普段自分が使わない関数が出てくると、ぱっと見ぃそれが関数の呼び出しなのかベクトルの要素の取得なのかわからへん。

A: あるある。

%%% Matlab %%%
foo(3) %関数fooに第一引数に3を与えて呼び出し
bar(3) %変数barに格納されたベクトルの3番目の要素を得る
### python ###
foo(3) #関数fooに第一引数に3を与えて呼び出し
bar[3] #変数barに格納されたベクトルの3番目の要素を得る

S: 行列演算はだいたい同じように書けるといったけど、行列の初期化に関してはMatlabな人がストレスを感じそうな大きな違いがひとつある。 ; の用法だ。Matlabでは要素を指定して行列を初期化するときに、 ; を使って行を区切ることができる。numpy.arrayではこの記法が使えない。numpy.matrixを使うと ; 記法が使えるが、引数を文字列として渡さないといけないし、numpyの多くの関数がnumpy.array型を利用するのでnumpy.array型の方が便利だ。

%%% Matlab %%%
% ;記法の例
data = [1,2,3;4,5,6]
### python ###
# ;記法は使えないので多重リストを使う必要がある
data = numpy.array([1,2,3],[4,5,6]])

# matrixなら;記法が使えるが文字列として引数を渡す必要がある
data = numpy.matrix("1,2,3;4,5,6")

A: ストレスっちゅーなら ; よりもゼロオリジンの方がよっぽどストレスなんじゃないのか?

S: おっと、確かにそうやね。 Matlabではベクトルの最初の要素にアクセスする時にv(1)と書くけど、pythonではv[0]と書く 。それを言ったら pythonのスライスの書き方もMtalabから引っ越してきた人が戸惑う ところやね。pythonのスライスについては 例題1-2 を参考にしてほしい。

%%% Matlab %%%
data = [1,2,3,4,5,6,7,8,9,10]
subdata = data(2:4) %subdataの値は[2,3,4]

data = [1,2,3,4,5,6,7,8,9,10]
subdata = data(5:end) %subdataの値は[5,6,7,8,9,10]
### python ###
data = numpy.array([1,2,3,4,5,6,7,8,9,10])
subdata = data[2:4] #subdataの値は [3,4]

data = numpy.array([1,2,3,4,5,6,7,8,9,10])
subdata = data[5:] #subdataの値は[6,7,8,9,10]

S: ああ、この例を書いて思い出したが、まだ違いがあるなあ。Matlabは等差数列を得るのにスライスとよく似た :記法が使えるが、numpyにはこれがないのでnumpy.arange()などを使う必要がある。numpy.arange()では数列の終わりを示す値(第2引数)自体は数列に含まれないのが実にややこしい。こいつなんかはMatlabの方が直観的やと思う。

%%% Matlab %%%
data = 0:2:10 %dataの値は[0,2,4,6,8,10]
### python ###
data = numpy.arange(0,10,2) #dataの値は[0,2,4,6,8]
data = numpy.arange(0,11,2) #10まで値が必要なら例えばこうする

A: おいSよ。言い出したらキリがないぞ。Matlabとの文法の違いは機会を改めたほうがいいんじゃないか。

S: んー。Matlabからpythonへの移行は楽だと言いたかったんやけど、藪蛇やったかなあ。最近またOctaveを使うようになって、pythonとの両立に特に不自由を感じてなかったんやけど、結構違うもんやな…。

A: ま、 C言語やVisualBasicなんかと比べるとMatlabとPython(NumPy)の行列操作がかなり似ているのは間違いない ので、それがわかってもらえりゃいいんじゃないか。次、行こうか。

行列データをウィンドウに描画する

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % ---------- Image Display ----------
    % Displays the image in the window.

    % Colors the entire window gray.
    Screen('FillRect', window, gray);

    % Writes the image to the window.
    Screen('PutImage', window, grayscaleImageMatrix);
########## PsychoPy ##########
#PsychoPyでは毎フレーム背景色で塗りつぶされるので明示的に塗りつぶさなくていい。
#PsychoPyでは行列を直接描画する機能がないのでまずPILで画像データに変換する。
img = Image.fromarray(numpy.uint8(grayscaleImageMatrix*128 + 128))

#画像を描画するクラスpsychopy.visual.ImageStimを用いる。
#ここまではアニメーションしない刺激であれば一度実行するだけで良い
grating = psychopy.visual.ImageStim(win=win, image=img)

#すでに作成されたImageStimオブジェクトを描画する時にはただdraw()すればよい。
grating.draw()

S: ここで一つ注意が必要なのはPsychoPyのコードの一行目。これは「色の設定」で黒を-1.0、白を1.0に設定したせいで必要になったコードで、「色の設定」で黒を0、白を255に設定していれば不要だ。さっき言った通りPsychoPyでは黒が-1.0、白が1.0なのだが、このサンプルでは行列データを画像データとして描画するためにPILを通している。PILでは黒が0、白が255なので-1.0から1.0の値が0から255になるように変換しているわけやね。

A: …Sよ。1.0だったら1.0×128+128だから256になるんじゃないのか。

S: あぅ、確かに…。+128じゃなくて+127の方がいいな。失敗失敗。

A: あとはコメントの通りでいいかな?

S: う、うん。さっきも言ったことだけれども、 PsychToolboxではScreen()関数が操作の起点になるけどPsychoPyではオブジェクトが起点となる という考え方がここにも出てくる。 刺激をどのように描画するかは個々の刺激が保持すべき情報なので、描画機能は刺激オブジェクトのメソッドを使う という考え方なので、draw()メソッドはwinのメソッドではなくgratingのメソッドというわけやね。

A: これもまずは理屈よりもこう書くんだという作法として覚えてもいいかも知れんね。次。

テキストをウィンドウに描画する

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % Writes text to the window.
    currentTextRow = 0;
    Screen('DrawText', window, sprintf('black = %d, white = %d', black, white), 0, currentTextRow, black);
    currentTextRow = currentTextRow + 20;
    Screen('DrawText', window, 'Press any key to exit.', 0, currentTextRow, black);
########## PsychoPy ##########
#テキストを描画するクラスpsychopy.visual.TextStimを用いる。
#改行文字が使えるので一回の描画で2行書ける。
#スクリーンの大きさはwin.sezeに格納されている。
#スクリーンの(0,0)が画面の中心、上と右が正の方向であることに注意。
#posは位置、alignHoriz, alignVertは文字列の左寄せ等の指定
hx,hy = win.size/2.0
message = psychopy.visual.TextStim(win=win,text=
    'black=%d, white=%d\nPress any key to exit.' % (black, white),
     pos = (-hx,hy), alignHoriz='left', alignVert='top')

#すでに作成されたTextStimオブジェクトを描画する時にはただdraw()すればよい。
message.draw()

S: ここでも考え方は行列データの描画と同じやねんけど、コメントにあるように PsychoPyではスクリーン座標系の原点(0,0)がスクリーン中心となり、上と右が正の方向であることに注意 せなあかん。PsychoPyのコードの一行目、hx,hy = win.size/2.0というのはウィンドウの左上の座標を求めるために縦横のサイズを2で割っている。上と右が正なんだから、(-hx, hy)がウィンドウ左上の角の座標っちゅうわけ。

A: PsychoPyのTextStimは複数行のテキストを出力できるのがありがたいね。ところで2ではなく2.0になってることは断っておかなくていいのか。

S: いま言おうと思っとったところよ。 2ではなく2.0となっているのは、pythonでは整数÷整数の除算は整数になるという問題があるのでそれを回避するため 。まあここではスクリーンサイズはまず2よりずっと大きい値なんで問題ないけど、3/2とか2/3とかした時にはえらいことになる。これは例題…何番やったかな。

A: 例題3-1 参照。

S: ありがと。あとポイントとしては、TextStimの引数に与えた座標がテキストのどの位置を指すかがPsychToolboxと異なるんで、PsychToolboxと合わせるようにalignHoriz, alignVertを指定してある。さて、これで描画が終わったんで後は表示。

スクリーンをフリップしてキー押しを待つ

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % Updates the screen to reflect our changes to the window.
    Screen('Flip', window);

    % Waits for the user to press a key.
    KbWait;
########## PsychoPy ##########
#Flipはpsychopy.visual.Windowのメソッド
win.flip()

#ただキー押しを待つにはpsychopy.event.waitKeysを使う。
#押されたキーを判定して反応時間を計測したりするときには
#psychopy.event.getKeys()を使うとよい。
psychopy.event.waitKeys()

S: オブジェクトが操作の起点になる、というさんざん言ってきた原則以外に特に言うことはないかな。刺激をバックバッファに描画してflipして表示する、という流れはPsychToolboxもPsychoPyも同じなんで、慣れてきたらPsychToolboxからPsychoPyへの移植はあんまし難しくない。

A: んじゃ、次。

S: あー、PsychToolboxの KbWait; っちゅー記法も個人的には好きやないね。関数そのものを指しているのか関数の呼び出しかが曖昧やんか。やっぱ引数がない関数でも( )は付けさせないと。

A: あー、はいはい。次行くぞ、次。

終了処理

%%%%%%%%%% PsychToolbox %%%%%%%%%%
    % ---------- Window Cleanup ----------

    % Closes all windows.
    Screen('CloseAll');

    % Restores the mouse cursor.
    ShowCursor;

    % Restore preferences
    Screen('Preference', 'VisualDebugLevel', oldVisualDebugLevel);
    Screen('Preference', 'SuppressAllWarnings', oldSupressAllWarnings);
########## PsychoPy ##########
#スクリーンを閉じるにはpsychopy.visual.Windowのclose()メソッドを使う。
#カーソルは自動的に表示されるので明示的に処理する必要はない。
win.close()

S: ここはもうコメントの通りとしか。

A: そうだな。

S: 最後に補足。元のGratingDemo.mではこれらのコード全体がtry-catchで囲まれているけど、これはpythonのtry-except( 例題5-4 )と同じだと思えばいい。フルスクリーン状態でプログラムがエラーで停止してしまったときに、catchの中でScreen('closeall')することでフルスクリーン状態から脱出できるようにしてるわけやね。もっとも、最近Octave上のPsychToolboxでちょっとしたToolboxを開発してるんやけどこのtry-catchがうまく働かへんでめっちゃイライラさせられてるんやけど。

A: Pythonでも実験全体をtry-exceptで囲むのは有効だろうね。不慮の事態で実験が停止してしまったときにそれまでのデータと再開のために必要な情報を出力してから終了するとか。

S: んー、でも不慮の事態で止まるっちゅーたら突然停電で全部落ちたとか、ブルースクリーンで死んだとか、try-exceptも通用せえへんようなのばっかりやしな。費用対効果っちゅーか、そこまで手間をかける価値があるかどうか。

A: ま、データの出力はともかく再開のために必要な情報まで全部出力して、その情報に基づいて実際に再開できるように実験スクリプトを組むのは面倒くさそうだね。一度テンプレートを作ってしまえばいいのかも知れんが。

S: ちょっと今はそこまで手が回らへんな。

A: だね。んじゃ、最後にPsychoPy向けに書き直したGratingDemo.mを全部つないだものと、PsychoPyのpsychopy.visual.GratingStimを使って書いたものを掲載して、いったんPsychToolboxの話題は終わりにしたいと思う。GratingStimを使った版はやたら短く見えるけどこれで全体でっせ。PsychToolboxの話題はご要望があればまた取り上げます。

S: あいよ。お付き合いどーもね。やっぱり一人でずっと話すよりしゃべりながらの方がやりやすいわぁ。

A: そりゃどーも。次回からは通常営業に戻る、かな?

########## GratingDemo.m PsychoPy移植版 ##########
#coding: utf-8
#GratingDemo.m PsychoPy移植版
import psychopy.visual
import psychopy.core
import Image
import numpy

tiltInDegrees = 7
tiltInRadians = tiltInDegrees * numpy.pi / 180

pixelsPerPeriod = 33
#次の行の計算はGratingDemo.mをそのままコピーすると整数÷整数なので0となってしまう。
#1を1.0に変更して小数の値が得られるようにする。つまづきやすい点なので注意。
spatialFrequency = 1.0 / pixelsPerPeriod
radiansPerPixel = spatialFrequency * (2 * numpy.pi)

periodsCoveredByOneStandardDeviation = 1.5
gaussianSpaceConstant = periodsCoveredByOneStandardDeviation  * pixelsPerPeriod

widthOfGrid = 400
halfWidthOfGrid = widthOfGrid / 2
widthArray = numpy.arange(-halfWidthOfGrid,halfWidthOfGrid)

win = psychopy.visual.Window(fullscr=False, units='pix')
win. setMouseVisible(False)

black = -1.0
white = 1.0
gray=0.0
absoluteDifferenceBetweenWhiteAndGray = abs(white-gray)

x,y = numpy.meshgrid(widthArray, widthArray)

a = numpy.cos(tiltInRadians)*radiansPerPixel;
b = numpy.sin(tiltInRadians)*radiansPerPixel;

gratingMatrix = numpy.sin(a*x+b*y);

circularGaussianMaskMatrix =
numpy.exp(-((x**2)+(y**2))/(gaussianSpaceConstant**2))

imageMatrix = gratingMatrix * circularGaussianMaskMatrix

grayscaleImageMatrix =
gray + absoluteDifferenceBetweenWhiteAndGray * imageMatrix

img = Image.fromarray(numpy.uint8(grayscaleImageMatrix*128 + 128))
grating = psychopy.visual.ImageStim(win=win, image=img)

hx,hy = win.size/2.0
message = psychopy.visual.TextStim(win=win, text='black=%.1f, white=%.1f\nPress any key to exit.' % (black, white), pos = (-hx,hy),alignHoriz='left',alignVert='top')

#draw()の順番だけ解説と入れ替えてある。
#画面を再描画する時はここより上の処理を繰り返す必要はなく、draw()してflip()すればよい
grating.draw()
message.draw()
win.flip()

psychopy.event.waitKeys()

win.close()
########## GratingDemo.mっぽいデモをpsychopy.visual.GratingDemoで作った版 ##########
#coding: utf-8
#GratingDemo.mっぽいものを表示するデモ
#短いけど抜粋ではなくこれですべて
import psychopy.visual
import psychopy.core

tiltInDegrees = 7
pixelsPerPeriod = 33
spatialFrequency = 1.0 / pixelsPerPeriod

win = psychopy.visual.Window(units='pix')

#これだけでグレーティングが書ける
#sizeは適当に決めたのでPsychToolboxの結果と大きさが少し異なる
grating = psychopy.visual.GratingStim(win,tex='sin',mask='gauss',
                    size=(200,200),ori=tiltInDegrees,sf=spatialFrequency)

grating.draw()
win.flip()
psychopy.event.waitKeys()
win.close()