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

このページでは、最近傍補間を用いた画像の拡大縮小法についての説明と、そのプログラムの例の紹介を行います。

補間とは

まずは簡単に補間について解説しておきます。

補間とは、「補間」という字が表しているように、間を補うことを言います。

特に、存在しないデータを補う時にこの補間が利用されます。

間を…?補う…?
最初は慣れない考え方かもしれないね…

ちょっと具体例で考えてみようか!

例えば7日間体重を計測する必要があったのに、4日目と5日目の体重計測を忘れてしまったとしましょう。つまり、4日目と5日目のデータは存在しないことになります。

他の日の体重計測結果が下記のような時、? で表した4日目と5日目の体重はいくつだったと考えられるでしょう?

50.5, 51.5, 51.0, ?, ?, 52.5, 52.0

ちなみにデータが存在していないのだから正解はないです。自由に考えてみましょう!

うーん…

51.552.0 かな!

3日目と6日目の間の体重が同じ分だけ増加してるって考えてみた!

素晴らしい!

まさにこの存在しないデータを補うことが補間だよ!

難しく考えずに単純に存在しないデータを推測するって考えていいよ

まさに ↑ の会話の中で行われていることが補間です。存在しないデータを推測して補うことが「補間」です。

おそらく他の体重でデータを補間した人もいると思います。それも間違いではありません。単純に補間の実現方法が違うだけです。

補間の実現方法はたくさん考えられていて、その実現方法をアルゴリズムと呼びます。

ちなみに ↑ の会話で行なった補間は線形補間と呼ばれます。

最近傍補間とは

で、最近傍補間とは補間アルゴリズムの1つになります。ニアレストネイバー補間とも呼ばれます。

最近傍補間とは「あるデータを補間する場合、その補間するデータに “一番近いデータと同じである” と仮定して補間する」アルゴリズムになります。

例えば先程の体重の例であれば、4日目の体重は3日目、5日目の体重は6日目と同じ体重であったと仮定して補間します。

したがって、最近傍補間により補間を行えば下記のような計測結果であったという結果が得られます。

50.5, 51.5, 51.0, 51.0, 52.5, 52.5, 52.0

単純だね!

僕が補間した体重の方が合ってそうだけど!

単純だから最初に補間について学ぶにはうってつけだよ!

プログラミングもしやすい!

この最近傍補間は単純でプログラミングもしやすいです。最初に補間を学ぶ上ではこの最近傍補間はまさにうってつけです。

また必要な計算も少ないため、高速な処理も行うこともできます。

スポンサーリンク

最近傍補間による画像の拡大縮小

では、いよいよ本題の最近傍補間による画像の拡大縮小について解説していきたいと思います。

画像の拡大縮小

画像の拡大縮小の基本的な考え方は下記ページで解説していますので、まだ読んでいない方は是非読んでみてください。ここからの解説がより理解しやすくなると思います。

画像の拡大縮小・リサイズの原理、アルゴリズムによる違いを解説!

↑ のページに記載の通り、横方向に H 倍、縦方向に V 倍拡大縮小する場合、拡大縮小後画像の座標(X, Y)と元画像の座標(x, y)の関係は下記式で表すことができます。

拡大縮小前後の座標の関係
x = 1 / H * X
y = 1 / V * Y

これはつまり、全座標 (X, Y) の画素に上式で求まる元画像の座標 (x, y) の画素をコピーしてやれば、拡大縮小後の画像を作成することができることになります。

画像拡大縮小時の最近傍補間

ただし、上式の計算で求められる座標(x, y)の画素が元画像に存在しない場合があります。これは具体的には xy が整数でない場合です。

つまり、画像の拡大縮小では、存在しない画素をコピーする必要があるということです。

存在しないデータをコピーするとか無理でしょ!
いや、さっき説明した「補間」を使えばできるよ!

あ!なるほど

