マルチスレッドで画像の拡大縮小を高速化

このページでは、マルチスレッドによる画像の拡大縮小の高速化方法およびそのC言語プログラムについて解説します。画像処理はたくさんの計算が必要になるものが多いため、マルチスレッドによる処理の高速化の恩恵が大きいです。

マルチスレッドによる画像の拡大縮小

まずマルチスレッドによる画像の拡大縮小について解説します。

マルチスレッドとは、下のページで解説しているように大きな仕事をもっと小さな単位の仕事に分割することで、複数のコアで同時に仕事を処理できるようにするものです。コア数が複数あるのであれば、このコアの同時処理により高速化が望めます。

徹底図解!入門者向け!C言語でのマルチスレッドをわかりやすく解説

画像の拡大縮小においては、この大きな仕事が「画像全体の拡大縮小」となります。

マルチスレッドでは、さらにこれをもっと小さな単位の仕事に分割します。

スレッドの分け方

では、どのように分割するかというと、今回は画像の領域を縦方向にN等分(Nはスレッド数)し、「画像全体の拡大縮小」を「画像の1/Nの領域の拡大縮小」というN個の小さな仕事に分割します。例えばスレッド数を4とした場合は、下記のように4つの仕事(スレッド)を作成することになります。

  • 仕事1:画像の上から1番目の領域の拡大縮小
  • 仕事2:画像の上から2番目の領域の拡大縮小
  • 仕事3:画像の上から3番目の領域の拡大縮小
  • 仕事4:画像の上から4番目の領域の拡大縮小

図で表すと下のような感じです。

スポンサーリンク

プログラム

画像の拡大縮小時の補間方法としては最近傍補間を用いています。マルチスレッドを用いていないプログラムは下のページで紹介していますので、こちらのプログラムと比較してもらえれば違いがわかりやすいと思います。

C言語で画像の拡大縮小(最近傍補間編)

動作はMac OSで確認していますが、おそらくLinuxでも動作すると思います。pthreadが使えない環境だとコンパイルができないので注意してください。

プログラム例

画像の拡大縮小をpthreadを用いて実装した時のプログラムは下記のようになります。

multiresize.c
#include "myJpeg.h"
#include <time.h>
#include <pthread.h>

/* スレッドの数 */
#define NUM_THREAD 4

/* スレッドで参照するデータ */
struct thread_data{
  /* 拡大後画像の情報および拡大後RAWデータ格納用 */
  RAWDATA_t *output;
  /* 入力画像の情報 */
  RAWDATA_t *input;
  /* 拡大率 */
  double scaleW;
  double scaleH;
  /* このスレッドで処理する範囲 */
  int startm;
  int endm;
  int startn;
  int endn;
};

void *resize(void *arg);

/* スレッドで実行する関数 */
void *resize(void *arg){
  int m, n, c;
  int m0, m1, n0, n1;
  double originalm, originaln;
  double dm, dn;
  struct thread_data *thdata = (struct thread_data*)arg;

  /* このスレッドの処理する範囲に対して最近傍補間で画像拡大 */
  for(n = thdata->startn; n < thdata->endn; n++){
    for(m = thdata->startm; m < thdata->endm; m++){
      for(c = 0; c < thdata->input->ch; c++){

        /* 入力画像における横方向座標を最近傍補間で算出 */
        originalm = (double)m / (double)thdata->scaleW;
        m0 = (int)originalm;
        dm = originalm - m0;
        m1 = m0 + 1;
        if(m1 == thdata->input->width) m1 = thdata->input->width - 1;

        /* 入力画像における縦方向座標を最近傍補間で算出 */
        originaln = (double)n / (double)thdata->scaleH;
        n0 = (int)originaln;
        dn = originaln - n0;
        n1 = n0 + 1;
        if(n1 == thdata->input->height) n1 = thdata->input->height - 1;

        /* 最近傍補間で算出した座標のRGB値を拡大後画像用のメモリに格納 */
        thdata->output->data[thdata->output->ch * (m + n * thdata->output->width) + c]
          = thdata->input->data[thdata->input->ch * (m1 + n1 * thdata->input->width) + c] * dm * dn
          + thdata->input->data[thdata->input->ch * (m1 + n0 * thdata->input->width) + c] * dm * (1 - dn)
          + thdata->input->data[thdata->input->ch * (m0 + n1 * thdata->input->width) + c] * (1- dm) * dn
          + thdata->input->data[thdata->input->ch * (m0 + n0 * thdata->input->width) + c] * (1 -dm) * (1 - dn);
      }
    }
  }
  return NULL;
}

