【C言語】ターミナルでBMP画像ファイルのスライドショーをした
コンテンツ
はじめに
前回ターミナルで画像を表示したので今回は、スライドショーのようにディレクトリの中のBMP(対応しているもののみ)を全て表示するプログラムを作成しました。画像のサイズが大きいと読み込みに時間がかかっていたので、読み込みの部分だけ並列処理にしました。以下が実行例ですが、320×180程度の大きさだと逐次的に読み込む方法とほとんど大差ありませんでした。
画像の読み込みを並列処理(別スレッドで読み込み)
逐次的に画像を読み込む
ターミナルでBMP画像ファイルのスライドショー
並列処理のコードを書いたのは初めてだったので、並列処理について簡単にまとめてそのしたにプログラムの簡単な説明をしています。
並列処理
並列処理とはその名の通り、複数の処理を同時に行うことです。シングルコア(独立して稼働するプロセッサコアが1つ)のCPUでは、以下で説明するマルチスレッドやマルチプロセスで並列処理にしようとしても、逐次処理になってしまいます。一定間隔で見れば同時に処理されている(CPUがごく短時間で処理する仕事を変えているので)ように見えるかもしれませんが、一瞬を見ればCPUは一つの仕事しかしていません。シングルコアのCPUで並列処理をするには、CPU自体を複数用意して行うしかありません。一方、マルチコアのCPUでは仕事をするプロセッサが実際に複数あるので、本当に同時に処理ができます。
並列処理を実装するには以下のように大きく二つあります。
- マルチプロセス
- 共有メモリ
- セマフォ
- マップドメモリ
- パイプ、FIFO
- ソケット通信
- マルチスレッド
- データの整合性
- デッドロック
プロセスとはメモリ上にある動いているプログラムです。プロセスは、仮想メモリのマッピングや実行コンテキスト等のプログラムを実行するのに必要な情報を持っています。また、プロセスはOSにお願いをして新しい仮想メモリのマッピングを持つ自分のコピー(子プロセス)を作成できます。これを用いて、複数のプロセスで並列処理が可能です(マルチプロセス)。
Unix系のOSでは、fork()を行うことで子プロセスを作成できます。子プロセスは上記のとおり、親プロセス(子プロセスを作成したプロセス)とは別の仮想メモリのマッピングをもつので親プロセスとデータのやり取りをするには以下の手段を使う必要があります(今回は羅列だけ)。
スレッドとは、プロセスよりも小さいプログラムの実行単位です。プロセスは1つ以上のスレッドから成ります。スレッドはプログラムの実行に必要な情報をもち、OSがCPUの時間を割り当てる単位です。
マルチスレッドはUnix系のOSでは、pthreadライブラリやOpenMPを用いれば実現できます。1つのプロセスが複数のスレッドを持つとき、そのスレッドどうしはメモリを共有しています。メモリを共有しているためデータのやりとりに便利な反面以下のようなことに気を付けなければなりません。
データに同時にアクセスしたりすると期待する値にならなかったりするため、順番にアクセスするように排他制御が必要になります。
すべてのスレッドが、他のスレッドの処理待ちとなり全く処理が進まなくなること。
プロセスとスレッドの関係イメージ

方針
今回のプログラムはディレクトリ中のBMPファイルを次々表示するだけのプログラムです。画像の読み込みには時間がかかるため、画像を表示している間(表示して2秒sleep)に画像の読み込みができるようにマルチスレッドで並列処理を実現します。以下がフローチャートです。

