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

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

線形補間による画像の拡大縮小

画像の拡大縮小には様々なアルゴリズムがあります。ここで説明するのは「線形補間法」です。

下記記事で説明しているように拡大縮小では画素と画素の間の距離を変化させ、必要になる画素を補間する処理です。

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

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

x = 1 / H × X

y = 1 / V × Y

この計算により求められる座標(x, y)の画素が元画像に存在しない場合、つまり x や y が整数でない場合に補間処理を行い存在しない画素を補って拡大縮小処理を行います。このページではこの補間処理を線形補間による補間で行います。

線形補間では、ある画素とある画素の間の輝度値は「均等に変化しているだろう」と予測して画素を補間します。画像処理において用いる線形補間は二次の線形補間が必要になります(バイリニア法とも言います)。

1次元の線形補間

いきなり二次元だと難しいので、一次の線形補間から解説していきます。下の図の x=x’ の時の f(x) をどうやって求めるかを考えてます。

 

 

色々考え方はあると思います。例えば別ページで紹介している最近傍補間だと一番近い点と同じ値と考えますので、f(x’) = f(x1) となります。

線形補間では、上述のように、点と点の間は「均等に変化しているだろう」と予測します。均等に変化しているのであれば下の図のように f(x0) と f(x1) とを直線で結べば、その直線上に f(x’) が存在すると考えられます。

ここで下の図のように dx = (x’ – x0) とすると、

dx とこの直線の傾きから f(x’) を下の式で計算することができます。

f(x’) = f(x0) + dx * (f(x1) – f(x0)) / (x1 – x0)

さらに、x1 と x0 は隣同士の座標であると考えると1しか異ならないため、x1 – x0 = 1 となり、さらに下記の式のように簡略化することができます。

f(x’) = f(x0) * (1 – dx) + f(x1) * dx

このようにして、元々存在する点から存在しない点を保管するのが1次元の線形補間です

2次元の線形補間(バイリニア)

基本的な考え方は1次元と同じです。下の図における f(x’, y’) を求めてみましょう。実際の画像処理では、f(x’, y’) は (x,’, y’) 座標の画素の輝度値(画素値)と考えられます。

y方向に対する線形補間

1次元の線形補間を2段階に分けて行うことで2次元の線形補間を行うことができます。まずは y 方向に対して1次元の線形補間を行ってみましょう。

これにより (x0, y’) の座標の値 f(x0, y’) と、(x1, y’) の座標の値 f(x1, y’) が求まります。

f(x0, y’)の求め方

f(x0, y0)とf(x0, y1)との間に存在する画素f(x0, y’)は1次元線形補間の考え方を適用するとf(x0, y0)とf(x0, y1)を用いて下式のように表す事ができます。

f(x0, y’) = f(x0, y0) + dy * (f(x0, y1) – f(x0, y0)) / (y1 – y0)

(x0, y0)と(x0, y1)座標は隣り合う画素と考えれば、y1 – y0 = 1 となりますので、

f(x0, y’) = f(x0, y0) * (1 – dy) + f(x0, y1) * dy

となります。

f(x1, y’)の求め方

こちらも同様の考え方で、f(x1, y0)とfx(1, y1)を用いて求める事ができます。

f(x1, y’) = f(x1, y0) * (1 – dy) + f(x1, y1) * dy

x方向に対する線形補間

次は先ほど求めた f(x0, y’) と f(x1, y’) を用いて x 方向に対して線形補間を行って f(x’ y’ )を求めます。

f(x’, y’)の求め方

f(x’, y’) は同じ y’ 上の (x0, y’) と (x1, y’) との間に存在しますので、こちらも1次元線形補間の考え方を用いて下式のように表す事ができます。

f(x’, y’) = f(x0, y’) + dx * (f(x1, y’) – f(x0, y’)) / (x1 – x0)

(x0, y0)と(x1, y0)座標は隣り合う画素なので、x1 – x0 = 1 となります。したがって

f(x’, y’) = f(x0, y’) * (1 – dx) +  f(x1, y’) * dx

のように式を簡略化できます。さらに、y方向の線形補間で求めた f(x0, y’) と f(x1, y’) を上式に代入して式を整理すると、

f(x’, y’) = 

 f(x0, y0) * (1 – dx) * (1 – dy) +

 f(x0, y1) * (1 – dx) * dy +

 f(x1, y0) *  dx * (1 – dy) +

 f(x1, y1) *  dx *  dy

となります。

2次元の線形補間の考え方まとめ

