【C言語】マルチスレッドで画像の拡大縮小を高速化

このページでは、マルチスレッドによる画像の拡大縮小の高速化方法およびそのC言語プログラムについて解説します。

画像処理はたくさんの計算が必要になるものが多いため、マルチスレッドによる処理の高速化の恩恵が大きいです。

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

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

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

コア数が複数あるのであれば、このコアの同時処理により高速化が望めます。

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

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

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

スレッドの分け方

では、どのように分割するかというと、今回は画像の領域を縦方向に N 等分(N はスレッド数)し、「画像全体の拡大縮小」を「画像の 1/N の領域の拡大縮小」という N 個の小さな仕事に分割します。

例えばスレッド数を 4 とした場合は(つまり 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{
  /* 拡大後画像の情報および拡大後BITMAPデータ格納用 */
  BITMAPDATA_t *output;
  /* 入力画像の情報 */
  BITMAPDATA_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[]){
  /* 画像のBITMAPデータ情報 */
  BITMAPDATA_t bitmap, scaledBitmap;
  /* 拡大率 */
  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ファイル読み込んで情報bitmapへ格納 */
  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

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

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

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

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

  /* 確保したメモリを解放 */
  freeBitmapData(&scaledBitmap);
  freeBitmapData(&bitmap);

  return 0;
}

プログラムのコンパイル

コンパイルを行う上で必要なソースコードファイルは下記の3つです。

  • multiresize.c:このページで紹介しているソースコード
  • myJpeg.c:JPEG 読み込み・書き込み用のソースコード
  • myJpeg.h:JPEG 読み込み・書き込みようのヘッダーファイル

myJpeg.cmyJpeg.h は下記ページで公開していますので、コピペして同じファイル名で保存して使用していただければと思います。

1888

また JPEG の読み込みと書き込みを行うため、libjpeg をインストールしておく必要があります。

libjpeg をインストールしておくとC言語でJPEGファイルを使ったプログラムが簡単に作れるようになりますので興味があればインストールしておくことをオススメします。

この libjpeg のインストール方法についても上記のページで紹介していますので、こちらを参考にしてインストールしていただければと思います。

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

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

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

実行時は下記のように3つの引数を指定します。

./multiresize.exe cat.jpeg 2 2

指定する引数は下記の3つになります。

  • 第1引数:入力する JPEG ファイルへのパス
  • 第2引数:横方向の拡大率
  • 第3引数:縦方向の拡大率

スポンサーリンク

プログラムの解説

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

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

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

[kanren id=1840]

スレッド数の定義

スレッド数は下記で定義しています。

[codebox title="スレッド数の定義"]
/* スレッドの数 */
#define NUM_THREAD 4

このプログラム例では 4 つのスレッドを生成していますが、マルチスレッドによる高速化の確認では、この 4 を変更し、それぞれでプログラムの処理時間がどのように変化するかの比較を行っています。

スレッド数を変更したい場合は、上記の 4 の部分を変更してください。

スレッドで参照するデータの定義

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

下記がその構造体の定義になります。

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

各メンバの意味はコメントでだいたい分かるかなぁと思います。

各メンバの関係を図で表すと下のようになります(一部省略しています)。

入力画像と拡大後画像の情報はそれぞれ下記の BITMAPDATA_t 構造体のメンバ格納されています。

  • input:入力画像
  • output:拡大後画像

BITMAPDATA_t 構造体は myJpeg.h で定義していますので、どのような構造体であるかを知りたい方はこの myJpeg.h を参照していただければと思います(画像データへのアドレスや画像のサイズなどを格納する構造体になっています)

また、下記の4つはそのスレッドで処理を行う領域(拡大後画像における領域)を示すものになります。

  • startm
  • endm
  • startn
  • endn

これらをスレッドごとに変更することで、それぞれのスレッドで異なる領域を拡大縮小処理できるようになります。

スレッドで参照するデータの設定

先程説明した thread_data 構造体への値の設定は main 関数内の下記で行なっています。

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

特に下記の4つに関しては、4つのスレッドそれぞれに対して異なる値を設定し、4つのスレッドで画像全体を拡大縮小処理できるようにしています。

  • startm
  • endm
  • startn
  • endn

スレッドの生成

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

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

特に第3引数では、スレッドで処理する resize 関数を渡し、第4引数で、そのスレッドに参照させるデータへのポインタを渡しています。

スレッドで処理する関数

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

拡大縮小を行うループ
  /* このスレッドの処理する範囲に対して線形補間で画像拡大 */
  for(n = thdata->startn; n < thdata->endn; n++){
    for(m = thdata->startm; m < thdata->endm; m++){
      /* 拡大縮小処理 */
   }
}

これにより、各スレッドで異なる領域を拡大縮小できるようにしています。

スレッドの同期

下記で pthread_join 関数により同期処理を行っています。

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

これにより全てのスレッドが resize 関数を終了するまでここで待機することになります。

全てのスレッドが resize 関数を終了したということは、画像の全体が拡大縮小できたということですので、このタイミングで JPEG ファイルの出力を行うようにしています。

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

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

スレッド数の変更

スレッド数によりどのように処理時間が変化を確認するために、スレッド数の変更を行いながら処理時間の計測を行なっていきます。

スレッド数は前述の通り下記で変更できますので、ここでスレッド数を 1 〜6 に変更し、各スレッド数における処理時間を計測していきます。

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

スポンサーリンク

処理時間の測り方

上記でプログラムで作成した実行ファイルに対し、サイズが 640 x 427のJPEG 入力ファイルを縦横ともに 30 倍に拡大します。

実行時のスレッド生成前から全スレッド終了後(pthread_join 終了後)までの時間を計測しています。

実行環境

私の 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

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

スレッド数が 1 に時に比べてスレッド数を 24 に変化させることで処理時間がどんどん減少していることが確認できると思います。

つまりマルチスレッドによりプログラムを高速化することに成功していると言えます。

一方で、スレッド数は 5 以上にしても特に処理時間に違いはありません。これはスレッド数 4 の時点で私の PC のコアがフルに使用されていることを意味しています。

スポンサーリンク

同期の効果の確認

ちょっと話はそれますが、プログラムの解説で説明したようにこのプログラムでは pthread_join 関数により同期処理を行っています。

この同期をなくすとどうなるでしょうか?実際に実行してみました!出力画像は下記のようになりました。

同期をなくしたので、各スレッドが終了を待たずに JPEG ファイルの出力が行われるようになります。

なので、各スレッドで途中までだけ拡大縮小処理した結果の JPEG ファイルが出力されてしまいます(絵がある部分は処理が終わった部分、黒色の部分は処理が行われていない部分)。

この画像を見ると、各スレッドの処理の進み具合に差があることが確認できますね。

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

まとめ

このページではマルチスレッドにより画像の拡大縮小を行うときの考え方やプログラムの説明を行いました。

画像全体の拡大縮小を画像の一部分のみの拡大縮小という小さな仕事に分割することで、処理が並列実行され高速化できたことを確認できたと思います。

特に最近だと CPU に複数のコアが搭載されていることが当たり前になっていますので、マルチスレッドにより簡単にプログラムを高速化することができます。

今回は画像の拡大縮小のマルチスレッド化を紹介しましたが、ループ処理であれば今回紹介した考え方を応用することでマルチスレッド化することができると思います!

皆さんもいろんなプログラムのマルチスレッド化、さらにはマルチスレッド化による高速化に挑戦してみてください!

コメントを残す

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