int main(int argc, char *argv[]){
  /* 画像のRAWデータ情報 */
  RAWDATA_t raw, scaledRaw;
  /* 拡大率 */
  double scaleW, scaleH;
  /* 出力ファイル名 */
  char outname[256];
  /* スレッドのインデックス */
  int n;
  /* スレッド */
  pthread_t thread[NUM_THREAD];
  /* スレッドで参照するデータ */
  struct thread_data thdata[NUM_THREAD];
  /* 処理時間計測用 */
  time_t start, end;

  /* 引数チェック */
  if(argc != 4){
    printf("ファイル名、幅方向拡大率、高さ方向拡大率の3つを引数に指定してください\n");
    return -1;
  }

  /* 引数から拡大率を取得 */
  scaleW = atof(argv[2]);
  scaleH = atof(argv[3]);

  /* 入力jpegファイル読み込んで情報rawへ格納 */
  if(jpegFileReadDecode(&raw, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

  /* 入力画像の情報と拡大率から拡大後の画像の情報を格納 */
  scaledRaw.width = scaleW * raw.width;
  scaledRaw.height = scaleH * raw.height;
  scaledRaw.ch = raw.ch;

  /* パラメータチェック */
  if(scaledRaw.width == 0 || scaledRaw.height == 0){
    printf("拡大縮小後の幅もしくは高さが0です\n");
    freeRawData(&raw);
    return -1;
  }

  /* 拡大後画像のRAWデータ格納用のメモリ確保 */
  scaledRaw.data = (unsigned char*)malloc(sizeof(unsigned char) * scaledRaw.width * scaledRaw.height * scaledRaw.ch);
  if(scaledRaw.data == NULL){
    printf("malloc scaledRaw error\n");
    freeRawData(&raw);
    return -1;
  }

  /* スレッドで参照するためのデータを格納 */
  for(n = 0; n < NUM_THREAD; n++){
    /* 出力画像の情報および拡大後画像のRAWデータ格納用 */
    thdata[n].output = &scaledRaw;
    /* 入力画像の情報 */
    thdata[n].input = &raw;
    /* 横方向拡大率 */
    thdata[n].scaleW = scaleW;
    /* 縦方向拡大率 */
    thdata[n].scaleH = scaleH;
    /* このスレッドで処理する横方向の開始点 */
    thdata[n].startm = 0;
    /* このスレッドで処理する横方向の終了点 */
    thdata[n].endm = scaledRaw.width;
    /* このスレッドで処理する横縦方向の開始点 */
    thdata[n].startn = n * scaledRaw.height / NUM_THREAD;
    /* このスレッドで処理する縦方向の終了点 */
    thdata[n].endn = (n+1) * scaledRaw.height / NUM_THREAD;
  }
  /* 画像の高さがNUM_THRADの倍数でない場合の微調整 */
  thdata[NUM_THREAD-1].endn = scaledRaw.height;

  start = time(NULL);

  /* NUM_THREAD個ののスレッドを生成 */
  /* スレッドで実行するのresize関数 */
  for(n = 0; n < NUM_THREAD; n++){
    pthread_create(&thread[n], NULL, resize, &thdata[n]);
  }

  /* NUM_THREAD個のスレッドの終了まで待つ */
  for(n = 0; n < NUM_THREAD; n++){
    pthread_join(thread[n], NULL);
  }

  end = time(NULL);
  printf("processing time:%ld[s]\n", end - start);

  /* 拡大後画像の出力ファイル名を設定 */
  sprintf(outname, "%s", "linear.jpeg");

  /* 拡大後画像をJPEG形式でファイル出力 */
  if(jpegFileEncodeWrite(&scaledRaw, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeRawData(&scaledRaw);
    freeRawData(&raw);
    return -1;
  }

  /* 確保したメモリを解放 */
  freeRawData(&scaledRaw);
  freeRawData(&raw);

  return 0;
}

プログラムコンパイル方法

必要なファイルは3つです。1つは上記の「multiresize.c」です。後の2つは下のページで紹介している「myJpeg.c」と「myJpeg.h」です。またlibjpegをインストールしておく必要があります。libjpegをインストールしておくとC言語でJPEGファイルを使ったプログラムが簡単に作れるようになりますので興味があればインストールしておくことをオススメします。

gccを用いたコマンドラインからのコンパイルは下記で行うことができます。

gcc myJpeg.c -c
gcc multiresize.c -c
gcc myJpeg.o multiresize.o -ljpeg -lpthread -o multiresize.exe

-ljpegを付けることでlibjpegライブラリを、-lpthreadを付けることでpthreadライブラリをそれぞれリンクしています。

実行方法は下記のように、第一引数に入力JPEGファイル名、第二引数に横方向の拡大率、第三引数に縦方向の拡大率を指定してください。

./multiresize.exe cat.jpeg 2 2

プログラムの解説

ではプログラムのポイントを解説していきます。

マルチスレッド以外の部分

JPEGの入出力や最近傍補間による座標の計算方法等のマルチスレッド以外の部分については下のページで解説していますのでこちらを参考にしてください。

C言語で画像の拡大縮小(最近傍補間編)

スレッド数

スレッド数は下記で定義しています。このプログラムでは4つのスレッドを生成しています。マルチスレッドによる高速化の確認では、この定義を変更したプログラムで処理時間の比較を行っています。

/* スレッドの数 */
#define NUM_THREAD 4

スレッドで参照するデータの生成

スレッドで参照できるデータは1つのみ(スレッドで処理する関数の引数はvoid *型の1つの変数のみ)ですので、スレッドが処理を行うのに必要なデータを詰め込むための構造体を定義しています。下記がその構造体です。

/* スレッドで参照するデータ */
struct thread_data{
  /* 拡大後画像の情報および拡大後RAWデータ格納用 */
  RAWDATA_t *output;
  /* 入力画像の情報 */
  RAWDATA_t *input;
  /* 拡大率 */
  double scaleW;
  double scaleH;
  /* このスレッドで処理する範囲 */
  int startm;
  int endm;
  int startn;
  int endn;
};

例えばstartm, endm, startn, endnはそのスレッドで処理を行う領域を示すものになります。

また入力画像のRAWデータへのポインタや入力画像のサイズなどはinputに格納されています。

さらに、出力画像のRAWデータへのポインタはoutputに格納されています。スレッドにより拡大縮小を行なったデータを、この出力画像のRAWデータへのポインタの先に格納することで、スレッドでの処理結果をスレッドを生成するメインスレッドに伝えることができます。

この構造体への値の設定はmain関数内の下記で行なっています。

  /* スレッドで参照するためのデータを格納 */
  for(n = 0; n < NUM_THREAD; n++){
    /* 出力画像の情報および拡大後画像のRAWデータ格納用 */
    thdata[n].output = &scaledRaw;
    /* 入力画像の情報 */
    thdata[n].input = &raw;
    /* 横方向拡大率 */
    thdata[n].scaleW = scaleW;
    /* 縦方向拡大率 */
    thdata[n].scaleH = scaleH;
    /* このスレッドで処理する横方向の開始点 */
    thdata[n].startm = 0;
    /* このスレッドで処理する横方向の終了点 */
    thdata[n].endm = scaledRaw.width;
    /* このスレッドで処理する横縦方向の開始点 */
    thdata[n].startn = n * scaledRaw.height / NUM_THREAD;
    /* このスレッドで処理する縦方向の終了点 */
    thdata[n].endn = (n+1) * scaledRaw.height / NUM_THREAD;
  }
  /* 画像の高さがNUM_THRADの倍数でない場合の微調整 */
  thdata[NUM_THREAD-1].endn = scaledRaw.height;

スレッドの生成

スレッドの生成は下記で行なっています。

  /* NUM_THREAD個ののスレッドを生成 */
  /* スレッドで実行するのresize関数 */
  for(n = 0; n < NUM_THREAD; n++){
    pthread_create(&thread[n], NULL, resize, &thdata[n]);
  }

第三引数で拡大縮小を行うresize関数を渡し、第四引数でresize関数を実行するのに必要になるデータをのポインタを渡しています。

スレッドで処理する関数

スレッドで処理する関数resizeは、画像の全体ではなく引数により与えられた範囲のみを拡大縮小するようにしています。

  /* このスレッドの処理する範囲に対して最近傍補間で画像拡大 */
  for(n = thdata->startn; n < thdata->endn; n++){
    for(m = thdata->startm; m < thdata->endm; m++){

スレッドの同期

下記でpthread_join関数により同期処理を行っています。全てのスレッドが終了するまでここで待機し、全てのスレッドが終了したらJPEGファイルの出力を行うように同期しています。

  /* NUM_THREAD個のスレッドの終了まで待つ */
  for(n = 0; n < NUM_THREAD; n++){
    pthread_join(thread[n], NULL);
  }

マルチスレッドによる高速化の確認

ここでは上記プログラムを用い、スレッド数を変更した時に処理時間がどのように変化するかを見ていきたいと思います。

処理時間の測り方

上記でプログラムで作成した実行ファイルに対し、640 x 427のJPEG入力ファイルを縦横30倍に拡大します。実行時のスレッド生成前から全スレッド終了後までの時間を計測しています。

実行環境

私のPCのCPUはコア数2・スレッド数4になります。つまり、CPU全体で同時に実行できるスレッドは4つです。なので、スレッド数4まではスレッドを増やすほど処理時間が短くなり、スレッド数5以上ではほぼ処理時間が変わらないと予想できます。

計測結果

実際の計測結果は下記の通りです。

スレッド数処理時間対スレッド数1
140秒1
222秒0.55
316秒0.4
412秒0.3
512秒0.3
612秒0.3

まあ大体結果は予想通りですね!

同期の効果の確認

プログラムの解説で説明したように、このプログラムではpthread_join関数により同期処理を行っています。この同期をなくすとどうなるでしょうか?実際に実行してみました!出力画像は下記のようになりました。

同期をなくしたので、各スレッドが終了を待たずにJPEGファイルの出力が行われるようになります。なので、各スレッドで途中までだけ拡大縮小処理した結果のJPEGファイルが出力されてしまいます(絵がある部分は処理が終わった部分、黒色の部分は処理が行われていない部分)。この絵を見ると、各スレッドの処理の進み具合に差があることが確認できますね。

同期を入れるとこのような画像ではなく、全画素が処理された画像が出力されます。このことより同期がしっかり動作し、その効果が出ていることが分かります。

スポンサーリンク

まとめ

このページでは画像の拡大縮小をマルチスレッド処理するプログラムの説明を行いました。画像全体の拡大縮小を画像の一部分のみの拡大縮小という小さな仕事に分割することで、処理が並列実行され高速化できたことを確認できたと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です