つまり、画像処理で線形補間を行う時の処理は

  • 追加する画素 (x’, y’) の周囲4画素の座標を取得する(x0, x1, y0, y1を求める)
  • 追加する画素と周囲画素との距離を求める(x’ と x0 から dx を、y’ と y0 から dy を求める)
  • 各画素の輝度値を下記式で重み付けをして足し合わして (x’ y’) の画素の輝度値を求める

となります。式で表すと下記の通りです。

 f(x’, y’) = 

  f(x0, y0) * (1 – dx) * (1 – dy) +

  f(x0, y1) * (1 – dx) * dy +

  f(x1, y0) *  dx * (1 – dy) +

  f(x1, y1) *  dx *  dy

プログラム例

上の説明で用いた x が m に、y が n に相当しています。

main.c
#include "myJpeg.h"

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

  RAWDATA_t raw, scaledRaw;
  int m, n, c;
  int m0, m1, n0, n1;
  double originalm, originaln;
  double dm, dn;
  double scaleW, scaleH;
  char outname[256];

  FILE *fo;

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

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

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

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

        originalm = (double)m / (double)scaleW;
        m0 = (int)originalm;
        dm = originalm - m0;
        m1 = m0 + 1;
        if(m1 == raw.width) m1 = raw.width - 1;

        originaln = (double)n / (double)scaleH;
        n0 = (int)originaln;
        dn = originaln - n0;
        n1 = n0 + 1;
        if(n1 == raw.height) n1 = raw.height - 1;

        scaledRaw.data[scaledRaw.ch * (m + n * scaledRaw.width) + c]
          = raw.data[raw.ch * (m1 + n1 * raw.width) + c] * dm * dn
          + raw.data[raw.ch * (m1 + n0 * raw.width) + c] * dm * (1 - dn)
          + raw.data[raw.ch * (m0 + n1 * raw.width) + c] * (1- dm) * dn
          + raw.data[raw.ch * (m0 + n0 * raw.width) + c] * (1 -dm) * (1 - dn);
      }
    }
  }
  /* ここまで画像処理 */

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

  if(jpegFileEncodeWrite(&scaledRaw, outname) == -1){
    printf("jpegFileEncodeWrite error\n");
    freeRawData(&raw);
    return -1;
  }

  freeRawData(&raw);

  return 0;
}

スポンサーリンク

プログラムの説明

拡大縮小以外の処理

拡大縮小以外の処理は下記記事の最近傍補間処理と全く同じです。

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

拡大縮小処理

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

  for(n = 0; n < scaledRaw.height; n++){
    for(m = 0; m < scaledRaw.width; m++){
      for(c = 0; c < scaledRaw.ch; c++){

ループ内では拡大縮小で必要になった画素(originalm, originaln)に対して周辺画素を用いて線形補間を行っています。プログラムの各変数と線形補間の考え方の図を照らし合わせて考えるとわかりやすいと思います。

(originalm, originaln)の周辺画素は下記で求めています。

        originalm = (double)m / (double)scaleW;
        m0 = (int)originalm;
        dm = originalm - m0;
        m1 = m0 + 1;
        if(m1 == raw.width) m1 = raw.width - 1;

        originaln = (double)n / (double)scaleH;
        n0 = (int)originaln;
        dn = originaln - n0;
        n1 = n0 + 1;
        if(n1 == raw.height) n1 = raw.height - 1;

ここで求めた周辺画素から下記で追加画素の輝度値を求め、代入しています。

        scaledRaw.data[scaledRaw.ch * (m + n * scaledRaw.width) + c]
          = raw.data[raw.ch * (m1 + n1 * raw.width) + c] * dm * dn
          + raw.data[raw.ch * (m1 + n0 * raw.width) + c] * dm * (1 - dn)
          + raw.data[raw.ch * (m0 + n1 * raw.width) + c] * (1- dm) * dn
          + raw.data[raw.ch * (m0 + n0 * raw.width) + c] * (1 -dm) * (1 - dn);

上記は1つの画素に対する処理であり、あとはこれを拡大縮小後の全画素に対して実行すれば良いだけです。

画像処理前後の画像

・画像処理前

・画像処理後(横幅方向を0.5倍・高さ方向を0.7倍に変倍した画像)

まとめ

線形補間法では、ある画素とある画素の間の輝度値は「均等に変化しているだろう」と予測して画素を追加していく補間法となります。一番ポピュラーな補間法ですので、最近傍補間法と合わせて原理を覚えておきましょう。

最近傍補間(ニアレストネイバー補間)については下のページで紹介していますので、興味があれば読んでみてください。

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

コメントを残す

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