2015年度GPGPU実践基礎工学 第12回 GPUによる画像処理
April 26, 2018 | Author: Anonymous |
Category:
Engineering
Description
第12回 GPUによる画像処理 長岡技術科学大学 電気電子情報工学専攻 出川智啓 授業項目 8. 並列計算の概念(プロセスとスレッド) 9. GPUのアーキテクチャ 10. GPGPUのプログラム構造 11. GPUでの並列プログラミング(ベクトル和) 12. GPUによる画像処理 13. GPUのメモリ階層 14. GPGPU組込開発環境 15. GPGPU開発環境(OpenCL) GPGPU実践基礎工学2 2015/11/25 今回の内容 GPGPU実践基礎工学3 画像処理 ネガティブ処理,画像反転,空間フィルタ,モザイク処理 画像処理を簡略化した1次元的な処理 2次元的な並列処理 2次元配列の表現 ベクトル和 モノクロ画像処理 2015/11/25 画像処理 GPGPU実践基礎工学4 デジタル画像を別の画像に変換 処理の種類 画像の拡大・縮小 画像の反転 フィルタ 2値化,ぼかしなど 2015/11/25 画像処理を行うプログラムの流れ GPGPU実践基礎工学5 読み込む画像を準備 対象ファイルをオープン 画像の読込 画像ファイルの種類(bmp,jpg,png等)に応じて読み込む方 法を選択 画像ファイルからヘッダ情報を読み込み,様々な情報を取得 メモリを確保し,画素情報を読込 定められたアルゴリズムで画像を処理 処理された画像の出力 2015/11/25 画像処理 GPGPU実践基礎工学6 画像を読むプログラムを作るだけで一苦労 画像処理が主目的ではなく,GPUの使い方を学習するこ とが目的 画像をモノクロに限定 プログラム中で画像に相当するデータを生成 ターミナルにテキスト出力(横の位置,縦の位置,色情報) 画像の表示にはgnuplotを利用 モノクロ画像に対して簡単な処理を実行 2015/11/25 取り扱うモノクロ画像 GPGPU実践基礎工学7 型はunsigned char 1バイトの整数型 取り扱える範囲は0~255 0を黒,255を白とみなす 1次元配列で取り扱う 画像は2次元なので,2次元配列で取り扱う方が自然 mallocやcudaMallocでは2次元配列を確保できない 2015/11/25 取り扱うモノクロ画像の内容 GPGPU実践基礎工学8 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255, 0,255,255,255,255,255,255, 255,255,255,255,255,255,255,255,255, 0, 0,255,255,255,255,255, 255,255,255,255,255,255,255,255,255, 0, 0, 0,255,255,255,255, 255,255,255,255,255,255,255,255,255, 0, 0, 0, 0,255,255,255, 255,255,255,255,255,255,255,255,255, 0, 0, 0, 0, 0,255,255, 255,255,255,255,255,255,255,255,255, 0, 0, 0, 0, 0, 0,255, 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255}; unsigned char picture[] = { x y 2015/11/25 取り扱うモノクロ画像の内容 GPGPU実践基礎工学9 gnuplotで表示 左下を原点として表示 画像処理では左上が原点 x y 2015/11/25 画像処理 GPGPU実践基礎工学10 ネガティブ処理 反転 水平反転,垂直反転 空間フィルタ ぼかし,輪郭抽出 モザイク処理 2015/11/25 画像処理の簡略化(1次元化) GPGPU実践基礎工学11 いきなり画像処理は難しい 1次元の配列に対して,画像処理と同じような処理を実行 どのような処理を行うかを確認 ネガティブ処理 水平反転 空間フィルタ(ぼかし,輪郭抽出) モザイク処理 2015/11/25 原画像もどき 2015/11/25GPGPU実践基礎工学12 幅 WIDTH=64 (0~63) [3*WIDTH/16,9*WIDTH/16)の範囲を白(255),それ 以外を黒(0) 変数名 原画像 p[] 処理画像 filtered[] 原画像pのピクセル値 画 素 値 横位置 ネガティブ処理 2015/11/25GPGPU実践基礎工学13 色の反転処理 黒(0)を白(255),白(255)を黒(0)に反転 (白−各画素の値)を新しい画素の値とする 画 素 値 横位置 画 素 値 原画像p[] 処理画像 filtered[] 白(255)を黒(0)に 黒(0)を白(255)に ネガティブ処理 2015/11/25GPGPU実践基礎工学14 色の反転処理 黒(0)を白(255),白(255)を黒(0)に反転 (白−各画素の値)を新しい画素の値とする void negative(unsigned char *p, unsigned char *filtered){ int i; for (i = 0; i 水平反転 2015/11/25GPGPU実践基礎工学15 画像の幾何的位置を反転 水平反転 i→(WIDTH‐1)‐i p[i]→filtered[(WIDTH‐1)‐i] 画 素 値 横位置 画 素 値 原画像p[] 処理画像 filtered[] 横位置1の画素値を62番目の画素値に 横位置35の画素値を28番目の画素値に 水平反転 2015/11/25GPGPU実践基礎工学16 画像の幾何的位置を反転 水平反転 i→(WIDTH‐1)‐i p[i]→filtered[(WIDTH‐1)‐i] void hreflect(unsigned char *p, unsigned char *filtered){ int i; for (i = 0; i 空間フィルタ 2015/11/25GPGPU実践基礎工学17 ある画素とその周囲の画素を使って処理 処理の仕方を規定したフィルタカーネルを定義 フィルタカーネルは1次元配列で表現,引数として関数に渡す ぼかし(平均)フィルタ 輪郭抽出(ラプラシアン)フィルタ 1/3 1/3 1/3 1 ‐2 1 float blur[3] ={1.0f/3.0f,1.0f/3.0f,1.0f/3.0f}; float laplacian[9] ={ 1.0f,‐2.0f, 1.0f}; 画 素 値 横位置 画 素 値 空間フィルタ 2015/11/25GPGPU実践基礎工学18 ある画素とその周囲の画素を使って処理 ある位置iの左右(i‐1,i+1)の計3画素の値を読み,それぞ れフィルタカーネルの各値との積を計算し,合計を取った値を 処理画像のi番目の画素とする ぼかしフィルタカーネル 1/3 1/3 1/3 255/3+255/3+255/3=255 原画像p[] 処理画像 filtered[] 画 素 値 横位置 画 素 値 空間フィルタ 2015/11/25GPGPU実践基礎工学19 ある画素とその周囲の画素を使って処理 ある位置iの左右(i‐1,i+1)の計3画素の値を読み,それぞ れフィルタカーネルの各値との積を計算し,合計を取った値を 処理画像のi番目の画素とする ぼかしフィルタカーネル 1/3 1/3 1/3 255/3+255/3+255/3=255 原画像p[] 処理画像 filtered[] 画 素 値 横位置 画 素 値 空間フィルタ 2015/11/25GPGPU実践基礎工学20 ある画素とその周囲の画素を使って処理 ある位置iの左右(i‐1,i+1)の計3画素の値を読み,それぞ れフィルタカーネルの各値との積を計算し,合計を取った値を 処理画像のi番目の画素とする ぼかしフィルタカーネル 1/3 1/3 1/3 255/3+255/3+255/3=255 原画像p[] 処理画像 filtered[] 画 素 値 横位置 空間フィルタ 2015/11/25GPGPU実践基礎工学21 ある画素とその周囲の画素を使って処理 ある位置iの左右(i‐1,i+1)の計3画素の値を読み,それぞ れフィルタカーネルの各値との積を計算し,合計を取った値を 処理画像のi番目の画素とする ぼかしフィルタカーネル 1/3 1/3 1/3 0/3+255/3+255/3=170 原画像p[] 処理画像 filtered[] 画 素 値 空間フィルタ 2015/11/25GPGPU実践基礎工学22 ある画素とその周囲の画素を使って処理 forループでiの値を変化させ,p[i‐1],p[i],p[i+1]の値 から処理後の画素値filtered[i]を計算 void boxfilter (unsigned char *p, unsigned char *filtered, float *filter){ int i, result=BLACK; for (i = 1; i 画 素 値 横位置 画 素 値 空間フィルタ 2015/11/25GPGPU実践基礎工学23 輪郭抽出フィルタの結果 白と黒の境界でのみ値が255になり,それ以外では0 両端はフィルタ処理できないため,初期値がそのまま使われる 原画像p[] 処理画像 filtered[]白と黒の境界でのみ 画素値が0より大きく なる モザイク処理 2015/11/25GPGPU実践基礎工学24 画像を小領域に分け,その領域を全て同じ色に置換 小領域内の全画素を,小領域内の画素の平均値に置換 小領域を移動するループ内に,小領域内の画素の平均値を 計算するループを設ける 画 素 値 横位置 原画像p[] 処理画像 filtered[] 画 素 値 小領域 モザイク処理 2015/11/25GPGPU実践基礎工学25 画像を小領域に分け,その領域を全て同じ色に置換 小領域内の全画素を,小領域内の画素の平均値に置換 小領域を移動するループ内に,小領域内の画素の平均値を 計算するループを設ける 画 素 値 横位置 原画像p[] 処理画像 filtered[] 画 素 値 モザイク処理 2015/11/25GPGPU実践基礎工学26 画像を小領域に分け,その領域を全て同じ色に置換 小領域内の全画素を,小領域内の画素の平均値に置換 小領域を移動するループ内に,小領域内の画素の平均値を 計算するループを設ける 画 素 値 横位置 原画像p[] 処理画像 filtered[] 画 素 値 小領域 モザイク処理 2015/11/25GPGPU実践基礎工学27 画像を小領域に分け,その領域を全て同じ色に置換 小領域内の全画素を,小領域内の画素の平均値に置換 小領域を移動するループ内に,小領域内の画素の平均値を 計算するループを設ける 画 素 値 横位置 原画像p[] 処理画像 filtered[] 画 素 値 モザイク処理 2015/11/25GPGPU実践基礎工学28 画像を小さな領域に分け,その領域を全て同じ色に置換 小領域内で画素の平均値を計算し,処理画像の小領域内の 全画素をその平均値とする void mosaic(unsigned char *p,unsigned char *filtered,int mosaicSize){ int i,isub,average; for (i = 0; i 処理の流れ GPGPU実践基礎工学29 変数を宣言し,mallocでメモリを確保 原画像を格納する変数 p 処理後の画像を格納する変数 filtered 原画像pを作成 pをfilteredにコピーしておく 処理を実行 関数を作成し,pとfilteredを渡す filteredの内容を画面に出力 2015/11/25 メイン関数(CPU版) GPGPU実践基礎工学30 #include #include #define WHITE (255) #define BLACK (0) #define WIDTH (64) #define Nbytes (WIDTH*sizeof(unsigned char)) void create(unsigned char *); void copy(unsigned char *,unsigned char *); void print(unsigned char *); void negative(unsigned char *, unsigned char *); void hreflect(unsigned char *, unsigned char *); void boxfilter(unsigned char *, unsigned char *, float *); void mosaic(unsigned char *, unsigned char *, int); int main(void){ unsigned char *p= (unsigned char *)malloc(Nbytes); unsigned char *filtered= (unsigned char *)malloc(Nbytes); //ここで空間フィルタのカーネルを宣言 2015/11/25 imageproc1d.c メイン関数(CPU版) GPGPU実践基礎工学31 create(p); copy(p,filtered); //ここで処理を行い,結果をfilteredに格納 //画面に各画素の値を表示 print(filtered); return 0; } //画像の内容をコピー void copy(unsigned char *src, unsigned char *dst){ int i; for (i = 0; i 画像の作成 2015/11/25GPGPU実践基礎工学32 [3*WIDTH/16,9*WIDTH/16)の範囲を白(255),それ 以外を黒(0) void create(unsigned char *p){ int i, x_origin; for (i = 0; i 結果の画面表示とファイル保存 GPGPU実践基礎工学33 print関数が画面に各画素の座標と値を表示 画面出力のファイル表示(出力のリダイレクトを利用) >を利用して保存,結果は表計算等で確認 $ コマンド > ファイル名 0 255 1 255 : 62 255 63 255 $ ./a.out > pic.txt $ cat pic.txt 0 255 1 255 : 62 255 63 255 2015/11/25 GPUへの移植の方針 GPGPU実践基礎工学 現画像の作成はCPUで行い,GPUへ転送 1スレッドが1画素の処理を実行 forループを排除し,配列添字とスレッド番号を対応 空間フィルタ 端の画素の処理ができないので今回は処理しない モザイク 領域の大きさは1ブロックと同じとする 領域内の画素の平均値の計算は非常に難易度が高い 各ブロックにおけるthreadIdx.x=0のスレッドが平均を計算して処 理画像に書き込む 34 2015/11/25 メイン関数(GPU版) GPGPU実践基礎工学35 #include #include #define WHITE (255) #define BLACK (0) #define WIDTH (64) #define Nbytes (WIDTH*sizeof(unsigned char)) #define THREAD 16 #define BLOCK (WIDTH/THREAD) void create(unsigned char *); void print(unsigned char *); int main(void){ unsigned char *p= (unsigned char *)malloc(Nbytes); unsigned char *dev_p, *dev_filtered; cudaMalloc( (void **)&dev_p, Nbytes); cudaMalloc( (void **)&dev_filtered, Nbytes); 2015/11/25 imageproc1d.cu メイン関数(GPU版) GPGPU実践基礎工学36 //ここで空間フィルタのカーネルを宣言 float *filter; cudaMalloc( (void **)&filter, sizeof(float)*3); cudaMemcpy(filter, フィルタのカーネル, sizeof(float)*3, cudaMemcpyHostToDevice); create(p); //CPUで画像を生成してGPUへ送った後,変数dev_filteredにコピーしておく cudaMemcpy(dev_p, p, Nbytes, cudaMemcpyHostToDevice); cudaMemcpy(dev_filtered, dev_p, Nbytes, cudaMemcpyDeviceToDevice); //ここで処理を行い,結果をdev_filteredに格納 //dev_filteredの内容をCPUへ送る unsigned char *filtered = (unsigned char *)malloc(Nbytes); cudaMemcpy(filtered, dev_filtered, Nbytes, cudaMemcpyDeviceToHost); free(p); free(filtered); cudaFree(dev_p); cudaFree(dev_filtered); cudaFree(filter); 2015/11/25 imageproc1d.cu メイン関数(GPU版) GPGPU実践基礎工学37 return 0; } //原画像の生成 void create(unsigned char *p){ int i, x_origin; for (i = 0; i ネガティブ処理 2015/11/25GPGPU実践基礎工学38 1スレッドが1画素の値を計算 ベクトル和のようにスレッド番号と配列添字の対応を計算 画 素 値 横位置 画 素 値 原画像p[] 処理画像 filtered[] ・・・0 1 2 3スレッド番号 void negative(unsigned char *p, unsigned char *filtered){ int i; for (i = 0; i __global__ void negative(unsigned char *p, unsigned char *filtered){ int i; i = blockIdx.x*blockDim.x + threadIdx.x; //スレッド数と配列添字の対応 filtered[i] = WHITE ‐ p[i]; } ネガティブ処理 GPGPU実践基礎工学40 2015/11/25 imageproc1d.cu 水平反転 2015/11/25GPGPU実践基礎工学41 1スレッドが1画素の値を反転 ベクトル和のようにスレッド番号と配列添字の対応を計算 書込先が変わる 画 素 値 横位置 画 素 値 原画像p[] 処理画像 filtered[] 横位置35の画素値を28番目の画素値に 0 1 2 3 ・・・スレッド番号 void hreflect(unsigned char *p, unsigned char *filtered){ int i; for (i = 0; i __global__ void hreflect(unsigned char *p, unsigned char *filtered){ int i; i = blockIdx.x*blockDim.x + threadIdx.x; //(WIDTH‐1)‐iは反転後の横位置 filtered[(WIDTH‐1)‐i] = p[i]; } 水平反転 GPGPU実践基礎工学43 2015/11/25 imageproc1d.cu 空間フィルタ 2015/11/25GPGPU実践基礎工学44 1スレッドが1画素の値を計算 ある位置iの画素だけではなく左右(i‐1,i+1)の画素も参照 全スレッドが同じフィルタカーネルを参照 画 素 値 横位置 画 素 値 ぼかしフィルタカーネル 原画像p[] 処理画像 filtered[] 0 1 2 3スレッド番号 1/3 1/3 1/3 ・・・ void boxfilter (unsigned char *p, unsigned char *filtered, float *filter){ int i, result=BLACK; for (i = 1; i __global__ void boxfilter (unsigned char *p, unsigned char *filtered, float *filter){ int i, result = BLACK; i = blockIdx.x*blockDim.x + threadIdx.x; if(0 モザイク処理 2015/11/25GPGPU実践基礎工学47 1ブロック内の1スレッドのみが処理 1スレッドがブロック内の全画素の平均を計算 平均の画素値を処理画像に書込 画 素 値 横位置 原画像p[] 処理画像 filtered[] 画 素 値 スレッド番号 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 ・・・ void mosaic(unsigned char *p,unsigned char *filtered,int mosaicSize){ int i,isub,average; for (i = 0; i __global__ void mosaic (unsigned char *p, unsigned char *filtered, int mosaicSize){ int i,isub,average; i = blockIdx.x*blockDim.x + threadIdx.x; if(threadIdx.x == 0){//ブロック内の1スレッドのみが処理 //小領域内の画素の平均値を計算 average = 0; for(isub = 0; isub ネガティブ処理 2015/11/25GPGPU実践基礎工学50 色の反転処理 黒を白,白を黒に反転 原画像 処理画像 画像反転 2015/11/25GPGPU実践基礎工学51 画像の幾何的位置を反転 画像(画素)の配列添字を変えて出力 原画像 処理画像 空間フィルタ 2015/11/25GPGPU実践基礎工学52 ある画素とその周囲の画素を使って処理 処理の仕方を規定したカーネルを定義 カーネルを変更することで様々な処理が可能 原画像 輪郭抽出 原画像 輪郭抽出 フィルタ処理 2015/11/25GPGPU実践基礎工学53 2次元的な処理 あるピクセルの周囲情報を利用して効果を計算 画像を矩形領域で分割した2次元的な並列処理が自然 0 1 0 1 ‐4 1 0 1 0 = b+d‐4e+f+h フィルタ (カーネル) a b c d e f g h i モザイク処理 2015/11/25GPGPU実践基礎工学54 画像を小さな領域に分け,その領域を全て同じ色に置換 領域内のある1画素の値か,領域内の画素の平均値にするこ とが多い 原画像 処理画像 2次元配列 GPGPU実践基礎工学55 変数名の後ろに[]を2個付けて宣言 1次元配列はベクトル,2次元配列は行列や画像 unsigned char picture[Nx][Ny]; vector[Nx] Nx=8 i Nx=8 j i matrix[Nx][Ny] Ny =8 2015/11/25 2次元配列の1次元配列表現 2015/11/25GPGPU実践基礎工学56 mallocやcudaMallocは1次元配列を宣言 2次元配列を宣言できない 1次元配列を宣言し,2次元配列的に読み書き アドレス空間は1次元 2次元配列でも1次元のアドレスで表現 1次元目が連続か,2次元目が連続かが言語によって異なる j i j i 1次元目が連続 matrix[Nx][Ny] matrix[Nx][Ny] 2次元目が連続 2次元配列の1次元配列表現 2015/11/25GPGPU実践基礎工学57 C言語は2次元目が連続 2次元配列の場合 1次元配列の場合 picutre[1][2] = 11 picture[_______] = 11 Nx=8 1*8 + 2 picture[i][j] picture[i*Ny+j] j i 1 2 3 4 5 9 10 11 12 6 7 8 picture[Nx][Ny] Ny =8 ベクトル和C=A+B(2次元版) 2015/11/25GPGPU実践基礎工学58 1次元のベクトル和とほぼ同じ 並列化が容易 for文の書く順番で実行速度が変化 c[][] a[][] b[][] #define Nx (1024) #define Ny (1024) void init(float a[Nx][Ny], float b[Nx][Ny], float c[Nx][Ny]){ int i,j; //iとjのループを入れ替えるとどうなるか? for(i=0;i #include #define Nx (1024) #define Ny (1024) #define Nbytes (Nx*Ny*sizeof(float)) void init(float *a, float *b, float *c){ int i,j,ij; for(i=0;i GPUによる2次元的な並列処理 2015/11/25GPGPU実践基礎工学61 ブロックとスレッドを2次元的に設定 1スレッドが配列の1要素(画像の1ピクセル)を処理 スレッドを2次元的に配置してブロックを構成 GPUの並列化の階層 グリッド-ブロック-スレッドの3階層 各階層の情報を参照できる変数 x,y,zをメンバにもつdim3型構造体 グリッド(Grid) gridDim グリッド内にあるブロックの数 ブロック(Block) blockIdx ブロックに割り当てられた番号 blockDim ブロック内にあるスレッドの数 スレッド(Thread) threadIdx スレッドに割り当てられた番号 GPGPU実践基礎工学62 2015/11/25 Hello Threads(2次元版) の中にどのように数字を書くか 2015/11/25GPGPU実践基礎工学63 #include __global__ void hello(){ printf("gridDim.x=%d,blockIdx.x=%d,blockDim.x=%d,threadIdx.x=%d¥n", gridDim.x, blockIdx.x, blockDim.x, threadIdx.x); printf("gridDim.y=%d,blockIdx.y=%d,blockDim.y=%d,threadIdx.y=%d¥n", gridDim.y, blockIdx.y, blockDim.y, threadIdx.y); } int main(void){ hello Hello Threads(2次元版) 1次元と同様等として実行 実行結果(画面出力) 2015/11/25GPGPU実践基礎工学64 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=0 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=1 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=2 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=3 gridDim.x=2, blockIdx.x=1, blockDim.x=4, threadIdx.x=0 gridDim.x=2, blockIdx.x=1, blockDim.x=4, threadIdx.x=1 gridDim.x=2, blockIdx.x=1, blockDim.x=4, threadIdx.x=2 gridDim.x=2, blockIdx.x=1, blockDim.x=4, threadIdx.x=3 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 gridDim.y=1, blockIdx.y=0, blockDim.y=1, threadIdx.y=0 並列度の指定が できていない 2次元的な並列度の指定 2015/11/25GPGPU実践基礎工学65 の中にどのように数字を書くか 1次元の場合は数字を書くことができた 2次元,3次元は数字を並べて書くことができない dim3型構造体を利用 int main(void){ dim3 block(2,4,1), thread(4,2,1); hello(); hello(...); } dim3型変数block, threadを利用 ・・・ あるいは直接dim3型として記述・・・ Hello Threads(2次元版) 実行結果(画面出力) 2015/11/25GPGPU実践基礎工学66 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=0 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=1 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=2 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=3 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=0 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=1 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=2 gridDim.x=2, blockIdx.x=0, blockDim.x=4, threadIdx.x=3 :(中略) gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=0 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=0 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=0 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=0 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=1 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=1 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=1 gridDim.y=4, blockIdx.y=0, blockDim.y=2, threadIdx.y=1 : 対応 対応 各スレッドが異なるi,jを参照するには CUDAでは並列化に階層がある 全体の領域(グリッド)をブロックに分割 ブロックの中をスレッドに分割 ブロックの数 1ブロックあたりの スレッドの数 x方向ブロック数×1ブロックあたりのスレッドの数 ×y方向ブロック数×1ブロックあたりのスレッドの数=総スレッド数 GPGPU実践基礎工学67 2015/11/25 総スレッド数=2×4×4×2=64 各スレッドが異なるi,jを参照するには 2015/11/25GPGPU実践基礎工学68 Nx=8, Ny=8, x,y方向スレッド数4,ブロック数2 gridDim.x=2, gridDim.y=2 blockDim.x=4,blockDim.y=4 (0,0)(1,0)(2,0)(3,0)(0,0) (3,3) (3,3) (0,1)(1,1)(2,1)(3,1) (0,2)(1,2)(2,2)(3,2) (0,3)(1,3)(2,3)(3,3) (3,3) (0,0) (0,0) threadIdx.x,threadIdx.y i= 0 1 2 3 4 5 6 7 j= 0 1 2 3 4 5 6 7 ・・・ ・ ・ ・ ・・・ ・ ・ ・ ・・・ ・ ・ ・ (0,0)(1,0)(2,0)(3,0)(0,0) (3,3) (3,3) (0,1)(1,1)(2,1)(3,1) (0,2)(1,2)(2,2)(3,2) (0,3)(1,3)(2,3)(3,3) (3,3) (0,0) (0,0) 各スレッドが異なるi,jを参照するには 2015/11/25GPGPU実践基礎工学69 Nx=8, Ny=8, x,y方向スレッド数4,ブロック数2 blockDim.x=4 bl oc kD im .y =4 threadIdx.x,threadIdx.y blockIdx.x=0 blockIdx.x=1 bl oc kI dx .y =0 bl oc kI dx .y =1 gridDim.x=2 gr id Di m. y= 2 各スレッドが異なるi,jを参照するには 2015/11/25GPGPU実践基礎工学70 Nx=8, Ny=8, x,y方向スレッド数4,ブロック数2 i = blockIdx.x*blockDim.x + threadIdx.x j = blockIdx.y*blockDim.y + threadIdx.y (0,0)(1,0)(2,0)(3,0)(0,0) (3,3) (3,3) (0,1)(1,1)(2,1)(3,1) (0,2)(1,2)(2,2)(3,2) (0,3)(1,3)(2,3)(3,3) (3,3) (0,0) (0,0) block(0,0) block(1,0) block(0,1) block(1,1) i= 0 1 2 3 4 5 6 7 j= 0 1 2 3 4 5 6 7 threadIdx.x,threadIdx.y #include #define Nx (1024) #define Ny (1024) #define Nbytes (Nx*Ny*sizeof(float)) void init(float *a, float *b, float *c){ int i,j,ij; for(i=0;i #define Nx (1024) #define Ny (1024) #define Nbytes (Nx*Ny*sizeof(float)) #define NTx (16) #define NTy (16) #define NBx (Nx/NTx) #define NBy (Ny/NTy) __global__ void init(float *a, float *b, float *c){ int i = blockIdx.x*blockDim.x + threadIdx.x; int j = blockIdx.y*blockDim.y + threadIdx.y; int ij = i*Ny + j; a[ij] = 1.0; b[ij] = 2.0; c[ij] = 0.0; } __global__ void add(float *a, float *b, float *c){ int i = blockIdx.x*blockDim.x + threadIdx.x; int j = blockIdx.y*blockDim.y + threadIdx.y; int ij = i*Ny + j; c[ij] = a[ij] + b[ij]; } int main(void){ float *a,*b,*c; dim3 thread(NTx, NTy, 1); dim3 block(NBx, NBy, 1); cudaMalloc( (void **)&a, Nbytes); cudaMalloc( (void **)&b, Nbytes); cudaMalloc( (void **)&c, Nbytes); init(a,b,c); add(a,b,c); cudaFree(a); cudaFree(b); cudaFree(c); return 0; } ベクトル和(2次元並列版) 2015/11/25GPGPU実践基礎工学72 vectoradd2d.cu 実行結果 GPGPU実践基礎工学73 2次元版 カーネル スレッド数 実行時間[s] init 16×16 735 add 16×16 298 2015/11/25 # CUDA_PROFILE_LOG_VERSION 2.0 # CUDA_DEVICE 0 Tesla M2050 # TIMESTAMPFACTOR fffff5f7a904f250 method,gputime,cputime,occupancy method=[ _Z4initPfS_S_ ] gputime=[ 735.072 ] cputime=[ 16.000 ] occupancy=[ 1.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 298.080 ] cputime=[ 7.000 ] occupancy=[ 1.000 ] 実行結果 GPGPU実践基礎工学74 1次元版(vectoradd.cu) カーネル スレッド数 実行時間[s] init 256(=16×16) 109 add 256(=16×16) 113 # CUDA_PROFILE_LOG_VERSION 2.0 # CUDA_DEVICE 0 Tesla M2050 # TIMESTAMPFACTOR fffff5f7dad0bb38 method,gputime,cputime,occupancy method=[ _Z4initPfS_S_ ] gputime=[ 108.928 ] cputime=[ 15.000 ] occupancy=[ 1.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 112.992 ] cputime=[ 7.000 ] occupancy=[ 1.000 ] 2015/11/25 ベクトル和(2次元並列版)の実行結果 GPGPU実践基礎工学75 実行結果 1次元版と比較して実行時間がかかる initで7倍 addで3倍 遅くなる要因は? 2次元は添字iだけでなくjも計算 添字の計算負荷は軽い 実行時の並列度の指定 2015/11/25 #define Nx (1024) #define Ny (1024) #define Nbytes (Nx*Ny*sizeof(float)) #define NTx (16) #define NTy (16) #define NBx (Nx/NTx) #define NBy (Ny/NTy) __global__ void init(float *a, float *b, float *c){ int i = blockIdx.x*blockDim.x + threadIdx.x; int j = blockIdx.y*blockDim.y + threadIdx.y; int ij = i+ Nx*j; a[ij] = 1.0; b[ij] = 2.0; c[ij] = 0.0; } __global__ void add(float *a, float *b, float *c){ int i = blockIdx.x*blockDim.x + threadIdx.x; int j = blockIdx.y*blockDim.y + threadIdx.y; int ij = i+ Nx*j; c[ij] = a[ij] + b[ij]; } int main(void){ float *a,*b,*c; dim3 thread(NTx, NTy, 1); dim3 block(NBx, NBy, 1); cudaMalloc( (void **)&a, Nbytes); cudaMalloc( (void **)&b, Nbytes); cudaMalloc( (void **)&c, Nbytes); init(a,b,c); add(a,b,c); cudaFree(a); cudaFree(b); cudaFree(c); return 0; } ベクトル和(2次元並列版) 2015/11/25GPGPU実践基礎工学76 vectoradd2d.cu 実行結果 GPGPU実践基礎工学77 2次元版 カーネル スレッド数 実行時間[s] init 16×16 230 add 16×16 153 # CUDA_PROFILE_LOG_VERSION 2.0 # CUDA_DEVICE 0 Tesla M2050 # TIMESTAMPFACTOR fffff5f8098b61e0 method,gputime,cputime,occupancy method=[ _Z4initPfS_S_ ] gputime=[ 230.976 ] cputime=[ 15.000 ] occupancy=[ 1.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 152.960 ] cputime=[ 6.000 ] occupancy=[ 1.000 ] 2015/11/25 2次元的な配列アクセスの優先方向(C言語) 2015/11/25GPGPU実践基礎工学78 2次元配列の場合 in[][],out[][] Nx Ny j i for(i=0;i 2次元的な配列アクセスの優先方向(C言語) 2015/11/25GPGPU実践基礎工学79 2次元配列の場合 for(i=0;i 2次元的な配列アクセスの優先方向(C言語) 2015/11/25GPGPU実践基礎工学80 2次元配列の1次元配列的表現 for(i=0;i 2次元的な配列アクセスの優先方向 2015/11/25GPGPU実践基礎工学81 CUDAで2次元的に並列化してアクセスする場合 i = blockIdx.x*blockDim.x + threadIdx.x; j = blockIdx.y*blockDim.y + threadIdx.y; out[i+Nx*j]=in[i+Nx*j]; in[],out[] Nx Ny j i for(i=0;i 2次元的な配列アクセスの優先方向 GPGPU実践基礎工学82 C言語 メモリアドレスは2次元目が連続 その関係を維持するように配列を1次元化 CUDA C メモリアクセスの際はthreadIdx.xが連続なアドレスにアク セスする方が高速 CUDA Cで2次元的な処理を行うとき 1次元配列は1次元目のメモリアドレスが連続となるように扱っ た方がよい 2015/11/25 2次元配列の1次元表現(C言語) 2015/11/25GPGPU実践基礎工学83 C言語は2次元目が連続 その関係を維持するように1次元化 2次元配列の場合 1次元配列の場合 picutre[1][2] = 11 picture[_______] = 11 Nx=8 1*8 + 2 picture[i][j] picture[i*Ny+j] j i 1 2 3 4 5 9 10 11 12 6 7 8 picture[Nx*Ny] Ny =8 メ モ リ が 連 続 な 方 向 2次元配列の1次元表現(CUDA C) 2015/11/25GPGPU実践基礎工学84 メモリアクセスの際はthreadIdx.xが連続なアドレスに アクセスする方が高速 2次元配列の場合 1次元配列の場合 picutre[1][2] = 11 Nx=8 j i 1 2 3 4 5 9 10 11 12 6 7 8 picture[Nx*Ny] Ny =8 2次元配列の1次元表現(CUDA C) 2015/11/25GPGPU実践基礎工学85 threadIdx.xが連続なアドレスにアクセスする方がメモ リアクセスが高速 2次元配列の場合 1次元配列の場合 picutre[1][2] = 11 picture[_______] = 111 + 8*2 picture[i][j] picture[i+Nx*j] 1 2 3 4 5 9 10 11 12 6 7 8 block(1,0) th re ad Id x. y threadIdx.x block(0,0) block(1,1)block(0,1) メモリが連続な方向 想定されるカーネル 1スレッドが2次元配列の1要素を計算 添字 i,j の要素を担当 2015/11/25GPGPU実践基礎工学86 #define Nx (1920) //水平方向ピクセル数 #define Ny (1080) //垂直方向ピクセル数 __global__ void filter(...){ int i = blockIdx.x*blockDim.x + threadIdx.x; int j = blockIdx.y*blockDim.y + threadIdx.y; picture[i + Nx*j] = ...; } 画像処理の流れ GPGPU実践基礎工学87 変数を宣言し,mallocでメモリを確保 画像を格納する変数 p 処理後の画像を格納する変数 filtered 画像pを作成 pをfilteredにコピーしておく 処理を実行 関数を作成し,pとfilteredを渡す filteredの内容を画面に出力 2015/11/25 メイン関数(CPU版) GPGPU実践基礎工学88 #include #include #define WHITE (255) #define BLACK (0) #define WIDTH 128 #define HEIGHT WIDTH #define Nbytes (WIDTH*HEIGHT*sizeof(unsigned char)) void create(unsigned char *); void copy(unsigned char *,unsigned char *); void print(unsigned char *); int main(void){ unsigned char *p= (unsigned char *)malloc(Nbytes); unsigned char *filtered= (unsigned char *)malloc(Nbytes); //ここで空間フィルタのカーネルを宣言 create(p); copy(p,filtered); //ここで処理を行い,結果をfilteredに格納 2015/11/25 imageproc.c メイン関数(CPU版) GPGPU実践基礎工学89 //画面に各画素の値を表示 print(filtered); return 0; } //画像の内容をコピー void copy(unsigned char *src, unsigned char *dst){ int i; for(i=0;i 画像の作成 GPGPU実践基礎工学90 void create(unsigned char *p){ int i,j; int x_origin,y_origin; for(j=0;j 結果の画面表示とファイル保存 GPGPU実践基礎工学91 print関数が画面に各画素の座標と値を表示 画面出力のファイル表示(出力のリダイレクトを利用) >を利用して保存 $ コマンド > ファイル名 0 0 255 1 0 255 : 126 127 255 127 127 255 $ ./a.out > pic.txt $ cat pic.txt 0 0 255 1 0 255 : 126 127 255 127 127 255 2015/11/25 gnuplotによる結果の表示 GPGPU実践基礎工学92 2次元,3次元データをプロットするアプリケーション コマンドラインで命令を実行してグラフを描画 関数の描画,ファイルから読み込んだデータの表示が可能 tesla??では正しく動作しないため,grouseで実行する こと 2015/11/25 gnuplotによる結果の表示 2015/11/25GPGPU実践基礎工学93 表示可能なファイルフォーマット データはスペース区切り 3次元表示では列(または行)の区切りとして空白行が必要 0 0 255 1 0 255 ... 126 0 255 127 0 255 gnuplotによる結果の表示 2015/11/25GPGPU実践基礎工学94 実行する命令 #表示設定(画像っぽく表示するための設定) set xrange[0:127] #x軸の表示範囲を0~127(画像サイズ)に固定 set yrange[0:127] #y軸の表示範囲を0~127(画像サイズ)に固定 set cbrange[0:255] #カラーバーの範囲を0~255に固定 set size square #画像を正方形で表示 set pm3d map #カラー表示して真上から表示 set palette define(0"black",255"white") #0を黒,255を白 #設定を反映してファイルを読み込み #(ファイル名はシングルクオートで囲む) splot 'pic.txt' plot.txt 画像処理 GPGPU実践基礎工学 比較的簡単な4種類の処理を実装 ネガティブ処理 画像反転 水平反転,垂直反転 空間フィルタ ぼかし,輪郭抽出 モザイク処理 95 2015/11/25 ネガティブ処理 2015/11/25GPGPU実践基礎工学96 色の反転処理 黒(0)を白(255),白(255)を黒(0)に反転 (255−各画素の値)を新しい画素の値とする 原画像 処理画像 void negative(unsigned char *p, unsigned char *filtered){ int i,j; for(j=0;j 水平反転 2015/11/25GPGPU実践基礎工学98 画像の幾何的位置を反転 水平反転 i→(WIDTH‐1)‐i 垂直反転 j→(HEIGHT‐1)‐j 原画像 処理画像 j HE IG HT ‐1 i0 WIDTH‐1 垂直反転 2015/11/25GPGPU実践基礎工学99 画像の幾何的位置を反転 水平反転 i→(WIDTH‐1)‐i 垂直反転 j→(HEIGHT‐1)‐j 原画像 処理画像 j HE IG HT ‐1 i0 WIDTH‐1 void hreflect(unsigned char *p, unsigned char *reflected){ int i,j,iref; for(j=0;j void vreflect(unsigned char *p, unsigned char *reflected){ int i,j,jref; for(j=0;j 空間フィルタ 2015/11/25GPGPU実践基礎工学102 ある画素とその周囲の画素を使って処理 処理の仕方を規定したカーネルを定義 カーネルは1次元配列で表現 原画像 輪郭抽出 0 1 0 1 ‐4 1 0 1 0 = b+d‐4e+f+h フィルタ (カーネル) a b c d e f g h i 空間フィルタ GPGPU実践基礎工学103 カーネルは1次元配列で表現し,引数で渡す ぼかし(平均フィルタ) 輪郭抽出(ラプラシアンフィルタ) 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 1/9 0 1 0 1 ‐4 1 0 1 0 float blur[9] ={1.0f/9.0f,1.0f/9.0f,1.0f/9.0f, 1.0f/9.0f,1.0f/9.0f,1.0f/9.0f, 1.0f/9.0f,1.0f/9.0f,1.0f/9.0f}; float laplacian[9] ={ 0.0f, 1.0f, 0.0f, 1.0f,‐4.0f, 1.0f, 0.0f, 1.0f, 0.0f}; 2015/11/25 void boxfilter(unsigned char *p, unsigned char *filtered, float *filter){ int i,j int result = BLACK; //端では何もしない for(j=1;j モザイク処理 2015/11/25GPGPU実践基礎工学105 画像を小さな領域に分け,その領域を全て同じ色にする 領域内の全画素を,領域内の画素の平均値に置き換える 原画像 処理画像 void mosaic(unsigned char *f,unsigned char *filtered, int mosaicSize){ int i,j, isub,jsub; int average; for(j=0;j GPUへの移植の方針 GPGPU実践基礎工学 画像の作成はCPUで行い,GPUへ転送 1スレッドが1画素の処理を実行 2重のforループを排除し,配列添字とスレッド番号を対応 空間フィルタ 端の画素の処理ができないので今回は処理しない モザイク 領域の大きさ(横×縦)は1ブロックと同じとする 領域内の画素の平均値の計算は非常に難易度が高い threadIdx.x=0,threadIdx.y=0のスレッドが平均を計算してメモ リに書き出す 107 2015/11/25 メイン関数(GPU版) GPGPU実践基礎工学108 #include #include #define WHITE (255) #define BLACK (0) #define WIDTH 4096 #define HEIGHT WIDTH #define Nbytes (WIDTH*HEIGHT*sizeof(unsigned char)) #define THREADX 16 #define THREADY 16 #define BLOCKX (WIDTH/THREADX) #define BLOCKY (HEIGHT/THREADY) void create(unsigned char *); void print(unsigned char *); int main(void){ unsigned char *p= (unsigned char *)malloc(Nbytes); unsigned char *dev_p, *dev_filtered; cudaMalloc( (void **)&dev_p, Nbytes); cudaMalloc( (void **)&dev_filtered, Nbytes); 2015/11/25 imageproc.cu メイン関数(GPU版) GPGPU実践基礎工学109 //ここで空間フィルタのカーネルを宣言 float *filter; cudaMalloc( (void **)&filter, sizeof(float)*9); cudaMemcpy(filter, フィルタのカーネル, sizeof(float)*9, cudaMemcpyHostToDevice); create(p); //CPUで画像を生成してGPUへ送った後,変数dev_filteredにコピーしておく cudaMemcpy(dev_p, p, Nbytes, cudaMemcpyHostToDevice); cudaMemcpy(dev_filtered, dev_p, Nbytes, cudaMemcpyDeviceToDevice); dim3 block( BLOCKX, BLOCKY, 1), thread(THREADX, THREADY, 1); //ここで処理を行い,結果をdev_filteredに格納 //dev_filteredの内容をCPUへ送る unsigned char *filtered = (unsigned char *)malloc(Nbytes); cudaMemcpy(filtered, dev_filtered, Nbytes, cudaMemcpyDeviceToHost); free(p); free(filtered); cudaFree(dev_p); cudaFree(dev_filtered); cudaFree(filter); return 0; } 2015/11/25 imageproc.cu メイン関数(GPU版) GPGPU実践基礎工学110 void create(unsigned char *p){ int i,j, x_origin,y_origin; for(j=0;j __global__ void negative(unsigned char *p, unsigned char *filtered){ int i,j; i = blockIdx.x*blockDim.x + threadIdx.x; //スレッド数と配列添字の対応 j = blockIdx.y*blockDim.y + threadIdx.y; // filtered[i+WIDTH*j] = WHITE ‐ p[i + WIDTH*j]; } ネガティブ処理 GPGPU実践基礎工学111 2015/11/25 imageproc.cu __global__ void hreflect(unsigned char *p, unsigned char *reflected){ int i,j; int iref; i = blockIdx.x*blockDim.x + threadIdx.x; j = blockIdx.y*blockDim.y + threadIdx.y; iref = (WIDTH‐1)‐i; //反転後のx座標値 reflected[iref+WIDTH*j] = p[i + WIDTH*j]; } 水平反転 GPGPU実践基礎工学112 2015/11/25 imageproc.cu __global__ void vreflect(unsigned char *p, unsigned char *reflected){ int i,j; int jref; i = blockIdx.x*blockDim.x + threadIdx.x; j = blockIdx.y*blockDim.y + threadIdx.y; jref = (HEIGHT‐1)‐j; //反転後のy座標値 reflected[i+WIDTH*jref] = p[i + WIDTH*j]; } 垂直反転 GPGPU実践基礎工学113 2015/11/25 imageproc.cu __global__ void boxfilter(unsigned char *p, unsigned char *filtered, float *filter){ int i,j; int result = BLACK; i = blockIdx.x*blockDim.x + threadIdx.x; j = blockIdx.y*blockDim.y + threadIdx.y; if(0 __global__ void mosaic(unsigned char *p, unsigned char *filtered, int mosaicSize){ int i,j, isub,jsub; int average; i = blockIdx.x*blockDim.x + threadIdx.x; j = blockIdx.y*blockDim.y + threadIdx.y; if(threadIdx.x == 0 && threadIdx.y == 0){//ブロック内の1スレッドのみが処理 //領域内の画素の平均値を計算 average = 0; for(jsub = 0; jsub 処理時間の比較 GPGPU実践基礎工学116 画像サイズ 4096×4096 1ブロックあたりのスレッド数 16×16 モザイクのサイズはスレッド数と同じ(16×16) 処理 処理時間[ms] CPU GPU ネガティブ処理 175 1.17 水平反転 187 1.18 垂直反転 185 1.18 空間フィルタ 553 4.13 モザイク処理 260 38.5 補足 ビットマップ画像のフォーマット ビットマップ画像 2015/11/25GPGPU実践基礎工学119 画像を画素(pixel)の集合として捉え,各画素の色を3 原色(赤緑青,RGB)で表す 1画素あたり1バイト~4バイト jpg画像のように画像圧縮を施さないのでデータ量が画 像サイズに比例して増大 圧縮形式もあるがあまり利用されない zip形式等のファイル圧縮を使うと効果的に圧縮可能 ビットマップファイルフォーマット 2015/11/25GPGPU実践基礎工学120 ファイルの情報を表すヘッダ+画像の各画素情報 ヘッダ ファイルヘッダ+情報ヘッダの合計54バイト ファイルヘッダ ファイルの種類,ファイルサイズ等 情報ヘッダ 画像の幅,高さ,1画素あたりのデータサイズなど 画像の各画素の情報 ビットマップファイルフォーマット 2015/11/25GPGPU実践基礎工学121 24bitビットマップファイルのバイナリダンプ ファイルヘッダ 情報ヘッダ 画像情報 1画素の情報 ビットマップファイルフォーマット 2015/11/25GPGPU実践基礎工学122 ファイルヘッダ エンディアン(endian, 多バイトデータの記述・格納形式) 内容 サイズ 備考 ファイルの種類 u2 ビットマップの場合はBM ファイルサイズ(byte) u4 16進数表記(リトルエンディアン,下位 ビットが先に書かれる) 予約領域1 u2 必ず0 予約領域2 u2 必ず0 ファイル先頭から画像までの オフセット u4 windowsビットマップでは54 (16進数表記で36) 36 00 03 00 リトルエンディアン 00 03 00 3616=196,662 byte ビッグエンディアン 36 00 03 0016=905,970,432 byte ビットマップファイルフォーマット 2015/11/25GPGPU実践基礎工学123 情報ヘッダ 内容 byte数 備考 情報ヘッダのサイズ u4 必ず40(byte, 16進数表記で28) 画像の幅(pixel) 4 16進数表記 画像の高さ(pixel) 4 16進数表記 プレーン数 u2 必ず1 色ビット数(bit/pixel) u2 1,4,8,24,32(16進数表記) 圧縮形式 u4 0,1,2,3のいずれか 画像データサイズ u4 16進数表記 水平解像度(dot/m) 4 0の場合もある 垂直解像度(dot/m) 4 0の場合もある パレット数(使用色数) u4 0の場合もある 重要な色の数 u4 0の場合もある ビットマップファイルフォーマット 2015/11/25GPGPU実践基礎工学124 画像情報(24bitビットマップ) 3原色の情報をそれぞれ1バイト(unsigned char)で保持 3原色の並び順はBGR 幅は行単位で4バイトの倍数に揃えられる 画素の並びは左下から右方向,下から上方向 B G R
Comments
Copyright © 2024 UPDOCS Inc.