プログラムを作る
フローチャートのように、前回の画像の表示を引数回数行います。表示する画像は引数で指定されたディレクトリのBMPファイルです。コードは以下の通りです。
前回の画像表示にファイル名取得(83~96行)、別のスレッドでの画像の読み込み(136,137)を追加して81行目からのfor文で表示回数分ループしています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <pthread.h>
#define INTERVALMICROSEC 2000000
#define CLEARSCREEN "\e[2J"
#define COUSORTOLEFTTOP "\e[0;0H"
pthread_mutex_t mutex;
int counter;
typedef struct {
unsigned char r; // 符号なしじゃないと整数に変換するとき符号拡張で負の値になる数値がある
unsigned char g;
unsigned char b;
} pixelColor;
typedef struct {
int height;
int width;
pixelColor *pixel;
} imgdata;
typedef struct {
FILE *fp;
imgdata *img;
} threadArg;
/* Main */
int main(int argc, char *argv[]){
DIR *dir;
struct dirent *dp;
FILE *fp; // 開くファイルのポインタ
unsigned int imgOffset, headerSize, compType = 0;
short pixelBit = 0;
imgdata *img[3] = {NULL, NULL, NULL};
int tmp, maxshow, i, j;
char *path = NULL;
threadArg *targ = NULL;
pthread_t pthread;
pthread_mutex_init(&mutex, NULL);
targ = (threadArg *)malloc(sizeof(threadArg));
// コマンドライン引数チェック
if (argc != 3) {
printf("引数の数が違います。\n使い方\n%s ディレクトリ 最大ループ数\n", argv[0]);
return 0;
}
// ディレクトリを開く
dir = opendir(argv[1]);
if (dir == NULL){
printf("ディレクトリがありません\n");
return 0;
}
// maxshow数を格納
maxshow = atoi(argv[2]) + 1;
// 最初のファイル名を取得
do{
dp = readdir(dir);
} while (dp != NULL && isExtensionBmp(dp->d_name) == NG);
if (dp == NULL){
printf("指定されたディレクトリにBMPファイルがありません\n");
return 0;
}
path = joinDirFilename(argv[1], dp->d_name);
if (path == NULL){
//printf("%s\n", dp->d_name);
printf("エラー\n");
return 0;
}
// 画像をひたすら表示
for(i = 0; i < maxshow; i++){
// ファイル名を取得
if(i > 0){
do{
dp = readdir(dir);
if( dp == NULL ){ // ループしたいので終端にいったらオフセットを0にして先頭に戻す
seekdir(dir, 0L);
dp = readdir(dir);
}
} while (isExtensionBmp(dp->d_name) == NG);
path = joinDirFilename(argv[1], dp->d_name);
if (path == NULL){
printf("エラー\n");
return 0;
}
}
// ファイルを開く
if ( (fp = fopen(path, "rb")) == NULL) {
printf("指定されたファイルを開けませんでした\n");
return 0;
}
// BMPファイルかチェック
tmp = isBmpFile(fp);
if(tmp == NG){
printf("BMPファイルでないです\n");
return 0;
}else if( tmp == SBRERR){
printf("BMPファイルかのチェックでエラーが発生\n");
return 0;
}
// 情報ヘッダのサイズ
readXbyte(fp, &headerSize, 14L, 4L);
if (headerSize <= 12){
printf("未対応の情報ヘッダのBMPファイルです\n");
return 0;
}
// 1pixelあたりのbit数
readXbyte(fp, &pixelBit, 28L, 2L);
// 圧縮タイプ
readXbyte(fp, &compType, 30L, 4L);
if ( pixelBit != 24 || compType != 0 ){
printf("未対応のBMPファイルです\n");
return 0;
}
// 画像を読み込む
if(i > 0){ // i > 0 は 別スレッドで画像を読み込む
if ( img[i % 3] != NULL)free(img[i % 3]);
img[i % 3] = (imgdata *)malloc(sizeof(imgdata));
targ->fp = fp;
targ->img = img[i % 3];
pthread_create(&pthread, NULL, &readImgdatathread, targ);
pthread_detach(pthread);
}else{
if ( img[i % 3] != NULL)free(img[i % 3]);
img[i % 3] = (imgdata *)malloc(sizeof(imgdata));
// イメージデータセットのオフセットを取得
readXbyte(fp, &imgOffset, 10L, 4L);
// 画像の高さと幅を取得
readXbyte(fp, &(img[i]->width), 18L, 4L);
readXbyte(fp, &(img[i]->height), 22L, 4L);
// 画像データを読み込む
readImgdata(img[i], fp, imgOffset);
counter += 1;
fclose(fp);
// 読み込みのストックしたいので
continue;
}
// 画面クリア → 画像を表示
if ( counter == 0 ){
// スレッドをデタッチしたらjoinできないので 適当な秒数まつ
for(j = 0; j < 100; j++){
sleep(1);
if (counter > 0)break;
}
}
write(STDOUT_FILENO, CLEARSCREEN, sizeof(CLEARSCREEN));
write(STDOUT_FILENO, COUSORTOLEFTTOP, sizeof(COUSORTOLEFTTOP));
printImage(img[(i - 1) % 3]);
// 読み込んだ画像数 (減らす)
pthread_mutex_lock(&mutex);
counter -= 1;
pthread_mutex_unlock(&mutex);
// スリープ
usleep(INTERVALMICROSEC);
// ファイルクローズ
fclose(fp);
}
if(img[0] != NULL)free(img[0]);
if(img[1] != NULL)free(img[1]);
if(img[2] != NULL)free(img[2]);
if(targ != NULL)free(targ);
return 0;
}
/* 別スレッドで画像データを読み込む
* 引数 : ファイルポインタとimgdataを持つ構造体
*/
void *readImgdatathread(void *arg){
threadArg *ptr;
imgdata *img;
FILE *fp;
unsigned int imgOffset;
ptr = (threadArg *)arg;
img = ptr->img;
fp = ptr->fp;
// イメージデータセットのオフセットを取得
readXbyte(fp, &imgOffset, 10L, 4L);
// 画像の高さと幅を取得
readXbyte(fp, &(img->width), 18L, 4L);
readXbyte(fp, &(img->height), 22L, 4L);
// 画像データを読み込む
readImgdata(img, fp, imgOffset);
// 読み込んだ画像数
pthread_mutex_lock(&mutex);
counter += 1;
pthread_mutex_unlock(&mutex);
return 0;
}

