データを複数のスナップショット、つまり時間の経過とともに動きの変わる連続した画像で視覚化する方法はさまざまにあります。しかし対話型のインターフェースを使って時間の経過に伴うデータ・セット間の関係を調べる方法はほとんどありません。この記事では、時間に応じて画像のさまざまな部分の視覚表現が行われる、名付けてデータの「アニメーション化変形ポータル」を作成するためのコードと手法を紹介します。さらに、処理速度に劣る計算プラットフォーム上でも実用性を犠牲にすることなく、効果的な視覚表現を可能にするコードの特徴についても紹介します。この記事に記載するコードで、各種のデータ・セットについて調べると同時にそれらのデータ・セットが時間とともにどのように移り変わるかを調べることで、アプリケーション・フローのモデルおよび使用パターンに対する新たな見識が得られるはずです。
この記事の視覚化アルゴリズムを効果的に使用するには、最新の高速ハードウェアが必要です。この記事では 1.8 GHz の IBM® ThinkPad で開発していますが、ピクセル単位で正確な視覚化を行うには、これより遥かに高速なプロセッサーおよび関連ハードウェア・データ・チャネルが推奨されます。実用性が損なわれることのないよう、記載するアルゴリズムには「チャンク描画」による効果が考慮されています。つまりチャンク単位で描画することにより、時間的変形の実用性をほぼ維持すると同時に、処理速度の遅いハードウェアでも迅速な動作を可能にしています。ただし、高速ハードウェアが必要なことには変わりなく、高性能のビデオ・カードの使用が推奨されます。
ここに記載するコードは、さまざまなオペレーティング・システムで実行することができますが、この記事で使用したのは Linux® です。手順に従うには、C プログラムのコンパイルが可能な開発環境が組み込まれた最新の Linux ディストリビューションを使用してください。SDL および SDL_image ライブラリーも必要になります。また、ビデオ・フレームをこの記事で開発するアプリケーションで使用可能なフォーマットに抽出するためには、mplayer も必要となります (「参考文献」を参照)。
まずはデモ・ビデオ (「参考文献」を参照) を見て、時間的変化による視覚表現を実現するデータ・セットに関する何らかのアイデアを持ってください。念頭に置いておかなければならないのは、静的な背景画像にするか、あるいは動的な背景画像にするかによって、視覚表現の明瞭さに関するさまざまな問題が出てくるという点です。この種の視覚化に初めて取り組む場合には、静的な背景にすることをお勧めします。静的な背景画像であれば、その変化を従来のフレームワークに統合しやすいためです。
データのソースとして考えられるのは、自然現象のビデオ、シミュレーション、またはユーザーが作成した連続画像です。付属の temporal.images/ ディレクトリーにあるファイルはその一例として、暴風雨の間に作成された降水量を反映するレーダーに似た画像で構成されています。
デモ・プログラムのすべての機能は、temporalVisualizer.c という 1 つのファイルに含まれています。この記事の内容に沿って独自のコピーを作成するのでも、または完全なソース・コードをダウンロードするのでも構いません。リスト 1 にプログラムの開始部分を記載します。
リスト 1. temporalVisualizer.c のインクルード、定義
//temporalVisualizer.c - display temporal distortion portals in video
#include <stdio.h>
#include <math.h>
#include "SDL.h"
#include "SDL_image.h"
#define WIDTH 1024 // screen dimensions
#define HEIGHT 768
#define MAX_IMAGES 110 // number of frames to read from disk
#define PORTAL_DIA 50 // center distortion portal size
// use 1 for per pixel correctness, multiples of ten for faster 'chunking'
int chunkSize=1;
int pixels[WIDTH][HEIGHT]; // frame number at each pixel coordinate
int animateGrid[WIDTH][HEIGHT]; // record animation position at each pixel
SDL_Surface *screen; // surface to display to user
SDL_Event event; // keyboard, mouse event handling
SDL_Surface* immutableImage; // base image to overwrite each frame
SDL_Surface* frame[MAX_IMAGES]; // array of frames to animate
SDL_Rect baseRect; // immutable image clipping rect
SDL_Rect src; // current frame clipping rect
int mouseX = 0;
int mouseY = 0;
int mouseIsDown = 0;
int stopMainLoop = 0;
|
ここに定義された変数は、ほとんどはプログラムに含まれるさまざまな関数で使用されます。以下の関数宣言を見てください。
リスト 2. 関数宣言
void initScreen();
void loadImages();
void checkEvents();
void resetPixels();
void circleSetPixels(float, float, float);
void lineSet(int, int, int, int, int);
void topCircle(float, float, int, float);
void bottomCircle(float, float, int, float);
void drawPixels();
void animateFrames();
|
上記の関数宣言に示されているように、プログラムの全体的なロジック・フローでは、まず画面がセットアップされて画像がロードされます。次に、メイン・ループのパスごとにイベントのチェック、描画状態のリセット、円形「ポータル」ウィンドウの描画が行われた上で、ウィンドウの変形が現行の時間枠に対して再びアニメーション化されるというわけです。
SDL には、ビデオ・モードと表示面を構成するための広範な構成オプションが用意されています。リスト 3 に、initScreen 関数が設定する表示オプションを示します。
リスト 3.
initScreen 関数
void initScreen()
{
const SDL_VideoInfo *info;
Uint8 video_bpp;
Uint32 videoflags;
if ( SDL_Init(SDL_INIT_VIDEO) < 0 )
{
fprintf(stderr, "Problem initializing SDL: %s\n",SDL_GetError());
exit(1);
}
atexit(SDL_Quit);
info = SDL_GetVideoInfo();
video_bpp = info->vfmt->BitsPerPixel;
// store surfaces in hardware video memory where possible, and enable
// double buffering on the displayable surface
videoflags = SDL_HWSURFACE | SDL_RESIZABLE | SDL_DOUBLEBUF;
if ( (screen=SDL_SetVideoMode(WIDTH,HEIGHT,video_bpp,videoflags)) == NULL )
{
fprintf(stderr, "Video mode error: %s\n",SDL_GetError());
exit(2);
}
SDL_WM_SetCaption("temporalVisualization","temporalVisualization");
}//initScreen
|
変数が定義され、ビデオ・インターフェースが使用可能かどうかのチェックが行われた後、ハードウェアのビデオ・メモリーを使用するように表示面が初期化されます。これにより、従来の 2D コンテキストで複数の表示面をブリットする際に伴う速度問題の一部はなくなります。SDL ライブラリーには、ここで取り上げるビデオ・インターフェースのオプションの他にも多数のビデオ・インターフェースのオプションがあることに注意してください。この基本構成のロードが上手くいかない場合は、SDL のドキュメント、または数多くあるチュートリアルを調べてください (「参考文献」を参照)。
表示面のセットアップに続くステップは、ディスクからの画像のロードです。リスト 4 に、loadImages 関数を記載します。
リスト 4.
loadImages 関数
void loadImages()
{
int i=0;
char filename[100];
for( i=1; i<=MAX_IMAGES; i++ )
{
sprintf(filename,"temporal.images/%d.jpg",i);
SDL_Surface *tempSurface;
if( (tempSurface=IMG_Load(filename))==NULL )
{
fprintf(stderr, "Image load error for file %s\n",filename);
exit(3);
}
frame[i] = SDL_ConvertSurface(tempSurface, screen->format, SDL_HWSURFACE);
fprintf(stdout,"loaded image %d\n",i);
}//for i
sprintf(filename,"temporal.images/%d.jpg",1);
immutableImage = SDL_ConvertSurface( IMG_Load(filename),
screen->format, SDL_HWSURFACE);
baseRect.x = 1;
baseRect.y = 1;
baseRect.w = immutableImage->w;
baseRect.h = immutableImage->h;
}//loadImages
|
それぞれの画像フォーマットによって、表示面で使用する色空間は異なります。IMG_Load 関数を使って各ビデオ・フレームを一時表示面にロードした後、今度は SDL_ConvertSurface 関数によって、使用されているすべての表示面のカラー・フォーマットが同じになるようにします。この事前変換は、フレームが部分的にバッファーにブリットされ、最終的には画面にブリットされるまで、さまざまなフレームの遷移中に色の状態を維持するためには不可欠です。
画像をロードして画面を構成した後は、アプリケーション・イベントを追跡する番です。
ここでのインターフェース・ストラテジーは、マウスのボタンが押されたときに、その時点のマウス座標で「変形ポータル」が開かれるようにすることです。SDL はビデオ・インターフェース・ライブラリーに加え、以下に示すように、イベント処理の大部分の作業を行うフレームワークを提供します。
リスト 5.
checkEvents 関数
void checkEvents()
{
while ( SDL_PollEvent(&event) )
{
if( event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)
{
stopMainLoop = 1;
}//if escape pressed
if( event.type == SDL_MOUSEBUTTONUP ){ mouseIsDown = 0; }
if( event.type == SDL_MOUSEBUTTONDOWN ){ mouseIsDown = 1; }
if( event.type == SDL_MOUSEMOTION )
{
mouseX = event.motion.x;
mouseY = event.motion.y;
//snap mouse to grid if not per-pixel mode
if( chunkSize > 1 )
{
mouseX = (int) mouseX/chunkSize;
mouseX *= chunkSize;
mouseY = (int) mouseY/chunkSize;
mouseY *= chunkSize;
}//if not trying high precision
}//mouse motion
}//while pollevent
}//checkEvents
|
マウスの座標をグリッドに変換する理由については、「速度に関する考慮事項」のセクションを参照してください。画像コンポーネントはその位置が記録されてから、描画可能な表示面にコピーされます。これは、これらのコンポーネントが確実に正しい順序で描画されるようにするためです。リスト 6 に、各描画パスの前にピクセルの 2D 配列を初期化する resetPixels 関数を記載します。
リスト 6.
resetPixels 関数
void resetPixels()
{
int x,y =0;
for( x=0; x<=WIDTH; x+=chunkSize )
{
for( y=0; y<=HEIGHT; y+=chunkSize )
{
pixels[x][y]= 1;
}//y
}//x
}//resetPixels
|
時間の経過に合わせてビデオ・フレームをさまざまな位置に描画する場合、フレームが重なる可能性があります。そのため、描画前のステップでは特別な注意が必要です。さらに、フレームが変形ポータルの端に向かって移動するにつれて正しく変形するという動きは、重なり合った円のマルチパス・セットとして実装されます。これらの画像コンポーネントを正しい順番で描画し続けるため、実際にブリットするステップの前に各ピクセル、つまりチャンク (詳細を以下に説明) のフレームが決定されます。
circleSetPixels、topCircle、および bottomCircle 関数
ある特定の座標のセット、そして終了フレームについては、circleSetPixels 関数が一連の重なり合った円を描画します。各円には、開始フレームから終了フレームまでの段階的変形ゾーンを表示するのに適切な数のフレームが設定されます。この circleSetPixels 関数は、リスト 7 のとおりです。
リスト 7.
circleSetPixels 関数
void circleSetPixels(float startx, float starty, float inFrame)
{
float frameInc = (inFrame-1) / PORTAL_DIA;
float i=0;
float currFrame=1;
for( i=(PORTAL_DIA*2); i>=PORTAL_DIA; i-- )
{
int xshift = (PORTAL_DIA-i) * 2;
int yshift = PORTAL_DIA-i;
topCircle( startx+xshift, starty+yshift, i, currFrame );
bottomCircle( startx+xshift, starty+yshift, i, currFrame );
currFrame += frameInc;
}//for i
}//circleSetPixels
|
円は上半分と下半分とに分けて、最大直径から最小直径へと描画されます。上下の半円には、それぞれの中心が同じになるように算出された x 座標と y 座標でオフセットが設定されます。各円の描画に従って、currFrame 変数は基本画像 (immutableImage 表示面に定義) から変形ポータルの終了フレームまでの範囲内で次のフレームを指す値へと変わっていきます。時間が経過して変形ポータルの終了フレームを指すところまで行くと、開始フレームと終了フレームとの間にあるフレームが有効な変形ゾーン全体に広がっています。このように、たくさんのフレームが混ざり合った変形ゾーン内でのフレームのインクリメント量が、1 からポータル・フレームの半分までの間で適切な値になるように frameInc 変数が定義されています。
リスト 8 に、circleSetPixels から呼び出される topCircle 関数と bottomCircle 関数を記載します。
リスト 8.
bottomCircle および topCircle 関数
void bottomCircle(float startx, float starty, int r, float inFrame)
{
int i = 1;
float nexty = starty +(r*2);
for( i=r; i>=1; i-- )
{
int length = (int) (sqrt( cos(0.5f * 3.14159 * (i-r)/r)) * r * 2);
int ofs = (r*2) - (length/2);
lineSet(startx+ofs, nexty-i, length, chunkSize, inFrame );
}//i
}//bottomCircle
void topCircle(float startx, float starty, int r, float inFrame)
{
int i = 1;
for( i=1; i<=r; i++ )
{
int length = (int) (sqrt( cos(0.5f * 3.14159 * (i-r)/r)) * r * 2);
int ofs = (r*2) - (length/2);
lineSet(startx+ofs, starty+i, length, chunkSize, inFrame );
}//i
}//topCircle
|
上下の半円のそれぞれは、水平線の集合に分割されます。上記の 2 つの関数が円の端からのオフセットと各水平線の長さを決定し、リスト 9 の lineSet 関数で水平線を端から端までトラバースします。
リスト 9.
lineSet 関数
void lineSet(int inX, int inY, int inWidth, int inHeight, int frameNum )
{
if( chunkSize > 1 )
{
inX = (int) inX/chunkSize;
inX *= chunkSize;
inY = (int) inY/chunkSize;
inY *= chunkSize;
}//snap to grid if not in per-pixel mode
int x=0;
for( x=inX; x<=(inX+inWidth); x+= chunkSize )
{
int y=0;
for( y=inY; y<=(inY+inHeight); y+=chunkSize )
{
if( x < WIDTH && y < HEIGHT )
{
if( frameNum > pixels[x][y] ){ pixels[x][y] = frameNum; }
}//if on screen
}//for y
}//for x
}//lineSet
|
今ではお馴染みのチャンク化コード (「速度に関する考慮事項」を参照) の後、各水平線のそれぞれのピクセルのフレーム番号が現行のフレーム番号と比較され、現行のフレーム番号の方が大きい場合には、それぞれのピクセルには現行フレームが設定されます。これにより、重なり合う変形ウィンドウがビデオ・フレームの競合する部分を表示しなくなります。さらに、このようにピクセル (チャンク) 単位でフレーム番号を事前に決定することにより、正しく描画するために必要なブリット操作の回数が大幅に減ることになります。
表示可能な可視コンポーネントを事前処理し、該当画像から正しいピクセルを選択するという作業は、このプログラムの複雑な部分のほとんどを占めています。実際、正確なピクセル値が決まれば、後は簡単にピクセルを描画可能な表示面に描画することができます。リスト 10 に、drawPixels 関数を記載します。
リスト 10.
drawPixels 関数
void drawPixels()
{
int x,y = 0;
for( x=0; x<=WIDTH; x+=chunkSize )
{
for( y=0; y<=HEIGHT; y+=chunkSize )
{
src.x=x;
src.y=y;
src.w = chunkSize;
src.h = chunkSize;
SDL_BlitSurface( frame[pixels[x][y]], &src, screen, &src);
}//y
}//x
}//drawPixels
|
この関数では、chunkSize 掛ける chunkSize のピクセルからなる四角形を、どのように該当するフレームから描画可能な表示面にコピーしているかに注目してください。chunkSize が 1 の場合、変形アルゴリズムがピクセル単位で正確に描画されると同時に、chunkSize が増えるごとに必要な SDL_BlitSurface 呼び出しの回数が大幅に減っていきます。描画可能な可視コンポーネントがコピーされた後は、animateFrames 関数が各フレームのアニメーション化を制御します。
リスト 11.
animateFrames 関数
void animateFrames()
{
int x,y =0;
for( x=0; x<=WIDTH; x+=chunkSize )
{
for( y=0; y<=HEIGHT; y+=chunkSize )
{
if( animateGrid[x][y] > 1 ){ animateGrid[x][y]--; }
}//y
}//x
}//animateFrames
|
この関数は、各フレームを最終画像から最初の画像へと逆に移動させています。マウスを押したときに各フレームを設定するコード・ブロックについては、次のセクションを読んでください。
リスト 2 のメイン・ロジック・フローを思い出してください。メイン・ロジックは、画面のセットアップ、画像のロード、イベントの処理、ピクセル状態の設定、描画、そしてフレームのアニメーション化というフローになっています。main() のコード、そしてこの順番でのフローの実装をリスト 12 に記載します。
リスト 12.
main() プログラム・コード
int main(int argc, char *argv[])
{
initScreen();
loadImages();
while ( stopMainLoop == 0 )
{
checkEvents();
if( mouseIsDown ){ animateGrid[mouseX][mouseY] = MAX_IMAGES-1; }
resetPixels();
int x,y = 0;
for( x=0; x<=WIDTH; x+=chunkSize )
{
for( y=0; y<=HEIGHT; y+=chunkSize )
{
if( animateGrid[x][y] > 1 ){ circleSetPixels(x,y, animateGrid[x][y] ); }
}//y
}//x
SDL_BlitSurface(immutableImage, NULL, screen, &baseRect);
drawPixels();
SDL_Flip(screen);
animateFrames();
}// while stopMainLoop == 0
SDL_Quit();
return(0);
}//main
|
画面がセットアップされて画像がロードされると、メイン・ループのパスごとに checkEvents と resetPixels が実行されます。この 2 つの関数の間で、マウスのボタンが押された場合の最終フレーム状態が現行のマウス位置の座標に設定されます。次のループ・セクションは、該当する位置のフレームが開始フレームより大きい場合にのみ、ピクセル座標の設定を制御します。SDL_BlitSurface は画面をクリアして基本画像 (immutableImage) を設定し、drawPixels 関数コールが描画可能な表示面に適切な画像コンポーネントを取り込みます。そして最後に、SDL_Flip がバック・バッファーを画面バッファーに転送し、animateFrames がビデオ・フレームの円滑な進行を行って画面を更新します。
この記事のソース・コードのディストリビューションにある temporal.images ディレクトリーには、実験に適した NOAA 気象サービスのレーダー画像が含まれています。このディレクトリーにお好きなビデオから抽出した画像を配置するか、あるいは既存の画像をそのまま使ってコマンド gcc `sdl-config --cflags --libs` -lSDL_image temporalVisualizer.c && ./a.out を実行してください。注意する点として、適切な SDL および SDL_image ライブラリーがインストールされていないと、このコンパイル・コマンドは機能しません。13 行目の chunkSize パラメーターは 10 に変更して再コンパイルすると、視覚化の速度が大幅に向上するはずなので、試してみてください。
それぞれに処理速度の異なる計算プラットフォームで実用的な視覚化をサポートするようにコードを適応させるには、chunkSize 変数を使用して制御します。checkEvents 関数では、保存されたマウスの座標が chunkSize 掛ける chunkSize の四角からなるグリッドに変換されます。以下に示す行および lineSet 関数の同様のエントリーによって、描画可能なピクセルのすべてが正しいグリッドで開始し、終了することが確実になります。chunkSize 変数が増加するごとに、比較の回数ならびにポータルを描画する際の表示面ブリットが大幅に減ります。
リスト 13. 描画可能なピクセルがすべて正しいグリッドで開始、終了することを確実にするための方法
mouseX = (int) mouseX / chunkSize;
mouseX *= chunkSize;
mouseY = (int) mouseY / chunkSize;
mouseY *= chunkSize;
|
上記のコード、そして独自に完成させた temporalVisualizer.c プログラムによって、ビデオやその他のシーケンシャル・データ・セットのフレームを新しく有益な方法で視覚化する関数プログラムを手に入れたことになります。
temporalVisualizer.c プログラムはさらに拡張可能なフレームワークとして設計されているので、クリックしたボタンに応じて逆方向または順方向にフレームを再生させるのも一考です。また、マウスをポータルに重ねると現行の位置でポータルがフリーズするオプションを追加したり、マウスのボタンを押している時間の長さに応じて過去に遡ったフレームを送信したりするという方法を試してみてください。
ここに記載したコードと独自のアイデアで、時系列でデータを理解し、探索できる新しい視覚化を実現してください。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample code | os-timedep.temporalVisualizer_0.1.zip | 3224KB | HTTP |
学ぶために
- YouTube.comで公開されている著者のデモ・ビデオを見て、時間依存データの視覚化を確認してください。
- クロスプラットフォームのマルチメディア・ライブラリー、Simple DirectMedia Layer プロジェクトについて読んでください。
- SDL ライブラリーを備えた画像ロード・ライブラリー、SDL_image について読んでください。
- SDL の使用方法に関する数多くの優れたチュートリアルがあります。そのうちの 3 つは、SDL Web サイト、Lazy Foo' Productions、Jari Komppa の Tutorials で入手できます。
-
MPlayer は、さまざまなビデオ・ファイル・フォーマットの再生をサポートするビデオ・プレイヤーです。Linux、Windows、OS X 対応の MPlayer をダウンロードしてください。一部の Linux 配布では、パッケージという形でも入手できます。
- この記事は、Khronos Projector から発想を得ています。
- ソフトウェア開発者を対象とした興味深いインタービューや討論については、developerWorks ポッドキャストをチェックしてください。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- 世界中で近日中に予定されている IBM オープンソース開発者を対象とした会議、見本市、ウェブ放送やその他のイベントをチェックしてください。
- オープンソース技術を使用して開発し、IBM の製品と併用するときに役立つ広範囲のハウツー情報、ツール、およびプロジェクト・アップデートについては、developerWorks Open source ゾーンを参照してください。
- 無料の developerWorks On demand demos で、IBM およびオープンン・ソースの技術と製品機能を調べて試してみてください。
製品や技術を入手するために
-
IBM ソフトウェアの試用版を使用して、次のオープンソース開発プロジェクトを革新してください。ダウンロード、あるいは DVD で入手できます。
-
IBM 製品の評価版をダウンロードして、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。
議論するために
-
developerWorks blogs から developerWorks コミュニティーに加わってください。