存在しない画素をコピーする場合、元画像の座標 (x, y) の存在しない画素を推測し、その推測値で補って拡大縮小処理を行います。この補う処理が補間処理です。

このページでは、この補間処理を前述で解説した「最近傍補間」により行います。

画像の拡大縮小においては、補う画素の輝度値を元画像の中で一番近い画素の輝度値とする方法で補間を行います。

存在しない画素を最近傍補間で補う様子

補間したい画素の座標が (x, y) であるとき、元画像に存在する画素の中で一番近い画素の座標 (x0, y0) は下記で求めることができます。

最近傍の座標の算出
x0 = (int)(x + 0.5) /* xはdouble型、xはint型 */
y0 = (int)(y + 0.5) /* yはdouble型、yはint型 */

double 型の変数を int 型へキャストすれば小数点以下が切り捨てされます。

ですので 0.5 を足してから int 型にキャストすれば小数点以下を四捨五入した結果が得られることになり、x に一番近い整数 x0y に一番近い整数 y0 を求める事ができます。

スポンサーリンク

最近傍補間を用いた画像の拡大縮小の処理の流れ

ここまでの解説のまとめとして、最近傍補間を用いた画像の拡大縮小の処理の流れを解説しておきます。

最近傍補間を用いた画像の拡大縮小は、拡大縮小後画像の全座標 (X, Y) に対して下記を行うことで実現することができます。

拡大縮小前画像の座標を求める

まずは下記式で拡大縮小後画像の座標  (X, Y) に対応する拡大縮小前画像における座標 (x, y) を求めます

拡大縮小前後の座標の関係
x = 1 / H * X
y = 1 / V * Y

最近傍の座標を求める

続いて下記により座標 (x, y) に一番近い座標 (x0, y0) を求めます。

最近傍の座標の算出
x0 = (int)(x + 0.5) /* xはdouble型、xはint型 */
y0 = (int)(y + 0.5) /* yはdouble型、yはint型 */

最近傍の画素から最近傍補間を行う

さらに、下記の式により最近傍の画素から最近傍補間を行い、座標 (x, y) の画素を求めます。

実際には画素は RGB 等の複数の色の輝度値から構成されますので、下記の式で色ごとに最近傍補間を行う必要があります。

座標 (x,y) の画素を求める計算式
a[x][y] =  a[x0][y0]

この求めた a[x][y] が拡大縮小後画像の座標 (X, Y) の画素になります。

つまり、この a[x][y] を拡大縮小後画像の画素を格納する配列などにコピーすることで、1画素分の処理が完了したことになります。

最近傍補間による画像の拡大縮小プログラム

ではここまで解説してきた「最近傍補間による画像の拡大縮小」をC言語でプログラミングするとどのようになるかについて解説していきたいと思います。

ソースコード

最近傍補間による画像の拡大縮小を行うプログラムのソースコード例は下記のようになります。

main.c
#include "myJpeg.h"

int main(int argc, char *argv[]){

  BITMAPDATA_t bitmap, scaledBitmap;
  int m, n, c;
  int m0, n0;
  double originalm, originaln;
  double scaleW, scaleH;
  char outname[256];

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

  scaleW = atof(argv[2]);
  scaleH = atof(argv[3]);

  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;
  }

  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 < scaledBitmap.height; n++){
    for(m = 0; m < scaledBitmap.width; m++){
      for(c = 0; c < scaledBitmap.ch; c++){

        originalm = (double)m / scaleW;
        m0 = (int)(originalm + 0.5); /* 一番近い座標を求める */
        if(m0 == bitmap.width) m0 = bitmap.width - 1;

        originaln = (double)n / scaleH;
        n0 = (int)(originaln + 0.5); /* 一番近い座標を求める */
        if(n0 == bitmap.height) n0 = bitmap.height - 1;

        scaledBitmap.data[scaledBitmap.ch * (m + n * scaledBitmap.width) + c]
          = bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];
      }
    }
  }
   /* ここまで最近傍補間による拡大縮小 */

  sprintf(outname, "%s", "neighbor.jpeg");

  if(jpegFileEncodeWrite(&scaledBitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&scaledBitmap);
    freeBitmapData(&bitmap);
    return -1;
  }

  freeBitmapData(&scaledBitmap);
  freeBitmapData(&bitmap);

  return 0;
}

ここまで解説を読んでくださった方であれば、変数名を下記のように対応づけて読むと分かりやすいかと思います。

  • m, n:X, Y
  • originalm, originaln:x, y
  • m0, n0:x0, y0
  • scaleW, scaleH:H, V

スポンサーリンク

コンパイル

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

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

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

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

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

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

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

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

> gcc myJpeg.c -c
> gcc main.c -c
> gcc myJpeg.o main.o -ljpeg -o main.exe

-ljpeg を付けることで libjpeg ライブラリをリンクしています。

実行

プログラムの実行は、コンパイルで生成した実行可能ファイル(main.exe)を下記のように3つの引数を指定して実行します。

./main.exe cat.jpeg 2 2

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

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

実行すると、拡大縮小後の画像が neibor.jpeg という名前の JPEG ファイルとして保存されます。

プログラムの説明

紹介したソースコードでどのようなことを行なっているかをポイントを絞って説明していきたいと思います。

拡大縮小率の取得

プログラム実行時に下記の3つの引数を受け取れるようにしています。

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

引数で渡された拡大縮小率は文字列のため、下記で浮動小数点型に変換しています。

拡大縮小率の取得
  scaleW = atof(argv[2]);
  scaleH = atof(argv[3]);

入力 JPEG 画像の読み込みとデコード

下記で jpegFileReadDecode 関数を実行し、引数で渡されたパス JPEG ファイルを読み込みとデコードを行なっています。

JPEG ファイルから BITMAP データ取得
  if(jpegFileReadDecode(&bitmap, argv[1]) == -1){
    printf("jpegFileReadDecode error\n");
    return -1;
  }

デコード後の BITMAP データは BITMAPDATA_t 型の変数 bitmap のメンバである data ポインタが指すことになります。

この data ポインタが指すアドレスのデータを拡大縮小することになります。

また、変数 bitmap の各メンバには jpegFileReadDecode 関数内で読み込んだ JPEG 画像(元画像)の情報(幅や高さなど)が格納されます。

この jpegFileReadDecode 関数や BITMAPDATA_t 構造体については下記ページで説明していますので必要に応じて参照してください。

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

拡大縮小後の画像データのメモリ領域確保

下記で、拡大縮小後画像の幅と高さを計算しています。

拡大縮小後のサイズ計算
  scaledBitmap.width = scaleW * bitmap.width;
  scaledBitmap.height = scaleH * bitmap.height;

単純に元画像(読み込んだ JPEG 画像)の幅・高さと引数で指定された拡大縮小率の掛け算を行なっているだけです。

さらに下記で、拡大縮小後画像を格納するのに必要なメモリを malloc 関数により取得し、scaledBitmap.data ポインタにそのメモリの先頭アドレスを指させています。

メモリの取得
  scaledBitmap.data = (unsigned char*)malloc(sizeof(unsigned char) * scaledBitmap.width * scaledBitmap.height * scaledBitmap.ch);

拡大縮小後画像を格納するのに必要なメモリのサイズは「拡大縮小後画像の画素数 * 1画素あたりのサイズ」から計算することができます。

拡大縮小処理の際には、拡大縮小後の各画素のデータを、scaledBitmap.data が指すメモリに格納していきます。

拡大縮小処理

ここからいよいよ拡大縮小を行なっていきます。

拡大縮小処理は、拡大縮小後の全画素&全色に対して補間処理を行なっていきますので、下記のループで繰り返し処理を行います。

全画素&全色に対するループ
  for(n = 0; n < scaledBitmap.height; n++){
    for(m = 0; m < scaledBitmap.width; m++){
      for(c = 0; c < scaledBitmap.ch; c++){

下記では拡大縮小後の座標(m, n)が、元画像(拡大縮小前の画像)ではどの座標 (originalm, originaln) にあたるかを計算しています。

拡大前画像の座標の計算
       originalm = (double)m / scaleW;
        m0 = (int)(originalm + 0.5); /* 一番近い座標を求める */
        if(m0 == bitmap.width) m0 = bitmap.width - 1;

        originaln = (double)n / scaleH;
        n0 = (int)(originaln + 0.5); /* 一番近い座標を求める */
        if(n0 == bitmap.height) n0 = bitmap.height - 1;

originalmoriginaln は純粋に拡大縮小率と拡大後画像の座標 (m, n) から計算した元画像上の座標になります。

さらに、originalmoriginaln の小数点未満を四捨五入し、元画像に画素が存在する&座標 (originalm, originaln) に一番近い 座標 (m0, n0) を求めています。

これにより、拡大縮小後の座標(m, n)へコピーを行うべき元画像の座標 (m0, n0) が決まりましたので、下記でコピーを行なっています。

最近傍補間した画素の輝度値をコピー
scaledBitmap.data[scaledBitmap.ch * (m + n * scaledBitmap.width) + c]
          = bitmap.data[bitmap.ch * (m0 + n0 * bitmap.width) + c];

c に対するループの中で上記を実行するため、各色の輝度値がコピーされる、つまり画素がコピーされることになります。

あとはこれを拡大縮小後の全画素に対して実行すれば良いだけです。

ちょっとここで注意が必要なのは画像データを2次元データではなく1次元データとして扱っているところです。

1次元データとして扱うために上記のように添字部分の計算がちょっと複雑になっています。

2次元データを1次元データとして扱う方法は下記ページの1次元データで解説していますので詳しく知りたい方は是非読んでみてください!

二次元データの扱い方を解説するページのアイキャッチC言語で2次元データをいろいろな方法で扱ってみる(二次元配列・ポインタのポインタなど)

拡大縮小後画像データのJPEGエンコードとファイル作成

下記で jpegFileEncodeWrite 関数を実行し、拡大縮小後の画像を JPEG ファイルとして保存しています。

JPEG保存
  if(jpegFileEncodeWrite(&scaledBitmap, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeBitmapData(&bitmap);
    return -1;
  }

この jpegFileEncodeWrite 関数についても下記ページで説明していますので必要に応じて参照してください。

libjpegの使い方解説ページアイキャッチ【C言語】libjpegのインストールと使用方法・使用例

スポンサーリンク

画像処理前後の画像

最後に紹介したプログラムを実行することでどのような結果が得られるのかを紹介しておきます。

プログラム実行時の第1引数に指定する JPEG ファイルは下図のようなものにしたいと思います。

さらに、横方向の拡大縮小率を 0.5、縦方向の拡大縮小率を 0.3 として引数に指定した場合、下図のように拡大縮小後の画像が得られます。

まとめ

このページでは、最近傍補間による画像の拡大縮小について解説し、そのプログラムの紹介を行いました!

最近傍補間法では、「追加したい画素」を「その画素に一番近い画素と同じ」と仮定して画素を補う補間法となります。

このように、存在しないデータを補間で補う処理は特に画像処理では頻繁に行われます。

この補間や、一番単純な補間法である最近傍補間法の考え方はしっかり覚えておきましょう。

例えば下記ページでは、このページで紹介した最近傍補間法を使った画像の回転を紹介しています。

画像の回転の解説ページアイキャッチC言語で画像を回転

また、最も有名な補間方法である線形補間(バイリニア補間)についても下のページで紹介していますので、興味があれば読んでみてください。

C言語で画像の拡大縮小(線形補間編)

コメントを残す

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